@engineeros/connector 0.15.2 → 0.16.0
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/README.md +105 -111
- package/bin/engineeros-connector.mjs +1021 -982
- package/package.json +41 -41
- package/src/acp-client.mjs +69 -68
- package/src/assessment-spool.mjs +282 -227
- package/src/codex-app-server.mjs +3 -2
- package/src/connection.mjs +8 -2
- package/src/runner.mjs +2097 -2094
package/src/runner.mjs
CHANGED
|
@@ -1,2121 +1,2124 @@
|
|
|
1
|
-
import { spawn } from "node:child_process";
|
|
2
|
-
import { createHash } from "node:crypto";
|
|
3
|
-
import {
|
|
4
|
-
lstat,
|
|
5
|
-
mkdir,
|
|
6
|
-
readFile,
|
|
7
|
-
readdir,
|
|
8
|
-
stat,
|
|
9
|
-
writeFile,
|
|
10
|
-
} from "node:fs/promises";
|
|
11
|
-
import os from "node:os";
|
|
12
|
-
import path from "node:path";
|
|
13
|
-
import { promisify } from "node:util";
|
|
14
|
-
import { deflateRaw, gzip } from "node:zlib";
|
|
15
|
-
import { inspectAcpExecutionProfiles, launchAcpAgent } from "./acp-client.mjs";
|
|
16
|
-
import {
|
|
17
|
-
inspectCodexExecutionProfiles,
|
|
18
|
-
launchCodexAppServer,
|
|
19
|
-
} from "./codex-app-server.mjs";
|
|
20
|
-
import {
|
|
21
|
-
buildAgentHarnessPrompt,
|
|
22
|
-
normalizeAgentStructuredOutput,
|
|
23
|
-
} from "./agent-harness.mjs";
|
|
24
|
-
|
|
25
|
-
const deflate = promisify(deflateRaw);
|
|
26
|
-
const gzipBuffer = promisify(gzip);
|
|
27
|
-
const MAX_ARCHIVE_BYTES = 25_000_000;
|
|
28
|
-
const MAX_EVIDENCE_BYTES = 24_000_000;
|
|
29
|
-
const MAX_SHAREABLE_FILE_BYTES = 5 * 1024 * 1024;
|
|
30
|
-
const MAX_EVIDENCE_FILES = 5_000;
|
|
31
|
-
const MAX_INVENTORY_FILES = 100_000;
|
|
32
|
-
const MAX_COMPRESSED_INVENTORY_BYTES = 10_000_000;
|
|
33
|
-
const EVIDENCE_POLICY_VERSION = "workspace-evidence-v1";
|
|
1
|
+
import { spawn } from "node:child_process";
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
3
|
+
import {
|
|
4
|
+
lstat,
|
|
5
|
+
mkdir,
|
|
6
|
+
readFile,
|
|
7
|
+
readdir,
|
|
8
|
+
stat,
|
|
9
|
+
writeFile,
|
|
10
|
+
} from "node:fs/promises";
|
|
11
|
+
import os from "node:os";
|
|
12
|
+
import path from "node:path";
|
|
13
|
+
import { promisify } from "node:util";
|
|
14
|
+
import { deflateRaw, gzip } from "node:zlib";
|
|
15
|
+
import { inspectAcpExecutionProfiles, launchAcpAgent } from "./acp-client.mjs";
|
|
16
|
+
import {
|
|
17
|
+
inspectCodexExecutionProfiles,
|
|
18
|
+
launchCodexAppServer,
|
|
19
|
+
} from "./codex-app-server.mjs";
|
|
20
|
+
import {
|
|
21
|
+
buildAgentHarnessPrompt,
|
|
22
|
+
normalizeAgentStructuredOutput,
|
|
23
|
+
} from "./agent-harness.mjs";
|
|
24
|
+
|
|
25
|
+
const deflate = promisify(deflateRaw);
|
|
26
|
+
const gzipBuffer = promisify(gzip);
|
|
27
|
+
const MAX_ARCHIVE_BYTES = 25_000_000;
|
|
28
|
+
const MAX_EVIDENCE_BYTES = 24_000_000;
|
|
29
|
+
const MAX_SHAREABLE_FILE_BYTES = 5 * 1024 * 1024;
|
|
30
|
+
const MAX_EVIDENCE_FILES = 5_000;
|
|
31
|
+
const MAX_INVENTORY_FILES = 100_000;
|
|
32
|
+
const MAX_COMPRESSED_INVENTORY_BYTES = 10_000_000;
|
|
33
|
+
const EVIDENCE_POLICY_VERSION = "workspace-evidence-v1";
|
|
34
34
|
export const ASSESSMENT_INACTIVITY_TIMEOUT_MS = 10 * 60 * 1_000;
|
|
35
35
|
export const ASSESSMENT_RESULT_TIMEOUT_MS = 120_000;
|
|
36
36
|
export const MAX_REPEATED_ASSESSMENT_VALIDATION_FAILURES = 3;
|
|
37
|
-
export const
|
|
38
|
-
export const
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
".
|
|
42
|
-
".
|
|
43
|
-
".
|
|
44
|
-
".
|
|
45
|
-
".
|
|
46
|
-
".
|
|
47
|
-
".
|
|
48
|
-
".
|
|
49
|
-
".
|
|
50
|
-
".
|
|
51
|
-
"
|
|
52
|
-
"
|
|
53
|
-
"
|
|
54
|
-
"
|
|
55
|
-
"
|
|
56
|
-
"
|
|
57
|
-
"
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
const
|
|
61
|
-
|
|
62
|
-
".
|
|
63
|
-
".
|
|
64
|
-
".
|
|
65
|
-
".
|
|
66
|
-
".
|
|
67
|
-
".
|
|
68
|
-
".
|
|
69
|
-
".
|
|
70
|
-
".
|
|
71
|
-
".
|
|
72
|
-
".
|
|
73
|
-
".
|
|
74
|
-
".
|
|
75
|
-
".
|
|
76
|
-
".
|
|
77
|
-
".
|
|
78
|
-
".
|
|
79
|
-
".
|
|
80
|
-
".
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
"
|
|
85
|
-
"
|
|
86
|
-
"
|
|
87
|
-
"
|
|
88
|
-
"
|
|
89
|
-
"
|
|
90
|
-
"
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
"
|
|
95
|
-
"index.
|
|
96
|
-
"
|
|
97
|
-
"main.
|
|
98
|
-
"main.
|
|
99
|
-
"
|
|
100
|
-
"
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
".
|
|
105
|
-
".
|
|
106
|
-
".
|
|
107
|
-
"
|
|
108
|
-
"
|
|
109
|
-
"
|
|
110
|
-
"
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
".
|
|
115
|
-
".
|
|
116
|
-
".
|
|
117
|
-
".
|
|
118
|
-
".
|
|
119
|
-
".
|
|
120
|
-
".
|
|
121
|
-
".
|
|
122
|
-
".
|
|
123
|
-
".
|
|
124
|
-
".
|
|
125
|
-
".
|
|
126
|
-
".
|
|
127
|
-
".
|
|
128
|
-
".
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
"
|
|
133
|
-
"
|
|
134
|
-
"
|
|
135
|
-
"
|
|
136
|
-
"
|
|
137
|
-
"
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
assignment.
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
execution.
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
const
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
assignment.
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
const
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
committed.
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
const
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
const
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
}
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
await run(
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
"
|
|
279
|
-
"
|
|
280
|
-
"
|
|
281
|
-
"
|
|
282
|
-
"
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
const
|
|
289
|
-
const
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
-
|
|
327
|
-
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
const
|
|
343
|
-
const
|
|
344
|
-
const
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
}
|
|
362
|
-
|
|
37
|
+
export const MIN_ASSESSMENT_WORKERS = 3;
|
|
38
|
+
export const MAX_ASSESSMENT_WORKERS = 8;
|
|
39
|
+
export const DEFAULT_ASSESSMENT_WORKERS = MIN_ASSESSMENT_WORKERS;
|
|
40
|
+
const EXCLUDED_DIRECTORIES = new Set([
|
|
41
|
+
".agents",
|
|
42
|
+
".claude",
|
|
43
|
+
".codex",
|
|
44
|
+
".engineeros",
|
|
45
|
+
".forge",
|
|
46
|
+
".gemini",
|
|
47
|
+
".git",
|
|
48
|
+
".next",
|
|
49
|
+
".pytest_cache",
|
|
50
|
+
".ruff_cache",
|
|
51
|
+
".venv",
|
|
52
|
+
"__pycache__",
|
|
53
|
+
"build",
|
|
54
|
+
"coverage",
|
|
55
|
+
"dist",
|
|
56
|
+
"node_modules",
|
|
57
|
+
"target",
|
|
58
|
+
"vendor",
|
|
59
|
+
]);
|
|
60
|
+
const EXCLUDED_PATH_PREFIXES = [".github/skills/"];
|
|
61
|
+
const EXCLUDED_FILE_EXTENSIONS = new Set([
|
|
62
|
+
".7z",
|
|
63
|
+
".a",
|
|
64
|
+
".bin",
|
|
65
|
+
".class",
|
|
66
|
+
".dll",
|
|
67
|
+
".dylib",
|
|
68
|
+
".exe",
|
|
69
|
+
".gz",
|
|
70
|
+
".jar",
|
|
71
|
+
".lib",
|
|
72
|
+
".o",
|
|
73
|
+
".obj",
|
|
74
|
+
".pyc",
|
|
75
|
+
".pyo",
|
|
76
|
+
".rar",
|
|
77
|
+
".so",
|
|
78
|
+
".tar",
|
|
79
|
+
".tgz",
|
|
80
|
+
".war",
|
|
81
|
+
".zip",
|
|
82
|
+
]);
|
|
83
|
+
const LOCK_FILE_NAMES = new Set([
|
|
84
|
+
"cargo.lock",
|
|
85
|
+
"composer.lock",
|
|
86
|
+
"go.sum",
|
|
87
|
+
"package-lock.json",
|
|
88
|
+
"pnpm-lock.yaml",
|
|
89
|
+
"poetry.lock",
|
|
90
|
+
"uv.lock",
|
|
91
|
+
"yarn.lock",
|
|
92
|
+
]);
|
|
93
|
+
const ENTRYPOINT_NAMES = new Set([
|
|
94
|
+
"app.py",
|
|
95
|
+
"index.ts",
|
|
96
|
+
"index.tsx",
|
|
97
|
+
"main.py",
|
|
98
|
+
"main.ts",
|
|
99
|
+
"main.tsx",
|
|
100
|
+
"manage.py",
|
|
101
|
+
"server.py",
|
|
102
|
+
]);
|
|
103
|
+
const REPOSITORY_CONFIGURATION_NAMES = new Set([
|
|
104
|
+
".dockerignore",
|
|
105
|
+
".editorconfig",
|
|
106
|
+
".gitattributes",
|
|
107
|
+
".gitignore",
|
|
108
|
+
"lerna.json",
|
|
109
|
+
"makefile",
|
|
110
|
+
"nx.json",
|
|
111
|
+
"turbo.json",
|
|
112
|
+
]);
|
|
113
|
+
const CODE_EXTENSIONS = new Set([
|
|
114
|
+
".c",
|
|
115
|
+
".cpp",
|
|
116
|
+
".cs",
|
|
117
|
+
".go",
|
|
118
|
+
".h",
|
|
119
|
+
".java",
|
|
120
|
+
".js",
|
|
121
|
+
".jsx",
|
|
122
|
+
".mjs",
|
|
123
|
+
".php",
|
|
124
|
+
".py",
|
|
125
|
+
".rb",
|
|
126
|
+
".rs",
|
|
127
|
+
".sql",
|
|
128
|
+
".ts",
|
|
129
|
+
".tsx",
|
|
130
|
+
]);
|
|
131
|
+
const CODE_MARKERS = new Set([
|
|
132
|
+
"cargo.toml",
|
|
133
|
+
"composer.json",
|
|
134
|
+
"go.mod",
|
|
135
|
+
"package.json",
|
|
136
|
+
"pom.xml",
|
|
137
|
+
"pyproject.toml",
|
|
138
|
+
"requirements.txt",
|
|
139
|
+
]);
|
|
140
|
+
|
|
141
|
+
export async function executeAssignment(assignment, config, callbacks) {
|
|
142
|
+
const execution = connectorExecution(assignment);
|
|
143
|
+
const runWorkspace = await prepareRunWorkspace(
|
|
144
|
+
config.workspace,
|
|
145
|
+
assignment.run_id,
|
|
146
|
+
assignment.base_revision,
|
|
147
|
+
);
|
|
148
|
+
const controller = launchAgentProcess(
|
|
149
|
+
runWorkspace,
|
|
150
|
+
execution.prompt,
|
|
151
|
+
execution.sandboxMode,
|
|
152
|
+
config,
|
|
153
|
+
callbacks,
|
|
154
|
+
execution.profile,
|
|
155
|
+
undefined,
|
|
156
|
+
{ runId: assignment.run_id },
|
|
157
|
+
);
|
|
158
|
+
callbacks.onProcess?.(controller.child);
|
|
159
|
+
const implementation = await controller.completed;
|
|
160
|
+
const changedFiles = await changedFilePaths(runWorkspace);
|
|
161
|
+
if (!changedFiles.length)
|
|
162
|
+
throw new Error("Agent completed without changing any files.");
|
|
163
|
+
const committed = await commitRunChange(
|
|
164
|
+
runWorkspace,
|
|
165
|
+
assignment.base_revision,
|
|
166
|
+
assignment.run_id,
|
|
167
|
+
);
|
|
168
|
+
const verificationHeading = "# Verification Report";
|
|
169
|
+
const verifier = launchAgentProcess(
|
|
170
|
+
runWorkspace,
|
|
171
|
+
buildAgentHarnessPrompt({
|
|
172
|
+
agentRole: "verification",
|
|
173
|
+
prompt: verificationPrompt(
|
|
174
|
+
assignment,
|
|
175
|
+
committed.revision,
|
|
176
|
+
committed.changedFiles,
|
|
177
|
+
),
|
|
178
|
+
sandboxMode: "read-only",
|
|
179
|
+
requiredOutputHeading: verificationHeading,
|
|
180
|
+
}),
|
|
181
|
+
"read-only",
|
|
182
|
+
config,
|
|
183
|
+
callbacks,
|
|
184
|
+
execution.profile,
|
|
185
|
+
);
|
|
186
|
+
callbacks.onProcess?.(verifier.child);
|
|
187
|
+
const verification = await verifier.completed;
|
|
188
|
+
const verificationReport = normalizeAgentStructuredOutput(
|
|
189
|
+
verification.finalMessage,
|
|
190
|
+
verificationHeading,
|
|
191
|
+
);
|
|
192
|
+
const proofEvidence = parseVerificationReport(
|
|
193
|
+
verificationReport,
|
|
194
|
+
assignment.proof_checks,
|
|
195
|
+
config.connector_id,
|
|
196
|
+
assignment.run_id,
|
|
197
|
+
);
|
|
198
|
+
return {
|
|
199
|
+
head_revision: committed.revision,
|
|
200
|
+
repository_locator: assignment.repository_locator,
|
|
201
|
+
external_reference: `connector:${config.connector_id}/run:${assignment.run_id}`,
|
|
202
|
+
diff_patch: committed.diffPatch,
|
|
203
|
+
changed_files: committed.changedFiles,
|
|
204
|
+
proof_evidence: proofEvidence,
|
|
205
|
+
verification_report: verificationReport,
|
|
206
|
+
usage: [
|
|
207
|
+
{
|
|
208
|
+
request_type: "goal_implementation",
|
|
209
|
+
model: implementation.model,
|
|
210
|
+
...implementation.usage,
|
|
211
|
+
},
|
|
212
|
+
{
|
|
213
|
+
request_type: "goal_verification",
|
|
214
|
+
model: verification.model,
|
|
215
|
+
...verification.usage,
|
|
216
|
+
},
|
|
217
|
+
].filter((entry) => entry.total_tokens > 0),
|
|
218
|
+
};
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
export async function applyAcceptedChange(
|
|
222
|
+
workspace,
|
|
223
|
+
baseRevision,
|
|
224
|
+
headRevision,
|
|
225
|
+
) {
|
|
226
|
+
const current = await run("git", ["rev-parse", "HEAD"], workspace, {
|
|
227
|
+
allowFailure: true,
|
|
228
|
+
});
|
|
229
|
+
if (current.code !== 0) {
|
|
230
|
+
return {
|
|
231
|
+
applied: false,
|
|
232
|
+
reason: "The connected workspace is not a Git repository.",
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
const currentHead = current.stdout.trim();
|
|
236
|
+
const alreadyApplied = await run(
|
|
237
|
+
"git",
|
|
238
|
+
["merge-base", "--is-ancestor", headRevision, currentHead],
|
|
239
|
+
workspace,
|
|
240
|
+
{ allowFailure: true },
|
|
241
|
+
);
|
|
242
|
+
if (alreadyApplied.code === 0)
|
|
243
|
+
return { applied: true, revision: currentHead };
|
|
244
|
+
const status = await run("git", ["status", "--porcelain"], workspace);
|
|
245
|
+
if (status.stdout.trim()) {
|
|
246
|
+
return {
|
|
247
|
+
applied: false,
|
|
248
|
+
reason: `The connected workspace has uncommitted changes. Apply accepted commit ${headRevision} after preserving them.`,
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
if (currentHead !== baseRevision) {
|
|
252
|
+
return {
|
|
253
|
+
applied: false,
|
|
254
|
+
reason: `The connected branch moved from frozen base ${baseRevision} to ${currentHead}. Apply accepted commit ${headRevision} with git cherry-pick.`,
|
|
255
|
+
};
|
|
256
|
+
}
|
|
257
|
+
const applied = await run("git", ["cherry-pick", headRevision], workspace, {
|
|
258
|
+
allowFailure: true,
|
|
259
|
+
});
|
|
260
|
+
if (applied.code !== 0) {
|
|
261
|
+
await run("git", ["cherry-pick", "--abort"], workspace, {
|
|
262
|
+
allowFailure: true,
|
|
263
|
+
});
|
|
264
|
+
return {
|
|
265
|
+
applied: false,
|
|
266
|
+
reason: `The accepted commit ${headRevision} could not be applied cleanly. Apply it manually with git cherry-pick.`,
|
|
267
|
+
};
|
|
268
|
+
}
|
|
269
|
+
const integrated = await run("git", ["rev-parse", "HEAD"], workspace);
|
|
270
|
+
return { applied: true, revision: integrated.stdout.trim() };
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
async function commitRunChange(workspace, baseRevision, runId) {
|
|
274
|
+
await run("git", ["add", "-A"], workspace);
|
|
275
|
+
await run(
|
|
276
|
+
"git",
|
|
277
|
+
[
|
|
278
|
+
"-c",
|
|
279
|
+
"user.name=EngineerOS Codex",
|
|
280
|
+
"-c",
|
|
281
|
+
"user.email=codex@engineeros.local",
|
|
282
|
+
"commit",
|
|
283
|
+
"-m",
|
|
284
|
+
`EngineerOS Goal Run ${runId}`,
|
|
285
|
+
],
|
|
286
|
+
workspace,
|
|
287
|
+
);
|
|
288
|
+
const head = await run("git", ["rev-parse", "HEAD"], workspace);
|
|
289
|
+
const revision = head.stdout.trim();
|
|
290
|
+
const diff = await run(
|
|
291
|
+
"git",
|
|
292
|
+
["diff", "--binary", baseRevision, revision, "--"],
|
|
293
|
+
workspace,
|
|
294
|
+
);
|
|
295
|
+
const paths = await run(
|
|
296
|
+
"git",
|
|
297
|
+
["diff", "--name-only", "-z", baseRevision, revision, "--"],
|
|
298
|
+
workspace,
|
|
299
|
+
);
|
|
300
|
+
return {
|
|
301
|
+
revision,
|
|
302
|
+
diffPatch: diff.stdout,
|
|
303
|
+
changedFiles: gitPathList(paths.stdout).sort(),
|
|
304
|
+
};
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
export function verificationPrompt(assignment, revision, changedFiles) {
|
|
308
|
+
const checks = (assignment.proof_checks ?? [])
|
|
309
|
+
.map(
|
|
310
|
+
(proof, index) =>
|
|
311
|
+
`## Proof ${index + 1}\n- Check: ${proof.check ?? ""}\n- Expected: ${proof.expected ?? ""}`,
|
|
312
|
+
)
|
|
313
|
+
.join("\n\n");
|
|
314
|
+
return `# EngineerOS Connected Verification
|
|
315
|
+
|
|
316
|
+
Independently verify the frozen Goal at Git commit \`${revision}\`. Do not modify files. Run the commands or inspections needed for every Proof item, and check the Goal boundaries and constraints in the supplied packet.
|
|
317
|
+
|
|
318
|
+
Changed files:
|
|
319
|
+
${changedFiles.map((item) => `- \`${item}\``).join("\n")}
|
|
320
|
+
|
|
321
|
+
${checks}
|
|
322
|
+
|
|
323
|
+
Return structured Markdown only, with exactly one section per Proof:
|
|
324
|
+
|
|
325
|
+
## Proof 1
|
|
326
|
+
- Status: passed or failed
|
|
327
|
+
- Exit code: integer or none
|
|
328
|
+
- Evidence: concise observed output and command
|
|
329
|
+
|
|
330
|
+
Do not claim a Proof passed unless you observed it directly.`;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
export function parseVerificationReport(
|
|
334
|
+
report,
|
|
335
|
+
proofChecks,
|
|
336
|
+
connectorId,
|
|
337
|
+
runId,
|
|
338
|
+
) {
|
|
339
|
+
if (!report?.trim())
|
|
340
|
+
throw new Error("Codex returned no connected verification report.");
|
|
341
|
+
return (proofChecks ?? []).map((_, index) => {
|
|
342
|
+
const start = new RegExp(`^## Proof ${index + 1}\\s*$`, "im").exec(report);
|
|
343
|
+
const tail = start ? report.slice(start.index + start[0].length) : "";
|
|
344
|
+
const section = tail.split(/^## Proof \d+\s*$/im)[0] ?? "";
|
|
345
|
+
const status =
|
|
346
|
+
/^- Status:\s*(passed|failed)\s*$/im.exec(section)?.[1] ?? "failed";
|
|
347
|
+
const exitValue =
|
|
348
|
+
/^- Exit code:\s*(\d+|none)\s*$/im.exec(section)?.[1] ?? "none";
|
|
349
|
+
const evidence = /^- Evidence:\s*(.+)$/im.exec(section)?.[1]?.trim();
|
|
350
|
+
return {
|
|
351
|
+
proof_index: index,
|
|
352
|
+
status,
|
|
353
|
+
verifier_type: "agent_reported",
|
|
354
|
+
verifier_identity: "Connected Codex CLI verifier",
|
|
355
|
+
locator: `connector:${connectorId}/run:${runId}#proof-${index + 1}`,
|
|
356
|
+
output_excerpt:
|
|
357
|
+
evidence ||
|
|
358
|
+
"The connected verifier did not provide evidence for this Proof.",
|
|
359
|
+
exit_code: exitValue === "none" ? null : Number(exitValue),
|
|
360
|
+
};
|
|
361
|
+
});
|
|
362
|
+
}
|
|
363
|
+
|
|
363
364
|
export async function executeWorkspaceAssessment(
|
|
364
365
|
assignment,
|
|
365
366
|
config,
|
|
366
367
|
callbacks,
|
|
368
|
+
{ previousSessionId } = {},
|
|
367
369
|
) {
|
|
368
|
-
const execution = workspaceAssessmentExecution(assignment);
|
|
369
|
-
const assessmentAcpContext =
|
|
370
|
-
config.agent_protocol === "acp"
|
|
371
|
-
? {
|
|
372
|
-
persistentAcp: true,
|
|
373
|
-
sessionKey: `assessment:${assignment.assessment_id}:${assignment.stage}`,
|
|
374
|
-
}
|
|
375
|
-
: undefined;
|
|
376
|
-
const launchAssessmentAgent = (turnPrompt, previousSessionId) => {
|
|
377
|
-
const controller = launchAgentProcess(
|
|
378
|
-
config.workspace,
|
|
379
|
-
turnPrompt,
|
|
380
|
-
execution.sandboxMode,
|
|
381
|
-
config,
|
|
382
|
-
callbacks,
|
|
383
|
-
execution.profile,
|
|
384
|
-
previousSessionId,
|
|
385
|
-
assessmentAcpContext,
|
|
386
|
-
);
|
|
387
|
-
callbacks.onController?.(controller);
|
|
388
|
-
callbacks.onProcess?.(controller.child);
|
|
389
|
-
return controller;
|
|
390
|
-
};
|
|
391
|
-
const startingRevision = await run(
|
|
392
|
-
"git",
|
|
393
|
-
["rev-parse", "HEAD"],
|
|
394
|
-
config.workspace,
|
|
395
|
-
{ allowFailure: true },
|
|
396
|
-
);
|
|
397
|
-
const startingHead =
|
|
398
|
-
startingRevision.code === 0
|
|
399
|
-
? startingRevision.stdout.trim().slice(0, 128)
|
|
400
|
-
: null;
|
|
401
|
-
if (
|
|
402
|
-
assignment.target_head_revision &&
|
|
403
|
-
startingHead !== assignment.target_head_revision
|
|
404
|
-
) {
|
|
405
|
-
throw new Error(
|
|
406
|
-
"This workspace is no longer at the commit inventoried by EngineerOS. Refresh the workspace inventory before assessing it.",
|
|
407
|
-
);
|
|
408
|
-
}
|
|
409
|
-
const changeImpact =
|
|
410
|
-
assignment.assessment_mode === "incremental"
|
|
411
|
-
? await workspaceChangeImpact(
|
|
412
|
-
config.workspace,
|
|
413
|
-
assignment.base_head_revision,
|
|
414
|
-
assignment.target_head_revision,
|
|
415
|
-
)
|
|
416
|
-
: { changedFiles: [], markdown: null };
|
|
417
|
-
const prompt = changeImpact.markdown
|
|
418
|
-
? execution.prompt.replace(
|
|
419
|
-
"<!-- ENGINEEROS_CHANGE_IMPACT -->",
|
|
420
|
-
changeImpact.markdown,
|
|
421
|
-
)
|
|
422
|
-
: execution.prompt;
|
|
423
|
-
const controller = launchAssessmentAgent(prompt);
|
|
424
|
-
let completed = await controller.completed;
|
|
425
|
-
const recovered = await recoverAssessmentStageOutput({
|
|
426
|
-
completed,
|
|
427
|
-
prompt,
|
|
428
|
-
requiredOutputHeading: execution.requiredOutputHeading,
|
|
429
|
-
retry: async (correctionPrompt, previousSessionId) => {
|
|
430
|
-
callbacks.onEvent?.({
|
|
431
|
-
type: "assessment.output_correction",
|
|
432
|
-
message: "The Agent is completing the required stage report structure",
|
|
433
|
-
});
|
|
434
|
-
const correction = launchAssessmentAgent(
|
|
435
|
-
correctionPrompt,
|
|
436
|
-
previousSessionId,
|
|
437
|
-
);
|
|
438
|
-
return correction.completed;
|
|
439
|
-
},
|
|
440
|
-
});
|
|
441
|
-
completed = recovered.completed;
|
|
442
|
-
const buildResult = async (turn, report) => {
|
|
443
|
-
const endingRevision = await run(
|
|
444
|
-
"git",
|
|
445
|
-
["rev-parse", "HEAD"],
|
|
446
|
-
config.workspace,
|
|
447
|
-
{ allowFailure: true },
|
|
448
|
-
);
|
|
449
|
-
const endingHead =
|
|
450
|
-
endingRevision.code === 0
|
|
451
|
-
? endingRevision.stdout.trim().slice(0, 128)
|
|
452
|
-
: null;
|
|
453
|
-
if (startingHead !== endingHead) {
|
|
454
|
-
throw new Error(
|
|
455
|
-
"The Git commit changed during assessment. Refresh the workspace inventory and assess the new commit.",
|
|
456
|
-
);
|
|
457
|
-
}
|
|
458
|
-
return {
|
|
459
|
-
stage: assignment.stage,
|
|
460
|
-
report_markdown: report,
|
|
461
|
-
observed_head_revision: endingHead,
|
|
462
|
-
changed_files: changeImpact.changedFiles,
|
|
463
|
-
change_impact_markdown: changeImpact.markdown,
|
|
464
|
-
model: turn.model ?? config.agent_protocol ?? "coding-agent",
|
|
465
|
-
usage: turn.usage ?? null,
|
|
466
|
-
agent_session_id: turn.sessionId ?? null,
|
|
467
|
-
};
|
|
468
|
-
};
|
|
469
|
-
const result = await buildResult(completed, recovered.report);
|
|
470
|
-
attachAssessmentRejectionCorrection(result, assignment, config, callbacks, {
|
|
471
|
-
previousSessionId: completed.sessionId,
|
|
472
|
-
buildResult,
|
|
473
|
-
});
|
|
474
|
-
return result;
|
|
475
|
-
}
|
|
476
|
-
|
|
477
|
-
export function attachAssessmentRejectionCorrection(
|
|
478
|
-
result,
|
|
479
|
-
assignment,
|
|
480
|
-
config,
|
|
481
|
-
callbacks,
|
|
482
|
-
{ previousSessionId = result.agent_session_id, draftPath, buildResult } = {},
|
|
483
|
-
) {
|
|
484
|
-
const execution = workspaceAssessmentExecution(assignment);
|
|
485
|
-
const assessmentAcpContext =
|
|
486
|
-
config.agent_protocol === "acp"
|
|
487
|
-
? {
|
|
488
|
-
persistentAcp: true,
|
|
489
|
-
sessionKey: `assessment:${assignment.assessment_id}:${assignment.stage}`,
|
|
490
|
-
}
|
|
491
|
-
: undefined;
|
|
492
|
-
Object.defineProperty(result, "correctAfterRejection", {
|
|
493
|
-
enumerable: false,
|
|
494
|
-
value: async (validationMessage) => {
|
|
495
|
-
callbacks.onEvent?.({
|
|
496
|
-
type: "assessment.output_correction",
|
|
497
|
-
message: "The Agent is correcting the rejected stage report",
|
|
498
|
-
});
|
|
499
|
-
const draftInstruction = draftPath
|
|
500
|
-
? [
|
|
501
|
-
"",
|
|
502
|
-
`The authoritative draft is stored at \`${draftPath}\`. Read that file before making the requested correction.`,
|
|
503
|
-
].join("\n")
|
|
504
|
-
: "";
|
|
505
|
-
const correctionPrompt = `${assessmentRejectionCorrectionPrompt(validationMessage, execution.requiredOutputHeading)}${draftInstruction}`;
|
|
506
|
-
const launchCorrection = (prompt, sessionId) => {
|
|
507
|
-
const controller = launchAgentProcess(
|
|
508
|
-
config.workspace,
|
|
509
|
-
prompt,
|
|
510
|
-
execution.sandboxMode,
|
|
511
|
-
config,
|
|
512
|
-
callbacks,
|
|
513
|
-
execution.profile,
|
|
514
|
-
sessionId,
|
|
515
|
-
assessmentAcpContext,
|
|
516
|
-
);
|
|
517
|
-
callbacks.onController?.(controller);
|
|
518
|
-
callbacks.onProcess?.(controller.child);
|
|
519
|
-
return controller;
|
|
520
|
-
};
|
|
521
|
-
let corrected = await launchCorrection(
|
|
522
|
-
correctionPrompt,
|
|
523
|
-
previousSessionId,
|
|
524
|
-
).completed;
|
|
525
|
-
const recovered = await recoverAssessmentStageOutput({
|
|
526
|
-
completed: corrected,
|
|
527
|
-
prompt: correctionPrompt,
|
|
528
|
-
requiredOutputHeading: execution.requiredOutputHeading,
|
|
529
|
-
retry: async (formatPrompt, sessionId) => {
|
|
530
|
-
callbacks.onEvent?.({
|
|
531
|
-
type: "assessment.output_correction",
|
|
532
|
-
message: "The Agent is formatting the corrected stage report",
|
|
533
|
-
});
|
|
534
|
-
return launchCorrection(formatPrompt, sessionId).completed;
|
|
535
|
-
},
|
|
536
|
-
});
|
|
537
|
-
corrected = recovered.completed;
|
|
538
|
-
const correctedReport = recovered.report;
|
|
539
|
-
if (buildResult) {
|
|
540
|
-
return buildResult(
|
|
541
|
-
{
|
|
542
|
-
...corrected,
|
|
543
|
-
usage: mergeTokenUsage(result.usage, corrected.usage),
|
|
544
|
-
},
|
|
545
|
-
correctedReport,
|
|
546
|
-
);
|
|
547
|
-
}
|
|
548
|
-
const endingRevision = await run(
|
|
549
|
-
"git",
|
|
550
|
-
["rev-parse", "HEAD"],
|
|
551
|
-
config.workspace,
|
|
552
|
-
{ allowFailure: true },
|
|
553
|
-
);
|
|
554
|
-
const endingHead =
|
|
555
|
-
endingRevision.code === 0
|
|
556
|
-
? endingRevision.stdout.trim().slice(0, 128)
|
|
557
|
-
: null;
|
|
558
|
-
if (
|
|
559
|
-
assignment.target_head_revision &&
|
|
560
|
-
endingHead !== assignment.target_head_revision
|
|
561
|
-
) {
|
|
562
|
-
throw new Error(
|
|
563
|
-
"The Git commit changed before assessment correction. Refresh the workspace inventory and assess the new commit.",
|
|
564
|
-
);
|
|
565
|
-
}
|
|
566
|
-
return {
|
|
567
|
-
...result,
|
|
568
|
-
report_markdown: correctedReport,
|
|
569
|
-
observed_head_revision: endingHead,
|
|
570
|
-
model: corrected.model ?? result.model,
|
|
571
|
-
usage: mergeTokenUsage(result.usage, corrected.usage),
|
|
572
|
-
agent_session_id: corrected.sessionId ?? previousSessionId ?? null,
|
|
573
|
-
};
|
|
574
|
-
},
|
|
575
|
-
});
|
|
576
|
-
return result;
|
|
577
|
-
}
|
|
578
|
-
|
|
579
|
-
export async function workspaceChangeImpact(
|
|
580
|
-
workspace,
|
|
581
|
-
baseRevision,
|
|
582
|
-
targetRevision,
|
|
583
|
-
) {
|
|
584
|
-
if (!baseRevision || !targetRevision) {
|
|
585
|
-
throw new Error(
|
|
586
|
-
"Incremental assessment requires both the previously assessed and current Git commits. Run a full assessment instead.",
|
|
587
|
-
);
|
|
588
|
-
}
|
|
589
|
-
const range = `${baseRevision}..${targetRevision}`;
|
|
590
|
-
const names = await run(
|
|
591
|
-
"git",
|
|
592
|
-
["diff", "--name-status", "--find-renames", range],
|
|
593
|
-
workspace,
|
|
594
|
-
{ allowFailure: true },
|
|
595
|
-
);
|
|
596
|
-
if (names.code !== 0) {
|
|
597
|
-
throw new Error(
|
|
598
|
-
"Codex could not compare the assessed and current commits. Fetch the missing Git history or run a full assessment.",
|
|
599
|
-
);
|
|
600
|
-
}
|
|
601
|
-
const committedFiles = await run(
|
|
602
|
-
"git",
|
|
603
|
-
["-c", "core.quotepath=false", "diff", "--name-only", range],
|
|
604
|
-
workspace,
|
|
605
|
-
{ allowFailure: true },
|
|
606
|
-
);
|
|
607
|
-
const workingFiles = await run(
|
|
608
|
-
"git",
|
|
609
|
-
[
|
|
610
|
-
"-c",
|
|
611
|
-
"core.quotepath=false",
|
|
612
|
-
"ls-files",
|
|
613
|
-
"--others",
|
|
614
|
-
"--modified",
|
|
615
|
-
"--deleted",
|
|
616
|
-
"--exclude-standard",
|
|
617
|
-
],
|
|
618
|
-
workspace,
|
|
619
|
-
{ allowFailure: true },
|
|
620
|
-
);
|
|
621
|
-
const status = await run("git", ["status", "--short"], workspace, {
|
|
622
|
-
allowFailure: true,
|
|
623
|
-
});
|
|
624
|
-
const stat = await run(
|
|
625
|
-
"git",
|
|
626
|
-
["diff", "--stat", "--compact-summary", range],
|
|
627
|
-
workspace,
|
|
628
|
-
{ allowFailure: true },
|
|
629
|
-
);
|
|
630
|
-
const allChangedFiles = [
|
|
631
|
-
...new Set([
|
|
632
|
-
...pathLines(committedFiles.stdout),
|
|
633
|
-
...pathLines(workingFiles.stdout),
|
|
634
|
-
]),
|
|
635
|
-
];
|
|
636
|
-
if (allChangedFiles.length > 500) {
|
|
637
|
-
throw new Error(
|
|
638
|
-
`This change affects ${allChangedFiles.length} paths, above the 500-path incremental limit. Run a full reassessment instead.`,
|
|
639
|
-
);
|
|
640
|
-
}
|
|
641
|
-
const changedFiles = allChangedFiles;
|
|
642
|
-
if (!changedFiles.length) {
|
|
643
|
-
throw new Error(
|
|
644
|
-
"The inventoried repository changed but Git reports no assessable paths. Refresh inventory or run a full assessment.",
|
|
645
|
-
);
|
|
646
|
-
}
|
|
647
|
-
const lines = [
|
|
648
|
-
`Comparison: \`${range}\``,
|
|
649
|
-
`Changed paths: ${changedFiles.length}`,
|
|
650
|
-
"",
|
|
651
|
-
"### Name status",
|
|
652
|
-
"",
|
|
653
|
-
"```text",
|
|
654
|
-
boundedText(names.stdout, 12_000),
|
|
655
|
-
"```",
|
|
656
|
-
];
|
|
657
|
-
if (status.stdout.trim()) {
|
|
658
|
-
lines.push(
|
|
659
|
-
"",
|
|
660
|
-
"### Working tree",
|
|
661
|
-
"",
|
|
662
|
-
"```text",
|
|
663
|
-
boundedText(status.stdout, 6_000),
|
|
664
|
-
"```",
|
|
665
|
-
);
|
|
666
|
-
}
|
|
667
|
-
if (stat.stdout.trim()) {
|
|
668
|
-
lines.push(
|
|
669
|
-
"",
|
|
670
|
-
"### Diff summary",
|
|
671
|
-
"",
|
|
672
|
-
"```text",
|
|
673
|
-
boundedText(stat.stdout, 6_000),
|
|
674
|
-
"```",
|
|
675
|
-
);
|
|
676
|
-
}
|
|
677
|
-
return { changedFiles, markdown: lines.join("\n") };
|
|
678
|
-
}
|
|
679
|
-
|
|
680
|
-
function pathLines(output) {
|
|
681
|
-
return String(output || "")
|
|
682
|
-
.split(/\r?\n/)
|
|
683
|
-
.map((line) => line.trim())
|
|
684
|
-
.filter(Boolean)
|
|
685
|
-
.map((line) => line.replace(/^"|"$/g, ""));
|
|
686
|
-
}
|
|
687
|
-
|
|
688
|
-
function boundedText(value, maximum) {
|
|
689
|
-
const text = String(value || "").trim();
|
|
690
|
-
return text.length <= maximum
|
|
691
|
-
? text
|
|
692
|
-
: `${text.slice(0, maximum)}\n... truncated by EngineerOS`;
|
|
693
|
-
}
|
|
694
|
-
|
|
695
|
-
export async function executeConnectedPrompt(assignment, config, callbacks) {
|
|
696
|
-
const execution = connectorExecution(assignment);
|
|
697
|
-
const previousSessionId = config.sessions?.[execution.sessionKey];
|
|
698
|
-
const controller = launchAgentProcess(
|
|
699
|
-
config.workspace,
|
|
700
|
-
execution.prompt,
|
|
701
|
-
execution.sandboxMode,
|
|
702
|
-
config,
|
|
703
|
-
callbacks,
|
|
704
|
-
execution.profile,
|
|
705
|
-
previousSessionId,
|
|
706
|
-
{
|
|
707
|
-
persistentAcp: true,
|
|
708
|
-
sessionKey: execution.sessionKey,
|
|
709
|
-
},
|
|
710
|
-
);
|
|
711
|
-
callbacks.onController?.(controller);
|
|
712
|
-
callbacks.onProcess?.(controller.child);
|
|
713
|
-
const completed = await controller.completed;
|
|
714
|
-
const content = sanitizeAgentResponse(completed.finalMessage).trim();
|
|
715
|
-
if (!content) {
|
|
716
|
-
throw new Error("Agent completed without returning a response.");
|
|
717
|
-
}
|
|
718
|
-
return {
|
|
719
|
-
content,
|
|
720
|
-
model: completed.model ?? config.agent_protocol ?? "coding-agent",
|
|
721
|
-
sessionId: completed.sessionId,
|
|
722
|
-
sessionKey: execution.sessionKey,
|
|
723
|
-
usage: completed.usage ?? null,
|
|
724
|
-
};
|
|
725
|
-
}
|
|
726
|
-
|
|
727
|
-
export async function inspectCodingAgent(config, workspace = process.cwd()) {
|
|
728
|
-
if (config.agent_protocol === "acp") {
|
|
729
|
-
if (!config.agent_command) {
|
|
730
|
-
throw new Error(
|
|
731
|
-
"ACP requires --agent-command when pairing the connector.",
|
|
732
|
-
);
|
|
733
|
-
}
|
|
734
|
-
const executionProfiles = await inspectAcpExecutionProfiles(
|
|
735
|
-
workspace,
|
|
736
|
-
config,
|
|
737
|
-
);
|
|
738
|
-
return {
|
|
739
|
-
protocol: "acp",
|
|
740
|
-
name: config.agent_name || path.basename(config.agent_command),
|
|
741
|
-
version: config.agent_version || "ACP v1",
|
|
742
|
-
executionProfiles,
|
|
743
|
-
};
|
|
744
|
-
}
|
|
745
|
-
const codex = await inspectCodexCli(workspace);
|
|
746
|
-
const modelProfiles = await inspectCodexExecutionProfiles({
|
|
747
|
-
workspace,
|
|
748
|
-
command: codex.command,
|
|
749
|
-
});
|
|
750
|
-
return {
|
|
751
|
-
protocol: "codex",
|
|
752
|
-
name: "Codex CLI",
|
|
753
|
-
version: codex.version,
|
|
754
|
-
executionProfiles: {
|
|
755
|
-
model_selection: modelProfiles.length > 0,
|
|
756
|
-
model_profiles: modelProfiles,
|
|
757
|
-
reasoning_efforts: [
|
|
758
|
-
...new Set(modelProfiles.flatMap((model) => model.reasoning_efforts)),
|
|
759
|
-
],
|
|
760
|
-
},
|
|
761
|
-
};
|
|
762
|
-
}
|
|
763
|
-
|
|
764
|
-
export async function inspectCodexCli(workspace = process.cwd()) {
|
|
765
|
-
const command =
|
|
766
|
-
process.env.CODEX_BIN ||
|
|
767
|
-
(process.platform === "win32" ? "codex.cmd" : "codex");
|
|
768
|
-
const result = await runCodexCommand(command, ["--version"], workspace);
|
|
769
|
-
if (result.code !== 0) {
|
|
770
|
-
throw new Error(
|
|
771
|
-
"Codex CLI is unavailable. Install it with `npm install -g @openai/codex@latest`, run `codex login`, then restart this connector.",
|
|
772
|
-
);
|
|
773
|
-
}
|
|
774
|
-
const version = result.stdout.trim().slice(0, 100);
|
|
775
|
-
if (!version) {
|
|
776
|
-
throw new Error(
|
|
777
|
-
"Codex CLI returned no version. Reinstall @openai/codex, then restart this connector.",
|
|
778
|
-
);
|
|
779
|
-
}
|
|
780
|
-
return { command, version };
|
|
781
|
-
}
|
|
782
|
-
|
|
783
|
-
export function codexFailureMessage(output, code) {
|
|
784
|
-
if (/requires a newer version of Codex/i.test(output)) {
|
|
785
|
-
return (
|
|
786
|
-
"The configured model requires a newer Codex CLI. " +
|
|
787
|
-
"Run `npm install -g @openai/codex@latest`, verify with `codex --version`, " +
|
|
788
|
-
"then restart the EngineerOS connector and retry the assessment."
|
|
789
|
-
);
|
|
790
|
-
}
|
|
791
|
-
if (/not logged in|login required|authentication required/i.test(output)) {
|
|
792
|
-
return "Codex CLI is not authenticated. Run `codex login`, then restart the EngineerOS connector.";
|
|
793
|
-
}
|
|
794
|
-
return `Codex exited with code ${code}. ${output.slice(-1_000)}`;
|
|
795
|
-
}
|
|
796
|
-
|
|
797
|
-
export function assessmentProgressMessage(event) {
|
|
798
|
-
if (!event || typeof event !== "object") return null;
|
|
799
|
-
if (event.type === "assessment.output_correction")
|
|
800
|
-
return "Completing the required stage report structure";
|
|
801
|
-
if (event.type === "agent.connected")
|
|
802
|
-
return "Connected agent is ready to inspect the workspace";
|
|
803
|
-
if (event.type === "acp.plan")
|
|
804
|
-
return "Organizing the repository assessment plan";
|
|
805
|
-
if (event.type === "acp.agent_thought_chunk")
|
|
806
|
-
return "Reasoning through the current implementation";
|
|
807
|
-
if (event.type === "acp.agent_message_chunk")
|
|
808
|
-
return "Drafting the stage report";
|
|
809
|
-
if (event.type === "acp.tool_call" || event.type === "acp.tool_call_update") {
|
|
810
|
-
return assessmentCommandMilestone(event.update?.title);
|
|
811
|
-
}
|
|
812
|
-
if (event.type === "turn.started")
|
|
813
|
-
return "Reviewing repository structure and current Git state";
|
|
814
|
-
if (event.type === "item.started") {
|
|
815
|
-
if (event.item?.type === "command_execution") {
|
|
816
|
-
return assessmentCommandMilestone(event.item.command);
|
|
817
|
-
}
|
|
818
|
-
if (event.item?.type === "mcp_tool_call")
|
|
819
|
-
return "Tracing architecture and code relationships";
|
|
820
|
-
if (event.item?.type === "web_search")
|
|
821
|
-
return "Checking an external technical reference";
|
|
822
|
-
return null;
|
|
823
|
-
}
|
|
824
|
-
if (event.type === "item.completed" && event.item?.type === "agent_message") {
|
|
825
|
-
return "Synthesizing findings and highest-return actions";
|
|
826
|
-
}
|
|
827
|
-
return null;
|
|
828
|
-
}
|
|
829
|
-
|
|
830
|
-
export function assessmentInactivityFailure(stage, inactiveMs) {
|
|
831
|
-
if (inactiveMs < ASSESSMENT_INACTIVITY_TIMEOUT_MS) return null;
|
|
832
|
-
return (
|
|
833
|
-
`The connected agent produced no activity for 10 minutes during ${stage}. ` +
|
|
834
|
-
"The stage was stopped instead of waiting indefinitely. Retry it after checking the agent terminal."
|
|
835
|
-
);
|
|
836
|
-
}
|
|
837
|
-
|
|
838
|
-
export function promptProgressMessage(event) {
|
|
839
|
-
if (!event || typeof event !== "object") return null;
|
|
840
|
-
if (event.type === "turn.started")
|
|
841
|
-
return "Reviewing the request and workspace context";
|
|
842
|
-
if (event.type === "item.started") {
|
|
843
|
-
if (event.item?.type === "command_execution") {
|
|
844
|
-
return assessmentCommandMilestone(event.item.command);
|
|
845
|
-
}
|
|
846
|
-
if (event.item?.type === "mcp_tool_call")
|
|
847
|
-
return "Checking connected project evidence";
|
|
848
|
-
if (event.item?.type === "web_search")
|
|
849
|
-
return "Checking an external technical reference";
|
|
850
|
-
return null;
|
|
851
|
-
}
|
|
852
|
-
if (event.type === "item.completed" && event.item?.type === "agent_message") {
|
|
853
|
-
return "Preparing the response";
|
|
854
|
-
}
|
|
855
|
-
if (event.type === "agent.connected") return "Agent connected";
|
|
856
|
-
if (event.type === "acp.tool_call" || event.type === "acp.tool_call_update") {
|
|
857
|
-
return "Agent is inspecting the workspace";
|
|
858
|
-
}
|
|
859
|
-
if (event.type === "acp.agent_message_chunk") return "Preparing the response";
|
|
860
|
-
return null;
|
|
861
|
-
}
|
|
862
|
-
|
|
863
|
-
export function promptStreamEvent(event) {
|
|
864
|
-
if (!event || typeof event !== "object") return null;
|
|
865
|
-
if (
|
|
866
|
-
event.type === "codex.agent_message_delta" &&
|
|
867
|
-
typeof event.delta === "string"
|
|
868
|
-
) {
|
|
869
|
-
const delta = sanitizeAgentResponse(event.delta);
|
|
870
|
-
return delta ? { kind: "message", delta } : null;
|
|
871
|
-
}
|
|
872
|
-
if (
|
|
873
|
-
event.type === "acp.agent_message_chunk" &&
|
|
874
|
-
event.update?.content?.type === "text"
|
|
875
|
-
) {
|
|
876
|
-
const delta = sanitizeAgentResponse(event.update.content.text);
|
|
877
|
-
return delta ? { kind: "message", delta } : null;
|
|
878
|
-
}
|
|
879
|
-
if (event.type === "acp.agent_thought_chunk") {
|
|
880
|
-
return {
|
|
881
|
-
kind: "thought",
|
|
882
|
-
message: "Agent is reasoning through the request",
|
|
883
|
-
};
|
|
884
|
-
}
|
|
885
|
-
if (event.type === "acp.permission") {
|
|
886
|
-
return {
|
|
887
|
-
kind: "permission",
|
|
888
|
-
message: event.update?.title || "Agent requested workspace permission",
|
|
889
|
-
status: event.update?.status,
|
|
890
|
-
};
|
|
891
|
-
}
|
|
892
|
-
if (event.type === "codex.usage") {
|
|
893
|
-
return { kind: "usage", message: "Agent usage updated" };
|
|
894
|
-
}
|
|
895
|
-
if (event.type === "acp.plan") {
|
|
896
|
-
return { kind: "plan", message: "Agent updated the working plan" };
|
|
897
|
-
}
|
|
898
|
-
if (event.type === "acp.tool_call" || event.type === "acp.tool_call_update") {
|
|
899
|
-
return {
|
|
900
|
-
kind: "tool",
|
|
901
|
-
message: event.update?.title || "Agent is inspecting the workspace",
|
|
902
|
-
status: event.update?.status,
|
|
903
|
-
};
|
|
904
|
-
}
|
|
905
|
-
if (event.type === "item.completed" && event.item?.type === "agent_message") {
|
|
906
|
-
const delta = sanitizeAgentResponse(event.item.text);
|
|
907
|
-
return delta ? { kind: "message", delta } : null;
|
|
908
|
-
}
|
|
909
|
-
const message = promptProgressMessage(event);
|
|
910
|
-
return message ? { kind: "status", message } : null;
|
|
911
|
-
}
|
|
912
|
-
|
|
913
|
-
function assessmentCommandMilestone(command) {
|
|
914
|
-
const value = Array.isArray(command)
|
|
915
|
-
? command.join(" ")
|
|
916
|
-
: String(command || "");
|
|
917
|
-
const normalized = value.replace(/\s+/g, " ").trim().toLowerCase();
|
|
918
|
-
if (!normalized) return "Inspecting workspace source";
|
|
919
|
-
if (/\bgit\s+(status|log|diff|show|rev-parse)\b/.test(normalized)) {
|
|
920
|
-
return "Comparing Git history and workspace changes";
|
|
921
|
-
}
|
|
922
|
-
if (
|
|
923
|
-
/\b(test|pytest|vitest|jest|ruff|eslint|tsc|build|lint)\b/.test(normalized)
|
|
924
|
-
) {
|
|
925
|
-
return "Checking verification and delivery signals";
|
|
926
|
-
}
|
|
927
|
-
if (
|
|
928
|
-
/\b(audit|dependency|dependencies|lockfile|package-lock|pnpm-lock|requirements)\b/.test(
|
|
929
|
-
normalized,
|
|
930
|
-
)
|
|
931
|
-
) {
|
|
932
|
-
return "Reviewing dependencies and security signals";
|
|
933
|
-
}
|
|
934
|
-
return "Tracing architecture and code relationships";
|
|
935
|
-
}
|
|
936
|
-
|
|
937
|
-
export function workspaceAssessmentExecution(assignment) {
|
|
938
|
-
const stage = assignment?.stage;
|
|
939
|
-
const requiredOutputHeading = String(
|
|
940
|
-
assignment?.required_output_heading || "",
|
|
941
|
-
).trim();
|
|
942
|
-
if (!stage || !requiredOutputHeading.startsWith("# Assessment Stage: ")) {
|
|
943
|
-
throw new Error(
|
|
944
|
-
"EngineerOS assessment assignment has an unsupported stage.",
|
|
945
|
-
);
|
|
946
|
-
}
|
|
947
|
-
return {
|
|
948
|
-
...connectorExecution(assignment, { requiredOutputHeading }),
|
|
949
|
-
requiredOutputHeading,
|
|
950
|
-
};
|
|
951
|
-
}
|
|
952
|
-
|
|
953
|
-
export async function recoverAssessmentStageOutput({
|
|
954
|
-
completed,
|
|
955
|
-
prompt,
|
|
956
|
-
requiredOutputHeading,
|
|
957
|
-
retry,
|
|
958
|
-
}) {
|
|
959
|
-
try {
|
|
960
|
-
return {
|
|
961
|
-
completed,
|
|
962
|
-
correctionUsed: false,
|
|
963
|
-
report: normalizeAgentStructuredOutput(
|
|
964
|
-
completed.finalMessage,
|
|
965
|
-
requiredOutputHeading,
|
|
966
|
-
),
|
|
967
|
-
};
|
|
968
|
-
} catch (error) {
|
|
969
|
-
if (!recoverableStructuredOutputFailure(error)) throw error;
|
|
970
|
-
}
|
|
971
|
-
|
|
972
|
-
const corrected = await retry(
|
|
973
|
-
assessmentStageCorrectionPrompt(prompt, requiredOutputHeading),
|
|
974
|
-
completed.sessionId,
|
|
975
|
-
);
|
|
976
|
-
try {
|
|
977
|
-
return {
|
|
978
|
-
completed: {
|
|
979
|
-
...corrected,
|
|
980
|
-
usage: mergeTokenUsage(completed.usage, corrected.usage),
|
|
981
|
-
},
|
|
982
|
-
correctionUsed: true,
|
|
983
|
-
report: normalizeAgentStructuredOutput(
|
|
984
|
-
corrected.finalMessage,
|
|
985
|
-
requiredOutputHeading,
|
|
986
|
-
),
|
|
987
|
-
};
|
|
988
|
-
} catch (error) {
|
|
989
|
-
throw new Error(
|
|
990
|
-
`Agent did not return the required stage report after one automatic correction attempt. ${error.message}`,
|
|
991
|
-
{ cause: error },
|
|
992
|
-
);
|
|
993
|
-
}
|
|
994
|
-
}
|
|
995
|
-
|
|
996
|
-
export function assessmentRejectionCorrectionPrompt(
|
|
997
|
-
validationMessage,
|
|
998
|
-
requiredOutputHeading,
|
|
999
|
-
) {
|
|
1000
|
-
return [
|
|
1001
|
-
"## Rejected Stage Output Recovery",
|
|
1002
|
-
"",
|
|
1003
|
-
"EngineerOS rejected the previous stage report because it was incomplete or violated the structured Markdown contract:",
|
|
1004
|
-
"",
|
|
1005
|
-
String(validationMessage || "The stage report was rejected.").trim(),
|
|
1006
|
-
"",
|
|
1007
|
-
"Treat the previous stage report as the authoritative draft.",
|
|
1008
|
-
"Copy every valid section and block unchanged; repair only the incomplete or invalid entries identified by EngineerOS validation.",
|
|
1009
|
-
"Do not re-inspect the repository or replace valid evidence unless the validation error requires it.",
|
|
1010
|
-
"Return the entire corrected structured Markdown stage report so EngineerOS can validate it atomically, not only the repaired fragment or missing tail.",
|
|
1011
|
-
`The first non-whitespace line must be exactly: ${requiredOutputHeading}`,
|
|
1012
|
-
"Do not return progress commentary, an explanation of the correction, or a code fence.",
|
|
1013
|
-
].join("\n");
|
|
1014
|
-
}
|
|
1015
|
-
|
|
1016
|
-
export function assessmentStageCorrectionPrompt(
|
|
1017
|
-
originalPrompt,
|
|
1018
|
-
requiredOutputHeading,
|
|
1019
|
-
) {
|
|
1020
|
-
return [
|
|
1021
|
-
String(originalPrompt || "").trim(),
|
|
1022
|
-
"",
|
|
1023
|
-
"## Incomplete Stage Output Recovery",
|
|
1024
|
-
"",
|
|
1025
|
-
"The previous turn ended without a complete stage deliverable. Return the entire structured Markdown stage report now.",
|
|
1026
|
-
"Use repository context already inspected in the previous turn when it is available; inspect only what remains necessary.",
|
|
1027
|
-
`The first non-whitespace line of the final answer must be exactly: ${requiredOutputHeading}`,
|
|
1028
|
-
"Follow every section, inspection, and completeness rule in the Assignment.",
|
|
1029
|
-
"Do not return progress commentary, an explanation of the correction, or a code fence.",
|
|
1030
|
-
].join("\n");
|
|
1031
|
-
}
|
|
1032
|
-
|
|
1033
|
-
function recoverableStructuredOutputFailure(error) {
|
|
1034
|
-
const message = error instanceof Error ? error.message : "";
|
|
1035
|
-
return (
|
|
1036
|
-
message.startsWith("Agent completed") ||
|
|
1037
|
-
message.startsWith("Agent response")
|
|
1038
|
-
);
|
|
1039
|
-
}
|
|
1040
|
-
|
|
1041
|
-
function mergeTokenUsage(first, second) {
|
|
1042
|
-
const usages = [first, second].filter(
|
|
1043
|
-
(usage) => usage && typeof usage === "object",
|
|
1044
|
-
);
|
|
1045
|
-
if (!usages.length) return null;
|
|
1046
|
-
return Object.fromEntries(
|
|
1047
|
-
[
|
|
1048
|
-
"input_tokens",
|
|
1049
|
-
"output_tokens",
|
|
1050
|
-
"cache_read_tokens",
|
|
1051
|
-
"cache_write_tokens",
|
|
1052
|
-
"reasoning_tokens",
|
|
1053
|
-
"total_tokens",
|
|
1054
|
-
].map((field) => [
|
|
1055
|
-
field,
|
|
1056
|
-
usages.reduce(
|
|
1057
|
-
(total, usage) =>
|
|
1058
|
-
total + (Number.isFinite(usage[field]) ? usage[field] : 0),
|
|
1059
|
-
0,
|
|
1060
|
-
),
|
|
1061
|
-
]),
|
|
1062
|
-
);
|
|
1063
|
-
}
|
|
1064
|
-
|
|
1065
|
-
export function enqueueWorkspaceAssessment(
|
|
1066
|
-
queue,
|
|
1067
|
-
activeAssessment,
|
|
1068
|
-
assignment,
|
|
1069
|
-
{ front = false } = {},
|
|
1070
|
-
) {
|
|
1071
|
-
const matches = (candidate) =>
|
|
1072
|
-
candidate?.assessment_id === assignment?.assessment_id &&
|
|
1073
|
-
candidate?.stage === assignment?.stage;
|
|
1074
|
-
if (
|
|
1075
|
-
activeAssessment?.kind === "assessment" &&
|
|
1076
|
-
activeAssessment.runId === assignment?.assessment_id &&
|
|
1077
|
-
activeAssessment.stage === assignment?.stage
|
|
1078
|
-
) {
|
|
1079
|
-
activeAssessment.resumeAssignment = assignment;
|
|
1080
|
-
return "deferred";
|
|
1081
|
-
}
|
|
1082
|
-
if (queue.some(matches)) return "duplicate";
|
|
1083
|
-
if (front) queue.unshift(assignment);
|
|
1084
|
-
else queue.push(assignment);
|
|
1085
|
-
return "queued";
|
|
1086
|
-
}
|
|
1087
|
-
|
|
370
|
+
const execution = workspaceAssessmentExecution(assignment);
|
|
371
|
+
const assessmentAcpContext =
|
|
372
|
+
config.agent_protocol === "acp"
|
|
373
|
+
? {
|
|
374
|
+
persistentAcp: true,
|
|
375
|
+
sessionKey: `assessment:${assignment.assessment_id}:${assignment.stage}`,
|
|
376
|
+
}
|
|
377
|
+
: undefined;
|
|
378
|
+
const launchAssessmentAgent = (turnPrompt, previousSessionId) => {
|
|
379
|
+
const controller = launchAgentProcess(
|
|
380
|
+
config.workspace,
|
|
381
|
+
turnPrompt,
|
|
382
|
+
execution.sandboxMode,
|
|
383
|
+
config,
|
|
384
|
+
callbacks,
|
|
385
|
+
execution.profile,
|
|
386
|
+
previousSessionId,
|
|
387
|
+
assessmentAcpContext,
|
|
388
|
+
);
|
|
389
|
+
callbacks.onController?.(controller);
|
|
390
|
+
callbacks.onProcess?.(controller.child);
|
|
391
|
+
return controller;
|
|
392
|
+
};
|
|
393
|
+
const startingRevision = await run(
|
|
394
|
+
"git",
|
|
395
|
+
["rev-parse", "HEAD"],
|
|
396
|
+
config.workspace,
|
|
397
|
+
{ allowFailure: true },
|
|
398
|
+
);
|
|
399
|
+
const startingHead =
|
|
400
|
+
startingRevision.code === 0
|
|
401
|
+
? startingRevision.stdout.trim().slice(0, 128)
|
|
402
|
+
: null;
|
|
403
|
+
if (
|
|
404
|
+
assignment.target_head_revision &&
|
|
405
|
+
startingHead !== assignment.target_head_revision
|
|
406
|
+
) {
|
|
407
|
+
throw new Error(
|
|
408
|
+
"This workspace is no longer at the commit inventoried by EngineerOS. Refresh the workspace inventory before assessing it.",
|
|
409
|
+
);
|
|
410
|
+
}
|
|
411
|
+
const changeImpact =
|
|
412
|
+
assignment.assessment_mode === "incremental"
|
|
413
|
+
? await workspaceChangeImpact(
|
|
414
|
+
config.workspace,
|
|
415
|
+
assignment.base_head_revision,
|
|
416
|
+
assignment.target_head_revision,
|
|
417
|
+
)
|
|
418
|
+
: { changedFiles: [], markdown: null };
|
|
419
|
+
const prompt = changeImpact.markdown
|
|
420
|
+
? execution.prompt.replace(
|
|
421
|
+
"<!-- ENGINEEROS_CHANGE_IMPACT -->",
|
|
422
|
+
changeImpact.markdown,
|
|
423
|
+
)
|
|
424
|
+
: execution.prompt;
|
|
425
|
+
const controller = launchAssessmentAgent(prompt, previousSessionId);
|
|
426
|
+
let completed = await controller.completed;
|
|
427
|
+
const recovered = await recoverAssessmentStageOutput({
|
|
428
|
+
completed,
|
|
429
|
+
prompt,
|
|
430
|
+
requiredOutputHeading: execution.requiredOutputHeading,
|
|
431
|
+
retry: async (correctionPrompt, previousSessionId) => {
|
|
432
|
+
callbacks.onEvent?.({
|
|
433
|
+
type: "assessment.output_correction",
|
|
434
|
+
message: "The Agent is completing the required stage report structure",
|
|
435
|
+
});
|
|
436
|
+
const correction = launchAssessmentAgent(
|
|
437
|
+
correctionPrompt,
|
|
438
|
+
previousSessionId,
|
|
439
|
+
);
|
|
440
|
+
return correction.completed;
|
|
441
|
+
},
|
|
442
|
+
});
|
|
443
|
+
completed = recovered.completed;
|
|
444
|
+
const buildResult = async (turn, report) => {
|
|
445
|
+
const endingRevision = await run(
|
|
446
|
+
"git",
|
|
447
|
+
["rev-parse", "HEAD"],
|
|
448
|
+
config.workspace,
|
|
449
|
+
{ allowFailure: true },
|
|
450
|
+
);
|
|
451
|
+
const endingHead =
|
|
452
|
+
endingRevision.code === 0
|
|
453
|
+
? endingRevision.stdout.trim().slice(0, 128)
|
|
454
|
+
: null;
|
|
455
|
+
if (startingHead !== endingHead) {
|
|
456
|
+
throw new Error(
|
|
457
|
+
"The Git commit changed during assessment. Refresh the workspace inventory and assess the new commit.",
|
|
458
|
+
);
|
|
459
|
+
}
|
|
460
|
+
return {
|
|
461
|
+
stage: assignment.stage,
|
|
462
|
+
report_markdown: report,
|
|
463
|
+
observed_head_revision: endingHead,
|
|
464
|
+
changed_files: changeImpact.changedFiles,
|
|
465
|
+
change_impact_markdown: changeImpact.markdown,
|
|
466
|
+
model: turn.model ?? config.agent_protocol ?? "coding-agent",
|
|
467
|
+
usage: turn.usage ?? null,
|
|
468
|
+
agent_session_id: turn.sessionId ?? null,
|
|
469
|
+
};
|
|
470
|
+
};
|
|
471
|
+
const result = await buildResult(completed, recovered.report);
|
|
472
|
+
attachAssessmentRejectionCorrection(result, assignment, config, callbacks, {
|
|
473
|
+
previousSessionId: completed.sessionId,
|
|
474
|
+
buildResult,
|
|
475
|
+
});
|
|
476
|
+
return result;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
export function attachAssessmentRejectionCorrection(
|
|
480
|
+
result,
|
|
481
|
+
assignment,
|
|
482
|
+
config,
|
|
483
|
+
callbacks,
|
|
484
|
+
{ previousSessionId = result.agent_session_id, draftPath, buildResult } = {},
|
|
485
|
+
) {
|
|
486
|
+
const execution = workspaceAssessmentExecution(assignment);
|
|
487
|
+
const assessmentAcpContext =
|
|
488
|
+
config.agent_protocol === "acp"
|
|
489
|
+
? {
|
|
490
|
+
persistentAcp: true,
|
|
491
|
+
sessionKey: `assessment:${assignment.assessment_id}:${assignment.stage}`,
|
|
492
|
+
}
|
|
493
|
+
: undefined;
|
|
494
|
+
Object.defineProperty(result, "correctAfterRejection", {
|
|
495
|
+
enumerable: false,
|
|
496
|
+
value: async (validationMessage) => {
|
|
497
|
+
callbacks.onEvent?.({
|
|
498
|
+
type: "assessment.output_correction",
|
|
499
|
+
message: "The Agent is correcting the rejected stage report",
|
|
500
|
+
});
|
|
501
|
+
const draftInstruction = draftPath
|
|
502
|
+
? [
|
|
503
|
+
"",
|
|
504
|
+
`The authoritative draft is stored at \`${draftPath}\`. Read that file before making the requested correction.`,
|
|
505
|
+
].join("\n")
|
|
506
|
+
: "";
|
|
507
|
+
const correctionPrompt = `${assessmentRejectionCorrectionPrompt(validationMessage, execution.requiredOutputHeading)}${draftInstruction}`;
|
|
508
|
+
const launchCorrection = (prompt, sessionId) => {
|
|
509
|
+
const controller = launchAgentProcess(
|
|
510
|
+
config.workspace,
|
|
511
|
+
prompt,
|
|
512
|
+
execution.sandboxMode,
|
|
513
|
+
config,
|
|
514
|
+
callbacks,
|
|
515
|
+
execution.profile,
|
|
516
|
+
sessionId,
|
|
517
|
+
assessmentAcpContext,
|
|
518
|
+
);
|
|
519
|
+
callbacks.onController?.(controller);
|
|
520
|
+
callbacks.onProcess?.(controller.child);
|
|
521
|
+
return controller;
|
|
522
|
+
};
|
|
523
|
+
let corrected = await launchCorrection(
|
|
524
|
+
correctionPrompt,
|
|
525
|
+
previousSessionId,
|
|
526
|
+
).completed;
|
|
527
|
+
const recovered = await recoverAssessmentStageOutput({
|
|
528
|
+
completed: corrected,
|
|
529
|
+
prompt: correctionPrompt,
|
|
530
|
+
requiredOutputHeading: execution.requiredOutputHeading,
|
|
531
|
+
retry: async (formatPrompt, sessionId) => {
|
|
532
|
+
callbacks.onEvent?.({
|
|
533
|
+
type: "assessment.output_correction",
|
|
534
|
+
message: "The Agent is formatting the corrected stage report",
|
|
535
|
+
});
|
|
536
|
+
return launchCorrection(formatPrompt, sessionId).completed;
|
|
537
|
+
},
|
|
538
|
+
});
|
|
539
|
+
corrected = recovered.completed;
|
|
540
|
+
const correctedReport = recovered.report;
|
|
541
|
+
if (buildResult) {
|
|
542
|
+
return buildResult(
|
|
543
|
+
{
|
|
544
|
+
...corrected,
|
|
545
|
+
usage: mergeTokenUsage(result.usage, corrected.usage),
|
|
546
|
+
},
|
|
547
|
+
correctedReport,
|
|
548
|
+
);
|
|
549
|
+
}
|
|
550
|
+
const endingRevision = await run(
|
|
551
|
+
"git",
|
|
552
|
+
["rev-parse", "HEAD"],
|
|
553
|
+
config.workspace,
|
|
554
|
+
{ allowFailure: true },
|
|
555
|
+
);
|
|
556
|
+
const endingHead =
|
|
557
|
+
endingRevision.code === 0
|
|
558
|
+
? endingRevision.stdout.trim().slice(0, 128)
|
|
559
|
+
: null;
|
|
560
|
+
if (
|
|
561
|
+
assignment.target_head_revision &&
|
|
562
|
+
endingHead !== assignment.target_head_revision
|
|
563
|
+
) {
|
|
564
|
+
throw new Error(
|
|
565
|
+
"The Git commit changed before assessment correction. Refresh the workspace inventory and assess the new commit.",
|
|
566
|
+
);
|
|
567
|
+
}
|
|
568
|
+
return {
|
|
569
|
+
...result,
|
|
570
|
+
report_markdown: correctedReport,
|
|
571
|
+
observed_head_revision: endingHead,
|
|
572
|
+
model: corrected.model ?? result.model,
|
|
573
|
+
usage: mergeTokenUsage(result.usage, corrected.usage),
|
|
574
|
+
agent_session_id: corrected.sessionId ?? previousSessionId ?? null,
|
|
575
|
+
};
|
|
576
|
+
},
|
|
577
|
+
});
|
|
578
|
+
return result;
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
export async function workspaceChangeImpact(
|
|
582
|
+
workspace,
|
|
583
|
+
baseRevision,
|
|
584
|
+
targetRevision,
|
|
585
|
+
) {
|
|
586
|
+
if (!baseRevision || !targetRevision) {
|
|
587
|
+
throw new Error(
|
|
588
|
+
"Incremental assessment requires both the previously assessed and current Git commits. Run a full assessment instead.",
|
|
589
|
+
);
|
|
590
|
+
}
|
|
591
|
+
const range = `${baseRevision}..${targetRevision}`;
|
|
592
|
+
const names = await run(
|
|
593
|
+
"git",
|
|
594
|
+
["diff", "--name-status", "--find-renames", range],
|
|
595
|
+
workspace,
|
|
596
|
+
{ allowFailure: true },
|
|
597
|
+
);
|
|
598
|
+
if (names.code !== 0) {
|
|
599
|
+
throw new Error(
|
|
600
|
+
"Codex could not compare the assessed and current commits. Fetch the missing Git history or run a full assessment.",
|
|
601
|
+
);
|
|
602
|
+
}
|
|
603
|
+
const committedFiles = await run(
|
|
604
|
+
"git",
|
|
605
|
+
["-c", "core.quotepath=false", "diff", "--name-only", range],
|
|
606
|
+
workspace,
|
|
607
|
+
{ allowFailure: true },
|
|
608
|
+
);
|
|
609
|
+
const workingFiles = await run(
|
|
610
|
+
"git",
|
|
611
|
+
[
|
|
612
|
+
"-c",
|
|
613
|
+
"core.quotepath=false",
|
|
614
|
+
"ls-files",
|
|
615
|
+
"--others",
|
|
616
|
+
"--modified",
|
|
617
|
+
"--deleted",
|
|
618
|
+
"--exclude-standard",
|
|
619
|
+
],
|
|
620
|
+
workspace,
|
|
621
|
+
{ allowFailure: true },
|
|
622
|
+
);
|
|
623
|
+
const status = await run("git", ["status", "--short"], workspace, {
|
|
624
|
+
allowFailure: true,
|
|
625
|
+
});
|
|
626
|
+
const stat = await run(
|
|
627
|
+
"git",
|
|
628
|
+
["diff", "--stat", "--compact-summary", range],
|
|
629
|
+
workspace,
|
|
630
|
+
{ allowFailure: true },
|
|
631
|
+
);
|
|
632
|
+
const allChangedFiles = [
|
|
633
|
+
...new Set([
|
|
634
|
+
...pathLines(committedFiles.stdout),
|
|
635
|
+
...pathLines(workingFiles.stdout),
|
|
636
|
+
]),
|
|
637
|
+
];
|
|
638
|
+
if (allChangedFiles.length > 500) {
|
|
639
|
+
throw new Error(
|
|
640
|
+
`This change affects ${allChangedFiles.length} paths, above the 500-path incremental limit. Run a full reassessment instead.`,
|
|
641
|
+
);
|
|
642
|
+
}
|
|
643
|
+
const changedFiles = allChangedFiles;
|
|
644
|
+
if (!changedFiles.length) {
|
|
645
|
+
throw new Error(
|
|
646
|
+
"The inventoried repository changed but Git reports no assessable paths. Refresh inventory or run a full assessment.",
|
|
647
|
+
);
|
|
648
|
+
}
|
|
649
|
+
const lines = [
|
|
650
|
+
`Comparison: \`${range}\``,
|
|
651
|
+
`Changed paths: ${changedFiles.length}`,
|
|
652
|
+
"",
|
|
653
|
+
"### Name status",
|
|
654
|
+
"",
|
|
655
|
+
"```text",
|
|
656
|
+
boundedText(names.stdout, 12_000),
|
|
657
|
+
"```",
|
|
658
|
+
];
|
|
659
|
+
if (status.stdout.trim()) {
|
|
660
|
+
lines.push(
|
|
661
|
+
"",
|
|
662
|
+
"### Working tree",
|
|
663
|
+
"",
|
|
664
|
+
"```text",
|
|
665
|
+
boundedText(status.stdout, 6_000),
|
|
666
|
+
"```",
|
|
667
|
+
);
|
|
668
|
+
}
|
|
669
|
+
if (stat.stdout.trim()) {
|
|
670
|
+
lines.push(
|
|
671
|
+
"",
|
|
672
|
+
"### Diff summary",
|
|
673
|
+
"",
|
|
674
|
+
"```text",
|
|
675
|
+
boundedText(stat.stdout, 6_000),
|
|
676
|
+
"```",
|
|
677
|
+
);
|
|
678
|
+
}
|
|
679
|
+
return { changedFiles, markdown: lines.join("\n") };
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
function pathLines(output) {
|
|
683
|
+
return String(output || "")
|
|
684
|
+
.split(/\r?\n/)
|
|
685
|
+
.map((line) => line.trim())
|
|
686
|
+
.filter(Boolean)
|
|
687
|
+
.map((line) => line.replace(/^"|"$/g, ""));
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
function boundedText(value, maximum) {
|
|
691
|
+
const text = String(value || "").trim();
|
|
692
|
+
return text.length <= maximum
|
|
693
|
+
? text
|
|
694
|
+
: `${text.slice(0, maximum)}\n... truncated by EngineerOS`;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
export async function executeConnectedPrompt(assignment, config, callbacks) {
|
|
698
|
+
const execution = connectorExecution(assignment);
|
|
699
|
+
const previousSessionId = config.sessions?.[execution.sessionKey];
|
|
700
|
+
const controller = launchAgentProcess(
|
|
701
|
+
config.workspace,
|
|
702
|
+
execution.prompt,
|
|
703
|
+
execution.sandboxMode,
|
|
704
|
+
config,
|
|
705
|
+
callbacks,
|
|
706
|
+
execution.profile,
|
|
707
|
+
previousSessionId,
|
|
708
|
+
{
|
|
709
|
+
persistentAcp: true,
|
|
710
|
+
sessionKey: execution.sessionKey,
|
|
711
|
+
},
|
|
712
|
+
);
|
|
713
|
+
callbacks.onController?.(controller);
|
|
714
|
+
callbacks.onProcess?.(controller.child);
|
|
715
|
+
const completed = await controller.completed;
|
|
716
|
+
const content = sanitizeAgentResponse(completed.finalMessage).trim();
|
|
717
|
+
if (!content) {
|
|
718
|
+
throw new Error("Agent completed without returning a response.");
|
|
719
|
+
}
|
|
720
|
+
return {
|
|
721
|
+
content,
|
|
722
|
+
model: completed.model ?? config.agent_protocol ?? "coding-agent",
|
|
723
|
+
sessionId: completed.sessionId,
|
|
724
|
+
sessionKey: execution.sessionKey,
|
|
725
|
+
usage: completed.usage ?? null,
|
|
726
|
+
};
|
|
727
|
+
}
|
|
728
|
+
|
|
729
|
+
export async function inspectCodingAgent(config, workspace = process.cwd()) {
|
|
730
|
+
if (config.agent_protocol === "acp") {
|
|
731
|
+
if (!config.agent_command) {
|
|
732
|
+
throw new Error(
|
|
733
|
+
"ACP requires --agent-command when pairing the connector.",
|
|
734
|
+
);
|
|
735
|
+
}
|
|
736
|
+
const executionProfiles = await inspectAcpExecutionProfiles(
|
|
737
|
+
workspace,
|
|
738
|
+
config,
|
|
739
|
+
);
|
|
740
|
+
return {
|
|
741
|
+
protocol: "acp",
|
|
742
|
+
name: config.agent_name || path.basename(config.agent_command),
|
|
743
|
+
version: config.agent_version || "ACP v1",
|
|
744
|
+
executionProfiles,
|
|
745
|
+
};
|
|
746
|
+
}
|
|
747
|
+
const codex = await inspectCodexCli(workspace);
|
|
748
|
+
const modelProfiles = await inspectCodexExecutionProfiles({
|
|
749
|
+
workspace,
|
|
750
|
+
command: codex.command,
|
|
751
|
+
});
|
|
752
|
+
return {
|
|
753
|
+
protocol: "codex",
|
|
754
|
+
name: "Codex CLI",
|
|
755
|
+
version: codex.version,
|
|
756
|
+
executionProfiles: {
|
|
757
|
+
model_selection: modelProfiles.length > 0,
|
|
758
|
+
model_profiles: modelProfiles,
|
|
759
|
+
reasoning_efforts: [
|
|
760
|
+
...new Set(modelProfiles.flatMap((model) => model.reasoning_efforts)),
|
|
761
|
+
],
|
|
762
|
+
},
|
|
763
|
+
};
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
export async function inspectCodexCli(workspace = process.cwd()) {
|
|
767
|
+
const command =
|
|
768
|
+
process.env.CODEX_BIN ||
|
|
769
|
+
(process.platform === "win32" ? "codex.cmd" : "codex");
|
|
770
|
+
const result = await runCodexCommand(command, ["--version"], workspace);
|
|
771
|
+
if (result.code !== 0) {
|
|
772
|
+
throw new Error(
|
|
773
|
+
"Codex CLI is unavailable. Install it with `npm install -g @openai/codex@latest`, run `codex login`, then restart this connector.",
|
|
774
|
+
);
|
|
775
|
+
}
|
|
776
|
+
const version = result.stdout.trim().slice(0, 100);
|
|
777
|
+
if (!version) {
|
|
778
|
+
throw new Error(
|
|
779
|
+
"Codex CLI returned no version. Reinstall @openai/codex, then restart this connector.",
|
|
780
|
+
);
|
|
781
|
+
}
|
|
782
|
+
return { command, version };
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
export function codexFailureMessage(output, code) {
|
|
786
|
+
if (/requires a newer version of Codex/i.test(output)) {
|
|
787
|
+
return (
|
|
788
|
+
"The configured model requires a newer Codex CLI. " +
|
|
789
|
+
"Run `npm install -g @openai/codex@latest`, verify with `codex --version`, " +
|
|
790
|
+
"then restart the EngineerOS connector and retry the assessment."
|
|
791
|
+
);
|
|
792
|
+
}
|
|
793
|
+
if (/not logged in|login required|authentication required/i.test(output)) {
|
|
794
|
+
return "Codex CLI is not authenticated. Run `codex login`, then restart the EngineerOS connector.";
|
|
795
|
+
}
|
|
796
|
+
return `Codex exited with code ${code}. ${output.slice(-1_000)}`;
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
export function assessmentProgressMessage(event) {
|
|
800
|
+
if (!event || typeof event !== "object") return null;
|
|
801
|
+
if (event.type === "assessment.output_correction")
|
|
802
|
+
return "Completing the required stage report structure";
|
|
803
|
+
if (event.type === "agent.connected")
|
|
804
|
+
return "Connected agent is ready to inspect the workspace";
|
|
805
|
+
if (event.type === "acp.plan")
|
|
806
|
+
return "Organizing the repository assessment plan";
|
|
807
|
+
if (event.type === "acp.agent_thought_chunk")
|
|
808
|
+
return "Reasoning through the current implementation";
|
|
809
|
+
if (event.type === "acp.agent_message_chunk")
|
|
810
|
+
return "Drafting the stage report";
|
|
811
|
+
if (event.type === "acp.tool_call" || event.type === "acp.tool_call_update") {
|
|
812
|
+
return assessmentCommandMilestone(event.update?.title);
|
|
813
|
+
}
|
|
814
|
+
if (event.type === "turn.started")
|
|
815
|
+
return "Reviewing repository structure and current Git state";
|
|
816
|
+
if (event.type === "item.started") {
|
|
817
|
+
if (event.item?.type === "command_execution") {
|
|
818
|
+
return assessmentCommandMilestone(event.item.command);
|
|
819
|
+
}
|
|
820
|
+
if (event.item?.type === "mcp_tool_call")
|
|
821
|
+
return "Tracing architecture and code relationships";
|
|
822
|
+
if (event.item?.type === "web_search")
|
|
823
|
+
return "Checking an external technical reference";
|
|
824
|
+
return null;
|
|
825
|
+
}
|
|
826
|
+
if (event.type === "item.completed" && event.item?.type === "agent_message") {
|
|
827
|
+
return "Synthesizing findings and highest-return actions";
|
|
828
|
+
}
|
|
829
|
+
return null;
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
export function assessmentInactivityFailure(stage, inactiveMs) {
|
|
833
|
+
if (inactiveMs < ASSESSMENT_INACTIVITY_TIMEOUT_MS) return null;
|
|
834
|
+
return (
|
|
835
|
+
`The connected agent produced no activity for 10 minutes during ${stage}. ` +
|
|
836
|
+
"The stage was stopped instead of waiting indefinitely. Retry it after checking the agent terminal."
|
|
837
|
+
);
|
|
838
|
+
}
|
|
839
|
+
|
|
840
|
+
export function promptProgressMessage(event) {
|
|
841
|
+
if (!event || typeof event !== "object") return null;
|
|
842
|
+
if (event.type === "turn.started")
|
|
843
|
+
return "Reviewing the request and workspace context";
|
|
844
|
+
if (event.type === "item.started") {
|
|
845
|
+
if (event.item?.type === "command_execution") {
|
|
846
|
+
return assessmentCommandMilestone(event.item.command);
|
|
847
|
+
}
|
|
848
|
+
if (event.item?.type === "mcp_tool_call")
|
|
849
|
+
return "Checking connected project evidence";
|
|
850
|
+
if (event.item?.type === "web_search")
|
|
851
|
+
return "Checking an external technical reference";
|
|
852
|
+
return null;
|
|
853
|
+
}
|
|
854
|
+
if (event.type === "item.completed" && event.item?.type === "agent_message") {
|
|
855
|
+
return "Preparing the response";
|
|
856
|
+
}
|
|
857
|
+
if (event.type === "agent.connected") return "Agent connected";
|
|
858
|
+
if (event.type === "acp.tool_call" || event.type === "acp.tool_call_update") {
|
|
859
|
+
return "Agent is inspecting the workspace";
|
|
860
|
+
}
|
|
861
|
+
if (event.type === "acp.agent_message_chunk") return "Preparing the response";
|
|
862
|
+
return null;
|
|
863
|
+
}
|
|
864
|
+
|
|
865
|
+
export function promptStreamEvent(event) {
|
|
866
|
+
if (!event || typeof event !== "object") return null;
|
|
867
|
+
if (
|
|
868
|
+
event.type === "codex.agent_message_delta" &&
|
|
869
|
+
typeof event.delta === "string"
|
|
870
|
+
) {
|
|
871
|
+
const delta = sanitizeAgentResponse(event.delta);
|
|
872
|
+
return delta ? { kind: "message", delta } : null;
|
|
873
|
+
}
|
|
874
|
+
if (
|
|
875
|
+
event.type === "acp.agent_message_chunk" &&
|
|
876
|
+
event.update?.content?.type === "text"
|
|
877
|
+
) {
|
|
878
|
+
const delta = sanitizeAgentResponse(event.update.content.text);
|
|
879
|
+
return delta ? { kind: "message", delta } : null;
|
|
880
|
+
}
|
|
881
|
+
if (event.type === "acp.agent_thought_chunk") {
|
|
882
|
+
return {
|
|
883
|
+
kind: "thought",
|
|
884
|
+
message: "Agent is reasoning through the request",
|
|
885
|
+
};
|
|
886
|
+
}
|
|
887
|
+
if (event.type === "acp.permission") {
|
|
888
|
+
return {
|
|
889
|
+
kind: "permission",
|
|
890
|
+
message: event.update?.title || "Agent requested workspace permission",
|
|
891
|
+
status: event.update?.status,
|
|
892
|
+
};
|
|
893
|
+
}
|
|
894
|
+
if (event.type === "codex.usage") {
|
|
895
|
+
return { kind: "usage", message: "Agent usage updated" };
|
|
896
|
+
}
|
|
897
|
+
if (event.type === "acp.plan") {
|
|
898
|
+
return { kind: "plan", message: "Agent updated the working plan" };
|
|
899
|
+
}
|
|
900
|
+
if (event.type === "acp.tool_call" || event.type === "acp.tool_call_update") {
|
|
901
|
+
return {
|
|
902
|
+
kind: "tool",
|
|
903
|
+
message: event.update?.title || "Agent is inspecting the workspace",
|
|
904
|
+
status: event.update?.status,
|
|
905
|
+
};
|
|
906
|
+
}
|
|
907
|
+
if (event.type === "item.completed" && event.item?.type === "agent_message") {
|
|
908
|
+
const delta = sanitizeAgentResponse(event.item.text);
|
|
909
|
+
return delta ? { kind: "message", delta } : null;
|
|
910
|
+
}
|
|
911
|
+
const message = promptProgressMessage(event);
|
|
912
|
+
return message ? { kind: "status", message } : null;
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
function assessmentCommandMilestone(command) {
|
|
916
|
+
const value = Array.isArray(command)
|
|
917
|
+
? command.join(" ")
|
|
918
|
+
: String(command || "");
|
|
919
|
+
const normalized = value.replace(/\s+/g, " ").trim().toLowerCase();
|
|
920
|
+
if (!normalized) return "Inspecting workspace source";
|
|
921
|
+
if (/\bgit\s+(status|log|diff|show|rev-parse)\b/.test(normalized)) {
|
|
922
|
+
return "Comparing Git history and workspace changes";
|
|
923
|
+
}
|
|
924
|
+
if (
|
|
925
|
+
/\b(test|pytest|vitest|jest|ruff|eslint|tsc|build|lint)\b/.test(normalized)
|
|
926
|
+
) {
|
|
927
|
+
return "Checking verification and delivery signals";
|
|
928
|
+
}
|
|
929
|
+
if (
|
|
930
|
+
/\b(audit|dependency|dependencies|lockfile|package-lock|pnpm-lock|requirements)\b/.test(
|
|
931
|
+
normalized,
|
|
932
|
+
)
|
|
933
|
+
) {
|
|
934
|
+
return "Reviewing dependencies and security signals";
|
|
935
|
+
}
|
|
936
|
+
return "Tracing architecture and code relationships";
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
export function workspaceAssessmentExecution(assignment) {
|
|
940
|
+
const stage = assignment?.stage;
|
|
941
|
+
const requiredOutputHeading = String(
|
|
942
|
+
assignment?.required_output_heading || "",
|
|
943
|
+
).trim();
|
|
944
|
+
if (!stage || !requiredOutputHeading.startsWith("# Assessment Stage: ")) {
|
|
945
|
+
throw new Error(
|
|
946
|
+
"EngineerOS assessment assignment has an unsupported stage.",
|
|
947
|
+
);
|
|
948
|
+
}
|
|
949
|
+
return {
|
|
950
|
+
...connectorExecution(assignment, { requiredOutputHeading }),
|
|
951
|
+
requiredOutputHeading,
|
|
952
|
+
};
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
export async function recoverAssessmentStageOutput({
|
|
956
|
+
completed,
|
|
957
|
+
prompt,
|
|
958
|
+
requiredOutputHeading,
|
|
959
|
+
retry,
|
|
960
|
+
}) {
|
|
961
|
+
try {
|
|
962
|
+
return {
|
|
963
|
+
completed,
|
|
964
|
+
correctionUsed: false,
|
|
965
|
+
report: normalizeAgentStructuredOutput(
|
|
966
|
+
completed.finalMessage,
|
|
967
|
+
requiredOutputHeading,
|
|
968
|
+
),
|
|
969
|
+
};
|
|
970
|
+
} catch (error) {
|
|
971
|
+
if (!recoverableStructuredOutputFailure(error)) throw error;
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
const corrected = await retry(
|
|
975
|
+
assessmentStageCorrectionPrompt(prompt, requiredOutputHeading),
|
|
976
|
+
completed.sessionId,
|
|
977
|
+
);
|
|
978
|
+
try {
|
|
979
|
+
return {
|
|
980
|
+
completed: {
|
|
981
|
+
...corrected,
|
|
982
|
+
usage: mergeTokenUsage(completed.usage, corrected.usage),
|
|
983
|
+
},
|
|
984
|
+
correctionUsed: true,
|
|
985
|
+
report: normalizeAgentStructuredOutput(
|
|
986
|
+
corrected.finalMessage,
|
|
987
|
+
requiredOutputHeading,
|
|
988
|
+
),
|
|
989
|
+
};
|
|
990
|
+
} catch (error) {
|
|
991
|
+
throw new Error(
|
|
992
|
+
`Agent did not return the required stage report after one automatic correction attempt. ${error.message}`,
|
|
993
|
+
{ cause: error },
|
|
994
|
+
);
|
|
995
|
+
}
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
export function assessmentRejectionCorrectionPrompt(
|
|
999
|
+
validationMessage,
|
|
1000
|
+
requiredOutputHeading,
|
|
1001
|
+
) {
|
|
1002
|
+
return [
|
|
1003
|
+
"## Rejected Stage Output Recovery",
|
|
1004
|
+
"",
|
|
1005
|
+
"EngineerOS rejected the previous stage report because it was incomplete or violated the structured Markdown contract:",
|
|
1006
|
+
"",
|
|
1007
|
+
String(validationMessage || "The stage report was rejected.").trim(),
|
|
1008
|
+
"",
|
|
1009
|
+
"Treat the previous stage report as the authoritative draft.",
|
|
1010
|
+
"Copy every valid section and block unchanged; repair only the incomplete or invalid entries identified by EngineerOS validation.",
|
|
1011
|
+
"Do not re-inspect the repository or replace valid evidence unless the validation error requires it.",
|
|
1012
|
+
"Return the entire corrected structured Markdown stage report so EngineerOS can validate it atomically, not only the repaired fragment or missing tail.",
|
|
1013
|
+
`The first non-whitespace line must be exactly: ${requiredOutputHeading}`,
|
|
1014
|
+
"Do not return progress commentary, an explanation of the correction, or a code fence.",
|
|
1015
|
+
].join("\n");
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
export function assessmentStageCorrectionPrompt(
|
|
1019
|
+
originalPrompt,
|
|
1020
|
+
requiredOutputHeading,
|
|
1021
|
+
) {
|
|
1022
|
+
return [
|
|
1023
|
+
String(originalPrompt || "").trim(),
|
|
1024
|
+
"",
|
|
1025
|
+
"## Incomplete Stage Output Recovery",
|
|
1026
|
+
"",
|
|
1027
|
+
"The previous turn ended without a complete stage deliverable. Return the entire structured Markdown stage report now.",
|
|
1028
|
+
"Use repository context already inspected in the previous turn when it is available; inspect only what remains necessary.",
|
|
1029
|
+
`The first non-whitespace line of the final answer must be exactly: ${requiredOutputHeading}`,
|
|
1030
|
+
"Follow every section, inspection, and completeness rule in the Assignment.",
|
|
1031
|
+
"Do not return progress commentary, an explanation of the correction, or a code fence.",
|
|
1032
|
+
].join("\n");
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
function recoverableStructuredOutputFailure(error) {
|
|
1036
|
+
const message = error instanceof Error ? error.message : "";
|
|
1037
|
+
return (
|
|
1038
|
+
message.startsWith("Agent completed") ||
|
|
1039
|
+
message.startsWith("Agent response")
|
|
1040
|
+
);
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
function mergeTokenUsage(first, second) {
|
|
1044
|
+
const usages = [first, second].filter(
|
|
1045
|
+
(usage) => usage && typeof usage === "object",
|
|
1046
|
+
);
|
|
1047
|
+
if (!usages.length) return null;
|
|
1048
|
+
return Object.fromEntries(
|
|
1049
|
+
[
|
|
1050
|
+
"input_tokens",
|
|
1051
|
+
"output_tokens",
|
|
1052
|
+
"cache_read_tokens",
|
|
1053
|
+
"cache_write_tokens",
|
|
1054
|
+
"reasoning_tokens",
|
|
1055
|
+
"total_tokens",
|
|
1056
|
+
].map((field) => [
|
|
1057
|
+
field,
|
|
1058
|
+
usages.reduce(
|
|
1059
|
+
(total, usage) =>
|
|
1060
|
+
total + (Number.isFinite(usage[field]) ? usage[field] : 0),
|
|
1061
|
+
0,
|
|
1062
|
+
),
|
|
1063
|
+
]),
|
|
1064
|
+
);
|
|
1065
|
+
}
|
|
1066
|
+
|
|
1067
|
+
export function enqueueWorkspaceAssessment(
|
|
1068
|
+
queue,
|
|
1069
|
+
activeAssessment,
|
|
1070
|
+
assignment,
|
|
1071
|
+
{ front = false } = {},
|
|
1072
|
+
) {
|
|
1073
|
+
const matches = (candidate) =>
|
|
1074
|
+
candidate?.assessment_id === assignment?.assessment_id &&
|
|
1075
|
+
candidate?.stage === assignment?.stage;
|
|
1076
|
+
if (
|
|
1077
|
+
activeAssessment?.kind === "assessment" &&
|
|
1078
|
+
activeAssessment.runId === assignment?.assessment_id &&
|
|
1079
|
+
activeAssessment.stage === assignment?.stage
|
|
1080
|
+
) {
|
|
1081
|
+
activeAssessment.resumeAssignment = assignment;
|
|
1082
|
+
return "deferred";
|
|
1083
|
+
}
|
|
1084
|
+
if (queue.some(matches)) return "duplicate";
|
|
1085
|
+
if (front) queue.unshift(assignment);
|
|
1086
|
+
else queue.push(assignment);
|
|
1087
|
+
return "queued";
|
|
1088
|
+
}
|
|
1089
|
+
|
|
1088
1090
|
export function takeWorkspaceAssessmentWave(queue, activeCount, limit) {
|
|
1089
|
-
const available = Math.max(0, limit - activeCount);
|
|
1090
|
-
if (!available || !queue.length) return [];
|
|
1091
|
-
if (queue[0]?.parallelizable !== true) {
|
|
1092
|
-
return activeCount === 0 ? queue.splice(0, 1) : [];
|
|
1093
|
-
}
|
|
1094
|
-
const wave = [];
|
|
1095
|
-
while (wave.length < available && queue[0]?.parallelizable === true) {
|
|
1096
|
-
wave.push(queue.shift());
|
|
1097
|
-
}
|
|
1098
|
-
return wave;
|
|
1099
|
-
}
|
|
1100
|
-
|
|
1101
|
-
export function workspaceAssessmentWorkerLimit(
|
|
1102
|
-
configuredLimit,
|
|
1103
|
-
availableParallelism = os.availableParallelism(),
|
|
1104
|
-
) {
|
|
1105
|
-
if (configuredLimit !== undefined && configuredLimit !== null) {
|
|
1106
|
-
const parsed = Number(configuredLimit);
|
|
1107
|
-
if (
|
|
1108
|
-
!Number.isInteger(parsed) ||
|
|
1109
|
-
parsed < 1 ||
|
|
1110
|
-
parsed > MAX_ASSESSMENT_WORKERS
|
|
1111
|
-
) {
|
|
1112
|
-
throw new Error(
|
|
1113
|
-
`Assessment workers must be a whole number from 1 to ${MAX_ASSESSMENT_WORKERS}.`,
|
|
1114
|
-
);
|
|
1115
|
-
}
|
|
1116
|
-
return parsed;
|
|
1117
|
-
}
|
|
1118
|
-
return Math.min(
|
|
1119
|
-
MAX_AUTOMATIC_ASSESSMENT_WORKERS,
|
|
1120
|
-
Math.max(1, Math.floor(Number(availableParallelism) / 2) || 1),
|
|
1121
|
-
);
|
|
1122
|
-
}
|
|
1123
|
-
|
|
1124
|
-
export function assessmentWorkerSnapshots(activeAssessments) {
|
|
1125
|
-
return [...activeAssessments.values()].map((state) => ({
|
|
1126
|
-
assessment_id: String(state.runId),
|
|
1127
|
-
stage: String(state.stage).slice(0, 96),
|
|
1128
|
-
phase: state.phase,
|
|
1129
|
-
progress_percent: Math.max(
|
|
1130
|
-
0,
|
|
1131
|
-
Math.min(95, Number(state.progressPercent) || 0),
|
|
1132
|
-
),
|
|
1133
|
-
message: String(state.lastMessage || "Assessment worker is starting").slice(
|
|
1134
|
-
0,
|
|
1135
|
-
500,
|
|
1136
|
-
),
|
|
1137
|
-
started_at: new Date(state.startedAt).toISOString(),
|
|
1138
|
-
last_activity_at: new Date(state.lastActivityAt).toISOString(),
|
|
1139
|
-
event_count: Math.max(0, Number(state.eventCount) || 0),
|
|
1140
|
-
}));
|
|
1091
|
+
const available = Math.max(0, limit - activeCount);
|
|
1092
|
+
if (!available || !queue.length) return [];
|
|
1093
|
+
if (queue[0]?.parallelizable !== true) {
|
|
1094
|
+
return activeCount === 0 ? queue.splice(0, 1) : [];
|
|
1095
|
+
}
|
|
1096
|
+
const wave = [];
|
|
1097
|
+
while (wave.length < available && queue[0]?.parallelizable === true) {
|
|
1098
|
+
wave.push(queue.shift());
|
|
1099
|
+
}
|
|
1100
|
+
return wave;
|
|
1141
1101
|
}
|
|
1142
1102
|
|
|
1143
|
-
export function
|
|
1144
|
-
|
|
1145
|
-
assessmentState,
|
|
1146
|
-
{ accepted, failureReported },
|
|
1147
|
-
) {
|
|
1148
|
-
if (accepted || failureReported || !assessmentState?.resumeAssignment) {
|
|
1149
|
-
return false;
|
|
1150
|
-
}
|
|
1151
|
-
return (
|
|
1152
|
-
enqueueWorkspaceAssessment(queue, null, assessmentState.resumeAssignment, {
|
|
1153
|
-
front: true,
|
|
1154
|
-
}) === "queued"
|
|
1155
|
-
);
|
|
1156
|
-
}
|
|
1157
|
-
|
|
1158
|
-
export async function submitAssessmentResultWithRetry(
|
|
1159
|
-
payload,
|
|
1160
|
-
submit,
|
|
1161
|
-
{
|
|
1162
|
-
isActive = () => true,
|
|
1163
|
-
onRetry = () => {},
|
|
1164
|
-
wait = (milliseconds) =>
|
|
1165
|
-
new Promise((resolve) => setTimeout(resolve, milliseconds)),
|
|
1166
|
-
initialDelayMs = 1_000,
|
|
1167
|
-
maxDelayMs = 30_000,
|
|
1168
|
-
} = {},
|
|
1169
|
-
) {
|
|
1170
|
-
let retryDelayMs = initialDelayMs;
|
|
1171
|
-
while (isActive()) {
|
|
1172
|
-
let retryFailure;
|
|
1173
|
-
try {
|
|
1174
|
-
const response = await submit(payload);
|
|
1175
|
-
if (!isTransientAssessmentResultResponse(response)) return response;
|
|
1176
|
-
await response.body?.cancel?.();
|
|
1177
|
-
retryFailure = new Error(
|
|
1178
|
-
`EngineerOS temporarily rejected the completed assessment result (${response.status}).`,
|
|
1179
|
-
);
|
|
1180
|
-
} catch (error) {
|
|
1181
|
-
retryFailure = error;
|
|
1182
|
-
}
|
|
1183
|
-
if (!isActive()) break;
|
|
1184
|
-
onRetry(retryFailure, retryDelayMs);
|
|
1185
|
-
await wait(retryDelayMs);
|
|
1186
|
-
retryDelayMs = Math.min(maxDelayMs, retryDelayMs * 2);
|
|
1187
|
-
}
|
|
1188
|
-
throw new Error(
|
|
1189
|
-
"Assessment result delivery stopped before EngineerOS accepted it.",
|
|
1190
|
-
);
|
|
1191
|
-
}
|
|
1192
|
-
|
|
1193
|
-
export async function submitAssessmentWithValidationRepair(
|
|
1194
|
-
initialResult,
|
|
1195
|
-
{
|
|
1196
|
-
submit,
|
|
1197
|
-
payloadFor,
|
|
1198
|
-
rejectionFor,
|
|
1199
|
-
correct,
|
|
1200
|
-
onCorrected = async () => {},
|
|
1201
|
-
maxRepeatedFailures = MAX_REPEATED_ASSESSMENT_VALIDATION_FAILURES,
|
|
1202
|
-
},
|
|
1203
|
-
) {
|
|
1204
|
-
let result = initialResult;
|
|
1205
|
-
let response = await submit(await payloadFor(result));
|
|
1206
|
-
const rejectionCounts = new Map();
|
|
1207
|
-
while (response.status === 422) {
|
|
1208
|
-
const rejection = await rejectionFor(response);
|
|
1209
|
-
const rejectionCount = (rejectionCounts.get(rejection) || 0) + 1;
|
|
1210
|
-
rejectionCounts.set(rejection, rejectionCount);
|
|
1211
|
-
if (rejectionCount > maxRepeatedFailures) {
|
|
1212
|
-
throw new Error(
|
|
1213
|
-
`${rejection} The Agent repeated this unresolved validation failure ${maxRepeatedFailures} times; the connector retained the latest local report for retry.`,
|
|
1214
|
-
);
|
|
1215
|
-
}
|
|
1216
|
-
result = await correct(result, rejection);
|
|
1217
|
-
result = (await onCorrected(result, rejection)) || result;
|
|
1218
|
-
response = await submit(await payloadFor(result));
|
|
1219
|
-
}
|
|
1220
|
-
return { result, response };
|
|
1221
|
-
}
|
|
1222
|
-
|
|
1223
|
-
function isTransientAssessmentResultResponse(response) {
|
|
1224
|
-
return (
|
|
1225
|
-
response.status === 408 ||
|
|
1226
|
-
response.status === 425 ||
|
|
1227
|
-
response.status === 429 ||
|
|
1228
|
-
response.status >= 500
|
|
1229
|
-
);
|
|
1230
|
-
}
|
|
1231
|
-
|
|
1232
|
-
export function connectorExecution(assignment, options = {}) {
|
|
1233
|
-
const rawPrompt = assignment?.prompt_markdown;
|
|
1234
|
-
if (typeof rawPrompt !== "string" || !rawPrompt.trim()) {
|
|
1235
|
-
throw new Error("EngineerOS assignment is missing prompt_markdown.");
|
|
1236
|
-
}
|
|
1237
|
-
const sandboxMode = assignment?.sandbox_mode;
|
|
1238
|
-
if (!new Set(["read-only", "workspace-write"]).has(sandboxMode)) {
|
|
1239
|
-
throw new Error("EngineerOS assignment has an unsupported sandbox_mode.");
|
|
1240
|
-
}
|
|
1241
|
-
const rawProfile = assignment?.execution_profile ?? {};
|
|
1242
|
-
const model =
|
|
1243
|
-
typeof rawProfile.model === "string" ? rawProfile.model.trim() : "";
|
|
1244
|
-
const reasoningEffort = rawProfile.reasoning_effort;
|
|
1103
|
+
export function workspaceAssessmentWorkerLimit(value) {
|
|
1104
|
+
const parsed = Number(value ?? DEFAULT_ASSESSMENT_WORKERS);
|
|
1245
1105
|
if (
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
)
|
|
1106
|
+
!Number.isInteger(parsed) ||
|
|
1107
|
+
parsed < MIN_ASSESSMENT_WORKERS ||
|
|
1108
|
+
parsed > MAX_ASSESSMENT_WORKERS
|
|
1250
1109
|
) {
|
|
1251
1110
|
throw new Error(
|
|
1252
|
-
|
|
1253
|
-
);
|
|
1254
|
-
}
|
|
1255
|
-
const agentRole = assignment?.agent_role;
|
|
1256
|
-
const prompt = buildAgentHarnessPrompt({
|
|
1257
|
-
agentRole,
|
|
1258
|
-
agentDefinition: assignment?.agent_definition,
|
|
1259
|
-
prompt: rawPrompt,
|
|
1260
|
-
sandboxMode,
|
|
1261
|
-
requiredOutputHeading: options.requiredOutputHeading,
|
|
1262
|
-
});
|
|
1263
|
-
return {
|
|
1264
|
-
prompt,
|
|
1265
|
-
agentRole,
|
|
1266
|
-
sandboxMode,
|
|
1267
|
-
sessionKey:
|
|
1268
|
-
typeof assignment.session_key === "string" &&
|
|
1269
|
-
assignment.session_key.trim()
|
|
1270
|
-
? assignment.session_key.trim()
|
|
1271
|
-
: `project-${String(agentRole)}-${String(
|
|
1272
|
-
assignment.purpose || "general",
|
|
1273
|
-
)
|
|
1274
|
-
.toLowerCase()
|
|
1275
|
-
.replace(/[^a-z0-9]+/g, "-")
|
|
1276
|
-
.slice(0, 80)}`,
|
|
1277
|
-
profile: {
|
|
1278
|
-
...(model ? { model } : {}),
|
|
1279
|
-
...(reasoningEffort ? { reasoning_effort: reasoningEffort } : {}),
|
|
1280
|
-
},
|
|
1281
|
-
};
|
|
1282
|
-
}
|
|
1283
|
-
|
|
1284
|
-
export async function stopProcess(child) {
|
|
1285
|
-
if (!child || child.exitCode !== null) return;
|
|
1286
|
-
if (typeof child.engineerOsCancel === "function") {
|
|
1287
|
-
await child.engineerOsCancel();
|
|
1288
|
-
return;
|
|
1289
|
-
}
|
|
1290
|
-
if (process.platform === "win32") {
|
|
1291
|
-
await run(
|
|
1292
|
-
"taskkill",
|
|
1293
|
-
["/pid", String(child.pid), "/t", "/f"],
|
|
1294
|
-
process.cwd(),
|
|
1295
|
-
{ allowFailure: true },
|
|
1111
|
+
`Assessment worker limit must be a whole number from ${MIN_ASSESSMENT_WORKERS} to ${MAX_ASSESSMENT_WORKERS}.`,
|
|
1296
1112
|
);
|
|
1297
|
-
} else {
|
|
1298
|
-
child.kill("SIGTERM");
|
|
1299
1113
|
}
|
|
1114
|
+
return parsed;
|
|
1300
1115
|
}
|
|
1301
|
-
|
|
1302
|
-
export
|
|
1303
|
-
|
|
1304
|
-
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
|
|
1318
|
-
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
|
|
1346
|
-
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
|
|
1353
|
-
|
|
1354
|
-
|
|
1355
|
-
|
|
1356
|
-
|
|
1357
|
-
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
1375
|
-
|
|
1376
|
-
|
|
1377
|
-
|
|
1378
|
-
|
|
1379
|
-
|
|
1380
|
-
|
|
1381
|
-
|
|
1382
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
);
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
const
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
)
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1430
|
-
|
|
1431
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
|
|
1502
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
1505
|
-
|
|
1506
|
-
|
|
1507
|
-
|
|
1508
|
-
|
|
1509
|
-
|
|
1510
|
-
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
|
|
1527
|
-
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1553
|
-
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
|
|
1574
|
-
|
|
1575
|
-
|
|
1576
|
-
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
)
|
|
1594
|
-
);
|
|
1595
|
-
}
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
|
|
1613
|
-
|
|
1614
|
-
);
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
)
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
"
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
)
|
|
1655
|
-
if (
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
|
|
1665
|
-
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1116
|
+
|
|
1117
|
+
export function assessmentWorkerSnapshots(activeAssessments) {
|
|
1118
|
+
return [...activeAssessments.values()].map((state) => ({
|
|
1119
|
+
assessment_id: String(state.runId),
|
|
1120
|
+
stage: String(state.stage).slice(0, 96),
|
|
1121
|
+
phase: state.phase,
|
|
1122
|
+
progress_percent: Math.max(
|
|
1123
|
+
0,
|
|
1124
|
+
Math.min(95, Number(state.progressPercent) || 0),
|
|
1125
|
+
),
|
|
1126
|
+
message: String(state.lastMessage || "Assessment worker is starting").slice(
|
|
1127
|
+
0,
|
|
1128
|
+
500,
|
|
1129
|
+
),
|
|
1130
|
+
started_at: new Date(state.startedAt).toISOString(),
|
|
1131
|
+
last_activity_at: new Date(state.lastActivityAt).toISOString(),
|
|
1132
|
+
event_count: Math.max(0, Number(state.eventCount) || 0),
|
|
1133
|
+
}));
|
|
1134
|
+
}
|
|
1135
|
+
|
|
1136
|
+
export function requeueInterruptedAssessment(
|
|
1137
|
+
queue,
|
|
1138
|
+
assessmentState,
|
|
1139
|
+
{ accepted, failureReported },
|
|
1140
|
+
) {
|
|
1141
|
+
if (accepted || failureReported || !assessmentState?.resumeAssignment) {
|
|
1142
|
+
return false;
|
|
1143
|
+
}
|
|
1144
|
+
return (
|
|
1145
|
+
enqueueWorkspaceAssessment(queue, null, assessmentState.resumeAssignment, {
|
|
1146
|
+
front: true,
|
|
1147
|
+
}) === "queued"
|
|
1148
|
+
);
|
|
1149
|
+
}
|
|
1150
|
+
|
|
1151
|
+
export async function submitAssessmentResultWithRetry(
|
|
1152
|
+
payload,
|
|
1153
|
+
submit,
|
|
1154
|
+
{
|
|
1155
|
+
isActive = () => true,
|
|
1156
|
+
onRetry = () => {},
|
|
1157
|
+
wait = (milliseconds) =>
|
|
1158
|
+
new Promise((resolve) => setTimeout(resolve, milliseconds)),
|
|
1159
|
+
initialDelayMs = 1_000,
|
|
1160
|
+
maxDelayMs = 30_000,
|
|
1161
|
+
} = {},
|
|
1162
|
+
) {
|
|
1163
|
+
let retryDelayMs = initialDelayMs;
|
|
1164
|
+
while (isActive()) {
|
|
1165
|
+
let retryFailure;
|
|
1166
|
+
try {
|
|
1167
|
+
const response = await submit(payload);
|
|
1168
|
+
if (!isTransientAssessmentResultResponse(response)) return response;
|
|
1169
|
+
await response.body?.cancel?.();
|
|
1170
|
+
retryFailure = new Error(
|
|
1171
|
+
`EngineerOS temporarily rejected the completed assessment result (${response.status}).`,
|
|
1172
|
+
);
|
|
1173
|
+
} catch (error) {
|
|
1174
|
+
retryFailure = error;
|
|
1175
|
+
}
|
|
1176
|
+
if (!isActive()) break;
|
|
1177
|
+
onRetry(retryFailure, retryDelayMs);
|
|
1178
|
+
await wait(retryDelayMs);
|
|
1179
|
+
retryDelayMs = Math.min(maxDelayMs, retryDelayMs * 2);
|
|
1180
|
+
}
|
|
1181
|
+
throw new Error(
|
|
1182
|
+
"Assessment result delivery stopped before EngineerOS accepted it.",
|
|
1183
|
+
);
|
|
1184
|
+
}
|
|
1185
|
+
|
|
1186
|
+
export async function submitAssessmentWithValidationRepair(
|
|
1187
|
+
initialResult,
|
|
1188
|
+
{
|
|
1189
|
+
submit,
|
|
1190
|
+
payloadFor,
|
|
1191
|
+
rejectionFor,
|
|
1192
|
+
correct,
|
|
1193
|
+
onCorrected = async () => {},
|
|
1194
|
+
maxRepeatedFailures = MAX_REPEATED_ASSESSMENT_VALIDATION_FAILURES,
|
|
1195
|
+
},
|
|
1196
|
+
) {
|
|
1197
|
+
let result = initialResult;
|
|
1198
|
+
let response = await submit(await payloadFor(result));
|
|
1199
|
+
const rejectionCounts = new Map();
|
|
1200
|
+
while (response.status === 422) {
|
|
1201
|
+
const rejection = await rejectionFor(response);
|
|
1202
|
+
const rejectionCount = (rejectionCounts.get(rejection) || 0) + 1;
|
|
1203
|
+
rejectionCounts.set(rejection, rejectionCount);
|
|
1204
|
+
if (rejectionCount > maxRepeatedFailures) {
|
|
1205
|
+
throw new Error(
|
|
1206
|
+
`${rejection} The Agent repeated this unresolved validation failure ${maxRepeatedFailures} times; the connector retained the latest local report for retry.`,
|
|
1207
|
+
);
|
|
1208
|
+
}
|
|
1209
|
+
result = await correct(result, rejection);
|
|
1210
|
+
result = (await onCorrected(result, rejection)) || result;
|
|
1211
|
+
response = await submit(await payloadFor(result));
|
|
1212
|
+
}
|
|
1213
|
+
return { result, response };
|
|
1214
|
+
}
|
|
1215
|
+
|
|
1216
|
+
function isTransientAssessmentResultResponse(response) {
|
|
1217
|
+
return (
|
|
1218
|
+
response.status === 408 ||
|
|
1219
|
+
response.status === 425 ||
|
|
1220
|
+
response.status === 429 ||
|
|
1221
|
+
response.status >= 500
|
|
1222
|
+
);
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
export function connectorExecution(assignment, options = {}) {
|
|
1226
|
+
const rawPrompt = assignment?.prompt_markdown;
|
|
1227
|
+
if (typeof rawPrompt !== "string" || !rawPrompt.trim()) {
|
|
1228
|
+
throw new Error("EngineerOS assignment is missing prompt_markdown.");
|
|
1229
|
+
}
|
|
1230
|
+
const sandboxMode = assignment?.sandbox_mode;
|
|
1231
|
+
if (!new Set(["read-only", "workspace-write"]).has(sandboxMode)) {
|
|
1232
|
+
throw new Error("EngineerOS assignment has an unsupported sandbox_mode.");
|
|
1233
|
+
}
|
|
1234
|
+
const rawProfile = assignment?.execution_profile ?? {};
|
|
1235
|
+
const model =
|
|
1236
|
+
typeof rawProfile.model === "string" ? rawProfile.model.trim() : "";
|
|
1237
|
+
const reasoningEffort = rawProfile.reasoning_effort;
|
|
1238
|
+
if (
|
|
1239
|
+
reasoningEffort &&
|
|
1240
|
+
!new Set(["minimal", "low", "medium", "high", "xhigh", "max", "ultra"]).has(
|
|
1241
|
+
reasoningEffort,
|
|
1242
|
+
)
|
|
1243
|
+
) {
|
|
1244
|
+
throw new Error(
|
|
1245
|
+
"EngineerOS assignment has an unsupported reasoning effort.",
|
|
1246
|
+
);
|
|
1247
|
+
}
|
|
1248
|
+
const agentRole = assignment?.agent_role;
|
|
1249
|
+
const prompt = buildAgentHarnessPrompt({
|
|
1250
|
+
agentRole,
|
|
1251
|
+
agentDefinition: assignment?.agent_definition,
|
|
1252
|
+
prompt: rawPrompt,
|
|
1253
|
+
sandboxMode,
|
|
1254
|
+
requiredOutputHeading: options.requiredOutputHeading,
|
|
1255
|
+
});
|
|
1256
|
+
return {
|
|
1257
|
+
prompt,
|
|
1258
|
+
agentRole,
|
|
1259
|
+
sandboxMode,
|
|
1260
|
+
sessionKey:
|
|
1261
|
+
typeof assignment.session_key === "string" &&
|
|
1262
|
+
assignment.session_key.trim()
|
|
1263
|
+
? assignment.session_key.trim()
|
|
1264
|
+
: `project-${String(agentRole)}-${String(
|
|
1265
|
+
assignment.purpose || "general",
|
|
1266
|
+
)
|
|
1267
|
+
.toLowerCase()
|
|
1268
|
+
.replace(/[^a-z0-9]+/g, "-")
|
|
1269
|
+
.slice(0, 80)}`,
|
|
1270
|
+
profile: {
|
|
1271
|
+
...(model ? { model } : {}),
|
|
1272
|
+
...(reasoningEffort ? { reasoning_effort: reasoningEffort } : {}),
|
|
1273
|
+
},
|
|
1274
|
+
};
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
export async function stopProcess(child) {
|
|
1278
|
+
if (!child || child.exitCode !== null) return;
|
|
1279
|
+
if (typeof child.engineerOsCancel === "function") {
|
|
1280
|
+
await child.engineerOsCancel();
|
|
1281
|
+
return;
|
|
1282
|
+
}
|
|
1283
|
+
if (process.platform === "win32") {
|
|
1284
|
+
await run(
|
|
1285
|
+
"taskkill",
|
|
1286
|
+
["/pid", String(child.pid), "/t", "/f"],
|
|
1287
|
+
process.cwd(),
|
|
1288
|
+
{ allowFailure: true },
|
|
1289
|
+
);
|
|
1290
|
+
} else {
|
|
1291
|
+
child.kill("SIGTERM");
|
|
1292
|
+
}
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1295
|
+
export async function workspaceSnapshot(workspace) {
|
|
1296
|
+
const root = path.resolve(workspace);
|
|
1297
|
+
const gitFiles = await run(
|
|
1298
|
+
"git",
|
|
1299
|
+
["ls-files", "-z", "-co", "--exclude-standard"],
|
|
1300
|
+
root,
|
|
1301
|
+
{ allowFailure: true },
|
|
1302
|
+
);
|
|
1303
|
+
const candidates =
|
|
1304
|
+
gitFiles.code === 0
|
|
1305
|
+
? gitPathList(gitFiles.stdout)
|
|
1306
|
+
: await workspaceFiles(root);
|
|
1307
|
+
const ignoredFiles =
|
|
1308
|
+
gitFiles.code === 0
|
|
1309
|
+
? gitPathList(
|
|
1310
|
+
(
|
|
1311
|
+
await run(
|
|
1312
|
+
"git",
|
|
1313
|
+
["ls-files", "-z", "--others", "--ignored", "--exclude-standard"],
|
|
1314
|
+
root,
|
|
1315
|
+
{ allowFailure: true },
|
|
1316
|
+
)
|
|
1317
|
+
).stdout,
|
|
1318
|
+
)
|
|
1319
|
+
: [];
|
|
1320
|
+
const trackedFiles =
|
|
1321
|
+
gitFiles.code === 0
|
|
1322
|
+
? new Set(
|
|
1323
|
+
gitPathList(
|
|
1324
|
+
(
|
|
1325
|
+
await run("git", ["ls-files", "-z", "--cached"], root, {
|
|
1326
|
+
allowFailure: true,
|
|
1327
|
+
})
|
|
1328
|
+
).stdout,
|
|
1329
|
+
),
|
|
1330
|
+
)
|
|
1331
|
+
: new Set();
|
|
1332
|
+
const modifiedFiles =
|
|
1333
|
+
gitFiles.code === 0
|
|
1334
|
+
? new Set(
|
|
1335
|
+
gitPathList(
|
|
1336
|
+
(
|
|
1337
|
+
await run("git", ["diff", "--name-only", "-z", "--"], root, {
|
|
1338
|
+
allowFailure: true,
|
|
1339
|
+
})
|
|
1340
|
+
).stdout,
|
|
1341
|
+
),
|
|
1342
|
+
)
|
|
1343
|
+
: new Set();
|
|
1344
|
+
const stagedFiles =
|
|
1345
|
+
gitFiles.code === 0
|
|
1346
|
+
? new Set(
|
|
1347
|
+
gitPathList(
|
|
1348
|
+
(
|
|
1349
|
+
await run(
|
|
1350
|
+
"git",
|
|
1351
|
+
["diff", "--cached", "--name-only", "-z", "--"],
|
|
1352
|
+
root,
|
|
1353
|
+
{ allowFailure: true },
|
|
1354
|
+
)
|
|
1355
|
+
).stdout,
|
|
1356
|
+
),
|
|
1357
|
+
)
|
|
1358
|
+
: new Set();
|
|
1359
|
+
const gitStatus =
|
|
1360
|
+
gitFiles.code === 0
|
|
1361
|
+
? await run(
|
|
1362
|
+
"git",
|
|
1363
|
+
["status", "--porcelain=v1", "-z", "--untracked-files=all"],
|
|
1364
|
+
root,
|
|
1365
|
+
{ allowFailure: true },
|
|
1366
|
+
)
|
|
1367
|
+
: { stdout: "" };
|
|
1368
|
+
const inventory = [];
|
|
1369
|
+
let excludedFileCount = new Set(ignoredFiles).size;
|
|
1370
|
+
const sortedCandidates = [...new Set(candidates)].sort();
|
|
1371
|
+
for (let start = 0; start < sortedCandidates.length; start += 256) {
|
|
1372
|
+
const batch = sortedCandidates.slice(start, start + 256);
|
|
1373
|
+
const inspected = await Promise.all(
|
|
1374
|
+
batch.map(async (relative) => ({
|
|
1375
|
+
relative,
|
|
1376
|
+
details: isShareablePath(relative)
|
|
1377
|
+
? await lstat(path.join(root, relative)).catch(() => null)
|
|
1378
|
+
: null,
|
|
1379
|
+
})),
|
|
1380
|
+
);
|
|
1381
|
+
for (const { relative, details } of inspected) {
|
|
1382
|
+
if (!details?.isFile() || details.isSymbolicLink()) {
|
|
1383
|
+
excludedFileCount += 1;
|
|
1384
|
+
continue;
|
|
1385
|
+
}
|
|
1386
|
+
inventory.push({
|
|
1387
|
+
path: relative,
|
|
1388
|
+
size: details.size,
|
|
1389
|
+
classification: evidenceClassification(relative),
|
|
1390
|
+
git_state: inventoryGitState(
|
|
1391
|
+
relative,
|
|
1392
|
+
trackedFiles,
|
|
1393
|
+
modifiedFiles,
|
|
1394
|
+
stagedFiles,
|
|
1395
|
+
),
|
|
1396
|
+
});
|
|
1397
|
+
if (inventory.length > MAX_INVENTORY_FILES) {
|
|
1398
|
+
throw new Error(
|
|
1399
|
+
`Workspace contains more than ${MAX_INVENTORY_FILES.toLocaleString()} safe files. Configure repository exclusions for generated or data directories, then reconnect.`,
|
|
1400
|
+
);
|
|
1401
|
+
}
|
|
1402
|
+
}
|
|
1403
|
+
}
|
|
1404
|
+
const representativeSources = representativeSourcePaths(inventory);
|
|
1405
|
+
const evidenceCandidates = [...inventory].sort((left, right) => {
|
|
1406
|
+
const priorityDifference =
|
|
1407
|
+
evidencePriority(left, representativeSources) -
|
|
1408
|
+
evidencePriority(right, representativeSources);
|
|
1409
|
+
return priorityDifference || left.path.localeCompare(right.path);
|
|
1410
|
+
});
|
|
1411
|
+
const evidenceEntries = [];
|
|
1412
|
+
let evidenceBytes = 0;
|
|
1413
|
+
for (const entry of evidenceCandidates) {
|
|
1414
|
+
if (
|
|
1415
|
+
evidenceEntries.length >= MAX_EVIDENCE_FILES ||
|
|
1416
|
+
entry.size > MAX_SHAREABLE_FILE_BYTES ||
|
|
1417
|
+
evidenceBytes + entry.size > MAX_EVIDENCE_BYTES
|
|
1418
|
+
) {
|
|
1419
|
+
continue;
|
|
1420
|
+
}
|
|
1421
|
+
evidenceEntries.push(entry);
|
|
1422
|
+
evidenceBytes += entry.size;
|
|
1423
|
+
}
|
|
1424
|
+
const evidenceFiles = await readEvidenceFiles(root, evidenceEntries);
|
|
1425
|
+
const archive = await createZip(evidenceFiles);
|
|
1426
|
+
if (archive.length > MAX_ARCHIVE_BYTES) {
|
|
1427
|
+
throw new Error(
|
|
1428
|
+
"Prioritized workspace evidence exceeds the 25 MB archive limit. Configure repository exclusions for large source or data files, then reconnect.",
|
|
1429
|
+
);
|
|
1430
|
+
}
|
|
1431
|
+
const inventoryJson = Buffer.from(
|
|
1432
|
+
JSON.stringify({ version: 1, entries: inventory }),
|
|
1433
|
+
"utf8",
|
|
1434
|
+
);
|
|
1435
|
+
const compressedInventory = await gzipBuffer(inventoryJson);
|
|
1436
|
+
if (compressedInventory.length > MAX_COMPRESSED_INVENTORY_BYTES) {
|
|
1437
|
+
throw new Error(
|
|
1438
|
+
"Compressed workspace inventory exceeds 10 MB. Configure repository exclusions for generated or data directories, then reconnect.",
|
|
1439
|
+
);
|
|
1440
|
+
}
|
|
1441
|
+
const head = await run("git", ["rev-parse", "HEAD"], root, {
|
|
1442
|
+
allowFailure: true,
|
|
1443
|
+
});
|
|
1444
|
+
return {
|
|
1445
|
+
inventory_base64: compressedInventory.toString("base64"),
|
|
1446
|
+
evidence_archive_base64: archive.toString("base64"),
|
|
1447
|
+
media_type: "application/zip",
|
|
1448
|
+
workspace_name: path.basename(root),
|
|
1449
|
+
workspace_kind: inventory.some(({ path: relative }) =>
|
|
1450
|
+
isCodeBearing(relative),
|
|
1451
|
+
)
|
|
1452
|
+
? "brownfield"
|
|
1453
|
+
: "greenfield",
|
|
1454
|
+
total_file_count: inventory.length,
|
|
1455
|
+
evidence_file_count: evidenceFiles.length,
|
|
1456
|
+
excluded_file_count: excludedFileCount,
|
|
1457
|
+
omitted_evidence_file_count: inventory.length - evidenceFiles.length,
|
|
1458
|
+
head_revision: head.code === 0 ? head.stdout.trim().slice(0, 128) : null,
|
|
1459
|
+
working_tree_status_digest: sha256(gitStatus.stdout),
|
|
1460
|
+
inventory_digest: sha256(inventoryJson),
|
|
1461
|
+
evidence_policy_version: EVIDENCE_POLICY_VERSION,
|
|
1462
|
+
};
|
|
1463
|
+
}
|
|
1464
|
+
|
|
1465
|
+
function evidenceClassification(relative) {
|
|
1466
|
+
const normalized = normalizePath(relative).toLowerCase();
|
|
1467
|
+
const name = path.posix.basename(normalized);
|
|
1468
|
+
const extension = path.posix.extname(name);
|
|
1469
|
+
if (CODE_MARKERS.has(name)) return "manifest";
|
|
1470
|
+
if (LOCK_FILE_NAMES.has(name)) return "lockfile";
|
|
1471
|
+
if (
|
|
1472
|
+
normalized.startsWith(".github/workflows/") ||
|
|
1473
|
+
[
|
|
1474
|
+
".gitlab-ci.yml",
|
|
1475
|
+
"azure-pipelines.yml",
|
|
1476
|
+
"docker-compose.yml",
|
|
1477
|
+
"docker-compose.yaml",
|
|
1478
|
+
"dockerfile",
|
|
1479
|
+
"jenkinsfile",
|
|
1480
|
+
].includes(name)
|
|
1481
|
+
) {
|
|
1482
|
+
return "workflow";
|
|
1483
|
+
}
|
|
1484
|
+
if (
|
|
1485
|
+
/(^|\/)(tests|__tests__)\//.test(normalized) ||
|
|
1486
|
+
name.startsWith("test_") ||
|
|
1487
|
+
name.includes(".test.") ||
|
|
1488
|
+
name.includes(".spec.") ||
|
|
1489
|
+
/^(jest|vitest|pytest|playwright|cypress)(\.|$)/.test(name)
|
|
1490
|
+
) {
|
|
1491
|
+
return "test";
|
|
1492
|
+
}
|
|
1493
|
+
if (/(^|\/)(security|compliance|policy|policies)(\/|\.|$)/.test(normalized)) {
|
|
1494
|
+
return "security_configuration";
|
|
1495
|
+
}
|
|
1496
|
+
if (
|
|
1497
|
+
REPOSITORY_CONFIGURATION_NAMES.has(name) ||
|
|
1498
|
+
/(^|\/)(eslint|ruff|mypy|tsconfig|biome|prettier)/.test(normalized)
|
|
1499
|
+
) {
|
|
1500
|
+
return "repository_configuration";
|
|
1501
|
+
}
|
|
1502
|
+
if (ENTRYPOINT_NAMES.has(name)) return "entrypoint";
|
|
1503
|
+
if (
|
|
1504
|
+
/(^|\/)(api|routes|controllers)\//.test(normalized) ||
|
|
1505
|
+
name.endsWith(".d.ts") ||
|
|
1506
|
+
name.includes("openapi") ||
|
|
1507
|
+
name.includes("swagger")
|
|
1508
|
+
) {
|
|
1509
|
+
return "public_api";
|
|
1510
|
+
}
|
|
1511
|
+
if (
|
|
1512
|
+
extension === ".md" ||
|
|
1513
|
+
normalized.startsWith("docs/") ||
|
|
1514
|
+
/(^|\/)(architecture|operations|runbook|deployment)(\/|\.|$)/.test(
|
|
1515
|
+
normalized,
|
|
1516
|
+
)
|
|
1517
|
+
) {
|
|
1518
|
+
return "documentation";
|
|
1519
|
+
}
|
|
1520
|
+
if (CODE_EXTENSIONS.has(extension)) return "source";
|
|
1521
|
+
return "other";
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
function inventoryGitState(relative, trackedFiles, modifiedFiles, stagedFiles) {
|
|
1525
|
+
if (!trackedFiles.has(relative)) return "untracked";
|
|
1526
|
+
const modified = modifiedFiles.has(relative);
|
|
1527
|
+
const staged = stagedFiles.has(relative);
|
|
1528
|
+
if (modified && staged) return "staged_and_modified";
|
|
1529
|
+
if (modified) return "modified";
|
|
1530
|
+
if (staged) return "staged";
|
|
1531
|
+
return "clean";
|
|
1532
|
+
}
|
|
1533
|
+
|
|
1534
|
+
function representativeSourcePaths(inventory) {
|
|
1535
|
+
const representatives = new Set();
|
|
1536
|
+
const groups = new Set();
|
|
1537
|
+
for (const entry of inventory) {
|
|
1538
|
+
if (entry.classification !== "source") continue;
|
|
1539
|
+
const parts = entry.path.split("/");
|
|
1540
|
+
const moduleName = parts.length > 1 ? parts[0] : ".";
|
|
1541
|
+
const extension = path.posix.extname(entry.path).toLowerCase();
|
|
1542
|
+
const group = `${moduleName}:${extension}`;
|
|
1543
|
+
if (groups.has(group)) continue;
|
|
1544
|
+
groups.add(group);
|
|
1545
|
+
representatives.add(entry.path);
|
|
1546
|
+
}
|
|
1547
|
+
return representatives;
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1550
|
+
function evidencePriority(entry, representativeSources) {
|
|
1551
|
+
const priorities = {
|
|
1552
|
+
manifest: 0,
|
|
1553
|
+
lockfile: 0,
|
|
1554
|
+
workflow: 1,
|
|
1555
|
+
security_configuration: 1,
|
|
1556
|
+
repository_configuration: 1,
|
|
1557
|
+
entrypoint: 2,
|
|
1558
|
+
public_api: 2,
|
|
1559
|
+
test: 3,
|
|
1560
|
+
documentation: 4,
|
|
1561
|
+
};
|
|
1562
|
+
if (entry.classification in priorities)
|
|
1563
|
+
return priorities[entry.classification];
|
|
1564
|
+
if (
|
|
1565
|
+
entry.classification === "source" &&
|
|
1566
|
+
representativeSources.has(entry.path)
|
|
1567
|
+
) {
|
|
1568
|
+
return 5;
|
|
1569
|
+
}
|
|
1570
|
+
return 6;
|
|
1571
|
+
}
|
|
1572
|
+
|
|
1573
|
+
function sha256(value) {
|
|
1574
|
+
return createHash("sha256").update(value).digest("hex");
|
|
1575
|
+
}
|
|
1576
|
+
|
|
1577
|
+
async function readEvidenceFiles(root, entries) {
|
|
1578
|
+
const files = [];
|
|
1579
|
+
for (let start = 0; start < entries.length; start += 128) {
|
|
1580
|
+
files.push(
|
|
1581
|
+
...(await Promise.all(
|
|
1582
|
+
entries.slice(start, start + 128).map(async (entry) => ({
|
|
1583
|
+
name: entry.path,
|
|
1584
|
+
data: await readFile(path.join(root, entry.path)),
|
|
1585
|
+
})),
|
|
1586
|
+
)),
|
|
1587
|
+
);
|
|
1588
|
+
}
|
|
1589
|
+
return files;
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1592
|
+
async function prepareRunWorkspace(workspace, runId, baseRevision) {
|
|
1593
|
+
const source = path.resolve(workspace);
|
|
1594
|
+
const root = path.join(os.homedir(), ".engineeros", "runs");
|
|
1595
|
+
const target = path.join(root, runId);
|
|
1596
|
+
await mkdir(root, { recursive: true });
|
|
1597
|
+
const isGit =
|
|
1598
|
+
(
|
|
1599
|
+
await run("git", ["rev-parse", "--is-inside-work-tree"], source, {
|
|
1600
|
+
allowFailure: true,
|
|
1601
|
+
})
|
|
1602
|
+
).code === 0;
|
|
1603
|
+
if (isGit) {
|
|
1604
|
+
const exists = await stat(target).then(
|
|
1605
|
+
() => true,
|
|
1606
|
+
() => false,
|
|
1607
|
+
);
|
|
1608
|
+
if (!exists) {
|
|
1609
|
+
const base = await worktreeBase(source, baseRevision);
|
|
1610
|
+
await run("git", ["worktree", "add", "--detach", target, base], source);
|
|
1611
|
+
}
|
|
1612
|
+
return target;
|
|
1613
|
+
}
|
|
1614
|
+
const exists = await stat(target).then(
|
|
1615
|
+
() => true,
|
|
1616
|
+
() => false,
|
|
1617
|
+
);
|
|
1618
|
+
if (!exists) {
|
|
1619
|
+
await mkdir(target, { recursive: true });
|
|
1620
|
+
await copyWorkspace(source, target);
|
|
1621
|
+
await run("git", ["init"], target);
|
|
1622
|
+
await run("git", ["config", "user.name", "EngineerOS Connector"], target);
|
|
1623
|
+
await run(
|
|
1624
|
+
"git",
|
|
1625
|
+
["config", "user.email", "connector@engineeros.local"],
|
|
1626
|
+
target,
|
|
1627
|
+
);
|
|
1628
|
+
await run("git", ["add", "-A"], target);
|
|
1629
|
+
await run(
|
|
1630
|
+
"git",
|
|
1631
|
+
["commit", "--allow-empty", "-m", "EngineerOS run baseline"],
|
|
1632
|
+
target,
|
|
1633
|
+
);
|
|
1634
|
+
}
|
|
1635
|
+
return target;
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1638
|
+
function launchCodexProcess(
|
|
1639
|
+
workspace,
|
|
1640
|
+
prompt,
|
|
1641
|
+
sandbox,
|
|
1642
|
+
callbacks,
|
|
1643
|
+
profile = {},
|
|
1644
|
+
previousSessionId,
|
|
1645
|
+
skipGitRepoCheck = false,
|
|
1646
|
+
mcpServer,
|
|
1647
|
+
) {
|
|
1648
|
+
if (!mcpServer) {
|
|
1649
|
+
return launchCodexAppServer({
|
|
1650
|
+
workspace,
|
|
1651
|
+
prompt,
|
|
1652
|
+
sandbox,
|
|
1653
|
+
profile,
|
|
1654
|
+
previousSessionId,
|
|
1655
|
+
callbacks,
|
|
1656
|
+
});
|
|
1657
|
+
}
|
|
1658
|
+
const command =
|
|
1659
|
+
process.env.CODEX_BIN ||
|
|
1660
|
+
(process.platform === "win32" ? "codex.cmd" : "codex");
|
|
1661
|
+
const args = codexExecutionArgs({
|
|
1662
|
+
workspace,
|
|
1663
|
+
sandbox,
|
|
1664
|
+
profile,
|
|
1665
|
+
previousSessionId,
|
|
1666
|
+
skipGitRepoCheck,
|
|
1667
|
+
mcpServer,
|
|
1668
|
+
});
|
|
1669
|
+
const child = spawn(command, args, {
|
|
1670
|
+
cwd: workspace,
|
|
1671
|
+
env: process.env,
|
|
1672
|
+
shell: process.platform === "win32",
|
|
1673
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
1674
|
+
});
|
|
1675
|
+
child.stdin.end(prompt);
|
|
1676
|
+
let output = "";
|
|
1677
|
+
let buffer = "";
|
|
1685
1678
|
let finalMessage = "";
|
|
1686
1679
|
let sessionId = previousSessionId || "";
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
buffer
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1680
|
+
let sessionCheckpoint = Promise.resolve();
|
|
1681
|
+
let sessionCheckpointError;
|
|
1682
|
+
child.stdout.setEncoding("utf8");
|
|
1683
|
+
child.stdout.on("data", (chunk) => {
|
|
1684
|
+
output += chunk;
|
|
1685
|
+
buffer += chunk;
|
|
1686
|
+
const lines = buffer.split(/\r?\n/);
|
|
1687
|
+
buffer = lines.pop() ?? "";
|
|
1688
|
+
for (const line of lines) {
|
|
1689
|
+
if (!line.trim()) continue;
|
|
1690
|
+
try {
|
|
1691
|
+
const event = JSON.parse(line);
|
|
1697
1692
|
if (
|
|
1698
1693
|
event.type === "thread.started" &&
|
|
1699
1694
|
typeof event.thread_id === "string"
|
|
1700
1695
|
) {
|
|
1701
1696
|
sessionId = event.thread_id;
|
|
1697
|
+
sessionCheckpoint = Promise.resolve()
|
|
1698
|
+
.then(() => callbacks.onSession?.(sessionId))
|
|
1699
|
+
.catch((error) => {
|
|
1700
|
+
sessionCheckpointError = error;
|
|
1701
|
+
});
|
|
1702
1702
|
}
|
|
1703
|
-
if (
|
|
1704
|
-
event.type === "item.completed" &&
|
|
1705
|
-
event.item?.type === "agent_message" &&
|
|
1706
|
-
typeof event.item.text === "string"
|
|
1707
|
-
) {
|
|
1708
|
-
finalMessage = event.item.text;
|
|
1709
|
-
}
|
|
1710
|
-
callbacks.onEvent?.(event);
|
|
1711
|
-
} catch {
|
|
1712
|
-
callbacks.onEvent?.({
|
|
1713
|
-
type: "agent.output",
|
|
1714
|
-
message: line.slice(0, 500),
|
|
1715
|
-
});
|
|
1716
|
-
}
|
|
1717
|
-
}
|
|
1718
|
-
});
|
|
1719
|
-
child.stderr.setEncoding("utf8");
|
|
1720
|
-
child.stderr.on("data", (chunk) => {
|
|
1721
|
-
output += chunk;
|
|
1722
|
-
callbacks.onEvent?.({
|
|
1723
|
-
type: "agent.stderr",
|
|
1724
|
-
message: chunk.trim().slice(0, 500),
|
|
1725
|
-
});
|
|
1726
|
-
});
|
|
1727
|
-
const completed = new Promise((resolve, reject) => {
|
|
1728
|
-
child.once("error", reject);
|
|
1729
|
-
child.once("close", (code) => {
|
|
1730
|
-
if (code === 0 && sessionId)
|
|
1731
|
-
resolve({ output: output.slice(-20_000), finalMessage, sessionId });
|
|
1732
|
-
else if (code === 0)
|
|
1733
|
-
reject(
|
|
1734
|
-
new Error(
|
|
1735
|
-
"Agent completed without announcing a resumable session id.",
|
|
1736
|
-
),
|
|
1737
|
-
);
|
|
1738
|
-
else reject(new Error(codexFailureMessage(output, code)));
|
|
1739
|
-
});
|
|
1740
|
-
});
|
|
1741
|
-
return { child, completed };
|
|
1742
|
-
}
|
|
1743
|
-
|
|
1744
|
-
export function codexExecutionArgs({
|
|
1745
|
-
workspace,
|
|
1746
|
-
sandbox,
|
|
1747
|
-
profile = {},
|
|
1748
|
-
previousSessionId,
|
|
1749
|
-
skipGitRepoCheck = false,
|
|
1750
|
-
mcpServer,
|
|
1751
|
-
}) {
|
|
1752
|
-
const optionArgs = [
|
|
1753
|
-
"--json",
|
|
1754
|
-
"--config",
|
|
1755
|
-
codexConfigArgument("skills.include_instructions=false"),
|
|
1756
|
-
];
|
|
1757
|
-
if (skipGitRepoCheck) optionArgs.push("--skip-git-repo-check");
|
|
1758
|
-
if (profile.model) optionArgs.push("--model", profile.model);
|
|
1759
|
-
if (profile.reasoning_effort) {
|
|
1760
|
-
optionArgs.push(
|
|
1761
|
-
"--config",
|
|
1762
|
-
codexConfigArgument(
|
|
1763
|
-
`model_reasoning_effort=${tomlLiteralString(profile.reasoning_effort)}`,
|
|
1764
|
-
),
|
|
1765
|
-
);
|
|
1766
|
-
}
|
|
1767
|
-
if (mcpServer) {
|
|
1768
|
-
const mcpArgs = [
|
|
1769
|
-
mcpServer.connectorBin,
|
|
1770
|
-
"mcp",
|
|
1771
|
-
"--workspace",
|
|
1772
|
-
mcpServer.workspace,
|
|
1773
|
-
"--run-id",
|
|
1774
|
-
mcpServer.runId,
|
|
1775
|
-
];
|
|
1776
|
-
optionArgs.push(
|
|
1777
|
-
"--config",
|
|
1778
|
-
codexConfigArgument(
|
|
1779
|
-
`mcp_servers.engineeros.command=${tomlLiteralString(process.execPath)}`,
|
|
1780
|
-
),
|
|
1781
|
-
"--config",
|
|
1782
|
-
codexConfigArgument(
|
|
1783
|
-
`mcp_servers.engineeros.args=[${mcpArgs.map(tomlLiteralString).join(",")}]`,
|
|
1784
|
-
),
|
|
1785
|
-
);
|
|
1786
|
-
}
|
|
1787
|
-
return previousSessionId
|
|
1788
|
-
? ["exec", "resume", ...optionArgs, previousSessionId, "-"]
|
|
1789
|
-
: ["exec", ...optionArgs, "--sandbox", sandbox, "-C", workspace, "-"];
|
|
1790
|
-
}
|
|
1791
|
-
|
|
1792
|
-
export function sanitizeAgentResponse(value) {
|
|
1793
|
-
return String(value || "").replace(
|
|
1794
|
-
/^Warning: Exceeded skills context budget(?: of \d+%)?\. All skill descriptions were removed and \d+ additional skills? (?:was|were) not included in the model-visible skills list\.\s*/i,
|
|
1795
|
-
"",
|
|
1796
|
-
);
|
|
1797
|
-
}
|
|
1798
|
-
|
|
1799
|
-
function tomlLiteralString(value) {
|
|
1800
|
-
const text = String(value);
|
|
1801
|
-
if (text.includes("'''")) {
|
|
1802
|
-
throw new Error(
|
|
1803
|
-
"Codex configuration values cannot contain three consecutive apostrophes.",
|
|
1804
|
-
);
|
|
1805
|
-
}
|
|
1806
|
-
return `'''${text}'''`;
|
|
1807
|
-
}
|
|
1808
|
-
|
|
1809
|
-
function codexConfigArgument(override) {
|
|
1810
|
-
return process.platform === "win32" ? `"${override}"` : override;
|
|
1811
|
-
}
|
|
1812
|
-
|
|
1813
|
-
function launchAgentProcess(
|
|
1814
|
-
workspace,
|
|
1815
|
-
prompt,
|
|
1816
|
-
sandbox,
|
|
1817
|
-
config,
|
|
1818
|
-
callbacks,
|
|
1819
|
-
profile = {},
|
|
1820
|
-
previousSessionId,
|
|
1821
|
-
mcpContext,
|
|
1822
|
-
) {
|
|
1823
|
-
if (config.agent_protocol === "acp") {
|
|
1824
|
-
return launchAcpAgent(workspace, prompt, config, callbacks, {
|
|
1825
|
-
persistent: mcpContext?.persistentAcp === true,
|
|
1826
|
-
sessionKey: mcpContext?.sessionKey,
|
|
1827
|
-
previousSessionId,
|
|
1828
|
-
sandbox,
|
|
1829
|
-
profile,
|
|
1830
|
-
});
|
|
1831
|
-
}
|
|
1832
|
-
return launchCodexProcess(
|
|
1833
|
-
workspace,
|
|
1834
|
-
prompt,
|
|
1835
|
-
sandbox,
|
|
1836
|
-
callbacks,
|
|
1837
|
-
profile,
|
|
1838
|
-
previousSessionId,
|
|
1839
|
-
config.skip_git_repo_check === true,
|
|
1840
|
-
mcpContext?.runId
|
|
1841
|
-
? {
|
|
1842
|
-
connectorBin: process.argv[1],
|
|
1843
|
-
workspace: config.workspace,
|
|
1844
|
-
runId: mcpContext.runId,
|
|
1845
|
-
}
|
|
1846
|
-
: undefined,
|
|
1847
|
-
);
|
|
1848
|
-
}
|
|
1849
|
-
|
|
1850
|
-
function runCodexCommand(command, args, cwd) {
|
|
1851
|
-
return new Promise((resolve, reject) => {
|
|
1852
|
-
const child = spawn(command, args, {
|
|
1853
|
-
cwd,
|
|
1854
|
-
env: process.env,
|
|
1855
|
-
shell: process.platform === "win32",
|
|
1856
|
-
windowsHide: true,
|
|
1857
|
-
});
|
|
1858
|
-
let stdout = "";
|
|
1859
|
-
let stderr = "";
|
|
1860
|
-
child.stdout.setEncoding("utf8");
|
|
1861
|
-
child.stderr.setEncoding("utf8");
|
|
1862
|
-
child.stdout.on("data", (chunk) => (stdout += chunk));
|
|
1863
|
-
child.stderr.on("data", (chunk) => (stderr += chunk));
|
|
1864
|
-
child.once("error", reject);
|
|
1865
|
-
child.once("close", (code) => resolve({ code: code ?? 1, stdout, stderr }));
|
|
1866
|
-
});
|
|
1867
|
-
}
|
|
1868
|
-
|
|
1869
|
-
async function changedFilePaths(workspace) {
|
|
1870
|
-
const tracked = await run(
|
|
1871
|
-
"git",
|
|
1872
|
-
["diff", "--name-only", "-z", "HEAD", "--"],
|
|
1873
|
-
workspace,
|
|
1874
|
-
);
|
|
1875
|
-
const untracked = await run(
|
|
1876
|
-
"git",
|
|
1877
|
-
["ls-files", "-z", "--others", "--exclude-standard"],
|
|
1878
|
-
workspace,
|
|
1879
|
-
);
|
|
1880
|
-
return [
|
|
1881
|
-
...new Set([
|
|
1882
|
-
...gitPathList(tracked.stdout),
|
|
1883
|
-
...gitPathList(untracked.stdout),
|
|
1884
|
-
]),
|
|
1885
|
-
].sort();
|
|
1886
|
-
}
|
|
1887
|
-
|
|
1888
|
-
async function boundedDiff(workspace, changedFiles) {
|
|
1889
|
-
const tracked = await run(
|
|
1890
|
-
"git",
|
|
1891
|
-
["diff", "--binary", "HEAD", "--"],
|
|
1892
|
-
workspace,
|
|
1893
|
-
);
|
|
1894
|
-
const untracked = new Set(
|
|
1895
|
-
gitPathList(
|
|
1896
|
-
(
|
|
1897
|
-
await run(
|
|
1898
|
-
"git",
|
|
1899
|
-
["ls-files", "-z", "--others", "--exclude-standard"],
|
|
1900
|
-
workspace,
|
|
1901
|
-
)
|
|
1902
|
-
).stdout,
|
|
1903
|
-
),
|
|
1904
|
-
);
|
|
1905
|
-
const parts = [tracked.stdout];
|
|
1906
|
-
for (const relative of changedFiles.filter((item) => untracked.has(item))) {
|
|
1907
|
-
const generated = await run(
|
|
1908
|
-
"git",
|
|
1909
|
-
["diff", "--no-index", "--binary", "--", "/dev/null", relative],
|
|
1910
|
-
workspace,
|
|
1911
|
-
{
|
|
1912
|
-
allowFailure: true,
|
|
1913
|
-
},
|
|
1914
|
-
);
|
|
1915
|
-
parts.push(generated.stdout);
|
|
1916
|
-
}
|
|
1917
|
-
const diff = parts.filter(Boolean).join("\n");
|
|
1918
|
-
if (!diff.trim()) throw new Error("The run produced no bounded diff.");
|
|
1919
|
-
return diff;
|
|
1920
|
-
}
|
|
1921
|
-
|
|
1922
|
-
export async function repositoryArchive(workspace) {
|
|
1923
|
-
const listed = await run(
|
|
1924
|
-
"git",
|
|
1925
|
-
["ls-files", "-z", "-co", "--exclude-standard"],
|
|
1926
|
-
workspace,
|
|
1927
|
-
);
|
|
1928
|
-
const files = [...new Set(gitPathList(listed.stdout))].sort();
|
|
1929
|
-
return createZip(
|
|
1930
|
-
await Promise.all(
|
|
1931
|
-
files.map(async (relative) => ({
|
|
1932
|
-
name: relative,
|
|
1933
|
-
data: await readFile(path.join(workspace, relative)),
|
|
1934
|
-
})),
|
|
1935
|
-
),
|
|
1936
|
-
);
|
|
1937
|
-
}
|
|
1938
|
-
|
|
1939
|
-
async function workspaceFiles(root, current = root) {
|
|
1940
|
-
const files = [];
|
|
1941
|
-
for (const entry of await readdir(current, { withFileTypes: true })) {
|
|
1942
|
-
const absolute = path.join(current, entry.name);
|
|
1943
|
-
const relative = normalizePath(path.relative(root, absolute));
|
|
1944
|
-
if (entry.isSymbolicLink()) continue;
|
|
1945
|
-
if (entry.isDirectory()) {
|
|
1946
|
-
files.push(...(await workspaceFiles(root, absolute)));
|
|
1947
|
-
} else if (entry.isFile()) {
|
|
1948
|
-
files.push(relative);
|
|
1949
|
-
}
|
|
1950
|
-
}
|
|
1951
|
-
return files;
|
|
1952
|
-
}
|
|
1953
|
-
|
|
1954
|
-
function isShareablePath(relative) {
|
|
1955
|
-
const normalized = normalizePath(relative);
|
|
1956
|
-
if (
|
|
1957
|
-
!normalized ||
|
|
1958
|
-
normalized === ".." ||
|
|
1959
|
-
normalized.startsWith("../") ||
|
|
1960
|
-
path.isAbsolute(normalized)
|
|
1961
|
-
) {
|
|
1962
|
-
return false;
|
|
1963
|
-
}
|
|
1964
|
-
const parts = normalized.split("/");
|
|
1965
|
-
if (parts.some((part) => EXCLUDED_DIRECTORIES.has(part.toLowerCase()))) {
|
|
1966
|
-
return false;
|
|
1967
|
-
}
|
|
1968
|
-
if (
|
|
1969
|
-
EXCLUDED_PATH_PREFIXES.some((prefix) =>
|
|
1970
|
-
normalized.toLowerCase().startsWith(prefix),
|
|
1971
|
-
)
|
|
1972
|
-
) {
|
|
1973
|
-
return false;
|
|
1974
|
-
}
|
|
1975
|
-
const name = parts.at(-1)?.toLowerCase() ?? "";
|
|
1976
|
-
if (EXCLUDED_FILE_EXTENSIONS.has(path.posix.extname(name))) return false;
|
|
1977
|
-
if (name === ".env.example" || name === ".env.sample") return true;
|
|
1978
|
-
if (
|
|
1979
|
-
name === ".env" ||
|
|
1980
|
-
name.startsWith(".env.") ||
|
|
1981
|
-
[".npmrc", ".netrc", "credentials", "credentials.json"].includes(name) ||
|
|
1982
|
-
/\.(?:key|pem|p12|pfx)$/i.test(name)
|
|
1983
|
-
) {
|
|
1984
|
-
return false;
|
|
1985
|
-
}
|
|
1986
|
-
return true;
|
|
1987
|
-
}
|
|
1988
|
-
|
|
1989
|
-
function isCodeBearing(relative) {
|
|
1990
|
-
const name = path.posix.basename(relative).toLowerCase();
|
|
1991
|
-
return (
|
|
1992
|
-
CODE_MARKERS.has(name) || CODE_EXTENSIONS.has(path.posix.extname(name))
|
|
1993
|
-
);
|
|
1994
|
-
}
|
|
1995
|
-
|
|
1996
|
-
async function worktreeBase(workspace, requested) {
|
|
1997
|
-
if (!requested || requested.startsWith("greenfield:")) return "HEAD";
|
|
1998
|
-
const exists = await run(
|
|
1999
|
-
"git",
|
|
2000
|
-
["cat-file", "-e", `${requested}^{commit}`],
|
|
2001
|
-
workspace,
|
|
2002
|
-
{ allowFailure: true },
|
|
2003
|
-
);
|
|
2004
|
-
if (exists.code !== 0) {
|
|
2005
|
-
throw new Error(
|
|
2006
|
-
`The frozen base revision ${requested} is not available in this repository.`,
|
|
2007
|
-
);
|
|
2008
|
-
}
|
|
2009
|
-
return requested;
|
|
2010
|
-
}
|
|
2011
|
-
|
|
2012
|
-
export async function createZip(files) {
|
|
2013
|
-
const localParts = [];
|
|
2014
|
-
const centralParts = [];
|
|
2015
|
-
let offset = 0;
|
|
2016
|
-
let total = 0;
|
|
2017
|
-
for (const file of files) {
|
|
2018
|
-
const name = Buffer.from(normalizePath(file.name), "utf8");
|
|
2019
|
-
const data = Buffer.from(file.data);
|
|
2020
|
-
total += data.length;
|
|
2021
|
-
if (total > MAX_ARCHIVE_BYTES)
|
|
2022
|
-
throw new Error("Repository archive exceeds the 25 MB connector limit.");
|
|
2023
|
-
const compressed = await deflate(data);
|
|
2024
|
-
const crc = crc32(data);
|
|
2025
|
-
const local = Buffer.alloc(30);
|
|
2026
|
-
local.writeUInt32LE(0x04034b50, 0);
|
|
2027
|
-
local.writeUInt16LE(20, 4);
|
|
2028
|
-
local.writeUInt16LE(0x0800, 6);
|
|
2029
|
-
local.writeUInt16LE(8, 8);
|
|
2030
|
-
local.writeUInt32LE(crc, 14);
|
|
2031
|
-
local.writeUInt32LE(compressed.length, 18);
|
|
2032
|
-
local.writeUInt32LE(data.length, 22);
|
|
2033
|
-
local.writeUInt16LE(name.length, 26);
|
|
2034
|
-
localParts.push(local, name, compressed);
|
|
2035
|
-
|
|
2036
|
-
const central = Buffer.alloc(46);
|
|
2037
|
-
central.writeUInt32LE(0x02014b50, 0);
|
|
2038
|
-
central.writeUInt16LE(20, 4);
|
|
2039
|
-
central.writeUInt16LE(20, 6);
|
|
2040
|
-
central.writeUInt16LE(0x0800, 8);
|
|
2041
|
-
central.writeUInt16LE(8, 10);
|
|
2042
|
-
central.writeUInt32LE(crc, 16);
|
|
2043
|
-
central.writeUInt32LE(compressed.length, 20);
|
|
2044
|
-
central.writeUInt32LE(data.length, 24);
|
|
2045
|
-
central.writeUInt16LE(name.length, 28);
|
|
2046
|
-
central.writeUInt32LE(offset, 42);
|
|
2047
|
-
centralParts.push(central, name);
|
|
2048
|
-
offset += local.length + name.length + compressed.length;
|
|
2049
|
-
}
|
|
2050
|
-
const central = Buffer.concat(centralParts);
|
|
2051
|
-
const end = Buffer.alloc(22);
|
|
2052
|
-
end.writeUInt32LE(0x06054b50, 0);
|
|
2053
|
-
end.writeUInt16LE(files.length, 8);
|
|
2054
|
-
end.writeUInt16LE(files.length, 10);
|
|
2055
|
-
end.writeUInt32LE(central.length, 12);
|
|
2056
|
-
end.writeUInt32LE(offset, 16);
|
|
2057
|
-
return Buffer.concat([...localParts, central, end]);
|
|
2058
|
-
}
|
|
2059
|
-
|
|
2060
|
-
async function copyWorkspace(source, target) {
|
|
2061
|
-
for (const entry of await readdir(source, { withFileTypes: true })) {
|
|
2062
|
-
if ([".git", ".engineeros", "node_modules"].includes(entry.name)) continue;
|
|
2063
|
-
const from = path.join(source, entry.name);
|
|
2064
|
-
const to = path.join(target, entry.name);
|
|
2065
|
-
if (entry.isDirectory()) {
|
|
2066
|
-
await mkdir(to, { recursive: true });
|
|
2067
|
-
await copyWorkspace(from, to);
|
|
2068
|
-
} else if (entry.isFile()) {
|
|
2069
|
-
await writeFile(to, await readFile(from));
|
|
2070
|
-
}
|
|
2071
|
-
}
|
|
2072
|
-
}
|
|
2073
|
-
|
|
2074
|
-
function normalizePath(value) {
|
|
2075
|
-
return String(value ?? "")
|
|
2076
|
-
.trim()
|
|
2077
|
-
.replaceAll("\\", "/")
|
|
2078
|
-
.replace(/^\.\//, "");
|
|
2079
|
-
}
|
|
2080
|
-
|
|
2081
|
-
function gitPathList(value) {
|
|
2082
|
-
return String(value ?? "")
|
|
2083
|
-
.split("\0")
|
|
2084
|
-
.map(normalizePath)
|
|
2085
|
-
.filter(Boolean);
|
|
2086
|
-
}
|
|
2087
|
-
|
|
2088
|
-
function run(command, args, cwd, options = {}) {
|
|
2089
|
-
return new Promise((resolve, reject) => {
|
|
2090
|
-
const child = spawn(command, args, {
|
|
2091
|
-
cwd,
|
|
2092
|
-
shell: false,
|
|
2093
|
-
windowsHide: true,
|
|
2094
|
-
});
|
|
2095
|
-
let stdout = "";
|
|
2096
|
-
let stderr = "";
|
|
2097
|
-
child.stdout.setEncoding("utf8");
|
|
2098
|
-
child.stderr.setEncoding("utf8");
|
|
2099
|
-
child.stdout.on("data", (chunk) => (stdout += chunk));
|
|
2100
|
-
child.stderr.on("data", (chunk) => (stderr += chunk));
|
|
2101
|
-
child.once("error", reject);
|
|
1703
|
+
if (
|
|
1704
|
+
event.type === "item.completed" &&
|
|
1705
|
+
event.item?.type === "agent_message" &&
|
|
1706
|
+
typeof event.item.text === "string"
|
|
1707
|
+
) {
|
|
1708
|
+
finalMessage = event.item.text;
|
|
1709
|
+
}
|
|
1710
|
+
callbacks.onEvent?.(event);
|
|
1711
|
+
} catch {
|
|
1712
|
+
callbacks.onEvent?.({
|
|
1713
|
+
type: "agent.output",
|
|
1714
|
+
message: line.slice(0, 500),
|
|
1715
|
+
});
|
|
1716
|
+
}
|
|
1717
|
+
}
|
|
1718
|
+
});
|
|
1719
|
+
child.stderr.setEncoding("utf8");
|
|
1720
|
+
child.stderr.on("data", (chunk) => {
|
|
1721
|
+
output += chunk;
|
|
1722
|
+
callbacks.onEvent?.({
|
|
1723
|
+
type: "agent.stderr",
|
|
1724
|
+
message: chunk.trim().slice(0, 500),
|
|
1725
|
+
});
|
|
1726
|
+
});
|
|
1727
|
+
const completed = new Promise((resolve, reject) => {
|
|
1728
|
+
child.once("error", reject);
|
|
2102
1729
|
child.once("close", (code) => {
|
|
2103
|
-
|
|
2104
|
-
|
|
2105
|
-
|
|
2106
|
-
|
|
2107
|
-
|
|
2108
|
-
|
|
1730
|
+
void sessionCheckpoint.then(() => {
|
|
1731
|
+
if (sessionCheckpointError) reject(sessionCheckpointError);
|
|
1732
|
+
else if (code === 0 && sessionId)
|
|
1733
|
+
resolve({ output: output.slice(-20_000), finalMessage, sessionId });
|
|
1734
|
+
else if (code === 0)
|
|
1735
|
+
reject(
|
|
1736
|
+
new Error(
|
|
1737
|
+
"Agent completed without announcing a resumable session id.",
|
|
1738
|
+
),
|
|
1739
|
+
);
|
|
1740
|
+
else reject(new Error(codexFailureMessage(output, code)));
|
|
1741
|
+
});
|
|
2109
1742
|
});
|
|
2110
|
-
});
|
|
2111
|
-
}
|
|
2112
|
-
|
|
2113
|
-
|
|
2114
|
-
|
|
2115
|
-
|
|
2116
|
-
|
|
2117
|
-
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
|
|
2121
|
-
}
|
|
1743
|
+
});
|
|
1744
|
+
return { child, completed };
|
|
1745
|
+
}
|
|
1746
|
+
|
|
1747
|
+
export function codexExecutionArgs({
|
|
1748
|
+
workspace,
|
|
1749
|
+
sandbox,
|
|
1750
|
+
profile = {},
|
|
1751
|
+
previousSessionId,
|
|
1752
|
+
skipGitRepoCheck = false,
|
|
1753
|
+
mcpServer,
|
|
1754
|
+
}) {
|
|
1755
|
+
const optionArgs = [
|
|
1756
|
+
"--json",
|
|
1757
|
+
"--config",
|
|
1758
|
+
codexConfigArgument("skills.include_instructions=false"),
|
|
1759
|
+
];
|
|
1760
|
+
if (skipGitRepoCheck) optionArgs.push("--skip-git-repo-check");
|
|
1761
|
+
if (profile.model) optionArgs.push("--model", profile.model);
|
|
1762
|
+
if (profile.reasoning_effort) {
|
|
1763
|
+
optionArgs.push(
|
|
1764
|
+
"--config",
|
|
1765
|
+
codexConfigArgument(
|
|
1766
|
+
`model_reasoning_effort=${tomlLiteralString(profile.reasoning_effort)}`,
|
|
1767
|
+
),
|
|
1768
|
+
);
|
|
1769
|
+
}
|
|
1770
|
+
if (mcpServer) {
|
|
1771
|
+
const mcpArgs = [
|
|
1772
|
+
mcpServer.connectorBin,
|
|
1773
|
+
"mcp",
|
|
1774
|
+
"--workspace",
|
|
1775
|
+
mcpServer.workspace,
|
|
1776
|
+
"--run-id",
|
|
1777
|
+
mcpServer.runId,
|
|
1778
|
+
];
|
|
1779
|
+
optionArgs.push(
|
|
1780
|
+
"--config",
|
|
1781
|
+
codexConfigArgument(
|
|
1782
|
+
`mcp_servers.engineeros.command=${tomlLiteralString(process.execPath)}`,
|
|
1783
|
+
),
|
|
1784
|
+
"--config",
|
|
1785
|
+
codexConfigArgument(
|
|
1786
|
+
`mcp_servers.engineeros.args=[${mcpArgs.map(tomlLiteralString).join(",")}]`,
|
|
1787
|
+
),
|
|
1788
|
+
);
|
|
1789
|
+
}
|
|
1790
|
+
return previousSessionId
|
|
1791
|
+
? ["exec", "resume", ...optionArgs, previousSessionId, "-"]
|
|
1792
|
+
: ["exec", ...optionArgs, "--sandbox", sandbox, "-C", workspace, "-"];
|
|
1793
|
+
}
|
|
1794
|
+
|
|
1795
|
+
export function sanitizeAgentResponse(value) {
|
|
1796
|
+
return String(value || "").replace(
|
|
1797
|
+
/^Warning: Exceeded skills context budget(?: of \d+%)?\. All skill descriptions were removed and \d+ additional skills? (?:was|were) not included in the model-visible skills list\.\s*/i,
|
|
1798
|
+
"",
|
|
1799
|
+
);
|
|
1800
|
+
}
|
|
1801
|
+
|
|
1802
|
+
function tomlLiteralString(value) {
|
|
1803
|
+
const text = String(value);
|
|
1804
|
+
if (text.includes("'''")) {
|
|
1805
|
+
throw new Error(
|
|
1806
|
+
"Codex configuration values cannot contain three consecutive apostrophes.",
|
|
1807
|
+
);
|
|
1808
|
+
}
|
|
1809
|
+
return `'''${text}'''`;
|
|
1810
|
+
}
|
|
1811
|
+
|
|
1812
|
+
function codexConfigArgument(override) {
|
|
1813
|
+
return process.platform === "win32" ? `"${override}"` : override;
|
|
1814
|
+
}
|
|
1815
|
+
|
|
1816
|
+
function launchAgentProcess(
|
|
1817
|
+
workspace,
|
|
1818
|
+
prompt,
|
|
1819
|
+
sandbox,
|
|
1820
|
+
config,
|
|
1821
|
+
callbacks,
|
|
1822
|
+
profile = {},
|
|
1823
|
+
previousSessionId,
|
|
1824
|
+
mcpContext,
|
|
1825
|
+
) {
|
|
1826
|
+
if (config.agent_protocol === "acp") {
|
|
1827
|
+
return launchAcpAgent(workspace, prompt, config, callbacks, {
|
|
1828
|
+
persistent: mcpContext?.persistentAcp === true,
|
|
1829
|
+
sessionKey: mcpContext?.sessionKey,
|
|
1830
|
+
previousSessionId,
|
|
1831
|
+
sandbox,
|
|
1832
|
+
profile,
|
|
1833
|
+
});
|
|
1834
|
+
}
|
|
1835
|
+
return launchCodexProcess(
|
|
1836
|
+
workspace,
|
|
1837
|
+
prompt,
|
|
1838
|
+
sandbox,
|
|
1839
|
+
callbacks,
|
|
1840
|
+
profile,
|
|
1841
|
+
previousSessionId,
|
|
1842
|
+
config.skip_git_repo_check === true,
|
|
1843
|
+
mcpContext?.runId
|
|
1844
|
+
? {
|
|
1845
|
+
connectorBin: process.argv[1],
|
|
1846
|
+
workspace: config.workspace,
|
|
1847
|
+
runId: mcpContext.runId,
|
|
1848
|
+
}
|
|
1849
|
+
: undefined,
|
|
1850
|
+
);
|
|
1851
|
+
}
|
|
1852
|
+
|
|
1853
|
+
function runCodexCommand(command, args, cwd) {
|
|
1854
|
+
return new Promise((resolve, reject) => {
|
|
1855
|
+
const child = spawn(command, args, {
|
|
1856
|
+
cwd,
|
|
1857
|
+
env: process.env,
|
|
1858
|
+
shell: process.platform === "win32",
|
|
1859
|
+
windowsHide: true,
|
|
1860
|
+
});
|
|
1861
|
+
let stdout = "";
|
|
1862
|
+
let stderr = "";
|
|
1863
|
+
child.stdout.setEncoding("utf8");
|
|
1864
|
+
child.stderr.setEncoding("utf8");
|
|
1865
|
+
child.stdout.on("data", (chunk) => (stdout += chunk));
|
|
1866
|
+
child.stderr.on("data", (chunk) => (stderr += chunk));
|
|
1867
|
+
child.once("error", reject);
|
|
1868
|
+
child.once("close", (code) => resolve({ code: code ?? 1, stdout, stderr }));
|
|
1869
|
+
});
|
|
1870
|
+
}
|
|
1871
|
+
|
|
1872
|
+
async function changedFilePaths(workspace) {
|
|
1873
|
+
const tracked = await run(
|
|
1874
|
+
"git",
|
|
1875
|
+
["diff", "--name-only", "-z", "HEAD", "--"],
|
|
1876
|
+
workspace,
|
|
1877
|
+
);
|
|
1878
|
+
const untracked = await run(
|
|
1879
|
+
"git",
|
|
1880
|
+
["ls-files", "-z", "--others", "--exclude-standard"],
|
|
1881
|
+
workspace,
|
|
1882
|
+
);
|
|
1883
|
+
return [
|
|
1884
|
+
...new Set([
|
|
1885
|
+
...gitPathList(tracked.stdout),
|
|
1886
|
+
...gitPathList(untracked.stdout),
|
|
1887
|
+
]),
|
|
1888
|
+
].sort();
|
|
1889
|
+
}
|
|
1890
|
+
|
|
1891
|
+
async function boundedDiff(workspace, changedFiles) {
|
|
1892
|
+
const tracked = await run(
|
|
1893
|
+
"git",
|
|
1894
|
+
["diff", "--binary", "HEAD", "--"],
|
|
1895
|
+
workspace,
|
|
1896
|
+
);
|
|
1897
|
+
const untracked = new Set(
|
|
1898
|
+
gitPathList(
|
|
1899
|
+
(
|
|
1900
|
+
await run(
|
|
1901
|
+
"git",
|
|
1902
|
+
["ls-files", "-z", "--others", "--exclude-standard"],
|
|
1903
|
+
workspace,
|
|
1904
|
+
)
|
|
1905
|
+
).stdout,
|
|
1906
|
+
),
|
|
1907
|
+
);
|
|
1908
|
+
const parts = [tracked.stdout];
|
|
1909
|
+
for (const relative of changedFiles.filter((item) => untracked.has(item))) {
|
|
1910
|
+
const generated = await run(
|
|
1911
|
+
"git",
|
|
1912
|
+
["diff", "--no-index", "--binary", "--", "/dev/null", relative],
|
|
1913
|
+
workspace,
|
|
1914
|
+
{
|
|
1915
|
+
allowFailure: true,
|
|
1916
|
+
},
|
|
1917
|
+
);
|
|
1918
|
+
parts.push(generated.stdout);
|
|
1919
|
+
}
|
|
1920
|
+
const diff = parts.filter(Boolean).join("\n");
|
|
1921
|
+
if (!diff.trim()) throw new Error("The run produced no bounded diff.");
|
|
1922
|
+
return diff;
|
|
1923
|
+
}
|
|
1924
|
+
|
|
1925
|
+
export async function repositoryArchive(workspace) {
|
|
1926
|
+
const listed = await run(
|
|
1927
|
+
"git",
|
|
1928
|
+
["ls-files", "-z", "-co", "--exclude-standard"],
|
|
1929
|
+
workspace,
|
|
1930
|
+
);
|
|
1931
|
+
const files = [...new Set(gitPathList(listed.stdout))].sort();
|
|
1932
|
+
return createZip(
|
|
1933
|
+
await Promise.all(
|
|
1934
|
+
files.map(async (relative) => ({
|
|
1935
|
+
name: relative,
|
|
1936
|
+
data: await readFile(path.join(workspace, relative)),
|
|
1937
|
+
})),
|
|
1938
|
+
),
|
|
1939
|
+
);
|
|
1940
|
+
}
|
|
1941
|
+
|
|
1942
|
+
async function workspaceFiles(root, current = root) {
|
|
1943
|
+
const files = [];
|
|
1944
|
+
for (const entry of await readdir(current, { withFileTypes: true })) {
|
|
1945
|
+
const absolute = path.join(current, entry.name);
|
|
1946
|
+
const relative = normalizePath(path.relative(root, absolute));
|
|
1947
|
+
if (entry.isSymbolicLink()) continue;
|
|
1948
|
+
if (entry.isDirectory()) {
|
|
1949
|
+
files.push(...(await workspaceFiles(root, absolute)));
|
|
1950
|
+
} else if (entry.isFile()) {
|
|
1951
|
+
files.push(relative);
|
|
1952
|
+
}
|
|
1953
|
+
}
|
|
1954
|
+
return files;
|
|
1955
|
+
}
|
|
1956
|
+
|
|
1957
|
+
function isShareablePath(relative) {
|
|
1958
|
+
const normalized = normalizePath(relative);
|
|
1959
|
+
if (
|
|
1960
|
+
!normalized ||
|
|
1961
|
+
normalized === ".." ||
|
|
1962
|
+
normalized.startsWith("../") ||
|
|
1963
|
+
path.isAbsolute(normalized)
|
|
1964
|
+
) {
|
|
1965
|
+
return false;
|
|
1966
|
+
}
|
|
1967
|
+
const parts = normalized.split("/");
|
|
1968
|
+
if (parts.some((part) => EXCLUDED_DIRECTORIES.has(part.toLowerCase()))) {
|
|
1969
|
+
return false;
|
|
1970
|
+
}
|
|
1971
|
+
if (
|
|
1972
|
+
EXCLUDED_PATH_PREFIXES.some((prefix) =>
|
|
1973
|
+
normalized.toLowerCase().startsWith(prefix),
|
|
1974
|
+
)
|
|
1975
|
+
) {
|
|
1976
|
+
return false;
|
|
1977
|
+
}
|
|
1978
|
+
const name = parts.at(-1)?.toLowerCase() ?? "";
|
|
1979
|
+
if (EXCLUDED_FILE_EXTENSIONS.has(path.posix.extname(name))) return false;
|
|
1980
|
+
if (name === ".env.example" || name === ".env.sample") return true;
|
|
1981
|
+
if (
|
|
1982
|
+
name === ".env" ||
|
|
1983
|
+
name.startsWith(".env.") ||
|
|
1984
|
+
[".npmrc", ".netrc", "credentials", "credentials.json"].includes(name) ||
|
|
1985
|
+
/\.(?:key|pem|p12|pfx)$/i.test(name)
|
|
1986
|
+
) {
|
|
1987
|
+
return false;
|
|
1988
|
+
}
|
|
1989
|
+
return true;
|
|
1990
|
+
}
|
|
1991
|
+
|
|
1992
|
+
function isCodeBearing(relative) {
|
|
1993
|
+
const name = path.posix.basename(relative).toLowerCase();
|
|
1994
|
+
return (
|
|
1995
|
+
CODE_MARKERS.has(name) || CODE_EXTENSIONS.has(path.posix.extname(name))
|
|
1996
|
+
);
|
|
1997
|
+
}
|
|
1998
|
+
|
|
1999
|
+
async function worktreeBase(workspace, requested) {
|
|
2000
|
+
if (!requested || requested.startsWith("greenfield:")) return "HEAD";
|
|
2001
|
+
const exists = await run(
|
|
2002
|
+
"git",
|
|
2003
|
+
["cat-file", "-e", `${requested}^{commit}`],
|
|
2004
|
+
workspace,
|
|
2005
|
+
{ allowFailure: true },
|
|
2006
|
+
);
|
|
2007
|
+
if (exists.code !== 0) {
|
|
2008
|
+
throw new Error(
|
|
2009
|
+
`The frozen base revision ${requested} is not available in this repository.`,
|
|
2010
|
+
);
|
|
2011
|
+
}
|
|
2012
|
+
return requested;
|
|
2013
|
+
}
|
|
2014
|
+
|
|
2015
|
+
export async function createZip(files) {
|
|
2016
|
+
const localParts = [];
|
|
2017
|
+
const centralParts = [];
|
|
2018
|
+
let offset = 0;
|
|
2019
|
+
let total = 0;
|
|
2020
|
+
for (const file of files) {
|
|
2021
|
+
const name = Buffer.from(normalizePath(file.name), "utf8");
|
|
2022
|
+
const data = Buffer.from(file.data);
|
|
2023
|
+
total += data.length;
|
|
2024
|
+
if (total > MAX_ARCHIVE_BYTES)
|
|
2025
|
+
throw new Error("Repository archive exceeds the 25 MB connector limit.");
|
|
2026
|
+
const compressed = await deflate(data);
|
|
2027
|
+
const crc = crc32(data);
|
|
2028
|
+
const local = Buffer.alloc(30);
|
|
2029
|
+
local.writeUInt32LE(0x04034b50, 0);
|
|
2030
|
+
local.writeUInt16LE(20, 4);
|
|
2031
|
+
local.writeUInt16LE(0x0800, 6);
|
|
2032
|
+
local.writeUInt16LE(8, 8);
|
|
2033
|
+
local.writeUInt32LE(crc, 14);
|
|
2034
|
+
local.writeUInt32LE(compressed.length, 18);
|
|
2035
|
+
local.writeUInt32LE(data.length, 22);
|
|
2036
|
+
local.writeUInt16LE(name.length, 26);
|
|
2037
|
+
localParts.push(local, name, compressed);
|
|
2038
|
+
|
|
2039
|
+
const central = Buffer.alloc(46);
|
|
2040
|
+
central.writeUInt32LE(0x02014b50, 0);
|
|
2041
|
+
central.writeUInt16LE(20, 4);
|
|
2042
|
+
central.writeUInt16LE(20, 6);
|
|
2043
|
+
central.writeUInt16LE(0x0800, 8);
|
|
2044
|
+
central.writeUInt16LE(8, 10);
|
|
2045
|
+
central.writeUInt32LE(crc, 16);
|
|
2046
|
+
central.writeUInt32LE(compressed.length, 20);
|
|
2047
|
+
central.writeUInt32LE(data.length, 24);
|
|
2048
|
+
central.writeUInt16LE(name.length, 28);
|
|
2049
|
+
central.writeUInt32LE(offset, 42);
|
|
2050
|
+
centralParts.push(central, name);
|
|
2051
|
+
offset += local.length + name.length + compressed.length;
|
|
2052
|
+
}
|
|
2053
|
+
const central = Buffer.concat(centralParts);
|
|
2054
|
+
const end = Buffer.alloc(22);
|
|
2055
|
+
end.writeUInt32LE(0x06054b50, 0);
|
|
2056
|
+
end.writeUInt16LE(files.length, 8);
|
|
2057
|
+
end.writeUInt16LE(files.length, 10);
|
|
2058
|
+
end.writeUInt32LE(central.length, 12);
|
|
2059
|
+
end.writeUInt32LE(offset, 16);
|
|
2060
|
+
return Buffer.concat([...localParts, central, end]);
|
|
2061
|
+
}
|
|
2062
|
+
|
|
2063
|
+
async function copyWorkspace(source, target) {
|
|
2064
|
+
for (const entry of await readdir(source, { withFileTypes: true })) {
|
|
2065
|
+
if ([".git", ".engineeros", "node_modules"].includes(entry.name)) continue;
|
|
2066
|
+
const from = path.join(source, entry.name);
|
|
2067
|
+
const to = path.join(target, entry.name);
|
|
2068
|
+
if (entry.isDirectory()) {
|
|
2069
|
+
await mkdir(to, { recursive: true });
|
|
2070
|
+
await copyWorkspace(from, to);
|
|
2071
|
+
} else if (entry.isFile()) {
|
|
2072
|
+
await writeFile(to, await readFile(from));
|
|
2073
|
+
}
|
|
2074
|
+
}
|
|
2075
|
+
}
|
|
2076
|
+
|
|
2077
|
+
function normalizePath(value) {
|
|
2078
|
+
return String(value ?? "")
|
|
2079
|
+
.trim()
|
|
2080
|
+
.replaceAll("\\", "/")
|
|
2081
|
+
.replace(/^\.\//, "");
|
|
2082
|
+
}
|
|
2083
|
+
|
|
2084
|
+
function gitPathList(value) {
|
|
2085
|
+
return String(value ?? "")
|
|
2086
|
+
.split("\0")
|
|
2087
|
+
.map(normalizePath)
|
|
2088
|
+
.filter(Boolean);
|
|
2089
|
+
}
|
|
2090
|
+
|
|
2091
|
+
function run(command, args, cwd, options = {}) {
|
|
2092
|
+
return new Promise((resolve, reject) => {
|
|
2093
|
+
const child = spawn(command, args, {
|
|
2094
|
+
cwd,
|
|
2095
|
+
shell: false,
|
|
2096
|
+
windowsHide: true,
|
|
2097
|
+
});
|
|
2098
|
+
let stdout = "";
|
|
2099
|
+
let stderr = "";
|
|
2100
|
+
child.stdout.setEncoding("utf8");
|
|
2101
|
+
child.stderr.setEncoding("utf8");
|
|
2102
|
+
child.stdout.on("data", (chunk) => (stdout += chunk));
|
|
2103
|
+
child.stderr.on("data", (chunk) => (stderr += chunk));
|
|
2104
|
+
child.once("error", reject);
|
|
2105
|
+
child.once("close", (code) => {
|
|
2106
|
+
const result = { code: code ?? 1, stdout, stderr };
|
|
2107
|
+
if (code === 0 || options.allowFailure) resolve(result);
|
|
2108
|
+
else
|
|
2109
|
+
reject(
|
|
2110
|
+
new Error(`${command} ${args.join(" ")} failed: ${stderr || stdout}`),
|
|
2111
|
+
);
|
|
2112
|
+
});
|
|
2113
|
+
});
|
|
2114
|
+
}
|
|
2115
|
+
|
|
2116
|
+
function crc32(buffer) {
|
|
2117
|
+
let crc = 0xffffffff;
|
|
2118
|
+
for (const byte of buffer) {
|
|
2119
|
+
crc ^= byte;
|
|
2120
|
+
for (let bit = 0; bit < 8; bit += 1)
|
|
2121
|
+
crc = (crc >>> 1) ^ (0xedb88320 & -(crc & 1));
|
|
2122
|
+
}
|
|
2123
|
+
return (crc ^ 0xffffffff) >>> 0;
|
|
2124
|
+
}
|