@voybio/ace-swarm 2.4.3 → 2.4.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (144) hide show
  1. package/CHANGELOG.md +78 -0
  2. package/README.md +72 -532
  3. package/assets/agent-state/INTERFACE_REGISTRY.md +2 -2
  4. package/assets/agent-state/MODULES/gates/gate-autonomy.json +4 -2
  5. package/assets/agent-state/MODULES/gates/gate-completeness.json +4 -2
  6. package/assets/agent-state/MODULES/gates/gate-operability.json +4 -2
  7. package/assets/agent-state/MODULES/gates/gate-security.json +3 -1
  8. package/assets/agent-state/MODULES/registry.json +1 -4
  9. package/assets/agent-state/MODULES/roles/capability-build.json +1 -2
  10. package/assets/agent-state/MODULES/roles/capability-eval.json +1 -1
  11. package/assets/agent-state/MODULES/roles/capability-qa.json +1 -2
  12. package/assets/agent-state/QUALITY_GATES.md +25 -9
  13. package/assets/agent-state/TEAL_CONFIG.md +2 -11
  14. package/assets/eval-battery/battery.json +85 -0
  15. package/assets/eval-battery/fixtures/edit-target.js +3 -0
  16. package/assets/scripts/render-mcp-configs.sh +12 -0
  17. package/dist/ace-context.d.ts +18 -0
  18. package/dist/ace-context.js +161 -16
  19. package/dist/ace-server-instructions.js +36 -9
  20. package/dist/astgrep-index.d.ts +7 -1
  21. package/dist/astgrep-index.js +66 -39
  22. package/dist/bridge-trace.d.ts +17 -0
  23. package/dist/bridge-trace.js +23 -0
  24. package/dist/cli.d.ts +9 -1
  25. package/dist/cli.js +619 -10
  26. package/dist/discovery-runtime-wrappers.d.ts +16 -1
  27. package/dist/discovery-runtime-wrappers.js +65 -9
  28. package/dist/eval-harness.d.ts +110 -0
  29. package/dist/eval-harness.js +254 -0
  30. package/dist/experience-reflector.d.ts +13 -0
  31. package/dist/experience-reflector.js +62 -0
  32. package/dist/gate-contract.d.ts +63 -0
  33. package/dist/gate-contract.js +178 -0
  34. package/dist/goal-engine.d.ts +155 -0
  35. package/dist/goal-engine.js +795 -0
  36. package/dist/helpers/store-resolution.js +40 -39
  37. package/dist/hermes/session-manager.d.ts +10 -0
  38. package/dist/hermes/session-manager.js +130 -27
  39. package/dist/job-scheduler.d.ts +58 -0
  40. package/dist/job-scheduler.js +202 -2
  41. package/dist/job-worker.d.ts +87 -0
  42. package/dist/job-worker.js +182 -0
  43. package/dist/local-model-policy.d.ts +8 -2
  44. package/dist/local-model-policy.js +31 -17
  45. package/dist/local-model-runtime.d.ts +6 -0
  46. package/dist/local-model-runtime.js +130 -2
  47. package/dist/measured-classing.d.ts +66 -0
  48. package/dist/measured-classing.js +165 -0
  49. package/dist/model-bridge.d.ts +45 -0
  50. package/dist/model-bridge.js +502 -44
  51. package/dist/orchestrator-supervisor.js +1 -1
  52. package/dist/plan-proposal.d.ts +7 -0
  53. package/dist/plan-proposal.js +74 -4
  54. package/dist/playbook-adaptation.d.ts +37 -0
  55. package/dist/playbook-adaptation.js +159 -0
  56. package/dist/playbook-curator.d.ts +16 -0
  57. package/dist/playbook-curator.js +106 -0
  58. package/dist/playbook-llm-reflector.d.ts +26 -0
  59. package/dist/playbook-llm-reflector.js +157 -0
  60. package/dist/public-surface.d.ts +2 -0
  61. package/dist/public-surface.js +26 -14
  62. package/dist/runtime-executor.js +45 -2
  63. package/dist/runtime-tool-specs.d.ts +8 -1
  64. package/dist/runtime-tool-specs.js +19 -1
  65. package/dist/safe-edit.d.ts +20 -0
  66. package/dist/safe-edit.js +355 -78
  67. package/dist/schemas.js +16 -15
  68. package/dist/server.d.ts +11 -2
  69. package/dist/server.js +55 -5
  70. package/dist/shared.d.ts +12 -0
  71. package/dist/shared.js +13 -1
  72. package/dist/source-mutation-coordinator.d.ts +54 -0
  73. package/dist/source-mutation-coordinator.js +259 -0
  74. package/dist/status-events.d.ts +4 -1
  75. package/dist/status-events.js +41 -6
  76. package/dist/store/ace-packed-store.d.ts +28 -9
  77. package/dist/store/ace-packed-store.js +324 -57
  78. package/dist/store/bgzf.d.ts +4 -0
  79. package/dist/store/bgzf.js +94 -0
  80. package/dist/store/bootstrap-store.d.ts +4 -1
  81. package/dist/store/bootstrap-store.js +87 -9
  82. package/dist/store/cache-workspace.d.ts +8 -0
  83. package/dist/store/cache-workspace.js +67 -1
  84. package/dist/store/crc32.d.ts +2 -0
  85. package/dist/store/crc32.js +16 -0
  86. package/dist/store/fsck.d.ts +16 -0
  87. package/dist/store/fsck.js +240 -0
  88. package/dist/store/gate-contract-migration.d.ts +10 -0
  89. package/dist/store/gate-contract-migration.js +413 -0
  90. package/dist/store/materializers/context-snapshot-materializer.d.ts +1 -0
  91. package/dist/store/materializers/context-snapshot-materializer.js +4 -1
  92. package/dist/store/materializers/projection-manager.d.ts +7 -0
  93. package/dist/store/materializers/projection-manager.js +37 -0
  94. package/dist/store/materializers/scheduler-projection-materializer.js +10 -3
  95. package/dist/store/repositories/goal-repository.d.ts +112 -0
  96. package/dist/store/repositories/goal-repository.js +157 -0
  97. package/dist/store/repositories/playbook-repository.d.ts +107 -0
  98. package/dist/store/repositories/playbook-repository.js +314 -0
  99. package/dist/store/store-artifacts.js +44 -2
  100. package/dist/store/store-authority-audit.js +1 -0
  101. package/dist/store/store-cli.d.ts +30 -0
  102. package/dist/store/store-cli.js +242 -0
  103. package/dist/store/store-snapshot.d.ts +12 -0
  104. package/dist/store/store-snapshot.js +74 -11
  105. package/dist/store/types.d.ts +1 -0
  106. package/dist/store/write-coordinator.d.ts +7 -0
  107. package/dist/store/write-coordinator.js +11 -0
  108. package/dist/todo-state.d.ts +21 -0
  109. package/dist/todo-state.js +150 -53
  110. package/dist/tools-agent.js +86 -22
  111. package/dist/tools-discovery.js +5 -3
  112. package/dist/tools-files.js +107 -23
  113. package/dist/tools-framework.js +146 -40
  114. package/dist/tools-goal.d.ts +11 -0
  115. package/dist/tools-goal.js +108 -0
  116. package/dist/tools-handoff.js +7 -11
  117. package/dist/tools-lifecycle.js +2 -0
  118. package/dist/tools-scheduler.js +44 -1
  119. package/dist/tools-skills.js +3 -2
  120. package/dist/tools-todo.js +64 -3
  121. package/dist/tools.js +2 -0
  122. package/dist/tui/chat.js +4 -0
  123. package/dist/tui/index.js +4 -1
  124. package/dist/tui/ollama.d.ts +2 -0
  125. package/dist/tui/openai-compatible.d.ts +14 -0
  126. package/dist/tui/openai-compatible.js +15 -2
  127. package/dist/tui/provider-discovery.d.ts +1 -0
  128. package/dist/tui/provider-discovery.js +2 -2
  129. package/docs/cli-reference.md +125 -0
  130. package/docs/development.md +46 -0
  131. package/docs/evaluation-safety.md +27 -0
  132. package/docs/experience-playbook.md +35 -0
  133. package/docs/hermes.md +129 -0
  134. package/docs/library-api.md +31 -0
  135. package/docs/mcp-tool-reference.md +290 -0
  136. package/docs/provider-security.md +85 -0
  137. package/docs/state-model.md +113 -0
  138. package/docs/troubleshooting.md +95 -0
  139. package/docs/tui-operator-guide.md +75 -0
  140. package/package.json +5 -1
  141. package/scripts/dev/run_hermes_crispr_live.mjs +212 -0
  142. package/assets/agent-state/MODULES/gates/gate-correctness.json +0 -7
  143. package/assets/agent-state/MODULES/gates/gate-evaluation.json +0 -7
  144. package/assets/agent-state/MODULES/gates/gate-typescript-public-surface.json +0 -7
@@ -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 seedOperationalArtifacts(existingStore);
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
- 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 };
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,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
@@ -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