diff --git a/.cursor-plugin/plugin.json b/.cursor-plugin/plugin.json index 076963b..cc02491 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.3.0", + "version": "5.4.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 bd3e4b4..b848172 100644 --- a/.cursor/agent-kit.json +++ b/.cursor/agent-kit.json @@ -1,6 +1,6 @@ { "schemaVersion": 1, - "version": "5.3.0", + "version": "5.4.0", "protected": [ ".cursor/HANDOFF.md", ".cursor/agents/test-suites.md", diff --git a/.cursor/agents/clickup-tasks.md b/.cursor/agents/clickup-tasks.md index 092756d..cdeaca1 100644 --- a/.cursor/agents/clickup-tasks.md +++ b/.cursor/agents/clickup-tasks.md @@ -10,8 +10,8 @@ rules: # ClickUp — Tasks and conventions -- Always follow conventions from the rule [cursor-skills-clickup.mdc](.cursor/rules/cursor-skills-clickup.mdc). -- Use the skill [clickup/SKILL.md](.cursor/skills/community/clickup/SKILL.md) for detailed process, examples and checklist. +- Always follow conventions from the rule [cursor-skills-clickup.mdc](../rules/cursor-skills-clickup.mdc). +- Use the skill [clickup/SKILL.md](../skills/community/clickup/SKILL.md) for detailed process, examples and checklist. ## When creating or updating tasks @@ -31,4 +31,4 @@ rules: - After `git staging`: update status of related task(s) to **staging**. - After `git prod`: update status to **complete** on delivered task(s). -- Details in [autogit/gitupdate.md](autogit/gitupdate.md) step 10.5. +- Details in [autogit/gitupdate.md](../../autogit/gitupdate.md) step 10.5. diff --git a/.cursor/agents/docs-repo.md b/.cursor/agents/docs-repo.md index 1a86854..f57e06f 100644 --- a/.cursor/agents/docs-repo.md +++ b/.cursor/agents/docs-repo.md @@ -27,7 +27,7 @@ Documentation belongs to the **project**: professional, inheritable, without peo ## References -- [README.md](README.md) +- [README.md](../../README.md) - Rule: `.cursor/rules/docs-professional-standard.mdc` ## Plan-monitor skim diff --git a/.cursor/agents/git-autogit.md b/.cursor/agents/git-autogit.md index b13c086..48bc471 100644 --- a/.cursor/agents/git-autogit.md +++ b/.cursor/agents/git-autogit.md @@ -15,6 +15,6 @@ rules: - **git prod:** approved staging → `origin/main`. - After each promote: update `.cursor/HANDOFF.md`; if applicable, memory-loop WRITE. - Messages: Conventional Commits (`feat:`, `fix:`, `docs:`, `chore:`, etc.). -- Details: [autogit/gitupdate.md](autogit/gitupdate.md). Rule: [cursor-skills-git-workflow.mdc](.cursor/rules/cursor-skills-git-workflow.mdc). +- Details: [autogit/gitupdate.md](../../autogit/gitupdate.md). Rule: [cursor-skills-git-workflow.mdc](../rules/cursor-skills-git-workflow.mdc). - Commands: `/git-staging`, `/git-prod`. - **Plan-monitor skim:** before promote, warn on dirty untracked `plan-monitor-*.md` and stage memory files add-by-name only. Advisory mention of Blocking untriaged monitors on `/git-prod` is allowed; never steal prod HITL. diff --git a/.cursor/agents/mission-kit-comms.md b/.cursor/agents/mission-kit-comms.md index 564397f..dc966a2 100644 --- a/.cursor/agents/mission-kit-comms.md +++ b/.cursor/agents/mission-kit-comms.md @@ -28,7 +28,7 @@ Follow skill `registry/skills/community/mission-kit-comms/SKILL.md` (factory ove - HTTP post, webhook, or `gh` issue comment that is marketing without Ask. - Put tokens, webhooks, or cookies in files that can be committed. -- Claim product behavior that is not in CHANGELOG 5.0.0 / getting-started / five-layer matrix. +- Claim product behavior that is not in the newest closed CHANGELOG version / getting-started / five-layer matrix. ## Naming diff --git a/.cursor/agents/n8n-workflows.md b/.cursor/agents/n8n-workflows.md index 26def94..e9d593c 100644 --- a/.cursor/agents/n8n-workflows.md +++ b/.cursor/agents/n8n-workflows.md @@ -13,4 +13,4 @@ rules: - Structure: nodes, connections, credentials; webhook → Switch/IF → Execute Workflow → Respond Webhook. - Document changes in docs/context/ or docs/n8n-manual-update-*.md when not re-importing. -- Rules: [cursor-skills-n8n.mdc](.cursor/rules/cursor-skills-n8n.mdc), [cursor-skills-json.mdc](.cursor/rules/cursor-skills-json.mdc). +- Rules: [cursor-skills-n8n.mdc](../rules/cursor-skills-n8n.mdc), [cursor-skills-json.mdc](../rules/cursor-skills-json.mdc). diff --git a/.cursor/agents/prompts-agents.md b/.cursor/agents/prompts-agents.md index 3097bf2..d1ea810 100644 --- a/.cursor/agents/prompts-agents.md +++ b/.cursor/agents/prompts-agents.md @@ -13,4 +13,4 @@ rules: - Follow reference model (e.g.: modelo-guest.md) for new prompts. - Naming: prompt-{name}-{context}.md or prompt-principal.md; versioning V1/V2 in subfolders. - .system/.user pairs when applicable. -- Project rule: [cursor-skills-prompts.mdc](.cursor/rules/cursor-skills-prompts.mdc). +- Project rule: [cursor-skills-prompts.mdc](../rules/cursor-skills-prompts.mdc). diff --git a/.cursor/agents/sql-schema.md b/.cursor/agents/sql-schema.md index ecb10c3..853e21f 100644 --- a/.cursor/agents/sql-schema.md +++ b/.cursor/agents/sql-schema.md @@ -12,4 +12,4 @@ rules: - Table scripts: e.g. 001_create_tables.sql, 002_indexes_constraints.sql. - DDL in dedicated folders (db/, scripts/, code/) as per project. -- Conventions: snake_case, IF NOT EXISTS, UUID, TIMESTAMPTZ, COMMENT ON. Rule: [cursor-skills-sql.mdc](.cursor/rules/cursor-skills-sql.mdc). +- Conventions: snake_case, IF NOT EXISTS, UUID, TIMESTAMPTZ, COMMENT ON. Rule: [cursor-skills-sql.mdc](../rules/cursor-skills-sql.mdc). diff --git a/.cursor/scripts/comms-draft.mjs b/.cursor/scripts/comms-draft.mjs index 00d46bb..243a93c 100644 --- a/.cursor/scripts/comms-draft.mjs +++ b/.cursor/scripts/comms-draft.mjs @@ -5,14 +5,13 @@ * node .cursor/scripts/comms-draft.mjs --kind recap --channel x * node .cursor/scripts/comms-draft.mjs --publish # always exits 2 */ -import { mkdirSync, readFileSync, writeFileSync, existsSync } from "node:fs"; +import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; const KINDS = new Set(["recap", "release", "contributor-ask"]); const CHANNELS = new Set(["x", "medium", "substack", "hn", "github", "site"]); -const PUBLISH_REFUSE = - "publish refused: comms-draft never posts; operator Ask then human publish"; +const PUBLISH_REFUSE = "publish refused: comms-draft never posts; operator Ask then human publish"; export function utcDay(d = new Date()) { return d.toISOString().slice(0, 10); @@ -67,7 +66,8 @@ export function renderDraft(opts, day) { const bodies = { recap: `Mission Kit recap (${day})\n\nShipped this cycle: fill from CHANGELOG [Unreleased] that is already in staging.\n\nInstall: npx @dadado/agent-kit-cli@${version} install\nSite: https://missionkit.io\nHITL: production still needs a human yes.\n`, release: `Mission Kit ${version} / Agent Kit CLI @${version}\n\nnpx @dadado/agent-kit-cli@${version} install\nhttps://missionkit.io\nPolyForm Noncommercial; commercial: sales@missionkit.io\n`, - "contributor-ask": `Help Mission Kit: skills under registry/skills/community/, or agent-kit contribute from a consumer project.\nIssues: https://github.com/agent-kit-startup/agent-kit\nDo not use public issues for vulnerabilities (SECURITY.md).\nCursor Marketplace submit is not this ask.\n`, + "contributor-ask": + "Help Mission Kit: skills under registry/skills/community/, or agent-kit contribute from a consumer project.\nIssues: https://github.com/agent-kit-startup/agent-kit\nDo not use public issues for vulnerabilities (SECURITY.md).\nCursor Marketplace submit is not this ask.\n", }; const body = redactSecrets(bodies[opts.kind]); return { diff --git a/.cursor/scripts/plan-external-review-progress-gate.test.mjs b/.cursor/scripts/plan-external-review-progress-gate.test.mjs index 6939b10..d44f4e8 100644 --- a/.cursor/scripts/plan-external-review-progress-gate.test.mjs +++ b/.cursor/scripts/plan-external-review-progress-gate.test.mjs @@ -145,9 +145,13 @@ test("heartbeat lines keep status prefixes; suffix is empty under CI", () => { assert.match(src, /Try agent-kit doctor for repository readiness\./); const fn = extractSuffix(); assert.match(fn, /audit_kit_suffix/); - const result = spawnSync("bash", ["-c", `${fn}\nexport CI=1\nprintf '[%s]' "$(audit_kit_suffix 20)"`], { - encoding: "utf8", - }); + const result = spawnSync( + "bash", + ["-c", `${fn}\nexport CI=1\nprintf '[%s]' "$(audit_kit_suffix 20)"`], + { + encoding: "utf8", + }, + ); assert.strictEqual(result.status ?? 1, 0, result.stderr); assert.strictEqual(result.stdout, "[]"); }); diff --git a/.cursor/skills/community/clickup/SKILL.md b/.cursor/skills/community/clickup/SKILL.md index 91638fb..6dcb202 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-skills-clickup.mdc](.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/rules/cursor-skills-clickup.mdc` (`alwaysApply: false`). ## When to Use diff --git a/.cursor/skills/community/mission-kit-comms/SKILL.md b/.cursor/skills/community/mission-kit-comms/SKILL.md index 7d796d0..1eb90b9 100644 --- a/.cursor/skills/community/mission-kit-comms/SKILL.md +++ b/.cursor/skills/community/mission-kit-comms/SKILL.md @@ -26,7 +26,7 @@ Thin reuse: short blocks and one ask per message from sibling skill `ux-message- - `docs/public-launch-announcement.md` - Channel map: `docs/comms-channel-map.md` - Calendar: `docs/comms-content-calendar.md` -- Product version floor: **5.0.0**. Do not write 4.x as current. +- Product version floor: **5.0.0**; current release is the newest closed `CHANGELOG.md` version (5.3.0 as of 2026-08-15). Do not write 4.x as current. Forbidden in drafts: full autonomy without HITL; Cursor Marketplace "listed" unless the parked submit plan has actually listed; npm/CLI renamed to Mission Kit; silent posting. diff --git a/.cursor/skills/community/n8n-workflows/checklist-n8n.md b/.cursor/skills/community/n8n-workflows/checklist-n8n.md new file mode 100644 index 0000000..b2d2a4b --- /dev/null +++ b/.cursor/skills/community/n8n-workflows/checklist-n8n.md @@ -0,0 +1,47 @@ +# Checklist: n8n/JSON Change + +## Identification +- **Workflow:** [name] +- **File:** [path] +- **Change type:** [new|edit|delete] + +## Pre-change +- [ ] Backup created in `.cursor/context/backups/` +- [ ] Current JSON is valid +- [ ] I understand the current structure + +## n8n Structure +- [ ] Webhook has responseMode: "responseNode" when used with Respond Webhook +- [ ] All branches end in Respond Webhook (or NoOp if not responding) +- [ ] Node IDs are unique and follow kebab-case pattern + +## Connections +- [ ] All connections point to existing nodes +- [ ] No orphaned nodes (without input and output, except Webhook and Respond) +- [ ] Execute Workflow has workflowId filled (not empty) + +## Credentials +- [ ] Credentials referenced by id (not hardcoded) +- [ ] No secrets in JSON (search for: password, token, key, secret) +- [ ] Document required credentials in README + +## Change +- [ ] Added/edited nodes are correct +- [ ] Connections are correct (from → to) +- [ ] $json references point to correct node in the chain +- [ ] $('NodeName') uses exact name (case-sensitive) +- [ ] Postgres nodes with RETURNING when data is used later + +## Validation +- [ ] JSON is valid after change +- [ ] n8n-checker script passes without errors: `node .cursor/hooks/lib/n8n-checker.js ` +- [ ] No hardcoded secrets + +## Documentation +- [ ] README updated (if necessary) +- [ ] docs/n8n-manual-update-*.md created (if manual change) + +## Testing +- [ ] Workflow tested in n8n (import and execute) +- [ ] Main flow works +- [ ] Errors handled correctly diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 53ff8b9..1ca2b35 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,8 +1,11 @@ name: CI on: + # Branch pushes are covered by pull_request; listing "**" here as well ran the + # whole build twice for every PR push. Keep the direct pushes that have no PR + # of their own: the two long-lived branches and release tags. push: - branches: ["**"] + branches: [main, staging] tags: ["v*"] pull_request: workflow_dispatch: @@ -12,6 +15,11 @@ on: type: boolean default: false +# Superseded PR runs are cancelled; main, staging and tag runs always finish. +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + jobs: build: runs-on: ubuntu-latest @@ -61,6 +69,12 @@ jobs: - name: Test run: pnpm test + # Build before the guard steps: test:root-node includes the session-start + # hook diagnostic, which asserts the hook resolves the CLI via + # packages/cli/dist instead of falling back to degraded mode. + - name: Build + run: pnpm build + # Private-origin allowlist (SoT: agent-kit-startup/agent-kit-dev). # Fail-safe: forks / renames / second mirrors skip private-only steps. # Adding another allowlisted step requires updating the exact-count pin in @@ -85,6 +99,9 @@ jobs: pnpm evidence:codebase-findings:check pnpm evidence:risk-hotspots:check pnpm evidence:knowledge-classification:check + # Guards that shipped with tests but no runner until the 10-cycle audit. + pnpm check:guidance-claims + pnpm check:memory-index pnpm landing:build pnpm landing:build:check # Test files for root node --test suites. Scan scripts already run @@ -101,8 +118,6 @@ jobs: exit 1 fi - - name: Build - run: pnpm build # ── Sync to public mirror (tag or manual trigger) ── # Secret PUBLIC_REPO_TOKEN: fine-grained token with public repo Contents, diff --git a/CHANGELOG.md b/CHANGELOG.md index bd2995f..735905d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,30 @@ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and ## [Unreleased] +## [5.4.0] - 2026-08-15 + +### Changed + +- CI no longer runs the entire build twice per pull-request push. `push: branches ["**"]` overlapped `pull_request` completely; pushes now trigger on `main`, `staging` and `v*` tags, where there is no PR to cover them. Superseded **PR** runs are cancelled by a concurrency group; `main`, `staging` and tag runs always finish. Both properties are pinned by tests. +- CI builds before its guard steps. `test:root-node` includes the session-start hook diagnostic, which asserts the hook resolves the CLI through `packages/cli/dist` instead of reporting degraded mode; with `Build` ordered last, that suite could only pass on a machine that had already built. A pin test now holds the order. +- Ten root test suites that no command ran are now in `test:root-node`, which CI executes: the plan-external-review atomic-wait / backend-cascade / model-routing suites, `comms-draft`, `check-guidance-stale-claims`, `validate-memory-index`, `hook-session-start-diagnostic`, `orchestration-defects-regression`, `sync-landing` and `sync-public-guards`. 46 root tests become 87. The two guards that had tests but no runner - `check:guidance-claims` and `check:memory-index` - are npm scripts now and run inside CI's Evidence checks. +- `docs/DEVELOPMENT.md` gains a repository-layout table for the root artifacts that read as leftovers but are not: `cursor-handoff`, the install contracts, the registry authoring docs, `skills-registry.json`, `HANDOFF.md.example`, `autogit/`, `_legacy/`, `dogfood/`. Each is path-stable for a reason (public-sync manifest entries, raw-URL install links, inbound references), which is why they stay at the root. +- `dashboard/README.md` documents the Mission Control runtime layout and, explicitly, why its tests live in `packages/cli/src/dashboard/` - the CLI package owns the workspace's only test runner and imports the `.mjs` files directly, so there is one implementation under test rather than a copy. +- `docs/evidence/README.md` states, per artifact, which generator produces it, whether it reproduces off the generating machine, and whether CI enforces it. `evidence:file-ledger:check` is documented as a local replay tool rather than a gate - its census covers the working tree, stashes and ignored-operational files, so it cannot pass on another checkout - and its failure message now says so instead of reading like a stale artifact. +- New `check:markdown-links` guard (`scripts/check-markdown-links.mjs`, wired into `test:root-node`, so CI's Evidence checks enforce it): every relative markdown link in the tracked surface must resolve, with template placeholders (`{plan-slug}`) explicitly excluded. 234 files scanned. +- `pnpm lint` now covers the half of the repository Biome never saw: `dashboard/**`, `scripts/**` and `.cursor/scripts/**` are checked before `turbo run lint` fans out to the workspace packages. Previously the only `lint` task in the graph was `packages/cli`'s `biome check src`, so 56 root-level files - including the Mission Control runtime and the evidence generators - were exempt from the same gate that CI and `/git-staging` present as repo-wide. + +### Fixed + +- `landing:vendor` reports the real failure. The React/ReactDOM download had no status check and no timeout, so a CDN 404 or 500 was hashed as if it were the library and surfaced as a confusing "SRI mismatch"; a hung connection had no bound at all. It now fails with the HTTP status and aborts after 30s. +- `sync-public` no longer shells out to `sleep(1)` between poll attempts - a POSIX-only dependency in the publication path. The synchronous wait uses `Atomics.wait` on a zeroed buffer instead. +- De-flake the dashboard auth-exemption test. Its readiness loop slept only when `fetch` threw, so a non-ready status spun the event loop the spawned `serve.mjs` needed to finish booting, and an accepted-but-unanswered connection could consume the entire budget because no attempt was bounded. Each attempt now carries a 2s `AbortSignal.timeout`, every iteration is spaced, an early child exit fails immediately with its stderr instead of waiting for the test timeout, and the budget is 20s inside a 60s test. +- `agent-kit contribute` carries a skill's companion files upstream. `buildRegistryPathMap` mapped only `SKILL.md`, and the path guess accepted only paths ending in `/SKILL.md`, so a consumer that added a checklist or a `references/` file to a skill contributed the entry point and silently dropped the rest. Both now treat a skill as the directory it is; the legacy flat layout still maps to `community`. +- Skill companion files reach the consumer. `agent-kit add`, pack install, L0 sync and `agent-kit diff` enumerated `SKILL.md` and nothing else, so a skill that ships a checklist or a `references/` folder arrived with dead links in every installed tree. Install and diff now share one `skillFileTargets()` enumerator (recursive, `SKILL.md` first, hidden files skipped, and a missing directory still yields the old single pair), so the whole skill lands. +- Derived guidance no longer pins a superseded release. `.cursor/project-context.md` recorded product version 5.2.1 and lane SHAs frozen at 2026-07-31 while the tree shipped 5.3.0; comms surfaces (`docs/comms*.md`, the `mission-kit-comms` skill and agent), `docs/cursor-update-awareness.md` and `install.md` restated 5.0.0 / 5.2.x as the current release. Where the number was load-bearing it is now dated and paired with the surface to read it from (`CHANGELOG.md`, npm `dist-tags`, `.cursor-plugin/plugin.json`); `docs/npm-publish-checklist.md` drops its 5.0.0-era "today" snapshots for the commands that answer the question at run time. +- Dead relative links across the tracked markdown surface: `.cursor/agents/**` pointed at `.cursor/rules/*.mdc`, `autogit/gitupdate.md` and `README.md` as if it were reading from the repository root, `docs/capability-inventory.md` prefixed its own siblings with `docs/`, and `autogit/plan-routine.md` did the same. References to private trees (`.cursor/memory/**`) are now inline paths rather than links, since those paths never reach the public sync. `.cursor/skills/community/n8n-workflows/` regained the `checklist-n8n.md` its `SKILL.md` links to. +- Clear the 16 Biome violations that scope gap had been hiding: formatting in `dashboard/lib/triage-heading.mjs`, `scripts/validate-memory-index*.mjs`, `scripts/git-hooks-pre-commit-composed.test.mjs`, `.cursor/scripts/comms-draft.mjs` and `.cursor/scripts/plan-external-review-progress-gate.test.mjs`; `resolveSnapshotRepoRoot` no longer declares a defaulted parameter before a required one (the `undefined` env still falls back to `process.env`, matching `guards.d.mts`); `indexLinkTargets` uses `matchAll` instead of an assignment-in-condition loop; the session-start hook diagnostic builds its env by destructuring instead of `delete`. + ## [5.3.0] - 2026-08-15 ### Added @@ -800,7 +824,7 @@ Follows 4.5.1. Version 4.6.0 was withdrawn after release because it carried an u - Mission Control terminal list shows capped `lastOutput` previews from the snapshot payload - Mission Control Processes section: Copy PID CTA per process row (no kill/restart) - Mission Control Git section: bounded dirty `files[]` from `git status --short` (paths only), status badges, and copy-to-clipboard staged/unstaged `git diff` commands per file -- Error memory entry: public sync PR merge-blocked by ruleset and merge-commit method ([public-sync-pr-merge-blocked-ruleset](errors/2026-07-24_public-sync-pr-merge-blocked-ruleset.md)) +- Error memory entry: public sync PR merge-blocked by ruleset and merge-commit method (`.cursor/memory/errors/2026-07-24_public-sync-pr-merge-blocked-ruleset.md`, private) - Decision record: Mission Control local-only security posture (`.cursor/memory/decisions/2026-07-24_mission-control-local-only-security.md`) - Repository personalization profile (`.cursor/context/personalization.json`, `.cursor/project-context.md`, `AGENTS.md`) with matching manifest packs and protected paths in `.cursor/agent-kit.json` - `docs-repo` core skill and `cursor-skills-node` community skill diff --git a/autogit/plan-routine.md b/autogit/plan-routine.md index 5d5b939..09c0ba8 100644 --- a/autogit/plan-routine.md +++ b/autogit/plan-routine.md @@ -166,7 +166,7 @@ When creating plans (`/start-project` or planner): prefer template; fill budget ## 7. Update HANDOFF at end of each phase -According to [cursor-plan-handoff.mdc](.cursor/rules/cursor-plan-handoff.mdc): +According to [cursor-plan-handoff.mdc](../.cursor/rules/cursor-plan-handoff.mdc): - Record completed phase, completed to-dos, next phase. - Include instruction for the next agent. @@ -274,9 +274,9 @@ When Task dispatch is unavailable (no subagent support in the current session), ## References - [`.cursor/context/templates/plan.md`](../.cursor/context/templates/plan.md) — canonical template with budget -- [cursor-plan-handoff.mdc](.cursor/rules/cursor-plan-handoff.mdc) -- [cursor-skills-git-workflow.mdc](.cursor/rules/cursor-skills-git-workflow.mdc) -- [autogit/gitupdate.md](autogit/gitupdate.md) (`git staging`, `git prod`) +- [cursor-plan-handoff.mdc](../.cursor/rules/cursor-plan-handoff.mdc) +- [cursor-skills-git-workflow.mdc](../.cursor/rules/cursor-skills-git-workflow.mdc) +- [autogit/gitupdate.md](gitupdate.md) (`git staging`, `git prod`) - [`.cursor/commands/run-plan.md`](../.cursor/commands/run-plan.md) (continuous; `/run-plan-loop` and `/run-plan-orchestrated` are deprecated aliases) - [`.cursor/commands/hotfix.md`](../.cursor/commands/hotfix.md) (mini plan + continuous tick contract) - [`.cursor/commands/run-plan-all.md`](../.cursor/commands/run-plan-all.md) (multi-plan queue); operator path in [docs/getting-started.md](../docs/getting-started.md) diff --git a/dashboard/README.md b/dashboard/README.md new file mode 100644 index 0000000..81838f1 --- /dev/null +++ b/dashboard/README.md @@ -0,0 +1,25 @@ +# Mission Control runtime + +The dashboard the CLI serves. Plain ES modules, no build step: `serve.mjs` reads this directory +directly, and `packages/cli/dashboard/` is a generated copy (gitignored, produced by +`scripts/sync-cli-dashboard.mjs` at build/prepack time so the npm tarball can ship it). + +| Path | Role | +| --- | --- | +| `serve.mjs` | HTTP server, auth gate, SSE | +| `start.mjs` / `start-broadcast.mjs` | loopback and LAN entry points, per-workspace port allocation | +| `dashboard-data.mjs` | snapshot builder for the panel | +| `dashboard.html`, `open.html` | panel and share shell | +| `lib/*.mjs` | guards, semantic model, live refresh, browser open, terminal snapshot | +| `lib/guards.d.mts` | hand-written types consumed by the CLI package (parity is pinned by a test) | + +## Where the tests live + +Tests for these modules are in `packages/cli/src/dashboard/*.test.ts`, not next to the source. The +CLI package owns the only test runner in the workspace (vitest), and those suites import the `.mjs` +files directly (`../../../../dashboard/lib/...`) so there is one implementation under test rather +than a copy. Lint and format are covered from the repository root (`pnpm lint` checks `dashboard/**` +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. diff --git a/dashboard/lib/guards.mjs b/dashboard/lib/guards.mjs index 6a11253..22a225b 100644 --- a/dashboard/lib/guards.mjs +++ b/dashboard/lib/guards.mjs @@ -47,12 +47,12 @@ export function escapePerlDoubleQuoted(value) { /** * Resolve the repository root Mission Control should snapshot. - * @param {NodeJS.ProcessEnv | Record} [env] + * @param {NodeJS.ProcessEnv | Record | undefined} env - `undefined` falls back to `process.env` * @param {string} kitRoot - absolute path to the kit tree (parent of `dashboard/`) * @returns {string} absolute snapshot root */ -export function resolveSnapshotRepoRoot(env = process.env, kitRoot) { - const raw = env?.[REPO_ROOT_ENV]; +export function resolveSnapshotRepoRoot(env, kitRoot) { + const raw = (env ?? process.env)?.[REPO_ROOT_ENV]; if (typeof raw === "string" && raw.trim()) { return resolve(raw.trim()); } @@ -400,7 +400,7 @@ export function tokensMatch(a, b) { /** * Resolve bind + token gate for Mission Control serve. * Non-loopback bind requires a valid MISSION_CONTROL_TOKEN (no warn-only 0.0.0.0). - * @param {NodeJS.ProcessEnv | Record} [env] + * @param {NodeJS.ProcessEnv | Record | undefined} env - `undefined` falls back to `process.env` * @returns * | { ok: true, host: string, tokenRequired: boolean, token: string | null, broadcast: boolean } * | { ok: false, error: string } diff --git a/dashboard/lib/triage-heading.mjs b/dashboard/lib/triage-heading.mjs index 71f58c9..99d421d 100644 --- a/dashboard/lib/triage-heading.mjs +++ b/dashboard/lib/triage-heading.mjs @@ -11,8 +11,7 @@ * ## Follow-up plan (also "Followup plan") * ## Residuals plan */ -export const TRIAGE_HEADING_RE = - /^#{2,6}\s+(?:Triage note|Follow-?up plan|Residuals plan)\b/im; +export const TRIAGE_HEADING_RE = /^#{2,6}\s+(?:Triage note|Follow-?up plan|Residuals plan)\b/im; /** True when markdown carries a durable triage heading. */ export function hasTriageHeading(text) { diff --git a/docs/DEVELOPMENT.md b/docs/DEVELOPMENT.md index 2e755a9..f6364c5 100644 --- a/docs/DEVELOPMENT.md +++ b/docs/DEVELOPMENT.md @@ -15,6 +15,24 @@ Projects that install Agent Kit receive only `.cursor/` + `autogit/` + the manif **Three layers:** local scratch (HANDOFF/plans, gitignored) · private Git (factory) · public (storefront + registry SoT). Cheat sheet: [repository-boundaries.md](repository-boundaries.md#cheat-sheet-three-layers). Topology phases: [topology-private-public.md](topology-private-public.md). +## Repository layout + +Root artifacts that are not self-explanatory, and why they are at the root rather than tucked away: + +| Path | What it is | +| --- | --- | +| `cursor-handoff` | POSIX shell CLI for the file-based context memory system (`new`, `update`, `status`, `handoff`, `archive`, `resume`, `list-projects`). Extensionless and at the root because consumers invoke it as `./cursor-handoff` and the public sync manifest ships it by that exact path. | +| `install.md`, `install-prompt.md` | The Port B install contract an agent fetches by raw URL, and the prompt that drives it. Path-stable by design: `README.md` and external instructions link them. | +| `add-skills.md`, `categories.md`, `registry-schema.md` | Registry authoring contracts referenced from `docs/creating-skills.md` and the registry itself. | +| `skills-registry.json` | Legacy flat skill index kept for compatibility; `registry/registry.json` is the generated source of truth (`pnpm registry:build`). | +| `HANDOFF.md.example` | Template a consumer copies to `.cursor/HANDOFF.md` (the real one is gitignored). | +| `autogit/` | The git staging / production routines the kit commands cite as source of truth. | +| `_legacy/` | Frozen v2 tree. Historical reference only - never edit, and do not treat its copies of the root files as current. | +| `dogfood/` | Field reports and the ingest ritual (factory intake), not product code. | + +`.cursor/` is the kit itself (commands, rules, skills, agents, plans, memory); `packages/cli` is the +published CLI; `dashboard/` is the Mission Control runtime; `registry/` is the skill/pack catalog. + ## Local monorepo setup ```bash @@ -81,6 +99,12 @@ npm run start:dashboard # foreground serve only Published CLI packs `dashboard/**` from 4.8.2 onward; consumers normally run `agent-kit dashboard` without a kit checkout. +### Where Mission Control's tests live + +`dashboard/**` has no test runner of its own: the suites for those modules are +`packages/cli/src/dashboard/*.test.ts`, importing the `.mjs` files directly so there is one +implementation under test rather than a copy. `dashboard/README.md` documents the layout. + ## Public sync awareness - Allowlist: `scripts/public-sync.manifest` (positive globs + exclusions). @@ -108,6 +132,19 @@ Public marketing uses **Mission Kit** / **MissionKit** (missionkit.io hero, READ Storefront README must not link private memory ADRs or dump dual-name legal essays. Private decision record (factory only): `2026-08-06_mission-kit-vs-agent-kit-naming.md` under `.cursor/memory/decisions/`. Public vs maintainer README split: `2026-08-02_public-vs-dev-readme-separation.md` in the same folder. +## Evidence artifacts + +`docs/evidence/**` is private (excluded from the public sync manifest) and holds generated ledgers +plus hand-authored audit records. Which artifact is machine-reproducible, and which one CI actually +enforces, is not uniform: the matrix lives in `docs/evidence/README.md`. Two rules worth knowing +before you touch them: + +- `evidence:file-ledger:check` is a **local replay** tool, not a CI gate - the census includes the + working tree, stashes and ignored-operational files, so it only reproduces on the machine that + generated it. +- `evidence:knowledge-classification` refuses a dirty tree, so a `.cursor/**` change ships as two + commits: the change, then the regenerated artifact. + ## Related docs - [CONTRIBUTING.md](CONTRIBUTING.md) - setup, standards, registry contributions diff --git a/docs/agentkit-landing.md b/docs/agentkit-landing.md index 98bf1b7..d3bdab5 100644 --- a/docs/agentkit-landing.md +++ b/docs/agentkit-landing.md @@ -14,7 +14,7 @@ design tool renders. **Design mirror:** `.cursor/context/landing-missionkit/remote/` (versioned) **Build output:** `.cursor/context/landing-missionkit/dist/` (gitignored, derived) -**Decision:** [landing-external-design-source-of-record](../memory/decisions/2026-08-05_landing-external-design-source-of-record.md) +**Decision:** `.cursor/memory/decisions/2026-08-05_landing-external-design-source-of-record.md` (private) **Design system:** [docs/design-system.md](design-system.md) (project id, upstream/downstream map, token divergence notes) ### Constraints diff --git a/docs/capability-inventory.md b/docs/capability-inventory.md index d12df45..9988fc0 100644 --- a/docs/capability-inventory.md +++ b/docs/capability-inventory.md @@ -339,14 +339,14 @@ Enumerated identity literals and publication routes from `scripts/public-sync.ma | `README.md` | L35 | ### Production-agent layers (L0) | allowlist-synced (`README.md` in `scripts/public-sync.manifest`) | | `README.md` | L95 | Mission Control is a local panel over Agent Kit runtime state. It binds to loopback by default and serves only its own static files. It is … | allowlist-synced (`README.md` in `scripts/public-sync.manifest`) | | `README.md` | L109 | Primary sections: Current mission, Flight Log, Checklist, Crew Monitor. More menu: Plans, Activity, Agents, Skills, Commands, Health, Git, Memory, Terminals, Processes, Config… | allowlist-synced (`README.md` in `scripts/public-sync.manifest`) | -| `README.md` | L119 | [Repository readiness](docs/repository-readiness-onboarding.md) — Install discovery, `/agent-kit-onboard`, and deliverable boundary | allowlist-synced (`README.md` in `scripts/public-sync.manifest`) | -| `README.md` | L120 | [Bootstrap](docs/bootstrap.md) — Exactly what lands in your project, and why there's no nested folder | allowlist-synced (`README.md` in `scripts/public-sync.manifest`) | -| `README.md` | L121 | [Layers](docs/layers-spec.md) — How the base install, optional packs, and your local files layer together | allowlist-synced (`README.md` in `scripts/public-sync.manifest`) | -| `README.md` | L122 | [Domain packs](docs/domain-packs.md) — Optional bundles: clean code, DevOps, testing, and more | allowlist-synced (`README.md` in `scripts/public-sync.manifest`) | -| `README.md` | L123 | [Agent Personas](docs/personas-contract.md) — Mode defaults, `agentPersona` config, hygiene boundary ([create / contribute](docs/creating-p… | allowlist-synced (`README.md` in `scripts/public-sync.manifest`) | -| `README.md` | L125 | [Manifest](docs/agent-kit-manifest.md) — The `.cursor/agent-kit.json` file | allowlist-synced (`README.md` in `scripts/public-sync.manifest`) | -| `README.md` | L126 | [Contributing](docs/CONTRIBUTING.md) — Working on the kit itself (includes contributor quickstart) | allowlist-synced (`README.md`) | -| `README.md` | L127 | [Development](docs/DEVELOPMENT.md) — Factory topology, local CLI, and maintainer workflows | allowlist-synced (`README.md`) | +| `README.md` | L119 | [Repository readiness](repository-readiness-onboarding.md) — Install discovery, `/agent-kit-onboard`, and deliverable boundary | allowlist-synced (`README.md` in `scripts/public-sync.manifest`) | +| `README.md` | L120 | [Bootstrap](bootstrap.md) — Exactly what lands in your project, and why there's no nested folder | allowlist-synced (`README.md` in `scripts/public-sync.manifest`) | +| `README.md` | L121 | [Layers](layers-spec.md) — How the base install, optional packs, and your local files layer together | allowlist-synced (`README.md` in `scripts/public-sync.manifest`) | +| `README.md` | L122 | [Domain packs](domain-packs.md) — Optional bundles: clean code, DevOps, testing, and more | allowlist-synced (`README.md` in `scripts/public-sync.manifest`) | +| `README.md` | L123 | [Agent Personas](personas-contract.md) — Mode defaults, `agentPersona` config, hygiene boundary ([create / contribute](creating-personas.md)) | allowlist-synced (`README.md` in `scripts/public-sync.manifest`) | +| `README.md` | L125 | [Manifest](agent-kit-manifest.md) — The `.cursor/agent-kit.json` file | allowlist-synced (`README.md` in `scripts/public-sync.manifest`) | +| `README.md` | L126 | [Contributing](CONTRIBUTING.md) — Working on the kit itself (includes contributor quickstart) | allowlist-synced (`README.md`) | +| `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) | diff --git a/docs/comms-channel-map.md b/docs/comms-channel-map.md index 2f881d9..4cb004e 100644 --- a/docs/comms-channel-map.md +++ b/docs/comms-channel-map.md @@ -18,7 +18,7 @@ Do not market unchecked full autonomy. Staging may be automatic; production (`/g | Channel | Priority | Effort | Disposition | Policy | HITL depth | |---------|----------|--------|-------------|--------|------------| -| missionkit.io / blog-equivalent | High | Med | **include** | Product voice; dual-name; claims match 5.0.0 shipped docs | Ask before any public publish | +| missionkit.io / blog-equivalent | High | Med | **include** | Product voice; dual-name; claims match the current release's shipped docs | Ask before any public publish | | Copy-paste social (X / Twitter) | High | Low | **include** | Short; no autonomy-first slogans; link site + public GitHub | Ask before post or reply | | Medium | Med | Med | **include** | Same claims as [public-launch-announcement.md](public-launch-announcement.md) and getting-started | Ask before publish | | Newsletter / Substack | Med | High | **include** | Digest of recaps and releases only; no unshipped features | Ask before send | @@ -37,7 +37,7 @@ Bind every cycle to artifacts that already exist: | Cycle | Trigger (must be real) | Output | Channels | |-------|------------------------|--------|----------| -| Recap | Merged CHANGELOG `[Unreleased]` items that have reached `staging`, or a dated recap of what already shipped in 5.0.0 | [recap template](comms-templates/recap.md) | X (short), Medium/newsletter (optional) | +| Recap | Merged CHANGELOG `[Unreleased]` items that have reached `staging`, or a dated recap of what already shipped in the current release | [recap template](comms-templates/recap.md) | X (short), Medium/newsletter (optional) | | Release | GitHub Release / closed CHANGELOG version / missionkit.io already showing that version | [release template](comms-templates/release.md) | Site, X, HN (optional), Medium | | Contributor-ask | Open issues or registry contribution paths that are actually ready | [contributor-ask template](comms-templates/contributor-ask.md) | GitHub, X, launch paste follow-up | diff --git a/docs/comms-content-calendar.md b/docs/comms-content-calendar.md index 92bab5f..7574201 100644 --- a/docs/comms-content-calendar.md +++ b/docs/comms-content-calendar.md @@ -2,7 +2,7 @@ Living schedule for Mission Kit recap, release, and contributor-ask drafts. Seed copy: [public-launch-announcement.md](public-launch-announcement.md) (keep that file; evolve here). -**SoT for "is it shipped?":** `CHANGELOG.md` closed versions, GitHub Releases on the public repo, and live [missionkit.io](https://missionkit.io). Product version for current claims: **5.0.0**. +**SoT for "is it shipped?":** `CHANGELOG.md` closed versions, GitHub Releases on the public repo, and live [missionkit.io](https://missionkit.io). Product version for current claims: newest closed `CHANGELOG.md` version, **5.3.0** as of 2026-08-15. ## How to add a row @@ -31,7 +31,7 @@ Do not schedule Cursor Marketplace submit here. That work is parked. ## Claim check (every draft) - [ ] Mission Kit = product; Agent Kit = CLI/npm/slash/pack; Mission Control = dashboard -- [ ] Version and features match 5.0.0 shipped docs (getting-started, README, five-layer matrix) +- [ ] Version and features match the shipped docs for the current release (getting-started, README, five-layer matrix) - [ ] HITL / staging→prod confirmation is stated; no "posts itself" or "full autonomy" - [ ] PolyForm Noncommercial + `sales@missionkit.io` for commercial use - [ ] Public GitHub: `https://github.com/agent-kit-startup/agent-kit` diff --git a/docs/comms.md b/docs/comms.md index ab4726b..623adb7 100644 --- a/docs/comms.md +++ b/docs/comms.md @@ -2,7 +2,7 @@ Repeatable **draft then HITL approve then publish** loop for Mission Kit adoption and contributor asks. Nothing in this tree posts to a public network by itself. -**Product version in copy:** 5.0.0 (npm `@dadado/agent-kit-cli`, site [missionkit.io](https://missionkit.io)). Docs are indicative; claims must match shipped surfaces. +**Product version in copy:** newest closed `CHANGELOG.md` version that is live on npm `@dadado/agent-kit-cli` and [missionkit.io](https://missionkit.io) - **5.3.0** as of 2026-08-15. Read it from those surfaces, not from this line. Docs are indicative; claims must match shipped surfaces. | Piece | Path | |-------|------| diff --git a/docs/cursor-update-awareness.md b/docs/cursor-update-awareness.md index 654fc2a..4970392 100644 --- a/docs/cursor-update-awareness.md +++ b/docs/cursor-update-awareness.md @@ -11,7 +11,7 @@ Opt-in advisory for Cursor product updates (releases, changelog entries, new MCP | No Field Reports | `fieldReportRecommended` is always `false` | | Conveyor | Confirmed gaps → Ask → `/backlog-add` or `/dogfood` (lane-aware) | | Separate from kit update | Kit self-release uses `updateCheck` / `/update` (ADR `2026-07-27_consumer-autoupdate-check-opt-in.md`) | -| Native-audit prose | Native-audit plugin version is **5.2.0**. Live Marketplace submission stays on `submit-cursor-marketplace` (publisher HITL) | +| Native-audit prose | Native-audit plugin version tracks `.cursor-plugin/plugin.json` (**5.3.0** as of 2026-08-15). Live Marketplace submission stays on `submit-cursor-marketplace` (publisher HITL) | ## Detection source diff --git a/docs/npm-publish-checklist.md b/docs/npm-publish-checklist.md index c7bd463..2ec1207 100644 --- a/docs/npm-publish-checklist.md +++ b/docs/npm-publish-checklist.md @@ -5,7 +5,7 @@ Human-in-the-loop gate before the first npm publish or any publish that changes ## Preconditions - [ ] Release content is on `origin/main` (promote via `git prod` from `origin/staging` when the release is not already on `main`). -- [ ] Root `package.json` `version`, `packages/cli/package.json` `version`, and the dated section in `CHANGELOG.md` match the intended release. **Tree today:** root + CLI + `CHANGELOG` `[5.0.0]` are aligned at **5.0.0**. **Registry today:** `npm view @dadado/agent-kit-cli version` is still **4.8.9** until `v5.0.0` publishes. Do not treat tree SemVer as live npm `latest`. +- [ ] Root `package.json` `version`, `packages/cli/package.json` `version`, and the dated section in `CHANGELOG.md` match the intended release. Read both from the tree (`node -p "require('./package.json').version"`) and from the registry (`npm view @dadado/agent-kit-cli version`) at the moment you run this checklist; they diverge by design until the tag publishes. Do not treat tree SemVer as live npm `latest`, and do not trust a version literal written into this file. - [ ] `pnpm lint`, `pnpm typecheck`, `pnpm test`, and `pnpm build` succeed on the **exact commit you intend to tag** (CI-green-at-tagged-SHA; see `.cursor/memory/decisions/2026-08-02_npm-5.0-go-no-go.md`). A later green SHA does not repair a red tagged commit. - [ ] Public sync and other post-`git prod` steps are done or explicitly deferred per [repository-boundaries.md](repository-boundaries.md). @@ -43,7 +43,7 @@ The `publish-npm` job in `.github/workflows/ci.yml` runs on **`v*` tags** only. ## Tag strategy and CI - [ ] Tags use the form **`vMAJOR.MINOR.PATCH`** (e.g. `v4.8.9`). -- [ ] **Existing tags on `origin`:** `v3.0.0` through `v4.8.9` (35 tags; full series: v3.0.0, v3.5.0, v3.5.1, v4.0.0, v4.0.1, v4.1.0, v4.2.0–v4.2.4, v4.3.0, v4.4.0–v4.4.7, v4.5.0–v4.5.1, v4.6.0, v4.7.0–v4.7.2, v4.8.0–v4.8.9). Next: `v5.0.0`. Pushing a **new** `v*` tag (or re-running CI for a tag) triggers `publish-npm` after `build` succeeds. +- [ ] **Existing tags on `origin`:** list them with `git tag --sort=-creatordate | head` (the series runs from `v3.0.0`; `v5.3.0` is the newest as of 2026-08-15). Pushing a **new** `v*` tag (or re-running CI for a tag) triggers `publish-npm` after `build` succeeds. - [ ] Tag the commit on `main` that matches the release version; do not tag staging-only commits unless that is an explicit exception documented in the release notes. - [ ] **Integrated with `/git-prod`**: Annotated tags are created automatically when absent during `/git-prod` workflow (step 9.5 in `autogit/gitupdate.md`). Manual tag creation via `git push origin vX.Y.Z` or GitHub Releases UI is fallback only. diff --git a/install.md b/install.md index 5ba5aa0..2e97b2f 100644 --- a/install.md +++ b/install.md @@ -154,7 +154,7 @@ Create if it doesn't exist (adjust `version` / `registry` to current SoT): ```json { "schemaVersion": 1, - "version": "5.2.1", + "version": "5.4.0", "profile": "default", "packs": [], "skills": [], diff --git a/package.json b/package.json index 491ad4c..d711563 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "agent-kit", - "version": "5.3.0", + "version": "5.4.0", "description": "HITL framework for AI-assisted IDEs: plan, handoff, staging-to-prod, memory loop; project-aware setup for Cursor, VS Code, and Windsurf.", "private": true, "license": "PolyForm-Noncommercial-1.0.0", @@ -29,10 +29,13 @@ "evidence:risk-hotspots": "node scripts/score-codebase-risk-surface.mjs --write", "evidence:risk-hotspots:check": "node --test scripts/score-codebase-risk-surface.test.mjs", "check:public-deny-links": "node scripts/check-public-deny-links.mjs", + "check:markdown-links": "node scripts/check-markdown-links.mjs", + "check:guidance-claims": "node scripts/check-guidance-stale-claims.mjs", + "check:memory-index": "node scripts/validate-memory-index.mjs", "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 scripts/check-public-deny-links.test.mjs scripts/verify-cli-dashboard-pack.test.mjs scripts/git-hooks-pre-commit-composed.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", "landing:sync": "node scripts/sync-landing.mjs", "landing:vendor": "node scripts/sync-landing.mjs", "landing:build": "node scripts/build-landing.mjs", @@ -40,7 +43,7 @@ "landing:serve": "node scripts/serve-landing.mjs", "build": "turbo run build", "dev": "turbo run dev", - "lint": "turbo run lint", + "lint": "biome check dashboard scripts .cursor/scripts && turbo run lint", "test": "turbo run test", "typecheck": "turbo run typecheck" }, diff --git a/packages/cli/package.json b/packages/cli/package.json index d5b8a4c..c5647e6 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@dadado/agent-kit-cli", - "version": "5.3.0", + "version": "5.4.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/dashboard/ci-private-origin-allowlist.test.ts b/packages/cli/src/dashboard/ci-private-origin-allowlist.test.ts index c4b98ec..2c894f5 100644 --- a/packages/cli/src/dashboard/ci-private-origin-allowlist.test.ts +++ b/packages/cli/src/dashboard/ci-private-origin-allowlist.test.ts @@ -71,4 +71,23 @@ describe("ci.yml private-origin allowlist pin", () => { expect(rootNode).toContain("check-public-deny-links.test.mjs"); expect(rootNode).toContain("verify-cli-dashboard-pack.test.mjs"); }); + + it.skipIf(!ciPresent)("does not run the whole build twice per pull-request push", () => { + // push: branches ["**"] plus pull_request duplicated every PR run. + const onIdx = body.indexOf("on:"); + const jobsIdx = body.indexOf("concurrency:"); + const triggers = body.slice(onIdx, jobsIdx); + expect(triggers).toContain("pull_request:"); + expect(triggers).not.toContain('branches: ["**"]'); + }); + + it.skipIf(!ciPresent)("builds before the guard steps that need packages/cli/dist", () => { + // hook-session-start-diagnostic asserts the session-start hook resolves the + // CLI through packages/cli/dist rather than reporting degraded mode, so a + // Build step ordered after Evidence checks fails it on a clean runner. + const buildIdx = body.indexOf("- name: Build"); + const evidenceIdx = body.indexOf("- name: Evidence checks"); + expect(buildIdx).toBeGreaterThan(-1); + expect(evidenceIdx).toBeGreaterThan(buildIdx); + }); }); diff --git a/packages/cli/src/dashboard/guards.test.ts b/packages/cli/src/dashboard/guards.test.ts index 7d5f694..85da095 100644 --- a/packages/cli/src/dashboard/guards.test.ts +++ b/packages/cli/src/dashboard/guards.test.ts @@ -695,20 +695,36 @@ describe("serve.mjs HTTP auth exemption for share shell", () => { stdio: ["ignore", "pipe", "pipe"], }); - const deadline = Date.now() + 15_000; + // Diagnose an early exit instead of spinning until the test times out. + let stderr = ""; + child.stderr?.on("data", (chunk) => { + stderr += String(chunk); + }); + let exited: number | null = null; + child.on("exit", (code) => { + exited = code ?? -1; + }); + + // Every attempt is bounded and spaced: an accepted-but-unanswered + // connection must not consume the whole readiness budget, and a non-ready + // status must not spin the event loop the child needs to finish booting. + const deadline = Date.now() + 20_000; let ready = false; - while (Date.now() < deadline) { + while (Date.now() < deadline && exited === null) { try { - const res = await fetch(`http://127.0.0.1:${port}/open.html`); + const res = await fetch(`http://127.0.0.1:${port}/open.html`, { + signal: AbortSignal.timeout(2_000), + }); if (res.status === 200 || res.status === 401) { ready = true; break; } } catch { - await new Promise((r) => setTimeout(r, 100)); + // not listening yet, or the attempt timed out } + if (!ready) await new Promise((r) => setTimeout(r, 100)); } - expect(ready).toBe(true); + expect(ready, `serve.mjs never answered on :${port} (exit=${exited}) ${stderr}`).toBe(true); try { const openHtml = await fetch(`http://127.0.0.1:${port}/open.html`); @@ -723,7 +739,7 @@ describe("serve.mjs HTTP auth exemption for share shell", () => { child.kill("SIGTERM"); await once(child, "exit").catch(() => undefined); } - }, 30_000); + }, 60_000); }); describe("broadcast multi-instance port allocation", () => { diff --git a/packages/cli/src/lifecycle/contribute-skill-files.test.ts b/packages/cli/src/lifecycle/contribute-skill-files.test.ts new file mode 100644 index 0000000..d07c4e4 --- /dev/null +++ b/packages/cli/src/lifecycle/contribute-skill-files.test.ts @@ -0,0 +1,81 @@ +import { mkdir, mkdtemp, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import path from "node:path"; +import { describe, expect, it } from "vitest"; +import type { AgentKitManifest } from "../manifest/types.js"; +import { buildRegistryPathMap, planContribute } from "./contribute.js"; + +/** Registry with one community skill that ships a companion file. */ +async function fixtureRegistry(): Promise { + const root = await mkdtemp(path.join(tmpdir(), "agent-kit-contribute-registry-")); + const skillDir = path.join(root, "registry/skills/community/demo"); + await mkdir(skillDir, { recursive: true }); + await writeFile(path.join(skillDir, "SKILL.md"), "# Demo\n"); + await writeFile(path.join(skillDir, "checklist.md"), "# Checklist\n"); + await writeFile( + path.join(root, "registry/registry.json"), + JSON.stringify({ + skills: { core: [], community: [{ id: "demo", path: "registry/skills/community/demo" }] }, + }), + ); + return root; +} + +const manifest = { schemaVersion: 1, skills: ["demo"] } as unknown as AgentKitManifest; + +describe("buildRegistryPathMap", () => { + it("maps a skill's companion files, not only SKILL.md", async () => { + const registryRoot = await fixtureRegistry(); + const map = await buildRegistryPathMap(registryRoot, manifest); + expect(map.get(".cursor/skills/community/demo/SKILL.md")).toBe( + "registry/skills/community/demo/SKILL.md", + ); + expect(map.get(".cursor/skills/community/demo/checklist.md")).toBe( + "registry/skills/community/demo/checklist.md", + ); + }); +}); + +describe("planContribute path mapping", () => { + it("maps a brand-new companion file under an installed skill", async () => { + const registryRoot = await fixtureRegistry(); + const project = await mkdtemp(path.join(tmpdir(), "agent-kit-contribute-project-")); + const skillDir = path.join(project, ".cursor/skills/community/demo/references"); + await mkdir(skillDir, { recursive: true }); + await writeFile(path.join(skillDir, "deep.md"), "# Deep reference\n"); + + const plan = await planContribute({ + registryRoot, + projectRoot: project, + manifest, + extraPaths: [".cursor/skills/community/demo/references/deep.md"], + includeDrift: false, + }); + + const candidate = plan.candidates.find( + (c) => c.projectPath === ".cursor/skills/community/demo/references/deep.md", + ); + expect(candidate?.registryPath).toBe("registry/skills/community/demo/references/deep.md"); + expect(candidate?.issues.map((i) => i.code)).not.toContain("unmapped"); + }); + + it("keeps the legacy flat skill layout mapping to community", async () => { + const registryRoot = await fixtureRegistry(); + const project = await mkdtemp(path.join(tmpdir(), "agent-kit-contribute-flat-")); + const skillDir = path.join(project, ".cursor/skills/legacy"); + await mkdir(skillDir, { recursive: true }); + await writeFile(path.join(skillDir, "SKILL.md"), "# Legacy\n"); + + const plan = await planContribute({ + registryRoot, + projectRoot: project, + manifest, + extraPaths: [".cursor/skills/legacy/SKILL.md"], + includeDrift: false, + }); + + expect( + plan.candidates.find((c) => c.projectPath === ".cursor/skills/legacy/SKILL.md")?.registryPath, + ).toBe("registry/skills/community/legacy/SKILL.md"); + }); +}); diff --git a/packages/cli/src/lifecycle/contribute.ts b/packages/cli/src/lifecycle/contribute.ts index 50b4a9a..5d9cedd 100644 --- a/packages/cli/src/lifecycle/contribute.ts +++ b/packages/cli/src/lifecycle/contribute.ts @@ -2,7 +2,7 @@ import { mkdir, readFile, writeFile } from "node:fs/promises"; import path from "node:path"; import type { AgentKitManifest } from "../manifest/types.js"; import { allSkills, loadRegistry } from "../registry/client.js"; -import { loadPackManifest, packMemberTargets } from "../registry/install.js"; +import { loadPackManifest, packMemberTargets, skillFileTargets } from "../registry/install.js"; import { type GateIssue, gateContributeContent, gateContributePath } from "./contribute-gate.js"; import { L0_ARTIFACTS } from "./l0.js"; import { resolveContained } from "./paths.js"; @@ -45,8 +45,13 @@ export async function buildRegistryPathMap( for (const packId of manifest.packs ?? []) { const pack = await loadPackManifest(registryRoot, packId); for (const member of pack.members) { - const { sourceRel, targetRel } = packMemberTargets(member); - map.set(targetRel.split(path.sep).join("/"), sourceRel.split(path.sep).join("/")); + const pairs = + member.kind === "skill" + ? await skillFileTargets(registryRoot, member.source, member.id) + : [packMemberTargets(member)]; + for (const { sourceRel, targetRel } of pairs) { + map.set(targetRel.split(path.sep).join("/"), sourceRel.split(path.sep).join("/")); + } } } @@ -56,10 +61,13 @@ export async function buildRegistryPathMap( for (const id of manifest.skills ?? []) { const skill = pool.find((s) => s.id === id); if (!skill) continue; - const category = skill.path.includes("/core/") ? "core" : "community"; - const sourceRel = path.posix.join(skill.path, "SKILL.md"); - const targetRel = path.posix.join(".cursor", "skills", category, skill.id, "SKILL.md"); - map.set(targetRel, sourceRel); + for (const { sourceRel, targetRel } of await skillFileTargets( + registryRoot, + skill.path, + skill.id, + )) { + map.set(targetRel, sourceRel); + } } } @@ -68,17 +76,19 @@ export async function buildRegistryPathMap( function guessRegistryPath(projectRel: string): string | null { const p = projectRel.split(path.sep).join("/"); - if (p.startsWith(".cursor/skills/") && p.endsWith("/SKILL.md")) { + if (p.startsWith(".cursor/skills/")) { const rest = p.slice(".cursor/skills/".length); const parts = rest.split("/"); - // .cursor/skills///SKILL.md → registry/skills///SKILL.md - if (parts.length === 3 && (parts[0] === "core" || parts[0] === "community")) { + // A skill is a directory, not a single file: companion files (checklists, + // references/) map alongside SKILL.md rather than being dropped. + // .cursor/skills/// → registry/skills/// + if (parts.length >= 3 && (parts[0] === "core" || parts[0] === "community")) { return path.posix.join("registry/skills", rest); } - // Legacy flat: .cursor/skills//SKILL.md → community by default + // Legacy flat: .cursor/skills// → community by default const skillId = parts[0]; - if (parts.length === 2 && skillId) { - return path.posix.join("registry/skills", "community", skillId, "SKILL.md"); + if (parts.length >= 2 && skillId) { + return path.posix.join("registry/skills", "community", rest); } } if (p.startsWith(".cursor/rules/")) { diff --git a/packages/cli/src/lifecycle/diff.ts b/packages/cli/src/lifecycle/diff.ts index 36fcf60..2aa3c10 100644 --- a/packages/cli/src/lifecycle/diff.ts +++ b/packages/cli/src/lifecycle/diff.ts @@ -2,7 +2,7 @@ import { readFile } from "node:fs/promises"; import path from "node:path"; import type { AgentKitManifest } from "../manifest/types.js"; import { allSkills, loadRegistry } from "../registry/client.js"; -import { loadPackManifest, packMemberTargets } from "../registry/install.js"; +import { loadPackManifest, packMemberTargets, skillFileTargets } from "../registry/install.js"; import { L0_ARTIFACTS } from "./l0.js"; import { resolveContained } from "./paths.js"; import { isProtectedPath, resolveProtectedGlobs } from "./protected.js"; @@ -85,8 +85,13 @@ export async function diffAgainstRegistry( for (const packId of manifest.packs ?? []) { const pack = await loadPackManifest(registryRoot, packId); for (const member of pack.members) { - const { sourceRel, targetRel } = packMemberTargets(member); - await pushUnique(sourceRel, targetRel); + const pairs = + member.kind === "skill" + ? await skillFileTargets(registryRoot, member.source, member.id) + : [packMemberTargets(member)]; + for (const { sourceRel, targetRel } of pairs) { + await pushUnique(sourceRel, targetRel); + } } } @@ -99,10 +104,13 @@ export async function diffAgainstRegistry( entries.push({ path: `skill:${id}`, status: "missing-registry" }); continue; } - const category = skill.path.includes("/core/") ? "core" : "community"; - const sourceRel = path.posix.join(skill.path, "SKILL.md"); - const targetRel = path.posix.join(".cursor", "skills", category, skill.id, "SKILL.md"); - await pushUnique(sourceRel, targetRel); + for (const { sourceRel, targetRel } of await skillFileTargets( + registryRoot, + skill.path, + skill.id, + )) { + await pushUnique(sourceRel, targetRel); + } } } diff --git a/packages/cli/src/lifecycle/overlay-known-hashes.ts b/packages/cli/src/lifecycle/overlay-known-hashes.ts index 1420aa2..0903c6c 100644 --- a/packages/cli/src/lifecycle/overlay-known-hashes.ts +++ b/packages/cli/src/lifecycle/overlay-known-hashes.ts @@ -150,4 +150,6 @@ export const KNOWN_SHIPPED_OVERLAY_HASHES: ReadonlySet = new Set([ "61b635ea8a08171062bd329a4615d7426eb787eb796271aa882f55f39db56810", "86afbea8f64de68a79ad5e374f3132bdbe2582b94321fb3d438314838e36c776", "65cc1c0293b145e48ed73ad0ca9ab33cbba5ed834a5bfb31f195ed30c2f143df", + "f04fcfe31354d1b09aeb256a17e4aab91c98ea48a5cff25e4a0281af3cfb289f", + "34e559ad9036d93d9cbc96d394bc2bbeb10ce50ead00d58a914158ae19daa76c", ]); diff --git a/packages/cli/src/registry/install-skill-files.test.ts b/packages/cli/src/registry/install-skill-files.test.ts new file mode 100644 index 0000000..26a9782 --- /dev/null +++ b/packages/cli/src/registry/install-skill-files.test.ts @@ -0,0 +1,91 @@ +import { mkdir, mkdtemp, readFile, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { describe, expect, it } from "vitest"; +import { installSkill, skillFileTargets, skillTargetDir } from "./install.js"; + +const kitRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../../.."); + +/** Registry fixture: one skill with a companion file and a nested reference. */ +async function fixtureRegistry(): Promise { + const root = await mkdtemp(path.join(tmpdir(), "agent-kit-skill-files-")); + const skillDir = path.join(root, "registry/skills/community/demo"); + await mkdir(path.join(skillDir, "references"), { recursive: true }); + await writeFile(path.join(skillDir, "SKILL.md"), "# Demo\n\n[checklist](checklist.md)\n"); + await writeFile(path.join(skillDir, "checklist.md"), "# Checklist\n"); + await writeFile(path.join(skillDir, "references/deep.md"), "# Deep\n"); + await writeFile(path.join(skillDir, ".DS_Store"), "noise"); + return root; +} + +describe("skillTargetDir", () => { + it("routes core and community skills to their category folder", () => { + expect(skillTargetDir("registry/skills/core/clean-code", "clean-code")).toBe( + ".cursor/skills/core/clean-code", + ); + expect(skillTargetDir("registry/skills/community/demo", "demo")).toBe( + ".cursor/skills/community/demo", + ); + }); +}); + +describe("skillFileTargets", () => { + it("lists SKILL.md first, then companions, including nested ones", async () => { + const root = await fixtureRegistry(); + const targets = await skillFileTargets(root, "registry/skills/community/demo", "demo"); + expect(targets.map((t) => t.sourceRel)).toEqual([ + "registry/skills/community/demo/SKILL.md", + "registry/skills/community/demo/checklist.md", + "registry/skills/community/demo/references/deep.md", + ]); + expect(targets.map((t) => t.targetRel)).toEqual([ + ".cursor/skills/community/demo/SKILL.md", + ".cursor/skills/community/demo/checklist.md", + ".cursor/skills/community/demo/references/deep.md", + ]); + }); + + it("falls back to the SKILL.md pair when the source directory is unreadable", async () => { + const root = await fixtureRegistry(); + const targets = await skillFileTargets(root, "registry/skills/community/absent", "absent"); + expect(targets).toEqual([ + { + sourceRel: "registry/skills/community/absent/SKILL.md", + targetRel: ".cursor/skills/community/absent/SKILL.md", + }, + ]); + }); + + it("keeps the real registry skill with a companion file whole", async () => { + const targets = await skillFileTargets( + kitRoot, + "registry/skills/community/n8n-workflows", + "n8n-workflows", + ); + expect(targets.map((t) => t.targetRel)).toContain( + ".cursor/skills/community/n8n-workflows/checklist-n8n.md", + ); + }); +}); + +describe("installSkill", () => { + it("writes companion files into the consumer tree, not just SKILL.md", async () => { + const registryRoot = await fixtureRegistry(); + const project = await mkdtemp(path.join(tmpdir(), "agent-kit-skill-project-")); + + const stats = await installSkill(registryRoot, project, { + id: "demo", + path: "registry/skills/community/demo", + } as Parameters[2]); + + expect(stats.written).toEqual([ + ".cursor/skills/community/demo/SKILL.md", + ".cursor/skills/community/demo/checklist.md", + ".cursor/skills/community/demo/references/deep.md", + ]); + expect( + await readFile(path.join(project, ".cursor/skills/community/demo/checklist.md"), "utf8"), + ).toBe("# Checklist\n"); + }); +}); diff --git a/packages/cli/src/registry/install.ts b/packages/cli/src/registry/install.ts index 441edac..8cde60a 100644 --- a/packages/cli/src/registry/install.ts +++ b/packages/cli/src/registry/install.ts @@ -1,4 +1,4 @@ -import { readFile } from "node:fs/promises"; +import { readFile, readdir } from "node:fs/promises"; import path from "node:path"; import { type ApplyStats, @@ -33,15 +33,61 @@ export interface InstallOptions { protectedGlobs?: readonly string[]; } +/** Where a skill's files land in a consumer tree: `.cursor/skills///`. */ +export function skillTargetDir(skillPath: string, skillId: string): string { + const category = skillPath.includes("/core/") ? "core" : "community"; + return path.posix.join(".cursor", "skills", category, skillId); +} + +/** + * Every file a skill ships, not just `SKILL.md`. + * + * Skills may carry companion files next to their entry point (checklists, + * references, fixtures). Enumerating the source directory keeps those files in + * the install/diff set instead of leaving the links in `SKILL.md` dangling in + * every consumer tree. `SKILL.md` is required and always comes first; hidden + * files are ignored. A missing or unreadable directory yields the `SKILL.md` + * pair alone, so callers behave exactly as before on a registry that has none. + */ +export async function skillFileTargets( + registryRoot: string, + skillPath: string, + skillId: string, +): Promise<{ sourceRel: string; targetRel: string }[]> { + const targetDir = skillTargetDir(skillPath, skillId); + const pair = (rel: string) => ({ + sourceRel: path.posix.join(skillPath, rel), + targetRel: path.posix.join(targetDir, rel), + }); + + let companions: string[] = []; + try { + const dirAbs = resolveContained(registryRoot, skillPath); + const entries = await readdir(dirAbs, { withFileTypes: true, recursive: true }); + companions = entries + .filter((entry) => entry.isFile()) + .map((entry) => { + const parent = path.relative(dirAbs, entry.parentPath ?? dirAbs); + return path.join(parent, entry.name).split(path.sep).join("/"); + }) + .filter((rel) => rel !== "SKILL.md" && !rel.split("/").some((seg) => seg.startsWith("."))) + .sort(); + } catch { + companions = []; + } + + return [pair("SKILL.md"), ...companions.map(pair)]; +} + function targetForMember(member: PackMember): { sourceRel: string; targetRel: string } { switch (member.kind) { - case "skill": { - const category = member.source.includes("/core/") ? "core" : "community"; + case "skill": + // Entry point only. Companion files come from skillFileTargets(); callers + // that install or diff a skill must use it instead of this single pair. return { sourceRel: path.posix.join(member.source, "SKILL.md"), - targetRel: path.posix.join(".cursor", "skills", category, member.id, "SKILL.md"), + targetRel: path.posix.join(skillTargetDir(member.source, member.id), "SKILL.md"), }; - } case "rule": return { sourceRel: member.source, @@ -96,19 +142,22 @@ export async function installSkill( options: InstallOptions = {}, ): Promise { const stats = emptyStats(); - const category = skill.path.includes("/core/") ? "core" : "community"; - const sourceRel = path.posix.join(skill.path, "SKILL.md"); - const targetRel = path.posix.join(".cursor", "skills", category, skill.id, "SKILL.md"); const managedHashes = await loadManagedHashLedger(projectRoot); - const outcome = await copyRegistryFile( + for (const { sourceRel, targetRel } of await skillFileTargets( registryRoot, - projectRoot, - sourceRel, - targetRel, - options.protectedGlobs ?? [], - { managedHashes, persistManagedHashes: false }, - ); - recordOutcome(stats, targetRel, outcome); + skill.path, + skill.id, + )) { + const outcome = await copyRegistryFile( + registryRoot, + projectRoot, + sourceRel, + targetRel, + options.protectedGlobs ?? [], + { managedHashes, persistManagedHashes: false }, + ); + recordOutcome(stats, targetRel, outcome); + } await saveManagedHashLedger(projectRoot, managedHashes); return stats; } @@ -165,16 +214,21 @@ export async function installPack( const copyOpts = { managedHashes, persistManagedHashes: false as const }; for (const member of packManifest.members) { - const { sourceRel, targetRel } = targetForMember(member); - const outcome = await copyRegistryFile( - registryRoot, - projectRoot, - sourceRel, - targetRel, - protectedGlobs, - copyOpts, - ); - recordOutcome(stats, targetRel, outcome); + const pairs = + member.kind === "skill" + ? await skillFileTargets(registryRoot, member.source, member.id) + : [targetForMember(member)]; + for (const { sourceRel, targetRel } of pairs) { + const outcome = await copyRegistryFile( + registryRoot, + projectRoot, + sourceRel, + targetRel, + protectedGlobs, + copyOpts, + ); + recordOutcome(stats, targetRel, outcome); + } } await saveManagedHashLedger(projectRoot, managedHashes); return stats; diff --git a/scripts/build-registry.mjs b/scripts/build-registry.mjs index 5c0e19e..58cf586 100644 --- a/scripts/build-registry.mjs +++ b/scripts/build-registry.mjs @@ -5,7 +5,7 @@ * * Usage: node scripts/build-registry.mjs */ -import { readdir, readFile, writeFile } from "node:fs/promises"; +import { readFile, readdir, writeFile } from "node:fs/promises"; import path from "node:path"; import { fileURLToPath } from "node:url";