@voybio/ace-swarm 2.4.4 → 2.4.7

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 +41 -12
  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 +623 -11
  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 +127 -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
package/dist/safe-edit.js CHANGED
@@ -2,13 +2,14 @@
2
2
  * Safe-edit module: copy → modify → validate → swap pattern.
3
3
  * Prevents bricking by never leaving core files in a broken state.
4
4
  */
5
- import { copyFileSync, cpSync, existsSync, mkdtempSync, mkdirSync, readFileSync, renameSync, rmSync, symlinkSync, unlinkSync, writeFileSync, } from "node:fs";
5
+ import { copyFileSync, cpSync, existsSync, mkdtempSync, mkdirSync, readdirSync, readFileSync, renameSync, rmSync, statSync, symlinkSync, unlinkSync, writeFileSync, } from "node:fs";
6
6
  import { createHash } from "node:crypto";
7
7
  import { basename, dirname, relative, resolve } from "node:path";
8
8
  import { spawnSync } from "node:child_process";
9
9
  import { tmpdir } from "node:os";
10
10
  import { WORKSPACE_ROOT, safeRead, wsPath } from "./helpers.js";
11
11
  import { isInside, isReadError, normalizeRelPath } from "./shared.js";
12
+ import { appendSourceEditEvent, SourceEditLeaseBusyError, withSourceMutationCoordinatorSync, } from "./source-mutation-coordinator.js";
12
13
  const STAGING_DIR = ".ace-staging";
13
14
  // ────────────────────────────────────────────────────────────────────
14
15
  // Helpers
@@ -16,6 +17,52 @@ const STAGING_DIR = ".ace-staging";
16
17
  function fileHash(content) {
17
18
  return createHash("sha256").update(content).digest("hex").slice(0, 16);
18
19
  }
20
+ function fullFileHash(content) {
21
+ return `sha256:${createHash("sha256").update(content).digest("hex")}`;
22
+ }
23
+ function hashMatchesExpected(expected, content) {
24
+ return expected === fullFileHash(content) || expected === fileHash(content);
25
+ }
26
+ function sourceConflictResult(input) {
27
+ return {
28
+ ok: false,
29
+ original_path: input.absPath,
30
+ staging_path: input.stagingDir,
31
+ backup_path: input.backupPath,
32
+ original_hash: input.originalHash,
33
+ new_hash: input.newHash,
34
+ validation_passed: input.validationCheck?.validation_passed,
35
+ validation_output: input.validationCheck?.validation_output,
36
+ test_passed: input.validationCheck?.test_passed,
37
+ test_output: input.validationCheck?.test_output,
38
+ error: "Source edit conflict — target changed after base hash was captured",
39
+ reason_code: "source_edit_conflict",
40
+ expected_hash: input.expectedHash,
41
+ observed_hash: input.observedHash,
42
+ lease_lock_path: input.leaseLockPath,
43
+ lease_waited_ms: input.leaseWaitedMs,
44
+ restored: true,
45
+ };
46
+ }
47
+ function leaseBusyResult(input) {
48
+ return {
49
+ ok: false,
50
+ original_path: input.absPath,
51
+ staging_path: input.stagingDir,
52
+ backup_path: input.backupPath,
53
+ original_hash: input.originalHash,
54
+ new_hash: input.newHash,
55
+ validation_passed: input.validationCheck?.validation_passed,
56
+ validation_output: input.validationCheck?.validation_output,
57
+ test_passed: input.validationCheck?.test_passed,
58
+ test_output: input.validationCheck?.test_output,
59
+ error: input.error.message,
60
+ reason_code: input.error.reason_code,
61
+ lease_lock_path: input.error.lockPath,
62
+ lease_waited_ms: input.error.waitedMs,
63
+ restored: true,
64
+ };
65
+ }
19
66
  function ensureDir(dirPath) {
20
67
  if (!existsSync(dirPath))
21
68
  mkdirSync(dirPath, { recursive: true });
@@ -30,12 +77,14 @@ function resolveWorkspaceTarget(inputPath) {
30
77
  relPath: normalizeRelPath(relative(WORKSPACE_ROOT, absPath)),
31
78
  };
32
79
  }
80
+ const STAGING_PRUNE_DIR_NAMES = new Set([".ace-staging", ".git", "node_modules", ".npm-cache"]);
33
81
  function shouldCopyIntoStaging(src) {
34
82
  const rel = normalizeRelPath(relative(WORKSPACE_ROOT, src));
35
83
  if (!rel || rel === ".")
36
84
  return true;
37
- const first = rel.split("/")[0];
38
- return !new Set([".ace-staging", ".git", "node_modules", ".npm-cache"]).has(first);
85
+ // Prune heavy/ignored directories at ANY depth (e.g. monorepo packages/*/node_modules),
86
+ // not just top-level — a first-segment-only check copied nested node_modules per edit.
87
+ return !rel.split("/").some((segment) => STAGING_PRUNE_DIR_NAMES.has(segment));
39
88
  }
40
89
  function prepareStagedWorkspace(stagingDir) {
41
90
  const stagedWorkspace = mkdtempSync(resolve(tmpdir(), "ace-staged-workspace-"));
@@ -56,12 +105,77 @@ function prepareStagedWorkspace(stagingDir) {
56
105
  function workspacePathIn(root, relPath) {
57
106
  return resolve(root, relPath);
58
107
  }
108
+ function backupPathForTarget(relPath, originalHash) {
109
+ const flatName = relPath.replace(/\//g, "__");
110
+ const backupDir = wsPath(STAGING_DIR, "backups");
111
+ ensureDir(backupDir);
112
+ return resolve(backupDir, `${flatName}.${originalHash.slice(0, 8)}.backup`);
113
+ }
59
114
  function swapTargetContent(absPath, content) {
60
115
  ensureDir(dirname(absPath));
61
116
  const swapPath = `${absPath}.ace-swap.${process.pid}.${Date.now()}`;
62
117
  writeFileSync(swapPath, content, "utf-8");
63
118
  renameSync(swapPath, absPath);
64
119
  }
120
+ export function gcStaging(input = {}) {
121
+ const maxAgeHours = input.max_age_hours ?? 72;
122
+ const maxEntries = input.max_entries ?? 200;
123
+ const stagingPath = wsPath(STAGING_DIR);
124
+ if (!existsSync(stagingPath)) {
125
+ return {
126
+ ok: true,
127
+ staging_path: stagingPath,
128
+ removed_entries: 0,
129
+ kept_entries: 0,
130
+ errors: [],
131
+ };
132
+ }
133
+ const protectedEntries = new Set(["backups", "locks"]);
134
+ const now = Date.now();
135
+ const maxAgeMs = Math.max(0, maxAgeHours) * 60 * 60 * 1000;
136
+ const entries = readdirSync(stagingPath, { withFileTypes: true })
137
+ .filter((entry) => !protectedEntries.has(entry.name))
138
+ .map((entry) => {
139
+ const absPath = resolve(stagingPath, entry.name);
140
+ const stats = statSync(absPath);
141
+ return {
142
+ absPath,
143
+ name: entry.name,
144
+ mtimeMs: stats.mtimeMs,
145
+ ageMs: now - stats.mtimeMs,
146
+ };
147
+ })
148
+ .sort((a, b) => b.mtimeMs - a.mtimeMs);
149
+ const toRemove = new Set();
150
+ for (const entry of entries) {
151
+ if (maxAgeMs === 0 || entry.ageMs > maxAgeMs) {
152
+ toRemove.add(entry.absPath);
153
+ }
154
+ }
155
+ if (Number.isFinite(maxEntries) && maxEntries >= 0) {
156
+ for (const entry of entries.slice(maxEntries)) {
157
+ toRemove.add(entry.absPath);
158
+ }
159
+ }
160
+ const errors = [];
161
+ let removed = 0;
162
+ for (const absPath of toRemove) {
163
+ try {
164
+ rmSync(absPath, { recursive: true, force: true });
165
+ removed += 1;
166
+ }
167
+ catch (error) {
168
+ errors.push(`${absPath}: ${error instanceof Error ? error.message : String(error)}`);
169
+ }
170
+ }
171
+ return {
172
+ ok: errors.length === 0,
173
+ staging_path: stagingPath,
174
+ removed_entries: removed,
175
+ kept_entries: entries.length - removed,
176
+ errors,
177
+ };
178
+ }
65
179
  function runValidation(validationCmd, testCmd, cwd = WORKSPACE_ROOT) {
66
180
  let validation_passed;
67
181
  let validation_output;
@@ -115,13 +229,16 @@ export function safeEditFile(input) {
115
229
  // Step 1: Create staging directory
116
230
  const stagingDir = wsPath(STAGING_DIR, `${timestamp}-${isNew ? "new" : originalHash.slice(0, 8)}`);
117
231
  ensureDir(stagingDir);
232
+ const needsValidationWorkspace = Boolean(input.validation_command || input.test_command);
118
233
  const flatName = relPath.replace(/\//g, "__");
119
234
  const stagingNew = resolve(stagingDir, `new__${flatName}`);
120
235
  writeFileSync(stagingNew, input.content, "utf-8");
121
- const stagedWorkspace = prepareStagedWorkspace(stagingDir);
122
- const stagedTarget = workspacePathIn(stagedWorkspace, relPath);
123
- ensureDir(dirname(stagedTarget));
124
- writeFileSync(stagedTarget, input.content, "utf-8");
236
+ const stagedWorkspace = needsValidationWorkspace ? prepareStagedWorkspace(stagingDir) : undefined;
237
+ const stagedTarget = stagedWorkspace ? workspacePathIn(stagedWorkspace, relPath) : stagingNew;
238
+ if (stagedWorkspace) {
239
+ ensureDir(dirname(stagedTarget));
240
+ writeFileSync(stagedTarget, input.content, "utf-8");
241
+ }
125
242
  // Step 2: If file exists, copy original to staging + backup
126
243
  let backupPath;
127
244
  if (!isNew) {
@@ -130,7 +247,19 @@ export function safeEditFile(input) {
130
247
  }
131
248
  // Step 3: Run validation/tests against the staged workspace.
132
249
  let check;
133
- if (input.validation_command || input.test_command) {
250
+ if (needsValidationWorkspace) {
251
+ if (!stagedWorkspace) {
252
+ return {
253
+ ok: false,
254
+ original_path: absPath,
255
+ staging_path: stagingDir,
256
+ backup_path: backupPath,
257
+ original_hash: originalHash,
258
+ new_hash: stagedNewHash,
259
+ error: "Validation workspace was not prepared",
260
+ restored: true,
261
+ };
262
+ }
134
263
  check = runValidation(input.validation_command, input.test_command, stagedWorkspace);
135
264
  if (!check.passed) {
136
265
  return {
@@ -149,26 +278,83 @@ export function safeEditFile(input) {
149
278
  };
150
279
  }
151
280
  }
152
- // Step 4: Promote staged content to the real target only after checks pass.
153
- if (!isNew && input.backup !== false) {
154
- backupPath = `${absPath}.ace-backup.${originalHash.slice(0, 8)}`;
155
- copyFileSync(absPath, backupPath);
281
+ try {
282
+ return withSourceMutationCoordinatorSync(relPath, (lease) => {
283
+ // Step 4: Promote staged content to the real target only after checks pass.
284
+ if (input.expected_base_hash) {
285
+ const currentContent = safeRead(input.path);
286
+ const observedHash = isReadError(currentContent) ? "none" : fullFileHash(currentContent);
287
+ if (isReadError(currentContent) || !hashMatchesExpected(input.expected_base_hash, currentContent)) {
288
+ appendSourceEditEvent({
289
+ event: "SOURCE_EDIT_CONFLICT",
290
+ target_path: relPath,
291
+ lock_path: lease.lockPath,
292
+ waited_ms: lease.waitedMs,
293
+ base_hash: originalHash,
294
+ expected_hash: input.expected_base_hash,
295
+ observed_hash: observedHash,
296
+ reason_code: "source_edit_conflict",
297
+ });
298
+ return sourceConflictResult({
299
+ absPath,
300
+ stagingDir,
301
+ backupPath,
302
+ originalHash,
303
+ newHash: stagedNewHash,
304
+ expectedHash: input.expected_base_hash,
305
+ observedHash,
306
+ validationCheck: check,
307
+ leaseLockPath: lease.lockPath,
308
+ leaseWaitedMs: lease.waitedMs,
309
+ });
310
+ }
311
+ }
312
+ let promotedBackupPath;
313
+ if (!isNew && input.backup !== false) {
314
+ promotedBackupPath = backupPathForTarget(relPath, originalHash);
315
+ copyFileSync(absPath, promotedBackupPath);
316
+ }
317
+ const promotedContent = needsValidationWorkspace ? readFileSync(stagedTarget, "utf-8") : input.content;
318
+ const promotedHash = fileHash(promotedContent);
319
+ swapTargetContent(absPath, promotedContent);
320
+ appendSourceEditEvent({
321
+ event: "SOURCE_EDIT_COMMITTED",
322
+ target_path: relPath,
323
+ lock_path: lease.lockPath,
324
+ waited_ms: lease.waitedMs,
325
+ base_hash: originalHash,
326
+ committed_hash: promotedHash,
327
+ });
328
+ return {
329
+ ok: true,
330
+ original_path: absPath,
331
+ staging_path: stagingDir,
332
+ backup_path: promotedBackupPath,
333
+ original_hash: originalHash,
334
+ new_hash: promotedHash,
335
+ validation_passed: check?.validation_passed,
336
+ validation_output: check?.validation_output,
337
+ test_passed: check?.test_passed,
338
+ test_output: check?.test_output,
339
+ lease_lock_path: lease.lockPath,
340
+ lease_waited_ms: lease.waitedMs,
341
+ };
342
+ });
343
+ }
344
+ catch (error) {
345
+ if (error instanceof SourceEditLeaseBusyError) {
346
+ return leaseBusyResult({
347
+ absPath,
348
+ stagingDir,
349
+ backupPath,
350
+ originalHash,
351
+ newHash: stagedNewHash,
352
+ error,
353
+ validationCheck: check,
354
+ });
355
+ }
356
+ throw error;
156
357
  }
157
- const promotedContent = readFileSync(stagedTarget, "utf-8");
158
- const promotedHash = fileHash(promotedContent);
159
- swapTargetContent(absPath, promotedContent);
160
- return {
161
- ok: true,
162
- original_path: absPath,
163
- staging_path: stagingDir,
164
- backup_path: backupPath,
165
- original_hash: originalHash,
166
- new_hash: promotedHash,
167
- validation_passed: check?.validation_passed,
168
- validation_output: check?.validation_output,
169
- test_passed: check?.test_passed,
170
- test_output: check?.test_output,
171
- };
172
358
  }
173
359
  // ────────────────────────────────────────────────────────────────────
174
360
  // Test runner
@@ -221,42 +407,72 @@ export function runTestSuite(command) {
221
407
  export function diffContents(original, updated) {
222
408
  const origLines = original.split(/\r?\n/);
223
409
  const newLines = updated.split(/\r?\n/);
224
- // Simple line-based diff summary
225
- const origSet = new Set(origLines);
226
- const newSet = new Set(newLines);
410
+ let prefix = 0;
411
+ while (prefix < origLines.length &&
412
+ prefix < newLines.length &&
413
+ origLines[prefix] === newLines[prefix])
414
+ prefix++;
415
+ let suffix = 0;
416
+ while (suffix < origLines.length - prefix &&
417
+ suffix < newLines.length - prefix &&
418
+ origLines[origLines.length - 1 - suffix] === newLines[newLines.length - 1 - suffix])
419
+ suffix++;
420
+ const origChanged = origLines.slice(prefix, origLines.length - suffix);
421
+ const newChanged = newLines.slice(prefix, newLines.length - suffix);
422
+ // Exact LCS is useful for normal source files but its matrix is quadratic.
423
+ // Above this fixed budget, emit a valid deterministic whole-file replacement
424
+ // hunk instead. That fallback is linear, preserves every line, and never
425
+ // turns a real change into a false negative.
426
+ const exactComparisonBudget = 1_000_000;
427
+ const useExactLcs = origChanged.length * newChanged.length <= exactComparisonBudget;
428
+ const changes = [];
227
429
  let added = 0;
228
430
  let removed = 0;
229
- for (const line of newLines) {
230
- if (!origSet.has(line))
231
- added++;
431
+ if (!useExactLcs) {
432
+ for (const line of origChanged)
433
+ changes.push(`-${line}`);
434
+ for (const line of newChanged)
435
+ changes.push(`+${line}`);
436
+ removed = origChanged.length;
437
+ added = newChanged.length;
232
438
  }
233
- for (const line of origLines) {
234
- if (!newSet.has(line))
235
- removed++;
236
- }
237
- const summaryLines = [];
238
- summaryLines.push(`Lines: ${origLines.length} → ${newLines.length} (${added > 0 ? `+${added}` : "+0"}, ${removed > 0 ? `-${removed}` : "-0"})`);
239
- // Show first 30 changed lines for context
240
- const changes = [];
241
- const maxContext = 30;
242
- let shown = 0;
243
- for (const line of origLines) {
244
- if (!newSet.has(line) && shown < maxContext) {
245
- changes.push(`- ${line}`);
246
- shown++;
439
+ else {
440
+ const lengths = Array.from({ length: origChanged.length + 1 }, () => new Uint32Array(newChanged.length + 1));
441
+ for (let i = origChanged.length - 1; i >= 0; i--) {
442
+ for (let j = newChanged.length - 1; j >= 0; j--) {
443
+ lengths[i][j] = origChanged[i] === newChanged[j]
444
+ ? lengths[i + 1][j + 1] + 1
445
+ : Math.max(lengths[i + 1][j], lengths[i][j + 1]);
446
+ }
247
447
  }
248
- }
249
- for (const line of newLines) {
250
- if (!origSet.has(line) && shown < maxContext) {
251
- changes.push(`+ ${line}`);
252
- shown++;
448
+ let i = 0;
449
+ let j = 0;
450
+ while (i < origChanged.length || j < newChanged.length) {
451
+ if (i < origChanged.length && j < newChanged.length && origChanged[i] === newChanged[j]) {
452
+ changes.push(` ${origChanged[i]}`);
453
+ i++;
454
+ j++;
455
+ }
456
+ else if (j < newChanged.length && (i === origChanged.length || lengths[i][j + 1] >= lengths[i + 1][j])) {
457
+ changes.push(`+${newChanged[j]}`);
458
+ added++;
459
+ j++;
460
+ }
461
+ else {
462
+ changes.push(`-${origChanged[i]}`);
463
+ removed++;
464
+ i++;
465
+ }
253
466
  }
254
467
  }
255
- if (changes.length > 0) {
256
- summaryLines.push("", "Changes (sample):", ...changes);
257
- }
468
+ const summaryLines = [
469
+ "--- original",
470
+ "+++ modified",
471
+ `@@ -${prefix + 1},${origChanged.length} +${prefix + 1},${newChanged.length} @@`,
472
+ ...changes,
473
+ ];
258
474
  return {
259
- has_diff: added > 0 || removed > 0,
475
+ has_diff: original !== updated,
260
476
  original_lines: origLines.length,
261
477
  new_lines: newLines.length,
262
478
  added,
@@ -311,17 +527,24 @@ export function applyPatch(input) {
311
527
  const timestamp = Date.now();
312
528
  const stagingDir = wsPath(STAGING_DIR, `${timestamp}-patch-${isNew ? "new" : originalHash.slice(0, 8)}`);
313
529
  ensureDir(stagingDir);
314
- const stagedWorkspace = prepareStagedWorkspace(stagingDir);
315
- const stagedTarget = workspacePathIn(stagedWorkspace, relPath);
530
+ const needsValidationWorkspace = Boolean(input.validation_command || input.test_command);
531
+ const stagedWorkspace = needsValidationWorkspace ? prepareStagedWorkspace(stagingDir) : undefined;
532
+ const stagedTarget = stagedWorkspace
533
+ ? workspacePathIn(stagedWorkspace, relPath)
534
+ : resolve(stagingDir, `patch-target__${relPath.replace(/\//g, "__")}`);
316
535
  ensureDir(dirname(stagedTarget));
317
- if (isNew && !existsSync(stagedTarget))
536
+ if (!stagedWorkspace && !isNew) {
537
+ writeFileSync(stagedTarget, originalContent, "utf-8");
538
+ }
539
+ else if (isNew && !existsSync(stagedTarget)) {
318
540
  writeFileSync(stagedTarget, "", "utf-8");
541
+ }
319
542
  const tmpPatch = resolve(stagingDir, `${basename(relPath)}.${timestamp}.patch`);
320
543
  try {
321
544
  writeFileSync(tmpPatch, input.patch, "utf8");
322
545
  const patchResult = spawnSync("patch", [stagedTarget, tmpPatch], {
323
546
  encoding: "utf8",
324
- cwd: stagedWorkspace,
547
+ cwd: stagedWorkspace ?? WORKSPACE_ROOT,
325
548
  });
326
549
  if (patchResult.status !== 0) {
327
550
  return {
@@ -336,7 +559,7 @@ export function applyPatch(input) {
336
559
  }
337
560
  const patchedContent = readFileSync(stagedTarget, "utf-8");
338
561
  const newHash = fileHash(patchedContent);
339
- const check = runValidation(input.validation_command, input.test_command, stagedWorkspace);
562
+ const check = runValidation(input.validation_command, input.test_command, stagedWorkspace ?? stagingDir);
340
563
  if (!check.passed) {
341
564
  return {
342
565
  ok: false,
@@ -352,24 +575,78 @@ export function applyPatch(input) {
352
575
  restored: true,
353
576
  };
354
577
  }
355
- let backupPath;
356
- if (!isNew) {
357
- backupPath = `${absPath}.ace-backup.${originalHash.slice(0, 8)}`;
358
- copyFileSync(absPath, backupPath);
578
+ try {
579
+ return withSourceMutationCoordinatorSync(relPath, (lease) => {
580
+ let backupPath;
581
+ if (input.expected_base_hash) {
582
+ const currentContent = safeRead(input.path);
583
+ const observedHash = isReadError(currentContent) ? "none" : fullFileHash(currentContent);
584
+ if (isReadError(currentContent) || !hashMatchesExpected(input.expected_base_hash, currentContent)) {
585
+ appendSourceEditEvent({
586
+ event: "SOURCE_EDIT_CONFLICT",
587
+ target_path: relPath,
588
+ lock_path: lease.lockPath,
589
+ waited_ms: lease.waitedMs,
590
+ base_hash: originalHash,
591
+ expected_hash: input.expected_base_hash,
592
+ observed_hash: observedHash,
593
+ reason_code: "source_edit_conflict",
594
+ });
595
+ return sourceConflictResult({
596
+ absPath,
597
+ stagingDir,
598
+ originalHash,
599
+ newHash,
600
+ expectedHash: input.expected_base_hash,
601
+ observedHash,
602
+ validationCheck: check,
603
+ leaseLockPath: lease.lockPath,
604
+ leaseWaitedMs: lease.waitedMs,
605
+ });
606
+ }
607
+ }
608
+ if (!isNew) {
609
+ backupPath = backupPathForTarget(relPath, originalHash);
610
+ copyFileSync(absPath, backupPath);
611
+ }
612
+ swapTargetContent(absPath, patchedContent);
613
+ appendSourceEditEvent({
614
+ event: "SOURCE_EDIT_COMMITTED",
615
+ target_path: relPath,
616
+ lock_path: lease.lockPath,
617
+ waited_ms: lease.waitedMs,
618
+ base_hash: originalHash,
619
+ committed_hash: newHash,
620
+ });
621
+ return {
622
+ ok: true,
623
+ original_path: absPath,
624
+ staging_path: stagingDir,
625
+ backup_path: backupPath,
626
+ original_hash: originalHash,
627
+ new_hash: newHash,
628
+ validation_passed: check.validation_passed,
629
+ validation_output: check.validation_output,
630
+ test_passed: check.test_passed,
631
+ test_output: check.test_output,
632
+ lease_lock_path: lease.lockPath,
633
+ lease_waited_ms: lease.waitedMs,
634
+ };
635
+ });
636
+ }
637
+ catch (error) {
638
+ if (error instanceof SourceEditLeaseBusyError) {
639
+ return leaseBusyResult({
640
+ absPath,
641
+ stagingDir,
642
+ originalHash,
643
+ newHash,
644
+ error,
645
+ validationCheck: check,
646
+ });
647
+ }
648
+ throw error;
359
649
  }
360
- swapTargetContent(absPath, patchedContent);
361
- return {
362
- ok: true,
363
- original_path: absPath,
364
- staging_path: stagingDir,
365
- backup_path: backupPath,
366
- original_hash: originalHash,
367
- new_hash: newHash,
368
- validation_passed: check.validation_passed,
369
- validation_output: check.validation_output,
370
- test_passed: check.test_passed,
371
- test_output: check.test_output,
372
- };
373
650
  }
374
651
  finally {
375
652
  try {
package/dist/server.d.ts CHANGED
@@ -1,6 +1,6 @@
1
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  export declare const ACE_MCP_SERVER_NAME = "ace-swarm";
3
- export declare const ACE_MCP_SERVER_VERSION = "0.2.1";
3
+ export declare const ACE_MCP_SERVER_VERSION: string;
4
4
  export interface CreateAceServerOptions {
5
5
  toolGovernance?: boolean;
6
6
  instructions?: string;
@@ -8,6 +8,15 @@ export interface CreateAceServerOptions {
8
8
  mode?: "public" | "hermes_shadow";
9
9
  }
10
10
  export declare function createAceServer(options?: CreateAceServerOptions): McpServer;
11
- export declare function startStdioServer(logStartup?: boolean): Promise<void>;
11
+ export type AceHostProfile = "host-min" | "full";
12
+ /**
13
+ * Curated premium-host surface (report WS-G). ~15–25 high-value tools instead of the
14
+ * full tool surface, cutting the per-session schema tax. Everything omitted here remains reachable
15
+ * via `run_orchestrator` delegation or by starting with `--profile full`. Goal-engine
16
+ * tools (start_goal/…) are intentionally absent until WS-F ships.
17
+ */
18
+ export declare const HOST_MIN_TOOL_PROFILE: readonly string[];
19
+ export declare function resolveHostProfileAllowlist(profile: AceHostProfile): readonly string[] | undefined;
20
+ export declare function startStdioServer(logStartup?: boolean, profile?: AceHostProfile): Promise<void>;
12
21
  export declare function startHermesShadowStdioServer(toolAllowlist: readonly string[], logStartup?: boolean): Promise<void>;
13
22
  //# sourceMappingURL=server.d.ts.map
package/dist/server.js CHANGED
@@ -1,7 +1,8 @@
1
1
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
2
2
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3
- import { existsSync, mkdirSync, writeFileSync } from "node:fs";
3
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
4
4
  import { dirname, join } from "node:path";
5
+ import { fileURLToPath } from "node:url";
5
6
  import { buildServerInstructions, installAceToolGovernance, } from "./ace-server-instructions.js";
6
7
  import { resolveWorkspaceRoot } from "./helpers.js";
7
8
  import { openStore } from "./store/ace-packed-store.js";
@@ -11,7 +12,21 @@ import { registerResources } from "./resources.js";
11
12
  import { registerTools } from "./tools.js";
12
13
  import { backfillHandoffsIntoScheduler } from "./tools-handoff.js";
13
14
  export const ACE_MCP_SERVER_NAME = "ace-swarm";
14
- export const ACE_MCP_SERVER_VERSION = "0.2.1";
15
+ // Report the true package version to MCP clients instead of a stale hard-coded string.
16
+ function resolvePackageVersion() {
17
+ try {
18
+ // dist/server.js → package root is one level up from dist/.
19
+ const pkgPath = join(dirname(fileURLToPath(import.meta.url)), "..", "package.json");
20
+ const parsed = JSON.parse(readFileSync(pkgPath, "utf-8"));
21
+ if (typeof parsed.version === "string" && parsed.version.length > 0)
22
+ return parsed.version;
23
+ }
24
+ catch {
25
+ // fall through to the compiled fallback
26
+ }
27
+ return "0.0.0";
28
+ }
29
+ export const ACE_MCP_SERVER_VERSION = resolvePackageVersion();
15
30
  const HERMES_SHADOW_INSTRUCTIONS = "ACE filtered shadow MCP tool transport for Hermes-local execution. Only tools allowed by the active ACE capability snapshot are registered; public ACE prompts and resources are not exposed.";
16
31
  export function createAceServer(options = {}) {
17
32
  const workspaceRoot = resolveWorkspaceRoot();
@@ -77,7 +92,40 @@ async function verifyMcpStoreStartup(workspaceRoot, logStartup) {
77
92
  console.error(message);
78
93
  return false;
79
94
  }
80
- export async function startStdioServer(logStartup = true) {
95
+ /**
96
+ * Curated premium-host surface (report WS-G). ~15–25 high-value tools instead of the
97
+ * full tool surface, cutting the per-session schema tax. Everything omitted here remains reachable
98
+ * via `run_orchestrator` delegation or by starting with `--profile full`. Goal-engine
99
+ * tools (start_goal/…) are intentionally absent until WS-F ships.
100
+ */
101
+ export const HOST_MIN_TOOL_PROFILE = [
102
+ "recall_context",
103
+ "route_task",
104
+ "run_orchestrator",
105
+ "propose_plan",
106
+ "validate_plan",
107
+ "create_handoff",
108
+ "ack_handoff",
109
+ "get_handoff_status",
110
+ "execute_gates",
111
+ "emit_status_event",
112
+ "append_vericify_process_post",
113
+ "get_vericify_context",
114
+ "get_vericify_delta",
115
+ "astgrep_locate",
116
+ "compile_structural_edit",
117
+ "preview_structural_edit",
118
+ "astgrep_rewrite",
119
+ "safe_edit_file",
120
+ "run_tests",
121
+ "run_local_model",
122
+ "context_snapshot",
123
+ "build_continuity_packet",
124
+ ];
125
+ export function resolveHostProfileAllowlist(profile) {
126
+ return profile === "host-min" ? HOST_MIN_TOOL_PROFILE : undefined;
127
+ }
128
+ export async function startStdioServer(logStartup = true, profile = "full") {
81
129
  const workspaceRoot = resolveWorkspaceRoot();
82
130
  if (!(await verifyMcpStoreStartup(workspaceRoot, logStartup))) {
83
131
  return;
@@ -93,11 +141,13 @@ export async function startStdioServer(logStartup = true) {
93
141
  console.error(`Scheduler backfill warning: ${error instanceof Error ? error.message : String(error)}`);
94
142
  }
95
143
  }
96
- const server = createAceServer();
144
+ const toolAllowlist = resolveHostProfileAllowlist(profile);
145
+ const server = createAceServer({ toolAllowlist });
97
146
  const transport = new StdioServerTransport();
98
147
  await server.connect(transport);
99
148
  if (logStartup) {
100
- console.error(`ACE MCP Server (${ACE_MCP_SERVER_NAME}) running on stdio`);
149
+ console.error(`ACE MCP Server (${ACE_MCP_SERVER_NAME}) running on stdio` +
150
+ (toolAllowlist ? ` (profile=${profile}, ${toolAllowlist.length} tools)` : ` (profile=${profile})`));
101
151
  }
102
152
  }
103
153
  export async function startHermesShadowStdioServer(toolAllowlist, logStartup = false) {