@voybio/ace-swarm 2.4.8 → 2.4.10
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 +52 -14
- package/README.md +14 -10
- package/assets/.agents/ACE/ACE_coders/AGENTS.md +23 -30
- package/assets/.agents/ACE/ACE_coders/INSTRUCTIONS.md +21 -30
- package/assets/.agents/skills/landing-review-watcher/SKILL.md +1 -1
- package/assets/.agents/skills/learn-from-work/SKILL.md +41 -0
- package/assets/agent-state/ACE_WORKFLOW.md +25 -2
- package/assets/agent-state/MODULES/schemas/ACE_RUNTIME_PROFILE.schema.json +143 -2
- package/assets/agent-state/MODULES/schemas/RUNTIME_EXECUTOR_SESSION_REGISTRY.schema.json +2 -2
- package/assets/agent-state/MODULES/schemas/SWARM_HANDOFF.schema.json +10 -0
- package/assets/eval-battery/battery.json +1 -1
- package/assets/instructions/ACE_Coder.instructions.md +9 -13
- package/assets/scripts/ace-hook-dispatch.mjs +16 -411
- package/assets/tasks/SWARM_HANDOFF.example_ui_to_coders.json +1 -2
- package/assets/vendor/README.md +9 -0
- package/assets/vendor/vericify-reader.mjs +272 -0
- package/assets/vendor/vericify-reader.provenance.json +10 -0
- package/dist/ace-autonomy.d.ts +58 -0
- package/dist/ace-autonomy.js +211 -6
- package/dist/ace-internal-tools.d.ts +1 -0
- package/dist/ace-internal-tools.js +3 -3
- package/dist/ace-server-instructions.d.ts +3 -3
- package/dist/ace-server-instructions.js +46 -192
- package/dist/cli.js +44 -6
- package/dist/experience-reflector.d.ts +7 -0
- package/dist/experience-reflector.js +39 -0
- package/dist/goal-engine.d.ts +64 -7
- package/dist/goal-engine.js +197 -41
- package/dist/handoff-registry.d.ts +10 -0
- package/dist/handoff-registry.js +59 -0
- package/dist/helpers/artifacts.d.ts +6 -2
- package/dist/helpers/artifacts.js +8 -6
- package/dist/helpers/bootstrap.js +13 -20
- package/dist/helpers/path-utils.js +2 -0
- package/dist/helpers/store-resolution.js +18 -3
- package/dist/helpers/workspace-root.d.ts +2 -0
- package/dist/helpers/workspace-root.js +9 -0
- package/dist/helpers.d.ts +1 -0
- package/dist/helpers.js +1 -0
- package/dist/hermes/bridge-protocol.d.ts +1 -1
- package/dist/hermes/session-manager.d.ts +17 -0
- package/dist/hermes/session-manager.js +67 -14
- package/dist/host-goal-engine.d.ts +138 -0
- package/dist/host-goal-engine.js +793 -0
- package/dist/host-goal-status.d.ts +113 -0
- package/dist/host-goal-status.js +175 -0
- package/dist/host-learning.d.ts +68 -0
- package/dist/host-learning.js +115 -0
- package/dist/host-observation.d.ts +12 -0
- package/dist/host-observation.js +91 -0
- package/dist/host-plan-revision.d.ts +14 -0
- package/dist/host-plan-revision.js +101 -0
- package/dist/host-verification-job.d.ts +12 -0
- package/dist/host-verification-job.js +183 -0
- package/dist/host-work-context.d.ts +12 -0
- package/dist/host-work-context.js +8 -0
- package/dist/job-scheduler.d.ts +58 -13
- package/dist/job-scheduler.js +298 -48
- package/dist/job-worker.d.ts +3 -1
- package/dist/job-worker.js +10 -3
- package/dist/kanban.d.ts +4 -1
- package/dist/kanban.js +44 -16
- package/dist/learned-artifacts.d.ts +64 -0
- package/dist/learned-artifacts.js +321 -0
- package/dist/learning-controls.d.ts +142 -0
- package/dist/learning-controls.js +135 -0
- package/dist/learning-input-context.d.ts +9 -0
- package/dist/learning-input-context.js +62 -0
- package/dist/learning-lifecycle.d.ts +10 -0
- package/dist/learning-lifecycle.js +11 -0
- package/dist/learning-overlays.d.ts +3 -0
- package/dist/learning-overlays.js +17 -0
- package/dist/learning-policy.d.ts +40 -0
- package/dist/learning-policy.js +38 -0
- package/dist/learning-records.d.ts +35 -0
- package/dist/learning-records.js +17 -0
- package/dist/learning-scorecard.d.ts +82 -0
- package/dist/learning-scorecard.js +86 -0
- package/dist/learning-serving.d.ts +29 -0
- package/dist/learning-serving.js +134 -0
- package/dist/linked-work.d.ts +9 -0
- package/dist/linked-work.js +28 -0
- package/dist/local-model-runtime.js +1 -1
- package/dist/mcp-command-progress.d.ts +16 -0
- package/dist/mcp-command-progress.js +70 -0
- package/dist/model-bridge.d.ts +14 -13
- package/dist/model-bridge.js +73 -41
- package/dist/orchestrator-supervisor.d.ts +111 -6
- package/dist/orchestrator-supervisor.js +67 -43
- package/dist/plan-proposal.js +3 -3
- package/dist/playbook-curator.d.ts +1 -0
- package/dist/playbook-curator.js +11 -9
- package/dist/process-identity.d.ts +8 -0
- package/dist/process-identity.js +38 -0
- package/dist/progress-command.d.ts +34 -0
- package/dist/progress-command.js +153 -0
- package/dist/prompts.js +15 -15
- package/dist/resources.js +2 -2
- package/dist/runtime-command.d.ts +8 -1
- package/dist/runtime-command.js +48 -7
- package/dist/runtime-executor.js +33 -11
- package/dist/runtime-profile-syntax.d.ts +6 -0
- package/dist/runtime-profile-syntax.js +121 -0
- package/dist/runtime-profile.d.ts +37 -5
- package/dist/runtime-profile.js +36 -129
- package/dist/runtime-tool-specs.js +1 -1
- package/dist/safe-edit.d.ts +7 -3
- package/dist/safe-edit.js +40 -47
- package/dist/schemas.js +4 -2
- package/dist/server.d.ts +2 -16
- package/dist/server.js +14 -20
- package/dist/shared.d.ts +2 -0
- package/dist/shared.js +2 -0
- package/dist/skill-auditor.d.ts +19 -0
- package/dist/skill-auditor.js +32 -2
- package/dist/skill-catalog.d.ts +4 -1
- package/dist/skill-catalog.js +6 -5
- package/dist/store/ace-packed-store.d.ts +5 -1
- package/dist/store/ace-packed-store.js +33 -1
- package/dist/store/bootstrap-store.js +19 -12
- package/dist/store/cache-workspace.d.ts +8 -0
- package/dist/store/cache-workspace.js +94 -27
- package/dist/store/importer.js +11 -20
- package/dist/store/knowledge-bake.d.ts +3 -1
- package/dist/store/knowledge-bake.js +10 -1
- package/dist/store/legacy-vericify-posts.d.ts +8 -0
- package/dist/store/legacy-vericify-posts.js +76 -0
- package/dist/store/materializers/hook-context-materializer.d.ts +2 -1
- package/dist/store/materializers/hook-context-materializer.js +6 -2
- package/dist/store/materializers/host-file-materializer.js +2 -2
- package/dist/store/materializers/projection-manager.d.ts +4 -1
- package/dist/store/materializers/projection-manager.js +10 -3
- package/dist/store/materializers/todo-syncer.d.ts +39 -20
- package/dist/store/materializers/todo-syncer.js +208 -110
- package/dist/store/materializers/vericify-projector.js +15 -26
- package/dist/store/repositories/goal-repository.d.ts +51 -2
- package/dist/store/repositories/goal-repository.js +114 -48
- package/dist/store/repositories/playbook-repository.d.ts +41 -3
- package/dist/store/repositories/playbook-repository.js +61 -5
- package/dist/store/repositories/todo-repository.d.ts +8 -2
- package/dist/store/repositories/todo-repository.js +1 -1
- package/dist/store/repositories/vericify-repository.d.ts +4 -0
- package/dist/store/repositories/vericify-repository.js +46 -0
- package/dist/store/skills-install.d.ts +1 -0
- package/dist/store/skills-install.js +20 -16
- package/dist/store/source-preservation.d.ts +24 -0
- package/dist/store/source-preservation.js +79 -0
- package/dist/store/store-artifacts.js +8 -1
- package/dist/store/test-write-boundary.d.ts +16 -0
- package/dist/store/test-write-boundary.js +175 -0
- package/dist/store/types.d.ts +5 -1
- package/dist/store/write-coordinator.js +16 -0
- package/dist/task-plan-context.d.ts +11 -0
- package/dist/task-plan-context.js +59 -0
- package/dist/todo-state.d.ts +19 -9
- package/dist/todo-state.js +204 -321
- package/dist/tools-agent.d.ts +1 -1
- package/dist/tools-agent.js +38 -62
- package/dist/tools-files.d.ts +0 -3
- package/dist/tools-files.js +21 -11
- package/dist/tools-framework.js +16 -25
- package/dist/tools-goal.d.ts +2 -4
- package/dist/tools-goal.js +119 -16
- package/dist/tools-handoff.d.ts +3 -2
- package/dist/tools-handoff.js +53 -23
- package/dist/tools-lifecycle.js +1 -1
- package/dist/tools-memory.d.ts +0 -3
- package/dist/tools-memory.js +51 -6
- package/dist/tools-scheduler.js +20 -17
- package/dist/tools-todo.js +160 -148
- package/dist/vericify-bridge.d.ts +8 -0
- package/dist/vericify-bridge.js +18 -0
- package/dist/vericify-context.d.ts +4 -0
- package/dist/vericify-context.js +26 -5
- package/dist/vericify-native.d.ts +101 -0
- package/dist/vericify-native.js +221 -0
- package/dist/verification-worker.d.ts +2 -0
- package/dist/verification-worker.js +31 -0
- package/docs/mcp-tool-reference.md +50 -10
- package/docs/troubleshooting.md +29 -0
- package/package.json +19 -6
- package/scripts/dev/run-tests-isolated.mjs +165 -0
- package/scripts/dev/run_hermes_crispr_live.mjs +1 -0
- package/scripts/dev/test-isolation-guard.mjs +20 -0
- package/scripts/dev/test-preload.mjs +153 -0
- package/scripts/dev/test-source-inventory.mjs +31 -0
- package/docs/development.md +0 -46
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { closeSync, constants, fstatSync, lstatSync, openSync, readFileSync, realpathSync } from "node:fs";
|
|
3
|
+
import { dirname, isAbsolute, relative } from "node:path";
|
|
4
|
+
import { readStoreJsonByPathSync } from "./store-snapshot.js";
|
|
5
|
+
export function sourceFromBytes(sourcePath, bytes) {
|
|
6
|
+
return {
|
|
7
|
+
source_path: sourcePath.replaceAll("\\", "/"),
|
|
8
|
+
sha256: createHash("sha256").update(bytes).digest("hex"),
|
|
9
|
+
byte_length: bytes.length,
|
|
10
|
+
content_base64: bytes.toString("base64"),
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
function identity(stat) {
|
|
14
|
+
const { dev, ino, size, mtimeMs, ctimeMs } = stat;
|
|
15
|
+
return { dev, ino, size, mtimeMs, ctimeMs };
|
|
16
|
+
}
|
|
17
|
+
function sameIdentity(a, b) {
|
|
18
|
+
return Object.entries(a).every(([key, value]) => b[key] === value);
|
|
19
|
+
}
|
|
20
|
+
/** Capture exact bytes without following a replaced final symlink or accepting an unstable read. */
|
|
21
|
+
export function captureWorkspaceSource(root, path) {
|
|
22
|
+
const parent = relative(realpathSync(root), realpathSync(dirname(path)));
|
|
23
|
+
if (parent.startsWith("..") || isAbsolute(parent))
|
|
24
|
+
throw new Error(`Source outside workspace: ${path}`);
|
|
25
|
+
const fd = openSync(path, constants.O_RDONLY | constants.O_NOFOLLOW);
|
|
26
|
+
try {
|
|
27
|
+
const before = fstatSync(fd);
|
|
28
|
+
if (!before.isFile())
|
|
29
|
+
throw new Error(`Source is not a regular file: ${path}`);
|
|
30
|
+
const bytes = readFileSync(fd);
|
|
31
|
+
const after = fstatSync(fd);
|
|
32
|
+
const current = lstatSync(path);
|
|
33
|
+
if (current.isSymbolicLink() || !sameIdentity(identity(before), identity(after)) || !sameIdentity(identity(after), identity(current))) {
|
|
34
|
+
throw new Error(`Source changed during read: ${path}`);
|
|
35
|
+
}
|
|
36
|
+
return { ...sourceFromBytes(relative(root, path), bytes), identity: identity(after) };
|
|
37
|
+
}
|
|
38
|
+
finally {
|
|
39
|
+
closeSync(fd);
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
export function sourceUnchanged(root, path, source) {
|
|
43
|
+
try {
|
|
44
|
+
const current = captureWorkspaceSource(root, path);
|
|
45
|
+
return current.sha256 === source.sha256 && !!source.identity && !!current.identity && sameIdentity(source.identity, current.identity);
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
export function preservedSourceKey(source) {
|
|
52
|
+
const pathHash = createHash("sha256").update(source.source_path).digest("hex");
|
|
53
|
+
return `state/migrations/sources/${pathHash}/${source.sha256}`;
|
|
54
|
+
}
|
|
55
|
+
export async function stagePreservedSource(store, source) {
|
|
56
|
+
const key = preservedSourceKey(source);
|
|
57
|
+
const existing = await store.getJSON(key);
|
|
58
|
+
if (existing) {
|
|
59
|
+
if (existing.source_path !== source.source_path || existing.content_base64 !== source.content_base64 || existing.sha256 !== source.sha256) {
|
|
60
|
+
throw new Error(`Preserved-source collision: ${source.source_path}`);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
else {
|
|
64
|
+
await store.setJSON(key, source);
|
|
65
|
+
}
|
|
66
|
+
return key;
|
|
67
|
+
}
|
|
68
|
+
/** Uses the committed-file reader, never the writer's pending values. */
|
|
69
|
+
export function verifyPreservedSource(storePath, source) {
|
|
70
|
+
const retained = readStoreJsonByPathSync(storePath, preservedSourceKey(source));
|
|
71
|
+
if (!retained || retained.source_path !== source.source_path || retained.byte_length !== source.byte_length || retained.sha256 !== source.sha256 || retained.content_base64 !== source.content_base64) {
|
|
72
|
+
throw new Error(`Exact-source readback failed: ${source.source_path}`);
|
|
73
|
+
}
|
|
74
|
+
const bytes = Buffer.from(retained.content_base64, "base64");
|
|
75
|
+
if (bytes.length !== source.byte_length || sourceFromBytes(source.source_path, bytes).sha256 !== source.sha256) {
|
|
76
|
+
throw new Error(`Exact-source digest mismatch: ${source.source_path}`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
//# sourceMappingURL=source-preservation.js.map
|
|
@@ -5,6 +5,7 @@ import { getWorkspaceStorePath, invalidateStoreSnapshotCache, readStoreBlobSync,
|
|
|
5
5
|
import { withStoreWriteCoordinator } from "./write-coordinator.js";
|
|
6
6
|
import { HEADER_SIZE, HEADER_SIZE_V1, STORE_VERSION, MAGIC } from "./types.js";
|
|
7
7
|
import { crc32 } from "./crc32.js";
|
|
8
|
+
import { assertTestWriteAllowed } from "./test-write-boundary.js";
|
|
8
9
|
// ── v2 header constants (mirrors ace-packed-store.ts) ─────────────────────────
|
|
9
10
|
const H_KV_IDX_OFF = 16;
|
|
10
11
|
const H_KV_IDX_LEN = 24;
|
|
@@ -146,6 +147,7 @@ function validateStoreHeaderInvariants(hdr, index, fileLength) {
|
|
|
146
147
|
}
|
|
147
148
|
function quarantineCorruptStore(storePath, rawFile) {
|
|
148
149
|
const quarantinePath = `${storePath}.corrupt-${Date.now()}`;
|
|
150
|
+
assertTestWriteAllowed(quarantinePath);
|
|
149
151
|
writeFileSync(quarantinePath, rawFile);
|
|
150
152
|
return quarantinePath;
|
|
151
153
|
}
|
|
@@ -194,6 +196,7 @@ export function operationalArtifactVirtualPath(workspaceRoot, relPath) {
|
|
|
194
196
|
}
|
|
195
197
|
export function writeStoreBlobsSync(workspaceRoot, blobs) {
|
|
196
198
|
const storePath = getWorkspaceStorePath(workspaceRoot);
|
|
199
|
+
assertTestWriteAllowed(storePath);
|
|
197
200
|
const { file, kvChunkEnd, evtBytes, evtCount, evtBaseId, index } = readStoreBuffer(storePath);
|
|
198
201
|
const encoded = blobs.map(({ key, content }) => ({
|
|
199
202
|
key,
|
|
@@ -255,8 +258,12 @@ export async function writeStoreBlobsQueued(workspaceRoot, blobs, options = {})
|
|
|
255
258
|
await store.commit();
|
|
256
259
|
return blobs.map(({ key }) => toVirtualStorePath(storePath, key));
|
|
257
260
|
}
|
|
261
|
+
catch (error) {
|
|
262
|
+
store.discardPending();
|
|
263
|
+
throw error;
|
|
264
|
+
}
|
|
258
265
|
finally {
|
|
259
|
-
await store.close();
|
|
266
|
+
await store.close({ commit: false });
|
|
260
267
|
}
|
|
261
268
|
}, { ...options, operation_label: options.operation_label ?? "writeStoreBlobsQueued" });
|
|
262
269
|
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export interface TestWriteAnchor {
|
|
2
|
+
path: string;
|
|
3
|
+
dev: string;
|
|
4
|
+
ino: string;
|
|
5
|
+
}
|
|
6
|
+
export declare class TestWriteBoundaryError extends Error {
|
|
7
|
+
readonly code = "ACE_TEST_WRITE_BOUNDARY";
|
|
8
|
+
constructor(message: string);
|
|
9
|
+
}
|
|
10
|
+
export declare function assertTestWriteAllowed(path: string): void;
|
|
11
|
+
/** Pin a store handle to the directory that actually existed when it opened. */
|
|
12
|
+
export declare function captureTestWriteBoundary(path: string): () => void;
|
|
13
|
+
/** Retain fixture ownership across promises and timers created by a mutation. */
|
|
14
|
+
export declare function withTestWriteBoundary<T>(path: string, fn: () => T): T;
|
|
15
|
+
export declare function currentTestWriteAnchors(): readonly TestWriteAnchor[];
|
|
16
|
+
//# sourceMappingURL=test-write-boundary.d.ts.map
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
+
import { lstatSync, readFileSync, realpathSync, statSync } from "node:fs";
|
|
3
|
+
import { basename, dirname, isAbsolute, join, relative } from "node:path";
|
|
4
|
+
export class TestWriteBoundaryError extends Error {
|
|
5
|
+
code = "ACE_TEST_WRITE_BOUNDARY";
|
|
6
|
+
constructor(message) {
|
|
7
|
+
super(`Test write refused: ${message}`);
|
|
8
|
+
this.name = "TestWriteBoundaryError";
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
const contexts = new AsyncLocalStorage();
|
|
12
|
+
const boundarySymbol = Symbol.for("ace.test.write-boundary");
|
|
13
|
+
// Node's JS realpath implementation lexically normalizes `..` before symlinks.
|
|
14
|
+
// The native resolver follows the same traversal as an eventual filesystem write.
|
|
15
|
+
const canonicalRealpath = realpathSync.native;
|
|
16
|
+
function testContextPresent() {
|
|
17
|
+
return process.env.ACE_TEST_ISOLATION !== undefined ||
|
|
18
|
+
process.env.ACE_TEST_ALLOWED_ROOTS !== undefined ||
|
|
19
|
+
process.env.ACE_TEST_WORKER_ROOT !== undefined ||
|
|
20
|
+
process.env.NODE_TEST_CONTEXT !== undefined;
|
|
21
|
+
}
|
|
22
|
+
function anchor(path) {
|
|
23
|
+
const canonical = canonicalRealpath(path);
|
|
24
|
+
const info = statSync(canonical, { bigint: true });
|
|
25
|
+
if (!info.isDirectory())
|
|
26
|
+
throw new Error("allocated root must be a directory");
|
|
27
|
+
return { path: canonical, dev: String(info.dev), ino: String(info.ino) };
|
|
28
|
+
}
|
|
29
|
+
function parseAnchors(raw) {
|
|
30
|
+
if (!raw)
|
|
31
|
+
return [];
|
|
32
|
+
const value = JSON.parse(raw);
|
|
33
|
+
if (!Array.isArray(value) || value.some((item) => !item || typeof item !== "object" ||
|
|
34
|
+
typeof item.path !== "string" || !isAbsolute(item.path) || typeof item.dev !== "string" || typeof item.ino !== "string")) {
|
|
35
|
+
throw new Error("invalid inherited fixture identities");
|
|
36
|
+
}
|
|
37
|
+
return value;
|
|
38
|
+
}
|
|
39
|
+
function captureConfiguration() {
|
|
40
|
+
if (!testContextPresent())
|
|
41
|
+
return { active: false, roots: [], inherited: [] };
|
|
42
|
+
try {
|
|
43
|
+
if (process.env.ACE_TEST_ISOLATION !== "1")
|
|
44
|
+
throw new Error("isolated runner marker is missing");
|
|
45
|
+
const paths = JSON.parse(process.env.ACE_TEST_ALLOWED_ROOTS ?? "null");
|
|
46
|
+
if (!Array.isArray(paths) || paths.length === 0 || paths.some((path) => typeof path !== "string" || !isAbsolute(path))) {
|
|
47
|
+
throw new Error("allocated fixture roots are missing or invalid");
|
|
48
|
+
}
|
|
49
|
+
if (!process.env.ACE_TEST_LEASE_FILE || !isAbsolute(process.env.ACE_TEST_LEASE_FILE) || !process.env.ACE_TEST_RUN_ID) {
|
|
50
|
+
throw new Error("fixture lease identity is missing");
|
|
51
|
+
}
|
|
52
|
+
return { active: true, roots: paths.map(anchor), inherited: parseAnchors(process.env.ACE_TEST_WRITE_ANCHORS), lease: process.env.ACE_TEST_LEASE_FILE, run: process.env.ACE_TEST_RUN_ID };
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
return { active: true, roots: [], inherited: [], error: error instanceof Error ? error.message : String(error) };
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
// Capture once before application loading: later environment changes cannot
|
|
59
|
+
// disable the test boundary or turn an outside workspace into an allowed root.
|
|
60
|
+
let configuration = captureConfiguration();
|
|
61
|
+
function validateAnchor(value) {
|
|
62
|
+
try {
|
|
63
|
+
const current = anchor(value.path);
|
|
64
|
+
if (current.path === value.path && current.dev === value.dev && current.ino === value.ino)
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
catch { /* Removed fixtures and symlink substitutions are both refused. */ }
|
|
68
|
+
throw new TestWriteBoundaryError(`allocated fixture was removed or replaced: ${value.path}`);
|
|
69
|
+
}
|
|
70
|
+
/**
|
|
71
|
+
* Resolve existing symlinks before appending a not-yet-created suffix.
|
|
72
|
+
* `parent` is the directory holding the target's entry. An existing target
|
|
73
|
+
* directory (such as a contended lock) is never its own ownership anchor:
|
|
74
|
+
* another owner may legitimately remove it.
|
|
75
|
+
*/
|
|
76
|
+
function destination(path) {
|
|
77
|
+
let cursor = isAbsolute(path) ? path : `${process.cwd()}/${path}`;
|
|
78
|
+
const suffix = [];
|
|
79
|
+
while (true) {
|
|
80
|
+
try {
|
|
81
|
+
const info = lstatSync(cursor);
|
|
82
|
+
const canonical = canonicalRealpath(cursor);
|
|
83
|
+
const parent = suffix.length > 0 && info.isDirectory() ? canonical : dirname(canonical);
|
|
84
|
+
return { canonical: join(canonical, ...suffix), parent };
|
|
85
|
+
}
|
|
86
|
+
catch (error) {
|
|
87
|
+
if (error.code !== "ENOENT")
|
|
88
|
+
throw error;
|
|
89
|
+
// A dangling symlink is an existing object, not a safe missing directory.
|
|
90
|
+
try {
|
|
91
|
+
if (lstatSync(cursor).isSymbolicLink())
|
|
92
|
+
throw new TestWriteBoundaryError(`unresolved symlink: ${cursor}`);
|
|
93
|
+
}
|
|
94
|
+
catch (probe) {
|
|
95
|
+
if (probe instanceof TestWriteBoundaryError)
|
|
96
|
+
throw probe;
|
|
97
|
+
}
|
|
98
|
+
const parent = dirname(cursor);
|
|
99
|
+
if (parent === cursor)
|
|
100
|
+
throw error;
|
|
101
|
+
suffix.unshift(basename(cursor));
|
|
102
|
+
cursor = parent;
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
function inside(root, path) {
|
|
107
|
+
const subpath = relative(root, path);
|
|
108
|
+
return subpath === "" || (!isAbsolute(subpath) && subpath !== ".." && !subpath.startsWith(`..${process.platform === "win32" ? "\\" : "/"}`));
|
|
109
|
+
}
|
|
110
|
+
export function assertTestWriteAllowed(path) {
|
|
111
|
+
// Also fail closed if a module was imported before a test context was set.
|
|
112
|
+
if (!configuration.active && testContextPresent())
|
|
113
|
+
configuration = captureConfiguration();
|
|
114
|
+
if (!configuration.active)
|
|
115
|
+
return;
|
|
116
|
+
if (configuration.error)
|
|
117
|
+
throw new TestWriteBoundaryError(configuration.error);
|
|
118
|
+
if (!globalThis[Symbol.for("ace.test.preload")]) {
|
|
119
|
+
throw new TestWriteBoundaryError("the isolated runner preloader has not established the fixture boundary");
|
|
120
|
+
}
|
|
121
|
+
try {
|
|
122
|
+
const lease = JSON.parse(readFileSync(configuration.lease, "utf8"));
|
|
123
|
+
if (lease.status !== "active" || lease.run_id !== configuration.run)
|
|
124
|
+
throw new Error("inactive lease");
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
throw new TestWriteBoundaryError("fixture lease is missing, closed, or belongs to another run");
|
|
128
|
+
}
|
|
129
|
+
for (const value of [...configuration.roots, ...configuration.inherited, ...(contexts.getStore() ?? [])])
|
|
130
|
+
validateAnchor(value);
|
|
131
|
+
let target;
|
|
132
|
+
try {
|
|
133
|
+
target = destination(path).canonical;
|
|
134
|
+
}
|
|
135
|
+
catch (error) {
|
|
136
|
+
throw error instanceof TestWriteBoundaryError ? error : new TestWriteBoundaryError(`cannot resolve destination: ${path}`);
|
|
137
|
+
}
|
|
138
|
+
if (!configuration.roots.some((root) => inside(root.path, target))) {
|
|
139
|
+
throw new TestWriteBoundaryError(`destination is outside allocated fixtures: ${path}`);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
/** Pin a store handle to the directory that actually existed when it opened. */
|
|
143
|
+
export function captureTestWriteBoundary(path) {
|
|
144
|
+
assertTestWriteAllowed(path);
|
|
145
|
+
if (!configuration.active)
|
|
146
|
+
return () => { };
|
|
147
|
+
const captured = [...(contexts.getStore() ?? []), anchor(destination(path).parent)];
|
|
148
|
+
return () => {
|
|
149
|
+
assertTestWriteAllowed(path);
|
|
150
|
+
for (const value of captured)
|
|
151
|
+
validateAnchor(value);
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
/** Retain fixture ownership across promises and timers created by a mutation. */
|
|
155
|
+
export function withTestWriteBoundary(path, fn) {
|
|
156
|
+
assertTestWriteAllowed(path);
|
|
157
|
+
if (!configuration.active)
|
|
158
|
+
return fn();
|
|
159
|
+
const captured = [...(contexts.getStore() ?? []), anchor(destination(path).parent)];
|
|
160
|
+
return contexts.run(captured, fn);
|
|
161
|
+
}
|
|
162
|
+
export function currentTestWriteAnchors() {
|
|
163
|
+
return [...configuration.inherited, ...(contexts.getStore() ?? [])];
|
|
164
|
+
}
|
|
165
|
+
// The isolated runner propagates these identities into owned subprocesses. This
|
|
166
|
+
// contains paths/inodes only, never store contents or application state.
|
|
167
|
+
if (!globalThis[boundarySymbol]) {
|
|
168
|
+
Object.defineProperty(globalThis, boundarySymbol, { value: Object.freeze({
|
|
169
|
+
serialize: () => JSON.stringify(currentTestWriteAnchors()),
|
|
170
|
+
}), configurable: false, writable: false });
|
|
171
|
+
}
|
|
172
|
+
if (!globalThis[Symbol.for("ace.test.write.anchors")]) {
|
|
173
|
+
Object.defineProperty(globalThis, Symbol.for("ace.test.write.anchors"), { get: currentTestWriteAnchors, configurable: false });
|
|
174
|
+
}
|
|
175
|
+
//# sourceMappingURL=test-write-boundary.js.map
|
package/dist/store/types.d.ts
CHANGED
|
@@ -62,7 +62,10 @@ export interface IAcePackedStore extends AsyncReadable, AsyncWritable {
|
|
|
62
62
|
open(path: string): Promise<void>;
|
|
63
63
|
commit(): Promise<void>;
|
|
64
64
|
compact(): Promise<void>;
|
|
65
|
-
|
|
65
|
+
discardPending(): void;
|
|
66
|
+
close(options?: {
|
|
67
|
+
commit?: boolean;
|
|
68
|
+
}): Promise<void>;
|
|
66
69
|
list(): AsyncGenerator<string>;
|
|
67
70
|
getJSON<T>(key: string): Promise<T | undefined>;
|
|
68
71
|
setJSON(key: string, value: unknown): Promise<void>;
|
|
@@ -130,6 +133,7 @@ export interface MaterializationChecksum {
|
|
|
130
133
|
}
|
|
131
134
|
export interface HookContextSnapshot {
|
|
132
135
|
schema_version: 1;
|
|
136
|
+
current_work?: string;
|
|
133
137
|
generated_at: string;
|
|
134
138
|
task: {
|
|
135
139
|
content: string;
|
|
@@ -4,6 +4,7 @@ import { hostname } from "node:os";
|
|
|
4
4
|
import { dirname, join } from "node:path";
|
|
5
5
|
import { getWorkspaceStorePath } from "./store-snapshot.js";
|
|
6
6
|
import { withStoreWriteQueue } from "./write-queue.js";
|
|
7
|
+
import { assertTestWriteAllowed, withTestWriteBoundary } from "./test-write-boundary.js";
|
|
7
8
|
const DEFAULT_WAIT_MS = 15_000;
|
|
8
9
|
const DEFAULT_POLL_INTERVAL_MS = 100;
|
|
9
10
|
const DEFAULT_LEASE_TTL_MS = 30_000;
|
|
@@ -203,20 +204,27 @@ function createLeaseRecord(storePath, lockPath, options, waitedMs) {
|
|
|
203
204
|
};
|
|
204
205
|
}
|
|
205
206
|
function tryAcquireLeaseSync(storePath, lockPath, options, waitedMs) {
|
|
207
|
+
assertTestWriteAllowed(storePath);
|
|
208
|
+
assertTestWriteAllowed(lockPath);
|
|
206
209
|
mkdirSync(dirname(lockPath), { recursive: true });
|
|
207
210
|
mkdirSync(lockPath, { recursive: false });
|
|
208
211
|
const lease = createLeaseRecord(storePath, lockPath, options, waitedMs);
|
|
212
|
+
assertTestWriteAllowed(lease.metadataPath);
|
|
209
213
|
writeFileSync(lease.metadataPath, JSON.stringify({ kind: "store_write_lease", ...lease }, null, 2), "utf-8");
|
|
210
214
|
return lease;
|
|
211
215
|
}
|
|
212
216
|
async function tryAcquireLeaseAsync(storePath, lockPath, options, waitedMs) {
|
|
217
|
+
assertTestWriteAllowed(storePath);
|
|
218
|
+
assertTestWriteAllowed(lockPath);
|
|
213
219
|
mkdirSync(dirname(lockPath), { recursive: true });
|
|
214
220
|
mkdirSync(lockPath, { recursive: false });
|
|
215
221
|
const lease = createLeaseRecord(storePath, lockPath, options, waitedMs);
|
|
222
|
+
assertTestWriteAllowed(lease.metadataPath);
|
|
216
223
|
writeFileSync(lease.metadataPath, JSON.stringify({ kind: "store_write_lease", ...lease }, null, 2), "utf-8");
|
|
217
224
|
return lease;
|
|
218
225
|
}
|
|
219
226
|
function releaseLease(lease) {
|
|
227
|
+
assertTestWriteAllowed(lease.lockPath);
|
|
220
228
|
const current = readLeaseRecord(lease.lockPath);
|
|
221
229
|
if (!current) {
|
|
222
230
|
rmSync(lease.lockPath, { recursive: true, force: true });
|
|
@@ -245,6 +253,7 @@ async function acquireLeaseAsync(storePath, options) {
|
|
|
245
253
|
throw error;
|
|
246
254
|
const current = readLeaseRecord(lockPath);
|
|
247
255
|
if (current && canRecoverStaleLease(current, staleGraceMs)) {
|
|
256
|
+
assertTestWriteAllowed(lockPath);
|
|
248
257
|
rmSync(lockPath, { recursive: true, force: true });
|
|
249
258
|
continue;
|
|
250
259
|
}
|
|
@@ -285,6 +294,7 @@ function acquireLeaseSync(storePath, options) {
|
|
|
285
294
|
throw error;
|
|
286
295
|
const current = readLeaseRecord(lockPath);
|
|
287
296
|
if (current && canRecoverStaleLease(current, staleGraceMs)) {
|
|
297
|
+
assertTestWriteAllowed(lockPath);
|
|
288
298
|
rmSync(lockPath, { recursive: true, force: true });
|
|
289
299
|
continue;
|
|
290
300
|
}
|
|
@@ -308,6 +318,9 @@ function acquireLeaseSync(storePath, options) {
|
|
|
308
318
|
});
|
|
309
319
|
}
|
|
310
320
|
export async function withStoreWriteCoordinator(storePath, fn, options = {}) {
|
|
321
|
+
return withTestWriteBoundary(storePath, () => withStoreWriteCoordinatorInternal(storePath, fn, options));
|
|
322
|
+
}
|
|
323
|
+
async function withStoreWriteCoordinatorInternal(storePath, fn, options) {
|
|
311
324
|
const operationLabel = getOperationLabel(options);
|
|
312
325
|
registerAsyncStoreOwnership(storePath, operationLabel);
|
|
313
326
|
emitLifecycleEvent(options, {
|
|
@@ -367,6 +380,9 @@ export async function withStoreWriteCoordinator(storePath, fn, options = {}) {
|
|
|
367
380
|
}
|
|
368
381
|
}
|
|
369
382
|
export function withStoreWriteCoordinatorSync(storePath, fn, options = {}) {
|
|
383
|
+
return withTestWriteBoundary(storePath, () => withStoreWriteCoordinatorSyncInternal(storePath, fn, options));
|
|
384
|
+
}
|
|
385
|
+
function withStoreWriteCoordinatorSyncInternal(storePath, fn, options) {
|
|
370
386
|
const operationLabel = getOperationLabel(options);
|
|
371
387
|
const asyncOwnership = getAsyncStoreOwnership(storePath);
|
|
372
388
|
if (asyncOwnership && asyncOwnership.count > 0) {
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { TaskPlan, TaskStep } from "./orchestrator-supervisor.js";
|
|
2
|
+
/** Preserve complete Unicode code points while respecting the delivered byte budget. */
|
|
3
|
+
export declare function utf8Prefix(text: string, maxBytes: number): string;
|
|
4
|
+
export declare function getReadyTaskSteps(plan: TaskPlan): TaskStep[];
|
|
5
|
+
export declare function formatStepTaskForBridge(step: Pick<TaskStep, "task" | "upstream_outputs">): string;
|
|
6
|
+
export declare function collectUpstreamOutputs(plan: TaskPlan, step: TaskStep): {
|
|
7
|
+
step_id: string;
|
|
8
|
+
result_summary: string;
|
|
9
|
+
evidence_refs: string[];
|
|
10
|
+
}[];
|
|
11
|
+
//# sourceMappingURL=task-plan-context.d.ts.map
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/** Preserve complete Unicode code points while respecting the delivered byte budget. */
|
|
2
|
+
export function utf8Prefix(text, maxBytes) {
|
|
3
|
+
let bytes = 0, result = '';
|
|
4
|
+
for (const char of text) {
|
|
5
|
+
const size = Buffer.byteLength(char);
|
|
6
|
+
if (bytes + size > maxBytes)
|
|
7
|
+
break;
|
|
8
|
+
result += char;
|
|
9
|
+
bytes += size;
|
|
10
|
+
}
|
|
11
|
+
return result;
|
|
12
|
+
}
|
|
13
|
+
// Shared pure plan readers: importing host context must not initialize an executor.
|
|
14
|
+
export function getReadyTaskSteps(plan) {
|
|
15
|
+
const completed = new Set(plan.steps
|
|
16
|
+
.filter((step) => step.status === "done" || step.status === "skipped")
|
|
17
|
+
.map((step) => step.step_id));
|
|
18
|
+
return plan.steps.filter((step) => {
|
|
19
|
+
if (step.status !== "planned")
|
|
20
|
+
return false;
|
|
21
|
+
const dependencies = step.depends_on ?? [];
|
|
22
|
+
return dependencies.every((dependency) => completed.has(dependency));
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
export function formatStepTaskForBridge(step) {
|
|
26
|
+
const upstreamOutputs = step.upstream_outputs ?? [];
|
|
27
|
+
if (upstreamOutputs.length === 0) {
|
|
28
|
+
return step.task;
|
|
29
|
+
}
|
|
30
|
+
const renderedOutputs = upstreamOutputs
|
|
31
|
+
.map((output) => `- ${output.step_id}: ${output.result_summary || "[no summary]"}${output.evidence_refs.length > 0 ? ` (evidence: ${output.evidence_refs.join(", ")})` : ""}`)
|
|
32
|
+
.join("\n");
|
|
33
|
+
return [
|
|
34
|
+
step.task,
|
|
35
|
+
"",
|
|
36
|
+
"Upstream outputs:",
|
|
37
|
+
renderedOutputs,
|
|
38
|
+
].join("\n");
|
|
39
|
+
}
|
|
40
|
+
export function collectUpstreamOutputs(plan, step) {
|
|
41
|
+
const dependencyOutputs = (step.depends_on ?? [])
|
|
42
|
+
.map((depId) => plan.steps.find((s) => s.step_id === depId))
|
|
43
|
+
.filter((dep) => dep?.status === "done")
|
|
44
|
+
.map((dep) => ({
|
|
45
|
+
step_id: dep.step_id,
|
|
46
|
+
result_summary: dep.result_summary ?? "",
|
|
47
|
+
evidence_refs: dep.evidence_refs ?? [],
|
|
48
|
+
}));
|
|
49
|
+
const carriedOutputs = (step.upstream_outputs ?? []).filter((output) => Boolean(output) &&
|
|
50
|
+
typeof output.step_id === "string" &&
|
|
51
|
+
typeof output.result_summary === "string" &&
|
|
52
|
+
Array.isArray(output.evidence_refs));
|
|
53
|
+
const merged = new Map();
|
|
54
|
+
for (const output of [...carriedOutputs, ...dependencyOutputs]) {
|
|
55
|
+
merged.set(output.step_id, output);
|
|
56
|
+
}
|
|
57
|
+
return [...merged.values()];
|
|
58
|
+
}
|
|
59
|
+
//# sourceMappingURL=task-plan-context.js.map
|
package/dist/todo-state.d.ts
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
|
|
1
|
+
import type { TodoNode as StoredTodo } from "./store/repositories/todo-repository.js";
|
|
2
|
+
import type { TaskStep } from './orchestrator-supervisor.js';
|
|
3
|
+
export type TodoNodeStatus = "backlog" | "in_progress" | "blocked" | "gated" | "done" | "cancelled";
|
|
2
4
|
export interface TodoNode {
|
|
3
5
|
id: string;
|
|
4
6
|
title: string;
|
|
@@ -21,6 +23,8 @@ export interface TodoAppendResult {
|
|
|
21
23
|
node_id?: string;
|
|
22
24
|
section?: string;
|
|
23
25
|
error?: string;
|
|
26
|
+
import_warnings?: string[];
|
|
27
|
+
projection_pending?: boolean;
|
|
24
28
|
}
|
|
25
29
|
export interface TodoTransitionResult {
|
|
26
30
|
ok: boolean;
|
|
@@ -32,6 +36,8 @@ export interface TodoTransitionResult {
|
|
|
32
36
|
unresolved_dependencies?: string[];
|
|
33
37
|
allowed?: TodoNodeStatus[];
|
|
34
38
|
error?: string;
|
|
39
|
+
import_warnings?: string[];
|
|
40
|
+
projection_pending?: boolean;
|
|
35
41
|
}
|
|
36
42
|
export declare function waitForTodoStoreMirror(): Promise<void>;
|
|
37
43
|
export declare function getTodoStatePath(): string;
|
|
@@ -39,24 +45,28 @@ export declare function syncTodoState(todoContent?: string): {
|
|
|
39
45
|
state: TodoStateFile;
|
|
40
46
|
path: string;
|
|
41
47
|
};
|
|
48
|
+
/** Repair a linked projection after its authoritative plan has been saved. */
|
|
49
|
+
export declare function syncLinkedTodoStepSafe(id: string, ref: NonNullable<StoredTodo['step_ref']>, step: TaskStep): Promise<{
|
|
50
|
+
ok: true;
|
|
51
|
+
} & {
|
|
52
|
+
import_warnings: string[];
|
|
53
|
+
projection_pending: boolean;
|
|
54
|
+
}>;
|
|
42
55
|
export declare function syncTodoStateSafe(todoContent?: string): Promise<{
|
|
43
56
|
state: TodoStateFile;
|
|
57
|
+
markdown: string;
|
|
44
58
|
path: string;
|
|
59
|
+
import_warnings: string[];
|
|
60
|
+
projection_pending?: boolean;
|
|
45
61
|
}>;
|
|
46
62
|
export declare function readTodoState(): TodoStateFile;
|
|
47
63
|
export declare function transitionTodoNode(nodeId: string, targetStatus: TodoNodeStatus): TodoTransitionResult;
|
|
48
64
|
export declare function transitionTodoNodeSafe(nodeId: string, targetStatus: TodoNodeStatus): Promise<TodoTransitionResult>;
|
|
49
|
-
/**
|
|
50
|
-
* Atomically append a new TODO item under a section, without requiring the
|
|
51
|
-
* caller to read the whole board and resubmit it via update_todo(new_content).
|
|
52
|
-
* That read-modify-write dance is what let concurrent agents silently clobber
|
|
53
|
-
* each other's appended items: the read and write are separate MCP calls, so
|
|
54
|
-
* a lock around the write alone cannot close the race. Here the read, insert,
|
|
55
|
-
* and write all happen inside one TODO_MARKDOWN_REL lock acquisition.
|
|
56
|
-
*/
|
|
65
|
+
/** Insert once under the cross-process coordinator; never reconstruct existing records. */
|
|
57
66
|
export declare function appendTodoItemSafe(title: string, options?: {
|
|
58
67
|
section?: string;
|
|
59
68
|
depends_on?: string[];
|
|
60
69
|
id?: string;
|
|
61
70
|
}): Promise<TodoAppendResult>;
|
|
71
|
+
export declare function deleteTodoItemSafe(nodeId: string): Promise<TodoAppendResult>;
|
|
62
72
|
//# sourceMappingURL=todo-state.d.ts.map
|