@open-agent-toolkit/cli 0.1.69 → 0.1.73
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 +1 -0
- package/assets/docs/cli-utilities/configuration.md +34 -0
- package/assets/docs/cli-utilities/index.md +2 -1
- package/assets/docs/cli-utilities/project-log.md +183 -0
- package/assets/docs/cli-utilities/workflow-gates.md +126 -3
- package/assets/docs/reference/cli-reference.md +6 -1
- package/assets/docs/workflows/projects/dispatch-ceiling.md +14 -5
- package/assets/docs/workflows/projects/orchestration-model.md +21 -0
- package/assets/docs/workflows/projects/review-flavors.md +9 -0
- package/assets/public-package-versions.json +4 -4
- package/assets/skills/oat-dispatch-subagents/SKILL.md +21 -1
- package/assets/skills/oat-dispatch-subagents/references/provider-claude.md +21 -0
- package/assets/skills/oat-dispatch-subagents/references/provider-codex.md +16 -0
- package/assets/skills/oat-dispatch-subagents/references/provider-cursor.md +21 -0
- package/assets/skills/oat-project-autonomous/references/gate-inventory.md +6 -2
- package/assets/skills/oat-project-complete/SKILL.md +60 -1
- package/assets/skills/oat-project-document/references/docs/autonomy-contract.md +6 -2
- package/assets/skills/oat-project-implement/SKILL.md +18 -2
- package/assets/skills/oat-project-implement/references/docs/autonomy-contract.md +6 -2
- package/assets/skills/oat-project-plan-writing/SKILL.md +23 -33
- package/assets/skills/oat-project-pr-final/references/docs/autonomy-contract.md +6 -2
- package/assets/skills/oat-project-quick-start/references/docs/autonomy-contract.md +6 -2
- package/assets/skills/oat-project-review-provide/SKILL.md +62 -1
- package/assets/skills/oat-project-summary/SKILL.md +122 -18
- package/assets/templates/project-log.md +47 -0
- package/dist/commands/config/index.d.ts.map +1 -1
- package/dist/commands/config/index.js +106 -1
- package/dist/commands/gate/__fixtures__/fake-runtime.d.mts +3 -0
- package/dist/commands/gate/__fixtures__/fake-runtime.d.mts.map +1 -0
- package/dist/commands/gate/__fixtures__/fake-runtime.mjs +167 -0
- package/dist/commands/gate/activity-probes.d.ts +35 -0
- package/dist/commands/gate/activity-probes.d.ts.map +1 -0
- package/dist/commands/gate/activity-probes.js +135 -0
- package/dist/commands/gate/branch-local-cli.d.ts +31 -0
- package/dist/commands/gate/branch-local-cli.d.ts.map +1 -0
- package/dist/commands/gate/branch-local-cli.js +116 -0
- package/dist/commands/gate/index.d.ts +31 -0
- package/dist/commands/gate/index.d.ts.map +1 -1
- package/dist/commands/gate/index.js +533 -32
- package/dist/commands/gate/route.d.ts +22 -0
- package/dist/commands/gate/route.d.ts.map +1 -0
- package/dist/commands/gate/route.js +109 -0
- package/dist/commands/init/tools/shared/skill-manifest.d.ts +1 -1
- package/dist/commands/init/tools/shared/skill-manifest.d.ts.map +1 -1
- package/dist/commands/init/tools/shared/skill-manifest.js +1 -0
- package/dist/commands/project/dispatch-ceiling/index.d.ts.map +1 -1
- package/dist/commands/project/dispatch-ceiling/index.js +100 -16
- package/dist/commands/project/index.d.ts.map +1 -1
- package/dist/commands/project/index.js +2 -0
- package/dist/commands/project/log/append.d.ts +41 -0
- package/dist/commands/project/log/append.d.ts.map +1 -0
- package/dist/commands/project/log/append.js +241 -0
- package/dist/commands/project/log/check.d.ts +66 -0
- package/dist/commands/project/log/check.d.ts.map +1 -0
- package/dist/commands/project/log/check.js +194 -0
- package/dist/commands/project/log/grammar.d.ts +28 -0
- package/dist/commands/project/log/grammar.d.ts.map +1 -0
- package/dist/commands/project/log/grammar.js +31 -0
- package/dist/commands/project/log/index.d.ts +9 -0
- package/dist/commands/project/log/index.d.ts.map +1 -0
- package/dist/commands/project/log/index.js +13 -0
- package/dist/commands/project/log/rollup.d.ts +40 -0
- package/dist/commands/project/log/rollup.d.ts.map +1 -0
- package/dist/commands/project/log/rollup.js +203 -0
- package/dist/commands/project/log/synthesize.d.ts +23 -0
- package/dist/commands/project/log/synthesize.d.ts.map +1 -0
- package/dist/commands/project/log/synthesize.js +112 -0
- package/dist/commands/project/new/index.d.ts +1 -0
- package/dist/commands/project/new/index.d.ts.map +1 -1
- package/dist/commands/project/new/index.js +7 -0
- package/dist/commands/project/new/scaffold.d.ts +1 -0
- package/dist/commands/project/new/scaffold.d.ts.map +1 -1
- package/dist/commands/project/new/scaffold.js +24 -0
- package/dist/config/oat-config.d.ts +12 -0
- package/dist/config/oat-config.d.ts.map +1 -1
- package/dist/config/oat-config.js +30 -0
- package/dist/config/resolve.d.ts.map +1 -1
- package/dist/config/resolve.js +9 -0
- package/package.json +2 -2
|
@@ -1,11 +1,14 @@
|
|
|
1
1
|
import { spawn } from 'node:child_process';
|
|
2
2
|
import { createHash, randomUUID } from 'node:crypto';
|
|
3
|
-
import { readdir, readFile } from 'node:fs/promises';
|
|
3
|
+
import { mkdir, readdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
4
5
|
import { basename, isAbsolute, join, relative } from 'node:path';
|
|
5
6
|
import { buildCommandContext, } from '../../app/command-context.js';
|
|
7
|
+
import { appendProjectLog } from '../project/log/append.js';
|
|
6
8
|
import { getFrontmatterBlock, parseFrontmatterScalarFields, parseGeneratedTime, } from '../shared/frontmatter.js';
|
|
7
9
|
import { readGlobalOptions } from '../shared/shared.utils.js';
|
|
8
|
-
import {
|
|
10
|
+
import { parseJsonConfig } from '../../config/json.js';
|
|
11
|
+
import { BUILTIN_EXEC_TARGETS, MAX_GATE_TIMEOUT_MS, MIN_GATE_TIMEOUT_MS, isValidGateTimeoutMs, readOatConfig, readOatLocalConfig, readUserConfig, writeOatConfig, writeOatLocalConfig, writeUserConfig, } from '../../config/oat-config.js';
|
|
9
12
|
import { resolveEffectiveConfig, resolveExecTargetViews, resolveExecTargets, resolveGate, } from '../../config/resolve.js';
|
|
10
13
|
import { dirExists, fileExists } from '../../fs/io.js';
|
|
11
14
|
import { normalizeToPosixPath, resolveProjectRoot, validateRealPathWithinScope, } from '../../fs/paths.js';
|
|
@@ -15,7 +18,10 @@ import { resolveIdentityConfidence, } from '../../providers/identity/provenance.
|
|
|
15
18
|
import { parseDispatchStamps } from '../../providers/identity/stamp.js';
|
|
16
19
|
import { Command } from 'commander';
|
|
17
20
|
import YAML from 'yaml';
|
|
21
|
+
import { createGateActivityProbe, } from './activity-probes.js';
|
|
22
|
+
import { createBranchLocalGateCli, currentGateCliLaunch, readGateRouteReceipt, removeBranchLocalGateCli, } from './branch-local-cli.js';
|
|
18
23
|
import { parseReviewGateVerdict, severityDisplayName, } from './review-verdict.js';
|
|
24
|
+
import { createGateRouteCommand } from './route.js';
|
|
19
25
|
const DEFAULT_DEPENDENCIES = {
|
|
20
26
|
buildCommandContext,
|
|
21
27
|
resolveProjectRoot,
|
|
@@ -26,9 +32,17 @@ const DEFAULT_DEPENDENCIES = {
|
|
|
26
32
|
readUserConfig,
|
|
27
33
|
writeUserConfig,
|
|
28
34
|
resolveEffectiveConfig,
|
|
35
|
+
createGateActivityProbe,
|
|
36
|
+
createBranchLocalGateCli,
|
|
37
|
+
currentGateCliLaunch,
|
|
38
|
+
removeBranchLocalGateCli,
|
|
39
|
+
readGateRouteReceipt,
|
|
29
40
|
runProcess: runChildProcess,
|
|
30
41
|
parseReviewGateVerdict,
|
|
42
|
+
appendProjectLog,
|
|
31
43
|
processEnv: process.env,
|
|
44
|
+
writeGateRunMarker,
|
|
45
|
+
removeGateRunMarker,
|
|
32
46
|
writeDiagnostic: (message) => process.stderr.write(message),
|
|
33
47
|
};
|
|
34
48
|
const VALID_ON_FAILURE = ['block', 'prompt', 'warn'];
|
|
@@ -61,6 +75,33 @@ const REVIEW_GATE_CONTEXT_NOTE = [
|
|
|
61
75
|
const GATE_CHECK_TIMEOUT_MS = 5_000;
|
|
62
76
|
const GATE_EXEC_TIMEOUT_MS = 15 * 60 * 1_000;
|
|
63
77
|
const GATE_LIVENESS_INTERVAL_MS = 30_000;
|
|
78
|
+
async function writeGateRunMarker(path, marker, warn) {
|
|
79
|
+
try {
|
|
80
|
+
await mkdir(join(tmpdir(), 'oat-gate-runs'), { recursive: true });
|
|
81
|
+
await writeFile(path, `${JSON.stringify(marker, null, 2)}\n`, 'utf8');
|
|
82
|
+
return true;
|
|
83
|
+
}
|
|
84
|
+
catch (error) {
|
|
85
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
86
|
+
warn(`Unable to write gate run marker ${path}: ${detail}`);
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
async function removeGateRunMarker(path, warn) {
|
|
91
|
+
try {
|
|
92
|
+
await rm(path);
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
if (error &&
|
|
96
|
+
typeof error === 'object' &&
|
|
97
|
+
'code' in error &&
|
|
98
|
+
error.code === 'ENOENT') {
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
102
|
+
warn(`Unable to remove gate run marker ${path}: ${detail}`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
64
105
|
function reviewGateProjectContext(project) {
|
|
65
106
|
return [
|
|
66
107
|
`Resolved OAT project path: ${project.path}. Run the review for this project path.`,
|
|
@@ -158,6 +199,7 @@ function buildGateDispatchReport(invocation, scope) {
|
|
|
158
199
|
}
|
|
159
200
|
function gateInvocationPromptContext(invocation) {
|
|
160
201
|
const frontmatter = YAML.stringify({
|
|
202
|
+
oat_gate_headless: true,
|
|
161
203
|
oat_gate_run_id: invocation.runId,
|
|
162
204
|
oat_gate_target: invocation.targetId,
|
|
163
205
|
oat_gate_runtime: invocation.runtime,
|
|
@@ -203,6 +245,12 @@ async function runChildProcess(command, args, options) {
|
|
|
203
245
|
let killTimeout = null;
|
|
204
246
|
let stderrBytes = 0;
|
|
205
247
|
let stdoutBytes = 0;
|
|
248
|
+
let refusal;
|
|
249
|
+
let stdoutLineBuffer = '';
|
|
250
|
+
let stderrLineBuffer = '';
|
|
251
|
+
let latestActivityEvidence;
|
|
252
|
+
let latestActivityProbeStatus;
|
|
253
|
+
let livenessProbePending = false;
|
|
206
254
|
const startedAt = Date.now();
|
|
207
255
|
let lastActivityAt = startedAt;
|
|
208
256
|
const child = spawn(command, args, {
|
|
@@ -215,23 +263,71 @@ async function runChildProcess(command, args, options) {
|
|
|
215
263
|
const recordActivity = () => {
|
|
216
264
|
lastActivityAt = Date.now();
|
|
217
265
|
};
|
|
266
|
+
const scanChunk = (buffer, chunk) => {
|
|
267
|
+
const combined = buffer + chunk.toString('utf8');
|
|
268
|
+
const lines = combined.split('\n');
|
|
269
|
+
const remainder = lines.pop() ?? '';
|
|
270
|
+
if (!refusal) {
|
|
271
|
+
for (const line of lines) {
|
|
272
|
+
refusal = extractStructuredRefusal(line.replace(/\r$/, ''));
|
|
273
|
+
if (refusal) {
|
|
274
|
+
break;
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
return remainder;
|
|
279
|
+
};
|
|
218
280
|
child.stdout?.on('data', (chunk) => {
|
|
219
281
|
stdoutBytes += chunk.byteLength;
|
|
282
|
+
stdoutLineBuffer = scanChunk(stdoutLineBuffer, chunk);
|
|
220
283
|
recordActivity();
|
|
221
284
|
process.stdout.write(chunk);
|
|
222
285
|
});
|
|
223
286
|
child.stderr?.on('data', (chunk) => {
|
|
224
287
|
stderrBytes += chunk.byteLength;
|
|
288
|
+
stderrLineBuffer = scanChunk(stderrLineBuffer, chunk);
|
|
225
289
|
recordActivity();
|
|
226
290
|
process.stderr.write(chunk);
|
|
227
291
|
});
|
|
292
|
+
const processAlive = () => {
|
|
293
|
+
if (child.pid === undefined)
|
|
294
|
+
return false;
|
|
295
|
+
try {
|
|
296
|
+
process.kill(child.pid, 0);
|
|
297
|
+
return true;
|
|
298
|
+
}
|
|
299
|
+
catch {
|
|
300
|
+
return false;
|
|
301
|
+
}
|
|
302
|
+
};
|
|
228
303
|
const livenessInterval = options.onLiveness && options.livenessIntervalMs
|
|
229
304
|
? setInterval(() => {
|
|
305
|
+
if (livenessProbePending)
|
|
306
|
+
return;
|
|
307
|
+
livenessProbePending = true;
|
|
230
308
|
const now = Date.now();
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
309
|
+
void (async () => {
|
|
310
|
+
const activityProbeStatus = await options.activityProbe?.observe(now);
|
|
311
|
+
const evidence = activityProbeStatus?.evidence;
|
|
312
|
+
if (activityProbeStatus) {
|
|
313
|
+
latestActivityProbeStatus = activityProbeStatus;
|
|
314
|
+
}
|
|
315
|
+
if (evidence)
|
|
316
|
+
latestActivityEvidence = evidence;
|
|
317
|
+
options.onLiveness?.({
|
|
318
|
+
elapsedMs: now - startedAt,
|
|
319
|
+
hardBudgetMs: options.timeoutMs,
|
|
320
|
+
idleMs: now - lastActivityAt,
|
|
321
|
+
processAlive: processAlive(),
|
|
322
|
+
...(latestActivityProbeStatus
|
|
323
|
+
? { activityProbeStatus: latestActivityProbeStatus }
|
|
324
|
+
: {}),
|
|
325
|
+
...(latestActivityEvidence
|
|
326
|
+
? { lastActivityEvidence: latestActivityEvidence }
|
|
327
|
+
: {}),
|
|
328
|
+
});
|
|
329
|
+
})().finally(() => {
|
|
330
|
+
livenessProbePending = false;
|
|
235
331
|
});
|
|
236
332
|
}, options.livenessIntervalMs)
|
|
237
333
|
: null;
|
|
@@ -263,8 +359,18 @@ async function runChildProcess(command, args, options) {
|
|
|
263
359
|
if (killTimeout) {
|
|
264
360
|
clearTimeout(killTimeout);
|
|
265
361
|
}
|
|
362
|
+
refusal ??=
|
|
363
|
+
extractStructuredRefusal(stdoutLineBuffer.replace(/\r$/, '')) ??
|
|
364
|
+
extractStructuredRefusal(stderrLineBuffer.replace(/\r$/, ''));
|
|
266
365
|
resolve({
|
|
366
|
+
...(latestActivityEvidence
|
|
367
|
+
? { activityEvidence: latestActivityEvidence }
|
|
368
|
+
: {}),
|
|
369
|
+
...(latestActivityProbeStatus
|
|
370
|
+
? { activityProbeStatus: latestActivityProbeStatus }
|
|
371
|
+
: {}),
|
|
267
372
|
exitCode: timedOut ? 124 : (code ?? 1),
|
|
373
|
+
...(refusal ? { refusal } : {}),
|
|
268
374
|
stderrBytes,
|
|
269
375
|
stdoutBytes,
|
|
270
376
|
...(timedOut ? { timedOut: true } : {}),
|
|
@@ -272,6 +378,10 @@ async function runChildProcess(command, args, options) {
|
|
|
272
378
|
});
|
|
273
379
|
});
|
|
274
380
|
}
|
|
381
|
+
function extractStructuredRefusal(output) {
|
|
382
|
+
const match = output.match(/^OAT_GATE_REFUSAL: (.*)$/m);
|
|
383
|
+
return match?.[1]?.replace(/\r$/, '');
|
|
384
|
+
}
|
|
275
385
|
function isGateWriteLayer(value) {
|
|
276
386
|
return VALID_WRITE_LAYERS.includes(value);
|
|
277
387
|
}
|
|
@@ -353,17 +463,131 @@ function parseNumericFlag(value, flag, defaultValue) {
|
|
|
353
463
|
}
|
|
354
464
|
return parsed;
|
|
355
465
|
}
|
|
356
|
-
function
|
|
357
|
-
const
|
|
358
|
-
if (!
|
|
359
|
-
|
|
360
|
-
}
|
|
361
|
-
const parsed = Number(rawValue);
|
|
362
|
-
if (!Number.isInteger(parsed) || parsed < 1) {
|
|
363
|
-
return GATE_EXEC_TIMEOUT_MS;
|
|
466
|
+
function parseGateTimeoutFlag(value, flag) {
|
|
467
|
+
const parsed = Number(value);
|
|
468
|
+
if (!isValidGateTimeoutMs(parsed)) {
|
|
469
|
+
throw new Error(`${flag} must be an integer between ${MIN_GATE_TIMEOUT_MS} and ${MAX_GATE_TIMEOUT_MS}.`);
|
|
364
470
|
}
|
|
365
471
|
return parsed;
|
|
366
472
|
}
|
|
473
|
+
function resolveGateExecTimeout(input) {
|
|
474
|
+
if (input.cliTimeoutMs !== undefined) {
|
|
475
|
+
return {
|
|
476
|
+
timeoutMs: parseGateTimeoutFlag(input.cliTimeoutMs, '--timeout-ms'),
|
|
477
|
+
source: 'cli',
|
|
478
|
+
};
|
|
479
|
+
}
|
|
480
|
+
const warned = new Set();
|
|
481
|
+
const warnOnce = (key, message) => {
|
|
482
|
+
if (!warned.has(key)) {
|
|
483
|
+
warned.add(key);
|
|
484
|
+
input.warn(message);
|
|
485
|
+
}
|
|
486
|
+
};
|
|
487
|
+
for (const persisted of input.rawPersisted?.target ?? []) {
|
|
488
|
+
if (isValidGateTimeoutMs(persisted.value)) {
|
|
489
|
+
return { timeoutMs: persisted.value, source: 'target' };
|
|
490
|
+
}
|
|
491
|
+
warnOnce(`target:${persisted.layer}`, `Ignoring invalid target.timeoutMs from ${persisted.layer} config; using the next timeout source.`);
|
|
492
|
+
}
|
|
493
|
+
if ((input.rawPersisted?.target.length ?? 0) === 0 &&
|
|
494
|
+
input.target.timeoutMs !== undefined) {
|
|
495
|
+
if (isValidGateTimeoutMs(input.target.timeoutMs)) {
|
|
496
|
+
return { timeoutMs: input.target.timeoutMs, source: 'target' };
|
|
497
|
+
}
|
|
498
|
+
input.warn('Ignoring invalid target.timeoutMs; using the next timeout source.');
|
|
499
|
+
}
|
|
500
|
+
const reviewType = input.reviewType?.trim().toLowerCase();
|
|
501
|
+
if (reviewType === 'code' || reviewType === 'artifact') {
|
|
502
|
+
const key = `workflow.gateTimeouts.${reviewType}`;
|
|
503
|
+
for (const persisted of input.rawPersisted?.workflow ?? []) {
|
|
504
|
+
if (isValidGateTimeoutMs(persisted.value)) {
|
|
505
|
+
return { timeoutMs: persisted.value, source: 'config' };
|
|
506
|
+
}
|
|
507
|
+
warnOnce(`${key}:${persisted.layer}`, `Ignoring invalid ${key} from ${persisted.layer} config; using the next timeout source.`);
|
|
508
|
+
}
|
|
509
|
+
const entry = input.effective.resolved[key];
|
|
510
|
+
if ((input.rawPersisted?.workflow.length ?? 0) === 0 &&
|
|
511
|
+
entry?.value !== null &&
|
|
512
|
+
entry?.value !== undefined) {
|
|
513
|
+
if (isValidGateTimeoutMs(entry.value)) {
|
|
514
|
+
return { timeoutMs: entry.value, source: 'config' };
|
|
515
|
+
}
|
|
516
|
+
warnOnce(`${key}:${entry.source}`, `Ignoring invalid ${key} from ${entry.source}; using the next timeout source.`);
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
const envValue = input.env.OAT_GATE_EXEC_TIMEOUT_MS?.trim();
|
|
520
|
+
if (envValue) {
|
|
521
|
+
const parsed = Number(envValue);
|
|
522
|
+
if (isValidGateTimeoutMs(parsed)) {
|
|
523
|
+
return { timeoutMs: parsed, source: 'env' };
|
|
524
|
+
}
|
|
525
|
+
warnOnce('env', 'Ignoring invalid OAT_GATE_EXEC_TIMEOUT_MS; using the next timeout source.');
|
|
526
|
+
}
|
|
527
|
+
const scope = input.reviewScope?.trim().toLowerCase() ?? '';
|
|
528
|
+
if (reviewType === 'artifact') {
|
|
529
|
+
return { timeoutMs: 900_000, source: 'scope-default' };
|
|
530
|
+
}
|
|
531
|
+
if (reviewType === 'code') {
|
|
532
|
+
if (/^p\d+-t\d+$/.test(scope)) {
|
|
533
|
+
return { timeoutMs: 900_000, source: 'scope-default' };
|
|
534
|
+
}
|
|
535
|
+
if (scope === 'final' ||
|
|
536
|
+
/^p\d+$/.test(scope) ||
|
|
537
|
+
/^p\d+-p\d+$/.test(scope)) {
|
|
538
|
+
return { timeoutMs: 1_800_000, source: 'scope-default' };
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
return { timeoutMs: GATE_EXEC_TIMEOUT_MS, source: 'default' };
|
|
542
|
+
}
|
|
543
|
+
function rawRecord(value) {
|
|
544
|
+
return typeof value === 'object' && value !== null && !Array.isArray(value)
|
|
545
|
+
? value
|
|
546
|
+
: null;
|
|
547
|
+
}
|
|
548
|
+
async function readRawConfig(path) {
|
|
549
|
+
try {
|
|
550
|
+
return rawRecord(parseJsonConfig(await readFile(path, 'utf8'), path)) ?? {};
|
|
551
|
+
}
|
|
552
|
+
catch (error) {
|
|
553
|
+
if (typeof error === 'object' &&
|
|
554
|
+
error !== null &&
|
|
555
|
+
'code' in error &&
|
|
556
|
+
error.code === 'ENOENT') {
|
|
557
|
+
return {};
|
|
558
|
+
}
|
|
559
|
+
throw error;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
async function readRawGateTimeoutLayers(input) {
|
|
563
|
+
const configs = await Promise.all([
|
|
564
|
+
readRawConfig(join(input.repoRoot, '.oat', 'config.local.json')),
|
|
565
|
+
readRawConfig(join(input.repoRoot, '.oat', 'config.json')),
|
|
566
|
+
readRawConfig(join(input.userConfigDir, 'config.json')),
|
|
567
|
+
]);
|
|
568
|
+
const layers = ['local', 'shared', 'user'];
|
|
569
|
+
const target = [];
|
|
570
|
+
const workflow = [];
|
|
571
|
+
const reviewType = input.reviewType?.trim().toLowerCase();
|
|
572
|
+
for (const [index, config] of configs.entries()) {
|
|
573
|
+
const layer = layers[index];
|
|
574
|
+
const workflowConfig = rawRecord(config.workflow);
|
|
575
|
+
const gates = rawRecord(workflowConfig?.gates);
|
|
576
|
+
const execTargets = rawRecord(gates?.execTargets);
|
|
577
|
+
const rawTarget = rawRecord(execTargets?.[input.targetId]);
|
|
578
|
+
if (rawTarget &&
|
|
579
|
+
Object.prototype.hasOwnProperty.call(rawTarget, 'timeoutMs')) {
|
|
580
|
+
target.push({ layer, value: rawTarget.timeoutMs });
|
|
581
|
+
}
|
|
582
|
+
const gateTimeouts = rawRecord(workflowConfig?.gateTimeouts);
|
|
583
|
+
if ((reviewType === 'code' || reviewType === 'artifact') &&
|
|
584
|
+
gateTimeouts &&
|
|
585
|
+
Object.prototype.hasOwnProperty.call(gateTimeouts, reviewType)) {
|
|
586
|
+
workflow.push({ layer, value: gateTimeouts[reviewType] });
|
|
587
|
+
}
|
|
588
|
+
}
|
|
589
|
+
return { target, workflow };
|
|
590
|
+
}
|
|
367
591
|
function resolveGateLivenessIntervalMs(env) {
|
|
368
592
|
const rawValue = env.OAT_GATE_LIVENESS_INTERVAL_MS?.trim();
|
|
369
593
|
if (!rawValue) {
|
|
@@ -431,6 +655,9 @@ function parseExecTargetConfig(options) {
|
|
|
431
655
|
...(options.priority !== undefined
|
|
432
656
|
? { priority: parseNumericFlag(options.priority, '--priority', 0) }
|
|
433
657
|
: {}),
|
|
658
|
+
...(options.timeoutMs !== undefined
|
|
659
|
+
? { timeoutMs: parseGateTimeoutFlag(options.timeoutMs, '--timeout-ms') }
|
|
660
|
+
: {}),
|
|
434
661
|
...(options.invocationModel !== undefined ||
|
|
435
662
|
options.invocationReasoningEffort !== undefined
|
|
436
663
|
? {
|
|
@@ -873,7 +1100,7 @@ async function resolveSelectedExecTarget(targets, options, producerIdentity, con
|
|
|
873
1100
|
}
|
|
874
1101
|
return attachDiversityMetadata(selected, avoid, producerIdentity);
|
|
875
1102
|
}
|
|
876
|
-
async function executeTarget(selected, prompt, context, dependencies) {
|
|
1103
|
+
async function executeTarget(selected, prompt, context, dependencies, timeout) {
|
|
877
1104
|
const [command, baseArgs] = argvHead(selected.target.baseCommand);
|
|
878
1105
|
if (!command) {
|
|
879
1106
|
throw new Error(`Exec target "${selected.id}" has an empty base command.`);
|
|
@@ -881,21 +1108,39 @@ async function executeTarget(selected, prompt, context, dependencies) {
|
|
|
881
1108
|
const modelArgs = selected.model && !findPinnedModelArg(selected.target.baseCommand)
|
|
882
1109
|
? ['--model', selected.model]
|
|
883
1110
|
: [];
|
|
884
|
-
const timeoutMs = resolveGateExecTimeoutMs(dependencies.processEnv);
|
|
885
1111
|
const livenessIntervalMs = resolveGateLivenessIntervalMs(dependencies.processEnv);
|
|
886
|
-
|
|
887
|
-
|
|
1112
|
+
const activityProbe = await dependencies.createGateActivityProbe({
|
|
1113
|
+
runtime: selected.target.runtime,
|
|
1114
|
+
cwd: context.cwd,
|
|
1115
|
+
home: context.home,
|
|
1116
|
+
spawnedAt: Date.now(),
|
|
1117
|
+
});
|
|
1118
|
+
if (context.json) {
|
|
1119
|
+
dependencies.writeDiagnostic(`${JSON.stringify({
|
|
1120
|
+
type: 'gate-start',
|
|
1121
|
+
target: selected.id,
|
|
1122
|
+
runtime: selected.target.runtime,
|
|
1123
|
+
timeoutMs: timeout.timeoutMs,
|
|
1124
|
+
timeoutSource: timeout.source,
|
|
1125
|
+
})}\n`);
|
|
1126
|
+
}
|
|
1127
|
+
else {
|
|
1128
|
+
context.logger.info(`Running gate target ${selected.id} (${selected.target.runtime}); timeout=${timeout.timeoutMs}ms (source=${timeout.source}).`);
|
|
888
1129
|
}
|
|
889
1130
|
try {
|
|
890
1131
|
return await dependencies.runProcess(command, [...baseArgs, ...modelArgs, ...prompt], {
|
|
891
1132
|
cwd: context.cwd,
|
|
892
1133
|
env: dependencies.processEnv,
|
|
1134
|
+
...(activityProbe ? { activityProbe } : {}),
|
|
893
1135
|
livenessIntervalMs,
|
|
894
|
-
onLiveness: ({ elapsedMs, hardBudgetMs, idleMs }) => {
|
|
1136
|
+
onLiveness: ({ elapsedMs, hardBudgetMs, idleMs, processAlive, activityProbeStatus, lastActivityEvidence, }) => {
|
|
895
1137
|
const telemetry = {
|
|
896
1138
|
elapsedMs,
|
|
897
1139
|
hardBudgetMs,
|
|
898
1140
|
idleMs,
|
|
1141
|
+
processAlive,
|
|
1142
|
+
...(activityProbeStatus ? { activityProbeStatus } : {}),
|
|
1143
|
+
...(lastActivityEvidence ? { lastActivityEvidence } : {}),
|
|
899
1144
|
target: selected.id,
|
|
900
1145
|
type: 'gate-liveness',
|
|
901
1146
|
};
|
|
@@ -903,13 +1148,18 @@ async function executeTarget(selected, prompt, context, dependencies) {
|
|
|
903
1148
|
dependencies.writeDiagnostic(`${JSON.stringify(telemetry)}\n`);
|
|
904
1149
|
}
|
|
905
1150
|
else {
|
|
906
|
-
|
|
1151
|
+
const activityDescription = lastActivityEvidence
|
|
1152
|
+
? lastActivityEvidence.scope === 'ambient-runtime'
|
|
1153
|
+
? 'ambient runtime activity (not attributable to this gate child)'
|
|
1154
|
+
: 'project-directory activity'
|
|
1155
|
+
: `${activityProbeStatus?.status ?? 'unavailable'} (${activityProbeStatus?.attemptedPath ?? 'no path'})`;
|
|
1156
|
+
context.logger.info(`Gate liveness: target=${selected.id} elapsed_ms=${elapsedMs} idle_ms=${idleMs} hard_budget_ms=${hardBudgetMs} process_alive=${processAlive} activity_evidence=${activityDescription}.`);
|
|
907
1157
|
}
|
|
908
1158
|
},
|
|
909
1159
|
purpose: 'execute',
|
|
910
1160
|
stdin: 'ignore',
|
|
911
1161
|
stdio: 'pipe',
|
|
912
|
-
timeoutMs,
|
|
1162
|
+
timeoutMs: timeout.timeoutMs,
|
|
913
1163
|
});
|
|
914
1164
|
}
|
|
915
1165
|
catch (error) {
|
|
@@ -1283,9 +1533,11 @@ function writeReviewGateResult(context, payload) {
|
|
|
1283
1533
|
context.logger.info(payload.handoff);
|
|
1284
1534
|
}
|
|
1285
1535
|
function writeReviewGateExecutionFailure(context, payload) {
|
|
1286
|
-
const message = payload.
|
|
1287
|
-
? `Review did not complete:
|
|
1288
|
-
:
|
|
1536
|
+
const message = payload.refusal
|
|
1537
|
+
? `Review did not complete: reviewer refused the headless route (${payload.refusal}).`
|
|
1538
|
+
: payload.timedOut
|
|
1539
|
+
? `Review did not complete: target ${payload.target} timed out after ${payload.timeoutMs}ms.`
|
|
1540
|
+
: `Review did not complete: target ${payload.target} exited with code ${payload.exitCode}.`;
|
|
1289
1541
|
if (context.json) {
|
|
1290
1542
|
context.logger.json({
|
|
1291
1543
|
status: 'review_failed',
|
|
@@ -1304,9 +1556,16 @@ function writeReviewGateExecutionFailure(context, payload) {
|
|
|
1304
1556
|
...(payload.timeoutMs !== undefined
|
|
1305
1557
|
? { timeoutMs: payload.timeoutMs }
|
|
1306
1558
|
: {}),
|
|
1559
|
+
...(payload.timeoutSource !== undefined
|
|
1560
|
+
? { timeoutSource: payload.timeoutSource }
|
|
1561
|
+
: {}),
|
|
1307
1562
|
...(payload.noOutputProduced !== undefined
|
|
1308
1563
|
? { noOutputProduced: payload.noOutputProduced }
|
|
1309
1564
|
: {}),
|
|
1565
|
+
...(payload.refusal ? { refusal: payload.refusal } : {}),
|
|
1566
|
+
...(payload.activityEvidence
|
|
1567
|
+
? { activityEvidence: payload.activityEvidence }
|
|
1568
|
+
: {}),
|
|
1310
1569
|
message,
|
|
1311
1570
|
});
|
|
1312
1571
|
return;
|
|
@@ -1397,6 +1656,29 @@ function writeReviewGateTargetingFailure(context, payload) {
|
|
|
1397
1656
|
context.logger.error(`Expected project=${payload.corroboration.expectedProject} run=${payload.gateInvocation.runId}; actual containing_project=${payload.corroboration.actual.containingProject ?? 'missing'} artifact_project=${payload.corroboration.actual.artifactProject ?? 'missing'} run_matches=${payload.corroboration.actual.matchingArtifactPaths.join(',') || 'none'}.`);
|
|
1398
1657
|
context.logger.error('This targeting failure is not receive-eligible and must be corrected before severity or invocation remediation.');
|
|
1399
1658
|
}
|
|
1659
|
+
async function finalizeReviewGateProjectLog(context, dependencies, finalization) {
|
|
1660
|
+
const findings = finalization.counts
|
|
1661
|
+
? ` findings=critical:${finalization.counts.critical},important:${finalization.counts.important},medium:${finalization.counts.medium},minor:${finalization.counts.minor}`
|
|
1662
|
+
: '';
|
|
1663
|
+
const artifact = finalization.artifactPath
|
|
1664
|
+
? ` artifact=${finalization.artifactPath}`
|
|
1665
|
+
: '';
|
|
1666
|
+
const body = `target=${finalization.target} threshold=${finalization.threshold}${findings} exit=${finalization.exitCode} status=${finalization.status}${artifact}`;
|
|
1667
|
+
try {
|
|
1668
|
+
await dependencies.appendProjectLog({
|
|
1669
|
+
repoRoot: finalization.repoRoot,
|
|
1670
|
+
home: finalization.home,
|
|
1671
|
+
project: finalization.project,
|
|
1672
|
+
structural: true,
|
|
1673
|
+
producer: 'oat gate review',
|
|
1674
|
+
ref: finalization.ref,
|
|
1675
|
+
body,
|
|
1676
|
+
});
|
|
1677
|
+
}
|
|
1678
|
+
catch (error) {
|
|
1679
|
+
context.logger.warn(`Warning: unable to append oat gate review result to the project log: ${error instanceof Error ? error.message : String(error)}. Gate result is unchanged.`);
|
|
1680
|
+
}
|
|
1681
|
+
}
|
|
1400
1682
|
async function updateConfigLayer(context, layer, dependencies, mutate) {
|
|
1401
1683
|
const repoRoot = await dependencies.resolveProjectRoot(context.cwd);
|
|
1402
1684
|
const userConfigDir = join(context.home, '.oat');
|
|
@@ -1526,7 +1808,21 @@ async function runCrossProviderExec(prompt, options, context, dependencies) {
|
|
|
1526
1808
|
const producerIdentity = parseProducerIdentityOption(options.producerIdentity);
|
|
1527
1809
|
const selected = await resolveSelectedExecTarget(targets, options, producerIdentity, context, dependencies);
|
|
1528
1810
|
logGateDiversity(selected, context);
|
|
1529
|
-
const
|
|
1811
|
+
const repoRoot = await dependencies.resolveProjectRoot(context.cwd);
|
|
1812
|
+
const rawPersisted = await readRawGateTimeoutLayers({
|
|
1813
|
+
repoRoot,
|
|
1814
|
+
userConfigDir: join(context.home, '.oat'),
|
|
1815
|
+
targetId: selected.id,
|
|
1816
|
+
});
|
|
1817
|
+
const timeout = resolveGateExecTimeout({
|
|
1818
|
+
cliTimeoutMs: options.timeoutMs,
|
|
1819
|
+
target: selected.target,
|
|
1820
|
+
effective,
|
|
1821
|
+
env: dependencies.processEnv,
|
|
1822
|
+
warn: context.logger.warn,
|
|
1823
|
+
rawPersisted,
|
|
1824
|
+
});
|
|
1825
|
+
const result = await executeTarget(selected, prompt, context, dependencies, timeout);
|
|
1530
1826
|
process.exitCode = result.exitCode;
|
|
1531
1827
|
}
|
|
1532
1828
|
catch (error) {
|
|
@@ -1535,6 +1831,11 @@ async function runCrossProviderExec(prompt, options, context, dependencies) {
|
|
|
1535
1831
|
}
|
|
1536
1832
|
async function runReviewGate(prompt, options, context, dependencies) {
|
|
1537
1833
|
const runId = randomUUID();
|
|
1834
|
+
let runMarkerPath;
|
|
1835
|
+
let runMarkerWritten = false;
|
|
1836
|
+
let branchLocalGateCli;
|
|
1837
|
+
let projectLogFinalization;
|
|
1838
|
+
let projectLogFinalized = false;
|
|
1538
1839
|
let postSelectionContext;
|
|
1539
1840
|
try {
|
|
1540
1841
|
const repoRoot = await dependencies.resolveProjectRoot(context.cwd);
|
|
@@ -1555,6 +1856,22 @@ async function runReviewGate(prompt, options, context, dependencies) {
|
|
|
1555
1856
|
});
|
|
1556
1857
|
const selected = await resolveSelectedExecTarget(targets, options, producerIdentity, context, dependencies);
|
|
1557
1858
|
const gateInvocation = createGateInvocationMetadata(runId, selected);
|
|
1859
|
+
const rawPersisted = await readRawGateTimeoutLayers({
|
|
1860
|
+
repoRoot,
|
|
1861
|
+
userConfigDir,
|
|
1862
|
+
targetId: selected.id,
|
|
1863
|
+
reviewType: options.reviewType,
|
|
1864
|
+
});
|
|
1865
|
+
const timeout = resolveGateExecTimeout({
|
|
1866
|
+
cliTimeoutMs: options.timeoutMs,
|
|
1867
|
+
target: selected.target,
|
|
1868
|
+
effective,
|
|
1869
|
+
reviewType: options.reviewType,
|
|
1870
|
+
reviewScope: options.reviewScope,
|
|
1871
|
+
env: dependencies.processEnv,
|
|
1872
|
+
warn: context.logger.warn,
|
|
1873
|
+
rawPersisted,
|
|
1874
|
+
});
|
|
1558
1875
|
const dispatchReport = buildGateDispatchReport(gateInvocation, options.reviewScope?.trim() || 'gate-review');
|
|
1559
1876
|
postSelectionContext = {
|
|
1560
1877
|
project: projectPath,
|
|
@@ -1564,6 +1881,16 @@ async function runReviewGate(prompt, options, context, dependencies) {
|
|
|
1564
1881
|
dispatchReport,
|
|
1565
1882
|
};
|
|
1566
1883
|
const threshold = parseReviewGateThreshold(options.exitNonzeroOn);
|
|
1884
|
+
projectLogFinalization = {
|
|
1885
|
+
repoRoot,
|
|
1886
|
+
home: context.home,
|
|
1887
|
+
project: projectPath,
|
|
1888
|
+
ref: options.reviewScope?.trim() || 'gate-review',
|
|
1889
|
+
target: selected.id,
|
|
1890
|
+
threshold,
|
|
1891
|
+
status: 'review_failed',
|
|
1892
|
+
exitCode: 1,
|
|
1893
|
+
};
|
|
1567
1894
|
const before = await listReviewGateArtifactCandidates({
|
|
1568
1895
|
repoRoot,
|
|
1569
1896
|
effective,
|
|
@@ -1581,9 +1908,65 @@ async function runReviewGate(prompt, options, context, dependencies) {
|
|
|
1581
1908
|
: []),
|
|
1582
1909
|
prompt.join(' '),
|
|
1583
1910
|
]);
|
|
1584
|
-
|
|
1911
|
+
runMarkerPath = join(tmpdir(), 'oat-gate-runs', `${runId}.json`);
|
|
1912
|
+
runMarkerWritten = await dependencies.writeGateRunMarker(runMarkerPath, {
|
|
1913
|
+
runId,
|
|
1914
|
+
targetId: selected.id,
|
|
1915
|
+
runtime: selected.target.runtime,
|
|
1916
|
+
reviewType: options.reviewType?.trim() || null,
|
|
1917
|
+
reviewScope: options.reviewScope?.trim() || null,
|
|
1918
|
+
project: projectPath,
|
|
1919
|
+
startedAt: new Date().toISOString(),
|
|
1920
|
+
budgetMs: timeout.timeoutMs,
|
|
1921
|
+
budgetSource: timeout.source,
|
|
1922
|
+
}, (message) => context.logger.warn(message));
|
|
1923
|
+
if (runMarkerWritten) {
|
|
1924
|
+
if (context.json) {
|
|
1925
|
+
dependencies.writeDiagnostic(`${JSON.stringify({
|
|
1926
|
+
type: 'gate-run-marker',
|
|
1927
|
+
runId,
|
|
1928
|
+
path: runMarkerPath,
|
|
1929
|
+
})}\n`);
|
|
1930
|
+
}
|
|
1931
|
+
else {
|
|
1932
|
+
context.logger.info(`Gate run marker: ${runMarkerPath}.`);
|
|
1933
|
+
}
|
|
1934
|
+
}
|
|
1935
|
+
branchLocalGateCli = await dependencies.createBranchLocalGateCli({
|
|
1936
|
+
runId,
|
|
1937
|
+
launch: dependencies.currentGateCliLaunch(),
|
|
1938
|
+
});
|
|
1939
|
+
const childResult = await executeTarget(selected, [reviewPrompt], context, {
|
|
1940
|
+
...dependencies,
|
|
1941
|
+
processEnv: {
|
|
1942
|
+
...dependencies.processEnv,
|
|
1943
|
+
OAT_GATE_HEADLESS: '1',
|
|
1944
|
+
OAT_NON_INTERACTIVE: '1',
|
|
1945
|
+
OAT_GATE_RUN_ID: runId,
|
|
1946
|
+
OAT_GATE_RUNTIME: gateInvocation.runtime,
|
|
1947
|
+
OAT_INVOCATION_MODEL: gateInvocation.model,
|
|
1948
|
+
OAT_GATE_CLI_PATH: branchLocalGateCli.cliPath,
|
|
1949
|
+
OAT_GATE_CLI_ROOT: branchLocalGateCli.cliRoot,
|
|
1950
|
+
OAT_GATE_ROUTE_RECEIPT_PATH: branchLocalGateCli.routeReceiptPath,
|
|
1951
|
+
},
|
|
1952
|
+
}, timeout);
|
|
1953
|
+
const routeReceipt = await dependencies.readGateRouteReceipt(branchLocalGateCli.routeReceiptPath, branchLocalGateCli.cliRoot, selected.target.runtime);
|
|
1954
|
+
dependencies.writeDiagnostic(`${JSON.stringify({
|
|
1955
|
+
type: 'gate-route',
|
|
1956
|
+
target: selected.id,
|
|
1957
|
+
...routeReceipt,
|
|
1958
|
+
})}\n`);
|
|
1585
1959
|
const childExitCode = childResult.exitCode;
|
|
1586
|
-
|
|
1960
|
+
const refusal = childResult.refusal ??
|
|
1961
|
+
extractStructuredRefusal(childResult.capturedOutput ?? '');
|
|
1962
|
+
const writeRefusalFailure = () => {
|
|
1963
|
+
if (!refusal) {
|
|
1964
|
+
return false;
|
|
1965
|
+
}
|
|
1966
|
+
if (projectLogFinalization) {
|
|
1967
|
+
projectLogFinalization.status = 'review_failed';
|
|
1968
|
+
projectLogFinalization.exitCode = 1;
|
|
1969
|
+
}
|
|
1587
1970
|
writeReviewGateExecutionFailure(context, {
|
|
1588
1971
|
runId,
|
|
1589
1972
|
target: selected.id,
|
|
@@ -1591,13 +1974,18 @@ async function runReviewGate(prompt, options, context, dependencies) {
|
|
|
1591
1974
|
projectResolutionSource: reviewProject.source,
|
|
1592
1975
|
exitCode: childExitCode,
|
|
1593
1976
|
timedOut: childResult.timedOut ?? false,
|
|
1594
|
-
timeoutMs:
|
|
1977
|
+
timeoutMs: timeout.timeoutMs,
|
|
1978
|
+
timeoutSource: timeout.source,
|
|
1979
|
+
refusal,
|
|
1980
|
+
...(childResult.activityEvidence
|
|
1981
|
+
? { activityEvidence: childResult.activityEvidence }
|
|
1982
|
+
: {}),
|
|
1595
1983
|
gateInvocation,
|
|
1596
1984
|
dispatchReport,
|
|
1597
1985
|
});
|
|
1598
|
-
process.exitCode =
|
|
1599
|
-
return;
|
|
1600
|
-
}
|
|
1986
|
+
process.exitCode = 1;
|
|
1987
|
+
return true;
|
|
1988
|
+
};
|
|
1601
1989
|
const after = await listReviewGateArtifactCandidates({
|
|
1602
1990
|
repoRoot,
|
|
1603
1991
|
effective,
|
|
@@ -1608,9 +1996,46 @@ async function runReviewGate(prompt, options, context, dependencies) {
|
|
|
1608
1996
|
before,
|
|
1609
1997
|
after,
|
|
1610
1998
|
});
|
|
1999
|
+
if (projectLogFinalization) {
|
|
2000
|
+
projectLogFinalization.artifactPath =
|
|
2001
|
+
artifactResolution.artifact?.path ??
|
|
2002
|
+
artifactResolution.diagnosticArtifact?.path;
|
|
2003
|
+
}
|
|
2004
|
+
if (!artifactResolution.artifact && writeRefusalFailure()) {
|
|
2005
|
+
return;
|
|
2006
|
+
}
|
|
2007
|
+
if (childExitCode !== 0 &&
|
|
2008
|
+
!childResult.timedOut &&
|
|
2009
|
+
!artifactResolution.artifact) {
|
|
2010
|
+
if (projectLogFinalization) {
|
|
2011
|
+
projectLogFinalization.status = 'review_failed';
|
|
2012
|
+
projectLogFinalization.exitCode = childExitCode;
|
|
2013
|
+
}
|
|
2014
|
+
writeReviewGateExecutionFailure(context, {
|
|
2015
|
+
runId,
|
|
2016
|
+
target: selected.id,
|
|
2017
|
+
project: projectPath,
|
|
2018
|
+
projectResolutionSource: reviewProject.source,
|
|
2019
|
+
exitCode: childExitCode,
|
|
2020
|
+
timedOut: childResult.timedOut ?? false,
|
|
2021
|
+
timeoutMs: timeout.timeoutMs,
|
|
2022
|
+
timeoutSource: timeout.source,
|
|
2023
|
+
...(childResult.activityEvidence
|
|
2024
|
+
? { activityEvidence: childResult.activityEvidence }
|
|
2025
|
+
: {}),
|
|
2026
|
+
gateInvocation,
|
|
2027
|
+
dispatchReport,
|
|
2028
|
+
});
|
|
2029
|
+
process.exitCode = childExitCode;
|
|
2030
|
+
return;
|
|
2031
|
+
}
|
|
1611
2032
|
if (childResult.timedOut &&
|
|
1612
2033
|
artifactResolution.matchingArtifactPaths.length === 0 &&
|
|
1613
2034
|
!artifactResolution.diagnosticArtifact) {
|
|
2035
|
+
if (projectLogFinalization) {
|
|
2036
|
+
projectLogFinalization.status = 'review_failed';
|
|
2037
|
+
projectLogFinalization.exitCode = childExitCode;
|
|
2038
|
+
}
|
|
1614
2039
|
writeReviewGateExecutionFailure(context, {
|
|
1615
2040
|
runId,
|
|
1616
2041
|
target: selected.id,
|
|
@@ -1618,8 +2043,12 @@ async function runReviewGate(prompt, options, context, dependencies) {
|
|
|
1618
2043
|
projectResolutionSource: reviewProject.source,
|
|
1619
2044
|
exitCode: childExitCode,
|
|
1620
2045
|
timedOut: true,
|
|
1621
|
-
timeoutMs:
|
|
2046
|
+
timeoutMs: timeout.timeoutMs,
|
|
2047
|
+
timeoutSource: timeout.source,
|
|
1622
2048
|
noOutputProduced: childResult.stdoutBytes + childResult.stderrBytes === 0,
|
|
2049
|
+
...(childResult.activityEvidence
|
|
2050
|
+
? { activityEvidence: childResult.activityEvidence }
|
|
2051
|
+
: {}),
|
|
1623
2052
|
gateInvocation,
|
|
1624
2053
|
dispatchReport,
|
|
1625
2054
|
});
|
|
@@ -1642,6 +2071,10 @@ async function runReviewGate(prompt, options, context, dependencies) {
|
|
|
1642
2071
|
: initialTargetCorroboration.run === 'mismatched'
|
|
1643
2072
|
? `The changed review artifact did not carry the expected gate run ID ${runId}.`
|
|
1644
2073
|
: `No direct active project review artifact carried gate run ID ${runId}.`;
|
|
2074
|
+
if (projectLogFinalization) {
|
|
2075
|
+
projectLogFinalization.status = 'targeting_correlation_failed';
|
|
2076
|
+
projectLogFinalization.exitCode = 1;
|
|
2077
|
+
}
|
|
1645
2078
|
writeReviewGateTargetingFailure(context, {
|
|
1646
2079
|
runId,
|
|
1647
2080
|
target: selected.id,
|
|
@@ -1660,6 +2093,13 @@ async function runReviewGate(prompt, options, context, dependencies) {
|
|
|
1660
2093
|
if (producedArtifact.containingProject !== reviewProject.path ||
|
|
1661
2094
|
(reviewProject.source === 'declared' &&
|
|
1662
2095
|
initialTargetCorroboration.project !== 'matched')) {
|
|
2096
|
+
if (writeRefusalFailure()) {
|
|
2097
|
+
return;
|
|
2098
|
+
}
|
|
2099
|
+
if (projectLogFinalization) {
|
|
2100
|
+
projectLogFinalization.status = 'targeting_correlation_failed';
|
|
2101
|
+
projectLogFinalization.exitCode = 1;
|
|
2102
|
+
}
|
|
1663
2103
|
writeReviewGateTargetingFailure(context, {
|
|
1664
2104
|
runId,
|
|
1665
2105
|
target: selected.id,
|
|
@@ -1681,6 +2121,13 @@ async function runReviewGate(prompt, options, context, dependencies) {
|
|
|
1681
2121
|
}
|
|
1682
2122
|
if (!producedArtifact.generatedAt ||
|
|
1683
2123
|
!Number.isFinite(producedArtifact.generatedTime)) {
|
|
2124
|
+
if (writeRefusalFailure()) {
|
|
2125
|
+
return;
|
|
2126
|
+
}
|
|
2127
|
+
if (projectLogFinalization) {
|
|
2128
|
+
projectLogFinalization.status = 'artifact_validation_failed';
|
|
2129
|
+
projectLogFinalization.exitCode = 1;
|
|
2130
|
+
}
|
|
1684
2131
|
writeReviewGateArtifactValidationFailure(context, {
|
|
1685
2132
|
runId,
|
|
1686
2133
|
target: selected.id,
|
|
@@ -1708,7 +2155,14 @@ async function runReviewGate(prompt, options, context, dependencies) {
|
|
|
1708
2155
|
});
|
|
1709
2156
|
}
|
|
1710
2157
|
catch (error) {
|
|
2158
|
+
if (writeRefusalFailure()) {
|
|
2159
|
+
return;
|
|
2160
|
+
}
|
|
1711
2161
|
const detail = error instanceof Error ? error.message : String(error);
|
|
2162
|
+
if (projectLogFinalization) {
|
|
2163
|
+
projectLogFinalization.status = 'artifact_validation_failed';
|
|
2164
|
+
projectLogFinalization.exitCode = 1;
|
|
2165
|
+
}
|
|
1712
2166
|
writeReviewGateArtifactValidationFailure(context, {
|
|
1713
2167
|
runId,
|
|
1714
2168
|
target: selected.id,
|
|
@@ -1725,12 +2179,22 @@ async function runReviewGate(prompt, options, context, dependencies) {
|
|
|
1725
2179
|
process.exitCode = 1;
|
|
1726
2180
|
return;
|
|
1727
2181
|
}
|
|
2182
|
+
if (projectLogFinalization) {
|
|
2183
|
+
projectLogFinalization.counts = verdict.counts;
|
|
2184
|
+
}
|
|
1728
2185
|
const targetCorroboration = initialTargetCorroboration;
|
|
1729
2186
|
const corroboration = corroborateGateInvocation(gateInvocation, verdict.gateInvocation, targetCorroboration);
|
|
1730
2187
|
if (corroboration.run !== 'matched' ||
|
|
1731
2188
|
corroboration.invocation !== 'matched') {
|
|
2189
|
+
if (writeRefusalFailure()) {
|
|
2190
|
+
return;
|
|
2191
|
+
}
|
|
1732
2192
|
const missing = corroboration.run === 'missing' ||
|
|
1733
2193
|
corroboration.invocation === 'missing';
|
|
2194
|
+
if (projectLogFinalization) {
|
|
2195
|
+
projectLogFinalization.status = 'artifact_validation_failed';
|
|
2196
|
+
projectLogFinalization.exitCode = 1;
|
|
2197
|
+
}
|
|
1734
2198
|
writeReviewGateArtifactValidationFailure(context, {
|
|
1735
2199
|
runId,
|
|
1736
2200
|
target: selected.id,
|
|
@@ -1750,6 +2214,13 @@ async function runReviewGate(prompt, options, context, dependencies) {
|
|
|
1750
2214
|
return;
|
|
1751
2215
|
}
|
|
1752
2216
|
if (verdict.invocation !== 'gate') {
|
|
2217
|
+
if (writeRefusalFailure()) {
|
|
2218
|
+
return;
|
|
2219
|
+
}
|
|
2220
|
+
if (projectLogFinalization) {
|
|
2221
|
+
projectLogFinalization.status = 'artifact_validation_failed';
|
|
2222
|
+
projectLogFinalization.exitCode = 1;
|
|
2223
|
+
}
|
|
1753
2224
|
writeReviewGateArtifactValidationFailure(context, {
|
|
1754
2225
|
runId,
|
|
1755
2226
|
target: selected.id,
|
|
@@ -1773,6 +2244,12 @@ async function runReviewGate(prompt, options, context, dependencies) {
|
|
|
1773
2244
|
threshold,
|
|
1774
2245
|
blocking,
|
|
1775
2246
|
});
|
|
2247
|
+
if (projectLogFinalization) {
|
|
2248
|
+
projectLogFinalization.status = blocking ? 'blocked' : 'ok';
|
|
2249
|
+
projectLogFinalization.exitCode = blocking ? 1 : 0;
|
|
2250
|
+
projectLogFinalization.counts = verdict.counts;
|
|
2251
|
+
projectLogFinalization.artifactPath = producedArtifact.path;
|
|
2252
|
+
}
|
|
1776
2253
|
writeReviewGateResult(context, {
|
|
1777
2254
|
status: blocking ? 'blocked' : 'ok',
|
|
1778
2255
|
runId,
|
|
@@ -1809,6 +2286,22 @@ async function runReviewGate(prompt, options, context, dependencies) {
|
|
|
1809
2286
|
writeError(context, error);
|
|
1810
2287
|
}
|
|
1811
2288
|
}
|
|
2289
|
+
finally {
|
|
2290
|
+
try {
|
|
2291
|
+
if (branchLocalGateCli) {
|
|
2292
|
+
await dependencies.removeBranchLocalGateCli(branchLocalGateCli);
|
|
2293
|
+
}
|
|
2294
|
+
if (runMarkerPath) {
|
|
2295
|
+
await dependencies.removeGateRunMarker(runMarkerPath, (message) => context.logger.warn(message));
|
|
2296
|
+
}
|
|
2297
|
+
}
|
|
2298
|
+
finally {
|
|
2299
|
+
if (projectLogFinalization && !projectLogFinalized) {
|
|
2300
|
+
projectLogFinalized = true;
|
|
2301
|
+
await finalizeReviewGateProjectLog(context, dependencies, projectLogFinalization);
|
|
2302
|
+
}
|
|
2303
|
+
}
|
|
2304
|
+
}
|
|
1812
2305
|
}
|
|
1813
2306
|
export function createGateCommand(overrides = {}) {
|
|
1814
2307
|
const dependencies = {
|
|
@@ -1816,6 +2309,10 @@ export function createGateCommand(overrides = {}) {
|
|
|
1816
2309
|
...overrides,
|
|
1817
2310
|
};
|
|
1818
2311
|
const cmd = new Command('gate').description('Resolve and manage workflow gate configuration');
|
|
2312
|
+
cmd.addCommand(createGateRouteCommand({
|
|
2313
|
+
buildCommandContext: dependencies.buildCommandContext,
|
|
2314
|
+
processEnv: dependencies.processEnv,
|
|
2315
|
+
}));
|
|
1819
2316
|
cmd
|
|
1820
2317
|
.command('resolve')
|
|
1821
2318
|
.description('Print the resolved gate configuration for a skill')
|
|
@@ -1849,11 +2346,13 @@ export function createGateCommand(overrides = {}) {
|
|
|
1849
2346
|
});
|
|
1850
2347
|
cmd
|
|
1851
2348
|
.command('cross-provider-exec')
|
|
2349
|
+
.alias('exec')
|
|
1852
2350
|
.description('Run a prompt through an alternate configured runtime target')
|
|
1853
2351
|
.option('--target <id>', 'Run this exact exec target')
|
|
1854
2352
|
.option('--avoid <mode>', 'Avoidance mode: same-family, same-runtime, or none')
|
|
1855
2353
|
.option('--current-runtime <runtime>', 'Override detected runtime for testing or manual routing')
|
|
1856
2354
|
.option('--producer-identity <identity>', 'Producer identity as <value>:<declared|observed|inferred|unknown>')
|
|
2355
|
+
.option('--timeout-ms <milliseconds>', `Gate timeout in milliseconds (${MIN_GATE_TIMEOUT_MS}-${MAX_GATE_TIMEOUT_MS})`)
|
|
1857
2356
|
.argument('<prompt...>', 'Prompt arguments appended to the target command')
|
|
1858
2357
|
.action(async (prompt, options, command) => {
|
|
1859
2358
|
const context = dependencies.buildCommandContext(readGlobalOptions(command));
|
|
@@ -1866,6 +2365,7 @@ export function createGateCommand(overrides = {}) {
|
|
|
1866
2365
|
.option('--avoid <mode>', 'Avoidance mode: same-family, same-runtime, or none')
|
|
1867
2366
|
.option('--current-runtime <runtime>', 'Override detected runtime for testing or manual routing')
|
|
1868
2367
|
.option('--producer-identity <identity>', 'Producer identity as <value>:<declared|observed|inferred|unknown>')
|
|
2368
|
+
.option('--timeout-ms <milliseconds>', `Gate timeout in milliseconds (${MIN_GATE_TIMEOUT_MS}-${MAX_GATE_TIMEOUT_MS})`)
|
|
1869
2369
|
.option('--project <path-or-name>', 'Project path or name to review; defaults to the active project')
|
|
1870
2370
|
.option('--review-scope <scope>', 'Review scope hint for the provider')
|
|
1871
2371
|
.option('--review-type <type>', 'Review type hint for the provider')
|
|
@@ -1887,6 +2387,7 @@ export function createGateCommand(overrides = {}) {
|
|
|
1887
2387
|
.option('--invocation-model <model>', 'Configured invocation model or provider-default')
|
|
1888
2388
|
.option('--invocation-reasoning-effort <effort>', 'Configured reasoning effort or provider-default')
|
|
1889
2389
|
.option('--priority <number>', 'Target priority, higher wins')
|
|
2390
|
+
.option('--timeout-ms <milliseconds>', `Target gate timeout in milliseconds (${MIN_GATE_TIMEOUT_MS}-${MAX_GATE_TIMEOUT_MS})`)
|
|
1890
2391
|
.option('--disable', 'Disable this exec target in the selected layer')
|
|
1891
2392
|
.option('--layer <layer>', 'Config layer to write: shared, local, or user')
|
|
1892
2393
|
.action(async (targetId, options, command) => {
|