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
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ What the verbs cover:
| **dashboard** | Opens a read-only local web dashboard (`127.0.0.1:7431`, localhost-only, never detaches) with seven tabs: **Overview · Hosts & Routing · Providers · Runtime · Intelligence · Usage · Live**. The first five render `ak status` health and routing; Usage indexes local Claude/Codex/OpenCode transcripts on demand and can display a separately cached OpenRouter account-analytics block. That block never changes local transcript/session/host totals; the dashboard itself never fetches it. Live groups work by project, then host-branded root sessions with nested agent/worker threads and independently evidenced provider/model metadata, and pairs an interactive agent/tool execution canvas with a rich, server-masked transcript stream. Active sessions can be followed live or reviewed with synchronized play/pause/seek; completed sessions remain available for bounded playback. Live contains no chat or control plane. Ruflo, agentic-qe, and dual-run stores are not auto-discovered; register each trusted structured JSONL file with repeatable `--live-source 'surface=path'` (`surface` is `ruflo`, `aqe`, or `dual-run`). The page is self-contained and offline-first (no internet fetches; local files and loopback subprocesses/endpoints only). A fresh **per-session token** is minted at startup and required by every `/api/*` route (`x-dash-token` header; the launch URL carries it in the `#` fragment, same contract as `ak admin` — ADR-0007, ADR-0014) — this page serves full transcript text, so it is gated the same way admin already gated GitHub/npm stats. See [Live Sessions](docs/LIVE-SESSIONS.md) for coverage, syntax, and privacy limits. **Auto-opens your browser** (`--no-open` for headless/SSH); `--port N` changes the port; tabs deep-link (`#live`) and persist. Stop with Ctrl-C. (Also available as `ak x dashboard`.) |
| **usage** | Reads provider-account analytics from local cache (`ak usage status`) or performs one explicit OpenRouter management-API refresh (`ak usage refresh openrouter`). Refresh requires `OPENROUTER_MANAGEMENT_KEY`, writes a credential-free mode-`0600` cache, and discards endpoint/user/key/session identifiers. `status` and dashboard reads make no network request. OpenRouter account rows have no grounded host/session/project correlation and are never merged into transcript totals. |
| **admin** | Opens the **maintainer admin** (`127.0.0.1:7432`, localhost-only, foreground) — the project-telemetry sibling of `dashboard`, with the same dark/light visual theme and persisted theme preference: unique repo visitors and cloners (GitHub traffic API, needs a push-access token via `GITHUB_TOKEN`/`GH_TOKEN`/`gh auth token` — panels degrade honestly without one), contributors and watchers, npm download momentum (last 7d vs prior 7d, sparklines — shown as trend only, never an absolute reach number, since mirrors/CI inflate the raw count), latest CI run status and open Dependabot alerts, a **"since you last looked"** delta strip over a local baseline, open issues/PRs from others (oldest first), and external humans ranked by recency (bots excluded). Access is gated by a **per-session token** carried in the URL fragment and sent header-only; the page makes **zero external fetches** (the server proxies GitHub/npm; your credential never reaches the page or the payload — ADR-0007, ADR-0013). Where `dashboard` is offline-first, `admin` does deliberate GitHub/npm egress — that contract split is why they're siblings, not tabs. `--port N`, `--no-open`; Ctrl-C stops. (Also available as `ak x admin`.) |
| **run** | **Canonical execution surface.** Executes the template vocabulary through host-neutral supervised adapters. It accepts an explicit OpenCode route (persisted or `--route`) alongside Claude/Codex; `--dry-run` prints the exact plan (with each worker's escalation ladder); `--escalate` advances a failed worker one rung of its route's ladder per attempt (bounded by the ladder; permission/consent and uncertain results are never escalated — ADR-0019). An OpenCode worker runs an isolated loopback server with ephemeral basic authentication, returns only normalized observed facts, and aborts instead of approving a permission request. `ak run` does not turn OpenCode into an AQE provider or primary host. |
| **run** | **Canonical execution surface.** Executes the template vocabulary through host-neutral supervised adapters. It accepts an explicit OpenCode route (persisted or `--route`) alongside Claude/Codex; `--dry-run` prints the exact static plan (with each worker's escalation ladder); at runtime, successful dependencies pass runtime-only, sanitized handoffs capped at 2 KiB each/8 KiB fan-in, never exposed in public JSON. A handoff may cross hosts/vendors and must exclude secrets, credentials, raw logs, and transcript excerpts. `--escalate` advances a failed worker one rung of its route's ladder per attempt (bounded by the ladder; permission/consent and uncertain results are never escalated — ADR-0019). `--timeout` is one absolute readiness→prepare→launch→observe budget per attempt, while separately bounded teardown proves whether resources terminated. An OpenCode worker runs an isolated loopback server with ephemeral basic authentication, returns only normalized observed facts, and aborts instead of approving a permission request. `ak run` does not turn OpenCode into an AQE provider or primary host. |
| **dual** | **Deprecated compatibility wrapper** for existing Claude+Codex `claude-flow-codex` scripts, including its legacy escalation behavior. New execution work should use `ak run`; `ak dual` warns on stderr and will be removed before the stable release. |
| **host** | Canonical alpha namespace for execution-host status, selection, primary-host choice, activity routing, and reversible teardown: `ak host status\|pick\|refresh\|off`. The plumbing spelling is `ak x host`. `ak provider` and `ak x provider` are deprecated compatibility aliases that warn on stderr and will be removed before the stable release. |
| **uninstall** | Removes the kit's footprint (and any legacy shell-kit install); project data is never touched; `--purge` also offers to remove the global packages. |
Expand Down
55 changes: 53 additions & 2 deletions docs/adr/0018-generalized-host-worker-execution.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
- **Status:** Accepted
- **Date:** 2026-07-29
- **Updated:** 2026-07-30
- **Update note:** Hardened trusted-policy validation, adapter invariants, plan guards, and bounded worker teardown.
- **Update note:** Added private bounded dependency handoffs and one absolute lifecycle deadline per attempt.
- **Deciders:** agentic-kit maintainers

## Context
Expand Down Expand Up @@ -34,7 +34,7 @@ permission-response contract required for a routable worker. [OpenCode CLI docum
2. Introduce an agentic-kit-owned, host-neutral execution contract:

```text
readiness → prepare → launch → observe → interpret → cancel → cleanup
readiness → prepare → launch → observe → interpret → summarize → cancel → cleanup
```

Every terminal result contains host, activity, configured selector, correlation,
Expand Down Expand Up @@ -70,6 +70,46 @@ permission-response contract required for a routable worker. [OpenCode CLI docum
conformance evidence. Its routes are accepted by `ak run`, but are never auto-seeded,
AQE-projected, primary-host eligible, or accepted by deprecated `ak dual`.

7. Dependency continuity uses a **runtime-only handoff protocol**. A worker with
dependents must end its final response with one tagged JSON object containing exactly
`outcome`, `artifacts`, `decisions`, and `risks`. Host adapters extract that object only
from the host's structured final assistant surface—Claude's JSON `result`, Codex's final
JSONL `agent_message`, or OpenCode's final assistant text parts. Raw stdout, tool output,
and whole protocol streams are never fallback handoffs.

Each handoff is sanitized and capped at 2 KiB; fan-in is capped at 8 KiB and preserves
the dependent's declared `dependsOn` order rather than completion order. The runner
appends summaries only at runtime inside an explicit untrusted-data/not-instructions
boundary. Materialized/dry-run prompts remain unchanged, and handoffs never enter
`WorkerResult` or `ak run --json`. A handoff may cross host and inference-vendor
boundaries, so its request explicitly forbids secrets, credentials, raw logs, and
transcript excerpts. A missing, duplicate, or malformed required handoff is a bounded,
non-escalatable `protocol_error` and prevents both duplicate side effects and silent
downstream execution. Escalation
retains only the final successful rung's handoff.

8. `timeoutMs` is **one absolute budget per escalation attempt**, created before readiness
and shared through:

```text
readiness → prepare → launch → observe
```

Every phase receives the same `AbortSignal` and only the remaining time; no phase
renews the budget. `readiness` and `prepare` are resource-free. During `launch`, an
adapter progressively registers each acquired resource on runner-owned prepared state
before its next await. A deadline before prepared state exists returns `timed_out`
without fabricated cleanup. A deadline after state exists aborts the operation, calls
`cancel`, and returns `timed_out` only when termination is confirmed; surviving or
uncertain resources are `orphaned`.

Final cleanup remains separately bounded and may extend wall time past the worker
deadline because termination proof is part of the safety contract. Explicit
`orphaned:true` cleanup evidence, or a cleanup exception that leaves termination
unproved, upgrades any apparent success or timeout and is never ignored or downgraded.
On Windows, termination targets the full wrapper/CLI process tree rather than treating
an exited npm PowerShell shim as proof that its descendant stopped.

### The trust boundary (stated, not weakened)

`ak run` executes workers with the **user's own CLI trust posture in the target
Expand Down Expand Up @@ -104,6 +144,11 @@ behalf, and terminal evidence that records what actually ran.
grounded provider evidence is diagnosed, never projected as an invented provider.
- Automatic seeding remains Claude/Codex subscription-only. No OpenCode route is seeded
from unknown or metered provider/billing facts.
- Dependency summaries improve cross-worker continuity without exposing raw host output or
creating a second public result schema. Disk mutations remain shared evidence, not the
only communication channel.
- One attempt cannot consume `timeoutMs` independently in each lifecycle phase. Cleanup can
exceed that budget only to establish whether owned resources actually terminated.

## Implementation evidence

Expand All @@ -117,3 +162,9 @@ behalf, and terminal evidence that records what actually ran.
- Routing-disable behavior, provenance, and cost safety are covered. OpenCode remains excluded
from AQE projection and vendor-diversity claims because a host/model selector is not provider
evidence.
- Handoff fixtures prove strict tag/schema extraction, control sanitization, UTF-8 bounds,
fan-in ordering, injection delimiters, no raw-output fallback, final-success-only escalation,
missing-summary blocking, and public-result privacy.
- Runner fixtures independently stall readiness, prepare, launch, and observe; prove one
shared attempt budget; verify progressive cancellation/cleanup; and upgrade cleanup survivors
to an orphaned terminal result.
5 changes: 4 additions & 1 deletion src/commands/run.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,8 @@ export const help = `ak run — execute a host-neutral activity pipeline

Materializes the managed per-activity routing policy and runs each worker through
its host adapter. OpenCode is accepted only after its routing capability is enabled.
Successful dependency outputs are threaded through runtime-only bounded handoffs at
runtime; dry-run prompts and public JSON results never contain those summaries.

Trust boundary: workers run with YOUR CLI trust posture in the target repo —
its opencode.json / .claude settings / AGENTS.md apply. Run this only in
Expand All @@ -36,7 +38,8 @@ Options:
never escalated)
--dry-run print the host-neutral execution plan only
--max-concurrent <n> max concurrent workers (default 4)
--timeout <ms> per-worker timeout, per attempt (default 120000)
--timeout <ms> one absolute readiness→observe budget per attempt
(default 120000; teardown is separately bounded)
--json emit machine-readable plan/results

Examples:
Expand Down
8 changes: 6 additions & 2 deletions src/lib/exec.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ export async function run(cmd, args = [], opts = {}) {
const { stdout, stderr } = await pexecFile(invocation.command, invocation.args, {
encoding: 'utf8',
timeout: opts.timeout ?? 120_000,
signal: opts.signal,
maxBuffer: 16 * 1024 * 1024,
cwd: opts.cwd,
env,
Expand All @@ -103,9 +104,12 @@ export async function run(cmd, args = [], opts = {}) {
}

/** Is `cmd` invokable? (cross-platform `command -v`) */
export async function have(cmd) {
export async function have(cmd, opts = {}) {
opts.signal?.throwIfAborted?.();
if (isWindows) {
return resolveShim(cmd).resolved;
}
return (await run('which', [cmd])).code === 0;
const present = (await run('which', [cmd], opts)).code === 0;
opts.signal?.throwIfAborted?.();
return present;
}
12 changes: 9 additions & 3 deletions src/lib/execution/claude.mjs
Original file line number Diff line number Diff line change
@@ -1,18 +1,24 @@
import { createSubprocessExecutionAdapter } from './subprocess.mjs';
import { createJsonlSummaryCapture, createSubprocessExecutionAdapter } from './subprocess.mjs';
import { extractHandoff } from './handoff.mjs';

/** Claude Code's documented print/json mode. No permission bypass is passed. */
/** @param {Omit<Parameters<typeof createSubprocessExecutionAdapter>[0], 'id'|'host'|'command'|'argumentsFor'>} [options] */
/** @param {Omit<Parameters<typeof createSubprocessExecutionAdapter>[0], 'id'|'host'|'command'|'argumentsFor'|'summaryFor'|'summaryCaptureFor'>} [options] */
export function createClaudeExecutionAdapter(options = {}) {
return createSubprocessExecutionAdapter({
id: 'claude-print-json', host: 'claude', command: 'claude',
argumentsFor: (worker) => [
'--print', '--output-format', 'json',
'--print', '--output-format', 'stream-json', '--verbose',
...(worker.configuredModel ? ['--model', worker.configuredModel] : []),
// Templates carry per-node turn caps (#88) — honored where the CLI has a
// surface; codex exec and opencode serve have none (documented there).
...(Number.isInteger(worker.maxTurns) && worker.maxTurns > 0 ? ['--max-turns', String(worker.maxTurns)] : []),
worker.prompt,
],
summaryCaptureFor: () => createJsonlSummaryCapture(
(event) => event?.type === 'result' && event?.subtype === 'success' ? event.result : null,
'Claude',
),
summaryFor: (_observation, finalText) => extractHandoff(finalText),
...options,
});
}
Expand Down
12 changes: 10 additions & 2 deletions src/lib/execution/codex.mjs
Original file line number Diff line number Diff line change
@@ -1,9 +1,10 @@
import { createSubprocessExecutionAdapter } from './subprocess.mjs';
import { createJsonlSummaryCapture, createSubprocessExecutionAdapter } from './subprocess.mjs';
import { extractHandoff } from './handoff.mjs';

/** Codex's documented exec/json mode. Its configured sandbox policy is retained.
* worker.maxTurns is deliberately NOT forwarded: codex exec has no turn-cap
* flag (verified against its help) — the bound rides on the runner timeout. */
/** @param {Omit<Parameters<typeof createSubprocessExecutionAdapter>[0], 'id'|'host'|'command'|'argumentsFor'>} [options] */
/** @param {Omit<Parameters<typeof createSubprocessExecutionAdapter>[0], 'id'|'host'|'command'|'argumentsFor'|'summaryFor'|'summaryCaptureFor'>} [options] */
export function createCodexExecutionAdapter(options = {}) {
return createSubprocessExecutionAdapter({
id: 'codex-exec-json', host: 'codex', command: 'codex',
Expand All @@ -12,6 +13,13 @@ export function createCodexExecutionAdapter(options = {}) {
...(worker.configuredModel ? ['--model', worker.configuredModel] : []),
worker.prompt,
],
summaryCaptureFor: () => createJsonlSummaryCapture(
(event) => event?.type === 'item.completed' && event.item?.type === 'agent_message'
? event.item.text
: null,
'Codex',
),
summaryFor: (_observation, finalText) => extractHandoff(finalText),
...options,
});
}
Expand Down
Loading
Loading