From 3dfd3c2ba8e5b486627ae2296b5869a0858e72f7 Mon Sep 17 00:00:00 2001 From: agent-kit-sync Date: Wed, 12 Aug 2026 13:49:29 +0000 Subject: [PATCH] chore: sync private v5.0.0 (a14d3cd) --- .cursor-plugin/plugin.json | 17 +- .cursor/agent-kit.json | 2 +- .cursor/commands/agent-kit-onboard.md | 24 +- .cursor/commands/archive-plan.md | 5 + .cursor/commands/backlog-add.md | 10 +- .cursor/commands/backlog-cancel.md | 5 + .cursor/commands/backlog-delete.md | 5 + .cursor/commands/backlog-edit.md | 5 + .cursor/commands/context-status.md | 5 + .cursor/commands/continue-plan.md | 14 +- .cursor/commands/cursor-update-awareness.md | 10 +- .cursor/commands/dashboard-broadcast.md | 51 +- .cursor/commands/dashboard.md | 44 +- .cursor/commands/dogfood.md | 8 +- .cursor/commands/field-report-resolve.md | 5 + .cursor/commands/git-prod.md | 5 + .cursor/commands/git-staging.md | 12 +- .cursor/commands/handoff.md | 5 + .cursor/commands/hotfix.md | 5 + .cursor/commands/plan-external-review.md | 15 +- .cursor/commands/plan-review-triage.md | 60 +- .cursor/commands/run-plan-all.md | 14 +- .cursor/commands/run-plan-loop.md | 5 + .cursor/commands/run-plan-orchestrated.md | 5 + .cursor/commands/run-plan.md | 10 +- .cursor/commands/start-project.md | 10 +- .cursor/commands/summary.md | 5 + .cursor/commands/tips.md | 5 + .cursor/commands/update.md | 5 + .cursor/context/config.example.json | 5 +- .cursor/context/templates/handoff.md | 5 + .../templates/plan-external-review-prompt.md | 30 +- .cursor/context/templates/plan-monitor.md | 9 +- .cursor/context/templates/plan.md | 18 + .cursor/rules/agent-output-hygiene.mdc | 2 +- .cursor/rules/docs-professional-standard.mdc | 19 +- .cursor/rules/hitl-ask-questions.mdc | 4 +- ...lan-external-review-progress-gate.test.mjs | 130 ++++ .cursor/scripts/plan-external-review.sh | 4 + .cursor/skills/core/docs-repo/SKILL.md | 3 +- .github/CODE_OF_CONDUCT.md | 144 ++++ .github/ISSUE_TEMPLATE/bug_report.yml | 110 +++ .github/ISSUE_TEMPLATE/config.yml | 14 + .github/ISSUE_TEMPLATE/feature_request.yml | 76 ++ .github/PULL_REQUEST_TEMPLATE.md | 45 ++ .github/SECURITY.md | 91 +++ .github/SUPPORT.md | 60 ++ .github/workflows/ci.yml | 49 +- .gitignore | 14 + CHANGELOG.md | 147 ++++ LICENSE | 96 ++- README.md | 171 ++--- autogit/gitupdate.md | 3 +- autogit/plan-routine.md | 17 +- dashboard/dashboard-data.mjs | 103 +++ dashboard/dashboard.html | 675 +++++++++++++----- dashboard/lib/broadcast-share.mjs | 251 +++++++ dashboard/lib/guards.d.mts | 10 + dashboard/lib/guards.mjs | 17 +- dashboard/lib/open-browser.d.mts | 31 + dashboard/lib/open-browser.mjs | 298 ++++++++ dashboard/lib/semantic-model.mjs | 224 +++++- dashboard/open.html | 213 ++++++ dashboard/serve.mjs | 8 +- dashboard/start-broadcast.mjs | 103 ++- dashboard/start.mjs | 46 +- docs/CONTRIBUTING.md | 67 +- docs/DEVELOPMENT.md | 115 +++ docs/README.md | 13 +- docs/agentkit-landing.md | 162 ++++- docs/bootstrap.md | 4 +- docs/capability-inventory.md | 151 ++-- docs/consumer-configuration.md | 5 +- docs/cursor-native-audit.md | 54 +- docs/cursor-update-awareness.md | 9 +- docs/design-system.md | 45 ++ docs/external-plan-review.md | 18 +- docs/five-layer-claim-matrix.md | 2 +- docs/getting-started.md | 52 +- docs/github-about.md | 13 +- docs/marketplace.md | 23 +- docs/npm-publish-checklist.md | 16 +- docs/public-launch-announcement.md | 39 +- docs/public-launch.md | 5 +- docs/repository-boundaries.md | 4 +- docs/repository-readiness-onboarding.md | 42 +- install.md | 8 +- package.json | 15 +- packages/cli/LICENSE | 75 ++ packages/cli/README.md | 17 + packages/cli/package.json | 6 +- packages/cli/src/commands/add.ts | 124 ++-- packages/cli/src/commands/contribute.ts | 104 +-- packages/cli/src/commands/cursor-awareness.ts | 3 +- .../cli/src/commands/dashboard-broadcast.ts | 17 +- packages/cli/src/commands/dashboard.test.ts | 11 + packages/cli/src/commands/dashboard.ts | 97 ++- packages/cli/src/commands/diff.ts | 30 +- packages/cli/src/commands/doctor.ts | 2 +- packages/cli/src/commands/guard.ts | 2 +- packages/cli/src/commands/handoff.ts | 3 +- packages/cli/src/commands/hook.ts | 3 +- packages/cli/src/commands/init.ts | 37 +- packages/cli/src/commands/install.test.ts | 51 ++ packages/cli/src/commands/install.ts | 163 +++-- packages/cli/src/commands/monitors.ts | 2 +- packages/cli/src/commands/run-plan.ts | 3 +- packages/cli/src/commands/scan.ts | 2 +- packages/cli/src/commands/status.ts | 2 +- packages/cli/src/commands/update.ts | 86 ++- packages/cli/src/commands/validate.ts | 2 +- .../ci-private-origin-allowlist.test.ts | 57 ++ packages/cli/src/dashboard/guards.test.ts | 165 +++++ .../cli/src/dashboard/open-browser.test.ts | 387 ++++++++++ .../dashboard/plugin-ux-validation.test.ts | 464 ++++++++++-- .../cli/src/dashboard/semantic-model.test.ts | 318 ++++++++- packages/cli/src/generator/personalization.ts | 58 ++ packages/cli/src/generator/vscode.test.ts | 134 ++++ packages/cli/src/generator/vscode.ts | 83 ++- packages/cli/src/hooks/session-start.test.ts | 38 + packages/cli/src/hooks/session-start.ts | 57 +- packages/cli/src/index.ts | 21 +- .../lifecycle/cursor-update-awareness.test.ts | 25 + .../src/lifecycle/cursor-update-awareness.ts | 49 +- packages/cli/src/lifecycle/l0.test.ts | 21 + packages/cli/src/lifecycle/l0.ts | 3 + .../cli/src/lifecycle/overlay-known-hashes.ts | 64 ++ packages/cli/src/lifecycle/overlay.test.ts | 74 ++ packages/cli/src/lifecycle/overlay.ts | 5 + packages/cli/src/registry/resolve.test.ts | 183 ++++- packages/cli/src/registry/resolve.ts | 186 ++++- packages/cli/src/utils/terminal.test.ts | 241 +++++++ packages/cli/src/utils/terminal.ts | 161 +++++ packages/cli/src/welcome/help-groups.ts | 114 +++ packages/cli/src/welcome/screen.test.ts | 149 ++++ packages/cli/src/welcome/screen.ts | 145 ++++ scripts/sync-cli-dashboard.mjs | 2 +- scripts/verify-cli-dashboard-pack.mjs | 4 + 138 files changed, 7417 insertions(+), 1111 deletions(-) create mode 100644 .cursor/scripts/plan-external-review-progress-gate.test.mjs create mode 100644 .github/CODE_OF_CONDUCT.md create mode 100644 .github/ISSUE_TEMPLATE/bug_report.yml create mode 100644 .github/ISSUE_TEMPLATE/config.yml create mode 100644 .github/ISSUE_TEMPLATE/feature_request.yml create mode 100644 .github/PULL_REQUEST_TEMPLATE.md create mode 100644 .github/SECURITY.md create mode 100644 .github/SUPPORT.md create mode 100644 dashboard/lib/broadcast-share.mjs create mode 100644 dashboard/lib/guards.d.mts create mode 100644 dashboard/lib/open-browser.d.mts create mode 100644 dashboard/lib/open-browser.mjs create mode 100644 dashboard/open.html create mode 100644 docs/DEVELOPMENT.md create mode 100644 docs/design-system.md create mode 100644 packages/cli/LICENSE create mode 100644 packages/cli/src/commands/install.test.ts create mode 100644 packages/cli/src/dashboard/ci-private-origin-allowlist.test.ts create mode 100644 packages/cli/src/dashboard/open-browser.test.ts create mode 100644 packages/cli/src/generator/vscode.test.ts create mode 100644 packages/cli/src/utils/terminal.test.ts create mode 100644 packages/cli/src/utils/terminal.ts create mode 100644 packages/cli/src/welcome/help-groups.ts create mode 100644 packages/cli/src/welcome/screen.test.ts create mode 100644 packages/cli/src/welcome/screen.ts diff --git a/.cursor-plugin/plugin.json b/.cursor-plugin/plugin.json index b507464..ac8988a 100644 --- a/.cursor-plugin/plugin.json +++ b/.cursor-plugin/plugin.json @@ -1,7 +1,9 @@ { "name": "agent-kit", "displayName": "Agent Kit", - "author": "agent-kit-startup", + "author": { + "name": "agent-kit-startup" + }, "description": "HITL framework for AI-assisted IDEs — plan, handoff, staging→prod, memory loop, anti-slop. Stack skills via agent-kit add.", "keywords": [ "agents", @@ -12,7 +14,14 @@ "multi-ide", "anti-slop" ], - "license": "MIT", - "version": "4.8.9", - "repository": "https://github.com/agent-kit-startup/agent-kit" + "license": "PolyForm-Noncommercial-1.0.0", + "version": "5.0.0", + "homepage": "https://github.com/agent-kit-startup/agent-kit", + "repository": "https://github.com/agent-kit-startup/agent-kit", + "logo": "dashboard/logo.svg", + "rules": ".cursor/rules", + "skills": ".cursor/skills/core", + "agents": ".cursor/agents", + "commands": ".cursor/commands", + "hooks": ".cursor/hooks.json" } diff --git a/.cursor/agent-kit.json b/.cursor/agent-kit.json index 4b32495..d0f04bc 100644 --- a/.cursor/agent-kit.json +++ b/.cursor/agent-kit.json @@ -1,6 +1,6 @@ { "schemaVersion": 1, - "version": "4.8.9", + "version": "5.0.0", "protected": [ ".cursor/HANDOFF.md", ".cursor/agents/test-suites.md", diff --git a/.cursor/commands/agent-kit-onboard.md b/.cursor/commands/agent-kit-onboard.md index 93957c3..74fe87a 100644 --- a/.cursor/commands/agent-kit-onboard.md +++ b/.cursor/commands/agent-kit-onboard.md @@ -1,3 +1,8 @@ +--- +name: agent-kit-onboard +description: Prepare and validate repository readiness before /start-project. +--- + # Command: /agent-kit-onboard ## Goal @@ -96,10 +101,25 @@ When complete: 3. Verify each remaining non-essential check is ready or explicitly deferred with both a reason and recovery action. Reject deferral for any check with `status: "blocked"`. 4. If verification fails, leave `onboarding.status: "in_progress"` and `onboarded` unchanged, then resume the first unresolved essential check derived from `pillars[].checks[]`. 5. Only after verification passes, merge `onboarding.status: "completed"` and `onboarded: true` into `.cursor/context/config.json` without removing other keys. -6. End with exactly one call to action: +6. **Domain-skills scaffold (optional HITL).** Before showing the final finish-setup CTA, run the scaffold gate: + - Read `.cursor/context/personalization.json` and `.cursor/agent-kit.config.json` to reuse install-time evidence. Do not invent a second detector. + - Build a short proposal from the already-applied L2 skills and L1 packs in personalization, plus any project-owned domain skills implied by the profile but not yet installed. + - Ask one question using **Ask questions** with concrete options: + + > "Essentials are ready. Before finish setup, scaffold domain skills from the detected profile?" + + Options: `Scaffold domain skills` / `Defer (record reason)` / `Skip` + + - **Fallback when Ask questions is unavailable:** present the same options as a numbered list, ask the user to reply with the number or the label, and note they can always **type their own answer** if none of the options fit (equivalent of the built-in "Other" choice). + - **Scaffold domain skills:** show the proposal list, then write any accepted project-owned skills under `.cursor/skills/domain//SKILL.md` only when the path does not already exist. Update the **Relevant skills** section of `.cursor/project-context.md` (create the heading if missing) with installed and newly accepted skill ids plus evidence; also ensure those ids appear under `.cursor/agent-kit.json` `skills[]` when that manifest is the project's install index. Record `onboarding.domainSkills` in `.cursor/context/config.json` with `status: "applied"`, the list of accepted items, and `appliedAt`. + - **Defer:** ask for a short reason, then record `onboarding.domainSkills` with `status: "deferred"`, `reason`, and `recoveryCommand: "/agent-kit-onboard"`. + - **Skip:** record `onboarding.domainSkills` with `status: "skipped"`. + - Never overwrite an existing project-owned skill or file without a separate HITL confirmation. This closes the gap reported in public issue https://github.com/agent-kit-startup/agent-kit/issues/36 and the dogfood note `dogfood/cursor_onboard_should_scaffold_domain_skills_2026_08_01.md`. + - **Instruction-only surface:** this gate is chat/`/agent-kit-onboard` prose executed by the agent session. There is no separate CLI subcommand that scaffolds domain skills; intentionally document defer/skip when the operator declines. Project-owned skills under `.cursor/skills/domain/` are one-way (not contributeable via `guessRegistryPath` / registry paths `core` and `community` only). +7. End with exactly one call to action: - `Next: /start-project` when the user wants to plan a deliverable. - `Next: finish setup` when no deliverable should start now. -After essentials are ready, Mission Control is **optional** and **not** an essential readiness check. Consumer L0 installs the `/dashboard` command text but not `dashboard/**`. If the operator wants the panel, `agent-kit dashboard` serves it from the installed CLI (4.8.2 onward); on older pins point them to an agent-kit checkout that includes `dashboard/start.mjs` (loopback `http://127.0.0.1:3333`). Do not block `/start-project` on Mission Control. Do not ask about skins or external review before essentials (Hard Stop 1). +After essentials are ready, `/agent-kit-onboard` offers one optional HITL gate: a **domain-skills scaffold** derived from the install-time personalization/doctor evidence. This gate is not a readiness blocker; deferring or skipping it must still allow `/start-project` to proceed. Mission Control is also **optional** and **not** an essential readiness check. Consumer L0 installs the `/dashboard` command text but not `dashboard/**`. If the operator wants the panel, `agent-kit dashboard` serves it from the installed CLI (4.8.2 onward); on older pins point them to an agent-kit checkout that includes `dashboard/start.mjs` (loopback `http://127.0.0.1:3333`). Do not block `/start-project` on Mission Control or on the domain-skills scaffold. Do not ask about skins or external review before essentials (Hard Stop 1). Agent Personas remain available through later personalization or settings. External review is offered only when a plan reaches exhaustion. diff --git a/.cursor/commands/archive-plan.md b/.cursor/commands/archive-plan.md index 8aab690..3e7af29 100644 --- a/.cursor/commands/archive-plan.md +++ b/.cursor/commands/archive-plan.md @@ -1,3 +1,8 @@ +--- +name: archive-plan +description: Archive a parked plan: drop it from the HANDOFF parked list and move the file into .cursor/plans/archive/. +--- + # Command: /archive-plan ## Goal diff --git a/.cursor/commands/backlog-add.md b/.cursor/commands/backlog-add.md index 2cb2586..2a449ac 100644 --- a/.cursor/commands/backlog-add.md +++ b/.cursor/commands/backlog-add.md @@ -1,3 +1,8 @@ +--- +name: backlog-add +description: Enqueue a new plan with to-dos under the HANDOFF Backlog without activating it. +--- + # Command: /backlog-add ## Goal @@ -47,6 +52,7 @@ Before enqueueing a new plan, **scan** these sources (read/skim; do not deep-div | **Archived context** | Prior packs for same theme | `.cursor/context/archive/**` (if present; glob by topic) | | **Decisions** | ADRs that constrain the goal | `.cursor/memory/decisions/`, `_index.md` Decisions table | | **Memory** | Errors, audits, consolidations, review logs, plan-monitors, findings audits | `.cursor/memory/errors/`, `.cursor/memory/plan-monitor-*.md`, theme-matched `plan-review-*.md`, `_index.md` (Audits + Decisions) | +| **Unprocessed dogfood** | Factory/consumer inbox notes awaiting triage (not sessionStart-only) | `dogfood/README.md` or `.cursor/dogfood/README.md` under `##` or `### Unprocessed Files`; skim titles/summaries only. Missing/empty inbox → no findings. Labels: ignore (owned by open plan), error/include (kit gap), note (inbox evidence only). Never auto-analyze or memory WRITE (ADR `decisions/2026-08-11_dogfood-unprocessed-broad-intake-bucket.md`) | | **Local docs** | SoT / inventories / getting-started that the goal touches | `docs/**`, especially files named in the payload or related SoT | | **Working tree** | Uncommitted local work that would collide | `git status`, `git diff` (staged + unstaged); do not commit | | **Recent commits** | What already shipped for this theme | `git log` (short, recent), related PR titles if available | @@ -66,8 +72,8 @@ The actual scanning and triage is delegated to a **Task(explore) subagent** usin 1. **Fill the template** — set these parameters: - **Repo:** `[absolute repo path]` - **Command:** `/backlog-add` - - **Task description:** "Scan the 9 Broad Intake buckets (prepared repository, active session, plans, archived context, decisions, memory, local docs, working tree, recent commits, product version) and return a structured triage report with findings per bucket, each labeled ignore/error/include/note." - - **read_scope:** `[".cursor/agent-kit.config.json", ".cursor/context/readiness.json", ".cursor/HANDOFF.md", ".cursor/context/current/", ".cursor/plans/*.plan.md", ".cursor/context/archive/**", ".cursor/memory/decisions/", ".cursor/memory/errors/", ".cursor/memory/plan-monitor-*.md", ".cursor/memory/plan-review-*.md", ".cursor/memory/_index.md", "docs/**", "package.json", "CHANGELOG.md"]` + - **Task description:** "Scan the Broad Intake buckets listed in this command (prepared repository, active session, plans, archived context, decisions, memory, Unprocessed dogfood, local docs, working tree, recent commits, product version) and return a structured triage report with findings per bucket, each labeled ignore/error/include/note. For Unprocessed dogfood: skim `dogfood/README.md` or `.cursor/dogfood/README.md` `##` or `### Unprocessed Files` only; never auto-analyze." + - **read_scope:** `[".cursor/agent-kit.config.json", ".cursor/context/readiness.json", ".cursor/HANDOFF.md", ".cursor/context/current/", ".cursor/plans/*.plan.md", ".cursor/context/archive/**", ".cursor/memory/decisions/", ".cursor/memory/errors/", ".cursor/memory/plan-monitor-*.md", ".cursor/memory/plan-review-*.md", ".cursor/memory/_index.md", "dogfood/README.md", ".cursor/dogfood/README.md", "docs/**", "package.json", "CHANGELOG.md"]` - **worker_contract:** "structured triage report: list of findings per bucket with triage labels (ignore/error/include/note)" - **max_ticks:** 2 diff --git a/.cursor/commands/backlog-cancel.md b/.cursor/commands/backlog-cancel.md index f17cb69..5903f61 100644 --- a/.cursor/commands/backlog-cancel.md +++ b/.cursor/commands/backlog-cancel.md @@ -1,3 +1,8 @@ +--- +name: backlog-cancel +description: Soft-cancel a backlog plan: mark open to-dos cancelled and drop it from the Backlog, keeping the file. +--- + # Command: /backlog-cancel ## Goal diff --git a/.cursor/commands/backlog-delete.md b/.cursor/commands/backlog-delete.md index e80e304..06aa257 100644 --- a/.cursor/commands/backlog-delete.md +++ b/.cursor/commands/backlog-delete.md @@ -1,3 +1,8 @@ +--- +name: backlog-delete +description: Remove a plan from the HANDOFF Backlog and move the file to .cursor/plans/archive/. +--- + # Command: /backlog-delete ## Goal diff --git a/.cursor/commands/backlog-edit.md b/.cursor/commands/backlog-edit.md index bcfadbc..93d7b97 100644 --- a/.cursor/commands/backlog-edit.md +++ b/.cursor/commands/backlog-edit.md @@ -1,3 +1,8 @@ +--- +name: backlog-edit +description: Edit an existing plan body or frontmatter to-dos without activating it. +--- + # Command: /backlog-edit ## Goal diff --git a/.cursor/commands/context-status.md b/.cursor/commands/context-status.md index 3b7faa6..098a4fd 100644 --- a/.cursor/commands/context-status.md +++ b/.cursor/commands/context-status.md @@ -1,3 +1,8 @@ +--- +name: context-status +description: Show the current context state: active task, handoff, archived tasks. +--- + # Command: /context-status ## Goal diff --git a/.cursor/commands/continue-plan.md b/.cursor/commands/continue-plan.md index 1d96102..c1496b8 100644 --- a/.cursor/commands/continue-plan.md +++ b/.cursor/commands/continue-plan.md @@ -1,3 +1,8 @@ +--- +name: continue-plan +description: Resume a plan from the last handoff and execute only the next unit. +--- + # Command: /continue-plan ## Goal @@ -14,10 +19,11 @@ Resume a plan from the last handoff. Confirm the next unit, then execute **only 1. **Read `.cursor/HANDOFF.md` first.** No handoff → say so and suggest `/start-project`. Do not invent progress. 2. **Pre-flight (API/usage limit):** if `- **Mode:**`, `- **Gaps:**`, or `- **Instruction for the next agent:**` still records an API/usage limit hard stop from a prior tick, **do not** mark a to-do `in_progress`, dispatch a Task, or edit product files until the operator confirms recovery (switch from Auto to a named model such as Claude Opus, Sonnet 4.6, or Composer 2.5 Fast; and/or wait for quota reset). Surface recovery via **Ask questions** when available. **Fallback after Auto→Grok:** Grok 4.5 and Auto often lack Ask questions (see `.cursor/memory/decisions/2026-07-20_ask-questions-model-availability.md`); use the numbered-list chat fallback (reply by number or label; typed answer = "Other"). Pre-flight is HANDOFF check plus operator model choice only; the kit has no remaining-quota API. Align with `context-guardian` Quota-blocked sessions and `/run-plan` Read-state pre-flight. 3. **Audits pre-flight** (config `externalPlanReview.preflight`: `off` | `warn` | `block`; missing = `off`): before the next-unit confirmation Ask, check owed / untriaged audits for the chosen plan slug (Field Report owed, untriaged `plan-monitor-*.md`, open cadence WARNING). `off`: skip. `warn`: surface once, then continue. `block`: arm via `.cursor/scripts/plan-external-review.sh` (prefer `--autonomous` when `mode: autonomous`, else `--paste-only`) or stop until the operator defers; never steal `/git-prod`. Distinct from the advisory pre-unit monitor skim below (ADR `2026-07-27_audits-autonomous-plan-review-contract.md`). -4. **Apply Agent Persona chrome.** Read `.cursor/context/config.json` for `agentPersona.modes.continue-plan` (fallback to legacy `workspaceSkin`, then "autopilot"). Use the corresponding persona's `chatHints` from `registry/personas/core/` for tone and confirmations. -5. **Confirm the next unit with Ask questions** before editing. Use concrete option labels (see What to Do step 5). Do not accept a typed "yes" as the gate. -6. **One unit per chat** (phase or one heavy to-do) unless the user explicitly ran `/run-plan`. -7. **Do not start a competing plan.** New goal requires `/start-project`, which parks the active plan and proceeds to create a new one. +4. **Unprocessed dogfood preflight (advisory):** before the next-unit confirmation Ask, skim `##` or `### Unprocessed Files` in factory `dogfood/README.md` or consumer `.cursor/dogfood/README.md`. Non-empty: mention count and top titles once with standard triage labels (`ignore` / `error` / `include` / `note`). Empty or missing inbox: silent OK. Do **not** force analysis, memory WRITE, or Field Reports; do **not** block resume solely because the inbox is non-empty. Runs regardless of `externalPlanReview.preflight`. sessionStart inbox tip stays complementary (ADR `2026-08-11_dogfood-unprocessed-broad-intake-bucket.md`). +5. **Apply Agent Persona chrome.** Read `.cursor/context/config.json` for `agentPersona.modes.continue-plan` (fallback to legacy `workspaceSkin`, then "autopilot"). Use the corresponding persona's `chatHints` from `registry/personas/core/` for tone and confirmations. +6. **Confirm the next unit with Ask questions** before editing. Use concrete option labels (see What to Do step 5). Do not accept a typed "yes" as the gate. +7. **One unit per chat** (phase or one heavy to-do) unless the user explicitly ran `/run-plan`. +8. **Do not start a competing plan.** New goal requires `/start-project`, which parks the active plan and proceeds to create a new one. ## What to Do diff --git a/.cursor/commands/cursor-update-awareness.md b/.cursor/commands/cursor-update-awareness.md index 391cb33..c3dac1c 100644 --- a/.cursor/commands/cursor-update-awareness.md +++ b/.cursor/commands/cursor-update-awareness.md @@ -1,3 +1,8 @@ +--- +name: cursor-update-awareness +description: Run an opt-in advisory check for Cursor product updates against the native audit inventory. +--- + # Command: /cursor-update-awareness ## Goal @@ -23,11 +28,12 @@ Run an **opt-in advisory** check for Cursor product updates (changelog + `docs/c ## Check-only (CLI) ```bash -agent-kit cursor-awareness --check [--json] [--respect-prefs] [--stamp] [--offline] +agent-kit cursor-awareness --check [--cwd ] [--json] [--respect-prefs] [--stamp] [--offline] ``` - Fetches `https://cursor.com/changelog` (override via `cursorUpdateCheck.changelogUrl`) unless `--offline`. -- Diffs against `docs/cursor-native-audit.md` (open Action items, refresh staleness) and validates `docs/cursor-3-features.md` presence. +- Resolves `docs/cursor-native-audit.md` by walking up from `--cwd` (default `process.cwd()`); errors with a `--cwd` hint when no ancestor has the inventory. +- Diffs against that inventory (open Action items, refresh staleness) and validates `docs/cursor-3-features.md` under the same root. - Prefs in `.cursor/context/config.json` under `cursorUpdateCheck` (`enabled` default `false`, `intervalDays`, `lastSeenCursorVersion`). Distinct from kit `updateCheck`. - `applyRecommended` and `fieldReportRecommended` are always `false`. diff --git a/.cursor/commands/dashboard-broadcast.md b/.cursor/commands/dashboard-broadcast.md index 9f8195e..e653ccd 100644 --- a/.cursor/commands/dashboard-broadcast.md +++ b/.cursor/commands/dashboard-broadcast.md @@ -1,8 +1,13 @@ +--- +name: dashboard-broadcast +description: Start Mission Control in opt-in LAN broadcast mode with a required session token. +--- + # Command: /dashboard-broadcast ## Goal -Start Mission Control in **opt-in LAN broadcast** mode: bind a non-loopback interface with a **required session token**, print LAN URL(s) + token, and open a browser when possible. +Start Mission Control in **opt-in LAN broadcast** mode: bind a non-loopback interface with a **required session token**, print LAN URL(s) + token, and open **one** browser surface when possible (preferred OS browser from config/env, or IDE local verify — not both unless Ask says so). This does **not** change `/dashboard` (loopback-first, no token). Do not set `HOST=0.0.0.0` on the loopback path without this command and token gate. @@ -23,6 +28,17 @@ node dashboard/start-broadcast.mjs ## What to Do +0. **Derive the workspace port** so the probe/stop snippets below are runnable. The starter also prints this port. + + From the agent-kit repo root: + + ```bash + cd "$(git rev-parse --show-toplevel)" + export MC_PORT=$(node -e 'import("./dashboard/lib/guards.mjs").then(m => console.log(m.preferredPortForRepoRoot(process.cwd()))).catch(e => { console.error(e.message); process.exit(1) })') + ``` + + If you set an explicit `PORT`, use that value instead of the derivation. + 1. **Prefer the starter** (handles token generation, `HOST=0.0.0.0`, detach, LAN URL print): ```bash @@ -35,19 +51,26 @@ node dashboard/start-broadcast.mjs ```bash export HOST=0.0.0.0 export MISSION_CONTROL_TOKEN="$(openssl rand -base64 24 | tr -d '/+=' | head -c 32)" + export MC_PORT=$(node -e 'import("./dashboard/lib/guards.mjs").then(m => console.log(m.preferredPortForRepoRoot(process.cwd()))).catch(e => { console.error(e.message); process.exit(1) })') + export PORT=$MC_PORT # same double-fork / setsid pattern as /dashboard (agent shell reaps bare &) ``` -2. **Confirm readiness** in a **separate** shell call. Probe with the token: +2. **Confirm readiness** in a **separate** shell call. Use the port printed by the starter (per-workspace allocation, often not `:3333`) or the `MC_PORT` derived above: ```bash - curl -sf -o /dev/null -w '%{http_code}' "http://127.0.0.1:3333/?token=$MISSION_CONTROL_TOKEN" - lsof -nP -iTCP:3333 -sTCP:LISTEN + curl -sf -o /dev/null -w '%{http_code}' "http://127.0.0.1:$MC_PORT/?token=$MISSION_CONTROL_TOKEN" + lsof -nP -iTCP:$MC_PORT -sTCP:LISTEN ``` -3. **Share the printed LAN URL** (includes `?token=`). After first load, an HttpOnly cookie keeps same-origin assets/SSE working. Do not paste the token into docs or commits. + Verify `system.repoRoot` matches this workspace before trusting the response as your own Mission Control. + +3. **Share the printed Share URL** (Mission Kit cosmetic mask, default `https://missionkit.io/mc/open.html#…`). The fragment is reversible base64url of the full LAN URL **including the live token** — treat the Share URL with the **same secret handling** as the raw token (do not paste into Slack, tickets, screenshots, docs, or commits). Hostinger access logs never see the fragment; that does not make the link non-secret. Phone/tablet must still be on the **same trusted LAN**. After first Mission Control load, an HttpOnly cookie keeps same-origin assets/SSE working. Override base with `MISSION_CONTROL_SHARE_BASE` (BYO **HTTPS** origin hosting `open.html`; non-HTTPS is rejected except loopback http for local preview). Set `off` for raw LAN-only print (also the recovery if a hosted resolver 404s). Extensionless `…/mc/open` may still 404 on Hostinger until an alias exists; the default uses `open.html`. Soft TTL via `MISSION_CONTROL_SHARE_TTL_SEC` (default 86400; `0` = never expires in the UI). Expiry is **advisory only** (client refuse); hard revoke = stop broadcast or rotate `MISSION_CONTROL_TOKEN`. Secondary LAN lines default on (`MISSION_CONTROL_SHARE_SHOW_LAN=0` to hide). -4. **IDE browser:** `cursor-ide-browser` may open `http://127.0.0.1:3333/?token=…` for local verify. LAN devices use the printed `http://:3333/?token=…` URL. +4. **Open one surface only** (never OS + IDE together by default): + - **Default (CLI / npm):** let `start-broadcast.mjs` open the preferred browser to the **Share URL** when masking is on (config `missionControl.preferredBrowser`, env `MISSION_CONTROL_PREFERRED_BROWSER`, or `--browser`). Use `--no-open` / `MISSION_CONTROL_NO_OPEN=1` to skip OS open (server still prints Share URL + token / LAN lines). + - **Slash / agent path:** prefer Ask when preference is unset/`ask`: `Open OS preferred browser` / `IDE verify only (no OS open)` / `Skip open`. Then either run the starter **without** `MISSION_CONTROL_NO_OPEN` (OS preferred, no IDE navigate) **or** set `MISSION_CONTROL_NO_OPEN=1` and navigate once with `cursor-ide-browser` to `http://127.0.0.1:$MC_PORT/?token=…` (or to a local `http://127.0.0.1:$MC_PORT/open.html#…` preview). Do not do both in the same run unless the operator explicitly asked for dual open. + - LAN devices use the printed **Share** URL (human paste). Raw `http://:$MC_PORT/?token=…` remains secondary/debug. ## Security @@ -55,17 +78,25 @@ node dashboard/start-broadcast.mjs - Static, `/dashboard-data.json`, `/api/data`, and `/api/events` require the token (Bearer, `?token=`, header, or cookie). - `PUT`/`PATCH /api/config` stays **loopback-only** (token does not unlock LAN writes). - CTAs remain copy-only; no git stage, process kill, or `/git-prod` from the panel. -- Trusted LAN only; not multi-user internet hosting. See ADR `2026-07-27_mission-control-opt-in-lan-broadcast.md`. +- Trusted LAN only; not multi-user internet hosting. Share URLs are **cosmetic** (fragment → private/loopback LAN only); they are not a WAN relay. The Share URL **is a secret** (embeds the live token). See ADR `2026-07-27_mission-control-opt-in-lan-broadcast.md` and `2026-08-11_mission-control-broadcast-url-mask.md`. +- `GET /open.html` (alias `/open`) is the public share-resolver shell under broadcast bind; it does not unlock snapshot/SSE/API without the token. Resolver rejects non-private targets. ## Stop / firewall -- Stop: `kill "$(lsof -nP -iTCP:3333 -sTCP:LISTEN -t)"` (always `-sTCP:LISTEN`). +- Stop **this workspace only**: kill the PID on the port printed by the starter or derived as `MC_PORT`, never a hardcoded `:3333`. Use the printed `system.port` (or `PORT` / `MC_PORT` if you set one): + ```bash + kill "$(lsof -nP -iTCP:$MC_PORT -sTCP:LISTEN -t)" + ``` +- **Never kill a listener on a port owned by another workspace.** Verify `system.repoRoot` before killing: + ```bash + curl -sf "http://127.0.0.1:$MC_PORT/dashboard-data.json?token=$MISSION_CONTROL_TOKEN" | node -e 'let d="";process.stdin.on("data",c=>d+=c);process.stdin.on("end",()=>console.log(JSON.parse(d).system?.repoRoot))' + ``` - If a loopback `/dashboard` already holds the port, stop it before broadcast. - OS firewall may block inbound LAN TCP; allow the chosen port for your local network profile if needed. ## Notes -- Port: `PORT` env, default `3333` +- Port: `PORT` env overrides; default is the per-workspace hash allocation (range `3333-3588`). Derive it with the snippet in step 0 or read the printed `system.port`. - Log default: `/tmp/mission-control-broadcast.log` - Loopback UX remains `/dashboard` / `npm run dashboard` / `agent-kit dashboard` - Detach lessons match `/dashboard` (error `2026-07-25_dashboard-server-reaped-agent-shell`) @@ -75,6 +106,6 @@ node dashboard/start-broadcast.mjs | Symptom | Cause | Fix | |---|---|---| | Serve exits: non-loopback requires token | `HOST` set without `MISSION_CONTROL_TOKEN` | Use `dashboard:broadcast` or set a ≥16 char token | -| Port busy / token rejected | Existing loopback instance on 3333 | Kill LISTEN pid; retry broadcast | +| Port busy / token rejected | Existing instance on the allocated port | Kill LISTEN pid for **this** workspace only (verify `repoRoot`); retry broadcast | | Phone cannot connect | Firewall or wrong IP | Confirm printed LAN IPv4; allow inbound TCP | | Config save 403 from phone | Expected | Config writes are loopback-only | diff --git a/.cursor/commands/dashboard.md b/.cursor/commands/dashboard.md index 6aa679c..fae8e89 100644 --- a/.cursor/commands/dashboard.md +++ b/.cursor/commands/dashboard.md @@ -1,12 +1,17 @@ +--- +name: dashboard +description: Start or reuse Mission Control for this workspace and open the printed URL. +--- + # Command: /dashboard ## Goal -Start (or reuse) Mission Control for **this Cursor workspace only**, then open the printed URL in the IDE browser. +Start (or reuse) Mission Control for **this workspace only**, then open the printed URL in **one** browser target: the IDE browser MCP (required path below). Do not open Chrome, Safari, Firefox, or other external browsers in addition. Preferred OS browser applies only when the operator runs `agent-kit dashboard` / `npm run dashboard` without `--no-open`. -Local-dev only. Read-only. No HITL gate. +Local-dev only. Read-only. No HITL gate unless `missionControl.preferredBrowser` is unset/`ask` and the operator wants to persist a preference (optional Ask; see step 4). -**Terminal counterpart:** `agent-kit dashboard` from the workspace cwd. The installed package includes `dashboard/start.mjs` from 4.8.2 onward. Fallbacks: env `MISSION_CONTROL_KIT_ROOT` / `AGENT_KIT_HOME`, sibling `../agent-kit`, or `node "$KIT_ROOT/dashboard/start.mjs"` with `MISSION_CONTROL_REPO_ROOT` set to this git root. On 4.8.0 or an older pin the panel assets are absent. +**Terminal / IDE-agnostic invoke:** `agent-kit dashboard` or `npx @dadado/agent-kit-cli dashboard` from the workspace cwd. The installed package includes `dashboard/start.mjs` from 4.8.2 onward. Fallbacks: env `MISSION_CONTROL_KIT_ROOT` / `AGENT_KIT_HOME`, sibling `../agent-kit`, or `node "$KIT_ROOT/dashboard/start.mjs"` with `MISSION_CONTROL_REPO_ROOT` set to this git root. On 4.8.0 or an older pin the panel assets are absent. ## When to Use @@ -19,7 +24,19 @@ Local-dev only. Read-only. No HITL gate. ### 1. Resolve roots ```bash -SNAPSHOT_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)" +# Prefer nearest Agent Kit install over git toplevel (nested monorepo packages). +SNAPSHOT_ROOT="$(pwd)" +d="$(pwd)" +while [ "$d" != "/" ]; do + if [ -f "$d/.cursor/agent-kit.json" ]; then + SNAPSHOT_ROOT="$d" + break + fi + d="$(dirname "$d")" +done +if [ ! -f "$SNAPSHOT_ROOT/.cursor/agent-kit.json" ]; then + SNAPSHOT_ROOT="$(git rev-parse --show-toplevel 2>/dev/null || pwd)" +fi ``` Find `KIT_ROOT` (directory that contains `dashboard/start.mjs`): @@ -58,11 +75,14 @@ curl -sf "${MC_URL}dashboard-data.json" | node -e 'let d="";process.stdin.on("da `system.repoRoot` must equal `SNAPSHOT_ROOT`. If it does not, do not open that URL; re-run step 2 and use the new printed URL. -### 4. Open the panel +### 4. Open the panel (single path) + +**Invariant:** open **at most one** browser surface. Never launch multiple external browsers. -1. `cursor-ide-browser` → `browser_navigate` with `newTab: true` and the **printed** `MC_URL` -2. If result is `chrome-error://chromewebdata/`: connection refused; fix step 2/3, then navigate once more -3. Copy the URL for the operator (`pbcopy` on macOS) and mention Simple Browser as a fallback +1. Optional Ask (only when `.cursor/context/config.json` → `missionControl.preferredBrowser` is missing, null, or `"ask"`, and the operator may want a saved OS preference for CLI opens): labels `Keep IDE browser only` / `Remember preferred browser for CLI` / `Skip open`. If they pick remember, set `missionControl.preferredBrowser` to an app name (e.g. `Google Chrome`) or leave null for OS default on future CLI opens. Slash `/dashboard` itself still uses the IDE MCP path below (not a second OS open). +2. `cursor-ide-browser` → `browser_navigate` with `newTab: true` and the **printed** `MC_URL` (**only** this navigate; do not also run `open` / `xdg-open` / MCP + OS). +3. If result is `chrome-error://chromewebdata/`: connection refused; fix step 2/3, then navigate **once** more (still a single surface). +4. Copy the URL for the operator (`pbcopy` on macOS). Mention Simple Browser as a **manual** fallback for the human, not as a second agent-driven open. ### 5. Verify @@ -72,13 +92,14 @@ Wait for the `Live` badge (~3–5s) or curl the JSON again. Header should show t | Do | Do not | |----|--------| -| Set `MISSION_CONTROL_REPO_ROOT` to this git toplevel | Assume `http://localhost:3333` | +| Set `MISSION_CONTROL_REPO_ROOT` to the nearest Agent Kit install (or git toplevel when none) | Assume `http://localhost:3333` | | Use `node "$KIT_ROOT/dashboard/start.mjs"` | `kill` a listener to "free" 3333 for another project | -| Open the **printed** URL | Reuse HTTP 200 on any port without checking `system.repoRoot` | +| Open the **printed** URL once (IDE MCP) | Reuse HTTP 200 on any port without checking `system.repoRoot`; open every installed browser | | Leave other workspaces' Mission Control running | Start `serve.mjs` from a kit tree without `MISSION_CONTROL_REPO_ROOT` | ## Notes +- Preferred browser for CLI/OS opens: `missionControl.preferredBrowser` in `.cursor/context/config.json`, env `MISSION_CONTROL_PREFERRED_BROWSER`, or `agent-kit dashboard --browser "App Name"`. ADR `2026-08-11_mission-control-preferred-browser.md`. - Snapshot (plans, HANDOFF, git, memory) = `MISSION_CONTROL_REPO_ROOT`. Static UI = kit tree that hosts `dashboard/`. - Explicit `PORT` overrides hashing; if that port belongs to another root, start refuses (no kill). - Loopback only by default. LAN: `/dashboard-broadcast` (token-gated). @@ -94,5 +115,6 @@ Wait for the `Live` badge (~3–5s) or curl the JSON again. Header should show t | Expected `:3333` | Per-workspace port allocation | Use printed URL / `system.port` | | `chrome-error://chromewebdata/` | Server not listening on that URL | Re-run starter; navigate again | | `PORT … will not kill another workspace` | Explicit `PORT` held by another root | `unset PORT` and re-run starter | -| `No dashboard/start.mjs found` | Consumer-only tree | Set `MISSION_CONTROL_KIT_ROOT` / `AGENT_KIT_HOME` or sibling `../agent-kit` | +| `No dashboard/start.mjs found` | Consumer-only tree | Set `MISSION_CONTROL_KIT_ROOT` / `AGENT_KIT_HOME`, sibling `../agent-kit`, or `npx @dadado/agent-kit-cli@latest dashboard` | +| `403 Forbidden` on CLI package fetch | Registry auth policy or private scope | `npm login`, check `.npmrc`, or use env/sibling fallback | | Empty skeletons | Cold snapshot | Wait for `Live` | diff --git a/.cursor/commands/dogfood.md b/.cursor/commands/dogfood.md index 75c4af0..b203a70 100644 --- a/.cursor/commands/dogfood.md +++ b/.cursor/commands/dogfood.md @@ -1,3 +1,8 @@ +--- +name: dogfood +description: File a private dogfood note from the current chat into the local inbox. +--- + # Command: /dogfood ## Goal @@ -85,7 +90,7 @@ Check in order: **Consumer lane:** - Ensure `.cursor/dogfood/` exists (create if missing). - Write `.cursor/dogfood/cursor__.md`. -- Write or append to `.cursor/dogfood/README.md` with the same Unprocessed/Processed structure as the factory README. +- Write or append to `.cursor/dogfood/README.md` using the same Unprocessed/Processed structure as the factory README, with headings pinned to `### Unprocessed Files` and `### Processed Files` (H3). Existing consumer indexes that already use `## Unprocessed Files` remain valid on read (sessionStart + Broad Intake accept `##` or `###`); do not rewrite them unless the operator asks. - The folder is local-only; do not `git add` it. ### Step 4: Cross-repo bridge (optional, operator-initiated only) @@ -136,3 +141,4 @@ If the operator chooses `Keep local only`, stop. The local file is the record. - `.cursor/memory/decisions/2026-07-31_dogfood-ingest-contract.md` — ingest contract - `.cursor/memory/decisions/2026-07-17_session-origin-not-product-usecase.md` — hygiene - Cursor product-update gaps may route here via `/cursor-update-awareness` (Ask → `/dogfood`) +- Incoming **public** issue triage (factory-only) is `/public-issue-triage`, not this command diff --git a/.cursor/commands/field-report-resolve.md b/.cursor/commands/field-report-resolve.md index 09df7ca..cc353d7 100644 --- a/.cursor/commands/field-report-resolve.md +++ b/.cursor/commands/field-report-resolve.md @@ -1,3 +1,8 @@ +--- +name: field-report-resolve +description: Dismiss Field Report attention ids by appending them to the local dismissals store. +--- + # Command: /field-report-resolve ## Goal diff --git a/.cursor/commands/git-prod.md b/.cursor/commands/git-prod.md index 4544ab9..22e6d30 100644 --- a/.cursor/commands/git-prod.md +++ b/.cursor/commands/git-prod.md @@ -1,3 +1,8 @@ +--- +name: git-prod +description: Promote origin/staging to origin/main following the git prod routine (HITL confirmation required). +--- + # Git prod Follow the **git prod** routine to promote `origin/staging` to `origin/main` (production): diff --git a/.cursor/commands/git-staging.md b/.cursor/commands/git-staging.md index e4dc5a1..f5b2d94 100644 --- a/.cursor/commands/git-staging.md +++ b/.cursor/commands/git-staging.md @@ -1,3 +1,8 @@ +--- +name: git-staging +description: Bring local changes to the pre-production branch following the git staging routine. +--- + # Git staging Follow the **git staging** routine to bring local changes to the pre-production branch (`origin/staging` in this repo, or the name configured for the project). @@ -8,6 +13,7 @@ Follow the **git staging** routine to bring local changes to the pre-production 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). 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. **Never** commit directly to `main`. -6. On completion: update `.cursor/HANDOFF.md` (phase in staging); memory-loop WRITE if it applies. -7. Optional: update the project's PM tool (ClickUp, Jira, ...) if MCP is configured. +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`. +6. **Never** commit directly to `main`. +7. On completion: update `.cursor/HANDOFF.md` (phase in staging); memory-loop WRITE if it applies. +8. Optional: update the project's PM tool (ClickUp, Jira, ...) if MCP is configured. diff --git a/.cursor/commands/handoff.md b/.cursor/commands/handoff.md index 68b3a94..d7dbd8d 100644 --- a/.cursor/commands/handoff.md +++ b/.cursor/commands/handoff.md @@ -1,3 +1,8 @@ +--- +name: handoff +description: Update HANDOFF.md to preserve current state for continuation in a new conversation. +--- + # Command: /handoff ## Goal diff --git a/.cursor/commands/hotfix.md b/.cursor/commands/hotfix.md index a4e8479..c56c281 100644 --- a/.cursor/commands/hotfix.md +++ b/.cursor/commands/hotfix.md @@ -1,3 +1,8 @@ +--- +name: hotfix +description: Ship a narrow, urgent change as a mini plan and run it with the /run-plan tick contract. +--- + # Command: /hotfix ## Goal diff --git a/.cursor/commands/plan-external-review.md b/.cursor/commands/plan-external-review.md index 754c257..5c80d4f 100644 --- a/.cursor/commands/plan-external-review.md +++ b/.cursor/commands/plan-external-review.md @@ -1,3 +1,8 @@ +--- +name: plan-external-review +description: Arm an optional external plan audit after /run-plan exhausts its implementable to-dos. +--- + # Command: /plan-external-review ## Goal @@ -46,7 +51,7 @@ 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 content sentinel). 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). 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 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. @@ -108,8 +113,14 @@ Script behavior (ADR): ## What Claude should produce +Same contract as `.cursor/context/templates/plan-external-review-prompt.md` (Claude's working prompt). Command prose must not lag the template. + - Monitor file: `.cursor/memory/plan-monitor-.md` (template: `plan-monitor.md`) -- Index row in `.cursor/memory/_index.md` when creating a new monitor +- Index row in `.cursor/memory/_index.md` when creating a new monitor (target must be git-tracked; add monitor by name) +- **Delivery truth first:** for each `completed` to-do, was the claimed work actually done? Verify against code, tests, APIs, infra, Git SHAs, and published artifacts. Docs, HANDOFF, and inventories are indicative only (`docs-professional-standard`; ADR `2026-08-01_docs-indicative-delivery-truth`) +- **Finding priority** (highest first): (1) delivery truth, (2) security, (3) logic gaps, (4) bad code/practices with path-level evidence. Rank Still open / residuals by this order +- **Evidence mandate:** every `PASS` / `GAP` / `FAIL` cites at least one path, SHA, command, or artifact check +- **Forbidden filler:** do not ship restated plan text with no verification; ceremony checklists marked Met without path/SHA/command evidence; "looks good" / empty praise with no findings; decorative prose that finds nothing because nothing was checked - No product commits unless a human asks after triage ## Cursor triage (next step) diff --git a/.cursor/commands/plan-review-triage.md b/.cursor/commands/plan-review-triage.md index 72a91d9..3ffdd37 100644 --- a/.cursor/commands/plan-review-triage.md +++ b/.cursor/commands/plan-review-triage.md @@ -1,10 +1,15 @@ +--- +name: plan-review-triage +description: Triage residuals from an external plan review monitor and guide next steps. +--- + # Command: /plan-review-triage ## Goal Triage residuals from a **Claude external plan review** monitor. Select the right monitor(s) (untriaged / explicit paths, **not** raw mtime), summarize open residuals, and guide next steps with **Ask questions**. -Supports **multi-path walk**: iterate multiple monitors in blocking-first then debt order when given several report paths (for example from Field Report **Review all**, or the path list printed by `/plan-external-review` after a batch). When remaining monitors share a **uniform** outcome class, use **one** batch Ask (still write a durable triage heading on every target). Mixed outcomes fall back to sequential Asks. +Supports **multi-path walk**: iterate multiple monitors in blocking-first then debt order when given several report paths (for example from Field Report **Review all**, or the path list printed by `/plan-external-review` after a batch). When remaining monitors share a **uniform** outcome class, use **one** batch Ask (still write a durable triage heading on every target). Mixed outcomes fall back to sequential Asks. Operator may expand Write residuals to the whole remaining set in one reply (`1 and write all the other`); enqueue via paced per-monitor Tasks (wave size 2) or one combined plan (see Step 6). ## When to Use @@ -106,6 +111,17 @@ Present a concise summary: > > **Still open:** [numbered list of residuals with IDs from monitor] +### Step 2b: Classify preferred outcome (termination policy) + +Before Ask, classify open residuals for **closeout depth** and severity (ADR `decisions/2026-08-11_plan-audit-residuals-termination.md`): + +1. **Theme family / closeout_depth:** strip leading `close-`, trailing `-residuals` / `-still-open`, and revision suffixes (`-rN`, `-rN-rM`, `-n2-n3`, `-a-f`, …). Depth includes prior Write-residuals/`close-*` hops for that family; a plan basename that already starts with `close-` is depth ≥ 1. +2. **Preferred class:** + - All open items are nits / docs-cite / process hygiene (Gaps voice, R14/R15, ledger, inherited CI dirt) with **no Blocking product defect** → prefer **Ack and stop** or **Fix nits only**. + - Monitor is already depth ≥ 1 and no Blocking product finding → prefer **Ack and stop** / **Fix nits only**; do **not** treat Write residuals as the happy path. + - Blocking product work remains and depth still allows a first residual (depth 0 → first `close-*`) → Write residuals remains eligible. +3. State the preferred class in the Step 2 summary. **Ask still shows all three labels** (HITL preserved). Operator may force Write residuals via Ask **Other** / explicit override when Blocking work remains past depth; record the override in the triage heading. + ### Step 3: Ask for triage decision Use **Ask questions** tool with these exact options: @@ -142,11 +158,13 @@ Rules: Enqueue residuals in-session via the `/backlog-add` contract (ADR `decisions/2026-07-28_triage-write-residuals-via-backlog.md`). Do **not** end on a clipboard `/start-project` paste as the happy path. +0. **Termination gate (before Broad Intake):** refuse another `close-*` enqueue when (a) closeout_depth ≥ 1 and no Blocking product finding, (b) Still open is only process/monitor hygiene owned by existing ADRs, or (c) the proposal would only restate acceptance / regenerate evidence for already-merged work. Redirect to **Ack and stop** or **Fix nits only** unless the operator explicitly overrides. Default **max closeout depth = 1** (ADR `decisions/2026-08-11_plan-audit-residuals-termination.md`). + 1. Persist the triage heading (Step 4) with Choice `Write residuals plan`. After the plan file exists, prefer upgrading or appending `## Residuals plan` / `## Follow-up plan` with the plan basename (durable heading on the monitor still required). -2. **Broad Intake Review** (required before propose): same buckets and triage labels as `/backlog-add` / `/start-project`. Reuse the Task(explore) worker contract from `.cursor/commands/backlog-add.md` (template: `.cursor/context/templates/command-worker-prompt.md`; Command may read `/plan-review-triage` Write residuals). Seed the goal from this monitor's Still open (and include-worthy Standing findings). Fallback: run Broad Intake inline when Task is unavailable. Do not invent a fifth triage label. +2. **Broad Intake Review** (required before propose): same Broad Intake buckets and triage labels as `/backlog-add` / `/start-project`, including **Unprocessed dogfood** (`dogfood/README.md` or `.cursor/dogfood/README.md` `##` or `### Unprocessed Files`; never auto-analyze). Reuse the Task(explore) worker contract from `.cursor/commands/backlog-add.md` (template: `.cursor/context/templates/command-worker-prompt.md`; Command may read `/plan-review-triage` Write residuals; include dogfood README paths in `read_scope`). Seed the goal from this monitor's Still open (and include-worthy Standing findings). Fallback: run Broad Intake inline when Task is unavailable. Do not invent a fifth triage label. -3. **Propose** a residuals plan from Still open + Broad Intake `include` / `error` findings (respect `ignore` / `note`). +3. **Propose** a residuals plan from Still open + Broad Intake `include` / `error` findings (respect `ignore` / `note`). Prefer a single combined residuals plan; do not invent a basename that continues an unbounded `close-*-still-open` chain when depth is already capped. 4. **Ask write confirm** with Ask questions (chat numbered-list fallback). Exact options: - `Write plan to backlog` @@ -197,22 +215,33 @@ Enqueue residuals in-session via the `/backlog-add` contract (ADR `decisions/202 When multiple report paths are in scope (explicit args **or** bare-command selection), the agent applies Steps 2-5 after gap-aware skip. Rules: -1. **Uniform batch HITL** - after skip, if **two or more** monitors still need a triage decision and their open residuals share the **same outcome class** (all Ack-and-stop, or all write-one-residuals / fix-nits for the shared set), present **one** Ask questions gate for the whole set. Do **not** require N identical replies. ADR: `decisions/2026-07-27_plan-review-triage-batch-uniform-hitl.md`. +1. **Uniform batch HITL** - after skip, if **two or more** monitors still need a triage decision and their open residuals share the **same outcome class** (all Ack-and-stop, or all write-one-residuals / fix-nits for the shared set), present **one** Ask questions gate for the whole set. Do **not** require N identical replies. ADR: `decisions/2026-07-27_plan-review-triage-batch-uniform-hitl.md`. When the uniform class is process-only or depth-capped, prefer batch **Ack and stop** / **Fix nits only** (still Ask; never silent-Ack). 2. **Mixed → sequential fallback** - if outcome classes differ, or the operator chooses a per-file path, Ask **per** monitor (legacy walk). -3. **No silent Ack** - every triage decision (batch or per-file) requires Ask questions (or chat numbered-list fallback). Never invent Ack without HITL. +3. **No silent Ack** - every triage decision (batch or per-file) requires Ask questions (or chat numbered-list fallback). Never invent Ack without HITL. Never silent-Ack **Blocking** findings that still need real product work when depth allows a first residual (or the operator overrides). 4. **Durable heading on every target** - after the chosen outcome, write `## Triage note` (or Follow-up / Residuals) on **each** monitor in the decided set before finishing. Batch Ack that updates only the first file is invalid. -5. **Batch Write residuals** - when the uniform choice is Write residuals plan, run **one** Broad Intake for the set, propose **one** combined residuals plan from the set's Still open items (plus intake `include` / `error`), then one backlog write-confirm Ask (`Write plan to backlog` / `Modify` / `Cancel`). On write, enqueue once and reference that plan path from each monitor's Residuals / Triage heading. Sequential/mixed walks may enqueue per monitor (each with its own Broad Intake). Never require a second `/start-project` paste after the backlog write. -6. **Stopping contract** - if the human stops mid-walk (disagrees, changes mind, or says stop), the walk stops at that point. Completed monitors keep their triage headings; remaining monitors stay untriaged. -7. **Path skipping** - non-existent or non-monitor paths are skipped with a one-line note. The walk continues to the next valid path. +5. **Batch Write residuals** - when the uniform (or operator-expanded) choice is Write residuals plan: + - **Default (shared theme):** one Broad Intake for the set → one combined residuals plan → one backlog write-confirm Ask. On write, enqueue once; each monitor's Residuals / Triage heading references that path. + - **Per-monitor (operator asks for one plan per monitor, or residuals do not share a coherent theme):** one Broad Intake seed per monitor (may share a skim pass), then **one residuals plan file per monitor**. Prefer this when the operator says e.g. `Write residuals for all` / `1 and write all the other` / `one plan per monitor`. + - **Write-confirm collapse:** after the first Write residuals choice in a multi-path walk, if the operator also authorizes the remaining untriaged targets in the same reply (e.g. `1 and write all the other`), treat that as write-confirm for the whole remaining set. Do **not** re-Ask write-confirm per monitor. Still write a durable triage heading on every target before enqueue. + - Never require a second `/start-project` paste after backlog write. Never park, activate, Gate B, or rewrite Run queue. +6. **Paced Task dispatch (API/usage hygiene)** - when writing **more than one** residuals plan via Task subagents: + - Dispatch **at most 2** plan-author Tasks in parallel (wave size 2). Do **not** fan out one Task per monitor in a single turn when N ≥ 3. + - After each wave returns, the parent consolidates (plan paths, HANDOFF Backlog bullets, `## Residuals plan` headings), then starts the next wave. Optional short pause between waves when the session is on Auto (same spirit as `interTickCooldownMs` ≥ 15000 after quota risk; see context-guardian). + - Each Task writes **only** its `.cursor/plans/.plan.md` (unique path). The **parent** appends all HANDOFF `- **Backlog plans:**` rows and monitor Residuals headings (avoids HANDOFF merge races). + - Shared cross-monitor debt (e.g. one stale ledger) must be **owned once**: first plan that includes it, or an explicit companion plan; later plans label that item `note` with a pointer. Do not enqueue N identical ledger-regen to-dos. + - Fallback: if Task dispatch is unavailable or quota-blocked, author plans inline one at a time (same pacing: finish one file before the next). Hard-stop on API/usage limit per context-guardian; do not keep dispatching. +7. **Stopping contract** - if the human stops mid-walk (disagrees, changes mind, or says stop), the walk stops at that point. Completed monitors keep their triage headings; remaining monitors stay untriaged. +8. **Path skipping** - non-existent or non-monitor paths are skipped with a one-line note. The walk continues to the next valid path. ## Hard stops 1. **Never treat Claude monitor as execute permission** - all paths require human confirmation 2. **Never `/git-prod`** from this command - residual fixes go through `/git-staging` only 3. **Never auto-implement** without the triage choice above -4. **Never skip Broad Intake or the backlog write-confirm Ask** on Write residuals plan; never park, activate, Gate B, or rewrite Run queue from this path. Clipboard `/start-project` is **not** the happy path (optional operator escape hatch only when they want activate + Gate B) +4. **Never skip Broad Intake or the backlog write-confirm Ask** on Write residuals plan (write-confirm may collapse for a remaining multi-path set when the operator authorizes it in the same reply as Write residuals). Never park, activate, Gate B, or rewrite Run queue from this path. Clipboard `/start-project` is **not** the happy path (optional operator escape hatch only when they want activate + Gate B). Do not fan out ≥3 plan-author Tasks in one turn (Step 6 pacing). 5. **No broad scope creep** in "Fix nits only" - redirect to Write residuals plan (backlog enqueue) for substantial work 6. **Never skip the triage heading** - including Ack and stop +7. **Never unbounded close-* conveyor** - enforce max closeout depth and nits/process-only defaults (Step 2b / Step 5A gate 0; ADR `decisions/2026-08-11_plan-audit-residuals-termination.md`). Depth-capped process-only Still open → Ack or Fix nits, not another `close-*`. ## Ask questions requirement @@ -261,6 +290,19 @@ Agent: Writes one plan file + Backlog row; ## Residuals plan (or Triage note) on referencing that path. No `/start-project` paste. Mixed outcomes stay sequential. ``` +## Example flow - Per-monitor Write residuals (paced Tasks) + +``` +User: /plan-review-triage +Agent: Six git-fresh untriaged monitors; mixed classes → sequential Ask starting at 1/6. +User: 1 and write all the other with a subagent for each +Agent: Treats as Write residuals + write-confirm for remaining set. + Appends ## Triage note on each target. + Dispatches plan-author Tasks in waves of 2 (not 5 at once). + Each Task writes one .cursor/plans/*.plan.md; parent updates HANDOFF + Backlog + ## Residuals plan on every monitor. Shared ledger owned once. +``` + ## References - Monitor template: `.cursor/context/templates/plan-monitor.md` diff --git a/.cursor/commands/run-plan-all.md b/.cursor/commands/run-plan-all.md index e56e93f..34c0dd2 100644 --- a/.cursor/commands/run-plan-all.md +++ b/.cursor/commands/run-plan-all.md @@ -1,3 +1,8 @@ +--- +name: run-plan-all +description: Orchestrate multiple plans as an ordered, deduplicated execution queue, one Task per plan. +--- + # Command: /run-plan-all ## Goal @@ -77,7 +82,7 @@ This step is delegated to a **Task(explore) subagent** using the reusable worker - **Repo:** `[absolute repo path]` - **Command:** `/run-plan-all` - **Task description:** "Scan recent merges (git log --first-parent --merges -20), recent commits (git log --first-parent --no-merges -10; git diff staging...HEAD --stat), CHANGELOG.md [Unreleased] + latest release, HANDOFF, and every eligible candidate plan (frontmatter + body) under .cursor/plans/. Return a structured PO synthesis report: logical execution order, overlap/dependency map, consolidation proposals, and coherence notes. See the Inputs table in the command for the full specification." - - **read_scope:** `[".cursor/plans/*.plan.md", ".cursor/HANDOFF.md", "CHANGELOG.md", ".cursor/memory/decisions/"]` (plus workspace-level git log/diff) + - **read_scope:** `[".cursor/plans/*.plan.md", ".cursor/HANDOFF.md", "CHANGELOG.md", ".cursor/memory/decisions/"]` (plus workspace-level git log/diff). Unprocessed dogfood is owned by the Confirm Queue preflight below; do not put dogfood paths on this explore worker. - **worker_contract:** "structured PO synthesis report: ordered plan list, overlap/dependency annotations, consolidation proposals, coherence notes, plus staging-ready (lint)" - **max_ticks:** 2 - **worker_type:** explore @@ -92,6 +97,8 @@ This step is delegated to a **Task(explore) subagent** using the reusable worker ## Confirm Queue (Ask questions) +**Unprocessed dogfood preflight (advisory):** before the confirm Ask, skim `##` or `### Unprocessed Files` in factory `dogfood/README.md` or consumer `.cursor/dogfood/README.md`. Non-empty: mention count and top titles once in the Ask body (standard triage labels). Empty or missing: silent OK. Never auto-analyze, never invent Field Reports, never refuse the queue solely because the inbox is non-empty. Orchestrator owns this skim for the batch: per-plan `/run-plan` workers must not re-recite the same inbox. sessionStart tip remains complementary (ADR `2026-08-11_dogfood-unprocessed-broad-intake-bucket.md`). + After synthesis, present the proposal using **Ask questions** tool. Include the ordered list, key overlaps/consolidations, and coherence notes in the question body. Fallback to chat numbered list if the tool is unavailable. > "Plans synthesized. Proposal: [N] plans in order, [M] consolidations, [K] overlaps. Here is the proposed execution queue..." @@ -177,9 +184,9 @@ 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). | +| `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` 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). Then suggest `/git-prod` if staging is ahead of `main` (separate HITL). | +| 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`. @@ -210,6 +217,7 @@ Rules: - Read `.cursor/HANDOFF.md` and the plan file first. Resume from the next pending/in_progress to-do. - Follow `/run-plan` (`.cursor/commands/run-plan.md`): tick contract, risk gates, staging-on-diff when there is a diff. - Findings-only: review workers (`review-*` / findings contracts) return structured findings (severity, path, evidence) and must not auto-fix product code. After findings, the in-plan `/run-plan` orchestrator applies `externalPlanReview.autoRemediate` (default false): fix-agent Task (small) or residuals backlog plan (large). Do not silent-apply. +- Unprocessed dogfood already skimmed by the orchestrator at queue-confirm — do not re-recite. - When this plan is exhausted (`outcome: completed`), **skip** chat exhaustion Ask/paste in the worker (parent orchestrator owns mid-batch + queue-end audits). Return the structured summary and stop. - Never `/git-prod`. Never ask the user for `/continue-plan` as the default path. - Do not start the next queued plan; this Task owns only this plan. diff --git a/.cursor/commands/run-plan-loop.md b/.cursor/commands/run-plan-loop.md index 5e9f40d..f9ee78e 100644 --- a/.cursor/commands/run-plan-loop.md +++ b/.cursor/commands/run-plan-loop.md @@ -1,3 +1,8 @@ +--- +name: run-plan-loop +description: Deprecated alias for /run-plan forcing the in-session loop strategy. +--- + # Command: /run-plan-loop (deprecated) **Deprecated alias.** Use **`/run-plan`**: one continuous command that picks the execution strategy itself. diff --git a/.cursor/commands/run-plan-orchestrated.md b/.cursor/commands/run-plan-orchestrated.md index 3a3f96e..c11420c 100644 --- a/.cursor/commands/run-plan-orchestrated.md +++ b/.cursor/commands/run-plan-orchestrated.md @@ -1,3 +1,8 @@ +--- +name: run-plan-orchestrated +description: Deprecated alias for /run-plan forcing the orchestrated strategy. +--- + # Command: /run-plan-orchestrated (deprecated) **Deprecated alias.** Use **`/run-plan`**: one continuous command that picks the execution strategy itself (orchestrated is already the default when Task / subagents are available). diff --git a/.cursor/commands/run-plan.md b/.cursor/commands/run-plan.md index a722089..c3f1dc4 100644 --- a/.cursor/commands/run-plan.md +++ b/.cursor/commands/run-plan.md @@ -1,3 +1,8 @@ +--- +name: run-plan +description: Run the active plan continuously until it is done or blocked, picking the execution strategy automatically. +--- + # Command: /run-plan ## Goal @@ -52,6 +57,7 @@ While `/run-plan` is active, it **overrides** the "1 phase = 1 chat / ask for a - Memory CHECK if the phase touches a known error/decision - **Pre-flight (API-limit stop):** if HANDOFF Gaps / Instruction / stop reason indicates an API/usage limit hard stop from a prior tick, **do not** mark a to-do `in_progress` or dispatch a Task until the operator confirms recovery (named model switch and/or wait for reset). Pre-flight is this HANDOFF check plus operator model choice only; the kit has **no** remaining-quota API. Align with `context-guardian` quota-blocked sessions. - **Audits pre-flight:** read `externalPlanReview.preflight` (`off` | `warn` | `block`; missing = `off`). When not `off`, check owed / untriaged audits for the active plan slug (Field Report owed, untriaged monitors, cadence WARNING). `warn`: surface once then continue. `block`: arm the launcher (prefer `--autonomous` when `mode: autonomous`, else `--paste-only`) or stop until deferred; never steal `/git-prod`. Stronger than advisory monitor skim (ADR `2026-07-27_audits-autonomous-plan-review-contract.md`). +- **Unprocessed dogfood preflight (advisory):** before marking the first to-do `in_progress` (or on resume of a continuous run after HANDOFF reload), skim `##` or `### Unprocessed Files` in factory `dogfood/README.md` or consumer `.cursor/dogfood/README.md`. Non-empty: mention count and top titles once with standard triage labels. Empty or missing: silent OK. Never auto-analyze, never invent Field Reports, never block the tick solely because the inbox is non-empty. When this tick runs inside a `/run-plan-all` per-plan Task and the orchestrator already skimmed Unprocessed at queue-confirm, skip re-reciting (orchestrator owns the skim). sessionStart tip remains complementary (ADR `2026-08-11_dogfood-unprocessed-broad-intake-bucket.md`). ### 2. Choose the next to-do @@ -105,7 +111,7 @@ After Final HANDOFF when the run stopped because all implementable to-dos are do - `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. - 2. On **exit 0:** run `/plan-review-triage` Ask for that monitor path (findings-only; no silent-Ack / auto-fix). + 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. ADR: `decisions/2026-07-27_audits-wait-freshness-enforce.md` (follow-on to `decisions/2026-07-27_audits-post-spawn-monitor-watch-continue.md`); silent PTY and session pile: `decisions/2026-07-30_audits-pty-progress-gate-zombie-policy.md`. @@ -132,7 +138,7 @@ After a findings-contract (or `review-*`) tick returns findings, read `.cursor/c | `autoRemediate` | Orchestrator action | |-----------------|---------------------| -| `false` (default) | **Do not** auto-fix product code. Do not treat `improvement-*` as an automatic apply. Choose: (1) **small/contained** → dispatch a separate **fix-agent** Task (implement to-do or explicit improvement unit), or (2) **large/multi-touch** → write or enqueue a **residuals backlog plan** (Ask when HITL is required). Record the choice in HANDOFF Gaps or Instruction. | +| `false` (default) | **Do not** auto-fix product code. Do not treat `improvement-*` as an automatic apply. Choose: (1) **small/contained** → dispatch a separate **fix-agent** Task (implement to-do or explicit improvement unit), or (2) **large/multi-touch** → write or enqueue a **residuals backlog plan** (Ask when HITL is required) **only when closeout depth and Blocking severity allow** (ADR `decisions/2026-08-11_plan-audit-residuals-termination.md`). Prefer Ack / Fix-nits path when findings are process-only. Record the choice in HANDOFF Gaps or Instruction. | | `true` | Review workers stay findings-only. Orchestrator **may** dispatch a fix-agent Task for small nits without an extra Ask; large/multi-touch still becomes a residuals plan. External-monitor path still requires `/plan-review-triage` before product edits. | **Invariant:** the review worker never edits product source, configs, or tests unless the to-do text explicitly authorizes product edits. `autoRemediate` only gates what the **orchestrator** does after findings exist. diff --git a/.cursor/commands/start-project.md b/.cursor/commands/start-project.md index 613d551..a067fb3 100644 --- a/.cursor/commands/start-project.md +++ b/.cursor/commands/start-project.md @@ -1,3 +1,8 @@ +--- +name: start-project +description: Bootstrap a plan with to-dos from any user payload, with two HITL gates. +--- + # Command: /start-project ## Goal @@ -52,6 +57,7 @@ Before proposing or writing a new plan, **scan** these sources (read/skim; do no | **Archived context** | Prior packs for same theme | `.cursor/context/archive/**` (if present; glob by topic) | | **Decisions** | ADRs that constrain the goal | `.cursor/memory/decisions/`, `_index.md` Decisions table | | **Memory** | Errors, audits, consolidations, review logs, plan-monitors, findings audits | `.cursor/memory/errors/`, `.cursor/memory/plan-monitor-*.md`, theme-matched `plan-review-*.md`, `_index.md` (Audits + Decisions) | +| **Unprocessed dogfood** | Factory/consumer inbox notes awaiting triage (not sessionStart-only) | `dogfood/README.md` or `.cursor/dogfood/README.md` under `##` or `### Unprocessed Files`; skim titles/summaries only. Missing/empty inbox → no findings. Labels: ignore (owned by open plan), error/include (kit gap), note (inbox evidence only). Never auto-analyze or memory WRITE (ADR `decisions/2026-08-11_dogfood-unprocessed-broad-intake-bucket.md`) | | **Local docs** | SoT / inventories / getting-started that the goal touches | `docs/**`, especially files named in the payload or related SoT | | **Working tree** | Uncommitted local work that would collide | `git status`, `git diff` (staged + unstaged); do not commit | | **Recent commits** | What already shipped for this theme | `git log` (short, recent), related PR titles if available | @@ -75,8 +81,8 @@ The actual scanning and triage is delegated to a **Task(explore) subagent** usin 1. **Fill the template** — set these parameters: - **Repo:** `[absolute repo path]` - **Command:** `/start-project` - - **Task description:** "Scan the 9 Broad Intake buckets (prepared repository, active session, plans, archived context, decisions, memory, local docs, working tree, recent commits, product version) and return a structured triage report with findings per bucket, each labeled ignore/error/include/note." - - **read_scope:** `[".cursor/agent-kit.config.json", ".cursor/context/readiness.json", ".cursor/HANDOFF.md", ".cursor/context/current/", ".cursor/plans/*.plan.md", ".cursor/context/archive/**", ".cursor/memory/decisions/", ".cursor/memory/errors/", ".cursor/memory/plan-monitor-*.md", ".cursor/memory/plan-review-*.md", ".cursor/memory/_index.md", "docs/**", "package.json", "CHANGELOG.md"]` + - **Task description:** "Scan the Broad Intake buckets listed in this command (prepared repository, active session, plans, archived context, decisions, memory, Unprocessed dogfood, local docs, working tree, recent commits, product version) and return a structured triage report with findings per bucket, each labeled ignore/error/include/note. For Unprocessed dogfood: skim `dogfood/README.md` or `.cursor/dogfood/README.md` `##` or `### Unprocessed Files` only; never auto-analyze." + - **read_scope:** `[".cursor/agent-kit.config.json", ".cursor/context/readiness.json", ".cursor/HANDOFF.md", ".cursor/context/current/", ".cursor/plans/*.plan.md", ".cursor/context/archive/**", ".cursor/memory/decisions/", ".cursor/memory/errors/", ".cursor/memory/plan-monitor-*.md", ".cursor/memory/plan-review-*.md", ".cursor/memory/_index.md", "dogfood/README.md", ".cursor/dogfood/README.md", "docs/**", "package.json", "CHANGELOG.md"]` - **worker_contract:** "structured triage report: list of findings per bucket with triage labels (ignore/error/include/note)" - **max_ticks:** 2 diff --git a/.cursor/commands/summary.md b/.cursor/commands/summary.md index f33181d..1c4d44c 100644 --- a/.cursor/commands/summary.md +++ b/.cursor/commands/summary.md @@ -1,3 +1,8 @@ +--- +name: summary +description: Give a quick summary of the current state for a user who lost track of progress. +--- + # Command: /summary ## Goal diff --git a/.cursor/commands/tips.md b/.cursor/commands/tips.md index 6a99630..2f03a9f 100644 --- a/.cursor/commands/tips.md +++ b/.cursor/commands/tips.md @@ -1,3 +1,8 @@ +--- +name: tips +description: Reference for Cursor native commands (worktree, best-of-n) and how Agent Kit relates to them. +--- + # Tips: Cursor native commands (3.0+) ## `/worktree` diff --git a/.cursor/commands/update.md b/.cursor/commands/update.md index 6277a67..51b2cc9 100644 --- a/.cursor/commands/update.md +++ b/.cursor/commands/update.md @@ -1,3 +1,8 @@ +--- +name: update +description: Update the Agent Kit layer from the public registry, respecting protected L3 paths. +--- + # Command: /update ## Goal diff --git a/.cursor/context/config.example.json b/.cursor/context/config.example.json index 72aa5a5..1da90f6 100644 --- a/.cursor/context/config.example.json +++ b/.cursor/context/config.example.json @@ -47,5 +47,8 @@ "run-plan": "night-shift", "cli-run-plan": "ghost-runner" } + }, + "missionControl": { + "preferredBrowser": null } -} \ No newline at end of file +} diff --git a/.cursor/context/templates/handoff.md b/.cursor/context/templates/handoff.md index 1fd500e..3168845 100644 --- a/.cursor/context/templates/handoff.md +++ b/.cursor/context/templates/handoff.md @@ -2,6 +2,8 @@ Machine fields below must stay as `- **Field:**` bullets (Mission Control parses those). Do not replace Backlog / Parked / Run queue with `##` section headings alone. +**Mid-batch monitor pointers are not durable here.** `.cursor/HANDOFF.md` is gitignored session state. Pointers to sibling monitors must live in the tracked `.cursor/memory/_index.md` Audits row for the watched monitor (R14-paired in the same commit). Cite that row in the plan or monitor notes; do not rely on a HANDOFF line as delivery evidence. + - **Plan:** `file.plan.md` - **Last updated:** [YYYY-MM-DD HH:MM] - **Mode:** [manual | run-plan (orchestrated) | run-plan (in-session loop) | run-plan-all] @@ -23,6 +25,9 @@ Machine fields below must stay as `- **Field:**` bullets (Mission Control parses | Exact `none` when audits/queue plumbing is the only noise | `/git-prod` suggestion boilerplate as Gaps body | | | Review all / Resolve all / Copy review chatter | | | `none. Residuals…` / `none. Mid-batch…` (OK + pointer in Gaps body) | +| | `- **Gaps:** none` while `build` / Evidence checks are red or in-flight at merge HEAD | + +**Evidence-checks closeout:** before normalizing Gaps to exact `none` after a staging PR merge (or closeout that claims staging-ready), confirm green via `gh pr checks ` or local `pnpm evidence:knowledge-classification:check`. Name red/pending Evidence checks under Gaps until fixed or waived. ADR: `decisions/2026-08-01_evidence-checks-merge-gate.md`. **Before / after:** queue exhausted or mid-batch → prefer exact `none` (audits are not Gaps; put pointers in Instruction). Residuals after triage → short enqueue note, not a full Still-open table. See ADR `2026-07-27_mc-flight-log-panel.md`. diff --git a/.cursor/context/templates/plan-external-review-prompt.md b/.cursor/context/templates/plan-external-review-prompt.md index b24e891..323200a 100644 --- a/.cursor/context/templates/plan-external-review-prompt.md +++ b/.cursor/context/templates/plan-external-review-prompt.md @@ -20,33 +20,47 @@ Use template: `.cursor/context/templates/plan-monitor.md` ## Method Requirements - **Tick-by-tick analysis:** Each `/run-plan` or `/continue-plan` cycle that ends with plan status change -- **Evidence-based only:** Compare plan to-do requirements vs actual git commits/PRs +- **Delivery truth first:** For each `completed` to-do, answer: was the claimed work actually done? Verify against code, tests, APIs, infra, Git SHAs, and published artifacts. Docs, HANDOFF, and inventories are indicative only (rule `docs-professional-standard`; ADR `2026-08-01_docs-indicative-delivery-truth`). +- **Evidence-based only:** Compare plan to-do requirements vs actual git commits/PRs/file reads. Every `PASS` / `GAP` / `FAIL` cites at least one path, SHA, command, or artifact check. +- **Finding priority** (highest first): (1) delivery truth, (2) security, (3) logic gaps, (4) bad code/practices with path-level evidence. Rank Still open / residuals by this order. - **WIP awareness:** No verdicts on in-progress work; only judge `completed` to-dos - **Verdict scale:** `PASS` / `GAP` / `FAIL` with specific evidence - **Append-only:** Never edit prior ticks; add new sections chronologically -- **Terminal review:** Full acceptance checklist when plan reaches terminal state +- **Terminal review:** Full acceptance checklist when plan reaches terminal state; each checklist row needs artifact evidence, not ceremony alone + +## Forbidden filler + +Do **not** ship: + +- Restated plan text with no verification against the diff or tree +- Ceremony checklists marked Met without path/SHA/command evidence +- "Looks good" / empty praise sections with no findings and no evidence +- Decorative monitor prose that finds nothing because nothing was checked + +A review that only produces pretty prose fails this contract. ## Key Constraints -1. **ADR compliance:** Follow 2026-07-20 optional-claude-code-plan-review decision +1. **ADR compliance:** Follow 2026-07-20 optional-claude-code-plan-review decision and 2026-08-01 docs-indicative-delivery-truth 2. **Staging hygiene:** Never broad-add monitor file into product PRs (use add-by-name) 3. **HITL respect:** Flag residuals for human triage; no auto-remediation. Honor `externalPlanReview.autoRemediate` (default false): findings-only monitor; product fixes only after `/plan-review-triage`. -4. **Evidence verification:** Use `git show`, file diffs, direct reads — not speculation +4. **Evidence verification:** Use `git show`, file diffs, direct reads, artifact checks - not speculation or docs alone ## Path Convention Monitor files: `.cursor/memory/plan-monitor-{plan-slug}.md` -When creating new monitor, add row to `.cursor/memory/_index.md` in "Audits" or "Memory/Reviews" table with pattern: +When creating a new monitor, add a row to `.cursor/memory/_index.md` in the "Audits" (or "Memory/Reviews") table. **R14:** the target monitor must be **git-tracked**; add the monitor **by name** in the **same commit** as the index row (no committed index link to an untracked file). Pattern: ``` -|| [Monitor log — {plan-name}](plan-monitor-{plan-slug}.md) | {YYYY-MM-DD} | loop, plan-phases, tick-review, hitl-watch, {additional-tags} | +| [Monitor log — {plan-name}](plan-monitor-{plan-slug}.md) | {YYYY-MM-DD} | loop, plan-phases, tick-review, hitl-watch, {additional-tags} | ``` ## Success Criteria - Accurate tick-by-tick trace with no false alarms on WIP -- Clear evidence for each verdict (PASS/GAP/FAIL) +- Clear evidence for each verdict (PASS/GAP/FAIL): path, SHA, command, or artifact check +- Delivery-truth answered before style or ceremony nits - Machine-readable "Current state" briefing for next agents - Respectful of HITL boundaries and staging hygiene -- Residuals flagged for human decision, not auto-fixed +- Residuals flagged for human decision, not auto-fixed; ordered by finding priority - **Closeout:** print a ready-to-paste `/plan-review-triage` line with **explicit** `.cursor/memory/plan-monitor-.md` path(s) for every monitor written in this session. Do not recommend bare `/plan-review-triage` alone (filesystem mtime can rank older, already-triaged monitors above a fresh review). \ No newline at end of file diff --git a/.cursor/context/templates/plan-monitor.md b/.cursor/context/templates/plan-monitor.md index 23b149d..75ca42c 100644 --- a/.cursor/context/templates/plan-monitor.md +++ b/.cursor/context/templates/plan-monitor.md @@ -8,6 +8,9 @@ - A "tick" = one `/run-plan` or `/continue-plan` cycle that closes with a plan-frontmatter status change + `.cursor/HANDOFF.md` update (per `run-plan.md` §"Tick contract"). - Each entry below: **Plan asked** (to-do content + `worker_contract`) vs **Agent did** (commit/PR diff, HANDOFF) vs **Verdict**. +- **Delivery truth first:** primary question per completed to-do is "was the work done?" Verify against code, tests, APIs, infra, SHAs, published artifacts. Docs/HANDOFF/inventories are indicative only (`docs-professional-standard`; ADR `2026-08-01_docs-indicative-delivery-truth`). +- **Evidence required:** every `PASS` / `GAP` / `FAIL` cites a path, SHA, command, or artifact check. No "looks good" without that evidence. No restated plan text as a finding. No checklist Met without an artifact check. +- **Finding priority:** (1) delivery truth, (2) security, (3) logic gaps, (4) bad code/practices. Order Still open by this rank. - **WIP is expected and not an alarm.** A to-do mid-`max_ticks` with a partial diff and no HANDOFF/plan-status change yet is logged as `WIP - no verdict`, not a failure. A verdict (`PASS` / `GAP` / `FAIL`) is only assigned once the to-do's status flips to `completed` (or the agent explicitly stops on it). - `GAP` = shipped and directionally correct, but a named requirement (usually in `worker_contract`) has no visible evidence either way — noted so it isn't silently lost, not treated as a defect. - `FAIL` = plan requirement contradicted or missing from the shipped diff. @@ -54,10 +57,12 @@ ### Acceptance checklist vs actual shipped state +Evidence column must cite path, SHA, command, or artifact check (not docs alone). + | Acceptance item | Status | Evidence | |---|---|---| -| {requirement-1} | **Met/Partial/Not met** | {evidence} | -| {requirement-2} | **Met/Partial/Not met** | {evidence} | +| {requirement-1} | **Met/Partial/Not met** | {path / SHA / command} | +| {requirement-2} | **Met/Partial/Not met** | {path / SHA / command} | ### Residual items for human attention (none are severe; none block using the shipped behavior) diff --git a/.cursor/context/templates/plan.md b/.cursor/context/templates/plan.md index 87d30e4..0417447 100644 --- a/.cursor/context/templates/plan.md +++ b/.cursor/context/templates/plan.md @@ -55,6 +55,24 @@ For a tick whose **only** deliverable is ADR / memory / CHANGELOG / L0 markdown: - Set `inline_first: true` **only** when those checks will pass - Do **not** pair `inline_first: true` with `security-reviewer`, `tech-lead`, `explore`, or product paths unless the intent is Task (`force_task: true`) +### Mission Control dashboard SoT path + +When a to-do touches Mission Control HTML/CSS/JS in the dashboard shell, put **`dashboard/dashboard.html`** (repo-root) in `read_scope` and edit that file. Do **not** list `packages/cli/dashboard/dashboard.html`: that path is a gitignored prepack mirror (`scripts/sync-cli-dashboard.mjs`), not the source of truth. Pins in `plugin-ux-validation.test.ts` already resolve the repo-root SoT. + +### Durable mid-batch monitor pointers + +When a plan watches another monitor mid-batch (e.g. a residual closeout references a sibling monitor), record the pointer in a **tracked** surface, not in `.cursor/HANDOFF.md`. The canonical durable pointer is the Audits row in `.cursor/memory/_index.md` for the watched monitor. Keep HANDOFF as session state only. + +**Pattern:** + +- Add the watched monitor file by name in the same commit as its `_index.md` Audits row (R14 pairing). +- In the plan body or monitor notes, cite the `_index.md` row rather than a HANDOFF line. +- Do not write acceptance criteria that depend on a gitignored HANDOFF line as evidence. + +### Ledger regeneration boundary + +Do not confuse "a companion plan owns the stale-ledger residual" with "ledger regeneration is forbidden here." The knowledge-classification evidence gate resolves Audits targets against git-tracked files, so any change that introduces or reclassifies a tracked `.cursor/memory/**` file must regenerate `docs/evidence/knowledge-classification.json` in the same commit. Regeneration is required for the technical necessity of tracking new files; it is only forbidden as a duplicate residual-cleanup to-do when another plan has already committed to that specific stale-ledger residual. ADR: `decisions/2026-08-08_ledger-regen-policy.md`. + ### Split docs-only vs product ticks When a phase would mix markdown close-out with product evidence, **author two to-dos** so Auto can inline-first the docs tick and Task-isolate the product tick. Do not put `packages/**`, `dashboard/**`, or other product paths on a docs close-out `read_scope` "for context." ADR: `decisions/2026-07-27_auto-run-no-regression-invariants.md`. diff --git a/.cursor/rules/agent-output-hygiene.mdc b/.cursor/rules/agent-output-hygiene.mdc index 5307c8e..d6902ca 100644 --- a/.cursor/rules/agent-output-hygiene.mdc +++ b/.cursor/rules/agent-output-hygiene.mdc @@ -47,7 +47,7 @@ If yes, rewrite it. ## Relation to other rules -- **docs-professional-standard** - the project's inheritable documentation standard. +- **docs-professional-standard** - the project's inheritable documentation standard (write-time voice + read-time delivery truth: docs are indicative). - **code-deslop / clean-code** - code slop. - **ux-tone** - light tone, chat only. - **security-review** - secrets. diff --git a/.cursor/rules/docs-professional-standard.mdc b/.cursor/rules/docs-professional-standard.mdc index 8b8077c..b8f2b03 100644 --- a/.cursor/rules/docs-professional-standard.mdc +++ b/.cursor/rules/docs-professional-standard.mdc @@ -13,12 +13,27 @@ Applies to: README, `docs/`, ADRs, runbooks, `CHANGELOG`, public API comments, s **Does not** apply to chat tone (see `ux-tone`). HANDOFF and active plans are session state; still no gossip/meta-language (`agent-output-hygiene`). When promoting plan/HANDOFF content into `docs/`, **rewrite** it to the standard below. +## Indicative docs; delivery truth (read-time law) + +README, HANDOFF, inventories, claim matrices, and monitor prose are **indicative**. They orient; they are never sole proof of runtime, release, or "work done." + +When a claim matters (install path, shipped version, completed to-do, public behavior), verify against **code, tests, APIs, infra, Git SHAs, and published artifacts** before trusting the prose. Prefer those sources when they conflict with docs. + +**Review / acceptance priority** (highest first): + +1. **Delivery truth** - was the claimed work actually done? Does the statement match reality? +2. **Security** - breaches, unsafe defaults, secret handling +3. **Logic gaps** - broken flows, missing edge cases, false acceptance +4. **Bad code / practices** - maintainability defects with path-level evidence + +Write-time **Verifiable** (below) is the authoring half of the same contract. This section is the read-time half: do not treat pretty docs or empty "looks good" reviews as proof. Evidence: `errors/2026-07-30_docs-claimed-path-c-unpublished-after-ship.md`; ADR `decisions/2026-08-01_docs-indicative-delivery-truth.md`. + ## Required 1. **Project voice** - describe the system ("the CLI does X"), not the session ("today I configured X"). 2. **Inheritable** - stable names for components, commands, paths, and contracts; a glossary consistent with the code. 3. **Timeless** - prefer behavior and invariants over meeting dates, sprints, or "this week". -4. **Verifiable** - what the doc claims must match the code; do not invent behavior. +4. **Verifiable** - what the doc claims must match the code; do not invent behavior. Docs never sole proof (see Indicative docs above). 5. **Complete enough** - setup, usage, limits, and internal links; no narrative filler. ## Forbidden (transient / outside the project) @@ -45,6 +60,6 @@ If not, rewrite it. If it would be embarrassing on public GitHub, rewrite it. ## Relation to other rules - **agent-output-hygiene** - chat is not the repo (commits, HANDOFF, memory). -- **This rule** - the standard for *product/project documentation* (README, docs, ADRs). +- **This rule** - the standard for *product/project documentation* (README, docs, ADRs), including read-time delivery truth (docs are indicative). - **docs-repo** (skill/agent) - the writing process; must follow this rule. - **ux-tone** - chat only. diff --git a/.cursor/rules/hitl-ask-questions.mdc b/.cursor/rules/hitl-ask-questions.mdc index 7311bac..7d9502e 100644 --- a/.cursor/rules/hitl-ask-questions.mdc +++ b/.cursor/rules/hitl-ask-questions.mdc @@ -24,7 +24,7 @@ Agent Kit uses **Ask questions** tool (`AskQuestion` / ACP `cursor/ask_question` | Surface | Gate | |---------|------| | `install.md` (Port B / drag-install) | Registry URL/ref if needed; migrate nested `agent-kit/` confirm; optional git-hooks install | -| `/agent-kit-onboard` | One unresolved readiness decision at a time; confirm Git, branch, remote, hooks, CI, deploy, protection, and external mutations before acting | +| `/agent-kit-onboard` | One unresolved readiness decision at a time; confirm Git, branch, remote, hooks, CI, deploy, protection, and external mutations before acting; optional domain-skills scaffold gate after essentials (`Scaffold domain skills` / `Defer (record reason)` / `Skip`) before finish-setup CTA | | `context-guardian` readiness | Surface the first pending readiness check without blocking active plan/HANDOFF work; auto vs manual handoff preference (first time) | | `/start-project` | Single composite Gate A question: disposition + write merged into one pick (with active plan: backlog+write / park+write / modify / cancel; without active plan: write / write+backlog / modify / cancel); Gate B (start first unit / run-plan / edit / Add to backlog / Stop here); vague goal clarify; fallback rule: one numbered list per message | | `/backlog-add` | Vague-goal clarify; write confirm (`Write plan to backlog` / `Modify proposal first` / `Cancel`); never park/activate/Gate B | @@ -36,7 +36,7 @@ Agent Kit uses **Ask questions** tool (`AskQuestion` / ACP `cursor/ask_question` | `/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-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. **Write residuals:** 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`) | +| `/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 | | Other commands / docs | Any "stop and ask" / "Want to… (yes/no)" prose in L0 + getting-started / bootstrap | diff --git a/.cursor/scripts/plan-external-review-progress-gate.test.mjs b/.cursor/scripts/plan-external-review-progress-gate.test.mjs new file mode 100644 index 0000000..c58884a --- /dev/null +++ b/.cursor/scripts/plan-external-review-progress-gate.test.mjs @@ -0,0 +1,130 @@ +import assert from "node:assert"; +import { spawnSync } from "node:child_process"; +import { dirname, join } from "node:path"; +import { test } from "node:test"; +import { fileURLToPath } from "node:url"; + +const ROOT = dirname(dirname(dirname(fileURLToPath(import.meta.url)))); +const SCRIPT = join(ROOT, ".cursor/scripts/plan-external-review.sh"); + +function extractFunction() { + return spawnSync("sed", ["-n", "/^wait_for_pty_progress() {/,/^}$/p", SCRIPT], { + encoding: "utf8", + }).stdout; +} + +function runGate({ bytesSequence, aliveSequence = [], timeout = 10, channel = "test" }) { + const fn = extractFunction(); + const aliveArray = aliveSequence.length > 0 ? aliveSequence : [1]; + const bash = ` +PROGRESS_TIMEOUT=${timeout} +MOCK_BYTES=(${bytesSequence.join(" ")}) +MOCK_ALIVE=(${aliveArray.join(" ")}) +MOCK_TIME=0 +BYTES_CTR="$(mktemp)" +ALIVE_CTR="$(mktemp)" +echo 0 > "$BYTES_CTR" +echo 0 > "$ALIVE_CTR" +trap 'rm -f "$BYTES_CTR" "$ALIVE_CTR"' EXIT + +pty_scrollback_bytes() { + local idx val + idx="$(cat "$BYTES_CTR")" + val="\${MOCK_BYTES[$idx]:-0}" + echo $((idx + 1)) > "$BYTES_CTR" + echo "$val" +} + +pty_session_alive() { + local idx val + idx="$(cat "$ALIVE_CTR")" + val="\${MOCK_ALIVE[$idx]:-1}" + echo $((idx + 1)) > "$ALIVE_CTR" + return "$((1 - val))" +} + +sleep() { + local n="$1" + if [[ -z "$n" || ! "$n" =~ ^[0-9]+$ ]]; then n=0; fi + MOCK_TIME=$((MOCK_TIME + n)) +} + +date() { + echo "$MOCK_TIME" +} + +${fn} + +wait_for_pty_progress "${channel}" "test-session" +`; + const result = spawnSync("bash", ["-c", bash], { encoding: "utf8" }); + return { output: result.stdout, exitCode: result.status ?? 1 }; +} + +test("banner appears within baseline window and growth passes gate", () => { + const { output, exitCode } = runGate({ + bytesSequence: [0, 100, 200], + timeout: 10, + }); + assert.strictEqual(exitCode, 0, output); + assert.match(output, /audits: progress gate passed/); + assert.match(output, /scrollback grew from 100 to 200 bytes/); +}); + +test("banner never appears within 7s baseline deadline -> fail with banner message", () => { + const { output, exitCode } = runGate({ + bytesSequence: [0, 0, 0, 0, 0, 0, 0], + timeout: 10, + }); + assert.strictEqual(exitCode, 1, output); + assert.match(output, /audits: progress gate failed \(launcher banner did not appear\)/); + assert.doesNotMatch(output, /scrollback grew/); +}); + +test("banner appears but no growth beyond threshold -> timeout failure", () => { + const { output, exitCode } = runGate({ + bytesSequence: [0, 100, 100, 100, 100, 100, 100], + timeout: 6, + }); + assert.strictEqual(exitCode, 1, output); + assert.match(output, /audits: progress gate failed \(no growth beyond launcher banner after/); + assert.match(output, /baseline=100, current=100/); +}); + +test("session vanishes before growth -> fail with vanish message", () => { + const { output, exitCode } = runGate({ + bytesSequence: [0, 100, 100], + aliveSequence: [1, 1, 0], + timeout: 60, + }); + assert.strictEqual(exitCode, 1, output); + assert.match( + output, + /audits: progress gate failed \(session test-session vanished before producing output\)/, + ); +}); + +test("unknown channel skips gate gracefully", () => { + const { output, exitCode } = runGate({ + bytesSequence: [-1], + channel: "unknown", + timeout: 10, + }); + assert.strictEqual(exitCode, 0, output); + assert.match(output, /audits: progress gate skipped \(channel: unknown; no scrollback API\)/); +}); + +test("disabled gate returns immediately", () => { + const fn = extractFunction(); + const bash = ` +PROGRESS_TIMEOUT=0 +${fn} +wait_for_pty_progress "screen" "test-session" +`; + const result = spawnSync("bash", ["-c", bash], { encoding: "utf8" }); + assert.strictEqual(result.status ?? 1, 0, result.stdout); + assert.match( + result.stdout, + /audits: progress gate disabled \(AGENT_KIT_AUDIT_PROGRESS_TIMEOUT=0\)/, + ); +}); diff --git a/.cursor/scripts/plan-external-review.sh b/.cursor/scripts/plan-external-review.sh index f99020f..d28b062 100755 --- a/.cursor/scripts/plan-external-review.sh +++ b/.cursor/scripts/plan-external-review.sh @@ -1172,6 +1172,8 @@ autoRemediate (from config): $auto_remediate Contract reminders: - Evidence-based monitor only under .cursor/memory/plan-monitor-.md +- Delivery truth first: was each completed to-do actually done? Cite path/SHA/command/artifact; docs and HANDOFF are indicative only +- Finding priority: (1) delivery truth (2) security (3) logic gaps (4) bad code/practices; no filler "looks good" without evidence - Findings-only: never auto-fix product source; write the monitor and flag residuals for triage - No product commits unless a human explicitly requests them after /plan-review-triage - When autoRemediate is false (default): do not apply or suggest starting product edits in this session @@ -1200,6 +1202,8 @@ $plan_list Contract reminders: - Evidence-based monitor only under .cursor/memory/plan-monitor-.md +- Delivery truth first: was each completed to-do actually done? Cite path/SHA/command/artifact; docs and HANDOFF are indicative only +- Finding priority: (1) delivery truth (2) security (3) logic gaps (4) bad code/practices; no filler "looks good" without evidence - Findings-only: never auto-fix product source; write the monitor and flag residuals for triage - No product commits unless a human explicitly requests them after /plan-review-triage - When autoRemediate is false (default): do not apply or suggest starting product edits in this session diff --git a/.cursor/skills/core/docs-repo/SKILL.md b/.cursor/skills/core/docs-repo/SKILL.md index 058baac..0425b2f 100644 --- a/.cursor/skills/core/docs-repo/SKILL.md +++ b/.cursor/skills/core/docs-repo/SKILL.md @@ -1,7 +1,7 @@ --- name: Docs & Repo description: Docs & Repo skill. -version: 0.1.0 +version: 0.1.1 category: core --- @@ -16,6 +16,7 @@ Documentation belongs to the **project**, not the chat or the author. Follow rul - Professional, inheritable voice (system behavior, not “today I…”) - No transient references: people, unrelated projects, session drama, client-specific workspace IDs - Timeless where possible; verify against code +- Docs/HANDOFF/inventories are **indicative**; delivery truth comes from code, APIs, infra, SHAs, and published artifacts (never docs alone) - Inheritance test: a new maintainer can use it in 12 months without the chat Also follow `agent-output-hygiene` (no agent metalinguage in artifacts). diff --git a/.github/CODE_OF_CONDUCT.md b/.github/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..3ae6534 --- /dev/null +++ b/.github/CODE_OF_CONDUCT.md @@ -0,0 +1,144 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, caste, color, religion, or sexual +identity and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +- Demonstrating empathy and kindness toward other people +- Being respectful of differing opinions, viewpoints, and experiences +- Giving and gracefully accepting constructive feedback +- Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +- Focusing on what is best not just for us as individuals, but for the overall + community + +Examples of unacceptable behavior include: + +- The use of sexualized language or imagery, and sexual attention or advances of + any kind +- Trolling, insulting or derogatory comments, and personal or political attacks +- Public or private harassment +- Publishing others' private information, such as a physical or email address, + without their explicit permission +- Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +Community leaders have the right and responsibility to remove, edit, or reject +comments, commits, code, wiki edits, issues, and other contributions that are +not aligned to this Code of Conduct, and will communicate reasons for moderation +decisions when appropriate. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. +Examples of representing our community include using an official e-mail address, +posting via an official social media account, or acting as an appointed +representative at an online or offline event. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the maintainers. + +This project does **not** publish a dedicated conduct mailbox. Until one exists, +use whichever of these fits the report: + +- **Anything that is also a security issue** — use the private channel in + [SECURITY.md](SECURITY.md). Never file a security report in a public issue. +- **Platform-level abuse** (harassment, spam, impersonation, threats) — GitHub's + [Report abuse](https://github.com/contact/report-abuse) form. It routes to + GitHub Trust & Safety, who can act on accounts and escalate to repository + owners. +- **Everything else** — open a normal issue on the public repository. Be aware + that issues are public; do not include another person's private information. + +All complaints will be reviewed and investigated promptly and fairly. + +All community leaders are obligated to respect the privacy and security of the +reporter of any incident. + +## Enforcement Guidelines + +Community leaders will follow these Community Impact Guidelines in determining +the consequences for any action they deem in violation of this Code of Conduct: + +### 1. Correction + +**Community Impact**: Use of inappropriate language or other behavior deemed +unprofessional or unwelcome in the community. + +**Consequence**: A private, written warning from community leaders, providing +clarity around the nature of the violation and an explanation of why the +behavior was inappropriate. A public apology may be requested. + +### 2. Warning + +**Community Impact**: A violation through a single incident or series of +actions. + +**Consequence**: A warning with consequences for continued behavior. No +interaction with the people involved, including unsolicited interaction with +those enforcing the Code of Conduct, for a specified period of time. This +includes avoiding interactions in community spaces as well as external channels +like social media. Violating these terms may lead to a temporary or permanent +ban. + +### 3. Temporary Ban + +**Community Impact**: A serious violation of community standards, including +sustained inappropriate behavior. + +**Consequence**: A temporary ban from any sort of interaction or public +communication with the community for a specified period of time. No public or +private interaction with the people involved, including unsolicited interaction +with those enforcing the Code of Conduct, is allowed during this period. +Violating these terms may lead to a permanent ban. + +### 4. Permanent Ban + +**Community Impact**: Demonstrating a pattern of violation of community +standards, including sustained inappropriate behavior, harassment of an +individual, or aggression toward or disparagement of classes of individuals. + +**Consequence**: A permanent ban from any sort of public interaction within the +community. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant][homepage], +version 2.1, available at +[https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. + +Community Impact Guidelines were inspired by +[Mozilla's code of conduct enforcement ladder][mozilla-coc]. + +For answers to common questions about this code of conduct, see the FAQ at +[https://www.contributor-covenant.org/faq][faq]. Translations are available at +[https://www.contributor-covenant.org/translations][translations]. + +[homepage]: https://www.contributor-covenant.org +[v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html +[mozilla-coc]: https://github.com/mozilla/diversity +[faq]: https://www.contributor-covenant.org/faq +[translations]: https://www.contributor-covenant.org/translations diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml new file mode 100644 index 0000000..23ed8b9 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.yml @@ -0,0 +1,110 @@ +name: Bug report +description: Something in the CLI, the installed files, Mission Control, or the docs is broken. +title: "bug: " +labels: ["bug"] +body: + - type: markdown + attributes: + value: | + Do **not** use this form for a security vulnerability. Follow the + [security policy](https://github.com/agent-kit-startup/agent-kit/blob/main/.github/SECURITY.md) + instead - a public issue exposes the problem before a fix exists. + + - type: checkboxes + id: preflight + attributes: + label: Before you file + options: + - label: I searched existing issues and this is not a duplicate. + required: true + - label: This is not a security vulnerability. + required: true + + - type: dropdown + id: surface + attributes: + label: Affected surface + description: Where does it go wrong? + options: + - CLI (install / update / add / doctor / contribute) + - Installed L0 files (commands, rules, hooks, templates) + - Registry skill or Agent Persona + - Mission Control dashboard + - Documentation + - Not sure + validations: + required: true + + - type: textarea + id: what-happened + attributes: + label: What happened + description: What you ran, what happened, and what you expected instead. + placeholder: | + I ran `npx @dadado/agent-kit-cli install` in an existing Next.js repo. + It failed with ... I expected ... + validations: + required: true + + - type: textarea + id: repro + attributes: + label: Steps to reproduce + value: | + 1. + 2. + 3. + validations: + required: true + + - type: textarea + id: doctor + attributes: + label: doctor output + description: Output of `npx @dadado/agent-kit-cli doctor --json`. This is the single most useful thing you can paste. + render: json + + - type: textarea + id: logs + attributes: + label: Terminal output + description: Relevant output as text, not a screenshot. Redact any tokens or paths you do not want public. + render: shell + + - type: input + id: cli-version + attributes: + label: CLI version + description: Output of `npx @dadado/agent-kit-cli --version`. + placeholder: "5.0.0" + validations: + required: true + + - type: input + id: installed-version + attributes: + label: Installed kit version + description: The `version` field in your project's `.cursor/agent-kit.json`, if the issue involves installed files. + placeholder: "5.0.0" + + - type: dropdown + id: ide + attributes: + label: IDE + description: Cursor is the first-class target; VS Code and Windsurf generators are partial by design. + options: + - Cursor + - VS Code + - Windsurf + - None / CLI only + - Other + validations: + required: true + + - type: input + id: environment + attributes: + label: OS and Node version + placeholder: "macOS 15.4, Node 20.11.1" + validations: + required: true diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 0000000..d11aa87 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,14 @@ +blank_issues_enabled: false +contact_links: + - name: Read the docs first + url: https://github.com/agent-kit-startup/agent-kit/blob/main/docs/README.md + about: Install, configuration, skills, layers, and packs are documented. Most questions already have a written answer. + - name: Support and question routing + url: https://github.com/agent-kit-startup/agent-kit/blob/main/.github/SUPPORT.md + about: Where to file what, and what makes a question answerable. Questions are welcome as issues - this page tells you which template to pick. + - name: Report a security vulnerability + url: https://github.com/agent-kit-startup/agent-kit/blob/main/.github/SECURITY.md + about: Never report a vulnerability in a public issue. Use the private channel described here. + - name: Code of Conduct + url: https://github.com/agent-kit-startup/agent-kit/blob/main/.github/CODE_OF_CONDUCT.md + about: Contributor Covenant 2.1, and how to raise a conduct concern. diff --git a/.github/ISSUE_TEMPLATE/feature_request.yml b/.github/ISSUE_TEMPLATE/feature_request.yml new file mode 100644 index 0000000..a8037d7 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.yml @@ -0,0 +1,76 @@ +name: Feature request +description: Propose a capability, a skill, or a change to how the kit behaves. +title: "feat: " +labels: ["enhancement"] +body: + - type: markdown + attributes: + value: | + Agent Kit is a human-in-the-loop harness: plans, handoff, memory, and a + staging to prod git flow with explicit confirmation before production. + Proposals that remove a human gate, or that add a parallel agent gateway + outside Cursor-native tooling, are usually declined - see + [CONTRIBUTING](https://github.com/agent-kit-startup/agent-kit/blob/main/docs/CONTRIBUTING.md) + and the + [five-layer claim matrix](https://github.com/agent-kit-startup/agent-kit/blob/main/docs/five-layer-claim-matrix.md). + + - type: checkboxes + id: preflight + attributes: + label: Before you file + options: + - label: I searched existing issues and the docs index, and this is not already covered or requested. + required: true + + - type: dropdown + id: kind + attributes: + label: What kind of change is this? + options: + - New registry skill + - New Agent Persona + - CLI command or flag + - Installed L0 behavior (command, rule, hook, template) + - Mission Control + - Documentation + - Other + validations: + required: true + + - type: textarea + id: problem + attributes: + label: Problem + description: What are you trying to do today, and where does the kit get in the way? Describe the situation, not the solution. + validations: + required: true + + - type: textarea + id: proposal + attributes: + label: Proposed change + description: What should happen instead. Include the command, file, or screen it would touch. + validations: + required: true + + - type: textarea + id: overlap + attributes: + label: Overlap with what exists + description: | + For a new skill or persona this is required: name the closest existing entries in the registry + and say why this is not the same scope. Improving an existing skill is a valid contribution and + is often the better path. + + - type: textarea + id: alternatives + attributes: + label: Alternatives considered + description: Workarounds you tried, and why they are not enough. + + - type: checkboxes + id: contribution + attributes: + label: Contribution + options: + - label: I am willing to open a PR for this. diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 0000000..1c15039 --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,45 @@ + + +## What and why + + + +## How to verify + + + +```bash +pnpm install && pnpm lint && pnpm test +``` + +## Checklist + +- [ ] Conventional Commits title (`feat:`, `fix:`, `docs:`, `chore:`, …) +- [ ] Small and focused — one topic per PR +- [ ] `pnpm install && pnpm lint && pnpm test` pass locally +- [ ] Docs updated when behavior changed +- [ ] No `HANDOFF.md`, no `.cursor/plans/*.plan.md`, no `.cursor/memory/`, no `.env`, no credentials +- [ ] No secrets and no agent metalanguage (same gate as `agent-kit contribute`) +- [ ] Prefer Cursor-native tooling — no parallel agent gateway added or documented + +## Registry contributions only + +Skip this block if you did not touch `registry/`. + +- [ ] Dedupe: listed the closest existing skills and why this is not an overlap (or targeted the existing skill instead) +- [ ] `version` and `category` present in frontmatter; semver bumped if behavior changed +- [ ] Community contributions stay under `community/`; no product PM/n8n added as a Core Pack `alwaysApply` rule +- [ ] `node scripts/build-registry.mjs` run, and `registry/registry.json` committed +- [ ] `pnpm --filter @dadado/agent-kit-cli test` run if the CLI was touched +- [ ] New category linked from `docs/marketplace.md` + + diff --git a/.github/SECURITY.md b/.github/SECURITY.md new file mode 100644 index 0000000..17ede9f --- /dev/null +++ b/.github/SECURITY.md @@ -0,0 +1,91 @@ +# Security Policy + +Agent Kit is a human-in-the-loop harness for AI-assisted IDEs. It installs prompt +and workflow files into a developer's project and ships a local CLI plus a +loopback observability panel (Mission Control). Reports about any of those +surfaces are welcome. + +## Supported versions + +| Version | Supported | +|---------|-----------| +| Latest `5.x` release of `@dadado/agent-kit-cli` | Yes | +| Anything older | No — upgrade first, then re-test | + +There is no long-term-support branch. Fixes land on the latest release line. + +## Reporting a vulnerability + +**Do not dump proof-of-concept detail into a public issue before a private channel exists.** + +Use, in order of preference: + +1. **GitHub private vulnerability reporting (preferred when enabled)** — on the public + repository, go to the **Security** tab → **Report a vulnerability**. This + opens a private advisory visible only to you and the maintainers. +2. **Public issue as a private-channel request (fallback)** — if private vulnerability + reporting is unavailable (button missing, fork without PVR, or reporting fails), + open a public issue that contains **no** technical detail, no proof of concept, + and no reproduction steps. Say only that you have a security report and need a + private channel. A maintainer will open a private advisory (or email) and invite + you to it. + +### What to include + +- Affected surface: CLI command, installed L0 template, registry skill, Mission + Control endpoint, or the sync/CI tooling. +- Version: output of `npx @dadado/agent-kit-cli --version`, plus the value of + `version` in the project's `.cursor/agent-kit.json` if the issue is about + installed files. +- Environment: OS, Node version, IDE (Cursor / VS Code / Windsurf). +- Reproduction steps and observed vs expected behavior. +- Impact you believe it has. + +### What to expect + +This project is maintained on a best-effort basis by a small team. There is no +contractual response time and no bug bounty. Reports are triaged in the order +received; a fix, a mitigation, or an explicit "won't fix, here's why" is the +intended outcome of every valid report. Please allow a reasonable window for a +fix before disclosing publicly, and tell us if you have a disclosure deadline. + +Credit is given in the advisory and the changelog unless you ask otherwise. + +## Scope and known posture + +These are documented design decisions, not vulnerabilities. Please read them +before reporting. + +- **Mission Control is local and single-developer.** It binds `127.0.0.1` by + default. The opt-in LAN mode (`/dashboard-broadcast`) binds a non-loopback + interface only with explicit intent and a required session token. Config writes + stay loopback-only and allowlisted. Multi-user or internet-facing hosting is + out of scope by design — see + [Mission Control production-ship constraints](../docs/getting-started.md#mission-control-production-ship-constraints). +- **Continuous plan execution runs with the IDE sandbox disabled.** `/run-plan` + needs filesystem and git access to do its job. Plan to-dos and registry skills + therefore function as direct agent instructions and must be reviewed before + they are run — see + [Security considerations](../docs/getting-started.md#security-considerations). +- **Prompt content is not a trust boundary.** A malicious skill, plan, or rule + that a user installs on purpose can instruct the agent. Report the *delivery* + path (for example: a way to get unreviewed content into an install) rather than + the fact that instructions are followed. + +In scope and worth reporting: arbitrary file write or command execution from CLI +input, path traversal in install or update, credential or token leakage, the +public sync allowlist leaking private paths, an unauthenticated mutation reachable +on a non-loopback bind, and dependency issues with a practical exploit path here. + +## Secrets + +The public repository contains no secrets, and CI secrets live only on the +private factory repository. If you believe a credential has been committed +anywhere in the history, report it through the private channel above rather than +in an issue. + +## Related + +- [Code of Conduct](CODE_OF_CONDUCT.md) +- [Support](SUPPORT.md) +- [Contributing](../docs/CONTRIBUTING.md) diff --git a/.github/SUPPORT.md b/.github/SUPPORT.md new file mode 100644 index 0000000..cbc27aa --- /dev/null +++ b/.github/SUPPORT.md @@ -0,0 +1,60 @@ +# Support + +Thanks for using Agent Kit. This page says where to go, so your question reaches +the right place instead of sitting unanswered. + +GitHub Discussions is not enabled on this project. **Issues are the support +channel** — questions are welcome there, not just bug reports. + +## Try the docs first + +Most support questions already have a written answer: + +| You want to | Read | +|-------------|------| +| Install and run the first loop | [Getting started](../docs/getting-started.md) | +| Install without a nested `agent-kit/` folder | [Bootstrap](../docs/bootstrap.md) | +| Prepare an existing repo before `/start-project` | [Repository readiness onboarding](../docs/repository-readiness-onboarding.md) | +| Change a setting, flag, or environment variable | [Consumer configuration](../docs/consumer-configuration.md) | +| Add, write, or publish a skill | [Marketplace catalog](../docs/marketplace.md), [Creating skills](../docs/creating-skills.md) | +| Understand what is core vs optional vs planned | [Five-layer claim matrix](../docs/five-layer-claim-matrix.md) | +| Work on the kit itself | [Contributing](../docs/CONTRIBUTING.md) | +| See the whole index | [Docs index](../docs/README.md) | + +Before filing anything, run: + +```bash +npx @dadado/agent-kit-cli doctor --json +``` + +`doctor` reports readiness problems and, with `--fix-safe`, repairs the safe +subset. Its output is the single most useful thing to paste into an issue. + +## Where to file what + +| Kind | Where | +|------|-------| +| Something is broken | Bug report issue | +| Something is missing | Feature request issue | +| "How do I…" / "is this supported?" | Open an issue; questions are in scope | +| Docs are wrong, stale, or unclear | Bug report issue, with the doc path | +| Security vulnerability | **Not an issue** — follow [SECURITY.md](SECURITY.md) | +| Conduct concern | [Code of Conduct](CODE_OF_CONDUCT.md) | +| Code, docs, or skill contribution | PR — see [Contributing](../docs/CONTRIBUTING.md) | + +## What makes a question answerable + +- CLI version (`npx @dadado/agent-kit-cli --version`) and the `version` field in + your `.cursor/agent-kit.json` +- OS, Node version, and IDE (Cursor / VS Code / Windsurf — note that VS Code and + Windsurf generators are partial by design) +- What you ran, what happened, what you expected +- `doctor --json` output, and the relevant terminal output as text rather than a + screenshot + +## Response expectations + +This project is maintained on a best-effort basis. There is no support contract +and no guaranteed response time. Issues with a clear reproduction and `doctor` +output are the ones that get resolved fastest. A closed issue is not a dismissal +— read the closing comment, and reopen if it did not actually cover your case. diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5bdb5a8..4e41187 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -18,6 +18,13 @@ jobs: steps: - name: Checkout uses: actions/checkout@v5 + with: + # Full history is required for git-range evidence checks (e.g. delivery + # reconciliation SHAs) and the private-origin allowlist pin work. Mirrors + # sync-public at line 98. The shallow-checkout default caused R1/R2 in + # close-bigfix-pty-monitor-residuals; see plan-monitor-close-public-ci-skip + # for the allowlist pin work it unblocks. + fetch-depth: 0 - name: Guard public tree (no tracked local-only Cursor paths) run: | @@ -44,26 +51,42 @@ jobs: - name: Lint run: pnpm lint + - name: Registry catalog parity + if: github.repository == 'agent-kit-startup/agent-kit-dev' + run: node scripts/build-registry.mjs && git diff --exit-code registry/registry.json + - name: Typecheck run: pnpm typecheck - name: Test run: pnpm test + # Private-origin allowlist (SoT: agent-kit-startup/agent-kit-dev). + # Fail-safe: forks / renames / second mirrors skip private-only steps. + # Adding another allowlisted step requires updating the exact-count pin in + # packages/cli/src/dashboard/ci-private-origin-allowlist.test.ts. + # Job-level `if` cannot read vars.PUBLIC_REPO_URL; that var redirects the + # sync target (push + gh --repo) below, never this guard. See ADR 2026-07-29. - name: Authority graph parity - if: github.repository != 'agent-kit-startup/agent-kit' + if: github.repository == 'agent-kit-startup/agent-kit-dev' run: pnpm evidence:authority-graph:check - name: Public-deny-link guard - if: github.repository != 'agent-kit-startup/agent-kit' - run: pnpm check:public-deny-links + if: github.repository == 'agent-kit-startup/agent-kit-dev' + run: | + pnpm check:public-deny-links + # Content denylist + allowlist dry-run (same guards as tag-gated sync-public). + # Catches inline denylist terms in synced docs before a v* tag. + node scripts/sync-public.mjs --dry-run - name: Evidence checks - if: github.repository != 'agent-kit-startup/agent-kit' + if: github.repository == 'agent-kit-startup/agent-kit-dev' run: | pnpm evidence:codebase-findings:check pnpm evidence:risk-hotspots:check pnpm evidence:knowledge-classification:check + pnpm landing:build + pnpm landing:build:check - name: Guard generated CLI dashboard is untracked run: | @@ -82,14 +105,17 @@ jobs: # Secret PUBLIC_REPO_TOKEN: fine-grained token with public repo Contents, # Pull requests, and merge (or auto-merge) permission. Repo must Allow auto-merge # when required checks exist. Opt out: PUBLIC_SYNC_AUTO_MERGE=false. - # Private-origin only: skip when this workflow runs on the public storefront - # slug (Path C mirrors ci.yml; public has no PUBLIC_REPO_TOKEN by design). + # Private-origin allowlist only (SoT: agent-kit-startup/agent-kit-dev). + # Path C mirrors ci.yml; public / forks have no PUBLIC_REPO_TOKEN by design. + # Adding another allowlisted site (this job `if` counts) requires updating the + # exact-count pin in packages/cli/src/dashboard/ci-private-origin-allowlist.test.ts. # The secrets context is not allowed in job-level `if`; the guard step # below fails loudly when the secret is unset on private tag/manual sync paths. + # vars.PUBLIC_REPO_URL redirects push + gh --repo (slug derived from URL); it cannot override this `if`. sync-public: needs: build if: >- - github.repository != 'agent-kit-startup/agent-kit' + github.repository == 'agent-kit-startup/agent-kit-dev' && (startsWith(github.ref, 'refs/tags/v') || (github.event_name == 'workflow_dispatch' && github.event.inputs.sync_public == 'true')) @@ -122,8 +148,8 @@ jobs: node scripts/sync-public.mjs env: GH_TOKEN: ${{ secrets.PUBLIC_REPO_TOKEN }} + # Slug for gh --repo is derived from this URL in sync-public.mjs (no hardcoded PUBLIC_REPO_SLUG). PUBLIC_REPO_URL: ${{ vars.PUBLIC_REPO_URL || 'https://github.com/agent-kit-startup/agent-kit.git' }} - PUBLIC_REPO_SLUG: agent-kit-startup/agent-kit PUBLIC_BRANCH: main # Default: auto-merge after create/update. Set repo/org var to "false" to opt out. PUBLIC_SYNC_AUTO_MERGE: ${{ vars.PUBLIC_SYNC_AUTO_MERGE || 'true' }} @@ -133,14 +159,15 @@ jobs: GIT_AUTHOR_EMAIL: sync@agent-kit.dev # ── Publish @dadado/agent-kit-cli to npm (tag only) ── - # Private-origin only: skip on the public storefront slug (same Path C - # mirror as sync-public; npm publish belongs on agent-kit-dev). + # Private-origin allowlist only (same SoT as sync-public; Path C mirror). + # Adding another allowlisted site (this job `if` counts) requires updating the + # exact-count pin in packages/cli/src/dashboard/ci-private-origin-allowlist.test.ts. # Secret NPM_TOKEN: the publish step skips gracefully if not configured # (secrets context is not allowed in job-level `if`). publish-npm: needs: build if: >- - github.repository != 'agent-kit-startup/agent-kit' + github.repository == 'agent-kit-startup/agent-kit-dev' && startsWith(github.ref, 'refs/tags/v') runs-on: ubuntu-latest permissions: diff --git a/.gitignore b/.gitignore index 9f786b4..a9d930d 100644 --- a/.gitignore +++ b/.gitignore @@ -63,9 +63,23 @@ coverage/ *credentials*.json *service-account*.json +# Local design sources (PSD/AI); production PNGs under assets/production/ are versioned +assets/design/ + +# Video masters and intermediate encodes. The three videos the landing actually +# ships are versioned inside the design mirror (landing-missionkit/remote/assets/, +# byte-identical); nothing in the repo references these copies. Keeping both would +# double ~11 MB of binaries. Decision: 2026-08-05_landing-external-design-source-of-record.md +assets/production/*.mp4 +assets/production/*.webm + # CLI publish copy of Mission Control (SoT: repo-root dashboard/; sync via scripts/sync-cli-dashboard.mjs) packages/cli/dashboard/ # Python bytecode (hooks) __pycache__/ *.pyc + +# Landing build output — derived from the Claude Design export by +# `pnpm landing:build`; never edit by hand. See docs/agentkit-landing.md. +.cursor/context/landing-missionkit/dist/ diff --git a/CHANGELOG.md b/CHANGELOG.md index d0822fc..b1fa784 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,153 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and ## [Unreleased] +## [5.0.0] - 2026-08-12 + +### Fixed + +- `agent-kit cursor-awareness --check`: walk up from `--cwd` to find `docs/cursor-native-audit.md` (fixes false Missing inventory when the shell is under `packages/cli` or another nested path); consumer/missing-docs path returns an actionable `--cwd` hint. Native-audit Action table: A5 Done (root `AGENTS.md` present), A4 Partial with dual-lane next step, A7 Open with scoped multi-IDE next step; A6 unchanged. +- Pre-v5.0.0 evidence gate: R14-pair mid-batch monitors `plan-monitor-fix-staging-ci-and-queue-end-product-residuals.md` and `plan-monitor-close-ship-5.0-npm-npx-install-residuals.md` with `_index.md` Audits rows and regenerate `docs/evidence/knowledge-classification.json` so `pnpm evidence:knowledge-classification:check` is green again (Still open A from ship-5.0 clean pre-git-prod gate). +- Pre-v5.0.0 tag gate: re-verify lint/typecheck/tests/deny-links/evidence/landing/Path-C-pack on staging `71dbbae`; persist matrix at `docs/evidence/runtime/ship-5.0-pre-tag-green-gate-2026-08-12.md`; R15 Closed-by A/B on `plan-monitor-ship-5.0-clean-pre-git-prod-gate.md`. No tag; no `/git-prod`. +- Landing closeout: factory CI runs `pnpm landing:build` + `landing:build:check`; Align Closed-by pointer notes public LICENSE still pending `/git-prod`; R15 Closed-by appends on the five queue-end monitors (Design SoR re-absorb + clipboard success-path evidence still owed before next sync). +- Mission Control broadcast: live `missionkit.io/mc/open.html` matches repo harden (R1 verified by sha256); `start-broadcast` degrades to LAN/token print when share encode rejects non-RFC1918 primary LAN (Tailscale 100.64/10); HTTP-level serve auth matrix covers `/open` + `/open.html` without token vs token-gated data. +- Mission Control preferred-browser residuals: CLI uses `resolveContextConfigPath`; OS-default and fallback opens detect failure before claiming success; reject win32/`cmd` metacharacters `" % ^ ' = , +`; expand hermetic which/win32 spawnSync tests; ADR trust-boundary + failure-honesty updated. +- Dogfood L0 nits: `/run-plan-all` per-plan worker template states orchestrator already skimmed Unprocessed; drop inert PO dogfood `read_scope` paths; promote `/continue-plan` dogfood preflight to its own hard stop (runs regardless of `externalPlanReview.preflight`); overlay hash set +2. +- Dogfood Unprocessed parser: accept markdown table rows and numbered list items; terminate the Unprocessed section on any `Processed Files` heading (stops mixed H2/H3 leaks). +- Knowledge-classification CLI: default to the handoff fixture and refuse baking live `.cursor/HANDOFF.md` into the tracked ledger unless `--allow-live-handoff` (prevents repeated Evidence-check red after bare `node …mjs` regenerations). +- Staging Evidence checks: regenerate `docs/evidence/knowledge-classification.json` via `pnpm evidence:knowledge-classification` (handoff fixture) and R14-pair five queue-end plan monitors plus `_index.md` Audits rows so `evidence:knowledge-classification:check` is green again. +- missionkit.io install / prompt copy CTAs: await clipboard writes, show honest failure UI (not optimistic ✓), and fall back to `execCommand` when the Clipboard API rejects; decorative "Copy plan path" / "Copy /git-staging" mock buttons marked disabled. Built via `landing:build`, deployed to Hostinger; Design SoR re-absorb notes in `UPSTREAM-DESIGN-FIX-PROMPT.md`. +- Mission Control preferred-browser residuals (A–H): validate `preferredBrowser` as an app/binary name (reject path separators and shell metacharacters); attach spawn `error` handlers and fall back once to the OS default opener with honest starter messages; align `start-broadcast.mjs` preference root with `resolveSnapshotRepoRoot`; share `normalizePreferredBrowser` / `OS_DEFAULT_TOKENS` with the CLI; document per-platform values and factory/CLI-only `/dashboard-broadcast` slash; expand hermetic tests (`win32`, `spawn-failed`, `invalid-url`, fallback). Pack-gate hygiene **I** remains on broadcast F8. ADR `2026-08-11_mission-control-preferred-browser.md` +- Dogfood Broad Intake / sessionStart now accept consumer `## Unprocessed Files` as well as factory `###` (parser ends on same-or-higher heading); `/dogfood` pins H3 on new consumer writes; ADR decision 2 append-only correction; bucket-count prose uses table-as-SoT (no hardcoded numeral); `/run-plan-all` orchestrator owns Unprocessed preflight (workers skip re-recite); `docs/external-plan-review.md` Broad Intake row names the dogfood bucket +- Cache lock release fail-closes when owner metadata is missing, unreadable, or mismatched (no longer deletes a successor's lock during the mkdir → owner publish window); owner.json is published via temp file + rename; install `RootRefusedError` matches update (`process.exitCode = 1` + return) so cleanup/finally stay reachable +- Crew Monitor structural pin (residuals E/F from `close-crew-glyph-avatar-still-open`): `expectBadgeIsRowSiblingBeforeActor` now whole-template-counts `${chipHtml}` / chip classes to zero and requires exactly one `${badgeHtml}`, closing the half-locked sibling gap left by the retired feed-label helper; empty-template assertion names the `crewMonitorRowRenderTemplate` marker for clearer diagnostics +- `npm pack --dry-run --json` now receives clean JSON on stdout because the CLI dashboard prepack sync message is written to stderr. +- Close multi-workspace install isolation residuals D2/L1/L2/L4/P2/P3: mode-independent root guard for `--yes`/`$HOME`/`/`/no-git+no-manifest with `--force-root` escape hatch; cache lock ownership token (PID/UUID) with mtime refresh and ownership check before stale reclaim/release; hold cache lock across install/update/add/diff/contribute registry copy; refused `update` exits non-zero; `dashboard-broadcast.md` derives `MC_PORT` from repo root; `install.md` Port B notes chat-install Ask as the `confirmProjectRoot` equivalent. D3/L3/P1 were already fixed at HEAD (see `.cursor/memory/plan-monitor-multi-workspace-install-isolation.md` Closed-by section). +- Close vscode-first-install dashboard onboard residuals C/E/K/J-vscode: skip-if-exists guard for `.vscode/settings.json`, `.github/copilot-instructions.md`, and `.vscode/security-review.agent.md`; generated artifacts registered in `protectedPaths` and `PersonalizationResult.items`; `generator/vscode.test.ts` pins the guard; `docs/getting-started.md` splits `npx -y` (suppresses `npx`'s own confirmation) from CLI `--yes` / `AGENT_KIT_YES=1` (skips project-root prompt); `terminal.test.ts` pins `isNonInteractive` for CI, `AGENT_KIT_YES`, and `stdin.isTTY`. A/B fixed at HEAD; D2 owned by multi-workspace plan; F/G/H/I out of scope. +- Close BIGFIX PTY monitor still-open residuals R1–R7: `build` job checkout now uses `fetch-depth: 0`; risk-hotspot scorer no longer depends on a shallow-unresolvable git range; knowledge-classification `--check` ignores commit provenance fields; ledger census scopes to tracked paths; HANDOFF Gaps updated to honest red-build voice; behavioral tests added for `wait_for_pty_progress` banner-baseline and deadline logic; liveness probe for cap/warn concurrent-arm refusal documented as backlog +- Config persona Inherit default sends null to clear a stored mode override (guards merge delete path) +- Overlay update: end-to-end ledger-absent evidence that known-shipped files refresh while customized peers stay preserved (`docs/evidence/overlay-update-preserve-refresh.md`) +- SECURITY.md: private vulnerability reporting preferred first when enabled; public no-detail issue is fallback; PVR enable remains repo-admin HITL +- CLI welcome helmet outline uses `HELMET_OUTLINE` via trueColor; test asserts rendered ANSI + +- CLI welcome grouped help: await citty `Resolvable` in `help-groups.ts` and accept generic `CommandDef` so `pnpm typecheck` and `packages/cli` DTS build pass after the bare-invoke welcome work +- Regenerated `docs/evidence/knowledge-classification.json` and tracked three queue-end plan monitors with paired `_index.md` Audits rows so `evidence:knowledge-classification:check` is green +- Ship `packages/cli/LICENSE` in the published package `files` list so npm tarballs include PolyForm Noncommercial text +- Landing Mission Control demo (`landing-missionkit/remote/mc`): refresh `#mc-mock-data` Current Mission agent to `Tech Lead` and feed labels to design-v2 wire tokens (`Eng` / `SQ`). The tracked snapshot does **not** ship product `CREW_ACTOR_MASK` / `crewActorRole`, so the iframe still renders wire tokens verbatim (not long display masks). Correcting the earlier false claim that fixtures were display-masked; demo lexicon restoration waits on product-snapshot regen or Design export → `landing:sync` (do not hand-edit `remote/` as SoT). See `docs/agentkit-landing.md` +- Evidence anti-overwrite: `generate-codebase-findings.mjs --write` preserves on-disk reviewed coverage batches; non-mutating findings/hotspots checks ignore HEAD-volatile `generatedAt` / working-tree digest fields +- Backfilled 11 registry skill hashes into `KNOWN_SHIPPED_OVERLAY_HASHES` and pin registry SKILL.md coverage in Vitest +- Capability inventory: regenerate launch-announcement anchors, restore SHA-verified counts, document factory-only `/public-issue-triage` counting policy +- Capability inventory: re-verify README positioning anchors against HEAD (Anchor L# column) and point verified-against SHA at `7fdb03c` +- Config tab: persona mode "Inherit default" skips empty mode overrides; document `/api/config` token exemption in write verification matrix +- SECURITY.md: list working private maintainer channel first while public PVR remains disabled +- CLI welcome nits: `guard` meta `(shell, prompt)`, column-aligned hints, broader CI env detect, `hasCliSubcommand` tests, helmet outline uses light text color +- `KNOWN_SHIPPED_OVERLAY_HASHES` was missing `.cursor/skills/core/docs-repo/SKILL.md`, so an unedited consumer copy of that skill was misread as customized and never refreshed by `agent-kit update`. Ledger refreshed to 75 entries (prior hashes retained) +- Overlay anti-overwrite: backfilled current hash for `.cursor/commands/dogfood.md` in `KNOWN_SHIPPED_OVERLAY_HASHES` and added a Vitest pin that every L0 overlay artifact body is present in the ledger (closes residual L3 drift that permanently stalled unedited consumer refreshes) +- Evidence anti-overwrite (AUDIT-001 follow-through): `generate-codebase-findings.mjs` and `score-codebase-risk-surface.mjs` refuse to rewrite reviewed JSON unless `--write` / `--fix`; package.json generators pass `--write`; pin test asserts every `evidence:*:check` script stays non-mutating +- Risk-hotspot scorer additionally excludes `.cursor/plans/` from the product corpus so the manifest is independent of gitignored local backlog files; `docs/evidence/codebase-risk-hotspots.json` regenerated +- Closed staging-evidence R14 still-open residuals A–G: verified R1 allowlist pin reconciliation, corrected `close-staging-evidence-checks-r14-index.md` Closed-by B/C honesty, added HANDOFF Gaps voice / merge-gate ownership / R15 provenance notes to `close-staging-evidence-checks-r14-a-d-residuals.md`, and qualified the `_index.md` `evidence-checks-green` tag to `evidence-checks-green-at-98e0cba` +- Closed MC health healthcenter R1–R3 still-open residuals: removed the fallback-specific negative pin at `packages/cli/src/dashboard/plugin-ux-validation.test.ts` (then believed covered by the broader scan), bounded the remaining `HEALTH_SEVERITY_CHROME` pin to the object literal scope so unrelated `token:` strings below the literal do not fail the severity-chrome test, and noted R14 batch monitor hygiene (later corrected as already closed by `a9498ca`) +- Closed landing Mission Control session production-shot residuals B-H: documented the canonical `missionkit.io` cutover and five-asset legacy rollback bundle, differentiated Mission Control tab copy, refreshed inventory wording, aligned the 20x20 logo spec, and exposed row/image labels without a frame `role="img"` subtree trap +- Landing legacy rollback truth (R1–R5): removed the false self-contained claim for blob `d0e43278cda5`; documented that no production-shot single-file artifact exists (CSS-mockup blob `11197c8db22f` @ `611c232` is the last pre-shot self-contained file); switched both legacy HTML files to root-relative `/dashboard/` + `/assets/production/` paths; dropped the stale worktree byte-identity assertion; refreshed source-monitor Audits tags + +- Mission Control Healthcenter residuals R1–R3 (post-PR #632): drop unread `token` from all `HEALTH_SEVERITY_CHROME` severities and fallback; correct false comment that tied `token` to `[data-sev]` CSS; remove redundant E3 pin and assert no chrome `token` field; document SoT `dashboard/dashboard.html` (never gitignored `packages/cli/dashboard/` mirror) in plan template and plan-routine +- Staging-evidence R14 residuals A/C (post-PR #626/#627): R15 Closed-by cites on public-ci-skip (R1/R4) and landing-v1-v6 (B) monitors for `743de13` / PR #626; R14-pair `plan-monitor-close-staging-evidence-checks-r14-index.md` with its `_index.md` Audits row and regenerate knowledge-classification ledger (B/D note-only; merge-gate enforcement deferred to public-ci-skip R8) +- Crew Monitor residuals R1–R4 (post-PR #631): replace inert whole-file avatar↔chip proximity regex with render-block structural pin (`feedSegSpans[0] + chipHtml` + no `${chipHtml}` between avatar and feed-label); regenerate `docs/evidence/knowledge-classification.json` and R14-pair the close monitor with its `_index.md` Audits row (PR #637/#638) +- Landing v1–v6 monitor residuals C/D/E: split runtime-captured counts from source-of-record values in `docs/evidence/runtime/landing-a11y-a-e-2026-08-01/` (new `sot-counts.txt` with "not served" header; `live-counts.txt` reduced to runtime capture); qualify README follow-on as source-of-record not yet served and mark pinned WP URL deprecated; reword wpautop guard comment in landing SoT files to avoid literal `
` (grep-clean); refresh `.cursor/project-context.md` product version and npm CLI lane `4.8.4` → `4.8.9`. A delegated to deploy residuals plan; B to R14 companion. +- Mission Control Healthcenter residuals (E1–E3): widen git Autofix to `git init && git commit --allow-empty` (zero-commit repos); unify memory path CTA to `Copy path` with README; drop unused `token` on aggregate `HEALTH_SEVERITY_CHROME.error` +- Mission Control Healthcenter residuals (C/E/F/G/I): prune unreachable per-check `error` chrome; remap Autofix for `handoff`/`git`/`memory`; drop dead `healthCheckKeydown` / `showHealthInfo` / `.health-message`; scope seven-check test to `HEALTH_CHECK_META` +- Landing static SoT: lighten `--text-muted` to `#7f93a8` (WCAG AA on card/gradient); document footer CTA cluster; rewrite `live-counts.txt` as real newlines; note dead `br` guard and unused purple contrast trap + +### Added + +- Mission Control broadcast Share URL mask: `dashboard:broadcast` / `agent-kit dashboard-broadcast` print a cosmetic Mission Kit (or BYO) share link (`https://missionkit.io/mc/open.html#v1.…` by default). The Share URL embeds the live token (same secret handling as the raw token); soft TTL is advisory. Fragment stays client-side; not a WAN relay. Resolver rejects non-private targets; BYO base must be HTTPS (loopback http allowed for local preview). Env: `MISSION_CONTROL_SHARE_BASE`, `MISSION_CONTROL_SHARE_TTL_SEC` (`0` = never), `MISSION_CONTROL_SHARE_SHOW_LAN`. ADR `2026-08-11_mission-control-broadcast-url-mask.md` +- Mission Control broadcast share residuals: default share base uses live `…/mc/open.html`; pack gate asserts `dashboard/lib/**` + `open.html`; `--no-open` help matches Share-primary print; auth-gate tests cover `/open` + `/open.html` exemption matrix. +- Mission Control preferred browser: shared `dashboard/lib/open-browser.mjs` opens at most one OS browser (config `missionControl.preferredBrowser`, env `MISSION_CONTROL_PREFERRED_BROWSER`, CLI `--browser`); `--no-open` / `MISSION_CONTROL_NO_OPEN` still skip. Slash `/dashboard` stays IDE MCP only; `/dashboard-broadcast` is one surface (OS preferred or IDE verify). ADR `2026-08-11_mission-control-preferred-browser.md` +- Broad Intake **Unprocessed dogfood** bucket (factory `dogfood/README.md` or consumer `.cursor/dogfood/README.md` `##` or `### Unprocessed Files`) on `/start-project`, `/backlog-add`, and Write residuals; advisory preflight on `/continue-plan`, `/run-plan`, and `/run-plan-all`. Same triage labels; never auto-analyze. ADR `2026-08-11_dogfood-unprocessed-broad-intake-bucket.md` +- Community health C–F residuals: `scripts/check-public-deny-links.mjs` now scans `.github/**/*.{yml,yaml}` (issue-template forms, CI) in addition to markdown, with three new tests (9/9 pass). ADR `2026-08-05_community-health-files-live-under-github-dir` point 5 corrected to "adapted in Enforcement section only". `docs/CONTRIBUTING.md` Standards bullet names both repos explicitly (`main` for public, `staging` for factory). `.cursor/memory/_index.md` trailing newline added. +- Design system pointer doc (`docs/design-system.md`): Claude Design project id (`4451a0e9-5258-45cd-91f7-a837bdcbde81`), upstream/downstream surface map, code-wins-on-divergence rule, `--text-muted` token divergence note. Crosslink added to `docs/agentkit-landing.md`. Comment added in `dashboard/dashboard.html` beside `--text-muted: #6d8094` citing the landing AA exception (`#7f93a8`). Closes residuals A+D from transport monitor `plan-monitor-design-system-transport-claude-design.md`; B/C/E/F noted as operator-decision or superseded +- CLI bare-invoke welcome: branded Mission Kit helmet ASCII, `agent-kit` / `@dadado/agent-kit-cli` version line, print-and-exit utility hints (`--help`, `doctor`, `status`, `dashboard`, `init`); `NO_COLOR` / CI / non-TTY plain fallback; grouped root `--help` (SETUP / MISSION / DASHBOARD / INTEGRITY). Does not reuse run-plan persona banners. Dual-name ADR `2026-08-06_mission-kit-vs-agent-kit-naming` +- Factory-only `/public-issue-triage` slash command for maintainers: list, classify, comment, label, and close incoming issues on `agent-kit-startup/agent-kit` with HITL gates; omitted from L0 install and excluded from public-sync; bridges optionally to dogfood, memory WRITE, or `/backlog-add`. ADR `2026-08-05_factory-only-public-issue-triage-command` +- Dogfood inbox notes (2026-08-05): external design source-of-record gaps when a deploy artifact moves off-repo; `/run-plan-all` queue orchestration pitfalls (inferred park written to HANDOFF, refused-command retry, stale audit-session pile) +- Plan monitors (post-hoc): `design-system-transport-claude-design`, `github-community-health-profile` (paired `_index.md` Audits rows) +- Crew Monitor real-time rows for two activity classes the operator previously could not see at all. `subagent` rows track Task worker lifecycle (`running` / `done` / `failed`) read from the worker transcript's terminal record under `~/.cursor/projects//agent-transcripts//subagents/`, labelled with the dispatched worker type and the to-do id lifted from the dispatch prompt. `plan_review` rows point at each recent `plan-monitor-*.md` and say whether it is still awaiting triage. Both kinds are additive to `MONITOR_ACTIVITY_KINDS`; both are bounded by the same recency / size / count discipline as the existing prompt and report collectors, and both degrade to an empty list rather than an error state. Flight Log and the attention inbox keep sole ownership of triage - a `plan_review` row is a pointer and never marks anything reviewed +- Crew Monitor row density toggle (compact / comfortable) in the card header, stored under the namespaced `agent-kit:monitor-density` key and restored before first paint. With no stored preference the mode is auto-picked from viewport width (comfortable at >=900px). Comfortable rows wrap the label to at most two lines instead of hard-truncating a token mid-word; the `labelFull` title tooltip stays required in both modes, because the display label is still capped upstream +- GitHub community health profile: `.github/CODE_OF_CONDUCT.md` (Contributor Covenant 2.1), `.github/SECURITY.md` (private disclosure via GitHub private vulnerability reporting, supported versions, documented Mission Control loopback / sandbox-disabled posture as out-of-scope-by-design), `.github/SUPPORT.md` (question routing, `doctor --json` first), `.github/ISSUE_TEMPLATE/` (bug + feature forms, `config.yml` with blank issues disabled and contact links), and `.github/PULL_REQUEST_TEMPLATE.md` mirroring the `docs/CONTRIBUTING.md` quality gate. All files sit under `.github/` so `scripts/public-sync.manifest` covers them via the existing `.github/**` include - no allowlist delta. `docs/CONTRIBUTING.md` stays the CONTRIBUTING source of truth (GitHub discovers `docs/`), and README Contribute plus the docs index carry thin cross-links +- Cursor Marketplace packaging: `.cursor-plugin/plugin.json` now declares explicit component paths (`rules`, `skills`, `agents`, `commands`, `hooks`) plus `logo`, `homepage`, and an object-shaped `author`. Without those entries the plugin would have listed with zero components, since Cursor's default discovery reads `rules/`/`skills/`/`agents/`/`commands/` at the repo root and Agent Kit keeps everything under `.cursor/`. `skills` points at `.cursor/skills/core` — one level deeper than the obvious path, because discovery only matches direct children holding a `SKILL.md` — which also keeps stack skills on `agent-kit add` +- `name` + `description` frontmatter on all 27 files in `.cursor/commands/`, required by the Marketplace submission checklist. `name` matches the existing filename slug, so no slash command was renamed +- Landing build pipeline for missionkit.io: `landing:sync` (Claude Design zip export → `.cursor/context/landing-missionkit/remote/`, React vendored and SRI-verified against the hashes the design runtime declares), `landing:build` / `landing:build:check` (derives the asset list from the canvas; drops unreferenced stylesheets; self-hosts React; injects a static ``/OG head for crawlers), and `landing:serve` (loopback stage with Range support and `no-store`, for hands-on testing before deploy). Self-containment is proven by an offline headless render, not asserted: DOM byte-identical with all external DNS blocked, zero unresolved bindings +- Landing deployed to `missionkit.io` via the design-runtime bundle: favicon (`assets/logo.svg`), Open Graph image (`assets/hero-astronaut.png`), full Twitter Card with `summary_large_image`, and `build-landing.mjs` crawler `<head>` inject extended to mirror icon, `og:image`, `twitter:image/title/description` with absolutized URLs. P1-1 (YouTube-on-load before modal open) fixed upstream: the demo modal iframe now uses a lazy `demoSrc` binding (empty at rest, set only in `openDemo`, cleared in `closeDemo`), so first-paint issues zero external requests. `docs/agentkit-landing.md` rewritten for the new pipeline; `check:landing-body-equality` guard retired; legacy `.cursor/context/landing-agentkit/` files kept for rollback reference only +- Public-sync compatibility evidence for the README and public maintainer guides: `docs/evidence/runtime/public-sync-readme-residuals-2026-08-11.md` records the allowlist dry-run, guard tests, and deny-link scan + +- Multi-workspace install isolation: CLI root-confirm guard (`confirmProjectRoot`) on both `install` and `update` commands; directory-lock (`acquireCacheLock`) serializes concurrent `~/.cache/agent-kit/registry/` refreshes; ADR surfaces matrix (L0 per-project, cache shared, MC per-workspace port); `/dashboard-broadcast` kill guidance aligned with never-kill-foreign-workspace +- IDE-agnostic CLI install resilience: `--yes` flag and non-interactive terminal detection (`isNonInteractive`) for CI, VS Code output panels, and piped stdin; actionable error classification for EPERM, exit 255, registry 403, and network failures with recovery steps +- Dashboard first-failure UX: structured recovery message when `dashboard/start.mjs` is absent in L0-only consumer trees, naming four resolution paths (upgrade CLI, env var, sibling, direct script) +- VS Code onboard path: `generateVSCodeArtifacts` wired into personalization flow (`.vscode/settings.json`, `.github/copilot-instructions.md`, optional `.vscode/security-review.agent.md`); IDE-agnostic readiness docs with CLI equivalents table and slash-less onboard checklist +- Registry clone error messaging: auth/access vs network vs generic failures with actionable recovery in `resolve.ts` +- Dogfood bridge: consumer install-fallback and dashboard-runtime-block notes (2026-08-02); memory errors for npm-cache EPERM/exit-255 and dashboard-host-missing/registry-403 +- Mission Control landing production shots: four PNGs under `assets/production/` (Current mission, Checklist, Crew Monitor, Flight Log); design sources stay local under gitignored `assets/design/` +- Mid-batch plan monitors (6) for residual closeouts (crew-monitor R1–R3, docs-indicative A–L, mc-chrome-icon A–F, mc-health R1–R3, public-ci-skip R7–R10, staging-evidence R14 A–D) with `_index.md` Audits rows; knowledge-classification ledger regenerated +- Dogfood note: `/git-prod` should prove tag CI green on the close-release commit before creating or pushing an annotated `v*` tag +- CI Registry catalog parity: `node scripts/build-registry.mjs && git diff --exit-code registry/registry.json` so SKILL frontmatter ↔ `registry/registry.json` drift fails the build +- `docs/DEVELOPMENT.md`: factory topology, local CLI loops, and public-sync awareness for maintainers (root README stays consumer storefront) +- Onboard domain-skills scaffold: after essentials are ready, `/agent-kit-onboard` offers a HITL `Scaffold domain skills` / `Defer` / `Skip` gate before finish-setup / `/start-project` CTAs, reusing install-time personalization/doctor evidence and recording the outcome in `onboarding.domainSkills` (shipped as squash `d00fb50`; public `agent-kit-startup/agent-kit#36` closed later via residuals with cross-repo evidence comment) +- Queue-end plan monitors (7) under `.cursor/memory/` with triage headings; `_index.md` Audits rows R14-paired (including crew-monitor + docs-indicative); knowledge-classification ledger regenerated +- Evidence checks merge-gate policy: ADR `2026-08-01_evidence-checks-merge-gate` (no silent continue-through-red; HANDOFF Gaps honesty) +- Docs indicative; delivery truth: always-apply read-time section in `docs-professional-standard`, ADR `2026-08-01_docs-indicative-delivery-truth`, tightened external-review prompt/monitor templates (evidence-backed findings; no filler) +- CI private-origin allowlist pin: Vitest asserts `github.repository == 'agent-kit-startup/agent-kit-dev'` remains in `.github/workflows/ci.yml` (Path C remirror guard) +- Static landing deploy artifact `.cursor/context/landing-agentkit/index.html` (full document wrapper over redesign SoT) +- Landing Product proof: four alternating MC rows (L/R/L/R) with MC product header chrome (logo + Mission Control + workspace) replacing browser-chrome mockup; production PNGs from `assets/production/`; a11y labels per row +- Landing workflow copy: seven narrative sections in `COPY.md` (onboarding, orchestration, HITL/triage, DevOps, settings/update, dogfood, MC tabs) integrated from `COPY-WORKFLOW-DRAFT.md` after HITL rewrite (version 4.8.9 pin, no autonomous pitch) +- Landing DESIGN-SYSTEM: mockup chrome rewritten from browser window (traffic lights + URL pill) to MC product header; framing invariant updated + +### Changed + +- Public root `README.md` storefront rewrite: Mission Kit product voice for strangers on GitHub (PolyForm Noncommercial / source-available / `sales@missionkit.io`); install and CLI keep real Agent Kit identifiers without ADR paths, private memory links, or dual-name legal essays. Maintainer naming table moved to `docs/DEVELOPMENT.md`. +- missionkit.io license copy cutover: Claude Design export synced (`landing:sync` / `landing:build`), Hostinger deploy of `dist/`, and as-served HTML verified with PolyForm Noncommercial / source-available / `sales@missionkit.io` (zero unqualified open-source claims). `docs/agentkit-landing.md` SEO section flipped from as-served-vs-target to live PolyForm wording. Public-sync allowlist/denylist unchanged (deny-link + dry-run guards pass). Public GitHub license label remains advisory until `/git-prod` HITL. +- Align Mission Kit public-compliance residuals (A–F): document that PR #698 already staged `783ca90`; flip the Crew-lexicon as-served table in `docs/agentkit-landing.md` to Design-export labels (`Engineering Manager` / `Squad ·`); disclose that the same Design sync refreshed `remote/mc/*` lexicon incidental to license copy (product SoT / display-mask ownership unchanged); record deployed `sha256` evidence; add an unqualified open-source wording assertion to `landing:build:check`. Public mirror `LICENSE` promote stays operator `/git-prod` HITL. +- Onboard domain-skills scaffold residuals A–J: closed public `agent-kit-startup/agent-kit#36` with evidence citing `d00fb50` (cross-repo `Closes` form documented in PR template + `docs/CONTRIBUTING.md`); documented instruction-only scaffold + `.cursor/skills/domain/` one-way category; added `## Relevant skills` to project-context generator and factory `.cursor/project-context.md`; HITL fallback free-text + gates table; moved domain-skills command pin to `lifecycle/l0.test.ts`; dogfood Processed provenance honesty; allowlist pin verified at 6 (CI green on staging). **Squash honesty:** original 4-phase plan landed as single squash `d00fb50` (plan file gitignored), so per-tick verdicts are reconstructed from that commit rather than per-phase SHAs. +- Landing v1–v6 CDE still-open residuals (R1–R7): regenerated evidence ledger for green Knowledge checks; corrected stale "not yet live" SoT/README prose against WP-deprecated + `agent.startupkit.com.br` 301→missionkit.io delivery truth; documented `index.html` drift / retired byte-identity (no redeploy); R14/R7 process notes; append-only Closed-by on `plan-monitor-close-landing-v1-v6-monitor-residuals-cde.md` +- Ship-5.0 npm/npx honesty residuals: go/no-go record restated as **NO-GO** until CI-green-at-tagged-SHA + gate sign-off; Release Latest / storefront rows marked POST-PUBLISH; cite drift fixed; `docs/npm-publish-checklist.md` distinguishes tree **5.0.0** from registry **4.8.9**; pack evidence at `docs/evidence/npm-pack-5.0.0-2026-08-11/npm-pack-5.0.0.json` (stdout JSON parseable after prepack stderr fix). Blank-folder `npx @5.0` dogfood remains blocked until publish +- Plan audit residuals termination: `/plan-review-triage` and continuous `/run-plan` / `/run-plan-all` paths prefer **Ack and stop** or **Fix nits only** when Still open is nits/process-only or closeout depth is already capped (max depth 1 per theme family); Write residuals must not mint unbounded `close-*` conveyors (ADR `2026-08-11_plan-audit-residuals-termination.md`). Throughput operator knobs documented under [external plan review](docs/external-plan-review.md#throughput-vs-coverage-operator-knobs). Unprocessed dogfood Broad Intake visibility is ADR `2026-08-11_dogfood-unprocessed-broad-intake-bucket.md` (not this process policy). +- Public vs dev README R2–R10 still-open residuals: restamp public-sync evidence `Source SHA` to green staging `84dbaa3` (342 allowlisted files); run `sync-public --dry-run` content denylist in the normal private CI deny-link step (pin count unchanged at 6); document private-filename content invariant beside the paid-spec path exclusion in the sync manifest; append-only A/D/F closeout corrections and a single S1 owner pointer (`close-public-ci-skip-r7-r10-still-open.plan.md`); CHANGELOG names the `DEVELOPMENT.md` private paid-spec filename generalization that unblocked denylist sync since `c41efde` (`8705144`); optional capability-inventory line-number assertion deferred (pattern scan only). Shared R14 ledger ownership unchanged. +- Deploy-agent startupkit post-merge residuals S5-S8: append-only R4 shared-owner basename correction (`plan-monitor-` prefix), historical S1-S4 honesty vs merged `fcf1de0`, allowlist counter-maintenance warnings on `sync-public` and `publish-npm` (count pin unchanged at 6), removed orphan `scripts/check-landing-body-equality.mjs`, and aligned DESIGN-SYSTEM deprecated Target line with the Current 301. Shared R14 Evidence ownership unchanged. +- Closed MC health healthcenter N2/N3 still-open residuals: restored a bounded `token:` pin on the `healthSeverityChrome` fallback return object and a positive `const HEALTH_SEVERITY_CHROME = { … };` shape anchor in `plugin-ux-validation.test.ts` (literal-scoped pin kept; unbounded regex not restored); append-only R15 corrections on the source monitor for the lost fallback coverage claim and the stale R14 hygiene note (`a9498ca` predecessor); acceptance language asserts suite success and exit 0 rather than a hard-coded test count. N1 batch ledger ownership unchanged. +- VS Code first-install residuals closeout metadata (N2/N3): source monitor heading normalized to `## Closed by residuals plan (C, E, K, J-vscode)`; acceptance wording aligned to shipped skip-if-exists (no merge path). N1 ledger ownership unchanged. +- R14 queue-end still-open monitors: durable triage / Residuals headings plus N1 Closed-by appends staged add-by-name with regenerated `docs/evidence/knowledge-classification.json` (`pnpm evidence:knowledge-classification:check` green locally). Unblocks Evidence gate for sibling residual lanes; does not close product Still open on those themes. +- Mission Control chrome citation R3/R4/R5 still-open closeout: append-only re-cite of parent `271d4d2`/PR #504 for mixed-surface lint evidence; Evidence-gate Validation and ledger-regen policy corrections; R15 bottom-up supersession readability ADR; knowledge-classification regenerated for edited monitors +- Mission Control chrome citation residuals R1-R5 closed through append-only monitor corrections: lint evidence reclassified as already recorded, assertion-message pins stabilized, shared ledger and HANDOFF Gaps ownership documented, and the supersession pointer aligned. +- Public sync slug resolution: `scripts/sync-public.mjs` warns on stderr whenever `PUBLIC_REPO_SLUG` diverges from a slug derivable from `--url`, the configured `public` remote, or `PUBLIC_REPO_URL` (not only when the URL env var wins); invalid `owner/repo` shape exits 1; `--self-test-slug` covers precedence. Docs updated in `docs/public-launch.md` and `docs/repository-boundaries.md` +- `docs/design-system.md`: drop markdown links into `.cursor/memory/` (public-deny-link guard) and remove a denylisted private client path from the upstream surface table so CI / public-sync stay green +- `seedManagedHashLedger` documents that the walk includes user-added non-kit basenames under overlay prefixes (harmless while those names stay outside the L0/pack/skill apply set) +- Docs/copy alignment to missionkit.io **Mission Kit 5** positioning under dual-name contract (ADR `2026-08-06_mission-kit-vs-agent-kit-naming`): README hero/tagline, consumer docs index/getting-started/CONTRIBUTING/claim matrix/github-about Website, `install.md` intro, public launch announcement, `project-context.md` version lane `5.0.0`, capability-inventory positioning rows; legacy `agent.startupkit.com.br` / `landing-agentkit` qualified as redirect/rollback-only. Landing canvas under `landing-missionkit/remote/` untouched. +- Consumer and maintainer documentation alignment: the README Mission Control pointer promises production constraints only; getting-started restores copy-only paste destinations, removes the duplicated maintainer three-way loop in favor of a pointer to `docs/DEVELOPMENT.md`, and keeps the consumer guide consumer-focused. `docs/DEVELOPMENT.md` describes private exclusions generically (no denylist-triggering private paid-spec filename) so private→public sync dry-run stays green after `c41efde`. +- Crew role mask SoT sync (plan `crew-role-mask-sot-sync`): glossary ADR display-mask contract updated to wire-short / display-long (`CREW_ACTOR_MASK` + `crewActorEngRole`); Eng collision withdrawn at display; team-member framework core-slot lexicon aligned (Tech Lead / DevOps / Project Manager / Developer / Product Owner); landing Mission Control fixture JSON uses wire tokens (`Eng` / `SQ`) and Current Mission agent `Tech Lead`. Landing iframe `#mc-mock-data` lexicon refreshed in residuals Phase 3 (was still Engineering Manager / Squad). Product masks unchanged (already shipped PR #662) +- Crew Monitor row redesign via Claude Design brief (plan `crew-monitor-design-brief-claude-design`): tinted initials badge returns as the kind + identity cue; separate kind glyph removed; compaction hides whole fields at breakpoints instead of mid-token ellipsis; `Eng` display split from `kind` (delivery → DevOps); plan chip has no max-width cap. Three porting deltas vs the mirrored design recorded in `.cursor/context/mission-control-design/remote/v1/ACCEPTANCE.md`. Glossary ADR amended; Mission Control visual SoR stays the repo (one-off brief ADR `2026-08-05_mission-control-design-one-off-brief`) +- Crew Monitor rows are compact and glyph-first. The kind glyph moved to the start of the row (it was mid-label, between actor and verb) and the avatar/initials box was removed entirely. The avatar shipped in #631 and #637-#639 and its removal is deliberate: initials were a lossier copy of the actor string two segments later, and the 18px box pushed the kind glyph off the left edge an operator scans down. Long profession display masks were replaced with the operator's short lexicon (`Engineering Manager` -> `Eng`, `Squad` -> `SQ`, `Scrum Master - awaiting gate` -> `PM - awaiting gate`, and so on), which retires the previously accepted actor-mask-vs-noshrink tradeoff instead of re-deciding it. (Interim compact-labels pass; design-v2 above restored long display masks while keeping short wire tokens.) Kind ids, `#hero-activity`, and the `.monitor-row*` class prefix are unchanged. ADR: `2026-07-27_crew-monitor-vs-plan-monitor-glossary` +- Delivery rows read `merged`, not `shipped`. The row is derived from a merge/squash entry that already carries its PR number and SHA, and `shipped` implied a production promote that `/git-staging` never performed. `shipped` is retired from the row wording contract's verb list; `failed` joins it for subagent runs that end in error +- Crew Monitor column stability: actor and verb segments carry a min-width floor so the metadata column starts at the same x on every short-mask row (long kit agent ids still grow past the floor rather than truncate), and the plan filename now gives up width before the mid segments +- Domain migration: canonical URLs updated to new domains (missionkit.io, dadado.dev, startupkit.rocks, agentkit.works → GitHub). Legacy domains redirect via 301 to preserve links. +- Landing source of record moved to the external design project; the design mirror is versioned so the deploy is reproducible from git alone, and the now-duplicate `assets/production/*.mp4` / `*.webm` masters are gitignored (ADR `2026-08-05_landing-external-design-source-of-record`) +- Residual closeout plans: composite squash commits trade per-tick observability for batch efficiency; post-hoc verification against the merged state is the documented delivery-truth pattern (ADR `2026-08-08_ledger-regen-policy`) +- Deploy-agent startupkit residuals R1-R8: corrected the DESIGN-SYSTEM current URL and its append-only monitor claim, documented shared ledger and HITL evidence paths, aligned the retired equality-guard wording, and added the CI allowlist counter coupling comment +- `docs/cursor-native-audit.md` inventory refreshed: plugin `3.0.0` → `5.0.0`, rules 23 → 25, commands 10 → 27, skills 7 → 9, hooks 2 events → 5. Also corrected a stale claim that CLI `init` writes `plugin.json` into target projects — no generator does + +- `/plan-review-triage`: paced Write residuals for multi-path walks (wave size 2 Tasks; write-confirm collapse when operator authorizes remaining set; per-monitor plans when themes diverge) +- Relocate Mission Control icon/vector sources off repo root into local `assets/design/` (removed tracked root `.ai` files) +- Memory cite hygiene: mc-chrome-icon Closed-by C/G narrowed (circle/ellipsis intro → `ec64be5`/PR #584; `>= 1`+chip/door → `6acb2d7`/PR #592; ADR/`git-staging` §3 amended in place; ellipsis/chip/door pins and thresholds re-derived at HEAD) +- Docs-indicative A–L residuals (D–G, I, K): restore R15 Still open rows on source monitor; prompt template R14 same-commit pairing + single-pipe Audits sample; name `audits-wait-fresh` sentinel in `/plan-external-review`; HANDOFF mid-batch monitor pointer; CI registry catalog parity step +- Public CI skip R7–R10 residuals: merge-gate ADR closeout contract (`gh pr checks` / Evidence green before Gaps-none); `knowledge-classification.json` regenerated by `05f6713` so `build` is green at `6d32c57`; `PUBLIC_REPO_SLUG` honored when `PUBLIC_REPO_URL` unset (warn when both set); denylist pin uses tolerant regex; allowlist exact-count pin updated to 6 (landing body equality step) +- Public vs dev README separation: root `README.md` is consumer storefront only; maintainer dual-repo table moved to `docs/DEVELOPMENT.md`; `docs/CONTRIBUTING.md` points at Development for monorepo loops; public-sync.manifest comments document the boundary (ADR `2026-08-02_public-vs-dev-readme-separation`) +- Crew Monitor feed hygiene (residuals C/E/F + D note): drop unreachable empty-segment branch; prune inert chip wrapper CSS; harden avatar↔chip sibling pin in `plugin-ux-validation`; document latent flat-label glyph-at-end in glossary ADR +- Public sync slug SoT: `sync-public` derives `gh --repo` owner/repo from `PUBLIC_REPO_URL` (removed hardcoded `PUBLIC_REPO_SLUG` from CI); docs/ADR note push + gh redirect; allowlist pin exact count 5 + denylist quote variants + skip when `ci.yml` absent +- `/plan-external-review` command doc: §What Claude should produce aligned with prompt template (delivery truth first, finding priority, evidence mandate, forbidden filler) +- `docs-repo` skill version `0.1.0` → `0.1.1` (`registry.json` + skill frontmatter mirrors) after delivery-truth content wire +- Memory cite hygiene: `plan-monitor-mc-chrome-icon-style-consistency` Closed-by F/C/G/E corrected (exact pins retained; C→`6acb2d7`/PR #592; G→`52a1fc3`+ADR; E deferred); close-monitor R15 Closed-by for A–D +- Public CI skip guards: flip denylist (`!=` public slug) to allowlist (`== agent-kit-startup/agent-kit-dev`) on sync-public, publish-npm, and private-only build steps; ADR + public-launch / repository-boundaries / gitupdate docs; verify checklist notes Path C one-release lag +- Crew Monitor feed row: order is avatar → actor → kind glyph → verb → metadata → time; kind chip is glyph-only (no solid fill); avatar carries the kind `*-bg` tint +- Landing docs: live URL truth for `https://agent.startupkit.com.br` (Hostinger ALIAS + static subdomain); WP `/agentkit` now 301 redirects to the subdomain via the `agentkit-redirect` WordPress plugin +- Landing SEO/a11y: hero promoted to `<h1>`, `twitter:card` downgraded to `summary` until an OG asset ships, and `INVENTORY.md`/`docs/agentkit-landing.md` current URL corrected to `agent.startupkit.com.br` +- Landing SoT guard: `scripts/check-landing-body-equality.mjs` + `pnpm check:landing-body-equality` + CI step pin `index.html` `<body>` to `page-content.html` fragment + ## [4.8.9] - 2026-08-01 ### Fixed diff --git a/LICENSE b/LICENSE index 90bbffc..ba93ed0 100644 --- a/LICENSE +++ b/LICENSE @@ -1,21 +1,75 @@ -MIT License - -Copyright (c) 2026 agent-kit-startup - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +Required Notice: Copyright (c) 2026 agent-kit-startup + +# PolyForm Noncommercial License 1.0.0 + +<https://polyformproject.org/licenses/noncommercial/1.0.0> + +## Acceptance + +In order to get any license under these terms, you must agree to them as both strict obligations and conditions to all your licenses. + +## Copyright License + +The licensor grants you a copyright license for the software to do everything you might do with the software that would otherwise infringe the licensor's copyright in it for any permitted purpose. However, you may only distribute the software according to [Distribution License](#distribution-license) and make changes or new works based on the software according to [Changes and New Works License](#changes-and-new-works-license). + +## Distribution License + +The licensor grants you an additional copyright license to distribute copies of the software. Your license to distribute covers distributing the software with changes and new works permitted by [Changes and New Works License](#changes-and-new-works-license). + +## Notices + +You must ensure that anyone who gets a copy of any part of the software from you also gets a copy of these terms or the URL for them above, as well as copies of any plain-text lines beginning with `Required Notice:` that the licensor provided with the software. For example: + +> Required Notice: Copyright Yoyodyne, Inc. (http://example.com) + +## Changes and New Works License + +The licensor grants you an additional copyright license to make changes and new works based on the software for any permitted purpose. + +## Patent License + +The licensor grants you a patent license for the software that covers patent claims the licensor can license, or becomes able to license, that you would infringe by using the software. + +## Noncommercial Purposes + +Any noncommercial purpose is a permitted purpose. + +## Personal Uses + +Personal use for research, experiment, and testing for the benefit of public knowledge, personal study, private entertainment, hobby projects, amateur pursuits, or religious observance, without any anticipated commercial application, is use for a permitted purpose. + +## Noncommercial Organizations + +Use by any charitable organization, educational institution, public research organization, public safety or health organization, environmental protection organization, or government institution is use for a permitted purpose regardless of the source of funding or obligations resulting from the funding. + +## Fair Use + +You may have "fair use" rights for the software under the law. These terms do not limit them. + +## No Other Rights + +These terms do not allow you to sublicense or transfer any of your licenses to anyone else, or prevent the licensor from granting licenses to anyone else. These terms do not imply any other licenses. + +## Patent Defense + +If you make any written claim that the software infringes or contributes to infringement of any patent, your patent license for the software granted under these terms ends immediately. If your company makes such a claim, your patent license ends immediately for work on behalf of your company. + +## Violations + +The first time you are notified in writing that you have violated any of these terms, or done anything with the software not covered by your licenses, your licenses can nonetheless continue if you come into full compliance with these terms, and take practical steps to correct past violations, within 32 days of receiving notice. Otherwise, all your licenses end immediately. + +## No Liability + +***As far as the law allows, the software comes as is, without any warranty or condition, and the licensor will not be liable to you for any damages arising out of these terms or the use or nature of the software, under any kind of legal claim.*** + +## Definitions + +The **licensor** is the individual or entity offering these terms, and the **software** is the software the licensor makes available under these terms. + +**You** refers to the individual or entity agreeing to these terms. + +**Your company** is any legal entity, sole proprietorship, or other kind of organization that you work for, plus all organizations that have control over, are under the control of, or are under common control with that organization. **Control** means ownership of substantially all the assets of an entity, or the power to direct its management and policies by vote, contract, or otherwise. Control can be direct or indirect. + +**Your licenses** are all the licenses granted to you for the software under these terms. + +**Use** means anything you do with the software requiring one of your licenses. diff --git a/README.md b/README.md index 21d7702..bd0cd85 100644 --- a/README.md +++ b/README.md @@ -1,163 +1,124 @@ -# Agent Kit +# Mission Kit -[Watch the demo on YouTube](https://www.youtube.com/watch?v=9mrAg6Mczfg) +[Watch the demo on YouTube](https://www.youtube.com/watch?v=9mrAg6Mczfg) · [missionkit.io](https://missionkit.io) -**Turn your AI coding agent into one that runs the whole workflow: plan it, build it, ship it, and remember it across long projects.** +**Development operations built into Cursor and VS Code.** -Long AI coding sessions fall apart when the context window fills up. Agent Kit fixes this with a small operating layer that handles planning, handoff between chats, and structured git flow. The agent builds against a checkable plan and writes down where it stopped so any fresh chat picks up exactly where the last one left off. +Mission Kit 5 is a free, source-available framework under [PolyForm Noncommercial](https://polyformproject.org/licenses/noncommercial/1.0.0). It adds project management, DevSecOps discipline, and agent orchestration so you can plan, build, review, and ship without leaving the IDE. Install and CLI packages still use the Agent Kit identifiers (`npx @dadado/agent-kit-cli`, `agent-kit`, `/agent-kit-onboard`). Commercial use: [sales@missionkit.io](mailto:sales@missionkit.io). -## Why you'd want it - -- **No more lost context.** The agent keeps a short state file; new chat, one command, and it's caught up. -- **Work against real plans.** To-dos you can watch tick off, not vibes. Confirmations stay human-in-the-loop (Ask questions), not unchecked autonomy. -- **Built-in DevOps discipline.** Staging-first git flow prevents history chaos. -- **Production needs confirmation.** Agent can push to staging alone; promoting to `main` always asks first. -- **Operational learning, not model training.** Memory and optional external review keep findings durable across chats; they do not retrain the model. -- **Clean history everywhere.** Commits and docs describe the software, not chat chatter. +Formerly **agent-kit**. Same toolkit, clearer product name. -## Features +Long AI coding sessions fall apart when the context window fills up. Mission Kit keeps work on a checkable plan, saves where you stopped, and lets any fresh chat pick up cleanly. Confirmations stay human-in-the-loop, not unchecked autonomy. -| Feature | What you get | -|---------|----------------| -| **Plans + HITL gates** | `/start-project` Broad Intake, then two gates (write plan, then first unit). Confirmations use Ask questions (clickable options; chat fallback when the tool is unavailable). | -| **Phase handoff** | `.cursor/HANDOFF.md` plus Context Guardian and native hooks (`sessionStart` / `preCompact`) so a fresh chat resumes without re-briefing. Local workspace state; not a hosted sync plane. | -| **Manual or continuous run** | `/continue-plan` (one phase per chat) or `/run-plan` (runs to the end; picks worker orchestration or in-session loop; headless via `agent-kit run-plan`). `/run-plan-all` queues multiple plans sequentially. Plan/queue orchestration, not a general graph runtime. | -| **Staging → prod git** | `/git-staging` for automatic promote to `origin/staging`; `/git-prod` only after explicit confirmation. Direct commits to `main` are blocked. | -| **Memory loop** | Resolved errors and tradeoff decisions in `.cursor/memory/` so the next chat can reuse them. | -| **Repository readiness** | Install scans the repo, applies safe local fixes, and writes a readiness snapshot. `/agent-kit-onboard` resolves remaining decisions one at a time before `/start-project`. | -| **Agent Personas** | Mode-aware chat/CLI chrome only: Autopilot (`/continue-plan`), Night Shift (`/run-plan`), Ghost Runner (CLI). Configure after readiness or set `agentPersona` in `.cursor/context/config.json`. Never changes commits, HANDOFF, memory, or product docs. | -| **Optional external plan review** | After a plan is exhausted, arm Claude Code for a gap monitor; triage with `/plan-review-triage`. Opt-in via config. Findings-only by default (no silent product auto-fix). | -| **Skills + domain packs** | Registry skills and optional L1 packs (clean code, context tools, and more). Install/update via CLI; contribute upstream with `agent-kit contribute`. | -| **Output hygiene** | Chat can be light; commits, docs, HANDOFF, and memory stay professional and inheritable. | - -### Production-agent layers (L0) +## Why you'd want it -How the kit maps to a five-layer production-agent lens. Classifications and public evidence anchors: [five-layer claim matrix](docs/five-layer-claim-matrix.md). Documentation alone is not proof of behavior. +- **No more lost context.** State travels with the repo; a new chat catches up with one command. +- **Work against real plans.** To-dos you can watch tick off. Autonomy stays optional and gated. +- **Built-in DevOps discipline.** Staging-first git flow keeps history clean. +- **Production needs confirmation.** Staging can run on autopilot; promoting to `main` always asks first. +- **Learnings that stick.** Resolved errors and decisions stay in the workspace for the next chat. Nothing retrains the model. +- **Clean history.** Commits and docs describe the software, not chat chatter. -| Layer | What ships in core | Explicit non-claim | -|-------|--------------------|--------------------| -| Prompt + HITL | Plan gates, Ask questions, `/git-prod` confirmation | Not full autonomy without review | -| Context + memory | HANDOFF, hooks, memory loop, personas (chrome only) | Not a hosted control plane or cloud HANDOFF sync | -| Safeguards | Staging-first git, shell/secrets hooks, output hygiene | Not a guarantee that every install is production-ready | -| Iterative review | Opt-in external monitor, triage, Field Report cadence | Not autonomous model self-improvement | -| Workflow coordination | `/run-plan`, `/run-plan-all`, headless CLI, local Mission Control | Not a general graph / DAG engine | +## What you get -Released consumer lane (npm / public GitHub) is version-qualified separately from private staging. Pin or check `@dadado/agent-kit-cli` when you need a reproducible floor. +| Capability | In practice | +|------------|-------------| +| **Plans with human gates** | `/start-project` reviews context, writes a plan, then runs the first unit only after you confirm. | +| **Resume across chats** | Finish a phase, open a fresh chat, run `/continue-plan`. Native hooks help the agent reload state. | +| **Manual or continuous run** | Drive one phase at a time (`/continue-plan`), let a plan run to the end (`/run-plan`), or queue several (`/run-plan-all`). | +| **Staging → production git** | `/git-staging` promotes to `origin/staging`. `/git-prod` reaches `main` only after explicit confirmation. | +| **Repository readiness** | Install scans the repo and applies safe local fixes. `/agent-kit-onboard` walks remaining decisions before planning. | +| **Optional external review** | After a plan finishes, arm a second-pass gap check and triage findings. Opt-in via config. | +| **Skills and packs** | Registry skills and optional packs (clean code, context tools, and more). Update via CLI; contribute upstream with `agent-kit contribute`. | +| **Mission Control** | Local dashboard over workspace runtime state (loopback by default). | -Deep dives: [getting started](docs/getting-started.md), [personas contract](docs/personas-contract.md), [creating personas](docs/creating-personas.md), [external plan review](docs/external-plan-review.md), [domain packs](docs/domain-packs.md). +Deep dives: [getting started](docs/getting-started.md), [five-layer claim matrix](docs/five-layer-claim-matrix.md), [external plan review](docs/external-plan-review.md), [domain packs](docs/domain-packs.md), [personas](docs/personas-contract.md). ## Install ### In Cursor (recommended) -Open your project in Cursor and copy-paste this into chat: +Open your project in Cursor and paste this into chat: ``` You are the installer for Agent Kit L0. Confirm the absolute workspace root path via Ask questions before any write operations. If Node.js and npx are available, run `npx @dadado/agent-kit-cli install` in the confirmed root directory. Otherwise, fetch the install contract from https://raw.githubusercontent.com/agent-kit-startup/agent-kit/main/install.md and follow the Port B instructions. Detect missing Node.js or Git and report either prerequisite. Preserve existing `.cursor/` content. After successful installation, run or offer `/agent-kit-onboard` (SoT: `.cursor/commands/agent-kit-onboard.md`, install.md section 6). Use Ask questions for unresolved readiness choices and confirmations, with chat fallback when unavailable. Do not ask about skins, external review, or a first deliverable before essential readiness passes. ``` -> **Source:** [install-prompt.md](install-prompt.md) - Copy from raw URL: https://raw.githubusercontent.com/agent-kit-startup/agent-kit/main/install-prompt.md +> **Source:** [install-prompt.md](install-prompt.md) · Raw: https://raw.githubusercontent.com/agent-kit-startup/agent-kit/main/install-prompt.md ### In the terminal -Run from your project root: +From your project root: ```bash 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). +Unpinned `npx` resolves to the latest publish. Pin when you need a reproducible install: `npx @dadado/agent-kit-cli@x.y.z install`. -That's it. You now have a handful of slash commands and a small set of rules. Full walkthrough: [docs/getting-started.md](docs/getting-started.md). +That installs slash commands and a small set of rules into the project. Walkthrough: [docs/getting-started.md](docs/getting-started.md). ## Usage -1. **Prepare the repository:** `/agent-kit-onboard` - progressive readiness (detect, safe fixes, one decision at a time). Completes only with verified essentials. -2. **Start a plan:** `/start-project` - after readiness, Broad Intake Review and two gates with Ask questions: (A) write plan file, (B) run first unit only after explicit confirmation. -3. **Work one phase:** agent implements the current phase, updates handoff, and stops. -4. **Continue later:** `/continue-plan` in a fresh chat picks up where you left off (Autopilot chat chrome by default). -5. **Ship to staging:** `/git-staging` - branches, commits, merges automatically. +1. **Prepare the repository:** `/agent-kit-onboard` - readiness, safe fixes, one decision at a time. +2. **Start a plan:** `/start-project` - describe a goal; confirm the plan, then the first unit. +3. **Work a phase:** the agent implements, saves resume state, and stops (manual mode). +4. **Continue later:** `/continue-plan` in a fresh chat. +5. **Ship to staging:** `/git-staging` - branch, commit, merge to `origin/staging`. -Two ways to drive a plan: +How to drive a plan: -- **`/continue-plan`** - you drive: one phase per chat, the agent stops and waits between units. Operator playbook: [Getting started - Manual playbook](docs/getting-started.md#manual-playbook-default). -- **`/run-plan`** - it drives: the agent works through the plan to the end, checking off to-dos and pushing each finished topic to staging (Night Shift chat chrome by default). It picks the best execution strategy itself (worker delegation when available, same-chat loop otherwise). Optional external plan review via Claude Code provides post-completion gap detection. Headless CLI ticks use Ghost Runner banners by default. -- **`/run-plan-all`** - Order and run multiple plans as a deduplicated queue (PO synthesis → confirm → execute). Pure orchestrator: after you confirm the queue, it dispatches one Task subagent per plan (each runs the `/run-plan` tick contract) instead of implementing in-window. +- **`/continue-plan`** - you drive: one phase per chat. +- **`/run-plan`** - the kit drives: runs the plan to the end and stages finished work. +- **`/run-plan-all`** - queue several plans and run them in order after you confirm the queue. -**Which command next?** Short chooser in [Getting started](docs/getting-started.md#which-command-next). Modes table: [plan-routine §5](autogit/plan-routine.md#5-two-execution-modes). +Short chooser: [Getting started](docs/getting-started.md#which-command-next). -**Production safety:** `/git-prod` promotes staging to `main` but always asks for confirmation first. Direct commits to `main` are blocked. +**Production safety:** `/git-prod` promotes staging to `main` only after confirmation. Direct commits to `main` are blocked. -### Dashboard +### Mission Control (local dashboard) -**Mission Control** is a local panel over the Agent Kit runtime state. It binds to loopback by default and serves only its own static files. It is a cockpit for one workspace, not a hosted multi-tenant control plane. Actions stay copy-only (clipboard + paste destination). The Config section is a narrow exception: it may merge allowlisted session prefs into `.cursor/context/config.json` via loopback `PUT`/`PATCH /api/config` (no git, process, or prod mutations). Opt-in LAN: `/dashboard-broadcast` (token-gated). Production-ship constraints: [Getting started - Mission Control production-ship constraints](docs/getting-started.md#mission-control-production-ship-constraints). - -The panel source lives under `dashboard/` in this repository (and the public agent-kit tree). **L0 install does not copy `dashboard/` into your app.** The published CLI ships `dashboard/**` inside `@dadado/agent-kit-cli` from 4.8.2 onward, so `agent-kit dashboard` resolves the panel from the installed package and snapshots your workspace via `MISSION_CONTROL_REPO_ROOT`. On 4.8.0 or an older pin the panel assets are absent: upgrade the CLI, or point it at a kit checkout (`MISSION_CONTROL_KIT_ROOT` / `AGENT_KIT_HOME` / sibling `../agent-kit`). +Mission Control is a local panel over Mission Kit runtime state. It binds to loopback by default and serves only its own static files. It is a cockpit for one workspace, not a hosted multi-tenant control plane. ```bash -# From a consumer workspace (snapshots this repo; UI from CLI package or kit host) agent-kit dashboard -# From an agent-kit tree that includes dashboard/ — start (or reuse) and open the URL -npm run dashboard -# or: node dashboard/start.mjs -# Explicit consumer snapshot while serving from the kit tree: -# MISSION_CONTROL_REPO_ROOT=/path/to/consumer npm run dashboard - -# Opt-in LAN broadcast (token-gated; prints LAN URL + token) -npm run dashboard:broadcast -# or: agent-kit dashboard-broadcast - -# Foreground serve only (debugging; does not open a browser) -npm run start:dashboard +# Opt-in LAN broadcast (token-gated) +agent-kit dashboard-broadcast ``` -Then open the **printed** URL if the browser did not open (with `PORT` unset, each workspace gets a stable port in `3333–3588`; do not assume `:3333`). In Cursor chat, `/dashboard` does the same start-and-open flow via the IDE browser. For trusted LAN, use `/dashboard-broadcast` (never silent `HOST=0.0.0.0` without a token). +Open the **printed** URL if the browser did not open (with `PORT` unset, each workspace gets a stable port in `3333–3588`; do not assume `:3333`). In Cursor chat, `/dashboard` starts the same flow. -**If `agent-kit dashboard` says no `dashboard/start.mjs`:** the installed CLI is older than 4.8.2. (1) Upgrade or pin `@dadado/agent-kit-cli@4.8.2` or newer, or (2) set `MISSION_CONTROL_KIT_ROOT` / `AGENT_KIT_HOME`, or (3) keep a sibling `../agent-kit` checkout. L0 alone never places the panel binary in your project tree. +**If `agent-kit dashboard` says no `dashboard/start.mjs`:** upgrade or pin `@dadado/agent-kit-cli@4.8.2` or newer, or set `MISSION_CONTROL_KIT_ROOT` / `AGENT_KIT_HOME`. Install does not copy `dashboard/` into your app tree. -The Cockpit reads as one page in four sections, each reachable from the primary navigation: - -| Section | What it answers | -|---------|------------------| -| Current mission | The plan in flight: status, progress, friendly Mode labels, and previous/current/next todo | -| Flight Log | HANDOFF Gaps log (**NOW** / **Earlier**, wipe on new flight; cap 15 within a flight) plus operator Warnings (Quota pause, Heads up); palette-by-type notification chrome (`ok` / `advice` / `prompt` / `residual` / `warning`); clipboard icon; **one dynamically-labeled action button per entry** (composed prompt + document path; `Copy fix prompt` / `Copy recovery prompt` / `Copy follow-up prompt` / `Copy triage command`) with the toast naming the chat input as paste destination; **All clear** when idle (no literal `none` as a yellow NOW debit) | -| Checklist | What remains: recent plan cards, parked and incomplete plans, and readiness notes | -| Crew Monitor | Live agent/crew feed: ticks, handoffs, deliveries, and denser `agent_step` rows for active-plan to-dos (cap 20) | - -Plans, Activity, Agents, Skills, Commands, Health, Git, Memory, Terminals, Processes, and Config live in the More sections menu next to those links, with their counts; tabs are deep-linkable via URL hash. Colored dots only signal state (good / important / attention) and are always paired with a label or icon; decorative dots are stripped. **Health** is a Healthcenter for the same seven workspace checks (`plans`, `handoff`, `agents`, `commands`, `memory`, `git`, `config`): vitals-style diagnosis cards, live severity, expand/detail per check, and per-problem Copy fix prompt CTAs plus Autofix/Fix controls that only copy a command or path and name where to paste it (chat, terminal, or file picker). Snapshot/serve errors are distinct from per-check fails. Health does not reorder main cockpit tabs and does not duplicate Checklist readiness. - -Section highlights: **Config** is a grid form with Save pinned in a top actions bar, per-fieldset copy-snippet buttons for when the write path is unavailable, and dead-control hints (backend is claude-only; `updateApply.auto` never writable). The full consumer knob inventory lives in [docs/consumer-configuration.md](docs/consumer-configuration.md). **Memory** pairs a live recent-errors panel (from `.cursor/memory/errors/`) with green/red icon panels and an error-o-meter KPI strip (counts, rates, top tags). **Git** shows promotion flow lanes (work → staging → main, ahead/behind vs both), a readable commit graph, and staging-hygiene hints. **Commands**, **Skills**, and **Agents** are card grids with copy-only CRUD CTAs and lock badges on kit-managed items. **Plans** rows are status-aware (resume/run/edit/archive prompts) with a live progress bar from frontmatter to-do counts. **Processes** lists live processes with a generated per-process description of what each one is doing. - -Every action copies text and names where to paste it: repo-relative paths go to the file picker, slash commands to the chat input, chat references to the past-chat picker, and shell commands, PIDs, and commit shas to the terminal. The panel cannot open a file or a chat, and no label claims it can. - -Full routine: `autogit/gitupdate.md` after install. +More: [Getting started - Mission Control](docs/getting-started.md#mission-control-production-ship-constraints) · [consumer configuration](docs/consumer-configuration.md). ## Docs | Guide | What's in it | |-------|--------------| -| [Five-layer claim matrix](docs/five-layer-claim-matrix.md) | Public five-layer positioning (core / optional / planned / unsupported) | | [Getting started](docs/getting-started.md) | Install, commands, day-to-day workflow | -| [Repository readiness](docs/repository-readiness-onboarding.md) | Install discovery, `/agent-kit-onboard`, and deliverable boundary | -| [Bootstrap](docs/bootstrap.md) | Exactly what lands in your project, and why there's no nested folder | -| [Layers](docs/layers-spec.md) | How the base install, optional packs, and your local files layer together | -| [Domain packs](docs/domain-packs.md) | Optional bundles: clean code, DevOps, testing, and more | -| [Agent Personas](docs/personas-contract.md) | Mode defaults, `agentPersona` config, hygiene boundary ([create / contribute](docs/creating-personas.md)) | -| [External plan review](docs/external-plan-review.md) | Opt-in Claude Code monitor after `/run-plan` exhaustion | -| [Manifest](docs/agent-kit-manifest.md) | The `.cursor/agent-kit.json` file | -| [Contributing](docs/CONTRIBUTING.md) | Working on the kit itself (includes contributor quickstart) | +| [Five-layer claim matrix](docs/five-layer-claim-matrix.md) | Public positioning (core / optional / planned / unsupported) | +| [Repository readiness](docs/repository-readiness-onboarding.md) | Install discovery and `/agent-kit-onboard` | +| [Bootstrap](docs/bootstrap.md) | What lands in your project | +| [Domain packs](docs/domain-packs.md) | Optional skill packs | +| [Agent Personas](docs/personas-contract.md) | Mode-aware chat chrome | +| [External plan review](docs/external-plan-review.md) | Opt-in post-plan gap monitor | +| [Manifest](docs/agent-kit-manifest.md) | `.cursor/agent-kit.json` | +| [Contributing](docs/CONTRIBUTING.md) | Working on the kit | +| [Development](docs/DEVELOPMENT.md) | Factory topology and maintainer workflows | | [Docs index](docs/README.md) | Everything else | -## For maintainers +## Licensing + +Mission Kit (Agent Kit) is source-available under the [PolyForm Noncommercial License 1.0.0](https://polyformproject.org/licenses/noncommercial/1.0.0) (see [LICENSE](LICENSE)). -Two GitHub repos, one product: +- Free for personal, non-commercial use under PolyForm Noncommercial. +- Commercial use, distribution, or embedding in a commercial product requires a separate commercial license. +- Companies: contact [sales@missionkit.io](mailto:sales@missionkit.io). -| Repo | Role | -|------|------| -| [agent-kit-dev](https://github.com/agent-kit-startup/agent-kit-dev) (private) | Factory: CLI, sync tooling, dogfood. Daily flow: `git staging` → `git prod` → allowlist sync. | -| [agent-kit](https://github.com/agent-kit-startup/agent-kit) (public) | Storefront and **canonical registry** (`registry/**`). Consumers install from here; registry PRs land here. | +## Contribute -Projects that install Agent Kit receive only `.cursor/` + `autogit/` + the manifest, never the whole monorepo. +Want to improve skills, docs, or the CLI? Start at [docs/CONTRIBUTING.md](docs/CONTRIBUTING.md). Factory and sync details live in [docs/DEVELOPMENT.md](docs/DEVELOPMENT.md). -**Three layers:** local scratch (HANDOFF/plans, gitignored) · private Git (factory) · public (storefront + registry SoT). Full cheat sheet: [docs/repository-boundaries.md](docs/repository-boundaries.md#cheat-sheet-three-layers). +Participation is covered by the [Code of Conduct](.github/CODE_OF_CONDUCT.md). Stuck or unsure where to ask? [Support](.github/SUPPORT.md). Found a vulnerability? Do not open an issue - follow the [security policy](.github/SECURITY.md). diff --git a/autogit/gitupdate.md b/autogit/gitupdate.md index 9cf624f..77206d7 100644 --- a/autogit/gitupdate.md +++ b/autogit/gitupdate.md @@ -295,6 +295,7 @@ This section contains the detailed prompts that should be followed when commands #### 9. **Open and merge Merge Request / Pull Request** - **GitLab:** Create the MR with `glab mr create --title "<title>" --description "<description>" --target-branch staging`. Then run `glab mr merge <number>` to merge. If it fails due to authentication, provide the manual creation link and await instructions. - **GitHub:** Create the PR with `gh pr create --title "<title>" --body "<description>" --base staging`. **Always pass `--base staging`** (default base is often `main`; never merge staging work straight to `main`). Then run `gh pr merge <number>` (or the returned number). If it fails due to authentication, provide the manual creation link and await instructions. + - **Evidence-checks merge gate (before merge / Gaps-none):** run `gh pr checks <N>` and confirm `build` (including the **Evidence checks** step) is green. Do **not** merge while required checks are pending or failing. If Evidence checks fail (`knowledge-classification.json` stale or missing `_index` targets), regenerate/fix and re-push before merge; do not write HANDOFF `- **Gaps:** none` over red. Optional operator follow-up: require `build` as a branch-protection check on `staging` (not a silent workflow edit). ADR: `.cursor/memory/decisions/2026-08-01_evidence-checks-merge-gate.md`. #### 10. **Cleanup and final update** - Run `git checkout staging` to return to staging branch (needed before deleting working branch). @@ -424,7 +425,7 @@ This section contains the detailed prompts that should be followed when commands | Check | How | |-------|-----| | Private tag CI | `gh run list` for the `vX.Y.Z` tag: `build`, `publish-npm`, and `sync-public` all green | - | Public storefront tag CI (advisory) | On `agent-kit-startup/agent-kit`, tag/Release runs should show `build` green with `sync-public` / `publish-npm` **skipped** (not failed). Do not treat a skipped public sync job as a private sync failure. | + | Public storefront tag CI (advisory) | On `agent-kit-startup/agent-kit`, tag/Release runs should show `build` green with `sync-public` / `publish-npm` **skipped** (not failed; allowlist is `github.repository == 'agent-kit-startup/agent-kit-dev'`). The guard lands on the mirror only after Path C syncs the updated `ci.yml` (one-release lag). Do not treat a skipped public sync job as a private sync failure. | | npm | `npm view @dadado/agent-kit-cli version` matches the release | | Public sync PR **merged** | `sync-public` may open a PR; do **not** pass this row on CI-green alone. Confirm the public sync PR is **merged** (`gh pr view` / `gh pr list -R <public> --state merged`) before claiming public `main` is current | | Public `main` | Latest commit message like `chore: sync private vX.Y.Z (...)` on the public default branch **after** that merge | diff --git a/autogit/plan-routine.md b/autogit/plan-routine.md index 119f461..5d5b939 100644 --- a/autogit/plan-routine.md +++ b/autogit/plan-routine.md @@ -94,7 +94,7 @@ Plans also support one **plan-level** attribution field (top-level frontmatter, **`staging-ready` and lint evidence:** `Staging ready: yes` means the worker ran repository-appropriate formatter/linter checks on touched files (or stated none applicable) and recorded commands/results under `Tests:` or `Validation:` in the summary. Prefer `worker_contract` strings that name lint explicitly (e.g. `staging-ready (lint)`). Orchestrator rejects `staging-ready=yes` without that evidence when the to-do changed formatted/linted files. Do not require a global lint for pure markdown (or other paths) when no applicable linter exists. Full rules: `/run-plan` "Review the return" / worker prompt. Cross-links: `.cursor/memory/errors/2026-07-21_ci-biome-blocked-440-publish.md`, `2026-07-23_biome-format-blocked-446-tag-ci.md`, process note in `plan-monitor-dashboard-field-report-and-skins.md` (lint after merge → extra formatting PR). -**Findings contracts** (`worker_contract` contains `findings`, names a plan-body artifact such as `backlog table in plan body`, or review-style to-do ids like `review-*`): the worker is the **first-party author** of that phase's findings in the plan (or agreed artifact). **Findings-only:** the review worker must not auto-fix product code. After findings exist, `/run-plan` reads `externalPlanReview.autoRemediate` (default `false`) and remediates via a separate fix-agent Task (small/contained) or a residuals backlog plan (large/multi-touch); never silent apply in the review tick. In orchestrated `/run-plan`, dispatch with **write access**; do not default to ask/read-only. The orchestrator reviews the summary and the written section; transcription from summary is **fallback only** (label secondhand; note gap in HANDOFF). Product code stays thin-orchestrator: only plan/artifact edits unless the to-do says otherwise. Full rules: `/run-plan` section "Findings contracts (review workers)" and "Remediation gate". Findings contracts still require lint evidence when the worker also edits formatted/linted product or test files; plan-only markdown findings with no applicable linter may state none applicable. +**Findings contracts** (`worker_contract` contains `findings`, names a plan-body artifact such as `backlog table in plan body`, or review-style to-do ids like `review-*`): the worker is the **first-party author** of that phase's findings in the plan (or agreed artifact). **Findings-only:** the review worker must not auto-fix product code. After findings exist, `/run-plan` reads `externalPlanReview.autoRemediate` (default `false`) and remediates via a separate fix-agent Task (small/contained) or, when closeout depth and Blocking severity allow, a residuals backlog plan (large/multi-touch); never silent apply in the review tick. Residuals enqueue is **not** the unbounded happy path: max closeout depth 1 per theme family; prefer Ack / Fix nits for nits or process-only Still open (ADR `decisions/2026-08-11_plan-audit-residuals-termination.md`). In orchestrated `/run-plan`, dispatch with **write access**; do not default to ask/read-only. The orchestrator reviews the summary and the written section; transcription from summary is **fallback only** (label secondhand; note gap in HANDOFF). Product code stays thin-orchestrator: only plan/artifact edits unless the to-do says otherwise. Full rules: `/run-plan` section "Findings contracts (review workers)" and "Remediation gate". Findings contracts still require lint evidence when the worker also edits formatted/linted product or test files; plan-only markdown findings with no applicable linter may state none applicable. Example: @@ -124,6 +124,8 @@ todos: - Do **not** set `inline_first: true` with `security-reviewer`, `tech-lead`, `explore`, or product `read_scope` unless Task isolation is intentional (`force_task: true`). Pairing the flag with non-lightweight tags still dispatches Task and can burn quota under Auto. - After an API-limit Task abort: recover, then `/continue-plan` or re-author a qualifying docs tick. Do not expect silent inline fallback of the aborted to-do. +**Mission Control dashboard SoT:** for dashboard HTML/CSS/JS work, author `read_scope` with `dashboard/dashboard.html` (repo root). Never seed `packages/cli/dashboard/dashboard.html` — that tree is gitignored and regenerated by `scripts/sync-cli-dashboard.mjs` at prepack. Template note: `.cursor/context/templates/plan.md` ("Mission Control dashboard SoT path"). + **Authoring checklist: split docs-only vs product ticks (Auto quota):** Prefer **two to-dos** when a phase would mix markdown deliverables with product evidence reads. More Auto ticks then qualify for inline-first; heavy work stays Task-isolated. ADR: `decisions/2026-07-27_auto-run-no-regression-invariants.md`. @@ -172,6 +174,7 @@ According to [cursor-plan-handoff.mdc](.cursor/rules/cursor-plan-handoff.mdc): - **Continuous** mode (`/run-plan`, any strategy): execute `/git-staging` at end of tick if diff exists (authorized by command); in the orchestrated strategy the main window stages, never the worker. - Production only via `/git-prod` with explicit confirmation via **Ask questions** (chat fallback if the tool is unavailable). - If PM tool tasks were updated, mention in HANDOFF. +- **Evidence-checks closeout (merge gate):** before writing `- **Gaps:** none` or claiming staging-ready on a closeout tick, confirm `build` / Evidence checks are green via `gh pr checks <N>` (open staging PR) or local `pnpm evidence:knowledge-classification:check`. Do not merge while checks are in-flight or red. Name red/pending Evidence checks under Gaps until green (or record an explicit waiver). ADR: `decisions/2026-08-01_evidence-checks-merge-gate.md`. --- @@ -210,9 +213,7 @@ Heavy I/O operations in slash commands (Broad Intake scans, transcript scans, re | Scan/Operation Type | subagent_type | Command examples | |---------------------|---------------|----------| -| Broad Intake (9-bucket scan) | explore | `/start-project`, `/backlog-add` | - -**Broad Intake Memory paths (must match command `read_scope`):** `.cursor/memory/decisions/`, `.cursor/memory/errors/`, `.cursor/memory/plan-monitor-*.md`, theme-matched `.cursor/memory/plan-review-*.md`, `.cursor/memory/_index.md` (Audits + Decisions). Prefer theme match over reading every Audits row. Same triage labels only (`ignore` / `error` / `include` / `note`). ADR: `decisions/2026-07-27_plan-monitor-consumer-awareness.md`. Field Report and `/plan-review-triage` stay attention/HITL SoT. +| Broad Intake (bucket table in command is SoT; includes Unprocessed dogfood) | explore | `/start-project`, `/backlog-add` | | Transcript + subject-context scan | explore | `/field-report-resolve` | | Readiness scan | explore | `/agent-kit-onboard` | | Monitor scan | explore | `/plan-review-triage` | @@ -222,6 +223,10 @@ Heavy I/O operations in slash commands (Broad Intake scans, transcript scans, re | Template/rule update | docs-repo | Phase 3 updates | | Fallback (no specific match) | generalPurpose | Any command | +**Broad Intake Memory paths (must match command `read_scope`):** `.cursor/memory/decisions/`, `.cursor/memory/errors/`, `.cursor/memory/plan-monitor-*.md`, theme-matched `.cursor/memory/plan-review-*.md`, `.cursor/memory/_index.md` (Audits + Decisions). Prefer theme match over reading every Audits row. Same triage labels only (`ignore` / `error` / `include` / `note`). ADR: `decisions/2026-07-27_plan-monitor-consumer-awareness.md`. Field Report and `/plan-review-triage` stay attention/HITL SoT. + +**Broad Intake Unprocessed dogfood paths:** factory `dogfood/README.md` or consumer `.cursor/dogfood/README.md` (`##` or `### Unprocessed Files` on read). Never auto-analyze. ADR: `decisions/2026-08-11_dogfood-unprocessed-broad-intake-bucket.md`. + **Resolution order:** explicit `worker_type` on the to-do (plan frontmatter), then the signal table above, then `generalPurpose` as fallback. ### How to use the worker prompt template @@ -249,8 +254,8 @@ When Task dispatch is unavailable (no subagent support in the current session), | Command | Delegation scope | Phase | |---------|-----------------|-------| -| `/start-project` | Broad Intake Review (9-bucket scan) → Task(explore) | Phase 2 | -| `/backlog-add` | Broad Intake Review (same 9 buckets) → Task(explore) | Phase 2 | +| `/start-project` | Broad Intake Review (buckets listed in the command table, includes Unprocessed dogfood) → Task(explore) | Phase 2 | +| `/backlog-add` | Broad Intake Review (same buckets as `/start-project`) → Task(explore) | Phase 2 | | `/agent-kit-onboard` | `agent-kit doctor --json` + readiness scan + progressive resolution → Task(explore) | Phase 2 | | `/field-report-resolve` | Claim-check: transcript (`answered`) + named subject context (`subject_resolved`, path/plan evidence only) + monitor scan → Task(explore) | Phase 2 | | `/plan-review-triage` | Monitor scan + summarization → Task(explore) | Phase 2 | diff --git a/dashboard/dashboard-data.mjs b/dashboard/dashboard-data.mjs index f62b7d3..c60d682 100644 --- a/dashboard/dashboard-data.mjs +++ b/dashboard/dashboard-data.mjs @@ -21,6 +21,7 @@ import { MAX_AGENT_PROMPTS, MAX_GIT_ACTIVITY, MISSION_TIMING_LEDGER_REL, + SUBAGENT_TRANSCRIPT_FILE_RE, buildMissionControlView, collectDeferredCheckIds, collectReadinessPendingFromReport, @@ -35,6 +36,7 @@ import { parseFlightLogLedger, parseHandoffMarkdown, parseMissionTimingLedger, + parseSubagentRun, serializeFlightLogLedger, serializeMissionTimingLedger, } from "./lib/semantic-model.mjs"; @@ -69,6 +71,12 @@ const MAX_REPORT_FILES = 20; // cap memory reads per snapshot const MAX_REPORT_BYTES = 512 * 1024; // skip oversized reports, degrade quietly const REPORT_RECENCY_MS = 90 * 24 * 60 * 60 * 1000; // 90-day recency window +// Task subagent run scan bounds (fs half of the lifecycle contract). +const MAX_SUBAGENT_PARENTS = 12; // cap parent chat directories walked per snapshot +const MAX_SUBAGENT_FILES = 24; // cap worker transcripts read per snapshot +const MAX_SUBAGENT_BYTES = 512 * 1024; // skip oversized transcripts, degrade quietly +const SUBAGENT_RECENCY_MS = 6 * 60 * 60 * 1000; // 6-hour window: live feed, not an archive + /** Redact likely secrets in terminal output (paths-only git payload uses separate rules). */ const SECRET_OUTPUT_PATTERNS = [ /(?:API_KEY|SECRET|PASSWORD|TOKEN|PRIVATE_KEY)\s*=\s*\S+/gi, @@ -794,6 +802,100 @@ function collectExternalReports() { return reports; } +/** + * Task subagent run scan (fs half of the lifecycle contract). + * + * Worker transcripts live beside the main chat transcript, at + * `~/.cursor/projects/<project-slug>/agent-transcripts/<parent>/subagents/<id>.jsonl` + * — the same store `collectAgentPrompts` walks, whose nested `subagents/` + * directories it deliberately skips so a worker question never masquerades as a + * user prompt. This scan reads only those nested files. + * + * Read-only and bounded: parents outside a 6-hour recency window are skipped + * (the Crew Monitor is a live feed, not an archive), at most + * MAX_SUBAGENT_PARENTS parent directories and MAX_SUBAGENT_FILES transcripts are + * read, oversized transcripts are skipped, and only the first and last records + * of each file are parsed — the phase decision needs the dispatch prompt and the + * terminal record, nothing between. A missing or unreadable store yields an + * empty list, never an error state. The phase/actor decision itself lives in + * `parseSubagentRun`. + */ +function collectSubagentRuns() { + if (!withinSnapshotBudget(700)) return []; + const projectsDir = resolve(process.env.HOME || "~", ".cursor", "projects"); + const slug = ROOT.replace(/\//g, "-").replace(/^-/, ""); + const transcriptsDir = join(projectsDir, slug, "agent-transcripts"); + if (!existsSync(transcriptsDir)) return []; + + const runs = []; + try { + const now = Date.now(); + const candidates = []; + for (const dirent of readdirSync(transcriptsDir, { withFileTypes: true })) { + if (!dirent.isDirectory()) continue; + const subDir = join(transcriptsDir, dirent.name, "subagents"); + if (!existsSync(subDir)) continue; + let dirStat; + try { + dirStat = statSync(subDir); + } catch { + continue; + } + if (now - dirStat.mtimeMs > SUBAGENT_RECENCY_MS) continue; + candidates.push({ parentId: dirent.name, dir: subDir, mtime: dirStat.mtimeMs }); + } + candidates.sort((a, b) => b.mtime - a.mtime); + + const files = []; + for (const parent of candidates.slice(0, MAX_SUBAGENT_PARENTS)) { + let names; + try { + names = readdirSync(parent.dir); + } catch { + continue; + } + for (const name of names) { + const match = SUBAGENT_TRANSCRIPT_FILE_RE.exec(name); + if (!match) continue; + const file = join(parent.dir, name); + let stat; + try { + stat = statSync(file); + } catch { + continue; + } + if (!stat.isFile()) continue; + if (now - stat.mtimeMs > SUBAGENT_RECENCY_MS) continue; + if (stat.size > MAX_SUBAGENT_BYTES) continue; + files.push({ id: match[1], parentId: parent.parentId, file, mtime: stat.mtime }); + } + } + files.sort((a, b) => b.mtime - a.mtime); + + for (const candidate of files.slice(0, MAX_SUBAGENT_FILES)) { + let raw; + try { + raw = readFileSync(candidate.file, "utf-8"); + } catch { + continue; + } + const lines = raw.split("\n").filter((l) => l.trim().length > 0); + if (lines.length === 0) continue; + const run = parseSubagentRun({ + id: candidate.id, + parentId: candidate.parentId, + firstLine: lines[0], + lastLine: lines[lines.length - 1], + modifiedAt: candidate.mtime.toISOString(), + }); + if (run) runs.push(run); + } + } catch { + return []; + } + return runs; +} + // 14. Mission Control semantic view model (now / activity / attention) let readinessPending = []; const readinessPath = join(ROOT, ".cursor", "context", "readiness.json"); @@ -944,6 +1046,7 @@ function readPreviousInventory() { deferredCheckIds: collectOnboardingDeferredCheckIds(), agentPrompts: collectAgentPrompts(), externalReports: collectExternalReports(), + subagentRuns: collectSubagentRuns(), dismissedIds: collectFieldReportDismissedIds(), archivedPlanFiles, agents: SNAPSHOT.agents, diff --git a/dashboard/dashboard.html b/dashboard/dashboard.html index 907186a..a63604d 100644 --- a/dashboard/dashboard.html +++ b/dashboard/dashboard.html @@ -19,6 +19,21 @@ var fav = document.getElementById('favicon'); if (fav) fav.setAttribute('href', skin === 'cursor' ? '/logo-cursor.svg' : '/logo.svg'); })(); +/* Restore Crew Monitor row density before first paint; auto-pick by viewport + when the operator has no stored preference. Same namespaced-key + blocked- + storage tolerance as the skin restore above. */ +(function () { + var key = 'agent-kit:monitor-density'; + var density = null; + try { + var stored = localStorage.getItem(key); + if (stored === 'compact' || stored === 'comfortable') density = stored; + } catch (e) { /* private mode / blocked storage */ } + if (!density) { + density = (window.innerWidth || 0) >= 900 ? 'comfortable' : 'compact'; + } + document.documentElement.setAttribute('data-monitor-density', density); +})(); </script> <style> /* ===== Reset & Base ===== */ @@ -34,7 +49,7 @@ --border-active: #2a3a5a; --text-primary: #e2e8f0; --text-secondary: #8899aa; - --text-muted: #6d8094; + --text-muted: #6d8094; /* landing uses #7f93a8 (AA over marketing gradient); see docs/design-system.md */ --green: #22c55e; --green-bg: rgba(34,197,94,0.12); --yellow: #eab308; @@ -1026,7 +1041,6 @@ .health-item-sev[data-sev="ok"] { color: var(--green); } .health-item-sev[data-sev="warning"] { color: var(--yellow); } .health-item-sev[data-sev="degraded"] { color: var(--orange); } -.health-item-sev[data-sev="error"] { color: var(--red); } .health-item-chevron { color: var(--text-muted); font-size: 10px; @@ -1700,11 +1714,20 @@ white-space: nowrap; } -/* ===== Monitor feed: single flat rows (Phase 1) ===== */ +/* ===== Crew Monitor row (design v2, 2026-08-05) ===== + Ported from the Claude Design project `Mission Control — Crew Monitor` + (verdict: .cursor/context/mission-control-design/remote/v1/ACCEPTANCE.md). + + The layout contract that replaced the equal-shrink segment row: exactly ONE + flexible field per row. `.monitor-row-primary` is the only field allowed to + ellipsis; every other column is fixed-width or fixed-content, and the ref + chips hide WHOLE at a breakpoint rather than truncating. That is what stops + `revi…` / `8f…` / `P…` from coming back — a field that cannot show one whole + token is removed instead of shrunk. */ .live-activity-feed .monitor-row { display: flex; align-items: center; - gap: 10px; + gap: 8px; padding: 6px 12px; font-size: 12px; color: var(--text-secondary); @@ -1716,90 +1739,195 @@ .live-activity-feed .monitor-row:last-child { padding-bottom: 10px; } -.live-activity-feed .monitor-row .monitor-row-chip { - display: inline-flex; - align-items: center; - justify-content: center; +/* Team badge at row start (design v2 identity pass). Replaces the kind glyph: + the glyph's two jobs split, so colour lands here (kind tint + solid semantic + initials) and meaning lands on the badge's tooltip gloss. Unlike the avatar + box removed in #631/#637-#639, this is not a decorative identity chip — it is + the kind cue and the actor cue in one 18px square, which is why `info.bg` + renders on a Crew row again. Stays at --mc-radius-sm while the status pills + sit at --mc-radius-chrome, so a badge never reads as a status pill. */ +.live-activity-feed .monitor-row .monitor-row-badge { + flex: 0 0 18px; width: 18px; - min-width: 18px; height: 18px; - padding: 0; - border-radius: 4px; - font-size: 10px; - font-weight: 700; - flex-shrink: 0; - line-height: 1; - box-sizing: border-box; - /* Solid *-bg fill only; no stroke / inset ring / left rail */ - border: none; - box-shadow: none; -} -.live-activity-feed .monitor-row .monitor-row-icon { - font-size: 11px; - line-height: 1; - width: 1em; - text-align: center; -} -.live-activity-feed .monitor-row .monitor-row-avatar { + border-radius: var(--mc-radius-sm); display: inline-flex; align-items: center; justify-content: center; - width: 18px; - min-width: 18px; - height: 18px; - border-radius: 4px; - font-size: 9px; + font-size: 7.5px; font-weight: 700; + letter-spacing: 0.03em; line-height: 1; - flex-shrink: 0; - background: var(--bg-card-hover); - color: var(--text-secondary); + user-select: none; } -.live-activity-feed .monitor-row .feed-label { - flex: 1; +/* Actor column: fixed basis, not a min-width floor. The floor let long kit + agent ids push the verb and metadata columns off their x; a hard basis keeps + the scan column stable across every kind. Sized so the longest mask in the + operator lexicon ("Project Manager", "Product Analyst") fits unclipped. */ +.live-activity-feed .monitor-row .monitor-row-actor { + flex: 0 0 9.5em; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; - color: var(--text-secondary); - font-size: 12px; - font-weight: 400; - display: flex; - align-items: baseline; - gap: 6px; + font-family: var(--mc-font-mono); + font-size: 10.5px; + color: var(--text-muted); } -/* Structured label spans: actor + verb never shrink; the plan filename is the - low-signal segment and ellipsises first. */ -.live-activity-feed .monitor-row .feed-seg { - flex-shrink: 0; +/* Verb column: state icon + word, both inheriting the verb-state colour. */ +.live-activity-feed .monitor-row .monitor-row-verb { + flex: 0 0 6.5em; + display: inline-flex; + align-items: center; + gap: 4px; white-space: nowrap; -} -.live-activity-feed .monitor-row .feed-sep { - flex-shrink: 0; + font-size: 11px; color: var(--text-muted); } -.live-activity-feed .monitor-row .feed-seg-mid { - flex-shrink: 1; +.live-activity-feed .monitor-row .monitor-row-verb em { + font-style: normal; +} +/* 12px against the chrome set's 16px, so the icon reads as a state mark inside + a text column rather than as chrome. The heavier stroke is optical + compensation: 1.5 at 12px renders lighter than 1.5 at 16px. */ +.live-activity-feed .monitor-row .monitor-row-verb-icon { + width: 12px; + height: 12px; + flex-shrink: 0; + stroke-width: 1.6; +} +.live-activity-feed .monitor-row .v-running { color: var(--green); } +.live-activity-feed .monitor-row .v-awaiting { color: var(--yellow); } +.live-activity-feed .monitor-row .v-failed { color: var(--red); } +.live-activity-feed .monitor-row .v-failed em { font-weight: 600; } +.live-activity-feed .monitor-row .v-merged { color: var(--cyan); } +/* The row's only flexible field, and the only one at --text-primary: the to-do + id / commit subject / gate is what distinguishes one row from the next. */ +.live-activity-feed .monitor-row .monitor-row-primary { + flex: 1 1 auto; min-width: 0; overflow: hidden; text-overflow: ellipsis; + white-space: nowrap; + color: var(--text-primary); } -.live-activity-feed .monitor-row .feed-seg-plan { - flex-shrink: 3; +.live-activity-feed .monitor-row .monitor-row-meta { + flex: 0 0 auto; + display: inline-flex; + align-items: center; + gap: 6px; min-width: 0; - overflow: hidden; - text-overflow: ellipsis; +} +/* Refs left the flexible text and became fixed-content chips (PR #NNN -> #NNN, + 7-8 char SHA, subagent worker id). A chip is either whole or hidden. */ +.live-activity-feed .monitor-row .monitor-row-ref { + font-family: var(--mc-font-mono); + font-size: 10px; color: var(--text-muted); + background: var(--bg-secondary); + border: 1px solid var(--border); + border-radius: 3px; + padding: 1px 5px; + line-height: 1.3; + white-space: nowrap; } -.live-activity-feed .monitor-row .feed-time { +/* Plan ref: flag + basename. Driven by refs.plan, so it renders even when the + label omitted the segment (truncation-safe: never sniffed from the string). */ +.live-activity-feed .monitor-row .monitor-row-plan { + display: inline-flex; + align-items: center; + gap: 4px; color: var(--text-muted); + min-width: 0; +} +.live-activity-feed .monitor-row .monitor-row-plan-flag { + width: 11px; + height: 11px; + flex-shrink: 0; + stroke-width: 1.6; +} +/* Fixed content, like every other chip: no max-width, no ellipsis. The design + capped this at 16em (24em comfortable) and kept ellipsis on it, which clipped + real basenames — `crew-monitor-compact-labels-realtime-activity` needs 271px + against a 240px cap, so the plan chip was rendering the same fragment the + redesign exists to remove. It drops WHOLE at ≤720px instead; above that it + takes the width it needs and the primary field absorbs the difference, which + is what the row's one flexible field is for. */ +.live-activity-feed .monitor-row .monitor-row-plan-name { font-family: var(--mc-font-mono); font-size: 10px; white-space: nowrap; - min-width: 40px; - flex-shrink: 0; +} +.live-activity-feed .monitor-row .monitor-row-time { + flex: 0 0 44px; + text-align: right; + font-family: var(--mc-font-mono); + font-size: 10px; + color: var(--text-muted); + white-space: nowrap; +} + +/* ===== Crew Monitor compaction ladder ===== + Container queries on the feed, not viewport media queries: the Crew Monitor + is a panel inside a 2x2 cockpit grid and a narrow plugin shell, so the width + that matters is the feed's own, never the window's. + Every step hides a field WHOLE. Nothing here narrows a column and leaves it + ellipsising — the actor drops out entirely rather than becoming `Project + Man…` (delta A in ACCEPTANCE.md; the design's ladder narrowed it instead). + Identity survives the drop: the badge keeps the initials, and the actor's own + title plus the row tooltip still name the role in full. */ +.monitor-cq { + container-type: inline-size; +} +@container (max-width: 720px) { + .live-activity-feed .monitor-row .monitor-row-plan-name { display: none; } +} +@container (max-width: 560px) { + .live-activity-feed .monitor-row .monitor-row-ref.ref-sha { display: none; } + .live-activity-feed .monitor-row .monitor-row-actor { display: none; } +} +@container (max-width: 450px) { + .live-activity-feed .monitor-row .monitor-row-verb { flex-basis: 12px; } + .live-activity-feed .monitor-row .monitor-row-verb em { display: none; } + .live-activity-feed .monitor-row .monitor-row-plan { display: none; } +} +@container (max-width: 360px) { + .live-activity-feed .monitor-row .monitor-row-meta { display: none; } +} + +/* ===== Crew Monitor row density (compact | comfortable) ===== + Root data attribute set before first paint; auto-picked from viewport width + when the operator has expressed no preference. Compact is the single-line row + and stays the default on narrow shells. + Comfortable is a second layout, not a taller first one: the meta chips stay + on line 1 (pushed right), and the primary field reflows to a full-width + second line where it gets two clamped lines at 12.5px instead of an ellipsis. + The 26px indent aligns it under the badge. The title tooltip stays required + in both modes — display `label` is capped at MAX_SEMANTIC_LABEL upstream, so + a second line is never a substitute for it. */ +html[data-monitor-density="comfortable"] .live-activity-feed .monitor-row { + flex-wrap: wrap; + row-gap: 3px; + padding: 9px 12px; +} +html[data-monitor-density="comfortable"] .live-activity-feed .monitor-row .monitor-row-meta { margin-left: auto; } +html[data-monitor-density="comfortable"] .live-activity-feed .monitor-row .monitor-row-primary { + order: 10; + flex: 1 0 100%; + white-space: normal; + overflow: hidden; + display: -webkit-box; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; + font-size: 12.5px; + line-height: 1.45; + padding-left: 26px; +} +.monitor-density-btn { + margin-left: var(--mc-space-sm); +} .activity-feed-empty { padding: var(--mc-space-2xl); text-align: center; @@ -1925,17 +2053,6 @@ vertical-align: middle; } -/* ===== Health Investigation Message (legacy; Healthcenter uses .health-detail) ===== */ -.health-message { - font-size: 11px; - color: var(--text-secondary); - padding: 6px 10px; - margin-top: 4px; - border-radius: 4px; - background: var(--bg-secondary); - border: 1px solid var(--border); - animation: fadeIn 0.2s ease; -} @keyframes fadeIn { from { opacity: 0; transform: translateY(-4px); } to { opacity: 1; transform: translateY(0); } @@ -2518,14 +2635,8 @@ .skill-grid, .agent-grid { grid-template-columns: 1fr; } .live-activity-feed { max-height: 180px; } - /* Flex feed-label: allow actor/verb to ellipsis so fixed spans do not hard-clip. */ - .live-activity-feed .monitor-row .feed-seg-actor, - .live-activity-feed .monitor-row .feed-seg-verb { - flex-shrink: 1; - min-width: 0; - overflow: hidden; - text-overflow: ellipsis; - } + /* Crew row narrow-width handling moved to the @container ladder on + .monitor-cq (design v2): the feed's own width decides, not the window's. */ } .overview-stack { @@ -2758,7 +2869,10 @@ outline: none; } /* Shared solid status/lifecycle pill: filled semantic surface, no outline, - slash-command chip geometry. */ + slash-command chip geometry. + Radius (design v2, 2026-08-05): capsule -> --mc-radius-chrome. The capsule + read as a foreign shape next to the squared chrome it always sits beside; + the fill and the mark carry the state, the outline never did. */ .lifecycle-pill { display: inline-flex; align-items: center; @@ -2767,7 +2881,7 @@ font-weight: 600; letter-spacing: 0.02em; padding: 2px 6px; - border-radius: var(--mc-radius-pill); + border-radius: var(--mc-radius-chrome); border: none; color: var(--text-muted); background: var(--bg-secondary, transparent); @@ -2951,7 +3065,7 @@ font-weight: 600; letter-spacing: 0.02em; padding: 3px var(--mc-space-md); - border-radius: var(--mc-radius-pill); + border-radius: var(--mc-radius-chrome); border: none; color: var(--text-secondary); background: var(--bg-card); @@ -2976,7 +3090,9 @@ color: var(--green); background: var(--green-bg); } -/* Idle: pill radius + hollow mark (shape cue vs squared solid chips) */ +/* Idle: hollow mark + unfilled surface. The radius stopped carrying this cue + when the pills squared off (design v2); the ○ mark is now the whole non-color + signal, which is why it may not be dropped. */ .now-status-idle { color: var(--text-muted); background: var(--bg-card); @@ -2991,7 +3107,7 @@ font-weight: var(--mc-chrome-meta-weight); letter-spacing: 0.02em; padding: 3px var(--mc-space-md); - border-radius: var(--mc-radius-pill); + border-radius: var(--mc-radius-chrome); color: var(--blue); background: var(--blue-bg); text-transform: uppercase; @@ -3922,6 +4038,61 @@ return false; } +// ===== Crew Monitor row density (compact | comfortable; local preference) ===== +const MONITOR_DENSITY_KEY = 'agent-kit:monitor-density'; +const MONITOR_DENSITIES = ['compact', 'comfortable']; +/** Comfortable needs horizontal room for a two-line label to be worth the height. */ +const MONITOR_DENSITY_COMFORTABLE_MIN_WIDTH = 900; + +function normalizeMonitorDensity(density) { + return MONITOR_DENSITIES.includes(density) ? density : null; +} + +/** Viewport auto-pick, used only when the operator stored no preference. */ +function autoMonitorDensity() { + return (window.innerWidth || 0) >= MONITOR_DENSITY_COMFORTABLE_MIN_WIDTH + ? 'comfortable' + : 'compact'; +} + +function getStoredMonitorDensity() { + try { + return normalizeMonitorDensity(localStorage.getItem(MONITOR_DENSITY_KEY)); + } catch (e) { + return null; + } +} + +/** Current mode from the root attribute (set pre-paint), else the auto-pick. */ +function currentMonitorDensity() { + return ( + normalizeMonitorDensity(document.documentElement.getAttribute('data-monitor-density')) || + getStoredMonitorDensity() || + autoMonitorDensity() + ); +} + +/** Apply density via root data attribute; an explicit choice persists and wins. */ +function applyMonitorDensity(density, opts) { + const next = normalizeMonitorDensity(density) || autoMonitorDensity(); + document.documentElement.setAttribute('data-monitor-density', next); + document.querySelectorAll('[data-monitor-density-toggle]').forEach((el) => { + el.setAttribute('aria-pressed', next === 'comfortable' ? 'true' : 'false'); + el.textContent = next === 'comfortable' ? 'Comfortable' : 'Compact'; + }); + if (!opts || opts.persist !== false) { + try { + localStorage.setItem(MONITOR_DENSITY_KEY, next); + } catch (e) { /* private mode / blocked storage */ } + } + return next; +} + +function toggleMonitorDensity() { + applyMonitorDensity(currentMonitorDensity() === 'comfortable' ? 'compact' : 'comfortable'); + return false; +} + // ===== More-sections dropdown ===== function isNavMoreOpen() { const menu = document.getElementById('navMoreMenu'); @@ -4190,11 +4361,15 @@ // Monitor live kinds: distinct resting color+icon (no kind-tag text, no left rail). // run_plan follows .now-status-executing green; delivery uses cyan to avoid green wash. // Gloss strings are default software lexicon display masks (kind ids unchanged). - run_plan: { icon: '\u25b6', tag: 'tick', gloss: 'Tech Lead - live execution', color: 'var(--green)', bg: 'var(--green-bg)' }, - handoff: { icon: '\u23f8', tag: 'handoff', gloss: 'Scrum Master - awaiting gate', color: 'var(--yellow)', bg: 'var(--yellow-bg)' }, - agent_step: { icon: '\u25c9', tag: 'step', gloss: 'Full-Stack Developer - task unit', color: 'var(--purple)', bg: 'var(--purple-bg)' }, + run_plan: { icon: '\u25b6', tag: 'tick', gloss: 'Project Manager - live execution', color: 'var(--green)', bg: 'var(--green-bg)' }, + handoff: { icon: '\u23f8', tag: 'handoff', gloss: 'Project Manager - awaiting gate', color: 'var(--yellow)', bg: 'var(--yellow-bg)' }, + agent_step: { icon: '\u25c9', tag: 'step', gloss: 'Developer - task unit', color: 'var(--purple)', bg: 'var(--purple-bg)' }, plan_progress: { icon: '\u2691', tag: 'plan', gloss: 'Product Owner - milestone', color: 'var(--orange)', bg: 'var(--orange-bg)' }, - delivery: { icon: '\u2714', tag: 'delivery', gloss: 'DevOps Engineer - shipped unit', color: 'var(--cyan)', bg: 'var(--cyan-bg)' }, + delivery: { icon: '\u2714', tag: 'delivery', gloss: 'DevOps - merged unit', color: 'var(--cyan)', bg: 'var(--cyan-bg)' }, + // Live Task-worker lifecycle (running / done / failed) and background + // mid-batch review pointers. Both added 2026-08-05; kind ids are additive. + subagent: { icon: '\u25cc', tag: 'subagent', gloss: 'Developer - subagent run', color: 'var(--blue)', bg: 'var(--blue-bg)' }, + plan_review: { icon: '\u2611', tag: 'review', gloss: 'QA - review evidence', color: 'var(--orange)', bg: 'var(--orange-bg)' }, commit: { icon: '\u25cf', tag: 'commit', gloss: 'commit', color: 'var(--orange)', bg: 'var(--orange-bg)' }, agent: { icon: '\u25c6', tag: 'agent', gloss: 'agent', color: 'var(--purple)', bg: 'var(--purple-bg)' }, skill: { icon: '\u2726', tag: 'skill', gloss: 'skill', color: 'var(--orange)', bg: 'var(--orange-bg)' }, @@ -4206,12 +4381,12 @@ // Locked BMP map (hotfix 2026-07-27): feat✦ fix⚙ docs✎ chore⚒ pr⑂ ship✈ if (kind === 'delivery' && commitType) { const subtypes = { - feat: { icon: '\u2726', tag: 'delivery', gloss: 'DevOps Engineer - feat', color: 'var(--purple)', bg: 'var(--purple-bg)' }, - fix: { icon: '\u2699', tag: 'delivery', gloss: 'DevOps Engineer - fix', color: 'var(--red)', bg: 'var(--red-bg)' }, - docs: { icon: '\u270e', tag: 'delivery', gloss: 'DevOps Engineer - docs', color: 'var(--blue)', bg: 'var(--blue-bg)' }, - chore: { icon: '\u2692', tag: 'delivery', gloss: 'DevOps Engineer - chore', color: 'var(--orange)', bg: 'var(--orange-bg)' }, - pr: { icon: '\u2442', tag: 'delivery', gloss: 'DevOps Engineer - PR', color: 'var(--cyan)', bg: 'var(--cyan-bg)' }, - ship: { icon: '\u2708', tag: 'delivery', gloss: 'DevOps Engineer - shipped unit', color: 'var(--cyan)', bg: 'var(--cyan-bg)' }, + feat: { icon: '\u2726', tag: 'delivery', gloss: 'DevOps - feat', color: 'var(--purple)', bg: 'var(--purple-bg)' }, + fix: { icon: '\u2699', tag: 'delivery', gloss: 'DevOps - fix', color: 'var(--red)', bg: 'var(--red-bg)' }, + docs: { icon: '\u270e', tag: 'delivery', gloss: 'DevOps - docs', color: 'var(--blue)', bg: 'var(--blue-bg)' }, + chore: { icon: '\u2692', tag: 'delivery', gloss: 'DevOps - chore', color: 'var(--orange)', bg: 'var(--orange-bg)' }, + pr: { icon: '\u2442', tag: 'delivery', gloss: 'DevOps - PR', color: 'var(--cyan)', bg: 'var(--cyan-bg)' }, + ship: { icon: '\u2708', tag: 'delivery', gloss: 'DevOps - merged unit', color: 'var(--cyan)', bg: 'var(--cyan-bg)' }, }; if (subtypes[commitType]) return subtypes[commitType]; } @@ -4220,7 +4395,7 @@ /** Monitor hero allowlist (curated subset). Inventory kinds stay on the full stream. */ const MONITOR_ACTIVITY_KINDS = new Set([ - 'run_plan', 'handoff', 'delivery', 'agent_step', + 'run_plan', 'handoff', 'delivery', 'agent_step', 'subagent', 'plan_review', ]); /** * Crew Monitor display cap from missionControl.monitorFeedCap @@ -4415,15 +4590,165 @@ } /** - * Crew feed actor: kit agent id, else Engineering Manager for delivery, else - * Squad when a plan is present (never the full plan filename), else Platform - * Engineer. Default software lexicon masks; mirrors briefActivityActor. + * Crew feed actor — WIRE value, not display. Kit agent id, else Eng for + * delivery, else SQ when a plan is present (never the full plan filename), + * else Eng. Mirrors briefActivityActor() in dashboard/lib/semantic-model.mjs; + * the two must not drift, which is why the masks below live in a separate + * layer instead of being folded in here. + * + * `Eng` still collides between the delivery and system fallbacks on the wire. + * The row no longer leaves it ambiguous: crewActorRole() splits it by kind. + * Used directly only as the aria-label fallback when a row has no label. */ function crewEventActor(ev) { if (ev && ev.agent) return String(ev.agent); - if (ev && ev.kind === 'delivery') return 'Engineering Manager'; - if (ev && ev.refs && ev.refs.plan) return 'Squad'; - return 'Platform Engineer'; + if (ev && ev.kind === 'delivery') return 'Eng'; + if (ev && ev.refs && ev.refs.plan) return 'SQ'; + return 'Eng'; +} + +/** + * Operator lexicon (design v2, 2026-08-05): wire actor -> display role. + * Display only. The wire format is frozen — `labelFull` keeps the raw seg0 + * (`Eng`, `docs-repo`, …) and the collectors emit exactly what they emitted + * before, so nothing here is a backend change. + */ +const CREW_ACTOR_MASK = { + 'docs-repo': 'Tech Writer', + explore: 'Product Analyst', + generalPurpose: 'Analyst', + SQ: 'Scrum Master', + Dev: 'Developer', + QA: 'QA', + PO: 'Product Owner', + PM: 'Project Manager', +}; + +/** + * The `Eng` split. The lexicon maps two different roles onto the same wire + * token, and six consecutive `Eng · merged` rows were the single worst thing in + * the shipped feed. `kind` already carries the distinction, so no new wire + * field is needed (ACCEPTANCE.md delta B): a delivery row's `Eng` is the + * Platform Engineer, every other `Eng` is the Engineering Manager. + */ +function crewActorEngRole(ev) { + return ev && ev.kind === 'delivery' ? 'DevOps' : 'Tech Lead'; +} + +/** + * Display role for a Crew row. Resolution order (design contract): + * per-event `ev.role` -> mask -> raw token. `ev.role` is optional and nothing + * renders from it today; it is honoured so a collector can override the split + * later without another dashboard change. + * @param {object} ev + * @param {string} raw seg0 of the row label (falls back to crewEventActor) + */ +function crewActorRole(ev, raw) { + if (ev && ev.role) return String(ev.role); + const token = String(raw || crewEventActor(ev) || '').trim(); + if (token === 'Eng') return crewActorEngRole(ev); + return CREW_ACTOR_MASK[token] || token; +} + +/** + * Badge monogram for a display role. First letter of the first two words, + * single word -> first two chars. Two explicit overrides: Developer and DevOps + * both start `D`+`e`/`DV`-shaped and would otherwise collide on `DE`. + * Distinct from agentInitials(), which monograms a kebab-case agent id for the + * Agents card; this one monograms a spaced display role. + */ +function crewActorInitials(role) { + const special = { Developer: 'DV', DevOps: 'DO' }; + if (special[role]) return special[role]; + const words = String(role || '').trim().split(/\s+/).filter(Boolean); + if (words.length === 0) return '?'; + if (words.length === 1) return words[0].slice(0, 2).toUpperCase(); + return (words[0][0] + words[1][0]).toUpperCase(); +} + +/** + * Verb state icons. The design specified Feather geometry on a viewBox-24 grid; + * these are the same shapes redrawn on the dashboard's own viewBox-16 grid, so + * the file keeps ONE icon geometry (spaceIconSvg, nowMetaIconSvg and the header + * refresh control are all 16). Importing a second grid for six row marks would + * have split the icon system for no legibility gain. + * + * They are row state marks, not chrome: they render at 12px inside a text + * column, so the CSS overrides stroke-width to 1.6 — the 16-grid equivalent of + * the design's 2.4-on-24 optical compensation, since 1.5 at 12px renders + * lighter than 1.5 at the chrome set's 16px. + * Static markup only — the verb is a map key, never concatenated into the SVG. + */ +const CREW_VERB_ICON_PATHS = { + play: '<polygon points="3.5 2.5 12.5 8 3.5 13.5"/>', + pause: '<line x1="6" y1="3.5" x2="6" y2="12.5"/><line x1="10" y1="3.5" x2="10" y2="12.5"/>', + check: '<polyline points="13.5 4 6 11.5 2.5 8"/>', + x: '<line x1="12" y1="4" x2="4" y2="12"/><line x1="4" y1="4" x2="12" y2="12"/>', + 'git-merge': '<circle cx="12" cy="12" r="2"/><circle cx="4" cy="4" r="2"/><path d="M4 14V6a6 6 0 0 0 6 6"/>', + 'circle-slash': '<circle cx="8" cy="8" r="6.5"/><line x1="3.4" y1="3.4" x2="12.6" y2="12.6"/>', + info: '<circle cx="8" cy="8" r="6.5"/><line x1="8" y1="11" x2="8" y2="8"/><line x1="8" y1="5" x2="8.01" y2="5"/>', + flag: '<path d="M2.7 10s.7-.7 2.7-.7 3.3 1.3 5.3 1.3 2.7-.7 2.7-.7V2s-.7.7-2.7.7-3.3-1.3-5.3-1.3-2.7.7-2.7.7z"/><line x1="2.7" y1="14.7" x2="2.7" y2="10"/>', +}; + +/** Verb -> state icon. Replaces the 6px state dot (design v2). */ +const CREW_VERB_ICON = { + running: 'play', + awaiting: 'pause', + done: 'check', + failed: 'x', + merged: 'git-merge', + parked: 'circle-slash', +}; + +/** Decorative row icon; unknown verbs fall back to `info` rather than nothing. */ +function crewRowIconSvg(name, className) { + const paths = CREW_VERB_ICON_PATHS[name] || CREW_VERB_ICON_PATHS.info; + return `<svg class="${className}" viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">${paths}</svg>`; +} + +/** + * Split a Crew row label into the v2 column contract. + * Refs (PR, SHA, worker id, plan) leave the flexible text so they can render as + * fixed-content chips that hide whole instead of ellipsising. Everything left + * over joins the primary field — the row's only wide, only ellipsising column. + * Plan detection prefers `refs.plan` over sniffing the rendered string, since + * the string is capped at MAX_SEMANTIC_LABEL upstream and may not carry it. + */ +function parseCrewRow(ev) { + const segs = String((ev && ev.label) || '').split(' · '); + const planRef = ev && ev.refs && ev.refs.plan ? String(ev.refs.plan) : ''; + const planBase = planRef.replace(/\.plan\.md$/i, ''); + const out = { + actor: segs[0] || '', + verb: segs[1] || '', + mids: [], + pr: null, + sha: null, + worker: null, + plan: planRef || null, + }; + for (let i = 2; i < segs.length; i++) { + const seg = segs[i]; + if (/\.plan\.md$/i.test(seg) || (planRef && (seg === planRef || seg === planBase))) { + out.plan = out.plan || seg; + continue; + } + if (/^PR #\d+/.test(seg)) { + out.pr = seg.replace(/^PR /, ''); + continue; + } + if (/^[0-9a-f]{7,8}$/.test(seg)) { + // A subagent's trailing hex is a worker id, not a commit — same shape, + // different meaning, and only the SHA chip is droppable at ≤560px. + if (ev && ev.kind === 'subagent') out.worker = seg; + else out.sha = seg; + continue; + } + out.mids.push(seg); + } + // A row with no mid segments still needs something in its wide column. + out.primary = out.mids.join(' · ') || planBase || out.verb; + return out; } /** Avatar-style monogram: first letters of the first two kebab/space parts, else first two chars. */ @@ -5164,10 +5489,15 @@ ]; function personaOptionHtml(selected) { - return CONFIG_PERSONA_OPTIONS.map((id) => { - const sel = id === selected ? ' selected' : ''; - return `<option value="${escapeAttr(id)}"${sel}>${escapeHtml(id)}</option>`; - }).join(''); + const inheritSel = selected === '' ? ' selected' : ''; + const inherit = `<option value=""${inheritSel}>Inherit default</option>`; + return ( + inherit + + CONFIG_PERSONA_OPTIONS.map((id) => { + const sel = id === selected ? ' selected' : ''; + return `<option value="${escapeAttr(id)}"${sel}>${escapeHtml(id)}</option>`; + }).join('') + ); } function renderConfigSection(d) { @@ -5201,7 +5531,7 @@ <div class="config-row"> <label for="config-persona-${escapeAttr(mode.id)}">${escapeHtml(mode.label)} persona</label> <select id="config-persona-${escapeAttr(mode.id)}" name="personaMode:${escapeAttr(mode.id)}" data-focus-key="config-persona-${escapeAttr(mode.id)}"> - ${personaOptionHtml(current || (ap.default || 'autopilot'))} + ${personaOptionHtml(current)} </select> </div>`; }).join(''); @@ -5361,7 +5691,9 @@ }; for (const mode of CONFIG_PERSONA_MODES) { const el = document.getElementById(`config-persona-${mode.id}`); - if (el && el.value) payload.agentPersona.modes[mode.id] = el.value; + if (!el) continue; + // Empty = Inherit default: send null so merge deletes a stored override. + payload.agentPersona.modes[mode.id] = el.value ? el.value : null; } return payload; } @@ -5452,7 +5784,7 @@ handoff: { okDetail: 'HANDOFF.md is present and parseable (idle Plan is still healthy).', failDetail: 'HANDOFF.md is missing or not parseable.', - autofix: { text: '/continue-plan', subject: '/continue-plan', destination: 'chatInput', label: 'Autofix' }, + autofix: { text: '/handoff', subject: '/handoff', destination: 'chatInput', label: 'Autofix' }, }, agents: { okDetail: 'Agents are optional in L0; empty .cursor/agents/ is healthy. Cataloged agents (if any) are listed in Agents.', @@ -5468,12 +5800,13 @@ memory: { okDetail: 'Memory has recorded errors and/or decisions.', failDetail: 'Memory index has no errors or decisions yet (optional until first WRITE).', - autofix: { text: '/agent-kit-onboard', subject: '/agent-kit-onboard', destination: 'chatInput', label: 'Autofix' }, + autofix: { text: '.cursor/memory/', subject: 'path .cursor/memory/', destination: 'filePicker', label: 'Copy path' }, }, git: { okDetail: 'Git branch signal is available.', failDetail: 'Git repository signal missing (no branch).', - autofix: { text: 'git status', subject: 'git status', destination: 'terminal', label: 'Autofix' }, + // Covers not-a-repo and git-init-but-zero-commits (rev-parse HEAD fails until first commit). + autofix: { text: 'git init && git commit --allow-empty -m "init"', subject: 'git init && git commit --allow-empty -m "init"', destination: 'terminal', label: 'Autofix' }, }, config: { okDetail: 'Workspace config is loaded.', @@ -5491,23 +5824,28 @@ ]; function healthCheckSeverity(check, aggregateStatus) { + // Per-check levels are ok | warning | degraded only. Aggregate snapshot/serve + // `error` is empty-checks (presence + empty state); producers never emit + // status:error with checks present, so do not invent a per-check error sev. if (check && check.ok) return 'ok'; if (aggregateStatus === 'degraded') return 'degraded'; - if (aggregateStatus === 'error') return 'error'; return 'warning'; } -/* Single severity chrome mapping (ok | warning | degraded | error): tone drives - the dot class, label drives text/aria, token names the [data-sev] CSS color. */ +/* Severity chrome: per-check uses ok | warning | degraded; aggregate `error` + remains for snapshot/serve presence only (empty checks). Tone drives the dot + class; label drives text/aria. Per-check [data-sev] CSS matches the severity + key written on the element, not a chrome field. Aggregate error has no + per-check CSS (producers never emit error+checks). */ const HEALTH_SEVERITY_CHROME = { - ok: { tone: 'green', label: 'ok', token: 'green' }, - warning: { tone: 'yellow', label: 'warn', token: 'yellow' }, - degraded: { tone: 'orange', label: 'degraded', token: 'orange' }, - error: { tone: 'red', label: 'error', token: 'red' }, + ok: { tone: 'green', label: 'ok' }, + warning: { tone: 'yellow', label: 'warn' }, + degraded: { tone: 'orange', label: 'degraded' }, + error: { tone: 'red', label: 'error' }, }; function healthSeverityChrome(sev) { - return HEALTH_SEVERITY_CHROME[sev] || { tone: 'gray', label: sev || 'unknown', token: 'text-muted' }; + return HEALTH_SEVERITY_CHROME[sev] || { tone: 'gray', label: sev || 'unknown' }; } function healthSeverityLabel(sev) { @@ -5526,13 +5864,6 @@ if (btn) btn.setAttribute('aria-expanded', open ? 'true' : 'false'); } -function healthCheckKeydown(event, checkId) { - if (event.key === 'Enter' || event.key === ' ') { - event.preventDefault(); - toggleHealthCheck(checkId); - } -} - // ===== Memory tab: interactive error rows (expand + copy fix prompt) ===== function toggleMemoryError(idx) { const card = document.getElementById('memory-error-' + idx); @@ -5584,19 +5915,6 @@ `; } -/** @deprecated Prefer toggleHealthCheck; kept for any leftover callers. */ -function showHealthInfo(el, ok) { - const parent = el.parentElement; - const existing = parent.querySelector('.health-message'); - if (existing) { existing.remove(); return; } - const msg = document.createElement('div'); - msg.className = 'health-message'; - msg.textContent = ok - ? 'All good: no action needed.' - : 'Check may indicate an issue. Review the relevant logs or configuration.'; - parent.appendChild(msg); -} - // ===== Get connection uptime (Phase 2) ===== function getConnectionUptime() { if (sseMode !== 'live') return ''; @@ -7438,8 +7756,9 @@ <div class="live-activity-header"> <span class="card-title">${spaceIconSvg('monitor')}Crew Monitor</span> <span class="section-subtitle" style="margin-left:auto">${feedEvents.length} shown</span> + <button type="button" class="git-action-btn monitor-density-btn" data-monitor-density-toggle data-focus-key="monitor-density-toggle" aria-pressed="${currentMonitorDensity() === 'comfortable' ? 'true' : 'false'}" title="Toggle Crew Monitor row density (compact single-line rows vs comfortable two-line rows)" onclick="toggleMonitorDensity()">${currentMonitorDensity() === 'comfortable' ? 'Comfortable' : 'Compact'}</button> </div> - <div class="live-activity-feed"> + <div class="live-activity-feed monitor-cq"> ${feedEvents.length === 0 ? renderEmptyStateCta({ headline: 'Listening', @@ -7453,42 +7772,56 @@ const info = semanticEventInfo(ev.kind, ev.refs?.commitType); const gloss = info.gloss || info.tag || ev.kind || 'event'; const staggerIdx = Math.min(idx, 5); - const actor = crewEventActor(ev); - const initials = agentInitials(actor); + const row = parseCrewRow(ev); + const role = crewActorRole(ev, row.actor); + const rawActor = row.actor || crewEventActor(ev); const actionAttrs = activityTargetAttributes(ev, 'monitor-activity', { kindGloss: gloss }); const rowA11y = actionAttrs ? actionAttrs - : ` aria-label="${escapeAttr(`${gloss}: ${ev.label || ''}`)}"`; - // Structured label spans: actor + verb stay fixed; later segments - // shrink, and the low-signal plan filename ellipsises first. - // Prefer refs.plan over sniffing the rendered string (truncation-safe). - const planRef = ev.refs && ev.refs.plan ? String(ev.refs.plan) : ''; - const planBase = planRef.replace(/\.plan\.md$/i, ''); - const feedSegs = String(ev.label || '').split(' · '); - const feedLabelHtml = feedSegs - .map((seg, i) => { - const isPlanSeg = Boolean(planRef) && ( - seg === planRef || - seg === planBase || - /\.plan\.md/i.test(seg) - ); - const cls = i === 0 - ? 'feed-seg feed-seg-actor' - : i === 1 - ? 'feed-seg feed-seg-verb' - : isPlanSeg - ? 'feed-seg feed-seg-plan' - : 'feed-seg feed-seg-mid'; - return `<span class="${cls}">${escapeHtml(seg)}</span>`; - }) - .join('<span class="feed-sep" aria-hidden="true"> · </span>'); - const feedTitle = ev.labelFull || ev.label || ''; + : ` aria-label="${escapeAttr(`${gloss}: ${ev.label || role}`)}"`; + // Row order (design v2, 2026-08-05): team badge, actor, verb, + // primary, ref chips, time. The kind glyph is gone — its colour + // moved onto the badge and its meaning onto the badge tooltip, so + // nothing composes a glyph into the text and a flat single-segment + // label cannot misplace one. `info.bg` tints the badge; that is the + // only place a Crew row carries it. + const badgeHtml = `<span class="monitor-row-badge" style="background:${info.bg};color:${info.color}" title="${escapeAttr(gloss)}" aria-hidden="true">${escapeHtml(crewActorInitials(role))}</span>`; + // Actor tooltip keeps the wire token next to the mask, so an + // operator reading `Tech Lead` can still see it came from `Eng`. + const actorHtml = `<span class="monitor-row-actor" title="${escapeAttr(`${role} (${rawActor})`)}">${escapeHtml(role)}</span>`; + const verbIcon = crewRowIconSvg(CREW_VERB_ICON[row.verb] || 'info', 'monitor-row-verb-icon'); + const verbHtml = row.verb + ? `<span class="monitor-row-verb v-${escapeAttr(row.verb)}" title="${escapeAttr(row.verb)}">${verbIcon}<em>${escapeHtml(row.verb)}</em></span>` + : ''; + // Ref chips: fixed content, dropped whole by the @container + // ladder. Never ellipsised — half a SHA is worse than no SHA. + const refsHtml = [ + row.pr ? `<span class="monitor-row-ref ref-pr">${escapeHtml(row.pr)}</span>` : '', + row.sha ? `<span class="monitor-row-ref ref-sha">${escapeHtml(row.sha)}</span>` : '', + row.worker ? `<span class="monitor-row-ref ref-worker" title="worker id">${escapeHtml(row.worker)}</span>` : '', + row.plan + ? `<span class="monitor-row-plan" title="${escapeAttr(row.plan)}">${crewRowIconSvg('flag', 'monitor-row-plan-flag')}<span class="monitor-row-plan-name">${escapeHtml(row.plan.replace(/\.plan\.md$/i, ''))}</span></span>` + : '', + ].join(''); + // labelFull (+ sourcePath) must be reachable in every layout: + // display `label` is capped at MAX_SEMANTIC_LABEL upstream, so + // no layout is a substitute for it. It rides on .monitor-row- + // primary rather than the row, because an actionable row's own + // title is already spoken for by activityTargetAttributes + // (the copy-path/copy-sha affordance) and a second title + // attribute on the same element is silently dropped. Primary is + // also the only field that ellipsises, so the full text sits on + // exactly the element whose text is cut. + const feedTitle = (ev.labelFull || ev.label || '') + (ev.sourcePath ? `\n${ev.sourcePath}` : ''); + const rowTitle = actionAttrs ? '' : ` title="${escapeAttr(feedTitle)}"`; return ` - <div class="monitor-row stagger-fade stagger-${staggerIdx}"${rowA11y}> - <span class="monitor-row-avatar" title="${escapeAttr(actor)}" aria-hidden="true">${escapeHtml(initials)}</span> - <span class="monitor-row-chip" style="background:${info.bg};color:${info.color}" title="${escapeAttr(gloss)}" aria-hidden="true"><span class="monitor-row-icon">${info.icon}</span></span> - <span class="feed-label" title="${escapeAttr(feedTitle)}">${feedLabelHtml}</span> - <span class="feed-time">${crewEventTime(ev, info)}</span> + <div class="monitor-row stagger-fade stagger-${staggerIdx}"${rowA11y}${rowTitle}> + ${badgeHtml} + ${actorHtml} + ${verbHtml} + <span class="monitor-row-primary" title="${escapeAttr(feedTitle)}">${escapeHtml(row.primary)}</span> + <span class="monitor-row-meta">${refsHtml}</span> + <span class="monitor-row-time">${crewEventTime(ev, info)}</span> </div>`; }).join('')} </div> @@ -7698,7 +8031,7 @@ : `${fixBtn}${autofixBtn}`; return ` <div class="health-card" role="listitem" id="health-card-${escapeAttr(id)}" data-check-id="${escapeAttr(id)}" data-severity="${escapeAttr(sev)}"> - <button type="button" class="health-item" aria-expanded="false" aria-controls="health-detail-${escapeAttr(id)}" aria-label="${escapeAttr(c.label)}: ${escapeAttr(healthSeverityLabel(sev))}" onclick="toggleHealthCheck('${escapeJsString(id)}')" onkeydown="healthCheckKeydown(event, '${escapeJsString(id)}')"> + <button type="button" class="health-item" aria-expanded="false" aria-controls="health-detail-${escapeAttr(id)}" aria-label="${escapeAttr(c.label)}: ${escapeAttr(healthSeverityLabel(sev))}" onclick="toggleHealthCheck('${escapeJsString(id)}')"> <span class="dot ${healthDotClass(sev)}"></span> <span class="health-item-label">${escapeHtml(c.label)}</span> <span class="health-item-sev" data-sev="${escapeAttr(sev)}">${escapeHtml(healthSeverityLabel(sev))}</span> diff --git a/dashboard/lib/broadcast-share.mjs b/dashboard/lib/broadcast-share.mjs new file mode 100644 index 0000000..657738c --- /dev/null +++ b/dashboard/lib/broadcast-share.mjs @@ -0,0 +1,251 @@ +/** + * Cosmetic Mission Kit / BYO share URLs for dashboard-broadcast. + * Fragment-only payloads (never sent to Hostinger access logs). + * ADR: .cursor/memory/decisions/2026-08-11_mission-control-broadcast-url-mask.md + */ + +/** Live Hostinger path uses the `.html` artifact; extensionless `/mc/open` may 404 until a host alias exists. */ +export const DEFAULT_SHARE_BASE = "https://missionkit.io/mc/open.html"; +export const SHARE_BASE_ENV = "MISSION_CONTROL_SHARE_BASE"; +export const SHARE_TTL_ENV = "MISSION_CONTROL_SHARE_TTL_SEC"; +export const SHARE_SHOW_LAN_ENV = "MISSION_CONTROL_SHARE_SHOW_LAN"; +export const DEFAULT_SHARE_TTL_SEC = 86_400; + +/** + * True when hostname is loopback, link-local, or RFC1918 private (IPv4) / ULA (IPv6). + * Used by share-target validation (open redirect harden). + * @param {string} hostname + * @returns {boolean} + */ +export function isPrivateOrLoopbackHostname(hostname) { + const host = String(hostname || "") + .trim() + .toLowerCase() + .replace(/^\[|\]$/g, ""); + if (!host) return false; + if (host === "localhost" || host.endsWith(".localhost") || host.endsWith(".local")) return true; + if (host === "::1" || host === "0:0:0:0:0:0:0:1") return true; + // IPv4 dotted quad + const m = /^(\d{1,3})\.(\d{1,3})\.(\d{1,3})\.(\d{1,3})$/.exec(host); + if (m) { + const a = Number(m[1]); + const b = Number(m[2]); + const c = Number(m[3]); + const d = Number(m[4]); + if ([a, b, c, d].some((n) => !Number.isInteger(n) || n < 0 || n > 255)) return false; + if (a === 127) return true; // loopback + if (a === 10) return true; // 10/8 + if (a === 192 && b === 168) return true; // 192.168/16 + if (a === 172 && b >= 16 && b <= 31) return true; // 172.16/12 + if (a === 169 && b === 254) return true; // link-local + return false; + } + // IPv6 ULA fc00::/7 and link-local fe80::/10 + if (host.includes(":")) { + if (host.startsWith("fc") || host.startsWith("fd")) return true; + if ( + host.startsWith("fe8") || + host.startsWith("fe9") || + host.startsWith("fea") || + host.startsWith("feb") + ) { + return true; + } + } + return false; +} + +/** + * Validate a decoded share target URL (LAN Mission Control with optional ?token=). + * @param {string} url + * @returns {{ ok: true, url: string } | { ok: false, error: string }} + */ +export function validateBroadcastShareTarget(url) { + const raw = typeof url === "string" ? url.trim() : ""; + if (!raw) return { ok: false, error: "invalid-target" }; + let parsed; + try { + parsed = new URL(raw); + } catch { + return { ok: false, error: "invalid-target" }; + } + if (parsed.protocol !== "http:" && parsed.protocol !== "https:") { + return { ok: false, error: "invalid-target" }; + } + if (!isPrivateOrLoopbackHostname(parsed.hostname)) { + return { ok: false, error: "non-private-target" }; + } + return { ok: true, url: raw }; +} + +/** + * Normalize and validate a share page base. Rejects non-HTTPS (except loopback http for local preview). + * @param {string} raw + * @returns {{ ok: true, base: string } | { ok: false, error: string }} + */ +export function normalizeShareBase(raw) { + const trimmed = String(raw || "") + .trim() + .replace(/#.*$/, "") + .replace(/\/$/, ""); + if (!trimmed) return { ok: false, error: "empty-base" }; + let parsed; + try { + parsed = new URL(trimmed); + } catch { + return { ok: false, error: "invalid-base" }; + } + const host = parsed.hostname.toLowerCase(); + const loopback = + host === "localhost" || host === "127.0.0.1" || host === "::1" || host.endsWith(".localhost"); + if (parsed.protocol === "https:") { + return { ok: true, base: trimmed }; + } + if (parsed.protocol === "http:" && loopback) { + return { ok: true, base: trimmed }; + } + return { ok: false, error: "non-https-base" }; +} + +/** + * GET /open.html and /open are the public cosmetic share-resolver shell under broadcast bind. + * All other methods/paths keep the normal token gate. + * @param {string} method + * @param {string} pathname + * @returns {boolean} + */ +export function isPublicBroadcastShareShell(method, pathname) { + return method === "GET" && (pathname === "/open.html" || pathname === "/open"); +} + +/** + * @param {boolean} tokenRequired + * @param {string} method + * @param {string} pathname + * @returns {boolean} + */ +export function shareShellTokenRequired(tokenRequired, method, pathname) { + return Boolean(tokenRequired) && !isPublicBroadcastShareShell(method, pathname); +} + +/** + * Resolve share page base URL. Empty / "0" / "off" / "false" disables masking. + * Non-HTTPS BYO bases (except loopback http) are rejected → masking off (with stderr warn when available). + * @param {NodeJS.ProcessEnv | Record<string, string | undefined>} [env] + * @returns {string | null} + */ +export function resolveShareBase(env = process.env) { + const raw = env?.[SHARE_BASE_ENV]; + if (raw === undefined || raw === null) return DEFAULT_SHARE_BASE; + const trimmed = String(raw).trim(); + if (!trimmed) return null; + const lower = trimmed.toLowerCase(); + if (lower === "0" || lower === "off" || lower === "false" || lower === "none") return null; + const normalized = normalizeShareBase(trimmed); + if (!normalized.ok) { + if (typeof console !== "undefined" && typeof console.warn === "function") { + console.warn( + `[broadcast-share] ${SHARE_BASE_ENV} rejected (${normalized.error}); printing LAN URL only. Use HTTPS (or loopback http) or set off.`, + ); + } + return null; + } + return normalized.base; +} + +/** + * @param {NodeJS.ProcessEnv | Record<string, string | undefined>} [env] + * @returns {number} + */ +export function resolveShareTtlSec(env = process.env) { + const raw = env?.[SHARE_TTL_ENV]; + if (raw === undefined || raw === null || String(raw).trim() === "") { + return DEFAULT_SHARE_TTL_SEC; + } + const n = Number.parseInt(String(raw), 10); + if (!Number.isFinite(n) || n < 0) return DEFAULT_SHARE_TTL_SEC; + return n; +} + +/** + * When false, starter omits secondary LAN/Local URL lines (share + token only). + * @param {NodeJS.ProcessEnv | Record<string, string | undefined>} [env] + */ +export function resolveShareShowLan(env = process.env) { + const raw = env?.[SHARE_SHOW_LAN_ENV]; + if (raw === undefined || raw === null || String(raw).trim() === "") return true; + const lower = String(raw).trim().toLowerCase(); + return !(lower === "0" || lower === "off" || lower === "false" || lower === "no"); +} + +/** + * @param {string} lanUrl full http URL including ?token= + * @param {{ ttlSec?: number, nowSec?: number }} [opts] + * @returns {string} fragment without leading '#' + */ +export function encodeBroadcastSharePayload(lanUrl, opts = {}) { + const url = typeof lanUrl === "string" ? lanUrl.trim() : ""; + const target = validateBroadcastShareTarget(url); + if (!target.ok) { + throw new Error(`encodeBroadcastSharePayload: ${target.error}`); + } + const ttlSec = opts.ttlSec ?? DEFAULT_SHARE_TTL_SEC; + /** @type {{ v: number, u: string, e?: number }} */ + const body = { v: 1, u: target.url }; + if (ttlSec > 0) { + const now = opts.nowSec ?? Math.floor(Date.now() / 1000); + body.e = now + ttlSec; + } + return `v1.${Buffer.from(JSON.stringify(body), "utf8").toString("base64url")}`; +} + +/** + * @param {string} lanUrl + * @param {{ base?: string | null, ttlSec?: number, nowSec?: number }} [opts] + * @returns {string | null} + */ +export function buildBroadcastShareUrl(lanUrl, opts = {}) { + const base = opts.base === undefined ? DEFAULT_SHARE_BASE : opts.base; + if (!base) return null; + const cleaned = String(base).replace(/#.*$/, "").replace(/\/$/, ""); + const frag = encodeBroadcastSharePayload(lanUrl, opts); + return `${cleaned}#${frag}`; +} + +/** + * @param {string} fragment hash with or without leading '#' + * @param {{ nowSec?: number }} [opts] + * @returns + * | { ok: true, url: string, expiresAt: number | null } + * | { ok: false, error: string } + */ +export function decodeBroadcastShareFragment(fragment, opts = {}) { + const raw = String(fragment || "") + .replace(/^#/, "") + .trim(); + if (!raw) return { ok: false, error: "missing-fragment" }; + const m = /^v1\.([A-Za-z0-9_-]+)$/.exec(raw); + if (!m) return { ok: false, error: "unsupported-version" }; + let parsed; + try { + const json = Buffer.from(m[1], "base64url").toString("utf8"); + parsed = JSON.parse(json); + } catch { + return { ok: false, error: "invalid-payload" }; + } + if (!parsed || typeof parsed !== "object" || parsed.v !== 1) { + return { ok: false, error: "unsupported-version" }; + } + const url = typeof parsed.u === "string" ? parsed.u.trim() : ""; + const target = validateBroadcastShareTarget(url); + if (!target.ok) return { ok: false, error: target.error }; + let expiresAt = null; + if (parsed.e !== undefined && parsed.e !== null) { + const e = Number(parsed.e); + if (!Number.isFinite(e)) return { ok: false, error: "invalid-expiry" }; + expiresAt = e; + const now = opts.nowSec ?? Math.floor(Date.now() / 1000); + if (now > e) return { ok: false, error: "expired" }; + } + return { ok: true, url: target.url, expiresAt }; +} diff --git a/dashboard/lib/guards.d.mts b/dashboard/lib/guards.d.mts new file mode 100644 index 0000000..8c13249 --- /dev/null +++ b/dashboard/lib/guards.d.mts @@ -0,0 +1,10 @@ +/** Ambient types for dashboard/lib/guards.mjs (consumed by CLI TypeScript). */ + +export function resolveContextConfigPath( + repoRoot: string, + fsHooks?: { + existsSync?: (path: string) => boolean; + realpathSync?: (path: string) => string; + mkdirSync?: (path: string, opts?: { recursive?: boolean }) => void; + }, +): { ok: true; path: string } | { ok: false; error: string }; diff --git a/dashboard/lib/guards.mjs b/dashboard/lib/guards.mjs index 8d86126..ac828d2 100644 --- a/dashboard/lib/guards.mjs +++ b/dashboard/lib/guards.mjs @@ -675,12 +675,17 @@ export function validateConfigWriteBody(body) { if (!modes || typeof modes !== "object" || Array.isArray(modes)) { return { ok: false, error: "agentPersona.modes must be an object" }; } - /** @type {Record<string, string>} */ + /** @type {Record<string, string | null>} */ const modesPatch = {}; for (const [mode, persona] of Object.entries(modes)) { if (!CONFIG_PERSONA_MODES.includes(mode)) { return { ok: false, error: `unknown agentPersona.modes key: ${mode}` }; } + // null clears an existing mode override (Inherit default). + if (persona === null) { + modesPatch[mode] = null; + continue; + } if (typeof persona !== "string" || !CONFIG_PERSONA_IDS.includes(persona)) { return { ok: false, error: `agentPersona.modes.${mode} must be a builtin persona id` }; } @@ -767,7 +772,15 @@ export function mergeConfigAllowlist(existing, patch) { prev.modes && typeof prev.modes === "object" && !Array.isArray(prev.modes) ? { ...prev.modes } : {}; - next.modes = { ...prevModes, ...patch.agentPersona.modes }; + const nextModes = { ...prevModes }; + for (const [mode, persona] of Object.entries(patch.agentPersona.modes)) { + if (persona === null) { + delete nextModes[mode]; + } else { + nextModes[mode] = persona; + } + } + next.modes = nextModes; } base.agentPersona = next; } diff --git a/dashboard/lib/open-browser.d.mts b/dashboard/lib/open-browser.d.mts new file mode 100644 index 0000000..1c069e9 --- /dev/null +++ b/dashboard/lib/open-browser.d.mts @@ -0,0 +1,31 @@ +import type { spawn, spawnSync } from "node:child_process"; +import type { readFileSync } from "node:fs"; + +export const OS_DEFAULT_TOKENS: Set<string>; +export function isSafePreferredBrowser(value: string): boolean; +export function shouldSkipOpen(env?: NodeJS.ProcessEnv): boolean; +export function normalizePreferredBrowser(value: unknown): string | null; +export function resolvePreferredBrowser(opts?: { + env?: NodeJS.ProcessEnv; + configValue?: unknown; +}): string | null; +export function readPreferredBrowserFromConfig( + configPath: string, + fsHooks?: { readFileSync?: typeof readFileSync }, +): unknown; +export function buildOpenBrowserCommand(opts: { + url: string; + preferred?: string | null; + platform?: NodeJS.Platform; +}): { command: string; args: string[] } | null; +export function openBrowser( + url: string, + options?: { + env?: NodeJS.ProcessEnv; + preferred?: string | null; + configValue?: unknown; + platform?: NodeJS.Platform; + spawnFn?: typeof spawn; + spawnSyncFn?: typeof spawnSync; + }, +): { opened: boolean; reason?: string; command?: string; args?: string[] }; diff --git a/dashboard/lib/open-browser.mjs b/dashboard/lib/open-browser.mjs new file mode 100644 index 0000000..52c0789 --- /dev/null +++ b/dashboard/lib/open-browser.mjs @@ -0,0 +1,298 @@ +/** + * Shared Mission Control browser open helper. + * + * Preference resolution (highest wins): + * 1. env MISSION_CONTROL_PREFERRED_BROWSER + * 2. config missionControl.preferredBrowser (passed in by caller) + * 3. OS default handler (null preferred) + * + * Skips open when MISSION_CONTROL_NO_OPEN=1. + * Never opens more than one process per call (preferred may fall back once). + * + * Trust boundary: preferredBrowser is an app/binary *name*, not a path or + * shell expression. Values with path separators or shell metacharacters are + * rejected and treated as OS default. + * + * ADR: .cursor/memory/decisions/2026-08-11_mission-control-preferred-browser.md + */ + +import { spawn, spawnSync } from "node:child_process"; +import { readFileSync } from "node:fs"; +import { platform as osPlatform } from "node:os"; + +/** Sentinel values that mean "use OS default" (and slash-only Ask). */ +export const OS_DEFAULT_TOKENS = new Set(["", "default", "os", "ask"]); + +/** + * Reject path separators, absolute/relative path forms, and shell metacharacters. + * Allowed examples: "Google Chrome", "firefox", "msedge", "Brave Browser". + * + * @param {string} value + * @returns {boolean} + */ +export function isSafePreferredBrowser(value) { + if (typeof value !== "string") return false; + const s = value.trim(); + if (!s) return false; + if (/[/\\]/.test(s)) return false; + // biome-ignore lint/suspicious/noControlCharactersInRegex: intentional reject of C0/DEL in browser names + if (/[\0-\x1f\x7f]/.test(s)) return false; + if (/[$`;&|<>(){}[\]!*?#~"'%^=,+]/.test(s)) return false; + if (s.includes(":")) return false; + if (/^-/.test(s)) return false; + return true; +} + +/** + * @param {NodeJS.ProcessEnv} [env] + * @returns {boolean} + */ +export function shouldSkipOpen(env = process.env) { + return env.MISSION_CONTROL_NO_OPEN === "1"; +} + +/** + * @param {unknown} value + * @returns {string | null} trimmed app/binary name, or null for OS default + */ +export function normalizePreferredBrowser(value) { + if (value == null) return null; + const s = String(value).trim(); + if (!s || OS_DEFAULT_TOKENS.has(s.toLowerCase())) return null; + if (!isSafePreferredBrowser(s)) return null; + return s; +} + +/** + * @param {{ env?: NodeJS.ProcessEnv, configValue?: unknown }} [opts] + * @returns {string | null} + */ +export function resolvePreferredBrowser(opts = {}) { + const env = opts.env ?? process.env; + const fromEnv = env.MISSION_CONTROL_PREFERRED_BROWSER; + if (fromEnv != null && String(fromEnv).trim() !== "") { + return normalizePreferredBrowser(fromEnv); + } + return normalizePreferredBrowser(opts.configValue); +} + +/** + * Read missionControl.preferredBrowser from a context config.json path. + * Missing/invalid file → null (OS default). Does not create the file. + * + * @param {string} configPath + * @param {{ readFileSync?: typeof readFileSync }} [fsHooks] + * @returns {unknown} + */ +export function readPreferredBrowserFromConfig(configPath, fsHooks = {}) { + const read = fsHooks.readFileSync ?? readFileSync; + try { + const raw = read(configPath, "utf8"); + const data = JSON.parse(raw); + if (!data || typeof data !== "object" || Array.isArray(data)) return null; + const mc = data.missionControl; + if (!mc || typeof mc !== "object" || Array.isArray(mc)) return null; + return mc.preferredBrowser ?? null; + } catch { + return null; + } +} + +/** + * Build an argv for a single open attempt (hermetic: no spawn). + * + * @param {{ + * url: string, + * preferred?: string | null, + * platform?: NodeJS.Platform, + * }} opts + * @returns {{ command: string, args: string[] } | null} + */ +export function buildOpenBrowserCommand(opts) { + const url = opts.url; + if (typeof url !== "string" || !url.trim()) return null; + const preferred = normalizePreferredBrowser(opts.preferred ?? null); + const os = opts.platform ?? osPlatform(); + + if (os === "darwin") { + if (preferred) { + return { command: "open", args: ["-a", preferred, url] }; + } + return { command: "open", args: [url] }; + } + + if (os === "win32") { + if (preferred) { + // `start` treats the first quoted arg as window title; pass empty title. + return { command: "cmd", args: ["/c", "start", "", preferred, url] }; + } + return { command: "cmd", args: ["/c", "start", "", url] }; + } + + // Linux / other: preferred is a binary or command name; else xdg-open. + if (preferred) { + return { command: preferred, args: [url] }; + } + return { command: "xdg-open", args: [url] }; +} + +/** + * @param {import("node:child_process").ChildProcess | { on?: Function, unref?: Function } | null | undefined} child + */ +function attachErrorSwallow(child) { + if (child && typeof child.on === "function") { + child.on("error", () => { + /* prevent unhandled 'error' (ENOENT) from killing the launcher */ + }); + } + if (child && typeof child.unref === "function") { + child.unref(); + } +} + +/** + * Open one browser for the URL. Returns whether a process was spawned. + * When a preferred open fails, falls back once to the OS default opener. + * + * @param {string} url + * @param {{ + * env?: NodeJS.ProcessEnv, + * preferred?: string | null, + * configValue?: unknown, + * platform?: NodeJS.Platform, + * spawnFn?: typeof spawn, + * spawnSyncFn?: typeof spawnSync, + * }} [options] + * @returns {{ opened: boolean, reason?: string, command?: string, args?: string[] }} + */ +export function openBrowser(url, options = {}) { + const env = options.env ?? process.env; + if (shouldSkipOpen(env)) { + return { opened: false, reason: "no-open" }; + } + + const preferred = + options.preferred !== undefined + ? normalizePreferredBrowser(options.preferred) + : resolvePreferredBrowser({ env, configValue: options.configValue }); + + const platform = options.platform ?? osPlatform(); + const spawnFn = options.spawnFn ?? spawn; + const spawnSyncFn = options.spawnSyncFn; + + /** + * @param {{ command: string, args: string[] }} built + * @returns {{ opened: boolean, reason?: string, command: string, args: string[] }} + */ + function runDetached(built) { + try { + const child = spawnFn(built.command, built.args, { detached: true, stdio: "ignore" }); + attachErrorSwallow(child); + return { opened: true, command: built.command, args: built.args }; + } catch { + return { + opened: false, + reason: "spawn-failed", + command: built.command, + args: built.args, + }; + } + } + + /** + * Preferred open: detect failure before claiming success, then caller may fall back. + * Hermetic tests that only inject spawnFn use the detached path (throw = fail). + * + * @param {{ command: string, args: string[] }} built + * @returns {{ opened: boolean, reason?: string, command: string, args: string[] }} + */ + function runPreferred(built) { + if (options.spawnFn && !spawnSyncFn) { + return runDetached(built); + } + + const sync = spawnSyncFn ?? spawnSync; + + if (platform !== "darwin" && platform !== "win32") { + // Long-lived browser binaries: probe PATH, then detach (do not spawnSync the app). + const probe = sync("which", [built.command], { encoding: "utf8" }); + if (probe.error || (typeof probe.status === "number" && probe.status !== 0)) { + return { + opened: false, + reason: "spawn-failed", + command: built.command, + args: built.args, + }; + } + return runDetached(built); + } + + // darwin `open` / win32 `cmd /c start` exit quickly. + try { + const result = sync(built.command, built.args, { + encoding: "utf8", + windowsHide: true, + }); + if (result.error || (typeof result.status === "number" && result.status !== 0)) { + return { + opened: false, + reason: "spawn-failed", + command: built.command, + args: built.args, + }; + } + return { opened: true, command: built.command, args: built.args }; + } catch { + return { + opened: false, + reason: "spawn-failed", + command: built.command, + args: built.args, + }; + } + } + + const built = buildOpenBrowserCommand({ + url, + preferred, + platform, + }); + if (!built) { + return { opened: false, reason: "invalid-url" }; + } + + if (!preferred) { + // Same failure detection as preferred opens (probe / spawnSync) so OS-default + // missing handlers are not reported as opened:true. + return runPreferred(built); + } + + const prefResult = runPreferred(built); + if (prefResult.opened) { + return prefResult; + } + + const fallback = buildOpenBrowserCommand({ + url, + preferred: null, + platform, + }); + if (!fallback) { + return { opened: false, reason: "invalid-url" }; + } + const fb = runPreferred(fallback); + if (fb.opened) { + return { + opened: true, + command: fb.command, + args: fb.args, + reason: "preferred-fallback", + }; + } + return { + opened: false, + reason: "spawn-failed", + command: built.command, + args: built.args, + }; +} diff --git a/dashboard/lib/semantic-model.mjs b/dashboard/lib/semantic-model.mjs index 436abb7..482e80f 100644 --- a/dashboard/lib/semantic-model.mjs +++ b/dashboard/lib/semantic-model.mjs @@ -24,10 +24,16 @@ export const MONITOR_FEED_CAP = 20; /** Cap agent_step rows emitted per active plan for the denser Crew feed. */ export const MONITOR_AGENT_STEP_EMIT_CAP = 12; +/** Cap subagent-run rows emitted per snapshot (fs scan bounds live in dashboard-data.mjs). */ +export const MONITOR_SUBAGENT_EMIT_CAP = 8; +/** Cap plan_review pointer rows emitted per snapshot. */ +export const MONITOR_PLAN_REVIEW_EMIT_CAP = 4; + /** * Monitor hero curated subset over the semantic activity stream. * Live agent steps: run_plan / handoff / delivery plus agent_step (Task/orchestrator - * to-do steps). plan_progress milestones stay on Activity / Checklist. + * to-do steps), subagent (Task worker lifecycle) and plan_review (background + * mid-batch review pointers). plan_progress milestones stay on Activity / Checklist. * Activity (Phase 2) is the superset; inventory kinds are excluded here. */ export const MONITOR_ACTIVITY_KINDS = Object.freeze([ @@ -35,6 +41,8 @@ export const MONITOR_ACTIVITY_KINDS = Object.freeze([ "handoff", "delivery", "agent_step", + "subagent", + "plan_review", ]); /** @@ -2152,9 +2160,12 @@ export function formatDeliveryActivity(logLines, { plans = [], limit = MAX_GIT_A const kitAgent = normalizeKitAgentId(agent); const actor = briefActivityActor(kitAgent, { kind: "delivery", plan: planName }); const prBit = `PR #${entry.pr}`; + // Verb `merged` (not `shipped`, retired 2026-08-05): the row is derived from + // a merge/squash entry, and `shipped` implied a prod promote /git-staging + // never performed. const label = brief - ? `${actor} \u00b7 shipped \u00b7 ${brief} \u00b7 ${prBit} \u00b7 ${entry.sha}` - : `${actor} \u00b7 shipped \u00b7 ${prBit} \u00b7 ${entry.sha}`; + ? `${actor} \u00b7 merged \u00b7 ${brief} \u00b7 ${prBit} \u00b7 ${entry.sha}` + : `${actor} \u00b7 merged \u00b7 ${prBit} \u00b7 ${entry.sha}`; const commitType = parseDeliveryCommitType(brief, { hasPr: entry.pr != null }); events.push({ id: activityId("delivery", ["merge", String(entry.pr)]), @@ -2173,18 +2184,23 @@ export function formatDeliveryActivity(logLines, { plans = [], limit = MAX_GIT_A /** * Actor segment for Monitor return-brief labels. - * Kit agent id, else Engineering Manager for delivery, else Squad when a plan - * is present (never the full plan filename), else Platform Engineer. - * Default software lexicon display masks (resolution kinds unchanged). + * Kit agent id, else `Eng` for delivery, else `SQ` when a plan is present + * (never the full plan filename), else `Eng`. + * + * Short display masks from the operator lexicon (2026-08-05): Engineering + * Manager -> Eng, Squad -> SQ, Platform Engineer -> Eng. `Eng` is a documented + * collision between the delivery and system fallbacks; the resolution keys + * (`orchestrator` / `crew` / `system`) and the row's kind glyph stay distinct. + * ADR: decisions/2026-07-27_crew-monitor-vs-plan-monitor-glossary.md. * @param {string|null|undefined} agent * @param {{ kind?: string, plan?: string|null }} [opts] */ export function briefActivityActor(agent, { kind, plan } = {}) { const kit = normalizeKitAgentId(agent); if (kit) return kit; - if (kind === "delivery") return "Engineering Manager"; - if (plan) return "Squad"; - return "Platform Engineer"; + if (kind === "delivery") return "Eng"; + if (plan) return "SQ"; + return "Eng"; } /** @@ -2316,6 +2332,191 @@ export function formatPlanHandoffActivity({ now, handoff, plans }) { return events; } +/** + * Task subagent transcripts are `<uuid>.jsonl` inside a parent chat's + * `subagents/` directory. + */ +export const SUBAGENT_TRANSCRIPT_FILE_RE = /^([0-9a-fA-F][0-9a-fA-F-]{7,})\.jsonl$/; + +/** + * Worker-prompt fields the kit's own dispatch template declares (see + * `.cursor/commands/run-plan.md`). Both forms occur in real dispatches: the + * bare `To-do id: x` of the plain template and the `- **worker_type:** x` of a + * bulleted orchestrator prompt, so the leading list marker and the markdown + * emphasis on either side of the colon are optional. The captured value + * excludes `*` and a backtick so `**explore**` and `` `explore` `` yield + * `explore` rather than the decoration. + */ +const SUBAGENT_TODO_ID_RE = + /^[ \t]*(?:[-*][ \t]*)?\**To-?do id\**[ \t]*[:=][ \t]*\**[ \t]*([^\s*`]+)/im; +const SUBAGENT_WORKER_TYPE_RE = + /^[ \t]*(?:[-*][ \t]*)?\**(?:worker_type(?:[ \t]*\/[ \t]*subagent_type)?|subagent_type)\**[ \t]*[:=][ \t]*\**[ \t]*([^\s*`]+)/im; + +/** Plain-text content of a transcript entry (user prompt or assistant reply). */ +function subagentEntryText(entry) { + const content = entry?.message?.content; + if (typeof content === "string") return content; + if (!Array.isArray(content)) return ""; + const parts = []; + for (const c of content) { + if (c && c.type === "text" && typeof c.text === "string") parts.push(c.text); + } + return parts.join("\n"); +} + +/** + * Lifecycle of one Task subagent run, from the two records that carry it: the + * dispatch prompt (first entry) and the terminal record (last entry). + * + * Phase contract: a transcript whose last record is not `turn_ended` is still + * `running`; `turn_ended` with `status: "success"` is `done`; any other status + * (including `error`) is `failed`. A transcript that is empty or entirely + * unparsable yields `null` rather than a phantom running row. + * + * The fs half (directory layout, recency window, file/byte caps) lives in + * `dashboard-data.mjs` next to the agent-prompt scan contract. Transcript paths + * live under `$HOME`, never in the repo, so no `sourcePath` is emitted. + * + * @param {{ id?: string, parentId?: string|null, firstLine?: string, lastLine?: string, modifiedAt?: string|null }} input + * @returns {{ id: string, parentId: string|null, phase: 'running'|'done'|'failed', todoId: string|null, workerType: string|null, modifiedAt: string|null }|null} + */ +export function parseSubagentRun({ + id, + parentId = null, + firstLine = "", + lastLine = "", + modifiedAt = null, +} = {}) { + const runId = String(id || "").trim(); + if (!runId) return null; + + let first = null; + let last = null; + try { + first = firstLine ? JSON.parse(firstLine) : null; + } catch { + first = null; + } + try { + last = lastLine ? JSON.parse(lastLine) : null; + } catch { + last = null; + } + if (!first && !last) return null; + + let phase = "running"; + if (last && last.type === "turn_ended") { + phase = last.status === "success" ? "done" : "failed"; + } + + const promptText = first && first.role === "user" ? subagentEntryText(first) : ""; + const todoMatch = promptText ? SUBAGENT_TODO_ID_RE.exec(promptText) : null; + const typeMatch = promptText ? SUBAGENT_WORKER_TYPE_RE.exec(promptText) : null; + const rawTodo = todoMatch ? todoMatch[1] : null; + const rawType = typeMatch ? typeMatch[1] : null; + // The template writes literal placeholders when a field is unset; those are + // not identities and must not reach a row. + const placeholder = /^(?:<.*>|none|n\/a|-{1,2})$/i; + return { + id: runId, + parentId: parentId ? String(parentId) : null, + phase, + todoId: rawTodo && !placeholder.test(rawTodo) ? rawTodo : null, + workerType: rawType && !placeholder.test(rawType) ? rawType : null, + modifiedAt: modifiedAt || null, + }; +} + +/** + * Live Crew Monitor rows for Task subagent runs (start / still running / + * complete / failed). Newest first; the caller passes an already-bounded list. + * + * Deliberately a distinct kind from `agent_step`: `agent_step` is derived from + * plan to-do status, so a subagent that runs without flipping a to-do would be + * invisible there and a to-do flipped by hand would be misattributed to a + * worker. ADR: decisions/2026-07-27_crew-monitor-vs-plan-monitor-glossary.md. + * + * @param {object[]} runs - `parseSubagentRun` output + * @param {{ limit?: number }} [opts] + */ +export function formatSubagentActivity(runs, { limit = MONITOR_SUBAGENT_EMIT_CAP } = {}) { + const events = []; + for (const run of runs || []) { + if (events.length >= limit) break; + if (!run || !run.id) continue; + const kitAgent = normalizeKitAgentId(run.workerType); + // Display actor is the dispatched worker type whenever the prompt declared + // one: a built-in type such as `explore` is a real worker identity even + // though it is not a `.cursor/agents/` id. `agent` stays kit-id-only so + // downstream attribution is unchanged. `Dev` is the operator-lexicon mask + // for Developer / Full-Stack Developer, used when no type was declared. + const actor = run.workerType ? truncateStr(String(run.workerType), 24) : "Dev"; + const shortId = String(run.id).slice(0, 8); + const subject = run.todoId || "task"; + const visible = `${actor} · ${run.phase} · ${subject} · ${shortId}`; + events.push({ + id: activityId("subagent", [run.id, run.phase]), + kind: "subagent", + at: run.modifiedAt || null, + agent: kitAgent, + label: truncateStr(visible, MAX_SEMANTIC_LABEL), + labelFull: visible, + // Transcripts live outside the repo (under $HOME); no repo path to copy. + sourcePath: null, + refs: { subagent: run.id, parent: run.parentId || null, phase: run.phase, todo: run.todoId }, + }); + } + return events; +} + +/** + * Crew Monitor pointer rows for background mid-batch plan reviews. + * + * The operator cannot otherwise see that a review ran: `plan-monitor-*.md` lands + * silently in `.cursor/memory/` and only surfaces once Flight Log / attention + * picks it up. These rows say a review exists and whether it is still owed + * triage. They are pointers only — Flight Log and the attention inbox keep sole + * ownership of triage state and actions, and a row never marks anything + * reviewed. Boundary amend recorded in the glossary ADR (2026-08-05). + * + * @param {object[]} reports - `parseExternalReport` output + * @param {object[]} plans - plan records from the snapshot + * @param {{ limit?: number }} [opts] + */ +export function formatPlanReviewActivity( + reports, + plans, + { limit = MONITOR_PLAN_REVIEW_EMIT_CAP } = {}, +) { + const sorted = (reports || []) + .filter((r) => r?.file) + .slice() + .sort((a, b) => String(b.modifiedAt || "").localeCompare(String(a.modifiedAt || ""))); + + const events = []; + for (const report of sorted) { + if (events.length >= limit) break; + const triaged = isReportTriaged(report, plans); + // `awaiting` reuses the existing gate verb: the review itself has landed, + // what is outstanding is the operator's triage. + const verb = triaged ? "done" : "awaiting"; + // `QA` is the operator-lexicon mask for QA Engineer. + const planRef = report.reviewedPlanFile || `${report.slug}.plan.md`; + const visible = `QA · ${verb} · review · ${planRef}`; + events.push({ + id: activityId("plan_review", [report.file, triaged ? "triaged" : "open"]), + kind: "plan_review", + at: report.modifiedAt || null, + agent: null, + label: truncateStr(visible, MAX_SEMANTIC_LABEL), + labelFull: visible, + sourcePath: report.path || null, + refs: { plan: report.reviewedPlanFile || null, report: report.file, triaged }, + }); + } + return events; +} + /** * Explicit run-plan loop lines in terminal output. Shared detection for the * Crew feed (formatTerminalRunEvidence) and the busy-outside-plan derivation @@ -3562,6 +3763,7 @@ export function buildMissionControlView({ deferredCheckIds = [], agentPrompts = [], externalReports = [], + subagentRuns = [], dismissedIds = [], archivedPlanFiles = [], agents = [], @@ -3615,7 +3817,11 @@ export function buildMissionControlView({ const activity = mergeActivity([ planEvents.filter((e) => e.kind === "run_plan" || e.kind === "handoff"), planEvents.filter((e) => e.kind === "agent_step"), + // Live Task-worker lifecycle before delivery: a running subagent is the + // freshest thing on the board and must not be starved by MAX_ACTIVITY. + formatSubagentActivity(subagentRuns), deliveryEvents, + formatPlanReviewActivity(externalReports, plans), planEvents.filter((e) => e.kind === "plan_progress"), formatGitActivity(gitLogLines, { excludeShas: supersededShas }), formatTerminalRunEvidence(terminals), diff --git a/dashboard/open.html b/dashboard/open.html new file mode 100644 index 0000000..fc71099 --- /dev/null +++ b/dashboard/open.html @@ -0,0 +1,213 @@ +<!DOCTYPE html> +<html lang="en"> + <head> + <meta charset="utf-8" /> + <meta name="viewport" content="width=device-width, initial-scale=1" /> + <meta name="robots" content="noindex,nofollow" /> + <title>Mission Control share + + + +
+

Mission Control

+

Opening your local Mission Control…

+ +

+ Cosmetic share link only. You must be on the same trusted LAN (or VPN) as the host. + Treat the full Share URL as a secret (it embeds the session token in the fragment). + Only private/loopback LAN targets are accepted. +

+
+ + + diff --git a/dashboard/serve.mjs b/dashboard/serve.mjs index 24c1ba5..fdc6031 100644 --- a/dashboard/serve.mjs +++ b/dashboard/serve.mjs @@ -10,6 +10,7 @@ import { existsSync, mkdirSync, readFileSync, realpathSync, watch, writeFileSync import { createServer } from "node:http"; import { dirname, extname, join } from "node:path"; import { fileURLToPath } from "node:url"; +import { shareShellTokenRequired } from "./lib/broadcast-share.mjs"; import { DEFAULT_HOST, REPO_ROOT_ENV, @@ -423,8 +424,10 @@ const server = createServer((req, res) => { return; } + // Cosmetic share resolver shell (fragment holds LAN+token client-side). + // ADR: 2026-08-11_mission-control-broadcast-url-mask.md const auth = authorizeMissionControlRequest(req, url, { - tokenRequired: TOKEN_REQUIRED, + tokenRequired: shareShellTokenRequired(TOKEN_REQUIRED, req.method || "GET", path), expectedToken: AUTH_TOKEN, }); if (!auth.ok) { @@ -496,7 +499,8 @@ const server = createServer((req, res) => { return; } - const staticPath = resolveStaticPath(path); + const staticLookup = path === "/open" ? "/open.html" : path; + const staticPath = resolveStaticPath(staticLookup); if (!staticPath) { res.writeHead(404); res.end("Not found"); diff --git a/dashboard/start-broadcast.mjs b/dashboard/start-broadcast.mjs index 9c1ef07..bdceba6 100644 --- a/dashboard/start-broadcast.mjs +++ b/dashboard/start-broadcast.mjs @@ -9,9 +9,14 @@ import { execFileSync, execSync, spawn } from "node:child_process"; import { existsSync, openSync } from "node:fs"; -import { platform } from "node:os"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; +import { + buildBroadcastShareUrl, + resolveShareBase, + resolveShareShowLan, + resolveShareTtlSec, +} from "./lib/broadcast-share.mjs"; import { BROADCAST_TOKEN_ENV, escapePerlDoubleQuoted, @@ -21,10 +26,15 @@ import { listLanIPv4Addresses, normalizeAuthToken, resolveBindHost, + resolveContextConfigPath, + resolveSnapshotRepoRoot, } from "./lib/guards.mjs"; +import { openBrowser, readPreferredBrowserFromConfig } from "./lib/open-browser.mjs"; const __dirname = dirname(fileURLToPath(import.meta.url)); -const ROOT = join(__dirname, ".."); +const KIT_ROOT = join(__dirname, ".."); +/** Workspace snapshots / preference config. Defaults to KIT_ROOT. */ +const ROOT = resolveSnapshotRepoRoot(process.env, KIT_ROOT); 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); @@ -97,7 +107,7 @@ function detachStart(env) { if (hasSetsid()) { const out = openSync(LOG, "a"); const child = spawn("setsid", ["node", SERVE], { - cwd: ROOT, + cwd: KIT_ROOT, detached: true, stdio: ["ignore", out, out], env, @@ -107,7 +117,7 @@ function detachStart(env) { } // Escape @/$ so scoped package paths (node_modules/@scope/...) survive Perl qq. - const rootEsc = escapePerlDoubleQuoted(ROOT); + const rootEsc = escapePerlDoubleQuoted(KIT_ROOT); const serveEsc = escapePerlDoubleQuoted(SERVE); const logEsc = escapePerlDoubleQuoted(LOG); const hostEsc = escapePerlDoubleQuoted(String(env.HOST)); @@ -129,7 +139,7 @@ function detachStart(env) { ].join(" "); const child = spawn("perl", ["-e", perl], { - cwd: ROOT, + cwd: KIT_ROOT, detached: true, stdio: "ignore", env, @@ -148,24 +158,6 @@ async function waitReady(urls) { return null; } -function openBrowser(url) { - const os = platform(); - try { - if (os === "darwin") { - spawn("open", [url], { detached: true, stdio: "ignore" }).unref(); - return true; - } - if (os === "win32") { - spawn("cmd", ["/c", "start", "", url], { detached: true, stdio: "ignore" }).unref(); - return true; - } - spawn("xdg-open", [url], { detached: true, stdio: "ignore" }).unref(); - return true; - } catch { - return false; - } -} - async function main() { const { env, host, token } = resolveBroadcastEnv(); if (isLoopbackBindHost(host)) { @@ -203,14 +195,43 @@ async function main() { console.log(`Mission Control broadcast already listening on port ${PORT}`); } + const shareBase = resolveShareBase(process.env); + let shareUrl = null; + if (shareBase != null) { + try { + shareUrl = buildBroadcastShareUrl(displayUrl, { + base: shareBase, + ttlSec: resolveShareTtlSec(process.env), + }); + } catch (err) { + // Non-RFC1918 primary LAN (Tailscale 100.64/10, public/DMZ) cannot encode into + // the share fragment allowlist — degrade to LAN/token print instead of exit 1. + const msg = err instanceof Error ? err.message : String(err); + console.warn(`Share URL skipped (${msg}). Printing LAN/token only.`); + shareUrl = null; + } + } + const showLan = resolveShareShowLan(process.env); + const openTarget = shareUrl || displayUrl; + console.log(""); console.log(" Mission Control (LAN broadcast)"); console.log(` Bind: ${host}:${PORT}`); + if (shareUrl) { + console.log(` Share: ${shareUrl}`); + } console.log(` Token: ${token}`); - for (const ip of listLanIPv4Addresses()) { - console.log(` LAN: http://${ip}:${PORT}/?token=${encodeURIComponent(token)}`); + if (showLan || !shareUrl) { + for (const ip of listLanIPv4Addresses()) { + console.log(` LAN: http://${ip}:${PORT}/?token=${encodeURIComponent(token)}`); + } + console.log(` Local: http://127.0.0.1:${PORT}/?token=${encodeURIComponent(token)}`); + } + if (shareUrl) { + console.log( + " Share is a cosmetic Mission Kit (or BYO) link; phone must still reach this LAN.", + ); } - console.log(` Local: http://127.0.0.1:${PORT}/?token=${encodeURIComponent(token)}`); console.log(" Config writes stay loopback-only. Stop: kill the LISTEN pid on this port."); console.log(" Firewall: allow inbound TCP on this port for your LAN profile if needed."); console.log(""); @@ -218,10 +239,32 @@ async function main() { if (process.env.MISSION_CONTROL_NO_OPEN === "1") { return; } - if (openBrowser(displayUrl)) { - console.log("Opened primary URL in the default browser."); - } else { - console.log("Open a LAN URL above on your phone/tablet browser."); + let configValue = null; + const cfg = resolveContextConfigPath(ROOT, { existsSync }); + if (cfg.ok) { + configValue = readPreferredBrowserFromConfig(cfg.path); + } + const result = openBrowser(openTarget, { configValue }); + if (result.opened) { + if (result.reason === "preferred-fallback") { + console.log( + shareUrl + ? "Preferred browser failed; opened share URL with the OS default." + : "Preferred browser failed; opened primary URL with the OS default.", + ); + } else { + console.log( + shareUrl + ? "Opened share URL in the preferred browser (or OS default)." + : "Opened primary URL in the preferred browser (or OS default).", + ); + } + } else if (result.reason !== "no-open") { + console.log( + shareUrl + ? "Open the Share URL above on your phone/tablet browser." + : "Open a LAN URL above on your phone/tablet browser.", + ); } } diff --git a/dashboard/start.mjs b/dashboard/start.mjs index c4c547d..4c1480e 100644 --- a/dashboard/start.mjs +++ b/dashboard/start.mjs @@ -4,7 +4,8 @@ * * Allocates a stable per-workspace listen port (hash of snapshot root in the * 3333–3588 range unless PORT is set), detach-starts `serve.mjs` when needed, - * waits until HTTP 200, prints the URL, and opens the default browser. + * waits until HTTP 200, prints the URL, and opens one preferred browser + * (or OS default). Never opens more than one browser process. * * Never kills a listener whose system.repoRoot belongs to another workspace. * @@ -17,17 +18,18 @@ import { execFileSync, execSync, spawn } from "node:child_process"; import { existsSync, openSync } from "node:fs"; -import { platform } from "node:os"; import { dirname, join, resolve } from "node:path"; import { fileURLToPath } from "node:url"; import { REPO_ROOT_ENV, escapePerlDoubleQuoted, repoRootLogId, + resolveContextConfigPath, resolveMissionControlPort, resolveSnapshotRepoRoot, sameRepoRoot, } from "./lib/guards.mjs"; +import { openBrowser, readPreferredBrowserFromConfig } from "./lib/open-browser.mjs"; const __dirname = dirname(fileURLToPath(import.meta.url)); const KIT_ROOT = join(__dirname, ".."); @@ -188,24 +190,6 @@ async function waitReady() { return false; } -function openBrowser(url) { - const os = platform(); - try { - if (os === "darwin") { - spawn("open", [url], { detached: true, stdio: "ignore" }).unref(); - return true; - } - if (os === "win32") { - spawn("cmd", ["/c", "start", "", url], { detached: true, stdio: "ignore" }).unref(); - return true; - } - spawn("xdg-open", [url], { detached: true, stdio: "ignore" }).unref(); - return true; - } catch { - return false; - } -} - async function ensureServer() { const allocation = resolveMissionControlPort({ repoRoot: ROOT, @@ -271,11 +255,23 @@ async function main() { if (process.env.MISSION_CONTROL_NO_OPEN === "1") { return; } - if (openBrowser(URL)) { - console.log( - "Opened in the default browser. In Cursor, Simple Browser or /dashboard also works.", - ); - } else { + let configValue = null; + const cfg = resolveContextConfigPath(ROOT, { existsSync }); + if (cfg.ok) { + configValue = readPreferredBrowserFromConfig(cfg.path); + } + const result = openBrowser(URL, { configValue }); + if (result.opened) { + if (result.reason === "preferred-fallback") { + console.log( + "Preferred browser failed; opened with the OS default. In Cursor, Simple Browser or /dashboard also works.", + ); + } else { + console.log( + "Opened in the preferred browser (or OS default). In Cursor, Simple Browser or /dashboard also works.", + ); + } + } else if (result.reason !== "no-open") { console.log("Open that URL in a browser (Cursor: Simple Browser, or run /dashboard in chat)."); } } diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index dd05790..9bc9f8d 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -1,6 +1,12 @@ # Contributing -Agent Kit is a HITL framework for AI-assisted IDEs. Contributions welcome - from skills to CLI features to docs. +**Mission Kit** is the storefront name; contributions land in the **Agent Kit** repository and packaging surfaces (CLI, rules, skills, docs). It is a HITL framework for AI-assisted IDEs. Contributions welcome - from skills to CLI features to docs. + +Consumer install and day-to-day usage live in the root [README](../README.md). Factory topology, local `pnpm --filter` CLI loops, and public-sync awareness live in [DEVELOPMENT.md](DEVELOPMENT.md). + +Participation is covered by the [Code of Conduct](../.github/CODE_OF_CONDUCT.md). Not sure where a question belongs? [SUPPORT.md](../.github/SUPPORT.md). **Never** report a vulnerability in a public issue or PR - use the private channel in [SECURITY.md](../.github/SECURITY.md). + +Issue forms live in `.github/ISSUE_TEMPLATE/`; the PR checklist below is mirrored in `.github/PULL_REQUEST_TEMPLATE.md`, which GitHub prefills on every pull request. Maintainers triage open public issues with a factory-only `/public-issue-triage` workflow (not installed to consumer projects); contributors continue to use GitHub issue forms and discussion on the public repository. ## Setup @@ -18,64 +24,17 @@ New to the kit? Here's where things land and how to test before your PR: - **Core changes:** CLI features, base rules, and templates live in their respective folders (`packages/cli/`, `.cursor/`, etc.) - **Test locally:** `pnpm install && pnpm lint && pnpm test` from the repo root - **Mission Control pack (Path C):** `@dadado/agent-kit-cli` includes `dashboard/**` in the npm tarball from 4.8.2 onward (synced from repo-root SoT at build/`prepack`). Local pack check: `node scripts/verify-cli-dashboard-pack.mjs`. To confirm a published tag, run `npm pack @dadado/agent-kit-cli@` and inspect the tarball for `package/dashboard/`. Version bump stays `/git-prod` HITL ([npm-publish-checklist.md](npm-publish-checklist.md)). +- **Factory / monorepo workflows:** [DEVELOPMENT.md](DEVELOPMENT.md) (local CLI install into a test project, factory self-consumer apply loop, Mission Control from a kit tree). -See [getting-started.md](getting-started.md) for the full development setup and workflow details. - -## Working on the kit - -When developing Agent Kit itself, you'll use local commands different from the consumer install path. - -### Installing from local checkout - -Test the CLI on a project using the monorepo: - -```bash -# install from local CLI with public registry -pnpm --filter @dadado/agent-kit-cli start -- install \ - --cwd /path/to/your-project \ - --url https://github.com/agent-kit-startup/agent-kit \ - --ref main - -# or install from local CLI with local registry source -pnpm --filter @dadado/agent-kit-cli start -- install \ - --cwd /path/to/your-project \ - --registry /path/to/agent-kit -``` - -Other local CLI commands follow the same pattern: - -```bash -# status, update, etc. -pnpm --filter @dadado/agent-kit-cli start -- status --cwd /path/to/your-project -``` - -### Factory self-consumer (local apply loop) - -This repo can act as its own consumer to validate L0 changes before a public release. This is distinct from the public consumer update-check and from the public sync mirror. - -1. **Build the CLI** from the current source: - ```bash - pnpm --filter @dadado/agent-kit-cli build - ``` -2. **First seed** (only when `.cursor/agent-kit.managed-hashes.json` is absent): - ```bash - pnpm --filter @dadado/agent-kit-cli start -- update --cwd . --seed-overlay - ``` - `--seed-overlay` records current local overlay files as the managed baseline so future updates can distinguish kit drift from local customization. -3. **Subsequent local refreshes**: - ```bash - pnpm --filter @dadado/agent-kit-cli start -- update --cwd . - ``` - The factory checkout resolves its own `registry/` as the source (cwd has `registry/registry.json`). The public update-check is skipped because the registry is local (`skipped-factory`). - -Do not use this path in a public consumer project; consumers should rely on the public release tag and `/update` HITL. +See [getting-started.md](getting-started.md) for the consumer workflow after install. ## Standards - Conventional Commits - Small, focused PRs - Update docs when behavior changes -- Open PRs from a fork or short-lived branch directly to `main`; the public repository has no long-lived `staging` branch +- **Base branch by repo:** target `main` for the **public repository** (`agent-kit`); target `staging` for the **private factory repository** (`agent-kit-dev`). The public repository has no long-lived `staging` branch. See [DEVELOPMENT.md](DEVELOPMENT.md) for the factory Git flow. +- **Cross-repo issue close form:** when a factory (`agent-kit-dev`) PR closes a public issue, use `Closes agent-kit-startup/agent-kit#N` (or the full issue URL). Bare `Closes #N` resolves against the PR's repository and will not close the public issue. - **Cursor-native tooling:** prefer Cursor-supported MCP servers, workspace hooks, and the official SDK. Do not add or document parallel agent gateways - see [cursor-3-features.md](cursor-3-features.md#mcp-hooks-e-sdk). ## What belongs in Git (vs local-only) @@ -140,3 +99,7 @@ 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). + +## 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 new file mode 100644 index 0000000..ce0a7d9 --- /dev/null +++ b/docs/DEVELOPMENT.md @@ -0,0 +1,115 @@ +# Development (factory and maintainers) + +This guide is for people working **on** Agent Kit (CLI, L0, sync, dogfood), not for end-user install into an application repo. Consumer install stays in the root [README](../README.md) and [getting-started.md](getting-started.md). + +## Two repositories, one product + +Name repos by URL and role. Do not write "this repo is `agent-kit-dev`" in allowlist-synced docs (the same markdown ships to both remotes). + +| Repo | Role | +|------|------| +| [agent-kit-dev](https://github.com/agent-kit-startup/agent-kit-dev) (private) | Factory: CLI, sync tooling, dogfood. Daily flow: `git staging` → `git prod` → allowlist sync. | +| [agent-kit](https://github.com/agent-kit-startup/agent-kit) (public) | Storefront and **canonical registry** (`registry/**`). Consumers install from here; registry PRs land here. | + +Projects that install Agent Kit receive only `.cursor/` + `autogit/` + the manifest, never the whole monorepo. + +**Three layers:** local scratch (HANDOFF/plans, gitignored) · private Git (factory) · public (storefront + registry SoT). Cheat sheet: [repository-boundaries.md](repository-boundaries.md#cheat-sheet-three-layers). Topology phases: [topology-private-public.md](topology-private-public.md). + +## Local monorepo setup + +```bash +pnpm install +pnpm lint +pnpm test +``` + +Contributor PR checklist and registry paths: [CONTRIBUTING.md](CONTRIBUTING.md). + +### Installing the local CLI into a test project + +```bash +# install from local CLI with public registry +pnpm --filter @dadado/agent-kit-cli start -- install \ + --cwd /path/to/your-project \ + --url https://github.com/agent-kit-startup/agent-kit \ + --ref main + +# or install from local CLI with local registry source +pnpm --filter @dadado/agent-kit-cli start -- install \ + --cwd /path/to/your-project \ + --registry /path/to/agent-kit +``` + +Other local CLI commands follow the same pattern: + +```bash +pnpm --filter @dadado/agent-kit-cli start -- status --cwd /path/to/your-project +``` + +### Factory self-consumer (local apply loop) + +A factory checkout can act as its own consumer to validate L0 changes before a public release. This is distinct from the public consumer update-check and from the public sync mirror. + +1. **Build the CLI** from the current source: + ```bash + pnpm --filter @dadado/agent-kit-cli build + ``` +2. **First seed** (only when `.cursor/agent-kit.managed-hashes.json` is absent): + ```bash + pnpm --filter @dadado/agent-kit-cli start -- update --cwd . --seed-overlay + ``` +3. **Subsequent local refreshes**: + ```bash + pnpm --filter @dadado/agent-kit-cli start -- update --cwd . + ``` + +Do not use this path in a public consumer project; consumers rely on the public release tag and `/update` HITL. Decision record: `decisions/2026-07-31_factory-pseudo-consumer-local-apply.md` (private memory). + +### Mission Control from a kit tree + +When the working tree includes `dashboard/`: + +```bash +npm run dashboard +# or: node dashboard/start.mjs +# Explicit consumer snapshot while serving from the kit tree: +# MISSION_CONTROL_REPO_ROOT=/path/to/consumer npm run dashboard + +npm run dashboard:broadcast +npm run start:dashboard # foreground serve only +``` + +Published CLI packs `dashboard/**` from 4.8.2 onward; consumers normally run `agent-kit dashboard` without a kit checkout. + +## Public sync awareness + +- Allowlist: `scripts/public-sync.manifest` (positive globs + exclusions). +- Root `README.md` is the **consumer storefront** (synced). Maintainer depth belongs here and in [CONTRIBUTING.md](CONTRIBUTING.md), not in the root README body. +- `docs/**` syncs except private exclusions (evidence ledgers, paid MCP documentation, and others listed with `!` in the manifest). +- `.cursor/memory/**` stays private (`!.cursor/memory/**`). +- Registry SoT is public after Phase B; do not re-add `registry/**` to the private→public allowlist. See [topology-private-public.md](topology-private-public.md). + +Release and sync ops: [public-launch.md](public-launch.md), [npm-publish-checklist.md](npm-publish-checklist.md). + +## Mission Kit vs Agent Kit 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. + +| 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 | +| 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 | +| Commercial contact | `sales@missionkit.io` | PolyForm Noncommercial path | +| Legacy `agent.startupkit.com.br`, `landing-agentkit/` | Historical / rollback-only | Qualify when linked | + +Storefront README must not link private memory ADRs or dump dual-name legal essays. Private decision record (factory only): `2026-08-06_mission-kit-vs-agent-kit-naming.md` under `.cursor/memory/decisions/`. Public vs maintainer README split: `2026-08-02_public-vs-dev-readme-separation.md` in the same folder. + +## Related docs + +- [CONTRIBUTING.md](CONTRIBUTING.md) - setup, standards, registry contributions +- [repository-boundaries.md](repository-boundaries.md) - local / private / public / npm +- [topology-private-public.md](topology-private-public.md) - Phase A/B/C +- [contribute-upstream.md](contribute-upstream.md) - `agent-kit contribute` from a consumer diff --git a/docs/README.md b/docs/README.md index 66fff60..6989da0 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,6 +1,6 @@ -# Agent Kit Docs +# Mission Kit Docs -Agent Kit is a HITL framework for AI-assisted IDEs: plan, handoff, staging-to-prod git flow, and memory across long projects. Install generates Cursor-first project setup; VS Code and Windsurf get partial generators (parity Low / Minimal per [cursor-native-audit.md](cursor-native-audit.md)). Mechanizable invariants live in the CLI so non-Cursor paths can run the same checks. +**Mission Kit** is the product-family name on [missionkit.io](https://missionkit.io). **Agent Kit** is the technical install surface (CLI, npm, slash commands, `.cursor/agent-kit.json`). The kit is a HITL framework for AI-assisted IDEs: plan, handoff, staging-to-prod git flow, and memory across long projects. Install generates Cursor-first project setup; VS Code and Windsurf get partial generators (parity Low / Minimal per [cursor-native-audit.md](cursor-native-audit.md)). Mechanizable invariants live in the CLI so non-Cursor paths can run the same checks. ## Guides @@ -11,7 +11,7 @@ Agent Kit is a HITL framework for AI-assisted IDEs: plan, handoff, staging-to-pr - [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) -- [Agent Kit landing](agentkit-landing.md) - public marketing page at startupkit.com.br/agentkit +- [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 - [Review layers](review-camadas.md) - final HITL / go-no-go pass @@ -31,4 +31,11 @@ Agent Kit is a HITL framework for AI-assisted IDEs: plan, handoff, staging-to-pr - [Agent Kit manifest](agent-kit-manifest.md) - `.cursor/agent-kit.json` schema (version, packs, protected L3) - [Repository Boundaries](repository-boundaries.md) - three-layer cheat sheet (local / private / public), npm, sync - [Contributing](CONTRIBUTING.md) - setup, standards, registry contributions +- [Development](DEVELOPMENT.md) - factory topology, local CLI, maintainer workflows (root README stays consumer storefront) - [GitHub About](github-about.md) - description and topics for the GitHub repo + +## Community + +- [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/agentkit-landing.md b/docs/agentkit-landing.md index 8748344..4f97f08 100644 --- a/docs/agentkit-landing.md +++ b/docs/agentkit-landing.md @@ -1,38 +1,142 @@ -# Agent Kit Public Landing (agent.startupkit.com.br) +# Mission Kit Landing (missionkit.io) -**New domain:** [https://agent.startupkit.com.br](https://agent.startupkit.com.br) (static deployment) -**Previous:** `startupkit.com.br/agentkit` (WordPress page ID 3001, deprecated due to wpautop issues) +**Live URL:** [https://missionkit.io](https://missionkit.io) (static Hostinger hosting + SSL) +**Previous:** `agent.startupkit.com.br` (deprecated; now 301 redirects to https://missionkit.io/) -**Design system:** STK visual identity + Mission Control tokens (`dashboard/dashboard.html`) -**Source of record:** `.cursor/context/landing-agentkit/page-content.html` (complete HTML/CSS/assets) +## Source of record -**Product claims:** track shipped **4.8.4** -**Positioning:** human-in-the-loop framework (ADR `2026-07-09_framework-hitl-positioning.md`) -**Copy style:** STK conversational tone adapted for developers +The landing page is authored in the **Claude Design** project and exported as a zip. +The repo is a consumer: it syncs the export, builds a deployable bundle, and deploys it. +The design canvas is a React component (bindings, `sc-if`/`sc-for`, a logic class) that +renders client-side via a vendored `dc-runtime` (`support.js`). The repo ships the runtime +rather than flattening the markup, so the deployed page is byte-identical to what the +design tool renders. -## Deployment Configuration +**Design mirror:** `.cursor/context/landing-missionkit/remote/` (versioned) +**Build output:** `.cursor/context/landing-missionkit/dist/` (gitignored, derived) +**Decision:** [landing-external-design-source-of-record](../memory/decisions/2026-08-05_landing-external-design-source-of-record.md) +**Design system:** [docs/design-system.md](design-system.md) (project id, upstream/downstream map, token divergence notes) -### Static Site Setup -- **Hosting:** Static files (HTML/CSS/JS) on CDN or dedicated hosting -- **DNS:** Configure `agent.startupkit.com.br` CNAME or A record -- **Assets:** Self-contained in `page-content.html` (no external dependencies) -- **SSL:** Required for production deployment +### Constraints + +- Do not edit files under `remote/`; `landing:sync` overwrites them wholesale. +- Do not hand-edit anything in `dist/` after build; `landing:build` derives it (plus the product overlay below). +- Do not add design assets by hand; the build derives the design asset list from the canvas. +- Visual or copy changes to the marketing canvas go through the design tool, then re-export and re-sync. +- **Product overlay (allowed):** `landing:build` copies `dashboard/open.html` → `dist/mc/open.html` for cosmetic Mission Control broadcast Share URLs (ADR `2026-08-11_mission-control-broadcast-url-mask.md`). That file is product SoT under `dashboard/`, not Claude Design. Default share base uses `…/mc/open.html` (live). Extensionless `/mc/open` remains optional Hostinger alias work (align/public compliance), not required for the CLI default. + +## Pipeline -### Domain Migration Strategy ```bash -# From WordPress setup -OLD: startupkit.com.br/agentkit (WordPress page ID 3001) -NEW: agent.startupkit.com.br (clean static deployment) - -# DNS Configuration -Type: CNAME or A record -Host: agent -Domain: startupkit.com.br -Target: [hosting provider endpoint] +# 1. Sync a new design export +pnpm landing:sync "~/Downloads/MissionKit landing page.zip" + +# 2. Build the deployable bundle (vendors React, injects crawler head, copies assets) +pnpm landing:build + +# 3. Verify the bundle matches the export +pnpm landing:build:check +# Factory CI (agent-kit-dev build job) runs `landing:build` then `landing:build:check` +# so open-source wording regressions and missing canvas sources fail the Evidence path. + +# 4. Local stage (loopback, Range-capable, no-store) +pnpm landing:serve +# Open http://127.0.0.1:4173/ + +# 5. Deploy +# Zip dist/ and deploy via hosting_deployStaticWebsite to missionkit.io +``` + +### What the build changes (and nothing more) + +1. Drops stylesheets the landing does not reference (design-system tokens, aggregator) +2. Self-hosts React so the runtime short-circuits `loadReactUmd()` and never hits unpkg +3. Injects a static ``, description, canonical, Open Graph, Twitter Card, and favicon + into `<head>` for crawlers (dc-runtime moves `<helmet>` into `<head>` at boot, which is + too late for anything that does not execute JS) + +## Deployment + +- **Hosting:** Hostinger web hosting (`u262837109`), addon vhost root `/home/u262837109/domains/missionkit.io/public_html` +- **DNS:** `@` ALIAS to `missionkit.io.cdn.hstgr.net` (Hostinger CDN) +- **SSL:** Hostinger HTTPS (HTTP/2 200 verified) +- **Deploy method:** `hosting_deployStaticWebsite` with a zip of `dist/` preserving directory structure + +### Rollback + +The hand-authored production-shot HTML under `.cursor/context/landing-agentkit/` is +rollback reference material only. It is **not** a self-contained single-file deploy +artifact. + +- Blob `d0e43278cda5` at commit `3dad9c6` (the `index.html` object cited by earlier + docs) carries eight external asset references (four logo + four production PNG + `src` values; historically `../../dashboard/logo.svg` and + `../../assets/production/*.png`). It cannot be zipped alone for + `hosting_deployStaticWebsite`. The current worktree files use root-relative + `/dashboard/…` and `/assets/production/…` paths for the same five document-root + assets; they are not asserted byte-identical to that blob. +- No production-shot-era single-file self-contained blob exists in Git. The last + pre-shot CSS-mockup `index.html` that embeds its UI without those five files is + blob `11197c8db22f` at commit `611c232` (pre-PR #646). Restoring that blob rolls + back content as well as packaging. + +To redeploy the production-shot legacy page (HITL only; not the supported live path), +zip `index.html` with these five paths at the **deployed document root** so the +root-relative URLs in both legacy HTML files resolve: + +- `/dashboard/logo.svg` ← repo `dashboard/logo.svg` +- `/assets/production/1-mission-control-current-mission.png` +- `/assets/production/2-mission-control-check-list.png` +- `/assets/production/3-mission-control-Crew-Monitor.png` +- `/assets/production/4-mission-control-flight-log.png` + +Do not deploy that `index.html` alone. The supported live path remains +`pnpm landing:build` followed by the `dist/` archive deployment described above. + +## SEO + +Live `missionkit.io` serves PolyForm Noncommercial / source-available copy (verified as-served HTML after Design sync → `landing:build` → Hostinger deploy): + +- **Description / OG / Twitter:** Mission Kit is a free, source-available framework (PolyForm Noncommercial) with built-in project management, DevSecOps, and agent orchestration. Plan, build, review, and ship without leaving Cursor or VS Code. Commercial use: sales@missionkit.io. +- **CTA:** Free for noncommercial use, source-available, and it runs inside the IDE you already use. +- **Footer meta:** free & source-available (PolyForm NC) + +Other crawler fields matching the build pipeline: + +- **Title:** Mission Kit 5 · Development operations built into Cursor and VS Code +- **Favicon:** `assets/logo.svg` (SVG, also `apple-touch-icon`) +- **Open Graph image:** `assets/hero-astronaut.png` (absolutized to `https://missionkit.io/assets/hero-astronaut.png` in crawler head) +- **Twitter Card:** `summary_large_image` with title, description, and image + +Operator path for copy changes: edit Claude Design SoR → Download zip → `pnpm landing:sync` → `pnpm landing:build` → `hosting_deployStaticWebsite`. Do not hand-edit `landing-missionkit/remote/` as the source of truth. Prompt notes: `.cursor/context/landing-missionkit/UPSTREAM-DESIGN-FIX-PROMPT.md` (license copy + install/prompt clipboard honesty). + +Install and prompt copy buttons await `navigator.clipboard.writeText`, fall back to `document.execCommand('copy')` when needed, and show a brief failure affordance instead of an optimistic checkmark. The How-it-works "Copy plan path" / "Copy /git-staging" controls are decorative (disabled). Product Mission Control paste-destination CTAs are a separate contract (`dashboard/dashboard.html`). + +## Mission Control demo iframe (Crew lexicon) + +The landing embeds a tracked Mission Control snapshot at `landing-missionkit/remote/mc/dashboard.html` (not the live product dashboard). + +| Surface | Current (as served from the snapshot) | Target (design-v2 / product SoT) | +|---------|----------------------------------------|----------------------------------| +| Current Mission `agent` | `Engineering Manager` | `Tech Lead` | +| Feed label seg0 | Design-export labels (`Squad ·`, …) emitted verbatim | Wire tokens in `#mc-mock-data`, display-masked via `CREW_ACTOR_MASK` / `crewActorRole` (e.g. `SQ` → Scrum Master) | +| Mask helpers in snapshot | Absent (`crewActorRole` count 0) | Same helpers as `dashboard/dashboard.html` | + +The PolyForm cutover Design sync (`783ca90` / PR #698) also refreshed `remote/mc/dashboard-data.json` and `remote/mc/dashboard.html` with the Design-export Crew labels above. That lexicon delta is incidental to license copy; product SoT and display masking remain owned by the Mission Control / crew-mask lanes. **Do not** hand-edit `remote/mc/` to force the target row. Prefer regenerating the snapshot from product SoT when that path exists, or folding MC demo fixtures into a Design export and running `pnpm landing:sync`. Until then, CHANGELOG and this section must not claim display masking the snapshot cannot perform. + +## Domain migration history + ``` +OLD: agent.startupkit.com.br (deprecated; 301 → missionkit.io) +NEW: missionkit.io (current canonical) +``` + +## Legacy files -### SEO Configuration -- **Title:** Agent Kit — Plan, build, ship, and remember AI coding projects -- **Meta description:** Stop losing context between AI chats. Agent Kit provides checkable plans, smart handoffs, and staging-first git workflow for long coding projects. -- **H1 hierarchy:** Page title → Hero H2 → Section headers -- **OpenGraph:** Include for social sharing (title, description, image) +`.cursor/context/landing-agentkit/` contains the previous hand-authored landing +(`index.html`, `page-content.html`) and three design reference files (`COPY.md`, +`DESIGN-SYSTEM.md`, `INVENTORY.md`). These predate the Mission Kit 5 redesign. +The retired `check:landing-body-equality` guard compared their trimmed body content; it is no longer an active delivery gate (call sites and `scripts/check-landing-body-equality.mjs` removed; use `pnpm landing:build:check`). +The production-shot variant also depends on the five document-root assets listed in +Rollback (root-relative `/dashboard/logo.svg` and `/assets/production/*.png`). These +files are kept for rollback reference only. diff --git a/docs/bootstrap.md b/docs/bootstrap.md index 7907eb1..6f91b3e 100644 --- a/docs/bootstrap.md +++ b/docs/bootstrap.md @@ -70,9 +70,11 @@ The kit can update itself against the same source without ever touching your pla | `agent-kit status` | Version, installed packs, readiness summary | | `agent-kit doctor` | Refresh or repair repository readiness | +**Multi-workspace safety:** each project keeps its own `.cursor/agent-kit.json` manifest and `.cursor/agent-kit.managed-hashes.json` overlay ledger. The CLI confirms the absolute root before writing (interactive prompt; `--yes` skips). The shared registry cache (`~/.cache/agent-kit/registry/`) is directory-locked so parallel installs cannot race. + **Check ≠ apply.** Opt-in session nudges use `updateCheck.enabled` in `.cursor/context/config.json` (default `false`). When enabled, `sessionStart` may advise that a newer public release exists; it never rewrites `.cursor/`. Applying still requires `/update` with Ask confirmation (or an explicit terminal `agent-kit update`). `updateApply.auto` defaults to `false` and is not a silent background path. -**Cursor product updates (separate):** opt-in `cursorUpdateCheck.enabled` (default `false`) plus `agent-kit cursor-awareness --check` / `/cursor-update-awareness` report advisory gaps vs `docs/cursor-native-audit.md`. Confirmed work routes through Ask → `/backlog-add` or `/dogfood`. Never auto Field Reports. See [cursor-update-awareness.md](cursor-update-awareness.md). +**Cursor product updates (separate):** opt-in `cursorUpdateCheck.enabled` (default `false`) plus `agent-kit cursor-awareness --check` / `/cursor-update-awareness` report advisory gaps vs `docs/cursor-native-audit.md` (walk-up from cwd; use `--cwd` when the inventory is not under the shell directory). Confirmed work routes through Ask → `/backlog-add` or `/dogfood`. Never auto Field Reports. See [cursor-update-awareness.md](cursor-update-awareness.md). Factory/dogfood installs (manifest registry URL `agent-kit-dev` or pre-prod refs such as `staging`) skip the public check with a warning so the factory is not treated as a consumer. diff --git a/docs/capability-inventory.md b/docs/capability-inventory.md index 62fadad..6841a8d 100644 --- a/docs/capability-inventory.md +++ b/docs/capability-inventory.md @@ -1,14 +1,16 @@ # Capability inventory -Agent Kit capability catalog grouped by surface family. Lists every shipped capability with one line per item. +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`). -**Status (2026-07-31):** Capability counts verified against the working tree on private `staging` @ `7e5315d` (package floor `4.8.4`). Normative intent lives in commands/rules/CLI; this catalog is **derived documentation** and is not proof of runtime behavior. 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`. +**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: 27 commands, 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. Positioning table below lists **25 unique paths / 89 literal rows** (see §Positioning surfaces) and is an open enumeration (not a closed surface census); the npm storefront `packages/cli/README.md` is a required surface. +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. --- -## Slash commands (.cursor/commands/ - 27) +## Slash commands (.cursor/commands/ - 28) + +Factory-only counting policy: inventories that describe the **consumer/L0** surface should cite **27** syncable commands (excluding `/public-issue-triage`). The on-disk factory tree has **28** files; `/public-issue-triage` is omitted from L0 install and excluded from `scripts/public-sync.manifest`. - `/start-project` - Plan creation with two-gate HITL (broad intake, write confirm, optional Gate B start unit) - `/backlog-add` - Enqueue plan under HANDOFF Backlog without activation @@ -37,6 +39,7 @@ Real counts: 27 commands, 25 rules, 13 agents, 9 skills, 5 Cursor hooks, 18 CLI - `/archive-plan` - Move parked plan to archive with status update - `/context-status` - Context window and memory status report - `/dogfood` - File a private dogfood note into the factory or consumer inbox +- `/public-issue-triage` - Factory-only maintainer triage for the public GitHub issues inbox (HITL; not L0 / not public-sync) --- @@ -310,79 +313,71 @@ Hash-based content preservation system introduced in [Unreleased]. Managed-conte Enumerated identity literals and publication routes from `scripts/public-sync.manifest`. This table is a working inventory, not a completeness proof. Do not treat row count as an authority for product positioning; re-validate literals against the files and the published npm/public lanes (`docs/evidence/guidance-claim-matrix.md`). -| Path | Line | Current literal text | Publication route | -|------|------|---------------------|------------------| -| `README.md` | 5 | Turn your AI coding agent into one that runs the whole workflow: plan it, build it, ship it, and remember it across long projects. | allowlist-synced (line 20) | -| `README.md` | 7 | Long AI coding sessions fall apart when the context window fills up. Agent Kit fixes this with a small operating layer that handles planning, handoff between chats, and structured git flow. The agent builds against a checkable plan and writes down where it stopped so any fresh chat picks up exactly where the last one left off. | allowlist-synced (line 20) | -| `README.md` | 11 | No more lost context. The agent keeps a short state file; new chat, one command, and it's caught up. | allowlist-synced (line 20) | -| `README.md` | 12 | Work against real plans. To-dos you can watch tick off, not vibes. Confirmations stay human-in-the-loop (Ask questions), not unchecked autonomy. | allowlist-synced (line 20) | -| `README.md` | 13 | Built-in DevOps discipline. Staging-first git flow prevents history chaos. | allowlist-synced (line 20) | -| `README.md` | 14 | Production needs confirmation. Agent can push to staging alone; promoting to `main` always asks first. | allowlist-synced (line 20) | -| `README.md` | 15 | Operational learning, not model training. Memory and optional external review keep findings durable across chats; they do not retrain the model. | allowlist-synced (line 20) | -| `README.md` | 16 | Clean history everywhere. Commits and docs describe the software, not chat chatter. | allowlist-synced (line 20) | -| `README.md` | 22 | Plans + HITL gates, `/start-project` Broad Intake, then two gates (write plan, then first unit). Confirmations use Ask questions (clickable options; chat fallback when the tool is unavailable). | allowlist-synced (line 20) | -| `README.md` | 23 | Phase handoff, `.cursor/HANDOFF.md` plus Context Guardian and native hooks (`sessionStart` / `preCompact`) so a fresh chat resumes without re-briefing. Local workspace state; not a hosted sync plane. | allowlist-synced (line 20) | -| `README.md` | 24 | Manual or continuous run, `/continue-plan` (one phase per chat) or `/run-plan` … `/run-plan-all` queues multiple plans sequentially. Plan/queue orchestration, not a general graph runtime. | allowlist-synced (line 20) | -| `README.md` | 25 | Staging → prod git, `/git-staging` for automatic promote to `origin/staging`; `/git-prod` only after explicit confirmation. Direct commits to `main` are blocked. | allowlist-synced (line 20) | -| `README.md` | 26 | Memory loop, Resolved errors and tradeoff decisions in `.cursor/memory/` so the next chat can reuse them. | allowlist-synced (line 20) | -| `README.md` | 27 | Repository readiness, Install scans the repo, applies safe local fixes, and writes a readiness snapshot. `/agent-kit-onboard` resolves remaining decisions one at a time before `/start-project`. | allowlist-synced (line 20) | -| `README.md` | 28 | Agent Personas, Mode-aware chat/CLI chrome only: Autopilot (`/continue-plan`), Night Shift (`/run-plan`), Ghost Runner (CLI). Configure after readiness or set `agentPersona` in `.cursor/context/config.json`. Never changes commits, HANDOFF, memory, or product docs. | allowlist-synced (line 20) | -| `README.md` | 29 | Optional external plan review, After a plan is exhausted, arm Claude Code for a gap monitor; triage with `/plan-review-triage`. Opt-in via config. Findings-only by default (no silent product auto-fix). | allowlist-synced (line 20) | -| `README.md` | 30 | Skills + domain packs, Registry skills and optional L1 packs (clean code, context tools, and more). Install/update via CLI; contribute upstream with `agent-kit contribute`. | allowlist-synced (line 20) | -| `README.md` | 31 | Output hygiene, Chat can be light; commits, docs, HANDOFF, and memory stay professional and inheritable. | allowlist-synced (line 20) | -| `README.md` | 33 | Production-agent layers (L0) five-layer table + link to `docs/evidence/five-layer-claim-matrix.md` | allowlist-synced (line 20) | -| `README.md` | 93 | Mission Control is a local panel over the Agent Kit runtime state. … It is a cockpit for one workspace, not a hosted multi-tenant control plane. Actions stay copy-only (clipboard + paste destination). | allowlist-synced (line 20) | -| `README.md` | 104 | The Cockpit reads as one page in four sections, each reachable from the primary navigation | allowlist-synced (line 20) | -| `README.md` | 108 | The plan in flight: status, progress, friendly Mode labels, and previous/current/next todo | allowlist-synced (line 20) | -| `README.md` | 109 | HANDOFF Gaps log (**NOW** / **Earlier**, wipe on new flight; cap 15 within a flight) plus operator Warnings (Quota pause, Heads up); palette-by-type notification chrome (`ok` / `advice` / `prompt` / `residual` / `warning`); clipboard icon; clickable copy text/path; **All clear** when idle | allowlist-synced (line 20) | -| `README.md` | 110 | What remains: recent plan cards, parked and incomplete plans, and readiness notes | allowlist-synced (line 20) | -| `README.md` | 111 | Live agent/crew feed: ticks, handoffs, deliveries, and denser `agent_step` rows for active-plan to-dos (cap 20) | allowlist-synced (line 20) | -| `README.md` | 119 | Docs | allowlist-synced (line 20) | -| `README.md` | 123 | Install, commands, day-to-day workflow | allowlist-synced (line 20) | -| `README.md` | 124 | Install discovery, `/agent-kit-onboard`, and deliverable boundary | allowlist-synced (line 20) | -| `README.md` | 125 | Exactly what lands in your project, and why there's no nested folder | allowlist-synced (line 20) | -| `README.md` | 126 | How the base install, optional packs, and your local files layer together | allowlist-synced (line 20) | -| `README.md` | 127 | Optional bundles: clean code, DevOps, testing, and more | allowlist-synced (line 20) | -| `README.md` | 128 | Mode defaults, `agentPersona` config, hygiene boundary ([create / contribute](docs/creating-personas.md)) | allowlist-synced (line 20) | -| `README.md` | 129 | Opt-in Claude Code monitor after `/run-plan` exhaustion | allowlist-synced (line 20) | -| `README.md` | 130 | The `.cursor/agent-kit.json` file | allowlist-synced (line 20) | -| `README.md` | 131 | Working on the kit itself (includes contributor quickstart) | allowlist-synced (line 20) | -| `README.md` | 132 | Everything else | allowlist-synced (line 20) | -| `README.md` | 134 | For maintainers | allowlist-synced (line 20) | -| `README.md` | 136 | Two GitHub repos, one product | allowlist-synced (line 20) | -| `README.md` | 140 | Factory: CLI, sync tooling, dogfood. Daily flow: `git staging` → `git prod` → allowlist sync. | allowlist-synced (line 20) | -| `README.md` | 141 | Storefront and **canonical registry** (`registry/**`). Consumers install from here; registry PRs land here. | allowlist-synced (line 20) | -| `README.md` | 143 | Projects that install Agent Kit receive only `.cursor/` + `autogit/` + the manifest, never the whole monorepo. | allowlist-synced (line 20) | -| `README.md` | 145 | **Three layers:** local scratch (HANDOFF/plans, gitignored) · private Git (factory) · public (storefront + registry SoT). Full cheat sheet: [docs/repository-boundaries.md](docs/repository-boundaries.md#cheat-sheet-three-layers). | allowlist-synced (line 20) | -| `package.json` | 4 | HITL framework for AI-assisted IDEs: plan, handoff, staging-to-prod, memory loop; project-aware setup for Cursor, VS Code, and Windsurf. | allowlist-synced (line 23) | -| `packages/cli/package.json` | 4 | Agent Kit CLI: HITL framework install and tooling for AI-assisted IDEs (rules, skills, plan/handoff, context). | allowlist-synced (packages/** line 39) | -| `packages/cli/README.md` | 3 | Agent Kit CLI: HITL operating-layer install and tooling for AI-assisted IDEs (rules, skills, plan/handoff, staging-to-prod, memory). It installs local workspace contracts; it is not a hosted control plane or graph workflow runtime. | allowlist-synced (packages/** line 39); npm pack storefront (`prepublishOnly` → `scripts/verify-cli-dashboard-pack.mjs`) | -| `packages/cli/src/index.ts` | 24 | HITL framework for AI-assisted IDEs | allowlist-synced (packages/** line 39) | -| `.cursor-plugin/plugin.json` | 5 | HITL framework for AI-assisted IDEs — plan, handoff, staging→prod, memory loop, anti-slop. Stack skills via agent-kit add. | allowlist-synced (.cursor-plugin/** line 69) | -| `.cursor-plugin/plugin.json` | 7-13 | ["agents","hitl","handoff","git-staging","context","multi-ide","anti-slop"] | allowlist-synced (.cursor-plugin/** line 69) | -| `docs/README.md` | 3 | Agent Kit is a HITL framework for AI-assisted IDEs: plan, handoff, staging-to-prod git flow, and memory across long projects. Install generates Cursor-first project setup; VS Code and Windsurf get partial generators (parity Low / Minimal per [cursor-native-audit.md](cursor-native-audit.md)). Mechanizable invariants live in the CLI so non-Cursor paths can run the same checks. | allowlist-synced (docs/** line 54) | -| `docs/getting-started.md` | 3 | Agent Kit keeps your AI coding agent working against a plan and stops you from losing context when a chat gets too long. This guide covers installing it, the commands you get, and how a normal day looks. | allowlist-synced (docs/** line 54) | -| `docs/CONTRIBUTING.md` | 3 | Agent Kit is a HITL framework for AI-assisted IDEs. Contributions welcome - from skills to CLI features to docs. | allowlist-synced (docs/** line 54) | -| `docs/github-about.md` | 8 | Human-in-the-loop harness for AI-assisted IDEs - plans, context handoff, memory loop, and staging→prod git workflow with explicit confirmation before production. | allowlist-synced (docs/** line 54) | -| `docs/github-about.md` | 14 | HITL framework for AI-assisted IDEs: plan → handoff → staging → prod, with a skill registry and opt-in stack packs. | allowlist-synced (docs/** line 54) | -| `docs/github-about.md` | 20 | Harness human-in-the-loop para IDEs com IA: planos, handoff de contexto, memory loop e fluxo git staging→prod com confirmação explícita antes de produção. | allowlist-synced (docs/** line 54) | -| `docs/github-about.md` | 25 | `ai-assisted-development` `cursor` `vscode` `windsurf` `developer-tools` `cli` `monorepo` `agent-kit` `prompt-engineering` `skills` `templates` `handoff` `context-management` `human-in-the-loop` | allowlist-synced (docs/** line 54) | -| `docs/cursor-native-audit.md` | 30 | HITL framework for AI-assisted IDEs (plan, handoff, staging→prod, memory loop) | allowlist-synced (docs/** line 54) | -| `docs/public-launch-announcement.md` | 12 | Para resolver esse e outros problemas, como a falta de um fluxo de DevOps estruturado, conexão segura com ferramentas e versionamento, ao longo de um ano, fui desenvolvendo o *Agent Kit*! | allowlist-synced (docs/** line 54) | -| `docs/public-launch-announcement.md` | 17 | É uma camada operacional leve que transforma seu IDE (Cursor, VS Code, etc.) em um framework que gerencia o planejamento, o handoff entre chats e o fluxo de Git / DevOps estruturado para você focar no que importa. | allowlist-synced (docs/** line 54) | -| `docs/public-launch-announcement.md` | 10 | Se você usa o Cursor ou outro IDE com IA assistida para codar, já deve ter passado pelo clássico problema de ver a IA se perder e alucinar quando o chat fica muito longo e o contexto enche. | allowlist-synced (docs/** line 54) | -| `docs/public-launch-announcement.md` | 16 | O que é o Agent Kit? | allowlist-synced (docs/** line 54) | -| `docs/public-launch-announcement.md` | 19 | O que ele resolve? | allowlist-synced (docs/** line 54) | -| `docs/public-launch-announcement.md` | 20 | Onboarding & Setup Inteligente: Ele analisa o seu projeto, descobre o que está faltando e gera regras, comandos e skills personalizados sob medida para a sua stack e padrões de código. | allowlist-synced (docs/** line 54) | -| `docs/public-launch-announcement.md` | 21 | Sem perda de contexto: Ele mantém o estado do seu projeto vivo. Abriu um chat novo? Um comando e a IA já sabe exatamente onde parou. | allowlist-synced (docs/** line 54) | -| `docs/public-launch-announcement.md` | 22 | Planos de verdade: "vibecoding" mas nem tanto. A IA trabalha em cima de to-dos reais que você acompanha passo a passo no loop. | allowlist-synced (docs/** line 54) | -| `docs/public-launch-announcement.md` | 23 | DevOps integrado: Fluxo de Git seguro com staging automático e commits limpos. | allowlist-synced (docs/** line 54) | -| `docs/public-launch-announcement.md` | 24 | Segurança em produção: A IA pode subir para staging sozinha, mas promover para `main` sempre exige sua confirmação direta. Hooks nativos protegem a IA de fazer isso alucinando. | allowlist-synced (docs/** line 54) | -| `docs/public-launch-announcement.md` | 26 | Como usar? | allowlist-synced (docs/** line 54) | -| `docs/public-launch-announcement.md` | 30 | `npx @dadado/agent-kit-cli install` | allowlist-synced (docs/** line 54) | -| `docs/public-launch-announcement.md` | 35 | Depois de instalado, você ganha comandos como `/agent-kit-onboard`, `/start-project` e `/continue-plan` diretamente no chat do seu editor. | allowlist-synced (docs/** line 54) | -| `install.md` | 1 | Agent Kit - Installation | allowlist-synced (line 32) | -| `install.md` | 3 | You are the installer. Set up the kit **in the user's project** without copying the entire Agent Kit monorepo into it. | allowlist-synced (line 32) | +| Path | Anchor | Current literal text | Publication route | +|------|--------|---------------------|------------------| +| `README.md` | L5 | Development operations built into Cursor and VS Code. | allowlist-synced (`README.md` in `scripts/public-sync.manifest`) | +| `README.md` | L7 | Mission Kit 5 is a free (personal and non-commercial) source-available framework under PolyForm Noncommercial for plan, build, review, and … | allowlist-synced (`README.md` in `scripts/public-sync.manifest`) | +| `README.md` | L9 | Long AI coding sessions fall apart when the context window fills up. The kit fixes this with a small operating layer that handles planning,… | allowlist-synced (`README.md` in `scripts/public-sync.manifest`) | +| `README.md` | L13 | No more lost context. The agent keeps a short state file; new chat, one command, and it's caught up. | allowlist-synced (`README.md` in `scripts/public-sync.manifest`) | +| `README.md` | L14 | Work against real plans. To-dos you can watch tick off, not vibes. Autonomy stays optional and gated. | allowlist-synced (`README.md` in `scripts/public-sync.manifest`) | +| `README.md` | L15 | Built-in DevOps discipline. Staging-first git flow prevents history chaos. | allowlist-synced (`README.md` in `scripts/public-sync.manifest`) | +| `README.md` | L16 | Production needs confirmation. Agent can push to staging alone; promoting to `main` always asks first. | allowlist-synced (`README.md` in `scripts/public-sync.manifest`) | +| `README.md` | L17 | Operational learning, not model training. Memory and optional external review keep findings durable across chats; they do not retrain the … | allowlist-synced (`README.md` in `scripts/public-sync.manifest`) | +| `README.md` | L18 | Clean history everywhere. Commits and docs describe the software, not chat chatter. | allowlist-synced (`README.md` in `scripts/public-sync.manifest`) | +| `README.md` | L24 | Plans + HITL gates — `/start-project` Broad Intake, then two gates (write plan, then first unit). Confirmations use Ask questions (clickabl… | allowlist-synced (`README.md` in `scripts/public-sync.manifest`) | +| `README.md` | L25 | Phase handoff — `.cursor/HANDOFF.md` plus Context Guardian and native hooks (`sessionStart` / `preCompact`) so a fresh chat resumes without… | allowlist-synced (`README.md` in `scripts/public-sync.manifest`) | +| `README.md` | L26 | Manual or continuous run — `/continue-plan` (one phase per chat) or `/run-plan` (runs to the end; picks worker orchestration or in-session … | allowlist-synced (`README.md` in `scripts/public-sync.manifest`) | +| `README.md` | L27 | Staging → prod git — `/git-staging` for automatic promote to `origin/staging`; `/git-prod` only after explicit confirmation. Direct commits… | allowlist-synced (`README.md` in `scripts/public-sync.manifest`) | +| `README.md` | L28 | Memory loop — Resolved errors and tradeoff decisions in `.cursor/memory/` so the next chat can reuse them. | allowlist-synced (`README.md` in `scripts/public-sync.manifest`) | +| `README.md` | L29 | Repository readiness — Install scans the repo, applies safe local fixes, and writes a readiness snapshot. `/agent-kit-onboard` resolves rem… | allowlist-synced (`README.md` in `scripts/public-sync.manifest`) | +| `README.md` | L30 | Agent Personas — Mode-aware chat/CLI chrome only: Autopilot (`/continue-plan`), Night Shift (`/run-plan`), Ghost Runner (CLI). Configure af… | allowlist-synced (`README.md` in `scripts/public-sync.manifest`) | +| `README.md` | L31 | Optional external plan review — After a plan is exhausted, arm Claude Code for a gap monitor; triage with `/plan-review-triage`. Opt-in via… | allowlist-synced (`README.md` in `scripts/public-sync.manifest`) | +| `README.md` | L32 | Skills + domain packs — Registry skills and optional L1 packs (clean code, context tools, and more). Install/update via CLI; contribute ups… | allowlist-synced (`README.md` in `scripts/public-sync.manifest`) | +| `README.md` | L33 | Output hygiene — Chat can be light; commits, docs, HANDOFF, and memory stay professional and inheritable. | allowlist-synced (`README.md` in `scripts/public-sync.manifest`) | +| `README.md` | L35 | ### Production-agent layers (L0) | allowlist-synced (`README.md` in `scripts/public-sync.manifest`) | +| `README.md` | L95 | Mission Control is a local panel over Agent Kit runtime state. It binds to loopback by default and serves only its own static files. It is … | allowlist-synced (`README.md` in `scripts/public-sync.manifest`) | +| `README.md` | L109 | Primary sections: Current mission, Flight Log, Checklist, Crew Monitor. More menu: Plans, Activity, Agents, Skills, Commands, Health, Git, Memory, Terminals, Processes, Config… | allowlist-synced (`README.md` in `scripts/public-sync.manifest`) | +| `README.md` | L119 | [Repository readiness](docs/repository-readiness-onboarding.md) — Install discovery, `/agent-kit-onboard`, and deliverable boundary | allowlist-synced (`README.md` in `scripts/public-sync.manifest`) | +| `README.md` | L120 | [Bootstrap](docs/bootstrap.md) — Exactly what lands in your project, and why there's no nested folder | allowlist-synced (`README.md` in `scripts/public-sync.manifest`) | +| `README.md` | L121 | [Layers](docs/layers-spec.md) — How the base install, optional packs, and your local files layer together | allowlist-synced (`README.md` in `scripts/public-sync.manifest`) | +| `README.md` | L122 | [Domain packs](docs/domain-packs.md) — Optional bundles: clean code, DevOps, testing, and more | allowlist-synced (`README.md` in `scripts/public-sync.manifest`) | +| `README.md` | L123 | [Agent Personas](docs/personas-contract.md) — Mode defaults, `agentPersona` config, hygiene boundary ([create / contribute](docs/creating-p… | allowlist-synced (`README.md` in `scripts/public-sync.manifest`) | +| `README.md` | L125 | [Manifest](docs/agent-kit-manifest.md) — The `.cursor/agent-kit.json` file | allowlist-synced (`README.md` in `scripts/public-sync.manifest`) | +| `README.md` | L126 | [Contributing](docs/CONTRIBUTING.md) — Working on the kit itself (includes contributor quickstart) | allowlist-synced (`README.md`) | +| `README.md` | L127 | [Development](docs/DEVELOPMENT.md) — Factory topology, local CLI, and maintainer workflows | allowlist-synced (`README.md`) | +| `README.md` | L140 | Want to improve skills, docs, or the CLI? Start at docs/CONTRIBUTING.md… | allowlist-synced (`README.md`) | +| `docs/DEVELOPMENT.md` | (H1/body) | Factory topology, local CLI loops, public-sync awareness | allowlist-synced (`docs/**`) | +| `package.json` | 4 | HITL framework for AI-assisted IDEs: plan, handoff, staging-to-prod, memory loop; project-aware setup for Cursor, VS Code, and Windsurf. | allowlist-synced (manifest path pattern) | +| `packages/cli/package.json` | 4 | Agent Kit CLI: HITL framework install and tooling for AI-assisted IDEs (rules, skills, plan/handoff, context). | allowlist-synced (`packages/**` in `scripts/public-sync.manifest`) | +| `packages/cli/README.md` | 3 | Agent Kit CLI: HITL operating-layer install and tooling for AI-assisted IDEs (rules, skills, plan/handoff, staging-to-prod, memory). It installs local workspace contracts; it is not a hosted control plane or graph workflow runtime. | allowlist-synced (`packages/**` in `scripts/public-sync.manifest`); npm pack storefront (`prepublishOnly` → `scripts/verify-cli-dashboard-pack.mjs`) | +| `packages/cli/src/index.ts` | 24 | HITL framework for AI-assisted IDEs | allowlist-synced (`packages/**` in `scripts/public-sync.manifest`) | +| `.cursor-plugin/plugin.json` | 5 | HITL framework for AI-assisted IDEs — plan, handoff, staging→prod, memory loop, anti-slop. Stack skills via agent-kit add. | allowlist-synced (`.cursor-plugin/**` in `scripts/public-sync.manifest`) | +| `.cursor-plugin/plugin.json` | 7-13 | ["agents","hitl","handoff","git-staging","context","multi-ide","anti-slop"] | allowlist-synced (`.cursor-plugin/**` in `scripts/public-sync.manifest`) | +| `docs/README.md` | 3 | **Mission Kit** is the product-family name on missionkit.io. **Agent Kit** is the technical install surface … HITL framework for AI-assisted IDEs … | allowlist-synced (`docs/**` in `scripts/public-sync.manifest`) | +| `docs/getting-started.md` | 3 | **Mission Kit** (marketing / missionkit.io) ships as **Agent Kit** on install … keeps your AI coding agent working against a plan … | allowlist-synced (`docs/**` in `scripts/public-sync.manifest`) | +| `docs/CONTRIBUTING.md` | 3 | **Mission Kit** is the storefront name; contributions land in the **Agent Kit** repository … HITL framework for AI-assisted IDEs. | allowlist-synced (`docs/**` in `scripts/public-sync.manifest`) | +| `docs/github-about.md` | 8 | Mission Kit 5: development operations for Cursor and VS Code. HITL plans, handoff, memory, and staging→prod git … (Installs as Agent Kit.) | allowlist-synced (`docs/**` in `scripts/public-sync.manifest`) | +| `docs/github-about.md` | 14 | HITL development operations for AI-assisted IDEs: plan → handoff → staging → prod (Agent Kit install / CLI). | allowlist-synced (`docs/**` in `scripts/public-sync.manifest`) | +| `docs/github-about.md` | 20 | Harness human-in-the-loop para IDEs com IA: planos, handoff de contexto, memory loop e fluxo git staging→prod com confirmação explícita antes de produção. | allowlist-synced (`docs/**` in `scripts/public-sync.manifest`) | +| `docs/github-about.md` | 25 | `ai-assisted-development` `cursor` `vscode` `windsurf` `developer-tools` `cli` `monorepo` `agent-kit` `prompt-engineering` `skills` `templates` `handoff` `context-management` `human-in-the-loop` | allowlist-synced (`docs/**` in `scripts/public-sync.manifest`) | +| `docs/cursor-native-audit.md` | 30 | HITL framework for AI-assisted IDEs (plan, handoff, staging→prod, memory loop) | allowlist-synced (`docs/**` in `scripts/public-sync.manifest`) | +| `docs/public-launch-announcement.md` | 12 | Para resolver isso (e a falta de DevOps estruturado no fluxo do agente), existe o *Mission Kit 5*: development operations dentro do Cursor e do VS Code. Site: https://missionkit.io | allowlist-synced (`docs/**` in `scripts/public-sync.manifest`) | +| `docs/public-launch-announcement.md` | 14 | No GitHub, npm e CLI o projeto ainda se chama *Agent Kit* (pacote `@dadado/agent-kit-cli`, comando `agent-kit`, `/agent-kit-onboard`). Mesmo produto, dois nomes de propósito. 🛠️ | allowlist-synced (`docs/**` in `scripts/public-sync.manifest`) | +| `docs/public-launch-announcement.md` | 17 | Uma camada operacional leve que transforma seu IDE em um framework HITL: planejamento, handoff entre chats, revisão externa opcional e fluxo Git staging→prod com confirmação antes de produção. Não é marketing de "autonomia sem freio". | allowlist-synced (`docs/**` in `scripts/public-sync.manifest`) | +| `docs/public-launch-announcement.md` | 10 | Se você usa o Cursor ou outro IDE com IA assistida para codar, já deve ter passado pelo clássico problema de ver a IA se perder e alucinar quando o chat fica muito longo e o contexto enche. 🤯 | allowlist-synced (`docs/**` in `scripts/public-sync.manifest`) | +| `docs/public-launch-announcement.md` | 16 | *🤔 O que é?* | allowlist-synced (`docs/**` in `scripts/public-sync.manifest`) | +| `docs/public-launch-announcement.md` | 19 | *✨ O que ele resolve?* | allowlist-synced (`docs/**` in `scripts/public-sync.manifest`) | +| `docs/public-launch-announcement.md` | 20 | • *Onboarding & Setup Inteligente:* analisa o projeto e prepara regras, comandos e skills sob medida (`/agent-kit-onboard`). 🧠✨ | allowlist-synced (`docs/**` in `scripts/public-sync.manifest`) | +| `docs/public-launch-announcement.md` | 21 | • *Sem perda de contexto:* estado vivo entre chats; um comando e a IA sabe onde parou. 🔄 | allowlist-synced (`docs/**` in `scripts/public-sync.manifest`) | +| `docs/public-launch-announcement.md` | 22 | • *Planos de verdade:* to-dos reais, com humano no loop (Ask questions). 📋 | allowlist-synced (`docs/**` in `scripts/public-sync.manifest`) | +| `docs/public-launch-announcement.md` | 23 | • *DevOps integrado:* staging automático, Conventional Commits, hooks. 🛡️ | allowlist-synced (`docs/**` in `scripts/public-sync.manifest`) | +| `docs/public-launch-announcement.md` | 24 | • *Produção com confirmação:* staging pode ir sozinho; `main` só depois de você confirmar. 🛑 | allowlist-synced (`docs/**` in `scripts/public-sync.manifest`) | +| `docs/public-launch-announcement.md` | 25 | • *Mission Control:* cockpit local (`/dashboard` / `agent-kit dashboard`) sobre o que está no disco. | allowlist-synced (`docs/**` in `scripts/public-sync.manifest`) | +| `docs/public-launch-announcement.md` | 27 | *🚀 Como usar?* | allowlist-synced (`docs/**` in `scripts/public-sync.manifest`) | +| `docs/public-launch-announcement.md` | 30 | `npx @dadado/agent-kit-cli install` | allowlist-synced (`docs/**` in `scripts/public-sync.manifest`) | +| `docs/public-launch-announcement.md` | 34 | Depois: `/agent-kit-onboard`, `/start-project`, `/continue-plan` ou `/run-plan`. | allowlist-synced (`docs/**` in `scripts/public-sync.manifest`) | +| `install.md` | 1 | Mission Kit / Agent Kit - Installation | allowlist-synced (`install.md` in `scripts/public-sync.manifest`) | +| `install.md` | 3 | You are the installer for Mission Kit 5. Marketing name: Mission Kit; technical surfaces: Agent Kit … | allowlist-synced (`install.md` in `scripts/public-sync.manifest`) | | `registry/registry.json` | 8-342 | 34 skill/pack descriptions across line range (e.g., "Remove AI code slop", "HITL framework install and tooling", "Create and update ClickUp tasks via MCP") | public-repo-PR-only (excluded line 89) | | `registry/packs/clean-code/pack.json` | 5 | Deslop, simplicity, and surgical refactors for AI-assisted codebases. | public-repo-PR-only (excluded line 89) | | `registry/packs/context-management/pack.json` | 5 | Advanced context packs, librarian/extractor agents, and window-budget helpers beyond L0 guardian, memory-loop, and native phase/context hooks. | public-repo-PR-only (excluded line 89) | @@ -400,4 +395,4 @@ Enumerated identity literals and publication routes from `scripts/public-sync.ma | `_legacy/v2/skills-registry.json` | 9 | Escreve e revisa SQL para Postgres (DDL, DML, índices, constraints). Use ao editar .sql, schemas para n8n ou quando o usuário mencionar tabelas, migrations, Postgres. | allowlist-synced (_legacy/** line 85) | | `_legacy/v2/skills-registry.json` | 10 | Boas práticas para fluxos conversacionais em agentes de chat (mensagens curtas, tom, confirmações, handoff). Use ao criar prompts de chat, WhatsApp, Telegram ou revisar fluxos de atendimento. | allowlist-synced (_legacy/** line 85) | | `_legacy/v2/skills-registry.json` | 11 | Remove AI-generated code slop (redundant comments, unnecessary try/catch, any casts, deep nesting). Use after AI-assisted coding sessions or before code review to clean up machine-generated patterns. | allowlist-synced (_legacy/** line 85) | -| `skills-registry.json` | 5-10 | Create and update ClickUp tasks via MCP..., Validate, format, and manipulate JSON..., Create, edit and document n8n workflows..., Create and edit agent prompts in Markdown..., Write and review SQL for Postgres..., Best practices for conversational flows... | allowlist-synced (no exclusion matches) | +| `skills-registry.json` | 5-10 | Create and update ClickUp tasks via MCP..., Validate, format, and manipulate JSON..., Create, edit and document n8n workflows..., Create and edit agent prompts in Markdown..., Write and review SQL for Postgres..., Best practices for conversational flows.. | allowlist-synced (no exclusion matches) | diff --git a/docs/consumer-configuration.md b/docs/consumer-configuration.md index 70dca18..90e4575 100644 --- a/docs/consumer-configuration.md +++ b/docs/consumer-configuration.md @@ -44,6 +44,9 @@ Writable via tab = the Mission Control Config tab (More menu) can save the key t | `onboarded`, `onboarding.status`, `onboarding.contractVersion` | onboarding flows | readiness, Mission Control | Yes | No (display-only in the Read-only fieldset) | written by `/agent-kit-onboard` | | `onboarding.checks` | readiness scanner | readiness | Yes | Never | not editable | | `dogfood.factoryRoot` (absolute path or null) | `config.example.json`, `.cursor/commands/dogfood.md` | `/dogfood` bridge step | Yes | No | `{ "dogfood": { "factoryRoot": "/absolute/path/to/agent-kit-dev" } }` | +| `missionControl.preferredBrowser` (string or null) | `config.example.json` | `dashboard/lib/open-browser.mjs`, `agent-kit dashboard` / `dashboard-broadcast` starters | Yes | No (edit config.json; not on Config-tab allowlist) | Platform-specific **name** only (not a path or shell expression): macOS app display name (`"Google Chrome"`), Linux binary on `PATH` (`"firefox"`), Windows program (`"msedge"` / `"chrome"`). null / omit / `"default"` / `"os"` → OS default; `"ask"` → slash Ask only, CLI still uses OS default. **Trust boundary:** values with `/`, `\`, `:`, or shell metacharacters are rejected and treated as OS default (ADR Evidence). | + +Audits knobs trade **coverage vs throughput**: keep `config.example.json` coverage defaults (`midBatchAudits: true` under `mode: "autonomous"`, `offerOnExhausted: true`, `preflight: "warn"`) unless you need fewer arms. Lower cost with `midBatchAudits: false` and/or `offerOnExhausted: false`; stop residual conveyors at triage (depth/Ack), not by flipping `autoRemediate`. See [external-plan-review.md](external-plan-review.md#throughput-vs-coverage-operator-knobs) and ADR `decisions/2026-08-11_plan-audit-residuals-termination.md`. ## Dashboard skin (localStorage-only) @@ -77,7 +80,7 @@ Invocation-time inputs; not persisted configuration. The table lists consumer-fa | Surface | Flags / env vars | Notes | |---------|------------------|-------| | `agent-kit run-plan` | `--max-ticks N`, `--model M`, `--sleep S`, `--backend cursor-agent\|claude`, `--dry-run` | `--backend claude` is **reserved, not implemented** (`packages/cli/src/plan-loop/backends.ts` throws); use `cursor-agent`. Distinct from `externalPlanReview.backend: claude`, which is the working Audits launcher backend. | -| Mission Control server | `PORT` (default 3333), `HOST`, `MISSION_CONTROL_REPO_ROOT`, `MISSION_CONTROL_KIT_ROOT` / `AGENT_KIT_HOME` (Path C kit-root fallback), `MISSION_CONTROL_TOKEN` (LAN broadcast), `MISSION_CONTROL_NO_OPEN`, `AGENT_KIT_DASHBOARD_DATA_TIMEOUT_MS`, `AGENT_KIT_DASHBOARD_DATA_BUDGET_MS` | Kit-root fallback is documented in README Path C and `/dashboard`. | +| Mission Control server | `PORT` (default 3333), `HOST`, `MISSION_CONTROL_REPO_ROOT`, `MISSION_CONTROL_KIT_ROOT` / `AGENT_KIT_HOME` (Path C kit-root fallback), `MISSION_CONTROL_TOKEN` (LAN broadcast), `MISSION_CONTROL_SHARE_BASE` / `MISSION_CONTROL_SHARE_TTL_SEC` / `MISSION_CONTROL_SHARE_SHOW_LAN` (cosmetic Share URL mask), `MISSION_CONTROL_NO_OPEN`, `MISSION_CONTROL_PREFERRED_BROWSER`, `agent-kit dashboard --browser`, `agent-kit dashboard-broadcast --browser`, `AGENT_KIT_DASHBOARD_DATA_TIMEOUT_MS`, `AGENT_KIT_DASHBOARD_DATA_BUDGET_MS` | Kit-root fallback is documented in README Path C and `/dashboard`. Preferred browser: one OS open (app/binary) or OS default; never multi-open. ADR `2026-08-11_mission-control-preferred-browser.md`. Share mask: ADR `2026-08-11_mission-control-broadcast-url-mask.md`. | | Registry / hooks | `AGENT_KIT_REGISTRY`, `ALLOW_MAIN_PUSH` | `ALLOW_MAIN_PUSH=1` disables main-push protection for the session (doctor warns). | ## How the Config tab saves diff --git a/docs/cursor-native-audit.md b/docs/cursor-native-audit.md index 54bf9c9..b3d064d 100644 --- a/docs/cursor-native-audit.md +++ b/docs/cursor-native-audit.md @@ -1,22 +1,22 @@ # Cursor-native audit - Agent Kit harness -Audit of Cursor-specific artifacts in the Agent Kit repository: what exists, what is missing, and how VS Code and Windsurf compare. Living audit; last refreshed **2026-07-19** (post EN sweep on staging). +Audit of Cursor-specific artifacts in the Agent Kit repository: what exists, what is missing, and how VS Code and Windsurf compare. Living audit; last refreshed **2026-08-12** (inventory cwd harden + Action A4/A5/A7 refresh). -**Awareness check (advisory):** `agent-kit cursor-awareness --check` and `/cursor-update-awareness` diff Cursor changelog signals against this inventory without mutating it. Version-prose / Marketplace packaging refresh remains on the parked Marketplace plan. See [cursor-update-awareness.md](cursor-update-awareness.md). +**Awareness check (advisory):** `agent-kit cursor-awareness --check` and `/cursor-update-awareness` diff Cursor changelog signals against this inventory without mutating it. Version-prose and Marketplace packaging were refreshed on 2026-08-05; only the live submission stays on the Marketplace plan (publisher HITL). See [cursor-update-awareness.md](cursor-update-awareness.md). ## Summary | Area | Status | Notes | |------|--------|-------| -| `.cursor/rules/` | Present (23 files) | 8 core rules `alwaysApply: true`; stack rules use globs | -| `.cursor/skills/` | Present (7 skills) | Install output from registry (`core/` + `community/`) | +| `.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/commands/` | Present (10 commands) | DevOps spine + handoff + orchestration | +| `.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)** | `sessionStart` + `preCompact`; no `stop` hook | -| `.cursor-plugin/plugin.json` | Present | Metadata only; version `3.0.0` (drift vs product **4.2.1**) | +| `.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 | | `git-hooks/prepare-commit-msg` | Present | Strips Cursor co-author trailer | -| `AGENTS.md` (dogfood) | **Absent** | CLI generates it for target projects; this repo does not use its own cross-IDE file | +| `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,22 +28,24 @@ Audit of Cursor-specific artifacts in the Agent Kit repository: what exists, wha | Field | Value | |-------|-------| | name | `agent-kit` | -| version | `3.0.0` | -| description | HITL framework for AI-assisted IDEs (plan, handoff, staging→prod, memory loop) | +| version | `5.0.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 | **Findings:** - Plugin manifest exists for Marketplace distribution path (install port A). -- Manifest is metadata-only: no `rules`, `skills`, or `hooks` entries in the plugin schema used here. -- CLI `init` also writes `plugin.json` into target projects with equivalent fields. +- Manifest is no longer metadata-only. The plugin root is the parent of `.cursor-plugin/`, and Cursor's default discovery reads `rules/`, `skills/`, `agents/`, `commands/`, `hooks/hooks.json` at that root - paths this repo does not use. Without explicit entries the plugin would list with **zero** components. Packaging contract: [marketplace.md](marketplace.md). +- `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. +**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. --- ## Rules - modes and coverage -**Location:** `.cursor/rules/*.mdc` (23 files) +**Location:** `.cursor/rules/*.mdc` (25 files) ### alwaysApply: true (core - structural) @@ -142,18 +144,22 @@ Separate from `.cursor/hooks/`; optional hygiene for teams that reject bot co-au Cursor subagent definitions. Consumed via Task tool / Agents Window. Full classification in [coherence-inventory.md](coherence-inventory.md). -### Commands (`.cursor/commands/` - 10) +### Commands (`.cursor/commands/` - 27) -Slash commands in Cursor: +Slash commands in Cursor. Every file carries `name` + `description` frontmatter (required by the Marketplace submission checklist; `name` matches the filename slug, so nothing was renamed). | Command | Spine | |---------|-------| | `/continue-plan` | Handoff resume | | `/handoff` | Save HANDOFF | -| `/run-plan-loop`, `/run-plan-orchestrated` | Continuous loop / worker delegation (no `/git-prod`) | +| `/run-plan`, `/run-plan-all` | Continuous run / multi-plan queue (no `/git-prod`) | +| `/run-plan-loop`, `/run-plan-orchestrated` | Deprecated aliases forcing one strategy | | `/git-staging`, `/git-prod` | DevOps spine | -| `/start-project` | Bootstrap (two HITL gates) | -| `/context-status` | Context pack status | +| `/start-project`, `/agent-kit-onboard`, `/hotfix` | Bootstrap and narrow-change entry | +| `/backlog-add`, `/backlog-edit`, `/backlog-cancel`, `/backlog-delete`, `/archive-plan` | Plan queue management | +| `/plan-external-review`, `/plan-review-triage`, `/field-report-resolve` | Audits and residual triage | +| `/dashboard`, `/dashboard-broadcast` | Mission Control | +| `/context-status`, `/update`, `/cursor-update-awareness`, `/dogfood` | Lifecycle and awareness | | `/summary`, `/tips` | UX helpers | Commands are **Cursor-only**. VS Code/Windsurf have no equivalent slash-command files; parity relies on `AGENTS.md` and IDE-specific instruction files. @@ -213,7 +219,7 @@ The Agent Kit repo uses the full Cursor workspace and **does**: Still open dogfood gaps: -2. Include root `AGENTS.md` (cross-IDE contract) +2. ~~Include root `AGENTS.md` (cross-IDE contract)~~ **Done** (root `AGENTS.md` present) 3. Auto-install git hooks on clone (documented manual copy) 4. Ship `mcp.json` for optional MCP servers @@ -228,10 +234,10 @@ Acceptable for private SoT until Phase B registry cutover defines minimum dogfoo | A1 | ✅ Done | Fix `ux-tone.mdc` frontmatter | | A2 | ✅ Done | Refresh [coherence-inventory.md](coherence-inventory.md) + [drift-inventory.md](drift-inventory.md) (2026-07-19) | | A3 | ✅ Done | `.cursor/hooks.json` + agent scripts (`sessionStart` / `preCompact`) | -| A4 | Open | Align CLI git-hooks with `.cursor/hooks/pre-commit/` chain | -| A5 | Open | Add root `AGENTS.md` to agent-kit repo (dogfood) | -| A6 | Open | Document Marketplace plugin path + VS Code/Windsurf install | -| A7 | Open | Expand generators or template files for multi-IDE parity | +| 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) | --- diff --git a/docs/cursor-update-awareness.md b/docs/cursor-update-awareness.md index 3692904..e38191e 100644 --- a/docs/cursor-update-awareness.md +++ b/docs/cursor-update-awareness.md @@ -25,9 +25,16 @@ ADR: `.cursor/memory/decisions/2026-08-01_cursor-update-detection-source.md` ## CLI ```bash -agent-kit cursor-awareness --check [--json] [--respect-prefs] [--stamp] [--offline] +agent-kit cursor-awareness --check [--cwd <path>] [--json] [--respect-prefs] [--stamp] [--offline] ``` +### Inventory path / cwd + +The check resolves `docs/cursor-native-audit.md` by walking up from `--cwd` (default: `process.cwd()`) until the file exists. Nested monorepo directories (for example `packages/cli`) therefore reuse the kit-root inventory. Prefs and `--stamp` still read/write `.cursor/context/config.json` under the caller `--cwd`. + +If no inventory is found in any ancestor (typical consumer checkout without the factory docs tree), the result is `status: error` with an actionable hint to pass `--cwd` at the kit/repo root that contains the inventory. There is no silent success without an inventory file. + +Secondary map `docs/cursor-3-features.md` is read from the same resolved inventory root. ## Slash command `/cursor-update-awareness` runs the check, summarizes gaps, then Ask-routes to `/backlog-add` or `/dogfood`. diff --git a/docs/design-system.md b/docs/design-system.md new file mode 100644 index 0000000..b7e6fe9 --- /dev/null +++ b/docs/design-system.md @@ -0,0 +1,45 @@ +# Design System + +Agent Kit maintains a remote design system in the **Claude Design** project +*Startup Kit — Design System* (project id `4451a0e9-5258-45cd-91f7-a837bdcbde81`). +The project documents three products: Startup Kit (`auto.startupkit.com.br`), +Mission Control (the `dashboard/dashboard.html` UI), and the Agent Kit landing +page (`missionkit.io`). + +## Upstream and downstream + +The repo is the **source of truth**. The remote design project is a downstream +mirror — it was populated from tree-extracted sources and is updated manually +when the repo changes. If any value in the remote DS differs from the code, the +code wins. + +| Surface | Upstream (repo) | What was mirrored | +|---------|-----------------|-------------------| +| Mission Control | `dashboard/dashboard.html` | Token tables, skin variables, component classes | +| Landing page | `.cursor/context/landing-missionkit/remote/` | CSS, component markup, production PNGs | +| Startup Kit marketing site | off-repo CSS tree (local design export; not tracked here) | Tokens, painel CSS, component markup | + +## Token divergence: `--text-muted` + +`dashboard/dashboard.html` sets `--text-muted: #6d8094`. +The landing sets `--text-muted: #7f93a8`. +The difference is intentional: the landing value was chosen to meet the AA +contrast ratio over the marketing gradient. Do not unify these tokens without +redoing the contrast math. See the comment beside the token in `dashboard.html` +and residual D in the transport monitor. + +## Update the remote DS + +To keep the remote DS current after a token or component change: + +1. Extract the relevant section from the source file (`awk` or copy). +2. Open the Claude Design project and update the affected page. +3. Record the sync in a commit message or CHANGELOG entry. + +Do not treat the remote DS as ground truth; always compare against the repo. + +## References + +- [Landing page runbook](agentkit-landing.md) +- Landing source-of-record decision: `.cursor/memory/decisions/2026-08-05_landing-external-design-source-of-record.md` (factory private; not a markdown link, so public sync does not emit a dead URL) +- Transport closeout monitor: `.cursor/memory/plan-monitor-design-system-transport-claude-design.md` (factory private; plain path, not a link) diff --git a/docs/external-plan-review.md b/docs/external-plan-review.md index 39a2711..43b6289 100644 --- a/docs/external-plan-review.md +++ b/docs/external-plan-review.md @@ -86,7 +86,7 @@ Monitors are durable evidence. Beyond Field Report and `/plan-review-triage` (at | Surface | Role | |---------|------| -| `/start-project`, `/backlog-add`, `/plan-review-triage` Write residuals Broad Intake | Memory bucket + worker `read_scope` include monitors/audits; same triage labels (`ignore` / `error` / `include` / `note`) | +| `/start-project`, `/backlog-add`, `/plan-review-triage` Write residuals Broad Intake | Memory bucket + **Unprocessed dogfood** (`dogfood/README.md` or `.cursor/dogfood/README.md`, `##` or `### Unprocessed Files`) + worker `read_scope` include monitors/audits and dogfood paths; same triage labels (`ignore` / `error` / `include` / `note`) | | `/continue-plan` | Pre-unit advisory skim for the chosen plan slug | | `memory-loop` CHECK | Glob monitors/audits before deep re-investigation | | `/git-staging` | Dirty untracked monitor warn; add-by-name only | @@ -98,7 +98,7 @@ ADR: `.cursor/memory/decisions/2026-07-27_plan-monitor-consumer-awareness.md`. Use `/plan-review-triage` to process the monitor with clickable options: -- **Write residuals plan:** Run the same Broad Intake as `/backlog-add` (buckets + triage labels), propose a residuals plan from Still open plus intake findings, confirm with Ask (`Write plan to backlog` / `Modify` / `Cancel`), write `.cursor/plans/*.plan.md` and append HANDOFF Backlog (no Gate B; `/start-project` is an optional escape hatch only) +- **Write residuals plan:** Run the same Broad Intake as `/backlog-add` (buckets + triage labels), propose a residuals plan from Still open plus intake findings, confirm with Ask (`Write plan to backlog` / `Modify` / `Cancel`), write `.cursor/plans/*.plan.md` and append HANDOFF Backlog (no Gate B; `/start-project` is an optional escape hatch only). **Termination:** max closeout depth 1 per theme family; prefer **Ack and stop** / **Fix nits only** for nits or process-only Still open; do not mint unbounded `close-*` plans (ADR `decisions/2026-08-11_plan-audit-residuals-termination.md`) - **Fix nits only:** Address small issues directly (typos, formatting, obvious omissions) - **Ack and stop:** Note findings for future reference without immediate action @@ -131,6 +131,19 @@ Mission Control **Flight Log** shows HANDOFF Gaps (**NOW** + **Earlier** history | `midBatchAudits` | When true, `/run-plan-all` runs full audits mid-queue (after each plan) and at queue end via the launcher `--batch` path. Prefer true under `mode: "autonomous"`. Missing or false preserves queue-end / owed-ledger behavior for paste installs. | | `preflight` | Audits pre-flight on plan-run commands (`/continue-plan`, `/run-plan`, `/run-plan-all`, `/hotfix`, …): `"off"` (default when missing), `"warn"` (surface once), or `"block"` (arm or stop until owed audits are launched or explicitly deferred). Enforced in L0 (`continue-plan`, `run-plan`, `run-plan-all`, `hotfix`, HITL gate table). | +### Throughput vs coverage (operator knobs) + +Example defaults in `config.example.json` favor **coverage** under autonomous mode (`midBatchAudits: true`, `offerOnExhausted: true`, `preflight: "warn"`) without silently turning audits off. Reduce conveyor cost without abandoning review: + +| Goal | Knob | Effect | +|------|------|--------| +| Fewer mid-queue Claude arms | `midBatchAudits: false` | Queue-end / owed ledger only; triage still applies when monitors exist | +| Less exhaustion nag | `offerOnExhausted: false` | No chat Ask when disabled; manual `/plan-external-review` still works | +| Soft pre-run pressure | `preflight: "off"` \| `"warn"` \| `"block"` | `off` skips; `warn` surfaces once; `block` arms or stops | +| Stop residual spawn loops | Triage termination (L0) | Max closeout depth 1; Ack/Fix nits preferred for process-only; never silent-Ack Blocking (ADR `decisions/2026-08-11_plan-audit-residuals-termination.md`) | + +Do **not** flip `autoRemediate` to `true` as a shortcut for fewer backlog plans; that weakens findings-only HITL. Prefer Ack / Fix nits / depth gate at `/plan-review-triage`. + ## Script options Canonical launcher: `.cursor/scripts/plan-external-review.sh` (wrapper: `scripts/plan-external-review.sh`). @@ -257,6 +270,7 @@ AGENT_KIT_AUDIT_SESSION_CAP=1 .cursor/scripts/plan-external-review.sh --force -- - Symptom: repeated autonomous arms leave `agent-kit-audit-<ws8>-<pid>` sessions detached (each one a PTY that never produced a monitor), and later arms keep adding to the pile. - Policy (preventive, not cleanup of a known pile): the launcher counts detached **workspace-owned** sessions before every autonomous spawn, warns at or above `AGENT_KIT_AUDIT_SESSION_WARN` (default 5), and refuses to spawn at or above `AGENT_KIT_AUDIT_SESSION_CAP` (default 20) instead of adding one more. Attached sessions are operator work in progress and are never counted or disposed. Foreign-workspace and legacy unscoped `agent-kit-audit-<pid>` names are out of scope for cap/reap. +- Backlog: a liveness probe that checks whether each counted detached session is still alive (multiplexer session present and/or scrollback growing) before refusing at the cap would distinguish healthy concurrent arms from a zombie pile. The current count is by session name only, so a host running many genuine concurrent reviews still triggers the cap and must use `--reap-audit-sessions` or raise the cap to continue. - Inspect: `screen -ls` (or `tmux ls`). - Dispose (opt-in, detached kit-owned sessions past the age floor only): diff --git a/docs/five-layer-claim-matrix.md b/docs/five-layer-claim-matrix.md index be4abf9..171b069 100644 --- a/docs/five-layer-claim-matrix.md +++ b/docs/five-layer-claim-matrix.md @@ -1,6 +1,6 @@ # Five-layer claim matrix (public) -Public storefront summary of how Agent Kit maps to a five-layer production-agent lens. Documentation alone is not proof of behavior. The private factory keeps a fuller evidence ledger under `docs/evidence/` (sync-denied); this page cites only paths that ship on the public lane. +Public storefront summary of how **Mission Kit** / **Agent Kit** maps to a five-layer production-agent lens (marketing name vs technical install identifiers; see naming ADR `2026-08-06_mission-kit-vs-agent-kit-naming`). Documentation alone is not proof of behavior. The private factory keeps a fuller evidence ledger under `docs/evidence/` (sync-denied); this page cites only paths that ship on the public lane. Classification: **shipped core** (L0), **optional pack** (L1/L2), **planned**, **unsupported**. diff --git a/docs/getting-started.md b/docs/getting-started.md index 18e8ce8..80e085d 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -1,6 +1,6 @@ # Getting Started -Agent Kit keeps your AI coding agent working against a plan and stops you from losing context when a chat gets too long. This guide covers installing it, the commands you get, and how a normal day looks. +**Mission Kit** (marketing / [missionkit.io](https://missionkit.io)) ships as **Agent Kit** on install: CLI, npm, and slash commands. The kit keeps your AI coding agent working against a plan and stops you from losing context when a chat gets too long. This guide covers installing it, the commands you get, and how a normal day looks. ## Install @@ -14,8 +14,27 @@ 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: + +```bash +npx -y @dadado/agent-kit-cli install --yes +``` + +- `npx -y` answers `npx`'s "Ok to proceed?" prompt when the package is not cached. +- `--yes` (or `AGENT_KIT_YES=1`) skips the CLI's project-root confirmation prompt. + +**Troubleshooting npm failures:** + +| Symptom | Cause | Recovery | +|---------|-------|----------| +| `EPERM` / `EACCES` on npm cache | User-level cache ownership drift | `npx --cache .npm-cache @dadado/agent-kit-cli install` or `npm cache clean --force` | +| Exit 255 (no output) | `npx` prompted for confirmation in a non-TTY environment | Use `npx -y @dadado/agent-kit-cli install` for the `npx` prompt; add `--yes` or set `AGENT_KIT_YES=1` for the CLI root prompt | +| `403 Forbidden` from registry | Auth policy or private scope | `npm login`, check `.npmrc`, or use Port B fallback | + That's the whole install for kit L0. It drops a small set of rules and slash commands into `.cursor/`, a git routine into `autogit/`, and a manifest (`.cursor/agent-kit.json`) that records what was installed so the kit can update itself later without touching your work. Mission Control's `dashboard/` server is **not** copied into your project; the panel runs from the CLI package (4.8.2 onward) or from an agent-kit checkout. See [Mission Control production-ship constraints](#mission-control-production-ship-constraints). +**Multi-workspace safety:** the CLI confirms the absolute project root before writing any files (interactive prompt; `--yes` skips the prompt). Each project gets its own `.cursor/` tree and overlay ledger. The shared registry cache (`~/.cache/agent-kit/registry/`) uses a directory lock so parallel installs on the same machine cannot corrupt it. + Want a few extra bundles up front? Add packs (clean code, context tools, and more - see [domain packs](domain-packs.md)): ```bash @@ -48,7 +67,7 @@ Keep this path light. No extra runtime packages beyond the CLI (`@clack/prompts` 3. **Install** - `npx @dadado/agent-kit-cli install` (or Port B via `install.md`). 4. **Onboard** - `/agent-kit-onboard` until every essential readiness check is ready (non-essentials may defer with a recovery action). 5. **Kit commands** - e.g. `/start-project` in the consumer project. -6. **Mission Control panel (optional)** - `/dashboard`, `npm run dashboard`, or `agent-kit dashboard`. Consumer L0 does not copy `dashboard/` into the project. `agent-kit dashboard` resolves `dashboard/start.mjs` from the installed package (4.8.2 onward); on older pins use a kit checkout or env/sibling discovery. Loopback only (`127.0.0.1`) by default. Opt-in LAN: `/dashboard-broadcast` / `npm run dashboard:broadcast` (token-gated). Posture: [Mission Control production-ship constraints](#mission-control-production-ship-constraints). +6. **Mission Control panel (optional)** - `/dashboard`, `npm run dashboard`, or `agent-kit dashboard`. Consumer L0 does not copy `dashboard/` into the project. `agent-kit dashboard` resolves `dashboard/start.mjs` from the installed package (4.8.2 onward); on older pins use a kit checkout or env/sibling discovery. Loopback only (`127.0.0.1`) by default. Opt-in LAN: `agent-kit dashboard-broadcast` / `npm run dashboard:broadcast` (token-gated) prints a Mission Kit **Share** URL (`https://missionkit.io/mc/open.html#…`, BYO HTTPS via `MISSION_CONTROL_SHARE_BASE`; set `off` for LAN-only). The Share URL embeds the live token (same secret handling); soft TTL is advisory (`MISSION_CONTROL_SHARE_TTL_SEC`, `0` = never). Still requires trusted-LAN reachability; not a WAN relay. The slash `/dashboard-broadcast` ships in the **factory** checkout and CLI docs only (not an L0 consumer artifact); consumers use the CLI/npm entrypoints above. CLI/OS opens use one preferred browser (`missionControl.preferredBrowser`, `MISSION_CONTROL_PREFERRED_BROWSER`, or `--browser`; platform-specific **name**, not a path) or the OS default; slash `/dashboard` opens via IDE browser MCP only (not multi-browser). Posture: [Mission Control production-ship constraints](#mission-control-production-ship-constraints). ## The commands you get @@ -76,9 +95,9 @@ The idea is simple: work against a plan, save your place before a conversation g Operator sequence when you drive each unit (command SoT: [`.cursor/commands/continue-plan.md`](../.cursor/commands/continue-plan.md); Gate A/B SoT: [`.cursor/commands/start-project.md`](../.cursor/commands/start-project.md)): 0. **`/agent-kit-onboard`** *(first time / incomplete readiness)* - Repository preparation only: show detected facts, safe fixes, and one pending decision at a time via **Ask questions**. Completes when essential readiness checks pass (or allowed non-essential items are deferred with a recovery action). Skins and external review stay optional after essentials. Path after CLI install: open the folder in Cursor → `/agent-kit-onboard` → `/start-project`. Contract: [repository-readiness-onboarding.md](repository-readiness-onboarding.md). -1. **`/start-project`** - After the repository is prepared, Broad Intake Review and two gates using **Ask questions**: (A) the agent proposes and writes a plan with checkable to-dos (no coding yet); (B) only after you confirm, it runs the **first** unit. Broad Intake's Memory bucket consults `.cursor/memory/plan-monitor-*.md` and theme-matched `plan-review-*` audits (duplicates / open residuals / outdated reviews) without changing Field Report detection. Uses clickable options with chat fallback when tool unavailable. Goal text in the same message is not execute permission. -1b. **Backlog without activation** - `/backlog-add` runs the same Broad Intake (including plan-monitor consult), writes a plan, and appends it under HANDOFF Backlog plans (no Gate B, never parks or activates the new plan). Manage rows with `/backlog-edit`, `/backlog-delete` (move to `.cursor/plans/archive/`), and `/backlog-cancel` (soft-cancel open to-dos, keep the file). All mutates use **Ask questions** (chat numbered-list fallback). Distinct from `/archive-plan` (parked list only). Routine backlog CRUD does not create Field Report cards. -2. **Confirm the unit, then work one phase.** On `/continue-plan` (and after Gate B on a new plan), the agent uses **Ask questions** before editing (`Start [to-do-id]` / `Edit plan first` / `Switch to different plan` / `Stop here`). After you pick Start, it implements **only** that phase (or one heavy to-do), checks it off, updates `.cursor/HANDOFF.md`, and **stops**. Context Guardian plus **native Cursor hooks** (`sessionStart` / `preCompact`) enforce that boundary; multi-phase in one window needs `/run-plan` or `/run-plan-all` below. +1. **`/start-project`** - After the repository is prepared, Broad Intake Review (buckets listed in the command, including **Unprocessed dogfood** from `dogfood/README.md` or `.cursor/dogfood/README.md` under `##` or `### Unprocessed Files`) and two gates using **Ask questions**: (A) the agent proposes and writes a plan with checkable to-dos (no coding yet); (B) only after you confirm, it runs the **first** unit. Broad Intake's Memory bucket consults `.cursor/memory/plan-monitor-*.md` and theme-matched `plan-review-*` audits (duplicates / open residuals / outdated reviews) without changing Field Report detection. Unprocessed dogfood uses the same triage labels and never auto-analyzes (ADR `2026-08-11_dogfood-unprocessed-broad-intake-bucket.md`). Uses clickable options with chat fallback when tool unavailable. Goal text in the same message is not execute permission. +1b. **Backlog without activation** - `/backlog-add` runs the same Broad Intake (including plan-monitor consult and Unprocessed dogfood), writes a plan, and appends it under HANDOFF Backlog plans (no Gate B, never parks or activates the new plan). Manage rows with `/backlog-edit`, `/backlog-delete` (move to `.cursor/plans/archive/`), and `/backlog-cancel` (soft-cancel open to-dos, keep the file). All mutates use **Ask questions** (chat numbered-list fallback). Distinct from `/archive-plan` (parked list only). Routine backlog CRUD does not create Field Report cards. +2. **Confirm the unit, then work one phase.** On `/continue-plan` (and after Gate B on a new plan), the agent uses **Ask questions** before editing (`Start [to-do-id]` / `Edit plan first` / `Switch to different plan` / `Stop here`). Before that Ask it also runs advisory Unprocessed dogfood preflight (mention non-empty inbox; never block solely for inbox). After you pick Start, it implements **only** that phase (or one heavy to-do), checks it off, updates `.cursor/HANDOFF.md`, and **stops**. Context Guardian plus **native Cursor hooks** (`sessionStart` / `preCompact`) enforce that boundary; multi-phase in one window needs `/run-plan` or `/run-plan-all` below. `/run-plan` and `/run-plan-all` apply the same advisory dogfood preflight on first tick / queue confirm. 3. **Suggest staging when there is a diff.** After the unit, the agent should suggest `/git-staging` (you run it when ready). Do not expect automatic promote to production; `/git-prod` is always a separate HITL step. 4. **Handoff if the window fills.** `/handoff` (or guardian auto-handoff when configured) writes where things stand before context is lost. 5. **New chat → paste `/continue-plan`.** Open a **fresh** conversation for the next phase. The agent reads HANDOFF, confirms the next unit again, and continues without you re-explaining the project. Chat tone follows the Autopilot persona by default (see [personas contract](personas-contract.md)). Mission Control Checklist Actions can copy `/continue-plan <plan>`; Current mission shows operator-friendly Mode labels (auto mode / run all / human-in-the-loop; HANDOFF keeps raw Mode tokens) and offers copy-only `/git-staging` (and mode-aware `/continue-plan` when Mode is manual). @@ -152,8 +171,8 @@ When `/run-plan` finishes all implementable to-dos, you can get a second-agent c 3. **Headless / CI:** `agent-kit run-plan` may arm the launcher with `--force` (`claude -p`) in the runner shell 4. **Exhaustion Ask:** if not enabled and `offerOnExhausted` is not `false`, chat must Ask `Run review now` / `Always enable automatic` / `Not now`. `Not now` is per-session only (no persist) 5. **Manual:** `/plan-external-review` anytime after a plan is done -6. **Triage:** after Claude writes a monitor file, use `/plan-review-triage` with explicit path(s). **Write residuals plan** runs the same Broad Intake as `/backlog-add`, then write-confirm Ask, and enqueues on HANDOFF Backlog (no clipboard `/start-project` happy path). Mission Control **Flight Log** is Gaps + operator Warnings with palette-by-type notification chrome (natural Gaps voice; wipe Earlier on new flight; exact `none` for OK, not `none.…` as a yellow debit). When Gaps and Warnings are empty, it may show bounded untriaged review rows (per-row Copy triage / path); it does not host **Review all** / **Resolve all**. Chat `/plan-review-triage` remains HITL SoT -7. **`/run-plan-all`:** mid-batch audits when enabled; queue-end review Ask/arm for remaining owed targets (cadence / Field Report **owed** ledger covers soft-fail skips; Flight Log UI stays Gaps + Warnings, with quiet open-triage only when those lanes are empty; keep HANDOFF Gaps short / exact `none` when only audit plumbing changed) +6. **Triage:** after Claude writes a monitor file, use `/plan-review-triage` with explicit path(s). Prefer **Ack and stop** or **Fix nits only** when residuals are nits/process-only or closeout depth is already capped; **Write residuals plan** runs Broad Intake (including Unprocessed dogfood) then backlog write-confirm and must not spawn unbounded `close-*` chains (ADR `decisions/2026-08-11_plan-audit-residuals-termination.md`). Unprocessed dogfood Broad Intake bucket membership is SoT in ADR `decisions/2026-08-11_dogfood-unprocessed-broad-intake-bucket.md` (not the termination policy). Mission Control **Flight Log** is Gaps + operator Warnings with palette-by-type notification chrome (natural Gaps voice; wipe Earlier on new flight; exact `none` for OK, not `none.…` as a yellow debit). When Gaps and Warnings are empty, it may show bounded untriaged review rows (per-row Copy triage / path); it does not host **Review all** / **Resolve all**. Chat `/plan-review-triage` remains HITL SoT +7. **`/run-plan-all`:** mid-batch audits when enabled (findings-only mid-queue; no auto Write residuals); queue-end triage Ask with explicit paths prefers uniform Ack/Fix nits when depth-capped. Throughput knobs: [external plan review](external-plan-review.md#throughput-vs-coverage-operator-knobs) Claude Code on PATH is optional. If disabled or `claude` is missing, the kit continues with a tip and exit 0 (no CI failure). Details: [external plan review](external-plan-review.md). @@ -170,10 +189,12 @@ Mission Control is a **local, single-developer** observability panel. Treat it a | Bind | Default host is `127.0.0.1` for `/dashboard`. Never silently bind `0.0.0.0` from that path. Opt-in LAN: `/dashboard-broadcast` (CLI counterpart) binds a non-loopback interface only with explicit intent and a required token gate. | | Network surface | HTTP static files under `dashboard/`, JSON snapshot (`/dashboard-data.json` / `/api/data`), SSE (`/api/events`), and loopback-only allowlisted `PUT`/`PATCH /api/config`. In broadcast mode, static/snapshot/SSE require the session token. No product WebSockets or inbound webhooks. | | Mutations | UI CTAs are copy-only (clipboard + paste destination). Config write is the only mutation API and stays loopback + allowlist (including when broadcast is active). No git stage, process kill, server restart, or `/git-prod` from the panel. | -| Remote / shared hosting | Multi-user internet / WAN hosting remains rejected. Personal local-only (loopback-first) is the **default**. Opt-in personal LAN broadcast (`/dashboard-broadcast` + token) is supported for a single operator on a trusted LAN; see ADR below. | +| Remote / shared hosting | Multi-user internet / WAN hosting remains rejected. Personal local-only (loopback-first) is the **default**. Opt-in personal LAN broadcast (`/dashboard-broadcast` + token) is supported for a single operator on a trusted LAN; printed Share URLs are cosmetic fragment masks (ADR below), not relays. | | Resources | Cold snapshot and periodic refresh use local CPU/IO (`git`, `ps`, filesystem). Do not throttle SSE/poll to "save" Cursor Agent quota; quota levers are named model + `interTickCooldownMs` (see tip under `/run-plan` above). | -Source of truth: `.cursor/memory/decisions/2026-07-27_mission-control-personal-local-only-posture.md` (default product goal), `.cursor/memory/decisions/2026-07-27_mission-control-opt-in-lan-broadcast.md` (opt-in LAN path), plus `.cursor/memory/decisions/2026-07-24_mission-control-local-only-security.md` and `.cursor/memory/decisions/2026-07-26_mission-control-config-write-allowlist.md` (technical guards). +**Copy-only actions:** The panel copies text and names where to paste it: repository-relative paths go to the editor's file picker, slash commands go to the chat input, and past-chat references go to the past-chat picker. The panel cannot open a file or a chat, so no label claims that it can. + +Source of truth: `.cursor/memory/decisions/2026-07-27_mission-control-personal-local-only-posture.md` (default product goal), `.cursor/memory/decisions/2026-07-27_mission-control-opt-in-lan-broadcast.md` (opt-in LAN path), `.cursor/memory/decisions/2026-08-11_mission-control-broadcast-url-mask.md` (cosmetic Share URL), plus `.cursor/memory/decisions/2026-07-24_mission-control-local-only-security.md` and `.cursor/memory/decisions/2026-07-26_mission-control-config-write-allowlist.md` (technical guards). **Where the panel runs:** Consumer `npx` / `install.md` installs kit L0 (including the `/dashboard` command text) but **does not** copy `dashboard/**` into the app tree. Snapshot root is always the operator workspace. The UI host is either (1) a published `@dadado/agent-kit-cli` that ships `dashboard/**` (Path C, 4.8.2 onward), or (2) an agent-kit checkout (`MISSION_CONTROL_KIT_ROOT` / `AGENT_KIT_HOME` / sibling `../agent-kit` / monorepo `dashboard/`). Start with `/dashboard`, `npm run dashboard`, `agent-kit dashboard`, or `node dashboard/start.mjs` (see root README). Several workspaces may run concurrent instances: each gets a stable listen port from its repo root (see printed URL / `system.port`); Mission Control never kills another workspace's listener. @@ -183,17 +204,6 @@ The exact git steps behind staging and production live in `autogit/gitupdate.md` ## Working on Agent Kit itself -If you're developing the kit (not just using it): - -1. Install dependencies: `pnpm install` -2. Build the CLI: `pnpm build` -3. Try the scanner: `pnpm --filter @dadado/agent-kit-cli start scan` -4. Install into a test project: `pnpm --filter @dadado/agent-kit-cli start install --cwd /path/to/project` -5. Check this repo's own install: `pnpm --filter @dadado/agent-kit-cli start status` -6. Refresh this repo's own L0 from local source (factory self-consumer): - - First time: `pnpm --filter @dadado/agent-kit-cli start -- update --cwd . --seed-overlay` - - Later: `pnpm --filter @dadado/agent-kit-cli start -- update --cwd .` - - This is a local maintainer loop, not a public consumer update. See [CONTRIBUTING](CONTRIBUTING.md) for the three-way distinction (public consumer / factory self-consumer / public sync). +Maintainer setup, local CLI workflows, factory self-consumer refreshes, and Mission Control from a kit tree are documented in [DEVELOPMENT](DEVELOPMENT.md). This guide covers the consumer workflow after installation. See the root [README](../README.md) for the big picture and [CONTRIBUTING](CONTRIBUTING.md) for how changes flow. diff --git a/docs/github-about.md b/docs/github-about.md index b5b6678..5e22933 100644 --- a/docs/github-about.md +++ b/docs/github-about.md @@ -2,16 +2,16 @@ Use **Settings → General** (repo root on GitHub): fill **Description**, **Website** (if any), **Topics**. -## Description (English - aligned with README) +## Description (English - aligned with README / Mission Kit 5) ``` -Human-in-the-loop harness for AI-assisted IDEs - plans, context handoff, memory loop, and staging→prod git workflow with explicit confirmation before production. +Mission Kit 5: development operations for Cursor and VS Code. HITL plans, handoff, memory, and staging→prod git with explicit confirmation before production. (Installs as Agent Kit.) ``` **Shorter** (if the layout fits better): ``` -HITL framework for AI-assisted IDEs: plan → handoff → staging → prod, with a skill registry and opt-in stack packs. +HITL development operations for AI-assisted IDEs: plan → handoff → staging → prod (Agent Kit install / CLI). ``` ## Description (Português - optional) @@ -26,8 +26,11 @@ Harness human-in-the-loop para IDEs com IA: planos, handoff de contexto, memory ## Website -- Public repo: leave blank or use the docs/deploy URL when it exists. -- If a project site is published: `https://…` +``` +https://missionkit.io +``` + +Legacy `agent.startupkit.com.br` redirects here; do not set it as the GitHub Website. ## Social preview diff --git a/docs/marketplace.md b/docs/marketplace.md index 877872f..47b7a91 100644 --- a/docs/marketplace.md +++ b/docs/marketplace.md @@ -60,6 +60,27 @@ Same rules as `agent-kit contribute` (see [contribute-upstream.md](contribute-up 3. Submission / listing follows Cursor’s publisher flow for the public repo - do not document third-party gateways. 4. After Phase B cutover, Marketplace updates track **public** tags, not private history. +### Packaging contract + +The plugin root is the **parent of `.cursor-plugin/`** - the repo root. Cursor's default component discovery reads `rules/`, `skills/`, `agents/`, `commands/`, `hooks/hooks.json` at that root. Agent Kit keeps every component under `.cursor/`, so the manifest **must** declare each path explicitly. A metadata-only manifest validates but ships an empty plugin. + +| Manifest key | Value | Why | +|--------------|-------|-----| +| `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 | +| `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 | + +`author` is an **object** (`{ "name": … }`), not a string. Rules, agents, skills, and commands all need YAML frontmatter; command frontmatter carries `name` (kebab-case, matching the filename slug) plus `description`. + +Editing any file under `.cursor/{agents,skills,commands}/` changes its consumer-overlay hash: append the new hashes to `KNOWN_SHIPPED_OVERLAY_HASHES` (`packages/cli/src/lifecycle/overlay-known-hashes.ts`) in the same commit, keeping the prior entries. Without that, an unedited consumer copy is misread as customized and never refreshed. `packages/cli/src/lifecycle/overlay.test.ts` covers this for `.cursor/commands/summary.md` only. + +### Submission checklist (publisher HITL) + +Cursor reviews the **public** repo, so the manifest must have reached public `main` first: `/git-prod` → annotated `vX.Y.Z` tag → `sync-public` PR. Submit `https://github.com/agent-kit-startup/agent-kit` at [cursor.com/marketplace/publish](https://cursor.com/marketplace/publish). No agent performs this step. + ## Listing UX (CLI) ```bash @@ -78,5 +99,5 @@ Future nicety (not required for this to-do): `agent-kit search <query>` over `re - [x] Builder emits version/category into `registry.json` - [x] CONTRIBUTING quality gate documented - [x] This marketplace doc + plugin.json thesis/version note -- [ ] Live Cursor Marketplace submission (publisher ops / HITL) +- [ ] Live Cursor Marketplace submission (publisher ops / HITL) - packaging ready at **5.0.0**; blocked on promoting that manifest to the public mirror (still **4.8.9**) - [x] Phase B cutover so public catalog is not overwritten by private sync diff --git a/docs/npm-publish-checklist.md b/docs/npm-publish-checklist.md index 1b2b68b..818d5dd 100644 --- a/docs/npm-publish-checklist.md +++ b/docs/npm-publish-checklist.md @@ -5,8 +5,8 @@ Human-in-the-loop gate before the first npm publish or any publish that changes ## Preconditions - [ ] Release content is on `origin/main` (promote via `git prod` from `origin/staging` when the release is not already on `main`). -- [ ] Root `package.json` `version`, `packages/cli/package.json` `version`, and the dated section in `CHANGELOG.md` match the intended release (currently aligned at **4.2.1**). -- [ ] `pnpm lint`, `pnpm typecheck`, `pnpm test`, and `pnpm build` succeed on the commit you intend to tag. +- [ ] Root `package.json` `version`, `packages/cli/package.json` `version`, and the dated section in `CHANGELOG.md` match the intended release. **Tree today:** root + CLI + `CHANGELOG` `[5.0.0]` are aligned at **5.0.0**. **Registry today:** `npm view @dadado/agent-kit-cli version` is still **4.8.9** until `v5.0.0` publishes. Do not treat tree SemVer as live npm `latest`. +- [ ] `pnpm lint`, `pnpm typecheck`, `pnpm test`, and `pnpm build` succeed on the **exact commit you intend to tag** (CI-green-at-tagged-SHA; see `.cursor/memory/decisions/2026-08-02_npm-5.0-go-no-go.md`). A later green SHA does not repair a red tagged commit. - [ ] Public sync and other post-`git prod` steps are done or explicitly deferred per [repository-boundaries.md](repository-boundaries.md). ## Registry state (before first publish) @@ -25,7 +25,7 @@ Human-in-the-loop gate before the first npm publish or any publish that changes | Source | Field | Must match | |--------|--------|------------| -| Repository root | `package.json` → `version` | Target release (e.g. `4.2.1`) | +| Repository root | `package.json` → `version` | Target release (e.g. `5.0.0`) | | CLI package | `packages/cli/package.json` → `version` | Same as root | | Changelog | Latest dated `[x.y.z]` section | Same version and date | @@ -42,8 +42,8 @@ The `publish-npm` job in `.github/workflows/ci.yml` runs on **`v*` tags** only. ## Tag strategy and CI -- [ ] Tags use the form **`vMAJOR.MINOR.PATCH`** (e.g. `v4.2.1`). -- [ ] **Existing tags on `origin`:** `v4.0.0`, `v4.0.1`, `v4.1.0`, `v4.2.0`, `v4.2.1`. Pushing a **new** `v*` tag (or re-running CI for a tag) triggers `publish-npm` after `build` succeeds. +- [ ] Tags use the form **`vMAJOR.MINOR.PATCH`** (e.g. `v4.8.9`). +- [ ] **Existing tags on `origin`:** `v3.0.0` through `v4.8.9` (35 tags; full series: v3.0.0, v3.5.0, v3.5.1, v4.0.0, v4.0.1, v4.1.0, v4.2.0–v4.2.4, v4.3.0, v4.4.0–v4.4.7, v4.5.0–v4.5.1, v4.6.0, v4.7.0–v4.7.2, v4.8.0–v4.8.9). Next: `v5.0.0`. Pushing a **new** `v*` tag (or re-running CI for a tag) triggers `publish-npm` after `build` succeeds. - [ ] Tag the commit on `main` that matches the release version; do not tag staging-only commits unless that is an explicit exception documented in the release notes. - [ ] **Integrated with `/git-prod`**: Annotated tags are created automatically when absent during `/git-prod` workflow (step 9.5 in `autogit/gitupdate.md`). Manual tag creation via `git push origin vX.Y.Z` or GitHub Releases UI is fallback only. @@ -78,8 +78,12 @@ If the answer is not an explicit **yes**, stop. No tag push, no token change for ## After publish (verification) -- [ ] `npm view @dadado/agent-kit-cli version` matches the release. +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`). - [ ] Smoke install: `npx @dadado/agent-kit-cli@<version> --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. +- [ ] 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). - [ ] GitHub Actions `publish-npm` job for the tag shows publish success (not skip), when using CI. diff --git a/docs/public-launch-announcement.md b/docs/public-launch-announcement.md index 0758f68..7ea1062 100644 --- a/docs/public-launch-announcement.md +++ b/docs/public-launch-announcement.md @@ -1,47 +1,46 @@ # Public launch announcement -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) and the root `README.md`. +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). +Related: [Public launch go/no-go](public-launch.md). Naming: `2026-08-06_mission-kit-vs-agent-kit-naming`. ```text Fala, devs! 🚀 Se você usa o Cursor ou outro IDE com IA assistida para codar, já deve ter passado pelo clássico problema de ver a IA se perder e alucinar quando o chat fica muito longo e o contexto enche. 🤯 -Para resolver esse e outros problemas, como a falta de um fluxo de DevOps estruturado, conexão segura com ferramentas e versionamento, ao longo de um ano, fui desenvolvendo o *Agent Kit*! 🛠️ +Para resolver isso (e a falta de DevOps estruturado no fluxo do agente), existe o *Mission Kit 5*: development operations dentro do Cursor e do VS Code. Site: https://missionkit.io -Diga-se de passagem, meu primeiro repositório público. 🥶 +No GitHub, npm e CLI o projeto ainda se chama *Agent Kit* (pacote `@dadado/agent-kit-cli`, comando `agent-kit`, `/agent-kit-onboard`). Mesmo produto, dois nomes de propósito. 🛠️ -*🤔 O que é o Agent Kit?* -É uma camada operacional leve que transforma seu IDE (Cursor, VS Code, etc.) em um framework que gerencia o planejamento, o handoff entre chats e o fluxo de Git / DevOps estruturado para você focar no que importa. +*🤔 O que é?* +Uma camada operacional leve que transforma seu IDE em um framework HITL: planejamento, handoff entre chats, revisão externa opcional e fluxo Git staging→prod com confirmação antes de produção. Não é marketing de "autonomia sem freio". *✨ O que ele resolve?* -• *Onboarding & Setup Inteligente:* Ele analisa o seu projeto, descobre o que está faltando e gera regras, comandos e skills personalizados sob medida para a sua stack e padrões de código. 🧠✨ -• *Sem perda de contexto:* Ele mantém o estado do seu projeto vivo. Abriu um chat novo? Um comando e a IA já sabe exatamente onde parou. 🔄 -• *Planos de verdade:* "vibecoding" mas nem tanto. A IA trabalha em cima de to-dos reais que você acompanha passo a passo no loop. 📋 -• *DevOps integrado:* Fluxo de Git seguro com staging automático e commits limpos. 🛡️ -• *Segurança em produção:* A IA pode subir para staging sozinha, mas promover para `main` sempre exige sua confirmação direta. Hooks nativos protegem a IA de fazer isso alucinando. 🛑 +• *Onboarding & Setup Inteligente:* analisa o projeto e prepara regras, comandos e skills sob medida (`/agent-kit-onboard`). 🧠✨ +• *Sem perda de contexto:* estado vivo entre chats; um comando e a IA sabe onde parou. 🔄 +• *Planos de verdade:* to-dos reais, com humano no loop (Ask questions). 📋 +• *DevOps integrado:* staging automático, Conventional Commits, hooks. 🛡️ +• *Produção com confirmação:* staging pode ir sozinho; `main` só depois de você confirmar. 🛑 +• *Mission Control:* cockpit local (`/dashboard` / `agent-kit dashboard`) sobre o que está no disco. *🚀 Como usar?* -É simples e rápido de instalar o projeto: -1️⃣ *No terminal do seu projeto, rode:* +1️⃣ *No terminal do projeto:* `npx @dadado/agent-kit-cli install` -2️⃣ *Ou no chat do Cursor, cole o prompt de instalação rápida:* -(Se for preguiçoso feito eu, encontre o prompt completo no README e LEIA antes de executar, mané!) +2️⃣ *Ou no chat do Cursor:* cole o prompt de instalação do README (leia antes de executar). -Depois de instalado, você ganha comandos como `/agent-kit-onboard`, `/start-project` e `/continue-plan` diretamente no chat do seu editor. +Depois: `/agent-kit-onboard`, `/start-project`, `/continue-plan` ou `/run-plan`. --- 💡 *Quer contribuir?* -O projeto é 100% open-source! Achou um bug, tem ideias de novos comandos ou quer criar regras/skills personalizadas para a sua stack? Abre uma Issue ou manda uma PR lá no GitHub. Toda contribuição é muito bem-vinda! 🤝 +Source-available sob PolyForm Noncommercial (uso pessoal/não comercial gratuito; comercial: sales@missionkit.io). Issue ou PR no GitHub. 🤝 -👉 *Bora testar e dar aquela estrela no GitHub?* -Acesse o repositório oficial, veja a documentação completa e comece a usar agora: +👉 *Site + repo:* +https://missionkit.io https://github.com/agent-kit-startup/agent-kit -Show? Se testar, me conta o que achou! 👊🔥 +Show? Se testar, conta o que achou! 👊🔥 ``` diff --git a/docs/public-launch.md b/docs/public-launch.md index 88aa040..1a4b790 100644 --- a/docs/public-launch.md +++ b/docs/public-launch.md @@ -96,16 +96,17 @@ After `git prod` on the private repo, the pipeline automatically: Opt-out: Set `PUBLIC_SYNC_AUTO_MERGE=false` to require manual merge. -**Public storefront tag CI:** Path C mirrors `.github/workflows/ci.yml` to `agent-kit-startup/agent-kit`. When a Release or tag push runs on that public slug, `sync-public` and `publish-npm` are **skipped** (`github.repository != 'agent-kit-startup/agent-kit'`). Only the `build` job should run. Do **not** add `PUBLIC_REPO_TOKEN` or `NPM_TOKEN` to the public repo; those secrets stay on private. Private tag/manual sync still fails loud when `PUBLIC_REPO_TOKEN` is unset. +**Public storefront tag CI:** Path C mirrors `.github/workflows/ci.yml` to `agent-kit-startup/agent-kit`. Private-origin jobs (`sync-public`, `publish-npm`) and private-only build steps run only when `github.repository == 'agent-kit-startup/agent-kit-dev'` (allowlist). On the public slug (and any other repo), those jobs/steps are **skipped**. Only the shared `build` work should run on the storefront. Do **not** add `PUBLIC_REPO_TOKEN` or `NPM_TOKEN` to the public repo; those secrets stay on private. Private tag/manual sync still fails loud when `PUBLIC_REPO_TOKEN` is unset. `vars.PUBLIC_REPO_URL` redirects the sync target for both `git push` and `gh --repo` (slug derived from that URL in `scripts/sync-public.mjs`). When `PUBLIC_REPO_URL` is unset, `PUBLIC_REPO_SLUG` may set `owner/repo` for `gh` calls; when both are set, the URL-derived slug wins and the script warns on stderr. The same stderr warning fires when `PUBLIC_REPO_SLUG` diverges from a slug derivable from `--url` or the configured `public` remote. Neither variable overrides the job-level allowlist `if`. ### Verify next public `v*` tag After the next storefront tag (from private sync + public Release): 1. Public Actions for that tag: `build` green. -2. `sync-public` and `publish-npm` do **not** run (skipped by repository slug guard). +2. `sync-public` and `publish-npm` do **not** run (skipped unless `github.repository == 'agent-kit-startup/agent-kit-dev'`). 3. No `PUBLIC_REPO_TOKEN` / `NPM_TOKEN` on the public repo. 4. Do **not** force-move an already-pushed `v*` tag; use a new patch tag if retry is needed. +5. **Path C lag:** the allowlist guard protects the public mirror only after a private sync has shipped the updated `ci.yml`. A red storefront tag before that sync is expected lag, not a broken private sync. Manual fallback: ```bash diff --git a/docs/repository-boundaries.md b/docs/repository-boundaries.md index 25234fc..4bc1dcf 100644 --- a/docs/repository-boundaries.md +++ b/docs/repository-boundaries.md @@ -38,7 +38,7 @@ Do not `git add -f` session paths. Contributions after Phase B: registry PRs go 1. **`git staging`** - PR to `staging` on the **private** repo (`agent-kit-dev`). Everything committed goes to private. 2. **`git prod`** - `staging` → `main` on **private**; push `origin main`; create/push annotated vX.Y.Z tag when absent. -3. **Automatic triggers** - Annotated `v*` tags on the **private** repo trigger both `publish-npm` (when `NPM_TOKEN` configured) and `sync-public` (when `PUBLIC_REPO_TOKEN` configured) CI jobs. The same workflow file is mirrored to the public storefront; on `agent-kit-startup/agent-kit`, those two jobs are **skipped** by repository slug so public tag CI does not fail on missing private-only secrets. +3. **Automatic triggers** - Annotated `v*` tags on the **private** repo (`agent-kit-startup/agent-kit-dev`) trigger both `publish-npm` (when `NPM_TOKEN` configured) and `sync-public` (when `PUBLIC_REPO_TOKEN` configured) CI jobs. The same workflow file is mirrored to the public storefront; those two jobs run only on the private-origin allowlist (`github.repository == 'agent-kit-startup/agent-kit-dev'`), so public tag CI does not fail on missing private-only secrets. `vars.PUBLIC_REPO_URL` redirects the sync target for both `git push` and `gh --repo` (slug derived from the URL). When that URL var is unset, `PUBLIC_REPO_SLUG` may override the `gh --repo` slug; when both are set, URL wins (stderr warning). The same stderr warning fires when `PUBLIC_REPO_SLUG` diverges from a slug derivable from `--url` or the configured `public` remote. Neither overrides that job-level `if`. 4. **Public sync PR** - Creates semantic PR body (Summary + CHANGELOG release notes + source SHA) against public `main`. 5. **Auto-merge** - PRs auto-merge after required checks pass (`gh pr merge --auto`). Set `PUBLIC_SYNC_AUTO_MERGE=false` to require manual merge. 6. **Public GitHub Release** - After the sync PR merges, sync creates/updates a public GitHub Release `vX.Y.Z` (CHANGELOG notes; Latest badge). Opt out with `PUBLIC_SYNC_CREATE_RELEASE=false`. Git tags alone do not move the Releases sidebar. @@ -84,7 +84,7 @@ Before the first publish or any new registry version, follow [npm-publish-checkl | `PUBLIC_REPO_TOKEN` | Fine-grained token limited to the public repo, with `Contents: write`, `Pull requests: write`, and `Workflows: write` (required when the sync tree includes `.github/workflows/`). **Private repo only**; never set on the public storefront. | | `NPM_TOKEN` | Token to publish to the npm registry (**private** tag CI only) | -Optional repository variable `PUBLIC_REPO_URL` overrides the default public Git URL without embedding credentials. +Optional repository variable `PUBLIC_REPO_URL` overrides the default public Git URL (and the derived `owner/repo` slug for `gh` PR/Release calls) without embedding credentials. Optional `PUBLIC_REPO_SLUG` sets `owner/repo` when `PUBLIC_REPO_URL` is unset (and remains the parse-failure fallback); when both are set, the URL-derived slug wins. A stderr warning is emitted whenever `PUBLIC_REPO_SLUG` diverges from a derivable URL slug. ### External contributions diff --git a/docs/repository-readiness-onboarding.md b/docs/repository-readiness-onboarding.md index 6dbc830..17aec8e 100644 --- a/docs/repository-readiness-onboarding.md +++ b/docs/repository-readiness-onboarding.md @@ -23,7 +23,7 @@ A completed onboarding provides: 4. secrets and hook safeguards; 5. a documented branch and promotion strategy; 6. enough project context for agents to act without inventing facts; -7. relevant rules, skills, and agents selected from evidence; +7. relevant rules, skills, and agents selected from evidence, plus an optional chat-time domain-skills scaffold before `/start-project`; 8. a readiness report with no unresolved essential blocker; 9. one clear next action: `/start-project` or finish setup. @@ -82,6 +82,12 @@ An acknowledgment alone is not completion. Agent Personas belong in a later settings or personalization step. External plan review belongs at plan exhaustion, where its purpose is visible. Neither feature blocks repository readiness. +A **domain-skills scaffold** is offered after essentials are ready and before the final `/start-project` CTA. It reuses the install-time personalization/doctor evidence to propose relevant L2 skills, L1 packs, or project-owned skills under `.cursor/skills/domain/`. The operator chooses **scaffold**, **defer**, or **skip**; the choice is recorded in `.cursor/context/config.json`. Deferring or skipping does not block `/start-project`. + +**Instruction-only (intentional):** the scaffold gate lives in the L0 `/agent-kit-onboard` command prose and is executed by the chat agent. There is no separate CLI subcommand that writes domain skills. Prefer keeping it instruction-only unless a clear CLI integration path appears later. + +**`.cursor/skills/domain/` category:** project-only skills that sit outside the registry `core` / `community` trees. `guessRegistryPath` does not map `domain/`, so these skills cannot be contributed upstream (one-way). Update does not wipe them (overlay leaves project-owned paths alone). When scaffolding applies, update the **Relevant skills** section of `.cursor/project-context.md` and keep `.cursor/agent-kit.json` `skills[]` aligned as the install index. + ### Stage 6: Start a Deliverable After readiness passes, the onboarding offers `/start-project`. The next command asks for the deliverable goal and preserves the existing Gate A and Gate B contract. @@ -311,6 +317,40 @@ Existing installs may still have `.cursor/commands/onboard.md` or `onboarded: tr 4. Treat `onboarded: true` without readiness evidence as incomplete; resume essential checks until they pass or an allowed non-essential item is deferred with a recovery action. 5. Keep skins and external plan review as optional settings after essentials, not as the first-session path. +## IDE-Agnostic / Slash-Less Onboarding + +Operators in VS Code, Windsurf, or other IDEs that lack Cursor's slash commands and Ask questions tool can complete readiness via the CLI and markdown checklists. + +### CLI equivalents + +| Cursor slash command | CLI / terminal equivalent | +|---|---| +| `/agent-kit-onboard` | `agent-kit doctor --json` then `agent-kit doctor --fix-safe` | +| `/start-project` | Create a plan file in `.cursor/plans/` with to-dos, then resume from HANDOFF | +| `/dashboard` | `agent-kit dashboard` or `npx @dadado/agent-kit-cli dashboard` | + +### Readiness without slash + +1. Run `agent-kit doctor --json` to see the readiness report. +2. Fix essential blockers: `agent-kit doctor --fix-safe` applies safe local preparation. +3. Review `.cursor/context/readiness.json` for remaining `needs_choice` and `manual` items. +4. Resolve decisions by editing config files directly (`.cursor/context/config.json`, `.cursor/agent-kit.config.json`). +5. Re-run `agent-kit doctor --json` until all essentials pass. + +### VS Code Copilot integration + +When the IDE profile is `vscode` or `other`, the install/personalization flow generates: + +- `.vscode/settings.json` with editor defaults +- `.github/copilot-instructions.md` with project conventions +- `.vscode/security-review.agent.md` (VS Code Pro only) + +These are standard VS Code / Copilot artifacts that work without Cursor. + +### Chat fallback + +Cursor's Ask questions tool provides clickable option buttons. When unavailable (VS Code, CLI-only), the same gates present options as a numbered list in chat. The user replies with a number or types a custom answer. This fallback covers all HITL gates. + ## UX Rules - Explain the outcome before requesting input. diff --git a/install.md b/install.md index d93a012..cc7ad66 100644 --- a/install.md +++ b/install.md @@ -1,6 +1,6 @@ -# Agent Kit - Installation +# Mission Kit / Agent Kit - Installation -> **You are the installer.** Set up the kit **in the user's project** without copying the entire Agent Kit monorepo into it. +> **You are the installer for Mission Kit 5.** Marketing name: **Mission Kit** ([missionkit.io](https://missionkit.io)). Technical surfaces: **Agent Kit** CLI, npm package, slash commands, and `.cursor/agent-kit.json`. Set up the kit **in the user's project** without copying the entire Agent Kit monorepo into it. ## Contract (mandatory) @@ -36,6 +36,8 @@ If CLI runs successfully, skip to **Onboarding** below. Entry path: either the user drags `install.md` into chat, or they paste the agent brief from [install-prompt.md](install-prompt.md) which points to this contract. Before any file writes, confirm the absolute workspace root path using **Ask questions** tool (fallback to chat if unavailable). Prefer running `npx @dadado/agent-kit-cli install` when Node.js/npx are available; use the file sync below only when CLI is not available. +> **Multi-workspace safety:** the Port B chat install performs the same root-confirm duty as the CLI's `confirmProjectRoot`. The Ask above is the Port B equivalent of that guard: confirm the workspace root before any L0 write. The shared registry cache is locked during CLI installs/updates, and each workspace keeps its own `.cursor/` tree plus its own Mission Control listen port. + ### 1. Create minimal structure (L0 + session) ``` @@ -152,7 +154,7 @@ Create if it doesn't exist (adjust `version` / `registry` to current SoT): ```json { "schemaVersion": 1, - "version": "4.4.7", + "version": "5.0.0", "profile": "default", "packs": [], "skills": [], diff --git a/package.json b/package.json index 81d70cd..27b1b30 100644 --- a/package.json +++ b/package.json @@ -1,9 +1,9 @@ { "name": "agent-kit", - "version": "4.8.9", + "version": "5.0.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": "MIT", + "license": "PolyForm-Noncommercial-1.0.0", "packageManager": "pnpm@10.0.0", "engines": { "node": ">=20" @@ -24,12 +24,17 @@ "evidence:authority-graph:check": "node scripts/generate-authority-graph.mjs --check && node --test scripts/generate-authority-graph.test.mjs", "evidence:knowledge-classification": "node scripts/generate-knowledge-classification.mjs --handoff-fixture scripts/fixtures/handoff-knowledge-test.md", "evidence:knowledge-classification:check": "node scripts/generate-knowledge-classification.mjs --check --handoff-fixture scripts/fixtures/handoff-knowledge-test.md && node --test scripts/generate-knowledge-classification.test.mjs", - "evidence:codebase-findings": "node scripts/generate-codebase-findings.mjs", - "evidence:codebase-findings:check": "node --test scripts/generate-codebase-findings.test.mjs", - "evidence:risk-hotspots": "node scripts/score-codebase-risk-surface.mjs", + "evidence:codebase-findings": "node scripts/generate-codebase-findings.mjs --write", + "evidence:codebase-findings:check": "node --test scripts/generate-codebase-findings.test.mjs scripts/evidence-check-scripts-non-mutating.test.mjs", + "evidence:risk-hotspots": "node scripts/score-codebase-risk-surface.mjs --write", "evidence:risk-hotspots:check": "node --test scripts/score-codebase-risk-surface.test.mjs", "check:public-deny-links": "node scripts/check-public-deny-links.mjs", "check:public-deny-links:test": "node --test scripts/check-public-deny-links.test.mjs", + "landing:sync": "node scripts/sync-landing.mjs", + "landing:vendor": "node scripts/sync-landing.mjs", + "landing:build": "node scripts/build-landing.mjs", + "landing:build:check": "node scripts/build-landing.mjs --check", + "landing:serve": "node scripts/serve-landing.mjs", "build": "turbo run build", "dev": "turbo run dev", "lint": "turbo run lint", diff --git a/packages/cli/LICENSE b/packages/cli/LICENSE new file mode 100644 index 0000000..ba93ed0 --- /dev/null +++ b/packages/cli/LICENSE @@ -0,0 +1,75 @@ +Required Notice: Copyright (c) 2026 agent-kit-startup + +# PolyForm Noncommercial License 1.0.0 + +<https://polyformproject.org/licenses/noncommercial/1.0.0> + +## Acceptance + +In order to get any license under these terms, you must agree to them as both strict obligations and conditions to all your licenses. + +## Copyright License + +The licensor grants you a copyright license for the software to do everything you might do with the software that would otherwise infringe the licensor's copyright in it for any permitted purpose. However, you may only distribute the software according to [Distribution License](#distribution-license) and make changes or new works based on the software according to [Changes and New Works License](#changes-and-new-works-license). + +## Distribution License + +The licensor grants you an additional copyright license to distribute copies of the software. Your license to distribute covers distributing the software with changes and new works permitted by [Changes and New Works License](#changes-and-new-works-license). + +## Notices + +You must ensure that anyone who gets a copy of any part of the software from you also gets a copy of these terms or the URL for them above, as well as copies of any plain-text lines beginning with `Required Notice:` that the licensor provided with the software. For example: + +> Required Notice: Copyright Yoyodyne, Inc. (http://example.com) + +## Changes and New Works License + +The licensor grants you an additional copyright license to make changes and new works based on the software for any permitted purpose. + +## Patent License + +The licensor grants you a patent license for the software that covers patent claims the licensor can license, or becomes able to license, that you would infringe by using the software. + +## Noncommercial Purposes + +Any noncommercial purpose is a permitted purpose. + +## Personal Uses + +Personal use for research, experiment, and testing for the benefit of public knowledge, personal study, private entertainment, hobby projects, amateur pursuits, or religious observance, without any anticipated commercial application, is use for a permitted purpose. + +## Noncommercial Organizations + +Use by any charitable organization, educational institution, public research organization, public safety or health organization, environmental protection organization, or government institution is use for a permitted purpose regardless of the source of funding or obligations resulting from the funding. + +## Fair Use + +You may have "fair use" rights for the software under the law. These terms do not limit them. + +## No Other Rights + +These terms do not allow you to sublicense or transfer any of your licenses to anyone else, or prevent the licensor from granting licenses to anyone else. These terms do not imply any other licenses. + +## Patent Defense + +If you make any written claim that the software infringes or contributes to infringement of any patent, your patent license for the software granted under these terms ends immediately. If your company makes such a claim, your patent license ends immediately for work on behalf of your company. + +## Violations + +The first time you are notified in writing that you have violated any of these terms, or done anything with the software not covered by your licenses, your licenses can nonetheless continue if you come into full compliance with these terms, and take practical steps to correct past violations, within 32 days of receiving notice. Otherwise, all your licenses end immediately. + +## No Liability + +***As far as the law allows, the software comes as is, without any warranty or condition, and the licensor will not be liable to you for any damages arising out of these terms or the use or nature of the software, under any kind of legal claim.*** + +## Definitions + +The **licensor** is the individual or entity offering these terms, and the **software** is the software the licensor makes available under these terms. + +**You** refers to the individual or entity agreeing to these terms. + +**Your company** is any legal entity, sole proprietorship, or other kind of organization that you work for, plus all organizations that have control over, are under the control of, or are under common control with that organization. **Control** means ownership of substantially all the assets of an entity, or the power to direct its management and policies by vote, contract, or otherwise. Control can be direct or indirect. + +**Your licenses** are all the licenses granted to you for the software under these terms. + +**Use** means anything you do with the software requiring one of your licenses. diff --git a/packages/cli/README.md b/packages/cli/README.md index 6ac8bb6..6b49ff9 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -38,6 +38,23 @@ The panel binds to loopback by default, serves its own static files, and snapsho Older tags before 4.8.2 do not include those assets. Prefer a current pin, or point `MISSION_CONTROL_KIT_ROOT` / `AGENT_KIT_HOME` at an agent-kit checkout that contains `dashboard/`. +## Bare invoke (welcome) + +With no subcommand, `agent-kit` prints a branded Mission Kit welcome (ASCII helmet, version, and short utility hints) then exits. Technical identifiers stay `agent-kit` / `@dadado/agent-kit-cli`. + +```bash +agent-kit +# → welcome + hints (doctor, status, dashboard, init, --help) + +agent-kit --help +# → grouped command list (SETUP / MISSION / DASHBOARD / INTEGRITY) + +NO_COLOR=1 agent-kit +# → plain text (no ANSI); also plain when stdout is not a TTY or CI=1 +``` + +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. + ## Common commands | Command | Purpose | diff --git a/packages/cli/package.json b/packages/cli/package.json index ccfe036..1f3b6b6 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,7 +1,8 @@ { "name": "@dadado/agent-kit-cli", - "version": "4.8.9", + "version": "5.0.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", "bin": { "agent-kit": "./dist/index.js" @@ -10,7 +11,8 @@ "types": "./dist/index.d.ts", "files": [ "dist", - "dashboard" + "dashboard", + "LICENSE" ], "scripts": { "sync-dashboard": "node ../../scripts/sync-cli-dashboard.mjs", diff --git a/packages/cli/src/commands/add.ts b/packages/cli/src/commands/add.ts index 5efbb84..3a36564 100644 --- a/packages/cli/src/commands/add.ts +++ b/packages/cli/src/commands/add.ts @@ -12,7 +12,7 @@ import { logger } from "../utils/logger.js"; export const addCommand = defineCommand({ meta: { name: "add", - description: "Install a skill or L1 pack from the registry into the project.", + description: "Install a skill or L1 pack from the registry.", }, args: { id: { @@ -51,69 +51,73 @@ export const addCommand = defineCommand({ manifest: existing, }); - const protectedGlobs = resolveProtectedGlobs(existing); - const index = await loadRegistry(registry.root); - const asSkill = allSkills(index).find((s) => s.id === args.id); - const asPack = allPacks(index).find((p) => p.id === args.id); + try { + const protectedGlobs = resolveProtectedGlobs(existing); + const index = await loadRegistry(registry.root); + const asSkill = allSkills(index).find((s) => s.id === args.id); + const asPack = allPacks(index).find((p) => p.id === args.id); - const base = existing - ? { ...existing } - : buildManifest({ - version: KIT_VERSION, - profile: "default", - registryUrl: registry.url, - registryRef: registry.ref, - }); + const base = existing + ? { ...existing } + : buildManifest({ + version: KIT_VERSION, + profile: "default", + registryUrl: registry.url, + registryRef: registry.ref, + }); - const finish = async (kind: "skill" | "pack", id: string) => { - if (kind === "skill") base.skills = upsertIdList(base.skills, id); - else base.packs = upsertIdList(base.packs, id); - if (registry.url || registry.ref) { - base.registry = { - url: registry.url ?? base.registry?.url, - ref: registry.ref ?? base.registry?.ref, - }; - } - base.version = KIT_VERSION; - await saveManifest(args.cwd, base); - }; + const finish = async (kind: "skill" | "pack", id: string) => { + if (kind === "skill") base.skills = upsertIdList(base.skills, id); + else base.packs = upsertIdList(base.packs, id); + if (registry.url || registry.ref) { + base.registry = { + url: registry.url ?? base.registry?.url, + ref: registry.ref ?? base.registry?.ref, + }; + } + base.version = KIT_VERSION; + await saveManifest(args.cwd, base); + }; - if (args.skill) { - const skill = await findSkill(registry.root, args.id); - const stats = await installSkill(registry.root, args.cwd, skill, { protectedGlobs }); - logApplyStats(stats); - await finish("skill", skill.id); - logger.success(`Skill '${skill.id}' added.`); - return; - } - if (args.pack) { - const pack = await findPack(registry.root, args.id); - const stats = await installPack(registry.root, args.cwd, pack.id, { protectedGlobs }); - logApplyStats(stats); - await finish("pack", pack.id); - logger.success(`Pack '${pack.id}' added.`); - return; - } + if (args.skill) { + const skill = await findSkill(registry.root, args.id); + const stats = await installSkill(registry.root, args.cwd, skill, { protectedGlobs }); + logApplyStats(stats); + await finish("skill", skill.id); + logger.success(`Skill '${skill.id}' added.`); + return; + } + if (args.pack) { + const pack = await findPack(registry.root, args.id); + const stats = await installPack(registry.root, args.cwd, pack.id, { protectedGlobs }); + logApplyStats(stats); + await finish("pack", pack.id); + logger.success(`Pack '${pack.id}' added.`); + return; + } - if (asSkill && asPack) { - throw new Error( - `'${args.id}' is both a skill and a pack. Disambiguate: agent-kit add --skill ${args.id} | agent-kit add --pack ${args.id}`, - ); - } - if (asSkill) { - const stats = await installSkill(registry.root, args.cwd, asSkill, { protectedGlobs }); - logApplyStats(stats); - await finish("skill", asSkill.id); - logger.success(`Skill '${asSkill.id}' added.`); - return; - } - if (asPack) { - const stats = await installPack(registry.root, args.cwd, asPack.id, { protectedGlobs }); - logApplyStats(stats); - await finish("pack", asPack.id); - logger.success(`Pack '${asPack.id}' added.`); - return; + if (asSkill && asPack) { + throw new Error( + `'${args.id}' is both a skill and a pack. Disambiguate: agent-kit add --skill ${args.id} | agent-kit add --pack ${args.id}`, + ); + } + if (asSkill) { + const stats = await installSkill(registry.root, args.cwd, asSkill, { protectedGlobs }); + logApplyStats(stats); + await finish("skill", asSkill.id); + logger.success(`Skill '${asSkill.id}' added.`); + return; + } + if (asPack) { + const stats = await installPack(registry.root, args.cwd, asPack.id, { protectedGlobs }); + logApplyStats(stats); + await finish("pack", asPack.id); + logger.success(`Pack '${asPack.id}' added.`); + return; + } + throw new Error(`'${args.id}' not found as skill or pack in registry.`); + } finally { + await registry.unlock?.(); } - throw new Error(`'${args.id}' not found as skill or pack in registry.`); }, }); diff --git a/packages/cli/src/commands/contribute.ts b/packages/cli/src/commands/contribute.ts index 96e1e4a..8e7ff8a 100644 --- a/packages/cli/src/commands/contribute.ts +++ b/packages/cli/src/commands/contribute.ts @@ -64,65 +64,69 @@ export const contributeCommand = defineCommand({ manifest, }); - if (registry.source !== "flag" && args.write) { - logger.warn( - "contribute --write needs a local kit checkout. Pass --registry /path/to/agent-kit.", - ); - return; - } - - logger.info(`Registry: ${registry.root} (${registry.source})`); - - const plan = await planContribute({ - registryRoot: registry.root, - projectRoot: args.cwd, - manifest, - extraPaths: parsePaths(args.path), - includeDrift: args.drift, - }); + try { + if (registry.source !== "flag" && args.write) { + logger.warn( + "contribute --write needs a local kit checkout. Pass --registry /path/to/agent-kit.", + ); + return; + } - console.log( - `Contribute plan: accepted=${plan.accepted.length} rejected=${plan.rejected.length} (drift scan=${args.drift})`, - ); + logger.info(`Registry: ${registry.root} (${registry.source})`); - if (plan.candidates.length === 0) { - logger.success("Nothing to contribute (no drift / no --path)."); - return; - } + const plan = await planContribute({ + registryRoot: registry.root, + projectRoot: args.cwd, + manifest, + extraPaths: parsePaths(args.path), + includeDrift: args.drift, + }); - for (const c of plan.candidates) { - const mark = c.gateOk ? "ok" : "FAIL"; console.log( - `${mark.padEnd(4)} ${c.kind.padEnd(5)} ${c.projectPath} → ${c.registryPath || "(unmapped)"}`, + `Contribute plan: accepted=${plan.accepted.length} rejected=${plan.rejected.length} (drift scan=${args.drift})`, ); - for (const issue of c.issues) { - console.log(` [${issue.code}] ${issue.message}`); + + if (plan.candidates.length === 0) { + logger.success("Nothing to contribute (no drift / no --path)."); + return; } - } - if (plan.accepted.length === 0) { - logger.warn("No files passed the contribute gate."); - return; - } + for (const c of plan.candidates) { + const mark = c.gateOk ? "ok" : "FAIL"; + console.log( + `${mark.padEnd(4)} ${c.kind.padEnd(5)} ${c.projectPath} → ${c.registryPath || "(unmapped)"}`, + ); + for (const issue of c.issues) { + console.log(` [${issue.code}] ${issue.message}`); + } + } - console.log("\n--- Suggested PR body ---\n"); - console.log(formatPrBody(plan.accepted)); + if (plan.accepted.length === 0) { + logger.warn("No files passed the contribute gate."); + return; + } - if (!args.write) { - logger.info( - "Dry run. Re-run with --write --registry /path/to/agent-kit to copy accepted files into the kit checkout, then open a PR (gh pr create --base main).", - ); - return; - } + console.log("\n--- Suggested PR body ---\n"); + console.log(formatPrBody(plan.accepted)); - const written = await writeContributeToRegistry(args.cwd, registry.root, plan.accepted); - logger.success(`Wrote ${written.length} file(s) into ${registry.root}`); - for (const p of written) console.log(` + ${p}`); - console.log("\nNext (HITL — review before push):"); - console.log(` cd ${registry.root}`); - console.log(" git checkout -b contribute/<short-topic>"); - console.log(' git add <files> && git commit -m "feat: contribute <topic> from consumer"'); - console.log(" git push -u origin HEAD"); - console.log(' gh pr create --base main --title "feat: contribute <topic>" --body-file -'); + if (!args.write) { + logger.info( + "Dry run. Re-run with --write --registry /path/to/agent-kit to copy accepted files into the kit checkout, then open a PR (gh pr create --base main).", + ); + return; + } + + const written = await writeContributeToRegistry(args.cwd, registry.root, plan.accepted); + logger.success(`Wrote ${written.length} file(s) into ${registry.root}`); + for (const p of written) console.log(` + ${p}`); + console.log("\nNext (HITL — review before push):"); + console.log(` cd ${registry.root}`); + console.log(" git checkout -b contribute/<short-topic>"); + console.log(' git add <files> && git commit -m "feat: contribute <topic> from consumer"'); + console.log(" git push -u origin HEAD"); + console.log(' gh pr create --base main --title "feat: contribute <topic>" --body-file -'); + } finally { + await registry.unlock?.(); + } }, }); diff --git a/packages/cli/src/commands/cursor-awareness.ts b/packages/cli/src/commands/cursor-awareness.ts index 86ca962..20c36be 100644 --- a/packages/cli/src/commands/cursor-awareness.ts +++ b/packages/cli/src/commands/cursor-awareness.ts @@ -5,8 +5,7 @@ import { logger } from "../utils/logger.js"; export const cursorAwarenessCommand = defineCommand({ meta: { name: "cursor-awareness", - description: - "Opt-in advisory: diff Cursor changelog / native-audit inventory for gaps (never apply, never Field Reports)", + description: "Opt-in advisory: Cursor changelog vs native-audit gaps (never apply).", }, args: { cwd: { diff --git a/packages/cli/src/commands/dashboard-broadcast.ts b/packages/cli/src/commands/dashboard-broadcast.ts index 3dd6679..1412c43 100644 --- a/packages/cli/src/commands/dashboard-broadcast.ts +++ b/packages/cli/src/commands/dashboard-broadcast.ts @@ -5,8 +5,10 @@ import { defineCommand } from "citty"; import { logger } from "../utils/logger.js"; import { type FindDashboardOptions, + applyDashboardOpenEnv, bundledDashboardCandidates, findDashboardStart, + resolveDashboardSnapshotRoot, } from "./dashboard.js"; /** @@ -99,7 +101,13 @@ export const dashboardBroadcastCommand = defineCommand({ "no-open": { type: "boolean", default: false, - description: "Do not open a browser; only ensure the server is up and print LAN URL + token", + description: + "Do not open a browser; only ensure the server is up and print Share URL (when masking is on) plus token / LAN lines", + }, + browser: { + type: "string", + description: + "Preferred browser app/binary for this launch (overrides config missionControl.preferredBrowser)", }, }, async run({ args }) { @@ -115,8 +123,11 @@ export const dashboardBroadcastCommand = defineCommand({ return; } - const env = { ...process.env }; - if (args["no-open"]) env.MISSION_CONTROL_NO_OPEN = "1"; + const snapshotRoot = resolveDashboardSnapshotRoot(args.cwd); + const env = applyDashboardOpenEnv( + { ...process.env }, + { noOpen: Boolean(args["no-open"]), browser: args.browser, cwd: 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 19ffdcd..63c5beb 100644 --- a/packages/cli/src/commands/dashboard.test.ts +++ b/packages/cli/src/commands/dashboard.test.ts @@ -1,3 +1,4 @@ +import { execFileSync } from "node:child_process"; import { mkdirSync, mkdtempSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { dirname, join } from "node:path"; @@ -88,6 +89,16 @@ describe("resolveDashboardSnapshotRoot", () => { const root = mkdtempSync(join(tmpdir(), "ak-snap-")); expect(resolveDashboardSnapshotRoot(root)).toBe(root); }); + + it("prefers the nearest Agent Kit install over a parent git toplevel", () => { + const mono = mkdtempSync(join(tmpdir(), "ak-mono-")); + // Create a parent git tree so rev-parse would climb above the package. + execFileSync("git", ["init"], { cwd: mono }); + const pkg = join(mono, "pkg"); + mkdirSync(join(pkg, ".cursor"), { recursive: true }); + writeFileSync(join(pkg, ".cursor", "agent-kit.json"), "{}\n"); + expect(resolveDashboardSnapshotRoot(pkg)).toBe(pkg); + }); }); describe("findDashboardBroadcastStart", () => { diff --git a/packages/cli/src/commands/dashboard.ts b/packages/cli/src/commands/dashboard.ts index cae0f10..2eba958 100644 --- a/packages/cli/src/commands/dashboard.ts +++ b/packages/cli/src/commands/dashboard.ts @@ -1,8 +1,14 @@ import { execFileSync, spawn } from "node:child_process"; +import { existsSync, readFileSync, realpathSync } from "node:fs"; import { access } from "node:fs/promises"; import path from "node:path"; import { fileURLToPath } from "node:url"; import { defineCommand } from "citty"; +import { resolveContextConfigPath } from "../../../../dashboard/lib/guards.mjs"; +import { + normalizePreferredBrowser, + readPreferredBrowserFromConfig, +} from "../../../../dashboard/lib/open-browser.mjs"; import { logger } from "../utils/logger.js"; /** Optional hooks for hermetic tests (inject moduleUrl; never required in production). */ @@ -10,6 +16,43 @@ export type FindDashboardOptions = { moduleUrl?: string; }; +/** + * Resolve preferred browser from workspace context config (if present). + * Returns null when unset / invalid / unsafe. Env override is applied by the starter. + * Validation SoT: `normalizePreferredBrowser` in dashboard/lib/open-browser.mjs. + * Path SoT: `resolveContextConfigPath` in dashboard/lib/guards.mjs. + */ +export function readPreferredBrowserFromWorkspace( + cwd: string, + readFile: typeof readFileSync = readFileSync, +): string | null { + const resolved = resolveContextConfigPath(path.resolve(cwd), { + existsSync, + realpathSync, + }); + if (!resolved.ok) return null; + const value = readPreferredBrowserFromConfig(resolved.path, { readFileSync: readFile }); + return normalizePreferredBrowser(value); +} + +/** Apply --no-open / --browser / config preferred browser onto env for the starter. */ +export function applyDashboardOpenEnv( + env: NodeJS.ProcessEnv, + opts: { noOpen?: boolean; browser?: string; cwd?: string }, +): NodeJS.ProcessEnv { + const next = { ...env }; + if (opts.noOpen) next.MISSION_CONTROL_NO_OPEN = "1"; + const flag = opts.browser?.trim(); + if (flag) { + const safe = normalizePreferredBrowser(flag); + if (safe) next.MISSION_CONTROL_PREFERRED_BROWSER = safe; + } else if (!next.MISSION_CONTROL_PREFERRED_BROWSER && opts.cwd) { + const fromConfig = readPreferredBrowserFromWorkspace(opts.cwd); + if (fromConfig) next.MISSION_CONTROL_PREFERRED_BROWSER = fromConfig; + } + return next; +} + /** Candidates for dashboard assets shipped beside the CLI package (Path C). */ export function bundledDashboardCandidates( filename: "start.mjs" | "start-broadcast.mjs", @@ -87,9 +130,23 @@ export async function findDashboardStart( ); } -/** Prefer git toplevel when available so snapshots match the workspace root. */ +/** + * Resolve the Mission Control snapshot root. + * Prefer the nearest Agent Kit install (`.cursor/agent-kit.json`) walking up from + * cwd so nested monorepo packages are not overwritten by the git toplevel. + * Fall back to git toplevel when no install marker is found, then cwd. + */ export function resolveDashboardSnapshotRoot(cwd: string): string { const abs = path.resolve(cwd); + let dir = abs; + for (;;) { + if (existsSync(path.join(dir, ".cursor", "agent-kit.json"))) { + return dir; + } + const parent = path.dirname(dir); + if (parent === dir) break; + dir = parent; + } try { const top = execFileSync("git", ["-C", abs, "rev-parse", "--show-toplevel"], { encoding: "utf8", @@ -117,36 +174,58 @@ function runStartScript(startPath: string, env: NodeJS.ProcessEnv): Promise<numb export const dashboardCommand = defineCommand({ meta: { name: "dashboard", - description: - "Start Mission Control for this workspace (stable per-root port) and open the panel URL.", + description: "Start Mission Control for this workspace (loopback; opens panel URL).", }, args: { cwd: { type: "string", default: process.cwd(), description: - "Workspace to snapshot (git root preferred); also searched upward for dashboard/start.mjs", + "Workspace to snapshot (nearest .cursor/agent-kit.json, else git root); also searched upward for dashboard/start.mjs", }, "no-open": { type: "boolean", default: false, description: "Do not open a browser; only ensure the server is up and print the URL", }, + browser: { + type: "string", + description: + "Preferred browser app/binary for this launch (overrides config missionControl.preferredBrowser)", + }, }, async run({ args }) { const snapshotRoot = resolveDashboardSnapshotRoot(args.cwd); const startPath = await findDashboardStart(args.cwd); if (!startPath) { - logger.error( - "No dashboard/start.mjs found. After a CLI publish that ships dashboard/ (Path C), reinstall @dadado/agent-kit-cli. Or set MISSION_CONTROL_KIT_ROOT / AGENT_KIT_HOME to an agent-kit checkout, place a sibling ../agent-kit tree, or run from that kit tree.", + logger.error("No dashboard/start.mjs found."); + console.error( + [ + "", + "The dashboard runtime is not available in this workspace.", + "L0 install provides the /dashboard command text but not the panel itself.", + "", + "Recovery (pick one):", + " 1. Upgrade the CLI: npx @dadado/agent-kit-cli@latest dashboard", + " (Path C ships dashboard/ from 4.8.2 onward)", + " 2. Set an env var pointing to an agent-kit checkout:", + " export MISSION_CONTROL_KIT_ROOT=/path/to/agent-kit", + " agent-kit dashboard", + " 3. Place an agent-kit sibling: ../agent-kit/dashboard/start.mjs", + " 4. Run directly from a kit tree: node dashboard/start.mjs", + "", + "Works in Cursor, VS Code, and any Node.js terminal.", + "", + ].join("\n"), ); process.exitCode = 1; return; } - const env = { ...process.env }; - env.MISSION_CONTROL_REPO_ROOT = snapshotRoot; - if (args["no-open"]) env.MISSION_CONTROL_NO_OPEN = "1"; + const env = applyDashboardOpenEnv( + { ...process.env, MISSION_CONTROL_REPO_ROOT: snapshotRoot }, + { noOpen: Boolean(args["no-open"]), browser: args.browser, cwd: snapshotRoot }, + ); const code = await runStartScript(startPath, env); if (code !== 0) process.exitCode = code; diff --git a/packages/cli/src/commands/diff.ts b/packages/cli/src/commands/diff.ts index 5cdb23b..5f26bbe 100644 --- a/packages/cli/src/commands/diff.ts +++ b/packages/cli/src/commands/diff.ts @@ -37,21 +37,25 @@ export const diffCommand = defineCommand({ manifest, }); - logger.info(`Registry: ${registry.root} (${registry.source})`); - const entries = await diffAgainstRegistry(registry.root, args.cwd, manifest); - const summary = summarizeDiff(entries); + try { + logger.info(`Registry: ${registry.root} (${registry.source})`); + const entries = await diffAgainstRegistry(registry.root, args.cwd, manifest); + const summary = summarizeDiff(entries); - console.log( - `Summary: match=${summary.match} drift=${summary.drift} missing-local=${summary["missing-local"]} missing-registry=${summary["missing-registry"]} protected=${summary.protected}`, - ); + console.log( + `Summary: match=${summary.match} drift=${summary.drift} missing-local=${summary["missing-local"]} missing-registry=${summary["missing-registry"]} protected=${summary.protected}`, + ); - const rows = args.all ? entries : entries.filter((e) => e.status !== "match"); - if (rows.length === 0) { - logger.success("No drift (matches only)."); - return; - } - for (const e of rows) { - console.log(`${e.status.padEnd(18)} ${e.path}`); + const rows = args.all ? entries : entries.filter((e) => e.status !== "match"); + if (rows.length === 0) { + logger.success("No drift (matches only)."); + return; + } + for (const e of rows) { + console.log(`${e.status.padEnd(18)} ${e.path}`); + } + } finally { + await registry.unlock?.(); } }, }); diff --git a/packages/cli/src/commands/doctor.ts b/packages/cli/src/commands/doctor.ts index 7bd68fc..7667f32 100644 --- a/packages/cli/src/commands/doctor.ts +++ b/packages/cli/src/commands/doctor.ts @@ -78,7 +78,7 @@ function printDoctorSummary(result: DoctorResult): void { export const doctorCommand = defineCommand({ meta: { name: "doctor", - description: "Diagnose repository readiness and optionally apply safe local fixes.", + description: "Diagnose repository readiness; optional --fix-safe local repairs.", }, args: { cwd: { diff --git a/packages/cli/src/commands/guard.ts b/packages/cli/src/commands/guard.ts index 9c8047a..90020ac 100644 --- a/packages/cli/src/commands/guard.ts +++ b/packages/cli/src/commands/guard.ts @@ -21,7 +21,7 @@ async function detectCurrentBranch(): Promise<string | undefined> { export const guardCommand = defineCommand({ meta: { name: "guard", - description: "Mechanizable deny/annotate guards (shell, prompt). Hooks are thin adapters.", + description: "Deny/annotate guards (shell, prompt). Hooks are thin adapters.", }, subCommands: { shell: defineCommand({ diff --git a/packages/cli/src/commands/handoff.ts b/packages/cli/src/commands/handoff.ts index 283a0fe..973383d 100644 --- a/packages/cli/src/commands/handoff.ts +++ b/packages/cli/src/commands/handoff.ts @@ -159,8 +159,7 @@ function runCursorHandoff(scriptPath: string, cwd: string): Promise<number> { export const handoffCommand = defineCommand({ meta: { name: "handoff", - description: - "Write .cursor/HANDOFF.md from the active Cursor plan, or run ./cursor-handoff handoff when no plan exists.", + description: "Write .cursor/HANDOFF.md from the active plan (or cursor-handoff fallback).", }, args: { cwd: { diff --git a/packages/cli/src/commands/hook.ts b/packages/cli/src/commands/hook.ts index ffe57af..d37eed6 100644 --- a/packages/cli/src/commands/hook.ts +++ b/packages/cli/src/commands/hook.ts @@ -11,8 +11,7 @@ import { export const hookCommand = defineCommand({ meta: { name: "hook", - description: - "Cursor hook adapters (session-start, pre-compact). CLI is SoT; thin hooks shell out here.", + description: "Cursor hook adapters (session-start, pre-compact). CLI is SoT.", }, subCommands: { "session-start": defineCommand({ diff --git a/packages/cli/src/commands/init.ts b/packages/cli/src/commands/init.ts index 5ab4b79..98b7c77 100644 --- a/packages/cli/src/commands/init.ts +++ b/packages/cli/src/commands/init.ts @@ -2,6 +2,7 @@ import { intro, outro } from "@clack/prompts"; 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 { type InstallResult, performInstall } from "./install.js"; type CompatibilityInstaller = (options: { cwd: string }) => Promise<InstallResult>; @@ -16,7 +17,7 @@ export async function runInitCompatibility( export const initCommand = defineCommand({ meta: { name: "init", - description: "Guided compatibility entry point for install and repository readiness.", + description: "Guided setup entry (compat path to install + readiness).", }, args: { cwd: { @@ -26,15 +27,31 @@ export const initCommand = defineCommand({ }, }, async run({ args }) { - intro(`agent-kit v${KIT_VERSION}`); + const nonInteractive = isNonInteractive(); + if (!nonInteractive) { + intro(`agent-kit v${KIT_VERSION}`); + } else { + logger.info(`agent-kit v${KIT_VERSION} (non-interactive mode)`); + } logger.info("init now uses the canonical install and readiness workflow."); - const result = await runInitCompatibility(args.cwd); - const pending = result.readiness.pendingActions.length; - logger.success(`L0 and readiness prepared in ${result.projectRoot}`); - outro( - pending > 0 - ? "Next: run /agent-kit-onboard in Cursor to resolve the first pending action." - : "Next: run /start-project in Cursor when you have a deliverable.", - ); + try { + const result = await runInitCompatibility(args.cwd); + const pending = result.readiness.pendingActions.length; + logger.success(`L0 and readiness prepared in ${result.projectRoot}`); + const nextStep = + pending > 0 + ? "Next: run /agent-kit-onboard in Cursor to resolve the first pending action." + : "Next: run /start-project in Cursor when you have a deliverable."; + if (!nonInteractive) { + outro(nextStep); + } else { + logger.info(nextStep); + } + } catch (err) { + const hint = classifyInstallError(err); + logger.error(hint.message); + console.error(`\n${hint.recovery}\n`); + process.exit(1); + } }, }); diff --git a/packages/cli/src/commands/install.test.ts b/packages/cli/src/commands/install.test.ts new file mode 100644 index 0000000..a72e4f1 --- /dev/null +++ b/packages/cli/src/commands/install.test.ts @@ -0,0 +1,51 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import { RootRefusedError } from "../utils/terminal.js"; +import { installCommand } from "./install.js"; + +const mockConfirmProjectRoot = vi.hoisted(() => vi.fn()); + +vi.mock("../utils/terminal.js", async (importOriginal) => { + const mod = await importOriginal<typeof import("../utils/terminal.js")>(); + return { + ...mod, + confirmProjectRoot: (...args: unknown[]) => mockConfirmProjectRoot(...args), + isNonInteractive: () => true, + }; +}); + +describe("installCommand RootRefusedError", () => { + afterEach(() => { + mockConfirmProjectRoot.mockReset(); + process.exitCode = undefined; + }); + + it("sets exitCode and returns without process.exit on root refusal", async () => { + mockConfirmProjectRoot.mockRejectedValue(new RootRefusedError("/tmp/not-a-project")); + const exitSpy = vi.spyOn(process, "exit").mockImplementation((() => { + throw new Error("process.exit must not be called on RootRefusedError"); + }) as never); + + try { + await ( + installCommand.run as unknown as (ctx: { args: Record<string, unknown> }) => Promise<void> + )({ + args: { + _: [], + cwd: "/tmp/not-a-project", + yes: true, + "force-root": false, + pack: undefined as unknown as string, + profile: undefined as unknown as string, + registry: undefined as unknown as string, + url: undefined as unknown as string, + ref: undefined as unknown as string, + refresh: false, + }, + }); + expect(process.exitCode).toBe(1); + expect(exitSpy).not.toHaveBeenCalled(); + } finally { + exitSpy.mockRestore(); + } + }); +}); diff --git a/packages/cli/src/commands/install.ts b/packages/cli/src/commands/install.ts index 58c86f0..9169f79 100644 --- a/packages/cli/src/commands/install.ts +++ b/packages/cli/src/commands/install.ts @@ -15,6 +15,12 @@ import { runScanner } from "../scanner/scan.js"; import { writeReadinessSnapshot } from "../scanner/snapshot.js"; import type { ReadinessReport, SafeReadinessChange } from "../types.js"; import { logger } from "../utils/logger.js"; +import { + RootRefusedError, + classifyInstallError, + confirmProjectRoot, + isNonInteractive, +} from "../utils/terminal.js"; function parsePackList(raw: string | undefined): string[] { if (!raw?.trim()) return []; @@ -74,59 +80,63 @@ export async function performInstall(options: InstallOptions): Promise<InstallRe refresh: options.refresh, manifest: existing, }); - const draft = buildManifest({ - version: KIT_VERSION, - profile: options.profile ?? existing?.profile ?? "default", - packs: packs.length > 0 ? packs : existing?.packs, - skills: existing?.skills, - protected: existing?.protected, - personalization: existing?.personalization, - registryUrl: registry.url ?? existing?.registry?.url, - registryRef: registry.ref ?? existing?.registry?.ref, - }); - - const stats = - (draft.packs?.length ?? 0) > 0 || (draft.skills?.length ?? 0) > 0 - ? await syncFromManifest(registry.root, projectRoot, draft) - : await installL0(registry.root, projectRoot, resolveProtectedGlobs(draft)); - const manifestPath = await saveManifest(projectRoot, draft); - const readinessExecution = await executeSafeReadinessFixes(projectRoot, { - generatorVersion: KIT_VERSION, - }); - let readiness = readinessExecution.after; - const profile = await readRepositoryProfile(projectRoot); - if (profile) { - const registryIndex = await loadRegistry(registry.root); - const personalization = await applyPersonalization({ - rootDir: projectRoot, - registryRoot: registry.root, - profile, - report: readinessExecution.after, - registry: registryIndex, - manifest: draft, - generatorVersion: KIT_VERSION, + try { + const draft = buildManifest({ + version: KIT_VERSION, + profile: options.profile ?? existing?.profile ?? "default", + packs: packs.length > 0 ? packs : existing?.packs, + skills: existing?.skills, + protected: existing?.protected, + personalization: existing?.personalization, + registryUrl: registry.url ?? existing?.registry?.url, + registryRef: registry.ref ?? existing?.registry?.ref, }); - await saveManifest(projectRoot, personalization.manifest); - readiness = createReadinessReport(await runScanner(projectRoot), { + + const stats = + (draft.packs?.length ?? 0) > 0 || (draft.skills?.length ?? 0) > 0 + ? await syncFromManifest(registry.root, projectRoot, draft) + : await installL0(registry.root, projectRoot, resolveProtectedGlobs(draft)); + const manifestPath = await saveManifest(projectRoot, draft); + const readinessExecution = await executeSafeReadinessFixes(projectRoot, { generatorVersion: KIT_VERSION, }); - readiness.appliedSafeFixes = readinessExecution.after.appliedSafeFixes; - } - await writeReadinessSnapshot(projectRoot, readiness); + let readiness = readinessExecution.after; + const profile = await readRepositoryProfile(projectRoot); + if (profile) { + const registryIndex = await loadRegistry(registry.root); + const personalization = await applyPersonalization({ + rootDir: projectRoot, + registryRoot: registry.root, + profile, + report: readinessExecution.after, + registry: registryIndex, + manifest: draft, + generatorVersion: KIT_VERSION, + }); + await saveManifest(projectRoot, personalization.manifest); + readiness = createReadinessReport(await runScanner(projectRoot), { + generatorVersion: KIT_VERSION, + }); + readiness.appliedSafeFixes = readinessExecution.after.appliedSafeFixes; + } + await writeReadinessSnapshot(projectRoot, readiness); - return { - projectRoot, - manifestPath, - stats, - readiness, - safeChanges: readinessExecution.changes, - }; + return { + projectRoot, + manifestPath, + stats, + readiness, + safeChanges: readinessExecution.changes, + }; + } finally { + await registry.unlock?.(); + } } export const installCommand = defineCommand({ meta: { name: "install", - description: "Bootstrap L0 (+ optional packs) from the registry and write agent-kit.json.", + description: "Bootstrap L0 (+ optional packs) and write .cursor/agent-kit.json.", }, args: { profile: { @@ -138,6 +148,17 @@ export const installCommand = defineCommand({ type: "string", description: `Comma-separated L1 pack ids (e.g. cybersec,devops). Known: ${DOMAIN_PACK_IDS.join(", ")}`, }, + yes: { + type: "boolean", + alias: "y", + description: "Skip interactive prompts; use defaults (IDE-agnostic non-interactive mode)", + default: false, + }, + "force-root": { + type: "boolean", + description: "Bypass the ambiguous-root guard (use with caution)", + default: false, + }, cwd: { type: "string", default: process.cwd(), @@ -145,7 +166,26 @@ export const installCommand = defineCommand({ ...REGISTRY_CLI_ARGS, }, async run({ args }) { - const projectRoot = path.resolve(args.cwd); + const nonInteractive = args.yes || isNonInteractive(); + if (nonInteractive) { + logger.info("Non-interactive mode: skipping prompts, using defaults."); + } + + let projectRoot: string; + try { + projectRoot = await confirmProjectRoot(args.cwd, { + nonInteractive, + command: "install", + forceRoot: args["force-root"], + }); + } catch (err) { + if (err instanceof RootRefusedError) { + logger.error(err.message); + process.exitCode = 1; + return; + } + throw err; + } logger.info(`Installing into: ${projectRoot}`); const packs = parsePackList(args.pack); @@ -155,18 +195,25 @@ export const installCommand = defineCommand({ } } - 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, - }); - logApplyStats(result.stats); - logger.success(`Manifest written: ${result.manifestPath}`); - logger.success("Readiness snapshot written: .cursor/context/readiness.json"); - printReadinessNarrative(result); + 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, + }); + logApplyStats(result.stats); + logger.success(`Manifest written: ${result.manifestPath}`); + logger.success("Readiness snapshot written: .cursor/context/readiness.json"); + printReadinessNarrative(result); + } catch (err) { + const hint = classifyInstallError(err); + logger.error(hint.message); + console.error(`\n${hint.recovery}\n`); + process.exit(1); + } }, }); diff --git a/packages/cli/src/commands/monitors.ts b/packages/cli/src/commands/monitors.ts index 574e1ef..37e06e7 100644 --- a/packages/cli/src/commands/monitors.ts +++ b/packages/cli/src/commands/monitors.ts @@ -5,7 +5,7 @@ import { selectUntriagedMonitors } from "../invariants/monitors-untriaged.js"; export const monitorsCommand = defineCommand({ meta: { name: "monitors", - description: "Plan-monitor selection helpers (untriaged SoT for /plan-review-triage)", + description: "Select untriaged plan monitors (--untriaged; SoT for chat triage).", }, args: { cwd: { diff --git a/packages/cli/src/commands/run-plan.ts b/packages/cli/src/commands/run-plan.ts index e259d81..f541cab 100644 --- a/packages/cli/src/commands/run-plan.ts +++ b/packages/cli/src/commands/run-plan.ts @@ -8,8 +8,7 @@ import { logger } from "../utils/logger.js"; export const runPlanCommand = defineCommand({ meta: { name: "run-plan", - description: - "Headless continuous plan runner: one fresh agent per tick (LOOP_TICK_RESULT contract). Never git-prod.", + description: "Headless continuous plan runner (one fresh agent per tick). Never git-prod.", }, args: { cwd: { diff --git a/packages/cli/src/commands/scan.ts b/packages/cli/src/commands/scan.ts index ff26de4..a3f9ace 100644 --- a/packages/cli/src/commands/scan.ts +++ b/packages/cli/src/commands/scan.ts @@ -5,7 +5,7 @@ import { logger } from "../utils/logger.js"; export const scanCommand = defineCommand({ meta: { name: "scan", - description: "Scan the current repository and print detected profile.", + description: "Scan the repository and print the detected profile.", }, args: { cwd: { diff --git a/packages/cli/src/commands/status.ts b/packages/cli/src/commands/status.ts index cee2beb..6e970dd 100644 --- a/packages/cli/src/commands/status.ts +++ b/packages/cli/src/commands/status.ts @@ -31,7 +31,7 @@ function profileStatus(profile: RepositoryProfile | Record<string, unknown> | nu export const statusCommand = defineCommand({ meta: { name: "status", - description: "Show Agent Kit distribution status (manifest + optional wizard profile).", + description: "Show installed kit version, manifest, and optional profile.", }, args: { cwd: { diff --git a/packages/cli/src/commands/update.ts b/packages/cli/src/commands/update.ts index 919a769..4d113b5 100644 --- a/packages/cli/src/commands/update.ts +++ b/packages/cli/src/commands/update.ts @@ -8,12 +8,13 @@ import { syncFromManifest } from "../lifecycle/sync.js"; 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"; export const updateCommand = defineCommand({ meta: { name: "update", description: - "Re-apply L0/packs/skills from the registry; never overwrites L3 protected paths. Use --check for notify-only.", + "Re-apply L0/packs/skills from the registry (never overwrites L3). --check = notify-only.", }, args: { cwd: { @@ -48,6 +49,17 @@ export const updateCommand = defineCommand({ "Seed the managed-hash ledger from current local overlay files before applying (factory/dogfood only; consumers should not use this)", default: false, }, + yes: { + type: "boolean", + alias: "y", + description: "Skip interactive prompts; use defaults (IDE-agnostic non-interactive mode)", + default: false, + }, + "force-root": { + type: "boolean", + description: "Bypass the ambiguous-root guard (use with caution)", + default: false, + }, ...REGISTRY_CLI_ARGS, }, async run({ args }) { @@ -74,46 +86,66 @@ export const updateCommand = defineCommand({ return; } - const existing = await loadAgentKitManifest(args.cwd); + const nonInteractive = args.yes || isNonInteractive(); + let projectRoot: string; + try { + projectRoot = await confirmProjectRoot(args.cwd, { + nonInteractive, + command: "update", + forceRoot: args["force-root"], + }); + } catch (err) { + if (err instanceof RootRefusedError) { + logger.error(err.message); + process.exitCode = 1; + return; + } + throw err; + } + + const existing = await loadAgentKitManifest(projectRoot); if (!existing) { logger.warn("No .cursor/agent-kit.json — run agent-kit install first."); return; } const registry = await resolveRegistryFromCli({ - cwd: args.cwd, + cwd: projectRoot, registry: args.registry, url: args.url, ref: args.ref, refresh: args.refresh, manifest: existing, }); + try { + logger.info(`Registry: ${registry.root} (${registry.source})`); - logger.info(`Registry: ${registry.root} (${registry.source})`); - - const next = buildManifest({ - version: KIT_VERSION, - profile: existing.profile, - packs: existing.packs, - skills: existing.skills, - protected: existing.protected, - personalization: existing.personalization, - registryUrl: registry.url ?? existing.registry?.url, - registryRef: registry.ref ?? existing.registry?.ref, - }); - // Preserve optional metadata from existing manifest - if (existing.overrides?.length) next.overrides = existing.overrides; - if (next.version === existing.version && existing.installedAt) { - next.installedAt = existing.installedAt; - } + const next = buildManifest({ + version: KIT_VERSION, + profile: existing.profile, + packs: existing.packs, + skills: existing.skills, + protected: existing.protected, + personalization: existing.personalization, + registryUrl: registry.url ?? existing.registry?.url, + registryRef: registry.ref ?? existing.registry?.ref, + }); + // Preserve optional metadata from existing manifest + if (existing.overrides?.length) next.overrides = existing.overrides; + if (next.version === existing.version && existing.installedAt) { + next.installedAt = existing.installedAt; + } - if (args["seed-overlay"]) { - await seedManagedHashLedger(args.cwd); - logger.info("Seeded managed-hash ledger from current local overlay files."); + if (args["seed-overlay"]) { + await seedManagedHashLedger(projectRoot); + logger.info("Seeded managed-hash ledger from current local overlay files."); + } + const stats = await syncFromManifest(registry.root, projectRoot, next); + await saveManifest(projectRoot, next); + logApplyStats(stats); + logger.success("Update complete (L3 protected paths left untouched)."); + } finally { + await registry.unlock?.(); } - const stats = await syncFromManifest(registry.root, args.cwd, next); - await saveManifest(args.cwd, next); - logApplyStats(stats); - logger.success("Update complete (L3 protected paths left untouched)."); }, }); diff --git a/packages/cli/src/commands/validate.ts b/packages/cli/src/commands/validate.ts index ac04a1c..c7de1a5 100644 --- a/packages/cli/src/commands/validate.ts +++ b/packages/cli/src/commands/validate.ts @@ -48,7 +48,7 @@ function isPlanPath(filePath: string): boolean { export const validateCommand = defineCommand({ meta: { name: "validate", - description: "Advisory validators for HANDOFF / plan frontmatter (afterFileEdit adapter)", + description: "Advisory validators for HANDOFF / plan frontmatter (hook adapter).", }, subCommands: { handoff: defineCommand({ diff --git a/packages/cli/src/dashboard/ci-private-origin-allowlist.test.ts b/packages/cli/src/dashboard/ci-private-origin-allowlist.test.ts new file mode 100644 index 0000000..6e2d357 --- /dev/null +++ b/packages/cli/src/dashboard/ci-private-origin-allowlist.test.ts @@ -0,0 +1,57 @@ +import { existsSync, readFileSync } from "node:fs"; +import { resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import { describe, expect, it } from "vitest"; + +const repoRoot = resolve(fileURLToPath(import.meta.url), "../../../../.."); +const ciYmlPath = resolve(repoRoot, ".github/workflows/ci.yml"); + +/** + * Mechanical pin: Path C remirrors ci.yml every release. If the private-origin + * allowlist condition is dropped or flipped back to a public denylist, public + * (or fork) tag CI can re-run sync-public / publish-npm without secrets. + * ADR: 2026-07-29_public-mirror-skip-sync-publish-ci (allowlist amend). + * + * When ci.yml is absent from a checkout, tests skip (no collection-time throw). + */ +describe("ci.yml private-origin allowlist pin", () => { + const ciPresent = existsSync(ciYmlPath); + const body = ciPresent ? readFileSync(ciYmlPath, "utf8") : ""; + const allowlist = "github.repository == 'agent-kit-startup/agent-kit-dev'"; + // Tolerant denylist pin (\s* around !=; single or double quotes), same technique as allowlist count. + const denylistRe = /github\.repository\s*!=\s*['"]agent-kit-startup\/agent-kit['"]/; + + it.skipIf(!ciPresent)( + "keeps the allowlist condition on sync-public and publish-npm job ifs", + () => { + expect(body).toContain("sync-public:"); + expect(body).toContain("publish-npm:"); + const syncIdx = body.indexOf("sync-public:"); + const publishIdx = body.indexOf("publish-npm:"); + expect(syncIdx).toBeGreaterThan(-1); + expect(publishIdx).toBeGreaterThan(syncIdx); + const syncBlock = body.slice(syncIdx, publishIdx); + const publishBlock = body.slice(publishIdx); + expect(syncBlock).toContain(allowlist); + expect(publishBlock).toContain(allowlist); + expect(syncBlock).not.toMatch(denylistRe); + expect(publishBlock).not.toMatch(denylistRe); + }, + ); + + it.skipIf(!ciPresent)( + "keeps private-only build steps on the same allowlist (exactly six sites, no denylist)", + () => { + expect(body).toContain("Authority graph parity"); + expect(body).toContain("Public-deny-link guard"); + expect(body).toContain("Evidence checks"); + expect(body).toContain("Registry catalog parity"); + // Exactly six allowlist sites today: 4 build steps + sync-public + publish-npm. + // Maintenance: changing this count requires matching comments at every counted + // site in .github/workflows/ci.yml (build private-only steps, sync-public, publish-npm). + const matches = body.match(/github\.repository\s*==\s*'agent-kit-startup\/agent-kit-dev'/g); + expect(matches?.length).toBe(6); + expect(body).not.toMatch(denylistRe); + }, + ); +}); diff --git a/packages/cli/src/dashboard/guards.test.ts b/packages/cli/src/dashboard/guards.test.ts index 55b9bf1..dc812b9 100644 --- a/packages/cli/src/dashboard/guards.test.ts +++ b/packages/cli/src/dashboard/guards.test.ts @@ -225,6 +225,27 @@ describe("config write allowlist", () => { expect(merged.customKeep).toEqual({ nested: true }); }); + it("clears an existing persona mode override when patch sends null (Inherit)", () => { + const merged = mergeConfigAllowlist( + { + agentPersona: { + default: "autopilot", + modes: { "run-plan": "night-shift", "continue-plan": "autopilot" }, + }, + }, + { + agentPersona: { modes: { "run-plan": null } }, + }, + ); + expect(merged.agentPersona).toEqual({ + default: "autopilot", + modes: { "continue-plan": "autopilot" }, + }); + expect(validateConfigWriteBody({ agentPersona: { modes: { "run-plan": null } } }).ok).toBe( + true, + ); + }); + it("locks config path under repo and recognizes loopback addresses", () => { const locked = resolveContextConfigPath(repoRoot, { existsSync, realpathSync }); expect(locked.ok).toBe(true); @@ -435,6 +456,89 @@ describe("broadcast auth gate", () => { }); }); +describe("broadcast share URL mask", () => { + it("encodes and decodes fragment payloads with TTL", async () => { + const { + buildBroadcastShareUrl, + decodeBroadcastShareFragment, + encodeBroadcastSharePayload, + resolveShareBase, + resolveShareShowLan, + DEFAULT_SHARE_BASE, + validateBroadcastShareTarget, + normalizeShareBase, + isPublicBroadcastShareShell, + shareShellTokenRequired, + } = await import("../../../../dashboard/lib/broadcast-share.mjs"); + + expect(DEFAULT_SHARE_BASE).toBe("https://missionkit.io/mc/open.html"); + expect(resolveShareBase({})).toBe(DEFAULT_SHARE_BASE); + expect(resolveShareBase({ MISSION_CONTROL_SHARE_BASE: "off" })).toBeNull(); + expect(resolveShareBase({ MISSION_CONTROL_SHARE_BASE: " https://share.example/open/ " })).toBe( + "https://share.example/open", + ); + expect(resolveShareBase({ MISSION_CONTROL_SHARE_BASE: "http://evil.example/open" })).toBeNull(); + expect(normalizeShareBase("http://127.0.0.1:4173/open.html").ok).toBe(true); + expect(resolveShareShowLan({})).toBe(true); + expect(resolveShareShowLan({ MISSION_CONTROL_SHARE_SHOW_LAN: "0" })).toBe(false); + + expect(validateBroadcastShareTarget("https://attacker.example/login").ok).toBe(false); + expect(validateBroadcastShareTarget("http://192.168.1.20:3340/?token=x").ok).toBe(true); + expect(validateBroadcastShareTarget("http://127.0.0.1:3333/?token=x").ok).toBe(true); + + expect(isPublicBroadcastShareShell("GET", "/open.html")).toBe(true); + expect(isPublicBroadcastShareShell("GET", "/open")).toBe(true); + expect(isPublicBroadcastShareShell("POST", "/open.html")).toBe(false); + expect(isPublicBroadcastShareShell("GET", "/dashboard-data.json")).toBe(false); + expect(shareShellTokenRequired(true, "GET", "/open.html")).toBe(false); + expect(shareShellTokenRequired(true, "GET", "/open")).toBe(false); + expect(shareShellTokenRequired(true, "GET", "/api/data")).toBe(true); + expect(shareShellTokenRequired(true, "POST", "/open")).toBe(true); + expect(shareShellTokenRequired(false, "GET", "/api/data")).toBe(false); + + const lan = "http://192.168.1.20:3340/?token=tokentrain123456"; + const frag = encodeBroadcastSharePayload(lan, { ttlSec: 60, nowSec: 1_700_000_000 }); + expect(frag.startsWith("v1.")).toBe(true); + const decoded = decodeBroadcastShareFragment(frag, { nowSec: 1_700_000_010 }); + expect(decoded).toEqual({ ok: true, url: lan, expiresAt: 1_700_000_060 }); + expect(decodeBroadcastShareFragment(frag, { nowSec: 1_700_000_061 })).toEqual({ + ok: false, + error: "expired", + }); + + expect(() => encodeBroadcastSharePayload("https://attacker.example/login")).toThrow( + /non-private-target/, + ); + expect(() => + encodeBroadcastSharePayload("http://100.64.1.2:3340/?token=tokentrain123456"), + ).toThrow(/non-private-target/); + expect( + decodeBroadcastShareFragment( + // crafted public-host payload (same shape as pre-harden attacker fragment) + `v1.${Buffer.from( + JSON.stringify({ v: 1, u: "https://attacker.example/login" }), + "utf8", + ).toString("base64url")}`, + ), + ).toEqual({ ok: false, error: "non-private-target" }); + + const share = buildBroadcastShareUrl(lan, { + base: "https://missionkit.io/mc/open.html/", + ttlSec: 0, + }); + expect(share).toBeTruthy(); + if (!share) throw new Error("expected share URL"); + expect(share.startsWith("https://missionkit.io/mc/open.html#v1.")).toBe(true); + const hash = share.slice(share.indexOf("#") + 1); + expect(decodeBroadcastShareFragment(hash)).toEqual({ + ok: true, + url: lan, + expiresAt: null, + }); + expect(buildBroadcastShareUrl(lan, { base: null })).toBeNull(); + }); +}); + describe("isAllowedOrigin", () => { const port = 3333; @@ -536,3 +640,64 @@ describe("isSafeRepoRelativePath", () => { expect(isSafeRepoRelativePath("")).toBe(false); }); }); + +describe("serve.mjs HTTP auth exemption for share shell", () => { + it("GET /open and /open.html succeed without token while data stays 401", async () => { + const { spawn } = await import("node:child_process"); + const { createServer } = await import("node:net"); + const { once } = await import("node:events"); + const servePath = resolve(repoRoot, "dashboard/serve.mjs"); + + const port = await new Promise<number>((resolvePort, reject) => { + const s = createServer(); + s.listen(0, "127.0.0.1", () => { + const addr = s.address(); + const p = typeof addr === "object" && addr ? addr.port : 0; + s.close(() => resolvePort(p)); + }); + s.on("error", reject); + }); + + const token = "a".repeat(16); + const child = spawn(process.execPath, [servePath], { + cwd: repoRoot, + env: { + ...process.env, + HOST: "0.0.0.0", + PORT: String(port), + MISSION_CONTROL_TOKEN: token, + MISSION_CONTROL_NO_OPEN: "1", + }, + stdio: ["ignore", "pipe", "pipe"], + }); + + const deadline = Date.now() + 15_000; + let ready = false; + while (Date.now() < deadline) { + try { + const res = await fetch(`http://127.0.0.1:${port}/open.html`); + if (res.status === 200 || res.status === 401) { + ready = true; + break; + } + } catch { + await new Promise((r) => setTimeout(r, 100)); + } + } + expect(ready).toBe(true); + + try { + const openHtml = await fetch(`http://127.0.0.1:${port}/open.html`); + expect(openHtml.status).toBe(200); + const openAlias = await fetch(`http://127.0.0.1:${port}/open`); + expect(openAlias.status).toBe(200); + const data = await fetch(`http://127.0.0.1:${port}/dashboard-data.json`); + expect(data.status).toBe(401); + const dataOk = await fetch(`http://127.0.0.1:${port}/dashboard-data.json?token=${token}`); + expect(dataOk.status).toBe(200); + } finally { + child.kill("SIGTERM"); + await once(child, "exit").catch(() => undefined); + } + }, 30_000); +}); diff --git a/packages/cli/src/dashboard/open-browser.test.ts b/packages/cli/src/dashboard/open-browser.test.ts new file mode 100644 index 0000000..c01da59 --- /dev/null +++ b/packages/cli/src/dashboard/open-browser.test.ts @@ -0,0 +1,387 @@ +import { mkdirSync, mkdtempSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { describe, expect, it, vi } from "vitest"; +import { + OS_DEFAULT_TOKENS, + buildOpenBrowserCommand, + isSafePreferredBrowser, + normalizePreferredBrowser, + openBrowser, + readPreferredBrowserFromConfig, + resolvePreferredBrowser, + shouldSkipOpen, +} from "../../../../dashboard/lib/open-browser.mjs"; +import { applyDashboardOpenEnv, readPreferredBrowserFromWorkspace } from "../commands/dashboard.js"; + +describe("normalizePreferredBrowser", () => { + it("maps empty and sentinel tokens to OS default (null)", () => { + expect(normalizePreferredBrowser(null)).toBeNull(); + expect(normalizePreferredBrowser("")).toBeNull(); + expect(normalizePreferredBrowser("default")).toBeNull(); + expect(normalizePreferredBrowser("OS")).toBeNull(); + expect(normalizePreferredBrowser("ask")).toBeNull(); + for (const token of OS_DEFAULT_TOKENS) { + if (!token) continue; + expect(normalizePreferredBrowser(token)).toBeNull(); + } + }); + + it("keeps named browsers", () => { + expect(normalizePreferredBrowser("Google Chrome")).toBe("Google Chrome"); + expect(normalizePreferredBrowser(" Firefox ")).toBe("Firefox"); + expect(normalizePreferredBrowser("msedge")).toBe("msedge"); + }); + + it("rejects path separators and shell metacharacters", () => { + expect(isSafePreferredBrowser("./pwn.sh")).toBe(false); + expect(isSafePreferredBrowser("/usr/bin/env")).toBe(false); + expect(isSafePreferredBrowser("C:\\Windows\\chrome.exe")).toBe(false); + expect(isSafePreferredBrowser("firefox;rm -rf /")).toBe(false); + expect(isSafePreferredBrowser("$(evil)")).toBe(false); + expect(isSafePreferredBrowser('a" x "b')).toBe(false); + expect(isSafePreferredBrowser("%USERPROFILE%")).toBe(false); + expect(isSafePreferredBrowser("chrome^")).toBe(false); + expect(isSafePreferredBrowser("bad'quote")).toBe(false); + expect(normalizePreferredBrowser("./pwn.sh")).toBeNull(); + expect(normalizePreferredBrowser("/usr/bin/env")).toBeNull(); + expect(normalizePreferredBrowser("bad`cmd`")).toBeNull(); + }); +}); + +describe("resolvePreferredBrowser", () => { + it("prefers env over config", () => { + expect( + resolvePreferredBrowser({ + env: { MISSION_CONTROL_PREFERRED_BROWSER: "Firefox" }, + configValue: "Google Chrome", + }), + ).toBe("Firefox"); + }); + + it("falls back to config when env unset", () => { + expect( + resolvePreferredBrowser({ + env: {}, + configValue: "Safari", + }), + ).toBe("Safari"); + }); + + it("returns null when both unset", () => { + expect(resolvePreferredBrowser({ env: {}, configValue: null })).toBeNull(); + }); + + it("treats unsafe env values as OS default", () => { + expect( + resolvePreferredBrowser({ + env: { MISSION_CONTROL_PREFERRED_BROWSER: "../evil" }, + configValue: "Firefox", + }), + ).toBeNull(); + }); +}); + +describe("shouldSkipOpen / openBrowser no-open", () => { + it("skips when MISSION_CONTROL_NO_OPEN=1", () => { + expect(shouldSkipOpen({ MISSION_CONTROL_NO_OPEN: "1" })).toBe(true); + const spawnFn = vi.fn(); + const result = openBrowser("http://127.0.0.1:3333/", { + env: { MISSION_CONTROL_NO_OPEN: "1" }, + spawnFn, + }); + expect(result).toEqual({ opened: false, reason: "no-open" }); + expect(spawnFn).not.toHaveBeenCalled(); + }); +}); + +describe("buildOpenBrowserCommand", () => { + it("uses open -a on darwin when preferred is set", () => { + expect( + buildOpenBrowserCommand({ + url: "http://127.0.0.1:3333/", + preferred: "Google Chrome", + platform: "darwin", + }), + ).toEqual({ + command: "open", + args: ["-a", "Google Chrome", "http://127.0.0.1:3333/"], + }); + }); + + it("uses bare open on darwin when preferred is null", () => { + expect( + buildOpenBrowserCommand({ + url: "http://127.0.0.1:3333/", + preferred: null, + platform: "darwin", + }), + ).toEqual({ command: "open", args: ["http://127.0.0.1:3333/"] }); + }); + + it("uses preferred binary on linux", () => { + expect( + buildOpenBrowserCommand({ + url: "http://127.0.0.1:3333/", + preferred: "firefox", + platform: "linux", + }), + ).toEqual({ command: "firefox", args: ["http://127.0.0.1:3333/"] }); + }); + + it("uses xdg-open on linux when preferred is null", () => { + expect( + buildOpenBrowserCommand({ + url: "http://127.0.0.1:3333/", + preferred: null, + platform: "linux", + }), + ).toEqual({ command: "xdg-open", args: ["http://127.0.0.1:3333/"] }); + }); + + it("uses cmd start with program name on win32 when preferred is set", () => { + expect( + buildOpenBrowserCommand({ + url: "http://127.0.0.1:3333/", + preferred: "msedge", + platform: "win32", + }), + ).toEqual({ + command: "cmd", + args: ["/c", "start", "", "msedge", "http://127.0.0.1:3333/"], + }); + }); + + it("uses bare cmd start on win32 when preferred is null", () => { + expect( + buildOpenBrowserCommand({ + url: "http://127.0.0.1:3333/", + preferred: null, + platform: "win32", + }), + ).toEqual({ command: "cmd", args: ["/c", "start", "", "http://127.0.0.1:3333/"] }); + }); + + it("returns null for invalid url", () => { + expect(buildOpenBrowserCommand({ url: "", preferred: null, platform: "darwin" })).toBeNull(); + expect(buildOpenBrowserCommand({ url: " ", preferred: null, platform: "linux" })).toBeNull(); + }); +}); + +describe("openBrowser spawn", () => { + it("spawns exactly one process with resolved args", () => { + const child = { unref: vi.fn(), on: vi.fn() }; + const spawnFn = vi.fn(() => child); + const result = openBrowser("http://127.0.0.1:3511/", { + env: {}, + preferred: "Google Chrome", + platform: "darwin", + spawnFn, + }); + expect(result.opened).toBe(true); + expect(spawnFn).toHaveBeenCalledTimes(1); + expect(spawnFn).toHaveBeenCalledWith( + "open", + ["-a", "Google Chrome", "http://127.0.0.1:3511/"], + { detached: true, stdio: "ignore" }, + ); + expect(child.unref).toHaveBeenCalled(); + expect(child.on).toHaveBeenCalledWith("error", expect.any(Function)); + }); + + it("falls back to OS default when preferred spawn throws", () => { + const child = { unref: vi.fn(), on: vi.fn() }; + const spawnFn = vi + .fn() + .mockImplementationOnce(() => { + throw Object.assign(new Error("spawn ENOENT"), { code: "ENOENT" }); + }) + .mockImplementationOnce(() => child); + const result = openBrowser("http://127.0.0.1:3511/", { + env: {}, + preferred: "NoSuchBrowser", + platform: "linux", + spawnFn, + }); + expect(result.opened).toBe(true); + expect(result.reason).toBe("preferred-fallback"); + expect(spawnFn).toHaveBeenCalledTimes(2); + expect(spawnFn.mock.calls[1]?.[0]).toBe("xdg-open"); + }); + + it("returns spawn-failed when preferred and fallback both fail", () => { + const spawnFn = vi.fn(() => { + throw new Error("spawn failed"); + }); + const result = openBrowser("http://127.0.0.1:3511/", { + env: {}, + preferred: "bad-bin", + platform: "linux", + spawnFn, + }); + expect(result.opened).toBe(false); + expect(result.reason).toBe("spawn-failed"); + }); + + it("returns invalid-url for empty url", () => { + const spawnFn = vi.fn(); + expect(openBrowser("", { env: {}, spawnFn })).toEqual({ + opened: false, + reason: "invalid-url", + }); + expect(spawnFn).not.toHaveBeenCalled(); + }); + + it("uses spawnSyncFn for darwin preferred and falls back on non-zero exit", () => { + const spawnSyncFn = vi + .fn() + .mockReturnValueOnce({ status: 1, error: null }) + .mockReturnValueOnce({ status: 0, error: null }); + const result = openBrowser("http://127.0.0.1:3511/", { + env: {}, + preferred: "NoSuchBrowser.app", + platform: "darwin", + spawnSyncFn, + }); + expect(result.opened).toBe(true); + expect(result.reason).toBe("preferred-fallback"); + expect(spawnSyncFn).toHaveBeenNthCalledWith( + 1, + "open", + ["-a", "NoSuchBrowser.app", "http://127.0.0.1:3511/"], + expect.objectContaining({ encoding: "utf8" }), + ); + expect(spawnSyncFn).toHaveBeenNthCalledWith( + 2, + "open", + ["http://127.0.0.1:3511/"], + expect.objectContaining({ encoding: "utf8" }), + ); + }); + + it("uses spawnSyncFn which-probe for linux preferred miss then falls back", () => { + const child = { unref: vi.fn(), on: vi.fn() }; + const spawnFn = vi.fn(() => child); + const spawnSyncFn = vi + .fn() + .mockReturnValueOnce({ status: 1, error: null }) + .mockReturnValueOnce({ status: 0, error: null }); + const result = openBrowser("http://127.0.0.1:3511/", { + env: {}, + preferred: "nosuch-browser", + platform: "linux", + spawnFn, + spawnSyncFn, + }); + expect(result.opened).toBe(true); + expect(result.reason).toBe("preferred-fallback"); + expect(spawnSyncFn).toHaveBeenNthCalledWith( + 1, + "which", + ["nosuch-browser"], + expect.objectContaining({ encoding: "utf8" }), + ); + expect(spawnSyncFn).toHaveBeenNthCalledWith( + 2, + "which", + ["xdg-open"], + expect.objectContaining({ encoding: "utf8" }), + ); + expect(spawnFn).toHaveBeenCalledWith("xdg-open", ["http://127.0.0.1:3511/"], { + detached: true, + stdio: "ignore", + }); + }); + + it("reports spawn-failed when win32 preferred spawnSync exits non-zero and fallback also fails", () => { + const spawnSyncFn = vi.fn(() => ({ status: 1, error: null })); + const result = openBrowser("http://127.0.0.1:3511/", { + env: {}, + preferred: "msedge", + platform: "win32", + spawnSyncFn, + }); + expect(result.opened).toBe(false); + expect(result.reason).toBe("spawn-failed"); + expect(spawnSyncFn).toHaveBeenCalledWith( + "cmd", + ["/c", "start", "", "msedge", "http://127.0.0.1:3511/"], + expect.objectContaining({ encoding: "utf8", windowsHide: true }), + ); + expect(spawnSyncFn).toHaveBeenCalledWith( + "cmd", + ["/c", "start", "", "http://127.0.0.1:3511/"], + expect.objectContaining({ encoding: "utf8", windowsHide: true }), + ); + }); + + it("reports spawn-failed when OS-default linux which-probe misses xdg-open", () => { + const spawnFn = vi.fn(); + const spawnSyncFn = vi.fn(() => ({ status: 1, error: null })); + const result = openBrowser("http://127.0.0.1:3511/", { + env: {}, + preferred: null, + platform: "linux", + spawnFn, + spawnSyncFn, + }); + expect(result).toEqual({ + opened: false, + reason: "spawn-failed", + command: "xdg-open", + args: ["http://127.0.0.1:3511/"], + }); + expect(spawnFn).not.toHaveBeenCalled(); + }); +}); + +describe("readPreferredBrowserFromConfig", () => { + it("reads missionControl.preferredBrowser from JSON", () => { + const dir = mkdtempSync(join(tmpdir(), "ak-pref-")); + const configPath = join(dir, "config.json"); + writeFileSync(configPath, JSON.stringify({ missionControl: { preferredBrowser: "Firefox" } })); + expect(readPreferredBrowserFromConfig(configPath)).toBe("Firefox"); + }); + + it("returns null for missing file", () => { + expect(readPreferredBrowserFromConfig(join(tmpdir(), "no-such-config.json"))).toBeNull(); + }); +}); + +describe("applyDashboardOpenEnv / workspace reader", () => { + it("sets NO_OPEN and --browser flag", () => { + const env = applyDashboardOpenEnv({}, { noOpen: true, browser: "Safari" }); + expect(env.MISSION_CONTROL_NO_OPEN).toBe("1"); + expect(env.MISSION_CONTROL_PREFERRED_BROWSER).toBe("Safari"); + }); + + it("drops unsafe --browser values", () => { + const env = applyDashboardOpenEnv({}, { browser: "./pwn.sh" }); + expect(env.MISSION_CONTROL_PREFERRED_BROWSER).toBeUndefined(); + }); + + it("loads preferred browser from workspace config when flag absent", () => { + const root = mkdtempSync(join(tmpdir(), "ak-ws-pref-")); + mkdirSync(join(root, ".cursor", "context"), { recursive: true }); + writeFileSync( + join(root, ".cursor", "context", "config.json"), + JSON.stringify({ missionControl: { preferredBrowser: "Brave Browser" } }), + ); + expect(readPreferredBrowserFromWorkspace(root)).toBe("Brave Browser"); + const env = applyDashboardOpenEnv({}, { cwd: root }); + expect(env.MISSION_CONTROL_PREFERRED_BROWSER).toBe("Brave Browser"); + }); + + it("CLI workspace reader shares OS_DEFAULT_TOKENS / normalize SoT with the helper", () => { + expect(normalizePreferredBrowser("default")).toBeNull(); + expect(normalizePreferredBrowser("ask")).toBeNull(); + expect(OS_DEFAULT_TOKENS.has("os")).toBe(true); + const root = mkdtempSync(join(tmpdir(), "ak-ws-sentinel-")); + mkdirSync(join(root, ".cursor", "context"), { recursive: true }); + writeFileSync( + join(root, ".cursor", "context", "config.json"), + JSON.stringify({ missionControl: { preferredBrowser: "os" } }), + ); + expect(readPreferredBrowserFromWorkspace(root)).toBeNull(); + }); +}); diff --git a/packages/cli/src/dashboard/plugin-ux-validation.test.ts b/packages/cli/src/dashboard/plugin-ux-validation.test.ts index 6d935fa..31bea1f 100644 --- a/packages/cli/src/dashboard/plugin-ux-validation.test.ts +++ b/packages/cli/src/dashboard/plugin-ux-validation.test.ts @@ -10,6 +10,7 @@ import { resolveBindHost, } from "../../../../dashboard/lib/guards.mjs"; import { + MONITOR_ACTIVITY_KINDS, MONITOR_FEED_CAP, buildMissionControlView, classifyPlan, @@ -20,6 +21,75 @@ const repoRoot = resolve(fileURLToPath(import.meta.url), "../../../../.."); const dashboardHtml = readFileSync(resolve(repoRoot, "dashboard/dashboard.html"), "utf8"); const serveSource = readFileSync(resolve(repoRoot, "dashboard/serve.mjs"), "utf8"); +/** + * The Crew Monitor render block only (`#hero-activity` through the Plans + * section), so a pin about the Crew row cannot be satisfied — or broken — by + * the Activity tab, which renders a different row from the same helpers. + */ +function crewMonitorFeedBlock(html: string): string { + const start = html.indexOf('<div class="section-hero" id="hero-activity">'); + if (start < 0) return ""; + const end = html.indexOf('id="section-plans"', start); + if (end < 0) return ""; + return html.slice(start, end); +} + +/** Crew Monitor row return-template only (not CSS / whole-file proximity). */ +function crewMonitorRowRenderTemplate(html: string): string { + const marker = '<div class="monitor-row stagger-fade stagger-${staggerIdx}"'; + const start = html.indexOf(marker); + if (start < 0) return ""; + const end = html.indexOf("</div>`;", start); + if (end < 0) return ""; + return html.slice(start, end); +} + +/** + * Structural pin for the design-v2 row order: the team badge is a row sibling at + * row start, then actor → verb → primary → meta → time. Inverts the previous + * `expectChipIsRowSiblingBeforeFeedLabel` / `expectChipInsideFeedLabelNotRowSibling` + * pins at the same strictness. + * + * Finding E (close-crew … r1-r3 residuals) showed the old chip helper only locked + * the avatar→feed-label gap, so `${chipHtml}` after feed-label still passed. The + * whole-template counts below close that half-locked slot for the badge-era row: + * `${chipHtml}` / `monitor-row-chip` must be zero anywhere in the row template, + * and `${badgeHtml}` must appear exactly once. + * + * That segment row is the layout the design was commissioned to replace: equal + * shrink across every segment is what produced `revi…` / `P…` / `8f…`, so a + * regression back to it must fail here rather than pass quietly. The badge is + * also NOT the `#631` avatar box returning — it carries the kind colour and the + * kind gloss, which the avatar never did. + * Verdict: .cursor/context/mission-control-design/remote/v1/ACCEPTANCE.md + * ADR: .cursor/memory/decisions/2026-07-27_crew-monitor-vs-plan-monitor-glossary.md + */ +function expectBadgeIsRowSiblingBeforeActor(html: string) { + const rowTpl = crewMonitorRowRenderTemplate(html); + expect( + rowTpl.length, + "crew row render template not found — update the marker in crewMonitorRowRenderTemplate", + ).toBeGreaterThan(0); + // Row order: badge → actor → verb → primary → meta → time, each a direct child. + expect(rowTpl).toMatch( + /\$\{badgeHtml\}[\s\S]*?\$\{actorHtml\}[\s\S]*?\$\{verbHtml\}[\s\S]*?<span class="monitor-row-primary"[\s\S]*?<span class="monitor-row-meta"[\s\S]*?<span class="monitor-row-time"/, + ); + // Nothing renders before the badge inside the row. + const beforeBadge = rowTpl.slice(0, rowTpl.indexOf("${badgeHtml}")); + expect(beforeBadge).not.toMatch(/<span/); + // The v1 segment row must not come back in any form. + expect(rowTpl).not.toMatch(/feed-label|feed-seg|feed-sep|feedSegSpans|feedLabelHtml/); + // Whole-template chip lock (finding E): no half-locked sibling slot after feed-label. + expect(rowTpl.match(/\$\{chipHtml\}/g)?.length ?? 0).toBe(0); + expect(rowTpl.match(/monitor-row-chip/g)?.length ?? 0).toBe(0); + expect(rowTpl.match(/monitor-row-icon/g)?.length ?? 0).toBe(0); + expect(rowTpl).not.toMatch(/info\.icon/); + // Exactly one badge interpolation owns the kind tint. + expect(rowTpl.match(/\$\{badgeHtml\}/g)?.length ?? 0).toBe(1); + // The avatar box stays gone (it is not what the badge is). + expect(rowTpl).not.toMatch(/monitor-row-avatar|agentInitials/); +} + const lifecycleStates = [ "executing", "awaiting_user", @@ -274,8 +344,14 @@ describe("plugin-ux-validation: narrow shell + a11y chrome", () => { expect(cursorBlock?.[1]).not.toMatch(/--mc-radius/); expect(cursorBlock?.[1]).not.toMatch(/--mc-card-padding/); expect(cursorBlock?.[1]).not.toMatch(/--mc-chrome-meta-size/); - // Capsule token feeds slash-command pill geometry (not --mc-radius-sm chrome). + // Status/lifecycle pills squared off to the chrome radius (design v2): the + // capsule read as a foreign shape next to the chrome it always sits beside. + // The capsule token stays alive for the queue-role pill below, which is the + // one pill that must NOT read as a status chip. expect(dashboardHtml).toMatch( + /\.lifecycle-pill\s*\{[^}]*border-radius:\s*var\(--mc-radius-chrome\)/, + ); + expect(dashboardHtml).not.toMatch( /\.lifecycle-pill\s*\{[^}]*border-radius:\s*var\(--mc-radius-pill\)/, ); expect(dashboardHtml).toMatch( @@ -672,64 +748,224 @@ describe("plugin-ux-validation: narrow shell + a11y chrome", () => { expect(dataSource).toMatch(/a\.kitManaged\s*=\s*kitAgentPaths\.has/); }); - it("renders Crew Monitor rows as a timestamped feed with agent initials monograms", () => { - // Per entry: avatar monogram (agent initials) + kind chip + action label + timestamp. - expect(dashboardHtml).toContain("monitor-row-avatar"); + it("renders Crew Monitor rows as badge-first timestamped rows with no kind glyph", () => { + // Per entry: team badge at row start + actor + verb + primary + refs + time. + expect(dashboardHtml).not.toContain("monitor-row-avatar"); + // agentInitials survives for the Agents card monogram; the Crew row monograms + // a spaced display role instead, so the two must stay separate functions. expect(dashboardHtml).toContain("function agentInitials(id)"); + expect(dashboardHtml).toContain("function crewActorInitials(role)"); expect(dashboardHtml).toContain("function crewEventActor(ev)"); expect(dashboardHtml).toContain("function crewEventTime(ev, info)"); - // Actor resolution mirrors briefActivityActor: kit agent, else Engineering - // Manager (delivery), else Squad when a plan is present, else Platform Engineer. + expect(dashboardHtml).toContain("function parseCrewRow(ev)"); + // WIRE resolution is unchanged and still mirrors briefActivityActor(): + // kit agent, else Eng (delivery), else SQ when a plan is present, else Eng. + // The lexicon is a display layer on top; it must not be folded in here. expect(dashboardHtml).toContain("if (ev && ev.agent) return String(ev.agent);"); - expect(dashboardHtml).toContain( - "if (ev && ev.kind === 'delivery') return 'Engineering Manager';", - ); - expect(dashboardHtml).toContain("if (ev && ev.refs && ev.refs.plan) return 'Squad';"); - expect(dashboardHtml).toContain("return 'Platform Engineer';"); - // Structured label spans: actor + verb fixed, plan filename ellipsises first; - // full context stays on the title tooltip. - expect(dashboardHtml).toContain("feed-seg-actor"); - expect(dashboardHtml).toContain("feed-seg-verb"); - expect(dashboardHtml).toContain("feed-seg-plan"); + expect(dashboardHtml).toContain("if (ev && ev.kind === 'delivery') return 'Eng';"); + expect(dashboardHtml).toContain("if (ev && ev.refs && ev.refs.plan) return 'SQ';"); + expect(dashboardHtml).toContain("return 'Eng';"); + // Display masks live in a map, never as returns out of crewEventActor. + expect(dashboardHtml).not.toContain("return 'Engineering Manager';"); + expect(dashboardHtml).not.toContain("return 'Platform Engineer';"); + expect(dashboardHtml).not.toContain("return 'Squad';"); + // The v1 segment row is gone: no equal-shrink spans, no separator spans. + expect(dashboardHtml).not.toContain("feed-seg-actor"); + expect(dashboardHtml).not.toContain("feed-seg-verb"); + expect(dashboardHtml).not.toContain("feed-seg-plan"); + expect(dashboardHtml).not.toContain("feed-seg-mid"); + expect(dashboardHtml).not.toContain('<span class="feed-sep" aria-hidden="true"> · </span>'); + // Label parsing and the truncation-safe plan ref survive the rewrite. expect(dashboardHtml).toContain(".split(' · ')"); expect(dashboardHtml).toContain("ev.labelFull || ev.label || ''"); + expect(dashboardHtml).toContain("ev.refs && ev.refs.plan"); + // Row order + structural pin: badge is a row sibling at row start. + expectBadgeIsRowSiblingBeforeActor(dashboardHtml); + // Timestamp fallback: first-seen stamp when the emitter omits `at`. + expect(dashboardHtml).toContain("semanticSeenAt.get(ev.id)"); + expect(dashboardHtml).toContain("${crewEventTime(ev, info)}"); + // Inside the Crew Monitor block, info.bg is interpolated exactly once: the + // team badge tint. It must not spread to a row background or a second chip. + // (The Activity tab's .activity-icon has its own, deliberate, tint.) + expect(crewMonitorFeedBlock(dashboardHtml).match(/\$\{info\.bg\}/g)?.length ?? 0).toBe(1); + }); + + it("splits the Eng collision into distinct display roles without a wire change", () => { + // Six consecutive `Eng · merged` rows were the worst case in the shipped + // feed. `kind` carries the split; no new emitted field is required, and the + // frozen label contract keeps the raw token in labelFull. + expect(dashboardHtml).toContain("function crewActorEngRole(ev)"); expect(dashboardHtml).toContain( - '<span class="feed-label" title="${escapeAttr(feedTitle)}">${feedLabelHtml}</span>', + "return ev && ev.kind === 'delivery' ? 'DevOps' : 'Tech Lead';", + ); + expect(dashboardHtml).toContain("function crewActorRole(ev, raw)"); + // Resolution order: per-event role -> Eng split -> mask -> raw token. + expect(dashboardHtml).toMatch( + /function crewActorRole\(ev, raw\) \{[\s\S]*?if \(ev && ev\.role\) return String\(ev\.role\);[\s\S]*?if \(token === 'Eng'\) return crewActorEngRole\(ev\);[\s\S]*?return CREW_ACTOR_MASK\[token\] \|\| token;/, + ); + // Closed-set and open-set masks, per the operator lexicon. + for (const [wire, role] of [ + ["'docs-repo'", "Tech Writer"], + ["explore", "Product Analyst"], + ["generalPurpose", "Analyst"], + ["SQ", "Scrum Master"], + ["Dev", "Developer"], + ["PO", "Product Owner"], + ["PM", "Project Manager"], + ] as const) { + expect(dashboardHtml).toMatch(new RegExp(`${wire}:\\s*'${role}'`)); + } + // Initials overrides: Developer and DevOps would otherwise collide on DE. + expect(dashboardHtml).toContain("{ Developer: 'DV', DevOps: 'DO' }"); + // Kind glosses follow the same lexicon (badge tooltip is the kind cue now). + expect(dashboardHtml).toMatch(/run_plan:.*gloss: 'Project Manager - live execution'/); + expect(dashboardHtml).toMatch(/handoff:.*gloss: 'Project Manager - awaiting gate'/); + expect(dashboardHtml).toMatch(/agent_step:.*gloss: 'Developer - task unit'/); + expect(dashboardHtml).toMatch(/plan_progress:.*gloss: 'Product Owner - milestone'/); + expect(dashboardHtml).toMatch(/subagent:.*gloss: 'Developer - subagent run'/); + // The short masks must not leak back into a rendered role. + expect(dashboardHtml).not.toMatch(/gloss: 'PM - /); + expect(dashboardHtml).not.toMatch(/gloss: 'Dev - /); + expect(dashboardHtml).not.toMatch(/gloss: 'PO - /); + }); + + it("holds Crew Monitor columns steady with one flexible field per row", () => { + // The layout contract that replaced the equal-shrink segment row: exactly + // one field may ellipsis. A second one reintroduces the fragment bug. + expect(dashboardHtml).toMatch( + /\.live-activity-feed \.monitor-row \.monitor-row-primary\s*\{[^}]*flex:\s*1 1 auto/, ); - expect(dashboardHtml).toContain('<span class="feed-sep" aria-hidden="true"> · </span>'); - expect(dashboardHtml).toContain("ev.refs && ev.refs.plan"); expect(dashboardHtml).toMatch( - /\.live-activity-feed \.monitor-row \.feed-seg-plan\s*\{[^}]*flex-shrink:\s*3/, + /\.live-activity-feed \.monitor-row \.monitor-row-primary\s*\{[^}]*text-overflow:\s*ellipsis/, ); + // Column stability: fixed bases, not min-width floors a long id can push past. expect(dashboardHtml).toMatch( - /\.live-activity-feed \.monitor-row \.feed-seg\s*\{[^}]*flex-shrink:\s*0/, + /\.live-activity-feed \.monitor-row \.monitor-row-badge\s*\{[^}]*flex:\s*0 0 18px/, ); - // Row order: avatar, chip, label, time. expect(dashboardHtml).toMatch( - /monitor-row-avatar[\s\S]*?monitor-row-chip[\s\S]*?feed-label[\s\S]*?feed-time/, + /\.live-activity-feed \.monitor-row \.monitor-row-actor\s*\{[^}]*flex:\s*0 0 9\.5em/, + ); + expect(dashboardHtml).toMatch( + /\.live-activity-feed \.monitor-row \.monitor-row-verb\s*\{[^}]*flex:\s*0 0 6\.5em/, + ); + expect(dashboardHtml).toMatch( + /\.live-activity-feed \.monitor-row \.monitor-row-time\s*\{[^}]*text-align:\s*right/, + ); + // Ref chips are fixed content: they may never ellipsis (half a SHA is worse + // than no SHA), so nowrap is the pin and text-overflow must stay off them. + expect(dashboardHtml).toMatch( + /\.live-activity-feed \.monitor-row \.monitor-row-ref\s*\{[^}]*white-space:\s*nowrap/, + ); + expect(dashboardHtml).not.toMatch( + /\.live-activity-feed \.monitor-row \.monitor-row-ref\s*\{[^}]*text-overflow/, + ); + }); + + it("compacts the Crew row by container width, dropping whole fields only", () => { + // Container queries, not viewport media queries: the Crew Monitor sits in a + // 2x2 cockpit grid and a narrow plugin shell, so the feed's own width is the + // one that matters. + expect(dashboardHtml).toMatch(/\.monitor-cq\s*\{[^}]*container-type:\s*inline-size/); + expect(dashboardHtml).toContain('class="live-activity-feed monitor-cq"'); + // The ladder, in order. Every step hides a field whole. + expect(dashboardHtml).toMatch( + /@container \(max-width: 720px\) \{[^}]*\.monitor-row-plan-name \{ display: none/, + ); + expect(dashboardHtml).toMatch( + /@container \(max-width: 560px\) \{[\s\S]*?\.monitor-row-ref\.ref-sha \{ display: none/, + ); + expect(dashboardHtml).toMatch( + /@container \(max-width: 450px\) \{[\s\S]*?\.monitor-row-verb em \{ display: none/, ); - // Timestamp fallback: first-seen stamp when the emitter omits `at`. - expect(dashboardHtml).toContain("semanticSeenAt.get(ev.id)"); - expect(dashboardHtml).toContain("${crewEventTime(ev, info)}"); - // Avatar styling: neutral square chip, not a state dot. expect(dashboardHtml).toMatch( - /\.live-activity-feed \.monitor-row \.monitor-row-avatar\s*\{[^}]*width:\s*18px/, + /@container \(max-width: 360px\) \{[\s\S]*?\.monitor-row-meta \{ display: none/, ); + // Delta A: the actor drops WHOLE at ≤560px. Narrowing its basis while it + // still ellipsises would render `Project Man…` — the exact fragment failure + // the redesign exists to remove, reintroduced by the compaction ladder. expect(dashboardHtml).toMatch( - /\.live-activity-feed \.monitor-row \.monitor-row-avatar\s*\{[^}]*background:\s*var\(--bg-card-hover\)/, + /@container \(max-width: 560px\) \{[\s\S]*?\.monitor-row-actor \{ display: none/, + ); + expect(dashboardHtml).not.toMatch(/\.monitor-row-actor \{ flex-basis: 6\.5em/); + expect(dashboardHtml).not.toMatch(/\.monitor-row-actor \{ flex-basis: 5\.5em/); + // Delta C: the plan chip is fixed content. A width cap plus ellipsis on it + // clipped real basenames at FULL width (271px of name against a 240px cap), + // which is the fragment bug reappearing on the widest layout. + expect(dashboardHtml).not.toMatch( + /\.live-activity-feed \.monitor-row \.monitor-row-plan-name\s*\{[^}]*max-width/, ); + expect(dashboardHtml).not.toMatch( + /\.live-activity-feed \.monitor-row \.monitor-row-plan-name\s*\{[^}]*text-overflow/, + ); + // The superseded viewport rule must not come back alongside the ladder. + expect(dashboardHtml).not.toMatch(/@media[\s\S]{0,400}\.monitor-row \.feed-seg-actor/); }); - it("keeps the Activity tab on the plain activity-label (no Crew feed spans)", () => { - // Contract: Crew Monitor structured spans stay scoped; Activity tab is plain. + it("offers compact and comfortable Crew Monitor density with a viewport auto-pick", () => { + // Root data attribute restored before first paint, like the skin preference. + expect(dashboardHtml).toContain("agent-kit:monitor-density"); + expect(dashboardHtml).toContain("data-monitor-density"); + expect(dashboardHtml).toContain("const MONITOR_DENSITIES = ['compact', 'comfortable']"); + expect(dashboardHtml).toContain("function autoMonitorDensity()"); + expect(dashboardHtml).toContain("function applyMonitorDensity(density, opts)"); + expect(dashboardHtml).toContain("function toggleMonitorDensity()"); + // Auto-pick threshold lives in one named constant, not a bare literal. + expect(dashboardHtml).toContain("const MONITOR_DENSITY_COMFORTABLE_MIN_WIDTH = 900"); + expect(dashboardHtml).toMatch( + /\(window\.innerWidth \|\| 0\) >= MONITOR_DENSITY_COMFORTABLE_MIN_WIDTH/, + ); + // Blocked storage must degrade, never throw (private mode). + expect(dashboardHtml).toMatch( + /function getStoredMonitorDensity\(\) \{[\s\S]*?catch \(e\) \{[\s\S]*?return null;/, + ); + // Operator control in the Crew Monitor header, with pressed state. + expect(dashboardHtml).toContain("data-monitor-density-toggle"); + expect(dashboardHtml).toContain('onclick="toggleMonitorDensity()"'); + expect(dashboardHtml).toMatch(/aria-pressed="\$\{currentMonitorDensity\(\)/); + // Comfortable is a second layout, not a taller first one: the row wraps and + // the primary field reflows to a clamped full-width second line. + expect(dashboardHtml).toMatch( + /html\[data-monitor-density="comfortable"\] \.live-activity-feed \.monitor-row\s*\{[^}]*flex-wrap:\s*wrap/, + ); + expect(dashboardHtml).toMatch( + /html\[data-monitor-density="comfortable"\] \.live-activity-feed \.monitor-row \.monitor-row-primary\s*\{[^}]*flex:\s*1 0 100%/, + ); + expect(dashboardHtml).toMatch( + /html\[data-monitor-density="comfortable"\] \.live-activity-feed \.monitor-row \.monitor-row-primary\s*\{[^}]*-webkit-line-clamp:\s*2/, + ); + expect(dashboardHtml).toMatch( + /html\[data-monitor-density="comfortable"\] \.live-activity-feed \.monitor-row \.monitor-row-primary\s*\{[^}]*white-space:\s*normal/, + ); + // Compact keeps the single-line ellipsis contract on the same field. + expect(dashboardHtml).toMatch( + /\.live-activity-feed \.monitor-row \.monitor-row-primary\s*\{[^}]*white-space:\s*nowrap/, + ); + // The title tooltip is required in BOTH modes: display label is still capped + // at MAX_SEMANTIC_LABEL upstream, so a second line is not a substitute for + // it. It rides on the primary span, because an actionable row's own title is + // already taken by the copy-path/copy-sha affordance and a duplicate title + // attribute on one element is silently dropped. + expect(dashboardHtml).toContain( + '<span class="monitor-row-primary" title="${escapeAttr(feedTitle)}">${escapeHtml(row.primary)}</span>', + ); + expect(dashboardHtml).toContain("const rowTitle = actionAttrs ? '' :"); + expect(dashboardHtml).toContain("ev.sourcePath ? `\\n${ev.sourcePath}` : ''"); + }); + + it("keeps the Activity tab on the plain activity-label (no Crew row columns)", () => { + // Contract: Crew Monitor row columns stay scoped; Activity tab is plain. expect(dashboardHtml).toContain('id="section-activity"'); expect(dashboardHtml).toContain('<span class="activity-label">${escapeHtml(ev.label)}</span>'); expect(dashboardHtml).not.toMatch( - /id="section-activity"[\s\S]*feed-seg-actor[\s\S]*<\/div>\s*<\/div>\s*`/, + /id="section-activity"[\s\S]*monitor-row-primary[\s\S]*<\/div>\s*<\/div>\s*`/, ); - // New Crew CSS must stay under .live-activity-feed .monitor-row, not .activity-label. - expect(dashboardHtml).toMatch(/\.live-activity-feed \.monitor-row \.feed-seg-plan/); - expect(dashboardHtml).not.toMatch(/\.activity-label[\s\S]{0,80}feed-seg/); + // Crew CSS must stay under .live-activity-feed .monitor-row, not .activity-label. + expect(dashboardHtml).toMatch(/\.live-activity-feed \.monitor-row \.monitor-row-plan/); + expect(dashboardHtml).not.toMatch(/\.activity-label[\s\S]{0,80}monitor-row-/); + // The Activity tab keeps the kind glyph the Crew row dropped, so semanticEventInfo + // must keep emitting icons even though no Crew row renders one. + expect(dashboardHtml).toMatch(/function activityEventInfo\(ev\)/); + expect(dashboardHtml).toMatch(/run_plan:\s+\{ icon: '\\u25b6'/); }); it("renders Flight Log Gaps stack (live + earlier) without Field Report review CTAs", () => { @@ -1011,8 +1247,13 @@ describe("plugin-ux-validation: narrow shell + a11y chrome", () => { expect(dashboardHtml).toContain("mark: '\\u2713'"); expect(dashboardHtml).toContain("mark: '\\u25CB'"); expect(dashboardHtml).toMatch(/\.now-status-completed\s*\{[^}]*var\(--green\)/); - // border-radius inherited from .now-status (--mc-radius-pill); no explicit idle override. - expect(dashboardHtml).toMatch(/\.now-status\s*\{[^}]*border-radius:\s*var\(--mc-radius-pill\)/); + // border-radius inherited from .now-status (--mc-radius-chrome, design v2); + // no explicit idle override. Idle's non-color cue is the ○ mark alone now + // that the radius no longer separates it from the solid chips. + expect(dashboardHtml).toMatch( + /\.now-status\s*\{[^}]*border-radius:\s*var\(--mc-radius-chrome\)/, + ); + expect(dashboardHtml).not.toMatch(/\.now-status-idle\s*\{[^}]*border-radius/); }); it("shimmers the current stepbar segment under prefers-reduced-motion no-preference", () => { @@ -1084,7 +1325,7 @@ describe("plugin-ux-validation: narrow shell + a11y chrome", () => { expect(dashboardHtml).toMatch(/\.mc-busy-chip\s*\{[^}]*color:\s*var\(--blue\)/); expect(dashboardHtml).toMatch(/\.mc-busy-chip\s*\{[^}]*background:\s*var\(--blue-bg\)/); expect(dashboardHtml).toMatch( - /\.mc-busy-chip\s*\{[^}]*border-radius:\s*var\(--mc-radius-pill\)/, + /\.mc-busy-chip\s*\{[^}]*border-radius:\s*var\(--mc-radius-chrome\)/, ); expect(dashboardHtml).toMatch( /\.mc-busy-chip\s*\{[^}]*font-size:\s*var\(--mc-chrome-meta-size\)/, @@ -1291,10 +1532,20 @@ describe("plugin-ux-validation: unified Activity feed", () => { }); it("keeps Monitor curated while Activity renders all event kinds", () => { - // Live-actions allowlist: tick/handoff/delivery + agent_step denser feed - expect(dashboardHtml).toMatch( - /const MONITOR_ACTIVITY_KINDS = new Set\(\[\s*'run_plan', 'handoff', 'delivery', 'agent_step',\s*\]\)/, - ); + // Live-actions allowlist: tick/handoff/delivery + agent_step denser feed, + // plus the 2026-08-05 real-time kinds (Task workers, background reviews). + expect(dashboardHtml).toMatch( + /const MONITOR_ACTIVITY_KINDS = new Set\(\[\s*'run_plan', 'handoff', 'delivery', 'agent_step', 'subagent', 'plan_review',\s*\]\)/, + ); + // HTML allowlist mirrors the semantic-model SoT exactly (order included). + expect(MONITOR_ACTIVITY_KINDS).toEqual([ + "run_plan", + "handoff", + "delivery", + "agent_step", + "subagent", + "plan_review", + ]); // Cap SoT is MONITOR_FEED_CAP via missionControl.monitorFeedCap (no HTML literal). expect(dashboardHtml).not.toMatch(/const MONITOR_FEED_CAP\s*=/); expect(dashboardHtml).toContain("function monitorFeedCap(d)"); @@ -1313,10 +1564,13 @@ describe("plugin-ux-validation: unified Activity feed", () => { expect(dashboardHtml).toContain("${filteredActivityEvents.map((ev, idx) => {"); }); - it("renders flat single-roll Monitor rows with distinct resting kind chips", () => { + it("renders flat single-roll Monitor rows with distinct resting kind colors", () => { expect(dashboardHtml).toContain("monitor-row"); - expect(dashboardHtml).toContain("monitor-row-chip"); - expect(dashboardHtml).toContain("monitor-row-icon"); + // The kind glyph and its chip wrapper are gone from the Crew row entirely + // (design v2): kind is carried by the badge tint + the badge tooltip gloss. + expect(dashboardHtml).not.toContain("monitor-row-chip"); + expect(dashboardHtml).not.toContain("monitor-row-icon"); + expect(dashboardHtml).toContain("monitor-row-badge"); expect(dashboardHtml).toContain("function semanticEventTime(ev, _info)"); expect(dashboardHtml).toMatch( /function semanticEventTime\(ev, _info\) \{[\s\S]*?if \(ev\.at\) return escapeHtml\(ev\.at\);\s*return '';/, @@ -1330,26 +1584,47 @@ describe("plugin-ux-validation: unified Activity feed", () => { expect(dashboardHtml).not.toContain("monitor-row-tag"); expect(dashboardHtml).not.toContain("monitor-row-identity"); expect(dashboardHtml).not.toContain("const showIdentity = !prevRaw || rawIdentity !== prevRaw"); - // Icon-only chip: fixed square, solid status tokens, no hover width expand - expect(dashboardHtml).not.toMatch( - /\.live-activity-feed \.monitor-row:hover \.monitor-row-chip/, - ); - expect(dashboardHtml).not.toMatch( - /\.live-activity-feed \.monitor-row:focus-within \.monitor-row-chip/, - ); + // Team badge: fixed 18px square, --mc-radius-sm so it never reads as a status + // pill (those squared off to --mc-radius-chrome in the same pass), tinted + // *-bg fill with the solid semantic color on the initials. expect(dashboardHtml).toMatch( - /\.live-activity-feed \.monitor-row \.monitor-row-chip\s*\{[^}]*min-width:\s*18px/, + /\.live-activity-feed \.monitor-row \.monitor-row-badge\s*\{[^}]*width:\s*18px/, ); expect(dashboardHtml).toMatch( - /\.live-activity-feed \.monitor-row \.monitor-row-chip\s*\{[^}]*width:\s*18px/, + /\.live-activity-feed \.monitor-row \.monitor-row-badge\s*\{[^}]*height:\s*18px/, ); - // Resting cue: solid *-bg fill only (no inset ring, no left rail, no kind-tag text) expect(dashboardHtml).toMatch( - /\.live-activity-feed \.monitor-row \.monitor-row-chip\s*\{[^}]*box-shadow:\s*none/, + /\.live-activity-feed \.monitor-row \.monitor-row-badge\s*\{[^}]*border-radius:\s*var\(--mc-radius-sm\)/, + ); + expect(dashboardHtml).toContain('style="background:${info.bg};color:${info.color}"'); + // No resting hover/focus width expand on the badge (the #631 avatar behaviour). + expect(dashboardHtml).not.toMatch( + /\.live-activity-feed \.monitor-row:hover \.monitor-row-badge/, ); expect(dashboardHtml).not.toMatch( - /\.live-activity-feed \.monitor-row \.monitor-row-chip\s*\{[^}]*box-shadow:\s*inset 0 0 0 1px currentColor/, + /\.live-activity-feed \.monitor-row:focus-within \.monitor-row-badge/, + ); + // Verb state icon replaced the 6px dot; stroke weight is optically + // compensated for the 12px render, on the dashboard's own 16 grid (the + // design's 24-grid Feather shapes were redrawn, not imported — the file + // keeps one icon geometry, pinned by the viewBox check on the refresh test). + expect(dashboardHtml).toContain("const CREW_VERB_ICON = {"); + expect(dashboardHtml).toContain( + 'viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5"', ); + expect(dashboardHtml).toMatch( + /\.live-activity-feed \.monitor-row \.monitor-row-verb-icon\s*\{[^}]*stroke-width:\s*1\.6/, + ); + for (const [verb, icon] of [ + ["running", "play"], + ["awaiting", "pause"], + ["done", "check"], + ["failed", "x"], + ["merged", "git-merge"], + ["parked", "circle-slash"], + ] as const) { + expect(dashboardHtml).toMatch(new RegExp(`${verb}:\\s*'${icon}'`)); + } expect(dashboardHtml).toMatch( /run_plan:\s+\{[^}]*color:\s*'var\(--green\)'[^}]*bg:\s*'var\(--green-bg\)'/, ); @@ -1385,18 +1660,28 @@ describe("plugin-ux-validation: unified Activity feed", () => { expect(dashboardHtml).toMatch(/chore:\s+\{\s*icon:\s*'\\u2692'/); expect(dashboardHtml).toMatch(/pr:\s+\{\s*icon:\s*'\\u2442'/); expect(dashboardHtml).toMatch(/ship:\s+\{\s*icon:\s*'\\u2708'/); - expect(dashboardHtml).toContain("gloss: 'DevOps Engineer - feat'"); - expect(dashboardHtml).toContain("gloss: 'Tech Lead - live execution'"); - expect(dashboardHtml).toContain("gloss: 'Scrum Master - awaiting gate'"); - expect(dashboardHtml).toContain("gloss: 'Full-Stack Developer - task unit'"); + // Glosses follow the operator lexicon (design v2): full roles, no short masks. + expect(dashboardHtml).toContain("gloss: 'DevOps - feat'"); + expect(dashboardHtml).toContain("gloss: 'Project Manager - live execution'"); + expect(dashboardHtml).toContain("gloss: 'Project Manager - awaiting gate'"); + expect(dashboardHtml).toContain("gloss: 'Developer - task unit'"); expect(dashboardHtml).toContain("gloss: 'Product Owner - milestone'"); - expect(dashboardHtml).toContain("gloss: 'DevOps Engineer - shipped unit'"); + expect(dashboardHtml).toContain("gloss: 'DevOps - merged unit'"); + // `shipped` is retired from the verb list and the gloss pack (2026-08-05). + expect(dashboardHtml).not.toContain("shipped unit"); + // New live kinds carry their own lexicon gloss. + expect(dashboardHtml).toContain("gloss: 'Developer - subagent run'"); + expect(dashboardHtml).toContain("gloss: 'QA - review evidence'"); expect(dashboardHtml).toContain("kindGloss: gloss"); - expect(dashboardHtml).toMatch( - /\.live-activity-feed \.monitor-row \.feed-time\s*\{[^}]*margin-left:\s*auto/, + // The time column is pushed right by the primary field's flex, not by an + // auto margin: with a wrapping comfortable row, margin-left:auto on the + // time would drag it onto the second line. + expect(dashboardHtml).not.toMatch( + /\.live-activity-feed \.monitor-row \.monitor-row-time\s*\{[^}]*margin-left:\s*auto/, ); - // Column order in markup: chip, then label, then trailing time - expect(dashboardHtml).toMatch(/monitor-row-chip[\s\S]*?feed-label[\s\S]*?feed-time/); + // Column order + structural pin (render block; not whole-file CSS proximity). + expectBadgeIsRowSiblingBeforeActor(dashboardHtml); + expect(dashboardHtml).toContain("const badgeHtml ="); // Hover affordance on flat rows (skin tokens only) expect(dashboardHtml).toMatch( /\.live-activity-feed \.monitor-row\[role="button"\]:hover\s*\{[^}]*background:\s*var\(--bg-card-hover\)/, @@ -3356,9 +3641,30 @@ describe("plugin-ux-validation: Healthcenter (More → Health)", () => { expect(dashboardHtml).toContain("HEALTH_CHECK_META"); expect(dashboardHtml).toContain("function toggleHealthCheck(checkId)"); expect(dashboardHtml).toContain("function healthCheckSeverity(check, aggregateStatus)"); + // Scope identity to the HEALTH_CHECK_META literal (whole-file `plans:` etc. is vacuous). + const metaBlock = dashboardHtml.match(/const HEALTH_CHECK_META = \{[\s\S]*?\n\};/)?.[0]; + expect(metaBlock).toBeTruthy(); for (const id of ["plans", "handoff", "agents", "commands", "memory", "git", "config"]) { - expect(dashboardHtml).toContain(`${id}:`); + expect(metaBlock).toMatch(new RegExp(`^\\s*${id}:\\s*\\{`, "m")); } + // Residual E/E1/E2: handoff/git remedy the fail; path-copy CTAs share Copy path; + // git Autofix covers zero-commit repos (init + empty commit). + expect(metaBlock).toMatch( + /handoff:\s*\{[\s\S]*?autofix:\s*\{\s*text:\s*'\/handoff'[\s\S]*?destination:\s*'chatInput'/, + ); + expect(metaBlock).toMatch( + /git:\s*\{[\s\S]*?autofix:\s*\{\s*text:\s*'git init && git commit --allow-empty -m "init"'[\s\S]*?destination:\s*'terminal'[\s\S]*?label:\s*'Autofix'/, + ); + expect(metaBlock).toMatch( + /memory:\s*\{[\s\S]*?autofix:\s*\{\s*text:\s*'\.cursor\/memory\/'[\s\S]*?label:\s*'Copy path'/, + ); + expect(metaBlock).toMatch( + /commands:\s*\{[\s\S]*?autofix:\s*\{\s*text:\s*'\.cursor\/commands\/'[\s\S]*?label:\s*'Copy path'/, + ); + // Hygiene F/G: native button needs no keydown shim; dead legacy helpers gone. + expect(dashboardHtml).not.toContain("healthCheckKeydown"); + expect(dashboardHtml).not.toContain("showHealthInfo"); + expect(dashboardHtml).not.toContain(".health-message"); }); it("renders a vitals diagnosis dashboard grouped by vital system", () => { @@ -3392,8 +3698,15 @@ describe("plugin-ux-validation: Healthcenter (More → Health)", () => { expect(dashboardHtml).toMatch( /\.health-item-sev\[data-sev="degraded"\]\s*\{[^}]*var\(--orange/, ); - expect(dashboardHtml).toMatch(/\.health-item-sev\[data-sev="error"\]\s*\{[^}]*var\(--red/); + // Per-check error chrome pruned (residual C): producers never emit error+checks. + expect(dashboardHtml).not.toMatch(/\.health-item-sev\[data-sev="error"\]/); expect(dashboardHtml).toContain('class="health-item-sev" data-sev='); + expect(dashboardHtml).toMatch( + /function healthCheckSeverity\(check, aggregateStatus\) \{[\s\S]*?if \(aggregateStatus === 'degraded'\) return 'degraded';\s*return 'warning';/, + ); + expect(dashboardHtml).not.toMatch( + /function healthCheckSeverity\(check, aggregateStatus\) \{[\s\S]*?aggregateStatus === 'error'[\s\S]*?return 'error'/, + ); // Token hygiene: no misleading fallback hexes on severity/radius rules. expect(dashboardHtml).not.toContain("var(--mc-radius-sm, 6px)"); expect(dashboardHtml).not.toContain("var(--green, #3fb950)"); @@ -3401,10 +3714,19 @@ describe("plugin-ux-validation: Healthcenter (More → Health)", () => { expect(dashboardHtml).not.toContain("var(--red, #f85149)"); }); - it("unifies severity chrome on one {tone, label, token} mapping across all call sites", () => { + it("unifies severity chrome on one {tone, label} mapping across all call sites", () => { expect(dashboardHtml).toContain("HEALTH_SEVERITY_CHROME"); - expect(dashboardHtml).toContain( - "degraded: { tone: 'orange', label: 'degraded', token: 'orange' }", + // Positive shape anchor: literal-scoped token pin must match this = { … }; form. + expect(dashboardHtml).toMatch(/const HEALTH_SEVERITY_CHROME = \{[\s\S]*?\};/); + expect(dashboardHtml).toContain("degraded: { tone: 'orange', label: 'degraded' }"); + // Aggregate error keeps tone+label for transport; no unread token field. + expect(dashboardHtml).toMatch(/error:\s*\{\s*tone:\s*'red',\s*label:\s*'error'\s*\}/); + expect(dashboardHtml).not.toMatch( + /HEALTH_SEVERITY_CHROME\s*=\s*\{(?:(?!\};)[\s\S])*\btoken\s*:(?:(?!\};)[\s\S])*\};/, + ); + // Fallback || { … }: reject token within that object only (not unbounded to EOF). + expect(dashboardHtml).not.toMatch( + /return HEALTH_SEVERITY_CHROME\[sev\] \|\| \{(?:(?!\})[\s\S])*\btoken\s*:/, ); // Presence dot, card dot, and severity label all read the same mapping. expect(dashboardHtml).toContain( diff --git a/packages/cli/src/dashboard/semantic-model.test.ts b/packages/cli/src/dashboard/semantic-model.test.ts index 24019a8..74ecce5 100644 --- a/packages/cli/src/dashboard/semantic-model.test.ts +++ b/packages/cli/src/dashboard/semantic-model.test.ts @@ -11,6 +11,9 @@ import { MONITOR_ACTIVITY_KINDS, MONITOR_AGENT_STEP_EMIT_CAP, MONITOR_FEED_CAP, + MONITOR_PLAN_REVIEW_EMIT_CAP, + MONITOR_SUBAGENT_EMIT_CAP, + SUBAGENT_TRANSCRIPT_FILE_RE, allowlistReadinessPending, briefActivityActor, buildAttentionItems, @@ -42,6 +45,8 @@ import { formatGitActivity, formatInventoryActivity, formatPlanHandoffActivity, + formatPlanReviewActivity, + formatSubagentActivity, isFieldReportAttentionId, listFlightLogQuietOpenTriages, listUnreviewedReviewTargets, @@ -61,6 +66,7 @@ import { parseQueueCursor, parseQueueOutcomes, parseRunQueue, + parseSubagentRun, planQueueRole, recordCadenceBatchComplete, recordCadenceTickClose, @@ -1139,7 +1145,7 @@ describe("formatDeliveryActivity", () => { commitType: "feat", }, }); - expect(events[0].label).toContain("docs-repo · shipped ·"); + expect(events[0].label).toContain("docs-repo · merged ·"); expect(events[0].label).toContain("feat(x): implement the thing"); expect(events[0].label).toContain("PR #42"); expect(events[0].label).toContain("bbb2222"); @@ -1161,7 +1167,9 @@ describe("formatDeliveryActivity", () => { expect(events[0].refs.commits).toHaveLength(1); expect(events[0].refs.plan).toBe("y.plan.md"); expect(events[0].refs.commitType).toBe("pr"); - expect(events[0].label).toMatch(/shipped · PR #10 · abc1234/); + expect(events[0].label).toMatch(/merged · PR #10 · abc1234/); + // `shipped` retired 2026-08-05: the row is a merge, not a prod promote. + expect(events[0].label).not.toContain("shipped"); expect(events[0].label).not.toContain("Merged PR"); expect(events[0].label).not.toContain("→"); }); @@ -1204,7 +1212,7 @@ describe("formatDeliveryActivity", () => { plan: "monitor-agent-activity-focus.plan.md", commitType: "feat", }); - expect(events[0].label).toContain("docs-repo · shipped ·"); + expect(events[0].label).toContain("docs-repo · merged ·"); expect(events[0].label).toContain("feat(dashboard): refocus Monitor hero on agent activity"); expect(events[0].label).toContain("PR #321 · 528901c"); expect(events[1].refs).toMatchObject({ @@ -1214,7 +1222,7 @@ describe("formatDeliveryActivity", () => { plan: null, commitType: "docs", }); - expect(events[1].label).toContain("Engineering Manager · shipped ·"); + expect(events[1].label).toContain("Eng · merged ·"); expect(events[1].label).toContain("docs: update plan-review-triage for multi-path walk"); expect(events[1].label).toContain("PR #320 · 74954c7"); expect(events[1].label).not.toMatch(/\(#320\)/); @@ -1278,9 +1286,9 @@ describe("formatDeliveryActivity", () => { ]); expect(events[1].refs.commits).toEqual(["528901c", "6051036"]); expect(events[0].label).not.toContain("field-report-owed-external-review"); - // Plan attribution stays on refs; label actor is Engineering Manager when agent is not a kit id. + // Plan attribution stays on refs; label actor is the Eng mask when agent is not a kit id. expect(events[1].refs.plan).toBe("monitor-agent-activity-focus.plan.md"); - expect(events[1].label).toContain("Engineering Manager · shipped ·"); + expect(events[1].label).toContain("Eng · merged ·"); }); it("honors a limit option like other activity producers", () => { @@ -1378,9 +1386,9 @@ describe("formatPlanHandoffActivity + mergeActivity", () => { expect(steps.map((e) => e.refs?.todo)).toEqual(["step-a", "step-b", "step-c"]); expect(steps.map((e) => e.refs?.phase)).toEqual(["done", "done", "running"]); // Natural voice: phase word first, then the todo id; no robotic "step" separator. - // "generalPurpose" is not a kit agent id, so the actor falls back to Squad. - expect(steps[0].label).toMatch(/^Squad · done · step-a/); - expect(steps[2].label).toMatch(/^Squad · running · step-c/); + // "generalPurpose" is not a kit agent id, so the actor falls back to the SQ mask. + expect(steps[0].label).toMatch(/^SQ · done · step-a/); + expect(steps[2].label).toMatch(/^SQ · running · step-c/); expect(steps.every((e) => MONITOR_ACTIVITY_KINDS.includes(e.kind))).toBe(true); expect(planEvents.some((e) => e.kind === "run_plan")).toBe(true); }); @@ -1411,7 +1419,7 @@ describe("formatPlanHandoffActivity + mergeActivity", () => { plans: samplePlans, }); const gate = planEvents.find((e) => e.kind === "handoff"); - expect(gate.label).toMatch(/^Squad · awaiting · next \S+/); + expect(gate.label).toMatch(/^SQ · awaiting · next \S+/); expect(gate.label).not.toContain("mission-control-plugin-ux.plan.md"); expect(gate.labelFull).toContain("mission-control-plugin-ux.plan.md"); expect(gate.labelFull.startsWith(gate.label)).toBe(true); @@ -1443,7 +1451,14 @@ describe("formatPlanHandoffActivity + mergeActivity", () => { expect(progress[0].label).toMatch(/^tech-lead · done ·/); expect(progress[0].label).toContain("2/2"); // Monitor hero: live actions + denser agent_step; milestones stay on Activity - expect(MONITOR_ACTIVITY_KINDS).toEqual(["run_plan", "handoff", "delivery", "agent_step"]); + expect(MONITOR_ACTIVITY_KINDS).toEqual([ + "run_plan", + "handoff", + "delivery", + "agent_step", + "subagent", + "plan_review", + ]); expect(MONITOR_ACTIVITY_KINDS).not.toContain("plan_progress"); expect(MONITOR_FEED_CAP).toBe(20); expect(MONITOR_AGENT_STEP_EMIT_CAP).toBe(12); @@ -1472,17 +1487,21 @@ describe("formatPlanHandoffActivity + mergeActivity", () => { }); describe("briefActivityActor", () => { - it("prefers kit agent, then delivery Engineering Manager, then Squad, then Platform Engineer", () => { + it("prefers kit agent, then the Eng delivery mask, then SQ, then Eng", () => { expect(briefActivityActor("docs-repo", { kind: "run_plan" })).toBe("docs-repo"); - expect(briefActivityActor(null, { kind: "delivery", plan: "x.plan.md" })).toBe( - "Engineering Manager", - ); + expect(briefActivityActor(null, { kind: "delivery", plan: "x.plan.md" })).toBe("Eng"); // Lexicon fallback: never the full plan filename in the actor slot. - expect(briefActivityActor(null, { kind: "run_plan", plan: "x.plan.md" })).toBe("Squad"); - expect(briefActivityActor(null, { kind: "agent_step", plan: "x.plan.md" })).toBe("Squad"); - expect(briefActivityActor(null, { kind: "handoff", plan: "x.plan.md" })).toBe("Squad"); - expect(briefActivityActor(null, { kind: "plan_progress", plan: "x.plan.md" })).toBe("Squad"); - expect(briefActivityActor(null, { kind: "handoff" })).toBe("Platform Engineer"); + expect(briefActivityActor(null, { kind: "run_plan", plan: "x.plan.md" })).toBe("SQ"); + expect(briefActivityActor(null, { kind: "agent_step", plan: "x.plan.md" })).toBe("SQ"); + expect(briefActivityActor(null, { kind: "handoff", plan: "x.plan.md" })).toBe("SQ"); + expect(briefActivityActor(null, { kind: "plan_progress", plan: "x.plan.md" })).toBe("SQ"); + expect(briefActivityActor(null, { kind: "handoff" })).toBe("Eng"); + // Short operator-lexicon masks only; the 2026-08-01 long forms are retired. + for (const kind of ["delivery", "run_plan", "handoff"]) { + const actor = briefActivityActor(null, { kind, plan: "x.plan.md" }); + expect(actor.length).toBeLessThanOrEqual(9); + expect(["Engineering Manager", "Platform Engineer", "Squad"]).not.toContain(actor); + } }); }); @@ -2990,3 +3009,262 @@ describe("describeProcess", () => { expect(long.length).toBeLessThanOrEqual(161); }); }); + +describe("parseSubagentRun", () => { + const dispatchPrompt = (extra = "") => + JSON.stringify({ + role: "user", + message: { + content: [ + { + type: "text", + text: [ + "You are an Agent Kit worker. Execute ONLY the to-do below and stop.", + "", + "Repo: /tmp/repo", + "To-do id: phase2-subagent-realtime-rows", + "worker_type / subagent_type: docs-repo", + extra, + ].join("\n"), + }, + ], + }, + }); + + it("reads the phase from the terminal record, not from the body", () => { + const running = parseSubagentRun({ + id: "abcd1234-0000", + firstLine: dispatchPrompt(), + lastLine: JSON.stringify({ role: "assistant", message: { content: [] } }), + }); + expect(running?.phase).toBe("running"); + + const done = parseSubagentRun({ + id: "abcd1234-0000", + firstLine: dispatchPrompt(), + lastLine: JSON.stringify({ type: "turn_ended", status: "success" }), + }); + expect(done?.phase).toBe("done"); + + const failed = parseSubagentRun({ + id: "abcd1234-0000", + firstLine: dispatchPrompt(), + lastLine: JSON.stringify({ type: "turn_ended", status: "error" }), + }); + expect(failed?.phase).toBe("failed"); + }); + + it("lifts the to-do id and subagent_type out of the dispatch prompt", () => { + const run = parseSubagentRun({ + id: "abcd1234-5678", + parentId: "parent-chat", + firstLine: dispatchPrompt(), + lastLine: JSON.stringify({ type: "turn_ended", status: "success" }), + modifiedAt: "2026-08-05T06:00:00.000Z", + }); + expect(run).toMatchObject({ + id: "abcd1234-5678", + parentId: "parent-chat", + phase: "done", + todoId: "phase2-subagent-realtime-rows", + workerType: "docs-repo", + modifiedAt: "2026-08-05T06:00:00.000Z", + }); + }); + + it("rejects unfilled template placeholders instead of rendering them", () => { + const raw = JSON.stringify({ + role: "user", + message: { + content: [{ type: "text", text: "To-do id: <id>\nsubagent_type: none\n" }], + }, + }); + const run = parseSubagentRun({ id: "x1", firstLine: raw, lastLine: raw }); + expect(run?.todoId).toBeNull(); + expect(run?.workerType).toBeNull(); + }); + + it("strips markdown emphasis around bulleted prompt fields", () => { + // Real orchestrator prompts bullet the fields: `- **worker_type:** explore`. + const raw = JSON.stringify({ + role: "user", + message: { + content: [ + { + type: "text", + text: "- **To-do id:** phase4-validate-redirects\n- **worker_type:** explore\n", + }, + ], + }, + }); + const run = parseSubagentRun({ id: "x1", firstLine: raw, lastLine: raw }); + expect(run?.todoId).toBe("phase4-validate-redirects"); + expect(run?.workerType).toBe("explore"); + }); + + it("accepts worker_type alone, subagent_type alone, and the combined template form", () => { + const promptWith = (line: string) => + JSON.stringify({ role: "user", message: { content: [{ type: "text", text: line }] } }); + for (const line of [ + "worker_type: docs-repo", + "subagent_type: docs-repo", + "worker_type / subagent_type: docs-repo", + ]) { + const parsed = parseSubagentRun({ id: "x1", firstLine: promptWith(line), lastLine: "{}" }); + expect(parsed?.workerType).toBe("docs-repo"); + } + }); + + it("returns null for an unusable transcript rather than a phantom running row", () => { + expect(parseSubagentRun({ id: "", firstLine: "{}", lastLine: "{}" })).toBeNull(); + expect( + parseSubagentRun({ id: "x1", firstLine: "not json", lastLine: "also not json" }), + ).toBeNull(); + }); + + it("matches only uuid-shaped transcript filenames", () => { + expect(SUBAGENT_TRANSCRIPT_FILE_RE.test("14d193b2-4327-48b0-8fc6-78062730ecad.jsonl")).toBe( + true, + ); + expect(SUBAGENT_TRANSCRIPT_FILE_RE.test("notes.md")).toBe(false); + expect(SUBAGENT_TRANSCRIPT_FILE_RE.test("summary.jsonl")).toBe(false); + }); +}); + +describe("formatSubagentActivity", () => { + const run = (over = {}) => ({ + id: "14d193b2-4327-48b0-8fc6-78062730ecad", + parentId: "parent-1", + phase: "running", + todoId: "phase1-remove-avatar-box", + workerType: "docs-repo", + modifiedAt: "2026-08-05T06:00:00.000Z", + ...over, + }); + + it("emits a live row per run with the phase as the verb", () => { + const [ev] = formatSubagentActivity([run()]); + expect(ev.kind).toBe("subagent"); + expect(ev.label).toBe("docs-repo · running · phase1-remove-avatar-box · 14d193b2"); + expect(ev.agent).toBe("docs-repo"); + expect(ev.at).toBe("2026-08-05T06:00:00.000Z"); + expect(ev.refs).toMatchObject({ phase: "running", parent: "parent-1" }); + // Transcripts live under $HOME; no repo-relative path may leak into the payload. + expect(ev.sourcePath).toBeNull(); + expect(MONITOR_ACTIVITY_KINDS).toContain(ev.kind); + }); + + it("falls back to the Dev mask and a generic subject when the prompt named neither", () => { + const [ev] = formatSubagentActivity([run({ workerType: null, todoId: null, phase: "failed" })]); + expect(ev.label).toBe("Dev · failed · task · 14d193b2"); + expect(ev.agent).toBeNull(); + }); + + it("shows a non-kit worker type as the actor but never as the attribution agent", () => { + // `explore` is a real dispatched worker identity; it is not a + // `.cursor/agents/` id, so it must not leak into `agent`. + const [ev] = formatSubagentActivity([run({ workerType: "explore" })]); + expect(ev.label).toBe("explore · running · phase1-remove-avatar-box · 14d193b2"); + expect(ev.agent).toBeNull(); + }); + + it("keeps distinct ids per phase so a run does not collapse with its own start", () => { + const [start] = formatSubagentActivity([run({ phase: "running" })]); + const [end] = formatSubagentActivity([run({ phase: "done" })]); + expect(start.id).not.toBe(end.id); + }); + + it("honors the emit cap and skips unusable rows", () => { + const many = Array.from({ length: 20 }, (_, i) => run({ id: `id-${i}` })); + expect(formatSubagentActivity(many)).toHaveLength(MONITOR_SUBAGENT_EMIT_CAP); + expect(formatSubagentActivity([null, { phase: "done" }])).toEqual([]); + expect(formatSubagentActivity(undefined)).toEqual([]); + }); +}); + +describe("formatPlanReviewActivity", () => { + const report = (over = {}) => ({ + file: "plan-monitor-some-plan.md", + path: ".cursor/memory/plan-monitor-some-plan.md", + slug: "some-plan", + reviewedPlanFile: "some-plan.plan.md", + triageNoteInReport: false, + findingsSummary: null, + hasOpenReviewGaps: true, + modifiedAt: "2026-08-05T05:00:00.000Z", + ...over, + }); + + it("surfaces an untriaged monitor as awaiting, with a copyable repo path", () => { + const [ev] = formatPlanReviewActivity([report()], []); + expect(ev.kind).toBe("plan_review"); + expect(ev.label).toBe("QA · awaiting · review · some-plan.plan.md"); + expect(ev.sourcePath).toBe(".cursor/memory/plan-monitor-some-plan.md"); + expect(ev.refs).toMatchObject({ report: "plan-monitor-some-plan.md", triaged: false }); + expect(MONITOR_ACTIVITY_KINDS).toContain(ev.kind); + }); + + it("flips to done once the report carries a triage heading", () => { + const [ev] = formatPlanReviewActivity([report({ triageNoteInReport: true })], []); + expect(ev.label).toBe("QA · done · review · some-plan.plan.md"); + expect(ev.refs.triaged).toBe(true); + expect(ev.id).toContain("triaged"); + }); + + it("falls back to the report slug when the report names no reviewed plan", () => { + const [ev] = formatPlanReviewActivity([report({ reviewedPlanFile: null })], []); + expect(ev.label).toContain("some-plan.plan.md"); + }); + + it("orders newest first and honors the emit cap", () => { + const reports = [ + report({ file: "plan-monitor-a.md", slug: "a", modifiedAt: "2026-08-01T00:00:00.000Z" }), + report({ file: "plan-monitor-b.md", slug: "b", modifiedAt: "2026-08-04T00:00:00.000Z" }), + ]; + const events = formatPlanReviewActivity(reports, []); + expect(events[0].refs.report).toBe("plan-monitor-b.md"); + const many = Array.from({ length: 12 }, (_, i) => + report({ file: `plan-monitor-${i}.md`, slug: String(i) }), + ); + expect(formatPlanReviewActivity(many, [])).toHaveLength(MONITOR_PLAN_REVIEW_EMIT_CAP); + expect(formatPlanReviewActivity([], [])).toEqual([]); + }); +}); + +describe("buildMissionControlView: real-time subagent + review rows", () => { + it("merges both new kinds into the activity stream", () => { + const view = buildMissionControlView({ + plans: [], + handoff: null, + subagentRuns: [ + { + id: "aaaa1111-2222", + parentId: "p1", + phase: "running", + todoId: "some-todo", + workerType: null, + modifiedAt: "2026-08-05T06:00:00.000Z", + }, + ], + externalReports: [ + { + file: "plan-monitor-x.md", + path: ".cursor/memory/plan-monitor-x.md", + slug: "x", + reviewedPlanFile: "x.plan.md", + triageNoteInReport: false, + modifiedAt: "2026-08-05T05:00:00.000Z", + }, + ], + }); + const kinds = view.activity.map((e) => e.kind); + expect(kinds).toContain("subagent"); + expect(kinds).toContain("plan_review"); + }); + + it("stays empty (never throws) when neither source is present", () => { + const view = buildMissionControlView({ plans: [], handoff: null }); + expect(view.activity.some((e) => e.kind === "subagent")).toBe(false); + expect(view.activity.some((e) => e.kind === "plan_review")).toBe(false); + }); +}); diff --git a/packages/cli/src/generator/personalization.ts b/packages/cli/src/generator/personalization.ts index e69549d..9c9ee6f 100644 --- a/packages/cli/src/generator/personalization.ts +++ b/packages/cli/src/generator/personalization.ts @@ -9,13 +9,17 @@ import { packMemberTargets, } from "../registry/install.js"; import type { RegistryIndex } from "../registry/types.js"; +import { detectIde } from "../scanner/detect-ide.js"; import type { DetectionEvidence, + GitDetection, + ProjectProfile, ReadinessReport, RepositoryProfile, RepositoryPurpose, } from "../types.js"; import { ensureDir, fileExists, writeJson } from "../utils/fs.js"; +import { generateVSCodeArtifacts } from "./vscode.js"; export const PERSONALIZATION_CONTRACT_VERSION = 1 as const; @@ -212,6 +216,16 @@ function renderProjectContext(profile: RepositoryProfile): string { if (profile.infra.ci !== "none" && profile.infra.ciFiles.length > 0) { sections.push(`- CI: ${profile.infra.ci}.`); } + sections.push( + "", + "## Relevant skills", + "", + "Installed and project-owned skills for this repository. Prefer `.cursor/agent-kit.json` `skills[]` as the install index; project-only domain skills may live under `.cursor/skills/domain/`.", + "", + "| Skill / path | Role | Evidence |", + "|--------------|------|----------|", + "| (none yet) | Add rows when `/agent-kit-onboard` scaffolds domain skills or personalization installs packs | — |", + ); if (profile.context.sources.length > 0) { sections.push("", "## Sources", ...profile.context.sources.map((item) => `- ${item.value}`)); } @@ -342,6 +356,50 @@ export async function applyPersonalization(input: { protectedPaths.add(CONTEXT_PATH); protectedPaths.add(AGENTS_PATH); + const ideDetection = await detectIde(input.rootDir); + if (ideDetection.ide === "vscode" || ideDetection.ide === "other") { + const git: GitDetection = { + providerKind: input.profile.git.providerKind ?? "unknown", + providerConfidence: input.profile.git.providerConfidence ?? "low", + providerEvidence: input.profile.git.providerEvidence ?? [], + remotes: input.profile.git.remotes ?? [], + mode: input.profile.git.mode ?? "none", + workflow: input.profile.git.workflow ?? "unknown", + isDirty: input.profile.git.isDirty ?? false, + hasLocalStaging: input.profile.git.hasLocalStaging ?? false, + hasRemoteStaging: input.profile.git.hasRemoteStaging ?? false, + provider: input.profile.git.provider, + remoteUrl: input.profile.git.remoteUrl, + remoteName: input.profile.git.remoteName, + currentBranch: input.profile.git.currentBranch, + defaultBranch: input.profile.git.defaultBranch, + }; + const projectProfile: ProjectProfile = { + rootDir: input.rootDir, + stack: input.profile.stack, + git, + ide: ideDetection, + infra: input.profile.infra, + services: input.profile.services, + installHooks: false, + selectedCoreComponents: [], + }; + const vscodeResults = await generateVSCodeArtifacts(projectProfile); + const ideEvidence: DetectionEvidence[] = [ + { source: "derived", value: `ide:${ideDetection.ide}` }, + ]; + for (const artifact of vscodeResults) { + protectedPaths.add(artifact.relativePath); + componentResults.push({ + kind: "file", + id: artifact.relativePath, + path: artifact.relativePath, + status: artifact.status, + evidence: ideEvidence, + }); + } + } + const result: PersonalizationResult = { contractVersion: PERSONALIZATION_CONTRACT_VERSION, generatorVersion: input.generatorVersion, diff --git a/packages/cli/src/generator/vscode.test.ts b/packages/cli/src/generator/vscode.test.ts new file mode 100644 index 0000000..554611f --- /dev/null +++ b/packages/cli/src/generator/vscode.test.ts @@ -0,0 +1,134 @@ +import { mkdir, mkdtemp, readFile, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import path from "node:path"; +import { afterEach, beforeEach, describe, expect, it } from "vitest"; +import type { ProjectProfile } from "../types.js"; +import { fileExists } from "../utils/fs.js"; +import { generateVSCodeArtifacts } from "./vscode.js"; + +async function createTempDir(): Promise<string> { + return mkdtemp(path.join(tmpdir(), "ak-vscode-")); +} + +function minimalProfile( + rootDir: string, + idePlan: "vscode-pro" | "default" = "vscode-pro", +): ProjectProfile { + return { + rootDir, + stack: { + language: "node", + workspaces: false, + testCommands: [], + validationCommands: [], + hasProjectFiles: true, + }, + git: { + providerKind: "unknown", + providerConfidence: "low", + providerEvidence: [], + remotes: [], + mode: "none", + workflow: "unknown", + isDirty: false, + hasLocalStaging: false, + hasRemoteStaging: false, + }, + ide: { ide: "vscode", plan: idePlan }, + infra: { + docker: false, + kubernetes: false, + ci: "none", + ciFiles: [], + infrastructureFiles: [], + deploymentFiles: [], + }, + services: {}, + installHooks: false, + selectedCoreComponents: [], + }; +} + +describe("generateVSCodeArtifacts", () => { + let root: string; + + beforeEach(async () => { + root = await createTempDir(); + }); + + afterEach(async () => { + // Temp directories are left for the OS to clean; no teardown needed. + }); + + it("writes VS Code and Copilot instructions on first run", async () => { + const profile = minimalProfile(root); + const results = await generateVSCodeArtifacts(profile); + + expect(results).toEqual( + expect.arrayContaining([ + { relativePath: ".vscode/settings.json", status: "applied" }, + { relativePath: ".github/copilot-instructions.md", status: "applied" }, + { relativePath: ".vscode/security-review.agent.md", status: "applied" }, + ]), + ); + + const settings = await readFile(path.join(root, ".vscode", "settings.json"), "utf8"); + expect(settings).toContain("editor.formatOnSave"); + + const copilot = await readFile(path.join(root, ".github", "copilot-instructions.md"), "utf8"); + expect(copilot).toContain("Conventional Commits"); + + const security = await readFile(path.join(root, ".vscode", "security-review.agent.md"), "utf8"); + expect(security).toContain("Security Review Agent"); + }); + + it("skips existing consumer files without overwriting them", async () => { + const profile = minimalProfile(root); + await mkdir(path.join(root, ".vscode"), { recursive: true }); + await mkdir(path.join(root, ".github"), { recursive: true }); + await writeFile(path.join(root, ".vscode", "settings.json"), '{"existing": true}\n', "utf8"); + await writeFile(path.join(root, ".github", "copilot-instructions.md"), "# Existing\n", "utf8"); + + const results = await generateVSCodeArtifacts(profile); + + expect(results).toContainEqual({ + relativePath: ".vscode/settings.json", + status: "skipped-customized", + }); + expect(results).toContainEqual({ + relativePath: ".github/copilot-instructions.md", + status: "skipped-customized", + }); + + const settings = await readFile(path.join(root, ".vscode", "settings.json"), "utf8"); + expect(settings).toContain("existing"); + expect(settings).not.toContain("formatOnSave"); + + const copilot = await readFile(path.join(root, ".github", "copilot-instructions.md"), "utf8"); + expect(copilot).toContain("Existing"); + expect(copilot).not.toContain("Conventional Commits"); + }); + + it("writes the security-review artifact only for the vscode-pro plan", async () => { + const freeProfile = minimalProfile(root, "default"); + const results = await generateVSCodeArtifacts(freeProfile); + + expect(results).not.toContainEqual( + expect.objectContaining({ relativePath: ".vscode/security-review.agent.md" }), + ); + expect(await fileExists(path.join(root, ".vscode", "security-review.agent.md"))).toBe(false); + }); + + it("returns an empty result for non-vscode-pro plans and skips the security file", async () => { + const freeProfile = minimalProfile(root, "default"); + const results = await generateVSCodeArtifacts(freeProfile); + + expect(results).toEqual( + expect.arrayContaining([ + { relativePath: ".vscode/settings.json", status: "applied" }, + { relativePath: ".github/copilot-instructions.md", status: "applied" }, + ]), + ); + expect(results.length).toBe(2); + }); +}); diff --git a/packages/cli/src/generator/vscode.ts b/packages/cli/src/generator/vscode.ts index 58b3b8e..ab05710 100644 --- a/packages/cli/src/generator/vscode.ts +++ b/packages/cli/src/generator/vscode.ts @@ -1,50 +1,81 @@ import { writeFile } from "node:fs/promises"; import path from "node:path"; import type { ProjectProfile } from "../types.js"; -import { ensureDir } from "../utils/fs.js"; +import { ensureDir, fileExists } from "../utils/fs.js"; import { gitProviderLabel, prTerminology } from "./platform.js"; -export async function generateVSCodeArtifacts(profile: ProjectProfile): Promise<void> { +export interface VSCodeArtifactResult { + relativePath: string; + status: "applied" | "skipped-customized"; +} + +export async function generateVSCodeArtifacts( + profile: ProjectProfile, +): Promise<VSCodeArtifactResult[]> { + const results: VSCodeArtifactResult[] = []; const vscodeDir = path.join(profile.rootDir, ".vscode"); const githubDir = path.join(profile.rootDir, ".github"); await Promise.all([ensureDir(vscodeDir), ensureDir(githubDir)]); - await writeFile( - path.join(vscodeDir, "settings.json"), - `${JSON.stringify( - { - "editor.formatOnSave": true, - "editor.codeActionsOnSave": { - "source.fixAll": "explicit", + const settingsPath = path.join(vscodeDir, "settings.json"); + if (await fileExists(settingsPath)) { + results.push({ relativePath: ".vscode/settings.json", status: "skipped-customized" }); + } else { + await writeFile( + settingsPath, + `${JSON.stringify( + { + "editor.formatOnSave": true, + "editor.codeActionsOnSave": { + "source.fixAll": "explicit", + }, + "files.autoSave": "afterDelay", }, - "files.autoSave": "afterDelay", - }, - null, - 2, - )}\n`, - "utf8", - ); + null, + 2, + )}\n`, + "utf8", + ); + results.push({ relativePath: ".vscode/settings.json", status: "applied" }); + } const provider = gitProviderLabel(profile); const prTerm = prTerminology(profile); - await writeFile( - path.join(githubDir, "copilot-instructions.md"), - `# Copilot Instructions + const copilotPath = path.join(githubDir, "copilot-instructions.md"); + if (await fileExists(copilotPath)) { + results.push({ relativePath: ".github/copilot-instructions.md", status: "skipped-customized" }); + } else { + await writeFile( + copilotPath, + `# Copilot Instructions - Keep code changes small and testable. - Use Conventional Commits (feat:, fix:, docs:, etc.). - Prefer security-safe defaults. - Git platform: ${provider}. Always create a ${prTerm} for review. `, - "utf8", - ); - - if (profile.ide.plan === "vscode-pro") { - await writeFile( - path.join(vscodeDir, "security-review.agent.md"), - "# Security Review Agent\n\nSpecialized mode for security review.\n", "utf8", ); + results.push({ relativePath: ".github/copilot-instructions.md", status: "applied" }); } + + if (profile.ide.plan === "vscode-pro") { + const securityPath = path.join(vscodeDir, "security-review.agent.md"); + if (await fileExists(securityPath)) { + results.push({ + relativePath: ".vscode/security-review.agent.md", + status: "skipped-customized", + }); + } else { + await writeFile( + securityPath, + "# Security Review Agent\n\nSpecialized mode for security review.\n", + "utf8", + ); + results.push({ relativePath: ".vscode/security-review.agent.md", status: "applied" }); + } + } + + return results; } diff --git a/packages/cli/src/hooks/session-start.test.ts b/packages/cli/src/hooks/session-start.test.ts index 279158d..8fc5275 100644 --- a/packages/cli/src/hooks/session-start.test.ts +++ b/packages/cli/src/hooks/session-start.test.ts @@ -23,6 +23,44 @@ describe("parseUnprocessedDogfoodItems", () => { const text = "### Unprocessed Files\n\n- `a.md`\n- `b.md`\n\n### Processed Files\n"; expect(parseUnprocessedDogfoodItems(text)).toEqual(["`a.md`", "`b.md`"]); }); + + it("accepts consumer ## Unprocessed Files and stops at ## Processed", () => { + const text = + "# Dogfood Inbox\n\n## Unprocessed Files\n- `note.md` — pending\n\n## Processed Files\n- `done.md`\n"; + expect(parseUnprocessedDogfoodItems(text)).toEqual(["`note.md` — pending"]); + }); + + it("stops ### Unprocessed at a higher ## Processed heading", () => { + const text = "### Unprocessed Files\n- `a.md`\n\n## Processed Files\n- `b.md`\n"; + expect(parseUnprocessedDogfoodItems(text)).toEqual(["`a.md`"]); + }); + + it("stops ## Unprocessed at a deeper ### Processed heading (no leak)", () => { + const text = "## Unprocessed Files\n- `a.md`\n\n### Processed Files\n- `done.md`\n"; + expect(parseUnprocessedDogfoodItems(text)).toEqual(["`a.md`"]); + }); + + it("collects markdown table rows (first cell)", () => { + const text = [ + "## Unprocessed Files", + "", + "| Note | Status |", + "|------|--------|", + "| `issue-37.md` | pending |", + "| `other.md` | pending |", + "", + "## Processed Files", + "| `done.md` | done |", + "", + ].join("\n"); + expect(parseUnprocessedDogfoodItems(text)).toEqual(["`issue-37.md`", "`other.md`"]); + }); + + it("collects numbered list items", () => { + const text = + "### Unprocessed Files\n1. `first.md`\n2) `second.md`\n\n### Processed Files\n3. `done.md`\n"; + expect(parseUnprocessedDogfoodItems(text)).toEqual(["`first.md`", "`second.md`"]); + }); }); describe("buildPreCompactUserMessage", () => { diff --git a/packages/cli/src/hooks/session-start.ts b/packages/cli/src/hooks/session-start.ts index 96960c2..0045f72 100644 --- a/packages/cli/src/hooks/session-start.ts +++ b/packages/cli/src/hooks/session-start.ts @@ -45,26 +45,69 @@ async function fileExists(p: string): Promise<boolean> { } } +/** Match factory (`###`) and consumer (`##`) Unprocessed headings. */ export function parseUnprocessedDogfoodItems(readmeText: string): string[] { const items: string[] = []; let inSection = false; + let sectionLevel = 0; for (const line of readmeText.split(/\r?\n/)) { - if (line.startsWith("### Unprocessed Files")) { + const unprocessedMatch = /^(#{2,3})\s+Unprocessed Files\b/.exec(line); + if (unprocessedMatch) { + const hashes = unprocessedMatch[1]; + if (!hashes) continue; inSection = true; + sectionLevel = hashes.length; continue; } if (!inSection) continue; - if (line.startsWith("### ")) break; - const stripped = line.trim(); - if (!stripped.startsWith("- ")) continue; - const body = stripped.slice(2).trim(); - const normalized = body.toLowerCase().replace(/[*_]/g, "").trim(); - if (!normalized || NONE_PLACEHOLDERS.has(normalized)) continue; + const headingMatch = /^(#{1,6})\s+/.exec(line); + if (headingMatch) { + // Any Processed Files heading ends the section (mixed H2/H3 must not leak). + if (/\bProcessed Files\b/.test(line)) break; + const hashes = headingMatch[1]; + if (hashes && hashes.length <= sectionLevel) break; + continue; + } + const body = extractUnprocessedDogfoodLine(line); + if (!body) continue; items.push(body); } return items; } +/** Bullets, numbered lists, and markdown table rows (first cell). */ +function extractUnprocessedDogfoodLine(line: string): string | null { + const stripped = line.trim(); + if (!stripped) return null; + + let raw: string | null = null; + if (stripped.startsWith("- ")) { + raw = stripped.slice(2).trim(); + } else { + const numbered = /^(\d+)[.)]\s+(.+)$/.exec(stripped); + if (numbered?.[2]) { + raw = numbered[2].trim(); + } else if (stripped.startsWith("|")) { + const parts = stripped + .replace(/^\|/, "") + .replace(/\|$/, "") + .split("|") + .map((cell) => cell.trim()); + if (parts.length === 0) return null; + if (parts.every((cell) => /^:?-+:?$/.test(cell))) return null; + const first = parts[0] ?? ""; + const headerish = first.toLowerCase().replace(/[*_`]/g, "").trim(); + if (/^(note|file|entrada|title|name|item|path)$/.test(headerish)) return null; + raw = first.trim(); + } + } + + if (!raw) return null; + const normalized = raw.toLowerCase().replace(/[*_]/g, "").trim(); + if (!normalized || NONE_PLACEHOLDERS.has(normalized)) return null; + return raw; +} + async function l0Present(root: string): Promise<boolean> { const cursor = path.join(root, ".cursor"); return ( diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index bbd2215..3db8050 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -1,5 +1,5 @@ #!/usr/bin/env node -import { defineCommand, runMain } from "citty"; +import { defineCommand, runMain, showUsage } from "citty"; import { addCommand } from "./commands/add.js"; import { contributeCommand } from "./commands/contribute.js"; import { cursorAwarenessCommand } from "./commands/cursor-awareness.js"; @@ -19,11 +19,13 @@ import { statusCommand } from "./commands/status.js"; import { updateCommand } from "./commands/update.js"; import { validateCommand } from "./commands/validate.js"; import { KIT_VERSION } from "./lifecycle/version.js"; +import { renderGroupedRootHelp } from "./welcome/help-groups.js"; +import { hasCliSubcommand, printWelcomeScreen } from "./welcome/screen.js"; const main = defineCommand({ meta: { name: "agent-kit", - description: "HITL framework for AI-assisted IDEs", + description: "HITL framework for AI-assisted IDEs (Mission Kit family)", version: KIT_VERSION, }, subCommands: { @@ -46,6 +48,19 @@ const main = defineCommand({ monitors: monitorsCommand, validate: validateCommand, }, + async run({ rawArgs }) { + // citty also invokes parent `run` after a subcommand; skip when one was selected. + if (hasCliSubcommand(rawArgs)) return; + printWelcomeScreen(); + }, }); -runMain(main); +runMain(main, { + showUsage: async (cmd, parent) => { + if (!parent) { + process.stdout.write(`${await renderGroupedRootHelp(cmd)}\n`); + return; + } + await showUsage(cmd, parent); + }, +}); diff --git a/packages/cli/src/lifecycle/cursor-update-awareness.test.ts b/packages/cli/src/lifecycle/cursor-update-awareness.test.ts index a1cbf0a..cd4ea5f 100644 --- a/packages/cli/src/lifecycle/cursor-update-awareness.test.ts +++ b/packages/cli/src/lifecycle/cursor-update-awareness.test.ts @@ -13,6 +13,7 @@ import { parseInventoryRefreshed, parseOpenActionIds, readCursorUpdateCheckPrefs, + resolveInventoryRoot, stampCursorUpdateCheck, } from "./cursor-update-awareness.js"; @@ -183,4 +184,28 @@ describe("checkCursorUpdateAwareness", () => { const result = await checkCursorUpdateAwareness(cwd, { respectPrefs: true, offline: true }); expect(result.status).toBe("skipped-disabled"); }); + + it("walks up from nested cwd to find inventory", async () => { + const root = mkdtempSync(path.join(tmpdir(), "cursor-awareness-walk-")); + writeInventory( + root, + "Living audit; last refreshed **2026-07-19**.\n\n| ID | Status | Action |\n|----|--------|--------|\n| A1 | ✅ Done | Fix |\n", + ); + const nested = path.join(root, "packages", "cli"); + mkdirSync(nested, { recursive: true }); + + expect(await resolveInventoryRoot(nested)).toBe(path.resolve(root)); + const result = await checkCursorUpdateAwareness(nested, { offline: true }); + expect(result.status).toBe("current"); + expect(result.message).not.toMatch(/Missing inventory/); + }); + + it("errors with --cwd hint when no inventory in ancestors", async () => { + const cwd = mkdtempSync(path.join(tmpdir(), "cursor-awareness-nodocs-")); + expect(await resolveInventoryRoot(cwd)).toBeNull(); + const result = await checkCursorUpdateAwareness(cwd, { offline: true }); + expect(result.status).toBe("error"); + expect(result.message).toMatch(/Missing inventory/); + expect(result.message).toMatch(/--cwd/); + }); }); diff --git a/packages/cli/src/lifecycle/cursor-update-awareness.ts b/packages/cli/src/lifecycle/cursor-update-awareness.ts index 445be2a..2a5c064 100644 --- a/packages/cli/src/lifecycle/cursor-update-awareness.ts +++ b/packages/cli/src/lifecycle/cursor-update-awareness.ts @@ -1,4 +1,4 @@ -import { readFile } from "node:fs/promises"; +import { access, readFile } from "node:fs/promises"; import path from "node:path"; import { readJson, writeJson } from "../utils/fs.js"; @@ -17,6 +17,30 @@ export const CURSOR_VERSION_MAJOR_MAX = 20; const INVENTORY_REL = path.join("docs", "cursor-native-audit.md"); const FEATURES_REL = path.join("docs", "cursor-3-features.md"); +/** + * Walk up from cwd until docs/cursor-native-audit.md exists. + * Returns the directory that contains docs/, or null when none is found. + */ +export async function resolveInventoryRoot(cwd: string): Promise<string | null> { + let dir = path.resolve(cwd); + for (;;) { + try { + await access(path.join(dir, INVENTORY_REL)); + return dir; + } catch { + // keep walking + } + const parent = path.dirname(dir); + if (parent === dir) break; + dir = parent; + } + return null; +} + +function missingInventoryMessage(): string { + return `Missing inventory at ${INVENTORY_REL}. Run from the kit/repo root that contains that file, or pass --cwd <path>.`; +} + export type CursorAwarenessStatus = | "current" | "gaps-found" @@ -301,8 +325,6 @@ export async function checkCursorUpdateAwareness( cwd: string, options: CursorAwarenessOptions = {}, ): Promise<CursorAwarenessResult> { - const inventoryPath = path.join(cwd, INVENTORY_REL); - const featuresPath = path.join(cwd, FEATURES_REL); const prefs = readCursorUpdateCheckPrefs(await loadContextConfig(cwd)); const changelogUrl = options.changelogUrl ?? prefs.changelogUrl; @@ -338,6 +360,25 @@ export async function checkCursorUpdateAwareness( } } + const inventoryRoot = await resolveInventoryRoot(cwd); + if (!inventoryRoot) { + return baseResult({ + status: "error", + inventoryPath: INVENTORY_REL, + featuresPath: FEATURES_REL, + changelogUrl, + latestCursorVersion: null, + lastSeenCursorVersion: prefs.lastSeenCursorVersion, + inventoryRefreshed: null, + openActionIds: [], + gaps: [], + message: missingInventoryMessage(), + }); + } + + const inventoryPath = path.join(inventoryRoot, INVENTORY_REL); + const featuresPath = path.join(inventoryRoot, FEATURES_REL); + let inventoryMd: string; try { inventoryMd = await readFile(inventoryPath, "utf8"); @@ -352,7 +393,7 @@ export async function checkCursorUpdateAwareness( inventoryRefreshed: null, openActionIds: [], gaps: [], - message: `Missing inventory at ${INVENTORY_REL}.`, + message: missingInventoryMessage(), }); } diff --git a/packages/cli/src/lifecycle/l0.test.ts b/packages/cli/src/lifecycle/l0.test.ts index d7dac71..1db4902 100644 --- a/packages/cli/src/lifecycle/l0.test.ts +++ b/packages/cli/src/lifecycle/l0.test.ts @@ -59,6 +59,8 @@ describe("canonical L0 inventory", () => { ]), ); expect(targets).not.toContain(".cursor/commands/onboard.md"); + // Factory-only maintainer triage; never install/update to consumers. + expect(targets).not.toContain(".cursor/commands/public-issue-triage.md"); }); it("keeps the onboarding command on the repository-readiness contract", async () => { @@ -87,6 +89,25 @@ describe("canonical L0 inventory", () => { expect(command).not.toContain("Enable Claude external review"); }); + it("onboard command offers a domain-skills HITL gate before finish-setup", async () => { + const command = await readRepositoryFile(".cursor/commands/agent-kit-onboard.md"); + + expect(command).toContain("Domain-skills scaffold"); + expect(command).toContain("Scaffold domain skills"); + expect(command).toContain("Defer (record reason)"); + expect(command).toMatch(/Options:.*Scaffold domain skills.*Defer \(record reason\).*Skip/); + expect(command).toContain("type their own answer"); + expect(command).toContain("Instruction-only surface"); + expect(command).toContain(".cursor/skills/domain/"); + expect(command).toContain("Relevant skills"); + expect(command).toContain(".cursor/agent-kit.json"); + expect(command).toContain(".cursor/context/personalization.json"); + expect(command).toContain("onboarding.domainSkills"); + expect(command).toContain("https://github.com/agent-kit-startup/agent-kit/issues/36"); + expect(command).toContain("Next: /start-project"); + expect(command).toContain("Next: finish setup"); + }); + it("keeps registry and Port B install sources aligned with the canonical inventory", async () => { const [registryText, installText] = await Promise.all([ readRepositoryFile("registry/registry.json"), diff --git a/packages/cli/src/lifecycle/l0.ts b/packages/cli/src/lifecycle/l0.ts index d024c86..19f7f80 100644 --- a/packages/cli/src/lifecycle/l0.ts +++ b/packages/cli/src/lifecycle/l0.ts @@ -122,6 +122,9 @@ export const L0_ARTIFACTS: readonly L0Artifact[] = [ source: ".cursor/commands/dogfood.md", target: ".cursor/commands/dogfood.md", }, + // Factory-only: `.cursor/commands/public-issue-triage.md` is intentionally + // omitted from L0 (and excluded from public-sync). See ADR + // 2026-08-05_factory-only-public-issue-triage-command.md. { source: ".cursor/commands/cursor-update-awareness.md", target: ".cursor/commands/cursor-update-awareness.md", diff --git a/packages/cli/src/lifecycle/overlay-known-hashes.ts b/packages/cli/src/lifecycle/overlay-known-hashes.ts index 2c582b4..1ad9cbe 100644 --- a/packages/cli/src/lifecycle/overlay-known-hashes.ts +++ b/packages/cli/src/lifecycle/overlay-known-hashes.ts @@ -6,51 +6,115 @@ * content changes across releases. */ export const KNOWN_SHIPPED_OVERLAY_HASHES: ReadonlySet<string> = new Set([ + "004d98f557942677264d053dcd8335f2bbcf19c5591123ec16a7a2697cb88932", + "05ee6ecde4215e33ace0ee152ffbf2cd885d2bd4b032900f0f0bb001d350ef0f", "082bdbc584be3f3d8aa6e9d7b9f4e076450f3ff48f9147a1deb557e31475036b", + "0b5fe4729c3c68654434837630d775b847c082418f25b0b2c581700fe84b797a", + "1106b279558b6a20f87951102f172a680976f3e6530855d5dad083e1f89b9ba3", "15566765ec95dad3bc3160f20ac910e01a13471df536e6d990f4b263700784a9", "1681e6cf7b80a67cd999b94b8b6e16eb8caed0e11b32905848ba733da956e902", + "17818e47e06349ee35c1d8e1cc16e8bcbbbcc89c664014e9096e1f08c8567807", "1974a5af0e4ba2bb0fc7e1872ef9ab70e323323ab216958c31731bcdd82c8241", + "1b384771cc6228215e31e6a9ab0943b13a5cdc463354a2641f1c0d85b4357e84", + "1b5f8c4197393dc9110d020f3b619b8502862adeb6522a6b9be4b49dd8794612", + "1c5c1af87951367ad04bb6fa5b1d6ebaaefbc0ee95989b624cabf406a1869907", + "1ded5c7b7eefc355fc1d17feb65ec472e35ed96076e47ce95f7bff5c1b84746c", "1e58e0d4f2e8459b95a40d1b1e72c5c85a87230cbde945840a84073fe3a20422", "285668d08f1b15d96286d27e29cc4a67e126e097fb8a940bdde33ccf25bd2bcd", + "2acdb148b07f3a4872b896da419af796fd3fefaace7ac79cdee0aab447004a83", + "2cd7fc018a384b0188737259ecf593ce25b4b75cc38b3e410063ac91745894b9", + "2f6cedfa61cb62126efd6b845bbf4d8d7913e39223d5f799ac09ef64c0489824", + "3204a7449f3031916a8d57b37144e5d8ded6c94aa5659493fa5cc2c7321f4eab", + "33982aea21a074d21a638850475ce1fefa7d043ec035de3855889cc68d60cf35", + "38675aec695e9d4e7f16e8fbe3facc3280f47fb875bf9ac50bd6837b92b7a7fa", + "3ee94e2441218025c085856679d17b4a4fe7a6e3d0274423abe20a422a5ab05d", "3f3354825ff06ff9f9ae1e29b71784fef4250020c23c1b490940cf06b2be0f28", + "46a1dd4a07a2190ab9fb09abc05a3962277209f8e0c28033bb41450301474c86", + "48a0d55e21132a305946b195db7f6b75da78d44a1d32166fb0c69bef4a5ffbe2", + "4b62a46c74e5fa6a80283f21e8e6711958fbcd577051dc73ff4050643a031475", "4bf23acbc9bd4e0f8468e61f4970e182600b72d9025d29e81ddbd83cb7b1710a", "4c5167d0ebde685f266dce32c516a7c43876e0520647b93f07f2e2147e6a7b2f", "4dc4efcd1b43c643d3889fb349df8d4964d0867c3a67eb67e2ec168fbb70fc75", + "4f133554d01cedc62280006f4cf6453e25a5cb41ef6dde4a43f9508cb36ef75d", + "4fe1e2bce8bd55a6decdd62c887a423cf85148b692f313aad4c777d0e50596b3", "4ff844cf789cef4b1e809a55ca006316cd964bcdb2557d91c916a08e30ad15f6", "533b41599c25698e5746166096a68fd4080efb5e16287b2a1ed5f73295cda446", + "54dda8addb74bc187ee23c31f8b7d17ad19e745a45169d29ffea094860eb18f2", "570c3f91fd31a4cc1e56e997c85feab5c6756f45fc73dc741afe573912111231", + "5cb36f5dab846b2551afd05eab92c14122853c147d7f02136d1c04e01e3c9def", "5d302d207521c53b9fa814ee0eba1376a5b2bfd2983c0373657c9fdf9884541b", + "5f5c125f7edf280029f214589ce3e088fc74bad717e30178405a3b537673c42f", "621bc6cecf7c710a97e93463ca9b8872a1fcab1325c358cc8be65b418eebb339", + "693c732658e31d6403cf59e85985518f24ab92bf7fcdc3c225e9a79e54597a43", "69715048da3963947aa8a39e7ac30ec01632fec862bfcb9559c46764b4833be0", + "6994595e3a62edb6c518006a6bece6848c7077977c041439cf7c822b89304a14", "6a42e76fc038ca69ddad18541b8df161770c58980fefaf9e5e80d87407074bec", "6a9af9e8a95dbb279347166c43dcee245ca673f9687841086b49911c54944c26", + "99fdd85310e658a1c1b0e8f92257493409c28c3d693f24deeaa665861788e1e4", // public registry SoT n8n-workflows/SKILL.md (em dash description) "6b66ed7747ea2d19cb457f5af656e3c07f9f563f7f32b3951fa4ae5f27f0ac46", + "6dede0588274f5b44d833bcc696ec4776eb978ed24a20efdcc9c38caf3df1576", + "71991a10972b8b956cae93c88517e31741c95e351814082df65e44f813590210", + "75e591bfde61ca1ca3ca1303cc9bc487197af2f713c68f346a46e94ca9432fd5", "76ae85bbc336c6f435d9653e8f254ae49774b4ebaecf7335697115933eb8797c", + "79878f0f6f59d1a0b65d94011c0cafe7b6f8e03e89f568aa638b8ba7a2c34076", "8108b202c829627c3c2df33575016ebe942df42acf4f1cc393900be3b82b402c", + "819b7bd3ba9e8bf12dcd4f81a76c98e48261b34c397f339b36d36eb364d9c3f4", "8619500b0c7024390d43e3aaefa2a2d731728d7c664aaf90e0acdd067f3397ce", + "875b0a8805280b2a2df43f2c42f9063fe0623a34fa19c3aa5d2ec48211f3ebae", "8a11c80b0fbf6c4aabf60eec05b8cca7dccf5bca472787a8afa3028fd8fbedfa", "8b25659727063013e8bf6d7925f815b2630c2919b21e6fdac354362bf122bfab", "8c7edbacc74b1431fbecb04c9857c0e7a9cb1578fc6ca6367be888dbcec3c98f", + "8ffc181bb152c68a3353309bfdc87c42bdb925b98675cdba959c0cdb90b2adea", + "91ce0c933e3c8ee81927b8224f944ec5ce5445098a2ad154773092e3541dbf69", + "9309af07a0a99a56143386b3ddd2a35fae29378fba3f6b4c9297a5d1c02a9724", "9439da963acbe5dd88a3aa4d6ba4fb297db666ce8f2c9d3917dbc6d6e517f9aa", + "989e7e1bdfb29dedf850405f28648810d008885196efe7ae38c6e7554c7ce45d", "98d96e25f67aa9c234cb0c422f0d701c22a04bfe184fb4eb68295003d429552b", "99031145609af46e7e0f315006bf633f47002eccece8eb7af8b5efd762313ade", "9a3cea681399b42ef930a0720802378fe1b06ee73370df9ede9d0309c3c60ab2", + "9ccf52fe53c6fa4d731650ab4928ce50e201fa34f3e174fee70e3d29867fef2a", "9d905aca0123f3018c81c579217104f1749d8da802f3837b2407fbad8f2d803f", + "9e324c52c1a9d1ceee738bccf47da92910aca9f7a5a21cc41738d9573c414830", + "a013a6a476160183d6457ff6c1c51ec228bdca359b11eb15cd9b7fc177a705db", "a08d04f8efa121ad498f96873778c22630c9172c07c216839e348cd0bb7c2292", + "a7cd7058bcb6fbd962c37317fa16893bc86a270d0164af5a0e6f3f026aa408ad", + "a833a7131cdfc003b45a48cb26c6ce33dc218c1814156047db5211a79192e0a0", + "ac2f1f7d9e68f9c98fc4fe5ca348b1a90cf484141bbd116db871d63bdcc3c7c3", "b5c7a10df90d209be07159b7ff9bfa24e869538a0b6026bb5ce79e1cdd5b2528", + "bd9fd1efacbd132c5dafc12cd0cf2b651a2b1fab7b2d7a9ba4a3e44128097f48", + "be964ff33a40280e13ce6071fb3e30ce87c374527efbb74c25c9d4534084a634", "c96d522be440fbb638dbe953cebf1bb31185c431ac2e14446369cfb7d155a951", "cf2bd11891b934484171fa705f470de284e3874402e1e487750735dbc7692857", + "d0e3bf82efbe32c09761689802ffea7838046dbdd4803b88ead5ec3dfc7efb7e", "d4659e4bb6fd6022559fc632b17db6a39007c04bc0ded59017a0dacc013632ed", "d484243167e13c34f9df7ff378baf2967f7cbfcd88170d4d32e745efbcae7480", + "d67ac8fa1edec6e50c82aeca064335e799e408d0e4bbc95e1407cf7463a61de3", + "d7ad53f06a41bcb2f2b998fd4a4c99209a0cf287cf0d0d4acbcc36c73dd46e4a", "d96aa15dde45147aa40db64684688a4b5c9ad660dcd16c55156b31933a42c37e", "d9ecd90f80ebc25db73ebed27c32596b9cfce518b3026a3a4d5787321ba6324a", "dd01d051dbab257e16609b02c983bad6a576ed8473781b4f1df9ef2eb5f85795", + "dd9ff2ff8aa7bcb2cdfc59092ad73227631c1c3619b495131f7faee65224cb17", "de6efabde6a1d69357926b88b5a4ed3eec19e11b82597cb999f5365f952305b8", + "debc0758b3e39294df85ccb7eab3455e93a2b8c608602d309a7b0ebdb6618805", "e03b5764b908f9ed5c592108fbc11d5962d9256fd129a63ab4a39c8efaa2ef3e", "e102e77f9f520f938d26861978249e4d85a8c8755d48c8e79b21fef9423325ee", + "e2d89f780c6ae5393dc05818f9e3e9a8bf6629eac05702eea8f9a25b578e3ff1", + "e2f1e9108fd450817ab42570db656b1e43b20b5ba703b14027ce540e1b8fe502", + "e37c62695b5c70451ef1a0c90b31b73a9c442ebc55fb3fc3f79c294b993b99ea", + "e3c32aec17e8f728cbd13162a123a9debffb8bbcec527a00b1c2df30a1a20080", "e511e04064ffb8c429b659d2ae2e0f223e8cc51039ae816753095a5f7a160595", + "e5682d4696e874d09f12a1d65891fee5971201a29c654c08f5d3b90fb9a6a0b7", + "e57437bf790fda1d43b895e2b5b0a7d2d57635c319ac0e2439ba2222a825bcde", + "e8bb214c3e551c44f379db13c5962ef20698b505b0227d283d3ae632e1bc3626", + "ea36e74c9d592980cba5c08d04ee70fc59a6cd01e87a50bbfae4f668557df4b4", "ebd41f8df764a14b3460016fafcfdde98c4c3a59a3c17f773451ef061fc42f95", + "ef181a4e2bd9f82f45628637b8d66925ffa20801558808f1b03ea72b7cb82a72", "f061432a2606049717c0cbf098ef07b1da4df2b136c4cb9cdaa7ea44c876bcd6", "f061d36a1180293519fbb560bbf659c621bd90885e995fca6eaf2eda9e418a50", "f28731a82f4b394a5765fdefe2d1b667810831f6dcea400ca62bc66e1d65371c", + "f46c108457e4fe12953dca75b409e06fca9cb264baffabe0c0d04c89af471e56", + "f6bc17efa746680a44bbc49be67ac3f924d9c306c6d937f9959c57a1d3c5b9af", + "f981764422d468567b5aff31148dc659aeee22cb13dc0ecd873d737deaf07372", + "fa306a0cdb0f40c817e32164cd03b946564a02b4b7f7c3f4f0b9513096584d28", + "fa5cf460eb314437081f7cea30dc8041c0bd6fc3f560a74bc2d7be1bf07384b0", ]); diff --git a/packages/cli/src/lifecycle/overlay.test.ts b/packages/cli/src/lifecycle/overlay.test.ts index 8f5a991..eb70b6f 100644 --- a/packages/cli/src/lifecycle/overlay.test.ts +++ b/packages/cli/src/lifecycle/overlay.test.ts @@ -6,6 +6,7 @@ import { describe, expect, it } from "vitest"; import { DEFAULT_PROTECTED_PATHS } from "../manifest/types.js"; import { installPack } from "../registry/install.js"; import { copyRegistryFile, emptyStats, recordOutcome } from "./apply.js"; +import { L0_ARTIFACTS } from "./l0.js"; import { KNOWN_SHIPPED_OVERLAY_HASHES } from "./overlay-known-hashes.js"; import { MANAGED_HASHES_REL, @@ -19,6 +20,43 @@ import { installL0 } from "./sync.js"; const kitRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../../.."); +describe("KNOWN_SHIPPED_OVERLAY_HASHES coverage", () => { + it("includes every current L0 overlay artifact body", async () => { + const overlay = L0_ARTIFACTS.filter((a) => isConsumerOverlayPath(a.target)); + expect(overlay.length).toBeGreaterThan(0); + const missing: string[] = []; + for (const artifact of overlay) { + const body = await readFile(path.join(kitRoot, artifact.source), "utf8"); + if (!KNOWN_SHIPPED_OVERLAY_HASHES.has(contentHash(body))) { + missing.push(artifact.source); + } + } + expect(missing).toEqual([]); + }); + + it("includes every registry skill SKILL.md body (core + community)", async () => { + const reg = JSON.parse( + await readFile(path.join(kitRoot, "registry/registry.json"), "utf8"), + ) as { + skills?: { + core?: Array<{ path: string; id: string }>; + community?: Array<{ path: string; id: string }>; + }; + }; + const entries = [...(reg.skills?.core || []), ...(reg.skills?.community || [])]; + expect(entries.length).toBeGreaterThan(0); + const missing: string[] = []; + for (const skill of entries) { + const source = path.join(skill.path, "SKILL.md"); + const body = await readFile(path.join(kitRoot, source), "utf8"); + if (!KNOWN_SHIPPED_OVERLAY_HASHES.has(contentHash(body))) { + missing.push(source); + } + } + expect(missing).toEqual([]); + }); +}); + describe("consumer overlay path detection", () => { it("matches agents, skills, and commands only", () => { expect(isConsumerOverlayPath(".cursor/agents/foo.md")).toBe(true); @@ -117,6 +155,42 @@ describe("consumer overlay apply policy", () => { expect(ledger.hashes[cmdRel]).toBe(contentHash(newer)); }); + it("end-to-end: ledger-absent update refreshes known shipped and preserves customized peers", async () => { + const project = await mkdtemp(path.join(tmpdir(), "agent-kit-overlay-e2e-update-")); + const registry = await mkdtemp(path.join(tmpdir(), "agent-kit-overlay-registry-e2e-")); + const uneditedRel = ".cursor/commands/summary.md"; + const customRel = ".cursor/commands/tips.md"; + await mkdir(path.join(project, ".cursor/commands"), { recursive: true }); + await mkdir(path.join(registry, ".cursor/commands"), { recursive: true }); + + const shippedSummary = await readFile(path.join(kitRoot, uneditedRel), "utf8"); + const shippedTips = await readFile(path.join(kitRoot, customRel), "utf8"); + expect(KNOWN_SHIPPED_OVERLAY_HASHES.has(contentHash(shippedSummary))).toBe(true); + + const bumpedSummary = `${shippedSummary}\n<!-- bump summary -->\n`; + const bumpedTips = `${shippedTips}\n<!-- bump tips -->\n`; + const customTips = `${shippedTips}\n<!-- consumer edit -->\n`; + + await writeFile(path.join(project, uneditedRel), shippedSummary, "utf8"); + await writeFile(path.join(project, customRel), customTips, "utf8"); + await writeFile(path.join(registry, uneditedRel), bumpedSummary, "utf8"); + await writeFile(path.join(registry, customRel), bumpedTips, "utf8"); + + const outcomes = { + unedited: await copyRegistryFile(registry, project, uneditedRel, uneditedRel, [ + ...DEFAULT_PROTECTED_PATHS, + ]), + custom: await copyRegistryFile(registry, project, customRel, customRel, [ + ...DEFAULT_PROTECTED_PATHS, + ]), + }; + + expect(outcomes.unedited).toBe("written"); + expect(outcomes.custom).toBe("preserved-customized"); + expect(await readFile(path.join(project, uneditedRel), "utf8")).toBe(bumpedSummary); + expect(await readFile(path.join(project, customRel), "utf8")).toBe(customTips); + }); + it("refreshes unedited kit command when local hash matches ledger", async () => { const project = await mkdtemp(path.join(tmpdir(), "agent-kit-overlay-refresh-")); const cmdRel = ".cursor/commands/summary.md"; diff --git a/packages/cli/src/lifecycle/overlay.ts b/packages/cli/src/lifecycle/overlay.ts index 440770f..d04fcc9 100644 --- a/packages/cli/src/lifecycle/overlay.ts +++ b/packages/cli/src/lifecycle/overlay.ts @@ -97,6 +97,11 @@ export function shouldPreserveCustomizedOverlay( * Use this in factory/dogfood checkouts on first update so the local files * become the baseline for subsequent refresh-vs-preserve decisions. * Does not overwrite existing ledger entries. + * + * Note: this walks every file under the overlay prefixes, including user-added + * non-kit basenames. That is harmless today because those basenames are never in + * the L0/pack/skill apply set; it only widens what the ledger claims to describe + * if the ledger later gains semantics beyond refresh-vs-preserve. */ export async function seedManagedHashLedger(projectRoot: string): Promise<ManagedHashLedger> { const ledger = await loadManagedHashLedger(projectRoot); diff --git a/packages/cli/src/registry/resolve.test.ts b/packages/cli/src/registry/resolve.test.ts index 567e722..a7decd3 100644 --- a/packages/cli/src/registry/resolve.test.ts +++ b/packages/cli/src/registry/resolve.test.ts @@ -1,11 +1,13 @@ -import { mkdir, mkdtemp, readFile, writeFile } from "node:fs/promises"; +import { mkdir, mkdtemp, readFile, rm, stat, utimes, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import path from "node:path"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { DEFAULT_REGISTRY_REF, DEFAULT_REGISTRY_URL, + acquireCacheLock, assertSafeRegistrySource, + releaseCacheLock, resolveRegistryRoot, } from "./resolve.js"; @@ -168,4 +170,183 @@ describe("resolveRegistryRoot remote-cache", () => { const fetchCalls = execFileMock.mock.calls.filter((call) => gitArgs(call)[0] === "fetch"); expect(fetchCalls).toHaveLength(0); }); + + it("returns an unlock function for remote-cache (caller releases after copy)", async () => { + const dest = await seedRemoteCache(); + const resolved = await resolveRegistryRoot({ cwd: projectCwd }); + + expect(resolved.source).toBe("remote-cache"); + expect(resolved.root).toBe(dest); + expect(typeof resolved.unlock).toBe("function"); + + const lockDir = `${dest}.lock`; + const locked = await stat(lockDir) + .then(() => true) + .catch(() => false); + expect(locked).toBe(true); + + await resolved.unlock?.(); + const unlocked = await stat(lockDir) + .then(() => true) + .catch(() => false); + expect(unlocked).toBe(false); + }); +}); + +describe("acquireCacheLock", () => { + it("acquires and releases a lock directory", async () => { + const dir = await mkdtemp(path.join(tmpdir(), "ak-lock-")); + const target = path.join(dir, "cache-target"); + const unlock = await acquireCacheLock(target); + const lockDir = `${target}.lock`; + const lockExists = await stat(lockDir) + .then(() => true) + .catch(() => false); + expect(lockExists).toBe(true); + await unlock(); + const afterRelease = await stat(lockDir) + .then(() => true) + .catch(() => false); + expect(afterRelease).toBe(false); + }); + + it("writes a PID/UUID owner marker and removes it on release", async () => { + const dir = await mkdtemp(path.join(tmpdir(), "ak-lock-")); + const target = path.join(dir, "cache-target"); + const unlock = await acquireCacheLock(target); + const lockDir = `${target}.lock`; + const ownerRaw = await readFile(path.join(lockDir, "owner.json"), "utf8"); + const owner = JSON.parse(ownerRaw) as { pid: number; uuid: string }; + expect(owner.pid).toBe(process.pid); + expect(owner.uuid).toMatch(/^[0-9a-f-]{36}$/); + await unlock(); + const afterRelease = await stat(lockDir) + .then(() => true) + .catch(() => false); + expect(afterRelease).toBe(false); + }); + + it("second caller waits until first releases", async () => { + const dir = await mkdtemp(path.join(tmpdir(), "ak-lock-")); + const target = path.join(dir, "cache-target"); + const unlock1 = await acquireCacheLock(target); + const events: string[] = []; + const p2 = acquireCacheLock(target).then((unlock) => { + events.push("acquired"); + return unlock; + }); + await new Promise((r) => setTimeout(r, 100)); + expect(events).not.toContain("acquired"); + await unlock1(); + const unlock2 = await p2; + expect(events).toContain("acquired"); + await unlock2(); + }); + + it("reclaims a stale lock owned by a dead process", async () => { + const dir = await mkdtemp(path.join(tmpdir(), "ak-lock-")); + const target = path.join(dir, "cache-target"); + const lockDir = `${target}.lock`; + await mkdir(lockDir, { recursive: true }); + const deadPid = 999_999_999; + const staleUpdatedAt = Date.now() - 6 * 60_000; + await writeFile( + path.join(lockDir, "owner.json"), + JSON.stringify({ pid: deadPid, uuid: "dead-uuid", updatedAt: staleUpdatedAt }), + "utf8", + ); + const oldDate = new Date(staleUpdatedAt); + await utimes(lockDir, oldDate, oldDate); + + const killSpy = vi.spyOn(process, "kill").mockImplementation((pid: number) => { + if (pid === deadPid) throw new Error("ESRCH"); + return true; + }); + try { + const unlock = await acquireCacheLock(target); + const ownerRaw = await readFile(path.join(lockDir, "owner.json"), "utf8"); + const owner = JSON.parse(ownerRaw) as { pid: number; uuid: string }; + expect(owner.pid).toBe(process.pid); + expect(owner.uuid).not.toBe("dead-uuid"); + await unlock(); + } finally { + killSpy.mockRestore(); + } + }); + + it("does not reclaim a fresh lock owned by a living process", async () => { + const dir = await mkdtemp(path.join(tmpdir(), "ak-lock-")); + const target = path.join(dir, "cache-target"); + const unlock1 = await acquireCacheLock(target); + const events: string[] = []; + const p2 = acquireCacheLock(target).then((unlock) => { + events.push("acquired"); + return unlock; + }); + await new Promise((r) => setTimeout(r, 100)); + expect(events).not.toContain("acquired"); + await unlock1(); + const unlock2 = await p2; + expect(events).toContain("acquired"); + await unlock2(); + }); + + it("releaseCacheLock fail-closes when owner metadata is missing", async () => { + const dir = await mkdtemp(path.join(tmpdir(), "ak-lock-")); + const target = path.join(dir, "cache-target"); + const unlock = await acquireCacheLock(target); + const lockDir = `${target}.lock`; + await rm(path.join(lockDir, "owner.json")); + // Ownerless successor publish window: releasing uuid must not delete the lock. + await releaseCacheLock(lockDir, "former-owner-uuid"); + const stillThere = await stat(lockDir) + .then(() => true) + .catch(() => false); + expect(stillThere).toBe(true); + // Cleanup: restore a matching owner so unlock can release, then leave dir clean. + await writeFile( + path.join(lockDir, "owner.json"), + JSON.stringify({ pid: process.pid, uuid: "cleanup", updatedAt: Date.now() }), + "utf8", + ); + await releaseCacheLock(lockDir, "cleanup"); + await unlock().catch(() => {}); + }); + + it("releaseCacheLock fail-closes on malformed owner metadata", async () => { + const dir = await mkdtemp(path.join(tmpdir(), "ak-lock-")); + const lockDir = path.join(dir, "cache-target.lock"); + await mkdir(lockDir, { recursive: true }); + await writeFile(path.join(lockDir, "owner.json"), "{not-json", "utf8"); + await releaseCacheLock(lockDir, "any-uuid"); + const stillThere = await stat(lockDir) + .then(() => true) + .catch(() => false); + expect(stillThere).toBe(true); + await rm(lockDir, { recursive: true, force: true }); + }); + + it("does not delete a successor lock when prior unlock races owner-less window", async () => { + const dir = await mkdtemp(path.join(tmpdir(), "ak-lock-")); + const target = path.join(dir, "cache-target"); + const lockDir = `${target}.lock`; + // Simulate successor: mkdir succeeded, owner.json not published yet. + await mkdir(lockDir, { recursive: false }); + await releaseCacheLock(lockDir, "prior-holder-uuid"); + const successorStillHolds = await stat(lockDir) + .then(() => true) + .catch(() => false); + expect(successorStillHolds).toBe(true); + // Successor can still publish and release normally. + await writeFile( + path.join(lockDir, "owner.json"), + JSON.stringify({ pid: process.pid, uuid: "successor", updatedAt: Date.now() }), + "utf8", + ); + await releaseCacheLock(lockDir, "successor"); + const gone = await stat(lockDir) + .then(() => true) + .catch(() => false); + expect(gone).toBe(false); + }); }); diff --git a/packages/cli/src/registry/resolve.ts b/packages/cli/src/registry/resolve.ts index e777e44..8e2708b 100644 --- a/packages/cli/src/registry/resolve.ts +++ b/packages/cli/src/registry/resolve.ts @@ -1,5 +1,6 @@ import { execFile } from "node:child_process"; -import { createHash } from "node:crypto"; +import { createHash, randomUUID } from "node:crypto"; +import { mkdir, readFile, rename, rm, rmdir, stat, writeFile } from "node:fs/promises"; import { homedir } from "node:os"; import path from "node:path"; import { promisify } from "node:util"; @@ -7,6 +8,135 @@ import { fileExists } from "../utils/fs.js"; const execFileAsync = promisify(execFile); +const LOCK_TIMEOUT_MS = 60_000; +const LOCK_RETRY_MS = 200; +const LOCK_STALE_MS = 5 * 60_000; +const LOCK_REFRESH_MS = 10_000; + +interface LockOwner { + pid: number; + uuid: string; + updatedAt: number; +} + +function lockOwnerPath(lockDir: string): string { + return path.join(lockDir, "owner.json"); +} + +function isProcessAlive(pid: number): boolean { + try { + process.kill(pid, 0); + return true; + } catch { + return false; + } +} + +async function readLockOwner(lockDir: string): Promise<LockOwner | null> { + try { + const raw = await readFile(lockOwnerPath(lockDir), "utf8"); + const parsed = JSON.parse(raw) as LockOwner; + if (typeof parsed.pid === "number" && typeof parsed.uuid === "string") { + return parsed; + } + } catch {} + return null; +} + +async function writeLockOwner(lockDir: string, owner: LockOwner): Promise<void> { + // Atomic publish: never leave an observable lock dir without a complete owner file. + const finalPath = lockOwnerPath(lockDir); + const tmpPath = path.join(lockDir, `owner.${owner.uuid}.tmp`); + await writeFile(tmpPath, JSON.stringify(owner), "utf8"); + await rename(tmpPath, finalPath); +} + +async function refreshLockOwner(lockDir: string, uuid: string): Promise<void> { + const owner = await readLockOwner(lockDir); + if (!owner || owner.uuid !== uuid) return; + await writeLockOwner(lockDir, { ...owner, updatedAt: Date.now() }); +} + +async function releaseCacheLock(lockDir: string, uuid: string): Promise<void> { + const owner = await readLockOwner(lockDir); + // Fail closed: null/unreadable/mismatched owner is not ours (avoids deleting a + // successor's lock during the mkdir → owner publish window). + if (!owner || owner.uuid !== uuid) { + return; + } + try { + await rm(lockDir, { recursive: true, force: true }); + } catch {} +} + +async function tryReclaimStaleLock(lockDir: string): Promise<boolean> { + let owner: LockOwner | null; + let lockStat: Awaited<ReturnType<typeof stat>>; + try { + lockStat = await stat(lockDir); + owner = await readLockOwner(lockDir); + } catch { + return false; + } + + const now = Date.now(); + if (owner) { + const ownerFresh = now - owner.updatedAt < LOCK_STALE_MS; + if (isProcessAlive(owner.pid) && ownerFresh) { + // Owner is still alive and refreshing; do not reclaim. + return false; + } + } + + if (now - lockStat.mtimeMs > LOCK_STALE_MS) { + try { + await rm(lockDir, { recursive: true, force: true }); + return true; + } catch {} + } + return false; +} + +async function acquireCacheLock(cacheDir: string): Promise<() => Promise<void>> { + const lockDir = `${cacheDir}.lock`; + const deadline = Date.now() + LOCK_TIMEOUT_MS; + const uuid = randomUUID(); + const owner: LockOwner = { pid: process.pid, uuid, updatedAt: Date.now() }; + + await mkdir(path.dirname(lockDir), { recursive: true }); + + while (Date.now() < deadline) { + try { + await mkdir(lockDir, { recursive: false }); + await writeLockOwner(lockDir, owner); + const refreshInterval = setInterval(() => { + refreshLockOwner(lockDir, uuid).catch(() => {}); + }, LOCK_REFRESH_MS); + return async () => { + clearInterval(refreshInterval); + await releaseCacheLock(lockDir, uuid); + }; + } catch (err) { + const code = (err as NodeJS.ErrnoException).code; + if (code === "EEXIST") { + if (await tryReclaimStaleLock(lockDir)) { + continue; + } + await new Promise((r) => setTimeout(r, LOCK_RETRY_MS + Math.random() * 100)); + continue; + } + // Lock dir vanished between mkdir and owner publish (peer fail-closed race); retry. + if (code === "ENOENT") { + continue; + } + throw err; + } + } + throw new Error(`Timed out waiting for cache lock on ${cacheDir}. Another install may be stuck.`); +} + +export { acquireCacheLock, releaseCacheLock }; + export const DEFAULT_REGISTRY_URL = "https://github.com/agent-kit-startup/agent-kit"; export const DEFAULT_REGISTRY_REF = "main"; @@ -39,6 +169,8 @@ export interface ResolvedRegistry { source: RegistrySource; url?: string; ref?: string; + /** Release the cache lock when the caller is done reading/writing from {@link root}. */ + unlock?: () => Promise<void>; } function cacheKey(url: string, ref: string): string { @@ -56,10 +188,26 @@ async function cloneRegistry(url: string, ref: string, dest: string): Promise<vo await execFileAsync("git", ["clone", "--depth", "1", "--branch", ref, "--", url, dest], { env: gitEnv(), }); - } catch { - // Some refs are tags or default branch only — retry without --branch then checkout. - await execFileAsync("git", ["clone", "--depth", "1", "--", url, dest], { env: gitEnv() }); - await execFileAsync("git", ["checkout", ref, "--"], { cwd: dest, env: gitEnv() }); + } catch (firstErr) { + try { + await execFileAsync("git", ["clone", "--depth", "1", "--", url, dest], { env: gitEnv() }); + await execFileAsync("git", ["checkout", ref, "--"], { cwd: dest, env: gitEnv() }); + } catch (secondErr) { + const msg = secondErr instanceof Error ? secondErr.message : String(secondErr); + if (/403|Authentication|could not read Username/i.test(msg)) { + throw new Error( + `Registry clone failed (auth/access): ${url}@${ref}. Check git credentials or use --registry <local-path> for a local checkout.`, + ); + } + if (/Could not resolve host|Network is unreachable|Connection refused/i.test(msg)) { + throw new Error( + `Registry clone failed (network): ${url}@${ref}. Check network/proxy settings or use --registry <local-path>.`, + ); + } + throw new Error( + `Registry clone failed: ${url}@${ref}. ${msg}. Use --registry <local-path> if you have a local checkout.`, + ); + } } } @@ -120,16 +268,22 @@ export async function resolveRegistryRoot(options: { assertSafeRegistrySource(url, ref); const dest = path.join(homedir(), ".cache", "agent-kit", "registry", cacheKey(url, ref)); - if (await hasRegistryIndex(dest)) { - // Always refresh so L0 artifacts added after the first clone are visible. - // options.refresh / --refresh remains accepted for CLI/docs compatibility (same path). - await refreshCache(dest); - return { root: dest, source: "remote-cache", url, ref }; - } - - await cloneRegistry(url, ref, dest); - if (!(await hasRegistryIndex(dest))) { - throw new Error(`Cloned ${url}@${ref} but registry/registry.json is missing`); + const unlock = await acquireCacheLock(dest); + let ownershipTransferred = false; + try { + if (await hasRegistryIndex(dest)) { + await refreshCache(dest); + } else { + await cloneRegistry(url, ref, dest); + if (!(await hasRegistryIndex(dest))) { + throw new Error(`Cloned ${url}@${ref} but registry/registry.json is missing`); + } + } + ownershipTransferred = true; + return { root: dest, source: "remote-cache", url, ref, unlock }; + } finally { + if (!ownershipTransferred) { + await unlock(); + } } - return { root: dest, source: "remote-cache", url, ref }; } diff --git a/packages/cli/src/utils/terminal.test.ts b/packages/cli/src/utils/terminal.test.ts new file mode 100644 index 0000000..8f29bf5 --- /dev/null +++ b/packages/cli/src/utils/terminal.test.ts @@ -0,0 +1,241 @@ +import { mkdir, mkdtemp, writeFile } from "node:fs/promises"; +import { homedir, tmpdir } from "node:os"; +import path from "node:path"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; +import { + RootRefusedError, + classifyInstallError, + confirmProjectRoot, + isNonInteractive, + validateProjectRoot, +} from "./terminal.js"; + +describe("isNonInteractive", () => { + const originalIsTTY = process.stdin.isTTY; + + function setStdinIsTTY(value: boolean | undefined): void { + Object.defineProperty(process.stdin, "isTTY", { + value, + writable: true, + configurable: true, + }); + } + + afterEach(() => { + vi.unstubAllEnvs(); + setStdinIsTTY(originalIsTTY); + }); + + it("returns true when CI=true", () => { + vi.stubEnv("CI", "true"); + setStdinIsTTY(true); + expect(isNonInteractive()).toBe(true); + }); + + it("returns true when CI=1", () => { + vi.stubEnv("CI", "1"); + setStdinIsTTY(true); + expect(isNonInteractive()).toBe(true); + }); + + it("returns true when AGENT_KIT_YES=1", () => { + vi.stubEnv("AGENT_KIT_YES", "1"); + setStdinIsTTY(true); + expect(isNonInteractive()).toBe(true); + }); + + it("returns true when stdin is not a TTY", () => { + vi.stubEnv("CI", undefined); + vi.stubEnv("AGENT_KIT_YES", undefined); + setStdinIsTTY(undefined); + expect(isNonInteractive()).toBe(true); + }); + + it("returns false in an interactive TTY without env flags", () => { + vi.stubEnv("CI", undefined); + vi.stubEnv("AGENT_KIT_YES", undefined); + setStdinIsTTY(true); + expect(isNonInteractive()).toBe(false); + }); +}); + +describe("classifyInstallError", () => { + it("classifies EPERM errors", () => { + const err = Object.assign(new Error("EPERM: operation not permitted"), { code: "EPERM" }); + const hint = classifyInstallError(err); + expect(hint.kind).toBe("eperm"); + expect(hint.recovery).toContain("npm cache"); + expect(hint.recovery).toContain("Port B"); + }); + + it("classifies EACCES errors", () => { + const err = Object.assign(new Error("EACCES: permission denied"), { code: "EACCES" }); + const hint = classifyInstallError(err); + expect(hint.kind).toBe("eperm"); + expect(hint.recovery).toContain("ownership drift"); + }); + + it("classifies 403 Forbidden errors", () => { + const hint = classifyInstallError( + new Error("403 Forbidden - GET https://registry.npmjs.org/@dadado/agent-kit-cli"), + ); + expect(hint.kind).toBe("registry-auth"); + expect(hint.recovery).toContain("npm whoami"); + }); + + it("classifies E401 / ENEEDAUTH errors", () => { + const hint = classifyInstallError(new Error("ENEEDAUTH")); + expect(hint.kind).toBe("registry-auth"); + }); + + it("classifies network errors by code", () => { + const err = Object.assign(new Error("getaddrinfo ENOTFOUND registry.npmjs.org"), { + code: "ENOTFOUND", + }); + const hint = classifyInstallError(err); + expect(hint.kind).toBe("network"); + expect(hint.recovery).toContain("network/proxy"); + }); + + it("classifies timeout errors", () => { + const err = Object.assign(new Error("connect ETIMEDOUT"), { code: "ETIMEDOUT" }); + const hint = classifyInstallError(err); + expect(hint.kind).toBe("network"); + }); + + it("returns unknown for unrecognized errors", () => { + const hint = classifyInstallError(new Error("something unexpected")); + expect(hint.kind).toBe("unknown"); + expect(hint.message).toContain("something unexpected"); + }); + + it("handles non-Error values", () => { + const hint = classifyInstallError("string error"); + expect(hint.kind).toBe("unknown"); + expect(hint.message).toBe("string error"); + }); +}); + +vi.mock("@clack/prompts", () => ({ + confirm: vi.fn(), + isCancel: vi.fn((v: unknown) => v === Symbol.for("cancel")), +})); + +describe("validateProjectRoot", () => { + it("refuses the filesystem root", async () => { + const result = await validateProjectRoot("/"); + expect(result.ok).toBe(false); + if (!result.ok) expect(result.reason).toContain("/"); + }); + + it("refuses the user's home directory", async () => { + const result = await validateProjectRoot(homedir()); + expect(result.ok).toBe(false); + if (!result.ok) expect(result.reason).toContain(homedir()); + }); + + it("refuses a directory with neither .git nor .cursor/agent-kit.json", async () => { + const dir = await mkdtemp(path.join(tmpdir(), "ak-root-")); + const result = await validateProjectRoot(dir); + expect(result.ok).toBe(false); + if (!result.ok) expect(result.reason).toContain("no .git"); + }); + + it("accepts a directory with a .git folder", async () => { + const dir = await mkdtemp(path.join(tmpdir(), "ak-root-")); + await mkdir(path.join(dir, ".git"), { recursive: true }); + await expect(validateProjectRoot(dir)).resolves.toEqual({ ok: true }); + }); + + it("accepts a directory with an existing manifest", async () => { + const dir = await mkdtemp(path.join(tmpdir(), "ak-root-")); + await mkdir(path.join(dir, ".cursor"), { recursive: true }); + await writeFile(path.join(dir, ".cursor", "agent-kit.json"), "{}", "utf8"); + await expect(validateProjectRoot(dir)).resolves.toEqual({ ok: true }); + }); +}); + +describe("confirmProjectRoot", () => { + let validProject: string; + + beforeEach(async () => { + validProject = await mkdtemp(path.join(tmpdir(), "ak-root-")); + await mkdir(path.join(validProject, ".git"), { recursive: true }); + }); + + afterEach(() => { + vi.clearAllMocks(); + }); + + it("returns resolved path in non-interactive mode for a valid directory", async () => { + const result = await confirmProjectRoot(validProject, { + nonInteractive: true, + command: "install", + }); + expect(result).toBe(path.resolve(validProject)); + }); + + it("refuses ambiguous root in non-interactive mode (--yes / CI)", async () => { + const badDir = await mkdtemp(path.join(tmpdir(), "ak-root-")); + await expect( + confirmProjectRoot(badDir, { nonInteractive: true, command: "install" }), + ).rejects.toThrow(RootRefusedError); + }); + + it("refuses home directory in non-interactive mode", async () => { + await expect( + confirmProjectRoot(homedir(), { nonInteractive: true, command: "install" }), + ).rejects.toThrow(RootRefusedError); + }); + + it("bypasses validation when forceRoot is true", async () => { + const badDir = await mkdtemp(path.join(tmpdir(), "ak-root-")); + const result = await confirmProjectRoot(badDir, { + nonInteractive: true, + command: "install", + forceRoot: true, + }); + expect(result).toBe(path.resolve(badDir)); + }); + + it("returns resolved path when user confirms in interactive mode", async () => { + const { confirm } = await import("@clack/prompts"); + vi.mocked(confirm).mockResolvedValueOnce(true); + const result = await confirmProjectRoot(validProject, { + nonInteractive: false, + command: "install", + }); + expect(result).toBe(path.resolve(validProject)); + expect(confirm).toHaveBeenCalledWith( + expect.objectContaining({ message: expect.stringContaining(validProject) }), + ); + }); + + it("warns and defaults to refusing an ambiguous directory in interactive mode", async () => { + const { confirm } = await import("@clack/prompts"); + vi.mocked(confirm).mockResolvedValueOnce(false); + const badDir = await mkdtemp(path.join(tmpdir(), "ak-root-")); + await expect( + confirmProjectRoot(badDir, { nonInteractive: false, command: "update" }), + ).rejects.toThrow(RootRefusedError); + expect(confirm).toHaveBeenCalledWith(expect.objectContaining({ initialValue: false })); + }); + + it("throws RootRefusedError when user declines", async () => { + const { confirm } = await import("@clack/prompts"); + vi.mocked(confirm).mockResolvedValueOnce(false); + await expect( + confirmProjectRoot(validProject, { nonInteractive: false, command: "update" }), + ).rejects.toThrow(RootRefusedError); + }); + + it("throws RootRefusedError on cancel", async () => { + const { confirm, isCancel } = await import("@clack/prompts"); + const cancelSymbol = Symbol.for("cancel"); + vi.mocked(confirm).mockResolvedValueOnce(cancelSymbol as unknown as boolean); + vi.mocked(isCancel).mockReturnValueOnce(true); + await expect( + confirmProjectRoot(validProject, { nonInteractive: false, command: "install" }), + ).rejects.toThrow(RootRefusedError); + }); +}); diff --git a/packages/cli/src/utils/terminal.ts b/packages/cli/src/utils/terminal.ts new file mode 100644 index 0000000..af04926 --- /dev/null +++ b/packages/cli/src/utils/terminal.ts @@ -0,0 +1,161 @@ +/** + * Terminal environment detection helpers. + * Used by install and init commands to handle non-interactive terminals + * (piped stdin, CI, VS Code output panels without TTY). + */ + +import { homedir } from "node:os"; +import path from "node:path"; +import { confirm, isCancel } from "@clack/prompts"; +import { fileExists } from "./fs.js"; + +/** True when stdin is not a TTY or CI env vars are set. */ +export function isNonInteractive(): boolean { + if (process.env.CI === "true" || process.env.CI === "1") return true; + if (process.env.AGENT_KIT_YES === "1") return true; + return !process.stdin.isTTY; +} + +export interface ConfirmProjectRootOptions { + nonInteractive: boolean; + command: "install" | "update"; + /** Bypass the ambiguous-root guard. */ + forceRoot?: boolean; +} + +/** Validate that a resolved path looks like a project root, not a global directory. */ +export async function validateProjectRoot( + resolved: string, +): Promise<{ ok: true } | { ok: false; reason: string }> { + const home = path.resolve(homedir()); + if (resolved === "/" || resolved === home) { + return { ok: false, reason: `Refused to use ${resolved} as a project root.` }; + } + const hasGit = await fileExists(path.join(resolved, ".git")); + const hasManifest = await fileExists(path.join(resolved, ".cursor", "agent-kit.json")); + if (!hasGit && !hasManifest) { + return { + ok: false, + reason: `Refused ${resolved}: no .git and no .cursor/agent-kit.json. Run from a project directory or use --force-root.`, + }; + } + return { ok: true }; +} + +/** + * Confirm the project root before any L0 writes. + * In interactive mode: prompt the user to confirm the absolute path. + * In non-interactive mode (--yes / CI): validate the path and refuse ambiguous roots. + * Returns the resolved absolute path, or throws RootRefusedError if the user refuses. + */ +export async function confirmProjectRoot( + cwd: string, + opts: ConfirmProjectRootOptions, +): Promise<string> { + const resolved = path.resolve(cwd); + if (opts.forceRoot) { + return resolved; + } + + const validation = await validateProjectRoot(resolved); + if (!validation.ok) { + if (opts.nonInteractive) { + throw new RootRefusedError(resolved, validation.reason); + } + // Interactive mode still confirms, but warns and defaults to refusing. + const ok = await confirm({ + message: `${validation.reason} Proceed anyway?`, + initialValue: false, + }); + if (isCancel(ok) || !ok) { + throw new RootRefusedError(resolved, validation.reason); + } + return resolved; + } + + if (opts.nonInteractive) { + return resolved; + } + + const ok = await confirm({ + message: `${opts.command === "install" ? "Install" : "Update"} Agent Kit in: ${resolved}`, + initialValue: true, + }); + if (isCancel(ok) || !ok) { + throw new RootRefusedError(resolved); + } + return resolved; +} + +export class RootRefusedError extends Error { + constructor( + public readonly root: string, + reason?: string, + ) { + super(reason ?? `Refused to write into ${root}. Re-run from the correct project directory.`); + this.name = "RootRefusedError"; + } +} + +export interface InstallErrorHint { + kind: "eperm" | "registry-auth" | "network" | "unknown"; + message: string; + recovery: string; +} + +export function classifyInstallError(err: unknown): InstallErrorHint { + const msg = err instanceof Error ? err.message : String(err); + const code = (err as NodeJS.ErrnoException)?.code; + + if (code === "EPERM" || code === "EACCES" || /EPERM|EACCES/.test(msg)) { + return { + kind: "eperm", + message: `Permission error: ${msg}`, + recovery: [ + "The npm cache may have ownership drift (root-written files in a user dir).", + "Recovery options:", + " 1. npx --cache .npm-cache @dadado/agent-kit-cli install", + " 2. npm cache clean --force && npx @dadado/agent-kit-cli install", + " 3. Use Port B fallback: drag install.md into the Cursor chat", + ].join("\n"), + }; + } + + if (/403|Forbidden|unauthorized|E401|ENEEDAUTH/.test(msg)) { + return { + kind: "registry-auth", + message: `Registry access denied: ${msg}`, + recovery: [ + "The npm registry returned 403/401 for the scoped package.", + "Recovery options:", + " 1. Check npm auth: npm whoami (login if needed: npm login)", + " 2. If using a private registry, verify .npmrc scope config", + " 3. Use Port B fallback: drag install.md into the Cursor chat", + ].join("\n"), + }; + } + + if ( + /ENOTFOUND|ETIMEDOUT|ECONNREFUSED|ECONNRESET|EAI_AGAIN|fetch failed/.test(msg) || + code === "ENOTFOUND" || + code === "ETIMEDOUT" + ) { + return { + kind: "network", + message: `Network error: ${msg}`, + recovery: [ + "Could not reach the registry or git remote.", + "Recovery options:", + " 1. Check network/proxy/VPN settings", + " 2. Retry: npx @dadado/agent-kit-cli install", + " 3. Use --registry <local-path> if you have a local checkout", + ].join("\n"), + }; + } + + return { + kind: "unknown", + message: msg, + recovery: "Unexpected error. Check the message above and retry, or use Port B fallback.", + }; +} diff --git a/packages/cli/src/welcome/help-groups.ts b/packages/cli/src/welcome/help-groups.ts new file mode 100644 index 0000000..71f9700 --- /dev/null +++ b/packages/cli/src/welcome/help-groups.ts @@ -0,0 +1,114 @@ +/** + * Grouped root --help for agent-kit (citty lists commands flat by default). + */ + +import type { ArgsDef, CommandDef } from "citty"; +import { bold, gray, underline } from "kolorist"; +import { KIT_VERSION } from "../lifecycle/version.js"; +import { shouldUseWelcomeColor } from "./screen.js"; + +export type HelpGroupId = "setup" | "mission" | "dashboard" | "integrity" | "other"; + +export interface HelpGroup { + id: HelpGroupId; + title: string; + commands: string[]; +} + +/** Command → group assignment for root help. Chat-only slash flows are omitted. */ +export const CLI_HELP_GROUPS: HelpGroup[] = [ + { + id: "setup", + title: "SETUP", + commands: ["init", "install", "doctor", "status", "update", "add", "scan"], + }, + { + id: "mission", + title: "MISSION", + commands: ["handoff", "run-plan"], + }, + { + id: "dashboard", + title: "DASHBOARD", + commands: ["dashboard", "dashboard-broadcast", "monitors"], + }, + { + id: "integrity", + title: "INTEGRITY", + commands: ["validate", "guard", "hook", "cursor-awareness", "diff", "contribute"], + }, +]; + +/** Resolve citty `Resolvable<CommandMeta>` (function | Promise | plain object). */ +async function resolveCommandMeta( + meta: CommandDef["meta"] | undefined, +): Promise<{ name?: string; version?: string; description?: string } | undefined> { + if (meta == null) return undefined; + const value = typeof meta === "function" ? await meta() : meta; + return await Promise.resolve(value); +} + +async function resolveSubMeta(subCommands: Record<string, unknown>): Promise<Map<string, string>> { + const out = new Map<string, string>(); + for (const [name, raw] of Object.entries(subCommands)) { + const resolved = typeof raw === "function" ? await raw() : raw; + const def = resolved as CommandDef | undefined; + const meta = await resolveCommandMeta(def?.meta); + out.set(name, meta?.description ?? ""); + } + return out; +} + +/** Root help text with SETUP / MISSION / DASHBOARD / INTEGRITY groupings. */ +export async function renderGroupedRootHelp<T extends ArgsDef = ArgsDef>( + cmd: CommandDef<T>, +): Promise<string> { + const color = shouldUseWelcomeColor(); + const u = (s: string) => (color ? underline(bold(s)) : s); + const g = (s: string) => (color ? gray(s) : s); + const meta = await resolveCommandMeta(cmd.meta); + const name = meta?.name ?? "agent-kit"; + const version = meta?.version ?? KIT_VERSION; + const description = + meta?.description ?? "HITL framework for AI-assisted IDEs (Mission Kit family)"; + + const subCommands = (await (typeof cmd.subCommands === "function" + ? cmd.subCommands() + : cmd.subCommands)) as Record<string, unknown> | undefined; + const descriptions = subCommands ? await resolveSubMeta(subCommands) : new Map<string, string>(); + + const placed = new Set(CLI_HELP_GROUPS.flatMap((grp) => grp.commands)); + const leftover = [...descriptions.keys()].filter((c) => !placed.has(c)).sort(); + const groups = [...CLI_HELP_GROUPS]; + if (leftover.length > 0) { + groups.push({ id: "other", title: "OTHER", commands: leftover }); + } + + const lines: string[] = [ + g(`${description} (${name} v${version})`), + "", + `${u("USAGE")} \`${name} <command> [OPTIONS]\``, + "", + ]; + + for (const group of groups) { + const rows = group.commands.filter((c) => descriptions.has(c)); + if (rows.length === 0) continue; + lines.push(u(group.title), ""); + const width = Math.max(...rows.map((c) => c.length)); + for (const c of rows) { + const pad = " ".repeat(width - c.length + 2); + lines.push(` ${c}${pad}${descriptions.get(c) ?? ""}`); + } + lines.push(""); + } + + lines.push( + g(`Use \`${name} <command> --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.", + ), + "", + ); + return lines.join("\n"); +} diff --git a/packages/cli/src/welcome/screen.test.ts b/packages/cli/src/welcome/screen.test.ts new file mode 100644 index 0000000..b766a6c --- /dev/null +++ b/packages/cli/src/welcome/screen.test.ts @@ -0,0 +1,149 @@ +import { defineCommand } from "citty"; +import { afterEach, describe, expect, it } from "vitest"; +import { CLI_HELP_GROUPS, renderGroupedRootHelp } from "./help-groups.js"; +import { + HELMET_ACCENT, + HELMET_FILL, + HELMET_OUTLINE, + hasCliSubcommand, + renderHelmetAscii, + renderWelcomeScreen, + shouldUseWelcomeColor, +} from "./screen.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("welcome color gate", () => { + 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, + }; + + afterEach(() => { + for (const [k, v] of Object.entries(prev)) { + setEnv(k, v); + } + }); + + it("disables color when NO_COLOR is set", () => { + setEnv("NO_COLOR", "1"); + clearEnv("CI"); + clearEnv("FORCE_COLOR"); + expect(shouldUseWelcomeColor({ stdoutIsTTY: true, color: undefined })).toBe(false); + }); + + it("disables color in CI even on TTY", () => { + clearEnv("NO_COLOR"); + setEnv("CI", "true"); + expect(shouldUseWelcomeColor({ stdoutIsTTY: true })).toBe(false); + }); + + it("treats any non-empty CI as CI", () => { + clearEnv("NO_COLOR"); + setEnv("CI", "yes"); + expect(shouldUseWelcomeColor({ 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); + }); + + it("allows color on TTY when env permits", () => { + clearEnv("NO_COLOR"); + clearEnv("CI"); + clearEnv("FORCE_COLOR"); + clearEnv("NODE_DISABLE_COLORS"); + expect(shouldUseWelcomeColor({ stdoutIsTTY: true })).toBe(true); + }); +}); + +describe("renderWelcomeScreen", () => { + it("prints Mission Kit chrome and agent-kit technical ids (plain)", () => { + const out = renderWelcomeScreen({ version: "9.9.9", color: false }); + expect(out).toContain("Mission Kit"); + expect(out).toContain("agent-kit v9.9.9"); + expect(out).toContain("@dadado/agent-kit-cli"); + expect(out).toContain("agent-kit doctor"); + expect(out).toContain("agent-kit dashboard"); + expect(out).toContain("HITL"); + expect(out.includes("\u001b")).toBe(false); + }); + + it("includes helmet ASCII frame lines", () => { + const helmet = renderHelmetAscii(false); + expect(helmet).toContain(".-'"); + expect(helmet.split("\n").length).toBeGreaterThanOrEqual(8); + }); + + it("exports brand token hex constants", () => { + expect(HELMET_OUTLINE).toMatch(/^#/); + expect(HELMET_FILL).toBe("#0C8DEB"); + expect(HELMET_ACCENT).toBe("#00D0E7"); + }); + + it("applies HELMET_OUTLINE trueColor to outline lines in colored helmet", () => { + const [r, g, b] = [ + Number.parseInt(HELMET_OUTLINE.slice(1, 3), 16), + Number.parseInt(HELMET_OUTLINE.slice(3, 5), 16), + Number.parseInt(HELMET_OUTLINE.slice(5, 7), 16), + ]; + const seq = `\u001b[38;2;${r};${g};${b}m`; + const colored = renderHelmetAscii(true); + expect(colored).toContain(seq); + expect(colored.split("\n")[0]).toContain(seq); + }); +}); + +describe("renderGroupedRootHelp", () => { + it("groups SETUP / MISSION / DASHBOARD / INTEGRITY", async () => { + const cmd = defineCommand({ + meta: { + name: "agent-kit", + version: "1.2.3", + description: "test", + }, + subCommands: { + init: defineCommand({ meta: { name: "init", description: "Init help" } }), + handoff: defineCommand({ meta: { name: "handoff", description: "Handoff help" } }), + dashboard: defineCommand({ meta: { name: "dashboard", description: "Dash help" } }), + validate: defineCommand({ meta: { name: "validate", description: "Val help" } }), + }, + }); + const text = await renderGroupedRootHelp(cmd); + expect(text).toContain("SETUP"); + expect(text).toContain("MISSION"); + expect(text).toContain("DASHBOARD"); + expect(text).toContain("INTEGRITY"); + expect(text).toContain("init"); + expect(text).toContain("Chat-only HITL"); + for (const g of CLI_HELP_GROUPS) { + expect(g.commands.length).toBeGreaterThan(0); + } + }); +}); + +describe("hasCliSubcommand", () => { + it("treats bare invoke and flag-only argv as welcome paths", () => { + expect(hasCliSubcommand([])).toBe(false); + expect(hasCliSubcommand(["--help"])).toBe(false); + expect(hasCliSubcommand(["--version"])).toBe(false); + expect(hasCliSubcommand(["--nope"])).toBe(false); + }); + + it("detects a subcommand token", () => { + expect(hasCliSubcommand(["status"])).toBe(true); + expect(hasCliSubcommand(["doctor", "--help"])).toBe(true); + }); +}); diff --git a/packages/cli/src/welcome/screen.ts b/packages/cli/src/welcome/screen.ts new file mode 100644 index 0000000..d2b1ed9 --- /dev/null +++ b/packages/cli/src/welcome/screen.ts @@ -0,0 +1,145 @@ +/** + * Bare `agent-kit` welcome chrome (ASCII helmet + utility hints). + * Separate from run-plan persona banners under plan-loop/. + */ + +import { + blue, + cyan, + gray, + options as koloristOptions, + lightCyan, + trueColor, + white, +} from "kolorist"; +import { KIT_VERSION } from "../lifecycle/version.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"; + +/** kolorist SupportLevel.TrueColor — needed so trueColor() emits when TTY probes say none (CI). */ +const KOLORIST_TRUECOLOR = 3; + +const HELMET_ASCII = [ + " ____", + " .-' '-.", + " / .--. \\", + " | / \\ |", + " | | () | |", + " | \\ / |", + " \\ '--' /", + " '-.____.-'", + " /_/ \\_\\", +]; + +/** Parse `#rrggbb` for trueColor helmet outline (contract pin for HELMET_OUTLINE). */ +export function hexToRgb(hex: string): [number, number, number] { + const m = /^#([0-9a-fA-F]{6})$/.exec(hex.trim()); + const hexBody = m?.[1]; + if (!hexBody) throw new Error(`invalid hex color: ${hex}`); + const n = Number.parseInt(hexBody, 16); + return [(n >> 16) & 255, (n >> 8) & 255, n & 255]; +} + +function outlineAnsi(line: string): string { + const [r, g, b] = hexToRgb(HELMET_OUTLINE); + return trueColor(r, g, b)(line); +} + +/** Run `fn` with kolorist forced on at trueColor support (restores prior options). */ +function withKoloristColor<T>(fn: () => T): T { + const prevEnabled = koloristOptions.enabled; + const prevLevel = koloristOptions.supportLevel; + koloristOptions.enabled = true; + koloristOptions.supportLevel = KOLORIST_TRUECOLOR; + try { + return fn(); + } finally { + koloristOptions.enabled = prevEnabled; + koloristOptions.supportLevel = prevLevel; + } +} + +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(() => + HELMET_ASCII.map((line, i) => { + // Outline (top/bottom / cheek straps) uses HELMET_OUTLINE via trueColor. + if (i <= 1 || i >= HELMET_ASCII.length - 1) return outlineAnsi(line); + if (i === 4) return lightCyan(line); + return blue(line); + }).join("\n"), + ); +} + +/** Print-and-exit utility hints (not an interactive menu). */ +export const WELCOME_UTILITY_HINTS = [ + { cmd: "agent-kit --help", hint: "grouped commands" }, + { cmd: "agent-kit doctor", hint: "repository readiness" }, + { cmd: "agent-kit status", hint: "installed kit version" }, + { cmd: "agent-kit dashboard", hint: "Mission Control panel" }, + { cmd: "agent-kit init", hint: "guided install entry" }, +] as const; + +export function renderWelcomeScreen(opts: WelcomeRenderOptions = {}): string { + const version = opts.version ?? KIT_VERSION; + const color = shouldUseWelcomeColor(opts); + const title = color ? white("Mission Kit") : "Mission Kit"; + const product = color ? cyan("agent-kit") : "agent-kit"; + const muted = (s: string) => (color ? gray(s) : s); + const lines = [ + renderHelmetAscii(color), + "", + `${title} · ${product} v${version}`, + muted("HITL framework for AI-assisted IDEs · @dadado/agent-kit-cli"), + "", + muted("Try:"), + ...(() => { + const width = Math.max(...WELCOME_UTILITY_HINTS.map(({ cmd }) => cmd.length)); + return WELCOME_UTILITY_HINTS.map(({ cmd, hint }) => { + const pad = " ".repeat(width - cmd.length + 2); + const left = color ? cyan(` ${cmd}`) : ` ${cmd}`; + return `${left}${pad}${muted(hint)}`; + }); + })(), + "", + muted( + "Chat HITL (start-project, git-staging/prod, run-plan-all) stays in Cursor slash commands.", + ), + ]; + return `${lines.join("\n")}\n`; +} + +export function printWelcomeScreen(opts: WelcomeRenderOptions = {}): void { + process.stdout.write(renderWelcomeScreen(opts)); +} diff --git a/scripts/sync-cli-dashboard.mjs b/scripts/sync-cli-dashboard.mjs index a9484be..9c53e69 100644 --- a/scripts/sync-cli-dashboard.mjs +++ b/scripts/sync-cli-dashboard.mjs @@ -19,4 +19,4 @@ if (!existsSync(src)) { rmSync(dest, { recursive: true, force: true }); cpSync(src, dest, { recursive: true }); -console.log(`Synced ${src} -> ${dest}`); +console.error(`Synced ${src} -> ${dest}`); diff --git a/scripts/verify-cli-dashboard-pack.mjs b/scripts/verify-cli-dashboard-pack.mjs index bdd9d9c..b1f316f 100644 --- a/scripts/verify-cli-dashboard-pack.mjs +++ b/scripts/verify-cli-dashboard-pack.mjs @@ -30,6 +30,10 @@ const cliDir = join(repoRoot, "packages", "cli"); export const REQUIRED_DASHBOARD = [ "package/dashboard/start.mjs", "package/dashboard/start-broadcast.mjs", + "package/dashboard/open.html", + "package/dashboard/lib/broadcast-share.mjs", + "package/dashboard/lib/guards.mjs", + "package/dashboard/lib/open-browser.mjs", ]; /** Case-insensitive README path under an extracted pack root. */