agentic-engineering-harness 0.6.13 → 0.6.14
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/dist/agents/outputContracts.d.ts +44 -1
- package/dist/agents/outputContracts.js +12 -2
- package/dist/agents/outputContracts.js.map +1 -1
- package/dist/agents/reviewLifecycle.js +54 -14
- package/dist/agents/reviewLifecycle.js.map +1 -1
- package/dist/agents/waveExecutor.d.ts +2 -13
- package/dist/audit/run.d.ts +7 -0
- package/dist/audit/run.js +198 -32
- package/dist/audit/run.js.map +1 -1
- package/dist/core/run.js +103 -28
- package/dist/core/run.js.map +1 -1
- package/dist/main.js +95 -29
- package/dist/main.js.map +1 -1
- package/dist/operations/artifacts.d.ts +13 -0
- package/dist/operations/artifacts.js +45 -0
- package/dist/operations/artifacts.js.map +1 -0
- package/dist/operations/change.d.ts +11 -0
- package/dist/operations/change.js +144 -0
- package/dist/operations/change.js.map +1 -0
- package/dist/operations/completion.d.ts +4 -1
- package/dist/operations/completion.js +69 -89
- package/dist/operations/completion.js.map +1 -1
- package/dist/operations/controller.d.ts +8 -5
- package/dist/operations/controller.js +270 -131
- package/dist/operations/controller.js.map +1 -1
- package/dist/operations/leadBinding.d.ts +2 -0
- package/dist/operations/leadBinding.js +22 -0
- package/dist/operations/leadBinding.js.map +1 -0
- package/dist/operations/liveness.d.ts +35 -0
- package/dist/operations/liveness.js +308 -0
- package/dist/operations/liveness.js.map +1 -0
- package/dist/operations/mcp.js +72 -55
- package/dist/operations/mcp.js.map +1 -1
- package/dist/operations/monitorProcess.d.ts +8 -0
- package/dist/operations/monitorProcess.js +33 -0
- package/dist/operations/monitorProcess.js.map +1 -0
- package/dist/operations/portfolio.d.ts +35 -0
- package/dist/operations/portfolio.js +158 -0
- package/dist/operations/portfolio.js.map +1 -0
- package/dist/operations/state.d.ts +174 -17
- package/dist/operations/state.js +178 -163
- package/dist/operations/state.js.map +1 -1
- package/dist/operations/supervisor.d.ts +38 -0
- package/dist/operations/supervisor.js +195 -0
- package/dist/operations/supervisor.js.map +1 -0
- package/dist/paseo/launchSpec.d.ts +4 -0
- package/dist/paseo/launchSpec.js +31 -37
- package/dist/paseo/launchSpec.js.map +1 -1
- package/dist/paseo/sdk.d.ts +5 -0
- package/dist/paseo/sdk.js +35 -87
- package/dist/paseo/sdk.js.map +1 -1
- package/dist/paseo/start.d.ts +1 -1
- package/dist/paseo/start.js +38 -87
- package/dist/paseo/start.js.map +1 -1
- package/dist/workers/agentPrompt.d.ts +2 -0
- package/dist/workers/agentPrompt.js +135 -190
- package/dist/workers/agentPrompt.js.map +1 -1
- package/docs/OPERATION_SUPERVISION.md +198 -0
- package/package.json +1 -1
- package/presets/agents/orchestration.jsonc +14 -4
- 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 {
|
|
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
|
-
|
|
18
|
-
version:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
118
|
-
|
|
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 (
|
|
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 (
|
|
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
|
|
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
|
-
|
|
213
|
-
|
|
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
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
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
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
warning
|
|
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
|
-
|
|
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
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
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
|
|
356
|
-
|
|
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("'", "'\\''")}'`;
|