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 @@ -93,7 +93,7 @@ What the verbs cover:
| **sync** | The one convergence verb: upgrades first when a new release exists, then re-heals everything an upgrade wipes, then re-checks and reports. Included in that heal: it **installs any enabled frontier host** (claude/codex/opencode) that's entirely absent — never touching an external (mise/brew/native) install — and **re-applies provider wiring** (the `ENABLE_*` host env, OpenCode's native configuration, the aqe fallback chain, and ruflo API providers) whenever it has drifted — and, on a dual-host project, **seeds/heals the Claude/Codex default routing policy** (materializing eligible routes into agentic-qe's `agentOverrides`, e.g. after an aqe upgrade first makes it eligible). It also **installs/repins the standalone `agentdb` CLI** to ruflo's bundled version (keeping the shared cognitive store coherent) and appends a **health-history snapshot** so `status` can flag regressions across syncs. It also **re-runs the RuvNet Brain installer** to pull the latest release when the on-disk KB has drifted (or installs it if absent, when enabled). It also **self-updates the kit**: when a newer `@pacphi/agentic-kit` exists it installs it as the *last* step (the new code applies from the next `ak` run, never mid-sync). Prerelease installs (`4.0.0-alpha.*`) track the `next` npm dist-tag as well as `latest`, so alphas see their successors; stable installs only ever follow `latest`. `--no-upgrade` skips the self-update along with the package upgrades. |
| **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 transcripts on demand. 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`.) |
| **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. 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 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. |
| **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
2 changes: 1 addition & 1 deletion docs/UPGRADING.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ family on your behalf.

| Migration surface | What to know |
| ------------------- | ----------------------------------------------- |
| `ak dual` → `ak run` | `ak dual` is a deprecated compatibility wrapper — existing scripts keep working (it warns on stderr and will be removed before the stable release); use `ak run` for new execution work. OpenCode routes require the current release — remove them before downgrading. |
| `ak dual` → `ak run` | `ak dual` is a deprecated compatibility wrapper — existing scripts keep working (it warns on stderr and will be removed before the stable release); use `ak run` for new execution work. OpenCode routes require the current release — remove them before downgrading. `--escalate` exists on both, with deliberately different semantics: the wrapper retries the *whole pipeline* once on any failure; `ak run` advances only the *failed worker* one rung of its route's ladder per attempt (ADR-0019) and records the attempt trail in the result. |

A **host** runs the work; a **provider** serves inference. A binding can connect one provider to
several hosts through separate native configuration **projections**, while **observability**
Expand Down
94 changes: 94 additions & 0 deletions docs/adr/0019-escalation-in-ak-run.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
# ADR-0019 — Bounded per-worker escalation in `ak run`

- **Status:** Accepted
- **Date:** 2026-07-29
- **Deciders:** agentic-kit maintainers

## Context

The #76 closure punch list requires that ordered, bounded cross-host escalation reach
the canonical `ak run` path — or be explicitly retired with an approved migration
decision. The legacy behavior lives only in the deprecated `ak dual` wrapper (ADR-0004):
on a non-zero pipeline exit and an explicit `--escalate` flag, the *entire* pipeline
re-runs once with `escalatePolicy(policy)` — every activity bumped to its ladder's first
non-self rung. That shape has three faults the canonical runner should not inherit:

1. **Whole-pipeline retry for a single worker's failure.** A failing `coder` re-ran the
successful `architect` too — spend and wall-clock doubled for no benefit.
2. **No attempt evidence.** The escalated run presented as a fresh run; the first
attempt's failure left no trace in the result contract.
3. **No consent boundary.** A permission-refused or uncertain worker would be retried
like any other failure.

## Decision

Escalation moves into the runner as **bounded per-worker ladder attempts**, opt-in per
invocation via `ak run --escalate`:

1. **The ladder travels with the worker.** `materializeRunPlan` attaches the resolved
route's `escalate` array to each worker. Self-equal rungs are dropped at
materialization (re-running the identical host+model changes nothing — the legacy L4
rule), and every rung must be a routable host or materialization fails exactly the
way an unroutable primary route does.
2. **Ordered and bounded.** A worker whose result is escalatable advances **one rung at
a time**, in ladder order, and stops when a rung succeeds or the ladder exhausts.
There is no unbounded retry: the ladder's length is the bound, and per-worker
`--timeout` applies per attempt.
3. **Escalatable means *cleanly failed*.** `failed` and `timed_out` may advance.
Never advanced: `blocked`/`cancelled` (dependency state, not a worker failure),
`permission_required` (a consent boundary — escalating around it would violate the
supervised-host contract that the OpenCode permission-abort implements), and
`orphaned` (execution state uncertain; a retry risks a double run).
4. **The trail is evidence, not noise.** The final result carries `attempts[]` — each
attempt's host, model, status, exitCategory, durationMs, and (on failure) bounded
reason — but ONLY when more than one attempt ran. A single attempt is
indistinguishable from escalation being off, and emitting a trail there would
fabricate an event that did not happen. The final result's host/model are the rung
that actually executed (observed truth), a succeeded-after-escalation unblocks
dependents normally, and `attempts` is schema-validated like every other result
field.
5. **Explicit opt-in, same as legacy.** Escalation doubles attempted work by design;
it stays behind `--escalate` rather than becoming the default posture.

This satisfies the punch list's "ordered, bounded cross-host escalation into the
canonical path, tested with an OpenCode-qualified route": an
`implementation:opencode` route with a claude ladder rung escalates through the real
OpenCode adapter (`permission_required` from its consent boundary excluded by rule 3).

The legacy `escalatePolicy` + `dual --escalate` stay untouched inside the deprecated
wrapper until #83 removes the wrapper; nothing here changes their behavior.

## Consequences

- A failing worker no longer re-runs successful siblings — escalation cost is scoped to
the failure.
- `ak run --json` results now carry `attempts[]` on escalated workers; consumers see
where a worker started and where it landed, and the consent/uncertainty exclusions
are visible in what is *absent*.
- The rung must have an execution adapter to advance: a ladder naming a host with no
adapter records `cli_unavailable` for that rung and continues to the next.
- The deprecated wrapper's whole-pipeline semantics and the canonical per-worker
semantics differ *deliberately*; the migration note in UPGRADING.md names that as an
intended improvement, not a drift.

## Alternatives considered

- **Port dual's whole-pipeline retry.** Rejected for the three faults above: it wastes
successful work, hides the first attempt, and ignores consent boundaries.
- **Always-on escalation.** Rejected: doubling attempted spend must remain a
per-invocation choice, exactly as it was in the legacy wrapper.
- **Retire escalation entirely** (the punch list's alternative). Rejected: the ladder
data already exists in the routing policy and the per-worker shape is strictly
better than retiring a capability users have.

## References

- `src/lib/execution/runner.mjs` (`executeWorkerWithEscalation`, escalatable rules),
`src/lib/execution/schema.mjs` (`attempts` validation),
`src/lib/routing.mjs` (`materializeRunPlan` ladder attach),
`src/commands/run.mjs` (`--escalate`).
- ADR-0004 (legacy escalation semantics), ADR-0018 (canonical execution contract),
#76 punch list item 1, #83 (deprecated-wrapper removal that consumes this migration).
- Tests: `tests/kit/execution-runner.test.mjs` (engine, boundaries, trail, schema),
`tests/kit/run-command.test.mjs` (materialization, CLI flow), plus the live
OpenCode-qualified smoke recorded on #76.
1 change: 1 addition & 0 deletions docs/adr/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ Consequences**, and cites the grounded source it rests on where relevant.
| [0016](0016-capability-driven-integration-adapters.md) | Capability-driven host, provider, binding, projection, and observability adapters | Accepted |
| [0017](0017-opencode-host.md) | OpenCode as a managed, observable host through native surfaces | Accepted |
| [0018](0018-generalized-host-worker-execution.md) | Generalized host-worker execution; `ak run` canonical | Accepted |
| [0019](0019-escalation-in-ak-run.md) | Bounded per-worker escalation in `ak run` | Accepted |

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
20 changes: 15 additions & 5 deletions src/commands/run.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { DUAL_RUN_TEMPLATE_NAMES, materializeRunPlan, parseRouteSpecs } from '..

export const options = {
route: { type: 'string', multiple: true },
escalate: { type: 'boolean', default: false },
'dry-run': { type: 'boolean', default: false },
'max-concurrent': { type: 'string' },
timeout: { type: 'string' },
Expand All @@ -25,14 +26,19 @@ Templates: ${DUAL_RUN_TEMPLATE_NAMES.join(', ')}

Options:
--route 'act:host[:model]' per-run routing override (repeatable; not persisted)
--escalate on failure, advance the worker one rung of its
route's escalation ladder (bounded by the ladder;
permission/consent and uncertain results are
never escalated)
--dry-run print the host-neutral execution plan only
--max-concurrent <n> max concurrent workers (default 4)
--timeout <ms> per-worker timeout (default 120000)
--timeout <ms> per-worker timeout, per attempt (default 120000)
--json emit machine-readable plan/results

Examples:
ak run feature "add token-bucket rate limiting" --dry-run
ak run security "src/auth/" --route 'security-scan:opencode'`;
ak run security "src/auth/" --route 'security-scan:opencode'
ak run feature "fix the flaky parser" --escalate`;

function positiveInt(value, name) {
if (value === undefined) return undefined;
Expand All @@ -54,14 +60,18 @@ function printPlan(plan) {
console.log(bold(`run: ${plan.template}`));
for (const worker of plan.workers) {
const dependency = worker.dependsOn?.length ? `after ${worker.dependsOn.join(', ')}` : 'start';
console.log(` ${worker.id.padEnd(12)} ${worker.host.padEnd(9)} ${(worker.configuredModel ?? '').padEnd(24)} ${dim(dependency)}`);
const ladder = worker.escalate?.length ? dim(` ↑ ${worker.escalate.map((rung) => rung.host).join('→')}`) : '';
console.log(` ${worker.id.padEnd(12)} ${worker.host.padEnd(9)} ${(worker.configuredModel ?? '').padEnd(24)} ${dim(dependency)}${ladder}`);
}
}

function printResults(results) {
for (const result of results) {
const detail = result.failure?.reason ? ` — ${result.failure.reason}` : '';
console.log(` ${result.workerId.padEnd(12)} ${result.host.padEnd(9)} ${result.status} (${result.exitCategory})${dim(detail)}`);
// The escalation trail is visible, not silent: a success after advancing
// rungs says where it started (ADR-0019).
const trail = result.attempts?.length > 1 ? dim(` (escalated from ${result.attempts[0].host})`) : '';
console.log(` ${result.workerId.padEnd(12)} ${result.host.padEnd(9)} ${result.status} (${result.exitCategory})${trail}${dim(detail)}`);
}
}

Expand Down Expand Up @@ -94,7 +104,7 @@ export async function run({ flags, positionals, executePlan = executeRunPlan, cf
timeoutMs = positiveInt(flags.timeout, 'timeout');
} catch (error) { fail(error.message); return 2; }
if (!flags.json) printPlan(plan);
const results = await executePlan(plan, { maxConcurrent, timeoutMs });
const results = await executePlan(plan, { maxConcurrent, timeoutMs, escalate: !!flags.escalate });
if (flags.json) console.log(JSON.stringify({ plan, results }, null, 2));
else printResults(results);
// The human status line is gated on !json — a trailing "✓ run complete"
Expand Down
Loading
Loading