@jaimevalasek/aioson 1.5.1 → 1.7.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 (341) hide show
  1. package/CHANGELOG.md +49 -0
  2. package/README.md +729 -226
  3. package/docs/design-previews/aurora-command-ui-website.html +884 -0
  4. package/docs/design-previews/aurora-command-ui.html +682 -0
  5. package/docs/design-previews/bold-editorial-ui-website.html +658 -0
  6. package/docs/design-previews/bold-editorial-ui.html +717 -0
  7. package/docs/design-previews/clean-saas-ui-website.html +1202 -0
  8. package/docs/design-previews/clean-saas-ui.html +549 -0
  9. package/docs/design-previews/cognitive-core-ui-website.html +1009 -0
  10. package/docs/design-previews/cognitive-core-ui.html +463 -0
  11. package/docs/design-previews/glassmorphism-ui-website.html +572 -0
  12. package/docs/design-previews/glassmorphism-ui.html +886 -0
  13. package/docs/design-previews/index.html +699 -0
  14. package/docs/design-previews/interface-design-website.html +1187 -0
  15. package/docs/design-previews/interface-design.html +513 -0
  16. package/docs/design-previews/neo-brutalist-ui-website.html +621 -0
  17. package/docs/design-previews/neo-brutalist-ui.html +797 -0
  18. package/docs/design-previews/premium-command-center-ui-website.html +1217 -0
  19. package/docs/design-previews/premium-command-center-ui.html +552 -0
  20. package/docs/design-previews/pt.squarespace.com-homepage.html +889 -0
  21. package/docs/design-previews/warm-craft-ui-website.html +684 -0
  22. package/docs/design-previews/warm-craft-ui.html +739 -0
  23. package/docs/en/cli-reference.md +20 -9
  24. package/docs/integrations/sdlc-genius-boundary.md +76 -0
  25. package/docs/integrations/sdlc-genius-eval-matrix.md +75 -0
  26. package/docs/integrations/sdlc-genius-install-checklist.md +93 -0
  27. package/docs/integrations/sdlc-genius-review-samples.md +86 -0
  28. package/docs/pt/README.md +10 -0
  29. package/docs/pt/agent-sharding.md +132 -0
  30. package/docs/pt/agentes.md +9 -2
  31. package/docs/pt/busca-de-contexto.md +129 -0
  32. package/docs/pt/cache-de-contexto.md +156 -0
  33. package/docs/pt/comandos-cli.md +915 -1
  34. package/docs/pt/design-hybrid-forge.md +356 -0
  35. package/docs/pt/devlog-pipeline.md +270 -0
  36. package/docs/pt/fluxo-artefatos.md +178 -0
  37. package/docs/pt/hooks-session-guard.md +454 -0
  38. package/docs/pt/inicio-rapido.md +54 -3
  39. package/docs/pt/inteligencia-adaptativa.md +324 -0
  40. package/docs/pt/monitor-de-contexto.md +158 -0
  41. package/docs/pt/recuperacao-de-sessao.md +125 -0
  42. package/docs/pt/sandbox.md +125 -0
  43. package/docs/pt/sdd-automation-scripts.md +557 -0
  44. package/docs/pt/site-forge.md +309 -0
  45. package/docs/pt/skills.md +98 -6
  46. package/docs/pt/spec-learnings-pipeline.md +265 -0
  47. package/package.json +1 -1
  48. package/src/a2a/client.js +165 -0
  49. package/src/a2a/server.js +223 -0
  50. package/src/agent-loader.js +280 -0
  51. package/src/cli.js +329 -1
  52. package/src/commands/agent-audit.js +397 -0
  53. package/src/commands/agent-export-skill.js +229 -0
  54. package/src/commands/agent-loader.js +85 -0
  55. package/src/commands/artifact-validate.js +189 -0
  56. package/src/commands/brief-gen.js +405 -0
  57. package/src/commands/brief-validate.js +65 -0
  58. package/src/commands/classify.js +256 -0
  59. package/src/commands/context-cache.js +90 -0
  60. package/src/commands/context-compact.js +49 -0
  61. package/src/commands/context-health.js +175 -0
  62. package/src/commands/context-monitor.js +163 -0
  63. package/src/commands/context-search.js +66 -0
  64. package/src/commands/context-trim.js +177 -0
  65. package/src/commands/design-hybrid-options.js +385 -0
  66. package/src/commands/detect-test-runner.js +55 -0
  67. package/src/commands/devlog-export-brains.js +27 -0
  68. package/src/commands/devlog-process.js +292 -0
  69. package/src/commands/devlog-watch.js +131 -0
  70. package/src/commands/feature-close.js +165 -0
  71. package/src/commands/gate-check.js +228 -0
  72. package/src/commands/health.js +214 -0
  73. package/src/commands/hooks-emit.js +253 -0
  74. package/src/commands/hooks-install.js +347 -0
  75. package/src/commands/init.js +54 -13
  76. package/src/commands/install.js +52 -13
  77. package/src/commands/learning-auto-promote.js +195 -0
  78. package/src/commands/learning-evolve.js +364 -0
  79. package/src/commands/learning-export.js +103 -0
  80. package/src/commands/learning-rollback.js +164 -0
  81. package/src/commands/live.js +59 -1
  82. package/src/commands/pattern-detect.js +33 -0
  83. package/src/commands/preflight-context.js +30 -0
  84. package/src/commands/preflight.js +208 -0
  85. package/src/commands/pulse-update.js +130 -0
  86. package/src/commands/recovery.js +43 -0
  87. package/src/commands/runner-daemon.js +274 -0
  88. package/src/commands/runner-plan.js +70 -0
  89. package/src/commands/runner-queue-from-plan.js +166 -0
  90. package/src/commands/runner-queue.js +189 -0
  91. package/src/commands/runner-run.js +129 -0
  92. package/src/commands/runtime.js +47 -1
  93. package/src/commands/sandbox.js +37 -0
  94. package/src/commands/self-implement-loop.js +256 -0
  95. package/src/commands/session-guard.js +218 -0
  96. package/src/commands/setup-context.js +22 -2
  97. package/src/commands/setup.js +178 -0
  98. package/src/commands/sizing.js +165 -0
  99. package/src/commands/skill.js +144 -32
  100. package/src/commands/spec-checkpoint.js +177 -0
  101. package/src/commands/spec-status.js +79 -0
  102. package/src/commands/spec-sync.js +190 -0
  103. package/src/commands/spec-tasks.js +288 -0
  104. package/src/commands/squad-autorun.js +1220 -0
  105. package/src/commands/squad-bus.js +217 -0
  106. package/src/commands/squad-card.js +149 -0
  107. package/src/commands/squad-daemon.js +134 -0
  108. package/src/commands/squad-dependency-graph.js +164 -0
  109. package/src/commands/squad-review.js +106 -0
  110. package/src/commands/squad-scaffold.js +55 -0
  111. package/src/commands/squad-tool-register.js +157 -0
  112. package/src/commands/state-save.js +122 -0
  113. package/src/commands/tool-registry-cmd.js +232 -0
  114. package/src/commands/update.js +9 -0
  115. package/src/commands/verify-gate.js +572 -0
  116. package/src/commands/workflow-execute.js +241 -0
  117. package/src/constants.js +18 -0
  118. package/src/context-cache.js +159 -0
  119. package/src/context-search.js +326 -0
  120. package/src/design-variation-catalog.js +503 -0
  121. package/src/i18n/messages/en.js +32 -2
  122. package/src/i18n/messages/es.js +30 -2
  123. package/src/i18n/messages/fr.js +30 -2
  124. package/src/i18n/messages/pt-BR.js +32 -2
  125. package/src/install-animation.js +260 -0
  126. package/src/install-profile.js +143 -0
  127. package/src/install-wizard.js +475 -0
  128. package/src/installer.js +44 -10
  129. package/src/lib/health-check.js +158 -0
  130. package/src/lib/hook-protocol.js +76 -0
  131. package/src/mcp/apps/squad-dashboard/app.js +163 -0
  132. package/src/mcp/apps/squad-dashboard/index.html +261 -0
  133. package/src/mcp/apps/squad-dashboard/mcp-manifest.json +23 -0
  134. package/src/mcp/resources/squad-state.js +130 -0
  135. package/src/parser.js +7 -1
  136. package/src/preflight-engine.js +443 -0
  137. package/src/recovery-context-session.js +154 -0
  138. package/src/runner/cascade.js +97 -0
  139. package/src/runner/cli-launcher.js +109 -0
  140. package/src/runner/plan-importer.js +63 -0
  141. package/src/runner/queue-store.js +159 -0
  142. package/src/runtime-store.js +158 -4
  143. package/src/sandbox.js +177 -0
  144. package/src/squad/agent-teams-adapter.js +264 -0
  145. package/src/squad/brief-validator.js +350 -0
  146. package/src/squad/bus-bridge.js +140 -0
  147. package/src/squad/context-compactor.js +265 -0
  148. package/src/squad/cross-ai-synthesizer.js +250 -0
  149. package/src/squad/hooks-generator.js +196 -0
  150. package/src/squad/inter-squad-events.js +175 -0
  151. package/src/squad/intra-bus.js +345 -0
  152. package/src/squad/learning-extractor.js +213 -0
  153. package/src/squad/pattern-detector.js +365 -0
  154. package/src/squad/preflight-context.js +296 -0
  155. package/src/squad/recovery-context.js +242 -71
  156. package/src/squad/reflection.js +365 -0
  157. package/src/squad/squad-scaffold.js +177 -0
  158. package/src/squad/state-manager.js +310 -0
  159. package/src/squad/task-decomposer.js +652 -0
  160. package/src/squad/verify-gate.js +303 -0
  161. package/src/tool-executor.js +94 -0
  162. package/src/updater.js +10 -3
  163. package/src/worker-runner.js +186 -1
  164. package/template/.aioson/agents/analyst.md +119 -3
  165. package/template/.aioson/agents/architect.md +98 -0
  166. package/template/.aioson/agents/design-hybrid-forge.md +141 -0
  167. package/template/.aioson/agents/dev.md +335 -14
  168. package/template/.aioson/agents/deyvin.md +117 -2
  169. package/template/.aioson/agents/discovery-design-doc.md +44 -0
  170. package/template/.aioson/agents/genome.md +14 -0
  171. package/template/.aioson/agents/neo.md +78 -1
  172. package/template/.aioson/agents/orache.md +50 -4
  173. package/template/.aioson/agents/orchestrator.md +197 -1
  174. package/template/.aioson/agents/pm.md +93 -0
  175. package/template/.aioson/agents/product.md +77 -4
  176. package/template/.aioson/agents/profiler-enricher.md +14 -0
  177. package/template/.aioson/agents/profiler-forge.md +14 -0
  178. package/template/.aioson/agents/profiler-researcher.md +14 -0
  179. package/template/.aioson/agents/qa.md +249 -19
  180. package/template/.aioson/agents/setup.md +144 -12
  181. package/template/.aioson/agents/sheldon.md +237 -11
  182. package/template/.aioson/agents/site-forge.md +1753 -0
  183. package/template/.aioson/agents/squad.md +162 -0
  184. package/template/.aioson/agents/tester.md +209 -0
  185. package/template/.aioson/agents/ux-ui.md +34 -1
  186. package/template/.aioson/brains/README.md +128 -0
  187. package/template/.aioson/brains/_index.json +16 -0
  188. package/template/.aioson/brains/scripts/query.js +103 -0
  189. package/template/.aioson/brains/site-forge/visual-patterns.brain.json +205 -0
  190. package/template/.aioson/config.md +158 -13
  191. package/template/.aioson/constitution.md +33 -0
  192. package/template/.aioson/context/forensics/.gitkeep +0 -0
  193. package/template/.aioson/context/project-pulse.md +34 -0
  194. package/template/.aioson/context/seeds/seed-example.md +27 -0
  195. package/template/.aioson/context/user-profile.md +42 -0
  196. package/template/.aioson/docs/LAYERS.md +79 -0
  197. package/template/.aioson/docs/README.md +76 -0
  198. package/template/.aioson/docs/example-external-api-context.md +72 -0
  199. package/template/.aioson/locales/en/agents/architect.md +17 -0
  200. package/template/.aioson/locales/en/agents/dev.md +79 -13
  201. package/template/.aioson/locales/en/agents/orache.md +6 -0
  202. package/template/.aioson/locales/en/agents/orchestrator.md +24 -0
  203. package/template/.aioson/locales/en/agents/product.md +50 -0
  204. package/template/.aioson/locales/en/agents/setup.md +33 -1
  205. package/template/.aioson/locales/en/agents/sheldon.md +115 -0
  206. package/template/.aioson/locales/en/agents/squad.md +14 -0
  207. package/template/.aioson/locales/en/agents/tester.md +6 -0
  208. package/template/.aioson/locales/es/agents/analyst.md +2 -0
  209. package/template/.aioson/locales/es/agents/architect.md +19 -0
  210. package/template/.aioson/locales/es/agents/dev.md +64 -4
  211. package/template/.aioson/locales/es/agents/deyvin.md +2 -0
  212. package/template/.aioson/locales/es/agents/discovery-design-doc.md +2 -0
  213. package/template/.aioson/locales/es/agents/genome.md +2 -0
  214. package/template/.aioson/locales/es/agents/neo.md +2 -0
  215. package/template/.aioson/locales/es/agents/orache.md +2 -0
  216. package/template/.aioson/locales/es/agents/orchestrator.md +26 -0
  217. package/template/.aioson/locales/es/agents/pair.md +2 -0
  218. package/template/.aioson/locales/es/agents/pm.md +2 -0
  219. package/template/.aioson/locales/es/agents/product.md +52 -0
  220. package/template/.aioson/locales/es/agents/profiler-enricher.md +2 -0
  221. package/template/.aioson/locales/es/agents/profiler-forge.md +2 -0
  222. package/template/.aioson/locales/es/agents/profiler-researcher.md +2 -0
  223. package/template/.aioson/locales/es/agents/qa.md +2 -0
  224. package/template/.aioson/locales/es/agents/setup.md +35 -1
  225. package/template/.aioson/locales/es/agents/sheldon.md +117 -0
  226. package/template/.aioson/locales/es/agents/squad.md +16 -0
  227. package/template/.aioson/locales/es/agents/tester.md +9 -0
  228. package/template/.aioson/locales/es/agents/ux-ui.md +2 -0
  229. package/template/.aioson/locales/fr/agents/analyst.md +2 -0
  230. package/template/.aioson/locales/fr/agents/architect.md +19 -0
  231. package/template/.aioson/locales/fr/agents/dev.md +64 -4
  232. package/template/.aioson/locales/fr/agents/deyvin.md +2 -0
  233. package/template/.aioson/locales/fr/agents/discovery-design-doc.md +2 -0
  234. package/template/.aioson/locales/fr/agents/genome.md +2 -0
  235. package/template/.aioson/locales/fr/agents/neo.md +2 -0
  236. package/template/.aioson/locales/fr/agents/orache.md +2 -0
  237. package/template/.aioson/locales/fr/agents/orchestrator.md +26 -0
  238. package/template/.aioson/locales/fr/agents/pair.md +2 -0
  239. package/template/.aioson/locales/fr/agents/pm.md +2 -0
  240. package/template/.aioson/locales/fr/agents/product.md +52 -0
  241. package/template/.aioson/locales/fr/agents/profiler-enricher.md +2 -0
  242. package/template/.aioson/locales/fr/agents/profiler-forge.md +2 -0
  243. package/template/.aioson/locales/fr/agents/profiler-researcher.md +2 -0
  244. package/template/.aioson/locales/fr/agents/qa.md +2 -0
  245. package/template/.aioson/locales/fr/agents/setup.md +35 -1
  246. package/template/.aioson/locales/fr/agents/sheldon.md +117 -0
  247. package/template/.aioson/locales/fr/agents/squad.md +16 -0
  248. package/template/.aioson/locales/fr/agents/tester.md +9 -0
  249. package/template/.aioson/locales/fr/agents/ux-ui.md +2 -0
  250. package/template/.aioson/locales/pt-BR/agents/analyst.md +64 -3
  251. package/template/.aioson/locales/pt-BR/agents/architect.md +42 -0
  252. package/template/.aioson/locales/pt-BR/agents/dev.md +147 -14
  253. package/template/.aioson/locales/pt-BR/agents/deyvin.md +47 -0
  254. package/template/.aioson/locales/pt-BR/agents/neo.md +62 -1
  255. package/template/.aioson/locales/pt-BR/agents/orchestrator.md +158 -2
  256. package/template/.aioson/locales/pt-BR/agents/pm.md +95 -1
  257. package/template/.aioson/locales/pt-BR/agents/product.md +145 -18
  258. package/template/.aioson/locales/pt-BR/agents/qa.md +16 -0
  259. package/template/.aioson/locales/pt-BR/agents/setup.md +134 -19
  260. package/template/.aioson/locales/pt-BR/agents/sheldon.md +132 -1
  261. package/template/.aioson/locales/pt-BR/agents/squad.md +14 -0
  262. package/template/.aioson/locales/pt-BR/agents/tester.md +449 -0
  263. package/template/.aioson/rules/README.md +69 -0
  264. package/template/.aioson/rules/data-format-convention.md +136 -0
  265. package/template/.aioson/rules/example-monetary-values.md +30 -0
  266. package/template/.aioson/schemas/squad-manifest.schema.json +124 -3
  267. package/template/.aioson/skills/design/aurora-command-ui/SKILL.md +243 -0
  268. package/template/.aioson/skills/design/aurora-command-ui/references/art-direction.md +293 -0
  269. package/template/.aioson/skills/design/aurora-command-ui/references/components.md +827 -0
  270. package/template/.aioson/skills/design/aurora-command-ui/references/dashboards.md +250 -0
  271. package/template/.aioson/skills/design/aurora-command-ui/references/design-tokens.md +585 -0
  272. package/template/.aioson/skills/design/aurora-command-ui/references/motion.md +365 -0
  273. package/template/.aioson/skills/design/aurora-command-ui/references/patterns.md +482 -0
  274. package/template/.aioson/skills/design/aurora-command-ui/references/websites.md +387 -0
  275. package/template/.aioson/skills/design/glassmorphism-ui/SKILL.md +222 -0
  276. package/template/.aioson/skills/design/glassmorphism-ui/references/art-direction.md +159 -0
  277. package/template/.aioson/skills/design/glassmorphism-ui/references/components.md +498 -0
  278. package/template/.aioson/skills/design/glassmorphism-ui/references/dashboards.md +236 -0
  279. package/template/.aioson/skills/design/glassmorphism-ui/references/design-tokens.md +274 -0
  280. package/template/.aioson/skills/design/glassmorphism-ui/references/motion.md +355 -0
  281. package/template/.aioson/skills/design/glassmorphism-ui/references/patterns.md +198 -0
  282. package/template/.aioson/skills/design/glassmorphism-ui/references/websites.md +307 -0
  283. package/template/.aioson/skills/design/neo-brutalist-ui/SKILL.md +213 -0
  284. package/template/.aioson/skills/design/neo-brutalist-ui/references/art-direction.md +228 -0
  285. package/template/.aioson/skills/design/neo-brutalist-ui/references/components.md +855 -0
  286. package/template/.aioson/skills/design/neo-brutalist-ui/references/dashboards.md +334 -0
  287. package/template/.aioson/skills/design/neo-brutalist-ui/references/design-tokens.md +342 -0
  288. package/template/.aioson/skills/design/neo-brutalist-ui/references/motion.md +286 -0
  289. package/template/.aioson/skills/design/neo-brutalist-ui/references/patterns.md +458 -0
  290. package/template/.aioson/skills/design/neo-brutalist-ui/references/websites.md +723 -0
  291. package/template/.aioson/skills/design/pt.squarespace.com/.skill-meta.json +31 -0
  292. package/template/.aioson/skills/design/pt.squarespace.com/SKILL.md +66 -0
  293. package/template/.aioson/skills/design/pt.squarespace.com/references/components.md +368 -0
  294. package/template/.aioson/skills/design/pt.squarespace.com/references/design-tokens.md +150 -0
  295. package/template/.aioson/skills/design/pt.squarespace.com/references/motion.md +270 -0
  296. package/template/.aioson/skills/design/pt.squarespace.com/references/patterns.md +189 -0
  297. package/template/.aioson/skills/design/pt.squarespace.com/references/websites.md +165 -0
  298. package/template/.aioson/skills/process/aioson-spec-driven/SKILL.md +46 -0
  299. package/template/.aioson/skills/process/aioson-spec-driven/references/analyst.md +30 -0
  300. package/template/.aioson/skills/process/aioson-spec-driven/references/approval-gates.md +109 -0
  301. package/template/.aioson/skills/process/aioson-spec-driven/references/architect.md +23 -0
  302. package/template/.aioson/skills/process/aioson-spec-driven/references/artifact-map.md +44 -0
  303. package/template/.aioson/skills/process/aioson-spec-driven/references/classification-map.md +37 -0
  304. package/template/.aioson/skills/process/aioson-spec-driven/references/dev.md +47 -0
  305. package/template/.aioson/skills/process/aioson-spec-driven/references/deyvin.md +27 -0
  306. package/template/.aioson/skills/process/aioson-spec-driven/references/hardening-lane.md +49 -0
  307. package/template/.aioson/skills/process/aioson-spec-driven/references/maintenance-and-state.md +101 -0
  308. package/template/.aioson/skills/process/aioson-spec-driven/references/product.md +25 -0
  309. package/template/.aioson/skills/process/aioson-spec-driven/references/qa.md +30 -0
  310. package/template/.aioson/skills/process/aioson-spec-driven/references/sheldon.md +25 -0
  311. package/template/.aioson/skills/process/aioson-spec-driven/references/ui-language.md +75 -0
  312. package/template/.aioson/skills/process/design-hybrid-forge/SKILL.md +147 -0
  313. package/template/.aioson/skills/process/design-hybrid-forge/references/crossover-protocol.md +221 -0
  314. package/template/.aioson/skills/process/design-hybrid-forge/references/naming-registry.md +88 -0
  315. package/template/.aioson/skills/process/design-hybrid-forge/references/output-contract.md +306 -0
  316. package/template/.aioson/skills/process/design-hybrid-forge/references/pair-compatibility.md +149 -0
  317. package/template/.aioson/skills/process/design-hybrid-forge/references/quality-gates.md +208 -0
  318. package/template/.aioson/skills/process/design-hybrid-forge/references/variation-library.md +125 -0
  319. package/template/.aioson/skills/process/simplify/SKILL.md +173 -0
  320. package/template/.aioson/skills/static/context-budget-guide.md +46 -0
  321. package/template/.aioson/skills/static/harness-sensors.md +74 -0
  322. package/template/.aioson/skills/static/multi-agent-patterns.md +43 -0
  323. package/template/.aioson/skills/static/react-motion-patterns.md +22 -0
  324. package/template/.aioson/skills/static/static-html-patterns/checklists.md +43 -0
  325. package/template/.aioson/skills/static/static-html-patterns/css-tokens.md +609 -0
  326. package/template/.aioson/skills/static/static-html-patterns/motion.md +193 -0
  327. package/template/.aioson/skills/static/static-html-patterns/premium.md +711 -0
  328. package/template/.aioson/skills/static/static-html-patterns/structure.md +209 -0
  329. package/template/.aioson/skills/static/static-html-patterns/utilities.md +190 -0
  330. package/template/.aioson/skills/static/static-html-patterns.md +58 -1913
  331. package/template/.aioson/skills/static/threejs-patterns.md +929 -0
  332. package/template/.aioson/skills/static/web-research-cache.md +112 -0
  333. package/template/.aioson/tasks/implementation-plan.md +21 -1
  334. package/template/.claude/commands/aioson/agent/design-hybrid-forge.md +5 -0
  335. package/template/.claude/commands/aioson/agent/orache.md +5 -0
  336. package/template/.claude/commands/aioson/agent/sheldon.md +5 -0
  337. package/template/.claude/commands/aioson/agent/site-forge.md +5 -0
  338. package/template/AGENTS.md +75 -1
  339. package/template/CLAUDE.md +31 -0
  340. package/template/OPENCODE.md +4 -0
  341. package/template/researchs/.gitkeep +0 -0
@@ -16,6 +16,47 @@ function resolveTargetDir(args) {
16
16
  return path.resolve(process.cwd(), args[0] || '.');
17
17
  }
18
18
 
19
+ async function copyRecursive(src, dest) {
20
+ const stat = await fs.stat(src);
21
+ if (stat.isDirectory()) {
22
+ await ensureDir(dest);
23
+ const entries = await fs.readdir(src);
24
+ for (const entry of entries) {
25
+ await copyRecursive(path.join(src, entry), path.join(dest, entry));
26
+ }
27
+ return;
28
+ }
29
+
30
+ await ensureDir(path.dirname(dest));
31
+ await fs.copyFile(src, dest);
32
+ }
33
+
34
+ async function replaceDirectory(srcDir, destDir) {
35
+ await fs.rm(destDir, { recursive: true, force: true });
36
+ await copyRecursive(srcDir, destDir);
37
+ }
38
+
39
+ async function readJsonIfExists(filePath) {
40
+ if (!(await exists(filePath))) return null;
41
+ try {
42
+ return JSON.parse(await fs.readFile(filePath, 'utf8'));
43
+ } catch {
44
+ return null;
45
+ }
46
+ }
47
+
48
+ async function writeSkillMeta(destDir, patch) {
49
+ const metaPath = path.join(destDir, '.skill-meta.json');
50
+ const existing = await readJsonIfExists(metaPath) || {};
51
+ const merged = {
52
+ ...existing,
53
+ ...patch
54
+ };
55
+
56
+ await fs.writeFile(metaPath, JSON.stringify(merged, null, 2), 'utf8');
57
+ return merged;
58
+ }
59
+
19
60
  /**
20
61
  * Parse YAML frontmatter from a SKILL.md file.
21
62
  */
@@ -44,17 +85,7 @@ async function distributeToTool(targetDir, slug, skillDir) {
44
85
  for (const toolPath of TOOL_TARGETS) {
45
86
  const toolSkillDir = path.join(targetDir, toolPath, slug);
46
87
  try {
47
- await ensureDir(toolSkillDir);
48
- // Copy all files from the installed skill dir
49
- const entries = await fs.readdir(skillDir);
50
- for (const entry of entries) {
51
- const src = path.join(skillDir, entry);
52
- const dest = path.join(toolSkillDir, entry);
53
- const stat = await fs.stat(src);
54
- if (stat.isFile()) {
55
- await fs.copyFile(src, dest);
56
- }
57
- }
88
+ await replaceDirectory(skillDir, toolSkillDir);
58
89
  results.push({ tool: toolPath, ok: true });
59
90
  } catch (err) {
60
91
  results.push({ tool: toolPath, ok: false, error: err.message });
@@ -187,12 +218,13 @@ async function installFromNpm(targetDir, slug, options, logger) {
187
218
 
188
219
  // Copy to .aioson/installed-skills/{slug}/
189
220
  const destDir = path.join(targetDir, INSTALLED_SKILLS_DIR, slug);
190
- await ensureDir(destDir);
191
-
192
- const entries = await fs.readdir(sourceDir);
193
- for (const entry of entries) {
194
- await fs.copyFile(path.join(sourceDir, entry), path.join(destDir, entry));
195
- }
221
+ await replaceDirectory(sourceDir, destDir);
222
+ await writeSkillMeta(destDir, {
223
+ source: 'npm',
224
+ sourcePackage: '@tech-leads-club/agent-skills',
225
+ sourcePath: path.relative(targetDir, sourceDir),
226
+ installedAt: new Date().toISOString()
227
+ });
196
228
 
197
229
  resolve({ ok: true, sourceDir, destDir });
198
230
  });
@@ -248,15 +280,16 @@ async function installFromCloud(targetDir, slug, options, logger) {
248
280
  ].join('\n');
249
281
 
250
282
  const destDir = path.join(targetDir, INSTALLED_SKILLS_DIR, slug);
283
+ await fs.rm(destDir, { recursive: true, force: true });
251
284
  await ensureDir(destDir);
252
285
  await fs.writeFile(path.join(destDir, 'SKILL.md'), fm, 'utf8');
253
286
 
254
287
  // Write meta
255
- await fs.writeFile(path.join(destDir, '.skill-meta.json'), JSON.stringify({
288
+ await writeSkillMeta(destDir, {
256
289
  source: 'cloud',
257
290
  cloudSlug: snapshot.skill.slug,
258
291
  installedAt: new Date().toISOString()
259
- }, null, 2), 'utf8');
292
+ });
260
293
 
261
294
  return { ok: true, destDir };
262
295
  }
@@ -271,30 +304,32 @@ async function installFromLocal(targetDir, slug, filePath, logger) {
271
304
  }
272
305
 
273
306
  const destDir = path.join(targetDir, INSTALLED_SKILLS_DIR, slug);
274
- await ensureDir(destDir);
275
-
276
307
  const stat = await fs.stat(absPath);
308
+ const samePath = path.resolve(absPath) === path.resolve(destDir);
309
+
310
+ if (!samePath) {
311
+ await fs.rm(destDir, { recursive: true, force: true });
312
+ await ensureDir(destDir);
313
+ } else if (!stat.isDirectory()) {
314
+ return { ok: false, error: 'Local self-install only supports skill directories' };
315
+ }
316
+
277
317
  if (stat.isDirectory()) {
278
- // Copy entire directory
279
- const entries = await fs.readdir(absPath);
280
- for (const entry of entries) {
281
- const src = path.join(absPath, entry);
282
- const srcStat = await fs.stat(src);
283
- if (srcStat.isFile()) {
284
- await fs.copyFile(src, path.join(destDir, entry));
285
- }
318
+ if (!samePath) {
319
+ await replaceDirectory(absPath, destDir);
286
320
  }
287
321
  } else {
288
322
  // Single file — copy as SKILL.md
323
+ await ensureDir(destDir);
289
324
  await fs.copyFile(absPath, path.join(destDir, 'SKILL.md'));
290
325
  }
291
326
 
292
327
  // Write meta
293
- await fs.writeFile(path.join(destDir, '.skill-meta.json'), JSON.stringify({
328
+ await writeSkillMeta(destDir, {
294
329
  source: 'local',
295
330
  sourcePath: filePath,
296
331
  installedAt: new Date().toISOString()
297
- }, null, 2), 'utf8');
332
+ });
298
333
 
299
334
  return { ok: true, destDir };
300
335
  }
@@ -362,6 +397,58 @@ async function runSkillInstall({ args, options = {}, logger, t }) {
362
397
  };
363
398
  }
364
399
 
400
+ /**
401
+ * Parse a minimal extension.yml manifest if present in a skill directory.
402
+ * Reads only top-level scalar fields and one level of nesting.
403
+ * Returns null if the file is absent or unreadable.
404
+ */
405
+ async function parseExtensionManifest(skillDir) {
406
+ const manifestPath = path.join(skillDir, 'extension.yml');
407
+ let raw;
408
+ try {
409
+ raw = await fs.readFile(manifestPath, 'utf8');
410
+ } catch {
411
+ return null; // file not present — this is the normal case for old skills
412
+ }
413
+
414
+ const manifest = {};
415
+ let currentSection = null;
416
+
417
+ for (const line of raw.split(/\r?\n/)) {
418
+ const trimmed = line.trim();
419
+ if (!trimmed || trimmed.startsWith('#')) continue;
420
+
421
+ // Detect top-level section (no leading spaces, ends with colon, no value)
422
+ if (/^\w[\w-]*:$/.test(trimmed)) {
423
+ currentSection = trimmed.slice(0, -1);
424
+ manifest[currentSection] = {};
425
+ continue;
426
+ }
427
+
428
+ // Nested key: value (indented with spaces or tab)
429
+ if (currentSection && /^\s+/.test(line)) {
430
+ const idx = trimmed.indexOf(':');
431
+ if (idx === -1) continue;
432
+ const key = trimmed.slice(0, idx).trim();
433
+ const val = trimmed.slice(idx + 1).trim().replace(/^["']|["']$/g, '');
434
+ if (key) manifest[currentSection][key] = val;
435
+ continue;
436
+ }
437
+
438
+ // Top-level scalar key: value
439
+ const idx = trimmed.indexOf(':');
440
+ if (idx === -1) continue;
441
+ const key = trimmed.slice(0, idx).trim();
442
+ const val = trimmed.slice(idx + 1).trim().replace(/^["']|["']$/g, '');
443
+ if (key) {
444
+ currentSection = null;
445
+ manifest[key] = val;
446
+ }
447
+ }
448
+
449
+ return manifest;
450
+ }
451
+
365
452
  /**
366
453
  * Scan a directory for .md files with SKILL.md or frontmatter descriptions.
367
454
  * Returns array of { slug, name, description, type, path }.
@@ -429,17 +516,31 @@ async function runSkillList({ args, options = {}, logger, t }) {
429
516
  const fm = parseSkillFrontmatter(raw);
430
517
 
431
518
  let source = 'unknown';
519
+ let meta = null;
432
520
  try {
433
521
  const metaRaw = await fs.readFile(path.join(skillsDir, slug, '.skill-meta.json'), 'utf8');
434
- const meta = JSON.parse(metaRaw);
522
+ meta = JSON.parse(metaRaw);
435
523
  source = meta.source || 'unknown';
436
524
  } catch { /* no meta */ }
437
525
 
526
+ const author = meta?.author?.name || meta?.author_name || null;
527
+ const model =
528
+ meta?.generator?.model ||
529
+ meta?.generation?.model ||
530
+ meta?.generated_by_model ||
531
+ null;
532
+
533
+ // Read optional extension.yml manifest — additive, does not affect existing behavior
534
+ const extManifest = showAll ? await parseExtensionManifest(path.join(skillsDir, slug)) : null;
535
+
438
536
  installed.push({
439
537
  slug,
440
538
  name: fm.name || slug,
441
539
  description: fm.description || '',
442
540
  source,
541
+ author,
542
+ model,
543
+ manifest: extManifest,
443
544
  path: path.relative(targetDir, path.join(skillsDir, slug))
444
545
  });
445
546
  }
@@ -472,6 +573,17 @@ async function runSkillList({ args, options = {}, logger, t }) {
472
573
  if (s.description) {
473
574
  logger.log(` ${s.description.slice(0, 100)}`);
474
575
  }
576
+ if (s.author) logger.log(` author: ${s.author}`);
577
+ if (s.model) logger.log(` model: ${s.model}`);
578
+ if (showAll && s.manifest) {
579
+ const ext = s.manifest.extension || {};
580
+ if (ext.version) logger.log(` version: ${ext.version}`);
581
+ const hooks = s.manifest.hooks;
582
+ if (hooks && typeof hooks === 'object') {
583
+ const hookNames = Object.keys(hooks).filter(h => hooks[h]?.enabled !== 'false' && hooks[h]?.enabled !== false);
584
+ if (hookNames.length > 0) logger.log(` hooks declared: ${hookNames.join(', ')}`);
585
+ }
586
+ }
475
587
  logger.log(` ${s.path}/SKILL.md`);
476
588
  logger.log('');
477
589
  }
@@ -0,0 +1,177 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs/promises');
4
+ const path = require('node:path');
5
+ const { openRuntimeDb, appendRunEvent } = require('../runtime-store');
6
+
7
+ function nowIso() {
8
+ return new Date().toISOString();
9
+ }
10
+
11
+ function parseFrontmatter(content) {
12
+ const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
13
+ if (!match) return {};
14
+ const result = {};
15
+ for (const line of match[1].split(/\r?\n/)) {
16
+ const colonIdx = line.indexOf(':');
17
+ if (colonIdx === -1) continue;
18
+ const key = line.slice(0, colonIdx).trim();
19
+ const value = line.slice(colonIdx + 1).trim().replace(/^["']|["']$/g, '');
20
+ if (key) result[key] = value;
21
+ }
22
+ return result;
23
+ }
24
+
25
+ function extractLastCheckpoint(content) {
26
+ // Try frontmatter first
27
+ const fmMatch = content.match(/^---[\s\S]*?last_checkpoint:\s*(.+)/m);
28
+ if (fmMatch) return fmMatch[1].trim().replace(/^["']|["']$/g, '');
29
+ // Try section header
30
+ const sectionMatch = content.match(/##\s+last_checkpoint[^\n]*\n([\s\S]*?)(?=\n##|\s*$)/i);
31
+ if (sectionMatch) return sectionMatch[1].replace(/^[-*]\s*/, '').trim();
32
+ return null;
33
+ }
34
+
35
+ function extractPhaseGates(content) {
36
+ const fm = parseFrontmatter(content);
37
+ if (!fm.phase_gates) return null;
38
+ try {
39
+ return JSON.parse(fm.phase_gates.replace(/'/g, '"'));
40
+ } catch {
41
+ return null;
42
+ }
43
+ }
44
+
45
+ async function runSpecCheckpoint({ args, options = {}, logger }) {
46
+ const targetDir = path.resolve(process.cwd(), args[0] || '.');
47
+ const featureSlug = options.feature ? String(options.feature).trim() : null;
48
+ const agentName = options.agent ? String(options.agent).trim() : 'dev';
49
+ const contextDir = path.join(targetDir, '.aioson', 'context');
50
+
51
+ if (!featureSlug) {
52
+ if (!options.json) logger.log('Error: --feature=<slug> is required.');
53
+ return { ok: false, reason: 'missing_feature' };
54
+ }
55
+
56
+ // Find spec file
57
+ const candidates = [
58
+ path.join(contextDir, `spec-${featureSlug}.md`),
59
+ path.join(contextDir, 'spec.md')
60
+ ];
61
+
62
+ let specPath = null;
63
+ let specContent = null;
64
+ for (const candidate of candidates) {
65
+ try {
66
+ specContent = await fs.readFile(candidate, 'utf8');
67
+ specPath = candidate;
68
+ break;
69
+ } catch { /* try next */ }
70
+ }
71
+
72
+ if (!specContent) {
73
+ if (!options.json) logger.log(`No spec file found for feature: ${featureSlug}`);
74
+ return { ok: false, reason: 'no_spec_file', featureSlug };
75
+ }
76
+
77
+ const lastCheckpoint = extractLastCheckpoint(specContent);
78
+ const phaseGates = extractPhaseGates(specContent);
79
+
80
+ if (!lastCheckpoint) {
81
+ if (!options.json) logger.log(`No last_checkpoint found in ${path.basename(specPath)}.`);
82
+ return { ok: false, reason: 'no_checkpoint', specPath };
83
+ }
84
+
85
+ const { db, dbPath } = await openRuntimeDb(targetDir, { mustExist: true });
86
+
87
+ if (!db) {
88
+ if (!options.json) logger.log('No runtime database found. Run aioson agent:done first.');
89
+ return { ok: false, reason: 'no_db' };
90
+ }
91
+
92
+ try {
93
+ // Find latest agent run for this feature
94
+ const run = db.prepare(`
95
+ SELECT r.run_key, r.status, r.summary, r.updated_at
96
+ FROM agent_runs r
97
+ WHERE r.agent_name = ?
98
+ AND (r.run_key LIKE ? OR r.task_key LIKE ?)
99
+ ORDER BY r.updated_at DESC
100
+ LIMIT 1
101
+ `).get(agentName, `%${featureSlug}%`, `%${featureSlug}%`);
102
+
103
+ // If no run with feature slug, try latest run by agent name
104
+ const activeRun = run || db.prepare(`
105
+ SELECT run_key, status, summary, updated_at
106
+ FROM agent_runs
107
+ WHERE agent_name = ? AND status IN ('running', 'completed')
108
+ ORDER BY updated_at DESC
109
+ LIMIT 1
110
+ `).get(agentName);
111
+
112
+ if (!activeRun) {
113
+ if (!options.json) {
114
+ logger.log(`No active run found for agent @${agentName}.`);
115
+ logger.log('Tip: run aioson runtime:start first, or specify --agent=<name>.');
116
+ }
117
+ return { ok: false, reason: 'no_run', featureSlug, agentName };
118
+ }
119
+
120
+ // Append checkpoint event
121
+ appendRunEvent(db, {
122
+ runKey: activeRun.run_key,
123
+ eventType: 'plan_checkpoint',
124
+ phase: 'spec',
125
+ status: 'in_progress',
126
+ message: lastCheckpoint,
127
+ payload: phaseGates ? { phase_gates: phaseGates } : null,
128
+ createdAt: nowIso()
129
+ });
130
+
131
+ // Update run summary to last_checkpoint if run is still in_progress
132
+ if (activeRun.status !== 'completed') {
133
+ db.prepare(
134
+ 'UPDATE agent_runs SET summary = ?, updated_at = ? WHERE run_key = ?'
135
+ ).run(lastCheckpoint, nowIso(), activeRun.run_key);
136
+ }
137
+
138
+ if (options.json) {
139
+ return {
140
+ ok: true,
141
+ featureSlug,
142
+ specPath,
143
+ lastCheckpoint,
144
+ phaseGates,
145
+ runKey: activeRun.run_key,
146
+ dbPath
147
+ };
148
+ }
149
+
150
+ logger.log(`Reading ${path.basename(specPath)}...`);
151
+ logger.log(`last_checkpoint: "${lastCheckpoint}"`);
152
+ if (phaseGates) {
153
+ logger.log(`phase_gates: ${JSON.stringify(phaseGates)}`);
154
+ }
155
+ logger.log('');
156
+ logger.log('Checkpoint registered:');
157
+ logger.log(` run_key: ${activeRun.run_key}`);
158
+ logger.log(` summary: "${lastCheckpoint}"`);
159
+ logger.log(` status: in_progress (checkpoint only — use agent:done to close)`);
160
+ logger.log('');
161
+ logger.log(`Next: continue with /${agentName} — start from last_checkpoint`);
162
+
163
+ return {
164
+ ok: true,
165
+ featureSlug,
166
+ specPath,
167
+ lastCheckpoint,
168
+ phaseGates,
169
+ runKey: activeRun.run_key,
170
+ dbPath
171
+ };
172
+ } finally {
173
+ db.close();
174
+ }
175
+ }
176
+
177
+ module.exports = { runSpecCheckpoint };
@@ -0,0 +1,79 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs/promises');
4
+ const path = require('node:path');
5
+ const { openRuntimeDb } = require('../runtime-store');
6
+
7
+ async function runSpecStatus({ args, options = {}, logger }) {
8
+ const targetDir = path.resolve(process.cwd(), args[0] || '.');
9
+ const { db, dbPath } = await openRuntimeDb(targetDir, { mustExist: true });
10
+
11
+ if (!db) {
12
+ if (!options.json) logger.log('No runtime database found. Run aioson agent:done first.');
13
+ return { ok: false, reason: 'no_db' };
14
+ }
15
+
16
+ try {
17
+ const plans = db.prepare(`
18
+ SELECT plan_id, feature_slug, status, phases_total, phases_completed, created_at, updated_at
19
+ FROM implementation_plans
20
+ WHERE status != 'archived'
21
+ ORDER BY updated_at DESC
22
+ `).all();
23
+
24
+ const rows = [];
25
+ for (const plan of plans) {
26
+ const lastRun = db.prepare(`
27
+ SELECT r.agent_name, r.summary, r.updated_at
28
+ FROM agent_runs r
29
+ JOIN tasks t ON r.task_key = t.task_key
30
+ WHERE r.status IN ('running', 'completed')
31
+ AND (t.session_key LIKE ? OR r.agent_name IS NOT NULL)
32
+ ORDER BY r.updated_at DESC
33
+ LIMIT 1
34
+ `).get(`%${plan.feature_slug || ''}%`);
35
+
36
+ rows.push({
37
+ feature: plan.feature_slug || '(project)',
38
+ phase: `${plan.phases_completed}/${plan.phases_total}`,
39
+ status: plan.status,
40
+ lastAgent: lastRun?.agent_name || '—',
41
+ checkpoint: lastRun?.summary ? lastRun.summary.slice(0, 60) : '—'
42
+ });
43
+ }
44
+
45
+ const totalLearnings = db.prepare(
46
+ "SELECT COUNT(*) as cnt FROM project_learnings WHERE status = 'active'"
47
+ ).get()?.cnt || 0;
48
+
49
+ const promotable = db.prepare(
50
+ "SELECT COUNT(*) as cnt FROM project_learnings WHERE status = 'active' AND frequency >= 3"
51
+ ).get()?.cnt || 0;
52
+
53
+ if (options.json) {
54
+ return { ok: true, features: rows, totalLearnings, promotable, dbPath };
55
+ }
56
+
57
+ logger.log(`Project Status — ${targetDir}`);
58
+ logger.log('─'.repeat(80));
59
+ logger.log('Feature'.padEnd(20) + 'Phase'.padEnd(10) + 'Status'.padEnd(16) + 'Last Agent'.padEnd(16) + 'Checkpoint');
60
+ logger.log('─'.repeat(80));
61
+ for (const r of rows) {
62
+ logger.log(
63
+ r.feature.padEnd(20) +
64
+ r.phase.padEnd(10) +
65
+ r.status.padEnd(16) +
66
+ r.lastAgent.padEnd(16) +
67
+ r.checkpoint
68
+ );
69
+ }
70
+ logger.log('─'.repeat(80));
71
+ logger.log(`Active learnings: ${totalLearnings} | Promotable (freq≥3): ${promotable}`);
72
+
73
+ return { ok: true, features: rows, totalLearnings, promotable, dbPath };
74
+ } finally {
75
+ db.close();
76
+ }
77
+ }
78
+
79
+ module.exports = { runSpecStatus };
@@ -0,0 +1,190 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs/promises');
4
+ const path = require('node:path');
5
+ const { openRuntimeDb } = require('../runtime-store');
6
+
7
+ function nowIso() {
8
+ return new Date().toISOString();
9
+ }
10
+
11
+ function createLearningId() {
12
+ return `learning-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
13
+ }
14
+
15
+ function parseFrontmatter(content) {
16
+ const match = content.match(/^---\r?\n([\s\S]*?)\r?\n---/);
17
+ if (!match) return {};
18
+ const result = {};
19
+ for (const line of match[1].split(/\r?\n/)) {
20
+ const colonIdx = line.indexOf(':');
21
+ if (colonIdx === -1) continue;
22
+ const key = line.slice(0, colonIdx).trim();
23
+ const value = line.slice(colonIdx + 1).trim().replace(/^["']|["']$/g, '');
24
+ if (key) result[key] = value;
25
+ }
26
+ return result;
27
+ }
28
+
29
+ function extractSection(content, sectionName) {
30
+ const re = new RegExp(`^#{1,4}\\s+${sectionName}[\\s\\S]*?(?=^#{1,4}\\s|$)`, 'im');
31
+ const match = content.match(re);
32
+ if (!match) return '';
33
+ return match[0].replace(/^#{1,4}\s+\S.*\n/, '').trim();
34
+ }
35
+
36
+ function extractLearnings(content) {
37
+ const section = extractSection(content, 'Session Learnings');
38
+ const learnings = [];
39
+ for (const line of section.split(/\r?\n/)) {
40
+ const trimmed = line.replace(/^[-*]\s*/, '').trim();
41
+ if (!trimmed) continue;
42
+ const typeMatch = trimmed.match(/^\[(process|domain|quality|preference)\]\s+(.+)/i);
43
+ if (typeMatch) {
44
+ learnings.push({ type: typeMatch[1].toLowerCase(), title: typeMatch[2].trim() });
45
+ } else if (trimmed.length > 5) {
46
+ learnings.push({ type: 'process', title: trimmed });
47
+ }
48
+ }
49
+ return learnings;
50
+ }
51
+
52
+ function extractLastCheckpoint(content) {
53
+ const section = extractSection(content, 'last_checkpoint');
54
+ if (section) return section.replace(/^.*:\s*/, '').trim();
55
+ const fmMatch = content.match(/^---[\s\S]*?last_checkpoint:\s*(.+)/m);
56
+ return fmMatch ? fmMatch[1].trim().replace(/^["']|["']$/g, '') : null;
57
+ }
58
+
59
+ function upsertProjectLearning(db, { title, type, featureSlug, evidence, sourceSession }) {
60
+ const existing = db.prepare(
61
+ 'SELECT learning_id, frequency FROM project_learnings WHERE title = ? AND feature_slug = ?'
62
+ ).get(title, featureSlug || null);
63
+
64
+ if (existing) {
65
+ db.prepare(
66
+ 'UPDATE project_learnings SET frequency = ?, last_reinforced = ?, updated_at = ? WHERE learning_id = ?'
67
+ ).run(existing.frequency + 1, nowIso(), nowIso(), existing.learning_id);
68
+ return { action: 'updated', learningId: existing.learning_id };
69
+ }
70
+
71
+ const learningId = createLearningId();
72
+ db.prepare(`
73
+ INSERT INTO project_learnings
74
+ (learning_id, feature_slug, type, title, confidence, frequency, last_reinforced,
75
+ applies_to, source_session, evidence, status, created_at, updated_at)
76
+ VALUES (?, ?, ?, ?, 'medium', 1, ?, 'project', ?, ?, 'active', ?, ?)
77
+ `).run(learningId, featureSlug || null, type, title, nowIso(), sourceSession || null, evidence || null, nowIso(), nowIso());
78
+ return { action: 'inserted', learningId };
79
+ }
80
+
81
+ function syncPlanPhases(db, featureSlug, phaseGates) {
82
+ if (!phaseGates || typeof phaseGates !== 'object') return 0;
83
+ const plan = db.prepare(
84
+ "SELECT plan_id FROM implementation_plans WHERE feature_slug = ? AND status != 'archived' ORDER BY created_at DESC LIMIT 1"
85
+ ).get(featureSlug);
86
+ if (!plan) return 0;
87
+
88
+ let updated = 0;
89
+ const gateMap = { plan: 1, requirements: 2, design: 3 };
90
+ for (const [gate, status] of Object.entries(phaseGates)) {
91
+ const phaseNum = gateMap[gate];
92
+ if (!phaseNum) continue;
93
+ const phase = db.prepare(
94
+ 'SELECT phase_number, status FROM plan_phases WHERE plan_id = ? AND phase_number = ?'
95
+ ).get(plan.plan_id, phaseNum);
96
+ if (!phase) continue;
97
+
98
+ const newStatus = status === 'approved' ? 'completed' : (status === 'pending' ? 'pending' : phase.status);
99
+ if (newStatus !== phase.status) {
100
+ db.prepare(
101
+ 'UPDATE plan_phases SET status = ?, completed_at = ? WHERE plan_id = ? AND phase_number = ?'
102
+ ).run(newStatus, newStatus === 'completed' ? nowIso() : null, plan.plan_id, phaseNum);
103
+ updated++;
104
+ }
105
+ }
106
+ return updated;
107
+ }
108
+
109
+ async function syncSpecFile(db, specPath, { verbose = false } = {}) {
110
+ let content;
111
+ try {
112
+ content = await fs.readFile(specPath, 'utf8');
113
+ } catch {
114
+ return { skipped: true, reason: 'not_found' };
115
+ }
116
+
117
+ const filename = path.basename(specPath, '.md');
118
+ const featureSlug = filename.startsWith('spec-') ? filename.slice(5) : null;
119
+ const fm = parseFrontmatter(content);
120
+ const phaseGates = fm.phase_gates ? JSON.parse(fm.phase_gates.replace(/'/g, '"')).catch?.() || null : null;
121
+
122
+ const learnings = extractLearnings(content);
123
+ const lastCheckpoint = extractLastCheckpoint(content);
124
+
125
+ let learningsSynced = 0;
126
+ for (const { type, title } of learnings) {
127
+ upsertProjectLearning(db, { title, type, featureSlug, sourceSession: filename });
128
+ learningsSynced++;
129
+ }
130
+
131
+ let phasesSynced = 0;
132
+ if (featureSlug && fm.phase_gates) {
133
+ try {
134
+ const gates = JSON.parse(fm.phase_gates.replace(/'/g, '"'));
135
+ phasesSynced = syncPlanPhases(db, featureSlug, gates);
136
+ } catch { /* malformed phase_gates — skip */ }
137
+ }
138
+
139
+ return { featureSlug, learningsSynced, phasesSynced, lastCheckpoint };
140
+ }
141
+
142
+ async function runSpecSync({ args, options = {}, logger }) {
143
+ const targetDir = path.resolve(process.cwd(), args[0] || '.');
144
+ const contextDir = path.join(targetDir, '.aioson', 'context');
145
+
146
+ let files;
147
+ try {
148
+ const entries = await fs.readdir(contextDir);
149
+ files = entries.filter((f) => f.startsWith('spec') && f.endsWith('.md'));
150
+ } catch {
151
+ if (!options.json) logger.log('No .aioson/context/ directory found.');
152
+ return { ok: false, reason: 'no_context_dir' };
153
+ }
154
+
155
+ const { db, dbPath } = await openRuntimeDb(targetDir);
156
+ const results = [];
157
+
158
+ try {
159
+ for (const file of files) {
160
+ const result = await syncSpecFile(db, path.join(contextDir, file), { verbose: options.verbose });
161
+ if (!result.skipped) {
162
+ results.push({ file, ...result });
163
+ }
164
+ }
165
+ } finally {
166
+ db.close();
167
+ }
168
+
169
+ const totalLearnings = results.reduce((s, r) => s + (r.learningsSynced || 0), 0);
170
+ const totalPhases = results.reduce((s, r) => s + (r.phasesSynced || 0), 0);
171
+
172
+ if (options.json) {
173
+ return { ok: true, files: results, totalLearnings, totalPhases, dbPath };
174
+ }
175
+
176
+ logger.log(`Spec Sync — ${targetDir}`);
177
+ logger.log('─'.repeat(50));
178
+ for (const r of results) {
179
+ logger.log(`${r.file}`);
180
+ if (r.learningsSynced > 0) logger.log(` Learnings synced: ${r.learningsSynced}`);
181
+ if (r.phasesSynced > 0) logger.log(` Plan phases updated: ${r.phasesSynced}`);
182
+ if (r.lastCheckpoint) logger.log(` last_checkpoint: "${r.lastCheckpoint}"`);
183
+ }
184
+ logger.log('─'.repeat(50));
185
+ logger.log(`Summary: ${totalLearnings} learnings synced, ${totalPhases} plan phases updated`);
186
+
187
+ return { ok: true, files: results, totalLearnings, totalPhases, dbPath };
188
+ }
189
+
190
+ module.exports = { runSpecSync };