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,581 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * `forge serve [--port N] [--open]`
5
+ *
6
+ * A minimal LOCAL loopback companion that turns the static dashboard
7
+ * (web/dashboard/) into an interactive one. It is a DUMB RELAY: the browser
8
+ * never mutates the kernel directly — it fires the SAME `forge` verbs the CLI
9
+ * and agents use, in-process, through the existing command dispatch. ALL
10
+ * validation lives in the verb/broker handlers; this server adds no parallel
11
+ * validator.
12
+ *
13
+ * Endpoints:
14
+ * GET /health — capability probe (app.js flips SNAPSHOT -> LIVE).
15
+ * GET /data.json — current snapshot (regenerated on demand, debounced).
16
+ * POST /api/mutation — the write path. Body { token, verb, args }.
17
+ * GET /* (static) — the dashboard assets.
18
+ *
19
+ * Security model (see docs/work/2026-07-13-forge-serve/design.md):
20
+ * - Binds 127.0.0.1 ONLY (never a public interface).
21
+ * - Per-run random token, minted at startup, injected into the served page
22
+ * URL fragment (#token=…), REQUIRED on every POST (constant-time compare).
23
+ * - POST also rejects any non-loopback Host / foreign Origin (CSRF fence).
24
+ * - No accounts, no cloud, no daemon; the token dies with the process.
25
+ * - Zero agent/token cost: reads SQLite/kernel + writes via the broker; it
26
+ * NEVER spawns an agent.
27
+ */
28
+
29
+ const http = require('node:http');
30
+ const fs = require('node:fs');
31
+ const path = require('node:path');
32
+ const crypto = require('node:crypto');
33
+ const { spawn } = require('node:child_process');
34
+
35
+ // Shared-machine hardening (single-instance lock, owner-only perms + audit,
36
+ // hash-chained mutation journal). No dependency on serve.js, so a top-level
37
+ // require is safe (unlike _registry — see the lazy-require note below).
38
+ const security = require('./_serve-security');
39
+
40
+ // NOTE: `_registry` / `_resolve-command-opts` are require()d LAZILY at call time
41
+ // (inside routeMutation / registry), never destructured at module top. serve.js
42
+ // is listed in the static command manifest, so `_manifest` require()s serve.js
43
+ // while serve.js is itself mid-load requiring `_registry` — a top-level
44
+ // destructure would capture `_registry`'s not-yet-assigned exports (undefined).
45
+ // A lazy require resolves the fully-initialized module every time.
46
+
47
+ const COMMANDS_DIR = __dirname;
48
+ const DEFAULT_PORT = 8730;
49
+ const MAX_BODY = 1024 * 1024; // 1 MB — issue/comment bodies are user data.
50
+ const SNAPSHOT_TTL_MS = 3000; // debounce background regeneration.
51
+ const GEN_TIMEOUT_MS = 30000;
52
+
53
+ // verb -> forge command. Every verb is an EXISTING handler; issue verbs append
54
+ // --json so the handler returns the machine envelope. No shell is ever invoked,
55
+ // so issue/comment bodies are inert user data (the broker stores them).
56
+ const VERB_MAP = {
57
+ 'issue.create': { command: 'create', json: true },
58
+ 'issue.update': { command: 'update', json: true },
59
+ 'issue.close': { command: 'close', json: true },
60
+ 'issue.comment': { command: 'comment', json: true },
61
+ gate: { command: 'gate', json: false },
62
+ role: { command: 'role', json: false },
63
+ };
64
+
65
+ const MIME = {
66
+ '.html': 'text/html; charset=utf-8',
67
+ '.js': 'text/javascript; charset=utf-8',
68
+ '.mjs': 'text/javascript; charset=utf-8',
69
+ '.css': 'text/css; charset=utf-8',
70
+ '.json': 'application/json; charset=utf-8',
71
+ '.svg': 'image/svg+xml',
72
+ '.map': 'application/json; charset=utf-8',
73
+ };
74
+
75
+ // Generated bundles that index.html <script>-loads. When absent (a fresh
76
+ // worktree gitignores them), return an empty 200 so the page cleanly falls to
77
+ // the live /data.json path with no console error.
78
+ const OPTIONAL_BUNDLES = new Set(['/snapshot.js', '/docs.js']);
79
+
80
+ // ---------------------------------------------------------------------------
81
+ // Arg parsing
82
+ // ---------------------------------------------------------------------------
83
+
84
+ function parseServeArgs(args = [], flags = {}) {
85
+ let port = Number(flags.port) || DEFAULT_PORT;
86
+ let open = Boolean(flags.open);
87
+ for (let i = 0; i < args.length; i += 1) {
88
+ const token = args[i];
89
+ if (token === '--open') open = true;
90
+ else if (token === '--port') { port = Number(args[i + 1]) || port; i += 1; }
91
+ else if (typeof token === 'string' && token.startsWith('--port=')) {
92
+ port = Number(token.slice('--port='.length)) || port;
93
+ }
94
+ }
95
+ return { port, open };
96
+ }
97
+
98
+ // ---------------------------------------------------------------------------
99
+ // Security helpers
100
+ // ---------------------------------------------------------------------------
101
+
102
+ function mintToken() {
103
+ return crypto.randomBytes(32).toString('hex');
104
+ }
105
+
106
+ function verifyToken(provided, expected) {
107
+ if (typeof provided !== 'string' || provided.length !== expected.length) return false;
108
+ return crypto.timingSafeEqual(Buffer.from(provided), Buffer.from(expected));
109
+ }
110
+
111
+ function hostname(hostHeader) {
112
+ if (typeof hostHeader !== 'string') return '';
113
+ // Strip the :port; handle bare IPv6 defensively.
114
+ const lastColon = hostHeader.lastIndexOf(':');
115
+ const host = lastColon > hostHeader.indexOf(']') ? hostHeader.slice(0, lastColon) : hostHeader;
116
+ return host.replace(/^\[|\]$/g, '').toLowerCase();
117
+ }
118
+
119
+ const LOOPBACK_HOSTS = new Set(['127.0.0.1', 'localhost', '::1']);
120
+
121
+ function isLoopbackRequest(req) {
122
+ if (!LOOPBACK_HOSTS.has(hostname(req.headers.host))) return false;
123
+ const origin = req.headers.origin;
124
+ if (typeof origin === 'string' && origin.length > 0) {
125
+ try {
126
+ if (!LOOPBACK_HOSTS.has(new URL(origin).hostname.toLowerCase())) return false;
127
+ } catch {
128
+ return false;
129
+ }
130
+ }
131
+ return true;
132
+ }
133
+
134
+ // Extract the per-run token from a GET request: prefer the `X-Forge-Token`
135
+ // header, fall back to a `?token=` query param. The served page holds the token
136
+ // in its URL fragment and attaches it on the data fetch. Mirrors the POST path,
137
+ // which reads the token from the JSON body.
138
+ function readRequestToken(req) {
139
+ const header = req.headers['x-forge-token'];
140
+ if (typeof header === 'string' && header.length > 0) return header;
141
+ try {
142
+ return new URL(req.url || '/', 'http://127.0.0.1').searchParams.get('token') || '';
143
+ } catch {
144
+ return '';
145
+ }
146
+ }
147
+
148
+ // ---------------------------------------------------------------------------
149
+ // Response helpers
150
+ // ---------------------------------------------------------------------------
151
+
152
+ function sendJson(res, status, payload) {
153
+ const body = JSON.stringify(payload);
154
+ res.writeHead(status, { 'Content-Type': 'application/json; charset=utf-8', 'Cache-Control': 'no-store' });
155
+ res.end(body);
156
+ }
157
+
158
+ function sendText(res, status, text, contentType = 'text/plain; charset=utf-8') {
159
+ res.writeHead(status, { 'Content-Type': contentType });
160
+ res.end(text);
161
+ }
162
+
163
+ function readBody(req, limit = MAX_BODY) {
164
+ return new Promise((resolve, reject) => {
165
+ let size = 0;
166
+ const chunks = [];
167
+ req.on('data', (chunk) => {
168
+ size += chunk.length;
169
+ if (size > limit) {
170
+ reject(new Error('request body too large'));
171
+ req.destroy();
172
+ return;
173
+ }
174
+ chunks.push(chunk);
175
+ });
176
+ req.on('end', () => resolve(Buffer.concat(chunks).toString('utf8')));
177
+ req.on('error', reject);
178
+ });
179
+ }
180
+
181
+ // ---------------------------------------------------------------------------
182
+ // Mutation relay — reuse the CLI's own dispatch, never a parallel validator.
183
+ // ---------------------------------------------------------------------------
184
+
185
+ let SHARED_REGISTRY = null;
186
+ function registry(deps = {}) {
187
+ if (deps?.registry) return deps.registry;
188
+ const loadCommands = deps.loadCommands || require('./_registry').loadCommands;
189
+ if (!SHARED_REGISTRY) SHARED_REGISTRY = loadCommands(COMMANDS_DIR);
190
+ return SHARED_REGISTRY;
191
+ }
192
+
193
+ function validateVerbArgs(verb, args) {
194
+ const spec = VERB_MAP[verb];
195
+ if (!spec) {
196
+ return { error: `Unknown verb '${verb}'. Allowed: ${Object.keys(VERB_MAP).join(', ')}` };
197
+ }
198
+ if (!Array.isArray(args) || !args.every((a) => typeof a === 'string')) {
199
+ return { error: 'args must be an array of strings' };
200
+ }
201
+ return { spec };
202
+ }
203
+
204
+ // Route a { verb, args } envelope to the EXISTING forge command handler,
205
+ // in-process. Returns { ok, output, error } — the handler's own success/
206
+ // rejection is surfaced verbatim (unknown gate, locked gate, invalid role, an
207
+ // issue-command-contract validation error).
208
+ async function routeMutation(verb, args, projectRoot, deps = {}) {
209
+ const { spec, error } = validateVerbArgs(verb, args);
210
+ if (error) return { ok: false, error };
211
+
212
+ const argv = spec.json && !args.includes('--json') ? [...args, '--json'] : args;
213
+ const resolveOpts = deps.resolveCommandOpts || require('./_resolve-command-opts').resolveCommandOpts;
214
+ const exec = deps.executeCommand || require('./_registry').executeCommand;
215
+
216
+ const { commandOpts, args: dispatchArgs } = await resolveOpts(
217
+ spec.command,
218
+ argv,
219
+ { env: process.env, projectRoot },
220
+ );
221
+ const result = await exec(
222
+ registry(deps).commands,
223
+ spec.command,
224
+ dispatchArgs,
225
+ {},
226
+ projectRoot,
227
+ { commandOpts },
228
+ );
229
+ return {
230
+ ok: result && result.success !== false,
231
+ output: result && typeof result.output === 'string' ? result.output : undefined,
232
+ error: result ? (result.error || undefined) : 'no result',
233
+ };
234
+ }
235
+
236
+ // ---------------------------------------------------------------------------
237
+ // Snapshot (GET /data.json) — regenerate on demand, debounced.
238
+ // ---------------------------------------------------------------------------
239
+
240
+ function defaultGenerate(projectRoot, dashboardDir) {
241
+ return new Promise((resolve, reject) => {
242
+ const script = path.join(dashboardDir, 'generate-snapshot.mjs');
243
+ const child = spawn(process.execPath, [script], { cwd: projectRoot, stdio: 'ignore' });
244
+ const timer = setTimeout(() => { child.kill(); reject(new Error('snapshot generation timed out')); }, GEN_TIMEOUT_MS);
245
+ child.on('error', (err) => { clearTimeout(timer); reject(err); });
246
+ child.on('exit', (code) => {
247
+ clearTimeout(timer);
248
+ if (code === 0) resolve();
249
+ else reject(new Error(`snapshot generator exited ${code}`));
250
+ });
251
+ });
252
+ }
253
+
254
+ function readBakedSnapshot(dashboardDir) {
255
+ const dataPath = path.join(dashboardDir, 'data.json');
256
+ if (fs.existsSync(dataPath)) return fs.readFileSync(dataPath);
257
+ return Buffer.from('{}');
258
+ }
259
+
260
+ // Regenerate the snapshot (bounded, single-flight). Best-effort: a generator
261
+ // failure keeps the last baked data.json rather than surfacing an error.
262
+ function regenerate(ctx) {
263
+ const cache = ctx.snapshot;
264
+ if (cache.inFlight) return cache.inFlight;
265
+ cache.inFlight = (async () => {
266
+ try { await cache.generate(ctx.projectRoot, ctx.dashboardDir); } catch { /* keep baked */ }
267
+ cache.buffer = readBakedSnapshot(ctx.dashboardDir);
268
+ cache.lastGen = Date.now();
269
+ })().finally(() => { cache.inFlight = null; });
270
+ return cache.inFlight;
271
+ }
272
+
273
+ // Reads NEVER block on the heavy generator (it shells forge/git/gh): serve the
274
+ // baked snapshot immediately and refresh in the BACKGROUND when stale. Only a
275
+ // cold start with nothing baked does one bounded synchronous generation, so the
276
+ // first paint isn't empty. Mutations call regenerate() directly (see
277
+ // handleMutation) so a post-write refetch is fresh.
278
+ async function getSnapshot(ctx) {
279
+ const cache = ctx.snapshot;
280
+ const hasBaked = cache.buffer || fs.existsSync(path.join(ctx.dashboardDir, 'data.json'));
281
+ if (!hasBaked) {
282
+ await regenerate(ctx);
283
+ return cache.buffer;
284
+ }
285
+ if (!cache.buffer) cache.buffer = readBakedSnapshot(ctx.dashboardDir);
286
+ if (Date.now() - cache.lastGen > SNAPSHOT_TTL_MS && !cache.inFlight) regenerate(ctx);
287
+ return cache.buffer;
288
+ }
289
+
290
+ // ---------------------------------------------------------------------------
291
+ // Static host
292
+ // ---------------------------------------------------------------------------
293
+
294
+ // Map a raw request URL to a SAFE absolute path inside `dashboardDir`, or null
295
+ // if it would escape (path traversal) or is malformed. This is the ONLY place a
296
+ // user-controlled URL is turned into a filesystem path — every fs access must go
297
+ // through the value it returns. The pattern (decode -> strip query/hash -> reject
298
+ // null bytes -> resolve a GUARANTEED-RELATIVE `'.' + path` under a pre-resolved
299
+ // root -> containment check on that same resolved value) is what breaks the taint
300
+ // flow both for real attackers and for static analysis (CodeQL).
301
+ function resolveStaticPath(dashboardDir, urlPath) {
302
+ let clean;
303
+ try {
304
+ // Drop query (?) and fragment (#), then percent-decode. A malformed escape
305
+ // (e.g. a lone '%') throws — treat that as "not found".
306
+ clean = decodeURIComponent(String(urlPath).split('?')[0].split('#')[0]);
307
+ } catch {
308
+ return null;
309
+ }
310
+ // Reject NUL bytes outright (can truncate paths at the syscall boundary).
311
+ if (clean.indexOf('\0') !== -1) return null;
312
+ // Normalize to a single leading slash so the join below is always relative.
313
+ const normalized = '/' + clean.replace(/^\/+/, '');
314
+ const rel = normalized === '/' ? '/index.html' : normalized;
315
+ // Resolve the root FIRST, then join a guaranteed-relative path ('.' + rel).
316
+ // Prefixing with '.' means path.resolve can never treat `rel` as absolute and
317
+ // discard the root, so the result is always rooted at `root`.
318
+ const root = path.resolve(dashboardDir);
319
+ const resolved = path.resolve(root, '.' + rel);
320
+ // Containment check on the EXACT value that will reach the fs calls. If it does
321
+ // not stay within root, refuse (path traversal).
322
+ if (resolved !== root && !resolved.startsWith(root + path.sep)) return null;
323
+ return resolved;
324
+ }
325
+
326
+ function serveStatic(res, dashboardDir, urlPath) {
327
+ const abs = resolveStaticPath(dashboardDir, urlPath);
328
+ if (!abs) return sendText(res, 403, 'forbidden');
329
+ if (!fs.existsSync(abs) || fs.statSync(abs).isDirectory()) {
330
+ // Absent optional bundle -> empty 200 so live mode has no console noise.
331
+ if (OPTIONAL_BUNDLES.has(urlPath.split('?')[0])) {
332
+ return sendText(res, 200, '// forge serve: live mode (bundle not baked)\n', MIME['.js']);
333
+ }
334
+ return sendText(res, 404, 'not found');
335
+ }
336
+ const type = MIME[path.extname(abs).toLowerCase()] || 'application/octet-stream';
337
+ res.writeHead(200, { 'Content-Type': type, 'Cache-Control': 'no-store' });
338
+ return res.end(fs.readFileSync(abs));
339
+ }
340
+
341
+ // ---------------------------------------------------------------------------
342
+ // Request router
343
+ // ---------------------------------------------------------------------------
344
+
345
+ async function handleMutation(req, res, ctx) {
346
+ if (req.method !== 'POST') return sendJson(res, 405, { ok: false, error: 'method not allowed' });
347
+ if (!isLoopbackRequest(req)) return sendJson(res, 403, { ok: false, error: 'forbidden: non-loopback request' });
348
+
349
+ let parsed;
350
+ try {
351
+ parsed = JSON.parse(await readBody(req));
352
+ } catch {
353
+ return sendJson(res, 400, { ok: false, error: 'invalid JSON body' });
354
+ }
355
+ if (!parsed || !verifyToken(parsed.token, ctx.token)) {
356
+ return sendJson(res, 403, { ok: false, error: 'invalid or missing token' });
357
+ }
358
+ const result = await routeMutation(parsed.verb, parsed.args || [], ctx.projectRoot, ctx.deps);
359
+ // Tamper-evident audit trail: record EVERY mutation attempt (accepted or
360
+ // rejected) into the hash-chained journal. The `actor`/`origin` are advisory
361
+ // coordination metadata only — loopback requests are not authenticated beyond
362
+ // the per-run token, so this records WHAT the server did, not a proven WHO.
363
+ // Best-effort: a journal write must never break the response.
364
+ try {
365
+ security.appendJournal(ctx.projectRoot, {
366
+ verb: typeof parsed.verb === 'string' ? parsed.verb : null,
367
+ ok: Boolean(result.ok),
368
+ actor: 'local',
369
+ origin: typeof req.headers.origin === 'string' ? req.headers.origin : null,
370
+ });
371
+ } catch { /* journal is best-effort; never fail the mutation on it */ }
372
+ // On a successful write, refresh the baked snapshot BEFORE responding so the
373
+ // client's follow-up /data.json refetch reflects the change. Best-effort.
374
+ if (result.ok) { try { await regenerate(ctx); } catch { /* keep baked */ } }
375
+ return sendJson(res, result.ok ? 200 : 422, result);
376
+ }
377
+
378
+ async function handleRequest(req, res, ctx) {
379
+ const urlPath = (req.url || '/').split('?')[0];
380
+ if (urlPath === '/health') {
381
+ return sendJson(res, 200, { ok: true, forge_serve: true, version: 1 });
382
+ }
383
+ if (urlPath === '/api/mutation') {
384
+ return handleMutation(req, res, ctx);
385
+ }
386
+ if (urlPath === '/data.json') {
387
+ // The snapshot carries the FULL issue/message data, so an unauthenticated
388
+ // read by any other local user/process is a real disclosure. Gate it with
389
+ // the SAME fence as POST /api/mutation: loopback-only + valid per-run token
390
+ // (constant-time compare). /health stays token-free (capability probe only).
391
+ if (!isLoopbackRequest(req) || !verifyToken(readRequestToken(req), ctx.token)) {
392
+ return sendJson(res, 403, { ok: false, error: 'forbidden: invalid or missing token' });
393
+ }
394
+ const buf = await getSnapshot(ctx);
395
+ res.writeHead(200, { 'Content-Type': MIME['.json'], 'Cache-Control': 'no-store' });
396
+ return res.end(buf);
397
+ }
398
+ if (req.method !== 'GET') return sendText(res, 405, 'method not allowed');
399
+ return serveStatic(res, ctx.dashboardDir, req.url || '/');
400
+ }
401
+
402
+ function buildContext(projectRoot, dashboardDir, token, deps = {}) {
403
+ return {
404
+ projectRoot,
405
+ dashboardDir,
406
+ token,
407
+ deps,
408
+ snapshot: {
409
+ buffer: null,
410
+ lastGen: 0,
411
+ inFlight: null,
412
+ generate: deps.generate || defaultGenerate,
413
+ },
414
+ };
415
+ }
416
+
417
+ function createServeServer(ctx) {
418
+ return http.createServer((req, res) => {
419
+ handleRequest(req, res, ctx).catch((err) => {
420
+ if (!res.headersSent) sendJson(res, 500, { ok: false, error: err.message });
421
+ });
422
+ });
423
+ }
424
+
425
+ // ---------------------------------------------------------------------------
426
+ // Startup
427
+ // ---------------------------------------------------------------------------
428
+
429
+ // Resolve the OS "open a URL" helper to an ABSOLUTE path — never a bare command
430
+ // name resolved through $PATH. A writable PATH entry could otherwise shadow the
431
+ // executable (S4036); an absolute path in a fixed system directory forecloses it.
432
+ function browserOpener() {
433
+ if (process.platform === 'win32') {
434
+ const root = process.env.SystemRoot || 'C:\\Windows';
435
+ return { file: path.join(root, 'System32', 'cmd.exe'), args: ['/c', 'start', ''] };
436
+ }
437
+ if (process.platform === 'darwin') return { file: '/usr/bin/open', args: [] };
438
+ return { file: '/usr/bin/xdg-open', args: [] };
439
+ }
440
+
441
+ function openBrowser(url) {
442
+ try {
443
+ const { file, args } = browserOpener();
444
+ const child = spawn(file, [...args, url], { detached: true, stdio: 'ignore' });
445
+ // spawn() failures (e.g. the opener binary is missing) surface ASYNChronously
446
+ // as an 'error' event, NOT via the try/catch — an unhandled one would crash
447
+ // `forge serve`. Opening a browser is best-effort; swallow it (URL is printed).
448
+ child.on('error', () => { /* best-effort; the URL is always printed */ });
449
+ child.unref();
450
+ } catch {
451
+ // Opening a browser is best-effort; the URL is always printed.
452
+ }
453
+ }
454
+
455
+ // Register signal handlers that release the single-instance lock, close the
456
+ // server, and resolve the handler's promise, so `forge serve` exits cleanly on
457
+ // Ctrl-C without leaving a stale lock behind. `onClose` releases the lock.
458
+ function installServeShutdown(server, resolve, onClose = () => {}) {
459
+ let done = false;
460
+ const shutdown = () => {
461
+ if (done) return; // SIGINT then SIGTERM must not double-release/close.
462
+ done = true;
463
+ try { onClose(); } catch { /* best effort */ }
464
+ server.close(() => resolve({ success: true, output: 'forge serve stopped.' }));
465
+ };
466
+ process.once('SIGINT', shutdown);
467
+ process.once('SIGTERM', shutdown);
468
+ // Last-ditch release if the process exits some other way (uncaught, exit()).
469
+ process.once('exit', () => { try { onClose(); } catch { /* best effort */ } });
470
+ }
471
+
472
+ // Print the loopback URL (token in the fragment), optionally open a browser, and
473
+ // wire shutdown. Extracted so startListening's listen-callback stays shallow.
474
+ function announceServe(server, token, options, resolve) {
475
+ const url = `http://127.0.0.1:${server.address().port}/#token=${token}`;
476
+ process.stdout.write(
477
+ `\nforge serve — interactive dashboard (loopback only)\n ${url}\n`
478
+ + ' Token dies with this process. Press Ctrl-C to stop.\n\n',
479
+ );
480
+ if (options.open) openBrowser(url);
481
+ installServeShutdown(server, resolve, options.onClose);
482
+ }
483
+
484
+ function startListening(server, port, token, options = {}) {
485
+ return new Promise((resolve, reject) => {
486
+ server.on('error', reject);
487
+ server.listen(port, '127.0.0.1', () => announceServe(server, token, options, resolve));
488
+ });
489
+ }
490
+
491
+ // Startup path audit: warn LOUDLY if the serve state dir / lock / journal are
492
+ // group- or other-readable on a shared box (POSIX). On Windows the mode bits do
493
+ // not reflect ACLs, so we print an honest one-line caveat instead of a false
494
+ // alarm. Never blocks startup — hardening is advisory, not a gate.
495
+ function auditServePaths(projectRoot) {
496
+ const audit = security.auditPaths([
497
+ security.serveStateDir(projectRoot),
498
+ security.lockPath(projectRoot),
499
+ security.journalPath(projectRoot),
500
+ ]);
501
+ if (!audit.ok) {
502
+ const paths = audit.offenders.map((o) => o.path).join(', ');
503
+ process.stderr.write(
504
+ `\n⚠ forge serve: sensitive path(s) are readable by other users: ${paths}\n`
505
+ + ' On a shared machine another local user could read the token/journal.\n'
506
+ + ' Tighten perms (chmod 700 .forge/serve) or run on a single-user box.\n\n',
507
+ );
508
+ } else if (audit.results.some((r) => r.exists && r.platformCaveat)) {
509
+ process.stderr.write(
510
+ 'note: forge serve stores its lock/journal under .forge/serve. On Windows,'
511
+ + ' file perms are enforced by NTFS ACLs, not POSIX mode bits — keep the'
512
+ + ' project on a single-user profile for the strongest isolation.\n',
513
+ );
514
+ }
515
+ }
516
+
517
+ // Verify the hash-chained mutation journal at startup so the tamper-evident
518
+ // control actually RUNS on every serve (not just in tests). A broken chain means
519
+ // a past record was edited or a non-tail record removed since we last wrote it —
520
+ // warn LOUDLY but NEVER block startup (integrity signal, not a gate). `warn` is
521
+ // injectable for testing; production writes to stderr. Returns the verify result.
522
+ function verifyJournalAtStartup(projectRoot, warn = (m) => process.stderr.write(m)) {
523
+ const result = security.verifyJournal(projectRoot);
524
+ if (!result.ok) {
525
+ warn(
526
+ `\n⚠ forge serve: mutation journal integrity check FAILED at record #${result.brokenAt} `
527
+ + `(${result.reason}).\n`
528
+ + ` A past entry in ${security.journalPath(projectRoot)} was edited or removed since it was written.\n`
529
+ + ' The tamper-evident history is compromised — investigate before trusting it.\n\n',
530
+ );
531
+ }
532
+ return result;
533
+ }
534
+
535
+ async function handler(args, flags = {}, projectRoot = process.cwd(), _opts = {}) {
536
+ const { port, open } = parseServeArgs(args, flags);
537
+ const dashboardDir = path.join(projectRoot, 'web', 'dashboard');
538
+ if (!fs.existsSync(path.join(dashboardDir, 'index.html'))) {
539
+ return { success: false, error: `Dashboard not found at ${dashboardDir}. Run from the repo root.` };
540
+ }
541
+
542
+ // Single-instance guard: refuse to start a rogue second server for this
543
+ // project (a live holder blocks; a stale lock from a dead PID is reclaimed).
544
+ const lock = security.acquireLock(projectRoot, { port });
545
+ if (!lock.ok) {
546
+ return {
547
+ success: false,
548
+ error: `forge serve is already running for this project (pid ${lock.held.pid}`
549
+ + `${lock.held.port ? `, port ${lock.held.port}` : ''}). Stop it before starting another, `
550
+ + 'or a second server would silently port-squat. If that process is dead, remove '
551
+ + `${security.lockPath(projectRoot)}.`,
552
+ };
553
+ }
554
+ auditServePaths(projectRoot);
555
+ verifyJournalAtStartup(projectRoot);
556
+
557
+ const token = mintToken();
558
+ const ctx = buildContext(projectRoot, dashboardDir, token);
559
+ const server = createServeServer(ctx);
560
+ return startListening(server, port, token, {
561
+ open,
562
+ onClose: () => security.releaseLock(projectRoot),
563
+ });
564
+ }
565
+
566
+ module.exports = {
567
+ name: 'serve',
568
+ description: 'Serve the interactive dashboard over 127.0.0.1 (trigger forge verbs from the browser)',
569
+ usage: 'forge serve [--port N] [--open]',
570
+ handler,
571
+ // Exposed for tests + reuse (in-process, no socket).
572
+ routeMutation,
573
+ parseServeArgs,
574
+ buildContext,
575
+ createServeServer,
576
+ verifyToken,
577
+ isLoopbackRequest,
578
+ resolveStaticPath,
579
+ verifyJournalAtStartup,
580
+ VERB_MAP,
581
+ };