forge-workflow 0.0.10 → 0.1.0-beta.3

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 (468) 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 +82 -5
  5. package/.forge/hooks/forge-native-hook.js +431 -0
  6. package/.forge/protected-paths.yaml +157 -0
  7. package/AGENTS.md +151 -61
  8. package/CHANGELOG.md +709 -0
  9. package/CLAUDE.md +9 -118
  10. package/QUICKSTART.md +175 -0
  11. package/README.md +275 -365
  12. package/bin/forge-cmd.js +120 -9
  13. package/bin/forge-preflight.js +26 -5
  14. package/bin/forge.js +532 -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 +121 -0
  29. package/docs/guides/SUPPORT.md +190 -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 +214 -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 +155 -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/activation/ensure-forge-home.js +135 -0
  67. package/lib/adapter-cli.js +307 -0
  68. package/lib/adapters/beads-issue-adapter.js +127 -0
  69. package/lib/adapters/beads-kernel-compat.js +1109 -0
  70. package/lib/adapters/greptile-review-adapter.js +141 -0
  71. package/lib/adapters/kernel-issue-adapter.js +101 -0
  72. package/lib/adapters/pr-state-adapter.js +484 -0
  73. package/lib/adoption-profiles.js +139 -0
  74. package/lib/agents/README.md +2 -6
  75. package/lib/agents/claude.plugin.json +3 -8
  76. package/lib/agents/codex.plugin.json +9 -1
  77. package/lib/agents/cursor.plugin.json +2 -6
  78. package/lib/agents/hermes.plugin.json +22 -0
  79. package/lib/agents-config.js +39 -1236
  80. package/lib/audit-evidence.js +282 -0
  81. package/lib/beads-detect.js +60 -0
  82. package/lib/beads-nudge.js +91 -0
  83. package/lib/beads-setup.js +121 -0
  84. package/lib/beads-sync-scaffold.js +25 -101
  85. package/lib/codex-skills.js +51 -1
  86. package/lib/commands/_aliases.js +248 -0
  87. package/lib/commands/_issue.js +780 -77
  88. package/lib/commands/_manifest.js +93 -0
  89. package/lib/commands/_registry.js +99 -34
  90. package/lib/commands/_resolve-command-opts.js +230 -0
  91. package/lib/commands/_serve-security.js +270 -0
  92. package/lib/commands/adapter.js +12 -0
  93. package/lib/commands/add.js +118 -0
  94. package/lib/commands/audit.js +70 -0
  95. package/lib/commands/blocked.js +5 -0
  96. package/lib/commands/board.js +64 -0
  97. package/lib/commands/claim.js +21 -2
  98. package/lib/commands/claims.js +7 -0
  99. package/lib/commands/clean.js +485 -75
  100. package/lib/commands/close.js +2 -2
  101. package/lib/commands/comment.js +5 -0
  102. package/lib/commands/control.js +148 -0
  103. package/lib/commands/create.js +2 -2
  104. package/lib/commands/dev.js +185 -7
  105. package/lib/commands/doc-gate.js +336 -0
  106. package/lib/commands/doctor.js +156 -0
  107. package/lib/commands/explain.js +15 -0
  108. package/lib/commands/export.js +237 -0
  109. package/lib/commands/gate.js +209 -0
  110. package/lib/commands/hooks.js +377 -0
  111. package/lib/commands/inbox.js +118 -0
  112. package/lib/commands/init.js +604 -0
  113. package/lib/commands/insights.js +79 -0
  114. package/lib/commands/issue.js +12 -1
  115. package/lib/commands/issues.js +17 -0
  116. package/lib/commands/lint.js +5 -0
  117. package/lib/commands/list.js +2 -2
  118. package/lib/commands/memory.js +81 -0
  119. package/lib/commands/merge.js +312 -0
  120. package/lib/commands/migrate.js +362 -0
  121. package/lib/commands/new.js +12 -0
  122. package/lib/commands/options.js +241 -0
  123. package/lib/commands/orient.js +13 -0
  124. package/lib/commands/orphans.js +5 -0
  125. package/lib/commands/patch.js +67 -0
  126. package/lib/commands/plan.js +481 -29
  127. package/lib/commands/pr.js +88 -0
  128. package/lib/commands/preflight.js +211 -0
  129. package/lib/commands/prime.js +13 -0
  130. package/lib/commands/push.js +135 -2
  131. package/lib/commands/ready.js +2 -2
  132. package/lib/commands/recall.js +171 -0
  133. package/lib/commands/recap.js +75 -0
  134. package/lib/commands/recommend.js +0 -1
  135. package/lib/commands/release.js +104 -0
  136. package/lib/commands/remember.js +140 -0
  137. package/lib/commands/role.js +99 -0
  138. package/lib/commands/serve.js +581 -0
  139. package/lib/commands/setup.js +900 -971
  140. package/lib/commands/shepherd.js +501 -0
  141. package/lib/commands/ship.js +59 -1
  142. package/lib/commands/show.js +2 -2
  143. package/lib/commands/stage.js +192 -0
  144. package/lib/commands/stale.js +5 -0
  145. package/lib/commands/status.js +158 -21
  146. package/lib/commands/sync.js +34 -46
  147. package/lib/commands/team.js +4 -1
  148. package/lib/commands/test.js +43 -27
  149. package/lib/commands/update.js +2 -2
  150. package/lib/commands/upgrade.js +47 -0
  151. package/lib/commands/validate.js +43 -18
  152. package/lib/commands/worktree.js +362 -99
  153. package/lib/config-writer.js +202 -0
  154. package/lib/control-plane.js +236 -0
  155. package/lib/core/runtime-graph.js +977 -0
  156. package/lib/dep-guard/keyword-ripple.js +2 -2
  157. package/lib/deprecated-sync-cleanup.js +362 -0
  158. package/lib/detect-agent.js +2 -28
  159. package/lib/detect-worktree.js +35 -9
  160. package/lib/doc-gate/declaration.js +177 -0
  161. package/lib/doc-gate/detect.js +289 -0
  162. package/lib/doc-gate/gate.js +375 -0
  163. package/lib/doc-gate/okf-config.js +128 -0
  164. package/lib/doc-gate/okf.js +429 -0
  165. package/lib/docs-command.js +1161 -6
  166. package/lib/forge-issues.js +382 -11
  167. package/lib/forge-lock.js +262 -0
  168. package/lib/gate-events.js +192 -0
  169. package/lib/global-flags.js +104 -0
  170. package/lib/greptile-match.js +7 -63
  171. package/lib/grounding/context-events.js +230 -0
  172. package/lib/grounding/read-first.js +112 -0
  173. package/lib/harness-capability-matrix.js +380 -0
  174. package/lib/hook-global-installer.js +347 -0
  175. package/lib/hook-renderer.js +541 -0
  176. package/lib/inbox.js +391 -0
  177. package/lib/insights.js +397 -0
  178. package/lib/issue-adapter.js +156 -0
  179. package/lib/issue-backend.js +145 -0
  180. package/lib/issue-render.js +220 -0
  181. package/lib/kernel/backing-issue.js +311 -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/owned-kernel.js +43 -0
  192. package/lib/kernel/planning-buckets-schema.js +109 -0
  193. package/lib/kernel/projection-jsonl-writer.js +450 -0
  194. package/lib/kernel/readiness-model.js +329 -0
  195. package/lib/kernel/schema.js +356 -0
  196. package/lib/kernel/sqlite-driver.js +2540 -0
  197. package/lib/kernel/taxonomy-validator.js +394 -0
  198. package/lib/lefthook-check.js +3 -2
  199. package/lib/lefthook-wiring.js +413 -0
  200. package/lib/mcp-config-renderer.js +288 -0
  201. package/lib/memory/graphiti-mcp.js +106 -0
  202. package/lib/memory/router.js +387 -0
  203. package/lib/memory/typed-api.js +102 -0
  204. package/lib/memory-digest.js +195 -0
  205. package/lib/merge-rules.js +395 -0
  206. package/lib/migrate-dry-run.js +466 -0
  207. package/lib/orientation.js +863 -0
  208. package/lib/package-manager-remediation.js +103 -0
  209. package/lib/package-root.js +381 -0
  210. package/lib/patch-intent.js +890 -0
  211. package/lib/plugin-catalog.js +3 -4
  212. package/lib/plugin-manager.js +0 -5
  213. package/lib/pr-bundle.js +186 -0
  214. package/lib/pr-monitor/auto-actions.js +175 -0
  215. package/lib/pr-monitor/differ.js +195 -0
  216. package/lib/pr-monitor/digest.js +206 -0
  217. package/lib/pr-monitor/events.js +0 -0
  218. package/lib/pr-monitor/gather.js +124 -0
  219. package/lib/pr-monitor/journal.js +299 -0
  220. package/lib/pr-monitor/monitor.js +146 -0
  221. package/lib/pr-monitor/render-sticky.js +192 -0
  222. package/lib/pr-monitor/upsert-sticky.js +169 -0
  223. package/lib/pr-monitor/watch-lifecycle.js +95 -0
  224. package/lib/pr-monitor/watch.js +247 -0
  225. package/lib/pr-pull.js +1314 -0
  226. package/lib/pr-shepherd.js +494 -0
  227. package/lib/pr-state-validator.js +59 -0
  228. package/lib/preflight/gates.js +237 -0
  229. package/lib/preflight/runner.js +116 -0
  230. package/lib/project-discovery.js +0 -53
  231. package/lib/project-memory.js +99 -497
  232. package/lib/protected-path-manifest.js +281 -0
  233. package/lib/protected-state-surfaces.js +387 -0
  234. package/lib/release-readiness.js +2105 -0
  235. package/lib/reset.js +59 -45
  236. package/lib/review-adapter.js +68 -0
  237. package/lib/rules-sync.js +260 -0
  238. package/lib/runtime-health.js +241 -20
  239. package/lib/safety-config-renderer.js +268 -0
  240. package/lib/setup-action-log.js +1 -7
  241. package/lib/setup.js +27 -65
  242. package/lib/shell-utils.js +76 -6
  243. package/lib/skills-sync.js +330 -0
  244. package/lib/smart-status/scoring.js +17 -3
  245. package/lib/status/beads-snapshot.js +45 -2
  246. package/lib/status/presenter.js +169 -18
  247. package/lib/status/snapshot.js +186 -0
  248. package/lib/sync-backend.js +202 -0
  249. package/lib/untrusted-content.js +52 -0
  250. package/lib/upgrade-safety.js +251 -0
  251. package/lib/workflow/enforce-stage.js +351 -45
  252. package/lib/workflow/stage-transition.js +115 -0
  253. package/lib/workflow/stages.js +30 -6
  254. package/lib/workflow/state-manager.js +11 -22
  255. package/lib/workflow/state.js +23 -1
  256. package/lib/workflow-profiles.js +17 -5
  257. package/package.json +37 -35
  258. package/rules/documentation.md +19 -0
  259. package/rules/kernel-tracking.md +26 -0
  260. package/rules/security.md +22 -0
  261. package/rules/tdd.md +20 -0
  262. package/rules/workflow.md +27 -0
  263. package/scripts/auto-backing-issue.js +47 -0
  264. package/scripts/beads-context.sh +81 -57
  265. package/scripts/beads-upgrade-smoke.sh +24 -3
  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.sh +22 -3
  272. package/scripts/file-index.sh +3 -0
  273. package/scripts/forge-team/lib/claim.sh +34 -18
  274. package/scripts/forge-team/lib/dashboard.sh +61 -86
  275. package/scripts/forge-team/lib/epic.sh +99 -263
  276. package/scripts/forge-team/lib/hooks.sh +26 -28
  277. package/scripts/forge-team/lib/identity.sh +4 -4
  278. package/scripts/forge-team/lib/sync-github.sh +49 -84
  279. package/scripts/forge-team/lib/verify.sh +93 -83
  280. package/scripts/forge-team/lib/workload.sh +41 -65
  281. package/scripts/forge-team/tests/claim.test.sh +25 -19
  282. package/scripts/forge-team/tests/dashboard.test.sh +31 -46
  283. package/scripts/forge-team/tests/epic.test.sh +52 -71
  284. package/scripts/forge-team/tests/hooks.test.sh +38 -50
  285. package/scripts/forge-team/tests/identity.test.sh +3 -3
  286. package/scripts/forge-team/tests/integration.test.sh +44 -66
  287. package/scripts/forge-team/tests/sync-github.test.sh +50 -83
  288. package/scripts/forge-team/tests/verify.test.sh +37 -46
  289. package/scripts/forge-team/tests/workflow-integration.test.sh +4 -4
  290. package/scripts/forge-team/tests/workload.test.sh +32 -66
  291. package/scripts/gen-command-manifest.js +153 -0
  292. package/scripts/gen-embedded-assets.mjs +129 -0
  293. package/scripts/install.ps1 +139 -0
  294. package/scripts/install.sh +268 -0
  295. package/scripts/lib/release-asset.mjs +84 -0
  296. package/scripts/parity-check.mjs +145 -0
  297. package/scripts/parity-check.test.mjs +58 -0
  298. package/scripts/pin-agentic-workflow-images.js +112 -0
  299. package/scripts/pr-auto-actions.js +93 -0
  300. package/scripts/pr-coordinator.sh +3 -0
  301. package/scripts/pr-verdict-label.js +50 -0
  302. package/scripts/preflight-sonar.eslint.config.mjs +44 -0
  303. package/scripts/preflight.sh +21 -94
  304. package/scripts/protected-state-check.js +104 -0
  305. package/scripts/smart-status.sh +60 -57
  306. package/scripts/spikes/config-race-bench.js +111 -0
  307. package/scripts/spikes/harness-capability-matrix.js +13 -0
  308. package/scripts/spikes/patch-anchor-stability-bench.js +125 -0
  309. package/scripts/spikes/protected-path-manifest.js +20 -0
  310. package/scripts/spikes/skill-auto-invoke-parity.js +292 -0
  311. package/scripts/sync-agent-skills.js +62 -0
  312. package/scripts/sync-utils.sh +3 -0
  313. package/scripts/test-ci-shard.js +13 -6
  314. package/scripts/test.js +95 -12
  315. package/skills/claim-safety/SKILL.md +102 -0
  316. package/skills/claim-safety/evals/evals.json +46 -0
  317. package/{.github/prompts/dev.prompt.md → skills/dev/SKILL.md} +44 -50
  318. package/skills/dev/evals/evals.json +50 -0
  319. package/skills/hermes-forge/SKILL.md +185 -0
  320. package/skills/hermes-forge/evals/evals.json +46 -0
  321. package/skills/issue-basics/SKILL.md +111 -0
  322. package/skills/issue-basics/evals/evals.json +46 -0
  323. package/skills/kernel/SKILL.md +166 -0
  324. package/skills/kernel/evals/evals.json +50 -0
  325. package/skills/memory/SKILL.md +102 -0
  326. package/skills/parallel-deep-research/SKILL.md +14 -11
  327. package/skills/parallel-deep-research/evals/evals.json +11 -27
  328. package/{.github/prompts/plan.prompt.md → skills/plan/SKILL.md} +132 -157
  329. package/skills/plan/evals/evals.json +42 -0
  330. package/skills/research/SKILL.md +195 -0
  331. package/skills/research/evals/evals.json +42 -0
  332. package/{.github/prompts/review.prompt.md → skills/review/SKILL.md} +98 -62
  333. package/skills/review/evals/evals.json +42 -0
  334. package/skills/rollback/SKILL.md +110 -0
  335. package/skills/rollback/evals/evals.json +46 -0
  336. package/skills/rollback/references/methods.md +204 -0
  337. package/{.cursor/commands/rollback.md → skills/rollback/references/workflow-integration.md} +10 -284
  338. package/skills/shepherd/SKILL.md +66 -0
  339. package/skills/shepherd/evals/evals.json +42 -0
  340. package/{.github/prompts/ship.prompt.md → skills/ship/SKILL.md} +81 -45
  341. package/skills/ship/evals/evals.json +42 -0
  342. package/skills/smith/SKILL.md +142 -0
  343. package/skills/smith/evals/evals.json +46 -0
  344. package/skills/smith/references/autonomy-and-gates.md +94 -0
  345. package/{.github/prompts/sonarcloud.prompt.md → skills/sonarcloud/SKILL.md} +14 -3
  346. package/skills/sonarcloud/evals/evals.json +46 -0
  347. package/skills/sonarcloud-analysis/SKILL.md +18 -13
  348. package/skills/sonarcloud-analysis/evals/evals.json +11 -15
  349. package/{.github/prompts/status.prompt.md → skills/status/SKILL.md} +20 -10
  350. package/skills/status/evals/evals.json +50 -0
  351. package/skills/triage-ready/SKILL.md +121 -0
  352. package/skills/triage-ready/evals/evals.json +42 -0
  353. package/{.github/prompts/validate.prompt.md → skills/validate/SKILL.md} +52 -29
  354. package/skills/validate/evals/evals.json +42 -0
  355. package/skills/verify/SKILL.md +299 -0
  356. package/skills/verify/evals/evals.json +50 -0
  357. package/.claude/commands/dev.md +0 -345
  358. package/.claude/commands/plan.md +0 -566
  359. package/.claude/commands/premerge.md +0 -186
  360. package/.claude/commands/research.md +0 -42
  361. package/.claude/commands/review.md +0 -451
  362. package/.claude/commands/rollback.md +0 -721
  363. package/.claude/commands/ship.md +0 -213
  364. package/.claude/commands/sonarcloud.md +0 -152
  365. package/.claude/commands/status.md +0 -90
  366. package/.claude/commands/validate.md +0 -288
  367. package/.claude/commands/verify.md +0 -269
  368. package/.claude/rules/workflow.md +0 -121
  369. package/.cline/workflows/dev.md +0 -342
  370. package/.cline/workflows/plan.md +0 -563
  371. package/.cline/workflows/premerge.md +0 -183
  372. package/.cline/workflows/research.md +0 -39
  373. package/.cline/workflows/review.md +0 -448
  374. package/.cline/workflows/rollback.md +0 -718
  375. package/.cline/workflows/ship.md +0 -210
  376. package/.cline/workflows/sonarcloud.md +0 -146
  377. package/.cline/workflows/status.md +0 -87
  378. package/.cline/workflows/validate.md +0 -285
  379. package/.cline/workflows/verify.md +0 -266
  380. package/.codex/config.toml +0 -11
  381. package/.codex/skills/dev/SKILL.md +0 -345
  382. package/.codex/skills/plan/SKILL.md +0 -566
  383. package/.codex/skills/premerge/SKILL.md +0 -186
  384. package/.codex/skills/research/SKILL.md +0 -42
  385. package/.codex/skills/review/SKILL.md +0 -451
  386. package/.codex/skills/rollback/SKILL.md +0 -721
  387. package/.codex/skills/ship/SKILL.md +0 -213
  388. package/.codex/skills/sonarcloud/SKILL.md +0 -149
  389. package/.codex/skills/status/SKILL.md +0 -90
  390. package/.codex/skills/validate/SKILL.md +0 -288
  391. package/.codex/skills/verify/SKILL.md +0 -269
  392. package/.cursor/commands/dev.md +0 -342
  393. package/.cursor/commands/plan.md +0 -563
  394. package/.cursor/commands/premerge.md +0 -183
  395. package/.cursor/commands/research.md +0 -39
  396. package/.cursor/commands/review.md +0 -448
  397. package/.cursor/commands/ship.md +0 -210
  398. package/.cursor/commands/sonarcloud.md +0 -146
  399. package/.cursor/commands/status.md +0 -87
  400. package/.cursor/commands/validate.md +0 -285
  401. package/.cursor/commands/verify.md +0 -266
  402. package/.cursorrules +0 -149
  403. package/.github/prompts/premerge.prompt.md +0 -188
  404. package/.github/prompts/research.prompt.md +0 -44
  405. package/.github/prompts/rollback.prompt.md +0 -723
  406. package/.github/prompts/verify.prompt.md +0 -271
  407. package/.github/workflows/beads-to-github.yml +0 -89
  408. package/.github/workflows/github-to-beads.yml +0 -100
  409. package/.kilocode/workflows/dev.md +0 -346
  410. package/.kilocode/workflows/plan.md +0 -567
  411. package/.kilocode/workflows/premerge.md +0 -187
  412. package/.kilocode/workflows/research.md +0 -43
  413. package/.kilocode/workflows/review.md +0 -452
  414. package/.kilocode/workflows/rollback.md +0 -722
  415. package/.kilocode/workflows/ship.md +0 -214
  416. package/.kilocode/workflows/sonarcloud.md +0 -150
  417. package/.kilocode/workflows/status.md +0 -91
  418. package/.kilocode/workflows/validate.md +0 -289
  419. package/.kilocode/workflows/verify.md +0 -270
  420. package/.opencode/commands/dev.md +0 -345
  421. package/.opencode/commands/plan.md +0 -566
  422. package/.opencode/commands/premerge.md +0 -186
  423. package/.opencode/commands/research.md +0 -42
  424. package/.opencode/commands/review.md +0 -451
  425. package/.opencode/commands/rollback.md +0 -721
  426. package/.opencode/commands/ship.md +0 -213
  427. package/.opencode/commands/sonarcloud.md +0 -149
  428. package/.opencode/commands/status.md +0 -90
  429. package/.opencode/commands/validate.md +0 -288
  430. package/.opencode/commands/verify.md +0 -269
  431. package/.roo/commands/dev.md +0 -346
  432. package/.roo/commands/plan.md +0 -567
  433. package/.roo/commands/premerge.md +0 -187
  434. package/.roo/commands/research.md +0 -43
  435. package/.roo/commands/review.md +0 -452
  436. package/.roo/commands/rollback.md +0 -722
  437. package/.roo/commands/ship.md +0 -214
  438. package/.roo/commands/sonarcloud.md +0 -150
  439. package/.roo/commands/status.md +0 -91
  440. package/.roo/commands/validate.md +0 -289
  441. package/.roo/commands/verify.md +0 -270
  442. package/docs/BEADS_GITHUB_SYNC.md +0 -281
  443. package/docs/GREPTILE_SETUP.md +0 -400
  444. package/docs/MANUAL_REVIEW_GUIDE.md +0 -106
  445. package/docs/SETUP.md +0 -663
  446. package/docs/VALIDATION.md +0 -363
  447. package/lib/agents/cline.plugin.json +0 -29
  448. package/lib/agents/copilot.plugin.json +0 -24
  449. package/lib/agents/kilocode.plugin.json +0 -22
  450. package/lib/agents/opencode.plugin.json +0 -23
  451. package/lib/agents/roo.plugin.json +0 -30
  452. package/lib/beads-bootstrap.js +0 -225
  453. package/lib/beads-health-check.js +0 -188
  454. package/lib/commands/commands-reset.js +0 -147
  455. package/opencode.json +0 -67
  456. package/scripts/beads-context.test.js +0 -584
  457. package/scripts/github-beads-sync/comment.mjs +0 -64
  458. package/scripts/github-beads-sync/config.mjs +0 -148
  459. package/scripts/github-beads-sync/github-api.mjs +0 -131
  460. package/scripts/github-beads-sync/index.mjs +0 -356
  461. package/scripts/github-beads-sync/label-mapper.mjs +0 -54
  462. package/scripts/github-beads-sync/mapping.mjs +0 -132
  463. package/scripts/github-beads-sync/reverse-sync-cli.mjs +0 -31
  464. package/scripts/github-beads-sync/reverse-sync.mjs +0 -162
  465. package/scripts/github-beads-sync/run-bd.mjs +0 -161
  466. package/scripts/github-beads-sync/sanitize.mjs +0 -121
  467. package/scripts/github-beads-sync.config.json +0 -26
  468. 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
+ };