@open-agent-toolkit/cli 0.1.46 → 0.1.50
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 +213 -113
- package/assets/docs/cli-utilities/workflow-gates.md +178 -27
- package/assets/docs/contributing/skills.md +14 -8
- package/assets/docs/provider-sync/providers.md +46 -8
- 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 +297 -197
- package/assets/docs/workflows/projects/implementation-execution.md +306 -249
- 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 +290 -149
- 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 +110 -22
- package/assets/skills/oat-project-review-provide-remote/SKILL.md +28 -1
- package/dist/commands/config/index.d.ts +3 -0
- package/dist/commands/config/index.d.ts.map +1 -1
- package/dist/commands/config/index.js +92 -125
- package/dist/commands/doctor/index.d.ts +3 -0
- package/dist/commands/doctor/index.d.ts.map +1 -1
- package/dist/commands/doctor/index.js +57 -95
- 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 +615 -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 +567 -99
- 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/dispatch-matrix.d.ts +59 -0
- package/dist/config/dispatch-matrix.d.ts.map +1 -0
- package/dist/config/dispatch-matrix.js +264 -0
- package/dist/config/oat-config.d.ts +9 -18
- package/dist/config/oat-config.d.ts.map +1 -1
- package/dist/config/oat-config.js +99 -120
- package/dist/config/resolve.d.ts +8 -0
- package/dist/config/resolve.d.ts.map +1 -1
- package/dist/config/resolve.js +66 -1
- 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/availability.d.ts +12 -0
- package/dist/providers/identity/availability.d.ts.map +1 -1
- package/dist/providers/identity/availability.js +72 -29
- package/dist/providers/identity/dispatch-report.d.ts +124 -0
- package/dist/providers/identity/dispatch-report.d.ts.map +1 -0
- package/dist/providers/identity/dispatch-report.js +285 -0
- package/dist/providers/identity/dispatch-validation.d.ts +28 -0
- package/dist/providers/identity/dispatch-validation.d.ts.map +1 -0
- package/dist/providers/identity/dispatch-validation.js +149 -0
- package/dist/providers/identity/stamp.d.ts +2 -0
- package/dist/providers/identity/stamp.d.ts.map +1 -1
- package/dist/providers/identity/stamp.js +7 -1
- 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,18 @@ 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
|
+
import { buildDispatchReport, } from '../../providers/identity/dispatch-report.js';
|
|
12
13
|
import { classifyModelFamily, } from '../../providers/identity/family.js';
|
|
13
14
|
import { resolveIdentityConfidence, } from '../../providers/identity/provenance.js';
|
|
14
15
|
import { parseDispatchStamps } from '../../providers/identity/stamp.js';
|
|
15
16
|
import { Command } from 'commander';
|
|
17
|
+
import YAML from 'yaml';
|
|
16
18
|
import { parseReviewGateVerdict, severityDisplayName, } from './review-verdict.js';
|
|
17
19
|
const DEFAULT_DEPENDENCIES = {
|
|
18
20
|
buildCommandContext,
|
|
@@ -25,6 +27,7 @@ const DEFAULT_DEPENDENCIES = {
|
|
|
25
27
|
writeUserConfig,
|
|
26
28
|
resolveEffectiveConfig,
|
|
27
29
|
runProcess: runChildProcess,
|
|
30
|
+
parseReviewGateVerdict,
|
|
28
31
|
processEnv: process.env,
|
|
29
32
|
};
|
|
30
33
|
const VALID_ON_FAILURE = ['block', 'prompt', 'warn'];
|
|
@@ -53,8 +56,11 @@ const VALID_IDENTITY_PROVENANCES = [
|
|
|
53
56
|
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
57
|
const GATE_CHECK_TIMEOUT_MS = 5_000;
|
|
55
58
|
const GATE_EXEC_TIMEOUT_MS = 10 * 60 * 1_000;
|
|
56
|
-
function reviewGateProjectContext(
|
|
57
|
-
return
|
|
59
|
+
function reviewGateProjectContext(project) {
|
|
60
|
+
return [
|
|
61
|
+
`Resolved OAT project path: ${project.path}. Run the review for this project path.`,
|
|
62
|
+
`Project resolution source: ${project.source}.`,
|
|
63
|
+
].join('\n');
|
|
58
64
|
}
|
|
59
65
|
function assembleReviewGatePrompt(segments) {
|
|
60
66
|
return segments
|
|
@@ -62,6 +68,130 @@ function assembleReviewGatePrompt(segments) {
|
|
|
62
68
|
.filter((segment) => segment.length > 0)
|
|
63
69
|
.join('\n\n');
|
|
64
70
|
}
|
|
71
|
+
function normalizedTargetInvocation(target) {
|
|
72
|
+
const invocation = target.invocation;
|
|
73
|
+
return {
|
|
74
|
+
model: invocation?.model ?? 'unknown',
|
|
75
|
+
reasoningEffort: invocation?.reasoningEffort ?? 'unknown',
|
|
76
|
+
source: invocation ? 'exec-target-config' : 'unknown',
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
function createGateInvocationMetadata(runId, selected) {
|
|
80
|
+
const configuredInvocation = normalizedTargetInvocation(selected.target);
|
|
81
|
+
const selectedModel = selected.model;
|
|
82
|
+
const configuredModel = selected.target.invocation?.model;
|
|
83
|
+
if (selectedModel &&
|
|
84
|
+
configuredModel !== undefined &&
|
|
85
|
+
configuredModel !== selectedModel) {
|
|
86
|
+
throw new Error(`Exec target "${selected.id}" configures invocation model ${configuredModel}, but selected model ${selectedModel} would be executed.`);
|
|
87
|
+
}
|
|
88
|
+
return Object.freeze({
|
|
89
|
+
runId,
|
|
90
|
+
targetId: selected.id,
|
|
91
|
+
runtime: selected.target.runtime,
|
|
92
|
+
...configuredInvocation,
|
|
93
|
+
...(selectedModel
|
|
94
|
+
? { model: selectedModel, source: 'exec-target-config' }
|
|
95
|
+
: {}),
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
function buildGateDispatchReport(invocation, scope) {
|
|
99
|
+
const report = buildDispatchReport({
|
|
100
|
+
scope,
|
|
101
|
+
action: 'review',
|
|
102
|
+
role: 'reviewer',
|
|
103
|
+
resolution: {
|
|
104
|
+
status: 'resolved',
|
|
105
|
+
provider: invocation.runtime,
|
|
106
|
+
value: null,
|
|
107
|
+
policyMode: null,
|
|
108
|
+
policy: null,
|
|
109
|
+
source: null,
|
|
110
|
+
providers: {
|
|
111
|
+
[invocation.runtime]: {
|
|
112
|
+
dispatchArgs: null,
|
|
113
|
+
selection: {
|
|
114
|
+
role: 'reviewer',
|
|
115
|
+
requestedCandidate: null,
|
|
116
|
+
candidateTier: null,
|
|
117
|
+
candidateIndex: null,
|
|
118
|
+
ceilingTier: null,
|
|
119
|
+
ceilingTarget: null,
|
|
120
|
+
selectedValue: null,
|
|
121
|
+
selectionMode: 'gate-invocation',
|
|
122
|
+
selectionBranch: 'gate-configured-invocation',
|
|
123
|
+
target: null,
|
|
124
|
+
cellSource: null,
|
|
125
|
+
},
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
},
|
|
129
|
+
requestedControls: {
|
|
130
|
+
model: {
|
|
131
|
+
value: null,
|
|
132
|
+
mechanism: 'base-role',
|
|
133
|
+
reason: 'Configured gate invocation is reported separately from runtime identity.',
|
|
134
|
+
},
|
|
135
|
+
effort: {
|
|
136
|
+
value: null,
|
|
137
|
+
mechanism: 'base-role',
|
|
138
|
+
reason: 'Configured gate invocation is reported separately from runtime identity.',
|
|
139
|
+
},
|
|
140
|
+
},
|
|
141
|
+
configuredDefaults: {
|
|
142
|
+
model: null,
|
|
143
|
+
modelSource: null,
|
|
144
|
+
effort: null,
|
|
145
|
+
effortSource: null,
|
|
146
|
+
},
|
|
147
|
+
gateInvocation: invocation,
|
|
148
|
+
});
|
|
149
|
+
return {
|
|
150
|
+
...report,
|
|
151
|
+
route: { ...report.route, target: invocation.targetId },
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
function gateInvocationPromptContext(invocation) {
|
|
155
|
+
const frontmatter = YAML.stringify({
|
|
156
|
+
oat_gate_run_id: invocation.runId,
|
|
157
|
+
oat_gate_target: invocation.targetId,
|
|
158
|
+
oat_gate_runtime: invocation.runtime,
|
|
159
|
+
oat_invocation_model: invocation.model,
|
|
160
|
+
oat_invocation_reasoning_effort: invocation.reasoningEffort,
|
|
161
|
+
oat_invocation_source: invocation.source,
|
|
162
|
+
}).trimEnd();
|
|
163
|
+
return [
|
|
164
|
+
'Gate invocation metadata (copy these exact values into the gate review artifact frontmatter):',
|
|
165
|
+
frontmatter,
|
|
166
|
+
].join('\n');
|
|
167
|
+
}
|
|
168
|
+
function corroborateGateInvocation(expected, actual, targetCorroboration) {
|
|
169
|
+
const invocationFields = [
|
|
170
|
+
['targetId', expected.targetId, actual?.targetId],
|
|
171
|
+
['runtime', expected.runtime, actual?.runtime],
|
|
172
|
+
['model', expected.model, actual?.model],
|
|
173
|
+
['reasoningEffort', expected.reasoningEffort, actual?.reasoningEffort],
|
|
174
|
+
['source', expected.source, actual?.source],
|
|
175
|
+
];
|
|
176
|
+
const invocation = invocationFields.some(([, , actualValue]) => !actualValue)
|
|
177
|
+
? 'missing'
|
|
178
|
+
: invocationFields.every(([, expectedValue, actualValue]) => expectedValue === actualValue)
|
|
179
|
+
? 'matched'
|
|
180
|
+
: 'mismatched';
|
|
181
|
+
return {
|
|
182
|
+
run: targetCorroboration.run,
|
|
183
|
+
project: targetCorroboration.project,
|
|
184
|
+
invocation,
|
|
185
|
+
expected: {
|
|
186
|
+
project: targetCorroboration.expectedProject,
|
|
187
|
+
invocation: expected,
|
|
188
|
+
},
|
|
189
|
+
actual: {
|
|
190
|
+
...targetCorroboration.actual,
|
|
191
|
+
invocation: actual ?? null,
|
|
192
|
+
},
|
|
193
|
+
};
|
|
194
|
+
}
|
|
65
195
|
async function runChildProcess(command, args, options) {
|
|
66
196
|
return new Promise((resolve, reject) => {
|
|
67
197
|
let timedOut = false;
|
|
@@ -244,7 +374,26 @@ function parseExecTargetConfig(options) {
|
|
|
244
374
|
return {
|
|
245
375
|
runtime: trimRequired(options.runtime ?? '', '--runtime'),
|
|
246
376
|
baseCommand: parseArgvJson(baseCommandJson, '--base-command-json'),
|
|
247
|
-
|
|
377
|
+
...(options.priority !== undefined
|
|
378
|
+
? { priority: parseNumericFlag(options.priority, '--priority', 0) }
|
|
379
|
+
: {}),
|
|
380
|
+
...(options.invocationModel !== undefined ||
|
|
381
|
+
options.invocationReasoningEffort !== undefined
|
|
382
|
+
? {
|
|
383
|
+
invocation: {
|
|
384
|
+
...(options.invocationModel !== undefined
|
|
385
|
+
? {
|
|
386
|
+
model: trimRequired(options.invocationModel, '--invocation-model'),
|
|
387
|
+
}
|
|
388
|
+
: {}),
|
|
389
|
+
...(options.invocationReasoningEffort !== undefined
|
|
390
|
+
? {
|
|
391
|
+
reasoningEffort: trimRequired(options.invocationReasoningEffort, '--invocation-reasoning-effort'),
|
|
392
|
+
}
|
|
393
|
+
: {}),
|
|
394
|
+
},
|
|
395
|
+
}
|
|
396
|
+
: {}),
|
|
248
397
|
...(options.hostDetectionJson !== undefined
|
|
249
398
|
? {
|
|
250
399
|
hostDetectionCommand: parseOptionalArgvJson(options.hostDetectionJson, '--host-detection-json'),
|
|
@@ -286,13 +435,32 @@ function unsetSkillGate(config, skillName) {
|
|
|
286
435
|
});
|
|
287
436
|
}
|
|
288
437
|
function setExecTarget(config, targetId, target) {
|
|
289
|
-
return updateWorkflowGates(config, (gates) =>
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
438
|
+
return updateWorkflowGates(config, (gates) => {
|
|
439
|
+
const existing = gates.execTargets?.[targetId];
|
|
440
|
+
const value = target === null
|
|
441
|
+
? target
|
|
442
|
+
: existing === null || existing === undefined
|
|
443
|
+
? { priority: 0, ...target }
|
|
444
|
+
: {
|
|
445
|
+
...existing,
|
|
446
|
+
...target,
|
|
447
|
+
...(existing.invocation || target.invocation
|
|
448
|
+
? {
|
|
449
|
+
invocation: {
|
|
450
|
+
...existing.invocation,
|
|
451
|
+
...target.invocation,
|
|
452
|
+
},
|
|
453
|
+
}
|
|
454
|
+
: {}),
|
|
455
|
+
};
|
|
456
|
+
return {
|
|
457
|
+
...gates,
|
|
458
|
+
execTargets: {
|
|
459
|
+
...gates.execTargets,
|
|
460
|
+
[targetId]: value,
|
|
461
|
+
},
|
|
462
|
+
};
|
|
463
|
+
});
|
|
296
464
|
}
|
|
297
465
|
function unsetExecTarget(config, targetId) {
|
|
298
466
|
return updateWorkflowGates(config, (gates) => {
|
|
@@ -322,6 +490,7 @@ function cloneExecTarget(target) {
|
|
|
322
490
|
runtime: target.runtime,
|
|
323
491
|
baseCommand: [...target.baseCommand],
|
|
324
492
|
priority: target.priority,
|
|
493
|
+
...(target.invocation ? { invocation: { ...target.invocation } } : {}),
|
|
325
494
|
...(target.models ? { models: [...target.models] } : {}),
|
|
326
495
|
...(target.hostDetectionCommand
|
|
327
496
|
? { hostDetectionCommand: [...target.hostDetectionCommand] }
|
|
@@ -363,17 +532,32 @@ function parseProducerIdentityOption(value) {
|
|
|
363
532
|
}
|
|
364
533
|
return identityFromRecords([{ value: producer, provenance: provenance }], 'flag');
|
|
365
534
|
}
|
|
366
|
-
function
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
const
|
|
371
|
-
|
|
535
|
+
function identityFromStamp(stamp) {
|
|
536
|
+
return identityFromRecords([{ value: stamp.producer, provenance: stamp.provenance }], 'stamp');
|
|
537
|
+
}
|
|
538
|
+
function exactIdentityFromStamp(stamp) {
|
|
539
|
+
const identity = identityFromStamp(stamp);
|
|
540
|
+
return identity.diversityClaimable && identity.family !== 'unknown'
|
|
541
|
+
? identity
|
|
542
|
+
: unknownProducerIdentity();
|
|
543
|
+
}
|
|
544
|
+
function aggregateIdentityFromStamps(stamps) {
|
|
545
|
+
if (stamps.length === 0) {
|
|
372
546
|
return unknownProducerIdentity();
|
|
373
547
|
}
|
|
548
|
+
const identities = stamps.map(identityFromStamp);
|
|
549
|
+
const avoidFamilies = [
|
|
550
|
+
...new Set(identities.flatMap((identity) => identity.diversityClaimable && identity.family !== 'unknown'
|
|
551
|
+
? [identity.family]
|
|
552
|
+
: [])),
|
|
553
|
+
];
|
|
374
554
|
return {
|
|
375
|
-
...
|
|
376
|
-
avoidFamilies
|
|
555
|
+
...unknownProducerIdentity(),
|
|
556
|
+
avoidFamilies,
|
|
557
|
+
contributingScopes: [...new Set(stamps.map((stamp) => stamp.scope))],
|
|
558
|
+
contributingStampCount: stamps.length,
|
|
559
|
+
diversityClaimable: avoidFamilies.length > 0,
|
|
560
|
+
source: 'aggregated-stamps',
|
|
377
561
|
};
|
|
378
562
|
}
|
|
379
563
|
function phaseNumber(scope) {
|
|
@@ -383,7 +567,7 @@ function phaseNumber(scope) {
|
|
|
383
567
|
}
|
|
384
568
|
function reviewScopeRange(scope) {
|
|
385
569
|
if (scope === 'final') {
|
|
386
|
-
return { start:
|
|
570
|
+
return { start: 0, end: Number.MAX_SAFE_INTEGER };
|
|
387
571
|
}
|
|
388
572
|
const range = scope.match(/^p(\d+)-p(\d+)$/);
|
|
389
573
|
if (!range) {
|
|
@@ -417,13 +601,15 @@ async function readStampedProducerIdentity(options) {
|
|
|
417
601
|
return unknownProducerIdentity();
|
|
418
602
|
}
|
|
419
603
|
const stamps = parseDispatchStamps(markdown).filter((candidate) => candidate.role === 'implementer' || candidate.role === 'fix');
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
return
|
|
604
|
+
if (!reviewScopeRange(scope)) {
|
|
605
|
+
const exactStamp = [...stamps]
|
|
606
|
+
.reverse()
|
|
607
|
+
.find((candidate) => candidate.scope === scope);
|
|
608
|
+
return exactStamp
|
|
609
|
+
? exactIdentityFromStamp(exactStamp)
|
|
610
|
+
: unknownProducerIdentity();
|
|
425
611
|
}
|
|
426
|
-
return
|
|
612
|
+
return aggregateIdentityFromStamps(stamps.filter((candidate) => stampInReviewScope(candidate.scope, scope)));
|
|
427
613
|
}
|
|
428
614
|
async function resolveReviewProducerIdentity(options) {
|
|
429
615
|
if (options.explicit?.trim()) {
|
|
@@ -480,7 +666,7 @@ function expandExecTargetCandidates(id, target) {
|
|
|
480
666
|
}));
|
|
481
667
|
}
|
|
482
668
|
function producerHasKnownFamily(identity) {
|
|
483
|
-
return identity.
|
|
669
|
+
return identity.avoidFamilies.length > 0;
|
|
484
670
|
}
|
|
485
671
|
function shouldAttemptNoDiverseFallback(avoid) {
|
|
486
672
|
return avoid === 'same-family';
|
|
@@ -493,7 +679,9 @@ function achievedDiversity(selected, producerIdentity) {
|
|
|
493
679
|
!producerIdentity.avoidFamilies.includes(selected.family)) {
|
|
494
680
|
return 'different-family';
|
|
495
681
|
}
|
|
496
|
-
if (
|
|
682
|
+
if (producerIdentity.source !== 'aggregated-stamps' &&
|
|
683
|
+
selected.model &&
|
|
684
|
+
selected.model !== producerIdentity.value) {
|
|
497
685
|
return 'degraded-to-different-slug';
|
|
498
686
|
}
|
|
499
687
|
return 'same-family - no diverse target available';
|
|
@@ -523,6 +711,14 @@ function attachDiversityMetadata(selected, avoid, producerIdentity) {
|
|
|
523
711
|
family: producerIdentity.family,
|
|
524
712
|
source: producerIdentity.source,
|
|
525
713
|
avoidFamilies: producerIdentity.avoidFamilies,
|
|
714
|
+
...(producerIdentity.contributingScopes
|
|
715
|
+
? { contributingScopes: producerIdentity.contributingScopes }
|
|
716
|
+
: {}),
|
|
717
|
+
...(producerIdentity.contributingStampCount === undefined
|
|
718
|
+
? {}
|
|
719
|
+
: {
|
|
720
|
+
contributingStampCount: producerIdentity.contributingStampCount,
|
|
721
|
+
}),
|
|
526
722
|
},
|
|
527
723
|
reviewer: {
|
|
528
724
|
target: selected.id,
|
|
@@ -719,11 +915,21 @@ async function listProjectCandidates(repoRoot, projectsRoot) {
|
|
|
719
915
|
async function assertProjectPath(repoRoot, projectPath, source) {
|
|
720
916
|
const normalizedPath = normalizeRepoRelativeProjectPath(repoRoot, projectPath);
|
|
721
917
|
const absolutePath = join(repoRoot, normalizedPath);
|
|
722
|
-
|
|
723
|
-
|
|
918
|
+
let realProjectPath;
|
|
919
|
+
let canonicalPath;
|
|
920
|
+
try {
|
|
921
|
+
const validated = await validateRealPathWithinScope(absolutePath, repoRoot);
|
|
922
|
+
realProjectPath = validated.realPath;
|
|
923
|
+
canonicalPath = normalizeRepoRelativeProjectPath(validated.realScopeRoot, validated.realPath);
|
|
924
|
+
}
|
|
925
|
+
catch {
|
|
926
|
+
throw new Error(`${source} project "${projectPath}" must resolve inside the current repository through a readable real path.`);
|
|
927
|
+
}
|
|
928
|
+
if (!(await dirExists(realProjectPath)) ||
|
|
929
|
+
!(await fileExists(join(realProjectPath, 'state.md')))) {
|
|
724
930
|
throw new Error(`${source} project "${projectPath}" does not resolve to a project directory containing state.md.`);
|
|
725
931
|
}
|
|
726
|
-
return
|
|
932
|
+
return canonicalPath;
|
|
727
933
|
}
|
|
728
934
|
async function resolveExplicitReviewProject(repoRoot, projectsRoot, projectValue) {
|
|
729
935
|
const trimmed = projectValue.trim();
|
|
@@ -744,16 +950,25 @@ async function resolveExplicitReviewProject(repoRoot, projectsRoot, projectValue
|
|
|
744
950
|
}
|
|
745
951
|
return assertProjectPath(repoRoot, `${projectsRoot}/${trimmed}`, '--project');
|
|
746
952
|
}
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
953
|
+
function resolvedProjectsRoot(effective) {
|
|
954
|
+
return String(effective.resolved['projects.root']?.value ??
|
|
955
|
+
effective.shared.projects?.root ??
|
|
750
956
|
'.oat/projects/shared');
|
|
957
|
+
}
|
|
958
|
+
async function resolveReviewProject(options) {
|
|
959
|
+
const projectsRoot = resolvedProjectsRoot(options.effective);
|
|
751
960
|
if (options.project !== undefined) {
|
|
752
|
-
return
|
|
961
|
+
return {
|
|
962
|
+
path: await resolveExplicitReviewProject(options.repoRoot, projectsRoot, options.project),
|
|
963
|
+
source: 'declared',
|
|
964
|
+
};
|
|
753
965
|
}
|
|
754
966
|
const activeProject = options.effective.local.activeProject?.trim();
|
|
755
967
|
if (activeProject) {
|
|
756
|
-
return
|
|
968
|
+
return {
|
|
969
|
+
path: await assertProjectPath(options.repoRoot, activeProject, 'Active'),
|
|
970
|
+
source: 'active-project',
|
|
971
|
+
};
|
|
757
972
|
}
|
|
758
973
|
const candidates = await listProjectCandidates(options.repoRoot, projectsRoot);
|
|
759
974
|
if (candidates.length === 0) {
|
|
@@ -762,7 +977,10 @@ async function resolveReviewProject(options) {
|
|
|
762
977
|
if (candidates.length > 1) {
|
|
763
978
|
throw new Error(`Multiple OAT projects could be resolved for gate review: ${candidates.join(', ')}. Pass --project <path-or-name>.`);
|
|
764
979
|
}
|
|
765
|
-
return
|
|
980
|
+
return {
|
|
981
|
+
path: candidates[0],
|
|
982
|
+
source: 'single-candidate',
|
|
983
|
+
};
|
|
766
984
|
}
|
|
767
985
|
function reviewGateLifecycleRank(scope) {
|
|
768
986
|
const normalizedScope = scope.trim().toLowerCase();
|
|
@@ -778,21 +996,27 @@ function reviewGateLifecycleRank(scope) {
|
|
|
778
996
|
const latestTask = tasks.length > 0 ? Math.max(...tasks) : 9999;
|
|
779
997
|
return latestPhase * 10_000 + latestTask;
|
|
780
998
|
}
|
|
781
|
-
async function readReviewGateArtifactCandidate(repoRoot, relativePath) {
|
|
999
|
+
async function readReviewGateArtifactCandidate(repoRoot, containingProject, relativePath) {
|
|
782
1000
|
const content = await readFile(join(repoRoot, relativePath), 'utf8');
|
|
783
1001
|
const frontmatter = getFrontmatterBlock(content);
|
|
784
1002
|
if (!frontmatter) {
|
|
785
1003
|
return null;
|
|
786
1004
|
}
|
|
787
|
-
const
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
const
|
|
1005
|
+
const scalarFields = parseFrontmatterScalarFields(frontmatter, [
|
|
1006
|
+
'oat_generated_at',
|
|
1007
|
+
'oat_review_scope',
|
|
1008
|
+
'oat_gate_run_id',
|
|
1009
|
+
'oat_project',
|
|
1010
|
+
]);
|
|
1011
|
+
const frontmatterString = (key) => scalarFields.values[key] ?? null;
|
|
1012
|
+
const generatedAt = frontmatterString('oat_generated_at');
|
|
1013
|
+
const parsedGeneratedTime = generatedAt
|
|
1014
|
+
? parseGeneratedTime(generatedAt)
|
|
1015
|
+
: Number.NaN;
|
|
1016
|
+
const generatedTime = Number.isNaN(parsedGeneratedTime)
|
|
1017
|
+
? Number.NEGATIVE_INFINITY
|
|
1018
|
+
: parsedGeneratedTime;
|
|
1019
|
+
const scope = frontmatterString('oat_review_scope') ?? '';
|
|
796
1020
|
return {
|
|
797
1021
|
path: relativePath,
|
|
798
1022
|
scope,
|
|
@@ -800,9 +1024,13 @@ async function readReviewGateArtifactCandidate(repoRoot, relativePath) {
|
|
|
800
1024
|
kind: 'project',
|
|
801
1025
|
archived: false,
|
|
802
1026
|
actionable: true,
|
|
1027
|
+
containingProject,
|
|
1028
|
+
gateRunId: frontmatterString('oat_gate_run_id'),
|
|
1029
|
+
artifactProject: frontmatterString('oat_project'),
|
|
803
1030
|
generatedTime,
|
|
804
1031
|
lifecycleRank: reviewGateLifecycleRank(scope),
|
|
805
1032
|
signature: createHash('sha256').update(content).digest('hex'),
|
|
1033
|
+
content,
|
|
806
1034
|
};
|
|
807
1035
|
}
|
|
808
1036
|
function sortReviewGateArtifacts(left, right) {
|
|
@@ -834,14 +1062,77 @@ async function listActiveProjectReviewCandidates(options) {
|
|
|
834
1062
|
}
|
|
835
1063
|
return (await Promise.all(entries
|
|
836
1064
|
.filter((entry) => entry.isFile() && entry.name.endsWith('.md'))
|
|
837
|
-
.map((entry) => readReviewGateArtifactCandidate(options.repoRoot, normalizeToPosixPath(join(reviewsDir, entry.name))))))
|
|
1065
|
+
.map((entry) => readReviewGateArtifactCandidate(options.repoRoot, projectPath, normalizeToPosixPath(join(reviewsDir, entry.name))))))
|
|
838
1066
|
.filter((candidate) => candidate !== null)
|
|
839
1067
|
.sort(sortReviewGateArtifacts);
|
|
840
1068
|
}
|
|
1069
|
+
async function listReviewGateArtifactCandidates(options) {
|
|
1070
|
+
const configuredProjects = await listProjectCandidates(options.repoRoot, resolvedProjectsRoot(options.effective));
|
|
1071
|
+
const projectPaths = [
|
|
1072
|
+
...new Set([...configuredProjects, options.reviewProject.path]),
|
|
1073
|
+
];
|
|
1074
|
+
const candidates = await Promise.all(projectPaths.map((projectPath) => listActiveProjectReviewCandidates({
|
|
1075
|
+
repoRoot: options.repoRoot,
|
|
1076
|
+
projectPath,
|
|
1077
|
+
})));
|
|
1078
|
+
return candidates.flat().sort(sortReviewGateArtifacts);
|
|
1079
|
+
}
|
|
841
1080
|
function findProducedReviewArtifact(before, after) {
|
|
842
1081
|
const beforeSignatures = new Map(before.map((candidate) => [candidate.path, candidate.signature]));
|
|
843
1082
|
return (after.find((candidate) => beforeSignatures.get(candidate.path) !== candidate.signature) ?? null);
|
|
844
1083
|
}
|
|
1084
|
+
function resolveRunCorrelatedReviewArtifact(options) {
|
|
1085
|
+
const matches = options.after.filter((candidate) => candidate.gateRunId === options.runId);
|
|
1086
|
+
const artifact = matches.length === 1 ? matches[0] : null;
|
|
1087
|
+
return {
|
|
1088
|
+
artifact,
|
|
1089
|
+
diagnosticArtifact: artifact ?? findProducedReviewArtifact(options.before, options.after),
|
|
1090
|
+
matchingArtifactPaths: matches.map((candidate) => candidate.path).sort(),
|
|
1091
|
+
};
|
|
1092
|
+
}
|
|
1093
|
+
function normalizeArtifactProject(repoRoot, artifactProject) {
|
|
1094
|
+
if (!artifactProject) {
|
|
1095
|
+
return null;
|
|
1096
|
+
}
|
|
1097
|
+
try {
|
|
1098
|
+
const normalized = normalizeRepoRelativeProjectPath(repoRoot, artifactProject);
|
|
1099
|
+
return normalized || null;
|
|
1100
|
+
}
|
|
1101
|
+
catch {
|
|
1102
|
+
return null;
|
|
1103
|
+
}
|
|
1104
|
+
}
|
|
1105
|
+
function corroborateReviewTarget(options) {
|
|
1106
|
+
const actualArtifact = options.artifact ?? options.diagnosticArtifact;
|
|
1107
|
+
const normalizedArtifactProject = normalizeArtifactProject(options.repoRoot, actualArtifact?.artifactProject ?? null);
|
|
1108
|
+
const run = options.matchingArtifactPaths.length > 1
|
|
1109
|
+
? 'mismatched'
|
|
1110
|
+
: actualArtifact?.gateRunId === options.gateInvocation.runId
|
|
1111
|
+
? 'matched'
|
|
1112
|
+
: actualArtifact?.gateRunId
|
|
1113
|
+
? 'mismatched'
|
|
1114
|
+
: 'missing';
|
|
1115
|
+
let project = 'ambient';
|
|
1116
|
+
if (options.reviewProject.source === 'declared') {
|
|
1117
|
+
project = !actualArtifact?.artifactProject
|
|
1118
|
+
? 'missing'
|
|
1119
|
+
: actualArtifact.containingProject === options.reviewProject.path &&
|
|
1120
|
+
normalizedArtifactProject === options.reviewProject.path
|
|
1121
|
+
? 'matched'
|
|
1122
|
+
: 'mismatched';
|
|
1123
|
+
}
|
|
1124
|
+
return {
|
|
1125
|
+
run,
|
|
1126
|
+
project,
|
|
1127
|
+
expectedProject: options.reviewProject.path,
|
|
1128
|
+
actual: {
|
|
1129
|
+
containingProject: actualArtifact?.containingProject ?? null,
|
|
1130
|
+
artifactProject: actualArtifact?.artifactProject ?? null,
|
|
1131
|
+
normalizedArtifactProject,
|
|
1132
|
+
matchingArtifactPaths: options.matchingArtifactPaths,
|
|
1133
|
+
},
|
|
1134
|
+
};
|
|
1135
|
+
}
|
|
845
1136
|
function reviewBlocksAtThreshold(verdict, threshold) {
|
|
846
1137
|
if (verdict.counts.critical > 0) {
|
|
847
1138
|
return true;
|
|
@@ -890,14 +1181,16 @@ function reviewGateOutcome(payload) {
|
|
|
890
1181
|
function writeReviewGateResult(context, payload) {
|
|
891
1182
|
const outcome = reviewGateOutcome(payload);
|
|
892
1183
|
if (context.json) {
|
|
893
|
-
context.logger.json({ outcome, ...payload });
|
|
1184
|
+
context.logger.json({ outcome, ...payload, receiveEligible: true });
|
|
894
1185
|
return;
|
|
895
1186
|
}
|
|
896
1187
|
if (outcome === 'review_completed_blocking_findings') {
|
|
897
1188
|
context.logger.info('Review completed and found blocking issues.');
|
|
898
1189
|
}
|
|
899
1190
|
else if (outcome === 'review_completed_artifact_normalized_gate_passed') {
|
|
900
|
-
context.logger.info(
|
|
1191
|
+
context.logger.info(payload.normalization?.persisted
|
|
1192
|
+
? 'Review completed, artifact was normalized, and gate passed.'
|
|
1193
|
+
: 'Review completed, the immutable artifact snapshot was normalized in memory, and gate passed.');
|
|
901
1194
|
}
|
|
902
1195
|
else {
|
|
903
1196
|
context.logger.info('Review completed and gate passed.');
|
|
@@ -905,7 +1198,7 @@ function writeReviewGateResult(context, payload) {
|
|
|
905
1198
|
context.logger.info(`Run: ${payload.runId} (generated ${payload.generatedAt})`);
|
|
906
1199
|
context.logger.info(`Review artifact: ${payload.artifactPath}`);
|
|
907
1200
|
if (payload.normalization) {
|
|
908
|
-
context.logger.info(
|
|
1201
|
+
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
1202
|
}
|
|
910
1203
|
context.logger.info(`Verdict: ${payload.status} (critical=${payload.counts.critical}, important=${payload.counts.important}, medium=${payload.counts.medium}, minor=${payload.counts.minor})`);
|
|
911
1204
|
if (payload.diversity) {
|
|
@@ -927,6 +1220,12 @@ function writeReviewGateExecutionFailure(context, payload) {
|
|
|
927
1220
|
runId: payload.runId,
|
|
928
1221
|
target: payload.target,
|
|
929
1222
|
project: payload.project,
|
|
1223
|
+
projectResolutionSource: payload.projectResolutionSource,
|
|
1224
|
+
gateInvocation: payload.gateInvocation,
|
|
1225
|
+
dispatchReport: payload.dispatchReport,
|
|
1226
|
+
...(payload.corroboration
|
|
1227
|
+
? { corroboration: payload.corroboration }
|
|
1228
|
+
: {}),
|
|
930
1229
|
exitCode: payload.exitCode,
|
|
931
1230
|
timedOut: payload.timedOut ?? false,
|
|
932
1231
|
...(payload.timeoutMs !== undefined
|
|
@@ -938,6 +1237,26 @@ function writeReviewGateExecutionFailure(context, payload) {
|
|
|
938
1237
|
}
|
|
939
1238
|
context.logger.error(message);
|
|
940
1239
|
}
|
|
1240
|
+
function writeReviewGateUnexpectedFailure(context, payload) {
|
|
1241
|
+
const message = payload.error instanceof Error
|
|
1242
|
+
? payload.error.message
|
|
1243
|
+
: String(payload.error);
|
|
1244
|
+
if (context.json) {
|
|
1245
|
+
context.logger.json({
|
|
1246
|
+
status: 'review_failed',
|
|
1247
|
+
outcome: 'unexpected_post_selection_failure',
|
|
1248
|
+
runId: payload.gateInvocation.runId,
|
|
1249
|
+
target: payload.target,
|
|
1250
|
+
project: payload.project,
|
|
1251
|
+
projectResolutionSource: payload.projectResolutionSource,
|
|
1252
|
+
gateInvocation: payload.gateInvocation,
|
|
1253
|
+
dispatchReport: payload.dispatchReport,
|
|
1254
|
+
message,
|
|
1255
|
+
});
|
|
1256
|
+
return;
|
|
1257
|
+
}
|
|
1258
|
+
context.logger.error(`Review failed after target selection for ${payload.target}: ${message}`);
|
|
1259
|
+
}
|
|
941
1260
|
function writeReviewGateArtifactValidationFailure(context, payload) {
|
|
942
1261
|
if (context.json) {
|
|
943
1262
|
context.logger.json({
|
|
@@ -946,8 +1265,16 @@ function writeReviewGateArtifactValidationFailure(context, payload) {
|
|
|
946
1265
|
runId: payload.runId,
|
|
947
1266
|
target: payload.target,
|
|
948
1267
|
project: payload.project,
|
|
1268
|
+
projectResolutionSource: payload.projectResolutionSource,
|
|
949
1269
|
artifactPath: payload.artifactPath,
|
|
950
1270
|
generatedAt: payload.generatedAt,
|
|
1271
|
+
gateInvocation: payload.gateInvocation,
|
|
1272
|
+
dispatchReport: payload.dispatchReport,
|
|
1273
|
+
...(payload.corroboration
|
|
1274
|
+
? { corroboration: payload.corroboration }
|
|
1275
|
+
: {}),
|
|
1276
|
+
receiveEligible: false,
|
|
1277
|
+
handoff: null,
|
|
951
1278
|
message: payload.message,
|
|
952
1279
|
recovery: payload.recovery,
|
|
953
1280
|
});
|
|
@@ -956,6 +1283,44 @@ function writeReviewGateArtifactValidationFailure(context, payload) {
|
|
|
956
1283
|
context.logger.error(`Review completed but artifact validation failed: ${payload.message}`);
|
|
957
1284
|
context.logger.error(payload.recovery);
|
|
958
1285
|
}
|
|
1286
|
+
function writeReviewGateTargetingFailure(context, payload) {
|
|
1287
|
+
const corroboration = {
|
|
1288
|
+
run: payload.corroboration.run,
|
|
1289
|
+
project: payload.corroboration.project,
|
|
1290
|
+
invocation: 'missing',
|
|
1291
|
+
expected: {
|
|
1292
|
+
project: payload.corroboration.expectedProject,
|
|
1293
|
+
invocation: payload.gateInvocation,
|
|
1294
|
+
},
|
|
1295
|
+
actual: {
|
|
1296
|
+
...payload.corroboration.actual,
|
|
1297
|
+
invocation: null,
|
|
1298
|
+
},
|
|
1299
|
+
};
|
|
1300
|
+
if (context.json) {
|
|
1301
|
+
context.logger.json({
|
|
1302
|
+
status: 'targeting_correlation_failed',
|
|
1303
|
+
outcome: 'review_completed_targeting_correlation_failed',
|
|
1304
|
+
runId: payload.runId,
|
|
1305
|
+
target: payload.target,
|
|
1306
|
+
project: payload.project,
|
|
1307
|
+
projectResolutionSource: payload.projectResolutionSource,
|
|
1308
|
+
artifactPath: payload.artifactPath,
|
|
1309
|
+
generatedAt: payload.generatedAt,
|
|
1310
|
+
gateInvocation: payload.gateInvocation,
|
|
1311
|
+
dispatchReport: payload.dispatchReport,
|
|
1312
|
+
corroboration,
|
|
1313
|
+
receiveEligible: false,
|
|
1314
|
+
remediable: false,
|
|
1315
|
+
handoff: null,
|
|
1316
|
+
message: payload.message,
|
|
1317
|
+
});
|
|
1318
|
+
return;
|
|
1319
|
+
}
|
|
1320
|
+
context.logger.error(`Review completed but target correlation failed: ${payload.message}`);
|
|
1321
|
+
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'}.`);
|
|
1322
|
+
context.logger.error('This targeting failure is not receive-eligible and must be corrected before severity or invocation remediation.');
|
|
1323
|
+
}
|
|
959
1324
|
async function updateConfigLayer(context, layer, dependencies, mutate) {
|
|
960
1325
|
const repoRoot = await dependencies.resolveProjectRoot(context.cwd);
|
|
961
1326
|
const userConfigDir = join(context.home, '.oat');
|
|
@@ -1045,6 +1410,39 @@ async function runTargetUnset(targetId, options, context, dependencies) {
|
|
|
1045
1410
|
writeError(context, error);
|
|
1046
1411
|
}
|
|
1047
1412
|
}
|
|
1413
|
+
async function runTargetList(context, dependencies) {
|
|
1414
|
+
try {
|
|
1415
|
+
const views = resolveExecTargetViews(await readEffectiveConfig(context, dependencies));
|
|
1416
|
+
const targets = await Promise.all(Object.entries(views)
|
|
1417
|
+
.sort(([leftId, left], [rightId, right]) => {
|
|
1418
|
+
const priority = right.target.priority - left.target.priority;
|
|
1419
|
+
return priority === 0 ? leftId.localeCompare(rightId) : priority;
|
|
1420
|
+
})
|
|
1421
|
+
.map(async ([id, view]) => {
|
|
1422
|
+
const availabilityCommand = view.target.availabilityCommand;
|
|
1423
|
+
const available = view.enabled &&
|
|
1424
|
+
(!availabilityCommand ||
|
|
1425
|
+
(await checkArgv(availabilityCommand, 'availability', context, dependencies)));
|
|
1426
|
+
const invocation = normalizedTargetInvocation(view.target);
|
|
1427
|
+
return {
|
|
1428
|
+
id,
|
|
1429
|
+
runtime: view.target.runtime,
|
|
1430
|
+
origin: view.origin,
|
|
1431
|
+
explicitlyConfigured: view.explicitlyConfigured,
|
|
1432
|
+
enabled: view.enabled,
|
|
1433
|
+
available,
|
|
1434
|
+
invocation: {
|
|
1435
|
+
...invocation,
|
|
1436
|
+
},
|
|
1437
|
+
};
|
|
1438
|
+
}));
|
|
1439
|
+
writeSuccess(context, { targets });
|
|
1440
|
+
process.exitCode = 0;
|
|
1441
|
+
}
|
|
1442
|
+
catch (error) {
|
|
1443
|
+
writeError(context, error);
|
|
1444
|
+
}
|
|
1445
|
+
}
|
|
1048
1446
|
async function runCrossProviderExec(prompt, options, context, dependencies) {
|
|
1049
1447
|
try {
|
|
1050
1448
|
const effective = await readEffectiveConfig(context, dependencies);
|
|
@@ -1061,15 +1459,17 @@ async function runCrossProviderExec(prompt, options, context, dependencies) {
|
|
|
1061
1459
|
}
|
|
1062
1460
|
async function runReviewGate(prompt, options, context, dependencies) {
|
|
1063
1461
|
const runId = randomUUID();
|
|
1462
|
+
let postSelectionContext;
|
|
1064
1463
|
try {
|
|
1065
1464
|
const repoRoot = await dependencies.resolveProjectRoot(context.cwd);
|
|
1066
1465
|
const userConfigDir = join(context.home, '.oat');
|
|
1067
1466
|
const effective = await dependencies.resolveEffectiveConfig(repoRoot, userConfigDir, dependencies.processEnv);
|
|
1068
|
-
const
|
|
1467
|
+
const reviewProject = await resolveReviewProject({
|
|
1069
1468
|
repoRoot,
|
|
1070
1469
|
effective,
|
|
1071
1470
|
project: options.project,
|
|
1072
1471
|
});
|
|
1472
|
+
const projectPath = reviewProject.path;
|
|
1073
1473
|
const targets = resolveExecTargets(effective);
|
|
1074
1474
|
const producerIdentity = await resolveReviewProducerIdentity({
|
|
1075
1475
|
explicit: options.producerIdentity,
|
|
@@ -1078,14 +1478,25 @@ async function runReviewGate(prompt, options, context, dependencies) {
|
|
|
1078
1478
|
reviewScope: options.reviewScope,
|
|
1079
1479
|
});
|
|
1080
1480
|
const selected = await resolveSelectedExecTarget(targets, options, producerIdentity, context, dependencies);
|
|
1481
|
+
const gateInvocation = createGateInvocationMetadata(runId, selected);
|
|
1482
|
+
const dispatchReport = buildGateDispatchReport(gateInvocation, options.reviewScope?.trim() || 'gate-review');
|
|
1483
|
+
postSelectionContext = {
|
|
1484
|
+
project: projectPath,
|
|
1485
|
+
projectResolutionSource: reviewProject.source,
|
|
1486
|
+
target: selected.id,
|
|
1487
|
+
gateInvocation,
|
|
1488
|
+
dispatchReport,
|
|
1489
|
+
};
|
|
1081
1490
|
const threshold = parseReviewGateThreshold(options.exitNonzeroOn);
|
|
1082
|
-
const before = await
|
|
1491
|
+
const before = await listReviewGateArtifactCandidates({
|
|
1083
1492
|
repoRoot,
|
|
1084
|
-
|
|
1493
|
+
effective,
|
|
1494
|
+
reviewProject,
|
|
1085
1495
|
});
|
|
1086
1496
|
const reviewPrompt = assembleReviewGatePrompt([
|
|
1087
1497
|
REVIEW_GATE_CONTEXT_NOTE,
|
|
1088
|
-
reviewGateProjectContext(
|
|
1498
|
+
reviewGateProjectContext(reviewProject),
|
|
1499
|
+
gateInvocationPromptContext(gateInvocation),
|
|
1089
1500
|
...(options.reviewType?.trim()
|
|
1090
1501
|
? [`Review type: ${options.reviewType.trim()}.`]
|
|
1091
1502
|
: []),
|
|
@@ -1101,35 +1512,105 @@ async function runReviewGate(prompt, options, context, dependencies) {
|
|
|
1101
1512
|
runId,
|
|
1102
1513
|
target: selected.id,
|
|
1103
1514
|
project: projectPath,
|
|
1515
|
+
projectResolutionSource: reviewProject.source,
|
|
1104
1516
|
exitCode: childExitCode,
|
|
1105
1517
|
timedOut: childResult.timedOut ?? false,
|
|
1106
1518
|
timeoutMs: resolveGateExecTimeoutMs(dependencies.processEnv),
|
|
1519
|
+
gateInvocation,
|
|
1520
|
+
dispatchReport,
|
|
1107
1521
|
});
|
|
1108
1522
|
process.exitCode = childExitCode;
|
|
1109
1523
|
return;
|
|
1110
1524
|
}
|
|
1111
|
-
const after = await
|
|
1525
|
+
const after = await listReviewGateArtifactCandidates({
|
|
1112
1526
|
repoRoot,
|
|
1113
|
-
|
|
1527
|
+
effective,
|
|
1528
|
+
reviewProject,
|
|
1529
|
+
});
|
|
1530
|
+
const artifactResolution = resolveRunCorrelatedReviewArtifact({
|
|
1531
|
+
runId,
|
|
1532
|
+
before,
|
|
1533
|
+
after,
|
|
1534
|
+
});
|
|
1535
|
+
const initialTargetCorroboration = corroborateReviewTarget({
|
|
1536
|
+
repoRoot,
|
|
1537
|
+
reviewProject,
|
|
1538
|
+
gateInvocation,
|
|
1539
|
+
artifact: artifactResolution.artifact,
|
|
1540
|
+
diagnosticArtifact: artifactResolution.diagnosticArtifact,
|
|
1541
|
+
matchingArtifactPaths: artifactResolution.matchingArtifactPaths,
|
|
1114
1542
|
});
|
|
1115
|
-
const producedArtifact =
|
|
1543
|
+
const producedArtifact = artifactResolution.artifact;
|
|
1116
1544
|
if (!producedArtifact) {
|
|
1545
|
+
const diagnosticArtifact = artifactResolution.diagnosticArtifact;
|
|
1546
|
+
const message = artifactResolution.matchingArtifactPaths.length > 1
|
|
1547
|
+
? `Multiple direct review artifacts carried gate run ID ${runId}.`
|
|
1548
|
+
: initialTargetCorroboration.run === 'mismatched'
|
|
1549
|
+
? `The changed review artifact did not carry the expected gate run ID ${runId}.`
|
|
1550
|
+
: `No direct active project review artifact carried gate run ID ${runId}.`;
|
|
1551
|
+
writeReviewGateTargetingFailure(context, {
|
|
1552
|
+
runId,
|
|
1553
|
+
target: selected.id,
|
|
1554
|
+
project: projectPath,
|
|
1555
|
+
projectResolutionSource: reviewProject.source,
|
|
1556
|
+
artifactPath: diagnosticArtifact?.path ?? null,
|
|
1557
|
+
generatedAt: diagnosticArtifact?.generatedAt ?? null,
|
|
1558
|
+
message,
|
|
1559
|
+
gateInvocation,
|
|
1560
|
+
dispatchReport,
|
|
1561
|
+
corroboration: initialTargetCorroboration,
|
|
1562
|
+
});
|
|
1563
|
+
process.exitCode = 1;
|
|
1564
|
+
return;
|
|
1565
|
+
}
|
|
1566
|
+
if (producedArtifact.containingProject !== reviewProject.path ||
|
|
1567
|
+
(reviewProject.source === 'declared' &&
|
|
1568
|
+
initialTargetCorroboration.project !== 'matched')) {
|
|
1569
|
+
writeReviewGateTargetingFailure(context, {
|
|
1570
|
+
runId,
|
|
1571
|
+
target: selected.id,
|
|
1572
|
+
project: projectPath,
|
|
1573
|
+
projectResolutionSource: reviewProject.source,
|
|
1574
|
+
artifactPath: producedArtifact.path,
|
|
1575
|
+
generatedAt: producedArtifact.generatedAt,
|
|
1576
|
+
message: producedArtifact.containingProject !== reviewProject.path
|
|
1577
|
+
? 'Review artifact was written outside the resolved review project.'
|
|
1578
|
+
: initialTargetCorroboration.project === 'missing'
|
|
1579
|
+
? 'Review artifact is missing oat_project for the explicitly declared project.'
|
|
1580
|
+
: 'Review artifact project identity does not match the explicitly declared project.',
|
|
1581
|
+
gateInvocation,
|
|
1582
|
+
dispatchReport,
|
|
1583
|
+
corroboration: initialTargetCorroboration,
|
|
1584
|
+
});
|
|
1585
|
+
process.exitCode = 1;
|
|
1586
|
+
return;
|
|
1587
|
+
}
|
|
1588
|
+
if (!producedArtifact.generatedAt ||
|
|
1589
|
+
!Number.isFinite(producedArtifact.generatedTime)) {
|
|
1117
1590
|
writeReviewGateArtifactValidationFailure(context, {
|
|
1118
1591
|
runId,
|
|
1119
1592
|
target: selected.id,
|
|
1120
1593
|
project: projectPath,
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1594
|
+
projectResolutionSource: reviewProject.source,
|
|
1595
|
+
artifactPath: producedArtifact.path,
|
|
1596
|
+
generatedAt: producedArtifact.generatedAt,
|
|
1597
|
+
message: 'Review artifact oat_generated_at is missing or is not a valid timestamp.',
|
|
1598
|
+
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.`,
|
|
1599
|
+
gateInvocation,
|
|
1600
|
+
dispatchReport,
|
|
1601
|
+
corroboration: corroborateGateInvocation(gateInvocation, undefined, initialTargetCorroboration),
|
|
1125
1602
|
});
|
|
1126
1603
|
process.exitCode = 1;
|
|
1127
1604
|
return;
|
|
1128
1605
|
}
|
|
1129
1606
|
let verdict;
|
|
1130
1607
|
try {
|
|
1131
|
-
verdict = await parseReviewGateVerdict(join(repoRoot, producedArtifact.path), {
|
|
1608
|
+
verdict = await dependencies.parseReviewGateVerdict(join(repoRoot, producedArtifact.path), {
|
|
1132
1609
|
normalizeMissingEmptySeveritySections: true,
|
|
1610
|
+
artifactSnapshot: {
|
|
1611
|
+
content: producedArtifact.content,
|
|
1612
|
+
signature: producedArtifact.signature,
|
|
1613
|
+
},
|
|
1133
1614
|
});
|
|
1134
1615
|
}
|
|
1135
1616
|
catch (error) {
|
|
@@ -1138,10 +1619,55 @@ async function runReviewGate(prompt, options, context, dependencies) {
|
|
|
1138
1619
|
runId,
|
|
1139
1620
|
target: selected.id,
|
|
1140
1621
|
project: projectPath,
|
|
1622
|
+
projectResolutionSource: reviewProject.source,
|
|
1141
1623
|
artifactPath: producedArtifact.path,
|
|
1142
1624
|
generatedAt: producedArtifact.generatedAt,
|
|
1143
1625
|
message: detail,
|
|
1144
|
-
recovery: `The review artifact was created at ${producedArtifact.path} but could not be consumed. Fix the artifact format, then
|
|
1626
|
+
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.`,
|
|
1627
|
+
gateInvocation,
|
|
1628
|
+
dispatchReport,
|
|
1629
|
+
corroboration: corroborateGateInvocation(gateInvocation, undefined, initialTargetCorroboration),
|
|
1630
|
+
});
|
|
1631
|
+
process.exitCode = 1;
|
|
1632
|
+
return;
|
|
1633
|
+
}
|
|
1634
|
+
const targetCorroboration = initialTargetCorroboration;
|
|
1635
|
+
const corroboration = corroborateGateInvocation(gateInvocation, verdict.gateInvocation, targetCorroboration);
|
|
1636
|
+
if (corroboration.run !== 'matched' ||
|
|
1637
|
+
corroboration.invocation !== 'matched') {
|
|
1638
|
+
const missing = corroboration.run === 'missing' ||
|
|
1639
|
+
corroboration.invocation === 'missing';
|
|
1640
|
+
writeReviewGateArtifactValidationFailure(context, {
|
|
1641
|
+
runId,
|
|
1642
|
+
target: selected.id,
|
|
1643
|
+
project: projectPath,
|
|
1644
|
+
projectResolutionSource: reviewProject.source,
|
|
1645
|
+
artifactPath: producedArtifact.path,
|
|
1646
|
+
generatedAt: producedArtifact.generatedAt,
|
|
1647
|
+
message: missing
|
|
1648
|
+
? 'Review artifact invocation metadata is missing required gate-owned values.'
|
|
1649
|
+
: 'Review artifact invocation metadata does not match the gate-owned configured invocation.',
|
|
1650
|
+
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.`,
|
|
1651
|
+
gateInvocation,
|
|
1652
|
+
dispatchReport,
|
|
1653
|
+
corroboration,
|
|
1654
|
+
});
|
|
1655
|
+
process.exitCode = 1;
|
|
1656
|
+
return;
|
|
1657
|
+
}
|
|
1658
|
+
if (verdict.invocation !== 'gate') {
|
|
1659
|
+
writeReviewGateArtifactValidationFailure(context, {
|
|
1660
|
+
runId,
|
|
1661
|
+
target: selected.id,
|
|
1662
|
+
project: projectPath,
|
|
1663
|
+
projectResolutionSource: reviewProject.source,
|
|
1664
|
+
artifactPath: producedArtifact.path,
|
|
1665
|
+
generatedAt: producedArtifact.generatedAt,
|
|
1666
|
+
message: 'Review artifact is missing the required gate invocation marker `oat_review_invocation: gate`.',
|
|
1667
|
+
recovery: `Set oat_review_invocation: gate in ${producedArtifact.path}, then run oat-project-review-receive only after the artifact validates.`,
|
|
1668
|
+
gateInvocation,
|
|
1669
|
+
dispatchReport,
|
|
1670
|
+
corroboration,
|
|
1145
1671
|
});
|
|
1146
1672
|
process.exitCode = 1;
|
|
1147
1673
|
return;
|
|
@@ -1158,6 +1684,7 @@ async function runReviewGate(prompt, options, context, dependencies) {
|
|
|
1158
1684
|
runId,
|
|
1159
1685
|
target: selected.id,
|
|
1160
1686
|
project: projectPath,
|
|
1687
|
+
projectResolutionSource: reviewProject.source,
|
|
1161
1688
|
artifactPath: producedArtifact.path,
|
|
1162
1689
|
generatedAt: producedArtifact.generatedAt,
|
|
1163
1690
|
threshold,
|
|
@@ -1169,11 +1696,23 @@ async function runReviewGate(prompt, options, context, dependencies) {
|
|
|
1169
1696
|
normalization: verdict.normalization,
|
|
1170
1697
|
handoff,
|
|
1171
1698
|
diversity: selected.diversity,
|
|
1699
|
+
gateInvocation,
|
|
1700
|
+
dispatchReport,
|
|
1701
|
+
corroboration,
|
|
1172
1702
|
});
|
|
1173
1703
|
process.exitCode = blocking ? 1 : 0;
|
|
1174
1704
|
}
|
|
1175
1705
|
catch (error) {
|
|
1176
|
-
|
|
1706
|
+
if (postSelectionContext) {
|
|
1707
|
+
writeReviewGateUnexpectedFailure(context, {
|
|
1708
|
+
...postSelectionContext,
|
|
1709
|
+
error,
|
|
1710
|
+
});
|
|
1711
|
+
process.exitCode = 1;
|
|
1712
|
+
}
|
|
1713
|
+
else {
|
|
1714
|
+
writeError(context, error);
|
|
1715
|
+
}
|
|
1177
1716
|
}
|
|
1178
1717
|
}
|
|
1179
1718
|
export function createGateCommand(overrides = {}) {
|
|
@@ -1250,6 +1789,8 @@ export function createGateCommand(overrides = {}) {
|
|
|
1250
1789
|
.option('--base-command-json <json>', 'JSON argv array for the base command')
|
|
1251
1790
|
.option('--host-detection-json <json>', 'JSON argv array for host detection')
|
|
1252
1791
|
.option('--availability-json <json>', 'JSON argv array for availability')
|
|
1792
|
+
.option('--invocation-model <model>', 'Configured invocation model or provider-default')
|
|
1793
|
+
.option('--invocation-reasoning-effort <effort>', 'Configured reasoning effort or provider-default')
|
|
1253
1794
|
.option('--priority <number>', 'Target priority, higher wins')
|
|
1254
1795
|
.option('--disable', 'Disable this exec target in the selected layer')
|
|
1255
1796
|
.option('--layer <layer>', 'Config layer to write: shared, local, or user')
|
|
@@ -1257,6 +1798,13 @@ export function createGateCommand(overrides = {}) {
|
|
|
1257
1798
|
const context = dependencies.buildCommandContext(readGlobalOptions(command));
|
|
1258
1799
|
await runTargetSet(targetId, options, context, dependencies);
|
|
1259
1800
|
});
|
|
1801
|
+
target
|
|
1802
|
+
.command('list')
|
|
1803
|
+
.description('List resolved exec targets and configuration provenance')
|
|
1804
|
+
.action(async (_options, command) => {
|
|
1805
|
+
const context = dependencies.buildCommandContext(readGlobalOptions(command));
|
|
1806
|
+
await runTargetList(context, dependencies);
|
|
1807
|
+
});
|
|
1260
1808
|
target
|
|
1261
1809
|
.command('unset')
|
|
1262
1810
|
.description('Remove an exec target entry from a concrete config layer')
|