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.
Files changed (47) hide show
  1. package/README.md +121 -9
  2. package/dist/src/agent/api.d.ts +5 -1
  3. package/dist/src/agent/api.js +41 -12
  4. package/dist/src/agent/index.d.ts +3 -1
  5. package/dist/src/agent/index.js +1 -0
  6. package/dist/src/agent/types.d.ts +21 -0
  7. package/dist/src/cli/main.js +150 -12
  8. package/dist/src/cognition/feedback.d.ts +9 -0
  9. package/dist/src/cognition/feedback.js +30 -0
  10. package/dist/src/cognition/filesystem-observer.d.ts +41 -0
  11. package/dist/src/cognition/filesystem-observer.js +164 -0
  12. package/dist/src/cognition/git-observer.d.ts +1 -0
  13. package/dist/src/cognition/git-observer.js +3 -2
  14. package/dist/src/cognition/journal.d.ts +4 -1
  15. package/dist/src/cognition/journal.js +83 -4
  16. package/dist/src/cognition/migration-v1.js +13 -11
  17. package/dist/src/cognition/orient.d.ts +4 -0
  18. package/dist/src/cognition/orient.js +14 -4
  19. package/dist/src/cognition/policy.d.ts +4 -0
  20. package/dist/src/cognition/policy.js +49 -0
  21. package/dist/src/cognition/projections.js +0 -0
  22. package/dist/src/cognition/retrieval.d.ts +26 -0
  23. package/dist/src/cognition/retrieval.js +273 -0
  24. package/dist/src/cognition/runtime.d.ts +20 -1
  25. package/dist/src/cognition/runtime.js +229 -53
  26. package/dist/src/cognition/types.d.ts +55 -1
  27. package/dist/src/cognition/v1-bridge.js +13 -4
  28. package/dist/src/compiler/activity.d.ts +3 -0
  29. package/dist/src/compiler/activity.js +79 -0
  30. package/dist/src/compiler/compile.d.ts +2 -0
  31. package/dist/src/compiler/compile.js +330 -0
  32. package/dist/src/compiler/sources.d.ts +19 -0
  33. package/dist/src/compiler/sources.js +130 -0
  34. package/dist/src/compiler/types.d.ts +187 -0
  35. package/dist/src/compiler/types.js +1 -0
  36. package/dist/src/hooks/claude-code.js +7 -15
  37. package/dist/src/mcp/main.js +2 -0
  38. package/dist/src/mcp/server.js +2 -0
  39. package/dist/src/runtime/knowledge.d.ts +2 -0
  40. package/dist/src/runtime/knowledge.js +63 -0
  41. package/dist/src/runtime/types.d.ts +19 -0
  42. package/dist/src/storage/layout.d.ts +2 -0
  43. package/dist/src/storage/layout.js +6 -0
  44. package/dist/src/version.d.ts +1 -1
  45. package/dist/src/version.js +1 -1
  46. package/package.json +2 -2
  47. package/skills/fluffy-context/SKILL.md +2 -1
@@ -1,11 +1,10 @@
1
1
  import { contextOrient } from '../agent/api.js';
2
2
  import { appendEvent } from '../cognition/journal.js';
3
3
  import { v2Enabled } from '../cognition/orient.js';
4
- import { startRuntime } from '../cognition/runtime.js';
4
+ import { ensureRuntimeForSession, touchRuntime } from '../cognition/runtime.js';
5
+ import { loadRuntimePolicy } from '../cognition/policy.js';
5
6
  import { readGitWorktreeState } from '../git/git-adapter.js';
6
7
  import { resolveProjectRoot } from '../project/project-resolver.js';
7
- import { cognitionPolicyPath } from '../storage/layout.js';
8
- import { readJson } from '../storage/json-store.js';
9
8
  const HOOK_RESPONSE = '{}\n';
10
9
  function text(value) {
11
10
  return typeof value === 'string' ? value : undefined;
@@ -68,15 +67,12 @@ function redactIntent(value) {
68
67
  .replace(/\b(?:sk|ghp|gho|github_pat)_[A-Za-z0-9_-]+\b/g, () => { redactionCount += 1; return '[REDACTED]'; });
69
68
  return { intent, redactionCount };
70
69
  }
71
- async function within(task, milliseconds) {
72
- return Promise.race([task, new Promise((_, reject) => setTimeout(() => reject(new Error('hook deadline exceeded')), milliseconds))]);
73
- }
74
70
  async function capture(event, input) {
75
71
  const cwd = text(input.cwd);
76
72
  const projectRoot = await resolveProjectRoot(cwd);
77
73
  if (!await v2Enabled(projectRoot))
78
74
  return;
79
- const policy = await readJson(cognitionPolicyPath(projectRoot));
75
+ const policy = await loadRuntimePolicy(projectRoot);
80
76
  const sessionId = stableId(input, 'session');
81
77
  const eventId = stableId(input, 'event');
82
78
  if (!sessionId || !eventId)
@@ -94,12 +90,7 @@ async function capture(event, input) {
94
90
  });
95
91
  }
96
92
  async function maybeStartRuntime(input) {
97
- const projectRoot = await resolveProjectRoot(text(input.cwd));
98
- if (!await v2Enabled(projectRoot))
99
- return;
100
- const policy = await readJson(cognitionPolicyPath(projectRoot));
101
- if (policy.allowHookStartup)
102
- await within(startRuntime(projectRoot), 1_000);
93
+ await ensureRuntimeForSession(text(input.cwd), { source: 'claude-hook', timeoutMs: 1_000 });
103
94
  }
104
95
  async function parseInput(raw) {
105
96
  try {
@@ -115,9 +106,10 @@ export async function runClaudeCodeHook(event, raw) {
115
106
  if (!input)
116
107
  return HOOK_RESPONSE;
117
108
  try {
118
- if (event === 'session-start')
119
- await maybeStartRuntime(input).catch(() => undefined);
109
+ await maybeStartRuntime(input).catch(() => undefined);
110
+ await touchRuntime(text(input.cwd)).catch(() => false);
120
111
  await capture(event, input).catch(() => undefined);
112
+ await touchRuntime(text(input.cwd)).catch(() => false);
121
113
  const cwd = text(input.cwd);
122
114
  const result = event === 'session-start'
123
115
  ? await contextOrient(cwd, { maxChars: 1200, noteLimit: 8 })
@@ -1,5 +1,7 @@
1
1
  import { serveStdio } from '@modelcontextprotocol/server/stdio';
2
+ import { ensureRuntimeForSession } from '../cognition/runtime.js';
2
3
  import { createMcpServer } from './server.js';
4
+ await ensureRuntimeForSession(process.cwd(), { source: 'mcp', timeoutMs: 1_000 }).catch(() => undefined);
3
5
  serveStdio(createMcpServer, {
4
6
  onerror: (error) => process.stderr.write(`MCP server error: ${error.message}\n`),
5
7
  });
@@ -1,6 +1,7 @@
1
1
  import { McpServer } from '@modelcontextprotocol/server';
2
2
  import { z } from 'zod';
3
3
  import { contextOrient } from '../agent/api.js';
4
+ import { ensureRuntimeForSession } from '../cognition/runtime.js';
4
5
  import { VERSION } from '../version.js';
5
6
  const orientInput = z.object({
6
7
  path: z.string().optional(),
@@ -23,6 +24,7 @@ export function createMcpServer() {
23
24
  inputSchema: orientInput,
24
25
  }, async ({ path, ...options }) => {
25
26
  try {
27
+ await ensureRuntimeForSession(path, { source: 'mcp', timeoutMs: 1_000 }).catch(() => undefined);
26
28
  const result = await contextOrient(path, options);
27
29
  return {
28
30
  content: [{ type: 'text', text: JSON.stringify(result) }],
@@ -3,7 +3,9 @@ export declare function learnKnowledge(startPath: string | undefined, statement:
3
3
  export declare function listKnowledge(startPath?: string, includeUnverified?: boolean): Promise<Knowledge[]>;
4
4
  export declare function discoverKnowledge(startPath: string | undefined, query: string, options?: KnowledgeDiscoverOptions): Promise<KnowledgeDiscoveryResult>;
5
5
  export declare function verifyKnowledge(startPath: string | undefined, knowledgeId: string): Promise<Knowledge>;
6
+ export declare function transitionKnowledge(startPath: string | undefined, knowledgeId: string, status: 'deprecated' | 'rejected', reason: string): Promise<Knowledge>;
6
7
  export declare function recordDeadend(startPath: string | undefined, attempt: string, reason: string, scopeOrInput?: string | DeadendInput): Promise<Deadend>;
7
8
  export declare function listDeadends(startPath?: string, includeUnverified?: boolean): Promise<Deadend[]>;
8
9
  export declare function discoverDeadends(startPath: string | undefined, query: string, options?: DeadendDiscoverOptions): Promise<DeadendDiscoveryResult>;
10
+ export declare function transitionDeadend(startPath: string | undefined, deadendId: string, status: 'obsolete' | 'rejected', reason: string): Promise<Deadend>;
9
11
  export declare function verifyDeadend(startPath: string | undefined, deadendId: string): Promise<Deadend>;
@@ -35,6 +35,23 @@ function isKnowledgeFile(value) {
35
35
  function cleanList(values) {
36
36
  return [...new Set((values ?? []).map((value) => value.trim()).filter(Boolean))];
37
37
  }
38
+ function cleanApplicability(input) {
39
+ if (!input)
40
+ return { project: true, branchMode: 'global' };
41
+ if (input.project !== true || !['global', 'exact'].includes(input.branchMode))
42
+ throw new Error('invalid record applicability');
43
+ const branches = cleanList(input.branches);
44
+ const paths = cleanList(input.paths);
45
+ if (input.branchMode === 'exact' && branches.length === 0)
46
+ throw new Error('exact applicability requires a branch');
47
+ return {
48
+ project: true,
49
+ branchMode: input.branchMode,
50
+ ...(branches.length > 0 ? { branches } : {}),
51
+ ...(paths.length > 0 ? { paths } : {}),
52
+ ...(input.sourceCommit === undefined ? {} : { sourceCommit: input.sourceCommit }),
53
+ };
54
+ }
38
55
  function normalized(value) {
39
56
  return value.trim().toLocaleLowerCase();
40
57
  }
@@ -116,6 +133,7 @@ export async function learnKnowledge(startPath, statement, scopeOrInput = 'proje
116
133
  sourceContextIds: cleanList(input.sourceContextId ? [input.sourceContextId] : []),
117
134
  sourceSnapshotIds: cleanList(input.sourceSnapshotId ? [input.sourceSnapshotId] : []),
118
135
  supportingEvidence: cleanList(input.evidence),
136
+ applicability: cleanApplicability(input.applicability),
119
137
  createdAt: now,
120
138
  updatedAt: now,
121
139
  };
@@ -208,6 +226,8 @@ export async function verifyKnowledge(startPath, knowledgeId) {
208
226
  const item = file.items.find((candidate) => candidate.knowledgeId === knowledgeId);
209
227
  if (!item)
210
228
  throw new Error(`knowledge not found: ${knowledgeId}`);
229
+ if (item.status !== 'candidate')
230
+ throw new Error(`knowledge must be a candidate to verify: ${knowledgeId}`);
211
231
  item.status = 'verified';
212
232
  item.confidence = Math.max(item.confidence, 0.8);
213
233
  item.updatedAt = new Date().toISOString();
@@ -216,6 +236,26 @@ export async function verifyKnowledge(startPath, knowledgeId) {
216
236
  return item;
217
237
  });
218
238
  }
239
+ export async function transitionKnowledge(startPath, knowledgeId, status, reason) {
240
+ const projectRoot = await resolveProjectRoot(startPath);
241
+ if (!reason.trim())
242
+ throw new Error('knowledge transition reason must not be empty');
243
+ return withLock(`${locksDirectory(projectRoot)}/knowledge.lock`, async () => {
244
+ const file = await loadKnowledge(projectRoot);
245
+ const item = file.items.find((candidate) => candidate.knowledgeId === knowledgeId);
246
+ if (!item)
247
+ throw new Error(`knowledge not found: ${knowledgeId}`);
248
+ if ((status === 'deprecated' && item.status !== 'verified') || (status === 'rejected' && item.status !== 'candidate')) {
249
+ throw new Error(`invalid knowledge transition: ${item.status} to ${status}`);
250
+ }
251
+ item.status = status;
252
+ item.updatedAt = new Date().toISOString();
253
+ item.governance = { reason: reason.trim(), transitionedAt: item.updatedAt };
254
+ await atomicWriteJson(knowledgePath(projectRoot), file);
255
+ await mirrorKnowledge(projectRoot, item);
256
+ return item;
257
+ });
258
+ }
219
259
  export async function recordDeadend(startPath, attempt, reason, scopeOrInput = 'project') {
220
260
  const projectRoot = await resolveProjectRoot(startPath);
221
261
  const input = typeof scopeOrInput === 'string' ? { scope: scopeOrInput } : scopeOrInput;
@@ -232,6 +272,7 @@ export async function recordDeadend(startPath, attempt, reason, scopeOrInput = '
232
272
  status: 'candidate',
233
273
  sourceContextId: input.sourceContextId ?? null,
234
274
  sourceSnapshotId: input.sourceSnapshotId ?? null,
275
+ applicability: cleanApplicability(input.applicability),
235
276
  createdAt: now,
236
277
  updatedAt: now,
237
278
  };
@@ -288,6 +329,26 @@ export async function discoverDeadends(startPath, query, options = {}) {
288
329
  hits: selected.map((item) => ({ ...item, deadend: limitedDeadend(item.deadend, maxChars) })),
289
330
  };
290
331
  }
332
+ export async function transitionDeadend(startPath, deadendId, status, reason) {
333
+ const projectRoot = await resolveProjectRoot(startPath);
334
+ if (!reason.trim())
335
+ throw new Error('deadend transition reason must not be empty');
336
+ return withLock(`${locksDirectory(projectRoot)}/deadends.lock`, async () => {
337
+ const file = await loadDeadends(projectRoot);
338
+ const item = file.items.find((candidate) => candidate.deadendId === deadendId);
339
+ if (!item)
340
+ throw new Error(`deadend not found: ${deadendId}`);
341
+ if ((status === 'obsolete' && item.status !== 'verified') || (status === 'rejected' && item.status !== 'candidate')) {
342
+ throw new Error(`invalid deadend transition: ${item.status} to ${status}`);
343
+ }
344
+ item.status = status;
345
+ item.updatedAt = new Date().toISOString();
346
+ item.governance = { reason: reason.trim(), transitionedAt: item.updatedAt };
347
+ await atomicWriteJson(deadendsPath(projectRoot), file);
348
+ await mirrorDeadend(projectRoot, item);
349
+ return item;
350
+ });
351
+ }
291
352
  export async function verifyDeadend(startPath, deadendId) {
292
353
  const projectRoot = await resolveProjectRoot(startPath);
293
354
  return withLock(`${locksDirectory(projectRoot)}/deadends.lock`, async () => {
@@ -295,6 +356,8 @@ export async function verifyDeadend(startPath, deadendId) {
295
356
  const item = file.items.find((candidate) => candidate.deadendId === deadendId);
296
357
  if (!item)
297
358
  throw new Error(`deadend not found: ${deadendId}`);
359
+ if (item.status !== 'candidate')
360
+ throw new Error(`deadend must be a candidate to verify: ${deadendId}`);
298
361
  item.status = 'verified';
299
362
  item.updatedAt = new Date().toISOString();
300
363
  await atomicWriteJson(deadendsPath(projectRoot), file);
@@ -136,6 +136,13 @@ export interface ActivityResult {
136
136
  }
137
137
  export type KnowledgeStatus = 'candidate' | 'verified' | 'deprecated' | 'rejected';
138
138
  export type DeadendStatus = 'candidate' | 'verified' | 'obsolete' | 'rejected';
139
+ export interface RecordApplicability {
140
+ project: true;
141
+ branches?: string[];
142
+ branchMode: 'global' | 'exact';
143
+ paths?: string[];
144
+ sourceCommit?: string | null;
145
+ }
139
146
  export interface Knowledge {
140
147
  knowledgeId: string;
141
148
  kind: string;
@@ -146,6 +153,11 @@ export interface Knowledge {
146
153
  sourceContextIds: string[];
147
154
  sourceSnapshotIds: string[];
148
155
  supportingEvidence: string[];
156
+ applicability?: RecordApplicability;
157
+ governance?: {
158
+ reason: string;
159
+ transitionedAt: string;
160
+ };
149
161
  createdAt: string;
150
162
  updatedAt: string;
151
163
  }
@@ -155,6 +167,7 @@ export interface KnowledgeInput {
155
167
  sourceContextId?: string;
156
168
  sourceSnapshotId?: string;
157
169
  evidence?: string[];
170
+ applicability?: RecordApplicability;
158
171
  }
159
172
  export interface KnowledgeDiscoverOptions {
160
173
  scope?: string;
@@ -205,6 +218,7 @@ export interface DeadendInput {
205
218
  sourceContextId?: string;
206
219
  sourceSnapshotId?: string;
207
220
  evidence?: string[];
221
+ applicability?: RecordApplicability;
208
222
  }
209
223
  export interface Deadend {
210
224
  deadendId: string;
@@ -215,6 +229,11 @@ export interface Deadend {
215
229
  status: DeadendStatus;
216
230
  sourceContextId: string | null;
217
231
  sourceSnapshotId: string | null;
232
+ applicability?: RecordApplicability;
233
+ governance?: {
234
+ reason: string;
235
+ transitionedAt: string;
236
+ };
218
237
  createdAt: string;
219
238
  updatedAt: string;
220
239
  }
@@ -22,10 +22,12 @@ export declare function cognitionContextsProjectionPath(projectRoot: string): st
22
22
  export declare function cognitionKnowledgeProjectionPath(projectRoot: string): string;
23
23
  export declare function cognitionDeadendsProjectionPath(projectRoot: string): string;
24
24
  export declare function cognitionNotesProjectionPath(projectRoot: string): string;
25
+ export declare function cognitionRetrievalProjectionPath(projectRoot: string): string;
25
26
  export declare function cognitionMigrationsDirectory(projectRoot: string): string;
26
27
  export declare function cognitionV1ImportPath(projectRoot: string): string;
27
28
  export declare function cognitionRuntimeDirectory(projectRoot: string): string;
28
29
  export declare function cognitionRuntimeLeasePath(projectRoot: string): string;
29
30
  export declare function cognitionRuntimeSecretPath(projectRoot: string): string;
31
+ export declare function cognitionRuntimeStartupPath(projectRoot: string): string;
30
32
  export declare function cognitionV1SyncPath(projectRoot: string): string;
31
33
  export declare function cognitionQuarantineDirectory(projectRoot: string): string;
@@ -71,6 +71,9 @@ export function cognitionDeadendsProjectionPath(projectRoot) {
71
71
  export function cognitionNotesProjectionPath(projectRoot) {
72
72
  return path.join(cognitionProjectionsDirectory(projectRoot), 'notes.json');
73
73
  }
74
+ export function cognitionRetrievalProjectionPath(projectRoot) {
75
+ return path.join(cognitionProjectionsDirectory(projectRoot), 'retrieval.json');
76
+ }
74
77
  export function cognitionMigrationsDirectory(projectRoot) {
75
78
  return path.join(cognitionRoot(projectRoot), 'migrations');
76
79
  }
@@ -86,6 +89,9 @@ export function cognitionRuntimeLeasePath(projectRoot) {
86
89
  export function cognitionRuntimeSecretPath(projectRoot) {
87
90
  return path.join(cognitionRuntimeDirectory(projectRoot), 'secret');
88
91
  }
92
+ export function cognitionRuntimeStartupPath(projectRoot) {
93
+ return path.join(cognitionRuntimeDirectory(projectRoot), 'startup.json');
94
+ }
89
95
  export function cognitionV1SyncPath(projectRoot) {
90
96
  return path.join(cognitionRuntimeDirectory(projectRoot), 'v1-sync.json');
91
97
  }
@@ -1 +1 @@
1
- export declare const VERSION = "0.5.0";
1
+ export declare const VERSION = "0.7.2";
@@ -1 +1 @@
1
- export const VERSION = '0.5.0';
1
+ export const VERSION = '0.7.2';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fluffy-context",
3
- "version": "0.5.0",
3
+ "version": "0.7.2",
4
4
  "description": "Local context management CLI and MCP tools for AI coding agents",
5
5
  "license": "MIT",
6
6
  "author": "FluffyChi-Xing",
@@ -55,7 +55,7 @@
55
55
  "scripts": {
56
56
  "build": "tsc -p tsconfig.json",
57
57
  "typecheck": "tsc -p tsconfig.test.json --noEmit",
58
- "test": "tsc -p tsconfig.test.json && node --test dist/test/*.test.js",
58
+ "test": "tsc -p tsconfig.test.json && node --test --test-concurrency=1 dist/test/*.test.js",
59
59
  "pack:check": "tsc -p tsconfig.test.json && node --test dist/test/package-smoke.test.js",
60
60
  "prepack": "npm run build",
61
61
  "prepublishOnly": "npm test && npm run pack:check",
@@ -17,7 +17,8 @@ compatibility: 需要 Node.js >=20.19.0;Git 可选。CLI 通过 npm 全局安
17
17
  - 人类需要了解 Agent 发生了什么时使用 `ctx activity`;它是只读时间线,不要求人类参与 Agent 的高频记录操作。
18
18
  - 新会话与新任务优先运行 `ctx orient [query] --max-chars <预算>`;它只读地返回有界摘要、匹配的已验证共识和当前 Context 的 open Note,不会创建 Snapshot 或更新 `lastUsedAt`。
19
19
  - 仅需要完整详情时再运行 `ctx resume --max-chars <预算>`;它会保留既有的 `lastUsedAt` 更新语义。
20
- - Agent 集成从 `fluffy-context` 或 `fluffy-context/agent` 导入 `contextOrient`、`saveContext`、`loadContext` 和 `searchContext`,不应直接读写 `.context` 或导入内部 `dist/...` 路径。
20
+ - Agent 集成从 `fluffy-context` 或 `fluffy-context/agent` 导入 `contextOrient`、`compileContext`、`saveContext`、`loadContext` 和 `searchContext`,不应直接读写 `.context` 或导入内部 `dist/...` 路径。
21
+ - `ctx compile` 的 `activity` section 只表示 Runtime 观察到的近期文件 metadata 变化;它不代表文件被读取或理解,也不替代 checkpoint、Note 或已验证 Knowledge。
21
22
  - 新任务先发现已验证共识,再开始实现;候选项只在显式审查时使用。
22
23
  - 普通查询只使用已验证 Knowledge 和 Deadend;需要审查候选项时显式使用 `--all`。
23
24
  - CLI 业务命令的标准输出是 JSON;错误写入标准错误并返回非零退出码。解析输出时不要把 `--help` 的纯文本当作 JSON。