@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.
- package/CHANGELOG.md +69 -0
- package/README.md +72 -532
- package/assets/eval-battery/battery.json +85 -0
- package/assets/eval-battery/fixtures/edit-target.js +3 -0
- package/assets/scripts/render-mcp-configs.sh +12 -0
- package/dist/ace-context.d.ts +18 -0
- package/dist/ace-context.js +161 -16
- package/dist/ace-server-instructions.js +41 -12
- package/dist/bridge-trace.d.ts +17 -0
- package/dist/bridge-trace.js +23 -0
- package/dist/cli.d.ts +9 -1
- package/dist/cli.js +623 -11
- package/dist/discovery-runtime-wrappers.d.ts +16 -1
- package/dist/discovery-runtime-wrappers.js +65 -9
- package/dist/eval-harness.d.ts +110 -0
- package/dist/eval-harness.js +254 -0
- package/dist/experience-reflector.d.ts +13 -0
- package/dist/experience-reflector.js +62 -0
- package/dist/goal-engine.d.ts +155 -0
- package/dist/goal-engine.js +795 -0
- package/dist/helpers/store-resolution.js +40 -39
- package/dist/hermes/session-manager.d.ts +10 -0
- package/dist/hermes/session-manager.js +130 -27
- package/dist/job-scheduler.d.ts +58 -0
- package/dist/job-scheduler.js +202 -2
- package/dist/job-worker.d.ts +87 -0
- package/dist/job-worker.js +182 -0
- package/dist/local-model-policy.d.ts +8 -2
- package/dist/local-model-policy.js +31 -17
- package/dist/local-model-runtime.d.ts +6 -0
- package/dist/local-model-runtime.js +130 -2
- package/dist/measured-classing.d.ts +66 -0
- package/dist/measured-classing.js +165 -0
- package/dist/model-bridge.d.ts +45 -0
- package/dist/model-bridge.js +502 -44
- package/dist/orchestrator-supervisor.js +1 -1
- package/dist/plan-proposal.d.ts +7 -0
- package/dist/plan-proposal.js +74 -4
- package/dist/playbook-adaptation.d.ts +37 -0
- package/dist/playbook-adaptation.js +159 -0
- package/dist/playbook-curator.d.ts +16 -0
- package/dist/playbook-curator.js +106 -0
- package/dist/playbook-llm-reflector.d.ts +26 -0
- package/dist/playbook-llm-reflector.js +157 -0
- package/dist/public-surface.d.ts +2 -0
- package/dist/public-surface.js +26 -14
- package/dist/safe-edit.d.ts +20 -0
- package/dist/safe-edit.js +355 -78
- package/dist/server.d.ts +11 -2
- package/dist/server.js +55 -5
- package/dist/shared.d.ts +12 -0
- package/dist/shared.js +13 -1
- package/dist/source-mutation-coordinator.d.ts +54 -0
- package/dist/source-mutation-coordinator.js +259 -0
- package/dist/status-events.d.ts +4 -1
- package/dist/status-events.js +41 -6
- package/dist/store/ace-packed-store.d.ts +28 -9
- package/dist/store/ace-packed-store.js +324 -57
- package/dist/store/bgzf.d.ts +4 -0
- package/dist/store/bgzf.js +94 -0
- package/dist/store/bootstrap-store.d.ts +4 -1
- package/dist/store/bootstrap-store.js +57 -3
- package/dist/store/cache-workspace.d.ts +8 -0
- package/dist/store/cache-workspace.js +67 -1
- package/dist/store/crc32.d.ts +2 -0
- package/dist/store/crc32.js +16 -0
- package/dist/store/fsck.d.ts +16 -0
- package/dist/store/fsck.js +240 -0
- package/dist/store/materializers/context-snapshot-materializer.d.ts +1 -0
- package/dist/store/materializers/context-snapshot-materializer.js +4 -1
- package/dist/store/materializers/projection-manager.d.ts +7 -0
- package/dist/store/materializers/projection-manager.js +37 -0
- package/dist/store/materializers/scheduler-projection-materializer.js +10 -3
- package/dist/store/repositories/goal-repository.d.ts +112 -0
- package/dist/store/repositories/goal-repository.js +157 -0
- package/dist/store/repositories/playbook-repository.d.ts +107 -0
- package/dist/store/repositories/playbook-repository.js +314 -0
- package/dist/store/store-artifacts.js +44 -2
- package/dist/store/store-authority-audit.js +1 -0
- package/dist/store/store-cli.d.ts +30 -0
- package/dist/store/store-cli.js +242 -0
- package/dist/store/store-snapshot.d.ts +12 -0
- package/dist/store/store-snapshot.js +74 -11
- package/dist/store/types.d.ts +1 -0
- package/dist/store/write-coordinator.d.ts +7 -0
- package/dist/store/write-coordinator.js +11 -0
- package/dist/todo-state.d.ts +21 -0
- package/dist/todo-state.js +150 -53
- package/dist/tools-agent.js +86 -22
- package/dist/tools-discovery.js +5 -3
- package/dist/tools-files.js +39 -18
- package/dist/tools-framework.js +31 -3
- package/dist/tools-goal.d.ts +11 -0
- package/dist/tools-goal.js +108 -0
- package/dist/tools-handoff.js +7 -11
- package/dist/tools-lifecycle.js +2 -0
- package/dist/tools-scheduler.js +44 -1
- package/dist/tools-skills.js +3 -2
- package/dist/tools-todo.js +64 -3
- package/dist/tools.js +2 -0
- package/dist/tui/chat.js +4 -0
- package/dist/tui/index.js +4 -1
- package/dist/tui/ollama.d.ts +2 -0
- package/dist/tui/openai-compatible.d.ts +14 -0
- package/dist/tui/openai-compatible.js +15 -2
- package/dist/tui/provider-discovery.d.ts +1 -0
- package/dist/tui/provider-discovery.js +2 -2
- package/docs/cli-reference.md +127 -0
- package/docs/development.md +46 -0
- package/docs/evaluation-safety.md +27 -0
- package/docs/experience-playbook.md +35 -0
- package/docs/hermes.md +129 -0
- package/docs/library-api.md +31 -0
- package/docs/mcp-tool-reference.md +290 -0
- package/docs/provider-security.md +85 -0
- package/docs/state-model.md +113 -0
- package/docs/troubleshooting.md +95 -0
- package/docs/tui-operator-guide.md +75 -0
- package/package.json +3 -1
- 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
|
-
|
|
38
|
-
|
|
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
|
-
|
|
124
|
-
|
|
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 (
|
|
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
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
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
|
-
|
|
225
|
-
|
|
226
|
-
|
|
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
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
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
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
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
|
-
|
|
256
|
-
|
|
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:
|
|
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
|
|
315
|
-
const
|
|
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 (
|
|
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
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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) {
|