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.
- package/AGENTS.md +18 -7
- package/CHANGELOG.md +79 -1
- package/CLAUDE.md +0 -12
- package/CODING_STANDARDS.md +72 -0
- package/README.md +6 -2
- package/bin/forge-cmd.js +20 -0
- package/bin/forge.js +28 -375
- package/docs/INDEX.md +1 -1
- package/docs/guides/BEADS_GITHUB_SYNC.md +2 -31
- package/docs/guides/MIGRATION.md +4 -4
- package/docs/guides/SETUP.md +16 -16
- package/docs/reference/COMMANDS.md +8 -5
- package/docs/reference/FORGE_KERNEL_STORAGE_MODEL.md +4 -0
- package/docs/reference/INSIGHTS_RECAP.md +9 -20
- package/docs/reference/INSTALL.md +4 -0
- package/docs/reference/LEGACY_CLAIM_REPAIR.md +112 -0
- package/docs/reference/RELEASE.md +5 -3
- package/docs/reference/TOOLCHAIN.md +8 -0
- package/docs/reference/github-accounts.md +134 -0
- package/docs/reference/protected-state-surfaces.md +4 -4
- package/docs/reference/shepherd.md +114 -35
- package/lefthook.yml +12 -0
- package/lib/activation/ensure-forge-home.js +33 -15
- package/lib/adapters/pr-state-adapter.js +359 -144
- package/lib/audit-evidence.js +71 -110
- package/lib/base-remote.js +138 -0
- package/lib/beta5-compatibility-evidence.js +1093 -0
- package/lib/bun-lockfile-proof.js +413 -0
- package/lib/bun-workflow-pins.js +461 -0
- package/lib/capabilities/index.js +9 -0
- package/lib/capabilities/model.js +141 -0
- package/lib/capabilities/probes.js +347 -0
- package/lib/capped-jsonl-log.js +236 -0
- package/lib/codex-skills.js +2 -2
- package/lib/commands/_manifest.js +1 -0
- package/lib/commands/_registry.js +50 -20
- package/lib/commands/clean.js +252 -32
- package/lib/commands/dev.js +4 -33
- package/lib/commands/doctor.js +37 -6
- package/lib/commands/gate.js +197 -27
- package/lib/commands/github.js +215 -0
- package/lib/commands/hooks.js +276 -30
- package/lib/commands/insights.js +8 -3
- package/lib/commands/memory.js +66 -2
- package/lib/commands/merge.js +1265 -58
- package/lib/commands/plan.js +33 -2
- package/lib/commands/pr.js +3 -1
- package/lib/commands/preflight.js +21 -4
- package/lib/commands/prime.js +21 -8
- package/lib/commands/push.js +146 -54
- package/lib/commands/recall.js +127 -49
- package/lib/commands/recap.js +6 -1
- package/lib/commands/release.js +39 -3
- package/lib/commands/remember.js +28 -4
- package/lib/commands/serve.js +26 -9
- package/lib/commands/setup.js +323 -98
- package/lib/commands/shepherd.js +591 -73
- package/lib/commands/ship.js +36 -91
- package/lib/commands/skill.js +127 -11
- package/lib/commands/status.js +17 -1
- package/lib/commands/team.js +47 -8
- package/lib/commands/test.js +187 -38
- package/lib/commands/validate.js +65 -21
- package/lib/commands/worktree.js +359 -45
- package/lib/core/runtime-graph.js +1 -1
- package/lib/doc-assertions.js +297 -0
- package/lib/existing-tdd-gate.js +253 -0
- package/lib/fixtures/beta5-corpus/v1/README.md +9 -0
- package/lib/fixtures/beta5-corpus/v1/contract/command-contract.json +26 -0
- package/lib/fixtures/beta5-corpus/v1/contract/package-contract.json +13 -0
- package/lib/fixtures/beta5-corpus/v1/contract/workflow-stage-matrix.json +8 -0
- package/lib/fixtures/beta5-corpus/v1/manifest.json +25 -0
- package/lib/fixtures/beta5-corpus/v1/state/comments.jsonl +1 -0
- package/lib/fixtures/beta5-corpus/v1/state/config.yaml +6 -0
- package/lib/fixtures/beta5-corpus/v1/state/dependencies.jsonl +1 -0
- package/lib/fixtures/beta5-corpus/v1/state/issues.jsonl +2 -0
- package/lib/fixtures/beta5-corpus/v1/state/kernel.sql +20 -0
- package/lib/forge-context.js +1 -4
- package/lib/forge-issues.js +134 -32
- package/lib/gate-events.js +98 -10
- package/lib/git-defaults.js +56 -0
- package/lib/github-context.js +308 -0
- package/lib/global-flags.js +1 -0
- package/lib/harness-capability-matrix.js +3 -3
- package/lib/hook-renderer.js +122 -5
- package/lib/insights.js +96 -80
- package/lib/issue-render.js +19 -0
- package/lib/kernel/backing-issue.js +14 -2
- package/lib/kernel/broker.js +739 -31
- package/lib/kernel/claim-reconciler.js +238 -0
- package/lib/kernel/cli-broker-factory.js +12 -1
- package/lib/kernel/close-on-merge.js +154 -0
- package/lib/kernel/fs-class.js +42 -25
- package/lib/kernel/lease-enforcer.js +9 -4
- package/lib/kernel/legacy-claim-repair.js +442 -0
- package/lib/kernel/live-claim-projection.js +26 -0
- package/lib/kernel/migrations.js +118 -3
- package/lib/kernel/readiness-model.js +184 -12
- package/lib/kernel/schema.js +49 -1
- package/lib/kernel/sqlite-driver.js +3435 -172
- package/lib/kernel/taxonomy-validator.js +4 -1
- package/lib/kernel/windows-private-acl.js +239 -0
- package/lib/lefthook-wiring.js +21 -1
- package/lib/memory/hygiene.js +191 -0
- package/lib/memory/router.js +110 -28
- package/lib/memory/usage-evidence.js +4 -0
- package/lib/memory-digest.js +106 -15
- package/lib/memory-recall-events.js +145 -0
- package/lib/memory-recall.js +71 -10
- package/lib/merge-rules.js +143 -21
- package/lib/npm-publish-workflow.js +465 -0
- package/lib/orientation.js +68 -43
- package/lib/package-root.js +2 -0
- package/lib/plugin-catalog.js +14 -4
- package/lib/pr-bundle.js +5 -6
- package/lib/pr-monitor/auto-actions.js +169 -28
- package/lib/pr-monitor/differ.js +110 -4
- package/lib/pr-monitor/events.js +0 -0
- package/lib/pr-monitor/flow-monitor.js +1424 -0
- package/lib/pr-monitor/gather.js +251 -44
- package/lib/pr-monitor/journal.js +18 -39
- package/lib/pr-monitor/monitor.js +117 -10
- package/lib/pr-monitor/process-identity.js +117 -0
- package/lib/pr-monitor/reconcile-executor.js +1129 -470
- package/lib/pr-monitor/reconcile.js +0 -0
- package/lib/pr-monitor/render-summary.js +293 -0
- package/lib/pr-monitor/review-preflight.js +269 -0
- package/lib/pr-monitor/shepherd-lease.js +38 -20
- package/lib/pr-monitor/verdict.js +438 -0
- package/lib/pr-monitor/watch-lifecycle.js +145 -27
- package/lib/pr-monitor/watch-owner.js +1414 -0
- package/lib/pr-monitor/watch.js +129 -58
- package/lib/pr-pull.js +33 -14
- package/lib/pr-shepherd.js +51 -11
- package/lib/preflight/gates.js +65 -18
- package/lib/preflight/runner.js +5 -0
- package/lib/project-memory.js +178 -4
- package/lib/protected-state-authority.js +1100 -0
- package/lib/protected-state-surfaces.js +243 -45
- package/lib/release-readiness.js +53 -7
- package/lib/review-adapter.js +65 -0
- package/lib/shell-utils.js +1 -1
- package/lib/skills-sync.js +71 -35
- package/lib/smart-merge.js +28 -4
- package/lib/symlink-utils.js +74 -26
- package/lib/upgrade-safety.js +39 -0
- package/lib/using-forge.js +19 -6
- package/lib/validation/risk-manifest.js +339 -0
- package/lib/workflow/enforce-stage.js +44 -0
- package/lib/workflow/plan-authority.js +225 -0
- package/package.json +12 -9
- package/scripts/commitlint.js +13 -15
- package/scripts/doc-asserting-tests.js +158 -0
- package/scripts/generate-risk-manifest.js +91 -0
- package/scripts/github-context-bridge.sh +10 -0
- package/scripts/legacy-claim-repair.js +145 -0
- package/scripts/lib/behavioral-eval-runner.js +310 -0
- package/scripts/lib/behavioral-eval-runtime.js +457 -0
- package/scripts/lib/eval-evidence.js +328 -0
- package/scripts/lib/eval-runner.js +81 -41
- package/scripts/lib/immutable-eval-corpus.js +309 -0
- package/scripts/lib/promotion-evidence-loader.js +94 -0
- package/scripts/lib/promotion-scorecard.js +314 -0
- package/scripts/npm-release-receipt.js +134 -0
- package/scripts/process-tree.js +773 -0
- package/scripts/protected-state-check.js +479 -31
- package/scripts/run-command-eval.js +29 -1
- package/scripts/sync-agent-skills.js +333 -34
- package/scripts/sync-d20-audit.js +172 -0
- package/scripts/test-full-suite.js +935 -37
- package/scripts/test-profile.js +13 -3
- package/scripts/test.js +271 -57
- package/skills/coverage.json +1 -0
- package/skills/review/SKILL.md +6 -11
- package/skills/review/evals/scorecard.json +4 -4
- package/skills/rollback/SKILL.md +4 -11
- package/skills/rollback/evals/scorecard.json +3 -3
- package/skills/setup/SKILL.md +18 -0
- package/skills/setup/evals/scorecard.json +3 -3
- package/skills/shepherd/SKILL.md +39 -16
- package/skills/shepherd/evals/scorecard.json +4 -4
- package/skills/ship/SKILL.md +4 -12
- package/skills/ship/evals/scorecard.json +3 -3
- package/skills/validate/SKILL.md +3 -0
- package/skills/validate/evals/scorecard.json +1 -1
- package/skills/worktree/SKILL.md +6 -1
- package/skills/worktree/evals/scorecard.json +2 -2
- package/lib/beads-setup.js +0 -538
- package/lib/beads-sync-scaffold.js +0 -189
- package/lib/pat-setup.js +0 -207
- package/lib/pr-monitor/render-sticky.js +0 -206
- package/lib/pr-monitor/upsert-sticky.js +0 -169
- package/scripts/beads-context.sh +0 -577
- package/scripts/beads-migrate-to-dolt.sh +0 -7
- package/scripts/beads-upgrade-smoke.sh +0 -284
- 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
|
|
113
|
-
|
|
114
|
-
|
|
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: '
|
|
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: '
|
|
202
|
+
source: 'kernel_events',
|
|
132
203
|
lastSeen: row.created_at,
|
|
133
204
|
});
|
|
134
205
|
}
|
|
135
206
|
}
|
|
136
|
-
return
|
|
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
|
|
158
|
-
|
|
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: '
|
|
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,
|
|
251
|
-
const issues = issuePatterns(projectRoot,
|
|
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
|
|
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
|
};
|
package/lib/issue-render.js
CHANGED
|
@@ -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
|
-
//
|
|
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
|
|
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
|
}
|