forge-workflow 0.0.10 → 0.1.0-beta.2

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 (454) hide show
  1. package/.claude/rules/{greptile-review-process.md → review-process.md} +56 -41
  2. package/.claude/scripts/{greptile-resolve.sh → review-resolve.sh} +13 -3
  3. package/.cursor/rules/permissions-guidance.mdc +2 -2
  4. package/.forge/hooks/check-tdd.js +3 -0
  5. package/.forge/hooks/forge-native-hook.js +245 -0
  6. package/.forge/protected-paths.yaml +157 -0
  7. package/AGENTS.md +150 -61
  8. package/CHANGELOG.md +681 -0
  9. package/CLAUDE.md +9 -118
  10. package/QUICKSTART.md +171 -0
  11. package/README.md +271 -363
  12. package/bin/forge-cmd.js +120 -9
  13. package/bin/forge-preflight.js +26 -5
  14. package/bin/forge.js +461 -489
  15. package/docs/INDEX.md +93 -0
  16. package/docs/PROJECT_DESIGN.md +685 -0
  17. package/docs/architecture/index.md +66 -0
  18. package/docs/architecture/notes/README.md +35 -0
  19. package/docs/architecture/subsystems/README.md +46 -0
  20. package/docs/forge/TOOLCHAIN.md +670 -0
  21. package/docs/forge/VALIDATION.md +82 -0
  22. package/docs/{AGENT_INSTALL_PROMPT.md → guides/AGENT_INSTALL_PROMPT.md} +3 -3
  23. package/docs/guides/BEADS_GITHUB_SYNC.md +32 -0
  24. package/docs/{ENHANCED_ONBOARDING.md → guides/ENHANCED_ONBOARDING.md} +16 -12
  25. package/docs/guides/GREPTILE_SETUP.md +46 -0
  26. package/docs/guides/MANUAL_REVIEW_GUIDE.md +58 -0
  27. package/docs/guides/MIGRATION.md +56 -0
  28. package/docs/guides/SETUP.md +118 -0
  29. package/docs/guides/SUPPORT.md +185 -0
  30. package/docs/guides/WORKFLOW_TEMPLATES.md +74 -0
  31. package/docs/guides/memory-backends.md +183 -0
  32. package/docs/reference/ADAPTERS.md +128 -0
  33. package/docs/reference/AGENT_SKILL_PARITY.md +175 -0
  34. package/docs/reference/COMMANDS.md +205 -0
  35. package/docs/reference/DECISION_DRIFT_GUARDS.md +97 -0
  36. package/docs/{EXAMPLES.md → reference/EXAMPLES.md} +7 -5
  37. package/docs/reference/FORGE_KERNEL_STORAGE_MODEL.md +135 -0
  38. package/docs/reference/HERMES_INTEGRATION.md +118 -0
  39. package/docs/reference/INSIGHTS_RECAP.md +63 -0
  40. package/docs/reference/INSTALL.md +164 -0
  41. package/docs/reference/KERNEL_TAXONOMY_VALIDATION.md +161 -0
  42. package/docs/reference/PROTECTED_PATH_MANIFEST.md +25 -0
  43. package/docs/reference/RELEASE.md +68 -0
  44. package/docs/reference/RESEARCH_TEMPLATE.md +292 -0
  45. package/docs/{ROADMAP.md → reference/ROADMAP.md} +12 -9
  46. package/docs/reference/SKILLS.md +35 -0
  47. package/docs/reference/STATUS_BOARD.md +80 -0
  48. package/docs/reference/TEMPLATES.md +106 -0
  49. package/docs/{TOOLCHAIN.md → reference/TOOLCHAIN.md} +62 -47
  50. package/docs/reference/VALIDATION.md +82 -0
  51. package/docs/reference/agent-permissions.md +169 -0
  52. package/docs/reference/beads-to-kernel-migration-ux.md +61 -0
  53. package/docs/reference/control-plane-guarantees.md +125 -0
  54. package/docs/reference/dependency-chain.md +331 -0
  55. package/docs/reference/forge-kernel-issue-command-contract.md +161 -0
  56. package/docs/reference/forge-kernel-schema.md +72 -0
  57. package/docs/reference/kernel-conflict-evaluators.md +27 -0
  58. package/docs/reference/patch-md-format.md +77 -0
  59. package/docs/reference/protected-state-surfaces.md +59 -0
  60. package/docs/reference/shepherd.md +115 -0
  61. package/docs/reference/superpowers-analysis.md +320 -0
  62. package/docs/reference/superpowers-integration-options.md +404 -0
  63. package/docs/reference/test-environment.md +519 -0
  64. package/docs/reference/upgrade-safety.md +59 -0
  65. package/lefthook.yml +18 -0
  66. package/lib/adapter-cli.js +307 -0
  67. package/lib/adapters/beads-issue-adapter.js +127 -0
  68. package/lib/adapters/beads-kernel-compat.js +1042 -0
  69. package/lib/adapters/greptile-review-adapter.js +141 -0
  70. package/lib/adapters/kernel-issue-adapter.js +101 -0
  71. package/lib/adapters/pr-state-adapter.js +484 -0
  72. package/lib/adoption-profiles.js +126 -0
  73. package/lib/agents/README.md +2 -6
  74. package/lib/agents/claude.plugin.json +3 -8
  75. package/lib/agents/codex.plugin.json +9 -1
  76. package/lib/agents/cursor.plugin.json +2 -6
  77. package/lib/agents/hermes.plugin.json +22 -0
  78. package/lib/agents-config.js +39 -1236
  79. package/lib/audit-evidence.js +282 -0
  80. package/lib/beads-setup.js +121 -0
  81. package/lib/beads-sync-scaffold.js +25 -101
  82. package/lib/codex-skills.js +51 -1
  83. package/lib/commands/_issue.js +741 -77
  84. package/lib/commands/_manifest.js +91 -0
  85. package/lib/commands/_registry.js +85 -34
  86. package/lib/commands/_resolve-command-opts.js +261 -0
  87. package/lib/commands/_serve-security.js +270 -0
  88. package/lib/commands/adapter.js +12 -0
  89. package/lib/commands/add.js +118 -0
  90. package/lib/commands/audit.js +70 -0
  91. package/lib/commands/blocked.js +5 -0
  92. package/lib/commands/board.js +64 -0
  93. package/lib/commands/claim.js +21 -2
  94. package/lib/commands/claims.js +7 -0
  95. package/lib/commands/clean.js +485 -75
  96. package/lib/commands/close.js +2 -2
  97. package/lib/commands/comment.js +5 -0
  98. package/lib/commands/control.js +148 -0
  99. package/lib/commands/create.js +2 -2
  100. package/lib/commands/dev.js +185 -7
  101. package/lib/commands/doc-gate.js +336 -0
  102. package/lib/commands/doctor.js +156 -0
  103. package/lib/commands/explain.js +15 -0
  104. package/lib/commands/export.js +237 -0
  105. package/lib/commands/gate.js +192 -0
  106. package/lib/commands/hooks.js +242 -0
  107. package/lib/commands/inbox.js +118 -0
  108. package/lib/commands/init.js +598 -0
  109. package/lib/commands/insights.js +79 -0
  110. package/lib/commands/issue.js +12 -1
  111. package/lib/commands/issues.js +17 -0
  112. package/lib/commands/lint.js +5 -0
  113. package/lib/commands/list.js +2 -2
  114. package/lib/commands/merge.js +312 -0
  115. package/lib/commands/migrate.js +523 -0
  116. package/lib/commands/new.js +12 -0
  117. package/lib/commands/options.js +241 -0
  118. package/lib/commands/orient.js +13 -0
  119. package/lib/commands/orphans.js +5 -0
  120. package/lib/commands/patch.js +67 -0
  121. package/lib/commands/plan.js +436 -24
  122. package/lib/commands/preflight.js +211 -0
  123. package/lib/commands/prime.js +13 -0
  124. package/lib/commands/push.js +69 -2
  125. package/lib/commands/ready.js +2 -2
  126. package/lib/commands/recall.js +116 -0
  127. package/lib/commands/recap.js +61 -0
  128. package/lib/commands/recommend.js +0 -1
  129. package/lib/commands/release.js +91 -0
  130. package/lib/commands/remember.js +74 -0
  131. package/lib/commands/role.js +99 -0
  132. package/lib/commands/serve.js +581 -0
  133. package/lib/commands/setup.js +838 -972
  134. package/lib/commands/shepherd.js +436 -0
  135. package/lib/commands/ship.js +23 -1
  136. package/lib/commands/show.js +2 -2
  137. package/lib/commands/stage.js +192 -0
  138. package/lib/commands/stale.js +5 -0
  139. package/lib/commands/status.js +158 -21
  140. package/lib/commands/sync.js +34 -46
  141. package/lib/commands/team.js +4 -1
  142. package/lib/commands/test.js +43 -27
  143. package/lib/commands/update.js +2 -2
  144. package/lib/commands/upgrade.js +47 -0
  145. package/lib/commands/validate.js +43 -18
  146. package/lib/commands/worktree.js +307 -100
  147. package/lib/config-writer.js +202 -0
  148. package/lib/control-plane.js +236 -0
  149. package/lib/core/runtime-graph.js +946 -0
  150. package/lib/dep-guard/keyword-ripple.js +2 -2
  151. package/lib/deprecated-sync-cleanup.js +362 -0
  152. package/lib/detect-agent.js +2 -28
  153. package/lib/detect-worktree.js +35 -9
  154. package/lib/doc-gate/declaration.js +177 -0
  155. package/lib/doc-gate/detect.js +289 -0
  156. package/lib/doc-gate/gate.js +375 -0
  157. package/lib/doc-gate/okf-config.js +128 -0
  158. package/lib/doc-gate/okf.js +429 -0
  159. package/lib/docs-command.js +1161 -6
  160. package/lib/forge-issues.js +382 -11
  161. package/lib/forge-lock.js +262 -0
  162. package/lib/gate-events.js +193 -0
  163. package/lib/global-flags.js +74 -0
  164. package/lib/greptile-match.js +7 -63
  165. package/lib/harness-capability-matrix.js +380 -0
  166. package/lib/hook-global-installer.js +347 -0
  167. package/lib/hook-renderer.js +451 -0
  168. package/lib/inbox.js +391 -0
  169. package/lib/insights.js +397 -0
  170. package/lib/issue-adapter.js +156 -0
  171. package/lib/issue-backend.js +145 -0
  172. package/lib/issue-render.js +220 -0
  173. package/lib/kernel/backing-issue.js +305 -0
  174. package/lib/kernel/broker.js +1218 -0
  175. package/lib/kernel/cli-broker-factory.js +130 -0
  176. package/lib/kernel/conflict-signal.js +82 -0
  177. package/lib/kernel/evaluators.js +195 -0
  178. package/lib/kernel/fs-class.js +495 -0
  179. package/lib/kernel/issue-command-contract.js +559 -0
  180. package/lib/kernel/issue-id-resolver.js +186 -0
  181. package/lib/kernel/lease-enforcer.js +158 -0
  182. package/lib/kernel/migrations.js +333 -0
  183. package/lib/kernel/planning-buckets-schema.js +109 -0
  184. package/lib/kernel/projection-jsonl-writer.js +450 -0
  185. package/lib/kernel/readiness-model.js +329 -0
  186. package/lib/kernel/schema.js +356 -0
  187. package/lib/kernel/sqlite-driver.js +2504 -0
  188. package/lib/kernel/taxonomy-validator.js +394 -0
  189. package/lib/lefthook-check.js +3 -2
  190. package/lib/lefthook-wiring.js +413 -0
  191. package/lib/mcp-config-renderer.js +288 -0
  192. package/lib/memory/graphiti-mcp.js +106 -0
  193. package/lib/memory/router.js +387 -0
  194. package/lib/memory/typed-api.js +102 -0
  195. package/lib/memory-digest.js +195 -0
  196. package/lib/merge-rules.js +395 -0
  197. package/lib/migrate-dry-run.js +466 -0
  198. package/lib/orientation.js +863 -0
  199. package/lib/package-manager-remediation.js +103 -0
  200. package/lib/package-root.js +381 -0
  201. package/lib/patch-intent.js +890 -0
  202. package/lib/plugin-catalog.js +3 -4
  203. package/lib/plugin-manager.js +0 -5
  204. package/lib/pr-bundle.js +186 -0
  205. package/lib/pr-monitor/differ.js +195 -0
  206. package/lib/pr-monitor/events.js +0 -0
  207. package/lib/pr-monitor/gather.js +124 -0
  208. package/lib/pr-monitor/journal.js +299 -0
  209. package/lib/pr-monitor/monitor.js +146 -0
  210. package/lib/pr-monitor/render-sticky.js +157 -0
  211. package/lib/pr-monitor/watch-lifecycle.js +95 -0
  212. package/lib/pr-monitor/watch.js +247 -0
  213. package/lib/pr-pull.js +1273 -0
  214. package/lib/pr-shepherd.js +494 -0
  215. package/lib/pr-state-validator.js +59 -0
  216. package/lib/preflight/gates.js +237 -0
  217. package/lib/preflight/runner.js +116 -0
  218. package/lib/project-discovery.js +0 -53
  219. package/lib/project-memory.js +99 -497
  220. package/lib/protected-path-manifest.js +281 -0
  221. package/lib/protected-state-surfaces.js +387 -0
  222. package/lib/release-readiness.js +2089 -0
  223. package/lib/reset.js +59 -45
  224. package/lib/review-adapter.js +68 -0
  225. package/lib/rules-sync.js +260 -0
  226. package/lib/runtime-health.js +241 -20
  227. package/lib/safety-config-renderer.js +268 -0
  228. package/lib/setup-action-log.js +1 -7
  229. package/lib/setup.js +27 -65
  230. package/lib/shell-utils.js +76 -6
  231. package/lib/skills-sync.js +330 -0
  232. package/lib/smart-status/scoring.js +17 -3
  233. package/lib/status/beads-snapshot.js +45 -2
  234. package/lib/status/presenter.js +169 -18
  235. package/lib/status/snapshot.js +186 -0
  236. package/lib/sync-backend.js +202 -0
  237. package/lib/untrusted-content.js +52 -0
  238. package/lib/upgrade-safety.js +199 -0
  239. package/lib/workflow/enforce-stage.js +296 -47
  240. package/lib/workflow/stage-transition.js +115 -0
  241. package/lib/workflow/stages.js +30 -6
  242. package/lib/workflow/state-manager.js +11 -22
  243. package/lib/workflow/state.js +23 -1
  244. package/lib/workflow-profiles.js +17 -5
  245. package/package.json +37 -35
  246. package/rules/documentation.md +19 -0
  247. package/rules/kernel-tracking.md +26 -0
  248. package/rules/security.md +22 -0
  249. package/rules/tdd.md +20 -0
  250. package/rules/workflow.md +27 -0
  251. package/scripts/auto-backing-issue.js +47 -0
  252. package/scripts/beads-context.sh +81 -57
  253. package/scripts/beads-upgrade-smoke.sh +24 -3
  254. package/scripts/bootstrap-windows-tools.sh +78 -0
  255. package/scripts/branch-protection.js +2 -3
  256. package/scripts/check-agents.js +34 -137
  257. package/scripts/commitlint.js +3 -1
  258. package/scripts/conflict-detect.sh +3 -0
  259. package/scripts/dep-guard.sh +22 -3
  260. package/scripts/file-index.sh +3 -0
  261. package/scripts/forge-team/lib/claim.sh +34 -18
  262. package/scripts/forge-team/lib/dashboard.sh +61 -86
  263. package/scripts/forge-team/lib/epic.sh +99 -263
  264. package/scripts/forge-team/lib/hooks.sh +26 -28
  265. package/scripts/forge-team/lib/identity.sh +4 -4
  266. package/scripts/forge-team/lib/sync-github.sh +49 -84
  267. package/scripts/forge-team/lib/verify.sh +93 -83
  268. package/scripts/forge-team/lib/workload.sh +41 -65
  269. package/scripts/forge-team/tests/claim.test.sh +25 -19
  270. package/scripts/forge-team/tests/dashboard.test.sh +31 -46
  271. package/scripts/forge-team/tests/epic.test.sh +52 -71
  272. package/scripts/forge-team/tests/hooks.test.sh +38 -50
  273. package/scripts/forge-team/tests/identity.test.sh +3 -3
  274. package/scripts/forge-team/tests/integration.test.sh +44 -66
  275. package/scripts/forge-team/tests/sync-github.test.sh +50 -83
  276. package/scripts/forge-team/tests/verify.test.sh +37 -46
  277. package/scripts/forge-team/tests/workflow-integration.test.sh +4 -4
  278. package/scripts/forge-team/tests/workload.test.sh +32 -66
  279. package/scripts/gen-command-manifest.js +153 -0
  280. package/scripts/gen-embedded-assets.mjs +129 -0
  281. package/scripts/install.ps1 +139 -0
  282. package/scripts/install.sh +268 -0
  283. package/scripts/lib/release-asset.mjs +84 -0
  284. package/scripts/parity-check.mjs +145 -0
  285. package/scripts/parity-check.test.mjs +58 -0
  286. package/scripts/pin-agentic-workflow-images.js +112 -0
  287. package/scripts/pr-coordinator.sh +3 -0
  288. package/scripts/preflight-sonar.eslint.config.mjs +44 -0
  289. package/scripts/preflight.sh +21 -94
  290. package/scripts/protected-state-check.js +104 -0
  291. package/scripts/smart-status.sh +60 -57
  292. package/scripts/spikes/config-race-bench.js +111 -0
  293. package/scripts/spikes/harness-capability-matrix.js +13 -0
  294. package/scripts/spikes/patch-anchor-stability-bench.js +125 -0
  295. package/scripts/spikes/protected-path-manifest.js +20 -0
  296. package/scripts/spikes/skill-auto-invoke-parity.js +292 -0
  297. package/scripts/sync-agent-skills.js +62 -0
  298. package/scripts/sync-utils.sh +3 -0
  299. package/scripts/test-ci-shard.js +13 -6
  300. package/scripts/test.js +95 -12
  301. package/skills/claim-safety/SKILL.md +102 -0
  302. package/skills/claim-safety/evals/evals.json +46 -0
  303. package/{.github/prompts/dev.prompt.md → skills/dev/SKILL.md} +44 -50
  304. package/skills/dev/evals/evals.json +50 -0
  305. package/skills/hermes-forge/SKILL.md +185 -0
  306. package/skills/hermes-forge/evals/evals.json +46 -0
  307. package/skills/issue-basics/SKILL.md +111 -0
  308. package/skills/issue-basics/evals/evals.json +46 -0
  309. package/skills/kernel/SKILL.md +166 -0
  310. package/skills/kernel/evals/evals.json +50 -0
  311. package/skills/memory/SKILL.md +102 -0
  312. package/skills/parallel-deep-research/SKILL.md +14 -11
  313. package/skills/parallel-deep-research/evals/evals.json +11 -27
  314. package/{.github/prompts/plan.prompt.md → skills/plan/SKILL.md} +132 -157
  315. package/skills/plan/evals/evals.json +42 -0
  316. package/skills/research/SKILL.md +195 -0
  317. package/skills/research/evals/evals.json +42 -0
  318. package/{.github/prompts/review.prompt.md → skills/review/SKILL.md} +98 -62
  319. package/skills/review/evals/evals.json +42 -0
  320. package/skills/rollback/SKILL.md +110 -0
  321. package/skills/rollback/evals/evals.json +46 -0
  322. package/skills/rollback/references/methods.md +204 -0
  323. package/{.cursor/commands/rollback.md → skills/rollback/references/workflow-integration.md} +10 -284
  324. package/skills/shepherd/SKILL.md +66 -0
  325. package/skills/shepherd/evals/evals.json +42 -0
  326. package/{.github/prompts/ship.prompt.md → skills/ship/SKILL.md} +81 -45
  327. package/skills/ship/evals/evals.json +42 -0
  328. package/skills/smith/SKILL.md +142 -0
  329. package/skills/smith/evals/evals.json +46 -0
  330. package/skills/smith/references/autonomy-and-gates.md +94 -0
  331. package/{.github/prompts/sonarcloud.prompt.md → skills/sonarcloud/SKILL.md} +14 -3
  332. package/skills/sonarcloud/evals/evals.json +46 -0
  333. package/skills/sonarcloud-analysis/SKILL.md +18 -13
  334. package/skills/sonarcloud-analysis/evals/evals.json +11 -15
  335. package/{.github/prompts/status.prompt.md → skills/status/SKILL.md} +20 -10
  336. package/skills/status/evals/evals.json +50 -0
  337. package/skills/triage-ready/SKILL.md +121 -0
  338. package/skills/triage-ready/evals/evals.json +42 -0
  339. package/{.github/prompts/validate.prompt.md → skills/validate/SKILL.md} +52 -29
  340. package/skills/validate/evals/evals.json +42 -0
  341. package/skills/verify/SKILL.md +299 -0
  342. package/skills/verify/evals/evals.json +50 -0
  343. package/.claude/commands/dev.md +0 -345
  344. package/.claude/commands/plan.md +0 -566
  345. package/.claude/commands/premerge.md +0 -186
  346. package/.claude/commands/research.md +0 -42
  347. package/.claude/commands/review.md +0 -451
  348. package/.claude/commands/rollback.md +0 -721
  349. package/.claude/commands/ship.md +0 -213
  350. package/.claude/commands/sonarcloud.md +0 -152
  351. package/.claude/commands/status.md +0 -90
  352. package/.claude/commands/validate.md +0 -288
  353. package/.claude/commands/verify.md +0 -269
  354. package/.claude/rules/workflow.md +0 -121
  355. package/.cline/workflows/dev.md +0 -342
  356. package/.cline/workflows/plan.md +0 -563
  357. package/.cline/workflows/premerge.md +0 -183
  358. package/.cline/workflows/research.md +0 -39
  359. package/.cline/workflows/review.md +0 -448
  360. package/.cline/workflows/rollback.md +0 -718
  361. package/.cline/workflows/ship.md +0 -210
  362. package/.cline/workflows/sonarcloud.md +0 -146
  363. package/.cline/workflows/status.md +0 -87
  364. package/.cline/workflows/validate.md +0 -285
  365. package/.cline/workflows/verify.md +0 -266
  366. package/.codex/config.toml +0 -11
  367. package/.codex/skills/dev/SKILL.md +0 -345
  368. package/.codex/skills/plan/SKILL.md +0 -566
  369. package/.codex/skills/premerge/SKILL.md +0 -186
  370. package/.codex/skills/research/SKILL.md +0 -42
  371. package/.codex/skills/review/SKILL.md +0 -451
  372. package/.codex/skills/rollback/SKILL.md +0 -721
  373. package/.codex/skills/ship/SKILL.md +0 -213
  374. package/.codex/skills/sonarcloud/SKILL.md +0 -149
  375. package/.codex/skills/status/SKILL.md +0 -90
  376. package/.codex/skills/validate/SKILL.md +0 -288
  377. package/.codex/skills/verify/SKILL.md +0 -269
  378. package/.cursor/commands/dev.md +0 -342
  379. package/.cursor/commands/plan.md +0 -563
  380. package/.cursor/commands/premerge.md +0 -183
  381. package/.cursor/commands/research.md +0 -39
  382. package/.cursor/commands/review.md +0 -448
  383. package/.cursor/commands/ship.md +0 -210
  384. package/.cursor/commands/sonarcloud.md +0 -146
  385. package/.cursor/commands/status.md +0 -87
  386. package/.cursor/commands/validate.md +0 -285
  387. package/.cursor/commands/verify.md +0 -266
  388. package/.cursorrules +0 -149
  389. package/.github/prompts/premerge.prompt.md +0 -188
  390. package/.github/prompts/research.prompt.md +0 -44
  391. package/.github/prompts/rollback.prompt.md +0 -723
  392. package/.github/prompts/verify.prompt.md +0 -271
  393. package/.github/workflows/beads-to-github.yml +0 -89
  394. package/.github/workflows/github-to-beads.yml +0 -100
  395. package/.kilocode/workflows/dev.md +0 -346
  396. package/.kilocode/workflows/plan.md +0 -567
  397. package/.kilocode/workflows/premerge.md +0 -187
  398. package/.kilocode/workflows/research.md +0 -43
  399. package/.kilocode/workflows/review.md +0 -452
  400. package/.kilocode/workflows/rollback.md +0 -722
  401. package/.kilocode/workflows/ship.md +0 -214
  402. package/.kilocode/workflows/sonarcloud.md +0 -150
  403. package/.kilocode/workflows/status.md +0 -91
  404. package/.kilocode/workflows/validate.md +0 -289
  405. package/.kilocode/workflows/verify.md +0 -270
  406. package/.opencode/commands/dev.md +0 -345
  407. package/.opencode/commands/plan.md +0 -566
  408. package/.opencode/commands/premerge.md +0 -186
  409. package/.opencode/commands/research.md +0 -42
  410. package/.opencode/commands/review.md +0 -451
  411. package/.opencode/commands/rollback.md +0 -721
  412. package/.opencode/commands/ship.md +0 -213
  413. package/.opencode/commands/sonarcloud.md +0 -149
  414. package/.opencode/commands/status.md +0 -90
  415. package/.opencode/commands/validate.md +0 -288
  416. package/.opencode/commands/verify.md +0 -269
  417. package/.roo/commands/dev.md +0 -346
  418. package/.roo/commands/plan.md +0 -567
  419. package/.roo/commands/premerge.md +0 -187
  420. package/.roo/commands/research.md +0 -43
  421. package/.roo/commands/review.md +0 -452
  422. package/.roo/commands/rollback.md +0 -722
  423. package/.roo/commands/ship.md +0 -214
  424. package/.roo/commands/sonarcloud.md +0 -150
  425. package/.roo/commands/status.md +0 -91
  426. package/.roo/commands/validate.md +0 -289
  427. package/.roo/commands/verify.md +0 -270
  428. package/docs/BEADS_GITHUB_SYNC.md +0 -281
  429. package/docs/GREPTILE_SETUP.md +0 -400
  430. package/docs/MANUAL_REVIEW_GUIDE.md +0 -106
  431. package/docs/SETUP.md +0 -663
  432. package/docs/VALIDATION.md +0 -363
  433. package/lib/agents/cline.plugin.json +0 -29
  434. package/lib/agents/copilot.plugin.json +0 -24
  435. package/lib/agents/kilocode.plugin.json +0 -22
  436. package/lib/agents/opencode.plugin.json +0 -23
  437. package/lib/agents/roo.plugin.json +0 -30
  438. package/lib/beads-bootstrap.js +0 -225
  439. package/lib/beads-health-check.js +0 -188
  440. package/lib/commands/commands-reset.js +0 -147
  441. package/opencode.json +0 -67
  442. package/scripts/beads-context.test.js +0 -584
  443. package/scripts/github-beads-sync/comment.mjs +0 -64
  444. package/scripts/github-beads-sync/config.mjs +0 -148
  445. package/scripts/github-beads-sync/github-api.mjs +0 -131
  446. package/scripts/github-beads-sync/index.mjs +0 -356
  447. package/scripts/github-beads-sync/label-mapper.mjs +0 -54
  448. package/scripts/github-beads-sync/mapping.mjs +0 -132
  449. package/scripts/github-beads-sync/reverse-sync-cli.mjs +0 -31
  450. package/scripts/github-beads-sync/reverse-sync.mjs +0 -162
  451. package/scripts/github-beads-sync/run-bd.mjs +0 -161
  452. package/scripts/github-beads-sync/sanitize.mjs +0 -121
  453. package/scripts/github-beads-sync.config.json +0 -26
  454. package/scripts/sync-commands.js +0 -600
@@ -0,0 +1,387 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * @module memory/router
5
+ *
6
+ * Single dispatch seam for `forge remember` / `forge recall`. The DEFAULT is
7
+ * `local`, which now means the kernel `kernel_memories` table indexed by FTS5
8
+ * (via `lib/project-memory.js`) — the same knowledge layer decisions and issues
9
+ * share. The router exists so the opt-in `graphiti` knowledge-graph tier can slot
10
+ * in behind the same CLI verbs WITHOUT changing the clean default path.
11
+ *
12
+ * Public config surface is deliberately `local | graphiti` only. `graphiti` is
13
+ * EXPERIMENTAL — its config/doctor scaffolding ships but the runtime emitter is a
14
+ * fast-follow, so today it always writes the local kernel floor and the emit is a
15
+ * best-effort no-op unless a caller injects an emitter.
16
+ *
17
+ * Design: docs/work/2026-07-09-decision-store/design.md §B.1 (memory consolidation
18
+ * onto the kernel + FTS5). The retired flat JSONL store (`lib/memory-store.js`) is
19
+ * imported once into kernel_memories on first use, then never written again.
20
+ *
21
+ * Hard rule: `remember`/`recall` must NEVER hang or fail. Under `graphiti`, the
22
+ * emit is FIRE-AND-FORGET with a HARD FALLBACK to the local kernel store on any
23
+ * error/timeout — a down sidecar can never strand a note. The local kernel write
24
+ * is the floor and always happens. Strict validation (`assertMemoryConfigValid`)
25
+ * is a separate, explicit gate for tooling (e.g. `forge doctor`).
26
+ */
27
+
28
+ const crypto = require('node:crypto');
29
+ const fs = require('node:fs');
30
+ const path = require('node:path');
31
+ const projectMemory = require('../project-memory');
32
+
33
+ /** Supported PUBLIC backends, default first. */
34
+ const MEMORY_BACKENDS = ['local', 'graphiti'];
35
+ const DEFAULT_MEMORY_BACKEND = 'local';
36
+ const ENV_VAR = 'FORGE_MEMORY_BACKEND';
37
+
38
+ /** Default recall cap — newest-N, so `recall` with no query never dumps the whole store. */
39
+ const DEFAULT_RECALL_LIMIT = 20;
40
+ /** sourceAgent stamped on CLI `remember` notes (distinct from insights-written rows). */
41
+ const REMEMBER_SOURCE_AGENT = 'forge remember';
42
+ /** sourceAgent stamped on notes imported once from the retired JSONL store. */
43
+ const IMPORT_SOURCE_AGENT = 'forge remember (imported)';
44
+ /**
45
+ * The source_agents that are human `remember` notes — the scope of the DEFAULT (no-query)
46
+ * `recall` view, so machine/insights records never pollute or miscount the plain listing.
47
+ * A query, or `--all`, still reaches every stored memory.
48
+ */
49
+ const HUMAN_MEMORY_AGENTS = [REMEMBER_SOURCE_AGENT, IMPORT_SOURCE_AGENT];
50
+ /** The retired flat JSONL store, imported once into kernel_memories, then renamed. */
51
+ const LEGACY_JSONL_RELATIVE = ['.forge', 'memory', 'notes.jsonl'];
52
+
53
+ /**
54
+ * Safely read the `memory` block from `<projectRoot>/.forge/config.yaml`.
55
+ * Never throws — a missing or malformed file resolves to `{}` so the default
56
+ * (local) path is byte-identical to shipping no config at all.
57
+ *
58
+ * @param {string|undefined} projectRoot
59
+ * @returns {object} The parsed `memory` object, or `{}`.
60
+ */
61
+ function readMemoryConfig(projectRoot) {
62
+ if (!projectRoot) return {};
63
+
64
+ const fs = require('node:fs');
65
+ const path = require('node:path');
66
+ const configPath = path.join(projectRoot, '.forge', 'config.yaml');
67
+ if (!fs.existsSync(configPath)) return {};
68
+
69
+ let parsed;
70
+ try {
71
+ // Lazy-require keeps the default no-config path free of the YAML parser.
72
+ const YAML = require('yaml');
73
+ parsed = YAML.parse(fs.readFileSync(configPath, 'utf8'));
74
+ } catch {
75
+ return {};
76
+ }
77
+
78
+ if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) return {};
79
+ const memory = parsed.memory;
80
+ if (!memory || typeof memory !== 'object' || Array.isArray(memory)) return {};
81
+ return memory;
82
+ }
83
+
84
+ /**
85
+ * Gather the raw backend signal by precedence (deps > env > config), WITHOUT
86
+ * validation. Returns `{ value, source }` or `{ value: null, source: null }`.
87
+ */
88
+ function collectBackendSignal({ deps = {}, env = process.env, projectRoot, config } = {}) {
89
+ if (typeof deps.memoryBackend === 'string' && deps.memoryBackend.trim()) {
90
+ return { value: deps.memoryBackend.trim(), source: 'deps' };
91
+ }
92
+
93
+ const envValue = env && env[ENV_VAR];
94
+ if (typeof envValue === 'string' && envValue.trim()) {
95
+ return { value: envValue.trim(), source: 'env' };
96
+ }
97
+
98
+ const memory = config || readMemoryConfig(projectRoot);
99
+ const configValue = memory && memory.backend;
100
+ if (typeof configValue === 'string' && configValue.trim()) {
101
+ return { value: configValue.trim(), source: 'config' };
102
+ }
103
+
104
+ return { value: null, source: null };
105
+ }
106
+
107
+ /**
108
+ * Resolve the active memory backend by precedence:
109
+ * deps.memoryBackend > FORGE_MEMORY_BACKEND env > .forge/config.yaml > 'local'.
110
+ *
111
+ * An UNKNOWN value (from any source) warns and falls back to `local` so a typo
112
+ * — or a legacy `kernel` value — can never break `remember`/`recall`. Use
113
+ * `assertMemoryConfigValid` when you need a hard error instead.
114
+ *
115
+ * @param {object} [options]
116
+ * @returns {'local'|'graphiti'}
117
+ */
118
+ function resolveMemoryBackend({
119
+ deps = {},
120
+ env = process.env,
121
+ projectRoot,
122
+ config,
123
+ warn = console.warn,
124
+ } = {}) {
125
+ const { value, source } = collectBackendSignal({ deps, env, projectRoot, config });
126
+ if (!value) return DEFAULT_MEMORY_BACKEND;
127
+
128
+ const normalized = value.toLowerCase();
129
+ if (MEMORY_BACKENDS.includes(normalized)) return normalized;
130
+
131
+ warn(
132
+ `Unknown memory backend "${value}" from ${source}; `
133
+ + `falling back to "${DEFAULT_MEMORY_BACKEND}". Valid backends: ${MEMORY_BACKENDS.join(', ')}.`,
134
+ );
135
+ return DEFAULT_MEMORY_BACKEND;
136
+ }
137
+
138
+ /**
139
+ * Strict validation for the resolved backend. Unlike `resolveMemoryBackend`
140
+ * (which soft-falls-back), this THROWS a clear, actionable error when the
141
+ * selection is inconsistent. Used by tooling (e.g. `forge doctor`).
142
+ *
143
+ * @param {object} [options]
144
+ * @returns {{ backend: string, graphiti: object|null }}
145
+ */
146
+ function assertMemoryConfigValid({ deps = {}, env = process.env, projectRoot, config } = {}) {
147
+ const memory = config || readMemoryConfig(projectRoot);
148
+ const backend = resolveMemoryBackend({ deps, env, projectRoot, config: memory, warn: () => {} });
149
+
150
+ if (backend !== 'graphiti') {
151
+ return { backend, graphiti: null };
152
+ }
153
+
154
+ const graphiti = memory && memory.graphiti;
155
+ const hasServerPath = graphiti
156
+ && typeof graphiti.mcpServerPath === 'string'
157
+ && graphiti.mcpServerPath.trim() !== '';
158
+ if (!hasServerPath) {
159
+ throw new Error(
160
+ 'memory.backend is "graphiti" but memory.graphiti.mcpServerPath is not set. '
161
+ + 'Configure the Graphiti MCP server path (the checkout\'s mcp_server directory) '
162
+ + 'in .forge/config.yaml. See docs/guides/memory-backends.md. '
163
+ + 'The local store stays the default and the safety floor when unset.',
164
+ );
165
+ }
166
+ return { backend, graphiti };
167
+ }
168
+
169
+ /**
170
+ * Best-effort, fire-and-forget emit of an episode to the graph backend. This is
171
+ * a SEAM: the actual Graphiti MCP client lands in a fast-follow PR. It NEVER
172
+ * throws and NEVER blocks the caller — any error/timeout is swallowed so the
173
+ * local floor write is the only thing that can affect `remember`'s result.
174
+ *
175
+ * CONTRACT for the fast-follow emitter (MUST hold — safe today only because no
176
+ * emitter is constructed): the emitter MUST NOT keep the Node event loop alive
177
+ * or delay CLI exit. Any spawned process/socket/timer it creates MUST be
178
+ * `child.unref()`'d (or otherwise detached / left with no lingering handle), and
179
+ * any network/RPC call MUST be bounded by its OWN timeout so a hung sidecar can
180
+ * never stall `forge remember`. This function does NOT await the emit, so a
181
+ * lingering handle inside `emit()` would be the ONLY way to break the
182
+ * never-hang guarantee — the emitter, not this seam, owns preventing that.
183
+ *
184
+ * @param {object} [emitter] - optional `{ emit(entry) }` injected by callers.
185
+ * @param {object} entry - the persisted local entry.
186
+ */
187
+ function fireAndForgetGraphitiEmit(emitter, entry) {
188
+ if (!emitter || typeof emitter.emit !== 'function') return;
189
+ try {
190
+ // Do not await: fire-and-forget. If it returns a promise, swallow rejection.
191
+ const maybePromise = emitter.emit(entry);
192
+ if (maybePromise && typeof maybePromise.then === 'function') {
193
+ maybePromise.then(() => {}, () => {});
194
+ }
195
+ } catch {
196
+ // Hard fallback: the local write already succeeded. Never surface emit errors.
197
+ }
198
+ }
199
+
200
+ /**
201
+ * Render a non-string memory value (e.g. an insights skill record) as a compact, READABLE
202
+ * one-liner rather than a raw JSON blob. Flattens the top level: primitive fields become
203
+ * `key: value`; nested fields fall back to compact JSON.
204
+ *
205
+ * @param {*} value
206
+ * @returns {string}
207
+ */
208
+ function renderStructuredValue(value) {
209
+ if (value === null || value === undefined) return String(value);
210
+ if (typeof value !== 'object') return String(value);
211
+ if (Array.isArray(value)) {
212
+ return value.map(item => (item !== null && typeof item === 'object' ? JSON.stringify(item) : String(item))).join(', ');
213
+ }
214
+ return Object.entries(value)
215
+ .map(([key, val]) => (val !== null && typeof val === 'object' ? `${key}: ${JSON.stringify(val)}` : `${key}: ${val}`))
216
+ .join(' · ');
217
+ }
218
+
219
+ /**
220
+ * Map a kernel_memories entry to the CLI note shape `remember`/`recall` render. A string
221
+ * value IS the note text; a structured value (an insights/typed record) is rendered readably
222
+ * and flagged `machine` with its `sourceAgent` so the CLI can LABEL it rather than mislabel a
223
+ * raw JSON blob as a plain note.
224
+ *
225
+ * @param {object} entry - a kernel_memories entry ({ key, value, sourceAgent, timestamp, tags }).
226
+ * @returns {{ id: string, note: string, sourceAgent: string, machine: boolean, timestamp: string, tags: string[] }}
227
+ */
228
+ function toNote(entry) {
229
+ if (!entry) return null;
230
+ const isString = typeof entry.value === 'string';
231
+ return {
232
+ id: entry.key,
233
+ note: isString ? entry.value : renderStructuredValue(entry.value),
234
+ sourceAgent: typeof entry.sourceAgent === 'string' ? entry.sourceAgent : '',
235
+ machine: !isString,
236
+ timestamp: typeof entry.timestamp === 'string' ? entry.timestamp : '',
237
+ tags: Array.isArray(entry.tags) ? entry.tags : [],
238
+ };
239
+ }
240
+
241
+ function legacyJsonlPath(projectRoot) {
242
+ return path.join(projectRoot, ...LEGACY_JSONL_RELATIVE);
243
+ }
244
+
245
+ // A STABLE key for a legacy record that lacks an `id`, derived from its content — so a
246
+ // re-run (or a failed rename) upserts the same row instead of double-inserting under a
247
+ // fresh random UUID.
248
+ function legacyContentKey(parsed) {
249
+ const basis = JSON.stringify({
250
+ note: parsed.note,
251
+ timestamp: typeof parsed.timestamp === 'string' ? parsed.timestamp : '',
252
+ tags: Array.isArray(parsed.tags) ? parsed.tags.filter(tag => typeof tag === 'string') : [],
253
+ });
254
+ return `import:${crypto.createHash('sha256').update(basis).digest('hex').slice(0, 32)}`;
255
+ }
256
+
257
+ /**
258
+ * One-time import of the retired flat JSONL store into kernel_memories. Idempotent (keys
259
+ * are the original note ids, so a re-run upserts) and best-effort (a malformed record or a
260
+ * write error can never break `remember`/`recall`). After a pass the file is renamed so the
261
+ * import runs at most once and JSONL is never read or written again.
262
+ *
263
+ * @param {string} projectRoot
264
+ * @param {object} [options] - forwarded to `projectMemory.write` (e.g. an injected store).
265
+ */
266
+ function migrateJsonlNotesOnce(projectRoot, options = {}) {
267
+ if (!projectRoot) return;
268
+ const storePath = legacyJsonlPath(projectRoot);
269
+ let raw;
270
+ try {
271
+ if (!fs.existsSync(storePath)) return;
272
+ raw = fs.readFileSync(storePath, 'utf8');
273
+ } catch {
274
+ return;
275
+ }
276
+
277
+ for (const line of raw.split(/\r?\n/)) {
278
+ const trimmed = line.trim();
279
+ if (!trimmed) continue;
280
+ let parsed;
281
+ try {
282
+ parsed = JSON.parse(trimmed);
283
+ } catch {
284
+ continue; // Skip an unparseable legacy line — one bad record can't block the import.
285
+ }
286
+ if (!parsed || typeof parsed !== 'object' || typeof parsed.note !== 'string' || parsed.note.trim() === '') {
287
+ continue;
288
+ }
289
+ const entry = {
290
+ // A record with a stable id keys off it; one without keys off its content hash, so a
291
+ // re-import can never double-insert the same note.
292
+ key: typeof parsed.id === 'string' && parsed.id ? parsed.id : legacyContentKey(parsed),
293
+ value: parsed.note,
294
+ sourceAgent: IMPORT_SOURCE_AGENT,
295
+ tags: Array.isArray(parsed.tags) ? parsed.tags.filter(tag => typeof tag === 'string') : [],
296
+ };
297
+ if (typeof parsed.timestamp === 'string' && parsed.timestamp && !Number.isNaN(Date.parse(parsed.timestamp))) {
298
+ entry.timestamp = parsed.timestamp;
299
+ }
300
+ try {
301
+ projectMemory.write(projectRoot, entry, options);
302
+ } catch {
303
+ // Skip an unwritable legacy record; never break remember/recall on import.
304
+ }
305
+ }
306
+
307
+ try {
308
+ fs.renameSync(storePath, `${storePath}.migrated`);
309
+ } catch {
310
+ // Best-effort: the import is idempotent by key, so a failed rename re-imports safely.
311
+ }
312
+ }
313
+
314
+ /**
315
+ * Append a note through kernel_memories — the local floor for EVERY backend, always written
316
+ * first so a note is durably captured. `graphiti` additionally fires a best-effort,
317
+ * non-blocking emit toward the graph backend. Imports any retired JSONL store on first use.
318
+ *
319
+ * @param {string} projectRoot
320
+ * @param {string} note
321
+ * @param {object} [options] - { tags, deps, env, config, graphitiEmitter, store }
322
+ * @returns {{ id: string, note: string, timestamp: string, tags: string[] }}
323
+ */
324
+ function append(projectRoot, note, options = {}) {
325
+ migrateJsonlNotesOnce(projectRoot, options);
326
+ const backend = resolveMemoryBackend({ ...options, projectRoot });
327
+ const written = projectMemory.write(projectRoot, {
328
+ key: crypto.randomUUID(),
329
+ value: note,
330
+ sourceAgent: REMEMBER_SOURCE_AGENT,
331
+ tags: Array.isArray(options.tags) ? options.tags : [],
332
+ }, options);
333
+ const entry = toNote(written);
334
+ if (backend === 'graphiti') {
335
+ fireAndForgetGraphitiEmit(options.graphitiEmitter, entry);
336
+ }
337
+ return entry;
338
+ }
339
+
340
+ /**
341
+ * Read notes back through the FTS-backed kernel layer. WITH a query: BM25 token-AND top-N.
342
+ * WITHOUT a query: the newest `limit` entries plus the total count (never a bare full dump).
343
+ * Imports any retired JSONL store on first use.
344
+ *
345
+ * A query searches the WHOLE store (human notes + insights/typed records), so anything the
346
+ * kernel knows is recallable. The default no-query view is scoped to human `remember` notes
347
+ * so machine/insights records never pollute or miscount the plain listing; `selection.all`
348
+ * widens it to every stored memory.
349
+ *
350
+ * @param {string} projectRoot
351
+ * @param {object} [selection] - { query, limit, all }
352
+ * @param {object} [options] - forwarded to the project-memory read paths (e.g. a store).
353
+ * @returns {{ notes: object[], total: number, capped: boolean, query: string, limit: number, scope: string }}
354
+ */
355
+ function recall(projectRoot, selection = {}, options = {}) {
356
+ migrateJsonlNotesOnce(projectRoot, options);
357
+ const requested = selection.limit;
358
+ const limit = Number.isInteger(requested) && requested > 0 ? requested : DEFAULT_RECALL_LIMIT;
359
+ const query = String(selection.query || '').trim();
360
+ const includeAll = Boolean(selection.all);
361
+
362
+ if (query) {
363
+ const notes = projectMemory.searchRanked(projectRoot, query, limit, options).map(toNote);
364
+ // BM25 returns at most `limit`; a full result set signals there may be more.
365
+ return { notes, total: notes.length, capped: notes.length >= limit, query, limit, scope: 'all' };
366
+ }
367
+
368
+ const agents = includeAll ? undefined : HUMAN_MEMORY_AGENTS;
369
+ const readOptions = { ...options, agents };
370
+ const notes = projectMemory.recent(projectRoot, limit, readOptions).map(toNote);
371
+ const total = projectMemory.count(projectRoot, readOptions);
372
+ return { notes, total, capped: total > notes.length, query: '', limit, scope: includeAll ? 'all' : 'remembered' };
373
+ }
374
+
375
+ module.exports = {
376
+ MEMORY_BACKENDS,
377
+ DEFAULT_MEMORY_BACKEND,
378
+ DEFAULT_RECALL_LIMIT,
379
+ ENV_VAR,
380
+ readMemoryConfig,
381
+ resolveMemoryBackend,
382
+ assertMemoryConfigValid,
383
+ migrateJsonlNotesOnce,
384
+ toNote,
385
+ append,
386
+ recall,
387
+ };
@@ -0,0 +1,102 @@
1
+ const projectMemory = require('../project-memory');
2
+
3
+ const CATEGORIES = new Set([
4
+ 'decisions',
5
+ 'episodes',
6
+ 'skills',
7
+ 'state',
8
+ 'issues',
9
+ 'audit',
10
+ 'preferences',
11
+ ]);
12
+
13
+ function assertCategory(category) {
14
+ if (!CATEGORIES.has(category)) {
15
+ throw new Error(`Unknown memory category: ${category}`);
16
+ }
17
+ }
18
+
19
+ function assertProvenance(provenance) {
20
+ if (!provenance || typeof provenance !== 'object') {
21
+ throw new TypeError('typed memory provenance is required');
22
+ }
23
+ for (const field of ['actor', 'reason', 'source']) {
24
+ if (typeof provenance[field] !== 'string' || provenance[field].trim() === '') {
25
+ throw new TypeError(`typed memory provenance.${field} is required`);
26
+ }
27
+ }
28
+ }
29
+
30
+ function keyFor(category, key) {
31
+ if (typeof key !== 'string' || key.trim() === '') {
32
+ throw new TypeError('typed memory key is required');
33
+ }
34
+ return `${category}:${key.trim()}`;
35
+ }
36
+
37
+ function adapter(options = {}) {
38
+ return options.memory ?? projectMemory;
39
+ }
40
+
41
+ function stringArrayOption(value, fieldName) {
42
+ if (value === undefined) return undefined;
43
+ const values = Array.isArray(value) ? value : [value];
44
+ if (values.some(item => typeof item !== 'string')) {
45
+ throw new TypeError(`typed memory ${fieldName} must contain only strings`);
46
+ }
47
+ return values.map(item => item.trim()).filter(Boolean);
48
+ }
49
+
50
+ function writeTyped(projectRoot, category, key, data, options = {}) {
51
+ assertCategory(category);
52
+ assertProvenance(options.provenance);
53
+
54
+ const provenance = {
55
+ actor: options.provenance.actor.trim(),
56
+ reason: options.provenance.reason.trim(),
57
+ source: options.provenance.source.trim(),
58
+ };
59
+
60
+ return adapter(options).write(projectRoot, {
61
+ key: keyFor(category, key),
62
+ value: {
63
+ category,
64
+ data,
65
+ provenance,
66
+ },
67
+ sourceAgent: provenance.actor,
68
+ tags: [category, ...(stringArrayOption(options.tags, 'tags') ?? [])],
69
+ beadsRefs: stringArrayOption(options.beadsRefs, 'beadsRefs'),
70
+ }, options);
71
+ }
72
+
73
+ function readTyped(projectRoot, category, key, options = {}) {
74
+ assertCategory(category);
75
+ return adapter(options).read(projectRoot, keyFor(category, key), options);
76
+ }
77
+
78
+ function searchTyped(projectRoot, category, query, options = {}) {
79
+ assertCategory(category);
80
+ const prefix = `${category}:`;
81
+ const results = adapter(options).search(projectRoot, `${category} ${query ?? ''}`.trim(), options) ?? [];
82
+ if (!Array.isArray(results)) return [];
83
+ return results.filter(entry => typeof entry?.key === 'string' && entry.key.startsWith(prefix));
84
+ }
85
+
86
+ function categoryWriter(category) {
87
+ return (projectRoot, key, data, options = {}) => writeTyped(projectRoot, category, key, data, options);
88
+ }
89
+
90
+ module.exports = {
91
+ CATEGORIES: [...CATEGORIES],
92
+ writeTyped,
93
+ readTyped,
94
+ searchTyped,
95
+ writeDecision: categoryWriter('decisions'),
96
+ writeEpisode: categoryWriter('episodes'),
97
+ writeSkill: categoryWriter('skills'),
98
+ writeState: categoryWriter('state'),
99
+ writeIssue: categoryWriter('issues'),
100
+ writeAudit: categoryWriter('audit'),
101
+ writePreference: categoryWriter('preferences'),
102
+ };
@@ -0,0 +1,195 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * @module memory-digest
5
+ *
6
+ * Builds the BOUNDED, token-capped memory digest that Forge PUSHES to an agent at
7
+ * session start (the `memory-inject` context intent in the hook contract). This is
8
+ * the missing "push" half of Forge memory: today an agent only sees remembered
9
+ * notes if it TYPES `forge recall`, so memory is effectively orphaned.
10
+ *
11
+ * Two layers, kept separate for testability:
12
+ * - collectDigestData(projectRoot, opts) — BEST-EFFORT fetch (each source wrapped;
13
+ * a failure yields [] for that source). Fetchers are injectable so tests never
14
+ * touch a real DB. Async (issue reads are async).
15
+ * - buildMemoryDigest(data, { budgetTokens }) — PURE formatting + token-capping via
16
+ * orientation's applyBudget. Empty data → empty digest (the caller then injects
17
+ * nothing). Never exceeds the budget.
18
+ *
19
+ * The digest is a small NUDGE, not a manual: the default budget is deliberately tiny.
20
+ */
21
+
22
+ const { applyBudget, buildSection, estimateTokens } = require('./orientation');
23
+ const { fenceUntrusted } = require('./untrusted-content');
24
+ const { collectInbox, inboxSection } = require('./inbox');
25
+
26
+ const DEFAULT_DIGEST_BUDGET_TOKENS = 400;
27
+ const DEFAULT_NOTE_LIMIT = 5;
28
+ const DEFAULT_ISSUE_LIMIT = 5;
29
+ const DIGEST_HEADER = 'Forge memory (auto-injected at session start):';
30
+
31
+ /** Run an async producer, returning `fallback` on any throw/rejection (never propagates). */
32
+ async function safe(producer, fallback) {
33
+ try {
34
+ const value = await producer();
35
+ return value === undefined || value === null ? fallback : value;
36
+ } catch {
37
+ return fallback;
38
+ }
39
+ }
40
+
41
+ /** Default note fetch: newest remembered notes via the kernel-backed memory router. */
42
+ function defaultFetchNotes(projectRoot, opts = {}) {
43
+ const memoryRouter = require('./memory/router');
44
+ const result = memoryRouter.recall(projectRoot, { limit: opts.noteLimit || DEFAULT_NOTE_LIMIT });
45
+ return Array.isArray(result && result.notes) ? result.notes : [];
46
+ }
47
+
48
+ /** Pull an issues array out of a runIssueOperation result, defensively (shape varies). */
49
+ function extractIssues(result) {
50
+ let payload = result && result.data;
51
+ if (!payload && result && typeof result.output === 'string') {
52
+ try { payload = JSON.parse(result.output); } catch { return []; }
53
+ }
54
+ if (Array.isArray(payload)) return payload;
55
+ if (payload && Array.isArray(payload.issues)) return payload.issues;
56
+ return [];
57
+ }
58
+
59
+ /**
60
+ * Default issue fetch for a status kind ('ready' | 'in_progress'). Best-effort.
61
+ * The CLI `--limit` is NOT trusted (`forge issue ready --json --limit 2` empirically
62
+ * returns the whole set), so the result is HARD-CAPPED with `.slice(0, limit)` — else
63
+ * the digest dumps every ready issue and applyBudget truncates the claimed tail away.
64
+ * `opts.runIssueOperation` is injectable so tests exercise the cap deterministically.
65
+ */
66
+ async function defaultFetchIssues(projectRoot, kind, opts = {}) {
67
+ const runIssueOperation = opts.runIssueOperation || require('./forge-issues').runIssueOperation;
68
+ const limit = opts.issueLimit || DEFAULT_ISSUE_LIMIT;
69
+ const [operation, args] = kind === 'ready'
70
+ ? ['ready', ['--json', '--limit', String(limit)]]
71
+ : ['list', ['--status', 'in_progress', '--json', '--limit', String(limit)]];
72
+ const result = await runIssueOperation(operation, args, projectRoot);
73
+ return extractIssues(result).slice(0, limit);
74
+ }
75
+
76
+ /** Default inbox fetch: pending targeted dashboard instruction comments (fail-open). */
77
+ function defaultFetchInbox(projectRoot, opts = {}) {
78
+ return collectInbox(projectRoot, opts);
79
+ }
80
+
81
+ /**
82
+ * Best-effort gather of the digest inputs. Each source degrades to [] independently.
83
+ * @param {string} projectRoot
84
+ * @param {object} [opts] - { fetchNotes, fetchIssues, fetchInbox, noteLimit, issueLimit }
85
+ * @returns {Promise<{ notes: object[], ready: object[], claimed: object[], inbox: object[] }>}
86
+ */
87
+ async function collectDigestData(projectRoot, opts = {}) {
88
+ const fetchNotes = opts.fetchNotes || defaultFetchNotes;
89
+ const fetchIssues = opts.fetchIssues || defaultFetchIssues;
90
+ const fetchInbox = opts.fetchInbox || defaultFetchInbox;
91
+ const notes = await safe(() => fetchNotes(projectRoot, opts), []);
92
+ const ready = await safe(() => fetchIssues(projectRoot, 'ready', opts), []);
93
+ const claimed = await safe(() => fetchIssues(projectRoot, 'in_progress', opts), []);
94
+ const inbox = await safe(() => fetchInbox(projectRoot, opts), []);
95
+ return {
96
+ notes: Array.isArray(notes) ? notes : [],
97
+ ready: Array.isArray(ready) ? ready : [],
98
+ claimed: Array.isArray(claimed) ? claimed : [],
99
+ inbox: Array.isArray(inbox) ? inbox : [],
100
+ };
101
+ }
102
+
103
+ /** `- [date ]note` for a recall note. */
104
+ function formatNoteLine(note) {
105
+ const date = typeof note.timestamp === 'string' && note.timestamp ? `${note.timestamp.slice(0, 10)} ` : '';
106
+ return `- ${date}${note.note}`;
107
+ }
108
+
109
+ /** `- [label] title` for an issue row (title/id defensively resolved). */
110
+ function formatIssueLine(label, issue) {
111
+ const title = (issue && (issue.title || issue.id)) || 'untitled';
112
+ return `- [${label}] ${title}`;
113
+ }
114
+
115
+ /** Build the notes section, or null when there are no notes. */
116
+ function notesSection(notes) {
117
+ if (!notes.length) return null;
118
+ return buildSection({
119
+ id: 'digest_notes',
120
+ title: 'Remembered notes',
121
+ content: notes.map(formatNoteLine).join('\n'),
122
+ priority: 10,
123
+ preserve: false,
124
+ // Untrusted: a planted note is DATA, not instructions. Fenced after truncation.
125
+ untrustedSource: 'memory',
126
+ });
127
+ }
128
+
129
+ /**
130
+ * Build the open-issues section, or null when both are empty. CLAIMED lines come FIRST
131
+ * so that when applyBudget truncates the tail, it is the (less critical) ready list that
132
+ * is cut — the agent's own in-progress work must never be the vanished tail.
133
+ */
134
+ function issuesSection(ready, claimed) {
135
+ const lines = [
136
+ ...claimed.map(issue => formatIssueLine('claimed', issue)),
137
+ ...ready.map(issue => formatIssueLine('ready', issue)),
138
+ ];
139
+ if (!lines.length) return null;
140
+ return buildSection({
141
+ id: 'digest_issues',
142
+ title: 'Open issues',
143
+ content: lines.join('\n'),
144
+ priority: 20,
145
+ preserve: false,
146
+ // Untrusted: an issue title is attacker-influenceable. Fenced after truncation.
147
+ untrustedSource: 'issue-titles',
148
+ });
149
+ }
150
+
151
+ /**
152
+ * Assemble the bounded digest text. PURE. Never exceeds `budgetTokens` (delegated to
153
+ * applyBudget). Empty inputs → { text: '', empty: true } so the caller injects nothing.
154
+ *
155
+ * @param {{ notes?: object[], ready?: object[], claimed?: object[] }} [data]
156
+ * @param {object} [options] - { budgetTokens }
157
+ * @returns {{ text: string, empty: boolean, tokens: number }}
158
+ */
159
+ function buildMemoryDigest(data = {}, options = {}) {
160
+ const notes = Array.isArray(data.notes) ? data.notes : [];
161
+ const ready = Array.isArray(data.ready) ? data.ready : [];
162
+ const claimed = Array.isArray(data.claimed) ? data.claimed : [];
163
+ const inbox = Array.isArray(data.inbox) ? data.inbox : [];
164
+
165
+ // Inbox (priority 5) is a THIRD section beside notes + issues; a fresh human directive
166
+ // outranks stale notes (10) and the agent's own issue list (20) under budget pressure.
167
+ const sections = [inboxSection(inbox), notesSection(notes), issuesSection(ready, claimed)].filter(Boolean);
168
+ if (!sections.length) return { text: '', empty: true, tokens: 0 };
169
+
170
+ const budgetTokens = options.budgetTokens || DEFAULT_DIGEST_BUDGET_TOKENS;
171
+ const budgeted = applyBudget(sections, budgetTokens);
172
+ const body = budgeted.sections
173
+ .filter(section => section.content)
174
+ // Fence AFTER applyBudget truncates, so the ⟦END UNTRUSTED⟧ close marker always
175
+ // survives (fencing before truncation would let the budget cut the terminator and
176
+ // leave an unclosed fence a payload could exploit). Provenance-labelled per section.
177
+ .map(section => `${section.title}:\n${fenceUntrusted(section.content, { source: section.untrustedSource })}`)
178
+ .join('\n\n');
179
+ if (!body) return { text: '', empty: true, tokens: 0 };
180
+
181
+ const text = `${DIGEST_HEADER}\n\n${body}`;
182
+ return { text, empty: false, tokens: estimateTokens(text) };
183
+ }
184
+
185
+ module.exports = {
186
+ DEFAULT_DIGEST_BUDGET_TOKENS,
187
+ DIGEST_HEADER,
188
+ buildMemoryDigest,
189
+ collectDigestData,
190
+ extractIssues,
191
+ // exported for focused reuse / tests
192
+ defaultFetchNotes,
193
+ defaultFetchIssues,
194
+ defaultFetchInbox,
195
+ };