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.
- package/AGENTS.md +14 -7
- package/CHANGELOG.md +43 -1
- package/README.md +6 -2
- package/bin/forge-cmd.js +20 -0
- package/bin/forge.js +16 -374
- 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/INSIGHTS_RECAP.md +9 -20
- package/docs/reference/RELEASE.md +5 -3
- package/docs/reference/TOOLCHAIN.md +8 -0
- package/docs/reference/protected-state-surfaces.md +4 -4
- package/docs/reference/shepherd.md +54 -25
- package/lefthook.yml +12 -0
- package/lib/activation/ensure-forge-home.js +33 -15
- package/lib/adapters/pr-state-adapter.js +344 -142
- package/lib/audit-evidence.js +71 -110
- package/lib/capped-jsonl-log.js +236 -0
- package/lib/commands/_registry.js +2 -2
- package/lib/commands/clean.js +196 -32
- package/lib/commands/dev.js +4 -33
- package/lib/commands/hooks.js +223 -25
- package/lib/commands/insights.js +8 -3
- package/lib/commands/merge.js +600 -40
- package/lib/commands/pr.js +1 -1
- package/lib/commands/preflight.js +11 -2
- package/lib/commands/prime.js +21 -8
- package/lib/commands/push.js +41 -51
- package/lib/commands/recall.js +60 -16
- package/lib/commands/recap.js +6 -1
- package/lib/commands/release.js +17 -2
- package/lib/commands/setup.js +191 -94
- package/lib/commands/shepherd.js +13 -1
- package/lib/commands/ship.js +22 -23
- package/lib/commands/skill.js +119 -11
- package/lib/commands/status.js +17 -1
- package/lib/commands/test.js +24 -34
- package/lib/commands/worktree.js +220 -42
- 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/forge-context.js +1 -4
- package/lib/forge-issues.js +56 -32
- package/lib/git-defaults.js +56 -0
- package/lib/harness-capability-matrix.js +3 -3
- package/lib/hook-renderer.js +93 -4
- package/lib/insights.js +96 -80
- package/lib/kernel/backing-issue.js +14 -2
- package/lib/kernel/broker.js +16 -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/sqlite-driver.js +153 -29
- package/lib/lefthook-wiring.js +21 -1
- package/lib/memory/router.js +16 -1
- package/lib/memory-digest.js +47 -15
- package/lib/memory-recall-events.js +145 -0
- package/lib/memory-recall.js +71 -10
- package/lib/merge-rules.js +8 -4
- package/lib/npm-publish-workflow.js +272 -0
- package/lib/orientation.js +68 -43
- package/lib/plugin-catalog.js +14 -4
- package/lib/pr-bundle.js +5 -6
- package/lib/pr-monitor/journal.js +18 -2
- package/lib/pr-monitor/reconcile-executor.js +224 -41
- package/lib/pr-monitor/render-summary.js +196 -0
- package/lib/pr-monitor/shepherd-lease.js +10 -1
- package/lib/pr-monitor/watch-lifecycle.js +13 -1
- package/lib/pr-pull.js +33 -14
- package/lib/pr-shepherd.js +34 -8
- package/lib/preflight/gates.js +65 -18
- package/lib/preflight/runner.js +5 -0
- package/lib/project-memory.js +33 -1
- package/lib/protected-state-authority.js +305 -0
- package/lib/protected-state-surfaces.js +64 -44
- package/lib/release-readiness.js +51 -4
- package/lib/shell-utils.js +1 -1
- package/lib/skills-sync.js +6 -3
- 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/package.json +6 -7
- package/scripts/doc-asserting-tests.js +158 -0
- package/scripts/lib/behavioral-eval-runner.js +310 -0
- package/scripts/lib/behavioral-eval-runtime.js +456 -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 +761 -0
- package/scripts/protected-state-check.js +47 -22
- package/scripts/run-command-eval.js +29 -1
- package/scripts/sync-d20-audit.js +172 -0
- package/scripts/test-full-suite.js +249 -37
- package/scripts/test.js +176 -43
- package/skills/review/SKILL.md +4 -11
- package/skills/review/evals/scorecard.json +3 -3
- package/skills/rollback/SKILL.md +4 -11
- package/skills/rollback/evals/scorecard.json +3 -3
- package/skills/shepherd/SKILL.md +20 -14
- package/skills/shepherd/evals/scorecard.json +2 -2
- package/skills/ship/SKILL.md +4 -12
- package/skills/ship/evals/scorecard.json +3 -3
- 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/commands/hooks.js
CHANGED
|
@@ -30,14 +30,15 @@ const {
|
|
|
30
30
|
const fs = require('node:fs');
|
|
31
31
|
const path = require('node:path');
|
|
32
32
|
const { sessionStartCapability, userPromptSubmitCapability, sessionEndCapability } = require('../hook-renderer');
|
|
33
|
-
const {
|
|
33
|
+
const { buildMemoryDigest, defaultFetchIssues, defaultFetchNotes } = require('../memory-digest');
|
|
34
34
|
const { collectInbox, buildInboxNudge } = require('../inbox');
|
|
35
35
|
const { collectDigest } = require('../pr-monitor/digest');
|
|
36
36
|
const { loadDispatchText } = require('../using-forge');
|
|
37
37
|
const projectMemory = require('../project-memory');
|
|
38
|
-
const { parseHookInput, selectInjection, DEFAULT_TOKEN_BUDGET, DEFAULT_SCORE_FLOOR } = require('../memory-recall');
|
|
39
|
-
const {
|
|
38
|
+
const { parseHookInput, selectInjection, meaningfulTokens, DEFAULT_TOKEN_BUDGET, DEFAULT_SCORE_FLOOR } = require('../memory-recall');
|
|
39
|
+
const { launchMemoryRecallEvent } = require('../memory-recall-events');
|
|
40
40
|
const { getResolvedRuntimeGraph } = require('../core/runtime-graph');
|
|
41
|
+
const { fireAndForget } = require('../pr-monitor/reconcile-executor');
|
|
41
42
|
|
|
42
43
|
// Default-ON rail; `forge gate disable rail.memory_recall` turns tier-2 off. Mirrors
|
|
43
44
|
// autoShepherdRailEnabled (lib/commands/ship.js): absent id = enabled, fail-open to true.
|
|
@@ -47,6 +48,42 @@ const MEMORY_RECALL_RAIL = 'rail.memory_recall';
|
|
|
47
48
|
const MEMORY_RECALL_CANDIDATES = 25;
|
|
48
49
|
// Cross-turn dedupe memory: how many recently-injected keys to remember per session.
|
|
49
50
|
const SEEN_KEYS_CAP = 40;
|
|
51
|
+
const SESSION_START_DEADLINE_MS = 9_000;
|
|
52
|
+
const PROMPT_RECALL_DEADLINE_MS = 4_500;
|
|
53
|
+
const PROMPT_RECALL_SQLITE_BUSY_MS = 1_000;
|
|
54
|
+
|
|
55
|
+
function withinDeadline(work, fallback, deadlineMs, onTimeout) {
|
|
56
|
+
return new Promise((resolve) => {
|
|
57
|
+
const timer = setTimeout(() => {
|
|
58
|
+
if (onTimeout) onTimeout();
|
|
59
|
+
resolve(fallback);
|
|
60
|
+
}, deadlineMs);
|
|
61
|
+
Promise.resolve().then(work).then(
|
|
62
|
+
value => {
|
|
63
|
+
clearTimeout(timer);
|
|
64
|
+
resolve(value);
|
|
65
|
+
},
|
|
66
|
+
() => {
|
|
67
|
+
clearTimeout(timer);
|
|
68
|
+
resolve(fallback);
|
|
69
|
+
},
|
|
70
|
+
);
|
|
71
|
+
});
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
async function collectSessionStartData(projectRoot, opts, deadlineMs) {
|
|
75
|
+
const fetchNotes = opts.fetchNotes || defaultFetchNotes;
|
|
76
|
+
const fetchIssues = opts.fetchIssues || defaultFetchIssues;
|
|
77
|
+
const fetchInbox = opts.fetchInbox || collectInbox;
|
|
78
|
+
const bounded = work => withinDeadline(work, [], deadlineMs);
|
|
79
|
+
const [notes, ready, claimed, inbox] = await Promise.all([
|
|
80
|
+
bounded(() => fetchNotes(projectRoot, opts)),
|
|
81
|
+
bounded(() => fetchIssues(projectRoot, 'ready', opts)),
|
|
82
|
+
bounded(() => fetchIssues(projectRoot, 'in_progress', opts)),
|
|
83
|
+
bounded(() => fetchInbox(projectRoot, opts)),
|
|
84
|
+
]);
|
|
85
|
+
return { notes, ready, claimed, inbox };
|
|
86
|
+
}
|
|
50
87
|
|
|
51
88
|
function memoryRecallRailEnabled(projectRoot, resolveGraph = getResolvedRuntimeGraph) {
|
|
52
89
|
try {
|
|
@@ -96,6 +133,39 @@ function saveSeenKeys(projectRoot, sessionId, keys) {
|
|
|
96
133
|
}
|
|
97
134
|
}
|
|
98
135
|
|
|
136
|
+
// Shadow-log the tier-2 recall decision (kernel issue f71784d3 step-0 instrument): one JSON
|
|
137
|
+
// line per run records what was retrieved and what cleared the floor, so the corpus-dependent
|
|
138
|
+
// scoreFloor can be tuned from real data instead of guessed. Capped so it never grows unbounded.
|
|
139
|
+
const SHADOW_LOG_MAX_BYTES = 512 * 1024;
|
|
140
|
+
// A shadow record is a tuning sample, not an archive. It contains only aggregate counts/scores.
|
|
141
|
+
|
|
142
|
+
function shadowLogPath(projectRoot) {
|
|
143
|
+
return path.join(projectRoot, '.forge', 'memory-recall', 'shadow.jsonl');
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Append one JSON line, then evict whole records oldest-first until the file fits the byte cap.
|
|
147
|
+
// Trimming by bytes (not line count) is what keeps this hook's synchronous read/write bounded: a
|
|
148
|
+
// record that exceeds the cap alone leaves the file empty rather than permanently oversized.
|
|
149
|
+
// Best-effort — any failure is swallowed by the caller so logging never affects the hook result.
|
|
150
|
+
function appendShadowLog(projectRoot, record) {
|
|
151
|
+
const file = shadowLogPath(projectRoot);
|
|
152
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
153
|
+
fs.appendFileSync(file, `${JSON.stringify(record)}\n`, 'utf8');
|
|
154
|
+
let size;
|
|
155
|
+
try { size = fs.statSync(file).size; } catch { size = 0; }
|
|
156
|
+
if (size <= SHADOW_LOG_MAX_BYTES) return;
|
|
157
|
+
const lines = fs.readFileSync(file, 'utf8').split('\n').filter(Boolean);
|
|
158
|
+
const keep = [];
|
|
159
|
+
let bytes = 0;
|
|
160
|
+
for (let i = lines.length - 1; i >= 0; i -= 1) {
|
|
161
|
+
const cost = Buffer.byteLength(`${lines[i]}\n`, 'utf8');
|
|
162
|
+
if (bytes + cost > SHADOW_LOG_MAX_BYTES) break;
|
|
163
|
+
bytes += cost;
|
|
164
|
+
keep.unshift(lines[i]);
|
|
165
|
+
}
|
|
166
|
+
fs.writeFileSync(file, keep.length ? `${keep.join('\n')}\n` : '', 'utf8');
|
|
167
|
+
}
|
|
168
|
+
|
|
99
169
|
function usage() {
|
|
100
170
|
return 'Usage: forge hooks install --global [--harness codex|hermes|all] [--dry-run]\n'
|
|
101
171
|
+ ' forge hooks session-start --harness <claude> (machine-facing; emits SessionStart context)\n'
|
|
@@ -176,16 +246,19 @@ function formatSessionStart(harness, text) {
|
|
|
176
246
|
*
|
|
177
247
|
* @param {string[]} rest - args after the `session-start` action.
|
|
178
248
|
* @param {string} projectRoot
|
|
179
|
-
* @param {object} [opts] - injectable digest fetchers ({ fetchNotes, fetchIssues })
|
|
249
|
+
* @param {object} [opts] - injectable digest fetchers ({ fetchNotes, fetchIssues }) and an
|
|
250
|
+
* optional host `harness` capability ({ hasBgShell: true, runBgShell(argv, { cwd }) }).
|
|
180
251
|
* @returns {Promise<{ success: boolean, output: string }>}
|
|
181
252
|
*/
|
|
182
253
|
async function handleSessionStart(rest, projectRoot, opts = {}) {
|
|
183
254
|
const harness = parseHarness(rest);
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
255
|
+
const capability = sessionStartCapability(harness);
|
|
256
|
+
if (!capability.rendered) return { success: true, output: '', reason: capability.reason };
|
|
257
|
+
try {
|
|
258
|
+
const triggerContext = { projectRoot };
|
|
259
|
+
if (opts.harness !== undefined) triggerContext.harness = opts.harness;
|
|
260
|
+
(opts.fireAndForget || fireAndForget)(triggerContext);
|
|
261
|
+
} catch { /* the automatic trigger must never break session start */ }
|
|
189
262
|
|
|
190
263
|
// The using-forge dispatch bootstrap is injected FIRST so Forge skills auto-trigger from turn
|
|
191
264
|
// one (the Superpowers mechanism): a reasoning-driven system, not just harness description
|
|
@@ -201,7 +274,11 @@ async function handleSessionStart(rest, projectRoot, opts = {}) {
|
|
|
201
274
|
|
|
202
275
|
let digestText = '';
|
|
203
276
|
try {
|
|
204
|
-
const data = await
|
|
277
|
+
const data = await collectSessionStartData(
|
|
278
|
+
projectRoot,
|
|
279
|
+
opts,
|
|
280
|
+
opts.sessionStartDeadlineMs || SESSION_START_DEADLINE_MS,
|
|
281
|
+
);
|
|
205
282
|
const digest = buildMemoryDigest(data, opts);
|
|
206
283
|
if (!digest.empty) digestText = digest.text;
|
|
207
284
|
} catch { /* fail-open: a kernel outage must not suppress the dispatch bootstrap */ }
|
|
@@ -243,7 +320,8 @@ function formatUserPromptSubmit(harness, text) {
|
|
|
243
320
|
async function handleInboxPickup(rest, projectRoot, opts = {}) {
|
|
244
321
|
try {
|
|
245
322
|
const harness = parseHarness(rest);
|
|
246
|
-
|
|
323
|
+
const capability = userPromptSubmitCapability(harness);
|
|
324
|
+
if (!capability.rendered) return { success: true, output: '', reason: capability.reason };
|
|
247
325
|
const pending = await collectInbox(projectRoot, opts);
|
|
248
326
|
const nudge = buildInboxNudge(pending);
|
|
249
327
|
if (nudge.empty) return { success: true, output: '' };
|
|
@@ -314,43 +392,161 @@ function handleShepherdEvents(rest, projectRoot, opts = {}) {
|
|
|
314
392
|
* loadSeen, saveSeen, scoreFloor, tokenBudget }.
|
|
315
393
|
* @returns {{ success: boolean, output: string }}
|
|
316
394
|
*/
|
|
317
|
-
function
|
|
395
|
+
async function runMemoryRecall(rest, projectRoot, opts = {}) {
|
|
396
|
+
const observation = opts.observation || {};
|
|
318
397
|
try {
|
|
319
398
|
const harness = parseHarness(rest);
|
|
320
|
-
|
|
399
|
+
observation.harness = harness;
|
|
400
|
+
const capability = userPromptSubmitCapability(harness);
|
|
401
|
+
if (!capability.rendered) {
|
|
402
|
+
observation.outcome = 'unsupported';
|
|
403
|
+
return { success: true, output: '', reason: capability.reason };
|
|
404
|
+
}
|
|
321
405
|
|
|
322
406
|
const railEnabled = opts.railEnabled || memoryRecallRailEnabled;
|
|
323
|
-
if (!railEnabled(projectRoot))
|
|
407
|
+
if (!railEnabled(projectRoot)) {
|
|
408
|
+
observation.outcome = 'filtered';
|
|
409
|
+
return { success: true, output: '' };
|
|
410
|
+
}
|
|
324
411
|
|
|
325
412
|
const readInput = opts.readInput || readHookStdin;
|
|
326
413
|
const { prompt, sessionId } = parseHookInput(readInput());
|
|
327
|
-
if (!prompt)
|
|
414
|
+
if (!prompt) {
|
|
415
|
+
observation.outcome = 'filtered';
|
|
416
|
+
return { success: true, output: '' };
|
|
417
|
+
}
|
|
328
418
|
|
|
329
419
|
const search = opts.search
|
|
330
|
-
|| ((root, query, limit) => projectMemory.searchRankedScored(root, query, limit));
|
|
420
|
+
|| ((root, query, limit, options) => projectMemory.searchRankedScored(root, query, limit, options));
|
|
331
421
|
const loadSeen = opts.loadSeen || loadSeenKeys;
|
|
332
422
|
const saveSeen = opts.saveSeen || saveSeenKeys;
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
423
|
+
const appendShadow = opts.appendShadow || appendShadowLog;
|
|
424
|
+
|
|
425
|
+
// Stopword-aware, unicode-safe tokenization lives HERE (single source); the driver's
|
|
426
|
+
// buildMemoryFtsMatchOr only quotes+ORs whatever tokens it receives. Passing the raw prompt
|
|
427
|
+
// to a token-AND match was the 0-recall bug (kernel issue f71784d3).
|
|
428
|
+
const tokens = meaningfulTokens(prompt);
|
|
429
|
+
const excludeKeys = await loadSeen(projectRoot, sessionId) || [];
|
|
430
|
+
const hits = await search(
|
|
431
|
+
projectRoot,
|
|
432
|
+
tokens.join(' '),
|
|
433
|
+
MEMORY_RECALL_CANDIDATES,
|
|
434
|
+
{
|
|
435
|
+
excludeKeys: excludeKeys.slice(0, 256),
|
|
436
|
+
busyTimeoutMs: Math.max(0, Math.min(
|
|
437
|
+
opts.promptRecallSqliteBusyMs ?? PROMPT_RECALL_SQLITE_BUSY_MS,
|
|
438
|
+
(opts.promptRecallDeadlineMs || PROMPT_RECALL_DEADLINE_MS) - 100,
|
|
439
|
+
)),
|
|
440
|
+
},
|
|
441
|
+
) || [];
|
|
442
|
+
observation.candidateCount = hits.length;
|
|
443
|
+
observation.eligibleCount = hits.length;
|
|
444
|
+
if (opts.deadline?.expired) {
|
|
445
|
+
observation.outcome = 'timeout';
|
|
446
|
+
return { success: true, output: '', reason: 'timeout' };
|
|
447
|
+
}
|
|
448
|
+
const scoreFloor = typeof opts.scoreFloor === 'number' ? opts.scoreFloor : DEFAULT_SCORE_FLOOR;
|
|
449
|
+
const { lines, entries, injectedKeys } = selectInjection({
|
|
337
450
|
query: prompt,
|
|
338
451
|
hits,
|
|
339
|
-
scoreFloor
|
|
452
|
+
scoreFloor,
|
|
340
453
|
tokenBudget: opts.tokenBudget || DEFAULT_TOKEN_BUDGET,
|
|
341
454
|
excludeKeys,
|
|
342
455
|
});
|
|
343
|
-
if (!lines.length) return { success: true, output: '' };
|
|
344
456
|
|
|
345
|
-
|
|
346
|
-
|
|
457
|
+
// Step-0 instrument: record every real query (even ones that inject nothing) so the floor is
|
|
458
|
+
// tuned from data. Own try/catch — a logging failure must NEVER fall through to the fail-open
|
|
459
|
+
// outer catch (which would suppress a legitimate injection).
|
|
460
|
+
if (tokens.length) {
|
|
461
|
+
try {
|
|
462
|
+
appendShadow(projectRoot, {
|
|
463
|
+
candidateCount: hits.length,
|
|
464
|
+
injectedCount: injectedKeys.length,
|
|
465
|
+
candidateScoreMin: hits.length ? Math.min(...hits.map(hit => Number(hit?.score) || 0)) : null,
|
|
466
|
+
candidateScoreMax: hits.length ? Math.max(...hits.map(hit => Number(hit?.score) || 0)) : null,
|
|
467
|
+
scoreFloor,
|
|
468
|
+
});
|
|
469
|
+
} catch { /* best-effort: shadow logging never affects the hook result */ }
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
if (!lines.length) {
|
|
473
|
+
observation.outcome = hits.length ? 'filtered' : 'empty';
|
|
474
|
+
return { success: true, output: '' };
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
if (opts.deadline?.expired) return { success: true, output: '', reason: 'timeout' };
|
|
478
|
+
try {
|
|
479
|
+
const pending = saveSeen(projectRoot, sessionId, injectedKeys);
|
|
480
|
+
if (pending && typeof pending.catch === 'function') pending.catch(() => {});
|
|
481
|
+
} catch { /* dedupe persistence never suppresses an injection */ }
|
|
482
|
+
const sections = [
|
|
483
|
+
['confirmed', 'Confirmed memory (project-local; provenance shown)'],
|
|
484
|
+
['suggested', 'Suggested memory — verify before relying'],
|
|
485
|
+
].map(([trust, heading]) => {
|
|
486
|
+
const selectedLines = entries.filter(entry => entry.trust === trust).map(entry => entry.line);
|
|
487
|
+
return selectedLines.length ? `${heading}\n${selectedLines.join('\n')}` : '';
|
|
488
|
+
}).filter(Boolean);
|
|
489
|
+
const fenced = sections.join('\n');
|
|
490
|
+
const selected = hits.filter(hit => injectedKeys.includes(hit.memory_id || hit.key));
|
|
491
|
+
observation.outcome = 'selected';
|
|
492
|
+
observation.selectedIds = injectedKeys;
|
|
493
|
+
observation.sourceMix = selected.reduce((mix, hit) => {
|
|
494
|
+
const sourceAgent = hit.provenance?.source_agent || hit.sourceAgent || 'unknown';
|
|
495
|
+
mix[sourceAgent] = (mix[sourceAgent] || 0) + 1;
|
|
496
|
+
return mix;
|
|
497
|
+
}, {});
|
|
498
|
+
observation.trustMix = selected.reduce((mix, hit) => {
|
|
499
|
+
const trust = hit.trust_status || 'unknown';
|
|
500
|
+
mix[trust] = (mix[trust] || 0) + 1;
|
|
501
|
+
return mix;
|
|
502
|
+
}, {});
|
|
503
|
+
observation.tokenEstimate = Math.ceil(fenced.length / 4);
|
|
347
504
|
return { success: true, output: formatUserPromptSubmit(harness, fenced) };
|
|
348
505
|
} catch {
|
|
349
506
|
// Fail-open: a context hook must never break a prompt.
|
|
507
|
+
observation.outcome = 'error';
|
|
350
508
|
return { success: true, output: '' };
|
|
351
509
|
}
|
|
352
510
|
}
|
|
353
511
|
|
|
512
|
+
async function handleMemoryRecall(rest, projectRoot, opts = {}) {
|
|
513
|
+
const harness = parseHarness(rest);
|
|
514
|
+
const startedAt = Date.now();
|
|
515
|
+
const observation = { harness };
|
|
516
|
+
const record = result => {
|
|
517
|
+
const outcome = result.reason === 'timeout'
|
|
518
|
+
? 'timeout'
|
|
519
|
+
: (observation.outcome || (result.output ? 'selected' : 'empty'));
|
|
520
|
+
const eventObservation = {
|
|
521
|
+
...observation,
|
|
522
|
+
outcome,
|
|
523
|
+
harness,
|
|
524
|
+
elapsedMs: Date.now() - startedAt,
|
|
525
|
+
};
|
|
526
|
+
try {
|
|
527
|
+
const pending = (opts.recordRecallEvent || opts.launchRecallEvent || launchMemoryRecallEvent)(
|
|
528
|
+
projectRoot,
|
|
529
|
+
eventObservation,
|
|
530
|
+
);
|
|
531
|
+
if (pending && typeof pending.catch === 'function') pending.catch(() => {});
|
|
532
|
+
} catch { /* Operational evidence is best-effort and must never delay or suppress a prompt. */ }
|
|
533
|
+
return result;
|
|
534
|
+
};
|
|
535
|
+
const capability = userPromptSubmitCapability(harness);
|
|
536
|
+
if (!capability.rendered) {
|
|
537
|
+
observation.outcome = 'unsupported';
|
|
538
|
+
return record({ success: true, output: '', reason: capability.reason });
|
|
539
|
+
}
|
|
540
|
+
const deadline = { expired: false };
|
|
541
|
+
const result = await withinDeadline(
|
|
542
|
+
() => runMemoryRecall(rest, projectRoot, { ...opts, deadline, observation }),
|
|
543
|
+
{ success: true, output: '', reason: 'timeout' },
|
|
544
|
+
opts.promptRecallDeadlineMs || PROMPT_RECALL_DEADLINE_MS,
|
|
545
|
+
() => { deadline.expired = true; },
|
|
546
|
+
);
|
|
547
|
+
return record(result);
|
|
548
|
+
}
|
|
549
|
+
|
|
354
550
|
/**
|
|
355
551
|
* `forge hooks capture --harness <h> --trigger <precompact|stop>` — the CAPTURE-on-exit hook.
|
|
356
552
|
* PreCompact (before context compaction) and Stop (turn end) fire it; it snapshots a bounded
|
|
@@ -374,7 +570,8 @@ function handleMemoryRecall(rest, projectRoot, opts = {}) {
|
|
|
374
570
|
async function handleCapture(rest, projectRoot, opts = {}) {
|
|
375
571
|
try {
|
|
376
572
|
const harness = parseHarness(rest);
|
|
377
|
-
|
|
573
|
+
const capability = sessionEndCapability(harness);
|
|
574
|
+
if (!capability.rendered) return { success: true, output: '', reason: capability.reason };
|
|
378
575
|
const trigger = parseTrigger(rest);
|
|
379
576
|
|
|
380
577
|
const fetchIssues = opts.fetchIssues || defaultFetchIssues;
|
|
@@ -521,4 +718,5 @@ module.exports = {
|
|
|
521
718
|
'--dry-run': 'Preview the merge without writing anything',
|
|
522
719
|
},
|
|
523
720
|
handler,
|
|
721
|
+
_internal: { appendShadowLog, SHADOW_LOG_MAX_BYTES },
|
|
524
722
|
};
|
package/lib/commands/insights.js
CHANGED
|
@@ -36,7 +36,7 @@ function positionals(args) {
|
|
|
36
36
|
return values;
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
-
async function handler(args, flags, projectRoot) {
|
|
39
|
+
async function handler(args, flags, projectRoot, opts = {}) {
|
|
40
40
|
const commandFlags = flags ?? {};
|
|
41
41
|
const [subcommand, candidateId] = positionals(args);
|
|
42
42
|
if (subcommand === 'accept' || subcommand === 'reject') {
|
|
@@ -54,14 +54,19 @@ async function handler(args, flags, projectRoot) {
|
|
|
54
54
|
};
|
|
55
55
|
}
|
|
56
56
|
|
|
57
|
-
const result = analyzeInsights(projectRoot, {
|
|
57
|
+
const result = await analyzeInsights(projectRoot, {
|
|
58
58
|
limit: readOption(args, '--limit', undefined),
|
|
59
59
|
minCount: readOption(args, '--min-count', undefined),
|
|
60
60
|
since: readOption(args, '--since', undefined),
|
|
61
|
+
// Injectable kernel-read seams (Slice C2); undefined falls back to the real
|
|
62
|
+
// cli-broker-factory-backed reads inside analyzeInsights.
|
|
63
|
+
runIssueOperation: opts.runIssueOperation,
|
|
64
|
+
listRecentEvents: opts.listRecentEvents,
|
|
65
|
+
env: opts.env,
|
|
61
66
|
});
|
|
62
67
|
if (args.includes('--review-feedback')) {
|
|
63
68
|
result.limitations = [
|
|
64
|
-
'Compatibility note: --review-feedback now reads
|
|
69
|
+
'Compatibility note: --review-feedback now reads kernel events and issue evidence; external review-provider comments are not inferred.',
|
|
65
70
|
...result.limitations,
|
|
66
71
|
];
|
|
67
72
|
}
|