@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.
Files changed (164) hide show
  1. package/.dz-manifest.json +253 -133
  2. package/README.md +209 -0
  3. package/dist/agentdb-index.d.ts +3 -3
  4. package/dist/agentdb-index.js +5 -5
  5. package/dist/agentdb-index.js.map +1 -1
  6. package/dist/amendment-trace.d.ts.map +1 -1
  7. package/dist/amendment-trace.js +4 -1
  8. package/dist/amendment-trace.js.map +1 -1
  9. package/dist/backlog.d.ts +2 -1
  10. package/dist/backlog.d.ts.map +1 -1
  11. package/dist/backlog.js +3 -2
  12. package/dist/backlog.js.map +1 -1
  13. package/dist/brain.d.ts.map +1 -1
  14. package/dist/brain.js +9 -2
  15. package/dist/brain.js.map +1 -1
  16. package/dist/bto-optimize.d.ts.map +1 -1
  17. package/dist/bto-optimize.js +9 -12
  18. package/dist/bto-optimize.js.map +1 -1
  19. package/dist/claim-check.d.ts.map +1 -1
  20. package/dist/claim-check.js +50 -14
  21. package/dist/claim-check.js.map +1 -1
  22. package/dist/cross-family-control.d.ts +35 -0
  23. package/dist/cross-family-control.d.ts.map +1 -1
  24. package/dist/cross-family-control.js +49 -3
  25. package/dist/cross-family-control.js.map +1 -1
  26. package/dist/experiment-assign.d.ts +110 -0
  27. package/dist/experiment-assign.d.ts.map +1 -0
  28. package/dist/experiment-assign.js +229 -0
  29. package/dist/experiment-assign.js.map +1 -0
  30. package/dist/feature-adr-checkpoints.d.ts +7 -2
  31. package/dist/feature-adr-checkpoints.d.ts.map +1 -1
  32. package/dist/feature-adr-checkpoints.js +20 -5
  33. package/dist/feature-adr-checkpoints.js.map +1 -1
  34. package/dist/feature-adr-envelope.d.ts +12 -0
  35. package/dist/feature-adr-envelope.d.ts.map +1 -1
  36. package/dist/feature-adr-envelope.js +12 -1
  37. package/dist/feature-adr-envelope.js.map +1 -1
  38. package/dist/feature-adr-routing.d.ts +4 -0
  39. package/dist/feature-adr-routing.d.ts.map +1 -1
  40. package/dist/feature-adr-routing.js +15 -1
  41. package/dist/feature-adr-routing.js.map +1 -1
  42. package/dist/index.d.ts +12 -5
  43. package/dist/index.d.ts.map +1 -1
  44. package/dist/index.js +11 -4
  45. package/dist/index.js.map +1 -1
  46. package/dist/ledger-cost-fill.d.ts +58 -0
  47. package/dist/ledger-cost-fill.d.ts.map +1 -0
  48. package/dist/ledger-cost-fill.js +78 -0
  49. package/dist/ledger-cost-fill.js.map +1 -0
  50. package/dist/loop-blobs.generated.js +2 -2
  51. package/dist/loop-blobs.generated.js.map +1 -1
  52. package/dist/mutation-gate.d.ts +29 -0
  53. package/dist/mutation-gate.d.ts.map +1 -1
  54. package/dist/mutation-gate.js +20 -0
  55. package/dist/mutation-gate.js.map +1 -1
  56. package/dist/name-check.d.ts +20 -1
  57. package/dist/name-check.d.ts.map +1 -1
  58. package/dist/name-check.js +42 -1
  59. package/dist/name-check.js.map +1 -1
  60. package/dist/no-stubs.d.ts +10 -0
  61. package/dist/no-stubs.d.ts.map +1 -1
  62. package/dist/no-stubs.js +13 -9
  63. package/dist/no-stubs.js.map +1 -1
  64. package/dist/patterns.d.ts +24 -0
  65. package/dist/patterns.d.ts.map +1 -1
  66. package/dist/patterns.js +49 -9
  67. package/dist/patterns.js.map +1 -1
  68. package/dist/publish-source-scope.d.ts +32 -0
  69. package/dist/publish-source-scope.d.ts.map +1 -0
  70. package/dist/publish-source-scope.js +53 -0
  71. package/dist/publish-source-scope.js.map +1 -0
  72. package/dist/publish.d.ts +5 -0
  73. package/dist/publish.d.ts.map +1 -1
  74. package/dist/publish.js +32 -21
  75. package/dist/publish.js.map +1 -1
  76. package/dist/qe-bridge.d.ts +12 -1
  77. package/dist/qe-bridge.d.ts.map +1 -1
  78. package/dist/qe-bridge.js +21 -11
  79. package/dist/qe-bridge.js.map +1 -1
  80. package/dist/rake-analyzer.d.ts +10 -3
  81. package/dist/rake-analyzer.d.ts.map +1 -1
  82. package/dist/rake-analyzer.js +84 -22
  83. package/dist/rake-analyzer.js.map +1 -1
  84. package/dist/recap.d.ts.map +1 -1
  85. package/dist/recap.js +8 -4
  86. package/dist/recap.js.map +1 -1
  87. package/dist/release.d.ts +2 -1
  88. package/dist/release.d.ts.map +1 -1
  89. package/dist/release.js +18 -4
  90. package/dist/release.js.map +1 -1
  91. package/dist/reqe.d.ts.map +1 -1
  92. package/dist/reqe.js +11 -1
  93. package/dist/reqe.js.map +1 -1
  94. package/dist/review-cost.d.ts +51 -0
  95. package/dist/review-cost.d.ts.map +1 -0
  96. package/dist/review-cost.js +110 -0
  97. package/dist/review-cost.js.map +1 -0
  98. package/dist/round.d.ts +136 -3
  99. package/dist/round.d.ts.map +1 -1
  100. package/dist/round.js +215 -6
  101. package/dist/round.js.map +1 -1
  102. package/dist/run-records.d.ts +51 -0
  103. package/dist/run-records.d.ts.map +1 -1
  104. package/dist/run-records.js +148 -2
  105. package/dist/run-records.js.map +1 -1
  106. package/dist/score.d.ts.map +1 -1
  107. package/dist/score.js +8 -1
  108. package/dist/score.js.map +1 -1
  109. package/dist/sign.d.ts +23 -0
  110. package/dist/sign.d.ts.map +1 -1
  111. package/dist/sign.js +52 -0
  112. package/dist/sign.js.map +1 -1
  113. package/dist/skills-verify.d.ts +8 -5
  114. package/dist/skills-verify.d.ts.map +1 -1
  115. package/dist/skills-verify.js +59 -7
  116. package/dist/skills-verify.js.map +1 -1
  117. package/dist/store-guard-prune.d.ts +22 -0
  118. package/dist/store-guard-prune.d.ts.map +1 -0
  119. package/dist/store-guard-prune.js +54 -0
  120. package/dist/store-guard-prune.js.map +1 -0
  121. package/dist/sweep-failure-classify.d.ts +45 -0
  122. package/dist/sweep-failure-classify.d.ts.map +1 -0
  123. package/dist/sweep-failure-classify.js +90 -0
  124. package/dist/sweep-failure-classify.js.map +1 -0
  125. package/dist/workflow-run-dispatch.d.ts +14 -2
  126. package/dist/workflow-run-dispatch.d.ts.map +1 -1
  127. package/dist/workflow-run-dispatch.js +14 -2
  128. package/dist/workflow-run-dispatch.js.map +1 -1
  129. package/package.json +1 -1
  130. package/sbom.json +432 -132
  131. package/src/agentdb-index.ts +5 -5
  132. package/src/amendment-trace.ts +4 -1
  133. package/src/backlog.ts +3 -2
  134. package/src/brain.ts +8 -2
  135. package/src/bto-optimize.ts +10 -8
  136. package/src/claim-check.ts +48 -12
  137. package/src/cross-family-control.ts +81 -3
  138. package/src/experiment-assign.ts +276 -0
  139. package/src/feature-adr-checkpoints.ts +20 -5
  140. package/src/feature-adr-envelope.ts +24 -1
  141. package/src/feature-adr-routing.ts +15 -1
  142. package/src/index.ts +23 -4
  143. package/src/loop-blobs.generated.ts +2 -2
  144. package/src/mutation-gate.ts +59 -0
  145. package/src/name-check.ts +43 -1
  146. package/src/no-stubs.ts +14 -5
  147. package/src/patterns.ts +49 -8
  148. package/src/publish-source-scope.ts +53 -0
  149. package/src/publish.ts +38 -16
  150. package/src/qe-bridge.ts +30 -11
  151. package/src/rake-analyzer.ts +75 -20
  152. package/src/rake-signatures.json +98 -0
  153. package/src/recap.ts +8 -4
  154. package/src/release.ts +18 -4
  155. package/src/reqe.ts +11 -1
  156. package/src/review-cost.ts +139 -0
  157. package/src/round.ts +327 -11
  158. package/src/run-records.ts +175 -1
  159. package/src/score.ts +8 -1
  160. package/src/sign.ts +53 -0
  161. package/src/skills-verify.ts +69 -9
  162. package/src/store-guard-prune.ts +81 -0
  163. package/src/sweep-failure-classify.ts +88 -0
  164. 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
- readonly reviewSource?: 'qe-bridge';
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;
@@ -1 +1 @@
1
- {"version":3,"file":"round.d.ts","sourceRoot":"","sources":["../src/round.ts"],"names":[],"mappings":"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"}
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: an explicit --reviewer always wins; the sidecar fills the
135
- // gap only, and only its OWN two derived fields travel with it (a flag-supplied reviewer never
136
- // carries a sidecar-sourced `reviewMinutes`/`reviewSource` — that would misattribute where the
137
- // minutes figure came from).
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
- if (reviewer === null && input.reviewSidecar !== undefined && nonEmpty(input.reviewSidecar.gradedBy)) {
142
- reviewer = input.reviewSidecar.gradedBy.trim();
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 [];