contextdevkit 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 (345) hide show
  1. package/CHANGELOG.md +592 -0
  2. package/LICENSE +21 -0
  3. package/README.md +401 -0
  4. package/docs/AGENT-PACKAGE-FORMAT.md +140 -0
  5. package/docs/ARCHITECTURE.md +258 -0
  6. package/docs/CHANGELOG.md +559 -0
  7. package/docs/CUSTOMIZING.md +211 -0
  8. package/docs/LEVELS.md +151 -0
  9. package/docs/ROADMAP.md +385 -0
  10. package/docs/SQUAD-PIPELINE-FORMAT.md +258 -0
  11. package/docs/SQUADS/agent-forge.md +65 -0
  12. package/docs/SQUADS/design-team.md +161 -0
  13. package/docs/token-economy-plan.md +135 -0
  14. package/install.mjs +273 -0
  15. package/instrucoes.md +274 -0
  16. package/package.json +46 -0
  17. package/templates/CLAUDE.md.tpl +133 -0
  18. package/templates/claude/agents/_TEMPLATE.md +52 -0
  19. package/templates/claude/agents/accessibility.md +36 -0
  20. package/templates/claude/agents/agent-architect.md +37 -0
  21. package/templates/claude/agents/architect.md +39 -0
  22. package/templates/claude/agents/code-reviewer.md +43 -0
  23. package/templates/claude/agents/code-security.md +59 -0
  24. package/templates/claude/agents/context-keeper.md +40 -0
  25. package/templates/claude/agents/devops.md +40 -0
  26. package/templates/claude/agents/eval-designer.md +40 -0
  27. package/templates/claude/agents/forge-orchestrator.md +42 -0
  28. package/templates/claude/agents/governance-officer.md +45 -0
  29. package/templates/claude/agents/growth.md +92 -0
  30. package/templates/claude/agents/infra-security.md +53 -0
  31. package/templates/claude/agents/landing-architect.md +154 -0
  32. package/templates/claude/agents/model-router.md +34 -0
  33. package/templates/claude/agents/packager.md +38 -0
  34. package/templates/claude/agents/privacy-lgpd.md +64 -0
  35. package/templates/claude/agents/product-owner.md +51 -0
  36. package/templates/claude/agents/prompt-engineer.md +33 -0
  37. package/templates/claude/agents/qa-e2e.md +52 -0
  38. package/templates/claude/agents/qa-fuzzer.md +24 -0
  39. package/templates/claude/agents/qa-integration.md +21 -0
  40. package/templates/claude/agents/qa-orchestrator.md +40 -0
  41. package/templates/claude/agents/qa-perf.md +40 -0
  42. package/templates/claude/agents/qa-unit.md +39 -0
  43. package/templates/claude/agents/rag-designer.md +54 -0
  44. package/templates/claude/agents/retention.md +85 -0
  45. package/templates/claude/agents/security.md +48 -0
  46. package/templates/claude/agents/seo-specialist.md +106 -0
  47. package/templates/claude/agents/test-engineer.md +48 -0
  48. package/templates/claude/agents/tool-designer.md +32 -0
  49. package/templates/claude/agents/ui-designer.md +37 -0
  50. package/templates/claude/agents/ux-designer.md +38 -0
  51. package/templates/claude/commands/README.md +95 -0
  52. package/templates/claude/commands/advise.md +80 -0
  53. package/templates/claude/commands/audit/analyze-code-ia-practices.md +75 -0
  54. package/templates/claude/commands/audit/audit.md +35 -0
  55. package/templates/claude/commands/audit/contract-check.md +21 -0
  56. package/templates/claude/commands/audit/deep-analysis.md +48 -0
  57. package/templates/claude/commands/audit/deps-audit.md +49 -0
  58. package/templates/claude/commands/audit/security-setup.md +35 -0
  59. package/templates/claude/commands/audit/seo-audit.md +63 -0
  60. package/templates/claude/commands/audit/tech-debt-sweep.md +35 -0
  61. package/templates/claude/commands/bug-hunt.md +42 -0
  62. package/templates/claude/commands/claude-md.md +36 -0
  63. package/templates/claude/commands/close-version.md +25 -0
  64. package/templates/claude/commands/context-refresh.md +19 -0
  65. package/templates/claude/commands/context-stats.md +15 -0
  66. package/templates/claude/commands/dashboard.md +66 -0
  67. package/templates/claude/commands/distill-apply.md +19 -0
  68. package/templates/claude/commands/distill-sessions.md +26 -0
  69. package/templates/claude/commands/fleet.md +47 -0
  70. package/templates/claude/commands/forge/forge-audit.md +16 -0
  71. package/templates/claude/commands/forge/forge-budget.md +16 -0
  72. package/templates/claude/commands/forge/forge-deprecate.md +16 -0
  73. package/templates/claude/commands/forge/forge-doctor.md +17 -0
  74. package/templates/claude/commands/forge/forge-eval.md +16 -0
  75. package/templates/claude/commands/forge/forge-fallback-test.md +17 -0
  76. package/templates/claude/commands/forge/forge-killswitch.md +17 -0
  77. package/templates/claude/commands/forge/forge-list.md +17 -0
  78. package/templates/claude/commands/forge/forge-new.md +41 -0
  79. package/templates/claude/commands/forge/forge-policy.md +16 -0
  80. package/templates/claude/commands/forge/forge-redteam.md +17 -0
  81. package/templates/claude/commands/forge/forge-refresh-matrix.md +20 -0
  82. package/templates/claude/commands/forge/forge-route.md +17 -0
  83. package/templates/claude/commands/forge/forge-show.md +16 -0
  84. package/templates/claude/commands/landing-page.md +71 -0
  85. package/templates/claude/commands/log-session.md +59 -0
  86. package/templates/claude/commands/media-gen.md +93 -0
  87. package/templates/claude/commands/new-adr.md +30 -0
  88. package/templates/claude/commands/pipeline/dev-start.md +64 -0
  89. package/templates/claude/commands/pipeline/pipeline.md +36 -0
  90. package/templates/claude/commands/pipeline/resume.md +70 -0
  91. package/templates/claude/commands/pipeline/retro.md +34 -0
  92. package/templates/claude/commands/pipeline/runs.md +63 -0
  93. package/templates/claude/commands/pipeline/ship.md +54 -0
  94. package/templates/claude/commands/pipeline/workflow.md +85 -0
  95. package/templates/claude/commands/playbook.md +27 -0
  96. package/templates/claude/commands/predictions-review.md +28 -0
  97. package/templates/claude/commands/qa/qa-signoff.md +24 -0
  98. package/templates/claude/commands/qa/scaffold-tests.md +27 -0
  99. package/templates/claude/commands/qa/test-plan.md +26 -0
  100. package/templates/claude/commands/qa/visual-test.md +42 -0
  101. package/templates/claude/commands/roadmap.md +48 -0
  102. package/templates/claude/commands/setup/aidevtool-from0.md +104 -0
  103. package/templates/claude/commands/setup/context-config.md +25 -0
  104. package/templates/claude/commands/setup/context-doctor.md +21 -0
  105. package/templates/claude/commands/setup/context-level.md +17 -0
  106. package/templates/claude/commands/setup/setupcontextdevkit.md +121 -0
  107. package/templates/claude/commands/simulate-impact.md +32 -0
  108. package/templates/claude/commands/squad.md +44 -0
  109. package/templates/claude/commands/state.md +21 -0
  110. package/templates/claude/commands/token-report.md +29 -0
  111. package/templates/claude/commands/tune-agents.md +35 -0
  112. package/templates/claude/commands/vcs/claim.md +18 -0
  113. package/templates/claude/commands/vcs/git.md +83 -0
  114. package/templates/claude/commands/vcs/release.md +15 -0
  115. package/templates/claude/commands/vcs/worktree-new.md +18 -0
  116. package/templates/claude/commands/watch.md +47 -0
  117. package/templates/contextkit/.env.example +36 -0
  118. package/templates/contextkit/CLAUDE.child.md.tpl +38 -0
  119. package/templates/contextkit/README.md +74 -0
  120. package/templates/contextkit/behaviors-examples.md +183 -0
  121. package/templates/contextkit/behaviors.md +116 -0
  122. package/templates/contextkit/best-practices.md +323 -0
  123. package/templates/contextkit/config.json +66 -0
  124. package/templates/contextkit/detectors/README.md +45 -0
  125. package/templates/contextkit/detectors/example-detector.mjs.example +25 -0
  126. package/templates/contextkit/instrucoes.md +114 -0
  127. package/templates/contextkit/memory/GLOSSARY.md +13 -0
  128. package/templates/contextkit/memory/SESSIONS.md +9 -0
  129. package/templates/contextkit/memory/WORKSPACE.md +7 -0
  130. package/templates/contextkit/memory/business-rules/_TEMPLATE.md +33 -0
  131. package/templates/contextkit/memory/decisions/0000-record-architecture-decisions.md +34 -0
  132. package/templates/contextkit/memory/decisions/_TEMPLATE.md +25 -0
  133. package/templates/contextkit/memory/predictions/.gitkeep +0 -0
  134. package/templates/contextkit/memory/roadmap.md +28 -0
  135. package/templates/contextkit/memory/sessions/.gitkeep +0 -0
  136. package/templates/contextkit/memory/workflows/.gitkeep +0 -0
  137. package/templates/contextkit/pipeline/backlog/.gitkeep +0 -0
  138. package/templates/contextkit/pipeline/conclusion/.gitkeep +0 -0
  139. package/templates/contextkit/pipeline/devpipeline.md +9 -0
  140. package/templates/contextkit/pipeline/testing/.gitkeep +0 -0
  141. package/templates/contextkit/pipeline/working/.gitkeep +0 -0
  142. package/templates/contextkit/review-protocol.md +214 -0
  143. package/templates/contextkit/runtime/config/defaults.mjs +215 -0
  144. package/templates/contextkit/runtime/config/levels.mjs +42 -0
  145. package/templates/contextkit/runtime/config/load.mjs +105 -0
  146. package/templates/contextkit/runtime/config/paths.mjs +92 -0
  147. package/templates/contextkit/runtime/config/presets.mjs +47 -0
  148. package/templates/contextkit/runtime/config/schema.mjs +88 -0
  149. package/templates/contextkit/runtime/config/settings-compose.mjs +55 -0
  150. package/templates/contextkit/runtime/git-hooks/commit-msg.mjs +55 -0
  151. package/templates/contextkit/runtime/git-hooks/pre-commit.mjs +47 -0
  152. package/templates/contextkit/runtime/git-hooks/pre-push.mjs +102 -0
  153. package/templates/contextkit/runtime/hooks/boot-context-readers.mjs +111 -0
  154. package/templates/contextkit/runtime/hooks/boot-signals.mjs +135 -0
  155. package/templates/contextkit/runtime/hooks/check-registration.mjs +228 -0
  156. package/templates/contextkit/runtime/hooks/concurrency-guard.mjs +110 -0
  157. package/templates/contextkit/runtime/hooks/ledger.mjs +231 -0
  158. package/templates/contextkit/runtime/hooks/md-extract.mjs +65 -0
  159. package/templates/contextkit/runtime/hooks/path-classification.mjs +62 -0
  160. package/templates/contextkit/runtime/hooks/safe-io.mjs +84 -0
  161. package/templates/contextkit/runtime/hooks/session-digest-core.mjs +85 -0
  162. package/templates/contextkit/runtime/hooks/session-start.mjs +248 -0
  163. package/templates/contextkit/runtime/hooks/simulate-gate.mjs +108 -0
  164. package/templates/contextkit/runtime/hooks/track-edits.mjs +154 -0
  165. package/templates/contextkit/runtime/providers/media/_adapter.mjs +120 -0
  166. package/templates/contextkit/runtime/providers/media/nano-banana.mjs +110 -0
  167. package/templates/contextkit/runtime/providers/media/veo.mjs +162 -0
  168. package/templates/contextkit/runtime/providers/review/_adapter.mjs +71 -0
  169. package/templates/contextkit/runtime/providers/review/detect.mjs +115 -0
  170. package/templates/contextkit/runtime/providers/review/gh.mjs +103 -0
  171. package/templates/contextkit/runtime/state/state-io.mjs +172 -0
  172. package/templates/contextkit/runtime/statusline.mjs +51 -0
  173. package/templates/contextkit/squads/README.md +115 -0
  174. package/templates/contextkit/squads/_BRIEFING.md.tpl +27 -0
  175. package/templates/contextkit/squads/agent-forge/README.md +69 -0
  176. package/templates/contextkit/squads/agent-forge/ROADMAP.md +108 -0
  177. package/templates/contextkit/squads/agent-forge/best-practices.md +89 -0
  178. package/templates/contextkit/squads/agent-forge/cli/forge-admin.mjs +132 -0
  179. package/templates/contextkit/squads/agent-forge/cli/forge-eval-cli.mjs +163 -0
  180. package/templates/contextkit/squads/agent-forge/cli/forge-new.mjs +97 -0
  181. package/templates/contextkit/squads/agent-forge/cli/forge-ops.mjs +177 -0
  182. package/templates/contextkit/squads/agent-forge/lib/architect.mjs +112 -0
  183. package/templates/contextkit/squads/agent-forge/lib/eval-designer.mjs +133 -0
  184. package/templates/contextkit/squads/agent-forge/lib/eval-runner.mjs +167 -0
  185. package/templates/contextkit/squads/agent-forge/lib/governance-officer.mjs +178 -0
  186. package/templates/contextkit/squads/agent-forge/lib/package-ops.mjs +101 -0
  187. package/templates/contextkit/squads/agent-forge/lib/packager.mjs +219 -0
  188. package/templates/contextkit/squads/agent-forge/lib/prompt-gen.mjs +122 -0
  189. package/templates/contextkit/squads/agent-forge/lib/rag-designer.mjs +102 -0
  190. package/templates/contextkit/squads/agent-forge/lib/router.mjs +165 -0
  191. package/templates/contextkit/squads/agent-forge/lib/tool-gen.mjs +113 -0
  192. package/templates/contextkit/squads/agent-forge/lib/yaml.mjs +47 -0
  193. package/templates/contextkit/squads/agent-forge/pipeline.yaml +65 -0
  194. package/templates/contextkit/squads/agent-forge/router/capability-matrix.json +112 -0
  195. package/templates/contextkit/squads/agent-forge/router/decision-rules.json +120 -0
  196. package/templates/contextkit/squads/agent-forge/templates/agent-package/.agentforgerc +12 -0
  197. package/templates/contextkit/squads/agent-forge/templates/agent-package/CHANGELOG.md +13 -0
  198. package/templates/contextkit/squads/agent-forge/templates/agent-package/LICENSE +5 -0
  199. package/templates/contextkit/squads/agent-forge/templates/agent-package/README.md +39 -0
  200. package/templates/contextkit/squads/agent-forge/templates/agent-package/adapters/go/README.md +10 -0
  201. package/templates/contextkit/squads/agent-forge/templates/agent-package/adapters/go/agent.go +14 -0
  202. package/templates/contextkit/squads/agent-forge/templates/agent-package/adapters/go/go.mod +3 -0
  203. package/templates/contextkit/squads/agent-forge/templates/agent-package/adapters/node/README.md +11 -0
  204. package/templates/contextkit/squads/agent-forge/templates/agent-package/adapters/node/index.js +53 -0
  205. package/templates/contextkit/squads/agent-forge/templates/agent-package/adapters/node/package.json +9 -0
  206. package/templates/contextkit/squads/agent-forge/templates/agent-package/adapters/python/README.md +10 -0
  207. package/templates/contextkit/squads/agent-forge/templates/agent-package/adapters/python/agent.py +16 -0
  208. package/templates/contextkit/squads/agent-forge/templates/agent-package/adapters/python/pyproject.toml +10 -0
  209. package/templates/contextkit/squads/agent-forge/templates/agent-package/evals/golden.jsonl +1 -0
  210. package/templates/contextkit/squads/agent-forge/templates/agent-package/evals/red-team.jsonl +3 -0
  211. package/templates/contextkit/squads/agent-forge/templates/agent-package/evals/rubric.yaml +14 -0
  212. package/templates/contextkit/squads/agent-forge/templates/agent-package/evals/run-eval.md +17 -0
  213. package/templates/contextkit/squads/agent-forge/templates/agent-package/evals/thresholds.yaml +18 -0
  214. package/templates/contextkit/squads/agent-forge/templates/agent-package/examples/basic.node.md +17 -0
  215. package/templates/contextkit/squads/agent-forge/templates/agent-package/examples/with-fallback.node.md +24 -0
  216. package/templates/contextkit/squads/agent-forge/templates/agent-package/examples/with-rag.python.md +20 -0
  217. package/templates/contextkit/squads/agent-forge/templates/agent-package/governance/audit.schema.json +23 -0
  218. package/templates/contextkit/squads/agent-forge/templates/agent-package/governance/compliance.policy.yaml +43 -0
  219. package/templates/contextkit/squads/agent-forge/templates/agent-package/governance/cost.policy.yaml +36 -0
  220. package/templates/contextkit/squads/agent-forge/templates/agent-package/governance/fallback-chain.yaml +16 -0
  221. package/templates/contextkit/squads/agent-forge/templates/agent-package/governance/quality.policy.yaml +43 -0
  222. package/templates/contextkit/squads/agent-forge/templates/agent-package/manifest.yaml +91 -0
  223. package/templates/contextkit/squads/agent-forge/templates/agent-package/prompts/system.anthropic.md +19 -0
  224. package/templates/contextkit/squads/agent-forge/templates/agent-package/prompts/system.canonical.md +25 -0
  225. package/templates/contextkit/squads/agent-forge/templates/agent-package/prompts/system.deepseek.md +21 -0
  226. package/templates/contextkit/squads/agent-forge/templates/agent-package/prompts/system.google.md +19 -0
  227. package/templates/contextkit/squads/agent-forge/templates/agent-package/prompts/system.ollama.md +21 -0
  228. package/templates/contextkit/squads/agent-forge/templates/agent-package/prompts/system.openai.md +20 -0
  229. package/templates/contextkit/squads/agent-forge/templates/agent-package/rag/config.yaml +17 -0
  230. package/templates/contextkit/squads/agent-forge/templates/agent-package/rag/index/.gitkeep +3 -0
  231. package/templates/contextkit/squads/agent-forge/templates/agent-package/rag/ingestion/chunker.config.yaml +6 -0
  232. package/templates/contextkit/squads/agent-forge/templates/agent-package/rag/ingestion/sources.yaml +8 -0
  233. package/templates/contextkit/squads/agent-forge/templates/agent-package/rag/retrieval/query-template.md +16 -0
  234. package/templates/contextkit/squads/agent-forge/templates/agent-package/rag/retrieval/rerank.config.yaml +6 -0
  235. package/templates/contextkit/squads/agent-forge/templates/agent-package/tools/adapters/anthropic.tools.json +11 -0
  236. package/templates/contextkit/squads/agent-forge/templates/agent-package/tools/adapters/deepseek.tools.json +14 -0
  237. package/templates/contextkit/squads/agent-forge/templates/agent-package/tools/adapters/google.tools.json +11 -0
  238. package/templates/contextkit/squads/agent-forge/templates/agent-package/tools/adapters/ollama.tools.json +14 -0
  239. package/templates/contextkit/squads/agent-forge/templates/agent-package/tools/adapters/openai.tools.json +14 -0
  240. package/templates/contextkit/squads/agent-forge/templates/agent-package/tools/schemas.canonical.json +25 -0
  241. package/templates/contextkit/starters/tanstack/README.md +86 -0
  242. package/templates/contextkit/starters/tanstack/index.html +12 -0
  243. package/templates/contextkit/starters/tanstack/package.json +25 -0
  244. package/templates/contextkit/starters/tanstack/src/main.tsx +40 -0
  245. package/templates/contextkit/starters/tanstack/src/router.tsx +12 -0
  246. package/templates/contextkit/starters/tanstack/src/routes/__root.tsx +10 -0
  247. package/templates/contextkit/starters/tanstack/src/routes/index.tsx +17 -0
  248. package/templates/contextkit/starters/tanstack/tsconfig.json +19 -0
  249. package/templates/contextkit/starters/tanstack/vite.config.ts +10 -0
  250. package/templates/contextkit/tools/scripts/adr-digest-core.mjs +42 -0
  251. package/templates/contextkit/tools/scripts/adr-digest.mjs +78 -0
  252. package/templates/contextkit/tools/scripts/agent-tuning.mjs +74 -0
  253. package/templates/contextkit/tools/scripts/aiso-audit.mjs +174 -0
  254. package/templates/contextkit/tools/scripts/audit-shared.mjs +129 -0
  255. package/templates/contextkit/tools/scripts/claim.mjs +133 -0
  256. package/templates/contextkit/tools/scripts/claude-md.mjs +123 -0
  257. package/templates/contextkit/tools/scripts/clean-drive.mjs +78 -0
  258. package/templates/contextkit/tools/scripts/context-config.mjs +111 -0
  259. package/templates/contextkit/tools/scripts/context-level.mjs +98 -0
  260. package/templates/contextkit/tools/scripts/context-pack.mjs +120 -0
  261. package/templates/contextkit/tools/scripts/contract-scan.mjs +186 -0
  262. package/templates/contextkit/tools/scripts/dashboard-data.mjs +198 -0
  263. package/templates/contextkit/tools/scripts/dashboard-html.mjs +215 -0
  264. package/templates/contextkit/tools/scripts/dashboard-server.mjs +129 -0
  265. package/templates/contextkit/tools/scripts/dashboard.mjs +107 -0
  266. package/templates/contextkit/tools/scripts/deep-analysis.mjs +62 -0
  267. package/templates/contextkit/tools/scripts/deps-audit.mjs +201 -0
  268. package/templates/contextkit/tools/scripts/detect-stack.mjs +164 -0
  269. package/templates/contextkit/tools/scripts/distill-detect.mjs +90 -0
  270. package/templates/contextkit/tools/scripts/doctor.mjs +165 -0
  271. package/templates/contextkit/tools/scripts/fleet.mjs +170 -0
  272. package/templates/contextkit/tools/scripts/generate-context.mjs +142 -0
  273. package/templates/contextkit/tools/scripts/gh-alerts.mjs +117 -0
  274. package/templates/contextkit/tools/scripts/git.mjs +97 -0
  275. package/templates/contextkit/tools/scripts/home.mjs +106 -0
  276. package/templates/contextkit/tools/scripts/mark-simulation.mjs +78 -0
  277. package/templates/contextkit/tools/scripts/media-gen.mjs +154 -0
  278. package/templates/contextkit/tools/scripts/pipeline-board.mjs +74 -0
  279. package/templates/contextkit/tools/scripts/pipeline-prioritize.mjs +68 -0
  280. package/templates/contextkit/tools/scripts/pipeline-session.mjs +99 -0
  281. package/templates/contextkit/tools/scripts/pipeline-validate.mjs +136 -0
  282. package/templates/contextkit/tools/scripts/pipeline.mjs +302 -0
  283. package/templates/contextkit/tools/scripts/playbook.mjs +123 -0
  284. package/templates/contextkit/tools/scripts/predictions-review.mjs +113 -0
  285. package/templates/contextkit/tools/scripts/release.mjs +60 -0
  286. package/templates/contextkit/tools/scripts/resume.mjs +114 -0
  287. package/templates/contextkit/tools/scripts/roadmap.mjs +86 -0
  288. package/templates/contextkit/tools/scripts/runs.mjs +116 -0
  289. package/templates/contextkit/tools/scripts/seo-audit.mjs +150 -0
  290. package/templates/contextkit/tools/scripts/session-digest.mjs +89 -0
  291. package/templates/contextkit/tools/scripts/session-reindex.mjs +91 -0
  292. package/templates/contextkit/tools/scripts/setup-complete.mjs +69 -0
  293. package/templates/contextkit/tools/scripts/squad-meta.mjs +23 -0
  294. package/templates/contextkit/tools/scripts/squad-pipeline-condition.mjs +192 -0
  295. package/templates/contextkit/tools/scripts/squad-pipeline.mjs +301 -0
  296. package/templates/contextkit/tools/scripts/squad.mjs +80 -0
  297. package/templates/contextkit/tools/scripts/stats.mjs +138 -0
  298. package/templates/contextkit/tools/scripts/sync-check.mjs +235 -0
  299. package/templates/contextkit/tools/scripts/tech-debt-detectors.mjs +76 -0
  300. package/templates/contextkit/tools/scripts/tech-debt-scan.mjs +164 -0
  301. package/templates/contextkit/tools/scripts/token-report.mjs +153 -0
  302. package/templates/contextkit/tools/scripts/visual-test.mjs +132 -0
  303. package/templates/contextkit/tools/scripts/watch.mjs +106 -0
  304. package/templates/contextkit/tools/scripts/workflow.mjs +136 -0
  305. package/templates/contextkit/tools/scripts/workspace-sync.mjs +220 -0
  306. package/templates/contextkit/tools/scripts/worktree-new.mjs +50 -0
  307. package/templates/contextkit/workflows/L1-static-loading.md +59 -0
  308. package/templates/contextkit/workflows/L2-session-ledger.md +86 -0
  309. package/templates/contextkit/workflows/L3-multi-session.md +80 -0
  310. package/templates/contextkit/workflows/L4-squads.md +68 -0
  311. package/templates/contextkit/workflows/L5-proactive.md +88 -0
  312. package/templates/contextkit/workflows/README.md +47 -0
  313. package/templates/contextkit/workflows/playbooks/distillation-cycle.md +74 -0
  314. package/templates/contextkit/workflows/playbooks/landing-page.md +197 -0
  315. package/templates/contextkit/workflows/playbooks/security-batch.md +68 -0
  316. package/templates/contextkit/workflows/playbooks/seo-aiso.md +288 -0
  317. package/templates/contextkit/workflows/playbooks/simulate-impact.md +83 -0
  318. package/templates/contextkit/workflows/playbooks/tanstack.md +164 -0
  319. package/templates/contextkit/workflows/playbooks/tech-debt-sweep.md +77 -0
  320. package/templates/docs/CHANGELOG.md.tpl +11 -0
  321. package/templates/gitattributes +3 -0
  322. package/templates/github/ISSUE_TEMPLATE/bug_report.md +30 -0
  323. package/templates/github/ISSUE_TEMPLATE/feature_request.md +22 -0
  324. package/templates/github/PULL_REQUEST_TEMPLATE.md +27 -0
  325. package/templates/github/dependabot.yml +27 -0
  326. package/templates/github/workflows/quality.yml +36 -0
  327. package/templates/github/workflows/security.yml +54 -0
  328. package/tools/install/cli.mjs +62 -0
  329. package/tools/install/fs.mjs +56 -0
  330. package/tools/install/git.mjs +114 -0
  331. package/tools/install/project.mjs +51 -0
  332. package/tools/install/uninstall.mjs +54 -0
  333. package/tools/integration-test-compozy.mjs +88 -0
  334. package/tools/integration-test-guards.mjs +269 -0
  335. package/tools/integration-test-tooling-agent-forge.mjs +189 -0
  336. package/tools/integration-test-tooling-pipeline.mjs +164 -0
  337. package/tools/integration-test-tooling.mjs +172 -0
  338. package/tools/integration-test.mjs +228 -0
  339. package/tools/it-helpers.mjs +60 -0
  340. package/tools/selfcheck-agent-forge-ops.mjs +107 -0
  341. package/tools/selfcheck-agent-forge.mjs +304 -0
  342. package/tools/selfcheck-config.mjs +80 -0
  343. package/tools/selfcheck-runtime.mjs +135 -0
  344. package/tools/selfcheck-source.mjs +326 -0
  345. package/tools/selfcheck.mjs +268 -0
@@ -0,0 +1,110 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * PreToolUse concurrency guard (Level >= 3) — prevents one session from
4
+ * silently clobbering another's work on the same file.
5
+ *
6
+ * Claude Code's own `Edit` already refuses to edit a file changed since the
7
+ * agent last read it. This guard covers the remaining same-machine gaps:
8
+ * - `Write` (full overwrite) — does NOT check freshness, so it can clobber.
9
+ * - cross-session awareness — another live session (or an external tool)
10
+ * modified this exact file recently.
11
+ *
12
+ * It WARNS (never blocks → no retry loops): prints a banner that becomes
13
+ * context, telling the agent to re-read and merge rather than overwrite.
14
+ * Cross-machine conflicts are caught at push time by the pre-push hook.
15
+ *
16
+ * Defensive: any error exits 0 silently.
17
+ */
18
+ import { stat } from 'node:fs/promises';
19
+ import { resolve } from 'node:path';
20
+ import { getLevel } from '../config/load.mjs';
21
+ import { listAllLedgers, readLedger, resolveSessionId, ROOT, toRepoRelative } from './ledger.mjs';
22
+
23
+ const RECENT_MS = 12 * 60 * 60 * 1000; // ignore stale ledgers older than 12h
24
+
25
+ async function readStdin() {
26
+ return new Promise((res) => {
27
+ let buf = '';
28
+ process.stdin.setEncoding('utf-8');
29
+ process.stdin.on('data', (c) => (buf += c));
30
+ process.stdin.on('end', () => res(buf));
31
+ setTimeout(() => res(buf), 500).unref?.();
32
+ });
33
+ }
34
+
35
+ function extractFilePath(payload) {
36
+ const t = payload?.tool_name;
37
+ const input = payload?.tool_input ?? {};
38
+ if ((t === 'Edit' || t === 'Write' || t === 'MultiEdit') && typeof input.file_path === 'string') return input.file_path;
39
+ return null;
40
+ }
41
+
42
+ /** Most recent modification entry for `path` in a ledger (or null). */
43
+ function lastMod(ledger, path) {
44
+ let best = null;
45
+ for (const m of ledger.modifications || []) {
46
+ if (m.path === path && (!best || m.at > best.at)) best = m;
47
+ }
48
+ return best;
49
+ }
50
+
51
+ async function main() {
52
+ if (getLevel(ROOT) < 3) return;
53
+ const raw = await readStdin();
54
+ if (!raw) return;
55
+ let payload;
56
+ try {
57
+ payload = JSON.parse(raw);
58
+ } catch {
59
+ return;
60
+ }
61
+
62
+ const filePath = extractFilePath(payload);
63
+ if (!filePath) return;
64
+ const target = toRepoRelative(filePath);
65
+ if (!target) return;
66
+
67
+ const myId = resolveSessionId(payload);
68
+ const tool = payload.tool_name;
69
+ const warnings = [];
70
+
71
+ // 1. Another active session touched this exact file recently.
72
+ const now = Date.now();
73
+ for (const { sessionId, ledger } of await listAllLedgers()) {
74
+ if (sessionId === myId) continue;
75
+ if (ledger.registered) continue; // their work is already saved/registered
76
+ const m = lastMod(ledger, target);
77
+ if (m && now - m.at < RECENT_MS) {
78
+ warnings.push(`another active session \`${sessionId.slice(0, 8)}\` modified it ${Math.round((now - m.at) / 60000)}m ago`);
79
+ }
80
+ }
81
+
82
+ // 2. The file changed on disk since THIS session last wrote it (external edit).
83
+ const mine = lastMod(await readLedger(myId), target);
84
+ if (mine && typeof mine.mtime === 'number') {
85
+ try {
86
+ const current = (await stat(resolve(ROOT, filePath))).mtimeMs;
87
+ if (current > mine.mtime + 1) warnings.push('it changed on disk since you last wrote it');
88
+ } catch {
89
+ /* gone — ignore */
90
+ }
91
+ }
92
+
93
+ if (warnings.length === 0) return;
94
+
95
+ const out = [
96
+ '<concurrency-warning>',
97
+ `⚠️ Concurrency on \`${target}\`: ${warnings.join('; ')}.`,
98
+ tool === 'Write'
99
+ ? ' You are about to OVERWRITE the whole file. Re-read it first, then preserve the other'
100
+ : ' Re-read it first, then make sure you only change your part —',
101
+ ' changes and add yours — do NOT clobber another session\'s work.',
102
+ '</concurrency-warning>',
103
+ ].join('\n');
104
+ process.stdout.write(out);
105
+ }
106
+
107
+ main().catch((err) => {
108
+ process.stderr.write(`[concurrency-guard] ${err?.message ?? err}\n`);
109
+ process.exit(0);
110
+ });
@@ -0,0 +1,231 @@
1
+ /**
2
+ * Shared helpers for the per-session ledger (L2).
3
+ *
4
+ * Each Claude Code session writes its OWN ledger at:
5
+ * `.claude/.sessions/<sessionId>.json`
6
+ *
7
+ * Why per-session and not a single file?
8
+ * - Multiple parallel chats on the same machine would otherwise stomp on
9
+ * each other's state. Per-session files isolate naturally.
10
+ * - For multi-machine parallelism, `git worktree` gives each chat its own
11
+ * `.claude/.sessions/` directory anyway (it lives outside `.git`).
12
+ *
13
+ * Ledger schema:
14
+ * {
15
+ * sessionId: string,
16
+ * startedAt: number,
17
+ * modifications: Array<{ path, tool, at }>,
18
+ * registered: boolean,
19
+ * stopWarnedAt: number | null,
20
+ * simulations: Array<{ objective, coveredPaths, predictionFile, at }>
21
+ * }
22
+ *
23
+ * All functions are defensive: never throw; on error return safe defaults so
24
+ * a hook never breaks a Claude session. Zero third-party deps.
25
+ */
26
+ import { mkdir, readdir, readFile, stat } from 'node:fs/promises';
27
+ import { dirname, relative, resolve } from 'node:path';
28
+ import { isImportant, isRegistrationFile, isTrackable } from './path-classification.mjs';
29
+ import { writeFileAtomic } from './safe-io.mjs';
30
+ import { LEDGER_DIR } from '../config/paths.mjs';
31
+
32
+ export { isImportant, isRegistrationFile, isTrackable };
33
+
34
+ export const ROOT = process.cwd();
35
+ export const SESSIONS_DIR = resolve(ROOT, LEDGER_DIR);
36
+
37
+ /**
38
+ * "Last touched" pointer — updated by every track-edits run. Lets slash
39
+ * commands (`/log-session`) discover the current session without a hook
40
+ * payload.
41
+ */
42
+ export const LAST_TOUCHED_PATH = resolve(SESSIONS_DIR, '.last-touched');
43
+
44
+ export function ledgerPathFor(sessionId) {
45
+ return resolve(SESSIONS_DIR, `${sanitizeSid(sessionId)}.json`);
46
+ }
47
+
48
+ /** Reads a ledger by sessionId. Returns a fresh ledger if missing/corrupt. */
49
+ export async function readLedger(sessionId) {
50
+ try {
51
+ const raw = await readFile(ledgerPathFor(sessionId), 'utf-8');
52
+ return normalizeLedger(JSON.parse(raw), sessionId);
53
+ } catch {
54
+ return freshLedger(sessionId);
55
+ }
56
+ }
57
+
58
+ /** Persists a ledger and updates the last-touched pointer (both atomic). */
59
+ export async function writeLedger(sessionId, ledger) {
60
+ try {
61
+ await mkdir(SESSIONS_DIR, { recursive: true });
62
+ await writeFileAtomic(ledgerPathFor(sessionId), JSON.stringify(ledger, null, 2));
63
+ await writeFileAtomic(LAST_TOUCHED_PATH, JSON.stringify({ sessionId, at: Date.now() }));
64
+ } catch (err) {
65
+ process.stderr.write(`[ledger] write failed: ${err?.message ?? err}\n`);
66
+ }
67
+ }
68
+
69
+ /**
70
+ * Most recently touched ledger across all session files. Used by slash
71
+ * commands that don't receive a hook payload.
72
+ *
73
+ * @returns {Promise<{ sessionId: string, ledger: any } | null>}
74
+ */
75
+ export async function readMostRecentLedger() {
76
+ try {
77
+ const raw = await readFile(LAST_TOUCHED_PATH, 'utf-8');
78
+ const ptr = JSON.parse(raw);
79
+ if (typeof ptr?.sessionId !== 'string') return null;
80
+ return { sessionId: ptr.sessionId, ledger: await readLedger(ptr.sessionId) };
81
+ } catch {
82
+ try {
83
+ const files = await readdir(SESSIONS_DIR);
84
+ let best = null;
85
+ for (const f of files) {
86
+ if (!f.endsWith('.json') || f.startsWith('.')) continue;
87
+ const st = await stat(resolve(SESSIONS_DIR, f));
88
+ if (!best || st.mtimeMs > best.mtime) {
89
+ best = { sessionId: f.slice(0, -5), mtime: st.mtimeMs };
90
+ }
91
+ }
92
+ if (!best) return null;
93
+ return { sessionId: best.sessionId, ledger: await readLedger(best.sessionId) };
94
+ } catch {
95
+ return null;
96
+ }
97
+ }
98
+ }
99
+
100
+ /** Lists all ledgers in this worktree. Used by SessionStart drift detection. */
101
+ export async function listAllLedgers() {
102
+ let files = [];
103
+ try {
104
+ files = await readdir(SESSIONS_DIR);
105
+ } catch {
106
+ return [];
107
+ }
108
+ const all = [];
109
+ for (const f of files) {
110
+ if (!f.endsWith('.json') || f.startsWith('.')) continue;
111
+ const sid = f.slice(0, -5);
112
+ all.push({ sessionId: sid, ledger: await readLedger(sid) });
113
+ }
114
+ return all;
115
+ }
116
+
117
+ export function freshLedger(sessionId) {
118
+ return {
119
+ sessionId,
120
+ startedAt: Date.now(),
121
+ modifications: [],
122
+ registered: false,
123
+ stopWarnedAt: null,
124
+ simulations: [],
125
+ };
126
+ }
127
+
128
+ /**
129
+ * Maps a session id to a filesystem-safe token: only `[A-Za-z0-9_-]`, capped at
130
+ * 64 chars. Exported so every path-constructing caller (ledger, claim/release,
131
+ * track-edits) sanitizes consistently — a session id is external input and must
132
+ * never be able to escape `.claude/.sessions` or `.claude/.workspace` (`../`).
133
+ */
134
+ export function sanitizeSid(sid) {
135
+ return String(sid).replace(/[^a-zA-Z0-9_-]/g, '_').slice(0, 64);
136
+ }
137
+
138
+ function normalizeLedger(obj, sessionId) {
139
+ const base = freshLedger(sessionId);
140
+ if (!obj || typeof obj !== 'object') return base;
141
+ return {
142
+ sessionId: typeof obj.sessionId === 'string' ? obj.sessionId : sessionId,
143
+ startedAt: typeof obj.startedAt === 'number' ? obj.startedAt : base.startedAt,
144
+ modifications: Array.isArray(obj.modifications) ? obj.modifications : [],
145
+ registered: obj.registered === true,
146
+ stopWarnedAt: typeof obj.stopWarnedAt === 'number' ? obj.stopWarnedAt : null,
147
+ simulations: Array.isArray(obj.simulations) ? obj.simulations : [],
148
+ };
149
+ }
150
+
151
+ export function toRepoRelative(absOrRel) {
152
+ if (!absOrRel) return '';
153
+ try {
154
+ return relative(ROOT, resolve(ROOT, absOrRel)).replaceAll('\\', '/');
155
+ } catch {
156
+ return String(absOrRel);
157
+ }
158
+ }
159
+
160
+ export function pendingImportantPaths(ledger) {
161
+ const seen = new Set();
162
+ for (const mod of ledger.modifications) {
163
+ if (isRegistrationFile(mod.path)) continue;
164
+ if (!isImportant(mod.path)) continue;
165
+ seen.add(mod.path);
166
+ }
167
+ return [...seen];
168
+ }
169
+
170
+ export function wasRegisteredDuringSession(ledger) {
171
+ return ledger.modifications.some((m) => isRegistrationFile(m.path));
172
+ }
173
+
174
+ /**
175
+ * Appends a simulate-impact record (L5). `coveredPaths` MUST already be
176
+ * repo-relative + forward-slashed. Mutates the ledger in place and returns it.
177
+ *
178
+ * @param {ReturnType<typeof freshLedger>} ledger
179
+ * @param {{ objective: string, coveredPaths: string[], predictionFile?: string }} entry
180
+ */
181
+ export function markSimulation(ledger, entry) {
182
+ if (!ledger || typeof ledger !== 'object') return ledger;
183
+ if (!entry || typeof entry.objective !== 'string') return ledger;
184
+ if (!Array.isArray(ledger.simulations)) ledger.simulations = [];
185
+ const covered = Array.isArray(entry.coveredPaths)
186
+ ? entry.coveredPaths.filter((p) => typeof p === 'string' && p.length > 0)
187
+ : [];
188
+ ledger.simulations.push({
189
+ objective: entry.objective,
190
+ coveredPaths: covered,
191
+ predictionFile: typeof entry.predictionFile === 'string' ? entry.predictionFile : null,
192
+ at: Date.now(),
193
+ });
194
+ return ledger;
195
+ }
196
+
197
+ /**
198
+ * True when at least one simulation entry covers `targetPath` (exact match or
199
+ * a directory-prefix claim ending in `/`).
200
+ *
201
+ * @param {ReturnType<typeof freshLedger>} ledger
202
+ * @param {string} targetPath — repo-relative, forward-slashed
203
+ */
204
+ export function hasSimulationFor(ledger, targetPath) {
205
+ if (!ledger || !Array.isArray(ledger.simulations)) return false;
206
+ if (typeof targetPath !== 'string' || targetPath.length === 0) return false;
207
+ const normalized = toRepoRelative(targetPath);
208
+ for (const sim of ledger.simulations) {
209
+ if (!sim || !Array.isArray(sim.coveredPaths)) continue;
210
+ for (const covered of sim.coveredPaths) {
211
+ if (typeof covered !== 'string' || covered.length === 0) continue;
212
+ if (covered === normalized) return true;
213
+ if (covered.endsWith('/') && normalized.startsWith(covered)) return true;
214
+ }
215
+ }
216
+ return false;
217
+ }
218
+
219
+ /**
220
+ * Reads the sessionId from a hook stdin payload, falling back to env then a
221
+ * synthetic per-process id so the script never crashes.
222
+ *
223
+ * @param {any} payload
224
+ */
225
+ export function resolveSessionId(payload) {
226
+ if (payload?.session_id && typeof payload.session_id === 'string') return payload.session_id;
227
+ if (process.env.CLAUDE_SESSION_ID) return process.env.CLAUDE_SESSION_ID;
228
+ return `local_${process.pid}_${Date.now()}`;
229
+ }
230
+
231
+ export { dirname };
@@ -0,0 +1,65 @@
1
+ /**
2
+ * Markdown extraction primitives — pure, zero-dep, defensive. [ADR-0027]
3
+ *
4
+ * Generic helpers shared by the session and ADR digest cores (the second
5
+ * consumer that justified the split). No I/O, no path construction — callers
6
+ * pass already-split lines or raw values. Each helper degrades to '' / [] rather
7
+ * than throwing, so a malformed document never breaks a digest.
8
+ */
9
+
10
+ /** Clips to `max` chars with a trailing ellipsis when over budget. */
11
+ export const clip = (text, max) =>
12
+ String(text).length > max ? `${String(text).slice(0, max - 1).trimEnd()}…` : String(text);
13
+
14
+ /** Strips markdown noise: `[label](url)` → `label`, drops `*_\`` markers, collapses ws. */
15
+ export function stripMd(value) {
16
+ return String(value)
17
+ .replace(/\[([^\]]+)\]\([^)]*\)/g, '$1')
18
+ .replace(/[`*_]/g, '')
19
+ .replace(/\s+/g, ' ')
20
+ .trim();
21
+ }
22
+
23
+ /** Body lines under a `## …<keyword>…` heading, until the next `##`/`---`/EOF. */
24
+ export function section(lines, keyword) {
25
+ const kw = keyword.toLowerCase();
26
+ const start = lines.findIndex((l) => /^##\s+/.test(l) && l.toLowerCase().includes(kw));
27
+ if (start === -1) return [];
28
+ const rest = lines.slice(start + 1);
29
+ const end = rest.findIndex((l) => /^##\s+/.test(l) || l.trim() === '---');
30
+ return rest.slice(0, end === -1 ? rest.length : end);
31
+ }
32
+
33
+ /** First prose paragraph of a section as one clean line (markdown stripped). */
34
+ export function firstParagraph(sectionLines, max = 180) {
35
+ const buffer = [];
36
+ for (const raw of sectionLines) {
37
+ const line = raw.replace(/^>\s?/, '');
38
+ if (!line.trim()) {
39
+ if (buffer.length) break;
40
+ continue;
41
+ }
42
+ if (/^#{1,6}\s/.test(line)) break;
43
+ buffer.push(line.replace(/^[-*]\s+/, '').trim());
44
+ if (buffer.join(' ').length >= max) break;
45
+ }
46
+ return clip(stripMd(buffer.join(' ')), max);
47
+ }
48
+
49
+ /** Bullet items of a section (text after the marker, markdown stripped), each clipped. */
50
+ export function bullets(sectionLines, max = 110) {
51
+ return sectionLines
52
+ .filter((l) => /^\s*[-*]\s+/.test(l))
53
+ .map((l) => clip(stripMd(l.replace(/^\s*[-*]\s+/, '')), max))
54
+ .filter(Boolean);
55
+ }
56
+
57
+ /** Reads a `- **Label**: value` metadata bullet (backticks stripped); '' if absent. */
58
+ export function metaValue(lines, label) {
59
+ const re = new RegExp(`^[-*]\\s*\\*\\*${label}\\*\\*:\\s*(.+)$`, 'i');
60
+ for (const l of lines) {
61
+ const m = re.exec(l.trim());
62
+ if (m) return m[1].replace(/`/g, '').trim();
63
+ }
64
+ return '';
65
+ }
@@ -0,0 +1,62 @@
1
+ /**
2
+ * Path classification for the L2 session ledger — CONFIG-DRIVEN.
3
+ *
4
+ * Three predicates power drift detection:
5
+ * - `isTrackable(path)` — record this edit in the ledger at all?
6
+ * Excludes build output, caches and the ledger's own runtime state.
7
+ * - `isImportant(path)` — does an edit here trigger the Stop drift nudge?
8
+ * Captures source, platform and top-level config.
9
+ * - `isRegistrationFile(path)` — does an edit here count AS registering
10
+ * the session (suppressing the nudge)?
11
+ *
12
+ * Prefix lists come from `contextkit/config.json` (`ledger.*`), falling back to
13
+ * the stack-agnostic defaults. This is the seam that makes the kit portable:
14
+ * a Python project sets `important: ["app/", "tests/"]`, a Go project sets
15
+ * `["cmd/", "internal/"]`, etc. — no code edits required.
16
+ *
17
+ * Zero third-party deps so it runs on a fresh project.
18
+ */
19
+ import { loadConfigSync } from '../config/load.mjs';
20
+
21
+ const config = loadConfigSync();
22
+ const IMPORTANT_PREFIXES = config.ledger.important;
23
+ const IRRELEVANT_PREFIXES = config.ledger.irrelevant;
24
+ const REGISTRATION_PATHS = config.ledger.registration;
25
+
26
+ function normalize(relPath) {
27
+ return relPath.replaceAll('\\', '/');
28
+ }
29
+
30
+ /**
31
+ * Returns true if the given repo-relative path should be persisted in the
32
+ * session ledger.
33
+ *
34
+ * @param {string} relPath
35
+ */
36
+ export function isTrackable(relPath) {
37
+ if (!relPath) return false;
38
+ const norm = normalize(relPath);
39
+ return !IRRELEVANT_PREFIXES.some((p) => norm === p || norm.startsWith(p));
40
+ }
41
+
42
+ /**
43
+ * Returns true if a modification at the given path should count toward the
44
+ * Stop-hook drift nudge.
45
+ *
46
+ * @param {string} relPath
47
+ */
48
+ export function isImportant(relPath) {
49
+ const norm = normalize(relPath);
50
+ return IMPORTANT_PREFIXES.some((p) => norm === p || norm.startsWith(p));
51
+ }
52
+
53
+ /**
54
+ * Returns true if a modification at the given path counts AS the session
55
+ * registration step (suppresses the nudge).
56
+ *
57
+ * @param {string} relPath
58
+ */
59
+ export function isRegistrationFile(relPath) {
60
+ const norm = normalize(relPath);
61
+ return REGISTRATION_PATHS.includes(norm);
62
+ }
@@ -0,0 +1,84 @@
1
+ /**
2
+ * Atomic file writes — shared, zero-dependency I/O helper.
3
+ *
4
+ * Strategy: write to a unique temp sibling, then `rename` it over the target.
5
+ * `rename(2)` is atomic on the same filesystem, so a concurrent reader always
6
+ * sees either the previous file or the complete new one — never a half-written,
7
+ * truncated, or interleaved file. This is the safe replacement for the bare
8
+ * `writeFile` calls in the ledger, workspace and pipeline writers, which could
9
+ * corrupt state when two sessions wrote the same artifact at once.
10
+ *
11
+ * Lives in `runtime/hooks/` alongside the other internal libs (ledger.mjs,
12
+ * path-classification.mjs) so both the runtime hooks and the tool scripts share
13
+ * one source. Sync variant for the scripts, async for the hot-path hooks.
14
+ */
15
+ import { existsSync, readFileSync, renameSync, unlinkSync, writeFileSync } from 'node:fs';
16
+ import { rename, unlink, writeFile } from 'node:fs/promises';
17
+
18
+ const BOM = /^/;
19
+
20
+ /**
21
+ * Parse JSON defensively: strips a leading UTF-8 BOM (common from Windows
22
+ * editors / PowerShell) and returns `fallback` on any error instead of throwing.
23
+ */
24
+ export function parseJsonSafe(text, fallback = null) {
25
+ if (typeof text !== 'string') return fallback;
26
+ try {
27
+ return JSON.parse(text.replace(BOM, ''));
28
+ } catch {
29
+ return fallback;
30
+ }
31
+ }
32
+
33
+ /**
34
+ * Read + parse a JSON file defensively (BOM-tolerant). A missing file or invalid
35
+ * JSON returns `fallback`. This is the single source for the BOM-strip+parse
36
+ * pattern previously copy-pasted across the tool scripts.
37
+ */
38
+ export function readJsonSafe(path, fallback = null) {
39
+ if (!existsSync(path)) return fallback;
40
+ try {
41
+ return parseJsonSafe(readFileSync(path, 'utf-8'), fallback);
42
+ } catch {
43
+ return fallback;
44
+ }
45
+ }
46
+
47
+ /** Unique temp path next to the target (same dir → same filesystem → atomic rename). */
48
+ function tmpName(path) {
49
+ return `${path}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
50
+ }
51
+
52
+ /**
53
+ * Atomically write `data` to `path` (synchronous — for the tool scripts).
54
+ * On failure the temp file is cleaned up and the original error re-thrown.
55
+ */
56
+ export function writeFileAtomicSync(path, data, encoding = 'utf-8') {
57
+ const tmp = tmpName(path);
58
+ try {
59
+ writeFileSync(tmp, data, encoding);
60
+ renameSync(tmp, path);
61
+ } catch (err) {
62
+ try {
63
+ unlinkSync(tmp);
64
+ } catch {
65
+ /* temp may not exist */
66
+ }
67
+ throw err;
68
+ }
69
+ }
70
+
71
+ /**
72
+ * Atomically write `data` to `path` (async — for the runtime hooks).
73
+ * On failure the temp file is cleaned up and the original error re-thrown.
74
+ */
75
+ export async function writeFileAtomic(path, data, encoding = 'utf-8') {
76
+ const tmp = tmpName(path);
77
+ try {
78
+ await writeFile(tmp, data, encoding);
79
+ await rename(tmp, path);
80
+ } catch (err) {
81
+ await unlink(tmp).catch(() => {});
82
+ throw err;
83
+ }
84
+ }
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Session-log digest core — pure, zero-dep structural extraction. [ADR-0027]
3
+ *
4
+ * Parses a ContextDevKit session log into a compact record so callers reason over a
5
+ * ~6-line digest instead of the 75–188 raw lines. SINGLE SOURCE (rule 4): used by
6
+ * BOTH the boot hook (`boot-context-readers.digestLatestSession`) and the
7
+ * `session-digest.mjs` / `context-pack.mjs` scripts.
8
+ *
9
+ * No I/O and no path construction here — callers pass the file text + name; this
10
+ * module only parses and renders. Every function is defensive: a malformed log
11
+ * yields `{ ok: false }` so the caller can fall back to the raw view (rule 8 —
12
+ * a digest miss is a visible fallback, never a silent drop).
13
+ */
14
+
15
+ import { bullets, firstParagraph, metaValue, section } from './md-extract.mjs';
16
+
17
+ /** `<YYYY-MM-DD>-<NN>-<slug>.md` — same shape the reindex/boot readers use. */
18
+ export const SESSION_FILENAME_RE = /^(\d{4}-\d{2}-\d{2})-(\d{2,})-([a-z0-9._-]+)\.md$/;
19
+ const ADR_RE = /ADR-(\d{4})/g;
20
+
21
+ /** Drops a redundant `Session <N> —` prefix the title often repeats. */
22
+ function cleanTitle(title, number) {
23
+ if (number == null) return title.trim();
24
+ return title.replace(new RegExp(`^session\\s*${number}\\s*[—:\\-]\\s*`, 'i'), '').trim();
25
+ }
26
+
27
+ /** Works / Pending / Mixed verdict from the `Final state` section (honest ''). */
28
+ function deriveVerdict(finalLines) {
29
+ const text = finalLines.join('\n');
30
+ const works = /\bworks?\b/i.test(text) || text.includes('✅');
31
+ const pending = /\bpending\b|\bnot done\b|\bawait/i.test(text) || text.includes('⚠️');
32
+ if (works && pending) return 'Mixed';
33
+ if (works) return 'Works';
34
+ if (pending) return 'Pending';
35
+ return '';
36
+ }
37
+
38
+ /**
39
+ * Parses one session-log markdown string into a compact record.
40
+ * @param {string} text the file contents
41
+ * @param {string} filename the `<date>-<NN>-<slug>.md` name (for fallbacks)
42
+ * @returns {{ok:boolean, number:?number, date:string, branch:string, title:string,
43
+ * request:string, done:string, decisions:string[], adrs:string[], verdict:string, slug:string}}
44
+ */
45
+ export function parseSessionLog(text, filename = '') {
46
+ const safe = String(text || '').replace(/^/, '');
47
+ const lines = safe.split('\n');
48
+ const fm = SESSION_FILENAME_RE.exec(filename) || [];
49
+ const number = Number.parseInt(metaValue(lines, 'Session number') || fm[2] || '', 10) || null;
50
+ const rawTitle = (lines.find((l) => l.startsWith('# ')) || '').slice(2).trim();
51
+ const title = cleanTitle(rawTitle, number);
52
+ const date = (metaValue(lines, 'Date') || fm[1] || '').trim();
53
+ const branch = metaValue(lines, 'Branch');
54
+ const request = firstParagraph(section(lines, 'request'));
55
+ const done = firstParagraph(section(lines, 'done'));
56
+ const decisions = bullets(section(lines, 'decision')).slice(0, 4);
57
+ const adrs = [...new Set(safe.match(ADR_RE) || [])].sort();
58
+ const verdict = deriveVerdict(section(lines, 'final'));
59
+ // `ok` means we extracted usable STRUCTURE (not just a number from the filename),
60
+ // so a structureless log degrades to the raw boot view rather than a bare header.
61
+ const ok = Boolean(title || request || done || decisions.length || verdict);
62
+ return { ok, number, date, branch, title, request, done, decisions, adrs, verdict, slug: fm[3] || '' };
63
+ }
64
+
65
+ /** Multi-line (~6) digest block for one session, or `null` if unparseable. */
66
+ export function renderDigest(record) {
67
+ if (!record || !record.ok) return null;
68
+ const head = `**Session ${record.number ?? '??'}${record.title ? ` — ${record.title}` : ''}**` +
69
+ `${record.date ? ` (${record.date}${record.branch ? ` · \`${record.branch}\`` : ''})` : ''}`;
70
+ const out = [head];
71
+ if (record.request) out.push(`- Request: ${record.request}`);
72
+ if (record.done) out.push(`- Done: ${record.done}`);
73
+ if (record.decisions.length) out.push(`- Decisions: ${record.decisions.join('; ')}`);
74
+ if (record.adrs.length) out.push(`- ADRs: ${record.adrs.join(', ')}`);
75
+ out.push(`- Final: ${record.verdict || '—'}`);
76
+ return out.join('\n');
77
+ }
78
+
79
+ /** One-line index form for a list of sessions. */
80
+ export function renderDigestLine(record) {
81
+ if (!record || !record.ok) return null;
82
+ const adrs = record.adrs.length ? ` [${record.adrs.join(', ')}]` : '';
83
+ const verdict = record.verdict ? ` _${record.verdict}_` : '';
84
+ return `- **${record.number ?? '??'}** ${record.date} — ${record.title || record.slug}${verdict}${adrs}`;
85
+ }