forge-workflow 0.0.10 → 0.1.0-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (454) hide show
  1. package/.claude/rules/{greptile-review-process.md → review-process.md} +56 -41
  2. package/.claude/scripts/{greptile-resolve.sh → review-resolve.sh} +13 -3
  3. package/.cursor/rules/permissions-guidance.mdc +2 -2
  4. package/.forge/hooks/check-tdd.js +3 -0
  5. package/.forge/hooks/forge-native-hook.js +245 -0
  6. package/.forge/protected-paths.yaml +157 -0
  7. package/AGENTS.md +150 -61
  8. package/CHANGELOG.md +681 -0
  9. package/CLAUDE.md +9 -118
  10. package/QUICKSTART.md +171 -0
  11. package/README.md +271 -363
  12. package/bin/forge-cmd.js +120 -9
  13. package/bin/forge-preflight.js +26 -5
  14. package/bin/forge.js +461 -489
  15. package/docs/INDEX.md +93 -0
  16. package/docs/PROJECT_DESIGN.md +685 -0
  17. package/docs/architecture/index.md +66 -0
  18. package/docs/architecture/notes/README.md +35 -0
  19. package/docs/architecture/subsystems/README.md +46 -0
  20. package/docs/forge/TOOLCHAIN.md +670 -0
  21. package/docs/forge/VALIDATION.md +82 -0
  22. package/docs/{AGENT_INSTALL_PROMPT.md → guides/AGENT_INSTALL_PROMPT.md} +3 -3
  23. package/docs/guides/BEADS_GITHUB_SYNC.md +32 -0
  24. package/docs/{ENHANCED_ONBOARDING.md → guides/ENHANCED_ONBOARDING.md} +16 -12
  25. package/docs/guides/GREPTILE_SETUP.md +46 -0
  26. package/docs/guides/MANUAL_REVIEW_GUIDE.md +58 -0
  27. package/docs/guides/MIGRATION.md +56 -0
  28. package/docs/guides/SETUP.md +118 -0
  29. package/docs/guides/SUPPORT.md +185 -0
  30. package/docs/guides/WORKFLOW_TEMPLATES.md +74 -0
  31. package/docs/guides/memory-backends.md +183 -0
  32. package/docs/reference/ADAPTERS.md +128 -0
  33. package/docs/reference/AGENT_SKILL_PARITY.md +175 -0
  34. package/docs/reference/COMMANDS.md +205 -0
  35. package/docs/reference/DECISION_DRIFT_GUARDS.md +97 -0
  36. package/docs/{EXAMPLES.md → reference/EXAMPLES.md} +7 -5
  37. package/docs/reference/FORGE_KERNEL_STORAGE_MODEL.md +135 -0
  38. package/docs/reference/HERMES_INTEGRATION.md +118 -0
  39. package/docs/reference/INSIGHTS_RECAP.md +63 -0
  40. package/docs/reference/INSTALL.md +164 -0
  41. package/docs/reference/KERNEL_TAXONOMY_VALIDATION.md +161 -0
  42. package/docs/reference/PROTECTED_PATH_MANIFEST.md +25 -0
  43. package/docs/reference/RELEASE.md +68 -0
  44. package/docs/reference/RESEARCH_TEMPLATE.md +292 -0
  45. package/docs/{ROADMAP.md → reference/ROADMAP.md} +12 -9
  46. package/docs/reference/SKILLS.md +35 -0
  47. package/docs/reference/STATUS_BOARD.md +80 -0
  48. package/docs/reference/TEMPLATES.md +106 -0
  49. package/docs/{TOOLCHAIN.md → reference/TOOLCHAIN.md} +62 -47
  50. package/docs/reference/VALIDATION.md +82 -0
  51. package/docs/reference/agent-permissions.md +169 -0
  52. package/docs/reference/beads-to-kernel-migration-ux.md +61 -0
  53. package/docs/reference/control-plane-guarantees.md +125 -0
  54. package/docs/reference/dependency-chain.md +331 -0
  55. package/docs/reference/forge-kernel-issue-command-contract.md +161 -0
  56. package/docs/reference/forge-kernel-schema.md +72 -0
  57. package/docs/reference/kernel-conflict-evaluators.md +27 -0
  58. package/docs/reference/patch-md-format.md +77 -0
  59. package/docs/reference/protected-state-surfaces.md +59 -0
  60. package/docs/reference/shepherd.md +115 -0
  61. package/docs/reference/superpowers-analysis.md +320 -0
  62. package/docs/reference/superpowers-integration-options.md +404 -0
  63. package/docs/reference/test-environment.md +519 -0
  64. package/docs/reference/upgrade-safety.md +59 -0
  65. package/lefthook.yml +18 -0
  66. package/lib/adapter-cli.js +307 -0
  67. package/lib/adapters/beads-issue-adapter.js +127 -0
  68. package/lib/adapters/beads-kernel-compat.js +1042 -0
  69. package/lib/adapters/greptile-review-adapter.js +141 -0
  70. package/lib/adapters/kernel-issue-adapter.js +101 -0
  71. package/lib/adapters/pr-state-adapter.js +484 -0
  72. package/lib/adoption-profiles.js +126 -0
  73. package/lib/agents/README.md +2 -6
  74. package/lib/agents/claude.plugin.json +3 -8
  75. package/lib/agents/codex.plugin.json +9 -1
  76. package/lib/agents/cursor.plugin.json +2 -6
  77. package/lib/agents/hermes.plugin.json +22 -0
  78. package/lib/agents-config.js +39 -1236
  79. package/lib/audit-evidence.js +282 -0
  80. package/lib/beads-setup.js +121 -0
  81. package/lib/beads-sync-scaffold.js +25 -101
  82. package/lib/codex-skills.js +51 -1
  83. package/lib/commands/_issue.js +741 -77
  84. package/lib/commands/_manifest.js +91 -0
  85. package/lib/commands/_registry.js +85 -34
  86. package/lib/commands/_resolve-command-opts.js +261 -0
  87. package/lib/commands/_serve-security.js +270 -0
  88. package/lib/commands/adapter.js +12 -0
  89. package/lib/commands/add.js +118 -0
  90. package/lib/commands/audit.js +70 -0
  91. package/lib/commands/blocked.js +5 -0
  92. package/lib/commands/board.js +64 -0
  93. package/lib/commands/claim.js +21 -2
  94. package/lib/commands/claims.js +7 -0
  95. package/lib/commands/clean.js +485 -75
  96. package/lib/commands/close.js +2 -2
  97. package/lib/commands/comment.js +5 -0
  98. package/lib/commands/control.js +148 -0
  99. package/lib/commands/create.js +2 -2
  100. package/lib/commands/dev.js +185 -7
  101. package/lib/commands/doc-gate.js +336 -0
  102. package/lib/commands/doctor.js +156 -0
  103. package/lib/commands/explain.js +15 -0
  104. package/lib/commands/export.js +237 -0
  105. package/lib/commands/gate.js +192 -0
  106. package/lib/commands/hooks.js +242 -0
  107. package/lib/commands/inbox.js +118 -0
  108. package/lib/commands/init.js +598 -0
  109. package/lib/commands/insights.js +79 -0
  110. package/lib/commands/issue.js +12 -1
  111. package/lib/commands/issues.js +17 -0
  112. package/lib/commands/lint.js +5 -0
  113. package/lib/commands/list.js +2 -2
  114. package/lib/commands/merge.js +312 -0
  115. package/lib/commands/migrate.js +523 -0
  116. package/lib/commands/new.js +12 -0
  117. package/lib/commands/options.js +241 -0
  118. package/lib/commands/orient.js +13 -0
  119. package/lib/commands/orphans.js +5 -0
  120. package/lib/commands/patch.js +67 -0
  121. package/lib/commands/plan.js +436 -24
  122. package/lib/commands/preflight.js +211 -0
  123. package/lib/commands/prime.js +13 -0
  124. package/lib/commands/push.js +69 -2
  125. package/lib/commands/ready.js +2 -2
  126. package/lib/commands/recall.js +116 -0
  127. package/lib/commands/recap.js +61 -0
  128. package/lib/commands/recommend.js +0 -1
  129. package/lib/commands/release.js +91 -0
  130. package/lib/commands/remember.js +74 -0
  131. package/lib/commands/role.js +99 -0
  132. package/lib/commands/serve.js +581 -0
  133. package/lib/commands/setup.js +838 -972
  134. package/lib/commands/shepherd.js +436 -0
  135. package/lib/commands/ship.js +23 -1
  136. package/lib/commands/show.js +2 -2
  137. package/lib/commands/stage.js +192 -0
  138. package/lib/commands/stale.js +5 -0
  139. package/lib/commands/status.js +158 -21
  140. package/lib/commands/sync.js +34 -46
  141. package/lib/commands/team.js +4 -1
  142. package/lib/commands/test.js +43 -27
  143. package/lib/commands/update.js +2 -2
  144. package/lib/commands/upgrade.js +47 -0
  145. package/lib/commands/validate.js +43 -18
  146. package/lib/commands/worktree.js +307 -100
  147. package/lib/config-writer.js +202 -0
  148. package/lib/control-plane.js +236 -0
  149. package/lib/core/runtime-graph.js +946 -0
  150. package/lib/dep-guard/keyword-ripple.js +2 -2
  151. package/lib/deprecated-sync-cleanup.js +362 -0
  152. package/lib/detect-agent.js +2 -28
  153. package/lib/detect-worktree.js +35 -9
  154. package/lib/doc-gate/declaration.js +177 -0
  155. package/lib/doc-gate/detect.js +289 -0
  156. package/lib/doc-gate/gate.js +375 -0
  157. package/lib/doc-gate/okf-config.js +128 -0
  158. package/lib/doc-gate/okf.js +429 -0
  159. package/lib/docs-command.js +1161 -6
  160. package/lib/forge-issues.js +382 -11
  161. package/lib/forge-lock.js +262 -0
  162. package/lib/gate-events.js +193 -0
  163. package/lib/global-flags.js +74 -0
  164. package/lib/greptile-match.js +7 -63
  165. package/lib/harness-capability-matrix.js +380 -0
  166. package/lib/hook-global-installer.js +347 -0
  167. package/lib/hook-renderer.js +451 -0
  168. package/lib/inbox.js +391 -0
  169. package/lib/insights.js +397 -0
  170. package/lib/issue-adapter.js +156 -0
  171. package/lib/issue-backend.js +145 -0
  172. package/lib/issue-render.js +220 -0
  173. package/lib/kernel/backing-issue.js +305 -0
  174. package/lib/kernel/broker.js +1218 -0
  175. package/lib/kernel/cli-broker-factory.js +130 -0
  176. package/lib/kernel/conflict-signal.js +82 -0
  177. package/lib/kernel/evaluators.js +195 -0
  178. package/lib/kernel/fs-class.js +495 -0
  179. package/lib/kernel/issue-command-contract.js +559 -0
  180. package/lib/kernel/issue-id-resolver.js +186 -0
  181. package/lib/kernel/lease-enforcer.js +158 -0
  182. package/lib/kernel/migrations.js +333 -0
  183. package/lib/kernel/planning-buckets-schema.js +109 -0
  184. package/lib/kernel/projection-jsonl-writer.js +450 -0
  185. package/lib/kernel/readiness-model.js +329 -0
  186. package/lib/kernel/schema.js +356 -0
  187. package/lib/kernel/sqlite-driver.js +2504 -0
  188. package/lib/kernel/taxonomy-validator.js +394 -0
  189. package/lib/lefthook-check.js +3 -2
  190. package/lib/lefthook-wiring.js +413 -0
  191. package/lib/mcp-config-renderer.js +288 -0
  192. package/lib/memory/graphiti-mcp.js +106 -0
  193. package/lib/memory/router.js +387 -0
  194. package/lib/memory/typed-api.js +102 -0
  195. package/lib/memory-digest.js +195 -0
  196. package/lib/merge-rules.js +395 -0
  197. package/lib/migrate-dry-run.js +466 -0
  198. package/lib/orientation.js +863 -0
  199. package/lib/package-manager-remediation.js +103 -0
  200. package/lib/package-root.js +381 -0
  201. package/lib/patch-intent.js +890 -0
  202. package/lib/plugin-catalog.js +3 -4
  203. package/lib/plugin-manager.js +0 -5
  204. package/lib/pr-bundle.js +186 -0
  205. package/lib/pr-monitor/differ.js +195 -0
  206. package/lib/pr-monitor/events.js +0 -0
  207. package/lib/pr-monitor/gather.js +124 -0
  208. package/lib/pr-monitor/journal.js +299 -0
  209. package/lib/pr-monitor/monitor.js +146 -0
  210. package/lib/pr-monitor/render-sticky.js +157 -0
  211. package/lib/pr-monitor/watch-lifecycle.js +95 -0
  212. package/lib/pr-monitor/watch.js +247 -0
  213. package/lib/pr-pull.js +1273 -0
  214. package/lib/pr-shepherd.js +494 -0
  215. package/lib/pr-state-validator.js +59 -0
  216. package/lib/preflight/gates.js +237 -0
  217. package/lib/preflight/runner.js +116 -0
  218. package/lib/project-discovery.js +0 -53
  219. package/lib/project-memory.js +99 -497
  220. package/lib/protected-path-manifest.js +281 -0
  221. package/lib/protected-state-surfaces.js +387 -0
  222. package/lib/release-readiness.js +2089 -0
  223. package/lib/reset.js +59 -45
  224. package/lib/review-adapter.js +68 -0
  225. package/lib/rules-sync.js +260 -0
  226. package/lib/runtime-health.js +241 -20
  227. package/lib/safety-config-renderer.js +268 -0
  228. package/lib/setup-action-log.js +1 -7
  229. package/lib/setup.js +27 -65
  230. package/lib/shell-utils.js +76 -6
  231. package/lib/skills-sync.js +330 -0
  232. package/lib/smart-status/scoring.js +17 -3
  233. package/lib/status/beads-snapshot.js +45 -2
  234. package/lib/status/presenter.js +169 -18
  235. package/lib/status/snapshot.js +186 -0
  236. package/lib/sync-backend.js +202 -0
  237. package/lib/untrusted-content.js +52 -0
  238. package/lib/upgrade-safety.js +199 -0
  239. package/lib/workflow/enforce-stage.js +296 -47
  240. package/lib/workflow/stage-transition.js +115 -0
  241. package/lib/workflow/stages.js +30 -6
  242. package/lib/workflow/state-manager.js +11 -22
  243. package/lib/workflow/state.js +23 -1
  244. package/lib/workflow-profiles.js +17 -5
  245. package/package.json +37 -35
  246. package/rules/documentation.md +19 -0
  247. package/rules/kernel-tracking.md +26 -0
  248. package/rules/security.md +22 -0
  249. package/rules/tdd.md +20 -0
  250. package/rules/workflow.md +27 -0
  251. package/scripts/auto-backing-issue.js +47 -0
  252. package/scripts/beads-context.sh +81 -57
  253. package/scripts/beads-upgrade-smoke.sh +24 -3
  254. package/scripts/bootstrap-windows-tools.sh +78 -0
  255. package/scripts/branch-protection.js +2 -3
  256. package/scripts/check-agents.js +34 -137
  257. package/scripts/commitlint.js +3 -1
  258. package/scripts/conflict-detect.sh +3 -0
  259. package/scripts/dep-guard.sh +22 -3
  260. package/scripts/file-index.sh +3 -0
  261. package/scripts/forge-team/lib/claim.sh +34 -18
  262. package/scripts/forge-team/lib/dashboard.sh +61 -86
  263. package/scripts/forge-team/lib/epic.sh +99 -263
  264. package/scripts/forge-team/lib/hooks.sh +26 -28
  265. package/scripts/forge-team/lib/identity.sh +4 -4
  266. package/scripts/forge-team/lib/sync-github.sh +49 -84
  267. package/scripts/forge-team/lib/verify.sh +93 -83
  268. package/scripts/forge-team/lib/workload.sh +41 -65
  269. package/scripts/forge-team/tests/claim.test.sh +25 -19
  270. package/scripts/forge-team/tests/dashboard.test.sh +31 -46
  271. package/scripts/forge-team/tests/epic.test.sh +52 -71
  272. package/scripts/forge-team/tests/hooks.test.sh +38 -50
  273. package/scripts/forge-team/tests/identity.test.sh +3 -3
  274. package/scripts/forge-team/tests/integration.test.sh +44 -66
  275. package/scripts/forge-team/tests/sync-github.test.sh +50 -83
  276. package/scripts/forge-team/tests/verify.test.sh +37 -46
  277. package/scripts/forge-team/tests/workflow-integration.test.sh +4 -4
  278. package/scripts/forge-team/tests/workload.test.sh +32 -66
  279. package/scripts/gen-command-manifest.js +153 -0
  280. package/scripts/gen-embedded-assets.mjs +129 -0
  281. package/scripts/install.ps1 +139 -0
  282. package/scripts/install.sh +268 -0
  283. package/scripts/lib/release-asset.mjs +84 -0
  284. package/scripts/parity-check.mjs +145 -0
  285. package/scripts/parity-check.test.mjs +58 -0
  286. package/scripts/pin-agentic-workflow-images.js +112 -0
  287. package/scripts/pr-coordinator.sh +3 -0
  288. package/scripts/preflight-sonar.eslint.config.mjs +44 -0
  289. package/scripts/preflight.sh +21 -94
  290. package/scripts/protected-state-check.js +104 -0
  291. package/scripts/smart-status.sh +60 -57
  292. package/scripts/spikes/config-race-bench.js +111 -0
  293. package/scripts/spikes/harness-capability-matrix.js +13 -0
  294. package/scripts/spikes/patch-anchor-stability-bench.js +125 -0
  295. package/scripts/spikes/protected-path-manifest.js +20 -0
  296. package/scripts/spikes/skill-auto-invoke-parity.js +292 -0
  297. package/scripts/sync-agent-skills.js +62 -0
  298. package/scripts/sync-utils.sh +3 -0
  299. package/scripts/test-ci-shard.js +13 -6
  300. package/scripts/test.js +95 -12
  301. package/skills/claim-safety/SKILL.md +102 -0
  302. package/skills/claim-safety/evals/evals.json +46 -0
  303. package/{.github/prompts/dev.prompt.md → skills/dev/SKILL.md} +44 -50
  304. package/skills/dev/evals/evals.json +50 -0
  305. package/skills/hermes-forge/SKILL.md +185 -0
  306. package/skills/hermes-forge/evals/evals.json +46 -0
  307. package/skills/issue-basics/SKILL.md +111 -0
  308. package/skills/issue-basics/evals/evals.json +46 -0
  309. package/skills/kernel/SKILL.md +166 -0
  310. package/skills/kernel/evals/evals.json +50 -0
  311. package/skills/memory/SKILL.md +102 -0
  312. package/skills/parallel-deep-research/SKILL.md +14 -11
  313. package/skills/parallel-deep-research/evals/evals.json +11 -27
  314. package/{.github/prompts/plan.prompt.md → skills/plan/SKILL.md} +132 -157
  315. package/skills/plan/evals/evals.json +42 -0
  316. package/skills/research/SKILL.md +195 -0
  317. package/skills/research/evals/evals.json +42 -0
  318. package/{.github/prompts/review.prompt.md → skills/review/SKILL.md} +98 -62
  319. package/skills/review/evals/evals.json +42 -0
  320. package/skills/rollback/SKILL.md +110 -0
  321. package/skills/rollback/evals/evals.json +46 -0
  322. package/skills/rollback/references/methods.md +204 -0
  323. package/{.cursor/commands/rollback.md → skills/rollback/references/workflow-integration.md} +10 -284
  324. package/skills/shepherd/SKILL.md +66 -0
  325. package/skills/shepherd/evals/evals.json +42 -0
  326. package/{.github/prompts/ship.prompt.md → skills/ship/SKILL.md} +81 -45
  327. package/skills/ship/evals/evals.json +42 -0
  328. package/skills/smith/SKILL.md +142 -0
  329. package/skills/smith/evals/evals.json +46 -0
  330. package/skills/smith/references/autonomy-and-gates.md +94 -0
  331. package/{.github/prompts/sonarcloud.prompt.md → skills/sonarcloud/SKILL.md} +14 -3
  332. package/skills/sonarcloud/evals/evals.json +46 -0
  333. package/skills/sonarcloud-analysis/SKILL.md +18 -13
  334. package/skills/sonarcloud-analysis/evals/evals.json +11 -15
  335. package/{.github/prompts/status.prompt.md → skills/status/SKILL.md} +20 -10
  336. package/skills/status/evals/evals.json +50 -0
  337. package/skills/triage-ready/SKILL.md +121 -0
  338. package/skills/triage-ready/evals/evals.json +42 -0
  339. package/{.github/prompts/validate.prompt.md → skills/validate/SKILL.md} +52 -29
  340. package/skills/validate/evals/evals.json +42 -0
  341. package/skills/verify/SKILL.md +299 -0
  342. package/skills/verify/evals/evals.json +50 -0
  343. package/.claude/commands/dev.md +0 -345
  344. package/.claude/commands/plan.md +0 -566
  345. package/.claude/commands/premerge.md +0 -186
  346. package/.claude/commands/research.md +0 -42
  347. package/.claude/commands/review.md +0 -451
  348. package/.claude/commands/rollback.md +0 -721
  349. package/.claude/commands/ship.md +0 -213
  350. package/.claude/commands/sonarcloud.md +0 -152
  351. package/.claude/commands/status.md +0 -90
  352. package/.claude/commands/validate.md +0 -288
  353. package/.claude/commands/verify.md +0 -269
  354. package/.claude/rules/workflow.md +0 -121
  355. package/.cline/workflows/dev.md +0 -342
  356. package/.cline/workflows/plan.md +0 -563
  357. package/.cline/workflows/premerge.md +0 -183
  358. package/.cline/workflows/research.md +0 -39
  359. package/.cline/workflows/review.md +0 -448
  360. package/.cline/workflows/rollback.md +0 -718
  361. package/.cline/workflows/ship.md +0 -210
  362. package/.cline/workflows/sonarcloud.md +0 -146
  363. package/.cline/workflows/status.md +0 -87
  364. package/.cline/workflows/validate.md +0 -285
  365. package/.cline/workflows/verify.md +0 -266
  366. package/.codex/config.toml +0 -11
  367. package/.codex/skills/dev/SKILL.md +0 -345
  368. package/.codex/skills/plan/SKILL.md +0 -566
  369. package/.codex/skills/premerge/SKILL.md +0 -186
  370. package/.codex/skills/research/SKILL.md +0 -42
  371. package/.codex/skills/review/SKILL.md +0 -451
  372. package/.codex/skills/rollback/SKILL.md +0 -721
  373. package/.codex/skills/ship/SKILL.md +0 -213
  374. package/.codex/skills/sonarcloud/SKILL.md +0 -149
  375. package/.codex/skills/status/SKILL.md +0 -90
  376. package/.codex/skills/validate/SKILL.md +0 -288
  377. package/.codex/skills/verify/SKILL.md +0 -269
  378. package/.cursor/commands/dev.md +0 -342
  379. package/.cursor/commands/plan.md +0 -563
  380. package/.cursor/commands/premerge.md +0 -183
  381. package/.cursor/commands/research.md +0 -39
  382. package/.cursor/commands/review.md +0 -448
  383. package/.cursor/commands/ship.md +0 -210
  384. package/.cursor/commands/sonarcloud.md +0 -146
  385. package/.cursor/commands/status.md +0 -87
  386. package/.cursor/commands/validate.md +0 -285
  387. package/.cursor/commands/verify.md +0 -266
  388. package/.cursorrules +0 -149
  389. package/.github/prompts/premerge.prompt.md +0 -188
  390. package/.github/prompts/research.prompt.md +0 -44
  391. package/.github/prompts/rollback.prompt.md +0 -723
  392. package/.github/prompts/verify.prompt.md +0 -271
  393. package/.github/workflows/beads-to-github.yml +0 -89
  394. package/.github/workflows/github-to-beads.yml +0 -100
  395. package/.kilocode/workflows/dev.md +0 -346
  396. package/.kilocode/workflows/plan.md +0 -567
  397. package/.kilocode/workflows/premerge.md +0 -187
  398. package/.kilocode/workflows/research.md +0 -43
  399. package/.kilocode/workflows/review.md +0 -452
  400. package/.kilocode/workflows/rollback.md +0 -722
  401. package/.kilocode/workflows/ship.md +0 -214
  402. package/.kilocode/workflows/sonarcloud.md +0 -150
  403. package/.kilocode/workflows/status.md +0 -91
  404. package/.kilocode/workflows/validate.md +0 -289
  405. package/.kilocode/workflows/verify.md +0 -270
  406. package/.opencode/commands/dev.md +0 -345
  407. package/.opencode/commands/plan.md +0 -566
  408. package/.opencode/commands/premerge.md +0 -186
  409. package/.opencode/commands/research.md +0 -42
  410. package/.opencode/commands/review.md +0 -451
  411. package/.opencode/commands/rollback.md +0 -721
  412. package/.opencode/commands/ship.md +0 -213
  413. package/.opencode/commands/sonarcloud.md +0 -149
  414. package/.opencode/commands/status.md +0 -90
  415. package/.opencode/commands/validate.md +0 -288
  416. package/.opencode/commands/verify.md +0 -269
  417. package/.roo/commands/dev.md +0 -346
  418. package/.roo/commands/plan.md +0 -567
  419. package/.roo/commands/premerge.md +0 -187
  420. package/.roo/commands/research.md +0 -43
  421. package/.roo/commands/review.md +0 -452
  422. package/.roo/commands/rollback.md +0 -722
  423. package/.roo/commands/ship.md +0 -214
  424. package/.roo/commands/sonarcloud.md +0 -150
  425. package/.roo/commands/status.md +0 -91
  426. package/.roo/commands/validate.md +0 -289
  427. package/.roo/commands/verify.md +0 -270
  428. package/docs/BEADS_GITHUB_SYNC.md +0 -281
  429. package/docs/GREPTILE_SETUP.md +0 -400
  430. package/docs/MANUAL_REVIEW_GUIDE.md +0 -106
  431. package/docs/SETUP.md +0 -663
  432. package/docs/VALIDATION.md +0 -363
  433. package/lib/agents/cline.plugin.json +0 -29
  434. package/lib/agents/copilot.plugin.json +0 -24
  435. package/lib/agents/kilocode.plugin.json +0 -22
  436. package/lib/agents/opencode.plugin.json +0 -23
  437. package/lib/agents/roo.plugin.json +0 -30
  438. package/lib/beads-bootstrap.js +0 -225
  439. package/lib/beads-health-check.js +0 -188
  440. package/lib/commands/commands-reset.js +0 -147
  441. package/opencode.json +0 -67
  442. package/scripts/beads-context.test.js +0 -584
  443. package/scripts/github-beads-sync/comment.mjs +0 -64
  444. package/scripts/github-beads-sync/config.mjs +0 -148
  445. package/scripts/github-beads-sync/github-api.mjs +0 -131
  446. package/scripts/github-beads-sync/index.mjs +0 -356
  447. package/scripts/github-beads-sync/label-mapper.mjs +0 -54
  448. package/scripts/github-beads-sync/mapping.mjs +0 -132
  449. package/scripts/github-beads-sync/reverse-sync-cli.mjs +0 -31
  450. package/scripts/github-beads-sync/reverse-sync.mjs +0 -162
  451. package/scripts/github-beads-sync/run-bd.mjs +0 -161
  452. package/scripts/github-beads-sync/sanitize.mjs +0 -121
  453. package/scripts/github-beads-sync.config.json +0 -26
  454. package/scripts/sync-commands.js +0 -600
@@ -0,0 +1,281 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+ const YAML = require('yaml');
6
+
7
+ const PROTECTED_PATH_CATEGORY_IDS = [
8
+ 'forge_core',
9
+ 'user_protocol',
10
+ 'generated_artifacts',
11
+ 'append_only_logs',
12
+ 'secrets',
13
+ 'beads_state',
14
+ 'immutable',
15
+ ];
16
+
17
+ const ALLOWED_MODES = new Set([
18
+ 'checksum-verified',
19
+ 'cli-only',
20
+ 'ci-blocked',
21
+ 'runtime-only',
22
+ 'secret-scan-blocked',
23
+ 'bd-cli-only',
24
+ 'tool-owned',
25
+ ]);
26
+
27
+ const EXPECTED_CATEGORY_MODES = {
28
+ forge_core: 'checksum-verified',
29
+ user_protocol: 'cli-only',
30
+ generated_artifacts: 'ci-blocked',
31
+ append_only_logs: 'runtime-only',
32
+ secrets: 'secret-scan-blocked',
33
+ beads_state: 'bd-cli-only',
34
+ immutable: 'tool-owned',
35
+ };
36
+
37
+ const DEFAULT_PROTECTED_PATH_MANIFEST_PATH = path.resolve(__dirname, '..', '.forge', 'protected-paths.yaml');
38
+
39
+ const PROTECTED_PATH_HARNESS_ENFORCEMENT = {
40
+ claude: {
41
+ status: 'native',
42
+ surface: 'Claude PreToolUse hook for write/edit tools',
43
+ configPath: '.claude/settings.json',
44
+ delivered: 'Rendered by lib/hook-renderer.js into .claude/settings.json; the installed .forge/hooks/forge-native-hook.js adapter receives tool_input.file_path and denies protected writes with a repair hint.',
45
+ evidenceRequired: ['hook receives target path', 'blocked write returns repair hint'],
46
+ },
47
+ cursor: {
48
+ status: 'fallback',
49
+ surface: 'Forge CLI/pre-commit check or file-watcher fallback',
50
+ configPath: 'lefthook.yml',
51
+ evidenceRequired: ['staged file check', 'watcher proof before native hook claim'],
52
+ knownIssue: 'No verified Cursor hook surface; keep Cursor protected-path enforcement on Forge CLI/pre-commit fallback until proven.',
53
+ },
54
+ codex: {
55
+ status: 'native-hook-contract',
56
+ surface: 'Codex lifecycle hook for write/edit tools',
57
+ configPath: '.codex/config.toml',
58
+ evidenceRequired: ['hook receives target path', 'blocked write returns repair hint'],
59
+ },
60
+ };
61
+
62
+ function clone(value) {
63
+ return structuredClone(value);
64
+ }
65
+
66
+ function getDefaultProtectedPathManifest() {
67
+ return loadProtectedPathManifest(DEFAULT_PROTECTED_PATH_MANIFEST_PATH);
68
+ }
69
+
70
+ function getProtectedPathHarnessEnforcement() {
71
+ return clone(PROTECTED_PATH_HARNESS_ENFORCEMENT);
72
+ }
73
+
74
+ function validateProtectedPathManifest(manifest) {
75
+ const errors = [];
76
+ if (!manifest || typeof manifest !== 'object') {
77
+ return { ok: false, errors: ['Manifest must be an object.'] };
78
+ }
79
+ if (!isSupportedManifestKind(manifest)) {
80
+ errors.push('kind must be ProtectedPathManifest or forge.protectedPaths.');
81
+ }
82
+ if (!isSupportedManifestVersion(manifest)) {
83
+ errors.push('schemaVersion/version must be 1.0.0 or 1.');
84
+ }
85
+
86
+ if (isLegacyProtectedPathsManifest(manifest)) {
87
+ if (!Array.isArray(manifest.paths) || manifest.paths.length === 0) {
88
+ errors.push('Legacy forge.protectedPaths manifest must declare at least one path.');
89
+ }
90
+ return { ok: errors.length === 0, errors };
91
+ }
92
+
93
+ if (!Array.isArray(manifest.categories)) errors.push('categories must be an array.');
94
+
95
+ const categories = Array.isArray(manifest.categories) ? manifest.categories : [];
96
+ const ids = categories.map(category => category?.id).filter(Boolean);
97
+ for (const requiredId of PROTECTED_PATH_CATEGORY_IDS) {
98
+ if (!ids.includes(requiredId)) errors.push(`Missing required protected path category: ${requiredId}.`);
99
+ }
100
+ const duplicates = ids.filter((id, index) => ids.indexOf(id) !== index);
101
+ if (duplicates.length > 0) errors.push(`Duplicate protected path categories: ${[...new Set(duplicates)].join(', ')}.`);
102
+
103
+ for (const category of categories) {
104
+ validateCategory(category, errors);
105
+ }
106
+ validateLegacySurfaceCoverage(manifest, categories, errors);
107
+
108
+ return { ok: errors.length === 0, errors };
109
+ }
110
+
111
+ function validateCategory(category, errors) {
112
+ if (!category || typeof category !== 'object') {
113
+ errors.push('Each category must be an object.');
114
+ return;
115
+ }
116
+ if (!category.id || typeof category.id !== 'string') errors.push('Category id must be a string.');
117
+ if (!ALLOWED_MODES.has(category.mode)) {
118
+ const modeLabel = category.mode ?? '<missing>';
119
+ errors.push(`Invalid mode for ${category.id || 'unknown'}: ${modeLabel}.`);
120
+ }
121
+ const expectedMode = EXPECTED_CATEGORY_MODES[category.id];
122
+ if (expectedMode && category.mode !== expectedMode) {
123
+ errors.push(`Category ${category.id} must use mode ${expectedMode}.`);
124
+ }
125
+ if (!Array.isArray(category.paths) || category.paths.length === 0) {
126
+ errors.push(`Category ${category.id || 'unknown'} must declare at least one path.`);
127
+ }
128
+ if (!category.ownerSurface || typeof category.ownerSurface !== 'string') {
129
+ errors.push(`Category ${category.id || 'unknown'} must declare ownerSurface.`);
130
+ }
131
+ if (!category.repairHint || typeof category.repairHint !== 'string') {
132
+ errors.push(`Category ${category.id || 'unknown'} must declare repairHint.`);
133
+ }
134
+ }
135
+
136
+ function isSupportedManifestKind(manifest) {
137
+ return manifest.kind === 'ProtectedPathManifest' || manifest.kind === 'forge.protectedPaths';
138
+ }
139
+
140
+ function isSupportedManifestVersion(manifest) {
141
+ if (manifest.kind === 'ProtectedPathManifest') {
142
+ return manifest.schemaVersion === '1.0.0';
143
+ }
144
+ if (manifest.kind === 'forge.protectedPaths') {
145
+ return manifest.version === '1' || manifest.version === 1;
146
+ }
147
+ return false;
148
+ }
149
+
150
+ function isLegacyProtectedPathsManifest(manifest) {
151
+ return manifest.kind === 'forge.protectedPaths';
152
+ }
153
+
154
+ function loadProtectedPathManifest(filePath) {
155
+ try {
156
+ const content = fs.readFileSync(filePath, 'utf8');
157
+ return YAML.parse(content);
158
+ } catch (error) {
159
+ const message = error instanceof Error ? error.message : String(error);
160
+ const wrapped = new Error(`Failed to load protected path manifest at ${filePath}: ${message}`, { cause: error });
161
+ if (error && typeof error === 'object' && 'code' in error) {
162
+ wrapped.code = error.code;
163
+ }
164
+ throw wrapped;
165
+ }
166
+ }
167
+
168
+ function buildProtectedPathManifestEvidence(manifest = getDefaultProtectedPathManifest()) {
169
+ let categoryIds = [];
170
+ if (Array.isArray(manifest.categories)) {
171
+ categoryIds = manifest.categories.map(category => category?.id).filter(Boolean);
172
+ }
173
+ return {
174
+ schemaVersion: manifest.schemaVersion,
175
+ kind: 'forge.protectedPathManifest',
176
+ manifestPath: manifest.manifestPath || '.forge/protected-paths.yaml',
177
+ categoryIds,
178
+ categories: manifest.categories,
179
+ harnessEnforcement: getProtectedPathHarnessEnforcement(),
180
+ validation: validateProtectedPathManifest(manifest),
181
+ };
182
+ }
183
+
184
+ function validateLegacySurfaceCoverage(manifest, categories, errors) {
185
+ if (!manifest.surfaces) return;
186
+ if (manifest.surfacesDeprecated !== true) {
187
+ errors.push('surfaces must be marked surfacesDeprecated: true when categories are authoritative.');
188
+ }
189
+ if (!manifest.surfacesMigrationNote || typeof manifest.surfacesMigrationNote !== 'string') {
190
+ errors.push('surfacesMigrationNote must describe the legacy surfaces migration.');
191
+ }
192
+ const categoryPaths = categories.flatMap(category => (Array.isArray(category?.paths) ? category.paths : []));
193
+ for (const [surfaceId, surface] of Object.entries(manifest.surfaces)) {
194
+ if (!Array.isArray(surface?.examples) || surface.examples.length === 0) {
195
+ errors.push(`Legacy surface ${surfaceId} must declare examples for category coverage.`);
196
+ continue;
197
+ }
198
+ for (const example of surface.examples) {
199
+ if (!categoryPaths.some(pattern => pathPatternCoversExample(pattern, example))) {
200
+ errors.push(`Legacy surface ${surfaceId} example is not covered by category paths: ${example}.`);
201
+ }
202
+ }
203
+ }
204
+ }
205
+
206
+ function pathPatternCoversExample(pattern, example) {
207
+ const normalizedPattern = normalizeManifestPath(pattern);
208
+ const normalizedExample = normalizeManifestPath(example);
209
+ if (normalizedPattern === normalizedExample) return true;
210
+ if (normalizedPattern.endsWith('/**')) {
211
+ const prefix = normalizedPattern.slice(0, -3);
212
+ return normalizedExample === prefix || normalizedExample.startsWith(`${prefix}/`);
213
+ }
214
+ return wildcardPathMatches(normalizedPattern, normalizedExample);
215
+ }
216
+
217
+ function normalizeManifestPath(value) {
218
+ let normalized = String(value || '').replaceAll('\\', '/');
219
+ if (normalized.startsWith('./')) normalized = normalized.slice(2);
220
+ while (normalized.endsWith('/')) normalized = normalized.slice(0, -1);
221
+ return normalized;
222
+ }
223
+
224
+ function wildcardPathMatches(pattern, candidate) {
225
+ return pathSegmentsMatch(pattern.split('/'), candidate.split('/'), 0, 0, new Map());
226
+ }
227
+
228
+ function pathSegmentsMatch(patternParts, candidateParts, patternIndex, candidateIndex, memo) {
229
+ const memoKey = `${patternIndex}:${candidateIndex}`;
230
+ if (memo.has(memoKey)) return memo.get(memoKey);
231
+ if (patternIndex === patternParts.length) return candidateIndex === candidateParts.length;
232
+
233
+ const patternPart = patternParts[patternIndex];
234
+ let matches;
235
+ if (patternPart === '**') {
236
+ matches =
237
+ pathSegmentsMatch(patternParts, candidateParts, patternIndex + 1, candidateIndex, memo) ||
238
+ (candidateIndex < candidateParts.length && pathSegmentsMatch(patternParts, candidateParts, patternIndex, candidateIndex + 1, memo));
239
+ } else {
240
+ matches =
241
+ candidateIndex < candidateParts.length &&
242
+ segmentPatternMatches(patternPart, candidateParts[candidateIndex]) &&
243
+ pathSegmentsMatch(patternParts, candidateParts, patternIndex + 1, candidateIndex + 1, memo);
244
+ }
245
+ memo.set(memoKey, matches);
246
+ return matches;
247
+ }
248
+
249
+ function segmentPatternMatches(pattern, candidate) {
250
+ let patternIndex = 0;
251
+ let candidateIndex = 0;
252
+ let starIndex = -1;
253
+ let candidateCheckpoint = 0;
254
+ while (candidateIndex < candidate.length) {
255
+ if (pattern[patternIndex] === candidate[candidateIndex]) {
256
+ patternIndex += 1;
257
+ candidateIndex += 1;
258
+ } else if (pattern[patternIndex] === '*') {
259
+ starIndex = patternIndex;
260
+ candidateCheckpoint = candidateIndex;
261
+ patternIndex += 1;
262
+ } else if (starIndex === -1) {
263
+ return false;
264
+ } else {
265
+ patternIndex = starIndex + 1;
266
+ candidateCheckpoint += 1;
267
+ candidateIndex = candidateCheckpoint;
268
+ }
269
+ }
270
+ while (pattern[patternIndex] === '*') patternIndex += 1;
271
+ return patternIndex === pattern.length;
272
+ }
273
+
274
+ module.exports = {
275
+ PROTECTED_PATH_CATEGORY_IDS,
276
+ buildProtectedPathManifestEvidence,
277
+ getDefaultProtectedPathManifest,
278
+ getProtectedPathHarnessEnforcement,
279
+ loadProtectedPathManifest,
280
+ validateProtectedPathManifest,
281
+ };
@@ -0,0 +1,387 @@
1
+ const fs = require('node:fs');
2
+ const path = require('node:path');
3
+ const { execFileSync } = require('node:child_process');
4
+ const { redact } = require('./audit-evidence');
5
+
6
+ function normalizeRepoPath(filePath) {
7
+ return String(filePath || '')
8
+ .replace(/\\/g, '/')
9
+ .replace(/^\.\//, '')
10
+ .replace(/\/+/g, '/');
11
+ }
12
+
13
+ function startsWithAny(filePath, prefixes) {
14
+ return prefixes.some(prefix => filePath === prefix || filePath.startsWith(`${prefix}/`));
15
+ }
16
+
17
+ const PROTECTED_SURFACES = [
18
+ {
19
+ id: 'immutable',
20
+ label: 'Immutable runtime paths',
21
+ matches: filePath => startsWithAny(filePath, ['.git', '.hg', '.svn']),
22
+ repairHint: 'Do not edit immutable VCS/runtime internals directly; use git or the owning tool command.',
23
+ },
24
+ {
25
+ id: 'secrets',
26
+ label: 'Secrets',
27
+ matches: filePath =>
28
+ filePath === '.env' ||
29
+ filePath.startsWith('.env.') ||
30
+ filePath.endsWith('/.env') ||
31
+ filePath.includes('/.env.') ||
32
+ /(^|\/)(secrets?|credentials?)(\.|\/|$)/i.test(filePath),
33
+ repairHint: 'Use the project secret manager or local environment setup command; never hand-edit or commit secrets.',
34
+ },
35
+ {
36
+ id: 'append_only_logs',
37
+ label: 'Append-only logs',
38
+ matches: filePath =>
39
+ [
40
+ '.forge/log.jsonl',
41
+ '.forge/audit.log',
42
+ '.forge/agent-log.ndjson',
43
+ '.beads/interactions.jsonl',
44
+ ].includes(filePath),
45
+ repairHint: 'append-only logs must be written by the Forge or Beads audit writer; do not rewrite existing log content.',
46
+ },
47
+ {
48
+ id: 'beads_state',
49
+ label: 'Beads state',
50
+ matches: filePath => startsWithAny(filePath, ['.beads']),
51
+ repairHint: 'Use bd or Forge issue commands such as bd update, bd close, forge ready, or forge close.',
52
+ },
53
+ {
54
+ id: 'forge_config',
55
+ label: 'Forge config',
56
+ matches: filePath =>
57
+ filePath === '.forge/config.yaml' ||
58
+ filePath === '.forge/config.yml' ||
59
+ filePath === '.forge/config.json' ||
60
+ filePath === '.forge/protected-paths.yaml',
61
+ repairHint: 'Use Forge setup/config commands or the protected Forge API writer for .forge configuration.',
62
+ },
63
+ {
64
+ id: 'extension_manifests',
65
+ label: 'Extension manifests',
66
+ matches: filePath =>
67
+ filePath === '.github/PLUGIN_TEMPLATE.json' ||
68
+ /^\.forge\/extensions\/[^/]+\/manifest\.json$/i.test(filePath) ||
69
+ /^plugins\/[^/]+\/(?:plugin|extension|manifest)\.json$/i.test(filePath),
70
+ repairHint: 'Use the Forge extension/plugin manager so manifests and trust metadata stay consistent.',
71
+ },
72
+ {
73
+ id: 'lockfiles',
74
+ label: 'Lockfiles',
75
+ matches: filePath =>
76
+ /(^|\/)(bun\.lock|package-lock\.json|pnpm-lock\.yaml|yarn\.lock|requirements\.lock|poetry\.lock|Cargo\.lock|go\.sum)$/.test(filePath) ||
77
+ filePath === '.forge/extensions.lock',
78
+ repairHint: 'Regenerate lockfiles through the package manager or Forge extension installer instead of editing them by hand.',
79
+ },
80
+ {
81
+ id: 'workflows',
82
+ label: 'Workflow definitions',
83
+ matches: filePath =>
84
+ startsWithAny(filePath, ['.github/workflows', '.forge/hooks']) ||
85
+ filePath === 'lefthook.yml',
86
+ repairHint: 'Use Forge workflow/setup commands so workflow files and hooks remain in sync.',
87
+ },
88
+ {
89
+ id: 'generated_harness',
90
+ label: 'Generated harness files',
91
+ matches: filePath =>
92
+ startsWithAny(filePath, [
93
+ '.agents/skills',
94
+ '.claude/skills',
95
+ '.codex/skills',
96
+ '.cursor/rules',
97
+ ]) ||
98
+ ['AGENTS.md', 'CLAUDE.md', '.mcp.json'].includes(filePath),
99
+ repairHint: 'Regenerate harness files with forge setup or the owning Forge API surface instead of direct edits.',
100
+ },
101
+ {
102
+ id: 'memory_projection',
103
+ label: 'Memory projection files',
104
+ matches: filePath =>
105
+ startsWithAny(filePath, ['docs/sessions', 'docs/memory', '.forge/memory']) ||
106
+ filePath.endsWith('/memory.md') ||
107
+ filePath.endsWith('/MEMORY.md'),
108
+ repairHint: 'Use the Forge memory projection command or append-only session writer; do not edit projections directly.',
109
+ },
110
+ ];
111
+
112
+ function classifyProtectedPath(filePath) {
113
+ const normalized = normalizeRepoPath(filePath);
114
+ if (!normalized) return null;
115
+ const surface = PROTECTED_SURFACES.find(candidate => candidate.matches(normalized));
116
+ if (!surface) return null;
117
+ return {
118
+ path: normalized,
119
+ surface: surface.id,
120
+ label: surface.label,
121
+ repairHint: surface.repairHint,
122
+ };
123
+ }
124
+
125
+ function resolveRepoRelativePath(projectRoot, filePath) {
126
+ const root = path.resolve(projectRoot);
127
+ const target = path.resolve(root, filePath);
128
+ if (!pathStaysInsideRoot(root, target)) {
129
+ return {
130
+ insideRoot: false,
131
+ root,
132
+ target,
133
+ relativePath: normalizeRepoPath(filePath),
134
+ };
135
+ }
136
+ return {
137
+ insideRoot: true,
138
+ root,
139
+ target,
140
+ relativePath: normalizeRepoPath(path.relative(root, target)),
141
+ };
142
+ }
143
+
144
+ function pathStaysInsideRoot(root, candidate) {
145
+ return candidate === root || candidate.startsWith(`${root}${path.sep}`);
146
+ }
147
+
148
+ function lstatIfPresent(candidate) {
149
+ try {
150
+ return fs.lstatSync(candidate);
151
+ } catch (error) {
152
+ if (error.code === 'ENOENT') return null;
153
+ throw error;
154
+ }
155
+ }
156
+
157
+ function nearestExistingPath(candidate) {
158
+ let current = candidate;
159
+ while (!lstatIfPresent(current)) {
160
+ const parent = path.dirname(current);
161
+ if (parent === current) return current;
162
+ current = parent;
163
+ }
164
+ return current;
165
+ }
166
+
167
+ function assertNoAncestorSymlinkEscape(root, target) {
168
+ const realRoot = fs.realpathSync(root);
169
+ const existing = nearestExistingPath(path.dirname(target));
170
+ const existingStat = lstatIfPresent(existing);
171
+ if (existing !== root && existingStat?.isSymbolicLink()) {
172
+ return {
173
+ allowed: false,
174
+ decision: 'blocked',
175
+ reason: `Write ancestor is a symlink: ${existing}`,
176
+ repairHint: 'Remove the symlink escape or write through a real directory inside the project root.',
177
+ };
178
+ }
179
+
180
+ const realExisting = fs.realpathSync(existing);
181
+ if (!pathStaysInsideRoot(realRoot, realExisting)) {
182
+ return {
183
+ allowed: false,
184
+ decision: 'blocked',
185
+ reason: `Write ancestor resolves outside project root: ${existing}`,
186
+ repairHint: 'Remove the symlink escape or write through a real directory inside the project root.',
187
+ };
188
+ }
189
+ return null;
190
+ }
191
+
192
+ function assertNoSymlinkEscape(root, target) {
193
+ const parent = path.dirname(target);
194
+ const realRoot = fs.realpathSync(root);
195
+ const existingParent = fs.existsSync(parent) ? fs.realpathSync(parent) : parent;
196
+ if (!pathStaysInsideRoot(realRoot, existingParent)) {
197
+ return {
198
+ allowed: false,
199
+ decision: 'blocked',
200
+ reason: `Write parent resolves outside project root: ${parent}`,
201
+ repairHint: 'Remove the symlink escape or write through a real directory inside the project root.',
202
+ };
203
+ }
204
+
205
+ try {
206
+ if (fs.lstatSync(target).isSymbolicLink()) {
207
+ return {
208
+ allowed: false,
209
+ decision: 'blocked',
210
+ reason: `Write target is a symlink: ${target}`,
211
+ repairHint: 'Replace the symlink with a real file through the owning Forge API before writing.',
212
+ };
213
+ }
214
+ } catch (error) {
215
+ if (error.code !== 'ENOENT') {
216
+ throw error;
217
+ }
218
+ }
219
+
220
+ return null;
221
+ }
222
+
223
+ function assertProtectedWriteAllowed(filePath, options = {}) {
224
+ const classification = classifyProtectedPath(filePath);
225
+ const actor = options.actor || process.env.FORGE_ACTOR || process.env.USER || process.env.USERNAME || 'unknown';
226
+ const operation = options.operation || 'write';
227
+
228
+ if (!classification) {
229
+ return {
230
+ allowed: true,
231
+ decision: 'allowed',
232
+ actor,
233
+ path: normalizeRepoPath(filePath),
234
+ operation,
235
+ requiredSurface: null,
236
+ reason: 'Path is not protected.',
237
+ repairHint: null,
238
+ };
239
+ }
240
+
241
+ const requiredSurface = classification.surface;
242
+ const declaredSurface = options.surface || options.requiredSurface;
243
+ const allowed = Boolean(options.viaForgeApi && declaredSurface === requiredSurface);
244
+
245
+ return {
246
+ allowed,
247
+ decision: allowed ? 'allowed' : 'blocked',
248
+ actor,
249
+ path: classification.path,
250
+ operation,
251
+ requiredSurface,
252
+ surfaceLabel: classification.label,
253
+ declaredSurface: declaredSurface || null,
254
+ reason: allowed
255
+ ? `Forge API write declared required surface: ${requiredSurface}.`
256
+ : `Direct edits to protected ${requiredSurface} paths are not allowed.`,
257
+ repairHint: classification.repairHint,
258
+ };
259
+ }
260
+
261
+ function writeProtectedFile(projectRoot, filePath, content, options = {}) {
262
+ const resolved = resolveRepoRelativePath(projectRoot, filePath);
263
+ if (!resolved.insideRoot) {
264
+ return {
265
+ allowed: false,
266
+ decision: 'blocked',
267
+ actor: options.actor || process.env.FORGE_ACTOR || process.env.USER || process.env.USERNAME || 'unknown',
268
+ path: resolved.relativePath,
269
+ operation: options.operation || 'write',
270
+ requiredSurface: null,
271
+ reason: `Write path escapes project root: ${filePath}`,
272
+ repairHint: 'Write only repo-relative paths inside the project root.',
273
+ };
274
+ }
275
+
276
+ const decision = assertProtectedWriteAllowed(resolved.relativePath, {
277
+ ...options,
278
+ operation: options.operation || 'write',
279
+ });
280
+ if (!decision.allowed) return decision;
281
+
282
+ const ancestorEscape = assertNoAncestorSymlinkEscape(resolved.root, resolved.target);
283
+ if (ancestorEscape) {
284
+ return {
285
+ ...decision,
286
+ ...ancestorEscape,
287
+ };
288
+ }
289
+
290
+ fs.mkdirSync(path.dirname(resolved.target), { recursive: true });
291
+ const symlinkEscape = assertNoSymlinkEscape(resolved.root, resolved.target);
292
+ if (symlinkEscape) {
293
+ return {
294
+ ...decision,
295
+ ...symlinkEscape,
296
+ };
297
+ }
298
+
299
+ fs.writeFileSync(resolved.target, content, options.encoding || 'utf8');
300
+ return { ...decision, fullPath: resolved.target };
301
+ }
302
+
303
+ function buildProtectedStateAuditEvent(decision) {
304
+ if (!decision || typeof decision !== 'object') {
305
+ throw new TypeError('Protected state decision is required');
306
+ }
307
+
308
+ const event = {
309
+ kind: 'protected_state_write',
310
+ actor: decision.actor || 'unknown',
311
+ path: decision.path,
312
+ decision: decision.decision,
313
+ requiredSurface: decision.requiredSurface,
314
+ declaredSurface: decision.declaredSurface || null,
315
+ operation: decision.operation || 'write',
316
+ reason: decision.reason,
317
+ repairHint: decision.repairHint,
318
+ metadata: {
319
+ actor: decision.actor || 'unknown',
320
+ path: decision.path,
321
+ operation: decision.operation || 'write',
322
+ requiredSurface: decision.requiredSurface,
323
+ declaredSurface: decision.declaredSurface || null,
324
+ decision: decision.decision,
325
+ reason: decision.reason,
326
+ repairHint: decision.repairHint,
327
+ },
328
+ };
329
+
330
+ return redact(event);
331
+ }
332
+
333
+ function recordProtectedStateAuditEvent(decision, options = {}) {
334
+ const event = buildProtectedStateAuditEvent(decision);
335
+ const runCommand = options.runCommand || execFileSync;
336
+ try {
337
+ const args = [
338
+ 'audit',
339
+ 'record',
340
+ '--json',
341
+ '--kind',
342
+ 'protected_state_write',
343
+ '--model',
344
+ 'forge-protected-state',
345
+ '--prompt',
346
+ JSON.stringify({
347
+ actor: event.actor,
348
+ path: event.path,
349
+ operation: event.operation,
350
+ }),
351
+ '--response',
352
+ JSON.stringify({
353
+ decision: event.decision,
354
+ requiredSurface: event.requiredSurface,
355
+ repairHint: event.repairHint,
356
+ }),
357
+ ];
358
+
359
+ if (event.metadata) {
360
+ args.push('--meta-json', JSON.stringify(event.metadata));
361
+ }
362
+
363
+ const output = runCommand('bd', args, {
364
+ cwd: options.cwd || process.cwd(),
365
+ encoding: 'utf8',
366
+ stdio: ['ignore', 'pipe', 'pipe'],
367
+ timeout: options.timeoutMs || 5000,
368
+ });
369
+ return { success: true, event, output };
370
+ } catch (error) {
371
+ return { success: false, event, error: error.message };
372
+ }
373
+ }
374
+
375
+ module.exports = {
376
+ PROTECTED_SURFACES,
377
+ normalizeRepoPath,
378
+ resolveRepoRelativePath,
379
+ lstatIfPresent,
380
+ assertNoAncestorSymlinkEscape,
381
+ assertNoSymlinkEscape,
382
+ classifyProtectedPath,
383
+ assertProtectedWriteAllowed,
384
+ writeProtectedFile,
385
+ buildProtectedStateAuditEvent,
386
+ recordProtectedStateAuditEvent,
387
+ };