fluffy-context 0.5.0 → 0.7.2
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 +121 -9
- package/dist/src/agent/api.d.ts +5 -1
- package/dist/src/agent/api.js +41 -12
- package/dist/src/agent/index.d.ts +3 -1
- package/dist/src/agent/index.js +1 -0
- package/dist/src/agent/types.d.ts +21 -0
- package/dist/src/cli/main.js +150 -12
- package/dist/src/cognition/feedback.d.ts +9 -0
- package/dist/src/cognition/feedback.js +30 -0
- package/dist/src/cognition/filesystem-observer.d.ts +41 -0
- package/dist/src/cognition/filesystem-observer.js +164 -0
- package/dist/src/cognition/git-observer.d.ts +1 -0
- package/dist/src/cognition/git-observer.js +3 -2
- package/dist/src/cognition/journal.d.ts +4 -1
- package/dist/src/cognition/journal.js +83 -4
- package/dist/src/cognition/migration-v1.js +13 -11
- package/dist/src/cognition/orient.d.ts +4 -0
- package/dist/src/cognition/orient.js +14 -4
- package/dist/src/cognition/policy.d.ts +4 -0
- package/dist/src/cognition/policy.js +49 -0
- package/dist/src/cognition/projections.js +0 -0
- package/dist/src/cognition/retrieval.d.ts +26 -0
- package/dist/src/cognition/retrieval.js +273 -0
- package/dist/src/cognition/runtime.d.ts +20 -1
- package/dist/src/cognition/runtime.js +229 -53
- package/dist/src/cognition/types.d.ts +55 -1
- package/dist/src/cognition/v1-bridge.js +13 -4
- package/dist/src/compiler/activity.d.ts +3 -0
- package/dist/src/compiler/activity.js +79 -0
- package/dist/src/compiler/compile.d.ts +2 -0
- package/dist/src/compiler/compile.js +330 -0
- package/dist/src/compiler/sources.d.ts +19 -0
- package/dist/src/compiler/sources.js +130 -0
- package/dist/src/compiler/types.d.ts +187 -0
- package/dist/src/compiler/types.js +1 -0
- package/dist/src/hooks/claude-code.js +7 -15
- package/dist/src/mcp/main.js +2 -0
- package/dist/src/mcp/server.js +2 -0
- package/dist/src/runtime/knowledge.d.ts +2 -0
- package/dist/src/runtime/knowledge.js +63 -0
- package/dist/src/runtime/types.d.ts +19 -0
- package/dist/src/storage/layout.d.ts +2 -0
- package/dist/src/storage/layout.js +6 -0
- package/dist/src/version.d.ts +1 -1
- package/dist/src/version.js +1 -1
- package/package.json +2 -2
- package/skills/fluffy-context/SKILL.md +2 -1
package/dist/src/cli/main.js
CHANGED
|
@@ -1,14 +1,20 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { checkpoint, resume } from '../runtime/runtime.js';
|
|
3
3
|
import { contextOrient } from '../agent/api.js';
|
|
4
|
+
import { compileContext } from '../compiler/compile.js';
|
|
4
5
|
import { importV1, verifyV1Import } from '../cognition/migration-v1.js';
|
|
5
|
-
import {
|
|
6
|
+
import { recordUse } from '../cognition/feedback.js';
|
|
7
|
+
import { verifyJournal } from '../cognition/journal.js';
|
|
8
|
+
import { projectEvents } from '../cognition/projections.js';
|
|
9
|
+
import { disableRuntime, enableRuntime, ensureRuntimeForSession, runtimeStatus, serveRuntime, setFilesystemEvents, startRuntime, stopRuntime } from '../cognition/runtime.js';
|
|
10
|
+
import { loadRuntimePolicy } from '../cognition/policy.js';
|
|
6
11
|
import { migrationReadiness } from '../cognition/readiness.js';
|
|
7
12
|
import { runClaudeCodeHook } from '../hooks/claude-code.js';
|
|
13
|
+
import { readGitWorktreeState } from '../git/git-adapter.js';
|
|
8
14
|
import { inspectClaudeIntegration, installClaudeIntegration } from '../integrations/claude-code.js';
|
|
9
15
|
import { doctor, status } from '../runtime/diagnostics.js';
|
|
10
16
|
import { initProject } from '../runtime/init.js';
|
|
11
|
-
import { discoverKnowledge, learnKnowledge, listKnowledge, verifyKnowledge, recordDeadend, listDeadends, verifyDeadend } from '../runtime/knowledge.js';
|
|
17
|
+
import { discoverKnowledge, learnKnowledge, listKnowledge, verifyKnowledge, recordDeadend, listDeadends, verifyDeadend, transitionKnowledge, transitionDeadend } from '../runtime/knowledge.js';
|
|
12
18
|
import { addNote, listActivity, listNotes } from '../runtime/notes.js';
|
|
13
19
|
import { VERSION } from '../version.js';
|
|
14
20
|
function option(args, name) {
|
|
@@ -111,6 +117,21 @@ Options:
|
|
|
111
117
|
--knowledge-limit <number> Maximum knowledge matches (default: 10)
|
|
112
118
|
--deadend-limit <number> Maximum deadend matches (default: 10)
|
|
113
119
|
--note-limit <number> Maximum open notes (default: 20)`,
|
|
120
|
+
compile: `usage: ctx compile [query] [options]
|
|
121
|
+
|
|
122
|
+
Compile bounded, provider-neutral context with source provenance and selection explanations.
|
|
123
|
+
|
|
124
|
+
Options:
|
|
125
|
+
--path <path> Project path
|
|
126
|
+
--context <id> Context ID
|
|
127
|
+
--scenario <scenario> resume|implementation|debugging|verification|handoff|exploration|unknown
|
|
128
|
+
--scope <scope> Exact consensus scope
|
|
129
|
+
--paths <paths> Comma-separated project-relative paths
|
|
130
|
+
--max-chars <number> Maximum compiled characters
|
|
131
|
+
--knowledge-limit <number> Maximum knowledge matches (default: 10)
|
|
132
|
+
--deadend-limit <number> Maximum deadend matches (default: 10)
|
|
133
|
+
--note-limit <number> Maximum open notes (default: 20)
|
|
134
|
+
--activity-limit <number> Maximum recent activity groups (default: 20)`,
|
|
114
135
|
agent: `usage: ctx agent serve
|
|
115
136
|
|
|
116
137
|
Start an MCP stdio server for Agent integrations.
|
|
@@ -158,16 +179,23 @@ Options:
|
|
|
158
179
|
--evidence <items> Comma-separated evidence references`,
|
|
159
180
|
knowledge: `usage: ctx knowledge [--all] [--path <path>]
|
|
160
181
|
ctx knowledge verify <knowledge-id> [--path <path>]
|
|
182
|
+
ctx knowledge deprecate|reject <knowledge-id> --reason <text> [--path <path>]
|
|
161
183
|
ctx knowledge discover <query> [options]
|
|
162
184
|
|
|
163
|
-
List
|
|
185
|
+
List, govern, or discover project knowledge.
|
|
164
186
|
|
|
165
187
|
Options:
|
|
166
188
|
--all Include unverified items
|
|
167
189
|
--path <path> Project path`,
|
|
168
190
|
'knowledge verify': `usage: ctx knowledge verify <knowledge-id> [--path <path>]
|
|
169
191
|
|
|
170
|
-
Mark a knowledge item as verified.`,
|
|
192
|
+
Mark a candidate knowledge item as verified.`,
|
|
193
|
+
'knowledge deprecate': `usage: ctx knowledge deprecate <knowledge-id> --reason <text> [--path <path>]
|
|
194
|
+
|
|
195
|
+
Retire a verified knowledge item with an explicit governance reason.`,
|
|
196
|
+
'knowledge reject': `usage: ctx knowledge reject <knowledge-id> --reason <text> [--path <path>]
|
|
197
|
+
|
|
198
|
+
Reject a candidate knowledge item with an explicit governance reason.`,
|
|
171
199
|
'knowledge discover': `usage: ctx knowledge discover <query> [options]
|
|
172
200
|
|
|
173
201
|
Find matching project knowledge using deterministic lexical and alias rules.
|
|
@@ -181,8 +209,9 @@ Options:
|
|
|
181
209
|
--max-chars <number> Maximum text characters per match`,
|
|
182
210
|
deadend: `usage: ctx deadend [attempt] [options]
|
|
183
211
|
ctx deadend verify <deadend-id> [--path <path>]
|
|
212
|
+
ctx deadend obsolete|reject <deadend-id> --reason <text> [--path <path>]
|
|
184
213
|
|
|
185
|
-
Record or
|
|
214
|
+
Record or govern a candidate deadend.
|
|
186
215
|
|
|
187
216
|
Options:
|
|
188
217
|
--path <path> Project path
|
|
@@ -195,7 +224,25 @@ Options:
|
|
|
195
224
|
--evidence <items> Comma-separated evidence references`,
|
|
196
225
|
'deadend verify': `usage: ctx deadend verify <deadend-id> [--path <path>]
|
|
197
226
|
|
|
198
|
-
Mark a deadend as verified.`,
|
|
227
|
+
Mark a candidate deadend as verified.`,
|
|
228
|
+
'deadend obsolete': `usage: ctx deadend obsolete <deadend-id> --reason <text> [--path <path>]
|
|
229
|
+
|
|
230
|
+
Retire a verified deadend with an explicit governance reason.`,
|
|
231
|
+
'deadend reject': `usage: ctx deadend reject <deadend-id> --reason <text> [--path <path>]
|
|
232
|
+
|
|
233
|
+
Reject a candidate deadend with an explicit governance reason.`,
|
|
234
|
+
feedback: `usage: ctx feedback use knowledge|deadend <id> --event-id <stable-id> [--path <path>]
|
|
235
|
+
|
|
236
|
+
Record explicit use of verified v2 retrieval material.`,
|
|
237
|
+
'feedback use': `usage: ctx feedback use knowledge|deadend <id> --event-id <stable-id> [--path <path>]
|
|
238
|
+
|
|
239
|
+
Record an idempotent use signal that affects deterministic rank only.`,
|
|
240
|
+
journal: `usage: ctx journal verify [--path <path>]
|
|
241
|
+
|
|
242
|
+
Validate the v2 journal hash chain and replay projections in memory.`,
|
|
243
|
+
'journal verify': `usage: ctx journal verify [--path <path>]
|
|
244
|
+
|
|
245
|
+
Validate journal integrity without writing derived state.`,
|
|
199
246
|
deadends: `usage: ctx deadends [--all] [--path <path>]
|
|
200
247
|
|
|
201
248
|
List recorded deadends.`,
|
|
@@ -259,6 +306,18 @@ Options:
|
|
|
259
306
|
runtime: `usage: ctx runtime enable|disable|start|stop|status [--path <path>]
|
|
260
307
|
|
|
261
308
|
Control the local event-driven Knowledge Runtime after v1 migration verification.`,
|
|
309
|
+
filesystem: `usage: ctx filesystem enable|disable|status [--path <path>]
|
|
310
|
+
|
|
311
|
+
Control whether the Runtime records filtered file metadata changes.`,
|
|
312
|
+
'filesystem enable': `usage: ctx filesystem enable [--path <path>]
|
|
313
|
+
|
|
314
|
+
Enable filtered filesystem change observation and restart a running Runtime.`,
|
|
315
|
+
'filesystem disable': `usage: ctx filesystem disable [--path <path>]
|
|
316
|
+
|
|
317
|
+
Disable filesystem change observation and restart a running Runtime.`,
|
|
318
|
+
'filesystem status': `usage: ctx filesystem status [--path <path>]
|
|
319
|
+
|
|
320
|
+
Show the filesystem observation policy and Runtime status.`,
|
|
262
321
|
'runtime status': `usage: ctx runtime status [--path <path>]
|
|
263
322
|
|
|
264
323
|
Show the read-only runtime policy, migration and authenticated lease status.
|
|
@@ -282,7 +341,7 @@ Stop the local runtime through authenticated loopback IPC.`,
|
|
|
282
341
|
Internal runtime daemon entry point.`,
|
|
283
342
|
};
|
|
284
343
|
function usage() {
|
|
285
|
-
return `usage: ctx init|checkpoint|resume|orient|agent|hook|integrate|status|doctor|learn|knowledge|deadend|deadends|note|activity|migrate|runtime [options]
|
|
344
|
+
return `usage: ctx init|checkpoint|resume|orient|compile|agent|hook|integrate|status|doctor|learn|knowledge|deadend|deadends|note|activity|migrate|runtime|filesystem|feedback|journal [options]
|
|
286
345
|
|
|
287
346
|
Run \"ctx <command> --help\" for command details.`;
|
|
288
347
|
}
|
|
@@ -304,11 +363,14 @@ function printHelp(args) {
|
|
|
304
363
|
const nested = (command === 'agent' && args[1] === 'serve')
|
|
305
364
|
|| (command === 'hook' && args[1] === 'claude-code')
|
|
306
365
|
|| (command === 'integrate' && args[1] === 'claude')
|
|
307
|
-
|| (command === 'knowledge' && ['verify', 'discover'].includes(args[1]))
|
|
308
|
-
|| (command === 'deadend' &&
|
|
366
|
+
|| (command === 'knowledge' && ['verify', 'discover', 'deprecate', 'reject'].includes(args[1]))
|
|
367
|
+
|| (command === 'deadend' && ['verify', 'obsolete', 'reject'].includes(args[1]))
|
|
368
|
+
|| (command === 'feedback' && args[1] === 'use')
|
|
369
|
+
|| (command === 'journal' && args[1] === 'verify')
|
|
309
370
|
|| (command === 'note' && ['add', 'list'].includes(args[1]))
|
|
310
371
|
|| (command === 'migrate' && ['v1', 'verify'].includes(args[1]))
|
|
311
|
-
|| (command === 'runtime' && ['enable', 'disable', 'start', 'stop', 'status', 'serve'].includes(args[1]))
|
|
372
|
+
|| (command === 'runtime' && ['enable', 'disable', 'start', 'stop', 'status', 'serve'].includes(args[1]))
|
|
373
|
+
|| (command === 'filesystem' && ['enable', 'disable', 'status'].includes(args[1]));
|
|
312
374
|
const key = nested ? `${command} ${args[1]}` : command;
|
|
313
375
|
const nestedKey = key === 'integrate claude' && (args[2] === 'inspect' || args[2] === 'install') ? `${key} ${args[2]}` : key;
|
|
314
376
|
const help = HELP[nestedKey];
|
|
@@ -373,6 +435,7 @@ async function run(args) {
|
|
|
373
435
|
validateOptions(args.slice(1), valueOptions, valueOptions);
|
|
374
436
|
const query = positionals(args.slice(1), valueOptions);
|
|
375
437
|
validatePositionals(query, 1, HELP.orient);
|
|
438
|
+
await ensureRuntimeForSession(target, { source: 'cli', timeoutMs: 1_000 }).catch(() => undefined);
|
|
376
439
|
print(await contextOrient(target, {
|
|
377
440
|
contextId: option(args, '--context'),
|
|
378
441
|
query: query[0],
|
|
@@ -384,6 +447,25 @@ async function run(args) {
|
|
|
384
447
|
}));
|
|
385
448
|
return;
|
|
386
449
|
}
|
|
450
|
+
case 'compile': {
|
|
451
|
+
const valueOptions = ['--path', '--context', '--scenario', '--scope', '--paths', '--max-chars', '--knowledge-limit', '--deadend-limit', '--note-limit', '--activity-limit'];
|
|
452
|
+
validateOptions(args.slice(1), valueOptions, valueOptions);
|
|
453
|
+
const query = positionals(args.slice(1), valueOptions);
|
|
454
|
+
validatePositionals(query, 1, HELP.compile);
|
|
455
|
+
print(await compileContext(target, {
|
|
456
|
+
contextId: option(args, '--context'),
|
|
457
|
+
query: query[0],
|
|
458
|
+
scenario: enumOption(args, '--scenario', ['resume', 'implementation', 'debugging', 'verification', 'handoff', 'exploration', 'unknown']),
|
|
459
|
+
scope: option(args, '--scope'),
|
|
460
|
+
paths: listOption(args, '--paths'),
|
|
461
|
+
maxChars: numericOption(args, '--max-chars', 4000),
|
|
462
|
+
knowledgeLimit: numericOption(args, '--knowledge-limit', 10),
|
|
463
|
+
deadendLimit: numericOption(args, '--deadend-limit', 10),
|
|
464
|
+
noteLimit: numericOption(args, '--note-limit', 20),
|
|
465
|
+
activityLimit: numericOption(args, '--activity-limit', 20),
|
|
466
|
+
}));
|
|
467
|
+
return;
|
|
468
|
+
}
|
|
387
469
|
case 'agent':
|
|
388
470
|
if (args[1] !== 'serve')
|
|
389
471
|
throw new Error(HELP.agent);
|
|
@@ -423,17 +505,20 @@ async function run(args) {
|
|
|
423
505
|
print(await doctor(target));
|
|
424
506
|
return;
|
|
425
507
|
case 'learn': {
|
|
426
|
-
const valueOptions = ['--path', '--kind', '--scope', '--context', '--snapshot', '--evidence'];
|
|
508
|
+
const valueOptions = ['--path', '--kind', '--scope', '--context', '--snapshot', '--evidence', '--branch-mode', '--paths'];
|
|
427
509
|
validateOptions(args.slice(1), valueOptions, valueOptions);
|
|
428
510
|
const statement = positionals(args.slice(1), valueOptions);
|
|
429
511
|
if (statement.length === 0)
|
|
430
512
|
throw new Error(HELP.learn);
|
|
513
|
+
const branchMode = enumOption(args, '--branch-mode', ['global', 'exact']) ?? 'global';
|
|
514
|
+
const git = branchMode === 'exact' ? await readGitWorktreeState(target ?? process.cwd()) : null;
|
|
431
515
|
const input = {
|
|
432
516
|
kind: option(args, '--kind'),
|
|
433
517
|
scope: option(args, '--scope'),
|
|
434
518
|
sourceContextId: option(args, '--context'),
|
|
435
519
|
sourceSnapshotId: option(args, '--snapshot'),
|
|
436
520
|
evidence: listOption(args, '--evidence'),
|
|
521
|
+
applicability: { project: true, branchMode, ...(branchMode === 'exact' ? { branches: [git?.ref ?? git?.branch ?? 'detached'], sourceCommit: git?.commit ?? null } : {}), ...(listOption(args, '--paths') ? { paths: listOption(args, '--paths') } : {}) },
|
|
437
522
|
};
|
|
438
523
|
print(await learnKnowledge(target, statement.join(' '), input));
|
|
439
524
|
return;
|
|
@@ -466,6 +551,42 @@ async function run(args) {
|
|
|
466
551
|
}
|
|
467
552
|
return;
|
|
468
553
|
}
|
|
554
|
+
case 'feedback': {
|
|
555
|
+
if (args[1] !== 'use' || !['knowledge', 'deadend'].includes(args[2]))
|
|
556
|
+
throw new Error(HELP.feedback);
|
|
557
|
+
validateOptions(args.slice(3), ['--path', '--event-id'], ['--path', '--event-id']);
|
|
558
|
+
const values = positionals(args.slice(3), ['--path', '--event-id']);
|
|
559
|
+
if (values.length !== 1)
|
|
560
|
+
throw new Error(HELP['feedback use']);
|
|
561
|
+
print(await recordUse(target, { entity: args[2], entityId: values[0], eventId: option(args, '--event-id') ?? '' }));
|
|
562
|
+
return;
|
|
563
|
+
}
|
|
564
|
+
case 'filesystem': {
|
|
565
|
+
if (!['enable', 'disable', 'status'].includes(args[1]))
|
|
566
|
+
throw new Error(HELP.filesystem);
|
|
567
|
+
validateOptions(args.slice(2), ['--path'], ['--path']);
|
|
568
|
+
validatePositionals(positionals(args.slice(2), ['--path']), 0, HELP[`filesystem ${args[1]}`]);
|
|
569
|
+
if (args[1] === 'status') {
|
|
570
|
+
const status = await runtimeStatus(target);
|
|
571
|
+
print({ projectRoot: status.projectRoot, migrated: status.migrated, enabled: status.enabled, running: status.running, filesystemEventsEnabled: status.migrated ? (await loadRuntimePolicy(status.projectRoot)).filesystemEventsEnabled : false });
|
|
572
|
+
}
|
|
573
|
+
else {
|
|
574
|
+
print(await setFilesystemEvents(target, args[1] === 'enable'));
|
|
575
|
+
}
|
|
576
|
+
return;
|
|
577
|
+
}
|
|
578
|
+
case 'journal': {
|
|
579
|
+
if (args[1] !== 'verify')
|
|
580
|
+
throw new Error(HELP.journal);
|
|
581
|
+
validateOptions(args.slice(2), ['--path'], ['--path']);
|
|
582
|
+
validatePositionals(positionals(args.slice(2), ['--path']), 0, HELP['journal verify']);
|
|
583
|
+
const root = target ?? process.cwd();
|
|
584
|
+
const events = await verifyJournal(root);
|
|
585
|
+
const projection = projectEvents(events);
|
|
586
|
+
const last = events.at(-1);
|
|
587
|
+
print({ verified: true, eventCount: events.length, cursor: { eventId: last?.eventId ?? null, hash: last?.hash ?? null }, counts: { contexts: Object.keys(projection.contexts).length, notes: Object.keys(projection.notes).length, knowledge: Object.keys(projection.knowledge).length, deadends: Object.keys(projection.deadends).length } });
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
469
590
|
case 'migrate': {
|
|
470
591
|
if (args[1] === 'v1') {
|
|
471
592
|
validateOptions(args.slice(2), ['--dry-run', '--apply', '--path'], ['--path']);
|
|
@@ -545,6 +666,13 @@ async function run(args) {
|
|
|
545
666
|
throw new Error(HELP['knowledge verify']);
|
|
546
667
|
print(await verifyKnowledge(target, values[0]));
|
|
547
668
|
}
|
|
669
|
+
else if (args[1] === 'deprecate' || args[1] === 'reject') {
|
|
670
|
+
validateOptions(args.slice(2), ['--path', '--reason'], ['--path', '--reason']);
|
|
671
|
+
const values = positionals(args.slice(2), ['--path', '--reason']);
|
|
672
|
+
if (values.length !== 1)
|
|
673
|
+
throw new Error(HELP[`knowledge ${args[1]}`]);
|
|
674
|
+
print(await transitionKnowledge(target, values[0], args[1] === 'deprecate' ? 'deprecated' : 'rejected', option(args, '--reason') ?? ''));
|
|
675
|
+
}
|
|
548
676
|
else {
|
|
549
677
|
validateOptions(args.slice(1), ['--all', '--path'], ['--path']);
|
|
550
678
|
validatePositionals(positionals(args.slice(1), ['--path']), 0, HELP.knowledge);
|
|
@@ -560,16 +688,26 @@ async function run(args) {
|
|
|
560
688
|
throw new Error(HELP['deadend verify']);
|
|
561
689
|
print(await verifyDeadend(target, values[0]));
|
|
562
690
|
}
|
|
691
|
+
else if (args[1] === 'obsolete' || args[1] === 'reject') {
|
|
692
|
+
validateOptions(args.slice(2), ['--path', '--reason'], ['--path', '--reason']);
|
|
693
|
+
const values = positionals(args.slice(2), ['--path', '--reason']);
|
|
694
|
+
if (values.length !== 1)
|
|
695
|
+
throw new Error(HELP[`deadend ${args[1]}`]);
|
|
696
|
+
print(await transitionDeadend(target, values[0], args[1] === 'obsolete' ? 'obsolete' : 'rejected', option(args, '--reason') ?? ''));
|
|
697
|
+
}
|
|
563
698
|
else {
|
|
564
|
-
const valueOptions = ['--path', '--attempt', '--reason', '-m', '--scope', '--context', '--snapshot', '--evidence'];
|
|
699
|
+
const valueOptions = ['--path', '--attempt', '--reason', '-m', '--scope', '--context', '--snapshot', '--evidence', '--branch-mode', '--paths'];
|
|
565
700
|
validateOptions(args.slice(1), valueOptions, valueOptions);
|
|
566
701
|
const values = positionals(args.slice(1), valueOptions);
|
|
567
702
|
validatePositionals(values, 1, HELP.deadend);
|
|
703
|
+
const branchMode = enumOption(args, '--branch-mode', ['global', 'exact']) ?? 'global';
|
|
704
|
+
const git = branchMode === 'exact' ? await readGitWorktreeState(target ?? process.cwd()) : null;
|
|
568
705
|
const input = {
|
|
569
706
|
scope: option(args, '--scope'),
|
|
570
707
|
sourceContextId: option(args, '--context'),
|
|
571
708
|
sourceSnapshotId: option(args, '--snapshot'),
|
|
572
709
|
evidence: listOption(args, '--evidence'),
|
|
710
|
+
applicability: { project: true, branchMode, ...(branchMode === 'exact' ? { branches: [git?.ref ?? git?.branch ?? 'detached'], sourceCommit: git?.commit ?? null } : {}), ...(listOption(args, '--paths') ? { paths: listOption(args, '--paths') } : {}) },
|
|
573
711
|
};
|
|
574
712
|
print(await recordDeadend(target, option(args, '--attempt') ?? values[0], option(args, '--reason') ?? option(args, '-m') ?? '', input));
|
|
575
713
|
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { resolveProjectRoot } from '../project/project-resolver.js';
|
|
2
|
+
import { readGitWorktreeState } from '../git/git-adapter.js';
|
|
3
|
+
import { appendEvent, readJournal } from './journal.js';
|
|
4
|
+
import { v2Enabled } from './orient.js';
|
|
5
|
+
import { rebuildProjections } from './projections.js';
|
|
6
|
+
import { buildRetrievalProjection } from './retrieval.js';
|
|
7
|
+
export async function recordUse(startPath, input) {
|
|
8
|
+
const projectRoot = await resolveProjectRoot(startPath);
|
|
9
|
+
if (!input.eventId.trim())
|
|
10
|
+
throw new Error('feedback event ID must not be empty');
|
|
11
|
+
if (!await v2Enabled(projectRoot))
|
|
12
|
+
throw new Error('record use requires an enabled v2 runtime');
|
|
13
|
+
const events = await readJournal(projectRoot);
|
|
14
|
+
const entry = buildRetrievalProjection(events).entries[`${input.entity}:${input.entityId}`];
|
|
15
|
+
if (!entry)
|
|
16
|
+
throw new Error(`${input.entity} not found in v2 retrieval: ${input.entityId}`);
|
|
17
|
+
if (entry.status !== 'verified')
|
|
18
|
+
throw new Error(`record use requires verified ${input.entity}: ${input.entityId}`);
|
|
19
|
+
const git = await readGitWorktreeState(projectRoot);
|
|
20
|
+
const result = await appendEvent(projectRoot, {
|
|
21
|
+
type: 'record.used',
|
|
22
|
+
source: { kind: 'cli' },
|
|
23
|
+
idempotencyKey: `record-use:${input.entity}:${input.entityId}:${input.eventId.trim()}`,
|
|
24
|
+
git: { worktreeId: git.worktreeId, branch: git.branch, ref: git.ref, head: git.commit },
|
|
25
|
+
payload: { feedbackId: input.eventId.trim(), entity: input.entity, entityId: input.entityId, outcome: 'used' },
|
|
26
|
+
});
|
|
27
|
+
if (result.appended)
|
|
28
|
+
await rebuildProjections(projectRoot);
|
|
29
|
+
return { recorded: result.appended, eventId: result.event.eventId };
|
|
30
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { CognitionGitAnchor } from './types.js';
|
|
2
|
+
export type WatchCallback = (eventType: string, filename: string | Buffer | null) => void;
|
|
3
|
+
export type WatchFactory = (projectRoot: string, callback: WatchCallback) => {
|
|
4
|
+
close(): void;
|
|
5
|
+
};
|
|
6
|
+
export interface FilesystemObserverOptions {
|
|
7
|
+
anchor?: () => CognitionGitAnchor;
|
|
8
|
+
debounceMs?: number;
|
|
9
|
+
maxPaths?: number;
|
|
10
|
+
maxPayloadBytes?: number;
|
|
11
|
+
watchFactory?: WatchFactory;
|
|
12
|
+
onChanged?: (result: FilesystemObservationResult) => void;
|
|
13
|
+
}
|
|
14
|
+
export interface FilesystemObservationResult {
|
|
15
|
+
changed: boolean;
|
|
16
|
+
paths: string[];
|
|
17
|
+
}
|
|
18
|
+
export declare class FilesystemObserver {
|
|
19
|
+
private readonly projectRoot;
|
|
20
|
+
private readonly debounceMs;
|
|
21
|
+
private readonly maxPaths;
|
|
22
|
+
private readonly maxPayloadBytes;
|
|
23
|
+
private readonly anchor;
|
|
24
|
+
private readonly watchFactory;
|
|
25
|
+
private readonly onChanged;
|
|
26
|
+
private readonly pendingPaths;
|
|
27
|
+
private readonly known;
|
|
28
|
+
private watcher;
|
|
29
|
+
private timer;
|
|
30
|
+
private pending;
|
|
31
|
+
private closed;
|
|
32
|
+
constructor(projectRoot: string, options?: FilesystemObserverOptions);
|
|
33
|
+
start(): void;
|
|
34
|
+
stop(): void;
|
|
35
|
+
observeNow(): Promise<FilesystemObservationResult>;
|
|
36
|
+
flush(): Promise<FilesystemObservationResult>;
|
|
37
|
+
drain(): Promise<void>;
|
|
38
|
+
close(): void;
|
|
39
|
+
private scheduleFlush;
|
|
40
|
+
private process;
|
|
41
|
+
}
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { watch } from 'node:fs';
|
|
2
|
+
import { stat } from 'node:fs/promises';
|
|
3
|
+
import { resolve } from 'node:path';
|
|
4
|
+
import { filterPaths } from '../capture/context-filter.js';
|
|
5
|
+
import { appendEvent } from './journal.js';
|
|
6
|
+
const defaultDebounceMs = 250;
|
|
7
|
+
const defaultMaxPaths = 100;
|
|
8
|
+
const defaultMaxPayloadBytes = 16 * 1024;
|
|
9
|
+
function defaultWatchFactory(projectRoot, callback) {
|
|
10
|
+
return watch(projectRoot, { recursive: true }, callback);
|
|
11
|
+
}
|
|
12
|
+
function normalizeCandidate(projectRoot, filename) {
|
|
13
|
+
if (filename === null)
|
|
14
|
+
return null;
|
|
15
|
+
const value = filename.toString().replaceAll('\\', '/').replace(/^\.\//, '');
|
|
16
|
+
if (!value || value.startsWith('/') || /^[A-Za-z]:\//.test(value))
|
|
17
|
+
return null;
|
|
18
|
+
const absolute = resolve(projectRoot, value);
|
|
19
|
+
const relative = absolute.slice(resolve(projectRoot).length).replaceAll('\\', '/').replace(/^\/+/, '');
|
|
20
|
+
return relative || null;
|
|
21
|
+
}
|
|
22
|
+
function metadataEqual(left, right) {
|
|
23
|
+
return left?.sizeBytes === right.sizeBytes && left.mtimeMs === right.mtimeMs;
|
|
24
|
+
}
|
|
25
|
+
function payloadSize(payload) {
|
|
26
|
+
return Buffer.byteLength(JSON.stringify(payload), 'utf8');
|
|
27
|
+
}
|
|
28
|
+
export class FilesystemObserver {
|
|
29
|
+
projectRoot;
|
|
30
|
+
debounceMs;
|
|
31
|
+
maxPaths;
|
|
32
|
+
maxPayloadBytes;
|
|
33
|
+
anchor;
|
|
34
|
+
watchFactory;
|
|
35
|
+
onChanged;
|
|
36
|
+
pendingPaths = new Map();
|
|
37
|
+
known = new Map();
|
|
38
|
+
watcher = null;
|
|
39
|
+
timer = null;
|
|
40
|
+
pending = Promise.resolve({ changed: false, paths: [] });
|
|
41
|
+
closed = false;
|
|
42
|
+
constructor(projectRoot, options = {}) {
|
|
43
|
+
this.projectRoot = projectRoot;
|
|
44
|
+
this.debounceMs = options.debounceMs ?? defaultDebounceMs;
|
|
45
|
+
this.maxPaths = options.maxPaths ?? defaultMaxPaths;
|
|
46
|
+
this.maxPayloadBytes = options.maxPayloadBytes ?? defaultMaxPayloadBytes;
|
|
47
|
+
this.anchor = options.anchor ?? (() => ({ worktreeId: null, branch: null, ref: null, head: null }));
|
|
48
|
+
this.watchFactory = options.watchFactory ?? defaultWatchFactory;
|
|
49
|
+
this.onChanged = options.onChanged;
|
|
50
|
+
}
|
|
51
|
+
start() {
|
|
52
|
+
if (this.closed || this.watcher)
|
|
53
|
+
return;
|
|
54
|
+
this.watcher = this.watchFactory(this.projectRoot, (eventType, filename) => {
|
|
55
|
+
const relative = normalizeCandidate(this.projectRoot, filename);
|
|
56
|
+
if (relative)
|
|
57
|
+
this.pendingPaths.set(relative, eventType);
|
|
58
|
+
this.scheduleFlush();
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
stop() {
|
|
62
|
+
if (this.timer)
|
|
63
|
+
clearTimeout(this.timer);
|
|
64
|
+
this.timer = null;
|
|
65
|
+
this.watcher?.close();
|
|
66
|
+
this.watcher = null;
|
|
67
|
+
this.closed = true;
|
|
68
|
+
}
|
|
69
|
+
observeNow() {
|
|
70
|
+
return this.flush();
|
|
71
|
+
}
|
|
72
|
+
async flush() {
|
|
73
|
+
if (this.timer)
|
|
74
|
+
clearTimeout(this.timer);
|
|
75
|
+
this.timer = null;
|
|
76
|
+
const paths = [...this.pendingPaths.entries()];
|
|
77
|
+
this.pendingPaths.clear();
|
|
78
|
+
if (paths.length === 0)
|
|
79
|
+
return { changed: false, paths: [] };
|
|
80
|
+
const operation = this.pending.then(() => this.process(paths), () => this.process(paths));
|
|
81
|
+
this.pending = operation;
|
|
82
|
+
return operation;
|
|
83
|
+
}
|
|
84
|
+
async drain() {
|
|
85
|
+
await this.pending;
|
|
86
|
+
if (this.pendingPaths.size > 0)
|
|
87
|
+
await this.flush();
|
|
88
|
+
await this.pending;
|
|
89
|
+
}
|
|
90
|
+
close() {
|
|
91
|
+
this.stop();
|
|
92
|
+
}
|
|
93
|
+
scheduleFlush(delay = this.debounceMs) {
|
|
94
|
+
if (this.timer || this.closed)
|
|
95
|
+
return;
|
|
96
|
+
this.timer = setTimeout(() => {
|
|
97
|
+
this.timer = null;
|
|
98
|
+
void this.flush().catch(() => undefined);
|
|
99
|
+
}, delay);
|
|
100
|
+
}
|
|
101
|
+
async process(paths) {
|
|
102
|
+
const filtered = await filterPaths(this.projectRoot, paths.map(([relative]) => relative));
|
|
103
|
+
const eventTypes = new Map(paths);
|
|
104
|
+
const changes = [];
|
|
105
|
+
for (const relative of filtered.slice(0, this.maxPaths)) {
|
|
106
|
+
const absolute = resolve(this.projectRoot, relative);
|
|
107
|
+
let current = null;
|
|
108
|
+
try {
|
|
109
|
+
const value = await stat(absolute);
|
|
110
|
+
if (!value.isFile())
|
|
111
|
+
continue;
|
|
112
|
+
current = { sizeBytes: value.size, mtimeMs: value.mtimeMs };
|
|
113
|
+
}
|
|
114
|
+
catch {
|
|
115
|
+
current = null;
|
|
116
|
+
}
|
|
117
|
+
const previous = this.known.get(relative);
|
|
118
|
+
if (current && metadataEqual(previous, current))
|
|
119
|
+
continue;
|
|
120
|
+
if (!current && !previous)
|
|
121
|
+
continue;
|
|
122
|
+
if (current)
|
|
123
|
+
this.known.set(relative, current);
|
|
124
|
+
else
|
|
125
|
+
this.known.delete(relative);
|
|
126
|
+
changes.push({
|
|
127
|
+
path: relative,
|
|
128
|
+
kind: current ? (previous ? 'modified' : eventTypes.get(relative) === 'rename' ? 'created' : 'modified') : 'deleted',
|
|
129
|
+
fileType: 'file',
|
|
130
|
+
...(current ? { sizeBytes: current.sizeBytes, mtimeMs: current.mtimeMs } : {}),
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
if (changes.length === 0)
|
|
134
|
+
return { changed: false, paths: [] };
|
|
135
|
+
const limitedChanges = changes.slice(0, this.maxPaths);
|
|
136
|
+
const basePayload = {
|
|
137
|
+
schemaVersion: 1,
|
|
138
|
+
paths: limitedChanges.map((change) => change.path),
|
|
139
|
+
changes: limitedChanges,
|
|
140
|
+
pathCount: limitedChanges.length,
|
|
141
|
+
coalesced: paths.length > 1,
|
|
142
|
+
truncated: filtered.length > limitedChanges.length || payloadSize({ changes: limitedChanges }) > this.maxPayloadBytes,
|
|
143
|
+
};
|
|
144
|
+
while (basePayload.changes.length > 1 && payloadSize(basePayload) > this.maxPayloadBytes) {
|
|
145
|
+
basePayload.changes.pop();
|
|
146
|
+
basePayload.paths.pop();
|
|
147
|
+
basePayload.pathCount = basePayload.changes.length;
|
|
148
|
+
basePayload.truncated = true;
|
|
149
|
+
}
|
|
150
|
+
if (basePayload.changes.length === 0 || payloadSize(basePayload) > this.maxPayloadBytes)
|
|
151
|
+
return { changed: false, paths: [] };
|
|
152
|
+
const result = await appendEvent(this.projectRoot, {
|
|
153
|
+
type: 'workspace.paths.changed',
|
|
154
|
+
source: { kind: 'fs-observer' },
|
|
155
|
+
idempotencyKey: `fs:paths:${basePayload.paths.join(',')}:${basePayload.changes.map((change) => `${change.kind}:${change.sizeBytes ?? ''}:${change.mtimeMs ?? ''}`).join(',')}`,
|
|
156
|
+
git: this.anchor(),
|
|
157
|
+
payload: basePayload,
|
|
158
|
+
});
|
|
159
|
+
const observation = { changed: result.appended, paths: basePayload.paths };
|
|
160
|
+
if (observation.changed)
|
|
161
|
+
this.onChanged?.(observation);
|
|
162
|
+
return observation;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { readGitWorktreeState } from '../git/git-adapter.js';
|
|
2
2
|
import { appendEvent } from './journal.js';
|
|
3
|
-
import { rebuildProjections } from './projections.js';
|
|
4
3
|
function anchor(state) {
|
|
5
4
|
return { worktreeId: state.worktreeId, branch: state.branch, ref: state.ref, head: state.commit };
|
|
6
5
|
}
|
|
@@ -14,6 +13,9 @@ export class GitObserver {
|
|
|
14
13
|
constructor(projectRoot) {
|
|
15
14
|
this.projectRoot = projectRoot;
|
|
16
15
|
}
|
|
16
|
+
get current() {
|
|
17
|
+
return this.previous;
|
|
18
|
+
}
|
|
17
19
|
observeNow() {
|
|
18
20
|
const observation = this.pending.then(() => this.observe());
|
|
19
21
|
this.pending = observation.then(() => undefined, () => undefined);
|
|
@@ -55,7 +57,6 @@ export class GitObserver {
|
|
|
55
57
|
});
|
|
56
58
|
}
|
|
57
59
|
this.previous = state;
|
|
58
|
-
await rebuildProjections(this.projectRoot);
|
|
59
60
|
return { state, changed: true };
|
|
60
61
|
}
|
|
61
62
|
}
|
|
@@ -17,7 +17,10 @@ export interface AppendEventInput<T = Record<string, unknown>> {
|
|
|
17
17
|
payload: T;
|
|
18
18
|
}
|
|
19
19
|
export declare function eventHash<T>(event: Omit<CognitionEvent<T>, 'hash'>): string;
|
|
20
|
-
export declare function readJournal(startPath?: string
|
|
20
|
+
export declare function readJournal(startPath?: string, options?: {
|
|
21
|
+
allowPartialTail?: boolean;
|
|
22
|
+
}): Promise<CognitionEvent[]>;
|
|
23
|
+
export declare function verifyJournal(startPath?: string): Promise<CognitionEvent[]>;
|
|
21
24
|
export declare function appendEvent<T>(startPath: string | undefined, input: AppendEventInput<T>): Promise<{
|
|
22
25
|
event: CognitionEvent<T>;
|
|
23
26
|
appended: boolean;
|