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/memory/router.js
CHANGED
|
@@ -29,11 +29,16 @@ const crypto = require('node:crypto');
|
|
|
29
29
|
const fs = require('node:fs');
|
|
30
30
|
const path = require('node:path');
|
|
31
31
|
const projectMemory = require('../project-memory');
|
|
32
|
+
const {
|
|
33
|
+
assertBackend,
|
|
34
|
+
createMemoryBackendRegistry,
|
|
35
|
+
} = require('../../packages/memory');
|
|
32
36
|
|
|
33
37
|
/** Supported PUBLIC backends, default first. */
|
|
34
38
|
const MEMORY_BACKENDS = ['local', 'graphiti'];
|
|
35
39
|
const DEFAULT_MEMORY_BACKEND = 'local';
|
|
36
40
|
const ENV_VAR = 'FORGE_MEMORY_BACKEND';
|
|
41
|
+
const registeredMemoryBackends = new Map();
|
|
37
42
|
|
|
38
43
|
/** Default recall cap — newest-N, so `recall` with no query never dumps the whole store. */
|
|
39
44
|
const DEFAULT_RECALL_LIMIT = 20;
|
|
@@ -126,7 +131,7 @@ function resolveMemoryBackend({
|
|
|
126
131
|
if (!value) return DEFAULT_MEMORY_BACKEND;
|
|
127
132
|
|
|
128
133
|
const normalized = value.toLowerCase();
|
|
129
|
-
if (MEMORY_BACKENDS.includes(normalized)) return normalized;
|
|
134
|
+
if (MEMORY_BACKENDS.includes(normalized) || registeredMemoryBackends.has(normalized)) return normalized;
|
|
130
135
|
|
|
131
136
|
warn(
|
|
132
137
|
`Unknown memory backend "${value}" from ${source}; `
|
|
@@ -135,6 +140,22 @@ function resolveMemoryBackend({
|
|
|
135
140
|
return DEFAULT_MEMORY_BACKEND;
|
|
136
141
|
}
|
|
137
142
|
|
|
143
|
+
function registerMemoryBackend(name, adapter) {
|
|
144
|
+
const normalized = typeof name === 'string' ? name.trim().toLowerCase() : '';
|
|
145
|
+
if (!/^[a-z][a-z0-9-]{0,63}$/.test(normalized) || MEMORY_BACKENDS.includes(normalized)) {
|
|
146
|
+
throw new TypeError('custom memory backend must have a unique lowercase name');
|
|
147
|
+
}
|
|
148
|
+
if (registeredMemoryBackends.has(normalized)) {
|
|
149
|
+
throw new Error(`custom memory backend "${normalized}" is already registered`);
|
|
150
|
+
}
|
|
151
|
+
assertBackend(adapter, normalized);
|
|
152
|
+
registeredMemoryBackends.set(normalized, adapter);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function unregisterMemoryBackend(name) {
|
|
156
|
+
registeredMemoryBackends.delete(String(name || '').trim().toLowerCase());
|
|
157
|
+
}
|
|
158
|
+
|
|
138
159
|
/**
|
|
139
160
|
* Strict validation for the resolved backend. Unlike `resolveMemoryBackend`
|
|
140
161
|
* (which soft-falls-back), this THROWS a clear, actionable error when the
|
|
@@ -145,6 +166,17 @@ function resolveMemoryBackend({
|
|
|
145
166
|
*/
|
|
146
167
|
function assertMemoryConfigValid({ deps = {}, env = process.env, projectRoot, config } = {}) {
|
|
147
168
|
const memory = config || readMemoryConfig(projectRoot);
|
|
169
|
+
const signal = collectBackendSignal({ deps, env, projectRoot, config: memory });
|
|
170
|
+
const requested = signal.value?.toLowerCase();
|
|
171
|
+
if (requested && !MEMORY_BACKENDS.includes(requested) && !registeredMemoryBackends.has(requested)) {
|
|
172
|
+
const error = new Error(
|
|
173
|
+
`memory backend "${signal.value}" from ${signal.source} is not registered. `
|
|
174
|
+
+ `Available backends: ${[...MEMORY_BACKENDS, ...registeredMemoryBackends.keys()]
|
|
175
|
+
.sort((left, right) => left.localeCompare(right)).join(', ')}.`,
|
|
176
|
+
);
|
|
177
|
+
error.backend = signal.value;
|
|
178
|
+
throw error;
|
|
179
|
+
}
|
|
148
180
|
const backend = resolveMemoryBackend({ deps, env, projectRoot, config: memory, warn: () => {} });
|
|
149
181
|
|
|
150
182
|
if (backend !== 'graphiti') {
|
|
@@ -182,19 +214,32 @@ function assertMemoryConfigValid({ deps = {}, env = process.env, projectRoot, co
|
|
|
182
214
|
* never-hang guarantee — the emitter, not this seam, owns preventing that.
|
|
183
215
|
*
|
|
184
216
|
* @param {object} [emitter] - optional `{ emit(entry) }` injected by callers.
|
|
185
|
-
* @param {
|
|
217
|
+
* @param {Function} entryProvider - returns the persisted local entry.
|
|
186
218
|
*/
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
}
|
|
219
|
+
// The implementation now lives in the adapter below so synchronous failures can
|
|
220
|
+
// be represented in the operation receipt while rejected promises stay consumed.
|
|
221
|
+
|
|
222
|
+
function graphitiAdapter(emitter, entryProvider) {
|
|
223
|
+
const unavailable = () => [];
|
|
224
|
+
const freeze = (value, seen = new WeakSet()) => {
|
|
225
|
+
if (!value || typeof value !== 'object' || seen.has(value)) return value;
|
|
226
|
+
seen.add(value);
|
|
227
|
+
for (const child of Object.values(value)) freeze(child, seen);
|
|
228
|
+
return Object.freeze(value);
|
|
229
|
+
};
|
|
230
|
+
return {
|
|
231
|
+
add() {
|
|
232
|
+
if (!emitter || typeof emitter.emit !== 'function') return;
|
|
233
|
+
const maybePromise = emitter.emit(freeze(structuredClone(entryProvider())));
|
|
234
|
+
if (maybePromise && typeof maybePromise.then === 'function') {
|
|
235
|
+
maybePromise.then(() => {}, () => {});
|
|
236
|
+
}
|
|
237
|
+
},
|
|
238
|
+
recall: unavailable,
|
|
239
|
+
search: unavailable,
|
|
240
|
+
capture: unavailable,
|
|
241
|
+
digest: unavailable,
|
|
242
|
+
};
|
|
198
243
|
}
|
|
199
244
|
|
|
200
245
|
/**
|
|
@@ -242,6 +287,21 @@ function legacyJsonlPath(projectRoot) {
|
|
|
242
287
|
return path.join(projectRoot, ...LEGACY_JSONL_RELATIVE);
|
|
243
288
|
}
|
|
244
289
|
|
|
290
|
+
function legacyTags(parsed) {
|
|
291
|
+
const tags = Array.isArray(parsed.tags)
|
|
292
|
+
? parsed.tags.filter(tag => typeof tag === 'string')
|
|
293
|
+
: [];
|
|
294
|
+
if (
|
|
295
|
+
typeof parsed.type === 'string'
|
|
296
|
+
&& /^[a-z0-9][a-z0-9-]{0,63}$/i.test(parsed.type)
|
|
297
|
+
&& !tags.some(tag => tag.startsWith('type:'))
|
|
298
|
+
) {
|
|
299
|
+
tags.push(`type:${parsed.type.toLowerCase()}`);
|
|
300
|
+
}
|
|
301
|
+
if (!tags.some(tag => tag.startsWith('trust:'))) tags.push('trust:suggested');
|
|
302
|
+
return tags;
|
|
303
|
+
}
|
|
304
|
+
|
|
245
305
|
// A STABLE key for a legacy record that lacks an `id`, derived from its content — so a
|
|
246
306
|
// re-run (or a failed rename) upserts the same row instead of double-inserting under a
|
|
247
307
|
// fresh random UUID.
|
|
@@ -292,7 +352,7 @@ function migrateJsonlNotesOnce(projectRoot, options = {}) {
|
|
|
292
352
|
key: typeof parsed.id === 'string' && parsed.id ? parsed.id : legacyContentKey(parsed),
|
|
293
353
|
value: parsed.note,
|
|
294
354
|
sourceAgent: IMPORT_SOURCE_AGENT,
|
|
295
|
-
tags:
|
|
355
|
+
tags: legacyTags(parsed),
|
|
296
356
|
};
|
|
297
357
|
if (typeof parsed.timestamp === 'string' && parsed.timestamp && !Number.isNaN(Date.parse(parsed.timestamp))) {
|
|
298
358
|
entry.timestamp = parsed.timestamp;
|
|
@@ -321,20 +381,38 @@ function migrateJsonlNotesOnce(projectRoot, options = {}) {
|
|
|
321
381
|
* @param {object} [options] - { tags, deps, env, config, graphitiEmitter, store }
|
|
322
382
|
* @returns {{ id: string, note: string, timestamp: string, tags: string[] }}
|
|
323
383
|
*/
|
|
324
|
-
function
|
|
384
|
+
function appendWithReceipt(projectRoot, note, options = {}) {
|
|
325
385
|
migrateJsonlNotesOnce(projectRoot, options);
|
|
326
386
|
const backend = resolveMemoryBackend({ ...options, projectRoot });
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
387
|
+
let entry;
|
|
388
|
+
const local = {
|
|
389
|
+
add() {
|
|
390
|
+
const written = projectMemory.write(projectRoot, {
|
|
391
|
+
key: crypto.randomUUID(),
|
|
392
|
+
value: note,
|
|
393
|
+
sourceAgent: REMEMBER_SOURCE_AGENT,
|
|
394
|
+
tags: Array.isArray(options.tags) ? options.tags : [],
|
|
395
|
+
}, options);
|
|
396
|
+
entry = toNote(written);
|
|
397
|
+
return entry;
|
|
398
|
+
},
|
|
399
|
+
recall: () => [],
|
|
400
|
+
search: () => [],
|
|
401
|
+
capture: () => null,
|
|
402
|
+
digest: () => null,
|
|
403
|
+
};
|
|
404
|
+
const registry = createMemoryBackendRegistry({ local });
|
|
334
405
|
if (backend === 'graphiti') {
|
|
335
|
-
|
|
406
|
+
registry.register('graphiti', graphitiAdapter(options.graphitiEmitter, () => entry));
|
|
407
|
+
} else if (backend !== 'local') {
|
|
408
|
+
registry.register(backend, registeredMemoryBackends.get(backend));
|
|
336
409
|
}
|
|
337
|
-
|
|
410
|
+
registry.select(backend);
|
|
411
|
+
return registry.add(note);
|
|
412
|
+
}
|
|
413
|
+
|
|
414
|
+
function append(projectRoot, note, options = {}) {
|
|
415
|
+
return appendWithReceipt(projectRoot, note, options).value;
|
|
338
416
|
}
|
|
339
417
|
|
|
340
418
|
/**
|
|
@@ -348,7 +426,7 @@ function append(projectRoot, note, options = {}) {
|
|
|
348
426
|
* widens it to every stored memory.
|
|
349
427
|
*
|
|
350
428
|
* @param {string} projectRoot
|
|
351
|
-
* @param {object} [selection] - { query, limit, all }
|
|
429
|
+
* @param {object} [selection] - { query, limit, all, kind }
|
|
352
430
|
* @param {object} [options] - forwarded to the project-memory read paths (e.g. a store).
|
|
353
431
|
* @returns {{ notes: object[], total: number, capped: boolean, query: string, limit: number, scope: string }}
|
|
354
432
|
*/
|
|
@@ -358,15 +436,16 @@ function recall(projectRoot, selection = {}, options = {}) {
|
|
|
358
436
|
const limit = Number.isInteger(requested) && requested > 0 ? requested : DEFAULT_RECALL_LIMIT;
|
|
359
437
|
const query = String(selection.query || '').trim();
|
|
360
438
|
const includeAll = Boolean(selection.all);
|
|
439
|
+
const kind = typeof selection.kind === 'string' ? selection.kind.trim() : '';
|
|
440
|
+
const agents = includeAll ? undefined : HUMAN_MEMORY_AGENTS;
|
|
441
|
+
const readOptions = { ...options, agents, kind };
|
|
361
442
|
|
|
362
443
|
if (query) {
|
|
363
|
-
const notes = projectMemory.searchRanked(projectRoot, query, limit,
|
|
444
|
+
const notes = projectMemory.searchRanked(projectRoot, query, limit, readOptions).map(toNote);
|
|
364
445
|
// BM25 returns at most `limit`; a full result set signals there may be more.
|
|
365
446
|
return { notes, total: notes.length, capped: notes.length >= limit, query, limit, scope: 'all' };
|
|
366
447
|
}
|
|
367
448
|
|
|
368
|
-
const agents = includeAll ? undefined : HUMAN_MEMORY_AGENTS;
|
|
369
|
-
const readOptions = { ...options, agents };
|
|
370
449
|
const notes = projectMemory.recent(projectRoot, limit, readOptions).map(toNote);
|
|
371
450
|
const total = projectMemory.count(projectRoot, readOptions);
|
|
372
451
|
return { notes, total, capped: total > notes.length, query: '', limit, scope: includeAll ? 'all' : 'remembered' };
|
|
@@ -380,8 +459,11 @@ module.exports = {
|
|
|
380
459
|
readMemoryConfig,
|
|
381
460
|
resolveMemoryBackend,
|
|
382
461
|
assertMemoryConfigValid,
|
|
462
|
+
registerMemoryBackend,
|
|
463
|
+
unregisterMemoryBackend,
|
|
383
464
|
migrateJsonlNotesOnce,
|
|
384
465
|
toNote,
|
|
385
466
|
append,
|
|
467
|
+
appendWithReceipt,
|
|
386
468
|
recall,
|
|
387
469
|
};
|
package/lib/memory-digest.js
CHANGED
|
@@ -22,11 +22,14 @@
|
|
|
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;
|
|
28
29
|
const DEFAULT_ISSUE_LIMIT = 5;
|
|
29
30
|
const DIGEST_HEADER = 'Forge memory (auto-injected at session start):';
|
|
31
|
+
const READ_ATTENTION_HEADER = 'Forge path memory (untrusted context; not authority):';
|
|
32
|
+
const SESSION_SUMMARY_REMINDER = 'Before ending this session, persist only durable learnings with `forge remember --session-summary --what <text> --why <text> --learned <text>`.';
|
|
30
33
|
|
|
31
34
|
/** Run an async producer, returning `fallback` on any throw/rejection (never propagates). */
|
|
32
35
|
async function safe(producer, fallback) {
|
|
@@ -103,7 +106,13 @@ async function collectDigestData(projectRoot, opts = {}) {
|
|
|
103
106
|
/** `- [date ]note` for a recall note. */
|
|
104
107
|
function formatNoteLine(note) {
|
|
105
108
|
const date = typeof note.timestamp === 'string' && note.timestamp ? `${note.timestamp.slice(0, 10)} ` : '';
|
|
106
|
-
|
|
109
|
+
const trust = memoryTrustStatus({
|
|
110
|
+
tags: note.tags,
|
|
111
|
+
sourceAgent: note.sourceAgent,
|
|
112
|
+
value: note.machine ? {} : note.note,
|
|
113
|
+
});
|
|
114
|
+
const sourceAgent = note.sourceAgent || 'unknown';
|
|
115
|
+
return `- [source=${sourceAgent} trust=${trust} updated=${date.trim() || 'unknown'}] ${note.note}`;
|
|
107
116
|
}
|
|
108
117
|
|
|
109
118
|
/** `- [label] title` for an issue row (title/id defensively resolved). */
|
|
@@ -112,18 +121,39 @@ function formatIssueLine(label, issue) {
|
|
|
112
121
|
return `- [${label}] ${title}`;
|
|
113
122
|
}
|
|
114
123
|
|
|
115
|
-
/** Build
|
|
116
|
-
function
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
124
|
+
/** Build separate confirmed/suggested note sections, skipping entries too large to fit. */
|
|
125
|
+
function notesSections(notes, budgetTokens) {
|
|
126
|
+
const eligible = notes
|
|
127
|
+
.map(note => ({ note, line: formatNoteLine(note) }))
|
|
128
|
+
.filter(entry => estimateTokens(entry.line) <= budgetTokens);
|
|
129
|
+
const groups = [
|
|
130
|
+
{ id: 'digest_notes', title: 'Confirmed memory', trust: 'confirmed', priority: 10 },
|
|
131
|
+
{
|
|
132
|
+
id: 'digest_suggested_memory',
|
|
133
|
+
title: 'Suggested memory — verify before relying',
|
|
134
|
+
trust: 'suggested',
|
|
135
|
+
priority: 11,
|
|
136
|
+
},
|
|
137
|
+
];
|
|
138
|
+
return groups.map(group => {
|
|
139
|
+
const content = eligible
|
|
140
|
+
.filter(({ note }) => memoryTrustStatus({
|
|
141
|
+
tags: note.tags,
|
|
142
|
+
sourceAgent: note.sourceAgent,
|
|
143
|
+
value: note.machine ? {} : note.note,
|
|
144
|
+
}) === group.trust)
|
|
145
|
+
.map(entry => entry.line)
|
|
146
|
+
.join('\n');
|
|
147
|
+
if (!content) return null;
|
|
148
|
+
return buildSection({
|
|
149
|
+
id: group.id,
|
|
150
|
+
title: group.title,
|
|
151
|
+
content,
|
|
152
|
+
priority: group.priority,
|
|
153
|
+
preserve: false,
|
|
154
|
+
untrustedSource: 'memory',
|
|
155
|
+
});
|
|
156
|
+
}).filter(Boolean);
|
|
127
157
|
}
|
|
128
158
|
|
|
129
159
|
/**
|
|
@@ -148,6 +178,59 @@ function issuesSection(ready, claimed) {
|
|
|
148
178
|
});
|
|
149
179
|
}
|
|
150
180
|
|
|
181
|
+
function sessionSummarySection(claimed) {
|
|
182
|
+
if (!claimed.length) return null;
|
|
183
|
+
return buildSection({
|
|
184
|
+
id: 'session_summary_nudge',
|
|
185
|
+
title: 'Session learning reminder',
|
|
186
|
+
content: SESSION_SUMMARY_REMINDER,
|
|
187
|
+
priority: 1,
|
|
188
|
+
preserve: true,
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function normalizePath(value) {
|
|
193
|
+
return String(value || '').replaceAll('\\', '/').replace(/^\.\//, '').toLowerCase();
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
function notePaths(note) {
|
|
197
|
+
const explicit = [note?.path, ...(Array.isArray(note?.paths) ? note.paths : [])];
|
|
198
|
+
const tagged = (Array.isArray(note?.tags) ? note.tags : [])
|
|
199
|
+
.filter(tag => typeof tag === 'string' && tag.startsWith('path:'))
|
|
200
|
+
.map(tag => tag.slice('path:'.length));
|
|
201
|
+
return [...explicit, ...tagged].map(normalizePath).filter(Boolean);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
function matchesPath(targetPath, candidatePath) {
|
|
205
|
+
const target = normalizePath(targetPath);
|
|
206
|
+
const candidate = normalizePath(candidatePath);
|
|
207
|
+
return target === candidate || target.endsWith(`/${candidate}`);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function buildReadAttentionDigest(targetPath, notes = [], options = {}) {
|
|
211
|
+
const budgetTokens = options.budgetTokens || 160;
|
|
212
|
+
const matched = (Array.isArray(notes) ? notes : []).filter(note =>
|
|
213
|
+
notePaths(note).some(candidate => matchesPath(targetPath, candidate)));
|
|
214
|
+
if (!matched.length) return { text: '', empty: true, tokens: 0 };
|
|
215
|
+
|
|
216
|
+
const lines = matched.map(formatNoteLine);
|
|
217
|
+
const section = buildSection({
|
|
218
|
+
id: 'read_attention',
|
|
219
|
+
title: 'Path-matched memory',
|
|
220
|
+
content: lines.join('\n'),
|
|
221
|
+
priority: 1,
|
|
222
|
+
preserve: false,
|
|
223
|
+
untrustedSource: 'path-memory',
|
|
224
|
+
});
|
|
225
|
+
const budgeted = applyBudget([section], Math.max(1, budgetTokens - estimateTokens(READ_ATTENTION_HEADER) - 20));
|
|
226
|
+
const content = budgeted.sections[0]?.content;
|
|
227
|
+
if (!content) return { text: '', empty: true, tokens: 0 };
|
|
228
|
+
const text = `${READ_ATTENTION_HEADER}\n\n${fenceUntrusted(content, { source: 'path-memory' })}`;
|
|
229
|
+
const tokens = estimateTokens(text);
|
|
230
|
+
if (tokens > budgetTokens) return { text: '', empty: true, tokens: 0 };
|
|
231
|
+
return { text, empty: false, tokens };
|
|
232
|
+
}
|
|
233
|
+
|
|
151
234
|
/**
|
|
152
235
|
* Assemble the bounded digest text. PURE. Never exceeds `budgetTokens` (delegated to
|
|
153
236
|
* applyBudget). Empty inputs → { text: '', empty: true } so the caller injects nothing.
|
|
@@ -161,13 +244,18 @@ function buildMemoryDigest(data = {}, options = {}) {
|
|
|
161
244
|
const ready = Array.isArray(data.ready) ? data.ready : [];
|
|
162
245
|
const claimed = Array.isArray(data.claimed) ? data.claimed : [];
|
|
163
246
|
const inbox = Array.isArray(data.inbox) ? data.inbox : [];
|
|
247
|
+
const budgetTokens = options.budgetTokens || DEFAULT_DIGEST_BUDGET_TOKENS;
|
|
164
248
|
|
|
165
249
|
// Inbox (priority 5) is a THIRD section beside notes + issues; a fresh human directive
|
|
166
250
|
// outranks stale notes (10) and the agent's own issue list (20) under budget pressure.
|
|
167
|
-
const sections = [
|
|
251
|
+
const sections = [
|
|
252
|
+
sessionSummarySection(claimed),
|
|
253
|
+
inboxSection(inbox),
|
|
254
|
+
...notesSections(notes, budgetTokens),
|
|
255
|
+
issuesSection(ready, claimed),
|
|
256
|
+
].filter(Boolean);
|
|
168
257
|
if (!sections.length) return { text: '', empty: true, tokens: 0 };
|
|
169
258
|
|
|
170
|
-
const budgetTokens = options.budgetTokens || DEFAULT_DIGEST_BUDGET_TOKENS;
|
|
171
259
|
const budgeted = applyBudget(sections, budgetTokens);
|
|
172
260
|
const body = budgeted.sections
|
|
173
261
|
.filter(section => section.content)
|
|
@@ -185,7 +273,10 @@ function buildMemoryDigest(data = {}, options = {}) {
|
|
|
185
273
|
module.exports = {
|
|
186
274
|
DEFAULT_DIGEST_BUDGET_TOKENS,
|
|
187
275
|
DIGEST_HEADER,
|
|
276
|
+
READ_ATTENTION_HEADER,
|
|
277
|
+
SESSION_SUMMARY_REMINDER,
|
|
188
278
|
buildMemoryDigest,
|
|
279
|
+
buildReadAttentionDigest,
|
|
189
280
|
collectDigestData,
|
|
190
281
|
extractIssues,
|
|
191
282
|
// exported for focused reuse / tests
|
|
@@ -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,
|