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

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 (196) hide show
  1. package/AGENTS.md +18 -7
  2. package/CHANGELOG.md +79 -1
  3. package/CLAUDE.md +0 -12
  4. package/CODING_STANDARDS.md +72 -0
  5. package/README.md +6 -2
  6. package/bin/forge-cmd.js +20 -0
  7. package/bin/forge.js +28 -375
  8. package/docs/INDEX.md +1 -1
  9. package/docs/guides/BEADS_GITHUB_SYNC.md +2 -31
  10. package/docs/guides/MIGRATION.md +4 -4
  11. package/docs/guides/SETUP.md +16 -16
  12. package/docs/reference/COMMANDS.md +8 -5
  13. package/docs/reference/FORGE_KERNEL_STORAGE_MODEL.md +4 -0
  14. package/docs/reference/INSIGHTS_RECAP.md +9 -20
  15. package/docs/reference/INSTALL.md +4 -0
  16. package/docs/reference/LEGACY_CLAIM_REPAIR.md +112 -0
  17. package/docs/reference/RELEASE.md +5 -3
  18. package/docs/reference/TOOLCHAIN.md +8 -0
  19. package/docs/reference/github-accounts.md +134 -0
  20. package/docs/reference/protected-state-surfaces.md +4 -4
  21. package/docs/reference/shepherd.md +114 -35
  22. package/lefthook.yml +12 -0
  23. package/lib/activation/ensure-forge-home.js +33 -15
  24. package/lib/adapters/pr-state-adapter.js +359 -144
  25. package/lib/audit-evidence.js +71 -110
  26. package/lib/base-remote.js +138 -0
  27. package/lib/beta5-compatibility-evidence.js +1093 -0
  28. package/lib/bun-lockfile-proof.js +413 -0
  29. package/lib/bun-workflow-pins.js +461 -0
  30. package/lib/capabilities/index.js +9 -0
  31. package/lib/capabilities/model.js +141 -0
  32. package/lib/capabilities/probes.js +347 -0
  33. package/lib/capped-jsonl-log.js +236 -0
  34. package/lib/codex-skills.js +2 -2
  35. package/lib/commands/_manifest.js +1 -0
  36. package/lib/commands/_registry.js +50 -20
  37. package/lib/commands/clean.js +252 -32
  38. package/lib/commands/dev.js +4 -33
  39. package/lib/commands/doctor.js +37 -6
  40. package/lib/commands/gate.js +197 -27
  41. package/lib/commands/github.js +215 -0
  42. package/lib/commands/hooks.js +276 -30
  43. package/lib/commands/insights.js +8 -3
  44. package/lib/commands/memory.js +66 -2
  45. package/lib/commands/merge.js +1265 -58
  46. package/lib/commands/plan.js +33 -2
  47. package/lib/commands/pr.js +3 -1
  48. package/lib/commands/preflight.js +21 -4
  49. package/lib/commands/prime.js +21 -8
  50. package/lib/commands/push.js +146 -54
  51. package/lib/commands/recall.js +127 -49
  52. package/lib/commands/recap.js +6 -1
  53. package/lib/commands/release.js +39 -3
  54. package/lib/commands/remember.js +28 -4
  55. package/lib/commands/serve.js +26 -9
  56. package/lib/commands/setup.js +323 -98
  57. package/lib/commands/shepherd.js +591 -73
  58. package/lib/commands/ship.js +36 -91
  59. package/lib/commands/skill.js +127 -11
  60. package/lib/commands/status.js +17 -1
  61. package/lib/commands/team.js +47 -8
  62. package/lib/commands/test.js +187 -38
  63. package/lib/commands/validate.js +65 -21
  64. package/lib/commands/worktree.js +359 -45
  65. package/lib/core/runtime-graph.js +1 -1
  66. package/lib/doc-assertions.js +297 -0
  67. package/lib/existing-tdd-gate.js +253 -0
  68. package/lib/fixtures/beta5-corpus/v1/README.md +9 -0
  69. package/lib/fixtures/beta5-corpus/v1/contract/command-contract.json +26 -0
  70. package/lib/fixtures/beta5-corpus/v1/contract/package-contract.json +13 -0
  71. package/lib/fixtures/beta5-corpus/v1/contract/workflow-stage-matrix.json +8 -0
  72. package/lib/fixtures/beta5-corpus/v1/manifest.json +25 -0
  73. package/lib/fixtures/beta5-corpus/v1/state/comments.jsonl +1 -0
  74. package/lib/fixtures/beta5-corpus/v1/state/config.yaml +6 -0
  75. package/lib/fixtures/beta5-corpus/v1/state/dependencies.jsonl +1 -0
  76. package/lib/fixtures/beta5-corpus/v1/state/issues.jsonl +2 -0
  77. package/lib/fixtures/beta5-corpus/v1/state/kernel.sql +20 -0
  78. package/lib/forge-context.js +1 -4
  79. package/lib/forge-issues.js +134 -32
  80. package/lib/gate-events.js +98 -10
  81. package/lib/git-defaults.js +56 -0
  82. package/lib/github-context.js +308 -0
  83. package/lib/global-flags.js +1 -0
  84. package/lib/harness-capability-matrix.js +3 -3
  85. package/lib/hook-renderer.js +122 -5
  86. package/lib/insights.js +96 -80
  87. package/lib/issue-render.js +19 -0
  88. package/lib/kernel/backing-issue.js +14 -2
  89. package/lib/kernel/broker.js +739 -31
  90. package/lib/kernel/claim-reconciler.js +238 -0
  91. package/lib/kernel/cli-broker-factory.js +12 -1
  92. package/lib/kernel/close-on-merge.js +154 -0
  93. package/lib/kernel/fs-class.js +42 -25
  94. package/lib/kernel/lease-enforcer.js +9 -4
  95. package/lib/kernel/legacy-claim-repair.js +442 -0
  96. package/lib/kernel/live-claim-projection.js +26 -0
  97. package/lib/kernel/migrations.js +118 -3
  98. package/lib/kernel/readiness-model.js +184 -12
  99. package/lib/kernel/schema.js +49 -1
  100. package/lib/kernel/sqlite-driver.js +3435 -172
  101. package/lib/kernel/taxonomy-validator.js +4 -1
  102. package/lib/kernel/windows-private-acl.js +239 -0
  103. package/lib/lefthook-wiring.js +21 -1
  104. package/lib/memory/hygiene.js +191 -0
  105. package/lib/memory/router.js +110 -28
  106. package/lib/memory/usage-evidence.js +4 -0
  107. package/lib/memory-digest.js +106 -15
  108. package/lib/memory-recall-events.js +145 -0
  109. package/lib/memory-recall.js +71 -10
  110. package/lib/merge-rules.js +143 -21
  111. package/lib/npm-publish-workflow.js +465 -0
  112. package/lib/orientation.js +68 -43
  113. package/lib/package-root.js +2 -0
  114. package/lib/plugin-catalog.js +14 -4
  115. package/lib/pr-bundle.js +5 -6
  116. package/lib/pr-monitor/auto-actions.js +169 -28
  117. package/lib/pr-monitor/differ.js +110 -4
  118. package/lib/pr-monitor/events.js +0 -0
  119. package/lib/pr-monitor/flow-monitor.js +1424 -0
  120. package/lib/pr-monitor/gather.js +251 -44
  121. package/lib/pr-monitor/journal.js +18 -39
  122. package/lib/pr-monitor/monitor.js +117 -10
  123. package/lib/pr-monitor/process-identity.js +117 -0
  124. package/lib/pr-monitor/reconcile-executor.js +1129 -470
  125. package/lib/pr-monitor/reconcile.js +0 -0
  126. package/lib/pr-monitor/render-summary.js +293 -0
  127. package/lib/pr-monitor/review-preflight.js +269 -0
  128. package/lib/pr-monitor/shepherd-lease.js +38 -20
  129. package/lib/pr-monitor/verdict.js +438 -0
  130. package/lib/pr-monitor/watch-lifecycle.js +145 -27
  131. package/lib/pr-monitor/watch-owner.js +1414 -0
  132. package/lib/pr-monitor/watch.js +129 -58
  133. package/lib/pr-pull.js +33 -14
  134. package/lib/pr-shepherd.js +51 -11
  135. package/lib/preflight/gates.js +65 -18
  136. package/lib/preflight/runner.js +5 -0
  137. package/lib/project-memory.js +178 -4
  138. package/lib/protected-state-authority.js +1100 -0
  139. package/lib/protected-state-surfaces.js +243 -45
  140. package/lib/release-readiness.js +53 -7
  141. package/lib/review-adapter.js +65 -0
  142. package/lib/shell-utils.js +1 -1
  143. package/lib/skills-sync.js +71 -35
  144. package/lib/smart-merge.js +28 -4
  145. package/lib/symlink-utils.js +74 -26
  146. package/lib/upgrade-safety.js +39 -0
  147. package/lib/using-forge.js +19 -6
  148. package/lib/validation/risk-manifest.js +339 -0
  149. package/lib/workflow/enforce-stage.js +44 -0
  150. package/lib/workflow/plan-authority.js +225 -0
  151. package/package.json +12 -9
  152. package/scripts/commitlint.js +13 -15
  153. package/scripts/doc-asserting-tests.js +158 -0
  154. package/scripts/generate-risk-manifest.js +91 -0
  155. package/scripts/github-context-bridge.sh +10 -0
  156. package/scripts/legacy-claim-repair.js +145 -0
  157. package/scripts/lib/behavioral-eval-runner.js +310 -0
  158. package/scripts/lib/behavioral-eval-runtime.js +457 -0
  159. package/scripts/lib/eval-evidence.js +328 -0
  160. package/scripts/lib/eval-runner.js +81 -41
  161. package/scripts/lib/immutable-eval-corpus.js +309 -0
  162. package/scripts/lib/promotion-evidence-loader.js +94 -0
  163. package/scripts/lib/promotion-scorecard.js +314 -0
  164. package/scripts/npm-release-receipt.js +134 -0
  165. package/scripts/process-tree.js +773 -0
  166. package/scripts/protected-state-check.js +479 -31
  167. package/scripts/run-command-eval.js +29 -1
  168. package/scripts/sync-agent-skills.js +333 -34
  169. package/scripts/sync-d20-audit.js +172 -0
  170. package/scripts/test-full-suite.js +935 -37
  171. package/scripts/test-profile.js +13 -3
  172. package/scripts/test.js +271 -57
  173. package/skills/coverage.json +1 -0
  174. package/skills/review/SKILL.md +6 -11
  175. package/skills/review/evals/scorecard.json +4 -4
  176. package/skills/rollback/SKILL.md +4 -11
  177. package/skills/rollback/evals/scorecard.json +3 -3
  178. package/skills/setup/SKILL.md +18 -0
  179. package/skills/setup/evals/scorecard.json +3 -3
  180. package/skills/shepherd/SKILL.md +39 -16
  181. package/skills/shepherd/evals/scorecard.json +4 -4
  182. package/skills/ship/SKILL.md +4 -12
  183. package/skills/ship/evals/scorecard.json +3 -3
  184. package/skills/validate/SKILL.md +3 -0
  185. package/skills/validate/evals/scorecard.json +1 -1
  186. package/skills/worktree/SKILL.md +6 -1
  187. package/skills/worktree/evals/scorecard.json +2 -2
  188. package/lib/beads-setup.js +0 -538
  189. package/lib/beads-sync-scaffold.js +0 -189
  190. package/lib/pat-setup.js +0 -207
  191. package/lib/pr-monitor/render-sticky.js +0 -206
  192. package/lib/pr-monitor/upsert-sticky.js +0 -169
  193. package/scripts/beads-context.sh +0 -577
  194. package/scripts/beads-migrate-to-dolt.sh +0 -7
  195. package/scripts/beads-upgrade-smoke.sh +0 -284
  196. package/scripts/lib/beads-migrate-to-dolt.mjs +0 -503
@@ -0,0 +1,465 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+ const { secureExecFileSync } = require('./shell-utils');
6
+
7
+ const {
8
+ createProtectedStateAuditRecord,
9
+ recordProtectedStateAuditEvent,
10
+ removeProtectedFile,
11
+ writeProtectedFile,
12
+ } = require('./protected-state-surfaces');
13
+ const {
14
+ completeNpmPublishWorkflowAuthorization,
15
+ issueNpmPublishWorkflowAuthorization,
16
+ } = require('./protected-state-authority');
17
+
18
+ const NPM_PUBLISH_WORKFLOW_PATH = '.github/workflows/npm-publish.yml';
19
+ const FULL_COMMIT_SHA = /^[0-9a-f]{40}$/;
20
+ const BUN_VERSION = /^bun@(\d+\.\d+\.\d+)$/.exec(require('../package.json').packageManager)?.[1];
21
+ if (!BUN_VERSION) throw new Error('package.json must pin an exact stable Bun version.');
22
+
23
+ function resolveCurrentHead(projectRoot, execGit = secureExecFileSync) {
24
+ return execGit('git', ['rev-parse', '--verify', 'HEAD^{commit}'], {
25
+ cwd: projectRoot,
26
+ encoding: 'utf8',
27
+ stdio: ['ignore', 'pipe', 'pipe'],
28
+ windowsHide: true,
29
+ }).trim();
30
+ }
31
+
32
+ function readNpmPublishWorkflow(projectRoot, revision, execGit = secureExecFileSync) {
33
+ const options = {
34
+ cwd: projectRoot,
35
+ encoding: 'utf8',
36
+ stdio: ['ignore', 'pipe', 'pipe'],
37
+ windowsHide: true,
38
+ };
39
+ const indexed = revision === ':';
40
+ const entry = execGit('git', indexed
41
+ ? ['ls-files', '--stage', '--', NPM_PUBLISH_WORKFLOW_PATH]
42
+ : ['ls-tree', revision, '--', NPM_PUBLISH_WORKFLOW_PATH], options).trim();
43
+ const expected = indexed
44
+ ? /^100(?:644|755) [0-9a-f]+ 0\t\.github\/workflows\/npm-publish\.yml$/
45
+ : /^100(?:644|755) blob [0-9a-f]+\t\.github\/workflows\/npm-publish\.yml$/;
46
+ if (!expected.test(entry)) {
47
+ throw new Error(`${NPM_PUBLISH_WORKFLOW_PATH} is not one regular file ${indexed ? 'in the Git index' : `at ${revision}`}.`);
48
+ }
49
+ return Buffer.from(execGit('git', ['show', `${revision}${indexed ? '' : ':'}${NPM_PUBLISH_WORKFLOW_PATH}`], {
50
+ ...options,
51
+ encoding: 'buffer',
52
+ }));
53
+ }
54
+
55
+ function readSourceNpmPublishWorkflow(projectRoot, sourceHead, execGit = secureExecFileSync) {
56
+ return readNpmPublishWorkflow(projectRoot, sourceHead, execGit);
57
+ }
58
+
59
+ function readIndexedNpmPublishWorkflow(projectRoot, execGit = secureExecFileSync) {
60
+ return readNpmPublishWorkflow(projectRoot, ':', execGit);
61
+ }
62
+
63
+ function verifyExpectedHead(projectRoot, expectedHead, resolveHead = resolveCurrentHead) {
64
+ if (!expectedHead) {
65
+ return { success: false, error: '--expect-head is required for protected workflow generation.' };
66
+ }
67
+ if (!FULL_COMMIT_SHA.test(expectedHead)) {
68
+ return { success: false, error: '--expect-head must be a full 40-character lowercase commit SHA.' };
69
+ }
70
+
71
+ let currentHead;
72
+ try {
73
+ currentHead = resolveHead(projectRoot);
74
+ } catch (error) {
75
+ return { success: false, error: `Could not resolve current HEAD: ${error.message}` };
76
+ }
77
+ if (!FULL_COMMIT_SHA.test(currentHead)) {
78
+ return { success: false, error: 'Current HEAD did not resolve to a full 40-character lowercase commit SHA.' };
79
+ }
80
+ if (currentHead !== expectedHead) {
81
+ return {
82
+ success: false,
83
+ error: `Expected HEAD ${expectedHead} does not match current HEAD ${currentHead}.`,
84
+ };
85
+ }
86
+ return { success: true, sourceHead: currentHead };
87
+ }
88
+
89
+ function renderNpmDistTagResolverScript() {
90
+ return `CORE_RE='(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)\\.(0|[1-9][0-9]*)'
91
+ NUMERIC_ID_RE='(0|[1-9][0-9]*)'
92
+ BUILD_RE='(\\+[0-9A-Za-z-]+(\\.[0-9A-Za-z-]+)*)?'
93
+ BETA_RE="^\${CORE_RE}-beta(\\.\${NUMERIC_ID_RE})*\${BUILD_RE}$"
94
+ RC_RE="^\${CORE_RE}-rc(\\.\${NUMERIC_ID_RE})*\${BUILD_RE}$"
95
+ STABLE_RE="^\${CORE_RE}\${BUILD_RE}$"
96
+
97
+ if [[ "$VERSION" =~ $BETA_RE ]]; then
98
+ echo "tag=beta" >> "$GITHUB_OUTPUT"
99
+ elif [[ "$VERSION" =~ $RC_RE ]]; then
100
+ echo "tag=rc" >> "$GITHUB_OUTPUT"
101
+ elif [[ "$VERSION" =~ $STABLE_RE ]]; then
102
+ echo "tag=latest" >> "$GITHUB_OUTPUT"
103
+ elif [[ "$VERSION" == *-* ]]; then
104
+ echo "::error::Unsupported npm prerelease: $VERSION"
105
+ exit 1
106
+ else
107
+ echo "::error::Invalid npm version: $VERSION"
108
+ exit 1
109
+ fi`;
110
+ }
111
+
112
+ function snapshotWorkflow(projectRoot) {
113
+ const fullPath = path.resolve(projectRoot, NPM_PUBLISH_WORKFLOW_PATH);
114
+ try {
115
+ const stat = fs.lstatSync(fullPath);
116
+ return {
117
+ fullPath,
118
+ existed: true,
119
+ content: stat.isFile() ? fs.readFileSync(fullPath) : null,
120
+ };
121
+ } catch (error) {
122
+ if (error.code === 'ENOENT') return { fullPath, existed: false, content: null };
123
+ throw error;
124
+ }
125
+ }
126
+
127
+ function sameSnapshot(expected, actual) {
128
+ if (!expected || typeof expected !== 'object' || Array.isArray(expected)) return false;
129
+ if (typeof expected.existed !== 'boolean') return false;
130
+ if (expected.existed && !Buffer.isBuffer(expected.content)) return false;
131
+ if (!expected.existed && expected.content !== null) return false;
132
+ return expected.existed === actual.existed
133
+ && (!expected.existed || expected.content.equals(actual.content));
134
+ }
135
+
136
+ function recoverWorkflow(snapshot, projectRoot, generatedContent, protectedWriter, protectedRemover, writeOptions) {
137
+ const recoveryOptions = {
138
+ ...writeOptions,
139
+ operation: 'recover_npm_workflow',
140
+ expectedContent: generatedContent,
141
+ };
142
+ if (!snapshot.existed) {
143
+ return protectedRemover(projectRoot, NPM_PUBLISH_WORKFLOW_PATH, recoveryOptions);
144
+ }
145
+ if (!Buffer.isBuffer(snapshot.content)) {
146
+ return { allowed: false, reason: 'Previous workflow was not a regular file' };
147
+ }
148
+ return protectedWriter(
149
+ projectRoot,
150
+ NPM_PUBLISH_WORKFLOW_PATH,
151
+ snapshot.content,
152
+ recoveryOptions,
153
+ );
154
+ }
155
+
156
+ async function prepareWorkflowAuthorization(prepareAuthorization, projectRoot, params, authorizationOptions) {
157
+ try {
158
+ return await prepareAuthorization(projectRoot, params, authorizationOptions);
159
+ } catch (error) {
160
+ return { success: false, error: error.message };
161
+ }
162
+ }
163
+
164
+ function recordWorkflowAudit(auditWriter, auditRecord, projectRoot) {
165
+ try {
166
+ return auditWriter(auditRecord, { cwd: projectRoot });
167
+ } catch (error) {
168
+ return { success: false, error: error.message };
169
+ }
170
+ }
171
+
172
+ function renderNpmPublishWorkflow(bunVersion = BUN_VERSION) {
173
+ if (!/^\d+\.\d+\.\d+$/.test(bunVersion)) throw new Error('npm workflow requires an exact stable Bun version.');
174
+ const distTagResolver = renderNpmDistTagResolverScript()
175
+ .split('\n')
176
+ .map(line => line ? ` ${line}` : '')
177
+ .join('\n');
178
+ return `# Generated by \`forge release generate-npm-workflow\`. Do not edit directly.
179
+ name: Bun Package
180
+
181
+ on:
182
+ release:
183
+ types: [published]
184
+
185
+ permissions:
186
+ contents: read
187
+
188
+ jobs:
189
+ resolve-release:
190
+ name: Resolve immutable release SHA
191
+ runs-on: ubuntu-latest
192
+ timeout-minutes: 10
193
+ permissions:
194
+ contents: read
195
+ outputs:
196
+ commitSha: \${{ steps.resolve.outputs.commitSha }}
197
+ steps:
198
+ - uses: actions/checkout@v7
199
+ with:
200
+ ref: \${{ github.event.release.tag_name }}
201
+ fetch-depth: 0
202
+ persist-credentials: false
203
+ - name: Resolve release tag
204
+ id: resolve
205
+ shell: bash
206
+ env:
207
+ RELEASE_TAG: \${{ github.event.release.tag_name }}
208
+ run: |
209
+ set -euo pipefail
210
+ COMMIT_SHA="$(git rev-parse "\${RELEASE_TAG}^{commit}")"
211
+ if [ -z "$COMMIT_SHA" ] || [ "$COMMIT_SHA" != "$(git rev-parse HEAD)" ]; then
212
+ echo "::error::Release tag did not resolve to the checked out commit"
213
+ exit 1
214
+ fi
215
+ echo "commitSha=$COMMIT_SHA" >> "$GITHUB_OUTPUT"
216
+
217
+ build:
218
+ needs: resolve-release
219
+ runs-on: ubuntu-latest
220
+ timeout-minutes: 25
221
+ permissions:
222
+ contents: read
223
+ outputs:
224
+ commitSha: \${{ steps.evidence.outputs.commitSha }}
225
+ receipt: \${{ steps.evidence.outputs.receipt }}
226
+ receiptSubject: \${{ steps.evidence.outputs.receiptSubject }}
227
+ steps:
228
+ - uses: actions/checkout@v7
229
+ with:
230
+ ref: \${{ needs.resolve-release.outputs.commitSha }}
231
+ fetch-depth: 1
232
+ persist-credentials: false
233
+ - uses: actions/setup-node@v7
234
+ with:
235
+ node-version: 22
236
+ - uses: oven-sh/setup-bun@v2
237
+ with:
238
+ bun-version: ${bunVersion}
239
+ - run: bun install --frozen-lockfile
240
+ - name: Setup test fixtures
241
+ shell: bash
242
+ run: bash test-env/automation/setup-fixtures.sh --force
243
+ - name: Run complete release suite
244
+ shell: bash
245
+ run: |
246
+ set -euo pipefail
247
+ node scripts/test-full-suite.js --label-prefix release-full
248
+ - name: Emit attributable release suite receipt
249
+ id: evidence
250
+ env:
251
+ EXPECTED_SHA: \${{ needs.resolve-release.outputs.commitSha }}
252
+ run: node scripts/npm-release-receipt.js emit
253
+
254
+ publish-npm:
255
+ needs: [resolve-release, build]
256
+ runs-on: ubuntu-latest
257
+ timeout-minutes: 20
258
+ permissions:
259
+ contents: read
260
+ id-token: write
261
+ steps:
262
+ - uses: actions/checkout@v7
263
+ with:
264
+ ref: \${{ needs.resolve-release.outputs.commitSha }}
265
+ fetch-depth: 1
266
+ persist-credentials: false
267
+ - uses: actions/setup-node@v7
268
+ with:
269
+ node-version: 22
270
+ registry-url: https://registry.npmjs.org/
271
+ - name: Verify exact release suite receipt
272
+ env:
273
+ EXPECTED_SHA: \${{ needs.resolve-release.outputs.commitSha }}
274
+ VERIFIED_SHA: \${{ needs.build.outputs.commitSha }}
275
+ RECEIPT: \${{ needs.build.outputs.receipt }}
276
+ RECEIPT_SUBJECT: \${{ needs.build.outputs.receiptSubject }}
277
+ run: node scripts/npm-release-receipt.js verify
278
+ - uses: oven-sh/setup-bun@v2
279
+ with:
280
+ bun-version: ${bunVersion}
281
+ - run: bun install --frozen-lockfile
282
+ - name: Release tag matches package.json version
283
+ shell: bash
284
+ run: |
285
+ set -euo pipefail
286
+ PKG="$(node -p "require('./package.json').version")"
287
+ TAG="\${GITHUB_REF_NAME#v}"
288
+ if [ "$PKG" != "$TAG" ]; then
289
+ echo "::error::Release tag ($TAG) != package.json version ($PKG). Bump package.json before releasing."
290
+ exit 1
291
+ fi
292
+ - name: Release-readiness gate
293
+ run: node bin/forge.js release check --target "$(node -p "require('./package.json').version.split('-')[0]")"
294
+ - name: Verify package contents
295
+ run: npm pack --dry-run
296
+ - name: Ensure OIDC-capable npm
297
+ run: npm install --global npm@11.5.1 && npm --version
298
+ - name: Resolve npm dist-tag
299
+ id: dist-tag
300
+ shell: bash
301
+ run: |
302
+ set -euo pipefail
303
+ VERSION="$(node -p "require('./package.json').version")"
304
+ ${distTagResolver}
305
+ - name: Publish to npm
306
+ shell: bash
307
+ run: |
308
+ set -euo pipefail
309
+ DIST_TAG="\${{ steps.dist-tag.outputs.tag }}"
310
+ case "$DIST_TAG" in
311
+ beta)
312
+ npm publish --provenance --tag beta
313
+ ;;
314
+ rc)
315
+ npm publish --provenance --tag rc
316
+ ;;
317
+ latest)
318
+ npm publish --provenance
319
+ ;;
320
+ *)
321
+ echo "::error::Unsupported npm dist-tag: $DIST_TAG"
322
+ exit 1
323
+ ;;
324
+ esac
325
+ `;
326
+ }
327
+
328
+ async function generateNpmPublishWorkflow(projectRoot, options = {}) {
329
+ const head = verifyExpectedHead(projectRoot, options.expectedHead, options.resolveHead);
330
+ if (!head.success) return head;
331
+ const sourceHead = head.sourceHead;
332
+ let targetBunVersion;
333
+ try {
334
+ targetBunVersion = options.targetBunVersion
335
+ || require('./bun-workflow-pins').resolveBunVersionTarget(projectRoot, options).version;
336
+ } catch (error) {
337
+ return { success: false, error: error.message };
338
+ }
339
+ const bunVersion = options.bunVersion || targetBunVersion;
340
+ if (bunVersion !== targetBunVersion) {
341
+ return { success: false, error: 'npm workflow Bun version must match the staged package.json Bun pin.' };
342
+ }
343
+ const content = renderNpmPublishWorkflow(bunVersion);
344
+ const actor = options.actor || options.env?.FORGE_ACTOR || process.env.FORGE_ACTOR || process.env.USER || process.env.USERNAME || 'unknown';
345
+ const protectedWriter = options.writeProtectedFile || writeProtectedFile;
346
+ const protectedRemover = options.removeProtectedFile || removeProtectedFile;
347
+ const auditWriter = options.recordProtectedStateAuditEvent || recordProtectedStateAuditEvent;
348
+ const prepareAuthorization = options.prepareNpmPublishWorkflowAuthorization || issueNpmPublishWorkflowAuthorization;
349
+ const activateAuthorization = options.activateNpmPublishWorkflowAuthorization || completeNpmPublishWorkflowAuthorization;
350
+ const snapshot = snapshotWorkflow(projectRoot);
351
+ if (options.expectedSnapshot && !sameSnapshot(options.expectedSnapshot, snapshot)) {
352
+ return {
353
+ success: false,
354
+ error: `${NPM_PUBLISH_WORKFLOW_PATH} changed after Bun pin batch preflight.`,
355
+ };
356
+ }
357
+ const writeOptions = {
358
+ actor,
359
+ operation: 'generate_npm_workflow',
360
+ viaForgeApi: true,
361
+ surface: 'workflows',
362
+ };
363
+ const trustedAuthorization = await prepareWorkflowAuthorization(
364
+ prepareAuthorization,
365
+ projectRoot,
366
+ { actor, sourceHead, bunVersion, targetBunVersion },
367
+ { deps: options.kernelDeps, capabilityId: options.capabilityId },
368
+ );
369
+ if (!trustedAuthorization.success) {
370
+ return {
371
+ success: false,
372
+ error: `Could not record trusted protected-state authorization: ${trustedAuthorization.error}`,
373
+ trustedAuthorization,
374
+ };
375
+ }
376
+
377
+ let write;
378
+ try {
379
+ write = protectedWriter(projectRoot, NPM_PUBLISH_WORKFLOW_PATH, content, {
380
+ ...writeOptions,
381
+ expectedContent: snapshot.existed ? snapshot.content : null,
382
+ ...(options.beforeAtomicCommit ? { beforeAtomicCommit: options.beforeAtomicCommit } : {}),
383
+ });
384
+ } catch (error) {
385
+ return { success: false, error: error.message };
386
+ }
387
+ if (!write.allowed) {
388
+ return { success: false, error: write.reason, write };
389
+ }
390
+
391
+ const auditRecord = createProtectedStateAuditRecord({
392
+ actor,
393
+ surface: 'workflows',
394
+ path: NPM_PUBLISH_WORKFLOW_PATH,
395
+ content,
396
+ operation: 'generate_npm_workflow',
397
+ viaForgeApi: true,
398
+ sourceHead,
399
+ });
400
+ const audit = recordWorkflowAudit(auditWriter, auditRecord, projectRoot);
401
+ if (!audit.success) {
402
+ let recovery;
403
+ try {
404
+ recovery = recoverWorkflow(snapshot, projectRoot, content, protectedWriter, protectedRemover, writeOptions);
405
+ } catch (error) {
406
+ recovery = { allowed: false, reason: error.message };
407
+ }
408
+ return {
409
+ success: false,
410
+ error: `Could not record protected-state authorization: ${audit.error}`,
411
+ audit,
412
+ recovery,
413
+ };
414
+ }
415
+
416
+ let writeCompletion;
417
+ try {
418
+ writeCompletion = await activateAuthorization(
419
+ projectRoot,
420
+ { actor, sourceHead, capabilityId: trustedAuthorization.capabilityId, bunVersion, targetBunVersion },
421
+ { deps: options.kernelDeps },
422
+ );
423
+ } catch (error) {
424
+ writeCompletion = { success: false, error: error.message };
425
+ }
426
+ if (!writeCompletion.success) {
427
+ let recovery;
428
+ try {
429
+ recovery = recoverWorkflow(snapshot, projectRoot, content, protectedWriter, protectedRemover, writeOptions);
430
+ } catch (error) {
431
+ recovery = { allowed: false, reason: error.message };
432
+ }
433
+ return {
434
+ success: false,
435
+ error: `Could not complete trusted protected-state authorization: ${writeCompletion.error}`,
436
+ audit,
437
+ trustedAuthorization,
438
+ writeCompletion,
439
+ recovery,
440
+ };
441
+ }
442
+
443
+ return {
444
+ success: true,
445
+ path: NPM_PUBLISH_WORKFLOW_PATH,
446
+ sourceHead,
447
+ bunVersion,
448
+ contentHash: write.contentHash,
449
+ write,
450
+ audit,
451
+ trustedAuthorization,
452
+ writeCompletion,
453
+ };
454
+ }
455
+
456
+ module.exports = {
457
+ NPM_PUBLISH_WORKFLOW_PATH,
458
+ readIndexedNpmPublishWorkflow,
459
+ readSourceNpmPublishWorkflow,
460
+ renderNpmDistTagResolverScript,
461
+ renderNpmPublishWorkflow,
462
+ resolveCurrentHead,
463
+ verifyExpectedHead,
464
+ generateNpmPublishWorkflow,
465
+ };
@@ -3,6 +3,8 @@
3
3
  const fs = require('node:fs');
4
4
  const path = require('node:path');
5
5
  const { fenceUntrusted } = require('./untrusted-content');
6
+ const { runIssueOperation: defaultRunIssueOperation } = require('./forge-issues');
7
+ const { memoryTrustStatus } = require('./memory-recall');
6
8
 
7
9
  const DEFAULT_BUDGET_TOKENS = 2000;
8
10
  const MIN_BUDGET_TOKENS = 40;
@@ -43,25 +45,6 @@ function readJsonFile(projectRoot, relativeFilePath) {
43
45
  }
44
46
  }
45
47
 
46
- function readJsonl(projectRoot, relativeFilePath) {
47
- const text = readText(projectRoot, relativeFilePath);
48
- if (!text) return [];
49
- return text
50
- .split(/\r?\n/)
51
- .map(line => line.trim())
52
- .filter(Boolean)
53
- .map((line, index) => {
54
- try {
55
- return JSON.parse(line);
56
- } catch (error) {
57
- return {
58
- _parseError: true,
59
- line: index + 1,
60
- error: error.message,
61
- };
62
- }
63
- });
64
- }
65
48
 
66
49
  function firstLine(value) {
67
50
  return String(value || '')
@@ -426,17 +409,43 @@ function buildMemorySection(projectRoot, options = {}) {
426
409
  const result = memoryRouter.recall(projectRoot, { limit: MEMORY_SECTION_NOTE_LIMIT }, { store });
427
410
  const notes = Array.isArray(result && result.notes) ? result.notes : [];
428
411
  if (notes.length === 0) return [];
429
- return [buildSection({
430
- id: 'remembered_notes',
431
- title: 'Remembered Notes',
432
- content: notes.map(formatMemoryNote).join('\n'),
433
- sources: [source('kernel.remembered_notes', 'kernel_memory', 'project_memory', 'remembered_notes')],
434
- priority: 45,
435
- preserve: false,
436
- // Remembered notes are untrusted DATA (a planted note must not read as directives).
437
- // Raw here; the assembler fences it AFTER applyBudget truncation.
438
- untrustedSource: 'memory',
439
- })];
412
+ const groups = [
413
+ {
414
+ id: 'remembered_notes',
415
+ title: 'Confirmed Memory',
416
+ trust: 'confirmed',
417
+ priority: 45,
418
+ },
419
+ {
420
+ id: 'suggested_memory',
421
+ title: 'Suggested Memory — Verify Before Relying',
422
+ trust: 'suggested',
423
+ priority: 46,
424
+ },
425
+ ];
426
+ return groups.map(group => {
427
+ const content = notes
428
+ .filter(note => memoryTrustStatus({
429
+ tags: note.tags,
430
+ sourceAgent: note.sourceAgent,
431
+ value: note.machine ? {} : note.note,
432
+ }) === group.trust)
433
+ .map(formatMemoryNote)
434
+ .filter(line => estimateTokens(line) <= DEFAULT_BUDGET_TOKENS)
435
+ .join('\n');
436
+ if (!content) return null;
437
+ return buildSection({
438
+ id: group.id,
439
+ title: group.title,
440
+ content,
441
+ sources: [source('kernel.remembered_notes', 'kernel_memory', 'project_memory', group.id)],
442
+ priority: group.priority,
443
+ preserve: false,
444
+ // Remembered notes are untrusted DATA (a planted note must not read as directives).
445
+ // Raw here; the assembler fences it AFTER applyBudget truncation.
446
+ untrustedSource: 'memory',
447
+ });
448
+ }).filter(Boolean);
440
449
  } catch {
441
450
  return [];
442
451
  } finally {
@@ -470,8 +479,13 @@ function openMemoryStore(projectRoot) {
470
479
  /** Render one recall note as a compact `- [date] text` line. */
471
480
  function formatMemoryNote(note) {
472
481
  const date = typeof note.timestamp === 'string' && note.timestamp ? note.timestamp.slice(0, 10) : '';
473
- const prefix = date ? `${date} ` : '';
474
- return `- ${prefix}${note.note}`;
482
+ const trust = memoryTrustStatus({
483
+ tags: note.tags,
484
+ sourceAgent: note.sourceAgent,
485
+ value: note.machine ? {} : note.note,
486
+ });
487
+ const sourceAgent = note.sourceAgent || 'unknown';
488
+ return `- [source=${sourceAgent} trust=${trust} updated=${date || 'unknown'}] ${note.note}`;
475
489
  }
476
490
 
477
491
  function buildQueueSections() {
@@ -671,7 +685,8 @@ const PRIME_KEY_COMMANDS_CONTENT = [
671
685
  'forge upgrade [--dry-run] — preview/self-heal safe Forge upgrade readiness',
672
686
  'forge gate <verb> <gate-id> — toggle a workflow gate, or approve/reject a human gate',
673
687
  'forge role <role> --use <skill> — bind a role to a skill/ideology',
674
- 'forge merge --auto <pr> — opt-in conditional auto-merge (off by default)',
688
+ 'forge merge --auto <pr> --expect-head <full-sha> --issue <issue-id> — opt-in guarded merge (off by default)',
689
+ 'post-ship loop: forge ship → forge shepherd <pr> --pull --json → guarded forge merge with exact head + owned issue',
675
690
  ].map(line => `- ${line}`).join('\n');
676
691
 
677
692
  function buildPrimeKeyCommandsSection() {
@@ -685,14 +700,24 @@ function buildPrimeKeyCommandsSection() {
685
700
  });
686
701
  }
687
702
 
688
- function findIssue(projectRoot, issueId) {
689
- const issues = readJsonl(projectRoot, '.beads/issues.jsonl')
690
- .filter(row => row && !row._parseError && row._type === 'issue');
691
- return issues.find(issue => issue.id === issueId) || null;
703
+ // Read a single issue from the Kernel (the sole issue-state authority). The `show`
704
+ // contract returns the issue summary DIRECTLY as `result.data` (ISSUE_SUMMARY_SCHEMA),
705
+ // NOT `result.data.issue` — the nested shape belongs to the mutation envelope. Resilient
706
+ // by contract: any read failure degrades to `null` (not-found) so orientation never crashes.
707
+ async function findIssue(projectRoot, issueId, runIssueOperation = defaultRunIssueOperation) {
708
+ try {
709
+ const result = await runIssueOperation('show', [issueId], projectRoot, { issueBackend: 'kernel' });
710
+ if (result && result.ok && result.data && result.data.id) {
711
+ return result.data;
712
+ }
713
+ } catch {
714
+ // best-effort: a kernel read failure surfaces as "not found", never a thrown error
715
+ }
716
+ return null;
692
717
  }
693
718
 
694
- function buildIssueSection(projectRoot, issueId) {
695
- const issue = findIssue(projectRoot, issueId);
719
+ async function buildIssueSection(projectRoot, issueId, options = {}) {
720
+ const issue = await findIssue(projectRoot, issueId, options.runIssueOperation);
696
721
  const content = issue
697
722
  ? [
698
723
  `id: ${issue.id}`,
@@ -700,7 +725,7 @@ function buildIssueSection(projectRoot, issueId) {
700
725
  issue.status ? `status: ${issue.status}` : null,
701
726
  issue.description ? `description: ${issue.description}` : null,
702
727
  ].filter(Boolean).join('\n')
703
- : `id: ${issueId}\nstatus: unknown\nIssue not found in compatibility projection.`;
728
+ : `id: ${issueId}\nstatus: unknown\nIssue not found in the kernel.`;
704
729
 
705
730
  return {
706
731
  issue: issue ? {
@@ -716,19 +741,19 @@ function buildIssueSection(projectRoot, issueId) {
716
741
  id: 'issue_summary',
717
742
  title: 'Issue Summary',
718
743
  content,
719
- sources: [source('.beads/issues.jsonl', 'beads_compat', 'compatibility_projection', 'issue_summary')],
744
+ sources: [source('kernel', 'kernel', 'issue_read', 'issue_summary')],
720
745
  priority: 5,
721
746
  preserve: true,
722
747
  }),
723
748
  };
724
749
  }
725
750
 
726
- function buildIssueRecap(projectRoot, issueId, options = {}) {
751
+ async function buildIssueRecap(projectRoot, issueId, options = {}) {
727
752
  if (!issueId || typeof issueId !== 'string') {
728
753
  throw new Error('Issue id is required for issue-scoped recap.');
729
754
  }
730
755
 
731
- const issueResult = buildIssueSection(projectRoot, issueId);
756
+ const issueResult = await buildIssueSection(projectRoot, issueId, options);
732
757
  const sections = [
733
758
  issueResult.section,
734
759
  ...buildProjectDesignSections(projectRoot),
@@ -67,8 +67,10 @@ const ASSET_ROOTS = Object.freeze([
67
67
  'skills',
68
68
  'rules',
69
69
  'AGENTS.md',
70
+ 'CODING_STANDARDS.md',
70
71
  '.claude/scripts',
71
72
  '.forge/hooks',
73
+ 'lib/kernel/windows-private-acl.js',
72
74
  'scripts',
73
75
  ]);
74
76
 
@@ -91,15 +91,25 @@ const CATALOG = Object.freeze({
91
91
  },
92
92
 
93
93
  // ── Plan ──
94
- beads: {
95
- name: 'Beads',
94
+ 'forge-kernel-issues': {
95
+ name: 'Forge Kernel issues',
96
96
  type: 'cli',
97
97
  tier: 'free',
98
98
  stage: 'plan',
99
- description: 'Git-backed issue tracking',
99
+ description: 'Built-in issue tracker backing /plan — forge issue create, forge ready, forge claim',
100
100
  detectWhen: [],
101
- install: { method: 'npm', cmd: 'bun add -g @beads/bd' },
101
+ install: { method: 'config', cmd: 'built-in (forge issue create)' },
102
102
  },
103
+ 'mermaid-cli': {
104
+ name: 'Mermaid CLI',
105
+ type: 'cli',
106
+ tier: 'free',
107
+ stage: 'plan',
108
+ description: 'Render Mermaid architecture diagrams from design docs to SVG/PNG',
109
+ detectWhen: [],
110
+ install: { method: 'npm', cmd: 'bun add -D @mermaid-js/mermaid-cli', dev: true },
111
+ },
112
+
103
113
  // ── Dev ──
104
114
  'typescript-lsp': {
105
115
  name: 'TypeScript LSP',