@open-agent-toolkit/cli 0.2.79 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/agents/oat-phase-implementer.md +5 -5
- package/assets/agents/oat-reviewer.md +15 -15
- package/assets/bundle-metadata.json +1 -1
- package/assets/docs/cli-utilities/workflow-gates.md +1 -1
- package/assets/docs/workflows/projects/artifacts.md +2 -2
- package/assets/docs/workflows/projects/autonomy.md +1 -1
- package/assets/docs/workflows/projects/implementation-execution.md +4 -4
- package/assets/docs/workflows/projects/lifecycle.md +1 -1
- package/assets/docs/workflows/projects/reviews.md +11 -11
- package/assets/public-package-versions.json +4 -4
- package/assets/skills/oat-agent-instructions-analyze/SKILL.md +3 -3
- package/assets/skills/oat-docs-analyze/SKILL.md +3 -3
- package/assets/skills/oat-project-autonomous/SKILL.md +2 -2
- package/assets/skills/oat-project-autonomous/references/gate-inventory.md +14 -14
- package/assets/skills/oat-project-design/SKILL.md +2 -2
- package/assets/skills/oat-project-discover/SKILL.md +2 -2
- package/assets/skills/oat-project-document/SKILL.md +1 -1
- package/assets/skills/oat-project-document/references/docs/autonomy-contract.md +14 -14
- package/assets/skills/oat-project-implement/SKILL.md +2 -2
- package/assets/skills/oat-project-implement/references/completion-and-closeout.md +1 -1
- package/assets/skills/oat-project-implement/references/docs/autonomy-contract.md +14 -14
- package/assets/skills/oat-project-implement/references/phase-execution.md +3 -3
- package/assets/skills/oat-project-implement/references/plan-and-resume.md +2 -2
- package/assets/skills/oat-project-import-plan/SKILL.md +2 -2
- package/assets/skills/oat-project-lite/SKILL.md +1 -1
- package/assets/skills/oat-project-lite/references/docs/autonomy-contract.md +14 -14
- package/assets/skills/oat-project-plan/SKILL.md +2 -2
- package/assets/skills/oat-project-plan-writing/SKILL.md +4 -4
- package/assets/skills/oat-project-pr-final/SKILL.md +1 -1
- package/assets/skills/oat-project-pr-final/references/docs/autonomy-contract.md +14 -14
- package/assets/skills/oat-project-quick-start/SKILL.md +2 -2
- package/assets/skills/oat-project-quick-start/references/docs/autonomy-contract.md +14 -14
- package/assets/skills/oat-project-review-provide/SKILL.md +14 -14
- package/assets/skills/oat-project-review-provide-remote/SKILL.md +10 -10
- package/assets/skills/oat-project-review-receive/SKILL.md +64 -45
- package/assets/skills/oat-project-review-receive-remote/SKILL.md +6 -6
- package/assets/skills/oat-project-revise/SKILL.md +2 -2
- package/assets/skills/oat-review-provide/SKILL.md +2 -2
- package/assets/skills/oat-review-provide/references/review-artifact-template.md +3 -3
- package/assets/skills/oat-review-provide-remote/SKILL.md +11 -11
- package/assets/skills/oat-review-receive/SKILL.md +18 -17
- package/assets/skills/oat-review-receive-remote/SKILL.md +13 -13
- package/assets/skills/oat-wave-execute/SKILL.md +2 -2
- package/assets/templates/plan.md +1 -1
- package/dist/commands/gate/__fixtures__/fake-runtime.mjs +4 -4
- package/dist/commands/gate/index.d.ts.map +1 -1
- package/dist/commands/gate/index.js +20 -11
- package/dist/commands/gate/review-verdict.d.ts +2 -2
- package/dist/commands/gate/review-verdict.d.ts.map +1 -1
- package/dist/commands/gate/review-verdict.js +223 -57
- package/dist/review-remote/body-builder.d.ts +3 -3
- package/dist/review-remote/body-builder.d.ts.map +1 -1
- package/dist/review-remote/body-builder.js +12 -12
- package/dist/review-remote/reviewer-dispatch.d.ts +2 -2
- package/dist/review-remote/reviewer-dispatch.d.ts.map +1 -1
- package/dist/review-remote/reviewer-dispatch.js +15 -7
- package/package.json +2 -2
|
@@ -5,7 +5,7 @@ disable-model-invocation: true
|
|
|
5
5
|
user-invocable: true
|
|
6
6
|
allowed-tools: Read, Write, Bash, Glob, Grep, AskUserQuestion
|
|
7
7
|
metadata:
|
|
8
|
-
version: 1.3.
|
|
8
|
+
version: 1.3.2
|
|
9
9
|
---
|
|
10
10
|
|
|
11
11
|
# Remote Review Receive (Ad-hoc GitHub PR)
|
|
@@ -71,8 +71,8 @@ Normalize every finding to this shape:
|
|
|
71
71
|
|
|
72
72
|
```yaml
|
|
73
73
|
finding:
|
|
74
|
-
id: "C1" | "
|
|
75
|
-
severity: critical |
|
|
74
|
+
id: "C1" | "H1" | "M1" | "L1"
|
|
75
|
+
severity: critical | high | medium | low
|
|
76
76
|
title: string
|
|
77
77
|
file: string | null
|
|
78
78
|
line: number | null
|
|
@@ -86,9 +86,9 @@ finding:
|
|
|
86
86
|
Severity conventions:
|
|
87
87
|
|
|
88
88
|
- `critical`: Broken behavior, security risk, or missing P0 requirement.
|
|
89
|
-
- `
|
|
89
|
+
- `high`: Missing P1 requirement, major robustness issue.
|
|
90
90
|
- `medium`: Meaningful but non-blocking quality/maintainability issue.
|
|
91
|
-
- `
|
|
91
|
+
- `low`: Cosmetic/style/documentation issue.
|
|
92
92
|
|
|
93
93
|
## Process
|
|
94
94
|
|
|
@@ -126,16 +126,16 @@ For each item in JSON:
|
|
|
126
126
|
|
|
127
127
|
- Capture `type` (`review_comment`, `issue_comment`, `review`).
|
|
128
128
|
- Map location fields (`path`, `line`) when present.
|
|
129
|
-
- Use comment body + context to assign severity (`critical`, `
|
|
130
|
-
- Treat `CHANGES_REQUESTED` review state as a strong hint toward `
|
|
129
|
+
- Use comment body + context to assign severity (`critical`, `high`, `medium`, `low`).
|
|
130
|
+
- Treat `CHANGES_REQUESTED` review state as a strong hint toward `high+`, not an automatic override.
|
|
131
131
|
- Normalize into the shared findings model.
|
|
132
132
|
|
|
133
133
|
ID assignment per severity bucket:
|
|
134
134
|
|
|
135
135
|
- Critical: `C1`, `C2`, ...
|
|
136
|
-
-
|
|
136
|
+
- High: `H1`, `H2`, ...
|
|
137
137
|
- Medium: `M1`, `M2`, ...
|
|
138
|
-
-
|
|
138
|
+
- Low: `L1`, `L2`, ...
|
|
139
139
|
|
|
140
140
|
### Step 4: Present Findings Overview and Triage
|
|
141
141
|
|
|
@@ -146,21 +146,21 @@ Before triage prompts, output:
|
|
|
146
146
|
|
|
147
147
|
Disposition options per finding:
|
|
148
148
|
|
|
149
|
-
- `convert` (default for critical/
|
|
149
|
+
- `convert` (default for critical/high/medium/low)
|
|
150
150
|
- `defer`
|
|
151
151
|
- `dismiss`
|
|
152
152
|
|
|
153
153
|
Rules:
|
|
154
154
|
|
|
155
|
-
- Require concrete rationale for `defer`/`dismiss` at any severity, including
|
|
156
|
-
- For any deferral (
|
|
155
|
+
- Require concrete rationale for `defer`/`dismiss` at any severity, including low. Small findings are usually cheaper to fix inline than to track as backlog items, so a low `defer` must be justified just like any other deferral.
|
|
156
|
+
- For any deferral (low included), require a concrete reason (duplicate, dependency, explicit out-of-scope follow-up, risky churn).
|
|
157
157
|
|
|
158
158
|
### Step 5: Generate Standalone Task List
|
|
159
159
|
|
|
160
160
|
Task entry format:
|
|
161
161
|
|
|
162
162
|
```markdown
|
|
163
|
-
- [ ] [
|
|
163
|
+
- [ ] [high] Add null-guard in OAuth callback parser (`packages/auth/src/callback.ts:142`) - Validate provider payload before dereference.
|
|
164
164
|
```
|
|
165
165
|
|
|
166
166
|
Output modes:
|
|
@@ -6,7 +6,7 @@ disable-model-invocation: false
|
|
|
6
6
|
user-invocable: true
|
|
7
7
|
allowed-tools: Read, Write, Edit, Bash, Grep, Glob, Task
|
|
8
8
|
metadata:
|
|
9
|
-
version: 1.9.
|
|
9
|
+
version: 1.9.5
|
|
10
10
|
---
|
|
11
11
|
|
|
12
12
|
# Execute a Wave of External Plans
|
|
@@ -181,7 +181,7 @@ excluded from groups) — never mid-run. **Reconciliation contract:** when recon
|
|
|
181
181
|
reveals a stale plan premise, the reconciliation must be NON-NARROWING (WHERE
|
|
182
182
|
the work happens may change; WHAT must be true may not), recorded exactly once
|
|
183
183
|
in the Drift Refresh Record with pointer-only references elsewhere — a
|
|
184
|
-
reconciliation that waives a source-plan requirement is a plan-gate
|
|
184
|
+
reconciliation that waives a source-plan requirement is a plan-gate High
|
|
185
185
|
(wave-4 evidence).
|
|
186
186
|
|
|
187
187
|
Recon is delegated; the Drift Refresh Record entries and any reconciliation
|
package/assets/templates/plan.md
CHANGED
|
@@ -189,7 +189,7 @@ cell; never truncate a widened row back to five columns.
|
|
|
189
189
|
- `received`: review artifact exists (not yet converted into fix tasks)
|
|
190
190
|
- `fixes_added`: fix tasks were added to the plan (work queued)
|
|
191
191
|
- `fixes_completed`: fix tasks implemented, awaiting re-review
|
|
192
|
-
- `passed`: re-review run and recorded as passing (no Critical/
|
|
192
|
+
- `passed`: re-review run and recorded as passing (no Critical/High)
|
|
193
193
|
|
|
194
194
|
---
|
|
195
195
|
|
|
@@ -55,9 +55,9 @@ async function writeArtifact() {
|
|
|
55
55
|
`oat_invocation_reasoning_effort: ${promptValue('oat_invocation_reasoning_effort')}`,
|
|
56
56
|
`oat_invocation_source: ${promptValue('oat_invocation_source')}`,
|
|
57
57
|
'oat_review_critical_count: 0',
|
|
58
|
-
'
|
|
58
|
+
'oat_review_high_count: 0',
|
|
59
59
|
'oat_review_medium_count: 0',
|
|
60
|
-
'
|
|
60
|
+
'oat_review_low_count: 0',
|
|
61
61
|
'---',
|
|
62
62
|
'',
|
|
63
63
|
'# Fake runtime review',
|
|
@@ -68,7 +68,7 @@ async function writeArtifact() {
|
|
|
68
68
|
'',
|
|
69
69
|
'None',
|
|
70
70
|
'',
|
|
71
|
-
'###
|
|
71
|
+
'### High',
|
|
72
72
|
'',
|
|
73
73
|
'None',
|
|
74
74
|
'',
|
|
@@ -76,7 +76,7 @@ async function writeArtifact() {
|
|
|
76
76
|
'',
|
|
77
77
|
'None',
|
|
78
78
|
'',
|
|
79
|
-
'###
|
|
79
|
+
'### Low',
|
|
80
80
|
'',
|
|
81
81
|
'None',
|
|
82
82
|
'',
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/commands/gate/index.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/commands/gate/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -65,10 +65,18 @@ const VALID_CROSS_PROVIDER_AVOIDS = [
|
|
|
65
65
|
];
|
|
66
66
|
const VALID_REVIEW_GATE_THRESHOLDS = [
|
|
67
67
|
'critical',
|
|
68
|
-
'
|
|
68
|
+
'high',
|
|
69
69
|
'medium',
|
|
70
|
-
'
|
|
70
|
+
'low',
|
|
71
71
|
];
|
|
72
|
+
/**
|
|
73
|
+
* Threshold aliases accepted from configuration written before the severity
|
|
74
|
+
* rename, so an existing `exit_nonzero_on: important` keeps its meaning.
|
|
75
|
+
*/
|
|
76
|
+
const LEGACY_REVIEW_GATE_THRESHOLDS = new Map([
|
|
77
|
+
['important', 'high'],
|
|
78
|
+
['minor', 'low'],
|
|
79
|
+
]);
|
|
72
80
|
const VALID_IDENTITY_PROVENANCES = [
|
|
73
81
|
'declared',
|
|
74
82
|
'observed',
|
|
@@ -76,7 +84,7 @@ const VALID_IDENTITY_PROVENANCES = [
|
|
|
76
84
|
'unknown',
|
|
77
85
|
];
|
|
78
86
|
const REVIEW_GATE_CONTEXT_NOTE = [
|
|
79
|
-
'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`, `###
|
|
87
|
+
'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`, `### High`, `### Medium`, and `### Low` headings in that order, using `None` for empty sections.',
|
|
80
88
|
'Complete the review, artifact write, and required bookkeeping inline or through a synchronously awaited child before this headless process exits. Do not start background tasks, monitors, or waiters that outlive this turn.',
|
|
81
89
|
"Artifact hygiene contract: Before finishing or committing, format every file you created or edited. Use the concrete write/fix formatting command supplied by the governing plan, task, or brief. If none is usable, discover the repository's documented write/fix command from applicable `AGENTS.md`/`CLAUDE.md` instructions and relevant package manifests; do not infer or hardcode a formatter. Prefer a file-scoped invocation when supported, and avoid rewriting unrelated files. If no command is discoverable, warn once with `no format command discovered in repo instructions; skipping`, then continue.",
|
|
82
90
|
].join('\n\n');
|
|
@@ -349,9 +357,10 @@ function parseCrossProviderAvoid(value) {
|
|
|
349
357
|
return avoid;
|
|
350
358
|
}
|
|
351
359
|
function parseReviewGateThreshold(value) {
|
|
352
|
-
const
|
|
360
|
+
const raw = value?.trim() || 'high';
|
|
361
|
+
const threshold = LEGACY_REVIEW_GATE_THRESHOLDS.get(raw) ?? raw;
|
|
353
362
|
if (!VALID_REVIEW_GATE_THRESHOLDS.includes(threshold)) {
|
|
354
|
-
throw new Error('--exit-nonzero-on must be one of critical |
|
|
363
|
+
throw new Error('--exit-nonzero-on must be one of critical | high | medium | low.');
|
|
355
364
|
}
|
|
356
365
|
return threshold;
|
|
357
366
|
}
|
|
@@ -1421,10 +1430,10 @@ function reviewBlocksAtThreshold(verdict, threshold) {
|
|
|
1421
1430
|
if (threshold === 'critical') {
|
|
1422
1431
|
return false;
|
|
1423
1432
|
}
|
|
1424
|
-
if (verdict.counts.
|
|
1433
|
+
if (verdict.counts.high > 0) {
|
|
1425
1434
|
return true;
|
|
1426
1435
|
}
|
|
1427
|
-
if (threshold === '
|
|
1436
|
+
if (threshold === 'high') {
|
|
1428
1437
|
return false;
|
|
1429
1438
|
}
|
|
1430
1439
|
if (verdict.counts.medium > 0) {
|
|
@@ -1433,7 +1442,7 @@ function reviewBlocksAtThreshold(verdict, threshold) {
|
|
|
1433
1442
|
if (threshold === 'medium') {
|
|
1434
1443
|
return false;
|
|
1435
1444
|
}
|
|
1436
|
-
return verdict.counts.
|
|
1445
|
+
return verdict.counts.low > 0;
|
|
1437
1446
|
}
|
|
1438
1447
|
function buildReviewGateHandoff(options) {
|
|
1439
1448
|
const receiveInstruction = `Run oat-project-review-receive for ${options.artifactPath} before treating this gate review as consumed.`;
|
|
@@ -1481,7 +1490,7 @@ function writeReviewGateResult(context, payload) {
|
|
|
1481
1490
|
if (payload.normalization) {
|
|
1482
1491
|
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).`);
|
|
1483
1492
|
}
|
|
1484
|
-
context.logger.info(`Verdict: ${payload.status} (critical=${payload.counts.critical},
|
|
1493
|
+
context.logger.info(`Verdict: ${payload.status} (critical=${payload.counts.critical}, high=${payload.counts.high}, medium=${payload.counts.medium}, low=${payload.counts.low})`);
|
|
1485
1494
|
if (payload.diversity) {
|
|
1486
1495
|
if (payload.diversity.warning) {
|
|
1487
1496
|
context.logger.warn(payload.diversity.warning);
|
|
@@ -1893,7 +1902,7 @@ async function emitGateProjectLogPartialFinalization(options) {
|
|
|
1893
1902
|
}
|
|
1894
1903
|
async function finalizeReviewGateProjectLog(context, dependencies, finalization) {
|
|
1895
1904
|
const findings = finalization.counts
|
|
1896
|
-
? ` findings=critical:${finalization.counts.critical},
|
|
1905
|
+
? ` findings=critical:${finalization.counts.critical},high:${finalization.counts.high},medium:${finalization.counts.medium},low:${finalization.counts.low}`
|
|
1897
1906
|
: '';
|
|
1898
1907
|
const artifact = finalization.artifactPath
|
|
1899
1908
|
? ` artifact=${finalization.artifactPath}`
|
|
@@ -3012,7 +3021,7 @@ export function createGateCommand(overrides = {}) {
|
|
|
3012
3021
|
.option('--project <path-or-name>', 'Project path or name to review; defaults to the active project')
|
|
3013
3022
|
.option('--review-scope <scope>', 'Review scope hint for the provider')
|
|
3014
3023
|
.option('--review-type <type>', 'Review type hint for the provider')
|
|
3015
|
-
.option('--exit-nonzero-on <severity>', 'Lowest severity that exits nonzero: critical,
|
|
3024
|
+
.option('--exit-nonzero-on <severity>', 'Lowest severity that exits nonzero: critical, high, medium, or low')
|
|
3016
3025
|
.argument('<prompt...>', 'Review prompt arguments appended to the target command')
|
|
3017
3026
|
.action(async (prompt, options, command) => {
|
|
3018
3027
|
const context = dependencies.buildCommandContext(readGlobalOptions(command));
|
|
@@ -7,9 +7,9 @@ export interface ReviewGateVerdict {
|
|
|
7
7
|
gateInvocation?: ReviewArtifactGateInvocation;
|
|
8
8
|
counts: {
|
|
9
9
|
critical: number;
|
|
10
|
-
|
|
10
|
+
high: number;
|
|
11
11
|
medium: number;
|
|
12
|
-
|
|
12
|
+
low: number;
|
|
13
13
|
};
|
|
14
14
|
blocking: boolean;
|
|
15
15
|
normalization?: {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"review-verdict.d.ts","sourceRoot":"","sources":["../../../src/commands/gate/review-verdict.ts"],"names":[],"mappings":"AAMA,MAAM,WAAW,iBAAiB;IAChC,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,GAAG,UAAU,GAAG,SAAS,CAAC;IAC5C,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,cAAc,CAAC,EAAE,4BAA4B,CAAC;IAC9C,MAAM,EAAE;QACN,QAAQ,EAAE,MAAM,CAAC;QACjB,
|
|
1
|
+
{"version":3,"file":"review-verdict.d.ts","sourceRoot":"","sources":["../../../src/commands/gate/review-verdict.ts"],"names":[],"mappings":"AAMA,MAAM,WAAW,iBAAiB;IAChC,YAAY,EAAE,MAAM,CAAC;IACrB,UAAU,EAAE,MAAM,GAAG,UAAU,GAAG,SAAS,CAAC;IAC5C,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,cAAc,CAAC,EAAE,4BAA4B,CAAC;IAC9C,MAAM,EAAE;QACN,QAAQ,EAAE,MAAM,CAAC;QACjB,IAAI,EAAE,MAAM,CAAC;QACb,MAAM,EAAE,MAAM,CAAC;QACf,GAAG,EAAE,MAAM,CAAC;KACb,CAAC;IACF,QAAQ,EAAE,OAAO,CAAC;IAClB,aAAa,CAAC,EAAE;QACd,kBAAkB,EAAE,QAAQ,EAAE,CAAC;QAC/B,SAAS,EAAE,OAAO,CAAC;KACpB,CAAC;CACH;AAED,MAAM,WAAW,4BAA4B;IAC3C,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,eAAe,EAAE,MAAM,GAAG,IAAI,CAAC;IAC/B,MAAM,EAAE,MAAM,GAAG,IAAI,CAAC;CACvB;AAED,MAAM,MAAM,QAAQ,GAAG,MAAM,iBAAiB,CAAC,QAAQ,CAAC,CAAC;AAEzD,MAAM,WAAW,0BAA0B;IACzC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;CAC5B;AAED,MAAM,WAAW,6BAA6B;IAC5C,qCAAqC,CAAC,EAAE,OAAO,CAAC;IAChD,gBAAgB,CAAC,EAAE,0BAA0B,CAAC;CAC/C;AAmOD,wBAAgB,mBAAmB,CAAC,QAAQ,EAAE,QAAQ,GAAG,MAAM,CAE9D;AA2eD,wBAAsB,sBAAsB,CAC1C,YAAY,EAAE,MAAM,EACpB,OAAO,GAAE,6BAAkC,GAC1C,OAAO,CAAC,iBAAiB,CAAC,CA8F5B"}
|
|
@@ -2,17 +2,12 @@ import { createHash } from 'node:crypto';
|
|
|
2
2
|
import { readFile, writeFile } from 'node:fs/promises';
|
|
3
3
|
import { getFrontmatterBlock } from '../shared/frontmatter.js';
|
|
4
4
|
import YAML from 'yaml';
|
|
5
|
-
const SEVERITIES = [
|
|
6
|
-
'critical',
|
|
7
|
-
'important',
|
|
8
|
-
'medium',
|
|
9
|
-
'minor',
|
|
10
|
-
];
|
|
5
|
+
const SEVERITIES = ['critical', 'high', 'medium', 'low'];
|
|
11
6
|
const FRONTMATTER_COUNT_KEYS = {
|
|
12
7
|
critical: ['oat_review_critical_count', 'critical'],
|
|
13
|
-
|
|
8
|
+
high: ['oat_review_high_count', 'high'],
|
|
14
9
|
medium: ['oat_review_medium_count', 'medium'],
|
|
15
|
-
|
|
10
|
+
low: ['oat_review_low_count', 'low'],
|
|
16
11
|
};
|
|
17
12
|
function artifactContentSignature(content) {
|
|
18
13
|
return createHash('sha256').update(content).digest('hex');
|
|
@@ -59,6 +54,9 @@ function readGateInvocation(frontmatter) {
|
|
|
59
54
|
};
|
|
60
55
|
}
|
|
61
56
|
function parseCountValue(value) {
|
|
57
|
+
if (typeof value === 'string' && value.trim().length === 0) {
|
|
58
|
+
return null;
|
|
59
|
+
}
|
|
62
60
|
const numberValue = typeof value === 'number'
|
|
63
61
|
? value
|
|
64
62
|
: typeof value === 'string'
|
|
@@ -69,40 +67,58 @@ function parseCountValue(value) {
|
|
|
69
67
|
}
|
|
70
68
|
return numberValue;
|
|
71
69
|
}
|
|
72
|
-
function readFrontmatterCounts(frontmatter) {
|
|
70
|
+
function readFrontmatterCounts(frontmatter, artifactPath) {
|
|
71
|
+
const hasNestedCounts = Object.hasOwn(frontmatter, 'oat_review_counts');
|
|
72
|
+
if (hasNestedCounts &&
|
|
73
|
+
(typeof frontmatter['oat_review_counts'] !== 'object' ||
|
|
74
|
+
frontmatter['oat_review_counts'] === null ||
|
|
75
|
+
Array.isArray(frontmatter['oat_review_counts']))) {
|
|
76
|
+
throw new Error(`Review artifact at ${artifactPath} declares an invalid oat_review_counts block; it must be an object of non-negative integer counts.`);
|
|
77
|
+
}
|
|
73
78
|
const nestedCounts = typeof frontmatter['oat_review_counts'] === 'object' &&
|
|
74
79
|
frontmatter['oat_review_counts'] !== null &&
|
|
75
80
|
!Array.isArray(frontmatter['oat_review_counts'])
|
|
76
81
|
? frontmatter['oat_review_counts']
|
|
77
82
|
: null;
|
|
78
|
-
const
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
minor: 0,
|
|
83
|
-
};
|
|
83
|
+
const parsed = new Map();
|
|
84
|
+
// Validate every supplied alias before deciding whether the block is
|
|
85
|
+
// complete. A partial block must not be able to hide a conflicting pair and
|
|
86
|
+
// fall through to a summary line that reports fewer findings.
|
|
84
87
|
for (const severity of SEVERITIES) {
|
|
85
88
|
const candidateValues = [
|
|
86
|
-
...FRONTMATTER_COUNT_KEYS[severity].
|
|
89
|
+
...FRONTMATTER_COUNT_KEYS[severity].flatMap((key) => Object.hasOwn(frontmatter, key) ? [frontmatter[key]] : []),
|
|
87
90
|
...(nestedCounts
|
|
88
|
-
? FRONTMATTER_COUNT_KEYS[severity].
|
|
91
|
+
? FRONTMATTER_COUNT_KEYS[severity].flatMap((key) => Object.hasOwn(nestedCounts, key) ? [nestedCounts[key]] : [])
|
|
89
92
|
: []),
|
|
90
|
-
]
|
|
91
|
-
|
|
92
|
-
|
|
93
|
+
];
|
|
94
|
+
const hasInvalidValue = candidateValues.some((value) => parseCountValue(value) === null);
|
|
95
|
+
if (hasInvalidValue) {
|
|
96
|
+
throw new Error(`Review artifact at ${artifactPath} declares an invalid ${severity} count; counts must be non-negative integers.`);
|
|
93
97
|
}
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
98
|
+
const distinct = [
|
|
99
|
+
...new Set(candidateValues
|
|
100
|
+
.map((value) => parseCountValue(value))
|
|
101
|
+
.filter((value) => value !== null)),
|
|
102
|
+
];
|
|
103
|
+
if (distinct.length > 1) {
|
|
104
|
+
throw new Error(`Review artifact at ${artifactPath} declares conflicting ${severity} counts (${distinct.join(', ')}); resolve the frontmatter before the gate can evaluate the review.`);
|
|
105
|
+
}
|
|
106
|
+
if (distinct.length === 1) {
|
|
107
|
+
parsed.set(severity, distinct[0]);
|
|
102
108
|
}
|
|
103
|
-
counts[severity] = parsedCount;
|
|
104
109
|
}
|
|
105
|
-
|
|
110
|
+
if (parsed.size === 0) {
|
|
111
|
+
return null;
|
|
112
|
+
}
|
|
113
|
+
return {
|
|
114
|
+
counts: {
|
|
115
|
+
critical: parsed.get('critical') ?? 0,
|
|
116
|
+
high: parsed.get('high') ?? 0,
|
|
117
|
+
medium: parsed.get('medium') ?? 0,
|
|
118
|
+
low: parsed.get('low') ?? 0,
|
|
119
|
+
},
|
|
120
|
+
tiers: [...parsed.keys()],
|
|
121
|
+
};
|
|
106
122
|
}
|
|
107
123
|
function sectionContentIsEmpty(content) {
|
|
108
124
|
const cleaned = content
|
|
@@ -146,15 +162,25 @@ function markdownLines(content) {
|
|
|
146
162
|
return lines;
|
|
147
163
|
}
|
|
148
164
|
function fenceMarker(line) {
|
|
149
|
-
const match = line.match(/^\s*(`{3,}|~{3,})
|
|
165
|
+
const match = line.match(/^\s*(`{3,}|~{3,})(.*)$/);
|
|
150
166
|
if (!match?.[1]) {
|
|
151
167
|
return null;
|
|
152
168
|
}
|
|
153
169
|
return {
|
|
154
170
|
marker: match[1].startsWith('`') ? '`' : '~',
|
|
155
171
|
length: match[1].length,
|
|
172
|
+
trailing: match[2] ?? '',
|
|
156
173
|
};
|
|
157
174
|
}
|
|
175
|
+
/**
|
|
176
|
+
* Lines outside fenced code blocks.
|
|
177
|
+
*
|
|
178
|
+
* An opening fence may carry an info string (```ts), but a closing fence may
|
|
179
|
+
* not: a marker run followed by other text is fence content, not a delimiter.
|
|
180
|
+
* Treating such a line as a closer ends the fence early and exposes quoted
|
|
181
|
+
* examples to the count scans, which is the failure the fence-awareness is
|
|
182
|
+
* there to prevent.
|
|
183
|
+
*/
|
|
158
184
|
function linesOutsideFences(content) {
|
|
159
185
|
const outsideFenceLines = [];
|
|
160
186
|
let activeFence = null;
|
|
@@ -165,7 +191,8 @@ function linesOutsideFences(content) {
|
|
|
165
191
|
activeFence = marker;
|
|
166
192
|
}
|
|
167
193
|
else if (marker.marker === activeFence.marker &&
|
|
168
|
-
marker.length >= activeFence.length
|
|
194
|
+
marker.length >= activeFence.length &&
|
|
195
|
+
marker.trailing.trim() === '') {
|
|
169
196
|
activeFence = null;
|
|
170
197
|
}
|
|
171
198
|
continue;
|
|
@@ -211,39 +238,114 @@ function missingSeverityHeadings(severityHeadings) {
|
|
|
211
238
|
const seenSeverities = new Set(severityHeadings.map((heading) => heading.severity));
|
|
212
239
|
return SEVERITIES.filter((severity) => !seenSeverities.has(severity));
|
|
213
240
|
}
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
241
|
+
const FINDINGS_SUMMARY_PATTERN = /^Findings by severity:\s*(\d+)\s+critical,\s*(\d+)\s+high,\s*(\d+)\s+medium,\s*(\d+)\s+low\s*$/i;
|
|
242
|
+
/**
|
|
243
|
+
* Read the canonical count line. Only lines outside fenced code blocks count,
|
|
244
|
+
* so a quoted example cannot be mistaken for the artifact's own counts, and two
|
|
245
|
+
* disagreeing count lines are rejected rather than silently resolved in favor
|
|
246
|
+
* of whichever appears first.
|
|
247
|
+
*/
|
|
248
|
+
function parseFindingsSummaryCounts(content, artifactPath) {
|
|
249
|
+
const found = [];
|
|
250
|
+
for (const line of linesOutsideFences(content)) {
|
|
251
|
+
const match = FINDINGS_SUMMARY_PATTERN.exec(line.text.trim());
|
|
252
|
+
if (!match) {
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
found.push({
|
|
256
|
+
critical: Number.parseInt(match[1] ?? '0', 10),
|
|
257
|
+
high: Number.parseInt(match[2] ?? '0', 10),
|
|
258
|
+
medium: Number.parseInt(match[3] ?? '0', 10),
|
|
259
|
+
low: Number.parseInt(match[4] ?? '0', 10),
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
if (found.length === 0) {
|
|
217
263
|
return null;
|
|
218
264
|
}
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
};
|
|
265
|
+
const distinct = new Map(found.map((counts) => [JSON.stringify(counts), counts]));
|
|
266
|
+
if (distinct.size > 1) {
|
|
267
|
+
throw new Error(`Review artifact at ${artifactPath} contains ${found.length} conflicting "Findings by severity" count lines; keep exactly one so the gate can evaluate the review.`);
|
|
268
|
+
}
|
|
269
|
+
return found[0];
|
|
225
270
|
}
|
|
226
|
-
|
|
271
|
+
/**
|
|
272
|
+
* Tier spellings retired by the severity rename. They are no longer read, and
|
|
273
|
+
* detecting them fails the artifact with a migration error rather than letting
|
|
274
|
+
* it parse. A retired heading that parsed alongside canonical ones would have
|
|
275
|
+
* its findings attributed to whichever canonical section precedes it, which can
|
|
276
|
+
* move a finding below the blocking threshold.
|
|
277
|
+
*/
|
|
278
|
+
function describeLegacySeverityUsage(content, frontmatter) {
|
|
279
|
+
const found = new Set();
|
|
280
|
+
const findingsSection = findFindingsSection(content);
|
|
281
|
+
for (const line of linesOutsideFences(findingsSection?.content ?? '')) {
|
|
282
|
+
if (/^#{1,6}\s+Important\s*#*\s*$/i.test(line.text)) {
|
|
283
|
+
found.add('a `### Important` heading');
|
|
284
|
+
}
|
|
285
|
+
if (/^#{1,6}\s+Minor\s*#*\s*$/i.test(line.text)) {
|
|
286
|
+
found.add('a `### Minor` heading');
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
for (const line of linesOutsideFences(content)) {
|
|
290
|
+
if (/^Findings:\s*\d+\s+critical,/i.test(line.text.trim())) {
|
|
291
|
+
found.add('a legacy `Findings:` count line');
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
const nested = typeof frontmatter['oat_review_counts'] === 'object' &&
|
|
295
|
+
frontmatter['oat_review_counts'] !== null &&
|
|
296
|
+
!Array.isArray(frontmatter['oat_review_counts'])
|
|
297
|
+
? frontmatter['oat_review_counts']
|
|
298
|
+
: {};
|
|
299
|
+
for (const key of [...Object.keys(frontmatter), ...Object.keys(nested)]) {
|
|
300
|
+
if (/^(oat_review_)?(important|minor)(_count)?$/.test(key)) {
|
|
301
|
+
found.add(`the \`${key}\` count key`);
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
return [...found];
|
|
305
|
+
}
|
|
306
|
+
function legacySeverityError(artifactPath, legacyUsage) {
|
|
307
|
+
if (legacyUsage.length === 0) {
|
|
308
|
+
return null;
|
|
309
|
+
}
|
|
310
|
+
return new Error(`Review artifact at ${artifactPath} uses retired severity tiers (${legacyUsage.join(', ')}). The tiers are now Critical, High, Medium, and Low: rename \`### Important\` to \`### High\` and \`### Minor\` to \`### Low\`, and \`oat_review_important_count\`/\`oat_review_minor_count\` to \`oat_review_high_count\`/\`oat_review_low_count\`, or re-run the review. If the reviewer that wrote this artifact still emits the retired tiers, its installed instructions are stale: run \`oat tools update\` to refresh the installed tools and their provider projections.`);
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Per-tier counts for every severity heading the body actually has.
|
|
314
|
+
*
|
|
315
|
+
* Deliberately tolerant: it counts what is present without requiring the full
|
|
316
|
+
* canonical heading set, so it can cross-check explicit counts without changing
|
|
317
|
+
* how a body-only artifact is validated.
|
|
318
|
+
*/
|
|
319
|
+
function tallySeveritySections(content) {
|
|
227
320
|
const findingsSection = findFindingsSection(content);
|
|
228
321
|
const severityHeadings = findSeverityHeadings(content);
|
|
229
322
|
if (severityHeadings.length === 0) {
|
|
230
323
|
return null;
|
|
231
324
|
}
|
|
232
|
-
const missingSeverities = missingSeverityHeadings(severityHeadings);
|
|
233
|
-
if (missingSeverities.length > 0) {
|
|
234
|
-
throw new Error(`Review artifact at ${artifactPath} has an incomplete Findings section; expected headings for Critical, Important, Medium, and Minor. Missing: ${missingSeverities.join(', ')}.`);
|
|
235
|
-
}
|
|
236
325
|
const counts = {
|
|
237
326
|
critical: 0,
|
|
238
|
-
|
|
327
|
+
high: 0,
|
|
239
328
|
medium: 0,
|
|
240
|
-
|
|
329
|
+
low: 0,
|
|
241
330
|
};
|
|
242
331
|
for (const [offset, heading] of severityHeadings.entries()) {
|
|
243
332
|
const nextHeading = severityHeadings[offset + 1];
|
|
244
333
|
const sectionStart = heading.index + heading.headingLength;
|
|
245
334
|
const sectionEnd = nextHeading?.index ?? findingsSection?.end ?? content.length;
|
|
246
|
-
|
|
335
|
+
// Accumulate rather than assign: a repeated heading for one tier must not
|
|
336
|
+
// erase findings already counted under an earlier occurrence.
|
|
337
|
+
counts[heading.severity] += countFindingsInSection(content.slice(sectionStart, sectionEnd));
|
|
338
|
+
}
|
|
339
|
+
return counts;
|
|
340
|
+
}
|
|
341
|
+
function parseFindingsSectionCounts(content, artifactPath) {
|
|
342
|
+
const counts = tallySeveritySections(content);
|
|
343
|
+
if (counts === null) {
|
|
344
|
+
return null;
|
|
345
|
+
}
|
|
346
|
+
const missingSeverities = missingSeverityHeadings(findSeverityHeadings(content));
|
|
347
|
+
if (missingSeverities.length > 0) {
|
|
348
|
+
throw new Error(`Review artifact at ${artifactPath} has an incomplete Findings section; expected headings for ${SEVERITIES.map(severityDisplayName).join(', ')}. Missing: ${missingSeverities.join(', ')}.`);
|
|
247
349
|
}
|
|
248
350
|
return counts;
|
|
249
351
|
}
|
|
@@ -263,7 +365,7 @@ function parseFrontmatterObject(frontmatter, artifactPath) {
|
|
|
263
365
|
}
|
|
264
366
|
}
|
|
265
367
|
function hasBlockingFindings(counts) {
|
|
266
|
-
return counts.critical > 0 || counts.
|
|
368
|
+
return counts.critical > 0 || counts.high > 0;
|
|
267
369
|
}
|
|
268
370
|
function insertionTextForSeverity(severity) {
|
|
269
371
|
return `\n### ${severityDisplayName(severity)}\n\nNone\n`;
|
|
@@ -307,16 +409,72 @@ async function normalizeMissingEmptySeveritySections(artifactPath, content, coun
|
|
|
307
409
|
persisted: insertedSeverities.length > 0 && persist,
|
|
308
410
|
};
|
|
309
411
|
}
|
|
310
|
-
function
|
|
311
|
-
|
|
412
|
+
function formatCounts(counts) {
|
|
413
|
+
return SEVERITIES.map((severity) => `${counts[severity]} ${severity}`).join(', ');
|
|
414
|
+
}
|
|
415
|
+
/**
|
|
416
|
+
* Fail closed when two count sources disagree.
|
|
417
|
+
*
|
|
418
|
+
* No single source may win by precedence: a stale all-zero frontmatter block or
|
|
419
|
+
* count line would otherwise mask real findings written in the body, and the
|
|
420
|
+
* gate would pass a review that clearly reports a blocking finding.
|
|
421
|
+
*/
|
|
422
|
+
function assertAgreeingCounts(artifactPath, left, right, tiers = SEVERITIES) {
|
|
423
|
+
const differing = tiers.filter((severity) => left.counts[severity] !== right.counts[severity]);
|
|
424
|
+
if (differing.length === 0) {
|
|
425
|
+
return;
|
|
426
|
+
}
|
|
427
|
+
throw new Error(`Review artifact at ${artifactPath} contradicts itself about finding counts (${differing.join(', ')}): declared counts are ${formatCounts(left.counts)} (${left.name}) but ${formatCounts(right.counts)} (${right.name}). Reconcile them before the gate can evaluate the review.`);
|
|
428
|
+
}
|
|
429
|
+
/**
|
|
430
|
+
* Resolve the artifact's counts, cross-checking every source that is present.
|
|
431
|
+
*
|
|
432
|
+
* Returns `null` when only the body carries counts, so the caller keeps the
|
|
433
|
+
* completeness-checked body path for body-only artifacts.
|
|
434
|
+
*/
|
|
435
|
+
function resolveCounts(content, frontmatter, artifactPath) {
|
|
436
|
+
const explicitSources = [];
|
|
437
|
+
const frontmatterCounts = readFrontmatterCounts(frontmatter, artifactPath);
|
|
312
438
|
if (frontmatterCounts) {
|
|
313
|
-
|
|
439
|
+
explicitSources.push({
|
|
440
|
+
name: 'the frontmatter count fields',
|
|
441
|
+
counts: frontmatterCounts.counts,
|
|
442
|
+
tiers: frontmatterCounts.tiers,
|
|
443
|
+
});
|
|
314
444
|
}
|
|
315
|
-
const summaryCounts = parseFindingsSummaryCounts(content);
|
|
445
|
+
const summaryCounts = parseFindingsSummaryCounts(content, artifactPath);
|
|
316
446
|
if (summaryCounts) {
|
|
317
|
-
|
|
447
|
+
explicitSources.push({
|
|
448
|
+
name: 'the "Findings by severity" count line',
|
|
449
|
+
counts: summaryCounts,
|
|
450
|
+
tiers: SEVERITIES,
|
|
451
|
+
});
|
|
318
452
|
}
|
|
319
|
-
|
|
453
|
+
const [primarySource, ...otherSources] = explicitSources;
|
|
454
|
+
for (const source of otherSources) {
|
|
455
|
+
const comparableTiers = primarySource.tiers.filter((severity) => source.tiers.includes(severity));
|
|
456
|
+
assertAgreeingCounts(artifactPath, primarySource, source, comparableTiers);
|
|
457
|
+
}
|
|
458
|
+
const bodyCounts = tallySeveritySections(content);
|
|
459
|
+
if (bodyCounts) {
|
|
460
|
+
// Only tiers the body actually has a heading for are comparable. A missing
|
|
461
|
+
// heading is not a claim of zero findings, it is a malformed body, and the
|
|
462
|
+
// completeness and normalization checks own that diagnosis.
|
|
463
|
+
const bodyTiers = [
|
|
464
|
+
...new Set(findSeverityHeadings(content).map((heading) => heading.severity)),
|
|
465
|
+
];
|
|
466
|
+
const bodySource = {
|
|
467
|
+
name: 'the Findings sections',
|
|
468
|
+
counts: bodyCounts,
|
|
469
|
+
tiers: bodyTiers,
|
|
470
|
+
};
|
|
471
|
+
for (const source of explicitSources) {
|
|
472
|
+
const comparableTiers = source.tiers.filter((severity) => bodyTiers.includes(severity));
|
|
473
|
+
assertAgreeingCounts(artifactPath, source, bodySource, comparableTiers);
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
return (explicitSources.find((source) => source.tiers.length === SEVERITIES.length)
|
|
477
|
+
?.counts ?? null);
|
|
320
478
|
}
|
|
321
479
|
export async function parseReviewGateVerdict(artifactPath, options = {}) {
|
|
322
480
|
let content;
|
|
@@ -340,7 +498,15 @@ export async function parseReviewGateVerdict(artifactPath, options = {}) {
|
|
|
340
498
|
const frontmatter = frontmatterBlock
|
|
341
499
|
? parseFrontmatterObject(frontmatterBlock, artifactPath)
|
|
342
500
|
: {};
|
|
343
|
-
|
|
501
|
+
// Retired tiers fail the artifact outright rather than being ignored, and do
|
|
502
|
+
// so before normalization can rewrite the file. Ignoring them is not safe: a
|
|
503
|
+
// retired heading that parses alongside canonical ones has its findings
|
|
504
|
+
// attributed to whichever canonical section precedes it.
|
|
505
|
+
const retiredTierError = legacySeverityError(artifactPath, describeLegacySeverityUsage(content, frontmatter));
|
|
506
|
+
if (retiredTierError) {
|
|
507
|
+
throw retiredTierError;
|
|
508
|
+
}
|
|
509
|
+
let counts = resolveCounts(content, frontmatter, artifactPath);
|
|
344
510
|
let insertedSeverities = [];
|
|
345
511
|
let normalizationPersisted = false;
|
|
346
512
|
if (counts && options.normalizeMissingEmptySeveritySections) {
|