@voybio/ace-swarm 2.4.3 → 2.4.6

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 (144) hide show
  1. package/CHANGELOG.md +78 -0
  2. package/README.md +72 -532
  3. package/assets/agent-state/INTERFACE_REGISTRY.md +2 -2
  4. package/assets/agent-state/MODULES/gates/gate-autonomy.json +4 -2
  5. package/assets/agent-state/MODULES/gates/gate-completeness.json +4 -2
  6. package/assets/agent-state/MODULES/gates/gate-operability.json +4 -2
  7. package/assets/agent-state/MODULES/gates/gate-security.json +3 -1
  8. package/assets/agent-state/MODULES/registry.json +1 -4
  9. package/assets/agent-state/MODULES/roles/capability-build.json +1 -2
  10. package/assets/agent-state/MODULES/roles/capability-eval.json +1 -1
  11. package/assets/agent-state/MODULES/roles/capability-qa.json +1 -2
  12. package/assets/agent-state/QUALITY_GATES.md +25 -9
  13. package/assets/agent-state/TEAL_CONFIG.md +2 -11
  14. package/assets/eval-battery/battery.json +85 -0
  15. package/assets/eval-battery/fixtures/edit-target.js +3 -0
  16. package/assets/scripts/render-mcp-configs.sh +12 -0
  17. package/dist/ace-context.d.ts +18 -0
  18. package/dist/ace-context.js +161 -16
  19. package/dist/ace-server-instructions.js +36 -9
  20. package/dist/astgrep-index.d.ts +7 -1
  21. package/dist/astgrep-index.js +66 -39
  22. package/dist/bridge-trace.d.ts +17 -0
  23. package/dist/bridge-trace.js +23 -0
  24. package/dist/cli.d.ts +9 -1
  25. package/dist/cli.js +619 -10
  26. package/dist/discovery-runtime-wrappers.d.ts +16 -1
  27. package/dist/discovery-runtime-wrappers.js +65 -9
  28. package/dist/eval-harness.d.ts +110 -0
  29. package/dist/eval-harness.js +254 -0
  30. package/dist/experience-reflector.d.ts +13 -0
  31. package/dist/experience-reflector.js +62 -0
  32. package/dist/gate-contract.d.ts +63 -0
  33. package/dist/gate-contract.js +178 -0
  34. package/dist/goal-engine.d.ts +155 -0
  35. package/dist/goal-engine.js +795 -0
  36. package/dist/helpers/store-resolution.js +40 -39
  37. package/dist/hermes/session-manager.d.ts +10 -0
  38. package/dist/hermes/session-manager.js +130 -27
  39. package/dist/job-scheduler.d.ts +58 -0
  40. package/dist/job-scheduler.js +202 -2
  41. package/dist/job-worker.d.ts +87 -0
  42. package/dist/job-worker.js +182 -0
  43. package/dist/local-model-policy.d.ts +8 -2
  44. package/dist/local-model-policy.js +31 -17
  45. package/dist/local-model-runtime.d.ts +6 -0
  46. package/dist/local-model-runtime.js +130 -2
  47. package/dist/measured-classing.d.ts +66 -0
  48. package/dist/measured-classing.js +165 -0
  49. package/dist/model-bridge.d.ts +45 -0
  50. package/dist/model-bridge.js +502 -44
  51. package/dist/orchestrator-supervisor.js +1 -1
  52. package/dist/plan-proposal.d.ts +7 -0
  53. package/dist/plan-proposal.js +74 -4
  54. package/dist/playbook-adaptation.d.ts +37 -0
  55. package/dist/playbook-adaptation.js +159 -0
  56. package/dist/playbook-curator.d.ts +16 -0
  57. package/dist/playbook-curator.js +106 -0
  58. package/dist/playbook-llm-reflector.d.ts +26 -0
  59. package/dist/playbook-llm-reflector.js +157 -0
  60. package/dist/public-surface.d.ts +2 -0
  61. package/dist/public-surface.js +26 -14
  62. package/dist/runtime-executor.js +45 -2
  63. package/dist/runtime-tool-specs.d.ts +8 -1
  64. package/dist/runtime-tool-specs.js +19 -1
  65. package/dist/safe-edit.d.ts +20 -0
  66. package/dist/safe-edit.js +355 -78
  67. package/dist/schemas.js +16 -15
  68. package/dist/server.d.ts +11 -2
  69. package/dist/server.js +55 -5
  70. package/dist/shared.d.ts +12 -0
  71. package/dist/shared.js +13 -1
  72. package/dist/source-mutation-coordinator.d.ts +54 -0
  73. package/dist/source-mutation-coordinator.js +259 -0
  74. package/dist/status-events.d.ts +4 -1
  75. package/dist/status-events.js +41 -6
  76. package/dist/store/ace-packed-store.d.ts +28 -9
  77. package/dist/store/ace-packed-store.js +324 -57
  78. package/dist/store/bgzf.d.ts +4 -0
  79. package/dist/store/bgzf.js +94 -0
  80. package/dist/store/bootstrap-store.d.ts +4 -1
  81. package/dist/store/bootstrap-store.js +87 -9
  82. package/dist/store/cache-workspace.d.ts +8 -0
  83. package/dist/store/cache-workspace.js +67 -1
  84. package/dist/store/crc32.d.ts +2 -0
  85. package/dist/store/crc32.js +16 -0
  86. package/dist/store/fsck.d.ts +16 -0
  87. package/dist/store/fsck.js +240 -0
  88. package/dist/store/gate-contract-migration.d.ts +10 -0
  89. package/dist/store/gate-contract-migration.js +413 -0
  90. package/dist/store/materializers/context-snapshot-materializer.d.ts +1 -0
  91. package/dist/store/materializers/context-snapshot-materializer.js +4 -1
  92. package/dist/store/materializers/projection-manager.d.ts +7 -0
  93. package/dist/store/materializers/projection-manager.js +37 -0
  94. package/dist/store/materializers/scheduler-projection-materializer.js +10 -3
  95. package/dist/store/repositories/goal-repository.d.ts +112 -0
  96. package/dist/store/repositories/goal-repository.js +157 -0
  97. package/dist/store/repositories/playbook-repository.d.ts +107 -0
  98. package/dist/store/repositories/playbook-repository.js +314 -0
  99. package/dist/store/store-artifacts.js +44 -2
  100. package/dist/store/store-authority-audit.js +1 -0
  101. package/dist/store/store-cli.d.ts +30 -0
  102. package/dist/store/store-cli.js +242 -0
  103. package/dist/store/store-snapshot.d.ts +12 -0
  104. package/dist/store/store-snapshot.js +74 -11
  105. package/dist/store/types.d.ts +1 -0
  106. package/dist/store/write-coordinator.d.ts +7 -0
  107. package/dist/store/write-coordinator.js +11 -0
  108. package/dist/todo-state.d.ts +21 -0
  109. package/dist/todo-state.js +150 -53
  110. package/dist/tools-agent.js +86 -22
  111. package/dist/tools-discovery.js +5 -3
  112. package/dist/tools-files.js +107 -23
  113. package/dist/tools-framework.js +146 -40
  114. package/dist/tools-goal.d.ts +11 -0
  115. package/dist/tools-goal.js +108 -0
  116. package/dist/tools-handoff.js +7 -11
  117. package/dist/tools-lifecycle.js +2 -0
  118. package/dist/tools-scheduler.js +44 -1
  119. package/dist/tools-skills.js +3 -2
  120. package/dist/tools-todo.js +64 -3
  121. package/dist/tools.js +2 -0
  122. package/dist/tui/chat.js +4 -0
  123. package/dist/tui/index.js +4 -1
  124. package/dist/tui/ollama.d.ts +2 -0
  125. package/dist/tui/openai-compatible.d.ts +14 -0
  126. package/dist/tui/openai-compatible.js +15 -2
  127. package/dist/tui/provider-discovery.d.ts +1 -0
  128. package/dist/tui/provider-discovery.js +2 -2
  129. package/docs/cli-reference.md +125 -0
  130. package/docs/development.md +46 -0
  131. package/docs/evaluation-safety.md +27 -0
  132. package/docs/experience-playbook.md +35 -0
  133. package/docs/hermes.md +129 -0
  134. package/docs/library-api.md +31 -0
  135. package/docs/mcp-tool-reference.md +290 -0
  136. package/docs/provider-security.md +85 -0
  137. package/docs/state-model.md +113 -0
  138. package/docs/troubleshooting.md +95 -0
  139. package/docs/tui-operator-guide.md +75 -0
  140. package/package.json +5 -1
  141. package/scripts/dev/run_hermes_crispr_live.mjs +212 -0
  142. package/assets/agent-state/MODULES/gates/gate-correctness.json +0 -7
  143. package/assets/agent-state/MODULES/gates/gate-evaluation.json +0 -7
  144. package/assets/agent-state/MODULES/gates/gate-typescript-public-surface.json +0 -7
@@ -0,0 +1,30 @@
1
+ export interface StoreLsEntry {
2
+ /** Logical path as a user would recognize it (e.g. "agent-state/kanban.json"), or the raw key with --keys. */
3
+ path: string;
4
+ key: string;
5
+ bytes: number;
6
+ }
7
+ export declare function listStoreEntries(workspaceRoot: string, prefix?: string, useRawKeys?: boolean): StoreLsEntry[];
8
+ export declare function runLsCommand(workspaceRoot: string, args: string[]): Promise<void>;
9
+ export interface ReadTarget {
10
+ content: string;
11
+ source: "virtual" | "store" | "disk";
12
+ key?: string;
13
+ divergent: boolean;
14
+ diskContent?: string;
15
+ storeContent?: string;
16
+ storeKey?: string;
17
+ }
18
+ /**
19
+ * Resolves a path to its content using the documented precedence:
20
+ * 1. `.ace#`-style virtual path -> direct store key read.
21
+ * 2. Logical path -> resolveStoreFallbackKeysForPath, first hit wins.
22
+ * 3. Raw store key (namespace-prefixed) -> direct store key read.
23
+ * 4. Workspace disk file.
24
+ * When both a disk copy and a store copy exist and differ, the result is
25
+ * flagged `divergent` with both variants attached so the caller can warn
26
+ * and honor an explicit `preferSource` override.
27
+ */
28
+ export declare function resolveReadTarget(workspaceRoot: string, inputPath: string, preferSource?: "disk" | "store"): ReadTarget | undefined;
29
+ export declare function runReadCommand(workspaceRoot: string, args: string[]): Promise<void>;
30
+ //# sourceMappingURL=store-cli.d.ts.map
@@ -0,0 +1,242 @@
1
+ /**
2
+ * `ace ls` / `ace read` (aliases: `ace get`, `ace cat`) — the human read
3
+ * surface for the ACEPACK store. Precondition for hiding generated
4
+ * artifacts by default (Phase 2): once content only lives in the store,
5
+ * these are the only way a human sees it without opening the binary file.
6
+ *
7
+ * Read contract: content is either the complete decoded block whose byte
8
+ * length equals the index entry's length (see `validateChunkBounds` /
9
+ * `decodeChunkSync` in store-snapshot.ts), or resolution fails loudly.
10
+ * Reads are length-driven, never delimiter-driven — a block containing the
11
+ * literal text "EOF" mid-content is read in full, not truncated at it.
12
+ */
13
+ import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
14
+ import { dirname, resolve } from "node:path";
15
+ import { resolveStoreFallbackKeysForPath, resolveWorkspaceReadPath } from "../helpers.js";
16
+ import { getWorkspaceStorePath, listStoreEntrySizesSync, listStoreKeysSync, readStoreBlobSync } from "./store-snapshot.js";
17
+ function logicalPathForKey(key) {
18
+ const artifactPrefix = "state/artifacts/";
19
+ if (key.startsWith(artifactPrefix))
20
+ return key.slice(artifactPrefix.length);
21
+ const snapshotPrefix = "state/memory/context_snapshots/";
22
+ if (key.startsWith(snapshotPrefix))
23
+ return `agent-state/context-snapshots/${key.slice(snapshotPrefix.length)}`;
24
+ if (key === "state/source-edit-events.ndjson")
25
+ return "agent-state/source-edit-events.ndjson";
26
+ if (key === "state/source-edit-events-archive.ndjson")
27
+ return "agent-state/source-edit-events-archive.ndjson";
28
+ return key;
29
+ }
30
+ function logicalPrefixToKeyPrefix(prefix) {
31
+ if (!prefix)
32
+ return "";
33
+ const snapshotPrefix = "agent-state/context-snapshots";
34
+ if (prefix.startsWith(snapshotPrefix)) {
35
+ return `state/memory/context_snapshots/${prefix.slice(snapshotPrefix.length).replace(/^\//, "")}`;
36
+ }
37
+ if (prefix.startsWith("agent-state")) {
38
+ return `state/artifacts/${prefix}`;
39
+ }
40
+ return prefix;
41
+ }
42
+ function sectionForKey(key) {
43
+ if (key.startsWith("state/artifacts/") || key.startsWith("state/memory/context_snapshots/"))
44
+ return "operational";
45
+ if (key.startsWith("knowledge/"))
46
+ return "knowledge";
47
+ return "state";
48
+ }
49
+ export function listStoreEntries(workspaceRoot, prefix = "", useRawKeys = false) {
50
+ const rawPrefix = useRawKeys ? prefix : logicalPrefixToKeyPrefix(prefix);
51
+ const keys = new Set(listStoreKeysSync(workspaceRoot, rawPrefix));
52
+ if (!useRawKeys) {
53
+ const sourceEditLogicalPath = "agent-state/source-edit-events.ndjson";
54
+ if (sourceEditLogicalPath.startsWith(prefix || "agent-state") ||
55
+ (prefix || "agent-state").startsWith(sourceEditLogicalPath)) {
56
+ for (const key of listStoreKeysSync(workspaceRoot, "state/source-edit-events.ndjson")) {
57
+ keys.add(key);
58
+ }
59
+ }
60
+ const sourceEditArchiveLogicalPath = "agent-state/source-edit-events-archive.ndjson";
61
+ if (sourceEditArchiveLogicalPath.startsWith(prefix || "agent-state") ||
62
+ (prefix || "agent-state").startsWith(sourceEditArchiveLogicalPath)) {
63
+ for (const key of listStoreKeysSync(workspaceRoot, "state/source-edit-events-archive.ndjson")) {
64
+ keys.add(key);
65
+ }
66
+ }
67
+ }
68
+ const sizeByKey = new Map(listStoreEntrySizesSync(workspaceRoot, "").map((entry) => [entry.key, entry.length]));
69
+ return [...keys].map((key) => ({
70
+ path: useRawKeys ? key : logicalPathForKey(key),
71
+ key,
72
+ bytes: sizeByKey.get(key) ?? 0,
73
+ }));
74
+ }
75
+ export async function runLsCommand(workspaceRoot, args) {
76
+ const positional = args.filter((a) => !a.startsWith("--"));
77
+ const prefixArg = positional[0] ?? "";
78
+ const useRawKeys = args.includes("--keys");
79
+ const asJson = args.includes("--json");
80
+ const storePath = getWorkspaceStorePath(workspaceRoot);
81
+ if (!existsSync(storePath)) {
82
+ console.error(`No store found at ${storePath}. Run 'ace init' first.`);
83
+ process.exitCode = 1;
84
+ return;
85
+ }
86
+ const entries = listStoreEntries(workspaceRoot, prefixArg, useRawKeys);
87
+ if (asJson) {
88
+ console.log(JSON.stringify(entries, null, 2));
89
+ return;
90
+ }
91
+ if (entries.length === 0) {
92
+ console.log(prefixArg ? `No store entries under '${prefixArg}'.` : "Store is empty.");
93
+ return;
94
+ }
95
+ const sections = { operational: [], knowledge: [], state: [] };
96
+ for (const entry of entries)
97
+ sections[sectionForKey(entry.key)].push(entry);
98
+ const labels = {
99
+ operational: "Operational artifacts",
100
+ knowledge: "Knowledge",
101
+ state: "State",
102
+ };
103
+ for (const section of ["operational", "knowledge", "state"]) {
104
+ const list = sections[section];
105
+ if (list.length === 0)
106
+ continue;
107
+ console.log(`\n${labels[section]} (${list.length}):`);
108
+ for (const entry of [...list].sort((a, b) => a.path.localeCompare(b.path))) {
109
+ console.log(` ${entry.path} (${entry.bytes}B)`);
110
+ }
111
+ }
112
+ }
113
+ function tryDiskRead(workspaceRoot, inputPath) {
114
+ try {
115
+ const abs = resolveWorkspaceReadPath(inputPath);
116
+ if (existsSync(abs) && statSync(abs).isFile()) {
117
+ return readFileSync(abs, "utf-8");
118
+ }
119
+ }
120
+ catch {
121
+ // not a resolvable disk path — not a divergence candidate
122
+ }
123
+ return undefined;
124
+ }
125
+ /**
126
+ * Resolves a path to its content using the documented precedence:
127
+ * 1. `.ace#`-style virtual path -> direct store key read.
128
+ * 2. Logical path -> resolveStoreFallbackKeysForPath, first hit wins.
129
+ * 3. Raw store key (namespace-prefixed) -> direct store key read.
130
+ * 4. Workspace disk file.
131
+ * When both a disk copy and a store copy exist and differ, the result is
132
+ * flagged `divergent` with both variants attached so the caller can warn
133
+ * and honor an explicit `preferSource` override.
134
+ */
135
+ export function resolveReadTarget(workspaceRoot, inputPath, preferSource) {
136
+ if (inputPath.includes(".ace#")) {
137
+ const marker = ".ace#";
138
+ const idx = inputPath.indexOf(marker);
139
+ const key = inputPath.slice(idx + marker.length);
140
+ const content = readStoreBlobSync(workspaceRoot, key);
141
+ if (typeof content !== "string")
142
+ return undefined;
143
+ return { content, source: "virtual", key, divergent: false };
144
+ }
145
+ const fallbackKeys = resolveStoreFallbackKeysForPath(inputPath);
146
+ for (const key of fallbackKeys) {
147
+ const storeContent = readStoreBlobSync(workspaceRoot, key);
148
+ if (typeof storeContent !== "string")
149
+ continue;
150
+ const diskContent = tryDiskRead(workspaceRoot, inputPath);
151
+ const divergent = typeof diskContent === "string" && diskContent !== storeContent;
152
+ const useDisk = divergent && preferSource === "disk";
153
+ return {
154
+ content: useDisk ? diskContent : storeContent,
155
+ source: useDisk ? "disk" : "store",
156
+ key,
157
+ storeKey: key,
158
+ divergent,
159
+ diskContent,
160
+ storeContent,
161
+ };
162
+ }
163
+ if (/^(state|knowledge|topology|meta)\//.test(inputPath)) {
164
+ const content = readStoreBlobSync(workspaceRoot, inputPath);
165
+ if (typeof content === "string") {
166
+ return { content, source: "store", key: inputPath, storeKey: inputPath, divergent: false };
167
+ }
168
+ }
169
+ const diskOnly = tryDiskRead(workspaceRoot, inputPath);
170
+ if (typeof diskOnly === "string") {
171
+ return { content: diskOnly, source: "disk", divergent: false };
172
+ }
173
+ return undefined;
174
+ }
175
+ const VALUE_FLAGS = new Set(["--out", "--prefer"]);
176
+ function parsePositionalArgs(args) {
177
+ const positional = [];
178
+ for (let i = 0; i < args.length; i++) {
179
+ const arg = args[i];
180
+ if (arg.startsWith("--")) {
181
+ if (VALUE_FLAGS.has(arg))
182
+ i++; // skip the flag's value
183
+ continue;
184
+ }
185
+ positional.push(arg);
186
+ }
187
+ return positional;
188
+ }
189
+ export async function runReadCommand(workspaceRoot, args) {
190
+ const positional = parsePositionalArgs(args);
191
+ const inputPath = positional[0];
192
+ if (!inputPath) {
193
+ console.error("Usage: ace read <path> [--out <file>] [--source] [--prefer disk|store] [--json]");
194
+ process.exitCode = 1;
195
+ return;
196
+ }
197
+ const outIdx = args.indexOf("--out");
198
+ const outFile = outIdx >= 0 ? args[outIdx + 1] : undefined;
199
+ const preferIdx = args.indexOf("--prefer");
200
+ const preferRaw = preferIdx >= 0 ? args[preferIdx + 1] : undefined;
201
+ if (preferIdx >= 0 && preferRaw !== "disk" && preferRaw !== "store") {
202
+ console.error(`ace read: --prefer expects disk|store, got: ${preferRaw ?? "(missing)"}`);
203
+ process.exitCode = 1;
204
+ return;
205
+ }
206
+ const preferSource = preferRaw === "disk" || preferRaw === "store" ? preferRaw : undefined;
207
+ const showSource = args.includes("--source");
208
+ const asJson = args.includes("--json");
209
+ const result = resolveReadTarget(workspaceRoot, inputPath, preferSource);
210
+ if (!result) {
211
+ console.error(`ace read: not found: ${inputPath}`);
212
+ process.exitCode = 1;
213
+ return;
214
+ }
215
+ if (result.divergent) {
216
+ console.error(`⚠ store/disk copies differ for '${inputPath}' (use --prefer disk|store) — showing ${result.source}`);
217
+ }
218
+ if (asJson) {
219
+ console.log(JSON.stringify({
220
+ path: inputPath,
221
+ source: result.source,
222
+ key: result.key,
223
+ divergent: result.divergent,
224
+ content: result.content,
225
+ }, null, 2));
226
+ return;
227
+ }
228
+ if (outFile) {
229
+ const absOut = resolve(outFile);
230
+ mkdirSync(dirname(absOut), { recursive: true });
231
+ writeFileSync(absOut, result.content, "utf-8");
232
+ console.log(`Wrote ${Buffer.byteLength(result.content, "utf-8")} bytes to ${absOut}`);
233
+ return;
234
+ }
235
+ if (showSource) {
236
+ console.error(`# source=${result.source}${result.key ? ` key=${result.key}` : ""}`);
237
+ }
238
+ process.stdout.write(result.content);
239
+ if (!result.content.endsWith("\n"))
240
+ process.stdout.write("\n");
241
+ }
242
+ //# sourceMappingURL=store-cli.js.map
@@ -1,3 +1,11 @@
1
+ import { type ChunkIndexEntry } from "./types.js";
2
+ /**
3
+ * Bounds-checks an index entry against the file it was read from. Every
4
+ * index-driven read must pass through this before slicing — JS `slice`
5
+ * silently clamps at EOF instead of erroring, which previously let a
6
+ * corrupt/short index produce truncated content that looked plausible.
7
+ */
8
+ export declare function validateChunkBounds(entry: ChunkIndexEntry, fileLength: number, minOffset?: number): boolean;
1
9
  export declare function invalidateStoreSnapshotCache(storePath?: string): void;
2
10
  export declare function getCanonicalWorkspaceStorePath(workspaceRoot: string): string;
3
11
  export declare function getLegacyWorkspaceStorePath(workspaceRoot: string): string;
@@ -9,6 +17,10 @@ export declare function readStoreJsonSync<T>(workspaceRoot: string, key: string)
9
17
  export declare function readStoreJsonByPathSync<T>(storePath: string, key: string): T | undefined;
10
18
  export declare function listStoreKeysSync(workspaceRoot: string, prefix?: string): string[];
11
19
  export declare function listStoreKeysByPathSync(storePath: string, prefix?: string): string[];
20
+ export declare function listStoreEntrySizesSync(workspaceRoot: string, prefix?: string): Array<{
21
+ key: string;
22
+ length: number;
23
+ }>;
12
24
  export declare function getStoreStatSync(storePath: string): {
13
25
  mtimeMs: number;
14
26
  size: number;
@@ -1,7 +1,56 @@
1
1
  import { existsSync, readFileSync, statSync } from "node:fs";
2
2
  import { resolve } from "node:path";
3
- import { HEADER_SIZE, MAGIC } from "./types.js";
3
+ import { HEADER_SIZE, HEADER_SIZE_V1, MAGIC } from "./types.js";
4
+ import { crc32 } from "./crc32.js";
4
5
  const DECODE = new TextDecoder();
6
+ /**
7
+ * Bounds-checks an index entry against the file it was read from. Every
8
+ * index-driven read must pass through this before slicing — JS `slice`
9
+ * silently clamps at EOF instead of erroring, which previously let a
10
+ * corrupt/short index produce truncated content that looked plausible.
11
+ */
12
+ export function validateChunkBounds(entry, fileLength, minOffset = HEADER_SIZE_V1) {
13
+ if (!Number.isFinite(entry.offset) || !Number.isFinite(entry.length))
14
+ return false;
15
+ if (entry.offset < minOffset || entry.length < 0)
16
+ return false;
17
+ return entry.offset + 4 + entry.length <= fileLength;
18
+ }
19
+ function warnStoreReadOutOfBounds(key, entry, fileLength, reason) {
20
+ console.error(JSON.stringify({
21
+ event: "store_read_out_of_bounds",
22
+ key,
23
+ offset: entry.offset,
24
+ length: entry.length,
25
+ fileSize: fileLength,
26
+ reason,
27
+ }));
28
+ }
29
+ /**
30
+ * Decodes a chunk only if its bounds are valid AND the length prefix stored
31
+ * at `entry.offset` agrees with the index's `entry.length`. Any mismatch
32
+ * (index corruption, short read of a partially-visible file) returns
33
+ * `undefined` instead of silently-wrong or cross-section-bled content.
34
+ */
35
+ function decodeChunkSync(file, entry, key) {
36
+ if (!validateChunkBounds(entry, file.length)) {
37
+ warnStoreReadOutOfBounds(key, entry, file.length, "out_of_bounds");
38
+ return undefined;
39
+ }
40
+ const prefixed = bytesToUint32(file, entry.offset);
41
+ if (prefixed !== entry.length) {
42
+ warnStoreReadOutOfBounds(key, entry, file.length, "length_prefix_mismatch");
43
+ return undefined;
44
+ }
45
+ const start = entry.offset + 4;
46
+ const end = start + entry.length;
47
+ const chunk = file.slice(start, end);
48
+ if (typeof entry.crc === "number" && crc32(chunk) !== entry.crc) {
49
+ warnStoreReadOutOfBounds(key, entry, file.length, "crc_mismatch");
50
+ return undefined;
51
+ }
52
+ return DECODE.decode(chunk);
53
+ }
5
54
  let cached;
6
55
  export function invalidateStoreSnapshotCache(storePath) {
7
56
  if (!cached)
@@ -28,19 +77,27 @@ function readHeader(bytes) {
28
77
  };
29
78
  }
30
79
  function loadSnapshot(storePath) {
31
- if (!existsSync(storePath))
80
+ if (!existsSync(storePath)) {
81
+ invalidateStoreSnapshotCache(storePath);
32
82
  return undefined;
83
+ }
33
84
  const stats = statSync(storePath);
34
85
  if (cached &&
35
86
  cached.path === storePath &&
36
87
  cached.mtimeMs === stats.mtimeMs &&
37
- cached.size === stats.size) {
88
+ cached.size === stats.size &&
89
+ cached.ino === stats.ino) {
38
90
  return cached;
39
91
  }
40
92
  try {
41
93
  const file = readFileSync(storePath);
42
- if (file.length < HEADER_SIZE)
43
- return cached?.path === storePath ? cached : undefined;
94
+ // Never serve a stale cached snapshot as a fallback for an unreadable or
95
+ // short file — that would silently return deleted/old data. Missing is
96
+ // the safe answer; the caller already treats undefined as "not present".
97
+ if (file.length < HEADER_SIZE) {
98
+ invalidateStoreSnapshotCache(storePath);
99
+ return undefined;
100
+ }
44
101
  const { indexOffset, indexLength } = readHeader(file);
45
102
  const indexBytes = file.slice(indexOffset, indexOffset + indexLength);
46
103
  const parsed = JSON.parse(DECODE.decode(indexBytes));
@@ -48,6 +105,7 @@ function loadSnapshot(storePath) {
48
105
  path: storePath,
49
106
  mtimeMs: stats.mtimeMs,
50
107
  size: stats.size,
108
+ ino: stats.ino,
51
109
  file,
52
110
  index: new Map(Object.entries(parsed)),
53
111
  };
@@ -81,18 +139,14 @@ export function readStoreBlobSync(workspaceRoot, key) {
81
139
  const entry = snapshot?.index.get(key);
82
140
  if (!snapshot || !entry)
83
141
  return undefined;
84
- const start = entry.offset + 4;
85
- const end = start + entry.length;
86
- return DECODE.decode(snapshot.file.slice(start, end));
142
+ return decodeChunkSync(snapshot.file, entry, key);
87
143
  }
88
144
  export function readStoreBlobByPathSync(storePath, key) {
89
145
  const snapshot = loadSnapshot(storePath);
90
146
  const entry = snapshot?.index.get(key);
91
147
  if (!snapshot || !entry)
92
148
  return undefined;
93
- const start = entry.offset + 4;
94
- const end = start + entry.length;
95
- return DECODE.decode(snapshot.file.slice(start, end));
149
+ return decodeChunkSync(snapshot.file, entry, key);
96
150
  }
97
151
  export function readStoreJsonSync(workspaceRoot, key) {
98
152
  const raw = readStoreBlobSync(workspaceRoot, key);
@@ -132,6 +186,15 @@ export function listStoreKeysByPathSync(storePath, prefix = "") {
132
186
  .filter((key) => key.startsWith(prefix))
133
187
  .sort((a, b) => a.localeCompare(b));
134
188
  }
189
+ export function listStoreEntrySizesSync(workspaceRoot, prefix = "") {
190
+ const snapshot = loadSnapshot(getWorkspaceStorePath(workspaceRoot));
191
+ if (!snapshot)
192
+ return [];
193
+ return [...snapshot.index.entries()]
194
+ .filter(([key]) => key.startsWith(prefix))
195
+ .map(([key, entry]) => ({ key, length: entry.length }))
196
+ .sort((a, b) => a.key.localeCompare(b.key));
197
+ }
135
198
  export function getStoreStatSync(storePath) {
136
199
  const snapshot = loadSnapshot(storePath);
137
200
  if (!snapshot)
@@ -46,6 +46,7 @@ export declare const HEADER_SIZE_V1 = 64;
46
46
  export interface ChunkIndexEntry {
47
47
  offset: number;
48
48
  length: number;
49
+ crc?: number;
49
50
  }
50
51
  /** Zarrita AsyncReadable protocol */
51
52
  export interface AsyncReadable {
@@ -59,6 +59,13 @@ export declare class StoreSyncWriteBlockedByAsyncLeaseError extends Error {
59
59
  currentAsyncOperationLabels: string[];
60
60
  });
61
61
  }
62
+ export type StoreWriteLockClassification = "active" | "stale" | "unreadable";
63
+ /**
64
+ * Classifies a write-lock directory for maintenance tools. "active" leases
65
+ * must never be removed; "stale" (expired past TTL+grace) and "unreadable"
66
+ * (no/invalid lease.json) are safe repair targets.
67
+ */
68
+ export declare function classifyStoreWriteLockDir(lockPath: string, staleGraceMs?: number): StoreWriteLockClassification;
62
69
  export declare function withStoreWriteCoordinator<T>(storePath: string, fn: () => Promise<T>, options?: StoreWriteCoordinatorOptions): Promise<T>;
63
70
  export declare function withStoreWriteCoordinatorSync<T>(storePath: string, fn: () => T, options?: StoreWriteCoordinatorOptions): T;
64
71
  export declare function resolveStoreWriteLeasePath(workspaceRoot: string): string;
@@ -159,6 +159,17 @@ function leaseIsStale(lease, staleGraceMs) {
159
159
  return true;
160
160
  return Date.now() > expiresAt + staleGraceMs;
161
161
  }
162
+ /**
163
+ * Classifies a write-lock directory for maintenance tools. "active" leases
164
+ * must never be removed; "stale" (expired past TTL+grace) and "unreadable"
165
+ * (no/invalid lease.json) are safe repair targets.
166
+ */
167
+ export function classifyStoreWriteLockDir(lockPath, staleGraceMs = DEFAULT_STALE_GRACE_MS) {
168
+ const lease = readLeaseRecord(lockPath);
169
+ if (!lease)
170
+ return "unreadable";
171
+ return leaseIsStale(lease, staleGraceMs) ? "stale" : "active";
172
+ }
162
173
  function canRecoverStaleLease(lease, staleGraceMs) {
163
174
  if (!leaseIsStale(lease, staleGraceMs))
164
175
  return false;
@@ -14,6 +14,14 @@ export interface TodoStateFile {
14
14
  order: string[];
15
15
  nodes: Record<string, TodoNode>;
16
16
  }
17
+ export interface TodoAppendResult {
18
+ ok: boolean;
19
+ path: string;
20
+ todo_path: string;
21
+ node_id?: string;
22
+ section?: string;
23
+ error?: string;
24
+ }
17
25
  export interface TodoTransitionResult {
18
26
  ok: boolean;
19
27
  path: string;
@@ -38,4 +46,17 @@ export declare function syncTodoStateSafe(todoContent?: string): Promise<{
38
46
  export declare function readTodoState(): TodoStateFile;
39
47
  export declare function transitionTodoNode(nodeId: string, targetStatus: TodoNodeStatus): TodoTransitionResult;
40
48
  export declare function transitionTodoNodeSafe(nodeId: string, targetStatus: TodoNodeStatus): Promise<TodoTransitionResult>;
49
+ /**
50
+ * Atomically append a new TODO item under a section, without requiring the
51
+ * caller to read the whole board and resubmit it via update_todo(new_content).
52
+ * That read-modify-write dance is what let concurrent agents silently clobber
53
+ * each other's appended items: the read and write are separate MCP calls, so
54
+ * a lock around the write alone cannot close the race. Here the read, insert,
55
+ * and write all happen inside one TODO_MARKDOWN_REL lock acquisition.
56
+ */
57
+ export declare function appendTodoItemSafe(title: string, options?: {
58
+ section?: string;
59
+ depends_on?: string[];
60
+ id?: string;
61
+ }): Promise<TodoAppendResult>;
41
62
  //# sourceMappingURL=todo-state.d.ts.map