@voybio/ace-swarm 2.4.4 → 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 (120) hide show
  1. package/CHANGELOG.md +69 -0
  2. package/README.md +72 -532
  3. package/assets/eval-battery/battery.json +85 -0
  4. package/assets/eval-battery/fixtures/edit-target.js +3 -0
  5. package/assets/scripts/render-mcp-configs.sh +12 -0
  6. package/dist/ace-context.d.ts +18 -0
  7. package/dist/ace-context.js +161 -16
  8. package/dist/ace-server-instructions.js +36 -9
  9. package/dist/bridge-trace.d.ts +17 -0
  10. package/dist/bridge-trace.js +23 -0
  11. package/dist/cli.d.ts +9 -1
  12. package/dist/cli.js +619 -10
  13. package/dist/discovery-runtime-wrappers.d.ts +16 -1
  14. package/dist/discovery-runtime-wrappers.js +65 -9
  15. package/dist/eval-harness.d.ts +110 -0
  16. package/dist/eval-harness.js +254 -0
  17. package/dist/experience-reflector.d.ts +13 -0
  18. package/dist/experience-reflector.js +62 -0
  19. package/dist/goal-engine.d.ts +155 -0
  20. package/dist/goal-engine.js +795 -0
  21. package/dist/helpers/store-resolution.js +40 -39
  22. package/dist/hermes/session-manager.d.ts +10 -0
  23. package/dist/hermes/session-manager.js +130 -27
  24. package/dist/job-scheduler.d.ts +58 -0
  25. package/dist/job-scheduler.js +202 -2
  26. package/dist/job-worker.d.ts +87 -0
  27. package/dist/job-worker.js +182 -0
  28. package/dist/local-model-policy.d.ts +8 -2
  29. package/dist/local-model-policy.js +31 -17
  30. package/dist/local-model-runtime.d.ts +6 -0
  31. package/dist/local-model-runtime.js +130 -2
  32. package/dist/measured-classing.d.ts +66 -0
  33. package/dist/measured-classing.js +165 -0
  34. package/dist/model-bridge.d.ts +45 -0
  35. package/dist/model-bridge.js +502 -44
  36. package/dist/orchestrator-supervisor.js +1 -1
  37. package/dist/plan-proposal.d.ts +7 -0
  38. package/dist/plan-proposal.js +74 -4
  39. package/dist/playbook-adaptation.d.ts +37 -0
  40. package/dist/playbook-adaptation.js +159 -0
  41. package/dist/playbook-curator.d.ts +16 -0
  42. package/dist/playbook-curator.js +106 -0
  43. package/dist/playbook-llm-reflector.d.ts +26 -0
  44. package/dist/playbook-llm-reflector.js +157 -0
  45. package/dist/public-surface.d.ts +2 -0
  46. package/dist/public-surface.js +26 -14
  47. package/dist/safe-edit.d.ts +20 -0
  48. package/dist/safe-edit.js +355 -78
  49. package/dist/server.d.ts +11 -2
  50. package/dist/server.js +55 -5
  51. package/dist/shared.d.ts +12 -0
  52. package/dist/shared.js +13 -1
  53. package/dist/source-mutation-coordinator.d.ts +54 -0
  54. package/dist/source-mutation-coordinator.js +259 -0
  55. package/dist/status-events.d.ts +4 -1
  56. package/dist/status-events.js +41 -6
  57. package/dist/store/ace-packed-store.d.ts +28 -9
  58. package/dist/store/ace-packed-store.js +324 -57
  59. package/dist/store/bgzf.d.ts +4 -0
  60. package/dist/store/bgzf.js +94 -0
  61. package/dist/store/bootstrap-store.d.ts +4 -1
  62. package/dist/store/bootstrap-store.js +57 -3
  63. package/dist/store/cache-workspace.d.ts +8 -0
  64. package/dist/store/cache-workspace.js +67 -1
  65. package/dist/store/crc32.d.ts +2 -0
  66. package/dist/store/crc32.js +16 -0
  67. package/dist/store/fsck.d.ts +16 -0
  68. package/dist/store/fsck.js +240 -0
  69. package/dist/store/materializers/context-snapshot-materializer.d.ts +1 -0
  70. package/dist/store/materializers/context-snapshot-materializer.js +4 -1
  71. package/dist/store/materializers/projection-manager.d.ts +7 -0
  72. package/dist/store/materializers/projection-manager.js +37 -0
  73. package/dist/store/materializers/scheduler-projection-materializer.js +10 -3
  74. package/dist/store/repositories/goal-repository.d.ts +112 -0
  75. package/dist/store/repositories/goal-repository.js +157 -0
  76. package/dist/store/repositories/playbook-repository.d.ts +107 -0
  77. package/dist/store/repositories/playbook-repository.js +314 -0
  78. package/dist/store/store-artifacts.js +44 -2
  79. package/dist/store/store-authority-audit.js +1 -0
  80. package/dist/store/store-cli.d.ts +30 -0
  81. package/dist/store/store-cli.js +242 -0
  82. package/dist/store/store-snapshot.d.ts +12 -0
  83. package/dist/store/store-snapshot.js +74 -11
  84. package/dist/store/types.d.ts +1 -0
  85. package/dist/store/write-coordinator.d.ts +7 -0
  86. package/dist/store/write-coordinator.js +11 -0
  87. package/dist/todo-state.d.ts +21 -0
  88. package/dist/todo-state.js +150 -53
  89. package/dist/tools-agent.js +86 -22
  90. package/dist/tools-discovery.js +5 -3
  91. package/dist/tools-files.js +39 -18
  92. package/dist/tools-framework.js +31 -3
  93. package/dist/tools-goal.d.ts +11 -0
  94. package/dist/tools-goal.js +108 -0
  95. package/dist/tools-handoff.js +7 -11
  96. package/dist/tools-lifecycle.js +2 -0
  97. package/dist/tools-scheduler.js +44 -1
  98. package/dist/tools-skills.js +3 -2
  99. package/dist/tools-todo.js +64 -3
  100. package/dist/tools.js +2 -0
  101. package/dist/tui/chat.js +4 -0
  102. package/dist/tui/index.js +4 -1
  103. package/dist/tui/ollama.d.ts +2 -0
  104. package/dist/tui/openai-compatible.d.ts +14 -0
  105. package/dist/tui/openai-compatible.js +15 -2
  106. package/dist/tui/provider-discovery.d.ts +1 -0
  107. package/dist/tui/provider-discovery.js +2 -2
  108. package/docs/cli-reference.md +125 -0
  109. package/docs/development.md +46 -0
  110. package/docs/evaluation-safety.md +27 -0
  111. package/docs/experience-playbook.md +35 -0
  112. package/docs/hermes.md +129 -0
  113. package/docs/library-api.md +31 -0
  114. package/docs/mcp-tool-reference.md +290 -0
  115. package/docs/provider-security.md +85 -0
  116. package/docs/state-model.md +113 -0
  117. package/docs/troubleshooting.md +95 -0
  118. package/docs/tui-operator-guide.md +75 -0
  119. package/package.json +3 -1
  120. package/scripts/dev/run_hermes_crispr_live.mjs +212 -0
@@ -15,7 +15,7 @@
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";
@@ -219,6 +219,39 @@ export async function bootstrapStoreWorkspace(opts) {
219
219
  }, { operation_label: "bootstrapStoreWorkspace" });
220
220
  }
221
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
+ }
222
255
  export async function repairWorkspace(workspaceRoot) {
223
256
  const resolved = await ensureCanonicalWorkspaceStore(workspaceRoot, {
224
257
  operationLabel: "repairWorkspace",
@@ -239,6 +272,21 @@ export async function repairWorkspace(workspaceRoot) {
239
272
  const store = await openStore(storePath);
240
273
  const assetsRoot = getAssetsRoot();
241
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
+ }
242
290
  await migrateExistingStoreGateContract(store, workspaceRoot, assetsRoot, warnings);
243
291
  const hostPolicy = (await store.getJSON("meta/host_materialization")) ?? {};
244
292
  // Re-materialize host files
@@ -260,7 +308,7 @@ export async function repairWorkspace(workspaceRoot) {
260
308
  return warnings;
261
309
  }
262
310
  // ── ace compact ───────────────────────────────────────────────────────────────
263
- export async function compactWorkspace(workspaceRoot) {
311
+ export async function compactWorkspace(workspaceRoot, options = {}) {
264
312
  const resolved = await ensureCanonicalWorkspaceStore(workspaceRoot, {
265
313
  operationLabel: "compactWorkspace",
266
314
  });
@@ -278,6 +326,12 @@ export async function compactWorkspace(workspaceRoot) {
278
326
  await store.close();
279
327
  }
280
328
  const after = statSync(storePath).size;
281
- return { before, after };
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 };
282
336
  }
283
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,2 @@
1
+ export declare function crc32(bytes: Uint8Array): number;
2
+ //# sourceMappingURL=crc32.d.ts.map
@@ -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
@@ -6,5 +6,6 @@ export declare class ContextSnapshotMaterializer {
6
6
  constructor(store: AcePackedStore, workspaceRoot: string);
7
7
  private directoryPath;
8
8
  materializeAll(): Promise<void>;
9
+ materializeAllForExport(outputDir: string): Promise<void>;
9
10
  }
10
11
  //# sourceMappingURL=context-snapshot-materializer.d.ts.map
@@ -22,7 +22,10 @@ export class ContextSnapshotMaterializer {
22
22
  return join(this.workspaceRoot, "agent-state", "context-snapshots");
23
23
  }
24
24
  async materializeAll() {
25
- const dir = this.directoryPath();
25
+ throw new Error("Context snapshots are store-only by default. Use `ace read agent-state/context-snapshots/<name>.json --out <file>` to export a copy.");
26
+ }
27
+ async materializeAllForExport(outputDir) {
28
+ const dir = outputDir;
26
29
  if (!existsSync(dir))
27
30
  mkdirSync(dir, { recursive: true });
28
31
  const snapshots = await this.repo.listSnapshots();
@@ -1,5 +1,11 @@
1
1
  import { AcePackedStore } from "../ace-packed-store.js";
2
2
  export type RuntimeProjectionTarget = "context_snapshots" | "handoffs" | "ledger" | "scheduler" | "status_events" | "todo_state" | "vericify_posts" | "hook_context";
3
+ /**
4
+ * Awaitable handle for the detached lifecycle sweep. CLI entry points and
5
+ * tests MUST await this before process exit / assertions; the MCP server may
6
+ * ignore it (long-lived process, sweep completes in background).
7
+ */
8
+ export declare function waitForPendingLifecycleSweep(): Promise<void>;
3
9
  export declare class ProjectionManager {
4
10
  private store;
5
11
  private workspaceRoot;
@@ -7,6 +13,7 @@ export declare class ProjectionManager {
7
13
  private todoSyncer;
8
14
  private hookContext;
9
15
  private scheduler;
16
+ private static lifecycleSweepActive;
10
17
  constructor(store: AcePackedStore, workspaceRoot: string);
11
18
  projectAfterCommit(targets: readonly RuntimeProjectionTarget[]): Promise<void>;
12
19
  }
@@ -2,6 +2,18 @@ import { HookContextMaterializer } from "./hook-context-materializer.js";
2
2
  import { SchedulerProjectionMaterializer } from "./scheduler-projection-materializer.js";
3
3
  import { TodoSyncer } from "./todo-syncer.js";
4
4
  import { VericifyProjector } from "./vericify-projector.js";
5
+ import { cacheWorkspaceArtifacts } from "../cache-workspace.js";
6
+ let pendingLifecycleSweep;
7
+ let lastLifecycleSweepStartedAt = 0;
8
+ const LIFECYCLE_SWEEP_MIN_INTERVAL_MS = 60_000;
9
+ /**
10
+ * Awaitable handle for the detached lifecycle sweep. CLI entry points and
11
+ * tests MUST await this before process exit / assertions; the MCP server may
12
+ * ignore it (long-lived process, sweep completes in background).
13
+ */
14
+ export function waitForPendingLifecycleSweep() {
15
+ return pendingLifecycleSweep ?? Promise.resolve();
16
+ }
5
17
  export class ProjectionManager {
6
18
  store;
7
19
  workspaceRoot;
@@ -9,6 +21,7 @@ export class ProjectionManager {
9
21
  todoSyncer;
10
22
  hookContext;
11
23
  scheduler;
24
+ static lifecycleSweepActive = false;
12
25
  constructor(store, workspaceRoot) {
13
26
  this.store = store;
14
27
  this.workspaceRoot = workspaceRoot;
@@ -55,6 +68,30 @@ export class ProjectionManager {
55
68
  if (stagedStoreWrites) {
56
69
  await this.store.commit();
57
70
  }
71
+ if (stagedStoreWrites &&
72
+ !ProjectionManager.lifecycleSweepActive &&
73
+ Date.now() - lastLifecycleSweepStartedAt >= LIFECYCLE_SWEEP_MIN_INTERVAL_MS) {
74
+ ProjectionManager.lifecycleSweepActive = true;
75
+ lastLifecycleSweepStartedAt = Date.now();
76
+ const workspaceRoot = this.workspaceRoot;
77
+ // Deliberately NOT awaited: projectAfterCommit runs inside
78
+ // withStoreWriteCoordinator in most callers, and the sweep's own
79
+ // writeStoreBlobsQueued must enqueue BEHIND the current operation in the
80
+ // non-reentrant write queue. Awaiting here deadlocks (see
81
+ // REPORT_STORE_WIRING_MITIGATIONS_2026-07-09.md F1). The detached promise
82
+ // is exposed via waitForPendingLifecycleSweep() for CLI/test flows.
83
+ pendingLifecycleSweep = cacheWorkspaceArtifacts(workspaceRoot, {
84
+ clean: true,
85
+ minAgeMs: 5 * 60 * 1000,
86
+ })
87
+ .then(() => undefined)
88
+ .catch((error) => {
89
+ console.error(`ACE lifecycle sweep-lite skipped: ${error instanceof Error ? error.message : String(error)}`);
90
+ })
91
+ .finally(() => {
92
+ ProjectionManager.lifecycleSweepActive = false;
93
+ });
94
+ }
58
95
  for (const target of uniqueTargets) {
59
96
  if (target === "todo_state") {
60
97
  await this.todoSyncer.writeToFile();
@@ -2,6 +2,7 @@ import { existsSync, mkdirSync, renameSync, writeFileSync } from "node:fs";
2
2
  import { dirname, join } from "node:path";
3
3
  import { operationalArtifactKey } from "../store-artifacts.js";
4
4
  import { SchedulerRepository } from "../repositories/scheduler-repository.js";
5
+ import { isProjectedAceWorkspacePath } from "../../helpers/path-utils.js";
5
6
  function writeText(path, content) {
6
7
  const dir = dirname(path);
7
8
  if (!existsSync(dir))
@@ -40,9 +41,15 @@ export class SchedulerProjectionMaterializer {
40
41
  }
41
42
  async materializeFiles(rendered) {
42
43
  const content = rendered ?? (await this.render());
43
- writeText(this.filePath("job-queue.json"), content.queue);
44
- writeText(this.filePath("job-locks.json"), content.locks);
45
- writeText(this.filePath("scheduler-lease.json"), content.lease);
44
+ if (isProjectedAceWorkspacePath("agent-state/job-queue.json")) {
45
+ writeText(this.filePath("job-queue.json"), content.queue);
46
+ }
47
+ if (isProjectedAceWorkspacePath("agent-state/job-locks.json")) {
48
+ writeText(this.filePath("job-locks.json"), content.locks);
49
+ }
50
+ if (isProjectedAceWorkspacePath("agent-state/scheduler-lease.json")) {
51
+ writeText(this.filePath("scheduler-lease.json"), content.lease);
52
+ }
46
53
  }
47
54
  }
48
55
  //# sourceMappingURL=scheduler-projection-materializer.js.map