forge-workflow 0.1.0-beta.6 → 0.1.0-beta.8

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 (179) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/bin/forge-gh-proxy.js +15 -0
  3. package/bin/forge-github-credential.js +21 -0
  4. package/bin/forge.js +16 -1
  5. package/docs/guides/MIGRATION.md +4 -3
  6. package/docs/reference/RELEASE.md +5 -3
  7. package/docs/reference/VALIDATION.md +14 -0
  8. package/docs/reference/github-accounts.md +114 -38
  9. package/lib/bun-workflow-pins.js +129 -3
  10. package/lib/commands/github.js +234 -49
  11. package/lib/commands/merge.js +2 -2
  12. package/lib/commands/push.js +308 -47
  13. package/lib/commands/release.js +18 -1
  14. package/lib/commands/shepherd.js +1 -1
  15. package/lib/commands/test.js +8 -0
  16. package/lib/commands/validate.js +219 -67
  17. package/lib/gh-proxy.js +309 -0
  18. package/lib/github-context.js +160 -25
  19. package/lib/github-credential.js +47 -0
  20. package/lib/github-router.js +774 -0
  21. package/lib/kernel/migrations.js +1 -1
  22. package/lib/kernel/sqlite-driver.js +1 -1
  23. package/lib/memory/router.js +1 -1
  24. package/lib/memory/usage-evidence.js +1 -1
  25. package/lib/native-gh.js +177 -0
  26. package/lib/npm-publish-workflow.js +5 -5
  27. package/lib/pr-monitor/flow-monitor.js +127 -28
  28. package/lib/pr-monitor/reconcile-executor.js +1 -1
  29. package/lib/project-memory.js +1 -1
  30. package/lib/protected-state-authority.js +214 -13
  31. package/lib/release-readiness.js +57 -41
  32. package/lib/shell-utils.js +41 -15
  33. package/lib/test-workflow.js +389 -0
  34. package/lib/validation-receipt.js +305 -0
  35. package/lib/workflow-templates/test.yml +654 -0
  36. package/node_modules/@forge/contracts/compatibility-matrix.v1.json +1 -0
  37. package/node_modules/@forge/contracts/contract-baseline.v1.json +134 -0
  38. package/node_modules/@forge/contracts/fixtures/v1/canonical-hash.json +1 -0
  39. package/node_modules/@forge/contracts/fixtures/v1/contract-inputs.v1.json +71 -0
  40. package/node_modules/@forge/contracts/fixtures/v1/feedback-secret-reject.json +1 -0
  41. package/node_modules/@forge/contracts/fixtures/v1/identity-conflict.json +1 -0
  42. package/node_modules/@forge/contracts/fixtures/v1/malformed-envelope-inputs.v1.json +16 -0
  43. package/node_modules/@forge/contracts/fixtures/v1/malformed-missing-required.json +1 -0
  44. package/node_modules/@forge/contracts/fixtures/v1/monitor-bounds-reject.json +1 -0
  45. package/node_modules/@forge/contracts/fixtures/v1/monitor-receipt-privacy-reject.json +1 -0
  46. package/node_modules/@forge/contracts/fixtures/v1/not-executed-receipt.json +1 -0
  47. package/node_modules/@forge/contracts/fixtures/v1/privacy-redaction.json +1 -0
  48. package/node_modules/@forge/contracts/fixtures/v1/retry-identical.json +1 -0
  49. package/node_modules/@forge/contracts/fixtures/v1/stale-authority-reject.json +1 -0
  50. package/node_modules/@forge/contracts/fixtures/v1/structured-error-privacy-reject.json +1 -0
  51. package/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-live-reject.json +1 -0
  52. package/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-test-reject.json +1 -0
  53. package/node_modules/@forge/contracts/fixtures/v1/unknown-advisory-roundtrip.json +1 -0
  54. package/node_modules/@forge/contracts/fixtures/v1/unknown-consequential-reject.json +1 -0
  55. package/node_modules/@forge/contracts/fixtures/v1/valid-full.json +1 -0
  56. package/node_modules/@forge/contracts/fixtures/v1/valid-minimal.json +1 -0
  57. package/node_modules/@forge/contracts/fixtures/v1/wrong-capability-digest.json +1 -0
  58. package/node_modules/@forge/contracts/index.js +32 -0
  59. package/node_modules/@forge/contracts/package.json +35 -0
  60. package/node_modules/@forge/contracts/schemas/v1/capability-manifest.schema.json +1 -0
  61. package/node_modules/@forge/contracts/schemas/v1/claim-request.schema.json +1 -0
  62. package/node_modules/@forge/contracts/schemas/v1/context-packet.schema.json +1 -0
  63. package/node_modules/@forge/contracts/schemas/v1/delivery-receipt.schema.json +1 -0
  64. package/node_modules/@forge/contracts/schemas/v1/feedback-report.schema.json +1 -0
  65. package/node_modules/@forge/contracts/schemas/v1/lease-receipt.schema.json +1 -0
  66. package/node_modules/@forge/contracts/schemas/v1/monitor-event.schema.json +1 -0
  67. package/node_modules/@forge/contracts/schemas/v1/monitor-receipt.schema.json +1 -0
  68. package/node_modules/@forge/contracts/schemas/v1/run-receipt.schema.json +1 -0
  69. package/node_modules/@forge/contracts/schemas/v1/structured-error.schema.json +1 -0
  70. package/node_modules/@forge/contracts/schemas/v1/work-packet.schema.json +1 -0
  71. package/node_modules/@forge/contracts/src/baseline.js +23 -0
  72. package/node_modules/@forge/contracts/src/canonical.js +151 -0
  73. package/node_modules/@forge/contracts/src/definitions.js +176 -0
  74. package/node_modules/@forge/contracts/src/identity.js +42 -0
  75. package/node_modules/@forge/contracts/src/schema.js +72 -0
  76. package/node_modules/@forge/contracts/src/validate.js +305 -0
  77. package/node_modules/@forge/flow/index.js +114 -0
  78. package/node_modules/@forge/flow/package.json +34 -0
  79. package/node_modules/@forge/flow/src/bounded-loop.js +415 -0
  80. package/node_modules/@forge/flow/src/efficiency-supervisor.js +89 -0
  81. package/node_modules/@forge/flow/src/executor.js +279 -0
  82. package/node_modules/@forge/flow/src/monitor-durability.js +419 -0
  83. package/node_modules/@forge/flow/src/monitor-runtime.js +460 -0
  84. package/node_modules/@forge/flow/src/process-lifecycle.js +469 -0
  85. package/node_modules/@forge/flow/src/skill-runtime.js +343 -0
  86. package/node_modules/@forge/memory/index.js +331 -0
  87. package/node_modules/@forge/memory/package.json +34 -0
  88. package/node_modules/@forge/memory/src/authority-provider.js +67 -0
  89. package/node_modules/@forge/memory/src/backend-registry.js +166 -0
  90. package/node_modules/@forge/memory/src/feedback-intake.js +243 -0
  91. package/node_modules/@forge/memory/src/pr-lifecycle-authority.js +946 -0
  92. package/node_modules/@forge/memory/src/usage-evidence.js +205 -0
  93. package/package.json +12 -2
  94. package/packages/flow/node_modules/@forge/contracts/compatibility-matrix.v1.json +1 -0
  95. package/packages/flow/node_modules/@forge/contracts/contract-baseline.v1.json +134 -0
  96. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/canonical-hash.json +1 -0
  97. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/contract-inputs.v1.json +71 -0
  98. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/feedback-secret-reject.json +1 -0
  99. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/identity-conflict.json +1 -0
  100. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/malformed-envelope-inputs.v1.json +16 -0
  101. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/malformed-missing-required.json +1 -0
  102. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/monitor-bounds-reject.json +1 -0
  103. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/monitor-receipt-privacy-reject.json +1 -0
  104. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/not-executed-receipt.json +1 -0
  105. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/privacy-redaction.json +1 -0
  106. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/retry-identical.json +1 -0
  107. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/stale-authority-reject.json +1 -0
  108. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/structured-error-privacy-reject.json +1 -0
  109. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-live-reject.json +1 -0
  110. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-test-reject.json +1 -0
  111. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/unknown-advisory-roundtrip.json +1 -0
  112. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/unknown-consequential-reject.json +1 -0
  113. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/valid-full.json +1 -0
  114. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/valid-minimal.json +1 -0
  115. package/packages/flow/node_modules/@forge/contracts/fixtures/v1/wrong-capability-digest.json +1 -0
  116. package/packages/flow/node_modules/@forge/contracts/index.js +32 -0
  117. package/packages/flow/node_modules/@forge/contracts/package.json +35 -0
  118. package/packages/flow/node_modules/@forge/contracts/schemas/v1/capability-manifest.schema.json +1 -0
  119. package/packages/flow/node_modules/@forge/contracts/schemas/v1/claim-request.schema.json +1 -0
  120. package/packages/flow/node_modules/@forge/contracts/schemas/v1/context-packet.schema.json +1 -0
  121. package/packages/flow/node_modules/@forge/contracts/schemas/v1/delivery-receipt.schema.json +1 -0
  122. package/packages/flow/node_modules/@forge/contracts/schemas/v1/feedback-report.schema.json +1 -0
  123. package/packages/flow/node_modules/@forge/contracts/schemas/v1/lease-receipt.schema.json +1 -0
  124. package/packages/flow/node_modules/@forge/contracts/schemas/v1/monitor-event.schema.json +1 -0
  125. package/packages/flow/node_modules/@forge/contracts/schemas/v1/monitor-receipt.schema.json +1 -0
  126. package/packages/flow/node_modules/@forge/contracts/schemas/v1/run-receipt.schema.json +1 -0
  127. package/packages/flow/node_modules/@forge/contracts/schemas/v1/structured-error.schema.json +1 -0
  128. package/packages/flow/node_modules/@forge/contracts/schemas/v1/work-packet.schema.json +1 -0
  129. package/packages/flow/node_modules/@forge/contracts/src/baseline.js +23 -0
  130. package/packages/flow/node_modules/@forge/contracts/src/canonical.js +151 -0
  131. package/packages/flow/node_modules/@forge/contracts/src/definitions.js +176 -0
  132. package/packages/flow/node_modules/@forge/contracts/src/identity.js +42 -0
  133. package/packages/flow/node_modules/@forge/contracts/src/schema.js +72 -0
  134. package/packages/flow/node_modules/@forge/contracts/src/validate.js +305 -0
  135. package/packages/memory/node_modules/@forge/contracts/compatibility-matrix.v1.json +1 -0
  136. package/packages/memory/node_modules/@forge/contracts/contract-baseline.v1.json +134 -0
  137. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/canonical-hash.json +1 -0
  138. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/contract-inputs.v1.json +71 -0
  139. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/feedback-secret-reject.json +1 -0
  140. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/identity-conflict.json +1 -0
  141. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/malformed-envelope-inputs.v1.json +16 -0
  142. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/malformed-missing-required.json +1 -0
  143. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/monitor-bounds-reject.json +1 -0
  144. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/monitor-receipt-privacy-reject.json +1 -0
  145. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/not-executed-receipt.json +1 -0
  146. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/privacy-redaction.json +1 -0
  147. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/retry-identical.json +1 -0
  148. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/stale-authority-reject.json +1 -0
  149. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/structured-error-privacy-reject.json +1 -0
  150. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-live-reject.json +1 -0
  151. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/structured-error-stripe-test-reject.json +1 -0
  152. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/unknown-advisory-roundtrip.json +1 -0
  153. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/unknown-consequential-reject.json +1 -0
  154. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/valid-full.json +1 -0
  155. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/valid-minimal.json +1 -0
  156. package/packages/memory/node_modules/@forge/contracts/fixtures/v1/wrong-capability-digest.json +1 -0
  157. package/packages/memory/node_modules/@forge/contracts/index.js +32 -0
  158. package/packages/memory/node_modules/@forge/contracts/package.json +35 -0
  159. package/packages/memory/node_modules/@forge/contracts/schemas/v1/capability-manifest.schema.json +1 -0
  160. package/packages/memory/node_modules/@forge/contracts/schemas/v1/claim-request.schema.json +1 -0
  161. package/packages/memory/node_modules/@forge/contracts/schemas/v1/context-packet.schema.json +1 -0
  162. package/packages/memory/node_modules/@forge/contracts/schemas/v1/delivery-receipt.schema.json +1 -0
  163. package/packages/memory/node_modules/@forge/contracts/schemas/v1/feedback-report.schema.json +1 -0
  164. package/packages/memory/node_modules/@forge/contracts/schemas/v1/lease-receipt.schema.json +1 -0
  165. package/packages/memory/node_modules/@forge/contracts/schemas/v1/monitor-event.schema.json +1 -0
  166. package/packages/memory/node_modules/@forge/contracts/schemas/v1/monitor-receipt.schema.json +1 -0
  167. package/packages/memory/node_modules/@forge/contracts/schemas/v1/run-receipt.schema.json +1 -0
  168. package/packages/memory/node_modules/@forge/contracts/schemas/v1/structured-error.schema.json +1 -0
  169. package/packages/memory/node_modules/@forge/contracts/schemas/v1/work-packet.schema.json +1 -0
  170. package/packages/memory/node_modules/@forge/contracts/src/baseline.js +23 -0
  171. package/packages/memory/node_modules/@forge/contracts/src/canonical.js +151 -0
  172. package/packages/memory/node_modules/@forge/contracts/src/definitions.js +176 -0
  173. package/packages/memory/node_modules/@forge/contracts/src/identity.js +42 -0
  174. package/packages/memory/node_modules/@forge/contracts/src/schema.js +72 -0
  175. package/packages/memory/node_modules/@forge/contracts/src/validate.js +305 -0
  176. package/scripts/check-forge-token.js +85 -67
  177. package/scripts/gen-embedded-assets.mjs +31 -5
  178. package/scripts/test-full-suite.js +69 -15
  179. package/scripts/test.js +11 -1
package/CHANGELOG.md CHANGED
@@ -7,6 +7,41 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  > **Note**: `/check` was renamed to `/validate` and `/merge` was renamed to `/premerge` in v0.0.3. Historical entries below may use the old names.
9
9
 
10
+ ## [Unreleased]
11
+
12
+ ## [0.1.0-beta.8] - 2026-09-26
13
+
14
+ ### Added
15
+
16
+ - **Deterministic full generation for the test workflow.** `forge release generate-test-workflow --expect-head <sha>` renders `.github/workflows/test.yml` from its canonical staged template and Bun pin under an exact actor, worktree, source-HEAD, path, operation, and content-bound authorization. Bun pin updates delegate this workflow to the same renderer, preventing partial pin replacement from authorizing unrelated CI changes. (PR #567; issue `a541c8ca-579d-4b8e-9bfd-7980b7547302`)
17
+ - **Optional transparent GitHub account routing per clone.** `forge github use <login> --auto` extends the existing secure account binding to shell-resolved `gh` and HTTPS Git commands when Forge's router is first on PATH, so concurrent terminals and harness shells can use different native GitHub CLI accounts without per-launch wrapping. Unbound and non-enabled clones pass through unchanged; local-only commands and `gh auth` remain native, while network-capable aliases and extensions inherit the selected account. V1 is public `github.com` only; built-in destination conflicts and non-public targets fail closed, and arbitrary user-installed code remains outside Forge's wrong-destination guarantee. The machine-local router registry must refuse uninstall while another verified clone remains enabled and must never store credentials. Release acceptance requires the installed PATH chain and warm median targets of 250 ms for unbound/local-only routing and 1,000 ms for bound `gh` and helper resolution. Forge never stores a token. (PR #563; issue `8168bee5-b74e-44e5-8fd8-1b9ccb917c1d`)
18
+
19
+ ### Fixed
20
+
21
+ - **The npm package installs with Bun.** `@forge/flow`, `@forge/memory`, and `@forge/contracts` are now only bundled in `forge-workflow`, not also declared as registry `dependencies`; Bun resolved those entries from the registry, where the unpublished workspaces return 404, so `bun add forge-workflow@0.1.0-beta.7` failed. The workspaces stay linked for development as `devDependencies`, and npm installs are unchanged. The packed-install smoke now also installs the tarball with `bun add` and runs `forge --version` and `forge setup --quick --yes`. (issue `95ed8d5f-0578-48de-bc44-17b5afa48161`)
22
+ - **Canonical validation exposes an honest full-suite resource budget.** `forge validate --shards N` forwards a positive integer budget to the Forge scheduler. Windows budgets that cannot fund a required worker fail before test workers start; automatic defaults normalize visibly. Requested/effective or rejection evidence is retained and displayed by the CLI without changing required tests, deadlines, or receipt identity. (#574, issue `e0cb0671-735c-4980-944b-286d6f78fe48`)
23
+ - **Standalone package smoke installs identify packed packages before npm builds the install tree.** The fixture now carries each package name from the same successful `npm pack --json` result into its local tarball specification, avoiding npm's redundant add-request name discovery while preserving tarball extraction, bundled workspaces, installed identity checks, CLI version/setup coverage, and the existing deadlines. (issue `6b432d06-5ab3-4188-8814-0ae056430cac`)
24
+ - **Exact version queries avoid loading the command graph.** Direct `forge --version` and `forge -V` print the packaged version before loading command modules, reducing cold startup during standalone installation checks. Imported helpers and mixed command arguments keep their existing behavior. (issue `6b432d06-5ab3-4188-8814-0ae056430cac`)
25
+ - **The workflow projection hook test avoids a duplicate Git scan.** It retains the real commit-hook rejection and verifies the exact projection mismatch and zero authority consumption through existing fixture readers, without changing test deadlines or production authorization. (issue `e70683d6-f8df-471b-b859-a6dacb69825c`)
26
+ - **Segmented Flow monitor checkpoints no longer repeat full-tail compression searches.** Recovery planning bounds records once and adapts from the last accepted segment width while preserving payload limits, ordered records, integrity metadata, terminal snapshot placement, and fail-closed interrupted-plan recovery. (issues `9eb98ac0-ebe1-4604-a57a-5fefadad6682`, `6648ef9b-381a-4a19-ad9a-06341e62676f`)
27
+ - **The failed full-suite child regression now selects only its test gate.** Its synthetic validation fixture still proves fail-closed classification and stale receipt deletion without running unrelated repository checks. (issue `3ce7c502-ccaf-46e6-af12-961ff5ffd2e7`)
28
+ - **The smart-status CRLF fixture avoids a redundant wrapper on compatible Windows jq executables.** It verifies genuine CRLF bytes before selecting native jq, preserves the CRLF-producing fallback for LF-only commands and non-Windows hosts, and propagates jq failures through that wrapper without changing the 20-second test deadline or production status behavior. (issue `c8ece1ef-e410-45b8-98bc-ca184fa4f828`)
29
+ - **Forge pushes use the supervised full-suite runner when validation cannot be reused.** Consumer repositories retain their configured test command, and failed or incomplete runs prevent the push. Git arguments retain their order across Forge's passthrough delimiter. (issues `11e5ef49-5ae3-4cd0-a32e-0f233fc00ce8`, `e8b5e91f-d9f3-40e3-921a-e12c392054f1`)
30
+ - **Slow pre-push hooks no longer expire authorization for completed checks.** Reuse is bound to the live push, its worktree, branch, unchanged inputs, and completed gates; ended or mismatched invocations run the ordinary hooks. Quick pushes continue to omit tests explicitly. (issue `291d09b0-b3c4-41a6-8fa0-6b843e7fad05`)
31
+ - **The test workflow template marker no longer resembles a credential assignment.** Internal renderer code now describes `__BUN_VERSION__` as a placeholder, so the shipped-library secret scan accepts the deterministic workflow writer without weakening its detection rules or changing generated workflow bytes. (issue `0acc4a68-ab0a-4008-83bf-6e954d075007`)
32
+
33
+ ## [0.1.0-beta.7] - 2026-09-11
34
+
35
+ ### Changed
36
+
37
+ - **A clean full validation is reused by the next unchanged `forge push`.** Forge stores a one-hour, signed local receipt bound to the canonical worktree, exact HEAD, clean state, Forge, Bun, and Node runtimes, and complete nonzero gate results. Branch protection and lint still run; invalid or stale evidence falls back to the full suite, while raw Git pushes and CI remain independent.
38
+ - **A local full suite no longer reruns its already-included E2E directory.** Targeted E2E execution is unchanged, while full validation avoids an exact duplicate. (issue `4aed2cc8-57eb-451b-bb3a-02bbdd93af49`)
39
+ - **The shared public contract package is now `@forge/contracts`.** The product-neutral name replaces the former unpublished Memory-qualified name before beta.7, and all three scoped packages explicitly publish as public MIT packages linked to the public Forge repository. (issue `4d41ffb7-8793-4a17-a30b-92130db69672`)
40
+
41
+ ### Fixed
42
+
43
+ - **The npm package now includes every internal runtime workspace.** Root runtime imports use the `@forge/memory`, `@forge/contracts`, and `@forge/flow` package boundaries, and npm bundles those unpublished workspaces into `forge-workflow`. The installed-package smoke accepts npm 10 lifecycle output before its JSON payload, so Node 22 verifies the same package correctly. A fresh packed install now runs `forge --version` and `forge setup --quick --yes`, preventing the `MODULE_NOT_FOUND` failure in v0.1.0-beta.6. (issue `95372af8-da00-42e0-af64-0111ddab3405`)
44
+
10
45
  ## [0.1.0-beta.6] - 2026-09-11
11
46
 
12
47
  ### Added
@@ -23,6 +58,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
23
58
 
24
59
  ### Fixed
25
60
 
61
+ - **Bun workflow commit-boundary validation avoids redundant Windows process startup.** Protected-state checks batch the complete source/index pin snapshot into four Git calls and reuse successful executable resolution for an unchanged command environment; Windows full-suite scheduling defers unit work until subprocess workers release the machine, preserving every assertion and fail-closed check without increasing timeouts. (#557, issue `c56c483c-34ef-4354-a976-030464ac416e`)
26
62
  - **Legacy claim repair now compares exact Windows file identities and timestamps.** BigInt filesystem metadata prevents rounded device/inode collisions from treating distinct files as aliases, while nanosecond timestamps keep recovery-state drift checks fail closed. (#556, issue `7e164066-4d41-4d14-90ef-1b7ee14e2b73`)
27
63
  - **Dep-guard apply-decision tests no longer contend with shared Windows subprocess shards.** The process-heavy suite now uses the existing exclusive resource lane, with exact scheduler coverage and no timeout increase or exemption. (issue `762ae565-0059-4827-9c3d-b4249f2b3364`)
28
64
  - **Bun workflow proof tests no longer contend with shared full-suite shards.** The process-heavy commit-boundary proof now uses the existing exclusive resource lane, with exact scheduler coverage and no timeout increase or exemption. (#552, issue `2daab8c0-34d6-4378-8cf4-dce6fdbfb86b`)
@@ -0,0 +1,15 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const { isLocalGhInvocation, runNativeGh } = require('../lib/native-gh');
5
+
6
+ function runProxyEntrypoint(args, projectRoot = process.cwd(), options = {}) {
7
+ const ghArgs = args[0] === '--' ? args.slice(1) : args;
8
+ if (isLocalGhInvocation(ghArgs)) return (options.runNative || runNativeGh)(ghArgs, projectRoot, options);
9
+ const runProxy = options.runProxy || require('../lib/gh-proxy').runGhProxy;
10
+ return runProxy(ghArgs, projectRoot);
11
+ }
12
+
13
+ if (require.main === module) process.exitCode = runProxyEntrypoint(process.argv.slice(2));
14
+
15
+ module.exports = { runProxyEntrypoint };
@@ -0,0 +1,21 @@
1
+ #!/usr/bin/env node
2
+ 'use strict';
3
+
4
+ const fs = require('node:fs');
5
+
6
+ function runCredentialEntrypoint(args, projectRoot = process.cwd(), options = {}) {
7
+ const operation = args[0] === '--' ? args[1] : args[0];
8
+ if (!['get', 'store', 'erase'].includes(operation)) return 1;
9
+ try {
10
+ const input = operation === 'get' ? (options.readInput || (() => fs.readFileSync(0, 'utf8')))() : '';
11
+ const runCredential = options.runCredential || require('../lib/github-credential').runCredentialHelper;
12
+ return runCredential(operation, { input, projectRoot });
13
+ } catch {
14
+ (options.writeError || (value => process.stderr.write(value)))('Forge could not read the Git credential request.\n');
15
+ return 1;
16
+ }
17
+ }
18
+
19
+ if (require.main === module) process.exitCode = runCredentialEntrypoint(process.argv.slice(2));
20
+
21
+ module.exports = { runCredentialEntrypoint };
package/bin/forge.js CHANGED
@@ -46,6 +46,11 @@ const { execSync } = require('node:child_process');
46
46
  const packageDir = path.dirname(__dirname);
47
47
  const packageJson = require('../package.json');
48
48
  const VERSION = packageJson.version;
49
+ const args = process.argv.slice(2);
50
+
51
+ if (require.main === module && args.length === 1 && ['--version', '-V'].includes(args[0])) {
52
+ console.log(`Forge v${VERSION}`);
53
+ } else {
49
54
 
50
55
  // Load PluginManager for discoverable agent architecture
51
56
  const PluginManager = require('../lib/plugin-manager');
@@ -104,7 +109,6 @@ const { detectHusky, migrateHusky } = require('../lib/husky-migration');
104
109
 
105
110
  // Get the project root (let allows reassignment after --path flag handling)
106
111
  let projectRoot = process.env.INIT_CWD || process.cwd();
107
- const args = process.argv.slice(2);
108
112
 
109
113
  // Incremental setup state (set during main() from parsed flags)
110
114
  let FORCE_MODE = false;
@@ -3650,6 +3654,16 @@ async function handleExternalServices(skipExternal, selectedAgents) {
3650
3654
 
3651
3655
  async function main() {
3652
3656
  let command = args[0];
3657
+ if (command === 'github' && args[1] === 'credential') {
3658
+ const { runCredentialEntrypoint } = require('./forge-github-credential');
3659
+ process.exitCode = runCredentialEntrypoint(args.slice(2), process.cwd());
3660
+ return;
3661
+ }
3662
+ if (command === 'github' && args[1] === 'proxy' && args[2] === '--') {
3663
+ const { runProxyEntrypoint } = require('./forge-gh-proxy');
3664
+ process.exitCode = runProxyEntrypoint(args.slice(2), process.cwd());
3665
+ return;
3666
+ }
3653
3667
  const flags = parseFlags();
3654
3668
  const suppressJsonIntrospectionOutput = ['options', 'explain'].includes(command) && args.includes('--json');
3655
3669
  const suppressCommandJsonOutput = args.includes('--json');
@@ -4520,3 +4534,4 @@ module.exports = {
4520
4534
  validateDirectoryPathInput,
4521
4535
  validateUserInput,
4522
4536
  };
4537
+ }
@@ -1,6 +1,6 @@
1
1
  # Migration Guide
2
2
 
3
- Use this guide when moving older Forge docs, habits, or installed scaffolding toward the v0.1.0-beta.6 public framing.
3
+ Use this guide when moving older Forge docs, habits, or installed scaffolding toward the v0.1.0-beta.8 public framing.
4
4
 
5
5
  ## What Changed
6
6
 
@@ -39,8 +39,9 @@ forge setup --agents claude,cursor
39
39
 
40
40
  ## Version Labels
41
41
 
42
- - `0.1.0-beta.6` is the current prerelease package version.
43
- - `0.1.0-beta.5` is the previous prerelease package version.
42
+ - `0.1.0-beta.8` is the current prerelease package version.
43
+ - `0.1.0-beta.7` is the previous prerelease package version; npm installs it, but Bun cannot, because it also declares the bundled runtime workspaces as registry dependencies. Upgrade directly to beta.8.
44
+ - `0.1.0-beta.6` omits required runtime workspaces from its npm tarball; do not install it.
44
45
  - Internal labels such as `0.0.19` or `v3` describe roadmap slices or historical codenames. Do not present them as current package versions.
45
46
 
46
47
  ## Safe Upgrade Path
@@ -2,9 +2,9 @@
2
2
 
3
3
  This page documents release readiness. Package publishing still requires the explicit publish step after merge.
4
4
 
5
- ## v0.1.0-beta.6 Boundary
5
+ ## v0.1.0-beta.8 Boundary
6
6
 
7
- v0.1.0-beta.6 is the current prerelease boundary. The release branch declares `0.1.0-beta.6`; publish only after the release PR is merged, tagged, and validated on that exact SHA.
7
+ v0.1.0-beta.8 is the current prerelease boundary. The release branch declares `0.1.0-beta.8`; publish only after the release PR is merged, tagged, and validated on that exact SHA.
8
8
 
9
9
  Keep these release steps explicit:
10
10
 
@@ -25,6 +25,8 @@ npm pack --dry-run
25
25
 
26
26
  The trusted npm workflow is generated through `forge release generate-npm-workflow --expect-head "$(git rev-parse HEAD)"`; the full expected SHA must match the current checkout, and direct workflow edits remain blocked. The generated authorization is bound to that SHA, actor, worktree, path, and exact content, so moving HEAD before staging also fails closed. On a release event, the workflow resolves the tag once, runs the complete supported repository suite on that immutable commit, and allows publication only when the attributable suite receipt, verification checkout, and publish checkout all name the same SHA.
27
27
 
28
+ Generate the canonical test workflow through `forge release generate-test-workflow --expect-head "$(git rev-parse HEAD)"`. The command renders the staged `lib/workflow-templates/test.yml` with the staged manifest's exact Bun version, rejects unrelated target bytes, and binds the protected write to the expected HEAD and exact generated content. For a Bun version change, use `forge release update-bun-pins --expect-head "$(git rev-parse HEAD)"`; it delegates `test.yml` to the same full renderer while preserving the pin-only updates for the other workflows.
29
+
28
30
  For docs-heavy changes, also run a Markdown link check if available. If no docs checker exists and adding one would broaden the PR, create a follow-up issue instead.
29
31
 
30
32
  ## Packaging Check
@@ -43,7 +45,7 @@ Release notes should include:
43
45
  - adapter compatibility
44
46
  - DeepWiki refresh checklist
45
47
 
46
- The v0.1.0-beta.6 release notes live in [CHANGELOG.md](../../CHANGELOG.md).
48
+ The v0.1.0-beta.8 release notes live in [CHANGELOG.md](../../CHANGELOG.md).
47
49
 
48
50
  ## Rollback
49
51
 
@@ -19,6 +19,20 @@ bun run check
19
19
 
20
20
  Security audit behavior distinguishes blocking high/critical vulnerabilities from lower-severity warnings.
21
21
 
22
+ ## Canonical Validation Resource Budget
23
+
24
+ In a Forge source checkout, run the receipt-producing command from a clean, committed worktree:
25
+
26
+ ```bash
27
+ forge validate --shards 2
28
+ ```
29
+
30
+ `--shards N` accepts a positive integer resource budget for the full-suite scheduler. Unit workers cost one unit; on Windows, subprocess and exclusive workers cost two. An explicit budget below a required worker's cost fails before test workers start. Omitting the flag selects an automatic budget, which may increase to the minimum needed to run every lane. Output records the requested and effective budget, or the requested and minimum budget on rejection.
31
+
32
+ These weights govern Forge's lane workers; they do not impose an operating-system limit on every process a test may create. The budget changes scheduling, not required test coverage or deadlines. Consumer repositories cannot use this flag because their test commands do not implement Forge's scheduler contract.
33
+
34
+ A successful full validation can be reused by the next unchanged `forge push` under the existing receipt checks. Changing the source or validation inputs requires fresh evidence.
35
+
22
36
  ## Supporting Commands
23
37
 
24
38
  ```bash
@@ -28,74 +28,123 @@ from that session before interactive login; do not print their values.
28
28
  Inside the work clone:
29
29
 
30
30
  ```sh
31
- forge github use WORK_LOGIN
31
+ forge github use WORK_LOGIN --auto
32
32
  forge github status
33
- forge github run -- codex
33
+ codex
34
34
  ```
35
35
 
36
36
  Inside the personal clone, in another terminal:
37
37
 
38
38
  ```sh
39
- forge github use PERSONAL_LOGIN
39
+ forge github use PERSONAL_LOGIN --auto
40
40
  forge github status
41
- forge github run -- t3
41
+ t3
42
42
  ```
43
43
 
44
44
  Replace the uppercase labels with your GitHub logins. Both sessions can remain
45
45
  open together. Substitute any installed, trusted CLI harness or shell for `codex`
46
46
  or `t3`, such as `claude`, `pwsh`, or `bash`.
47
47
 
48
- `use` verifies the named stored account and repository access, then writes only
49
- `git config --local github.account`. It does not log in, switch the globally
50
- active account, or modify another clone. Linked Git worktrees normally share this
51
- clone-local configuration; use separate clones when you need separate bindings.
52
-
53
- Supported GitHub-dependent Forge commands use the binding automatically. To give
54
- the same identity to a harness's own `gh` calls, start that harness with
55
- `forge github run --`. Already-running terminals and harnesses are not changed.
56
-
57
- The launcher deliberately gives its child GitHub authority through transient
58
- environment variables. Only launch programs you trust with that account. Forge
59
- does not persist the token, but the launched program can access it. Harnesses or
60
- tools using their own credentials instead of the inherited `gh` environment are
61
- outside this guarantee.
48
+ `use --auto` verifies the named stored account and repository access, installs a
49
+ marked router beside the current Forge launcher, then writes only clone-local Git
50
+ settings: the account selector, an automatic-routing marker, and an absolute
51
+ GitHub HTTPS credential-helper route. It refuses to overwrite a non-Forge `gh`
52
+ launcher in that directory. It does not store a credential, log in, switch the
53
+ globally active account, or modify another clone. Linked Git
54
+ worktrees normally share this clone-local configuration; use separate clones when
55
+ you need separate bindings. The machine-local clone registration is durable before
56
+ automatic routing is enabled; disabling routing removes the clone setting before
57
+ its registry entry. If either ordering step cannot complete, automatic routing
58
+ stays disabled or fails closed.
59
+
60
+ The opt-in `gh` router reads the clone marker on each invocation. In an enabled
61
+ clone it selects the named account for the real GitHub CLI process; elsewhere it
62
+ passes through unchanged. Zero-argument, help, completion, configuration, and
63
+ other local-only `gh` operations also bypass account resolution. The complete
64
+ `gh auth` namespace always passes through so login and recovery remain native.
65
+ Network-capable built-in commands, configured aliases, and installed extensions
66
+ inherit the selected child account; they do not trigger per-command account
67
+ discovery. Built-in destinations from command arguments, URLs, `GH_HOST`,
68
+ `GH_REPO`, and repository inference are checked for conflicts and public-host
69
+ support before a credential is injected. Arbitrary code inside a user-installed
70
+ alias or extension is outside Forge's wrong-destination guarantee. HTTPS Git uses
71
+ the same account through the clone-local helper. SSH key selection remains
72
+ controlled by SSH configuration.
73
+ Enablement stops if another `gh` resolves before Forge on `PATH`, instead of
74
+ claiming automatic routing when the router cannot run. Run automatic setup from
75
+ an installed Forge command; transient `npx`/`bunx` package-runner shims are ignored.
76
+
77
+ Repositories can therefore be opened normally in T3 Code, VS Code, terminals,
78
+ Codex, or Claude Code. Shell-based `gh` and HTTPS Git commands started with that
79
+ clone as their working directory route automatically when the application PATH
80
+ resolves Forge's router first. Start a new terminal or application after changing
81
+ PATH order; POSIX shells may also need `hash -r`. A program that invokes an
82
+ absolute `gh` executable or bypasses shell command resolution cannot be
83
+ intercepted and must use `forge github run -- PROGRAM` or its own account setting.
84
+
85
+ `forge github use LOGIN` without `--auto` preserves the original explicit mode.
86
+ Supported Forge routes use the binding, and arbitrary trusted children can still
87
+ be launched with `forge github run -- PROGRAM`.
62
88
 
63
89
  ## Check or undo
64
90
 
65
91
  ```sh
66
92
  forge github status --json
93
+ forge github auto --disable
67
94
  forge github unset
95
+ forge github router --uninstall --force
68
96
  ```
69
97
 
70
98
  Status reports the selected and verified login, clone-local binding source,
71
- effective Git author, origin transport, helper classification, and repository
99
+ router reachability, effective Git author, origin transport, helper classification, and repository
72
100
  access. It does not print tokens, raw remote URLs, or raw credential-helper
73
101
  commands. Treat author and account labels as personal information when sharing
74
102
  diagnostics.
75
103
 
76
- `unset` is safe to repeat. It removes only the clone binding, not either stored
77
- login. It does not revoke authority from a running child: close that session and
78
- start a new one after changing or removing a binding.
104
+ `auto --disable` removes only Forge-owned transparent routing and keeps the clone
105
+ binding. `unset` is safe to repeat and removes both the binding and Forge-owned
106
+ routing. Forge records only canonical clone and Git common-directory paths in a
107
+ machine-local registry under the router lock; it stores no credentials. Current
108
+ enabled state comes from each clone's live `github.auto` value, and router
109
+ ownership comes from the launcher marker. Missing or corrupt registry state, an
110
+ enabled clone absent from the registry, or an unavailable registered clone fails
111
+ closed and leaves the shared router in place. The shared router remains while
112
+ another clone uses it and passes through unchanged outside enabled clones. When
113
+ `router --uninstall --force` runs, Forge prunes only entries it can prove are
114
+ disabled and refuses to remove marked router files while any verified clone
115
+ remains. None of these commands removes a
116
+ stored login or revokes authority from a running child: close that process after
117
+ changing or removing a binding.
118
+
119
+ The safe order is: disable or unset every opted-in clone, run
120
+ `router --uninstall --force` last, then uninstall Forge itself. Package managers
121
+ cannot clean clone-local Git configuration after the executable is gone. If that
122
+ order was missed, reinstall Forge and run `forge github auto --disable` or
123
+ `forge github unset`; these commands remove only Forge-owned configuration. Do not
124
+ use `git config --unset-all` because another local HTTPS helper may share the same
125
+ key.
79
126
 
80
127
  ## Three separate identities
81
128
 
82
129
  | Identity | Controlled by |
83
130
  | --- | --- |
84
- | GitHub API account used by supported Forge routes | Clone-local `github.account` |
131
+ | GitHub API account used by Forge and automatic `gh` routes | Clone-local `github.account` + `github.auto` |
85
132
  | Commit author | Git `user.name` and `user.email` |
86
133
  | Fetch/push authentication | SSH configuration or the HTTPS credential helper |
87
134
 
88
- Forge does not change your author, origin, SSH keys, or credential helper.
135
+ Forge does not change your author, origin, or SSH keys. Automatic mode owns only
136
+ the clone-local GitHub HTTPS helper it installs and refuses to replace another
137
+ clone-local helper.
89
138
  Existing SSH keys and host aliases remain valid; custom aliases must resolve to
90
- `github.com` for Forge's repository-access check. HTTPS and SSH GitHub.com origins
91
- are supported; insecure HTTP/Git transports and Enterprise hosts are not V1 targets.
92
-
93
- HTTPS has one important coupling: if Git uses `gh auth git-credential` as its
94
- helper, Git commands inside the explicitly launched session may use that session's
95
- selected token too. Other HTTPS helpers and SSH key selection remain independent.
96
- For native multi-account transport, keep separate SSH keys/aliases or configure
97
- your HTTPS credential manager to distinguish repository URLs (`useHttpPath`).
98
- These are optional Git choices, not Forge setup steps; see
139
+ `github.com` for Forge's repository-access check. V1 automatic routing supports
140
+ public `github.com` only. An explicit non-public `--hostname` or remote host fails
141
+ closed before the selected public credential is injected; GitHub Enterprise
142
+ routing remains native and outside this feature. HTTPS and SSH GitHub.com origins
143
+ are supported; insecure HTTP/Git transports are not V1 targets.
144
+
145
+ Automatic mode routes canonical GitHub HTTPS remotes through the selected native
146
+ GitHub CLI account. SSH key selection remains independent; keep separate SSH
147
+ keys/aliases when using SSH. See
99
148
  [GitHub's multi-account transport guide](https://docs.github.com/en/account-and-profile/how-tos/account-management/managing-multiple-accounts).
100
149
 
101
150
  ## Platforms and recovery
@@ -119,16 +168,43 @@ and `--path`. Git Bash is still required for Forge's existing Windows Bash helpe
119
168
  - **Unsupported GitHub CLI:** upgrade `gh` to a version supporting named-account
120
169
  token retrieval. Unbound projects do not need that capability.
121
170
  - **Program not found:** install the harness and check its executable name/PATH.
122
-
123
- The supported workflow entrypoints are `forge`, `forge-workflow`, and the compiled
124
- Forge executable. Direct developer invocation of `node bin/forge-cmd.js` is an
171
+ - **Router conflict:** another `gh` launcher already exists beside Forge. Forge
172
+ leaves it untouched; move or remove it deliberately before retrying.
173
+ - **Router shadowed:** put Forge's launcher directory before the native GitHub CLI
174
+ directory on PATH, restart the application, and rerun `use --auto`.
175
+ - **Explicit repository override:** `gh -R` and `gh --repo` still use the account
176
+ bound to the current working clone; run them from the clone whose identity you intend.
177
+ - **Configured `gh` alias or extension:** network-capable aliases and extensions inherit
178
+ the selected clone account. Local-only or account-management commands retain their
179
+ native bypass rules. Forge cannot guarantee the destination of arbitrary code inside
180
+ an opaque alias or extension; inspect it yourself if it targets another host.
181
+
182
+ The supported workflow entrypoints are `forge`, `forge-workflow`, and a compiled
183
+ Forge executable whose directory is on PATH; it installs the router beside itself. Rerun `use --auto`
184
+ after moving a compiled executable or changing Forge installation managers so
185
+ the marked launchers refresh their target. Direct developer invocation of `node bin/forge-cmd.js` is an
125
186
  internal legacy utility, not an account-isolated V1 entrypoint.
126
187
 
188
+ ## Performance and installed-product acceptance
189
+
190
+ Automatic routing is designed to be warm and local on the common path. Release
191
+ acceptance measures the added latency over at least five warm runs, excluding the
192
+ native command itself: a median of at most 250 ms for unbound or local-only
193
+ routing, 1,000 ms for a bound `gh` route before native command work, and 1,000 ms
194
+ for credential-helper resolution. These are acceptance targets, not a promise
195
+ about a slow machine or the remote GitHub operation.
196
+
197
+ The installed-product acceptance run must exercise the generated PATH launchers
198
+ and the real Forge router, proxy, and credential-helper chain in two concurrent
199
+ clone-local contexts, plus unbound and fail-closed cases. The native `gh` process
200
+ may be substituted only at that process boundary; Forge itself is not substituted.
201
+ The release run uses two securely stored accounts and never prints their tokens.
202
+
127
203
  ## CuraPod adoption
128
204
 
129
205
  Only after this Forge feature is merged, the merged build is installed, and its
130
206
  acceptance checks pass: open each intended CuraPod clone, run
131
- `forge github use WORK_LOGIN`, verify `forge github status`, then launch the
132
- chosen harness with `forge github run --`. Keep personal clones separately bound
207
+ `forge github use WORK_LOGIN --auto`, verify `forge github status`, then open the
208
+ chosen harness normally. Keep personal clones separately bound
133
209
  to `PERSONAL_LOGIN`. This example does not discover or modify existing CuraPod
134
210
  repositories, remotes, keys, or accounts automatically.
@@ -19,7 +19,6 @@ const BUN_WORKFLOW_SPECS = Object.freeze([
19
19
  { path: '.github/workflows/pr-monitor.yml', key: 'bun-version', count: 1 },
20
20
  { path: '.github/workflows/required-checks-bypass.yml', key: 'bun-version', count: 1 },
21
21
  { path: '.github/workflows/size-check.yml', key: 'bun-version', count: 1 },
22
- { path: '.github/workflows/test.yml', key: 'bun-version', count: 8 },
23
22
  { path: '.github/workflows/yaml-lint.yml', key: 'bun-version', count: 1 },
24
23
  ].map(Object.freeze));
25
24
 
@@ -128,6 +127,76 @@ function readSourcePackageManifest(projectRoot, sourceHead, execGit = secureExec
128
127
  return Buffer.from(execGit('git', ['show', `${sourceHead}:package.json`], { ...options, encoding: 'buffer' }));
129
128
  }
130
129
 
130
+ function parseRegularEntries(raw, expectedPaths, pattern, errorForPath) {
131
+ const entries = new Map();
132
+ for (const line of raw.trim().split(/\r?\n/).filter(Boolean)) {
133
+ const match = pattern.exec(line);
134
+ if (match) entries.set(match[2], match[1]);
135
+ }
136
+ for (const filePath of expectedPaths) {
137
+ if (!entries.has(filePath)) throw new Error(errorForPath(filePath));
138
+ }
139
+ return entries;
140
+ }
141
+
142
+ function readBlobBatch(projectRoot, objectIds, execGit) {
143
+ const output = Buffer.from(execGit('git', ['cat-file', '--batch'], {
144
+ cwd: projectRoot,
145
+ encoding: 'buffer',
146
+ input: Buffer.from(`${objectIds.join('\n')}\n`),
147
+ stdio: ['pipe', 'pipe', 'pipe'],
148
+ windowsHide: true,
149
+ }));
150
+ const blobs = [];
151
+ let offset = 0;
152
+ for (const expectedId of objectIds) {
153
+ const headerEnd = output.indexOf(0x0a, offset);
154
+ if (headerEnd < 0) throw new Error(`Git did not return blob metadata for ${expectedId}.`);
155
+ const match = /^([0-9a-f]+) blob (\d+)$/.exec(output.subarray(offset, headerEnd).toString('utf8'));
156
+ if (!match || match[1] !== expectedId) throw new Error(`Git did not return the expected blob ${expectedId}.`);
157
+ const contentStart = headerEnd + 1;
158
+ const contentEnd = contentStart + Number(match[2]);
159
+ if (contentEnd >= output.length || output[contentEnd] !== 0x0a) {
160
+ throw new Error(`Git returned an incomplete blob ${expectedId}.`);
161
+ }
162
+ blobs.push(Buffer.from(output.subarray(contentStart, contentEnd)));
163
+ offset = contentEnd + 1;
164
+ }
165
+ if (offset !== output.length) throw new Error('Git returned unexpected extra blob data.');
166
+ return blobs;
167
+ }
168
+
169
+ function readCompleteBunPinBatch(projectRoot, sourceHead, extraIndexedPaths = [], execGit = secureExecFileSync) {
170
+ const sourcePaths = ['package.json', ...BUN_WORKFLOW_SPECS.map(spec => spec.path)];
171
+ const indexedPaths = [...sourcePaths, ...extraIndexedPaths];
172
+ const options = {
173
+ cwd: projectRoot,
174
+ encoding: 'utf8',
175
+ stdio: ['ignore', 'pipe', 'pipe'],
176
+ windowsHide: true,
177
+ };
178
+ const sourceEntries = parseRegularEntries(
179
+ execGit('git', ['ls-tree', sourceHead, '--', ...sourcePaths], options),
180
+ sourcePaths,
181
+ /^100(?:644|755) blob ([0-9a-f]+)\t(.+)$/,
182
+ filePath => filePath === 'package.json'
183
+ ? `package.json is not a regular file at ${sourceHead}.`
184
+ : `${filePath} is not a regular workflow file at ${sourceHead}.`,
185
+ );
186
+ const indexedEntries = parseRegularEntries(
187
+ execGit('git', ['ls-files', '--stage', '--', ...indexedPaths], options),
188
+ indexedPaths,
189
+ /^100(?:644|755) ([0-9a-f]+) 0\t(.+)$/,
190
+ filePath => `${filePath} is not one regular file in the Git index.`,
191
+ );
192
+ const sourceBlobs = readBlobBatch(projectRoot, sourcePaths.map(filePath => sourceEntries.get(filePath)), execGit);
193
+ const indexedBlobs = readBlobBatch(projectRoot, indexedPaths.map(filePath => indexedEntries.get(filePath)), execGit);
194
+ return {
195
+ source: new Map(sourcePaths.map((filePath, index) => [filePath, sourceBlobs[index]])),
196
+ indexed: new Map(indexedPaths.map((filePath, index) => [filePath, indexedBlobs[index]])),
197
+ };
198
+ }
199
+
131
200
  function resolveBunVersionTarget(projectRoot, options = {}) {
132
201
  const indexReader = options.readIndexedPackageManifest || readIndexedPackageManifest;
133
202
  const indexed = Buffer.from(indexReader(projectRoot, options.execGit));
@@ -308,16 +377,24 @@ async function applyWorkflowUpdate(projectRoot, update, context) {
308
377
  return { success: true, result: { path: update.path, write, authorization, completion } };
309
378
  }
310
379
 
380
+ /**
381
+ * Runs the Bun-pin, npm, and test workflow writers as one recoverable batch bound to the preflight
382
+ * target and generated test bytes. On failure, earlier writes are restored only while their bytes
383
+ * still match this batch's output; concurrent changes are preserved and reported as unrecovered.
384
+ */
311
385
  async function updateBunWorkflowPins(projectRoot, options = {}) {
312
386
  const npmWorkflow = require('./npm-publish-workflow');
387
+ const testWorkflow = require('./test-workflow');
313
388
  const { verifyExpectedHead } = npmWorkflow;
314
389
  const head = verifyExpectedHead(projectRoot, options.expectedHead, options.resolveHead);
315
390
  if (!head.success) return head;
316
391
  const sourceHead = head.sourceHead;
317
- const actor = options.actor || options.env?.FORGE_ACTOR || process.env.FORGE_ACTOR || process.env.USER || process.env.USERNAME || 'forge-release';
392
+ const env = options.env || process.env;
393
+ const actor = options.actor || env.FORGE_PROTECTED_STATE_ACTOR || env.FORGE_ACTOR || env.USER || env.USERNAME || 'unknown';
318
394
 
319
395
  let updates;
320
396
  let npmUpdate;
397
+ let testUpdate;
321
398
  try {
322
399
  const target = resolveBunVersionTarget(projectRoot, options);
323
400
  updates = BUN_WORKFLOW_SPECS.map(spec => {
@@ -371,6 +448,11 @@ async function updateBunWorkflowPins(projectRoot, options = {}) {
371
448
  },
372
449
  recoveryOperation: 'recover_npm_workflow',
373
450
  };
451
+ testUpdate = (options.resolveTestWorkflowUpdate || testWorkflow.resolveTestWorkflowUpdate)(
452
+ projectRoot,
453
+ sourceHead,
454
+ options,
455
+ );
374
456
  } catch (error) {
375
457
  return { success: false, error: error.message };
376
458
  }
@@ -437,12 +519,55 @@ async function updateBunWorkflowPins(projectRoot, options = {}) {
437
519
  return failedUpdate(npm.error, results, attempts, authorizations, projectRoot, context, { npm });
438
520
  }
439
521
 
522
+ const testAuthorization = {
523
+ actor,
524
+ path: testWorkflow.TEST_WORKFLOW_PATH,
525
+ sourceHead,
526
+ capabilityId: context.createCapabilityId(),
527
+ };
528
+ authorizations.push(testAuthorization);
529
+ attempts.push({
530
+ ...testUpdate,
531
+ original: testUpdate.snapshot,
532
+ writeOptions: {
533
+ actor,
534
+ operation: 'generate_test_workflow',
535
+ viaForgeApi: true,
536
+ surface: 'workflows',
537
+ },
538
+ recoveryOperation: 'recover_test_workflow',
539
+ });
540
+ const testGenerator = options.generateTestWorkflow || testWorkflow.generateTestWorkflow;
541
+ let testWorkflowResult;
542
+ try {
543
+ testWorkflowResult = await testGenerator(projectRoot, {
544
+ actor,
545
+ env: options.env,
546
+ kernelDeps: options.kernelDeps,
547
+ expectedHead: sourceHead,
548
+ resolveHead: options.resolveHead,
549
+ capabilityId: testAuthorization.capabilityId,
550
+ createCapabilityId: () => testAuthorization.capabilityId,
551
+ expectedSnapshot: testUpdate.snapshot,
552
+ expectedUpdate: { version: testUpdate.version, content: testUpdate.content },
553
+ });
554
+ } catch (error) {
555
+ return failedUpdate(error.message, results, attempts, authorizations, projectRoot, context, { npm });
556
+ }
557
+ if (!testWorkflowResult.success) {
558
+ return failedUpdate(testWorkflowResult.error, results, attempts, authorizations, projectRoot, context, {
559
+ npm,
560
+ testWorkflow: testWorkflowResult,
561
+ });
562
+ }
563
+
440
564
  return {
441
565
  success: true,
442
566
  version: updates[0].version,
443
- paths: [...results.map(result => result.path), npm.path],
567
+ paths: [...results.map(result => result.path), npm.path, testWorkflowResult.path],
444
568
  results,
445
569
  npm,
570
+ testWorkflow: testWorkflowResult,
446
571
  };
447
572
  }
448
573
 
@@ -454,6 +579,7 @@ module.exports = {
454
579
  readIndexedWorkflow,
455
580
  readIndexedPackageManifest,
456
581
  readSourcePackageManifest,
582
+ readCompleteBunPinBatch,
457
583
  resolveBunVersionTarget,
458
584
  resolveCurrentHead,
459
585
  deriveBunWorkflowUpdate,