fluffy-context 0.5.0 → 0.6.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.
package/README.md CHANGED
@@ -4,7 +4,20 @@
4
4
 
5
5
  Git 仍然负责代码和真实文件变更,Issue 系统仍然负责任务,项目文档仍然负责设计;`fluffy-context` 补齐的是持续演进的项目认知层。最终架构是 `project → knowledge runtime → LLM`:CLI、MCP、Claude Hook 和未来 IDE/Agent 集成只是事件入口与信息注入适配器,而不是认知本体。
6
6
 
7
- 0.5.0 在 v2 迁移、可验证事件日志、确定性投影和按项目 Runtime 基础上,完成首个 branch-aware 注入闭环:迁移并启用 Runtime 后,Orient、MCP 与 Claude Hook 会按当前 worktree/ref 选择 Context,避免跨分支任务状态泄漏。v1 的 checkpoint、Note、Knowledge 与 Deadend 写入会最佳努力镜像为 v2 事件;Runtime 也会协调检测到的外部 v1 变更。自动产物默认仍仅进入 Candidate,Verified Knowledge 仍需治理确认。
7
+ 0.6.0 在 branch-aware 注入闭环上补齐了可治理的确定性检索:迁移并启用 Runtime 后,Orient 从 v2 journal 的可重建索引检索已验证、适用的 Knowledge 与 Deadend,并返回有界的选择、排除与截断说明。v1 的 checkpoint、Note、Knowledge 与 Deadend 写入仍会最佳努力镜像为 v2 事件;Runtime 也会协调检测到的外部 v1 变更。自动产物默认仍仅进入 Candidate,Verified Knowledge 仍需治理确认。
8
+
9
+ ## 版本演进
10
+
11
+ | 版本 | 主要能力 | 关键特点 |
12
+ | --- | --- | --- |
13
+ | 0.1.0 | 本地 Context CLI、baseline/patch Snapshot、resume、Note、Candidate Knowledge/Deadend | Git 继续作为代码权威;Context 只保存有界任务状态,不复制项目文件;checkpoint 显式触发且支持 no-op、限流与敏感路径过滤。 |
14
+ | 0.2.0 | 确定性 Knowledge Discovery、Agent TypeScript API、可安装 Skill | Candidate 与 Verified 分离;默认只注入已验证共识;不依赖向量库、模型或远端服务。 |
15
+ | 0.3.x | MCP `context_orient`、Claude Code Hook 与集成配置生成 | 通过 stdio MCP 和 fail-open Hook 降低 Agent 主动调用成本;集成安装仅在 `--apply` 时写入项目配置。 |
16
+ | 0.4.0 | v2 事件 journal、v1 显式迁移、可重建投影、本地 Runtime、Git 观察 | v2 与 v1 并存;NDJSON hash chain 是 v2 权威;Runtime 仅监听 loopback 并记录分支、ref 与 HEAD 元数据。 |
17
+ | 0.5.0 | branch-aware Orient、v1 写入镜像、Runtime 协调、宿主事件捕获 | Context 按当前 worktree/ref 选择,避免跨分支任务状态泄漏;稳定宿主 ID 才会写入限长、脱敏后的 session/prompt intent。 |
18
+ | 0.6.0 | journal 派生的确定性检索、治理终态、可解释 Orient、显式使用反馈 | 启用 v2 后直接从可重建索引检索已验证且适用的共识;使用反馈只影响稳定排序,绝不自动验证或提升 Candidate。 |
19
+
20
+ 所有版本均保持本地优先、显式治理与 fail-open 集成原则。v1 数据不会在迁移过程中被重写;当 v2 未启用、不可用、损坏或尚未同步时,读取接口会回退到兼容的 v1 行为。
8
21
 
9
22
  ## 适用场景
10
23
 
@@ -93,7 +106,11 @@ ctx note add "测试环境缺少回调凭据" --kind problem --context <context-
93
106
  ctx activity --context <context-id> --open
94
107
  ctx knowledge discover "支付回调"
95
108
  ctx knowledge verify <knowledge-id>
109
+ ctx knowledge deprecate <knowledge-id> --reason "已被新的结论取代"
96
110
  ctx deadend verify <deadend-id>
111
+ ctx deadend obsolete <deadend-id> --reason "当前架构已不再适用"
112
+ ctx feedback use knowledge <knowledge-id> --event-id <stable-id>
113
+ ctx journal verify
97
114
  ctx migrate v1 --dry-run
98
115
  ctx migrate v1 --apply
99
116
  ctx migrate verify
@@ -127,7 +144,7 @@ ctx runtime disable
127
144
 
128
145
  Runtime 是单项目、按需启动的本地守护进程。它只绑定 `127.0.0.1` 的动态端口,并要求存储在项目本地 `.context/v2/runtime/secret` 中的能力令牌;令牌不会显示在 CLI 状态输出或写入事件日志。启动后,Runtime 持续观察当前 worktree 的 Git 状态:初始状态会记录 `git.state.observed`,分支/ref 切换记录 `git.branch.changed`,HEAD 更新记录 `git.head.changed`。这些事件会重建 branch workspace 投影,但不会自动创建 checkpoint 或将候选信息提升为已验证共识。
129
146
 
130
- 完成迁移并显式 `ctx runtime enable` 后,`ctx orient`、MCP `context_orient` 与 Claude Hook 会优先选择当前 worktree/ref 的 v2 workspace;它不会借用其他分支的 Context。没有匹配 workspace、v2 状态不可用或 Runtime 已禁用时,接口会安全回退到既有 v1 选择逻辑。Runtime 停止时仍可读取已验证的 v2 状态,但运行中的 Runtime 负责持续 Git 观察和 v1 写入镜像。
147
+ 完成迁移并显式 `ctx runtime enable` 后,`ctx orient`、MCP `context_orient` 与 Claude Hook 会优先选择当前 worktree/ref 的 v2 workspace;它不会借用其他分支的 Context。v2 可用时,Knowledge 与 Deadend 由 journal 重放出的确定性倒排索引直接检索,默认仅返回已验证且适用的记录;无匹配 workspace、v2 状态不可用、journal 损坏、v1 尚未同步或 Runtime 已禁用时,接口会安全回退到既有 v1 选择逻辑。Runtime 停止时仍可读取已验证的 v2 状态,但运行中的 Runtime 负责持续 Git 观察和 v1 写入镜像。
131
148
 
132
149
  Claude Hook 仅在宿主提供稳定的 session 与 event 标识时记录 `host.session.started` 或 `host.prompt.submitted`。提示只保存按 `promptIntentMaxChars` 截断并经过敏感值脱敏后的 intent,绝不保存原始提示;缺少稳定标识时不写入事件。默认 Hook 不启动守护进程;将 v2 policy 的 `allowHookStartup` 设为 `true` 后,SessionStart 才会在严格超时内尽力启动 Runtime,失败仍会 fail open。UserPromptSubmit 不会启动 Runtime。
133
150
 
@@ -141,7 +158,11 @@ Claude Hook 仅在宿主提供稳定的 session 与 event 标识时记录 `host.
141
158
 
142
159
  ### Knowledge 与 Deadend
143
160
 
144
- `ctx learn` 和 `ctx deadend` 默认创建 `candidate` 项。普通发现与 `ctx orient` 只使用已验证项目共识;候选项需要通过 `ctx knowledge verify` 或 `ctx deadend verify` 显式确认,或以 `--all` 审查。
161
+ `ctx learn` 和 `ctx deadend` 默认创建 `candidate` 项。普通发现与 `ctx orient` 只使用已验证项目共识;候选项需要通过 `ctx knowledge verify` 或 `ctx deadend verify` 显式确认,或以 `--all` 审查。验证后的 Knowledge 可用 `ctx knowledge deprecate` 退役,候选项可用 `ctx knowledge reject` 拒绝;验证后的 Deadend 可用 `ctx deadend obsolete` 退役,候选项可用 `ctx deadend reject` 拒绝。终态记录保留证据与治理原因,但不会被默认注入。
162
+
163
+ 新记录默认具有 `global` 适用性。需要限制到当前 worktree/ref 时,可使用 `--branch-mode exact`;`--paths` 只保存精确项目相对路径约束,当前版本不会推断目录前缀、Git 祖先关系、依赖关系或语义相似路径。`ctx orient` 的 `explanation` 字段会有界地给出 v2/v1 选择方式、排除计数和各类别截断原因。
164
+
165
+ `ctx feedback use knowledge|deadend ... --event-id <stable-id>` 只记录一个幂等的 v2 使用信号。它只作为同等词法相关性结果的确定性排序因素,永不改变验证状态、confidence、evidence 或适用性;Orient、MCP 与 Hook 不会隐式写入该信号。
145
166
 
146
167
  ```bash
147
168
  ctx learn "订单取消后不能再次进入支付中状态" --scope project
@@ -222,6 +243,6 @@ npm pack --dry-run --json
222
243
 
223
244
  ## 当前范围
224
245
 
225
- 0.5.0 聚焦单项目、本地优先的 branch-aware Knowledge Runtime MVP:有界 Orient、显式 checkpoint、确定性 Knowledge/Deadend discovery、MCP `context_orient`、可选 Claude Code Hook,以及 v1 到 v2 的显式事件日志迁移和 Git-aware 本地 Runtime。完成迁移并启用 Runtime 后,Orient、MCP 与 Hook 使用当前 worktree/ref 的 workspace 选择分支正确的 Context,并在 v2 不可用时保持 v1 回退;Runtime 负责可靠捕获生命周期、Git 元数据与后续 v1 写入镜像。带稳定宿主标识的 Hook 事件仅以脱敏、限长 intent 进入 journal。
246
+ 0.6.0 聚焦单项目、本地优先、可治理的 branch-aware Knowledge Runtime MVP:有界且可解释的 Orient、显式 checkpoint、journal 派生的确定性 Knowledge/Deadend 检索、终态治理、显式使用反馈、MCP `context_orient`、可选 Claude Code Hook,以及 v1 到 v2 的显式事件日志迁移和 Git-aware 本地 Runtime。完成迁移并启用 Runtime 后,Orient、MCP 与 Hook 使用当前 worktree/ref 的 workspace 选择分支正确的 Context,并在 v2 不可用时保持 v1 回退;Runtime 负责可靠捕获生命周期、Git 元数据与后续 v1 写入镜像。带稳定宿主标识的 Hook 事件仅以脱敏、限长 intent 进入 journal。
226
247
 
227
248
  远程同步、多人协作、复杂语义检索、自动模型总结、自动 checkpoint、Context merge、自动验证候选共识,以及更多状态变更型 MCP 工具不属于当前版本范围。
@@ -1,8 +1,12 @@
1
1
  import type { AgentLoadOptions, AgentLoadResult, AgentSaveResult, CheckpointInput, ContextSearchOptions, ContextSearchResult } from '../runtime/types.js';
2
- import type { ContextOrientOptions, ContextOrientResult } from './types.js';
2
+ import type { ContextOrientOptions, ContextOrientResult, ContextUseInput } from './types.js';
3
3
  export declare function saveContext(startPath: string | undefined, input: CheckpointInput, options?: {
4
4
  minSaveIntervalMs?: number;
5
5
  }): Promise<AgentSaveResult>;
6
6
  export declare function loadContext(startPath: string | undefined, options?: AgentLoadOptions): Promise<AgentLoadResult>;
7
7
  export declare function contextOrient(startPath: string | undefined, options?: ContextOrientOptions): Promise<ContextOrientResult>;
8
+ export declare function recordContextUse(startPath: string | undefined, input: ContextUseInput): Promise<{
9
+ recorded: boolean;
10
+ eventId: string;
11
+ }>;
8
12
  export declare function searchContext(startPath: string | undefined, query: string, options?: ContextSearchOptions): Promise<ContextSearchResult>;
@@ -3,7 +3,9 @@ import { resolveProjectRoot } from '../project/project-resolver.js';
3
3
  import { contextsRoot, contextMetadataPath } from '../storage/layout.js';
4
4
  import { isRecord, readJson } from '../storage/json-store.js';
5
5
  import { checkpoint, rebuildSnapshot, resume, resumeSnapshot } from '../runtime/runtime.js';
6
+ import { recordUse } from '../cognition/feedback.js';
6
7
  import { selectV2Orientation } from '../cognition/orient.js';
8
+ import { discoverV2Deadends, discoverV2Knowledge } from '../cognition/retrieval.js';
7
9
  import { discoverDeadends, discoverKnowledge, listDeadends } from '../runtime/knowledge.js';
8
10
  import { listNotes } from '../runtime/notes.js';
9
11
  function normalized(value) {
@@ -176,6 +178,12 @@ export async function contextOrient(startPath, options = {}) {
176
178
  deadends: emptyDeadends(query ?? ''),
177
179
  notes: [],
178
180
  truncated: false,
181
+ explanation: {
182
+ mode: 'v1-fallback',
183
+ selection: 'v1',
184
+ retrieval: { knowledge: {}, deadends: {} },
185
+ truncation: { summary: false, knowledge: false, deadends: false, notes: false },
186
+ },
179
187
  };
180
188
  }
181
189
  throw error;
@@ -187,26 +195,30 @@ export async function contextOrient(startPath, options = {}) {
187
195
  limit: v2 ? Number.MAX_SAFE_INTEGER : options.noteLimit ?? 20,
188
196
  maxChars: options.maxChars ?? 4000,
189
197
  })).filter((note) => !v2 || v2.noteIds.has(note.noteId)).slice(0, options.noteLimit ?? 20);
190
- const knowledge = query === null
198
+ const v2Knowledge = query === null || !v2 ? null : discoverV2Knowledge(v2.retrieval, query, v2.currentGit, {
199
+ scope: options.scope,
200
+ limit: options.knowledgeLimit ?? 10,
201
+ maxChars: options.maxChars ?? 4000,
202
+ });
203
+ const v2Deadends = query === null || !v2 ? null : discoverV2Deadends(v2.retrieval, query, v2.currentGit, {
204
+ scope: options.scope,
205
+ limit: options.deadendLimit ?? 10,
206
+ maxChars: options.maxChars ?? 4000,
207
+ });
208
+ const applicableKnowledge = query === null
191
209
  ? emptyKnowledge('')
192
- : await discoverKnowledge(projectRoot, query, {
210
+ : v2Knowledge?.result ?? await discoverKnowledge(projectRoot, query, {
193
211
  scope: options.scope,
194
- limit: v2 ? Number.MAX_SAFE_INTEGER : options.knowledgeLimit ?? 10,
212
+ limit: options.knowledgeLimit ?? 10,
195
213
  maxChars: options.maxChars ?? 4000,
196
214
  });
197
- const deadends = query === null
215
+ const applicableDeadends = query === null
198
216
  ? emptyDeadends('')
199
- : await discoverDeadends(projectRoot, query, {
217
+ : v2Deadends?.result ?? await discoverDeadends(projectRoot, query, {
200
218
  scope: options.scope,
201
- limit: v2 ? Number.MAX_SAFE_INTEGER : options.deadendLimit ?? 10,
219
+ limit: options.deadendLimit ?? 10,
202
220
  maxChars: options.maxChars ?? 4000,
203
221
  });
204
- const applicableKnowledge = v2
205
- ? { ...knowledge, total: knowledge.hits.filter((hit) => v2.knowledgeIds.has(hit.knowledge.knowledgeId)).length, hits: knowledge.hits.filter((hit) => v2.knowledgeIds.has(hit.knowledge.knowledgeId)).slice(0, options.knowledgeLimit ?? 10) }
206
- : knowledge;
207
- const applicableDeadends = v2
208
- ? { ...deadends, total: deadends.hits.filter((hit) => v2.deadendIds.has(hit.deadend.deadendId)).length, hits: deadends.hits.filter((hit) => v2.deadendIds.has(hit.deadend.deadendId)).slice(0, options.deadendLimit ?? 10) }
209
- : deadends;
210
222
  const budget = { remaining: Math.max(0, options.maxChars ?? 4000) };
211
223
  const resumeSummary = {
212
224
  progressSummary: limited(loaded.resumeSummary.progressSummary, budget),
@@ -252,8 +264,25 @@ export async function contextOrient(startPath, options = {}) {
252
264
  deadends: limitedDeadendResult.result,
253
265
  notes: limitedNotesResult.notes,
254
266
  truncated: resumeTruncated || limitedKnowledgeResult.truncated || limitedDeadendResult.truncated || limitedNotesResult.truncated,
267
+ explanation: {
268
+ mode: v2 ? 'v2' : 'v1-fallback',
269
+ selection: v2?.selection ?? 'v1',
270
+ retrieval: {
271
+ knowledge: v2Knowledge?.exclusions ?? {},
272
+ deadends: v2Deadends?.exclusions ?? {},
273
+ },
274
+ truncation: {
275
+ summary: resumeTruncated,
276
+ knowledge: limitedKnowledgeResult.truncated,
277
+ deadends: limitedDeadendResult.truncated,
278
+ notes: limitedNotesResult.truncated,
279
+ },
280
+ },
255
281
  };
256
282
  }
283
+ export async function recordContextUse(startPath, input) {
284
+ return recordUse(startPath, input);
285
+ }
257
286
  export async function searchContext(startPath, query, options = {}) {
258
287
  const projectRoot = await resolveProjectRoot(startPath);
259
288
  const cleanQuery = query.trim();
@@ -1,3 +1,3 @@
1
1
  export * from './api.js';
2
- export type { ContextOrientContext, ContextOrientNoContextResult, ContextOrientOptions, ContextOrientReadyResult, ContextOrientResult, ContextOrientSnapshot, } from './types.js';
2
+ export type { ContextOrientContext, ContextUseInput, ContextOrientExplanation, ContextOrientNoContextResult, ContextOrientOptions, ContextOrientReadyResult, ContextOrientResult, ContextOrientSnapshot, } from './types.js';
3
3
  export type { AgentLoadOptions, AgentLoadResult, AgentSaveResult, ContextSearchHit, ContextSearchOptions, ContextSearchResult, } from '../runtime/types.js';
@@ -23,6 +23,25 @@ export interface ContextOrientSnapshot {
23
23
  branch: string | null;
24
24
  commit: string | null;
25
25
  }
26
+ export interface ContextUseInput {
27
+ entity: 'knowledge' | 'deadend';
28
+ entityId: string;
29
+ eventId: string;
30
+ }
31
+ export interface ContextOrientExplanation {
32
+ mode: 'v2' | 'v1-fallback';
33
+ selection: 'requested-context' | 'exact-worktree-ref' | 'legacy-ref-branch' | 'v1';
34
+ retrieval: {
35
+ knowledge: Record<'status_not_verified' | 'applicability_mismatch' | 'scope_mismatch' | 'query_no_match' | 'result_limit', number> | {};
36
+ deadends: Record<'status_not_verified' | 'applicability_mismatch' | 'scope_mismatch' | 'query_no_match' | 'result_limit', number> | {};
37
+ };
38
+ truncation: {
39
+ summary: boolean;
40
+ knowledge: boolean;
41
+ deadends: boolean;
42
+ notes: boolean;
43
+ };
44
+ }
26
45
  export interface ContextOrientReadyResult {
27
46
  status: 'ready';
28
47
  projectRoot: string;
@@ -34,6 +53,7 @@ export interface ContextOrientReadyResult {
34
53
  deadends: DeadendDiscoveryResult;
35
54
  notes: Note[];
36
55
  truncated: boolean;
56
+ explanation: ContextOrientExplanation;
37
57
  }
38
58
  export interface ContextOrientNoContextResult {
39
59
  status: 'no_context';
@@ -46,5 +66,6 @@ export interface ContextOrientNoContextResult {
46
66
  deadends: DeadendDiscoveryResult;
47
67
  notes: Note[];
48
68
  truncated: false;
69
+ explanation: ContextOrientExplanation;
49
70
  }
50
71
  export type ContextOrientResult = ContextOrientReadyResult | ContextOrientNoContextResult;
@@ -2,13 +2,17 @@
2
2
  import { checkpoint, resume } from '../runtime/runtime.js';
3
3
  import { contextOrient } from '../agent/api.js';
4
4
  import { importV1, verifyV1Import } from '../cognition/migration-v1.js';
5
+ import { recordUse } from '../cognition/feedback.js';
6
+ import { readJournal } from '../cognition/journal.js';
7
+ import { projectEvents } from '../cognition/projections.js';
5
8
  import { disableRuntime, enableRuntime, runtimeStatus, serveRuntime, startRuntime, stopRuntime } from '../cognition/runtime.js';
6
9
  import { migrationReadiness } from '../cognition/readiness.js';
7
10
  import { runClaudeCodeHook } from '../hooks/claude-code.js';
11
+ import { readGitWorktreeState } from '../git/git-adapter.js';
8
12
  import { inspectClaudeIntegration, installClaudeIntegration } from '../integrations/claude-code.js';
9
13
  import { doctor, status } from '../runtime/diagnostics.js';
10
14
  import { initProject } from '../runtime/init.js';
11
- import { discoverKnowledge, learnKnowledge, listKnowledge, verifyKnowledge, recordDeadend, listDeadends, verifyDeadend } from '../runtime/knowledge.js';
15
+ import { discoverKnowledge, learnKnowledge, listKnowledge, verifyKnowledge, recordDeadend, listDeadends, verifyDeadend, transitionKnowledge, transitionDeadend } from '../runtime/knowledge.js';
12
16
  import { addNote, listActivity, listNotes } from '../runtime/notes.js';
13
17
  import { VERSION } from '../version.js';
14
18
  function option(args, name) {
@@ -158,16 +162,23 @@ Options:
158
162
  --evidence <items> Comma-separated evidence references`,
159
163
  knowledge: `usage: ctx knowledge [--all] [--path <path>]
160
164
  ctx knowledge verify <knowledge-id> [--path <path>]
165
+ ctx knowledge deprecate|reject <knowledge-id> --reason <text> [--path <path>]
161
166
  ctx knowledge discover <query> [options]
162
167
 
163
- List candidate/verified knowledge, verify one item, or discover matching knowledge.
168
+ List, govern, or discover project knowledge.
164
169
 
165
170
  Options:
166
171
  --all Include unverified items
167
172
  --path <path> Project path`,
168
173
  'knowledge verify': `usage: ctx knowledge verify <knowledge-id> [--path <path>]
169
174
 
170
- Mark a knowledge item as verified.`,
175
+ Mark a candidate knowledge item as verified.`,
176
+ 'knowledge deprecate': `usage: ctx knowledge deprecate <knowledge-id> --reason <text> [--path <path>]
177
+
178
+ Retire a verified knowledge item with an explicit governance reason.`,
179
+ 'knowledge reject': `usage: ctx knowledge reject <knowledge-id> --reason <text> [--path <path>]
180
+
181
+ Reject a candidate knowledge item with an explicit governance reason.`,
171
182
  'knowledge discover': `usage: ctx knowledge discover <query> [options]
172
183
 
173
184
  Find matching project knowledge using deterministic lexical and alias rules.
@@ -181,8 +192,9 @@ Options:
181
192
  --max-chars <number> Maximum text characters per match`,
182
193
  deadend: `usage: ctx deadend [attempt] [options]
183
194
  ctx deadend verify <deadend-id> [--path <path>]
195
+ ctx deadend obsolete|reject <deadend-id> --reason <text> [--path <path>]
184
196
 
185
- Record or verify a candidate deadend.
197
+ Record or govern a candidate deadend.
186
198
 
187
199
  Options:
188
200
  --path <path> Project path
@@ -195,7 +207,25 @@ Options:
195
207
  --evidence <items> Comma-separated evidence references`,
196
208
  'deadend verify': `usage: ctx deadend verify <deadend-id> [--path <path>]
197
209
 
198
- Mark a deadend as verified.`,
210
+ Mark a candidate deadend as verified.`,
211
+ 'deadend obsolete': `usage: ctx deadend obsolete <deadend-id> --reason <text> [--path <path>]
212
+
213
+ Retire a verified deadend with an explicit governance reason.`,
214
+ 'deadend reject': `usage: ctx deadend reject <deadend-id> --reason <text> [--path <path>]
215
+
216
+ Reject a candidate deadend with an explicit governance reason.`,
217
+ feedback: `usage: ctx feedback use knowledge|deadend <id> --event-id <stable-id> [--path <path>]
218
+
219
+ Record explicit use of verified v2 retrieval material.`,
220
+ 'feedback use': `usage: ctx feedback use knowledge|deadend <id> --event-id <stable-id> [--path <path>]
221
+
222
+ Record an idempotent use signal that affects deterministic rank only.`,
223
+ journal: `usage: ctx journal verify [--path <path>]
224
+
225
+ Validate the v2 journal hash chain and replay projections in memory.`,
226
+ 'journal verify': `usage: ctx journal verify [--path <path>]
227
+
228
+ Validate journal integrity without writing derived state.`,
199
229
  deadends: `usage: ctx deadends [--all] [--path <path>]
200
230
 
201
231
  List recorded deadends.`,
@@ -282,7 +312,7 @@ Stop the local runtime through authenticated loopback IPC.`,
282
312
  Internal runtime daemon entry point.`,
283
313
  };
284
314
  function usage() {
285
- return `usage: ctx init|checkpoint|resume|orient|agent|hook|integrate|status|doctor|learn|knowledge|deadend|deadends|note|activity|migrate|runtime [options]
315
+ return `usage: ctx init|checkpoint|resume|orient|agent|hook|integrate|status|doctor|learn|knowledge|deadend|deadends|note|activity|migrate|runtime|feedback|journal [options]
286
316
 
287
317
  Run \"ctx <command> --help\" for command details.`;
288
318
  }
@@ -304,8 +334,10 @@ function printHelp(args) {
304
334
  const nested = (command === 'agent' && args[1] === 'serve')
305
335
  || (command === 'hook' && args[1] === 'claude-code')
306
336
  || (command === 'integrate' && args[1] === 'claude')
307
- || (command === 'knowledge' && ['verify', 'discover'].includes(args[1]))
308
- || (command === 'deadend' && args[1] === 'verify')
337
+ || (command === 'knowledge' && ['verify', 'discover', 'deprecate', 'reject'].includes(args[1]))
338
+ || (command === 'deadend' && ['verify', 'obsolete', 'reject'].includes(args[1]))
339
+ || (command === 'feedback' && args[1] === 'use')
340
+ || (command === 'journal' && args[1] === 'verify')
309
341
  || (command === 'note' && ['add', 'list'].includes(args[1]))
310
342
  || (command === 'migrate' && ['v1', 'verify'].includes(args[1]))
311
343
  || (command === 'runtime' && ['enable', 'disable', 'start', 'stop', 'status', 'serve'].includes(args[1]));
@@ -423,17 +455,20 @@ async function run(args) {
423
455
  print(await doctor(target));
424
456
  return;
425
457
  case 'learn': {
426
- const valueOptions = ['--path', '--kind', '--scope', '--context', '--snapshot', '--evidence'];
458
+ const valueOptions = ['--path', '--kind', '--scope', '--context', '--snapshot', '--evidence', '--branch-mode', '--paths'];
427
459
  validateOptions(args.slice(1), valueOptions, valueOptions);
428
460
  const statement = positionals(args.slice(1), valueOptions);
429
461
  if (statement.length === 0)
430
462
  throw new Error(HELP.learn);
463
+ const branchMode = enumOption(args, '--branch-mode', ['global', 'exact']) ?? 'global';
464
+ const git = branchMode === 'exact' ? await readGitWorktreeState(target ?? process.cwd()) : null;
431
465
  const input = {
432
466
  kind: option(args, '--kind'),
433
467
  scope: option(args, '--scope'),
434
468
  sourceContextId: option(args, '--context'),
435
469
  sourceSnapshotId: option(args, '--snapshot'),
436
470
  evidence: listOption(args, '--evidence'),
471
+ applicability: { project: true, branchMode, ...(branchMode === 'exact' ? { branches: [git?.ref ?? git?.branch ?? 'detached'], sourceCommit: git?.commit ?? null } : {}), ...(listOption(args, '--paths') ? { paths: listOption(args, '--paths') } : {}) },
437
472
  };
438
473
  print(await learnKnowledge(target, statement.join(' '), input));
439
474
  return;
@@ -466,6 +501,28 @@ async function run(args) {
466
501
  }
467
502
  return;
468
503
  }
504
+ case 'feedback': {
505
+ if (args[1] !== 'use' || !['knowledge', 'deadend'].includes(args[2]))
506
+ throw new Error(HELP.feedback);
507
+ validateOptions(args.slice(3), ['--path', '--event-id'], ['--path', '--event-id']);
508
+ const values = positionals(args.slice(3), ['--path', '--event-id']);
509
+ if (values.length !== 1)
510
+ throw new Error(HELP['feedback use']);
511
+ print(await recordUse(target, { entity: args[2], entityId: values[0], eventId: option(args, '--event-id') ?? '' }));
512
+ return;
513
+ }
514
+ case 'journal': {
515
+ if (args[1] !== 'verify')
516
+ throw new Error(HELP.journal);
517
+ validateOptions(args.slice(2), ['--path'], ['--path']);
518
+ validatePositionals(positionals(args.slice(2), ['--path']), 0, HELP['journal verify']);
519
+ const root = target ?? process.cwd();
520
+ const events = await readJournal(root);
521
+ const projection = projectEvents(events);
522
+ const last = events.at(-1);
523
+ 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 } });
524
+ return;
525
+ }
469
526
  case 'migrate': {
470
527
  if (args[1] === 'v1') {
471
528
  validateOptions(args.slice(2), ['--dry-run', '--apply', '--path'], ['--path']);
@@ -545,6 +602,13 @@ async function run(args) {
545
602
  throw new Error(HELP['knowledge verify']);
546
603
  print(await verifyKnowledge(target, values[0]));
547
604
  }
605
+ else if (args[1] === 'deprecate' || args[1] === 'reject') {
606
+ validateOptions(args.slice(2), ['--path', '--reason'], ['--path', '--reason']);
607
+ const values = positionals(args.slice(2), ['--path', '--reason']);
608
+ if (values.length !== 1)
609
+ throw new Error(HELP[`knowledge ${args[1]}`]);
610
+ print(await transitionKnowledge(target, values[0], args[1] === 'deprecate' ? 'deprecated' : 'rejected', option(args, '--reason') ?? ''));
611
+ }
548
612
  else {
549
613
  validateOptions(args.slice(1), ['--all', '--path'], ['--path']);
550
614
  validatePositionals(positionals(args.slice(1), ['--path']), 0, HELP.knowledge);
@@ -560,16 +624,26 @@ async function run(args) {
560
624
  throw new Error(HELP['deadend verify']);
561
625
  print(await verifyDeadend(target, values[0]));
562
626
  }
627
+ else if (args[1] === 'obsolete' || args[1] === 'reject') {
628
+ validateOptions(args.slice(2), ['--path', '--reason'], ['--path', '--reason']);
629
+ const values = positionals(args.slice(2), ['--path', '--reason']);
630
+ if (values.length !== 1)
631
+ throw new Error(HELP[`deadend ${args[1]}`]);
632
+ print(await transitionDeadend(target, values[0], args[1] === 'obsolete' ? 'obsolete' : 'rejected', option(args, '--reason') ?? ''));
633
+ }
563
634
  else {
564
- const valueOptions = ['--path', '--attempt', '--reason', '-m', '--scope', '--context', '--snapshot', '--evidence'];
635
+ const valueOptions = ['--path', '--attempt', '--reason', '-m', '--scope', '--context', '--snapshot', '--evidence', '--branch-mode', '--paths'];
565
636
  validateOptions(args.slice(1), valueOptions, valueOptions);
566
637
  const values = positionals(args.slice(1), valueOptions);
567
638
  validatePositionals(values, 1, HELP.deadend);
639
+ const branchMode = enumOption(args, '--branch-mode', ['global', 'exact']) ?? 'global';
640
+ const git = branchMode === 'exact' ? await readGitWorktreeState(target ?? process.cwd()) : null;
568
641
  const input = {
569
642
  scope: option(args, '--scope'),
570
643
  sourceContextId: option(args, '--context'),
571
644
  sourceSnapshotId: option(args, '--snapshot'),
572
645
  evidence: listOption(args, '--evidence'),
646
+ applicability: { project: true, branchMode, ...(branchMode === 'exact' ? { branches: [git?.ref ?? git?.branch ?? 'detached'], sourceCommit: git?.commit ?? null } : {}), ...(listOption(args, '--paths') ? { paths: listOption(args, '--paths') } : {}) },
573
647
  };
574
648
  print(await recordDeadend(target, option(args, '--attempt') ?? values[0], option(args, '--reason') ?? option(args, '-m') ?? '', input));
575
649
  }
@@ -0,0 +1,9 @@
1
+ export interface RecordUseInput {
2
+ entity: 'knowledge' | 'deadend';
3
+ entityId: string;
4
+ eventId: string;
5
+ }
6
+ export declare function recordUse(startPath: string | undefined, input: RecordUseInput): Promise<{
7
+ recorded: boolean;
8
+ eventId: string;
9
+ }>;
@@ -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
+ }
@@ -21,6 +21,16 @@ function sha256(value) {
21
21
  function isNotFound(error) {
22
22
  return typeof error === 'object' && error !== null && 'code' in error && error.code === 'ENOENT';
23
23
  }
24
+ function eventType(record, live = false) {
25
+ const status = record.record.status;
26
+ if (record.kind === 'context')
27
+ return live ? 'context.state.recorded' : 'context.checkpoint.imported';
28
+ if (record.kind === 'note')
29
+ return record.record.absorbedBySnapshotId ? 'note.absorbed' : record.record.status === 'resolved' ? 'note.resolved' : 'note.recorded';
30
+ if (record.kind === 'knowledge')
31
+ return status === 'verified' ? 'knowledge.verified' : status === 'deprecated' ? 'knowledge.deprecated' : status === 'rejected' ? 'knowledge.rejected' : 'knowledge.proposed';
32
+ return status === 'verified' ? 'deadend.verified' : status === 'obsolete' ? 'deadend.obsoleted' : status === 'rejected' ? 'deadend.rejected' : 'deadend.proposed';
33
+ }
24
34
  async function readOptional(filePath) {
25
35
  try {
26
36
  return await readFile(filePath, 'utf8');
@@ -129,12 +139,8 @@ export async function importV1(startPath) {
129
139
  await writeV2Metadata(projectRoot);
130
140
  const importedEventIds = [];
131
141
  for (const record of records) {
132
- const status = record.record.status;
133
142
  const event = await appendEvent(projectRoot, {
134
- type: record.kind === 'context' ? 'context.checkpoint.imported'
135
- : record.kind === 'note' ? 'note.recorded'
136
- : record.kind === 'knowledge' ? status === 'verified' ? 'knowledge.verified' : 'knowledge.proposed'
137
- : status === 'verified' ? 'deadend.verified' : 'deadend.proposed',
143
+ type: eventType(record),
138
144
  occurredAt: record.occurredAt,
139
145
  source: { kind: 'migration' },
140
146
  idempotencyKey: `v1:${record.sourceHash}:${record.kind}:${record.id}`,
@@ -158,12 +164,8 @@ export async function reconcileV1State(startPath) {
158
164
  return;
159
165
  const { records, sourceHash } = await sourceRecords(projectRoot);
160
166
  for (const record of records) {
161
- const status = record.record.status;
162
167
  await appendEvent(projectRoot, {
163
- type: record.kind === 'context' ? 'context.state.recorded'
164
- : record.kind === 'note' ? record.record.absorbedBySnapshotId ? 'note.absorbed' : record.record.status === 'resolved' ? 'note.resolved' : 'note.recorded'
165
- : record.kind === 'knowledge' ? status === 'verified' ? 'knowledge.verified' : 'knowledge.proposed'
166
- : status === 'verified' ? 'deadend.verified' : 'deadend.proposed',
168
+ type: eventType(record, true),
167
169
  occurredAt: record.occurredAt,
168
170
  source: { kind: 'migration' },
169
171
  idempotencyKey: `v1-live-sync:${sourceHash}:${record.kind}:${record.id}`,
@@ -1,9 +1,13 @@
1
+ import type { CognitionEvent, CognitionRetrievalProjection } from './types.js';
1
2
  export interface V2OrientationPlan {
2
3
  contextId: string;
3
4
  snapshotId: string;
4
5
  knowledgeIds: ReadonlySet<string>;
5
6
  deadendIds: ReadonlySet<string>;
6
7
  noteIds: ReadonlySet<string>;
8
+ retrieval: CognitionRetrievalProjection;
9
+ currentGit: CognitionEvent['git'];
10
+ selection: 'requested-context' | 'exact-worktree-ref' | 'legacy-ref-branch';
7
11
  }
8
12
  export declare function selectV2Orientation(projectRoot: string, contextId?: string): Promise<V2OrientationPlan | null>;
9
13
  export declare function v2Enabled(projectRoot: string): Promise<boolean>;
@@ -5,6 +5,7 @@ import { readJson } from '../storage/json-store.js';
5
5
  import { loadManifest } from '../runtime/init.js';
6
6
  import { readJournal } from './journal.js';
7
7
  import { projectEvents, workspaceKey } from './projections.js';
8
+ import { buildRetrievalProjection } from './retrieval.js';
8
9
  import { v1Synced } from './v1-bridge.js';
9
10
  function contextReference(value) {
10
11
  const boundary = value.lastIndexOf(':');
@@ -56,6 +57,7 @@ export async function selectV2Orientation(projectRoot, contextId) {
56
57
  return null;
57
58
  const projection = projectEvents(events);
58
59
  let selected = null;
60
+ let selection = 'requested-context';
59
61
  if (contextId) {
60
62
  const matching = [...Object.keys(projection.contexts)].reverse().find((reference) => contextReference(reference)?.contextId === contextId);
61
63
  selected = matching ? contextReference(matching) : null;
@@ -65,10 +67,17 @@ export async function selectV2Orientation(projectRoot, contextId) {
65
67
  const legacy = Object.values(projection.workspaces).find((workspace) => workspace.worktreeId === null && (workspace.ref === current.ref || workspace.branch === current.branch));
66
68
  const reference = (exact?.activeContextRef ? exact : legacy)?.activeContextRef;
67
69
  selected = reference ? contextReference(reference) : null;
70
+ selection = exact?.activeContextRef ? 'exact-worktree-ref' : 'legacy-ref-branch';
68
71
  }
69
72
  if (!selected || !projection.contexts[`${selected.contextId}:${selected.snapshotId}`])
70
73
  return null;
71
- return { ...selected, ...latestRecords(events, current, selected.contextId) };
74
+ return {
75
+ ...selected,
76
+ ...latestRecords(events, current, selected.contextId),
77
+ retrieval: buildRetrievalProjection(events),
78
+ currentGit: { worktreeId: current.worktreeId, branch: current.branch, ref: current.ref, head: current.commit },
79
+ selection,
80
+ };
72
81
  }
73
82
  catch {
74
83
  return null;
@@ -2,8 +2,9 @@ import crypto from 'node:crypto';
2
2
  import { readdir, rm } from 'node:fs/promises';
3
3
  import path from 'node:path';
4
4
  import { atomicWriteJson } from '../storage/atomic-write.js';
5
- import { cognitionBranchesDirectory, cognitionContextsProjectionPath, cognitionDeadendsProjectionPath, cognitionKnowledgeProjectionPath, cognitionNotesProjectionPath, cognitionProjectionCursorPath, } from '../storage/layout.js';
5
+ import { cognitionBranchesDirectory, cognitionContextsProjectionPath, cognitionDeadendsProjectionPath, cognitionKnowledgeProjectionPath, cognitionNotesProjectionPath, cognitionProjectionCursorPath, cognitionRetrievalProjectionPath, } from '../storage/layout.js';
6
6
  import { readJournal } from './journal.js';
7
+ import { buildRetrievalProjection } from './retrieval.js';
7
8
  export function workspaceKey(anchor) {
8
9
  return crypto.createHash('sha256')
9
10
  .update(`${anchor.worktreeId ?? 'default'}\0${anchor.ref ?? anchor.branch ?? 'detached'}`)
@@ -74,12 +75,12 @@ export function projectEvents(events) {
74
75
  if (noteId)
75
76
  projection.notes[noteId] = payload;
76
77
  }
77
- if ((event.type === 'knowledge.proposed' || event.type === 'knowledge.verified') && record) {
78
+ if (['knowledge.proposed', 'knowledge.verified', 'knowledge.deprecated', 'knowledge.rejected'].includes(event.type) && record) {
78
79
  const knowledgeId = recordId(record, 'knowledge');
79
80
  if (knowledgeId)
80
81
  projection.knowledge[knowledgeId] = payload;
81
82
  }
82
- if ((event.type === 'deadend.proposed' || event.type === 'deadend.verified') && record) {
83
+ if (['deadend.proposed', 'deadend.verified', 'deadend.obsoleted', 'deadend.rejected'].includes(event.type) && record) {
83
84
  const deadendId = recordId(record, 'deadend');
84
85
  if (deadendId)
85
86
  projection.deadends[deadendId] = payload;
@@ -107,6 +108,7 @@ async function removeObsoleteWorkspaceFiles(projectRoot, workspaceKeys) {
107
108
  export async function rebuildProjections(projectRoot) {
108
109
  const events = await readJournal(projectRoot);
109
110
  const projection = projectEvents(events);
111
+ const retrieval = buildRetrievalProjection(events);
110
112
  const last = events.at(-1);
111
113
  const cursor = {
112
114
  schemaVersion: 2,
@@ -120,6 +122,7 @@ export async function rebuildProjections(projectRoot) {
120
122
  atomicWriteJson(cognitionNotesProjectionPath(projectRoot), projection.notes),
121
123
  atomicWriteJson(cognitionKnowledgeProjectionPath(projectRoot), projection.knowledge),
122
124
  atomicWriteJson(cognitionDeadendsProjectionPath(projectRoot), projection.deadends),
125
+ atomicWriteJson(cognitionRetrievalProjectionPath(projectRoot), retrieval),
123
126
  ...Object.values(projection.workspaces).map((workspace) => atomicWriteJson(`${cognitionBranchesDirectory(projectRoot)}/${workspace.workspaceKey}.json`, workspace)),
124
127
  ]);
125
128
  await atomicWriteJson(cognitionProjectionCursorPath(projectRoot), cursor);
@@ -0,0 +1,26 @@
1
+ import type { DeadendDiscoveryResult, KnowledgeDiscoveryResult } from '../runtime/types.js';
2
+ import type { CognitionEvent, CognitionRetrievalProjection } from './types.js';
3
+ export declare function normalized(value: string): string;
4
+ export declare function queryTerms(query: string): string[];
5
+ export declare function buildRetrievalProjection(events: CognitionEvent[]): CognitionRetrievalProjection;
6
+ export interface RetrievalExclusions {
7
+ status_not_verified: number;
8
+ applicability_mismatch: number;
9
+ scope_mismatch: number;
10
+ query_no_match: number;
11
+ result_limit: number;
12
+ }
13
+ export interface V2Discovery<T> {
14
+ result: T;
15
+ exclusions: RetrievalExclusions;
16
+ }
17
+ export declare function discoverV2Knowledge(index: CognitionRetrievalProjection, query: string, current: CognitionEvent['git'], options: {
18
+ scope?: string;
19
+ limit: number;
20
+ maxChars: number;
21
+ }): V2Discovery<KnowledgeDiscoveryResult>;
22
+ export declare function discoverV2Deadends(index: CognitionRetrievalProjection, query: string, current: CognitionEvent['git'], options: {
23
+ scope?: string;
24
+ limit: number;
25
+ maxChars: number;
26
+ }): V2Discovery<DeadendDiscoveryResult>;
@@ -0,0 +1,273 @@
1
+ const aliasGroups = [
2
+ ['投保人认证', '投保人身份认证', '身份核验', '实名认证', 'applicant authentication', 'identity verification'],
3
+ ['短信验证码', '验证码', 'sms verification', 'otp'],
4
+ ['证件ocr', '证件识别', 'identity ocr', 'document ocr'],
5
+ ['人脸识别', '人脸认证', 'face recognition', 'facial verification'],
6
+ ];
7
+ export function normalized(value) {
8
+ return value.trim().toLocaleLowerCase();
9
+ }
10
+ export function queryTerms(query) {
11
+ const clean = normalized(query);
12
+ const terms = new Set(clean.split(/\s+/).filter(Boolean));
13
+ for (const group of aliasGroups) {
14
+ if (group.some((alias) => clean.includes(normalized(alias)))) {
15
+ for (const alias of group)
16
+ terms.add(normalized(alias));
17
+ }
18
+ }
19
+ return [...terms].sort();
20
+ }
21
+ function strings(record, entity) {
22
+ return entity === 'knowledge'
23
+ ? {
24
+ statement: typeof record.statement === 'string' ? record.statement : '',
25
+ kind: typeof record.kind === 'string' ? record.kind : '',
26
+ scope: typeof record.scope === 'string' ? record.scope : '',
27
+ }
28
+ : {
29
+ attempt: typeof record.attempt === 'string' ? record.attempt : '',
30
+ reason: typeof record.reason === 'string' ? record.reason : '',
31
+ scope: typeof record.scope === 'string' ? record.scope : '',
32
+ };
33
+ }
34
+ function defaultApplicability(event) {
35
+ if (event.git.ref === null && event.git.branch === null)
36
+ return { project: true, branchMode: 'global' };
37
+ return {
38
+ project: true,
39
+ branchMode: 'exact',
40
+ branches: [event.git.ref ?? event.git.branch ?? 'detached'],
41
+ sourceCommit: event.git.head,
42
+ };
43
+ }
44
+ function applicability(record, event) {
45
+ const value = record.applicability;
46
+ if (value && typeof value === 'object' && !Array.isArray(value)) {
47
+ const candidate = value;
48
+ if (candidate.project === true && (candidate.branchMode === 'global' || candidate.branchMode === 'exact')) {
49
+ return {
50
+ project: true,
51
+ branchMode: candidate.branchMode,
52
+ ...(Array.isArray(candidate.branches) ? { branches: candidate.branches.filter((item) => typeof item === 'string').sort() } : {}),
53
+ ...(Array.isArray(candidate.paths) ? { paths: candidate.paths.filter((item) => typeof item === 'string').sort() } : {}),
54
+ ...(typeof candidate.sourceCommit === 'string' || candidate.sourceCommit === null ? { sourceCommit: candidate.sourceCommit } : {}),
55
+ };
56
+ }
57
+ }
58
+ return defaultApplicability(event);
59
+ }
60
+ function recordPayload(event) {
61
+ const payload = event.payload;
62
+ return payload.record && typeof payload.record === 'object' && !Array.isArray(payload.record)
63
+ ? payload.record
64
+ : null;
65
+ }
66
+ function lifecycleEntity(event, record) {
67
+ if (event.type.startsWith('knowledge.') && typeof record.knowledgeId === 'string')
68
+ return { entity: 'knowledge', entityId: record.knowledgeId };
69
+ if (event.type.startsWith('deadend.') && typeof record.deadendId === 'string')
70
+ return { entity: 'deadend', entityId: record.deadendId };
71
+ return null;
72
+ }
73
+ function lifecycleEvent(event) {
74
+ return [
75
+ 'knowledge.proposed', 'knowledge.verified', 'knowledge.deprecated', 'knowledge.rejected',
76
+ 'deadend.proposed', 'deadend.verified', 'deadend.obsoleted', 'deadend.rejected',
77
+ ].includes(event.type);
78
+ }
79
+ export function buildRetrievalProjection(events) {
80
+ const entries = new Map();
81
+ const feedback = new Map();
82
+ for (const event of events) {
83
+ if (event.type === 'record.used') {
84
+ const payload = event.payload;
85
+ const entity = payload.entity === 'knowledge' || payload.entity === 'deadend' ? payload.entity : null;
86
+ const entityId = typeof payload.entityId === 'string' ? payload.entityId : null;
87
+ if (!entity || !entityId)
88
+ continue;
89
+ const key = `${entity}:${entityId}`;
90
+ const prior = feedback.get(key) ?? { usedCount: 0, latestUsedAt: event.occurredAt };
91
+ feedback.set(key, { usedCount: prior.usedCount + 1, latestUsedAt: event.occurredAt });
92
+ continue;
93
+ }
94
+ if (!lifecycleEvent(event))
95
+ continue;
96
+ const record = recordPayload(event);
97
+ if (!record)
98
+ continue;
99
+ const identity = lifecycleEntity(event, record);
100
+ if (!identity)
101
+ continue;
102
+ const fields = strings(record, identity.entity);
103
+ const normalizedTerms = [...new Set(Object.values(fields).flatMap((value) => {
104
+ const normalizedValue = normalized(value);
105
+ return normalizedValue ? [normalizedValue, ...normalizedValue.split(/\s+/).filter(Boolean)] : [];
106
+ }))].sort();
107
+ const key = `${identity.entity}:${identity.entityId}`;
108
+ entries.set(key, {
109
+ entity: identity.entity,
110
+ entityId: identity.entityId,
111
+ status: typeof record.status === 'string' ? record.status : 'candidate',
112
+ record,
113
+ applicability: applicability(record, event),
114
+ searchable: Object.fromEntries(Object.entries(fields).map(([field, value]) => [field, normalized(value)])),
115
+ normalizedTerms,
116
+ provenance: {
117
+ eventId: event.eventId,
118
+ eventType: event.type,
119
+ occurredAt: event.occurredAt,
120
+ git: event.git,
121
+ ...(typeof event.payload.anchorProvenance === 'string' ? { anchorProvenance: event.payload.anchorProvenance } : {}),
122
+ },
123
+ feedback: { usedCount: 0, latestUsedAt: null },
124
+ });
125
+ }
126
+ for (const [key, entry] of entries) {
127
+ const value = feedback.get(key);
128
+ if (value)
129
+ entry.feedback = value;
130
+ }
131
+ const ordered = Object.fromEntries([...entries.entries()].sort(([left], [right]) => left.localeCompare(right)));
132
+ const terms = new Map();
133
+ for (const [key, entry] of Object.entries(ordered)) {
134
+ for (const term of entry.normalizedTerms)
135
+ terms.set(term, [...(terms.get(term) ?? []), key]);
136
+ }
137
+ return {
138
+ schemaVersion: 2,
139
+ entries: ordered,
140
+ terms: Object.fromEntries([...terms.entries()].sort(([left], [right]) => left.localeCompare(right)).map(([term, ids]) => [term, ids.sort()])),
141
+ };
142
+ }
143
+ function emptyExclusions() {
144
+ return { status_not_verified: 0, applicability_mismatch: 0, scope_mismatch: 0, query_no_match: 0, result_limit: 0 };
145
+ }
146
+ function applicable(entry, current) {
147
+ if (entry.applicability.branchMode === 'global')
148
+ return true;
149
+ const anchor = entry.provenance.git;
150
+ return anchor.ref === current.ref
151
+ && anchor.branch === current.branch
152
+ && (anchor.worktreeId === null || anchor.worktreeId === current.worktreeId);
153
+ }
154
+ function match(entry, terms, entity) {
155
+ const fields = new Set();
156
+ const matched = new Set();
157
+ let score = 0;
158
+ const phrase = terms.join(' ');
159
+ const statementField = entity === 'knowledge' ? 'statement' : 'attempt';
160
+ if (entry.searchable[statementField]?.includes(phrase))
161
+ score += entity === 'knowledge' ? 20 : 0;
162
+ for (const term of terms) {
163
+ for (const [field, value] of Object.entries(entry.searchable)) {
164
+ if (!value.includes(term))
165
+ continue;
166
+ fields.add(field);
167
+ matched.add(term);
168
+ score += field === statementField ? 5 : field === 'scope' ? 4 : 3;
169
+ }
170
+ }
171
+ if (entry.searchable.scope === phrase)
172
+ score += 10;
173
+ return { score, matchedTerms: [...matched].sort(), matchedFields: [...fields].sort() };
174
+ }
175
+ function limitText(value, maxChars) {
176
+ return value.length <= maxChars ? value : `${value.slice(0, Math.max(0, maxChars - 1))}…`;
177
+ }
178
+ export function discoverV2Knowledge(index, query, current, options) {
179
+ const exclusions = emptyExclusions();
180
+ const terms = queryTerms(query);
181
+ const hits = Object.values(index.entries).flatMap((entry) => {
182
+ if (entry.entity !== 'knowledge')
183
+ return [];
184
+ if (entry.status !== 'verified') {
185
+ exclusions.status_not_verified += 1;
186
+ return [];
187
+ }
188
+ if (!applicable(entry, current)) {
189
+ exclusions.applicability_mismatch += 1;
190
+ return [];
191
+ }
192
+ const knowledge = entry.record;
193
+ if (options.scope && normalized(knowledge.scope) !== normalized(options.scope)) {
194
+ exclusions.scope_mismatch += 1;
195
+ return [];
196
+ }
197
+ const matched = match(entry, terms, 'knowledge');
198
+ if (matched.matchedTerms.length === 0) {
199
+ exclusions.query_no_match += 1;
200
+ return [];
201
+ }
202
+ return [{ entry, knowledge, ...matched }];
203
+ }).sort((left, right) => right.score - left.score
204
+ || Math.min(right.entry.feedback.usedCount, 5) - Math.min(left.entry.feedback.usedCount, 5)
205
+ || right.knowledge.confidence - left.knowledge.confidence
206
+ || right.entry.provenance.occurredAt.localeCompare(left.entry.provenance.occurredAt)
207
+ || left.knowledge.knowledgeId.localeCompare(right.knowledge.knowledgeId));
208
+ const selected = hits.slice(0, options.limit);
209
+ exclusions.result_limit = hits.length - selected.length;
210
+ return {
211
+ result: {
212
+ query,
213
+ total: hits.length,
214
+ truncated: hits.length > selected.length,
215
+ hits: selected.map(({ knowledge, score, matchedTerms, matchedFields, entry }) => ({
216
+ knowledge: { ...knowledge, statement: limitText(knowledge.statement, options.maxChars), supportingEvidence: knowledge.supportingEvidence.map((value) => limitText(value, options.maxChars)) },
217
+ score,
218
+ matchedTerms,
219
+ matchedFields,
220
+ why: [...matchedFields.map((field) => `matched ${field}`), ...(entry.feedback.usedCount > 0 ? ['prior recorded use'] : [])],
221
+ })),
222
+ duplicateIds: [],
223
+ possibleConflicts: [],
224
+ },
225
+ exclusions,
226
+ };
227
+ }
228
+ export function discoverV2Deadends(index, query, current, options) {
229
+ const exclusions = emptyExclusions();
230
+ const terms = queryTerms(query);
231
+ const hits = Object.values(index.entries).flatMap((entry) => {
232
+ if (entry.entity !== 'deadend')
233
+ return [];
234
+ if (entry.status !== 'verified') {
235
+ exclusions.status_not_verified += 1;
236
+ return [];
237
+ }
238
+ if (!applicable(entry, current)) {
239
+ exclusions.applicability_mismatch += 1;
240
+ return [];
241
+ }
242
+ const deadend = entry.record;
243
+ if (options.scope && normalized(deadend.scope) !== normalized(options.scope)) {
244
+ exclusions.scope_mismatch += 1;
245
+ return [];
246
+ }
247
+ const matched = match(entry, terms, 'deadend');
248
+ if (matched.matchedTerms.length === 0) {
249
+ exclusions.query_no_match += 1;
250
+ return [];
251
+ }
252
+ return [{ entry, deadend, ...matched }];
253
+ }).sort((left, right) => right.score - left.score
254
+ || Math.min(right.entry.feedback.usedCount, 5) - Math.min(left.entry.feedback.usedCount, 5)
255
+ || right.entry.provenance.occurredAt.localeCompare(left.entry.provenance.occurredAt)
256
+ || left.deadend.deadendId.localeCompare(right.deadend.deadendId));
257
+ const selected = hits.slice(0, options.limit);
258
+ exclusions.result_limit = hits.length - selected.length;
259
+ return {
260
+ result: {
261
+ query,
262
+ total: hits.length,
263
+ truncated: hits.length > selected.length,
264
+ hits: selected.map(({ deadend, score, matchedTerms, matchedFields }) => ({
265
+ deadend: { ...deadend, attempt: limitText(deadend.attempt, options.maxChars), reason: limitText(deadend.reason, options.maxChars), observedEvidence: deadend.observedEvidence.map((value) => limitText(value, options.maxChars)) },
266
+ score,
267
+ matchedTerms,
268
+ matchedFields,
269
+ })),
270
+ },
271
+ exclusions,
272
+ };
273
+ }
@@ -1,6 +1,6 @@
1
1
  export declare const COGNITION_LAYOUT_VERSION: "event-journal-v2";
2
2
  export type CognitionEventSource = 'cli' | 'claude-hook' | 'mcp' | 'git-observer' | 'fs-observer' | 'migration';
3
- export type CognitionEventType = 'runtime.enabled' | 'runtime.started' | 'runtime.stopped' | 'host.session.started' | 'host.prompt.submitted' | 'git.state.observed' | 'git.branch.changed' | 'git.head.changed' | 'workspace.paths.changed' | 'context.checkpoint.imported' | 'context.state.recorded' | 'note.recorded' | 'note.resolved' | 'note.absorbed' | 'knowledge.proposed' | 'knowledge.verified' | 'knowledge.deprecated' | 'deadend.proposed' | 'deadend.verified' | 'deadend.obsoleted' | 'projection.advanced' | 'projection.failed';
3
+ export type CognitionEventType = 'runtime.enabled' | 'runtime.started' | 'runtime.stopped' | 'host.session.started' | 'host.prompt.submitted' | 'git.state.observed' | 'git.branch.changed' | 'git.head.changed' | 'workspace.paths.changed' | 'context.checkpoint.imported' | 'context.state.recorded' | 'note.recorded' | 'note.resolved' | 'note.absorbed' | 'knowledge.proposed' | 'knowledge.verified' | 'knowledge.deprecated' | 'knowledge.rejected' | 'deadend.proposed' | 'deadend.verified' | 'deadend.obsoleted' | 'deadend.rejected' | 'record.used' | 'projection.advanced' | 'projection.failed';
4
4
  export interface CognitionGitAnchor {
5
5
  worktreeId: string | null;
6
6
  branch: string | null;
@@ -53,6 +53,44 @@ export interface Applicability {
53
53
  sourceCommit?: string | null;
54
54
  dependencyConstraints?: string[];
55
55
  }
56
+ export interface CognitionLifecycleTransition {
57
+ entity: 'knowledge' | 'deadend';
58
+ entityId: string;
59
+ from: 'candidate' | 'verified';
60
+ to: 'verified' | 'deprecated' | 'rejected' | 'obsolete';
61
+ reason?: string;
62
+ }
63
+ export interface CognitionUseFeedback {
64
+ feedbackId: string;
65
+ entity: 'knowledge' | 'deadend';
66
+ entityId: string;
67
+ outcome: 'used';
68
+ }
69
+ export interface CognitionRetrievalEntry {
70
+ entity: 'knowledge' | 'deadend';
71
+ entityId: string;
72
+ status: string;
73
+ record: Record<string, unknown>;
74
+ applicability: Applicability;
75
+ searchable: Record<string, string>;
76
+ normalizedTerms: string[];
77
+ provenance: {
78
+ eventId: string;
79
+ eventType: CognitionEventType;
80
+ occurredAt: string;
81
+ git: CognitionGitAnchor;
82
+ anchorProvenance?: CognitionRecordPayload['anchorProvenance'];
83
+ };
84
+ feedback: {
85
+ usedCount: number;
86
+ latestUsedAt: string | null;
87
+ };
88
+ }
89
+ export interface CognitionRetrievalProjection {
90
+ schemaVersion: 2;
91
+ entries: Record<string, CognitionRetrievalEntry>;
92
+ terms: Record<string, string[]>;
93
+ }
56
94
  export interface BranchWorkspace {
57
95
  schemaVersion: 2;
58
96
  workspaceKey: string;
@@ -67,8 +67,16 @@ export async function mirrorNote(projectRoot, note, absorbed = false) {
67
67
  await mirror(projectRoot, absorbed ? 'note.absorbed' : 'note.recorded', note, note.noteId);
68
68
  }
69
69
  export async function mirrorKnowledge(projectRoot, knowledge) {
70
- await mirror(projectRoot, knowledge.status === 'verified' ? 'knowledge.verified' : 'knowledge.proposed', knowledge, knowledge.knowledgeId);
70
+ const type = knowledge.status === 'verified' ? 'knowledge.verified'
71
+ : knowledge.status === 'deprecated' ? 'knowledge.deprecated'
72
+ : knowledge.status === 'rejected' ? 'knowledge.rejected'
73
+ : 'knowledge.proposed';
74
+ await mirror(projectRoot, type, knowledge, knowledge.knowledgeId);
71
75
  }
72
76
  export async function mirrorDeadend(projectRoot, deadend) {
73
- await mirror(projectRoot, deadend.status === 'verified' ? 'deadend.verified' : 'deadend.proposed', deadend, deadend.deadendId);
77
+ const type = deadend.status === 'verified' ? 'deadend.verified'
78
+ : deadend.status === 'obsolete' ? 'deadend.obsoleted'
79
+ : deadend.status === 'rejected' ? 'deadend.rejected'
80
+ : 'deadend.proposed';
81
+ await mirror(projectRoot, type, deadend, deadend.deadendId);
74
82
  }
@@ -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,6 +22,7 @@ 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;
@@ -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
  }
@@ -1 +1 @@
1
- export declare const VERSION = "0.5.0";
1
+ export declare const VERSION = "0.6.0";
@@ -1 +1 @@
1
- export const VERSION = '0.5.0';
1
+ export const VERSION = '0.6.0';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fluffy-context",
3
- "version": "0.5.0",
3
+ "version": "0.6.0",
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",