fluffy-context 0.7.2 → 0.7.7

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.
@@ -93,7 +93,7 @@ export async function loadCompilerSources(startPath, options) {
93
93
  knowledgeLimit: options.knowledgeLimit ?? 10,
94
94
  deadendLimit: options.deadendLimit ?? 10,
95
95
  noteLimit: options.noteLimit ?? 20,
96
- });
96
+ }, null);
97
97
  const projectRoot = orientation.projectRoot;
98
98
  const git = await readGitWorktreeState(projectRoot);
99
99
  let events = [];
@@ -110,21 +110,17 @@ export async function loadCompilerSources(startPath, options) {
110
110
  return { orientation, anchor, events, workspace, fallbackReason };
111
111
  }
112
112
  export function latestHostIntent(events, anchor) {
113
- return [...events].reverse().find((event) => {
114
- if (event.type !== 'host.prompt.submitted')
113
+ const event = [...events].reverse().find((candidate) => {
114
+ if (candidate.type !== 'host.prompt.submitted')
115
115
  return false;
116
- if (event.git.worktreeId !== null && event.git.worktreeId !== anchor.worktreeId)
116
+ if (candidate.git.worktreeId !== null && candidate.git.worktreeId !== anchor.worktreeId)
117
117
  return false;
118
- if (event.git.ref !== null && event.git.ref !== anchor.ref)
118
+ if (candidate.git.ref !== null && candidate.git.ref !== anchor.ref)
119
119
  return false;
120
- const payload = event.payload;
120
+ const payload = candidate.payload;
121
121
  return typeof payload.intent === 'string' && payload.intent.trim().length > 0;
122
- })?.payload
123
- ? (() => {
124
- const event = [...events].reverse().find((item) => item.type === 'host.prompt.submitted' && item.git.worktreeId === anchor.worktreeId && item.git.ref === anchor.ref && typeof item.payload.intent === 'string');
125
- if (!event)
126
- return null;
127
- return { value: event.payload.intent, eventId: event.eventId };
128
- })()
129
- : null;
122
+ });
123
+ if (!event)
124
+ return null;
125
+ return { value: event.payload.intent, eventId: event.eventId };
130
126
  }
@@ -2,6 +2,8 @@ import type { CognitionGitAnchor, WorkspacePathChange } from '../cognition/types
2
2
  import type { ContextOrientResult } from '../agent/types.js';
3
3
  import type { Deadend, Knowledge, Note, ResumeSummary, StructuredContext } from '../runtime/types.js';
4
4
  export type ContextScenario = 'resume' | 'implementation' | 'debugging' | 'verification' | 'handoff' | 'exploration' | 'unknown';
5
+ export type ContextLevel = 'signal' | 'summary' | 'structured' | 'evidence';
6
+ export type ContextExpandLevel = 'structured' | 'evidence';
5
7
  export type ContextSourceKind = 'current-context' | 'checkpoint' | 'note' | 'journal' | 'knowledge' | 'deadend';
6
8
  export type ContextCandidateSection = 'summary' | 'pending' | 'decision' | 'risk' | 'note' | 'knowledge' | 'deadend' | 'activity';
7
9
  export type ContextSignalOrigin = 'explicit' | 'deterministic';
@@ -41,6 +43,12 @@ export interface ContextBudget {
41
43
  reservedForRenderer: number;
42
44
  sections?: Partial<Record<ContextCandidateSection, number>>;
43
45
  }
46
+ export interface ContextTokenEstimate {
47
+ input: number;
48
+ output: number;
49
+ unit: 'estimated-tokens';
50
+ method: 'chars-div-four';
51
+ }
44
52
  export interface ContextSourceApplicability {
45
53
  project: true;
46
54
  branchMode: 'global' | 'exact' | 'unsupported';
@@ -100,6 +108,10 @@ export interface ContextCandidate<T = unknown> {
100
108
  selectedChars: number;
101
109
  selected: boolean;
102
110
  truncated: boolean;
111
+ level: ContextLevel;
112
+ itemHash: string;
113
+ tokenEstimate: number;
114
+ expandable: boolean;
103
115
  }
104
116
  export type ContextExclusionReason = 'no_active_context' | 'context_mismatch' | 'status_not_verified' | 'not_open' | 'absorbed' | 'applicability_mismatch' | 'scope_mismatch' | 'path_mismatch' | 'query_no_match' | 'result_limit' | 'budget' | 'unsupported_applicability' | 'v2_unavailable';
105
117
  export interface ContextExclusion {
@@ -138,6 +150,11 @@ export interface ContextManifestProvenance {
138
150
  selectedEventIds: string[];
139
151
  fallbackReason: string | null;
140
152
  }
153
+ export interface ContextManifestIdentity {
154
+ manifestHash: string;
155
+ selectedItemHashes: string[];
156
+ tokenEstimate: ContextTokenEstimate;
157
+ }
141
158
  export interface ContextCompileOptions {
142
159
  contextId?: string;
143
160
  query?: string;
@@ -154,9 +171,37 @@ export interface ContextCompileOptions {
154
171
  sessionId?: string;
155
172
  eventId?: string;
156
173
  }
174
+ export interface ContextExpandInput {
175
+ manifestHash: string;
176
+ candidateId?: string;
177
+ itemHash?: string;
178
+ level: ContextExpandLevel;
179
+ maxChars?: number;
180
+ compile: ContextCompileOptions;
181
+ }
182
+ export interface ContextExpandResult {
183
+ status: 'complete' | 'partial';
184
+ projectRoot: string;
185
+ manifestHash: string;
186
+ candidateId: string;
187
+ itemHash: string;
188
+ previousLevel: ContextLevel;
189
+ requestedLevel: ContextExpandLevel;
190
+ resolvedLevel: ContextExpandLevel;
191
+ source: ContextSourceDescriptor;
192
+ content: unknown | null;
193
+ text: string;
194
+ budget: {
195
+ requestedChars: number;
196
+ usedChars: number;
197
+ remainingChars: number;
198
+ truncated: boolean;
199
+ };
200
+ provenance: ContextManifestProvenance;
201
+ }
157
202
  export interface ContextManifest {
158
203
  schemaVersion: 1;
159
- compilerVersion: '0.7.2';
204
+ compilerVersion: '0.7.7';
160
205
  rankingVersion: 'compiler-ranking-v2';
161
206
  status: 'ready' | 'no_context';
162
207
  projectRoot: string;
@@ -178,6 +223,7 @@ export interface ContextManifest {
178
223
  };
179
224
  selected: ContextCandidate[];
180
225
  excluded: ContextExclusion[];
226
+ identity: ContextManifestIdentity;
181
227
  budget: ContextBudgetReport;
182
228
  truncation: ContextTruncation;
183
229
  provenance: ContextManifestProvenance;
@@ -1,6 +1,7 @@
1
1
  import { McpServer } from '@modelcontextprotocol/server';
2
2
  import { z } from 'zod';
3
- import { contextOrient } from '../agent/api.js';
3
+ import { addNote, listNotes } from '../runtime/notes.js';
4
+ import { contextCompile, contextExpand, contextOrient, contextUsageReport, loadContext, saveContext } from '../agent/api.js';
4
5
  import { ensureRuntimeForSession } from '../cognition/runtime.js';
5
6
  import { VERSION } from '../version.js';
6
7
  const orientInput = z.object({
@@ -13,29 +14,203 @@ const orientInput = z.object({
13
14
  deadendLimit: z.int().nonnegative().optional(),
14
15
  noteLimit: z.int().nonnegative().optional(),
15
16
  });
17
+ const scenario = z.enum(['resume', 'implementation', 'debugging', 'verification', 'handoff', 'exploration', 'unknown']);
18
+ const trigger = z.enum(['session-start', 'prompt', 'manual', 'tool-call']);
19
+ const noteKind = z.enum(['problem', 'action', 'observation', 'decision']);
20
+ const noteActor = z.enum(['agent', 'human']);
21
+ const noteStatus = z.enum(['open', 'resolved']);
22
+ const compileOptions = z.object({
23
+ contextId: z.string().optional(),
24
+ query: z.string().optional(),
25
+ scenario: scenario.optional(),
26
+ scope: z.string().optional(),
27
+ paths: z.array(z.string()).optional(),
28
+ maxChars: z.int().nonnegative().optional(),
29
+ knowledgeLimit: z.int().nonnegative().optional(),
30
+ deadendLimit: z.int().nonnegative().optional(),
31
+ noteLimit: z.int().nonnegative().optional(),
32
+ activityLimit: z.int().nonnegative().optional(),
33
+ trigger: trigger.optional(),
34
+ adapter: z.string().optional(),
35
+ sessionId: z.string().optional(),
36
+ eventId: z.string().optional(),
37
+ });
38
+ const expandInput = z.object({
39
+ path: z.string().optional(),
40
+ manifestHash: z.string(),
41
+ candidateId: z.string().optional(),
42
+ itemHash: z.string().optional(),
43
+ level: z.enum(['structured', 'evidence']),
44
+ maxChars: z.int().nonnegative().optional(),
45
+ compile: compileOptions,
46
+ });
47
+ const compileInput = z.object({ path: z.string().optional(), ...compileOptions.shape });
48
+ const resumeInput = z.object({
49
+ path: z.string().optional(),
50
+ contextId: z.string().optional(),
51
+ maxChars: z.int().nonnegative().optional(),
52
+ includeDetails: z.boolean().optional(),
53
+ query: z.string().optional(),
54
+ scope: z.string().optional(),
55
+ });
56
+ const noteListInput = z.object({
57
+ path: z.string().optional(),
58
+ contextId: z.string().optional(),
59
+ openOnly: z.boolean().optional(),
60
+ limit: z.int().nonnegative().optional(),
61
+ maxChars: z.int().nonnegative().optional(),
62
+ });
63
+ const noteAddInput = z.object({
64
+ path: z.string().optional(),
65
+ message: z.string(),
66
+ kind: noteKind.optional(),
67
+ actor: noteActor.optional(),
68
+ contextId: z.string().optional(),
69
+ snapshotId: z.string().optional(),
70
+ relatedFiles: z.array(z.string()).optional(),
71
+ status: noteStatus.optional(),
72
+ });
73
+ const checkpointInput = z.object({
74
+ path: z.string().optional(),
75
+ contextId: z.string().optional(),
76
+ absorbNotes: z.boolean().optional(),
77
+ title: z.string().optional(),
78
+ progressSummary: z.string().optional(),
79
+ lastError: z.string().nullable().optional(),
80
+ completed: z.array(z.string()).optional(),
81
+ pendingTasks: z.array(z.string()).optional(),
82
+ decisions: z.array(z.string()).optional(),
83
+ risks: z.array(z.string()).optional(),
84
+ relatedFiles: z.array(z.string()).optional(),
85
+ });
86
+ const usageReportInput = z.object({
87
+ path: z.string().optional(),
88
+ since: z.string().optional(),
89
+ until: z.string().optional(),
90
+ days: z.int().nonnegative().optional(),
91
+ month: z.string().optional(),
92
+ granularity: z.enum(['day', 'week', 'month']).optional(),
93
+ baselineTokens: z.number().nonnegative().optional(),
94
+ baselineSource: z.enum(['manual', 'recorded-control']).optional(),
95
+ });
16
96
  function errorMessage(error) {
17
97
  return error instanceof Error ? error.message : String(error);
18
98
  }
99
+ function successResponse(result) {
100
+ return {
101
+ content: [{ type: 'text', text: JSON.stringify(result) }],
102
+ structuredContent: result,
103
+ };
104
+ }
105
+ function failureResponse(tool, error) {
106
+ return {
107
+ isError: true,
108
+ content: [{ type: 'text', text: `${tool} failed: ${errorMessage(error)}` }],
109
+ };
110
+ }
19
111
  export function createMcpServer() {
20
112
  const server = new McpServer({ name: 'fluffy-context', version: VERSION });
21
113
  server.registerTool('context_orient', {
22
114
  title: 'Orient context',
23
- description: 'Load bounded, read-only task context with verified consensus and open notes.',
115
+ description: 'Load bounded, read-only task context with verified consensus and open notes. Compiler manifests expose deterministic levels, hashes, and estimated tokens through the Agent API and CLI.',
24
116
  inputSchema: orientInput,
25
117
  }, async ({ path, ...options }) => {
26
118
  try {
27
119
  await ensureRuntimeForSession(path, { source: 'mcp', timeoutMs: 1_000 }).catch(() => undefined);
28
- const result = await contextOrient(path, options);
29
- return {
30
- content: [{ type: 'text', text: JSON.stringify(result) }],
31
- structuredContent: result,
32
- };
120
+ return successResponse(await contextOrient(path, options, 'mcp'));
121
+ }
122
+ catch (error) {
123
+ return failureResponse('context_orient', error);
124
+ }
125
+ });
126
+ server.registerTool('context_expand', {
127
+ title: 'Expand context candidate',
128
+ description: 'Read a selected compiler candidate at structured or evidence level without writing project state or reading file contents.',
129
+ inputSchema: expandInput,
130
+ }, async ({ path, ...input }) => {
131
+ try {
132
+ await ensureRuntimeForSession(path, { source: 'mcp', timeoutMs: 1_000 }).catch(() => undefined);
133
+ return successResponse(await contextExpand(path, input, 'mcp'));
134
+ }
135
+ catch (error) {
136
+ return failureResponse('context_expand', error);
137
+ }
138
+ });
139
+ server.registerTool('context_compile', {
140
+ title: 'Compile context manifest',
141
+ description: 'Compile a bounded, deterministic, read-only context manifest without writing project state or reading file contents.',
142
+ inputSchema: compileInput,
143
+ }, async ({ path, ...options }) => {
144
+ try {
145
+ await ensureRuntimeForSession(path, { source: 'mcp', timeoutMs: 1_000 }).catch(() => undefined);
146
+ return successResponse(await contextCompile(path, options, 'mcp'));
147
+ }
148
+ catch (error) {
149
+ return failureResponse('context_compile', error);
150
+ }
151
+ });
152
+ server.registerTool('context_resume', {
153
+ title: 'Resume context',
154
+ description: 'Resume the selected active context with a bounded summary and optional details. This may update the existing lastUsedAt field.',
155
+ inputSchema: resumeInput,
156
+ }, async ({ path, ...options }) => {
157
+ try {
158
+ await ensureRuntimeForSession(path, { source: 'mcp', timeoutMs: 1_000 }).catch(() => undefined);
159
+ return successResponse(await loadContext(path, options, 'mcp'));
160
+ }
161
+ catch (error) {
162
+ return failureResponse('context_resume', error);
163
+ }
164
+ });
165
+ server.registerTool('context_note_list', {
166
+ title: 'List context notes',
167
+ description: 'List bounded notes without modifying project state or absorbing or resolving notes.',
168
+ inputSchema: noteListInput,
169
+ }, async ({ path, ...query }) => {
170
+ try {
171
+ await ensureRuntimeForSession(path, { source: 'mcp', timeoutMs: 1_000 }).catch(() => undefined);
172
+ return successResponse({ notes: await listNotes(path, query) });
173
+ }
174
+ catch (error) {
175
+ return failureResponse('context_note_list', error);
176
+ }
177
+ });
178
+ server.registerTool('context_note_add', {
179
+ title: 'Add context note',
180
+ description: 'Append one explicit note to project state. This writes the local context store but does not create a Snapshot.',
181
+ inputSchema: noteAddInput,
182
+ }, async ({ path, message, ...input }) => {
183
+ try {
184
+ await ensureRuntimeForSession(path, { source: 'mcp', timeoutMs: 1_000 }).catch(() => undefined);
185
+ return successResponse(await addNote(path, message, input));
186
+ }
187
+ catch (error) {
188
+ return failureResponse('context_note_add', error);
189
+ }
190
+ });
191
+ server.registerTool('context_usage_report', {
192
+ title: 'Context usage report',
193
+ description: 'Build a read-only report of context assets, explicit reuse, estimated token investment, and operation metrics.',
194
+ inputSchema: usageReportInput,
195
+ }, async ({ path, ...options }) => {
196
+ try {
197
+ return successResponse(await contextUsageReport(path, options));
198
+ }
199
+ catch (error) {
200
+ return failureResponse('context_usage_report', error);
201
+ }
202
+ });
203
+ server.registerTool('context_checkpoint', {
204
+ title: 'Checkpoint context',
205
+ description: 'Explicitly save context progress and create a Snapshot when changes are present; runtime rate limits remain enforced.',
206
+ inputSchema: checkpointInput,
207
+ }, async ({ path, ...input }) => {
208
+ try {
209
+ await ensureRuntimeForSession(path, { source: 'mcp', timeoutMs: 1_000 }).catch(() => undefined);
210
+ return successResponse(await saveContext(path, input));
33
211
  }
34
212
  catch (error) {
35
- return {
36
- isError: true,
37
- content: [{ type: 'text', text: `context_orient failed: ${errorMessage(error)}` }],
38
- };
213
+ return failureResponse('context_checkpoint', error);
39
214
  }
40
215
  });
41
216
  return server;
@@ -253,6 +253,16 @@ export async function checkpoint(startPath, input, options = {}) {
253
253
  }
254
254
  const content = await contextFromInput(projectRoot, input, previous);
255
255
  const patch = diffContext(previous, content);
256
+ const hasContent = content.progressSummary.length > 0
257
+ || content.lastError !== null
258
+ || content.completed.length > 0
259
+ || content.pendingTasks.length > 0
260
+ || content.decisions.length > 0
261
+ || content.risks.length > 0
262
+ || content.relatedFiles.length > 0;
263
+ if (!hasContent && !previousSnapshot) {
264
+ return { status: 'no_change', context, snapshot: null };
265
+ }
256
266
  if (previousSnapshot && Object.keys(patch).length === 0)
257
267
  return { status: 'no_change', context, snapshot: previousSnapshot };
258
268
  const minSaveIntervalMs = options.minSaveIntervalMs ?? 10_000;
@@ -304,7 +304,7 @@ export type CheckpointResult = {
304
304
  } | {
305
305
  status: 'no_change';
306
306
  context: ContextMetadata;
307
- snapshot: Snapshot;
307
+ snapshot: Snapshot | null;
308
308
  } | {
309
309
  status: 'rate_limited';
310
310
  context: ContextMetadata;
@@ -1 +1 @@
1
- export declare const VERSION = "0.7.2";
1
+ export declare const VERSION = "0.7.7";
@@ -1 +1 @@
1
- export const VERSION = '0.7.2';
1
+ export const VERSION = '0.7.7';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fluffy-context",
3
- "version": "0.7.2",
3
+ "version": "0.7.7",
4
4
  "description": "Local context management CLI and MCP tools for AI coding agents",
5
5
  "license": "MIT",
6
6
  "author": "FluffyChi-Xing",
@@ -19,9 +19,16 @@ compatibility: 需要 Node.js >=20.19.0;Git 可选。CLI 通过 npm 全局安
19
19
  - 仅需要完整详情时再运行 `ctx resume --max-chars <预算>`;它会保留既有的 `lastUsedAt` 更新语义。
20
20
  - Agent 集成从 `fluffy-context` 或 `fluffy-context/agent` 导入 `contextOrient`、`compileContext`、`saveContext`、`loadContext` 和 `searchContext`,不应直接读写 `.context` 或导入内部 `dist/...` 路径。
21
21
  - `ctx compile` 的 `activity` section 只表示 Runtime 观察到的近期文件 metadata 变化;它不代表文件被读取或理解,也不替代 checkpoint、Note 或已验证 Knowledge。
22
+ - 0.7.7 的 Compiler Manifest 提供稳定 `identity.manifestHash`、候选 `itemHash`、`tokenEstimate` 和 `level` 契约;默认输出 `summary` 层。
23
+ - 只对 selected candidate 使用 `ctx expand --manifest-hash ... --candidate-id ... --level structured|evidence`。必须带上原始 compile 参数和相同的 compile budget;Manifest 过期时应重新 compile,不要绕过 hash 校验。
24
+ - `expand` 是无状态、只读的渐进式展开:`complete` 才提供完整 `content`,`partial` 提供有界 `text` 且 `content` 为 null。它不写入 Context/journal、不记录 feedback、不读取项目文件正文、diff、命令输出或敏感数据。Agent API 使用 `contextExpand`,MCP 使用 `context_expand`。
22
25
  - 新任务先发现已验证共识,再开始实现;候选项只在显式审查时使用。
23
26
  - 普通查询只使用已验证 Knowledge 和 Deadend;需要审查候选项时显式使用 `--all`。
24
27
  - CLI 业务命令的标准输出是 JSON;错误写入标准错误并返回非零退出码。解析输出时不要把 `--help` 的纯文本当作 JSON。
28
+ - 每周或每月复盘时使用 `ctx usage report`,不要为每条 prompt 运行;它只读 journal 和 v1 数据,不创建事件或修改项目状态。
29
+ - Report 中的 `chars / 4` 是明确标注的 estimated token,不是真实 Provider token;Provider token 和计费金额当前 unavailable。
30
+ - 只有显式 `record.used` 才算 Knowledge/Deadend reuse;compile 命中或 orient 返回不等于复用。
31
+ - 没有显式 baseline 时,report 不生成 savings、ROI 或 payback 数字;report 不能替代 `doctor`、`journal verify` 或人工治理。
25
32
 
26
33
  ## 快速工作流
27
34
 
@@ -43,6 +50,21 @@ ctx learn ... / ctx deadend ... → 显式 verify
43
50
 
44
51
  `ctx orient` 不带查询时也有效,用于恢复当前任务和 open Note。带查询时只发现已验证的 Knowledge 与 Deadend;不要以候选项驱动普通实现。
45
52
 
53
+ ### Usage report
54
+
55
+ 在阶段性复盘、周报或月报时运行只读观测报告:
56
+
57
+ ```bash
58
+ ctx usage report --days 30
59
+ ctx usage report --month 2026-08 --granularity week
60
+ ctx usage report --since 2026-08-01T00:00:00.000Z --until 2026-09-01T00:00:00.000Z --baseline-tokens 12000 --baseline-source manual
61
+ ctx usage report --format ascii
62
+ ```
63
+
64
+ JSON 是默认输出;`--format ascii` 仅用于终端展示。报告分别统计 v1 文件资产和 v2 journal 事件,展示 Knowledge、Deadend、Note、Context、Snapshot、显式 reuse、操作趋势与 estimated token 投入。`record.used` 是唯一的 reuse 信号,候选项命中、compile 选择和普通检索都不算复用。
65
+
66
+ `--baseline-tokens` 必须是显式对照输入。未提供 baseline 时 savings、ROI 和 payback 为 `null` 或 `no_baseline`;字符换算不代表 Provider 真实 token,也不提供默认单价。报告失败时应使用 `ctx doctor` 与 `ctx journal verify` 进一步诊断,而不是把 report 当作数据修复工具。
67
+
46
68
  ### 初始化
47
69
 
48
70
  ```bash
@@ -100,7 +122,9 @@ ctx resume --path path/to/project --max-chars 2000
100
122
 
101
123
  ## MCP 与 Claude Code 集成
102
124
 
103
- `ctx agent serve` 提供 MCP stdio server,注册 `context_orient` 工具。标准输入和输出都是 MCP 协议,不能输出提示、日志或交互文本;调用错误是单次工具错误,不应让 server 退出。
125
+ `ctx agent serve` 提供 MCP stdio server,注册以下工具:`context_orient`、`context_expand`、`context_compile`、`context_resume`、`context_note_list`、`context_note_add`、`context_usage_report` 和 `context_checkpoint`。标准输入和输出都是 MCP 协议,不能输出提示、日志或交互文本;调用错误是单次工具错误,不应让 server 退出。
126
+
127
+ 读取工作流通常是 `context_orient → context_compile → context_expand`。`context_compile` 与 `context_expand` 继续保持有界、确定性和不读取文件正文的边界;`context_resume` 复用 `loadContext`,可能更新 `lastUsedAt`。`context_note_list` 是只读的,`context_note_add` 和 `context_checkpoint` 是显式写入操作,分别新增 Note 或保存 Snapshot;它们不会自动记录 feedback、Knowledge、Deadend 或重复 checkpoint。Knowledge/Deadend 的完整记录和治理仍使用 CLI/Agent API。
104
128
 
105
129
  ```bash
106
130
  ctx agent serve
@@ -336,7 +360,9 @@ ctx learn "订单取消后不能再次进入支付中状态"
336
360
  [ ] .contextignored 排除敏感和越界路径
337
361
  [ ] Knowledge candidate → verified
338
362
  [ ] Deadend candidate → verified
339
- [ ] MCP context_orient 的 no_context 和单次错误不会终止 server
363
+ [ ] MCP context_orient/context_expand/context_compile/context_resume/context_note_list/context_note_add/context_usage_report/context_checkpoint 可调用,且单次错误不会终止 server
364
+ [ ] ctx usage report 的 JSON/ASCII 输出、baseline 边界和只读性符合预期
365
+ [ ] MCP 只读工具不写入 Context;Note add 和 checkpoint 仅在显式调用时写入
340
366
  [ ] Claude 集成预览不写文件,--apply 幂等且拒绝冲突
341
367
  [ ] Hook 失败时 fail open,且不会自动 checkpoint
342
368
  [ ] npm pack 的安装包可运行 ctx 并导入 contextOrient