beaver-build 1.0.7 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +389 -237
  2. package/dist/index.js +1354 -63
  3. package/package.json +55 -57
package/dist/index.js CHANGED
@@ -38,6 +38,12 @@ var init_constants = __esm({
38
38
  value: "NUXT",
39
39
  description: "Upcoming...",
40
40
  disabled: true
41
+ },
42
+ HarnessOnly: {
43
+ display: "Apply AI Harness",
44
+ value: "HARNESS_ONLY",
45
+ description: "Add Claude Code harness to an existing project",
46
+ disabled: false
41
47
  }
42
48
  };
43
49
  }
@@ -288,10 +294,6 @@ var init_package_json = __esm({
288
294
  if (cart.testing === "PLAYWRIGHT") {
289
295
  scripts["test:e2e"] = "playwright test";
290
296
  }
291
- if (cart.ai === "CLAUDE") {
292
- scripts["docs:index"] = "node .claude/scripts/build-docs-index.mjs";
293
- scripts["docs:lint"] = "node .claude/scripts/lint-docs-frontmatter.mjs";
294
- }
295
297
  return JSON.stringify(
296
298
  {
297
299
  name: cart.projectName,
@@ -771,10 +773,55 @@ declare module '*.css' {
771
773
  });
772
774
 
773
775
  // src/scaffold/shared/claude-setup.ts
774
- var STATUS_ENUM, LANG_ENUM, docsTemplateMdTemplate, docsReadmeTemplate, docsIndexPlaceholderTemplate, docsSharedMjsTemplate, buildDocsIndexMjsTemplate, lintDocsFrontmatterMjsTemplate, docsFirstReminderShTemplate, claudeSettingsTemplate, docsSkillTemplate, docsWriterAgentTemplate, agentMemorySeedTemplate, buildClaudeFileMap;
776
+ var AGENTS, TOOL_OVERRIDES, agentTools, agentMemoryFrontmatter, claudeHarnessTableTemplate, STATUS_ENUM, LANG_ENUM, docsTemplateMdTemplate, docsReadmeTemplate, docsIndexPlaceholderTemplate, docsSharedMjsTemplate, buildDocsIndexMjsTemplate, lintDocsFrontmatterMjsTemplate, docsFirstReminderShTemplate, TEST_WRITER_DEF, agentGuardMjsTemplate, claudeSettingsTemplate, docsSkillTemplate, docsWriterAgentTemplate, plannerAgentTemplate, advisorAgentTemplate, scoutAgentTemplate, plansReadmeTemplate, backlogReadmeTemplate, agentMemorySeedTemplate, validateStructureMjsTemplate, validatePlansMjsTemplate, buildClaudeFileMap;
775
777
  var init_claude_setup = __esm({
776
778
  "src/scaffold/shared/claude-setup.ts"() {
777
779
  "use strict";
780
+ AGENTS = [
781
+ {
782
+ name: "dev",
783
+ model: "sonnet",
784
+ writeScope: ["src/", "package.json", "tsconfig.json", "vite.config.ts", "biome.json", "eslint.config.js", ".github/"],
785
+ memory: true
786
+ },
787
+ {
788
+ name: "docs-writer",
789
+ model: "haiku",
790
+ writeScope: ["docs/"],
791
+ memory: true
792
+ },
793
+ {
794
+ name: "planner",
795
+ model: "sonnet",
796
+ writeScope: ["plans/"],
797
+ memory: true
798
+ },
799
+ {
800
+ name: "advisor",
801
+ model: "opus",
802
+ writeScope: [],
803
+ memory: true
804
+ },
805
+ {
806
+ name: "scout",
807
+ model: "haiku",
808
+ writeScope: [],
809
+ memory: false
810
+ }
811
+ ];
812
+ TOOL_OVERRIDES = {
813
+ advisor: "Read, Grep, Glob, Bash, WebFetch, WebSearch, Skill, TodoWrite",
814
+ scout: "Read, Grep, Glob, Skill"
815
+ };
816
+ agentTools = (agent) => {
817
+ if (TOOL_OVERRIDES[agent.name]) return TOOL_OVERRIDES[agent.name];
818
+ return agent.writeScope.length > 0 ? "Read, Grep, Glob, Write, Edit, Skill, TodoWrite" : "Read, Grep, Glob, Skill";
819
+ };
820
+ agentMemoryFrontmatter = (agent) => agent.memory ? "\nmemory: project" : "";
821
+ claudeHarnessTableTemplate = () => `| Brainstorming / trade-off analysis / "what's the best approach?" before any change | \`advisor\` | read-only; deepest source mental model; recommends, never edits |
822
+ | Quick factual lookup about the code/docs (answer + \`path:line\`) | \`scout\` | read-only; cheap; for facts, not design reasoning |
823
+ | Decomposing a story into a resumable plan | \`planner\` | owns \`plans/\`; writes phase files only, never code |
824
+ | Analyzing requirements, writing/updating feature docs | \`docs-writer\` | owns \`docs/\`; rebuilds INDEX.md after every change |`;
778
825
  STATUS_ENUM = ["active", "draft", "deprecated"];
779
826
  LANG_ENUM = ["en", "vi"];
780
827
  docsTemplateMdTemplate = (flowEnum3, layerEnum3) => `---
@@ -831,14 +878,14 @@ Frontmatter-indexed task docs. \`INDEX.md\` is auto-generated \u2014 never hand-
831
878
 
832
879
  1. Copy \`docs/_template.md\`, fill ALL frontmatter fields (the index and lint are built from it).
833
880
  2. Save to the right directory per the table above.
834
- 3. \`npm run docs:index\` \u2014 regenerates \`INDEX.md\`.
835
- 4. \`npm run docs:lint\` \u2014 must pass before committing.
881
+ 3. \`node .claude/scripts/build-docs-index.mjs\` \u2014 regenerates \`INDEX.md\`.
882
+ 4. \`node .claude/scripts/lint-docs-frontmatter.mjs\` \u2014 must pass before committing.
836
883
  5. Commit the doc and \`INDEX.md\` together.
837
884
  `;
838
885
  docsIndexPlaceholderTemplate = () => `# Docs Index
839
886
 
840
887
  > Auto-generated by \`.claude/scripts/build-docs-index.mjs\` \u2014 do not edit by hand.
841
- > Run \`npm run docs:index\` to regenerate.
888
+ > Run \`node .claude/scripts/build-docs-index.mjs\` to regenerate.
842
889
  `;
843
890
  docsSharedMjsTemplate = (flowEnum3, layerEnum3) => `// Shared helpers for docs tooling \u2014 single source of truth for the frontmatter schema.
844
891
  import { readdirSync, readFileSync } from 'fs';
@@ -903,7 +950,7 @@ export function walkDocs(dir = DOCS_DIR, records = []) {
903
950
  }
904
951
  `;
905
952
  buildDocsIndexMjsTemplate = () => `// Regenerates docs/INDEX.md from frontmatter. Deterministic (sorted) so diffs stay clean.
906
- // Run via: npm run docs:index
953
+ // Run via: node .claude/scripts/build-docs-index.mjs
907
954
  import { writeFileSync } from 'fs';
908
955
  import { join } from 'path';
909
956
  import { walkDocs, DOCS_DIR } from './_docs-shared.mjs';
@@ -981,7 +1028,7 @@ writeFileSync(join(DOCS_DIR, 'INDEX.md'), lines.join('\\n').replace(/\\n+$/, '\\
981
1028
  console.log('docs/INDEX.md updated \u2014 ' + records.length + ' doc(s), ' + withFm + ' with frontmatter.');
982
1029
  `;
983
1030
  lintDocsFrontmatterMjsTemplate = () => `// Validates every doc against the frontmatter schema. Exits non-zero on violation \u2192 CI-ready.
984
- // Run via: npm run docs:lint
1031
+ // Run via: node .claude/scripts/lint-docs-frontmatter.mjs
985
1032
  import { existsSync } from 'fs';
986
1033
  import { join } from 'path';
987
1034
  import { walkDocs, ENUMS, REQUIRED_FIELDS, DOCS_DIR } from './_docs-shared.mjs';
@@ -1052,6 +1099,88 @@ EOF
1052
1099
  fi
1053
1100
  exit 0
1054
1101
  `;
1102
+ TEST_WRITER_DEF = {
1103
+ name: "test-writer",
1104
+ model: "haiku",
1105
+ writeScope: ["src/", "test/", "tests/", "e2e/", "playwright/"],
1106
+ memory: true
1107
+ };
1108
+ agentGuardMjsTemplate = (agents) => {
1109
+ const scopesObj = {};
1110
+ for (const agent of agents) {
1111
+ scopesObj[agent.name] = agent.writeScope;
1112
+ }
1113
+ const scopesJson = JSON.stringify(scopesObj, null, 2);
1114
+ return `// PreToolUse guard \u2014 registry-driven path enforcement.
1115
+ // Generated from the AGENTS registry; do not edit by hand.
1116
+ // Each agent may only write to its declared writeScope prefixes plus its own
1117
+ // .claude/agent-memory/<name>/ directory (implicit, added at runtime below).
1118
+ // Agents NOT in WRITE_SCOPES (unknown agents, main thread) pass through.
1119
+ // NOTE: writeScope is a heuristic backstop, not a precise ACL. Projects that
1120
+ // place test files under src/__tests__ instead of test/ or tests/ will need to
1121
+ // extend the test-writer scope \u2014 this guard is lenient for dev rather than tight.
1122
+ import { readFileSync } from 'fs';
1123
+
1124
+ const WRITE_SCOPES = ${scopesJson};
1125
+
1126
+ let payload;
1127
+ try {
1128
+ payload = JSON.parse(readFileSync(0, 'utf-8'));
1129
+ } catch {
1130
+ process.exit(0);
1131
+ }
1132
+
1133
+ const agentType = payload.agent_type;
1134
+ // Unknown agent or main thread \u2014 pass through.
1135
+ if (!agentType || !(agentType in WRITE_SCOPES)) process.exit(0);
1136
+
1137
+ const filePath = payload.tool_input?.file_path;
1138
+ if (!filePath) process.exit(0);
1139
+
1140
+ // Normalize to a project-relative path when an absolute path is given.
1141
+ const cwd = payload.cwd ?? '';
1142
+ let rel = filePath;
1143
+ if (filePath.startsWith('/') && cwd && filePath.startsWith(cwd + '/')) {
1144
+ rel = filePath.slice(cwd.length + 1);
1145
+ }
1146
+
1147
+ const allowedPrefixes = WRITE_SCOPES[agentType];
1148
+ const memoryPrefix = \`.claude/agent-memory/\${agentType}/\`;
1149
+
1150
+ // Always allow an agent to write its own memory dir (even read-only agents).
1151
+ if (rel.startsWith(memoryPrefix)) process.exit(0);
1152
+
1153
+ // Read-only agents (empty writeScope): deny all other writes with a specific message.
1154
+ if (allowedPrefixes.length === 0) {
1155
+ process.stdout.write(
1156
+ JSON.stringify({
1157
+ hookSpecificOutput: {
1158
+ hookEventName: 'PreToolUse',
1159
+ permissionDecision: 'deny',
1160
+ permissionDecisionReason: \`read-only agent may not write any file. \${agentType} attempted to write \${filePath}. Route implementation to the dev agent instead.\`,
1161
+ },
1162
+ })
1163
+ );
1164
+ process.exit(0);
1165
+ }
1166
+
1167
+ // Allow if path is within the agent's declared scope.
1168
+ const allowed = allowedPrefixes.some((prefix) => rel.startsWith(prefix));
1169
+
1170
+ if (allowed) process.exit(0);
1171
+
1172
+ process.stdout.write(
1173
+ JSON.stringify({
1174
+ hookSpecificOutput: {
1175
+ hookEventName: 'PreToolUse',
1176
+ permissionDecision: 'deny',
1177
+ permissionDecisionReason: \`\${agentType} may only write under [\${allowedPrefixes.join(', ')}] (and \${memoryPrefix}). Blocked write to \${filePath}. Route to the correct agent instead.\`,
1178
+ },
1179
+ })
1180
+ );
1181
+ process.exit(0);
1182
+ `;
1183
+ };
1055
1184
  claudeSettingsTemplate = () => JSON.stringify(
1056
1185
  {
1057
1186
  permissions: {
@@ -1078,6 +1207,17 @@ exit 0
1078
1207
  }
1079
1208
  ]
1080
1209
  }
1210
+ ],
1211
+ PreToolUse: [
1212
+ {
1213
+ matcher: "Write|Edit|MultiEdit",
1214
+ hooks: [
1215
+ {
1216
+ type: "command",
1217
+ command: 'node "$CLAUDE_PROJECT_DIR/.claude/scripts/agent-guard.mjs"'
1218
+ }
1219
+ ]
1220
+ }
1081
1221
  ]
1082
1222
  },
1083
1223
  sandbox: {
@@ -1087,8 +1227,8 @@ exit 0
1087
1227
  null,
1088
1228
  2
1089
1229
  );
1090
- docsSkillTemplate = (projectName, slug, flowEnum3, layerEnum3) => `---
1091
- name: ${slug}-docs
1230
+ docsSkillTemplate = (projectName, slug4, flowEnum3, layerEnum3) => `---
1231
+ name: ${slug4}-docs
1092
1232
  description: How to find and write knowledge-base docs for ${projectName}. Use when asked "is there a doc about\u2026", "explain the X feature/flow", "document this", "write a spec", or the Vietnamese equivalents ("c\xF3 t\xE0i li\u1EC7u v\u1EC1\u2026", "gi\u1EA3i th\xEDch flow\u2026", "vi\u1EBFt docs/spec\u2026"). Also use before modifying any documented feature.
1093
1233
  ---
1094
1234
 
@@ -1112,14 +1252,14 @@ description: How to find and write knowledge-base docs for ${projectName}. Use w
1112
1252
  3. Keywords: lowercase, prefer real symbol/file names an engineer would grep for.
1113
1253
  4. Rebuild + validate, then commit doc and INDEX.md together:
1114
1254
  \`\`\`bash
1115
- npm run docs:index && npm run docs:lint
1255
+ node .claude/scripts/build-docs-index.mjs && node .claude/scripts/lint-docs-frontmatter.mjs
1116
1256
  \`\`\`
1117
1257
  `;
1118
- docsWriterAgentTemplate = (projectName, slug) => `---
1258
+ docsWriterAgentTemplate = (projectName, slug4, agent) => `---
1119
1259
  name: docs-writer
1120
1260
  description: "Documentation agent for ${projectName} \u2014 analyzes requirements from any source (conversation, tickets, Slack) and maintains docs/. <example>user: 'Here are the requirements for the profile page, write them up' \u2192 docs-writer <commentary>synthesizing requirements into a feature spec</commentary></example> <example>user: 'We just finished the auth refactor, document what changed' \u2192 docs-writer <commentary>post-task knowledge capture</commentary></example> <example>user: 'Update the home spec \u2014 the hero section was redesigned' \u2192 docs-writer <commentary>doc maintenance</commentary></example>"
1121
- model: haiku
1122
- memory: project
1261
+ model: ${agent.model}${agentMemoryFrontmatter(agent)}
1262
+ tools: ${agentTools(agent)}
1123
1263
  ---
1124
1264
 
1125
1265
  You are the documentation agent for ${projectName}. You own \`docs/\`.
@@ -1128,7 +1268,7 @@ You are the documentation agent for ${projectName}. You own \`docs/\`.
1128
1268
 
1129
1269
  1. Read \`.claude/agent-memory/docs-writer/MEMORY.md\`.
1130
1270
  2. Read \`docs/README.md\` (placement + naming rules) and \`docs/INDEX.md\` (what already exists).
1131
- 3. Load the \`${slug}-docs\` skill.
1271
+ 3. Load the \`${slug4}-docs\` skill.
1132
1272
 
1133
1273
  ## Workflow
1134
1274
 
@@ -1136,7 +1276,7 @@ You are the documentation agent for ${projectName}. You own \`docs/\`.
1136
1276
  2. Check INDEX.md for an existing doc to update before creating a new one.
1137
1277
  3. Copy \`docs/_template.md\`; fill ALL frontmatter fields (feature, flow, layer, status, lang, keywords, updated). Specs describe WHAT, not HOW.
1138
1278
  4. Save per docs/README.md rules: \`docs/features/<feature>/<feature>.spec.en.md\` for feature specs, \`<topic>.en.md\` for findings, \`docs/architecture/\` for cross-cutting topics.
1139
- 5. Run \`npm run docs:index\` then \`npm run docs:lint\` \u2014 both must succeed.
1279
+ 5. Run \`node .claude/scripts/build-docs-index.mjs\` then \`node .claude/scripts/lint-docs-frontmatter.mjs\` \u2014 both must succeed.
1140
1280
  6. If the feature introduces new domain nouns, add them to the \`trigger\` list in \`.claude/scripts/docs-first-reminder.sh\`.
1141
1281
  7. Report created/updated paths. Append lessons to \`.claude/agent-memory/docs-writer/MEMORY.md\`.
1142
1282
 
@@ -1145,28 +1285,675 @@ You are the documentation agent for ${projectName}. You own \`docs/\`.
1145
1285
  - Never hand-edit \`docs/INDEX.md\`.
1146
1286
  - Never edit application code \u2014 you write markdown and run the docs scripts only.
1147
1287
  - Never commit or push.
1288
+ `;
1289
+ plannerAgentTemplate = (projectName, agent) => `---
1290
+ name: planner
1291
+ description: "Planning agent for ${projectName} \u2014 analyzes a request/story and produces a professional, detailed, resumable implementation plan under plans/. Splits work into phase files so a failure in one phase never breaks the whole flow; execution can resume from the unfinished phase. <example>user: 'Break this story into a step-by-step plan we can resume' \u2192 planner <commentary>resumable multi-phase plan</commentary></example> <example>user: 'Plan the rollout for the new feature' \u2192 planner <commentary>decompose a large feature into ordered, verifiable phases</commentary></example> <example>user: 'Fix this small bug' \u2192 dev, NOT planner <commentary>small, single-pass change \u2014 code directly</commentary></example> <example>user: 'Write a spec for X' \u2192 docs-writer, NOT planner <commentary>specs describe WHAT; plans describe HOW/when</commentary></example>"
1292
+ model: ${agent.model}${agentMemoryFrontmatter(agent)}
1293
+ tools: ${agentTools(agent)}
1294
+ ---
1295
+
1296
+ You are the planning agent for ${projectName}. You own \`plans/\`. You produce the plan; the \`dev\` agent executes it.
1297
+
1298
+ ## Onboarding protocol (in order, before writing any plan)
1299
+
1300
+ 1. Read \`.claude/agent-memory/planner/MEMORY.md\` \u2014 accumulated planning gotchas.
1301
+ 2. Read \`plans/README.md\` \u2014 folder layout, file naming, and frontmatter contract.
1302
+ 3. Read \`docs/INDEX.md\` and the relevant \`docs/features/<feature>/\` spec(s) \u2014 the spec is your source of truth for WHAT. If no relevant spec exists, STOP and tell the user to run the docs-writer agent first.
1303
+ 4. Skim the code under change only enough to anchor the plan in real file paths.
1304
+
1305
+ ## Workflow
1306
+
1307
+ 1. Restate the request and surface ambiguity. If interpretations conflict, ask \u2014 do not guess.
1308
+ 2. Decompose the work into the **minimum** set of ordered phases. Each phase is independently completable and leaves the repo in a working state. Do not invent speculative phases.
1309
+ 3. Write \`plans/<slug>/00-overview.md\` (goal, scope, non-goals, and the **Ordered phases** tracker table \u2014 see below) and one \`plans/<slug>/NN-<phase>.md\` per phase.
1310
+ 4. Every phase file MUST be resumable on its own \u2014 see Phase file contract. Reference real source paths and the relevant \`docs/\` spec.
1311
+ 5. Report the created plan paths and the recommended starting phase. Append durable planning lessons to \`.claude/agent-memory/planner/MEMORY.md\`.
1312
+
1313
+ ## Phase file contract (this is what makes plans resumable)
1314
+
1315
+ Each \`NN-<phase>.md\` begins with frontmatter:
1316
+
1317
+ \`\`\`
1318
+ ---
1319
+ phase: NN
1320
+ title: <short title>
1321
+ status: pending # pending | in-progress | done | blocked
1322
+ depends_on: [<NN>, ...]
1323
+ ---
1324
+ \`\`\`
1325
+
1326
+ Body, in this order:
1327
+ - **Goal** \u2014 one sentence; what "done" means.
1328
+ - **Steps** \u2014 a \`- [ ]\` checklist; each item is a concrete, single action tied to a real path.
1329
+ - **Verify** \u2014 explicit, runnable success criteria. A phase is only \`done\` when these pass.
1330
+ - **Notes / risks** \u2014 gotchas, rollback hints.
1331
+
1332
+ The executor resumes by finding the first phase whose \`status\` is not \`done\` and continuing at its first unchecked step.
1333
+
1334
+ ## Progress tracker (00-overview.md is the single index)
1335
+
1336
+ \`00-overview.md\` is the only tracker \u2014 never add a separate \`index.md\` per folder. End it with an **Ordered phases** table that doubles as the resume-from view:
1337
+
1338
+ \`\`\`
1339
+ | # | Phase | Status | Steps | Updated |
1340
+ |---|---|---|---|---|
1341
+ | 01 | <phase> | pending | 0/<n> | <ISO date> |
1342
+ \`\`\`
1343
+
1344
+ - **Status** mirrors each phase file's frontmatter \`status\`; **Steps** is \`<checked>/<total>\` of that phase's \`- [ ]\` checklist; **Updated** is the ISO date the row last changed.
1345
+ - This table is the one place that aggregates across phases. Keep it in sync whenever a phase's status, step count, or block state changes; a \`blocked\` row carries its \`backlog/<id>\` link inline.
1346
+
1347
+ ## Backlog integration
1348
+
1349
+ - A phase that the executor parks gets \`status: blocked\` and a link to a \`backlog/<id>\` entry (see \`backlog/README.md\`). Treat blocked phases as paused, not failed \u2014 they don't invalidate completed work.
1350
+ - When a backlog item is revived, read it for context and fold its **Suggested direction** into new or amended phases here. Backlog holds context; \`plans/\` holds the executable plan.
1351
+
1352
+ ## Hard rules
1353
+
1354
+ - Write ONLY under \`plans/\` (and your own \`.claude/agent-memory/planner/\`). Never edit source code, never write code, never run/modify the build. Your toolset has no Bash, and a PreToolUse hook hard-blocks any write outside \`plans/\` \u2014 if you feel the urge to implement, produce the plan and hand off to \`dev\` instead.
1355
+ - Never write feature specs \u2014 that is docs-writer's job (specs = WHAT, plans = HOW/when). Flag when a spec is missing instead of writing one.
1356
+ - Plans are consumable artifacts: keep them concrete and current, not aspirational prose.
1357
+ - Never edit \`docs/INDEX.md\` or any docs/ file.
1358
+ - Never commit or push \u2014 a human does that.
1359
+ - Non-blocking follow-up work (spec gaps, deferred tasks, adjacent improvements) must be filed as a \`backlog/<NNNN>-<slug>.md\` entry (see \`backlog/README.md\`). Do NOT leave follow-up work as prose in a plan's overview or phase files \u2014 prose in done plans is archived and lost.
1360
+ `;
1361
+ advisorAgentTemplate = (projectName, slug4, agent) => `---
1362
+ name: advisor
1363
+ description: "Read-only brainstorming & advisory agent for ${projectName} \u2014 the engineer who understands the source logic most deeply. Reads the code, reasons about trade-offs, and returns the best, most optimal recommendation; never edits anything. <example>user: 'Should this be one flat interface or split apart? Talk me through it' \u2192 advisor <commentary>design brainstorm / trade-off analysis, no code change</commentary></example> <example>user: 'What is the cleanest way to add this option without bloating things?' \u2192 advisor <commentary>wants the optimal approach before any implementation</commentary></example> <example>user: 'Add the option' \u2192 dev, NOT advisor <commentary>actual implementation belongs to dev</commentary></example> <example>user: 'Break this into a resumable plan' \u2192 planner, NOT advisor <commentary>plan artifacts belong to planner</commentary></example>"
1364
+ model: ${agent.model}${agentMemoryFrontmatter(agent)}
1365
+ tools: ${agentTools(agent)}
1366
+ ---
1367
+
1368
+ You are the advisor for ${projectName}. You are the engineer who holds the **deepest, most accurate mental model of the source** and the trade-offs baked into it. Your job is to read, reason, and recommend \u2014 to brainstorm with the user and hand them the single best path forward. You do not write code, docs, or plans; \`dev\`, \`docs-writer\`, and \`planner\` do that after you've clarified the thinking.
1369
+
1370
+ ## Onboarding protocol (in order, before advising)
1371
+
1372
+ 1. Read \`.claude/agent-memory/advisor/MEMORY.md\` \u2014 accumulated architectural insights and recurring trade-offs.
1373
+ 2. Read \`docs/INDEX.md\` and the relevant \`docs/features/<feature>/\` spec(s) for the area in question \u2014 the spec is the source of truth for WHAT.
1374
+ 3. Load the \`${slug4}-conventions\` skill for the project's patterns and rules.
1375
+ 4. Read the actual source under discussion. Never advise from memory or assumption when the file is one Read away \u2014 ground every claim in a real path/line.
1376
+
1377
+ ## Workflow
1378
+
1379
+ 1. Restate the question and the real goal behind it. If interpretations conflict, surface them \u2014 don't silently pick one.
1380
+ 2. Read enough source to be certain. Trace the real data flow rather than guessing at it.
1381
+ 3. Brainstorm: lay out the viable options with their concrete trade-offs (simplicity, coupling, correctness, maintenance cost).
1382
+ 4. **Give a recommendation, not a survey.** Name the single best option, say why it wins, and cite the files/lines that justify it. Note the cheapest next step and which agent owns it (\`dev\` / \`planner\` / \`docs-writer\`).
1383
+ 5. Append durable architectural insights to \`.claude/agent-memory/advisor/MEMORY.md\`.
1384
+
1385
+ ## What "best advice" means here
1386
+
1387
+ - Favor the **minimum** change that solves the problem. Call out over-engineering explicitly.
1388
+ - Optimal \u2260 clever. Prefer the option a senior engineer would call obvious over the one that's impressive.
1389
+ - When the user's instinct is wrong, say so plainly and explain the cost \u2014 pushing back is the job, not friction.
1390
+
1391
+ ## Hard rules
1392
+
1393
+ - **Read-only. Never edit, create, or delete any file** \u2014 not source, not \`docs/\`, not \`plans/\`, not \`backlog/\`. The only file you ever write is your own \`.claude/agent-memory/advisor/MEMORY.md\` (insights). If a change is warranted, recommend it and route it to the owning agent.
1394
+ - Never run the build, never commit, never push.
1395
+ - Ground claims in real paths/lines; if you haven't read it, say so instead of asserting.
1396
+ - Hand off, don't implement: end with a clear, actionable recommendation and who should execute it.
1397
+ `;
1398
+ scoutAgentTemplate = (projectName, slug4, agent) => `---
1399
+ name: scout
1400
+ description: "Fast, cheap read-only Q&A & lookup agent for ${projectName} \u2014 reads, synthesizes, and answers in a few sentences with path:line citations. Use for factual questions about the codebase/docs, not design reasoning (\u2192 advisor) or implementation (\u2192 dev). <example>user: 'What version does this project pin for X?' \u2192 scout <commentary>factual lookup, answer + cite the source</commentary></example> <example>user: 'Is there a spec for feature X yet?' \u2192 scout <commentary>doc existence check via docs/INDEX.md</commentary></example> <example>user: 'Should this be split apart? Talk me through the trade-offs' \u2192 advisor, NOT scout <commentary>design reasoning belongs to advisor</commentary></example> <example>user: 'Add the option' \u2192 dev, NOT scout <commentary>implementation belongs to dev</commentary></example>"
1401
+ model: ${agent.model}
1402
+ tools: ${agentTools(agent)}
1403
+ ---
1404
+
1405
+ You are scout for ${projectName}. You answer factual questions about the codebase and docs **quickly and cheaply** \u2014 read what you need, synthesize, and reply in a few sentences. You do not reason about design trade-offs (that is \`advisor\`), and you never edit anything (that is \`dev\` / \`docs-writer\` / \`planner\`).
1406
+
1407
+ ## Lookup protocol (DOCS-FIRST)
1408
+
1409
+ 1. If the question touches a documented feature, start at \`docs/INDEX.md\` and read the relevant \`docs/features/<feature>/\` spec before opening source. Load the \`${slug4}-docs\` skill when you need to locate a doc.
1410
+ 2. Otherwise grep/glob to the right file fast. Trace the real data flow rather than guessing.
1411
+ 3. Read only the lines you need to be certain \u2014 excerpts, not whole files. Stop as soon as you can answer.
1412
+
1413
+ ## Answer style
1414
+
1415
+ - **Lead with the answer**, then a one-line why/where. Keep it to a few sentences.
1416
+ - **Always cite** the real \`path:line\` you got it from. If you didn't read it, say so \u2014 never assert from memory.
1417
+ - If the question is ambiguous, ask one short clarifying question instead of guessing.
1418
+
1419
+ ## Hard rules
1420
+
1421
+ - **Read-only.** Never create, edit, or delete any file. No Bash, no build, no commit.
1422
+ - **Stay in your lane \u2014 hand off, don't expand scope:**
1423
+ - Design / trade-off / "what's the best approach?" \u2192 recommend \`advisor\`.
1424
+ - Implementation, bug fix, new option \u2192 recommend \`dev\`.
1425
+ - Writing or updating docs \u2192 recommend \`docs-writer\`; multi-phase plan \u2192 \`planner\`.
1426
+ - If answering would require running code or reasoning through a non-trivial design decision, stop and route it rather than overreaching.
1427
+ `;
1428
+ plansReadmeTemplate = () => `# plans/
1429
+
1430
+ Resumable, phase-by-phase implementation plans authored by the \`planner\` agent and executed by the \`dev\` agent.
1431
+
1432
+ A plan is a **consumable** artifact (short-lived, kept in sync with reality), distinct from \`docs/\` which holds long-lived feature specs (WHAT). Plans describe **HOW and in what order** work gets done.
1433
+
1434
+ ## Layout
1435
+
1436
+ \`\`\`
1437
+ plans/
1438
+ <slug>/
1439
+ 00-overview.md goal, scope, non-goals, + the progress tracker (see below)
1440
+ 01-<phase>.md one file per phase
1441
+ 02-<phase>.md
1442
+ ...
1443
+ \`\`\`
1444
+
1445
+ - \`<slug>\` is kebab-case, derived from the feature/story.
1446
+ - Phases are numbered \`NN-\` so they sort in execution order.
1447
+
1448
+ \`00-overview.md\` is the single tracker for the plan \u2014 there is **no separate \`index.md\`**. Its **Ordered phases** table is the at-a-glance "where are we" view; each phase file's \`status\` frontmatter is the per-phase source of truth. The table aggregates across phases, so keep it in sync whenever a phase's status or step count changes.
1449
+
1450
+ ## Progress tracker (the Ordered phases table)
1451
+
1452
+ \`00-overview.md\` ends with this table \u2014 it is what an executor reads first to know where to resume:
1453
+
1454
+ \`\`\`
1455
+ | # | Phase | Status | Steps | Updated |
1456
+ |---|---|---|---|---|
1457
+ | 01 | <phase> | done | 5/5 | 2026-01-01 |
1458
+ | 02 | <phase> | in-progress | 2/6 | 2026-01-01 |
1459
+ \`\`\`
1460
+
1461
+ - **Status** \u2014 mirrors the phase file's frontmatter \`status\` (\`pending | in-progress | done | blocked\`).
1462
+ - **Steps** \u2014 \`<checked>/<total>\` of the phase's \`- [ ]\` checklist; the quick "how far in" signal.
1463
+ - **Updated** \u2014 ISO date the row last changed.
1464
+
1465
+ A \`blocked\` row should also carry the \`backlog/<id>\` link inline (e.g. \`blocked \u2192 backlog/0003\`).
1466
+
1467
+ ## Phase file frontmatter
1468
+
1469
+ \`\`\`
1470
+ ---
1471
+ phase: NN
1472
+ title: <short title>
1473
+ status: pending # pending | in-progress | done | blocked
1474
+ depends_on: [<NN>, ...]
1475
+ ---
1476
+ \`\`\`
1477
+
1478
+ Body sections, in order: **Goal**, **Steps** (\`- [ ]\` checklist), **Verify** (runnable success criteria), **Notes / risks**.
1479
+
1480
+ ## Resuming a plan
1481
+
1482
+ 1. Open \`00-overview.md\` and read the **Ordered phases** table for overall progress.
1483
+ 2. Find the first phase whose \`status\` is not \`done\`; its **Steps** cell shows how far in it is.
1484
+ 3. Continue at its first unchecked \`- [ ]\` step in the phase file.
1485
+ 4. After each step, update the phase's **Steps** count and **Updated** date in the table. When a phase's \`Verify\` passes, set its frontmatter \`status: done\` and flip the table row to \`done\`.
1486
+
1487
+ A failure in one phase never invalidates completed phases \u2014 fix and resume from the unfinished phase.
1488
+
1489
+ ## Blocked phases \u2192 backlog
1490
+
1491
+ If a phase can't proceed and the cause isn't fixable right now (missing info, needs a user decision, environment/out-of-scope), don't loop. Apply the **park rule** in \`backlog/README.md\`: set the phase \`status: blocked\`, file a \`backlog/<id>\` entry, link both ways (\`[[backlog/<id>]]\` from the phase; the entry's \`source:\` points back to the phase file), then move on to the next workable phase.
1492
+
1493
+ ## Plan lifecycle and archival
1494
+
1495
+ When all phases are \`done\`, the plan becomes a completed artifact \u2014 it can either:
1496
+ - **Remain in \`plans/\`** as historical record (git preserves it; useful for auditing how the work was actually decomposed).
1497
+ - **Be archived** to \`plans/.archive/<slug>/\` (keep it searchable but out of the active directory).
1498
+ - **Be deleted** if context is not valuable (rare; prefer archival so git history is preserved).
1499
+
1500
+ Choose based on project norms. The key: **plans are owned by whoever executed them** (usually \`dev\`) and the decision to archive/delete is theirs. Do not leave a stale plan in \`plans/\` \u2014 either keep it active (if next phases are coming) or archive it. The \`00-overview.md\` progress table is the arbiter: if all rows are \`done\` and no new work is queued, the plan is complete.
1501
+ `;
1502
+ backlogReadmeTemplate = () => `# backlog/
1503
+
1504
+ Unfinished work: blockers that can't be solved right now, technical debt, or anything **deliberately deferred** so it doesn't derail the current flow.
1505
+
1506
+ How it differs from \`docs/\` and \`plans/\`:
1507
+
1508
+ | | \`docs/\` | \`plans/\` | \`backlog/\` |
1509
+ |---|---|---|---|
1510
+ | Nature | WHAT \u2014 long-lived spec | HOW/when \u2014 phases to run | deferred work / blockers / tech debt |
1511
+ | Lifecycle | long-lived | consumable, deleted when done | append-only log, lives until \`resolved\` |
1512
+ | Author | docs-writer | planner | any agent that hits a blocker (usually \`dev\`) |
1513
+
1514
+ A backlog item is **not** a plan. When an item is revived, \`planner\` reads it and produces new phases under \`plans/\` \u2014 backlog holds context, not the executable plan.
1515
+
1516
+ ## Park rule (this is what stops the token-wasting loop)
1517
+
1518
+ Applies while executing a step/phase and hitting a problem:
1519
+
1520
+ > If a step **fails twice** and the cause is **not fixable right now** \u2014 missing info, needs a user decision, environment error, or out of scope \u2014 then **STOP, do not retry a third time**:
1521
+ > 1. Set the owning phase \`status: blocked\` (see \`plans/README.md\`).
1522
+ > 2. File a new entry in \`backlog/\` (template below); its **Tried** section must list what already failed so it isn't repeated.
1523
+ > 3. Link both ways: the phase file points to \`backlog/<id>\`, the entry points back to the phase.
1524
+ > 4. Move on to the next workable phase/task. Tell the user it was parked \u2014 don't silently skip it.
1525
+
1526
+ If the error is fixable on the spot, just fix it \u2014 this rule is only for real blockers.
1527
+
1528
+ ## Layout
1529
+
1530
+ One item = one file: \`backlog/<NNN>-<slug>.md\` (\`NNN\` ascending so items sort and are easy to reference, e.g. "backlog/004").
1531
+
1532
+ ## Frontmatter
1533
+
1534
+ \`\`\`
1535
+ ---
1536
+ id: NNN
1537
+ title: <short title>
1538
+ status: open # open | resolved | wontfix
1539
+ source: plans/<slug>/NN-<phase>.md # or "conversation", or the originating file path
1540
+ severity: low # low | medium | high
1541
+ created: YYYY-MM-DD
1542
+ ---
1543
+ \`\`\`
1544
+
1545
+ Body, in order:
1546
+
1547
+ - **Symptom** \u2014 what happened and where (real file paths).
1548
+ - **Tried** \u2014 what was run/changed and how it failed. The most important section: prevents repeating failed attempts.
1549
+ - **Why parked** \u2014 why it can't be solved now (missing info / needs user / environment / out of scope).
1550
+ - **Suggested direction** \u2014 the next step if any, or the question the user needs to answer.
1551
+
1552
+ Link to other phases/specs/items with \`[[path]]\`.
1553
+
1554
+ ## Lifecycle
1555
+
1556
+ - When solved: set \`status: resolved\` and add a one-line conclusion at the end of the body \u2014 **don't delete the file** (keep the history). To drop it entirely, set \`status: wontfix\` with a reason.
1557
+ - When revived: hand the context to \`planner\` to create new phases under \`plans/\`; the entry stays \`open\` until the work is actually done.
1148
1558
  `;
1149
1559
  agentMemorySeedTemplate = (agentName) => `# ${agentName} \u2014 Agent Memory
1150
1560
 
1151
- Append durable, non-obvious gotchas and patterns discovered while working \u2014 one bullet each, newest first. Link related docs/ files. Read this file at the start of every session.
1561
+ Append durable, non-obvious gotchas and patterns discovered while working \u2014 one bullet each, newest first. Link related docs/ files. Read this file at the start of every session.
1562
+
1563
+ _(no entries yet)_
1564
+ `;
1565
+ validateStructureMjsTemplate = (agents) => {
1566
+ const scopesObj = {};
1567
+ for (const agent of agents) {
1568
+ scopesObj[agent.name] = agent.writeScope;
1569
+ }
1570
+ const scopesJson = JSON.stringify(scopesObj, null, 2);
1571
+ return `// Mechanically checks derived invariants between the AGENTS registry, emitted
1572
+ // agent .md files, and the guard. Generated from the AGENTS registry at scaffold
1573
+ // time \u2014 do not edit by hand.
1574
+ // Run via: node .claude/scripts/validate-structure.mjs
1575
+ import { readdirSync, readFileSync } from 'fs';
1576
+ import { join } from 'path';
1577
+
1578
+ // Baked at scaffold time from the AGENTS registry.
1579
+ const WRITE_SCOPES = ${scopesJson};
1580
+
1581
+ // Minimal frontmatter parser (no external deps \u2014 inlined from _docs-shared.mjs pattern).
1582
+ function parseFrontmatter(content) {
1583
+ const match = content.match(/^---\\n([\\s\\S]*?)\\n---/);
1584
+ if (!match) return null;
1585
+ const meta = {};
1586
+ for (const line of match[1].split('\\n')) {
1587
+ const idx = line.indexOf(':');
1588
+ if (idx === -1) continue;
1589
+ const key = line.slice(0, idx).trim();
1590
+ let value = line.slice(idx + 1).trim();
1591
+ if (!key) continue;
1592
+ // Strip inline comments.
1593
+ value = value.replace(/\\s+#.*$/, '').trim();
1594
+ if (value.startsWith('[') && value.endsWith(']')) {
1595
+ const inner = value.slice(1, -1).trim();
1596
+ meta[key] = inner === ''
1597
+ ? []
1598
+ : inner.split(',').map((item) => item.trim().replace(/^['"]|['"]$/g, ''));
1599
+ } else {
1600
+ meta[key] = value.replace(/^['"]|['"]$/g, '');
1601
+ }
1602
+ }
1603
+ return meta;
1604
+ }
1605
+
1606
+ const AGENTS_DIR = '.claude/agents';
1607
+ const errors = [];
1608
+
1609
+ // 1. Read and parse all agent .md files.
1610
+ let agentFiles;
1611
+ try {
1612
+ agentFiles = readdirSync(AGENTS_DIR).filter((f) => f.endsWith('.md'));
1613
+ } catch {
1614
+ console.error('validate-structure: cannot read ' + AGENTS_DIR);
1615
+ process.exit(1);
1616
+ }
1617
+
1618
+ const parsed = [];
1619
+ for (const file of agentFiles) {
1620
+ const fullPath = join(AGENTS_DIR, file);
1621
+ const content = readFileSync(fullPath, 'utf-8');
1622
+ const fm = parseFrontmatter(content);
1623
+ if (!fm) {
1624
+ errors.push(file + ': missing frontmatter block');
1625
+ continue;
1626
+ }
1627
+ const name = fm.name ?? file.replace(/\\.md$/, '');
1628
+ const toolsRaw = fm.tools ?? '';
1629
+ parsed.push({ file, name, toolsRaw });
1630
+ }
1631
+
1632
+ // 2. Check read-only constraint: if writeScope is empty, tools must not include Write or Edit.
1633
+ for (const { file, name, toolsRaw } of parsed) {
1634
+ if (!(name in WRITE_SCOPES)) continue; // unknown agent \u2014 skip
1635
+ const scope = WRITE_SCOPES[name];
1636
+ if (scope.length > 0) continue; // write-capable agent \u2014 constraint does not apply
1637
+ // Read-only agent: assert no Write/Edit in tools list.
1638
+ const tools = toolsRaw.split(',').map((t) => t.trim());
1639
+ for (const tool of tools) {
1640
+ if (tool === 'Write' || tool === 'Edit') {
1641
+ errors.push(
1642
+ file + ': read-only agent "' + name + '" must not have "' + tool +
1643
+ '" in its tools: list (writeScope is empty)'
1644
+ );
1645
+ }
1646
+ }
1647
+ }
1648
+
1649
+ // 3. Check uniqueness of primary owned directories (first element of writeScope).
1650
+ const primaryDirOwners = {};
1651
+ for (const [agentName, scope] of Object.entries(WRITE_SCOPES)) {
1652
+ if (scope.length === 0) continue; // read-only agents have no primary dir
1653
+ const primaryDir = scope[0];
1654
+ if (primaryDirOwners[primaryDir]) {
1655
+ errors.push(
1656
+ 'agents "' + primaryDirOwners[primaryDir] + '" and "' + agentName +
1657
+ '" share the same primary owned directory "' + primaryDir + '"'
1658
+ );
1659
+ } else {
1660
+ primaryDirOwners[primaryDir] = agentName;
1661
+ }
1662
+ }
1663
+
1664
+ if (errors.length > 0) {
1665
+ console.error('validate-structure: failed with ' + errors.length + ' error(s):');
1666
+ for (const error of errors) console.error(' - ' + error);
1667
+ process.exit(1);
1668
+ }
1669
+ console.log('validate-structure: passed.');
1670
+ `;
1671
+ };
1672
+ validatePlansMjsTemplate = () => `// Mechanically checks plan/backlog consistency across four invariants.
1673
+ // Run via: node .claude/scripts/validate-plans.mjs
1674
+ import { readdirSync, readFileSync, existsSync } from 'fs';
1675
+ import { join } from 'path';
1676
+
1677
+ const PLANS_DIR = 'plans';
1678
+ const BACKLOG_DIR = 'backlog';
1679
+
1680
+ // Minimal frontmatter parser \u2014 same pattern as validate-structure.mjs (no external deps).
1681
+ function parseFrontmatter(content) {
1682
+ const match = content.match(/^---\\n([\\s\\S]*?)\\n---/);
1683
+ if (!match) return null;
1684
+ const meta = {};
1685
+ for (const line of match[1].split('\\n')) {
1686
+ const idx = line.indexOf(':');
1687
+ if (idx === -1) continue;
1688
+ const key = line.slice(0, idx).trim();
1689
+ let value = line.slice(idx + 1).trim();
1690
+ if (!key) continue;
1691
+ value = value.replace(/\\s+#.*$/, '').trim();
1692
+ if (value.startsWith('[') && value.endsWith(']')) {
1693
+ const inner = value.slice(1, -1).trim();
1694
+ meta[key] = inner === ''
1695
+ ? []
1696
+ : inner.split(',').map((item) => item.trim().replace(/^['"]|['"]$/g, ''));
1697
+ } else {
1698
+ meta[key] = value.replace(/^['"]|['"]$/g, '');
1699
+ }
1700
+ }
1701
+ return meta;
1702
+ }
1703
+
1704
+ // Parse the "Ordered phases" Markdown table from an overview file body.
1705
+ // Returns array of { num, slug, status } where num is zero-padded (e.g. "01").
1706
+ // Only parses rows from the section headed "## Ordered phases" (case-insensitive).
1707
+ function parseOrderedPhasesTable(content) {
1708
+ const rows = [];
1709
+ // Find the "Ordered phases" section and extract only that table.
1710
+ const sectionMatch = content.match(/##\\s+Ordered phases\\b[\\s\\S]*?(?=\\n##\\s|\\n---\\n|$)/i);
1711
+ if (!sectionMatch) return rows;
1712
+ const section = sectionMatch[0];
1713
+ for (const line of section.split('\\n')) {
1714
+ // Match data rows: | 01 | slug | status | ... |
1715
+ if (!/^\\|\\s*\\d+\\s*\\|/.test(line)) continue;
1716
+ const cells = line.split('|').map((c) => c.trim()).filter(Boolean);
1717
+ if (cells.length < 3) continue;
1718
+ const num = cells[0].padStart(2, '0');
1719
+ const slug = cells[1];
1720
+ const status = cells[2].toLowerCase();
1721
+ rows.push({ num, slug, status });
1722
+ }
1723
+ return rows;
1724
+ }
1725
+
1726
+ // Collect active plan dirs (skip .archive and README.md).
1727
+ function getActivePlanDirs() {
1728
+ let entries;
1729
+ try {
1730
+ entries = readdirSync(PLANS_DIR, { withFileTypes: true });
1731
+ } catch {
1732
+ return [];
1733
+ }
1734
+ return entries
1735
+ .filter((e) => e.isDirectory() && e.name !== '.archive' && !e.name.startsWith('.'))
1736
+ .map((e) => e.name);
1737
+ }
1738
+
1739
+ // Collect all plan dirs including .archive.
1740
+ function getAllPlanDirs() {
1741
+ let entries;
1742
+ try {
1743
+ const archiveEntries = readdirSync(join(PLANS_DIR, '.archive'), { withFileTypes: true });
1744
+ const activeEntries = readdirSync(PLANS_DIR, { withFileTypes: true });
1745
+ return [
1746
+ ...activeEntries.filter((e) => e.isDirectory() && !e.name.startsWith('.')).map((e) => ({ name: e.name, archived: false })),
1747
+ ...archiveEntries.filter((e) => e.isDirectory()).map((e) => ({ name: e.name, archived: true })),
1748
+ ];
1749
+ } catch {
1750
+ return getActivePlanDirs().map((name) => ({ name, archived: false }));
1751
+ }
1752
+ }
1753
+
1754
+ const errors = [];
1755
+ const warnings = [];
1756
+
1757
+ // \u2500\u2500 Check A \u2014 Phase table \u2194 frontmatter status consistency \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1758
+ for (const planDir of getActivePlanDirs()) {
1759
+ const overviewPath = join(PLANS_DIR, planDir, '00-overview.md');
1760
+ if (!existsSync(overviewPath)) continue;
1761
+ const overviewContent = readFileSync(overviewPath, 'utf-8');
1762
+ const rows = parseOrderedPhasesTable(overviewContent);
1763
+ for (const { num, slug, status: tableStatus } of rows) {
1764
+ const phaseFile = join(PLANS_DIR, planDir, \`\${num}-\${slug}.md\`);
1765
+ if (!existsSync(phaseFile)) {
1766
+ warnings.push(\`WARN \${phaseFile}: phase file not found (table row \${num} references it)\`);
1767
+ continue;
1768
+ }
1769
+ const phaseContent = readFileSync(phaseFile, 'utf-8');
1770
+ const fm = parseFrontmatter(phaseContent);
1771
+ if (!fm) {
1772
+ warnings.push(\`WARN \${phaseFile}: missing frontmatter \u2014 cannot compare with table status\`);
1773
+ continue;
1774
+ }
1775
+ const fmStatus = (fm.status ?? '').toLowerCase();
1776
+ if (fmStatus !== tableStatus) {
1777
+ warnings.push(
1778
+ \`WARN \${phaseFile}: phase frontmatter status "\${fmStatus}" but table row says "\${tableStatus}"\`
1779
+ );
1780
+ }
1781
+ }
1782
+ }
1783
+
1784
+ // \u2500\u2500 Check B \u2014 All-done plans not yet archived \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1785
+ for (const planDir of getActivePlanDirs()) {
1786
+ const overviewPath = join(PLANS_DIR, planDir, '00-overview.md');
1787
+ if (!existsSync(overviewPath)) continue;
1788
+ const overviewContent = readFileSync(overviewPath, 'utf-8');
1789
+ const rows = parseOrderedPhasesTable(overviewContent);
1790
+ if (rows.length === 0) continue;
1791
+ if (rows.every(({ status }) => status === 'done')) {
1792
+ warnings.push(
1793
+ \`WARN plans/\${planDir}/00-overview.md: all phases done \u2014 consider archiving to plans/.archive/\`
1794
+ );
1795
+ }
1796
+ }
1797
+
1798
+ // \u2500\u2500 Check C \u2014 Backlog ID uniqueness and sequence \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1799
+ const REQUIRED_BACKLOG_FIELDS = ['id', 'title', 'status', 'source', 'severity', 'created'];
1800
+ const VALID_BACKLOG_STATUSES = ['open', 'resolved', 'wontfix'];
1801
+ const seenIds = new Set();
1802
+
1803
+ let backlogFiles;
1804
+ try {
1805
+ backlogFiles = readdirSync(BACKLOG_DIR).filter((f) => /^\\d{4}-.*\\.md$/.test(f));
1806
+ } catch {
1807
+ backlogFiles = [];
1808
+ }
1809
+
1810
+ for (const file of backlogFiles) {
1811
+ const filePath = join(BACKLOG_DIR, file);
1812
+ const content = readFileSync(filePath, 'utf-8');
1813
+ const fm = parseFrontmatter(content);
1814
+ const prefix = file.slice(0, 4); // "0001"
1815
+
1816
+ if (!fm) {
1817
+ errors.push(\`ERROR backlog/\${file}: missing frontmatter block\`);
1818
+ continue;
1819
+ }
1820
+
1821
+ // Required fields
1822
+ for (const field of REQUIRED_BACKLOG_FIELDS) {
1823
+ if (fm[field] === undefined || fm[field] === '') {
1824
+ errors.push(\`ERROR backlog/\${file}: missing required frontmatter field "\${field}"\`);
1825
+ }
1826
+ }
1827
+
1828
+ // ID uniqueness
1829
+ const id = fm.id ?? '';
1830
+ if (id) {
1831
+ if (seenIds.has(id)) {
1832
+ errors.push(\`ERROR backlog/\${file}: duplicate id "\${id}"\`);
1833
+ } else {
1834
+ seenIds.add(id);
1835
+ }
1836
+
1837
+ // ID/filename prefix match
1838
+ if (id.padStart(4, '0') !== prefix) {
1839
+ errors.push(\`ERROR backlog/\${file}: filename prefix "\${prefix}" does not match id "\${id}"\`);
1840
+ }
1841
+ }
1842
+
1843
+ // Status enum
1844
+ const status = fm.status ?? '';
1845
+ if (status && !VALID_BACKLOG_STATUSES.includes(status)) {
1846
+ errors.push(
1847
+ \`ERROR backlog/\${file}: invalid status "\${status}" (allowed: \${VALID_BACKLOG_STATUSES.join(', ')})\`
1848
+ );
1849
+ }
1850
+ }
1851
+
1852
+ // \u2500\u2500 Check D \u2014 Two-way links between blocked phases and backlog entries \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1853
+ // Collect all phase files (non-overview, non-archive).
1854
+ function collectPhaseFiles() {
1855
+ const result = [];
1856
+ for (const planDir of getActivePlanDirs()) {
1857
+ let files;
1858
+ try {
1859
+ files = readdirSync(join(PLANS_DIR, planDir)).filter(
1860
+ (f) => f.endsWith('.md') && f !== '00-overview.md'
1861
+ );
1862
+ } catch {
1863
+ continue;
1864
+ }
1865
+ for (const f of files) {
1866
+ result.push({ path: join(PLANS_DIR, planDir, f), rel: \`plans/\${planDir}/\${f}\` });
1867
+ }
1868
+ }
1869
+ return result;
1870
+ }
1871
+
1872
+ for (const { path: phaseFilePath, rel } of collectPhaseFiles()) {
1873
+ const content = readFileSync(phaseFilePath, 'utf-8');
1874
+ const fm = parseFrontmatter(content);
1875
+ if (!fm || fm.status !== 'blocked') continue;
1876
+
1877
+ // Look for a backlog link in the body.
1878
+ const bodyMatch = content.match(/^---\\n[\\s\\S]*?\\n---\\n([\\s\\S]*)$/);
1879
+ const body = bodyMatch ? bodyMatch[1] : content;
1880
+ const backlogLinkMatch = body.match(/backlog\\/(\\d{4})/g);
1881
+
1882
+ if (!backlogLinkMatch) {
1883
+ warnings.push(\`WARN \${rel}: status blocked but no backlog link found in body\`);
1884
+ continue;
1885
+ }
1886
+
1887
+ for (const linkStr of backlogLinkMatch) {
1888
+ const id = linkStr.replace('backlog/', '');
1889
+ // Find the backlog file with this prefix.
1890
+ const matchingFile = (backlogFiles ?? []).find((f) => f.startsWith(id));
1891
+ if (!matchingFile) {
1892
+ warnings.push(\`WARN \${rel}: references \${linkStr} but no backlog/\${id}-*.md file found\`);
1893
+ continue;
1894
+ }
1895
+ const backlogContent = readFileSync(join(BACKLOG_DIR, matchingFile), 'utf-8');
1896
+ const backlogFm = parseFrontmatter(backlogContent);
1897
+ const source = backlogFm?.source ?? '';
1898
+ if (!source.includes(rel.replace(\`plans/\`, ''))) {
1899
+ warnings.push(
1900
+ \`WARN backlog/\${matchingFile}: source field "\${source}" does not reference \${rel}\`
1901
+ );
1902
+ }
1903
+ }
1904
+ }
1905
+
1906
+ // \u2500\u2500 Report \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500
1907
+ const totalErrors = errors.length;
1908
+ const totalWarnings = warnings.length;
1909
+
1910
+ if (totalErrors > 0 || totalWarnings > 0) {
1911
+ console.log(
1912
+ \`validate-plans: \${totalErrors} error(s), \${totalWarnings} warning(s):\`
1913
+ );
1914
+ for (const e of errors) console.error(' ' + e);
1915
+ for (const w of warnings) console.warn(' ' + w);
1916
+ }
1152
1917
 
1153
- _(no entries yet)_
1918
+ if (totalErrors > 0) {
1919
+ console.error(\`validate-plans: failed with \${totalErrors} error(s).\`);
1920
+ process.exit(1);
1921
+ } else if (totalWarnings > 0) {
1922
+ console.log(\`validate-plans: passed with \${totalWarnings} warning(s).\`);
1923
+ } else {
1924
+ console.log('validate-plans: passed.');
1925
+ }
1154
1926
  `;
1155
1927
  buildClaudeFileMap = (params) => {
1156
- const { projectName, slug, flowEnum: flowEnum3, layerEnum: layerEnum3 } = params;
1928
+ const { projectName, slug: slug4, flowEnum: flowEnum3, layerEnum: layerEnum3 } = params;
1929
+ const agentDef = (name) => {
1930
+ const found = AGENTS.find((a) => a.name === name);
1931
+ if (!found) throw new Error(`Agent "${name}" not found in AGENTS registry`);
1932
+ return found;
1933
+ };
1157
1934
  const files = [
1158
1935
  { relativePath: "CLAUDE.md", content: params.claudeMd },
1159
1936
  { relativePath: ".claude/settings.json", content: claudeSettingsTemplate() },
1160
1937
  { relativePath: ".claude/scripts/_docs-shared.mjs", content: docsSharedMjsTemplate(flowEnum3, layerEnum3) },
1161
1938
  { relativePath: ".claude/scripts/build-docs-index.mjs", content: buildDocsIndexMjsTemplate() },
1162
1939
  { relativePath: ".claude/scripts/lint-docs-frontmatter.mjs", content: lintDocsFrontmatterMjsTemplate() },
1940
+ { relativePath: ".claude/scripts/validate-structure.mjs", content: validateStructureMjsTemplate([...AGENTS, ...params.testing ? [TEST_WRITER_DEF] : []]) },
1941
+ { relativePath: ".claude/scripts/validate-plans.mjs", content: validatePlansMjsTemplate() },
1163
1942
  { relativePath: ".claude/scripts/docs-first-reminder.sh", content: docsFirstReminderShTemplate(params.reminderTrigger) },
1164
- { relativePath: `.claude/skills/${slug}-conventions/SKILL.md`, content: params.conventionsSkill },
1165
- { relativePath: `.claude/skills/${slug}-docs/SKILL.md`, content: docsSkillTemplate(projectName, slug, flowEnum3, layerEnum3) },
1943
+ { relativePath: ".claude/scripts/agent-guard.mjs", content: agentGuardMjsTemplate([...AGENTS, ...params.testing ? [TEST_WRITER_DEF] : []]) },
1944
+ { relativePath: `.claude/skills/${slug4}-conventions/SKILL.md`, content: params.conventionsSkill },
1945
+ { relativePath: `.claude/skills/${slug4}-docs/SKILL.md`, content: docsSkillTemplate(projectName, slug4, flowEnum3, layerEnum3) },
1166
1946
  { relativePath: ".claude/agents/dev.md", content: params.devAgent },
1167
- { relativePath: ".claude/agents/docs-writer.md", content: docsWriterAgentTemplate(projectName, slug) },
1947
+ { relativePath: ".claude/agents/docs-writer.md", content: docsWriterAgentTemplate(projectName, slug4, agentDef("docs-writer")) },
1948
+ { relativePath: ".claude/agents/planner.md", content: plannerAgentTemplate(projectName, agentDef("planner")) },
1949
+ { relativePath: ".claude/agents/advisor.md", content: advisorAgentTemplate(projectName, slug4, agentDef("advisor")) },
1950
+ { relativePath: ".claude/agents/scout.md", content: scoutAgentTemplate(projectName, slug4, agentDef("scout")) },
1168
1951
  { relativePath: ".claude/agent-memory/dev/MEMORY.md", content: agentMemorySeedTemplate("dev") },
1169
1952
  { relativePath: ".claude/agent-memory/docs-writer/MEMORY.md", content: agentMemorySeedTemplate("docs-writer") },
1953
+ { relativePath: ".claude/agent-memory/planner/MEMORY.md", content: agentMemorySeedTemplate("planner") },
1954
+ { relativePath: ".claude/agent-memory/advisor/MEMORY.md", content: agentMemorySeedTemplate("advisor") },
1955
+ { relativePath: "plans/README.md", content: plansReadmeTemplate() },
1956
+ { relativePath: "backlog/README.md", content: backlogReadmeTemplate() },
1170
1957
  { relativePath: "docs/README.md", content: docsReadmeTemplate() },
1171
1958
  { relativePath: "docs/INDEX.md", content: docsIndexPlaceholderTemplate() },
1172
1959
  { relativePath: "docs/_template.md", content: docsTemplateMdTemplate(flowEnum3, layerEnum3) },
@@ -1176,7 +1963,7 @@ _(no entries yet)_
1176
1963
  files.push(
1177
1964
  { relativePath: ".claude/agents/test-writer.md", content: params.testing.testWriterAgent },
1178
1965
  { relativePath: ".claude/agent-memory/test-writer/MEMORY.md", content: agentMemorySeedTemplate("test-writer") },
1179
- { relativePath: `.claude/skills/${slug}-test-author/SKILL.md`, content: params.testing.testAuthorSkill }
1966
+ { relativePath: `.claude/skills/${slug4}-test-author/SKILL.md`, content: params.testing.testAuthorSkill }
1180
1967
  );
1181
1968
  }
1182
1969
  return files;
@@ -1213,7 +2000,7 @@ var init_claude_setup2 = __esm({
1213
2000
  const hasVitest = cart.testing === "VITEST";
1214
2001
  const hasPlaywright = cart.testing === "PLAYWRIGHT";
1215
2002
  const hasTesting = cart.testing !== "NOT_USING";
1216
- const slug = projectSlug(cart);
2003
+ const slug4 = projectSlug(cart);
1217
2004
  const stack = [
1218
2005
  "React 19, TypeScript 5.8, Vite 6",
1219
2006
  hasRouter ? "TanStack Router v1.144.0 (file-based routes in src/routes/)" : null,
@@ -1230,8 +2017,9 @@ var init_claude_setup2 = __esm({
1230
2017
  cart.linter !== "NOT_USING" ? "- `npm run lint` \u2014 lint code" : null,
1231
2018
  hasVitest ? "- `npm run test:run` \u2014 run unit/component tests once (`npm test` for watch)" : null,
1232
2019
  hasPlaywright ? "- `npm run test:e2e` \u2014 run Playwright E2E tests" : null,
1233
- "- `npm run docs:index` \u2014 regenerate docs/INDEX.md (run after any doc change)",
1234
- "- `npm run docs:lint` \u2014 validate docs frontmatter (CI-ready, exits non-zero on violation)"
2020
+ "- `node .claude/scripts/build-docs-index.mjs` \u2014 regenerate docs/INDEX.md (run after any doc change)",
2021
+ "- `node .claude/scripts/lint-docs-frontmatter.mjs` \u2014 validate docs frontmatter (CI-ready, exits non-zero on violation)",
2022
+ "- `node .claude/scripts/validate-plans.mjs` \u2014 check plan/backlog consistency (table\u2194frontmatter, archived, ID gaps, two-way links)"
1235
2023
  ].filter(Boolean);
1236
2024
  const fsdArchitecture = `\`\`\`
1237
2025
  app \u2192 app shell, providers, global setup
@@ -1357,18 +2145,20 @@ ${testSection}## Agent Routing
1357
2145
 
1358
2146
  | Task / trigger | Agent | Notes |
1359
2147
  |---|---|---|
1360
- | Feature work or bug fix in \`src/\` | \`dev\` | MUST read relevant \`docs/features/\` spec before coding |
1361
- | Analyzing requirements, writing/updating feature docs | \`docs-writer\` | owns \`docs/\`; rebuilds INDEX.md after every change |${testWriterRow}
2148
+ ${claudeHarnessTableTemplate()}
2149
+ | Feature work or bug fix in \`src/\` | \`dev\` | MUST read relevant \`docs/features/\` spec before coding |${testWriterRow}
2150
+
2151
+ **PARK RULE (anti-loop):** when executing a step/phase, if it fails twice and the cause isn't fixable right now (missing info, needs a user decision, environment, or out-of-scope), STOP \u2014 don't retry a third time. Set the phase \`status: blocked\`, file a \`backlog/<id>\` entry (record what was already tried so it isn't repeated), link both ways, tell the user it was parked, and move on to the next workable item. See \`backlog/README.md\`.
1362
2152
 
1363
2153
  Each agent has persistent memory at \`.claude/agent-memory/<agent>/MEMORY.md\` \u2014 agents read it on start and append new gotchas. Do NOT use the general assistant for work an agent owns \u2014 always delegate.
1364
2154
 
1365
2155
  ## Task Documentation Convention
1366
2156
 
1367
- After any non-trivial fix or new pattern: copy \`docs/_template.md\`, fill the frontmatter, save as \`docs/features/<feature>/<topic>.en.md\` (or \`docs/architecture/\` for cross-cutting topics), then run \`npm run docs:index\` and commit the doc together with \`INDEX.md\`. Validate with \`npm run docs:lint\`.
2157
+ After any non-trivial fix or new pattern: copy \`docs/_template.md\`, fill the frontmatter, save as \`docs/features/<feature>/<topic>.en.md\` (or \`docs/architecture/\` for cross-cutting topics), then run \`node .claude/scripts/build-docs-index.mjs\` and commit the doc together with \`INDEX.md\`. Validate with \`node .claude/scripts/lint-docs-frontmatter.mjs\`.
1368
2158
 
1369
2159
  ## Further Reading + DOCS-FIRST RULE
1370
2160
 
1371
- Skills: \`.claude/skills/${slug}-conventions\` (architecture depth), \`.claude/skills/${slug}-docs\` (how to query the knowledge base)${hasTesting ? `, \`.claude/skills/${slug}-test-author\` (test conventions)` : ""}.
2161
+ Skills: \`.claude/skills/${slug4}-conventions\` (architecture depth), \`.claude/skills/${slug4}-docs\` (how to query the knowledge base)${hasTesting ? `, \`.claude/skills/${slug4}-test-author\` (test conventions)` : ""}.
1372
2162
 
1373
2163
  **DOCS-FIRST RULE:** for any request to describe, explain, or modify a documented feature, you MUST grep \`docs/\` frontmatter and read the relevant docs BEFORE opening source files \u2014 and state what the docs already covered. Start at \`docs/INDEX.md\`.
1374
2164
 
@@ -1412,9 +2202,9 @@ _(Trade-offs made and alternatives rejected.)_
1412
2202
  };
1413
2203
  conventionsSkillTemplate = (cart) => {
1414
2204
  const isFsd = cart.layout === "FSD";
1415
- const slug = projectSlug(cart);
2205
+ const slug4 = projectSlug(cart);
1416
2206
  return `---
1417
- name: ${slug}-conventions
2207
+ name: ${slug4}-conventions
1418
2208
  description: Coding conventions and architecture rules for ${cart.projectName}. Use when writing or reviewing ANY code under src/ \u2014 components, pages, hooks${cart.stateManagement === "ZUSTAND" ? ", stores" : ""}${cart.router === "TANSTACK_ROUTER" ? ", routes" : ""}, or styling. Triggers on tasks mentioning components, pages, layout, naming, imports, or project structure.
1419
2209
  ---
1420
2210
 
@@ -1465,10 +2255,10 @@ Check the reference implementation (\`src/pages/home${isFsd ? "/" : ".tsx"}\`) f
1465
2255
  `;
1466
2256
  };
1467
2257
  testAuthorSkillTemplate = (cart) => {
1468
- const slug = projectSlug(cart);
2258
+ const slug4 = projectSlug(cart);
1469
2259
  const hasVitest = cart.testing === "VITEST";
1470
2260
  return `---
1471
- name: ${slug}-test-author
2261
+ name: ${slug4}-test-author
1472
2262
  description: Centralized test conventions for ${cart.projectName}. Use when asked to "write a test", "add a spec", "cover this with tests", or fix a failing test. All test code lives under test/ \u2014 never inside src/.
1473
2263
  ---
1474
2264
 
@@ -1494,7 +2284,7 @@ ${hasVitest ? `- Unit tests import via the \`@/\` alias and explicit vitest impo
1494
2284
  `;
1495
2285
  };
1496
2286
  devAgentTemplate = (cart) => {
1497
- const slug = projectSlug(cart);
2287
+ const slug4 = projectSlug(cart);
1498
2288
  const hasTesting = cart.testing !== "NOT_USING";
1499
2289
  const testWriterExample = hasTesting ? " <example>user: 'Cover the login form with tests' \u2192 test-writer, NOT dev <commentary>test authoring belongs to test-writer</commentary></example>" : "";
1500
2290
  const delegationRule = hasTesting ? "- Never write tests (delegate to test-writer) or docs (delegate to docs-writer); flag when they are needed." : "- Never write docs (delegate to docs-writer); flag when they are needed.";
@@ -1511,7 +2301,7 @@ You are the implementation agent for ${cart.projectName}.
1511
2301
 
1512
2302
  1. Read \`.claude/agent-memory/dev/MEMORY.md\` \u2014 your accumulated gotchas.
1513
2303
  2. Read \`docs/INDEX.md\` and the relevant \`docs/features/<feature>/\` spec for the task.
1514
- 3. Load the \`${slug}-conventions\` skill for layer rules and patterns.
2304
+ 3. Load the \`${slug4}-conventions\` skill for layer rules and patterns.
1515
2305
  4. Read the code under change.
1516
2306
 
1517
2307
  If no relevant feature spec exists, STOP and tell the user to run the docs-writer agent first.
@@ -1523,6 +2313,10 @@ If no relevant feature spec exists, STOP and tell the user to run the docs-write
1523
2313
  3. Verify (build${hasTesting ? " + run the relevant tests" : ""}); report results faithfully.
1524
2314
  4. Append newly discovered gotchas/patterns to \`.claude/agent-memory/dev/MEMORY.md\`.
1525
2315
 
2316
+ ## Park rule (anti-loop)
2317
+
2318
+ If a step fails twice and the cause isn't fixable right now (missing info, needs a user decision, environment, out-of-scope), STOP \u2014 do not retry a third time. File a \`backlog/<id>\` entry per \`backlog/README.md\` (its **Tried** section must list what already failed so it isn't repeated), set the owning phase \`status: blocked\` and link both ways, tell the user it was parked, then continue with the next workable task. Don't loop, don't silently skip.
2319
+
1526
2320
  ## Hard rules
1527
2321
 
1528
2322
  - Never commit or push \u2014 a human does that.
@@ -1531,7 +2325,7 @@ ${delegationRule}
1531
2325
  `;
1532
2326
  };
1533
2327
  testWriterAgentTemplate = (cart) => {
1534
- const slug = projectSlug(cart);
2328
+ const slug4 = projectSlug(cart);
1535
2329
  return `---
1536
2330
  name: test-writer
1537
2331
  description: "Test authoring agent for ${cart.projectName} \u2014 writes tests and their paired spec.md contracts, only under test/. Runs after docs-writer has produced the feature spec. <example>user: 'Cover the home page with tests' \u2192 test-writer <commentary>test authoring from an existing feature spec</commentary></example> <example>user: 'Add edge-case tests for the date validator' \u2192 test-writer <commentary>narrow test scope, reads the spec for edge cases</commentary></example> <example>user: 'Fix the validation bug the test caught' \u2192 dev, NOT test-writer <commentary>app-code changes belong to dev</commentary></example>"
@@ -1545,7 +2339,7 @@ You are the test-writing agent for ${cart.projectName}. You write ONLY under \`t
1545
2339
 
1546
2340
  1. Read \`.claude/agent-memory/test-writer/MEMORY.md\`.
1547
2341
  2. Read the feature spec: \`docs/features/<feature>/<feature>.spec.en.md\` \u2014 requirements and edge cases drive the test plan. If it does not exist, STOP and request docs-writer first.
1548
- 3. Load the \`${slug}-test-author\` skill and \`test/README.md\`.
2342
+ 3. Load the \`${slug4}-test-author\` skill and \`test/README.md\`.
1549
2343
 
1550
2344
  ## Workflow
1551
2345
 
@@ -2478,10 +3272,6 @@ var init_package_json2 = __esm({
2478
3272
  } else if (cart.linter === "ESLINT") {
2479
3273
  scripts["lint"] = "eslint .";
2480
3274
  }
2481
- if (cart.ai === "CLAUDE") {
2482
- scripts["docs:index"] = "node .claude/scripts/build-docs-index.mjs";
2483
- scripts["docs:lint"] = "node .claude/scripts/lint-docs-frontmatter.mjs";
2484
- }
2485
3275
  return JSON.stringify(
2486
3276
  {
2487
3277
  name: cart.projectName,
@@ -2680,7 +3470,7 @@ var init_claude_setup3 = __esm({
2680
3470
  claudeMdTemplate2 = (cart) => {
2681
3471
  const hasZustand = cart.stateManagement === "ZUSTAND";
2682
3472
  const hasQuery = cart.query === "TANSTACK_QUERY";
2683
- const slug = projectSlug2(cart);
3473
+ const slug4 = projectSlug2(cart);
2684
3474
  const stack = [
2685
3475
  "React 19, TypeScript 5.8, Vite 6",
2686
3476
  "Chrome Extension Manifest V3 (popup-only)",
@@ -2694,8 +3484,9 @@ var init_claude_setup3 = __esm({
2694
3484
  "- `npm run build` \u2014 type-check + production build into `dist/`",
2695
3485
  "- `npm run build-extension` \u2014 build + copy `manifest.json` into `dist/`; load `dist/` as an unpacked extension at chrome://extensions",
2696
3486
  cart.linter !== "NOT_USING" ? "- `npm run lint` \u2014 lint code" : null,
2697
- "- `npm run docs:index` \u2014 regenerate docs/INDEX.md (run after any doc change)",
2698
- "- `npm run docs:lint` \u2014 validate docs frontmatter (CI-ready, exits non-zero on violation)"
3487
+ "- `node .claude/scripts/build-docs-index.mjs` \u2014 regenerate docs/INDEX.md (run after any doc change)",
3488
+ "- `node .claude/scripts/lint-docs-frontmatter.mjs` \u2014 validate docs frontmatter (CI-ready, exits non-zero on violation)",
3489
+ "- `node .claude/scripts/validate-plans.mjs` \u2014 check plan/backlog consistency (table\u2194frontmatter, archived, ID gaps, two-way links)"
2699
3490
  ].filter(Boolean);
2700
3491
  const keyPatterns = [
2701
3492
  `**Popup lifecycle \u2014 the popup unmounts when it closes:**
@@ -2776,18 +3567,20 @@ ${keyPatterns.join("\n\n")}
2776
3567
 
2777
3568
  | Task / trigger | Agent | Notes |
2778
3569
  |---|---|---|
3570
+ ${claudeHarnessTableTemplate()}
2779
3571
  | Feature work or bug fix in \`src/\` or \`manifest.json\` | \`dev\` | MUST read relevant \`docs/features/\` spec before coding |
2780
- | Analyzing requirements, writing/updating feature docs | \`docs-writer\` | owns \`docs/\`; rebuilds INDEX.md after every change |
3572
+
3573
+ **PARK RULE (anti-loop):** when executing a step/phase, if it fails twice and the cause isn't fixable right now (missing info, needs a user decision, environment, or out-of-scope), STOP \u2014 don't retry a third time. Set the phase \`status: blocked\`, file a \`backlog/<id>\` entry (record what was already tried so it isn't repeated), link both ways, tell the user it was parked, and move on to the next workable item. See \`backlog/README.md\`.
2781
3574
 
2782
3575
  Each agent has persistent memory at \`.claude/agent-memory/<agent>/MEMORY.md\` \u2014 agents read it on start and append new gotchas. Do NOT use the general assistant for work an agent owns \u2014 always delegate.
2783
3576
 
2784
3577
  ## Task Documentation Convention
2785
3578
 
2786
- After any non-trivial fix or new pattern: copy \`docs/_template.md\`, fill the frontmatter, save as \`docs/features/<feature>/<topic>.en.md\` (or \`docs/architecture/\` for cross-cutting topics), then run \`npm run docs:index\` and commit the doc together with \`INDEX.md\`. Validate with \`npm run docs:lint\`.
3579
+ After any non-trivial fix or new pattern: copy \`docs/_template.md\`, fill the frontmatter, save as \`docs/features/<feature>/<topic>.en.md\` (or \`docs/architecture/\` for cross-cutting topics), then run \`node .claude/scripts/build-docs-index.mjs\` and commit the doc together with \`INDEX.md\`. Validate with \`node .claude/scripts/lint-docs-frontmatter.mjs\`.
2787
3580
 
2788
3581
  ## Further Reading + DOCS-FIRST RULE
2789
3582
 
2790
- Skills: \`.claude/skills/${slug}-conventions\` (architecture depth), \`.claude/skills/${slug}-docs\` (how to query the knowledge base).
3583
+ Skills: \`.claude/skills/${slug4}-conventions\` (architecture depth), \`.claude/skills/${slug4}-docs\` (how to query the knowledge base).
2791
3584
 
2792
3585
  **DOCS-FIRST RULE:** for any request to describe, explain, or modify a documented feature, you MUST grep \`docs/\` frontmatter and read the relevant docs BEFORE opening source files \u2014 and state what the docs already covered. Start at \`docs/INDEX.md\`.
2793
3586
 
@@ -2831,10 +3624,10 @@ _(Trade-offs made and alternatives rejected.)_
2831
3624
  `;
2832
3625
  };
2833
3626
  conventionsSkillTemplate2 = (cart) => {
2834
- const slug = projectSlug2(cart);
3627
+ const slug4 = projectSlug2(cart);
2835
3628
  const hasZustand = cart.stateManagement === "ZUSTAND";
2836
3629
  return `---
2837
- name: ${slug}-conventions
3630
+ name: ${slug4}-conventions
2838
3631
  description: Coding conventions and architecture rules for ${cart.projectName} (Chrome extension, MV3 popup). Use when writing or reviewing ANY code under src/ \u2014 components, hooks${hasZustand ? ", stores" : ""}, or styling \u2014 or when touching manifest.json. Triggers on tasks mentioning popup, manifest, components, naming, imports, or project structure.
2839
3632
  ---
2840
3633
 
@@ -2881,7 +3674,7 @@ Check \`src/App.tsx\` first, then the docs (\`docs/INDEX.md\`), then ask.
2881
3674
  `;
2882
3675
  };
2883
3676
  devAgentTemplate2 = (cart) => {
2884
- const slug = projectSlug2(cart);
3677
+ const slug4 = projectSlug2(cart);
2885
3678
  return `---
2886
3679
  name: dev
2887
3680
  description: "Implementation agent for ${cart.projectName} \u2014 all feature work and bug fixes under src/ and manifest.json. <example>user: 'Add a settings toggle to the popup' \u2192 dev <commentary>feature work in src/; dev reads the feature spec first, then implements</commentary></example> <example>user: 'The popup crashes on empty data \u2014 fix it' \u2192 dev <commentary>bug fix inside a documented feature</commentary></example> <example>user: 'Write a spec for the options page' \u2192 docs-writer, NOT dev <commentary>doc authoring belongs to docs-writer</commentary></example>"
@@ -2895,7 +3688,7 @@ You are the implementation agent for ${cart.projectName}.
2895
3688
 
2896
3689
  1. Read \`.claude/agent-memory/dev/MEMORY.md\` \u2014 your accumulated gotchas.
2897
3690
  2. Read \`docs/INDEX.md\` and the relevant \`docs/features/<feature>/\` spec for the task.
2898
- 3. Load the \`${slug}-conventions\` skill for structure rules and patterns.
3691
+ 3. Load the \`${slug4}-conventions\` skill for structure rules and patterns.
2899
3692
  4. Read the code under change.
2900
3693
 
2901
3694
  If no relevant feature spec exists, STOP and tell the user to run the docs-writer agent first.
@@ -2907,6 +3700,10 @@ If no relevant feature spec exists, STOP and tell the user to run the docs-write
2907
3700
  3. Verify (build; for manifest/extension changes run \`npm run build-extension\` and report what to check at chrome://extensions); report results faithfully.
2908
3701
  4. Append newly discovered gotchas/patterns to \`.claude/agent-memory/dev/MEMORY.md\`.
2909
3702
 
3703
+ ## Park rule (anti-loop)
3704
+
3705
+ If a step fails twice and the cause isn't fixable right now (missing info, needs a user decision, environment, out-of-scope), STOP \u2014 do not retry a third time. File a \`backlog/<id>\` entry per \`backlog/README.md\` (its **Tried** section must list what already failed so it isn't repeated), set the owning phase \`status: blocked\` and link both ways, tell the user it was parked, then continue with the next workable task. Don't loop, don't silently skip.
3706
+
2910
3707
  ## Hard rules
2911
3708
 
2912
3709
  - Never commit or push \u2014 a human does that.
@@ -3125,6 +3922,484 @@ var init_chrome_extension2 = __esm({
3125
3922
  }
3126
3923
  });
3127
3924
 
3925
+ // src/options/harness-only/constants/index.ts
3926
+ var HARNESS_MENU_PROJECT_TYPE;
3927
+ var init_constants4 = __esm({
3928
+ "src/options/harness-only/constants/index.ts"() {
3929
+ "use strict";
3930
+ HARNESS_MENU_PROJECT_TYPE = {
3931
+ ReactVite: {
3932
+ display: "React + Vite",
3933
+ value: "REACT_VITE",
3934
+ description: "React + Vite project",
3935
+ disabled: false
3936
+ },
3937
+ ChromeExtension: {
3938
+ display: "Chrome Extension",
3939
+ value: "CHROME_EXTENSION",
3940
+ description: "Chrome Extension project",
3941
+ disabled: false
3942
+ },
3943
+ Generic: {
3944
+ display: "Other (Generic)",
3945
+ value: "GENERIC",
3946
+ description: "Any other project",
3947
+ disabled: false
3948
+ }
3949
+ };
3950
+ }
3951
+ });
3952
+
3953
+ // src/scaffold/harness-only/templates/react-vite-skeleton.ts
3954
+ var slug, claudeMdTemplate3, conventionsSkillTemplate3, devAgentTemplate3, seedDocsTemplate, getReactViteHarnessFileMap;
3955
+ var init_react_vite_skeleton = __esm({
3956
+ "src/scaffold/harness-only/templates/react-vite-skeleton.ts"() {
3957
+ "use strict";
3958
+ init_claude_setup();
3959
+ slug = (cart) => cart.projectName.toLowerCase().replace(/_/g, "-");
3960
+ claudeMdTemplate3 = (cart) => `# ${cart.projectName}
3961
+
3962
+ > This project uses a Claude Code harness for AI-assisted development.
3963
+ > Run \`claude /init\` to auto-generate a detailed CLAUDE.md tailored to this codebase.
3964
+
3965
+ ## Quick start
3966
+
3967
+ \`\`\`bash
3968
+ npm run dev
3969
+ npm run build
3970
+ \`\`\`
3971
+
3972
+ ## Docs
3973
+
3974
+ - \`docs/INDEX.md\` \u2014 knowledge base index (auto-generated)
3975
+ - \`node .claude/scripts/build-docs-index.mjs\` \u2014 regenerate index after adding a doc
3976
+ - \`node .claude/scripts/lint-docs-frontmatter.mjs\` \u2014 validate docs frontmatter
3977
+ `;
3978
+ conventionsSkillTemplate3 = (cart) => `---
3979
+ name: ${slug(cart)}-conventions
3980
+ description: Coding conventions and architecture rules for ${cart.projectName} (React + Vite). Use when writing or reviewing ANY code in this project.
3981
+ ---
3982
+
3983
+ # ${cart.projectName} \u2014 Conventions
3984
+
3985
+ ## Stack
3986
+
3987
+ React 19, TypeScript, Vite
3988
+
3989
+ ## Architecture
3990
+
3991
+ This project uses a feature-slice structure. Check the source layout and run \`claude /init\` to generate detailed conventions.
3992
+
3993
+ ## General rules
3994
+
3995
+ - TypeScript strict mode \u2014 no \`any\`, no unchecked assertions
3996
+ - Co-locate component, hook, and type in the same folder
3997
+ - Barrel exports via \`index.ts\`
3998
+ `;
3999
+ devAgentTemplate3 = (cart) => `---
4000
+ name: dev
4001
+ description: "Implementation agent for ${cart.projectName}. Use for feature work and bug fixes."
4002
+ model: sonnet
4003
+ ---
4004
+
4005
+ You are the implementation agent for ${cart.projectName} (React + Vite project).
4006
+
4007
+ ## Onboarding
4008
+
4009
+ 1. Read \`.claude/agent-memory/dev/MEMORY.md\`.
4010
+ 2. Read \`CLAUDE.md\` for project overview and commands.
4011
+ 3. Load the \`${slug(cart)}-conventions\` skill.
4012
+
4013
+ ## Park rule (anti-loop)
4014
+
4015
+ If a step fails twice and the cause isn't fixable right now (missing info, needs a user decision, environment, out-of-scope), STOP \u2014 do not retry a third time. File a \`backlog/<id>\` entry per \`backlog/README.md\` (its **Tried** section must list what already failed so it isn't repeated), set the owning phase \`status: blocked\` and link both ways, tell the user it was parked, then continue with the next workable task. Don't loop, don't silently skip.
4016
+
4017
+ ## Hard rules
4018
+
4019
+ - Never commit or push.
4020
+ - Run lint and type-check before reporting a task done.
4021
+ - Docs-first: check \`docs/INDEX.md\` before modifying a documented feature.
4022
+ `;
4023
+ seedDocsTemplate = (cart) => [
4024
+ {
4025
+ relativePath: "docs/features/home/home.spec.en.md",
4026
+ content: `---
4027
+ title: Home \u2014 Feature Spec
4028
+ feature: home
4029
+ flow: ui
4030
+ layer: _cross
4031
+ status: draft
4032
+ lang: en
4033
+ related: []
4034
+ keywords: [home]
4035
+ updated: ${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}
4036
+ ---
4037
+
4038
+ # Home
4039
+
4040
+ ## Context
4041
+
4042
+ Starting feature doc for ${cart.projectName}. Replace with real content.
4043
+
4044
+ ## Solution / Pattern
4045
+
4046
+ _To be documented._
4047
+ `
4048
+ }
4049
+ ];
4050
+ getReactViteHarnessFileMap = (cart) => buildClaudeFileMap({
4051
+ projectName: cart.projectName,
4052
+ slug: slug(cart),
4053
+ flowEnum: ["ui", "data", "infra", "architecture", "_meta"],
4054
+ layerEnum: ["app", "pages", "features", "entities", "shared", "_cross"],
4055
+ reminderTrigger: "home|landing",
4056
+ claudeMd: claudeMdTemplate3(cart),
4057
+ conventionsSkill: conventionsSkillTemplate3(cart),
4058
+ devAgent: devAgentTemplate3(cart),
4059
+ seedDocs: seedDocsTemplate(cart)
4060
+ });
4061
+ }
4062
+ });
4063
+
4064
+ // src/scaffold/harness-only/templates/chrome-extension-skeleton.ts
4065
+ var slug2, claudeMdTemplate4, conventionsSkillTemplate4, devAgentTemplate4, seedDocsTemplate2, getChromeExtensionHarnessFileMap;
4066
+ var init_chrome_extension_skeleton = __esm({
4067
+ "src/scaffold/harness-only/templates/chrome-extension-skeleton.ts"() {
4068
+ "use strict";
4069
+ init_claude_setup();
4070
+ slug2 = (cart) => cart.projectName.toLowerCase().replace(/_/g, "-");
4071
+ claudeMdTemplate4 = (cart) => `# ${cart.projectName}
4072
+
4073
+ > This project uses a Claude Code harness for AI-assisted development.
4074
+ > Run \`claude /init\` to auto-generate a detailed CLAUDE.md tailored to this codebase.
4075
+
4076
+ ## Quick start
4077
+
4078
+ \`\`\`bash
4079
+ npm run dev
4080
+ npm run build-extension # loads dist/ as unpacked extension at chrome://extensions
4081
+ \`\`\`
4082
+
4083
+ ## Docs
4084
+
4085
+ - \`docs/INDEX.md\` \u2014 knowledge base index (auto-generated)
4086
+ - \`node .claude/scripts/build-docs-index.mjs\` \u2014 regenerate index after adding a doc
4087
+ - \`node .claude/scripts/lint-docs-frontmatter.mjs\` \u2014 validate docs frontmatter
4088
+ `;
4089
+ conventionsSkillTemplate4 = (cart) => `---
4090
+ name: ${slug2(cart)}-conventions
4091
+ description: Coding conventions and architecture rules for ${cart.projectName} (Chrome Extension). Use when writing or reviewing ANY code in this project.
4092
+ ---
4093
+
4094
+ # ${cart.projectName} \u2014 Conventions
4095
+
4096
+ ## Stack
4097
+
4098
+ React 19, TypeScript, Vite, Chrome Extension Manifest V3
4099
+
4100
+ ## Architecture
4101
+
4102
+ Popup-centric structure. Check the source layout and run \`claude /init\` to generate detailed conventions.
4103
+
4104
+ ## General rules
4105
+
4106
+ - TypeScript strict mode \u2014 no \`any\`, no unchecked assertions
4107
+ - Manifest permissions: request only what's needed
4108
+ - No cross-origin requests from content scripts without explicit host permissions
4109
+ `;
4110
+ devAgentTemplate4 = (cart) => `---
4111
+ name: dev
4112
+ description: "Implementation agent for ${cart.projectName}. Use for feature work and bug fixes."
4113
+ model: sonnet
4114
+ ---
4115
+
4116
+ You are the implementation agent for ${cart.projectName} (Chrome Extension project).
4117
+
4118
+ ## Onboarding
4119
+
4120
+ 1. Read \`.claude/agent-memory/dev/MEMORY.md\`.
4121
+ 2. Read \`CLAUDE.md\` for project overview and commands.
4122
+ 3. Load the \`${slug2(cart)}-conventions\` skill.
4123
+
4124
+ ## Park rule (anti-loop)
4125
+
4126
+ If a step fails twice and the cause isn't fixable right now (missing info, needs a user decision, environment, out-of-scope), STOP \u2014 do not retry a third time. File a \`backlog/<id>\` entry per \`backlog/README.md\` (its **Tried** section must list what already failed so it isn't repeated), set the owning phase \`status: blocked\` and link both ways, tell the user it was parked, then continue with the next workable task. Don't loop, don't silently skip.
4127
+
4128
+ ## Hard rules
4129
+
4130
+ - Never commit or push.
4131
+ - Run lint and type-check before reporting a task done.
4132
+ - Docs-first: check \`docs/INDEX.md\` before modifying a documented feature.
4133
+ `;
4134
+ seedDocsTemplate2 = (cart) => [
4135
+ {
4136
+ relativePath: "docs/features/popup/popup.spec.en.md",
4137
+ content: `---
4138
+ title: Popup \u2014 Feature Spec
4139
+ feature: popup
4140
+ flow: ui
4141
+ layer: popup
4142
+ status: draft
4143
+ lang: en
4144
+ related: []
4145
+ keywords: [popup]
4146
+ updated: ${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}
4147
+ ---
4148
+
4149
+ # Popup
4150
+
4151
+ ## Context
4152
+
4153
+ Starting feature doc for ${cart.projectName}. Replace with real content.
4154
+
4155
+ ## Solution / Pattern
4156
+
4157
+ _To be documented._
4158
+ `
4159
+ }
4160
+ ];
4161
+ getChromeExtensionHarnessFileMap = (cart) => buildClaudeFileMap({
4162
+ projectName: cart.projectName,
4163
+ slug: slug2(cart),
4164
+ flowEnum: ["ui", "data", "extension", "infra", "_meta"],
4165
+ layerEnum: ["popup", "components", "hooks", "lib", "utils", "_cross"],
4166
+ reminderTrigger: "popup|manifest",
4167
+ claudeMd: claudeMdTemplate4(cart),
4168
+ conventionsSkill: conventionsSkillTemplate4(cart),
4169
+ devAgent: devAgentTemplate4(cart),
4170
+ seedDocs: seedDocsTemplate2(cart)
4171
+ });
4172
+ }
4173
+ });
4174
+
4175
+ // src/scaffold/harness-only/templates/generic-skeleton.ts
4176
+ var slug3, claudeMdTemplate5, conventionsSkillTemplate5, devAgentTemplate5, seedDocsTemplate3, getGenericHarnessFileMap;
4177
+ var init_generic_skeleton = __esm({
4178
+ "src/scaffold/harness-only/templates/generic-skeleton.ts"() {
4179
+ "use strict";
4180
+ init_claude_setup();
4181
+ slug3 = (cart) => cart.projectName.toLowerCase().replace(/_/g, "-");
4182
+ claudeMdTemplate5 = (cart) => `# ${cart.projectName}
4183
+
4184
+ > This project uses a Claude Code harness for AI-assisted development.
4185
+ > Run \`claude /init\` to auto-generate a detailed CLAUDE.md tailored to this codebase.
4186
+
4187
+ ## Docs
4188
+
4189
+ - \`docs/INDEX.md\` \u2014 knowledge base index (auto-generated)
4190
+ - \`node .claude/scripts/build-docs-index.mjs\` \u2014 regenerate index after adding a doc
4191
+ - \`node .claude/scripts/lint-docs-frontmatter.mjs\` \u2014 validate docs frontmatter
4192
+ `;
4193
+ conventionsSkillTemplate5 = (cart) => `---
4194
+ name: ${slug3(cart)}-conventions
4195
+ description: Coding conventions for ${cart.projectName}. Run \`claude /init\` to replace with project-specific content.
4196
+ ---
4197
+
4198
+ # ${cart.projectName} \u2014 Conventions
4199
+
4200
+ > Run \`claude /init\` in this project to generate detailed, codebase-specific conventions.
4201
+ `;
4202
+ devAgentTemplate5 = (cart) => `---
4203
+ name: dev
4204
+ description: "Implementation agent for ${cart.projectName}."
4205
+ model: sonnet
4206
+ ---
4207
+
4208
+ You are the implementation agent for ${cart.projectName}.
4209
+
4210
+ ## Onboarding
4211
+
4212
+ 1. Read \`.claude/agent-memory/dev/MEMORY.md\`.
4213
+ 2. Read \`CLAUDE.md\` for project overview.
4214
+ 3. Load the \`${slug3(cart)}-conventions\` skill.
4215
+
4216
+ ## Park rule (anti-loop)
4217
+
4218
+ If a step fails twice and the cause isn't fixable right now (missing info, needs a user decision, environment, out-of-scope), STOP \u2014 do not retry a third time. File a \`backlog/<id>\` entry per \`backlog/README.md\` (its **Tried** section must list what already failed so it isn't repeated), set the owning phase \`status: blocked\` and link both ways, tell the user it was parked, then continue with the next workable task. Don't loop, don't silently skip.
4219
+
4220
+ ## Hard rules
4221
+
4222
+ - Never commit or push.
4223
+ - Docs-first: check \`docs/INDEX.md\` before modifying a documented feature.
4224
+ `;
4225
+ seedDocsTemplate3 = (cart) => [
4226
+ {
4227
+ relativePath: "docs/features/home/home.spec.en.md",
4228
+ content: `---
4229
+ title: Home \u2014 Feature Spec
4230
+ feature: home
4231
+ flow: ui
4232
+ layer: _cross
4233
+ status: draft
4234
+ lang: en
4235
+ related: []
4236
+ keywords: [home]
4237
+ updated: ${(/* @__PURE__ */ new Date()).toISOString().slice(0, 10)}
4238
+ ---
4239
+
4240
+ # Home
4241
+
4242
+ ## Context
4243
+
4244
+ Starting feature doc for ${cart.projectName}. Replace with real content.
4245
+
4246
+ ## Solution / Pattern
4247
+
4248
+ _To be documented._
4249
+ `
4250
+ }
4251
+ ];
4252
+ getGenericHarnessFileMap = (cart) => buildClaudeFileMap({
4253
+ projectName: cart.projectName,
4254
+ slug: slug3(cart),
4255
+ flowEnum: ["ui", "data", "infra", "_meta"],
4256
+ layerEnum: ["src", "_cross"],
4257
+ reminderTrigger: "home",
4258
+ claudeMd: claudeMdTemplate5(cart),
4259
+ conventionsSkill: conventionsSkillTemplate5(cart),
4260
+ devAgent: devAgentTemplate5(cart),
4261
+ seedDocs: seedDocsTemplate3(cart)
4262
+ });
4263
+ }
4264
+ });
4265
+
4266
+ // src/scaffold/harness-only/index.ts
4267
+ var harness_only_exports = {};
4268
+ __export(harness_only_exports, {
4269
+ scaffoldHarnessOnly: () => scaffoldHarnessOnly
4270
+ });
4271
+ import path4 from "path";
4272
+ import chalk6 from "chalk";
4273
+ import { createSpinner as createSpinner4 } from "nanospinner";
4274
+ var getFileMap, scaffoldHarnessOnly;
4275
+ var init_harness_only = __esm({
4276
+ "src/scaffold/harness-only/index.ts"() {
4277
+ "use strict";
4278
+ init_constants();
4279
+ init_utils();
4280
+ init_errors();
4281
+ init_react_vite_skeleton();
4282
+ init_chrome_extension_skeleton();
4283
+ init_generic_skeleton();
4284
+ getFileMap = (cart) => {
4285
+ switch (cart.projectType) {
4286
+ case "REACT_VITE":
4287
+ return getReactViteHarnessFileMap(cart);
4288
+ case "CHROME_EXTENSION":
4289
+ return getChromeExtensionHarnessFileMap(cart);
4290
+ default:
4291
+ return getGenericHarnessFileMap(cart);
4292
+ }
4293
+ };
4294
+ scaffoldHarnessOnly = async (cart) => {
4295
+ if (!cart || cart.type !== MENU_OPTIONS_LEVEL_1.HarnessOnly.value) return;
4296
+ const { targetDirectory, projectName } = cart;
4297
+ if (!await dirExists(targetDirectory)) {
4298
+ throw new ScaffoldError(`Directory "${path4.relative(process.cwd(), targetDirectory)}" does not exist.`);
4299
+ }
4300
+ const spinner = createSpinner4(`Applying harness to ${chalk6.cyan(projectName)}...`).start();
4301
+ try {
4302
+ const fileMap = getFileMap(cart);
4303
+ for (const { relativePath, content } of fileMap) {
4304
+ await writeProjectFile(targetDirectory, relativePath, content);
4305
+ }
4306
+ spinner.success({
4307
+ text: chalk6.green(`AI harness applied to ${chalk6.bold(projectName)} successfully!`)
4308
+ });
4309
+ console.log("");
4310
+ console.log(chalk6.whiteBright("Next steps:"));
4311
+ console.log(chalk6.cyan(` cd ${path4.relative(process.cwd(), targetDirectory) || "."}`));
4312
+ console.log(chalk6.cyan(" claude /init") + chalk6.gray(" \u2190 let Claude generate a detailed CLAUDE.md for your codebase"));
4313
+ console.log("");
4314
+ } catch (err) {
4315
+ spinner.error({ text: chalk6.red("Harness setup failed.") });
4316
+ if (err instanceof ScaffoldError) {
4317
+ console.error(chalk6.red(err.message));
4318
+ } else if (isNodeError(err)) {
4319
+ console.error(chalk6.red(`File system error (${err.code}): ${err.message}`));
4320
+ } else {
4321
+ console.error(chalk6.red("An unexpected error occurred."), err);
4322
+ }
4323
+ process.exit(1);
4324
+ }
4325
+ };
4326
+ }
4327
+ });
4328
+
4329
+ // src/options/harness-only/index.ts
4330
+ var harness_only_exports2 = {};
4331
+ __export(harness_only_exports2, {
4332
+ flowHarnessOnly: () => flowHarnessOnly
4333
+ });
4334
+ import { select as select3, input as input3 } from "@inquirer/prompts";
4335
+ import path5 from "path";
4336
+ import chalk7 from "chalk";
4337
+ var selectFromMenu3, menuTargetDirectory, menuProjectName3, menuProjectType, flowHarnessOnly;
4338
+ var init_harness_only2 = __esm({
4339
+ "src/options/harness-only/index.ts"() {
4340
+ "use strict";
4341
+ init_constants();
4342
+ init_constants4();
4343
+ init_utils();
4344
+ selectFromMenu3 = async (menuOptions, message) => {
4345
+ const keys = Object.keys(menuOptions);
4346
+ const choices = keys.filter((key) => !menuOptions[key].disabled).map((key) => ({
4347
+ name: menuOptions[key].display,
4348
+ value: menuOptions[key].value,
4349
+ description: menuOptions[key].description
4350
+ }));
4351
+ const answer = await select3({ message: chalk7.whiteBright(message), choices });
4352
+ return answer;
4353
+ };
4354
+ menuTargetDirectory = async (cart) => {
4355
+ if (!cart || cart.type !== MENU_OPTIONS_LEVEL_1.HarnessOnly.value) return;
4356
+ const raw = await input3({
4357
+ message: chalk7.whiteBright("Target directory (existing project):"),
4358
+ default: ".",
4359
+ validate: async (value) => {
4360
+ const resolved = path5.resolve(process.cwd(), value.trim());
4361
+ if (!await dirExists(resolved)) {
4362
+ return `Directory "${value.trim()}" does not exist`;
4363
+ }
4364
+ return true;
4365
+ },
4366
+ transformer: (value) => value.trim()
4367
+ });
4368
+ cart.targetDirectory = path5.resolve(process.cwd(), raw.trim());
4369
+ };
4370
+ menuProjectName3 = async (cart) => {
4371
+ if (!cart || cart.type !== MENU_OPTIONS_LEVEL_1.HarnessOnly.value) return;
4372
+ const defaultName = path5.basename(cart.targetDirectory);
4373
+ cart.projectName = await input3({
4374
+ message: chalk7.whiteBright("Project name:"),
4375
+ default: defaultName,
4376
+ validate: (value) => {
4377
+ if (!value.trim()) return "Project name cannot be empty";
4378
+ if (!/^[a-zA-Z0-9_-]+$/.test(value.trim()))
4379
+ return "Only letters, numbers, hyphens, and underscores allowed";
4380
+ return true;
4381
+ },
4382
+ transformer: (value) => value.trim()
4383
+ });
4384
+ };
4385
+ menuProjectType = async (cart) => {
4386
+ if (!cart || cart.type !== MENU_OPTIONS_LEVEL_1.HarnessOnly.value) return;
4387
+ cart.projectType = await selectFromMenu3(
4388
+ HARNESS_MENU_PROJECT_TYPE,
4389
+ "Project type:"
4390
+ );
4391
+ };
4392
+ flowHarnessOnly = async () => {
4393
+ const cart = { type: MENU_OPTIONS_LEVEL_1.HarnessOnly.value };
4394
+ await menuTargetDirectory(cart);
4395
+ await menuProjectName3(cart);
4396
+ await menuProjectType(cart);
4397
+ const { scaffoldHarnessOnly: scaffoldHarnessOnly2 } = await Promise.resolve().then(() => (init_harness_only(), harness_only_exports));
4398
+ await scaffoldHarnessOnly2(cart);
4399
+ };
4400
+ }
4401
+ });
4402
+
3128
4403
  // src/commands/update.ts
3129
4404
  import { exec } from "child_process";
3130
4405
  import { promisify } from "util";
@@ -3172,8 +4447,8 @@ var runUpdate = async (currentVersion) => {
3172
4447
 
3173
4448
  // src/options/index.ts
3174
4449
  init_constants();
3175
- import { select as select3, Separator as Separator3 } from "@inquirer/prompts";
3176
- import chalk6 from "chalk";
4450
+ import { select as select4, Separator as Separator3 } from "@inquirer/prompts";
4451
+ import chalk8 from "chalk";
3177
4452
  var menu = async () => {
3178
4453
  const keys = Object.keys(MENU_OPTIONS_LEVEL_1);
3179
4454
  const enabledKeys = keys.filter((key) => !MENU_OPTIONS_LEVEL_1[key].disabled);
@@ -3190,8 +4465,8 @@ var menu = async () => {
3190
4465
  disabled: MENU_OPTIONS_LEVEL_1.Nuxt.description
3191
4466
  }
3192
4467
  ];
3193
- const answer = await select3({
3194
- message: chalk6.whiteBright("How can I help you \u{1F468}\u200D\u{1F373}"),
4468
+ const answer = await select4({
4469
+ message: chalk8.whiteBright("How can I help you \u{1F468}\u200D\u{1F373}"),
3195
4470
  choices
3196
4471
  });
3197
4472
  const cart = { type: answer };
@@ -3201,6 +4476,9 @@ var menu = async () => {
3201
4476
  } else if (answer === MENU_OPTIONS_LEVEL_1.ChromeExtension.value) {
3202
4477
  const { flowChromeExtension: flowChromeExtension2 } = await Promise.resolve().then(() => (init_chrome_extension2(), chrome_extension_exports2));
3203
4478
  await flowChromeExtension2(cart);
4479
+ } else if (answer === MENU_OPTIONS_LEVEL_1.HarnessOnly.value) {
4480
+ const { flowHarnessOnly: flowHarnessOnly2 } = await Promise.resolve().then(() => (init_harness_only2(), harness_only_exports2));
4481
+ await flowHarnessOnly2();
3204
4482
  }
3205
4483
  return cart;
3206
4484
  };
@@ -3218,13 +4496,13 @@ var typeWriter = async (text, colorFunc, speed = 50) => {
3218
4496
  };
3219
4497
 
3220
4498
  // src/utils/check-node-version.ts
3221
- import chalk7 from "chalk";
4499
+ import chalk9 from "chalk";
3222
4500
  var MIN_NODE_MAJOR = 20;
3223
4501
  var checkNodeVersion = () => {
3224
4502
  const major = parseInt(process.version.slice(1).split(".")[0], 10);
3225
4503
  if (major < MIN_NODE_MAJOR) {
3226
4504
  console.error(
3227
- chalk7.red(
4505
+ chalk9.red(
3228
4506
  `\u2716 beaver requires Node.js v${MIN_NODE_MAJOR} or higher.
3229
4507
  You are running ${process.version}.
3230
4508
  Please upgrade Node.js: https://nodejs.org`
@@ -3242,7 +4520,7 @@ var getUserName = () => {
3242
4520
  };
3243
4521
 
3244
4522
  // src/index.ts
3245
- import chalk8 from "chalk";
4523
+ import chalk10 from "chalk";
3246
4524
  import { readFileSync } from "fs";
3247
4525
  import { dirname, join } from "path";
3248
4526
  import { fileURLToPath } from "url";
@@ -3275,6 +4553,7 @@ Commands:
3275
4553
  Options:
3276
4554
  -v, --version Show version
3277
4555
  -h, --help Show help
4556
+ --ai Apply AI harness to an existing project
3278
4557
  `);
3279
4558
  process.exit(0);
3280
4559
  }
@@ -3282,13 +4561,25 @@ Options:
3282
4561
  await runUpdate(getVersion());
3283
4562
  process.exit(0);
3284
4563
  }
3285
- await typeWriter(`Hi! ${getUserName()} \u{1F646}`, chalk8.whiteBright, 50);
4564
+ if (args.includes("--ai")) {
4565
+ try {
4566
+ const { flowHarnessOnly: flowHarnessOnly2 } = await Promise.resolve().then(() => (init_harness_only2(), harness_only_exports2));
4567
+ await flowHarnessOnly2();
4568
+ } catch (err) {
4569
+ if (err instanceof Error) {
4570
+ console.error(chalk10.red(err.message));
4571
+ }
4572
+ process.exit(1);
4573
+ }
4574
+ process.exit(0);
4575
+ }
4576
+ await typeWriter(`Hi! ${getUserName()} \u{1F646}`, chalk10.whiteBright, 50);
3286
4577
  await sleep(500);
3287
4578
  try {
3288
4579
  await menu();
3289
4580
  } catch (err) {
3290
4581
  if (err instanceof Error) {
3291
- console.error(chalk8.red(err.message));
4582
+ console.error(chalk10.red(err.message));
3292
4583
  }
3293
4584
  process.exit(1);
3294
4585
  }