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/memory/router.js
CHANGED
|
@@ -242,6 +242,21 @@ function legacyJsonlPath(projectRoot) {
|
|
|
242
242
|
return path.join(projectRoot, ...LEGACY_JSONL_RELATIVE);
|
|
243
243
|
}
|
|
244
244
|
|
|
245
|
+
function legacyTags(parsed) {
|
|
246
|
+
const tags = Array.isArray(parsed.tags)
|
|
247
|
+
? parsed.tags.filter(tag => typeof tag === 'string')
|
|
248
|
+
: [];
|
|
249
|
+
if (
|
|
250
|
+
typeof parsed.type === 'string'
|
|
251
|
+
&& /^[a-z0-9][a-z0-9-]{0,63}$/i.test(parsed.type)
|
|
252
|
+
&& !tags.some(tag => tag.startsWith('type:'))
|
|
253
|
+
) {
|
|
254
|
+
tags.push(`type:${parsed.type.toLowerCase()}`);
|
|
255
|
+
}
|
|
256
|
+
if (!tags.some(tag => tag.startsWith('trust:'))) tags.push('trust:suggested');
|
|
257
|
+
return tags;
|
|
258
|
+
}
|
|
259
|
+
|
|
245
260
|
// A STABLE key for a legacy record that lacks an `id`, derived from its content — so a
|
|
246
261
|
// re-run (or a failed rename) upserts the same row instead of double-inserting under a
|
|
247
262
|
// fresh random UUID.
|
|
@@ -292,7 +307,7 @@ function migrateJsonlNotesOnce(projectRoot, options = {}) {
|
|
|
292
307
|
key: typeof parsed.id === 'string' && parsed.id ? parsed.id : legacyContentKey(parsed),
|
|
293
308
|
value: parsed.note,
|
|
294
309
|
sourceAgent: IMPORT_SOURCE_AGENT,
|
|
295
|
-
tags:
|
|
310
|
+
tags: legacyTags(parsed),
|
|
296
311
|
};
|
|
297
312
|
if (typeof parsed.timestamp === 'string' && parsed.timestamp && !Number.isNaN(Date.parse(parsed.timestamp))) {
|
|
298
313
|
entry.timestamp = parsed.timestamp;
|
package/lib/memory-digest.js
CHANGED
|
@@ -22,6 +22,7 @@
|
|
|
22
22
|
const { applyBudget, buildSection, estimateTokens } = require('./orientation');
|
|
23
23
|
const { fenceUntrusted } = require('./untrusted-content');
|
|
24
24
|
const { collectInbox, inboxSection } = require('./inbox');
|
|
25
|
+
const { memoryTrustStatus } = require('./memory-recall');
|
|
25
26
|
|
|
26
27
|
const DEFAULT_DIGEST_BUDGET_TOKENS = 400;
|
|
27
28
|
const DEFAULT_NOTE_LIMIT = 5;
|
|
@@ -103,7 +104,13 @@ async function collectDigestData(projectRoot, opts = {}) {
|
|
|
103
104
|
/** `- [date ]note` for a recall note. */
|
|
104
105
|
function formatNoteLine(note) {
|
|
105
106
|
const date = typeof note.timestamp === 'string' && note.timestamp ? `${note.timestamp.slice(0, 10)} ` : '';
|
|
106
|
-
|
|
107
|
+
const trust = memoryTrustStatus({
|
|
108
|
+
tags: note.tags,
|
|
109
|
+
sourceAgent: note.sourceAgent,
|
|
110
|
+
value: note.machine ? {} : note.note,
|
|
111
|
+
});
|
|
112
|
+
const sourceAgent = note.sourceAgent || 'unknown';
|
|
113
|
+
return `- [source=${sourceAgent} trust=${trust} updated=${date.trim() || 'unknown'}] ${note.note}`;
|
|
107
114
|
}
|
|
108
115
|
|
|
109
116
|
/** `- [label] title` for an issue row (title/id defensively resolved). */
|
|
@@ -112,18 +119,39 @@ function formatIssueLine(label, issue) {
|
|
|
112
119
|
return `- [${label}] ${title}`;
|
|
113
120
|
}
|
|
114
121
|
|
|
115
|
-
/** Build
|
|
116
|
-
function
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
122
|
+
/** Build separate confirmed/suggested note sections, skipping entries too large to fit. */
|
|
123
|
+
function notesSections(notes, budgetTokens) {
|
|
124
|
+
const eligible = notes
|
|
125
|
+
.map(note => ({ note, line: formatNoteLine(note) }))
|
|
126
|
+
.filter(entry => estimateTokens(entry.line) <= budgetTokens);
|
|
127
|
+
const groups = [
|
|
128
|
+
{ id: 'digest_notes', title: 'Confirmed memory', trust: 'confirmed', priority: 10 },
|
|
129
|
+
{
|
|
130
|
+
id: 'digest_suggested_memory',
|
|
131
|
+
title: 'Suggested memory — verify before relying',
|
|
132
|
+
trust: 'suggested',
|
|
133
|
+
priority: 11,
|
|
134
|
+
},
|
|
135
|
+
];
|
|
136
|
+
return groups.map(group => {
|
|
137
|
+
const content = eligible
|
|
138
|
+
.filter(({ note }) => memoryTrustStatus({
|
|
139
|
+
tags: note.tags,
|
|
140
|
+
sourceAgent: note.sourceAgent,
|
|
141
|
+
value: note.machine ? {} : note.note,
|
|
142
|
+
}) === group.trust)
|
|
143
|
+
.map(entry => entry.line)
|
|
144
|
+
.join('\n');
|
|
145
|
+
if (!content) return null;
|
|
146
|
+
return buildSection({
|
|
147
|
+
id: group.id,
|
|
148
|
+
title: group.title,
|
|
149
|
+
content,
|
|
150
|
+
priority: group.priority,
|
|
151
|
+
preserve: false,
|
|
152
|
+
untrustedSource: 'memory',
|
|
153
|
+
});
|
|
154
|
+
}).filter(Boolean);
|
|
127
155
|
}
|
|
128
156
|
|
|
129
157
|
/**
|
|
@@ -161,13 +189,17 @@ function buildMemoryDigest(data = {}, options = {}) {
|
|
|
161
189
|
const ready = Array.isArray(data.ready) ? data.ready : [];
|
|
162
190
|
const claimed = Array.isArray(data.claimed) ? data.claimed : [];
|
|
163
191
|
const inbox = Array.isArray(data.inbox) ? data.inbox : [];
|
|
192
|
+
const budgetTokens = options.budgetTokens || DEFAULT_DIGEST_BUDGET_TOKENS;
|
|
164
193
|
|
|
165
194
|
// Inbox (priority 5) is a THIRD section beside notes + issues; a fresh human directive
|
|
166
195
|
// outranks stale notes (10) and the agent's own issue list (20) under budget pressure.
|
|
167
|
-
const sections = [
|
|
196
|
+
const sections = [
|
|
197
|
+
inboxSection(inbox),
|
|
198
|
+
...notesSections(notes, budgetTokens),
|
|
199
|
+
issuesSection(ready, claimed),
|
|
200
|
+
].filter(Boolean);
|
|
168
201
|
if (!sections.length) return { text: '', empty: true, tokens: 0 };
|
|
169
202
|
|
|
170
|
-
const budgetTokens = options.budgetTokens || DEFAULT_DIGEST_BUDGET_TOKENS;
|
|
171
203
|
const budgeted = applyBudget(sections, budgetTokens);
|
|
172
204
|
const body = budgeted.sections
|
|
173
205
|
.filter(section => section.content)
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { randomUUID } = require('node:crypto');
|
|
4
|
+
const { spawn } = require('node:child_process');
|
|
5
|
+
|
|
6
|
+
const { resolveKernelDatabasePath } = require('./kernel/cli-broker-factory');
|
|
7
|
+
const { createBuiltinSQLiteDriver } = require('./kernel/sqlite-driver');
|
|
8
|
+
const projectMemory = require('./project-memory');
|
|
9
|
+
|
|
10
|
+
const EVENT_TYPE = 'memory.recall.observed';
|
|
11
|
+
const OUTCOMES = new Set(['selected', 'empty', 'filtered', 'unsupported', 'timeout', 'error']);
|
|
12
|
+
const MAX_COUNT = 1_000_000;
|
|
13
|
+
const MAX_SELECTED_IDS = 20;
|
|
14
|
+
const MAX_MIX_KEYS = 10;
|
|
15
|
+
const EVENT_WRITER = "const { recordMemoryRecallPayload } = require(process.argv[1]); recordMemoryRecallPayload(process.argv[2], JSON.parse(process.argv[3])).catch(() => {});";
|
|
16
|
+
|
|
17
|
+
function boundedInteger(value) {
|
|
18
|
+
if (!Number.isFinite(value)) return 0;
|
|
19
|
+
return Math.min(MAX_COUNT, Math.max(0, Math.trunc(value)));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function boundedString(value, maxLength) {
|
|
23
|
+
return String(value ?? '').slice(0, maxLength);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function boundedMix(mix) {
|
|
27
|
+
if (!mix || typeof mix !== 'object' || Array.isArray(mix)) return {};
|
|
28
|
+
return Object.fromEntries(
|
|
29
|
+
Object.entries(mix)
|
|
30
|
+
.slice(0, MAX_MIX_KEYS)
|
|
31
|
+
.map(([label, count]) => [boundedString(label, 64), boundedInteger(count)])
|
|
32
|
+
.filter(([label, count]) => label && count > 0)
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function buildRecallEventPayload(observation = {}) {
|
|
37
|
+
const selectedIds = Array.isArray(observation.selectedIds)
|
|
38
|
+
? observation.selectedIds
|
|
39
|
+
.slice(0, MAX_SELECTED_IDS)
|
|
40
|
+
.map(id => boundedString(id, 128))
|
|
41
|
+
.filter(Boolean)
|
|
42
|
+
: [];
|
|
43
|
+
return {
|
|
44
|
+
outcome: OUTCOMES.has(observation.outcome) ? observation.outcome : 'error',
|
|
45
|
+
counts: {
|
|
46
|
+
candidates: boundedInteger(observation.candidateCount),
|
|
47
|
+
eligible: boundedInteger(observation.eligibleCount),
|
|
48
|
+
selected: selectedIds.length,
|
|
49
|
+
},
|
|
50
|
+
selected_ids: selectedIds,
|
|
51
|
+
source_mix: boundedMix(observation.sourceMix),
|
|
52
|
+
trust_mix: boundedMix(observation.trustMix),
|
|
53
|
+
token_estimate: boundedInteger(observation.tokenEstimate),
|
|
54
|
+
elapsed_ms: boundedInteger(observation.elapsedMs),
|
|
55
|
+
harness: boundedString(observation.harness || 'unknown', 32),
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function normalizeRecallEventPayload(payload = {}) {
|
|
60
|
+
return buildRecallEventPayload({
|
|
61
|
+
outcome: payload.outcome,
|
|
62
|
+
candidateCount: payload.counts?.candidates,
|
|
63
|
+
eligibleCount: payload.counts?.eligible,
|
|
64
|
+
selectedIds: payload.selected_ids,
|
|
65
|
+
sourceMix: payload.source_mix,
|
|
66
|
+
trustMix: payload.trust_mix,
|
|
67
|
+
tokenEstimate: payload.token_estimate,
|
|
68
|
+
elapsedMs: payload.elapsed_ms,
|
|
69
|
+
harness: payload.harness,
|
|
70
|
+
});
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async function recordMemoryRecallEvent(projectRoot, observation = {}, options = {}) {
|
|
74
|
+
return recordMemoryRecallPayload(projectRoot, buildRecallEventPayload(observation), options);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
async function recordMemoryRecallPayload(projectRoot, payload, options = {}) {
|
|
78
|
+
let store = options.store;
|
|
79
|
+
let ownsStore = false;
|
|
80
|
+
try {
|
|
81
|
+
const safePayload = normalizeRecallEventPayload(payload);
|
|
82
|
+
const projectId = options.projectId || projectMemory.resolveProjectId(projectRoot, options);
|
|
83
|
+
if (!store) {
|
|
84
|
+
store = createBuiltinSQLiteDriver({
|
|
85
|
+
databasePath: resolveKernelDatabasePath({
|
|
86
|
+
projectRoot,
|
|
87
|
+
gitCommonDir: options.gitCommonDir,
|
|
88
|
+
databasePath: options.databasePath,
|
|
89
|
+
}),
|
|
90
|
+
});
|
|
91
|
+
ownsStore = true;
|
|
92
|
+
}
|
|
93
|
+
const id = (options.randomUUID || randomUUID)();
|
|
94
|
+
const createdAt = options.now || new Date().toISOString();
|
|
95
|
+
await store.insertKernelEvent({
|
|
96
|
+
id,
|
|
97
|
+
entity_type: 'project',
|
|
98
|
+
entity_id: projectId,
|
|
99
|
+
event_type: EVENT_TYPE,
|
|
100
|
+
idempotency_key: id,
|
|
101
|
+
expected_revision: 0,
|
|
102
|
+
actor: 'forge',
|
|
103
|
+
origin: 'hook',
|
|
104
|
+
payload: safePayload,
|
|
105
|
+
created_at: createdAt,
|
|
106
|
+
});
|
|
107
|
+
return { recorded: true, eventId: id };
|
|
108
|
+
} catch (error) {
|
|
109
|
+
return {
|
|
110
|
+
recorded: false,
|
|
111
|
+
reason: error && error.message ? error.message : 'telemetry unavailable',
|
|
112
|
+
};
|
|
113
|
+
} finally {
|
|
114
|
+
if (ownsStore && store && typeof store.close === 'function') {
|
|
115
|
+
try {
|
|
116
|
+
store.close();
|
|
117
|
+
} catch {
|
|
118
|
+
// Best-effort evidence must never block recall.
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function launchMemoryRecallEvent(projectRoot, observation = {}, options = {}) {
|
|
125
|
+
try {
|
|
126
|
+
const child = (options.spawn || spawn)(
|
|
127
|
+
process.execPath,
|
|
128
|
+
['-e', EVENT_WRITER, __filename, projectRoot, JSON.stringify(buildRecallEventPayload(observation))],
|
|
129
|
+
{ detached: true, stdio: 'ignore', windowsHide: true },
|
|
130
|
+
);
|
|
131
|
+
if (child && typeof child.on === 'function') child.on('error', () => {});
|
|
132
|
+
if (child && typeof child.unref === 'function') child.unref();
|
|
133
|
+
return { launched: true };
|
|
134
|
+
} catch {
|
|
135
|
+
return { launched: false };
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
module.exports = {
|
|
140
|
+
EVENT_TYPE,
|
|
141
|
+
buildRecallEventPayload,
|
|
142
|
+
launchMemoryRecallEvent,
|
|
143
|
+
recordMemoryRecallEvent,
|
|
144
|
+
recordMemoryRecallPayload,
|
|
145
|
+
};
|
package/lib/memory-recall.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
const { fenceUntrusted } = require('./untrusted-content');
|
|
4
|
+
|
|
3
5
|
/**
|
|
4
6
|
* @module memory-recall
|
|
5
7
|
*
|
|
@@ -50,6 +52,50 @@ function estimateTokens(text) {
|
|
|
50
52
|
return Math.ceil(String(text || '').length / 4);
|
|
51
53
|
}
|
|
52
54
|
|
|
55
|
+
function normalizeMemoryType(entry) {
|
|
56
|
+
const tags = Array.isArray(entry?.tags) ? entry.tags : [];
|
|
57
|
+
const tagged = tags.find(tag => /^type:[a-z0-9][a-z0-9-]{0,63}$/i.test(tag));
|
|
58
|
+
if (tagged) return tagged.slice('type:'.length).toLowerCase();
|
|
59
|
+
const category = entry?.value && typeof entry.value === 'object'
|
|
60
|
+
? entry.value.category
|
|
61
|
+
: null;
|
|
62
|
+
if (typeof category === 'string' && /^[a-z0-9][a-z0-9-]{0,63}$/i.test(category)) {
|
|
63
|
+
return category.toLowerCase();
|
|
64
|
+
}
|
|
65
|
+
return entry?.value && typeof entry.value === 'object' ? 'machine-record' : 'note';
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function memoryTrustStatus(entry) {
|
|
69
|
+
const tags = Array.isArray(entry?.tags) ? entry.tags : [];
|
|
70
|
+
const normalizedTags = tags.map(tag => String(tag).toLowerCase());
|
|
71
|
+
if (normalizedTags.includes('trust:confirmed')) return 'confirmed';
|
|
72
|
+
if (normalizedTags.some(tag => tag.startsWith('trust:')
|
|
73
|
+
|| tag === 'forge:auto-capture')) return 'suggested';
|
|
74
|
+
if (entry?.sourceAgent === 'forge remember (imported)') return 'suggested';
|
|
75
|
+
if (entry?.value && typeof entry.value === 'object') return 'suggested';
|
|
76
|
+
if (entry?.sourceAgent === 'forge remember' && typeof entry.value === 'string') return 'confirmed';
|
|
77
|
+
return 'suggested';
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function normalizeRecallHit(entry, projectId) {
|
|
81
|
+
if (!entry) return null;
|
|
82
|
+
if (entry.memory_id) return entry;
|
|
83
|
+
const sourceRefs = Array.isArray(entry.beadsRefs) ? entry.beadsRefs : [];
|
|
84
|
+
return {
|
|
85
|
+
memory_id: entry.key,
|
|
86
|
+
type: normalizeMemoryType(entry),
|
|
87
|
+
content: typeof entry.value === 'string' ? entry.value : JSON.stringify(entry.value),
|
|
88
|
+
scope: !entry.scope || entry.scope === 'project' ? projectId : entry.scope,
|
|
89
|
+
trust_status: memoryTrustStatus(entry),
|
|
90
|
+
provenance: {
|
|
91
|
+
source_agent: entry.sourceAgent || '',
|
|
92
|
+
source_refs: sourceRefs,
|
|
93
|
+
},
|
|
94
|
+
updated_at: entry.timestamp,
|
|
95
|
+
score: entry.score,
|
|
96
|
+
};
|
|
97
|
+
}
|
|
98
|
+
|
|
53
99
|
/**
|
|
54
100
|
* Parse the JSON payload Claude Code delivers on a UserPromptSubmit hook's stdin. Never
|
|
55
101
|
* throws — any malformed input yields an empty prompt so the hook fails open.
|
|
@@ -99,7 +145,7 @@ function meaningfulTokens(query) {
|
|
|
99
145
|
*
|
|
100
146
|
* @param {object} args
|
|
101
147
|
* @param {string} args.query — the submitted prompt
|
|
102
|
-
* @param {Array<{
|
|
148
|
+
* @param {Array<{memory_id:string, content:string, score:number}>} args.hits — bm25-ordered
|
|
103
149
|
* (best/lowest score first), already relevance-only (token-AND matched)
|
|
104
150
|
* @param {number} [args.scoreFloor] — keep only hits with score <= floor (more negative =
|
|
105
151
|
* stronger). Omit/null to rely on the FTS match alone. The VALUE is corpus-dependent and
|
|
@@ -116,28 +162,40 @@ function selectInjection({ query, hits, scoreFloor = null, tokenBudget = DEFAULT
|
|
|
116
162
|
|
|
117
163
|
const exclude = new Set(excludeKeys || []);
|
|
118
164
|
const lines = [];
|
|
165
|
+
const entries = [];
|
|
119
166
|
const injectedKeys = [];
|
|
167
|
+
const usedSections = new Set();
|
|
120
168
|
let spent = 0;
|
|
121
169
|
|
|
122
170
|
for (const hit of hits || []) {
|
|
123
|
-
|
|
124
|
-
if (
|
|
171
|
+
const memoryId = hit?.memory_id ?? hit?.key;
|
|
172
|
+
if (typeof memoryId !== 'string') continue;
|
|
173
|
+
if (exclude.has(memoryId)) continue;
|
|
125
174
|
// Relevance floor: below the bar contributes nothing. bm25 is more-negative-is-better.
|
|
126
175
|
if (typeof scoreFloor === 'number' && !(typeof hit.score === 'number' && hit.score <= scoreFloor)) {
|
|
127
176
|
continue;
|
|
128
177
|
}
|
|
129
|
-
const body = String(hit.value == null ? '' : hit.value);
|
|
130
|
-
const
|
|
178
|
+
const body = String((hit.content ?? hit.value) == null ? '' : (hit.content ?? hit.value));
|
|
179
|
+
const trust = hit.trust_status === 'confirmed' ? 'confirmed' : 'suggested';
|
|
180
|
+
const source = String(hit.provenance?.source_agent || hit.sourceAgent || 'unknown').slice(0, 80);
|
|
181
|
+
const updated = String(hit.updated_at || hit.timestamp || 'unknown').slice(0, 40);
|
|
182
|
+
const labeled = `[trust=${trust} source=${source} updated=${updated}] ${body}`;
|
|
183
|
+
const line = fenceUntrusted(labeled, { source: 'memory' });
|
|
184
|
+
const heading = trust === 'confirmed'
|
|
185
|
+
? 'Confirmed memory (project-local; provenance shown)'
|
|
186
|
+
: 'Suggested memory — verify before relying';
|
|
187
|
+
const cost = estimateTokens(line) + (usedSections.has(trust) ? 0 : estimateTokens(`${heading}\n`));
|
|
131
188
|
if (spent + cost > tokenBudget) {
|
|
132
|
-
|
|
133
|
-
break;
|
|
189
|
+
continue;
|
|
134
190
|
}
|
|
135
|
-
lines.push(
|
|
136
|
-
|
|
191
|
+
lines.push(line);
|
|
192
|
+
entries.push({ trust, line });
|
|
193
|
+
injectedKeys.push(memoryId);
|
|
194
|
+
usedSections.add(trust);
|
|
137
195
|
spent += cost;
|
|
138
196
|
}
|
|
139
197
|
|
|
140
|
-
return { lines, injectedKeys };
|
|
198
|
+
return { lines, entries, injectedKeys };
|
|
141
199
|
}
|
|
142
200
|
|
|
143
201
|
module.exports = {
|
|
@@ -145,6 +203,9 @@ module.exports = {
|
|
|
145
203
|
DEFAULT_TOKEN_BUDGET,
|
|
146
204
|
DEFAULT_SCORE_FLOOR,
|
|
147
205
|
estimateTokens,
|
|
206
|
+
memoryTrustStatus,
|
|
207
|
+
normalizeMemoryType,
|
|
208
|
+
normalizeRecallHit,
|
|
148
209
|
parseHookInput,
|
|
149
210
|
meaningfulTokens,
|
|
150
211
|
selectInjection,
|
package/lib/merge-rules.js
CHANGED
|
@@ -54,7 +54,7 @@
|
|
|
54
54
|
* (squash | merge | rebase); and post-merge branch deletion.
|
|
55
55
|
*/
|
|
56
56
|
|
|
57
|
-
const SUCCESS_CONCLUSIONS = new Set(['SUCCESS'
|
|
57
|
+
const SUCCESS_CONCLUSIONS = new Set(['SUCCESS']);
|
|
58
58
|
const MS_PER_MIN = 60_000;
|
|
59
59
|
|
|
60
60
|
/** Coerce an epoch-ms number, a Date, or an ISO string to epoch ms (NaN if unparseable). */
|
|
@@ -85,11 +85,15 @@ function toAccountList(arg) {
|
|
|
85
85
|
return [String(arg).trim().toLowerCase()];
|
|
86
86
|
}
|
|
87
87
|
|
|
88
|
-
/** A CI check is green
|
|
88
|
+
/** A CI check is green only when it is terminal and explicitly successful. */
|
|
89
89
|
function isCheckGreen(check) {
|
|
90
90
|
if (!check || typeof check !== 'object') return false;
|
|
91
|
-
|
|
92
|
-
|
|
91
|
+
if (Object.prototype.hasOwnProperty.call(check, 'state')
|
|
92
|
+
&& !Object.prototype.hasOwnProperty.call(check, 'status')) {
|
|
93
|
+
return String(check.state || '').toUpperCase() === 'SUCCESS';
|
|
94
|
+
}
|
|
95
|
+
return String(check.status || '').toUpperCase() === 'COMPLETED'
|
|
96
|
+
&& SUCCESS_CONCLUSIONS.has(String(check.conclusion || '').toUpperCase());
|
|
93
97
|
}
|
|
94
98
|
|
|
95
99
|
/** The most recent comment by timestamp; ties and unparseable stamps fall back to array order. */
|