diff --git a/.cursor-plugin/plugin.json b/.cursor-plugin/plugin.json index 4454e00..698c407 100644 --- a/.cursor-plugin/plugin.json +++ b/.cursor-plugin/plugin.json @@ -15,7 +15,7 @@ "anti-slop" ], "license": "PolyForm-Noncommercial-1.0.0", - "version": "5.6.0", + "version": "5.7.0", "homepage": "https://github.com/agent-kit-startup/agent-kit", "repository": "https://github.com/agent-kit-startup/agent-kit", "logo": "dashboard/logo-marketplace.svg", diff --git a/.cursor/agent-kit.json b/.cursor/agent-kit.json index 2d42b09..6442883 100644 --- a/.cursor/agent-kit.json +++ b/.cursor/agent-kit.json @@ -1,7 +1,8 @@ { "schemaVersion": 1, - "version": "5.6.0", + "version": "5.7.0", "protected": [ + ".claude/commands/agent-kit.md", ".cursor/HANDOFF.md", ".cursor/agents/test-suites.md", ".cursor/context/backups/**", @@ -15,12 +16,17 @@ ".cursor/skills/community/cursor-skills-node/SKILL.md", ".cursor/skills/core/docs-repo/SKILL.md", "AGENTS.md", + "CLAUDE.md", "templates/CODEOWNERS", "templates/gitlab-ci-content.yml", "templates/gitlab-ci-docker.yml", "templates/gitlab-ci-firebase.yml" ], "profile": "default", + "packs": [ + "devops", + "quality" + ], "skills": [ "clean-code", "clickup", @@ -32,19 +38,15 @@ "sql-postgres", "ux-message-flows" ], - "registry": { - "url": "https://github.com/agent-kit-startup/agent-kit-dev", - "ref": "staging" - }, - "packs": [ - "devops", - "quality" - ], "personalization": { "contractVersion": 1, - "generatorVersion": "4.5.1", + "generatorVersion": "5.4.0", "origin": "repository-profile", "resultPath": ".cursor/context/personalization.json" }, - "installedAt": "2026-07-25T12:09:19.292Z" + "registry": { + "url": "https://github.com/agent-kit-startup/agent-kit-dev", + "ref": "staging" + }, + "installedAt": "2026-08-20T17:28:41.571Z" } diff --git a/.cursor/commands/dogfood.md b/.cursor/commands/dogfood.md index 9c4b7ca..300e4c0 100644 --- a/.cursor/commands/dogfood.md +++ b/.cursor/commands/dogfood.md @@ -32,7 +32,7 @@ Without arguments, summarize the current chat turn into the topic and body. 1. **Detect lane before writing.** - **Factory lane** — this checkout is the canonical `agent-kit-dev` repository (`origin` remote contains `agent-kit-dev`, or `dogfood/` already exists at repo root). Write to `dogfood/cursor__.md` and update `dogfood/README.md` Unprocessed index. - - **Consumer lane** — any other project with an Agent Kit install (`.cursor/agent-kit.json` exists). Write to `.cursor/dogfood/cursor__.md` and a local index. Do **not** track the folder in git; it should already be gitignored by the base install. + - **Consumer lane** — any other project with an Agent Kit install (`.cursor/agent-kit.json` exists). Write to `.cursor/dogfood/cursor__.md` and a local index. Do **not** track the folder in git; `agent-kit install`/`doctor` gitignores `.cursor/dogfood/` via the kit-owned `.gitignore` merge (run `agent-kit doctor` if an older install predates this and the folder isn't yet ignored). - **Unknown lane** — stop and ask the operator which lane to use. 2. **Hygiene strip (mandatory).** Remove before writing: - Consumer workspace names, project names, domain names, or external product names. diff --git a/.cursor/commands/git-prod.md b/.cursor/commands/git-prod.md index b597aff..5763a66 100644 --- a/.cursor/commands/git-prod.md +++ b/.cursor/commands/git-prod.md @@ -11,7 +11,7 @@ Follow the **git prod** routine to promote `origin/staging` to `origin/main` (pr 1. **Read** the "Prompt: git prod" section in `autogit/gitupdate.md` (when it exists in the project). 2. Critical validation: no uncommitted local changes; never commit directly to main. -3. Close the release in the CHANGELOG (move `[Unreleased]` to a dated version) before merging. +3. Close the release in the CHANGELOG (move `[Unreleased]` to a dated version) before merging. Keep `` fences. Public excerpt: `node scripts/public-changelog.mjs --version ` (GitHub Release body) and `--blurb` (landing `--notes`). 4. Show a summary of the changes (diff/log) between staging and main and **ask for explicit confirmation using Ask questions tool** before merging and pushing to main. Options: `Proceed with production deploy` / `Review changes first` / `Cancel` diff --git a/.cursor/commands/kit-prod.md b/.cursor/commands/kit-prod.md new file mode 100644 index 0000000..5ef3c68 --- /dev/null +++ b/.cursor/commands/kit-prod.md @@ -0,0 +1,92 @@ +--- +name: kit-prod +description: Run the git-prod routine, then update and promote the public landing only when this promotion includes a product release. +--- + +# Kit prod + +Wraps native **git prod**. Does not replace `/git-prod`. Native `/git-prod` promotes the repo (`staging` → `main`, tag, npm) and must not put the landing on the air by itself. This bundle may promote the landing after git prod, only when a release changelog actually changed. + +**Runs in the main window by default.** Do not dispatch this command to a Task subagent by default. + +## 1. Git prod HITL (do not steal) + +Follow the **Prompt: git prod** section in `autogit/gitupdate.md` and `.cursor/commands/git-prod.md`. Do not copy that routine into this file. + +Before merge or push to `main`, Ask questions with the **same** git-prod labels. Do not rename them, skip them, or fold landing into this Ask. + +Options: `Proceed with production deploy` / `Review changes first` / `Cancel` + +**Fallback:** if Ask questions is unavailable, present the same labels as one numbered list and wait. + +`Cancel` or skipped: stop. No merge, no landing. + +`Review changes first`: stop after the summary; wait for a later confirm. + +`Proceed with production deploy`: continue the git-prod routine (close release, merge, authorized main push, tag, public sync, post-prod verification). + +## 2. Decide whether landing work applies + +Only after git prod **succeeds**. Landing scripts must exist (`pnpm landing:promote`). + +Run `node scripts/kit-landing-gate.mjs --mode prod`. Same rules if the script is missing: + +**Run the landing path when:** + +- This promotion closed a CHANGELOG version (moved `[Unreleased]` to `## [X.Y.Z]`) or cut a product `v*` tag / manifest bump. +- `scripts/lib/public-changelog.mjs` `extractPublicReleaseNotes` / `latestClosedPublicRelease` has product-facing bullets, not only kit-memory monitors. + +**Skip landing (do not Ask a second time) when any of:** + +- Git prod did not run (`Cancel` / `Review changes first`). +- No landing scripts in `package.json`. +- No product release changelog in this promotion (`docs(memory):` / `chore(kit):` only). +- The gate prints `"needed": false`. + +Repo-only production stays native `/git-prod`. + +## 3. Landing HITL (extra confirm) + +Ask questions. This Ask is **in addition to** the git-prod Ask, not a replacement. Fallback: numbered list, wait; skip or cancel means stop. + +Options: + +- `Promote landing to production` +- `Skip landing (repo only)` +- `Cancel` + +`Cancel` or skipped: stop. Repo is already on `main`; do not revert git prod. + +`Skip landing (repo only)`: stop. Report skipped. + +`Promote landing to production`: continue. + +## 4. Surgical update + promote (this monorepo) + +Do not hand-edit landing markup, CSS, or JS. Do not invent a second updater. The only stamp is `pnpm landing:update-release` (`scripts/update-landing-release.mjs`). + +**Fields stamped** (existing canvas nodes, not a second render path): + +- Version: `a[data-release-version]` inside `p.ak-brand` (pill "New release") becomes text `Mission Kit X.Y.Z`, href `https://github.com/agent-kit-startup/agent-kit/releases/tag/vX.Y.Z` (public repo only). +- Notes: `div[data-changelog-content]` becomes a short public blurb in a single `

`. + +**Fail-closed (do not pass these):** `CHANGELOG.md` as `--notes-file`; Keep-a-Changelog headers (`## [`); empty notes; notes over 1200 characters; private `agent-kit-dev` release URLs. + +**Sync trap:** `landing:sync` overwrites `remote/` wholesale. Re-run `landing:update-release` after every Design zip or `landing:build:check` fails. + +1. **Public excerpt.** `node scripts/public-changelog.mjs --version --blurb` (stdout = `publicNotesBlurb` for `--notes`). `--json` `notes` is GitHub Release body; `publicNotesBlurb` is landing. Gate `stamp.notes` is the same blurb. Never flatten by hand. +2. **Stamp (does not deploy).** Exact CLI: + +```bash +NOTES=$(node scripts/public-changelog.mjs --version --blurb) +pnpm landing:update-release -- --version --notes "$NOTES" +pnpm landing:update-release -- --version --notes-file ./public-release-notes.txt +pnpm landing:update-release -- --version --notes "$NOTES" --dry-run +``` + +Version is the closed release / `stamp.version`. Optional `--dry-run` first. +3. **Build.** `pnpm landing:build` so `dist/` matches the stamp. This is the field-update step, not promote. +4. **Promote.** `pnpm landing:promote`. The promote script itself must not rebuild (existing invariant). Do not use `landing:deploy:staging` as a substitute for promote. +5. Update `.cursor/HANDOFF.md` ("promoted to production"; landing URL if promoted). + +Never run the landing half from `/run-plan` or `/run-plan-all`. Those commands must not steal `/git-prod` HITL. diff --git a/.cursor/commands/kit-staging.md b/.cursor/commands/kit-staging.md new file mode 100644 index 0000000..e498fc4 --- /dev/null +++ b/.cursor/commands/kit-staging.md @@ -0,0 +1,81 @@ +--- +name: kit-staging +description: Run the git-staging routine, then update and deploy the public landing to staging only when this change includes a product changelog or release. +--- + +# Kit staging + +Wraps native **git staging**. Does not replace `/git-staging`. Native `/git-staging` stays git-only and must not deploy the landing. + +**Runs in the main window by default.** Do not dispatch this command to a Task subagent by default. + +## 1. Git staging (SoT) + +Follow the **Prompt: git staging** section in `autogit/gitupdate.md` (same contract as `/git-staging`). Do not copy that routine into this file. + +Also honor `.cursor/commands/git-staging.md` (monitor hygiene, lint evidence, Evidence-checks merge gate). + +## 2. Decide whether landing work applies + +Landing steps run only when this repo has `pnpm landing:deploy:staging`. Consumers without those scripts: stop after git staging; report `landing skipped: no-landing-scripts`. + +Run `node scripts/kit-landing-gate.mjs --mode staging` (prints `{ needed, reason, stamp }`). Same rules if the script is missing: + +**Run the landing path when all of:** + +- Git staging completed (merged to `origin/staging`, or the project's pre-prod branch). +- Public `[Unreleased]` has at least one product-facing bullet (CLI, slash commands, dashboard, landing, npm, consumer-visible behavior), after `scripts/lib/public-changelog.mjs` (`toPublicChangelog` / Unreleased section). +- This change is not only a `docs(memory):` / `chore(kit):` monitor, Audits row, or HANDOFF-only commit. + +**Skip landing (do not Ask) when any of:** + +- No landing scripts in `package.json`. +- `[Unreleased]` was left empty because git staging found no significant product change. +- The only remaining public bullets are kit-memory (plan-monitors, Audits index rows, session HANDOFF). +- The gate prints `"needed": false`. + +Repo-only shipping stays native `/git-staging`. + +## 3. Landing HITL (only if landing-worthy) + +Ask questions. Fallback: one numbered list, wait; skip or cancel means stop. + +Options: + +- `Deploy landing to staging` +- `Skip landing (repo only)` +- `Cancel` + +`Cancel` or skipped: stop. Do not deploy. Git staging already landed; do not revert it. + +`Skip landing (repo only)`: stop after git staging. Report skipped. + +`Deploy landing to staging`: continue. + +## 4. Surgical landing update (this monorepo) + +Do not hand-edit landing markup, CSS, or JS. Do not invent a second updater. The only stamp is `pnpm landing:update-release` (`scripts/update-landing-release.mjs`). + +**Fields stamped** (existing canvas nodes, not a second render path): + +- Version: `a[data-release-version]` inside `p.ak-brand` (pill "New release") becomes text `Mission Kit X.Y.Z`, href `https://github.com/agent-kit-startup/agent-kit/releases/tag/vX.Y.Z` (public repo only). +- Notes: `div[data-changelog-content]` becomes a short public blurb in a single `

`. + +**Fail-closed (do not pass these):** `CHANGELOG.md` as `--notes-file`; Keep-a-Changelog headers (`## [`); empty notes; notes over 1200 characters; private `agent-kit-dev` release URLs. + +**Sync trap:** `landing:sync` overwrites `remote/` wholesale. Re-run `landing:update-release` after every Design zip or `landing:build:check` fails. + +1. **Public excerpt.** `node scripts/public-changelog.mjs --version Unreleased --blurb` (stdout = `publicNotesBlurb` for `--notes`). Same field as gate `stamp.notes`. `--json` `notes` is GitHub Release body, not landing. Never flatten by hand. Not Hostinger tokens, not CI internals, not plan-monitor notes. +2. **Stamp (does not deploy).** Exact CLI: + +```bash +NOTES=$(node scripts/public-changelog.mjs --version Unreleased --blurb) +pnpm landing:update-release -- --version --notes "$NOTES" +pnpm landing:update-release -- --version --notes-file ./public-release-notes.txt +pnpm landing:update-release -- --version --notes "$NOTES" --dry-run +``` + +Version is `package.json` / `stamp.version`. Optional `--dry-run` first. +3. **Build, then staging deploy.** `pnpm landing:build`, then `pnpm landing:deploy:staging`. Never `pnpm landing:promote` from this command. +4. Optional: `pnpm landing:verify:staging`. +5. Update `.cursor/HANDOFF.md`. Never `/git-prod` from this command. diff --git a/.cursor/commands/run-plan.md b/.cursor/commands/run-plan.md index 82aa60d..7f2f07d 100644 --- a/.cursor/commands/run-plan.md +++ b/.cursor/commands/run-plan.md @@ -152,6 +152,7 @@ After a findings-contract (or `review-*`) tick returns findings, read `.cursor/c 1. Mark the tick's to-do as `completed` in the plan's frontmatter 2. Update `.cursor/HANDOFF.md` (include `Mode: run-plan ()`). HITL claims (parked, approved, deferred, confirmed, stopped-by-operator) must record Ask id, operator reply, or `agent-inferred`. 3. **Cadence ledger:** run `.cursor/scripts/field-report-cadence-bump.sh tick` (increments the gitignored Field Report activity counter; may open a cadence warning when threshold + unreviewed work). Never commit the ledger. ADR: `2026-07-27_field-report-activity-review-cadence.md`. +3a. **Context-usage cap (Cursor only):** if this tick's session observed a `preCompact` hook firing (a `Context compacting (...)` message), the emitted `user_message` already tells you whether it crossed the `<=50%` per-agent target (ADR `decisions/2026-08-24_agent-context-usage-cap-window-pressure.md`): a message containing "forced checkpoint" means treat the HANDOFF update in step 2 as **mandatory before continuing to the next tick**, not optional bookkeeping. No live per-tick poll exists — this is reactive to an observed firing, not a proactive scan. Claude Code sessions have no `preCompact` signal at all (Cursor-only hook; see the ADR's coverage-gap note) — nothing to check there. 4. If `git status` has commitable changes: run the `/git-staging` routine **without asking for confirmation** (authorized by this command), following `autogit/gitupdate.md` inventory → theme-bucket → ship. Do **not** skip because the diff is "just" HANDOFF or memory: versioned HANDOFF, `plan-monitor-*.md`, and `_index.md` Audits rows are a `docs(memory):` / `chore(kit):` bucket (own commit when the product theme differs). **Monitor hygiene:** warn on untracked or unrelated dirty `.cursor/memory/plan-monitor-*.md`; stage **add-by-name only**; never broad `git add` of `.cursor/memory/` WIP into a product commit. Unrelated-plan monitors still get a kit/memory bucket in this staging run; they do not ride inside the product commit. 1 MR/PR -> staging branch -> merge. 5. No commitable diff (working tree clean except gitignored paths and hard excludes): just HANDOFF + plan status diff --git a/.cursor/context/templates/handoff.md b/.cursor/context/templates/handoff.md index 66224f1..84483be 100644 --- a/.cursor/context/templates/handoff.md +++ b/.cursor/context/templates/handoff.md @@ -2,7 +2,7 @@ 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. +**Mid-batch monitor pointers are not durable here.** `.cursor/HANDOFF.md` is gitignored session state — `agent-kit install`/`doctor` merges this entry into `.gitignore` (kit-owned ignore patterns; see `packages/cli/src/scanner/detect-repository.ts`). 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. ### HITL provenance (required) diff --git a/.cursor/context/templates/plan-fable-copy-prompt.md b/.cursor/context/templates/plan-fable-copy-prompt.md new file mode 100644 index 0000000..1244e3c --- /dev/null +++ b/.cursor/context/templates/plan-fable-copy-prompt.md @@ -0,0 +1,50 @@ +# Fable copy-writer prompt + +This is **not** a findings audit. Ignore any launcher wrapper that asks for a findings-contract, PASS/GAP/FAIL tables, Still open residuals, advisor escalate, or `/plan-review-triage` closeout. Do not invent commands, hosts, or IDE parity. + +## Role + +You are writing usage-first documentation for Mission Kit / Agent Kit. Follow the plan's `## Fable copy brief` and `## Technical fact sheet` as the only fact sources. Do not treat README, HANDOFF, or claim matrices as proof. + +## Read + +1. The plan file named in the launcher prompt (`## Fable copy brief`, `## Technical fact sheet`). +2. Do not start an audit of completed to-dos. + +## Write + +Create or refresh `.cursor/memory/plan-monitor-docs-usage-first-anti-slop-revamp.md`. + +Near the top, include exactly one HTML comment line so `--wait-monitor` can succeed: + +`` + +(use `updated` instead of `created` if the file already existed). + +Return copy under these headings only (subheads under them are allowed): + +``` +## Consumer copy +### README +### getting-started +## Contributor copy +### CONTRIBUTING +``` + +## Voice + +- Senior open-source maintainer. Short sentences. Concrete commands. +- No inventory dumps, no "layer" taxonomy, no claim matrices, no "comprehensive ecosystem". +- No em dash as a sentence connector. +- Dual-name: Mission Kit for the product; Agent Kit for npm/CLI/slash; Mission Control only for the dashboard/TUI. + +## Forbidden + +- Do not edit README, `docs/getting-started.md`, `docs/CONTRIBUTING.md`, or other product files. +- Do not rewrite the naming glossary. +- Do not `/git-prod`. +- Do not append an Audits findings table. + +## Index + +If you create the monitor, add one Audits row to `.cursor/memory/_index.md` in the same pass (add-by-name later; do not broad git add). diff --git a/.cursor/rules/cursor-skills-git-workflow.mdc b/.cursor/rules/cursor-skills-git-workflow.mdc index e840ca4..b2aa8a5 100644 --- a/.cursor/rules/cursor-skills-git-workflow.mdc +++ b/.cursor/rules/cursor-skills-git-workflow.mdc @@ -16,6 +16,8 @@ Agent Kit treats this flow as the agents' **operational memory**: plan -> handof |---------|-------|------------------------------|-----| | `git staging` | `/git-staging` | `origin/staging` | Bring local changes to staging (working branch, commit, MR, merge). | | `git prod` | `/git-prod` | `origin/main` | Promote approved staging to production. | +| `kit staging` | `/kit-staging` | `origin/staging` + optional landing staging | Git-staging wrap, then landing staging deploy only when a product changelog or release changed. | +| `kit prod` | `/kit-prod` | `origin/main` + optional landing promote | Git-prod wrap (same HITL), then landing promote only when this promotion includes a release. Native `/git-prod` does not put the site on the air. | Legacy projects may name the pre-prod branch `homologacao`, `develop`, etc.; the **two-step pattern** stays the same and the canonical name is `staging`. See the decision in `.cursor/memory/decisions/` (branch naming). diff --git a/.cursor/rules/hitl-ask-questions.mdc b/.cursor/rules/hitl-ask-questions.mdc index 15c30fd..0812cb1 100644 --- a/.cursor/rules/hitl-ask-questions.mdc +++ b/.cursor/rules/hitl-ask-questions.mdc @@ -33,6 +33,8 @@ Agent Kit uses **Ask questions** tool (`AskQuestion` / ACP `cursor/ask_question` | `/backlog-cancel` | Confirm soft cancel (`Cancel [plan-file] on backlog` / `Keep on backlog`) | | `/continue-plan` | Confirm next `[to-do-id]`; if multiple plans, pick which to resume; when Unprocessed is non-empty, inbox Ask (`Analyze inbox now` / `Enqueue Fix now` / `Not now`) before the next-unit Ask | | `/git-prod` | Explicit confirm before merge/push to `main` | +| `/kit-staging` | After git staging, if landing-worthy: `Deploy landing to staging` / `Skip landing (repo only)` / `Cancel`. Skip the Ask when the landing gate says skip. | +| `/kit-prod` | Keep the git-prod Ask first (`Proceed with production deploy` / `Review changes first` / `Cancel`). Do not steal that Ask. After git prod succeeds, if landing-worthy: extra Ask `Promote landing to production` / `Skip landing (repo only)` / `Cancel`. | | `/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. When Unprocessed is non-empty (and this is not a `/run-plan-all` per-plan Task), inbox Ask (`Analyze inbox now` / `Enqueue Fix now` / `Not now`) before the first to-do. Audits pre-flight per `externalPlanReview.preflight`. Plan-exhausted audits arm when enabled (`mode: autonomous` → `--force --autonomous --wait-monitor`; `paste` → paste-only); else `offerOnExhausted` Ask (`Run review now` / `Always enable automatic` / `Not now`); AwaitShell until exit `0|3|4` (fresh monitor; chat slice `waitSliceSeconds` ~90s; remaining budget in `.cursor/context/audit-wait/`; not spawn-only); `backend: "auto"` uses Claude or Cursor Agent; exit `0` → `/plan-review-triage` Ask (not Final HANDOFF "after monitor lands"); chat never silent headless `--force`; must not steal `/git-prod` HITL (Ask after Final HANDOFF / prod suggestion) | | `/run-plan-all` | When Unprocessed is non-empty, inbox Ask (`Analyze inbox now` / `Enqueue Fix now` / `Not now`) before the 6-way confirm. 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. | diff --git a/.cursor/scripts/plan-external-review-model-routing.test.mjs b/.cursor/scripts/plan-external-review-model-routing.test.mjs index f2c07b4..5161a8f 100644 --- a/.cursor/scripts/plan-external-review-model-routing.test.mjs +++ b/.cursor/scripts/plan-external-review-model-routing.test.mjs @@ -1,6 +1,6 @@ import assert from "node:assert/strict"; import { spawnSync } from "node:child_process"; -import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { existsSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { dirname, join, resolve } from "node:path"; import { test } from "node:test"; @@ -174,3 +174,70 @@ echo "$(effective_reviewer_model)" assert.equal(cursorNamed.status, 0, cursorNamed.stderr); assert.equal(cursorNamed.stdout.trim(), "composer-2.5-fast"); }); + +test("launcher --prompt-file overrides TEMPLATE_REL; default remains the findings template", () => { + assert.match( + SRC, + /DEFAULT_TEMPLATE_REL="\.cursor\/context\/templates\/plan-external-review-prompt\.md"/, + ); + assert.match(SRC, /TEMPLATE_REL="\$DEFAULT_TEMPLATE_REL"/); + assert.match(SRC, /--prompt-file requires PATH/); + assert.match(SRC, /INTERACTIVE_FLAGS\+=\(--prompt-file "\$TEMPLATE_REL"\)/); + assert.match(SRC, /echo " {2}prompt-file: \$TEMPLATE_REL"/); + const findings = join(repoRoot, ".cursor/context/templates/plan-external-review-prompt.md"); + assert.equal(existsSync(findings), true, "do not replace the L0 findings template in place"); + assert.match(readFileSync(findings, "utf8"), /post-hoc evidence-based monitoring/); + + const missingArg = spawnSync("bash", [SCRIPT, "--force", "--prompt-file"], { encoding: "utf8" }); + assert.equal(missingArg.status, 2); + assert.match(missingArg.stderr, /--prompt-file requires PATH/); +}); + +const COPY_PLAN = "docs-usage-first-anti-slop-revamp.plan.md"; +const COPY_PROMPT = ".cursor/context/templates/plan-fable-copy-prompt.md"; + +test( + "dry-run prints default prompt-file; override forwards --prompt-file on paste-cmd", + { + skip: + !existsSync(join(repoRoot, ".cursor/plans", COPY_PLAN)) && + "plan file absent (gitignored session state)", + }, + () => { + const def = spawnSync( + "bash", + [SCRIPT, "--force", "--autonomous", "--wait-monitor", "--dry-run", COPY_PLAN], + { encoding: "utf8", cwd: repoRoot }, + ); + assert.equal(def.status, 0, def.stderr); + assert.match( + def.stdout, + /prompt-file: \.cursor\/context\/templates\/plan-external-review-prompt\.md/, + ); + assert.doesNotMatch(def.stdout, /paste-cmd:.*--prompt-file/); + + const ov = spawnSync( + "bash", + [ + SCRIPT, + "--force", + "--autonomous", + "--wait-monitor", + "--prompt-file", + COPY_PROMPT, + "--dry-run", + COPY_PLAN, + ], + { encoding: "utf8", cwd: repoRoot }, + ); + assert.equal(ov.status, 0, ov.stderr); + assert.match( + ov.stdout, + /prompt-file: \.cursor\/context\/templates\/plan-fable-copy-prompt\.md/, + ); + assert.match( + ov.stdout, + /paste-cmd:.*--prompt-file \.cursor\/context\/templates\/plan-fable-copy-prompt\.md/, + ); + }, +); diff --git a/.cursor/scripts/plan-external-review.sh b/.cursor/scripts/plan-external-review.sh index 2922814..68e8697 100755 --- a/.cursor/scripts/plan-external-review.sh +++ b/.cursor/scripts/plan-external-review.sh @@ -66,6 +66,8 @@ # cloud is an opt-in pin only (Cursor Cloud Agents over REST); it is never in the # auto cascade. cursor reviews the working tree; cloud reviews the PUSHED branch. # --reviewer-model NAME: reviewer model id (default sonnet / config reviewerModel). +# --prompt-file PATH: repo-relative reviewer prompt markdown. Default remains +# .cursor/context/templates/plan-external-review-prompt.md (findings audit). # --advisor-model NAME: escalate-only advisor (default opus / config advisorModel). # --implementer-model NAME: stamp the model that shipped the tick (default auto / # AGENT_KIT_AUDIT_IMPLEMENTER_MODEL). Same-family reviewer is refused. @@ -184,7 +186,8 @@ set -euo pipefail ROOT="$(cd "$(dirname "$0")/../.." && pwd)" CONFIG="$ROOT/.cursor/context/config.json" -TEMPLATE_REL=".cursor/context/templates/plan-external-review-prompt.md" +DEFAULT_TEMPLATE_REL=".cursor/context/templates/plan-external-review-prompt.md" +TEMPLATE_REL="$DEFAULT_TEMPLATE_REL" HANDOFF_REL=".cursor/HANDOFF.md" PLANS_DIR="$ROOT/.cursor/plans" LAUNCHER_REL=".cursor/scripts/plan-external-review.sh" @@ -428,7 +431,7 @@ if [[ "${AGENT_KIT_AUDIT_GC_WAIT_STATE:-}" == "1" || "${AGENT_KIT_AUDIT_GC_WAIT_ fi usage() { - sed -n '2,181p' "$0" | sed 's/^# \{0,1\}//' + sed -n '2,183p' "$0" | sed 's/^# \{0,1\}//' } while [[ $# -gt 0 ]]; do @@ -533,6 +536,14 @@ while [[ $# -gt 0 ]]; do REVIEWER_MODEL_EXPLICIT=1 shift 2 ;; + --prompt-file) + if [[ $# -lt 2 || -z "${2:-}" ]]; then + echo "error: --prompt-file requires PATH" >&2 + exit 2 + fi + TEMPLATE_REL="$2" + shift 2 + ;; --advisor-model) if [[ $# -lt 2 || -z "${2:-}" ]]; then echo "error: --advisor-model requires NAME" >&2 @@ -2794,6 +2805,9 @@ fi if [[ -n "${ADVISOR_MODEL:-}" ]]; then INTERACTIVE_FLAGS+=(--advisor-model "$ADVISOR_MODEL") fi +if [[ "$TEMPLATE_REL" != "$DEFAULT_TEMPLATE_REL" ]]; then + INTERACTIVE_FLAGS+=(--prompt-file "$TEMPLATE_REL") +fi enforce_implementer_reviewer_split if [[ "$BATCH" -eq 1 ]]; then @@ -2834,6 +2848,7 @@ if [[ "$BATCH" -eq 1 ]]; then echo " reviewer-model: ${WAIT_REVIEWER_MODEL:-unresolved}" echo " implementer-model: ${WAIT_IMPLEMENTER_MODEL:-auto}" echo " advisor-model: ${ADVISOR_MODEL:-opus}" + echo " prompt-file: $TEMPLATE_REL" echo " same-model-refuse: $([[ "$SAME_MODEL_REFUSE" -eq 1 ]] && echo yes || echo no)" echo " midBatchAudits: $MID_BATCH_AUDITS" echo " autoRemediate: $AUTO_REMEDIATE" @@ -2941,6 +2956,7 @@ echo " reviewer-backend: ${REVIEWER_BACKEND:-unresolved}" echo " reviewer-model: ${WAIT_REVIEWER_MODEL:-unresolved}" echo " implementer-model: ${WAIT_IMPLEMENTER_MODEL:-auto}" echo " advisor-model: ${ADVISOR_MODEL:-opus}" +echo " prompt-file: $TEMPLATE_REL" echo " same-model-refuse: $([[ "$SAME_MODEL_REFUSE" -eq 1 ]] && echo yes || echo no)" echo " midBatchAudits: $MID_BATCH_AUDITS" echo " autoRemediate: $AUTO_REMEDIATE" diff --git a/.cursor/skills/community/clickup/SKILL.md b/.cursor/skills/community/clickup/SKILL.md index 6dcb202..9b4c62e 100644 --- a/.cursor/skills/community/clickup/SKILL.md +++ b/.cursor/skills/community/clickup/SKILL.md @@ -7,7 +7,7 @@ category: pm # ClickUp - Task management (optional stack) -**Not part of the Core Pack.** Install/use only when the repository integrates ClickUp. Aligned with rule `.cursor/rules/cursor-skills-clickup.mdc` (`alwaysApply: false`). +**Not part of the Core Pack.** Install/use only when the repository integrates ClickUp. Aligned with rule [cursor-skills-clickup.mdc](../../../rules/cursor-skills-clickup.mdc) (`alwaysApply: false`). ## When to Use diff --git a/.cursor/skills/community/n8n-workflows/SKILL.md b/.cursor/skills/community/n8n-workflows/SKILL.md index d2aa415..9579769 100644 --- a/.cursor/skills/community/n8n-workflows/SKILL.md +++ b/.cursor/skills/community/n8n-workflows/SKILL.md @@ -1,6 +1,6 @@ --- name: n8n-workflows -description: Workflows n8n: only when the project uses n8n (stack, not Core Pack). Edit workflow JSON or when the user mentions n8n. +description: Workflows n8n — only when the project uses n8n (stack, not Core Pack). Edit workflow JSON or when the user mentions n8n. version: 0.1.0 category: integrations --- diff --git a/CHANGELOG.md b/CHANGELOG.md index 533cc1c..de49255 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,24 +4,42 @@ All notable changes to this project are documented in this file. The format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/). +**Public vs private:** the private factory tree may keep a full developer log. Public surfaces (public GitHub `CHANGELOG.md`, GitHub Release notes, landing product notes) publish only consumer and contributor product notes. Fence factory-only bullets (CI/CD internals, landing staging/promote, Hostinger/deploy plumbing, internal ADR chatter, dogfood-only mechanics) in `` ... `` blocks, or under a `### Internal` heading. Public-sync strips those fences. Preview: `node scripts/public-changelog.mjs`. Landing stamp: `node scripts/public-changelog.mjs --version X.Y.Z --blurb` then `pnpm landing:update-release -- --version X.Y.Z --notes ""`. Never pass `CHANGELOG.md` as `--notes-file`. + --- ## [Unreleased] +## [5.7.0] - 2026-08-27 + +### Changed + +- Consumer install copy is Cursor-first: root `package.json`, README eyebrow, and getting-started no longer list VS Code or Windsurf as peer IDEs. Partial generators still ship; Action A7 (generator parity) stays a non-goal. +- Mission Kit landing shows the current public release version (New release pill plus a link to the public GitHub Release) and short product notes in the existing changelog field, matching the original pill-and-frame visual. Release version and product notes are static page fields, not a third-party feed or a factory-changelog dump. Operators stamp those two fields with `pnpm landing:update-release` (does not deploy). +- Consumer documentation rewritten with usage-first Fable copy. README hero section, getting-started guide, and CONTRIBUTING page now focus on concrete install steps, surfaces, and day-to-day workflow. Retired five-layer claim matrix from consumer storefront; factory evidence files preserved for CI. + +### Added + +- **Public changelog filter:** public GitHub `CHANGELOG.md`, GitHub Release notes, and the landing product-notes field publish only consumer and contributor product notes. Factory-only bullets stay behind `` fences. Preview with `node scripts/public-changelog.mjs`; landing stamp uses `--version X.Y.Z --blurb` (never pass `CHANGELOG.md` as `--notes-file`). +- **`/kit-staging` and `/kit-prod`** wrap native `/git-staging` and `/git-prod` (those stay git-only: branch/MR for staging, `staging` → `main` plus tag and npm for prod). When a product changelog or release actually changed, the kit bundles add a surgical public-landing field update and, after an explicit Ask, deploy the landing to staging or promote it to production. `docs(memory)` monitor commits do not trigger a landing deploy. Repo-only shipping stays the native git commands. +- **CLI-native Mission Control TUI** (`agent-kit mission-control`): browser-free ASCII view of Mission, Flight Log, Checklist, and Crew Monitor. Reuses `dashboard/dashboard-data.mjs` / `semantic-model.mjs` without starting the HTTP server; live refresh uses the existing `PERIODIC_REFRESH_MS` poll. Visual-kit tokens (`HELMET_*` / `LABEL_MUTED`) and TTY / `NO_COLOR` / `CI` / `AGENT_KIT_REDUCED_MOTION` gates stay as in ADR `2026-08-13_cli-visual-kit-space-chrome.md`. ADR `2026-08-27_cli-native-mission-control-tui-thin-deps-third-surface.md` records stay-inside-thin-deps (`@clack/prompts` / `citty` / `kolorist` only; no Ink/ora/chalk/figlet) and that the TUI is a **third surface**, not a Path C replacement. Claude Code `/agent-kit` renders `mission-control --once` (one-shot; not a live loop). Plan `cli-native-mission-control-tui`. +- Mission Control **Git tab visual tree + new DevOps tab**. The Git tab's existing markdown `git log --graph` block stays as-is; a new state-colored vertical tree renders next to it, reusing the `.now-stepper`/`.now-step-marker` timeline primitive built for the Current Mission panel (parses `SNAPSHOT.git.graph`'s commit lines, flattened to one row per commit — HEAD/promoted-to-`origin/main`-or-`origin/staging` state-colored, degrading to a compact empty-state on an unparseable graph). New **DevOps** tab (`#devops`, nav dot, hash-deep-linkable) is scoped to CI/CD + deploy signal, distinct from the existing local-process-only Processes tab: a pipeline card from a new `dashboard-data.mjs` `gh run list` collector (budget-guarded, fails soft to an honest empty-state when `gh` is unavailable/unauthenticated or the run itself would blow the snapshot's soft time budget), and a best-effort "what shipped recently" deploy-activity card from `v*` git tags + the latest non-`Unreleased` `CHANGELOG.md` entry — deliberately not a live infra/hosting poll, and says so. `dashboardDataVersion` 1.3.0 -> 1.4.0. Plan `mc-git-tab-visual-tree-and-devops-panel`. +- **Per-agent/subagent context-usage cap**, docs + enforcement: ADR `2026-08-24_agent-context-usage-cap-window-pressure.md` sets a `<=50%`-of-window target using the existing `context_usage_percent` signal from the Cursor `preCompact` hook — the only real local window-pressure signal, reused exactly as-is (no new token/cost telemetry, no violation of the standing `2026-08-14_mc-idle-session-token-cost-no-go.md` no-go). Documents the operator's "190k tokens" framing as a window-size-class ratio (200K-class ~95%, 1M-class ~19% — hence the enforceable cap is a percentage, not a literal token count), and the coverage gap that `preCompact` is Cursor-only (no signal exists for Claude Code sessions today). `packages/cli/src/hooks/pre-compact.ts` gained `CONTEXT_USAGE_CAP_PERCENT = 50` and threshold logic in `buildPreCompactUserMessage`: a `preCompact` firing at/above the cap (or with no reported percentage — auto-compaction implies high pressure) escalates the emitted `user_message` to name the cap and force a HANDOFF checkpoint before continuing; below-cap firings keep the original gentle wording. `.cursor/commands/run-plan.md`'s tick-close contract gained step 3a treating an observed escalated firing as a mandatory (not optional) HANDOFF checkpoint. New `packages/cli/src/hooks/pre-compact.test.ts` (5 tests). Also surveyed 5 real third-party token-reduction/context-compression tools (Headroom, Caveman, Graphify, `atlassian-labs/mcp-compressor`, Tokalator) for scam/legitimacy risk — all rated `evaluate-further` (no confirmed malicious signal in any, but three show anomalous star-velocity worth independent scrutiny before reliance); none installed or adopted. Plan `agent-context-cap-and-token-tool-research`. + +### Fixed + +- DevOps tab pipeline card: `collectPipelineRuns()`'s `gh run list` timeout (`GH_RUN_LIST_TIMEOUT_MS`) had no real headroom over this repo's observed latency — 3500ms against live-measured samples spanning 2.1-9.9s across sessions (8 fresh samples this pass: 3476-6584ms, one 7674ms outlier). Fixed in dependency order: (1) the collector now runs **last** among the snapshot's budget-guarded collectors (after processes, detached-audit-sessions, agentPrompts, externalReports, subagentRuns), not ahead of them, so a slow `gh` network call can no longer starve the local-only collectors sharing `SNAPSHOT_BUDGET_MS`; call cost trimmed too (`--limit` 10 -> 5, unused `databaseId`/`url` `--json` fields dropped — neither is read by the card). (2) `GH_RUN_LIST_TIMEOUT_MS` raised 3500ms -> 12000ms with real margin over the measured range; `SNAPSHOT_BUDGET_MS` default raised by the same delta (12000ms -> 20500ms) so the larger reserve the collector needs is actually available. (3) The pipeline card's empty-state copy no longer unconditionally blames "gh is unavailable or not authenticated" — the collector now returns a `reason` (`"budget"` when the shared snapshot budget ran out before `gh` was even attempted, `"call-failed"` when `gh` was attempted and failed/timed out) and the card copy follows it. Live 5-run + 1-diagnostic-budget verification (this sandboxed dev environment) found the dominant real failure mode is in fact `"budget"`: a separate, out-of-scope collector (`SNAPSHOT.git`, ~10 unguarded `execSync` calls with no budget check) is consuming the entire default budget on its own here, before the snapshot even reaches the now-last-placed pipeline collector — confirmed via a diagnostic run with `AGENT_KIT_DASHBOARD_DATA_BUDGET_MS=60000`, which returns `available: true` with real runs. That collector is unguarded/unbudgeted and outside this fix's scope; noted as a residual. Plan `close-mc-git-tab-devops-pipeline-timeout-residuals`. + ## [5.6.0] - 2026-08-24 ### Added -- Mission Kit landing gains a **staging.missionkit.io hop before every production deploy**: `pnpm landing:deploy:staging` (per-file upload into a `staging/` subdomain directory, never the production root), `pnpm landing:verify:staging` (headless-Chrome acceptance gate, reused as `scripts/verify-landing.mjs --url `), and a Phase-3 HITL gate (operator compares staging against the Claude Design canvas) before `pnpm landing:promote` re-deploys the *exact* staging-validated `dist/` bytes to `missionkit.io` — never rebuilding — with `pnpm landing:rollback [release]` restoring a prior archived production artifact in one command. New `scripts/lib/hostinger.mjs` reimplements the Hostinger REST calls the prior interactive-MCP deploy used, standalone (zero new dependencies). Staging noindex ships as a generated `staging/.htaccess` + `robots.txt`, not a `dist/` toggle, so promote's identical-bytes invariant can't accidentally noindex production. **Live-verified in production 2026-08-23**: staged, operator-approved, and promoted to `missionkit.io` for real, byte-identical DOM confirmed on both hosts. Plan `missionkit-staging-promote`. -- `scripts/build-landing.mjs` gains a **build-time changelog box injector**: it reads `CHANGELOG.md`'s latest public-facing (non-`[Unreleased]`) release entry and injects a plain-text (markdown decoration stripped, not converted — so truncation can never leak an unclosed token), HTML-escaped preview into a canvas-provided `

` container. The container does not exist in the current Mission Kit Landing canvas export yet, so this step currently no-ops with a logged warning; it is the repo-side half of replacing the Footer CTA's terminal-typer animation with a real changelog box. Canvas-side change (plus the hero eyebrow → live Mission Kit release badge, scoped to the one authorized `img.shields.io` live-request exception) is a pending hand-off: `.cursor/context/landing-missionkit/UPSTREAM-DESIGN-FIX-PROMPT-badge-changelog.md`. Confirmed edit route (this account has no owned/shared write access to that canvas) and the scoped self-containment exception are recorded in ADR `2026-08-05_landing-external-design-source-of-record.md` (2026-08-22 addendum). Plan `landing-eyebrow-live-badge-and-changelog-box`. - `registry/registry.json`'s L0 section is now **generated from code** (`packages/cli/src/lifecycle/l0.ts`'s `L0_ARTIFACTS`) on every `scripts/build-registry.mjs` run, instead of hand-curated — reviving the abandoned `archive/l0-generate-from-code` tag against the current pipeline. New `scripts/derive-l0-artifacts.ts` + `scripts/emit-l0-artifacts.mjs` (via `pnpm --filter @dadado/agent-kit-cli exec tsx`, since `tsx` isn't hoisted to the repo root). Verified idempotent (two consecutive rebuilds produce zero diff) and non-regressive on hand-curated L2 entries (`readCuratedArtifacts` narrowed to `layer === "L2"` only — L0 no longer needs preserving-by-hand). Closed two real drift gaps surfaced along the way: `dashboard-broadcast`/`update` commands added to `L0_ARTIFACTS` (existed on disk, were never declared), and `install.md`'s Port B table reconciled to match. `l0.test.ts` deliberately keeps its portable "legacy `onboard` absent" assert rather than a strict `registryL0Paths === canonicalSources` equality — private-repo parity is already enforced by CI's repo-gated "Registry catalog parity" step, and the public mirror independently owns `registry/**`, so a second unconditional test-level assert would reintroduce the public-CI break `errors/2026-07-24_public-sync-ci-test-portability.md` already paid down twice. Plan `l0-generate-from-code-revival`. - Public storefront README gets the visual identity it never had: a shields badge row (npm version, PolyForm NC license, Node >=20, public Release Latest — no CI badge, the public mirror pipeline skips factory tests), a helmet hero (`dashboard/logo.svg`, visual-kit palette) plus the CLI's 9-line `HELMET_ASCII` as a monochrome fenced block, a curated 2×2 Mission Control screenshot gallery (`assets/production/*.png`, newly synced), and a GitHub social preview asset (`dashboard/social-preview.svg`/`.png`, 1280×640, helmet on the `#0b0e14` plate per the marketplace logo spec) with an apply checklist in `docs/github-about.md`. `packages/cli/README.md` (the actual npm storefront) gets minimal badge/hero parity. Plan `readme-public-hero-badges`. -- **DeepSeek Harness (`dsh`) modernization study**, docs-only: ADR `2026-08-22_deepseek-harness-reject-adapt-concepts-docs.md` ranks 12 deepseek-harness concepts against Agent Kit surfaces (`/run-plan` tick contract, Task worker dispatch, HITL Ask questions, memory-loop) — 6 reject, 4 already covered kit-native, 1 adapt-concepts (docs-awareness only: a durable per-step replayable event log is a real gap but stays unadopted to avoid a second state system alongside markdown HANDOFF/memory), 1 thin-adapter note (a second, unrelated ACP surface). No `dsh`/Cordis dependency, no plugin-composition runtime, no relaxation of sequential one-Task-per-plan dispatch. No incorporation/modernization work ships from this study; any uptake of the two non-reject items is deferred to a separately-scoped future plan. Plan `deepseek-harness-modernization-study` (Phase 0/1; Phase 2 is this changelog closeout). ### Fixed - Audits `monitor_wants_advisor()` (`.cursor/scripts/plan-external-review.sh:1734`) used an unanchored `grep -qF` substring match on the `` sentinel, so any prose *mention* of it — including a negation like "no `` needed" — false-positived and re-spawned a costly Opus advisor pass on every future audit run touching that monitor. Now anchored to a standalone HTML-comment line (`grep -qE '^[[:space:]]*[[:space:]]*$'`), matching the prompt template's own "exactly one HTML comment line" contract. Verified against both known false positives (`plan-monitor-audits-atomic-wait-reviewer-fallback.md`, `plan-monitor-deepseek-harness-modernization-study.md:79`), which no longer match; a genuine standalone sentinel line still triggers escalation. Regression test added (`plan-external-review-model-routing.test.mjs`). Found by an Opus advisor pass during the 2026-08-22 `deepseek-harness-modernization-study` audit; done-marker suppression (Phase 2) considered and skipped as unsafe — see plan body. Plan `audits-advisor-escalate-sentinel-anchor-fix`. -- `scripts/deploy-landing-staging.mjs`'s DNS-record step raced Hostinger's own auto-provisioning: creating a new subdomain already auto-adds an `ALIAS` record for it, but the script immediately re-checked the zone and, seeing it not yet propagated, tried to add a conflicting `CNAME` — a real `HTTP 422` (`RRset ... IN ALIAS must not be used with CNAME on the same name`) on the first live production run. Now skips the explicit DNS step entirely when this run just created the subdomain; only checks/adds on the branch where the subdomain already existed. Found live 2026-08-23, plan `missionkit-staging-promote`. - `check-public-deny-links.mjs` failed closed only on an explicit manifest `!exclude`; a README/docs link into a path the manifest never mentions at all (neither included nor excluded) passed silently and 404'd on the public mirror. Now checks `isPublicSyncedPath` directly, so "absent from the manifest" fails too. Also fixes two by-products of the stricter check rather than suppressing them: template `{placeholder}` interpolations (`isPlaceholderTarget`, matching the existing `check-markdown-links.mjs` convention) and a real folder link in a `.mdc` rule file whose trailing-slash target needs restoring before it matches the `git-hooks/**` glob. Broad Intake 2026-08-16 error finding, plan `readme-public-hero-badges`. - `sync-public.mjs`'s content-denylist scan read every synced file via `readFileSync(path, "utf8")` and relied on a try/catch to skip binaries — but that read never throws on binary bytes in Node, so the intended binary exemption never actually applied. Latent since the manifest never included a binary file before; surfaced as a false positive (a private-client-name denylist term coincidentally present in a screenshot's compressed PNG bytes) the moment `assets/production/*.png` was added. Fixed with a real binary check (NUL byte in the first 8000 bytes, the same heuristic `git`/`grep -I` use) ahead of the UTF-8 decode + regex scan. diff --git a/README.md b/README.md index 98ab5d7..59f1009 100644 --- a/README.md +++ b/README.md @@ -11,37 +11,57 @@ [Watch the demo on YouTube](https://www.youtube.com/watch?v=9mrAg6Mczfg) · [missionkit.io](https://missionkit.io) -**Development operations built into Cursor and VS Code.** +**Mission Kit** puts a working spine under your AI coding agent: plans with to-dos, human confirmation gates, and a staging-then-prod git flow. You describe the goal. The kit writes a plan, runs it one unit at a time, and never promotes to production without your explicit yes. -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). +Ships as **Agent Kit** on npm (`@dadado/agent-kit-cli`). -Formerly **agent-kit**. Same toolkit, clearer product name. +## Install + +From your project root: + +```bash +npx @dadado/agent-kit-cli install +``` + +Node.js 20+. Git is recommended; the staging and prod routines depend on it. Pin a version (`@x.y.z`) when you need a reproducible install. Non-interactive: add `-y` and `--yes`. -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. +`npx` is ephemeral, so bare `agent-kit` will not be on your PATH yet. If you hit `command not found`, run: + +```bash +npx @dadado/agent-kit-cli setup-global +``` + +or keep prefixing commands with `npx @dadado/agent-kit-cli`. + +## Pick your surface + +- **Cursor.** The first-class surface. Slash commands land in `.cursor/commands/`: `/start-project`, `/continue-plan`, `/run-plan`, `/git-staging`, `/git-prod`, `/dashboard` and more. Confirmations use Cursor's Ask questions. +- **Claude Code.** Run the kit-load with `--claude`, then `CLAUDE.md` plus `/agent-kit` give you the same contracts. Confirmations fall back to numbered lists. +- **Terminal.** `npx @dadado/agent-kit-cli mission-control` renders Mission Control as a live TUI in your terminal. `--once` prints a single frame and exits. Shipped in 5.6.0. +- **Browser.** `npx @dadado/agent-kit-cli dashboard` serves Mission Control on `127.0.0.1`. LAN sharing is opt-in and token-gated (`dashboard-broadcast`). + +Mission Kit is Cursor-first. VS Code and Windsurf get partial config generators, not full parity. + +## A normal day + +``` +/start-project describe a goal, approve the plan, approve the first unit +/continue-plan confirm the next to-do, ship one unit, stop +/run-plan run the active plan to the end or until blocked +/backlog-add queue a plan for later without activating it +/git-staging branch, PR, merge to staging +/git-prod staging to main, only after you say yes +``` -## Why you'd want it +Production promotion is never automatic. The kit asks; you answer. -- **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. +## License -## What you get +PolyForm Noncommercial 1.0.0. Commercial licensing: sales@missionkit.io. -| 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). | +## Contribute -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). +Skills, docs fixes, and CLI patches are welcome. Start at `docs/CONTRIBUTING.md`. ## Mission Control @@ -58,43 +78,6 @@ A local dashboard over the same workspace state the CLI drives — current missi -## Install - -### In Cursor (recommended) - -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) · Raw: https://raw.githubusercontent.com/agent-kit-startup/agent-kit/main/install-prompt.md - -### In the terminal - -From your project root: - -```bash -npx @dadado/agent-kit-cli install -``` - -The CLI welcome screen greets you with the same helmet, in ASCII: - -```text - ____ - .-' '-. - / .--. \ - | / \ | - | | () | | - | \ / | - \ '--' / - '-.____.-' - /_/ \_\ -``` - -Unpinned `npx` resolves to the latest publish. Pin when you need a reproducible install: `npx @dadado/agent-kit-cli@x.y.z install`. - -That installs slash commands and a small set of rules into the project. Walkthrough: [docs/getting-started.md](docs/getting-started.md). ## Usage @@ -140,7 +123,6 @@ More: [Getting started - Mission Control](docs/getting-started.md#mission-contro | Guide | What's in it | |-------|--------------| | [Getting started](docs/getting-started.md) | Install, commands, day-to-day workflow | -| [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 | diff --git a/autogit/gitupdate.md b/autogit/gitupdate.md index 37f61c9..3ff12d2 100644 --- a/autogit/gitupdate.md +++ b/autogit/gitupdate.md @@ -14,6 +14,10 @@ plan → /handoff → git staging → git prod → memory |---------|---------------|----------------------------|-------------| | `git staging` | `/git-staging` | `origin/staging` | Updates the staging branch with local changes | | `git prod` | `/git-prod` | `origin/main` | Promotes `origin/staging` → `origin/main` (production) after approval | +| `kit staging` | `/kit-staging` | `origin/staging`, then optional landing staging | Git-staging routine, then landing field update + `landing:deploy:staging` only when a product changelog or release changed | +| `kit prod` | `/kit-prod` | `origin/main`, then optional landing promote | Git-prod routine (same HITL), then landing field update + `landing:promote` only when this promotion includes a release | + +**Bundles vs native:** `/git-staging` and `/git-prod` remain the SoT for git-only work. They do not deploy the public landing. `/kit-staging` and `/kit-prod` wrap those prompts, then may update and deploy the landing when a product changelog or release actually changed (not on every `docs(memory)` monitor commit). Repo-only shipping stays `/git-staging` / `/git-prod`. Command SoT: `.cursor/commands/kit-staging.md`, `.cursor/commands/kit-prod.md`. In legacy projects the pre-prod branch may be called `homologacao`, `develop`, etc. The **two-step pattern** is fixed; the canonical name in Agent Kit is **`staging`**. @@ -184,6 +188,8 @@ or - **`git staging`:** add bullets only in `[Unreleased]`. - **`git prod`:** before merging `staging → main`, **close the release** - move everything from `[Unreleased]` to `## [YYYY.MM.DD] - YYYY-MM-DD` (today) or SemVer version and leave `[Unreleased]` empty. Set root and `packages/cli` `package.json` `"version"` to that same SemVer. Never promote with Unreleased full. +**Public excerpt:** keep `` fences when closing a release. Public GitHub, GitHub Releases, and the landing product-notes field receive the stripped consumer/contributor notes only (`node scripts/public-changelog.mjs`). Landing stamp uses `--version X.Y.Z --blurb`, never `CHANGELOG.md` as `--notes-file`. + --- ## ⚠️ Important Warnings @@ -334,6 +340,18 @@ This section contains the detailed prompts that should be followed when commands > ### Whenever I type `git prod` in the chat, follow exactly the routine below to promote changes from `origin/staging` to `origin/main` (production): +**Claude CLI lane — known blockers, read once before running (saves retries):** this repo's private `agent-kit-dev` has a `Protect main and staging` ruleset (PR-only on both branches) *plus* the Claude Code auto-mode permission classifier blocks several of the commands below outright. Full detail and recurrence history in `.cursor/memory/errors/2026-08-14_git-prod-private-main-requires-pr.md` and `.cursor/memory/errors/2026-07-24_public-sync-pr-merge-blocked-ruleset.md`. The short version: + +| Command | Expect it to work? | +|---|---| +| `ALLOW_MAIN_PUSH=1 git push origin main` (step 9) | **No.** Blocked by both the classifier and the GitHub ruleset. Don't spend a turn on it — go straight to `gh pr create --base main --head staging`. | +| Any direct `git push origin staging` (e.g. closing the release) | **No.** Same ruleset covers `staging`. Commit on a fresh branch, PR to `staging` instead. | +| `gh pr merge` (any repo, any PR — staging→main, feature→staging, public sync) | **No.** The classifier refuses this every time in this lane. One attempt is enough to log; treat the merge as operator-owed immediately rather than retrying. | +| `gh pr merge` additionally erroring `head branch is not up to date with base branch` | A second, distinct GitHub check — `main` accumulates a merge-commit SHA per past release that `staging` doesn't contain as a direct ancestor (structural, not a content conflict; step 7 always uses a real merge). Surface this explicitly when merging staging→main; the operator may need `--admin` or a UI squash-merge to clear it. | +| `gh pr create`, `gh release create`, `git push origin ` | **Yes**, these are not classifier-blocked — safe to run directly. | + +Net effect: budget for exactly two operator-owed merges per `/git-prod` run (staging-close PR, then staging→main PR), plus a third if the public sync PR (step 12) also needs one — everything else in this routine is agent-doable. + #### 1. **CRITICAL Security Validation** - Run `git status -sb` to check modified, staged files and current branch. - **CRITICAL BLOCK**: diff --git a/dashboard/README.md b/dashboard/README.md index 81838f1..4637b96 100644 --- a/dashboard/README.md +++ b/dashboard/README.md @@ -23,3 +23,20 @@ before it fans out to the workspace packages). `dashboard.html` is outside Biome's scope; CSS/HTML-only changes are covered by `packages/cli/src/dashboard/plugin-ux-validation.test.ts` instead. + +## Git tab and DevOps tab + +The Git tab (`#git`) keeps the pre-rendered `git log --graph` markdown block and adds a +second, state-colored visual tree next to it, reusing the `.now-stepper`/`.now-step-marker` +timeline component built for the Current Mission panel (`renderGitVisualTree` in +`dashboard.html`, parsing `SNAPSHOT.git.graph`). It is single-lane by design — the markdown +block keeps the true branch-lane geometry; the stepper trades that for an at-a-glance +promotion read (HEAD, promoted to `origin/main`/`origin/staging`, or neither). + +The DevOps tab (`#devops`) is scoped to CI/CD + deploy signal, separate from the +local-process-only Processes tab. `dashboard-data.mjs`'s `collectPipelineRuns()` shells to +`gh run list` (budget-guarded via `withinSnapshotBudget()`, fails soft to an honest +empty-state when `gh` is unavailable/unauthenticated); `collectDeploySignal()` reads `v*` +git tags plus the latest non-`Unreleased` `CHANGELOG.md` entry as a best-effort "what +shipped recently" proxy. Neither collector polls live infra/hosting — the DevOps tab never +implies monitoring it does not perform. diff --git a/dashboard/dashboard-data.mjs b/dashboard/dashboard-data.mjs index 4ef3d8b..b807cc9 100644 --- a/dashboard/dashboard-data.mjs +++ b/dashboard/dashboard-data.mjs @@ -49,13 +49,33 @@ const MAX_TERMINALS = 20; const MAX_PROCESSES = 25; const MAX_GIT_GRAPH_LINES = 25; const MAX_GIT_GRAPH_LINE_CHARS = 160; +const MAX_PIPELINE_RUNS = 5; +const MAX_PIPELINE_NAME_CHARS = 80; +// Live-measured (2026-08-24, this repo, 8 fresh `gh run list` samples via the +// exact trimmed command): 3476-6584ms, one earlier ad-hoc sample 7674ms. +// Prior review sessions (.cursor/memory/plan-monitor-mc-git-tab-visual-tree-and-devops-panel.md) +// spanned 2.1-9.9s. 3500ms had no headroom at all over that range; 12000ms +// gives real margin over the observed worst case (~2.1s over the historical +// 9.9s max, ~4.3s over this session's tight-sample max) while staying well +// under the 60s outer child-process timeout (serve.mjs DATA_SCRIPT_TIMEOUT_MS). +const GH_RUN_LIST_TIMEOUT_MS = 12_000; +const MAX_DEPLOY_TAGS = 5; +const MAX_DEPLOY_TAG_NAME_CHARS = 64; +const MAX_CHANGELOG_ITEMS = 4; +const MAX_CHANGELOG_ITEM_CHARS = 140; /** Soft wall-clock budget for optional collectors (transcripts, reports, ps). */ const SNAPSHOT_STARTED_MS = Date.now(); +// Default raised from 12000ms by the same delta GH_RUN_LIST_TIMEOUT_MS grew +// (3500ms -> 12000ms, +8500ms): collectPipelineRuns() now runs last among the +// budget-guarded collectors and reserves GH_RUN_LIST_TIMEOUT_MS + 400ms of +// budget before attempting `gh run list` (see withinSnapshotBudget call +// there), so the total budget must grow by the same amount the reserve did +// or the larger timeout can never actually be used. const SNAPSHOT_BUDGET_MS = (() => { const raw = process.env.AGENT_KIT_DASHBOARD_DATA_BUDGET_MS; - const n = raw != null && raw !== "" ? Number(raw) : 12_000; - return Number.isFinite(n) && n > 0 ? n : 12_000; + const n = raw != null && raw !== "" ? Number(raw) : 20_500; + return Number.isFinite(n) && n > 0 ? n : 20_500; })(); function withinSnapshotBudget(reserveMs = 400) { return Date.now() - SNAPSHOT_STARTED_MS + reserveMs < SNAPSHOT_BUDGET_MS; @@ -104,7 +124,7 @@ function redactTerminalOutput(text) { const SNAPSHOT = { _schema: { - version: "1.2.0", + version: "1.3.0", description: "Mission Control dashboard data model", fields: { generatedAt: "ISO-8601 timestamp of snapshot generation", @@ -117,6 +137,8 @@ const SNAPSHOT = { memory: "Memory records: error count, decision count, recent decisions, recent parsed errors, error-o-meter stats", git: "Git repository state: branch, dirty status, commit, ahead/behind, bounded files[], promotion flow vs staging/main, graph lines, staging hygiene", + devops: + "Best-effort DevOps signal: pipeline ({available, runs[], reason?} recent gh run list rows; reason is 'budget' when the shared snapshot budget ran out before gh was attempted, or 'call-failed' when gh was attempted and missing/unauthenticated/timed out/errored; reason is only present when available is false) and deploy ({tags[], changelog} v* git tags + latest CHANGELOG release entry as a 'what shipped recently' proxy, not a live infra poll)", terminals: "Active Cursor terminal sessions with metadata, output line count, and capped lastOutput", processes: @@ -127,7 +149,7 @@ const SNAPSHOT = { }, }, generatedAt: new Date().toISOString(), - dashboardDataVersion: "1.3.0", + dashboardDataVersion: "1.4.0", plans: [], system: { repoRoot: ROOT, @@ -246,6 +268,7 @@ const kitCommandPaths = new Set(); const kitSkillDirs = new Set(); const kitAgentPaths = new Set(); const registryFile = join(ROOT, "registry", "registry.json"); +let registryHadCommands = false; if (existsSync(registryFile)) { try { const registry = JSON.parse(readFileSync(registryFile, "utf8")); @@ -265,10 +288,58 @@ if (existsSync(registryFile)) { kitAgentPaths.add(entry.path); } } + registryHadCommands = kitCommandPaths.size > 0; } catch { // Unreadable registry: degrade to all-editable rather than locking everything. } } +// `registry/registry.json` is a factory-only artifact index — consumer installs +// never ship it, so the block above always leaves kitCommandPaths empty there and +// every command reads kitManaged: false even though `update` owns it. Fall back to +// the locally-tracked kit markers: `.cursor/agent-kit.managed-hashes.json` (already +// keyed by the same repo-relative path as c.path / a.path) and, as a secondary +// source, `.cursor/agent-kit.json`'s `protected[]` list (exact paths only; glob +// entries like `.cursor/plans/**` don't identify individual kit-managed commands so +// they're skipped). Only engages when the registry is absent or yielded no commands, +// so factory behavior (registry present + populated) is unchanged. +if (!registryHadCommands) { + const managedHashesFile = join(ROOT, ".cursor", "agent-kit.managed-hashes.json"); + const agentKitConfigFile = join(ROOT, ".cursor", "agent-kit.json"); + const fallbackPaths = new Set(); + if (existsSync(managedHashesFile)) { + try { + const managed = JSON.parse(readFileSync(managedHashesFile, "utf8")); + const hashes = managed?.hashes && typeof managed.hashes === "object" ? managed.hashes : {}; + for (const path of Object.keys(hashes)) { + fallbackPaths.add(path); + } + } catch { + // Unreadable managed-hashes.json: fall through to protected[] (if any). + } + } + if (existsSync(agentKitConfigFile)) { + try { + const agentKitConfig = JSON.parse(readFileSync(agentKitConfigFile, "utf8")); + const protectedList = Array.isArray(agentKitConfig?.protected) + ? agentKitConfig.protected + : []; + for (const entry of protectedList) { + if (typeof entry === "string" && !entry.includes("*")) fallbackPaths.add(entry); + } + } catch { + // Unreadable agent-kit.json: managed-hashes.json (if any) still applies. + } + } + for (const path of fallbackPaths) { + if (path.startsWith(".cursor/commands/")) { + kitCommandPaths.add(path); + } else if (path.startsWith(".cursor/agents/")) { + kitAgentPaths.add(path); + } else if (path.startsWith(".cursor/skills/") && path.endsWith("/SKILL.md")) { + kitSkillDirs.add(path.slice(0, -"/SKILL.md".length)); + } + } +} for (const c of SNAPSHOT.commands) { c.kitManaged = kitCommandPaths.has(c.path); } @@ -402,7 +473,12 @@ if (existsSync(memoryDecisionsDir)) { try { const gitOpts = { cwd: ROOT, encoding: "utf-8", timeout: 5000 }; const branch = execSync("git rev-parse --abbrev-ref HEAD", gitOpts).trim(); - const status = execSync("git status --short", gitOpts).trim(); + // Only strip the trailing newline(s) here — `git status --short` uses + // positional XY status columns, so an unstaged-only row starts with a + // literal leading space (e.g. " M path"). A full-string .trim() eats that + // leading space on line 1 only, shifting parseGitStatusShort's staged / + // unstaged read for the first row. + const status = execSync("git status --short", gitOpts).replace(/\n+$/, ""); const lastCommit = execSync("git log -1 --oneline", gitOpts).trim(); let ahead = 0; let behind = 0; @@ -487,6 +563,128 @@ try { SNAPSHOT._gitRecentLog = []; } +// 6b. DevOps: CI/CD pipeline status (best-effort, `gh` CLI) + a "what shipped +// recently" deploy-activity proxy from v* git tags and the latest CHANGELOG +// release entry. Both fail soft to an empty/unavailable shape; this snapshot +// never blocks or errors on either signal, and never claims live +// infra/hosting monitoring it does not perform. +function collectPipelineRuns() { + // gh run list is a network call, unlike the local-only git collectors + // above; the budget reserve must cover its own timeout, not just a nominal + // buffer (see .cursor/memory/errors/2026-07-26_mission-control-dashboard-data-timeout.md + // for why this file's soft-budget guards exist). + if (!withinSnapshotBudget(GH_RUN_LIST_TIMEOUT_MS + 400)) { + // `gh` was never even attempted this snapshot — the shared budget ran out + // first. Distinguished from a failed/timed-out call below so the UI does + // not misattribute this (the dominant real case; see live measurements + // in .cursor/memory/plan-monitor-mc-git-tab-visual-tree-and-devops-panel.md + // and the residual note this fix landed from) to an auth/install problem. + return { available: false, runs: [], reason: "budget" }; + } + try { + // Trimmed to the fields renderDevopsPipelineCard() / navDevopsDot actually + // read (workflow, status, conclusion, branch, event, createdAt). databaseId + // and url are dropped: the pipeline card is display-only with no + // paste-destination for a run URL (ADR 2026-07-25 copy-only convention), + // so requesting them only adds payload cost without a consumer. + const out = execSync( + `gh run list --limit ${MAX_PIPELINE_RUNS} --json workflowName,status,conclusion,headBranch,event,createdAt`, + { cwd: ROOT, encoding: "utf-8", timeout: GH_RUN_LIST_TIMEOUT_MS }, + ); + const rows = JSON.parse(out); + if (!Array.isArray(rows)) return { available: false, runs: [], reason: "call-failed" }; + const runs = rows.slice(0, MAX_PIPELINE_RUNS).map((r) => ({ + workflow: truncateStr(String(r.workflowName || ""), MAX_PIPELINE_NAME_CHARS), + status: String(r.status || ""), + conclusion: r.conclusion ? String(r.conclusion) : null, + branch: truncateStr(String(r.headBranch || ""), MAX_STRING.branch), + event: String(r.event || ""), + createdAt: r.createdAt || null, + })); + return { available: true, runs }; + } catch { + // gh missing, unauthenticated, its own execSync timeout, or non-repo cwd + // all read the same to the caller: no pipeline signal for this snapshot. + // Not further split (e.g. timeout vs auth) — that would need parsing the + // execSync error shape, which is more taxonomy than the empty-state copy + // needs; "call-failed" is enough to stop the UI from guessing "auth". + return { available: false, runs: [], reason: "call-failed" }; + } +} + +/** Markdown decoration removed, not converted (mirrors scripts/build-landing.mjs's + * stripMdInline): safe against unclosed tokens after the item-length cap below. */ +function stripMdInlineForDeploySignal(s) { + return s + .replace(/\[([^\]]+)\]\([^)]+\)/g, "$1") + .replace(/\*\*([^*]+)\*\*/g, "$1") + .replace(/`([^`]+)`/g, "$1"); +} + +/** First non-"Unreleased" `## [x.y.z] - date` entry and a capped bullet list. */ +function parseLatestChangelogEntryForDeploySignal(changelog) { + const headerRe = /^## \[([^\]]+)\](?:\s*-\s*(.+))?$/gm; + const headers = [...changelog.matchAll(headerRe)]; + const latest = headers.find((m) => m[1].toLowerCase() !== "unreleased"); + if (!latest) return null; + const start = latest.index + latest[0].length; + const next = headers.find((m) => m.index > latest.index); + const body = changelog.slice(start, next ? next.index : undefined); + const items = [...body.matchAll(/^- (.+)$/gm)] + .map((m) => stripMdInlineForDeploySignal(m[1].trim())) + .filter(Boolean) + .slice(0, MAX_CHANGELOG_ITEMS) + .map((line) => + line.length > MAX_CHANGELOG_ITEM_CHARS + ? `${line.slice(0, MAX_CHANGELOG_ITEM_CHARS).trimEnd()}…` + : line, + ); + return { version: latest[1], date: (latest[2] || "").trim() || null, items }; +} + +function collectDeploySignal() { + if (!withinSnapshotBudget(400)) return { tags: [], changelog: null }; + const gitOpts = { cwd: ROOT, encoding: "utf-8", timeout: 5000 }; + let tags = []; + try { + // v*-only: an archive/* or other non-release tag under "what shipped + // recently" would be exactly the misleading signal this phase avoids. + const out = execSync( + `git for-each-ref --sort=-creatordate --format='%(refname:short)|%(creatordate:short)' --count=${MAX_DEPLOY_TAGS} 'refs/tags/v*'`, + gitOpts, + ).trim(); + tags = out + ? out + .split("\n") + .filter(Boolean) + .map((line) => { + const [name, date] = line.split("|"); + return { name: truncateStr(name || "", MAX_DEPLOY_TAG_NAME_CHARS), date: date || null }; + }) + : []; + } catch { + tags = []; + } + + let changelog = null; + try { + const changelogPath = join(ROOT, "CHANGELOG.md"); + if (existsSync(changelogPath)) { + changelog = parseLatestChangelogEntryForDeploySignal(readFileSync(changelogPath, "utf-8")); + } + } catch { + changelog = null; + } + + return { tags, changelog }; +} + +// SNAPSHOT.devops is assigned near the end of the snapshot build (after the +// transcripts/reports/ps collectors below), not here — gh run list is a +// network call that must not run ahead of and starve the local-only +// budget-guarded collectors that share SNAPSHOT_BUDGET_MS. See the call site +// right before the missionControl assembly for the ordering rationale. + // 7. Terminals (read from Cursor terminal files) const terminalsDir = resolve(process.env.HOME || "~", ".cursor", "projects"); // Derive project path from ROOT rather than hardcoding a specific slug @@ -1142,4 +1340,16 @@ function readPreviousInventory() { } SNAPSHOT._gitRecentLog = undefined; +// DevOps: CI/CD pipeline status (best-effort, `gh` CLI) + a "what shipped +// recently" deploy-activity proxy. Deliberately collected last, after every +// other budget-guarded collector above (processes, detached-audit-sessions, +// agentPrompts, externalReports, subagentRuns): collectPipelineRuns() makes +// a network call (gh run list) unlike every other collector in this file, so +// it must not run ahead of and eat into the shared SNAPSHOT_BUDGET_MS budget +// that the local-only collectors also need. +SNAPSHOT.devops = { + pipeline: collectPipelineRuns(), + deploy: collectDeploySignal(), +}; + process.stdout.write(JSON.stringify(SNAPSHOT, null, 2)); diff --git a/dashboard/dashboard.html b/dashboard/dashboard.html index d90949b..e9622bc 100644 --- a/dashboard/dashboard.html +++ b/dashboard/dashboard.html @@ -1173,6 +1173,45 @@ color: var(--text-secondary); margin: 12px 0 6px; } +.git-graph-title:first-child { margin-top: 0; } + +/* ===== DevOps ===== */ +/* Row visual language mirrors .git-flow-row (state dot + lane + meta) so the + * new tab reads as the same product, not a second component vocabulary. */ +.devops-row { + display: flex; + align-items: center; + gap: 10px; + font-size: 12px; + padding: 8px 10px; + border: 1px solid var(--border); + border-radius: 8px; + background: var(--bg-card); + flex-wrap: wrap; + margin-bottom: 6px; +} +.devops-row:last-child { margin-bottom: 0; } +.devops-name { + font-weight: 600; + color: var(--text-primary); + flex-shrink: 0; +} +.devops-meta { + color: var(--text-secondary); + flex: 1; + min-width: 0; +} +.devops-time { + color: var(--text-muted); + font-family: var(--mc-font-mono); + font-size: 11px; + flex-shrink: 0; +} +.devops-note { + font-size: 11px; + color: var(--text-muted); + margin: 0 0 8px; +} .git-hygiene { display: flex; align-items: flex-start; @@ -3644,6 +3683,10 @@ Git + + DevOps + + Memory @@ -3819,6 +3862,7 @@ 'commands', 'health', 'git', + 'devops', 'memory', 'terminals', 'processes', @@ -5430,6 +5474,199 @@ `; } +/** Cap for the visual tree row count (mirrors MAX_GIT_GRAPH_LINES server-side cap, further + * bounded here so the stepper stays a glance, not a scroll well). */ +const MAX_GIT_VISUAL_TREE_ROWS = 12; + +/** + * Parse `git log --graph --oneline --decorate` lines into flattened commit + * entries. Connector-only lines (`|`, `/`, `\`, whitespace between lanes) + * carry no commit and are skipped — see the plan's "Phase 0 lane-flattening + * interpretation" note: this trades true branch-lane geometry (still fully + * present in the kept markdown block) for one row per commit. + */ +function parseGitGraphCommits(lines) { + const commits = []; + for (const line of Array.isArray(lines) ? lines : []) { + const m = /\*\s*([0-9a-f]{4,40})\s+(?:\(([^)]*)\)\s*)?(.*)$/.exec(line); + if (!m) continue; + const decoration = (m[2] || '').trim(); + commits.push({ + hash: m[1], + decoration, + subject: (m[3] || '').trim(), + isHead: /\bHEAD\b/.test(decoration), + isPromoted: /\borigin\/(main|staging)\b/.test(decoration), + }); + } + return commits; +} + +/** + * Visual companion to the markdown git-graph block: reuses the exact + * .now-stepper/.now-step/.now-step-marker timeline primitive (state-colored + * markers + connecting line) with one row per parsed commit. Dot/marker + * semantics stay locked to the existing Now-panel vocabulary: current/blue = + * HEAD, done/green = already promoted to origin/main or origin/staging, + * neutral = neither. Degrades to a compact empty-state when the graph has no + * parseable commit line (e.g. a shallow clone) while a branch still exists. + */ +function renderGitVisualTree(git) { + const commits = parseGitGraphCommits(git?.graph); + if (!commits.length) { + if (!git?.branch) return ''; + return renderEmptyStateCta({ + headline: 'No visual tree yet', + support: 'The commit graph had no parseable commit line to render.', + compact: true, + className: 'git-visual-tree-empty', + }); + } + const shown = commits.slice(0, MAX_GIT_VISUAL_TREE_ROWS); + const rows = shown + .map((c) => { + const stateClass = c.isHead ? ' now-step-current' : c.isPromoted ? ' now-step-done' : ''; + const marker = c.isHead ? '●' : c.isPromoted ? '✓' : '○'; + const label = c.decoration ? escapeHtml(c.decoration) : 'Commit'; + const text = `${escapeHtml(c.hash)}${c.subject ? ` — ${escapeHtml(c.subject)}` : ''}`; + return ` +
  • + +
    + ${label} +
    ${text}
    +
    +
  • + `; + }) + .join(''); + return ` +
    Visual tree (${shown.length} of ${commits.length} commit${commits.length !== 1 ? 's' : ''})
    +
      + ${rows} +
    + `; +} + +/** `in_progress` / `timed_out` -> `In progress` / `Timed out`. */ +function humanizeRunStatus(s) { + const raw = String(s || '').trim(); + if (!raw) return 'Unknown'; + return raw + .split('_') + .map((w) => (w ? w[0].toUpperCase() + w.slice(1) : w)) + .join(' '); +} + +/** Dot tone for one `gh run list` row: in-flight states read yellow; a + * completed run reads by conclusion (green/red/yellow), never decorative. */ +function pipelineRunTone(run) { + if (run.status !== 'completed') return 'yellow'; + if (run.conclusion === 'success') return 'green'; + if (['failure', 'cancelled', 'timed_out'].includes(run.conclusion)) return 'red'; + return 'yellow'; +} + +/** + * CI/CD pipeline card (Phase 2): recent `gh run list` rows, honest empty-state + * when the collector didn't produce runs. Display only, no CTA — a + * workflow-run URL has no named paste destination in the copy-only + * convention (ADR 2026-07-25), so this card ships without one rather than + * inventing a destination. + * + * Empty-state copy is keyed off `pipeline.reason` rather than one blanket + * message: live measurement (2026-08-24, mc-git-tab-devops-pipeline-timeout + * residuals) found the dominant real case is the shared snapshot budget + * running out before `gh` is even attempted ('budget'), not gh being + * missing/unauthenticated — the old unconditional "gh is unavailable or not + * authenticated" copy misattributed that case and pointed the operator at + * the wrong fix. + */ +function renderDevopsPipelineCard(devops) { + const pipeline = devops?.pipeline || null; + if (!pipeline?.available) { + const support = pipeline?.reason === 'budget' + ? 'This snapshot ran out of time before checking recent workflow runs — refresh to retry.' + : 'Recent workflow runs could not be read this snapshot (gh may be missing, unauthenticated, or the call timed out).'; + return renderEmptyStateCta({ + headline: 'No pipeline signal', + support, + compact: true, + className: 'devops-pipeline-empty', + }); + } + const runs = pipeline.runs || []; + if (!runs.length) { + return renderEmptyStateCta({ + headline: 'No recent runs', + support: 'No GitHub Actions workflow runs found for this repository.', + compact: true, + className: 'devops-pipeline-empty', + }); + } + const rows = runs + .map((r) => { + const tone = pipelineRunTone(r); + const label = humanizeRunStatus(r.status === 'completed' ? r.conclusion || 'unknown' : r.status); + return ` +
    + + ${escapeHtml(r.workflow || 'Workflow')} + ${escapeHtml(label)} · ${escapeHtml(r.branch || '—')} · ${escapeHtml(r.event || '—')} + ${escapeHtml(fmtDate(r.createdAt))} +
    + `; + }) + .join(''); + return rows; +} + +/** + * Deploy-activity card (Phase 3, best-effort, honest): a "what shipped + * recently" proxy from `v*` git tags and the latest non-Unreleased CHANGELOG + * entry. Deliberately not a live infra/hosting poll — no fabricated service + * tiles for sources this dashboard does not actually reach. + */ +function renderDevopsDeployCard(devops) { + const deploy = devops?.deploy || null; + const tags = deploy?.tags || []; + const changelog = deploy?.changelog || null; + if (!tags.length && !changelog) { + return renderEmptyStateCta({ + headline: 'No deploy signal yet', + support: 'No git release tags (v*) or a parseable CHANGELOG release entry were found. This section reflects what shipped, from tags and CHANGELOG only — it does not poll live infra or hosting.', + compact: true, + className: 'devops-deploy-empty', + }); + } + const changelogRow = changelog + ? ` +
    + + CHANGELOG ${escapeHtml(changelog.version)} + ${escapeHtml((changelog.items || []).join(' · ') || 'Release notes')} + ${escapeHtml(changelog.date || '—')} +
    + ` + : ''; + const tagRows = tags + .map( + (t) => ` +
    + + ${escapeHtml(t.name)} + git tag + ${escapeHtml(t.date || '—')} +
    + `, + ) + .join(''); + return ` +
    Best-effort "what shipped recently" from git tags + CHANGELOG — not a live infra/deploy poll.
    + ${changelogRow}${tagRows} + `; +} + /** Staging hygiene: untracked plan-monitor WIP (add-by-name only; ADR 2026-07-29 R14). */ function renderGitHygieneHint(git) { const wip = git?.hygiene?.monitorWip || []; @@ -6788,7 +7025,7 @@ * features >= 1 unit. Detail that cannot meet the floor is drawn filled * (fill="currentColor" stroke="none"), never as a sub-stroke stroked shape. * Static path markup only; never concatenate untrusted text into the SVG. - * @param {'current-mission'|'monitor'|'field-report'|'checklist'|'more-sections'|'overview'|'plans'|'activity'|'agents'|'skills'|'skins'|'commands'|'health'|'git'|'memory'|'terminals'|'processes'|'config'} kind + * @param {'current-mission'|'monitor'|'field-report'|'checklist'|'more-sections'|'overview'|'plans'|'activity'|'agents'|'skills'|'skins'|'commands'|'health'|'git'|'devops'|'memory'|'terminals'|'processes'|'config'} kind * @param {{ decorative?: boolean }} [opts] decorative true (default): aria-hidden next to a visible label. * decorative false: role=img + aria-label + title for icon-only controls (e.g. more-sections). */ @@ -6810,6 +7047,7 @@ commands: 'Commands', health: 'Health', git: 'Git', + devops: 'DevOps', memory: 'Memory', terminals: 'Terminals', processes: 'Processes', @@ -6877,6 +7115,12 @@ '' + '' + '', + // Linked nodes (devops) — two pipeline-stage circles joined by a short + // link; distinct from the Git branch glyph and from the Skills gear. + devops: + '' + + '' + + '', // Chip (memory) — two internal lines 3 units apart; a third line would drop clearance below the stroke floor memory: '' + @@ -7779,6 +8023,24 @@ gitDot.className = 'dot dot-green'; } + // DevOps nav dot: state of the most recent pipeline run only (gray = no + // signal / gh unavailable, green = latest run succeeded, yellow = latest + // run in progress or non-success conclusion other than a hard failure, + // red = latest run failed/cancelled/timed out). + const devopsDot = document.getElementById('navDevopsDot'); + const latestRun = (d.devops?.pipeline?.runs || [])[0] || null; + if (!latestRun) { + devopsDot.className = 'dot dot-gray'; + } else if (latestRun.status !== 'completed') { + devopsDot.className = 'dot dot-yellow'; + } else if (latestRun.conclusion === 'success') { + devopsDot.className = 'dot dot-green'; + } else if (['failure', 'cancelled', 'timed_out'].includes(latestRun.conclusion)) { + devopsDot.className = 'dot dot-red'; + } else { + devopsDot.className = 'dot dot-yellow'; + } + // Terminals / processes nav items carry count badges only (no decorative dots). // Build full HTML string once @@ -8169,11 +8431,33 @@ ${renderGitGraphCard(d.git)} + ${renderGitVisualTree(d.git)} ${d.git?.dirty ? renderGitFileList(d.git) : ''} `); + // ===== DevOps ===== + // Distinct scope from Processes (local-process-only): CI/CD pipeline status + // + a best-effort, honestly-scoped deploy-activity proxy. No fabricated + // "service health" widgets for infra this dashboard does not actually poll. + const devopsPipelineRuns = d.devops?.pipeline?.runs || []; + const devopsRunCount = devopsPipelineRuns.length; + parts.push(` +
    +
    + DevOps + ${devopsRunCount} pipeline run${devopsRunCount !== 1 ? 's' : ''} +
    +
    +
    CI/CD pipeline
    + ${renderDevopsPipelineCard(d.devops)} +
    Deploy activity
    + ${renderDevopsDeployCard(d.devops)} +
    +
    + `); + // ===== Memory ===== const recentDecisions = d.memory?.recentDecisions || []; const recentErrors = d.memory?.recentErrors || []; diff --git a/dashboard/lib/live-refresh.d.mts b/dashboard/lib/live-refresh.d.mts new file mode 100644 index 0000000..b203a1d --- /dev/null +++ b/dashboard/lib/live-refresh.d.mts @@ -0,0 +1,23 @@ +/** Ambient types for dashboard/lib/live-refresh.mjs (consumed by CLI TypeScript). */ + +export const WATCH_DEBOUNCE_MS: 400; +export const PERIODIC_REFRESH_MS: 15000; +export const SSE_SILENCE_MS: 20000; +export const SNAPSHOT_REPO_SOURCE_RELS: readonly string[]; + +export function projectSlugFromRoot(root: string): string; +export function resolveAgentTranscriptsWatchPath(root: string, home?: string): string; +export function resolveWatchPaths(root: string, dashboardDir: string): string[]; +export function watchCoversPath(watchAbs: string, targetAbs: string): boolean; +export function isCoveredByWatchPaths(watchAbsPaths: string[], targetAbs: string): boolean; +export function createTrailingDebounce( + fn: () => void, + ms: number, + timers?: { + setTimeout?: typeof setTimeout; + clearTimeout?: typeof clearTimeout; + now?: () => number; + maxWait?: number; + }, +): () => void; +export function isSseSilent(lastEventAt: number, now: number, silenceMs?: number): boolean; diff --git a/dashboard/lib/semantic-model.mjs b/dashboard/lib/semantic-model.mjs index 3ae577e..ba15130 100644 --- a/dashboard/lib/semantic-model.mjs +++ b/dashboard/lib/semantic-model.mjs @@ -24,10 +24,17 @@ 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; +/** + * Cap subagent-run and plan_review pointer rows emitted per snapshot (fs scan + * bounds for the former live in dashboard-data.mjs). Halved from 8/4 (C-G3, + * plan-monitor-crew-monitor-compact-labels-realtime-activity-2026-08-05.md): + * at full caps the two kinds sat ahead of `plan_progress` in mergeActivity's + * priority concat and could claim 12 of MONITOR_FEED_CAP's 20 slots during a + * busy run-plan-all batch, squeezing out plan-progress rows. 4/2 leaves more + * headroom without reordering the merge itself. + */ +export const MONITOR_SUBAGENT_EMIT_CAP = 4; +export const MONITOR_PLAN_REVIEW_EMIT_CAP = 2; /** * Monitor hero curated subset over the semantic activity stream. @@ -2511,7 +2518,7 @@ export function formatPlanReviewActivity( label: truncateStr(visible, MAX_SEMANTIC_LABEL), labelFull: visible, sourcePath: report.path || null, - refs: { plan: report.reviewedPlanFile || null, report: report.file, triaged }, + refs: { plan: planRef, report: report.file, triaged }, }); } return events; diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index 1c4e119..876d469 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -1,12 +1,10 @@ # Contributing -**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. +Thanks for helping. Three kinds of contributions land well here: a skill, a docs fix, a CLI patch. -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). +## Where your PR goes -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. +The public repo is `agent-kit`. Open PRs against **`main`**. There is no long-lived `staging` branch on the public repo; that branch topology belongs to the private factory, and you do not need it. ## Setup @@ -16,25 +14,32 @@ pnpm lint pnpm test ``` -## Contributor quickstart +Green before you push. Conventional Commits (`feat:`, `fix:`, `docs:`). Small PRs merge faster than big ones. + +## Contributing a skill -New to the kit? Here's where things land and how to test before your PR: +Skills live at `registry/skills/community//SKILL.md`. If you work from a project that has the kit installed, `agent-kit contribute` is the return channel: it runs a hygiene gate over your skill and can write it into a local kit checkout with `--write`. It never pushes and never promotes anything. You still open the PR yourself. -- **Skills:** Community contributions go under `registry/skills/community//SKILL.md` - see the [new skill vs improving existing](#new-skill-vs-improving-an-existing-one) section below -- **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). +## What the gate rejects -See [getting-started.md](getting-started.md) for the consumer workflow after install. +- Plans and `HANDOFF.md` in a commit. These are working state, not source. +- Factory-only CHANGELOG bullets in public-facing sections. Fence them. +- Claims the code does not back. Docs orient; code proves. -Optional: `agent-kit add mission-kit-comms` installs the adoption-comms skill (draft recap/release copy with HITL before any public post). Not part of Core Pack. Guide: [comms.md](comms.md). +## House rules + +- The public README is the consumer storefront. Factory workflow detail belongs in CONTRIBUTING and DEVELOPMENT, not the hero. +- Production promotion is human-confirmed, always. No PR changes that. +- When in doubt, open a small PR and ask in it. A rough patch with a clear question beats a polished guess. + +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). ## Standards - Conventional Commits - Small, focused PRs - Update docs when behavior changes +- **Changelog:** public surfaces (public GitHub file, GitHub Releases, landing product notes) take consumer and contributor product notes only. Fence factory-only bullets. See [DEVELOPMENT.md](DEVELOPMENT.md#public-changelog). - **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). diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index f6364c5..24d1b95 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -113,6 +113,32 @@ implementation under test rather than a copy. `dashboard/README.md` documents th - `.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). +### Public changelog + +The private factory `CHANGELOG.md` may keep a full developer log. Public-sync copies a stripped excerpt: public GitHub `CHANGELOG.md`, GitHub Release notes, and the landing product-notes field must stay consumer and contributor product voice. Never publish CI/CD internals, landing staging/promote plumbing, Hostinger/deploy details, internal ADR chatter, or dogfood-only kit mechanics. + +Fence factory-only bullets on their own lines: + +```markdown + +- Hostinger staging hop / landing:promote internals + +``` + +A `### Internal` heading is stripped until the next `##` / `###`. Preview: + +```bash +node scripts/public-changelog.mjs # full public markdown +node scripts/public-changelog.mjs --version 5.6.0 # GitHub Release body (no ## [x.y.z] header) +node scripts/public-changelog.mjs --version 5.6.0 --blurb +# short publicNotesBlurb for: +# pnpm landing:update-release -- --version 5.6.0 --notes "" +# or --notes-file ./public-release-notes.txt +# Never pass CHANGELOG.md as --notes-file. +``` + +`--json` emits `{ version, date, notes, publicNotesBlurb }`. `notes` is the Keep-a-Changelog section body (GitHub Release). `publicNotesBlurb` is the landing stamp field (heading-free, under 1000 characters). + Release and sync ops: [public-launch.md](public-launch.md), [npm-publish-checklist.md](npm-publish-checklist.md). ## Mission Kit / Agent Kit / Mission Control naming diff --git a/docs/README.md b/docs/README.md index b7dd2e9..8274ffc 100644 --- a/docs/README.md +++ b/docs/README.md @@ -1,44 +1,44 @@ # Mission Kit Docs -**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. +**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. -## Guides +## Usage first - [Getting Started](getting-started.md) - install, commands, workflow +- [CONTRIBUTING](CONTRIBUTING.md) - setup, standards, registry contributions +- [Contribute upstream](contribute-upstream.md) - `agent-kit contribute` return channel + gate + +## Repository setup + - [Bootstrap](bootstrap.md) - install without nested `agent-kit/` folder - [Repository readiness onboarding](repository-readiness-onboarding.md) - install discovery, guided preparation, and handoff to `/start-project` - [Migrate consumer](migrate-consumer.md) - generic runbook to leave nested `agent-kit/` (`YOUR_PROJECT`) -- [Contribute upstream](contribute-upstream.md) - `agent-kit contribute` return channel + gate -- [Public launch](public-launch.md) - go/no-go + append-only sync -- [Public launch announcement](public-launch-announcement.md) - copy-paste launch text (chat / social) -- [Adoption comms](comms.md) - channel map, recap/release cadence, HITL draft pipeline (does not auto-post) -- [Mission Kit Landing](agentkit-landing.md) - public marketing page at [missionkit.io](https://missionkit.io) (filename kept for link stability; previous `agent.startupkit.com.br` is deprecated and 301-redirects) -- [Topology private × public](topology-private-public.md) - Fase 7 registry-canonical public -- [Marketplace catalog](marketplace.md) - versioning, CLI add, Cursor plugin, quality gate -- [Review layers](review-camadas.md) - final HITL / go-no-go pass -- [Creating Skills](creating-skills.md) - skill format, placement, registry -- [Creating Agent Personas](creating-personas.md) - persona pack format, placement, contribute checklist -- [Agent Personas contract](personas-contract.md) - persona pack schema, mode defaults, acceptance rules (also summarized in the root [README Features](../README.md#features)) -- [External plan review](external-plan-review.md) - opt-in Claude Code monitor after plan exhaustion -- [Claude CLI kit-load](claude-cli-kit-load.md) - thin `CLAUDE.md` plus `/agent-kit` session bootstrap (not audits, not A7) + +## Configuration and features + - [Consumer configuration](consumer-configuration.md) - every consumer knob (session config, skin, install choices, CLI flags/env) with copy snippets -- Config tab write verification - durable allowlist PATCH matrix for Mission Control Config (private factory evidence under `docs/evidence/`; not public-synced) -- [Cursor 3.0 Features](cursor-3-features.md) - how Mission Kit uses native IDE features -- [Cursor-native audit](cursor-native-audit.md) - hooks.json, plugin, rule modes, VS Code/Windsurf gaps -- [Coherence inventory](coherence-inventory.md) - classification of rules, skills, hooks, agents, commands -- [Drift inventory](drift-inventory.md) - per-workspace kit copies, L0 candidates, L3 uniques -- [Layers specification](layers-spec.md) - L0–L3 model, precedence, nomenclature -- [Five-layer claim matrix](five-layer-claim-matrix.md) - public five-layer positioning (core / optional / planned / unsupported) - [Domain packs (L1)](domain-packs.md) - seven discipline packs and membership +- [Agent Personas contract](personas-contract.md) - persona pack schema, mode defaults, acceptance rules +- [External plan review](external-plan-review.md) - opt-in Claude Code monitor after plan exhaustion +- [Claude CLI kit-load](claude-cli-kit-load.md) - thin `CLAUDE.md` plus `/agent-kit` session bootstrap (not audits, not A7) + +## Reference + - [Agent Kit manifest](agent-kit-manifest.md) - `.cursor/agent-kit.json` schema (version, packs, protected L3) +- [Layers specification](layers-spec.md) - L0–L3 model, precedence, nomenclature - [Repository Boundaries](repository-boundaries.md) - three-layer cheat sheet (local / private / public), npm, sync -- [Contributing](CONTRIBUTING.md) - setup, standards, registry contributions +- [Cursor 3.0 Features](cursor-3-features.md) - how Mission Kit uses native IDE features +- [Cursor-native audit](cursor-native-audit.md) - hooks.json, plugin, rule modes, VS Code/Windsurf gaps + +## Development + - [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 +- [Creating Skills](creating-skills.md) - skill format, placement, registry +- [Creating Agent Personas](creating-personas.md) - persona pack format, placement, contribute checklist +- [Marketplace catalog](marketplace.md) - versioning, CLI add, Cursor plugin, quality gate ## Community -- [Adoption comms](comms.md) - spread-the-word drafts with HITL; contributor funnel stays CONTRIBUTING - [Code of Conduct](../.github/CODE_OF_CONDUCT.md) - Contributor Covenant 2.1 and how to report a concern - [Security policy](../.github/SECURITY.md) - private disclosure channel, supported versions, documented posture (never open a public issue for a vulnerability) - [Support](../.github/SUPPORT.md) - where to ask what, and what makes a question answerable diff --git a/docs/agentkit-landing.md b/docs/agentkit-landing.md index df79f61..9296155 100644 --- a/docs/agentkit-landing.md +++ b/docs/agentkit-landing.md @@ -52,8 +52,10 @@ pnpm landing:deploy:staging # 6. Automated acceptance against the live staging URL pnpm landing:verify:staging # Headless-Chrome render (not curl): 0 unresolved bindings, #dc-root -# present, video + iframe present, no request outside the origin (mod the -# hero badge's img.shields.io exception below). +# present, video + iframe present, no request outside the origin on load. +# The demo modal's youtube-nocookie embed is an opt-in exception that only +# fires if a person clicks to open it: this gate never clicks, so it neither +# tests nor needs to allow that host. See "On-interaction exception". # 7. HITL — open https://staging.missionkit.io, compare side by side # against the Claude Design canvas, approve explicitly. Nothing below @@ -80,24 +82,57 @@ assertion, in code) to write anywhere outside `staging/`. 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) -4. Injects a build-time **changelog box**: the latest public-facing `CHANGELOG.md` - release entry, rendered as plain escaped text, into a canvas-provided - `<div data-changelog-content></div>` container. No-ops with a logged warning - (never fails the build) until the canvas ships that container — see - `.cursor/context/landing-missionkit/UPSTREAM-DESIGN-FIX-PROMPT-badge-changelog.md` - for the exact contract and the paired hero-eyebrow live-badge change. - -### Live vs. build-time exception - -Everything the build ships is self-contained by default (proven headless, external -DNS blocked). One narrow, named exception: the hero release badge is authorized to -make a live client-side request to `img.shields.io` only (same pattern as the -`README.md` release badge). No other element — including the changelog box above, -which is build-time only — gets a live/runtime external request. See ADR -`decisions/2026-08-05_landing-external-design-source-of-record.md` (2026-08-22 -addendum) for the full reasoning and the confirmed edit route (this repo has no -owned/shared write access to the canvas; changes go through the hand-off-prompt -pattern, not a direct canvas edit). + +Release version and changelog notes are **not** a build-time feed. They live in the +canvas fields below and are stamped with `pnpm landing:update-release` (does not deploy). + +### Release version and product notes + +Two existing canvas fields, restored to the original pill-and-frame visual (no live badge, +no CHANGELOG.md dump): + +| Field | Selector | Content | +|---|---|---| +| Current release | `a[data-release-version]` inside `p.ak-brand` (New release pill + version text) | Version string plus a link to the **public** GitHub Release (`agent-kit-startup/agent-kit`, not the private `agent-kit-dev` repo) | +| Product notes | `div[data-changelog-content]` in the Footer CTA `.ak-mc-frame` | Short public product notes. Not a live feed and not `CHANGELOG.md`. | + +Stamp (idempotent; fails closed if the fields are missing, if notes look like a changelog dump, or if the URL is not the public tag URL): + +```bash +node scripts/public-changelog.mjs --version 5.6.0 --blurb +pnpm landing:update-release -- --version 5.6.0 --notes "Short public product notes." +pnpm landing:update-release -- --version 5.6.0 --notes-file ./public-release-notes.txt +pnpm landing:update-release -- --version 5.6.0 --notes "Short public product notes." --dry-run +``` + +`--blurb` is the stamp-safe `publicNotesBlurb` (heading-free, under 1000 characters). Never pass `CHANGELOG.md` as `--notes-file`. + +Then `pnpm landing:build`. Deploy stays `landing:deploy:staging` / `landing:promote`. + +`landing:sync` overwrites `remote/` wholesale. Re-run `landing:update-release` after a Design zip so the two fields are not reverted to a live badge or an empty box. + +### On-load vs on-interaction requests + +Everything the build ships is self-contained **on load** by default (proven headless, +external DNS blocked). There is no on-load third-party exception: the former hero +release badge (`img.shields.io`) is gone; version is a static `data-release-version` +link. See ADR `decisions/2026-08-05_landing-external-design-source-of-record.md` +(2026-08-27 addendum). Visual or copy changes to the marketing canvas still go through +Claude Design, then re-export and `landing:sync`. The two release fields above are +the sanctioned stamp via `landing:update-release` (re-run after every sync). + +**On-interaction exception:** the demo video modal mounts a `youtube-nocookie.com` +iframe, but only when a person clicks to open it. `demoSrc` defaults to `''` and the +modal (iframe included) is not in the DOM at all until `demoOpen` is true +(`<sc-if value="{{ demoOpen }}">`), so first paint issues zero third-party requests +(fixed 2026-08-05, `85a9e61`; re-verified live 2026-08-11: `missionkit.io`'s served +bytes carried no outward `src`; re-verified again from source at HEAD 2026-08-24 +via `pnpm landing:build` with no network access). This is expected, deliberate +behavior, not a defect: a video demo has to come from somewhere. The acceptance +line is "the live page issues no request outside its own origin **on load**; opening +the demo modal is a user-opted exception to `youtube-nocookie.com`", not an +unqualified "no request outside origin," which the page was never trying to +guarantee for every possible click. ## Deployment @@ -112,7 +147,7 @@ pattern, not a direct canvas edit). - **Subdomain:** `staging.missionkit.io`, created via `hosting_createWebsiteSubdomainV1` (`POST .../websites/missionkit.io/subdomains`), directory `staging/` under the same document root as production — not a separate hosting account. - **Never indexed.** Staging must not carry `noindex` inside `dist/`'s own bytes: `landing:promote` ships the *exact same* `dist/` bytes already validated on staging, and if those bytes carried a `noindex` meta tag, promoting would noindex production too. Instead, `landing:deploy:staging` writes two generated, staging-only files alongside the upload — `staging/.htaccess` (`Header set X-Robots-Tag "noindex, nofollow"`) and `staging/robots.txt` (`Disallow: /`) — neither is part of `dist/`, neither is ever promoted. This is a deliberate deviation from the plan's literal wording ("toggle in build-landing.mjs"), made to preserve the plan's own higher-priority rule that promote never rebuilds and ships identical bytes. - **Residual:** the subdomain directory lives under the same document root as production (`.../public_html/staging/`), so the staged build may also be reachable at `missionkit.io/staging/` in addition to `staging.missionkit.io/`. The `X-Robots-Tag` header covers indexing either way; this is noted, not solved, here. -- **Acceptance:** `pnpm landing:verify:staging` (`scripts/verify-landing.mjs --url https://staging.missionkit.io/`) — headless Chrome (`--headless=new --dump-dom`, external DNS blocked via `--host-resolver-rules`, same self-containment technique as the original production acceptance gate) asserting `#dc-root`, zero unresolved bindings, a `<video>`, the `mc/dashboard.html` iframe, and no request to a host outside the origin (the hero badge's `img.shields.io` call is the one named live exception; see "Live vs. build-time exception" above). +- **Acceptance:** `pnpm landing:verify:staging` (`scripts/verify-landing.mjs --url https://staging.missionkit.io/`): headless Chrome (`--headless=new --dump-dom`, external DNS blocked via `--host-resolver-rules`, same self-containment technique as the original production acceptance gate) asserting `#dc-root`, zero unresolved bindings, a `<video>`, the `mc/dashboard.html` iframe, and no request to a host outside the origin on load. This is a load-time check only. It never clicks anything, so it does not exercise (and does not need to allow) the demo modal's on-interaction `youtube-nocookie.com` request; see the on-interaction exception paragraph above. - **Promote/rollback safety:** `landing:promote` archives whatever is *currently live* at `missionkit.io` into `.cursor/context/landing-missionkit/releases/<timestamp>/` (zip, gitignored) **before** deploying — fetched over public HTTPS per file (not the Hostinger file-content API, which refuses binary files) so videos/images are captured too. `landing:rollback [release]` redeploys an archived release; with no argument, the immediately previous one. Neither script ever calls `landing:build`. - **HITL gate:** Phase 3 of the plan — operator opens `https://staging.missionkit.io`, compares against the Claude Design canvas, approves explicitly. No script promotes on a passing `landing:verify:staging` alone. - **Operator-owed first run:** the upload leg (`scripts/lib/hostinger.mjs`'s `uploadFile`) reconstructs an undocumented Hostinger upload sequence (POST pre-create, then a TUS-style `PATCH`) from the vendored `hostinger-api-mcp` package's source, since this sandbox's permission classifier refused every mutating call attempted (subdomain create, adding a vendored dependency) and that refusal was not retried per this repo's worker contract. All read-only calls (website lookup, subdomain listing, DNS zone, file listing) were exercised live against the real account and work as documented; run `pnpm landing:deploy:staging -- --dry-run` first, and have the first real run be operator-attended. @@ -163,7 +198,7 @@ Other crawler fields matching the build pipeline: - **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). +Operator path for copy changes: edit Claude Design SoR → Download zip → `pnpm landing:sync` → `pnpm landing:update-release` → `pnpm landing:build` → staging deploy / promote. Do not hand-edit `landing-missionkit/remote/` as the source of truth. Standing `/design` paste template: `.cursor/context/landing-missionkit/CLAUDE-DESIGN-TEMPLATE.md` (New release pill + stamped notes). One-off prompt notes: `.cursor/context/landing-missionkit/UPSTREAM-DESIGN-FIX-PROMPT.md` (license copy + install/prompt clipboard honesty). Do not re-apply `UPSTREAM-DESIGN-FIX-PROMPT-badge-changelog.md`. 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`). diff --git a/docs/capability-inventory.md b/docs/capability-inventory.md index ab2b4f6..f592c49 100644 --- a/docs/capability-inventory.md +++ b/docs/capability-inventory.md @@ -4,13 +4,13 @@ Mission Kit capability catalog grouped by surface family. Lists every shipped ca **Status (2026-08-06):** Product manifests at `5.0.0`. Capability counts verified against the working tree on private `staging` @ `7fdb03c` (see Real counts). Catalog narrative remains indicative for non-count claims. Evidence lanes: `docs/evidence/artifact-ledger-summary.md`, `docs/evidence/delivery-reconciliation.json` (RC-003/RC-004). Five-layer README positioning claims: `docs/evidence/five-layer-claim-matrix.md` / `docs/five-layer-claim-matrix.md`. -Real counts (verified against the working tree on 2026-08-13): **28** slash commands under `.cursor/commands/` (**27** synced/L0-oriented; **1** factory-only `/public-issue-triage` excluded from public-sync and L0 install), 25 rules, 14 agents, 10 skills, 5 Cursor hooks, 18 CLI commands (plus 5 subsystems), 7 packs, 3 personas, Mission Control dashboard, Git hooks, root scripts, and auxiliary tooling. Prior SHA snapshot `7fdb03c` was 13 agents / 9 skills before `mission-kit-comms`. +Real counts (verified against the working tree on 2026-08-27): **30** slash commands under `.cursor/commands/` (**29** synced/L0-oriented; **1** factory-only `/public-issue-triage` excluded from public-sync and L0 install), 25 rules, 14 agents, 10 skills, 5 Cursor hooks, 18 CLI commands (plus 5 subsystems), 7 packs, 3 personas, Mission Control dashboard, Git hooks, root scripts, and auxiliary tooling. Prior SHA snapshot `7fdb03c` was 13 agents / 9 skills before `mission-kit-comms`. --- -## Slash commands (.cursor/commands/ - 28) +## Slash commands (.cursor/commands/ - 30) -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`. +Factory-only counting policy: inventories that describe the **consumer/L0** surface should cite **29** syncable commands (excluding `/public-issue-triage`). The on-disk factory tree has **30** files; `/public-issue-triage` is omitted from L0 install and excluded from `scripts/public-sync.manifest`. `/kit-staging` and `/kit-prod` are L0 wraps of native git commands plus an optional public-landing deploy when a product changelog or release changed. - `/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 @@ -33,6 +33,8 @@ Factory-only counting policy: inventories that describe the **consumer/L0** surf - `/cursor-update-awareness` - Advisory Cursor product-update check (changelog + inventory; HITL conveyor) - `/git-staging` - Staging branch promotion with CHANGELOG and MR workflow - `/git-prod` - Production promotion from staging with HITL confirmation +- `/kit-staging` - Git-staging wrap; optional public-landing staging deploy when a product changelog or release changed +- `/kit-prod` - Git-prod wrap (same HITL); optional public-landing promote when this promotion includes a release - `/plan-external-review` - External plan review launcher with audit modes - `/plan-review-triage` - Triage choice after external review (residuals/fixes/ack) - `/field-report-resolve` - Resolve Field Report findings with structured closure @@ -317,7 +319,7 @@ Enumerated identity literals and publication routes from `scripts/public-sync.ma | 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` | L5 | Development operations built into Cursor, with partial VS Code support. | 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`) | @@ -349,7 +351,7 @@ Enumerated identity literals and publication routes from `scripts/public-sync.ma | `README.md` | L127 | [Development](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) | +| `package.json` | 4 | HITL framework for AI-assisted IDEs: plan, handoff, staging-to-prod, memory loop; Cursor-first with partial generators for 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`) | diff --git a/docs/claude-cli-kit-load.md b/docs/claude-cli-kit-load.md index a6922c4..a75375a 100644 --- a/docs/claude-cli-kit-load.md +++ b/docs/claude-cli-kit-load.md @@ -80,6 +80,10 @@ Mid-session refresh: `/agent-kit`. Cursor Ask questions is not available in this CLI. When a command requires a choice, list the same labels as a numbered list and wait. Skip or cancel means stop. Never `/git-prod` without an explicit operator yes. +## Commit messages + +Never append `Co-Authored-By: Claude ...` or `Claude-Session: https://claude.ai/code/...` (or any session-ID trailer) to commit messages, PR descriptions, or PR bodies in this repo. This overrides the harness's default git-commit template. Plain Conventional Commits messages only. + ## Non-goals - Not Action A7 (Windsurf / VS Code generator parity) @@ -96,18 +100,20 @@ description: Load Agent Kit session context (HANDOFF, project-context, commands) disable-model-invocation: true --- -Read these files if they exist, then summarize the active plan, next to-do, and any Gaps. Do not scan the whole repository first. +Read these files if they exist. Prefer a one-shot ASCII Mission Control snapshot over a plain HANDOFF paraphrase. -1. `AGENTS.md` -2. `.cursor/project-context.md` -3. `.cursor/HANDOFF.md` -4. The plan file named in HANDOFF `- **Plan:**` under `.cursor/plans/` +1. Run `agent-kit mission-control --once` (or `npx @dadado/agent-kit-cli mission-control --once`) in the project root and paste the stdout frame as the snapshot. Do not start a live loop: Claude Code cannot sustain one across turns. +2. If that command is missing or fails, fall back to reading: + - `AGENTS.md` + - `.cursor/project-context.md` + - `.cursor/HANDOFF.md` + - The plan file named in HANDOFF `- **Plan:**` under `.cursor/plans/` If HANDOFF is missing, say so and point at `/agent-kit-onboard` or `/start-project` rather than inventing a plan. HITL: numbered-list fallback for Ask questions labels. Never `/git-prod` from this skill. -Non-goals: not audits / `/plan-external-review`, not `--backend claude` ticks, not A7, not Cursor hook clones. +Non-goals: not audits / `/plan-external-review`, not `--backend claude` ticks, not A7, not Cursor hook clones, not a continuous TUI loop. ``` ## Opt-in surfaces: command adapters and the SessionStart hook diff --git a/docs/design-system.md b/docs/design-system.md index b7e6fe9..fc1f1c3 100644 --- a/docs/design-system.md +++ b/docs/design-system.md @@ -41,5 +41,6 @@ Do not treat the remote DS as ground truth; always compare against the repo. ## References - [Landing page runbook](agentkit-landing.md) +- Landing Claude Design / `/design` paste template: `.cursor/context/landing-missionkit/CLAUDE-DESIGN-TEMPLATE.md` (factory private; plain path) - 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 5438514..987a75b 100644 --- a/docs/external-plan-review.md +++ b/docs/external-plan-review.md @@ -159,7 +159,7 @@ Do **not** flip `autoRemediate` to `true` as a shortcut for fewer backlog plans; Canonical launcher: `.cursor/scripts/plan-external-review.sh` (wrapper: `scripts/plan-external-review.sh`). -The launcher starts Claude with `--permission-mode auto` and `--model` from `reviewerModel` (default `sonnet`) in interactive and headless modes. Auto requires a classifier-capable reviewer; the default is Sonnet so that mode can run (ADR `2026-08-14_audits-haiku-auto-permission-amend.md`). This does not use `bypassPermissions`. Override with `--reviewer-model`, `--advisor-model`, and `--implementer-model` (or `AGENT_KIT_AUDIT_IMPLEMENTER_MODEL`). Same-family implementer and reviewer is an honest skip (exit 4 with `--wait-monitor`), including Auto/Auto. +The launcher starts Claude with `--permission-mode auto` and `--model` from `reviewerModel` (default `sonnet`) in interactive and headless modes. Auto requires a classifier-capable reviewer; the default is Sonnet so that mode can run (ADR `2026-08-14_audits-haiku-auto-permission-amend.md`). This does not use `bypassPermissions`. Override with `--reviewer-model`, `--advisor-model`, and `--implementer-model` (or `AGENT_KIT_AUDIT_IMPLEMENTER_MODEL`). Same-family implementer and reviewer is an honest skip (exit 4 with `--wait-monitor`), including Auto/Auto. `--prompt-file PATH` (repo-relative markdown) overrides the reviewer prompt for that run; the default remains `.cursor/context/templates/plan-external-review-prompt.md`. **Background/inspectable auto-launch (`mode: "autonomous"` or `--autonomous`):** prefers tmux/screen detached PTY, then macOS Terminal.app `do script` **without** `activate`, then Linux/Windows emulators. Soft-falls back to `--paste-only` when spawn is unavailable. Soft-fails with tip + exit 0 when `claude` is missing (Field Report owed). Never runs silent `claude -p` in a chat agent shell. Rollback to OS window focus: `--focus-terminal` or `AGENT_KIT_AUDIT_FOCUS_TERMINAL=1`. ADR: `.cursor/memory/decisions/2026-07-28_audits-headless-terminal-honesty.md`. diff --git a/docs/getting-started.md b/docs/getting-started.md index 2c32e1a..d830197 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -1,68 +1,51 @@ # Getting Started -**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. +## What you need -## Install +- Node.js 20+ +- A git repository (recommended; staging and prod routines use it) +- Cursor, Claude Code, or just a terminal -Run this from your project's root folder: +## Install the kit ```bash +cd your-project npx @dadado/agent-kit-cli install ``` -**Prerequisites:** Node.js 20+ (CLI `engines`). Git is recommended so `/agent-kit-onboard` and the staging→prod flow can complete; Port B can still copy L0 files without the CLI when Node is unavailable. +Pin with `npx @dadado/agent-kit-cli@x.y.z install` for reproducible setups. In CI or scripts, use `npx -y @dadado/agent-kit-cli install --yes`. -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). +No Node available? `install.md` documents a manual copy path for the base files without the CLI. -**IDE-agnostic:** works in Cursor, VS Code, and any terminal with Node.js. Claude Code CLI session kit-load (`CLAUDE.md` / `/agent-kit`) is documented under [Claude Code CLI (session kit-load)](#claude-code-cli-session-kit-load). For non-interactive terminals (CI, piped stdin, VS Code output panels without TTY), suppress both `npx`'s own confirmation and the CLI's root prompt: +## First session in Cursor -```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:** +Open the Agent chat and type `/start-project` with your goal. The kit runs a broad intake, writes a plan with to-dos, and stops. You approve the plan, then approve the first unit. Goal text is not execute permission; the kit waits for both gates. -| 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 | -| `EACCES` on `npm i -g @dadado/agent-kit-cli` | Root-owned npm global prefix (e.g. `/usr/local/lib/node_modules`) | Check first with `agent-kit doctor --json` (`env.npmPrefixWritable`), then run `npx @dadado/agent-kit-cli setup-global` — it relocates the prefix to `~/.npm-global`, fixes `PATH`, and reinstalls | -| `command not found` after install (or for a bare `agent-kit`) | `npx` is ephemeral; a bare `agent-kit` isn't on `PATH` yet | The install/init epilogue prints this automatically with 3 options; run `npx @dadado/agent-kit-cli setup-global` to fix `PATH`, or keep using `npx @dadado/agent-kit-cli <subcommand>` | +From there: -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). +- `/continue-plan` ships exactly one unit and stops. +- `/run-plan` keeps going until the plan is done or blocked. +- `/git-staging` lands the work on the staging branch through a PR. +- `/git-prod` promotes staging to main, and only after an explicit Ask. -**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. +## First session in Claude Code -Before that confirmation, the CLI also runs a **project root guard** - see [Project root guard](#project-root-guard) below. +Load the kit with the `--claude` flag during install. You get `CLAUDE.md` and the `/agent-kit` command. Confirmations arrive as numbered lists instead of Cursor's Ask dialogs; reply with the number. `/agent-kit` prints one Mission Control frame (`mission-control --once`) rather than a live loop. -Want a few extra bundles up front? Add packs (clean code, context tools, and more - see [domain packs](domain-packs.md)): - -```bash -npx @dadado/agent-kit-cli install --pack clean-code,context-management -``` +## Watch progress -**Prefer chat install?** Copy-paste the installer brief from the [README](../README.md#install) into Cursor chat. You get exactly the same result. The chat installer uses **Ask questions** for confirmations (clickable options in IDE UI, with chat fallback when tool unavailable), while the CLI uses terminal prompts. +Two views of the same state, both named Mission Control: -> Don't clone the Agent Kit repo into your project. Installing writes only the files your project needs - see [bootstrap](bootstrap.md) for the exact layout. +- **Terminal TUI:** `npx @dadado/agent-kit-cli mission-control`. Live in a TTY; `--once` for a single frame. It reads the same data as the browser dashboard and does not start an HTTP server. +- **Browser:** `npx @dadado/agent-kit-cli dashboard` (or `/dashboard` from chat). Binds to `127.0.0.1` by default. To share on your LAN, use `dashboard-broadcast` with its required token. -`install` also scans the repository, applies safe local preparation, and writes `.cursor/context/readiness.json`. Guided entry still works through `init`, which reuses the same install and readiness path: +The browser dashboard ships inside the CLI package since 4.8.2. The install does not copy a `dashboard/` folder into your project; that is expected. -```bash -npx @dadado/agent-kit-cli init -``` - -Diagnose without installing: - -```bash -npx @dadado/agent-kit-cli doctor --json -npx @dadado/agent-kit-cli doctor --fix-safe -``` +## What the kit will not do -`doctor` also reports an environment pillar: `env.binOnPath`, `env.npmPrefixWritable` (with prefix detail), `env.nodeVersionOk`, and `env.shellProfile` in `--json`, plus a human "environment:" summary — read-only diagnostics, no writes. See the [`setup-global`](#how-to-invoke-them) fix for a root-owned npm prefix. +- It will not push to production without your explicit confirmation. +- It will not treat docs as proof. Code, tests, and shipped artifacts are the delivery truth. +- It will not pretend VS Code or Windsurf are peer surfaces. They get partial generators; Cursor is the primary IDE. ### Project root guard @@ -107,7 +90,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 `npx @dadado/agent-kit-cli dashboard` (bare `agent-kit dashboard` only after a global install). Consumer L0 does not copy `dashboard/` into the project. The `dashboard` subcommand 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: `npx @dadado/agent-kit-cli dashboard-broadcast` / `npm run dashboard:broadcast` (token-gated) binds the same per-workspace port allocation as `/dashboard` (`3333-3588` unless `PORT` is set), so it starts **beside** an already-running Mission Control - this workspace's loopback panel, another workspace, or an unidentified listener is skipped and left running, never killed. It 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). +6. **Mission Control (optional)** - Browser panel: `/dashboard`, `npm run dashboard`, or `npx @dadado/agent-kit-cli dashboard` (bare `agent-kit dashboard` only after a global install). Browser-free TUI: `npx @dadado/agent-kit-cli mission-control` (live view on a TTY) or `mission-control --once` for a one-shot ASCII snapshot (Claude Code `/agent-kit` uses this path). The TUI is a **third surface**; it does not replace the web dashboard. Consumer L0 does not copy `dashboard/` into the project. The `dashboard` subcommand 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: `npx @dadado/agent-kit-cli dashboard-broadcast` / `npm run dashboard:broadcast` (token-gated) binds the same per-workspace port allocation as `/dashboard` (`3333-3588` unless `PORT` is set), so it starts **beside** an already-running Mission Control - this workspace's loopback panel, another workspace, or an unidentified listener is skipped and left running, never killed. It 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 @@ -142,6 +125,7 @@ The table below lists **subcommands**. Prefix each one with `npx @dadado/agent-k | `diff` | Show what changed between what you have and the latest | | `contribute` | Send an improvement you made locally back upstream | | `handoff` | Save your progress to `.cursor/HANDOFF.md` | +| `mission-control` | ASCII Mission Control (mission, flight log, checklist, crew monitor); `--once` prints one frame | | `scan` | Just scan the project, don't install | Optional: `add mission-kit-comms` drafts recap/release/contributor copy. It does **not** post. Ask before any public network. Guide: [comms.md](comms.md). @@ -175,6 +159,8 @@ Operator sequence when you drive each unit (command SoT: [`.cursor/commands/cont | Run several plans as one ordered queue | `/run-plan-all` | | Commit / MR to `staging` after a manual unit | `/git-staging` | | Promote `staging` → `main` | `/git-prod` (explicit confirm only) | +| Staging plus public-landing preview (product changelog only) | `/kit-staging` | +| Prod plus public-landing promote (release changelog only) | `/kit-prod` (git-prod Ask, then extra landing Ask) | Do not re-author Gate A/B or continuous tick contracts here; link L0 commands when you need the full contract. @@ -252,7 +238,7 @@ Install writes a thin root `CLAUDE.md` and `.claude/commands/agent-kit.md` so Cl 1. Start Claude Code in the project root. 2. `CLAUDE.md` loads automatically. Mid-session refresh: type `/agent-kit`. -3. Claude should read `.cursor/HANDOFF.md`, `.cursor/project-context.md`, and `.cursor/commands/` instead of a full archaeology pass. +3. `/agent-kit` prefers `agent-kit mission-control --once` (styled ASCII snapshot of Mission, Flight Log, Checklist, Crew Monitor) and falls back to reading `.cursor/HANDOFF.md`, `.cursor/project-context.md`, and `.cursor/commands/` if the CLI is missing. Do not start a live TUI loop from that slash (Claude Code cannot sustain one across turns). HITL in Claude Code is a numbered-list fallback (Cursor Ask questions is not available). Never `/git-prod` from kit-load. @@ -302,7 +288,7 @@ Mission Control is a **local, single-developer** observability panel. Treat it a 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`, `npx @dadado/agent-kit-cli 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. +**Where Mission Control 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 **browser** 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 the browser panel with `/dashboard`, `npm run dashboard`, `npx @dadado/agent-kit-cli dashboard`, or `node dashboard/start.mjs` (see root README). The **terminal** surface is `npx @dadado/agent-kit-cli mission-control` (live TTY) or `mission-control --once` (one frame); it reuses `dashboard-data.mjs` and does not start the HTTP server. A browser is not required to read Mission, Flight Log, Checklist, or Crew Monitor. Several workspaces may run concurrent browser instances: each gets a stable listen port from its repo root (see printed URL / `system.port`); Mission Control never kills another workspace's listener. **First failure (no `dashboard/start.mjs`):** the installed CLI is older than 4.8.2 (4.8.0 has no Path C assets; 4.8.1 was never published). Upgrade to 4.8.2+, or point env/sibling at a kit tree. Do not expect Port B alone to place the panel binary in the project. diff --git a/docs/marketplace.md b/docs/marketplace.md index eaad9a2..1cf59a6 100644 --- a/docs/marketplace.md +++ b/docs/marketplace.md @@ -83,6 +83,8 @@ All five hook adapters (`sessionStart`, `preCompact`, `beforeShellExecution`, `a `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`. +`displayName` (currently `"Agent Kit"`) is **not** in Cursor's documented plugin manifest schema. Kept deliberately as a listing-friendly name distinct from the package-style `name`; unknown keys have not been observed to fail manifest validation. Drop it only if a future submission attempt reports it as rejected by validation. + Editing any file under `.cursor/{agents,skills,commands}/` (or a registry skill's `SKILL.md`) changes its consumer-overlay hash: run `pnpm overlay:hashes` (root; or `npm run overlay:hashes` from `packages/cli`) in the same commit to append the new hashes to `KNOWN_SHIPPED_OVERLAY_HASHES` (`packages/cli/src/lifecycle/overlay-known-hashes.ts`). The helper is append-only — prior entries are never removed or reordered — and `pnpm overlay:hashes:check` lists missing hashes (exit 1) without writing. Without the append, an unedited consumer copy is misread as customized and never refreshed. The "KNOWN_SHIPPED_OVERLAY_HASHES coverage" tests in `packages/cli/src/lifecycle/overlay.test.ts` enforce this for every L0 overlay artifact and every registry skill `SKILL.md` (core + community). ### Submission checklist (publisher HITL) @@ -96,7 +98,7 @@ The promotion path is ready only when the evidence gate is green; each step belo 1. **Evidence gate green:** `npm run evidence:knowledge-classification:check` passes on the staging tip being promoted. The shared ledger (`docs/evidence/knowledge-classification.json`) has **one regeneration owner** — the ship-5.0 evidence closeout (clean-tree regen guard + regen shipped in the PR #719–#725 stack). Marketplace-side plans record the dependency and never regenerate the ledger themselves. Note: the check recomputes from the working tree, so uncommitted `_index.md` rows pointing at untracked `plan-monitor-*.md` files fail it locally; verify at committed HEAD content (e.g. a clean worktree) when audit WIP is present. 2. **Merge pending staging PRs** in stack order (operator merge gate). 3. **`/git-prod`** (never agent-initiated) → annotated `vX.Y.Z` tag → `sync-public` PR, per the submission checklist above. -4. **Public mirror recheck:** verify the public repo exposes `.cursor-plugin/plugin.json` at the released version with explicit component paths. Owned by `submit-cursor-marketplace.plan.md` (`recheck-public-mirror`), alongside the publisher HITL submission gate. Rechecked 2026-08-14: public manifest is **5.2.0** with explicit `rules` / `skills` / `agents` / `commands` / `hooks`. +4. **Public mirror recheck:** verify the public repo exposes `.cursor-plugin/plugin.json` at the released version with explicit component paths. Rechecked live 2026-08-24 (`gh api repos/agent-kit-startup/agent-kit/contents/.cursor-plugin/plugin.json`): public manifest is **5.6.0**, matches the private manifest field-for-field — object `author`, explicit `rules` / `skills` / `agents` / `commands` / `hooks` / `logo` / `homepage`. The `/git-prod` → tag → `sync-public` chain has run multiple times since the 2026-08-14 recheck (5.2.0 → 5.6.0); no schema or path gap remains on the mirror side. Re-verify at the version about to be submitted before the actual publisher HITL step. 5. **Post-promote plugin smoke:** rerun the local-plugin smoke checklist ([plugin-smoke-checklist.md](plugin-smoke-checklist.md)) against the public ref before submitting. ## Listing UX (CLI) diff --git a/docs/public-launch.md b/docs/public-launch.md index 1a4b790..1a089ae 100644 --- a/docs/public-launch.md +++ b/docs/public-launch.md @@ -91,7 +91,7 @@ node scripts/sync-public.mjs --dry-run After `git prod` on the private repo, the pipeline automatically: 1. **Creates annotated vX.Y.Z tag** (triggers npm publish + sync-public jobs on **private** `agent-kit-dev`) -2. **Opens sync PR** with semantic body: Summary + CHANGELOG release notes + source SHA +2. **Opens sync PR** with semantic body: Summary + public CHANGELOG excerpt (private fences stripped) + source SHA 3. **Auto-merges PR** after required checks pass (`gh pr merge --auto`) Opt-out: Set `PUBLIC_SYNC_AUTO_MERGE=false` to require manual merge. diff --git a/docs/repository-boundaries.md b/docs/repository-boundaries.md index 4bc1dcf..d8e5b85 100644 --- a/docs/repository-boundaries.md +++ b/docs/repository-boundaries.md @@ -39,9 +39,9 @@ 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 (`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`. +4. **Public sync PR** - Creates semantic PR body (Summary + public CHANGELOG excerpt + source SHA) against public `main`. The copied `CHANGELOG.md` and GitHub Release notes are the stripped public excerpt, not the private-full developer log. 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. +6. **Public GitHub Release** - After the sync PR merges, sync creates/updates a public GitHub Release `vX.Y.Z` (public excerpt notes; Latest badge). Opt out with `PUBLIC_SYNC_CREATE_RELEASE=false`. Git tags alone do not move the Releases sidebar. Landing product notes are a separate short blurb (`node scripts/public-changelog.mjs --version X.Y.Z --blurb` into `pnpm landing:update-release`), not a `## [x.y.z]` dump. 7. **Fallback** (manual dispatch): `pnpm git:trigger-public-sync` or *workflow_dispatch* in Actions when tag-based trigger is insufficient. Without `PUBLIC_REPO_TOKEN` configured on the **private** GitHub repo, tag and manual public-sync jobs **fail loudly** (visible Actions error). Do not configure that secret on the **public** storefront; public tag runs skip `sync-public` / `publish-npm` entirely. diff --git a/docs/repository-readiness-onboarding.md b/docs/repository-readiness-onboarding.md index 6315d4e..6c60f5d 100644 --- a/docs/repository-readiness-onboarding.md +++ b/docs/repository-readiness-onboarding.md @@ -323,11 +323,14 @@ Operators in VS Code, Windsurf, or other IDEs that lack Cursor's slash commands ### CLI equivalents +A browser is not required to read Mission, Flight Log, Checklist, or Crew Monitor. Use `agent-kit mission-control` (live TTY) or `agent-kit mission-control --once` (one frame). `agent-kit dashboard` remains the browser panel (third surface; not replaced). + | 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` | +| `/dashboard` | `agent-kit dashboard` or `npx @dadado/agent-kit-cli dashboard` (browser panel) | +| `/agent-kit` (Claude Code) | `agent-kit mission-control --once` (ASCII snapshot; live TUI is `agent-kit mission-control`) | ### Readiness without slash diff --git a/docs/research/cartesi-primer.md b/docs/research/cartesi-primer.md new file mode 100644 index 0000000..137c558 --- /dev/null +++ b/docs/research/cartesi-primer.md @@ -0,0 +1,134 @@ +# Cartesi Primer — Primary-Source Study + +**Phase:** `phase0-primary-source-study` (`cartesi-decentralized-llm-compute.plan.md`) +**Accessed:** 2026-08-24, from this environment (unproxied — see "Environment correction" below). +**Method:** Primary sources only — official GitHub repos (via `gh api`, which returns exact commit SHAs), `docs.cartesi.io` (fetched live), the original 2018 whitepaper PDF (fetched and read in full, 16 pages), and `governance.cartesi.io`. Every claim below is tagged with its source URL and either a commit SHA (GitHub) or a page/doc-version identifier (docs site, whitepaper). Anything not independently confirmed here is marked `UNVERIFIED`. + +## Environment correction (important for later phases) + +The plan's "Evidence note (2026-08-24)" states that the Broad Intake research session's egress proxy **blocked** `docs.cartesi.io`, `cartesi.io`, and `docs.mugen.builders`, and that Phase 0 exists to re-derive claims "in an unblocked environment." **That block does not apply in this environment.** `docs.cartesi.io`, `cartesi.io/cartesi_whitepaper.pdf`, `governance.cartesi.io`, and all `github.com/cartesi/*` repositories were all directly reachable via `curl`, `WebFetch`, and `gh api` in this session, with no proxy interference observed. This primer is built entirely from those directly-fetched primary sources, not from search-result summaries. + +One correction to the plan's source list itself: **`cartesi/cartesi-coprocessor` does not exist.** The actual repository is **`cartesi/coprocessor`** (confirmed via `gh api repos/cartesi/coprocessor`, commit `f8c94a105ba3ba6edb7b130d91f5600aca66642b`). This primer cites the correct repo. + +--- + +## 1. The Cartesi Machine + +**What it is.** A deterministic RISC-V virtual machine that boots a full Linux operating system, used as the reproducible off-chain execution environment for Cartesi applications. + +- **Architecture:** Implements RV64GC-class RISC-V (the whitepaper specifies the more precise **RV64IMASU** ISA — 64-bit, Integer+Mul/Div, Atomics, Supervisor+User privilege levels, Sv48 virtual memory — 99 total instructions). Runs a full Linux kernel via a Berkeley Boot Loader (BBL)/SBI shim, giving DApp developers "all the programming languages, tools, libraries, software, and services they are already familiar with." *(Whitepaper §1, §3, p.1–4; `cartesi/machine-emulator` README.)* +- **Self-contained / isolated:** "The Cartesi machine is self-contained and can't make an external request." *(docs.cartesi.io, `cartesi-rollups/2.0/core-concepts/cartesi-machine/`, verbatim.)* +- **Deterministic / reproducible:** "Given the same initial state and input, the Cartesi Machine will always produce the same output and final state." State transitions happen deterministically as RISC-V instructions execute. Floating point is emulated via a consistent software layer (not native hardware FP) specifically to preserve bit-for-bit reproducibility across hosts — the whitepaper spends an entire section (§4.3) justifying this because "different floating-point implementations can disagree subtly when ostensibly performing the same operation." *(docs.cartesi.io `cartesi-rollups/2.0/core-concepts/cartesi-machine/`; Whitepaper §4.3, p.7.)* +- **State representation — Merkle root:** The machine's entire physical address space (64-bit) is exposed as a binary Merkle tree keyed on Keccak-256 (chosen for Ethereum compatibility), built bottom-up from 2^61 64-bit-word leaves to a single root hash. A state is represented on-chain purely as this root hash; contents are known only off-chain. The reference emulator maintains this tree in PATRICIA form for storage efficiency and updates it lazily (dirty-page bitmap + TLB) for time efficiency. *(Whitepaper §5.1, p.8, incl. equations (2)-(6); `cartesi/machine-emulator` README: "State Merkle tree computation, for generating cryptographic proofs.")* +- **Transparent / auditable:** the emulator is open-source C/C++ (POSIX-restricted to terminal/process/mmap), current stable release **v0.21.0** (published 2026-08-04), default branch `main` at commit `bd09538131e589319e371d7d65e81c2c82dd3411` as of 2026-08-23. *(`gh api repos/cartesi/machine-emulator`.)* +- **Runs standard software (llama.cpp-class workloads):** because it's a real Linux/RISC-V machine (not a custom VM/circuit), it can run any software that compiles for RISC-V Linux — the whitepaper's stated goal is exactly to avoid "ad-hoc new architecture" and "the porting of a toolchain and operating system" that custom smart-contract VMs would require. No primary source directly says "llama.cpp" or "PyTorch" run today — that capability follows from "full Linux + standard toolchain," not from a specific named benchmark. **Marking the specific claim "llama.cpp runs inside a Cartesi Machine today" `UNVERIFIED`** — it is a reasonable inference from the general-purpose-Linux design, not a primary-source-confirmed fact, and Phase 1's harness is precisely the place this gets tested for real. + +## 2. Rollups (2.0) — on-chain / off-chain architecture + +*Primary source: `docs.cartesi.io/cartesi-rollups/2.0/` (marked "Version: 2.0 (Unreleased)" on the architecture page, last-updated 2026-02-11), cross-checked against `cartesi/rollups-contracts` (commit `549408f8514f2ff74348bc3413524078b9e1dbb2`, default branch `main`, latest tagged release `v2.2.0` published 2026-02-06).* + +### On-chain layer +- **InputBox:** "All inputs destined for a Cartesi dApp are first submitted to this contract, which then emits events that the off-chain components can process." Entry point for data availability and transaction ordering. +- **Application contract** (docs call it "CartesiDApp"; the current contracts repo names the interface `Application`/`ApplicationFactory`): "acts as the on-chain representation of the dApp and can hold ownership of digital assets on the base layer, including Ether, ERC-20 tokens, and NFTs." +- **ApplicationFactory:** "allows developers to deploy new CartesiDApp instances with a single function call." +- **Portals:** asset-transfer contracts for ETH, ERC-20, ERC-721, ERC-1155 (single and batch) — `ERC20Portal` is the one named in the plan; confirmed present alongside `EtherPortal`, `ERC721Portal`, `ERC1155SinglePortal`, `ERC1155BatchPortal` in the 2.0 API reference tree. +- **IConsensus** and implementations: + - **Authority:** "Single-owner consensus controlled by one address." + - **Quorum:** "Multi-validator consensus requiring majority approval." + - **DaveConsensus:** confirmed via a separate docs page (`fraud-proofs/references/daveconsensus/`) — "the consensus contract for applications that use Dave-style tournaments (such as PRT) for verification," which "manages epochs, defined as half-open block-number intervals" and links "input ingestion, epoch progression, and Dave tournament-based verification under a single consensus interface." **The exact migration mechanics between Authority → Quorum → DaveConsensus were not found on a single primary page** in this pass — the consensus-overview page enumerates Authority and Quorum but did not surface DaveConsensus content in the same fetch, and a direct `.../consensus/daveconsensus/` API path returned 404. Treat "the migration path is a live, seamless swap" as `UNVERIFIED`; Phase 3 (which owns this specifically) should re-fetch `fraud-proofs/references/daveconsensus/` directly and also check `IConsensus`'s interface definition in the contracts repo source for whether swapping the consensus contract is a first-class supported operation or requires redeployment. +- **Claim structure:** a claim = {application contract address, last processed block number, outputs Merkle root}. Validated when: the block number lands on an epoch boundary (`block % epochLength == epochLength - 1`), the block is in the past, and no duplicate claim exists for that application+epoch. Once valid, "the outputs Merkle root becomes valid and can be used to validate individual outputs in the application contract." + +### Off-chain layer / data flow +- **Advance state:** modifications to application state, submitted via InputBox. The node processes each input, executes the backend logic in the Cartesi Machine, and produces outputs — **vouchers** (on-chain calls, e.g. ERC-20 transfers), **notices** (arbitrary informational log entries with a validity proof), and **reports** (diagnostic, non-authenticated output), queryable via the node's JSON/GraphQL API. +- **Inspect state:** "sends an inspect-state request to the dApp backend passing the payload string in the URL." Key properties, quoted: "the whole Cartesi Machine is rolled back after processing the inspect-state request" (machine mode); "this method is synchronous, so it is not advised to perform resource-intensive operations"; "Inspect-state inputs do not produce vouchers or notices." Response is a status (`accept`/`reject`/`exception`) plus reports only. This is the read-only query path — no state mutation, no on-chain settlement. +- **advance → finish handshake:** the backend calls `finish` after processing each advance request, with `status: accept | reject`. "If the advance-state request is rejected, the vouchers and notices are discarded. In contrast, reports are not discarded in case of rejection." The Rollup HTTP Server then returns the next pending request in the `finish` response body (long-poll style; host mode returns HTTP 202 after ~10s if nothing is pending). +- **Epoch/claim lifecycle:** the node "bundles multiple advance-state inputs into an epoch" and submits a claim (outputs Merkle root over that epoch) for on-chain validation once the epoch closes, per the IConsensus rules above. + +## 3. Coprocessor + EigenLayer AVS model + +*Primary source: `cartesi/coprocessor` README (commit `f8c94a105ba3ba6edb7b130d91f5600aca66642b`).* + +- **What it is:** "The Cartesi Coprocessor combines the technologies of EigenLayer and the Cartesi Machine to provide expressive, verifiable off-chain computation for Ethereum smart contracts," built as an EigenLayer AVS (Actively Validated Service). +- **Components (off-chain):** + - **Coprocessor Operator** — "a stateless executor that receives and runs Cartesi Machines with a specific input and signs the resulting computation. Operators do not retain any local state between requests." + - **Coprocessor Solver** — "a coordination agent that listens for computation requests emitted by smart contracts. It orchestrates the execution using a set of Operators, aggregates their BLS signatures on the result, and submits the aggregated proof back on-chain via a callback to the originating contract." +- **Model, precisely:** this is a smart-contract-requests-computation → operator-set-executes → **BLS-signature-aggregation** → **on-chain callback** pattern. It is **economically/cryptographically attested via EigenLayer restaking + operator signatures**, not a fraud-proof/dispute-game pattern — the README states this explicitly: "This architecture enables highly parallel, verifiable computation with strong cryptographic guarantees of correctness, **without requiring interactive fraud proofs or continual on-chain execution**." That is an important distinction for the plan's Phase 2 threat model: the Coprocessor's trust model is EigenLayer operator-set honesty/slashing, not Dave/PRT's one-of-N challenge game. Related repos: `coprocessor-solver`, `coprocessor-operator`, `coprocessor-advance-runner`, `coprocessor-evm-call` (all under `cartesi/`). +- **Deployment target:** targets EigenLayer's "Operator Sets" and "Slashing" contract generation; ships a Holesky testnet deployment config. Apache-2.0 licensed. + +## 4. Dave / PRT — one-of-N fraud-proof security + +*Primary sources: `cartesi/dave` README (commit `34d84178e4f120877ea87f496de9225617315f3f`, default branch `main`, latest tag `v2.1.1` published 2026-03-10) and `docs.cartesi.io/fraud-proofs/prt/prt-introduction/`.* + +- **What it is:** "Dave" is Cartesi's permissionless fraud-proof consensus system — Solidity contracts + a Rust validator node + Lua clients + dispute-algorithm specs, using the Cartesi Machine as the execution/verification substrate. Its initial implementation is **PRT (Permissionless Refereed Tournaments)**. +- **One-of-N security, verbatim from the repo:** Dave targets "one-of-N security: under the protocol's stated correctness, clock, and censorship assumptions, one correct validator can prevent an incorrect result." Docs echo this: "fraud proofs that are secure where 1-of-N honesty suffices, decentralized where anyone can participate." +- **Mechanism, high level (docs.cartesi.io):** incompatible claims enter a bracket-style tournament; each round is refereed by comparing **computation hashes** (Merkle trees over the *entire computational history*, not just final state) rather than the older single-final-state comparison — this lets mutually-honest validators collaborate without trusting each other, and prevents adversaries from "misplaying" honest claims mid-dispute. Disputes start from sparse hashes and recurse into denser hashes as the disagreement narrows, terminating in a one-step proof (this is the same bisection-to-one-step idea as the whitepaper's original verification game — see §5 below — generalized to survive Sybil crowds). Settlement time scales as "logarithm squared in the number of Sybils," while an honest validator's own workload stays logarithmic. Bond requirements scale similarly, making mass-Sybil attacks economically unattractive without requiring a permissioned validator set. +- **Relation to the 2018 whitepaper's verification game:** the whitepaper (§5.2, "Crowd disputes," §7) explicitly flags that its original two-player (Alice/Bob) verification game is vulnerable to a "coordinated crowd of dishonest participants ... using sequential disputes over an honest result as a denial-of-service attack," and says Cartesi has "developed a variant of the verification game that enables any honest participant to defend his result against an entire crowd at negligible cost" — PRT/Dave is that promised generalization, now shipped. + +## 5. The 2018 whitepaper — foundational model + +*Primary source: `cartesi.io/cartesi_whitepaper.pdf`, fetched directly (937,605 bytes, PDF 1.3, 16 pages). Title page: "This is the original whitepaper for the Cartesi project published in 2018 ... the Cartesi technology in its current state is the product of years of research, development, and contributions." Document itself: **"The Core of Cartesi," Version 1.01**, authors Augusto Teixeira and Diego Nehab.* + +Core ideas that still hold (per the docs/repo cross-checks above) vs. superseded: + +| Whitepaper concept (2018, v1.01) | Current status (2026, per primary sources above) | +|---|---| +| Cartesi Machine: self-contained, deterministic RISC-V, Linux-capable | **Unchanged in principle**; ISA/tooling has evolved (current emulator targets RV64GC-class per the repo, whitepaper specified RV64IMASU) | +| State = Merkle root over 64-bit address space, Keccak-256 | **Unchanged**, confirmed in `cartesi-rollups/2.0` outputs-Merkle-root claim structure | +| Two-player verification game (Alice/Bob, binary-search bisection to one step, arbiter = blockchain) | **Superseded/generalized** by Dave/PRT's tournament model, explicitly to fix the whitepaper's own acknowledged crowd-dispute weakness (§5.2, §7) | +| Dispute delegation market (principal delegates a dispute to a proxy for a fee, proxy stakes principal's collateral) | **Not found** as a currently-documented, named feature in `docs.cartesi.io` or the rollups-contracts / dave repos surveyed here — mark `UNVERIFIED` whether this shipped as designed, was renamed, or was dropped. Worth a direct follow-up search in a later phase if the plan's provider/scheduler design (Phase 6) wants to reuse this pattern. | +| "Cartesi Node" as the per-user off-chain software+hardware host | **Terminology evolved**: current docs speak of "Cartesi Rollups Node" with Validator/Reader modes, not the whitepaper's Alice/Bob/Charlie per-user node model — the 2018 paper predates the rollups architecture entirely (it describes a more general "expression DAG" model of composable disputable primitives, not the InputBox/Application/IConsensus contracts of Rollups 2.0) | +| Layer-2, hybrid on-chain/off-chain DApps, "any dispute settles at negligible on-chain cost" | **Unchanged as the core value proposition**, now delivered via the Rollups + Dave stack rather than the whitepaper's original generic expression-DAG interface | + +**Important framing correction for this plan:** the 2018 whitepaper describes an earlier, more general architecture (arbitrary "expression DAGs" of disputable/constant primitives, generic two-party verification games) that is **not** what ships today. The plan's Phase 2/3 architecture should be designed against the **current** Rollups 2.0 + Dave/PRT + Coprocessor stack (sections 1–4 above), using the whitepaper only for the foundational *why* (determinism requirement, Merkle-root state commitment, dispute-arbitration cost argument) — not as a literal current API reference. + +## 6. governance.cartesi.io + +*Primary source: fetched directly.* + +A Discourse-based governance forum ("Cartesi Governance - Grants and governance proposal area"), with categories: General Discussions, Metagovernance & Policies, Cartesi Grants Program, Technical Vision Forum, Proposed RFPs (explicitly "for proposing RFPs for high-impact work needed in the ecosystem, aligned with the CGP Intents"), and an Old & Archived RFPs section. This is the correct venue named in Phase 8's worker_contract ("ecosystem-fit note for governance.cartesi.io") — confirmed reachable and structurally suited to that deliverable. No specific proposal content was reviewed in this pass (out of scope for Phase 0; Phase 8 should read live threads before drafting its post). + +## 7. Claim table + +| # | Claim | Source URL | SHA / Version | Verified? | +|---|---|---|---|---| +| 1 | Cartesi Machine implements RISC-V, boots full Linux | `cartesi.io/cartesi_whitepaper.pdf` §3 | v1.01 (2018) | Verified | +| 2 | Cartesi Machine implements RV64IMASU (whitepaper) | `cartesi.io/cartesi_whitepaper.pdf` §3.1, p.4 | v1.01 | Verified | +| 3 | Cartesi Machine is self-contained, cannot make external requests | `docs.cartesi.io/cartesi-rollups/2.0/core-concepts/cartesi-machine/` | fetched 2026-08-24, docs site (no per-page version marker found) | Verified | +| 4 | Same input+initial state -> same output+final state (determinism) | `docs.cartesi.io/cartesi-rollups/2.0/core-concepts/cartesi-machine/` | fetched 2026-08-24 | Verified | +| 5 | State represented as Merkle tree, root = single hash, Keccak-based | `cartesi.io/cartesi_whitepaper.pdf` §5.1, p.8 | v1.01 | Verified | +| 6 | Current stable emulator release v0.21.0; default branch `main` | `github.com/cartesi/machine-emulator` | release `v0.21.0` (2026-08-04); commit `bd09538131e589319e371d7d65e81c2c82dd3411` (2026-08-23) | Verified (via `gh api`) | +| 7 | InputBox = data availability + input ordering entry point | `docs.cartesi.io/cartesi-rollups/2.0/getting-started/architecture/` | "2.0 (Unreleased)", last-updated 2026-02-11 | Verified | +| 8 | Application contract holds ETH/ERC-20/NFT ownership on base layer | `docs.cartesi.io/cartesi-rollups/2.0/getting-started/architecture/` | same as #7 | Verified | +| 9 | ApplicationFactory deploys new Application instances | `docs.cartesi.io/cartesi-rollups/2.0/getting-started/architecture/` | same as #7 | Verified | +| 10 | Portals support ETH, ERC-20, ERC-721, ERC-1155 (single+batch) | `docs.cartesi.io/cartesi-rollups/2.0/api-reference/contracts/overview/` sitemap paths | 2.0 | Verified (via sitemap + architecture page) | +| 11 | Authority = single-owner consensus | `docs.cartesi.io/cartesi-rollups/2.0/api-reference/contracts/consensus/overview/` | 2.0 | Verified | +| 12 | Quorum = multi-validator majority consensus | same as #11 | 2.0 | Verified | +| 13 | DaveConsensus manages epochs (half-open block intervals), ties input ingestion + epoch progression + Dave tournaments under one IConsensus | `docs.cartesi.io/fraud-proofs/references/daveconsensus/` | fetched 2026-08-24 | Verified | +| 14 | Exact Authority -> Quorum -> DaveConsensus migration mechanics | not found on a single primary page in this pass | — | **UNVERIFIED** — re-check in Phase 3 | +| 15 | rollups-contracts repo: default branch `main`, latest tag `v2.2.0` | `github.com/cartesi/rollups-contracts` | tag `v2.2.0` (2026-02-06); commit `549408f8514f2ff74348bc3413524078b9e1dbb2` (2026-08-19) | Verified (via `gh api`) | +| 15b | Claim = {app address, last processed block, outputs Merkle root}; validity rules (epoch-boundary block, past block, no duplicate) | `docs.cartesi.io/cartesi-rollups/2.0/api-reference/contracts/consensus/overview/` | 2.0 | Verified | +| 16 | Advance = InputBox-submitted, produces vouchers/notices/reports | `docs.cartesi.io/cartesi-rollups/2.0/getting-started/architecture/`, `.../api-reference/rollup/finish/` | 2.0 | Verified | +| 17 | Inspect = synchronous, read-only, machine rolled back after, no vouchers/notices, reports+status only | `docs.cartesi.io/cartesi-rollups/2.0/api-reference/inspect/inspect/` | 2.0 | Verified | +| 18 | finish() advance semantics: accept/reject; rejected discards vouchers/notices but keeps reports; long-poll for next input | `docs.cartesi.io/cartesi-rollups/2.0/api-reference/rollup/finish/` | 2.0 | Verified | +| 19 | Correct Coprocessor repo is `cartesi/coprocessor`, not `cartesi/cartesi-coprocessor` | `github.com/cartesi/coprocessor` | commit `f8c94a105ba3ba6edb7b130d91f5600aca66642b` | Verified (404 confirmed for the plan's originally-named repo) | +| 20 | Coprocessor = EigenLayer AVS; Operator (stateless executor, signs result) + Solver (aggregates BLS sigs, submits callback) | `github.com/cartesi/coprocessor` README | commit `f8c94a105ba3ba6edb7b130d91f5600aca66642b` | Verified | +| 21 | Coprocessor explicitly does NOT use interactive fraud proofs — attested via signature aggregation, not dispute game | same as #20 | same | Verified — **material for Phase 2 threat model: Coprocessor security = EigenLayer operator honesty/slashing, not Dave-style 1-of-N** | +| 22 | Dave = permissionless fraud-proof system (Solidity + Rust validator + Lua clients); PRT = initial tournament implementation | `github.com/cartesi/dave` README | commit `34d84178e4f120877ea87f496de9225617315f3f`; tag `v2.1.1` (2026-03-10) | Verified | +| 23 | Dave/PRT one-of-N: "one correct validator can prevent an incorrect result" | `github.com/cartesi/dave` README | same as #22 | Verified | +| 24 | PRT tournament mechanism: computation-hash brackets, sparse->dense recursion, one-step proof, settlement scales as log^2(Sybils) | `docs.cartesi.io/fraud-proofs/prt/prt-introduction/` | fetched 2026-08-24 | Verified | +| 25 | Whitepaper's original verification game (2-player bisection) is vulnerable to crowd DoS; a crowd-resistant variant was "developed" (i.e., promised) | `cartesi.io/cartesi_whitepaper.pdf` §7, p.14 | v1.01 (2018) | Verified as a 2018 statement of intent; PRT/Dave (item 24) is the shipped realization — **inference**, not a primary source explicitly saying "PRT is that promised variant" | +| 26 | Whitepaper's "dispute delegation market" (proxy buys a dispute role for a fee+stake) | `cartesi.io/cartesi_whitepaper.pdf` §6.2, p.13 | v1.01 (2018) | Verified as a 2018 design; **current shipping status UNVERIFIED** — not found in docs.cartesi.io or dave/rollups-contracts repos surveyed here | +| 27 | Fraud-proof bonds: dishonest party's bond is slashed (partial or full) on a proven-wrong claim | `docs.cartesi.io/fraud-proofs/fraud-proof-basics/introduction/` | fetched 2026-08-24 | Verified | +| 28 | Honeypot = Stage-2 app-specific rollup, asymmetric-access ERC-20 vault, built to test PRT security in a real deployment | `docs.cartesi.io/fraud-proofs/honeypot/introduction/` | fetched 2026-08-24 | Verified — **not confirmed as a currently-live bounty with real funds**; Phase 4/8 should re-check live status before citing it as an active adversarial-proof program | +| 29 | governance.cartesi.io = Discourse forum with Grants/RFP/Technical Vision categories | `governance.cartesi.io` | fetched 2026-08-24 | Verified | +| 30 | CTSI staking: delegation-based pools, automatic reward compounding | `docs.cartesi.io/earn-ctsi/staking/` | page states "Last updated on May 7, 2024" | Verified as documented, but **the page is ~2.3 years stale relative to today (2026-08-24)** — treat current CTSI/Noether staking mechanics as `UNVERIFIED-CURRENT` pending a Phase-4-time re-check (this is the same item the plan's own Evidence note flagged; it remains open, now with a concrete staleness date attached) | +| 31 | Emulation slowdown factor vs. bare metal ("at least 50x") | not re-derived here — explicitly out of Phase 0 scope | — | **UNVERIFIED**, deliberately deferred to Phase 1 measurement per the plan's own Evidence note and Phase 1 worker_contract | +| 32 | llama.cpp (or any specific LLM-inference stack) runs inside a Cartesi Machine today | no primary source found stating this specifically | — | **UNVERIFIED** — inferred only from "general-purpose Linux/RISC-V machine," not confirmed by a named benchmark or example; Phase 1 is the actual test | + +## 8. Open items carried forward + +- **Phase 1 (determinism/perf spike):** must independently measure the emulation slowdown factor (#31) and should confirm llama.cpp actually boots and runs inside the machine (#32) before any architecture decision leans on it. +- **Phase 3 (verification/disputes):** re-fetch `docs.cartesi.io/fraud-proofs/references/daveconsensus/` and the `IConsensus` Solidity interface directly to nail down the Authority → Quorum → DaveConsensus migration mechanics (#14) — this primer found DaveConsensus's *role* but not the *migration procedure*. +- **Phase 2 (threat model):** the Coprocessor's trust model (#21 — EigenLayer operator-set attestation, not a dispute game) is a hard input to the two-lane design. Lane B ("attested-accelerated") should be modeled on the Coprocessor's actual mechanism (signature aggregation over an operator set with slashing), not on a hand-waved generic "attestation." +- **Phase 4 (tokenomics):** the CTSI staking page (#30) is stale (dated May 2024) relative to today; do not design the delegation-pool model against it without a fresh check of current staking mechanics (Noether or its successor) and the Honeypot's live-bounty status (#28). +- **Phase 6/8 (provider node / pilot):** the whitepaper's "dispute delegation market" (#26) is architecturally close to what a censorship-resistant scheduler fallback needs (a party that can defend a claim on your behalf for a fee) — worth a direct check of whether an equivalent exists in the current Dave/PRT tooling before designing a new one from scratch. diff --git a/install.md b/install.md index 159b619..c972d6d 100644 --- a/install.md +++ b/install.md @@ -108,6 +108,8 @@ Copy **only** these artifacts (same content from SoT / registry), not the monore | `.cursor/commands/dashboard-broadcast.md` | idem | | `.cursor/commands/git-staging.md` | idem | | `.cursor/commands/git-prod.md` | idem | +| `.cursor/commands/kit-staging.md` | idem | +| `.cursor/commands/kit-prod.md` | idem | | `.cursor/commands/plan-external-review.md` | idem | | `.cursor/commands/plan-review-triage.md` | idem | | `.cursor/commands/field-report-resolve.md` | idem | diff --git a/package.json b/package.json index cde03a4..80cbb8e 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "agent-kit", - "version": "5.6.0", - "description": "HITL framework for AI-assisted IDEs: plan, handoff, staging-to-prod, memory loop; project-aware setup for Cursor, VS Code, and Windsurf.", + "version": "5.7.0", + "description": "HITL framework for AI-assisted IDEs: plan, handoff, staging-to-prod, memory loop; Cursor-first with partial generators for VS Code and Windsurf.", "private": true, "license": "PolyForm-Noncommercial-1.0.0", "packageManager": "pnpm@10.0.0", @@ -35,11 +35,13 @@ "overlay:hashes": "pnpm --dir packages/cli run overlay:hashes", "overlay:hashes:check": "pnpm --dir packages/cli run overlay:hashes:check", "check:public-deny-links:test": "node --test scripts/check-public-deny-links.test.mjs", - "test:root-node": "node --test .cursor/scripts/plan-external-review-progress-gate.test.mjs .cursor/scripts/plan-external-review-cloud-backend.test.mjs .cursor/scripts/plan-external-review-atomic-wait.test.mjs .cursor/scripts/plan-external-review-backend-cascade.test.mjs .cursor/scripts/plan-external-review-model-routing.test.mjs .cursor/scripts/comms-draft.test.mjs scripts/check-public-deny-links.test.mjs scripts/check-markdown-links.test.mjs scripts/check-guidance-stale-claims.test.mjs scripts/validate-memory-index.test.mjs scripts/hook-session-start-diagnostic.test.mjs scripts/orchestration-defects-regression.test.mjs scripts/sync-landing.test.mjs scripts/sync-public-guards.test.mjs scripts/verify-cli-dashboard-pack.test.mjs scripts/git-hooks-pre-commit-composed.test.mjs", + "changelog:public": "node scripts/public-changelog.mjs", + "test:root-node": "node --test .cursor/scripts/plan-external-review-progress-gate.test.mjs .cursor/scripts/plan-external-review-cloud-backend.test.mjs .cursor/scripts/plan-external-review-atomic-wait.test.mjs .cursor/scripts/plan-external-review-backend-cascade.test.mjs .cursor/scripts/plan-external-review-model-routing.test.mjs .cursor/scripts/comms-draft.test.mjs scripts/check-public-deny-links.test.mjs scripts/check-markdown-links.test.mjs scripts/check-guidance-stale-claims.test.mjs scripts/validate-memory-index.test.mjs scripts/hook-session-start-diagnostic.test.mjs scripts/orchestration-defects-regression.test.mjs scripts/sync-landing.test.mjs scripts/update-landing-release.test.mjs scripts/public-changelog.test.mjs scripts/kit-landing-gate.test.mjs scripts/sync-public-guards.test.mjs scripts/verify-cli-dashboard-pack.test.mjs scripts/git-hooks-pre-commit-composed.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:update-release": "node scripts/update-landing-release.mjs", "landing:serve": "node scripts/serve-landing.mjs", "landing:deploy:staging": "node scripts/deploy-landing-staging.mjs", "landing:verify": "node scripts/verify-landing.mjs", diff --git a/packages/cli/README.md b/packages/cli/README.md index 0c2c974..8a74fbc 100644 --- a/packages/cli/README.md +++ b/packages/cli/README.md @@ -43,6 +43,8 @@ agent-kit dashboard The panel binds to loopback by default, serves its own static files, and snapshots the current workspace. L0 install does **not** copy `dashboard/` into your app; `agent-kit dashboard` resolves the panel from the installed package. +Browser-free: `agent-kit mission-control` is a third surface (ASCII Mission, Flight Log, Checklist, Crew Monitor) that reuses the same snapshot builders without starting the HTTP server. `agent-kit mission-control --once` prints one frame (Claude Code `/agent-kit`). The web dashboard stays shipped. + 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) @@ -73,7 +75,8 @@ On an interactive TTY, long-running commands (`init`, `install`, `doctor`, `upda | `agent-kit doctor` | Diagnose repository readiness (`--json` includes an `env` pillar: bin-on-PATH, npm prefix writability, Node version, shell profile) | | `agent-kit setup-global` | Self-heal a root-owned npm global prefix (relocate to `~/.npm-global`, fix `PATH`, reinstall) | | `agent-kit update` | Re-apply L0/packs/skills from the registry | -| `agent-kit dashboard` | Start Mission Control for this workspace | +| `agent-kit dashboard` | Start Mission Control for this workspace (browser panel) | +| `agent-kit mission-control` | ASCII Mission Control TUI (`--once` for one frame) | | `agent-kit add <id>` | Install a skill or L1 pack | | `agent-kit run-plan` | Headless continuous plan runner (never promotes to production) | diff --git a/packages/cli/package.json b/packages/cli/package.json index 358f465..1183a75 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@dadado/agent-kit-cli", - "version": "5.6.0", + "version": "5.7.0", "description": "Agent Kit CLI: HITL framework install and tooling for AI-assisted IDEs (rules, skills, plan/handoff, context).", "license": "PolyForm-Noncommercial-1.0.0", "type": "module", diff --git a/packages/cli/src/commands/dashboard.ts b/packages/cli/src/commands/dashboard.ts index c2f68a1..512ec17 100644 --- a/packages/cli/src/commands/dashboard.ts +++ b/packages/cli/src/commands/dashboard.ts @@ -71,7 +71,7 @@ export function dashboardProcessTitle(snapshotRoot: string): string { /** Candidates for dashboard assets shipped beside the CLI package (Path C). */ export function bundledDashboardCandidates( - filename: "start.mjs" | "start-broadcast.mjs", + filename: "start.mjs" | "start-broadcast.mjs" | "dashboard-data.mjs", moduleUrl: string = import.meta.url, ): string[] { const here = path.dirname(fileURLToPath(moduleUrl)); diff --git a/packages/cli/src/commands/doctor.ts b/packages/cli/src/commands/doctor.ts index 015447b..37b5b3d 100644 --- a/packages/cli/src/commands/doctor.ts +++ b/packages/cli/src/commands/doctor.ts @@ -4,7 +4,7 @@ import { type HooksHealthReport, assessHooksHealth } from "../invariants/hooks-h import { KIT_VERSION } from "../lifecycle/version.js"; import { type EnvironmentReport, assessEnvironment } from "../readiness/env-checks.js"; import { createReadinessReport } from "../scanner/readiness.js"; -import { executeSafeReadinessFixes } from "../scanner/safe-fixes.js"; +import { executeSafeReadinessFixes, refreshRepositoryProfile } from "../scanner/safe-fixes.js"; import { runScanner } from "../scanner/scan.js"; import { writeReadinessSnapshot } from "../scanner/snapshot.js"; import type { ReadinessReport, SafeReadinessChange } from "../types.js"; @@ -15,11 +15,13 @@ export interface DoctorResult { safeChanges: SafeReadinessChange[]; hooks: HooksHealthReport; env: EnvironmentReport; + /** Present only when `--refresh-profile` ran: whether the on-disk profile changed. */ + profileRefreshed?: boolean; } export async function runDoctor( cwd: string, - options: { fixSafe?: boolean; generatedAt?: string } = {}, + options: { fixSafe?: boolean; refreshProfile?: boolean; generatedAt?: string } = {}, ): Promise<DoctorResult> { const rootDir = path.resolve(cwd); const hooks = await assessHooksHealth(rootDir); @@ -35,6 +37,20 @@ export async function runDoctor( return { report: execution.after, safeChanges: execution.changes, hooks, env }; } + if (options.refreshProfile) { + const refresh = await refreshRepositoryProfile(rootDir, { + generatorVersion: KIT_VERSION, + generatedAt: options.generatedAt, + }); + const scan = await runScanner(rootDir); + const report = createReadinessReport(scan, { + generatorVersion: KIT_VERSION, + generatedAt: options.generatedAt, + }); + await writeReadinessSnapshot(rootDir, report); + return { report, safeChanges: [], hooks, env, profileRefreshed: refresh.changed }; + } + const scan = await runScanner(rootDir); const report = createReadinessReport(scan, { generatorVersion: KIT_VERSION, @@ -54,6 +70,11 @@ function printDoctorSummary(result: DoctorResult): void { ); console.log(` safe fixes applied: ${fixed}`); console.log(` pending actions: ${pendingActions.length}`); + if (result.profileRefreshed !== undefined) { + console.log( + ` profile refreshed: ${result.profileRefreshed ? "yes (facts changed)" : "no (already current)"}`, + ); + } console.log(`hooks: ${result.hooks.status}`); if (result.hooks.reasons.length > 0) { for (const reason of result.hooks.reasons.slice(0, 5)) { @@ -120,9 +141,19 @@ export const doctorCommand = defineCommand({ description: "Apply only local, reversible, merge-safe readiness fixes", default: false, }, + "refresh-profile": { + type: "boolean", + description: + "Reconcile .cursor/agent-kit.config.json with current scanner facts: fresh values win on shared keys, unrecognized existing keys are preserved", + default: false, + }, }, async run({ args }) { - const run = () => runDoctor(args.cwd, { fixSafe: args["fix-safe"] }); + const run = () => + runDoctor(args.cwd, { + fixSafe: args["fix-safe"], + refreshProfile: args["refresh-profile"], + }); const result = args.json ? await run() : await withCliProgress("doctor", run); if (args.json) { console.log(JSON.stringify(result, null, 2)); diff --git a/packages/cli/src/commands/handoff.test.ts b/packages/cli/src/commands/handoff.test.ts new file mode 100644 index 0000000..871864e --- /dev/null +++ b/packages/cli/src/commands/handoff.test.ts @@ -0,0 +1,74 @@ +import { execFile } from "node:child_process"; +import { mkdtemp, writeFile } from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import { promisify } from "node:util"; +import { describe, expect, it } from "vitest"; +import { detectGit } from "../scanner/detect-git.js"; +import type { ProjectProfile } from "../types.js"; +import { buildRoutines } from "./handoff.js"; + +const exec = promisify(execFile); + +async function git(root: string, ...args: string[]): Promise<void> { + await exec("git", args, { cwd: root }); +} + +async function initializeGit(root: string): Promise<void> { + await git(root, "init", "-b", "main"); + await git(root, "config", "user.email", "handoff@example.test"); + await git(root, "config", "user.name", "Handoff Test"); + await git(root, "add", "."); + await git(root, "commit", "-m", "test fixture"); +} + +function profileWithWorkflow(workflow: ProjectProfile["git"]["workflow"]): ProjectProfile { + return { + git: { workflow }, + services: {}, + } as unknown as ProjectProfile; +} + +describe("buildRoutines", () => { + it("suggests staging/prod routines when the profile workflow is homolog-prod", () => { + const routines = buildRoutines(profileWithWorkflow("homolog-prod")); + + expect(routines).toEqual( + expect.arrayContaining([ + "- [ ] `git staging` - move changes to staging", + "- [ ] `git prod` - promote to production (after approval)", + ]), + ); + }); + + it("suggests a plain PR/MR routine for feature-pr workflows", () => { + const routines = buildRoutines(profileWithWorkflow("feature-pr")); + + expect(routines).toContain("- [ ] Commit, push and open PR/MR"); + expect(routines.some((line) => line.includes("git staging"))).toBe(false); + }); + + it("returns the PR/MR routine when there is no profile at all", () => { + const routines = buildRoutines(null); + + expect(routines).toContain("- [ ] Commit, push and open PR/MR"); + }); + + it("benefits from the branch-independent workflow fix: staging exists but HEAD is on main", async () => { + const root = await mkdtemp(path.join(os.tmpdir(), "agent-kit-handoff-routines-")); + await writeFile(path.join(root, "README.md"), "# Repository\n"); + await initializeGit(root); + await git(root, "branch", "staging"); + + const git_ = await detectGit(root); + const routines = buildRoutines({ git: git_, services: {} } as unknown as ProjectProfile); + + expect(git_.currentBranch).toBe("main"); + expect(routines).toEqual( + expect.arrayContaining([ + "- [ ] `git staging` - move changes to staging", + "- [ ] `git prod` - promote to production (after approval)", + ]), + ); + }, 15_000); +}); diff --git a/packages/cli/src/commands/handoff.ts b/packages/cli/src/commands/handoff.ts index 973383d..9991f24 100644 --- a/packages/cli/src/commands/handoff.ts +++ b/packages/cli/src/commands/handoff.ts @@ -58,7 +58,7 @@ async function loadProfile(rootDir: string): Promise<ProjectProfile | null> { } } -function buildRoutines(profile: ProjectProfile | null): string[] { +export function buildRoutines(profile: ProjectProfile | null): string[] { const lines: string[] = []; const workflow = profile?.git.workflow; if (workflow === "homolog-prod") { diff --git a/packages/cli/src/commands/mission-control.test.ts b/packages/cli/src/commands/mission-control.test.ts new file mode 100644 index 0000000..cf1c9c9 --- /dev/null +++ b/packages/cli/src/commands/mission-control.test.ts @@ -0,0 +1,16 @@ +import { describe, expect, it } from "vitest"; +import { missionControlCommand } from "./mission-control.js"; + +describe("missionControlCommand", () => { + it("registers the citty subcommand with --once", async () => { + const meta = await (typeof missionControlCommand.meta === "function" + ? missionControlCommand.meta() + : missionControlCommand.meta); + expect(meta?.name).toBe("mission-control"); + expect(meta?.description).toMatch(/without a browser/i); + const args = await (typeof missionControlCommand.args === "function" + ? missionControlCommand.args() + : missionControlCommand.args); + expect(args).toHaveProperty("once"); + }); +}); diff --git a/packages/cli/src/commands/mission-control.ts b/packages/cli/src/commands/mission-control.ts new file mode 100644 index 0000000..4185f5b --- /dev/null +++ b/packages/cli/src/commands/mission-control.ts @@ -0,0 +1,80 @@ +import { defineCommand } from "citty"; +import { runMcTuiLoop } from "../mission-control/loop.js"; +import { + findDashboardDataScript, + loadDashboardSnapshot, + resolveDashboardSnapshotRoot, +} from "../mission-control/snapshot.js"; +import { buildMcTuiView } from "../mission-control/view.js"; +import { logger } from "../utils/logger.js"; + +export const missionControlCommand = defineCommand({ + meta: { + name: "mission-control", + description: + "ASCII Mission Control (mission, flight log, checklist, crew monitor) without a browser.", + }, + args: { + cwd: { + type: "string", + default: process.cwd(), + description: "Workspace to snapshot (nearest .cursor/agent-kit.json, else git root)", + }, + once: { + type: "boolean", + default: false, + description: "Print one frame and exit (Claude Code / non-TTY / CI)", + }, + }, + async run({ args }) { + const snapshotRoot = resolveDashboardSnapshotRoot(args.cwd); + const dataScript = await findDashboardDataScript(args.cwd); + if (!dataScript) { + logger.error("No dashboard/dashboard-data.mjs found."); + console.error( + [ + "", + "The Mission Control snapshot script is not available in this workspace.", + "The TUI reuses the Path C dashboard data collector; it does not start an HTTP server.", + "", + "Recovery (pick one):", + " 1. Upgrade the CLI: npx @dadado/agent-kit-cli@latest mission-control", + " 2. Set MISSION_CONTROL_KIT_ROOT or AGENT_KIT_HOME to an agent-kit checkout", + " 3. Place an agent-kit sibling: ../agent-kit/dashboard/dashboard-data.mjs", + "", + ].join("\n"), + ); + process.exitCode = 1; + return; + } + + const handle = await runMcTuiLoop({ + once: Boolean(args.once), + stdoutIsTTY: Boolean(process.stdout.isTTY), + env: process.env, + renderOpts: { stdoutIsTTY: Boolean(process.stdout.isTTY) }, + loadView: async () => { + const loaded = await loadDashboardSnapshot({ + dataScript, + snapshotRoot, + }); + if (!loaded.ok) return buildMcTuiView(null, loaded.error); + return buildMcTuiView(loaded.snapshot); + }, + hooks: { + write: (chunk) => { + process.stdout.write(chunk); + }, + }, + }); + + if (!handle) return; + + const onSignal = () => { + handle.stop(); + process.exit(0); + }; + process.on("SIGINT", onSignal); + process.on("SIGTERM", onSignal); + }, +}); diff --git a/packages/cli/src/commands/readiness.test.ts b/packages/cli/src/commands/readiness.test.ts index 7c92935..2e2b45b 100644 --- a/packages/cli/src/commands/readiness.test.ts +++ b/packages/cli/src/commands/readiness.test.ts @@ -69,6 +69,51 @@ describe("readiness commands", () => { expect(typeof result.env.nodeVersionOk).toBe("boolean"); }, 20_000); + it("is idempotent: refreshing twice in a row is a no-op on the second run", async () => { + const root = await mkdtemp(path.join(os.tmpdir(), "agent-kit-doctor-refresh-noop-")); + await writeFile(path.join(root, "README.md"), "# Consumer\n"); + await performInstall({ cwd: root, registry: REPOSITORY_ROOT }); + // Install writes the profile mid-process (before later generator steps + // add more files), so the very first refresh legitimately picks up + // additional facts; idempotency is about the *second* refresh onward. + await runDoctor(root, { refreshProfile: true, generatedAt: GENERATED_AT }); + const before = await readFile(path.join(root, ".cursor/agent-kit.config.json"), "utf8"); + + const result = await runDoctor(root, { + refreshProfile: true, + generatedAt: "2026-07-25T12:00:00.000Z", + }); + const after = await readFile(path.join(root, ".cursor/agent-kit.config.json"), "utf8"); + + expect(result.profileRefreshed).toBe(false); + expect(after).toBe(before); + }, 20_000); + + it("reconciles a stale profile on demand via doctor --refresh-profile", async () => { + const root = await mkdtemp(path.join(os.tmpdir(), "agent-kit-doctor-refresh-stale-")); + await writeFile(path.join(root, "README.md"), "# Consumer\n"); + await performInstall({ cwd: root, registry: REPOSITORY_ROOT }); + const staleProfile = await readFile(path.join(root, ".cursor/agent-kit.config.json"), "utf8"); + + // Simulate drift: hand-edit the profile the way an old install-time + // snapshot would look, with a git.currentBranch that no longer matches + // the repository's real state and no `.git` (local-only repo case). + const staleParsed = JSON.parse(staleProfile); + staleParsed.git = { ...staleParsed.git, currentBranch: "stale-branch-name" }; + await writeFile( + path.join(root, ".cursor/agent-kit.config.json"), + `${JSON.stringify(staleParsed, null, 2)}\n`, + ); + + const result = await runDoctor(root, { refreshProfile: true, generatedAt: GENERATED_AT }); + const refreshed = JSON.parse( + await readFile(path.join(root, ".cursor/agent-kit.config.json"), "utf8"), + ); + + expect(result.profileRefreshed).toBe(true); + expect(refreshed.git.currentBranch).not.toBe("stale-branch-name"); + }, 20_000); + it("keeps init as a compatibility wrapper over install", async () => { const expected = { projectRoot: "/tmp/example" } as InstallResult; const installer = vi.fn(async () => expected); diff --git a/packages/cli/src/dashboard/dashboard-data-snapshot.test.ts b/packages/cli/src/dashboard/dashboard-data-snapshot.test.ts new file mode 100644 index 0000000..954d3d3 --- /dev/null +++ b/packages/cli/src/dashboard/dashboard-data-snapshot.test.ts @@ -0,0 +1,182 @@ +// Integration coverage for dashboard/dashboard-data.mjs run as a real subprocess +// against a scratch git repo. Unlike the "wiring" tests elsewhere in this +// directory (which grep dashboard-data.mjs's source text), these tests execute +// the script end-to-end so a regression in the git-status trim or the +// kitManaged registry/fallback resolution actually fails the suite. +import { execFileSync } from "node:child_process"; +import { mkdirSync, mkdtempSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join, resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import { describe, expect, it } from "vitest"; + +const repoRoot = resolve(fileURLToPath(import.meta.url), "../../../../.."); +const dashboardDataScript = resolve(repoRoot, "dashboard/dashboard-data.mjs"); + +/** Minimal local git identity so `git commit` never hits an interactive prompt. */ +function initGitRepo(dir: string) { + execFileSync("git", ["init", "-q"], { cwd: dir }); + execFileSync("git", ["config", "user.email", "dashboard-data-test@example.com"], { cwd: dir }); + execFileSync("git", ["config", "user.name", "Dashboard Data Test"], { cwd: dir }); + execFileSync("git", ["config", "commit.gpgsign", "false"], { cwd: dir }); +} + +/** Run dashboard-data.mjs against `repo` as MISSION_CONTROL_REPO_ROOT and parse its JSON stdout. */ +function runDashboardData(repo: string): Record<string, unknown> { + const out = execFileSync("node", [dashboardDataScript], { + cwd: repo, + encoding: "utf8", + env: { + ...process.env, + MISSION_CONTROL_REPO_ROOT: repo, + // Keep optional collectors from doing extra work on a scratch repo. + AGENT_KIT_DASHBOARD_DATA_BUDGET_MS: "5000", + }, + }); + return JSON.parse(out); +} + +describe("dashboard-data.mjs: git status snapshot (Phase 0 - trim regression)", () => { + it("reports an unstaged-only first row correctly instead of shifting it to staged", () => { + const repo = mkdtempSync(join(tmpdir(), "ak-dashdata-git-")); + initGitRepo(repo); + writeFileSync(join(repo, "unstaged-only.txt"), "v1\n"); + execFileSync("git", ["add", "unstaged-only.txt"], { cwd: repo }); + execFileSync("git", ["commit", "-q", "-m", "init"], { cwd: repo }); + // Modify the tracked file without staging: `git status --short` for this + // single-file repo now produces exactly one line, " M unstaged-only.txt" - + // a leading status-column space on line 1 that a full-string .trim() would eat. + writeFileSync(join(repo, "unstaged-only.txt"), "v2\n"); + + const rawStatus = execFileSync("git", ["status", "--short"], { cwd: repo, encoding: "utf8" }); + expect(rawStatus).toBe(" M unstaged-only.txt\n"); + + const snapshot = runDashboardData(repo) as { git: { files: Array<Record<string, unknown>> } }; + + expect(snapshot.git.files).toHaveLength(1); + expect(snapshot.git.files[0]).toMatchObject({ + path: "unstaged-only.txt", + status: " M", + staged: false, + unstaged: true, + untracked: false, + }); + }); + + it("keeps the first row's staged/unstaged read correct even with other rows following", () => { + const repo = mkdtempSync(join(tmpdir(), "ak-dashdata-git-multi-")); + initGitRepo(repo); + writeFileSync(join(repo, "unstaged-only.txt"), "v1\n"); + writeFileSync(join(repo, "staged-only.txt"), "v1\n"); + execFileSync("git", ["add", "unstaged-only.txt", "staged-only.txt"], { cwd: repo }); + execFileSync("git", ["commit", "-q", "-m", "init"], { cwd: repo }); + writeFileSync(join(repo, "unstaged-only.txt"), "v2\n"); // unstaged modification + writeFileSync(join(repo, "staged-only.txt"), "v2\n"); + execFileSync("git", ["add", "staged-only.txt"], { cwd: repo }); // staged modification + + const snapshot = runDashboardData(repo) as { git: { files: Array<Record<string, unknown>> } }; + const byPath = Object.fromEntries(snapshot.git.files.map((f) => [f.path, f])); + + expect(byPath["unstaged-only.txt"]).toMatchObject({ + status: " M", + staged: false, + unstaged: true, + }); + expect(byPath["staged-only.txt"]).toMatchObject({ + status: "M ", + staged: true, + unstaged: false, + }); + }); +}); + +describe("dashboard-data.mjs: kitManaged resolution (Phase 1 - consumer fallback)", () => { + function writeCommand(repo: string, file: string) { + mkdirSync(join(repo, ".cursor", "commands"), { recursive: true }); + writeFileSync(join(repo, ".cursor", "commands", file), `# ${file}\n`); + } + + it("falls back to .cursor/agent-kit.managed-hashes.json when registry/registry.json is absent", () => { + const repo = mkdtempSync(join(tmpdir(), "ak-dashdata-kitmanaged-")); + initGitRepo(repo); + writeCommand(repo, "run-plan.md"); + writeCommand(repo, "project-local.md"); + writeFileSync( + join(repo, ".cursor", "agent-kit.managed-hashes.json"), + JSON.stringify({ + schemaVersion: 1, + hashes: { ".cursor/commands/run-plan.md": "deadbeef" }, + }), + ); + execFileSync("git", ["add", "-A"], { cwd: repo }); + execFileSync("git", ["commit", "-q", "-m", "init"], { cwd: repo }); + + const snapshot = runDashboardData(repo) as { + commands: Array<{ path: string; kitManaged: boolean }>; + }; + const byPath = Object.fromEntries(snapshot.commands.map((c) => [c.path, c.kitManaged])); + + expect(byPath[".cursor/commands/run-plan.md"]).toBe(true); + expect(byPath[".cursor/commands/project-local.md"]).toBe(false); + }); + + it("also honors .cursor/agent-kit.json's protected[] exact-path entries as a fallback source", () => { + const repo = mkdtempSync(join(tmpdir(), "ak-dashdata-kitmanaged-protected-")); + initGitRepo(repo); + writeCommand(repo, "hotfix.md"); + writeCommand(repo, "project-local.md"); + writeFileSync( + join(repo, ".cursor", "agent-kit.json"), + JSON.stringify({ + schemaVersion: 1, + protected: [".cursor/commands/hotfix.md", ".cursor/plans/**"], + }), + ); + execFileSync("git", ["add", "-A"], { cwd: repo }); + execFileSync("git", ["commit", "-q", "-m", "init"], { cwd: repo }); + + const snapshot = runDashboardData(repo) as { + commands: Array<{ path: string; kitManaged: boolean }>; + }; + const byPath = Object.fromEntries(snapshot.commands.map((c) => [c.path, c.kitManaged])); + + expect(byPath[".cursor/commands/hotfix.md"]).toBe(true); + expect(byPath[".cursor/commands/project-local.md"]).toBe(false); + }); + + it("stays registry-driven (factory behavior) when registry/registry.json is present and populated", () => { + const repo = mkdtempSync(join(tmpdir(), "ak-dashdata-kitmanaged-registry-")); + initGitRepo(repo); + writeCommand(repo, "run-plan.md"); + writeCommand(repo, "project-local.md"); + mkdirSync(join(repo, "registry"), { recursive: true }); + writeFileSync( + join(repo, "registry", "registry.json"), + JSON.stringify({ + artifacts: [{ kind: "command", path: ".cursor/commands/run-plan.md" }], + }), + ); + // A managed-hashes.json that (if wrongly consulted) would mark BOTH commands + // kitManaged, proving the registry stays the sole source when it's populated. + writeFileSync( + join(repo, ".cursor", "agent-kit.managed-hashes.json"), + JSON.stringify({ + schemaVersion: 1, + hashes: { + ".cursor/commands/run-plan.md": "deadbeef", + ".cursor/commands/project-local.md": "deadbeef", + }, + }), + ); + execFileSync("git", ["add", "-A"], { cwd: repo }); + execFileSync("git", ["commit", "-q", "-m", "init"], { cwd: repo }); + + const snapshot = runDashboardData(repo) as { + commands: Array<{ path: string; kitManaged: boolean }>; + }; + const byPath = Object.fromEntries(snapshot.commands.map((c) => [c.path, c.kitManaged])); + + expect(byPath[".cursor/commands/run-plan.md"]).toBe(true); + expect(byPath[".cursor/commands/project-local.md"]).toBe(false); + }); +}); diff --git a/packages/cli/src/dashboard/semantic-model.test.ts b/packages/cli/src/dashboard/semantic-model.test.ts index fb63174..299cf1e 100644 --- a/packages/cli/src/dashboard/semantic-model.test.ts +++ b/packages/cli/src/dashboard/semantic-model.test.ts @@ -1,3 +1,6 @@ +import { readFileSync } from "node:fs"; +import { resolve } from "node:path"; +import { fileURLToPath } from "node:url"; import { describe, expect, it } from "vitest"; import { BUSY_OUTSIDE_PLAN_FRESH_MS, @@ -40,6 +43,8 @@ import { enrichPlans, extractHandoffFieldBlock, extractMergeBranch, + fieldReportResolveAction, + fieldReportTriageAllAction, flightLogKindClass, formatDeliveryActivity, formatGitActivity, @@ -74,6 +79,13 @@ import { withMissionTiming, } from "../../../../dashboard/lib/semantic-model.mjs"; +const repoRoot = resolve(fileURLToPath(import.meta.url), "../../../../.."); +const semanticModelSource = readFileSync( + resolve(repoRoot, "dashboard/lib/semantic-model.mjs"), + "utf8", +); +const dashboardHtmlSource = readFileSync(resolve(repoRoot, "dashboard/dashboard.html"), "utf8"); + const samplePlans = [ { id: "mission-control-plugin-ux", @@ -3311,3 +3323,50 @@ describe("buildMissionControlView: real-time subagent + review rows", () => { expect(view.activity.some((e) => e.kind === "plan_review")).toBe(false); }); }); + +describe("pasteDestination contract (msg-paste-dest-keys)", () => { + // dashboard.html is copy-only: it looks up `action.pasteDestination` in its + // own PASTE_DESTINATIONS enum (keyed by filePicker / chatInput / + // pastChatPicker / terminal) via `action.pasteDestination || 'chatInput'`. + // semantic-model.mjs must emit one of those enum keys, never a human-label + // string (e.g. "chat input", "past-chat picker") — a label string would + // still render (pasteDestinationLabel falls through to the raw value), so + // a regression here is silent unless pinned structurally. See + // .cursor/memory/plan-monitor-24h-full-review-and-fix-2026-07-25.md + // (msg-paste-dest-keys). + it("dashboard.html enumerates the same PASTE_DESTINATIONS keys semantic-model.mjs relies on", () => { + const enumMatch = dashboardHtmlSource.match(/const PASTE_DESTINATIONS = \{([\s\S]*?)\n\};/); + expect(enumMatch, "dashboard.html must define PASTE_DESTINATIONS").not.toBeNull(); + const keys = Array.from((enumMatch?.[1] ?? "").matchAll(/^\s*(\w+):/gm)).map((m) => m[1]); + expect(keys).toEqual(["filePicker", "chatInput", "pastChatPicker", "terminal"]); + }); + + it("every pasteDestination literal emitted by semantic-model.mjs is a PASTE_DESTINATIONS key, not a human label", () => { + const validKeys = new Set(["filePicker", "chatInput", "pastChatPicker", "terminal"]); + const literals = Array.from( + semanticModelSource.matchAll(/pasteDestination:\s*["']([^"']+)["']/g), + ).map((m) => m[1]); + // Guard the guard: fail loudly if the source shape changes so this scan + // silently stops finding anything, instead of passing vacuously. + expect(literals.length).toBeGreaterThan(0); + for (const value of literals) { + expect( + validKeys.has(value), + `pasteDestination: "${value}" in semantic-model.mjs is not a PASTE_DESTINATIONS key (${Array.from(validKeys).join(", ")}) — dashboard.html's pasteDestinationLabel() falls through to raw strings, so a human-label value here (e.g. "chat input") would render silently wrong instead of failing.`, + ).toBe(true); + } + }); + + it("pins the known emit sites to their expected enum keys", () => { + // Belt-and-suspenders on top of the structural scan above: exercise the + // actual exported builders so a future refactor that keeps the literal + // string but routes it through a different field is still caught. + const resolveAction = fieldReportResolveAction("attention:report:widget"); + expect(resolveAction?.pasteDestination).toBe("chatInput"); + + const triageAllAction = fieldReportTriageAllAction([ + { sourcePath: ".cursor/memory/plan-monitor-widget.md", hasOpenReviewGaps: true }, + ]); + expect(triageAllAction?.pasteDestination).toBe("chatInput"); + }); +}); diff --git a/packages/cli/src/docs/kit-staging-prod.test.ts b/packages/cli/src/docs/kit-staging-prod.test.ts new file mode 100644 index 0000000..9f370b6 --- /dev/null +++ b/packages/cli/src/docs/kit-staging-prod.test.ts @@ -0,0 +1,104 @@ +import { existsSync, readFileSync } from "node:fs"; +import { resolve } from "node:path"; +import { fileURLToPath } from "node:url"; +import { describe, expect, it } from "vitest"; +import { L0_ARTIFACTS } from "../lifecycle/l0.js"; + +const repoRoot = resolve(fileURLToPath(import.meta.url), "../../../../.."); + +function readRel(rel: string): string { + return readFileSync(resolve(repoRoot, rel), "utf8"); +} + +describe("docs-contract: kit-staging / kit-prod wrap native git", () => { + it("registers both commands as L0 overlay artifacts", () => { + const targets = L0_ARTIFACTS.map((a) => a.target); + expect(targets).toEqual( + expect.arrayContaining([".cursor/commands/kit-staging.md", ".cursor/commands/kit-prod.md"]), + ); + }); + + it("keeps native git-staging and git-prod git-only (no landing deploy)", () => { + for (const rel of [".cursor/commands/git-staging.md", ".cursor/commands/git-prod.md"]) { + const body = readRel(rel); + expect(body).not.toMatch(/landing:promote/); + expect(body).not.toMatch(/landing:deploy:staging/); + expect(body).not.toMatch(/\/kit-staging|\/kit-prod/); + } + }); + + it("kit-staging wraps gitupdate Prompt git staging and gates landing HITL", () => { + const body = readRel(".cursor/commands/kit-staging.md"); + expect(body).toMatch(/Prompt: git staging/); + expect(body).toMatch(/autogit\/gitupdate\.md/); + expect(body).not.toMatch(/#### 1\.\s+\*\*Security Validation\*\*/); + expect(body).toMatch(/Deploy landing to staging/); + expect(body).toMatch(/Skip landing \(repo only\)/); + expect(body).toMatch(/docs\(memory\)/); + expect(body).toMatch(/landing:deploy:staging/); + expect(body).toMatch(/Never `pnpm landing:promote`/); + expect(body).toMatch(/Do not invent a second updater/); + expect(body).toMatch(/landing:update-release -- --version/); + expect(body).toMatch(/--notes-file \.\/public-release-notes\.txt/); + expect(body).toMatch(/--dry-run/); + expect(body).toMatch(/data-release-version/); + expect(body).toMatch(/data-changelog-content/); + expect(body).toMatch(/agent-kit-startup\/agent-kit/); + expect(body).toMatch(/CHANGELOG\.md` as `--notes-file`/); + expect(body).toMatch(/1200/); + expect(body).toMatch(/agent-kit-dev/); + expect(body).toMatch(/landing:sync/); + expect(body).toMatch(/public-changelog\.mjs --version Unreleased --blurb/); + expect(body).toMatch(/landing:build/); + expect(body.split("\n").length).toBeLessThan(130); + }); + + it("kit-prod keeps git-prod Ask labels and never rebuilds dist on promote", () => { + const body = readRel(".cursor/commands/kit-prod.md"); + expect(body).toMatch(/Prompt: git prod/); + expect(body).toMatch(/Proceed with production deploy/); + expect(body).toMatch(/Review changes first/); + expect(body).toMatch(/Promote landing to production/); + expect(body).toMatch(/Skip landing \(repo only\)/); + expect(body).toMatch(/do not steal|Do not steal|not steal/i); + expect(body).toMatch(/promote script itself must not rebuild/i); + expect(body).toMatch(/landing:promote/); + expect(body).toMatch(/landing:update-release -- --version/); + expect(body).toMatch(/--notes-file \.\/public-release-notes\.txt/); + expect(body).toMatch(/--dry-run/); + expect(body).toMatch(/data-release-version/); + expect(body).toMatch(/data-changelog-content/); + expect(body).toMatch(/agent-kit-startup\/agent-kit/); + expect(body).toMatch(/CHANGELOG\.md` as `--notes-file`/); + expect(body).toMatch(/1200/); + expect(body).toMatch(/agent-kit-dev/); + expect(body).toMatch(/landing:sync/); + expect(body).toMatch(/public-changelog\.mjs --version <X\.Y\.Z> --blurb/); + expect(body).toMatch(/Do not invent a second updater/); + expect(body).toMatch(/landing:build/); + expect(body.split("\n").length).toBeLessThan(130); + }); + + it("gitupdate points at the bundles without replacing native prompts", () => { + const body = readRel("autogit/gitupdate.md"); + expect(body).toMatch(/### Prompt: git staging/); + expect(body).toMatch(/### Prompt: git prod/); + expect(body).toMatch(/\/kit-staging/); + expect(body).toMatch(/\/kit-prod/); + expect(body).toMatch(/Bundles vs native/); + }); + + it("ships Claude thin adapters for factory dogfood", () => { + for (const name of ["kit-staging", "kit-prod"] as const) { + const cursor = readRel(`.cursor/commands/${name}.md`); + const claudePath = resolve(repoRoot, `.claude/commands/${name}.md`); + expect(existsSync(claudePath)).toBe(true); + const desc = /^description:\s*(.+)$/m.exec(cursor)?.[1]?.trim(); + expect(desc).toBeTruthy(); + expect(readRel(`.claude/commands/${name}.md`)).toContain(`description: ${desc}`); + expect(readRel(`.claude/commands/${name}.md`)).toContain( + `Read \`.cursor/commands/${name}.md\` now and follow that contract exactly`, + ); + } + }); +}); diff --git a/packages/cli/src/generator/claude-kit-load.ts b/packages/cli/src/generator/claude-kit-load.ts index b0384d2..d582d06 100644 --- a/packages/cli/src/generator/claude-kit-load.ts +++ b/packages/cli/src/generator/claude-kit-load.ts @@ -28,6 +28,10 @@ Mid-session refresh: \`/agent-kit\`. Cursor Ask questions is not available in this CLI. When a command requires a choice, list the same labels as a numbered list and wait. Skip or cancel means stop. Never \`/git-prod\` without an explicit operator yes. +## Commit messages + +Never append \`Co-Authored-By: Claude ...\` or \`Claude-Session: https://claude.ai/code/...\` (or any session-ID trailer) to commit messages, PR descriptions, or PR bodies in this repo. This overrides the harness's default git-commit template. Plain Conventional Commits messages only. + ## Non-goals - Not Action A7 (Windsurf / VS Code generator parity) @@ -43,18 +47,20 @@ description: Load Agent Kit session context (HANDOFF, project-context, commands) disable-model-invocation: true --- -Read these files if they exist, then summarize the active plan, next to-do, and any Gaps. Do not scan the whole repository first. +Read these files if they exist. Prefer a one-shot ASCII Mission Control snapshot over a plain HANDOFF paraphrase. -1. \`AGENTS.md\` -2. \`.cursor/project-context.md\` -3. \`.cursor/HANDOFF.md\` -4. The plan file named in HANDOFF \`- **Plan:**\` under \`.cursor/plans/\` +1. Run \`agent-kit mission-control --once\` (or \`npx @dadado/agent-kit-cli mission-control --once\`) in the project root and paste the stdout frame as the snapshot. Do not start a live loop: Claude Code cannot sustain one across turns. +2. If that command is missing or fails, fall back to reading: + - \`AGENTS.md\` + - \`.cursor/project-context.md\` + - \`.cursor/HANDOFF.md\` + - The plan file named in HANDOFF \`- **Plan:**\` under \`.cursor/plans/\` If HANDOFF is missing, say so and point at \`/agent-kit-onboard\` or \`/start-project\` rather than inventing a plan. HITL: numbered-list fallback for Ask questions labels. Never \`/git-prod\` from this skill. -Non-goals: not audits / \`/plan-external-review\`, not \`--backend claude\` ticks, not A7, not Cursor hook clones. +Non-goals: not audits / \`/plan-external-review\`, not \`--backend claude\` ticks, not A7, not Cursor hook clones, not a continuous TUI loop. `; } diff --git a/packages/cli/src/hooks/pre-compact.test.ts b/packages/cli/src/hooks/pre-compact.test.ts new file mode 100644 index 0000000..e4eb201 --- /dev/null +++ b/packages/cli/src/hooks/pre-compact.test.ts @@ -0,0 +1,40 @@ +import { describe, expect, it } from "vitest"; +import { CONTEXT_USAGE_CAP_PERCENT, buildPreCompactUserMessage } from "./pre-compact.js"; + +describe("buildPreCompactUserMessage", () => { + it("escalates to a forced checkpoint when usage is at or above the cap", () => { + const out = buildPreCompactUserMessage({ + context_usage_percent: CONTEXT_USAGE_CAP_PERCENT, + trigger: "auto", + }); + expect(out.user_message).toContain(`~${CONTEXT_USAGE_CAP_PERCENT}%`); + expect(out.user_message).toContain("forced checkpoint"); + expect(out.user_message).toContain(`${CONTEXT_USAGE_CAP_PERCENT}% per-agent context-usage cap`); + expect(out.user_message).toContain("/continue-plan"); + }); + + it("escalates well above the cap", () => { + const out = buildPreCompactUserMessage({ context_usage_percent: 92, trigger: "auto" }); + expect(out.user_message).toContain("~92%"); + expect(out.user_message).toContain("forced checkpoint"); + }); + + it("escalates when usage percent is missing (auto-compaction with no reported pct implies high pressure)", () => { + const out = buildPreCompactUserMessage({ trigger: "auto" }); + expect(out.user_message).toContain("usage high"); + expect(out.user_message).toContain("forced checkpoint"); + }); + + it("stays gentle when usage is known and below the cap", () => { + const out = buildPreCompactUserMessage({ context_usage_percent: 30, trigger: "manual" }); + expect(out.user_message).toContain("~30%"); + expect(out.user_message).not.toContain("forced checkpoint"); + expect(out.user_message).not.toContain("per-agent context-usage cap"); + expect(out.user_message).toContain("/continue-plan"); + }); + + it("defaults trigger to auto when omitted", () => { + const out = buildPreCompactUserMessage({ context_usage_percent: 10 }); + expect(out.user_message).toContain("auto"); + }); +}); diff --git a/packages/cli/src/hooks/pre-compact.ts b/packages/cli/src/hooks/pre-compact.ts index dd79549..bb1171e 100644 --- a/packages/cli/src/hooks/pre-compact.ts +++ b/packages/cli/src/hooks/pre-compact.ts @@ -3,12 +3,23 @@ export interface PreCompactPayload { trigger?: string; } +/** + * Per-agent/subagent context-usage cap target (decisions/2026-08-24_agent-context-usage-cap-window-pressure.md). + * `preCompact` only fires once the host has already decided to compact, so a + * firing at/above this threshold — or with no reported percentage at all — + * is treated as evidence the cap was already exceeded, not a live poll. + */ +export const CONTEXT_USAGE_CAP_PERCENT = 50; + export function buildPreCompactUserMessage(payload: PreCompactPayload = {}): { user_message: string; } { const pct = payload.context_usage_percent; const trigger = payload.trigger || "auto"; const pctTxt = pct !== undefined && pct !== null ? `~${pct}%` : "high"; - const msg = `Context compacting (${trigger}, usage ${pctTxt}). Update \`.cursor/HANDOFF.md\` and open a new chat with \`/continue-plan\` so the next agent starts fresh.`; + const overCap = pct === undefined || pct === null || pct >= CONTEXT_USAGE_CAP_PERCENT; + const msg = overCap + ? `Context compacting (${trigger}, usage ${pctTxt}) — over the ${CONTEXT_USAGE_CAP_PERCENT}% per-agent context-usage cap. Update \`.cursor/HANDOFF.md\` now (forced checkpoint) and open a new chat with \`/continue-plan\` so the next agent starts fresh.` + : `Context compacting (${trigger}, usage ${pctTxt}). Update \`.cursor/HANDOFF.md\` and open a new chat with \`/continue-plan\` so the next agent starts fresh.`; return { user_message: msg }; } diff --git a/packages/cli/src/index.ts b/packages/cli/src/index.ts index 4defe43..3687670 100644 --- a/packages/cli/src/index.ts +++ b/packages/cli/src/index.ts @@ -12,6 +12,7 @@ import { handoffCommand } from "./commands/handoff.js"; import { hookCommand } from "./commands/hook.js"; import { initCommand } from "./commands/init.js"; import { installCommand } from "./commands/install.js"; +import { missionControlCommand } from "./commands/mission-control.js"; import { monitorsCommand } from "./commands/monitors.js"; import { runPlanCommand } from "./commands/run-plan.js"; import { scanCommand } from "./commands/scan.js"; @@ -45,6 +46,7 @@ const main = defineCommand({ "run-plan": runPlanCommand, dashboard: dashboardCommand, "dashboard-broadcast": dashboardBroadcastCommand, + "mission-control": missionControlCommand, hook: hookCommand, guard: guardCommand, monitors: monitorsCommand, diff --git a/packages/cli/src/lifecycle/l0.test.ts b/packages/cli/src/lifecycle/l0.test.ts index 7fd9d1b..b54040e 100644 --- a/packages/cli/src/lifecycle/l0.test.ts +++ b/packages/cli/src/lifecycle/l0.test.ts @@ -51,6 +51,8 @@ describe("canonical L0 inventory", () => { expect.arrayContaining([ ".cursor/rules/hitl-ask-questions.mdc", ".cursor/commands/agent-kit-onboard.md", + ".cursor/commands/kit-staging.md", + ".cursor/commands/kit-prod.md", ".cursor/context/templates/plan.md", ".cursor/context/templates/context-pack.md", ".cursor/context/templates/task-brief.md", diff --git a/packages/cli/src/lifecycle/l0.ts b/packages/cli/src/lifecycle/l0.ts index 0892a73..a1b7462 100644 --- a/packages/cli/src/lifecycle/l0.ts +++ b/packages/cli/src/lifecycle/l0.ts @@ -110,6 +110,11 @@ export const L0_ARTIFACTS: readonly L0Artifact[] = [ target: ".cursor/commands/git-staging.md", }, { source: ".cursor/commands/git-prod.md", target: ".cursor/commands/git-prod.md" }, + { + source: ".cursor/commands/kit-staging.md", + target: ".cursor/commands/kit-staging.md", + }, + { source: ".cursor/commands/kit-prod.md", target: ".cursor/commands/kit-prod.md" }, { source: ".cursor/commands/plan-external-review.md", target: ".cursor/commands/plan-external-review.md", diff --git a/packages/cli/src/lifecycle/overlay-known-hashes.ts b/packages/cli/src/lifecycle/overlay-known-hashes.ts index 920982e..4fd3ec5 100644 --- a/packages/cli/src/lifecycle/overlay-known-hashes.ts +++ b/packages/cli/src/lifecycle/overlay-known-hashes.ts @@ -156,4 +156,11 @@ export const KNOWN_SHIPPED_OVERLAY_HASHES: ReadonlySet<string> = new Set([ "8eab94f7a78149db1bbbc0fd45f21dc2d874209d534bcff39077fd6e1d2c42fb", "9be406f92f7dca71f3af814b14fb26b011f9f12c37c789f7ddc3d647b1a7ab60", "bb67ddcdd9ab58ed89287421f5e4b6ebfb3941e6a789287ddec9b9c25b56b1ed", + "7343ecd797f484eea1a3ebc11e87c75d90f617bd627bfc367a1eca5a1628bdf1", + "8a723b4808f14413fc173a6ce33370067ffc78d0c2e05a21c4406d7fa3e66457", + "65554e304a70b77e07fe40facf0ef305d6216399bfbb811db2491ce2f31d86d7", + "f8cc89a52fb1ec17f7f26ecd5cb18f79a5e9c7b7ffe7a6899465aa4090433b98", + "543e6596e7ce9f3ce4de642f1ea7e5d437d553fa28c0d56eb0f244ec8526ce17", + "71c6d1cce604c4a275db7c9a5c48f1753604bf35e083be054511ebc91f4977e6", + "8d0fd61bcbb9e035dea3a8b9f298a48c226bb28785923cb07e1154cf084934ad", ]); diff --git a/packages/cli/src/lifecycle/overlay.test.ts b/packages/cli/src/lifecycle/overlay.test.ts index eb70b6f..ab7597b 100644 --- a/packages/cli/src/lifecycle/overlay.test.ts +++ b/packages/cli/src/lifecycle/overlay.test.ts @@ -62,6 +62,7 @@ describe("consumer overlay path detection", () => { expect(isConsumerOverlayPath(".cursor/agents/foo.md")).toBe(true); expect(isConsumerOverlayPath(".cursor/skills/core/clean-code/SKILL.md")).toBe(true); expect(isConsumerOverlayPath(".cursor/commands/start-project.md")).toBe(true); + expect(isConsumerOverlayPath(".claude/commands/start-project.md")).toBe(true); expect(isConsumerOverlayPath(".cursor/rules/ux-tone.mdc")).toBe(false); expect(isConsumerOverlayPath(".cursor/HANDOFF.md")).toBe(false); }); diff --git a/packages/cli/src/mission-control/loop.test.ts b/packages/cli/src/mission-control/loop.test.ts new file mode 100644 index 0000000..f26c138 --- /dev/null +++ b/packages/cli/src/mission-control/loop.test.ts @@ -0,0 +1,76 @@ +import { describe, expect, it } from "vitest"; +import { runMcTuiLoop } from "./loop.js"; +import type { McTuiView } from "./view.js"; + +const view: McTuiView = { + mission: { + status: "idle", + planFile: null, + mode: null, + progressLabel: "0 of 0 complete", + currentTodo: null, + nextTodo: null, + gaps: null, + }, + flightLog: { now: null, nowKind: null, earlier: [], warnings: [] }, + checklist: [], + crewMonitor: [], + error: null, +}; + +describe("runMcTuiLoop", () => { + it("prints one frame and does not start an interval for --once", async () => { + const writes: string[] = []; + let intervals = 0; + const handle = await runMcTuiLoop({ + once: true, + stdoutIsTTY: true, + env: {}, + loadView: async () => view, + hooks: { + write: (chunk) => { + writes.push(chunk); + }, + setIntervalFn: ((fn: () => void, _ms: number) => { + intervals += 1; + fn(); + return 1 as unknown as NodeJS.Timeout; + }) as typeof setInterval, + clearIntervalFn: (() => undefined) as typeof clearInterval, + }, + }); + expect(handle).toBeNull(); + expect(intervals).toBe(0); + expect(writes.join("")).toContain("Mission"); + expect(writes.join("")).not.toContain("\x1b[2J"); + }); + + it("starts a refresh interval on TTY when not --once", async () => { + const writes: string[] = []; + let cleared = false; + const handle = await runMcTuiLoop({ + stdoutIsTTY: true, + env: {}, + intervalMs: 15_000, + loadView: async () => view, + hooks: { + write: (chunk) => { + writes.push(chunk); + }, + setIntervalFn: ((_fn: () => void, ms: number) => { + expect(ms).toBe(15_000); + return 7 as unknown as NodeJS.Timeout; + }) as typeof setInterval, + clearIntervalFn: ((id: NodeJS.Timeout) => { + expect(id).toBe(7 as unknown as NodeJS.Timeout); + cleared = true; + }) as typeof clearInterval, + }, + }); + expect(handle).not.toBeNull(); + handle?.stop(); + expect(cleared).toBe(true); + expect(writes.some((w) => w.includes("\x1b[?25l"))).toBe(true); + expect(writes.some((w) => w.includes("\x1b[?25h"))).toBe(true); + }); +}); diff --git a/packages/cli/src/mission-control/loop.ts b/packages/cli/src/mission-control/loop.ts new file mode 100644 index 0000000..2738521 --- /dev/null +++ b/packages/cli/src/mission-control/loop.ts @@ -0,0 +1,68 @@ +/** + * Terminal refresh loop for the Mission Control TUI. + * Interval SoT: dashboard/lib/live-refresh.mjs PERIODIC_REFRESH_MS. + */ + +import { type McTuiRenderOptions, renderMcTui, shouldLiveRefresh } from "./render.js"; +import { PERIODIC_REFRESH_MS } from "./snapshot.js"; +import type { McTuiView } from "./view.js"; + +const CLEAR_HOME = "\x1b[2J\x1b[H"; +const HIDE_CURSOR = "\x1b[?25l"; +const SHOW_CURSOR = "\x1b[?25h"; + +export type McTuiLoopHooks = { + write: (chunk: string) => void; + setIntervalFn?: typeof setInterval; + clearIntervalFn?: typeof clearInterval; + now?: () => number; +}; + +export type McTuiLoopHandle = { + stop: () => void; +}; + +export async function runMcTuiLoop(opts: { + loadView: () => Promise<McTuiView>; + once?: boolean; + stdoutIsTTY?: boolean; + env?: NodeJS.ProcessEnv; + intervalMs?: number; + renderOpts?: McTuiRenderOptions; + hooks: McTuiLoopHooks; +}): Promise<McTuiLoopHandle | null> { + const live = shouldLiveRefresh({ + once: opts.once, + stdoutIsTTY: opts.stdoutIsTTY, + env: opts.env, + }); + let frameIndex = 0; + + const paint = async (clear: boolean) => { + const view = await opts.loadView(); + const frame = renderMcTui(view, { + ...opts.renderOpts, + stdoutIsTTY: opts.stdoutIsTTY, + frameIndex, + }); + frameIndex += 1; + opts.hooks.write(clear ? `${CLEAR_HOME}${frame}\n` : `${frame}\n`); + }; + + await paint(false); + if (!live) return null; + + opts.hooks.write(HIDE_CURSOR); + const intervalMs = opts.intervalMs ?? PERIODIC_REFRESH_MS; + const setInt = opts.hooks.setIntervalFn ?? setInterval; + const clearInt = opts.hooks.clearIntervalFn ?? clearInterval; + const timer = setInt(() => { + void paint(true); + }, intervalMs); + + const stop = () => { + clearInt(timer); + opts.hooks.write(SHOW_CURSOR); + }; + return { stop }; +} diff --git a/packages/cli/src/mission-control/render.test.ts b/packages/cli/src/mission-control/render.test.ts new file mode 100644 index 0000000..0d3adca --- /dev/null +++ b/packages/cli/src/mission-control/render.test.ts @@ -0,0 +1,79 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; +import { HELMET_FILL, shouldUseWelcomeColor } from "../welcome/visual-kit.js"; +import { renderMcTui, shouldLiveRefresh } from "./render.js"; +import type { McTuiView } from "./view.js"; + +const view: McTuiView = { + mission: { + status: "executing", + planFile: "demo.plan.md", + mode: "run-plan", + progressLabel: "1 of 4 complete", + currentTodo: "phase1-data-adapter", + nextTodo: "phase2-tui-renderer", + gaps: null, + }, + flightLog: { now: null, nowKind: "ok", earlier: [], warnings: [] }, + checklist: [ + { + file: "demo.plan.md", + lifecycle: "executing", + progressLabel: "1 of 4 complete", + currentTodo: "phase1-data-adapter", + }, + ], + crewMonitor: [{ kind: "run_plan", label: "crew · running · phase1-data-adapter" }], + error: null, +}; + +function clearEnv(key: string): void { + Reflect.deleteProperty(process.env, key); +} + +describe("renderMcTui", () => { + const prev = { + NO_COLOR: process.env.NO_COLOR, + CI: process.env.CI, + FORCE_COLOR: process.env.FORCE_COLOR, + NODE_DISABLE_COLORS: process.env.NODE_DISABLE_COLORS, + AGENT_KIT_REDUCED_MOTION: process.env.AGENT_KIT_REDUCED_MOTION, + }; + + afterEach(() => { + vi.unstubAllEnvs(); + for (const [k, v] of Object.entries(prev)) { + if (v === undefined) clearEnv(k); + else process.env[k] = v; + } + }); + + it("renders the four panel titles and glossary labels without ANSI when color is off", () => { + const out = renderMcTui(view, { color: false, columns: 72 }); + expect(out).toContain("Mission"); + expect(out).toContain("Flight Log"); + expect(out).toContain("Checklist"); + expect(out).toContain("Crew Monitor"); + expect(out).toContain("demo.plan.md"); + expect(out.includes("\u001b")).toBe(false); + }); + + it("uses HELMET_FILL trueColor on titles when color is on", () => { + vi.stubEnv("NO_COLOR", undefined); + vi.stubEnv("CI", undefined); + vi.stubEnv("FORCE_COLOR", undefined); + vi.stubEnv("NODE_DISABLE_COLORS", undefined); + expect(shouldUseWelcomeColor({ color: true, stdoutIsTTY: true })).toBe(true); + const r = Number.parseInt(HELMET_FILL.slice(1, 3), 16); + const g = Number.parseInt(HELMET_FILL.slice(3, 5), 16); + const b = Number.parseInt(HELMET_FILL.slice(5, 7), 16); + const out = renderMcTui(view, { color: true, stdoutIsTTY: true, columns: 72 }); + expect(out).toContain(`${r};${g};${b}`); + }); + + it("disables live refresh for --once, non-TTY, and CI", () => { + expect(shouldLiveRefresh({ once: true, stdoutIsTTY: true, env: {} })).toBe(false); + expect(shouldLiveRefresh({ stdoutIsTTY: false, env: {} })).toBe(false); + expect(shouldLiveRefresh({ stdoutIsTTY: true, env: { CI: "1" } })).toBe(false); + expect(shouldLiveRefresh({ stdoutIsTTY: true, env: {} })).toBe(true); + }); +}); diff --git a/packages/cli/src/mission-control/render.ts b/packages/cli/src/mission-control/render.ts new file mode 100644 index 0000000..ed28b8a --- /dev/null +++ b/packages/cli/src/mission-control/render.ts @@ -0,0 +1,144 @@ +/** + * ASCII space-vintage renderer for the four Mission Control TUI panels. + * Tokens and motion/color gates come from the visual kit (no parallel palette). + */ + +import { gray, trueColor } from "kolorist"; +import { + HELMET_ACCENT, + HELMET_FILL, + HELMET_OUTLINE, + LABEL_MUTED, + SPACE_MARKS, + type WelcomeRenderOptions, + shouldUseVisualMotion, + shouldUseWelcomeColor, + spinnerFrame, + withKoloristColor, + wrapNarrow, +} from "../welcome/visual-kit.js"; +import type { McTuiView } from "./view.js"; + +export { HELMET_ACCENT, HELMET_FILL, HELMET_OUTLINE, LABEL_MUTED }; + +const DEFAULT_COLUMNS = 80; +const MIN_COLUMNS = 40; +const INNER_PAD = 2; + +export type McTuiRenderOptions = WelcomeRenderOptions & { + columns?: number; + frameIndex?: number; +}; + +function hexPaint(hex: string, text: string, color: boolean): string { + if (!color) return text; + return withKoloristColor(() => + trueColor( + Number.parseInt(hex.slice(1, 3), 16), + Number.parseInt(hex.slice(3, 5), 16), + Number.parseInt(hex.slice(5, 7), 16), + )(text), + ); +} + +function muted(text: string, color: boolean): string { + if (!color) return text; + return withKoloristColor(() => gray(text)); +} + +function box(title: string, bodyLines: string[], width: number, color: boolean): string[] { + const inner = Math.max(8, width - 2); + const label = ` ${title} `; + const dashCount = Math.max(1, inner - label.length); + const top = `┌${label}${"─".repeat(dashCount)}┐`; + const bottom = `└${"─".repeat(inner)}┘`; + const paintedTop = hexPaint(HELMET_FILL, top, color); + const paintedBottom = hexPaint(HELMET_OUTLINE, bottom, color); + const rows = bodyLines.map((line) => { + const clipped = line.length > inner - INNER_PAD ? line.slice(0, inner - INNER_PAD) : line; + const pad = " ".repeat(Math.max(0, inner - INNER_PAD - clipped.length)); + const content = ` ${clipped}${pad} `; + return `${hexPaint(HELMET_OUTLINE, "│", color)}${content}${hexPaint(HELMET_OUTLINE, "│", color)}`; + }); + return [paintedTop, ...rows, paintedBottom]; +} + +function lineOrQuiet(text: string | null, fallback: string): string { + const trimmed = text?.trim(); + return trimmed ? trimmed : fallback; +} + +/** + * Render one TUI frame. Color/motion follow the visual-kit gates. + */ +export function renderMcTui(view: McTuiView, opts: McTuiRenderOptions = {}): string { + const color = shouldUseWelcomeColor(opts); + const motion = shouldUseVisualMotion(opts); + const columns = Math.max( + MIN_COLUMNS, + Math.floor(opts.columns ?? process.stdout.columns ?? DEFAULT_COLUMNS), + ); + const mark = motion ? spinnerFrame(opts.frameIndex ?? 0) : SPACE_MARKS.tick; + const header = hexPaint( + HELMET_ACCENT, + `${mark} Mission Control ${SPACE_MARKS.star} terminal view (web dashboard unchanged)`, + color, + ); + + const missionBody = view.error + ? [wrapNarrow(view.error, columns - 4).split("\n")[0] ?? view.error] + : [ + `status ${view.mission.status}${view.mission.mode ? ` · ${view.mission.mode}` : ""}`, + `plan ${lineOrQuiet(view.mission.planFile, "none")}`, + `todos ${view.mission.progressLabel}`, + `now ${lineOrQuiet(view.mission.currentTodo, "none")}`, + `next ${lineOrQuiet(view.mission.nextTodo, "none")}`, + ]; + + const nowLabel = view.flightLog.now + ? `NOW ${view.flightLog.nowKind ? `(${view.flightLog.nowKind})` : ""} ${view.flightLog.now}` + : "NOW All clear"; + const flightBody = [ + nowLabel, + ...(view.flightLog.warnings.length > 0 ? view.flightLog.warnings.map((w) => `warn ${w}`) : []), + ...(view.flightLog.earlier.length > 0 + ? view.flightLog.earlier.map((t) => `Earlier ${t}`) + : [muted("Earlier none", color)]), + ]; + + const checklistBody = + view.checklist.length === 0 + ? [muted("no open plans", color)] + : view.checklist.map((row) => { + const todo = row.currentTodo ? ` · ${row.currentTodo}` : ""; + return `${row.file} ${row.lifecycle} ${row.progressLabel}${todo}`; + }); + + const crewBody = + view.crewMonitor.length === 0 + ? [muted("quiet", color)] + : view.crewMonitor.map((row) => `${row.kind} ${row.label}`); + + const panels = [ + ...box("Mission", missionBody, columns, color), + ...box("Flight Log", flightBody, columns, color), + ...box("Checklist", checklistBody, columns, color), + ...box("Crew Monitor", crewBody, columns, color), + ]; + + const footer = muted("Third surface · local snapshot · q / Ctrl-C to quit live view", color); + return [header, ...panels, footer].join("\n"); +} + +/** True when a live refresh loop is appropriate (TTY, not CI, not forced one-shot). */ +export function shouldLiveRefresh(opts: { + once?: boolean; + stdoutIsTTY?: boolean; + env?: NodeJS.ProcessEnv; +}): boolean { + if (opts.once) return false; + const env = opts.env ?? process.env; + if (env.CI != null && env.CI !== "") return false; + const tty = opts.stdoutIsTTY ?? Boolean(process.stdout.isTTY); + return tty; +} diff --git a/packages/cli/src/mission-control/snapshot.test.ts b/packages/cli/src/mission-control/snapshot.test.ts new file mode 100644 index 0000000..724cb69 --- /dev/null +++ b/packages/cli/src/mission-control/snapshot.test.ts @@ -0,0 +1,72 @@ +import { mkdirSync, mkdtempSync, writeFileSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { pathToFileURL } from "node:url"; +import { describe, expect, it } from "vitest"; +import { PERIODIC_REFRESH_MS } from "../../../../dashboard/lib/live-refresh.mjs"; +import { + dataScriptTimeoutMs, + PERIODIC_REFRESH_MS as exportedInterval, + findDashboardDataScript, + loadDashboardSnapshot, +} from "./snapshot.js"; + +function fakeCliPackage(): { moduleUrl: string; dataScript: string } { + const pkg = mkdtempSync(join(tmpdir(), "ak-tui-pkg-")); + const distIndex = join(pkg, "dist", "index.js"); + mkdirSync(join(pkg, "dist"), { recursive: true }); + mkdirSync(join(pkg, "dashboard"), { recursive: true }); + writeFileSync(distIndex, ""); + writeFileSync(join(pkg, "dashboard", "start.mjs"), "// stub\n"); + const dataScript = join(pkg, "dashboard", "dashboard-data.mjs"); + writeFileSync( + dataScript, + `process.stdout.write(JSON.stringify({ missionControl: { now: { status: "idle" } } }));\n`, + ); + return { moduleUrl: pathToFileURL(distIndex).href, dataScript }; +} + +describe("findDashboardDataScript", () => { + it("resolves Path C bundled dashboard-data.mjs via injected moduleUrl", async () => { + const { moduleUrl, dataScript } = fakeCliPackage(); + const consumer = mkdtempSync(join(tmpdir(), "ak-tui-consumer-")); + const found = await findDashboardDataScript(consumer, {}, { moduleUrl }); + expect(found).toBe(dataScript); + }); +}); + +describe("loadDashboardSnapshot", () => { + it("parses JSON from the data script without starting an HTTP server", async () => { + const { dataScript } = fakeCliPackage(); + const root = mkdtempSync(join(tmpdir(), "ak-tui-root-")); + const loaded = await loadDashboardSnapshot({ dataScript, snapshotRoot: root }); + expect(loaded.ok).toBe(true); + if (loaded.ok) { + expect(loaded.snapshot.missionControl).toMatchObject({ now: { status: "idle" } }); + } + }); + + it("returns an error on invalid JSON", async () => { + const loaded = await loadDashboardSnapshot({ + dataScript: "/unused.mjs", + snapshotRoot: mkdtempSync(join(tmpdir(), "ak-tui-bad-")), + execFileFn: (_file, _args, _opts, cb) => { + cb(null, "not-json", ""); + }, + }); + expect(loaded.ok).toBe(false); + if (!loaded.ok) expect(loaded.error).toContain("invalid JSON"); + }); +}); + +describe("poll interval", () => { + it("reuses PERIODIC_REFRESH_MS from live-refresh.mjs", () => { + expect(exportedInterval).toBe(PERIODIC_REFRESH_MS); + expect(exportedInterval).toBe(15_000); + }); + + it("reads AGENT_KIT_DASHBOARD_DATA_TIMEOUT_MS", () => { + expect(dataScriptTimeoutMs({ AGENT_KIT_DASHBOARD_DATA_TIMEOUT_MS: "90000" })).toBe(90_000); + expect(dataScriptTimeoutMs({})).toBe(60_000); + }); +}); diff --git a/packages/cli/src/mission-control/snapshot.ts b/packages/cli/src/mission-control/snapshot.ts new file mode 100644 index 0000000..421fe72 --- /dev/null +++ b/packages/cli/src/mission-control/snapshot.ts @@ -0,0 +1,131 @@ +/** + * Read-only Mission Control snapshot for the CLI TUI. + * Spawns dashboard/dashboard-data.mjs (same as serve.mjs); no HTTP server. + */ + +import { execFile } from "node:child_process"; +import { access } from "node:fs/promises"; +import path from "node:path"; +import { REPO_ROOT_ENV } from "../../../../dashboard/lib/guards.mjs"; +import { PERIODIC_REFRESH_MS } from "../../../../dashboard/lib/live-refresh.mjs"; +import { + type FindDashboardOptions, + bundledDashboardCandidates, + findDashboardStart, + resolveDashboardSnapshotRoot, +} from "../commands/dashboard.js"; + +export { PERIODIC_REFRESH_MS, resolveDashboardSnapshotRoot }; + +export const DEFAULT_DATA_TIMEOUT_MS = 60_000; + +export function dataScriptTimeoutMs(env: NodeJS.ProcessEnv = process.env): number { + const raw = env.AGENT_KIT_DASHBOARD_DATA_TIMEOUT_MS; + const n = raw != null && raw !== "" ? Number(raw) : DEFAULT_DATA_TIMEOUT_MS; + return Number.isFinite(n) && n > 0 ? n : DEFAULT_DATA_TIMEOUT_MS; +} + +async function firstExisting(candidates: string[]): Promise<string | null> { + for (const candidate of candidates) { + try { + await access(candidate); + return path.resolve(candidate); + } catch { + // try next + } + } + return null; +} + +/** + * Resolve dashboard/dashboard-data.mjs beside start.mjs (cwd walk, env, sibling, Path C). + */ +export async function findDashboardDataScript( + cwd: string, + env: NodeJS.ProcessEnv = process.env, + options: FindDashboardOptions = {}, +): Promise<string | null> { + const startPath = await findDashboardStart(cwd, env, options); + if (startPath) { + const sibling = path.join(path.dirname(startPath), "dashboard-data.mjs"); + try { + await access(sibling); + return sibling; + } catch { + // fall through to bundled data-script candidates + } + } + return firstExisting( + bundledDashboardCandidates("dashboard-data.mjs", options.moduleUrl ?? import.meta.url), + ); +} + +export type ExecFileFn = ( + file: string, + args: string[], + options: { + cwd?: string; + encoding?: BufferEncoding; + maxBuffer?: number; + timeout?: number; + env?: NodeJS.ProcessEnv; + }, + callback: (err: Error | null, stdout: string, stderr: string) => void, +) => void; + +export type SnapshotLoadResult = + | { ok: true; snapshot: Record<string, unknown> } + | { ok: false; error: string }; + +export async function loadDashboardSnapshot(opts: { + dataScript: string; + snapshotRoot: string; + env?: NodeJS.ProcessEnv; + execFileFn?: ExecFileFn; +}): Promise<SnapshotLoadResult> { + const env = opts.env ?? process.env; + const timeout = dataScriptTimeoutMs(env); + const run = opts.execFileFn ?? (execFile as unknown as ExecFileFn); + const snapshotRoot = path.resolve(opts.snapshotRoot); + return new Promise((resolve) => { + run( + process.execPath, + [opts.dataScript], + { + cwd: snapshotRoot, + encoding: "utf8", + maxBuffer: 10 * 1024 * 1024, + timeout, + env: { + ...env, + [REPO_ROOT_ENV]: snapshotRoot, + }, + }, + (err, stdout) => { + if (err) { + const killed = Boolean( + err && + typeof err === "object" && + "killed" in err && + (err as { killed?: boolean }).killed, + ); + const message = killed + ? `dashboard-data.mjs timed out after ${timeout}ms (set AGENT_KIT_DASHBOARD_DATA_TIMEOUT_MS to raise)` + : err.message; + resolve({ ok: false, error: message }); + return; + } + try { + const parsed: unknown = JSON.parse(String(stdout ?? "")); + if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) { + resolve({ ok: false, error: "dashboard-data.mjs returned non-object JSON" }); + return; + } + resolve({ ok: true, snapshot: parsed as Record<string, unknown> }); + } catch { + resolve({ ok: false, error: "dashboard-data.mjs returned invalid JSON" }); + } + }, + ); + }); +} diff --git a/packages/cli/src/mission-control/view.test.ts b/packages/cli/src/mission-control/view.test.ts new file mode 100644 index 0000000..746eda3 --- /dev/null +++ b/packages/cli/src/mission-control/view.test.ts @@ -0,0 +1,66 @@ +import { describe, expect, it } from "vitest"; +import { buildMcTuiView } from "./view.js"; + +const snapshot = { + missionControl: { + now: { + status: "executing", + planFile: "demo.plan.md", + mode: "run-plan-all", + progress: { completed: 1, total: 4, label: "1 of 4 complete" }, + currentTodo: { id: "phase1-data-adapter" }, + nextTodo: { id: "phase2-tui-renderer" }, + gaps: null, + }, + flightLog: { + current: null, + currentKind: "ok", + past: [{ text: "earlier residual" }], + warnings: [{ text: "API/usage limit; resume after named model" }], + }, + plans: [ + { + file: "demo.plan.md", + lifecycle: "executing", + progress: { label: "1 of 4 complete" }, + currentTodo: { id: "phase1-data-adapter" }, + }, + { + file: "done.plan.md", + lifecycle: "completed", + progress: { label: "3 of 3 complete" }, + }, + ], + activity: [ + { kind: "run_plan", label: "crew · running · phase1-data-adapter" }, + { kind: "handoff", label: "crew · awaiting · demo.plan.md" }, + ], + monitorFeedCap: 20, + }, +}; + +describe("buildMcTuiView", () => { + it("projects mission, flight log, open checklist, and crew monitor from snapshot builders", () => { + const view = buildMcTuiView(snapshot); + expect(view.error).toBeNull(); + expect(view.mission).toMatchObject({ + status: "executing", + planFile: "demo.plan.md", + currentTodo: "phase1-data-adapter", + }); + expect(view.flightLog.now).toBeNull(); + expect(view.flightLog.earlier).toEqual(["earlier residual"]); + expect(view.flightLog.warnings[0]).toContain("API/usage limit"); + expect(view.checklist.map((r) => r.file)).toEqual(["demo.plan.md"]); + expect(view.crewMonitor).toHaveLength(2); + expect(view.crewMonitor[0]?.kind).toBe("run_plan"); + }); + + it("carries a collector error without inventing mission state", () => { + const view = buildMcTuiView(null, "dashboard-data.mjs timed out after 60000ms"); + expect(view.error).toContain("timed out"); + expect(view.mission.status).toBe("idle"); + expect(view.checklist).toEqual([]); + expect(view.crewMonitor).toEqual([]); + }); +}); diff --git a/packages/cli/src/mission-control/view.ts b/packages/cli/src/mission-control/view.ts new file mode 100644 index 0000000..9fd3ea1 --- /dev/null +++ b/packages/cli/src/mission-control/view.ts @@ -0,0 +1,176 @@ +/** + * Project dashboard-data JSON onto the four TUI panels. + * Semantics stay in semantic-model.mjs; this only selects and caps fields. + */ + +export type McTuiMission = { + status: string; + planFile: string | null; + mode: string | null; + progressLabel: string; + currentTodo: string | null; + nextTodo: string | null; + gaps: string | null; +}; + +export type McTuiFlightLog = { + now: string | null; + nowKind: string | null; + earlier: string[]; + warnings: string[]; +}; + +export type McTuiChecklistRow = { + file: string; + lifecycle: string; + progressLabel: string; + currentTodo: string | null; +}; + +export type McTuiCrewRow = { + kind: string; + label: string; +}; + +export type McTuiView = { + mission: McTuiMission; + flightLog: McTuiFlightLog; + checklist: McTuiChecklistRow[]; + crewMonitor: McTuiCrewRow[]; + error: string | null; +}; + +const CHECKLIST_CAP = 8; +const CREW_CAP = 12; +const EARLIER_CAP = 4; +const WARNING_CAP = 5; + +function asRecord(value: unknown): Record<string, unknown> | null { + if (!value || typeof value !== "object" || Array.isArray(value)) return null; + return value as Record<string, unknown>; +} + +function asString(value: unknown): string | null { + if (typeof value !== "string") return null; + const trimmed = value.trim(); + return trimmed.length > 0 ? trimmed : null; +} + +function compactTodo(value: unknown): string | null { + const rec = asRecord(value); + if (!rec) return asString(value); + return asString(rec.id) ?? asString(rec.content) ?? asString(rec.label); +} + +function progressLabel(progress: unknown): string { + const rec = asRecord(progress); + if (!rec) return "0 of 0 complete"; + const labeled = asString(rec.label); + if (labeled) return labeled; + const completed = typeof rec.completed === "number" ? rec.completed : 0; + const total = typeof rec.total === "number" ? rec.total : 0; + return `${completed} of ${total} complete`; +} + +function emptyMission(): McTuiMission { + return { + status: "idle", + planFile: null, + mode: null, + progressLabel: "0 of 0 complete", + currentTodo: null, + nextTodo: null, + gaps: null, + }; +} + +function emptyFlightLog(): McTuiFlightLog { + return { now: null, nowKind: null, earlier: [], warnings: [] }; +} + +/** + * Build the four-panel TUI view from a dashboard-data snapshot object. + * Does not re-derive mission / activity / flight-log semantics. + */ +export function buildMcTuiView( + snapshot: Record<string, unknown> | null | undefined, + error: string | null = null, +): McTuiView { + if (error) { + return { + mission: emptyMission(), + flightLog: emptyFlightLog(), + checklist: [], + crewMonitor: [], + error, + }; + } + const mc = asRecord(snapshot?.missionControl); + const now = asRecord(mc?.now); + const mission: McTuiMission = now + ? { + status: asString(now.status) ?? "idle", + planFile: asString(now.planFile), + mode: asString(now.mode), + progressLabel: progressLabel(now.progress), + currentTodo: compactTodo(now.currentTodo), + nextTodo: compactTodo(now.nextTodo), + gaps: asString(now.gaps), + } + : emptyMission(); + + const flight = asRecord(mc?.flightLog); + const earlierRaw = Array.isArray(flight?.past) ? flight.past : []; + const warningRaw = Array.isArray(flight?.warnings) ? flight.warnings : []; + const flightLog: McTuiFlightLog = flight + ? { + now: asString(flight.current), + nowKind: asString(flight.currentKind), + earlier: earlierRaw + .map((entry) => asString(asRecord(entry)?.text) ?? asString(entry)) + .filter((text): text is string => Boolean(text)) + .slice(0, EARLIER_CAP), + warnings: warningRaw + .map((entry) => asString(asRecord(entry)?.text) ?? asString(asRecord(entry)?.title)) + .filter((text): text is string => Boolean(text)) + .slice(0, WARNING_CAP), + } + : emptyFlightLog(); + + const plansRaw = Array.isArray(mc?.plans) ? mc.plans : []; + const checklist: McTuiChecklistRow[] = plansRaw + .map((plan) => { + const rec = asRecord(plan); + if (!rec) return null; + const file = asString(rec.file); + if (!file) return null; + const lifecycle = asString(rec.lifecycle) ?? "unknown"; + if (lifecycle === "completed") return null; + return { + file, + lifecycle, + progressLabel: progressLabel(rec.progress), + currentTodo: compactTodo(rec.currentTodo), + }; + }) + .filter((row): row is McTuiChecklistRow => Boolean(row)) + .slice(0, CHECKLIST_CAP); + + const feedCap = + typeof mc?.monitorFeedCap === "number" && mc.monitorFeedCap > 0 + ? Math.min(CREW_CAP, Math.floor(mc.monitorFeedCap)) + : CREW_CAP; + const activityRaw = Array.isArray(mc?.activity) ? mc.activity : []; + const crewMonitor: McTuiCrewRow[] = activityRaw + .map((event) => { + const rec = asRecord(event); + if (!rec) return null; + const label = asString(rec.label) ?? asString(rec.labelFull); + if (!label) return null; + return { kind: asString(rec.kind) ?? "activity", label }; + }) + .filter((row): row is McTuiCrewRow => Boolean(row)) + .slice(0, feedCap); + + return { mission, flightLog, checklist, crewMonitor, error: null }; +} diff --git a/packages/cli/src/scanner/detect-git.test.ts b/packages/cli/src/scanner/detect-git.test.ts new file mode 100644 index 0000000..17d283c --- /dev/null +++ b/packages/cli/src/scanner/detect-git.test.ts @@ -0,0 +1,98 @@ +import { execFile } from "node:child_process"; +import { mkdtemp, writeFile } from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import { promisify } from "node:util"; +import { describe, expect, it } from "vitest"; +import { detectGit, inferWorkflow } from "./detect-git.js"; + +const exec = promisify(execFile); + +async function git(root: string, ...args: string[]): Promise<void> { + await exec("git", args, { cwd: root }); +} + +async function initializeGit(root: string, branch = "main"): Promise<void> { + await git(root, "init", "-b", branch); + await git(root, "config", "user.email", "detect-git@example.test"); + await git(root, "config", "user.name", "Detect Git Test"); + await git(root, "add", "."); + await git(root, "commit", "-m", "test fixture"); +} + +describe("inferWorkflow", () => { + it("returns homolog-prod when a local staging branch exists, regardless of the current branch", () => { + expect(inferWorkflow("main", true, false)).toBe("homolog-prod"); + }); + + it("returns homolog-prod when only a remote staging branch exists", () => { + expect(inferWorkflow("main", false, true)).toBe("homolog-prod"); + }); + + it("falls back to feature-pr on main/master when no staging branch exists anywhere", () => { + expect(inferWorkflow("main", false, false)).toBe("feature-pr"); + expect(inferWorkflow("master", false, false)).toBe("feature-pr"); + }); + + it("falls back to gitflow for develop/release branches when no staging branch exists", () => { + expect(inferWorkflow("develop", false, false)).toBe("gitflow"); + expect(inferWorkflow("release/1.0", false, false)).toBe("gitflow"); + }); + + it("falls back to branch-name matching for homolog/staging-like branches", () => { + expect(inferWorkflow("homolog", false, false)).toBe("homolog-prod"); + expect(inferWorkflow("feature/staging-fix", false, false)).toBe("homolog-prod"); + }); + + it("returns unknown when there is no current branch and no staging fact", () => { + expect(inferWorkflow(undefined, false, false)).toBe("unknown"); + }); + + it("returns feature-pr for an unmatched branch name with no staging fact", () => { + expect(inferWorkflow("feature/add-widget", false, false)).toBe("feature-pr"); + }); +}); + +describe("detectGit workflow", () => { + it("reports homolog-prod even when HEAD is on the default branch, not the staging branch", async () => { + const root = await mkdtemp(path.join(os.tmpdir(), "agent-kit-detect-git-local-staging-")); + await writeFile(path.join(root, "README.md"), "# Repository\n"); + await initializeGit(root); + await git(root, "branch", "staging"); + // HEAD stays on "main" -- the bug this guards against is workflow + // flipping to feature-pr just because the operator isn't checked out + // on the staging branch itself. + + const result = await detectGit(root); + + expect(result.currentBranch).toBe("main"); + expect(result.hasLocalStaging).toBe(true); + expect(result.workflow).toBe("homolog-prod"); + }, 15_000); + + it("reports homolog-prod from a remote-only staging branch", async () => { + const root = await mkdtemp(path.join(os.tmpdir(), "agent-kit-detect-git-remote-staging-")); + await writeFile(path.join(root, "README.md"), "# Repository\n"); + await initializeGit(root); + await git(root, "remote", "add", "origin", "git@git.example.test:team/repo.git"); + await git(root, "update-ref", "refs/remotes/origin/staging", "HEAD"); + + const result = await detectGit(root); + + expect(result.hasLocalStaging).toBe(false); + expect(result.hasRemoteStaging).toBe(true); + expect(result.workflow).toBe("homolog-prod"); + }, 15_000); + + it("reports feature-pr on main when there is no staging branch anywhere", async () => { + const root = await mkdtemp(path.join(os.tmpdir(), "agent-kit-detect-git-no-staging-")); + await writeFile(path.join(root, "README.md"), "# Repository\n"); + await initializeGit(root); + + const result = await detectGit(root); + + expect(result.hasLocalStaging).toBe(false); + expect(result.hasRemoteStaging).toBe(false); + expect(result.workflow).toBe("feature-pr"); + }, 15_000); +}); diff --git a/packages/cli/src/scanner/detect-git.ts b/packages/cli/src/scanner/detect-git.ts index 7e55066..5b1876e 100644 --- a/packages/cli/src/scanner/detect-git.ts +++ b/packages/cli/src/scanner/detect-git.ts @@ -135,7 +135,21 @@ export async function detectProvider(rootDir: string, remoteUrl?: string): Promi }; } -function inferWorkflow(currentBranch?: string): GitWorkflow { +/** + * Infers the repository's promotion workflow. `hasLocalStaging`/ + * `hasRemoteStaging` are repo-level facts (does a `staging` branch exist + * anywhere in the repo?) and take priority over the currently checked-out + * branch name, which is only a session-level fact: an operator can be on + * `main` in a repo that otherwise uses staging/prod promotion, and the + * workflow classification must not flip depending on which branch happens + * to be checked out. + */ +export function inferWorkflow( + currentBranch: string | undefined, + hasLocalStaging: boolean, + hasRemoteStaging: boolean, +): GitWorkflow { + if (hasLocalStaging || hasRemoteStaging) return "homolog-prod"; if (!currentBranch) return "unknown"; if (currentBranch === "main" || currentBranch === "master") return "feature-pr"; if (currentBranch.includes("develop") || currentBranch.includes("release")) return "gitflow"; @@ -203,6 +217,10 @@ export async function detectGit(rootDir: string): Promise<GitDetection> { : localBranches?.includes("master") ? "master" : undefined); + const hasLocalStaging = localBranches?.includes("staging") ?? false; + const hasRemoteStaging = + remoteBranches?.some((branch) => branch === "origin/staging" || branch.endsWith("/staging")) ?? + false; return { provider: provider.provider, @@ -216,11 +234,8 @@ export async function detectGit(rootDir: string): Promise<GitDetection> { currentBranch, defaultBranch, isDirty: Boolean(status), - hasLocalStaging: localBranches?.includes("staging") ?? false, - hasRemoteStaging: - remoteBranches?.some( - (branch) => branch === "origin/staging" || branch.endsWith("/staging"), - ) ?? false, - workflow: inferWorkflow(currentBranch), + hasLocalStaging, + hasRemoteStaging, + workflow: inferWorkflow(currentBranch, hasLocalStaging, hasRemoteStaging), }; } diff --git a/packages/cli/src/scanner/detect-repository.test.ts b/packages/cli/src/scanner/detect-repository.test.ts new file mode 100644 index 0000000..7e7222a --- /dev/null +++ b/packages/cli/src/scanner/detect-repository.test.ts @@ -0,0 +1,117 @@ +import { mkdir, mkdtemp, writeFile } from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import { describe, expect, it } from "vitest"; +import { detectPurpose } from "./detect-repository.js"; +import { detectStack } from "./detect-stack.js"; +import { createReadinessReport } from "./readiness.js"; +import { runScanner } from "./scan.js"; + +async function writeProfile(root: string, profile: Record<string, unknown>): Promise<void> { + await mkdir(path.join(root, ".cursor"), { recursive: true }); + await writeFile( + path.join(root, ".cursor", "agent-kit.config.json"), + `${JSON.stringify(profile, null, 2)}\n`, + ); +} + +describe("detectPurpose", () => { + it("falls back to directory heuristics when no profile is present", async () => { + const root = await mkdtemp(path.join(os.tmpdir(), "agent-kit-purpose-none-")); + await mkdir(path.join(root, "docs")); + const stack = await detectStack(root); + + const purpose = await detectPurpose(root, stack); + + expect(purpose.value).toBe("documentation"); + }); + + it("falls back to directory heuristics when the profile's purpose is unknown", async () => { + const root = await mkdtemp(path.join(os.tmpdir(), "agent-kit-purpose-unknown-")); + await mkdir(path.join(root, "docs")); + await writeProfile(root, { purpose: { value: "unknown" } }); + const stack = await detectStack(root); + + const purpose = await detectPurpose(root, stack); + + expect(purpose.value).toBe("documentation"); + }); + + it("honors an operator-confirmed purpose over a directory-name mismatch", async () => { + const root = await mkdtemp(path.join(os.tmpdir(), "agent-kit-purpose-confirmed-")); + // Directory heuristics alone would classify this as "documentation". + await mkdir(path.join(root, "docs")); + await writeProfile(root, { purpose: { value: "operations", confirmed: true } }); + const stack = await detectStack(root); + + const purpose = await detectPurpose(root, stack); + + expect(purpose.value).toBe("operations"); + expect(purpose.confidence).toBe("high"); + }); + + it("ignores a malformed or unrecognized confirmed purpose value", async () => { + const root = await mkdtemp(path.join(os.tmpdir(), "agent-kit-purpose-malformed-")); + await mkdir(path.join(root, "docs")); + await writeProfile(root, { purpose: { value: "not-a-real-purpose" } }); + const stack = await detectStack(root); + + const purpose = await detectPurpose(root, stack); + + expect(purpose.value).toBe("documentation"); + }); + + it("changes purpose.classification on the next scan once an operator confirms it", async () => { + const root = await mkdtemp(path.join(os.tmpdir(), "agent-kit-purpose-scan-")); + const before = await runScanner(root); + expect(before.purpose.value).toBe("unknown"); + + await writeProfile(root, { purpose: { value: "knowledge" } }); + const after = await runScanner(root); + + expect(after.purpose.value).toBe("knowledge"); + }); +}); + +describe("readiness stack.detected", () => { + it("does not derive stack readiness from the purpose signal", async () => { + const root = await mkdtemp(path.join(os.tmpdir(), "agent-kit-stack-purpose-")); + // Purpose is confirmed/known (non-unknown), but there is no real stack evidence. + await mkdir(path.join(root, "docs")); + await writeProfile(root, { purpose: { value: "documentation" } }); + + const scan = await runScanner(root); + const report = createReadinessReport(scan, { + generatorVersion: "test", + generatedAt: "2026-08-20T00:00:00.000Z", + }); + const stackCheck = report.pillars + .find((item) => item.pillar === "stack-tooling") + ?.checks.find((item) => item.id === "stack.detected"); + + expect(scan.purpose.value).not.toBe("unknown"); + expect(scan.stack.language).toBe("unknown"); + expect(stackCheck?.status).toBe("needs_choice"); + }); + + it("is ready when genuine stack facts exist even if purpose is unknown", async () => { + const root = await mkdtemp(path.join(os.tmpdir(), "agent-kit-stack-detected-")); + await writeFile( + path.join(root, "package.json"), + JSON.stringify({ private: true, scripts: { test: "vitest run" } }), + ); + await writeFile(path.join(root, "package-lock.json"), "{}"); + + const scan = await runScanner(root); + const report = createReadinessReport(scan, { + generatorVersion: "test", + generatedAt: "2026-08-20T00:00:00.000Z", + }); + const stackCheck = report.pillars + .find((item) => item.pillar === "stack-tooling") + ?.checks.find((item) => item.id === "stack.detected"); + + expect(scan.stack.language).toBe("node"); + expect(stackCheck?.status).toBe("ready"); + }); +}); diff --git a/packages/cli/src/scanner/detect-repository.ts b/packages/cli/src/scanner/detect-repository.ts index d660764..f0ff8ad 100644 --- a/packages/cli/src/scanner/detect-repository.ts +++ b/packages/cli/src/scanner/detect-repository.ts @@ -55,10 +55,77 @@ export async function detectContext(rootDir: string): Promise<ContextDetection> }; } +const REPOSITORY_PURPOSE_VALUES: readonly RepositoryPurpose[] = [ + "application", + "library", + "monorepo", + "documentation", + "knowledge", + "operations", + "automation", + "mixed", + "unknown", +]; + +function isRepositoryPurpose(value: unknown): value is RepositoryPurpose { + return ( + typeof value === "string" && (REPOSITORY_PURPOSE_VALUES as readonly string[]).includes(value) + ); +} + +interface ProfileConfiguration { + purpose?: { + value?: unknown; + categories?: unknown; + evidence?: unknown; + }; +} + +/** + * An operator can confirm a repository's purpose (e.g. via onboarding), which + * is persisted to `.cursor/agent-kit.config.json#purpose`. Once confirmed, + * that value is the source of truth: it must not be silently reclassified by + * directory-name heuristics on the next scan, or onboarding's "confirm + * purpose" question has no effect and the readiness gate loops forever. + */ +async function readConfirmedPurpose( + rootDir: string, +): Promise<RepositoryPurposeDetection | undefined> { + const configuration = await readJson<ProfileConfiguration>( + path.join(rootDir, ".cursor", "agent-kit.config.json"), + ); + const configuredPurpose = configuration?.purpose; + if ( + !configuredPurpose || + !isRepositoryPurpose(configuredPurpose.value) || + configuredPurpose.value === "unknown" + ) { + return undefined; + } + const categories = Array.isArray(configuredPurpose.categories) + ? configuredPurpose.categories.filter(isRepositoryPurpose) + : []; + const evidence = Array.isArray(configuredPurpose.evidence) + ? (configuredPurpose.evidence as DetectionEvidence[]) + : []; + return { + value: configuredPurpose.value, + categories: categories.length > 0 ? categories : [configuredPurpose.value], + confidence: "high", + evidence: + evidence.length > 0 + ? evidence + : [{ source: "configuration", value: ".cursor/agent-kit.config.json#purpose.value" }], + }; +} + export async function detectPurpose( rootDir: string, stack: StackDetection, ): Promise<RepositoryPurposeDetection> { + const confirmed = await readConfirmedPurpose(rootDir); + if (confirmed) return confirmed; + const entries = await listDirectory(rootDir); const lowerEntries = entries.map((entry) => entry.toLowerCase()); const packageJson = await readJson<{ @@ -153,6 +220,19 @@ export const REQUIRED_SECRET_PATTERNS = [ "*service-account*.json", ] as const; +// Kit-owned session state and derived snapshots that churn on every +// `doctor`/Mission Control run. These are separate from +// REQUIRED_SECRET_PATTERNS so the safety.secrets readiness check keeps its +// secrets-only semantics; this list only affects what the installer writes +// into .gitignore. +export const KIT_OWNED_IGNORE_PATTERNS = [ + ".cursor/HANDOFF.md", + ".cursor/dogfood/", + ".cursor/context/readiness.json", + ".cursor/context/flight-log.json", + ".cursor/context/mission-timing.json", +] as const; + export async function detectSafety( rootDir: string, trackedFiles: string[], diff --git a/packages/cli/src/scanner/readiness.ts b/packages/cli/src/scanner/readiness.ts index 1b3aa22..65964e5 100644 --- a/packages/cli/src/scanner/readiness.ts +++ b/packages/cli/src/scanner/readiness.ts @@ -217,11 +217,9 @@ function buildPillars(scan: ScanResult): ReadinessPillarReport[] { check( "stack.detected", "Stack and package manager", - scan.stack.language !== "unknown" || scan.purpose.value !== "unknown" - ? "ready" - : "needs_choice", + scan.stack.language !== "unknown" || scan.stack.hasProjectFiles ? "ready" : "needs_choice", true, - scan.stack.packageManagerEvidence ?? scan.purpose.evidence, + scan.stack.packageManagerEvidence ?? [], ), ]), pillar("quality-ci", [ diff --git a/packages/cli/src/scanner/refresh-profile.test.ts b/packages/cli/src/scanner/refresh-profile.test.ts new file mode 100644 index 0000000..533fdb7 --- /dev/null +++ b/packages/cli/src/scanner/refresh-profile.test.ts @@ -0,0 +1,113 @@ +import { execFile } from "node:child_process"; +import { mkdir, mkdtemp, writeFile } from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import { promisify } from "node:util"; +import { describe, expect, it } from "vitest"; +import { readJson } from "../utils/fs.js"; +import { refreshRepositoryProfile } from "./safe-fixes.js"; + +const exec = promisify(execFile); +const GENERATED_AT = "2026-08-20T00:00:00.000Z"; + +async function git(root: string, ...args: string[]): Promise<void> { + await exec("git", args, { cwd: root }); +} + +async function initializeGit(root: string, branch: string): Promise<void> { + await git(root, "init", "-b", branch); + await git(root, "config", "user.email", "refresh-profile@example.test"); + await git(root, "config", "user.name", "Refresh Profile Test"); + await git(root, "add", "."); + await git(root, "commit", "-m", "test fixture"); +} + +async function createRepository(): Promise<string> { + const root = await mkdtemp(path.join(os.tmpdir(), "agent-kit-refresh-profile-")); + await writeFile(path.join(root, "README.md"), "# Test repository\n"); + return root; +} + +describe("refreshRepositoryProfile", () => { + it("writes a fresh profile on first run when none exists", async () => { + const root = await createRepository(); + + const result = await refreshRepositoryProfile(root, { + generatorVersion: "test", + generatedAt: GENERATED_AT, + }); + + expect(result.changed).toBe(true); + const onDisk = await readJson<Record<string, unknown>>( + path.join(root, ".cursor/agent-kit.config.json"), + ); + expect(onDisk).toMatchObject({ schemaVersion: 1, contractVersion: 1 }); + }); + + it("is idempotent: a second refresh with no repository change is a no-op", async () => { + const root = await createRepository(); + await refreshRepositoryProfile(root, { generatorVersion: "test", generatedAt: GENERATED_AT }); + const firstContent = await readJson<Record<string, unknown>>( + path.join(root, ".cursor/agent-kit.config.json"), + ); + + const second = await refreshRepositoryProfile(root, { + generatorVersion: "test", + generatedAt: "2026-08-21T00:00:00.000Z", + }); + + expect(second.changed).toBe(false); + const secondContent = await readJson<Record<string, unknown>>( + path.join(root, ".cursor/agent-kit.config.json"), + ); + expect(secondContent).toEqual(firstContent); + }); + + it("overwrites a stale scanner-owned fact (currentBranch) instead of staying sticky", async () => { + const root = await createRepository(); + await initializeGit(root, "main"); + await refreshRepositoryProfile(root, { generatorVersion: "test", generatedAt: GENERATED_AT }); + + await git(root, "checkout", "-b", "feature/rename"); + const result = await refreshRepositoryProfile(root, { + generatorVersion: "test", + generatedAt: "2026-08-21T00:00:00.000Z", + }); + + expect(result.changed).toBe(true); + const onDisk = await readJson<{ git: { currentBranch: string } }>( + path.join(root, ".cursor/agent-kit.config.json"), + ); + expect(onDisk?.git.currentBranch).toBe("feature/rename"); + }); + + it("preserves an operator-confirmed purpose and unrecognized custom keys", async () => { + const root = await createRepository(); + await mkdir(path.join(root, ".cursor"), { recursive: true }); + await writeFile( + path.join(root, ".cursor/agent-kit.config.json"), + `${JSON.stringify( + { + purpose: { value: "operations", confirmed: true }, + customProjectFact: "keep", + }, + null, + 2, + )}\n`, + ); + + const result = await refreshRepositoryProfile(root, { + generatorVersion: "test", + generatedAt: GENERATED_AT, + }); + + expect(result.changed).toBe(true); + const onDisk = await readJson<Record<string, unknown>>( + path.join(root, ".cursor/agent-kit.config.json"), + ); + expect(onDisk).toMatchObject({ + purpose: { value: "operations", confirmed: true }, + customProjectFact: "keep", + }); + }); +}); diff --git a/packages/cli/src/scanner/safe-fixes.test.ts b/packages/cli/src/scanner/safe-fixes.test.ts index 80dbac7..81a32dd 100644 --- a/packages/cli/src/scanner/safe-fixes.test.ts +++ b/packages/cli/src/scanner/safe-fixes.test.ts @@ -148,6 +148,30 @@ describe("executeSafeReadinessFixes", () => { } }); + it("merges kit-owned session/derived ignore patterns without removing custom entries", async () => { + const root = await createRepository(); + await writeFile( + path.join(root, ".gitignore"), + "node_modules\n.env\n# project files\ncustom.tmp", + ); + + await execute(root); + const gitignore = await readFile(path.join(root, ".gitignore"), "utf8"); + + expect(gitignore).toContain("node_modules\n"); + expect(gitignore).toContain("# project files\n"); + expect(gitignore).toContain("custom.tmp\n"); + for (const pattern of [ + ".cursor/HANDOFF.md", + ".cursor/dogfood/", + ".cursor/context/readiness.json", + ".cursor/context/flight-log.json", + ".cursor/context/mission-timing.json", + ]) { + expect(gitignore.split(/\r?\n/).filter((line) => line === pattern)).toHaveLength(1); + } + }); + it("does not modify protected or project-owned content", async () => { const root = await createRepository(); const protectedFiles = [ diff --git a/packages/cli/src/scanner/safe-fixes.ts b/packages/cli/src/scanner/safe-fixes.ts index 5655bba..27438fc 100644 --- a/packages/cli/src/scanner/safe-fixes.ts +++ b/packages/cli/src/scanner/safe-fixes.ts @@ -10,7 +10,7 @@ import type { ScanResult, } from "../types.js"; import { ensureDir, fileExists, readJson, writeJson } from "../utils/fs.js"; -import { REQUIRED_SECRET_PATTERNS } from "./detect-repository.js"; +import { KIT_OWNED_IGNORE_PATTERNS, REQUIRED_SECRET_PATTERNS } from "./detect-repository.js"; import { createReadinessReport } from "./readiness.js"; import { runScanner } from "./scan.js"; @@ -192,19 +192,27 @@ function preferenceDefaults(onboarding: OnboardingState, onboarded: unknown): Js }; } -function mergeSecretIgnores(existing: string): string { +function mergeIgnorePatterns(existing: string, patterns: readonly string[]): string { const activeLines = new Set( existing .split(/\r?\n/) .map((line) => line.trim()) .filter((line) => line && !line.startsWith("#")), ); - const missing = REQUIRED_SECRET_PATTERNS.filter((pattern) => !activeLines.has(pattern)); + const missing = patterns.filter((pattern) => !activeLines.has(pattern)); if (missing.length === 0) return existing; const prefix = existing.length === 0 ? "" : existing.endsWith("\n") ? existing : `${existing}\n`; return `${prefix}${missing.join("\n")}\n`; } +function mergeSecretIgnores(existing: string): string { + return mergeIgnorePatterns(existing, REQUIRED_SECRET_PATTERNS); +} + +function mergeKitOwnedIgnores(existing: string): string { + return mergeIgnorePatterns(existing, KIT_OWNED_IGNORE_PATTERNS); +} + function recordChange( changes: SafeReadinessChange[], id: string, @@ -232,6 +240,91 @@ function appliedActions(changes: SafeReadinessChange[]): ReadinessAction[] { })); } +export interface ProfileRefreshOptions { + generatorVersion: string; + generatedAt?: string; +} + +export interface ProfileRefreshResult { + /** False when the reconciled profile is identical to the on-disk one (ignoring the timestamp), so nothing was written. */ + changed: boolean; + profile: RepositoryProfile; +} + +function withoutGeneratedAtForComparison(profile: JsonObject): JsonObject { + const { generatedAt: _generatedAt, ...detection } = isObject(profile.detection) + ? profile.detection + : {}; + return { ...profile, detection }; +} + +/** + * Deep-merges `desired` (fresh scanner output) over `existing` (the on-disk + * profile): every key `desired` defines wins outright -- including an + * `undefined`/cleared value, so a fact that's no longer true (e.g. no + * current branch) actually clears instead of leaking the stale value back + * in. `mergeMissing` above can't be reused here with swapped arguments: its + * `existing ?? defaults` fallback treats a fresh `undefined` as "not set" + * and resurrects the stale default, which is exactly the staleness this + * function exists to fix. Keys `existing` has that `desired` doesn't (an + * operator- or tool-added extra field) are preserved untouched. + */ +function reconcileFreshOverExisting(desired: JsonObject, existing: JsonObject): JsonObject { + const merged: JsonObject = { ...existing }; + for (const [key, value] of Object.entries(desired)) { + const existingValue = existing[key]; + merged[key] = + isObject(value) && isObject(existingValue) + ? reconcileFreshOverExisting(value, existingValue) + : value; + } + return merged; +} + +/** + * Reconciles `.cursor/agent-kit.config.json` with the scanner's current + * output: fresh, scanner-owned values win on every key both sides define + * (so a stale `git.currentBranch`/`stack`/`context` gets corrected), while + * any extra keys an operator or another tool added to the profile that the + * scanner doesn't know about (e.g. a hand-added `purpose.confirmed` flag) + * are preserved. This is the inverse of `executeSafeReadinessFixes`' + * merge-missing-only profile write, which intentionally never overwrites an + * already-present value (so install-time facts can otherwise go stale + * forever). + * + * A no-op refresh (nothing besides `detection.generatedAt` would change) + * skips the write entirely, so repeated refreshes are idempotent rather + * than only bumping a timestamp. + */ +export async function refreshRepositoryProfile( + rootDir: string, + options: ProfileRefreshOptions, +): Promise<ProfileRefreshResult> { + const generatedAt = options.generatedAt ?? new Date().toISOString(); + const scan = await runScanner(rootDir); + const report = createReadinessReport(scan, { + generatorVersion: options.generatorVersion, + generatedAt, + }); + const desiredProfile = createProfile(scan, report, generatedAt) as unknown as JsonObject; + const profilePath = path.join(scan.rootDir, PROFILE_RELATIVE_PATH); + const existingProfile = (await readJson<JsonObject>(profilePath)) ?? {}; + // Fresh values win on shared keys (including clearing to undefined); + // existing-only keys survive. + const reconciled = reconcileFreshOverExisting(desiredProfile, existingProfile); + + const meaningfulChange = !jsonEqual( + withoutGeneratedAtForComparison(existingProfile), + withoutGeneratedAtForComparison(reconciled), + ); + if (!meaningfulChange) { + return { changed: false, profile: existingProfile as unknown as RepositoryProfile }; + } + + await writeJson(profilePath, reconciled); + return { changed: true, profile: reconciled as unknown as RepositoryProfile }; +} + export async function executeSafeReadinessFixes( rootDir: string, options: SafeReadinessOptions, @@ -264,18 +357,34 @@ export async function executeSafeReadinessFixes( const existingGitignore = (await fileExists(gitignorePath)) ? await readFile(gitignorePath, "utf8") : ""; - const mergedGitignore = mergeSecretIgnores(existingGitignore); + const gitignoreAfterSecrets = mergeSecretIgnores(existingGitignore); + const secretsChanged = gitignoreAfterSecrets !== existingGitignore; + const mergedGitignore = mergeKitOwnedIgnores(gitignoreAfterSecrets); const gitignoreChanged = mergedGitignore !== existingGitignore; + const kitOwnedChanged = mergedGitignore !== gitignoreAfterSecrets; if (gitignoreChanged && !dryRun) await writeFile(gitignorePath, mergedGitignore, "utf8"); recordChange( changes, "merge-secret-ignores", gitignoreRelativePath, - gitignoreChanged, + secretsChanged, + dryRun, + relativeEvidence( + gitignoreRelativePath, + secretsChanged ? "required secret patterns are missing" : "required patterns are present", + ), + ); + recordChange( + changes, + "merge-kit-owned-ignores", + gitignoreRelativePath, + kitOwnedChanged, dryRun, relativeEvidence( gitignoreRelativePath, - gitignoreChanged ? "required secret patterns are missing" : "required patterns are present", + kitOwnedChanged + ? "kit-owned session/derived paths are missing" + : "kit-owned session/derived paths are present", ), ); diff --git a/packages/cli/src/welcome/help-groups.ts b/packages/cli/src/welcome/help-groups.ts index b4776de..bbb796b 100644 --- a/packages/cli/src/welcome/help-groups.ts +++ b/packages/cli/src/welcome/help-groups.ts @@ -31,7 +31,7 @@ export const CLI_HELP_GROUPS: HelpGroup[] = [ { id: "dashboard", title: "DASHBOARD", - commands: ["dashboard", "dashboard-broadcast", "monitors"], + commands: ["dashboard", "dashboard-broadcast", "mission-control", "monitors"], }, { id: "integrity", diff --git a/packages/cli/src/welcome/screen.ts b/packages/cli/src/welcome/screen.ts index bb2571d..33cfb16 100644 --- a/packages/cli/src/welcome/screen.ts +++ b/packages/cli/src/welcome/screen.ts @@ -3,15 +3,7 @@ * Separate from run-plan persona banners under plan-loop/. */ -import { - blue, - cyan, - gray, - options as koloristOptions, - lightCyan, - trueColor, - white, -} from "kolorist"; +import { blue, cyan, gray, lightCyan, trueColor, white } from "kolorist"; import { KIT_VERSION } from "../lifecycle/version.js"; import { HELMET_ACCENT, @@ -23,6 +15,7 @@ import { shouldUseVisualMotion, shouldUseWelcomeColor, tipAt, + withKoloristColor, } from "./visual-kit.js"; export { @@ -34,9 +27,6 @@ export { type WelcomeRenderOptions, }; -/** kolorist SupportLevel.TrueColor — needed so trueColor() emits when TTY probes say none (CI). */ -const KOLORIST_TRUECOLOR = 3; - const HELMET_ASCII = [ " ____", " .-' '-.", @@ -63,20 +53,6 @@ function outlineAnsi(line: string): string { 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; - } -} - /** 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("-"))); @@ -100,6 +76,7 @@ export const WELCOME_UTILITY_HINTS = [ { 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 mission-control", hint: "Mission Control TUI" }, { cmd: "agent-kit init", hint: "guided install entry" }, ] as const; diff --git a/packages/cli/src/welcome/visual-kit.ts b/packages/cli/src/welcome/visual-kit.ts index 1de139f..c9f2f98 100644 --- a/packages/cli/src/welcome/visual-kit.ts +++ b/packages/cli/src/welcome/visual-kit.ts @@ -5,6 +5,9 @@ import { cyan, gray, options as koloristOptions } from "kolorist"; +/** kolorist SupportLevel.TrueColor — emit 24-bit even when TTY probes say none (CI). */ +const KOLORIST_TRUECOLOR = 3; + /** Helmet outline / primary text: MC `--text-primary` / landing logo stroke. */ export const HELMET_OUTLINE = "#e2e8f0"; /** Deep brand blue: landing logo gradient mid. */ @@ -54,6 +57,20 @@ export function shouldUseWelcomeColor(opts: WelcomeRenderOptions = {}): boolean return tty; } +/** Run `fn` with kolorist forced on at trueColor support (restores prior options). */ +export 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; + } +} + /** Frames and spinners: color gate plus optional reduced-motion. */ export function shouldUseVisualMotion(opts: WelcomeRenderOptions = {}): boolean { if (process.env.AGENT_KIT_REDUCED_MOTION === "1") return false; diff --git a/scripts/verify-cli-dashboard-pack.mjs b/scripts/verify-cli-dashboard-pack.mjs index b1f316f..ac496c4 100644 --- a/scripts/verify-cli-dashboard-pack.mjs +++ b/scripts/verify-cli-dashboard-pack.mjs @@ -30,6 +30,7 @@ const cliDir = join(repoRoot, "packages", "cli"); export const REQUIRED_DASHBOARD = [ "package/dashboard/start.mjs", "package/dashboard/start-broadcast.mjs", + "package/dashboard/dashboard-data.mjs", "package/dashboard/open.html", "package/dashboard/lib/broadcast-share.mjs", "package/dashboard/lib/guards.mjs",