fluffy-context 0.3.1 → 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.
Files changed (43) hide show
  1. package/README.md +61 -5
  2. package/dist/src/agent/api.d.ts +5 -1
  3. package/dist/src/agent/api.js +50 -10
  4. package/dist/src/agent/index.d.ts +1 -1
  5. package/dist/src/agent/types.d.ts +21 -0
  6. package/dist/src/cli/main.js +177 -9
  7. package/dist/src/cognition/feedback.d.ts +9 -0
  8. package/dist/src/cognition/feedback.js +30 -0
  9. package/dist/src/cognition/git-observer.d.ts +13 -0
  10. package/dist/src/cognition/git-observer.js +61 -0
  11. package/dist/src/cognition/journal.d.ts +24 -0
  12. package/dist/src/cognition/journal.js +113 -0
  13. package/dist/src/cognition/migration-v1.d.ts +7 -0
  14. package/dist/src/cognition/migration-v1.js +201 -0
  15. package/dist/src/cognition/orient.d.ts +13 -0
  16. package/dist/src/cognition/orient.js +94 -0
  17. package/dist/src/cognition/projections.d.ts +7 -0
  18. package/dist/src/cognition/projections.js +130 -0
  19. package/dist/src/cognition/readiness.d.ts +2 -0
  20. package/dist/src/cognition/readiness.js +81 -0
  21. package/dist/src/cognition/retrieval.d.ts +26 -0
  22. package/dist/src/cognition/retrieval.js +273 -0
  23. package/dist/src/cognition/runtime.d.ts +12 -0
  24. package/dist/src/cognition/runtime.js +295 -0
  25. package/dist/src/cognition/types.d.ts +170 -0
  26. package/dist/src/cognition/types.js +1 -0
  27. package/dist/src/cognition/v1-bridge.d.ts +7 -0
  28. package/dist/src/cognition/v1-bridge.js +82 -0
  29. package/dist/src/git/git-adapter.d.ts +7 -0
  30. package/dist/src/git/git-adapter.js +16 -6
  31. package/dist/src/hooks/claude-code.js +53 -0
  32. package/dist/src/runtime/diagnostics.js +6 -0
  33. package/dist/src/runtime/knowledge.d.ts +2 -0
  34. package/dist/src/runtime/knowledge.js +68 -0
  35. package/dist/src/runtime/notes.js +8 -1
  36. package/dist/src/runtime/runtime.d.ts +1 -0
  37. package/dist/src/runtime/runtime.js +28 -0
  38. package/dist/src/runtime/types.d.ts +19 -0
  39. package/dist/src/storage/layout.d.ts +19 -0
  40. package/dist/src/storage/layout.js +57 -0
  41. package/dist/src/version.d.ts +1 -1
  42. package/dist/src/version.js +1 -1
  43. package/package.json +2 -2
package/README.md CHANGED
@@ -1,8 +1,23 @@
1
1
  # Context Runtime
2
2
 
3
- `fluffy-context` 是面向 AI 编程会话的本地 Context Runtime CLI。0.3.1 提供可重复、安全调用的任务定位入口、MCP 工具,以及 Claude Code 的阶段性工作流引导,让 Agent 不必自行拼接恢复、共识发现与短期记录。
3
+ `fluffy-context` 正在从面向 AI 编程会话的本地 Context Runtime CLI,演进为 **Agent 无关的本地 Knowledge Runtime**。它负责自动捕获开发过程中产生的事件、候选共识、问题、不可达路径和分支工作状态,并向任意 Agent 注入有界、可验证的业务认知。
4
4
 
5
- 它更接近“Context 的 Git”,而不是代码备份工具:Git 仍然负责代码和真实文件变更;Context Runtime 负责 AI 工作状态、已验证共识与交接信息的本地保存和恢复。
5
+ Git 仍然负责代码和真实文件变更,Issue 系统仍然负责任务,项目文档仍然负责设计;`fluffy-context` 补齐的是持续演进的项目认知层。最终架构是 `project → knowledge runtime → LLM`:CLI、MCP、Claude Hook 和未来 IDE/Agent 集成只是事件入口与信息注入适配器,而不是认知本体。
6
+
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 行为。
6
21
 
7
22
  ## 适用场景
8
23
 
@@ -91,11 +106,48 @@ ctx note add "测试环境缺少回调凭据" --kind problem --context <context-
91
106
  ctx activity --context <context-id> --open
92
107
  ctx knowledge discover "支付回调"
93
108
  ctx knowledge verify <knowledge-id>
109
+ ctx knowledge deprecate <knowledge-id> --reason "已被新的结论取代"
94
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
114
+ ctx migrate v1 --dry-run
115
+ ctx migrate v1 --apply
116
+ ctx migrate verify
117
+ ctx runtime enable
118
+ ctx runtime start
119
+ ctx runtime status
120
+ ctx runtime stop
95
121
  ctx status
96
122
  ctx doctor
97
123
  ```
98
124
 
125
+ ### v2 迁移与本地 Runtime
126
+
127
+ v2 与既有 `.context/` 数据并存,迁移绝不会重写或修复 v1 文件。先执行只读检查,再显式导入并验证投影一致性:
128
+
129
+ ```bash
130
+ ctx migrate v1 --dry-run
131
+ ctx migrate v1 --apply
132
+ ctx migrate verify
133
+ ```
134
+
135
+ 只有完成迁移后才可启用 Runtime:
136
+
137
+ ```bash
138
+ ctx runtime enable
139
+ ctx runtime start
140
+ ctx runtime status
141
+ ctx runtime stop
142
+ ctx runtime disable
143
+ ```
144
+
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 或将候选信息提升为已验证共识。
146
+
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 写入镜像。
148
+
149
+ Claude Hook 仅在宿主提供稳定的 session 与 event 标识时记录 `host.session.started` 或 `host.prompt.submitted`。提示只保存按 `promptIntentMaxChars` 截断并经过敏感值脱敏后的 intent,绝不保存原始提示;缺少稳定标识时不写入事件。默认 Hook 不启动守护进程;将 v2 policy 的 `allowHookStartup` 设为 `true` 后,SessionStart 才会在严格超时内尽力启动 Runtime,失败仍会 fail open。UserPromptSubmit 不会启动 Runtime。
150
+
99
151
  ### `ctx checkpoint` 和 `ctx resume`
100
152
 
101
153
  第一次 checkpoint 创建 baseline,后续变化创建 patch。相同内容返回 `no_change`;短时间内的变化可能返回 `rate_limited`,应在完成更多阶段工作后再保存。`ctx resume` 返回轻量摘要和按需使用的完整 details,并会保留其既有的 `lastUsedAt` 更新语义。
@@ -106,7 +158,11 @@ ctx doctor
106
158
 
107
159
  ### Knowledge 与 Deadend
108
160
 
109
- `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 不会隐式写入该信号。
110
166
 
111
167
  ```bash
112
168
  ctx learn "订单取消后不能再次进入支付中状态" --scope project
@@ -187,6 +243,6 @@ npm pack --dry-run --json
187
243
 
188
244
  ## 当前范围
189
245
 
190
- 0.3.1 聚焦单项目、本地优先的可靠闭环和 Agent 采用路径:有界 Orient、显式 checkpoint、确定性 Knowledge/Deadend discovery、MCP `context_orient` 与可选的 Claude Code Hook 集成。
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。
191
247
 
192
- 远程同步、多人协作、复杂语义检索、自动模型总结、自动 checkpoint、Context merge,以及更多状态变更型 MCP 工具不属于当前版本范围。
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>;
@@ -2,7 +2,10 @@ import { readdir } from 'node:fs/promises';
2
2
  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
- import { checkpoint, rebuildSnapshot, resume } from '../runtime/runtime.js';
5
+ import { checkpoint, rebuildSnapshot, resume, resumeSnapshot } from '../runtime/runtime.js';
6
+ import { recordUse } from '../cognition/feedback.js';
7
+ import { selectV2Orientation } from '../cognition/orient.js';
8
+ import { discoverV2Deadends, discoverV2Knowledge } from '../cognition/retrieval.js';
6
9
  import { discoverDeadends, discoverKnowledge, listDeadends } from '../runtime/knowledge.js';
7
10
  import { listNotes } from '../runtime/notes.js';
8
11
  function normalized(value) {
@@ -157,7 +160,10 @@ export async function contextOrient(startPath, options = {}) {
157
160
  throw new Error('context orient query must not be empty');
158
161
  let loaded;
159
162
  try {
160
- loaded = await resume(projectRoot, options.contextId, options.maxChars ?? 4000, { touchLastUsedAt: false });
163
+ const v2 = await selectV2Orientation(projectRoot, options.contextId);
164
+ loaded = v2
165
+ ? await resumeSnapshot(projectRoot, v2.contextId, v2.snapshotId, options.maxChars ?? 4000)
166
+ : await resume(projectRoot, options.contextId, options.maxChars ?? 4000, { touchLastUsedAt: false });
161
167
  }
162
168
  catch (error) {
163
169
  if (error instanceof Error && error.message === 'no active context found') {
@@ -172,26 +178,43 @@ export async function contextOrient(startPath, options = {}) {
172
178
  deadends: emptyDeadends(query ?? ''),
173
179
  notes: [],
174
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
+ },
175
187
  };
176
188
  }
177
189
  throw error;
178
190
  }
179
- const notes = await listNotes(projectRoot, {
191
+ const v2 = await selectV2Orientation(projectRoot, options.contextId);
192
+ const notes = (await listNotes(projectRoot, {
180
193
  contextId: loaded.context.id,
181
194
  openOnly: true,
182
- limit: options.noteLimit ?? 20,
195
+ limit: v2 ? Number.MAX_SAFE_INTEGER : options.noteLimit ?? 20,
196
+ maxChars: options.maxChars ?? 4000,
197
+ })).filter((note) => !v2 || v2.noteIds.has(note.noteId)).slice(0, options.noteLimit ?? 20);
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,
183
206
  maxChars: options.maxChars ?? 4000,
184
207
  });
185
- const knowledge = query === null
208
+ const applicableKnowledge = query === null
186
209
  ? emptyKnowledge('')
187
- : await discoverKnowledge(projectRoot, query, {
210
+ : v2Knowledge?.result ?? await discoverKnowledge(projectRoot, query, {
188
211
  scope: options.scope,
189
212
  limit: options.knowledgeLimit ?? 10,
190
213
  maxChars: options.maxChars ?? 4000,
191
214
  });
192
- const deadends = query === null
215
+ const applicableDeadends = query === null
193
216
  ? emptyDeadends('')
194
- : await discoverDeadends(projectRoot, query, {
217
+ : v2Deadends?.result ?? await discoverDeadends(projectRoot, query, {
195
218
  scope: options.scope,
196
219
  limit: options.deadendLimit ?? 10,
197
220
  maxChars: options.maxChars ?? 4000,
@@ -214,8 +237,8 @@ export async function contextOrient(startPath, options = {}) {
214
237
  || textLength(resumeSummary.decisions) < textLength(loaded.resumeSummary.decisions)
215
238
  || textLength(resumeSummary.risks) < textLength(loaded.resumeSummary.risks)
216
239
  || textLength(resumeSummary.relatedFiles) < textLength(loaded.resumeSummary.relatedFiles);
217
- const limitedKnowledgeResult = limitKnowledge(knowledge, budget);
218
- const limitedDeadendResult = limitDeadends(deadends, budget);
240
+ const limitedKnowledgeResult = limitKnowledge(applicableKnowledge, budget);
241
+ const limitedDeadendResult = limitDeadends(applicableDeadends, budget);
219
242
  const limitedNotesResult = limitNotes(notes, budget);
220
243
  return {
221
244
  status: 'ready',
@@ -241,8 +264,25 @@ export async function contextOrient(startPath, options = {}) {
241
264
  deadends: limitedDeadendResult.result,
242
265
  notes: limitedNotesResult.notes,
243
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
+ },
244
281
  };
245
282
  }
283
+ export async function recordContextUse(startPath, input) {
284
+ return recordUse(startPath, input);
285
+ }
246
286
  export async function searchContext(startPath, query, options = {}) {
247
287
  const projectRoot = await resolveProjectRoot(startPath);
248
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;
@@ -1,11 +1,18 @@
1
1
  #!/usr/bin/env node
2
2
  import { checkpoint, resume } from '../runtime/runtime.js';
3
3
  import { contextOrient } from '../agent/api.js';
4
+ import { 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';
8
+ import { disableRuntime, enableRuntime, runtimeStatus, serveRuntime, startRuntime, stopRuntime } from '../cognition/runtime.js';
9
+ import { migrationReadiness } from '../cognition/readiness.js';
4
10
  import { runClaudeCodeHook } from '../hooks/claude-code.js';
11
+ import { readGitWorktreeState } from '../git/git-adapter.js';
5
12
  import { inspectClaudeIntegration, installClaudeIntegration } from '../integrations/claude-code.js';
6
13
  import { doctor, status } from '../runtime/diagnostics.js';
7
14
  import { initProject } from '../runtime/init.js';
8
- 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';
9
16
  import { addNote, listActivity, listNotes } from '../runtime/notes.js';
10
17
  import { VERSION } from '../version.js';
11
18
  function option(args, name) {
@@ -155,16 +162,23 @@ Options:
155
162
  --evidence <items> Comma-separated evidence references`,
156
163
  knowledge: `usage: ctx knowledge [--all] [--path <path>]
157
164
  ctx knowledge verify <knowledge-id> [--path <path>]
165
+ ctx knowledge deprecate|reject <knowledge-id> --reason <text> [--path <path>]
158
166
  ctx knowledge discover <query> [options]
159
167
 
160
- List candidate/verified knowledge, verify one item, or discover matching knowledge.
168
+ List, govern, or discover project knowledge.
161
169
 
162
170
  Options:
163
171
  --all Include unverified items
164
172
  --path <path> Project path`,
165
173
  'knowledge verify': `usage: ctx knowledge verify <knowledge-id> [--path <path>]
166
174
 
167
- 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.`,
168
182
  'knowledge discover': `usage: ctx knowledge discover <query> [options]
169
183
 
170
184
  Find matching project knowledge using deterministic lexical and alias rules.
@@ -178,8 +192,9 @@ Options:
178
192
  --max-chars <number> Maximum text characters per match`,
179
193
  deadend: `usage: ctx deadend [attempt] [options]
180
194
  ctx deadend verify <deadend-id> [--path <path>]
195
+ ctx deadend obsolete|reject <deadend-id> --reason <text> [--path <path>]
181
196
 
182
- Record or verify a candidate deadend.
197
+ Record or govern a candidate deadend.
183
198
 
184
199
  Options:
185
200
  --path <path> Project path
@@ -192,7 +207,25 @@ Options:
192
207
  --evidence <items> Comma-separated evidence references`,
193
208
  'deadend verify': `usage: ctx deadend verify <deadend-id> [--path <path>]
194
209
 
195
- 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.`,
196
229
  deadends: `usage: ctx deadends [--all] [--path <path>]
197
230
 
198
231
  List recorded deadends.`,
@@ -235,9 +268,51 @@ Options:
235
268
  --open Only open notes
236
269
  --limit <number> Maximum items (default: 50)
237
270
  --max-chars <number> Maximum message characters`,
271
+ migrate: `usage: ctx migrate v1 --dry-run|--apply [--path <path>]
272
+ ctx migrate verify [--path <path>]
273
+
274
+ Inspect, explicitly import, or verify v1 Context data for the event-journal runtime.`,
275
+ 'migrate v1': `usage: ctx migrate v1 --dry-run|--apply [--path <path>]
276
+
277
+ Validate v1 Context, Snapshot, Note, Knowledge, and Deadend data for v2 migration.
278
+
279
+ Options:
280
+ --dry-run Inspect only; no v2 data is created
281
+ --apply Explicitly append immutable v1 provenance events
282
+ --path <path> Project path`,
283
+ 'migrate verify': `usage: ctx migrate verify [--path <path>]
284
+
285
+ Rebuild v2 projections and verify imported v1 parity.
286
+
287
+ Options:
288
+ --path <path> Project path`,
289
+ runtime: `usage: ctx runtime enable|disable|start|stop|status [--path <path>]
290
+
291
+ Control the local event-driven Knowledge Runtime after v1 migration verification.`,
292
+ 'runtime status': `usage: ctx runtime status [--path <path>]
293
+
294
+ Show the read-only runtime policy, migration and authenticated lease status.
295
+
296
+ Options:
297
+ --path <path> Project path`,
298
+ 'runtime enable': `usage: ctx runtime enable [--path <path>]
299
+
300
+ Explicitly enable the migrated local Knowledge Runtime.`,
301
+ 'runtime disable': `usage: ctx runtime disable [--path <path>]
302
+
303
+ Stop the local runtime and disable future startup without deleting v2 history.`,
304
+ 'runtime start': `usage: ctx runtime start [--path <path>]
305
+
306
+ Start the enabled runtime as a loopback-only background process.`,
307
+ 'runtime stop': `usage: ctx runtime stop [--path <path>]
308
+
309
+ Stop the local runtime through authenticated loopback IPC.`,
310
+ 'runtime serve': `usage: ctx runtime serve [--path <path>]
311
+
312
+ Internal runtime daemon entry point.`,
238
313
  };
239
314
  function usage() {
240
- return `usage: ctx init|checkpoint|resume|orient|agent|hook|integrate|status|doctor|learn|knowledge|deadend|deadends|note|activity [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]
241
316
 
242
317
  Run \"ctx <command> --help\" for command details.`;
243
318
  }
@@ -256,7 +331,17 @@ function printHelp(args) {
256
331
  process.stdout.write(`${usage()}\n`);
257
332
  return true;
258
333
  }
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;
334
+ const nested = (command === 'agent' && args[1] === 'serve')
335
+ || (command === 'hook' && args[1] === 'claude-code')
336
+ || (command === 'integrate' && args[1] === 'claude')
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')
341
+ || (command === 'note' && ['add', 'list'].includes(args[1]))
342
+ || (command === 'migrate' && ['v1', 'verify'].includes(args[1]))
343
+ || (command === 'runtime' && ['enable', 'disable', 'start', 'stop', 'status', 'serve'].includes(args[1]));
344
+ const key = nested ? `${command} ${args[1]}` : command;
260
345
  const nestedKey = key === 'integrate claude' && (args[2] === 'inspect' || args[2] === 'install') ? `${key} ${args[2]}` : key;
261
346
  const help = HELP[nestedKey];
262
347
  if (!help)
@@ -370,17 +455,20 @@ async function run(args) {
370
455
  print(await doctor(target));
371
456
  return;
372
457
  case 'learn': {
373
- const valueOptions = ['--path', '--kind', '--scope', '--context', '--snapshot', '--evidence'];
458
+ const valueOptions = ['--path', '--kind', '--scope', '--context', '--snapshot', '--evidence', '--branch-mode', '--paths'];
374
459
  validateOptions(args.slice(1), valueOptions, valueOptions);
375
460
  const statement = positionals(args.slice(1), valueOptions);
376
461
  if (statement.length === 0)
377
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;
378
465
  const input = {
379
466
  kind: option(args, '--kind'),
380
467
  scope: option(args, '--scope'),
381
468
  sourceContextId: option(args, '--context'),
382
469
  sourceSnapshotId: option(args, '--snapshot'),
383
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') } : {}) },
384
472
  };
385
473
  print(await learnKnowledge(target, statement.join(' '), input));
386
474
  return;
@@ -413,6 +501,69 @@ async function run(args) {
413
501
  }
414
502
  return;
415
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
+ }
526
+ case 'migrate': {
527
+ if (args[1] === 'v1') {
528
+ validateOptions(args.slice(2), ['--dry-run', '--apply', '--path'], ['--path']);
529
+ validatePositionals(positionals(args.slice(2), ['--path']), 0, HELP['migrate v1']);
530
+ if (args.includes('--dry-run') === args.includes('--apply'))
531
+ throw new Error(HELP['migrate v1']);
532
+ print(args.includes('--apply') ? await importV1(target) : await migrationReadiness(target));
533
+ }
534
+ else if (args[1] === 'verify') {
535
+ validateOptions(args.slice(2), ['--path'], ['--path']);
536
+ validatePositionals(positionals(args.slice(2), ['--path']), 0, HELP['migrate verify']);
537
+ print(await verifyV1Import(target));
538
+ }
539
+ else {
540
+ throw new Error(HELP.migrate);
541
+ }
542
+ return;
543
+ }
544
+ case 'runtime': {
545
+ if (!['enable', 'disable', 'start', 'stop', 'status', 'serve'].includes(args[1]))
546
+ throw new Error(HELP.runtime);
547
+ validateOptions(args.slice(2), ['--path'], ['--path']);
548
+ validatePositionals(positionals(args.slice(2), ['--path']), 0, HELP[`runtime ${args[1]}`]);
549
+ if (args[1] === 'enable')
550
+ print(await enableRuntime(target));
551
+ else if (args[1] === 'disable')
552
+ print(await disableRuntime(target));
553
+ else if (args[1] === 'start')
554
+ print(await startRuntime(target));
555
+ else if (args[1] === 'stop')
556
+ print(await stopRuntime(target));
557
+ else if (args[1] === 'status')
558
+ print(await runtimeStatus(target));
559
+ else {
560
+ const runtime = await serveRuntime(target);
561
+ process.once('SIGINT', () => { void runtime.close('signal'); });
562
+ process.once('SIGTERM', () => { void runtime.close('signal'); });
563
+ await new Promise(() => undefined);
564
+ }
565
+ return;
566
+ }
416
567
  case 'activity': {
417
568
  const valueOptions = ['--path', '--context', '--since', '--limit', '--max-chars'];
418
569
  validateOptions(args.slice(1), [...valueOptions, '--open'], valueOptions);
@@ -451,6 +602,13 @@ async function run(args) {
451
602
  throw new Error(HELP['knowledge verify']);
452
603
  print(await verifyKnowledge(target, values[0]));
453
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
+ }
454
612
  else {
455
613
  validateOptions(args.slice(1), ['--all', '--path'], ['--path']);
456
614
  validatePositionals(positionals(args.slice(1), ['--path']), 0, HELP.knowledge);
@@ -466,16 +624,26 @@ async function run(args) {
466
624
  throw new Error(HELP['deadend verify']);
467
625
  print(await verifyDeadend(target, values[0]));
468
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
+ }
469
634
  else {
470
- const valueOptions = ['--path', '--attempt', '--reason', '-m', '--scope', '--context', '--snapshot', '--evidence'];
635
+ const valueOptions = ['--path', '--attempt', '--reason', '-m', '--scope', '--context', '--snapshot', '--evidence', '--branch-mode', '--paths'];
471
636
  validateOptions(args.slice(1), valueOptions, valueOptions);
472
637
  const values = positionals(args.slice(1), valueOptions);
473
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;
474
641
  const input = {
475
642
  scope: option(args, '--scope'),
476
643
  sourceContextId: option(args, '--context'),
477
644
  sourceSnapshotId: option(args, '--snapshot'),
478
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') } : {}) },
479
647
  };
480
648
  print(await recordDeadend(target, option(args, '--attempt') ?? values[0], option(args, '--reason') ?? option(args, '-m') ?? '', input));
481
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
+ }
@@ -0,0 +1,13 @@
1
+ import { type GitWorktreeState } from '../git/git-adapter.js';
2
+ export declare class GitObserver {
3
+ private readonly projectRoot;
4
+ private previous;
5
+ private pending;
6
+ constructor(projectRoot: string);
7
+ observeNow(): Promise<{
8
+ state: GitWorktreeState;
9
+ changed: boolean;
10
+ }>;
11
+ drain(): Promise<void>;
12
+ private observe;
13
+ }