agentic-engineering-harness 0.6.13 → 0.6.15

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 (68) hide show
  1. package/dist/agents/outputContracts.d.ts +44 -1
  2. package/dist/agents/outputContracts.js +12 -2
  3. package/dist/agents/outputContracts.js.map +1 -1
  4. package/dist/agents/reviewLifecycle.js +54 -14
  5. package/dist/agents/reviewLifecycle.js.map +1 -1
  6. package/dist/agents/waveExecutor.d.ts +2 -13
  7. package/dist/audit/run.d.ts +7 -0
  8. package/dist/audit/run.js +198 -32
  9. package/dist/audit/run.js.map +1 -1
  10. package/dist/core/run.js +103 -28
  11. package/dist/core/run.js.map +1 -1
  12. package/dist/main.js +95 -29
  13. package/dist/main.js.map +1 -1
  14. package/dist/operations/artifacts.d.ts +13 -0
  15. package/dist/operations/artifacts.js +45 -0
  16. package/dist/operations/artifacts.js.map +1 -0
  17. package/dist/operations/change.d.ts +11 -0
  18. package/dist/operations/change.js +144 -0
  19. package/dist/operations/change.js.map +1 -0
  20. package/dist/operations/completion.d.ts +4 -1
  21. package/dist/operations/completion.js +69 -89
  22. package/dist/operations/completion.js.map +1 -1
  23. package/dist/operations/controller.d.ts +8 -5
  24. package/dist/operations/controller.js +270 -131
  25. package/dist/operations/controller.js.map +1 -1
  26. package/dist/operations/leadBinding.d.ts +2 -0
  27. package/dist/operations/leadBinding.js +22 -0
  28. package/dist/operations/leadBinding.js.map +1 -0
  29. package/dist/operations/liveness.d.ts +1 -0
  30. package/dist/operations/liveness.js +2 -0
  31. package/dist/operations/liveness.js.map +1 -0
  32. package/dist/operations/livenessV2.d.ts +34 -0
  33. package/dist/operations/livenessV2.js +339 -0
  34. package/dist/operations/livenessV2.js.map +1 -0
  35. package/dist/operations/mcp.d.ts +1 -0
  36. package/dist/operations/mcp.js +84 -57
  37. package/dist/operations/mcp.js.map +1 -1
  38. package/dist/operations/monitorProcess.d.ts +8 -0
  39. package/dist/operations/monitorProcess.js +33 -0
  40. package/dist/operations/monitorProcess.js.map +1 -0
  41. package/dist/operations/portfolio.d.ts +35 -0
  42. package/dist/operations/portfolio.js +158 -0
  43. package/dist/operations/portfolio.js.map +1 -0
  44. package/dist/operations/state.d.ts +174 -17
  45. package/dist/operations/state.js +178 -163
  46. package/dist/operations/state.js.map +1 -1
  47. package/dist/operations/supervisor.d.ts +38 -0
  48. package/dist/operations/supervisor.js +248 -0
  49. package/dist/operations/supervisor.js.map +1 -0
  50. package/dist/operations/wakeBudget.d.ts +15 -0
  51. package/dist/operations/wakeBudget.js +142 -0
  52. package/dist/operations/wakeBudget.js.map +1 -0
  53. package/dist/paseo/launchSpec.d.ts +4 -0
  54. package/dist/paseo/launchSpec.js +31 -37
  55. package/dist/paseo/launchSpec.js.map +1 -1
  56. package/dist/paseo/sdk.d.ts +5 -0
  57. package/dist/paseo/sdk.js +35 -87
  58. package/dist/paseo/sdk.js.map +1 -1
  59. package/dist/paseo/start.d.ts +1 -1
  60. package/dist/paseo/start.js +38 -87
  61. package/dist/paseo/start.js.map +1 -1
  62. package/dist/workers/agentPrompt.d.ts +2 -0
  63. package/dist/workers/agentPrompt.js +135 -190
  64. package/dist/workers/agentPrompt.js.map +1 -1
  65. package/docs/OPERATION_SUPERVISION.md +198 -0
  66. package/package.json +1 -1
  67. package/presets/agents/orchestration.jsonc +14 -4
  68. package/skills/engineering-workflow/SKILL.md +149 -104
@@ -1,35 +1,66 @@
1
1
  import crypto from "node:crypto";
2
2
  import { spawn } from "node:child_process";
3
+ import fs from "node:fs/promises";
3
4
  import path from "node:path";
4
5
  import process from "node:process";
5
6
  import { runAudit } from "../audit/run.js";
6
7
  import { loadProjectConfig, loadTaskContract } from "../core/config.js";
7
8
  import { runTask } from "../core/run.js";
9
+ import { deliveryWorkspaceId, deliveryWorkspacePath, materializeTaskContext } from "../delivery/handoff.js";
8
10
  import { listManagedPaseoAgents } from "../paseo/runtime.js";
9
11
  import { recordPaseoTrace } from "../paseo/trace.js";
10
12
  import { runProcess } from "../utils/process.js";
13
+ import { runChangeOperation } from "./change.js";
11
14
  import { disableOperationCompletionTarget, notifyOperationCompletion, registerOperationCompletionTarget } from "./completion.js";
12
- import { loadOperation, patchOperation, saveOperation, transitionOperationToTerminal } from "./state.js";
15
+ import { startOperationWatchdog } from "./liveness.js";
16
+ import { assertOperationCapacity, syncOperationPortfolio } from "./portfolio.js";
17
+ import { bindOperationLead, isTerminalOperation, loadOperation, patchOperation, saveOperation, transitionOperationToTerminal } from "./state.js";
13
18
  export async function startDetachedOperation(root, kind, payload, options) {
14
19
  const absoluteRoot = path.resolve(root);
20
+ const config = await loadProjectConfigIfPresent(absoluteRoot);
21
+ if (config)
22
+ await assertOperationCapacity(absoluteRoot, config, operationPriority(payload));
15
23
  const now = new Date().toISOString();
16
24
  const id = createOperationId(kind, JSON.stringify(payload));
17
- const record = {
18
- version: 1,
25
+ let record = {
26
+ version: 2,
19
27
  id,
20
28
  kind,
21
29
  status: "QUEUED",
22
30
  phase: "queued",
23
31
  root: absoluteRoot,
24
32
  payload,
33
+ revision: 1,
25
34
  createdAt: now,
26
- updatedAt: now
35
+ updatedAt: now,
36
+ lastProgressAt: now,
37
+ intent: initialIntent(kind, payload),
38
+ supervision: {
39
+ required: kind === "audit" || kind === "change",
40
+ materialized: false,
41
+ generations: []
42
+ },
43
+ stages: {
44
+ queued: {
45
+ name: "queued",
46
+ status: "RUNNING",
47
+ revision: 1,
48
+ startedAt: now
49
+ }
50
+ },
51
+ participants: {},
52
+ progress: { expected: 0, registered: 0, running: 0, completed: 0, failed: 0, blocked: 0 },
53
+ notification: { lastLeadWakeRevision: 0, terminalDelivered: false, attempts: 0 }
27
54
  };
28
55
  await saveOperation(absoluteRoot, record);
29
56
  const completionAgentId = options.completionAgentId?.trim() || process.env.PASEO_AGENT_ID?.trim() || undefined;
30
57
  if (completionAgentId) {
31
- await registerOperationCompletionTarget(absoluteRoot, id, completionAgentId, options.completionSource ?? (options.completionAgentId ? "explicit" : "environment"));
58
+ const source = options.completionSource ?? (options.completionAgentId ? "explicit" : "environment");
59
+ await registerOperationCompletionTarget(absoluteRoot, id, completionAgentId, source);
60
+ record = await bindOperationLead(absoluteRoot, id, completionAgentId, source);
32
61
  }
62
+ if (config)
63
+ await syncOperationPortfolio(absoluteRoot, config.project.name, record);
33
64
  const spawnProcess = options.spawnProcess ?? spawn;
34
65
  let child;
35
66
  try {
@@ -39,6 +70,7 @@ export async function startDetachedOperation(root, kind, payload, options) {
39
70
  stdio: "ignore",
40
71
  env: {
41
72
  ...process.env,
73
+ AEH_CONTROL_ROOT: absoluteRoot,
42
74
  AEH_OPERATION_ID: id,
43
75
  AEH_OPERATION_KIND: kind
44
76
  }
@@ -48,21 +80,28 @@ export async function startDetachedOperation(root, kind, payload, options) {
48
80
  if (completionAgentId) {
49
81
  await disableOperationCompletionTarget(absoluteRoot, id, `Detached controller spawn failed before the initiating tool returned: ${String(error)}`).catch(() => undefined);
50
82
  }
51
- return patchOperation(absoluteRoot, id, {
83
+ record = await patchOperation(absoluteRoot, id, {
52
84
  status: "FAILED",
53
85
  phase: "spawn-failed",
54
86
  error: String(error),
55
87
  finishedAt: new Date().toISOString()
56
88
  });
89
+ if (config)
90
+ await syncOperationPortfolio(absoluteRoot, config.project.name, record);
91
+ return record;
57
92
  }
58
93
  child.unref();
59
- return patchOperation(absoluteRoot, id, {
94
+ record = await patchOperation(absoluteRoot, id, {
60
95
  pid: child.pid,
61
96
  phase: "dispatched"
62
97
  });
98
+ if (config)
99
+ await syncOperationPortfolio(absoluteRoot, config.project.name, record);
100
+ return record;
63
101
  }
64
102
  export async function executeOperation(root, operationId, deps = {}) {
65
103
  const absoluteRoot = path.resolve(root);
104
+ process.env.AEH_CONTROL_ROOT = absoluteRoot;
66
105
  const trace = deps.trace ?? recordPaseoTrace;
67
106
  let record = await loadOperation(absoluteRoot, operationId);
68
107
  if (record.status === "CANCELLED")
@@ -70,33 +109,30 @@ export async function executeOperation(root, operationId, deps = {}) {
70
109
  record = await patchOperation(absoluteRoot, operationId, {
71
110
  status: "RUNNING",
72
111
  phase: "preparing",
73
- startedAt: new Date().toISOString(),
112
+ startedAt: record.startedAt ?? new Date().toISOString(),
74
113
  pid: process.pid,
75
114
  error: undefined
76
115
  });
77
116
  process.env.AEH_OPERATION_ID = record.id;
78
117
  process.env.AEH_OPERATION_KIND = record.kind;
79
- const workspace = await ensureOperationWorkspace(absoluteRoot, record, deps.run ?? runProcess, trace);
80
- if (workspace.workspaceId) {
81
- process.env.AEH_OPERATION_WORKSPACE_ID = workspace.workspaceId;
118
+ const config = await loadProjectConfig(absoluteRoot);
119
+ await syncOperationPortfolio(absoluteRoot, config.project.name, record);
120
+ let stopWatchdog;
121
+ try {
122
+ const workspace = await ensureOperationWorkspace(absoluteRoot, record, config, deps.run ?? runProcess, trace);
123
+ if (workspace.workspaceId)
124
+ process.env.AEH_OPERATION_WORKSPACE_ID = workspace.workspaceId;
125
+ const executionRoot = path.resolve(workspace.workspaceRoot ?? absoluteRoot);
82
126
  record = await patchOperation(absoluteRoot, operationId, {
83
127
  workspaceId: workspace.workspaceId,
84
- workspaceWarning: undefined
85
- });
86
- }
87
- else if (workspace.warning) {
88
- record = await patchOperation(absoluteRoot, operationId, {
128
+ workspaceRoot: executionRoot,
89
129
  workspaceWarning: workspace.warning
90
130
  });
91
- }
92
- try {
93
- const config = await loadProjectConfig(absoluteRoot);
131
+ await syncOperationPortfolio(absoluteRoot, config.project.name, record);
132
+ stopWatchdog = (deps.startWatchdog ?? startOperationWatchdog)(absoluteRoot, config, operationId);
94
133
  if (record.kind === "audit") {
95
134
  const payload = record.payload;
96
- const report = await runAudit(absoluteRoot, config, {
97
- ...payload,
98
- auditId: record.id
99
- });
135
+ const report = await runAudit(executionRoot, config, { ...payload, auditId: record.id });
100
136
  const current = await loadOperation(absoluteRoot, operationId);
101
137
  if (current.status === "CANCELLED")
102
138
  return current;
@@ -110,13 +146,33 @@ export async function executeOperation(root, operationId, deps = {}) {
110
146
  productionSafe: report.productionSafe,
111
147
  report: `.harness/audits/${report.auditId}.json`
112
148
  }
113
- }, deps);
149
+ }, deps, config);
150
+ }
151
+ if (record.kind === "change") {
152
+ const result = await runChangeOperation(executionRoot, absoluteRoot, config, await loadOperation(absoluteRoot, operationId), record.payload);
153
+ const current = await loadOperation(absoluteRoot, operationId);
154
+ if (current.status === "CANCELLED")
155
+ return current;
156
+ return terminalizeOperation(absoluteRoot, operationId, {
157
+ status: result.run.status === "PASS" ? "SUCCEEDED" : "FAILED",
158
+ phase: "finished",
159
+ finishedAt: new Date().toISOString(),
160
+ result: {
161
+ taskId: result.taskId,
162
+ mode: result.mode,
163
+ status: result.run.status,
164
+ attempts: result.run.attempts,
165
+ specChange: result.specChange,
166
+ triageReasons: result.triageReasons
167
+ }
168
+ }, deps, config);
114
169
  }
115
170
  const payload = record.payload;
116
171
  const contract = await loadTaskContract(absoluteRoot, payload.taskId, config);
117
- const result = await runTask(absoluteRoot, config, contract, {
118
- profile: payload.profile
119
- });
172
+ if (executionRoot !== absoluteRoot) {
173
+ await materializeTaskContext(absoluteRoot, executionRoot, config, contract);
174
+ }
175
+ const result = await runTask(executionRoot, config, contract, { profile: payload.profile });
120
176
  const current = await loadOperation(absoluteRoot, operationId);
121
177
  if (current.status === "CANCELLED")
122
178
  return current;
@@ -129,7 +185,7 @@ export async function executeOperation(root, operationId, deps = {}) {
129
185
  status: result.status,
130
186
  attempts: result.attempts
131
187
  }
132
- }, deps);
188
+ }, deps, config);
133
189
  }
134
190
  catch (error) {
135
191
  const current = await loadOperation(absoluteRoot, operationId).catch(() => record);
@@ -140,18 +196,20 @@ export async function executeOperation(root, operationId, deps = {}) {
140
196
  phase: "failed",
141
197
  error: error instanceof Error ? error.stack ?? error.message : String(error),
142
198
  finishedAt: new Date().toISOString()
143
- }, deps);
199
+ }, deps, config);
200
+ }
201
+ finally {
202
+ stopWatchdog?.();
144
203
  }
145
204
  }
146
205
  export async function waitForOperation(root, operationId, timeoutMs = 1_800_000, pollMs = 500) {
147
206
  const deadline = Date.now() + timeoutMs;
148
207
  for (;;) {
149
208
  const record = await loadOperation(root, operationId);
150
- if (isTerminal(record.status))
209
+ if (isTerminalOperation(record.status))
151
210
  return record;
152
- if (Date.now() >= deadline) {
211
+ if (Date.now() >= deadline)
153
212
  throw new Error(`Timed out waiting for operation ${operationId} after ${timeoutMs}ms.`);
154
- }
155
213
  await new Promise((resolve) => setTimeout(resolve, pollMs));
156
214
  }
157
215
  }
@@ -160,7 +218,7 @@ export async function cancelOperation(root, operationId, deps = {}) {
160
218
  const trace = deps.trace ?? recordPaseoTrace;
161
219
  const run = deps.run ?? runProcess;
162
220
  const record = await loadOperation(absoluteRoot, operationId);
163
- if (isTerminal(record.status))
221
+ if (isTerminalOperation(record.status))
164
222
  return record;
165
223
  const cleanupWarnings = [];
166
224
  if (record.pid && record.pid !== process.pid) {
@@ -168,81 +226,53 @@ export async function cancelOperation(root, operationId, deps = {}) {
168
226
  process.kill(record.pid, "SIGTERM");
169
227
  }
170
228
  catch (error) {
171
- if (!/ESRCH/.test(String(error))) {
229
+ if (!/ESRCH/.test(String(error)))
172
230
  cleanupWarnings.push(`controller: ${String(error)}`);
173
- }
174
231
  }
175
232
  }
176
233
  let agentIds = [...new Set((record.agents ?? []).map((agent) => agent.id).filter(Boolean))];
177
234
  if (agentIds.length > 0) {
178
- await trace(absoluteRoot, "cleanup.discovery", {
179
- operationId,
180
- source: "operation-state",
181
- agentCount: agentIds.length
182
- });
235
+ await trace(absoluteRoot, "cleanup.discovery", { operationId, source: "operation-state", agentCount: agentIds.length });
183
236
  }
184
237
  else {
185
238
  try {
186
- const discovered = await listManagedPaseoAgents(absoluteRoot, {
187
- "aeh.operation": operationId
188
- });
239
+ const discovered = await listManagedPaseoAgents(absoluteRoot, { "aeh.operation": operationId });
189
240
  agentIds = [...new Set(discovered.map((agent) => agent.id))];
190
- await trace(absoluteRoot, "cleanup.discovery", {
191
- operationId,
192
- source: "paseo-list-compatibility",
193
- agentCount: agentIds.length,
194
- reason: "legacy operation record has no registered agent identities"
195
- });
241
+ await trace(absoluteRoot, "cleanup.discovery", { operationId, source: "paseo-list-compatibility", agentCount: agentIds.length, reason: "legacy operation record has no registered agent identities" });
196
242
  }
197
243
  catch (error) {
198
244
  cleanupWarnings.push(`agent discovery: ${String(error)}`);
199
- await trace(absoluteRoot, "cleanup.cli.error", {
200
- operationId,
201
- error: String(error)
202
- });
245
+ await trace(absoluteRoot, "cleanup.cli.error", { operationId, error: String(error) });
203
246
  }
204
247
  }
205
248
  await trace(absoluteRoot, "cleanup.cli.required", {
206
249
  operationId,
207
- reason: "Paseo public SDK 0.3.1 lacks cancel/kill parity for external controller cleanup",
250
+ reason: "Paseo public SDK lacks cancel/kill parity for external controller cleanup",
208
251
  agentCount: agentIds.length
209
252
  });
210
253
  for (const agentId of agentIds) {
211
- const stopped = await run(`paseo stop ${quote(agentId)}`, {
212
- cwd: absoluteRoot,
213
- timeoutMs: 30_000
214
- }).catch((error) => ({
215
- exitCode: 1,
216
- stdout: "",
217
- stderr: String(error),
218
- durationMs: 0
219
- }));
220
- await trace(absoluteRoot, "cleanup.cli.stop", {
221
- operationId,
222
- agentId,
223
- exitCode: stopped.exitCode
224
- });
225
- if (stopped.exitCode !== 0) {
254
+ const stopped = await run(`paseo stop ${quote(agentId)}`, { cwd: absoluteRoot, timeoutMs: 30_000 }).catch((error) => ({ exitCode: 1, stdout: "", stderr: String(error), durationMs: 0 }));
255
+ await trace(absoluteRoot, "cleanup.cli.stop", { operationId, agentId, exitCode: stopped.exitCode });
256
+ if (stopped.exitCode !== 0)
226
257
  cleanupWarnings.push(`agent ${agentId}: ${stopped.stderr || stopped.stdout || `exit ${stopped.exitCode}`}`);
227
- }
228
258
  }
259
+ const config = await loadProjectConfigIfPresent(absoluteRoot);
229
260
  return terminalizeOperation(absoluteRoot, operationId, {
230
261
  status: "CANCELLED",
231
262
  phase: "cancelled",
232
263
  finishedAt: new Date().toISOString(),
233
264
  cleanupWarnings: cleanupWarnings.length ? cleanupWarnings : undefined
234
- }, deps);
265
+ }, deps, config);
235
266
  }
236
267
  export function createOperationId(kind, seed) {
237
- const stamp = new Date()
238
- .toISOString()
239
- .replace(/[-:]/g, "")
240
- .replace(/\.\d{3}Z$/, "Z");
268
+ const stamp = new Date().toISOString().replace(/[-:]/g, "").replace(/\.\d{3}Z$/, "Z");
241
269
  const hash = crypto.createHash("sha256").update(seed).digest("hex").slice(0, 8);
242
270
  return `${kind.toUpperCase()}-${stamp}-${hash}`;
243
271
  }
244
- async function terminalizeOperation(root, operationId, patch, deps) {
272
+ async function terminalizeOperation(root, operationId, patch, deps, config) {
245
273
  const { record: terminal, transitioned } = await transitionOperationToTerminal(root, operationId, patch);
274
+ if (config)
275
+ await syncOperationPortfolio(root, config.project.name, terminal).catch(() => undefined);
246
276
  if (!transitioned)
247
277
  return terminal;
248
278
  const trace = deps.trace ?? recordPaseoTrace;
@@ -260,56 +290,71 @@ async function terminalizeOperation(root, operationId, patch, deps) {
260
290
  boundary: "controller"
261
291
  }).catch(() => undefined);
262
292
  }
263
- return terminal;
293
+ return loadOperation(root, operationId).catch(() => terminal);
264
294
  }
265
- async function ensureOperationWorkspace(root, record, run, trace) {
266
- const title = `AEH ${record.kind.toUpperCase()} · ${record.id}`;
267
- const command = `paseo workspace create --isolation local --path ${quote(root)} --title ${quote(title)} --json`;
268
- await trace(root, "workspace.cli.required", {
269
- operationId: record.id,
270
- kind: record.kind,
271
- reason: "Paseo SDK 0.3.1 workspace create lacks isolation/title parity",
272
- isolation: "local"
273
- });
274
- let result;
275
- try {
276
- result = await run(command, { cwd: root, timeoutMs: 60_000 });
295
+ async function ensureOperationWorkspace(root, record, config, run, trace) {
296
+ if (record.kind === "run") {
297
+ const payload = record.payload;
298
+ const [existingRoot, existingId] = await Promise.all([
299
+ deliveryWorkspacePath(root, config, payload.taskId),
300
+ deliveryWorkspaceId(root, config, payload.taskId)
301
+ ]);
302
+ if (existingRoot) {
303
+ await trace(root, "workspace.delivery.reused", {
304
+ operationId: record.id,
305
+ workspaceId: existingId ?? "",
306
+ workspaceRoot: existingRoot
307
+ });
308
+ return { workspaceId: existingId, workspaceRoot: existingRoot, reusedDelivery: true };
309
+ }
277
310
  }
278
- catch (error) {
279
- await trace(root, "workspace.cli.error", {
280
- operationId: record.id,
281
- error: String(error)
282
- });
283
- return {
284
- warning: `Paseo operation workspace could not be created: ${String(error)}`
285
- };
311
+ const title = `AEH ${record.kind.toUpperCase()} · ${record.id}`;
312
+ if (record.kind === "audit") {
313
+ const command = `paseo workspace create --isolation local --path ${quote(root)} --title ${quote(title)} --json`;
314
+ await trace(root, "workspace.cli.required", { operationId: record.id, kind: record.kind, reason: "Paseo public SDK workspace create lacks isolation/title parity", isolation: "local" });
315
+ const result = await safeRun(run, command, root, 60_000);
316
+ if (result.exitCode !== 0) {
317
+ const warning = `Paseo audit workspace could not be created: ${result.stderr || result.stdout || `exit ${result.exitCode}`}`;
318
+ await trace(root, "workspace.cli.error", { operationId: record.id, error: warning });
319
+ // AUDIT is read-only, so a local workspace failure does not create a
320
+ // write race; execution may continue at the repository root.
321
+ return { workspaceRoot: root, warning };
322
+ }
323
+ return { workspaceId: extractWorkspaceId(result.stdout), workspaceRoot: root };
286
324
  }
325
+ const slug = `aeh-${record.id.toLowerCase().replace(/[^a-z0-9-]+/g, "-").slice(0, 80)}`;
326
+ const branch = `aeh/op-${record.id.toLowerCase().replace(/[^a-z0-9-]+/g, "-").slice(0, 96)}`;
327
+ const base = config.validation?.baseRef ?? "HEAD";
328
+ const command = [
329
+ "paseo workspace create",
330
+ "--isolation worktree",
331
+ `--path ${quote(root)}`,
332
+ "--mode branch-off",
333
+ `--new-branch ${quote(branch)}`,
334
+ `--base ${quote(base)}`,
335
+ `--worktree-slug ${quote(slug)}`,
336
+ `--title ${quote(title)}`,
337
+ "--json"
338
+ ].join(" ");
339
+ await trace(root, "workspace.cli.required", { operationId: record.id, kind: record.kind, reason: "mutating operations require isolated worktree execution", isolation: "worktree", branch, base });
340
+ const result = await safeRun(run, command, root, 180_000);
287
341
  if (result.exitCode !== 0) {
288
- await trace(root, "workspace.cli.error", {
289
- operationId: record.id,
290
- exitCode: result.exitCode,
291
- error: result.stderr || result.stdout
292
- });
293
- return {
294
- warning: `Paseo operation workspace could not be created: ${result.stderr || result.stdout || `exit ${result.exitCode}`}`
295
- };
296
- }
297
- const workspaceId = extractWorkspaceId(result.stdout);
298
- if (workspaceId) {
299
- await trace(root, "workspace.cli.created", {
300
- operationId: record.id,
301
- workspaceId,
302
- isolation: "local"
303
- });
304
- return { workspaceId };
342
+ throw new Error(`AEH_OPERATION_WORKTREE_REQUIRED: unable to create isolated worktree for ${record.id}: ${result.stderr || result.stdout || `exit ${result.exitCode}`}`);
305
343
  }
306
- await trace(root, "workspace.cli.error", {
307
- operationId: record.id,
308
- error: "workspace id missing from CLI response"
309
- });
310
- return {
311
- warning: "Paseo created an operation workspace but AEH could not resolve its id."
312
- };
344
+ let workspaceId = extractWorkspaceId(result.stdout);
345
+ let workspaceRoot = extractWorkspacePath(result.stdout);
346
+ if (!workspaceId || !workspaceRoot) {
347
+ const list = await safeRun(run, "paseo workspace ls --json", root, 60_000);
348
+ if (list.exitCode === 0) {
349
+ workspaceId ??= findWorkspaceByBranch(list.stdout, branch)?.workspaceId;
350
+ workspaceRoot ??= findWorkspaceByBranch(list.stdout, branch)?.workspaceRoot;
351
+ }
352
+ }
353
+ if (!workspaceRoot) {
354
+ throw new Error(`AEH_OPERATION_WORKTREE_REQUIRED: Paseo created a worktree workspace for ${record.id} but did not expose a resolvable worktree path.`);
355
+ }
356
+ await trace(root, "workspace.cli.created", { operationId: record.id, workspaceId: workspaceId ?? "", workspaceRoot, isolation: "worktree", branch });
357
+ return { workspaceId, workspaceRoot };
313
358
  }
314
359
  export function extractWorkspaceId(text) {
315
360
  if (!text.trim())
@@ -321,6 +366,16 @@ export function extractWorkspaceId(text) {
321
366
  return text.match(/\b(?:workspace(?:Id)?[=: ]+)?(workspace-[A-Za-z0-9._-]+)\b/i)?.[1];
322
367
  }
323
368
  }
369
+ export function extractWorkspacePath(text) {
370
+ if (!text.trim())
371
+ return undefined;
372
+ try {
373
+ return findWorkspacePath(JSON.parse(text));
374
+ }
375
+ catch {
376
+ return undefined;
377
+ }
378
+ }
324
379
  function findWorkspaceId(value) {
325
380
  if (!value || typeof value !== "object")
326
381
  return undefined;
@@ -335,15 +390,8 @@ function findWorkspaceId(value) {
335
390
  const record = value;
336
391
  for (const key of ["workspaceId", "workspace_id", "id"]) {
337
392
  const candidate = record[key];
338
- if (typeof candidate === "string" &&
339
- candidate &&
340
- (key !== "id" ||
341
- /workspace/i.test(candidate) ||
342
- "cwd" in record ||
343
- "worktreePath" in record ||
344
- "path" in record)) {
393
+ if (typeof candidate === "string" && candidate && (key !== "id" || /workspace/i.test(candidate) || "cwd" in record || "worktreePath" in record || "path" in record))
345
394
  return candidate;
346
- }
347
395
  }
348
396
  for (const child of Object.values(record)) {
349
397
  const id = findWorkspaceId(child);
@@ -352,8 +400,99 @@ function findWorkspaceId(value) {
352
400
  }
353
401
  return undefined;
354
402
  }
355
- function isTerminal(status) {
356
- return status === "SUCCEEDED" || status === "FAILED" || status === "CANCELLED";
403
+ function findWorkspacePath(value) {
404
+ if (!value || typeof value !== "object")
405
+ return undefined;
406
+ if (Array.isArray(value)) {
407
+ for (const item of value) {
408
+ const found = findWorkspacePath(item);
409
+ if (found)
410
+ return found;
411
+ }
412
+ return undefined;
413
+ }
414
+ const record = value;
415
+ for (const key of ["worktreePath", "worktree_path", "checkoutPath", "checkout_path"]) {
416
+ const candidate = record[key];
417
+ if (typeof candidate === "string" && path.isAbsolute(candidate))
418
+ return candidate;
419
+ }
420
+ if (String(record.isolation ?? record.isolationMode ?? "").toLowerCase().includes("worktree")) {
421
+ for (const key of ["path", "cwd"]) {
422
+ const candidate = record[key];
423
+ if (typeof candidate === "string" && path.isAbsolute(candidate))
424
+ return candidate;
425
+ }
426
+ }
427
+ for (const child of Object.values(record)) {
428
+ const found = findWorkspacePath(child);
429
+ if (found)
430
+ return found;
431
+ }
432
+ return undefined;
433
+ }
434
+ function findWorkspaceByBranch(text, branch) {
435
+ try {
436
+ const value = JSON.parse(text);
437
+ return findBranchWorkspace(value, branch);
438
+ }
439
+ catch {
440
+ return undefined;
441
+ }
442
+ }
443
+ function findBranchWorkspace(value, branch) {
444
+ if (Array.isArray(value)) {
445
+ for (const item of value) {
446
+ const found = findBranchWorkspace(item, branch);
447
+ if (found)
448
+ return found;
449
+ }
450
+ return undefined;
451
+ }
452
+ if (!value || typeof value !== "object")
453
+ return undefined;
454
+ const record = value;
455
+ const branchValue = [record.branch, record.branchName, record.branch_name, record.gitBranch].find((item) => typeof item === "string");
456
+ if (branchValue === branch)
457
+ return { workspaceId: findWorkspaceId(record), workspaceRoot: findWorkspacePath(record) };
458
+ for (const child of Object.values(record)) {
459
+ const found = findBranchWorkspace(child, branch);
460
+ if (found)
461
+ return found;
462
+ }
463
+ return undefined;
464
+ }
465
+ async function safeRun(run, command, cwd, timeoutMs) {
466
+ try {
467
+ return await run(command, { cwd, timeoutMs });
468
+ }
469
+ catch (error) {
470
+ return { exitCode: 1, stdout: "", stderr: String(error), durationMs: 0 };
471
+ }
472
+ }
473
+ async function loadProjectConfigIfPresent(root) {
474
+ try {
475
+ await fs.access(path.resolve(root, ".harness/project.yaml"));
476
+ }
477
+ catch {
478
+ return undefined;
479
+ }
480
+ return loadProjectConfig(root);
481
+ }
482
+ function initialIntent(kind, payload) {
483
+ if (kind === "audit") {
484
+ const audit = payload;
485
+ return { request: audit.request, classification: "AUDIT", risk: audit.risk, priority: 50 };
486
+ }
487
+ if (kind === "change") {
488
+ const change = payload;
489
+ return { request: change.request, classification: "CHANGE", risk: change.risk, priority: change.priority ?? 50 };
490
+ }
491
+ return { classification: "RUN", priority: payload.priority ?? 50 };
492
+ }
493
+ function operationPriority(payload) {
494
+ const value = "priority" in payload ? payload.priority : undefined;
495
+ return typeof value === "number" && Number.isFinite(value) ? value : 50;
357
496
  }
358
497
  function quote(value) {
359
498
  return `'${value.replaceAll("'", "'\\''")}'`;