@open-agent-toolkit/cli 0.1.45 → 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.
Files changed (112) hide show
  1. package/assets/agents/oat-phase-implementer.md +202 -191
  2. package/assets/agents/oat-reviewer.md +12 -2
  3. package/assets/config/dispatch-matrix-recommendation.json +120 -13
  4. package/assets/docs/cli-utilities/config-and-local-state.md +7 -0
  5. package/assets/docs/cli-utilities/configuration.md +183 -84
  6. package/assets/docs/cli-utilities/workflow-gates.md +161 -27
  7. package/assets/docs/contributing/skills.md +14 -8
  8. package/assets/docs/provider-sync/config.md +5 -1
  9. package/assets/docs/provider-sync/manifest-and-drift.md +6 -1
  10. package/assets/docs/provider-sync/providers.md +42 -5
  11. package/assets/docs/provider-sync/scope-and-surface.md +3 -2
  12. package/assets/docs/reference/cli-reference.md +3 -1
  13. package/assets/docs/reference/oat-directory-structure.md +27 -26
  14. package/assets/docs/workflows/projects/artifacts.md +31 -1
  15. package/assets/docs/workflows/projects/dispatch-ceiling.md +235 -174
  16. package/assets/docs/workflows/projects/implementation-execution.md +283 -253
  17. package/assets/docs/workflows/projects/lifecycle.md +26 -5
  18. package/assets/docs/workflows/projects/reviews.md +27 -2
  19. package/assets/migration/pjm-restructure.md +1 -1
  20. package/assets/public-package-versions.json +4 -4
  21. package/assets/skills/oat-project-implement/SKILL.md +347 -178
  22. package/assets/skills/oat-project-import-plan/SKILL.md +173 -16
  23. package/assets/skills/oat-project-next/SKILL.md +2 -2
  24. package/assets/skills/oat-project-plan/SKILL.md +122 -92
  25. package/assets/skills/oat-project-plan-writing/SKILL.md +246 -15
  26. package/assets/skills/oat-project-quick-start/SKILL.md +156 -94
  27. package/assets/skills/oat-project-review-provide/SKILL.md +94 -22
  28. package/dist/commands/config/index.d.ts +2 -2
  29. package/dist/commands/config/index.d.ts.map +1 -1
  30. package/dist/commands/config/index.js +129 -53
  31. package/dist/commands/doctor/index.d.ts +2 -2
  32. package/dist/commands/doctor/index.d.ts.map +1 -1
  33. package/dist/commands/doctor/index.js +102 -31
  34. package/dist/commands/gate/index.d.ts +7 -1
  35. package/dist/commands/gate/index.d.ts.map +1 -1
  36. package/dist/commands/gate/index.js +544 -67
  37. package/dist/commands/gate/review-verdict.d.ts +16 -0
  38. package/dist/commands/gate/review-verdict.d.ts.map +1 -1
  39. package/dist/commands/gate/review-verdict.js +72 -9
  40. package/dist/commands/project/dispatch-ceiling/index.d.ts.map +1 -1
  41. package/dist/commands/project/dispatch-ceiling/index.js +586 -23
  42. package/dist/commands/providers/codex/index.d.ts +4 -0
  43. package/dist/commands/providers/codex/index.d.ts.map +1 -0
  44. package/dist/commands/providers/codex/index.js +7 -0
  45. package/dist/commands/providers/codex/materialize.d.ts +5 -0
  46. package/dist/commands/providers/codex/materialize.d.ts.map +1 -0
  47. package/dist/commands/providers/codex/materialize.js +157 -0
  48. package/dist/commands/providers/index.d.ts +2 -2
  49. package/dist/commands/providers/index.d.ts.map +1 -1
  50. package/dist/commands/providers/index.js +4 -2
  51. package/dist/commands/providers/providers.types.d.ts +23 -0
  52. package/dist/commands/providers/providers.types.d.ts.map +1 -1
  53. package/dist/commands/shared/codex-strays.d.ts.map +1 -1
  54. package/dist/commands/shared/codex-strays.js +11 -1
  55. package/dist/commands/shared/frontmatter.d.ts +4 -0
  56. package/dist/commands/shared/frontmatter.d.ts.map +1 -1
  57. package/dist/commands/shared/frontmatter.js +23 -0
  58. package/dist/commands/status/index.d.ts +5 -1
  59. package/dist/commands/status/index.d.ts.map +1 -1
  60. package/dist/commands/status/index.js +10 -4
  61. package/dist/commands/sync/index.d.ts.map +1 -1
  62. package/dist/commands/sync/index.js +10 -3
  63. package/dist/commands/sync/sync.types.d.ts +5 -1
  64. package/dist/commands/sync/sync.types.d.ts.map +1 -1
  65. package/dist/config/dispatch-ceiling-preset.d.ts +4 -0
  66. package/dist/config/dispatch-ceiling-preset.d.ts.map +1 -1
  67. package/dist/config/dispatch-ceiling-preset.js +3 -0
  68. package/dist/config/dispatch-policy-options.d.ts +20 -0
  69. package/dist/config/dispatch-policy-options.d.ts.map +1 -0
  70. package/dist/config/dispatch-policy-options.js +96 -0
  71. package/dist/config/oat-config.d.ts +25 -2
  72. package/dist/config/oat-config.d.ts.map +1 -1
  73. package/dist/config/oat-config.js +154 -24
  74. package/dist/config/resolve.d.ts +8 -0
  75. package/dist/config/resolve.d.ts.map +1 -1
  76. package/dist/config/resolve.js +66 -2
  77. package/dist/engine/index.d.ts +1 -1
  78. package/dist/engine/index.d.ts.map +1 -1
  79. package/dist/engine/index.js +1 -1
  80. package/dist/engine/scanner.d.ts +1 -0
  81. package/dist/engine/scanner.d.ts.map +1 -1
  82. package/dist/engine/scanner.js +17 -1
  83. package/dist/fs/paths.d.ts +4 -0
  84. package/dist/fs/paths.d.ts.map +1 -1
  85. package/dist/fs/paths.js +18 -1
  86. package/dist/providers/ceiling/registry.d.ts +8 -5
  87. package/dist/providers/ceiling/registry.d.ts.map +1 -1
  88. package/dist/providers/ceiling/registry.js +25 -7
  89. package/dist/providers/codex/codec/catalog.d.ts +14 -0
  90. package/dist/providers/codex/codec/catalog.d.ts.map +1 -0
  91. package/dist/providers/codex/codec/catalog.js +21 -0
  92. package/dist/providers/codex/codec/config-merge.d.ts +6 -0
  93. package/dist/providers/codex/codec/config-merge.d.ts.map +1 -1
  94. package/dist/providers/codex/codec/config-merge.js +7 -0
  95. package/dist/providers/codex/codec/materialize.d.ts +16 -0
  96. package/dist/providers/codex/codec/materialize.d.ts.map +1 -0
  97. package/dist/providers/codex/codec/materialize.js +58 -0
  98. package/dist/providers/codex/codec/shared.d.ts +20 -0
  99. package/dist/providers/codex/codec/shared.d.ts.map +1 -1
  100. package/dist/providers/codex/codec/shared.js +107 -6
  101. package/dist/providers/codex/codec/sync-extension.d.ts +7 -1
  102. package/dist/providers/codex/codec/sync-extension.d.ts.map +1 -1
  103. package/dist/providers/codex/codec/sync-extension.js +299 -52
  104. package/dist/providers/identity/availability.d.ts +24 -1
  105. package/dist/providers/identity/availability.d.ts.map +1 -1
  106. package/dist/providers/identity/availability.js +253 -19
  107. package/dist/providers/identity/stamp.d.ts.map +1 -1
  108. package/dist/providers/identity/stamp.js +4 -0
  109. package/dist/shared/types.d.ts +1 -0
  110. package/dist/shared/types.d.ts.map +1 -1
  111. package/dist/shared/types.js +4 -0
  112. 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, getFrontmatterField, parseGeneratedTime, } from '../shared/frontmatter.js';
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(projectPath) {
57
- return `Resolved OAT project path: ${projectPath}. Run the review for this project path.`;
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
- priority: parseNumericFlag(options.priority, '--priority', 0),
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
- ...gates,
291
- execTargets: {
292
- ...gates.execTargets,
293
- [targetId]: target,
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 identityFromStamps(stamps) {
367
- const identities = stamps
368
- .map((stamp) => identityFromRecords([{ value: stamp.producer, provenance: stamp.provenance }], 'stamp'))
369
- .filter((identity) => identity.diversityClaimable && identity.family !== 'unknown');
370
- const latest = identities.at(-1);
371
- if (!latest) {
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
- ...latest,
376
- avoidFamilies: [...new Set(identities.map((identity) => identity.family))],
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: 1, end: Number.MAX_SAFE_INTEGER };
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
- const exactStamp = [...stamps]
421
- .reverse()
422
- .find((candidate) => candidate.scope === scope);
423
- if (exactStamp) {
424
- return identityFromStamps([exactStamp]);
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 identityFromStamps(stamps.filter((candidate) => stampInReviewScope(candidate.scope, scope)));
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.diversityClaimable && identity.avoidFamilies.length > 0;
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 (selected.model && selected.model !== producerIdentity.value) {
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
- if (!(await dirExists(absolutePath)) ||
723
- !(await fileExists(join(absolutePath, 'state.md')))) {
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 normalizedPath;
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
- async function resolveReviewProject(options) {
748
- const projectsRoot = String(options.effective.resolved['projects.root']?.value ??
749
- options.effective.shared.projects?.root ??
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 resolveExplicitReviewProject(options.repoRoot, projectsRoot, options.project);
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 assertProjectPath(options.repoRoot, activeProject, 'Active');
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 candidates[0];
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 generatedAt = getFrontmatterField(frontmatter, 'oat_generated_at');
788
- if (!generatedAt) {
789
- return null;
790
- }
791
- const generatedTime = parseGeneratedTime(generatedAt);
792
- if (Number.isNaN(generatedTime)) {
793
- return null;
794
- }
795
- const scope = getFrontmatterField(frontmatter, 'oat_review_scope') ?? '';
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('Review completed, artifact was normalized, and gate passed.');
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(`Artifact normalized: inserted ${payload.normalization.insertedSeverities.map((severity) => severityDisplayName(severity)).join(', ')} empty Findings section(s).`);
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 projectPath = await resolveReviewProject({
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 listActiveProjectReviewCandidates({
1428
+ const before = await listReviewGateArtifactCandidates({
1083
1429
  repoRoot,
1084
- projectPath,
1430
+ effective,
1431
+ reviewProject,
1085
1432
  });
1086
1433
  const reviewPrompt = assembleReviewGatePrompt([
1087
1434
  REVIEW_GATE_CONTEXT_NOTE,
1088
- reviewGateProjectContext(projectPath),
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 listActiveProjectReviewCandidates({
1461
+ const after = await listReviewGateArtifactCandidates({
1112
1462
  repoRoot,
1113
- projectPath,
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 = findProducedReviewArtifact(before, after);
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
- artifactPath: null,
1122
- generatedAt: null,
1123
- message: `No new review artifact was detected for project ${projectPath}.`,
1124
- recovery: 'Ensure the review provider wrote a project review artifact. If it did, fix or attach that artifact and run oat-project-review-receive before treating the review as consumed.',
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 run oat-project-review-receive for ${producedArtifact.path}; 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.`,
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
- writeError(context, error);
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')