@dzhechkov/harness-core 0.8.37 → 0.8.39
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 +253 -133
- package/README.md +209 -0
- package/dist/agentdb-index.d.ts +3 -3
- package/dist/agentdb-index.js +5 -5
- package/dist/agentdb-index.js.map +1 -1
- package/dist/amendment-trace.d.ts.map +1 -1
- package/dist/amendment-trace.js +4 -1
- package/dist/amendment-trace.js.map +1 -1
- package/dist/backlog.d.ts +2 -1
- package/dist/backlog.d.ts.map +1 -1
- package/dist/backlog.js +3 -2
- package/dist/backlog.js.map +1 -1
- package/dist/brain.d.ts.map +1 -1
- package/dist/brain.js +9 -2
- package/dist/brain.js.map +1 -1
- package/dist/bto-optimize.d.ts.map +1 -1
- package/dist/bto-optimize.js +9 -12
- package/dist/bto-optimize.js.map +1 -1
- package/dist/claim-check.d.ts.map +1 -1
- package/dist/claim-check.js +50 -14
- package/dist/claim-check.js.map +1 -1
- package/dist/cross-family-control.d.ts +35 -0
- package/dist/cross-family-control.d.ts.map +1 -1
- package/dist/cross-family-control.js +49 -3
- package/dist/cross-family-control.js.map +1 -1
- package/dist/experiment-assign.d.ts +110 -0
- package/dist/experiment-assign.d.ts.map +1 -0
- package/dist/experiment-assign.js +229 -0
- package/dist/experiment-assign.js.map +1 -0
- package/dist/feature-adr-checkpoints.d.ts +7 -2
- package/dist/feature-adr-checkpoints.d.ts.map +1 -1
- package/dist/feature-adr-checkpoints.js +20 -5
- package/dist/feature-adr-checkpoints.js.map +1 -1
- package/dist/feature-adr-envelope.d.ts +12 -0
- package/dist/feature-adr-envelope.d.ts.map +1 -1
- package/dist/feature-adr-envelope.js +12 -1
- package/dist/feature-adr-envelope.js.map +1 -1
- package/dist/feature-adr-routing.d.ts +4 -0
- package/dist/feature-adr-routing.d.ts.map +1 -1
- package/dist/feature-adr-routing.js +15 -1
- package/dist/feature-adr-routing.js.map +1 -1
- package/dist/index.d.ts +12 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -4
- package/dist/index.js.map +1 -1
- package/dist/ledger-cost-fill.d.ts +58 -0
- package/dist/ledger-cost-fill.d.ts.map +1 -0
- package/dist/ledger-cost-fill.js +78 -0
- package/dist/ledger-cost-fill.js.map +1 -0
- package/dist/loop-blobs.generated.js +2 -2
- package/dist/loop-blobs.generated.js.map +1 -1
- package/dist/mutation-gate.d.ts +29 -0
- package/dist/mutation-gate.d.ts.map +1 -1
- package/dist/mutation-gate.js +20 -0
- package/dist/mutation-gate.js.map +1 -1
- package/dist/name-check.d.ts +20 -1
- package/dist/name-check.d.ts.map +1 -1
- package/dist/name-check.js +42 -1
- package/dist/name-check.js.map +1 -1
- package/dist/no-stubs.d.ts +10 -0
- package/dist/no-stubs.d.ts.map +1 -1
- package/dist/no-stubs.js +13 -9
- package/dist/no-stubs.js.map +1 -1
- package/dist/patterns.d.ts +24 -0
- package/dist/patterns.d.ts.map +1 -1
- package/dist/patterns.js +49 -9
- package/dist/patterns.js.map +1 -1
- package/dist/publish-source-scope.d.ts +32 -0
- package/dist/publish-source-scope.d.ts.map +1 -0
- package/dist/publish-source-scope.js +53 -0
- package/dist/publish-source-scope.js.map +1 -0
- package/dist/publish.d.ts +5 -0
- package/dist/publish.d.ts.map +1 -1
- package/dist/publish.js +32 -21
- package/dist/publish.js.map +1 -1
- package/dist/qe-bridge.d.ts +12 -1
- package/dist/qe-bridge.d.ts.map +1 -1
- package/dist/qe-bridge.js +21 -11
- package/dist/qe-bridge.js.map +1 -1
- package/dist/rake-analyzer.d.ts +10 -3
- package/dist/rake-analyzer.d.ts.map +1 -1
- package/dist/rake-analyzer.js +84 -22
- package/dist/rake-analyzer.js.map +1 -1
- package/dist/recap.d.ts.map +1 -1
- package/dist/recap.js +8 -4
- package/dist/recap.js.map +1 -1
- package/dist/release.d.ts +2 -1
- package/dist/release.d.ts.map +1 -1
- package/dist/release.js +18 -4
- package/dist/release.js.map +1 -1
- package/dist/reqe.d.ts.map +1 -1
- package/dist/reqe.js +11 -1
- package/dist/reqe.js.map +1 -1
- package/dist/review-cost.d.ts +51 -0
- package/dist/review-cost.d.ts.map +1 -0
- package/dist/review-cost.js +110 -0
- package/dist/review-cost.js.map +1 -0
- package/dist/round.d.ts +136 -3
- package/dist/round.d.ts.map +1 -1
- package/dist/round.js +215 -6
- package/dist/round.js.map +1 -1
- package/dist/run-records.d.ts +51 -0
- package/dist/run-records.d.ts.map +1 -1
- package/dist/run-records.js +148 -2
- package/dist/run-records.js.map +1 -1
- package/dist/score.d.ts.map +1 -1
- package/dist/score.js +8 -1
- package/dist/score.js.map +1 -1
- package/dist/sign.d.ts +23 -0
- package/dist/sign.d.ts.map +1 -1
- package/dist/sign.js +52 -0
- package/dist/sign.js.map +1 -1
- package/dist/skills-verify.d.ts +8 -5
- package/dist/skills-verify.d.ts.map +1 -1
- package/dist/skills-verify.js +59 -7
- package/dist/skills-verify.js.map +1 -1
- package/dist/store-guard-prune.d.ts +22 -0
- package/dist/store-guard-prune.d.ts.map +1 -0
- package/dist/store-guard-prune.js +54 -0
- package/dist/store-guard-prune.js.map +1 -0
- package/dist/sweep-failure-classify.d.ts +45 -0
- package/dist/sweep-failure-classify.d.ts.map +1 -0
- package/dist/sweep-failure-classify.js +90 -0
- package/dist/sweep-failure-classify.js.map +1 -0
- package/dist/workflow-run-dispatch.d.ts +14 -2
- package/dist/workflow-run-dispatch.d.ts.map +1 -1
- package/dist/workflow-run-dispatch.js +14 -2
- package/dist/workflow-run-dispatch.js.map +1 -1
- package/package.json +1 -1
- package/sbom.json +432 -132
- package/src/agentdb-index.ts +5 -5
- package/src/amendment-trace.ts +4 -1
- package/src/backlog.ts +3 -2
- package/src/brain.ts +8 -2
- package/src/bto-optimize.ts +10 -8
- package/src/claim-check.ts +48 -12
- package/src/cross-family-control.ts +81 -3
- package/src/experiment-assign.ts +276 -0
- package/src/feature-adr-checkpoints.ts +20 -5
- package/src/feature-adr-envelope.ts +24 -1
- package/src/feature-adr-routing.ts +15 -1
- package/src/index.ts +23 -4
- package/src/loop-blobs.generated.ts +2 -2
- package/src/mutation-gate.ts +59 -0
- package/src/name-check.ts +43 -1
- package/src/no-stubs.ts +14 -5
- package/src/patterns.ts +49 -8
- package/src/publish-source-scope.ts +53 -0
- package/src/publish.ts +38 -16
- package/src/qe-bridge.ts +30 -11
- package/src/rake-analyzer.ts +75 -20
- package/src/rake-signatures.json +98 -0
- package/src/recap.ts +8 -4
- package/src/release.ts +18 -4
- package/src/reqe.ts +11 -1
- package/src/review-cost.ts +139 -0
- package/src/round.ts +327 -11
- package/src/run-records.ts +175 -1
- package/src/score.ts +8 -1
- package/src/sign.ts +53 -0
- package/src/skills-verify.ts +69 -9
- package/src/store-guard-prune.ts +81 -0
- package/src/sweep-failure-classify.ts +88 -0
- package/src/workflow-run-dispatch.ts +14 -2
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* review-cost-ledger T1/FR-1/A1 (ADR-001 п.1): a pure parser for the qe-bridge reviewer's own cost
|
|
3
|
+
* receipt — the last non-empty line of the Claude CLI's raw stdout, which (for every observed
|
|
4
|
+
* signoff) is a JSON object carrying `total_cost_usd`, `usage.*`, `duration_ms`, `num_turns`.
|
|
5
|
+
*
|
|
6
|
+
* Deliberately narrow and honest: absence of a price is never silently read as a price of zero.
|
|
7
|
+
* `status:'absent'` (no non-empty line at all) and `status:'unparseable'` (a line that IS present
|
|
8
|
+
* but does not parse into a usable cost) are each their own outcome — never collapsed into `ok`, and
|
|
9
|
+
* never guessed. This module owns no filesystem access (NFR-2): the caller (the cli) reads the
|
|
10
|
+
* stdout sidecar file and hands its TEXT in here.
|
|
11
|
+
*/
|
|
12
|
+
function finiteNonNegative(value) {
|
|
13
|
+
return typeof value === 'number' && Number.isFinite(value) && value >= 0;
|
|
14
|
+
}
|
|
15
|
+
function finiteNumberOrNull(value) {
|
|
16
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : null;
|
|
17
|
+
}
|
|
18
|
+
/** fix-round-1 #6 (Codex r1 HIGH #6): a token COMPONENT is a count, never a measurement — it is only
|
|
19
|
+
* ever trustworthy as a nonnegative SAFE integer. `1e308` is `typeof 'number'` and finite, but adding
|
|
20
|
+
* four of those silently produces `Infinity` (which then serializes to `null` on the ledger row while
|
|
21
|
+
* the in-memory type still claims `number`); a fractional or >2^53 value is likewise not a real token
|
|
22
|
+
* count. Distinct from "component ABSENT" (undefined — stays the existing partial-with-zero path). */
|
|
23
|
+
function safeNonNegativeInteger(value) {
|
|
24
|
+
return typeof value === 'number' && Number.isSafeInteger(value) && value >= 0;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* `text` is the full raw stdout of a qe-bridge reviewer invocation (Claude CLI, `--output-format
|
|
28
|
+
* json`-shaped result line). The COST line is always the LAST non-empty line — Claude CLI streams
|
|
29
|
+
* intermediate events first and finishes with one `type:"result"` object carrying the run's totals.
|
|
30
|
+
*/
|
|
31
|
+
export function parseQeBridgeStdoutCost(text) {
|
|
32
|
+
const lines = text.split('\n');
|
|
33
|
+
let lastLine = '';
|
|
34
|
+
for (let i = lines.length - 1; i >= 0; i -= 1) {
|
|
35
|
+
const candidate = lines[i].trim();
|
|
36
|
+
if (candidate !== '') {
|
|
37
|
+
lastLine = candidate;
|
|
38
|
+
break;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
if (lastLine === '')
|
|
42
|
+
return { status: 'absent' };
|
|
43
|
+
let parsed;
|
|
44
|
+
try {
|
|
45
|
+
parsed = JSON.parse(lastLine);
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
return { status: 'unparseable', reason: `last non-empty line is not valid JSON: ${error instanceof Error ? error.message : String(error)}` };
|
|
49
|
+
}
|
|
50
|
+
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
|
51
|
+
return { status: 'unparseable', reason: 'last non-empty line did not parse to a JSON object' };
|
|
52
|
+
}
|
|
53
|
+
const rec = parsed;
|
|
54
|
+
const totalCostUsd = rec['total_cost_usd'];
|
|
55
|
+
if (!finiteNonNegative(totalCostUsd)) {
|
|
56
|
+
return {
|
|
57
|
+
status: 'unparseable',
|
|
58
|
+
reason: typeof totalCostUsd === 'number'
|
|
59
|
+
? `total_cost_usd is not a finite non-negative number: ${totalCostUsd}`
|
|
60
|
+
: `total_cost_usd field is missing or not a number (got ${JSON.stringify(totalCostUsd)})`,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
const usageRaw = rec['usage'];
|
|
64
|
+
const usage = typeof usageRaw === 'object' && usageRaw !== null && !Array.isArray(usageRaw)
|
|
65
|
+
? usageRaw
|
|
66
|
+
: {};
|
|
67
|
+
let tokensPartial = false;
|
|
68
|
+
// fix-round-1 #6: a MISSING component (undefined) is the pre-existing partial-with-zero case
|
|
69
|
+
// (never touched by this fix); a PRESENT-but-invalid component (adversarial: 1e308, a fraction,
|
|
70
|
+
// 2^53+1, negative, NaN) is a different failure — the whole result turns `unparseable` rather than
|
|
71
|
+
// silently zeroing a value that was actually there. `invalidComponent` names which key failed.
|
|
72
|
+
let invalidComponent = null;
|
|
73
|
+
const component = (key) => {
|
|
74
|
+
const value = usage[key];
|
|
75
|
+
if (value === undefined) {
|
|
76
|
+
tokensPartial = true;
|
|
77
|
+
return 0;
|
|
78
|
+
}
|
|
79
|
+
if (safeNonNegativeInteger(value))
|
|
80
|
+
return value;
|
|
81
|
+
invalidComponent = key;
|
|
82
|
+
return 0;
|
|
83
|
+
};
|
|
84
|
+
const input = component('input_tokens');
|
|
85
|
+
const output = component('output_tokens');
|
|
86
|
+
const cacheCreation = component('cache_creation_input_tokens');
|
|
87
|
+
const cacheRead = component('cache_read_input_tokens');
|
|
88
|
+
if (invalidComponent !== null) {
|
|
89
|
+
return { status: 'unparseable', reason: `usage.${invalidComponent} is not a nonnegative safe integer: ${JSON.stringify(usage[invalidComponent])}` };
|
|
90
|
+
}
|
|
91
|
+
const total = input + output + cacheCreation + cacheRead;
|
|
92
|
+
if (!Number.isSafeInteger(total)) {
|
|
93
|
+
return { status: 'unparseable', reason: `token total ${total} exceeds Number.MAX_SAFE_INTEGER — not a trustworthy count` };
|
|
94
|
+
}
|
|
95
|
+
return {
|
|
96
|
+
status: 'ok',
|
|
97
|
+
costUsd: totalCostUsd,
|
|
98
|
+
tokens: {
|
|
99
|
+
input,
|
|
100
|
+
output,
|
|
101
|
+
cacheCreation,
|
|
102
|
+
cacheRead,
|
|
103
|
+
total,
|
|
104
|
+
...(tokensPartial ? { tokensPartial: true } : {}),
|
|
105
|
+
},
|
|
106
|
+
durationMs: finiteNumberOrNull(rec['duration_ms']),
|
|
107
|
+
numTurns: finiteNumberOrNull(rec['num_turns']),
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
//# sourceMappingURL=review-cost.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"review-cost.js","sourceRoot":"","sources":["../src/review-cost.ts"],"names":[],"mappings":"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"}
|
package/dist/round.d.ts
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* lesson ids, ledger writing/reading and pid liveness. In particular, this file never imports a
|
|
4
4
|
* filesystem or process API; the CLI owns `.dz/rounds/` and the witnessed ledger writer.
|
|
5
5
|
*/
|
|
6
|
+
import type { QeBridgeCost } from './review-cost.js';
|
|
6
7
|
declare const ROUND_OUTCOMES: readonly ["shipped", "refuted", "blocked", "abandoned"];
|
|
7
8
|
type RoundOutcome = typeof ROUND_OUTCOMES[number];
|
|
8
9
|
export interface RoundState {
|
|
@@ -20,6 +21,11 @@ export interface RoundState {
|
|
|
20
21
|
* unchanged through `round open --envelope` into `closeRound`'s ledger row. Opaque here (never
|
|
21
22
|
* interpreted by round.ts) — validated once, at `openRound`, and trusted from then on. */
|
|
22
23
|
readonly envelope?: unknown;
|
|
24
|
+
/** experiment-instrument FR-1/FR-3 (ADR-001): minted once at `openRound` — `--task` when given
|
|
25
|
+
* (validated), else `slug@startedAt`. Optional at the TYPE level, not because a fresh round ever
|
|
26
|
+
* omits it, but because a state file written before this feature landed has no such key on disk;
|
|
27
|
+
* `closeRound` derives the same default for that legacy case and names the source (A4/A5). */
|
|
28
|
+
readonly taskId?: string;
|
|
23
29
|
}
|
|
24
30
|
export interface RoundExecState {
|
|
25
31
|
readonly startedAt: string;
|
|
@@ -57,11 +63,85 @@ export interface RoundLedgerRow {
|
|
|
57
63
|
* closes — lets a retried `close` detect its own earlier row regardless of the clock. */
|
|
58
64
|
readonly stateId?: string;
|
|
59
65
|
/** measurement-integrity FR-7: present ONLY when `reviewer` was filled from the qe-bridge sidecar
|
|
60
|
-
* (no explicit `--reviewer`) — `elapsedMs / 60000`, rounded to 1 decimal.
|
|
66
|
+
* (no explicit `--reviewer`) — `elapsedMs / 60000`, rounded to 1 decimal. review-cost-ledger
|
|
67
|
+
* fix-round-1 #1/#2 (ADR-001 п.2 amended): also present when an EXPLICIT `--reviewer` AGREES with
|
|
68
|
+
* the sidecar's own `gradedBy` (`reviewSource:'flag+qe-bridge'` below names that case). */
|
|
61
69
|
readonly reviewMinutes?: number;
|
|
62
70
|
/** measurement-integrity FR-7: present ONLY alongside `reviewMinutes` — names where `reviewer` and
|
|
63
|
-
* `reviewMinutes` came from, so a reader never confuses a sidecar-sourced figure for a flag.
|
|
64
|
-
|
|
71
|
+
* `reviewMinutes` came from, so a reader never confuses a sidecar-sourced figure for a flag.
|
|
72
|
+
* review-cost-ledger fix-round-1 #1/#2 (ADR-001 п.2 amended): `'qe-bridge'` when `reviewer` was
|
|
73
|
+
* FILLED from the sidecar (no `--reviewer` flag); `'flag+qe-bridge'` when an explicit `--reviewer`
|
|
74
|
+
* AGREES with the sidecar's own `gradedBy` (case-insensitive `family:model` equality, or a
|
|
75
|
+
* family-only flag matching the sidecar's family) — the flag's IDENTITY is confirmed by the
|
|
76
|
+
* sidecar, so its price/minutes are attributed too. A DISAGREEING explicit `--reviewer` is refused
|
|
77
|
+
* outright (`closeRound` never reaches row-building for that case) — never silently ignored. */
|
|
78
|
+
readonly reviewSource?: 'qe-bridge' | 'flag+qe-bridge';
|
|
79
|
+
/** experiment-instrument FR-1/FR-3 (ADR-001): the task identity this round's state carried (or, for
|
|
80
|
+
* a legacy state with no `taskId` on disk, the same `slug@startedAt` default `openRound` would have
|
|
81
|
+
* minted — always present, never null: a round always has a slug and a startedAt). */
|
|
82
|
+
readonly taskId?: string;
|
|
83
|
+
/** experiment-instrument A4/A5: present ONLY when `taskId` above was DERIVED here rather than read
|
|
84
|
+
* from the state file — i.e. the state predates this feature. A fresh round never carries this. */
|
|
85
|
+
readonly taskIdSource?: 'derived-legacy';
|
|
86
|
+
/** experiment-instrument FR-3/A2: the git sha `dz round close` resolved at close time (cli-only —
|
|
87
|
+
* the core never shells out), present only for a FINISHED outcome (`shipped|refuted`). */
|
|
88
|
+
readonly shipSha?: string | null;
|
|
89
|
+
/** experiment-instrument FR-3/A2: same timestamp as this row's own close (`closedAt`), present only
|
|
90
|
+
* alongside `shipSha` — the anchor "when was this shipped", not a duplicate of `date`. */
|
|
91
|
+
readonly shippedAt?: string;
|
|
92
|
+
/** experiment-instrument A2: present only when `shipSha` is `null` for a finished outcome — why the
|
|
93
|
+
* sha could not be resolved (e.g. "not a git repository", "git failed: …"). */
|
|
94
|
+
readonly shipShaReason?: string;
|
|
95
|
+
/** experiment-instrument r1-5 (Codex r1 HIGH #5, ADR-001 amended): `HEAD` alone does not identify
|
|
96
|
+
* what was actually shipped when the worktree carries uncommitted changes — this hub's own tree is
|
|
97
|
+
* ALWAYS dirty with unrelated files, so a clean-tree requirement was rejected; instead the row
|
|
98
|
+
* names the limit. `true` when `git status --porcelain` was non-empty at close time, `false` when
|
|
99
|
+
* empty, present only for a FINISHED outcome, alongside `shipSha`. */
|
|
100
|
+
readonly shipTreeDirty?: boolean;
|
|
101
|
+
/** experiment-instrument r1-5: present ONLY when `shipTreeDirty` above could not be determined (the
|
|
102
|
+
* `git status --porcelain` probe itself failed) — the same "never a bare unexplained gap" rule
|
|
103
|
+
* `shipShaReason` already follows, for the sibling probe. */
|
|
104
|
+
readonly shipTreeDirtyReason?: string;
|
|
105
|
+
/** review-cost-ledger FR-2/A2 (ADR-001 п.1-2): the qe-bridge reviewer's OWN price, from the stdout
|
|
106
|
+
* sidecar — present only when {@link reviewerCostSource} is `'qe-bridge-stdout'` (a usable price
|
|
107
|
+
* was found) or `'unavailable'` (present with the reviewer known but the price is not — `null` in
|
|
108
|
+
* that case, never omitted: NFR-4, "nothing is guessed"). */
|
|
109
|
+
readonly reviewerCostUsd?: number | null;
|
|
110
|
+
/** review-cost-ledger FR-2/A2: sum of the four token components below, present only alongside a
|
|
111
|
+
* successful {@link reviewerCostUsd} (`reviewerCostSource:'qe-bridge-stdout'`). fix-round-1 #5
|
|
112
|
+
* (Codex r1 HIGH #5): `null` — never a silently-zeroed sum — when the sidecar's own
|
|
113
|
+
* `tokens.tokensPartial` was `true` (at least one component was missing from the source JSON): an
|
|
114
|
+
* incomplete sum read as an exact zero is indistinguishable from a genuine zero-token review, which
|
|
115
|
+
* is the defect this field exists to avoid. */
|
|
116
|
+
readonly reviewerTokens?: number | null;
|
|
117
|
+
/** review-cost-ledger FR-2/A2: the same four components broken out, present only alongside a
|
|
118
|
+
* successful {@link reviewerCostUsd}. fix-round-1 #5: `partial:true` when the sidecar's `total`
|
|
119
|
+
* above is `null` for the reason described there — the four components themselves are still the
|
|
120
|
+
* real (possibly zero-substituted) values, only the aggregate is withheld. */
|
|
121
|
+
readonly reviewerTokensBreakdown?: {
|
|
122
|
+
readonly input: number;
|
|
123
|
+
readonly output: number;
|
|
124
|
+
readonly cacheCreation: number;
|
|
125
|
+
readonly cacheRead: number;
|
|
126
|
+
readonly partial?: true;
|
|
127
|
+
};
|
|
128
|
+
/** review-cost-ledger FR-2/A2/A3: `'qe-bridge-stdout'` when {@link reviewerCostUsd} is a real,
|
|
129
|
+
* parsed price; `'unavailable'` when a reviewer is known but no usable price could be found (the
|
|
130
|
+
* Codex limit, an unreadable/absent/unparseable stdout sidecar, or — fix-round-1 #2 — a sidecar
|
|
131
|
+
* price that exists but is NOT tied to this row's reviewer). Present ONLY in the same branch as
|
|
132
|
+
* {@link reviewMinutes}/{@link reviewSource} above (`reviewSource` is `'qe-bridge'` or
|
|
133
|
+
* `'flag+qe-bridge'`) — WITH two named exceptions, neither of which ever yields an `'ok'` price:
|
|
134
|
+
* (a) a Codex reviewer supplied via `--reviewer` with no matching signoff still gets `'unavailable'`
|
|
135
|
+
* + a reason naming the limit (ADR-001 п.3/FR-4); (b) an explicit `--reviewer` alongside a sidecar
|
|
136
|
+
* that carries a REAL cost record but is NOT tied to it (empty `gradedBy`, i.e. never fills
|
|
137
|
+
* `reviewer` and never agrees with it either) also gets `'unavailable'` — fix-round-1 #2 (Codex r1
|
|
138
|
+
* CRITICAL #2): an untied sidecar price, even `status:'ok'`, is NEVER attributed to an explicit
|
|
139
|
+
* reviewer it was not written for; a DISAGREEING sidecar (real `gradedBy`, does not match the flag)
|
|
140
|
+
* is refused outright by `closeRound` before any row is built at all (fix-round-1 #1/BLOCKER). */
|
|
141
|
+
readonly reviewerCostSource?: 'qe-bridge-stdout' | 'unavailable';
|
|
142
|
+
/** review-cost-ledger FR-2/A3: present ONLY alongside `reviewerCostSource:'unavailable'` — the
|
|
143
|
+
* status text explaining WHY (never a bare unexplained gap, same discipline as `shipShaReason`). */
|
|
144
|
+
readonly reviewerCostReason?: string;
|
|
65
145
|
}
|
|
66
146
|
/**
|
|
67
147
|
* measurement-integrity FR-7: the qe-bridge cross-model-review signoff for THIS round — read by the
|
|
@@ -87,6 +167,12 @@ export interface RoundReviewSidecar {
|
|
|
87
167
|
/** measurement-integrity fix-round-1/F9: when this signoff was emitted — the timestamp
|
|
88
168
|
* `findQeBridgeSignoffForRound` uses to confirm it falls inside THIS round's own interval. */
|
|
89
169
|
readonly emittedAt?: string;
|
|
170
|
+
/** review-cost-ledger FR-2/T3 (ADR-001 п.1): the reviewer's own price, read by the CALLER (the cli
|
|
171
|
+
* — `readQeBridgeCostSidecar`, T3) from the signoff's `rawStdoutFile` and parsed by
|
|
172
|
+
* `parseQeBridgeStdoutCost` (T1). `round.ts` never opens a file itself, same discipline as
|
|
173
|
+
* `gradedBy`/`elapsedMs` above. Absent when the caller never attempted a cost lookup at all
|
|
174
|
+
* (distinct from `{status:'absent'}`, which means a lookup WAS attempted and found nothing). */
|
|
175
|
+
readonly cost?: QeBridgeCost;
|
|
90
176
|
}
|
|
91
177
|
type RoundRefusal = {
|
|
92
178
|
readonly ok: false;
|
|
@@ -111,6 +197,10 @@ export declare function openRound(input: {
|
|
|
111
197
|
* envelope refuses the open (exit 2) with the validator's own reason, same as any other malformed
|
|
112
198
|
* input to this command. Absent stays absent (round open without --envelope is unaffected). */
|
|
113
199
|
readonly envelope?: unknown;
|
|
200
|
+
/** experiment-instrument FR-1 (ADR-001): explicit task identity for this round. Absent ⇒
|
|
201
|
+
* `slug@startedAt` (minted below); present but invalid (empty, >120 chars, control characters) ⇒
|
|
202
|
+
* refused (A1) — never silently substituted with the default. */
|
|
203
|
+
readonly task?: string | undefined;
|
|
114
204
|
}): {
|
|
115
205
|
readonly ok: true;
|
|
116
206
|
readonly state: RoundState;
|
|
@@ -136,6 +226,20 @@ export declare function closeRound(input: {
|
|
|
136
226
|
readonly grade?: string | undefined;
|
|
137
227
|
/** measurement-integrity FR-7: the qe-bridge signoff for this slug, read by the CALLER. */
|
|
138
228
|
readonly reviewSidecar?: RoundReviewSidecar | undefined;
|
|
229
|
+
/** experiment-instrument FR-3/A2/NFR-3: the git sha the CLI resolved (`git rev-parse HEAD`) — the
|
|
230
|
+
* core never shells out, so this always arrives as data, `null` when it could not be resolved.
|
|
231
|
+
* Only consulted for a FINISHED outcome (`shipped|refuted`); ignored otherwise. */
|
|
232
|
+
readonly shipSha?: string | null | undefined;
|
|
233
|
+
/** experiment-instrument A2: why `shipSha` is `null` (e.g. "not a git repository", "git failed: …")
|
|
234
|
+
* — required to explain a null sha on a finished outcome, ignored when `shipSha` is non-null. */
|
|
235
|
+
readonly shipShaReason?: string | undefined;
|
|
236
|
+
/** experiment-instrument r1-5: whether `git status --porcelain` in `projectRoot` was non-empty at
|
|
237
|
+
* close time (cli-resolved, same discipline as `shipSha` — the core never shells out). Ignored for
|
|
238
|
+
* an unfinished outcome, same as `shipSha`. */
|
|
239
|
+
readonly shipTreeDirty?: boolean | undefined;
|
|
240
|
+
/** experiment-instrument r1-5: why `shipTreeDirty` could not be determined — required only when the
|
|
241
|
+
* probe itself failed (`shipTreeDirty` absent), ignored otherwise. */
|
|
242
|
+
readonly shipTreeDirtyReason?: string | undefined;
|
|
139
243
|
}, io: {
|
|
140
244
|
readonly writeLedger: (row: RoundLedgerRow) => unknown;
|
|
141
245
|
readonly readLedgerTail: () => string;
|
|
@@ -168,6 +272,35 @@ export declare function validateClosedRoundLedgerRow(row: unknown): {
|
|
|
168
272
|
readonly ok: false;
|
|
169
273
|
readonly reason: string;
|
|
170
274
|
};
|
|
275
|
+
/**
|
|
276
|
+
* experiment-instrument FR-1/FR-3 (ADR-001 D-A): the single source every OTHER writer (an auto ledger
|
|
277
|
+
* row, a qe-bridge signoff, a control-review row) consults to find this slug's task identity —
|
|
278
|
+
* never guessed, never minted here. `states` is whatever the CALLER already read as "currently open
|
|
279
|
+
* round state files for this slug" (the cli walks `.dz/rounds/<slug>-*.json`); this function does not
|
|
280
|
+
* touch a filesystem and does not assume the caller pre-filtered by slug, so it filters again itself.
|
|
281
|
+
*
|
|
282
|
+
* `unreadableStateCount` (r1-2, Codex r1 HIGH #2) is how many FILENAMES the caller found matching this
|
|
283
|
+
* slug's pattern that it could NOT parse into a `RoundState` — a corrupt or half-written state file is
|
|
284
|
+
* never silent absence; it means the true open-round count for this slug is UNKNOWN, not zero.
|
|
285
|
+
*
|
|
286
|
+
* - Any unreadable candidate exists AND no readable one does ⇒ `{taskId: null, source: 'unavailable'}`
|
|
287
|
+
* — a round MAY be open here, but its state cannot be read (r1-2).
|
|
288
|
+
* - Any unreadable candidate exists ALONGSIDE at least one readable one ⇒ `{taskId: null, source:
|
|
289
|
+
* 'ambiguous'}` — the readable one might not be the only genuinely open round (r1-2).
|
|
290
|
+
* - Zero candidates at all (readable or not) ⇒ `{taskId: null, source: 'no-open-round'}` — no open
|
|
291
|
+
* round, nothing to fill from (A4).
|
|
292
|
+
* - Two or more readable matches, no unreadable ones ⇒ `{taskId: null, source: 'ambiguous'}` — which
|
|
293
|
+
* one is authoritative is not decidable here; a NULL is the honest answer, never "the latest wins" (A5).
|
|
294
|
+
* - Exactly one readable match, no unreadable ones ⇒ its `taskId` when the state carries one
|
|
295
|
+
* (`source: 'open-round'`), or the SAME `slug@startedAt` default `openRound`/`closeRound` would
|
|
296
|
+
* derive for a legacy state missing the field on disk — labelled `source: 'derived-legacy'` (r1-1,
|
|
297
|
+
* Codex r1 CRITICAL #1: this used to report `'open-round'` for an invented value, indistinguishable
|
|
298
|
+
* from a value the round itself actually minted).
|
|
299
|
+
*/
|
|
300
|
+
export declare function readOpenRoundTaskId(states: readonly RoundState[], slug: string, unreadableStateCount?: number): {
|
|
301
|
+
readonly taskId: string | null;
|
|
302
|
+
readonly source: 'open-round' | 'derived-legacy' | 'no-open-round' | 'ambiguous' | 'unavailable';
|
|
303
|
+
};
|
|
171
304
|
export declare function listRounds(states: readonly RoundState[], input: {
|
|
172
305
|
readonly now: number;
|
|
173
306
|
readonly olderThanMinutes: number;
|
package/dist/round.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"round.d.ts","sourceRoot":"","sources":["../src/round.ts"],"names":[],"mappings":"AAAA;;;;GAIG;
|
|
1
|
+
{"version":3,"file":"round.d.ts","sourceRoot":"","sources":["../src/round.ts"],"names":[],"mappings":"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"}
|
package/dist/round.js
CHANGED
|
@@ -8,12 +8,37 @@ const ROUND_OUTCOMES = ['shipped', 'refuted', 'blocked', 'abandoned'];
|
|
|
8
8
|
function nonEmpty(value) {
|
|
9
9
|
return typeof value === 'string' && value.trim() !== '';
|
|
10
10
|
}
|
|
11
|
+
/** review-cost-ledger fix-round-1 #1/#2 (ADR-001 п.2 amended): does an explicit `--reviewer` AGREE
|
|
12
|
+
* with the qe-bridge sidecar's own `gradedBy` (already normalized by the caller to `family:model`,
|
|
13
|
+
* e.g. `claude:sonnet`)? Case-insensitive. An exact `family:model` match agrees; a FAMILY-ONLY flag
|
|
14
|
+
* (no `:` in it at all, e.g. `claude`) agrees with ANY `family:*` gradedBy of that same family —
|
|
15
|
+
* the lead naming only the family (not a specific model) is still confirming the sidecar's identity.
|
|
16
|
+
* Anything else (a different family, or a full `family:model` that does not match exactly) does NOT
|
|
17
|
+
* agree — `closeRound` refuses that case outright rather than guessing which one is right. */
|
|
18
|
+
function reviewerAgreesWithSidecar(reviewerFlag, gradedBy) {
|
|
19
|
+
const flag = reviewerFlag.trim().toLowerCase();
|
|
20
|
+
const graded = gradedBy.trim().toLowerCase();
|
|
21
|
+
if (flag === '' || graded === '')
|
|
22
|
+
return false;
|
|
23
|
+
if (flag === graded)
|
|
24
|
+
return true;
|
|
25
|
+
if (!flag.includes(':')) {
|
|
26
|
+
const gradedFamily = graded.split(':')[0] ?? '';
|
|
27
|
+
return flag === gradedFamily;
|
|
28
|
+
}
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
11
31
|
function validSlug(value) {
|
|
12
32
|
return /^[a-z0-9][a-z0-9._-]*$/i.test(value);
|
|
13
33
|
}
|
|
14
34
|
function validCount(value) {
|
|
15
35
|
return value === undefined || (Number.isInteger(value) && value >= 0);
|
|
16
36
|
}
|
|
37
|
+
/** experiment-instrument A1: a `--task` value is a non-empty string, at most 120 characters, with no
|
|
38
|
+
* control characters (C0 or DEL) — long/garbled input refuses rather than being silently minted. */
|
|
39
|
+
function validTaskId(value) {
|
|
40
|
+
return value.trim() !== '' && value.length <= 120 && !/[\u0000-\u001f\u007f]/.test(value);
|
|
41
|
+
}
|
|
17
42
|
export function openRound(input) {
|
|
18
43
|
if (!validSlug(input.slug) || !Number.isInteger(input.round) || input.round < 1 || !nonEmpty(input.topic)) {
|
|
19
44
|
return { ok: false, exit: 2, reason: 'нужны безопасный --slug, положительный --round и непустой --topic' };
|
|
@@ -23,6 +48,9 @@ export function openRound(input) {
|
|
|
23
48
|
if (!v.ok)
|
|
24
49
|
return { ok: false, exit: 2, reason: `--envelope invalid — ${v.reason}` };
|
|
25
50
|
}
|
|
51
|
+
if (input.task !== undefined && !validTaskId(input.task)) {
|
|
52
|
+
return { ok: false, exit: 2, reason: '--task должен быть непустой строкой ≤120 символов без управляющих символов' };
|
|
53
|
+
}
|
|
26
54
|
const runOwner = input.ownerKind === 'run';
|
|
27
55
|
if (!Number.isFinite(Date.parse(input.startedAt)) || !Number.isInteger(input.ownerPid)
|
|
28
56
|
|| (runOwner ? input.ownerPid !== 0 || !nonEmpty(input.ownerRun) : input.ownerPid < 1)
|
|
@@ -40,6 +68,7 @@ export function openRound(input) {
|
|
|
40
68
|
...(nonEmpty(input.run) ? { run: input.run.trim() } : {}),
|
|
41
69
|
recalled: [...input.recalled],
|
|
42
70
|
...(input.envelope !== undefined ? { envelope: input.envelope } : {}),
|
|
71
|
+
taskId: input.task !== undefined ? input.task.trim() : `${input.slug}@${input.startedAt}`,
|
|
43
72
|
};
|
|
44
73
|
let existingOwnerAlive = input.existingOwnerAlive;
|
|
45
74
|
if (input.existing?.ownerKind === 'run' && input.force) {
|
|
@@ -131,19 +160,123 @@ export function closeRound(input, io) {
|
|
|
131
160
|
return { ok: false, exit: 2, reason: `grade required for a finished review: outcome=${outcome} требует --grade <A|A-|B+|…>` };
|
|
132
161
|
}
|
|
133
162
|
const gradeForRow = gradeFlag;
|
|
134
|
-
// reviewer/reviewMinutes/reviewSource:
|
|
135
|
-
//
|
|
136
|
-
//
|
|
137
|
-
//
|
|
163
|
+
// reviewer/reviewMinutes/reviewSource: reviewer is TIED to the sidecar (ADR-001 п.2, amended by
|
|
164
|
+
// fix-round-1 #1/#2 after the Codex r1 BLOCKER/CRITICAL pair) either (a) it is FILLED from the
|
|
165
|
+
// sidecar (no explicit --reviewer), or (b) it is an explicit --reviewer that AGREES with the
|
|
166
|
+
// sidecar's own gradedBy. Case (b) exists because the pipeline's normal path ALWAYS passes
|
|
167
|
+
// --reviewer (feature-adr.js) — under the old "flag always wins, sidecar never trusted for cost"
|
|
168
|
+
// rule, the price this feature exists to record would NEVER be written on that path (BLOCKER #1).
|
|
169
|
+
// A DISAGREEING explicit --reviewer is refused outright, below — never silently ignored, and never
|
|
170
|
+
// silently misattributed (CRITICAL #2).
|
|
138
171
|
let reviewer = nonEmpty(input.reviewer) ? input.reviewer.trim() : null;
|
|
139
172
|
let reviewMinutes = null;
|
|
140
173
|
let reviewSource = null;
|
|
141
|
-
|
|
142
|
-
|
|
174
|
+
const sidecarGradedBy = input.reviewSidecar !== undefined && nonEmpty(input.reviewSidecar.gradedBy)
|
|
175
|
+
? input.reviewSidecar.gradedBy.trim()
|
|
176
|
+
: null;
|
|
177
|
+
if (reviewer === null && sidecarGradedBy !== null) {
|
|
178
|
+
reviewer = sidecarGradedBy;
|
|
143
179
|
const elapsedMs = input.reviewSidecar.elapsedMs;
|
|
144
180
|
reviewMinutes = Number.isFinite(elapsedMs) && elapsedMs >= 0 ? Math.round((elapsedMs / 60_000) * 10) / 10 : null;
|
|
145
181
|
reviewSource = 'qe-bridge';
|
|
146
182
|
}
|
|
183
|
+
else if (reviewer !== null && sidecarGradedBy !== null) {
|
|
184
|
+
if (reviewerAgreesWithSidecar(reviewer, sidecarGradedBy)) {
|
|
185
|
+
const elapsedMs = input.reviewSidecar.elapsedMs;
|
|
186
|
+
reviewMinutes = Number.isFinite(elapsedMs) && elapsedMs >= 0 ? Math.round((elapsedMs / 60_000) * 10) / 10 : null;
|
|
187
|
+
reviewSource = 'flag+qe-bridge';
|
|
188
|
+
}
|
|
189
|
+
else {
|
|
190
|
+
// fix-round-1 #1/#2 (ADR-001 п.2 amended): the same discipline a --grade disagreeing with the
|
|
191
|
+
// sidecar's own grade already follows (FR-7) — an explicit reviewer naming someone ELSE than
|
|
192
|
+
// the sidecar's own reviewer must never silently receive (or silently omit) that reviewer's
|
|
193
|
+
// price; it refuses, naming both values, before any row is built.
|
|
194
|
+
return {
|
|
195
|
+
ok: false,
|
|
196
|
+
exit: 2,
|
|
197
|
+
reason: `--reviewer "${reviewer}" conflicts with the qe-bridge sidecar's reviewer "${sidecarGradedBy}" for slug ${input.state.slug}`,
|
|
198
|
+
};
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
// review-cost-ledger FR-3/A4 (ADR-001 п.4): a FINISHED review (shipped|refuted) with NEITHER an
|
|
202
|
+
// explicit --reviewer NOR one filled from the sidecar is unmeasurable by definition — the audit
|
|
203
|
+
// this feature exists to close (ADR-001 Q5). blocked|abandoned carry no such requirement, same as
|
|
204
|
+
// they carry no grade requirement above: a review that never finished has no reviewer to name yet.
|
|
205
|
+
if (finishedOutcome && reviewer === null) {
|
|
206
|
+
return {
|
|
207
|
+
ok: false,
|
|
208
|
+
exit: 2,
|
|
209
|
+
reason: `finished review requires a reviewer: outcome=${outcome} needs --reviewer <model> (no --reviewer flag and no qe-bridge signoff to fill it from)`,
|
|
210
|
+
};
|
|
211
|
+
}
|
|
212
|
+
// review-cost-ledger FR-2/A2/A3 (ADR-001 п.1-3, amended by fix-round-1 #2/#3/#5): the reviewer's
|
|
213
|
+
// price is written ONLY when the row's reviewer is TIED to the sidecar — reviewSource is
|
|
214
|
+
// 'qe-bridge' or 'flag+qe-bridge' (the two cases computed above). fix-round-1 #2 (Codex r1
|
|
215
|
+
// CRITICAL #2): the old "or it carries no gradedBy at all" hack is REMOVED — an untrusted sidecar
|
|
216
|
+
// (empty gradedBy, e.g. T3's Codex-no-signoff synthesis) can still explain an ABSENT/UNPARSEABLE
|
|
217
|
+
// price (that is a structural fact about the instrument, not an attribution), but it can NEVER
|
|
218
|
+
// yield an 'ok' price for a reviewer it was not written for — an adversarial ok-cost sidecar with
|
|
219
|
+
// an empty gradedBy is exactly the CRITICAL #2 failing input, and it must never buy a price here.
|
|
220
|
+
let reviewerCostFields = null;
|
|
221
|
+
const sidecarCost = input.reviewSidecar?.cost;
|
|
222
|
+
const reviewerTiedToSidecar = reviewSource === 'qe-bridge' || reviewSource === 'flag+qe-bridge';
|
|
223
|
+
if (reviewerTiedToSidecar) {
|
|
224
|
+
// fix-round-1 #3 (Codex r1 HIGH #3): a sidecar that filled/confirmed the reviewer but carries NO
|
|
225
|
+
// cost record at all (cost undefined) used to leave every price field silently absent — the same
|
|
226
|
+
// "never a bare unexplained gap" discipline as `shipShaReason` now applies here too.
|
|
227
|
+
if (sidecarCost === undefined) {
|
|
228
|
+
reviewerCostFields = { reviewerCostUsd: null, reviewerCostSource: 'unavailable', reviewerCostReason: 'sidecar carried no cost record' };
|
|
229
|
+
}
|
|
230
|
+
else if (sidecarCost.status === 'ok') {
|
|
231
|
+
// fix-round-1 #5 (Codex r1 HIGH #5): tokensPartial on the sidecar means the sum is NOT a real
|
|
232
|
+
// count (at least one component was missing) — reviewerTokens goes null rather than a silently
|
|
233
|
+
// zeroed/short sum, while the still-valid price and the partial breakdown are kept.
|
|
234
|
+
const partial = sidecarCost.tokens.tokensPartial === true;
|
|
235
|
+
reviewerCostFields = {
|
|
236
|
+
reviewerCostUsd: sidecarCost.costUsd,
|
|
237
|
+
reviewerTokens: partial ? null : sidecarCost.tokens.total,
|
|
238
|
+
reviewerTokensBreakdown: {
|
|
239
|
+
input: sidecarCost.tokens.input,
|
|
240
|
+
output: sidecarCost.tokens.output,
|
|
241
|
+
cacheCreation: sidecarCost.tokens.cacheCreation,
|
|
242
|
+
cacheRead: sidecarCost.tokens.cacheRead,
|
|
243
|
+
...(partial ? { partial: true } : {}),
|
|
244
|
+
},
|
|
245
|
+
reviewerCostSource: 'qe-bridge-stdout',
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
else {
|
|
249
|
+
const reason = nonEmpty(sidecarCost.reason)
|
|
250
|
+
? sidecarCost.reason.trim()
|
|
251
|
+
: 'qe-bridge stdout sidecar carried no parseable cost line';
|
|
252
|
+
reviewerCostFields = { reviewerCostUsd: null, reviewerCostSource: 'unavailable', reviewerCostReason: reason };
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
else if (sidecarCost !== undefined) {
|
|
256
|
+
// The reviewer is NOT tied to this sidecar (an untrusted/untied sidecar — e.g. T3's
|
|
257
|
+
// Codex-no-signoff synthesis, empty gradedBy). It may still explain a NAMED limit
|
|
258
|
+
// (status !== 'ok') — that is honest instrument metadata, not an attribution. fix-round-1 #2: an
|
|
259
|
+
// 'ok' status here is NEVER trusted for a price, whatever the number — the sidecar was not
|
|
260
|
+
// written for this reviewer, full stop.
|
|
261
|
+
if (sidecarCost.status !== 'ok') {
|
|
262
|
+
// Lead delta after Codex r2 (#2 partial): an untied sidecar's OWN reason (the cli's Codex
|
|
263
|
+
// "tokens not visible" synthesis) was copied onto ANY explicit reviewer's row — a Claude
|
|
264
|
+
// reviewer got a Codex-specific limit as its explanation. The sidecar's reason travels only
|
|
265
|
+
// to a reviewer of the codex family; every other untied reviewer gets the neutral fact.
|
|
266
|
+
const reviewerIsCodex = /^codex\b/i.test(reviewer ?? '');
|
|
267
|
+
const reason = reviewerIsCodex && nonEmpty(sidecarCost.reason)
|
|
268
|
+
? sidecarCost.reason.trim()
|
|
269
|
+
: 'explicit --reviewer is not tied to a qe-bridge sidecar (no signoff names this reviewer)';
|
|
270
|
+
reviewerCostFields = { reviewerCostUsd: null, reviewerCostSource: 'unavailable', reviewerCostReason: reason };
|
|
271
|
+
}
|
|
272
|
+
else {
|
|
273
|
+
reviewerCostFields = {
|
|
274
|
+
reviewerCostUsd: null,
|
|
275
|
+
reviewerCostSource: 'unavailable',
|
|
276
|
+
reviewerCostReason: 'qe-bridge sidecar carried a price but is not tied to this row\'s reviewer',
|
|
277
|
+
};
|
|
278
|
+
}
|
|
279
|
+
}
|
|
147
280
|
const startedMs = Date.parse(input.state.startedAt);
|
|
148
281
|
const closedMs = Date.parse(input.closedAt);
|
|
149
282
|
if (!Number.isFinite(startedMs) || !Number.isFinite(closedMs) || closedMs < startedMs) {
|
|
@@ -152,6 +285,34 @@ export function closeRound(input, io) {
|
|
|
152
285
|
const compactTs = new Date(closedMs).toISOString().replace(/[-:.]/g, '');
|
|
153
286
|
const marker = `round-${input.state.slug}-${input.state.round}-${compactTs}`;
|
|
154
287
|
const note = input.note?.trim() ?? '';
|
|
288
|
+
// experiment-instrument FR-1/A4/A5: the state ALWAYS carries a taskId for a round opened after this
|
|
289
|
+
// feature; a state written before it lacks the key on disk, so the same default `openRound` mints
|
|
290
|
+
// is derived here and the derivation is named (`taskIdSource`) — never silently blended with a
|
|
291
|
+
// fresh-mint taskId, which is what `taskIdSource` absent means.
|
|
292
|
+
const taskIdFromState = input.state.taskId;
|
|
293
|
+
const taskId = nonEmpty(taskIdFromState) ? taskIdFromState.trim() : `${input.state.slug}@${input.state.startedAt}`;
|
|
294
|
+
const taskIdSource = nonEmpty(taskIdFromState) ? null : 'derived-legacy';
|
|
295
|
+
// experiment-instrument FR-3/A2: only a FINISHED outcome carries a ship anchor — `blocked|abandoned`
|
|
296
|
+
// never shipped anything, so a sha there would misleadingly imply a release. A null sha REQUIRES its
|
|
297
|
+
// reason to be recorded next to it (never a bare, unexplained null on a finished row) — r1-6 (Codex
|
|
298
|
+
// r1 MEDIUM #6): a caller that supplies no reason at all no longer leaves the row silently
|
|
299
|
+
// unexplained; a stable default fills the gap instead of an absent key.
|
|
300
|
+
const finishedForShip = outcome === 'shipped' || outcome === 'refuted';
|
|
301
|
+
const shipSha = finishedForShip ? (input.shipSha ?? null) : null;
|
|
302
|
+
const shipShaReason = finishedForShip && shipSha === null
|
|
303
|
+
? (nonEmpty(input.shipShaReason) ? input.shipShaReason.trim() : 'not provided')
|
|
304
|
+
: null;
|
|
305
|
+
// experiment-instrument r1-5 (Codex r1 HIGH #5, ADR-001 amended): `shipTreeDirty` is present only
|
|
306
|
+
// when the caller (the cli) actually resolved it; `shipTreeDirtyReason` fills the gap when it could
|
|
307
|
+
// not be. Neither is ever guessed here — the core never shells out to compute either.
|
|
308
|
+
const shipTreeDirtyValue = finishedForShip && typeof input.shipTreeDirty === 'boolean'
|
|
309
|
+
? input.shipTreeDirty
|
|
310
|
+
: undefined;
|
|
311
|
+
// r2-2 (Codex r2 MEDIUM N2, lead delta): a finished row with NEITHER field used to omit both —
|
|
312
|
+
// the same bare-gap shape r1-6 closed for the sha. The rule is symmetric: boolean, or a reason.
|
|
313
|
+
const shipTreeDirtyReason = finishedForShip && shipTreeDirtyValue === undefined
|
|
314
|
+
? (nonEmpty(input.shipTreeDirtyReason) ? input.shipTreeDirtyReason.trim() : 'not provided')
|
|
315
|
+
: undefined;
|
|
155
316
|
const row = {
|
|
156
317
|
slug: input.state.slug,
|
|
157
318
|
stage: 'round',
|
|
@@ -175,6 +336,13 @@ export function closeRound(input, io) {
|
|
|
175
336
|
...(input.state.envelope !== undefined ? { envelope: input.state.envelope } : {}),
|
|
176
337
|
...(reviewSource !== null ? { reviewSource } : {}),
|
|
177
338
|
...(reviewMinutes !== null ? { reviewMinutes } : {}),
|
|
339
|
+
taskId,
|
|
340
|
+
...(taskIdSource !== null ? { taskIdSource } : {}),
|
|
341
|
+
...(finishedForShip ? { shipSha, shippedAt: input.closedAt } : {}),
|
|
342
|
+
...(shipShaReason !== null ? { shipShaReason } : {}),
|
|
343
|
+
...(shipTreeDirtyValue !== undefined ? { shipTreeDirty: shipTreeDirtyValue } : {}),
|
|
344
|
+
...(shipTreeDirtyReason !== undefined ? { shipTreeDirtyReason } : {}),
|
|
345
|
+
...(reviewerCostFields !== null ? reviewerCostFields : {}),
|
|
178
346
|
};
|
|
179
347
|
try {
|
|
180
348
|
io.writeLedger(row);
|
|
@@ -231,6 +399,47 @@ export function validateClosedRoundLedgerRow(row) {
|
|
|
231
399
|
}
|
|
232
400
|
return { ok: true };
|
|
233
401
|
}
|
|
402
|
+
/**
|
|
403
|
+
* experiment-instrument FR-1/FR-3 (ADR-001 D-A): the single source every OTHER writer (an auto ledger
|
|
404
|
+
* row, a qe-bridge signoff, a control-review row) consults to find this slug's task identity —
|
|
405
|
+
* never guessed, never minted here. `states` is whatever the CALLER already read as "currently open
|
|
406
|
+
* round state files for this slug" (the cli walks `.dz/rounds/<slug>-*.json`); this function does not
|
|
407
|
+
* touch a filesystem and does not assume the caller pre-filtered by slug, so it filters again itself.
|
|
408
|
+
*
|
|
409
|
+
* `unreadableStateCount` (r1-2, Codex r1 HIGH #2) is how many FILENAMES the caller found matching this
|
|
410
|
+
* slug's pattern that it could NOT parse into a `RoundState` — a corrupt or half-written state file is
|
|
411
|
+
* never silent absence; it means the true open-round count for this slug is UNKNOWN, not zero.
|
|
412
|
+
*
|
|
413
|
+
* - Any unreadable candidate exists AND no readable one does ⇒ `{taskId: null, source: 'unavailable'}`
|
|
414
|
+
* — a round MAY be open here, but its state cannot be read (r1-2).
|
|
415
|
+
* - Any unreadable candidate exists ALONGSIDE at least one readable one ⇒ `{taskId: null, source:
|
|
416
|
+
* 'ambiguous'}` — the readable one might not be the only genuinely open round (r1-2).
|
|
417
|
+
* - Zero candidates at all (readable or not) ⇒ `{taskId: null, source: 'no-open-round'}` — no open
|
|
418
|
+
* round, nothing to fill from (A4).
|
|
419
|
+
* - Two or more readable matches, no unreadable ones ⇒ `{taskId: null, source: 'ambiguous'}` — which
|
|
420
|
+
* one is authoritative is not decidable here; a NULL is the honest answer, never "the latest wins" (A5).
|
|
421
|
+
* - Exactly one readable match, no unreadable ones ⇒ its `taskId` when the state carries one
|
|
422
|
+
* (`source: 'open-round'`), or the SAME `slug@startedAt` default `openRound`/`closeRound` would
|
|
423
|
+
* derive for a legacy state missing the field on disk — labelled `source: 'derived-legacy'` (r1-1,
|
|
424
|
+
* Codex r1 CRITICAL #1: this used to report `'open-round'` for an invented value, indistinguishable
|
|
425
|
+
* from a value the round itself actually minted).
|
|
426
|
+
*/
|
|
427
|
+
export function readOpenRoundTaskId(states, slug, unreadableStateCount = 0) {
|
|
428
|
+
const matches = states.filter((s) => s.slug === slug);
|
|
429
|
+
const unreadable = Number.isFinite(unreadableStateCount) && unreadableStateCount > 0 ? Math.floor(unreadableStateCount) : 0;
|
|
430
|
+
if (unreadable > 0 && matches.length === 0)
|
|
431
|
+
return { taskId: null, source: 'unavailable' };
|
|
432
|
+
if (unreadable > 0)
|
|
433
|
+
return { taskId: null, source: 'ambiguous' };
|
|
434
|
+
if (matches.length === 0)
|
|
435
|
+
return { taskId: null, source: 'no-open-round' };
|
|
436
|
+
if (matches.length > 1)
|
|
437
|
+
return { taskId: null, source: 'ambiguous' };
|
|
438
|
+
const state = matches[0];
|
|
439
|
+
const taskId = nonEmpty(state.taskId) ? state.taskId.trim() : `${state.slug}@${state.startedAt}`;
|
|
440
|
+
const source = nonEmpty(state.taskId) ? 'open-round' : 'derived-legacy';
|
|
441
|
+
return { taskId, source };
|
|
442
|
+
}
|
|
234
443
|
export function listRounds(states, input) {
|
|
235
444
|
if (!Number.isFinite(input.now) || !Number.isFinite(input.olderThanMinutes) || input.olderThanMinutes < 0)
|
|
236
445
|
return [];
|