From d9ffa058a08f8f6796daffbfd9286beb9bf22e23 Mon Sep 17 00:00:00 2001 From: agent-kit-sync Date: Fri, 14 Aug 2026 01:12:39 +0000 Subject: [PATCH] chore: sync private v5.1.0 (fdc9b54) --- .cursor-plugin/plugin.json | 2 +- .cursor/agent-kit.json | 2 +- .cursor/agents/mission-kit-comms.md | 35 + .cursor/commands/git-prod.md | 6 +- .cursor/commands/git-staging.md | 2 +- .cursor/commands/handoff.md | 4 +- .cursor/commands/plan-external-review.md | 9 +- .cursor/commands/run-plan-all.md | 4 +- .cursor/commands/run-plan-loop.md | 2 +- .cursor/commands/run-plan-orchestrated.md | 2 +- .cursor/commands/run-plan.md | 6 +- .cursor/context/config.example.json | 4 +- .../templates/plan-external-review-prompt.md | 6 +- .cursor/rules/cursor-skills-git-workflow.mdc | 4 +- .cursor/rules/hitl-ask-questions.mdc | 2 +- .cursor/scripts/comms-draft.mjs | 134 +++ .cursor/scripts/comms-draft.test.mjs | 73 ++ .../plan-external-review-atomic-wait.test.mjs | 181 ++++ ...n-external-review-backend-cascade.test.mjs | 26 + ...lan-external-review-model-routing.test.mjs | 101 +++ ...lan-external-review-progress-gate.test.mjs | 34 + .cursor/scripts/plan-external-review.sh | 800 +++++++++++++++++- .../community/mission-kit-comms/SKILL.md | 78 ++ .gitignore | 4 + CHANGELOG.md | 32 +- autogit/gitupdate.md | 24 +- dashboard/dashboard.html | 36 +- dashboard/lib/guards.mjs | 58 +- dashboard/start-broadcast.mjs | 3 +- dashboard/start.mjs | 3 +- docs/CONTRIBUTING.md | 15 + docs/DEVELOPMENT.md | 9 +- docs/README.md | 5 +- docs/bootstrap.md | 4 +- docs/capability-inventory.md | 12 +- docs/claude-cli-kit-load.md | 120 +++ docs/coherence-inventory.md | 3 +- docs/comms-channel-map.md | 55 ++ docs/comms-content-calendar.md | 38 + docs/comms-publish-pipeline.md | 52 ++ docs/comms-templates/contributor-ask.md | 30 + docs/comms-templates/recap.md | 33 + docs/comms-templates/release.md | 33 + docs/comms.md | 18 + docs/cursor-3-features.md | 14 +- docs/cursor-native-audit.md | 14 +- docs/external-plan-review.md | 35 +- docs/five-layer-claim-matrix.md | 1 + docs/getting-started.md | 22 +- docs/marketplace.md | 2 +- docs/npm-publish-checklist.md | 6 +- docs/public-launch-announcement.md | 2 +- install.md | 2 +- package.json | 2 +- packages/cli/README.md | 2 + packages/cli/package.json | 2 +- .../cli/src/commands/dashboard-broadcast.ts | 12 +- packages/cli/src/commands/dashboard.test.ts | 21 +- packages/cli/src/commands/dashboard.ts | 25 +- packages/cli/src/commands/doctor.ts | 4 +- packages/cli/src/commands/init.ts | 3 +- packages/cli/src/commands/install.ts | 21 +- packages/cli/src/commands/update.test.ts | 63 ++ packages/cli/src/commands/update.ts | 9 +- packages/cli/src/dashboard/guards.test.ts | 26 +- .../dashboard/plugin-ux-validation.test.ts | 9 +- .../cli/src/generator/claude-kit-load.test.ts | 72 ++ packages/cli/src/generator/claude-kit-load.ts | 83 ++ packages/cli/src/generator/index.ts | 2 + .../cli/src/generator/personalization.test.ts | 7 +- packages/cli/src/generator/personalization.ts | 15 +- .../cli/src/lifecycle/check-updates.test.ts | 57 ++ packages/cli/src/lifecycle/check-updates.ts | 144 +++- .../cli/src/lifecycle/overlay-known-hashes.ts | 13 + .../cli/src/plan-loop/persona-banners.test.ts | 36 +- packages/cli/src/plan-loop/run-loop.ts | 17 +- packages/cli/src/welcome/help-groups.ts | 4 + packages/cli/src/welcome/screen.test.ts | 6 + packages/cli/src/welcome/screen.ts | 51 +- packages/cli/src/welcome/visual-kit.test.ts | 214 +++++ packages/cli/src/welcome/visual-kit.ts | 232 +++++ 81 files changed, 3178 insertions(+), 176 deletions(-) create mode 100644 .cursor/agents/mission-kit-comms.md create mode 100644 .cursor/scripts/comms-draft.mjs create mode 100644 .cursor/scripts/comms-draft.test.mjs create mode 100644 .cursor/scripts/plan-external-review-atomic-wait.test.mjs create mode 100644 .cursor/scripts/plan-external-review-backend-cascade.test.mjs create mode 100644 .cursor/scripts/plan-external-review-model-routing.test.mjs create mode 100644 .cursor/skills/community/mission-kit-comms/SKILL.md create mode 100644 docs/claude-cli-kit-load.md create mode 100644 docs/comms-channel-map.md create mode 100644 docs/comms-content-calendar.md create mode 100644 docs/comms-publish-pipeline.md create mode 100644 docs/comms-templates/contributor-ask.md create mode 100644 docs/comms-templates/recap.md create mode 100644 docs/comms-templates/release.md create mode 100644 docs/comms.md create mode 100644 packages/cli/src/generator/claude-kit-load.test.ts create mode 100644 packages/cli/src/generator/claude-kit-load.ts create mode 100644 packages/cli/src/welcome/visual-kit.test.ts create mode 100644 packages/cli/src/welcome/visual-kit.ts diff --git a/.cursor-plugin/plugin.json b/.cursor-plugin/plugin.json index ac8988a..b3918c7 100644 --- a/.cursor-plugin/plugin.json +++ b/.cursor-plugin/plugin.json @@ -15,7 +15,7 @@ "anti-slop" ], "license": "PolyForm-Noncommercial-1.0.0", - "version": "5.0.0", + "version": "5.1.0", "homepage": "https://github.com/agent-kit-startup/agent-kit", "repository": "https://github.com/agent-kit-startup/agent-kit", "logo": "dashboard/logo.svg", diff --git a/.cursor/agent-kit.json b/.cursor/agent-kit.json index d0f04bc..c6e99ec 100644 --- a/.cursor/agent-kit.json +++ b/.cursor/agent-kit.json @@ -1,6 +1,6 @@ { "schemaVersion": 1, - "version": "5.0.0", + "version": "5.1.0", "protected": [ ".cursor/HANDOFF.md", ".cursor/agents/test-suites.md", diff --git a/.cursor/agents/mission-kit-comms.md b/.cursor/agents/mission-kit-comms.md new file mode 100644 index 0000000..564397f --- /dev/null +++ b/.cursor/agents/mission-kit-comms.md @@ -0,0 +1,35 @@ +--- +name: mission-kit-comms +description: Dedicated Mission Kit comms/community agent. Drafts recap, release, contributor-ask, and reply copy under explicit HITL gates. Not an Agent Persona (personas are chat chrome only). +model: claude-sonnet-4 +readonly: false +--- + +# Mission Kit comms agent + +You draft adoption and contributor copy for **Mission Kit**. You do not post. + +Follow skill `registry/skills/community/mission-kit-comms/SKILL.md` (factory overlay: `.cursor/skills/community/mission-kit-comms/SKILL.md`). Tone: short blocks, one ask per message ([ux-message-flows](../skills/community/ux-message-flows/SKILL.md)). Prompt shape: [prompts-markdown](../skills/community/prompts-markdown/SKILL.md). + +## HITL contract + +- Before any **public post or public reply**, Ask questions with `Post this` / `Edit draft` / `Discard` (chat numbered list if the tool is missing). +- Skip or cancel = stop. Never silent cross-network posting. +- Never `/git-prod`. Never buy ads. Never submit Cursor Marketplace (parked plan owns that). +- Never load Agent Personas (`autopilot` / `night-shift` / `ghost-runner`) as the poster. + +## What you may do + +- Draft from [docs/comms-templates/](../../docs/comms-templates/recap.md) and the [calendar](../../docs/comms-content-calendar.md). +- Run `node .cursor/scripts/comms-draft.mjs` for local files under `.cursor/comms-drafts/`. +- Align contributor asks with [CONTRIBUTING](../../docs/CONTRIBUTING.md), [contribute-upstream](../../docs/contribute-upstream.md), CoC, SUPPORT, SECURITY. + +## What you must not do + +- HTTP post, webhook, or `gh` issue comment that is marketing without Ask. +- Put tokens, webhooks, or cookies in files that can be committed. +- Claim product behavior that is not in CHANGELOG 5.0.0 / getting-started / five-layer matrix. + +## Naming + +Mission Kit = product. Agent Kit = CLI/npm/slash/pack. Mission Control = dashboard. diff --git a/.cursor/commands/git-prod.md b/.cursor/commands/git-prod.md index 22e6d30..b597aff 100644 --- a/.cursor/commands/git-prod.md +++ b/.cursor/commands/git-prod.md @@ -19,7 +19,11 @@ Follow the **git prod** routine to promote `origin/staging` to `origin/main` (pr **Advisory (does not replace confirmation):** if Blocking untriaged `.cursor/memory/plan-monitor-*.md` match themes in the staging→main delta, mention them once in the summary. Never steal this Ask; Field Report / `/plan-review-triage` stay attention/HITL SoT. **Fallback:** if Ask questions tool unavailable, ask for explicit confirmation in chat. -5. Run merge to main, push main, create/push annotated vX.Y.Z tag (when absent), confirm production. +5. Run merge to main, then push with the authorized inline form only: + + `ALLOW_MAIN_PUSH=1 git push origin main` + + Bare `git push origin main` stays denied by `agent-kit guard shell` and `git-hooks/pre-push`. Do not export `ALLOW_MAIN_PUSH=1` as a session environment variable. Do not add `--force`, `--no-verify`, or a non-main destination. Then create/push annotated vX.Y.Z tag (when absent) and confirm production. Details: `autogit/gitupdate.md` Prompt git prod step 9. 6. Update `.cursor/HANDOFF.md` ("promoted to production") and memory-loop WRITE if it applies. 7. In this monorepo: annotated tags trigger `publish-npm` + `sync-public` CI; `pnpm git:trigger-public-sync` fallback when needed per `autogit/gitupdate.md`. 8. **Post-prod verification (mandatory in this monorepo):** before ending, check tag CI jobs (`publish-npm`, `sync-public`), `npm view @dadado/agent-kit-cli version`, **public sync PR merged** (not CI-green alone), public `main` sync commit, and public GitHub Release Latest. Report each row. Silent npm success with a stale public Releases badge, or CI-green with an unmerged sync PR, is a kit failure mode; see `autogit/gitupdate.md` step 12.5. diff --git a/.cursor/commands/git-staging.md b/.cursor/commands/git-staging.md index f5b2d94..7963d2a 100644 --- a/.cursor/commands/git-staging.md +++ b/.cursor/commands/git-staging.md @@ -10,7 +10,7 @@ Follow the **git staging** routine to bring local changes to the pre-production **Runs in the main window by default.** Do not dispatch this command to a Task subagent by default; Task isolation is opt-in and used only when the kit repo wants it. 1. **Read** the "Prompt: git staging" section in `autogit/gitupdate.md` (when it exists). -2. **Staging hygiene (monitors):** if `git status` shows untracked or unrelated dirty `.cursor/memory/plan-monitor-*.md`, **warn** before commit. Stage memory/monitor files **add-by-name only**; never broad `git add` of `.cursor/memory/` WIP into a product commit (ADR `decisions/2026-07-27_plan-monitor-consumer-awareness.md`, external-review staging hygiene). +2. **Staging hygiene (monitors):** if `git status` shows untracked or unrelated dirty `.cursor/memory/plan-monitor-*.md`, **warn** before commit. Stage memory/monitor files **add-by-name only**; never broad `git add` of `.cursor/memory/` WIP into a product commit (ADR `decisions/2026-07-27_plan-monitor-consumer-awareness.md`, external-review staging hygiene). Warn/add-by-name forbids sweeps; it does **not** mean leave soft kit artifacts dirty. Inventory remaining safe dirt into a `docs(memory):` / `chore(kit):` bucket and ship it in this run (SoT: `autogit/gitupdate.md` Prompt git staging §1 and §7). Do not stop-and-quiz solely because a path looks out of the current flow. 3. **Lint evidence (required when code/format paths change):** before claiming staging-ready, **run** the repo formatter/linter on touched files and **record the command + result** (pass/fail) in the worker summary or tick notes. Writing `Staging ready: yes` or the contract string alone is **not** evidence. Pure markdown / docs-only with no applicable linter: state `none applicable`. Same gate as `/run-plan` Staging-ready lint gate. **Dashboard CSS/HTML only** (`dashboard/dashboard.html` and similar, outside Biome scope): record `Tests: none applicable (dashboard-CSS); covered by plugin-ux-validation` when the UX suite pins the change (ADR `decisions/2026-07-29_dashboard-css-lint-evidence-convention.md`); do not claim Biome covered the HTML. 4. Run in order: validation (not on `main`), CHANGELOG (`[Unreleased]`), checkout staging, pull, working branch, Conventional Commits, push, MR/PR (**always `--base staging` / target `staging`**), merge, cleanup. 5. **Evidence-checks gate (before merge / Gaps-none):** run `gh pr checks ` and confirm `build` (including the **Evidence checks** step) is green. If Evidence checks fail, regenerate and re-push before merge; do not write HANDOFF `- **Gaps:** none` over red. ADR: `2026-08-01_evidence-checks-merge-gate`. diff --git a/.cursor/commands/handoff.md b/.cursor/commands/handoff.md index d7dbd8d..08b282e 100644 --- a/.cursor/commands/handoff.md +++ b/.cursor/commands/handoff.md @@ -40,9 +40,9 @@ Update the handoff document to preserve current state and allow continuation in If they pick automatic, save `{ "autoHandoff": true }` in `.cursor/context/config.json`. 4. **DevOps spine (suggest, do not run without being asked):** - - If the phase produced commitable code, suggest `/git-staging` to promote to pre-prod. + - If the phase produced commitable changes (including versioned HANDOFF, `plan-monitor-*.md`, or `_index.md` Audits rows), suggest `/git-staging`. That command follows `autogit/gitupdate.md` inventory → theme-bucket → ship. Do not treat HANDOFF/memory as skippable dirt. - If there was an error or a tradeoff decision, suggest a memory-loop WRITE (`.cursor/memory/`). - - Never suggest committing directly to `main`; production only via `/git-prod` after staging. + - Never suggest committing directly to `main`; production only via `/git-prod` after staging. `/git-prod` still requires a clean tree except hard excludes; staging must leave it that way. 5. **Respond to the user:** > "Handoff updated! Continue: `/continue-plan`. With code ready: `/git-staging`. Production: `/git-prod`." diff --git a/.cursor/commands/plan-external-review.md b/.cursor/commands/plan-external-review.md index 5c80d4f..f43c8db 100644 --- a/.cursor/commands/plan-external-review.md +++ b/.cursor/commands/plan-external-review.md @@ -7,7 +7,7 @@ description: Arm an optional external plan audit after /run-plan exhausts its im ## Goal -Manually arm **optional plan audits** (external plan review via Claude Code) after `/run-plan` has exhausted implementable to-dos. Claude writes an evidence-based monitor under `.cursor/memory/plan-monitor-*.md`. Cursor triage of findings is a **later** step (not this command). +Manually arm **optional plan audits** (external plan review via Claude Code or Cursor Agent) after `/run-plan` has exhausted implementable to-dos. The reviewer writes an evidence-based monitor under `.cursor/memory/plan-monitor-*.md`. Cursor triage of findings is a **later** step (not this command). ## When to Use @@ -34,7 +34,7 @@ If any are missing: stop. Do **not** claim a review ran. Tell the user to run `a 1. Prefight files above exist. 2. `.cursor/context/config.json` has `externalPlanReview.enabled: true` (see `config.example.json`), **or** use `--force` for a one-shot arm without persisting opt-in. Missing file = disabled unless `--force`. 3. Prefer `externalPlanReview.mode: "autonomous"` for background/inspectable auto-launch. Missing `mode` keeps paste-compatible / legacy behavior. -4. Claude Code CLI (`claude`) on PATH for autonomous / interactive / headless launch; if missing, soft tip + exit 0 (Field Report stays owed). `--paste-only` still prints the command without requiring `claude` yet. +4. A usable reviewer: `backend: "auto"` (default for new example/docs) uses Claude when present, else Cursor Agent. Pinned `backend: "claude"` still tips + no-op when Claude is missing. `--paste-only` still prints the command without requiring a binary yet. Same-family implementer and reviewer is an honest skip (including Auto/Auto). Claude review uses `reviewerModel` (default `haiku`); `advisorModel` (default `opus`) runs only on escalate. ## Manual arm @@ -51,11 +51,12 @@ If any are missing: stop. Do **not** claim a review ran. Tell the user to run `a ### What "operator-visible" means (smoke notes) -- **Autonomous success:** chat arm **must** use `--force --autonomous --wait-monitor`. The launcher prefers a background/inspectable PTY (no OS Terminal focus by default; `--focus-terminal` / `AGENT_KIT_AUDIT_FOCUS_TERMINAL=1` restores activate), then polls until a **fresh** monitor exists (`mtime >= arm epoch` or the HTML comment sentinel `` written into the monitor). Exit `0` = fresh ready; `3` = timeout; `4` = soft-fail while waiting. Spawn-only exit 0 without wait is **not** review done. **Chat continuation:** AwaitShell until `0|3|4`; on `0` run `/plan-review-triage` Ask in the same session. Do **not** stop at Final HANDOFF "after monitor lands" or require typing `done`. ADR: `decisions/2026-07-27_audits-wait-freshness-enforce.md`. +- **Autonomous success:** chat arm **must** use `--force --autonomous --wait-monitor`. The launcher prefers a background/inspectable PTY (no OS Terminal focus by default; `--focus-terminal` / `AGENT_KIT_AUDIT_FOCUS_TERMINAL=1` restores activate), then polls until a **fresh** monitor exists (`mtime >= arm epoch` or the HTML comment sentinel `` written into the monitor). Chat AwaitShell uses `waitSliceSeconds` (default 90); total budget is `waitTimeoutSeconds` (default 900) persisted in `.cursor/context/audit-wait/.json`. A new session resumes remaining budget and does not restart 900s. Early-ready still exits `0` at first freshness. Exit `0` = fresh ready; `3` = timeout (slice or total; not review done); `4` = soft-fail while waiting. Spawn-only exit 0 without wait is **not** review done. **Chat continuation:** AwaitShell until `0|3|4`; on `0` run `/plan-review-triage` Ask in the same session. Do **not** stop at Final HANDOFF "after monitor lands" or require typing `done`. ADRs: `decisions/2026-07-27_audits-wait-freshness-enforce.md`, `decisions/2026-08-13_audits-atomic-wait-reviewer-fallback.md`. - **Autonomous soft-fail:** missing `claude` → tip + exit `4` when `--wait-monitor` was requested (Field Report owed). Background spawn unavailable → falls back to `--paste-only` UX with an honest "NOT running yet" banner. A **silent PTY** (spawn succeeded, no scrollback within the progress-gate grace window) is reported as a failed launch: the launcher disposes the session it just spawned, prints the paste fallback, and soft-fails instead of burning the wait budget. A **session-cap refusal** (detached `agent-kit-audit-*` sessions at the cap) never spawns at all. Soft-fail does **not** invent a monitor or run triage as if review completed. - **Exit 3 is timeout-only:** it means the freshness gate was not satisfied inside the budget, never that the review finished. A monitor that appears later, including one written by a different or later arm, does **not** convert a `3` into success. Leave the target Field Report **owed** and re-arm. ADR: `decisions/2026-07-30_audits-pty-progress-gate-zombie-policy.md`. - **Paste-only:** clipboard + printed interactive one-liner; review starts only after the operator pastes into their Cursor Terminal. After paste (Claude running), the session still waits for the monitor file then continues into triage Ask when possible. -- **`--dry-run`:** resolves mode/plan and prints `background-cmd` / `paste-cmd` / `focus-terminal` without spawning Claude (useful for smoke). +- **`--dry-run`:** resolves mode/plan and prints `background-cmd` / `paste-cmd` / `focus-terminal` / `reviewer-backend` / `same-model-refuse` without spawning a reviewer (useful for smoke). +- **Reviewer cascade:** `--backend auto|claude|cursor` (or config). Claude spawn passes `--model` from `reviewerModel`. Cursor fallback cannot honor a Claude-family name; Auto/Auto is refused. Findings-only until `/plan-review-triage`. Never `/git-prod`. ### A. Script (preferred) diff --git a/.cursor/commands/run-plan-all.md b/.cursor/commands/run-plan-all.md index 34c0dd2..35996fa 100644 --- a/.cursor/commands/run-plan-all.md +++ b/.cursor/commands/run-plan-all.md @@ -184,13 +184,13 @@ Read `externalPlanReview` before the queue confirm Ask and at each advance: | Config | Behavior | |--------|----------| | Audits **pre-flight** (`preflight`: `off` \| `warn` \| `block`) | Before the confirm Ask and before each mid-queue advance: same owed/untriaged check as `/run-plan`. `block` arms or stops; never steals `/git-prod`. | -| `midBatchAudits: true` and audits enabled | After each plan Task returns `outcome: completed`, the **orchestrator** arms **one** full audit for that plan with `--force --autonomous --wait-monitor` (or one `--batch` + wait_all when batching is intentional) **before** advancing the cursor. No paste Ask between plans. Soft-fail → Field Report owed; still advance. AwaitShell until exit `0|3|4`; wait success requires a **fresh** monitor after arm start. Do **not** fan out N background sessions without wait. Do **not** insert a mid-queue triage Ask (operator non-stop preserved; record ready path for queue-end). Mid-batch stays findings-only: **never** auto-Write residuals or rewrite the Run queue between plans. | +| `midBatchAudits: true` and audits enabled | After each plan Task returns `outcome: completed`, the **orchestrator** arms **one** full audit for that plan with `--force --autonomous --wait-monitor` (or one `--batch` + wait_all when batching is intentional) **before** advancing the cursor. No paste Ask between plans. Soft-fail → Field Report owed; still advance. AwaitShell until exit `0|3|4` (chat slice ~90s; remaining budget in `.cursor/context/audit-wait/.json`). Wait success requires a **fresh** monitor after arm start. Reviewer cascade: `backend: "auto"` uses Claude (Haiku) when usable, else Cursor Agent. Same-model implementer/reviewer is an honest skip. Do **not** fan out N background sessions without wait. Do **not** insert a mid-queue triage Ask (operator non-stop preserved; record ready path for queue-end). Mid-batch stays findings-only: **never** auto-Write residuals or rewrite the Run queue between plans. | | `midBatchAudits` false/missing | **Non-stop** mid-queue: do **not** pause for audit Ask/paste between plans. Mid-queue completed plans stay Field Report **owed** until reviewed. | | Queue exhausted | Final HANDOFF; cadence `batch-complete`; then queue-end audit arm covering remaining owed/unreviewed targets (enabled → `--force --autonomous --wait-monitor` or paste per `mode`; else `offerOnExhausted` Ask). Prefer one launcher `--batch` + wait_all when multiple basenames. After wait exit `0`: run `/plan-review-triage` Ask with an **explicit path list** of fresh monitors (batch uniform Ask when outcomes match; sequential fallback when mixed; durable heading per file). **Batch exhaust without conveyor:** when remaining monitors are process-only / depth-capped, prefer uniform **Ack and stop** or **Fix nits only**; do not spawn unbounded `close-*` backlog from Write residuals (ADR `decisions/2026-08-11_plan-audit-residuals-termination.md`). Then suggest `/git-prod` if staging is ahead of `main` (separate HITL). | Never steal `/git-prod` confirmation. Chat never runs silent headless `--force` / `claude -p` in the agent shell. Spawn-only exit 0 without `--wait-monitor` is **not** review done. Never stop at Final HANDOFF "when monitors exist, run triage" after arming: wait (freshness) then continue (mid-batch waits for file only; queue-end waits then triage Ask with explicit paths). ADR: `2026-07-27_audits-autonomous-plan-review-contract.md` (supersedes queue-end-only); wait freshness: `2026-07-27_audits-wait-freshness-enforce.md`. -**Exit 3 stays timeout-only across the queue.** A mid-queue or queue-end arm that returns `3` reviewed nothing: leave that plan Field Report **owed**, keep its path out of the queue-end triage list, and never narrate it as reviewed. Monitors that show up later, including monitors written by a different arm or a later queue position, do **not** retroactively upgrade an earlier `3`. Exit `4` covers the launcher soft-fails: missing `claude`, background spawn unavailable, a **silent PTY** early abort (spawn succeeded but produced no scrollback in the grace window), and a **session-cap refusal** (detached `agent-kit-audit-*` pile at the cap, so nothing spawned). Advance the queue on soft-fail, but record the target as owed, never as reviewed. ADR: `2026-07-30_audits-pty-progress-gate-zombie-policy.md`. +**Exit 3 stays timeout-only across the queue.** A mid-queue or queue-end arm that returns `3` reviewed nothing: leave that plan Field Report **owed**, keep its path out of the queue-end triage list, and never narrate it as reviewed. Monitors that show up later, including monitors written by a different arm or a later queue position, do **not** retroactively upgrade an earlier `3`. Exit `4` covers the launcher soft-fails: no usable reviewer (`backend: "auto"` tried Claude then Cursor; pinned `claude` still tips when Claude is missing), same-model refuse, background spawn unavailable, a **silent PTY** early abort (spawn succeeded but produced no scrollback in the grace window), and a **session-cap refusal** (detached `agent-kit-audit-*` pile at the cap, so nothing spawned). Advance the queue on soft-fail, but record the target as owed, never as reviewed. ADR: `2026-07-30_audits-pty-progress-gate-zombie-policy.md`. ### External plan review (legacy heading) diff --git a/.cursor/commands/run-plan-loop.md b/.cursor/commands/run-plan-loop.md index f9ee78e..1036bc7 100644 --- a/.cursor/commands/run-plan-loop.md +++ b/.cursor/commands/run-plan-loop.md @@ -11,4 +11,4 @@ When invoked, follow [`run-plan.md`](run-plan.md) forcing the **in-session loop* > "Heads up: `/run-plan-loop` is now `/run-plan`. Running with the in-session strategy." -The full tick contract (plan status per tick, HANDOFF, automatic `/git-staging` on diff, **never** `/git-prod`, stop conditions) lives in `run-plan.md`, including the headless runner (`scripts/plan-loop.sh`) section. +The full tick contract (plan status per tick, HANDOFF, automatic `/git-staging` on diff including HANDOFF/memory kit buckets, **never** skip those as trivial, **never** `/git-prod`, stop conditions) lives in `run-plan.md`, including the headless runner (`scripts/plan-loop.sh`) section. diff --git a/.cursor/commands/run-plan-orchestrated.md b/.cursor/commands/run-plan-orchestrated.md index c11420c..82a4cd7 100644 --- a/.cursor/commands/run-plan-orchestrated.md +++ b/.cursor/commands/run-plan-orchestrated.md @@ -11,4 +11,4 @@ When invoked, follow [`run-plan.md`](run-plan.md) forcing the **orchestrated** s > "Heads up: `/run-plan-orchestrated` is now `/run-plan`. Running with the orchestrated strategy." -The full tick contract, worker routing table, and worker prompt template live in `run-plan.md`. Invariants unchanged: automatic `/git-staging` on diff, **never** `/git-prod`. +The full tick contract, worker routing table, and worker prompt template live in `run-plan.md`. Invariants unchanged: automatic `/git-staging` on diff (including HANDOFF/memory kit buckets; never skip as trivial), **never** `/git-prod`. diff --git a/.cursor/commands/run-plan.md b/.cursor/commands/run-plan.md index c3f1dc4..afc9d4d 100644 --- a/.cursor/commands/run-plan.md +++ b/.cursor/commands/run-plan.md @@ -110,7 +110,7 @@ After Final HANDOFF when the run stopped because all implementable to-dos are do - `Always enable automatic`: merge `enabled: true` (and prefer `mode: autonomous` when setting defaults for new opt-in) into `externalPlanReview`, then same background/inspectable arming rules as enabled-true above (not agent-shell `-p`; always include `--wait-monitor` on autonomous arm). - `Not now`: merge `offerOnExhausted: false` (no nag on later exhaustion). Manual `/plan-external-review` still works. 6. **Post-arm monitor watch + continue (chat required):** after arming, **do not** stop at Final HANDOFF "when the monitor lands, run `/plan-review-triage`" or wait for the operator to type `done`. Chat autonomous arm **always** includes `--wait-monitor`. In the **same session**: - 1. AwaitShell / block on the launcher until exit `0` (fresh monitor ready), `3` (timeout), or `4` (soft-fail while waiting). Wait success requires a **fresh** monitor after arm start (mtime/arm-epoch or content sentinel); pre-existing files are not ready. + 1. AwaitShell / block on the launcher until exit `0` (fresh monitor ready), `3` (timeout), or `4` (soft-fail while waiting). Chat slice is ~90s (`waitSliceSeconds`); remaining total budget lives in `.cursor/context/audit-wait/.json` (not HANDOFF). A later session polls remaining budget only. Wait success requires a **fresh** monitor after arm start (mtime/arm-epoch or content sentinel); pre-existing files are not ready. Reviewer cascade: `backend: "auto"` uses Claude (Haiku) when usable, else Cursor Agent; pin `claude` or `cursor` to force one backend. Same-family implementer and reviewer is an honest skip (including Auto/Auto). Opus advisor runs only on escalate. Findings-only until `/plan-review-triage`. 2. On **exit 0:** run `/plan-review-triage` Ask for that monitor path (findings-only; no silent-Ack / auto-fix). Apply termination policy (max closeout depth 1; nits/process-only prefer Ack / Fix nits; do not spawn unbounded `close-*` Write residuals) per ADR `decisions/2026-08-11_plan-audit-residuals-termination.md` and `/plan-review-triage` Step 2b. 3. On **timeout / soft-fail (3|4):** honest tip + Field Report owed; do **not** invent a finished review or run triage as if the monitor is ready. Exit `3` is **timeout only**: it never means review done, and a monitor that appears afterwards (later writer, separate arm, another queue position) does **not** convert it into success. Exit `4` now also covers a **silent PTY** early abort (spawn succeeded, no scrollback in the grace window) and a **session-cap refusal** (detached `agent-kit-audit-*` pile at the cap, nothing spawned): both mean no audit is running. 4. Never claim the audit finished on spawn-only exit 0 or on a stale pre-arm monitor path. @@ -148,8 +148,8 @@ After a findings-contract (or `review-*`) tick returns findings, read `.cursor/c 1. Mark the tick's to-do as `completed` in the plan's frontmatter 2. Update `.cursor/HANDOFF.md` (include `Mode: run-plan ()`) 3. **Cadence ledger:** run `.cursor/scripts/field-report-cadence-bump.sh tick` (increments the gitignored Field Report activity counter; may open a cadence warning when threshold + unreviewed work). Never commit the ledger. ADR: `2026-07-27_field-report-activity-review-cadence.md`. -4. If `git status` has commitable changes **and** it is not just a trivial HANDOFF/memory update: run the `/git-staging` routine **without asking for confirmation** (authorized by this command); 1 MR/PR -> staging branch -> merge. **Monitor hygiene:** if untracked or unrelated dirty `.cursor/memory/plan-monitor-*.md` appear, warn and stage product (and intentional memory) files **add-by-name only**; never broad `git add` of `.cursor/memory/` WIP into a product commit. -5. No commitable diff: just HANDOFF + plan status +4. If `git status` has commitable changes: run the `/git-staging` routine **without asking for confirmation** (authorized by this command), following `autogit/gitupdate.md` inventory → theme-bucket → ship. Do **not** skip because the diff is "just" HANDOFF or memory: versioned HANDOFF, `plan-monitor-*.md`, and `_index.md` Audits rows are a `docs(memory):` / `chore(kit):` bucket (own commit when the product theme differs). **Monitor hygiene:** warn on untracked or unrelated dirty `.cursor/memory/plan-monitor-*.md`; stage **add-by-name only**; never broad `git add` of `.cursor/memory/` WIP into a product commit. Unrelated-plan monitors still get a kit/memory bucket in this staging run; they do not ride inside the product commit. 1 MR/PR -> staging branch -> merge. +5. No commitable diff (working tree clean except gitignored paths and hard excludes): just HANDOFF + plan status ### 6. Reschedule or stop diff --git a/.cursor/context/config.example.json b/.cursor/context/config.example.json index 1da90f6..d053484 100644 --- a/.cursor/context/config.example.json +++ b/.cursor/context/config.example.json @@ -9,7 +9,9 @@ }, "externalPlanReview": { "enabled": false, - "backend": "claude", + "backend": "auto", + "reviewerModel": "haiku", + "advisorModel": "opus", "autoRemediate": false, "offerOnExhausted": true, "mode": "autonomous", diff --git a/.cursor/context/templates/plan-external-review-prompt.md b/.cursor/context/templates/plan-external-review-prompt.md index 323200a..8c2971f 100644 --- a/.cursor/context/templates/plan-external-review-prompt.md +++ b/.cursor/context/templates/plan-external-review-prompt.md @@ -1,6 +1,6 @@ -# External Plan Review Prompt (Claude Code) +# External Plan Review Prompt -Use this prompt when conducting external review of completed Agent Kit plans via Claude Code. +Use this prompt when conducting external review of completed Agent Kit plans via Claude Code or Cursor Agent. ## Role and Contract @@ -12,6 +12,8 @@ You are conducting post-hoc evidence-based monitoring of an Agent Kit plan execu 3. **Evidence gathering** from git history, commits, PRs, and file diffs 4. **Gap detection** between plan requirements and actual shipped deliverables 5. **NO product commits** unless explicitly requested by human after triage +6. **Findings-contract vs git delta:** compare the plan to-dos to `git log` / `git diff` / `git show` vs HEAD. This is not a second implement pass. Do not re-implement features or expand scope. +7. **Advisor escalate:** if any finding is high/critical severity or you are uncertain, add exactly one HTML comment line ``. Do not invoke the advisor yourself. ## Template Path diff --git a/.cursor/rules/cursor-skills-git-workflow.mdc b/.cursor/rules/cursor-skills-git-workflow.mdc index 5c37998..e840ca4 100644 --- a/.cursor/rules/cursor-skills-git-workflow.mdc +++ b/.cursor/rules/cursor-skills-git-workflow.mdc @@ -52,11 +52,11 @@ The "never commit to `main`" rule only works if the checkout is not on `main` wh ## Conventional Commits -Use prefixes: `feat:`, `fix:`, `docs:`, `refactor:`, `chore:`, `test:`, `style:`, `perf:`. +Use prefixes: `feat:`, `fix:`, `docs:`, `docs(memory):`, `refactor:`, `chore:`, `chore(kit):`, `test:`, `style:`, `perf:`. Soft kit paths (plan-monitors, `_index.md` Audits rows, versioned HANDOFF) ship as `docs(memory):` / `chore(kit):` when they are not the product theme. ## When the user says -- **`git staging`** - Full routine in [autogit/gitupdate.md](autogit/gitupdate.md) ("Prompt: git staging" section): validation, CHANGELOG (`[Unreleased]`), checkout staging, pull, working branch, commit, push, MR, merge, cleanup; then HANDOFF. Warn on dirty untracked `plan-monitor-*.md`; stage memory/monitor files add-by-name only (never broad `git add` of `.cursor/memory/` WIP). +- **`git staging`** - Full routine in [autogit/gitupdate.md](autogit/gitupdate.md) ("Prompt: git staging" section): inventory the dirty tree, theme-bucket, CHANGELOG (`[Unreleased]`), checkout staging, pull, working branch, commit, push, MR, merge, cleanup; then HANDOFF. SoT for inventory → bucket → ship. Warn on dirty untracked `plan-monitor-*.md`; stage memory/monitor files add-by-name only (never broad `git add` of `.cursor/memory/` WIP into a product commit). Remaining safe kit dirt ships in a `docs(memory):` / `chore(kit):` bucket in the same run; do not stop-and-quiz because a path looks out of flow. - **`git prod`** - Routine in [autogit/gitupdate.md](autogit/gitupdate.md) ("Prompt: git prod" section): validation, close the release in the CHANGELOG, summary, **explicit confirmation**, merge to main, push; then HANDOFF (+ public sync in this monorepo if applicable). Advisory mention of Blocking untriaged monitors matching the delta is allowed; never steal the confirmation Ask. ## Quick troubleshooting diff --git a/.cursor/rules/hitl-ask-questions.mdc b/.cursor/rules/hitl-ask-questions.mdc index 7d9502e..3b74cec 100644 --- a/.cursor/rules/hitl-ask-questions.mdc +++ b/.cursor/rules/hitl-ask-questions.mdc @@ -34,7 +34,7 @@ Agent Kit uses **Ask questions** tool (`AskQuestion` / ACP `cursor/ask_question` | `/continue-plan` | Confirm next `[to-do-id]`; if multiple plans, pick which to resume | | `/git-prod` | Explicit confirm before merge/push to `main` | | `/hotfix` | Confirm before write+run (`Write mini plan and run` / `Write mini plan only (stop)` / `Modify proposal first` / `Cancel`); lock ambiguous glyphs/tokens before run; risk pause same as `/run-plan` while ticking; never `/git-prod` | -| `/run-plan` | Risk pause (PII, secrets, ambiguous scope); not every tick. Audits pre-flight per `externalPlanReview.preflight`. Plan-exhausted audits arm when enabled (`mode: autonomous` → `--force --autonomous --wait-monitor`; `paste` → paste-only); else `offerOnExhausted` Ask (`Run review now` / `Always enable automatic` / `Not now`); AwaitShell until exit `0|3|4` (fresh monitor; not spawn-only); exit `0` → `/plan-review-triage` Ask (not Final HANDOFF "after monitor lands"); chat never silent headless `--force`; must not steal `/git-prod` HITL (Ask after Final HANDOFF / prod suggestion) | +| `/run-plan` | Risk pause (PII, secrets, ambiguous scope); not every tick. Audits pre-flight per `externalPlanReview.preflight`. Plan-exhausted audits arm when enabled (`mode: autonomous` → `--force --autonomous --wait-monitor`; `paste` → paste-only); else `offerOnExhausted` Ask (`Run review now` / `Always enable automatic` / `Not now`); AwaitShell until exit `0|3|4` (fresh monitor; chat slice `waitSliceSeconds` ~90s; remaining budget in `.cursor/context/audit-wait/`; not spawn-only); `backend: "auto"` uses Claude or Cursor Agent; exit `0` → `/plan-review-triage` Ask (not Final HANDOFF "after monitor lands"); chat never silent headless `--force`; must not steal `/git-prod` HITL (Ask after Final HANDOFF / prod suggestion) | | `/run-plan-all` | Confirm queue Ask (6-way) before execute. Audits pre-flight per config. Mid-queue: when `midBatchAudits` one arm+wait per plan (or one `--batch` + wait_all); no N-Terminal fan-out without wait; no mid-queue triage Ask. Queue-end: audits arm / optional Ask, wait then `/plan-review-triage` Ask with explicit path list, then `/git-prod` suggestion as separate HITL. Malformed Task summary Ask before advancing cursor. Risk gates stay inside each plan Task. | | `/plan-review-triage` | Triage choice (write residuals plan / fix nits only / ack and stop); every outcome persists a durable triage heading on the monitor; reached automatically after wait-monitor exit `0` (freshness) when chat armed the audit. Classify preferred class before Ask (max closeout depth 1; nits/process-only prefer Ack/Fix nits; ADR `2026-08-11_plan-audit-residuals-termination.md`). **Write residuals:** gate 0 refuses another `close-*` when depth-capped/process-only unless operator overrides; Broad Intake (same buckets/labels as `/backlog-add`) → propose from Still open + intake → Ask `Write plan to backlog` / `Modify proposal first` / `Cancel` → plan file + HANDOFF Backlog (no Gate B, no activate, no Run-queue rewrite; `/start-project` optional escape hatch only). **Multi-path:** one Ask when remaining monitors share a uniform outcome class (batch Ack / batch residuals enqueue with one intake + one combined plan); sequential fallback when mixed; never silent-Ack (ADRs `2026-07-27_plan-review-triage-batch-uniform-hitl.md`, `2026-07-28_triage-write-residuals-via-backlog.md`) | | `/handoff` / guardian | Prefer Ask questions when offering auto vs manual handoff | diff --git a/.cursor/scripts/comms-draft.mjs b/.cursor/scripts/comms-draft.mjs new file mode 100644 index 0000000..00d46bb --- /dev/null +++ b/.cursor/scripts/comms-draft.mjs @@ -0,0 +1,134 @@ +#!/usr/bin/env node +/** + * Local Mission Kit comms drafts. Never posts to a network. + * + * node .cursor/scripts/comms-draft.mjs --kind recap --channel x + * node .cursor/scripts/comms-draft.mjs --publish # always exits 2 + */ +import { mkdirSync, readFileSync, writeFileSync, existsSync } from "node:fs"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; + +const KINDS = new Set(["recap", "release", "contributor-ask"]); +const CHANNELS = new Set(["x", "medium", "substack", "hn", "github", "site"]); +const PUBLISH_REFUSE = + "publish refused: comms-draft never posts; operator Ask then human publish"; + +export function utcDay(d = new Date()) { + return d.toISOString().slice(0, 10); +} + +export function draftId(kind, channel, day) { + return `${kind}-${channel}-${day}`; +} + +export function parseArgs(argv) { + const out = { + kind: "recap", + channel: "x", + version: "5.0.0", + publish: false, + forceNew: false, + help: false, + }; + for (let i = 0; i < argv.length; i += 1) { + const a = argv[i]; + if (a === "--help" || a === "-h") out.help = true; + else if (a === "--publish") out.publish = true; + else if (a === "--force-new") out.forceNew = true; + else if (a === "--kind") out.kind = argv[++i] ?? out.kind; + else if (a === "--channel") out.channel = argv[++i] ?? out.channel; + else if (a === "--version") out.version = argv[++i] ?? out.version; + else if (a.startsWith("--kind=")) out.kind = a.slice("--kind=".length); + else if (a.startsWith("--channel=")) out.channel = a.slice("--channel=".length); + else if (a.startsWith("--version=")) out.version = a.slice("--version=".length); + } + return out; +} + +export function validate(opts) { + if (!KINDS.has(opts.kind)) return `unknown --kind ${opts.kind}`; + if (!CHANNELS.has(opts.channel)) return `unknown --channel ${opts.channel}`; + if (opts.publish) return PUBLISH_REFUSE; + return null; +} + +/** Redact any accidental secret-shaped assignment in draft bodies. */ +export function redactSecrets(text) { + return text.replace( + /\b(MISSION_KIT_COMMS_[A-Z0-9_]+|BEARER|TOKEN|WEBHOOK|PASSWORD|SECRET)\s*=\s*\S+/gi, + "$1=", + ); +} + +export function renderDraft(opts, day) { + const id = draftId(opts.kind, opts.channel, day); + const version = opts.version; + const bodies = { + recap: `Mission Kit recap (${day})\n\nShipped this cycle: fill from CHANGELOG [Unreleased] that is already in staging.\n\nInstall: npx @dadado/agent-kit-cli@${version} install\nSite: https://missionkit.io\nHITL: production still needs a human yes.\n`, + release: `Mission Kit ${version} / Agent Kit CLI @${version}\n\nnpx @dadado/agent-kit-cli@${version} install\nhttps://missionkit.io\nPolyForm Noncommercial; commercial: sales@missionkit.io\n`, + "contributor-ask": `Help Mission Kit: skills under registry/skills/community/, or agent-kit contribute from a consumer project.\nIssues: https://github.com/agent-kit-startup/agent-kit\nDo not use public issues for vulnerabilities (SECURITY.md).\nCursor Marketplace submit is not this ask.\n`, + }; + const body = redactSecrets(bodies[opts.kind]); + return { + id, + kind: opts.kind, + channel: opts.channel, + version, + day, + hitl: "pending", + body, + }; +} + +export function draftsDir(repoRoot) { + return join(repoRoot, ".cursor", "comms-drafts"); +} + +export function writeDraft(repoRoot, draft, { forceNew = false } = {}) { + const dir = draftsDir(repoRoot); + mkdirSync(dir, { recursive: true }); + const file = join(dir, `${draft.id}.json`); + if (existsSync(file) && !forceNew) { + return { path: file, reused: true, draft: JSON.parse(readFileSync(file, "utf8")) }; + } + const payload = `${JSON.stringify(draft, null, 2)}\n`; + writeFileSync(file, payload, "utf8"); + return { path: file, reused: false, draft }; +} + +function printHelp() { + process.stdout.write(`comms-draft: local drafts only (never publishes) + +Options: + --kind recap|release|contributor-ask + --channel x|medium|substack|hn|github|site + --version 5.0.0 + --force-new + --publish refused (exit 2) +`); +} + +export function main(argv, repoRoot) { + const opts = parseArgs(argv); + if (opts.help) { + printHelp(); + return 0; + } + const err = validate(opts); + if (err) { + process.stderr.write(`${err}\n`); + return opts.publish ? 2 : 1; + } + const day = utcDay(); + const draft = renderDraft(opts, day); + const result = writeDraft(repoRoot, draft, { forceNew: opts.forceNew }); + process.stdout.write(`${result.reused ? "reused" : "wrote"} ${result.path}\n`); + return 0; +} + +const isDirect = process.argv[1] && fileURLToPath(import.meta.url) === process.argv[1]; +if (isDirect) { + const root = join(dirname(fileURLToPath(import.meta.url)), "..", ".."); + process.exitCode = main(process.argv.slice(2), root); +} diff --git a/.cursor/scripts/comms-draft.test.mjs b/.cursor/scripts/comms-draft.test.mjs new file mode 100644 index 0000000..f1a4ab4 --- /dev/null +++ b/.cursor/scripts/comms-draft.test.mjs @@ -0,0 +1,73 @@ +import assert from "node:assert/strict"; +import { mkdtempSync, readFileSync, rmSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { test } from "node:test"; +import { fileURLToPath } from "node:url"; +import { + draftId, + main, + parseArgs, + redactSecrets, + renderDraft, + validate, + writeDraft, +} from "./comms-draft.mjs"; + +test("publish flag fails closed", () => { + const opts = parseArgs(["--publish", "--kind", "recap"]); + assert.equal(opts.publish, true); + const msg = validate(opts); + assert.match(msg, /never posts/i); +}); + +test("main --publish exits 2 and writes nothing", () => { + const room = mkdtempSync(join(tmpdir(), "ak-comms-")); + try { + const code = main(["--publish"], room); + assert.equal(code, 2); + } finally { + rmSync(room, { recursive: true, force: true }); + } +}); + +test("unknown kind exits 1", () => { + assert.equal(validate(parseArgs(["--kind", "ads"])), "unknown --kind ads"); +}); + +test("renderDraft stays on 5.0.0 and dual-name install", () => { + const d = renderDraft({ kind: "release", channel: "x", version: "5.0.0" }, "2026-08-13"); + assert.equal(d.id, draftId("release", "x", "2026-08-13")); + assert.match(d.body, /@dadado\/agent-kit-cli@5\.0\.0/); + assert.match(d.body, /missionkit\.io/); + assert.equal(d.hitl, "pending"); + assert.doesNotMatch(d.body, /Marketplace listed/i); +}); + +test("redactSecrets strips assignment values", () => { + const out = redactSecrets("MISSION_KIT_COMMS_X_BEARER=live-secret-value TOKEN=abc"); + assert.match(out, //); + assert.doesNotMatch(out, /live-secret-value/); + assert.doesNotMatch(out, /TOKEN=abc/); +}); + +test("writeDraft is idempotent unless force-new", () => { + const room = mkdtempSync(join(tmpdir(), "ak-comms-")); + try { + const draft = renderDraft({ kind: "recap", channel: "x", version: "5.0.0" }, "2026-08-13"); + const a = writeDraft(room, draft); + const b = writeDraft(room, draft); + assert.equal(a.reused, false); + assert.equal(b.reused, true); + const c = writeDraft(room, { ...draft, body: "second" }, { forceNew: true }); + assert.equal(c.reused, false); + const saved = JSON.parse(readFileSync(a.path, "utf8")); + assert.equal(saved.body, "second"); + } finally { + rmSync(room, { recursive: true, force: true }); + } +}); + +test("direct module path is this file's sibling script", () => { + assert.match(fileURLToPath(import.meta.url), /comms-draft\.test\.mjs$/); +}); diff --git a/.cursor/scripts/plan-external-review-atomic-wait.test.mjs b/.cursor/scripts/plan-external-review-atomic-wait.test.mjs new file mode 100644 index 0000000..d6dba9f --- /dev/null +++ b/.cursor/scripts/plan-external-review-atomic-wait.test.mjs @@ -0,0 +1,181 @@ +import assert from "node:assert/strict"; +import { spawnSync } from "node:child_process"; +import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { dirname, join, resolve } from "node:path"; +import { test } from "node:test"; +import { fileURLToPath } from "node:url"; + +const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "../.."); +const SCRIPT = join(repoRoot, ".cursor/scripts/plan-external-review.sh"); +const SRC = readFileSync(SCRIPT, "utf8"); + +function extractFn(name) { + const out = spawnSync("sed", ["-n", `/^${name}() {/,/^}$/p`, SCRIPT], { encoding: "utf8" }); + assert.ok(out.stdout.includes(`${name}()`), `failed to extract ${name}`); + return out.stdout; +} + +function runBash(room, body) { + const fns = [ + "is_headless_env", + "slug_from_monitor_path", + "wait_state_path", + "wait_state_get", + "wait_state_write", + "wait_state_clear", + "wait_state_is_resumable", + "wait_state_load_or_init", + "wait_effective_timeout", + "wait_state_finish", + "file_mtime_epoch", + "monitor_is_fresh", + ] + .map(extractFn) + .join("\n"); + const bash = ` +set -euo pipefail +ROOT=${JSON.stringify(room)} +WAIT_TIMEOUT=40 +WAIT_TIMEOUT_EXPLICIT=1 +WAIT_SLICE=2 +WAIT_SLICE_EXPLICIT=1 +WAIT_ARM_EPOCH="" +WAIT_DEADLINE="" +WAIT_REMAINING="" +WAIT_RESUME=0 +WAIT_STATE_DIR_REL=".cursor/context/audit-wait" +WAIT_BACKEND_STAMP="claude" +WAIT_IMPLEMENTER_MODEL="" +WAIT_REVIEWER_MODEL="" +DRY_RUN=0 +unset CI GITHUB_ACTIONS GITLAB_CI AGENT_KIT_HEADLESS +MOCK_TIME=1000 +date() { echo "\$MOCK_TIME"; } +${fns} +${body} +`; + return spawnSync("bash", ["-c", bash], { encoding: "utf8" }); +} + +function makeRoom() { + const room = mkdtempSync(join(tmpdir(), "ak-atomic-wait.")); + mkdirSync(join(room, ".cursor/memory"), { recursive: true }); + mkdirSync(join(room, ".cursor/context/audit-wait"), { recursive: true }); + return room; +} + +test("launcher wait_for_monitors uses persisted state and slice timeout", () => { + assert.match(SRC, /wait_state_load_or_init/); + assert.match(SRC, /wait_effective_timeout/); + assert.match(SRC, /wait_state_finish ready/); + assert.match(SRC, /wait_state_finish timeout/); + assert.match(SRC, /WAIT_STATE_DIR_REL="\.cursor\/context\/audit-wait"/); + assert.match(SRC, /--wait-slice/); + assert.match(SRC, /waitSliceSeconds/); +}); + +test("wait_effective_timeout uses the chat slice when remaining is larger", () => { + const room = makeRoom(); + try { + const result = runBash( + room, + ` +WAIT_REMAINING=900 +WAIT_SLICE=90 +WAIT_SLICE_EXPLICIT=1 +echo "effective=$(wait_effective_timeout)" +`, + ); + assert.equal(result.status, 0, result.stderr || result.stdout); + assert.match(result.stdout, /effective=90/); + } finally { + rmSync(room, { recursive: true, force: true }); + } +}); + +test("early-ready: sentinel monitor is fresh and finish ready clears wait-state", () => { + const room = makeRoom(); + try { + writeFileSync( + join(room, ".cursor/memory/plan-monitor-a.md"), + "\n# ready\n", + ); + const result = runBash( + room, + ` +wait_state_load_or_init ".cursor/memory/plan-monitor-a.md" +if monitor_is_fresh ".cursor/memory/plan-monitor-a.md"; then + echo "fresh=yes" + wait_state_finish ready ".cursor/memory/plan-monitor-a.md" +else + echo "fresh=no" + exit 1 +fi +`, + ); + assert.equal(result.status, 0, result.stderr || result.stdout); + assert.match(result.stdout, /fresh=yes/); + assert.equal(existsSync(join(room, ".cursor/context/audit-wait/a.json")), false); + } finally { + rmSync(room, { recursive: true, force: true }); + } +}); + +test("slice timeout persists remaining budget outside HANDOFF", () => { + const room = makeRoom(); + try { + const result = runBash( + room, + ` +wait_state_load_or_init ".cursor/memory/plan-monitor-a.md" +echo "effective=$(wait_effective_timeout) remaining=$WAIT_REMAINING" +wait_state_finish timeout ".cursor/memory/plan-monitor-a.md" +`, + ); + assert.equal(result.status, 0, result.stderr || result.stdout); + assert.match(result.stdout, /effective=2/); + assert.match(result.stdout, /wait-slice timeout \(remaining=\d+s/); + const state = JSON.parse(readFileSync(join(room, ".cursor/context/audit-wait/a.json"), "utf8")); + assert.equal(state.status, "armed"); + assert.ok(state.remainingBudgetSeconds >= 30, JSON.stringify(state)); + assert.ok(state.remainingBudgetSeconds <= 40, JSON.stringify(state)); + assert.equal(state.backend, "claude"); + assert.ok(!("token" in state) && !("secret" in state)); + assert.equal(existsSync(join(room, ".cursor/HANDOFF.md")), false); + } finally { + rmSync(room, { recursive: true, force: true }); + } +}); + +test("resume loads stored epoch and remaining instead of restarting 900s", () => { + const room = makeRoom(); + try { + writeFileSync( + join(room, ".cursor/context/audit-wait/a.json"), + JSON.stringify({ + armEpoch: 990, + deadline: 1002, + remainingBudgetSeconds: 2, + backend: "claude", + implementerModel: "", + reviewerModel: "", + status: "armed", + }), + ); + const result = runBash( + room, + ` +WAIT_TIMEOUT=900 +WAIT_SLICE=90 +wait_state_load_or_init ".cursor/memory/plan-monitor-a.md" +echo "resume=$WAIT_RESUME remaining=$WAIT_REMAINING epoch=$WAIT_ARM_EPOCH effective=$(wait_effective_timeout)" +`, + ); + assert.equal(result.status, 0, result.stderr || result.stdout); + assert.match(result.stdout, /wait-state resume/); + assert.match(result.stdout, /resume=1 remaining=2 epoch=990 effective=2/); + } finally { + rmSync(room, { recursive: true, force: true }); + } +}); diff --git a/.cursor/scripts/plan-external-review-backend-cascade.test.mjs b/.cursor/scripts/plan-external-review-backend-cascade.test.mjs new file mode 100644 index 0000000..e360f7b --- /dev/null +++ b/.cursor/scripts/plan-external-review-backend-cascade.test.mjs @@ -0,0 +1,26 @@ +import assert from "node:assert/strict"; +import { readFileSync } from "node:fs"; +import { dirname, resolve } from "node:path"; +import { test } from "node:test"; +import { fileURLToPath } from "node:url"; + +const SCRIPT = resolve( + dirname(fileURLToPath(import.meta.url)), + "../..", + ".cursor/scripts/plan-external-review.sh", +); +const SRC = readFileSync(SCRIPT, "utf8"); + +test("launcher cascade: auto prefers Claude, else cursor-agent", () => { + assert.match(SRC, /resolve_reviewer_backend/); + assert.match(SRC, /AGENT_KIT_AUDIT_CLAUDE_QUOTA_EMPTY/); + assert.match(SRC, /cursor-agent -p --force --sandbox disabled/); + assert.match(SRC, /--backend auto\|claude\|cursor/); + assert.match(SRC, /tip_no_reviewer/); + assert.match(SRC, /This is not a Cursor tick API\/usage-limit hard-stop/); +}); + +test("pinned claude does not share the auto fallback path", () => { + assert.match(SRC, /backend=claude is pinned/); + assert.match(SRC, /REVIEWER_BACKEND" == "none"/); +}); diff --git a/.cursor/scripts/plan-external-review-model-routing.test.mjs b/.cursor/scripts/plan-external-review-model-routing.test.mjs new file mode 100644 index 0000000..096d68f --- /dev/null +++ b/.cursor/scripts/plan-external-review-model-routing.test.mjs @@ -0,0 +1,101 @@ +import assert from "node:assert/strict"; +import { spawnSync } from "node:child_process"; +import { readFileSync } from "node:fs"; +import { dirname, join, resolve } from "node:path"; +import { test } from "node:test"; +import { fileURLToPath } from "node:url"; + +const repoRoot = resolve(dirname(fileURLToPath(import.meta.url)), "../.."); +const SCRIPT = join(repoRoot, ".cursor/scripts/plan-external-review.sh"); +const SRC = readFileSync(SCRIPT, "utf8"); + +function extractFn(name) { + const out = spawnSync("sed", ["-n", `/^${name}() {/,/^}$/p`, SCRIPT], { encoding: "utf8" }); + assert.ok(out.stdout.includes(`${name}()`), `failed to extract ${name}`); + return out.stdout; +} + +function runBash(body) { + const fns = ["normalize_model_family", "models_same_family", "effective_reviewer_model"] + .map(extractFn) + .join("\n"); + const bash = ` +set -euo pipefail +REVIEWER_BACKEND="\${REVIEWER_BACKEND:-claude}" +REVIEWER_MODEL="\${REVIEWER_MODEL:-haiku}" +${fns} +${body} +`; + return spawnSync("bash", ["-c", bash], { encoding: "utf8" }); +} + +test("launcher model routing: Haiku spawn, escalate sentinel, same-model refuse", () => { + assert.match(SRC, /--reviewer-model/); + assert.match(SRC, /--advisor-model/); + assert.match(SRC, /--implementer-model/); + assert.match(SRC, /AGENT_KIT_AUDIT_IMPLEMENTER_MODEL/); + assert.match(SRC, /--model "\$\{WAIT_REVIEWER_MODEL:-haiku\}"/); + assert.match(SRC, //); + assert.match(SRC, /maybe_run_advisor/); + assert.match(SRC, /enforce_implementer_reviewer_split/); + assert.match(SRC, /findings-contract against the git delta/); + assert.match(SRC, /This is not a silent self-review/); +}); + +test("normalize_model_family collapses vendor aliases", () => { + const out = runBash(` +echo "$(normalize_model_family "")" +echo "$(normalize_model_family Auto)" +echo "$(normalize_model_family haiku)" +echo "$(normalize_model_family claude-haiku-4-5-20251001)" +echo "$(normalize_model_family claude-3-5-haiku-latest)" +echo "$(normalize_model_family opus)" +echo "$(normalize_model_family claude-opus-4-20250514)" +echo "$(normalize_model_family composer-2.5-fast)" +`); + assert.equal(out.status, 0, out.stderr); + assert.equal( + out.stdout.trim(), + ["auto", "auto", "haiku", "haiku", "haiku", "opus", "opus", "composer"].join("\n"), + ); +}); + +test("models_same_family refuses Auto/Auto and Haiku/Haiku, allows Auto/Haiku", () => { + const out = runBash(` +models_same_family auto Auto && echo auto-auto || echo auto-auto-no +models_same_family haiku claude-haiku-4-5 && echo haiku-haiku || echo haiku-haiku-no +models_same_family auto haiku && echo auto-haiku || echo auto-haiku-no +models_same_family sonnet opus && echo sonnet-opus || echo sonnet-opus-no +`); + assert.equal(out.status, 0, out.stderr); + assert.equal( + out.stdout.trim(), + ["auto-auto", "haiku-haiku", "auto-haiku-no", "sonnet-opus-no"].join("\n"), + ); +}); + +test("effective_reviewer_model: Claude keeps haiku; Cursor Claude-family collapses to auto", () => { + const claude = runBash(` +REVIEWER_BACKEND=claude +REVIEWER_MODEL=haiku +echo "$(effective_reviewer_model)" +`); + assert.equal(claude.status, 0, claude.stderr); + assert.equal(claude.stdout.trim(), "haiku"); + + const cursorHaiku = runBash(` +REVIEWER_BACKEND=cursor +REVIEWER_MODEL=haiku +echo "$(effective_reviewer_model)" +`); + assert.equal(cursorHaiku.status, 0, cursorHaiku.stderr); + assert.equal(cursorHaiku.stdout.trim(), "auto"); + + const cursorNamed = runBash(` +REVIEWER_BACKEND=cursor +REVIEWER_MODEL=composer-2.5-fast +echo "$(effective_reviewer_model)" +`); + assert.equal(cursorNamed.status, 0, cursorNamed.stderr); + assert.equal(cursorNamed.stdout.trim(), "composer-2.5-fast"); +}); diff --git a/.cursor/scripts/plan-external-review-progress-gate.test.mjs b/.cursor/scripts/plan-external-review-progress-gate.test.mjs index c58884a..6939b10 100644 --- a/.cursor/scripts/plan-external-review-progress-gate.test.mjs +++ b/.cursor/scripts/plan-external-review-progress-gate.test.mjs @@ -128,3 +128,37 @@ wait_for_pty_progress "screen" "test-session" /audits: progress gate disabled \(AGENT_KIT_AUDIT_PROGRESS_TIMEOUT=0\)/, ); }); + +function extractSuffix() { + return spawnSync("sed", ["-n", "/^audit_kit_suffix() {/,/^}$/p", SCRIPT], { + encoding: "utf8", + }).stdout; +} + +test("heartbeat lines keep status prefixes; suffix is empty under CI", () => { + const src = spawnSync("cat", [SCRIPT], { encoding: "utf8" }).stdout; + assert.match(src, /audit_kit_suffix\(\)/); + assert.match(src, /audits: progress gate still silent beyond banner/); + assert.match(src, /audits: wait-monitor still waiting/); + assert.match(src, /audits: wait-monitor timeout after \$\{elapsed\}s \(exit 3\)/); + assert.match(src, /audits: wait-monitor soft-fail \(exit 4\)/); + assert.match(src, /Try agent-kit doctor for repository readiness\./); + const fn = extractSuffix(); + assert.match(fn, /audit_kit_suffix/); + const result = spawnSync("bash", ["-c", `${fn}\nexport CI=1\nprintf '[%s]' "$(audit_kit_suffix 20)"`], { + encoding: "utf8", + }); + assert.strictEqual(result.status ?? 1, 0, result.stderr); + assert.strictEqual(result.stdout, "[]"); +}); + +test("audit_kit_suffix is empty under NO_COLOR even without CI", () => { + const fn = extractSuffix(); + const result = spawnSync( + "bash", + ["-c", `${fn}\nunset CI\nexport NO_COLOR=1\nprintf '[%s]' "$(audit_kit_suffix 8)"`], + { encoding: "utf8" }, + ); + assert.strictEqual(result.status ?? 1, 0, result.stderr); + assert.strictEqual(result.stdout, "[]"); +}); diff --git a/.cursor/scripts/plan-external-review.sh b/.cursor/scripts/plan-external-review.sh index d28b062..8f88407 100755 --- a/.cursor/scripts/plan-external-review.sh +++ b/.cursor/scripts/plan-external-review.sh @@ -48,13 +48,24 @@ # --focus-terminal / AGENT_KIT_AUDIT_FOCUS_TERMINAL=1: rollback to OS Terminal activate / # emulator focus (legacy foreground window). Default is no-focus background spawn. # --wait-monitor: after autonomous spawn (or standalone), poll until plan-monitor-.md -# is fresh under .cursor/memory/, or until --wait-timeout (default 900s). +# is fresh under .cursor/memory/, or until the effective slice/remaining budget. # Fresh = mtime >= arm epoch (recorded when wait arm starts) OR file contains # the content sentinel line: # Pre-arm / stale files are ignored (do not exit 0 on existence alone). # Combine with --force --autonomous (spawn then wait), or use alone to poll an # already-running review. Does not switch to invisible agent-shell claude -p. -# --wait-timeout SECONDS: poll budget for --wait-monitor (default 900). +# Wait state lives in .cursor/context/audit-wait/.json (not HANDOFF). +# A new session resumes remaining budget; it does not restart 900s from zero. +# --wait-timeout SECONDS: total remaining budget for a new arm (default 900; config +# waitTimeoutSeconds). CI/headless uses this as the invocation cap. +# --wait-slice SECONDS: chat AwaitShell budget per session (default 90; config +# waitSliceSeconds). Early-ready still exits 0 at first freshness. +# --backend auto|claude|cursor: reviewer cascade override. auto uses Claude when +# usable, else cursor-agent. Existing config with missing backend stays claude. +# --reviewer-model NAME: reviewer model id (default haiku / config reviewerModel). +# --advisor-model NAME: escalate-only advisor (default opus / config advisorModel). +# --implementer-model NAME: stamp the model that shipped the tick (default auto / +# AGENT_KIT_AUDIT_IMPLEMENTER_MODEL). Same-family reviewer is refused. # # Progress gate (post-spawn PTY activity): # A successful spawn is a launch, not a running review. After an autonomous background @@ -98,6 +109,8 @@ # AGENT_KIT_AUDIT_REAP 1/true: same as --reap-audit-sessions (opt-in disposal # of detached workspace-owned sessions past the age floor). # AGENT_KIT_AUDIT_FOCUS_TERMINAL 1/true: rollback to OS Terminal activate / emulator focus. +# AGENT_KIT_AUDIT_IMPLEMENTER_MODEL model id that shipped the tick (stamp only; no secrets). +# AGENT_KIT_AUDIT_REVIEWER_MODEL override reviewer model id (default haiku). # # Exit codes: # 0 ok / fresh monitor ready (with --wait-monitor) / soft-fail tip when NOT waiting @@ -112,6 +125,7 @@ # Freshness ADR: .cursor/memory/decisions/2026-07-27_audits-wait-freshness-enforce.md # Background PTY ADR: .cursor/memory/decisions/2026-07-28_audits-headless-terminal-honesty.md # Progress gate ADR: .cursor/memory/decisions/2026-07-30_audits-pty-progress-gate-zombie-policy.md +# Atomic wait ADR: .cursor/memory/decisions/2026-08-13_audits-atomic-wait-reviewer-fallback.md set -euo pipefail @@ -130,12 +144,59 @@ BATCH=0 DRY_RUN=0 WAIT_MONITOR=0 WAIT_TIMEOUT=900 +WAIT_TIMEOUT_EXPLICIT=0 +WAIT_SLICE=90 +WAIT_SLICE_EXPLICIT=0 WAIT_ARM_EPOCH="" +WAIT_DEADLINE="" +WAIT_REMAINING="" +WAIT_RESUME=0 +WAIT_STATE_DIR_REL=".cursor/context/audit-wait" +WAIT_BACKEND_STAMP="claude" +WAIT_IMPLEMENTER_MODEL="" +WAIT_REVIEWER_MODEL="" +REVIEWER_BACKEND="" +REVIEWER_BACKEND_EXPLICIT=0 +REVIEWER_MODEL="" +REVIEWER_MODEL_EXPLICIT=0 +ADVISOR_MODEL="" +ADVISOR_MODEL_EXPLICIT=0 +IMPLEMENTER_MODEL_EXPLICIT=0 +SAME_MODEL_REFUSE=0 +ADVISOR_ESCALATE_SENTINEL="" # Post-spawn PTY activity gate grace window (seconds). 0 disables. PROGRESS_TIMEOUT=60 # Kit-owned audit session namespace. Cap/reap only touch workspace-owned names (see token). AUDIT_SESSION_NS_PREFIX="agent-kit-audit-" # 8-hex token from ROOT so concurrent workspaces do not share cap/reap scope. + +# Visual-kit heartbeat suffix (frames + tip). Empty under NO_COLOR / CI / non-TTY. +# Does not change wait freshness exits 0|3|4. Same catalog as packages/cli visual-kit.ts. +audit_kit_suffix() { + local elapsed="${1:-0}" + if [[ -n "${NO_COLOR:-}" || -n "${NODE_DISABLE_COLORS:-}" || "${FORCE_COLOR:-}" == "0" ]]; then + return 0 + fi + if [[ -n "${CI:-}" || "${AGENT_KIT_REDUCED_MOTION:-}" == "1" ]]; then + return 0 + fi + if [[ ! -t 1 ]]; then + return 0 + fi + local frames=(⠋ ⠙ ⠹ ⠸ ⠼ ⠴ ⠦ ⠧ ⠇ ⠏) + local tips=( + "Try agent-kit doctor for repository readiness." + "HITL gates stay in Cursor slash commands." + "/run-plan never promotes to production." + "Mission Control is the dashboard, not the CLI name." + "NO_COLOR and CI keep this output static." + "agent-kit --help groups SETUP, MISSION, DASHBOARD, INTEGRITY." + ) + local fi=$((elapsed % ${#frames[@]})) + local ti=$(((elapsed / 4) % ${#tips[@]})) + printf ' %s ✦ %s' "${frames[$fi]}" "${tips[$ti]}" +} + audit_workspace_token() { local hash="" if command -v shasum >/dev/null 2>&1; then @@ -225,7 +286,7 @@ if [[ "${AGENT_KIT_AUDIT_REAP:-}" == "1" || "${AGENT_KIT_AUDIT_REAP:-}" == "true fi usage() { - sed -n '2,111p' "$0" | sed 's/^# \{0,1\}//' + sed -n '2,128p' "$0" | sed 's/^# \{0,1\}//' } while [[ $# -gt 0 ]]; do @@ -288,6 +349,60 @@ while [[ $# -gt 0 ]]; do exit 2 fi WAIT_TIMEOUT="$2" + WAIT_TIMEOUT_EXPLICIT=1 + shift 2 + ;; + --wait-slice) + if [[ $# -lt 2 || -z "${2:-}" ]]; then + echo "error: --wait-slice requires SECONDS" >&2 + exit 2 + fi + if ! [[ "$2" =~ ^[1-9][0-9]*$ ]]; then + echo "error: --wait-slice must be a positive integer (got: $2)" >&2 + exit 2 + fi + WAIT_SLICE="$2" + WAIT_SLICE_EXPLICIT=1 + shift 2 + ;; + --backend) + if [[ $# -lt 2 || -z "${2:-}" ]]; then + echo "error: --backend requires auto|claude|cursor" >&2 + exit 2 + fi + if [[ "$2" != "auto" && "$2" != "claude" && "$2" != "cursor" ]]; then + echo "error: --backend must be auto, claude, or cursor (got: $2)" >&2 + exit 2 + fi + REVIEWER_BACKEND="$2" + REVIEWER_BACKEND_EXPLICIT=1 + shift 2 + ;; + --reviewer-model) + if [[ $# -lt 2 || -z "${2:-}" ]]; then + echo "error: --reviewer-model requires NAME" >&2 + exit 2 + fi + REVIEWER_MODEL="$2" + REVIEWER_MODEL_EXPLICIT=1 + shift 2 + ;; + --advisor-model) + if [[ $# -lt 2 || -z "${2:-}" ]]; then + echo "error: --advisor-model requires NAME" >&2 + exit 2 + fi + ADVISOR_MODEL="$2" + ADVISOR_MODEL_EXPLICIT=1 + shift 2 + ;; + --implementer-model) + if [[ $# -lt 2 || -z "${2:-}" ]]; then + echo "error: --implementer-model requires NAME" >&2 + exit 2 + fi + WAIT_IMPLEMENTER_MODEL="$2" + IMPLEMENTER_MODEL_EXPLICIT=1 shift 2 ;; --) @@ -333,14 +448,25 @@ EOF tip_no_claude() { cat </dev/null 2>&1; then + _ak_suf="$(audit_kit_suffix "$elapsed" || true)" + fi + echo "audits: progress gate still silent beyond banner (${elapsed}s elapsed, baseline=${baseline}, current=${bytes})${_ak_suf}" fi sleep 2 done @@ -1018,6 +1148,547 @@ CI / headless: $LAUNCHER_REL --force --print (claude -p; agent-kit run-plan on EOF } +# Prints a positive integer from externalPlanReview., or $2 when missing/invalid. +config_positive_int() { + local key="$1" + local default="$2" + if [[ ! -f "$CONFIG" ]]; then + echo "$default" + return + fi + if command -v node >/dev/null 2>&1; then + node -e ' + const fs = require("fs"); + const key = process.argv[2]; + const fallback = process.argv[3]; + try { + const j = JSON.parse(fs.readFileSync(process.argv[1], "utf8")); + const v = j && j.externalPlanReview && j.externalPlanReview[key]; + process.stdout.write(Number.isInteger(v) && v >= 1 ? String(v) : fallback); + } catch { + process.stdout.write(fallback); + } + ' "$CONFIG" "$key" "$default" + return + fi + echo "$default" +} + +apply_wait_budget_defaults() { + if [[ "$WAIT_TIMEOUT_EXPLICIT" -eq 0 ]]; then + WAIT_TIMEOUT="$(config_positive_int waitTimeoutSeconds 900)" + fi + if [[ "$WAIT_SLICE_EXPLICIT" -eq 0 ]]; then + WAIT_SLICE="$(config_positive_int waitSliceSeconds 90)" + fi +} + +# Prints a non-empty string from externalPlanReview., or $2 when missing/invalid. +config_review_string() { + local key="$1" + local default="$2" + if [[ ! -f "$CONFIG" ]]; then + echo "$default" + return + fi + if command -v node >/dev/null 2>&1; then + node -e ' + const fs = require("fs"); + const key = process.argv[2]; + const fallback = process.argv[3]; + try { + const j = JSON.parse(fs.readFileSync(process.argv[1], "utf8")); + const v = j && j.externalPlanReview && j.externalPlanReview[key]; + process.stdout.write(typeof v === "string" && v.trim() ? v.trim() : fallback); + } catch { + process.stdout.write(fallback); + } + ' "$CONFIG" "$key" "$default" + return + fi + echo "$default" +} + +# Collapse vendor aliases to a family id for implementer≠reviewer compare. +normalize_model_family() { + local raw + raw="$(printf '%s' "${1:-}" | tr '[:upper:]' '[:lower:]' | tr -d '[:space:]')" + if [[ -z "$raw" ]]; then + printf '%s' "auto" + return + fi + case "$raw" in + auto|cursor-auto|cursorauto|default) + printf '%s' "auto" + ;; + haiku|claude-haiku|claude-3-haiku*|claude-3.5-haiku*|claude-3-5-haiku*|claude-haiku-*|claude-4-haiku*|claude-4.5-haiku*) + printf '%s' "haiku" + ;; + opus|claude-opus|claude-3-opus*|claude-opus-*|claude-4-opus*|claude-4.5-opus*) + printf '%s' "opus" + ;; + sonnet|claude-sonnet|claude-3-sonnet*|claude-3.5-sonnet*|claude-3-5-sonnet*|claude-3-7-sonnet*|claude-sonnet-*|claude-4-sonnet*|claude-4.5-sonnet*) + printf '%s' "sonnet" + ;; + composer|composer-*|cursor-composer*) + printf '%s' "composer" + ;; + grok|grok-*) + printf '%s' "grok" + ;; + *) + printf '%s' "$raw" + ;; + esac +} + +models_same_family() { + [[ "$(normalize_model_family "$1")" == "$(normalize_model_family "$2")" ]] +} + +# Runtime reviewer id. Cursor cannot honor Claude-family names; those collapse to auto. +effective_reviewer_model() { + local named="${REVIEWER_MODEL:-haiku}" + if [[ "${REVIEWER_BACKEND:-}" == "cursor" ]]; then + local fam + fam="$(normalize_model_family "$named")" + case "$fam" in + haiku|opus|sonnet) + printf '%s' "auto" + return + ;; + esac + fi + printf '%s' "$named" +} + +tip_same_model() { + cat < claude (existing-install pin). +config_review_backend() { + if [[ ! -f "$CONFIG" ]]; then + echo "claude" + return + fi + if command -v node >/dev/null 2>&1; then + node -e ' + const fs = require("fs"); + try { + const j = JSON.parse(fs.readFileSync(process.argv[1], "utf8")); + const v = j && j.externalPlanReview && j.externalPlanReview.backend; + process.stdout.write(v === "auto" || v === "cursor" || v === "claude" ? v : "claude"); + } catch { + process.stdout.write("claude"); + } + ' "$CONFIG" + return + fi + echo "claude" +} + +claude_usable() { + if [[ "${AGENT_KIT_AUDIT_CLAUDE_QUOTA_EMPTY:-}" == "1" || "${AGENT_KIT_AUDIT_CLAUDE_QUOTA_EMPTY:-}" == "true" ]]; then + return 1 + fi + command -v claude >/dev/null 2>&1 +} + +cursor_agent_usable() { + command -v cursor-agent >/dev/null 2>&1 +} + +# Sets REVIEWER_BACKEND to claude|cursor|none. Distinct from Cursor tick quota hard-stop. +resolve_reviewer_backend() { + local want + if [[ "$REVIEWER_BACKEND_EXPLICIT" -eq 1 && -n "$REVIEWER_BACKEND" ]]; then + want="$REVIEWER_BACKEND" + else + want="$(config_review_backend)" + fi + case "$want" in + cursor) + if cursor_agent_usable; then + REVIEWER_BACKEND="cursor" + else + REVIEWER_BACKEND="none" + fi + ;; + auto) + if claude_usable; then + REVIEWER_BACKEND="claude" + elif cursor_agent_usable; then + REVIEWER_BACKEND="cursor" + else + REVIEWER_BACKEND="none" + fi + ;; + *) + if claude_usable; then + REVIEWER_BACKEND="claude" + else + REVIEWER_BACKEND="none" + fi + ;; + esac + if [[ "$REVIEWER_BACKEND" != "none" ]]; then + WAIT_BACKEND_STAMP="$REVIEWER_BACKEND" + fi +} + +cursor_agent_cmd() { + local prompt="$1" + if [[ -n "${WAIT_REVIEWER_MODEL:-}" && "$(normalize_model_family "$WAIT_REVIEWER_MODEL")" != "auto" ]]; then + printf '%s' "cursor-agent -p --force --sandbox disabled --workspace $(printf '%q' "$ROOT") --model $(printf '%q' "$WAIT_REVIEWER_MODEL") $(printf '%q' "$prompt")" + return + fi + printf '%s' "cursor-agent -p --force --sandbox disabled --workspace $(printf '%q' "$ROOT") $(printf '%q' "$prompt")" +} + +# Interactive (no -p) or print/headless (-p). Never used for chat autonomous spawn. +exec_reviewer() { + local print_flag="${1:-}" + cd "$ROOT" + if [[ "$REVIEWER_BACKEND" == "cursor" ]]; then + if [[ -n "${WAIT_REVIEWER_MODEL:-}" && "$(normalize_model_family "$WAIT_REVIEWER_MODEL")" != "auto" ]]; then + exec cursor-agent -p --force --sandbox disabled --workspace "$ROOT" --model "$WAIT_REVIEWER_MODEL" "$PROMPT" + fi + exec cursor-agent -p --force --sandbox disabled --workspace "$ROOT" "$PROMPT" + fi + if [[ "$print_flag" == "-p" ]]; then + exec claude --permission-mode "$CLAUDE_PERMISSION_MODE" --model "${WAIT_REVIEWER_MODEL:-haiku}" -p "$PROMPT" + fi + exec claude --permission-mode "$CLAUDE_PERMISSION_MODE" --model "${WAIT_REVIEWER_MODEL:-haiku}" "$PROMPT" +} + +monitor_wants_advisor() { + local rel="$1" + [[ -f "$ROOT/$rel" ]] && grep -qF "$ADVISOR_ESCALATE_SENTINEL" "$ROOT/$rel" +} + +build_advisor_prompt() { + local paths=("$@") + local list="" + local p + for p in "${paths[@]}"; do + list+="- $p"$'\n' + done + cat < after the Advisor section. +EOF +} + +# After Haiku monitor is fresh: Opus only when the escalate sentinel is present. +# Headless/print may use claude -p (CI path). Chat uses inspectable PTY or a tip; does not re-burn wait. +maybe_run_advisor() { + local paths=("$@") + local want=0 + local p + for p in "${paths[@]}"; do + if monitor_wants_advisor "$p"; then + want=1 + break + fi + done + [[ "$want" -eq 1 ]] || return 0 + if models_same_family "${WAIT_IMPLEMENTER_MODEL:-auto}" "${ADVISOR_MODEL:-opus}"; then + echo "audits: advisor escalate skipped (advisor-model=${ADVISOR_MODEL:-opus} matches implementer; not a silent self-review)" + return 0 + fi + if models_same_family "${WAIT_REVIEWER_MODEL:-haiku}" "${ADVISOR_MODEL:-opus}"; then + echo "audits: advisor escalate skipped (advisor-model matches reviewer)" + return 0 + fi + echo "audits: advisor escalate (advisor-model=${ADVISOR_MODEL:-opus}; findings-only; not a second implement pass)" + if ! claude_usable; then + echo "tip: advisor requires Claude. Haiku/Cursor monitor is already ready; triage still applies." + return 0 + fi + local advisor_prompt + advisor_prompt="$(build_advisor_prompt "${paths[@]}")" + if is_headless_env || [[ "${MODE:-}" == "print" ]]; then + claude --permission-mode "$CLAUDE_PERMISSION_MODE" --model "${ADVISOR_MODEL:-opus}" -p "$advisor_prompt" || true + return 0 + fi + local advisor_cmd + advisor_cmd="cd $(printf '%q' "$ROOT") && claude --permission-mode $(printf '%q' "$CLAUDE_PERMISSION_MODE") --model $(printf '%q' "${ADVISOR_MODEL:-opus}") $(printf '%q' "$advisor_prompt")" + if launch_background_terminal "$advisor_cmd"; then + echo "audits: advisor PTY launched (channel: ${LAUNCH_CHANNEL:-unknown}). Haiku monitor is already ready for triage." + else + echo "tip: advisor escalate is marked on the monitor. Paste in a terminal (findings-only):" + echo " claude --permission-mode ${CLAUDE_PERMISSION_MODE} --model ${ADVISOR_MODEL:-opus}" + fi +} + +slug_from_monitor_path() { + local rel="$1" + local base + base="$(basename "$rel")" + base="${base#plan-monitor-}" + printf '%s' "${base%.md}" +} + +wait_state_path() { + local slug="$1" + printf '%s' "$ROOT/$WAIT_STATE_DIR_REL/${slug}.json" +} + +wait_state_get() { + local slug="$1" + local field="$2" + local path + path="$(wait_state_path "$slug")" + if [[ ! -f "$path" ]]; then + echo "" + return + fi + if ! command -v node >/dev/null 2>&1; then + echo "" + return + fi + node -e ' + const fs = require("fs"); + try { + const j = JSON.parse(fs.readFileSync(process.argv[1], "utf8")); + const v = j && j[process.argv[2]]; + process.stdout.write(v == null ? "" : String(v)); + } catch { + process.stdout.write(""); + } + ' "$path" "$field" +} + +wait_state_write() { + local slug="$1" + local arm_epoch="$2" + local deadline="$3" + local remaining="$4" + local status="$5" + local path + path="$(wait_state_path "$slug")" + mkdir -p "$(dirname "$path")" + if ! command -v node >/dev/null 2>&1; then + echo "tip: node required to persist audit wait-state at $WAIT_STATE_DIR_REL" >&2 + return 1 + fi + node -e ' + const fs = require("fs"); + const out = { + armEpoch: Number(process.argv[2]), + deadline: Number(process.argv[3]), + remainingBudgetSeconds: Number(process.argv[4]), + backend: process.argv[5], + implementerModel: process.argv[6], + reviewerModel: process.argv[7], + status: process.argv[8], + }; + fs.writeFileSync(process.argv[1], JSON.stringify(out, null, 2) + "\n"); + ' "$path" "$arm_epoch" "$deadline" "$remaining" "${WAIT_BACKEND_STAMP:-claude}" "${WAIT_IMPLEMENTER_MODEL:-}" "${WAIT_REVIEWER_MODEL:-}" "$status" +} + +wait_state_clear() { + local slug="$1" + rm -f "$(wait_state_path "$slug")" +} + +wait_state_is_resumable() { + local slug="$1" + local status deadline remaining now + status="$(wait_state_get "$slug" status)" + [[ "$status" == "armed" ]] || return 1 + deadline="$(wait_state_get "$slug" deadline)" + remaining="$(wait_state_get "$slug" remainingBudgetSeconds)" + now="$(date +%s)" + [[ "$deadline" =~ ^[1-9][0-9]*$ ]] || return 1 + [[ "$remaining" =~ ^[1-9][0-9]*$ ]] || return 1 + [[ "$now" -lt "$deadline" ]] || return 1 + return 0 +} + +# Sets WAIT_RESUME, WAIT_ARM_EPOCH, WAIT_DEADLINE, WAIT_REMAINING. Writes state unless dry-run. +wait_state_load_or_init() { + local paths=("$@") + local all_resume=1 + local p slug + if [[ ${#paths[@]} -eq 0 ]]; then + WAIT_RESUME=0 + return + fi + for p in "${paths[@]}"; do + slug="$(slug_from_monitor_path "$p")" + if ! wait_state_is_resumable "$slug"; then + all_resume=0 + break + fi + done + local now + now="$(date +%s)" + if [[ "$all_resume" -eq 1 ]]; then + local first_slug + first_slug="$(slug_from_monitor_path "${paths[0]}")" + WAIT_ARM_EPOCH="$(wait_state_get "$first_slug" armEpoch)" + WAIT_DEADLINE="$(wait_state_get "$first_slug" deadline)" + WAIT_REMAINING=$((WAIT_DEADLINE - now)) + if [[ "$WAIT_REMAINING" -ge 1 ]]; then + local min_rem="$WAIT_REMAINING" + for p in "${paths[@]}"; do + slug="$(slug_from_monitor_path "$p")" + local d r + d="$(wait_state_get "$slug" deadline)" + r=$((d - now)) + if [[ "$r" -lt "$min_rem" ]]; then + min_rem="$r" + fi + done + WAIT_REMAINING="$min_rem" + WAIT_RESUME=1 + echo "audits: wait-state resume (arm-epoch=${WAIT_ARM_EPOCH} remaining=${WAIT_REMAINING}s)" + return + fi + fi + WAIT_RESUME=0 + if [[ -z "${WAIT_ARM_EPOCH:-}" ]]; then + WAIT_ARM_EPOCH="$now" + fi + WAIT_DEADLINE=$((WAIT_ARM_EPOCH + WAIT_TIMEOUT)) + WAIT_REMAINING="$WAIT_TIMEOUT" + if [[ "$DRY_RUN" -ne 1 ]]; then + for p in "${paths[@]}"; do + slug="$(slug_from_monitor_path "$p")" + wait_state_write "$slug" "$WAIT_ARM_EPOCH" "$WAIT_DEADLINE" "$WAIT_REMAINING" "armed" + done + fi +} + +wait_effective_timeout() { + local remaining="${WAIT_REMAINING:-$WAIT_TIMEOUT}" + if [[ "$remaining" -lt 1 ]]; then + echo 1 + return + fi + if is_headless_env && [[ "$WAIT_SLICE_EXPLICIT" -eq 0 ]]; then + echo "$remaining" + return + fi + local slice="${WAIT_SLICE:-90}" + if [[ "$slice" -lt "$remaining" ]]; then + echo "$slice" + else + echo "$remaining" + fi +} + +wait_state_finish() { + local outcome="$1" + shift + local paths=("$@") + local now remaining status + now="$(date +%s)" + remaining=$((WAIT_DEADLINE - now)) + if [[ "$remaining" -lt 0 ]]; then + remaining=0 + fi + local p slug + if [[ "$outcome" == "ready" ]]; then + for p in "${paths[@]}"; do + slug="$(slug_from_monitor_path "$p")" + wait_state_clear "$slug" + done + return + fi + status="armed" + if [[ "$remaining" -le 0 ]]; then + status="timeout" + remaining=0 + fi + for p in "${paths[@]}"; do + slug="$(slug_from_monitor_path "$p")" + wait_state_write "$slug" "$WAIT_ARM_EPOCH" "$WAIT_DEADLINE" "$remaining" "$status" + done + if [[ "$status" == "armed" ]]; then + echo "audits: wait-slice timeout (remaining=${remaining}s; resume via --wait-monitor; not review done)" + fi +} + +maybe_resume_from_plan_args() { + [[ "$WAIT_MONITOR" -eq 1 ]] || return 0 + [[ ${#PLAN_ARGS[@]} -gt 0 ]] || return 0 + local paths=() + local arg base + for arg in "${PLAN_ARGS[@]}"; do + base="$(basename "$arg")" + paths+=("$(monitor_path_for_plan_base "$base")") + done + wait_state_load_or_init "${paths[@]}" + if [[ "$WAIT_RESUME" -eq 1 ]]; then + POLL_ONLY=1 + if [[ "$MODE_EXPLICIT" -eq 0 ]]; then + MODE="paste-only" + fi + fi +} + # Prints "true" or "false". Missing config / key / parse failure => false (default). config_auto_remediate() { if [[ ! -f "$CONFIG" ]]; then @@ -1169,6 +1840,12 @@ Also read: $HANDOFF_REL Git HEAD: $HEAD_SHA Repo root: $ROOT autoRemediate (from config): $auto_remediate +Reviewer model: ${WAIT_REVIEWER_MODEL:-haiku} +Implementer model (stamp): ${WAIT_IMPLEMENTER_MODEL:-auto} +Advisor model (escalate only): ${ADVISOR_MODEL:-opus} + +This is a findings-contract against the git delta plus the plan, not a second implement pass. +Use git log / git diff / git show vs HEAD and the plan to-dos. Do not re-implement features. Contract reminders: - Evidence-based monitor only under .cursor/memory/plan-monitor-.md @@ -1179,6 +1856,9 @@ Contract reminders: - When autoRemediate is false (default): do not apply or suggest starting product edits in this session - Never /git-prod; never broad git add (add-by-name if staging monitor) - Index new monitors in .cursor/memory/_index.md (Audits table) +- If any finding is high/critical severity or you are uncertain, add exactly one HTML comment line: + + Do not invoke the advisor yourself. - Closeout: print a ready-to-paste line with explicit paths for every monitor written this session: /plan-review-triage .cursor/memory/plan-monitor-.md Never recommend bare /plan-review-triage alone (mtime can miss fresh reviews behind bulk-touched older monitors). @@ -1196,6 +1876,12 @@ Also read: $HANDOFF_REL Git HEAD: $HEAD_SHA Repo root: $ROOT autoRemediate (from config): $auto_remediate +Reviewer model: ${WAIT_REVIEWER_MODEL:-haiku} +Implementer model (stamp): ${WAIT_IMPLEMENTER_MODEL:-auto} +Advisor model (escalate only): ${ADVISOR_MODEL:-opus} + +This is a findings-contract against the git delta plus each plan, not a second implement pass. +Use git log / git diff / git show vs HEAD and the plan to-dos. Do not re-implement features. Review each of these plans in one session (write one plan-monitor-.md per plan): $plan_list @@ -1209,6 +1895,9 @@ Contract reminders: - When autoRemediate is false (default): do not apply or suggest starting product edits in this session - Never /git-prod; never broad git add (add-by-name if staging monitor) - Index new monitors in .cursor/memory/_index.md (Audits table) +- If any finding is high/critical severity or you are uncertain, add exactly one HTML comment line: + + Do not invoke the advisor yourself. - Closeout: print one ready-to-paste line covering every monitor written this session, e.g. /plan-review-triage .cursor/memory/plan-monitor-.md .cursor/memory/plan-monitor-.md Never recommend bare /plan-review-triage alone (mtime can miss fresh reviews behind bulk-touched older monitors). @@ -1225,6 +1914,14 @@ monitor_path_for_plan_base() { # Soft-fail while waiting: exit 4. Soft-fail without wait: tip already printed, exit 0. soft_fail_exit() { if [[ "$WAIT_MONITOR" -eq 1 ]]; then + local arg base slug + for arg in "${PLAN_ARGS[@]+"${PLAN_ARGS[@]}"}"; do + base="$(basename "$arg")" + slug="${base%.plan.md}" + if [[ -f "$(wait_state_path "$slug")" ]]; then + wait_state_write "$slug" "${WAIT_ARM_EPOCH:-$(date +%s)}" "${WAIT_DEADLINE:-0}" 0 "soft-fail" + fi + done echo "audits: wait-monitor soft-fail (exit 4)" exit 4 fi @@ -1266,19 +1963,19 @@ monitor_is_fresh() { # Poll until every relative monitor path is fresh under $ROOT, or timeout. # Prints waiting/created/timeout status lines. Exits 0 (fresh ready) or 3 (timeout). +# Chat uses waitSliceSeconds (default 90); remaining total budget is persisted. wait_for_monitors() { local paths=("$@") if [[ ${#paths[@]} -eq 0 ]]; then echo "error: wait_for_monitors requires at least one monitor path" >&2 exit 2 fi - if [[ -z "${WAIT_ARM_EPOCH:-}" ]]; then - WAIT_ARM_EPOCH="$(date +%s)" - fi - local timeout="$WAIT_TIMEOUT" + wait_state_load_or_init "${paths[@]}" + local timeout + timeout="$(wait_effective_timeout)" local start now elapsed remaining start="$(date +%s)" - echo "audits: wait-monitor waiting (timeout=${timeout}s arm-epoch=${WAIT_ARM_EPOCH})" + echo "audits: wait-monitor waiting (timeout=${timeout}s slice=${WAIT_SLICE}s total=${WAIT_TIMEOUT}s remaining=${WAIT_REMAINING}s arm-epoch=${WAIT_ARM_EPOCH})" local p for p in "${paths[@]}"; do if [[ -f "$ROOT/$p" ]] && ! monitor_is_fresh "$p"; then @@ -1300,6 +1997,8 @@ wait_for_monitors() { for p in "${paths[@]}"; do echo " ready: $p" done + wait_state_finish ready "${paths[@]}" + maybe_run_advisor "${paths[@]}" exit 0 fi now="$(date +%s)" @@ -1315,11 +2014,16 @@ wait_for_monitors() { echo " missing: $p" fi done + wait_state_finish timeout "${paths[@]}" exit 3 fi remaining=$((timeout - elapsed)) if [[ $((elapsed % 30)) -eq 0 ]]; then - echo "audits: wait-monitor still waiting (${elapsed}s elapsed, ${remaining}s left)" + local _ak_suf="" + if declare -F audit_kit_suffix >/dev/null 2>&1; then + _ak_suf="$(audit_kit_suffix "$elapsed" || true)" + fi + echo "audits: wait-monitor still waiting (${elapsed}s elapsed, ${remaining}s left this slice)${_ak_suf}" fi sleep 1 done @@ -1335,16 +2039,23 @@ print_wait_monitor_dry_run() { echo " progress-timeout: ${PROGRESS_TIMEOUT}s" echo " progress-gate-channel: resolved at spawn time (tmux/screen sampled; other channels advisory)" if [[ "$WAIT_MONITOR" -eq 1 ]]; then + wait_state_load_or_init "${paths[@]}" if [[ -z "${WAIT_ARM_EPOCH:-}" ]]; then WAIT_ARM_EPOCH="$(date +%s)" fi echo " wait-monitor: yes" echo " wait-timeout: ${WAIT_TIMEOUT}s" + echo " wait-slice: ${WAIT_SLICE}s" + echo " wait-remaining: ${WAIT_REMAINING:-$WAIT_TIMEOUT}s" + echo " wait-resume: $([[ "$WAIT_RESUME" -eq 1 ]] && echo yes || echo no)" echo " wait-arm-epoch: ${WAIT_ARM_EPOCH}" + echo " wait-state-dir: ${WAIT_STATE_DIR_REL}" echo " wait-freshness: mtime>=arm-epoch or " - local p + local p slug for p in "${paths[@]}"; do + slug="$(slug_from_monitor_path "$p")" echo " wait-path: $p" + echo " wait-state: ${WAIT_STATE_DIR_REL}/${slug}.json" if [[ -f "$ROOT/$p" ]]; then if monitor_is_fresh "$p"; then echo " wait-path-status: fresh (would ready)" @@ -1404,6 +2115,9 @@ if [[ "$REAP_SESSIONS" -eq 1 && "$BATCH" -eq 0 && "${#PLAN_ARGS[@]}" -eq 0 ]]; t exit 0 fi +apply_wait_budget_defaults +resolve_reviewer_backend + # Arm epoch before spawn/poll so pre-existing monitors are not false-ready. if [[ "$WAIT_MONITOR" -eq 1 && -z "${WAIT_ARM_EPOCH:-}" ]]; then WAIT_ARM_EPOCH="$(date +%s)" @@ -1420,14 +2134,23 @@ if [[ "$WAIT_MONITOR" -eq 1 && "$MODE_EXPLICIT" -eq 0 ]]; then POLL_ONLY=1 MODE="paste-only" fi +maybe_resume_from_plan_args +resolve_review_models if [[ "$WAIT_MONITOR" -eq 1 && ( "$MODE" == "interactive" || "$MODE" == "print" ) && "$MODE_EXPLICIT" -eq 1 ]]; then echo "tip: --wait-monitor is ignored with --interactive/--print (process is replaced by claude)" >&2 fi -# paste-only / dry-run / poll-only do not require claude on PATH. +# paste-only / dry-run / poll-only do not require a reviewer binary. +# backend=auto uses Cursor Agent when Claude is missing or quota-empty. +# Cursor tick API/usage-limit hard-stop is a different path (do not reuse here). if [[ "$MODE" != "paste-only" && "$DRY_RUN" -ne 1 && "$POLL_ONLY" -ne 1 ]]; then - if ! command -v claude >/dev/null 2>&1; then - tip_no_claude + if [[ "$REVIEWER_BACKEND" == "none" ]]; then + local_want="$(config_review_backend)" + if [[ "$REVIEWER_BACKEND_EXPLICIT" -eq 0 && "$local_want" == "claude" ]]; then + tip_no_claude + else + tip_no_reviewer + fi soft_fail_exit fi fi @@ -1437,6 +2160,19 @@ if [[ "$FORCE" -eq 1 ]]; then INTERACTIVE_FLAGS+=(--force) fi INTERACTIVE_FLAGS+=(--interactive) +if [[ "$REVIEWER_BACKEND" == "claude" || "$REVIEWER_BACKEND" == "cursor" ]]; then + INTERACTIVE_FLAGS+=(--backend "$REVIEWER_BACKEND") +fi +if [[ -n "${REVIEWER_MODEL:-}" ]]; then + INTERACTIVE_FLAGS+=(--reviewer-model "$REVIEWER_MODEL") +fi +if [[ -n "${WAIT_IMPLEMENTER_MODEL:-}" ]]; then + INTERACTIVE_FLAGS+=(--implementer-model "$WAIT_IMPLEMENTER_MODEL") +fi +if [[ -n "${ADVISOR_MODEL:-}" ]]; then + INTERACTIVE_FLAGS+=(--advisor-model "$ADVISOR_MODEL") +fi +enforce_implementer_reviewer_split if [[ "$BATCH" -eq 1 ]]; then if [[ ${#PLAN_ARGS[@]} -eq 0 ]]; then @@ -1472,6 +2208,11 @@ if [[ "$BATCH" -eq 1 ]]; then echo " plans: ${BATCH_BASENAMES[*]}" echo " head: $HEAD_SHA" echo " mode: $MODE (config mode: ${CONFIG_MODE_RAW:-missing/legacy})" + echo " reviewer-backend: ${REVIEWER_BACKEND:-unresolved}" + echo " reviewer-model: ${WAIT_REVIEWER_MODEL:-unresolved}" + echo " implementer-model: ${WAIT_IMPLEMENTER_MODEL:-auto}" + echo " advisor-model: ${ADVISOR_MODEL:-opus}" + echo " same-model-refuse: $([[ "$SAME_MODEL_REFUSE" -eq 1 ]] && echo yes || echo no)" echo " midBatchAudits: $MID_BATCH_AUDITS" echo " autoRemediate: $AUTO_REMEDIATE" if [[ "$DRY_RUN" -eq 1 ]]; then @@ -1533,12 +2274,10 @@ EOF exit 0 ;; interactive) - cd "$ROOT" - exec claude --permission-mode "$CLAUDE_PERMISSION_MODE" "$PROMPT" + exec_reviewer ;; print) - cd "$ROOT" - exec claude --permission-mode "$CLAUDE_PERMISSION_MODE" -p "$PROMPT" + exec_reviewer -p ;; *) echo "error: internal mode bug: $MODE" >&2 @@ -1565,6 +2304,11 @@ echo "audits / external plan review: prepared" echo " plan: $PLAN_REL" echo " head: $HEAD_SHA" echo " mode: $MODE (config mode: ${CONFIG_MODE_RAW:-missing/legacy})" +echo " reviewer-backend: ${REVIEWER_BACKEND:-unresolved}" +echo " reviewer-model: ${WAIT_REVIEWER_MODEL:-unresolved}" +echo " implementer-model: ${WAIT_IMPLEMENTER_MODEL:-auto}" +echo " advisor-model: ${ADVISOR_MODEL:-opus}" +echo " same-model-refuse: $([[ "$SAME_MODEL_REFUSE" -eq 1 ]] && echo yes || echo no)" echo " midBatchAudits: $MID_BATCH_AUDITS" echo " autoRemediate: $AUTO_REMEDIATE" echo " permission mode: $CLAUDE_PERMISSION_MODE" @@ -1573,6 +2317,11 @@ if command -v claude >/dev/null 2>&1; then else echo " claude: (not on PATH yet)" fi +if command -v cursor-agent >/dev/null 2>&1; then + echo " cursor-agent: $(command -v cursor-agent)" +else + echo " cursor-agent: (not on PATH yet)" +fi SINGLE_MONITOR_PATH="$(monitor_path_for_plan_base "$SINGLE_BASE")" if [[ "$DRY_RUN" -eq 1 ]]; then echo " dry-run: yes (no spawn)" @@ -1636,15 +2385,10 @@ EOF exit 0 ;; interactive) - cd "$ROOT" - # Interactive session in Cursor terminal (user continues the review). - exec claude --permission-mode "$CLAUDE_PERMISSION_MODE" "$PROMPT" + exec_reviewer ;; print) - cd "$ROOT" - # Verified Claude Code flag: -p/--print (non-interactive). Do not invent other flags. - # Headless / CI only. Chat agents must use --autonomous or --paste-only, never silent -p. - exec claude --permission-mode "$CLAUDE_PERMISSION_MODE" -p "$PROMPT" + exec_reviewer -p ;; *) echo "error: internal mode bug: $MODE" >&2 diff --git a/.cursor/skills/community/mission-kit-comms/SKILL.md b/.cursor/skills/community/mission-kit-comms/SKILL.md new file mode 100644 index 0000000..7d796d0 --- /dev/null +++ b/.cursor/skills/community/mission-kit-comms/SKILL.md @@ -0,0 +1,78 @@ +--- +name: mission-kit-comms +description: "Draft Mission Kit adoption posts (recap, release, contributor-ask) with dual-name, claim checks, per-channel checklists, and HITL-before-post. Use when spreading the word, social copy, changelog recaps, or community replies." +version: 0.1.0 +category: ux +--- + +# Mission Kit comms (spread the word) + +Draft public copy for **Mission Kit** adoption. Never publish or reply on a network without an explicit operator Ask (clickable Ask questions, or numbered-list fallback). + +Thin reuse: short blocks and one ask per message from sibling skill `ux-message-flows`; prompt structure from sibling `prompts-markdown`. Do not duplicate those skills. Do not treat Agent Personas as this skill. + +## Dual-name + +| Use | Name | +|-----|------| +| Product / marketing / site | Mission Kit / missionkit.io | +| CLI, npm, slash, pack | Agent Kit / `agent-kit` / `@dadado/agent-kit-cli` | +| Dashboard | Mission Control | + +## Claim sources (indicative docs; verify shipped) + +- `docs/five-layer-claim-matrix.md` +- `docs/getting-started.md` +- `docs/public-launch-announcement.md` +- Channel map: `docs/comms-channel-map.md` +- Calendar: `docs/comms-content-calendar.md` +- Product version floor: **5.0.0**. Do not write 4.x as current. + +Forbidden in drafts: full autonomy without HITL; Cursor Marketplace "listed" unless the parked submit plan has actually listed; npm/CLI renamed to Mission Kit; silent posting. + +## HITL-before-post + +1. Draft only (chat, plan, or `.cursor/comms-drafts/` via `.cursor/scripts/comms-draft.mjs`). +2. Ask: `Post this` / `Edit draft` / `Discard`. +3. Skip or cancel = do not post. +4. Secrets stay out of git. Env names: `docs/comms-publish-pipeline.md`. + +## Per-channel checklist + +Copy the channel row, then tick before Ask. + +### X / Twitter + +- [ ] Two short blocks or fewer; one link (missionkit.io or GitHub) +- [ ] Dual-name if install is mentioned +- [ ] No thread that claims unshipped CHANGELOG items + +### Medium / newsletter / Substack + +- [ ] Recap or release template filled from CHANGELOG / Release +- [ ] HITL positioning sentence present +- [ ] Commercial: PolyForm NC + sales@missionkit.io + +### Hacker News + +- [ ] Tied to a real release or Show HN, not a daily dump +- [ ] Title does not say "autonomous agent that ships to prod alone" + +### GitHub (issues / PR) + +- [ ] Contributor funnel: CONTRIBUTING, contribute-upstream, CoC, SECURITY private path +- [ ] Not a Marketplace submit CTA +- [ ] Promotional maintainer replies still need Ask + +### Slack / Discord + +- [ ] Deferred unless an operator-owned workspace exists +- [ ] Same HITL as other networks + +### GitHub Discussions + +- [ ] Do not post; Discussions is not enabled (`.github/SUPPORT.md`) + +## Output + +Return: channel, draft body, claim-check result, **HITL pending**. Do not call network APIs. diff --git a/.gitignore b/.gitignore index a9d930d..4438cc9 100644 --- a/.gitignore +++ b/.gitignore @@ -16,6 +16,10 @@ .cursor/context/flight-log.json # Local Field Report activity cadence ledger (tick/batch counters) .cursor/context/field-report-cadence.json +# Local audit wait-state (arm epoch / remaining budget; launcher-owned) +.cursor/context/audit-wait/ +# Local comms drafts (HITL publish; never commit) +.cursor/comms-drafts/ .cursor/agent-kit.config.json # Active Context Packs (are local) diff --git a/CHANGELOG.md b/CHANGELOG.md index b1fa784..05ace5d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -# Changelog - Agent Kit +# Changelog - Mission Kit All notable changes to this project are documented in this file. @@ -8,6 +8,36 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and ## [Unreleased] +## [5.1.0] - 2026-08-13 + +### Added + +- CLI visual kit: in-process ANSI spinner frames, space marks, and rotating Mission Kit tips on welcome, grouped help, long-running commands, run-plan ticks, and audits wait heartbeats. Motion off under `NO_COLOR`, `CI`, non-TTY, and `AGENT_KIT_REDUCED_MOTION=1`. Runtime deps stay `@clack/prompts`, `citty`, `kolorist`. See `.cursor/memory/decisions/2026-08-13_cli-visual-kit-space-chrome.md`. +- Mission Kit adoption comms loop (community skill `mission-kit-comms`, agent `.cursor/agents/mission-kit-comms.md`, channel map/calendar/templates, fail-closed local draft script). Drafts only; HITL Ask before any public post. Not Core Pack; not auto-posting; Cursor Marketplace submit stays parked. +- Audits atomic wait: launcher persists arm epoch / deadline / remaining budget in `.cursor/context/audit-wait/.json` (not HANDOFF). Chat AwaitShell uses `waitSliceSeconds` (default 90); CI/headless may use the full remaining `waitTimeoutSeconds` (default 900). Early-ready and freshness `0|3|4` stay. See `.cursor/memory/decisions/2026-08-13_audits-atomic-wait-reviewer-fallback.md`. +- Audits reviewer cascade: `backend: "auto"` uses Claude when usable, else Cursor Agent writing the same `plan-monitor-*.md` contract. Pinned `backend: "claude"` keeps tip+no-op when Claude is missing. Claude quota empty (`AGENT_KIT_AUDIT_CLAUDE_QUOTA_EMPTY`) is not the Cursor tick API/usage-limit hard-stop. +- Audits model routing: Claude review spawn uses `reviewerModel` (default `haiku`); `advisorModel` (default `opus`) runs only when the monitor marks ``. Implementer model is stamped at arm (`--implementer-model` / `AGENT_KIT_AUDIT_IMPLEMENTER_MODEL`, default `auto`). Same-family reviewer is refused (including Auto/Auto). The reviewer prompt is a findings-contract against the git delta plus the plan, not a second implement pass. +- ADR: Claude Code dynamic workflows and ultracode stay Claude-native. Agent Kit records a docs-only thin adapter (no ultracode runner, no `--backend claude` ticks, distinct from kit-load and audits). See `.cursor/memory/decisions/2026-08-13_claude-cli-ultracode-orchestration-thin-adapter.md`. +- Claude CLI kit-load: pack contract (`docs/claude-cli-kit-load.md`), generator snippets, factory `CLAUDE.md` + `/agent-kit`, and getting-started / audits / A7 boundary. Install emits root `CLAUDE.md` and `.claude/commands/agent-kit.md` (skip if present). Distinct from audits, `--backend claude` ticks, and Action A7 (ADR `2026-08-13_claude-cli-kit-load-bootstrap.md`). + +### Changed + +- Mission Control Config audits backend is no longer Claude-only: `auto` / `claude` / `cursor`, plus writable `reviewerModel`, `advisorModel`, `waitSliceSeconds`, and `waitTimeoutSeconds`. L0 `/run-plan`, `/run-plan-all`, and `/plan-external-review` document the wait-slice and reviewer cascade. Dogfood `cursor_audit_cursor_auto_fallback_20260813.md` is Processed with a Fix-now pointer to the atomic-wait ADR. +- `/git-prod` documents the authorized post-Ask push form `ALLOW_MAIN_PUSH=1 git push origin main` (inline prefix, not a session export). Bare `git push origin main` stays denied. CLI guard already shipped; this closes the L0 gap for agent-kit-startup/agent-kit#38. +- Factory dogfood bridge for git-staging dirty-tree spine drift (public issue #41). Acceptance is against factory `staging` wording; public tree updates on the next promote. +- `/handoff` and the git-workflow rule now point at the same `/git-staging` inventory → theme-bucket SoT (`autogit/gitupdate.md`). Loop/orchestrated aliases already follow `/run-plan` closeout. Factory has no `/recupera-staging` command file. +- `/run-plan` tick closeout no longer skips "trivial" HANDOFF/memory diffs. Those paths ship as a `docs(memory):` / `chore(kit):` bucket via `/git-staging`; broad `git add` of unrelated monitor WIP into a product commit stays forbidden. +- `/git-staging` inventories the dirty tree and ships every safe theme-bucket (product vs `docs(memory):` / `chore(kit):`) instead of stop-and-quiz when soft kit paths look "out of the current flow." Warn/add-by-name still forbids sweeping monitor WIP into a product commit. Ask HITL stays on `/git-prod`. Public issue #41. +- Naming glossary tighten (ADR `2026-08-06`): Mission Kit / MissionKit = product; Agent Kit = CLI, npm, slash commands, install manifest, and pack only; Mission Control = dashboard shell and tabs. Residual product-voice Agent Kit openers updated (CHANGELOG H1, bootstrap, capability-inventory, cursor-3-features, getting-started, docs index, DEVELOPMENT naming table). npm/CLI/slash/manifest identifiers and marketplace `displayName` unchanged. +- Post-publish D2 dogfood closed: blank-folder `npx @dadado/agent-kit-cli@5.0 install` against npm `latest` 5.0.0 passed five assertions + Path C dashboard HTTP 200; checklist After-publish rows and ship-5.0 npm/npx monitor R15 Closed-by updated (no retag). +- Forward note (do not rewrite `[5.0.0]`): the released evidence-gate "green again" line overstates a staging-red. CI at the predecessor SHA already had Evidence checks success; the red was a local worktree with untracked monitors. +- `docs/evidence/npm-pack-5.0.0-2026-08-11/` is a pre-tag proxy (18 packed files) and is superseded by published npm `5.0.0` (`dist.fileCount` 23, shasum `203db8ec…`). See that directory README. The JSON is left as history, not rewritten. + +### Fixed + +- `agent-kit update --check` honors `--registry` (local checkout): compare against that source's version and L0 drift instead of the public latest tag; JSON `registryUrl` / `registryRef` report the resolved source (agent-kit-startup/agent-kit#37). +- `agent-kit dashboard-broadcast` window and process title used the CLI package folder `cli` when snapshot env was missing. Spawn now sets `MISSION_CONTROL_REPO_ROOT` and titles from the workspace basename. + ## [5.0.0] - 2026-08-12 ### Fixed diff --git a/autogit/gitupdate.md b/autogit/gitupdate.md index 77206d7..37f61c9 100644 --- a/autogit/gitupdate.md +++ b/autogit/gitupdate.md @@ -56,8 +56,10 @@ git staging - `feat:` New feature - `fix:` Bug fix - `docs:` Documentation +- `docs(memory):` Plan-monitors, Audits index rows, memory ADRs - `refactor:` Refactoring - `chore:` Maintenance tasks +- `chore(kit):` Kit-command / HANDOFF-adjacent hygiene (own bucket when the product theme differs) --- @@ -246,7 +248,15 @@ This section contains the detailed prompts that should be followed when commands #### 1. **Security Validation** - Run `git status -sb` to check modified, staged files and current branch. - **BLOCK**: If attempting to commit directly to `main` or `origin/main`, BLOCK and inform: "Direct commits to main are blocked. Use 'git staging' to promote changes via staging." - - If there are uncommitted local changes that don't belong to the current flow, stop and request instructions. + - **Inventory the dirty tree** (do not stop-and-quiz because a path looks "out of the current flow"): + 1. List every uncommitted path. + 2. **Hard exclude** (never stage): secrets, PII, `.env` / keys / credentials, design-source dumps that belong in gitignore. + 3. **Theme-bucket** the rest: + - **Product:** the current feature/fix/docs theme (`feat:`, `fix:`, `refactor:`, `docs:`, `test:`, `perf:`). + - **Kit/memory:** versioned HANDOFF (when tracked), intentional plan files, `plan-monitor-*.md`, `_index.md` Audits rows (`docs(memory):` or `chore(kit):`). + 4. Ship every **safe** bucket add-by-name (product commit first, then kit/memory as its own commit when the theme differs). Same PR is allowed; prefer a separate commit when the product diff is large. + 5. After all safe buckets ship, the tree must be clean except hard excludes. Leftover safe dirt means the inventory is incomplete: bucket and ship, do not abandon. + 6. **Ask HITL stays on `/git-prod`** (and on true risk: secrets, PII, ambiguous product scope). Do **not** invent a theme-Ask stop for routine dirty soft kit paths. #### 2. **Check and Update CHANGELOG.md** - Check if `CHANGELOG.md` exists and read its content. @@ -278,14 +288,20 @@ This section contains the detailed prompts that should be followed when commands - **Lint evidence (staging-ready):** when the diff touches formatted/linted paths (e.g. `*.ts` / `*.tsx` / `*.js` / `*.mjs` under `packages/` or other Biome/ESLint scopes), **run** the focused linter on those files (e.g. `pnpm exec biome check `) **before** commit and **record the exact command + pass/fail output** in the tick / worker summary. Claiming `Staging ready: yes` or pasting the contract phrase without that recorded run is invalid. **`dashboard/dashboard.html` is outside Biome** (`biome check dashboard/dashboard.html` processes nothing): for dashboard CSS/HTML-only diffs, record `Tests: none applicable (dashboard-CSS); covered by plugin-ux-validation` when the UX suite pins the change (ADR `decisions/2026-07-29_dashboard-css-lint-evidence-convention.md`); do not claim Biome covered the HTML. Pure markdown / docs-only with no applicable repo linter: record `Tests: none applicable` (or `Validation: none applicable`). Aligns with `/run-plan` Staging-ready lint gate (background: Biome-red merges fixed only after the fact). #### 7. **Stage and commit with semantic message** - - Add relevant files with `git add` **by name**. If `git status` shows untracked or unrelated dirty `.cursor/memory/plan-monitor-*.md`, **warn** and do **not** broad-`git add` `.cursor/memory/` WIP into a product commit (ADR `decisions/2026-07-27_plan-monitor-consumer-awareness.md`). - - **Monitor closeout (R14):** when a tick intentionally stages a `plan-monitor-*.md` (and/or `_index.md` Audits row), add those paths **by name**. Prefer a separate docs/memory commit when the same PR also has large product diffs. Never sweep unrelated monitor WIP. **An `_index.md` Audits row and its target monitor file must land in the same commit** (no index link without the file). Product commits must not pick up unrelated untracked monitors (dogfood residual R7). ADR: `decisions/2026-07-29_plan-monitor-staging-hygiene-r14-r15.md`. + - Add relevant files with `git add` **by name**. Never broad-`git add` `.cursor/memory/` WIP into a product commit (ADR `decisions/2026-07-27_plan-monitor-consumer-awareness.md`). + - If `git status` shows untracked or unrelated dirty `.cursor/memory/plan-monitor-*.md`, **warn**, then **bucket** (warn is not leave-dirty-forever): + - Closeout evidence for **this** product change: add-by-name into the product commit only when it is that change's monitor (still prefer a separate `docs(memory):` commit when the product diff is large). + - Remaining **safe** monitors, versioned HANDOFF, and kit docs: ship in a `docs(memory):` or `chore(kit):` commit in this `/git-staging` run. Unrelated-plan monitors still get their own kit/memory bucket; they do not ride along inside the product commit. + - Hard excludes stay unstaged. + - **Monitor closeout (R14):** when a tick intentionally stages a `plan-monitor-*.md` (and/or `_index.md` Audits row), add those paths **by name**. Prefer a separate docs/memory commit when the same PR also has large product diffs. Never sweep unrelated monitor WIP into a product commit. **An `_index.md` Audits row and its target monitor file must land in the same commit** (no index link without the file). Product commits must not pick up unrelated untracked monitors (dogfood residual R7). ADR: `decisions/2026-07-29_plan-monitor-staging-hygiene-r14-r15.md`. - Create a commit following [Conventional Commits](https://www.conventionalcommits.org/): - `feat:` for new features - `fix:` for bug fixes - `docs:` for documentation + - `docs(memory):` for plan-monitors, `_index.md` Audits rows, memory ADRs - `refactor:` for refactoring - `chore:` for maintenance tasks + - `chore(kit):` for kit-command / HANDOFF-adjacent hygiene that is not the product theme - Example: `git commit -m "feat: add support for new agent"` or `git commit -m "fix: correct CPF validation"` - If CHANGELOG.md was updated, mention it in the commit: `git commit -m "feat: add new agent\n\nUpdate CHANGELOG.md with new version"` @@ -302,7 +318,7 @@ This section contains the detailed prompts that should be followed when commands - Delete the remote branch on origin with `git push origin --delete update/<...>` or `git push origin --delete feature/<...>`. - Delete the local branch with `git branch -D update/<...>` or `git branch -D feature/<...>` (use `-D` since merge was done remotely and Git might not detect locally). - Update local `staging` with `git pull --ff-only origin staging` to incorporate merged changes. - - Confirm clean state with `git status -sb`. + - Confirm clean state with `git status -sb`. If leftover **safe** dirt remains (HANDOFF, monitors, kit docs), return to §1 inventory and ship the remaining bucket. Hard excludes may remain unstaged. #### 10.5. **Project manager — optional** - **If** the project has PM tool MCP/skill configured: update related task status (e.g., "in staging"). If the user indicated task(s) or there's context in handoff/plan, update them. No tool or no tasks: skip without warning. diff --git a/dashboard/dashboard.html b/dashboard/dashboard.html index a63604d..57db20d 100644 --- a/dashboard/dashboard.html +++ b/dashboard/dashboard.html @@ -5606,9 +5606,31 @@
- claude is the only backend today. + auto uses Claude when usable, else Cursor Agent. Pin claude or cursor to force one backend. +
+
+ + + Claude default haiku. Must differ from the implementer stamp (Auto/Auto refused). +
+
+ + + Opus only when the monitor marks high severity or uncertainty. +
+
+ + + Chat AwaitShell budget per session. Default 90. +
+
+ + + Total remaining budget across slices and CI. Default 900.
@@ -5680,6 +5702,18 @@ offerOnExhausted: !!document.getElementById('config-epr-offer')?.checked, autoRemediate: !!document.getElementById('config-epr-auto')?.checked, backend: document.getElementById('config-epr-backend')?.value || 'claude', + reviewerModel: (document.getElementById('config-epr-reviewerModel')?.value || 'haiku').trim(), + advisorModel: (document.getElementById('config-epr-advisorModel')?.value || 'opus').trim(), + waitSliceSeconds: (() => { + const raw = document.getElementById('config-epr-waitSlice')?.value; + const n = Number.parseInt(String(raw || '90'), 10); + return Number.isFinite(n) && n >= 1 ? n : 90; + })(), + waitTimeoutSeconds: (() => { + const raw = document.getElementById('config-epr-waitTimeout')?.value; + const n = Number.parseInt(String(raw || '900'), 10); + return Number.isFinite(n) && n >= 1 ? n : 900; + })(), mode: document.getElementById('config-epr-mode')?.value || 'paste', midBatchAudits: !!document.getElementById('config-epr-midBatch')?.checked, preflight: document.getElementById('config-epr-preflight')?.value || 'off', diff --git a/dashboard/lib/guards.mjs b/dashboard/lib/guards.mjs index ac828d2..a5c6e53 100644 --- a/dashboard/lib/guards.mjs +++ b/dashboard/lib/guards.mjs @@ -445,7 +445,10 @@ export const CONFIG_PERSONA_IDS = Object.freeze(["autopilot", "night-shift", "gh export const CONFIG_PERSONA_MODES = Object.freeze(["continue-plan", "run-plan", "cli-run-plan"]); /** Allowed externalPlanReview.backend values. */ -export const CONFIG_REVIEW_BACKENDS = Object.freeze(["claude"]); +export const CONFIG_REVIEW_BACKENDS = Object.freeze(["auto", "claude", "cursor"]); + +/** Named reviewer / advisor / implementer model id (no secrets). */ +const REVIEW_MODEL_ID = /^[A-Za-z0-9._:+-]{1,64}$/; /** Allowed externalPlanReview.mode values (audits arming path). */ export const CONFIG_REVIEW_MODES = Object.freeze(["paste", "autonomous"]); @@ -591,6 +594,10 @@ export function validateConfigWriteBody(body) { const eprAllowed = new Set([ "enabled", "backend", + "reviewerModel", + "advisorModel", + "waitSliceSeconds", + "waitTimeoutSeconds", "autoRemediate", "offerOnExhausted", "mode", @@ -612,10 +619,45 @@ export function validateConfigWriteBody(body) { } if ("backend" in epr) { if (typeof epr.backend !== "string" || !CONFIG_REVIEW_BACKENDS.includes(epr.backend)) { - return { ok: false, error: "externalPlanReview.backend must be a known backend" }; + return { ok: false, error: "externalPlanReview.backend must be auto, claude, or cursor" }; } eprPatch.backend = epr.backend; } + if ("reviewerModel" in epr) { + if ( + typeof epr.reviewerModel !== "string" || + !REVIEW_MODEL_ID.test(epr.reviewerModel.trim()) + ) { + return { ok: false, error: "externalPlanReview.reviewerModel must be a model id" }; + } + eprPatch.reviewerModel = epr.reviewerModel.trim(); + } + if ("advisorModel" in epr) { + if (typeof epr.advisorModel !== "string" || !REVIEW_MODEL_ID.test(epr.advisorModel.trim())) { + return { ok: false, error: "externalPlanReview.advisorModel must be a model id" }; + } + eprPatch.advisorModel = epr.advisorModel.trim(); + } + if ("waitSliceSeconds" in epr) { + const n = epr.waitSliceSeconds; + if (typeof n !== "number" || !Number.isInteger(n) || n < 1 || n > 3600) { + return { + ok: false, + error: "externalPlanReview.waitSliceSeconds must be an integer 1..3600", + }; + } + eprPatch.waitSliceSeconds = n; + } + if ("waitTimeoutSeconds" in epr) { + const n = epr.waitTimeoutSeconds; + if (typeof n !== "number" || !Number.isInteger(n) || n < 1 || n > 86400) { + return { + ok: false, + error: "externalPlanReview.waitTimeoutSeconds must be an integer 1..86400", + }; + } + eprPatch.waitTimeoutSeconds = n; + } if ("autoRemediate" in epr) { if (typeof epr.autoRemediate !== "boolean") { return { ok: false, error: "externalPlanReview.autoRemediate must be boolean" }; @@ -847,6 +889,18 @@ export function allowlistConfig(raw) { if (typeof raw.externalPlanReview.preflight === "string") { epr.preflight = truncateStr(raw.externalPlanReview.preflight, 16); } + if (typeof raw.externalPlanReview.reviewerModel === "string") { + epr.reviewerModel = truncateStr(raw.externalPlanReview.reviewerModel, 64); + } + if (typeof raw.externalPlanReview.advisorModel === "string") { + epr.advisorModel = truncateStr(raw.externalPlanReview.advisorModel, 64); + } + if (typeof raw.externalPlanReview.waitSliceSeconds === "number") { + epr.waitSliceSeconds = raw.externalPlanReview.waitSliceSeconds; + } + if (typeof raw.externalPlanReview.waitTimeoutSeconds === "number") { + epr.waitTimeoutSeconds = raw.externalPlanReview.waitTimeoutSeconds; + } summary.externalPlanReview = epr; } if (raw.agentPersona && typeof raw.agentPersona === "object") { diff --git a/dashboard/start-broadcast.mjs b/dashboard/start-broadcast.mjs index bdceba6..ba75008 100644 --- a/dashboard/start-broadcast.mjs +++ b/dashboard/start-broadcast.mjs @@ -9,7 +9,7 @@ import { execFileSync, execSync, spawn } from "node:child_process"; import { existsSync, openSync } from "node:fs"; -import { dirname, join } from "node:path"; +import { basename, dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; import { buildBroadcastShareUrl, @@ -35,6 +35,7 @@ const __dirname = dirname(fileURLToPath(import.meta.url)); const KIT_ROOT = join(__dirname, ".."); /** Workspace snapshots / preference config. Defaults to KIT_ROOT. */ const ROOT = resolveSnapshotRepoRoot(process.env, KIT_ROOT); +process.title = `Mission Control · ${basename(ROOT) || "workspace"}`; const SERVE = join(__dirname, "serve.mjs"); const LOG = process.env.MISSION_CONTROL_LOG || "/tmp/mission-control-broadcast.log"; const PORT = Number.parseInt(process.env.PORT || "3333", 10); diff --git a/dashboard/start.mjs b/dashboard/start.mjs index 4c1480e..658fcfa 100644 --- a/dashboard/start.mjs +++ b/dashboard/start.mjs @@ -18,7 +18,7 @@ import { execFileSync, execSync, spawn } from "node:child_process"; import { existsSync, openSync } from "node:fs"; -import { dirname, join, resolve } from "node:path"; +import { basename, dirname, join, resolve } from "node:path"; import { fileURLToPath } from "node:url"; import { REPO_ROOT_ENV, @@ -34,6 +34,7 @@ import { openBrowser, readPreferredBrowserFromConfig } from "./lib/open-browser. const __dirname = dirname(fileURLToPath(import.meta.url)); const KIT_ROOT = join(__dirname, ".."); const ROOT = resolveSnapshotRepoRoot(process.env, KIT_ROOT); +process.title = `Mission Control · ${basename(ROOT) || "workspace"}`; const SERVE = join(__dirname, "serve.mjs"); const HOST = process.env.HOST || "127.0.0.1"; const DISPLAY_HOST = HOST === "0.0.0.0" ? "127.0.0.1" : HOST; diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index 9bc9f8d..1c4e119 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -28,6 +28,8 @@ New to the kit? Here's where things land and how to test before your PR: See [getting-started.md](getting-started.md) for the consumer workflow after install. +Optional: `agent-kit add mission-kit-comms` installs the adoption-comms skill (draft recap/release copy with HITL before any public post). Not part of Core Pack. Guide: [comms.md](comms.md). + ## Standards - Conventional Commits @@ -100,6 +102,19 @@ pnpm --filter @dadado/agent-kit-cli start -- contribute \ See [contribute-upstream.md](contribute-upstream.md). Registry contributions now target the **public** repo as Phase B is complete - [topology-private-public.md](topology-private-public.md). +## Contributor funnel (issues to PR to upstream) + +1. **Question or bug:** GitHub issue forms (Discussions is not enabled; see [SUPPORT.md](../.github/SUPPORT.md)). +2. **Code or skill PR:** this guide; public repo targets `main`, factory targets `staging`. +3. **Consumer-project drift:** [`agent-kit contribute`](contribute-upstream.md) (CLI never pushes). +4. **Vulnerability:** [SECURITY.md](../.github/SECURITY.md) private channel only. + +Maintainer replies that are promotional still need HITL (skill `mission-kit-comms`). Cursor Marketplace publisher submit is not this funnel. + +## Telling people about Mission Kit + +Drafts and cadence live in [comms.md](comms.md). The launch paste in [public-launch-announcement.md](public-launch-announcement.md) remains valid seed copy. Nothing in the kit posts to social networks by itself. + ## Contribution license (follow-on) Inbound CLA/DCO terms for a paid-commercial PolyForm NC project are a tracked follow-on (polyform residual E). Until that ships, pull requests are welcome under the repository LICENSE for noncommercial contribution review, and maintainers may request a CLA before merging commercial-impact changes. diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index ce0a7d9..2e755a9 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -91,17 +91,18 @@ Published CLI packs `dashboard/**` from 4.8.2 onward; consumers normally run `ag Release and sync ops: [public-launch.md](public-launch.md), [npm-publish-checklist.md](npm-publish-checklist.md). -## Mission Kit vs Agent Kit naming +## Mission Kit / Agent Kit / Mission Control naming -Public marketing uses **Mission Kit** (missionkit.io hero, README first screen, consumer product-family prose). Install and runtime identifiers stay **Agent Kit** / `agent-kit`. Blind find-replace either way breaks install truth or storefront positioning. +Public marketing uses **Mission Kit** / **MissionKit** (missionkit.io hero, README first screen, consumer product-family prose). Install and runtime identifiers stay **Agent Kit** / `agent-kit`. **Mission Control** is the dashboard shell and tabs only. Blind find-replace either way breaks install truth, storefront positioning, or dashboard voice. | Surface | Prefer | Notes | |---------|--------|-------| | missionkit.io hero / SEO | Mission Kit | External design SoT; do not hand-edit `landing-missionkit/remote/` | | Root README storefront | Mission Kit framing | Keep install/CLI names as Agent Kit / `agent-kit` | -| Consumer docs | Mission Kit for the product family; Agent Kit when naming CLI or installed kit | One sentence can introduce both | +| Consumer docs | Mission Kit for the product family; Agent Kit when naming the CLI or pack | One sentence can introduce both | | npm / CLI / npx | `@dadado/agent-kit-cli`, `agent-kit` | Never rename in docs alone | -| Slash commands, `.cursor/agent-kit.json` | Agent Kit identifiers | Literal command and file names | +| Slash commands, `.cursor/agent-kit.json`, agent pack | Agent Kit identifiers | Literal command, file, and pack names | +| Dashboard shell and tabs | Mission Control | Never use Mission Control as the product or CLI name | | Commercial contact | `sales@missionkit.io` | PolyForm Noncommercial path | | Legacy `agent.startupkit.com.br`, `landing-agentkit/` | Historical / rollback-only | Qualify when linked | diff --git a/docs/README.md b/docs/README.md index 6989da0..b7dd2e9 100644 --- a/docs/README.md +++ b/docs/README.md @@ -11,6 +11,7 @@ - [Contribute upstream](contribute-upstream.md) - `agent-kit contribute` return channel + gate - [Public launch](public-launch.md) - go/no-go + append-only sync - [Public launch announcement](public-launch-announcement.md) - copy-paste launch text (chat / social) +- [Adoption comms](comms.md) - channel map, recap/release cadence, HITL draft pipeline (does not auto-post) - [Mission Kit Landing](agentkit-landing.md) - public marketing page at [missionkit.io](https://missionkit.io) (filename kept for link stability; previous `agent.startupkit.com.br` is deprecated and 301-redirects) - [Topology private × public](topology-private-public.md) - Fase 7 registry-canonical public - [Marketplace catalog](marketplace.md) - versioning, CLI add, Cursor plugin, quality gate @@ -19,9 +20,10 @@ - [Creating Agent Personas](creating-personas.md) - persona pack format, placement, contribute checklist - [Agent Personas contract](personas-contract.md) - persona pack schema, mode defaults, acceptance rules (also summarized in the root [README Features](../README.md#features)) - [External plan review](external-plan-review.md) - opt-in Claude Code monitor after plan exhaustion +- [Claude CLI kit-load](claude-cli-kit-load.md) - thin `CLAUDE.md` plus `/agent-kit` session bootstrap (not audits, not A7) - [Consumer configuration](consumer-configuration.md) - every consumer knob (session config, skin, install choices, CLI flags/env) with copy snippets - Config tab write verification - durable allowlist PATCH matrix for Mission Control Config (private factory evidence under `docs/evidence/`; not public-synced) -- [Cursor 3.0 Features](cursor-3-features.md) - how Agent Kit uses native IDE features +- [Cursor 3.0 Features](cursor-3-features.md) - how Mission Kit uses native IDE features - [Cursor-native audit](cursor-native-audit.md) - hooks.json, plugin, rule modes, VS Code/Windsurf gaps - [Coherence inventory](coherence-inventory.md) - classification of rules, skills, hooks, agents, commands - [Drift inventory](drift-inventory.md) - per-workspace kit copies, L0 candidates, L3 uniques @@ -36,6 +38,7 @@ ## Community +- [Adoption comms](comms.md) - spread-the-word drafts with HITL; contributor funnel stays CONTRIBUTING - [Code of Conduct](../.github/CODE_OF_CONDUCT.md) - Contributor Covenant 2.1 and how to report a concern - [Security policy](../.github/SECURITY.md) - private disclosure channel, supported versions, documented posture (never open a public issue for a vulnerability) - [Support](../.github/SUPPORT.md) - where to ask what, and what makes a question answerable diff --git a/docs/bootstrap.md b/docs/bootstrap.md index 6f91b3e..eeb0443 100644 --- a/docs/bootstrap.md +++ b/docs/bootstrap.md @@ -1,6 +1,6 @@ -# What installing Agent Kit puts in your project +# What a Mission Kit install puts in your project -When you install Agent Kit, it doesn't copy its own repository into your project. It writes just the files your project needs: a few rules and commands under `.cursor/`, a git routine under `autogit/`, and a small manifest that tracks what was installed. This page shows exactly what lands where and why. +When you install via the Agent Kit CLI (`npx @dadado/agent-kit-cli install`), it does not copy the kit repository into your project. It writes just the files your project needs: a few rules and commands under `.cursor/`, a git routine under `autogit/`, and a small manifest that tracks what was installed. This page shows exactly what lands where and why. ## The layout you get diff --git a/docs/capability-inventory.md b/docs/capability-inventory.md index 6841a8d..d12df45 100644 --- a/docs/capability-inventory.md +++ b/docs/capability-inventory.md @@ -1,10 +1,10 @@ # Capability inventory -Agent Kit capability catalog grouped by surface family. Lists every shipped capability with one line per item. Storefront positioning uses **Mission Kit** on [missionkit.io](https://missionkit.io); technical identifiers remain Agent Kit (naming ADR `2026-08-06_mission-kit-vs-agent-kit-naming`). +Mission Kit capability catalog grouped by surface family. Lists every shipped capability with one line per item. Storefront positioning uses **Mission Kit** on [missionkit.io](https://missionkit.io); CLI, npm, slash commands, and `.cursor/agent-kit.json` remain Agent Kit identifiers (naming ADR `2026-08-06_mission-kit-vs-agent-kit-naming`). **Status (2026-08-06):** Product manifests at `5.0.0`. Capability counts verified against the working tree on private `staging` @ `7fdb03c` (see Real counts). Catalog narrative remains indicative for non-count claims. Evidence lanes: `docs/evidence/artifact-ledger-summary.md`, `docs/evidence/delivery-reconciliation.json` (RC-003/RC-004). Five-layer README positioning claims: `docs/evidence/five-layer-claim-matrix.md` / `docs/five-layer-claim-matrix.md`. -Real counts (verified against private staging @ 7fdb03c): **28** slash commands under `.cursor/commands/` (**27** synced/L0-oriented; **1** factory-only `/public-issue-triage` excluded from public-sync and L0 install), 25 rules, 13 agents, 9 skills, 5 Cursor hooks, 18 CLI commands (plus 5 subsystems), 7 packs, 3 personas, Mission Control dashboard, Git hooks, root scripts, and auxiliary tooling. +Real counts (verified against the working tree on 2026-08-13): **28** slash commands under `.cursor/commands/` (**27** synced/L0-oriented; **1** factory-only `/public-issue-triage` excluded from public-sync and L0 install), 25 rules, 14 agents, 10 skills, 5 Cursor hooks, 18 CLI commands (plus 5 subsystems), 7 packs, 3 personas, Mission Control dashboard, Git hooks, root scripts, and auxiliary tooling. Prior SHA snapshot `7fdb03c` was 13 agents / 9 skills before `mission-kit-comms`. --- @@ -76,7 +76,7 @@ Factory-only counting policy: inventories that describe the **consumer/L0** surf --- -## Named subagents (.cursor/agents/ - 13) +## Named subagents (.cursor/agents/ - 14) - `cleancode-refactor` - Architecture and readability refactoring - `clickup-tasks` - ClickUp task creation and management via MCP @@ -91,19 +91,21 @@ Factory-only counting policy: inventories that describe the **consumer/L0** surf - `sql-schema` - SQL schema creation and modification (demoted to skill-first) - `tech-lead` - Technology decisions, ADRs, architecture tradeoffs - `test-suites` - Test suite maintenance and E2E testing +- `mission-kit-comms` - Adoption/comms drafts with HITL-before-post (not an Agent Persona) --- -## Skills (.cursor/skills/ - 9) +## Skills (.cursor/skills/ - 10) ### Core skills (2) - `clean-code` - AI code slop removal and clean patterns - `docs-repo` - Repository documentation with professional standard -### Community skills (7) +### Community skills (8) - `clickup` - ClickUp task management via MCP - `cursor-skills-node` - Node.js development standards - `json-data-config` - JSON validation, formatting, manipulation +- `mission-kit-comms` - Mission Kit recap/release/contributor drafts; HITL-before-post - `n8n-workflows` - n8n workflow creation and editing - `prompts-markdown` - Agent prompt structure and versioning - `sql-postgres` - PostgreSQL schema and query patterns diff --git a/docs/claude-cli-kit-load.md b/docs/claude-cli-kit-load.md new file mode 100644 index 0000000..4f462ca --- /dev/null +++ b/docs/claude-cli-kit-load.md @@ -0,0 +1,120 @@ +# Claude CLI kit-load pack + +Thin markdown adapters so Claude Code CLI (including a session started in Cursor's terminal) can load Agent Kit / Mission Control context without rediscovering the repository. Cursor agents already receive that context via `sessionStart` and always-apply rules. Claude does not. + +**Decision:** ADR `.cursor/memory/decisions/2026-08-13_claude-cli-kit-load-bootstrap.md` (thin auto-load `CLAUDE.md` plus manual `/agent-kit` refresh). + +This page is the pack contract. The generator in `packages/cli/src/generator/` must emit the snippets below (skip if the target already exists). Docs here are indicative; delivery truth is generator output plus tests. + +## Surfaces + +| File | Role | Load behavior | +|------|------|----------------| +| `CLAUDE.md` (repository root) | Always-on pointers | Claude Code loads this every session. Keep short. Scanner already treats this path as agent guidance. | +| `.claude/commands/agent-kit.md` | Slash `/agent-kit` | Manual refresh. Frontmatter `disable-model-invocation: true` so it does not auto-inject a second copy of the same pack. | + +Do not emit `.claude/CLAUDE.md` as the primary file: the scanner only lists root `CLAUDE.md`. + +## Shared SoT (read these; do not duplicate) + +The pack points at existing files. It does not copy Cursor rules, hooks, or slash commands into a Claude dialect. + +| Pointer | Why | +|---------|-----| +| `AGENTS.md` | Cross-IDE contract | +| `.cursor/project-context.md` | Derived repository facts | +| `.cursor/HANDOFF.md` | Active plan, queue, next to-do (gitignored session state; read when present) | +| `.cursor/commands/` | L0 slash command index (Cursor chat). In Claude Code, treat the filenames as the command catalog and follow the same HITL prose. | +| `.cursor/memory/decisions/` | Accepted tradeoffs | +| `autogit/gitupdate.md` | Staging to production spine | + +Use backticked paths in `CLAUDE.md`. Do **not** use Claude `@path` imports for these files: imports expand at launch and would recreate Cursor always-on bulk. + +## HITL in Claude Code + +Cursor **Ask questions** is an IDE tool. Claude Code in the terminal does not have it. + +When a kit command says to Ask, Claude Code must present the **same option labels** as a numbered list, wait for a reply (number, label, or a typed Other), and treat skip/cancel as stop. CLI wizards (`agent-kit init`) stay on `@clack/prompts`. Never invent a second confirmation dialect. + +`/git-prod` remains explicit operator confirmation. Kit-load must not promote to `main`. + +## Readiness + +`/agent-kit-onboard` (and `agent-kit doctor`) remain the readiness path. Missing a legacy `onboarded` marker must not block unrelated work. Essential unreadiness: surface the first check and offer onboard. Non-essentials are advisory. + +## Non-goals (must appear in the emitted pack) + +These lines are part of the generated `CLAUDE.md` so a Claude session does not invent adjacent scope: + +- Not multi-IDE generator parity (Windsurf `.windsurfrules` / VS Code instructions). That is Action A7 in [cursor-native-audit.md](cursor-native-audit.md). +- Not opt-in **audits** / external plan review (`docs/external-plan-review.md`, `/plan-external-review`). Session kit-load is not that backend. +- Not `agent-kit run-plan --backend claude` tick-runner parity (`packages/cli/src/plan-loop/backends.ts`). +- Not a Claude copy of Cursor hooks (`sessionStart`, `preCompact`, shell/edit/prompt guards). Invariants stay in the CLI; Cursor hooks stay thin adapters. + +## Generator wiring + +- Emit on install/personalization **always**, same as `AGENTS.md`, not gated on `detectIde`. Claude CLI inside Cursor is the target. +- Skip each target independently if it already exists (`skipped-customized`). +- Register both paths on `protectedPaths`. +- Call from `applyPersonalization` and from `generateFromProfile` (compat `init` path). +- Do not add `.claude/` to scanner `CONTEXT_PATHS`; root `CLAUDE.md` is enough for honesty. +- Factory dogfood: commit the same two files in this repository so a Claude Code session here loads the pack without running install. + +## Canonical `CLAUDE.md` + +```markdown +# Agent Kit (Claude Code) + +This repository uses Agent Kit / Mission Control. Before rediscovering the tree, read the shared sources of truth (paths below are pointers; do not treat this file as a second rulebook). + +## Read first + +1. `AGENTS.md` - cross-IDE contract +2. `.cursor/project-context.md` - verified repository facts (derived; prefer code, tests, SHAs when docs conflict) +3. `.cursor/HANDOFF.md` - if present: active plan, next to-do, queue fields +4. `.cursor/commands/` - slash catalog (Cursor). Follow the same HITL contracts here. + +Mid-session refresh: `/agent-kit`. + +## HITL + +Cursor Ask questions is not available in this CLI. When a command requires a choice, list the same labels as a numbered list and wait. Skip or cancel means stop. Never `/git-prod` without an explicit operator yes. + +## Non-goals + +- Not Action A7 (Windsurf / VS Code generator parity) +- Not Claude external plan-review audits (`/plan-external-review`) +- Not `--backend claude` plan-loop ticks +- Not a copy of Cursor `sessionStart` / other IDE hooks +``` + +## Canonical `.claude/commands/agent-kit.md` + +```markdown +--- +description: Load Agent Kit session context (HANDOFF, project-context, commands). Manual refresh only. +disable-model-invocation: true +--- + +Read these files if they exist, then summarize the active plan, next to-do, and any Gaps. Do not scan the whole repository first. + +1. `AGENTS.md` +2. `.cursor/project-context.md` +3. `.cursor/HANDOFF.md` +4. The plan file named in HANDOFF `- **Plan:**` under `.cursor/plans/` + +If HANDOFF is missing, say so and point at `/agent-kit-onboard` or `/start-project` rather than inventing a plan. + +HITL: numbered-list fallback for Ask questions labels. Never `/git-prod` from this skill. + +Non-goals: not audits / `/plan-external-review`, not `--backend claude` ticks, not A7, not Cursor hook clones. +``` + +## Related + +- Operator path: [Getting Started](getting-started.md#claude-code-cli-session-kit-load) +- ADR `2026-08-13_claude-cli-kit-load-bootstrap.md` +- ADR `2026-07-29_cli-invariants-thin-hook-adapters.md` +- ADR `2026-07-20_optional-claude-code-plan-review.md` +- [External plan review](external-plan-review.md) (audits only) +- [Cursor-native audit](cursor-native-audit.md) Action A7 diff --git a/docs/coherence-inventory.md b/docs/coherence-inventory.md index aad7450..1046ab4 100644 --- a/docs/coherence-inventory.md +++ b/docs/coherence-inventory.md @@ -75,7 +75,7 @@ Workspace layout after dogfood `update`: `.cursor/skills/core|community//SKI --- -## Agents (`.cursor/agents/` - 13) +## Agents (`.cursor/agents/` - 14) Install path = pack/registry member, or dogfood-only / demoted (see [domain-packs.md](domain-packs.md)). Catalog label `core` ≠ L0 install layer. @@ -94,6 +94,7 @@ Install path = pack/registry member, or dogfood-only / demoted (see [domain-pack | `sql-schema.md` | stack | Demoted; prefer `sql-postgres` skill | | `clickup-tasks.md` | stack | L1 `project-management` | | `test-suites.md` | stack | L1 `quality` | +| `mission-kit-comms.md` | stack | Community skill `mission-kit-comms`; not Core Pack; HITL drafts only | --- diff --git a/docs/comms-channel-map.md b/docs/comms-channel-map.md new file mode 100644 index 0000000..7f2d959 --- /dev/null +++ b/docs/comms-channel-map.md @@ -0,0 +1,55 @@ +# Comms channel map and cadence + +Inventory of where Mission Kit may be talked about, with priority, effort, posting policy, and HITL depth. Cadence is inspired by public recap/release cycles (weekly shipped notes, release-tied longer posts). Do not copy another project's brand, assets, or claims. + +Related: [comms hub](comms.md), naming ADR `2026-08-06_mission-kit-vs-agent-kit-naming`, HITL ADR `2026-07-09_framework-hitl-positioning`, claim matrix [five-layer-claim-matrix.md](five-layer-claim-matrix.md). + +## Dual-name (locked) + +| Use | Name | +|-----|------| +| Product / marketing / site | Mission Kit / MissionKit / missionkit.io | +| CLI, npm, slash, manifest, pack | Agent Kit / `agent-kit` / `@dadado/agent-kit-cli` | +| Local dashboard | Mission Control | + +Do not market unchecked full autonomy. Staging may be automatic; production (`/git-prod`) stays a human confirmation. + +## Channel map + +| Channel | Priority | Effort | Disposition | Policy | HITL depth | +|---------|----------|--------|-------------|--------|------------| +| missionkit.io / blog-equivalent | High | Med | **include** | Product voice; dual-name; claims match 5.0.0 shipped docs | Ask before any public publish | +| Copy-paste social (X / Twitter) | High | Low | **include** | Short; no autonomy-first slogans; link site + public GitHub | Ask before post or reply | +| Medium | Med | Med | **include** | Same claims as [public-launch-announcement.md](public-launch-announcement.md) and getting-started | Ask before publish | +| Newsletter / Substack | Med | High | **include** | Digest of recaps and releases only; no unshipped features | Ask before send | +| Hacker News | Med | Low | **include** | Occasional Show HN or comment on a real release; no daily spam | Ask before submit or reply | +| GitHub Issues / PR discussion | High | Low | **include** | Contributor funnel, not ads. Align with CoC and SUPPORT | Ask before maintainer reply that is promotional; routine triage is existing maintainer HITL | +| GitHub Discussions | - | - | **never** (until enabled) | [SUPPORT.md](../.github/SUPPORT.md) states Discussions is not enabled; issues are the support channel | Do not invent a Discussions presence | +| Slack | Low | Med | **defer** | No official Mission Kit Slack is documented in this repo | Do not create or post until an operator-owned workspace exists | +| Discord | Low | Med | **defer** | Same as Slack | Same | +| Cursor Marketplace listing | - | - | **never** (this loop) | Distribution channel owned by parked `submit-cursor-marketplace` | Reference only; no submit from this plan | + +**Never:** paid ads as the first delivery; scraping PII into recaps; silent cross-network posting; renaming npm/CLI/slash to Mission Kit; overloading Agent Personas as the poster. + +## Cadence sketch (Cartesi-style input, not brand copy) + +Bind every cycle to artifacts that already exist: + +| Cycle | Trigger (must be real) | Output | Channels | +|-------|------------------------|--------|----------| +| Recap | Merged CHANGELOG `[Unreleased]` items that have reached `staging`, or a dated recap of what already shipped in 5.0.0 | [recap template](comms-templates/recap.md) | X (short), Medium/newsletter (optional) | +| Release | GitHub Release / closed CHANGELOG version / missionkit.io already showing that version | [release template](comms-templates/release.md) | Site, X, HN (optional), Medium | +| Contributor-ask | Open issues or registry contribution paths that are actually ready | [contributor-ask template](comms-templates/contributor-ask.md) | GitHub, X, launch paste follow-up | + +If CHANGELOG, Releases, and missionkit.io disagree, **stop and fix docs**; do not invent a feature in the recap. + +Living schedule: [comms-content-calendar.md](comms-content-calendar.md). + +## Constraints (comms loop) + +- HITL Ask (or numbered-list fallback) before any public network post or reply. +- Secrets for networks never enter git. Env **names** only: [comms-publish-pipeline.md](comms-publish-pipeline.md). +- Core Pack unchanged. Skill lives under `registry/skills/community/`. +- n8n is not a factory or Core Pack dependency. Use it only if the operator already runs n8n. +- Claims: Mission Kit 5 / Agent Kit install / HITL framework. Do not claim Cursor Marketplace submit or an npm rename. +- Draft script must fail closed: it writes local drafts and refuses publish. diff --git a/docs/comms-content-calendar.md b/docs/comms-content-calendar.md new file mode 100644 index 0000000..92bab5f --- /dev/null +++ b/docs/comms-content-calendar.md @@ -0,0 +1,38 @@ +# Comms content calendar + +Living schedule for Mission Kit recap, release, and contributor-ask drafts. Seed copy: [public-launch-announcement.md](public-launch-announcement.md) (keep that file; evolve here). + +**SoT for "is it shipped?":** `CHANGELOG.md` closed versions, GitHub Releases on the public repo, and live [missionkit.io](https://missionkit.io). Product version for current claims: **5.0.0**. + +## How to add a row + +1. Confirm the item exists in CHANGELOG or a GitHub Release (or is explicitly a contributor process ask, not a product claim). +2. Pick template: recap / release / contributor-ask. +3. Draft with `.cursor/scripts/comms-draft.mjs` (or the `mission-kit-comms` agent). +4. Operator Ask before any public post. Record the date posted only after HITL yes. + +## Calendar + +| When | Kind | Hook (shipped artifact) | Status | Channels (after HITL) | +|------|------|-------------------------|--------|------------------------| +| 2026-08-12 | release | CHANGELOG `[5.0.0]`; npm `@dadado/agent-kit-cli@5.0.0`; missionkit.io Mission Kit 5 | **seed** (launch paste already exists) | site, GitHub, social paste in [public-launch-announcement.md](public-launch-announcement.md) | +| After next closed CHANGELOG version | release | New `## [X.Y.Z]` plus GitHub Release | planned | site, X, optional HN/Medium | +| Weekly while `[Unreleased]` has merged staging work | recap | `[Unreleased]` bullets that are already in `staging` (do not recap unmerged WIP) | planned | X short; optional newsletter | +| Ongoing | contributor-ask | [CONTRIBUTING.md](CONTRIBUTING.md), [contribute-upstream.md](contribute-upstream.md), community skills under `registry/skills/community/` | planned | GitHub issues, social only after Ask | + +Do not schedule Cursor Marketplace submit here. That work is parked. + +## Templates + +- [Recap](comms-templates/recap.md) +- [Release](comms-templates/release.md) +- [Contributor ask](comms-templates/contributor-ask.md) + +## Claim check (every draft) + +- [ ] Mission Kit = product; Agent Kit = CLI/npm/slash/pack; Mission Control = dashboard +- [ ] Version and features match 5.0.0 shipped docs (getting-started, README, five-layer matrix) +- [ ] HITL / staging→prod confirmation is stated; no "posts itself" or "full autonomy" +- [ ] PolyForm Noncommercial + `sales@missionkit.io` for commercial use +- [ ] Public GitHub: `https://github.com/agent-kit-startup/agent-kit` +- [ ] No Marketplace listing claimed as done diff --git a/docs/comms-publish-pipeline.md b/docs/comms-publish-pipeline.md new file mode 100644 index 0000000..06ff302 --- /dev/null +++ b/docs/comms-publish-pipeline.md @@ -0,0 +1,52 @@ +# Comms draft → HITL → publish pipeline + +Kit-native first: community skill `mission-kit-comms`, agent `.cursor/agents/mission-kit-comms.md`, Ask questions (numbered-list fallback), optional local draft script. **n8n is not wired** and is not a Core Pack or factory dependency. + +Nothing in this pipeline performs an HTTP post to X, Discord, Slack, Medium, Substack, or HN. + +## Flow + +```text +CHANGELOG / Release / calendar row + → draft (agent or comms-draft.mjs) + → operator Ask: Post / Edit / Discard + → only the human publishes on the network +``` + +Fail closed: if Ask is skipped, do not publish. The script's `--publish` flag exits non-zero on purpose. + +## Draft script + +```bash +node .cursor/scripts/comms-draft.mjs --kind recap --channel x +node .cursor/scripts/comms-draft.mjs --kind release --channel medium --version 5.0.0 +node .cursor/scripts/comms-draft.mjs --kind contributor-ask --channel github +``` + +Writes under `.cursor/comms-drafts/` (gitignored). Idempotent: same kind+channel+UTC day reuses the file unless `--force-new`. `--publish` always refuses. + +Tests: `node --test .cursor/scripts/comms-draft.test.mjs`. + +## Env contract (names only) + +Do **not** create a committed `.env` file (`.env` and `.env.*` are gitignored). Document names here. Live values stay in the operator's local `.env` (untracked). + +| Name | Used for | Required to draft? | Required to post? | +|------|----------|--------------------|-------------------| +| `MISSION_KIT_COMMS_X_BEARER` | X API (operator tools only) | no | yes, if the human uses an API client | +| `MISSION_KIT_COMMS_DISCORD_WEBHOOK` | Discord webhook | no | yes, if used | +| `MISSION_KIT_COMMS_SLACK_WEBHOOK` | Slack webhook | no | yes, if used | +| `MISSION_KIT_COMMS_MEDIUM_TOKEN` | Medium | no | yes, if used | +| `MISSION_KIT_COMMS_SUBSTACK_SESSION` | Substack | no | yes, if used | + +The draft script must not print env values. Webhooks that post without a recorded HITL decision are forbidden. + +## Rate limits and idempotency + +- One public post per calendar row per channel unless the operator explicitly asks to retry. +- Draft ids: `{kind}-{channel}-{YYYY-MM-DD}`. +- Recaps must not duplicate the previous day's file without `--force-new`. + +## GitHub Action + +No workflow in this repo posts comms. A `workflow_dispatch` that published would still need a human click; even that is deferred so secrets never sit on CI for social posts. diff --git a/docs/comms-templates/contributor-ask.md b/docs/comms-templates/contributor-ask.md new file mode 100644 index 0000000..821d7dd --- /dev/null +++ b/docs/comms-templates/contributor-ask.md @@ -0,0 +1,30 @@ +# Template: contributor-ask + +Point people at existing contribution paths. Do not invent a Marketplace submit call-to-action. + +## English + +```text +Want to help Mission Kit? + +- Skills: registry/skills/community//SKILL.md (see docs/CONTRIBUTING.md) +- From a consumer project: agent-kit contribute (docs/contribute-upstream.md) +- Bugs and questions: GitHub issues (Discussions is not enabled) +- Code of Conduct: .github/CODE_OF_CONDUCT.md +- Security: private channel in .github/SECURITY.md (never a public issue) + +Public repo: https://github.com/agent-kit-startup/agent-kit +Install: npx @dadado/agent-kit-cli@5.0.0 install +``` + +## Maintainer reply draft (HITL before posting) + +Keep under three short blocks (ux-message-flows). One ask per message. Confirm before linking commercial sales. + +```text +Thanks for the interest. + +Best next step: {{issue form / CONTRIBUTING section / agent-kit contribute}}. + +If this is a vulnerability, please use the private channel in SECURITY.md instead of this thread. +``` diff --git a/docs/comms-templates/recap.md b/docs/comms-templates/recap.md new file mode 100644 index 0000000..ccad5bd --- /dev/null +++ b/docs/comms-templates/recap.md @@ -0,0 +1,33 @@ +# Template: recap + +Short cycle tied to **already merged** CHANGELOG `[Unreleased]` or a dated slice of shipped 5.0.0 work. Not a preview of unmerged branches. + +Fill `{{placeholders}}`. Run claim check in [comms-content-calendar.md](../comms-content-calendar.md) before Ask. + +## English (docs / Medium / newsletter) + +```text +Mission Kit recap ({{date}}) + +Shipped in the Agent Kit repo / CLI this cycle: +{{bullet list from CHANGELOG, no unshipped items}} + +Install stays Agent Kit: npx @dadado/agent-kit-cli@5.0.0 install +Site: https://missionkit.io +Contribute: issues and PRs on https://github.com/agent-kit-startup/agent-kit + +HITL framework: plans and staging can move; production still needs a human yes. +``` + +## Short (X) + +```text +Mission Kit recap: {{one shipped fact}}. + +npx @dadado/agent-kit-cli@5.0.0 install +https://missionkit.io +``` + +## Portuguese (optional; launch voice) + +Reuse tone from [public-launch-announcement.md](../public-launch-announcement.md). Keep dual-name (Mission Kit product / Agent Kit install) and do not add features absent from CHANGELOG. diff --git a/docs/comms-templates/release.md b/docs/comms-templates/release.md new file mode 100644 index 0000000..5c03dd0 --- /dev/null +++ b/docs/comms-templates/release.md @@ -0,0 +1,33 @@ +# Template: release + +Use only when a CHANGELOG version is **closed**, a GitHub Release exists (or is the same tag), and missionkit.io claims match. Current floor: **5.0.0**. + +## English + +```text +Mission Kit {{version}} is out. + +What it is: HITL development operations in Cursor and VS Code (plan, handoff, staging→prod with confirmation). Dashboard: Mission Control. + +Install (Agent Kit CLI): +npx @dadado/agent-kit-cli@{{version}} install + +Site: https://missionkit.io +Notes: {{link to CHANGELOG section or GitHub Release}} +Source-available: PolyForm Noncommercial; commercial: sales@missionkit.io +``` + +## Short (X) + +```text +Mission Kit {{version}} / Agent Kit CLI @{{version}} + +npx @dadado/agent-kit-cli@{{version}} install +https://missionkit.io +``` + +## Hacker News (optional, after Ask) + +Title: `Show HN: Mission Kit {{version}} – HITL plan/handoff/staging→prod for AI IDEs` + +Body: dual-name one-liner, install command, what is **not** claimed (no full autonomy, Marketplace listing not this release unless the parked submit plan has actually listed it). diff --git a/docs/comms.md b/docs/comms.md new file mode 100644 index 0000000..ab4726b --- /dev/null +++ b/docs/comms.md @@ -0,0 +1,18 @@ +# Mission Kit adoption comms + +Repeatable **draft then HITL approve then publish** loop for Mission Kit adoption and contributor asks. Nothing in this tree posts to a public network by itself. + +**Product version in copy:** 5.0.0 (npm `@dadado/agent-kit-cli`, site [missionkit.io](https://missionkit.io)). Docs are indicative; claims must match shipped surfaces. + +| Piece | Path | +|-------|------| +| Channel map, cadence, constraints | [comms-channel-map.md](comms-channel-map.md) | +| Living calendar | [comms-content-calendar.md](comms-content-calendar.md) | +| Templates (recap, release, contributor-ask) | [comms-templates/](comms-templates/recap.md) | +| Draft pipeline and env names | [comms-publish-pipeline.md](comms-publish-pipeline.md) | +| Contributor funnel | [CONTRIBUTING.md](CONTRIBUTING.md), [contribute-upstream.md](contribute-upstream.md) | +| Community skill | `registry/skills/community/mission-kit-comms/SKILL.md` (`agent-kit add mission-kit-comms`) | +| Dedicated agent | `.cursor/agents/mission-kit-comms.md` (not an Agent Persona) | +| One-shot launch paste | [public-launch-announcement.md](public-launch-announcement.md) | + +Agent Personas (`autopilot`, `night-shift`, `ghost-runner`) stay chat chrome. They are not posters. Cursor Marketplace publisher submit is owned by a parked plan; do not treat listing as shipped from this loop. diff --git a/docs/cursor-3-features.md b/docs/cursor-3-features.md index 88704ec..275b4c4 100644 --- a/docs/cursor-3-features.md +++ b/docs/cursor-3-features.md @@ -1,14 +1,14 @@ -# Cursor 3.0 Features - How Agent Kit Uses Them +# Cursor 3.0 Features - How Mission Kit Uses Them -Agent Kit helps develop without losing context and uses native Cursor features as **complement**, not replacement. +Mission Kit helps develop without losing context and uses native Cursor features as **complement**, not replacement. Install and CLI identifiers stay Agent Kit (`agent-kit`, `@dadado/agent-kit-cli`). -## Agent Kit Position +## Mission Kit position -File-based handoff (`.cursor/HANDOFF.md`) is the **source of truth** for continuity. No IDE guarantees infinite memory; context limits are real, and new sessions start from zero. That's why Agent Kit maintains state on disk: structured plans, progress, and suggested routines. +File-based handoff (`.cursor/HANDOFF.md`) is the **source of truth** for continuity. No IDE guarantees infinite memory; context limits are real, and new sessions start from zero. That's why Mission Kit maintains state on disk: structured plans, progress, and suggested routines. ## Native features and how to use them -| Feature | What it does | How Agent Kit uses it | +| Feature | What it does | How Mission Kit uses it | |---------|-----------|----------------------| | `/resume` | Resume previous conversation | Complement to HANDOFF - good for quick reference, doesn't replace file state | | Summaries | Automatic summary of long sessions | Useful for revisiting decisions; handoff remains mandatory | @@ -16,11 +16,11 @@ File-based handoff (`.cursor/HANDOFF.md`) is the **source of truth** for continu | Agents Window | Multiple agents in parallel | Each agent reads HANDOFF before acting; shared state is in file | | `/worktree` | Isolated git worktree | For risky changes without dirtying the working tree | | `/best-of-n` | Compare approaches side by side | For architecture decisions | -| Plans | Native Cursor plans | Agent Kit generates plans with todos in frontmatter; HANDOFF references active plan | +| Plans | Native Cursor plans | Mission Kit generates plans with todos in frontmatter; HANDOFF references active plan | ## MCP, hooks and SDK -**Agent gateways parallel to Cursor are not part of the Agent Kit stack** - we don't document or version config for that. +**Agent gateways parallel to Cursor are not part of the Mission Kit stack** - we don't document or version config for that. For extensions and automation, use what Cursor itself offers: diff --git a/docs/cursor-native-audit.md b/docs/cursor-native-audit.md index b3d064d..8915388 100644 --- a/docs/cursor-native-audit.md +++ b/docs/cursor-native-audit.md @@ -9,12 +9,12 @@ Audit of Cursor-specific artifacts in the Agent Kit repository: what exists, wha | Area | Status | Notes | |------|--------|-------| | `.cursor/rules/` | Present (25 files) | 10 core rules `alwaysApply: true`; stack rules use globs | -| `.cursor/skills/` | Present (9 skills) | Install output from registry (`core/` 2 + `community/` 7) | -| `.cursor/agents/` | Present (13 agents) | Mix of core and stack subagents; EN pack ids | +| `.cursor/skills/` | Present (10 skills) | Install output from registry (`core/` 2 + `community/` 8) | +| `.cursor/agents/` | Present (14 agents) | Mix of core and stack subagents; EN pack ids | | `.cursor/commands/` | Present (27 commands) | DevOps spine + handoff + orchestration + backlog/dashboard | | `.cursor/hooks/` (shell) | Present | Git pre-commit + edit validators; not wired to Cursor agent events | | `.cursor/hooks.json` | **Present (L0)** | 5 events (`sessionStart`, `preCompact`, `beforeShellExecution`, `afterFileEdit`, `beforeSubmitPrompt`); no `stop` hook | -| `.cursor-plugin/plugin.json` | Present | Marketplace-ready at **5.0.0**, aligned with product; declares explicit component paths | +| `.cursor-plugin/plugin.json` | Present | Marketplace-ready at **5.1.0**, aligned with product; declares explicit component paths | | `git-hooks/prepare-commit-msg` | Present | Strips Cursor co-author trailer | | `AGENTS.md` (dogfood) | **Present** | Root cross-IDE contract; points at `.cursor/project-context.md` | | `mcp.json` | Absent | No project-level MCP config in core | @@ -28,7 +28,7 @@ Audit of Cursor-specific artifacts in the Agent Kit repository: what exists, wha | Field | Value | |-------|-------| | name | `agent-kit` | -| version | `5.0.0` (pinned to `KIT_VERSION` by `packages/cli/src/lifecycle/l0.test.ts`) | +| version | `5.1.0` (pinned to `KIT_VERSION` by `packages/cli/src/lifecycle/l0.test.ts`) | | description | HITL framework for AI-assisted IDEs (plan, handoff, staging→prod, memory loop, anti-slop) | | components | `rules`, `skills`, `agents`, `commands`, `hooks` declared explicitly | @@ -39,7 +39,7 @@ Audit of Cursor-specific artifacts in the Agent Kit repository: what exists, wha - `skills` points at `.cursor/skills/core`, not `.cursor/skills`: discovery matches direct children holding a `SKILL.md`, and `core/`/`community/` are one level too shallow. This also keeps stack skills on `agent-kit add`. - CLI `init` does **not** write `.cursor-plugin/plugin.json` into target projects; no generator under `packages/cli/src/generator/` references it. (Corrected 2026-08-05; the earlier claim was stale.) -**Gap:** Plugin packaging and Marketplace submission flow are not documented end-to-end in `docs/getting-started.md`. See [marketplace.md](marketplace.md) and Phase B registry cutover plan. Live submission stays publisher HITL and is gated on the public mirror carrying the 5.0.0 manifest. +**Gap:** Plugin packaging and Marketplace submission flow are not documented end-to-end in `docs/getting-started.md`. See [marketplace.md](marketplace.md) and Phase B registry cutover plan. Live submission stays publisher HITL and is gated on the public mirror carrying the 5.1.0 manifest. --- @@ -140,7 +140,7 @@ Separate from `.cursor/hooks/`; optional hygiene for teams that reject bot co-au ## Agents and commands (Cursor-native) -### Agents (`.cursor/agents/` - 13) +### Agents (`.cursor/agents/` - 14) Cursor subagent definitions. Consumed via Task tool / Agents Window. Full classification in [coherence-inventory.md](coherence-inventory.md). @@ -237,7 +237,7 @@ Acceptable for private SoT until Phase B registry cutover defines minimum dogfoo | A4 | Partial | Dual hook lanes remain: `git-hooks/` (main/push guards + co-author strip) vs `.cursor/hooks/pre-commit/` (secrets + JSON). Next step: document install matrix and decide whether CLI `git-hooks` generator should mirror the Cursor pre-commit chain (no silent merge). | | A5 | ✅ Done | Root `AGENTS.md` present (dogfood cross-IDE contract) | | A6 | Partial | Marketplace plugin path documented ([marketplace.md](marketplace.md) packaging contract, 2026-08-05); VS Code/Windsurf install still open | -| A7 | Open | Expand generators or template files for multi-IDE parity (scoped: Windsurf `.windsurfrules` handoff/git bullets + VS Code instruction parity with [cursor-3-features.md](cursor-3-features.md); not Marketplace submit) | +| A7 | Open | Expand generators or template files for multi-IDE parity (scoped: Windsurf `.windsurfrules` handoff/git bullets + VS Code instruction parity with [cursor-3-features.md](cursor-3-features.md); not Marketplace submit). Claude CLI session kit-load (`CLAUDE.md` / `/agent-kit`) is a separate surface: [claude-cli-kit-load.md](claude-cli-kit-load.md). | --- diff --git a/docs/external-plan-review.md b/docs/external-plan-review.md index 43b6289..5943ebc 100644 --- a/docs/external-plan-review.md +++ b/docs/external-plan-review.md @@ -2,7 +2,9 @@ Optional post-completion **audits** of shipped work against the original plan using Claude Code CLI. When a plan finishes all implementable to-dos, get evidence-based gap detection from a second agent without interfering with the original execution flow. Config object key remains `externalPlanReview` for compatibility; L0 and docs prefer the product name **audits**. -L0 ships the commands, templates, launcher, and `config.example.json` with the base install. The feature stays **opt-in** (`enabled: false` by default). Claude Code is never required for install or CI; a missing `claude` binary or missing prompt template yields a tip and exit 0. +This page is **not** session kit-load. Loading Agent Kit context at the start of a Claude Code session (`CLAUDE.md`, `/agent-kit`) is documented in [claude-cli-kit-load.md](claude-cli-kit-load.md). + +L0 ships the commands, templates, launcher, and `config.example.json` with the base install. The feature stays **opt-in** (`enabled: false` by default). Claude Code is never required for install or CI. `backend: "auto"` uses Cursor Agent when Claude is missing or quota-empty. A pinned `backend: "claude"` with no usable Claude still yields a tip and exit 0 (or exit 4 with `--wait-monitor`). A missing prompt template yields a tip and exit 0. **Install note:** session L3 protection covers `config.json`, `current/**`, and `backups/**` only. If an older manifest still lists `.cursor/context/**`, `agent-kit update` expands that glob so templates can install. If the prompt file is missing after a fresh offer, run `agent-kit update --refresh` and re-arm. @@ -14,7 +16,9 @@ L0 ships the commands, templates, launcher, and `config.example.json` with the b { "externalPlanReview": { "enabled": true, - "backend": "claude", + "backend": "auto", + "reviewerModel": "haiku", + "advisorModel": "opus", "autoRemediate": false, "offerOnExhausted": true, "mode": "autonomous", @@ -111,7 +115,9 @@ Mission Control **Flight Log** shows HANDOFF Gaps (**NOW** + **Earlier** history { "externalPlanReview": { "enabled": false, - "backend": "claude", + "backend": "auto", + "reviewerModel": "haiku", + "advisorModel": "opus", "autoRemediate": false, "offerOnExhausted": true, "mode": "paste", @@ -124,7 +130,9 @@ Mission Control **Flight Log** shows HANDOFF Gaps (**NOW** + **Earlier** history | Field | Meaning | |-------|---------| | `enabled` | Auto-arm on plan exhaustion (default: false) | -| `backend` | External agent type (default: `"claude"`) | +| `backend` | Reviewer: `"auto"` (Claude if usable, else Cursor Agent), `"claude"`, or `"cursor"`. Missing key keeps `"claude"` for existing installs. Claude quota empty (`AGENT_KIT_AUDIT_CLAUDE_QUOTA_EMPTY=1`) is not the Cursor tick API/usage-limit hard-stop. | +| `reviewerModel` | Named reviewer (default `haiku`). Claude spawn passes `--model`. Cursor fallback cannot honor a Claude-family name; runtime is Auto unless a named Cursor model is set. | +| `advisorModel` | Escalate-only advisor (default `opus`). Runs only when the Haiku monitor includes `` (high severity or uncertainty). Findings-only; not a second implement pass. | | `autoRemediate` | When `false` (default): review workers and external Claude stay findings-only; `/run-plan` must not auto-fix product code after findings (fix-agent Task for small nits, or residuals backlog plan for large). When `true`: review workers remain findings-only; orchestrator may dispatch a fix agent for small nits without an extra Ask. External-monitor path still requires `/plan-review-triage` before product edits. | | `offerOnExhausted` | When `enabled` is false, allow exhaustion Ask until Always or Not now (default: true) | | `mode` | Audits arming path: `"paste"` (legacy clipboard/paste into Cursor Terminal) or `"autonomous"` (background/inspectable PTY auto-launch when enabled). Missing key keeps paste-compatible behavior for existing installs. Greenfield example may show `"autonomous"`. | @@ -148,11 +156,11 @@ Do **not** flip `autoRemediate` to `true` as a shortcut for fewer backlog plans; Canonical launcher: `.cursor/scripts/plan-external-review.sh` (wrapper: `scripts/plan-external-review.sh`). -The launcher starts Claude with `--permission-mode auto` in interactive and headless modes. This removes the need to toggle auto mode manually while retaining Claude's permission policy; it does not use `bypassPermissions`. +The launcher starts Claude with `--permission-mode auto` and `--model` from `reviewerModel` (default `haiku`) in interactive and headless modes. This removes the need to toggle auto mode manually while retaining Claude's permission policy; it does not use `bypassPermissions`. Override with `--reviewer-model`, `--advisor-model`, and `--implementer-model` (or `AGENT_KIT_AUDIT_IMPLEMENTER_MODEL`). Same-family implementer and reviewer is an honest skip (exit 4 with `--wait-monitor`), including Auto/Auto. **Background/inspectable auto-launch (`mode: "autonomous"` or `--autonomous`):** prefers tmux/screen detached PTY, then macOS Terminal.app `do script` **without** `activate`, then Linux/Windows emulators. Soft-falls back to `--paste-only` when spawn is unavailable. Soft-fails with tip + exit 0 when `claude` is missing (Field Report owed). Never runs silent `claude -p` in a chat agent shell. Rollback to OS window focus: `--focus-terminal` or `AGENT_KIT_AUDIT_FOCUS_TERMINAL=1`. ADR: `.cursor/memory/decisions/2026-07-28_audits-headless-terminal-honesty.md`. -**Post-spawn monitor watch (`--wait-monitor`):** chat autonomous arms **must** pass `--force --autonomous --wait-monitor`. The launcher records an arm epoch, then polls until `.cursor/memory/plan-monitor-.md` is **fresh** (`mtime >= arm epoch`, or a content sentinel line `` / `updated`), or until `--wait-timeout` (default 900s). Pre-arm files are ignored (existence alone is not ready). Exit codes: `0` fresh ready (soft-fail tip + exit 0 only when wait is off), `3` timeout, `4` soft-fail while waiting. Dry-run prints wait path, timeout, arm-epoch, and stale/missing status. Spawn-only exit 0 without wait is not review done. Exit `3` is **timeout only**: it never means review done, and a monitor written afterwards by a later or separate arm does not convert it into success (leave the target Field Report owed and re-arm). ADRs: `.cursor/memory/decisions/2026-07-27_audits-wait-freshness-enforce.md`, `.cursor/memory/decisions/2026-07-27_audits-post-spawn-monitor-watch-continue.md`. +**Post-spawn monitor watch (`--wait-monitor`):** chat autonomous arms **must** pass `--force --autonomous --wait-monitor`. The launcher records an arm epoch and persists wait state in `.cursor/context/audit-wait/.json` (gitignored; not HANDOFF). It then polls until `.cursor/memory/plan-monitor-.md` is **fresh** (`mtime >= arm epoch`, or a content sentinel line `` / `updated`). Chat AwaitShell uses `waitSliceSeconds` / `--wait-slice` (default 90s). Total budget across slices is `waitTimeoutSeconds` / `--wait-timeout` (default 900s). CI/headless may use the full remaining budget in one invocation. A new session that finds an armed wait-state file either exits `0` immediately (monitor already fresh vs stored epoch) or polls **remaining** budget only; it does not restart 900s from zero. Early-ready is unchanged: first freshness exits `0`. Pre-arm files are ignored (existence alone is not ready). Exit codes: `0` fresh ready (soft-fail tip + exit 0 only when wait is off), `3` timeout (slice or total; not review done), `4` soft-fail while waiting. Dry-run prints wait path, slice, timeout, remaining, resume, arm-epoch, wait-state path, and stale/missing status. Spawn-only exit 0 without wait is not review done. Exit `3` is **timeout only**: it never means review done, and a monitor written afterwards by a later or separate arm does not convert it into success (leave the target Field Report owed and re-arm). ADRs: `.cursor/memory/decisions/2026-07-27_audits-wait-freshness-enforce.md`, `.cursor/memory/decisions/2026-07-27_audits-post-spawn-monitor-watch-continue.md`, `.cursor/memory/decisions/2026-08-13_audits-atomic-wait-reviewer-fallback.md`. **Post-spawn progress gate:** a successful spawn is a launch, not a running review. After an autonomous background spawn on a channel that exposes scrollback (tmux `capture-pane`, screen `hardcopy`), the launcher waits briefly for its own pre-exec banner to land, measures that banner as a baseline, then polls every 2 seconds for scrollback growth *beyond* the banner before entering the monitor wait. Default grace window is 60s, overridable with `AGENT_KIT_AUDIT_PROGRESS_TIMEOUT` (`0` disables the gate; a non-integer value prints a tip and falls back to 60). Channels without a scrollback API (Terminal.app, Linux/Windows emulators) degrade to advisory (`progress gate skipped`) and proceed to the normal wait; the gate never aborts a channel it cannot sample. A silent PTY (no growth beyond the banner, or a session that vanished before producing output) is reported as a failed launch: the launcher disposes only the session it just spawned, prints the paste fallback, and soft-fails (exit `4` with `--wait-monitor`, tip + exit `0` otherwise) instead of burning the remaining `--wait-timeout`. No new exit code. Dry-run prints `progress-gate`, `progress-timeout`, and a note that the channel is resolved at spawn time. ADR: `.cursor/memory/decisions/2026-07-30_audits-pty-progress-gate-zombie-policy.md`. @@ -286,9 +294,16 @@ AGENT_KIT_AUDIT_REAP_MIN_AGE=0 .cursor/scripts/plan-external-review.sh --reap-au **Claude CLI not found:** -- External review skips with a tip message and exit 0 (exit 4 when `--wait-monitor` was requested) -- Plan execution continues normally -- Manual review remains available when Claude is installed later +- `backend: "auto"` uses Cursor Agent when Claude is missing or `AGENT_KIT_AUDIT_CLAUDE_QUOTA_EMPTY` is set. That is not the Cursor tick API/usage-limit hard-stop. +- Pinned `backend: "claude"` still tips and skips (exit 0, or exit 4 with `--wait-monitor`). Field Report stays owed. +- Same-family implementer and reviewer is an honest skip (including Auto/Auto), not a silent self-review. + +## Security + +- **Findings-only:** reviewers write `plan-monitor-*.md` and do not auto-fix product source. `autoRemediate` stays false unless the operator opts in; `/plan-review-triage` remains the HITL gate. Never silent-Ack. +- **Secrets:** review prompts and `.cursor/context/audit-wait/.json` hold model ids and timestamps only. No tokens, `.env`, or credentials. +- **Integrity:** freshness stays `mtime >=` arm epoch or the wait-fresh sentinel. Exit `0` ready / `3` timeout / `4` soft-fail. `/git-prod` is never armed from this path. +- **Split-brain:** implementer ≠ reviewer is a hard refuse. Cursor fallback cannot honor a Claude-family `reviewerModel`; runtime is Auto unless a named Cursor model is set. **Review disabled:** @@ -305,7 +320,7 @@ AGENT_KIT_AUDIT_REAP_MIN_AGE=0 .cursor/scripts/plan-external-review.sh --reap-au ## Implementation notes - **Not a native Cursor hook:** Avoids interfering with HITL confirmation prompts -- **Evidence-based:** Claude reviews actual git history and deliverables +- **Evidence-based:** the reviewer (Claude Haiku or Cursor Agent) checks git history and deliverables against the plan. Tests: `.cursor/scripts/plan-external-review-atomic-wait.test.mjs` (early-ready, slice honesty), `plan-external-review-backend-cascade.test.mjs` (Claude-missing cascade), `plan-external-review-model-routing.test.mjs` (same-model refuse). - **Findings-only review workers:** Claude and Cursor review ticks write monitors/findings; they do not auto-fix product code. `/run-plan` enforces `externalPlanReview.autoRemediate` (default false) as the remediation gate (fix agent vs residuals plan). The launcher injects the current `autoRemediate` value into the Claude prompt. - **Staging-first:** All fixes follow standard `/git-staging` → `/git-prod` flow - **Opt-in default:** Requires explicit configuration (or Always / onboard Enable) to auto-arm diff --git a/docs/five-layer-claim-matrix.md b/docs/five-layer-claim-matrix.md index 171b069..575222e 100644 --- a/docs/five-layer-claim-matrix.md +++ b/docs/five-layer-claim-matrix.md @@ -33,6 +33,7 @@ Classification: **shipped core** (L0), **optional pack** (L1/L2), **planned**, * 3. Hosted multi-tenant control plane or cloud HANDOFF sync. 4. Guaranteed production readiness without lane-qualified release evidence. 5. Silent auto-remediation of product code from external review when `autoRemediate` is false (default). +6. Silent cross-network social posting (adoption comms draft with HITL only; community skill `mission-kit-comms`). ## Lane note diff --git a/docs/getting-started.md b/docs/getting-started.md index 80e085d..c8b6e8d 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -14,7 +14,7 @@ npx @dadado/agent-kit-cli install Unpinned `npx` resolves to the latest publish. Pin a version when you need a reproducible install: `npx @dadado/agent-kit-cli@x.y.z install` (replace `x.y.z` with a version from npm). -**IDE-agnostic:** works in Cursor, VS Code, and any terminal with Node.js. For non-interactive terminals (CI, piped stdin, VS Code output panels without TTY), suppress both `npx`'s own confirmation and the CLI's root prompt: +**IDE-agnostic:** works in Cursor, VS Code, and any terminal with Node.js. Claude Code CLI session kit-load (`CLAUDE.md` / `/agent-kit`) is documented under [Claude Code CLI (session kit-load)](#claude-code-cli-session-kit-load). For non-interactive terminals (CI, piped stdin, VS Code output panels without TTY), suppress both `npx`'s own confirmation and the CLI's root prompt: ```bash npx -y @dadado/agent-kit-cli install --yes @@ -60,7 +60,7 @@ npx @dadado/agent-kit-cli doctor --fix-safe ### After install checklist (personal path) -Keep this path light. No extra runtime packages beyond the CLI (`@clack/prompts`, `citty`, `kolorist` only). +Keep this path light. No extra runtime packages beyond the CLI (`@clack/prompts`, `citty`, `kolorist` only). TTY chrome (spinners, tips) is ANSI in the CLI package; do not add `ora`, `figlet`, `chalk`, or `ink`. 1. **Node.js 20+** - required for `npx` / `agent-kit` CLI (`engines` in package manifests). 2. **Git** - recommended so readiness pillars and `/git-staging` → `/git-prod` work; local-only Git is valid. @@ -86,6 +86,8 @@ Keep this path light. No extra runtime packages beyond the CLI (`@clack/prompts` | `agent-kit handoff` | Save your progress to `.cursor/HANDOFF.md` | | `agent-kit scan` | Just scan the project, don't install | +Optional: `agent-kit add mission-kit-comms` drafts recap/release/contributor copy. It does **not** post. Ask before any public network. Guide: [comms.md](comms.md). + ## A normal day The idea is simple: work against a plan, save your place before a conversation gets too big, and (in **manual** mode) keep **one phase per chat**. Continuous and queue modes are opt-in denser paths under [Less babysitting](#less-babysitting). Modes overview: [plan-routine §5](../autogit/plan-routine.md#5-two-execution-modes). @@ -118,7 +120,7 @@ Operator sequence when you drive each unit (command SoT: [`.cursor/commands/cont Do not re-author Gate A/B or continuous tick contracts here; link L0 commands when you need the full contract. -### Keeping Agent Kit current (consumers) +### Keeping Mission Kit current (consumers) - **Opt-in check:** set `updateCheck.enabled: true` in `.cursor/context/config.json` (Mission Control Config can toggle it). SessionStart may then nudge when a newer public release exists; interval is `updateCheck.intervalDays` (default 7). - **Manual check:** `agent-kit update --check --json`. @@ -162,9 +164,21 @@ For the full list of consumer-configurable knobs (session config, dashboard skin **Mission Control shortcut:** the Checklist **Run all** header button copies `/run-plan-all` into chat input (copy-only; it does not write HANDOFF or start the queue). While a queue is live, Checklist cards sort by role-priority (executing, then next up, then queued / completed), not by Run queue index. Plan card Actions also offer **Run (manual)** (`/continue-plan`) and **Run (auto)** (`/run-plan`) as copy-only pastes. +### Claude Code CLI (session kit-load) + +Install writes a thin root `CLAUDE.md` and `.claude/commands/agent-kit.md` so Claude Code (including a session started in the Cursor terminal) can load Agent Kit context without rediscovering the repository. Cursor agents already get that context from `sessionStart` and always-apply rules. + +1. Start Claude Code in the project root. +2. `CLAUDE.md` loads automatically. Mid-session refresh: type `/agent-kit`. +3. Claude should read `.cursor/HANDOFF.md`, `.cursor/project-context.md`, and `.cursor/commands/` instead of a full archaeology pass. + +HITL in Claude Code is a numbered-list fallback (Cursor Ask questions is not available). Never `/git-prod` from kit-load. + +Pack contract: [claude-cli-kit-load.md](claude-cli-kit-load.md). This is **not** audits, **not** `agent-kit run-plan --backend claude`, and **not** Action A7 (Windsurf / VS Code generators). + ### Optional external plan review -When `/run-plan` finishes all implementable to-dos, you can get a second-agent check of the shipped work. Artifacts ship with L0; the feature stays opt-in (`enabled: false` by default). +When `/run-plan` finishes all implementable to-dos, you can get a second-agent check of the shipped work. Artifacts ship with L0; the feature stays opt-in (`enabled: false` by default). Session kit-load (`CLAUDE.md` / `/agent-kit`) is a different surface; see [Claude Code CLI (session kit-load)](#claude-code-cli-session-kit-load). 1. **Enable it:** set `"externalPlanReview": { "enabled": true, "offerOnExhausted": true }` in `.cursor/context/config.json` (see `config.example.json`), or accept the exhaustion Ask when a single `/run-plan` finishes (or once when a `/run-plan-all` queue exhausts) 2. **Chat path:** when enabled or you pick `Run review now`, the agent prepares a paste command (`--paste-only` / `--force --paste-only`); you run it in **your** Cursor Terminal (not a silent agent-shell `claude -p`) diff --git a/docs/marketplace.md b/docs/marketplace.md index 47b7a91..56bfdb0 100644 --- a/docs/marketplace.md +++ b/docs/marketplace.md @@ -68,7 +68,7 @@ The plugin root is the **parent of `.cursor-plugin/`** - the repo root. Cursor's |--------------|-------|-----| | `rules` | `.cursor/rules` | 25 `.mdc`; 10 `alwaysApply: true` structural, the rest glob-gated per stack | | `skills` | `.cursor/skills/core` | **Core only.** Discovery matches direct children holding a `SKILL.md`, so `.cursor/skills` (whose children are `core/` and `community/`) would find nothing. Pointing at `core/` also encodes the thesis: stack skills stay on `agent-kit add` | -| `agents` | `.cursor/agents` | 13 subagent definitions | +| `agents` | `.cursor/agents` | 14 subagent definitions | | `commands` | `.cursor/commands` | 27 slash commands | | `hooks` | `.cursor/hooks.json` | Thin adapters; every one is fail-open, so an unresolved path degrades quietly | | `logo` | `dashboard/logo.svg` | Must sit on a path inside `scripts/public-sync.manifest`. `assets/**` is **not** synced and would be dropped from the public mirror without failing anything | diff --git a/docs/npm-publish-checklist.md b/docs/npm-publish-checklist.md index 818d5dd..c7bd463 100644 --- a/docs/npm-publish-checklist.md +++ b/docs/npm-publish-checklist.md @@ -80,11 +80,11 @@ If the answer is not an explicit **yes**, stop. No tag push, no token change for These rows are **post-publish** only. Do not mark them Met from pre-tag mechanism review while registry `latest` is still behind the tree. -- [ ] `npm view @dadado/agent-kit-cli version` matches the release (for 5.0: expect `5.0.0`, not residual `4.8.9`). +- [x] `npm view @dadado/agent-kit-cli version` matches the release (for 5.0: expect `5.0.0`, not residual `4.8.9`). - [ ] Smoke install: `npx @dadado/agent-kit-cli@ --help` (or `pnpm dlx`). -- [ ] Blank-folder dogfood: `npx @dadado/agent-kit-cli@5.0 install` (or `@5.0.0`) plus five assertions (`--version`, manifest pin, `hooks.json`, dashboard invoke, no nested `agent-kit/` folder). Owned after publish; blocked while `latest` is 4.8.9. +- [x] Blank-folder dogfood: `npx @dadado/agent-kit-cli@5.0 install` (or `@5.0.0`) plus five assertions (`--version`, manifest pin, `hooks.json`, dashboard invoke, no nested `agent-kit/` folder). Verified 2026-08-12 against npm `latest` 5.0.0 (see monitor D2 Closed-by). - [ ] Public GitHub Release Latest and public storefront label resolve to 5.0 (after tag + sync-public), not pre-tag mechanism-only checks. -- [ ] Scoped Path C smoke (blank folder): install the published package under `node_modules/@dadado/agent-kit-cli` and confirm `agent-kit dashboard` returns HTTP 200 on loopback (required after Path C / detach-start changes; also a `/git-prod` §12.5 row). +- [x] Scoped Path C smoke (blank folder): `npx @dadado/agent-kit-cli@5.0.0 dashboard --no-open` served from `_npx/.../@dadado/agent-kit-cli/dashboard/` with HTTP 200 on loopback (2026-08-12 D2 dogfood). - [ ] GitHub Actions `publish-npm` job for the tag shows publish success (not skip), when using CI. ## Cross-lens go/no-go (before tagging) diff --git a/docs/public-launch-announcement.md b/docs/public-launch-announcement.md index 7ea1062..fef4af7 100644 --- a/docs/public-launch-announcement.md +++ b/docs/public-launch-announcement.md @@ -2,7 +2,7 @@ Copy-paste text for announcing the public repository (chat, social, communities). Portuguese body is the shippable launch copy; keep product claims aligned with [getting-started.md](getting-started.md), the root `README.md`, and the dual-name contract (Mission Kit marketing / Agent Kit install). -Related: [Public launch go/no-go](public-launch.md). Naming: `2026-08-06_mission-kit-vs-agent-kit-naming`. +Related: [Public launch go/no-go](public-launch.md). Recurring recap/release drafts: [comms.md](comms.md). Naming: `2026-08-06_mission-kit-vs-agent-kit-naming`. ```text Fala, devs! 🚀 diff --git a/install.md b/install.md index cc7ad66..5d209aa 100644 --- a/install.md +++ b/install.md @@ -154,7 +154,7 @@ Create if it doesn't exist (adjust `version` / `registry` to current SoT): ```json { "schemaVersion": 1, - "version": "5.0.0", + "version": "5.1.0", "profile": "default", "packs": [], "skills": [], diff --git a/package.json b/package.json index 27b1b30..ac2e898 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "agent-kit", - "version": "5.0.0", + "version": "5.1.0", "description": "HITL framework for AI-assisted IDEs: plan, handoff, staging-to-prod, memory loop; project-aware setup for Cursor, VS Code, and Windsurf.", "private": true, "license": "PolyForm-Noncommercial-1.0.0", diff --git a/packages/cli/README.md b/packages/cli/README.md index 6b49ff9..71caf05 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -55,6 +55,8 @@ NO_COLOR=1 agent-kit Subcommands and `agent-kit --version` are unchanged. Chat-only HITL flows (`/start-project`, `/git-staging`, `/git-prod`, `/run-plan-all`, backlog CRUD) are not CLI commands. +On an interactive TTY, long-running commands (`init`, `install`, `doctor`, `update`, `run-plan` ticks) show an in-process ANSI spinner plus a rotating Mission Kit tip. Set `AGENT_KIT_REDUCED_MOTION=1` for static text on a capable TTY. Runtime dependencies stay `@clack/prompts`, `citty`, and `kolorist` (no `ora` / `figlet` / `chalk` / `ink`). Window titles for `agent-kit dashboard` and `agent-kit dashboard-broadcast` use the workspace basename, not the CLI package folder. + ## Common commands | Command | Purpose | diff --git a/packages/cli/package.json b/packages/cli/package.json index 1f3b6b6..bdfd024 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@dadado/agent-kit-cli", - "version": "5.0.0", + "version": "5.1.0", "description": "Agent Kit CLI: HITL framework install and tooling for AI-assisted IDEs (rules, skills, plan/handoff, context).", "license": "PolyForm-Noncommercial-1.0.0", "type": "module", diff --git a/packages/cli/src/commands/dashboard-broadcast.ts b/packages/cli/src/commands/dashboard-broadcast.ts index 1412c43..3c97094 100644 --- a/packages/cli/src/commands/dashboard-broadcast.ts +++ b/packages/cli/src/commands/dashboard-broadcast.ts @@ -5,8 +5,9 @@ import { defineCommand } from "citty"; import { logger } from "../utils/logger.js"; import { type FindDashboardOptions, - applyDashboardOpenEnv, bundledDashboardCandidates, + dashboardProcessTitle, + dashboardSpawnEnv, findDashboardStart, resolveDashboardSnapshotRoot, } from "./dashboard.js"; @@ -124,10 +125,11 @@ export const dashboardBroadcastCommand = defineCommand({ } const snapshotRoot = resolveDashboardSnapshotRoot(args.cwd); - const env = applyDashboardOpenEnv( - { ...process.env }, - { noOpen: Boolean(args["no-open"]), browser: args.browser, cwd: snapshotRoot }, - ); + const env = dashboardSpawnEnv({ ...process.env }, snapshotRoot, { + noOpen: Boolean(args["no-open"]), + browser: args.browser, + }); + process.title = dashboardProcessTitle(snapshotRoot); const code = await runStartScript(startPath, env); if (code !== 0) process.exitCode = code; diff --git a/packages/cli/src/commands/dashboard.test.ts b/packages/cli/src/commands/dashboard.test.ts index 63c5beb..583aec4 100644 --- a/packages/cli/src/commands/dashboard.test.ts +++ b/packages/cli/src/commands/dashboard.test.ts @@ -1,12 +1,14 @@ import { execFileSync } from "node:child_process"; import { mkdirSync, mkdtempSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; -import { dirname, join } from "node:path"; +import { dirname, join, resolve } from "node:path"; import { pathToFileURL } from "node:url"; import { describe, expect, it } from "vitest"; import { findDashboardBroadcastStart } from "../commands/dashboard-broadcast.js"; import { bundledDashboardCandidates, + dashboardProcessTitle, + dashboardSpawnEnv, findDashboardStart, resolveDashboardSnapshotRoot, } from "../commands/dashboard.js"; @@ -138,3 +140,20 @@ describe("findDashboardBroadcastStart", () => { await expect(findDashboardBroadcastStart(consumer, {}, { moduleUrl })).resolves.toBeNull(); }); }); + +describe("dashboardSpawnEnv / dashboardProcessTitle", () => { + it("sets MISSION_CONTROL_REPO_ROOT to the resolved snapshot root", () => { + const root = join(tmpdir(), "ak-workspace"); + const env = dashboardSpawnEnv({ FOO: "1" }, root, {}); + expect(env.MISSION_CONTROL_REPO_ROOT).toBe(resolve(root)); + expect(env.FOO).toBe("1"); + }); + + it("uses workspace basename only (no home path, not the CLI package folder)", () => { + expect(dashboardProcessTitle("/Users/example/Git/agent-kit")).toBe( + "Mission Control · agent-kit", + ); + expect(dashboardProcessTitle("/Users/example/Git/agent-kit")).not.toContain("/Users/"); + expect(dashboardProcessTitle("/tmp/work/my-app")).toBe("Mission Control · my-app"); + }); +}); diff --git a/packages/cli/src/commands/dashboard.ts b/packages/cli/src/commands/dashboard.ts index 2eba958..c2f68a1 100644 --- a/packages/cli/src/commands/dashboard.ts +++ b/packages/cli/src/commands/dashboard.ts @@ -53,6 +53,22 @@ export function applyDashboardOpenEnv( return next; } +/** Snapshot env + open flags for Mission Control starters. */ +export function dashboardSpawnEnv( + env: NodeJS.ProcessEnv, + snapshotRoot: string, + opts: { noOpen?: boolean; browser?: string }, +): NodeJS.ProcessEnv { + const root = path.resolve(snapshotRoot); + return applyDashboardOpenEnv({ ...env, MISSION_CONTROL_REPO_ROOT: root }, { ...opts, cwd: root }); +} + +/** Terminal / tab identity: workspace basename only (no absolute home paths). */ +export function dashboardProcessTitle(snapshotRoot: string): string { + const base = path.basename(path.resolve(snapshotRoot)) || "workspace"; + return `Mission Control · ${base}`; +} + /** Candidates for dashboard assets shipped beside the CLI package (Path C). */ export function bundledDashboardCandidates( filename: "start.mjs" | "start-broadcast.mjs", @@ -222,10 +238,11 @@ export const dashboardCommand = defineCommand({ return; } - const env = applyDashboardOpenEnv( - { ...process.env, MISSION_CONTROL_REPO_ROOT: snapshotRoot }, - { noOpen: Boolean(args["no-open"]), browser: args.browser, cwd: snapshotRoot }, - ); + const env = dashboardSpawnEnv({ ...process.env }, snapshotRoot, { + noOpen: Boolean(args["no-open"]), + browser: args.browser, + }); + process.title = dashboardProcessTitle(snapshotRoot); const code = await runStartScript(startPath, env); if (code !== 0) process.exitCode = code; diff --git a/packages/cli/src/commands/doctor.ts b/packages/cli/src/commands/doctor.ts index 7667f32..02a131d 100644 --- a/packages/cli/src/commands/doctor.ts +++ b/packages/cli/src/commands/doctor.ts @@ -7,6 +7,7 @@ import { executeSafeReadinessFixes } from "../scanner/safe-fixes.js"; import { runScanner } from "../scanner/scan.js"; import { writeReadinessSnapshot } from "../scanner/snapshot.js"; import type { ReadinessReport, SafeReadinessChange } from "../types.js"; +import { withCliProgress } from "../welcome/visual-kit.js"; export interface DoctorResult { report: ReadinessReport; @@ -97,7 +98,8 @@ export const doctorCommand = defineCommand({ }, }, async run({ args }) { - const result = await runDoctor(args.cwd, { fixSafe: args["fix-safe"] }); + const run = () => runDoctor(args.cwd, { fixSafe: args["fix-safe"] }); + const result = args.json ? await run() : await withCliProgress("doctor", run); if (args.json) { console.log(JSON.stringify(result, null, 2)); return; diff --git a/packages/cli/src/commands/init.ts b/packages/cli/src/commands/init.ts index 98b7c77..bfe5db9 100644 --- a/packages/cli/src/commands/init.ts +++ b/packages/cli/src/commands/init.ts @@ -3,6 +3,7 @@ import { defineCommand } from "citty"; import { KIT_VERSION } from "../lifecycle/version.js"; import { logger } from "../utils/logger.js"; import { classifyInstallError, isNonInteractive } from "../utils/terminal.js"; +import { withCliProgress } from "../welcome/visual-kit.js"; import { type InstallResult, performInstall } from "./install.js"; type CompatibilityInstaller = (options: { cwd: string }) => Promise; @@ -35,7 +36,7 @@ export const initCommand = defineCommand({ } logger.info("init now uses the canonical install and readiness workflow."); try { - const result = await runInitCompatibility(args.cwd); + const result = await withCliProgress("init", () => runInitCompatibility(args.cwd)); const pending = result.readiness.pendingActions.length; logger.success(`L0 and readiness prepared in ${result.projectRoot}`); const nextStep = diff --git a/packages/cli/src/commands/install.ts b/packages/cli/src/commands/install.ts index 9169f79..eaf629f 100644 --- a/packages/cli/src/commands/install.ts +++ b/packages/cli/src/commands/install.ts @@ -21,6 +21,7 @@ import { confirmProjectRoot, isNonInteractive, } from "../utils/terminal.js"; +import { withCliProgress } from "../welcome/visual-kit.js"; function parsePackList(raw: string | undefined): string[] { if (!raw?.trim()) return []; @@ -196,15 +197,17 @@ export const installCommand = defineCommand({ } try { - const result = await performInstall({ - cwd: projectRoot, - profile: args.profile as string | undefined, - pack: args.pack, - registry: args.registry, - url: args.url, - ref: args.ref, - refresh: args.refresh, - }); + const result = await withCliProgress("install", () => + performInstall({ + cwd: projectRoot, + profile: args.profile as string | undefined, + pack: args.pack, + registry: args.registry, + url: args.url, + ref: args.ref, + refresh: args.refresh, + }), + ); logApplyStats(result.stats); logger.success(`Manifest written: ${result.manifestPath}`); logger.success("Readiness snapshot written: .cursor/context/readiness.json"); diff --git a/packages/cli/src/commands/update.test.ts b/packages/cli/src/commands/update.test.ts index 7bb59ef..b0c960a 100644 --- a/packages/cli/src/commands/update.test.ts +++ b/packages/cli/src/commands/update.test.ts @@ -86,4 +86,67 @@ describe("updateCommand", () => { // original installedAt value, not a fresh timestamp. expect(saved.installedAt).toBe("2026-07-30T00:00:00.000Z"); }, 15_000); + + it("passes --registry into update --check", async () => { + const consumer = await mkdtemp(path.join(tmpdir(), "ak-update-check-c-")); + const kit = await mkdtemp(path.join(tmpdir(), "ak-update-check-k-")); + await mkdir(path.join(consumer, ".cursor"), { recursive: true }); + await writeFile( + path.join(consumer, ".cursor", "agent-kit.json"), + JSON.stringify({ + schemaVersion: 1, + version: "5.0.0", + packs: [], + skills: [], + protected: [], + registry: { url: "https://github.com/agent-kit-startup/agent-kit", ref: "main" }, + }), + "utf8", + ); + await mkdir(path.join(kit, "registry"), { recursive: true }); + await writeFile(path.join(kit, "registry", "registry.json"), "{}\n", "utf8"); + await mkdir(path.join(kit, "packages", "cli"), { recursive: true }); + await writeFile( + path.join(kit, "packages", "cli", "package.json"), + JSON.stringify({ version: "5.1.0" }), + "utf8", + ); + + const logs: string[] = []; + const spy = vi.spyOn(console, "log").mockImplementation((line: unknown) => { + logs.push(String(line)); + }); + try { + await ( + updateCommand.run as unknown as (ctx: { args: Record }) => Promise + )({ + args: { + _: [], + cwd: consumer, + check: true, + json: true, + "respect-prefs": false, + stamp: false, + "seed-overlay": false, + registry: kit, + url: undefined as unknown as string, + ref: undefined as unknown as string, + refresh: false, + }, + }); + } finally { + spy.mockRestore(); + } + + const payload = JSON.parse(logs.join("\n")) as { + status: string; + registryUrl: string; + registryRef: string; + message: string; + }; + expect(payload.status).toBe("update-available"); + expect(payload.registryUrl).toBe(path.resolve(kit)); + expect(payload.registryRef).toBe("local"); + expect(payload.message).not.toMatch(/public/i); + }); }); diff --git a/packages/cli/src/commands/update.ts b/packages/cli/src/commands/update.ts index 4d113b5..d6c13d6 100644 --- a/packages/cli/src/commands/update.ts +++ b/packages/cli/src/commands/update.ts @@ -9,6 +9,7 @@ import { KIT_VERSION } from "../lifecycle/version.js"; import { loadAgentKitManifest } from "../manifest/index.js"; import { logger } from "../utils/logger.js"; import { RootRefusedError, confirmProjectRoot, isNonInteractive } from "../utils/terminal.js"; +import { withCliProgress } from "../welcome/visual-kit.js"; export const updateCommand = defineCommand({ meta: { @@ -24,7 +25,7 @@ export const updateCommand = defineCommand({ check: { type: "boolean", description: - "Check-only: compare installed version to latest public tag; never apply L0 writes", + "Check-only: compare installed version to the resolved registry (public tags, or --registry local checkout); never apply L0 writes", default: false, }, json: { @@ -68,6 +69,8 @@ export const updateCommand = defineCommand({ respectPrefs: Boolean(args["respect-prefs"]), stamp: Boolean(args.stamp), publicRegistryUrl: typeof args.url === "string" && args.url ? args.url : undefined, + registryPath: + typeof args.registry === "string" && args.registry ? args.registry : undefined, }); if (args.json) { @@ -140,7 +143,9 @@ export const updateCommand = defineCommand({ await seedManagedHashLedger(projectRoot); logger.info("Seeded managed-hash ledger from current local overlay files."); } - const stats = await syncFromManifest(registry.root, projectRoot, next); + const stats = await withCliProgress("update", () => + syncFromManifest(registry.root, projectRoot, next), + ); await saveManifest(projectRoot, next); logApplyStats(stats); logger.success("Update complete (L3 protected paths left untouched)."); diff --git a/packages/cli/src/dashboard/guards.test.ts b/packages/cli/src/dashboard/guards.test.ts index dc812b9..0b0cb84 100644 --- a/packages/cli/src/dashboard/guards.test.ts +++ b/packages/cli/src/dashboard/guards.test.ts @@ -56,6 +56,10 @@ describe("allowlistConfig", () => { offerOnExhausted: false, autoRemediate: false, backend: "claude", + reviewerModel: "haiku", + advisorModel: "opus", + waitSliceSeconds: 90, + waitTimeoutSeconds: 900, mode: "autonomous", midBatchAudits: true, preflight: "warn", @@ -82,6 +86,10 @@ describe("allowlistConfig", () => { mode: "autonomous", midBatchAudits: true, preflight: "warn", + reviewerModel: "haiku", + advisorModel: "opus", + waitSliceSeconds: 90, + waitTimeoutSeconds: 900, }, agentPersona: { default: "night-shift", @@ -123,7 +131,11 @@ describe("config write allowlist", () => { updateCheck: { enabled: true, intervalDays: 7 }, externalPlanReview: { enabled: true, - backend: "claude", + backend: "auto", + reviewerModel: "haiku", + advisorModel: "opus", + waitSliceSeconds: 90, + waitTimeoutSeconds: 900, autoRemediate: false, offerOnExhausted: true, mode: "autonomous", @@ -178,6 +190,18 @@ describe("config write allowlist", () => { expect(validateConfigWriteBody({ externalPlanReview: { unknownAuditsKey: true } }).ok).toBe( false, ); + expect(validateConfigWriteBody({ externalPlanReview: { backend: "auto" } }).ok).toBe(true); + expect(validateConfigWriteBody({ externalPlanReview: { backend: "cursor" } }).ok).toBe(true); + expect(validateConfigWriteBody({ externalPlanReview: { backend: "windsurf" } }).ok).toBe(false); + expect( + validateConfigWriteBody({ + externalPlanReview: { reviewerModel: "haiku", waitSliceSeconds: 90 }, + }).ok, + ).toBe(true); + expect(validateConfigWriteBody({ externalPlanReview: { waitSliceSeconds: 0 } }).ok).toBe(false); + expect(validateConfigWriteBody({ externalPlanReview: { reviewerModel: "bad name" } }).ok).toBe( + false, + ); }); it("merges without wiping onboarding nests", () => { diff --git a/packages/cli/src/dashboard/plugin-ux-validation.test.ts b/packages/cli/src/dashboard/plugin-ux-validation.test.ts index 31bea1f..4ccdd91 100644 --- a/packages/cli/src/dashboard/plugin-ux-validation.test.ts +++ b/packages/cli/src/dashboard/plugin-ux-validation.test.ts @@ -2764,8 +2764,13 @@ describe("plugin-ux-validation: hardening regressions", () => { expect(staticFn).not.toContain("/api/config"); // Clipboard failure toast truncates long snippets instead of dumping the full body. expect(dashboardHtml).toContain("const preview = raw.length > 120"); - // Dead-control hints: backend is claude-only; updateApply.auto never writable. - expect(dashboardHtml).toContain("claude is the only backend today."); + // Audits backend cascade is writable (auto / claude / cursor); updateApply.auto never writable. + expect(dashboardHtml).toContain("auto uses Claude when usable, else Cursor Agent."); + expect(dashboardHtml).not.toContain("claude is the only backend today."); + expect(dashboardHtml).toContain('option value="auto"'); + expect(dashboardHtml).toContain('option value="cursor"'); + expect(dashboardHtml).toContain('id="config-epr-reviewerModel"'); + expect(dashboardHtml).toContain('id="config-epr-waitSlice"'); expect(dashboardHtml).toContain("updateApply.auto is never writable here."); }); diff --git a/packages/cli/src/generator/claude-kit-load.test.ts b/packages/cli/src/generator/claude-kit-load.test.ts new file mode 100644 index 0000000..92182d8 --- /dev/null +++ b/packages/cli/src/generator/claude-kit-load.test.ts @@ -0,0 +1,72 @@ +import { mkdtemp, readFile, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { describe, expect, it } from "vitest"; +import { detectContext } from "../scanner/detect-repository.js"; +import { fileExists } from "../utils/fs.js"; +import { + AGENT_KIT_COMMAND_REL, + CLAUDE_MD_REL, + generateClaudeKitLoadArtifacts, + renderAgentKitCommand, + renderClaudeMd, +} from "./claude-kit-load.js"; + +const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../../.."); + +describe("generateClaudeKitLoadArtifacts", () => { + it("matches the pack-contract canonical fences", async () => { + const docs = await readFile(path.join(REPO_ROOT, "docs/claude-cli-kit-load.md"), "utf8"); + expect(docs).toContain(renderClaudeMd().trimEnd()); + expect(docs).toContain(renderAgentKitCommand().trimEnd()); + }); + + it("keeps factory dogfood files identical to generator snippets", async () => { + expect(await readFile(path.join(REPO_ROOT, CLAUDE_MD_REL), "utf8")).toBe(renderClaudeMd()); + expect(await readFile(path.join(REPO_ROOT, AGENT_KIT_COMMAND_REL), "utf8")).toBe( + renderAgentKitCommand(), + ); + }); + + it("writes CLAUDE.md and /agent-kit on first run", async () => { + const root = await mkdtemp(path.join(tmpdir(), "ak-claude-load-")); + const results = await generateClaudeKitLoadArtifacts(root); + + expect(results).toEqual([ + { relativePath: CLAUDE_MD_REL, status: "applied" }, + { relativePath: AGENT_KIT_COMMAND_REL, status: "applied" }, + ]); + + expect(await readFile(path.join(root, CLAUDE_MD_REL), "utf8")).toBe(renderClaudeMd()); + expect(await readFile(path.join(root, AGENT_KIT_COMMAND_REL), "utf8")).toBe( + renderAgentKitCommand(), + ); + + const context = await detectContext(root); + expect(context.hasAgentGuidance).toBe(true); + expect(context.sources).toEqual( + expect.arrayContaining([{ source: "file", value: "CLAUDE.md:agent guidance" }]), + ); + }); + + it("skips existing consumer files without overwriting them", async () => { + const root = await mkdtemp(path.join(tmpdir(), "ak-claude-load-skip-")); + await writeFile(path.join(root, CLAUDE_MD_REL), "# Existing Claude notes\n", "utf8"); + + const results = await generateClaudeKitLoadArtifacts(root); + + expect(results).toContainEqual({ + relativePath: CLAUDE_MD_REL, + status: "skipped-customized", + }); + expect(results).toContainEqual({ + relativePath: AGENT_KIT_COMMAND_REL, + status: "applied", + }); + expect(await readFile(path.join(root, CLAUDE_MD_REL), "utf8")).toBe( + "# Existing Claude notes\n", + ); + expect(await fileExists(path.join(root, AGENT_KIT_COMMAND_REL))).toBe(true); + }); +}); diff --git a/packages/cli/src/generator/claude-kit-load.ts b/packages/cli/src/generator/claude-kit-load.ts new file mode 100644 index 0000000..bbcf015 --- /dev/null +++ b/packages/cli/src/generator/claude-kit-load.ts @@ -0,0 +1,83 @@ +import { writeFile } from "node:fs/promises"; +import path from "node:path"; +import { ensureDir, fileExists } from "../utils/fs.js"; + +export const CLAUDE_MD_REL = "CLAUDE.md"; +export const AGENT_KIT_COMMAND_REL = ".claude/commands/agent-kit.md"; + +export interface ClaudeKitLoadArtifactResult { + relativePath: string; + status: "applied" | "skipped-customized"; +} + +export function renderClaudeMd(): string { + return `# Agent Kit (Claude Code) + +This repository uses Agent Kit / Mission Control. Before rediscovering the tree, read the shared sources of truth (paths below are pointers; do not treat this file as a second rulebook). + +## Read first + +1. \`AGENTS.md\` - cross-IDE contract +2. \`.cursor/project-context.md\` - verified repository facts (derived; prefer code, tests, SHAs when docs conflict) +3. \`.cursor/HANDOFF.md\` - if present: active plan, next to-do, queue fields +4. \`.cursor/commands/\` - slash catalog (Cursor). Follow the same HITL contracts here. + +Mid-session refresh: \`/agent-kit\`. + +## HITL + +Cursor Ask questions is not available in this CLI. When a command requires a choice, list the same labels as a numbered list and wait. Skip or cancel means stop. Never \`/git-prod\` without an explicit operator yes. + +## Non-goals + +- Not Action A7 (Windsurf / VS Code generator parity) +- Not Claude external plan-review audits (\`/plan-external-review\`) +- Not \`--backend claude\` plan-loop ticks +- Not a copy of Cursor \`sessionStart\` / other IDE hooks +`; +} + +export function renderAgentKitCommand(): string { + return `--- +description: Load Agent Kit session context (HANDOFF, project-context, commands). Manual refresh only. +disable-model-invocation: true +--- + +Read these files if they exist, then summarize the active plan, next to-do, and any Gaps. Do not scan the whole repository first. + +1. \`AGENTS.md\` +2. \`.cursor/project-context.md\` +3. \`.cursor/HANDOFF.md\` +4. The plan file named in HANDOFF \`- **Plan:**\` under \`.cursor/plans/\` + +If HANDOFF is missing, say so and point at \`/agent-kit-onboard\` or \`/start-project\` rather than inventing a plan. + +HITL: numbered-list fallback for Ask questions labels. Never \`/git-prod\` from this skill. + +Non-goals: not audits / \`/plan-external-review\`, not \`--backend claude\` ticks, not A7, not Cursor hook clones. +`; +} + +async function writeUnlessExists( + rootDir: string, + relativePath: string, + content: string, +): Promise { + const target = path.join(rootDir, relativePath); + if (await fileExists(target)) { + return { relativePath, status: "skipped-customized" }; + } + await ensureDir(path.dirname(target)); + await writeFile(target, content, "utf8"); + return { relativePath, status: "applied" }; +} + +/** Always emit (not detectIde). Skip each path independently when the consumer already has a file. */ +export async function generateClaudeKitLoadArtifacts( + rootDir: string, +): Promise { + return Promise.all([ + writeUnlessExists(rootDir, CLAUDE_MD_REL, renderClaudeMd()), + writeUnlessExists(rootDir, AGENT_KIT_COMMAND_REL, renderAgentKitCommand()), + ]); +} diff --git a/packages/cli/src/generator/index.ts b/packages/cli/src/generator/index.ts index 8d5c3a9..f1e4c1b 100644 --- a/packages/cli/src/generator/index.ts +++ b/packages/cli/src/generator/index.ts @@ -1,5 +1,6 @@ import type { ProjectProfile } from "../types.js"; import { generateAgentsMd } from "./agents-md.js"; +import { generateClaudeKitLoadArtifacts } from "./claude-kit-load.js"; import { generateGitHooks } from "./git-hooks.js"; export { @@ -17,5 +18,6 @@ export type { /** Compatibility path for legacy callers. Repository readiness uses applyPersonalization. */ export async function generateFromProfile(profile: ProjectProfile): Promise { await generateAgentsMd(profile); + await generateClaudeKitLoadArtifacts(profile.rootDir); if (profile.installHooks) await generateGitHooks(profile); } diff --git a/packages/cli/src/generator/personalization.test.ts b/packages/cli/src/generator/personalization.test.ts index 8664f37..910b893 100644 --- a/packages/cli/src/generator/personalization.test.ts +++ b/packages/cli/src/generator/personalization.test.ts @@ -112,7 +112,12 @@ describe("repository personalization", () => { ]), ); expect(applied.manifest.protected).toEqual( - expect.arrayContaining(["AGENTS.md", ".cursor/project-context.md"]), + expect.arrayContaining([ + "AGENTS.md", + ".cursor/project-context.md", + "CLAUDE.md", + ".claude/commands/agent-kit.md", + ]), ); expect(applied.manifest.personalization).toEqual( expect.objectContaining({ diff --git a/packages/cli/src/generator/personalization.ts b/packages/cli/src/generator/personalization.ts index 9c9ee6f..94050e9 100644 --- a/packages/cli/src/generator/personalization.ts +++ b/packages/cli/src/generator/personalization.ts @@ -19,6 +19,7 @@ import type { RepositoryPurpose, } from "../types.js"; import { ensureDir, fileExists, writeJson } from "../utils/fs.js"; +import { generateClaudeKitLoadArtifacts } from "./claude-kit-load.js"; import { generateVSCodeArtifacts } from "./vscode.js"; export const PERSONALIZATION_CONTRACT_VERSION = 1 as const; @@ -356,6 +357,18 @@ export async function applyPersonalization(input: { protectedPaths.add(CONTEXT_PATH); protectedPaths.add(AGENTS_PATH); + const claudeResults = await generateClaudeKitLoadArtifacts(input.rootDir); + const claudeItems = claudeResults.map((artifact) => { + protectedPaths.add(artifact.relativePath); + return { + kind: "file" as const, + id: artifact.relativePath, + path: artifact.relativePath, + status: artifact.status, + evidence: profileEvidence, + }; + }); + const ideDetection = await detectIde(input.rootDir); if (ideDetection.ide === "vscode" || ideDetection.ide === "other") { const git: GitDetection = { @@ -404,7 +417,7 @@ export async function applyPersonalization(input: { contractVersion: PERSONALIZATION_CONTRACT_VERSION, generatorVersion: input.generatorVersion, repositoryFingerprint: input.report.repositoryFingerprint, - items: [...fileResults, ...componentResults], + items: [...fileResults, ...claudeItems, ...componentResults], protectedPaths: [...protectedPaths].sort(), }; await writeJson(path.join(input.rootDir, RESULT_PATH), result); diff --git a/packages/cli/src/lifecycle/check-updates.test.ts b/packages/cli/src/lifecycle/check-updates.test.ts index 9cdc775..4549df5 100644 --- a/packages/cli/src/lifecycle/check-updates.test.ts +++ b/packages/cli/src/lifecycle/check-updates.test.ts @@ -199,4 +199,61 @@ describe("checkForUpdates", () => { const result = await checkForUpdates(cwd); expect(result.status).toBe("skipped-no-manifest"); }); + + it("compares --registry local checkout instead of public tags", async () => { + const cwd = await mkdtemp(path.join(tmpdir(), "ak-check-consumer-")); + const kit = await mkdtemp(path.join(tmpdir(), "ak-check-kit-")); + temps.push(cwd, kit); + await writeManifest(cwd, "5.0.0", { + url: "https://github.com/agent-kit-startup/agent-kit", + ref: "main", + }); + await mkdir(path.join(kit, "registry"), { recursive: true }); + await writeFile(path.join(kit, "registry", "registry.json"), "{}\n", "utf8"); + await mkdir(path.join(kit, "packages", "cli"), { recursive: true }); + await writeFile( + path.join(kit, "packages", "cli", "package.json"), + JSON.stringify({ version: "5.1.0" }), + "utf8", + ); + + const result = await checkForUpdates(cwd, { registryPath: kit }); + expect(result.status).toBe("update-available"); + expect(result.installedVersion).toBe("5.0.0"); + expect(result.latestVersion).toBe("5.1.0"); + expect(result.registryUrl).toBe(path.resolve(kit)); + expect(result.registryRef).toBe("local"); + expect(result.message).not.toMatch(/public/i); + expect(result.applyRecommended).toBe(false); + }); + + it("does not report public up-to-date when local --registry L0 drifted", async () => { + const cwd = await mkdtemp(path.join(tmpdir(), "ak-check-drift-c-")); + const kit = await mkdtemp(path.join(tmpdir(), "ak-check-drift-k-")); + temps.push(cwd, kit); + await writeManifest(cwd, "5.0.0", { + url: "https://github.com/agent-kit-startup/agent-kit", + ref: "main", + }); + await mkdir(path.join(kit, "registry"), { recursive: true }); + await writeFile(path.join(kit, "registry", "registry.json"), "{}\n", "utf8"); + await mkdir(path.join(kit, "packages", "cli"), { recursive: true }); + await writeFile( + path.join(kit, "packages", "cli", "package.json"), + JSON.stringify({ version: "5.0.0" }), + "utf8", + ); + const rel = path.join(".cursor", "rules", "cursor-plan-handoff.mdc"); + await mkdir(path.join(kit, ".cursor", "rules"), { recursive: true }); + await mkdir(path.join(cwd, ".cursor", "rules"), { recursive: true }); + await writeFile(path.join(kit, rel), "# kit newer\n", "utf8"); + await writeFile(path.join(cwd, rel), "# consumer older\n", "utf8"); + + const result = await checkForUpdates(cwd, { registryPath: kit }); + expect(result.status).toBe("update-available"); + expect(result.registryUrl).toBe(path.resolve(kit)); + expect(result.registryRef).toBe("local"); + expect(result.message).not.toMatch(/public/i); + expect(result.message).toMatch(/drift/i); + }); }); diff --git a/packages/cli/src/lifecycle/check-updates.ts b/packages/cli/src/lifecycle/check-updates.ts index 3067b04..c70017a 100644 --- a/packages/cli/src/lifecycle/check-updates.ts +++ b/packages/cli/src/lifecycle/check-updates.ts @@ -2,8 +2,14 @@ import { execFile } from "node:child_process"; import path from "node:path"; import { promisify } from "node:util"; import { loadAgentKitManifest } from "../manifest/index.js"; -import { DEFAULT_REGISTRY_URL, assertSafeRegistrySource } from "../registry/resolve.js"; +import type { AgentKitManifest } from "../manifest/types.js"; +import { + DEFAULT_REGISTRY_URL, + assertSafeRegistrySource, + resolveRegistryRoot, +} from "../registry/resolve.js"; import { readJson, writeJson } from "../utils/fs.js"; +import { diffAgainstRegistry, summarizeDiff } from "./diff.js"; const execFileAsync = promisify(execFile); @@ -194,14 +200,142 @@ export interface CheckForUpdatesOptions { stamp?: boolean; /** Override public registry URL for tag lookup (HTTPS only). */ publicRegistryUrl?: string; + /** Local kit checkout (`--registry`). Compared instead of public tags. */ + registryPath?: string; /** Injected latest version (tests). */ latestVersion?: string | null; /** Injected now (tests). */ now?: Date; } +async function readLocalKitVersion(registryRoot: string): Promise { + for (const rel of ["packages/cli/package.json", "package.json"] as const) { + const data = await readJson<{ version?: unknown }>(path.join(registryRoot, rel)); + if (data && typeof data.version === "string" && data.version.length > 0) { + return data.version; + } + } + return null; +} + +async function checkAgainstLocalRegistry( + cwd: string, + manifest: AgentKitManifest, + options: CheckForUpdatesOptions, +): Promise { + const registryPath = options.registryPath; + if (!registryPath) { + throw new Error("checkAgainstLocalRegistry requires registryPath"); + } + + let resolved: Awaited>; + try { + resolved = await resolveRegistryRoot({ cwd, registryPath }); + } catch (err) { + const msg = err instanceof Error ? err.message : String(err); + return { + status: "error", + installedVersion: manifest.version, + latestVersion: null, + registryUrl: path.resolve(registryPath), + registryRef: "local", + applyRecommended: false, + message: `Failed to resolve --registry: ${msg}`, + }; + } + + const registryUrl = resolved.root; + const registryRef = "local"; + + try { + let latest: string | null; + if (options.latestVersion !== undefined) { + latest = options.latestVersion; + } else { + const raw = await readLocalKitVersion(resolved.root); + latest = raw ? normalizeSemver(raw) : null; + } + + const summary = summarizeDiff(await diffAgainstRegistry(resolved.root, cwd, manifest)); + const hasFileDrift = summary.drift > 0 || summary["missing-local"] > 0; + + const installed = normalizeSemver(manifest.version); + if (!installed) { + return { + status: "error", + installedVersion: manifest.version, + latestVersion: latest, + registryUrl, + registryRef, + applyRecommended: false, + message: `Installed version "${manifest.version}" is not valid semver.`, + }; + } + + if (!latest) { + return { + status: "error", + installedVersion: installed, + latestVersion: null, + registryUrl, + registryRef, + applyRecommended: false, + message: + "No semver found in the local --registry checkout (packages/cli/package.json or package.json).", + }; + } + + const cmp = compareSemver(installed, latest); + if (hasFileDrift) { + return { + status: "update-available", + installedVersion: installed, + latestVersion: latest, + registryUrl, + registryRef, + applyRecommended: false, + message: `Local registry has drift vs this install (drift=${summary.drift}, missing-local=${summary["missing-local"]}; source v${latest}). Run /update --registry (Ask confirm) to apply; never silent.`, + }; + } + if (cmp === 0) { + return { + status: "up-to-date", + installedVersion: installed, + latestVersion: latest, + registryUrl, + registryRef, + applyRecommended: false, + message: `Installed v${installed} matches local registry v${latest}.`, + }; + } + if (cmp < 0) { + return { + status: "update-available", + installedVersion: installed, + latestVersion: latest, + registryUrl, + registryRef, + applyRecommended: false, + message: `Update available from local registry: v${installed} → v${latest}. Run /update --registry (Ask confirm) to apply; never silent.`, + }; + } + return { + status: "ahead-of-public", + installedVersion: installed, + latestVersion: latest, + registryUrl, + registryRef, + applyRecommended: false, + message: `Installed v${installed} is ahead of local registry v${latest}.`, + }; + } finally { + await resolved.unlock?.(); + } +} + /** - * Check-only: compare installed manifest version to latest public tag. + * Check-only: compare installed manifest version to latest public tag, + * or to a local `--registry` checkout when `registryPath` is set. * Never writes L0 / packs / skills. applyRecommended is always false. */ export async function checkForUpdates( @@ -221,7 +355,11 @@ export async function checkForUpdates( }; } - const registryUrl = manifest.registry?.url ?? null; + if (options.registryPath) { + return checkAgainstLocalRegistry(cwd, manifest, options); + } + + const registryUrl = options.publicRegistryUrl ?? manifest.registry?.url ?? null; const registryRef = manifest.registry?.ref ?? null; if (isFactoryOrDevRegistry(registryUrl, registryRef)) { diff --git a/packages/cli/src/lifecycle/overlay-known-hashes.ts b/packages/cli/src/lifecycle/overlay-known-hashes.ts index 1ad9cbe..7a17e88 100644 --- a/packages/cli/src/lifecycle/overlay-known-hashes.ts +++ b/packages/cli/src/lifecycle/overlay-known-hashes.ts @@ -9,8 +9,11 @@ export const KNOWN_SHIPPED_OVERLAY_HASHES: ReadonlySet = new Set([ "004d98f557942677264d053dcd8335f2bbcf19c5591123ec16a7a2697cb88932", "05ee6ecde4215e33ace0ee152ffbf2cd885d2bd4b032900f0f0bb001d350ef0f", "082bdbc584be3f3d8aa6e9d7b9f4e076450f3ff48f9147a1deb557e31475036b", + "09850afc201eccb3e700cb934400d49c9cbe7510d4a546ee84ee519771a8200e", "0b5fe4729c3c68654434837630d775b847c082418f25b0b2c581700fe84b797a", "1106b279558b6a20f87951102f172a680976f3e6530855d5dad083e1f89b9ba3", + "12bed56b332406477cf546d9e8360d3cab129bd82a638bea356726d00a6847a3", + "13f6a0711a2fcadcc7f1b751772448517ce4871cdef6a0269fc26dad51ebe26d", "15566765ec95dad3bc3160f20ac910e01a13471df536e6d990f4b263700784a9", "1681e6cf7b80a67cd999b94b8b6e16eb8caed0e11b32905848ba733da956e902", "17818e47e06349ee35c1d8e1cc16e8bcbbbcc89c664014e9096e1f08c8567807", @@ -20,6 +23,7 @@ export const KNOWN_SHIPPED_OVERLAY_HASHES: ReadonlySet = new Set([ "1c5c1af87951367ad04bb6fa5b1d6ebaaefbc0ee95989b624cabf406a1869907", "1ded5c7b7eefc355fc1d17feb65ec472e35ed96076e47ce95f7bff5c1b84746c", "1e58e0d4f2e8459b95a40d1b1e72c5c85a87230cbde945840a84073fe3a20422", + "2280e3ca96129ab2de8a85a301df7c2b15ffaa6204b734ed3b1f303e0055c41d", "285668d08f1b15d96286d27e29cc4a67e126e097fb8a940bdde33ccf25bd2bcd", "2acdb148b07f3a4872b896da419af796fd3fefaace7ac79cdee0aab447004a83", "2cd7fc018a384b0188737259ecf593ce25b4b75cc38b3e410063ac91745894b9", @@ -32,6 +36,7 @@ export const KNOWN_SHIPPED_OVERLAY_HASHES: ReadonlySet = new Set([ "46a1dd4a07a2190ab9fb09abc05a3962277209f8e0c28033bb41450301474c86", "48a0d55e21132a305946b195db7f6b75da78d44a1d32166fb0c69bef4a5ffbe2", "4b62a46c74e5fa6a80283f21e8e6711958fbcd577051dc73ff4050643a031475", + "4ba6e319ac79455a7d2713674f64fabbc9cd401b5d26b1c0edbd19c27a52297e", "4bf23acbc9bd4e0f8468e61f4970e182600b72d9025d29e81ddbd83cb7b1710a", "4c5167d0ebde685f266dce32c516a7c43876e0520647b93f07f2e2147e6a7b2f", "4dc4efcd1b43c643d3889fb349df8d4964d0867c3a67eb67e2ec168fbb70fc75", @@ -54,8 +59,10 @@ export const KNOWN_SHIPPED_OVERLAY_HASHES: ReadonlySet = new Set([ "6b66ed7747ea2d19cb457f5af656e3c07f9f563f7f32b3951fa4ae5f27f0ac46", "6dede0588274f5b44d833bcc696ec4776eb978ed24a20efdcc9c38caf3df1576", "71991a10972b8b956cae93c88517e31741c95e351814082df65e44f813590210", + "75ae3018a70ed838428c0e63e8b6901ac05bf1f1cf1845faa6f73c3940d22e4a", "75e591bfde61ca1ca3ca1303cc9bc487197af2f713c68f346a46e94ca9432fd5", "76ae85bbc336c6f435d9653e8f254ae49774b4ebaecf7335697115933eb8797c", + "76c4361e70a73276771e841e2c939253b4124d690a0fd25aff2a3ac636d8f4b0", // mission-kit-comms/SKILL.md "79878f0f6f59d1a0b65d94011c0cafe7b6f8e03e89f568aa638b8ba7a2c34076", "8108b202c829627c3c2df33575016ebe942df42acf4f1cc393900be3b82b402c", "819b7bd3ba9e8bf12dcd4f81a76c98e48261b34c397f339b36d36eb364d9c3f4", @@ -64,6 +71,7 @@ export const KNOWN_SHIPPED_OVERLAY_HASHES: ReadonlySet = new Set([ "8a11c80b0fbf6c4aabf60eec05b8cca7dccf5bca472787a8afa3028fd8fbedfa", "8b25659727063013e8bf6d7925f815b2630c2919b21e6fdac354362bf122bfab", "8c7edbacc74b1431fbecb04c9857c0e7a9cb1578fc6ca6367be888dbcec3c98f", + "8d8d60e929ebf717eed3c1a29ee0de7c71ae917d125eb1b220a77220597281ed", "8ffc181bb152c68a3353309bfdc87c42bdb925b98675cdba959c0cdb90b2adea", "91ce0c933e3c8ee81927b8224f944ec5ce5445098a2ad154773092e3541dbf69", "9309af07a0a99a56143386b3ddd2a35fae29378fba3f6b4c9297a5d1c02a9724", @@ -75,6 +83,7 @@ export const KNOWN_SHIPPED_OVERLAY_HASHES: ReadonlySet = new Set([ "9ccf52fe53c6fa4d731650ab4928ce50e201fa34f3e174fee70e3d29867fef2a", "9d905aca0123f3018c81c579217104f1749d8da802f3837b2407fbad8f2d803f", "9e324c52c1a9d1ceee738bccf47da92910aca9f7a5a21cc41738d9573c414830", + "9eb457654167df562481637b25ecd91f0ff3c9282ba79409089d644d3b09b926", "a013a6a476160183d6457ff6c1c51ec228bdca359b11eb15cd9b7fc177a705db", "a08d04f8efa121ad498f96873778c22630c9172c07c216839e348cd0bb7c2292", "a7cd7058bcb6fbd962c37317fa16893bc86a270d0164af5a0e6f3f026aa408ad", @@ -83,6 +92,8 @@ export const KNOWN_SHIPPED_OVERLAY_HASHES: ReadonlySet = new Set([ "b5c7a10df90d209be07159b7ff9bfa24e869538a0b6026bb5ce79e1cdd5b2528", "bd9fd1efacbd132c5dafc12cd0cf2b651a2b1fab7b2d7a9ba4a3e44128097f48", "be964ff33a40280e13ce6071fb3e30ce87c374527efbb74c25c9d4534084a634", + "c245035b539e41e822fab8c74fcde3b18c45f4b9f26f1c8583fc9e7254249698", + "c794accc6668d74a739c982c1b63985a8be4be08afe3766214ab1e08d25ea95c", "c96d522be440fbb638dbe953cebf1bb31185c431ac2e14446369cfb7d155a951", "cf2bd11891b934484171fa705f470de284e3874402e1e487750735dbc7692857", "d0e3bf82efbe32c09761689802ffea7838046dbdd4803b88ead5ec3dfc7efb7e", @@ -112,9 +123,11 @@ export const KNOWN_SHIPPED_OVERLAY_HASHES: ReadonlySet = new Set([ "f061432a2606049717c0cbf098ef07b1da4df2b136c4cb9cdaa7ea44c876bcd6", "f061d36a1180293519fbb560bbf659c621bd90885e995fca6eaf2eda9e418a50", "f28731a82f4b394a5765fdefe2d1b667810831f6dcea400ca62bc66e1d65371c", + "f43a5484e39aca4e6af86e7f8d2941773cdc1c960838ad88ae3540c49a1097df", "f46c108457e4fe12953dca75b409e06fca9cb264baffabe0c0d04c89af471e56", "f6bc17efa746680a44bbc49be67ac3f924d9c306c6d937f9959c57a1d3c5b9af", "f981764422d468567b5aff31148dc659aeee22cb13dc0ecd873d737deaf07372", "fa306a0cdb0f40c817e32164cd03b946564a02b4b7f7c3f4f0b9513096584d28", "fa5cf460eb314437081f7cea30dc8041c0bd6fc3f560a74bc2d7be1bf07384b0", + "fc39ec6d8a22498697f968ffc0fe5f717bed97afd68f922c76a80ed1f10d6579", ]); diff --git a/packages/cli/src/plan-loop/persona-banners.test.ts b/packages/cli/src/plan-loop/persona-banners.test.ts index 236f1ef..9944398 100644 --- a/packages/cli/src/plan-loop/persona-banners.test.ts +++ b/packages/cli/src/plan-loop/persona-banners.test.ts @@ -1,6 +1,8 @@ -import { mkdir, mkdtemp, writeFile } from "node:fs/promises"; +import { existsSync } from "node:fs"; +import { mkdir, mkdtemp, readFile, readdir, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import path from "node:path"; +import { fileURLToPath } from "node:url"; import { afterEach, describe, expect, it, vi } from "vitest"; import { DEFAULT_CLI_PERSONA_ID, @@ -121,4 +123,36 @@ describe("persona-banners", () => { expect(createPersonaBannerPrinter({ id: "x" })).toBeNull(); expect(createPersonaBannerPrinter(null)).toBeNull(); }); + + it("keeps shipped core cliBanners prefixes at or under 40 characters", async () => { + let dir = path.dirname(fileURLToPath(import.meta.url)); + let repoRoot: string | null = null; + for (;;) { + if ( + existsSync(path.join(dir, "registry", "personas", "core", "ghost-runner", "persona.json")) + ) { + repoRoot = dir; + break; + } + const parent = path.dirname(dir); + if (parent === dir) break; + dir = parent; + } + expect(repoRoot).not.toBeNull(); + const coreDir = path.join(repoRoot ?? "", "registry", "personas", "core"); + const ids = await readdir(coreDir); + expect(ids.length).toBeGreaterThan(0); + let packs = 0; + for (const id of ids) { + const personaPath = path.join(coreDir, id, "persona.json"); + if (!existsSync(personaPath)) continue; + packs += 1; + const raw = await readFile(personaPath, "utf8"); + const pack = JSON.parse(raw) as { cliBanners?: Record }; + for (const [key, value] of Object.entries(pack.cliBanners ?? {})) { + expect(value.length, `${id} ${key}`).toBeLessThanOrEqual(40); + } + } + expect(packs).toBeGreaterThan(0); + }); }); diff --git a/packages/cli/src/plan-loop/run-loop.ts b/packages/cli/src/plan-loop/run-loop.ts index e0062e7..27e3986 100644 --- a/packages/cli/src/plan-loop/run-loop.ts +++ b/packages/cli/src/plan-loop/run-loop.ts @@ -2,6 +2,7 @@ import { mkdir, readFile, rm, unlink } from "node:fs/promises"; import path from "node:path"; import { fileExists } from "../utils/fs.js"; import { logger } from "../utils/logger.js"; +import { withCliProgress } from "../welcome/visual-kit.js"; import type { AgentBackend } from "./backends.js"; import { armExternalPlanReview, @@ -126,12 +127,14 @@ export async function runPlanLoop(opts: RunPlanLoopOptions): Promise { let agentExit = 0; try { - const result = await opts.backend.run({ - workspace: opts.root, - prompt: TICK_PROMPT, - model: opts.model, - logPath, - }); + const result = await withCliProgress(`tick ${tick}`, () => + opts.backend.run({ + workspace: opts.root, + prompt: TICK_PROMPT, + model: opts.model, + logPath, + }), + ); agentExit = result.exitCode; } catch (err) { logger.error(String(err)); @@ -197,7 +200,7 @@ export async function runPlanLoop(opts: RunPlanLoopOptions): Promise { if (opts.sleepSeconds > 0) { if (banners) banners.sleep(opts.sleepSeconds); - await sleep(opts.sleepSeconds * 1000); + await withCliProgress(`sleep ${opts.sleepSeconds}s`, () => sleep(opts.sleepSeconds * 1000)); } } diff --git a/packages/cli/src/welcome/help-groups.ts b/packages/cli/src/welcome/help-groups.ts index 71f9700..b4776de 100644 --- a/packages/cli/src/welcome/help-groups.ts +++ b/packages/cli/src/welcome/help-groups.ts @@ -6,6 +6,7 @@ import type { ArgsDef, CommandDef } from "citty"; import { bold, gray, underline } from "kolorist"; import { KIT_VERSION } from "../lifecycle/version.js"; import { shouldUseWelcomeColor } from "./screen.js"; +import { SPACE_MARKS, shouldUseVisualMotion, tipAt } from "./visual-kit.js"; export type HelpGroupId = "setup" | "mission" | "dashboard" | "integrity" | "other"; @@ -103,11 +104,14 @@ export async function renderGroupedRootHelp( lines.push(""); } + const tipMark = shouldUseVisualMotion() ? SPACE_MARKS.star : SPACE_MARKS.tick; + const tip = tipAt([...version].reduce((n, c) => n + c.charCodeAt(0), 0)); lines.push( g(`Use \`${name} --help\` for more information about a command.`), g( "Chat-only HITL (start-project, backlog, git-staging/prod, run-plan-all) is not a CLI surface.", ), + g(`${tipMark} ${tip}`), "", ); return lines.join("\n"); diff --git a/packages/cli/src/welcome/screen.test.ts b/packages/cli/src/welcome/screen.test.ts index b766a6c..079dc0d 100644 --- a/packages/cli/src/welcome/screen.test.ts +++ b/packages/cli/src/welcome/screen.test.ts @@ -79,6 +79,9 @@ describe("renderWelcomeScreen", () => { expect(out).toContain("agent-kit dashboard"); expect(out).toContain("HITL"); expect(out.includes("\u001b")).toBe(false); + expect(out).toMatch( + /Try agent-kit doctor|HITL gates stay|never promotes|Mission Control is the dashboard|NO_COLOR and CI|groups SETUP/, + ); }); it("includes helmet ASCII frame lines", () => { @@ -128,6 +131,9 @@ describe("renderGroupedRootHelp", () => { expect(text).toContain("INTEGRITY"); expect(text).toContain("init"); expect(text).toContain("Chat-only HITL"); + expect(text).toMatch( + /Try agent-kit doctor|HITL gates stay|never promotes|Mission Control is the dashboard|NO_COLOR and CI|groups SETUP/, + ); for (const g of CLI_HELP_GROUPS) { expect(g.commands.length).toBeGreaterThan(0); } diff --git a/packages/cli/src/welcome/screen.ts b/packages/cli/src/welcome/screen.ts index d2b1ed9..bb2571d 100644 --- a/packages/cli/src/welcome/screen.ts +++ b/packages/cli/src/welcome/screen.ts @@ -13,15 +13,26 @@ import { white, } from "kolorist"; import { KIT_VERSION } from "../lifecycle/version.js"; +import { + HELMET_ACCENT, + HELMET_FILL, + HELMET_OUTLINE, + LABEL_MUTED, + SPACE_MARKS, + type WelcomeRenderOptions, + shouldUseVisualMotion, + shouldUseWelcomeColor, + tipAt, +} from "./visual-kit.js"; -/** Helmet outline / primary text — MC `--text-primary` / landing logo stroke. */ -export const HELMET_OUTLINE = "#e2e8f0"; -/** Deep brand blue — landing logo gradient mid. */ -export const HELMET_FILL = "#0C8DEB"; -/** Aqua accent — landing logo gradient late stops. */ -export const HELMET_ACCENT = "#00D0E7"; -/** Muted labels — MC `--text-secondary`. */ -export const LABEL_MUTED = "#8899aa"; +export { + HELMET_ACCENT, + HELMET_FILL, + HELMET_OUTLINE, + LABEL_MUTED, + shouldUseWelcomeColor, + type WelcomeRenderOptions, +}; /** kolorist SupportLevel.TrueColor — needed so trueColor() emits when TTY probes say none (CI). */ const KOLORIST_TRUECOLOR = 3; @@ -66,30 +77,11 @@ function withKoloristColor(fn: () => T): T { } } -export interface WelcomeRenderOptions { - version?: string; - /** Force color on/off; when omitted, derive from env + TTY. */ - color?: boolean; - stdoutIsTTY?: boolean; -} - /** True when argv names a citty subcommand (non-flag token), so skip root welcome. */ export function hasCliSubcommand(rawArgs: string[] | undefined): boolean { return Boolean(rawArgs?.some((arg) => !arg.startsWith("-"))); } -/** Whether welcome / root help should emit ANSI (respects NO_COLOR / CI / non-TTY). */ -export function shouldUseWelcomeColor(opts: WelcomeRenderOptions = {}): boolean { - if (opts.color === false) return false; - if (process.env.NO_COLOR) return false; - if (process.env.NODE_DISABLE_COLORS) return false; - if (process.env.FORCE_COLOR === "0") return false; - if (process.env.CI != null && process.env.CI !== "") return false; - if (opts.color === true) return true; - const tty = opts.stdoutIsTTY ?? Boolean(process.stdout.isTTY); - return tty; -} - export function renderHelmetAscii(color: boolean): string { if (!color) return HELMET_ASCII.join("\n"); return withKoloristColor(() => @@ -136,6 +128,11 @@ export function renderWelcomeScreen(opts: WelcomeRenderOptions = {}): string { muted( "Chat HITL (start-project, git-staging/prod, run-plan-all) stays in Cursor slash commands.", ), + muted( + `${shouldUseVisualMotion(opts) ? SPACE_MARKS.star : SPACE_MARKS.tick} ${tipAt( + [...version].reduce((n, c) => n + c.charCodeAt(0), 0), + )}`, + ), ]; return `${lines.join("\n")}\n`; } diff --git a/packages/cli/src/welcome/visual-kit.test.ts b/packages/cli/src/welcome/visual-kit.test.ts new file mode 100644 index 0000000..7a8d9c2 --- /dev/null +++ b/packages/cli/src/welcome/visual-kit.test.ts @@ -0,0 +1,214 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import { + HELMET_ACCENT, + HELMET_FILL, + HELMET_OUTLINE, + KIT_TIPS, + SPACE_MARKS, + SPINNER_FRAMES, + TtySpinner, + renderProgressLine, + shouldUseVisualMotion, + shouldUseWelcomeColor, + spinnerFrame, + tipAt, + withCliProgress, + wrapNarrow, +} from "./visual-kit.js"; + +function clearEnv(key: string): void { + Reflect.deleteProperty(process.env, key); +} + +function setEnv(key: string, value: string | undefined): void { + if (value === undefined) clearEnv(key); + else process.env[key] = value; +} + +describe("visual-kit color and motion gates", () => { + const prev = { + NO_COLOR: process.env.NO_COLOR, + CI: process.env.CI, + FORCE_COLOR: process.env.FORCE_COLOR, + NODE_DISABLE_COLORS: process.env.NODE_DISABLE_COLORS, + AGENT_KIT_REDUCED_MOTION: process.env.AGENT_KIT_REDUCED_MOTION, + }; + + afterEach(() => { + for (const [k, v] of Object.entries(prev)) { + setEnv(k, v); + } + }); + + it("disables color and motion when NO_COLOR is set", () => { + setEnv("NO_COLOR", "1"); + clearEnv("CI"); + clearEnv("FORCE_COLOR"); + clearEnv("AGENT_KIT_REDUCED_MOTION"); + expect(shouldUseWelcomeColor({ stdoutIsTTY: true, color: undefined })).toBe(false); + expect(shouldUseVisualMotion({ stdoutIsTTY: true })).toBe(false); + }); + + it("disables color in CI even on TTY", () => { + clearEnv("NO_COLOR"); + setEnv("CI", "true"); + expect(shouldUseWelcomeColor({ stdoutIsTTY: true })).toBe(false); + expect(shouldUseVisualMotion({ stdoutIsTTY: true })).toBe(false); + }); + + it("disables color when stdout is not a TTY", () => { + clearEnv("NO_COLOR"); + clearEnv("CI"); + clearEnv("FORCE_COLOR"); + expect(shouldUseWelcomeColor({ stdoutIsTTY: false })).toBe(false); + expect(shouldUseVisualMotion({ stdoutIsTTY: false })).toBe(false); + }); + + it("disables motion when AGENT_KIT_REDUCED_MOTION=1 on a color TTY", () => { + clearEnv("NO_COLOR"); + clearEnv("CI"); + clearEnv("FORCE_COLOR"); + clearEnv("NODE_DISABLE_COLORS"); + setEnv("AGENT_KIT_REDUCED_MOTION", "1"); + expect(shouldUseWelcomeColor({ stdoutIsTTY: true })).toBe(true); + expect(shouldUseVisualMotion({ stdoutIsTTY: true })).toBe(false); + }); +}); + +describe("visual-kit catalog", () => { + it("reuses HELMET_* space tokens", () => { + expect(HELMET_OUTLINE).toBe("#e2e8f0"); + expect(HELMET_FILL).toBe("#0C8DEB"); + expect(HELMET_ACCENT).toBe("#00D0E7"); + }); + + it("exposes spinner frames and small marks without a full helmet reprint", () => { + expect(SPINNER_FRAMES.length).toBeGreaterThanOrEqual(4); + expect(SPACE_MARKS.star).toBe("✦"); + expect(SPACE_MARKS.visor).toBe("( )"); + expect(Object.values(SPACE_MARKS).join("\n")).not.toContain(".-'"); + }); + + it("rotates tips without home paths or token-like secrets", () => { + expect(KIT_TIPS.length).toBeGreaterThanOrEqual(4); + const blob = KIT_TIPS.join("\n"); + expect(blob).not.toMatch(/\/Users\/|\/home\/|API_KEY|SECRET=|TOKEN=/); + expect(tipAt(0)).toBe(KIT_TIPS[0]); + expect(tipAt(KIT_TIPS.length)).toBe(KIT_TIPS[0]); + expect(spinnerFrame(0)).toBe(SPINNER_FRAMES[0]); + expect(spinnerFrame(SPINNER_FRAMES.length)).toBe(SPINNER_FRAMES[0]); + }); +}); + +describe("wrapNarrow", () => { + it("wraps to the column budget", () => { + const out = wrapNarrow("HITL gates stay in Cursor slash commands.", 12); + const lines = out.split("\n"); + expect(lines.length).toBeGreaterThan(1); + expect(lines.every((line) => line.length <= 12)).toBe(true); + }); + + it("floors width at 8 and splits overlong tokens", () => { + const out = wrapNarrow("abcdefghijk", 4); + expect(out.split("\n").every((line) => line.length <= 8)).toBe(true); + expect(out).toContain("abcdefgh"); + }); +}); + +describe("renderProgressLine", () => { + it("uses a static mark when motion is off", () => { + const line = renderProgressLine({ + frameIndex: 3, + tipIndex: 0, + columns: 80, + motion: false, + color: false, + }); + expect(line.startsWith(SPACE_MARKS.tick)).toBe(true); + expect(line).toContain(KIT_TIPS[0]); + expect(line.includes("\u001b")).toBe(false); + }); + + it("uses a spinner frame when motion is on", () => { + const line = renderProgressLine({ + frameIndex: 1, + tipIndex: 1, + columns: 120, + motion: true, + color: false, + }); + expect(line.startsWith(SPINNER_FRAMES[1])).toBe(true); + expect(line).toContain(KIT_TIPS[1]); + }); +}); + +describe("TtySpinner", () => { + afterEach(() => { + vi.useRealTimers(); + }); + + it("writes one static line when motion is off", () => { + const chunks: string[] = []; + const spinner = new TtySpinner({ + write: (c) => { + chunks.push(c); + }, + columns: () => 80, + motion: false, + color: false, + }); + spinner.start("install"); + spinner.stop(); + expect(chunks.join("")).toContain("install"); + expect(chunks.join("")).toContain("\n"); + expect(chunks.join("")).not.toContain("\r"); + }); + + it("advances frames on the interval when motion is on", () => { + vi.useFakeTimers(); + const chunks: string[] = []; + const spinner = new TtySpinner({ + write: (c) => { + chunks.push(c); + }, + columns: () => 80, + motion: true, + color: false, + intervalMs: 80, + now: () => Date.now(), + }); + spinner.start("sync"); + vi.advanceTimersByTime(160); + spinner.stop("done"); + const out = chunks.join(""); + expect(out).toContain("sync"); + expect(out).toContain("\r"); + expect(out).toContain("done"); + }); +}); + +describe("withCliProgress", () => { + it("does not start a spinner when motion is off", async () => { + const start = vi.spyOn(TtySpinner.prototype, "start"); + await expect(withCliProgress("install", async () => "ok", { motion: false })).resolves.toBe( + "ok", + ); + expect(start).not.toHaveBeenCalled(); + start.mockRestore(); + }); + + it("stops the spinner when fn throws", async () => { + const stop = vi.spyOn(TtySpinner.prototype, "stop"); + await expect( + withCliProgress( + "install", + async () => { + throw new Error("boom"); + }, + { motion: true, color: false, write: () => undefined }, + ), + ).rejects.toThrow("boom"); + expect(stop).toHaveBeenCalled(); + stop.mockRestore(); + }); +}); diff --git a/packages/cli/src/welcome/visual-kit.ts b/packages/cli/src/welcome/visual-kit.ts new file mode 100644 index 0000000..1de139f --- /dev/null +++ b/packages/cli/src/welcome/visual-kit.ts @@ -0,0 +1,232 @@ +/** + * Shared CLI visual-kit primitives (frames, marks, tips, motion gate). + * Welcome helmet ASCII stays in screen.ts; this module does not replace it. + */ + +import { cyan, gray, options as koloristOptions } from "kolorist"; + +/** Helmet outline / primary text: MC `--text-primary` / landing logo stroke. */ +export const HELMET_OUTLINE = "#e2e8f0"; +/** Deep brand blue: landing logo gradient mid. */ +export const HELMET_FILL = "#0C8DEB"; +/** Aqua accent: landing logo gradient late stops. */ +export const HELMET_ACCENT = "#00D0E7"; +/** Muted labels: MC `--text-secondary`. */ +export const LABEL_MUTED = "#8899aa"; + +/** Braille spinner frames (no ora). */ +export const SPINNER_FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"] as const; + +/** Small space marks. Full helmet art remains `HELMET_ASCII` in screen.ts. */ +export const SPACE_MARKS = { + star: "✦", + tick: "·", + diamond: "✧", + visor: "( )", +} as const; + +/** Rotating Mission Kit tips. No secrets, tokens, or absolute home paths. */ +export const KIT_TIPS = [ + "Try agent-kit doctor for repository readiness.", + "HITL gates stay in Cursor slash commands.", + "/run-plan never promotes to production.", + "Mission Control is the dashboard, not the CLI name.", + "NO_COLOR and CI keep this output static.", + "agent-kit --help groups SETUP, MISSION, DASHBOARD, INTEGRITY.", +] as const; + +export interface WelcomeRenderOptions { + version?: string; + /** Force color on/off; when omitted, derive from env + TTY. */ + color?: boolean; + stdoutIsTTY?: boolean; +} + +/** Whether welcome / root help should emit ANSI (respects NO_COLOR / CI / non-TTY). */ +export function shouldUseWelcomeColor(opts: WelcomeRenderOptions = {}): boolean { + if (opts.color === false) return false; + if (process.env.NO_COLOR) return false; + if (process.env.NODE_DISABLE_COLORS) return false; + if (process.env.FORCE_COLOR === "0") return false; + if (process.env.CI != null && process.env.CI !== "") return false; + if (opts.color === true) return true; + const tty = opts.stdoutIsTTY ?? Boolean(process.stdout.isTTY); + return tty; +} + +/** Frames and spinners: color gate plus optional reduced-motion. */ +export function shouldUseVisualMotion(opts: WelcomeRenderOptions = {}): boolean { + if (process.env.AGENT_KIT_REDUCED_MOTION === "1") return false; + return shouldUseWelcomeColor(opts); +} + +export function spinnerFrame(index: number): string { + const n = SPINNER_FRAMES.length; + return SPINNER_FRAMES[((index % n) + n) % n] ?? SPINNER_FRAMES[0]; +} + +export function tipAt(index: number): string { + const n = KIT_TIPS.length; + return KIT_TIPS[((index % n) + n) % n] ?? KIT_TIPS[0]; +} + +/** Word-wrap to a column budget. Width below 8 collapses to 8. */ +export function wrapNarrow(text: string, columns: number): string { + const width = Math.max(8, Number.isFinite(columns) ? Math.floor(columns) : 80); + const words = text.trim().split(/\s+/).filter(Boolean); + if (words.length === 0) return ""; + const lines: string[] = []; + let current = ""; + for (const word of words) { + if (word.length > width) { + if (current) { + lines.push(current); + current = ""; + } + for (let i = 0; i < word.length; i += width) { + const chunk = word.slice(i, i + width); + if (i + width >= word.length) current = chunk; + else lines.push(chunk); + } + continue; + } + const next = current ? `${current} ${word}` : word; + if (next.length > width) { + lines.push(current); + current = word; + } else { + current = next; + } + } + if (current) lines.push(current); + return lines.join("\n"); +} + +export interface ProgressLineOptions { + frameIndex: number; + tipIndex: number; + columns?: number; + label?: string; + motion?: boolean; + color?: boolean; +} + +/** One progress row: spinner or static mark, optional label, rotating tip. */ +export function renderProgressLine(opts: ProgressLineOptions): string { + const motion = opts.motion ?? shouldUseVisualMotion(); + const color = opts.color ?? shouldUseWelcomeColor(); + const mark = motion ? spinnerFrame(opts.frameIndex) : SPACE_MARKS.tick; + const tip = tipAt(opts.tipIndex); + const label = opts.label?.trim() ?? ""; + const raw = label ? `${mark} ${label} ${SPACE_MARKS.star} ${tip}` : `${mark} ${tip}`; + const cols = opts.columns ?? process.stdout.columns ?? 80; + const wrapped = wrapNarrow(raw, cols); + if (!color) return wrapped; + const prevEnabled = koloristOptions.enabled; + koloristOptions.enabled = true; + try { + return wrapped + .split("\n") + .map((line, i) => (i === 0 ? cyan(line) : gray(line))) + .join("\n"); + } finally { + koloristOptions.enabled = prevEnabled; + } +} + +export interface TtySpinnerOptions { + write?: (chunk: string) => void; + columns?: () => number; + motion?: boolean; + color?: boolean; + intervalMs?: number; + now?: () => number; +} + +/** + * Lightweight TTY spinner. No-op interval when motion is off (one static line). + * Callers must `stop()` to clear the interval. + */ +export class TtySpinner { + private timer: ReturnType | null = null; + private frame = 0; + private startedAt = 0; + private label = ""; + private readonly write: (chunk: string) => void; + private readonly columns: () => number; + private readonly motion: boolean; + private readonly color: boolean; + private readonly intervalMs: number; + private readonly now: () => number; + + constructor(opts: TtySpinnerOptions = {}) { + this.write = opts.write ?? ((chunk) => process.stdout.write(chunk)); + this.columns = opts.columns ?? (() => process.stdout.columns ?? 80); + this.motion = opts.motion ?? shouldUseVisualMotion(); + this.color = opts.color ?? shouldUseWelcomeColor(); + this.intervalMs = opts.intervalMs ?? 80; + this.now = opts.now ?? (() => Date.now()); + } + + start(label = ""): void { + this.stop(); + this.label = label; + this.frame = 0; + this.startedAt = this.now(); + this.paint(false); + if (!this.motion) return; + this.timer = setInterval(() => { + this.frame += 1; + this.paint(true); + }, this.intervalMs); + this.timer.unref?.(); + } + + stop(finalLine?: string): void { + if (this.timer) { + clearInterval(this.timer); + this.timer = null; + } + if (this.motion) this.write("\r\u001b[K"); + if (finalLine) this.write(`${finalLine}\n`); + } + + private paint(overwrite: boolean): void { + const elapsed = this.now() - this.startedAt; + const tipIndex = Math.floor(elapsed / 4000); + const line = + renderProgressLine({ + frameIndex: this.frame, + tipIndex, + columns: this.columns(), + label: this.label, + motion: this.motion, + color: this.color, + }).split("\n")[0] ?? ""; + if (overwrite && this.motion) this.write(`\r\u001b[K${line}`); + else if (this.motion) this.write(line); + else this.write(`${line}\n`); + } +} + +/** Run `fn` with a TTY spinner; always stops, including on throw. No-op when motion is off. */ +export async function withCliProgress( + label: string, + fn: () => Promise, + opts?: TtySpinnerOptions, +): Promise { + const motion = opts?.motion ?? shouldUseVisualMotion(); + if (!motion) { + return fn(); + } + const spinner = new TtySpinner({ ...opts, motion: true }); + spinner.start(label); + try { + const result = await fn(); + spinner.stop(); + return result; + } catch (err) { + spinner.stop(); + throw err; + } +}