fluffy-context 0.6.0 → 0.7.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/README.md +99 -8
  2. package/dist/src/agent/index.d.ts +2 -0
  3. package/dist/src/agent/index.js +1 -0
  4. package/dist/src/cli/main.js +69 -5
  5. package/dist/src/cognition/filesystem-observer.d.ts +41 -0
  6. package/dist/src/cognition/filesystem-observer.js +164 -0
  7. package/dist/src/cognition/git-observer.d.ts +1 -0
  8. package/dist/src/cognition/git-observer.js +3 -2
  9. package/dist/src/cognition/journal.d.ts +4 -1
  10. package/dist/src/cognition/journal.js +83 -4
  11. package/dist/src/cognition/migration-v1.js +1 -1
  12. package/dist/src/cognition/orient.js +4 -3
  13. package/dist/src/cognition/policy.d.ts +4 -0
  14. package/dist/src/cognition/policy.js +49 -0
  15. package/dist/src/cognition/projections.js +0 -0
  16. package/dist/src/cognition/runtime.d.ts +20 -1
  17. package/dist/src/cognition/runtime.js +229 -53
  18. package/dist/src/cognition/types.d.ts +16 -0
  19. package/dist/src/cognition/v1-bridge.js +3 -2
  20. package/dist/src/compiler/activity.d.ts +3 -0
  21. package/dist/src/compiler/activity.js +79 -0
  22. package/dist/src/compiler/compile.d.ts +2 -0
  23. package/dist/src/compiler/compile.js +330 -0
  24. package/dist/src/compiler/sources.d.ts +19 -0
  25. package/dist/src/compiler/sources.js +130 -0
  26. package/dist/src/compiler/types.d.ts +187 -0
  27. package/dist/src/compiler/types.js +1 -0
  28. package/dist/src/hooks/claude-code.js +7 -15
  29. package/dist/src/mcp/main.js +2 -0
  30. package/dist/src/mcp/server.js +2 -0
  31. package/dist/src/storage/layout.d.ts +1 -0
  32. package/dist/src/storage/layout.js +3 -0
  33. package/dist/src/version.d.ts +1 -1
  34. package/dist/src/version.js +1 -1
  35. package/package.json +1 -1
  36. package/skills/fluffy-context/SKILL.md +2 -1
package/README.md CHANGED
@@ -1,10 +1,22 @@
1
- # Context Runtime
1
+ # fluffy-context
2
2
 
3
- `fluffy-context` 正在从面向 AI 编程会话的本地 Context Runtime CLI,演进为 **Agent 无关的本地 Knowledge Runtime**。它负责自动捕获开发过程中产生的事件、候选共识、问题、不可达路径和分支工作状态,并向任意 Agent 注入有界、可验证的业务认知。
3
+ `fluffy-context` 是一个本地优先、Agent 无关的 Context 与 Knowledge Runtime。它帮助 AI 编程 Agent 在新会话中恢复任务状态、检索已验证共识、避开已验证死路,并将结果编译成有界、可追溯的上下文。
4
4
 
5
- Git 仍然负责代码和真实文件变更,Issue 系统仍然负责任务,项目文档仍然负责设计;`fluffy-context` 补齐的是持续演进的项目认知层。最终架构是 `project → knowledge runtime → LLM`:CLI、MCP、Claude Hook 和未来 IDE/Agent 集成只是事件入口与信息注入适配器,而不是认知本体。
5
+ ```text
6
+ 项目代码与 Git → Context Runtime → Context Compiler → Agent / MCP / Hook
7
+ ```
8
+
9
+ 它不替代 Git、Issue 系统、项目文档、`CLAUDE.md`、`AGENTS.md` 或宿主 Skills:Git 仍是代码和文件变更的权威,文档仍负责设计,`fluffy-context` 只维护任务认知及其来源。
10
+
11
+ ## 你可以用它做什么
12
+
13
+ - 保存和恢复进行中的任务、进度、待办、决策与风险。
14
+ - 让 Agent 只读取已验证的 Knowledge 和 Deadend,减少重复试错。
15
+ - 使用 `ctx compile` 生成带 provenance、纳入/排除原因和字符预算的 provider-neutral Manifest。
16
+ - 通过 MCP 或 Claude Code Hook 在会话开始时注入有界上下文。
17
+ - 可选记录经过 `.contextignored` 过滤的文件变化 metadata;默认关闭,不记录文件内容或 diff。
6
18
 
7
- 0.6.0 在 branch-aware 注入闭环上补齐了可治理的确定性检索:迁移并启用 Runtime 后,Orient 从 v2 journal 的可重建索引检索已验证、适用的 Knowledge 与 Deadend,并返回有界的选择、排除与截断说明。v1 的 checkpoint、Note、Knowledge 与 Deadend 写入仍会最佳努力镜像为 v2 事件;Runtime 也会协调检测到的外部 v1 变更。自动产物默认仍仅进入 Candidate,Verified Knowledge 仍需治理确认。
19
+ 0.6.0 在 branch-aware 注入闭环上补齐了可治理的确定性检索:迁移并启用 Runtime 后,Orient 从 v2 journal 的可重建索引检索已验证、适用的 Knowledge 与 Deadend,并返回有界的选择、排除与截断说明。v1 的 checkpoint、Note、Knowledge 与 Deadend 写入仍会最佳努力镜像为 v2 事件;Runtime 也会协调检测到的外部 v1 变更。自动产物默认仍仅进入 Candidate,Verified Knowledge 仍需治理确认。0.6.6 已在此基础上完成无感 Runtime 生命周期的首个闭环:已迁移项目默认自动启动,Claude Hook、MCP 与 CLI Orient 可幂等唤醒 Runtime,显式 disable 仍保持永久禁用,启动失败继续 fail-open。
8
20
 
9
21
  ## 版本演进
10
22
 
@@ -16,8 +28,54 @@ Git 仍然负责代码和真实文件变更,Issue 系统仍然负责任务,
16
28
  | 0.4.0 | v2 事件 journal、v1 显式迁移、可重建投影、本地 Runtime、Git 观察 | v2 与 v1 并存;NDJSON hash chain 是 v2 权威;Runtime 仅监听 loopback 并记录分支、ref 与 HEAD 元数据。 |
17
29
  | 0.5.0 | branch-aware Orient、v1 写入镜像、Runtime 协调、宿主事件捕获 | Context 按当前 worktree/ref 选择,避免跨分支任务状态泄漏;稳定宿主 ID 才会写入限长、脱敏后的 session/prompt intent。 |
18
30
  | 0.6.0 | journal 派生的确定性检索、治理终态、可解释 Orient、显式使用反馈 | 启用 v2 后直接从可重建索引检索已验证且适用的共识;使用反馈只影响稳定排序,绝不自动验证或提升 Candidate。 |
31
+ | 0.6.5 | Runtime 活跃度修复与 Hook 保活 | Git 真实状态变化、宿主 Hook 活动和认证 IPC 会刷新 Runtime;heartbeat 与无变化轮询不会伪装成活跃,Runtime 在不可用时保持 fail-open。 |
32
+ | 0.6.6 | 无感 Runtime 生命周期 | 通过 `ensureRuntimeForSession()` 在已迁移项目的首次会话、Claude Hook、MCP 握手或 CLI Orient 时幂等拉起 Runtime;显式 disable 保持禁用,启动失败继续 fail-open。 |
33
+ | 0.7.0 | Context Compiler 与文件活动基础能力 | 提供 `compileContext()`、`ctx compile`、provider-neutral `ContextManifest`、确定性候选排序、来源 provenance、纳入/排除说明、字符预算、v1/v2 来源和默认关闭的文件活动观察器。 |
34
+ | 0.7.1 | Correctness hardening 与显式文件监听开关 | 强化 policy 校验、journal schema/hash/幂等性验证、Runtime 清理、候选 ID 稳定性,并提供 `ctx filesystem enable|disable|status`;文件观察严格复用 `.contextignored` 过滤规则。 |
35
+ | 0.7.2 | 文件活动进入 Context Compiler | 将当前 branch/worktree/ref 的 `workspace.paths.changed` 活动作为有界 `activity` section 编译;支持 `--activity-limit`,再次执行 `.contextignored` 和内置敏感路径过滤,不记录内容或 diff。 |
36
+
37
+ 所有版本均保持本地优先、显式治理与 fail-open 集成原则。v1 数据不会在迁移过程中被重写;当 v2 未启用、不可用、损坏或尚未同步时,支持回退的读取接口会回退到兼容的 v1 行为。0.7.x 的 `ctx compile`、journal verify 和 Runtime 文件观察均保留明确的来源、验证或过滤边界。
38
+
39
+ ## 产品愿景与 1.0 前路线图
40
+
41
+ `fluffy-context` 的目标不是成为另一套规则、Memory 或 Agent 能力系统,而是成为本地优先、Agent 无关的 **Context Orchestrator(上下文编排层)**:在理解当前任务、项目、worktree/ref、工作状态与 Token Budget 后,选择并编译最适合当前 Agent 的有界上下文。
42
+
43
+ ```text
44
+ project → knowledge runtime → context compiler → LLM
45
+ ```
46
+
47
+ Git 继续是代码与文件变更的权威,Issue 系统继续负责工作项,项目文档继续承载设计;`AGENTS.md`、`CLAUDE.md`、`.cursor/rules`、宿主 Memory、Skills 与 MCP 工具也继续由各自宿主和原生格式维护。`fluffy-context` 不复制、改写或同步这些外部主体;它只维护任务感知的选择所需元数据、引用与按输出需要截取的有界片段,并给出来源、优先级、纳入/排除原因与 Token 分配。
48
+
49
+ CLI 是 Runtime 面向用户的 console pane / frontend:它用于初始化、检视、治理、诊断与调试编译结果,而非产品认知本体。Claude、Cursor、MCP 和未来 IDE/Agent 集成同样只是可选适配器,不应成为核心 Runtime 的依赖。
50
+
51
+ | 计划版本 | 核心目标 | 主要能力 | 明确边界 |
52
+ | --- | --- | --- | --- |
53
+ | 0.7.0 | Context Compiler 基础 | 提供版本化 `ContextSourceDescriptor`、`ContextIntent`、`ContextManifest`,将当前 Context、checkpoint、Note、v2 journal 派生来源和已验证 Knowledge/Deadend 统一编译为可追溯输入;按字符预算确定性选择、排序、截断并生成纳入/排除理由。 | 不自动把原始事件提炼为 Verified Knowledge;不重写、合并或同步外部规则;不做模型调用、向量/Embedding、远程注册表或 provider-specific Renderer。 |
54
+ | 0.8.0 | Decision Graph | 将 Decision 升级为可治理、可追溯的一等 journal 对象,记录结果、理由、备选方案、证据、适用性与生命周期;通过 `depends-on`、`supersedes`、`contradicts`、`supported-by`、`rejected-alternative`、`implemented-by` 等边构成确定性投影图;Compiler 只注入当前任务所需的有界决策子图。 | 不自动从代码或模型输出推断决策;不自动解决冲突;不自动提升 Candidate。 |
55
+ | 0.9.0 | Knowledge Evolution | 以受治理事件支持 Knowledge 的合并、拆分、替代、适用范围收窄/扩展、退役、归档,以及作为新记录的复活;根据 Git/path 事件、source anchor、Decision Graph 依赖、重复/冲突和适用性漂移生成确定性的 `review-needed` 信号;Compiler 优先选择新鲜、适用且已验证的认知。 | 自动机制只能提出待审查候选,不能自动验证、降级、归档或作出真实性结论;不引入远程同步、团队权限、强制模型提供方或语义向量依赖。 |
56
+
57
+ ### 0.6.6 Runtime 生命周期
58
+
59
+ 0.6.6 的目标是消除用户反复执行 `ctx runtime start` 的负担。Runtime 已成为本地后台基础设施,而不是用户或 Agent 必须理解和操作的功能。核心 Runtime 保持 Agent 无关;Claude Code、MCP、CLI、IDE Extension 和其他宿主都只能通过可选 adapter 发出统一的活动信号。
60
+
61
+ 已迁移项目默认启用自动启动。首次会话、Claude Hook、MCP 握手或 CLI Orient 到达时,会通过幂等的 `ensureRuntimeForSession()` 检查 v2 policy 与有效 lease,复用启动锁避免重复进程,并在严格短超时内尽力拉起 Runtime。启动失败、宿主没有对应 hook 或 Runtime 暂时不可用时,宿主工作流仍继续,保持 fail-open;后续用户或 Agent 活动可再次唤醒,而不要求手动重复启动。显式执行 `ctx runtime disable` 会持久化禁用状态,直到再次执行 `ctx runtime enable`。
62
+
63
+ 当前 Runtime 使用 `active` 与 inactivity stop 的轻量模型:认证 IPC、Hook、MCP、CLI 和真实 Git 状态变化会刷新活跃度;heartbeat 与无变化轮询只维护健康状态,不会伪装成用户活动。更完整的 `limited`、`idle`、`sleep`、`destroyed` 生命周期状态机,以及统一的 session、tool、file 和 explicit-end 信号,属于后续版本设计。
64
+
65
+ 生命周期适配器不绑定某个 Agent 的内部实现:适配器只负责归一化宿主事件,Runtime 核心不把 inactivity 超时推断当作可靠的 agent-end 事实。当前可可靠捕获的 Claude 事件仍受宿主提供的 Hook 和稳定标识限制。
66
+
67
+ 低开销、隐私优先的 Git/file activity summary 仍属于后续版本范围:只记录经过过滤的相对路径、计数、状态变化和摘要 metadata,不记录原始命令、stdout、diff 或文件内容;事件必须 debounce/coalesce,并作为后续 Context Compiler 的候选证据,而不是自动验证的 Knowledge。
19
68
 
20
- 所有版本均保持本地优先、显式治理与 fail-open 集成原则。v1 数据不会在迁移过程中被重写;当 v2 未启用、不可用、损坏或尚未同步时,读取接口会回退到兼容的 v1 行为。
69
+ ### 1.0 发布门槛
70
+
71
+ 1. **稳定契约**:Context Source Descriptor、Context Manifest/Compiler、Decision Graph、Knowledge Evolution、只读 MCP 与 Agent API 具有明确的版本化兼容契约。
72
+ 2. **正确性与恢复**:编译与 journal replay 可确定性复现;hash chain 验证、投影 rebuild/repair、迁移一致性、损坏回退与 Windows/macOS/Linux 支持完善。
73
+ 3. **治理与隐私**:外部来源始终保持原生权威;持久化和输出前均应用敏感信息过滤;每个注入的项目认知都可追溯、适用且具有受治理生命周期。
74
+ 4. **集成中立**:Claude、Cursor 与未来宿主均为可选适配器;核心不依赖特定模型、IDE、SaaS、网络或向量数据库。
75
+ 5. **运行质量**:延迟与存储有界,Runtime 健康状态可观测,选择与截断可解释,适配器不可用时保持 fail-open。
76
+ 6. **产品表达一致**:文档与 API 明确 `ctx` 是 Runtime 的前端;`fluffy-context` 是对上下文进行编排的 Runtime,而不是仓库规则、Memory、Skills 或 MCP 能力的替代品。
77
+
78
+ 上述路线图描述的是 `0.6.6` 及之后的演进方向。0.6.5 提供 Runtime 活跃度修复与 Hook 保活;0.6.6 已提供已迁移项目的无感自动启动与 fail-open 唤醒。更完整的生命周期状态机、Git/file activity summary 和被动学习仍属于后续版本范围。
21
79
 
22
80
  ## 适用场景
23
81
 
@@ -79,6 +137,39 @@ ctx orient
79
137
  ctx orient --context <context-id> --note-limit 10
80
138
  ```
81
139
 
140
+ ### 0.7.x Context Compiler
141
+
142
+ `ctx orient` 面向快速恢复,`ctx compile` 面向需要结构化输入的 Agent 或适配器。Compiler 只读当前 Context、Snapshot、open Note、已验证 Knowledge/Deadend 及可用的 v2 来源,返回 JSON Manifest:
143
+
144
+ ```bash
145
+ ctx compile "修复支付回调" --max-chars 4000
146
+ ctx compile "验证签名逻辑" --scenario verification --paths src/auth.ts
147
+ ctx compile "查看最近修改" --activity-limit 10 --max-chars 4000
148
+ ```
149
+
150
+ Manifest 包含 `intent`、`selected`、`sections`、`excluded`、`provenance`、`budget` 和最终 `text`。`maxChars` 使用 JavaScript UTF-16 字符串长度;Compiler 不调用模型、不写入 Context 或 journal,重复输入会得到确定性结果。普通查询默认只纳入已验证 Knowledge 和 Deadend,Candidate、deprecated、rejected、obsolete 等记录会明确排除。
151
+
152
+ 0.7.2 新增的 `activity` section 来自已记录的 `workspace.paths.changed` 事件,只包含近期当前 branch/worktree/ref 的相对路径和有限 metadata。它表示“观察到文件发生变化”,不表示文件已被读取、理解或测试;不包含内容、diff、命令或输出。`--activity-limit 0` 可在单次编译中关闭 activity 候选,且不会修改 Runtime 的文件监听策略。v1 fallback 没有等价的文件活动来源,因此返回空的 `activity` section。
153
+
154
+ ### 文件变化观察
155
+
156
+ 文件监听是可选能力,默认关闭。要显式开启:
157
+
158
+ ```bash
159
+ ctx filesystem enable
160
+ ctx filesystem status
161
+ ```
162
+
163
+ 关闭监听:
164
+
165
+ ```bash
166
+ ctx filesystem disable
167
+ ```
168
+
169
+ 如果 Runtime 已在运行,enable/disable 会自动按新策略重启 Runtime;如果 Runtime 尚未运行,只更新 policy。观察器只记录项目相对路径、文件类型、大小和修改时间等有限 metadata,不记录文件内容、diff、命令或输出。
170
+
171
+ 文件路径会经过与 Context 关联文件相同的 `.contextignored` 规则过滤。被 `.contextignored` 排除的文件,即使发生创建、修改或删除,也不会写入 `workspace.paths.changed` 事件;即使历史事件中存在这类路径,Compiler 也会再次过滤。内置保护规则仍然优先排除 `.context/`、`.git/`、依赖/构建目录、环境变量、密钥和越界路径。
172
+
82
173
  ## Agent 工作流
83
174
 
84
175
  推荐阶段:
@@ -132,7 +223,7 @@ ctx migrate v1 --apply
132
223
  ctx migrate verify
133
224
  ```
134
225
 
135
- 只有完成迁移后才可启用 Runtime:
226
+ 迁移成功后,Runtime 默认已启用自动启动;以下命令用于显式诊断、手动控制或持久化选择退出:
136
227
 
137
228
  ```bash
138
229
  ctx runtime enable
@@ -144,9 +235,9 @@ ctx runtime disable
144
235
 
145
236
  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
237
 
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 写入镜像。
238
+ 完成迁移且未显式 disable 时,`ctx orient`、MCP `context_orient` 与 Claude Hook 会在严格短超时内幂等确保 Runtime 可用,并优先选择当前 worktree/ref 的 v2 workspace;它不会借用其他分支的 Context。v2 可用时,Knowledge 与 Deadend 由 journal 重放出的确定性倒排索引直接检索,默认仅返回已验证且适用的记录;无匹配 workspace、v2 状态不可用、journal 损坏、v1 尚未同步或 Runtime 已禁用时,接口会安全回退到既有 v1 选择逻辑。Runtime 停止时仍可读取已验证的 v2 状态,但运行中的 Runtime 负责持续 Git 观察和 v1 写入镜像。
148
239
 
149
- Claude Hook 仅在宿主提供稳定的 session 与 event 标识时记录 `host.session.started` 或 `host.prompt.submitted`。提示只保存按 `promptIntentMaxChars` 截断并经过敏感值脱敏后的 intent,绝不保存原始提示;缺少稳定标识时不写入事件。默认 Hook 不启动守护进程;将 v2 policy 的 `allowHookStartup` 设为 `true` 后,SessionStart 才会在严格超时内尽力启动 Runtime,失败仍会 fail open。UserPromptSubmit 不会启动 Runtime。
240
+ Claude Hook 仅在宿主提供稳定的 session 与 event 标识时记录 `host.session.started` 或 `host.prompt.submitted`。提示只保存按 `promptIntentMaxChars` 截断并经过敏感值脱敏后的 intent,绝不保存原始提示;缺少稳定标识时不写入事件。SessionStart 与 UserPromptSubmit 都会在严格短超时内尽力确保已迁移、未显式 disable 的项目 Runtime 可用;稳定标识仅影响宿主事件是否写入。启动或读取失败始终 fail open,不阻塞宿主工作流。
150
241
 
151
242
  ### `ctx checkpoint` 和 `ctx resume`
152
243
 
@@ -1,3 +1,5 @@
1
1
  export * from './api.js';
2
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';
4
+ export { compileContext } from '../compiler/compile.js';
5
+ export type { ContextActivityRecord, ContextAnchor, ContextBudget, ContextBudgetReport, ContextCandidate, ContextCandidateSection, ContextCompileOptions, ContextExclusion, ContextExclusionReason, ContextIntent, ContextIntentSignal, ContextManifest, ContextProvenance, ContextScenario, ContextSourceDescriptor, ContextTruncation, } from '../compiler/types.js';
@@ -1 +1,2 @@
1
1
  export * from './api.js';
2
+ export { compileContext } from '../compiler/compile.js';
@@ -1,11 +1,13 @@
1
1
  #!/usr/bin/env node
2
2
  import { checkpoint, resume } from '../runtime/runtime.js';
3
3
  import { contextOrient } from '../agent/api.js';
4
+ import { compileContext } from '../compiler/compile.js';
4
5
  import { importV1, verifyV1Import } from '../cognition/migration-v1.js';
5
6
  import { recordUse } from '../cognition/feedback.js';
6
- import { readJournal } from '../cognition/journal.js';
7
+ import { verifyJournal } from '../cognition/journal.js';
7
8
  import { projectEvents } from '../cognition/projections.js';
8
- import { disableRuntime, enableRuntime, runtimeStatus, serveRuntime, startRuntime, stopRuntime } from '../cognition/runtime.js';
9
+ import { disableRuntime, enableRuntime, ensureRuntimeForSession, runtimeStatus, serveRuntime, setFilesystemEvents, startRuntime, stopRuntime } from '../cognition/runtime.js';
10
+ import { loadRuntimePolicy } from '../cognition/policy.js';
9
11
  import { migrationReadiness } from '../cognition/readiness.js';
10
12
  import { runClaudeCodeHook } from '../hooks/claude-code.js';
11
13
  import { readGitWorktreeState } from '../git/git-adapter.js';
@@ -115,6 +117,21 @@ Options:
115
117
  --knowledge-limit <number> Maximum knowledge matches (default: 10)
116
118
  --deadend-limit <number> Maximum deadend matches (default: 10)
117
119
  --note-limit <number> Maximum open notes (default: 20)`,
120
+ compile: `usage: ctx compile [query] [options]
121
+
122
+ Compile bounded, provider-neutral context with source provenance and selection explanations.
123
+
124
+ Options:
125
+ --path <path> Project path
126
+ --context <id> Context ID
127
+ --scenario <scenario> resume|implementation|debugging|verification|handoff|exploration|unknown
128
+ --scope <scope> Exact consensus scope
129
+ --paths <paths> Comma-separated project-relative paths
130
+ --max-chars <number> Maximum compiled characters
131
+ --knowledge-limit <number> Maximum knowledge matches (default: 10)
132
+ --deadend-limit <number> Maximum deadend matches (default: 10)
133
+ --note-limit <number> Maximum open notes (default: 20)
134
+ --activity-limit <number> Maximum recent activity groups (default: 20)`,
118
135
  agent: `usage: ctx agent serve
119
136
 
120
137
  Start an MCP stdio server for Agent integrations.
@@ -289,6 +306,18 @@ Options:
289
306
  runtime: `usage: ctx runtime enable|disable|start|stop|status [--path <path>]
290
307
 
291
308
  Control the local event-driven Knowledge Runtime after v1 migration verification.`,
309
+ filesystem: `usage: ctx filesystem enable|disable|status [--path <path>]
310
+
311
+ Control whether the Runtime records filtered file metadata changes.`,
312
+ 'filesystem enable': `usage: ctx filesystem enable [--path <path>]
313
+
314
+ Enable filtered filesystem change observation and restart a running Runtime.`,
315
+ 'filesystem disable': `usage: ctx filesystem disable [--path <path>]
316
+
317
+ Disable filesystem change observation and restart a running Runtime.`,
318
+ 'filesystem status': `usage: ctx filesystem status [--path <path>]
319
+
320
+ Show the filesystem observation policy and Runtime status.`,
292
321
  'runtime status': `usage: ctx runtime status [--path <path>]
293
322
 
294
323
  Show the read-only runtime policy, migration and authenticated lease status.
@@ -312,7 +341,7 @@ Stop the local runtime through authenticated loopback IPC.`,
312
341
  Internal runtime daemon entry point.`,
313
342
  };
314
343
  function usage() {
315
- return `usage: ctx init|checkpoint|resume|orient|agent|hook|integrate|status|doctor|learn|knowledge|deadend|deadends|note|activity|migrate|runtime|feedback|journal [options]
344
+ return `usage: ctx init|checkpoint|resume|orient|compile|agent|hook|integrate|status|doctor|learn|knowledge|deadend|deadends|note|activity|migrate|runtime|filesystem|feedback|journal [options]
316
345
 
317
346
  Run \"ctx <command> --help\" for command details.`;
318
347
  }
@@ -340,7 +369,8 @@ function printHelp(args) {
340
369
  || (command === 'journal' && args[1] === 'verify')
341
370
  || (command === 'note' && ['add', 'list'].includes(args[1]))
342
371
  || (command === 'migrate' && ['v1', 'verify'].includes(args[1]))
343
- || (command === 'runtime' && ['enable', 'disable', 'start', 'stop', 'status', 'serve'].includes(args[1]));
372
+ || (command === 'runtime' && ['enable', 'disable', 'start', 'stop', 'status', 'serve'].includes(args[1]))
373
+ || (command === 'filesystem' && ['enable', 'disable', 'status'].includes(args[1]));
344
374
  const key = nested ? `${command} ${args[1]}` : command;
345
375
  const nestedKey = key === 'integrate claude' && (args[2] === 'inspect' || args[2] === 'install') ? `${key} ${args[2]}` : key;
346
376
  const help = HELP[nestedKey];
@@ -405,6 +435,7 @@ async function run(args) {
405
435
  validateOptions(args.slice(1), valueOptions, valueOptions);
406
436
  const query = positionals(args.slice(1), valueOptions);
407
437
  validatePositionals(query, 1, HELP.orient);
438
+ await ensureRuntimeForSession(target, { source: 'cli', timeoutMs: 1_000 }).catch(() => undefined);
408
439
  print(await contextOrient(target, {
409
440
  contextId: option(args, '--context'),
410
441
  query: query[0],
@@ -416,6 +447,25 @@ async function run(args) {
416
447
  }));
417
448
  return;
418
449
  }
450
+ case 'compile': {
451
+ const valueOptions = ['--path', '--context', '--scenario', '--scope', '--paths', '--max-chars', '--knowledge-limit', '--deadend-limit', '--note-limit', '--activity-limit'];
452
+ validateOptions(args.slice(1), valueOptions, valueOptions);
453
+ const query = positionals(args.slice(1), valueOptions);
454
+ validatePositionals(query, 1, HELP.compile);
455
+ print(await compileContext(target, {
456
+ contextId: option(args, '--context'),
457
+ query: query[0],
458
+ scenario: enumOption(args, '--scenario', ['resume', 'implementation', 'debugging', 'verification', 'handoff', 'exploration', 'unknown']),
459
+ scope: option(args, '--scope'),
460
+ paths: listOption(args, '--paths'),
461
+ maxChars: numericOption(args, '--max-chars', 4000),
462
+ knowledgeLimit: numericOption(args, '--knowledge-limit', 10),
463
+ deadendLimit: numericOption(args, '--deadend-limit', 10),
464
+ noteLimit: numericOption(args, '--note-limit', 20),
465
+ activityLimit: numericOption(args, '--activity-limit', 20),
466
+ }));
467
+ return;
468
+ }
419
469
  case 'agent':
420
470
  if (args[1] !== 'serve')
421
471
  throw new Error(HELP.agent);
@@ -511,13 +561,27 @@ async function run(args) {
511
561
  print(await recordUse(target, { entity: args[2], entityId: values[0], eventId: option(args, '--event-id') ?? '' }));
512
562
  return;
513
563
  }
564
+ case 'filesystem': {
565
+ if (!['enable', 'disable', 'status'].includes(args[1]))
566
+ throw new Error(HELP.filesystem);
567
+ validateOptions(args.slice(2), ['--path'], ['--path']);
568
+ validatePositionals(positionals(args.slice(2), ['--path']), 0, HELP[`filesystem ${args[1]}`]);
569
+ if (args[1] === 'status') {
570
+ const status = await runtimeStatus(target);
571
+ print({ projectRoot: status.projectRoot, migrated: status.migrated, enabled: status.enabled, running: status.running, filesystemEventsEnabled: status.migrated ? (await loadRuntimePolicy(status.projectRoot)).filesystemEventsEnabled : false });
572
+ }
573
+ else {
574
+ print(await setFilesystemEvents(target, args[1] === 'enable'));
575
+ }
576
+ return;
577
+ }
514
578
  case 'journal': {
515
579
  if (args[1] !== 'verify')
516
580
  throw new Error(HELP.journal);
517
581
  validateOptions(args.slice(2), ['--path'], ['--path']);
518
582
  validatePositionals(positionals(args.slice(2), ['--path']), 0, HELP['journal verify']);
519
583
  const root = target ?? process.cwd();
520
- const events = await readJournal(root);
584
+ const events = await verifyJournal(root);
521
585
  const projection = projectEvents(events);
522
586
  const last = events.at(-1);
523
587
  print({ verified: true, eventCount: events.length, cursor: { eventId: last?.eventId ?? null, hash: last?.hash ?? null }, counts: { contexts: Object.keys(projection.contexts).length, notes: Object.keys(projection.notes).length, knowledge: Object.keys(projection.knowledge).length, deadends: Object.keys(projection.deadends).length } });
@@ -0,0 +1,41 @@
1
+ import type { CognitionGitAnchor } from './types.js';
2
+ export type WatchCallback = (eventType: string, filename: string | Buffer | null) => void;
3
+ export type WatchFactory = (projectRoot: string, callback: WatchCallback) => {
4
+ close(): void;
5
+ };
6
+ export interface FilesystemObserverOptions {
7
+ anchor?: () => CognitionGitAnchor;
8
+ debounceMs?: number;
9
+ maxPaths?: number;
10
+ maxPayloadBytes?: number;
11
+ watchFactory?: WatchFactory;
12
+ onChanged?: (result: FilesystemObservationResult) => void;
13
+ }
14
+ export interface FilesystemObservationResult {
15
+ changed: boolean;
16
+ paths: string[];
17
+ }
18
+ export declare class FilesystemObserver {
19
+ private readonly projectRoot;
20
+ private readonly debounceMs;
21
+ private readonly maxPaths;
22
+ private readonly maxPayloadBytes;
23
+ private readonly anchor;
24
+ private readonly watchFactory;
25
+ private readonly onChanged;
26
+ private readonly pendingPaths;
27
+ private readonly known;
28
+ private watcher;
29
+ private timer;
30
+ private pending;
31
+ private closed;
32
+ constructor(projectRoot: string, options?: FilesystemObserverOptions);
33
+ start(): void;
34
+ stop(): void;
35
+ observeNow(): Promise<FilesystemObservationResult>;
36
+ flush(): Promise<FilesystemObservationResult>;
37
+ drain(): Promise<void>;
38
+ close(): void;
39
+ private scheduleFlush;
40
+ private process;
41
+ }
@@ -0,0 +1,164 @@
1
+ import { watch } from 'node:fs';
2
+ import { stat } from 'node:fs/promises';
3
+ import { resolve } from 'node:path';
4
+ import { filterPaths } from '../capture/context-filter.js';
5
+ import { appendEvent } from './journal.js';
6
+ const defaultDebounceMs = 250;
7
+ const defaultMaxPaths = 100;
8
+ const defaultMaxPayloadBytes = 16 * 1024;
9
+ function defaultWatchFactory(projectRoot, callback) {
10
+ return watch(projectRoot, { recursive: true }, callback);
11
+ }
12
+ function normalizeCandidate(projectRoot, filename) {
13
+ if (filename === null)
14
+ return null;
15
+ const value = filename.toString().replaceAll('\\', '/').replace(/^\.\//, '');
16
+ if (!value || value.startsWith('/') || /^[A-Za-z]:\//.test(value))
17
+ return null;
18
+ const absolute = resolve(projectRoot, value);
19
+ const relative = absolute.slice(resolve(projectRoot).length).replaceAll('\\', '/').replace(/^\/+/, '');
20
+ return relative || null;
21
+ }
22
+ function metadataEqual(left, right) {
23
+ return left?.sizeBytes === right.sizeBytes && left.mtimeMs === right.mtimeMs;
24
+ }
25
+ function payloadSize(payload) {
26
+ return Buffer.byteLength(JSON.stringify(payload), 'utf8');
27
+ }
28
+ export class FilesystemObserver {
29
+ projectRoot;
30
+ debounceMs;
31
+ maxPaths;
32
+ maxPayloadBytes;
33
+ anchor;
34
+ watchFactory;
35
+ onChanged;
36
+ pendingPaths = new Map();
37
+ known = new Map();
38
+ watcher = null;
39
+ timer = null;
40
+ pending = Promise.resolve({ changed: false, paths: [] });
41
+ closed = false;
42
+ constructor(projectRoot, options = {}) {
43
+ this.projectRoot = projectRoot;
44
+ this.debounceMs = options.debounceMs ?? defaultDebounceMs;
45
+ this.maxPaths = options.maxPaths ?? defaultMaxPaths;
46
+ this.maxPayloadBytes = options.maxPayloadBytes ?? defaultMaxPayloadBytes;
47
+ this.anchor = options.anchor ?? (() => ({ worktreeId: null, branch: null, ref: null, head: null }));
48
+ this.watchFactory = options.watchFactory ?? defaultWatchFactory;
49
+ this.onChanged = options.onChanged;
50
+ }
51
+ start() {
52
+ if (this.closed || this.watcher)
53
+ return;
54
+ this.watcher = this.watchFactory(this.projectRoot, (eventType, filename) => {
55
+ const relative = normalizeCandidate(this.projectRoot, filename);
56
+ if (relative)
57
+ this.pendingPaths.set(relative, eventType);
58
+ this.scheduleFlush();
59
+ });
60
+ }
61
+ stop() {
62
+ if (this.timer)
63
+ clearTimeout(this.timer);
64
+ this.timer = null;
65
+ this.watcher?.close();
66
+ this.watcher = null;
67
+ this.closed = true;
68
+ }
69
+ observeNow() {
70
+ return this.flush();
71
+ }
72
+ async flush() {
73
+ if (this.timer)
74
+ clearTimeout(this.timer);
75
+ this.timer = null;
76
+ const paths = [...this.pendingPaths.entries()];
77
+ this.pendingPaths.clear();
78
+ if (paths.length === 0)
79
+ return { changed: false, paths: [] };
80
+ const operation = this.pending.then(() => this.process(paths), () => this.process(paths));
81
+ this.pending = operation;
82
+ return operation;
83
+ }
84
+ async drain() {
85
+ await this.pending;
86
+ if (this.pendingPaths.size > 0)
87
+ await this.flush();
88
+ await this.pending;
89
+ }
90
+ close() {
91
+ this.stop();
92
+ }
93
+ scheduleFlush(delay = this.debounceMs) {
94
+ if (this.timer || this.closed)
95
+ return;
96
+ this.timer = setTimeout(() => {
97
+ this.timer = null;
98
+ void this.flush().catch(() => undefined);
99
+ }, delay);
100
+ }
101
+ async process(paths) {
102
+ const filtered = await filterPaths(this.projectRoot, paths.map(([relative]) => relative));
103
+ const eventTypes = new Map(paths);
104
+ const changes = [];
105
+ for (const relative of filtered.slice(0, this.maxPaths)) {
106
+ const absolute = resolve(this.projectRoot, relative);
107
+ let current = null;
108
+ try {
109
+ const value = await stat(absolute);
110
+ if (!value.isFile())
111
+ continue;
112
+ current = { sizeBytes: value.size, mtimeMs: value.mtimeMs };
113
+ }
114
+ catch {
115
+ current = null;
116
+ }
117
+ const previous = this.known.get(relative);
118
+ if (current && metadataEqual(previous, current))
119
+ continue;
120
+ if (!current && !previous)
121
+ continue;
122
+ if (current)
123
+ this.known.set(relative, current);
124
+ else
125
+ this.known.delete(relative);
126
+ changes.push({
127
+ path: relative,
128
+ kind: current ? (previous ? 'modified' : eventTypes.get(relative) === 'rename' ? 'created' : 'modified') : 'deleted',
129
+ fileType: 'file',
130
+ ...(current ? { sizeBytes: current.sizeBytes, mtimeMs: current.mtimeMs } : {}),
131
+ });
132
+ }
133
+ if (changes.length === 0)
134
+ return { changed: false, paths: [] };
135
+ const limitedChanges = changes.slice(0, this.maxPaths);
136
+ const basePayload = {
137
+ schemaVersion: 1,
138
+ paths: limitedChanges.map((change) => change.path),
139
+ changes: limitedChanges,
140
+ pathCount: limitedChanges.length,
141
+ coalesced: paths.length > 1,
142
+ truncated: filtered.length > limitedChanges.length || payloadSize({ changes: limitedChanges }) > this.maxPayloadBytes,
143
+ };
144
+ while (basePayload.changes.length > 1 && payloadSize(basePayload) > this.maxPayloadBytes) {
145
+ basePayload.changes.pop();
146
+ basePayload.paths.pop();
147
+ basePayload.pathCount = basePayload.changes.length;
148
+ basePayload.truncated = true;
149
+ }
150
+ if (basePayload.changes.length === 0 || payloadSize(basePayload) > this.maxPayloadBytes)
151
+ return { changed: false, paths: [] };
152
+ const result = await appendEvent(this.projectRoot, {
153
+ type: 'workspace.paths.changed',
154
+ source: { kind: 'fs-observer' },
155
+ idempotencyKey: `fs:paths:${basePayload.paths.join(',')}:${basePayload.changes.map((change) => `${change.kind}:${change.sizeBytes ?? ''}:${change.mtimeMs ?? ''}`).join(',')}`,
156
+ git: this.anchor(),
157
+ payload: basePayload,
158
+ });
159
+ const observation = { changed: result.appended, paths: basePayload.paths };
160
+ if (observation.changed)
161
+ this.onChanged?.(observation);
162
+ return observation;
163
+ }
164
+ }
@@ -4,6 +4,7 @@ export declare class GitObserver {
4
4
  private previous;
5
5
  private pending;
6
6
  constructor(projectRoot: string);
7
+ get current(): GitWorktreeState | null;
7
8
  observeNow(): Promise<{
8
9
  state: GitWorktreeState;
9
10
  changed: boolean;
@@ -1,6 +1,5 @@
1
1
  import { readGitWorktreeState } from '../git/git-adapter.js';
2
2
  import { appendEvent } from './journal.js';
3
- import { rebuildProjections } from './projections.js';
4
3
  function anchor(state) {
5
4
  return { worktreeId: state.worktreeId, branch: state.branch, ref: state.ref, head: state.commit };
6
5
  }
@@ -14,6 +13,9 @@ export class GitObserver {
14
13
  constructor(projectRoot) {
15
14
  this.projectRoot = projectRoot;
16
15
  }
16
+ get current() {
17
+ return this.previous;
18
+ }
17
19
  observeNow() {
18
20
  const observation = this.pending.then(() => this.observe());
19
21
  this.pending = observation.then(() => undefined, () => undefined);
@@ -55,7 +57,6 @@ export class GitObserver {
55
57
  });
56
58
  }
57
59
  this.previous = state;
58
- await rebuildProjections(this.projectRoot);
59
60
  return { state, changed: true };
60
61
  }
61
62
  }
@@ -17,7 +17,10 @@ export interface AppendEventInput<T = Record<string, unknown>> {
17
17
  payload: T;
18
18
  }
19
19
  export declare function eventHash<T>(event: Omit<CognitionEvent<T>, 'hash'>): string;
20
- export declare function readJournal(startPath?: string): Promise<CognitionEvent[]>;
20
+ export declare function readJournal(startPath?: string, options?: {
21
+ allowPartialTail?: boolean;
22
+ }): Promise<CognitionEvent[]>;
23
+ export declare function verifyJournal(startPath?: string): Promise<CognitionEvent[]>;
21
24
  export declare function appendEvent<T>(startPath: string | undefined, input: AppendEventInput<T>): Promise<{
22
25
  event: CognitionEvent<T>;
23
26
  appended: boolean;