@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
|
@@ -15,13 +15,14 @@
|
|
|
15
15
|
* 9. Materialize hook context snapshot
|
|
16
16
|
* 10. Write tasks/todo.md starter
|
|
17
17
|
*/
|
|
18
|
-
import { mkdirSync, existsSync } from "fs";
|
|
18
|
+
import { mkdirSync, existsSync, writeFileSync } from "fs";
|
|
19
19
|
import { join, dirname, resolve } from "path";
|
|
20
20
|
import { fileURLToPath } from "url";
|
|
21
21
|
import { openStore } from "./ace-packed-store.js";
|
|
22
22
|
import { bakeAllCoreKnowledge } from "./knowledge-bake.js";
|
|
23
23
|
import { bakeTopology } from "./topology-bake.js";
|
|
24
24
|
import { buildCatalog } from "./catalog-builder.js";
|
|
25
|
+
import { migratePackageGateContract, recordPackageGateContractVersion, } from "./gate-contract-migration.js";
|
|
25
26
|
import { HostFileMaterializer } from "./materializers/host-file-materializer.js";
|
|
26
27
|
import { ProjectionManager } from "./materializers/projection-manager.js";
|
|
27
28
|
import { TodoSyncer } from "./materializers/todo-syncer.js";
|
|
@@ -104,6 +105,11 @@ async function materializeStoreSurfaces(store, workspaceRoot, opts) {
|
|
|
104
105
|
}
|
|
105
106
|
// Artifacts that receive live timestamp substitution on first seed.
|
|
106
107
|
const TIMESTAMPED_ARTIFACT_RELS = new Set(["agent-state/STATUS.md", "agent-state/EVIDENCE_LOG.md"]);
|
|
108
|
+
const GATE_CONTRACT_OPERATIONAL_ARTIFACT_RELS = [
|
|
109
|
+
"agent-state/TEAL_CONFIG.md",
|
|
110
|
+
"agent-state/QUALITY_GATES.md",
|
|
111
|
+
"agent-state/INTERFACE_REGISTRY.md",
|
|
112
|
+
];
|
|
107
113
|
async function seedOperationalArtifacts(store) {
|
|
108
114
|
const bootstrapTs = new Date().toISOString();
|
|
109
115
|
for (const relPath of OPERATIONAL_ARTIFACT_REL_PATHS) {
|
|
@@ -117,8 +123,23 @@ async function seedOperationalArtifacts(store) {
|
|
|
117
123
|
await store.setBlob(operationalArtifactKey(relPath), content);
|
|
118
124
|
}
|
|
119
125
|
}
|
|
126
|
+
async function syncOperationalArtifacts(store, relPaths) {
|
|
127
|
+
for (const relPath of relPaths) {
|
|
128
|
+
const knowledgeKey = `knowledge/agent-state/${relPath.replace(/^agent-state\//, "")}`;
|
|
129
|
+
const content = (await store.getBlob(knowledgeKey)) ?? "";
|
|
130
|
+
await store.setBlob(operationalArtifactKey(relPath), content);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
async function migrateExistingStoreGateContract(store, workspaceRoot, assetsRoot, warnings) {
|
|
134
|
+
const migration = await migratePackageGateContract(store, workspaceRoot, assetsRoot);
|
|
135
|
+
warnings.push(...migration.messages);
|
|
136
|
+
if (migration.changed) {
|
|
137
|
+
await syncOperationalArtifacts(store, GATE_CONTRACT_OPERATIONAL_ARTIFACT_RELS);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
120
140
|
export async function bootstrapStoreWorkspace(opts) {
|
|
121
141
|
const { workspaceRoot, force = false } = opts;
|
|
142
|
+
const assetsRoot = getAssetsRoot();
|
|
122
143
|
const resolved = await ensureCanonicalWorkspaceStore(workspaceRoot, {
|
|
123
144
|
operationLabel: "bootstrapStoreWorkspace",
|
|
124
145
|
});
|
|
@@ -131,13 +152,14 @@ export async function bootstrapStoreWorkspace(opts) {
|
|
|
131
152
|
return { storePath, agents: 0, modules: { gates: 0, roles: 0, schemas: 0 }, materialized, warnings };
|
|
132
153
|
}
|
|
133
154
|
if (existsSync(storePath) && !force) {
|
|
134
|
-
if (!(opts.includeMcpConfig ?? false) && !(opts.includeClientConfigBundle ?? false) && !opts.llm) {
|
|
135
|
-
warnings.push(`Store already exists at ${storePath}. Use --force to reinitialize, or run 'ace migrate' to import existing state.`);
|
|
136
|
-
return { storePath, agents: 0, modules: { gates: 0, roles: 0, schemas: 0 }, materialized, warnings };
|
|
137
|
-
}
|
|
138
155
|
const existingStore = await openStore(storePath);
|
|
139
156
|
try {
|
|
140
|
-
await
|
|
157
|
+
await migrateExistingStoreGateContract(existingStore, workspaceRoot, assetsRoot, warnings);
|
|
158
|
+
if (!(opts.includeMcpConfig ?? false) && !(opts.includeClientConfigBundle ?? false) && !opts.llm) {
|
|
159
|
+
const agents = (await existingStore.listAgents()).length;
|
|
160
|
+
warnings.push(`Reused existing store at ${storePath}. Refreshed package gate contract state.`);
|
|
161
|
+
return { storePath, agents, modules: { gates: 0, roles: 0, schemas: 0 }, materialized, warnings };
|
|
162
|
+
}
|
|
141
163
|
materialized.push(...(await materializeStoreSurfaces(existingStore, workspaceRoot, opts)));
|
|
142
164
|
const agents = (await existingStore.listAgents()).length;
|
|
143
165
|
warnings.push(`Reused existing store at ${storePath} and refreshed host materializations.`);
|
|
@@ -163,8 +185,8 @@ export async function bootstrapStoreWorkspace(opts) {
|
|
|
163
185
|
include_mcp_config: opts.includeMcpConfig ?? false,
|
|
164
186
|
include_client_config_bundle: opts.includeClientConfigBundle ?? false,
|
|
165
187
|
});
|
|
166
|
-
const assetsRoot = getAssetsRoot();
|
|
167
188
|
const knowledge = await bakeAllCoreKnowledge(store, assetsRoot);
|
|
189
|
+
await recordPackageGateContractVersion(store);
|
|
168
190
|
await bakeTopology(store, assetsRoot);
|
|
169
191
|
await store.setJSON("state/handoffs/__index", []);
|
|
170
192
|
await store.setJSON("state/todo/index", []);
|
|
@@ -197,6 +219,39 @@ export async function bootstrapStoreWorkspace(opts) {
|
|
|
197
219
|
}, { operation_label: "bootstrapStoreWorkspace" });
|
|
198
220
|
}
|
|
199
221
|
// ── ace repair ────────────────────────────────────────────────────────────────
|
|
222
|
+
const GOAL_STORE_KEY_PREFIX = "state/artifacts/agent-state/goals/";
|
|
223
|
+
/**
|
|
224
|
+
* G1 recovery: restore goal records that a pre-fix `ace compact` swept into the
|
|
225
|
+
* store. `GoalRepository` reads plain files, so a swept goal is invisible to the
|
|
226
|
+
* engine even though its bytes survive in ACEPACK. Idempotent: never overwrites
|
|
227
|
+
* a goal file that is already on disk (disk is authoritative for goals).
|
|
228
|
+
*/
|
|
229
|
+
async function restoreSweptGoalState(store, workspaceRoot, warnings) {
|
|
230
|
+
const goalsDir = join(workspaceRoot, "agent-state", "goals");
|
|
231
|
+
const keys = [];
|
|
232
|
+
for await (const key of store.list()) {
|
|
233
|
+
if (key.startsWith(GOAL_STORE_KEY_PREFIX))
|
|
234
|
+
keys.push(key);
|
|
235
|
+
}
|
|
236
|
+
let restored = 0;
|
|
237
|
+
for (const key of keys) {
|
|
238
|
+
const name = key.slice(GOAL_STORE_KEY_PREFIX.length);
|
|
239
|
+
if (!name || name.includes("/"))
|
|
240
|
+
continue;
|
|
241
|
+
const dest = join(goalsDir, name);
|
|
242
|
+
if (existsSync(dest))
|
|
243
|
+
continue;
|
|
244
|
+
const body = await store.getBlob(key);
|
|
245
|
+
if (body === undefined) {
|
|
246
|
+
warnings.push(`goal restore: key ${key} present in index but unreadable`);
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
mkdirSync(goalsDir, { recursive: true });
|
|
250
|
+
writeFileSync(dest, body);
|
|
251
|
+
restored += 1;
|
|
252
|
+
}
|
|
253
|
+
return restored;
|
|
254
|
+
}
|
|
200
255
|
export async function repairWorkspace(workspaceRoot) {
|
|
201
256
|
const resolved = await ensureCanonicalWorkspaceStore(workspaceRoot, {
|
|
202
257
|
operationLabel: "repairWorkspace",
|
|
@@ -215,7 +270,24 @@ export async function repairWorkspace(workspaceRoot) {
|
|
|
215
270
|
const { storePath } = resolved;
|
|
216
271
|
const warnings = [...resolved.warnings];
|
|
217
272
|
const store = await openStore(storePath);
|
|
273
|
+
const assetsRoot = getAssetsRoot();
|
|
218
274
|
try {
|
|
275
|
+
// A pre-fix `ace cache` could have ingested the write-coordinator's lease
|
|
276
|
+
// file as a permanent store key (it scanned .write.lock/ before that
|
|
277
|
+
// path was excluded). Delete it if present — it's transient runtime
|
|
278
|
+
// state, never a durable artifact.
|
|
279
|
+
const pollutedLeaseKeyPrefix = "state/artifacts/agent-state/ace-state.ace.write.lock/";
|
|
280
|
+
for await (const key of store.list()) {
|
|
281
|
+
if (key.startsWith(pollutedLeaseKeyPrefix)) {
|
|
282
|
+
await store.delete(key);
|
|
283
|
+
warnings.push(`Removed stale write-lock lease key ingested by a prior 'ace cache' run: ${key}`);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
const restoredGoals = await restoreSweptGoalState(store, workspaceRoot, warnings);
|
|
287
|
+
if (restoredGoals > 0) {
|
|
288
|
+
warnings.push(`Restored ${restoredGoals} goal file(s) swept into the store by a pre-fix \`ace compact\`.`);
|
|
289
|
+
}
|
|
290
|
+
await migrateExistingStoreGateContract(store, workspaceRoot, assetsRoot, warnings);
|
|
219
291
|
const hostPolicy = (await store.getJSON("meta/host_materialization")) ?? {};
|
|
220
292
|
// Re-materialize host files
|
|
221
293
|
const hostMat = new HostFileMaterializer(store, workspaceRoot);
|
|
@@ -236,7 +308,7 @@ export async function repairWorkspace(workspaceRoot) {
|
|
|
236
308
|
return warnings;
|
|
237
309
|
}
|
|
238
310
|
// ── ace compact ───────────────────────────────────────────────────────────────
|
|
239
|
-
export async function compactWorkspace(workspaceRoot) {
|
|
311
|
+
export async function compactWorkspace(workspaceRoot, options = {}) {
|
|
240
312
|
const resolved = await ensureCanonicalWorkspaceStore(workspaceRoot, {
|
|
241
313
|
operationLabel: "compactWorkspace",
|
|
242
314
|
});
|
|
@@ -254,6 +326,12 @@ export async function compactWorkspace(workspaceRoot) {
|
|
|
254
326
|
await store.close();
|
|
255
327
|
}
|
|
256
328
|
const after = statSync(storePath).size;
|
|
257
|
-
|
|
329
|
+
const sweep = options.sweep === false
|
|
330
|
+
? undefined
|
|
331
|
+
: await (await import("./cache-workspace.js")).cacheWorkspaceArtifacts(workspaceRoot, {
|
|
332
|
+
clean: true,
|
|
333
|
+
minAgeMs: 0,
|
|
334
|
+
});
|
|
335
|
+
return { before, after, sweep };
|
|
258
336
|
}
|
|
259
337
|
//# sourceMappingURL=bootstrap-store.js.map
|
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
export interface CacheWorkspaceOptions {
|
|
2
2
|
dryRun?: boolean;
|
|
3
3
|
clean?: boolean;
|
|
4
|
+
/**
|
|
5
|
+
* Sweep-lite guard: do not ingest or delete files newer than this many
|
|
6
|
+
* milliseconds. This keeps lifecycle-triggered sweeps from racing a host
|
|
7
|
+
* agent that just wrote a loose compatibility file.
|
|
8
|
+
*/
|
|
9
|
+
minAgeMs?: number;
|
|
10
|
+
gcSemanticCache?: boolean;
|
|
4
11
|
}
|
|
5
12
|
export interface CacheWorkspaceResult {
|
|
6
13
|
storePath: string;
|
|
@@ -9,6 +16,7 @@ export interface CacheWorkspaceResult {
|
|
|
9
16
|
scanned_files: number;
|
|
10
17
|
cached_files: number;
|
|
11
18
|
removed_files: number;
|
|
19
|
+
removed_semantic_cache_files: number;
|
|
12
20
|
kept_projected_files: number;
|
|
13
21
|
skipped_files: number;
|
|
14
22
|
warnings: string[];
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { existsSync, readdirSync, readFileSync, rmSync } from "node:fs";
|
|
1
|
+
import { existsSync, readdirSync, readFileSync, rmSync, statSync } from "node:fs";
|
|
2
2
|
import { dirname, isAbsolute, join, relative } from "node:path";
|
|
3
3
|
import { ACE_ROOT_REL, isProjectedAceWorkspacePath, mapAceWorkspaceRelativePath, resolveStoreFallbackKeysForPath, } from "../helpers.js";
|
|
4
4
|
import { normalizeRelPath } from "../shared.js";
|
|
@@ -9,6 +9,10 @@ const SCAN_ROOTS_REL = [
|
|
|
9
9
|
".agents/ACE/agent-state",
|
|
10
10
|
".agents/ACE/tasks",
|
|
11
11
|
".agents/ACE/skills",
|
|
12
|
+
// Root-level tasks/ — home of create_handoff's SWARM_HANDOFF.*.json payloads
|
|
13
|
+
// (see resolveStoreFallbackKeys). Distinct from .agents/ACE/tasks/, which
|
|
14
|
+
// holds the projected todo.md and is scanned separately above.
|
|
15
|
+
"tasks",
|
|
12
16
|
];
|
|
13
17
|
function isInside(base, target) {
|
|
14
18
|
const rel = relative(base, target);
|
|
@@ -49,7 +53,58 @@ function isTextBuffer(input) {
|
|
|
49
53
|
function selectScanRoots(workspaceRoot) {
|
|
50
54
|
return SCAN_ROOTS_REL.map((relPath) => join(workspaceRoot, relPath)).filter((abs) => existsSync(abs));
|
|
51
55
|
}
|
|
56
|
+
function removeSemanticCache(workspaceRoot, minAgeMs, dryRun) {
|
|
57
|
+
const root = join(workspaceRoot, ".tmp", "cache", "semantic");
|
|
58
|
+
if (!existsSync(root))
|
|
59
|
+
return 0;
|
|
60
|
+
const now = Date.now();
|
|
61
|
+
let removed = 0;
|
|
62
|
+
for (const absPath of collectFiles(root)) {
|
|
63
|
+
const stats = statSync(absPath);
|
|
64
|
+
if (now - stats.mtimeMs < minAgeMs)
|
|
65
|
+
continue;
|
|
66
|
+
if (!dryRun) {
|
|
67
|
+
rmSync(absPath, { force: true });
|
|
68
|
+
pruneEmptyParents(absPath, root);
|
|
69
|
+
}
|
|
70
|
+
removed += 1;
|
|
71
|
+
}
|
|
72
|
+
return removed;
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* Paths that must never be scanned/ingested by the sweeper, even though they
|
|
76
|
+
* live under a scanned root: an active write-coordinator lease dir (ingesting
|
|
77
|
+
* it destroys a live lease or pollutes the store with a permanent key for a
|
|
78
|
+
* transient file), temp/rename-in-flight files, and quarantined corrupt-store
|
|
79
|
+
* copies left by M0.5's write guard.
|
|
80
|
+
*/
|
|
81
|
+
function isUnsafeToScan(canonicalPath) {
|
|
82
|
+
return (/(^|\/)ace-state\.ace\.write\.lock(\/|$)/.test(canonicalPath) ||
|
|
83
|
+
/\.tmp$/.test(canonicalPath) ||
|
|
84
|
+
/\.corrupt-\d+$/.test(canonicalPath) ||
|
|
85
|
+
/ace-state\.ace(\.[^/]+)?$/.test(canonicalPath));
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* Engine-owned durable state that lives under a scanned root but is NOT
|
|
89
|
+
* store-managed. `GoalRepository` (src/store/repositories/goal-repository.ts)
|
|
90
|
+
* reads and writes these as plain files via `readFileSync`/`renameSync`;
|
|
91
|
+
* ingesting and deleting them makes every goal unreadable (`Unknown goal_id`)
|
|
92
|
+
* and kills any detached `ace goal run` mid-flight. The sweep must leave them
|
|
93
|
+
* on disk. See REPORT_GOAL_EVAL_PLAYBOOK_MITIGATIONS_2026-07-09.md (G1).
|
|
94
|
+
*/
|
|
95
|
+
const SWEEP_EXCLUDED_DIRS = ["agent-state/goals"];
|
|
96
|
+
function isSweepExcluded(canonicalPath) {
|
|
97
|
+
return SWEEP_EXCLUDED_DIRS.some((dir) => canonicalPath === dir || canonicalPath.startsWith(`${dir}/`));
|
|
98
|
+
}
|
|
52
99
|
function isCacheableAcePath(path) {
|
|
100
|
+
if (isUnsafeToScan(path))
|
|
101
|
+
return false;
|
|
102
|
+
// Sits above the SWARM_HANDOFF early-return: an exclusion must never be
|
|
103
|
+
// overridden by a later allow.
|
|
104
|
+
if (isSweepExcluded(path))
|
|
105
|
+
return false;
|
|
106
|
+
if (/^tasks\/SWARM_HANDOFF\..+\.json$/.test(path))
|
|
107
|
+
return true;
|
|
53
108
|
return path === "agent-state"
|
|
54
109
|
|| path.startsWith("agent-state/")
|
|
55
110
|
|| path.startsWith(`${ACE_ROOT_REL}/`);
|
|
@@ -57,6 +112,8 @@ function isCacheableAcePath(path) {
|
|
|
57
112
|
export async function cacheWorkspaceArtifacts(workspaceRoot, options = {}) {
|
|
58
113
|
const dryRun = options.dryRun ?? false;
|
|
59
114
|
const clean = options.clean ?? true;
|
|
115
|
+
const minAgeMs = options.minAgeMs ?? 0;
|
|
116
|
+
const gcSemanticCache = options.gcSemanticCache ?? true;
|
|
60
117
|
const storePath = getWorkspaceStorePath(workspaceRoot);
|
|
61
118
|
if (!existsSync(storePath)) {
|
|
62
119
|
throw new Error(`No store found at ${storePath}. Run 'ace init' first.`);
|
|
@@ -72,6 +129,11 @@ export async function cacheWorkspaceArtifacts(workspaceRoot, options = {}) {
|
|
|
72
129
|
scanned += 1;
|
|
73
130
|
const relPath = normalizeRelPath(relative(workspaceRoot, absPath));
|
|
74
131
|
const canonicalPath = mapAceWorkspaceRelativePath(relPath);
|
|
132
|
+
const stats = statSync(absPath);
|
|
133
|
+
if (minAgeMs > 0 && Date.now() - stats.mtimeMs < minAgeMs) {
|
|
134
|
+
skipped += 1;
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
75
137
|
if (!isCacheableAcePath(canonicalPath)) {
|
|
76
138
|
skipped += 1;
|
|
77
139
|
continue;
|
|
@@ -129,6 +191,9 @@ export async function cacheWorkspaceArtifacts(workspaceRoot, options = {}) {
|
|
|
129
191
|
pruneEmptyParents(candidate.absPath, pruneRoot);
|
|
130
192
|
}
|
|
131
193
|
}
|
|
194
|
+
const removedSemanticCache = gcSemanticCache
|
|
195
|
+
? removeSemanticCache(workspaceRoot, minAgeMs, dryRun)
|
|
196
|
+
: 0;
|
|
132
197
|
return {
|
|
133
198
|
storePath,
|
|
134
199
|
dry_run: dryRun,
|
|
@@ -136,6 +201,7 @@ export async function cacheWorkspaceArtifacts(workspaceRoot, options = {}) {
|
|
|
136
201
|
scanned_files: scanned,
|
|
137
202
|
cached_files: candidates.length,
|
|
138
203
|
removed_files: removed,
|
|
204
|
+
removed_semantic_cache_files: removedSemanticCache,
|
|
139
205
|
kept_projected_files: keptProjected,
|
|
140
206
|
skipped_files: skipped,
|
|
141
207
|
warnings,
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
const CRC32_TABLE = new Uint32Array(256);
|
|
2
|
+
for (let i = 0; i < CRC32_TABLE.length; i += 1) {
|
|
3
|
+
let c = i;
|
|
4
|
+
for (let bit = 0; bit < 8; bit += 1) {
|
|
5
|
+
c = (c & 1) ? (0xedb88320 ^ (c >>> 1)) : (c >>> 1);
|
|
6
|
+
}
|
|
7
|
+
CRC32_TABLE[i] = c >>> 0;
|
|
8
|
+
}
|
|
9
|
+
export function crc32(bytes) {
|
|
10
|
+
let crc = 0xffffffff;
|
|
11
|
+
for (const byte of bytes) {
|
|
12
|
+
crc = CRC32_TABLE[(crc ^ byte) & 0xff] ^ (crc >>> 8);
|
|
13
|
+
}
|
|
14
|
+
return (crc ^ 0xffffffff) >>> 0;
|
|
15
|
+
}
|
|
16
|
+
//# sourceMappingURL=crc32.js.map
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export interface StoreFsckCheck {
|
|
2
|
+
name: string;
|
|
3
|
+
ok: boolean;
|
|
4
|
+
detail: string;
|
|
5
|
+
}
|
|
6
|
+
export interface StoreFsckResult {
|
|
7
|
+
ok: boolean;
|
|
8
|
+
repaired: boolean;
|
|
9
|
+
storePath: string;
|
|
10
|
+
checks: StoreFsckCheck[];
|
|
11
|
+
repaired_paths: string[];
|
|
12
|
+
}
|
|
13
|
+
export declare function fsckWorkspaceStore(workspaceRoot: string, options?: {
|
|
14
|
+
repair?: boolean;
|
|
15
|
+
}): StoreFsckResult;
|
|
16
|
+
//# sourceMappingURL=fsck.d.ts.map
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
import { existsSync, readdirSync, readFileSync, rmSync } from "node:fs";
|
|
2
|
+
import { basename, isAbsolute, join, relative } from "node:path";
|
|
3
|
+
import { HEADER_SIZE, HEADER_SIZE_V1, MAGIC } from "./types.js";
|
|
4
|
+
import { getCanonicalWorkspaceStorePath, getLegacyWorkspaceStorePath, getWorkspaceStorePath } from "./store-snapshot.js";
|
|
5
|
+
import { crc32 } from "./crc32.js";
|
|
6
|
+
import { classifyStoreWriteLockDir } from "./write-coordinator.js";
|
|
7
|
+
const DECODE = new TextDecoder();
|
|
8
|
+
function u32(bytes, offset) {
|
|
9
|
+
return new DataView(bytes.buffer, bytes.byteOffset).getUint32(offset, false);
|
|
10
|
+
}
|
|
11
|
+
function u64(bytes, offset) {
|
|
12
|
+
const view = new DataView(bytes.buffer, bytes.byteOffset);
|
|
13
|
+
return view.getUint32(offset, false) * 2 ** 32 + view.getUint32(offset + 4, false);
|
|
14
|
+
}
|
|
15
|
+
function minEventSectionLength(count) {
|
|
16
|
+
const pad = (4 - (count % 4)) % 4;
|
|
17
|
+
return 4 + 8 * count + count + count + count + pad + 4 * count + 4 * count + 4;
|
|
18
|
+
}
|
|
19
|
+
function isInside(base, target) {
|
|
20
|
+
const rel = relative(base, target);
|
|
21
|
+
return rel === "" || (!rel.startsWith("..") && !isAbsolute(rel));
|
|
22
|
+
}
|
|
23
|
+
function collectOrphans(root) {
|
|
24
|
+
const stateRoot = join(root, "agent-state");
|
|
25
|
+
if (!existsSync(stateRoot))
|
|
26
|
+
return [];
|
|
27
|
+
const found = [];
|
|
28
|
+
const walk = (dir) => {
|
|
29
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
30
|
+
const abs = join(dir, entry.name);
|
|
31
|
+
const rel = relative(root, abs);
|
|
32
|
+
if (/(^|\/)ace-state\.ace\.write\.lock(\/|$)/.test(rel)) {
|
|
33
|
+
found.push({ path: abs, kind: "lock" });
|
|
34
|
+
if (entry.isDirectory())
|
|
35
|
+
continue;
|
|
36
|
+
}
|
|
37
|
+
else if (/\.tmp$/.test(entry.name)) {
|
|
38
|
+
found.push({ path: abs, kind: "tmp" });
|
|
39
|
+
if (entry.isDirectory())
|
|
40
|
+
continue;
|
|
41
|
+
}
|
|
42
|
+
else if (/\.corrupt-\d+$/.test(entry.name)) {
|
|
43
|
+
found.push({ path: abs, kind: "corrupt" });
|
|
44
|
+
if (entry.isDirectory())
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
if (entry.isDirectory())
|
|
48
|
+
walk(abs);
|
|
49
|
+
}
|
|
50
|
+
};
|
|
51
|
+
walk(stateRoot);
|
|
52
|
+
return found;
|
|
53
|
+
}
|
|
54
|
+
function validateEventSection(bytes) {
|
|
55
|
+
if (bytes.length < 4)
|
|
56
|
+
return "event section shorter than count field";
|
|
57
|
+
const count = u32(bytes, 0);
|
|
58
|
+
const min = minEventSectionLength(count);
|
|
59
|
+
if (bytes.length < min)
|
|
60
|
+
return `event section count=${count} requires >= ${min} bytes, got ${bytes.length}`;
|
|
61
|
+
let pos = 4 + 8 * count + count + count + count + ((4 - (count % 4)) % 4);
|
|
62
|
+
const offsets = [];
|
|
63
|
+
const lengths = [];
|
|
64
|
+
for (let i = 0; i < count; i += 1) {
|
|
65
|
+
offsets.push(u32(bytes, pos));
|
|
66
|
+
pos += 4;
|
|
67
|
+
}
|
|
68
|
+
for (let i = 0; i < count; i += 1) {
|
|
69
|
+
lengths.push(u32(bytes, pos));
|
|
70
|
+
pos += 4;
|
|
71
|
+
}
|
|
72
|
+
const poolLength = u32(bytes, pos);
|
|
73
|
+
pos += 4;
|
|
74
|
+
if (bytes.length < pos + poolLength) {
|
|
75
|
+
return `event payload pool truncated (declared=${poolLength}, available=${bytes.length - pos})`;
|
|
76
|
+
}
|
|
77
|
+
for (let i = 0; i < count; i += 1) {
|
|
78
|
+
if (offsets[i] + lengths[i] > poolLength) {
|
|
79
|
+
return `event ${i} payload exceeds pool bounds (offset=${offsets[i]}, length=${lengths[i]}, pool=${poolLength})`;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
84
|
+
export function fsckWorkspaceStore(workspaceRoot, options = {}) {
|
|
85
|
+
const storePath = getWorkspaceStorePath(workspaceRoot);
|
|
86
|
+
const checks = [];
|
|
87
|
+
const repairedPaths = [];
|
|
88
|
+
const canonicalPath = getCanonicalWorkspaceStorePath(workspaceRoot);
|
|
89
|
+
const legacyPath = getLegacyWorkspaceStorePath(workspaceRoot);
|
|
90
|
+
checks.push({
|
|
91
|
+
name: "store:exists",
|
|
92
|
+
ok: existsSync(storePath),
|
|
93
|
+
detail: existsSync(storePath) ? storePath : `missing: ${storePath}`,
|
|
94
|
+
});
|
|
95
|
+
checks.push({
|
|
96
|
+
name: "store:legacy-dual-store",
|
|
97
|
+
ok: !(existsSync(canonicalPath) && existsSync(legacyPath)),
|
|
98
|
+
detail: existsSync(canonicalPath) && existsSync(legacyPath)
|
|
99
|
+
? `both canonical and legacy stores exist (${canonicalPath}, ${legacyPath})`
|
|
100
|
+
: "no dual-store conflict",
|
|
101
|
+
});
|
|
102
|
+
if (existsSync(storePath)) {
|
|
103
|
+
try {
|
|
104
|
+
const file = readFileSync(storePath);
|
|
105
|
+
checks.push({
|
|
106
|
+
name: "header:size",
|
|
107
|
+
ok: file.length >= HEADER_SIZE,
|
|
108
|
+
detail: `${file.length} bytes`,
|
|
109
|
+
});
|
|
110
|
+
if (file.length >= HEADER_SIZE_V1) {
|
|
111
|
+
const magic = DECODE.decode(file.subarray(0, 8));
|
|
112
|
+
const version = u32(file, 8);
|
|
113
|
+
const kvIndexOffset = u64(file, 16);
|
|
114
|
+
const kvIndexLength = u64(file, 24);
|
|
115
|
+
const kvChunkEnd = version >= 2 ? u64(file, 32) : kvIndexOffset;
|
|
116
|
+
const evtOffset = version >= 2 ? u64(file, 40) : 0;
|
|
117
|
+
const evtLength = version >= 2 ? u64(file, 48) : 0;
|
|
118
|
+
const evtCount = version >= 2 ? u32(file, 56) : 0;
|
|
119
|
+
checks.push({ name: "header:magic", ok: magic === MAGIC, detail: magic });
|
|
120
|
+
checks.push({ name: "header:version", ok: version >= 1 && version <= 2, detail: String(version) });
|
|
121
|
+
const headerOrderOk = version >= 2
|
|
122
|
+
? HEADER_SIZE <= kvChunkEnd &&
|
|
123
|
+
kvChunkEnd <= evtOffset &&
|
|
124
|
+
evtOffset <= kvIndexOffset &&
|
|
125
|
+
kvIndexOffset + kvIndexLength <= file.length
|
|
126
|
+
: HEADER_SIZE_V1 <= kvIndexOffset && kvIndexOffset + kvIndexLength <= file.length;
|
|
127
|
+
checks.push({
|
|
128
|
+
name: "header:regions",
|
|
129
|
+
ok: headerOrderOk,
|
|
130
|
+
detail: `kvChunkEnd=${kvChunkEnd}, evtOffset=${evtOffset}, evtLength=${evtLength}, kvIndexOffset=${kvIndexOffset}, kvIndexLength=${kvIndexLength}, fileLength=${file.length}`,
|
|
131
|
+
});
|
|
132
|
+
if (headerOrderOk) {
|
|
133
|
+
const indexRaw = file.subarray(kvIndexOffset, kvIndexOffset + kvIndexLength);
|
|
134
|
+
let index;
|
|
135
|
+
try {
|
|
136
|
+
index = JSON.parse(DECODE.decode(indexRaw));
|
|
137
|
+
checks.push({ name: "index:parse", ok: true, detail: `${Object.keys(index).length} entries` });
|
|
138
|
+
}
|
|
139
|
+
catch (error) {
|
|
140
|
+
checks.push({ name: "index:parse", ok: false, detail: error instanceof Error ? error.message : String(error) });
|
|
141
|
+
}
|
|
142
|
+
if (index) {
|
|
143
|
+
const minChunkOffset = version >= 2 ? HEADER_SIZE : HEADER_SIZE_V1;
|
|
144
|
+
const maxChunkEnd = version >= 2 ? kvChunkEnd : kvIndexOffset;
|
|
145
|
+
for (const [key, entry] of Object.entries(index)) {
|
|
146
|
+
const boundsOk = Number.isFinite(entry.offset) &&
|
|
147
|
+
Number.isFinite(entry.length) &&
|
|
148
|
+
entry.offset >= minChunkOffset &&
|
|
149
|
+
entry.offset + 4 + entry.length <= maxChunkEnd;
|
|
150
|
+
checks.push({
|
|
151
|
+
name: `chunk:${key}:bounds`,
|
|
152
|
+
ok: boundsOk,
|
|
153
|
+
detail: `offset=${entry.offset}, length=${entry.length}, maxChunkEnd=${maxChunkEnd}`,
|
|
154
|
+
});
|
|
155
|
+
if (!boundsOk)
|
|
156
|
+
continue;
|
|
157
|
+
const prefix = u32(file, entry.offset);
|
|
158
|
+
checks.push({
|
|
159
|
+
name: `chunk:${key}:length-prefix`,
|
|
160
|
+
ok: prefix === entry.length,
|
|
161
|
+
detail: `prefix=${prefix}, index=${entry.length}`,
|
|
162
|
+
});
|
|
163
|
+
if (typeof entry.crc === "number") {
|
|
164
|
+
const chunk = file.subarray(entry.offset + 4, entry.offset + 4 + entry.length);
|
|
165
|
+
const actual = crc32(chunk);
|
|
166
|
+
checks.push({
|
|
167
|
+
name: `chunk:${key}:crc32`,
|
|
168
|
+
ok: actual === entry.crc,
|
|
169
|
+
detail: `expected=${entry.crc}, actual=${actual}`,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
if (version >= 2) {
|
|
175
|
+
const eventError = validateEventSection(file.subarray(evtOffset, evtOffset + evtLength));
|
|
176
|
+
checks.push({
|
|
177
|
+
name: "events:section",
|
|
178
|
+
ok: !eventError,
|
|
179
|
+
detail: eventError ?? `count=${evtCount}, length=${evtLength}`,
|
|
180
|
+
});
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
catch (error) {
|
|
186
|
+
checks.push({
|
|
187
|
+
name: "store:read",
|
|
188
|
+
ok: false,
|
|
189
|
+
detail: error instanceof Error ? error.message : String(error),
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
const allOrphans = collectOrphans(workspaceRoot);
|
|
194
|
+
const failingOrphans = [];
|
|
195
|
+
for (const orphan of allOrphans) {
|
|
196
|
+
if (orphan.kind !== "lock") {
|
|
197
|
+
failingOrphans.push(orphan);
|
|
198
|
+
continue;
|
|
199
|
+
}
|
|
200
|
+
const classification = classifyStoreWriteLockDir(orphan.path);
|
|
201
|
+
if (classification === "active") {
|
|
202
|
+
checks.push({
|
|
203
|
+
name: "workspace:write-lock",
|
|
204
|
+
ok: true,
|
|
205
|
+
detail: `active write lease (holder pid in lease.json) — not an orphan: ${relative(workspaceRoot, orphan.path)}`,
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
else {
|
|
209
|
+
failingOrphans.push(orphan);
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
checks.push({
|
|
213
|
+
name: "workspace:orphan-store-artifacts",
|
|
214
|
+
ok: failingOrphans.length === 0,
|
|
215
|
+
detail: failingOrphans.length === 0
|
|
216
|
+
? "none"
|
|
217
|
+
: failingOrphans.map((o) => relative(workspaceRoot, o.path)).join(", "),
|
|
218
|
+
});
|
|
219
|
+
if (options.repair) {
|
|
220
|
+
for (const orphan of failingOrphans) {
|
|
221
|
+
const rel = relative(workspaceRoot, orphan.path);
|
|
222
|
+
if (/\.corrupt-\d+$/.test(basename(orphan.path)))
|
|
223
|
+
continue;
|
|
224
|
+
if (!isInside(workspaceRoot, orphan.path))
|
|
225
|
+
continue;
|
|
226
|
+
if (orphan.kind === "lock" && classifyStoreWriteLockDir(orphan.path) === "active")
|
|
227
|
+
continue;
|
|
228
|
+
rmSync(orphan.path, { recursive: true, force: true });
|
|
229
|
+
repairedPaths.push(rel);
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
return {
|
|
233
|
+
ok: checks.every((check) => check.ok),
|
|
234
|
+
repaired: repairedPaths.length > 0,
|
|
235
|
+
storePath,
|
|
236
|
+
checks,
|
|
237
|
+
repaired_paths: repairedPaths,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
//# sourceMappingURL=fsck.js.map
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { IAcePackedStore } from "./types.js";
|
|
2
|
+
export declare const PACKAGE_GATE_CONTRACT_VERSION = 2;
|
|
3
|
+
export declare const PACKAGE_GATE_CONTRACT_VERSION_KEY = "meta/package_gate_contract_version";
|
|
4
|
+
export interface GateContractMigrationResult {
|
|
5
|
+
changed: boolean;
|
|
6
|
+
messages: string[];
|
|
7
|
+
}
|
|
8
|
+
export declare function recordPackageGateContractVersion(store: IAcePackedStore): Promise<void>;
|
|
9
|
+
export declare function migratePackageGateContract(store: IAcePackedStore, workspaceRoot: string, assetsRoot: string): Promise<GateContractMigrationResult>;
|
|
10
|
+
//# sourceMappingURL=gate-contract-migration.d.ts.map
|