qualia-framework 2.5.1 → 3.1.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 (327) hide show
  1. package/CLAUDE.md +63 -0
  2. package/README.md +108 -30
  3. package/agents/builder.md +110 -0
  4. package/agents/planner.md +186 -0
  5. package/agents/qa-browser.md +186 -0
  6. package/agents/verifier.md +369 -0
  7. package/bin/cli.js +706 -417
  8. package/bin/install.js +622 -0
  9. package/bin/qualia-ui.js +284 -0
  10. package/bin/state.js +824 -0
  11. package/bin/statusline.js +252 -0
  12. package/docs/erp-contract.md +161 -0
  13. package/guide.md +63 -0
  14. package/hooks/auto-update.js +117 -0
  15. package/hooks/block-env-edit.js +52 -0
  16. package/hooks/branch-guard.js +68 -0
  17. package/hooks/migration-guard.js +83 -0
  18. package/hooks/pre-compact.js +52 -0
  19. package/hooks/pre-deploy-gate.js +149 -0
  20. package/hooks/pre-push.js +53 -0
  21. package/hooks/session-start.js +126 -0
  22. package/package.json +31 -17
  23. package/rules/design-reference.md +179 -0
  24. package/rules/frontend.md +126 -0
  25. package/rules/infrastructure.md +87 -0
  26. package/skills/qualia/SKILL.md +88 -0
  27. package/skills/qualia-build/SKILL.md +115 -0
  28. package/skills/qualia-debug/SKILL.md +87 -0
  29. package/skills/qualia-design/SKILL.md +99 -0
  30. package/skills/qualia-handoff/SKILL.md +66 -0
  31. package/skills/qualia-help/SKILL.md +60 -0
  32. package/skills/qualia-idk/SKILL.md +8 -0
  33. package/skills/qualia-learn/SKILL.md +111 -0
  34. package/skills/qualia-new/SKILL.md +323 -0
  35. package/skills/qualia-pause/SKILL.md +63 -0
  36. package/skills/qualia-plan/SKILL.md +101 -0
  37. package/skills/qualia-polish/SKILL.md +207 -0
  38. package/skills/qualia-quick/SKILL.md +37 -0
  39. package/skills/qualia-report/SKILL.md +114 -0
  40. package/skills/qualia-resume/SKILL.md +49 -0
  41. package/skills/qualia-review/SKILL.md +161 -0
  42. package/skills/qualia-ship/SKILL.md +90 -0
  43. package/skills/qualia-skill-new/SKILL.md +167 -0
  44. package/skills/qualia-task/SKILL.md +91 -0
  45. package/skills/qualia-test/SKILL.md +134 -0
  46. package/skills/qualia-verify/SKILL.md +113 -0
  47. package/templates/DESIGN.md +475 -0
  48. package/templates/help.html +476 -0
  49. package/templates/plan.md +42 -0
  50. package/templates/project.md +22 -0
  51. package/templates/state.md +27 -0
  52. package/templates/tracking.json +20 -0
  53. package/tests/bin.test.sh +687 -0
  54. package/tests/hooks.test.sh +384 -0
  55. package/tests/runner.js +1956 -0
  56. package/tests/state.test.sh +713 -0
  57. package/tests/statusline.test.sh +243 -0
  58. package/bin/collect-metrics.sh +0 -62
  59. package/framework/.claudeignore +0 -51
  60. package/framework/CLAUDE.md +0 -51
  61. package/framework/MCP_SETUP.md +0 -229
  62. package/framework/agents/architecture-strategist.md +0 -53
  63. package/framework/agents/backend-agent.md +0 -150
  64. package/framework/agents/code-simplicity-reviewer.md +0 -86
  65. package/framework/agents/frontend-agent.md +0 -111
  66. package/framework/agents/kieran-typescript-reviewer.md +0 -96
  67. package/framework/agents/performance-oracle.md +0 -111
  68. package/framework/agents/qualia-codebase-mapper.md +0 -761
  69. package/framework/agents/qualia-debugger.md +0 -1204
  70. package/framework/agents/qualia-executor.md +0 -882
  71. package/framework/agents/qualia-integration-checker.md +0 -424
  72. package/framework/agents/qualia-phase-researcher.md +0 -457
  73. package/framework/agents/qualia-plan-checker.md +0 -700
  74. package/framework/agents/qualia-planner.md +0 -1245
  75. package/framework/agents/qualia-project-researcher.md +0 -603
  76. package/framework/agents/qualia-research-synthesizer.md +0 -200
  77. package/framework/agents/qualia-roadmapper.md +0 -606
  78. package/framework/agents/qualia-verifier.md +0 -686
  79. package/framework/agents/red-team-qa.md +0 -130
  80. package/framework/agents/security-auditor.md +0 -72
  81. package/framework/agents/team-orchestrator.md +0 -229
  82. package/framework/agents/teams/framework-audit-team.md +0 -66
  83. package/framework/agents/teams/full-stack-team.md +0 -48
  84. package/framework/agents/teams/optimize-team.md +0 -53
  85. package/framework/agents/teams/review-team.md +0 -70
  86. package/framework/agents/teams/ship-team.md +0 -86
  87. package/framework/agents/test-agent.md +0 -182
  88. package/framework/hooks/auto-format.sh +0 -54
  89. package/framework/hooks/block-env-edit.sh +0 -42
  90. package/framework/hooks/branch-guard.sh +0 -43
  91. package/framework/hooks/confirm-delete.sh +0 -59
  92. package/framework/hooks/migration-validate.sh +0 -77
  93. package/framework/hooks/notification-speak.sh +0 -16
  94. package/framework/hooks/pre-commit.sh +0 -100
  95. package/framework/hooks/pre-compact.sh +0 -56
  96. package/framework/hooks/pre-deploy-gate.sh +0 -160
  97. package/framework/hooks/qualia-colors.sh +0 -32
  98. package/framework/hooks/retention-cleanup.sh +0 -62
  99. package/framework/hooks/save-session-state.sh +0 -185
  100. package/framework/hooks/session-context-loader.sh +0 -96
  101. package/framework/hooks/session-learn.sh +0 -32
  102. package/framework/hooks/skill-announce.sh +0 -123
  103. package/framework/hooks/tool-error-announce.sh +0 -27
  104. package/framework/install.ps1 +0 -323
  105. package/framework/install.sh +0 -313
  106. package/framework/qualia-framework/VERSION +0 -1
  107. package/framework/qualia-framework/assets/qualia-logo.png +0 -0
  108. package/framework/qualia-framework/bin/collect-metrics.sh +0 -67
  109. package/framework/qualia-framework/bin/generate-report-docx.py +0 -429
  110. package/framework/qualia-framework/bin/qualia-tools.js +0 -2201
  111. package/framework/qualia-framework/bin/qualia-tools.test.js +0 -1054
  112. package/framework/qualia-framework/references/checkpoints.md +0 -775
  113. package/framework/qualia-framework/references/completion-checklists.md +0 -359
  114. package/framework/qualia-framework/references/continuation-format.md +0 -249
  115. package/framework/qualia-framework/references/continuation-prompt.md +0 -97
  116. package/framework/qualia-framework/references/decimal-phase-calculation.md +0 -65
  117. package/framework/qualia-framework/references/design-quality.md +0 -56
  118. package/framework/qualia-framework/references/employee-guide.md +0 -167
  119. package/framework/qualia-framework/references/git-integration.md +0 -254
  120. package/framework/qualia-framework/references/git-planning-commit.md +0 -50
  121. package/framework/qualia-framework/references/model-profile-resolution.md +0 -32
  122. package/framework/qualia-framework/references/model-profiles.md +0 -73
  123. package/framework/qualia-framework/references/phase-argument-parsing.md +0 -61
  124. package/framework/qualia-framework/references/planning-config.md +0 -195
  125. package/framework/qualia-framework/references/questioning.md +0 -141
  126. package/framework/qualia-framework/references/tdd.md +0 -263
  127. package/framework/qualia-framework/references/ui-brand.md +0 -160
  128. package/framework/qualia-framework/references/verification-patterns.md +0 -612
  129. package/framework/qualia-framework/templates/DEBUG.md +0 -159
  130. package/framework/qualia-framework/templates/DESIGN.md +0 -81
  131. package/framework/qualia-framework/templates/UAT.md +0 -247
  132. package/framework/qualia-framework/templates/codebase/architecture.md +0 -255
  133. package/framework/qualia-framework/templates/codebase/concerns.md +0 -310
  134. package/framework/qualia-framework/templates/codebase/conventions.md +0 -307
  135. package/framework/qualia-framework/templates/codebase/integrations.md +0 -280
  136. package/framework/qualia-framework/templates/codebase/stack.md +0 -186
  137. package/framework/qualia-framework/templates/codebase/structure.md +0 -285
  138. package/framework/qualia-framework/templates/codebase/testing.md +0 -480
  139. package/framework/qualia-framework/templates/config.json +0 -35
  140. package/framework/qualia-framework/templates/context.md +0 -283
  141. package/framework/qualia-framework/templates/continue-here.md +0 -78
  142. package/framework/qualia-framework/templates/debug-subagent-prompt.md +0 -91
  143. package/framework/qualia-framework/templates/discovery.md +0 -146
  144. package/framework/qualia-framework/templates/lab-notes.md +0 -16
  145. package/framework/qualia-framework/templates/milestone-archive.md +0 -123
  146. package/framework/qualia-framework/templates/milestone.md +0 -115
  147. package/framework/qualia-framework/templates/phase-prompt.md +0 -567
  148. package/framework/qualia-framework/templates/planner-subagent-prompt.md +0 -117
  149. package/framework/qualia-framework/templates/project.md +0 -184
  150. package/framework/qualia-framework/templates/projects/ai-agent.md +0 -156
  151. package/framework/qualia-framework/templates/projects/mobile-app.md +0 -181
  152. package/framework/qualia-framework/templates/projects/voice-agent.md +0 -134
  153. package/framework/qualia-framework/templates/projects/website.md +0 -137
  154. package/framework/qualia-framework/templates/requirements.md +0 -231
  155. package/framework/qualia-framework/templates/research-project/ARCHITECTURE.md +0 -204
  156. package/framework/qualia-framework/templates/research-project/FEATURES.md +0 -147
  157. package/framework/qualia-framework/templates/research-project/PITFALLS.md +0 -200
  158. package/framework/qualia-framework/templates/research-project/STACK.md +0 -120
  159. package/framework/qualia-framework/templates/research-project/SUMMARY.md +0 -170
  160. package/framework/qualia-framework/templates/research.md +0 -552
  161. package/framework/qualia-framework/templates/roadmap.md +0 -206
  162. package/framework/qualia-framework/templates/state.md +0 -179
  163. package/framework/qualia-framework/templates/summary-complex.md +0 -59
  164. package/framework/qualia-framework/templates/summary-minimal.md +0 -41
  165. package/framework/qualia-framework/templates/summary-standard.md +0 -48
  166. package/framework/qualia-framework/templates/summary.md +0 -246
  167. package/framework/qualia-framework/templates/user-setup.md +0 -311
  168. package/framework/qualia-framework/templates/verification-report.md +0 -322
  169. package/framework/qualia-framework/workflows/add-phase.md +0 -179
  170. package/framework/qualia-framework/workflows/add-todo.md +0 -157
  171. package/framework/qualia-framework/workflows/audit-milestone.md +0 -241
  172. package/framework/qualia-framework/workflows/check-todos.md +0 -176
  173. package/framework/qualia-framework/workflows/complete-milestone.md +0 -858
  174. package/framework/qualia-framework/workflows/diagnose-issues.md +0 -219
  175. package/framework/qualia-framework/workflows/discovery-phase.md +0 -289
  176. package/framework/qualia-framework/workflows/discuss-phase.md +0 -534
  177. package/framework/qualia-framework/workflows/execute-phase.md +0 -559
  178. package/framework/qualia-framework/workflows/execute-plan.md +0 -438
  179. package/framework/qualia-framework/workflows/help.md +0 -470
  180. package/framework/qualia-framework/workflows/insert-phase.md +0 -220
  181. package/framework/qualia-framework/workflows/list-phase-assumptions.md +0 -178
  182. package/framework/qualia-framework/workflows/map-codebase.md +0 -327
  183. package/framework/qualia-framework/workflows/new-milestone.md +0 -363
  184. package/framework/qualia-framework/workflows/new-project.md +0 -982
  185. package/framework/qualia-framework/workflows/pause-work.md +0 -122
  186. package/framework/qualia-framework/workflows/plan-milestone-gaps.md +0 -256
  187. package/framework/qualia-framework/workflows/plan-phase.md +0 -422
  188. package/framework/qualia-framework/workflows/progress.md +0 -389
  189. package/framework/qualia-framework/workflows/quick.md +0 -252
  190. package/framework/qualia-framework/workflows/remove-phase.md +0 -326
  191. package/framework/qualia-framework/workflows/research-phase.md +0 -74
  192. package/framework/qualia-framework/workflows/resume-project.md +0 -306
  193. package/framework/qualia-framework/workflows/set-profile.md +0 -80
  194. package/framework/qualia-framework/workflows/settings.md +0 -145
  195. package/framework/qualia-framework/workflows/transition.md +0 -556
  196. package/framework/qualia-framework/workflows/update.md +0 -197
  197. package/framework/qualia-framework/workflows/verify-phase.md +0 -195
  198. package/framework/qualia-framework/workflows/verify-work.md +0 -625
  199. package/framework/rules/context7.md +0 -14
  200. package/framework/rules/frontend.md +0 -33
  201. package/framework/rules/speed.md +0 -23
  202. package/framework/scripts/__pycache__/say.cpython-314.pyc +0 -0
  203. package/framework/scripts/apply-retention.sh +0 -120
  204. package/framework/scripts/bootstrap-pop-os.sh +0 -354
  205. package/framework/scripts/claude-voice +0 -13
  206. package/framework/scripts/cleanup.sh +0 -131
  207. package/framework/scripts/cowork-mode.sh +0 -141
  208. package/framework/scripts/generate-project-claude-md.sh +0 -153
  209. package/framework/scripts/load-test-webhook.js +0 -172
  210. package/framework/scripts/say.py +0 -236
  211. package/framework/scripts/showcase-video-recorder/ffmpeg-builder.js +0 -167
  212. package/framework/scripts/showcase-video-recorder/playwright-helpers.js +0 -216
  213. package/framework/scripts/speak.py +0 -55
  214. package/framework/scripts/speak.sh +0 -18
  215. package/framework/scripts/status.sh +0 -138
  216. package/framework/scripts/sync-to-framework.sh +0 -65
  217. package/framework/scripts/voice-hotkey.py +0 -227
  218. package/framework/scripts/voice-input.sh +0 -51
  219. package/framework/skills/animate/SKILL.md +0 -202
  220. package/framework/skills/bolder/SKILL.md +0 -144
  221. package/framework/skills/browser-qa/SKILL.md +0 -536
  222. package/framework/skills/clarify/SKILL.md +0 -179
  223. package/framework/skills/client-handoff/SKILL.md +0 -135
  224. package/framework/skills/collab-onboard/SKILL.md +0 -111
  225. package/framework/skills/colorize/SKILL.md +0 -170
  226. package/framework/skills/critique/SKILL.md +0 -126
  227. package/framework/skills/deep-research/SKILL.md +0 -240
  228. package/framework/skills/delight/SKILL.md +0 -329
  229. package/framework/skills/deploy/SKILL.md +0 -261
  230. package/framework/skills/deploy-verify/SKILL.md +0 -377
  231. package/framework/skills/deploy-verify/scripts/canary-check.sh +0 -206
  232. package/framework/skills/deploy-verify/scripts/check-console-errors.js +0 -147
  233. package/framework/skills/deploy-verify/scripts/check-cwv.js +0 -139
  234. package/framework/skills/deploy-verify/scripts/project-detect.sh +0 -84
  235. package/framework/skills/deploy-verify/scripts/verify.sh +0 -548
  236. package/framework/skills/design-quieter/SKILL.md +0 -130
  237. package/framework/skills/distill/SKILL.md +0 -149
  238. package/framework/skills/docs-lookup/SKILL.md +0 -79
  239. package/framework/skills/fcm-notifications/SKILL.md +0 -125
  240. package/framework/skills/financial-ledger/SKILL.md +0 -1039
  241. package/framework/skills/frontend-master/NOTICE.md +0 -4
  242. package/framework/skills/frontend-master/SKILL.md +0 -127
  243. package/framework/skills/frontend-master/reference/color-and-contrast.md +0 -132
  244. package/framework/skills/frontend-master/reference/interaction-design.md +0 -123
  245. package/framework/skills/frontend-master/reference/motion-design.md +0 -99
  246. package/framework/skills/frontend-master/reference/responsive-design.md +0 -114
  247. package/framework/skills/frontend-master/reference/spatial-design.md +0 -100
  248. package/framework/skills/frontend-master/reference/typography.md +0 -131
  249. package/framework/skills/frontend-master/reference/ux-writing.md +0 -107
  250. package/framework/skills/harden/SKILL.md +0 -357
  251. package/framework/skills/i18n-rtl/SKILL.md +0 -752
  252. package/framework/skills/learn/SKILL.md +0 -95
  253. package/framework/skills/memory/SKILL.md +0 -50
  254. package/framework/skills/mobile-expo/SKILL.md +0 -977
  255. package/framework/skills/mobile-expo/references/store-checklist.md +0 -550
  256. package/framework/skills/nestjs-backend/README.md +0 -73
  257. package/framework/skills/nestjs-backend/SKILL.md +0 -446
  258. package/framework/skills/nestjs-backend/references/templates.md +0 -1173
  259. package/framework/skills/normalize/SKILL.md +0 -79
  260. package/framework/skills/onboard/SKILL.md +0 -242
  261. package/framework/skills/openrouter-agent/SKILL.md +0 -922
  262. package/framework/skills/polish/SKILL.md +0 -209
  263. package/framework/skills/pr/SKILL.md +0 -66
  264. package/framework/skills/qualia/SKILL.md +0 -199
  265. package/framework/skills/qualia-add-todo/SKILL.md +0 -68
  266. package/framework/skills/qualia-audit-milestone/SKILL.md +0 -95
  267. package/framework/skills/qualia-check-todos/SKILL.md +0 -55
  268. package/framework/skills/qualia-complete-milestone/SKILL.md +0 -134
  269. package/framework/skills/qualia-debug/SKILL.md +0 -149
  270. package/framework/skills/qualia-design/SKILL.md +0 -203
  271. package/framework/skills/qualia-discuss-phase/SKILL.md +0 -72
  272. package/framework/skills/qualia-evolve/SKILL.md +0 -200
  273. package/framework/skills/qualia-execute-phase/SKILL.md +0 -89
  274. package/framework/skills/qualia-framework-audit/SKILL.md +0 -604
  275. package/framework/skills/qualia-guide/SKILL.md +0 -32
  276. package/framework/skills/qualia-help/SKILL.md +0 -114
  277. package/framework/skills/qualia-idk/SKILL.md +0 -352
  278. package/framework/skills/qualia-list-phase-assumptions/SKILL.md +0 -67
  279. package/framework/skills/qualia-new-milestone/SKILL.md +0 -72
  280. package/framework/skills/qualia-new-project/SKILL.md +0 -232
  281. package/framework/skills/qualia-optimize/SKILL.md +0 -417
  282. package/framework/skills/qualia-pause-work/SKILL.md +0 -96
  283. package/framework/skills/qualia-plan-milestone-gaps/SKILL.md +0 -57
  284. package/framework/skills/qualia-plan-phase/SKILL.md +0 -104
  285. package/framework/skills/qualia-production-check/SKILL.md +0 -0
  286. package/framework/skills/qualia-progress/SKILL.md +0 -53
  287. package/framework/skills/qualia-quick/SKILL.md +0 -89
  288. package/framework/skills/qualia-report/SKILL.md +0 -166
  289. package/framework/skills/qualia-research-phase/SKILL.md +0 -88
  290. package/framework/skills/qualia-resume-work/SKILL.md +0 -62
  291. package/framework/skills/qualia-review/SKILL.md +0 -263
  292. package/framework/skills/qualia-start/SKILL.md +0 -161
  293. package/framework/skills/qualia-verify-work/SKILL.md +0 -132
  294. package/framework/skills/rag/SKILL.md +0 -750
  295. package/framework/skills/responsive/SKILL.md +0 -231
  296. package/framework/skills/retro/SKILL.md +0 -284
  297. package/framework/skills/sakani-conventions/SKILL.md +0 -136
  298. package/framework/skills/sakani-conventions/evals/evals.json +0 -23
  299. package/framework/skills/sakani-conventions/references/entities.md +0 -365
  300. package/framework/skills/sakani-conventions/references/error-codes.md +0 -95
  301. package/framework/skills/seo-master/SKILL.md +0 -490
  302. package/framework/skills/seo-master/references/checklist.md +0 -199
  303. package/framework/skills/seo-master/references/structured-data.md +0 -609
  304. package/framework/skills/ship/SKILL.md +0 -239
  305. package/framework/skills/stack-researcher/SKILL.md +0 -215
  306. package/framework/skills/status/SKILL.md +0 -154
  307. package/framework/skills/status/scripts/health-check.sh +0 -562
  308. package/framework/skills/subscription-payments/SKILL.md +0 -250
  309. package/framework/skills/supabase/SKILL.md +0 -973
  310. package/framework/skills/supabase/references/templates.md +0 -159
  311. package/framework/skills/team/SKILL.md +0 -67
  312. package/framework/skills/test-runner/SKILL.md +0 -202
  313. package/framework/skills/voice-agent/SKILL.md +0 -1312
  314. package/framework/skills/zoho-workflow/SKILL.md +0 -51
  315. package/framework/statusline-command.sh +0 -117
  316. package/framework/teams/default/inboxes/plan-04.json +0 -9
  317. package/framework/teams/review-team.md +0 -75
  318. package/framework/teams/ship-team.md +0 -86
  319. package/profiles/fawzi.json +0 -16
  320. package/profiles/hasan.json +0 -16
  321. package/profiles/moayad.json +0 -16
  322. package/templates/CLAUDE-owner.md +0 -52
  323. package/templates/CLAUDE.md.hbs +0 -58
  324. package/templates/env.claude.template +0 -12
  325. package/templates/settings.json +0 -172
  326. /package/{framework/rules → rules}/deployment.md +0 -0
  327. /package/{framework/rules → rules}/security.md +0 -0
package/bin/state.js ADDED
@@ -0,0 +1,824 @@
1
+ #!/usr/bin/env node
2
+ // Qualia State Machine — atomic state transitions with precondition validation
3
+ // No external dependencies. Node >= 18 only.
4
+
5
+ const fs = require("fs");
6
+ const path = require("path");
7
+
8
+ const PLANNING = ".planning";
9
+ const STATE_FILE = path.join(PLANNING, "STATE.md");
10
+ const TRACKING_FILE = path.join(PLANNING, "tracking.json");
11
+
12
+ // ─── Trace ──────────────────────────────────────────────
13
+ function _trace(event, data) {
14
+ try {
15
+ const traceDir = path.join(require("os").homedir(), ".claude", ".qualia-traces");
16
+ if (!fs.existsSync(traceDir)) fs.mkdirSync(traceDir, { recursive: true });
17
+ const entry = { hook: event, timestamp: new Date().toISOString(), ...data };
18
+ const file = path.join(traceDir, `${new Date().toISOString().split("T")[0]}.jsonl`);
19
+ fs.appendFileSync(file, JSON.stringify(entry) + "\n");
20
+ } catch { /* trace failures must not disrupt state machine */ }
21
+ }
22
+
23
+ // ─── Arg Parsing ─────────────────────────────────────────
24
+ function parseArgs(argv) {
25
+ const args = {};
26
+ for (let i = 0; i < argv.length; i++) {
27
+ if (argv[i].startsWith("--")) {
28
+ const key = argv[i].slice(2).replace(/-/g, "_");
29
+ const next = argv[i + 1];
30
+ if (!next || next.startsWith("--")) {
31
+ args[key] = true;
32
+ } else {
33
+ args[key] = next;
34
+ i++;
35
+ }
36
+ }
37
+ }
38
+ return args;
39
+ }
40
+
41
+ // ─── File I/O ────────────────────────────────────────────
42
+ function readTracking() {
43
+ try {
44
+ return JSON.parse(fs.readFileSync(TRACKING_FILE, "utf8"));
45
+ } catch {
46
+ return null;
47
+ }
48
+ }
49
+
50
+ function writeTracking(t) {
51
+ fs.writeFileSync(TRACKING_FILE, JSON.stringify(t, null, 2) + "\n");
52
+ }
53
+
54
+ function readState() {
55
+ try {
56
+ return fs.readFileSync(STATE_FILE, "utf8");
57
+ } catch {
58
+ return null;
59
+ }
60
+ }
61
+
62
+ // ─── STATE.md Parser ─────────────────────────────────────
63
+ function parseStateMd(content) {
64
+ if (!content) return null;
65
+ const schema_errors = [];
66
+ const get = (prefix) => {
67
+ const m = content.match(new RegExp(`^${prefix}:\\s*(.+)$`, "m"));
68
+ return m ? m[1].trim() : "";
69
+ };
70
+ const hasField = (prefix) =>
71
+ new RegExp(`^${prefix}:\\s*`, "m").test(content);
72
+
73
+ const phaseMatch = content.match(
74
+ /^Phase:\s*(\d+)\s+of\s+(\d+)\s*[—-]\s*(.+)$/m
75
+ );
76
+ if (!phaseMatch) {
77
+ schema_errors.push({
78
+ field: "phase_header",
79
+ message: 'Missing or malformed "Phase: N of M — Name" header',
80
+ severity: "error",
81
+ });
82
+ }
83
+
84
+ // Status field presence (independent of value)
85
+ if (!hasField("Status")) {
86
+ schema_errors.push({
87
+ field: "status_field",
88
+ message: "Missing Status: field",
89
+ severity: "warning",
90
+ });
91
+ }
92
+
93
+ // Parse roadmap table
94
+ const phases = [];
95
+ const tableHeaderRe = /\| # \| Phase \| Goal \| Status \|/;
96
+ const tableMatch = content.match(
97
+ /\| # \| Phase \| Goal \| Status \|\n\|[-|]+\|\n([\s\S]*?)(?=\n##|\n$|$)/
98
+ );
99
+ if (!tableHeaderRe.test(content)) {
100
+ schema_errors.push({
101
+ field: "roadmap_table",
102
+ message: "Roadmap table header not found",
103
+ severity: "error",
104
+ });
105
+ } else if (!tableMatch) {
106
+ // Header is there but the separator row or body is malformed
107
+ schema_errors.push({
108
+ field: "roadmap_table",
109
+ message: "Roadmap table is malformed (missing separator row or body)",
110
+ severity: "error",
111
+ });
112
+ } else {
113
+ for (const row of tableMatch[1].trim().split("\n")) {
114
+ const cols = row.split("|").map((c) => c.trim()).filter(Boolean);
115
+ if (cols.length >= 4) {
116
+ phases.push({
117
+ num: parseInt(cols[0]),
118
+ name: cols[1],
119
+ goal: cols[2],
120
+ status: cols[3],
121
+ });
122
+ }
123
+ }
124
+ }
125
+
126
+ // Row count vs header "of M"
127
+ if (phaseMatch) {
128
+ const declaredTotal = parseInt(phaseMatch[2]);
129
+ if (phases.length && phases.length !== declaredTotal) {
130
+ schema_errors.push({
131
+ field: "roadmap_rows",
132
+ message: `Expected ${declaredTotal} phases in roadmap, found ${phases.length}`,
133
+ severity: "warning",
134
+ });
135
+ }
136
+ }
137
+
138
+ return {
139
+ phase: phaseMatch ? parseInt(phaseMatch[1]) : 1,
140
+ total_phases: phaseMatch ? parseInt(phaseMatch[2]) : phases.length || 1,
141
+ phase_name: phaseMatch ? phaseMatch[3].trim() : "",
142
+ status: get("Status").toLowerCase().replace(/\s+/g, "_") || "setup",
143
+ assigned_to: get("Assigned to") || "",
144
+ phases,
145
+ schema_errors,
146
+ };
147
+ }
148
+
149
+ // ─── STATE.md Writer ─────────────────────────────────────
150
+ function writeStateMd(s) {
151
+ const phaseFrac = Math.round(((s.phase - 1) / s.total_phases) * 100);
152
+ const filled = Math.round(phaseFrac / 10);
153
+ const bar = "█".repeat(filled) + "░".repeat(10 - filled);
154
+ const now = new Date().toISOString().split("T")[0];
155
+
156
+ const roadmap = s.phases
157
+ .map((p) => `| ${p.num} | ${p.name} | ${p.goal} | ${p.status} |`)
158
+ .join("\n");
159
+
160
+ const md = `# Project State
161
+
162
+ ## Project
163
+ See: .planning/PROJECT.md
164
+
165
+ ## Current Position
166
+ Phase: ${s.phase} of ${s.total_phases} — ${s.phase_name}
167
+ Status: ${s.status}
168
+ Assigned to: ${s.assigned_to}
169
+ Last activity: ${now} — ${s.last_activity || "State updated"}
170
+
171
+ Progress: [${bar}] ${phaseFrac}%
172
+
173
+ ## Roadmap
174
+ | # | Phase | Goal | Status |
175
+ |---|-------|------|--------|
176
+ ${roadmap}
177
+
178
+ ## Blockers
179
+ ${s.blockers || "None."}
180
+
181
+ ## Session
182
+ Last session: ${now}
183
+ Last worked by: ${s.assigned_to}
184
+ Resume: ${s.resume || "—"}
185
+ `;
186
+ fs.writeFileSync(STATE_FILE, md);
187
+ }
188
+
189
+ // ─── Precondition Checks ─────────────────────────────────
190
+ const VALID_FROM = {
191
+ planned: ["setup", "verified"], // verified(fail) → planned = gap closure
192
+ built: ["planned"],
193
+ verified: ["built"],
194
+ polished: ["verified"],
195
+ shipped: ["polished"],
196
+ handed_off: ["shipped"],
197
+ done: ["handed_off"],
198
+ };
199
+
200
+ // ─── Configurable Gap Cycle Limit ────────────────────────
201
+ function getGapCycleLimit() {
202
+ // Priority: tracking.json.gap_cycle_limit > PROJECT.md > default (2)
203
+ try {
204
+ const t = readTracking();
205
+ if (t && typeof t.gap_cycle_limit === "number" && t.gap_cycle_limit > 0) {
206
+ return t.gap_cycle_limit;
207
+ }
208
+ } catch {}
209
+
210
+ try {
211
+ const projectMd = fs.readFileSync(path.join(PLANNING, "PROJECT.md"), "utf8");
212
+ const match = projectMd.match(/^gap_cycle_limit:\s*(\d+)/m);
213
+ if (match) return parseInt(match[1]);
214
+ } catch {}
215
+
216
+ return 2; // default
217
+ }
218
+
219
+ function checkPreconditions(current, target, opts) {
220
+ const phase = parseInt(opts.phase) || current.phase;
221
+
222
+ // Special transitions (no status gate)
223
+ if (target === "note" || target === "activity") return { ok: true };
224
+
225
+ // Check valid transition
226
+ const allowed = VALID_FROM[target];
227
+ if (!allowed) return fail("INVALID_STATUS", `Unknown status: ${target}`);
228
+ if (!allowed.includes(current.status)) {
229
+ return fail(
230
+ "PRECONDITION_FAILED",
231
+ `Cannot go from '${current.status}' to '${target}'. Allowed from: ${allowed.join(", ")}`
232
+ );
233
+ }
234
+
235
+ // File checks
236
+ if (target === "planned") {
237
+ const planFile = path.join(PLANNING, `phase-${phase}-plan.md`);
238
+ if (!fs.existsSync(planFile))
239
+ return fail("MISSING_FILE", `Plan file not found: ${planFile}`);
240
+ // Validate plan content (not just existence)
241
+ const planContent = fs.readFileSync(planFile, "utf8");
242
+ const taskHeaders = planContent.match(/^## Task \d+/gm);
243
+ if (!taskHeaders || taskHeaders.length === 0)
244
+ return fail("INVALID_PLAN", "Plan file has no task headers (expected '## Task N')");
245
+ const doneWhenCount = (planContent.match(/\*\*Done when:\*\*/g) || []).length;
246
+ if (doneWhenCount < taskHeaders.length)
247
+ return fail("INVALID_PLAN", `${taskHeaders.length} tasks but only ${doneWhenCount} 'Done when:' entries`);
248
+ }
249
+
250
+ if (target === "verified") {
251
+ const vFile = path.join(PLANNING, `phase-${phase}-verification.md`);
252
+ if (!fs.existsSync(vFile))
253
+ return fail("MISSING_FILE", `Verification file not found: ${vFile}`);
254
+ if (!opts.verification || !["pass", "fail"].includes(opts.verification))
255
+ return fail("MISSING_ARG", "--verification must be 'pass' or 'fail'");
256
+ }
257
+
258
+ if (target === "shipped") {
259
+ if (!opts.deployed_url)
260
+ return fail("MISSING_ARG", "--deployed-url is required for 'shipped'");
261
+ }
262
+
263
+ if (target === "handed_off") {
264
+ const hFile = path.join(PLANNING, "HANDOFF.md");
265
+ if (!fs.existsSync(hFile))
266
+ return fail("MISSING_FILE", `Handoff file not found: ${hFile}`);
267
+ }
268
+
269
+ // Gap-closure circuit breaker (configurable limit)
270
+ if (target === "planned" && current.status === "verified") {
271
+ const t = readTracking() || {};
272
+ const cycles = (t.gap_cycles || {})[String(phase)] || 0;
273
+ const limit = getGapCycleLimit();
274
+ if (cycles >= limit) {
275
+ return fail(
276
+ "GAP_CYCLE_LIMIT",
277
+ `Phase ${phase} has failed verification ${cycles} times (limit: ${limit}). Escalate to Fawzi or re-plan from scratch.`
278
+ );
279
+ }
280
+ }
281
+
282
+ return { ok: true };
283
+ }
284
+
285
+ function fail(error, message) {
286
+ return { ok: false, error, message };
287
+ }
288
+
289
+ // ─── Next Command Logic ──────────────────────────────────
290
+ function nextCommand(status, phase, totalPhases, verification) {
291
+ switch (status) {
292
+ case "setup":
293
+ return `/qualia-plan ${phase}`;
294
+ case "planned":
295
+ return `/qualia-build ${phase}`;
296
+ case "built":
297
+ return `/qualia-verify ${phase}`;
298
+ case "verified":
299
+ if (verification === "fail") return `/qualia-plan ${phase} --gaps`;
300
+ if (phase < totalPhases) return `/qualia-plan ${phase + 1}`;
301
+ return "/qualia-polish";
302
+ case "polished":
303
+ return "/qualia-ship";
304
+ case "shipped":
305
+ return "/qualia-handoff";
306
+ case "handed_off":
307
+ return "/qualia-report";
308
+ case "done":
309
+ return "Done.";
310
+ default:
311
+ return `/qualia`;
312
+ }
313
+ }
314
+
315
+ // ─── Commands ────────────────────────────────────────────
316
+
317
+ function cmdCheck(opts) {
318
+ const t = readTracking();
319
+ const s = parseStateMd(readState());
320
+ if (!t || !s) {
321
+ return output({
322
+ ok: false,
323
+ error: "NO_PROJECT",
324
+ message: "No .planning/ found. Run /qualia-new to start.",
325
+ });
326
+ }
327
+ output({
328
+ ok: true,
329
+ phase: s.phase,
330
+ phase_name: s.phase_name,
331
+ total_phases: s.total_phases,
332
+ status: s.status,
333
+ assigned_to: s.assigned_to,
334
+ verification: t.verification || "pending",
335
+ gap_cycles: (t.gap_cycles || {})[String(s.phase)] || 0,
336
+ gap_cycle_limit: getGapCycleLimit(),
337
+ tasks_done: t.tasks_done || 0,
338
+ tasks_total: t.tasks_total || 0,
339
+ deployed_url: t.deployed_url || "",
340
+ next_command: nextCommand(
341
+ s.status,
342
+ s.phase,
343
+ s.total_phases,
344
+ t.verification
345
+ ),
346
+ schema_errors: s.schema_errors && s.schema_errors.length ? s.schema_errors : undefined,
347
+ });
348
+ }
349
+
350
+ function cmdTransition(opts) {
351
+ const target = opts.to;
352
+ if (!target) return output(fail("MISSING_ARG", "--to is required"));
353
+
354
+ const t = readTracking();
355
+ const s = parseStateMd(readState());
356
+ if (!t || !s) {
357
+ return output(
358
+ fail("NO_PROJECT", "No .planning/ found. Run /qualia-new.")
359
+ );
360
+ }
361
+
362
+ // Refuse transitions if STATE.md has schema errors (severity=error)
363
+ if (s.schema_errors && s.schema_errors.some((e) => e.severity === "error")) {
364
+ return output(
365
+ fail(
366
+ "STATE_SCHEMA_ERROR",
367
+ "STATE.md is malformed. Run `node state.js check` to see errors. Consider `state.js fix` to rewrite canonically."
368
+ )
369
+ );
370
+ }
371
+
372
+ // Special: note/activity (no status change)
373
+ if (target === "note" || target === "activity") {
374
+ if (opts.notes) t.notes = opts.notes;
375
+ t.last_updated = new Date().toISOString();
376
+ writeTracking(t);
377
+ s.last_activity = opts.notes || "Activity logged";
378
+ writeStateMd(s);
379
+ return output({
380
+ ok: true,
381
+ phase: s.phase,
382
+ status: s.status,
383
+ action: target,
384
+ });
385
+ }
386
+
387
+ const phase = parseInt(opts.phase) || s.phase;
388
+
389
+ // Precondition check
390
+ const check = checkPreconditions(
391
+ { ...s, phase },
392
+ target,
393
+ { ...opts, phase }
394
+ );
395
+ if (!check.ok) {
396
+ // Force only bypasses status-ordering errors (PRECONDITION_FAILED, GAP_CYCLE_LIMIT).
397
+ // Never bypass MISSING_FILE, MISSING_ARG, INVALID_PLAN — those cause broken state.
398
+ const forceableErrors = ["PRECONDITION_FAILED", "GAP_CYCLE_LIMIT"];
399
+ if (opts.force && forceableErrors.includes(check.error)) {
400
+ console.error(`WARNING: Forcing transition despite: ${check.message}`);
401
+ } else {
402
+ return output(check);
403
+ }
404
+ }
405
+
406
+ const prevStatus = s.status;
407
+
408
+ // Apply transition
409
+ s.status = target;
410
+ s.last_activity = `${target} (phase ${phase})`;
411
+
412
+ // Update tracking fields
413
+ t.status = target;
414
+ t.phase = phase;
415
+ t.phase_name = s.phases[phase - 1]?.name || s.phase_name;
416
+ t.last_updated = new Date().toISOString();
417
+
418
+ if (target === "planned") {
419
+ // Gap closure: increment counter if coming from verified(fail)
420
+ if (prevStatus === "verified") {
421
+ if (!t.gap_cycles) t.gap_cycles = {};
422
+ t.gap_cycles[String(phase)] = (t.gap_cycles[String(phase)] || 0) + 1;
423
+ s.last_activity = `Gap closure #${t.gap_cycles[String(phase)]} planned (phase ${phase})`;
424
+ }
425
+ // Update roadmap
426
+ if (s.phases[phase - 1]) s.phases[phase - 1].status = "planned";
427
+ }
428
+
429
+ if (target === "built") {
430
+ t.tasks_done = parseInt(opts.tasks_done) || 0;
431
+ t.tasks_total = parseInt(opts.tasks_total) || 0;
432
+ t.wave = parseInt(opts.wave) || 0;
433
+ s.last_activity = `Phase ${phase} built (${t.tasks_done}/${t.tasks_total} tasks)`;
434
+ if (s.phases[phase - 1]) s.phases[phase - 1].status = "built";
435
+ }
436
+
437
+ if (target === "verified") {
438
+ t.verification = opts.verification;
439
+ s.last_activity = `Phase ${phase} verified — ${opts.verification}`;
440
+ if (s.phases[phase - 1])
441
+ s.phases[phase - 1].status =
442
+ opts.verification === "pass" ? "verified" : "failed";
443
+
444
+ // Auto-advance on pass
445
+ if (opts.verification === "pass") {
446
+ if (phase < s.total_phases) {
447
+ s.phase = phase + 1;
448
+ s.phase_name = s.phases[phase]?.name || `Phase ${phase + 1}`;
449
+ s.status = "setup";
450
+ t.phase = s.phase;
451
+ t.phase_name = s.phase_name;
452
+ t.status = "setup";
453
+ t.verification = "pending";
454
+ t.tasks_done = 0;
455
+ t.tasks_total = 0;
456
+ s.last_activity = `Phase ${phase} passed — advancing to phase ${s.phase}`;
457
+ }
458
+ // Reset gap counter for the passed phase
459
+ if (t.gap_cycles) t.gap_cycles[String(phase)] = 0;
460
+ }
461
+ }
462
+
463
+ if (target === "polished") {
464
+ if (s.phases[s.phases.length - 1])
465
+ s.phases[s.phases.length - 1].status = "verified";
466
+ }
467
+
468
+ if (target === "shipped") {
469
+ t.deployed_url = opts.deployed_url || "";
470
+ }
471
+
472
+ // Write both files
473
+ const backupState = readState();
474
+ try {
475
+ writeStateMd(s);
476
+ writeTracking(t);
477
+ } catch (e) {
478
+ // Revert STATE.md on failure
479
+ if (backupState) fs.writeFileSync(STATE_FILE, backupState);
480
+ return output(fail("WRITE_ERROR", e.message));
481
+ }
482
+
483
+ // Skill outcome scoring — log transition for analytics
484
+ _trace("state-transition", {
485
+ result: "allow",
486
+ phase: s.phase,
487
+ status: s.status,
488
+ previous_status: prevStatus,
489
+ verification: t.verification,
490
+ gap_closure: prevStatus === "verified" && target === "planned",
491
+ duration_ms: 0,
492
+ extra: { verification: t.verification, gap_closure: prevStatus === "verified" && target === "planned" }
493
+ });
494
+
495
+ output({
496
+ ok: true,
497
+ phase: s.phase,
498
+ phase_name: s.phase_name,
499
+ status: s.status,
500
+ previous_status: prevStatus,
501
+ verification: t.verification,
502
+ gap_cycles: (t.gap_cycles || {})[String(s.phase)] || 0,
503
+ next_command: nextCommand(s.status, s.phase, s.total_phases, t.verification),
504
+ });
505
+ }
506
+
507
+ function cmdInit(opts) {
508
+ if (!opts.project) return output(fail("MISSING_ARG", "--project required"));
509
+
510
+ // Parse phases
511
+ let phases = [];
512
+ if (opts.phases) {
513
+ try {
514
+ phases = JSON.parse(opts.phases);
515
+ } catch {
516
+ return output(fail("INVALID_ARG", "--phases must be valid JSON array"));
517
+ }
518
+ }
519
+ const totalPhases = parseInt(opts.total_phases) || phases.length || 1;
520
+
521
+ // Ensure phases array has entries
522
+ while (phases.length < totalPhases) {
523
+ phases.push({
524
+ name: `Phase ${phases.length + 1}`,
525
+ goal: "TBD",
526
+ });
527
+ }
528
+
529
+ // Create .planning/ if needed
530
+ if (!fs.existsSync(PLANNING)) fs.mkdirSync(PLANNING, { recursive: true });
531
+
532
+ const now = new Date().toISOString();
533
+ const date = now.split("T")[0];
534
+
535
+ // Build state
536
+ const s = {
537
+ phase: 1,
538
+ total_phases: totalPhases,
539
+ phase_name: phases[0].name,
540
+ status: "setup",
541
+ assigned_to: opts.assigned_to || "",
542
+ last_activity: `Project initialized`,
543
+ phases: phases.map((p, i) => ({
544
+ num: i + 1,
545
+ name: p.name,
546
+ goal: p.goal,
547
+ status: i === 0 ? "ready" : "—",
548
+ })),
549
+ blockers: "None.",
550
+ resume: "—",
551
+ };
552
+
553
+ // Build tracking
554
+ const t = {
555
+ project: opts.project,
556
+ client: opts.client || "",
557
+ type: opts.type || "",
558
+ assigned_to: opts.assigned_to || "",
559
+ phase: 1,
560
+ phase_name: phases[0].name,
561
+ total_phases: totalPhases,
562
+ status: "setup",
563
+ wave: 0,
564
+ tasks_done: 0,
565
+ tasks_total: 0,
566
+ verification: "pending",
567
+ gap_cycles: {},
568
+ blockers: [],
569
+ last_updated: now,
570
+ last_commit: "",
571
+ deployed_url: "",
572
+ notes: "",
573
+ };
574
+
575
+ writeStateMd(s);
576
+ writeTracking(t);
577
+
578
+ output({
579
+ ok: true,
580
+ action: "init",
581
+ project: opts.project,
582
+ phase: 1,
583
+ total_phases: totalPhases,
584
+ status: "setup",
585
+ next_command: "/qualia-plan 1",
586
+ });
587
+ }
588
+
589
+ function cmdFix(opts) {
590
+ const raw = readState();
591
+ const t = readTracking();
592
+ if (!raw && !t) {
593
+ return output(
594
+ fail("NO_PROJECT", "No .planning/ found. Run /qualia-new.")
595
+ );
596
+ }
597
+ const parsed = parseStateMd(raw) || {
598
+ phase: 1,
599
+ total_phases: 1,
600
+ phase_name: "",
601
+ status: "setup",
602
+ assigned_to: "",
603
+ phases: [],
604
+ schema_errors: [
605
+ { field: "content", message: "STATE.md missing or empty", severity: "error" },
606
+ ],
607
+ };
608
+ const previousErrors = (parsed.schema_errors || []).length;
609
+
610
+ // Prefer tracking.json values when parsed fields are defaulted/missing
611
+ const tr = t || {};
612
+ const totalPhases =
613
+ parseInt(tr.total_phases) || parsed.total_phases || parsed.phases.length || 1;
614
+ const phaseNum = parseInt(tr.phase) || parsed.phase || 1;
615
+ const phaseName =
616
+ (parsed.phase_name && parsed.phase_name.trim()) ||
617
+ tr.phase_name ||
618
+ `Phase ${phaseNum}`;
619
+ const status = parsed.status || tr.status || "setup";
620
+ const assignedTo = parsed.assigned_to || tr.assigned_to || "";
621
+
622
+ // Build a phases array of the right length
623
+ const phases = [];
624
+ for (let i = 0; i < totalPhases; i++) {
625
+ const existing = parsed.phases[i];
626
+ phases.push({
627
+ num: i + 1,
628
+ name: existing?.name || `Phase ${i + 1}`,
629
+ goal: existing?.goal || "TBD",
630
+ status: existing?.status || (i === 0 ? "ready" : "—"),
631
+ });
632
+ }
633
+
634
+ const s = {
635
+ phase: phaseNum,
636
+ total_phases: totalPhases,
637
+ phase_name: phaseName,
638
+ status,
639
+ assigned_to: assignedTo,
640
+ last_activity: "STATE.md repaired by state.js fix",
641
+ phases,
642
+ blockers: "None.",
643
+ resume: "—",
644
+ };
645
+
646
+ try {
647
+ writeStateMd(s);
648
+ } catch (e) {
649
+ return output(fail("WRITE_ERROR", e.message));
650
+ }
651
+
652
+ output({
653
+ ok: true,
654
+ action: "fix",
655
+ previous_errors: previousErrors,
656
+ fixed: true,
657
+ });
658
+ }
659
+
660
+ function cmdValidatePlan(opts) {
661
+ const phase = parseInt(opts.phase) || 1;
662
+ const planFile = path.join(PLANNING, `phase-${phase}-plan.md`);
663
+
664
+ if (!fs.existsSync(planFile)) {
665
+ return output(fail("MISSING_FILE", `Plan file not found: ${planFile}`));
666
+ }
667
+
668
+ const content = fs.readFileSync(planFile, "utf8");
669
+ const errors = [];
670
+
671
+ // Check frontmatter exists
672
+ if (!/^---\n/.test(content)) {
673
+ errors.push("Missing frontmatter (---) at start of file");
674
+ }
675
+
676
+ // Check task count > 0
677
+ const taskHeaders = content.match(/^## Task \d+/gm);
678
+ if (!taskHeaders || taskHeaders.length === 0) {
679
+ errors.push("No task headers found (expected '## Task N — title')");
680
+ }
681
+
682
+ // Check "Done when" exists for each task
683
+ const taskCount = taskHeaders ? taskHeaders.length : 0;
684
+ const doneWhenCount = (content.match(/\*\*Done when:\*\*/g) || []).length;
685
+ if (doneWhenCount < taskCount) {
686
+ errors.push(
687
+ `${taskCount} tasks but only ${doneWhenCount} 'Done when:' entries`
688
+ );
689
+ }
690
+
691
+ // Check Success Criteria section exists
692
+ if (!/## Success Criteria/m.test(content)) {
693
+ errors.push("Missing '## Success Criteria' section");
694
+ }
695
+
696
+ // Check goal in frontmatter
697
+ if (!/^goal:/m.test(content)) {
698
+ errors.push("Missing 'goal:' in frontmatter");
699
+ }
700
+
701
+ // ─── Verification Contract Validation (non-blocking) ────
702
+ const warnings = [];
703
+ const VALID_CHECK_TYPES = ["file-exists", "grep-match", "command-exit", "behavioral"];
704
+ let contractCount = 0;
705
+
706
+ if (/^## Verification Contract/m.test(content)) {
707
+ // Extract the contract section (from header to next ## or end of file)
708
+ const contractSectionMatch = content.match(
709
+ /^## Verification Contract\s*\n([\s\S]+)/m
710
+ );
711
+ if (contractSectionMatch) {
712
+ // Trim at the next ## heading that isn't ### (i.e., a new top-level section)
713
+ let contractSection = contractSectionMatch[1];
714
+ const nextH2 = contractSection.search(/\n## (?!#)/);
715
+ if (nextH2 !== -1) contractSection = contractSection.substring(0, nextH2);
716
+ // Each contract starts with ### Contract for Task N
717
+ const contractBlocks = contractSection.match(/^### Contract for Task \d+/gm);
718
+ contractCount = contractBlocks ? contractBlocks.length : 0;
719
+
720
+ if (contractCount === 0) {
721
+ warnings.push("Verification Contract section exists but contains no contract blocks (expected '### Contract for Task N')");
722
+ } else {
723
+ // Split into individual contract blocks for validation
724
+ const blockSplits = contractSection.split(/^(?=### Contract for Task \d+)/m).filter(Boolean);
725
+ for (const block of blockSplits) {
726
+ const taskNumMatch = block.match(/^### Contract for Task (\d+)/);
727
+ if (!taskNumMatch) continue;
728
+ const taskNum = taskNumMatch[1];
729
+
730
+ const checkTypeMatch = block.match(/\*\*Check type:\*\*\s*(.+)/);
731
+ const hasCommand = /\*\*Command:\*\*/.test(block);
732
+ const hasExpected = /\*\*Expected:\*\*/.test(block);
733
+ const hasFailIf = /\*\*Fail if:\*\*/.test(block);
734
+
735
+ if (!checkTypeMatch) {
736
+ warnings.push(`Contract for Task ${taskNum}: missing 'Check type'`);
737
+ } else {
738
+ const checkType = checkTypeMatch[1].trim().toLowerCase();
739
+ if (!VALID_CHECK_TYPES.includes(checkType)) {
740
+ warnings.push(
741
+ `Contract for Task ${taskNum}: invalid check type '${checkType}' (valid: ${VALID_CHECK_TYPES.join(", ")})`
742
+ );
743
+ }
744
+ // behavioral type doesn't require Command or Expected
745
+ const isBehavioral = checkType === "behavioral";
746
+ if (!isBehavioral && !hasCommand) {
747
+ warnings.push(`Contract for Task ${taskNum}: missing 'Command' (required for ${checkType})`);
748
+ }
749
+ if (!isBehavioral && !hasExpected) {
750
+ warnings.push(`Contract for Task ${taskNum}: missing 'Expected' (required for ${checkType})`);
751
+ }
752
+ }
753
+
754
+ if (!hasFailIf) {
755
+ warnings.push(`Contract for Task ${taskNum}: missing 'Fail if'`);
756
+ }
757
+ }
758
+ }
759
+
760
+ // Warn if contract count < task count
761
+ if (taskCount > 0 && contractCount > 0 && contractCount < taskCount) {
762
+ warnings.push(
763
+ `Only ${contractCount} contract(s) for ${taskCount} task(s) — not all tasks have verification contracts`
764
+ );
765
+ }
766
+ }
767
+ }
768
+
769
+ if (errors.length > 0) {
770
+ return output({
771
+ ok: false,
772
+ error: "PLAN_VALIDATION_FAILED",
773
+ phase,
774
+ errors,
775
+ warnings: warnings.length > 0 ? warnings : undefined,
776
+ message: `Plan file has ${errors.length} issue(s)`,
777
+ });
778
+ }
779
+
780
+ output({
781
+ ok: true,
782
+ action: "validate-plan",
783
+ phase,
784
+ task_count: taskCount,
785
+ done_when_count: doneWhenCount,
786
+ contract_count: contractCount,
787
+ warnings: warnings.length > 0 ? warnings : undefined,
788
+ });
789
+ }
790
+
791
+ // ─── Output ──────────────────────────────────────────────
792
+ function output(obj) {
793
+ console.log(JSON.stringify(obj, null, 2));
794
+ if (!obj.ok) process.exit(1);
795
+ }
796
+
797
+ // ─── Main ────────────────────────────────────────────────
798
+ const [cmd, ...rest] = process.argv.slice(2);
799
+ const opts = parseArgs(rest);
800
+
801
+ switch (cmd) {
802
+ case "check":
803
+ cmdCheck(opts);
804
+ break;
805
+ case "transition":
806
+ cmdTransition(opts);
807
+ break;
808
+ case "init":
809
+ cmdInit(opts);
810
+ break;
811
+ case "fix":
812
+ cmdFix(opts);
813
+ break;
814
+ case "validate-plan":
815
+ cmdValidatePlan(opts);
816
+ break;
817
+ default:
818
+ output(
819
+ fail(
820
+ "UNKNOWN_COMMAND",
821
+ `Usage: state.js <check|transition|init|fix|validate-plan> [--options]`
822
+ )
823
+ );
824
+ }