@dzhechkov/harness-core 0.8.40 → 0.8.41
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/.dz-manifest.json +194 -114
- package/README.md +43 -8
- package/dist/__tests__/golden-baseline.test.js +1 -1
- package/dist/__tests__/golden-baseline.test.js.map +1 -1
- package/dist/agentdb-index.js +3 -3
- package/dist/agentdb-index.js.map +1 -1
- package/dist/agentdb-reindex-marker.d.ts +2 -2
- package/dist/agentdb-reindex-marker.js +4 -4
- package/dist/agentdb-reindex-marker.js.map +1 -1
- package/dist/agentdb-snapshot-rotation.d.ts +1 -1
- package/dist/agentdb-snapshot-rotation.js +1 -1
- package/dist/backup-freshness.d.ts +32 -0
- package/dist/backup-freshness.d.ts.map +1 -0
- package/dist/backup-freshness.js +69 -0
- package/dist/backup-freshness.js.map +1 -0
- package/dist/cmd-usage.d.ts +2 -2
- package/dist/cmd-usage.d.ts.map +1 -1
- package/dist/cmd-usage.js +8 -6
- package/dist/cmd-usage.js.map +1 -1
- package/dist/codex-hooks-assets.d.ts +2 -1
- package/dist/codex-hooks-assets.d.ts.map +1 -1
- package/dist/codex-hooks-assets.js +15 -2
- package/dist/codex-hooks-assets.js.map +1 -1
- package/dist/compounding.d.ts +43 -1
- package/dist/compounding.d.ts.map +1 -1
- package/dist/compounding.js +96 -0
- package/dist/compounding.js.map +1 -1
- package/dist/discrimination-gate.d.ts +0 -3
- package/dist/discrimination-gate.d.ts.map +1 -1
- package/dist/discrimination-gate.js +2 -2
- package/dist/discrimination-gate.js.map +1 -1
- package/dist/guard.d.ts +22 -2
- package/dist/guard.d.ts.map +1 -1
- package/dist/guard.js +102 -14
- package/dist/guard.js.map +1 -1
- package/dist/index.d.ts +15 -6
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -4
- package/dist/index.js.map +1 -1
- package/dist/integration-apply.js +2 -2
- package/dist/integration-apply.js.map +1 -1
- package/dist/lesson-payoff.js +3 -3
- package/dist/lesson-payoff.js.map +1 -1
- package/dist/named-lock.d.ts +5 -2
- package/dist/named-lock.d.ts.map +1 -1
- package/dist/named-lock.js +30 -10
- package/dist/named-lock.js.map +1 -1
- package/dist/operations.d.ts.map +1 -1
- package/dist/operations.js +24 -0
- package/dist/operations.js.map +1 -1
- package/dist/pack-inventory.d.ts +51 -0
- package/dist/pack-inventory.d.ts.map +1 -0
- package/dist/pack-inventory.js +306 -0
- package/dist/pack-inventory.js.map +1 -0
- package/dist/package-skill-layouts.d.ts +3 -2
- package/dist/package-skill-layouts.d.ts.map +1 -1
- package/dist/package-skill-layouts.js +3 -2
- package/dist/package-skill-layouts.js.map +1 -1
- package/dist/patterns.d.ts +8 -0
- package/dist/patterns.d.ts.map +1 -1
- package/dist/patterns.js +11 -1
- package/dist/patterns.js.map +1 -1
- package/dist/publish.d.ts +29 -7
- package/dist/publish.d.ts.map +1 -1
- package/dist/publish.js +92 -15
- package/dist/publish.js.map +1 -1
- package/dist/registry.d.ts.map +1 -1
- package/dist/registry.js +24 -20
- package/dist/registry.js.map +1 -1
- package/dist/reqe-verdict.d.ts +55 -0
- package/dist/reqe-verdict.d.ts.map +1 -0
- package/dist/reqe-verdict.js +173 -0
- package/dist/reqe-verdict.js.map +1 -0
- package/dist/reqe.d.ts +35 -14
- package/dist/reqe.d.ts.map +1 -1
- package/dist/reqe.js +85 -32
- package/dist/reqe.js.map +1 -1
- package/dist/round.d.ts +10 -0
- package/dist/round.d.ts.map +1 -1
- package/dist/round.js +7 -0
- package/dist/round.js.map +1 -1
- package/dist/session-retro.d.ts +1 -1
- package/dist/session-retro.js +3 -3
- package/dist/session-retro.js.map +1 -1
- package/dist/skill-drift.d.ts +18 -1
- package/dist/skill-drift.d.ts.map +1 -1
- package/dist/skill-drift.js +46 -13
- package/dist/skill-drift.js.map +1 -1
- package/dist/statusline.js +2 -2
- package/dist/statusline.js.map +1 -1
- package/dist/store-guard.js +3 -3
- package/dist/store-guard.js.map +1 -1
- package/dist/store-lock.d.ts +9 -0
- package/dist/store-lock.d.ts.map +1 -1
- package/dist/store-lock.js +22 -3
- package/dist/store-lock.js.map +1 -1
- package/dist/test-receipt.d.ts +66 -0
- package/dist/test-receipt.d.ts.map +1 -0
- package/dist/test-receipt.js +73 -0
- package/dist/test-receipt.js.map +1 -0
- package/dist/workflow-run.d.ts.map +1 -1
- package/dist/workflow-run.js +7 -8
- package/dist/workflow-run.js.map +1 -1
- package/package.json +1 -1
- package/sbom.json +313 -113
- package/src/__tests__/golden-baseline.test.ts +1 -1
- package/src/agentdb-index.ts +3 -3
- package/src/agentdb-reindex-marker.ts +4 -4
- package/src/agentdb-snapshot-rotation.ts +1 -1
- package/src/backup-freshness.ts +96 -0
- package/src/cmd-usage.ts +7 -6
- package/src/codex-hooks-assets.ts +15 -2
- package/src/compounding.ts +132 -1
- package/src/discrimination-gate.ts +2 -5
- package/src/guard.ts +104 -17
- package/src/index.ts +23 -5
- package/src/integration-apply.ts +2 -2
- package/src/lesson-payoff.ts +3 -3
- package/src/named-lock.ts +36 -11
- package/src/operations.ts +24 -0
- package/src/pack-inventory.ts +313 -0
- package/src/package-skill-layouts.ts +3 -2
- package/src/patterns.ts +18 -1
- package/src/publish.ts +101 -22
- package/src/registry.ts +23 -17
- package/src/reqe-verdict.ts +174 -0
- package/src/reqe.ts +111 -35
- package/src/round.ts +18 -0
- package/src/session-retro.ts +3 -3
- package/src/skill-drift.ts +66 -12
- package/src/statusline.ts +2 -2
- package/src/store-guard.ts +3 -3
- package/src/store-lock.ts +20 -3
- package/src/test-receipt.ts +106 -0
- package/src/workflow-run.ts +7 -8
|
@@ -0,0 +1,174 @@
|
|
|
1
|
+
export type ReqeSeverity = 'BLOCKER' | 'HIGH' | 'MEDIUM' | 'LOW' | 'unknown';
|
|
2
|
+
export interface ReqeSeverityCounts {
|
|
3
|
+
readonly blocker: number; readonly high: number; readonly medium: number;
|
|
4
|
+
readonly low: number; readonly unknown: number;
|
|
5
|
+
}
|
|
6
|
+
/** `where` is null when the location cell is an em dash — the title alone is echoed (ADR AM-5). */
|
|
7
|
+
export interface ReqeBlockingRow {
|
|
8
|
+
readonly severity: 'BLOCKER' | 'HIGH'; readonly title: string; readonly where: string | null;
|
|
9
|
+
}
|
|
10
|
+
/** THREE states, never two (03.5 C5b): `absent` = no Findings section; `rejected` = section present,
|
|
11
|
+
* table not canonical, reason NAMED; `accepted` = parsed. */
|
|
12
|
+
export type ReqeFindingsClass =
|
|
13
|
+
| { readonly kind: 'accepted'; readonly counts: ReqeSeverityCounts; readonly rows: number;
|
|
14
|
+
readonly declared: number | null; readonly blocking: readonly ReqeBlockingRow[] }
|
|
15
|
+
| { readonly kind: 'rejected'; readonly reason: string }
|
|
16
|
+
| { readonly kind: 'absent' };
|
|
17
|
+
export type ReqePriorVerdict =
|
|
18
|
+
| { readonly kind: 'closed'; readonly closed: number; readonly total: number }
|
|
19
|
+
| { readonly kind: 'open'; readonly open: readonly number[]; readonly total: number }
|
|
20
|
+
| { readonly kind: 'unassessed' };
|
|
21
|
+
export interface ReqeVerdict {
|
|
22
|
+
readonly new: 'ready' | 'blocked' | 'unassessed';
|
|
23
|
+
readonly prior: ReqePriorVerdict;
|
|
24
|
+
readonly findings: ReqeFindingsClass;
|
|
25
|
+
readonly counts: ReqeSeverityCounts; // all zeros unless findings.kind === 'accepted'
|
|
26
|
+
readonly rows: number; // 0 unless accepted
|
|
27
|
+
readonly blocking: readonly ReqeBlockingRow[]; // [] unless accepted
|
|
28
|
+
readonly source: 'reviewer-declared'; // FR-6: the declarer is named in the data too
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
const emptyCounts = (): ReqeSeverityCounts => ({ blocker: 0, high: 0, medium: 0, low: 0, unknown: 0 });
|
|
32
|
+
const heading = /^#{1,6}[ \t]+/;
|
|
33
|
+
const findingsHeading = /^#{1,6}[ \t]*Findings(?:[ \t]+\((\d+)\))?[ \t]*$/;
|
|
34
|
+
const expectedTable = 'expected severity plus finding/title and location/where columns';
|
|
35
|
+
|
|
36
|
+
/** qe-findings.ts does not export maskMarkdown. This local fallback blanks fences and
|
|
37
|
+
* HTML comments without moving source lines, as permitted by the TASK-1 contract. */
|
|
38
|
+
function maskMarkdown(text: string): string {
|
|
39
|
+
let fence: string | null = null;
|
|
40
|
+
let inComment = false;
|
|
41
|
+
return text.split('\n').map((line) => {
|
|
42
|
+
if (fence !== null) {
|
|
43
|
+
const close = /^ {0,3}(`{3,}|~{3,})[ \t]*\r?$/.exec(line)?.[1];
|
|
44
|
+
if (close && close[0] === fence[0] && close.length >= fence.length) fence = null;
|
|
45
|
+
return ' '.repeat(line.length);
|
|
46
|
+
}
|
|
47
|
+
let visible = '';
|
|
48
|
+
for (let i = 0; i < line.length;) {
|
|
49
|
+
if (inComment) {
|
|
50
|
+
const end = line.indexOf('-->', i);
|
|
51
|
+
if (end === -1) { visible += ' '.repeat(line.length - i); break; }
|
|
52
|
+
visible += ' '.repeat(end + 3 - i);
|
|
53
|
+
i = end + 3;
|
|
54
|
+
inComment = false;
|
|
55
|
+
} else {
|
|
56
|
+
const start = line.indexOf('<!--', i);
|
|
57
|
+
if (start === -1) { visible += line.slice(i); break; }
|
|
58
|
+
visible += line.slice(i, start) + ' ';
|
|
59
|
+
i = start + 4;
|
|
60
|
+
inComment = true;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
const open = /^ {0,3}(`{3,}|~{3,})/.exec(visible)?.[1];
|
|
64
|
+
if (open) { fence = open; return ' '.repeat(line.length); }
|
|
65
|
+
return visible;
|
|
66
|
+
}).join('\n');
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function cells(line: string): string[] {
|
|
70
|
+
const parts = line.trim().split(/(?<!\\)\|/);
|
|
71
|
+
if (parts[0] === '') parts.shift();
|
|
72
|
+
if (parts[parts.length - 1] === '') parts.pop();
|
|
73
|
+
return parts.map((cell) => cell.replace(/\\\|/g, '|').trim());
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function severityOf(cell: string): ReqeSeverity {
|
|
77
|
+
switch (cell.trim().toLowerCase()) {
|
|
78
|
+
case 'blocker': case 'critical': return 'BLOCKER';
|
|
79
|
+
case 'high': case 'major': return 'HIGH';
|
|
80
|
+
case 'medium': case 'med': return 'MEDIUM';
|
|
81
|
+
case 'low': case 'minor': return 'LOW';
|
|
82
|
+
default: return 'unknown';
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Read only the single Findings section; malformed attempts carry an explicit reason. */
|
|
87
|
+
export function classifyReqeFindings(text: string): ReqeFindingsClass {
|
|
88
|
+
const lines = maskMarkdown(text).split(/\r?\n/);
|
|
89
|
+
const sections = lines.flatMap((line, index) => {
|
|
90
|
+
const match = findingsHeading.exec(line);
|
|
91
|
+
return match ? [{ index, declared: match[1] === undefined ? null : Number(match[1]) }] : [];
|
|
92
|
+
});
|
|
93
|
+
if (sections.length === 0) return { kind: 'absent' };
|
|
94
|
+
if (sections.length !== 1) return { kind: 'rejected', reason: `ambiguous: ${sections.length} Findings headings` };
|
|
95
|
+
const section = sections[0]!;
|
|
96
|
+
let end = section.index + 1;
|
|
97
|
+
while (end < lines.length && !heading.test(lines[end]!)) end++;
|
|
98
|
+
let start = section.index + 1;
|
|
99
|
+
while (start < end && !/(?<!\\)\|/.test(lines[start]!)) start++;
|
|
100
|
+
if (start === end) {
|
|
101
|
+
if (section.declared === 0) {
|
|
102
|
+
return { kind: 'accepted', counts: emptyCounts(), rows: 0, declared: 0, blocking: [] };
|
|
103
|
+
}
|
|
104
|
+
return { kind: 'rejected', reason: `no canonical table; ${expectedTable}` };
|
|
105
|
+
}
|
|
106
|
+
const headers = cells(lines[start]!).map((cell) => cell.toLowerCase());
|
|
107
|
+
const severityIndex = headers.indexOf('severity');
|
|
108
|
+
if (severityIndex === -1) return { kind: 'rejected', reason: `no severity column; ${expectedTable}; parsed columns: ${headers.join(', ')}` };
|
|
109
|
+
if (headers.lastIndexOf('severity') !== severityIndex) return { kind: 'rejected', reason: 'ambiguous: multiple severity columns' };
|
|
110
|
+
const titleIndex = headers.findIndex((cell) => cell === 'finding' || cell === 'title');
|
|
111
|
+
const whereIndex = headers.findIndex((cell) => cell === 'location' || cell === 'where');
|
|
112
|
+
if (titleIndex === -1 || whereIndex === -1) return { kind: 'rejected', reason: `missing finding/title or location/where column; ${expectedTable}` };
|
|
113
|
+
const delimiter = start + 1 < end ? cells(lines[start + 1]!) : [];
|
|
114
|
+
if (delimiter.length !== headers.length || !delimiter.every((cell) => /^:?-{3,}:?$/.test(cell))) {
|
|
115
|
+
return { kind: 'rejected', reason: `row ${start + 2}: invalid delimiter; expected ${headers.length} delimiter cells for columns: ${headers.join(', ')}` };
|
|
116
|
+
}
|
|
117
|
+
const counts = { ...emptyCounts() };
|
|
118
|
+
const blocking: ReqeBlockingRow[] = [];
|
|
119
|
+
let rows = 0;
|
|
120
|
+
let finished = false;
|
|
121
|
+
for (let index = start + 2; index < end; index++) {
|
|
122
|
+
const line = lines[index]!;
|
|
123
|
+
if (!/(?<!\\)\|/.test(line)) { finished = true; continue; }
|
|
124
|
+
if (finished) return { kind: 'rejected', reason: `row ${index + 1}: ambiguous table continuation` };
|
|
125
|
+
const row = cells(line);
|
|
126
|
+
if (row.length !== headers.length) {
|
|
127
|
+
return { kind: 'rejected', reason: `row ${index + 1}: ${row.length} cells for ${headers.length} columns (unescaped pipe?); parsed columns: ${headers.join(', ')}` };
|
|
128
|
+
}
|
|
129
|
+
if (row.every((cell) => /^:?-{3,}:?$/.test(cell))) return { kind: 'rejected', reason: `row ${index + 1}: ambiguous additional table delimiter` };
|
|
130
|
+
const severity = severityOf(row[severityIndex]!);
|
|
131
|
+
const key = severity.toLowerCase() as keyof ReqeSeverityCounts;
|
|
132
|
+
counts[key]++;
|
|
133
|
+
rows++;
|
|
134
|
+
if (severity === 'BLOCKER' || severity === 'HIGH') {
|
|
135
|
+
const where = row[whereIndex]!;
|
|
136
|
+
blocking.push({ severity, title: row[titleIndex]!, where: where === '—' || where === '' ? null : where });
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
return { kind: 'accepted', counts, rows, declared: section.declared, blocking };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Independent pass: no classification or new-findings state participates in prior status. */
|
|
143
|
+
export function parsePriorFindings(text: string): ReqePriorVerdict {
|
|
144
|
+
const lines = maskMarkdown(text).split(/\r?\n/);
|
|
145
|
+
const sections = lines.flatMap((line, index) => /^#{1,6}[ \t]*Prior findings[ \t]*$/.test(line) ? [index] : []);
|
|
146
|
+
if (sections.length !== 1) return { kind: 'unassessed' };
|
|
147
|
+
let total = 0;
|
|
148
|
+
const open: number[] = [];
|
|
149
|
+
for (let index = sections[0]! + 1; index < lines.length; index++) {
|
|
150
|
+
const line = lines[index]!;
|
|
151
|
+
if (heading.test(line)) break;
|
|
152
|
+
const match = /^[ \t]*(\d+):[ \t]+(closed|open)[ \t]+—[ \t]+\S.*$/.exec(line);
|
|
153
|
+
if (!match) continue;
|
|
154
|
+
total++;
|
|
155
|
+
if (match[2] === 'open') open.push(Number(match[1]));
|
|
156
|
+
}
|
|
157
|
+
if (total === 0) return { kind: 'unassessed' };
|
|
158
|
+
return open.length > 0 ? { kind: 'open', open, total } : { kind: 'closed', closed: total, total };
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** The only blocking rule; prior status is reported independently. */
|
|
162
|
+
export function buildReqeVerdict(text: string): ReqeVerdict {
|
|
163
|
+
const findings = classifyReqeFindings(text);
|
|
164
|
+
const prior = parsePriorFindings(text);
|
|
165
|
+
const accepted = findings.kind === 'accepted';
|
|
166
|
+
const counts = accepted ? findings.counts : emptyCounts();
|
|
167
|
+
return {
|
|
168
|
+
new: accepted ? (counts.blocker >= 1 || counts.high >= 1 ? 'blocked' : 'ready') : 'unassessed',
|
|
169
|
+
prior, findings, counts,
|
|
170
|
+
rows: accepted ? findings.rows : 0,
|
|
171
|
+
blocking: accepted ? findings.blocking : [],
|
|
172
|
+
source: 'reviewer-declared',
|
|
173
|
+
};
|
|
174
|
+
}
|
package/src/reqe.ts
CHANGED
|
@@ -9,8 +9,8 @@
|
|
|
9
9
|
* DEBT with a lifecycle:
|
|
10
10
|
*
|
|
11
11
|
* emit — the workflow records features/<slug>/.fa-state/reqe-due.json when Step-8 actually ran
|
|
12
|
-
* same-family
|
|
13
|
-
*
|
|
12
|
+
* same-family after a usage switch, failed probe, fallback or explicit QE pin;
|
|
13
|
+
* a review that stayed cross-family creates no debt;
|
|
14
14
|
* list — `dz reqe` scans the debts; `dz usage` surfaces the count so the moment limits free up
|
|
15
15
|
* is the moment the debt is visible;
|
|
16
16
|
* brief — `dz reqe --slug <s>` prints a ready cross-family review brief (the OTHER family than
|
|
@@ -18,19 +18,37 @@
|
|
|
18
18
|
* settle — `dz reqe --slug <s> --done --report <file>` clears the debt FAIL-CLOSED: only against
|
|
19
19
|
* an existing, non-trivial report that names a grade; the settlement is appended to
|
|
20
20
|
* 08_qe_report.md so the artifact trail closes.
|
|
21
|
+
* The two-part verdict reports new and prior findings separately; blocked new findings signal CLI exit 3.
|
|
21
22
|
*
|
|
22
23
|
* HONEST SCOPE: nothing here re-runs QE automatically (no background spend — the human decides);
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
* feature carry no marker and are UNDETERMINABLE, not debt-free.
|
|
24
|
+
* routing-off runs are excluded by configuration. Old checkpoints without bridge facts or a usage
|
|
25
|
+
* marker are UNDETERMINABLE and explicitly degraded.
|
|
26
26
|
*/
|
|
27
27
|
|
|
28
|
+
import { buildReqeVerdict, type ReqeVerdict } from './reqe-verdict.js';
|
|
29
|
+
|
|
28
30
|
export const REQE_SCHEMA = 'reqe-due-1';
|
|
29
31
|
|
|
30
32
|
export const REQE_SCOPE =
|
|
31
|
-
'scope:
|
|
32
|
-
'
|
|
33
|
-
'automatically.';
|
|
33
|
+
'scope: same-family Step-8 QE records cause=usage-switched, probe-failed, same-family-fallback ' +
|
|
34
|
+
'or same-family-pinned; routing OFF is excluded by configuration; settling requires a graded ' +
|
|
35
|
+
'cross-family report (fail-closed); nothing re-runs QE automatically.';
|
|
36
|
+
|
|
37
|
+
export type ReqeCause = 'usage-switched' | 'probe-failed' | 'same-family-fallback' | 'same-family-pinned';
|
|
38
|
+
export type ReqeRungState = 'pending' | 'dispatched' | 'probe-failed' | 'refused-before-dispatch';
|
|
39
|
+
export interface ReqeBridgeFacts {
|
|
40
|
+
happened: boolean | null;
|
|
41
|
+
rungState: ReqeRungState | null;
|
|
42
|
+
rungReason: string | null;
|
|
43
|
+
decline: string | null;
|
|
44
|
+
}
|
|
45
|
+
export interface ReqeEmitInput {
|
|
46
|
+
coderUsed: string | null | undefined;
|
|
47
|
+
qeReviewerUsed: string | null | undefined;
|
|
48
|
+
qeModelLabel: string | null | undefined;
|
|
49
|
+
routingRequested: boolean | null | undefined;
|
|
50
|
+
bridge?: ReqeBridgeFacts | null;
|
|
51
|
+
}
|
|
34
52
|
|
|
35
53
|
export type ModelFamily = 'claude' | 'openai';
|
|
36
54
|
|
|
@@ -46,35 +64,57 @@ export function modelFamily(spec: string | null | undefined): ModelFamily {
|
|
|
46
64
|
export interface ReqeEmitDecision {
|
|
47
65
|
emit: boolean;
|
|
48
66
|
reason: string;
|
|
67
|
+
cause: ReqeCause | null;
|
|
68
|
+
degraded: boolean;
|
|
49
69
|
}
|
|
50
70
|
|
|
51
|
-
/**
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
|
|
55
|
-
export function shouldEmitReqeDebt(input: {
|
|
56
|
-
coderUsed: string | null | undefined;
|
|
57
|
-
qeReviewerUsed: string | null | undefined;
|
|
58
|
-
qeModelLabel: string | null | undefined;
|
|
59
|
-
}): ReqeEmitDecision {
|
|
60
|
-
const switched = /\(usage-switched\)/.test(String(input.qeModelLabel ?? ''));
|
|
71
|
+
/** Families are authoritative; routing OFF is an explicit exclusion. For same-family reviews,
|
|
72
|
+
* classify usage overrides before probe failures, fallbacks and explicit pins. Missing checkpoint
|
|
73
|
+
* facts and unknown rung states stay visibly degraded rather than inventing a cause. */
|
|
74
|
+
export function shouldEmitReqeDebt(input: ReqeEmitInput): ReqeEmitDecision {
|
|
61
75
|
const coderFam = modelFamily(input.coderUsed);
|
|
62
76
|
const qeFam = modelFamily(input.qeReviewerUsed);
|
|
63
|
-
if (
|
|
77
|
+
if (coderFam !== qeFam) {
|
|
78
|
+
return { emit: false, cause: null, degraded: false, reason: 'cross-family QE ran' };
|
|
79
|
+
}
|
|
80
|
+
if (input.routingRequested === false) {
|
|
81
|
+
return { emit: false, cause: null, degraded: false,
|
|
82
|
+
reason: 'same-family by configuration (cross-family never requested)' };
|
|
83
|
+
}
|
|
84
|
+
if (/\(usage-switched\)/.test(String(input.qeModelLabel ?? ''))) {
|
|
64
85
|
return {
|
|
65
|
-
emit: true,
|
|
86
|
+
emit: true, cause: 'usage-switched', degraded: false,
|
|
66
87
|
reason:
|
|
67
88
|
'usage-switched self-review: Step-8 QE ran on the coder’s own family (' + coderFam +
|
|
68
89
|
') under the limit override — the cross-model guard was suspended (FR-2.9)',
|
|
69
90
|
};
|
|
70
91
|
}
|
|
71
|
-
if (
|
|
72
|
-
|
|
73
|
-
|
|
92
|
+
if (input.bridge?.rungState === 'probe-failed') {
|
|
93
|
+
return { emit: true, cause: 'probe-failed', degraded: false,
|
|
94
|
+
reason: 'codex probe found no usable id; review ran on the coder’s own family (' + coderFam + ')' };
|
|
95
|
+
}
|
|
96
|
+
if (input.bridge?.rungState === 'dispatched' || input.bridge?.rungState === 'refused-before-dispatch' ||
|
|
97
|
+
input.qeReviewerUsed === 'codex-fallback') {
|
|
98
|
+
return { emit: true, cause: 'same-family-fallback', degraded: false,
|
|
99
|
+
reason: 'same-family fallback: ' + (input.bridge?.decline ?? input.bridge?.rungReason ?? 'cross-family reviewer unavailable') +
|
|
100
|
+
'; review ran on the coder’s own family (' + coderFam + ')' };
|
|
101
|
+
}
|
|
102
|
+
if (input.bridge == null) {
|
|
103
|
+
return { emit: false, cause: null, degraded: true,
|
|
104
|
+
reason: "cause undeterminable (pre-change checkpoint) — re-run with resume:'never' to classify" };
|
|
105
|
+
}
|
|
106
|
+
if (input.routingRequested === true && input.bridge.rungState === 'pending') {
|
|
107
|
+
return { emit: true, cause: 'same-family-pinned', degraded: false,
|
|
108
|
+
reason: 'same-family by explicit qe pin (args.models.qe) while cross-family routing was requested' };
|
|
109
|
+
}
|
|
110
|
+
return { emit: false, cause: null, degraded: true,
|
|
111
|
+
reason: 'cause undeterminable (unknown rung state or routing request)' };
|
|
74
112
|
}
|
|
75
113
|
|
|
76
114
|
export interface ReqeDebt {
|
|
77
115
|
schema: typeof REQE_SCHEMA;
|
|
116
|
+
cause: ReqeCause;
|
|
117
|
+
bridge?: ReqeBridgeFacts | null;
|
|
78
118
|
slug: string;
|
|
79
119
|
coderFamily: ModelFamily;
|
|
80
120
|
qeFamily: ModelFamily;
|
|
@@ -97,15 +137,21 @@ export function buildReqeDebt(input: {
|
|
|
97
137
|
qeGrade: string | null | undefined;
|
|
98
138
|
reason: string;
|
|
99
139
|
emittedAt?: string | null;
|
|
140
|
+
cause: ReqeCause;
|
|
141
|
+
bridge?: ReqeBridgeFacts | null;
|
|
142
|
+
runStamp?: string | null;
|
|
100
143
|
}): ReqeDebt {
|
|
101
144
|
return {
|
|
102
145
|
schema: REQE_SCHEMA,
|
|
146
|
+
cause: input.cause,
|
|
147
|
+
...(input.bridge === undefined ? {} : { bridge: input.bridge }),
|
|
103
148
|
slug: input.slug,
|
|
104
149
|
coderFamily: modelFamily(input.coderUsed),
|
|
105
150
|
qeFamily: modelFamily(input.qeReviewerUsed),
|
|
106
151
|
qeGrade: input.qeGrade == null || String(input.qeGrade).trim() === '' ? null : String(input.qeGrade).trim(),
|
|
107
152
|
reason: input.reason,
|
|
108
153
|
emittedAt: input.emittedAt ?? null,
|
|
154
|
+
runStamp: input.runStamp ?? null,
|
|
109
155
|
};
|
|
110
156
|
}
|
|
111
157
|
|
|
@@ -125,16 +171,21 @@ export function parseReqeDebt(text: string): ReqeDebt | null {
|
|
|
125
171
|
if (d.coderFamily !== 'claude' && d.coderFamily !== 'openai') return null;
|
|
126
172
|
if (d.qeFamily !== 'claude' && d.qeFamily !== 'openai') return null;
|
|
127
173
|
if (typeof d.reason !== 'string' || d.reason.trim() === '') return null;
|
|
128
|
-
|
|
129
|
-
|
|
174
|
+
if (d.cause !== undefined && d.cause !== 'usage-switched' && d.cause !== 'probe-failed' &&
|
|
175
|
+
d.cause !== 'same-family-fallback' && d.cause !== 'same-family-pinned') return null;
|
|
176
|
+
return buildReqeDebt({
|
|
130
177
|
slug: d.slug,
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
qeGrade: typeof d.qeGrade === 'string'
|
|
178
|
+
coderUsed: d.coderFamily,
|
|
179
|
+
qeReviewerUsed: d.qeFamily,
|
|
180
|
+
qeGrade: typeof d.qeGrade === 'string' ? d.qeGrade : null,
|
|
134
181
|
reason: d.reason,
|
|
182
|
+
cause: d.cause ?? 'usage-switched',
|
|
183
|
+
...(d.bridge === undefined ? {} : {
|
|
184
|
+
bridge: d.bridge && typeof d.bridge === 'object' && !Array.isArray(d.bridge) ? d.bridge : null,
|
|
185
|
+
}),
|
|
135
186
|
emittedAt: typeof d.emittedAt === 'string' && d.emittedAt.trim() !== '' ? d.emittedAt : null,
|
|
136
187
|
runStamp: typeof d.runStamp === 'string' && d.runStamp.trim() !== '' ? d.runStamp : null,
|
|
137
|
-
};
|
|
188
|
+
});
|
|
138
189
|
}
|
|
139
190
|
|
|
140
191
|
export interface ReqeBrief {
|
|
@@ -155,11 +206,12 @@ export function buildReqeBrief(debt: ReqeDebt, artifactsDir: string): ReqeBrief
|
|
|
155
206
|
artifactsDir + '/03_adr/',
|
|
156
207
|
];
|
|
157
208
|
const instructions = [
|
|
158
|
-
'Independent re-QE for "' + debt.slug + '": the recorded Step-8 review ran on the coder’s own family (' + debt.coderFamily + ')
|
|
209
|
+
'Independent re-QE for "' + debt.slug + '": the recorded Step-8 review ran on the coder’s own family (' + debt.coderFamily + '); cause=' + debt.cause + (debt.qeGrade ? ' and graded ' + debt.qeGrade : '') + '.',
|
|
159
210
|
'Review with the ' + reviewFamily.toUpperCase() + ' family (the OTHER family than the coder — the suspended guard, restored).',
|
|
160
211
|
'Read: ' + files.join(' , ') + ' plus every file the change manifest lists.',
|
|
161
212
|
'Adversarially verify: correctness, edge cases, the ADR-named load-bearing property HAS a test, and whether the same-family review missed anything.',
|
|
162
213
|
'Output: GRADE A-F + numbered findings with file:line and severity; write the report to ' + artifactsDir + '/08b_reqe_report.md.',
|
|
214
|
+
'Also list every finding of the same-family 08_qe_report.md by number under a "## Prior findings" heading, one line each: <n>: closed|open — <reason>.',
|
|
163
215
|
'Then settle the debt: dz reqe --slug ' + debt.slug + ' --done --report ' + artifactsDir + '/08b_reqe_report.md',
|
|
164
216
|
];
|
|
165
217
|
const codexCmdTemplate = reviewFamily === 'openai'
|
|
@@ -178,6 +230,7 @@ export interface ReqeSettlement {
|
|
|
178
230
|
error: string | null;
|
|
179
231
|
grade: string | null;
|
|
180
232
|
epilogue: string | null;
|
|
233
|
+
verdict: ReqeVerdict | null;
|
|
181
234
|
}
|
|
182
235
|
|
|
183
236
|
/** Extract the verdict grade from a report, or null. LINE-ANCHORED and range-proof (Codex QE #7):
|
|
@@ -212,12 +265,18 @@ export function extractReportGrade(text: string): string | null {
|
|
|
212
265
|
export function settleReqeDebt(debt: ReqeDebt, reportText: string, reportPath: string): ReqeSettlement {
|
|
213
266
|
const text = String(reportText ?? '');
|
|
214
267
|
if (text.trim().length < 200) {
|
|
215
|
-
return { ok: false, error: 'report too small to be a review (< 200 chars of substance) — refusing to settle', grade: null, epilogue: null };
|
|
268
|
+
return { ok: false, error: 'report too small to be a review (< 200 chars of substance) — refusing to settle', grade: null, epilogue: null, verdict: null };
|
|
216
269
|
}
|
|
217
270
|
const grade = extractReportGrade(text);
|
|
218
271
|
if (grade === null) {
|
|
219
|
-
return { ok: false, error: 'report names no unambiguous line-anchored GRADE (A-F) — text without exactly one verdict grade is not a verdict; refusing to settle', grade: null, epilogue: null };
|
|
272
|
+
return { ok: false, error: 'report names no unambiguous line-anchored GRADE (A-F) — text without exactly one verdict grade is not a verdict; refusing to settle', grade: null, epilogue: null, verdict: null };
|
|
220
273
|
}
|
|
274
|
+
const verdict = buildReqeVerdict(text);
|
|
275
|
+
const { counts, findings, prior } = verdict;
|
|
276
|
+
const priorSummary = prior.kind === 'closed' ? `closed (${prior.closed}/${prior.total})`
|
|
277
|
+
: prior.kind === 'open' ? `open(${prior.open.length}) of ${prior.total}` : 'unassessed';
|
|
278
|
+
const mismatch = findings.kind === 'accepted' && findings.declared !== null && findings.declared !== findings.rows
|
|
279
|
+
? ` (declared ${findings.declared}, parsed ${findings.rows})` : '';
|
|
221
280
|
const reviewFamily: ModelFamily = debt.coderFamily === 'openai' ? 'claude' : 'openai';
|
|
222
281
|
const epilogue = [
|
|
223
282
|
'',
|
|
@@ -230,24 +289,41 @@ export function settleReqeDebt(debt: ReqeDebt, reportText: string, reportPath: s
|
|
|
230
289
|
'',
|
|
231
290
|
'- report: `' + reportPath + '`',
|
|
232
291
|
'- re-QE grade: **' + grade + '**' + (debt.qeGrade ? ' (same-family grade on record: ' + debt.qeGrade + ')' : ''),
|
|
292
|
+
`- new findings: ${verdict.new} — BLOCKER ${counts.blocker}, HIGH ${counts.high}, MEDIUM ${counts.medium}, LOW ${counts.low}, unknown ${counts.unknown} (rows ${verdict.rows})${mismatch}`,
|
|
293
|
+
`- prior findings: ${priorSummary}`,
|
|
294
|
+
...(findings.kind === 'accepted' ? [] : [
|
|
295
|
+
`- findings: unclassified (${findings.kind === 'absent' ? 'absent' : `rejected: ${findings.reason}`})`,
|
|
296
|
+
]),
|
|
297
|
+
...verdict.blocking.map((row) => `- ${row.severity} — ${row.title}${row.where === null ? '' : ` (${row.where})`}`),
|
|
298
|
+
'- severities and prior-finding status as declared by the reviewer; dz reqe counts them, it does not judge them.',
|
|
233
299
|
'- settled via `dz reqe --done` (fail-closed: an existing graded report is required).',
|
|
234
300
|
'',
|
|
235
301
|
].join('\n');
|
|
236
|
-
return { ok: true, error: null, grade, epilogue };
|
|
302
|
+
return { ok: true, error: null, grade, epilogue, verdict };
|
|
237
303
|
}
|
|
238
304
|
|
|
239
305
|
/** Render the debt list for `dz reqe` / the `dz usage` surfacing line. */
|
|
240
306
|
export function renderReqeList(debts: readonly ReqeDebt[], malformed: number): string[] {
|
|
241
307
|
const lines: string[] = [];
|
|
242
308
|
if (debts.length === 0 && malformed === 0) {
|
|
243
|
-
lines.push('dz reqe: no re-QE debts —
|
|
309
|
+
lines.push('dz reqe: no re-QE debts — no unsettled machine debts recorded.');
|
|
244
310
|
} else {
|
|
245
311
|
lines.push('dz reqe — ' + debts.length + ' unsettled re-QE debt(s):');
|
|
246
312
|
for (const d of debts) {
|
|
247
|
-
lines.push(' ' + d.slug + ' coder=' + d.coderFamily + ' qe=' + d.qeFamily + (d.qeGrade ? ' grade=' + d.qeGrade : '') + (d.emittedAt ? ' ' + d.emittedAt : '') + ' → dz reqe --slug ' + d.slug);
|
|
313
|
+
lines.push(' ' + d.slug + ' coder=' + d.coderFamily + ' qe=' + d.qeFamily + (d.qeGrade ? ' grade=' + d.qeGrade : '') + (d.emittedAt ? ' ' + d.emittedAt : '') + ' cause=' + d.cause + ' → dz reqe --slug ' + d.slug);
|
|
248
314
|
}
|
|
249
315
|
}
|
|
250
316
|
if (malformed > 0) lines.push(' ' + malformed + ' malformed debt file(s) skipped (named, never silent).');
|
|
251
317
|
lines.push(REQE_SCOPE);
|
|
252
318
|
return lines;
|
|
253
319
|
}
|
|
320
|
+
|
|
321
|
+
|
|
322
|
+
/** Stable keys, including zero counts, for the dz usage JSON output. */
|
|
323
|
+
export function countReqeByCause(debts: readonly ReqeDebt[]): Record<ReqeCause, number> {
|
|
324
|
+
const counts: Record<ReqeCause, number> = {
|
|
325
|
+
'usage-switched': 0, 'probe-failed': 0, 'same-family-fallback': 0, 'same-family-pinned': 0,
|
|
326
|
+
};
|
|
327
|
+
for (const debt of debts) counts[debt.cause] += 1;
|
|
328
|
+
return counts;
|
|
329
|
+
}
|
package/src/round.ts
CHANGED
|
@@ -78,6 +78,16 @@ export interface RoundLedgerRow {
|
|
|
78
78
|
readonly reason: string | null;
|
|
79
79
|
readonly round: number;
|
|
80
80
|
readonly lessons: readonly string[];
|
|
81
|
+
/** Recall coverage for the "доведённая работа" metric: measured on 2026-09-21, 0 of 462
|
|
82
|
+
* ledger rows carried `recalled`, losing the shown-to-used link across 124 round rows.
|
|
83
|
+
* Preserve the lessons shown at open as exact IDs, deduplicated by Set and sorted; omit when empty. */
|
|
84
|
+
readonly recalled?: readonly string[];
|
|
85
|
+
/** Used lessons drawn from the shown set: `recalled` intersected with `lessons`, sorted and
|
|
86
|
+
* omitted when empty. Lessons obtained elsewhere do not count toward recall coverage. */
|
|
87
|
+
readonly recalledUsed?: readonly string[];
|
|
88
|
+
/** Shown lessons not used: `recalled` minus `lessons`, sorted and omitted when empty. Together
|
|
89
|
+
* with `recalledUsed`, preserves the denominator for the "доведённая работа" metric. */
|
|
90
|
+
readonly recalledUnused?: readonly string[];
|
|
81
91
|
readonly noNewKnowledge: string | null;
|
|
82
92
|
readonly note: string;
|
|
83
93
|
readonly date: null;
|
|
@@ -568,6 +578,11 @@ export function closeRound(input: {
|
|
|
568
578
|
? (nonEmpty(input.shipTreeDirtyReason) ? input.shipTreeDirtyReason.trim() : 'not provided')
|
|
569
579
|
: undefined;
|
|
570
580
|
|
|
581
|
+
const recalled = [...new Set(input.state.recalled)].sort();
|
|
582
|
+
const usedLessons = new Set(lessons);
|
|
583
|
+
const recalledUsed = recalled.filter((id) => usedLessons.has(id));
|
|
584
|
+
const recalledUnused = recalled.filter((id) => !usedLessons.has(id));
|
|
585
|
+
|
|
571
586
|
const row: RoundLedgerRow = {
|
|
572
587
|
slug: input.state.slug,
|
|
573
588
|
stage: 'round',
|
|
@@ -584,6 +599,9 @@ export function closeRound(input: {
|
|
|
584
599
|
reason: nonEmpty(input.reason) ? input.reason.trim() : null,
|
|
585
600
|
round: input.state.round,
|
|
586
601
|
lessons,
|
|
602
|
+
...(recalled.length > 0 ? { recalled } : {}),
|
|
603
|
+
...(recalledUsed.length > 0 ? { recalledUsed } : {}),
|
|
604
|
+
...(recalledUnused.length > 0 ? { recalledUnused } : {}),
|
|
587
605
|
noNewKnowledge: noNewKnowledge === '' ? null : noNewKnowledge,
|
|
588
606
|
note: note === '' ? marker : `${marker} | ${note}`,
|
|
589
607
|
date: null,
|
package/src/session-retro.ts
CHANGED
|
@@ -18,7 +18,7 @@ import { existsSync, readFileSync, readdirSync, statSync, openSync, readSync, cl
|
|
|
18
18
|
import { join, basename, dirname } from 'node:path';
|
|
19
19
|
import { homedir } from 'node:os';
|
|
20
20
|
|
|
21
|
-
import {
|
|
21
|
+
import { withProjectLockSync, NamedLockTimeoutError } from './named-lock.js';
|
|
22
22
|
|
|
23
23
|
export interface SessionEvent {
|
|
24
24
|
readonly kind: 'user' | 'assistant' | 'tool';
|
|
@@ -696,7 +696,7 @@ const writeJsonAtomic = (path: string, value: unknown): void => {
|
|
|
696
696
|
* a partial trailing line is left for the next scan) → fold the debt → persist state + sentinel.
|
|
697
697
|
* NEVER throws (it runs inside a Stop hook; a broken scan must never surface as a turn failure).
|
|
698
698
|
*
|
|
699
|
-
* The WHOLE read→fold→write runs under `
|
|
699
|
+
* The WHOLE read→fold→write runs under `withProjectLockSync` (cross-family QE P1-3): two overlapping
|
|
700
700
|
* Stop scans that both read the same offset/sentinel and then rename/unlink independently lose one
|
|
701
701
|
* side's update — atomic per-file renames do not prevent that, only mutual exclusion does. The
|
|
702
702
|
* critical section is short and synchronous (bounded ≤8 MB read, no subprocess, 65 ms measured on
|
|
@@ -707,7 +707,7 @@ const writeJsonAtomic = (path: string, value: unknown): void => {
|
|
|
707
707
|
export function runRetroTailScan(dzDir: string, transcriptPath: string | null, nowIso?: string): TailScanOutcome {
|
|
708
708
|
if (transcriptPath === null || transcriptPath === '') return { status: 'no-transcript', scannedBytes: 0, offset: 0 };
|
|
709
709
|
try {
|
|
710
|
-
return
|
|
710
|
+
return withProjectLockSync(
|
|
711
711
|
dirname(dzDir),
|
|
712
712
|
RETRO_SCAN_LOCK_NAME,
|
|
713
713
|
() => scanTailUnderLock(dzDir, transcriptPath, nowIso),
|