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

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 (119) hide show
  1. package/AGENTS.md +14 -7
  2. package/CHANGELOG.md +43 -1
  3. package/README.md +6 -2
  4. package/bin/forge-cmd.js +20 -0
  5. package/bin/forge.js +16 -374
  6. package/docs/INDEX.md +1 -1
  7. package/docs/guides/BEADS_GITHUB_SYNC.md +2 -31
  8. package/docs/guides/MIGRATION.md +4 -4
  9. package/docs/guides/SETUP.md +16 -16
  10. package/docs/reference/COMMANDS.md +8 -5
  11. package/docs/reference/INSIGHTS_RECAP.md +9 -20
  12. package/docs/reference/RELEASE.md +5 -3
  13. package/docs/reference/TOOLCHAIN.md +8 -0
  14. package/docs/reference/protected-state-surfaces.md +4 -4
  15. package/docs/reference/shepherd.md +54 -25
  16. package/lefthook.yml +12 -0
  17. package/lib/activation/ensure-forge-home.js +33 -15
  18. package/lib/adapters/pr-state-adapter.js +344 -142
  19. package/lib/audit-evidence.js +71 -110
  20. package/lib/capped-jsonl-log.js +236 -0
  21. package/lib/commands/_registry.js +2 -2
  22. package/lib/commands/clean.js +196 -32
  23. package/lib/commands/dev.js +4 -33
  24. package/lib/commands/hooks.js +223 -25
  25. package/lib/commands/insights.js +8 -3
  26. package/lib/commands/merge.js +600 -40
  27. package/lib/commands/pr.js +1 -1
  28. package/lib/commands/preflight.js +11 -2
  29. package/lib/commands/prime.js +21 -8
  30. package/lib/commands/push.js +41 -51
  31. package/lib/commands/recall.js +60 -16
  32. package/lib/commands/recap.js +6 -1
  33. package/lib/commands/release.js +17 -2
  34. package/lib/commands/setup.js +191 -94
  35. package/lib/commands/shepherd.js +13 -1
  36. package/lib/commands/ship.js +22 -23
  37. package/lib/commands/skill.js +119 -11
  38. package/lib/commands/status.js +17 -1
  39. package/lib/commands/test.js +24 -34
  40. package/lib/commands/worktree.js +220 -42
  41. package/lib/core/runtime-graph.js +1 -1
  42. package/lib/doc-assertions.js +297 -0
  43. package/lib/existing-tdd-gate.js +253 -0
  44. package/lib/forge-context.js +1 -4
  45. package/lib/forge-issues.js +56 -32
  46. package/lib/git-defaults.js +56 -0
  47. package/lib/harness-capability-matrix.js +3 -3
  48. package/lib/hook-renderer.js +93 -4
  49. package/lib/insights.js +96 -80
  50. package/lib/kernel/backing-issue.js +14 -2
  51. package/lib/kernel/broker.js +16 -0
  52. package/lib/kernel/cli-broker-factory.js +12 -1
  53. package/lib/kernel/close-on-merge.js +154 -0
  54. package/lib/kernel/fs-class.js +42 -25
  55. package/lib/kernel/sqlite-driver.js +153 -29
  56. package/lib/lefthook-wiring.js +21 -1
  57. package/lib/memory/router.js +16 -1
  58. package/lib/memory-digest.js +47 -15
  59. package/lib/memory-recall-events.js +145 -0
  60. package/lib/memory-recall.js +71 -10
  61. package/lib/merge-rules.js +8 -4
  62. package/lib/npm-publish-workflow.js +272 -0
  63. package/lib/orientation.js +68 -43
  64. package/lib/plugin-catalog.js +14 -4
  65. package/lib/pr-bundle.js +5 -6
  66. package/lib/pr-monitor/journal.js +18 -2
  67. package/lib/pr-monitor/reconcile-executor.js +224 -41
  68. package/lib/pr-monitor/render-summary.js +196 -0
  69. package/lib/pr-monitor/shepherd-lease.js +10 -1
  70. package/lib/pr-monitor/watch-lifecycle.js +13 -1
  71. package/lib/pr-pull.js +33 -14
  72. package/lib/pr-shepherd.js +34 -8
  73. package/lib/preflight/gates.js +65 -18
  74. package/lib/preflight/runner.js +5 -0
  75. package/lib/project-memory.js +33 -1
  76. package/lib/protected-state-authority.js +305 -0
  77. package/lib/protected-state-surfaces.js +64 -44
  78. package/lib/release-readiness.js +51 -4
  79. package/lib/shell-utils.js +1 -1
  80. package/lib/skills-sync.js +6 -3
  81. package/lib/smart-merge.js +28 -4
  82. package/lib/symlink-utils.js +74 -26
  83. package/lib/upgrade-safety.js +39 -0
  84. package/lib/using-forge.js +19 -6
  85. package/package.json +6 -7
  86. package/scripts/doc-asserting-tests.js +158 -0
  87. package/scripts/lib/behavioral-eval-runner.js +310 -0
  88. package/scripts/lib/behavioral-eval-runtime.js +456 -0
  89. package/scripts/lib/eval-evidence.js +328 -0
  90. package/scripts/lib/eval-runner.js +81 -41
  91. package/scripts/lib/immutable-eval-corpus.js +309 -0
  92. package/scripts/lib/promotion-evidence-loader.js +94 -0
  93. package/scripts/lib/promotion-scorecard.js +314 -0
  94. package/scripts/npm-release-receipt.js +134 -0
  95. package/scripts/process-tree.js +761 -0
  96. package/scripts/protected-state-check.js +47 -22
  97. package/scripts/run-command-eval.js +29 -1
  98. package/scripts/sync-d20-audit.js +172 -0
  99. package/scripts/test-full-suite.js +249 -37
  100. package/scripts/test.js +176 -43
  101. package/skills/review/SKILL.md +4 -11
  102. package/skills/review/evals/scorecard.json +3 -3
  103. package/skills/rollback/SKILL.md +4 -11
  104. package/skills/rollback/evals/scorecard.json +3 -3
  105. package/skills/shepherd/SKILL.md +20 -14
  106. package/skills/shepherd/evals/scorecard.json +2 -2
  107. package/skills/ship/SKILL.md +4 -12
  108. package/skills/ship/evals/scorecard.json +3 -3
  109. package/skills/worktree/SKILL.md +6 -1
  110. package/skills/worktree/evals/scorecard.json +2 -2
  111. package/lib/beads-setup.js +0 -538
  112. package/lib/beads-sync-scaffold.js +0 -189
  113. package/lib/pat-setup.js +0 -207
  114. package/lib/pr-monitor/render-sticky.js +0 -206
  115. package/lib/pr-monitor/upsert-sticky.js +0 -169
  116. package/scripts/beads-context.sh +0 -577
  117. package/scripts/beads-migrate-to-dolt.sh +0 -7
  118. package/scripts/beads-upgrade-smoke.sh +0 -284
  119. package/scripts/lib/beads-migrate-to-dolt.mjs +0 -503
@@ -0,0 +1,272 @@
1
+ 'use strict';
2
+
3
+ const fs = require('node:fs');
4
+ const path = require('node:path');
5
+
6
+ const {
7
+ createProtectedStateAuditRecord,
8
+ recordProtectedStateAuditEvent,
9
+ writeProtectedFile,
10
+ } = require('./protected-state-surfaces');
11
+ const {
12
+ issueNpmPublishWorkflowAuthorization,
13
+ } = require('./protected-state-authority');
14
+
15
+ const NPM_PUBLISH_WORKFLOW_PATH = '.github/workflows/npm-publish.yml';
16
+
17
+ function snapshotWorkflow(projectRoot) {
18
+ const fullPath = path.resolve(projectRoot, NPM_PUBLISH_WORKFLOW_PATH);
19
+ try {
20
+ const stat = fs.lstatSync(fullPath);
21
+ return {
22
+ fullPath,
23
+ existed: true,
24
+ content: stat.isFile() ? fs.readFileSync(fullPath) : null,
25
+ };
26
+ } catch (error) {
27
+ if (error.code === 'ENOENT') return { fullPath, existed: false, content: null };
28
+ throw error;
29
+ }
30
+ }
31
+
32
+ function rollbackWorkflow(snapshot, projectRoot, protectedWriter, writeOptions) {
33
+ if (!snapshot.existed) {
34
+ fs.rmSync(snapshot.fullPath, { force: true });
35
+ return;
36
+ }
37
+ if (!Buffer.isBuffer(snapshot.content)) {
38
+ throw new Error('Previous workflow was not a regular file');
39
+ }
40
+ const restored = protectedWriter(
41
+ projectRoot,
42
+ NPM_PUBLISH_WORKFLOW_PATH,
43
+ snapshot.content,
44
+ writeOptions,
45
+ );
46
+ if (!restored.allowed) throw new Error(restored.reason || 'Protected workflow restore was denied');
47
+ }
48
+
49
+ function renderNpmPublishWorkflow() {
50
+ return `# Generated by \`forge release generate-npm-workflow\`. Do not edit directly.
51
+ name: Bun Package
52
+
53
+ on:
54
+ release:
55
+ types: [published]
56
+
57
+ permissions:
58
+ contents: read
59
+
60
+ jobs:
61
+ resolve-release:
62
+ name: Resolve immutable release SHA
63
+ runs-on: ubuntu-latest
64
+ permissions:
65
+ contents: read
66
+ outputs:
67
+ commitSha: \${{ steps.resolve.outputs.commitSha }}
68
+ steps:
69
+ - uses: actions/checkout@v7
70
+ with:
71
+ ref: \${{ github.event.release.tag_name }}
72
+ fetch-depth: 0
73
+ persist-credentials: false
74
+ - name: Resolve release tag
75
+ id: resolve
76
+ shell: bash
77
+ env:
78
+ RELEASE_TAG: \${{ github.event.release.tag_name }}
79
+ run: |
80
+ set -euo pipefail
81
+ COMMIT_SHA="$(git rev-parse "\${RELEASE_TAG}^{commit}")"
82
+ if [ -z "$COMMIT_SHA" ] || [ "$COMMIT_SHA" != "$(git rev-parse HEAD)" ]; then
83
+ echo "::error::Release tag did not resolve to the checked out commit"
84
+ exit 1
85
+ fi
86
+ echo "commitSha=$COMMIT_SHA" >> "$GITHUB_OUTPUT"
87
+
88
+ build:
89
+ needs: resolve-release
90
+ runs-on: ubuntu-latest
91
+ permissions:
92
+ contents: read
93
+ outputs:
94
+ commitSha: \${{ steps.evidence.outputs.commitSha }}
95
+ receipt: \${{ steps.evidence.outputs.receipt }}
96
+ receiptSubject: \${{ steps.evidence.outputs.receiptSubject }}
97
+ steps:
98
+ - uses: actions/checkout@v7
99
+ with:
100
+ ref: \${{ needs.resolve-release.outputs.commitSha }}
101
+ fetch-depth: 1
102
+ persist-credentials: false
103
+ - uses: actions/setup-node@v7
104
+ with:
105
+ node-version: 22
106
+ - uses: oven-sh/setup-bun@v2
107
+ with:
108
+ bun-version: 1.3.12
109
+ - run: bun install --frozen-lockfile
110
+ - name: Setup test fixtures
111
+ shell: bash
112
+ run: bash test-env/automation/setup-fixtures.sh --force
113
+ - name: Run complete release suite
114
+ shell: bash
115
+ run: |
116
+ set -euo pipefail
117
+ node scripts/test-full-suite.js --label-prefix release-full
118
+ - name: Emit attributable release suite receipt
119
+ id: evidence
120
+ env:
121
+ EXPECTED_SHA: \${{ needs.resolve-release.outputs.commitSha }}
122
+ run: node scripts/npm-release-receipt.js emit
123
+
124
+ publish-npm:
125
+ needs: [resolve-release, build]
126
+ runs-on: ubuntu-latest
127
+ permissions:
128
+ contents: read
129
+ id-token: write
130
+ steps:
131
+ - uses: actions/checkout@v7
132
+ with:
133
+ ref: \${{ needs.resolve-release.outputs.commitSha }}
134
+ fetch-depth: 1
135
+ persist-credentials: false
136
+ - uses: actions/setup-node@v7
137
+ with:
138
+ node-version: 22
139
+ registry-url: https://registry.npmjs.org/
140
+ - name: Verify exact release suite receipt
141
+ env:
142
+ EXPECTED_SHA: \${{ needs.resolve-release.outputs.commitSha }}
143
+ VERIFIED_SHA: \${{ needs.build.outputs.commitSha }}
144
+ RECEIPT: \${{ needs.build.outputs.receipt }}
145
+ RECEIPT_SUBJECT: \${{ needs.build.outputs.receiptSubject }}
146
+ run: node scripts/npm-release-receipt.js verify
147
+ - uses: oven-sh/setup-bun@v2
148
+ with:
149
+ bun-version: 1.3.12
150
+ - run: bun install --frozen-lockfile
151
+ - name: Release tag matches package.json version
152
+ shell: bash
153
+ run: |
154
+ set -euo pipefail
155
+ PKG="$(node -p "require('./package.json').version")"
156
+ TAG="\${GITHUB_REF_NAME#v}"
157
+ if [ "$PKG" != "$TAG" ]; then
158
+ echo "::error::Release tag ($TAG) != package.json version ($PKG). Bump package.json before releasing."
159
+ exit 1
160
+ fi
161
+ - name: Release-readiness gate
162
+ run: node bin/forge.js release check --target "$(node -p "require('./package.json').version.split('-')[0]")"
163
+ - name: Verify package contents
164
+ run: npm pack --dry-run
165
+ - name: Ensure OIDC-capable npm
166
+ run: npm install --global npm@11.5.1 && npm --version
167
+ - name: Publish to npm
168
+ shell: bash
169
+ run: |
170
+ set -euo pipefail
171
+ VERSION="$(node -p "require('./package.json').version")"
172
+ if echo "$VERSION" | grep -q '-'; then
173
+ echo "Prerelease $VERSION -> publishing under the beta dist-tag"
174
+ npm publish --provenance --tag beta
175
+ else
176
+ npm publish --provenance
177
+ fi
178
+ `;
179
+ }
180
+
181
+ async function generateNpmPublishWorkflow(projectRoot, options = {}) {
182
+ const content = renderNpmPublishWorkflow();
183
+ const actor = options.actor || options.env?.FORGE_ACTOR || process.env.FORGE_ACTOR || process.env.USER || process.env.USERNAME || 'unknown';
184
+ const protectedWriter = options.writeProtectedFile || writeProtectedFile;
185
+ const auditWriter = options.recordProtectedStateAuditEvent || recordProtectedStateAuditEvent;
186
+ const authorizationWriter = options.issueNpmPublishWorkflowAuthorization || issueNpmPublishWorkflowAuthorization;
187
+ const snapshot = snapshotWorkflow(projectRoot);
188
+ const writeOptions = {
189
+ actor,
190
+ operation: 'generate_npm_workflow',
191
+ viaForgeApi: true,
192
+ surface: 'workflows',
193
+ };
194
+ let write;
195
+ try {
196
+ write = protectedWriter(projectRoot, NPM_PUBLISH_WORKFLOW_PATH, content, writeOptions);
197
+ } catch (error) {
198
+ return { success: false, error: error.message };
199
+ }
200
+ if (!write.allowed) {
201
+ return { success: false, error: write.reason, write };
202
+ }
203
+
204
+ const auditRecord = createProtectedStateAuditRecord({
205
+ actor,
206
+ surface: 'workflows',
207
+ path: NPM_PUBLISH_WORKFLOW_PATH,
208
+ content,
209
+ operation: 'generate_npm_workflow',
210
+ });
211
+ let audit;
212
+ try {
213
+ audit = auditWriter(auditRecord, { cwd: projectRoot });
214
+ } catch (error) {
215
+ audit = { success: false, error: error.message };
216
+ }
217
+ if (!audit.success) {
218
+ let rollbackError;
219
+ try {
220
+ rollbackWorkflow(snapshot, projectRoot, protectedWriter, writeOptions);
221
+ } catch (error) {
222
+ rollbackError = error.message;
223
+ }
224
+ return {
225
+ success: false,
226
+ error: `Could not record protected-state authorization: ${audit.error}`,
227
+ audit,
228
+ rollbackError,
229
+ };
230
+ }
231
+
232
+ let trustedAuthorization;
233
+ try {
234
+ trustedAuthorization = await authorizationWriter(
235
+ projectRoot,
236
+ { actor },
237
+ { deps: options.kernelDeps },
238
+ );
239
+ } catch (error) {
240
+ trustedAuthorization = { success: false, error: error.message };
241
+ }
242
+ if (!trustedAuthorization.success) {
243
+ let rollbackError;
244
+ try {
245
+ rollbackWorkflow(snapshot, projectRoot, protectedWriter, writeOptions);
246
+ } catch (error) {
247
+ rollbackError = error.message;
248
+ }
249
+ return {
250
+ success: false,
251
+ error: `Could not record trusted protected-state authorization: ${trustedAuthorization.error}`,
252
+ audit,
253
+ trustedAuthorization,
254
+ rollbackError,
255
+ };
256
+ }
257
+
258
+ return {
259
+ success: true,
260
+ path: NPM_PUBLISH_WORKFLOW_PATH,
261
+ contentHash: write.contentHash,
262
+ write,
263
+ audit,
264
+ trustedAuthorization,
265
+ };
266
+ }
267
+
268
+ module.exports = {
269
+ NPM_PUBLISH_WORKFLOW_PATH,
270
+ renderNpmPublishWorkflow,
271
+ generateNpmPublishWorkflow,
272
+ };
@@ -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),
@@ -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',
package/lib/pr-bundle.js CHANGED
@@ -86,12 +86,12 @@ async function gatherUnresolvedComments(adapter, { owner, repo, pr }) {
86
86
  * Predict merge conflicts. `detectConflicts` is optional on the adapter; when
87
87
  * absent or failing, report it as unsupported rather than throwing.
88
88
  */
89
- async function gatherConflicts(adapter, { baseRef, cwd }) {
89
+ async function gatherConflicts(adapter, { baseRef, cwd, headRef }) {
90
90
  if (typeof adapter.detectConflicts !== 'function') {
91
91
  return { supported: false, reason: 'adapter has no detectConflicts capability' };
92
92
  }
93
93
  try {
94
- return await adapter.detectConflicts({ baseRef, cwd });
94
+ return await adapter.detectConflicts({ baseRef, cwd, headRef });
95
95
  } catch (error) {
96
96
  return { supported: false, reason: error.message || 'conflict detection failed' };
97
97
  }
@@ -148,9 +148,9 @@ async function gatherPrBundle({ pr, owner, repo, base, baseRef, cwd, adapter })
148
148
  // silently yields a null required set. `pr` lets the adapter fall back to the
149
149
  // rollup `isRequired` set when branch protection is unreadable in CI.
150
150
  const requiredRaw = await adapter.readRequiredChecks({ owner, repo, base, pr });
151
- const divergence = await adapter.readDivergence({ baseRef, cwd });
151
+ const divergence = await adapter.readDivergence({ baseRef, cwd, headRef: state.headSha });
152
152
  const comments = await gatherUnresolvedComments(adapter, { owner, repo, pr });
153
- const conflicts = await gatherConflicts(adapter, { baseRef, cwd });
153
+ const conflicts = await gatherConflicts(adapter, { baseRef, cwd, headRef: state.headSha });
154
154
 
155
155
  const requiredSet = requiredRaw === undefined ? null : requiredRaw;
156
156
 
@@ -172,8 +172,7 @@ async function gatherPrBundle({ pr, owner, repo, base, baseRef, cwd, adapter })
172
172
  state: String(state.state || 'OPEN').toUpperCase(),
173
173
  },
174
174
  ci: buildCi(state.checks, requiredSet),
175
- // Which source answered the required-checks read (`protection` | `rollup` |
176
- // null) — `rollup` is the CI path where branch protection is unreadable.
175
+ // The authoritative required-check source (`protection` or null).
177
176
  requiredSource: adapter.lastRequiredSource || null,
178
177
  branch: {
179
178
  ahead: divergence.ahead || 0,
@@ -35,14 +35,29 @@ function sanitize(part) {
35
35
  return String(part || '').replace(/[^A-Za-z0-9._-]+/g, '-');
36
36
  }
37
37
 
38
+ /**
39
+ * Resolve the one journal authority shared by every worktree. Standard repos
40
+ * use `<main-root>/.forge/pr-monitor`; a nonstandard common dir centralizes
41
+ * beneath that shared metadata directory. No common dir preserves legacy use.
42
+ */
43
+ function resolveJournalRoot({ root, gitCommonDir }) {
44
+ const fallbackRoot = path.resolve(root);
45
+ if (!gitCommonDir) return path.join(fallbackRoot, '.forge', 'pr-monitor');
46
+ const commonDir = path.resolve(gitCommonDir);
47
+ if (path.basename(commonDir).toLowerCase() === '.git') {
48
+ return path.join(path.dirname(commonDir), '.forge', 'pr-monitor');
49
+ }
50
+ return path.join(commonDir, 'forge', 'pr-monitor');
51
+ }
52
+
38
53
  /**
39
54
  * Resolve (and create) the per-PR journal directory.
40
55
  *
41
56
  * @param {{ root: string, repo: string, pr: string|number }} ctx
42
57
  * @returns {string} absolute directory path
43
58
  */
44
- function journalDir({ root, repo, pr }) {
45
- const dir = path.join(root, '.forge', 'pr-monitor', `${sanitize(repo)}-${sanitize(pr)}`);
59
+ function journalDir({ root, gitCommonDir, repo, pr }) {
60
+ const dir = path.join(resolveJournalRoot({ root, gitCommonDir }), `${sanitize(repo)}-${sanitize(pr)}`);
46
61
  fs.mkdirSync(dir, { recursive: true });
47
62
  return dir;
48
63
  }
@@ -278,6 +293,7 @@ function watcherRunning(dir) {
278
293
 
279
294
  module.exports = {
280
295
  sanitize,
296
+ resolveJournalRoot,
281
297
  journalDir,
282
298
  journalPath,
283
299
  snapshotPath,