forge-workflow 0.1.0-beta.5 → 0.1.0-beta.7

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 (268) hide show
  1. package/AGENTS.md +4 -0
  2. package/CHANGELOG.md +48 -0
  3. package/CLAUDE.md +0 -12
  4. package/CODING_STANDARDS.md +72 -0
  5. package/bin/forge.js +12 -1
  6. package/docs/guides/MIGRATION.md +3 -3
  7. package/docs/reference/FORGE_KERNEL_STORAGE_MODEL.md +4 -0
  8. package/docs/reference/INSTALL.md +4 -0
  9. package/docs/reference/LEGACY_CLAIM_REPAIR.md +112 -0
  10. package/docs/reference/RELEASE.md +4 -4
  11. package/docs/reference/github-accounts.md +134 -0
  12. package/docs/reference/shepherd.md +63 -13
  13. package/lib/adapters/pr-state-adapter.js +15 -2
  14. package/lib/base-remote.js +138 -0
  15. package/lib/beta5-compatibility-evidence.js +1093 -0
  16. package/lib/bun-lockfile-proof.js +413 -0
  17. package/lib/bun-workflow-pins.js +461 -0
  18. package/lib/capabilities/index.js +9 -0
  19. package/lib/capabilities/model.js +141 -0
  20. package/lib/capabilities/probes.js +347 -0
  21. package/lib/codex-skills.js +2 -2
  22. package/lib/commands/_manifest.js +1 -0
  23. package/lib/commands/_registry.js +48 -18
  24. package/lib/commands/clean.js +57 -1
  25. package/lib/commands/doctor.js +37 -6
  26. package/lib/commands/gate.js +197 -27
  27. package/lib/commands/github.js +215 -0
  28. package/lib/commands/hooks.js +54 -6
  29. package/lib/commands/memory.js +66 -2
  30. package/lib/commands/merge.js +720 -73
  31. package/lib/commands/plan.js +33 -2
  32. package/lib/commands/pr.js +2 -0
  33. package/lib/commands/preflight.js +10 -2
  34. package/lib/commands/push.js +120 -9
  35. package/lib/commands/recall.js +95 -61
  36. package/lib/commands/release.js +23 -2
  37. package/lib/commands/remember.js +28 -4
  38. package/lib/commands/serve.js +26 -9
  39. package/lib/commands/setup.js +132 -4
  40. package/lib/commands/shepherd.js +578 -72
  41. package/lib/commands/ship.js +15 -69
  42. package/lib/commands/skill.js +8 -0
  43. package/lib/commands/team.js +47 -8
  44. package/lib/commands/test.js +163 -4
  45. package/lib/commands/validate.js +78 -22
  46. package/lib/commands/worktree.js +155 -19
  47. package/lib/fixtures/beta5-corpus/v1/README.md +9 -0
  48. package/lib/fixtures/beta5-corpus/v1/contract/command-contract.json +26 -0
  49. package/lib/fixtures/beta5-corpus/v1/contract/package-contract.json +13 -0
  50. package/lib/fixtures/beta5-corpus/v1/contract/workflow-stage-matrix.json +8 -0
  51. package/lib/fixtures/beta5-corpus/v1/manifest.json +25 -0
  52. package/lib/fixtures/beta5-corpus/v1/state/comments.jsonl +1 -0
  53. package/lib/fixtures/beta5-corpus/v1/state/config.yaml +6 -0
  54. package/lib/fixtures/beta5-corpus/v1/state/dependencies.jsonl +1 -0
  55. package/lib/fixtures/beta5-corpus/v1/state/issues.jsonl +2 -0
  56. package/lib/fixtures/beta5-corpus/v1/state/kernel.sql +20 -0
  57. package/lib/forge-issues.js +78 -0
  58. package/lib/gate-events.js +98 -10
  59. package/lib/github-context.js +308 -0
  60. package/lib/global-flags.js +1 -0
  61. package/lib/hook-renderer.js +29 -1
  62. package/lib/issue-render.js +19 -0
  63. package/lib/kernel/broker.js +723 -31
  64. package/lib/kernel/claim-reconciler.js +238 -0
  65. package/lib/kernel/lease-enforcer.js +9 -4
  66. package/lib/kernel/legacy-claim-repair.js +442 -0
  67. package/lib/kernel/live-claim-projection.js +26 -0
  68. package/lib/kernel/migrations.js +118 -3
  69. package/lib/kernel/readiness-model.js +184 -12
  70. package/lib/kernel/schema.js +49 -1
  71. package/lib/kernel/sqlite-driver.js +3322 -183
  72. package/lib/kernel/taxonomy-validator.js +4 -1
  73. package/lib/kernel/windows-private-acl.js +239 -0
  74. package/lib/memory/hygiene.js +191 -0
  75. package/lib/memory/router.js +94 -27
  76. package/lib/memory/usage-evidence.js +4 -0
  77. package/lib/memory-digest.js +59 -0
  78. package/lib/merge-rules.js +135 -17
  79. package/lib/npm-publish-workflow.js +233 -40
  80. package/lib/package-root.js +2 -0
  81. package/lib/pr-monitor/auto-actions.js +169 -28
  82. package/lib/pr-monitor/differ.js +110 -4
  83. package/lib/pr-monitor/events.js +0 -0
  84. package/lib/pr-monitor/flow-monitor.js +1424 -0
  85. package/lib/pr-monitor/gather.js +251 -44
  86. package/lib/pr-monitor/journal.js +0 -37
  87. package/lib/pr-monitor/monitor.js +117 -10
  88. package/lib/pr-monitor/process-identity.js +117 -0
  89. package/lib/pr-monitor/reconcile-executor.js +1101 -625
  90. package/lib/pr-monitor/reconcile.js +0 -0
  91. package/lib/pr-monitor/render-summary.js +121 -24
  92. package/lib/pr-monitor/review-preflight.js +269 -0
  93. package/lib/pr-monitor/shepherd-lease.js +28 -19
  94. package/lib/pr-monitor/verdict.js +438 -0
  95. package/lib/pr-monitor/watch-lifecycle.js +144 -38
  96. package/lib/pr-monitor/watch-owner.js +1414 -0
  97. package/lib/pr-monitor/watch.js +129 -58
  98. package/lib/pr-shepherd.js +17 -3
  99. package/lib/project-memory.js +145 -3
  100. package/lib/protected-state-authority.js +799 -4
  101. package/lib/protected-state-surfaces.js +181 -3
  102. package/lib/release-readiness.js +2 -3
  103. package/lib/review-adapter.js +65 -0
  104. package/lib/skills-sync.js +65 -32
  105. package/lib/validation/risk-manifest.js +339 -0
  106. package/lib/validation-receipt.js +190 -0
  107. package/lib/workflow/enforce-stage.js +44 -0
  108. package/lib/workflow/plan-authority.js +225 -0
  109. package/node_modules/@forge/contracts/compatibility-matrix.v1.json +1 -0
  110. package/node_modules/@forge/contracts/contract-baseline.v1.json +134 -0
  111. package/node_modules/@forge/contracts/fixtures/v1/canonical-hash.json +1 -0
  112. package/node_modules/@forge/contracts/fixtures/v1/contract-inputs.v1.json +71 -0
  113. package/node_modules/@forge/contracts/fixtures/v1/feedback-secret-reject.json +1 -0
  114. package/node_modules/@forge/contracts/fixtures/v1/identity-conflict.json +1 -0
  115. package/node_modules/@forge/contracts/fixtures/v1/malformed-envelope-inputs.v1.json +16 -0
  116. package/node_modules/@forge/contracts/fixtures/v1/malformed-missing-required.json +1 -0
  117. package/node_modules/@forge/contracts/fixtures/v1/monitor-bounds-reject.json +1 -0
  118. package/node_modules/@forge/contracts/fixtures/v1/monitor-receipt-privacy-reject.json +1 -0
  119. package/node_modules/@forge/contracts/fixtures/v1/not-executed-receipt.json +1 -0
  120. package/node_modules/@forge/contracts/fixtures/v1/privacy-redaction.json +1 -0
  121. package/node_modules/@forge/contracts/fixtures/v1/retry-identical.json +1 -0
  122. package/node_modules/@forge/contracts/fixtures/v1/stale-authority-reject.json +1 -0
  123. package/node_modules/@forge/contracts/fixtures/v1/structured-error-privacy-reject.json +1 -0
  124. package/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-live-reject.json +1 -0
  125. package/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-test-reject.json +1 -0
  126. package/node_modules/@forge/contracts/fixtures/v1/unknown-advisory-roundtrip.json +1 -0
  127. package/node_modules/@forge/contracts/fixtures/v1/unknown-consequential-reject.json +1 -0
  128. package/node_modules/@forge/contracts/fixtures/v1/valid-full.json +1 -0
  129. package/node_modules/@forge/contracts/fixtures/v1/valid-minimal.json +1 -0
  130. package/node_modules/@forge/contracts/fixtures/v1/wrong-capability-digest.json +1 -0
  131. package/node_modules/@forge/contracts/index.js +32 -0
  132. package/node_modules/@forge/contracts/package.json +35 -0
  133. package/node_modules/@forge/contracts/schemas/v1/capability-manifest.schema.json +1 -0
  134. package/node_modules/@forge/contracts/schemas/v1/claim-request.schema.json +1 -0
  135. package/node_modules/@forge/contracts/schemas/v1/context-packet.schema.json +1 -0
  136. package/node_modules/@forge/contracts/schemas/v1/delivery-receipt.schema.json +1 -0
  137. package/node_modules/@forge/contracts/schemas/v1/feedback-report.schema.json +1 -0
  138. package/node_modules/@forge/contracts/schemas/v1/lease-receipt.schema.json +1 -0
  139. package/node_modules/@forge/contracts/schemas/v1/monitor-event.schema.json +1 -0
  140. package/node_modules/@forge/contracts/schemas/v1/monitor-receipt.schema.json +1 -0
  141. package/node_modules/@forge/contracts/schemas/v1/run-receipt.schema.json +1 -0
  142. package/node_modules/@forge/contracts/schemas/v1/structured-error.schema.json +1 -0
  143. package/node_modules/@forge/contracts/schemas/v1/work-packet.schema.json +1 -0
  144. package/node_modules/@forge/contracts/src/baseline.js +23 -0
  145. package/node_modules/@forge/contracts/src/canonical.js +151 -0
  146. package/node_modules/@forge/contracts/src/definitions.js +176 -0
  147. package/node_modules/@forge/contracts/src/identity.js +42 -0
  148. package/node_modules/@forge/contracts/src/schema.js +72 -0
  149. package/node_modules/@forge/contracts/src/validate.js +305 -0
  150. package/node_modules/@forge/flow/index.js +114 -0
  151. package/node_modules/@forge/flow/package.json +34 -0
  152. package/node_modules/@forge/flow/src/bounded-loop.js +415 -0
  153. package/node_modules/@forge/flow/src/efficiency-supervisor.js +89 -0
  154. package/node_modules/@forge/flow/src/executor.js +279 -0
  155. package/node_modules/@forge/flow/src/monitor-durability.js +419 -0
  156. package/node_modules/@forge/flow/src/monitor-runtime.js +460 -0
  157. package/node_modules/@forge/flow/src/process-lifecycle.js +469 -0
  158. package/node_modules/@forge/flow/src/skill-runtime.js +343 -0
  159. package/node_modules/@forge/memory/index.js +331 -0
  160. package/node_modules/@forge/memory/package.json +34 -0
  161. package/node_modules/@forge/memory/src/authority-provider.js +67 -0
  162. package/node_modules/@forge/memory/src/backend-registry.js +166 -0
  163. package/node_modules/@forge/memory/src/feedback-intake.js +243 -0
  164. package/node_modules/@forge/memory/src/pr-lifecycle-authority.js +946 -0
  165. package/node_modules/@forge/memory/src/usage-evidence.js +205 -0
  166. package/package.json +16 -4
  167. package/packages/flow/node_modules/@forge/contracts/compatibility-matrix.v1.json +1 -0
  168. package/packages/flow/node_modules/@forge/contracts/contract-baseline.v1.json +134 -0
  169. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/canonical-hash.json +1 -0
  170. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/contract-inputs.v1.json +71 -0
  171. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/feedback-secret-reject.json +1 -0
  172. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/identity-conflict.json +1 -0
  173. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/malformed-envelope-inputs.v1.json +16 -0
  174. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/malformed-missing-required.json +1 -0
  175. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/monitor-bounds-reject.json +1 -0
  176. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/monitor-receipt-privacy-reject.json +1 -0
  177. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/not-executed-receipt.json +1 -0
  178. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/privacy-redaction.json +1 -0
  179. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/retry-identical.json +1 -0
  180. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/stale-authority-reject.json +1 -0
  181. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/structured-error-privacy-reject.json +1 -0
  182. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-live-reject.json +1 -0
  183. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-test-reject.json +1 -0
  184. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/unknown-advisory-roundtrip.json +1 -0
  185. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/unknown-consequential-reject.json +1 -0
  186. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/valid-full.json +1 -0
  187. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/valid-minimal.json +1 -0
  188. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/wrong-capability-digest.json +1 -0
  189. package/packages/flow/node_modules/@forge/contracts/index.js +32 -0
  190. package/packages/flow/node_modules/@forge/contracts/package.json +35 -0
  191. package/packages/flow/node_modules/@forge/contracts/schemas/v1/capability-manifest.schema.json +1 -0
  192. package/packages/flow/node_modules/@forge/contracts/schemas/v1/claim-request.schema.json +1 -0
  193. package/packages/flow/node_modules/@forge/contracts/schemas/v1/context-packet.schema.json +1 -0
  194. package/packages/flow/node_modules/@forge/contracts/schemas/v1/delivery-receipt.schema.json +1 -0
  195. package/packages/flow/node_modules/@forge/contracts/schemas/v1/feedback-report.schema.json +1 -0
  196. package/packages/flow/node_modules/@forge/contracts/schemas/v1/lease-receipt.schema.json +1 -0
  197. package/packages/flow/node_modules/@forge/contracts/schemas/v1/monitor-event.schema.json +1 -0
  198. package/packages/flow/node_modules/@forge/contracts/schemas/v1/monitor-receipt.schema.json +1 -0
  199. package/packages/flow/node_modules/@forge/contracts/schemas/v1/run-receipt.schema.json +1 -0
  200. package/packages/flow/node_modules/@forge/contracts/schemas/v1/structured-error.schema.json +1 -0
  201. package/packages/flow/node_modules/@forge/contracts/schemas/v1/work-packet.schema.json +1 -0
  202. package/packages/flow/node_modules/@forge/contracts/src/baseline.js +23 -0
  203. package/packages/flow/node_modules/@forge/contracts/src/canonical.js +151 -0
  204. package/packages/flow/node_modules/@forge/contracts/src/definitions.js +176 -0
  205. package/packages/flow/node_modules/@forge/contracts/src/identity.js +42 -0
  206. package/packages/flow/node_modules/@forge/contracts/src/schema.js +72 -0
  207. package/packages/flow/node_modules/@forge/contracts/src/validate.js +305 -0
  208. package/packages/memory/node_modules/@forge/contracts/compatibility-matrix.v1.json +1 -0
  209. package/packages/memory/node_modules/@forge/contracts/contract-baseline.v1.json +134 -0
  210. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/canonical-hash.json +1 -0
  211. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/contract-inputs.v1.json +71 -0
  212. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/feedback-secret-reject.json +1 -0
  213. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/identity-conflict.json +1 -0
  214. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/malformed-envelope-inputs.v1.json +16 -0
  215. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/malformed-missing-required.json +1 -0
  216. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/monitor-bounds-reject.json +1 -0
  217. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/monitor-receipt-privacy-reject.json +1 -0
  218. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/not-executed-receipt.json +1 -0
  219. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/privacy-redaction.json +1 -0
  220. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/retry-identical.json +1 -0
  221. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/stale-authority-reject.json +1 -0
  222. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/structured-error-privacy-reject.json +1 -0
  223. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-live-reject.json +1 -0
  224. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-test-reject.json +1 -0
  225. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/unknown-advisory-roundtrip.json +1 -0
  226. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/unknown-consequential-reject.json +1 -0
  227. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/valid-full.json +1 -0
  228. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/valid-minimal.json +1 -0
  229. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/wrong-capability-digest.json +1 -0
  230. package/packages/memory/node_modules/@forge/contracts/index.js +32 -0
  231. package/packages/memory/node_modules/@forge/contracts/package.json +35 -0
  232. package/packages/memory/node_modules/@forge/contracts/schemas/v1/capability-manifest.schema.json +1 -0
  233. package/packages/memory/node_modules/@forge/contracts/schemas/v1/claim-request.schema.json +1 -0
  234. package/packages/memory/node_modules/@forge/contracts/schemas/v1/context-packet.schema.json +1 -0
  235. package/packages/memory/node_modules/@forge/contracts/schemas/v1/delivery-receipt.schema.json +1 -0
  236. package/packages/memory/node_modules/@forge/contracts/schemas/v1/feedback-report.schema.json +1 -0
  237. package/packages/memory/node_modules/@forge/contracts/schemas/v1/lease-receipt.schema.json +1 -0
  238. package/packages/memory/node_modules/@forge/contracts/schemas/v1/monitor-event.schema.json +1 -0
  239. package/packages/memory/node_modules/@forge/contracts/schemas/v1/monitor-receipt.schema.json +1 -0
  240. package/packages/memory/node_modules/@forge/contracts/schemas/v1/run-receipt.schema.json +1 -0
  241. package/packages/memory/node_modules/@forge/contracts/schemas/v1/structured-error.schema.json +1 -0
  242. package/packages/memory/node_modules/@forge/contracts/schemas/v1/work-packet.schema.json +1 -0
  243. package/packages/memory/node_modules/@forge/contracts/src/baseline.js +23 -0
  244. package/packages/memory/node_modules/@forge/contracts/src/canonical.js +151 -0
  245. package/packages/memory/node_modules/@forge/contracts/src/definitions.js +176 -0
  246. package/packages/memory/node_modules/@forge/contracts/src/identity.js +42 -0
  247. package/packages/memory/node_modules/@forge/contracts/src/schema.js +72 -0
  248. package/packages/memory/node_modules/@forge/contracts/src/validate.js +305 -0
  249. package/scripts/commitlint.js +13 -15
  250. package/scripts/generate-risk-manifest.js +91 -0
  251. package/scripts/github-context-bridge.sh +10 -0
  252. package/scripts/legacy-claim-repair.js +145 -0
  253. package/scripts/lib/behavioral-eval-runtime.js +3 -2
  254. package/scripts/process-tree.js +14 -2
  255. package/scripts/protected-state-check.js +440 -17
  256. package/scripts/sync-agent-skills.js +333 -34
  257. package/scripts/test-full-suite.js +704 -18
  258. package/scripts/test-profile.js +13 -3
  259. package/scripts/test.js +96 -15
  260. package/skills/coverage.json +1 -0
  261. package/skills/review/SKILL.md +2 -0
  262. package/skills/review/evals/scorecard.json +2 -2
  263. package/skills/setup/SKILL.md +18 -0
  264. package/skills/setup/evals/scorecard.json +3 -3
  265. package/skills/shepherd/SKILL.md +19 -2
  266. package/skills/shepherd/evals/scorecard.json +3 -3
  267. package/skills/validate/SKILL.md +3 -0
  268. package/skills/validate/evals/scorecard.json +1 -1
@@ -8,13 +8,66 @@ const fs = require('node:fs');
8
8
  const path = require('node:path');
9
9
  const { resolveOwnedKernel, closeIfOwned } = require('./kernel/owned-kernel');
10
10
  const { hashProtectedContent, normalizeRepoPath } = require('./protected-state-surfaces');
11
+ const {
12
+ BUN_WORKFLOW_SPECS,
13
+ deriveBunWorkflowUpdate,
14
+ readIndexedPackageManifest,
15
+ readIndexedWorkflow,
16
+ readPinnedBunVersion,
17
+ readSourcePackageManifest,
18
+ readSourceWorkflow,
19
+ renderBunWorkflowPin,
20
+ resolveBunVersionTarget,
21
+ resolveCurrentHead,
22
+ } = require('./bun-workflow-pins');
11
23
 
12
24
  const PROTECTED_STATE_ENTITY_TYPE = 'protected_state';
13
25
  const PROTECTED_STATE_AUTHORIZATION_ISSUED = 'protected_state.authorization.issued';
26
+ const PROTECTED_STATE_WRITE_COMPLETED = 'protected_state.write.completed';
14
27
  const PROTECTED_STATE_AUTHORIZATION_CONSUMED = 'protected_state.authorization.consumed';
15
28
  const PROTECTED_STATE_AUTHORIZATION_VERSION = 1;
16
29
  const PROTECTED_STATE_AUTHORIZATION_ORIGIN = 'cli';
17
30
  const NPM_WORKFLOW_SOURCE_COMMAND = 'forge release generate-npm-workflow';
31
+ const BUN_WORKFLOW_SOURCE_COMMAND = 'forge release update-bun-pins';
32
+ const BUN_WORKFLOW_PATHS = new Set(BUN_WORKFLOW_SPECS.map(spec => spec.path));
33
+ const SKILL_MIRROR_SOURCE_COMMAND = 'scripts/sync-agent-skills.js';
34
+ const CLAUDE_SETUP_SOURCE_COMMAND = 'forge setup';
35
+
36
+ function isValidGitObjectId(value) {
37
+ return typeof value === 'string' && /^(?:[0-9a-f]{40}|[0-9a-f]{64})$/.test(value);
38
+ }
39
+
40
+ function owningWriter(pathname, surface) {
41
+ if (surface === 'workflows' && BUN_WORKFLOW_PATHS.has(pathname)) {
42
+ return {
43
+ sourceCommand: BUN_WORKFLOW_SOURCE_COMMAND,
44
+ issueOperation: 'update_bun_workflow_pin',
45
+ completionOperation: 'update_bun_workflow_pin_completed',
46
+ };
47
+ }
48
+ if (surface === 'workflows' && pathname === '.github/workflows/npm-publish.yml') {
49
+ return {
50
+ sourceCommand: NPM_WORKFLOW_SOURCE_COMMAND,
51
+ issueOperation: 'generate_npm_workflow',
52
+ completionOperation: 'generate_npm_workflow_completed',
53
+ };
54
+ }
55
+ if (surface === 'generated_harness' && /^\.agents\/skills\/[^/]+\/.+/.test(pathname)) {
56
+ return {
57
+ sourceCommand: SKILL_MIRROR_SOURCE_COMMAND,
58
+ issueOperation: 'sync_agent_skill',
59
+ completionOperation: 'sync_agent_skill_completed',
60
+ };
61
+ }
62
+ if (surface === 'generated_harness' && pathname === 'CLAUDE.md') {
63
+ return {
64
+ sourceCommand: CLAUDE_SETUP_SOURCE_COMMAND,
65
+ issueOperation: 'generate_claude_import',
66
+ completionOperation: 'generate_claude_import_completed',
67
+ };
68
+ }
69
+ return null;
70
+ }
18
71
 
19
72
  function resolveWorktreeScope(projectRoot, deps = {}) {
20
73
  const resolved = path.resolve(projectRoot);
@@ -56,8 +109,12 @@ function parseAuthorizationEvent(row) {
56
109
  surface: payload.surface,
57
110
  contentHash: payload.contentHash,
58
111
  operation: payload.operation,
112
+ viaForgeApi: payload.viaForgeApi === true,
59
113
  sourceCommand: payload.sourceCommand,
114
+ sourceHead: payload.sourceHead,
60
115
  worktreeScope: payload.worktreeScope,
116
+ writeIntent: payload.writeIntent,
117
+ targetBunVersion: payload.targetBunVersion,
61
118
  };
62
119
  }
63
120
 
@@ -71,6 +128,7 @@ function blockedDecision(request, reason) {
71
128
  requiredSurface: request.surface,
72
129
  declaredSurface: request.surface,
73
130
  contentHash: hashProtectedContent(request.content),
131
+ sourceHead: request.sourceHead,
74
132
  reason,
75
133
  repairHint: 'Regenerate the protected file through its owning Forge command, then stage that exact output.',
76
134
  };
@@ -83,6 +141,8 @@ function evaluateAuthorization(request, rows = []) {
83
141
  surface: request.surface,
84
142
  contentHash: hashProtectedContent(request.content),
85
143
  worktreeScope: request.worktreeScope,
144
+ sourceHead: request.sourceHead,
145
+ writeIntent: request.operation === 'staged_delete' ? 'delete' : 'update',
86
146
  };
87
147
  const issued = rows
88
148
  .filter(row => row?.event_type === PROTECTED_STATE_AUTHORIZATION_ISSUED)
@@ -103,15 +163,23 @@ function evaluateAuthorization(request, rows = []) {
103
163
  return blockedDecision(request, 'Protected-state authority is ambiguous because multiple unconsumed authorizations exist; failing closed.');
104
164
  }
105
165
  const latest = active[0];
166
+ const writer = owningWriter(latest.path, latest.surface);
106
167
 
168
+ const bunPinWriter = [BUN_WORKFLOW_SOURCE_COMMAND, NPM_WORKFLOW_SOURCE_COMMAND].includes(writer?.sourceCommand);
107
169
  const structurallyValid =
170
+ writer !== null &&
108
171
  latest.version === PROTECTED_STATE_AUTHORIZATION_VERSION &&
109
172
  typeof latest.capabilityId === 'string' && latest.capabilityId.length > 0 &&
110
173
  latest.origin === PROTECTED_STATE_AUTHORIZATION_ORIGIN &&
111
174
  latest.actor === latest.payloadActor &&
112
175
  latest.worktreeScope === expected.worktreeScope &&
113
176
  latest.entityId === authorizationEntityId(expected.worktreeScope, latest.path) &&
114
- latest.sourceCommand === NPM_WORKFLOW_SOURCE_COMMAND;
177
+ latest.viaForgeApi === true &&
178
+ latest.sourceCommand === writer?.sourceCommand &&
179
+ latest.operation === writer?.issueOperation &&
180
+ latest.writeIntent === expected.writeIntent &&
181
+ isValidGitObjectId(latest.sourceHead) &&
182
+ (!bunPinWriter || /^\d+\.\d+\.\d+$/.test(latest.targetBunVersion));
115
183
  if (!structurallyValid) {
116
184
  return blockedDecision(request, 'The latest Forge-owned authorization is malformed or was not issued by the owning command.');
117
185
  }
@@ -120,9 +188,36 @@ function evaluateAuthorization(request, rows = []) {
120
188
  latest.actor !== expected.actor ||
121
189
  latest.path !== expected.path ||
122
190
  latest.surface !== expected.surface ||
123
- latest.contentHash !== expected.contentHash
191
+ latest.contentHash !== expected.contentHash ||
192
+ latest.sourceHead !== expected.sourceHead ||
193
+ latest.writeIntent !== expected.writeIntent
124
194
  ) {
125
- return blockedDecision(request, 'The latest Forge-owned content-bound authorization does not match this actor, surface, path, and content hash.');
195
+ return blockedDecision(request, 'The latest Forge-owned authorization does not match this actor, surface, path, content hash, and source HEAD.');
196
+ }
197
+
198
+ const matchingCompletions = rows
199
+ .filter(row => row?.event_type === PROTECTED_STATE_WRITE_COMPLETED)
200
+ .map(parseAuthorizationEvent)
201
+ .filter(event =>
202
+ event.version === PROTECTED_STATE_AUTHORIZATION_VERSION &&
203
+ event.capabilityId === latest.capabilityId &&
204
+ event.origin === PROTECTED_STATE_AUTHORIZATION_ORIGIN &&
205
+ event.actor === latest.actor &&
206
+ event.payloadActor === latest.payloadActor &&
207
+ event.entityId === latest.entityId &&
208
+ event.path === latest.path &&
209
+ event.surface === latest.surface &&
210
+ event.contentHash === latest.contentHash &&
211
+ event.sourceHead === latest.sourceHead &&
212
+ event.worktreeScope === latest.worktreeScope &&
213
+ event.writeIntent === latest.writeIntent &&
214
+ event.operation === writer.completionOperation &&
215
+ event.viaForgeApi === true &&
216
+ event.sourceCommand === writer.sourceCommand
217
+ && event.targetBunVersion === latest.targetBunVersion
218
+ );
219
+ if (matchingCompletions.length !== 1) {
220
+ return blockedDecision(request, 'The latest Forge-owned authorization is not completed by its owning writer; failing closed.');
126
221
  }
127
222
 
128
223
  return {
@@ -134,8 +229,11 @@ function evaluateAuthorization(request, rows = []) {
134
229
  requiredSurface: expected.surface,
135
230
  declaredSurface: expected.surface,
136
231
  contentHash: expected.contentHash,
232
+ sourceHead: expected.sourceHead,
233
+ viaForgeApi: true,
137
234
  capabilityId: latest.capabilityId,
138
235
  worktreeScope: latest.worktreeScope,
236
+ targetBunVersion: latest.targetBunVersion,
139
237
  reason: 'Staged content matches the latest unconsumed Forge-owned authorization.',
140
238
  repairHint: null,
141
239
  };
@@ -169,7 +267,11 @@ async function issueProtectedStateAuthorization(projectRoot, request = {}, optio
169
267
  contentHash: hashProtectedContent(request.content),
170
268
  worktreeScope,
171
269
  operation: request.operation || 'generate',
270
+ viaForgeApi: request.viaForgeApi === true,
172
271
  sourceCommand: request.sourceCommand || NPM_WORKFLOW_SOURCE_COMMAND,
272
+ ...(request.sourceHead ? { sourceHead: request.sourceHead } : {}),
273
+ ...(request.targetBunVersion ? { targetBunVersion: request.targetBunVersion } : {}),
274
+ writeIntent: request.writeIntent || 'update',
173
275
  },
174
276
  created_at: createdAt,
175
277
  };
@@ -182,6 +284,10 @@ async function issueProtectedStateAuthorization(projectRoot, request = {}, optio
182
284
  {},
183
285
  kernel.config,
184
286
  );
287
+ if (request.verifyBeforeInsert) {
288
+ const verification = await request.verifyBeforeInsert();
289
+ if (!verification?.success) return verification || { success: false, error: 'Protected-state source changed during authorization.' };
290
+ }
185
291
  const consumedCapabilities = new Set((existingRows || [])
186
292
  .filter(row => row?.event_type === PROTECTED_STATE_AUTHORIZATION_CONSUMED)
187
293
  .map(row => parseAuthorizationEvent(row).capabilityId)
@@ -206,13 +312,19 @@ async function issueProtectedStateAuthorization(projectRoot, request = {}, optio
206
312
  path: normalizedPath,
207
313
  surface: existing.surface,
208
314
  contentHash: existing.contentHash,
315
+ sourceHead: existing.sourceHead,
209
316
  worktreeScope,
210
317
  operation: 'superseded',
318
+ viaForgeApi: existing.viaForgeApi === true,
211
319
  sourceCommand: request.sourceCommand || NPM_WORKFLOW_SOURCE_COMMAND,
212
320
  },
213
321
  created_at: createdAt,
214
322
  }, {}, kernel.config);
215
323
  }
324
+ if (request.verifyBeforeInsert) {
325
+ const verification = await request.verifyBeforeInsert();
326
+ if (!verification?.success) return verification || { success: false, error: 'Protected-state source changed during authorization.' };
327
+ }
216
328
  const inserted = await kernel.driver.insertKernelEvent(event, {}, kernel.config);
217
329
  return { success: true, capabilityId, event: parseAuthorizationEvent(inserted) };
218
330
  } finally {
@@ -220,22 +332,687 @@ async function issueProtectedStateAuthorization(projectRoot, request = {}, optio
220
332
  }
221
333
  }
222
334
 
335
+ function resolveCurrentBunWorkflowTarget(projectRoot, params, requireWritten = false) {
336
+ let currentHead;
337
+ try {
338
+ currentHead = resolveCurrentHead(projectRoot);
339
+ } catch (error) {
340
+ return { success: false, error: `Could not resolve current HEAD: ${error.message}` };
341
+ }
342
+ if (currentHead !== params.sourceHead) {
343
+ return { success: false, error: `Protected Bun workflow source HEAD changed from ${params.sourceHead} to ${currentHead}.` };
344
+ }
345
+
346
+ let update;
347
+ try {
348
+ const target = resolveBunVersionTarget(projectRoot);
349
+ if (params.targetBunVersion !== target.version) {
350
+ return { success: false, error: 'Protected Bun workflow target does not match the staged package.json Bun pin.' };
351
+ }
352
+ update = deriveBunWorkflowUpdate(projectRoot, params.sourceHead, params.path, { version: target.version });
353
+ if (requireWritten) {
354
+ const fullPath = path.join(projectRoot, update.path);
355
+ const stat = fs.lstatSync(fullPath);
356
+ if (!stat.isFile() || stat.isSymbolicLink() || !fs.readFileSync(fullPath).equals(update.content)) {
357
+ return { success: false, error: 'Protected Bun workflow completion requires the exact generated workflow bytes.' };
358
+ }
359
+ }
360
+ } catch (error) {
361
+ return { success: false, error: error.message };
362
+ }
363
+ return { success: true, update };
364
+ }
365
+
366
+ function resolveBoundNpmBunTarget(projectRoot, params) {
367
+ if (!/^\d+\.\d+\.\d+$/.test(params.targetBunVersion || '')
368
+ || params.bunVersion !== params.targetBunVersion
369
+ || resolveBunVersionTarget(projectRoot).version !== params.targetBunVersion) {
370
+ return { success: false, error: 'Protected npm workflow target does not match the staged package.json Bun pin.' };
371
+ }
372
+ return { success: true, version: params.targetBunVersion };
373
+ }
374
+
223
375
  async function issueNpmPublishWorkflowAuthorization(projectRoot, params = {}, options = {}) {
376
+ if (!isValidGitObjectId(params.sourceHead)) {
377
+ return {
378
+ success: false,
379
+ error: 'Protected npm workflow authorization requires a full 40- or 64-character lowercase source HEAD.',
380
+ };
381
+ }
224
382
  const {
225
383
  NPM_PUBLISH_WORKFLOW_PATH,
226
384
  renderNpmPublishWorkflow,
227
385
  } = require('./npm-publish-workflow');
386
+ let content;
387
+ try {
388
+ const target = resolveBoundNpmBunTarget(projectRoot, params);
389
+ if (!target.success) return target;
390
+ content = renderNpmPublishWorkflow(params.bunVersion);
391
+ } catch (error) {
392
+ return { success: false, error: error.message };
393
+ }
228
394
  return issueProtectedStateAuthorization(projectRoot, {
229
395
  actor: params.actor,
230
396
  surface: 'workflows',
231
397
  path: NPM_PUBLISH_WORKFLOW_PATH,
232
- content: renderNpmPublishWorkflow(),
398
+ content,
233
399
  operation: 'generate_npm_workflow',
400
+ viaForgeApi: true,
234
401
  sourceCommand: NPM_WORKFLOW_SOURCE_COMMAND,
402
+ sourceHead: params.sourceHead,
403
+ targetBunVersion: params.targetBunVersion,
404
+ verifyBeforeInsert: () => {
405
+ try {
406
+ return resolveBoundNpmBunTarget(projectRoot, params).success
407
+ ? { success: true }
408
+ : { success: false, error: 'Protected npm workflow target changed during authorization.' };
409
+ } catch (error) {
410
+ return { success: false, error: error.message };
411
+ }
412
+ },
235
413
  }, options);
236
414
  }
237
415
 
416
+ async function issueBunWorkflowAuthorization(projectRoot, params = {}, options = {}) {
417
+ if (!isValidGitObjectId(params.sourceHead) || !BUN_WORKFLOW_PATHS.has(params.path)) {
418
+ return {
419
+ success: false,
420
+ error: 'Protected Bun workflow authorization requires an allowlisted path and a full 40- or 64-character lowercase source HEAD.',
421
+ };
422
+ }
423
+ const target = resolveCurrentBunWorkflowTarget(projectRoot, params);
424
+ if (!target.success) return target;
425
+ const { update } = target;
426
+ const expectedHash = hashProtectedContent(update.content);
427
+ return issueProtectedStateAuthorization(projectRoot, {
428
+ actor: params.actor,
429
+ surface: 'workflows',
430
+ path: update.path,
431
+ content: update.content,
432
+ operation: 'update_bun_workflow_pin',
433
+ viaForgeApi: true,
434
+ sourceCommand: BUN_WORKFLOW_SOURCE_COMMAND,
435
+ sourceHead: params.sourceHead,
436
+ verifyBeforeInsert: () => {
437
+ const fresh = resolveCurrentBunWorkflowTarget(projectRoot, params);
438
+ if (!fresh.success) return fresh;
439
+ return hashProtectedContent(fresh.update.content) === expectedHash
440
+ ? { success: true }
441
+ : { success: false, error: 'Protected Bun workflow target changed during authorization.' };
442
+ },
443
+ targetBunVersion: params.targetBunVersion,
444
+ }, options);
445
+ }
446
+
447
+ function findExactAuthorization(rows, params, expected) {
448
+ const issued = (rows || [])
449
+ .filter(row => row?.event_type === PROTECTED_STATE_AUTHORIZATION_ISSUED)
450
+ .map(parseAuthorizationEvent)
451
+ .find(event => event.capabilityId === params.capabilityId);
452
+ const consumed = (rows || [])
453
+ .filter(row => row?.event_type === PROTECTED_STATE_AUTHORIZATION_CONSUMED)
454
+ .map(parseAuthorizationEvent)
455
+ .some(event => event.capabilityId === params.capabilityId);
456
+ const exactIssued = issued?.version === PROTECTED_STATE_AUTHORIZATION_VERSION &&
457
+ issued.origin === PROTECTED_STATE_AUTHORIZATION_ORIGIN &&
458
+ issued.actor === expected.actor && issued.payloadActor === expected.actor &&
459
+ issued.entityId === expected.entityId && issued.path === expected.path &&
460
+ issued.surface === expected.surface && issued.contentHash === expected.contentHash &&
461
+ issued.sourceHead === params.sourceHead && issued.worktreeScope === expected.worktreeScope &&
462
+ issued.operation === expected.operation && issued.viaForgeApi === true &&
463
+ issued.writeIntent === expected.writeIntent && issued.sourceCommand === expected.sourceCommand;
464
+ const targetMatches = expected.targetBunVersion === undefined
465
+ || issued.targetBunVersion === expected.targetBunVersion;
466
+ return { exactIssued: exactIssued && targetMatches, consumed };
467
+ }
468
+
469
+ async function completeNpmPublishWorkflowAuthorization(projectRoot, params = {}, options = {}) {
470
+ if (!isValidGitObjectId(params.sourceHead)) {
471
+ return {
472
+ success: false,
473
+ error: 'Protected npm workflow completion requires a full 40- or 64-character lowercase source HEAD.',
474
+ };
475
+ }
476
+ if (typeof params.capabilityId !== 'string' || params.capabilityId.length === 0) {
477
+ return { success: false, error: 'Protected npm workflow completion requires a capability id.' };
478
+ }
479
+
480
+ const {
481
+ NPM_PUBLISH_WORKFLOW_PATH,
482
+ renderNpmPublishWorkflow,
483
+ } = require('./npm-publish-workflow');
484
+ const actor = params.actor;
485
+ const normalizedPath = normalizeRepoPath(NPM_PUBLISH_WORKFLOW_PATH);
486
+ let contentHash;
487
+ try {
488
+ const target = resolveBoundNpmBunTarget(projectRoot, params);
489
+ if (!target.success) return target;
490
+ contentHash = hashProtectedContent(renderNpmPublishWorkflow(params.bunVersion));
491
+ } catch (error) {
492
+ return { success: false, error: error.message };
493
+ }
494
+ const worktreeScope = options.worktreeScope || resolveWorktreeScope(projectRoot, options.deps);
495
+ const entityId = authorizationEntityId(worktreeScope, normalizedPath);
496
+ const createdAt = options.now || new Date().toISOString();
497
+ const kernel = await resolveOwnedKernel(projectRoot, options.deps);
498
+ try {
499
+ const rows = await kernel.driver.listKernelEvents(
500
+ PROTECTED_STATE_ENTITY_TYPE,
501
+ entityId,
502
+ {},
503
+ kernel.config,
504
+ );
505
+ const { exactIssued, consumed } = findExactAuthorization(rows, params, {
506
+ actor,
507
+ entityId,
508
+ path: normalizedPath,
509
+ surface: 'workflows',
510
+ contentHash,
511
+ worktreeScope,
512
+ operation: 'generate_npm_workflow',
513
+ writeIntent: 'update',
514
+ sourceCommand: NPM_WORKFLOW_SOURCE_COMMAND,
515
+ targetBunVersion: params.targetBunVersion,
516
+ });
517
+ if (!exactIssued || consumed) {
518
+ return {
519
+ success: false,
520
+ error: 'Protected npm workflow completion does not match one active exact authorization.',
521
+ };
522
+ }
523
+ try {
524
+ if (!resolveBoundNpmBunTarget(projectRoot, params).success) {
525
+ return { success: false, error: 'Protected npm workflow target changed during completion.' };
526
+ }
527
+ } catch (error) {
528
+ return { success: false, error: error.message };
529
+ }
530
+
531
+ const event = {
532
+ entity_type: PROTECTED_STATE_ENTITY_TYPE,
533
+ entity_id: entityId,
534
+ event_type: PROTECTED_STATE_WRITE_COMPLETED,
535
+ idempotency_key: `${PROTECTED_STATE_WRITE_COMPLETED}:${params.capabilityId}`,
536
+ expected_revision: 0,
537
+ actor,
538
+ origin: PROTECTED_STATE_AUTHORIZATION_ORIGIN,
539
+ payload: {
540
+ version: PROTECTED_STATE_AUTHORIZATION_VERSION,
541
+ capabilityId: params.capabilityId,
542
+ actor,
543
+ path: normalizedPath,
544
+ surface: 'workflows',
545
+ contentHash,
546
+ sourceHead: params.sourceHead,
547
+ worktreeScope,
548
+ writeIntent: 'update',
549
+ operation: 'generate_npm_workflow_completed',
550
+ viaForgeApi: true,
551
+ sourceCommand: NPM_WORKFLOW_SOURCE_COMMAND,
552
+ targetBunVersion: params.targetBunVersion,
553
+ },
554
+ created_at: createdAt,
555
+ };
556
+ const inserted = await kernel.driver.insertKernelEvent(event, {}, kernel.config);
557
+ return { success: true, capabilityId: params.capabilityId, event: parseAuthorizationEvent(inserted) };
558
+ } finally {
559
+ closeIfOwned(kernel);
560
+ }
561
+ }
562
+
563
+ async function completeBunWorkflowAuthorization(projectRoot, params = {}, options = {}) {
564
+ if (!isValidGitObjectId(params.sourceHead) || !BUN_WORKFLOW_PATHS.has(params.path)) {
565
+ return {
566
+ success: false,
567
+ error: 'Protected Bun workflow completion requires an allowlisted path and a full 40- or 64-character lowercase source HEAD.',
568
+ };
569
+ }
570
+ if (typeof params.capabilityId !== 'string' || params.capabilityId.length === 0) {
571
+ return { success: false, error: 'Protected Bun workflow completion requires a capability id.' };
572
+ }
573
+
574
+ const target = resolveCurrentBunWorkflowTarget(projectRoot, params, true);
575
+ if (!target.success) return target;
576
+ const { update } = target;
577
+
578
+ const actor = params.actor;
579
+ const normalizedPath = normalizeRepoPath(update.path);
580
+ const contentHash = hashProtectedContent(update.content);
581
+ const worktreeScope = options.worktreeScope || resolveWorktreeScope(projectRoot, options.deps);
582
+ const entityId = authorizationEntityId(worktreeScope, normalizedPath);
583
+ const createdAt = options.now || new Date().toISOString();
584
+ const kernel = await resolveOwnedKernel(projectRoot, options.deps);
585
+ try {
586
+ const rows = await kernel.driver.listKernelEvents(
587
+ PROTECTED_STATE_ENTITY_TYPE,
588
+ entityId,
589
+ {},
590
+ kernel.config,
591
+ );
592
+ const { exactIssued, consumed } = findExactAuthorization(rows, params, {
593
+ actor,
594
+ entityId,
595
+ path: normalizedPath,
596
+ surface: 'workflows',
597
+ contentHash,
598
+ worktreeScope,
599
+ operation: 'update_bun_workflow_pin',
600
+ writeIntent: 'update',
601
+ sourceCommand: BUN_WORKFLOW_SOURCE_COMMAND,
602
+ targetBunVersion: params.targetBunVersion,
603
+ });
604
+ if (!exactIssued || consumed) {
605
+ return {
606
+ success: false,
607
+ error: 'Protected Bun workflow completion does not match one active exact authorization.',
608
+ };
609
+ }
610
+ const fresh = resolveCurrentBunWorkflowTarget(projectRoot, params, true);
611
+ if (!fresh.success) return fresh;
612
+ if (hashProtectedContent(fresh.update.content) !== contentHash) {
613
+ return { success: false, error: 'Protected Bun workflow target changed during completion.' };
614
+ }
615
+
616
+ const event = {
617
+ entity_type: PROTECTED_STATE_ENTITY_TYPE,
618
+ entity_id: entityId,
619
+ event_type: PROTECTED_STATE_WRITE_COMPLETED,
620
+ idempotency_key: `${PROTECTED_STATE_WRITE_COMPLETED}:${params.capabilityId}`,
621
+ expected_revision: 0,
622
+ actor,
623
+ origin: PROTECTED_STATE_AUTHORIZATION_ORIGIN,
624
+ payload: {
625
+ version: PROTECTED_STATE_AUTHORIZATION_VERSION,
626
+ capabilityId: params.capabilityId,
627
+ actor,
628
+ path: normalizedPath,
629
+ surface: 'workflows',
630
+ contentHash,
631
+ sourceHead: params.sourceHead,
632
+ worktreeScope,
633
+ writeIntent: 'update',
634
+ operation: 'update_bun_workflow_pin_completed',
635
+ viaForgeApi: true,
636
+ sourceCommand: BUN_WORKFLOW_SOURCE_COMMAND,
637
+ targetBunVersion: params.targetBunVersion,
638
+ },
639
+ created_at: createdAt,
640
+ };
641
+ const inserted = await kernel.driver.insertKernelEvent(event, {}, kernel.config);
642
+ return { success: true, capabilityId: params.capabilityId, event: parseAuthorizationEvent(inserted) };
643
+ } finally {
644
+ closeIfOwned(kernel);
645
+ }
646
+ }
647
+
648
+ async function cancelBunWorkflowBatchAuthorizations(projectRoot, requests = [], options = {}) {
649
+ if (requests.length === 0) return { success: true, results: [] };
650
+ const worktreeScope = options.worktreeScope || resolveWorktreeScope(projectRoot, options.deps);
651
+ const kernel = await resolveOwnedKernel(projectRoot, options.deps);
652
+ const results = [];
653
+ try {
654
+ for (const request of requests) {
655
+ const normalizedPath = normalizeRepoPath(request.path);
656
+ const writer = owningWriter(normalizedPath, 'workflows');
657
+ if (!request.actor || !isValidGitObjectId(request.sourceHead) || !request.capabilityId ||
658
+ !writer || ![BUN_WORKFLOW_SOURCE_COMMAND, NPM_WORKFLOW_SOURCE_COMMAND].includes(writer.sourceCommand)) {
659
+ results.push({ success: false, path: normalizedPath, capabilityId: request.capabilityId, error: 'Invalid Bun workflow batch cancellation request.' });
660
+ continue;
661
+ }
662
+ try {
663
+ const entityId = authorizationEntityId(worktreeScope, normalizedPath);
664
+ const rows = await kernel.driver.listKernelEvents(PROTECTED_STATE_ENTITY_TYPE, entityId, {}, kernel.config);
665
+ const events = (rows || []).map(parseAuthorizationEvent);
666
+ const issued = events.find(event =>
667
+ event.eventType === PROTECTED_STATE_AUTHORIZATION_ISSUED &&
668
+ event.capabilityId === request.capabilityId
669
+ );
670
+ if (!issued) {
671
+ results.push({ success: true, path: normalizedPath, capabilityId: request.capabilityId, status: 'not-issued' });
672
+ continue;
673
+ }
674
+ const exact = issued.version === PROTECTED_STATE_AUTHORIZATION_VERSION &&
675
+ issued.origin === PROTECTED_STATE_AUTHORIZATION_ORIGIN &&
676
+ issued.actor === request.actor && issued.payloadActor === request.actor &&
677
+ issued.entityId === entityId && issued.path === normalizedPath &&
678
+ issued.surface === 'workflows' && issued.sourceHead === request.sourceHead &&
679
+ issued.worktreeScope === worktreeScope && issued.operation === writer.issueOperation &&
680
+ issued.viaForgeApi === true && issued.writeIntent === 'update' &&
681
+ issued.sourceCommand === writer.sourceCommand;
682
+ if (!exact) {
683
+ results.push({ success: false, path: normalizedPath, capabilityId: request.capabilityId, error: 'Capability does not match this Bun workflow batch.' });
684
+ continue;
685
+ }
686
+ if (events.some(event => event.eventType === PROTECTED_STATE_AUTHORIZATION_CONSUMED && event.capabilityId === request.capabilityId)) {
687
+ results.push({ success: true, path: normalizedPath, capabilityId: request.capabilityId, status: 'already-consumed' });
688
+ continue;
689
+ }
690
+ await kernel.driver.insertKernelEvent({
691
+ entity_type: PROTECTED_STATE_ENTITY_TYPE,
692
+ entity_id: entityId,
693
+ event_type: PROTECTED_STATE_AUTHORIZATION_CONSUMED,
694
+ idempotency_key: `${PROTECTED_STATE_AUTHORIZATION_CONSUMED}:${request.capabilityId}`,
695
+ expected_revision: 0,
696
+ actor: request.actor,
697
+ origin: PROTECTED_STATE_AUTHORIZATION_ORIGIN,
698
+ payload: {
699
+ version: PROTECTED_STATE_AUTHORIZATION_VERSION,
700
+ capabilityId: request.capabilityId,
701
+ actor: request.actor,
702
+ path: normalizedPath,
703
+ surface: 'workflows',
704
+ contentHash: issued.contentHash,
705
+ sourceHead: request.sourceHead,
706
+ worktreeScope,
707
+ writeIntent: 'update',
708
+ operation: 'batch_rollback',
709
+ viaForgeApi: true,
710
+ sourceCommand: BUN_WORKFLOW_SOURCE_COMMAND,
711
+ },
712
+ created_at: options.now || new Date().toISOString(),
713
+ }, {}, kernel.config);
714
+ results.push({ success: true, path: normalizedPath, capabilityId: request.capabilityId, status: 'consumed' });
715
+ } catch (error) {
716
+ results.push({ success: false, path: normalizedPath, capabilityId: request.capabilityId, error: error.message });
717
+ }
718
+ }
719
+ return { success: results.every(result => result.success), results };
720
+ } finally {
721
+ closeIfOwned(kernel);
722
+ }
723
+ }
724
+
725
+ const CLAUDE_POINTER_CONTENT = Buffer.from('@AGENTS.md\n');
726
+ const CLAUDE_POINTER_CRLF_CONTENT = Buffer.from('@AGENTS.md\r\n');
727
+
728
+ function isExactClaudePointer(content) {
729
+ const bytes = Buffer.from(content || '');
730
+ return bytes.equals(Buffer.from('@AGENTS.md')) || bytes.equals(CLAUDE_POINTER_CONTENT) || bytes.equals(CLAUDE_POINTER_CRLF_CONTENT);
731
+ }
732
+
733
+ async function issueGeneratedHarnessClaudeAuthorization(projectRoot, params = {}, options = {}) {
734
+ if (!isValidGitObjectId(params.sourceHead) || !isExactClaudePointer(params.content)) {
735
+ return {
736
+ success: false,
737
+ error: 'Root CLAUDE.md authorization requires exact @AGENTS.md pointer bytes and a full 40- or 64-character source HEAD.',
738
+ };
739
+ }
740
+ const content = Buffer.from(params.content || CLAUDE_POINTER_CONTENT);
741
+ return issueProtectedStateAuthorization(projectRoot, {
742
+ actor: params.actor,
743
+ surface: 'generated_harness',
744
+ path: 'CLAUDE.md',
745
+ content,
746
+ operation: 'generate_claude_import',
747
+ viaForgeApi: true,
748
+ sourceCommand: CLAUDE_SETUP_SOURCE_COMMAND,
749
+ sourceHead: params.sourceHead,
750
+ }, options);
751
+ }
752
+
753
+ async function completeGeneratedHarnessClaudeAuthorization(projectRoot, params = {}, options = {}) {
754
+ const content = Buffer.from(params.content || CLAUDE_POINTER_CONTENT);
755
+ if (!isValidGitObjectId(params.sourceHead) || !params.capabilityId || !isExactClaudePointer(content)) {
756
+ return { success: false, error: 'Root CLAUDE.md completion requires a capability id and full 40- or 64-character source HEAD.' };
757
+ }
758
+ const actor = params.actor;
759
+ const normalizedPath = 'CLAUDE.md';
760
+ const contentHash = hashProtectedContent(content);
761
+ const worktreeScope = options.worktreeScope || resolveWorktreeScope(projectRoot, options.deps);
762
+ const entityId = authorizationEntityId(worktreeScope, normalizedPath);
763
+ const kernel = await resolveOwnedKernel(projectRoot, options.deps);
764
+ try {
765
+ const rows = await kernel.driver.listKernelEvents(PROTECTED_STATE_ENTITY_TYPE, entityId, {}, kernel.config);
766
+ const { exactIssued, consumed } = findExactAuthorization(rows, params, {
767
+ actor,
768
+ entityId,
769
+ path: normalizedPath,
770
+ surface: 'generated_harness',
771
+ contentHash,
772
+ worktreeScope,
773
+ operation: 'generate_claude_import',
774
+ writeIntent: 'update',
775
+ sourceCommand: CLAUDE_SETUP_SOURCE_COMMAND,
776
+ });
777
+ if (!exactIssued || consumed) {
778
+ return { success: false, error: 'Root CLAUDE.md completion does not match one active exact authorization.' };
779
+ }
780
+ const inserted = await kernel.driver.insertKernelEvent({
781
+ entity_type: PROTECTED_STATE_ENTITY_TYPE,
782
+ entity_id: entityId,
783
+ event_type: PROTECTED_STATE_WRITE_COMPLETED,
784
+ idempotency_key: `${PROTECTED_STATE_WRITE_COMPLETED}:${params.capabilityId}`,
785
+ expected_revision: 0,
786
+ actor,
787
+ origin: PROTECTED_STATE_AUTHORIZATION_ORIGIN,
788
+ payload: {
789
+ version: PROTECTED_STATE_AUTHORIZATION_VERSION,
790
+ capabilityId: params.capabilityId,
791
+ actor,
792
+ path: normalizedPath,
793
+ surface: 'generated_harness',
794
+ contentHash,
795
+ sourceHead: params.sourceHead,
796
+ worktreeScope,
797
+ writeIntent: 'update',
798
+ operation: 'generate_claude_import_completed',
799
+ viaForgeApi: true,
800
+ sourceCommand: CLAUDE_SETUP_SOURCE_COMMAND,
801
+ },
802
+ created_at: options.now || new Date().toISOString(),
803
+ }, {}, kernel.config);
804
+ return { success: true, capabilityId: params.capabilityId, event: parseAuthorizationEvent(inserted) };
805
+ } finally {
806
+ closeIfOwned(kernel);
807
+ }
808
+ }
809
+
810
+ function resolveCanonicalSkillMirror(projectRoot, mirrorPath, options = {}) {
811
+ const normalizedPath = normalizeRepoPath(mirrorPath);
812
+ const match = /^\.agents\/skills\/([^/]+)\/(.+)$/.exec(normalizedPath);
813
+ if (!match || match[1] === '.' || match[1] === '..') return null;
814
+ const segments = match[2].split('/');
815
+ if (segments.some(segment => !segment || segment === '.' || segment === '..')) return null;
816
+ const skillRoot = path.resolve(projectRoot, 'skills', match[1]);
817
+ const canonicalPath = path.resolve(skillRoot, match[2]);
818
+ if (canonicalPath !== skillRoot && !canonicalPath.startsWith(`${skillRoot}${path.sep}`)) return null;
819
+ try {
820
+ const skillsRoot = path.resolve(projectRoot, 'skills');
821
+ const skillsStat = fs.lstatSync(skillsRoot);
822
+ if (!skillsStat.isDirectory() || skillsStat.isSymbolicLink()) return null;
823
+ const rootStat = fs.lstatSync(skillRoot);
824
+ if (!rootStat.isDirectory() || rootStat.isSymbolicLink()) return null;
825
+ let ancestor = skillRoot;
826
+ for (const segment of segments.slice(0, -1)) {
827
+ ancestor = path.join(ancestor, segment);
828
+ const ancestorStat = fs.lstatSync(ancestor);
829
+ if (!ancestorStat.isDirectory() || ancestorStat.isSymbolicLink()) return null;
830
+ }
831
+ const stat = fs.lstatSync(canonicalPath);
832
+ if (!stat.isFile() || stat.isSymbolicLink()) return null;
833
+ return { normalizedPath, content: fs.readFileSync(canonicalPath) };
834
+ } catch (error) {
835
+ if (options.allowMissing === true && error.code === 'ENOENT' && Buffer.isBuffer(options.priorContent)) {
836
+ return { normalizedPath, content: options.priorContent, missing: true };
837
+ }
838
+ return null;
839
+ }
840
+ }
841
+
842
+ async function issueGeneratedHarnessSkillAuthorization(projectRoot, params = {}, options = {}) {
843
+ if (!isValidGitObjectId(params.sourceHead)) {
844
+ return { success: false, error: 'Protected skill mirror authorization requires a full 40- or 64-character lowercase source HEAD.' };
845
+ }
846
+ const writeIntent = params.writeIntent === 'delete' ? 'delete' : 'update';
847
+ const canonical = resolveCanonicalSkillMirror(projectRoot, params.path, {
848
+ allowMissing: writeIntent === 'delete',
849
+ priorContent: params.priorContent,
850
+ });
851
+ if (!canonical) {
852
+ return { success: false, error: 'Protected skill mirror authorization requires a canonical .agents/skills/<name>/<file> path.' };
853
+ }
854
+ if (writeIntent === 'delete' && canonical.missing !== true) {
855
+ return { success: false, error: 'Protected skill mirror deletion authorization requires the canonical source to be absent.' };
856
+ }
857
+ return issueProtectedStateAuthorization(projectRoot, {
858
+ actor: params.actor,
859
+ surface: 'generated_harness',
860
+ path: canonical.normalizedPath,
861
+ content: canonical.content,
862
+ operation: 'sync_agent_skill',
863
+ viaForgeApi: true,
864
+ sourceCommand: SKILL_MIRROR_SOURCE_COMMAND,
865
+ sourceHead: params.sourceHead,
866
+ writeIntent,
867
+ }, options);
868
+ }
869
+
870
+ async function completeGeneratedHarnessSkillAuthorization(projectRoot, params = {}, options = {}) {
871
+ if (!isValidGitObjectId(params.sourceHead) || !params.capabilityId) {
872
+ return { success: false, error: 'Protected skill mirror completion requires a capability id and full 40- or 64-character source HEAD.' };
873
+ }
874
+ const writeIntent = params.writeIntent === 'delete' ? 'delete' : 'update';
875
+ const canonical = resolveCanonicalSkillMirror(projectRoot, params.path, {
876
+ allowMissing: writeIntent === 'delete',
877
+ priorContent: params.priorContent,
878
+ });
879
+ if (!canonical) {
880
+ return { success: false, error: 'Protected skill mirror completion requires a canonical .agents/skills/<name>/<file> path.' };
881
+ }
882
+ if (writeIntent === 'delete' && canonical.missing !== true) {
883
+ return { success: false, error: 'Protected skill mirror deletion completion requires the canonical source to be absent.' };
884
+ }
885
+ const actor = params.actor;
886
+ const contentHash = hashProtectedContent(canonical.content);
887
+ const mirrorTarget = path.resolve(projectRoot, canonical.normalizedPath);
888
+ if (writeIntent === 'delete' && fs.existsSync(mirrorTarget)) {
889
+ return { success: false, error: 'Protected skill mirror deletion completion requires the exact mirror path to be absent.' };
890
+ }
891
+ const worktreeScope = options.worktreeScope || resolveWorktreeScope(projectRoot, options.deps);
892
+ const entityId = authorizationEntityId(worktreeScope, canonical.normalizedPath);
893
+ const kernel = await resolveOwnedKernel(projectRoot, options.deps);
894
+ try {
895
+ const rows = await kernel.driver.listKernelEvents(PROTECTED_STATE_ENTITY_TYPE, entityId, {}, kernel.config);
896
+ const { exactIssued, consumed } = findExactAuthorization(rows, params, {
897
+ actor,
898
+ entityId,
899
+ path: canonical.normalizedPath,
900
+ surface: 'generated_harness',
901
+ contentHash,
902
+ worktreeScope,
903
+ operation: 'sync_agent_skill',
904
+ writeIntent,
905
+ sourceCommand: SKILL_MIRROR_SOURCE_COMMAND,
906
+ });
907
+ if (!exactIssued || consumed) {
908
+ return { success: false, error: 'Protected skill mirror completion does not match one active exact authorization.' };
909
+ }
910
+ const inserted = await kernel.driver.insertKernelEvent({
911
+ entity_type: PROTECTED_STATE_ENTITY_TYPE,
912
+ entity_id: entityId,
913
+ event_type: PROTECTED_STATE_WRITE_COMPLETED,
914
+ idempotency_key: `${PROTECTED_STATE_WRITE_COMPLETED}:${params.capabilityId}`,
915
+ expected_revision: 0,
916
+ actor,
917
+ origin: PROTECTED_STATE_AUTHORIZATION_ORIGIN,
918
+ payload: {
919
+ version: PROTECTED_STATE_AUTHORIZATION_VERSION,
920
+ capabilityId: params.capabilityId,
921
+ actor,
922
+ path: canonical.normalizedPath,
923
+ surface: 'generated_harness',
924
+ contentHash,
925
+ sourceHead: params.sourceHead,
926
+ worktreeScope,
927
+ writeIntent,
928
+ operation: 'sync_agent_skill_completed',
929
+ viaForgeApi: true,
930
+ sourceCommand: SKILL_MIRROR_SOURCE_COMMAND,
931
+ },
932
+ created_at: options.now || new Date().toISOString(),
933
+ }, {}, kernel.config);
934
+ return { success: true, capabilityId: params.capabilityId, event: parseAuthorizationEvent(inserted) };
935
+ } finally {
936
+ closeIfOwned(kernel);
937
+ }
938
+ }
939
+
940
+ function validateBunDecisionTargets(projectRoot, decisions) {
941
+ const bunDecisions = decisions.filter(decision => decision.allowed && decision.targetBunVersion);
942
+ if (bunDecisions.length === 0) return;
943
+ let stagedVersion;
944
+ try {
945
+ stagedVersion = resolveBunVersionTarget(projectRoot).version;
946
+ } catch (error) {
947
+ for (const decision of bunDecisions) Object.assign(decision, { allowed: false, decision: 'blocked', reason: error.message });
948
+ return;
949
+ }
950
+ for (const decision of bunDecisions) {
951
+ if (decision.targetBunVersion !== stagedVersion) {
952
+ Object.assign(decision, { allowed: false, decision: 'blocked', reason: 'The staged package.json Bun pin changed after workflow generation.' });
953
+ }
954
+ }
955
+ }
956
+
957
+ function validateCompleteBunPinBatch(projectRoot, options = {}) {
958
+ if (options.sourceHead === null) return { success: true };
959
+ let sourceHead;
960
+ let sourcePackage;
961
+ try {
962
+ sourceHead = options.sourceHead || resolveCurrentHead(projectRoot);
963
+ sourcePackage = Buffer.from((options.readSourcePackageManifest || readSourcePackageManifest)(projectRoot, sourceHead));
964
+ } catch (error) {
965
+ if (error.message.includes('package.json is not a regular file at')) return { success: true };
966
+ return { success: false, error: `Could not verify the complete staged Bun pin update: ${error.message}` };
967
+ }
968
+ try {
969
+ if (JSON.parse(sourcePackage.toString('utf8')).name !== 'forge-workflow') return { success: true };
970
+ const sourceVersion = readPinnedBunVersion(sourcePackage);
971
+ const indexedPackage = Buffer.from((options.readIndexedPackageManifest || readIndexedPackageManifest)(projectRoot));
972
+ const targetVersion = readPinnedBunVersion(indexedPackage);
973
+ if (targetVersion === sourceVersion) return { success: true };
974
+ if (resolveBunVersionTarget(projectRoot, options).version !== targetVersion) {
975
+ return { success: false, error: 'The working package.json Bun pin differs from the staged pin.' };
976
+ }
977
+
978
+ for (const spec of BUN_WORKFLOW_SPECS) {
979
+ const baseline = (options.readSourceWorkflow || readSourceWorkflow)(projectRoot, sourceHead, spec.path);
980
+ const expected = Buffer.from(renderBunWorkflowPin(spec.path, baseline, targetVersion));
981
+ const indexed = Buffer.from((options.readIndexedWorkflow || readIndexedWorkflow)(projectRoot, spec.path));
982
+ if (!indexed.equals(expected)) {
983
+ return { success: false, error: `A staged Bun pin change requires the complete generated workflow set; ${spec.path} does not match Bun ${targetVersion}.` };
984
+ }
985
+ }
986
+
987
+ const npmWorkflow = require('./npm-publish-workflow');
988
+ const indexedNpm = Buffer.from((options.readIndexedNpmPublishWorkflow || npmWorkflow.readIndexedNpmPublishWorkflow)(projectRoot));
989
+ if (!indexedNpm.equals(Buffer.from(npmWorkflow.renderNpmPublishWorkflow(targetVersion)))) {
990
+ return { success: false, error: `A staged Bun pin change requires the complete generated workflow set; ${npmWorkflow.NPM_PUBLISH_WORKFLOW_PATH} does not match Bun ${targetVersion}.` };
991
+ }
992
+ return { success: true, sourceVersion, targetVersion };
993
+ } catch (error) {
994
+ return { success: false, error: `Could not verify the complete staged Bun pin update: ${error.message}` };
995
+ }
996
+ }
997
+
998
+ function blockedBunBatchDecision(reason) {
999
+ return {
1000
+ allowed: false,
1001
+ decision: 'blocked',
1002
+ path: 'package.json',
1003
+ operation: 'staged_edit',
1004
+ requiredSurface: 'workflows',
1005
+ declaredSurface: 'workflows',
1006
+ reason,
1007
+ repairHint: 'Run forge release update-bun-pins from the staged package.json target, then stage the complete generated workflow set.',
1008
+ };
1009
+ }
1010
+
238
1011
  async function authorizeAndConsumeProtectedStateWrites(projectRoot, requests = [], options = {}) {
1012
+ const bunBatch = (options.validateCompleteBunPinBatch || validateCompleteBunPinBatch)(projectRoot, options);
1013
+ if (!bunBatch.success) {
1014
+ return { success: false, decisions: [], batchDecision: blockedBunBatchDecision(bunBatch.error) };
1015
+ }
239
1016
  if (requests.length === 0) return { success: true, decisions: [] };
240
1017
  const worktreeScope = options.worktreeScope || resolveWorktreeScope(projectRoot, options.deps);
241
1018
  const kernel = await resolveOwnedKernel(projectRoot, options.deps);
@@ -256,10 +1033,15 @@ async function authorizeAndConsumeProtectedStateWrites(projectRoot, requests = [
256
1033
  worktreeScope,
257
1034
  }, rows || []));
258
1035
  }
1036
+ validateBunDecisionTargets(projectRoot, decisions);
259
1037
 
260
1038
  if (decisions.some(decision => !decision.allowed)) {
261
1039
  return { success: false, decisions };
262
1040
  }
1041
+ const currentBunBatch = (options.validateCompleteBunPinBatch || validateCompleteBunPinBatch)(projectRoot, options);
1042
+ if (!currentBunBatch.success) {
1043
+ return { success: false, decisions, batchDecision: blockedBunBatchDecision(currentBunBatch.error) };
1044
+ }
263
1045
 
264
1046
  for (const decision of decisions) {
265
1047
  const createdAt = options.now || new Date().toISOString();
@@ -278,8 +1060,11 @@ async function authorizeAndConsumeProtectedStateWrites(projectRoot, requests = [
278
1060
  path: decision.path,
279
1061
  surface: decision.requiredSurface,
280
1062
  contentHash: decision.contentHash,
1063
+ sourceHead: decision.sourceHead,
281
1064
  worktreeScope: decision.worktreeScope,
1065
+ writeIntent: decision.operation === 'staged_delete' ? 'delete' : 'update',
282
1066
  operation: 'staged_edit',
1067
+ viaForgeApi: decision.viaForgeApi === true,
283
1068
  sourceCommand: 'scripts/protected-state-check.js',
284
1069
  },
285
1070
  created_at: createdAt,
@@ -295,11 +1080,21 @@ async function authorizeAndConsumeProtectedStateWrites(projectRoot, requests = [
295
1080
  module.exports = {
296
1081
  PROTECTED_STATE_ENTITY_TYPE,
297
1082
  PROTECTED_STATE_AUTHORIZATION_ISSUED,
1083
+ PROTECTED_STATE_WRITE_COMPLETED,
298
1084
  PROTECTED_STATE_AUTHORIZATION_CONSUMED,
1085
+ isValidGitObjectId,
299
1086
  resolveWorktreeScope,
300
1087
  authorizationEntityId,
301
1088
  parseAuthorizationEvent,
302
1089
  evaluateAuthorization,
303
1090
  issueNpmPublishWorkflowAuthorization,
1091
+ completeNpmPublishWorkflowAuthorization,
1092
+ issueBunWorkflowAuthorization,
1093
+ completeBunWorkflowAuthorization,
1094
+ cancelBunWorkflowBatchAuthorizations,
1095
+ issueGeneratedHarnessClaudeAuthorization,
1096
+ completeGeneratedHarnessClaudeAuthorization,
1097
+ issueGeneratedHarnessSkillAuthorization,
1098
+ completeGeneratedHarnessSkillAuthorization,
304
1099
  authorizeAndConsumeProtectedStateWrites,
305
1100
  };