@voybio/ace-swarm 2.4.9 → 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.
Files changed (185) hide show
  1. package/CHANGELOG.md +52 -14
  2. package/README.md +14 -10
  3. package/assets/.agents/ACE/ACE_coders/AGENTS.md +23 -30
  4. package/assets/.agents/ACE/ACE_coders/INSTRUCTIONS.md +21 -30
  5. package/assets/.agents/skills/landing-review-watcher/SKILL.md +1 -1
  6. package/assets/.agents/skills/learn-from-work/SKILL.md +41 -0
  7. package/assets/agent-state/ACE_WORKFLOW.md +25 -2
  8. package/assets/agent-state/MODULES/schemas/ACE_RUNTIME_PROFILE.schema.json +143 -2
  9. package/assets/agent-state/MODULES/schemas/RUNTIME_EXECUTOR_SESSION_REGISTRY.schema.json +2 -2
  10. package/assets/agent-state/MODULES/schemas/SWARM_HANDOFF.schema.json +10 -0
  11. package/assets/eval-battery/battery.json +1 -1
  12. package/assets/instructions/ACE_Coder.instructions.md +9 -13
  13. package/assets/scripts/ace-hook-dispatch.mjs +16 -411
  14. package/assets/tasks/SWARM_HANDOFF.example_ui_to_coders.json +1 -2
  15. package/assets/vendor/README.md +9 -0
  16. package/assets/vendor/vericify-reader.mjs +272 -0
  17. package/assets/vendor/vericify-reader.provenance.json +10 -0
  18. package/dist/ace-autonomy.d.ts +58 -0
  19. package/dist/ace-autonomy.js +211 -6
  20. package/dist/ace-internal-tools.d.ts +1 -0
  21. package/dist/ace-internal-tools.js +3 -3
  22. package/dist/ace-server-instructions.d.ts +3 -3
  23. package/dist/ace-server-instructions.js +46 -192
  24. package/dist/cli.js +44 -6
  25. package/dist/experience-reflector.d.ts +7 -0
  26. package/dist/experience-reflector.js +39 -0
  27. package/dist/goal-engine.d.ts +64 -7
  28. package/dist/goal-engine.js +197 -41
  29. package/dist/handoff-registry.d.ts +10 -0
  30. package/dist/handoff-registry.js +59 -0
  31. package/dist/helpers/artifacts.d.ts +6 -2
  32. package/dist/helpers/artifacts.js +8 -6
  33. package/dist/helpers/bootstrap.js +13 -20
  34. package/dist/helpers/path-utils.js +2 -0
  35. package/dist/helpers/store-resolution.js +18 -3
  36. package/dist/helpers/workspace-root.d.ts +2 -0
  37. package/dist/helpers/workspace-root.js +9 -0
  38. package/dist/helpers.d.ts +1 -0
  39. package/dist/helpers.js +1 -0
  40. package/dist/hermes/bridge-protocol.d.ts +1 -1
  41. package/dist/hermes/session-manager.d.ts +17 -0
  42. package/dist/hermes/session-manager.js +67 -14
  43. package/dist/host-goal-engine.d.ts +138 -0
  44. package/dist/host-goal-engine.js +793 -0
  45. package/dist/host-goal-status.d.ts +113 -0
  46. package/dist/host-goal-status.js +175 -0
  47. package/dist/host-learning.d.ts +68 -0
  48. package/dist/host-learning.js +115 -0
  49. package/dist/host-observation.d.ts +12 -0
  50. package/dist/host-observation.js +91 -0
  51. package/dist/host-plan-revision.d.ts +14 -0
  52. package/dist/host-plan-revision.js +101 -0
  53. package/dist/host-verification-job.d.ts +12 -0
  54. package/dist/host-verification-job.js +183 -0
  55. package/dist/host-work-context.d.ts +12 -0
  56. package/dist/host-work-context.js +8 -0
  57. package/dist/job-scheduler.d.ts +48 -14
  58. package/dist/job-scheduler.js +169 -23
  59. package/dist/job-worker.d.ts +3 -1
  60. package/dist/job-worker.js +10 -3
  61. package/dist/kanban.d.ts +4 -1
  62. package/dist/kanban.js +44 -16
  63. package/dist/learned-artifacts.d.ts +64 -0
  64. package/dist/learned-artifacts.js +321 -0
  65. package/dist/learning-controls.d.ts +142 -0
  66. package/dist/learning-controls.js +135 -0
  67. package/dist/learning-input-context.d.ts +9 -0
  68. package/dist/learning-input-context.js +62 -0
  69. package/dist/learning-lifecycle.d.ts +10 -0
  70. package/dist/learning-lifecycle.js +11 -0
  71. package/dist/learning-overlays.d.ts +3 -0
  72. package/dist/learning-overlays.js +17 -0
  73. package/dist/learning-policy.d.ts +40 -0
  74. package/dist/learning-policy.js +38 -0
  75. package/dist/learning-records.d.ts +35 -0
  76. package/dist/learning-records.js +17 -0
  77. package/dist/learning-scorecard.d.ts +82 -0
  78. package/dist/learning-scorecard.js +86 -0
  79. package/dist/learning-serving.d.ts +29 -0
  80. package/dist/learning-serving.js +134 -0
  81. package/dist/linked-work.d.ts +9 -0
  82. package/dist/linked-work.js +28 -0
  83. package/dist/local-model-runtime.js +1 -1
  84. package/dist/mcp-command-progress.d.ts +16 -0
  85. package/dist/mcp-command-progress.js +70 -0
  86. package/dist/model-bridge.d.ts +14 -13
  87. package/dist/model-bridge.js +73 -41
  88. package/dist/orchestrator-supervisor.d.ts +108 -6
  89. package/dist/orchestrator-supervisor.js +4 -32
  90. package/dist/plan-proposal.js +3 -3
  91. package/dist/playbook-curator.d.ts +1 -0
  92. package/dist/playbook-curator.js +11 -9
  93. package/dist/process-identity.d.ts +8 -0
  94. package/dist/process-identity.js +38 -0
  95. package/dist/progress-command.d.ts +34 -0
  96. package/dist/progress-command.js +153 -0
  97. package/dist/prompts.js +15 -15
  98. package/dist/resources.js +2 -2
  99. package/dist/runtime-command.d.ts +8 -1
  100. package/dist/runtime-command.js +48 -7
  101. package/dist/runtime-executor.js +33 -11
  102. package/dist/runtime-profile-syntax.d.ts +6 -0
  103. package/dist/runtime-profile-syntax.js +121 -0
  104. package/dist/runtime-profile.d.ts +37 -5
  105. package/dist/runtime-profile.js +36 -129
  106. package/dist/runtime-tool-specs.js +1 -1
  107. package/dist/safe-edit.d.ts +7 -3
  108. package/dist/safe-edit.js +40 -47
  109. package/dist/schemas.js +4 -2
  110. package/dist/server.d.ts +2 -16
  111. package/dist/server.js +14 -20
  112. package/dist/shared.d.ts +2 -0
  113. package/dist/shared.js +2 -0
  114. package/dist/skill-auditor.d.ts +19 -0
  115. package/dist/skill-auditor.js +32 -2
  116. package/dist/skill-catalog.d.ts +4 -1
  117. package/dist/skill-catalog.js +6 -5
  118. package/dist/store/ace-packed-store.d.ts +5 -1
  119. package/dist/store/ace-packed-store.js +33 -1
  120. package/dist/store/bootstrap-store.js +19 -12
  121. package/dist/store/cache-workspace.d.ts +8 -0
  122. package/dist/store/cache-workspace.js +94 -27
  123. package/dist/store/importer.js +11 -20
  124. package/dist/store/knowledge-bake.d.ts +3 -1
  125. package/dist/store/knowledge-bake.js +10 -1
  126. package/dist/store/legacy-vericify-posts.d.ts +8 -0
  127. package/dist/store/legacy-vericify-posts.js +76 -0
  128. package/dist/store/materializers/hook-context-materializer.d.ts +2 -1
  129. package/dist/store/materializers/hook-context-materializer.js +6 -2
  130. package/dist/store/materializers/host-file-materializer.js +2 -2
  131. package/dist/store/materializers/projection-manager.d.ts +4 -1
  132. package/dist/store/materializers/projection-manager.js +10 -3
  133. package/dist/store/materializers/todo-syncer.d.ts +39 -20
  134. package/dist/store/materializers/todo-syncer.js +208 -110
  135. package/dist/store/materializers/vericify-projector.js +15 -26
  136. package/dist/store/repositories/goal-repository.d.ts +51 -2
  137. package/dist/store/repositories/goal-repository.js +114 -48
  138. package/dist/store/repositories/playbook-repository.d.ts +41 -3
  139. package/dist/store/repositories/playbook-repository.js +61 -5
  140. package/dist/store/repositories/todo-repository.d.ts +8 -2
  141. package/dist/store/repositories/todo-repository.js +1 -1
  142. package/dist/store/repositories/vericify-repository.d.ts +4 -0
  143. package/dist/store/repositories/vericify-repository.js +46 -0
  144. package/dist/store/skills-install.d.ts +1 -0
  145. package/dist/store/skills-install.js +20 -16
  146. package/dist/store/source-preservation.d.ts +24 -0
  147. package/dist/store/source-preservation.js +79 -0
  148. package/dist/store/store-artifacts.js +8 -1
  149. package/dist/store/test-write-boundary.d.ts +16 -0
  150. package/dist/store/test-write-boundary.js +175 -0
  151. package/dist/store/types.d.ts +5 -1
  152. package/dist/store/write-coordinator.js +16 -0
  153. package/dist/task-plan-context.d.ts +11 -0
  154. package/dist/task-plan-context.js +59 -0
  155. package/dist/todo-state.d.ts +19 -9
  156. package/dist/todo-state.js +204 -321
  157. package/dist/tools-agent.d.ts +1 -1
  158. package/dist/tools-agent.js +38 -62
  159. package/dist/tools-files.d.ts +0 -3
  160. package/dist/tools-files.js +21 -11
  161. package/dist/tools-framework.js +16 -25
  162. package/dist/tools-goal.d.ts +2 -4
  163. package/dist/tools-goal.js +119 -16
  164. package/dist/tools-handoff.js +28 -4
  165. package/dist/tools-lifecycle.js +1 -1
  166. package/dist/tools-memory.d.ts +0 -3
  167. package/dist/tools-memory.js +51 -6
  168. package/dist/tools-scheduler.js +19 -17
  169. package/dist/tools-todo.js +160 -148
  170. package/dist/vericify-bridge.d.ts +8 -0
  171. package/dist/vericify-bridge.js +18 -0
  172. package/dist/vericify-context.d.ts +4 -0
  173. package/dist/vericify-context.js +26 -5
  174. package/dist/vericify-native.d.ts +101 -0
  175. package/dist/vericify-native.js +221 -0
  176. package/dist/verification-worker.d.ts +2 -0
  177. package/dist/verification-worker.js +31 -0
  178. package/docs/mcp-tool-reference.md +44 -9
  179. package/package.json +19 -6
  180. package/scripts/dev/run-tests-isolated.mjs +165 -0
  181. package/scripts/dev/run_hermes_crispr_live.mjs +1 -0
  182. package/scripts/dev/test-isolation-guard.mjs +20 -0
  183. package/scripts/dev/test-preload.mjs +153 -0
  184. package/scripts/dev/test-source-inventory.mjs +31 -0
  185. package/docs/development.md +0 -46
@@ -0,0 +1,221 @@
1
+ /** Optional read-only native consumer. The vendored owner client selects history. */
2
+ import { realpathSync } from 'node:fs';
3
+ import { resolve } from 'node:path';
4
+ const protocol = 'vericify-readonly-adapter/v1';
5
+ const capabilities = { command_pass: false, ace_attempt_mapping: false };
6
+ const U64_MAX = 18446744073709551615n;
7
+ function decimal(value) {
8
+ if (typeof value !== 'string' || !/^(0|[1-9][0-9]{0,19})$/.test(value) || BigInt(value) > U64_MAX)
9
+ throw new ConsumerError('invalid_native_response');
10
+ return value;
11
+ }
12
+ function text(value) {
13
+ if (typeof value !== 'string' || value.length < 1 || value.length > 512 || /[\u0000-\u001f]/.test(value))
14
+ throw new ConsumerError('invalid_native_response');
15
+ return value;
16
+ }
17
+ class ConsumerError extends Error {
18
+ code;
19
+ constructor(code) {
20
+ super(code);
21
+ this.code = code;
22
+ }
23
+ }
24
+ function fail(status, reason_code) {
25
+ return { status, reason_code, records: [], coverage: { complete: false, expired_ranges: [] }, capabilities };
26
+ }
27
+ function bound(value, fallback, low, high) {
28
+ const result = value ?? fallback;
29
+ if (!Number.isSafeInteger(result) || result < low || result > high)
30
+ throw new ConsumerError('invalid_native_configuration');
31
+ return result;
32
+ }
33
+ function confidence(value) {
34
+ const result = String(value ?? '').toLowerCase();
35
+ return result === 'linked' ? 'Linked' : result === 'inferred' ? 'Inferred' : result === 'ambiguous' ? 'Ambiguous' : 'Unknown';
36
+ }
37
+ function identity(snapshot, config) {
38
+ if (snapshot.workspace_id !== config.workspace_id || (config.incarnation && snapshot.incarnation !== config.incarnation))
39
+ throw new ConsumerError('workspace_identity_mismatch');
40
+ text(snapshot.incarnation);
41
+ decimal(snapshot.high_water);
42
+ if (snapshot.record_profile !== 'vericify/records/v1')
43
+ throw new ConsumerError('incompatible_native_profile');
44
+ }
45
+ function cursorEncode(position) {
46
+ return 'vcn1:' + Buffer.from(JSON.stringify({ workspace_id: position.workspace_id, incarnation: position.incarnation, sequence: position.sequence })).toString('base64url');
47
+ }
48
+ function cursorDecode(cursor) {
49
+ try {
50
+ if (!cursor.startsWith('vcn1:') || cursor.length > 2048)
51
+ throw new Error();
52
+ const result = JSON.parse(Buffer.from(cursor.slice(5), 'base64url').toString('utf8'));
53
+ text(result.workspace_id);
54
+ text(result.incarnation);
55
+ decimal(result.sequence);
56
+ return result;
57
+ }
58
+ catch {
59
+ throw new ConsumerError('invalid_native_cursor');
60
+ }
61
+ }
62
+ /** Map references already selected/enriched by Vericify. No history scan or join lives here. */
63
+ function observationRef(item, snapshot) {
64
+ const record = item.record, data = record.payload.data;
65
+ const selected = item.latest_link?.payload.data ?? data;
66
+ const capture = data.coverage ?? {};
67
+ return {
68
+ id: record.id, attribution: 'Unknown', native_attribution: confidence(selected.attribution?.confidence), kind: 'file_change',
69
+ ...(typeof data.path?.display === 'string' ? { path_display: data.path.display.slice(0, 512), path_display_truncated: data.path.display.length > 512 } : {}),
70
+ ...(typeof data.kind === 'string' ? { change_kind: text(data.kind) } : {}),
71
+ sequence: record.seq, workspace_id: record.workspace_id, incarnation: record.incarnation, attempt_linked: false,
72
+ link_refs: item.latest_link ? [item.latest_link.id] : [],
73
+ evidence_refs: Array.isArray(selected.attribution?.evidence_refs) ? selected.attribution.evidence_refs.slice(0, 16).map(text) : [],
74
+ purpose_refs: Array.isArray(selected.purpose_refs) ? selected.purpose_refs.slice(0, 16).map((ref) => ({ evidence_id: text(ref.evidence_id), basis: text(ref.basis), uncertainty: text(ref.uncertainty) })) : [],
75
+ purpose_availability: 'unresolved', native_purpose_availability: typeof selected.purpose_availability === 'string' ? text(selected.purpose_availability) : 'unknown', purpose_resolved: false,
76
+ capture_coverage: {
77
+ source: typeof capture.source === 'string' ? text(capture.source) : 'unknown',
78
+ stable_read: typeof capture.stable_read === 'string' ? text(capture.stable_read) : 'unknown',
79
+ gaps: Array.isArray(capture.gaps) ? capture.gaps.slice(0, 16).map(text) : [],
80
+ omission_reason: typeof capture.omission_reason === 'string' ? text(capture.omission_reason) : null,
81
+ },
82
+ binding: { protocol, workspace_id: record.workspace_id, incarnation: record.incarnation, sequence: record.seq, snapshot: snapshot.binding },
83
+ observed_from: record.payload.data.observed_from, observed_to: record.payload.data.observed_to,
84
+ };
85
+ }
86
+ function recordRef(record) {
87
+ return { id: record.id, sequence: record.seq, kind: record.payload.record_type };
88
+ }
89
+ function nativeFailure(code) {
90
+ switch (code) {
91
+ case 'stale_snapshot': return fail('pending', 'native_stale_snapshot');
92
+ case 'capacity': return fail('pending', 'native_capacity');
93
+ case 'timeout': return fail('pending', 'query_timeout');
94
+ case 'missing_history': return fail('unavailable', 'native_history_missing');
95
+ case 'identity_mismatch': return fail('unavailable', 'workspace_identity_mismatch');
96
+ case 'incompatible_protocol': return fail('unavailable', 'incompatible_native_protocol');
97
+ case 'invalid_query': return fail('unavailable', 'invalid_native_query');
98
+ case 'expired': return fail('unavailable', 'history_expired');
99
+ default: return fail('unavailable', 'native_query_unavailable');
100
+ }
101
+ }
102
+ export async function getNativeVericifyContext(input) {
103
+ const config = input.config;
104
+ if (!config.enabled)
105
+ return fail('unavailable', 'native_disabled');
106
+ if (!config.workspace_root || !config.workspace_id)
107
+ return fail('unavailable', 'workspace_identity_unconfigured');
108
+ let workspaceRoot;
109
+ try {
110
+ workspaceRoot = realpathSync(input.workspaceRoot);
111
+ if (workspaceRoot !== realpathSync(resolve(input.workspaceRoot, config.workspace_root)))
112
+ return fail('unavailable', 'workspace_identity_mismatch');
113
+ }
114
+ catch {
115
+ return fail('unavailable', 'workspace_identity_unavailable');
116
+ }
117
+ if (!config.executable || !config.data_dir)
118
+ return fail('unavailable', 'native_configuration_incomplete');
119
+ let timeout, maxBytes, retries;
120
+ try {
121
+ timeout = bound(config.query_timeout_ms, 15000, 1000, 60000);
122
+ maxBytes = bound(config.max_bytes, 262144, 4096, 1048576);
123
+ retries = bound(config.retries, 1, 0, 3);
124
+ }
125
+ catch {
126
+ return fail('unavailable', 'invalid_native_configuration');
127
+ }
128
+ let since;
129
+ try {
130
+ if (input.since)
131
+ since = cursorDecode(input.since);
132
+ if (since && (since.workspace_id !== config.workspace_id || (config.incarnation && since.incarnation !== config.incarnation)))
133
+ throw new Error();
134
+ }
135
+ catch {
136
+ return fail('unavailable', 'invalid_native_cursor');
137
+ }
138
+ // No-window context/delta reads cover all epoch values without Number coercion.
139
+ let fromMs = '0', toMs = U64_MAX.toString();
140
+ if (input.window) {
141
+ const from = Date.parse(input.window.started_at), to = Date.parse(input.window.ended_at ?? new Date().toISOString());
142
+ if (!Number.isSafeInteger(from) || !Number.isSafeInteger(to) || from < 0 || to < from)
143
+ return fail('unavailable', 'invalid_observation_window');
144
+ fromMs = String(from);
145
+ toMs = String(to);
146
+ }
147
+ const after = since ?? (config.incarnation ? { workspace_id: config.workspace_id, incarnation: config.incarnation, sequence: '0' } : undefined);
148
+ const clientUrl = new URL('../assets/vendor/vericify-reader.mjs', import.meta.url).href;
149
+ let owner;
150
+ try {
151
+ owner = await import(clientUrl);
152
+ }
153
+ catch {
154
+ return fail('unavailable', 'native_consumer_unavailable');
155
+ }
156
+ const end = Date.now() + timeout;
157
+ for (let attempt = 0; attempt <= retries; attempt++) {
158
+ const remaining = attempt === 0 ? timeout : end - Date.now();
159
+ if (remaining < 1000)
160
+ return fail('pending', 'query_timeout');
161
+ try {
162
+ const client = owner.openVericifyHistory({ executable: resolve(input.workspaceRoot, config.executable), dataDir: resolve(input.workspaceRoot, config.data_dir), timeoutMs: Math.min(60000, remaining) });
163
+ if (typeof client.window !== 'function')
164
+ return fail('unavailable', 'incompatible_native_protocol');
165
+ // The owner resolves identity, history, latest links and evidence in one pinned native lifetime.
166
+ const result = await client.window({ workspaceRoot, fromMs, toMs, after, limit: 100, maxBytes });
167
+ identity(result.snapshot, config);
168
+ const status = result.state === 'pending' ? 'pending' : result.state === 'expired' ? 'unavailable' : 'available';
169
+ return {
170
+ status,
171
+ ...(result.state === 'pending' ? { reason_code: 'native_window_pending' } : result.state === 'expired' ? { reason_code: 'history_expired' } : {}),
172
+ records: result.changes.map((item) => observationRef(item, result.snapshot)),
173
+ query: {
174
+ protocol, workspace_id: result.snapshot.workspace_id, incarnation: result.snapshot.incarnation, snapshot: result.snapshot.binding,
175
+ ...(result.snapshot.generation !== undefined ? { generation: decimal(result.snapshot.generation) } : {}),
176
+ record_profile: result.snapshot.record_profile, high_water: result.high_water.sequence, since: result.query.after_sequence,
177
+ window: input.window ?? null, from_ms: fromMs, to_ms: toMs, bytes: Buffer.byteLength(JSON.stringify(result)), attempts: attempt + 1,
178
+ state: result.state, scan_complete: result.scan_complete, evidence_complete: result.evidence.complete,
179
+ },
180
+ coverage: {
181
+ complete: result.complete && result.state === 'complete', expired_ranges: result.expired_ranges,
182
+ from_sequence: result.query.after_sequence, through_sequence: result.scanned_through.sequence,
183
+ native_scope: result.coverage_scope,
184
+ native_coverage: result.coverage.map(recordRef), host_gaps: result.host_gaps.map(recordRef),
185
+ evidence_unavailable: result.evidence.unavailable.map((ref) => ({ id: ref.id, reason: ref.reason })),
186
+ capture_complete: result.host_gaps.length ? false : null,
187
+ },
188
+ // Only acknowledged progress can be resumed. Never substitute the snapshot high-water.
189
+ cursor: cursorEncode(result.next_after), capabilities,
190
+ };
191
+ }
192
+ catch (error) {
193
+ if (error instanceof ConsumerError)
194
+ return fail('unavailable', error.code);
195
+ if (typeof owner.NativeReadError === 'function' && error instanceof owner.NativeReadError) {
196
+ const native = error;
197
+ if (native.retryable && attempt < retries && end - Date.now() >= 1000)
198
+ continue;
199
+ return nativeFailure(native.code);
200
+ }
201
+ return fail('unavailable', 'invalid_native_response');
202
+ }
203
+ }
204
+ return fail('pending', 'native_query_unavailable');
205
+ }
206
+ export function createNativeObservationAdapter(config, workspaceRoot) {
207
+ return {
208
+ supported_requirements: [],
209
+ async readWindow(window) {
210
+ try {
211
+ if (realpathSync(window.workspace_id) !== realpathSync(workspaceRoot))
212
+ return fail('unavailable', 'workspace_identity_mismatch');
213
+ }
214
+ catch {
215
+ return fail('unavailable', 'workspace_identity_unavailable');
216
+ }
217
+ return getNativeVericifyContext({ config, workspaceRoot, window });
218
+ },
219
+ };
220
+ }
221
+ //# sourceMappingURL=vericify-native.js.map
@@ -0,0 +1,2 @@
1
+ export declare function runVerificationWorker(root: string, jobId: string): Promise<void>;
2
+ //# sourceMappingURL=verification-worker.d.ts.map
@@ -0,0 +1,31 @@
1
+ /** Detached execution process; the MCP server owns no long verification request. */
2
+ import { fileURLToPath } from 'node:url';
3
+ import { resolve } from 'node:path';
4
+ import { withWorkspaceRoot } from './helpers.js';
5
+ import { registerJobHandler, runJobWorkerOnce } from './job-worker.js';
6
+ import { readJobQueue } from './job-scheduler.js';
7
+ import { executeHostVerificationJob } from './host-verification-job.js';
8
+ export async function runVerificationWorker(root, jobId) {
9
+ await withWorkspaceRoot(root, async () => {
10
+ registerJobHandler('goal.verify', async ({ job }) => {
11
+ await executeHostVerificationJob(root, job.job_id);
12
+ return { success: true, evidence_ref: `goal:${String(job.payload.goal_id)}:verification:${job.job_id}` };
13
+ });
14
+ for (;;) {
15
+ const job = readJobQueue().jobs.find(j => j.job_id === jobId);
16
+ if (!job || ['done', 'failed', 'failed_terminal', 'canceled', 'unknown_recovery'].includes(job.status))
17
+ return;
18
+ const tick = await runJobWorkerOnce({ owner: 'capability-ops', job_ids: [jobId] });
19
+ if (tick.executed.length)
20
+ return;
21
+ await new Promise(resolve => setTimeout(resolve, 1000));
22
+ }
23
+ });
24
+ }
25
+ if (process.argv[1] && resolve(process.argv[1]) === fileURLToPath(import.meta.url)) {
26
+ const [root, jobId] = process.argv.slice(2);
27
+ if (!root || !jobId)
28
+ throw new Error('Verification worker requires workspace and job id.');
29
+ await runVerificationWorker(root, jobId).catch(error => { console.error(error); process.exitCode = 1; });
30
+ }
31
+ //# sourceMappingURL=verification-worker.js.map
@@ -1,8 +1,8 @@
1
1
  # MCP Tool Reference
2
2
 
3
- > Companion to the [README](../README.md). The full MCP surface spans **119 registered tools** across 13 domain modules, plus resources and prompts. For premium hosts (Claude Code, Codex), the curated `ace mcp --profile host-min` exposes 22 high-value tools; the rest remain reachable via `run_orchestrator` delegation or `--profile full`.
3
+ > Companion to the [README](../README.md). The full MCP surface spans **121 registered tools** across 13 domain modules, plus resources and prompts. For premium hosts (Claude Code, Codex), `ace mcp --profile host-min` exposes 30 tools, including work-state and stop/resume operations. Optional `run_local_model`, `run_orchestrator`, `emit_status_event` and `append_vericify_process_post` are available through `--profile full`.
4
4
 
5
- ACE exposes tools through a domain-modular registration system (`src/tools.ts` delegates to 13 domain modules). Tools can be allow-listed at startup; `ace mcp-shadow` uses this to give Hermes a scoped tools-only subset. Public `ace mcp` always exposes the full prompts and resources surface; its visible tool schema depends on `--profile` (119 tools for `full`, 22 for `host-min`). `ace mcp-shadow` instead exposes only its explicitly allow-listed tools and omits prompts and resources.
5
+ ACE exposes tools through a domain-modular registration system (`src/tools.ts` delegates to 13 domain modules). Tools can be allow-listed at startup; `ace mcp-shadow` uses this to give Hermes a scoped tools-only subset. Public `ace mcp` always exposes the full prompts and resources surface; its visible tool schema depends on `--profile` (121 tools for `full`, 30 for `host-min`). `ace mcp-shadow` instead exposes only its explicitly allow-listed tools and omits prompts and resources.
6
6
 
7
7
  ## Agent & Runtime
8
8
 
@@ -99,13 +99,33 @@ enqueue_job {
99
99
 
100
100
  | Tool | Purpose |
101
101
  |---|---|
102
- | `start_goal` | Compile an objective into a validated plan and open a goal record |
103
- | `get_goal_status` | Read goal status, tick/replan counts, ready steps, and escalations |
104
- | `resume_goal` | Resume a stopped or escalated goal from its persisted state |
102
+ | `start_goal` | Choose `executor:"host"` or `executor:"engine"` explicitly; host plans return their first assignment |
103
+ | `next_step` | Receive/resume a ready host assignment with dependency results; never takes another participant's running step |
104
+ | `complete_step` | Record a host outcome once, update linked views, optionally begin the successor; identical retries replay the original response |
105
+ | `get_goal_status` | `view:"compact"` returns bounded verification, blockers and executable next actions; default/`view:"full"` retains complete records |
106
+ | `resume_goal` | Resume an engine goal; for host goals, preserve running work and explicitly retry blocked work or interrupted verification |
105
107
  | `stop_goal` | Operator stop for a running goal |
106
108
 
107
109
  A goal reaches `done` only through evidence-gated success; it otherwise leaves `executing` through budget exhaustion, operator stop, or an escalated blocker. The same loop is available from the CLI as `ace goal start|status|run|resume|stop`.
108
110
 
111
+ Host loop: `start_goal({executor:"host", agent:"codex", steps:[{id:"S1", task:"Implement"}]})` returns an assignment. Do the work with the host's tools, then call `complete_step` with its `goal_id`, `step_id`, `attempt`, and `{status, summary}`. `continue:true` returns the next assignment in that response. Use a stable `agent` label across restarts; `recall_context` recovers it. The attached host performs the step work; ACE does not launch a model for it. Final acceptance runs in a detached scheduler worker, with no external observation capture.
112
+
113
+ Goal-end acceptance enqueues background verification and promptly returns an immutable `verification:"pending"` receipt with an executable `get_goal_status({goal_id, view:"compact", agent})` action. Follow it to confirm success, inspect a bounded failure preview, or receive `next_step` for corrective work. Identical completion retries return the entire original reply verbatim. Compact status targets 4 KiB of structured JSON and retains counts and a `view:"full"` locator when optional previews are shortened; full plans, attempts and failure output are preserved.
114
+ Requirement changes use optional `outcome.requirement_updates: [{step_id, reason, task?, expected_artifacts?, required_evidence_refs?}]`. ACE reopens only those steps and their dependents, retains earlier results, and charges a total correction allowance only if the operator explicitly supplied one. The completion response names reopened steps or changes pending admission; unrelated sibling outcomes stay intact.
115
+
116
+
117
+ `verify` declares goal-end shell checks as strings or `{cmd,stall_min?,kill_min?,deadline_min?}` objects. The command defaults flag a suspected stall after 10 minutes of silence and terminate its entire process group after 60 minutes of silence total (50 more minutes after the warning). `stall_min:0` disables silence detection, `kill_min:0` disables automatic stall termination, and a total deadline exists only when `deadline_min` is supplied. Compact status includes the verification job, worker identity, command duration, time since output and a bounded output tail; full logs live under `.ace-runtime/verification/<job_id>/`. A server restart retains live/uncertain work; explicit recovery reruns checks only after old processes are confirmed gone. The existing scheduler tracks both worker and command identities so stale heartbeats cannot release live work’s locks.
118
+
119
+ `run_tests` accepts the same three policy fields directly. `safe_edit_file`, `apply_patch`, structural rewrites and executable gates accept them in `command_policy`. These commands run asynchronously and emit MCP progress notifications when the request supplies a progress token. Clients must opt into `resetTimeoutOnProgress` to extend their request wait; an explicit `maxTotalTimeout` still bounds the client’s patience. This does not reset ACE’s command progress timestamps. A client wait timeout or connection loss does not cancel the work; explicit operator cancellation and declared command deadlines still do. Learned-payload validation can remain blocking and uses the same progress notifications. Native local-model and orchestrated child-step turn counts default to unlimited; external Hermes adapter and client settings are configured separately.
120
+
121
+ Three consecutive unchanged failure sets park a goal for review instead of exhausting a default correction count. Failure identity retains meaningful output, test names and evidence references; changed failures can continue beyond three corrections. Host contact/age is advisory and never authorizes automatic reassignment.
122
+
123
+ `verify` checks determine acceptance. Without shell checks, a permitted host goal completes as **done (not machine-verified)**. Required final `expected_artifacts` are rechecked at closure even without shell commands; missing deliverables create bounded corrective work. Existing string entries declare required final deliverables. Object entries such as `{path:"scratch.txt", transient:true}` declare outputs required at step completion but not at goal closure; `{path:"optional.txt", required:false}` remains optional. Requirement updates accept the same object metadata. Step `required_evidence_refs` and an optional `required_observation` still apply. Optional native observation is configured under `observability.vericify_bridge.native`; missing reads do not block unrelated work. Native records retain their own IDs/confidence, with ACE attempt attribution Unknown. The current native adapter cannot satisfy `action` or `command_pass` requirements and reports that capability gap explicitly. A capable adapter may use bounded late acceptance in the existing verification job; resume then collects evidence for the original attempt instead of repeating the edit or changing its receipt.
124
+
125
+ Host goals are unlimited unless started with explicit `budget_hours`; goals without the new marker, including legacy goals and `G-learning`, have no deadline. Explicit hours bound new assignments from the retained start and may exceed 24; engine goals also have no implicit deadline or 200-tick ceiling, and explicit deadlines may exceed 24 hours. In-flight outcomes and final acceptance survive expiry. A blocked host can recover through `resume_goal({goal_id,budget_hours:3})` or `resume_goal({goal_id,max_replans:3})`, using a sufficient nondecreasing total allowance. Hours do not restart the clock; a finite deadline cannot be imposed on an unlimited goal through resume. All provided changes are validated before either takes effect, and decreases are refused. Raised replan allowance admits pending dependency closures with their retained IDs; any remainder stays in `goal.pending_follow_ups`. Compact status retains sufficient recovery arguments in `operator_proposal` with `requires_operator_approval:true`, outside executable `options`. Deadline/replan exhaustion leaves `options:[]` and waits on `operator:budget`/`operator:replan_budget`. The host must obtain operator approval before submitting those arguments, including when it receives a proposal from plain `resume_goal`. Status and plain resume never raise an allowance. This is an instruction boundary: MCP does not authenticate whether the caller obtained approval. Clearing a legacy unmarked deadline blocker needs no increase and remains an executable option. Plain resume/restart changes no allowance, and completion receipts stay identical after recovery.
126
+
127
+ Link existing todos with `todo_ids: {S1:"todo-id"}`. Linked todo completion routes to the step. A handoff may carry `step_ref:{goal_id,step_id}`, the submitting `agent` and optional `recipient_agent`; `ack_handoff({status:"accepted",agent:recipient})` transfers ownership and advances the attempt. A sender's old completion is then stale. Standalone todos/handoffs keep their existing behavior.
128
+
109
129
  ## Files & Structural Editing
110
130
 
111
131
  | Tool | Purpose |
@@ -160,7 +180,7 @@ A goal reaches `done` only through evidence-gated success; it otherwise leaves `
160
180
  |---|---|
161
181
  | `context_snapshot` | Capture a structured context snapshot for the current session |
162
182
  | `build_continuity_packet` | Build a continuity packet for cross-session handoff |
163
- | `recall_context` | Recall an exact snapshot name, or list all snapshots when no name is supplied |
183
+ | `recall_context` | Return current work, a running assignment and linked context by default; `list: true` lists snapshots; `name` retrieves one |
164
184
 
165
185
  Example — before switching models mid-task:
166
186
 
@@ -197,11 +217,22 @@ emit_status_event {
197
217
 
198
218
  | Tool | Purpose |
199
219
  |---|---|
200
- | `get_todo` | Read the current todo surface |
201
- | `get_todo_state` | Read structured todo state from the store |
202
- | `update_todo` | Update one or more todo items |
220
+ | `get_todo` | Import changed native rows and read the todo board, with import warnings |
221
+ | `get_todo_state` | Import changed native rows and return authoritative task records |
222
+ | `update_todo` | Append, transition, or explicitly delete (`delete_id`) a task; `new_content` imports edits and retains omitted tasks |
203
223
  | `get_run_ledger` | Read the run ledger (history of major tool invocations) |
204
224
 
225
+ Todo statuses are `backlog`, `in_progress`, `blocked`, `gated`, `done`, and `cancelled`.
226
+ `pending` remains the repository spelling of backlog. A cancelled dependency does not satisfy
227
+ `done`. Native edits are imported on todo reads and mutations; malformed, duplicate or absent
228
+ rows produce `import_warnings` and retain canonical records. `get_todo` returns the canonical
229
+ board even when the native file is awaiting repair. Use `delete_id` for intentional removal;
230
+ `new_content` no longer deletes omitted tasks. Prefer append and transition operations for
231
+ concurrent work. Whole-content field edits do not provide a three-way merge.
232
+
233
+ Kanban distinguishes cancelled work from completion. During debounce, tools report a
234
+ scheduled refresh and retain the last actual generation timestamp; a trailing render follows.
235
+
205
236
  ## Vericify (Verification)
206
237
 
207
238
  | Tool | Purpose |
@@ -293,3 +324,7 @@ Prompts:
293
324
  `ace-astgrep`, `ace-codemunch`, `ace-coders`, `ace-codesnipe`, `ace-eval`, `ace-genesis`, `ace-handoff`, `ace-initiate`, `ace-lessons`, `ace-memory`, `ace-observability`, `ace-orchestrator`, `ace-release`, `ace-runtime-executor`, `ace-security`, `ace-skill`, `ace-skills-index`, `ace-ui`, and `ace-vos`.
294
325
 
295
326
  For schema-exact tool inputs, use MCP `tools/list`; for the release-bound schema files and their surface mappings, run `audit_public_surface`. This avoids maintaining a second hand-copied schema that can silently diverge from the registered Zod contracts.
327
+
328
+ Host learning uses optional `complete_step.outcome.learned` (up to three cited lessons, one skill and one instruction proposal). The response reports acceptance errors and a bounded correction option. Mechanical checks earn `checked`; only exact failing-command recovery earns `demonstrated`. Role instructions remain proposed until operator approval. `get_skill_instructions` is available in `host-min`; active skills under `learned/<slug>` appear in the normal catalog.
329
+
330
+ `ace learn list|show|approve|retire|export|report` provides operator control outside the normal step loop. `ACE_LEARN=0` disables automatic learning without deleting knowledge. Only explicit approval activates a proposed role instruction or distiller overlay. Configure bounded defaults through `learning:` in the existing `ACE_WORKFLOW.md`; instruction approval remains required. Exports with episode citations are reviewable source documents, not an automatic publication.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@voybio/ace-swarm",
3
- "version": "2.4.9",
4
- "description": "ACE Framework MCP server and CLI — single-file ACEPACK state, local-model serving, agent orchestration, and host compliance enforcement.",
3
+ "version": "2.4.10",
4
+ "description": "ACE Framework MCP server and CLI \u2014 single-file ACEPACK state, local-model serving, agent orchestration, and host compliance enforcement.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
7
7
  "types": "dist/index.d.ts",
@@ -23,8 +23,21 @@
23
23
  "dist/**/*.d.ts",
24
24
  "scripts/hermes_bridge_worker.py",
25
25
  "scripts/dev/run_hermes_crispr_live.mjs",
26
+ "scripts/dev/run-tests-isolated.mjs",
27
+ "scripts/dev/test-isolation-guard.mjs",
28
+ "scripts/dev/test-preload.mjs",
29
+ "scripts/dev/test-source-inventory.mjs",
26
30
  "assets",
27
- "docs",
31
+ "docs/cli-reference.md",
32
+ "docs/evaluation-safety.md",
33
+ "docs/experience-playbook.md",
34
+ "docs/hermes.md",
35
+ "docs/library-api.md",
36
+ "docs/mcp-tool-reference.md",
37
+ "docs/provider-security.md",
38
+ "docs/state-model.md",
39
+ "docs/troubleshooting.md",
40
+ "docs/tui-operator-guide.md",
28
41
  "README.md",
29
42
  "CHANGELOG.md"
30
43
  ],
@@ -38,14 +51,14 @@
38
51
  },
39
52
  "scripts": {
40
53
  "build": "tsc",
41
- "test": "npm run build && node --test 'test/*.test.mjs' 'test/**/*.test.mjs'",
42
- "test:real-astgrep": "npm run build && ACE_TEST_REAL_ASTGREP=1 node --test test/astgrep-real-cli.test.mjs",
54
+ "test": "npm run build && node scripts/dev/run-tests-isolated.mjs",
55
+ "test:real-astgrep": "npm run build && ACE_TEST_REAL_ASTGREP=1 node scripts/dev/run-tests-isolated.mjs test/astgrep-real-cli.test.mjs",
43
56
  "start": "node dist/cli.js mcp",
44
57
  "dev": "tsc && node dist/cli.js mcp",
45
58
  "init": "node dist/cli.js init",
46
59
  "turnkey": "node dist/cli.js turnkey",
47
60
  "mcp-config": "node dist/cli.js mcp-config",
48
- "prepublishOnly": "npm run build"
61
+ "prepack": "node scripts/dev/prepack.mjs"
49
62
  },
50
63
  "keywords": [
51
64
  "mcp",
@@ -0,0 +1,165 @@
1
+ #!/usr/bin/env node
2
+ // Run the whole test process outside the developer's workspace, including fallbacks.
3
+ import { cpSync, existsSync, mkdirSync, mkdtempSync, readFileSync, readdirSync, statSync, symlinkSync, writeFileSync, realpathSync, renameSync, unlinkSync } from "node:fs";
4
+ import { createHash, randomUUID } from "node:crypto";
5
+ import { spawn, spawnSync, execFileSync } from "node:child_process";
6
+ import { tmpdir } from "node:os";
7
+ import { dirname, join, relative, resolve } from "node:path";
8
+ import { fileURLToPath, pathToFileURL } from "node:url";
9
+ import { testSourceFiles, findTestDependencies } from './test-source-inventory.mjs';
10
+
11
+ const source = resolve(dirname(fileURLToPath(import.meta.url)), "../..");
12
+ // Darwin sockaddr_un paths are short. Keep every allocated fixture beneath a
13
+ // canonical short base so native control sockets fit without moving outside it.
14
+ const sandbox = mkdtempSync(join(process.platform === 'darwin' ? '/private/tmp' : tmpdir(), 'at-'));
15
+ const checkout = join(sandbox, "checkout");
16
+ mkdirSync(checkout);
17
+ const fixtures = join(sandbox, 'fixtures'); const registry = join(sandbox, 'owned-node-processes');
18
+ mkdirSync(fixtures); mkdirSync(registry);
19
+
20
+ function stateHashes(root) {
21
+ const values = {};
22
+ function visit(path) {
23
+ if (!existsSync(path)) return;
24
+ if (statSync(path).isDirectory()) {
25
+ for (const child of readdirSync(path)) visit(join(path, child));
26
+ } else values[relative(root, path)] = createHash("sha256").update(readFileSync(path)).digest("hex");
27
+ }
28
+ for (const name of ["agent-state", ".agents/ACE"]) visit(join(root, name));
29
+ return values;
30
+ }
31
+
32
+ const protectedBefore = stateHashes(source);
33
+ // Include current edits and new tests, but never copy a live store or host state.
34
+ const files = testSourceFiles(source);
35
+ for (const name of new Set(files)) {
36
+ if (/^(?:agent-state|\.agents|\.git|node_modules|dist)(?:\/|$)/.test(name)) continue;
37
+ const from = join(source, name);
38
+ if (!existsSync(from) || !statSync(from).isFile()) continue;
39
+ mkdirSync(dirname(join(checkout, name)), { recursive: true });
40
+ cpSync(from, join(checkout, name));
41
+ }
42
+ cpSync(join(source, "dist"), join(checkout, "dist"), { recursive: true });
43
+ symlinkSync(findTestDependencies(source), join(checkout, "node_modules"), "dir");
44
+ writeFileSync(join(checkout, '.ace-test-source-files.json'), JSON.stringify(files));
45
+
46
+ const runId = randomUUID(); const leaseFile = join(sandbox, 'lease.json'); const sessionFile = join(sandbox, 'session.json');
47
+ const sentinel = join(checkout, '.ace-test-protected-sentinel'); writeFileSync(sentinel, 'Protected disposable source sentinel\n');
48
+ const sentinelBefore = readFileSync(sentinel);
49
+ const nativeSentinel = join(sandbox, 'outside-fixture-sentinel');
50
+ writeFileSync(nativeSentinel, 'Protected disposable non-source sentinel\n');
51
+ const nativeSentinelBefore = readFileSync(nativeSentinel);
52
+ const lease = status => writeFileSync(leaseFile, JSON.stringify({ version: 1, run_id: runId, status }));
53
+ lease('active');
54
+ writeFileSync(sessionFile, JSON.stringify({ run_id: runId, lease_file: leaseFile, fixtures_root: realpathSync(fixtures), registry_root: registry, sentinel }));
55
+ const preload = join(checkout, 'scripts', 'dev', 'test-preload.mjs');
56
+ // On macOS, a verified Seatbelt profile also covers shell/native descendants.
57
+ // Availability varies under an outer sandbox, so never infer enforcement from
58
+ // the executable merely existing. The self-test touches a disposable sentinel.
59
+ let osBoundary = { mode: process.platform === 'darwin' ? 'unavailable' : 'not_supported' };
60
+ let command = process.execPath; let prefix = [];
61
+ const processRequests = join(sandbox, 'process-info-requests'); const processResponses = join(sandbox, 'process-info-responses');
62
+ mkdirSync(processRequests); mkdirSync(processResponses);
63
+ if (process.platform === 'darwin' && existsSync('/usr/bin/sandbox-exec')) {
64
+ const writable = [fixtures, registry, processRequests, processResponses].map(path => `(subpath ${JSON.stringify(realpathSync.native(path))})`).join(' ');
65
+ const seatbelt = `(version 1) (allow default) (deny file-write*) (allow file-write* ${writable} (literal "/dev/null") (literal "/dev/tty") (literal "/dev/stdout") (literal "/dev/stderr") (subpath "/dev/fd"))`;
66
+ const probePath = join(checkout, '.ace-test-os-sentinel'); const original = 'Protected native sentinel\n'; writeFileSync(probePath, original);
67
+ const result = spawnSync('/usr/bin/sandbox-exec', ['-p', seatbelt, '/bin/sh', '-c', 'printf mutation > "$1"', 'isolation-probe', probePath], { encoding: 'utf8' });
68
+ const preserved = readFileSync(probePath, 'utf8') === original;
69
+ if (!preserved) { lease('quarantined'); throw new Error('OS isolation self-test failed: a disposable protected sentinel was writable'); }
70
+ if (result.status === 1 && /Operation not permitted|Permission denied/.test(result.stderr)) {
71
+ osBoundary = { mode: 'seatbelt', self_test: 'native write refused' }; command = '/usr/bin/sandbox-exec'; prefix = ['-p', seatbelt, process.execPath];
72
+ } else osBoundary = { mode: 'unavailable', reason: (result.stderr || result.error?.message || `probe exit ${result.status}`).trim() };
73
+ }
74
+ if (process.env.ACE_TEST_REQUIRE_OS_BOUNDARY === '1' && osBoundary.mode !== 'seatbelt') { lease('quarantined'); throw new Error(`Required OS test boundary unavailable: ${JSON.stringify(osBoundary)}`); }
75
+ // Seatbelt can restrict Darwin process inspection even though file reads work.
76
+ // Broker only fixed, read-only ps fields through the unsandboxed owning runner.
77
+ // Responses are fresh OS output, never inferred from a heartbeat or PID alone.
78
+ const session = JSON.parse(readFileSync(sessionFile, 'utf8'));
79
+ session.process_info_broker = osBoundary.mode === 'seatbelt';
80
+ session.process_info_requests = processRequests; session.process_info_responses = processResponses;
81
+ writeFileSync(sessionFile, JSON.stringify(session));
82
+ const brokerStats = { requests: 0, failed: 0 };
83
+ function serveProcessInfo() {
84
+ for (const name of readdirSync(processRequests).slice(0, 64)) {
85
+ if (!/^[a-f0-9-]+\.json$/.test(name)) continue;
86
+ let request;
87
+ try { request = JSON.parse(readFileSync(join(processRequests, name), 'utf8')); } catch { continue; }
88
+ if (request.run_id !== runId || !Number.isSafeInteger(request.pid) || request.pid <= 1 || !['start', 'stat', 'both'].includes(request.fields)) { unlinkSync(join(processRequests, name)); continue; }
89
+ const psArgs = ['-p', String(request.pid), ...(request.fields === 'stat' ? [] : ['-o', 'lstart=']), ...(request.fields === 'start' ? [] : ['-o', 'stat='])];
90
+ let response;
91
+ try { response = { ok: true, stdout: execFileSync('ps', psArgs, { encoding: 'utf8', timeout: 2000, stdio: ['ignore', 'pipe', 'pipe'] }) }; }
92
+ catch (error) { brokerStats.failed++; response = { ok: false, stdout: String(error.stdout ?? ''), stderr: String(error.stderr ?? ''), status: error.status ?? null, code: error.code ?? 'PROCESS_INFO_UNAVAILABLE' }; }
93
+ brokerStats.requests++;
94
+ const path = join(processResponses, name); writeFileSync(`${path}.tmp`, JSON.stringify({ ...response, pid: request.pid, run_id: runId, checked_at: Date.now() })); renameSync(`${path}.tmp`, path); unlinkSync(join(processRequests, name));
95
+ }
96
+ }
97
+ const broker = session.process_info_broker ? setInterval(serveProcessInfo, 5) : undefined;
98
+ console.error(`[test isolation] OS source boundary: ${osBoundary.mode}`);
99
+ const requested = process.argv.slice(2);
100
+ const probe = requested[0] === '--probe'; if (probe) requested.shift();
101
+ if (probe && !requested.length) throw new Error('Usage: run-tests-isolated.mjs --probe <probe.mjs> [arguments]');
102
+ const args = requested.length ? requested : ["test/*.test.mjs", "test/**/*.test.mjs"];
103
+ console.error(`[test isolation] Disposable checkout: ${checkout}`);
104
+ const childEnv = { ...process.env, ACE_WORKSPACE_ROOT: fixtures, INIT_CWD: checkout, PWD: checkout, ACE_TEST_SESSION_FILE: sessionFile,
105
+ ACE_TEST_ISOLATION: '1', ACE_TEST_RUN_ID: runId, ACE_TEST_LEASE_FILE: leaseFile,
106
+ ACE_TEST_NATIVE_SENTINEL_PATH: nativeSentinel,
107
+ ACE_TEST_PROBE: probe ? '1' : '0',
108
+ NODE_OPTIONS: [process.env.NODE_OPTIONS ?? '', `--import=${pathToFileURL(preload).href}`].join(' ').trim() };
109
+ delete childEnv.ACE_TUI_WORKSPACE_ROOT;
110
+ delete childEnv.ACE_TEST_WORKER_ROOT;
111
+ const child = spawn(command, [...prefix, ...(probe ? args : ["--test", ...args])], {
112
+ cwd: checkout,
113
+ env: childEnv,
114
+ stdio: "inherit",
115
+ });
116
+ const outcome = await new Promise((resolveExit, reject) => {
117
+ child.on("error", reject);
118
+ child.on("close", (code, signal) => resolveExit({ code, signal }));
119
+ });
120
+ function ownedProcesses() {
121
+ return readdirSync(registry).flatMap(name => {
122
+ try {
123
+ const record = JSON.parse(readFileSync(join(registry, name), 'utf8'));
124
+ try { process.kill(record.pid, 0); } catch (error) { if (error.code === 'ESRCH') return []; return [{ ...record, state: 'uncertain' }]; }
125
+ try {
126
+ const actual = execFileSync('ps', ['-p', String(record.pid), '-o', 'lstart=', '-o', 'stat='], { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
127
+ const match = actual.match(/^(.*?)\s+([A-Za-z+<>NsLslWEIXZ-]+)$/);
128
+ if (match?.[2].includes('Z')) return [];
129
+ if (record.identity && match && match[1].replace(/\s+/g, ' ') !== record.identity) return [];
130
+ return [{ ...record, state: record.identity && match ? 'alive' : 'uncertain' }];
131
+ } catch { return [{ ...record, state: 'uncertain' }]; }
132
+ } catch { return []; }
133
+ });
134
+ }
135
+ const configuredWait = Number(process.env.ACE_TEST_CHILD_WAIT_MS ?? 15000);
136
+ const waitUntil = Date.now() + (Number.isFinite(configuredWait) && configuredWait >= 0 ? configuredWait : 15000);
137
+ let retained = ownedProcesses();
138
+ while (retained.length && Date.now() < waitUntil) {
139
+ await new Promise(resolveWait => setTimeout(resolveWait, 100)); retained = ownedProcesses();
140
+ }
141
+ // Closing the allocation is an instruction to test writers, not a total-runtime
142
+ // kill of healthy work. Unfinished/uncertain descendants quarantine this run.
143
+ lease(retained.length ? 'quarantined' : 'closed');
144
+ if (broker) clearInterval(broker);
145
+ // close fires after the test runner and its workers have finished, not when an
146
+ // alphabetically-last test happens to run. The copy's source root is a sentinel:
147
+ // fixtures must use their disposable workspace, including in spawned children.
148
+ const unexpected = Object.keys(stateHashes(checkout));
149
+ const protectedAfter = stateHashes(source);
150
+ const changed = [...new Set([...Object.keys(protectedBefore), ...Object.keys(protectedAfter)])]
151
+ .filter((name) => protectedBefore[name] !== protectedAfter[name]);
152
+ const sentinelChanged = !readFileSync(sentinel).equals(sentinelBefore);
153
+ const nativeSentinelChanged = !existsSync(nativeSentinel) || !readFileSync(nativeSentinel).equals(nativeSentinelBefore);
154
+ const quarantined = Boolean(retained.length || unexpected.length || changed.length || sentinelChanged || nativeSentinelChanged);
155
+ if (quarantined) lease('quarantined');
156
+ writeFileSync(join(sandbox, "isolation.json"), JSON.stringify({ checkout, outcome, unexpected, protected_source_changes: changed, sentinel_changed: sentinelChanged, native_sentinel_changed: nativeSentinelChanged, retained_owned_node_processes: retained, allocation: quarantined ? 'quarantined' : 'closed', os_boundary: osBoundary, process_info_broker: { enabled: session.process_info_broker, ...brokerStats } }, null, 2));
157
+ if (quarantined) {
158
+ console.error(`[test isolation] Unauthorized fixture writes: ${unexpected.join(", ") || "none"}; source changes: ${changed.join(", ") || "none"}`);
159
+ if (nativeSentinelChanged) console.error('[test isolation] Native write outside allocated fixtures changed a disposable sentinel.');
160
+ if (retained.length) console.error(`[test isolation] Allocation quarantined; ${retained.length} owned Node descendants remain alive or uncertain. Lease closed; no claim of finished child work.`);
161
+ process.exitCode = 1;
162
+ } else {
163
+ console.error("[test isolation] No source-state changes or fallback writes into the disposable source root.");
164
+ process.exitCode = outcome.code ?? 1;
165
+ }
@@ -1,4 +1,5 @@
1
1
  #!/usr/bin/env node
2
+ import './test-isolation-guard.mjs';
2
3
 
3
4
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
4
5
  import { resolve } from "node:path";
@@ -0,0 +1,20 @@
1
+ import { readFileSync, realpathSync, statSync } from 'node:fs';
2
+ import { isAbsolute, relative, sep } from 'node:path';
3
+
4
+ const denied = detail => { throw new Error(`ACE test isolation required: ${detail}. Use node scripts/dev/run-tests-isolated.mjs.`); };
5
+ export function assertTestIsolation() {
6
+ if (process.env.ACE_TEST_ISOLATION !== '1' || !globalThis[Symbol.for('ace.test.preload')]) denied('the isolation preloader must run before test imports');
7
+ let roots, lease;
8
+ try {
9
+ roots = JSON.parse(process.env.ACE_TEST_ALLOWED_ROOTS ?? 'null');
10
+ lease = JSON.parse(readFileSync(process.env.ACE_TEST_LEASE_FILE, 'utf8'));
11
+ } catch { denied('missing fixture allocation or lease'); }
12
+ if (!Array.isArray(roots) || !roots.length || roots.some(root => typeof root !== 'string' || !isAbsolute(root))) denied('invalid fixture roots');
13
+ if (lease.status !== 'active' || lease.run_id !== process.env.ACE_TEST_RUN_ID) denied('the fixture lease is no longer active');
14
+ let workspace;
15
+ try { workspace = realpathSync.native(process.env.ACE_TEST_WORKER_ROOT); } catch { denied('missing worker allocation'); }
16
+ if (!roots.some(root => {
17
+ try { const base = realpathSync.native(root); const path = relative(base, workspace); return statSync(base).isDirectory() && path !== '..' && !path.startsWith(`..${sep}`) && !isAbsolute(path); } catch { return false; }
18
+ })) denied('worker root is outside the fixture allocation');
19
+ }
20
+ assertTestIsolation();