From ad44e7cf69c319068178c24b649d0d512205ffc5 Mon Sep 17 00:00:00 2001 From: Elber Domingos Date: Mon, 17 Aug 2026 22:24:36 -0300 Subject: [PATCH] =?UTF-8?q?feat:=20sync:commands=20pipeline=20+=20Pi=20age?= =?UTF-8?q?nts=20como=20ponteiros=20can=C3=B4nicos?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adiciona npm run sync:commands que gera .cursor/commands/ a partir de .claude/commands/ + overlays YAML (frontmatter, patchlets Cursor-only). Converte os 9 agents Pi do roster de wrappers verbosos para ponteiros finos (frontmatter FIA + referência ao .claude/agents/.md canônico). Testes e docs atualizados para o novo contrato. Co-authored-by: Cursor --- CLAUDE.md | 1 + DOCS.md | 27 ++-- package.json | 2 + .../.pi/agents/api-docs-researcher.md | 6 +- .../.pi/agents/component-architect.md | 6 +- .../.pi/agents/screen-routes-generator.md | 6 +- pi-templates/.pi/agents/start-mapper.md | 6 +- pi-templates/.pi/agents/start-scaffolding.md | 6 +- .../.pi/agents/task-master-generator.md | 6 +- pi-templates/.pi/agents/task-sequencer.md | 6 +- pi-templates/.pi/agents/ui-component-page.md | 6 +- .../.pi/agents/ui-component-researcher.md | 6 +- scripts/command-overlays.yaml | 127 ++++++++++++++++ scripts/sync-commands.mjs | 142 ++++++++++++++++++ test/fda-auto-recovery.test.js | 9 ++ test/interaction-contracts.test.js | 11 +- test/semantic-fields.test.js | 5 +- test/sync-commands.test.js | 93 ++++++++++++ 18 files changed, 447 insertions(+), 24 deletions(-) create mode 100644 scripts/command-overlays.yaml create mode 100644 scripts/sync-commands.mjs create mode 100644 test/sync-commands.test.js diff --git a/CLAUDE.md b/CLAUDE.md index 4ec0a3d..233f59d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -36,6 +36,7 @@ keys or extra usage). npm test # full suite (node test/run.mjs) npm run lint # eslint npm run sync:skills # regenerates .cursor/skills mirrors (never edit those by hand) +npm run sync:commands # regenerates .cursor/commands from .claude + overlays ``` ## Hard rules diff --git a/DOCS.md b/DOCS.md index 719300a..9d9a35c 100644 --- a/DOCS.md +++ b/DOCS.md @@ -955,7 +955,7 @@ Going live and meta: | Command | What it does | | --- | --- | | `/launch` | From "runs on my machine" to LIVE, in rungs: **Ready** gate (lint/typecheck/test/build green, pushed, CI green) → **Safe** gate (the security skill's checklist walked top to bottom; two hard stops: open data-ownership and any committed secret) → **BETA** (public vercel.app + production backend: Convex prod deploy key, env promotion, deploy, smoke test) → **PRODUCTION** (own domain, `pk_live_`/`sk_live_`, prod webhooks, live payments with one real tested charge). Uses `node imp/scripts/fia-launch-check.mjs --json` as the source of truth when the FIA is installed; everything logged in `ai-docs/launch.md`; every outward step needs an explicit "yes". | -| `/create-command [name] [description]` | Scaffolds a new professionally-structured slash command in `.claude/commands/` and mirrors it to `.cursor/commands/`. | +| `/create-command [name] [description]` | Scaffolds a new professionally-structured slash command in `.claude/commands/`, registers it in `scripts/command-overlays.yaml`, and regenerates the Cursor twin via `npm run sync:commands`. | Every command that generates durable docs commits them via `node imp/scripts/docs-commit.mjs` (when the FIA is present) and guards its @@ -2955,6 +2955,7 @@ node bin/create-iai.js my-test # runs the installer locally npm test # node --test (CLI unit tests) npm run lint && npm run format npm run sync:skills # regenerate harness/.cursor/skills +npm run sync:commands # regenerate harness/.cursor/commands ``` **Mirror rule (single source of truth):** shared skills are edited ONLY in @@ -2966,15 +2967,21 @@ stamped project (Claude Code gets a symlink of its own to the same store). `test/consistency.test.js` fails on skill drift and `sync:skills:check` reports it without writing. Agents are the other way around: edit `harness/.claude/agents/`; `.cursor/agents/` and `.agents/agents/` are -file symlinks to it. Commands (`.claude/commands` ↔ `.cursor/commands`) -stay manual on purpose: their diffs are intentional (frontmatter, -`.claude→.cursor` paths, Cursor-only extras like `bugbot`); -`.agents/commands/` links at the Cursor copy. Cross-runtime knowledge -follows the same principle: one canonical file + pointers (the -semantic-fields catalog and the interaction catalog in the design-system -skill; `test/semantic-fields.test.js` and `test/interaction-contracts.test.js` -are the tripwires that no runtime — including the `.agents/` path Cursor -actually opens — loses its pointer). +file symlinks to it. Shared commands are edited ONLY in +`harness/.claude/commands/` — `harness/.cursor/commands/` is GENERATED by +`npm run sync:commands` from `scripts/command-overlays.yaml` (Cursor +frontmatter plus intentional patchlets like `AskQuestion`, `bugbot`, and +`.claude→.cursor` skill paths); `.agents/commands/` links at the Cursor +copy. Pi harness agents (`.pi/agents/.md` for the nine start/kit +roster entries) are thin wrappers: FIA frontmatter + a pointer to +`.claude/agents/.md` — the rehearsal lives in one place. FIA-only +agents (planner, builder, scout, reviewer, documenter) stay self-contained. +`test/sync-commands.test.js` is the tripwire for command drift and Pi +pointer regressions. Cross-runtime knowledge follows the same principle: one +canonical file + pointers (the semantic-fields catalog and the interaction +catalog in the design-system skill; `test/semantic-fields.test.js` and +`test/interaction-contracts.test.js` are the tripwires that no runtime — +including the `.agents/` path Cursor actually opens — loses its pointer). Structure: `bin/` (entrypoints: `create-iai.js` = `npx impactus`, `imp.js` = the launcher) · `src/main.js` (pipeline) · `src/config.js` (catalogs: diff --git a/package.json b/package.json index 87b9d6f..957e368 100644 --- a/package.json +++ b/package.json @@ -14,6 +14,8 @@ "test": "node test/run.mjs", "sync:skills": "node scripts/sync-skills.mjs", "sync:skills:check": "node scripts/sync-skills.mjs --check", + "sync:commands": "node scripts/sync-commands.mjs", + "sync:commands:check": "node scripts/sync-commands.mjs --check", "lint": "eslint .", "lint:fix": "eslint . --fix", "format": "prettier --write ." diff --git a/pi-templates/.pi/agents/api-docs-researcher.md b/pi-templates/.pi/agents/api-docs-researcher.md index 14b094b..62cd1c6 100644 --- a/pi-templates/.pi/agents/api-docs-researcher.md +++ b/pi-templates/.pi/agents/api-docs-researcher.md @@ -7,6 +7,8 @@ thinking: medium inheritProjectContext: true --- -You are the harness api-docs-researcher adapted for the FIA roster. Read `ai-docs/start.md` (or `ai-docs/map.yaml`) for project context, research the requested API (official docs first; use `web_search` to locate current pages and `fetch_content` to read them live), and write `ai-docs/apis/.md`: auth, the endpoints THIS project needs, request/response examples, rate limits, and error handling — tailored to the project's use case, not a generic dump. Documentation only. +You are the FIA roster entry for the **api-docs-researcher** harness agent. -When the brief asks for tooling research (/stack always does), also check the FOUR dimensions — docs (+ `llms.txt` probe), official agent skills (`https://skills.sh/`), official CLI (verify npm packages with `npm view version`), official MCP server — and log each one right after checking it: `node imp/scripts/stack-research.mjs log --dim --found "…" --source ` (or `--none --source `). Never log from memory; every dimension needs a real source. Summarize the four rows in the doc's **Tooling** section. +**Canonical prompt:** read and follow `.claude/agents/api-docs-researcher.md` in full. That file is the single source of truth for behavior, checklists, and contracts — do not restate or shorten it here. + +Runtime notes for Pi: use the tools in this frontmatter; when the FIA is installed, `imp/` paths and gates in the canonical prompt apply. diff --git a/pi-templates/.pi/agents/component-architect.md b/pi-templates/.pi/agents/component-architect.md index eb51868..78650d4 100644 --- a/pi-templates/.pi/agents/component-architect.md +++ b/pi-templates/.pi/agents/component-architect.md @@ -7,4 +7,8 @@ thinking: high inheritProjectContext: true --- -You are the harness component-architect adapted for the FIA roster. FIRST seed the CORE KIT — always, before any app-specific analysis. No runnable scaffold (true greenfield): every row of `.claude/skills/design-system/references/core-kit.md` (buttons, inputs, MaskedInput, Select, Combobox, MultiSelect, DateInput/DatePicker/DateTimePicker, menus incl. ContextMenu, dialogs, toast, Skeleton, EmptyState, and the project `DataTable` on TanStack Table as `default` with REUI Data Grid as `alternative`) enters the registry as `planned` regardless of what map.yaml shows — the always-needed components are built up front by the `Kind: kit` task, never lazily by a feature task. A scaffold/codebase already exists (template install, `/kit` inventory, `/absorb`): seed AS-BUILT — one `installed` row per component the code actually has (real paths; duplicates for the same need → record both and flag), and `planned` rows only for needs the PRD/map data model shows; speculative needs enter through `/kit`'s gap audit, never by default. On a non-default stack keep every NEED and swap Origin/File/Install per `ai-docs/stack.md`. Then analyze `ai-docs/map.yaml` and existing docs in `ai-docs/components/`, and produce `ai-docs/components/ideal-components.md`: the full inventory of reusable components the app needs (forms with Zod validation, shadcn-based, accessible), each with props, variants, and usage guidance — tables default to the project DataTable's full contract (global fuzzy multi-word search with yellow highlights; header menu on click AND right-click; per-column type-adapted filters; a single Filter control + chips with an x — never a toolbar row of per-column filter buttons; visibility; pagination; selection + bulk bar; skeleton/empty/no-results; design-system `references/interaction.md`), never to REUI unless a screen explicitly asks. Then seed/update `ai-docs/components/registry.md` from that inventory: one row per component in the markdown table between the `` / `` markers (columns Component | Category | Origin | File | Install | URL | Status | Role | When to use), status `installed` for components the code already has and `planned` otherwise, role `default`/`alternative` when two entries serve the same need — the task briefs' reuse lock depends on this registry being seeded. Specification only — implementation belongs to FDAs or ui-component-page. +You are the FIA roster entry for the **component-architect** harness agent. + +**Canonical prompt:** read and follow `.claude/agents/component-architect.md` in full. That file is the single source of truth for behavior, checklists, and contracts — do not restate or shorten it here. + +Runtime notes for Pi: use the tools in this frontmatter; when the FIA is installed, `imp/` paths and gates in the canonical prompt apply. diff --git a/pi-templates/.pi/agents/screen-routes-generator.md b/pi-templates/.pi/agents/screen-routes-generator.md index 915c072..b81632a 100644 --- a/pi-templates/.pi/agents/screen-routes-generator.md +++ b/pi-templates/.pi/agents/screen-routes-generator.md @@ -7,4 +7,8 @@ thinking: medium inheritProjectContext: true --- -You are the harness screen-routes-generator adapted for the FIA roster. Read the PRD (`ai-docs/PRD.md`, or `ai-docs/prd.md` if that's the one that exists) and produce `ai-docs/screens-routes.md`: every screen, its route, navigation flow, per-screen components, and current implementation status found in the codebase. Output contract: routes go in markdown tables with columns `Route | Screen Component | File Location | Auth Required | Status`, status marked ✅ implemented / 🔄 partial / ⏳ to be implemented — the FIA viewer's Plan view parses exactly this shape. Documentation only — no code changes. +You are the FIA roster entry for the **screen-routes-generator** harness agent. + +**Canonical prompt:** read and follow `.claude/agents/screen-routes-generator.md` in full. That file is the single source of truth for behavior, checklists, and contracts — do not restate or shorten it here. + +Runtime notes for Pi: use the tools in this frontmatter; when the FIA is installed, `imp/` paths and gates in the canonical prompt apply. diff --git a/pi-templates/.pi/agents/start-mapper.md b/pi-templates/.pi/agents/start-mapper.md index 2d7474d..9d94b8c 100644 --- a/pi-templates/.pi/agents/start-mapper.md +++ b/pi-templates/.pi/agents/start-mapper.md @@ -7,4 +7,8 @@ thinking: medium inheritProjectContext: true --- -You are the harness start-mapper adapted for the FIA roster. Read `ai-docs/start/map-start.yaml` to learn the schema, explore the entire codebase (skip node_modules, .git, build output), and write a complete `ai-docs/map.yaml`: purpose, stack, routes, models, reusable components, conventions. Read `ai-docs/stack.md` too: when its Automations layer names an external service (e.g. Modal), fill the schema's `automations:` section (provider, path, jobs with triggers, deploy command); when it says "none", omit that section. If the schema file is missing, stop and say so. You document — you never modify source code. +You are the FIA roster entry for the **start-mapper** harness agent. + +**Canonical prompt:** read and follow `.claude/agents/start-mapper.md` in full. That file is the single source of truth for behavior, checklists, and contracts — do not restate or shorten it here. + +Runtime notes for Pi: use the tools in this frontmatter; when the FIA is installed, `imp/` paths and gates in the canonical prompt apply. diff --git a/pi-templates/.pi/agents/start-scaffolding.md b/pi-templates/.pi/agents/start-scaffolding.md index 560ae10..2ce68df 100644 --- a/pi-templates/.pi/agents/start-scaffolding.md +++ b/pi-templates/.pi/agents/start-scaffolding.md @@ -7,4 +7,8 @@ thinking: medium inheritProjectContext: true --- -You are the harness start-scaffolding agent adapted for the FIA roster. Read `ai-docs/map.yaml`, compare with what already exists, and create ONLY the missing skeleton: folders and near-empty files containing a single TODO comment describing what belongs there (`// TODO: …` for TS/JS, `/* TODO: … */` for CSS). Never write implementations, never overwrite existing files. Other agents (or FDAs) fill in the code. +You are the FIA roster entry for the **start-scaffolding** harness agent. + +**Canonical prompt:** read and follow `.claude/agents/start-scaffolding.md` in full. That file is the single source of truth for behavior, checklists, and contracts — do not restate or shorten it here. + +Runtime notes for Pi: use the tools in this frontmatter; when the FIA is installed, `imp/` paths and gates in the canonical prompt apply. diff --git a/pi-templates/.pi/agents/task-master-generator.md b/pi-templates/.pi/agents/task-master-generator.md index 16011c5..68c7632 100644 --- a/pi-templates/.pi/agents/task-master-generator.md +++ b/pi-templates/.pi/agents/task-master-generator.md @@ -7,4 +7,8 @@ thinking: high inheritProjectContext: true --- -You are the harness task-master-generator adapted for the FIA roster. Read the PRD (`ai-docs/PRD.md`, or `ai-docs/prd.md` if that's the one that exists) and `ai-docs/screens-routes.md`, inventory what the codebase already implements (never create tasks for existing features), and generate one issue per vertical slice at `ai-docs/todos/issues/NN-.md` plus the index `ai-docs/todos/task-master.md` with the dependency graph and implementation inventory. Greenfield (no runnable app scaffold in the inventory) → Task 01 is ALWAYS the Foundation task, with this fixed scope in the stack `ai-docs/stack.md` names: scaffold the app, install the base components the registry (`ai-docs/components/registry.md`) plans (`planned → installed`), create the `/ui-components` design system page, apply the stack's neutral DEFAULT theme, and keep the build hermetic — `npm run build` passes with NO `.env.local` (providers guard missing env with a clear runtime notice, never a prerender crash) and the task ships `.env.example` naming every key the stack reads (names only) — never visual-identity decisions (identity is approved via `/theme` right after this task: the theme checkpoint the executors gate on, satisfied by a closed `theme` decision log in `ai-docs/decisions/`). The foundation issue carries a `Kind: foundation` meta line below `Milestone:` (plain text at line start, NEVER bold — the FDA's build gate greps `^Kind:`), no `Spec:` line, and every other task blocked by it directly or transitively. Greenfield → Task 02 is ALWAYS the Core component kit task (`Kind: kit` meta line, same position and same plain-text rule; blocked by 01; every feature task blocked by it directly or transitively): build every composed core-kit component still `planned` in the registry (catalog + contracts: `.claude/skills/design-system/references/core-kit.md`) — at minimum the `DataTable` on TanStack Table with its FULL contract (global fuzzy multi-word search; header menu on click AND right-click with sort/hide/filter; per-column filters adapted to the column type; filter chips + clear-all; column visibility; pagination; row selection + bulk-actions bar; row-click edit; skeleton/empty/no-results states), `Combobox`, `MultiSelect`, `MaskedInput`, `DateInput`, `DatePicker`, `DateTimePicker`, `EmptyState` — demo each with realistic sample data on `/ui-components`, flip each registry row `planned → installed`, and list ONE CHECKBOX PER COMPONENT in the issue body (the FDA checklist gate refuses an open box). The always-needed components are built up front — never lazily by whichever feature task first needs one. (`Kind: kit` also marks the design-only upgrade tasks `/kit` generates in an existing codebase — same build arming, same checkbox discipline.) The task that wires authentication end-to-end (and any task adding a new profile/role) also creates, inside its own scope, ONE dev test user per profile/role the PRD names via the auth provider's native test mechanism (see `ai-docs/apis/.md` § Test users — Clerk dev instances: `+clerk_test` emails with the fixed verification code `424242`; the code replaces email verification, so a password is only needed if the instance signs in by password — then seed one via an env var name in `.env.local`), records them in the `ai-docs/test-credentials.md` roster (real passwords only as env var names in `.env.local`), and its verify step SHOWS the roster to the engineer — these credentials are how every later task, /test-ui run and QA session signs in. Also write the specs: in FULL mode (whole-PRD breakdown) each major capability gets one at `ai-docs/specs/NNNN-.md` following `ai-docs/specs/0000-example.md` (the reference example — never a live spec; numbering continues from the highest existing file), with requirements (`FR-n`/`NFR-n`) and BDD scenarios (`S-n`) derived from the PRD, `Status: defined` plus a `Definition Gate: passed — ` line once nothing P1 is open. In DELTA mode (invoked by `/feature`) the delta spec already exists — link to it, never duplicate. Linkage runs both ways: an issue that proves scenarios carries a plain `Spec: NNNN (S-1, S-4)` line (line start, never bold — the coverage gate greps it) and the spec's `Tasks:` line lists those task numbers; glue/scaffolding tasks carry no `Spec:` line. An issue's `Spec:` scenarios must be implementable after its `Blocked by` predecessors land — never assign a scenario that needs a schema or feature a later, blocked-by-this-task issue will introduce (hidden cycle: Task 06 proving tag–task deletion while Task 07 creates the tasks table and is blocked by 06); put that scenario on the later issue instead. Output contract: every issue carries `Status:` / `Blocked by:` / `Priority:` meta lines and task-master.md keeps a markdown task table — the FIA viewer's Plan view parses exactly these. On revision feedback, restructure only `pending`/`blocked` tasks — never renumber or delete `in-progress`/`done` ones. These issues feed the FDAs (see the FIA skill's harness_bridge cookbook). +You are the FIA roster entry for the **task-master-generator** harness agent. + +**Canonical prompt:** read and follow `.claude/agents/task-master-generator.md` in full. That file is the single source of truth for behavior, checklists, and contracts — do not restate or shorten it here. + +Runtime notes for Pi: use the tools in this frontmatter; when the FIA is installed, `imp/` paths and gates in the canonical prompt apply. diff --git a/pi-templates/.pi/agents/task-sequencer.md b/pi-templates/.pi/agents/task-sequencer.md index 932bc22..bd8af2a 100644 --- a/pi-templates/.pi/agents/task-sequencer.md +++ b/pi-templates/.pi/agents/task-sequencer.md @@ -7,4 +7,8 @@ thinking: medium inheritProjectContext: true --- -You are the harness task-sequencer adapted for the FIA roster. Archive any files in `ai-docs/actual-todo/` to `ai-docs/todos/done/` — but check them first: a brief being archived with unchecked `- [ ]` boxes means its run never closed properly (the FDA's checklist gate ticks-or-fails), so archive it anyway and REPORT it loudly ("archived with N unchecked boxes — its task may not be done; verify the issue status") instead of moving it silently. Then pick the next unblocked issue from `ai-docs/todos/issues/` (or the task given), and write ONE fresh implementation brief in `ai-docs/actual-todo/` with concrete file paths, code examples, and test seams based on the CURRENT codebase. **Theme gate (greenfield checkpoint):** when the roadmap has a `done` issue carrying a `Kind: foundation` line and the picked task is any other roadmap task, first verify a closed `theme` decision log exists — `node imp/scripts/decision-log.mjs list --command theme --json` shows ANY record with `"status": "closed"` (never `latest`: a newer open log from an abandoned re-run must not cancel an earlier approval); script absent (FIA not installed) → grep `ai-docs/decisions/*-theme-*.md` for a `status: closed` frontmatter line instead. If none, claim nothing and write no brief: report "Theme gate: foundation done, visual identity never decided — run /theme or explicitly accept the default theme (also recorded as a decision)"; the orchestrator resolves it with the engineer. Any closed theme log passes the gate. **Env gate (build preflight):** when the picked issue itself carries `Kind: foundation` or `Kind: kit` (both run the production build in their Test phase), first run `node imp/scripts/env-preflight.mjs --json` — it derives the dev keys the declared stack (`ai-docs/stack.md`) reads at build/boot and checks `.env.local`. `"passed": false` → claim nothing and write no brief: report "Env gate: the foundation build reads dev keys not in `.env.local` yet — " plus each `fix` line from the report (Convex provisions via CLI; Clerk keys come from the dashboard — a human step); the orchestrator provisions with the engineer mid-flow and re-delegates. Script absent → check `.env.local` by hand for the keys the manifest's layers imply. **Impossible dependency (auto-split once):** if the picked issue's spec or acceptance requires a schema/table/feature that a LATER issue introduces, and that later issue is `Blocked by` the picked one (hidden cycle: 06 needs 07's tasks table, 07 is blocked by 06), do NOT ask and do NOT write a brief against the impossible scope. Apply ONE split: narrow the current issue to what the current tree plus its `Blocked by` predecessors can implement; move the leftover scenarios/acceptance onto the later issue that introduces the missing piece; keep every `S-n` covered (never drop a spec ID); update both issue files and `ai-docs/todos/task-master.md`; then write the narrowed brief and continue. Report the split in one line. Theme/env/stack decisions and any split that would drop a spec ID still stop. If after that rewrite you still cannot write a brief, THEN stop with one question and a recommendation. Copy the issue's `Kind:` line (`Kind: foundation` or `Kind: kit`) verbatim into the brief — plain text at line start (the FDA greps it to add `npm run build` to its test phase, the same mechanism as `Spec:` arming coverage); for a `Kind: kit` brief, resolve the component contracts from `.claude/skills/design-system/references/core-kit.md` and keep the issue's one-checkbox-per-component list intact (the checklist gate enforces it). When the issue carries a `Spec:` line, copy it verbatim into the brief — plain text at line start, never bold (the FDA greps it to arm the spec-coverage gate) — and add a `## Spec scenarios` section with the covered `### S-n` blocks copied from `ai-docs/specs/NNNN-.md`; an issue without one gets neither. When the task touches UI, write a `Surface: ui` line in the brief — plain text at line start, under `Spec:` (the FDA greps it to arm the UI-conformance gate; a 100%-backend task gets no `Surface:` line, and `Surface: api` explicitly stands the gate down). Every UI task's brief carries a `## Design system components` section mapping each UI need to a row of `ai-docs/components/registry.md` — creating a component the registry already covers is FORBIDDEN (variations are props/composition; a need with no registry row becomes a blocking question, never a hand-rolled component) — plus a "Semantic fields" table when the task's data includes state/UF, country, address/CEP, phone, money, dates or fixed categories (known-domain data never ships as a free-text input; catalog: `.claude/skills/design-system/references/semantic-fields.md`) — plus an interaction-patterns block naming which `ai-docs/ui/patterns.md` defaults apply (create/edit in a `Dialog` opened by button/row click, field errors inline via the form library's message slot, success/failure toasts after the mutation resolves, `AlertDialog` for destructive actions); a screen may deviate from a patterns.md default only when the brief names the deviation and its reason. Every brief ends with checkbox lists the FDA's gates enforce — `## Objectives`, `## Acceptance Criteria`, and the standard `## Quality Checklist`: tests written first at the agreed seams; full suite passing; TypeScript types (no `any`); zod validation; i18n keys when the project has i18n; components from the registry; no known-domain field rendered as a free-text input (semantic-fields contract); field errors inline with each field (never only a banner or toast); mutations announcing success AND failure via toast after resolving; create/edit/destructive flows per `ai-docs/ui/patterns.md`; error handling; mobile responsiveness; touch-friendly targets (min 44x44px). When the picked issue touches authentication, sign-up, profiles/roles or permissions, the brief carries the test-users rule: create/update ONE dev test user per profile/role via the auth provider's native test mechanism (`ai-docs/apis/.md` § Test users — Clerk dev instances: `+clerk_test` emails, fixed code `424242`) and update the `ai-docs/test-credentials.md` roster (real passwords only as env var names in `.env.local`); the run's summary surfaces those credentials to the engineer. When `ai-docs/examples/registry.md` has rows whose Tags match this task, add a `## Reference examples` section listing each match as slug · what to take · license — the same way registry components are cited; no match means no section (the shelf is never a gate). Examples teach shape: the brief says to learn the approach and write our own code, and anything under `AGPL-3.0`/`GPL-*`/`unknown` is never copied verbatim. Update the issue `Status:` lines. You prepare briefs — you never implement; hand the brief to an FDA (`node imp/fda_plan_build_test.mjs`) or the engineer. +You are the FIA roster entry for the **task-sequencer** harness agent. + +**Canonical prompt:** read and follow `.claude/agents/task-sequencer.md` in full. That file is the single source of truth for behavior, checklists, and contracts — do not restate or shorten it here. + +Runtime notes for Pi: use the tools in this frontmatter; when the FIA is installed, `imp/` paths and gates in the canonical prompt apply. diff --git a/pi-templates/.pi/agents/ui-component-page.md b/pi-templates/.pi/agents/ui-component-page.md index 72b710d..042691c 100644 --- a/pi-templates/.pi/agents/ui-component-page.md +++ b/pi-templates/.pi/agents/ui-component-page.md @@ -7,4 +7,8 @@ thinking: medium inheritProjectContext: true --- -You are the harness ui-component-page agent adapted for the FIA roster. The SOURCE OF TRUTH is `ai-docs/components/registry.md` — one section per registry row (falling back to `ai-docs/map.yaml` reusable_components and `ai-docs/components/ideal-components.md` only when the registry does not exist yet). Detect the router (App Router → `app/ui-components/page.tsx`) and create or update a live design-system page: sidebar navigation with a search input and the fixed category groups (Foundations first — design tokens rendered live: color swatches from the CSS variables, type scale, spacing & radius), then one section per component with title, one-line "when to use" from the registry, live interactive examples (variants, sizes, disabled/loading/error states) and the file path. **Isolation (non-negotiable):** each card demos ONLY that registry component — a kitchen-sink form (UF + Categoria + MultiSelect + three date fields) reused as the body of DateInput, Calendar, DateTimePicker, or Combobox is a contract failure (design-system `references/interaction.md` §6). A composed form demo lives only under Form. Core-kit components (design-system skill, `references/core-kit.md` + `references/interaction.md`) get REAL working demos, never static renders: the `DataTable` section uses realistic sample data and exercises its full contract live (global fuzzy search with yellow `` highlights, header menu + right-click, a single Filter control + header Filter… — NEVER a toolbar row of per-column filter buttons — chips with an x + clear-all, column visibility, pagination, selection + bulk bar, empty/no-results states); `Combobox`/`MultiSelect` demos are searchable with enough options to scroll, popover width matching the trigger, and yellow highlights on the typed query; the date components open their calendars/masks and the caption jumps month AND year (arrows are not enough). Every clickable control uses `cursor-pointer`. Follow the project's design tokens; reuse existing components — never fork them. +You are the FIA roster entry for the **ui-component-page** harness agent. + +**Canonical prompt:** read and follow `.claude/agents/ui-component-page.md` in full. That file is the single source of truth for behavior, checklists, and contracts — do not restate or shorten it here. + +Runtime notes for Pi: use the tools in this frontmatter; when the FIA is installed, `imp/` paths and gates in the canonical prompt apply. diff --git a/pi-templates/.pi/agents/ui-component-researcher.md b/pi-templates/.pi/agents/ui-component-researcher.md index 0f4cb40..c9ccae9 100644 --- a/pi-templates/.pi/agents/ui-component-researcher.md +++ b/pi-templates/.pi/agents/ui-component-researcher.md @@ -7,4 +7,8 @@ thinking: low inheritProjectContext: true --- -You are the harness ui-component-researcher adapted for the FIA roster. For each requested component, research the library's official docs (use `web_search` to locate them and `fetch_content` to read the current pages) and write `ai-docs/components//.md`: install command, imports, props/API, variants, accessibility notes, and a usage example consistent with this project's stack (`ai-docs/map.yaml`). Documentation only. +You are the FIA roster entry for the **ui-component-researcher** harness agent. + +**Canonical prompt:** read and follow `.claude/agents/ui-component-researcher.md` in full. That file is the single source of truth for behavior, checklists, and contracts — do not restate or shorten it here. + +Runtime notes for Pi: use the tools in this frontmatter; when the FIA is installed, `imp/` paths and gates in the canonical prompt apply. diff --git a/scripts/command-overlays.yaml b/scripts/command-overlays.yaml new file mode 100644 index 0000000..46d5614 --- /dev/null +++ b/scripts/command-overlays.yaml @@ -0,0 +1,127 @@ +# Cursor overlays for harness slash commands. +# +# Canonical body: harness/.claude/commands/.md (human/agent edits ONLY here). +# Generated twin: harness/.cursor/commands/.md via `npm run sync:commands`. +# `.agents/commands/` symlinks at the Cursor copy — that is what Cursor/Pi open. +# +# `description` is required for every command (consistency.test.js frontmatter check). +# `mergeFrontmatter: true` keeps Claude-only keys (argument-hint, allowed-tools) and +# overlays `name` + `description`. `replace` / `append` patch the body after globals. + +globalReplace: + - from: ".claude/skills/" + to: ".cursor/skills/" + - from: ".claude/commands/" + to: ".cursor/commands/" + - from: AskUserQuestion + to: AskQuestion + +commands: + absorb: + description: Workflow command /absorb — onboard an existing system (as-built PRD, map, conventions) + replace: + - from: (and mirror to `.pi/skills/project/SKILL.md` when `.pi/` exists) + to: (and mirror to `.claude/skills/project/SKILL.md` and `.pi/skills/project/SKILL.md` when they exist) + + bug: + description: Workflow command /bug — fix a defect, reproduction first + + component: + description: Workflow command /component — see AGENTS.md + + create-command: + description: Create a new slash command with professional structure and best practices + mergeFrontmatter: true + replace: + - from: "Check: `ls .claude/commands/`" + to: "Check: `ls .cursor/commands/` (and `ls .claude/commands/`)" + + design: + description: Workflow command /design — see AGENTS.md + + dev: + description: Workflow command /dev — see AGENTS.md + replace: + - from: >- + present them with their recommended defaults (so the user can answer "yes to all") + and **WAIT for the answers before writing any code**. + to: >- + present them with their recommended defaults via `AskQuestion` (so the user can + answer "yes to all") and **WAIT for the answers before writing any code**. + append: | + + + In Cursor you can additionally launch the `bugbot` subagent for an independent review of the branch changes when the task is large or risky. + + example: + description: Workflow command /example — register an external reference (repo, code, docs, design) in ai-docs/examples/ + replace: + - from: | + Ask (ONE question at a time, with a recommendation): + to: | + Ask (ONE question at a time, with a recommendation, via `AskQuestion` when the + options are enumerable): + + feature: + description: Workflow command /feature — new functionality on an existing system (delta interview → delta spec → integrated tasks) + + grill: + description: Workflow command /grill — stress-test a plan, PRD, or decision one question at a time + replace: + - from: >- + 1. **One question at a time.** Ask, wait for the answer, then continue. Multiple + questions at once are bewildering. + to: >- + 1. **One question at a time.** Ask, wait for the answer, then continue. Multiple + questions at once are bewildering. Use the AskQuestion tool when the options are + enumerable; free text otherwise. + - from: >- + 2. **Recommend an answer with every question.** "My recommendation: X, because Y" + — the user can accept in two words or push back. + to: >- + 2. **Recommend an answer with every question.** "My recommendation: X, because Y" + — the user can accept in two words or push back. When using AskQuestion, put the + recommended option first with "(Recommended)". + + kit: + description: Workflow command /kit — design-system onboarding & audit of existing code (as-built registry, /ui-components, gap report, approved design-only tasks) + + launch: + description: Workflow command /launch — see AGENTS.md + + note: + description: Workflow command /note — capture an idea in ai-docs/inbox.md with zero questions + + onboarding: + description: "Workflow command /onboarding — first run on an existing system: /absorb → /stack → /kit in one guided pass" + + quick: + description: Workflow command /quick — small change without ceremony (triage → guarded implementation → quick-log entry) + + restore: + description: Workflow command /restore — see AGENTS.md + + spec: + description: Workflow command /spec — create or update a durable spec in ai-docs/specs/ (requirements + BDD scenarios + traceability) + replace: + - from: "always with a recommended answer; facts the codebase answers are research, not questions): observable problem" + to: "always with a recommended answer, via `AskQuestion` when the options are enumerable; facts the codebase answers are research, not questions): observable problem" + + stack: + description: Workflow command /stack — see AGENTS.md + + start: + description: Workflow command /start — see AGENTS.md + + sv: + description: Workflow command /sv — see AGENTS.md + + team: + description: Workflow command /team — see AGENTS.md + + test-ui: + description: Test UI functionality in a real browser with automated sign-in and issue detection + mergeFrontmatter: true + + theme: + description: Workflow command /theme — see AGENTS.md diff --git a/scripts/sync-commands.mjs b/scripts/sync-commands.mjs new file mode 100644 index 0000000..7e0c6ff --- /dev/null +++ b/scripts/sync-commands.mjs @@ -0,0 +1,142 @@ +#!/usr/bin/env node +// Deterministic mirror: harness/.claude/commands → harness/.cursor/commands. +// +// The shared command BODIES live in .claude/commands/; Cursor twins are GENERATED +// with frontmatter + patchlets from scripts/command-overlays.yaml (test/sync-commands.test.js +// guards drift). Editing rule: harness/.claude/commands is the ONLY side a human or +// agent edits for command content — this script regenerates each .cursor twin. +// +// node scripts/sync-commands.mjs # write: mirror .claude → .cursor +// node scripts/sync-commands.mjs --check # verify only; exit 1 on drift +import { existsSync, readdirSync, readFileSync, mkdirSync, writeFileSync, rmSync } from 'node:fs'; +import { join, basename } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { parse as parseYaml } from 'yaml'; + +const ROOT = fileURLToPath(new URL('..', import.meta.url)); +const SRC = join(ROOT, 'harness', '.claude', 'commands'); +const DST = join(ROOT, 'harness', '.cursor', 'commands'); +const OVERLAYS_PATH = join(ROOT, 'scripts', 'command-overlays.yaml'); +const check = process.argv.includes('--check'); + +if (!existsSync(SRC)) { + console.log('harness/ not present (nested repo, absent on fresh checkout) — nothing to sync'); + process.exit(0); +} + +const overlaysDoc = parseYaml(readFileSync(OVERLAYS_PATH, 'utf8')); +const { globalReplace = [], commands: commandOverlays = {} } = overlaysDoc; + +function splitFrontmatter(text) { + const m = text.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/); + if (!m) return { frontmatter: null, body: text }; + return { frontmatter: m[1], body: text.slice(m[0].length) }; +} + +function parseFrontmatterMap(raw) { + if (!raw) return new Map(); + const map = new Map(); + for (const line of raw.split(/\r?\n/)) { + const idx = line.indexOf(':'); + if (idx === -1) continue; + const key = line.slice(0, idx).trim(); + const value = line.slice(idx + 1).trim(); + map.set(key, value); + } + return map; +} + +function applyReplacements(text, replacements, { optional = false } = {}) { + let out = text; + for (const { from, to } of replacements) { + if (!out.includes(from)) { + if (optional) continue; + throw new Error(`replacement miss — substring not found:\n${from.slice(0, 120)}…`); + } + out = out.replaceAll(from, to); + } + return out; +} + +function buildCursorFrontmatter(name, claudeFm, overlay) { + const merged = overlay.mergeFrontmatter && claudeFm ? new Map(claudeFm) : new Map(); + merged.set('name', name); + merged.set('description', overlay.description ?? merged.get('description') ?? `Workflow command /${name} — see AGENTS.md`); + // Stable key order: name, description, then the rest alphabetically. + const rest = [...merged.keys()].filter((k) => k !== 'name' && k !== 'description').sort(); + const lines = ['---', `name: ${merged.get('name')}`, `description: ${merged.get('description')}`]; + for (const key of rest) lines.push(`${key}: ${merged.get(key)}`); + lines.push('---', ''); + return lines.join('\n'); +} + +function renderCommand(name) { + const overlay = commandOverlays[name]; + if (!overlay?.description && !overlay?.mergeFrontmatter) { + throw new Error(`command-overlays.yaml missing entry (or description) for "${name}"`); + } + const srcPath = join(SRC, `${name}.md`); + const { frontmatter: claudeFmRaw, body: rawBody } = splitFrontmatter(readFileSync(srcPath, 'utf8')); + const claudeFm = parseFrontmatterMap(claudeFmRaw); + + let body = rawBody; + if (overlay.replace?.length) body = applyReplacements(body, overlay.replace); + body = applyReplacements(body, globalReplace, { optional: true }); + if (overlay.append) body += overlay.append; + + const header = buildCursorFrontmatter(name, claudeFm, overlay); + return header + body.replace(/^\n/, ''); +} + +const srcFiles = readdirSync(SRC) + .filter((f) => f.endsWith('.md')) + .sort(); +const copied = []; +const removed = []; +let unchanged = 0; + +for (const file of srcFiles) { + const name = basename(file, '.md'); + const rendered = renderCommand(name); + const twin = join(DST, `${name}.md`); + if (existsSync(twin) && readFileSync(twin, 'utf8') === rendered) { + unchanged++; + continue; + } + copied.push(`${name}.md`); + if (!check) { + mkdirSync(DST, { recursive: true }); + writeFileSync(twin, rendered); + } +} + +// Orphans: a .cursor command with no .claude counterpart is a leftover rename/delete. +for (const file of readdirSync(DST).filter((f) => f.endsWith('.md')).sort()) { + if (!existsSync(join(SRC, file))) { + removed.push(file); + if (!check) rmSync(join(DST, file)); + } +} + +// Overlay keys must match source files (no stale config). +const overlayKeys = Object.keys(commandOverlays).sort(); +const srcNames = srcFiles.map((f) => basename(f, '.md')).sort(); +const missingOverlay = srcNames.filter((n) => !commandOverlays[n]); +const staleOverlay = overlayKeys.filter((n) => !srcNames.includes(n)); +if (missingOverlay.length || staleOverlay.length) { + console.error('command-overlays.yaml out of sync with harness/.claude/commands/:'); + if (missingOverlay.length) console.error(` missing overlays: ${missingOverlay.join(', ')}`); + if (staleOverlay.length) console.error(` stale overlays: ${staleOverlay.join(', ')}`); + process.exit(1); +} + +const drift = copied.length + removed.length; +const verb = check ? 'would sync' : 'synced'; +if (drift === 0) { + console.log(`in sync — ${unchanged} files identical`); +} else { + for (const rel of copied) console.log(`${check ? 'drift' : 'copied'}: ${rel}`); + for (const rel of removed) console.log(`${check ? 'orphan' : 'removed'}: ${rel}`); + console.log(`${verb}: ${copied.length} copied, ${removed.length} removed, ${unchanged} unchanged`); +} +process.exit(check && drift > 0 ? 1 : 0); diff --git a/test/fda-auto-recovery.test.js b/test/fda-auto-recovery.test.js index 7fda983..366afb9 100644 --- a/test/fda-auto-recovery.test.js +++ b/test/fda-auto-recovery.test.js @@ -45,8 +45,17 @@ test('orchestrator surfaces share the one-shot recovery policy', () => { }); test('sequencer surfaces auto-split a hidden cycle instead of asking first', () => { + const harnessAgents = new Set(['agents/task-sequencer.md', 'agents/task-master-generator.md']); for (const [rel, needle] of HIDDEN_CYCLE) { const text = readFileSync(join(PI, rel), 'utf8'); + if (harnessAgents.has(rel)) { + const name = rel.replace('agents/', '').replace('.md', ''); + assert.ok( + text.includes('`.claude/agents/' + name + '.md`'), + `${rel} must point at the harness agent (canonical auto-split lives there)`, + ); + continue; + } assert.ok(text.includes(needle), `${rel} lost its "${needle}" clause`); } }); diff --git a/test/interaction-contracts.test.js b/test/interaction-contracts.test.js index 10c2436..ba2b8ab 100644 --- a/test/interaction-contracts.test.js +++ b/test/interaction-contracts.test.js @@ -68,12 +68,17 @@ test('the Pi cookbook and wrappers carry the interaction hard rule', () => { assert.ok(cookbook.includes('interaction.md'), 'cookbook lost the pointer to the interaction catalog'); assert.ok(cookbook.includes('yellow'), 'cookbook lost the yellow-highlight contract'); const page = readFileSync(join(ROOT, 'pi-templates', '.pi', 'agents', 'ui-component-page.md'), 'utf8'); - assert.ok(page.includes('interaction.md'), 'Pi ui-component-page wrapper lost the isolation pointer'); - assert.ok(page.includes('kitchen-sink'), 'Pi ui-component-page wrapper lost the kitchen-sink ban'); + assert.ok( + page.includes('`.claude/agents/ui-component-page.md`'), + 'Pi ui-component-page wrapper must point at the harness agent (isolation contract lives there)', + ); const kit = readFileSync(join(ROOT, 'pi-templates', '.pi', 'prompts', 'kit.md'), 'utf8'); assert.ok(kit.includes('interaction.md'), '/kit prompt lost the interaction audit'); const architect = readFileSync(join(ROOT, 'pi-templates', '.pi', 'agents', 'component-architect.md'), 'utf8'); - assert.ok(architect.includes('interaction.md'), 'Pi component-architect wrapper lost the interaction pointer'); + assert.ok( + architect.includes('`.claude/agents/component-architect.md`'), + 'Pi component-architect wrapper must point at the harness agent', + ); }); test('the UI-conformance rubric carries the interaction item', () => { diff --git a/test/semantic-fields.test.js b/test/semantic-fields.test.js index ac41e2e..2fae380 100644 --- a/test/semantic-fields.test.js +++ b/test/semantic-fields.test.js @@ -75,7 +75,10 @@ test('the Pi cookbook carries the semantic-fields hard rule', () => { const idea = readFileSync(join(ROOT, 'pi-templates', '.pi', 'prompts', 'idea.md'), 'utf8'); assert.ok(idea.includes('semantic type'), '/idea wrap-up no longer asks for semantic types in the data model'); const wrapper = readFileSync(join(ROOT, 'pi-templates', '.pi', 'agents', 'task-sequencer.md'), 'utf8'); - assert.ok(wrapper.includes('semantic-fields'), 'Pi task-sequencer wrapper lost the Semantic fields mandate'); + assert.ok( + wrapper.includes('`.claude/agents/task-sequencer.md`'), + 'Pi task-sequencer wrapper must point at the harness agent (Semantic fields mandate lives there)', + ); }); // The deterministic UI gate must audit the semantic-fields rule too — this diff --git a/test/sync-commands.test.js b/test/sync-commands.test.js new file mode 100644 index 0000000..ac9ed50 --- /dev/null +++ b/test/sync-commands.test.js @@ -0,0 +1,93 @@ +// Commands follow the same "one canonical + generated distribution" model as skills: +// harness/.claude/commands/ — the ONLY side humans/agents edit +// scripts/command-overlays.yaml — Cursor frontmatter + patchlets +// harness/.cursor/commands/ — GENERATED by npm run sync:commands +// harness/.agents/commands/ — symlinks at the Cursor copy (what Cursor opens) +import { test } from 'node:test'; +import assert from 'node:assert/strict'; +import { existsSync, readdirSync, readFileSync } from 'node:fs'; +import { join, basename } from 'node:path'; +import { fileURLToPath } from 'node:url'; +import { spawnSync } from 'node:child_process'; +import { parse as parseYaml } from 'yaml'; + +const ROOT = fileURLToPath(new URL('..', import.meta.url)); +const HARNESS = join(ROOT, 'harness'); +const SRC = join(HARNESS, '.claude', 'commands'); +const DST = join(HARNESS, '.cursor', 'commands'); +const OVERLAYS = join(ROOT, 'scripts', 'command-overlays.yaml'); +const SYNC = join(ROOT, 'scripts', 'sync-commands.mjs'); +const hasHarness = existsSync(SRC); +const skip = !hasHarness && 'harness/ not present (nested repo, absent on fresh checkout)'; + +const HARNESS_AGENTS = new Set([ + 'api-docs-researcher', + 'component-architect', + 'screen-routes-generator', + 'start-mapper', + 'start-scaffolding', + 'task-master-generator', + 'task-sequencer', + 'ui-component-page', + 'ui-component-researcher', +]); + +test('command-overlays.yaml covers every .claude command', { skip }, () => { + const names = readdirSync(SRC) + .filter((f) => f.endsWith('.md')) + .map((f) => basename(f, '.md')) + .sort(); + const overlays = parseYaml(readFileSync(OVERLAYS, 'utf8')).commands ?? {}; + const missing = names.filter((n) => !overlays[n]?.description); + const stale = Object.keys(overlays).filter((n) => !names.includes(n)); + assert.deepEqual(missing, [], `missing overlay descriptions: ${missing.join(', ')}`); + assert.deepEqual(stale, [], `stale overlay keys: ${stale.join(', ')}`); +}); + +test('sync:commands --check passes (cursor twins match generator)', { skip }, () => { + const result = spawnSync(process.execPath, [SYNC, '--check'], { encoding: 'utf8' }); + assert.equal(result.status, 0, result.stdout + result.stderr); + assert.match(result.stdout, /in sync/); +}); + +test('every .cursor command has frontmatter name matching filename', { skip }, () => { + const problems = []; + for (const file of readdirSync(DST).filter((f) => f.endsWith('.md')).sort()) { + const expected = file.replace(/\.md$/, ''); + const text = readFileSync(join(DST, file), 'utf8'); + const m = text.match(/^---\r?\n[\s\S]*?^name:\s*(\S+)\s*$/m); + if (!m) problems.push(`${file}: no frontmatter name:`); + else if (m[1] !== expected) problems.push(`${file}: frontmatter name "${m[1]}" != "${expected}"`); + } + assert.deepEqual(problems, [], problems.join('\n')); +}); + +test('harness-backed Pi agents point at .claude/agents/ instead of restating the prompt', { skip }, () => { + const piAgents = join(ROOT, 'pi-templates', '.pi', 'agents'); + const problems = []; + for (const name of HARNESS_AGENTS) { + const file = join(piAgents, `${name}.md`); + assert.ok(existsSync(file), `${name}.md missing from pi-templates`); + const text = readFileSync(file, 'utf8'); + const body = text.replace(/^---[\s\S]*?---\r?\n?/, ''); + const lines = body.split('\n').filter((l) => l.trim()).length; + assert.ok(lines <= 8, `${name}: Pi wrapper grew to ${lines} body lines — restating the harness agent?`); + if (!text.includes('`.claude/agents/' + name + '.md`')) { + problems.push(`${name}: missing pointer to .claude/agents/${name}.md`); + } + if (text.includes('You are the harness') && text.includes('adapted for the FIA roster')) { + problems.push(`${name}: still restates the old one-paragraph harness summary`); + } + } + assert.deepEqual(problems, [], problems.join('\n')); +}); + +test('FIA-only Pi agents stay self-contained (no harness pointer required)', () => { + const fiaOnly = ['planner', 'builder', 'scout', 'reviewer', 'documenter']; + const piAgents = join(ROOT, 'pi-templates', '.pi', 'agents'); + for (const name of fiaOnly) { + const text = readFileSync(join(piAgents, `${name}.md`), 'utf8'); + assert.doesNotMatch(text, /\.claude\/agents\//, `${name} should not point at a harness agent`); + assert.ok(text.includes('FIA'), `${name} should identify as an FIA roster agent`); + } +});