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,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
+ };