@jaimevalasek/aioson 1.7.2 → 1.8.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 (362) hide show
  1. package/CHANGELOG.md +35 -0
  2. package/README.md +153 -10
  3. package/docs/en/cli-reference.md +56 -1
  4. package/docs/en/i18n.md +18 -18
  5. package/docs/en/schemas/index.json +10 -0
  6. package/docs/en/schemas/parallel-assign.schema.json +9 -0
  7. package/docs/en/schemas/parallel-doctor.schema.json +36 -0
  8. package/docs/en/schemas/parallel-guard.schema.json +63 -0
  9. package/docs/en/schemas/parallel-merge.schema.json +84 -0
  10. package/docs/en/schemas/parallel-status.schema.json +91 -1
  11. package/docs/integrations/apps-publish-marketplace.md +94 -0
  12. package/docs/pt/README.md +9 -0
  13. package/docs/pt/agentes.md +324 -3
  14. package/docs/pt/clientes-ai.md +7 -3
  15. package/docs/pt/comandos-cli.md +160 -13
  16. package/docs/pt/compress-agents.md +304 -0
  17. package/docs/pt/design-docs-governance.md +59 -0
  18. package/docs/pt/feature-archive.md +191 -0
  19. package/docs/pt/genome-3.0-spec.md +115 -4
  20. package/docs/pt/genome-distribution.md +232 -0
  21. package/docs/pt/inicio-rapido.md +1 -0
  22. package/docs/pt/motor-hardening.md +492 -0
  23. package/docs/pt/runner-system.md +113 -0
  24. package/package.json +2 -1
  25. package/src/agent-manifests.js +66 -0
  26. package/src/agents.js +27 -7
  27. package/src/autonomy-policy.js +139 -0
  28. package/src/brain-query.js +161 -0
  29. package/src/cli.js +1377 -1099
  30. package/src/commands/agents.js +102 -7
  31. package/src/commands/artifact-validate.js +33 -4
  32. package/src/commands/auth.js +272 -0
  33. package/src/commands/brain-query.js +44 -0
  34. package/src/commands/briefing.js +344 -0
  35. package/src/commands/commit-prepare.js +547 -0
  36. package/src/commands/compress-agents.js +416 -0
  37. package/src/commands/context-health.js +4 -2
  38. package/src/commands/context-trim.js +17 -11
  39. package/src/commands/design-hybrid-options.js +3 -3
  40. package/src/commands/devlog-process.js +6 -4
  41. package/src/commands/dossier.js +423 -0
  42. package/src/commands/feature-archive.js +513 -0
  43. package/src/commands/feature-close.js +123 -18
  44. package/src/commands/gate-approve.js +198 -0
  45. package/src/commands/gate-check.js +24 -5
  46. package/src/commands/genome-doctor.js +166 -9
  47. package/src/commands/git-guard.js +170 -0
  48. package/src/commands/harness.js +121 -0
  49. package/src/commands/implementation-plan.js +47 -20
  50. package/src/commands/init.js +6 -2
  51. package/src/commands/install.js +6 -2
  52. package/src/commands/live.js +497 -56
  53. package/src/commands/locale-apply.js +9 -6
  54. package/src/commands/locale-diff.js +11 -112
  55. package/src/commands/mcp-doctor.js +2 -1
  56. package/src/commands/mcp-init.js +4 -10
  57. package/src/commands/memory.js +234 -0
  58. package/src/commands/parallel-assign.js +107 -27
  59. package/src/commands/parallel-doctor.js +416 -3
  60. package/src/commands/parallel-guard.js +241 -0
  61. package/src/commands/parallel-init.js +66 -4
  62. package/src/commands/parallel-merge.js +299 -0
  63. package/src/commands/parallel-status.js +147 -3
  64. package/src/commands/preflight.js +63 -4
  65. package/src/commands/qa-init.js +10 -5
  66. package/src/commands/revision.js +235 -0
  67. package/src/commands/scaffold-complete.js +188 -0
  68. package/src/commands/security-audit.js +275 -0
  69. package/src/commands/security-scan.js +376 -0
  70. package/src/commands/self-implement-loop.js +46 -2
  71. package/src/commands/setup-context.js +11 -10
  72. package/src/commands/squad-agent-create.js +51 -9
  73. package/src/commands/squad-investigate.js +53 -0
  74. package/src/commands/squad-plan.js +33 -1
  75. package/src/commands/squad-scaffold.js +4 -3
  76. package/src/commands/squad-score.js +71 -14
  77. package/src/commands/squad-status.js +22 -1
  78. package/src/commands/squad-validate.js +93 -2
  79. package/src/commands/store-genome.js +304 -0
  80. package/src/commands/store-skill.js +247 -0
  81. package/src/commands/store-squad.js +431 -0
  82. package/src/commands/store-system.js +392 -0
  83. package/src/commands/tool-capabilities.js +63 -0
  84. package/src/commands/update.js +3 -3
  85. package/src/commands/verify-gate.js +40 -0
  86. package/src/commands/workflow-execute.js +644 -155
  87. package/src/commands/workflow-harden.js +231 -0
  88. package/src/commands/workflow-heal.js +136 -0
  89. package/src/commands/workflow-next.js +460 -22
  90. package/src/commands/workflow-status.js +328 -138
  91. package/src/commands/workspace.js +144 -0
  92. package/src/constants.js +42 -75
  93. package/src/context-memory.js +133 -4
  94. package/src/context-writer.js +2 -1
  95. package/src/context.js +32 -2
  96. package/src/doctor.js +46 -6
  97. package/src/dossier/codemap-store.js +267 -0
  98. package/src/dossier/dossier-bootstrap.js +222 -0
  99. package/src/dossier/dossier-compact.js +159 -0
  100. package/src/dossier/lock.js +128 -0
  101. package/src/dossier/revision-store.js +313 -0
  102. package/src/dossier/schema.js +155 -0
  103. package/src/dossier/store.js +400 -0
  104. package/src/execution-gateway.js +3 -0
  105. package/src/friction-scanner.js +202 -0
  106. package/src/genome-schema.js +24 -1
  107. package/src/genomes.js +33 -0
  108. package/src/handoff-contract.js +363 -0
  109. package/src/handoff-validator.js +45 -0
  110. package/src/harness/circuit-breaker.js +135 -0
  111. package/src/i18n/messages/en.js +317 -22
  112. package/src/i18n/messages/es.js +259 -18
  113. package/src/i18n/messages/fr.js +260 -18
  114. package/src/i18n/messages/pt-BR.js +313 -22
  115. package/src/install-profile.js +0 -16
  116. package/src/installer.js +70 -6
  117. package/src/lib/git-commit-guard.js +691 -0
  118. package/src/lib/security/artifact-reader.js +167 -0
  119. package/src/lib/security/exit-codes.js +51 -0
  120. package/src/lib/security/findings-writer.js +176 -0
  121. package/src/lib/security/runtime-events.js +77 -0
  122. package/src/lib/security/secrets-regex.js +115 -0
  123. package/src/lib/store/security-scan.js +173 -0
  124. package/src/lib/terminal-checkbox.js +130 -0
  125. package/src/lib/tmux-launcher.js +163 -0
  126. package/src/lib/tool-capabilities.js +102 -0
  127. package/src/locales.js +12 -8
  128. package/src/parallel-workspace.js +756 -0
  129. package/src/parser.js +8 -1
  130. package/src/path-guard.js +47 -0
  131. package/src/preflight-engine.js +237 -26
  132. package/src/self-healing.js +142 -0
  133. package/src/session-handoff.js +111 -1
  134. package/src/squad/squad-scaffold.js +183 -19
  135. package/src/test-briefing.js +226 -0
  136. package/src/updater.js +1 -1
  137. package/src/utils.js +3 -0
  138. package/src/workflow-gates.js +185 -0
  139. package/template/.aioson/agents/analyst.md +76 -130
  140. package/template/.aioson/agents/architect.md +53 -86
  141. package/template/.aioson/agents/committer.md +161 -0
  142. package/template/.aioson/agents/cypher.md +252 -0
  143. package/template/.aioson/agents/dev.md +112 -628
  144. package/template/.aioson/agents/deyvin.md +33 -236
  145. package/template/.aioson/agents/discover.md +235 -0
  146. package/template/.aioson/agents/discovery-design-doc.md +17 -252
  147. package/template/.aioson/agents/genome.md +76 -26
  148. package/template/.aioson/agents/manifests/analyst.manifest.json +26 -0
  149. package/template/.aioson/agents/manifests/architect.manifest.json +23 -0
  150. package/template/.aioson/agents/manifests/committer.manifest.json +23 -0
  151. package/template/.aioson/agents/manifests/dev.manifest.json +37 -0
  152. package/template/.aioson/agents/manifests/orchestrator.manifest.json +30 -0
  153. package/template/.aioson/agents/manifests/pentester.manifest.json +39 -0
  154. package/template/.aioson/agents/manifests/pm.manifest.json +26 -0
  155. package/template/.aioson/agents/manifests/product.manifest.json +23 -0
  156. package/template/.aioson/agents/manifests/qa.manifest.json +25 -0
  157. package/template/.aioson/agents/manifests/setup.manifest.json +20 -0
  158. package/template/.aioson/agents/manifests/ux-ui.manifest.json +24 -0
  159. package/template/.aioson/agents/neo.md +5 -7
  160. package/template/.aioson/agents/orache.md +2 -6
  161. package/template/.aioson/agents/orchestrator.md +81 -182
  162. package/template/.aioson/agents/pentester.md +235 -0
  163. package/template/.aioson/agents/pm.md +40 -104
  164. package/template/.aioson/agents/product.md +99 -344
  165. package/template/.aioson/agents/profiler-enricher.md +57 -6
  166. package/template/.aioson/agents/profiler-forge.md +17 -7
  167. package/template/.aioson/agents/profiler-researcher.md +29 -6
  168. package/template/.aioson/agents/qa.md +168 -514
  169. package/template/.aioson/agents/setup.md +52 -278
  170. package/template/.aioson/agents/sheldon.md +122 -754
  171. package/template/.aioson/agents/site-forge.md +111 -1583
  172. package/template/.aioson/agents/squad.md +139 -2010
  173. package/template/.aioson/agents/tester.md +10 -0
  174. package/template/.aioson/agents/ux-ui.md +104 -812
  175. package/template/.aioson/agents/validator.md +69 -0
  176. package/template/.aioson/brains/scripts/query.js +5 -1
  177. package/template/.aioson/config/autonomy-protocol.json +43 -0
  178. package/template/.aioson/config.md +43 -15
  179. package/template/.aioson/constitution.md +36 -33
  180. package/template/.aioson/context/design-doc.md +136 -0
  181. package/template/.aioson/context/project-map.md +57 -0
  182. package/template/.aioson/design-docs/code-reuse.md +48 -0
  183. package/template/.aioson/design-docs/componentization.md +47 -0
  184. package/template/.aioson/design-docs/file-size.md +52 -0
  185. package/template/.aioson/design-docs/folder-structure.md +51 -0
  186. package/template/.aioson/design-docs/naming.md +54 -0
  187. package/template/.aioson/docs/LAYERS.md +12 -2
  188. package/template/.aioson/docs/dev/execution-discipline.md +106 -0
  189. package/template/.aioson/docs/dev/stack-conventions.md +83 -0
  190. package/template/.aioson/docs/deyvin/continuity-recovery.md +57 -0
  191. package/template/.aioson/docs/deyvin/debugging-escalation.md +30 -0
  192. package/template/.aioson/docs/deyvin/pair-execution.md +44 -0
  193. package/template/.aioson/docs/deyvin/runtime-handoffs.md +36 -0
  194. package/template/.aioson/docs/product/conversation-playbook.md +116 -0
  195. package/template/.aioson/docs/product/prd-contract.md +107 -0
  196. package/template/.aioson/docs/product/quality-lens.md +57 -0
  197. package/template/.aioson/docs/product/research-loop.md +65 -0
  198. package/template/.aioson/docs/sheldon/enrichment-paths.md +134 -0
  199. package/template/.aioson/docs/sheldon/quality-lens.md +57 -0
  200. package/template/.aioson/docs/sheldon/research-loop.md +56 -0
  201. package/template/.aioson/docs/sheldon/web-intelligence.md +75 -0
  202. package/template/.aioson/docs/site-forge-build.md +195 -0
  203. package/template/.aioson/docs/site-forge-extraction.md +135 -0
  204. package/template/.aioson/docs/site-forge-qa.md +155 -0
  205. package/template/.aioson/docs/site-forge-recon.md +434 -0
  206. package/template/.aioson/docs/site-forge-transform.md +249 -0
  207. package/template/.aioson/docs/squad/content-output.md +91 -0
  208. package/template/.aioson/docs/squad/creation-flow.md +135 -0
  209. package/template/.aioson/docs/squad/domain-classification.md +117 -0
  210. package/template/.aioson/docs/squad/genome-bindings.md +47 -0
  211. package/template/.aioson/docs/squad/package-contract.md +234 -0
  212. package/template/.aioson/docs/squad/quality-lens.md +56 -0
  213. package/template/.aioson/docs/squad/research-loop.md +59 -0
  214. package/template/.aioson/docs/squad/session-operations.md +117 -0
  215. package/template/.aioson/docs/squad/workflow-quality.md +165 -0
  216. package/template/.aioson/docs/ux-ui/accessibility-audit.md +55 -0
  217. package/template/.aioson/docs/ux-ui/audit-mode.md +86 -0
  218. package/template/.aioson/docs/ux-ui/component-map.md +35 -0
  219. package/template/.aioson/docs/ux-ui/design-execution.md +111 -0
  220. package/template/.aioson/docs/ux-ui/design-gate.md +27 -0
  221. package/template/.aioson/docs/ux-ui/research-mode.md +39 -0
  222. package/template/.aioson/docs/ux-ui/site-delivery.md +156 -0
  223. package/template/.aioson/docs/ux-ui/token-contract.md +57 -0
  224. package/template/.aioson/genomes/copywriting.meta.json +48 -0
  225. package/template/.aioson/git-guard.json +11 -0
  226. package/template/.aioson/mcp/servers.md +0 -1
  227. package/template/.aioson/rules/agent-language-policy.md +93 -0
  228. package/template/.aioson/rules/aioson-context-boundary.md +63 -0
  229. package/template/.aioson/rules/canonical-path-contract.md +47 -0
  230. package/template/.aioson/rules/data-format-convention.md +24 -86
  231. package/template/.aioson/rules/disk-first-artifacts.md +44 -0
  232. package/template/.aioson/rules/output-brevity.md +44 -0
  233. package/template/.aioson/rules/prd-section-ownership.md +49 -0
  234. package/template/.aioson/rules/security-baseline.md +139 -0
  235. package/template/.aioson/rules/spec-level-ownership.md +61 -0
  236. package/template/.aioson/rules/squad-driver-pattern.md +81 -0
  237. package/template/.aioson/schemas/squad-blueprint.schema.json +24 -0
  238. package/template/.aioson/schemas/squad-manifest.schema.json +44 -0
  239. package/template/.aioson/skills/process/aioson-spec-driven/references/pm.md +30 -0
  240. package/template/.aioson/skills/process/secure-tdd/SKILL.md +97 -0
  241. package/template/.aioson/skills/process/secure-tdd/references/nextjs.md +81 -0
  242. package/template/.aioson/skills/process/secure-tdd/references/node-express.md +91 -0
  243. package/template/.aioson/skills/process/secure-tdd/references/planned-stacks.md +33 -0
  244. package/template/.aioson/skills/static/harness-validate/SKILL.md +46 -0
  245. package/template/.aioson/skills/static/web-research-cache.md +3 -0
  246. package/template/.aioson/tasks/squad-create.md +35 -8
  247. package/template/.aioson/tasks/squad-design.md +50 -2
  248. package/template/.aioson/tasks/squad-investigate.md +14 -1
  249. package/template/.claude/commands/aioson/agent/committer.md +5 -0
  250. package/template/.claude/commands/aioson/agent/copywriter.md +5 -0
  251. package/template/.claude/commands/aioson/agent/cypher.md +5 -0
  252. package/template/.claude/commands/aioson/agent/pair.md +5 -0
  253. package/template/.claude/commands/aioson/agent/validator.md +5 -0
  254. package/template/.gemini/commands/aios-analyst.toml +6 -3
  255. package/template/.gemini/commands/aios-architect.toml +7 -6
  256. package/template/.gemini/commands/aios-committer.toml +7 -0
  257. package/template/.gemini/commands/aios-copywriter.toml +7 -0
  258. package/template/.gemini/commands/aios-cypher.toml +7 -0
  259. package/template/.gemini/commands/aios-dev.toml +8 -7
  260. package/template/.gemini/commands/aios-deyvin.toml +6 -5
  261. package/template/.gemini/commands/aios-discovery-design-doc.toml +6 -3
  262. package/template/.gemini/commands/aios-genome.toml +7 -0
  263. package/template/.gemini/commands/aios-neo.toml +5 -3
  264. package/template/.gemini/commands/aios-orache.toml +7 -0
  265. package/template/.gemini/commands/aios-orchestrator.toml +8 -7
  266. package/template/.gemini/commands/aios-pair.toml +6 -5
  267. package/template/.gemini/commands/aios-pm.toml +8 -7
  268. package/template/.gemini/commands/aios-product.toml +5 -3
  269. package/template/.gemini/commands/aios-qa.toml +6 -5
  270. package/template/.gemini/commands/aios-setup.toml +5 -2
  271. package/template/.gemini/commands/aios-sheldon.toml +7 -0
  272. package/template/.gemini/commands/aios-site-forge.toml +7 -0
  273. package/template/.gemini/commands/aios-squad.toml +7 -0
  274. package/template/.gemini/commands/aios-tester.toml +6 -5
  275. package/template/.gemini/commands/aios-ux-ui.toml +8 -7
  276. package/template/.gemini/commands/aios-validator.toml +7 -0
  277. package/template/AGENTS.md +12 -1
  278. package/template/CLAUDE.md +5 -1
  279. package/template/.aioson/locales/en/agents/analyst.md +0 -244
  280. package/template/.aioson/locales/en/agents/architect.md +0 -245
  281. package/template/.aioson/locales/en/agents/dev.md +0 -397
  282. package/template/.aioson/locales/en/agents/deyvin.md +0 -137
  283. package/template/.aioson/locales/en/agents/discovery-design-doc.md +0 -27
  284. package/template/.aioson/locales/en/agents/genome.md +0 -212
  285. package/template/.aioson/locales/en/agents/neo.md +0 -8
  286. package/template/.aioson/locales/en/agents/orache.md +0 -6
  287. package/template/.aioson/locales/en/agents/orchestrator.md +0 -189
  288. package/template/.aioson/locales/en/agents/pair.md +0 -5
  289. package/template/.aioson/locales/en/agents/pm.md +0 -84
  290. package/template/.aioson/locales/en/agents/product.md +0 -378
  291. package/template/.aioson/locales/en/agents/profiler-enricher.md +0 -5
  292. package/template/.aioson/locales/en/agents/profiler-forge.md +0 -5
  293. package/template/.aioson/locales/en/agents/profiler-researcher.md +0 -5
  294. package/template/.aioson/locales/en/agents/qa.md +0 -270
  295. package/template/.aioson/locales/en/agents/setup.md +0 -421
  296. package/template/.aioson/locales/en/agents/sheldon.md +0 -455
  297. package/template/.aioson/locales/en/agents/squad.md +0 -449
  298. package/template/.aioson/locales/en/agents/tester.md +0 -6
  299. package/template/.aioson/locales/en/agents/ux-ui.md +0 -668
  300. package/template/.aioson/locales/es/agents/analyst.md +0 -225
  301. package/template/.aioson/locales/es/agents/architect.md +0 -245
  302. package/template/.aioson/locales/es/agents/dev.md +0 -370
  303. package/template/.aioson/locales/es/agents/deyvin.md +0 -99
  304. package/template/.aioson/locales/es/agents/discovery-design-doc.md +0 -21
  305. package/template/.aioson/locales/es/agents/genome.md +0 -104
  306. package/template/.aioson/locales/es/agents/neo.md +0 -50
  307. package/template/.aioson/locales/es/agents/orache.md +0 -105
  308. package/template/.aioson/locales/es/agents/orchestrator.md +0 -194
  309. package/template/.aioson/locales/es/agents/pair.md +0 -7
  310. package/template/.aioson/locales/es/agents/pm.md +0 -90
  311. package/template/.aioson/locales/es/agents/product.md +0 -372
  312. package/template/.aioson/locales/es/agents/profiler-enricher.md +0 -7
  313. package/template/.aioson/locales/es/agents/profiler-forge.md +0 -7
  314. package/template/.aioson/locales/es/agents/profiler-researcher.md +0 -7
  315. package/template/.aioson/locales/es/agents/qa.md +0 -198
  316. package/template/.aioson/locales/es/agents/setup.md +0 -405
  317. package/template/.aioson/locales/es/agents/sheldon.md +0 -309
  318. package/template/.aioson/locales/es/agents/squad.md +0 -532
  319. package/template/.aioson/locales/es/agents/tester.md +0 -9
  320. package/template/.aioson/locales/es/agents/ux-ui.md +0 -212
  321. package/template/.aioson/locales/fr/agents/analyst.md +0 -225
  322. package/template/.aioson/locales/fr/agents/architect.md +0 -245
  323. package/template/.aioson/locales/fr/agents/dev.md +0 -370
  324. package/template/.aioson/locales/fr/agents/deyvin.md +0 -99
  325. package/template/.aioson/locales/fr/agents/discovery-design-doc.md +0 -21
  326. package/template/.aioson/locales/fr/agents/genome.md +0 -104
  327. package/template/.aioson/locales/fr/agents/neo.md +0 -50
  328. package/template/.aioson/locales/fr/agents/orache.md +0 -106
  329. package/template/.aioson/locales/fr/agents/orchestrator.md +0 -194
  330. package/template/.aioson/locales/fr/agents/pair.md +0 -7
  331. package/template/.aioson/locales/fr/agents/pm.md +0 -90
  332. package/template/.aioson/locales/fr/agents/product.md +0 -372
  333. package/template/.aioson/locales/fr/agents/profiler-enricher.md +0 -7
  334. package/template/.aioson/locales/fr/agents/profiler-forge.md +0 -7
  335. package/template/.aioson/locales/fr/agents/profiler-researcher.md +0 -7
  336. package/template/.aioson/locales/fr/agents/qa.md +0 -198
  337. package/template/.aioson/locales/fr/agents/setup.md +0 -405
  338. package/template/.aioson/locales/fr/agents/sheldon.md +0 -309
  339. package/template/.aioson/locales/fr/agents/squad.md +0 -532
  340. package/template/.aioson/locales/fr/agents/tester.md +0 -9
  341. package/template/.aioson/locales/fr/agents/ux-ui.md +0 -212
  342. package/template/.aioson/locales/pt-BR/agents/analyst.md +0 -319
  343. package/template/.aioson/locales/pt-BR/agents/architect.md +0 -284
  344. package/template/.aioson/locales/pt-BR/agents/dev.md +0 -483
  345. package/template/.aioson/locales/pt-BR/agents/deyvin.md +0 -184
  346. package/template/.aioson/locales/pt-BR/agents/discovery-design-doc.md +0 -198
  347. package/template/.aioson/locales/pt-BR/agents/genome.md +0 -297
  348. package/template/.aioson/locales/pt-BR/agents/neo.md +0 -208
  349. package/template/.aioson/locales/pt-BR/agents/orache.md +0 -137
  350. package/template/.aioson/locales/pt-BR/agents/orchestrator.md +0 -324
  351. package/template/.aioson/locales/pt-BR/agents/pair.md +0 -5
  352. package/template/.aioson/locales/pt-BR/agents/pm.md +0 -182
  353. package/template/.aioson/locales/pt-BR/agents/product.md +0 -466
  354. package/template/.aioson/locales/pt-BR/agents/profiler-enricher.md +0 -5
  355. package/template/.aioson/locales/pt-BR/agents/profiler-forge.md +0 -5
  356. package/template/.aioson/locales/pt-BR/agents/profiler-researcher.md +0 -5
  357. package/template/.aioson/locales/pt-BR/agents/qa.md +0 -300
  358. package/template/.aioson/locales/pt-BR/agents/setup.md +0 -533
  359. package/template/.aioson/locales/pt-BR/agents/sheldon.md +0 -323
  360. package/template/.aioson/locales/pt-BR/agents/squad.md +0 -1330
  361. package/template/.aioson/locales/pt-BR/agents/tester.md +0 -449
  362. package/template/.aioson/locales/pt-BR/agents/ux-ui.md +0 -669
@@ -0,0 +1,513 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * aioson feature:archive — move artefatos de uma feature done para .aioson/context/done/{slug}/
5
+ *
6
+ * Designed to be called by agents automatically (e.g. from feature:close --verdict=PASS)
7
+ * so the end user never needs to type archive commands manually.
8
+ *
9
+ * Usage:
10
+ * aioson feature:archive . --feature=checkout
11
+ * aioson feature:archive . --feature=checkout --dry-run
12
+ * aioson feature:archive . --feature=checkout --restore
13
+ * aioson feature:archive . --feature=checkout --json
14
+ * aioson feature:archive . --feature=checkout --force (skip features.md status guard)
15
+ */
16
+
17
+ const fs = require('node:fs/promises');
18
+ const path = require('node:path');
19
+ const { contextDir, readFileSafe } = require('../preflight-engine');
20
+
21
+ const ARCHIVED_EXTENSIONS = ['md', 'yaml', 'yml', 'json'];
22
+
23
+ const GLOBAL_FILES = new Set([
24
+ 'project.context.md',
25
+ 'project-pulse.md',
26
+ 'project-map.md',
27
+ 'context-pack.md',
28
+ 'memory-index.md',
29
+ 'module-src.md',
30
+ 'features.md',
31
+ 'dev-state.md',
32
+ 'tasks.md',
33
+ 'discovery.md',
34
+ 'design-doc.md',
35
+ 'prd.md',
36
+ 'architecture.md',
37
+ 'spec.md',
38
+ 'spec.md.template',
39
+ 'test-plan.md',
40
+ 'test-inventory.md',
41
+ 'handoff-protocol.json',
42
+ 'last-handoff.json',
43
+ 'hardening-report.md',
44
+ 'qa-report-test-coverage.md',
45
+ 'sheldon-enrichment.md',
46
+ 'sheldon-validation.md'
47
+ ]);
48
+
49
+ function escapeRegExp(str) {
50
+ return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
51
+ }
52
+
53
+ function buildSlugMatcher(slug) {
54
+ const extsGroup = ARCHIVED_EXTENSIONS.join('|');
55
+ // Accepts `<prefix>-<slug>.<ext>` and also `<prefix>-<slug>-<tail>.<ext>`
56
+ // (e.g. qa-report-pentester-agent-hardening.md). Prefix collisions with other
57
+ // slugs are filtered out via readOtherSlugs() before the matcher is applied.
58
+ return new RegExp(`^[a-z][a-z0-9-]*-${escapeRegExp(slug)}(?:-[a-z0-9][a-z0-9-]*)?\\.(${extsGroup})$`, 'i');
59
+ }
60
+
61
+ async function readOtherSlugs(featuresPath, currentSlug) {
62
+ const content = await readFileSafe(featuresPath);
63
+ if (!content) return [];
64
+ const slugs = new Set();
65
+ const lines = content.split(/\r?\n/);
66
+ for (const line of lines) {
67
+ const m = line.match(/^\|\s*([a-z][a-z0-9-]*)\s*\|/i);
68
+ if (!m) continue;
69
+ const s = m[1].toLowerCase();
70
+ if (s === 'slug' || s === currentSlug.toLowerCase()) continue;
71
+ slugs.add(s);
72
+ }
73
+ return Array.from(slugs);
74
+ }
75
+
76
+ function belongsToOtherSlug(fileName, slug, otherSlugs) {
77
+ // If another registered slug starts with `${slug}-` and the file suffix
78
+ // matches that longer slug (possibly with an extra tail), the file belongs
79
+ // to the longer-named feature, not to `slug`.
80
+ const base = fileName.replace(/\.(md|yaml|yml|json)$/i, '');
81
+ const slugLower = slug.toLowerCase();
82
+ for (const other of otherSlugs) {
83
+ if (!other.startsWith(`${slugLower}-`)) continue;
84
+ const idx = base.toLowerCase().lastIndexOf(`-${other}`);
85
+ if (idx === -1) continue;
86
+ const afterMatch = base.slice(idx + 1 + other.length);
87
+ if (afterMatch === '' || afterMatch.startsWith('-')) return true;
88
+ }
89
+ return false;
90
+ }
91
+
92
+ async function dirExists(dirPath) {
93
+ try {
94
+ const stat = await fs.stat(dirPath);
95
+ return stat.isDirectory();
96
+ } catch {
97
+ return false;
98
+ }
99
+ }
100
+
101
+ async function readDirSafe(dirPath) {
102
+ try {
103
+ return await fs.readdir(dirPath, { withFileTypes: true });
104
+ } catch {
105
+ return [];
106
+ }
107
+ }
108
+
109
+ async function featureStatus(featuresPath, slug) {
110
+ const content = await readFileSafe(featuresPath);
111
+ if (!content) return { exists: false, status: null };
112
+ const row = new RegExp(`\\|\\s*${escapeRegExp(slug)}\\s*\\|\\s*([a-z_]+)\\s*\\|`, 'i');
113
+ const match = content.match(row);
114
+ if (!match) return { exists: false, status: null };
115
+ return { exists: true, status: match[1].toLowerCase() };
116
+ }
117
+
118
+ async function findSlugFiles(ctxDir, slug, otherSlugs = []) {
119
+ const matcher = buildSlugMatcher(slug);
120
+ const entries = await readDirSafe(ctxDir);
121
+ return entries
122
+ .filter((e) => e.isFile())
123
+ .map((e) => e.name)
124
+ .filter((name) => !GLOBAL_FILES.has(name))
125
+ .filter((name) => matcher.test(name))
126
+ .filter((name) => !belongsToOtherSlug(name, slug, otherSlugs));
127
+ }
128
+
129
+ async function findArchivedFiles(archiveDir) {
130
+ const entries = await readDirSafe(archiveDir);
131
+ return entries.filter((e) => e.isFile()).map((e) => e.name);
132
+ }
133
+
134
+ async function extractSummary(prdPath) {
135
+ const content = await readFileSafe(prdPath);
136
+ if (!content) return null;
137
+ const visionIdx = content.indexOf('## Vision');
138
+ if (visionIdx === -1) return null;
139
+ const after = content.slice(visionIdx + '## Vision'.length);
140
+ const lines = after.split(/\r?\n/);
141
+ for (const line of lines) {
142
+ const trimmed = line.trim();
143
+ if (!trimmed) continue;
144
+ if (trimmed.startsWith('#')) break;
145
+ if (trimmed.startsWith('[') && trimmed.endsWith(']')) continue;
146
+ return trimmed.replace(/\s+/g, ' ').slice(0, 160);
147
+ }
148
+ return null;
149
+ }
150
+
151
+ async function readCompletedDate(featuresPath, slug) {
152
+ const content = await readFileSafe(featuresPath);
153
+ if (!content) return null;
154
+ const re = new RegExp(`\\|\\s*${escapeRegExp(slug)}\\s*\\|[^|]*\\|[^|]*\\|\\s*([^|]+?)\\s*\\|`, 'i');
155
+ const match = content.match(re);
156
+ if (!match) return null;
157
+ const raw = match[1].trim();
158
+ if (!raw || raw === '—' || raw === '-' || raw.toLowerCase() === 'tbd') return null;
159
+ const isoMatch = raw.match(/\d{4}-\d{2}-\d{2}/);
160
+ return isoMatch ? isoMatch[0] : raw;
161
+ }
162
+
163
+ function manifestHeader() {
164
+ return [
165
+ '# Archived Features Manifest',
166
+ '',
167
+ '> Features whose artefacts were moved into `.aioson/context/done/{slug}/` after QA sign-off.',
168
+ '> Agents that need historical awareness (@cypher, @neo, @discover, @sheldon) read this file instead of globbing archived PRDs.',
169
+ '',
170
+ '| slug | completed | files | summary |',
171
+ '|------|-----------|-------|---------|',
172
+ ''
173
+ ].join('\n');
174
+ }
175
+
176
+ function parseManifest(content) {
177
+ if (!content) return { header: manifestHeader(), rows: new Map() };
178
+ const rows = new Map();
179
+ const lines = content.split(/\r?\n/);
180
+ for (const line of lines) {
181
+ const trimmed = line.trim();
182
+ if (!trimmed.startsWith('|')) continue;
183
+ if (/^\|\s*-+\s*\|/.test(trimmed)) continue;
184
+ if (/^\|\s*slug\s*\|/i.test(trimmed)) continue;
185
+ const cols = trimmed.split('|').slice(1, -1).map((c) => c.trim());
186
+ if (cols.length < 4) continue;
187
+ const [slug, completed, files, summary] = cols;
188
+ if (!slug) continue;
189
+ rows.set(slug, { slug, completed, files, summary });
190
+ }
191
+ return { header: manifestHeader(), rows };
192
+ }
193
+
194
+ function renderManifest(rows) {
195
+ const sorted = Array.from(rows.values()).sort((a, b) => {
196
+ if (a.completed && b.completed) return b.completed.localeCompare(a.completed);
197
+ if (a.completed) return -1;
198
+ if (b.completed) return 1;
199
+ return a.slug.localeCompare(b.slug);
200
+ });
201
+ const body = sorted
202
+ .map((r) => `| ${r.slug} | ${r.completed || '—'} | ${r.files} | ${r.summary || '—'} |`)
203
+ .join('\n');
204
+ return manifestHeader() + body + (body ? '\n' : '');
205
+ }
206
+
207
+ async function updateManifest(manifestPath, entry, mode) {
208
+ const existing = await readFileSafe(manifestPath);
209
+ const { rows } = parseManifest(existing);
210
+ if (mode === 'remove') {
211
+ rows.delete(entry.slug);
212
+ } else {
213
+ rows.set(entry.slug, entry);
214
+ }
215
+ await fs.writeFile(manifestPath, renderManifest(rows), 'utf8');
216
+ }
217
+
218
+ async function runFeatureArchive({ args = [], options = {}, logger }) {
219
+ const targetDir = path.resolve(process.cwd(), args[0] || '.');
220
+ const slug = options.feature ? String(options.feature) : null;
221
+ const dryRun = Boolean(options['dry-run'] || options.dryRun);
222
+ const restore = Boolean(options.restore);
223
+ const force = Boolean(options.force);
224
+ const jsonOut = Boolean(options.json);
225
+
226
+ const log = (msg) => { if (logger && !jsonOut) logger.log(msg); };
227
+
228
+ if (!slug) {
229
+ if (jsonOut) return { ok: false, reason: 'missing_feature' };
230
+ log('--feature=<slug> is required.');
231
+ return { ok: false };
232
+ }
233
+
234
+ if (!/^[a-z][a-z0-9-]*$/i.test(slug)) {
235
+ if (jsonOut) return { ok: false, reason: 'invalid_slug' };
236
+ log(`Invalid slug "${slug}" — use lowercase letters, digits and hyphens only.`);
237
+ return { ok: false };
238
+ }
239
+
240
+ const ctxDir = contextDir(targetDir);
241
+ const doneDir = path.join(ctxDir, 'done');
242
+ const archiveDir = path.join(doneDir, slug);
243
+ const manifestPath = path.join(doneDir, 'MANIFEST.md');
244
+ const featuresPath = path.join(ctxDir, 'features.md');
245
+
246
+ if (!(await dirExists(ctxDir))) {
247
+ if (jsonOut) return { ok: false, reason: 'no_context_dir' };
248
+ log(`.aioson/context/ not found at ${targetDir}. Run aioson setup first.`);
249
+ return { ok: false };
250
+ }
251
+
252
+ if (restore) {
253
+ return await runRestore({
254
+ slug, ctxDir, archiveDir, manifestPath, dryRun, jsonOut, log
255
+ });
256
+ }
257
+
258
+ const status = await featureStatus(featuresPath, slug);
259
+ if (!status.exists && !force) {
260
+ if (jsonOut) return { ok: false, reason: 'not_in_features', slug };
261
+ log(`Feature "${slug}" is not registered in features.md. Use --force to archive anyway.`);
262
+ return { ok: false };
263
+ }
264
+ if (status.exists && status.status !== 'done' && !force) {
265
+ if (jsonOut) return { ok: false, reason: 'not_done', slug, status: status.status };
266
+ log(`Feature "${slug}" has status "${status.status}" in features.md — only "done" features can be archived. Use --force to override.`);
267
+ return { ok: false };
268
+ }
269
+
270
+ const otherSlugs = await readOtherSlugs(featuresPath, slug);
271
+ const rootFiles = await findSlugFiles(ctxDir, slug, otherSlugs);
272
+ const alreadyArchived = (await dirExists(archiveDir)) ? await findArchivedFiles(archiveDir) : [];
273
+ const dossierSourceDir = path.join(ctxDir, 'features', slug);
274
+ const dossierTargetDir = path.join(archiveDir, 'dossier');
275
+ const hasDossierToMove = await dirExists(dossierSourceDir);
276
+ const dossierAlreadyArchived = await dirExists(dossierTargetDir);
277
+
278
+ if (
279
+ rootFiles.length === 0 &&
280
+ alreadyArchived.length === 0 &&
281
+ !hasDossierToMove &&
282
+ !dossierAlreadyArchived
283
+ ) {
284
+ if (jsonOut) return { ok: true, slug, moved: [], skipped: [], alreadyArchived: [], noop: true };
285
+ log(`No files matched "*-${slug}.{${ARCHIVED_EXTENSIONS.join(',')}}" in .aioson/context/ root and no features/${slug}/ dossier dir — nothing to archive.`);
286
+ return { ok: true, noop: true };
287
+ }
288
+
289
+ const toMove = [];
290
+ const toSkip = [];
291
+ for (const name of rootFiles) {
292
+ if (alreadyArchived.includes(name)) {
293
+ toSkip.push({ name, reason: 'already_archived' });
294
+ } else {
295
+ toMove.push(name);
296
+ }
297
+ }
298
+
299
+ const completed = await readCompletedDate(featuresPath, slug) || new Date().toISOString().slice(0, 10);
300
+ const prdName = `prd-${slug}.md`;
301
+ const prdPathInRoot = path.join(ctxDir, prdName);
302
+ const prdPathInArchive = path.join(archiveDir, prdName);
303
+ const summarySource = rootFiles.includes(prdName) ? prdPathInRoot
304
+ : alreadyArchived.includes(prdName) ? prdPathInArchive
305
+ : null;
306
+ const summary = summarySource ? await extractSummary(summarySource) : null;
307
+
308
+ const dossierPlan = hasDossierToMove
309
+ ? (dossierAlreadyArchived ? { action: 'skip', reason: 'already_archived' } : { action: 'move' })
310
+ : (dossierAlreadyArchived ? { action: 'noop', reason: 'already_archived' } : null);
311
+
312
+ if (dryRun) {
313
+ const result = {
314
+ ok: true,
315
+ dryRun: true,
316
+ slug,
317
+ targetDir: path.relative(targetDir, archiveDir),
318
+ move: toMove,
319
+ skip: toSkip,
320
+ dossier: dossierPlan
321
+ ? {
322
+ source: path.relative(targetDir, dossierSourceDir),
323
+ target: path.relative(targetDir, dossierTargetDir),
324
+ ...dossierPlan
325
+ }
326
+ : null,
327
+ manifestEntry: {
328
+ slug,
329
+ completed,
330
+ files: String(toMove.length + alreadyArchived.length),
331
+ summary: summary || '—'
332
+ }
333
+ };
334
+ if (jsonOut) return result;
335
+ log(`[dry-run] feature:archive — ${slug}:`);
336
+ log(` target: ${path.relative(targetDir, archiveDir)}/`);
337
+ log(` would move: ${toMove.length} file(s)`);
338
+ for (const f of toMove) log(` • ${f}`);
339
+ if (toSkip.length) {
340
+ log(` would skip: ${toSkip.length} file(s)`);
341
+ for (const s of toSkip) log(` • ${s.name} (${s.reason})`);
342
+ }
343
+ if (dossierPlan && dossierPlan.action === 'move') {
344
+ log(` would move dossier dir: features/${slug}/ → ${path.relative(targetDir, dossierTargetDir)}/`);
345
+ } else if (dossierPlan && dossierPlan.action === 'skip') {
346
+ log(` would skip dossier dir: already archived at ${path.relative(targetDir, dossierTargetDir)}/`);
347
+ }
348
+ log(` manifest entry: | ${slug} | ${completed} | ${toMove.length + alreadyArchived.length} | ${summary || '—'} |`);
349
+ return result;
350
+ }
351
+
352
+ await fs.mkdir(archiveDir, { recursive: true });
353
+
354
+ const moved = [];
355
+ for (const name of toMove) {
356
+ const from = path.join(ctxDir, name);
357
+ const to = path.join(archiveDir, name);
358
+ await fs.rename(from, to);
359
+ moved.push(name);
360
+ }
361
+
362
+ let dossierResult = null;
363
+ if (dossierPlan && dossierPlan.action === 'move') {
364
+ await fs.rename(dossierSourceDir, dossierTargetDir);
365
+ dossierResult = {
366
+ action: 'moved',
367
+ source: path.relative(targetDir, dossierSourceDir),
368
+ target: path.relative(targetDir, dossierTargetDir)
369
+ };
370
+ try {
371
+ const parent = path.join(ctxDir, 'features');
372
+ const remaining = await fs.readdir(parent);
373
+ if (remaining.length === 0) await fs.rmdir(parent);
374
+ } catch {
375
+ // parent missing or non-empty — leave it
376
+ }
377
+ } else if (dossierPlan && dossierPlan.action === 'skip') {
378
+ dossierResult = {
379
+ action: 'skipped',
380
+ reason: dossierPlan.reason,
381
+ target: path.relative(targetDir, dossierTargetDir)
382
+ };
383
+ }
384
+
385
+ const totalArchived = (await findArchivedFiles(archiveDir)).length;
386
+ const entry = {
387
+ slug,
388
+ completed,
389
+ files: String(totalArchived),
390
+ summary: summary || '—'
391
+ };
392
+ await updateManifest(manifestPath, entry, 'upsert');
393
+
394
+ const result = {
395
+ ok: true,
396
+ slug,
397
+ completed,
398
+ archiveDir: path.relative(targetDir, archiveDir),
399
+ moved,
400
+ skipped: toSkip,
401
+ totalArchived,
402
+ dossier: dossierResult,
403
+ manifestEntry: entry
404
+ };
405
+
406
+ if (jsonOut) return result;
407
+ log(`feature:archive — ${slug}:`);
408
+ log(` archive dir: ${path.relative(targetDir, archiveDir)}/`);
409
+ log(` moved: ${moved.length} file(s)`);
410
+ for (const f of moved) log(` • ${f}`);
411
+ if (toSkip.length) {
412
+ log(` skipped: ${toSkip.length} file(s) already in archive`);
413
+ for (const s of toSkip) log(` • ${s.name}`);
414
+ }
415
+ if (dossierResult && dossierResult.action === 'moved') {
416
+ log(` moved dossier dir: ${dossierResult.source}/ → ${dossierResult.target}/`);
417
+ } else if (dossierResult && dossierResult.action === 'skipped') {
418
+ log(` skipped dossier dir: already archived at ${dossierResult.target}/`);
419
+ }
420
+ log(` manifest updated: .aioson/context/done/MANIFEST.md`);
421
+ return result;
422
+ }
423
+
424
+ async function runRestore({ slug, ctxDir, archiveDir, manifestPath, dryRun, jsonOut, log }) {
425
+ if (!(await dirExists(archiveDir))) {
426
+ if (jsonOut) return { ok: false, reason: 'nothing_to_restore', slug };
427
+ log(`No archive found at .aioson/context/done/${slug}/ — nothing to restore.`);
428
+ return { ok: false };
429
+ }
430
+
431
+ const dossierTargetDir = path.join(archiveDir, 'dossier');
432
+ const dossierSourceDir = path.join(ctxDir, 'features', slug);
433
+ const hasDossierToRestore = await dirExists(dossierTargetDir);
434
+ const dossierConflict = hasDossierToRestore && (await dirExists(dossierSourceDir));
435
+
436
+ const archived = await findArchivedFiles(archiveDir);
437
+ const conflicts = [];
438
+ const toRestore = [];
439
+ for (const name of archived) {
440
+ const rootPath = path.join(ctxDir, name);
441
+ try {
442
+ await fs.access(rootPath);
443
+ conflicts.push(name);
444
+ } catch {
445
+ toRestore.push(name);
446
+ }
447
+ }
448
+ if (dossierConflict) conflicts.push(`features/${slug}/`);
449
+
450
+ if (conflicts.length > 0) {
451
+ if (jsonOut) return { ok: false, reason: 'restore_conflict', slug, conflicts };
452
+ log(`Cannot restore "${slug}" — files already exist in .aioson/context/ root:`);
453
+ for (const c of conflicts) log(` • ${c}`);
454
+ log(`Resolve manually before retrying --restore.`);
455
+ return { ok: false };
456
+ }
457
+
458
+ if (dryRun) {
459
+ const result = {
460
+ ok: true,
461
+ dryRun: true,
462
+ slug,
463
+ restore: toRestore,
464
+ dossier: hasDossierToRestore ? { action: 'restore', target: path.relative(ctxDir, dossierSourceDir) } : null
465
+ };
466
+ if (jsonOut) return result;
467
+ log(`[dry-run] feature:archive --restore — ${slug}:`);
468
+ log(` would restore: ${toRestore.length} file(s)`);
469
+ for (const f of toRestore) log(` • ${f}`);
470
+ if (hasDossierToRestore) log(` would restore dossier dir: ${path.relative(ctxDir, dossierTargetDir)}/ → features/${slug}/`);
471
+ return result;
472
+ }
473
+
474
+ const restored = [];
475
+ for (const name of toRestore) {
476
+ const from = path.join(archiveDir, name);
477
+ const to = path.join(ctxDir, name);
478
+ await fs.rename(from, to);
479
+ restored.push(name);
480
+ }
481
+
482
+ let dossierRestored = null;
483
+ if (hasDossierToRestore) {
484
+ await fs.mkdir(path.dirname(dossierSourceDir), { recursive: true });
485
+ await fs.rename(dossierTargetDir, dossierSourceDir);
486
+ dossierRestored = path.relative(ctxDir, dossierSourceDir);
487
+ }
488
+
489
+ try {
490
+ await fs.rmdir(archiveDir);
491
+ } catch {
492
+ // Directory not empty (manual files) — leave it alone.
493
+ }
494
+
495
+ await updateManifest(manifestPath, { slug }, 'remove');
496
+
497
+ const result = {
498
+ ok: true,
499
+ slug,
500
+ restored,
501
+ dossierRestored,
502
+ archiveDir: path.relative(ctxDir, archiveDir)
503
+ };
504
+ if (jsonOut) return result;
505
+ log(`feature:archive --restore — ${slug}:`);
506
+ log(` restored: ${restored.length} file(s)`);
507
+ for (const f of restored) log(` • ${f}`);
508
+ if (dossierRestored) log(` restored dossier dir: ${dossierRestored}/`);
509
+ log(` manifest updated: .aioson/context/done/MANIFEST.md`);
510
+ return result;
511
+ }
512
+
513
+ module.exports = { runFeatureArchive };
@@ -15,11 +15,83 @@
15
15
  const fs = require('node:fs/promises');
16
16
  const path = require('node:path');
17
17
  const { contextDir, readFileSafe, parseFrontmatter } = require('../preflight-engine');
18
+ const { runFeatureArchive } = require('./feature-archive');
18
19
 
19
20
  function nowDate() {
20
21
  return new Date().toISOString().slice(0, 10);
21
22
  }
22
23
 
24
+ function nowTimestamp() {
25
+ return new Date().toISOString();
26
+ }
27
+
28
+ function quoteYaml(value) {
29
+ return `"${String(value).replace(/\\/g, '\\\\').replace(/"/g, '\\"')}"`;
30
+ }
31
+
32
+ function extractRecentActivities(content) {
33
+ if (!content) return [];
34
+ const activityMatch = content.match(/## Recent Activity\n([\s\S]*?)(?=\n##|\s*$)/);
35
+ if (!activityMatch) return [];
36
+ return activityMatch[1]
37
+ .split('\n')
38
+ .filter((line) => line.trim().startsWith('-'))
39
+ .slice(-2);
40
+ }
41
+
42
+ async function updateProjectPulseFile(pulsePath, slug, verdict, summary, date) {
43
+ const existing = await readFileSafe(pulsePath);
44
+ if (!existing) return false;
45
+
46
+ const fm = parseFrontmatter(existing);
47
+ const gate = `Gate D: ${verdict === 'PASS' ? 'approved' : 'rejected'}`;
48
+ const recentActivities = extractRecentActivities(existing);
49
+ let activityLine = `- ${date} @qa → ${slug} (${gate}) VERDICT: ${verdict}`;
50
+ if (summary) activityLine += `: ${summary}`;
51
+ const dedupedActivities = recentActivities.filter((line) => line !== activityLine);
52
+
53
+ const activeFeature = verdict === 'PASS' ? '(none)' : slug;
54
+ const activeWork = verdict === 'PASS' ? '' : `${slug} → @qa → qa_failed`;
55
+ const blockers = verdict === 'PASS'
56
+ ? 'none'
57
+ : (summary || fm.blockers || 'QA blockers pending');
58
+ const nextRecommendation = verdict === 'PASS'
59
+ ? '@product start the next feature'
60
+ : '@dev fix QA blockers and return to @qa';
61
+
62
+ const lines = [
63
+ '---',
64
+ `last_updated: ${nowTimestamp()}`,
65
+ 'last_agent: qa',
66
+ `last_gate: ${gate}`,
67
+ `active_feature: ${activeFeature}`,
68
+ `active_work: ${quoteYaml(activeWork)}`,
69
+ `blockers: ${quoteYaml(blockers)}`,
70
+ `next_recommendation: ${quoteYaml(nextRecommendation)}`,
71
+ '---',
72
+ '',
73
+ '# Project Pulse',
74
+ '',
75
+ '## Status',
76
+ '',
77
+ '- **Last agent:** @qa',
78
+ `- **Last gate:** ${gate}`,
79
+ `- **Active feature:** ${activeFeature}`,
80
+ `- **Active work:** ${activeWork || 'none'}`,
81
+ `- **Blockers:** ${blockers}`,
82
+ `- **Next:** ${nextRecommendation}`,
83
+ '',
84
+ '## Recent Activity',
85
+ '',
86
+ ...dedupedActivities,
87
+ activityLine,
88
+ ''
89
+ ];
90
+
91
+ await fs.writeFile(pulsePath, lines.join('\n'), 'utf8');
92
+ return true;
93
+ }
94
+
23
95
  async function updateSpecFile(specPath, verdict, residual, date) {
24
96
  const content = await readFileSafe(specPath);
25
97
  if (!content) return false;
@@ -65,26 +137,34 @@ async function updateSpecFile(specPath, verdict, residual, date) {
65
137
  return true;
66
138
  }
67
139
 
140
+ function escapeSlugForRegex(slug) {
141
+ return slug.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
142
+ }
143
+
68
144
  async function updateFeaturesFile(featuresPath, slug, verdict, date) {
69
145
  const content = await readFileSafe(featuresPath);
70
146
  if (!content) return false;
71
147
 
72
148
  const status = verdict === 'PASS' ? 'done' : 'qa_failed';
73
-
74
- // Try to find and update the feature row
75
- const updated = content.replace(
76
- new RegExp(`(\\|[^|]*${slug}[^|]*\\|[^|]*\\|)[^|]*(\\|)`, 'g'),
77
- (match, before, after) => `${before} ${status} (${date}) ${after}`
149
+ const rowRe = new RegExp(
150
+ `^(\\|\\s*${escapeSlugForRegex(slug)}\\s*\\|)\\s*[^|]*\\s*\\|\\s*([^|]*)\\s*\\|\\s*([^|]*)\\s*\\|(.*)$`,
151
+ 'm'
78
152
  );
79
153
 
154
+ const updated = content.replace(rowRe, (match, slugCol, startedCol, _completedCol, rest) => {
155
+ const started = startedCol.trim() || date;
156
+ return `${slugCol} ${status} | ${started} | ${date} |${rest}`;
157
+ });
158
+
80
159
  if (updated !== content) {
81
160
  await fs.writeFile(featuresPath, updated, 'utf8');
82
161
  return true;
83
162
  }
84
163
 
85
164
  // Append if not found
86
- const line = `| ${slug} | ${verdict === 'PASS' ? 'done' : 'qa_failed'} | ${date} | QA ${verdict} |`;
87
- await fs.appendFile(featuresPath, `\n${line}\n`, 'utf8');
165
+ const line = `| ${slug} | ${status} | ${date} | ${date} |`;
166
+ const needsNewline = !content.endsWith('\n');
167
+ await fs.appendFile(featuresPath, `${needsNewline ? '\n' : ''}${line}\n`, 'utf8');
88
168
  return true;
89
169
  }
90
170
 
@@ -132,17 +212,41 @@ async function runFeatureClose({ args, options = {}, logger }) {
132
212
 
133
213
  // 3. Update project-pulse.md
134
214
  const pulsePath = path.join(dir, 'project-pulse.md');
135
- const pulseContent = await readFileSafe(pulsePath);
136
- if (pulseContent) {
137
- const fm = parseFrontmatter(pulseContent);
138
- const status = verdict === 'PASS' ? 'closed' : 'qa_failed';
139
- const updatedPulse = pulseContent
140
- .replace(/active_feature:\s*.+/, `active_feature: (none)`)
141
- .replace(/active_work:\s*".+"/, `active_work: ""`)
142
- .replace(/last_agent:\s*.+/, `last_agent: qa`)
143
- .replace(/last_gate:\s*.+/, `last_gate: Gate D: ${verdict === 'PASS' ? 'approved' : 'rejected'}`);
144
- await fs.writeFile(pulsePath, updatedPulse, 'utf8');
215
+ const pulseUpdated = await updateProjectPulseFile(
216
+ pulsePath,
217
+ slug,
218
+ verdict,
219
+ residual || notes || null,
220
+ today
221
+ );
222
+ if (pulseUpdated) {
145
223
  updates.push('project-pulse.md: updated active work');
224
+ } else {
225
+ updates.push('project-pulse.md: not found (skipped)');
226
+ }
227
+
228
+ // 4. Auto-archive on PASS (default-on — user never has to remember).
229
+ // Disable explicitly with --no-archive when needed (e.g. re-running feature:close idempotently).
230
+ let archive = null;
231
+ const skipArchive = options['no-archive'] === true || options.archive === false;
232
+ if (verdict === 'PASS' && !skipArchive) {
233
+ try {
234
+ archive = await runFeatureArchive({
235
+ args: [targetDir],
236
+ options: { feature: slug, json: true },
237
+ logger: null
238
+ });
239
+ if (archive && archive.ok && archive.moved && archive.moved.length > 0) {
240
+ updates.push(`archive: moved ${archive.moved.length} file(s) to ${archive.archiveDir}/`);
241
+ updates.push(`archive: manifest updated at .aioson/context/done/MANIFEST.md`);
242
+ } else if (archive && archive.ok && archive.noop) {
243
+ updates.push('archive: nothing to move (already clean)');
244
+ } else if (archive && !archive.ok) {
245
+ updates.push(`archive: skipped (${archive.reason || 'unknown'})`);
246
+ }
247
+ } catch (err) {
248
+ updates.push(`archive: failed (${err.message || err})`);
249
+ }
146
250
  }
147
251
 
148
252
  const result = {
@@ -151,7 +255,8 @@ async function runFeatureClose({ args, options = {}, logger }) {
151
255
  verdict,
152
256
  date: today,
153
257
  residual: residual || notes || null,
154
- updates
258
+ updates,
259
+ archive
155
260
  };
156
261
 
157
262
  if (options.json) return result;