fluffy-context 0.1.0 → 0.3.0

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.
Files changed (40) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +86 -136
  3. package/dist/src/agent/api.d.ts +8 -0
  4. package/dist/src/agent/api.js +285 -0
  5. package/dist/src/agent/index.d.ts +3 -0
  6. package/dist/src/agent/index.js +1 -0
  7. package/dist/src/agent/types.d.ts +50 -0
  8. package/dist/src/agent/types.js +1 -0
  9. package/dist/src/capture/context-filter.d.ts +7 -0
  10. package/dist/src/cli/main.d.ts +2 -0
  11. package/dist/src/cli/main.js +382 -30
  12. package/dist/src/git/git-adapter.d.ts +2 -0
  13. package/dist/src/hooks/claude-code.d.ts +1 -0
  14. package/dist/src/hooks/claude-code.js +84 -0
  15. package/dist/src/integrations/claude-code.d.ts +19 -0
  16. package/dist/src/integrations/claude-code.js +122 -0
  17. package/dist/src/mcp/main.d.ts +1 -0
  18. package/dist/src/mcp/main.js +5 -0
  19. package/dist/src/mcp/server.d.ts +2 -0
  20. package/dist/src/mcp/server.js +40 -0
  21. package/dist/src/project/project-resolver.d.ts +1 -0
  22. package/dist/src/runtime/diagnostics.d.ts +6 -0
  23. package/dist/src/runtime/diagnostics.js +34 -5
  24. package/dist/src/runtime/init.d.ts +7 -0
  25. package/dist/src/runtime/knowledge.d.ts +9 -0
  26. package/dist/src/runtime/knowledge.js +179 -12
  27. package/dist/src/runtime/notes.d.ts +6 -0
  28. package/dist/src/runtime/notes.js +173 -0
  29. package/dist/src/runtime/runtime.d.ts +11 -0
  30. package/dist/src/runtime/runtime.js +48 -18
  31. package/dist/src/runtime/types.d.ts +294 -0
  32. package/dist/src/storage/atomic-write.d.ts +1 -0
  33. package/dist/src/storage/json-store.d.ts +6 -0
  34. package/dist/src/storage/layout.d.ts +13 -0
  35. package/dist/src/storage/layout.js +3 -0
  36. package/dist/src/storage/lock.d.ts +1 -0
  37. package/dist/src/version.d.ts +1 -0
  38. package/dist/src/version.js +1 -0
  39. package/package.json +48 -11
  40. package/skills/fluffy-context/SKILL.md +345 -0
@@ -0,0 +1,50 @@
1
+ import type { ContextStatus, DeadendDiscoveryResult, KnowledgeDiscoveryResult, Note, ResumeSummary, SnapshotMode } from '../runtime/types.js';
2
+ export interface ContextOrientOptions {
3
+ contextId?: string;
4
+ query?: string;
5
+ scope?: string;
6
+ maxChars?: number;
7
+ knowledgeLimit?: number;
8
+ deadendLimit?: number;
9
+ noteLimit?: number;
10
+ }
11
+ export interface ContextOrientContext {
12
+ id: string;
13
+ title: string;
14
+ status: ContextStatus;
15
+ branch: string | null;
16
+ commit: string | null;
17
+ updatedAt: string;
18
+ }
19
+ export interface ContextOrientSnapshot {
20
+ snapshotId: string;
21
+ mode: SnapshotMode;
22
+ createdAt: string;
23
+ branch: string | null;
24
+ commit: string | null;
25
+ }
26
+ export interface ContextOrientReadyResult {
27
+ status: 'ready';
28
+ projectRoot: string;
29
+ query: string | null;
30
+ context: ContextOrientContext;
31
+ snapshot: ContextOrientSnapshot;
32
+ resumeSummary: ResumeSummary;
33
+ knowledge: KnowledgeDiscoveryResult;
34
+ deadends: DeadendDiscoveryResult;
35
+ notes: Note[];
36
+ truncated: boolean;
37
+ }
38
+ export interface ContextOrientNoContextResult {
39
+ status: 'no_context';
40
+ projectRoot: string;
41
+ query: string | null;
42
+ context: null;
43
+ snapshot: null;
44
+ resumeSummary: null;
45
+ knowledge: KnowledgeDiscoveryResult;
46
+ deadends: DeadendDiscoveryResult;
47
+ notes: Note[];
48
+ truncated: false;
49
+ }
50
+ export type ContextOrientResult = ContextOrientReadyResult | ContextOrientNoContextResult;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,7 @@
1
+ interface Rule {
2
+ pattern: string;
3
+ negated: boolean;
4
+ }
5
+ export declare function readProjectRules(projectRoot: string): Promise<Rule[]>;
6
+ export declare function filterPaths(projectRoot: string, paths: string[]): Promise<string[]>;
7
+ export {};
@@ -0,0 +1,2 @@
1
+ #!/usr/bin/env node
2
+ export {};
@@ -1,32 +1,31 @@
1
1
  #!/usr/bin/env node
2
2
  import { checkpoint, resume } from '../runtime/runtime.js';
3
+ import { contextOrient } from '../agent/api.js';
4
+ import { runClaudeCodeHook } from '../hooks/claude-code.js';
5
+ import { inspectClaudeIntegration, installClaudeIntegration } from '../integrations/claude-code.js';
3
6
  import { doctor, status } from '../runtime/diagnostics.js';
4
7
  import { initProject } from '../runtime/init.js';
5
- import { learnKnowledge, listKnowledge, verifyKnowledge, recordDeadend, listDeadends, verifyDeadend } from '../runtime/knowledge.js';
8
+ import { discoverKnowledge, learnKnowledge, listKnowledge, verifyKnowledge, recordDeadend, listDeadends, verifyDeadend } from '../runtime/knowledge.js';
9
+ import { addNote, listActivity, listNotes } from '../runtime/notes.js';
10
+ import { VERSION } from '../version.js';
6
11
  function option(args, name) {
7
12
  const index = args.indexOf(name);
8
13
  if (index < 0)
9
14
  return undefined;
10
15
  const value = args[index + 1];
11
- if (!value || value.startsWith('--'))
16
+ if (value === undefined || value.startsWith('--'))
12
17
  throw new Error(`missing value for ${name}`);
13
18
  return value;
14
19
  }
15
- function required(args, name) {
16
- const value = option(args, name);
17
- if (!value)
18
- throw new Error(`missing required option ${name}`);
19
- return value;
20
- }
21
20
  function listOption(args, name) {
22
21
  const value = option(args, name);
23
- return value ? value.split(',').map((item) => item.trim()).filter(Boolean) : undefined;
22
+ return value === undefined ? undefined : value.split(',').map((item) => item.trim()).filter(Boolean);
24
23
  }
25
- function positionals(args, options) {
24
+ function positionals(args, valueOptions) {
26
25
  const values = [];
27
26
  for (let index = 0; index < args.length; index += 1) {
28
- if (args[index].startsWith('--') || args[index] === '-m') {
29
- if (options.includes(args[index]))
27
+ if (args[index].startsWith('-')) {
28
+ if (valueOptions.includes(args[index]))
30
29
  index += 1;
31
30
  continue;
32
31
  }
@@ -34,15 +33,19 @@ function positionals(args, options) {
34
33
  }
35
34
  return values;
36
35
  }
37
- function validateOptions(args, allowed) {
36
+ function validateOptions(args, allowed, valueOptions = []) {
38
37
  for (let index = 0; index < args.length; index += 1) {
39
38
  const argument = args[index];
40
39
  if (!argument.startsWith('-'))
41
40
  continue;
42
41
  if (!allowed.includes(argument))
43
42
  throw new Error(`unknown option ${argument}`);
44
- if (argument !== '--all' && argument !== '--help' && argument !== '--version')
43
+ if (valueOptions.includes(argument)) {
44
+ const value = args[index + 1];
45
+ if (value === undefined || value.startsWith('-'))
46
+ throw new Error(`missing value for ${argument}`);
45
47
  index += 1;
48
+ }
46
49
  }
47
50
  }
48
51
  function numericOption(args, name, defaultValue) {
@@ -54,33 +57,250 @@ function numericOption(args, name, defaultValue) {
54
57
  throw new Error(`${name} must be a non-negative integer`);
55
58
  return parsed;
56
59
  }
60
+ function enumOption(args, name, values) {
61
+ const value = option(args, name);
62
+ if (value === undefined)
63
+ return undefined;
64
+ if (!values.includes(value))
65
+ throw new Error(`${name} must be one of: ${values.join(', ')}`);
66
+ return value;
67
+ }
68
+ const HELP = {
69
+ init: `usage: ctx init [path] [--path <path>]
70
+
71
+ Initialize the local .context runtime layout. Re-running init is safe.
72
+
73
+ Options:
74
+ --path <path> Project path`,
75
+ checkpoint: `usage: ctx checkpoint [options]
76
+
77
+ Save structured work state as a baseline or incremental snapshot.
78
+
79
+ Options:
80
+ --path <path> Project path
81
+ --context <id> Existing context ID
82
+ --title <text> Context title
83
+ --progress <text> Current progress summary
84
+ --last-error <text> Last error (blank clears it)
85
+ --completed <items> Comma-separated completed items
86
+ --pending <items> Comma-separated pending tasks
87
+ --decisions <items> Comma-separated decisions
88
+ --risks <items> Comma-separated risks
89
+ --files <paths> Comma-separated related paths
90
+ --absorb-notes Mark open notes for this context as absorbed`,
91
+ resume: `usage: ctx resume [options]
92
+
93
+ Resume the most relevant active or stable context.
94
+
95
+ Options:
96
+ --path <path> Project path
97
+ --context <id> Context ID
98
+ --max-chars <number> Maximum summary characters`,
99
+ orient: `usage: ctx orient [query] [options]
100
+
101
+ Prepare bounded task context from the current work, verified consensus, matching deadends, and open notes.
102
+
103
+ Options:
104
+ --path <path> Project path
105
+ --context <id> Context ID
106
+ --scope <scope> Exact consensus scope
107
+ --max-chars <number> Maximum display characters
108
+ --knowledge-limit <number> Maximum knowledge matches (default: 10)
109
+ --deadend-limit <number> Maximum deadend matches (default: 10)
110
+ --note-limit <number> Maximum open notes (default: 20)`,
111
+ agent: `usage: ctx agent serve
112
+
113
+ Start an MCP stdio server for Agent integrations.
114
+
115
+ Run "ctx agent serve --help" for details.`,
116
+ 'agent serve': `usage: ctx agent serve
117
+
118
+ Start an MCP stdio server exposing the context_orient tool.
119
+
120
+ The server owns standard input/output; do not use it interactively.`,
121
+ hook: `usage: ctx hook claude-code session-start|user-prompt
122
+
123
+ Run a Claude Code lifecycle hook. This command reads Hook JSON from standard input.`,
124
+ 'hook claude-code': `usage: ctx hook claude-code session-start|user-prompt
125
+
126
+ Emit best-effort, read-only phased Context guidance for Claude Code.`,
127
+ integrate: `usage: ctx integrate claude inspect|install [--path <path>] [--apply]
128
+
129
+ Inspect or explicitly install project-local Claude Code hooks and MCP configuration.`,
130
+ 'integrate claude': `usage: ctx integrate claude inspect|install [--path <path>] [--apply]
131
+
132
+ Inspect configuration, or preview/install project-local Claude Code integration.`,
133
+ 'integrate claude inspect': `usage: ctx integrate claude inspect [--path <path>]
134
+
135
+ Report whether the project has the Claude Code hooks and MCP server configured.`,
136
+ 'integrate claude install': `usage: ctx integrate claude install [--path <path>] [--apply]
137
+
138
+ Preview configuration changes by default. Pass --apply to merge project-local .claude/settings.json and .mcp.json.`,
139
+ status: `usage: ctx status [--path <path>]
140
+
141
+ Show the initialized project and context index.`,
142
+ doctor: `usage: ctx doctor [--path <path>]
143
+
144
+ Check runtime files and snapshot integrity without modifying them.`,
145
+ learn: `usage: ctx learn <statement> [options]
146
+
147
+ Record a candidate project knowledge item.
148
+
149
+ Options:
150
+ --path <path> Project path
151
+ --kind <kind> Knowledge kind
152
+ --scope <scope> Knowledge scope
153
+ --context <id> Source context ID
154
+ --snapshot <id> Source snapshot ID
155
+ --evidence <items> Comma-separated evidence references`,
156
+ knowledge: `usage: ctx knowledge [--all] [--path <path>]
157
+ ctx knowledge verify <knowledge-id> [--path <path>]
158
+ ctx knowledge discover <query> [options]
159
+
160
+ List candidate/verified knowledge, verify one item, or discover matching knowledge.
161
+
162
+ Options:
163
+ --all Include unverified items
164
+ --path <path> Project path`,
165
+ 'knowledge verify': `usage: ctx knowledge verify <knowledge-id> [--path <path>]
166
+
167
+ Mark a knowledge item as verified.`,
168
+ 'knowledge discover': `usage: ctx knowledge discover <query> [options]
169
+
170
+ Find matching project knowledge using deterministic lexical and alias rules.
171
+
172
+ Options:
173
+ --path <path> Project path
174
+ --scope <scope> Exact knowledge scope
175
+ --status <statuses> Comma-separated candidate/verified/deprecated/rejected
176
+ --all Include all knowledge statuses
177
+ --limit <number> Maximum matches (default: 10)
178
+ --max-chars <number> Maximum text characters per match`,
179
+ deadend: `usage: ctx deadend [attempt] [options]
180
+ ctx deadend verify <deadend-id> [--path <path>]
181
+
182
+ Record or verify a candidate deadend.
183
+
184
+ Options:
185
+ --path <path> Project path
186
+ --attempt <text> Attempt description
187
+ --reason <text> Why it failed
188
+ -m <text> Alias for --reason
189
+ --scope <scope> Deadend scope
190
+ --context <id> Source context ID
191
+ --snapshot <id> Source snapshot ID
192
+ --evidence <items> Comma-separated evidence references`,
193
+ 'deadend verify': `usage: ctx deadend verify <deadend-id> [--path <path>]
194
+
195
+ Mark a deadend as verified.`,
196
+ deadends: `usage: ctx deadends [--all] [--path <path>]
197
+
198
+ List recorded deadends.`,
199
+ note: `usage: ctx note add <message> [options]
200
+ ctx note list [options]
201
+
202
+ Agent-facing short-term notes for problems, actions, observations, and decisions.
203
+
204
+ Run \"ctx note add --help\" or \"ctx note list --help\" for details.`,
205
+ 'note add': `usage: ctx note add <message> [options]
206
+
207
+ Append a short note without creating a Context Snapshot.
208
+
209
+ Options:
210
+ --path <path> Project path
211
+ --kind <kind> problem|action|observation|decision
212
+ --context <id> Context ID
213
+ --snapshot <id> Snapshot ID
214
+ --files <paths> Comma-separated related paths
215
+ --actor <actor> agent|human
216
+ --status <status> open|resolved`,
217
+ 'note list': `usage: ctx note list [options]
218
+
219
+ List recent notes for an Agent or integration.
220
+
221
+ Options:
222
+ --path <path> Project path
223
+ --context <id> Context ID
224
+ --open Only unresolved and unabsorbed notes
225
+ --limit <number> Maximum notes (default: 50)
226
+ --max-chars <number> Maximum message characters`,
227
+ activity: `usage: ctx activity [options]
228
+
229
+ Show a Human-oriented timeline of notes and saved checkpoints.
230
+
231
+ Options:
232
+ --path <path> Project path
233
+ --context <id> Context ID
234
+ --since <ISO> Only activity at or after this time
235
+ --open Only open notes
236
+ --limit <number> Maximum items (default: 50)
237
+ --max-chars <number> Maximum message characters`,
238
+ };
57
239
  function usage() {
58
- return 'usage: ctx init|checkpoint|resume|status|doctor|learn|knowledge|deadend|deadends [options]';
240
+ return `usage: ctx init|checkpoint|resume|orient|agent|hook|integrate|status|doctor|learn|knowledge|deadend|deadends|note|activity [options]
241
+
242
+ Run \"ctx <command> --help\" for command details.`;
59
243
  }
60
244
  function print(value) {
61
245
  process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
62
246
  }
247
+ async function stdin() {
248
+ let value = '';
249
+ for await (const chunk of process.stdin)
250
+ value += chunk.toString();
251
+ return value;
252
+ }
253
+ function printHelp(args) {
254
+ const command = args[0];
255
+ if (!command) {
256
+ process.stdout.write(`${usage()}\n`);
257
+ return true;
258
+ }
259
+ const key = args[1] === 'verify' || args[1] === 'add' || args[1] === 'list' || args[1] === 'discover' || args[1] === 'serve' || args[1] === 'claude' ? `${command} ${args[1]}` : command;
260
+ const nestedKey = key === 'integrate claude' && (args[2] === 'inspect' || args[2] === 'install') ? `${key} ${args[2]}` : key;
261
+ const help = HELP[nestedKey];
262
+ if (!help)
263
+ return false;
264
+ if (args.includes('--help')) {
265
+ process.stdout.write(`${help}\n`);
266
+ return true;
267
+ }
268
+ return false;
269
+ }
270
+ function validatePositionals(values, max, usageText) {
271
+ if (values.length > max)
272
+ throw new Error(usageText);
273
+ }
63
274
  async function run(args) {
64
275
  if (args.includes('--version')) {
65
- print('0.1.0');
276
+ print(VERSION);
66
277
  return;
67
278
  }
68
- if (args.length === 0 || args.includes('--help')) {
69
- process.stdout.write(`${usage()}\n`);
279
+ if (args.length === 0 || args[0] === '--help') {
280
+ printHelp([]);
70
281
  return;
71
282
  }
283
+ if (args.includes('--help') && printHelp(args))
284
+ return;
72
285
  const command = args[0];
73
286
  const target = option(args, '--path');
74
287
  switch (command) {
75
- case 'init':
76
- print(await initProject(target ?? args[1]));
288
+ case 'init': {
289
+ validateOptions(args.slice(1), ['--path'], ['--path']);
290
+ const values = positionals(args.slice(1), ['--path']);
291
+ validatePositionals(values, 1, HELP.init);
292
+ print(await initProject(target ?? values[0]));
77
293
  return;
294
+ }
78
295
  case 'checkpoint': {
296
+ validateOptions(args.slice(1), ['--path', '--context', '--title', '--progress', '--last-error', '--completed', '--pending', '--decisions', '--risks', '--files', '--absorb-notes'], ['--path', '--context', '--title', '--progress', '--last-error', '--completed', '--pending', '--decisions', '--risks', '--files']);
297
+ validatePositionals(positionals(args.slice(1), ['--path', '--context', '--title', '--progress', '--last-error', '--completed', '--pending', '--decisions', '--risks', '--files']), 0, HELP.checkpoint);
79
298
  const input = {
80
299
  contextId: option(args, '--context'),
300
+ absorbNotes: args.includes('--absorb-notes'),
81
301
  title: option(args, '--title'),
82
302
  progressSummary: option(args, '--progress'),
83
- lastError: option(args, '--last-error') ?? null,
303
+ lastError: option(args, '--last-error'),
84
304
  completed: listOption(args, '--completed'),
85
305
  pendingTasks: listOption(args, '--pending'),
86
306
  decisions: listOption(args, '--decisions'),
@@ -91,18 +311,72 @@ async function run(args) {
91
311
  return;
92
312
  }
93
313
  case 'resume':
94
- validateOptions(args.slice(1), ['--path', '--context', '--max-chars']);
314
+ validateOptions(args.slice(1), ['--path', '--context', '--max-chars'], ['--path', '--context', '--max-chars']);
315
+ validatePositionals(positionals(args.slice(1), ['--path', '--context', '--max-chars']), 0, HELP.resume);
95
316
  print(await resume(target, option(args, '--context'), numericOption(args, '--max-chars', 4000)));
96
317
  return;
318
+ case 'orient': {
319
+ const valueOptions = ['--path', '--context', '--scope', '--max-chars', '--knowledge-limit', '--deadend-limit', '--note-limit'];
320
+ validateOptions(args.slice(1), valueOptions, valueOptions);
321
+ const query = positionals(args.slice(1), valueOptions);
322
+ validatePositionals(query, 1, HELP.orient);
323
+ print(await contextOrient(target, {
324
+ contextId: option(args, '--context'),
325
+ query: query[0],
326
+ scope: option(args, '--scope'),
327
+ maxChars: numericOption(args, '--max-chars', 4000),
328
+ knowledgeLimit: numericOption(args, '--knowledge-limit', 10),
329
+ deadendLimit: numericOption(args, '--deadend-limit', 10),
330
+ noteLimit: numericOption(args, '--note-limit', 20),
331
+ }));
332
+ return;
333
+ }
334
+ case 'agent':
335
+ if (args[1] !== 'serve')
336
+ throw new Error(HELP.agent);
337
+ validateOptions(args.slice(2), [], []);
338
+ validatePositionals(positionals(args.slice(2), []), 0, HELP['agent serve']);
339
+ await import('../mcp/main.js');
340
+ return;
341
+ case 'hook': {
342
+ if (args[1] !== 'claude-code' || !['session-start', 'user-prompt'].includes(args[2]))
343
+ throw new Error(HELP['hook claude-code']);
344
+ validateOptions(args.slice(3), [], []);
345
+ validatePositionals(positionals(args.slice(3), []), 0, HELP['hook claude-code']);
346
+ process.stdout.write(await runClaudeCodeHook(args[2], await stdin()));
347
+ return;
348
+ }
349
+ case 'integrate': {
350
+ if (args[1] !== 'claude' || !['inspect', 'install'].includes(args[2]))
351
+ throw new Error(HELP['integrate claude']);
352
+ const valueOptions = ['--path'];
353
+ const allowed = args[2] === 'install' ? [...valueOptions, '--apply'] : valueOptions;
354
+ validateOptions(args.slice(3), allowed, valueOptions);
355
+ validatePositionals(positionals(args.slice(3), valueOptions), 0, HELP[`integrate claude ${args[2]}`]);
356
+ if (args[2] === 'inspect')
357
+ print(await inspectClaudeIntegration(target));
358
+ else
359
+ print(await installClaudeIntegration(target, args.includes('--apply')));
360
+ return;
361
+ }
97
362
  case 'status':
363
+ validateOptions(args.slice(1), ['--path'], ['--path']);
364
+ validatePositionals(positionals(args.slice(1), ['--path']), 0, HELP.status);
98
365
  print(await status(target));
99
366
  return;
100
367
  case 'doctor':
368
+ validateOptions(args.slice(1), ['--path'], ['--path']);
369
+ validatePositionals(positionals(args.slice(1), ['--path']), 0, HELP.doctor);
101
370
  print(await doctor(target));
102
371
  return;
103
372
  case 'learn': {
104
- const statement = positionals(args.slice(1), ['--path', '--scope', '--context', '--snapshot', '--evidence']);
373
+ const valueOptions = ['--path', '--kind', '--scope', '--context', '--snapshot', '--evidence'];
374
+ validateOptions(args.slice(1), valueOptions, valueOptions);
375
+ const statement = positionals(args.slice(1), valueOptions);
376
+ if (statement.length === 0)
377
+ throw new Error(HELP.learn);
105
378
  const input = {
379
+ kind: option(args, '--kind'),
106
380
  scope: option(args, '--scope'),
107
381
  sourceContextId: option(args, '--context'),
108
382
  sourceSnapshotId: option(args, '--snapshot'),
@@ -111,26 +385,104 @@ async function run(args) {
111
385
  print(await learnKnowledge(target, statement.join(' '), input));
112
386
  return;
113
387
  }
114
- case 'knowledge':
115
- if (args[1] === 'verify')
116
- print(await verifyKnowledge(target, args[2]));
117
- else
388
+ case 'note': {
389
+ if (args[1] === 'add') {
390
+ const valueOptions = ['--path', '--kind', '--context', '--snapshot', '--files', '--actor', '--status'];
391
+ validateOptions(args.slice(2), valueOptions, valueOptions);
392
+ const values = positionals(args.slice(2), valueOptions);
393
+ if (values.length === 0)
394
+ throw new Error(HELP['note add']);
395
+ const input = {
396
+ kind: enumOption(args, '--kind', ['problem', 'action', 'observation', 'decision']),
397
+ actor: enumOption(args, '--actor', ['agent', 'human']),
398
+ contextId: option(args, '--context'),
399
+ snapshotId: option(args, '--snapshot'),
400
+ relatedFiles: listOption(args, '--files'),
401
+ status: enumOption(args, '--status', ['open', 'resolved']),
402
+ };
403
+ print(await addNote(target, values.join(' '), input));
404
+ }
405
+ else if (args[1] === 'list') {
406
+ const valueOptions = ['--path', '--context', '--limit', '--max-chars'];
407
+ validateOptions(args.slice(2), [...valueOptions, '--open'], valueOptions);
408
+ validatePositionals(positionals(args.slice(2), valueOptions), 0, HELP['note list']);
409
+ print(await listNotes(target, { contextId: option(args, '--context'), openOnly: args.includes('--open'), limit: numericOption(args, '--limit', 50), maxChars: numericOption(args, '--max-chars', 500) }));
410
+ }
411
+ else {
412
+ throw new Error(HELP.note);
413
+ }
414
+ return;
415
+ }
416
+ case 'activity': {
417
+ const valueOptions = ['--path', '--context', '--since', '--limit', '--max-chars'];
418
+ validateOptions(args.slice(1), [...valueOptions, '--open'], valueOptions);
419
+ validatePositionals(positionals(args.slice(1), valueOptions), 0, HELP.activity);
420
+ const query = {
421
+ contextId: option(args, '--context'),
422
+ since: option(args, '--since'),
423
+ openOnly: args.includes('--open'),
424
+ limit: numericOption(args, '--limit', 50),
425
+ maxChars: numericOption(args, '--max-chars', 500),
426
+ };
427
+ print(await listActivity(target, query));
428
+ return;
429
+ }
430
+ case 'knowledge': {
431
+ if (args[1] === 'discover') {
432
+ const valueOptions = ['--path', '--scope', '--status', '--limit', '--max-chars'];
433
+ validateOptions(args.slice(2), [...valueOptions, '--all'], valueOptions);
434
+ const query = positionals(args.slice(2), valueOptions);
435
+ if (query.length === 0)
436
+ throw new Error(HELP['knowledge discover']);
437
+ const statuses = args.includes('--all') ? ['candidate', 'verified', 'deprecated', 'rejected'] : (listOption(args, '--status') ?? ['verified']);
438
+ if (statuses.some((status) => !['candidate', 'verified', 'deprecated', 'rejected'].includes(status)))
439
+ throw new Error('--status contains an invalid knowledge status');
440
+ print(await discoverKnowledge(target, query.join(' '), {
441
+ scope: option(args, '--scope'),
442
+ statuses: statuses,
443
+ limit: numericOption(args, '--limit', 10),
444
+ maxChars: numericOption(args, '--max-chars', 4000),
445
+ }));
446
+ }
447
+ else if (args[1] === 'verify') {
448
+ validateOptions(args.slice(2), ['--path'], ['--path']);
449
+ const values = positionals(args.slice(2), ['--path']);
450
+ if (values.length !== 1)
451
+ throw new Error(HELP['knowledge verify']);
452
+ print(await verifyKnowledge(target, values[0]));
453
+ }
454
+ else {
455
+ validateOptions(args.slice(1), ['--all', '--path'], ['--path']);
456
+ validatePositionals(positionals(args.slice(1), ['--path']), 0, HELP.knowledge);
118
457
  print(await listKnowledge(target, args.includes('--all')));
458
+ }
119
459
  return;
460
+ }
120
461
  case 'deadend':
121
- if (args[1] === 'verify')
122
- print(await verifyDeadend(target, args[2]));
462
+ if (args[1] === 'verify') {
463
+ validateOptions(args.slice(2), ['--path'], ['--path']);
464
+ const values = positionals(args.slice(2), ['--path']);
465
+ if (values.length !== 1)
466
+ throw new Error(HELP['deadend verify']);
467
+ print(await verifyDeadend(target, values[0]));
468
+ }
123
469
  else {
470
+ const valueOptions = ['--path', '--attempt', '--reason', '-m', '--scope', '--context', '--snapshot', '--evidence'];
471
+ validateOptions(args.slice(1), valueOptions, valueOptions);
472
+ const values = positionals(args.slice(1), valueOptions);
473
+ validatePositionals(values, 1, HELP.deadend);
124
474
  const input = {
125
475
  scope: option(args, '--scope'),
126
476
  sourceContextId: option(args, '--context'),
127
477
  sourceSnapshotId: option(args, '--snapshot'),
128
478
  evidence: listOption(args, '--evidence'),
129
479
  };
130
- print(await recordDeadend(target, option(args, '--attempt') ?? args[1], option(args, '--reason') ?? option(args, '-m') ?? '', input));
480
+ print(await recordDeadend(target, option(args, '--attempt') ?? values[0], option(args, '--reason') ?? option(args, '-m') ?? '', input));
131
481
  }
132
482
  return;
133
483
  case 'deadends':
484
+ validateOptions(args.slice(1), ['--all', '--path'], ['--path']);
485
+ validatePositionals(positionals(args.slice(1), ['--path']), 0, HELP.deadends);
134
486
  print(await listDeadends(target, args.includes('--all')));
135
487
  return;
136
488
  default:
@@ -0,0 +1,2 @@
1
+ import type { GitState } from '../runtime/types.js';
2
+ export declare function readGitState(projectRoot: string): Promise<GitState>;
@@ -0,0 +1 @@
1
+ export declare function runClaudeCodeHook(event: 'session-start' | 'user-prompt', raw: string): Promise<string>;
@@ -0,0 +1,84 @@
1
+ import { contextOrient } from '../agent/api.js';
2
+ const HOOK_RESPONSE = '{}\n';
3
+ function text(value) {
4
+ return typeof value === 'string' ? value : undefined;
5
+ }
6
+ function compact(value, maxChars) {
7
+ const normalized = value.replace(/\s+/g, ' ').trim();
8
+ return normalized.length <= maxChars ? normalized : `${normalized.slice(0, maxChars - 1)}…`;
9
+ }
10
+ function lines(label, values, limit) {
11
+ return values.slice(0, limit).map((value) => `- ${label}: ${compact(value, 240)}`);
12
+ }
13
+ function readyContext(result) {
14
+ const content = [
15
+ `当前 Context:${compact(result.context.title, 160)}`,
16
+ `进度:${compact(result.resumeSummary.progressSummary, 360)}`,
17
+ ...lines('待办', result.resumeSummary.pendingTasks, 4),
18
+ ...lines('决策', result.resumeSummary.decisions, 3),
19
+ ...lines('风险', result.resumeSummary.risks, 3),
20
+ ...result.knowledge.hits.slice(0, 5).map((hit) => `- 已验证知识:${compact(hit.knowledge.statement, 240)}`),
21
+ ...result.deadends.hits.slice(0, 3).map((hit) => `- 已验证死路(避免):${compact(hit.deadend.attempt, 180)};${compact(hit.deadend.reason, 180)}`),
22
+ ...result.notes.slice(0, 8).map((note) => `- 开放 Note:${compact(note.message, 220)}`),
23
+ ];
24
+ if (result.truncated)
25
+ content.push('- 上下文已按预算截断;需要时使用 context_orient 或 ctx orient 进一步查询。');
26
+ return content;
27
+ }
28
+ function sessionContext(result) {
29
+ const phases = [
30
+ '开发阶段:Orient → Plan → Implement → Verify → Handoff。',
31
+ '先基于以下上下文明确计划;实现中记录重要观察/决策/阻塞为 ctx note;验证后显式 ctx checkpoint,总结完成、待办、决策和风险。不要自动保存 Snapshot。',
32
+ ];
33
+ if (result.status === 'no_context') {
34
+ phases.push('当前没有可恢复的 Context。开始有意义的工作后,使用 ctx checkpoint 创建一个。');
35
+ }
36
+ else {
37
+ phases.push(...readyContext(result));
38
+ }
39
+ return phases.join('\n');
40
+ }
41
+ function promptContext(result) {
42
+ const phases = [
43
+ '开发阶段:Implement → Verify → Handoff。',
44
+ '实现时用 ctx note 记录重要观察、决策或阻塞;完成后运行适用的验证。阶段结束时显式 ctx checkpoint(可 --absorb-notes),不要由 Hook 自动保存。',
45
+ ];
46
+ if (result.status === 'no_context') {
47
+ phases.push('当前没有可恢复的 Context;如果这项工作会继续,先建立 checkpoint。');
48
+ }
49
+ else {
50
+ phases.push(...readyContext(result));
51
+ }
52
+ return phases.join('\n');
53
+ }
54
+ async function parseInput(raw) {
55
+ try {
56
+ const value = JSON.parse(raw);
57
+ return value !== null && typeof value === 'object' ? value : null;
58
+ }
59
+ catch {
60
+ return null;
61
+ }
62
+ }
63
+ export async function runClaudeCodeHook(event, raw) {
64
+ const input = await parseInput(raw);
65
+ if (!input)
66
+ return HOOK_RESPONSE;
67
+ try {
68
+ const cwd = text(input.cwd);
69
+ const result = event === 'session-start'
70
+ ? await contextOrient(cwd, { maxChars: 1200, noteLimit: 8 })
71
+ : await contextOrient(cwd, {
72
+ query: compact(text(input.prompt) ?? '', 500) || undefined,
73
+ maxChars: 1800,
74
+ knowledgeLimit: 5,
75
+ deadendLimit: 3,
76
+ noteLimit: 8,
77
+ });
78
+ const additionalContext = event === 'session-start' ? sessionContext(result) : promptContext(result);
79
+ return `${JSON.stringify({ additionalContext })}\n`;
80
+ }
81
+ catch {
82
+ return HOOK_RESPONSE;
83
+ }
84
+ }