@open-agent-toolkit/cli 0.1.46 → 0.1.48
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/assets/agents/oat-phase-implementer.md +202 -191
- package/assets/agents/oat-reviewer.md +11 -1
- package/assets/config/dispatch-matrix-recommendation.json +120 -13
- package/assets/docs/cli-utilities/configuration.md +172 -113
- package/assets/docs/cli-utilities/workflow-gates.md +161 -27
- package/assets/docs/contributing/skills.md +14 -8
- package/assets/docs/provider-sync/providers.md +41 -7
- package/assets/docs/provider-sync/scope-and-surface.md +2 -2
- package/assets/docs/reference/cli-reference.md +3 -1
- package/assets/docs/reference/oat-directory-structure.md +27 -26
- package/assets/docs/workflows/projects/artifacts.md +31 -1
- package/assets/docs/workflows/projects/dispatch-ceiling.md +235 -187
- package/assets/docs/workflows/projects/implementation-execution.md +283 -260
- package/assets/docs/workflows/projects/lifecycle.md +26 -5
- package/assets/docs/workflows/projects/reviews.md +27 -2
- package/assets/migration/pjm-restructure.md +1 -1
- package/assets/public-package-versions.json +4 -4
- package/assets/skills/oat-project-implement/SKILL.md +247 -133
- package/assets/skills/oat-project-import-plan/SKILL.md +173 -16
- package/assets/skills/oat-project-next/SKILL.md +2 -2
- package/assets/skills/oat-project-plan/SKILL.md +122 -92
- package/assets/skills/oat-project-plan-writing/SKILL.md +246 -15
- package/assets/skills/oat-project-quick-start/SKILL.md +157 -107
- package/assets/skills/oat-project-review-provide/SKILL.md +94 -22
- package/dist/commands/config/index.d.ts.map +1 -1
- package/dist/commands/config/index.js +64 -39
- package/dist/commands/doctor/index.d.ts.map +1 -1
- package/dist/commands/doctor/index.js +21 -6
- package/dist/commands/gate/index.d.ts +7 -1
- package/dist/commands/gate/index.d.ts.map +1 -1
- package/dist/commands/gate/index.js +544 -67
- package/dist/commands/gate/review-verdict.d.ts +16 -0
- package/dist/commands/gate/review-verdict.d.ts.map +1 -1
- package/dist/commands/gate/review-verdict.js +72 -9
- package/dist/commands/project/dispatch-ceiling/index.d.ts.map +1 -1
- package/dist/commands/project/dispatch-ceiling/index.js +452 -16
- package/dist/commands/providers/codex/materialize.d.ts.map +1 -1
- package/dist/commands/providers/codex/materialize.js +6 -1
- package/dist/commands/shared/frontmatter.d.ts +4 -0
- package/dist/commands/shared/frontmatter.d.ts.map +1 -1
- package/dist/commands/shared/frontmatter.js +23 -0
- package/dist/commands/status/index.d.ts +1 -0
- package/dist/commands/status/index.d.ts.map +1 -1
- package/dist/commands/status/index.js +10 -4
- package/dist/commands/sync/index.d.ts.map +1 -1
- package/dist/commands/sync/index.js +10 -3
- package/dist/commands/sync/sync.types.d.ts +1 -0
- package/dist/commands/sync/sync.types.d.ts.map +1 -1
- package/dist/config/oat-config.d.ts +19 -2
- package/dist/config/oat-config.d.ts.map +1 -1
- package/dist/config/oat-config.js +139 -24
- package/dist/config/resolve.d.ts +8 -0
- package/dist/config/resolve.d.ts.map +1 -1
- package/dist/config/resolve.js +66 -2
- package/dist/engine/index.d.ts +1 -1
- package/dist/engine/index.d.ts.map +1 -1
- package/dist/engine/index.js +1 -1
- package/dist/engine/scanner.d.ts +1 -0
- package/dist/engine/scanner.d.ts.map +1 -1
- package/dist/engine/scanner.js +17 -1
- package/dist/fs/paths.d.ts +4 -0
- package/dist/fs/paths.d.ts.map +1 -1
- package/dist/fs/paths.js +18 -1
- package/dist/providers/ceiling/registry.d.ts +1 -0
- package/dist/providers/ceiling/registry.d.ts.map +1 -1
- package/dist/providers/ceiling/registry.js +12 -5
- package/dist/providers/codex/codec/catalog.d.ts +14 -0
- package/dist/providers/codex/codec/catalog.d.ts.map +1 -0
- package/dist/providers/codex/codec/catalog.js +21 -0
- package/dist/providers/codex/codec/materialize.d.ts.map +1 -1
- package/dist/providers/codex/codec/materialize.js +6 -5
- package/dist/providers/codex/codec/shared.d.ts +19 -0
- package/dist/providers/codex/codec/shared.d.ts.map +1 -1
- package/dist/providers/codex/codec/shared.js +98 -5
- package/dist/providers/codex/codec/sync-extension.d.ts.map +1 -1
- package/dist/providers/codex/codec/sync-extension.js +142 -32
- package/dist/providers/identity/stamp.d.ts.map +1 -1
- package/dist/providers/identity/stamp.js +4 -0
- package/dist/shared/types.d.ts +1 -0
- package/dist/shared/types.d.ts.map +1 -1
- package/dist/shared/types.js +4 -0
- package/package.json +2 -2
|
@@ -3,16 +3,17 @@ import { createHash, randomUUID } from 'node:crypto';
|
|
|
3
3
|
import { readdir, readFile } from 'node:fs/promises';
|
|
4
4
|
import { basename, isAbsolute, join, relative } from 'node:path';
|
|
5
5
|
import { buildCommandContext, } from '../../app/command-context.js';
|
|
6
|
-
import { getFrontmatterBlock,
|
|
6
|
+
import { getFrontmatterBlock, parseFrontmatterScalarFields, parseGeneratedTime, } from '../shared/frontmatter.js';
|
|
7
7
|
import { readGlobalOptions } from '../shared/shared.utils.js';
|
|
8
8
|
import { BUILTIN_EXEC_TARGETS, readOatConfig, readOatLocalConfig, readUserConfig, writeOatConfig, writeOatLocalConfig, writeUserConfig, } from '../../config/oat-config.js';
|
|
9
|
-
import { resolveEffectiveConfig, resolveExecTargets, resolveGate, } from '../../config/resolve.js';
|
|
9
|
+
import { resolveEffectiveConfig, resolveExecTargetViews, resolveExecTargets, resolveGate, } from '../../config/resolve.js';
|
|
10
10
|
import { dirExists, fileExists } from '../../fs/io.js';
|
|
11
|
-
import { normalizeToPosixPath, resolveProjectRoot } from '../../fs/paths.js';
|
|
11
|
+
import { normalizeToPosixPath, resolveProjectRoot, validateRealPathWithinScope, } from '../../fs/paths.js';
|
|
12
12
|
import { classifyModelFamily, } from '../../providers/identity/family.js';
|
|
13
13
|
import { resolveIdentityConfidence, } from '../../providers/identity/provenance.js';
|
|
14
14
|
import { parseDispatchStamps } from '../../providers/identity/stamp.js';
|
|
15
15
|
import { Command } from 'commander';
|
|
16
|
+
import YAML from 'yaml';
|
|
16
17
|
import { parseReviewGateVerdict, severityDisplayName, } from './review-verdict.js';
|
|
17
18
|
const DEFAULT_DEPENDENCIES = {
|
|
18
19
|
buildCommandContext,
|
|
@@ -25,6 +26,7 @@ const DEFAULT_DEPENDENCIES = {
|
|
|
25
26
|
writeUserConfig,
|
|
26
27
|
resolveEffectiveConfig,
|
|
27
28
|
runProcess: runChildProcess,
|
|
29
|
+
parseReviewGateVerdict,
|
|
28
30
|
processEnv: process.env,
|
|
29
31
|
};
|
|
30
32
|
const VALID_ON_FAILURE = ['block', 'prompt', 'warn'];
|
|
@@ -53,8 +55,11 @@ const VALID_IDENTITY_PROVENANCES = [
|
|
|
53
55
|
const REVIEW_GATE_CONTEXT_NOTE = 'This review is gate-originated. If you run `oat-project-review-provide`, set `oat_review_invocation: gate` in the review artifact. Write a canonical review artifact with `### Critical`, `### Important`, `### Medium`, and `### Minor` headings in that order, using `None` for empty sections.';
|
|
54
56
|
const GATE_CHECK_TIMEOUT_MS = 5_000;
|
|
55
57
|
const GATE_EXEC_TIMEOUT_MS = 10 * 60 * 1_000;
|
|
56
|
-
function reviewGateProjectContext(
|
|
57
|
-
return
|
|
58
|
+
function reviewGateProjectContext(project) {
|
|
59
|
+
return [
|
|
60
|
+
`Resolved OAT project path: ${project.path}. Run the review for this project path.`,
|
|
61
|
+
`Project resolution source: ${project.source}.`,
|
|
62
|
+
].join('\n');
|
|
58
63
|
}
|
|
59
64
|
function assembleReviewGatePrompt(segments) {
|
|
60
65
|
return segments
|
|
@@ -62,6 +67,74 @@ function assembleReviewGatePrompt(segments) {
|
|
|
62
67
|
.filter((segment) => segment.length > 0)
|
|
63
68
|
.join('\n\n');
|
|
64
69
|
}
|
|
70
|
+
function normalizedTargetInvocation(target) {
|
|
71
|
+
const invocation = target.invocation;
|
|
72
|
+
return {
|
|
73
|
+
model: invocation?.model ?? 'unknown',
|
|
74
|
+
reasoningEffort: invocation?.reasoningEffort ?? 'unknown',
|
|
75
|
+
source: invocation ? 'exec-target-config' : 'unknown',
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
function createGateInvocationMetadata(runId, selected) {
|
|
79
|
+
const configuredInvocation = normalizedTargetInvocation(selected.target);
|
|
80
|
+
const selectedModel = selected.model;
|
|
81
|
+
const configuredModel = selected.target.invocation?.model;
|
|
82
|
+
if (selectedModel &&
|
|
83
|
+
configuredModel !== undefined &&
|
|
84
|
+
configuredModel !== selectedModel) {
|
|
85
|
+
throw new Error(`Exec target "${selected.id}" configures invocation model ${configuredModel}, but selected model ${selectedModel} would be executed.`);
|
|
86
|
+
}
|
|
87
|
+
return Object.freeze({
|
|
88
|
+
runId,
|
|
89
|
+
targetId: selected.id,
|
|
90
|
+
runtime: selected.target.runtime,
|
|
91
|
+
...configuredInvocation,
|
|
92
|
+
...(selectedModel
|
|
93
|
+
? { model: selectedModel, source: 'exec-target-config' }
|
|
94
|
+
: {}),
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
function gateInvocationPromptContext(invocation) {
|
|
98
|
+
const frontmatter = YAML.stringify({
|
|
99
|
+
oat_gate_run_id: invocation.runId,
|
|
100
|
+
oat_gate_target: invocation.targetId,
|
|
101
|
+
oat_gate_runtime: invocation.runtime,
|
|
102
|
+
oat_invocation_model: invocation.model,
|
|
103
|
+
oat_invocation_reasoning_effort: invocation.reasoningEffort,
|
|
104
|
+
oat_invocation_source: invocation.source,
|
|
105
|
+
}).trimEnd();
|
|
106
|
+
return [
|
|
107
|
+
'Gate invocation metadata (copy these exact values into the gate review artifact frontmatter):',
|
|
108
|
+
frontmatter,
|
|
109
|
+
].join('\n');
|
|
110
|
+
}
|
|
111
|
+
function corroborateGateInvocation(expected, actual, targetCorroboration) {
|
|
112
|
+
const invocationFields = [
|
|
113
|
+
['targetId', expected.targetId, actual?.targetId],
|
|
114
|
+
['runtime', expected.runtime, actual?.runtime],
|
|
115
|
+
['model', expected.model, actual?.model],
|
|
116
|
+
['reasoningEffort', expected.reasoningEffort, actual?.reasoningEffort],
|
|
117
|
+
['source', expected.source, actual?.source],
|
|
118
|
+
];
|
|
119
|
+
const invocation = invocationFields.some(([, , actualValue]) => !actualValue)
|
|
120
|
+
? 'missing'
|
|
121
|
+
: invocationFields.every(([, expectedValue, actualValue]) => expectedValue === actualValue)
|
|
122
|
+
? 'matched'
|
|
123
|
+
: 'mismatched';
|
|
124
|
+
return {
|
|
125
|
+
run: targetCorroboration.run,
|
|
126
|
+
project: targetCorroboration.project,
|
|
127
|
+
invocation,
|
|
128
|
+
expected: {
|
|
129
|
+
project: targetCorroboration.expectedProject,
|
|
130
|
+
invocation: expected,
|
|
131
|
+
},
|
|
132
|
+
actual: {
|
|
133
|
+
...targetCorroboration.actual,
|
|
134
|
+
invocation: actual ?? null,
|
|
135
|
+
},
|
|
136
|
+
};
|
|
137
|
+
}
|
|
65
138
|
async function runChildProcess(command, args, options) {
|
|
66
139
|
return new Promise((resolve, reject) => {
|
|
67
140
|
let timedOut = false;
|
|
@@ -244,7 +317,26 @@ function parseExecTargetConfig(options) {
|
|
|
244
317
|
return {
|
|
245
318
|
runtime: trimRequired(options.runtime ?? '', '--runtime'),
|
|
246
319
|
baseCommand: parseArgvJson(baseCommandJson, '--base-command-json'),
|
|
247
|
-
|
|
320
|
+
...(options.priority !== undefined
|
|
321
|
+
? { priority: parseNumericFlag(options.priority, '--priority', 0) }
|
|
322
|
+
: {}),
|
|
323
|
+
...(options.invocationModel !== undefined ||
|
|
324
|
+
options.invocationReasoningEffort !== undefined
|
|
325
|
+
? {
|
|
326
|
+
invocation: {
|
|
327
|
+
...(options.invocationModel !== undefined
|
|
328
|
+
? {
|
|
329
|
+
model: trimRequired(options.invocationModel, '--invocation-model'),
|
|
330
|
+
}
|
|
331
|
+
: {}),
|
|
332
|
+
...(options.invocationReasoningEffort !== undefined
|
|
333
|
+
? {
|
|
334
|
+
reasoningEffort: trimRequired(options.invocationReasoningEffort, '--invocation-reasoning-effort'),
|
|
335
|
+
}
|
|
336
|
+
: {}),
|
|
337
|
+
},
|
|
338
|
+
}
|
|
339
|
+
: {}),
|
|
248
340
|
...(options.hostDetectionJson !== undefined
|
|
249
341
|
? {
|
|
250
342
|
hostDetectionCommand: parseOptionalArgvJson(options.hostDetectionJson, '--host-detection-json'),
|
|
@@ -286,13 +378,32 @@ function unsetSkillGate(config, skillName) {
|
|
|
286
378
|
});
|
|
287
379
|
}
|
|
288
380
|
function setExecTarget(config, targetId, target) {
|
|
289
|
-
return updateWorkflowGates(config, (gates) =>
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
381
|
+
return updateWorkflowGates(config, (gates) => {
|
|
382
|
+
const existing = gates.execTargets?.[targetId];
|
|
383
|
+
const value = target === null
|
|
384
|
+
? target
|
|
385
|
+
: existing === null || existing === undefined
|
|
386
|
+
? { priority: 0, ...target }
|
|
387
|
+
: {
|
|
388
|
+
...existing,
|
|
389
|
+
...target,
|
|
390
|
+
...(existing.invocation || target.invocation
|
|
391
|
+
? {
|
|
392
|
+
invocation: {
|
|
393
|
+
...existing.invocation,
|
|
394
|
+
...target.invocation,
|
|
395
|
+
},
|
|
396
|
+
}
|
|
397
|
+
: {}),
|
|
398
|
+
};
|
|
399
|
+
return {
|
|
400
|
+
...gates,
|
|
401
|
+
execTargets: {
|
|
402
|
+
...gates.execTargets,
|
|
403
|
+
[targetId]: value,
|
|
404
|
+
},
|
|
405
|
+
};
|
|
406
|
+
});
|
|
296
407
|
}
|
|
297
408
|
function unsetExecTarget(config, targetId) {
|
|
298
409
|
return updateWorkflowGates(config, (gates) => {
|
|
@@ -322,6 +433,7 @@ function cloneExecTarget(target) {
|
|
|
322
433
|
runtime: target.runtime,
|
|
323
434
|
baseCommand: [...target.baseCommand],
|
|
324
435
|
priority: target.priority,
|
|
436
|
+
...(target.invocation ? { invocation: { ...target.invocation } } : {}),
|
|
325
437
|
...(target.models ? { models: [...target.models] } : {}),
|
|
326
438
|
...(target.hostDetectionCommand
|
|
327
439
|
? { hostDetectionCommand: [...target.hostDetectionCommand] }
|
|
@@ -363,17 +475,32 @@ function parseProducerIdentityOption(value) {
|
|
|
363
475
|
}
|
|
364
476
|
return identityFromRecords([{ value: producer, provenance: provenance }], 'flag');
|
|
365
477
|
}
|
|
366
|
-
function
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
const
|
|
371
|
-
|
|
478
|
+
function identityFromStamp(stamp) {
|
|
479
|
+
return identityFromRecords([{ value: stamp.producer, provenance: stamp.provenance }], 'stamp');
|
|
480
|
+
}
|
|
481
|
+
function exactIdentityFromStamp(stamp) {
|
|
482
|
+
const identity = identityFromStamp(stamp);
|
|
483
|
+
return identity.diversityClaimable && identity.family !== 'unknown'
|
|
484
|
+
? identity
|
|
485
|
+
: unknownProducerIdentity();
|
|
486
|
+
}
|
|
487
|
+
function aggregateIdentityFromStamps(stamps) {
|
|
488
|
+
if (stamps.length === 0) {
|
|
372
489
|
return unknownProducerIdentity();
|
|
373
490
|
}
|
|
491
|
+
const identities = stamps.map(identityFromStamp);
|
|
492
|
+
const avoidFamilies = [
|
|
493
|
+
...new Set(identities.flatMap((identity) => identity.diversityClaimable && identity.family !== 'unknown'
|
|
494
|
+
? [identity.family]
|
|
495
|
+
: [])),
|
|
496
|
+
];
|
|
374
497
|
return {
|
|
375
|
-
...
|
|
376
|
-
avoidFamilies
|
|
498
|
+
...unknownProducerIdentity(),
|
|
499
|
+
avoidFamilies,
|
|
500
|
+
contributingScopes: [...new Set(stamps.map((stamp) => stamp.scope))],
|
|
501
|
+
contributingStampCount: stamps.length,
|
|
502
|
+
diversityClaimable: avoidFamilies.length > 0,
|
|
503
|
+
source: 'aggregated-stamps',
|
|
377
504
|
};
|
|
378
505
|
}
|
|
379
506
|
function phaseNumber(scope) {
|
|
@@ -383,7 +510,7 @@ function phaseNumber(scope) {
|
|
|
383
510
|
}
|
|
384
511
|
function reviewScopeRange(scope) {
|
|
385
512
|
if (scope === 'final') {
|
|
386
|
-
return { start:
|
|
513
|
+
return { start: 0, end: Number.MAX_SAFE_INTEGER };
|
|
387
514
|
}
|
|
388
515
|
const range = scope.match(/^p(\d+)-p(\d+)$/);
|
|
389
516
|
if (!range) {
|
|
@@ -417,13 +544,15 @@ async function readStampedProducerIdentity(options) {
|
|
|
417
544
|
return unknownProducerIdentity();
|
|
418
545
|
}
|
|
419
546
|
const stamps = parseDispatchStamps(markdown).filter((candidate) => candidate.role === 'implementer' || candidate.role === 'fix');
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
return
|
|
547
|
+
if (!reviewScopeRange(scope)) {
|
|
548
|
+
const exactStamp = [...stamps]
|
|
549
|
+
.reverse()
|
|
550
|
+
.find((candidate) => candidate.scope === scope);
|
|
551
|
+
return exactStamp
|
|
552
|
+
? exactIdentityFromStamp(exactStamp)
|
|
553
|
+
: unknownProducerIdentity();
|
|
425
554
|
}
|
|
426
|
-
return
|
|
555
|
+
return aggregateIdentityFromStamps(stamps.filter((candidate) => stampInReviewScope(candidate.scope, scope)));
|
|
427
556
|
}
|
|
428
557
|
async function resolveReviewProducerIdentity(options) {
|
|
429
558
|
if (options.explicit?.trim()) {
|
|
@@ -480,7 +609,7 @@ function expandExecTargetCandidates(id, target) {
|
|
|
480
609
|
}));
|
|
481
610
|
}
|
|
482
611
|
function producerHasKnownFamily(identity) {
|
|
483
|
-
return identity.
|
|
612
|
+
return identity.avoidFamilies.length > 0;
|
|
484
613
|
}
|
|
485
614
|
function shouldAttemptNoDiverseFallback(avoid) {
|
|
486
615
|
return avoid === 'same-family';
|
|
@@ -493,7 +622,9 @@ function achievedDiversity(selected, producerIdentity) {
|
|
|
493
622
|
!producerIdentity.avoidFamilies.includes(selected.family)) {
|
|
494
623
|
return 'different-family';
|
|
495
624
|
}
|
|
496
|
-
if (
|
|
625
|
+
if (producerIdentity.source !== 'aggregated-stamps' &&
|
|
626
|
+
selected.model &&
|
|
627
|
+
selected.model !== producerIdentity.value) {
|
|
497
628
|
return 'degraded-to-different-slug';
|
|
498
629
|
}
|
|
499
630
|
return 'same-family - no diverse target available';
|
|
@@ -523,6 +654,14 @@ function attachDiversityMetadata(selected, avoid, producerIdentity) {
|
|
|
523
654
|
family: producerIdentity.family,
|
|
524
655
|
source: producerIdentity.source,
|
|
525
656
|
avoidFamilies: producerIdentity.avoidFamilies,
|
|
657
|
+
...(producerIdentity.contributingScopes
|
|
658
|
+
? { contributingScopes: producerIdentity.contributingScopes }
|
|
659
|
+
: {}),
|
|
660
|
+
...(producerIdentity.contributingStampCount === undefined
|
|
661
|
+
? {}
|
|
662
|
+
: {
|
|
663
|
+
contributingStampCount: producerIdentity.contributingStampCount,
|
|
664
|
+
}),
|
|
526
665
|
},
|
|
527
666
|
reviewer: {
|
|
528
667
|
target: selected.id,
|
|
@@ -719,11 +858,21 @@ async function listProjectCandidates(repoRoot, projectsRoot) {
|
|
|
719
858
|
async function assertProjectPath(repoRoot, projectPath, source) {
|
|
720
859
|
const normalizedPath = normalizeRepoRelativeProjectPath(repoRoot, projectPath);
|
|
721
860
|
const absolutePath = join(repoRoot, normalizedPath);
|
|
722
|
-
|
|
723
|
-
|
|
861
|
+
let realProjectPath;
|
|
862
|
+
let canonicalPath;
|
|
863
|
+
try {
|
|
864
|
+
const validated = await validateRealPathWithinScope(absolutePath, repoRoot);
|
|
865
|
+
realProjectPath = validated.realPath;
|
|
866
|
+
canonicalPath = normalizeRepoRelativeProjectPath(validated.realScopeRoot, validated.realPath);
|
|
867
|
+
}
|
|
868
|
+
catch {
|
|
869
|
+
throw new Error(`${source} project "${projectPath}" must resolve inside the current repository through a readable real path.`);
|
|
870
|
+
}
|
|
871
|
+
if (!(await dirExists(realProjectPath)) ||
|
|
872
|
+
!(await fileExists(join(realProjectPath, 'state.md')))) {
|
|
724
873
|
throw new Error(`${source} project "${projectPath}" does not resolve to a project directory containing state.md.`);
|
|
725
874
|
}
|
|
726
|
-
return
|
|
875
|
+
return canonicalPath;
|
|
727
876
|
}
|
|
728
877
|
async function resolveExplicitReviewProject(repoRoot, projectsRoot, projectValue) {
|
|
729
878
|
const trimmed = projectValue.trim();
|
|
@@ -744,16 +893,25 @@ async function resolveExplicitReviewProject(repoRoot, projectsRoot, projectValue
|
|
|
744
893
|
}
|
|
745
894
|
return assertProjectPath(repoRoot, `${projectsRoot}/${trimmed}`, '--project');
|
|
746
895
|
}
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
896
|
+
function resolvedProjectsRoot(effective) {
|
|
897
|
+
return String(effective.resolved['projects.root']?.value ??
|
|
898
|
+
effective.shared.projects?.root ??
|
|
750
899
|
'.oat/projects/shared');
|
|
900
|
+
}
|
|
901
|
+
async function resolveReviewProject(options) {
|
|
902
|
+
const projectsRoot = resolvedProjectsRoot(options.effective);
|
|
751
903
|
if (options.project !== undefined) {
|
|
752
|
-
return
|
|
904
|
+
return {
|
|
905
|
+
path: await resolveExplicitReviewProject(options.repoRoot, projectsRoot, options.project),
|
|
906
|
+
source: 'declared',
|
|
907
|
+
};
|
|
753
908
|
}
|
|
754
909
|
const activeProject = options.effective.local.activeProject?.trim();
|
|
755
910
|
if (activeProject) {
|
|
756
|
-
return
|
|
911
|
+
return {
|
|
912
|
+
path: await assertProjectPath(options.repoRoot, activeProject, 'Active'),
|
|
913
|
+
source: 'active-project',
|
|
914
|
+
};
|
|
757
915
|
}
|
|
758
916
|
const candidates = await listProjectCandidates(options.repoRoot, projectsRoot);
|
|
759
917
|
if (candidates.length === 0) {
|
|
@@ -762,7 +920,10 @@ async function resolveReviewProject(options) {
|
|
|
762
920
|
if (candidates.length > 1) {
|
|
763
921
|
throw new Error(`Multiple OAT projects could be resolved for gate review: ${candidates.join(', ')}. Pass --project <path-or-name>.`);
|
|
764
922
|
}
|
|
765
|
-
return
|
|
923
|
+
return {
|
|
924
|
+
path: candidates[0],
|
|
925
|
+
source: 'single-candidate',
|
|
926
|
+
};
|
|
766
927
|
}
|
|
767
928
|
function reviewGateLifecycleRank(scope) {
|
|
768
929
|
const normalizedScope = scope.trim().toLowerCase();
|
|
@@ -778,21 +939,27 @@ function reviewGateLifecycleRank(scope) {
|
|
|
778
939
|
const latestTask = tasks.length > 0 ? Math.max(...tasks) : 9999;
|
|
779
940
|
return latestPhase * 10_000 + latestTask;
|
|
780
941
|
}
|
|
781
|
-
async function readReviewGateArtifactCandidate(repoRoot, relativePath) {
|
|
942
|
+
async function readReviewGateArtifactCandidate(repoRoot, containingProject, relativePath) {
|
|
782
943
|
const content = await readFile(join(repoRoot, relativePath), 'utf8');
|
|
783
944
|
const frontmatter = getFrontmatterBlock(content);
|
|
784
945
|
if (!frontmatter) {
|
|
785
946
|
return null;
|
|
786
947
|
}
|
|
787
|
-
const
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
const
|
|
948
|
+
const scalarFields = parseFrontmatterScalarFields(frontmatter, [
|
|
949
|
+
'oat_generated_at',
|
|
950
|
+
'oat_review_scope',
|
|
951
|
+
'oat_gate_run_id',
|
|
952
|
+
'oat_project',
|
|
953
|
+
]);
|
|
954
|
+
const frontmatterString = (key) => scalarFields.values[key] ?? null;
|
|
955
|
+
const generatedAt = frontmatterString('oat_generated_at');
|
|
956
|
+
const parsedGeneratedTime = generatedAt
|
|
957
|
+
? parseGeneratedTime(generatedAt)
|
|
958
|
+
: Number.NaN;
|
|
959
|
+
const generatedTime = Number.isNaN(parsedGeneratedTime)
|
|
960
|
+
? Number.NEGATIVE_INFINITY
|
|
961
|
+
: parsedGeneratedTime;
|
|
962
|
+
const scope = frontmatterString('oat_review_scope') ?? '';
|
|
796
963
|
return {
|
|
797
964
|
path: relativePath,
|
|
798
965
|
scope,
|
|
@@ -800,9 +967,13 @@ async function readReviewGateArtifactCandidate(repoRoot, relativePath) {
|
|
|
800
967
|
kind: 'project',
|
|
801
968
|
archived: false,
|
|
802
969
|
actionable: true,
|
|
970
|
+
containingProject,
|
|
971
|
+
gateRunId: frontmatterString('oat_gate_run_id'),
|
|
972
|
+
artifactProject: frontmatterString('oat_project'),
|
|
803
973
|
generatedTime,
|
|
804
974
|
lifecycleRank: reviewGateLifecycleRank(scope),
|
|
805
975
|
signature: createHash('sha256').update(content).digest('hex'),
|
|
976
|
+
content,
|
|
806
977
|
};
|
|
807
978
|
}
|
|
808
979
|
function sortReviewGateArtifacts(left, right) {
|
|
@@ -834,14 +1005,77 @@ async function listActiveProjectReviewCandidates(options) {
|
|
|
834
1005
|
}
|
|
835
1006
|
return (await Promise.all(entries
|
|
836
1007
|
.filter((entry) => entry.isFile() && entry.name.endsWith('.md'))
|
|
837
|
-
.map((entry) => readReviewGateArtifactCandidate(options.repoRoot, normalizeToPosixPath(join(reviewsDir, entry.name))))))
|
|
1008
|
+
.map((entry) => readReviewGateArtifactCandidate(options.repoRoot, projectPath, normalizeToPosixPath(join(reviewsDir, entry.name))))))
|
|
838
1009
|
.filter((candidate) => candidate !== null)
|
|
839
1010
|
.sort(sortReviewGateArtifacts);
|
|
840
1011
|
}
|
|
1012
|
+
async function listReviewGateArtifactCandidates(options) {
|
|
1013
|
+
const configuredProjects = await listProjectCandidates(options.repoRoot, resolvedProjectsRoot(options.effective));
|
|
1014
|
+
const projectPaths = [
|
|
1015
|
+
...new Set([...configuredProjects, options.reviewProject.path]),
|
|
1016
|
+
];
|
|
1017
|
+
const candidates = await Promise.all(projectPaths.map((projectPath) => listActiveProjectReviewCandidates({
|
|
1018
|
+
repoRoot: options.repoRoot,
|
|
1019
|
+
projectPath,
|
|
1020
|
+
})));
|
|
1021
|
+
return candidates.flat().sort(sortReviewGateArtifacts);
|
|
1022
|
+
}
|
|
841
1023
|
function findProducedReviewArtifact(before, after) {
|
|
842
1024
|
const beforeSignatures = new Map(before.map((candidate) => [candidate.path, candidate.signature]));
|
|
843
1025
|
return (after.find((candidate) => beforeSignatures.get(candidate.path) !== candidate.signature) ?? null);
|
|
844
1026
|
}
|
|
1027
|
+
function resolveRunCorrelatedReviewArtifact(options) {
|
|
1028
|
+
const matches = options.after.filter((candidate) => candidate.gateRunId === options.runId);
|
|
1029
|
+
const artifact = matches.length === 1 ? matches[0] : null;
|
|
1030
|
+
return {
|
|
1031
|
+
artifact,
|
|
1032
|
+
diagnosticArtifact: artifact ?? findProducedReviewArtifact(options.before, options.after),
|
|
1033
|
+
matchingArtifactPaths: matches.map((candidate) => candidate.path).sort(),
|
|
1034
|
+
};
|
|
1035
|
+
}
|
|
1036
|
+
function normalizeArtifactProject(repoRoot, artifactProject) {
|
|
1037
|
+
if (!artifactProject) {
|
|
1038
|
+
return null;
|
|
1039
|
+
}
|
|
1040
|
+
try {
|
|
1041
|
+
const normalized = normalizeRepoRelativeProjectPath(repoRoot, artifactProject);
|
|
1042
|
+
return normalized || null;
|
|
1043
|
+
}
|
|
1044
|
+
catch {
|
|
1045
|
+
return null;
|
|
1046
|
+
}
|
|
1047
|
+
}
|
|
1048
|
+
function corroborateReviewTarget(options) {
|
|
1049
|
+
const actualArtifact = options.artifact ?? options.diagnosticArtifact;
|
|
1050
|
+
const normalizedArtifactProject = normalizeArtifactProject(options.repoRoot, actualArtifact?.artifactProject ?? null);
|
|
1051
|
+
const run = options.matchingArtifactPaths.length > 1
|
|
1052
|
+
? 'mismatched'
|
|
1053
|
+
: actualArtifact?.gateRunId === options.gateInvocation.runId
|
|
1054
|
+
? 'matched'
|
|
1055
|
+
: actualArtifact?.gateRunId
|
|
1056
|
+
? 'mismatched'
|
|
1057
|
+
: 'missing';
|
|
1058
|
+
let project = 'ambient';
|
|
1059
|
+
if (options.reviewProject.source === 'declared') {
|
|
1060
|
+
project = !actualArtifact?.artifactProject
|
|
1061
|
+
? 'missing'
|
|
1062
|
+
: actualArtifact.containingProject === options.reviewProject.path &&
|
|
1063
|
+
normalizedArtifactProject === options.reviewProject.path
|
|
1064
|
+
? 'matched'
|
|
1065
|
+
: 'mismatched';
|
|
1066
|
+
}
|
|
1067
|
+
return {
|
|
1068
|
+
run,
|
|
1069
|
+
project,
|
|
1070
|
+
expectedProject: options.reviewProject.path,
|
|
1071
|
+
actual: {
|
|
1072
|
+
containingProject: actualArtifact?.containingProject ?? null,
|
|
1073
|
+
artifactProject: actualArtifact?.artifactProject ?? null,
|
|
1074
|
+
normalizedArtifactProject,
|
|
1075
|
+
matchingArtifactPaths: options.matchingArtifactPaths,
|
|
1076
|
+
},
|
|
1077
|
+
};
|
|
1078
|
+
}
|
|
845
1079
|
function reviewBlocksAtThreshold(verdict, threshold) {
|
|
846
1080
|
if (verdict.counts.critical > 0) {
|
|
847
1081
|
return true;
|
|
@@ -890,14 +1124,16 @@ function reviewGateOutcome(payload) {
|
|
|
890
1124
|
function writeReviewGateResult(context, payload) {
|
|
891
1125
|
const outcome = reviewGateOutcome(payload);
|
|
892
1126
|
if (context.json) {
|
|
893
|
-
context.logger.json({ outcome, ...payload });
|
|
1127
|
+
context.logger.json({ outcome, ...payload, receiveEligible: true });
|
|
894
1128
|
return;
|
|
895
1129
|
}
|
|
896
1130
|
if (outcome === 'review_completed_blocking_findings') {
|
|
897
1131
|
context.logger.info('Review completed and found blocking issues.');
|
|
898
1132
|
}
|
|
899
1133
|
else if (outcome === 'review_completed_artifact_normalized_gate_passed') {
|
|
900
|
-
context.logger.info(
|
|
1134
|
+
context.logger.info(payload.normalization?.persisted
|
|
1135
|
+
? 'Review completed, artifact was normalized, and gate passed.'
|
|
1136
|
+
: 'Review completed, the immutable artifact snapshot was normalized in memory, and gate passed.');
|
|
901
1137
|
}
|
|
902
1138
|
else {
|
|
903
1139
|
context.logger.info('Review completed and gate passed.');
|
|
@@ -905,7 +1141,7 @@ function writeReviewGateResult(context, payload) {
|
|
|
905
1141
|
context.logger.info(`Run: ${payload.runId} (generated ${payload.generatedAt})`);
|
|
906
1142
|
context.logger.info(`Review artifact: ${payload.artifactPath}`);
|
|
907
1143
|
if (payload.normalization) {
|
|
908
|
-
context.logger.info(
|
|
1144
|
+
context.logger.info(`${payload.normalization.persisted ? 'Artifact normalized' : 'Artifact snapshot normalized in memory; source file unchanged'}: inserted ${payload.normalization.insertedSeverities.map((severity) => severityDisplayName(severity)).join(', ')} empty Findings section(s).`);
|
|
909
1145
|
}
|
|
910
1146
|
context.logger.info(`Verdict: ${payload.status} (critical=${payload.counts.critical}, important=${payload.counts.important}, medium=${payload.counts.medium}, minor=${payload.counts.minor})`);
|
|
911
1147
|
if (payload.diversity) {
|
|
@@ -927,6 +1163,11 @@ function writeReviewGateExecutionFailure(context, payload) {
|
|
|
927
1163
|
runId: payload.runId,
|
|
928
1164
|
target: payload.target,
|
|
929
1165
|
project: payload.project,
|
|
1166
|
+
projectResolutionSource: payload.projectResolutionSource,
|
|
1167
|
+
gateInvocation: payload.gateInvocation,
|
|
1168
|
+
...(payload.corroboration
|
|
1169
|
+
? { corroboration: payload.corroboration }
|
|
1170
|
+
: {}),
|
|
930
1171
|
exitCode: payload.exitCode,
|
|
931
1172
|
timedOut: payload.timedOut ?? false,
|
|
932
1173
|
...(payload.timeoutMs !== undefined
|
|
@@ -938,6 +1179,25 @@ function writeReviewGateExecutionFailure(context, payload) {
|
|
|
938
1179
|
}
|
|
939
1180
|
context.logger.error(message);
|
|
940
1181
|
}
|
|
1182
|
+
function writeReviewGateUnexpectedFailure(context, payload) {
|
|
1183
|
+
const message = payload.error instanceof Error
|
|
1184
|
+
? payload.error.message
|
|
1185
|
+
: String(payload.error);
|
|
1186
|
+
if (context.json) {
|
|
1187
|
+
context.logger.json({
|
|
1188
|
+
status: 'review_failed',
|
|
1189
|
+
outcome: 'unexpected_post_selection_failure',
|
|
1190
|
+
runId: payload.gateInvocation.runId,
|
|
1191
|
+
target: payload.target,
|
|
1192
|
+
project: payload.project,
|
|
1193
|
+
projectResolutionSource: payload.projectResolutionSource,
|
|
1194
|
+
gateInvocation: payload.gateInvocation,
|
|
1195
|
+
message,
|
|
1196
|
+
});
|
|
1197
|
+
return;
|
|
1198
|
+
}
|
|
1199
|
+
context.logger.error(`Review failed after target selection for ${payload.target}: ${message}`);
|
|
1200
|
+
}
|
|
941
1201
|
function writeReviewGateArtifactValidationFailure(context, payload) {
|
|
942
1202
|
if (context.json) {
|
|
943
1203
|
context.logger.json({
|
|
@@ -946,8 +1206,15 @@ function writeReviewGateArtifactValidationFailure(context, payload) {
|
|
|
946
1206
|
runId: payload.runId,
|
|
947
1207
|
target: payload.target,
|
|
948
1208
|
project: payload.project,
|
|
1209
|
+
projectResolutionSource: payload.projectResolutionSource,
|
|
949
1210
|
artifactPath: payload.artifactPath,
|
|
950
1211
|
generatedAt: payload.generatedAt,
|
|
1212
|
+
gateInvocation: payload.gateInvocation,
|
|
1213
|
+
...(payload.corroboration
|
|
1214
|
+
? { corroboration: payload.corroboration }
|
|
1215
|
+
: {}),
|
|
1216
|
+
receiveEligible: false,
|
|
1217
|
+
handoff: null,
|
|
951
1218
|
message: payload.message,
|
|
952
1219
|
recovery: payload.recovery,
|
|
953
1220
|
});
|
|
@@ -956,6 +1223,43 @@ function writeReviewGateArtifactValidationFailure(context, payload) {
|
|
|
956
1223
|
context.logger.error(`Review completed but artifact validation failed: ${payload.message}`);
|
|
957
1224
|
context.logger.error(payload.recovery);
|
|
958
1225
|
}
|
|
1226
|
+
function writeReviewGateTargetingFailure(context, payload) {
|
|
1227
|
+
const corroboration = {
|
|
1228
|
+
run: payload.corroboration.run,
|
|
1229
|
+
project: payload.corroboration.project,
|
|
1230
|
+
invocation: 'missing',
|
|
1231
|
+
expected: {
|
|
1232
|
+
project: payload.corroboration.expectedProject,
|
|
1233
|
+
invocation: payload.gateInvocation,
|
|
1234
|
+
},
|
|
1235
|
+
actual: {
|
|
1236
|
+
...payload.corroboration.actual,
|
|
1237
|
+
invocation: null,
|
|
1238
|
+
},
|
|
1239
|
+
};
|
|
1240
|
+
if (context.json) {
|
|
1241
|
+
context.logger.json({
|
|
1242
|
+
status: 'targeting_correlation_failed',
|
|
1243
|
+
outcome: 'review_completed_targeting_correlation_failed',
|
|
1244
|
+
runId: payload.runId,
|
|
1245
|
+
target: payload.target,
|
|
1246
|
+
project: payload.project,
|
|
1247
|
+
projectResolutionSource: payload.projectResolutionSource,
|
|
1248
|
+
artifactPath: payload.artifactPath,
|
|
1249
|
+
generatedAt: payload.generatedAt,
|
|
1250
|
+
gateInvocation: payload.gateInvocation,
|
|
1251
|
+
corroboration,
|
|
1252
|
+
receiveEligible: false,
|
|
1253
|
+
remediable: false,
|
|
1254
|
+
handoff: null,
|
|
1255
|
+
message: payload.message,
|
|
1256
|
+
});
|
|
1257
|
+
return;
|
|
1258
|
+
}
|
|
1259
|
+
context.logger.error(`Review completed but target correlation failed: ${payload.message}`);
|
|
1260
|
+
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'}.`);
|
|
1261
|
+
context.logger.error('This targeting failure is not receive-eligible and must be corrected before severity or invocation remediation.');
|
|
1262
|
+
}
|
|
959
1263
|
async function updateConfigLayer(context, layer, dependencies, mutate) {
|
|
960
1264
|
const repoRoot = await dependencies.resolveProjectRoot(context.cwd);
|
|
961
1265
|
const userConfigDir = join(context.home, '.oat');
|
|
@@ -1045,6 +1349,39 @@ async function runTargetUnset(targetId, options, context, dependencies) {
|
|
|
1045
1349
|
writeError(context, error);
|
|
1046
1350
|
}
|
|
1047
1351
|
}
|
|
1352
|
+
async function runTargetList(context, dependencies) {
|
|
1353
|
+
try {
|
|
1354
|
+
const views = resolveExecTargetViews(await readEffectiveConfig(context, dependencies));
|
|
1355
|
+
const targets = await Promise.all(Object.entries(views)
|
|
1356
|
+
.sort(([leftId, left], [rightId, right]) => {
|
|
1357
|
+
const priority = right.target.priority - left.target.priority;
|
|
1358
|
+
return priority === 0 ? leftId.localeCompare(rightId) : priority;
|
|
1359
|
+
})
|
|
1360
|
+
.map(async ([id, view]) => {
|
|
1361
|
+
const availabilityCommand = view.target.availabilityCommand;
|
|
1362
|
+
const available = view.enabled &&
|
|
1363
|
+
(!availabilityCommand ||
|
|
1364
|
+
(await checkArgv(availabilityCommand, 'availability', context, dependencies)));
|
|
1365
|
+
const invocation = normalizedTargetInvocation(view.target);
|
|
1366
|
+
return {
|
|
1367
|
+
id,
|
|
1368
|
+
runtime: view.target.runtime,
|
|
1369
|
+
origin: view.origin,
|
|
1370
|
+
explicitlyConfigured: view.explicitlyConfigured,
|
|
1371
|
+
enabled: view.enabled,
|
|
1372
|
+
available,
|
|
1373
|
+
invocation: {
|
|
1374
|
+
...invocation,
|
|
1375
|
+
},
|
|
1376
|
+
};
|
|
1377
|
+
}));
|
|
1378
|
+
writeSuccess(context, { targets });
|
|
1379
|
+
process.exitCode = 0;
|
|
1380
|
+
}
|
|
1381
|
+
catch (error) {
|
|
1382
|
+
writeError(context, error);
|
|
1383
|
+
}
|
|
1384
|
+
}
|
|
1048
1385
|
async function runCrossProviderExec(prompt, options, context, dependencies) {
|
|
1049
1386
|
try {
|
|
1050
1387
|
const effective = await readEffectiveConfig(context, dependencies);
|
|
@@ -1061,15 +1398,17 @@ async function runCrossProviderExec(prompt, options, context, dependencies) {
|
|
|
1061
1398
|
}
|
|
1062
1399
|
async function runReviewGate(prompt, options, context, dependencies) {
|
|
1063
1400
|
const runId = randomUUID();
|
|
1401
|
+
let postSelectionContext;
|
|
1064
1402
|
try {
|
|
1065
1403
|
const repoRoot = await dependencies.resolveProjectRoot(context.cwd);
|
|
1066
1404
|
const userConfigDir = join(context.home, '.oat');
|
|
1067
1405
|
const effective = await dependencies.resolveEffectiveConfig(repoRoot, userConfigDir, dependencies.processEnv);
|
|
1068
|
-
const
|
|
1406
|
+
const reviewProject = await resolveReviewProject({
|
|
1069
1407
|
repoRoot,
|
|
1070
1408
|
effective,
|
|
1071
1409
|
project: options.project,
|
|
1072
1410
|
});
|
|
1411
|
+
const projectPath = reviewProject.path;
|
|
1073
1412
|
const targets = resolveExecTargets(effective);
|
|
1074
1413
|
const producerIdentity = await resolveReviewProducerIdentity({
|
|
1075
1414
|
explicit: options.producerIdentity,
|
|
@@ -1078,14 +1417,23 @@ async function runReviewGate(prompt, options, context, dependencies) {
|
|
|
1078
1417
|
reviewScope: options.reviewScope,
|
|
1079
1418
|
});
|
|
1080
1419
|
const selected = await resolveSelectedExecTarget(targets, options, producerIdentity, context, dependencies);
|
|
1420
|
+
const gateInvocation = createGateInvocationMetadata(runId, selected);
|
|
1421
|
+
postSelectionContext = {
|
|
1422
|
+
project: projectPath,
|
|
1423
|
+
projectResolutionSource: reviewProject.source,
|
|
1424
|
+
target: selected.id,
|
|
1425
|
+
gateInvocation,
|
|
1426
|
+
};
|
|
1081
1427
|
const threshold = parseReviewGateThreshold(options.exitNonzeroOn);
|
|
1082
|
-
const before = await
|
|
1428
|
+
const before = await listReviewGateArtifactCandidates({
|
|
1083
1429
|
repoRoot,
|
|
1084
|
-
|
|
1430
|
+
effective,
|
|
1431
|
+
reviewProject,
|
|
1085
1432
|
});
|
|
1086
1433
|
const reviewPrompt = assembleReviewGatePrompt([
|
|
1087
1434
|
REVIEW_GATE_CONTEXT_NOTE,
|
|
1088
|
-
reviewGateProjectContext(
|
|
1435
|
+
reviewGateProjectContext(reviewProject),
|
|
1436
|
+
gateInvocationPromptContext(gateInvocation),
|
|
1089
1437
|
...(options.reviewType?.trim()
|
|
1090
1438
|
? [`Review type: ${options.reviewType.trim()}.`]
|
|
1091
1439
|
: []),
|
|
@@ -1101,35 +1449,101 @@ async function runReviewGate(prompt, options, context, dependencies) {
|
|
|
1101
1449
|
runId,
|
|
1102
1450
|
target: selected.id,
|
|
1103
1451
|
project: projectPath,
|
|
1452
|
+
projectResolutionSource: reviewProject.source,
|
|
1104
1453
|
exitCode: childExitCode,
|
|
1105
1454
|
timedOut: childResult.timedOut ?? false,
|
|
1106
1455
|
timeoutMs: resolveGateExecTimeoutMs(dependencies.processEnv),
|
|
1456
|
+
gateInvocation,
|
|
1107
1457
|
});
|
|
1108
1458
|
process.exitCode = childExitCode;
|
|
1109
1459
|
return;
|
|
1110
1460
|
}
|
|
1111
|
-
const after = await
|
|
1461
|
+
const after = await listReviewGateArtifactCandidates({
|
|
1112
1462
|
repoRoot,
|
|
1113
|
-
|
|
1463
|
+
effective,
|
|
1464
|
+
reviewProject,
|
|
1465
|
+
});
|
|
1466
|
+
const artifactResolution = resolveRunCorrelatedReviewArtifact({
|
|
1467
|
+
runId,
|
|
1468
|
+
before,
|
|
1469
|
+
after,
|
|
1470
|
+
});
|
|
1471
|
+
const initialTargetCorroboration = corroborateReviewTarget({
|
|
1472
|
+
repoRoot,
|
|
1473
|
+
reviewProject,
|
|
1474
|
+
gateInvocation,
|
|
1475
|
+
artifact: artifactResolution.artifact,
|
|
1476
|
+
diagnosticArtifact: artifactResolution.diagnosticArtifact,
|
|
1477
|
+
matchingArtifactPaths: artifactResolution.matchingArtifactPaths,
|
|
1114
1478
|
});
|
|
1115
|
-
const producedArtifact =
|
|
1479
|
+
const producedArtifact = artifactResolution.artifact;
|
|
1116
1480
|
if (!producedArtifact) {
|
|
1481
|
+
const diagnosticArtifact = artifactResolution.diagnosticArtifact;
|
|
1482
|
+
const message = artifactResolution.matchingArtifactPaths.length > 1
|
|
1483
|
+
? `Multiple direct review artifacts carried gate run ID ${runId}.`
|
|
1484
|
+
: initialTargetCorroboration.run === 'mismatched'
|
|
1485
|
+
? `The changed review artifact did not carry the expected gate run ID ${runId}.`
|
|
1486
|
+
: `No direct active project review artifact carried gate run ID ${runId}.`;
|
|
1487
|
+
writeReviewGateTargetingFailure(context, {
|
|
1488
|
+
runId,
|
|
1489
|
+
target: selected.id,
|
|
1490
|
+
project: projectPath,
|
|
1491
|
+
projectResolutionSource: reviewProject.source,
|
|
1492
|
+
artifactPath: diagnosticArtifact?.path ?? null,
|
|
1493
|
+
generatedAt: diagnosticArtifact?.generatedAt ?? null,
|
|
1494
|
+
message,
|
|
1495
|
+
gateInvocation,
|
|
1496
|
+
corroboration: initialTargetCorroboration,
|
|
1497
|
+
});
|
|
1498
|
+
process.exitCode = 1;
|
|
1499
|
+
return;
|
|
1500
|
+
}
|
|
1501
|
+
if (producedArtifact.containingProject !== reviewProject.path ||
|
|
1502
|
+
(reviewProject.source === 'declared' &&
|
|
1503
|
+
initialTargetCorroboration.project !== 'matched')) {
|
|
1504
|
+
writeReviewGateTargetingFailure(context, {
|
|
1505
|
+
runId,
|
|
1506
|
+
target: selected.id,
|
|
1507
|
+
project: projectPath,
|
|
1508
|
+
projectResolutionSource: reviewProject.source,
|
|
1509
|
+
artifactPath: producedArtifact.path,
|
|
1510
|
+
generatedAt: producedArtifact.generatedAt,
|
|
1511
|
+
message: producedArtifact.containingProject !== reviewProject.path
|
|
1512
|
+
? 'Review artifact was written outside the resolved review project.'
|
|
1513
|
+
: initialTargetCorroboration.project === 'missing'
|
|
1514
|
+
? 'Review artifact is missing oat_project for the explicitly declared project.'
|
|
1515
|
+
: 'Review artifact project identity does not match the explicitly declared project.',
|
|
1516
|
+
gateInvocation,
|
|
1517
|
+
corroboration: initialTargetCorroboration,
|
|
1518
|
+
});
|
|
1519
|
+
process.exitCode = 1;
|
|
1520
|
+
return;
|
|
1521
|
+
}
|
|
1522
|
+
if (!producedArtifact.generatedAt ||
|
|
1523
|
+
!Number.isFinite(producedArtifact.generatedTime)) {
|
|
1117
1524
|
writeReviewGateArtifactValidationFailure(context, {
|
|
1118
1525
|
runId,
|
|
1119
1526
|
target: selected.id,
|
|
1120
1527
|
project: projectPath,
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1528
|
+
projectResolutionSource: reviewProject.source,
|
|
1529
|
+
artifactPath: producedArtifact.path,
|
|
1530
|
+
generatedAt: producedArtifact.generatedAt,
|
|
1531
|
+
message: 'Review artifact oat_generated_at is missing or is not a valid timestamp.',
|
|
1532
|
+
recovery: `Set oat_generated_at to a valid timestamp in ${producedArtifact.path}, then rerun the gate. Invoke oat-project-review-receive only after the gate returns a receive-eligible result.`,
|
|
1533
|
+
gateInvocation,
|
|
1534
|
+
corroboration: corroborateGateInvocation(gateInvocation, undefined, initialTargetCorroboration),
|
|
1125
1535
|
});
|
|
1126
1536
|
process.exitCode = 1;
|
|
1127
1537
|
return;
|
|
1128
1538
|
}
|
|
1129
1539
|
let verdict;
|
|
1130
1540
|
try {
|
|
1131
|
-
verdict = await parseReviewGateVerdict(join(repoRoot, producedArtifact.path), {
|
|
1541
|
+
verdict = await dependencies.parseReviewGateVerdict(join(repoRoot, producedArtifact.path), {
|
|
1132
1542
|
normalizeMissingEmptySeveritySections: true,
|
|
1543
|
+
artifactSnapshot: {
|
|
1544
|
+
content: producedArtifact.content,
|
|
1545
|
+
signature: producedArtifact.signature,
|
|
1546
|
+
},
|
|
1133
1547
|
});
|
|
1134
1548
|
}
|
|
1135
1549
|
catch (error) {
|
|
@@ -1138,10 +1552,52 @@ async function runReviewGate(prompt, options, context, dependencies) {
|
|
|
1138
1552
|
runId,
|
|
1139
1553
|
target: selected.id,
|
|
1140
1554
|
project: projectPath,
|
|
1555
|
+
projectResolutionSource: reviewProject.source,
|
|
1141
1556
|
artifactPath: producedArtifact.path,
|
|
1142
1557
|
generatedAt: producedArtifact.generatedAt,
|
|
1143
1558
|
message: detail,
|
|
1144
|
-
recovery: `The review artifact was created at ${producedArtifact.path} but could not be consumed. Fix the artifact format, then
|
|
1559
|
+
recovery: `The review artifact was created at ${producedArtifact.path} but could not be consumed. Fix the artifact format, then rerun the gate to revalidate it. Invoke oat-project-review-receive only after the gate returns a receive-eligible result; if the only issue is a missing zero-count severity heading, rerun the gate to normalize the same artifact instead of creating a new review version.`,
|
|
1560
|
+
gateInvocation,
|
|
1561
|
+
corroboration: corroborateGateInvocation(gateInvocation, undefined, initialTargetCorroboration),
|
|
1562
|
+
});
|
|
1563
|
+
process.exitCode = 1;
|
|
1564
|
+
return;
|
|
1565
|
+
}
|
|
1566
|
+
const targetCorroboration = initialTargetCorroboration;
|
|
1567
|
+
const corroboration = corroborateGateInvocation(gateInvocation, verdict.gateInvocation, targetCorroboration);
|
|
1568
|
+
if (corroboration.run !== 'matched' ||
|
|
1569
|
+
corroboration.invocation !== 'matched') {
|
|
1570
|
+
const missing = corroboration.run === 'missing' ||
|
|
1571
|
+
corroboration.invocation === 'missing';
|
|
1572
|
+
writeReviewGateArtifactValidationFailure(context, {
|
|
1573
|
+
runId,
|
|
1574
|
+
target: selected.id,
|
|
1575
|
+
project: projectPath,
|
|
1576
|
+
projectResolutionSource: reviewProject.source,
|
|
1577
|
+
artifactPath: producedArtifact.path,
|
|
1578
|
+
generatedAt: producedArtifact.generatedAt,
|
|
1579
|
+
message: missing
|
|
1580
|
+
? 'Review artifact invocation metadata is missing required gate-owned values.'
|
|
1581
|
+
: 'Review artifact invocation metadata does not match the gate-owned configured invocation.',
|
|
1582
|
+
recovery: `Copy the exact gate invocation fields from the review prompt into ${producedArtifact.path}, then run oat-project-review-receive only after the artifact validates.`,
|
|
1583
|
+
gateInvocation,
|
|
1584
|
+
corroboration,
|
|
1585
|
+
});
|
|
1586
|
+
process.exitCode = 1;
|
|
1587
|
+
return;
|
|
1588
|
+
}
|
|
1589
|
+
if (verdict.invocation !== 'gate') {
|
|
1590
|
+
writeReviewGateArtifactValidationFailure(context, {
|
|
1591
|
+
runId,
|
|
1592
|
+
target: selected.id,
|
|
1593
|
+
project: projectPath,
|
|
1594
|
+
projectResolutionSource: reviewProject.source,
|
|
1595
|
+
artifactPath: producedArtifact.path,
|
|
1596
|
+
generatedAt: producedArtifact.generatedAt,
|
|
1597
|
+
message: 'Review artifact is missing the required gate invocation marker `oat_review_invocation: gate`.',
|
|
1598
|
+
recovery: `Set oat_review_invocation: gate in ${producedArtifact.path}, then run oat-project-review-receive only after the artifact validates.`,
|
|
1599
|
+
gateInvocation,
|
|
1600
|
+
corroboration,
|
|
1145
1601
|
});
|
|
1146
1602
|
process.exitCode = 1;
|
|
1147
1603
|
return;
|
|
@@ -1158,6 +1614,7 @@ async function runReviewGate(prompt, options, context, dependencies) {
|
|
|
1158
1614
|
runId,
|
|
1159
1615
|
target: selected.id,
|
|
1160
1616
|
project: projectPath,
|
|
1617
|
+
projectResolutionSource: reviewProject.source,
|
|
1161
1618
|
artifactPath: producedArtifact.path,
|
|
1162
1619
|
generatedAt: producedArtifact.generatedAt,
|
|
1163
1620
|
threshold,
|
|
@@ -1169,11 +1626,22 @@ async function runReviewGate(prompt, options, context, dependencies) {
|
|
|
1169
1626
|
normalization: verdict.normalization,
|
|
1170
1627
|
handoff,
|
|
1171
1628
|
diversity: selected.diversity,
|
|
1629
|
+
gateInvocation,
|
|
1630
|
+
corroboration,
|
|
1172
1631
|
});
|
|
1173
1632
|
process.exitCode = blocking ? 1 : 0;
|
|
1174
1633
|
}
|
|
1175
1634
|
catch (error) {
|
|
1176
|
-
|
|
1635
|
+
if (postSelectionContext) {
|
|
1636
|
+
writeReviewGateUnexpectedFailure(context, {
|
|
1637
|
+
...postSelectionContext,
|
|
1638
|
+
error,
|
|
1639
|
+
});
|
|
1640
|
+
process.exitCode = 1;
|
|
1641
|
+
}
|
|
1642
|
+
else {
|
|
1643
|
+
writeError(context, error);
|
|
1644
|
+
}
|
|
1177
1645
|
}
|
|
1178
1646
|
}
|
|
1179
1647
|
export function createGateCommand(overrides = {}) {
|
|
@@ -1250,6 +1718,8 @@ export function createGateCommand(overrides = {}) {
|
|
|
1250
1718
|
.option('--base-command-json <json>', 'JSON argv array for the base command')
|
|
1251
1719
|
.option('--host-detection-json <json>', 'JSON argv array for host detection')
|
|
1252
1720
|
.option('--availability-json <json>', 'JSON argv array for availability')
|
|
1721
|
+
.option('--invocation-model <model>', 'Configured invocation model or provider-default')
|
|
1722
|
+
.option('--invocation-reasoning-effort <effort>', 'Configured reasoning effort or provider-default')
|
|
1253
1723
|
.option('--priority <number>', 'Target priority, higher wins')
|
|
1254
1724
|
.option('--disable', 'Disable this exec target in the selected layer')
|
|
1255
1725
|
.option('--layer <layer>', 'Config layer to write: shared, local, or user')
|
|
@@ -1257,6 +1727,13 @@ export function createGateCommand(overrides = {}) {
|
|
|
1257
1727
|
const context = dependencies.buildCommandContext(readGlobalOptions(command));
|
|
1258
1728
|
await runTargetSet(targetId, options, context, dependencies);
|
|
1259
1729
|
});
|
|
1730
|
+
target
|
|
1731
|
+
.command('list')
|
|
1732
|
+
.description('List resolved exec targets and configuration provenance')
|
|
1733
|
+
.action(async (_options, command) => {
|
|
1734
|
+
const context = dependencies.buildCommandContext(readGlobalOptions(command));
|
|
1735
|
+
await runTargetList(context, dependencies);
|
|
1736
|
+
});
|
|
1260
1737
|
target
|
|
1261
1738
|
.command('unset')
|
|
1262
1739
|
.description('Remove an exec target entry from a concrete config layer')
|