@open-agent-toolkit/cli 0.2.62 → 0.2.63
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/bundle-metadata.json +1 -1
- package/assets/docs/cli-utilities/config-and-local-state.md +1 -0
- package/assets/docs/cli-utilities/configuration.md +10 -8
- package/assets/docs/cli-utilities/project-log.md +16 -0
- package/assets/docs/cli-utilities/workflow-gates.md +124 -16
- package/assets/docs/provider-sync/commands.md +3 -0
- package/assets/docs/provider-sync/instruction-sync.md +107 -0
- package/assets/docs/reference/cli-reference.md +16 -6
- package/assets/docs/reference/oat-directory-structure.md +32 -31
- package/assets/docs/workflows/projects/lifecycle.md +45 -0
- package/assets/docs/workflows/projects/picking-up-projects.md +14 -0
- package/assets/docs/workflows/skills/explainer-kit.md +10 -4
- package/assets/docs/workflows/skills/repo-improve.md +15 -1
- package/assets/public-package-versions.json +4 -4
- package/assets/skills/oat-explainer-kit/SKILL.md +14 -1
- package/assets/skills/oat-explainer-kit/references/config-contract.md +12 -0
- package/assets/skills/oat-explainer-kit/references/lifecycle-contract.md +57 -10
- package/assets/skills/oat-explainer-kit/scripts/check-terminal-outcome.mjs +28 -5
- package/assets/skills/oat-explainer-kit/scripts/probe-recap-seams.mjs +371 -0
- package/assets/skills/oat-explainer-kit/scripts/resolve-intent.mjs +123 -1
- package/assets/skills/oat-project-autonomous/SKILL.md +10 -1
- package/assets/skills/oat-project-autonomous/references/gate-inventory.md +121 -121
- package/assets/skills/oat-project-complete/SKILL.md +134 -4
- package/assets/skills/oat-project-document/references/docs/autonomy-contract.md +121 -121
- package/assets/skills/oat-project-implement/SKILL.md +1 -1
- package/assets/skills/oat-project-implement/references/completion-and-closeout.md +38 -6
- package/assets/skills/oat-project-implement/references/docs/autonomy-contract.md +121 -121
- package/assets/skills/oat-project-lite/references/docs/autonomy-contract.md +121 -121
- package/assets/skills/oat-project-next/SKILL.md +62 -19
- package/assets/skills/oat-project-plan/SKILL.md +31 -5
- package/assets/skills/oat-project-pr-final/references/docs/autonomy-contract.md +121 -121
- package/assets/skills/oat-project-progress/SKILL.md +12 -3
- package/assets/skills/oat-project-quick-start/SKILL.md +264 -4
- package/assets/skills/oat-project-quick-start/references/docs/autonomy-contract.md +121 -121
- package/assets/skills/oat-project-summary/SKILL.md +34 -23
- package/assets/skills/oat-repo-improve/SKILL.md +40 -5
- package/assets/skills/oat-repo-improve/references/plan-template.md +84 -7
- package/dist/commands/config/index.d.ts +1 -0
- package/dist/commands/config/index.d.ts.map +1 -1
- package/dist/commands/config/index.js +401 -18
- package/dist/commands/gate/index.d.ts +3 -1
- package/dist/commands/gate/index.d.ts.map +1 -1
- package/dist/commands/gate/index.js +699 -211
- package/dist/commands/init/tools/shared/skill-script-references.d.ts +126 -0
- package/dist/commands/init/tools/shared/skill-script-references.d.ts.map +1 -0
- package/dist/commands/init/tools/shared/skill-script-references.js +311 -0
- package/dist/commands/instructions/instructions.types.d.ts +63 -0
- package/dist/commands/instructions/instructions.types.d.ts.map +1 -1
- package/dist/commands/instructions/instructions.utils.d.ts +51 -2
- package/dist/commands/instructions/instructions.utils.d.ts.map +1 -1
- package/dist/commands/instructions/instructions.utils.js +201 -4
- package/dist/commands/instructions/sync/sync.d.ts.map +1 -1
- package/dist/commands/instructions/sync/sync.js +12 -1
- package/dist/commands/instructions/validate/validate.d.ts.map +1 -1
- package/dist/commands/instructions/validate/validate.js +12 -1
- package/dist/commands/project/log/append.d.ts +195 -0
- package/dist/commands/project/log/append.d.ts.map +1 -1
- package/dist/commands/project/log/append.js +1034 -11
- package/dist/config/oat-config.d.ts +41 -0
- package/dist/config/oat-config.d.ts.map +1 -1
- package/dist/config/oat-config.js +111 -0
- package/package.json +2 -2
|
@@ -2,9 +2,9 @@ import { execFileSync } from 'node:child_process';
|
|
|
2
2
|
import { createHash, randomUUID } from 'node:crypto';
|
|
3
3
|
import { mkdir, readdir, readFile, rm, writeFile } from 'node:fs/promises';
|
|
4
4
|
import { tmpdir } from 'node:os';
|
|
5
|
-
import { basename, isAbsolute, join, relative } from 'node:path';
|
|
5
|
+
import { basename, dirname, isAbsolute, join, relative, resolve, } from 'node:path';
|
|
6
6
|
import { buildCommandContext, } from '../../app/command-context.js';
|
|
7
|
-
import { appendProjectLog } from '../project/log/append.js';
|
|
7
|
+
import { appendProjectLog, commitProjectLog, committedProjectLogIdentityState, GATE_RECEIPTS_DIRNAME, PROJECT_LOG_FILENAME, } from '../project/log/append.js';
|
|
8
8
|
import { getFrontmatterBlock, parseFrontmatterScalarFields, parseGeneratedTime, parseSkillGateOverrides, SKILL_GATE_OVERRIDE_DISABLED, SKILL_GATE_OVERRIDE_SOURCE, } from '../shared/frontmatter.js';
|
|
9
9
|
import { readGlobalOptions } from '../shared/shared.utils.js';
|
|
10
10
|
import { parseJsonConfig } from '../../config/json.js';
|
|
@@ -45,6 +45,10 @@ const DEFAULT_DEPENDENCIES = {
|
|
|
45
45
|
processEnv: process.env,
|
|
46
46
|
writeGateRunMarker,
|
|
47
47
|
removeGateRunMarker,
|
|
48
|
+
writeGateProjectLogReceipt,
|
|
49
|
+
sleep: async (ms) => {
|
|
50
|
+
await new Promise((settle) => setTimeout(settle, ms));
|
|
51
|
+
},
|
|
48
52
|
writeDiagnostic: (message) => process.stderr.write(message),
|
|
49
53
|
};
|
|
50
54
|
const VALID_ON_FAILURE = ['block', 'prompt', 'warn'];
|
|
@@ -91,6 +95,58 @@ async function writeGateRunMarker(path, marker, warn) {
|
|
|
91
95
|
return false;
|
|
92
96
|
}
|
|
93
97
|
}
|
|
98
|
+
/**
|
|
99
|
+
* Writes a gate partial-finalization receipt.
|
|
100
|
+
*
|
|
101
|
+
* Injected like `writeGateRunMarker`, but deliberately not stored beside it:
|
|
102
|
+
* the run marker lives in `tmpdir()` because it dies with the run, while a
|
|
103
|
+
* receipt has to survive into the later process that finishes the
|
|
104
|
+
* finalization, so it lives under the project it belongs to.
|
|
105
|
+
*/
|
|
106
|
+
async function writeGateProjectLogReceipt(path, receipt, warn) {
|
|
107
|
+
try {
|
|
108
|
+
await mkdir(dirname(path), { recursive: true });
|
|
109
|
+
await writeFile(path, `${JSON.stringify(receipt, null, 2)}\n`, 'utf8');
|
|
110
|
+
}
|
|
111
|
+
catch (error) {
|
|
112
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
113
|
+
warn(`Unable to write gate project log receipt ${path}: ${detail}`);
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
warnWhenReceiptIsTracked(path, warn);
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
119
|
+
/**
|
|
120
|
+
* Warns when the receipt just written is not ignored by git.
|
|
121
|
+
*
|
|
122
|
+
* "Untracked" is a property of the repository's ignore rules, not of this
|
|
123
|
+
* module: a repository that relocates `projects.root`, or drops the shipped
|
|
124
|
+
* repository-wide gate-receipts ignore rule, gets a receipt that the next
|
|
125
|
+
* `git add -A` would commit — the exact dirty worktree finalization exists to
|
|
126
|
+
* prevent. The
|
|
127
|
+
* receipt is still written, because losing the finalization would be worse
|
|
128
|
+
* than a tracked file; the operator is told instead. A repository this path
|
|
129
|
+
* cannot interrogate stays silent rather than warning speculatively.
|
|
130
|
+
*/
|
|
131
|
+
function warnWhenReceiptIsTracked(path, warn) {
|
|
132
|
+
try {
|
|
133
|
+
execFileSync('git', ['check-ignore', '--quiet', '--', path], {
|
|
134
|
+
cwd: dirname(path),
|
|
135
|
+
stdio: ['ignore', 'ignore', 'ignore'],
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
catch (error) {
|
|
139
|
+
// Exit 1 means "not ignored"; anything else (not a repository, git
|
|
140
|
+
// missing) is not evidence either way.
|
|
141
|
+
const status = error && typeof error === 'object' && 'status' in error
|
|
142
|
+
? error.status
|
|
143
|
+
: undefined;
|
|
144
|
+
if (status !== 1) {
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
warn(`Gate project log receipt ${path} is not ignored by git. Add a gate-receipts ignore rule for this projects root, or the receipt will be committed by the next repository-wide add.`);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
94
150
|
async function removeGateRunMarker(path, warn) {
|
|
95
151
|
try {
|
|
96
152
|
await rm(path);
|
|
@@ -1485,10 +1541,31 @@ function formatGateActivityEvidenceDiagnostic(evidence, now = Date.now()) {
|
|
|
1485
1541
|
: '';
|
|
1486
1542
|
return `Activity evidence: ${transcriptScope} metadata ${baselineState}; observed ${observedAgoMs}ms ago; ${lastChangeTiming}.${attributionWarning}`;
|
|
1487
1543
|
}
|
|
1544
|
+
/**
|
|
1545
|
+
* A stable, routable code for the post-selection failure: the thrown error's
|
|
1546
|
+
* own `code` when it has one, otherwise its constructor name.
|
|
1547
|
+
*/
|
|
1548
|
+
function postSelectionFailureCode(error) {
|
|
1549
|
+
const code = error?.code;
|
|
1550
|
+
if (typeof code === 'string' && code.trim().length > 0) {
|
|
1551
|
+
return code;
|
|
1552
|
+
}
|
|
1553
|
+
if (typeof code === 'number') {
|
|
1554
|
+
return String(code);
|
|
1555
|
+
}
|
|
1556
|
+
if (error instanceof Error) {
|
|
1557
|
+
return error.constructor.name;
|
|
1558
|
+
}
|
|
1559
|
+
return 'UnknownError';
|
|
1560
|
+
}
|
|
1488
1561
|
function writeReviewGateUnexpectedFailure(context, payload) {
|
|
1489
1562
|
const message = payload.error instanceof Error
|
|
1490
1563
|
? payload.error.message
|
|
1491
1564
|
: String(payload.error);
|
|
1565
|
+
// When a committed artifact was found but did not survive re-validation, the
|
|
1566
|
+
// eligibility cause is the routable code; the thrown sub-step still names
|
|
1567
|
+
// what failed.
|
|
1568
|
+
const code = payload.ineligibility ?? postSelectionFailureCode(payload.error);
|
|
1492
1569
|
if (context.json) {
|
|
1493
1570
|
context.logger.json({
|
|
1494
1571
|
status: 'review_failed',
|
|
@@ -1499,11 +1576,12 @@ function writeReviewGateUnexpectedFailure(context, payload) {
|
|
|
1499
1576
|
projectResolutionSource: payload.projectResolutionSource,
|
|
1500
1577
|
gateInvocation: payload.gateInvocation,
|
|
1501
1578
|
dispatchReport: payload.dispatchReport,
|
|
1579
|
+
postSelection: { step: payload.step, code },
|
|
1502
1580
|
message,
|
|
1503
1581
|
});
|
|
1504
1582
|
return;
|
|
1505
1583
|
}
|
|
1506
|
-
context.logger.error(`Review failed after target selection for ${payload.target}: ${message}`);
|
|
1584
|
+
context.logger.error(`Review failed after target selection for ${payload.target}: ${message} (post-selection step: ${payload.step})`);
|
|
1507
1585
|
}
|
|
1508
1586
|
function writeReviewGateArtifactValidationFailure(context, payload) {
|
|
1509
1587
|
if (context.json) {
|
|
@@ -1595,74 +1673,216 @@ function writeReviewGateTargetingFailure(context, payload) {
|
|
|
1595
1673
|
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'}.`);
|
|
1596
1674
|
context.logger.error('This targeting failure is not receive-eligible and must be corrected before severity or invocation remediation.');
|
|
1597
1675
|
}
|
|
1676
|
+
const GATE_PROJECT_LOG_PRODUCER = 'oat gate review';
|
|
1677
|
+
const GATE_PROJECT_LOG_COMMIT_MESSAGE = 'chore(oat): record gate review in project log';
|
|
1598
1678
|
/**
|
|
1599
1679
|
* Commits `project-log.md` after this gate run appends to it.
|
|
1600
1680
|
*
|
|
1601
|
-
* The
|
|
1602
|
-
*
|
|
1603
|
-
*
|
|
1604
|
-
*
|
|
1681
|
+
* The retry, the index-lock classification, and the log mutation itself belong
|
|
1682
|
+
* to the `oat project log` module (DR-260718), so this is a thin gate-side
|
|
1683
|
+
* wrapper that supplies the gate's commit message and its injected sleep. The
|
|
1684
|
+
* recovery entry point calls the same implementation, so a finalization
|
|
1685
|
+
* completed later is the finalization the gate would have made.
|
|
1605
1686
|
*
|
|
1606
|
-
*
|
|
1607
|
-
*
|
|
1608
|
-
*
|
|
1609
|
-
* tree this exists to prevent — but it does mean the commit is not always
|
|
1610
|
-
* exactly one entry.
|
|
1611
|
-
*
|
|
1612
|
-
* Never throws: git failures are reported to the caller, which degrades to a
|
|
1613
|
-
* diagnostic rather than altering the gate's exit status. On failure the index
|
|
1614
|
-
* is restored so a partially staged log is not left behind.
|
|
1687
|
+
* Never throws and never removes an index lock: git failures are reported to
|
|
1688
|
+
* the caller, which degrades to a diagnostic rather than altering the gate's
|
|
1689
|
+
* exit status.
|
|
1615
1690
|
*/
|
|
1616
|
-
function commitReviewGateProjectLog(repoRoot, logPath) {
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1691
|
+
async function commitReviewGateProjectLog(repoRoot, logPath, identity, sleep) {
|
|
1692
|
+
return commitProjectLog({
|
|
1693
|
+
repoRoot,
|
|
1694
|
+
logPath,
|
|
1695
|
+
message: GATE_PROJECT_LOG_COMMIT_MESSAGE,
|
|
1696
|
+
identity,
|
|
1697
|
+
}, { sleep });
|
|
1698
|
+
}
|
|
1699
|
+
function resolveGateProjectPath(repoRoot, project) {
|
|
1700
|
+
return isAbsolute(project) ? project : join(repoRoot, project);
|
|
1701
|
+
}
|
|
1702
|
+
function resolveWorktreeRoot(repoRoot) {
|
|
1624
1703
|
try {
|
|
1625
|
-
|
|
1704
|
+
return execFileSync('git', ['rev-parse', '--show-toplevel'], {
|
|
1705
|
+
cwd: repoRoot,
|
|
1706
|
+
encoding: 'utf8',
|
|
1707
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
1708
|
+
}).trim();
|
|
1626
1709
|
}
|
|
1627
1710
|
catch {
|
|
1628
|
-
return
|
|
1711
|
+
return repoRoot;
|
|
1629
1712
|
}
|
|
1630
|
-
|
|
1713
|
+
}
|
|
1714
|
+
/**
|
|
1715
|
+
* Quotes a value for the copy-pasteable recovery command. Anything outside the
|
|
1716
|
+
* safe set is single-quoted so a body full of spaces and `=` tokens survives a
|
|
1717
|
+
* shell round trip verbatim.
|
|
1718
|
+
*/
|
|
1719
|
+
function shellQuote(value) {
|
|
1720
|
+
return /^[\w@%+=:,./-]+$/.test(value)
|
|
1721
|
+
? value
|
|
1722
|
+
: `'${value.replaceAll("'", `'\\''`)}'`;
|
|
1723
|
+
}
|
|
1724
|
+
function composeGateProjectLogRecoveryCommand(input) {
|
|
1725
|
+
return [
|
|
1726
|
+
'oat project log append',
|
|
1727
|
+
`--project ${shellQuote(input.projectPath)}`,
|
|
1728
|
+
'--structural',
|
|
1729
|
+
`--producer ${shellQuote(input.producer)}`,
|
|
1730
|
+
`--ref ${shellQuote(input.ref)}`,
|
|
1731
|
+
`--body ${shellQuote(input.body)}`,
|
|
1732
|
+
`--idempotency-key ${shellQuote(input.runId)}`,
|
|
1733
|
+
'--commit',
|
|
1734
|
+
].join(' ');
|
|
1735
|
+
}
|
|
1736
|
+
/**
|
|
1737
|
+
* Warns once per pending gate project-log receipt at the start of a run.
|
|
1738
|
+
*
|
|
1739
|
+
* A receipt is `stale` only once the *committed* log carries its run id: the
|
|
1740
|
+
* append landed and was committed, so the recovery command will observe
|
|
1741
|
+
* `already-appended` and clear the receipt. Anything else is `pending`
|
|
1742
|
+
* finalization work.
|
|
1743
|
+
*
|
|
1744
|
+
* Reading the working tree instead is what made this wrong. A run whose commit
|
|
1745
|
+
* retries were exhausted leaves its entry in the working tree and nowhere else
|
|
1746
|
+
* — that is precisely the state the receipt exists to describe — so a
|
|
1747
|
+
* working-tree reader calls every unfinished finalization finished, and the
|
|
1748
|
+
* receipt that would have completed it is dismissed as leftovers.
|
|
1749
|
+
*
|
|
1750
|
+
* The staleness question is answered by the log module, so the gate never reads
|
|
1751
|
+
* `project-log.md` itself.
|
|
1752
|
+
*/
|
|
1753
|
+
async function warnPendingGateProjectLogReceipts(options) {
|
|
1754
|
+
const projectDir = resolveGateProjectPath(options.repoRoot, options.project);
|
|
1755
|
+
const receiptsDir = join(projectDir, GATE_RECEIPTS_DIRNAME);
|
|
1756
|
+
// The staleness question is about the log of the project being gated. A
|
|
1757
|
+
// receipt that travelled with a copied project directory records the other
|
|
1758
|
+
// tree's absolute path, and reading that would make the verdict a statement
|
|
1759
|
+
// about a log this run does not own.
|
|
1760
|
+
const projectLogPath = join(projectDir, PROJECT_LOG_FILENAME);
|
|
1761
|
+
let entries;
|
|
1631
1762
|
try {
|
|
1632
|
-
|
|
1633
|
-
return { committed: false };
|
|
1634
|
-
}
|
|
1635
|
-
run(['add', '--', logPath]);
|
|
1636
|
-
staged = true;
|
|
1637
|
-
run([
|
|
1638
|
-
'commit',
|
|
1639
|
-
'-m',
|
|
1640
|
-
'chore(oat): record gate review in project log',
|
|
1641
|
-
'--',
|
|
1642
|
-
logPath,
|
|
1643
|
-
]);
|
|
1644
|
-
return { committed: true };
|
|
1763
|
+
entries = await readdir(receiptsDir);
|
|
1645
1764
|
}
|
|
1646
|
-
catch
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
}
|
|
1765
|
+
catch {
|
|
1766
|
+
return;
|
|
1767
|
+
}
|
|
1768
|
+
for (const name of entries
|
|
1769
|
+
.filter((entry) => entry.endsWith('.json'))
|
|
1770
|
+
.sort()) {
|
|
1771
|
+
const receiptPath = join(receiptsDir, name);
|
|
1772
|
+
let receipt;
|
|
1773
|
+
try {
|
|
1774
|
+
receipt = JSON.parse(await readFile(receiptPath, 'utf8'));
|
|
1657
1775
|
}
|
|
1658
|
-
|
|
1659
|
-
|
|
1776
|
+
catch {
|
|
1777
|
+
continue;
|
|
1778
|
+
}
|
|
1779
|
+
const foreignLogPath = typeof receipt.logPath === 'string' &&
|
|
1780
|
+
resolve(receipt.logPath) !== resolve(projectLogPath)
|
|
1781
|
+
? receipt.logPath
|
|
1660
1782
|
: undefined;
|
|
1661
|
-
|
|
1662
|
-
|
|
1663
|
-
|
|
1783
|
+
// A receipt that names another tree's log cannot be shown to be stale
|
|
1784
|
+
// here, so it stays pending and the disagreement is reported.
|
|
1785
|
+
//
|
|
1786
|
+
// `present` is required, not merely "not absent": a log the committed tree
|
|
1787
|
+
// cannot answer for leaves the work pending, because an unfinished
|
|
1788
|
+
// finalization that is reported as stale is one nobody will finish.
|
|
1789
|
+
const stale = foreignLogPath === undefined &&
|
|
1790
|
+
(await committedProjectLogIdentityState(options.repoRoot, projectLogPath, receipt.runId, receipt.body)) === 'present';
|
|
1791
|
+
const state = stale ? 'stale' : 'pending';
|
|
1792
|
+
if (options.context.json) {
|
|
1793
|
+
options.dependencies.writeDiagnostic(`${JSON.stringify({
|
|
1794
|
+
type: 'gate-project-log-receipt-pending',
|
|
1795
|
+
state,
|
|
1796
|
+
project: options.project,
|
|
1797
|
+
receiptPath,
|
|
1798
|
+
logPath: projectLogPath,
|
|
1799
|
+
...(foreignLogPath !== undefined
|
|
1800
|
+
? { recordedLogPath: foreignLogPath }
|
|
1801
|
+
: {}),
|
|
1802
|
+
runId: receipt.runId,
|
|
1803
|
+
recovery: receipt.recovery?.command,
|
|
1804
|
+
})}\n`);
|
|
1805
|
+
continue;
|
|
1806
|
+
}
|
|
1807
|
+
options.context.logger.warn(`Warning: a ${state} gate project log receipt exists at ${receiptPath}${foreignLogPath !== undefined
|
|
1808
|
+
? ` (it records another tree's log at ${foreignLogPath})`
|
|
1809
|
+
: ''}. Complete it with: ${receipt.recovery?.command ?? 'oat project log append --commit'}`);
|
|
1664
1810
|
}
|
|
1665
1811
|
}
|
|
1812
|
+
/**
|
|
1813
|
+
* Emits the durable partial-finalization receipt and its diagnostic.
|
|
1814
|
+
*
|
|
1815
|
+
* Reached only when the retry budget is exhausted against an index lock: the
|
|
1816
|
+
* entry is in the log but not in a commit, so a later process must finish the
|
|
1817
|
+
* job. The receipt carries the exact recovery command, and the diagnostic
|
|
1818
|
+
* prints it verbatim so an operator or orchestrator can run it without
|
|
1819
|
+
* reconstructing any of the gate's state.
|
|
1820
|
+
*/
|
|
1821
|
+
async function emitGateProjectLogPartialFinalization(options) {
|
|
1822
|
+
const { commit, context, dependencies, finalization } = options;
|
|
1823
|
+
const projectPath = resolveGateProjectPath(finalization.repoRoot, finalization.project);
|
|
1824
|
+
const receiptPath = join(projectPath, GATE_RECEIPTS_DIRNAME, `${finalization.runId}.json`);
|
|
1825
|
+
const command = composeGateProjectLogRecoveryCommand({
|
|
1826
|
+
projectPath,
|
|
1827
|
+
producer: GATE_PROJECT_LOG_PRODUCER,
|
|
1828
|
+
ref: finalization.ref,
|
|
1829
|
+
body: options.body,
|
|
1830
|
+
runId: finalization.runId,
|
|
1831
|
+
});
|
|
1832
|
+
const receipt = {
|
|
1833
|
+
runId: finalization.runId,
|
|
1834
|
+
project: finalization.project,
|
|
1835
|
+
projectPath,
|
|
1836
|
+
worktreeRoot: resolveWorktreeRoot(finalization.repoRoot),
|
|
1837
|
+
logPath: options.logPath,
|
|
1838
|
+
artifactPath: finalization.artifactPath ?? null,
|
|
1839
|
+
artifactSignature: finalization.artifactSignature ?? null,
|
|
1840
|
+
appendStatus: options.appendStatus,
|
|
1841
|
+
commitStatus: commit.outcome,
|
|
1842
|
+
lockClass: commit.lockClass ?? null,
|
|
1843
|
+
attempts: commit.attempts,
|
|
1844
|
+
producer: GATE_PROJECT_LOG_PRODUCER,
|
|
1845
|
+
ref: finalization.ref,
|
|
1846
|
+
body: options.body,
|
|
1847
|
+
recovery: { command },
|
|
1848
|
+
};
|
|
1849
|
+
const written = await dependencies.writeGateProjectLogReceipt(receiptPath, receipt,
|
|
1850
|
+
// `logger.warn` is suppressed in JSON mode, and a receipt warning is
|
|
1851
|
+
// exactly what automation needs to see, so route it the same way the
|
|
1852
|
+
// finalization diagnostics go.
|
|
1853
|
+
(message) => {
|
|
1854
|
+
if (context.json) {
|
|
1855
|
+
dependencies.writeDiagnostic(`${JSON.stringify({
|
|
1856
|
+
type: 'gate-project-log-receipt-warning',
|
|
1857
|
+
project: finalization.project,
|
|
1858
|
+
receiptPath,
|
|
1859
|
+
message,
|
|
1860
|
+
})}\n`);
|
|
1861
|
+
return;
|
|
1862
|
+
}
|
|
1863
|
+
context.logger.warn(message);
|
|
1864
|
+
});
|
|
1865
|
+
if (!written) {
|
|
1866
|
+
return;
|
|
1867
|
+
}
|
|
1868
|
+
if (context.json) {
|
|
1869
|
+
dependencies.writeDiagnostic(`${JSON.stringify({
|
|
1870
|
+
type: 'gate-project-log-partial-finalization',
|
|
1871
|
+
project: finalization.project,
|
|
1872
|
+
receiptPath,
|
|
1873
|
+
logPath: options.logPath,
|
|
1874
|
+
lockClass: receipt.lockClass,
|
|
1875
|
+
attempts: receipt.attempts,
|
|
1876
|
+
recovery: command,
|
|
1877
|
+
})}\n`);
|
|
1878
|
+
return;
|
|
1879
|
+
}
|
|
1880
|
+
context.logger.warn(`Warning: the oat gate review project log entry is appended but not committed (${receipt.commitStatus === 'entry-missing-after-commit'
|
|
1881
|
+
? 'the committed log does not carry this run'
|
|
1882
|
+
: receipt.commitStatus === 'commit-unverified'
|
|
1883
|
+
? 'the committed log could not be read back'
|
|
1884
|
+
: (receipt.lockClass ?? 'unknown')} after ${receipt.attempts} attempts). Receipt: ${receiptPath}. Complete it with: ${command}`);
|
|
1885
|
+
}
|
|
1666
1886
|
async function finalizeReviewGateProjectLog(context, dependencies, finalization) {
|
|
1667
1887
|
const findings = finalization.counts
|
|
1668
1888
|
? ` findings=critical:${finalization.counts.critical},important:${finalization.counts.important},medium:${finalization.counts.medium},minor:${finalization.counts.minor}`
|
|
@@ -1670,7 +1890,11 @@ async function finalizeReviewGateProjectLog(context, dependencies, finalization)
|
|
|
1670
1890
|
const artifact = finalization.artifactPath
|
|
1671
1891
|
? ` artifact=${finalization.artifactPath}`
|
|
1672
1892
|
: '';
|
|
1673
|
-
|
|
1893
|
+
// The run id is the finalization's stable event identity: it makes the entry
|
|
1894
|
+
// recognizable to a retry or to a later recovery, so neither ever appends a
|
|
1895
|
+
// second entry for the same run. It is an additive token on the one-line
|
|
1896
|
+
// structural body, which no consumer parses.
|
|
1897
|
+
const body = `target=${finalization.target} threshold=${finalization.threshold}${findings} exit=${finalization.exitCode} status=${finalization.status}${artifact} run=${finalization.runId}`;
|
|
1674
1898
|
// Finalization runs after the JSON envelope is emitted, and `logger.warn` is
|
|
1675
1899
|
// suppressed in JSON mode. Automation would otherwise get no signal that the
|
|
1676
1900
|
// project log is still dirty, so route these through the diagnostic channel.
|
|
@@ -1695,15 +1919,37 @@ async function finalizeReviewGateProjectLog(context, dependencies, finalization)
|
|
|
1695
1919
|
home: finalization.home,
|
|
1696
1920
|
project: finalization.project,
|
|
1697
1921
|
structural: true,
|
|
1698
|
-
producer:
|
|
1922
|
+
producer: GATE_PROJECT_LOG_PRODUCER,
|
|
1699
1923
|
ref: finalization.ref,
|
|
1700
1924
|
body,
|
|
1925
|
+
idempotencyKey: finalization.runId,
|
|
1701
1926
|
});
|
|
1702
|
-
if (result.status === '
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1706
|
-
|
|
1927
|
+
if (result.status === 'skipped') {
|
|
1928
|
+
return;
|
|
1929
|
+
}
|
|
1930
|
+
// `already-appended` is success: this run's entry is in the log, so the
|
|
1931
|
+
// only work left is the commit.
|
|
1932
|
+
const commit = await commitReviewGateProjectLog(finalization.repoRoot, result.logPath, { key: finalization.runId, body }, dependencies.sleep);
|
|
1933
|
+
if (commit.error != null) {
|
|
1934
|
+
report('gate-project-log-commit-failed', commit.error, result.logPath);
|
|
1935
|
+
}
|
|
1936
|
+
// Both dispositions leave the run's entry uncommitted, and both are
|
|
1937
|
+
// finished by the same idempotent recovery command: the lock case never
|
|
1938
|
+
// reached a commit, and the missing-entry case reached one that did not
|
|
1939
|
+
// carry this run. Either way the durable receipt is what carries the work
|
|
1940
|
+
// into the later process that completes it.
|
|
1941
|
+
if (commit.outcome === 'blocked-by-index-lock' ||
|
|
1942
|
+
commit.outcome === 'entry-missing-after-commit' ||
|
|
1943
|
+
commit.outcome === 'commit-unverified') {
|
|
1944
|
+
await emitGateProjectLogPartialFinalization({
|
|
1945
|
+
context,
|
|
1946
|
+
dependencies,
|
|
1947
|
+
finalization,
|
|
1948
|
+
appendStatus: result.status,
|
|
1949
|
+
logPath: result.logPath,
|
|
1950
|
+
body,
|
|
1951
|
+
commit,
|
|
1952
|
+
});
|
|
1707
1953
|
}
|
|
1708
1954
|
}
|
|
1709
1955
|
catch (error) {
|
|
@@ -1958,6 +2204,216 @@ async function runCrossProviderExec(prompt, options, context, dependencies) {
|
|
|
1958
2204
|
writeError(context, error);
|
|
1959
2205
|
}
|
|
1960
2206
|
}
|
|
2207
|
+
/**
|
|
2208
|
+
* Collects the result envelope before any of it reaches the real logger.
|
|
2209
|
+
*
|
|
2210
|
+
* `writeReviewGateResult` makes several human-mode calls, so a failure partway
|
|
2211
|
+
* through would otherwise leave a half-printed success followed by the
|
|
2212
|
+
* `review_failed` line. Buffering keeps the recovery emission all-or-nothing.
|
|
2213
|
+
*/
|
|
2214
|
+
function createBufferedResultContext(context) {
|
|
2215
|
+
const pending = [];
|
|
2216
|
+
return {
|
|
2217
|
+
context: {
|
|
2218
|
+
...context,
|
|
2219
|
+
logger: {
|
|
2220
|
+
...context.logger,
|
|
2221
|
+
debug: (message) => {
|
|
2222
|
+
pending.push(() => context.logger.debug(message));
|
|
2223
|
+
},
|
|
2224
|
+
info: (message) => {
|
|
2225
|
+
pending.push(() => context.logger.info(message));
|
|
2226
|
+
},
|
|
2227
|
+
warn: (message) => {
|
|
2228
|
+
pending.push(() => context.logger.warn(message));
|
|
2229
|
+
},
|
|
2230
|
+
error: (message) => {
|
|
2231
|
+
pending.push(() => context.logger.error(message));
|
|
2232
|
+
},
|
|
2233
|
+
success: (message) => {
|
|
2234
|
+
pending.push(() => context.logger.success(message));
|
|
2235
|
+
},
|
|
2236
|
+
json: (payload) => {
|
|
2237
|
+
pending.push(() => context.logger.json(payload));
|
|
2238
|
+
},
|
|
2239
|
+
},
|
|
2240
|
+
},
|
|
2241
|
+
flush: () => {
|
|
2242
|
+
for (const emit of pending) {
|
|
2243
|
+
emit();
|
|
2244
|
+
}
|
|
2245
|
+
pending.length = 0;
|
|
2246
|
+
},
|
|
2247
|
+
};
|
|
2248
|
+
}
|
|
2249
|
+
/**
|
|
2250
|
+
* The single eligibility pipeline for a selected review artifact: project
|
|
2251
|
+
* containment, timestamp validity, verdict parsing from the immutable
|
|
2252
|
+
* snapshot, gate-invocation corroboration, the gate invocation marker, and the
|
|
2253
|
+
* blocking threshold.
|
|
2254
|
+
*
|
|
2255
|
+
* Both the normal path and post-selection recovery call this function so that
|
|
2256
|
+
* a recovered artifact can never skip a check the normal path applies. It
|
|
2257
|
+
* never re-reads `snapshot.path`, never re-dispatches a reviewer, and never
|
|
2258
|
+
* writes an envelope itself: an ineligible artifact returns the exact existing
|
|
2259
|
+
* failure writer for its caller to invoke.
|
|
2260
|
+
*/
|
|
2261
|
+
async function disposeValidatedReviewArtifact(snapshot, identity, dependencies, hooks = {}) {
|
|
2262
|
+
hooks.onStep?.('artifact-validation');
|
|
2263
|
+
const outsideReviewProject = snapshot.containingProject !== identity.project;
|
|
2264
|
+
if (outsideReviewProject ||
|
|
2265
|
+
(identity.projectResolutionSource === 'declared' &&
|
|
2266
|
+
identity.targetCorroboration.project !== 'matched')) {
|
|
2267
|
+
const message = outsideReviewProject
|
|
2268
|
+
? 'Review artifact was written outside the resolved review project.'
|
|
2269
|
+
: identity.targetCorroboration.project === 'missing'
|
|
2270
|
+
? 'Review artifact is missing oat_project for the explicitly declared project.'
|
|
2271
|
+
: 'Review artifact project identity does not match the explicitly declared project.';
|
|
2272
|
+
return {
|
|
2273
|
+
eligible: false,
|
|
2274
|
+
status: 'targeting_correlation_failed',
|
|
2275
|
+
cause: outsideReviewProject
|
|
2276
|
+
? 'artifact_outside_review_project'
|
|
2277
|
+
: identity.targetCorroboration.project === 'missing'
|
|
2278
|
+
? 'declared_project_identity_missing'
|
|
2279
|
+
: 'declared_project_identity_mismatched',
|
|
2280
|
+
writeEnvelope: (context) => {
|
|
2281
|
+
writeReviewGateTargetingFailure(context, {
|
|
2282
|
+
runId: identity.runId,
|
|
2283
|
+
target: identity.target,
|
|
2284
|
+
project: identity.project,
|
|
2285
|
+
projectResolutionSource: identity.projectResolutionSource,
|
|
2286
|
+
artifactPath: snapshot.path,
|
|
2287
|
+
generatedAt: snapshot.generatedAt,
|
|
2288
|
+
message,
|
|
2289
|
+
gateInvocation: identity.gateInvocation,
|
|
2290
|
+
dispatchReport: identity.dispatchReport,
|
|
2291
|
+
corroboration: identity.targetCorroboration,
|
|
2292
|
+
});
|
|
2293
|
+
},
|
|
2294
|
+
};
|
|
2295
|
+
}
|
|
2296
|
+
const generatedAt = snapshot.generatedAt;
|
|
2297
|
+
if (!generatedAt || !Number.isFinite(snapshot.generatedTime)) {
|
|
2298
|
+
return {
|
|
2299
|
+
eligible: false,
|
|
2300
|
+
status: 'artifact_validation_failed',
|
|
2301
|
+
cause: 'artifact_generated_at_invalid',
|
|
2302
|
+
writeEnvelope: (context) => {
|
|
2303
|
+
writeReviewGateArtifactValidationFailure(context, {
|
|
2304
|
+
runId: identity.runId,
|
|
2305
|
+
target: identity.target,
|
|
2306
|
+
project: identity.project,
|
|
2307
|
+
projectResolutionSource: identity.projectResolutionSource,
|
|
2308
|
+
artifactPath: snapshot.path,
|
|
2309
|
+
generatedAt: snapshot.generatedAt,
|
|
2310
|
+
message: 'Review artifact oat_generated_at is missing or is not a valid timestamp.',
|
|
2311
|
+
recovery: `Set oat_generated_at to a valid timestamp in ${snapshot.path}, then rerun the gate. Invoke oat-project-review-receive only after the gate returns a receive-eligible result.`,
|
|
2312
|
+
gateInvocation: identity.gateInvocation,
|
|
2313
|
+
dispatchReport: identity.dispatchReport,
|
|
2314
|
+
corroboration: corroborateGateInvocation(identity.gateInvocation, undefined, identity.targetCorroboration),
|
|
2315
|
+
});
|
|
2316
|
+
},
|
|
2317
|
+
};
|
|
2318
|
+
}
|
|
2319
|
+
hooks.onStep?.('verdict-parse');
|
|
2320
|
+
let verdict;
|
|
2321
|
+
try {
|
|
2322
|
+
verdict = await dependencies.parseReviewGateVerdict(join(identity.repoRoot, snapshot.path), {
|
|
2323
|
+
normalizeMissingEmptySeveritySections: true,
|
|
2324
|
+
artifactSnapshot: {
|
|
2325
|
+
content: snapshot.content,
|
|
2326
|
+
signature: snapshot.signature,
|
|
2327
|
+
},
|
|
2328
|
+
});
|
|
2329
|
+
}
|
|
2330
|
+
catch (error) {
|
|
2331
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
2332
|
+
return {
|
|
2333
|
+
eligible: false,
|
|
2334
|
+
status: 'artifact_validation_failed',
|
|
2335
|
+
cause: 'artifact_verdict_unparsable',
|
|
2336
|
+
writeEnvelope: (context) => {
|
|
2337
|
+
writeReviewGateArtifactValidationFailure(context, {
|
|
2338
|
+
runId: identity.runId,
|
|
2339
|
+
target: identity.target,
|
|
2340
|
+
project: identity.project,
|
|
2341
|
+
projectResolutionSource: identity.projectResolutionSource,
|
|
2342
|
+
artifactPath: snapshot.path,
|
|
2343
|
+
generatedAt: snapshot.generatedAt,
|
|
2344
|
+
message: detail,
|
|
2345
|
+
recovery: `The review artifact was created at ${snapshot.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.`,
|
|
2346
|
+
gateInvocation: identity.gateInvocation,
|
|
2347
|
+
dispatchReport: identity.dispatchReport,
|
|
2348
|
+
corroboration: corroborateGateInvocation(identity.gateInvocation, undefined, identity.targetCorroboration),
|
|
2349
|
+
});
|
|
2350
|
+
},
|
|
2351
|
+
};
|
|
2352
|
+
}
|
|
2353
|
+
hooks.onVerdict?.(verdict);
|
|
2354
|
+
hooks.onStep?.('invocation-corroboration');
|
|
2355
|
+
const corroboration = corroborateGateInvocation(identity.gateInvocation, verdict.gateInvocation, identity.targetCorroboration);
|
|
2356
|
+
if (corroboration.run !== 'matched' ||
|
|
2357
|
+
corroboration.invocation !== 'matched') {
|
|
2358
|
+
const missing = corroboration.run === 'missing' || corroboration.invocation === 'missing';
|
|
2359
|
+
return {
|
|
2360
|
+
eligible: false,
|
|
2361
|
+
status: 'artifact_validation_failed',
|
|
2362
|
+
cause: missing
|
|
2363
|
+
? 'gate_invocation_metadata_missing'
|
|
2364
|
+
: 'gate_invocation_metadata_mismatched',
|
|
2365
|
+
verdict,
|
|
2366
|
+
writeEnvelope: (context) => {
|
|
2367
|
+
writeReviewGateArtifactValidationFailure(context, {
|
|
2368
|
+
runId: identity.runId,
|
|
2369
|
+
target: identity.target,
|
|
2370
|
+
project: identity.project,
|
|
2371
|
+
projectResolutionSource: identity.projectResolutionSource,
|
|
2372
|
+
artifactPath: snapshot.path,
|
|
2373
|
+
generatedAt: snapshot.generatedAt,
|
|
2374
|
+
message: missing
|
|
2375
|
+
? 'Review artifact invocation metadata is missing required gate-owned values.'
|
|
2376
|
+
: 'Review artifact invocation metadata does not match the gate-owned configured invocation.',
|
|
2377
|
+
recovery: `Copy the exact gate invocation fields from the review prompt into ${snapshot.path}, then run oat-project-review-receive only after the artifact validates.`,
|
|
2378
|
+
gateInvocation: identity.gateInvocation,
|
|
2379
|
+
dispatchReport: identity.dispatchReport,
|
|
2380
|
+
corroboration,
|
|
2381
|
+
});
|
|
2382
|
+
},
|
|
2383
|
+
};
|
|
2384
|
+
}
|
|
2385
|
+
if (verdict.invocation !== 'gate') {
|
|
2386
|
+
return {
|
|
2387
|
+
eligible: false,
|
|
2388
|
+
status: 'artifact_validation_failed',
|
|
2389
|
+
cause: 'gate_invocation_marker_missing',
|
|
2390
|
+
verdict,
|
|
2391
|
+
writeEnvelope: (context) => {
|
|
2392
|
+
writeReviewGateArtifactValidationFailure(context, {
|
|
2393
|
+
runId: identity.runId,
|
|
2394
|
+
target: identity.target,
|
|
2395
|
+
project: identity.project,
|
|
2396
|
+
projectResolutionSource: identity.projectResolutionSource,
|
|
2397
|
+
artifactPath: snapshot.path,
|
|
2398
|
+
generatedAt: snapshot.generatedAt,
|
|
2399
|
+
message: 'Review artifact is missing the required gate invocation marker `oat_review_invocation: gate`.',
|
|
2400
|
+
recovery: `Set oat_review_invocation: gate in ${snapshot.path}, then run oat-project-review-receive only after the artifact validates.`,
|
|
2401
|
+
gateInvocation: identity.gateInvocation,
|
|
2402
|
+
dispatchReport: identity.dispatchReport,
|
|
2403
|
+
corroboration,
|
|
2404
|
+
});
|
|
2405
|
+
},
|
|
2406
|
+
};
|
|
2407
|
+
}
|
|
2408
|
+
hooks.onStep?.('verdict-disposition');
|
|
2409
|
+
return {
|
|
2410
|
+
eligible: true,
|
|
2411
|
+
verdict,
|
|
2412
|
+
blocking: reviewBlocksAtThreshold(verdict, identity.threshold),
|
|
2413
|
+
corroboration,
|
|
2414
|
+
generatedAt,
|
|
2415
|
+
};
|
|
2416
|
+
}
|
|
1961
2417
|
async function runReviewGate(prompt, options, context, dependencies) {
|
|
1962
2418
|
const runId = randomUUID();
|
|
1963
2419
|
let runMarkerPath;
|
|
@@ -2009,6 +2465,12 @@ async function runReviewGate(prompt, options, context, dependencies) {
|
|
|
2009
2465
|
target: selected.id,
|
|
2010
2466
|
gateInvocation,
|
|
2011
2467
|
dispatchReport,
|
|
2468
|
+
step: 'target-dispatch',
|
|
2469
|
+
};
|
|
2470
|
+
const markPostSelectionStep = (step) => {
|
|
2471
|
+
if (postSelectionContext) {
|
|
2472
|
+
postSelectionContext.step = step;
|
|
2473
|
+
}
|
|
2012
2474
|
};
|
|
2013
2475
|
const threshold = parseReviewGateThreshold(options.exitNonzeroOn);
|
|
2014
2476
|
projectLogFinalization = {
|
|
@@ -2020,12 +2482,21 @@ async function runReviewGate(prompt, options, context, dependencies) {
|
|
|
2020
2482
|
threshold,
|
|
2021
2483
|
status: 'review_failed',
|
|
2022
2484
|
exitCode: 1,
|
|
2485
|
+
runId,
|
|
2023
2486
|
};
|
|
2487
|
+
await warnPendingGateProjectLogReceipts({
|
|
2488
|
+
context,
|
|
2489
|
+
dependencies,
|
|
2490
|
+
repoRoot,
|
|
2491
|
+
project: projectPath,
|
|
2492
|
+
});
|
|
2493
|
+
markPostSelectionStep('artifact-scan');
|
|
2024
2494
|
const before = await listReviewGateArtifactCandidates({
|
|
2025
2495
|
repoRoot,
|
|
2026
2496
|
effective,
|
|
2027
2497
|
reviewProject,
|
|
2028
2498
|
});
|
|
2499
|
+
markPostSelectionStep('target-dispatch');
|
|
2029
2500
|
const reviewPrompt = assembleReviewGatePrompt([
|
|
2030
2501
|
REVIEW_GATE_CONTEXT_NOTE,
|
|
2031
2502
|
reviewGateProjectContext(reviewProject),
|
|
@@ -2115,20 +2586,22 @@ async function runReviewGate(prompt, options, context, dependencies) {
|
|
|
2115
2586
|
process.exitCode = 1;
|
|
2116
2587
|
return true;
|
|
2117
2588
|
};
|
|
2589
|
+
markPostSelectionStep('artifact-scan');
|
|
2118
2590
|
const after = await listReviewGateArtifactCandidates({
|
|
2119
2591
|
repoRoot,
|
|
2120
2592
|
effective,
|
|
2121
2593
|
reviewProject,
|
|
2122
2594
|
});
|
|
2595
|
+
markPostSelectionStep('artifact-correlation');
|
|
2123
2596
|
const artifactResolution = resolveRunCorrelatedReviewArtifact({
|
|
2124
2597
|
runId,
|
|
2125
2598
|
before,
|
|
2126
2599
|
after,
|
|
2127
2600
|
});
|
|
2128
2601
|
if (projectLogFinalization) {
|
|
2129
|
-
|
|
2130
|
-
|
|
2131
|
-
|
|
2602
|
+
const correlated = artifactResolution.artifact ?? artifactResolution.diagnosticArtifact;
|
|
2603
|
+
projectLogFinalization.artifactPath = correlated?.path;
|
|
2604
|
+
projectLogFinalization.artifactSignature = correlated?.signature;
|
|
2132
2605
|
}
|
|
2133
2606
|
if (!artifactResolution.artifact && writeRefusalFailure()) {
|
|
2134
2607
|
return;
|
|
@@ -2236,166 +2709,60 @@ async function runReviewGate(prompt, options, context, dependencies) {
|
|
|
2236
2709
|
process.exitCode = 1;
|
|
2237
2710
|
return;
|
|
2238
2711
|
}
|
|
2239
|
-
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
|
|
2243
|
-
|
|
2244
|
-
|
|
2245
|
-
|
|
2246
|
-
|
|
2247
|
-
|
|
2248
|
-
|
|
2249
|
-
|
|
2250
|
-
|
|
2251
|
-
|
|
2252
|
-
|
|
2253
|
-
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
|
|
2257
|
-
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2261
|
-
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
});
|
|
2265
|
-
process.exitCode = 1;
|
|
2266
|
-
return;
|
|
2267
|
-
}
|
|
2268
|
-
if (!producedArtifact.generatedAt ||
|
|
2269
|
-
!Number.isFinite(producedArtifact.generatedTime)) {
|
|
2270
|
-
if (writeRefusalFailure()) {
|
|
2271
|
-
return;
|
|
2272
|
-
}
|
|
2273
|
-
if (projectLogFinalization) {
|
|
2274
|
-
projectLogFinalization.status = 'artifact_validation_failed';
|
|
2275
|
-
projectLogFinalization.exitCode = 1;
|
|
2276
|
-
}
|
|
2277
|
-
writeReviewGateArtifactValidationFailure(context, {
|
|
2278
|
-
runId,
|
|
2279
|
-
target: selected.id,
|
|
2280
|
-
project: projectPath,
|
|
2281
|
-
projectResolutionSource: reviewProject.source,
|
|
2282
|
-
artifactPath: producedArtifact.path,
|
|
2283
|
-
generatedAt: producedArtifact.generatedAt,
|
|
2284
|
-
message: 'Review artifact oat_generated_at is missing or is not a valid timestamp.',
|
|
2285
|
-
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.`,
|
|
2286
|
-
gateInvocation,
|
|
2287
|
-
dispatchReport,
|
|
2288
|
-
corroboration: corroborateGateInvocation(gateInvocation, undefined, initialTargetCorroboration),
|
|
2289
|
-
});
|
|
2290
|
-
process.exitCode = 1;
|
|
2291
|
-
return;
|
|
2292
|
-
}
|
|
2293
|
-
let verdict;
|
|
2294
|
-
try {
|
|
2295
|
-
verdict = await dependencies.parseReviewGateVerdict(join(repoRoot, producedArtifact.path), {
|
|
2296
|
-
normalizeMissingEmptySeveritySections: true,
|
|
2297
|
-
artifactSnapshot: {
|
|
2298
|
-
content: producedArtifact.content,
|
|
2299
|
-
signature: producedArtifact.signature,
|
|
2300
|
-
},
|
|
2301
|
-
});
|
|
2302
|
-
}
|
|
2303
|
-
catch (error) {
|
|
2304
|
-
if (writeRefusalFailure()) {
|
|
2305
|
-
return;
|
|
2306
|
-
}
|
|
2307
|
-
const detail = error instanceof Error ? error.message : String(error);
|
|
2308
|
-
if (projectLogFinalization) {
|
|
2309
|
-
projectLogFinalization.status = 'artifact_validation_failed';
|
|
2310
|
-
projectLogFinalization.exitCode = 1;
|
|
2311
|
-
}
|
|
2312
|
-
writeReviewGateArtifactValidationFailure(context, {
|
|
2313
|
-
runId,
|
|
2314
|
-
target: selected.id,
|
|
2315
|
-
project: projectPath,
|
|
2316
|
-
projectResolutionSource: reviewProject.source,
|
|
2317
|
-
artifactPath: producedArtifact.path,
|
|
2318
|
-
generatedAt: producedArtifact.generatedAt,
|
|
2319
|
-
message: detail,
|
|
2320
|
-
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.`,
|
|
2321
|
-
gateInvocation,
|
|
2322
|
-
dispatchReport,
|
|
2323
|
-
corroboration: corroborateGateInvocation(gateInvocation, undefined, initialTargetCorroboration),
|
|
2324
|
-
});
|
|
2325
|
-
process.exitCode = 1;
|
|
2326
|
-
return;
|
|
2327
|
-
}
|
|
2328
|
-
if (projectLogFinalization) {
|
|
2329
|
-
projectLogFinalization.counts = verdict.counts;
|
|
2330
|
-
}
|
|
2331
|
-
const targetCorroboration = initialTargetCorroboration;
|
|
2332
|
-
const corroboration = corroborateGateInvocation(gateInvocation, verdict.gateInvocation, targetCorroboration);
|
|
2333
|
-
if (corroboration.run !== 'matched' ||
|
|
2334
|
-
corroboration.invocation !== 'matched') {
|
|
2335
|
-
if (writeRefusalFailure()) {
|
|
2336
|
-
return;
|
|
2337
|
-
}
|
|
2338
|
-
const missing = corroboration.run === 'missing' ||
|
|
2339
|
-
corroboration.invocation === 'missing';
|
|
2340
|
-
if (projectLogFinalization) {
|
|
2341
|
-
projectLogFinalization.status = 'artifact_validation_failed';
|
|
2342
|
-
projectLogFinalization.exitCode = 1;
|
|
2712
|
+
const artifactSnapshot = {
|
|
2713
|
+
path: producedArtifact.path,
|
|
2714
|
+
generatedAt: producedArtifact.generatedAt,
|
|
2715
|
+
generatedTime: producedArtifact.generatedTime,
|
|
2716
|
+
containingProject: producedArtifact.containingProject,
|
|
2717
|
+
content: producedArtifact.content,
|
|
2718
|
+
signature: producedArtifact.signature,
|
|
2719
|
+
};
|
|
2720
|
+
const dispositionIdentity = {
|
|
2721
|
+
runId,
|
|
2722
|
+
repoRoot,
|
|
2723
|
+
target: selected.id,
|
|
2724
|
+
project: projectPath,
|
|
2725
|
+
projectResolutionSource: reviewProject.source,
|
|
2726
|
+
gateInvocation,
|
|
2727
|
+
dispatchReport,
|
|
2728
|
+
targetCorroboration: initialTargetCorroboration,
|
|
2729
|
+
threshold,
|
|
2730
|
+
};
|
|
2731
|
+
if (postSelectionContext) {
|
|
2732
|
+
postSelectionContext.artifact = artifactSnapshot;
|
|
2733
|
+
postSelectionContext.dispositionIdentity = dispositionIdentity;
|
|
2734
|
+
postSelectionContext.diversity = selected.diversity;
|
|
2735
|
+
if (childResult.timedOut) {
|
|
2736
|
+
postSelectionContext.lateCompletion = true;
|
|
2343
2737
|
}
|
|
2344
|
-
writeReviewGateArtifactValidationFailure(context, {
|
|
2345
|
-
runId,
|
|
2346
|
-
target: selected.id,
|
|
2347
|
-
project: projectPath,
|
|
2348
|
-
projectResolutionSource: reviewProject.source,
|
|
2349
|
-
artifactPath: producedArtifact.path,
|
|
2350
|
-
generatedAt: producedArtifact.generatedAt,
|
|
2351
|
-
message: missing
|
|
2352
|
-
? 'Review artifact invocation metadata is missing required gate-owned values.'
|
|
2353
|
-
: 'Review artifact invocation metadata does not match the gate-owned configured invocation.',
|
|
2354
|
-
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.`,
|
|
2355
|
-
gateInvocation,
|
|
2356
|
-
dispatchReport,
|
|
2357
|
-
corroboration,
|
|
2358
|
-
});
|
|
2359
|
-
process.exitCode = 1;
|
|
2360
|
-
return;
|
|
2361
2738
|
}
|
|
2362
|
-
|
|
2739
|
+
const disposition = await disposeValidatedReviewArtifact(artifactSnapshot, dispositionIdentity, dependencies, {
|
|
2740
|
+
onStep: markPostSelectionStep,
|
|
2741
|
+
onVerdict: (parsed) => {
|
|
2742
|
+
if (projectLogFinalization) {
|
|
2743
|
+
projectLogFinalization.counts = parsed.counts;
|
|
2744
|
+
}
|
|
2745
|
+
},
|
|
2746
|
+
});
|
|
2747
|
+
if (!disposition.eligible) {
|
|
2363
2748
|
if (writeRefusalFailure()) {
|
|
2364
2749
|
return;
|
|
2365
2750
|
}
|
|
2366
2751
|
if (projectLogFinalization) {
|
|
2367
|
-
projectLogFinalization.status =
|
|
2752
|
+
projectLogFinalization.status = disposition.status;
|
|
2368
2753
|
projectLogFinalization.exitCode = 1;
|
|
2369
2754
|
}
|
|
2370
|
-
|
|
2371
|
-
runId,
|
|
2372
|
-
target: selected.id,
|
|
2373
|
-
project: projectPath,
|
|
2374
|
-
projectResolutionSource: reviewProject.source,
|
|
2375
|
-
artifactPath: producedArtifact.path,
|
|
2376
|
-
generatedAt: producedArtifact.generatedAt,
|
|
2377
|
-
message: 'Review artifact is missing the required gate invocation marker `oat_review_invocation: gate`.',
|
|
2378
|
-
recovery: `Set oat_review_invocation: gate in ${producedArtifact.path}, then run oat-project-review-receive only after the artifact validates.`,
|
|
2379
|
-
gateInvocation,
|
|
2380
|
-
dispatchReport,
|
|
2381
|
-
corroboration,
|
|
2382
|
-
});
|
|
2755
|
+
disposition.writeEnvelope(context);
|
|
2383
2756
|
process.exitCode = 1;
|
|
2384
2757
|
return;
|
|
2385
2758
|
}
|
|
2386
|
-
const blocking =
|
|
2759
|
+
const { verdict, blocking, corroboration, generatedAt } = disposition;
|
|
2387
2760
|
const handoff = buildReviewGateHandoff({
|
|
2388
2761
|
artifactPath: producedArtifact.path,
|
|
2389
2762
|
verdict,
|
|
2390
2763
|
threshold,
|
|
2391
2764
|
blocking,
|
|
2392
2765
|
});
|
|
2393
|
-
if (projectLogFinalization) {
|
|
2394
|
-
projectLogFinalization.status = blocking ? 'blocked' : 'ok';
|
|
2395
|
-
projectLogFinalization.exitCode = blocking ? 1 : 0;
|
|
2396
|
-
projectLogFinalization.counts = verdict.counts;
|
|
2397
|
-
projectLogFinalization.artifactPath = producedArtifact.path;
|
|
2398
|
-
}
|
|
2399
2766
|
writeReviewGateResult(context, {
|
|
2400
2767
|
status: blocking ? 'blocked' : 'ok',
|
|
2401
2768
|
runId,
|
|
@@ -2403,7 +2770,7 @@ async function runReviewGate(prompt, options, context, dependencies) {
|
|
|
2403
2770
|
project: projectPath,
|
|
2404
2771
|
projectResolutionSource: reviewProject.source,
|
|
2405
2772
|
artifactPath: producedArtifact.path,
|
|
2406
|
-
generatedAt
|
|
2773
|
+
generatedAt,
|
|
2407
2774
|
threshold,
|
|
2408
2775
|
blocking,
|
|
2409
2776
|
counts: verdict.counts,
|
|
@@ -2418,12 +2785,133 @@ async function runReviewGate(prompt, options, context, dependencies) {
|
|
|
2418
2785
|
corroboration,
|
|
2419
2786
|
...(childResult.timedOut ? { lateCompletion: true } : {}),
|
|
2420
2787
|
});
|
|
2788
|
+
// Record the disposition only once the envelope is written, so a failed
|
|
2789
|
+
// emission cannot leave the log claiming a result no caller received.
|
|
2790
|
+
if (projectLogFinalization) {
|
|
2791
|
+
projectLogFinalization.status = blocking ? 'blocked' : 'ok';
|
|
2792
|
+
projectLogFinalization.exitCode = blocking ? 1 : 0;
|
|
2793
|
+
projectLogFinalization.counts = verdict.counts;
|
|
2794
|
+
projectLogFinalization.artifactPath = producedArtifact.path;
|
|
2795
|
+
projectLogFinalization.artifactSignature = producedArtifact.signature;
|
|
2796
|
+
}
|
|
2421
2797
|
process.exitCode = blocking ? 1 : 0;
|
|
2422
2798
|
}
|
|
2423
2799
|
catch (error) {
|
|
2424
2800
|
if (postSelectionContext) {
|
|
2801
|
+
const snapshot = postSelectionContext.artifact;
|
|
2802
|
+
const identity = postSelectionContext.dispositionIdentity;
|
|
2803
|
+
let ineligibility;
|
|
2804
|
+
if (snapshot && identity) {
|
|
2805
|
+
// Recovery re-validates the already-committed snapshot through the same
|
|
2806
|
+
// eligibility function the normal path uses, rather than re-parsing
|
|
2807
|
+
// whatever the artifact path now holds, and never re-dispatches the
|
|
2808
|
+
// reviewer. The path is still read to assert the artifact is present
|
|
2809
|
+
// and unchanged, so a deleted or rewritten artifact fails closed.
|
|
2810
|
+
let disposition;
|
|
2811
|
+
try {
|
|
2812
|
+
disposition = await disposeValidatedReviewArtifact(snapshot, identity, dependencies);
|
|
2813
|
+
}
|
|
2814
|
+
catch (revalidationError) {
|
|
2815
|
+
// The recovery attempt itself failed. Report that distinctly instead
|
|
2816
|
+
// of letting the envelope blame the transient error that triggered
|
|
2817
|
+
// recovery, and leave a diagnostic behind for the same reason.
|
|
2818
|
+
disposition = undefined;
|
|
2819
|
+
ineligibility = 'recovery_revalidation_failed';
|
|
2820
|
+
try {
|
|
2821
|
+
dependencies.writeDiagnostic(`${JSON.stringify({
|
|
2822
|
+
type: 'gate-recovery-failed',
|
|
2823
|
+
runId: identity.runId,
|
|
2824
|
+
target: identity.target,
|
|
2825
|
+
project: identity.project,
|
|
2826
|
+
step: postSelectionContext.step,
|
|
2827
|
+
message: revalidationError instanceof Error
|
|
2828
|
+
? revalidationError.message
|
|
2829
|
+
: String(revalidationError),
|
|
2830
|
+
})}\n`);
|
|
2831
|
+
}
|
|
2832
|
+
catch {
|
|
2833
|
+
// Diagnostics are best effort and must never mask the terminal
|
|
2834
|
+
// review_failed envelope written below.
|
|
2835
|
+
}
|
|
2836
|
+
}
|
|
2837
|
+
if (disposition?.eligible) {
|
|
2838
|
+
// Emission is the last thing that can fail. Build the human-mode
|
|
2839
|
+
// lines into a buffer first so a mid-construction failure cannot
|
|
2840
|
+
// half-print a recovered result and then print `review_failed` after
|
|
2841
|
+
// it, and keep the project-log status at its pre-recovery
|
|
2842
|
+
// `review_failed` value until the envelope is actually written.
|
|
2843
|
+
let recoveryEmitted = false;
|
|
2844
|
+
try {
|
|
2845
|
+
const recoveredHandoff = buildReviewGateHandoff({
|
|
2846
|
+
artifactPath: snapshot.path,
|
|
2847
|
+
verdict: disposition.verdict,
|
|
2848
|
+
threshold: identity.threshold,
|
|
2849
|
+
blocking: disposition.blocking,
|
|
2850
|
+
});
|
|
2851
|
+
const buffered = createBufferedResultContext(context);
|
|
2852
|
+
writeReviewGateResult(buffered.context, {
|
|
2853
|
+
status: disposition.blocking ? 'blocked' : 'ok',
|
|
2854
|
+
runId: identity.runId,
|
|
2855
|
+
target: identity.target,
|
|
2856
|
+
project: identity.project,
|
|
2857
|
+
projectResolutionSource: identity.projectResolutionSource,
|
|
2858
|
+
artifactPath: snapshot.path,
|
|
2859
|
+
generatedAt: disposition.generatedAt,
|
|
2860
|
+
threshold: identity.threshold,
|
|
2861
|
+
blocking: disposition.blocking,
|
|
2862
|
+
counts: disposition.verdict.counts,
|
|
2863
|
+
reviewType: disposition.verdict.reviewType,
|
|
2864
|
+
scope: disposition.verdict.scope,
|
|
2865
|
+
invocation: disposition.verdict.invocation,
|
|
2866
|
+
normalization: disposition.verdict.normalization,
|
|
2867
|
+
handoff: recoveredHandoff,
|
|
2868
|
+
diversity: postSelectionContext.diversity,
|
|
2869
|
+
gateInvocation: identity.gateInvocation,
|
|
2870
|
+
dispatchReport: identity.dispatchReport,
|
|
2871
|
+
corroboration: disposition.corroboration,
|
|
2872
|
+
...(postSelectionContext.lateCompletion
|
|
2873
|
+
? { lateCompletion: true }
|
|
2874
|
+
: {}),
|
|
2875
|
+
postSelectionRecovery: true,
|
|
2876
|
+
});
|
|
2877
|
+
buffered.flush();
|
|
2878
|
+
recoveryEmitted = true;
|
|
2879
|
+
}
|
|
2880
|
+
catch {
|
|
2881
|
+
recoveryEmitted = false;
|
|
2882
|
+
}
|
|
2883
|
+
if (recoveryEmitted) {
|
|
2884
|
+
if (projectLogFinalization) {
|
|
2885
|
+
projectLogFinalization.status = disposition.blocking
|
|
2886
|
+
? 'blocked'
|
|
2887
|
+
: 'ok';
|
|
2888
|
+
projectLogFinalization.exitCode = disposition.blocking ? 1 : 0;
|
|
2889
|
+
projectLogFinalization.counts = disposition.verdict.counts;
|
|
2890
|
+
projectLogFinalization.artifactPath = snapshot.path;
|
|
2891
|
+
projectLogFinalization.artifactSignature = snapshot.signature;
|
|
2892
|
+
}
|
|
2893
|
+
process.exitCode = disposition.blocking ? 1 : 0;
|
|
2894
|
+
return;
|
|
2895
|
+
}
|
|
2896
|
+
}
|
|
2897
|
+
if (disposition && !disposition.eligible) {
|
|
2898
|
+
ineligibility = disposition.cause;
|
|
2899
|
+
}
|
|
2900
|
+
}
|
|
2901
|
+
// The terminal envelope is review_failed, so the log must say so even if
|
|
2902
|
+
// an earlier disposition had already been staged.
|
|
2903
|
+
if (projectLogFinalization) {
|
|
2904
|
+
projectLogFinalization.status = 'review_failed';
|
|
2905
|
+
projectLogFinalization.exitCode = 1;
|
|
2906
|
+
}
|
|
2425
2907
|
writeReviewGateUnexpectedFailure(context, {
|
|
2426
|
-
|
|
2908
|
+
project: postSelectionContext.project,
|
|
2909
|
+
projectResolutionSource: postSelectionContext.projectResolutionSource,
|
|
2910
|
+
target: postSelectionContext.target,
|
|
2911
|
+
gateInvocation: postSelectionContext.gateInvocation,
|
|
2912
|
+
dispatchReport: postSelectionContext.dispatchReport,
|
|
2913
|
+
step: postSelectionContext.step,
|
|
2914
|
+
...(ineligibility ? { ineligibility } : {}),
|
|
2427
2915
|
error,
|
|
2428
2916
|
});
|
|
2429
2917
|
process.exitCode = 1;
|