byterover-cli 3.6.1 → 3.7.1
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/README.md +1 -1
- package/dist/agent/core/interfaces/cipher-services.d.ts +7 -0
- package/dist/agent/infra/agent/cipher-agent.js +4 -2
- package/dist/agent/infra/agent/service-initializer.js +28 -14
- package/dist/agent/infra/sandbox/curate-service.d.ts +4 -2
- package/dist/agent/infra/sandbox/curate-service.js +6 -4
- package/dist/agent/infra/tools/implementations/curate-tool.d.ts +4 -2
- package/dist/agent/infra/tools/implementations/curate-tool.js +169 -25
- package/dist/agent/infra/tools/implementations/expand-knowledge-tool.d.ts +2 -0
- package/dist/agent/infra/tools/implementations/expand-knowledge-tool.js +1 -1
- package/dist/agent/infra/tools/implementations/memory-symbol-tree.d.ts +0 -8
- package/dist/agent/infra/tools/implementations/memory-symbol-tree.js +3 -15
- package/dist/agent/infra/tools/implementations/search-knowledge-service.d.ts +49 -4
- package/dist/agent/infra/tools/implementations/search-knowledge-service.js +123 -53
- package/dist/agent/infra/tools/implementations/search-knowledge-tool.d.ts +2 -0
- package/dist/agent/infra/tools/tool-provider.js +1 -0
- package/dist/agent/infra/tools/tool-registry.d.ts +7 -0
- package/dist/agent/infra/tools/tool-registry.js +13 -6
- package/dist/oclif/commands/dream.d.ts +4 -0
- package/dist/oclif/commands/dream.js +31 -13
- package/dist/server/constants.d.ts +1 -1
- package/dist/server/constants.js +20 -1
- package/dist/server/core/domain/knowledge/markdown-writer.d.ts +12 -42
- package/dist/server/core/domain/knowledge/markdown-writer.js +55 -96
- package/dist/server/core/domain/knowledge/memory-scoring.d.ts +18 -37
- package/dist/server/core/domain/knowledge/memory-scoring.js +36 -85
- package/dist/server/core/domain/knowledge/runtime-signals-schema.d.ts +59 -0
- package/dist/server/core/domain/knowledge/runtime-signals-schema.js +46 -0
- package/dist/server/core/domain/knowledge/sidecar-logging.d.ts +14 -0
- package/dist/server/core/domain/knowledge/sidecar-logging.js +18 -0
- package/dist/server/core/interfaces/storage/i-runtime-signal-store.d.ts +111 -0
- package/dist/server/core/interfaces/storage/i-runtime-signal-store.js +38 -0
- package/dist/server/infra/context-tree/file-context-tree-archive-service.d.ts +16 -6
- package/dist/server/infra/context-tree/file-context-tree-archive-service.js +91 -32
- package/dist/server/infra/context-tree/file-context-tree-manifest-service.d.ts +14 -0
- package/dist/server/infra/context-tree/file-context-tree-manifest-service.js +20 -7
- package/dist/server/infra/context-tree/runtime-signal-store.d.ts +46 -0
- package/dist/server/infra/context-tree/runtime-signal-store.js +118 -0
- package/dist/server/infra/daemon/agent-process.js +25 -4
- package/dist/server/infra/dream/operations/consolidate.d.ts +17 -0
- package/dist/server/infra/dream/operations/consolidate.js +40 -19
- package/dist/server/infra/dream/operations/prune.d.ts +18 -0
- package/dist/server/infra/dream/operations/prune.js +31 -20
- package/dist/server/infra/dream/operations/synthesize.d.ts +13 -0
- package/dist/server/infra/dream/operations/synthesize.js +15 -3
- package/dist/server/infra/executor/dream-executor.d.ts +8 -0
- package/dist/server/infra/executor/dream-executor.js +3 -0
- package/dist/server/templates/skill/SKILL.md +79 -22
- package/oclif.manifest.json +429 -429
- package/package.json +1 -1
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IKeyStorage-backed implementation of {@link IRuntimeSignalStore}.
|
|
3
|
+
*
|
|
4
|
+
* Uses composite keys `["signals", ...pathSegments]`. Atomicity within a
|
|
5
|
+
* single process comes from `IKeyStorage.update`'s per-key RWLock; the
|
|
6
|
+
* interface docs cover the cross-process caveat.
|
|
7
|
+
*/
|
|
8
|
+
import type { IKeyStorage } from '../../../agent/core/interfaces/i-key-storage.js';
|
|
9
|
+
import type { ILogger } from '../../../agent/core/interfaces/i-logger.js';
|
|
10
|
+
import type { IRuntimeSignalStore, RuntimeSignalsUpdater } from '../../core/interfaces/storage/i-runtime-signal-store.js';
|
|
11
|
+
import { type RuntimeSignals } from '../../core/domain/knowledge/runtime-signals-schema.js';
|
|
12
|
+
export declare class RuntimeSignalStore implements IRuntimeSignalStore {
|
|
13
|
+
private readonly keyStorage;
|
|
14
|
+
private readonly logger;
|
|
15
|
+
constructor(keyStorage: IKeyStorage, logger: ILogger);
|
|
16
|
+
batchUpdate(updates: Map<string, RuntimeSignalsUpdater>): Promise<void>;
|
|
17
|
+
delete(relPath: string): Promise<void>;
|
|
18
|
+
get(relPath: string): Promise<RuntimeSignals>;
|
|
19
|
+
getMany(relPaths: readonly string[]): Promise<Map<string, RuntimeSignals>>;
|
|
20
|
+
list(): Promise<Map<string, RuntimeSignals>>;
|
|
21
|
+
set(relPath: string, signals: RuntimeSignals): Promise<void>;
|
|
22
|
+
update(relPath: string, updater: RuntimeSignalsUpdater): Promise<RuntimeSignals>;
|
|
23
|
+
/**
|
|
24
|
+
* Reconstruct the relative path from a `["signals", ...segments]` key.
|
|
25
|
+
* Returns null for keys that do not belong to this store (defensive against
|
|
26
|
+
* the remote chance of namespace collisions during listing).
|
|
27
|
+
*/
|
|
28
|
+
private relPathFromKey;
|
|
29
|
+
/**
|
|
30
|
+
* Encode a relative path into a composite storage key.
|
|
31
|
+
*
|
|
32
|
+
* FileKeyStorage rejects `/` inside segments, so each path component
|
|
33
|
+
* becomes its own segment. Empty components (from leading, trailing, or
|
|
34
|
+
* consecutive slashes) are dropped so the encoding is insensitive to
|
|
35
|
+
* path normalization variants.
|
|
36
|
+
*/
|
|
37
|
+
private signalKey;
|
|
38
|
+
/**
|
|
39
|
+
* Coerce an unknown stored value into a valid RuntimeSignals record.
|
|
40
|
+
*
|
|
41
|
+
* Missing (`undefined`) yields defaults silently — the common fresh-path
|
|
42
|
+
* case. Corrupt stored data logs a warning and also falls back to
|
|
43
|
+
* defaults so callers never have to handle read errors inline.
|
|
44
|
+
*/
|
|
45
|
+
private validateOrDefault;
|
|
46
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* IKeyStorage-backed implementation of {@link IRuntimeSignalStore}.
|
|
3
|
+
*
|
|
4
|
+
* Uses composite keys `["signals", ...pathSegments]`. Atomicity within a
|
|
5
|
+
* single process comes from `IKeyStorage.update`'s per-key RWLock; the
|
|
6
|
+
* interface docs cover the cross-process caveat.
|
|
7
|
+
*/
|
|
8
|
+
import { createDefaultRuntimeSignals, RuntimeSignalsSchema, } from '../../core/domain/knowledge/runtime-signals-schema.js';
|
|
9
|
+
const SIGNALS_PREFIX = 'signals';
|
|
10
|
+
// The store does not enforce the importance ↔ maturity hysteresis —
|
|
11
|
+
// callers bumping importance must recompute maturity via `determineTier`
|
|
12
|
+
// in the same updater. Invariant upheld at every write site; see
|
|
13
|
+
// interface-level docs for the rationale. Orphan-entry cleanup is tracked
|
|
14
|
+
// in features/runtime-signals/backlog.md (`pruneOrphans`).
|
|
15
|
+
export class RuntimeSignalStore {
|
|
16
|
+
keyStorage;
|
|
17
|
+
logger;
|
|
18
|
+
constructor(keyStorage, logger) {
|
|
19
|
+
this.keyStorage = keyStorage;
|
|
20
|
+
this.logger = logger;
|
|
21
|
+
}
|
|
22
|
+
async batchUpdate(updates) {
|
|
23
|
+
// Different relPaths do not share a per-key lock, so parallel updates
|
|
24
|
+
// scale naturally. Each individual update remains atomic because
|
|
25
|
+
// IKeyStorage.update is atomic per key (within one process).
|
|
26
|
+
await Promise.all([...updates.entries()].map(([relPath, updater]) => this.update(relPath, updater)));
|
|
27
|
+
}
|
|
28
|
+
async delete(relPath) {
|
|
29
|
+
await this.keyStorage.delete(this.signalKey(relPath));
|
|
30
|
+
}
|
|
31
|
+
async get(relPath) {
|
|
32
|
+
const raw = await this.keyStorage.get(this.signalKey(relPath));
|
|
33
|
+
return this.validateOrDefault(raw, relPath);
|
|
34
|
+
}
|
|
35
|
+
async getMany(relPaths) {
|
|
36
|
+
// Only include paths that have a stored record. Callers distinguish
|
|
37
|
+
// missing via `.has(path)`; ergonomic default-on-miss via
|
|
38
|
+
// `map.get(path) ?? createDefaultRuntimeSignals()`.
|
|
39
|
+
const entries = await Promise.all(relPaths.map(async (relPath) => {
|
|
40
|
+
const raw = await this.keyStorage.get(this.signalKey(relPath));
|
|
41
|
+
if (raw === undefined)
|
|
42
|
+
return null;
|
|
43
|
+
const parsed = RuntimeSignalsSchema.safeParse(raw);
|
|
44
|
+
if (parsed.success) {
|
|
45
|
+
return [relPath, parsed.data];
|
|
46
|
+
}
|
|
47
|
+
this.logger.warn(`RuntimeSignalStore: discarding corrupt entry for ${relPath}: ${parsed.error.message}`);
|
|
48
|
+
return null;
|
|
49
|
+
}));
|
|
50
|
+
return new Map(entries.filter((entry) => entry !== null));
|
|
51
|
+
}
|
|
52
|
+
async list() {
|
|
53
|
+
const entries = await this.keyStorage.listWithValues([SIGNALS_PREFIX]);
|
|
54
|
+
const result = new Map();
|
|
55
|
+
for (const entry of entries) {
|
|
56
|
+
const relPath = this.relPathFromKey(entry.key);
|
|
57
|
+
if (relPath === null)
|
|
58
|
+
continue;
|
|
59
|
+
result.set(relPath, this.validateOrDefault(entry.value, relPath));
|
|
60
|
+
}
|
|
61
|
+
return result;
|
|
62
|
+
}
|
|
63
|
+
async set(relPath, signals) {
|
|
64
|
+
const validated = RuntimeSignalsSchema.parse(signals);
|
|
65
|
+
await this.keyStorage.set(this.signalKey(relPath), validated);
|
|
66
|
+
}
|
|
67
|
+
async update(relPath, updater) {
|
|
68
|
+
return this.keyStorage.update(this.signalKey(relPath), (current) => {
|
|
69
|
+
// `current` is typed as RuntimeSignals but the underlying value may be
|
|
70
|
+
// anything the disk held (missing, partial, corrupt). validateOrDefault
|
|
71
|
+
// coerces it into a valid record before the updater runs.
|
|
72
|
+
const base = this.validateOrDefault(current, relPath);
|
|
73
|
+
const merged = updater(base);
|
|
74
|
+
// Re-validate updater output so a buggy caller cannot land invalid
|
|
75
|
+
// data (e.g. importance out of range) on disk.
|
|
76
|
+
return RuntimeSignalsSchema.parse(merged);
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Reconstruct the relative path from a `["signals", ...segments]` key.
|
|
81
|
+
* Returns null for keys that do not belong to this store (defensive against
|
|
82
|
+
* the remote chance of namespace collisions during listing).
|
|
83
|
+
*/
|
|
84
|
+
relPathFromKey(key) {
|
|
85
|
+
if (key.length < 2 || key[0] !== SIGNALS_PREFIX)
|
|
86
|
+
return null;
|
|
87
|
+
return key.slice(1).join('/');
|
|
88
|
+
}
|
|
89
|
+
/**
|
|
90
|
+
* Encode a relative path into a composite storage key.
|
|
91
|
+
*
|
|
92
|
+
* FileKeyStorage rejects `/` inside segments, so each path component
|
|
93
|
+
* becomes its own segment. Empty components (from leading, trailing, or
|
|
94
|
+
* consecutive slashes) are dropped so the encoding is insensitive to
|
|
95
|
+
* path normalization variants.
|
|
96
|
+
*/
|
|
97
|
+
signalKey(relPath) {
|
|
98
|
+
const segments = relPath.split('/').filter((s) => s.length > 0);
|
|
99
|
+
return [SIGNALS_PREFIX, ...segments];
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Coerce an unknown stored value into a valid RuntimeSignals record.
|
|
103
|
+
*
|
|
104
|
+
* Missing (`undefined`) yields defaults silently — the common fresh-path
|
|
105
|
+
* case. Corrupt stored data logs a warning and also falls back to
|
|
106
|
+
* defaults so callers never have to handle read errors inline.
|
|
107
|
+
*/
|
|
108
|
+
validateOrDefault(raw, relPath) {
|
|
109
|
+
if (raw === undefined) {
|
|
110
|
+
return createDefaultRuntimeSignals();
|
|
111
|
+
}
|
|
112
|
+
const parsed = RuntimeSignalsSchema.safeParse(raw);
|
|
113
|
+
if (parsed.success)
|
|
114
|
+
return parsed.data;
|
|
115
|
+
this.logger.warn(`RuntimeSignalStore: discarding corrupt entry for ${relPath}: ${parsed.error.message}`);
|
|
116
|
+
return createDefaultRuntimeSignals();
|
|
117
|
+
}
|
|
118
|
+
}
|
|
@@ -27,6 +27,7 @@ import { CipherAgent } from '../../../agent/infra/agent/index.js';
|
|
|
27
27
|
import { FileSystemService } from '../../../agent/infra/file-system/file-system-service.js';
|
|
28
28
|
import { FolderPackService } from '../../../agent/infra/folder-pack/folder-pack-service.js';
|
|
29
29
|
import { SessionMetadataStore } from '../../../agent/infra/session/session-metadata-store.js';
|
|
30
|
+
import { FileKeyStorage } from '../../../agent/infra/storage/file-key-storage.js';
|
|
30
31
|
import { createSearchKnowledgeService } from '../../../agent/infra/tools/implementations/search-knowledge-service.js';
|
|
31
32
|
import { AuthEvents } from '../../../shared/transport/events/auth-events.js';
|
|
32
33
|
import { decodeSearchContent } from '../../../shared/transport/search-content.js';
|
|
@@ -36,6 +37,7 @@ import { serializeTaskError, TaskError, TaskErrorCode } from '../../core/domain/
|
|
|
36
37
|
import { loadSources } from '../../core/domain/source/source-schema.js';
|
|
37
38
|
import { TransportAgentEventNames, TransportDaemonEventNames, TransportStateEventNames, TransportTaskEventNames, } from '../../core/domain/transport/schemas.js';
|
|
38
39
|
import { FileContextTreeArchiveService } from '../context-tree/file-context-tree-archive-service.js';
|
|
40
|
+
import { RuntimeSignalStore } from '../context-tree/runtime-signal-store.js';
|
|
39
41
|
import { DreamLockService } from '../dream/dream-lock-service.js';
|
|
40
42
|
import { DreamLogStore } from '../dream/dream-log-store.js';
|
|
41
43
|
import { DreamStateService } from '../dream/dream-state-service.js';
|
|
@@ -300,7 +302,25 @@ async function start() {
|
|
|
300
302
|
workingDirectory: projectPath,
|
|
301
303
|
});
|
|
302
304
|
await fileSystemService.initialize();
|
|
303
|
-
|
|
305
|
+
// Runtime-signal sidecar for this daemon. FileKeyStorage is file-backed
|
|
306
|
+
// under configResult.storagePath, so the daemon and any other process for
|
|
307
|
+
// the same project write to the same on-disk store. `brv search`, curate,
|
|
308
|
+
// and archive in this daemon all mirror scoring writes through it.
|
|
309
|
+
const daemonKeyStorage = new FileKeyStorage({
|
|
310
|
+
storageDir: configResult.storagePath,
|
|
311
|
+
});
|
|
312
|
+
await daemonKeyStorage.initialize();
|
|
313
|
+
const daemonLogger = {
|
|
314
|
+
debug: (msg) => agentLog(msg),
|
|
315
|
+
error: (msg) => agentLog(msg),
|
|
316
|
+
info: (msg) => agentLog(msg),
|
|
317
|
+
warn: (msg) => agentLog(msg),
|
|
318
|
+
};
|
|
319
|
+
const daemonRuntimeSignalStore = new RuntimeSignalStore(daemonKeyStorage, daemonLogger);
|
|
320
|
+
const searchService = createSearchKnowledgeService(fileSystemService, {
|
|
321
|
+
baseDirectory: projectPath,
|
|
322
|
+
runtimeSignalStore: daemonRuntimeSignalStore,
|
|
323
|
+
});
|
|
304
324
|
// 7. Create executors and listen for task:execute from pool
|
|
305
325
|
const curateExecutor = new CurateExecutor();
|
|
306
326
|
const folderPackService = new FolderPackService(fileSystemService);
|
|
@@ -316,7 +336,7 @@ async function start() {
|
|
|
316
336
|
transport.on(TransportTaskEventNames.EXECUTE, (task) => {
|
|
317
337
|
agentLog(`task:execute received taskId=${task.taskId} type=${task.type} activeTaskCount=${activeTaskCount + 1}`);
|
|
318
338
|
// eslint-disable-next-line no-void
|
|
319
|
-
void executeTask(task, curateExecutor, folderPackExecutor, queryExecutor, searchExecutor, searchService, configResult.storagePath);
|
|
339
|
+
void executeTask(task, curateExecutor, folderPackExecutor, queryExecutor, searchExecutor, searchService, configResult.storagePath, daemonRuntimeSignalStore);
|
|
320
340
|
});
|
|
321
341
|
// 8. Register with transport server (for TransportHandlers tracking)
|
|
322
342
|
await transport.requestWithAck('agent:register', { projectPath });
|
|
@@ -324,7 +344,7 @@ async function start() {
|
|
|
324
344
|
process.send?.({ clientId, type: 'ready' });
|
|
325
345
|
agentLog('Ready — listening for tasks');
|
|
326
346
|
}
|
|
327
|
-
async function executeTask(task, curateExecutor, folderPackExecutor, queryExecutor, searchExecutor, searchKnowledgeService, storagePath) {
|
|
347
|
+
async function executeTask(task, curateExecutor, folderPackExecutor, queryExecutor, searchExecutor, searchKnowledgeService, storagePath, runtimeSignalStore) {
|
|
328
348
|
const { clientCwd, clientId, content, files, folderPath, force, taskId, trigger, type, worktreeRoot } = task;
|
|
329
349
|
if (!transport || !agent)
|
|
330
350
|
return;
|
|
@@ -438,12 +458,13 @@ async function executeTask(task, curateExecutor, folderPackExecutor, queryExecut
|
|
|
438
458
|
break;
|
|
439
459
|
}
|
|
440
460
|
const dreamExecutor = new DreamExecutor({
|
|
441
|
-
archiveService: new FileContextTreeArchiveService(),
|
|
461
|
+
archiveService: new FileContextTreeArchiveService(runtimeSignalStore),
|
|
442
462
|
curateLogStore: new FileCurateLogStore({ baseDir: storagePath }),
|
|
443
463
|
dreamLockService,
|
|
444
464
|
dreamLogStore: new DreamLogStore({ baseDir: brvDir }),
|
|
445
465
|
dreamStateService,
|
|
446
466
|
reviewBackupStore: new FileReviewBackupStore(brvDir),
|
|
467
|
+
runtimeSignalStore,
|
|
447
468
|
searchService: searchKnowledgeService,
|
|
448
469
|
});
|
|
449
470
|
const dreamResult = await dreamExecutor.executeWithAgent(agent, {
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
* Never throws — returns partial results on errors.
|
|
12
12
|
*/
|
|
13
13
|
import type { ICipherAgent } from '../../../../agent/core/interfaces/i-cipher-agent.js';
|
|
14
|
+
import type { ILogger } from '../../../../agent/core/interfaces/i-logger.js';
|
|
14
15
|
import type { DreamOperation } from '../dream-log-schema.js';
|
|
15
16
|
import type { DreamState } from '../dream-state-schema.js';
|
|
16
17
|
export type ConsolidateDeps = {
|
|
@@ -27,9 +28,25 @@ export type ConsolidateDeps = {
|
|
|
27
28
|
update(updater: (state: DreamState) => DreamState): Promise<DreamState>;
|
|
28
29
|
write(state: DreamState): Promise<void>;
|
|
29
30
|
};
|
|
31
|
+
/**
|
|
32
|
+
* Optional logger. When provided, per-file sidecar failures during the
|
|
33
|
+
* CROSS_REFERENCE review gate emit a warn so silent swallows are visible.
|
|
34
|
+
*/
|
|
35
|
+
logger?: ILogger;
|
|
30
36
|
reviewBackupStore?: {
|
|
31
37
|
save(relativePath: string, content: string): Promise<void>;
|
|
32
38
|
};
|
|
39
|
+
/**
|
|
40
|
+
* Optional. When present, the CROSS_REFERENCE review-gate consults the
|
|
41
|
+
* sidecar to check whether any input file has `maturity === 'core'`. Absent
|
|
42
|
+
* store or missing entries mean no file qualifies as core — review is
|
|
43
|
+
* skipped, matching the pre-migration behaviour for paths without scoring.
|
|
44
|
+
*/
|
|
45
|
+
runtimeSignalStore?: {
|
|
46
|
+
get(relPath: string): Promise<{
|
|
47
|
+
maturity: 'core' | 'draft' | 'validated';
|
|
48
|
+
}>;
|
|
49
|
+
};
|
|
33
50
|
searchService: {
|
|
34
51
|
search(query: string, options?: {
|
|
35
52
|
limit?: number;
|
|
@@ -14,7 +14,7 @@ import { dump as yamlDump, load as yamlLoad } from 'js-yaml';
|
|
|
14
14
|
import { randomUUID } from 'node:crypto';
|
|
15
15
|
import { access, mkdir, readdir, readFile, rename, unlink, writeFile } from 'node:fs/promises';
|
|
16
16
|
import { dirname, join } from 'node:path';
|
|
17
|
-
import {
|
|
17
|
+
import { warnSidecarFailure } from '../../../core/domain/knowledge/sidecar-logging.js';
|
|
18
18
|
import { ConsolidateResponseSchema } from '../dream-response-schemas.js';
|
|
19
19
|
import { parseDreamResponse } from '../parse-dream-response.js';
|
|
20
20
|
/**
|
|
@@ -148,7 +148,13 @@ async function processDomain(domain, files, deps, hints = []) {
|
|
|
148
148
|
for (const action of parsed.actions) {
|
|
149
149
|
try {
|
|
150
150
|
// eslint-disable-next-line no-await-in-loop
|
|
151
|
-
const op = await executeAction(action,
|
|
151
|
+
const op = await executeAction(action, {
|
|
152
|
+
contextTreeDir,
|
|
153
|
+
fileContents,
|
|
154
|
+
logger: deps.logger,
|
|
155
|
+
reviewBackupStore: deps.reviewBackupStore,
|
|
156
|
+
runtimeSignalStore: deps.runtimeSignalStore,
|
|
157
|
+
});
|
|
152
158
|
if (op)
|
|
153
159
|
results.push(op);
|
|
154
160
|
}
|
|
@@ -338,23 +344,24 @@ function buildPrompt(changedFiles, relatedFiles, filesPayload, pendingMergeHints
|
|
|
338
344
|
lines.push('', 'File contents:', fileBlocks, '', 'For each pair/group of related files, classify the relationship and recommend an action:', '- MERGE: Files are redundant/overlapping → combine into one, specify outputFile and mergedContent', '- TEMPORAL_UPDATE: File has contradictory/outdated info → rewrite with temporal narrative, specify updatedContent', '- CROSS_REFERENCE: Files are complementary → add cross-references (no content changes needed)', '- SKIP: Files are genuinely unrelated → no action needed', '', 'Respond with JSON matching this schema:', '```', '{ "actions": [{ "type": "MERGE"|"TEMPORAL_UPDATE"|"CROSS_REFERENCE"|"SKIP", "files": ["path1", ...], "reason": "...", "confidence?": 0.0-1.0, "mergedContent?": "...", "outputFile?": "...", "updatedContent?": "..." }] }', '```', '', 'Rules:', '- Default to MERGE when files share >50% of content or cover the same topic. SKIP only when files are genuinely on unrelated topics.', '- Returning all SKIP when duplicates exist is a failure, not caution.', '- For MERGE, choose the richer/more complete file as outputFile. The mergedContent should preserve all unique details from both sources.', '- For TEMPORAL_UPDATE, preserve all facts and add temporal context. Include confidence (0-1) indicating certainty that the update is correct.', '- For CROSS_REFERENCE, just list the files — the system will add frontmatter links.', '- Preserve all diagrams, tables, code examples, and structured data verbatim.');
|
|
339
345
|
return lines.join('\n');
|
|
340
346
|
}
|
|
341
|
-
async function executeAction(action,
|
|
347
|
+
async function executeAction(action, ctx) {
|
|
342
348
|
switch (action.type) {
|
|
343
349
|
case 'CROSS_REFERENCE': {
|
|
344
|
-
return executeCrossReference(action,
|
|
350
|
+
return executeCrossReference(action, ctx);
|
|
345
351
|
}
|
|
346
352
|
case 'MERGE': {
|
|
347
|
-
return executeMerge(action,
|
|
353
|
+
return executeMerge(action, ctx);
|
|
348
354
|
}
|
|
349
355
|
case 'SKIP': {
|
|
350
356
|
return undefined;
|
|
351
357
|
}
|
|
352
358
|
case 'TEMPORAL_UPDATE': {
|
|
353
|
-
return executeTemporalUpdate(action,
|
|
359
|
+
return executeTemporalUpdate(action, ctx);
|
|
354
360
|
}
|
|
355
361
|
}
|
|
356
362
|
}
|
|
357
|
-
async function executeMerge(action,
|
|
363
|
+
async function executeMerge(action, ctx) {
|
|
364
|
+
const { contextTreeDir, fileContents, reviewBackupStore, runtimeSignalStore } = ctx;
|
|
358
365
|
const outputFile = action.outputFile ?? action.files[0];
|
|
359
366
|
if (!action.mergedContent) {
|
|
360
367
|
throw new Error(`MERGE action missing mergedContent for ${outputFile}`);
|
|
@@ -386,7 +393,7 @@ async function executeMerge(action, contextTreeDir, fileContents, reviewBackupSt
|
|
|
386
393
|
const toDelete = action.files.filter((f) => f !== outputFile);
|
|
387
394
|
await Promise.all(toDelete.map((f) => unlink(join(contextTreeDir, f)).catch(() => { })));
|
|
388
395
|
// Determine needsReview
|
|
389
|
-
const needsReview = determineNeedsReview('MERGE', action.files,
|
|
396
|
+
const needsReview = await determineNeedsReview('MERGE', action.files, { runtimeSignalStore });
|
|
390
397
|
return {
|
|
391
398
|
action: 'MERGE',
|
|
392
399
|
inputFiles: action.files,
|
|
@@ -397,7 +404,8 @@ async function executeMerge(action, contextTreeDir, fileContents, reviewBackupSt
|
|
|
397
404
|
type: 'CONSOLIDATE',
|
|
398
405
|
};
|
|
399
406
|
}
|
|
400
|
-
async function executeTemporalUpdate(action,
|
|
407
|
+
async function executeTemporalUpdate(action, ctx) {
|
|
408
|
+
const { contextTreeDir, fileContents, reviewBackupStore, runtimeSignalStore } = ctx;
|
|
401
409
|
const targetFile = action.files[0];
|
|
402
410
|
if (!action.updatedContent) {
|
|
403
411
|
throw new Error(`TEMPORAL_UPDATE action missing updatedContent for ${targetFile}`);
|
|
@@ -409,7 +417,10 @@ async function executeTemporalUpdate(action, contextTreeDir, fileContents, revie
|
|
|
409
417
|
if (original !== undefined) {
|
|
410
418
|
previousTexts[targetFile] = original;
|
|
411
419
|
}
|
|
412
|
-
const needsReview = determineNeedsReview('TEMPORAL_UPDATE', action.files,
|
|
420
|
+
const needsReview = await determineNeedsReview('TEMPORAL_UPDATE', action.files, {
|
|
421
|
+
confidence: action.confidence,
|
|
422
|
+
runtimeSignalStore,
|
|
423
|
+
});
|
|
413
424
|
// Create review backup only when the operation needs human review
|
|
414
425
|
if (reviewBackupStore && original !== undefined && needsReview) {
|
|
415
426
|
try {
|
|
@@ -432,7 +443,8 @@ async function executeTemporalUpdate(action, contextTreeDir, fileContents, revie
|
|
|
432
443
|
type: 'CONSOLIDATE',
|
|
433
444
|
};
|
|
434
445
|
}
|
|
435
|
-
async function executeCrossReference(action,
|
|
446
|
+
async function executeCrossReference(action, ctx) {
|
|
447
|
+
const { contextTreeDir, fileContents, logger, reviewBackupStore, runtimeSignalStore } = ctx;
|
|
436
448
|
const previousTexts = {};
|
|
437
449
|
for (const file of action.files) {
|
|
438
450
|
const content = fileContents.get(file);
|
|
@@ -440,7 +452,7 @@ async function executeCrossReference(action, contextTreeDir, fileContents, revie
|
|
|
440
452
|
previousTexts[file] = content;
|
|
441
453
|
}
|
|
442
454
|
}
|
|
443
|
-
const needsReview = determineNeedsReview('CROSS_REFERENCE', action.files,
|
|
455
|
+
const needsReview = await determineNeedsReview('CROSS_REFERENCE', action.files, { logger, runtimeSignalStore });
|
|
444
456
|
if (needsReview && reviewBackupStore) {
|
|
445
457
|
await Promise.all(Object.entries(previousTexts).map(([file, content]) => reviewBackupStore.save(file, content).catch(() => { })));
|
|
446
458
|
}
|
|
@@ -494,21 +506,30 @@ async function addRelatedLinks(filePath, relatedPaths) {
|
|
|
494
506
|
const yaml = yamlDump({ related: relatedPaths }, { flowLevel: 1, lineWidth: -1, sortKeys: true }).trimEnd();
|
|
495
507
|
await atomicWrite(filePath, `---\n${yaml}\n---\n${content}`);
|
|
496
508
|
}
|
|
497
|
-
function determineNeedsReview(actionType, files,
|
|
509
|
+
async function determineNeedsReview(actionType, files, opts) {
|
|
498
510
|
// MERGE always needs review
|
|
499
511
|
if (actionType === 'MERGE')
|
|
500
512
|
return true;
|
|
501
513
|
// TEMPORAL_UPDATE: needs review when confidence is low or absent
|
|
502
514
|
if (actionType === 'TEMPORAL_UPDATE')
|
|
503
|
-
return (confidence ?? 0) < 0.7;
|
|
504
|
-
// CROSS_REFERENCE: only if any file has core maturity
|
|
515
|
+
return (opts.confidence ?? 0) < 0.7;
|
|
516
|
+
// CROSS_REFERENCE: only if any file has core maturity in the sidecar.
|
|
517
|
+
// Without a store, no file can qualify as core — review is skipped, which
|
|
518
|
+
// matches the pre-migration default when no scoring was present.
|
|
519
|
+
const { logger, runtimeSignalStore } = opts;
|
|
520
|
+
if (!runtimeSignalStore)
|
|
521
|
+
return false;
|
|
505
522
|
for (const file of files) {
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
const
|
|
509
|
-
if (
|
|
523
|
+
try {
|
|
524
|
+
// eslint-disable-next-line no-await-in-loop
|
|
525
|
+
const signals = await runtimeSignalStore.get(file);
|
|
526
|
+
if (signals.maturity === 'core')
|
|
510
527
|
return true;
|
|
511
528
|
}
|
|
529
|
+
catch (error) {
|
|
530
|
+
// Ignore per-file sidecar failures — continue checking remaining files.
|
|
531
|
+
warnSidecarFailure(logger, 'consolidate', 'get', `${file} (CROSS_REFERENCE gate)`, error);
|
|
532
|
+
}
|
|
512
533
|
}
|
|
513
534
|
return false;
|
|
514
535
|
}
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
* Never throws — returns empty array on errors.
|
|
13
13
|
*/
|
|
14
14
|
import type { ICipherAgent } from '../../../../agent/core/interfaces/i-cipher-agent.js';
|
|
15
|
+
import type { ILogger } from '../../../../agent/core/interfaces/i-logger.js';
|
|
15
16
|
import type { DreamOperation } from '../dream-log-schema.js';
|
|
16
17
|
import type { DreamState } from '../dream-state-schema.js';
|
|
17
18
|
export type PruneDeps = {
|
|
@@ -31,10 +32,27 @@ export type PruneDeps = {
|
|
|
31
32
|
update(updater: (state: DreamState) => DreamState): Promise<DreamState>;
|
|
32
33
|
write(state: DreamState): Promise<void>;
|
|
33
34
|
};
|
|
35
|
+
/**
|
|
36
|
+
* Optional logger. When provided, sidecar failures in the candidate scan
|
|
37
|
+
* emit a warn so the fail-open degradation is visible.
|
|
38
|
+
*/
|
|
39
|
+
logger?: ILogger;
|
|
34
40
|
projectRoot: string;
|
|
35
41
|
reviewBackupStore?: {
|
|
36
42
|
save(relativePath: string, content: string): Promise<void>;
|
|
37
43
|
};
|
|
44
|
+
/**
|
|
45
|
+
* Runtime-signal sidecar. Source of truth for `importance` and `maturity`
|
|
46
|
+
* used in prune's candidacy decisions. Absent store or missing-per-path
|
|
47
|
+
* entries are treated as defaults (importance 50, maturity 'draft') —
|
|
48
|
+
* matches the plan's "paths without entries use defaults" principle.
|
|
49
|
+
*/
|
|
50
|
+
runtimeSignalStore?: {
|
|
51
|
+
list(): Promise<Map<string, {
|
|
52
|
+
importance: number;
|
|
53
|
+
maturity: 'core' | 'draft' | 'validated';
|
|
54
|
+
}>>;
|
|
55
|
+
};
|
|
38
56
|
signal?: AbortSignal;
|
|
39
57
|
taskId: string;
|
|
40
58
|
};
|
|
@@ -13,6 +13,8 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import { readdir, readFile, stat, utimes } from 'node:fs/promises';
|
|
15
15
|
import { join } from 'node:path';
|
|
16
|
+
import { DEFAULT_IMPORTANCE, DEFAULT_MATURITY } from '../../../core/domain/knowledge/runtime-signals-schema.js';
|
|
17
|
+
import { warnSidecarFailure } from '../../../core/domain/knowledge/sidecar-logging.js';
|
|
16
18
|
import { isExcludedFromSync } from '../../context-tree/derived-artifact.js';
|
|
17
19
|
import { toUnixPath } from '../../context-tree/path-utils.js';
|
|
18
20
|
import { PruneResponseSchema } from '../dream-response-schemas.js';
|
|
@@ -48,10 +50,25 @@ export async function prune(deps) {
|
|
|
48
50
|
async function findCandidates(deps) {
|
|
49
51
|
const candidateMap = new Map();
|
|
50
52
|
const now = Date.now();
|
|
53
|
+
// Source of truth for importance/maturity is the sidecar. Preload once per
|
|
54
|
+
// scan so per-path lookups are O(1) map reads instead of repeated regex
|
|
55
|
+
// passes over markdown content. On sidecar failure the map is empty and
|
|
56
|
+
// every path falls back to defaults (importance 50, maturity 'draft').
|
|
57
|
+
let signalsByPath;
|
|
58
|
+
try {
|
|
59
|
+
signalsByPath = deps.runtimeSignalStore ? await deps.runtimeSignalStore.list() : new Map();
|
|
60
|
+
}
|
|
61
|
+
catch (error) {
|
|
62
|
+
warnSidecarFailure(deps.logger, 'prune', 'list', 'findCandidates', error);
|
|
63
|
+
signalsByPath = new Map();
|
|
64
|
+
}
|
|
51
65
|
// Signal A: archive service importance decay
|
|
52
66
|
try {
|
|
53
67
|
const importancePaths = await deps.archiveService.findArchiveCandidates(deps.projectRoot);
|
|
54
|
-
const infoResults = await Promise.all(importancePaths.map(async (path) => ({
|
|
68
|
+
const infoResults = await Promise.all(importancePaths.map(async (path) => ({
|
|
69
|
+
info: await readCandidateInfo(deps.contextTreeDir, path, now, signalsByPath),
|
|
70
|
+
path,
|
|
71
|
+
})));
|
|
55
72
|
for (const { info, path } of infoResults) {
|
|
56
73
|
if (info && info.maturity !== 'core') {
|
|
57
74
|
candidateMap.set(path, { ...info, signal: 'importance' });
|
|
@@ -63,7 +80,7 @@ async function findCandidates(deps) {
|
|
|
63
80
|
}
|
|
64
81
|
// Signal B: mtime staleness
|
|
65
82
|
try {
|
|
66
|
-
const stalePaths = await findStaleFiles(deps.contextTreeDir, now);
|
|
83
|
+
const stalePaths = await findStaleFiles(deps.contextTreeDir, now, signalsByPath);
|
|
67
84
|
for (const { info, path } of stalePaths) {
|
|
68
85
|
if (candidateMap.has(path)) {
|
|
69
86
|
// Already found by Signal A — mark as both
|
|
@@ -84,16 +101,16 @@ async function findCandidates(deps) {
|
|
|
84
101
|
candidates.sort((a, b) => b.daysSinceModified - a.daysSinceModified);
|
|
85
102
|
return candidates.slice(0, MAX_CANDIDATES);
|
|
86
103
|
}
|
|
87
|
-
async function readCandidateInfo(contextTreeDir, relativePath, now) {
|
|
104
|
+
async function readCandidateInfo(contextTreeDir, relativePath, now, signalsByPath) {
|
|
88
105
|
try {
|
|
89
106
|
const fullPath = join(contextTreeDir, relativePath);
|
|
90
|
-
const content = await readFile(fullPath, 'utf8');
|
|
91
107
|
const fileStat = await stat(fullPath);
|
|
92
108
|
const daysSinceModified = (now - fileStat.mtimeMs) / MS_PER_DAY;
|
|
109
|
+
const signals = signalsByPath.get(relativePath);
|
|
93
110
|
return {
|
|
94
111
|
daysSinceModified,
|
|
95
|
-
importance:
|
|
96
|
-
maturity:
|
|
112
|
+
importance: signals?.importance ?? DEFAULT_IMPORTANCE,
|
|
113
|
+
maturity: signals?.maturity ?? DEFAULT_MATURITY,
|
|
97
114
|
path: relativePath,
|
|
98
115
|
signal: 'importance',
|
|
99
116
|
};
|
|
@@ -102,13 +119,16 @@ async function readCandidateInfo(contextTreeDir, relativePath, now) {
|
|
|
102
119
|
return undefined;
|
|
103
120
|
}
|
|
104
121
|
}
|
|
105
|
-
async function findStaleFiles(contextTreeDir, now) {
|
|
122
|
+
async function findStaleFiles(contextTreeDir, now, signalsByPath) {
|
|
106
123
|
const results = [];
|
|
107
124
|
await walkMdFiles(contextTreeDir, async (relativePath, fullPath) => {
|
|
108
125
|
try {
|
|
109
|
-
const
|
|
110
|
-
const maturity =
|
|
111
|
-
// core files NEVER pruned
|
|
126
|
+
const signals = signalsByPath.get(relativePath);
|
|
127
|
+
const maturity = signals?.maturity ?? DEFAULT_MATURITY;
|
|
128
|
+
// core files NEVER pruned. Absent sidecar entry means maturity defaults
|
|
129
|
+
// to 'draft', so core protection depends on the sidecar being populated.
|
|
130
|
+
// That is intentional: post-migration, a file is only 'core' when the
|
|
131
|
+
// maturity tier has been earned via repeated access / curate updates.
|
|
112
132
|
if (maturity === 'core')
|
|
113
133
|
return;
|
|
114
134
|
const threshold = maturity === 'validated' ? VALIDATED_STALE_DAYS : DRAFT_STALE_DAYS;
|
|
@@ -118,7 +138,7 @@ async function findStaleFiles(contextTreeDir, now) {
|
|
|
118
138
|
results.push({
|
|
119
139
|
info: {
|
|
120
140
|
daysSinceModified,
|
|
121
|
-
importance:
|
|
141
|
+
importance: signals?.importance ?? DEFAULT_IMPORTANCE,
|
|
122
142
|
maturity,
|
|
123
143
|
path: relativePath,
|
|
124
144
|
signal: 'mtime',
|
|
@@ -351,12 +371,3 @@ async function writePendingMerge(decision, deps) {
|
|
|
351
371
|
};
|
|
352
372
|
});
|
|
353
373
|
}
|
|
354
|
-
// ── Frontmatter helpers ────────────────────────────────────────────────────
|
|
355
|
-
function extractMaturity(content) {
|
|
356
|
-
const match = /^maturity:\s*['"]?(core|draft|validated)['"]?/m.exec(content);
|
|
357
|
-
return match?.[1] ?? 'draft';
|
|
358
|
-
}
|
|
359
|
-
function extractImportance(content) {
|
|
360
|
-
const match = /^importance:\s*(\d+(?:\.\d+)?)/m.exec(content);
|
|
361
|
-
return match ? Number.parseFloat(match[1]) : 50;
|
|
362
|
-
}
|
|
@@ -11,10 +11,23 @@
|
|
|
11
11
|
* Never throws — returns empty array on errors.
|
|
12
12
|
*/
|
|
13
13
|
import type { ICipherAgent } from '../../../../agent/core/interfaces/i-cipher-agent.js';
|
|
14
|
+
import type { ILogger } from '../../../../agent/core/interfaces/i-logger.js';
|
|
15
|
+
import type { IRuntimeSignalStore } from '../../../core/interfaces/storage/i-runtime-signal-store.js';
|
|
14
16
|
import type { DreamOperation } from '../dream-log-schema.js';
|
|
15
17
|
export type SynthesizeDeps = {
|
|
16
18
|
agent: ICipherAgent;
|
|
17
19
|
contextTreeDir: string;
|
|
20
|
+
/**
|
|
21
|
+
* Optional logger. When provided, sidecar seed failures emit a warn
|
|
22
|
+
* so the fail-open degradation is observable rather than silent.
|
|
23
|
+
*/
|
|
24
|
+
logger?: ILogger;
|
|
25
|
+
/**
|
|
26
|
+
* Optional sidecar store for runtime ranking signals. When provided,
|
|
27
|
+
* newly created synthesis files are seeded with default signals so
|
|
28
|
+
* ranking data lives in the sidecar rather than in markdown frontmatter.
|
|
29
|
+
*/
|
|
30
|
+
runtimeSignalStore?: IRuntimeSignalStore;
|
|
18
31
|
searchService: {
|
|
19
32
|
search(query: string, options?: {
|
|
20
33
|
limit?: number;
|
|
@@ -14,6 +14,8 @@ import { dump as yamlDump, load as yamlLoad } from 'js-yaml';
|
|
|
14
14
|
import { randomUUID } from 'node:crypto';
|
|
15
15
|
import { access, mkdir, readdir, readFile, rename, writeFile } from 'node:fs/promises';
|
|
16
16
|
import { dirname, join, resolve } from 'node:path';
|
|
17
|
+
import { createDefaultRuntimeSignals } from '../../../core/domain/knowledge/runtime-signals-schema.js';
|
|
18
|
+
import { warnSidecarFailure } from '../../../core/domain/knowledge/sidecar-logging.js';
|
|
17
19
|
import { isDescendantOf } from '../../../utils/path-utils.js';
|
|
18
20
|
import { SynthesizeResponseSchema } from '../dream-response-schemas.js';
|
|
19
21
|
import { parseDreamResponse } from '../parse-dream-response.js';
|
|
@@ -66,7 +68,7 @@ export async function synthesize(deps) {
|
|
|
66
68
|
for (const candidate of novel) {
|
|
67
69
|
try {
|
|
68
70
|
// eslint-disable-next-line no-await-in-loop
|
|
69
|
-
const op = await writeSynthesisFile(candidate, contextTreeDir);
|
|
71
|
+
const op = await writeSynthesisFile(candidate, contextTreeDir, deps.runtimeSignalStore, deps.logger);
|
|
70
72
|
if (op)
|
|
71
73
|
results.push(op);
|
|
72
74
|
}
|
|
@@ -181,7 +183,7 @@ async function isDuplicateCandidate(candidate, existingSyntheses, searchService)
|
|
|
181
183
|
return false; // Search failure → assume novel
|
|
182
184
|
}
|
|
183
185
|
}
|
|
184
|
-
async function writeSynthesisFile(candidate, contextTreeDir) {
|
|
186
|
+
async function writeSynthesisFile(candidate, contextTreeDir, runtimeSignalStore, logger) {
|
|
185
187
|
const slug = slugify(candidate.title);
|
|
186
188
|
const relativePath = `${candidate.placement}/${slug}.md`;
|
|
187
189
|
const absPath = resolve(contextTreeDir, relativePath);
|
|
@@ -201,7 +203,6 @@ async function writeSynthesisFile(candidate, contextTreeDir) {
|
|
|
201
203
|
/* eslint-disable camelcase */
|
|
202
204
|
const frontmatter = {
|
|
203
205
|
confidence: candidate.confidence,
|
|
204
|
-
maturity: 'draft',
|
|
205
206
|
sources,
|
|
206
207
|
synthesized_at: new Date().toISOString(),
|
|
207
208
|
type: 'synthesis',
|
|
@@ -220,6 +221,17 @@ async function writeSynthesisFile(candidate, contextTreeDir) {
|
|
|
220
221
|
].join('\n');
|
|
221
222
|
const content = `---\n${yaml}\n---\n\n${body}`;
|
|
222
223
|
await atomicWrite(absPath, content);
|
|
224
|
+
// Seed the sidecar with default signals so ranking data lives in the
|
|
225
|
+
// sidecar rather than in markdown frontmatter. Best-effort — a sidecar
|
|
226
|
+
// failure must never prevent the synthesis file from being created.
|
|
227
|
+
if (runtimeSignalStore) {
|
|
228
|
+
try {
|
|
229
|
+
await runtimeSignalStore.set(relativePath, createDefaultRuntimeSignals());
|
|
230
|
+
}
|
|
231
|
+
catch (error) {
|
|
232
|
+
warnSidecarFailure(logger, 'synthesize', 'seed', relativePath, error);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
223
235
|
return {
|
|
224
236
|
action: 'CREATE',
|
|
225
237
|
confidence: candidate.confidence,
|