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
package/lib/insights.js CHANGED
@@ -3,10 +3,76 @@
3
3
  const fs = require('node:fs');
4
4
  const path = require('node:path');
5
5
  const typedMemory = require('./memory/typed-api');
6
+ const { runIssueOperation: defaultRunIssueOperation } = require('./forge-issues');
7
+ const { buildMigratedKernelIssueDeps } = require('./kernel/cli-broker-factory');
6
8
 
7
9
  const ISSUE_REFS = ['forge-besw.12', 'forge-1gry', 'forge-5q7s'];
8
10
  const DEFAULT_MIN_COUNT = 5;
9
11
  const DEFAULT_LIMIT = 10;
12
+ // Upper bound on kernel_events scanned for interaction patterns — a spot-read, not a
13
+ // full history walk. Newest-first, so recent activity dominates the signal.
14
+ const EVENT_READ_LIMIT = 2000;
15
+
16
+ // Default kernel activity read for `forge insights` (Slice C2). Builds a short-lived
17
+ // migrated broker, reads recent kernel_events, and ALWAYS closes the driver (Windows
18
+ // CI fails on a leaked handle). Injectable via analyzeInsights options for tests.
19
+ async function defaultListRecentEvents(projectRoot, { since = null, limit = null } = {}) {
20
+ let deps;
21
+ try {
22
+ deps = await buildMigratedKernelIssueDeps({ projectRoot });
23
+ } catch {
24
+ return [];
25
+ }
26
+ try {
27
+ return await deps.kernelBroker.listRecentEvents({ since, limit });
28
+ } catch {
29
+ return [];
30
+ } finally {
31
+ if (deps.kernelDriver && typeof deps.kernelDriver.close === 'function') {
32
+ deps.kernelDriver.close();
33
+ }
34
+ }
35
+ }
36
+
37
+ // Map a kernel_events row back to the interaction shape insights consumes. Imported beads
38
+ // interactions carry event_type `beads.interaction.<kind>` and payload_json `{ kind, ...extra }`
39
+ // where field/new_value/reason live at the top level alongside kind (see beads-kernel-compat
40
+ // mapBeadsInteractionToKernel). Native kernel events fall back to their event_type as the kind.
41
+ function eventToInteraction(row) {
42
+ if (!row || typeof row !== 'object') return null;
43
+ let parsed;
44
+ try {
45
+ parsed = row.payload_json ? JSON.parse(row.payload_json) : row.payload;
46
+ } catch {
47
+ parsed = null;
48
+ }
49
+ // A scalar or array payload carries no interaction fields — treat it as empty.
50
+ const payload = parsed && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : {};
51
+ const kind = payload.kind
52
+ || String(row.event_type || '').replace(/^beads\.interaction\./, '')
53
+ || 'interaction';
54
+ return {
55
+ kind,
56
+ id: row.id,
57
+ issue_id: row.entity_id || null,
58
+ created_at: row.created_at || null,
59
+ extra: payload,
60
+ };
61
+ }
62
+
63
+ // Read all issues from the Kernel (the sole issue-state authority) for theme mining.
64
+ // Resilient: any read failure degrades to empty issue evidence rather than throwing.
65
+ async function listKernelIssues(projectRoot, runIssueOperation, env) {
66
+ try {
67
+ const result = await runIssueOperation('list', [], projectRoot, { issueBackend: 'kernel', env });
68
+ if (result && result.ok && result.data && Array.isArray(result.data.issues)) {
69
+ return result.data.issues;
70
+ }
71
+ } catch {
72
+ // best-effort: insights never crashes on a kernel read failure
73
+ }
74
+ return [];
75
+ }
10
76
  const STOP_WORDS = new Set([
11
77
  'after',
12
78
  'against',
@@ -108,10 +174,15 @@ function addPattern(map, key, patch) {
108
174
  map.set(key, existing);
109
175
  }
110
176
 
111
- function interactionPatterns(projectRoot, options, map) {
112
- const rows = readJsonl(path.join(projectRoot, '.beads', 'interactions.jsonl'));
113
- for (const row of rows) {
114
- if (!row || typeof row !== 'object' || row._parseError || !isSince(row.created_at, options.since)) continue;
177
+ async function interactionPatterns(projectRoot, options, map) {
178
+ const listRecentEvents = options.listRecentEvents || defaultListRecentEvents;
179
+ const since = options.since ? options.since.toISOString() : null;
180
+ const rawEvents = await listRecentEvents(projectRoot, { since, limit: EVENT_READ_LIMIT, env: options.env });
181
+ const interactions = (Array.isArray(rawEvents) ? rawEvents : [])
182
+ .map(eventToInteraction)
183
+ .filter(Boolean);
184
+ for (const row of interactions) {
185
+ if (!isSince(row.created_at, options.since)) continue;
115
186
  const extra = row.extra && typeof row.extra === 'object' ? row.extra : {};
116
187
  if (row.kind === 'field_change' && extra.field) {
117
188
  const family = reasonFamily(extra.reason);
@@ -120,7 +191,7 @@ function interactionPatterns(projectRoot, options, map) {
120
191
  kind: 'interaction',
121
192
  title: `${extra.field} changed to ${extra.new_value || 'changed'} (${family})`,
122
193
  evidence: row.issue_id || row.id,
123
- source: '.beads/interactions.jsonl',
194
+ source: 'kernel_events',
124
195
  lastSeen: row.created_at,
125
196
  });
126
197
  } else if (row.kind) {
@@ -128,12 +199,12 @@ function interactionPatterns(projectRoot, options, map) {
128
199
  kind: 'interaction',
129
200
  title: `Interaction event: ${row.kind}`,
130
201
  evidence: row.issue_id || row.id,
131
- source: '.beads/interactions.jsonl',
202
+ source: 'kernel_events',
132
203
  lastSeen: row.created_at,
133
204
  });
134
205
  }
135
206
  }
136
- return rows;
207
+ return interactions;
137
208
  }
138
209
 
139
210
  function words(value) {
@@ -153,9 +224,9 @@ function words(value) {
153
224
  return tokens;
154
225
  }
155
226
 
156
- function issuePatterns(projectRoot, options, map) {
157
- const rows = readJsonl(path.join(projectRoot, '.beads', 'issues.jsonl'))
158
- .filter(row => row && !row._parseError && row._type === 'issue');
227
+ async function issuePatterns(projectRoot, options, map) {
228
+ const runIssueOperation = options.runIssueOperation || defaultRunIssueOperation;
229
+ const rows = await listKernelIssues(projectRoot, runIssueOperation, options.env);
159
230
  const perWord = new Map();
160
231
  for (const issue of rows) {
161
232
  if (!isSince(issue.updated_at || issue.closed_at || issue.created_at, options.since)) continue;
@@ -173,7 +244,7 @@ function issuePatterns(projectRoot, options, map) {
173
244
  title: `Recurring issue theme: ${word}`,
174
245
  count: issues.length,
175
246
  evidence: issues.slice(0, 5).map(issue => issue.id).join(', '),
176
- source: '.beads/issues.jsonl',
247
+ source: 'kernel',
177
248
  lastSeen: issues.map(issue => asDate(issue.updated_at || issue.closed_at || issue.created_at))
178
249
  .filter(Boolean)
179
250
  .sort((a, b) => b - a)[0],
@@ -244,11 +315,19 @@ function candidateFromPattern(pattern) {
244
315
  };
245
316
  }
246
317
 
247
- function analyzeInsights(projectRoot, options = {}) {
318
+ async function analyzeInsights(projectRoot, options = {}) {
248
319
  const normalized = normalizeOptions(options);
320
+ // Read options carry the normalized thresholds PLUS the injectable kernel seams
321
+ // (defaulted to the real cli-broker-factory-backed reads) so tests can supply fakes.
322
+ const readOptions = {
323
+ ...normalized,
324
+ runIssueOperation: options.runIssueOperation,
325
+ listRecentEvents: options.listRecentEvents,
326
+ env: options.env,
327
+ };
249
328
  const map = new Map();
250
- const interactions = interactionPatterns(projectRoot, normalized, map);
251
- const issues = issuePatterns(projectRoot, normalized, map);
329
+ const interactions = await interactionPatterns(projectRoot, readOptions, map);
330
+ const issues = await issuePatterns(projectRoot, readOptions, map);
252
331
  const audit = auditPatterns(projectRoot, normalized, map);
253
332
  const patterns = toPatternList(map, normalized);
254
333
  const candidates = patterns.map(candidateFromPattern);
@@ -267,7 +346,7 @@ function analyzeInsights(projectRoot, options = {}) {
267
346
  candidates,
268
347
  limitations: [
269
348
  'Insights are local workflow signals, not proof of correctness.',
270
- 'Sparse Beads interactions or missing audit logs reduce confidence.',
349
+ 'Sparse kernel events or missing audit logs reduce confidence.',
271
350
  'Accepting a suggestion records a decision; it does not install trusted executable code.',
272
351
  ],
273
352
  };
@@ -314,6 +393,8 @@ function recordInsightDecision(projectRoot, candidateId, status, options = {}) {
314
393
  }, {
315
394
  memory: options.memory,
316
395
  tags: ['insights', status],
396
+ // `beadsRefs` is a persisted typed-memory field name kept for data-shape compat
397
+ // (imported/legacy memories carry it); the values are historical issue references.
317
398
  beadsRefs: ISSUE_REFS,
318
399
  provenance: {
319
400
  actor: 'forge insights',
@@ -323,75 +404,10 @@ function recordInsightDecision(projectRoot, candidateId, status, options = {}) {
323
404
  });
324
405
  }
325
406
 
326
- function issueSummary(issues) {
327
- return issues.reduce((summary, issue) => {
328
- summary.total += 1;
329
- if (issue.status === 'closed') summary.closed += 1;
330
- else summary.open += 1;
331
- return summary;
332
- }, { total: 0, open: 0, closed: 0 });
333
- }
334
-
335
- function buildRecap(projectRoot, options = {}) {
336
- const normalized = normalizeOptions(options);
337
- const insights = analyzeInsights(projectRoot, options);
338
- const issues = readJsonl(path.join(projectRoot, '.beads', 'issues.jsonl'))
339
- .filter(row => row && !row._parseError && row._type === 'issue')
340
- .filter(issue => isSince(issue.updated_at || issue.closed_at || issue.created_at, normalized.since));
341
- const interactions = readJsonl(path.join(projectRoot, '.beads', 'interactions.jsonl'))
342
- .filter(row => row && typeof row === 'object' && !row._parseError)
343
- .filter(row => isSince(row.created_at, normalized.since));
344
- const reviewOutcomes = interactions.filter(row => {
345
- const reason = row.extra?.reason || '';
346
- return reasonFamily(reason) === 'merged-and-verified' || String(reason).toLowerCase().includes('review');
347
- }).length;
348
- const recentIssues = [...issues]
349
- .sort((a, b) => String(b.updated_at || b.closed_at || b.created_at || '').localeCompare(String(a.updated_at || a.closed_at || a.created_at || '')))
350
- .slice(0, normalized.limit)
351
- .map(issue => ({
352
- id: issue.id,
353
- title: issue.title,
354
- status: issue.status,
355
- }));
356
-
357
- return {
358
- generatedAt: new Date().toISOString(),
359
- issueSummary: issueSummary(issues),
360
- reviewOutcomes,
361
- recentIssues,
362
- insights,
363
- };
364
- }
365
-
366
- function formatRecapText(recap) {
367
- const lines = [
368
- 'Forge recap',
369
- `Issues: ${recap.issueSummary.total} total, ${recap.issueSummary.open} open, ${recap.issueSummary.closed} closed`,
370
- `Review outcomes found: ${recap.reviewOutcomes}`,
371
- 'Recent work:',
372
- ];
373
- for (const issue of recap.recentIssues) {
374
- lines.push(`- ${issue.id}: ${issue.title} [${issue.status || 'unknown'}]`);
375
- }
376
- lines.push('Insight candidates:');
377
- if (recap.insights.candidates.length === 0) {
378
- lines.push('- No strong recurring patterns found.');
379
- } else {
380
- for (const candidate of recap.insights.candidates) {
381
- lines.push(`- ${candidate.id}: ${candidate.title}`);
382
- }
383
- }
384
- lines.push('Limitations:');
385
- for (const limitation of recap.insights.limitations) lines.push(`- ${limitation}`);
386
- return `${lines.join('\n')}\n`;
387
- }
388
-
389
407
  module.exports = {
390
408
  ISSUE_REFS,
391
409
  analyzeInsights,
392
- buildRecap,
393
410
  formatInsightsText,
394
- formatRecapText,
395
411
  readJsonl,
396
412
  recordInsightDecision,
397
413
  };
@@ -114,6 +114,19 @@ function pushSection(lines, label, text) {
114
114
  lines.push('', `${label}:`, text);
115
115
  }
116
116
 
117
+ function renderReadinessReason(reason) {
118
+ if (!reason || typeof reason !== 'object') return String(reason ?? 'unknown');
119
+ const detail = reason.field || reason.issue_id || reason.gate || reason.actor || reason.status;
120
+ const parts = [];
121
+ if (detail) parts.push(String(detail));
122
+ if (reason.expected !== undefined) parts.push(`expected ${JSON.stringify(reason.expected)}`);
123
+ if (reason.actual !== undefined) parts.push(`actual ${JSON.stringify(reason.actual)}`);
124
+ if (reason.declared !== undefined) parts.push(`declared ${JSON.stringify(reason.declared)}`);
125
+ return parts.length > 0
126
+ ? `${reason.code} (${parts.join(', ')})`
127
+ : String(reason.code || 'unknown');
128
+ }
129
+
117
130
  // forge show: the full detail view. This is the surface that prints the FULL id
118
131
  // (list/ready show only the 8-char prefix), so every field a script or human
119
132
  // needs to act on the issue is reachable without --json.
@@ -135,6 +148,12 @@ function renderIssueShow(envelope) {
135
148
  pushField(lines, 'Claimed by', issue.claimed_by);
136
149
  pushField(lines, 'Parent', shortId(issue.parent_id ?? null) || null);
137
150
  pushField(lines, 'Blocked by', Array.isArray(issue.blocked_by) ? issue.blocked_by.map(shortId) : null);
151
+ pushField(lines, 'Readiness', issue.readiness_state);
152
+ pushField(
153
+ lines,
154
+ 'Readiness reasons',
155
+ Array.isArray(issue.readiness_reasons) ? issue.readiness_reasons.map(renderReadinessReason) : null,
156
+ );
138
157
  pushField(lines, 'Dependencies', Array.isArray(issue.dependencies) ? issue.dependencies.map(shortId) : null);
139
158
  pushField(lines, 'Dependents', Array.isArray(issue.dependents) ? issue.dependents.map(shortId) : null);
140
159
  pushField(lines, 'Created', issue.created_at);
@@ -39,7 +39,14 @@ const DEFAULT_IGNORE_GLOBS = Object.freeze(['tmp/*', 'spike/*', 'wip/*', 'throwa
39
39
  const AUTO_STUB_LABEL = 'auto-stub';
40
40
  // Branch prefixes stripped before deriving a title / probing for an encoded issue id.
41
41
  const BRANCH_PREFIX = /^(feat|feature|fix|bugfix|hotfix|chore|refactor|docs|test|spike|wip)\//i;
42
- // An issue key encoded in a branch slug, e.g. `feat/kap-7-foo` -> `kap-7`.
42
+ // A full Kernel issue id (UUID) named anywhere in the branch, e.g.
43
+ // `feat/18f1988e-...-close-on-merge`. Kernel ids ARE UUIDs, so this is the shape a
44
+ // real branch->issue reference actually takes; kept identical to the UUID_RE that
45
+ // resolveActiveIssueId (lib/workflow/enforce-stage.js) matches, so every resolver
46
+ // agrees on which issue a branch names.
47
+ const ENCODED_UUID = /[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i;
48
+ // Legacy Beads-era issue key encoded in a branch slug, e.g. `feat/kap-7-foo` -> `kap-7`.
49
+ // Still honored for stores imported via `forge migrate --from beads`.
43
50
  const ENCODED_ISSUE_ID = /^([a-z][a-z0-9]*-\d+)\b/i;
44
51
  // Regex metacharacters escaped when compiling an ignore glob.
45
52
  const GLOB_METACHARS = '.+^${}()|[]\\';
@@ -145,12 +152,17 @@ function deriveTitle(branch) {
145
152
  }
146
153
 
147
154
  /**
148
- * Extract an issue id encoded in the branch name (e.g. `feat/kap-7-foo` -> `kap-7`).
155
+ * Extract an issue id named in the branch: a Kernel UUID anywhere in the name
156
+ * (`feat/18f1988e-...-foo`), else a legacy Beads key at the head of the slug
157
+ * (`feat/kap-7-foo` -> `kap-7`). The caller VERIFIES the id exists before linking,
158
+ * so a branch that merely looks like an id never binds to a phantom issue.
149
159
  *
150
160
  * @param {string} branch
151
161
  * @returns {string|null}
152
162
  */
153
163
  function extractEncodedIssueId(branch) {
164
+ const uuid = ENCODED_UUID.exec(String(branch));
165
+ if (uuid) return uuid[0];
154
166
  const match = ENCODED_ISSUE_ID.exec(stripPrefix(branch));
155
167
  return match ? match[1] : null;
156
168
  }