forge-workflow 0.0.9 → 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 (479) 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 +151 -61
  8. package/CHANGELOG.md +681 -0
  9. package/CLAUDE.md +9 -106
  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 +466 -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/{TOOLCHAIN.md → forge/TOOLCHAIN.md} +56 -47
  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/reference/TOOLCHAIN.md +658 -0
  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 +225 -28
  81. package/lib/beads-sync-scaffold.js +36 -107
  82. package/lib/codex-skills.js +51 -1
  83. package/lib/commands/_issue.js +744 -70
  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 +66 -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 +22 -2
  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 +851 -979
  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 +329 -11
  140. package/lib/commands/sync.js +34 -46
  141. package/lib/commands/team.js +15 -2
  142. package/lib/commands/test.js +58 -7
  143. package/lib/commands/update.js +2 -2
  144. package/lib/commands/upgrade.js +47 -0
  145. package/lib/commands/validate.js +56 -25
  146. package/lib/commands/worktree.js +308 -128
  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 +184 -0
  151. package/lib/deprecated-sync-cleanup.js +362 -0
  152. package/lib/detect-agent.js +2 -28
  153. package/lib/detect-worktree.js +42 -17
  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 +697 -0
  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/issue-sync/authority.js +100 -0
  174. package/lib/issue-sync/github-pull.js +184 -0
  175. package/lib/issue-sync/import-primitives.js +98 -0
  176. package/lib/issue-sync/legacy-link-bridge.js +436 -0
  177. package/lib/issue-sync/link-store.js +292 -0
  178. package/lib/issue-sync/project-github.js +123 -0
  179. package/lib/issue-sync/reconcile.js +195 -0
  180. package/lib/issue-sync/schema.js +126 -0
  181. package/lib/kernel/backing-issue.js +305 -0
  182. package/lib/kernel/broker.js +1218 -0
  183. package/lib/kernel/cli-broker-factory.js +130 -0
  184. package/lib/kernel/conflict-signal.js +82 -0
  185. package/lib/kernel/evaluators.js +195 -0
  186. package/lib/kernel/fs-class.js +495 -0
  187. package/lib/kernel/issue-command-contract.js +559 -0
  188. package/lib/kernel/issue-id-resolver.js +186 -0
  189. package/lib/kernel/lease-enforcer.js +158 -0
  190. package/lib/kernel/migrations.js +333 -0
  191. package/lib/kernel/planning-buckets-schema.js +109 -0
  192. package/lib/kernel/projection-jsonl-writer.js +450 -0
  193. package/lib/kernel/readiness-model.js +329 -0
  194. package/lib/kernel/schema.js +356 -0
  195. package/lib/kernel/sqlite-driver.js +2504 -0
  196. package/lib/kernel/taxonomy-validator.js +394 -0
  197. package/lib/lefthook-check.js +8 -4
  198. package/lib/lefthook-wiring.js +413 -0
  199. package/lib/mcp-config-renderer.js +288 -0
  200. package/lib/memory/graphiti-mcp.js +106 -0
  201. package/lib/memory/router.js +387 -0
  202. package/lib/memory/typed-api.js +102 -0
  203. package/lib/memory-digest.js +195 -0
  204. package/lib/merge-rules.js +395 -0
  205. package/lib/migrate-dry-run.js +466 -0
  206. package/lib/orientation.js +863 -0
  207. package/lib/package-manager-remediation.js +103 -0
  208. package/lib/package-root.js +381 -0
  209. package/lib/patch-intent.js +890 -0
  210. package/lib/plugin-catalog.js +3 -4
  211. package/lib/plugin-manager.js +0 -5
  212. package/lib/pr-bundle.js +186 -0
  213. package/lib/pr-monitor/differ.js +195 -0
  214. package/lib/pr-monitor/events.js +0 -0
  215. package/lib/pr-monitor/gather.js +124 -0
  216. package/lib/pr-monitor/journal.js +299 -0
  217. package/lib/pr-monitor/monitor.js +146 -0
  218. package/lib/pr-monitor/render-sticky.js +157 -0
  219. package/lib/pr-monitor/watch-lifecycle.js +95 -0
  220. package/lib/pr-monitor/watch.js +247 -0
  221. package/lib/pr-pull.js +1273 -0
  222. package/lib/pr-shepherd.js +494 -0
  223. package/lib/pr-state-validator.js +59 -0
  224. package/lib/preflight/gates.js +237 -0
  225. package/lib/preflight/runner.js +116 -0
  226. package/lib/project-discovery.js +0 -53
  227. package/lib/project-memory.js +166 -0
  228. package/lib/protected-path-manifest.js +281 -0
  229. package/lib/protected-state-surfaces.js +387 -0
  230. package/lib/release-readiness.js +2089 -0
  231. package/lib/reset.js +59 -45
  232. package/lib/review-adapter.js +68 -0
  233. package/lib/rules-sync.js +260 -0
  234. package/lib/runtime-health.js +332 -23
  235. package/lib/safety-config-renderer.js +268 -0
  236. package/lib/setup-action-log.js +1 -7
  237. package/lib/setup.js +27 -65
  238. package/lib/shell-utils.js +76 -6
  239. package/lib/skills-sync.js +330 -0
  240. package/lib/smart-status/conflicts.js +205 -0
  241. package/lib/smart-status/scoring.js +191 -0
  242. package/lib/status/beads-snapshot.js +145 -0
  243. package/lib/status/presenter.js +216 -0
  244. package/lib/status/snapshot.js +186 -0
  245. package/lib/sync-backend.js +202 -0
  246. package/lib/untrusted-content.js +52 -0
  247. package/lib/upgrade-safety.js +199 -0
  248. package/lib/workflow/enforce-stage.js +298 -47
  249. package/lib/workflow/stage-transition.js +115 -0
  250. package/lib/workflow/stages.js +30 -6
  251. package/lib/workflow/state-manager.js +159 -14
  252. package/lib/workflow/state.js +23 -1
  253. package/lib/workflow-profiles.js +17 -5
  254. package/package.json +46 -36
  255. package/rules/documentation.md +19 -0
  256. package/rules/kernel-tracking.md +26 -0
  257. package/rules/security.md +22 -0
  258. package/rules/tdd.md +20 -0
  259. package/rules/workflow.md +27 -0
  260. package/scripts/auto-backing-issue.js +47 -0
  261. package/scripts/beads-context.sh +165 -22
  262. package/scripts/beads-migrate-to-dolt.sh +7 -0
  263. package/scripts/beads-upgrade-smoke.sh +284 -0
  264. package/scripts/behavioral-judge.sh +115 -11
  265. package/scripts/benchmark.js +349 -63
  266. package/scripts/bootstrap-windows-tools.sh +78 -0
  267. package/scripts/branch-protection.js +2 -3
  268. package/scripts/check-agents.js +34 -137
  269. package/scripts/commitlint.js +3 -1
  270. package/scripts/conflict-detect.sh +3 -0
  271. package/scripts/dep-guard-analyze.js +52 -17
  272. package/scripts/dep-guard-keyword-ripple.js +29 -0
  273. package/scripts/dep-guard-render-review.js +86 -0
  274. package/scripts/dep-guard.sh +64 -232
  275. package/scripts/file-index.sh +3 -0
  276. package/scripts/forge-team/lib/claim.sh +34 -18
  277. package/scripts/forge-team/lib/dashboard.sh +61 -86
  278. package/scripts/forge-team/lib/epic.sh +99 -263
  279. package/scripts/forge-team/lib/hooks.sh +26 -28
  280. package/scripts/forge-team/lib/identity.sh +4 -4
  281. package/scripts/forge-team/lib/sync-github.sh +144 -47
  282. package/scripts/forge-team/lib/verify.sh +93 -83
  283. package/scripts/forge-team/lib/workload.sh +41 -65
  284. package/scripts/forge-team/tests/claim.test.sh +25 -19
  285. package/scripts/forge-team/tests/dashboard.test.sh +31 -46
  286. package/scripts/forge-team/tests/epic.test.sh +52 -71
  287. package/scripts/forge-team/tests/hooks.test.sh +38 -50
  288. package/scripts/forge-team/tests/identity.test.sh +3 -3
  289. package/scripts/forge-team/tests/integration.test.sh +44 -66
  290. package/scripts/forge-team/tests/sync-github.test.sh +183 -79
  291. package/scripts/forge-team/tests/verify.test.sh +37 -46
  292. package/scripts/forge-team/tests/workflow-integration.test.sh +4 -4
  293. package/scripts/forge-team/tests/workload.test.sh +32 -66
  294. package/scripts/gen-command-manifest.js +153 -0
  295. package/scripts/gen-embedded-assets.mjs +129 -0
  296. package/scripts/install.ps1 +139 -0
  297. package/scripts/install.sh +268 -0
  298. package/scripts/lib/beads-migrate-to-dolt.mjs +503 -0
  299. package/scripts/lib/release-asset.mjs +84 -0
  300. package/scripts/parity-check.mjs +145 -0
  301. package/scripts/parity-check.test.mjs +58 -0
  302. package/scripts/pin-agentic-workflow-images.js +112 -0
  303. package/scripts/pr-coordinator.sh +3 -0
  304. package/scripts/preflight-sonar.eslint.config.mjs +44 -0
  305. package/scripts/preflight.sh +108 -0
  306. package/scripts/protected-state-check.js +104 -0
  307. package/scripts/smart-status-score.js +31 -0
  308. package/scripts/smart-status-sessions.js +51 -0
  309. package/scripts/smart-status.sh +117 -369
  310. package/scripts/spikes/config-race-bench.js +111 -0
  311. package/scripts/spikes/harness-capability-matrix.js +13 -0
  312. package/scripts/spikes/patch-anchor-stability-bench.js +125 -0
  313. package/scripts/spikes/protected-path-manifest.js +20 -0
  314. package/scripts/spikes/skill-auto-invoke-parity.js +292 -0
  315. package/scripts/sync-agent-skills.js +62 -0
  316. package/scripts/sync-agentic-workflow.js +48 -0
  317. package/scripts/sync-utils.sh +3 -0
  318. package/scripts/test-ci-shard.js +251 -0
  319. package/scripts/test-dashboard.js +188 -52
  320. package/scripts/test-full-suite.js +186 -0
  321. package/scripts/test-profile.js +278 -0
  322. package/scripts/test.js +302 -28
  323. package/scripts/validate.js +143 -0
  324. package/scripts/validate.sh +18 -1
  325. package/skills/claim-safety/SKILL.md +102 -0
  326. package/skills/claim-safety/evals/evals.json +46 -0
  327. package/{.github/prompts/dev.prompt.md → skills/dev/SKILL.md} +46 -52
  328. package/skills/dev/evals/evals.json +50 -0
  329. package/skills/hermes-forge/SKILL.md +185 -0
  330. package/skills/hermes-forge/evals/evals.json +46 -0
  331. package/skills/issue-basics/SKILL.md +111 -0
  332. package/skills/issue-basics/evals/evals.json +46 -0
  333. package/skills/kernel/SKILL.md +166 -0
  334. package/skills/kernel/evals/evals.json +50 -0
  335. package/skills/memory/SKILL.md +102 -0
  336. package/skills/parallel-deep-research/SKILL.md +14 -11
  337. package/skills/parallel-deep-research/evals/evals.json +11 -27
  338. package/{.github/prompts/plan.prompt.md → skills/plan/SKILL.md} +134 -159
  339. package/skills/plan/evals/evals.json +42 -0
  340. package/skills/research/SKILL.md +195 -0
  341. package/skills/research/evals/evals.json +42 -0
  342. package/{.github/prompts/review.prompt.md → skills/review/SKILL.md} +98 -62
  343. package/skills/review/evals/evals.json +42 -0
  344. package/skills/rollback/SKILL.md +110 -0
  345. package/skills/rollback/evals/evals.json +46 -0
  346. package/skills/rollback/references/methods.md +204 -0
  347. package/{.cursor/commands/rollback.md → skills/rollback/references/workflow-integration.md} +10 -284
  348. package/skills/shepherd/SKILL.md +66 -0
  349. package/skills/shepherd/evals/evals.json +42 -0
  350. package/skills/ship/SKILL.md +251 -0
  351. package/skills/ship/evals/evals.json +42 -0
  352. package/skills/smith/SKILL.md +142 -0
  353. package/skills/smith/evals/evals.json +46 -0
  354. package/skills/smith/references/autonomy-and-gates.md +94 -0
  355. package/{.github/prompts/sonarcloud.prompt.md → skills/sonarcloud/SKILL.md} +14 -3
  356. package/skills/sonarcloud/evals/evals.json +46 -0
  357. package/skills/sonarcloud-analysis/SKILL.md +18 -13
  358. package/skills/sonarcloud-analysis/evals/evals.json +11 -15
  359. package/skills/status/SKILL.md +102 -0
  360. package/skills/status/evals/evals.json +50 -0
  361. package/skills/triage-ready/SKILL.md +121 -0
  362. package/skills/triage-ready/evals/evals.json +42 -0
  363. package/{.github/prompts/validate.prompt.md → skills/validate/SKILL.md} +52 -29
  364. package/skills/validate/evals/evals.json +42 -0
  365. package/skills/verify/SKILL.md +299 -0
  366. package/skills/verify/evals/evals.json +50 -0
  367. package/.claude/commands/dev.md +0 -345
  368. package/.claude/commands/plan.md +0 -566
  369. package/.claude/commands/premerge.md +0 -186
  370. package/.claude/commands/research.md +0 -42
  371. package/.claude/commands/review.md +0 -451
  372. package/.claude/commands/rollback.md +0 -721
  373. package/.claude/commands/ship.md +0 -213
  374. package/.claude/commands/sonarcloud.md +0 -152
  375. package/.claude/commands/status.md +0 -90
  376. package/.claude/commands/validate.md +0 -288
  377. package/.claude/commands/verify.md +0 -269
  378. package/.claude/rules/workflow.md +0 -121
  379. package/.cline/workflows/dev.md +0 -342
  380. package/.cline/workflows/plan.md +0 -563
  381. package/.cline/workflows/premerge.md +0 -183
  382. package/.cline/workflows/research.md +0 -39
  383. package/.cline/workflows/review.md +0 -448
  384. package/.cline/workflows/rollback.md +0 -718
  385. package/.cline/workflows/ship.md +0 -210
  386. package/.cline/workflows/sonarcloud.md +0 -146
  387. package/.cline/workflows/status.md +0 -87
  388. package/.cline/workflows/validate.md +0 -285
  389. package/.cline/workflows/verify.md +0 -266
  390. package/.codex/config.toml +0 -11
  391. package/.codex/skills/dev/SKILL.md +0 -345
  392. package/.codex/skills/plan/SKILL.md +0 -566
  393. package/.codex/skills/premerge/SKILL.md +0 -186
  394. package/.codex/skills/research/SKILL.md +0 -42
  395. package/.codex/skills/review/SKILL.md +0 -451
  396. package/.codex/skills/rollback/SKILL.md +0 -721
  397. package/.codex/skills/ship/SKILL.md +0 -213
  398. package/.codex/skills/sonarcloud/SKILL.md +0 -149
  399. package/.codex/skills/status/SKILL.md +0 -90
  400. package/.codex/skills/validate/SKILL.md +0 -288
  401. package/.codex/skills/verify/SKILL.md +0 -269
  402. package/.cursor/commands/dev.md +0 -342
  403. package/.cursor/commands/plan.md +0 -563
  404. package/.cursor/commands/premerge.md +0 -183
  405. package/.cursor/commands/research.md +0 -39
  406. package/.cursor/commands/review.md +0 -448
  407. package/.cursor/commands/ship.md +0 -210
  408. package/.cursor/commands/sonarcloud.md +0 -146
  409. package/.cursor/commands/status.md +0 -87
  410. package/.cursor/commands/validate.md +0 -285
  411. package/.cursor/commands/verify.md +0 -266
  412. package/.cursorrules +0 -149
  413. package/.github/prompts/premerge.prompt.md +0 -188
  414. package/.github/prompts/research.prompt.md +0 -44
  415. package/.github/prompts/rollback.prompt.md +0 -723
  416. package/.github/prompts/ship.prompt.md +0 -215
  417. package/.github/prompts/status.prompt.md +0 -92
  418. package/.github/prompts/verify.prompt.md +0 -271
  419. package/.github/workflows/beads-to-github.yml +0 -56
  420. package/.github/workflows/github-to-beads.yml +0 -97
  421. package/.kilocode/workflows/dev.md +0 -346
  422. package/.kilocode/workflows/plan.md +0 -567
  423. package/.kilocode/workflows/premerge.md +0 -187
  424. package/.kilocode/workflows/research.md +0 -43
  425. package/.kilocode/workflows/review.md +0 -452
  426. package/.kilocode/workflows/rollback.md +0 -722
  427. package/.kilocode/workflows/ship.md +0 -214
  428. package/.kilocode/workflows/sonarcloud.md +0 -150
  429. package/.kilocode/workflows/status.md +0 -91
  430. package/.kilocode/workflows/validate.md +0 -289
  431. package/.kilocode/workflows/verify.md +0 -270
  432. package/.opencode/commands/dev.md +0 -345
  433. package/.opencode/commands/plan.md +0 -566
  434. package/.opencode/commands/premerge.md +0 -186
  435. package/.opencode/commands/research.md +0 -42
  436. package/.opencode/commands/review.md +0 -451
  437. package/.opencode/commands/rollback.md +0 -721
  438. package/.opencode/commands/ship.md +0 -213
  439. package/.opencode/commands/sonarcloud.md +0 -149
  440. package/.opencode/commands/status.md +0 -90
  441. package/.opencode/commands/validate.md +0 -288
  442. package/.opencode/commands/verify.md +0 -269
  443. package/.roo/commands/dev.md +0 -346
  444. package/.roo/commands/plan.md +0 -567
  445. package/.roo/commands/premerge.md +0 -187
  446. package/.roo/commands/research.md +0 -43
  447. package/.roo/commands/review.md +0 -452
  448. package/.roo/commands/rollback.md +0 -722
  449. package/.roo/commands/ship.md +0 -214
  450. package/.roo/commands/sonarcloud.md +0 -150
  451. package/.roo/commands/status.md +0 -91
  452. package/.roo/commands/validate.md +0 -289
  453. package/.roo/commands/verify.md +0 -270
  454. package/docs/BEADS_GITHUB_SYNC.md +0 -255
  455. package/docs/GREPTILE_SETUP.md +0 -400
  456. package/docs/MANUAL_REVIEW_GUIDE.md +0 -106
  457. package/docs/SETUP.md +0 -663
  458. package/docs/VALIDATION.md +0 -363
  459. package/lib/agents/cline.plugin.json +0 -29
  460. package/lib/agents/copilot.plugin.json +0 -24
  461. package/lib/agents/kilocode.plugin.json +0 -22
  462. package/lib/agents/opencode.plugin.json +0 -23
  463. package/lib/agents/roo.plugin.json +0 -30
  464. package/lib/beads-health-check.js +0 -143
  465. package/lib/commands/commands-reset.js +0 -147
  466. package/opencode.json +0 -67
  467. package/scripts/beads-context.test.js +0 -567
  468. package/scripts/github-beads-sync/comment.mjs +0 -64
  469. package/scripts/github-beads-sync/config.mjs +0 -148
  470. package/scripts/github-beads-sync/github-api.mjs +0 -131
  471. package/scripts/github-beads-sync/index.mjs +0 -332
  472. package/scripts/github-beads-sync/label-mapper.mjs +0 -54
  473. package/scripts/github-beads-sync/mapping.mjs +0 -78
  474. package/scripts/github-beads-sync/reverse-sync-cli.mjs +0 -31
  475. package/scripts/github-beads-sync/reverse-sync.mjs +0 -138
  476. package/scripts/github-beads-sync/run-bd.mjs +0 -161
  477. package/scripts/github-beads-sync/sanitize.mjs +0 -121
  478. package/scripts/github-beads-sync.config.json +0 -26
  479. 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
+ };