Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions bin/agentic-kit.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -144,6 +144,24 @@ async function main() {
allowPositionals: true,
strict: false,
});

// Experimental host-adapter bootstrap (Wave 4, adapter door) — the single
// place every command passes through. Gated on the env var BEFORE anything
// else runs so the default (flag unset) is truly zero calls, zero output,
// zero behavior change: no dynamic import, no config read, nothing.
// Refusals are warnings on stderr, never fatal — a bad external adapter
// must never block a command that doesn't use it.
if (process.env.AK_EXPERIMENTAL_HOST_ADAPTERS === '1') {
try {
const { loadKitConfig } = await import('../src/lib/config.mjs');
const { bootstrapHostAdapters } = await import('../src/lib/adapters/admission.mjs');
const { warnings } = await bootstrapHostAdapters({ cfg: loadKitConfig(), env: process.env });
for (const w of warnings) {
console.error(dim(`⚠ host adapter '${w.name}' not admitted (${w.reason}): ${w.detail ?? ''}`.trimEnd()));
}
} catch { /* experimental surface — never blocks a command */ }
}

const code = await mod.run({ flags: values, positionals, pkgRoot: PKG_ROOT });

// Drift nudge: one line, cached, never blocks (skipped in --json contexts).
Expand Down
24 changes: 24 additions & 0 deletions docs/PROVIDERS.md
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,29 @@ however the endpoint is served. `local-openai` is not an AQE provider type — `
credentials, fragments, or secret-bearing query parameters. See
[ADR-0028](adr/0028-local-openai-compatible-providers.md).

## External host adapters (experimental)

Want `ak` to manage a host CLI it doesn't ship in-tree — driving local models through something
like Hermes, say? Set `AK_EXPERIMENTAL_HOST_ADAPTERS=1` and declare it as **data**, never code:

```json
{
"hostAdapters": [
{ "name": "hermes", "source": "~/.config/ak/adapters/hermes.json", "contract": 1 }
]
}
```

An adapter is a manifest plus a handful of subprocess hooks — nothing an adapter declares ever
runs inside the `ak` process itself. Registering one asks you to confirm a content hash of the
manifest; edit the manifest afterward and that consent is invalidated until you confirm again. A
broken adapter is reported and skipped — it never takes down the hosts that already work.

An external adapter can never claim to be the primary host, an AQE provider, or the status-line
owner; those stay first-party. Nothing here installs itself: you declare the adapter, you consent
to it, and teardown remains reversible. See
[ADR-0029](adr/0029-host-adapter-extension-point.md).

---

## Level 0 — do nothing (the point)
Expand Down Expand Up @@ -368,5 +391,6 @@ just makes the good default automatic and the customization reversible.
- Capability-driven integration axes, bindings, and provenance:
[ADR-0016](adr/0016-capability-driven-integration-adapters.md).
- The generic local OpenAI-compatible provider: [ADR-0028](adr/0028-local-openai-compatible-providers.md).
- External host adapters (experimental): [ADR-0029](adr/0029-host-adapter-extension-point.md).
- Host env flags (`ENABLE_CLAUDE_CODE` / `ENABLE_CODEX`): upstream ruflo
ADR-034, "Optional MCP Backends".
11 changes: 9 additions & 2 deletions docs/adr/0016-capability-driven-integration-adapters.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
# ADR-0016 — Capability-driven host, provider, binding, projection, and observability adapters

- **Status:** Accepted; compatibility clauses superseded by
[ADR-0020](0020-ga-stable-surfaces.md)
[ADR-0020](0020-ga-stable-surfaces.md); closed-registry clause superseded by
[ADR-0029](0029-host-adapter-extension-point.md)
- **Date:** 2026-07-28
- **Updated:** 2026-08-14
- **Updated:** 2026-08-15
- **Update note:** Added read-only Codex plugin-hook compatibility facts,
runtime-selected Ruflo project-memory store proofs, and the non-correlatable
OpenRouter account-analytics boundary; removed the pre-GA compatibility command,
Expand All @@ -18,6 +19,12 @@
warnings (F-16); and the integrations migrator derives each host's native
default provider from the provider registry's host-login entries instead of a
literal map, inferring no binding at all for hosts without one (F-13).
2026-08-15: [ADR-0029](0029-host-adapter-extension-point.md) supersedes §1's
closed-registry requirement — the registry admits an explicitly registered,
hash-pinned, subprocess-only external host adapter behind an experimental
flag; every other property that clause protected (zero-runtime-dependency,
offline-first normal operation, no in-process third-party code) remains
intact.
- **Deciders:** agentic-kit maintainers
- **Related:** [ADR-0001](0001-one-routing-policy-many-projections.md),
[ADR-0003](0003-auto-seed-dual-host-provenance.md),
Expand Down
366 changes: 366 additions & 0 deletions docs/adr/0029-host-adapter-extension-point.md

Large diffs are not rendered by default.

16 changes: 15 additions & 1 deletion docs/adr/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ Consequences**, and cites the grounded source it rests on where relevant.
| [0013](0013-admin-build-security-signals-and-honest-reach.md) | Admin: build/security signals, an honest Reach panel, and a pagination fix | Accepted |
| [0014](0014-dashboard-auth-and-remediation.md) | Dashboard auth token, plus a security/quality remediation pass | Accepted |
| [0015](0015-managed-codex-native-statusline.md) | Manage Codex's native user-wide status line without claiming rich-renderer parity | Accepted |
| [0016](0016-capability-driven-integration-adapters.md) | Capability-driven host, provider, binding, projection, and observability adapters | Accepted; compatibility amended |
| [0016](0016-capability-driven-integration-adapters.md) | Capability-driven host, provider, binding, projection, and observability adapters | Accepted; compatibility amended; closed-registry clause superseded by 0029 |
| [0017](0017-opencode-host.md) | OpenCode as a managed, observable host through native surfaces | Accepted; compatibility amended |
| [0018](0018-generalized-host-worker-execution.md) | Generalized host-worker execution; `ak run` canonical | Accepted; compatibility amended |
| [0019](0019-escalation-in-ak-run.md) | Bounded per-worker escalation in `ak run` | Accepted; historical context closed |
Expand All @@ -37,6 +37,7 @@ Consequences**, and cites the grounded source it rests on where relevant.
| [0026](0026-about-component-directory.md) | About: a component directory that explains everything ak installs | Implemented |
| [0027](0027-shared-project-census.md) | One project census, four scopes, every count explains itself | Implemented |
| [0028](0028-local-openai-compatible-providers.md) | One generic local OpenAI-compatible provider, not a vendor enumeration | Accepted |
| [0029](0029-host-adapter-extension-point.md) | External host adapters: declarative manifest, subprocess hooks | Accepted (experimental contract) |

Theme: ADRs **0001–0006** define **dual-host LLM routing and leadership** — how `ak` lets ruflo route
each development activity (architecture, implementation, testing, review, …) to the right host (Claude
Expand Down Expand Up @@ -199,3 +200,16 @@ transport) and no `aqe` (AQE's provider set is upstream's own enumeration; `olla
`local-openai` deliberately is not). Proposed by community contributor adrianco in PR #131, accepted
with a correction to the PR's quoted Hermes reference config, whose `api_mode: openai` is not a
valid Hermes value.

**0029** answers the same PR #131 with a different mechanism than it proposed. Where the PR asked
for an in-process ESM module loaded by `import()`, this ADR accepts a declarative manifest driving
a fixed set of consented, subprocess-only hooks — no third-party code ever runs inside the `ak`
process, gated behind `AK_EXPERIMENTAL_HOST_ADAPTERS=1`, admitted only after a hash-pinned consent
that invalidates the moment the manifest's content changes. `canBePrimary`, `aqeProvider`, and
`commandStatusline` are not fields the manifest schema accepts at all, so those three obligations
stay first-party by construction rather than by an adapter's own promise. It formally supersedes
ADR-0016's closed-registry clause, folds in the maintainer's full PR #131 gate list (import-time
routable-host invariant, uninstall-through-undo, permission authorization by host, attribution
surfaces policy, and kit.json's unknown-key warning — landed in wave 1 and Phase 0; registry↔directory
test pins remain wave 3), and stays experimental until a real external adapter clears the
conformance kit and a release of soak.
16 changes: 10 additions & 6 deletions src/commands/setup.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { reconcileGuidance } from '../lib/blocks.mjs';
import { register as mcpRegister, applyExclusions } from '../lib/mcp.mjs';
import { reconcileOpencodeGuidance } from '../lib/opencode.mjs';
import { runLifecycle } from '../lib/adapters/lifecycle.mjs';
import { hostsWithLifecycle, lifecycleAdapterFor } from '../lib/adapters/lifecycle-registry.mjs';
import { builtinHostsWithLifecycle, lifecycleAdapterFor } from '../lib/adapters/lifecycle-registry.mjs';
import { loadKitConfig, saveKitConfig } from '../lib/config.mjs';
import { HOSTS, applyHosts, applyProviders, hostInstallState, installHost, applyAqeRouter, seedActivityRoutesIfMultiHost, printActivityRoutingTable, aqeSupportsAgentOverrides, ensureCodexMcp, ensureRufloMcpInCodex, applySetupHostFlags, bothHostsEnabled } from '../lib/providers.mjs';
import { installedVersion } from '../lib/versions.mjs';
Expand Down Expand Up @@ -216,14 +216,18 @@ export async function run_machine({ flags, pkgRoot, cfg }) {
// 6b. host lifecycle wiring — config-file MCP + skills, lifecycle plugin,
// converted agents, platform skill (each adapter owns its own surfaces
// — opencode.mjs for opencode). Registry-driven: loops
// hostsWithLifecycle() rather than naming opencode, so a second
// lifecycle host needs no new branch here. Only when the CLI is
// actually present: a declined/failed install must not leave a
// builtinHostsWithLifecycle() rather than naming opencode, so a second
// BUILT-IN lifecycle host needs no new branch here. Only when the CLI
// is actually present: a declined/failed install must not leave a
// freshly-created config home behind (codex-review #4). The result
// SHAPE consumed below (stack.oc/plugin/agents/skill) is still
// opencode's own — the lifecycle contract doesn't mandate a common
// `apply()` result shape across hosts.
for (const hostId of hostsWithLifecycle()) {
// `apply()` result shape across hosts. builtinHostsWithLifecycle()
// (not hostsWithLifecycle()) deliberately excludes admitted external
// hosts: this loop body is opencode-shaped, and external lifecycle
// execution graduates in a later wave alongside a shape-agnostic body
// (see lifecycle-registry.mjs's registerAdmittedLifecycle comment).
for (const hostId of builtinHostsWithLifecycle()) {
if (!cfg.integrations?.hosts?.[hostId]) continue;
if (!(await have(hostId))) {
const pkg = HOSTS.find((h) => h.id === hostId)?.pkg ?? hostId;
Expand Down
18 changes: 10 additions & 8 deletions src/commands/sync.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { fixStatusline, helperStampStale } from '../lib/statusline.mjs';
import { reconcileGuidance } from '../lib/blocks.mjs';
import { register as mcpRegister, applyExclusions } from '../lib/mcp.mjs';
import { runLifecycle } from '../lib/adapters/lifecycle.mjs';
import { hostsWithLifecycle, lifecycleAdapterFor } from '../lib/adapters/lifecycle-registry.mjs';
import { builtinHostsWithLifecycle, lifecycleAdapterFor } from '../lib/adapters/lifecycle-registry.mjs';
import { listDaemons, staleDaemons, reap } from '../lib/daemons.mjs';
import { loadKitConfig, saveKitConfig } from '../lib/config.mjs';
import { commandHosts, applyHosts, applyProviders, hostInstallState, installHost, applyAqeRouter, seedActivityRoutesIfMultiHost, migrateRetiredRoutesInConfig, ensureCodexMcp, ensureRufloMcpInCodex, bothHostsEnabled } from '../lib/providers.mjs';
Expand Down Expand Up @@ -188,13 +188,15 @@ export async function run({ flags, pkgRoot, fetchLatest }) {
// Runs BEFORE the blocks branch: the agents-opencode guidance target is gated
// on the config home this branch creates — this order lets a fresh enable
// converge guidance in the SAME sync (a second sync is then a true no-op).
// Registry-driven: loops hostsWithLifecycle() rather than naming opencode,
// so a second lifecycle host needs no new branch here. Only opencode is
// registered today, so this loop runs exactly once — byte-identical to the
// single-host branch it replaces. The result SHAPE consumed below
// (stack.oc/plugin/agents/skill) is still opencode's own — the lifecycle
// contract doesn't mandate a common `apply()` result shape across hosts.
for (const hostId of hostsWithLifecycle()) {
// Registry-driven: loops builtinHostsWithLifecycle() rather than naming
// opencode, so a second BUILT-IN lifecycle host needs no new branch here.
// Only opencode is registered today, so this loop runs exactly once —
// byte-identical to the single-host branch it replaces. The result SHAPE
// consumed below (stack.oc/plugin/agents/skill) is still opencode's own —
// the lifecycle contract doesn't mandate a common `apply()` result shape
// across hosts. builtinHostsWithLifecycle() (not hostsWithLifecycle())
// deliberately excludes admitted external hosts — see lifecycle-registry.mjs.
for (const hostId of builtinHostsWithLifecycle()) {
if (!subsystems.has(hostId) || !cfg.integrations?.hosts?.[hostId]) continue;
if (!(await have(hostId))) {
info(`${hostId}: enabled but CLI not installed — wiring skipped (hosts step installs it)`);
Expand Down
10 changes: 7 additions & 3 deletions src/commands/uninstall.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { stripBlock, BEGIN, BUILTIN_BLOCKS } from '../lib/blocks.mjs';
import { unregister } from '../lib/mcp.mjs';
import { loadKitConfig, saveKitConfig } from '../lib/config.mjs';
import { runLifecycle } from '../lib/adapters/lifecycle.mjs';
import { hostsWithLifecycle, lifecycleAdapterFor } from '../lib/adapters/lifecycle-registry.mjs';
import { builtinHostsWithLifecycle, lifecycleAdapterFor } from '../lib/adapters/lifecycle-registry.mjs';
import { present as rbPresent } from '../lib/ruvnet-brain.mjs';
import * as paths from '../lib/paths.mjs';
import { ok, warn, info } from '../lib/output.mjs';
Expand Down Expand Up @@ -131,8 +131,12 @@ export async function run({ flags }) {
// artifact removal independent of that), so this call is unconditional per
// host — the only kit-side gate is "did anything actually happen", to
// avoid a no-op teardown line (and a needless kit.json rewrite) on a host
// that was never enabled.
for (const hostId of hostsWithLifecycle()) {
// that was never enabled. builtinHostsWithLifecycle() (not
// hostsWithLifecycle()) deliberately excludes admitted external hosts:
// this loop body destructures an opencode-shaped result (ret.undo,
// ret.artifacts) — see lifecycle-registry.mjs's registerAdmittedLifecycle
// comment for why external lifecycle execution isn't wired through here yet.
for (const hostId of builtinHostsWithLifecycle()) {
const adapter = lifecycleAdapterFor(hostId);
if (dry) {
info(`[dry-run] stripped ak-managed ${hostId} wiring + artifacts (opencode.json, plugin, agents, skill)`);
Expand Down
Loading
Loading