@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.
- package/CHANGELOG.md +78 -0
- package/README.md +72 -532
- package/assets/agent-state/INTERFACE_REGISTRY.md +2 -2
- package/assets/agent-state/MODULES/gates/gate-autonomy.json +4 -2
- package/assets/agent-state/MODULES/gates/gate-completeness.json +4 -2
- package/assets/agent-state/MODULES/gates/gate-operability.json +4 -2
- package/assets/agent-state/MODULES/gates/gate-security.json +3 -1
- package/assets/agent-state/MODULES/registry.json +1 -4
- package/assets/agent-state/MODULES/roles/capability-build.json +1 -2
- package/assets/agent-state/MODULES/roles/capability-eval.json +1 -1
- package/assets/agent-state/MODULES/roles/capability-qa.json +1 -2
- package/assets/agent-state/QUALITY_GATES.md +25 -9
- package/assets/agent-state/TEAL_CONFIG.md +2 -11
- package/assets/eval-battery/battery.json +85 -0
- package/assets/eval-battery/fixtures/edit-target.js +3 -0
- package/assets/scripts/render-mcp-configs.sh +12 -0
- package/dist/ace-context.d.ts +18 -0
- package/dist/ace-context.js +161 -16
- package/dist/ace-server-instructions.js +36 -9
- package/dist/astgrep-index.d.ts +7 -1
- package/dist/astgrep-index.js +66 -39
- package/dist/bridge-trace.d.ts +17 -0
- package/dist/bridge-trace.js +23 -0
- package/dist/cli.d.ts +9 -1
- package/dist/cli.js +619 -10
- package/dist/discovery-runtime-wrappers.d.ts +16 -1
- package/dist/discovery-runtime-wrappers.js +65 -9
- package/dist/eval-harness.d.ts +110 -0
- package/dist/eval-harness.js +254 -0
- package/dist/experience-reflector.d.ts +13 -0
- package/dist/experience-reflector.js +62 -0
- package/dist/gate-contract.d.ts +63 -0
- package/dist/gate-contract.js +178 -0
- package/dist/goal-engine.d.ts +155 -0
- package/dist/goal-engine.js +795 -0
- package/dist/helpers/store-resolution.js +40 -39
- package/dist/hermes/session-manager.d.ts +10 -0
- package/dist/hermes/session-manager.js +130 -27
- package/dist/job-scheduler.d.ts +58 -0
- package/dist/job-scheduler.js +202 -2
- package/dist/job-worker.d.ts +87 -0
- package/dist/job-worker.js +182 -0
- package/dist/local-model-policy.d.ts +8 -2
- package/dist/local-model-policy.js +31 -17
- package/dist/local-model-runtime.d.ts +6 -0
- package/dist/local-model-runtime.js +130 -2
- package/dist/measured-classing.d.ts +66 -0
- package/dist/measured-classing.js +165 -0
- package/dist/model-bridge.d.ts +45 -0
- package/dist/model-bridge.js +502 -44
- package/dist/orchestrator-supervisor.js +1 -1
- package/dist/plan-proposal.d.ts +7 -0
- package/dist/plan-proposal.js +74 -4
- package/dist/playbook-adaptation.d.ts +37 -0
- package/dist/playbook-adaptation.js +159 -0
- package/dist/playbook-curator.d.ts +16 -0
- package/dist/playbook-curator.js +106 -0
- package/dist/playbook-llm-reflector.d.ts +26 -0
- package/dist/playbook-llm-reflector.js +157 -0
- package/dist/public-surface.d.ts +2 -0
- package/dist/public-surface.js +26 -14
- package/dist/runtime-executor.js +45 -2
- package/dist/runtime-tool-specs.d.ts +8 -1
- package/dist/runtime-tool-specs.js +19 -1
- package/dist/safe-edit.d.ts +20 -0
- package/dist/safe-edit.js +355 -78
- package/dist/schemas.js +16 -15
- package/dist/server.d.ts +11 -2
- package/dist/server.js +55 -5
- package/dist/shared.d.ts +12 -0
- package/dist/shared.js +13 -1
- package/dist/source-mutation-coordinator.d.ts +54 -0
- package/dist/source-mutation-coordinator.js +259 -0
- package/dist/status-events.d.ts +4 -1
- package/dist/status-events.js +41 -6
- package/dist/store/ace-packed-store.d.ts +28 -9
- package/dist/store/ace-packed-store.js +324 -57
- package/dist/store/bgzf.d.ts +4 -0
- package/dist/store/bgzf.js +94 -0
- package/dist/store/bootstrap-store.d.ts +4 -1
- package/dist/store/bootstrap-store.js +87 -9
- package/dist/store/cache-workspace.d.ts +8 -0
- package/dist/store/cache-workspace.js +67 -1
- package/dist/store/crc32.d.ts +2 -0
- package/dist/store/crc32.js +16 -0
- package/dist/store/fsck.d.ts +16 -0
- package/dist/store/fsck.js +240 -0
- package/dist/store/gate-contract-migration.d.ts +10 -0
- package/dist/store/gate-contract-migration.js +413 -0
- package/dist/store/materializers/context-snapshot-materializer.d.ts +1 -0
- package/dist/store/materializers/context-snapshot-materializer.js +4 -1
- package/dist/store/materializers/projection-manager.d.ts +7 -0
- package/dist/store/materializers/projection-manager.js +37 -0
- package/dist/store/materializers/scheduler-projection-materializer.js +10 -3
- package/dist/store/repositories/goal-repository.d.ts +112 -0
- package/dist/store/repositories/goal-repository.js +157 -0
- package/dist/store/repositories/playbook-repository.d.ts +107 -0
- package/dist/store/repositories/playbook-repository.js +314 -0
- package/dist/store/store-artifacts.js +44 -2
- package/dist/store/store-authority-audit.js +1 -0
- package/dist/store/store-cli.d.ts +30 -0
- package/dist/store/store-cli.js +242 -0
- package/dist/store/store-snapshot.d.ts +12 -0
- package/dist/store/store-snapshot.js +74 -11
- package/dist/store/types.d.ts +1 -0
- package/dist/store/write-coordinator.d.ts +7 -0
- package/dist/store/write-coordinator.js +11 -0
- package/dist/todo-state.d.ts +21 -0
- package/dist/todo-state.js +150 -53
- package/dist/tools-agent.js +86 -22
- package/dist/tools-discovery.js +5 -3
- package/dist/tools-files.js +107 -23
- package/dist/tools-framework.js +146 -40
- package/dist/tools-goal.d.ts +11 -0
- package/dist/tools-goal.js +108 -0
- package/dist/tools-handoff.js +7 -11
- package/dist/tools-lifecycle.js +2 -0
- package/dist/tools-scheduler.js +44 -1
- package/dist/tools-skills.js +3 -2
- package/dist/tools-todo.js +64 -3
- package/dist/tools.js +2 -0
- package/dist/tui/chat.js +4 -0
- package/dist/tui/index.js +4 -1
- package/dist/tui/ollama.d.ts +2 -0
- package/dist/tui/openai-compatible.d.ts +14 -0
- package/dist/tui/openai-compatible.js +15 -2
- package/dist/tui/provider-discovery.d.ts +1 -0
- package/dist/tui/provider-discovery.js +2 -2
- package/docs/cli-reference.md +125 -0
- package/docs/development.md +46 -0
- package/docs/evaluation-safety.md +27 -0
- package/docs/experience-playbook.md +35 -0
- package/docs/hermes.md +129 -0
- package/docs/library-api.md +31 -0
- package/docs/mcp-tool-reference.md +290 -0
- package/docs/provider-security.md +85 -0
- package/docs/state-model.md +113 -0
- package/docs/troubleshooting.md +95 -0
- package/docs/tui-operator-guide.md +75 -0
- package/package.json +5 -1
- package/scripts/dev/run_hermes_crispr_live.mjs +212 -0
- package/assets/agent-state/MODULES/gates/gate-correctness.json +0 -7
- package/assets/agent-state/MODULES/gates/gate-evaluation.json +0 -7
- 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
|
-
|
|
43
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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)
|
package/dist/store/types.d.ts
CHANGED
|
@@ -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;
|
package/dist/todo-state.d.ts
CHANGED
|
@@ -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
|