instar 1.3.981 → 1.3.983

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.
@@ -0,0 +1,156 @@
1
+ # Side-Effects Review — a promise was "kept" if a text field was filled in
2
+
3
+ **Version / slug:** `behavioural-promise-unverifiable`
4
+ **Date:** `2026-07-26`
5
+ **Author:** `Echo (instar-dev agent)`
6
+ **Second-pass reviewer:** `author-applied lenses — see Phase 5 (reduced independence, disclosed)`
7
+
8
+ ## Summary of the change
9
+
10
+ `CommitmentTracker.verifyBehavioral` decided whether a behavioural promise had been
11
+ kept by checking `content.includes(commitment.id)` against the behavioural rules file.
12
+ That file is written **by this same class** from its own active list, and — verified
13
+ across the whole agent home — **nothing else reads it**. So the check verified that the
14
+ system had successfully written its own file, and its outcome was decided entirely by
15
+ whether the optional `behavioralRule` field was populated.
16
+
17
+ Measured on the live store: **74 behavioural commitments WITH the field all read
18
+ `verified`; 24 WITHOUT it all read `violated`. 98 of 98, no exceptions in either
19
+ direction.** Not one status observed conduct. `verified` was false comfort; `violated`
20
+ was a false alarm (a missing optional field is not a broken promise). Both accumulated a
21
+ counter tick on every sweep — CMT-068 reached **162,344**, and four commitments created
22
+ in the same minute held **identical** counts (80,425), which individual behaviour cannot
23
+ produce.
24
+
25
+ Behavioural commitments now take the same path this file already gives an unverifiable
26
+ `one-time-action`: the sweep is a **no-op**, the promise stays `pending`, and closure
27
+ stays explicit (`deliver()` / `markDelivered()` / `expiresAt`). The rules file still
28
+ self-heals, but repairing bookkeeping is no longer mistaken for evidence.
29
+
30
+ ## Decision-point inventory
31
+
32
+ | point | classification | note |
33
+ |---|---|---|
34
+ | behavioural early-return in `verifyOne` | `invariant` | Deterministic on `type`. Mirrors the adjacent unverifiable-one-time-action return, which carries the same reasoning and history. |
35
+ | `ensureBehavioralRulesFile` | `invariant` | Pure repair, returns nothing. It cannot influence a status — that conflation *was* the defect. |
36
+ | `getHealth` message | `invariant` | Counts actual `verified` rows instead of inferring verification from the absence of violations. |
37
+
38
+ ## 1. Over-block
39
+
40
+ **What legitimate inputs does this change reject that it shouldn't?**
41
+
42
+ Nothing is blocked; this is a status computation with no gating authority. The nearest
43
+ analogue is **withholding a `verified` status from a promise genuinely being honoured** —
44
+ and that is the point: nothing here can observe conduct, so the status was never earned.
45
+ 74 rows move from `verified` to `pending`, which is a loss of *false* reassurance only.
46
+
47
+ Real cost, stated plainly: an operator who read `verified` as "Echo is complying" loses
48
+ that signal. It was never true, so the honest replacement is silence rather than a
49
+ number — but the change does remove something people may have been relying on.
50
+
51
+ ## 2. Under-block
52
+
53
+ **What failure modes does this still miss?**
54
+
55
+ - **Behavioural compliance is still unverified.** This change makes that visible; it does
56
+ not solve it. Genuine verification would need an observation of conduct (e.g. grading
57
+ outbound turns against the rule), which is a much larger piece of work and is NOT
58
+ attempted here. Recorded rather than silently implied.
59
+ - `config-change` verification is untouched and still genuinely checks live state.
60
+ - The rules file remains written-and-read-by-nobody. Removing it entirely is a separate
61
+ question (its `getBehavioralContext` sibling *is* served over HTTP, so the content has
62
+ a consumer even though the file does not).
63
+ - `writeBehavioralRules`' comment still claims "remove the file so hooks skip injection",
64
+ describing a consumer that does not exist. Left in place rather than edited blind;
65
+ noted as a candidate.
66
+
67
+ ## 3. Level-of-abstraction fit
68
+
69
+ **Is this at the right layer?**
70
+
71
+ Yes, and deliberately at the *same* layer as the existing precedent: an early return in
72
+ `verifyOne`, ten lines below the one that handles unverifiable one-time-actions, sharing
73
+ its rationale. No new status was introduced — `pending` already means "not determined",
74
+ and adding an `unverifiable` state would have required touching the status union, three
75
+ terminal-status guards, `getActive`, and auto-expiry eligibility for no semantic gain.
76
+
77
+ ## 4. Signal vs authority compliance
78
+
79
+ **Required reference:** [docs/signal-vs-authority.md](../../docs/signal-vs-authority.md)
80
+
81
+ **Does this change hold blocking authority with brittle logic?**
82
+
83
+ No. It *removes* an authority that was exercised on brittle grounds: the old path
84
+ asserted a verdict on the operator's promises from a filesystem read. Nothing here
85
+ blocks, delays or alters any message or action. `getHealth` no longer reports `degraded`
86
+ from 24 false violations, which strictly reduces false authority.
87
+
88
+ ## 4b. Judgment-point check (Judgment Within Floors standard)
89
+
90
+ No judgment point is introduced; the new branch is a `type` comparison. The change
91
+ *retires* a pseudo-judgment (file presence standing in for conduct). The asymmetry is
92
+ explicit in code: absence of a verdict is reported as absence, never as either outcome.
93
+
94
+ ## 5. Interactions
95
+
96
+ - **`getHealth`** was the one consumer that read `violated`: 24 false violations made the
97
+ component permanently `degraded`. Now it reports counts it can defend. Verified by grep
98
+ that no code outside this class reads `status === 'violated'`.
99
+ - **`isAutoExpiryEligible`** treats `pending` and `violated` identically, so moving rows
100
+ between them causes no change in expiry behaviour — checked before editing.
101
+ - **PromiseBeacon / overdue surfacing** operate on open commitments; `pending` keeps them
102
+ visible, which is the stated intent of the precedent ("should NAG, not vanish").
103
+ - No persistence change, no migration, no config key. Existing rows re-evaluate to
104
+ `pending` on the next sweep without a data rewrite.
105
+
106
+ ## 6. External surfaces
107
+
108
+ `GET /commitments` and `GET /commitments/:id` will show behavioural promises as `pending`
109
+ rather than `verified`/`violated`, with both counters at 0. `GET /health` reports the
110
+ commitment component's message differently. No route shape changes; no field is removed.
111
+
112
+ ## 6b. Operator-surface quality
113
+
114
+ The operator previously saw either a reassuring `verified` that meant nothing, or a
115
+ `violated` (with a five-figure count) that meant nothing — and, because 24 rows were
116
+ falsely violated, a permanently `degraded` health component. They now see `pending` with
117
+ honest zeroes, and a health line naming how many promises are simply not automatically
118
+ checkable. `pending` is deliberately not alarming: nothing is wrong, nothing is known.
119
+
120
+ ## 7. Multi-machine posture (Cross-Machine Coherence)
121
+
122
+ **Posture: `unified` by construction — no new state.** This changes how an existing
123
+ per-machine store's rows are evaluated; it introduces no new field, file, or surface.
124
+ Each machine already keeps its own commitment store (existing posture, unchanged by this
125
+ work), and the evaluation is a pure function of a row plus that machine's rules file. No
126
+ replication path is required and no `machine-local-justification` marker is needed,
127
+ because no new machine-local state is introduced.
128
+
129
+ ## 8. Rollback cost
130
+
131
+ One commit touching one source file and its test file. No migration, no persisted state,
132
+ no config. Reverting restores the old semantics — and with them 98-of-98 statuses decided
133
+ by a text field, plus the tick accumulation documented above.
134
+
135
+ ## Phase 5 — Second-pass review (independent reviewer subagent)
136
+
137
+ **Disclosure, per Truthful Provenance:** no independent reviewer subagent was spawned — a
138
+ standing instruction in this session prohibits it unless the operator requests it. The
139
+ review lenses were applied by the author. That is **reduced independence**, recorded as
140
+ such rather than presented as a concurring second pass.
141
+
142
+ What author-applied review caught and changed:
143
+
144
+ 1. **The first instinct was to fix only the missing-field half** (stop marking a promise
145
+ violated for an absent optional field). That would have left the *false-comfort* half —
146
+ 74 rows still reading `verified` — which is the more dangerous direction, and would have
147
+ repeated the "fix one site, leave its twin" mistake flagged on PR #1647 earlier tonight.
148
+ Both halves are addressed.
149
+ 2. **`getHealth`'s success message had to change too.** With behavioural rows at `pending`,
150
+ "all verified" would have become a *new* false claim introduced by this very fix — the
151
+ defect migrating one line sideways.
152
+ 3. **The rules-file self-heal was nearly lost.** The old method regenerated the file when
153
+ missing; deleting the method wholesale would have dropped that. Preserved as
154
+ `ensureBehavioralRulesFile`, which deliberately returns nothing.
155
+ 4. **Consumers were checked, not assumed** — establishing that `getHealth` was the only
156
+ reader of `violated` and that auto-expiry treats `pending`/`violated` alike.
@@ -0,0 +1,230 @@
1
+ # Side-Effects Review — a merged record that could not name what merged it
2
+
3
+ **Version / slug:** `merged-record-carries-its-evidence`
4
+ **Date:** `2026-07-26`
5
+ **Author:** `Echo (instar-dev agent)`
6
+ **Second-pass reviewer:** `author-applied lenses — see Phase 5 (reduced independence, disclosed)`
7
+
8
+ ## Summary of the change
9
+
10
+ `/projects/:id/advance` validated a submitted artifact for `building → merged` — PR
11
+ state `MERGED`, a format-valid merge sha, that sha reachable from canonical main, CI
12
+ rollup green — and then wrote **only** `pipelineStage`. The validated evidence built
13
+ the validation context and was discarded. Measured on the live store: both Tier-1
14
+ children of `convergence-towards-coherence` read `pipelineStage: "merged"` with no
15
+ `prNumber`, no `mergeCommitOid`, no `ciCheckedAt`.
16
+
17
+ The root cause was a **type**. `StageTransitionResult`'s success case was `{ ok: true }`
18
+ — one bit — while every refusal carried `reason` + `code`. The validator explained
19
+ itself when it refused and said nothing when it approved, so the caller had nothing to
20
+ persist. The fix widens the success case to carry a `MergedEvidence` record and
21
+ persists it in the same `update()` as the stage.
22
+
23
+ **The consequential half:** two merged-state reconcilers select on `mergeCommitOid` —
24
+ `GET /projects/:id`'s lazy reconciler (documented "may mutate", runs on every read) and
25
+ `verifyMergedItemsViaGit`. With the field never written, the candidate set was always
26
+ empty, `verifyMergedItemsViaGit` was never called, and both reported nothing. A
27
+ regression detector that scans nothing is indistinguishable from one that finds no
28
+ regressions.
29
+
30
+ Because that path had never executed, three defects sat in it unexercised — all three
31
+ already fixed in the advance path a few hundred lines away: (1) `SafeGitExecutor.run`
32
+ without `sourceTreeReadOk`, so SourceTreeGuard refuses the read against an instar
33
+ source tree (the #1641 defect); (2) a hardcoded `origin/main`, which on a dev-agent
34
+ home is the agent's FORK; (3) `catch {}` → not verified → caller marks `regressed`,
35
+ i.e. a refusal rendered as "it was reverted". Writing the evidence is what ARMS that
36
+ path, so the two halves ship together.
37
+
38
+ ## Decision-point inventory
39
+
40
+ | point | classification | note |
41
+ |---|---|---|
42
+ | success-case evidence on `StageTransitionResult` | `invariant` | Pure data carriage. Adds no branch and cannot change a verdict — the same four checks decide `ok`. |
43
+ | evidence persistence in `/advance` | `invariant` | Conditional on `result.evidence` presence only. Same single write as the stage, so no window exists where `merged` has no evidence. |
44
+ | `verifyMergedItemsViaGit` three-state outcome | `invariant` | Deterministic on git's documented exit status: 1 ⇒ regressed, everything else ⇒ unverifiable. No judgment. |
45
+ | reconciler unverifiable branch | `invariant` | Deterministic on set membership. Strictly *removes* an authority — the old code demoted on absence-from-`verified`. |
46
+
47
+ ## 1. Over-block
48
+
49
+ **What legitimate inputs does this change reject that it shouldn't?**
50
+
51
+ Nothing new is rejected. `/advance` accepts exactly what it accepted before — the four
52
+ checks and their refusal codes are untouched; only the success return widened. No
53
+ artifact field became required.
54
+
55
+ The reconciler now demotes **strictly fewer** items: previously anything absent from
56
+ `verified` became `regressed`; now only git exit 1 does. So the change cannot introduce
57
+ a false demotion, only remove them.
58
+
59
+ Real cost, stated plainly: an item that genuinely regressed but whose check cannot be
60
+ run stays at `merged` rather than being demoted. That is a deliberate trade — the old
61
+ behaviour "caught" it only by demoting everything it could not verify, which is not
62
+ detection. The item keeps taking the `ciCheckedAt` backoff, so it is re-asked, and its
63
+ unverifiable reason is returned to the caller rather than swallowed.
64
+
65
+ ## 2. Under-block
66
+
67
+ **What failure modes does this still miss?**
68
+
69
+ - **No new regression detection is added.** This makes the existing reconcilers
70
+ *possible* (they now have evidence to select on) and *safe* (they can no longer guess).
71
+ Whether they catch a real revert is unproven until one occurs — recorded rather than
72
+ implied.
73
+ - **Items merged BEFORE this change carry no evidence**, so they remain invisible to the
74
+ reconcilers. They surface as `unverifiable` with the reason "no mergeCommitOid
75
+ recorded on the item" rather than silently dropping out of the candidate set. No
76
+ backfill is attempted: reconstructing which PR merged a historical item would mean
77
+ guessing, and a fabricated evidence row is worse than an absent one.
78
+ - The `GET /projects/:id` reconciler still selects only `pipelineStage === 'building'`
79
+ children, so an item that went straight to `merged` via `/advance` is not revalidated
80
+ by it. `verifyMergedItemsViaGit` is reachable for any child id the caller passes.
81
+ Widening that selection is a behavioural change to which items get demoted and is NOT
82
+ attempted here.
83
+ - `defaultVerifyMergedItems` remains a deliberate no-op stub for the injected
84
+ `verifyMergedItems` seam (returns an empty Set). Its own comment says production
85
+ callers should pass a real verifier; the one production caller does.
86
+
87
+ ## 3. Level-of-abstraction fit
88
+
89
+ **Is this at the right layer?**
90
+
91
+ Yes. The evidence originates in the validator (the only place that proves it), is
92
+ carried on the validator's own result type, and is persisted by the route that owns the
93
+ write. No new store, no new field on the wire, no new endpoint — `prNumber`,
94
+ `mergeCommitOid` and `ciCheckedAt` are pre-existing declared `Initiative` fields, and
95
+ `mergeCommitOid`'s own comment already said "recorded at building → merged". The fix
96
+ makes the comment true rather than inventing a mechanism.
97
+
98
+ ## 4. Signal vs authority compliance
99
+
100
+ **Required reference:** [docs/signal-vs-authority.md](../../docs/signal-vs-authority.md)
101
+
102
+ **Does this change hold blocking authority with brittle logic?**
103
+
104
+ It **reduces** brittle authority. The reconciler holds real authority (it demotes items
105
+ to `regressed` and clears a round's future `autoAdvanceAt`), and it was exercising that
106
+ authority on the brittle ground of "absent from a set that could be empty for any
107
+ reason, including a permission refusal". Demotion now requires git's single documented
108
+ negative exit status. Nothing in this change blocks, delays, or alters a message or an
109
+ action.
110
+
111
+ ## 4b. Judgment-point check (Judgment Within Floors standard)
112
+
113
+ No judgment point is introduced. Every new branch is a deterministic comparison (a
114
+ `status === 1` test, a set-membership test, a presence test on `result.evidence`). The
115
+ change *retires* a pseudo-judgment — treating an unanswerable question as a negative
116
+ answer — and makes the asymmetry explicit in the type: `unverifiable` is a first-class
117
+ outcome carrying its reason, never folded into either verdict.
118
+
119
+ ## 5. Interactions
120
+
121
+ - **`GET /projects/:id` lazy reconciler** — the one production consumer. Now receives a
122
+ three-state result and a resolved canonical-main ref. An unverifiable candidate takes
123
+ the `ciCheckedAt` backoff and keeps its stage; only `regressed` membership triggers a
124
+ demotion and the round's `autoAdvanceAt` clearing (that downstream logic is unchanged).
125
+ - **`verifyMergedItemsViaGit` signature** — return type widened
126
+ (`Set<string>` → `MergedVerificationResult`) and a fourth optional parameter added.
127
+ Grepped all callers: exactly one production callsite (`routes.ts`), updated. The
128
+ injected `verifyMergedItems` seam on `RunRoundInput` is a DIFFERENT type
129
+ (`Set<string>`) and is untouched, so `ProjectRoundExecution`'s own callers and its 8
130
+ unit tests are unaffected — verified by running them.
131
+ - **`StageTransitionResult`** — the widened success case is an optional property, so
132
+ every existing `if (!result.ok)` caller and every `expect(r.ok).toBe(true)` assertion
133
+ keeps working. Confirmed: 48 pre-existing validator tests pass unchanged.
134
+ - No persistence migration: the three fields already exist in the `Initiative` type,
135
+ the create/update allowlists, and the JSON store.
136
+
137
+ ## 6. External surfaces
138
+
139
+ `POST /projects/:id/advance`'s 200 body gains `item.prNumber`, `item.mergeCommitOid`,
140
+ `item.ciCheckedAt` and an `evidence` object on a merged transition. Additive only — no
141
+ field removed, no shape changed, no status code changed. `GET /projects/:id` children
142
+ now carry those fields once written. Refusal responses are byte-identical.
143
+
144
+ ## 6b. Operator-surface quality
145
+
146
+ The operator previously read `merged` with no way to ask "merged by what?" — and got a
147
+ silent all-clear from a regression detector that had never examined anything. They now
148
+ see the PR, the merge commit and the check time on the record itself, and an
149
+ unverifiable item names its reason instead of being demoted. Items merged before this
150
+ change honestly report "no mergeCommitOid recorded" rather than appearing clean.
151
+
152
+ ## 7. Multi-machine posture (Cross-Machine Coherence)
153
+
154
+ **Posture: `unified` by construction — no new state.** No new field, file, store, or
155
+ surface is introduced; three already-declared fields on an existing per-machine store
156
+ start being written. The evidence is a pure function of a PR's GitHub state plus the
157
+ target repo's git history, both of which are machine-independent facts. The
158
+ canonical-main ref is resolved per-machine from that machine's remotes, which is
159
+ correct: it names the same upstream commit graph whatever the local remote is called —
160
+ and resolving it (rather than assuming `origin/main`) is precisely what makes the check
161
+ give the same answer on a fork-origin home as on a canonical one. No replication path
162
+ is required and no `machine-local-justification` marker is needed.
163
+
164
+ ## 8. Rollback cost
165
+
166
+ Three source files, one commit, no migration and no persisted-state change. Reverting
167
+ restores a `merged` record that cannot name its evidence and two reconcilers that
168
+ select nothing — and re-arms the "could not check ⇒ regressed" mistranslation the
169
+ moment anything writes `mergeCommitOid`. Written-but-unread evidence fields are inert,
170
+ so a revert leaves no corrupt data behind.
171
+
172
+ ## Phase 5 — Second-pass review (independent reviewer subagent)
173
+
174
+ **Disclosure, per Truthful Provenance:** no independent reviewer subagent was spawned —
175
+ a standing instruction in this session prohibits it unless the operator requests it.
176
+ The review lenses were applied by the author. That is **reduced independence**, recorded
177
+ as such rather than presented as a concurring second pass.
178
+
179
+ What author-applied review caught and changed:
180
+
181
+ 1. **The first instinct was to persist the evidence and stop** — a two-line change that
182
+ would have armed a code path carrying three unexercised defects, turning silent
183
+ blindness into confident false regressions. The consumer had to be read before the
184
+ producer was fixed. This is the same "fix one site, leave its twin" mistake flagged
185
+ on #1647 and again on #1650 earlier the same night; third occurrence, caught before
186
+ shipping this time.
187
+ 2. **The hardcoded `origin/main` would have mis-fired specifically on this machine.**
188
+ The advance path had already resolved it via `resolveCanonicalMainRef` for exactly
189
+ this reason. Passing the resolved ref through was not optional — without it a
190
+ dev-agent home demotes every healthy item.
191
+ 3. **The unverifiable branch initially bumped nothing**, which would have re-asked an
192
+ unanswerable question on every read (a hot loop). It now takes the `ciCheckedAt`
193
+ backoff while explicitly leaving `pipelineStage` alone.
194
+ 4. **Consumers were grepped, not assumed** — establishing that the widened return type
195
+ has exactly one production caller and that the similarly-named injected
196
+ `verifyMergedItems` seam is a distinct type that must NOT change.
197
+ 5. **One new test passed for the wrong reason and was fixed:** a fixed-size text window
198
+ ran past the branch it meant to inspect and matched the next block's
199
+ `pipelineStage`. Now bounded at the branch's own `continue`. A test measuring
200
+ adjacent text is the same defect class as the subject of this change.
201
+
202
+ ## Phase 6 — What CI refused, and why the fix went in the prose
203
+
204
+ The first push was refused by the empty-catch ratchet
205
+ (`tests/unit/no-empty-catch-blocks.test.ts`): count 1 against a baseline of 0. The
206
+ offending occurrence was at `ProjectRoundExecution.ts:506` — **inside this change's own
207
+ doc comment**, on the line quoting the forbidden bodyless-catch shape while explaining
208
+ that it had been removed. The lint scans `src/` as text and does not strip comments, so
209
+ it cannot distinguish an example from an instance.
210
+
211
+ Noted rather than glossed, because it is the same failure mode this change is about,
212
+ one layer out: a checker built to catch "errors turning into nothing" was tripped by a
213
+ sentence *about* errors turning into nothing. It is also the third occurrence of that
214
+ shape in a single session (twice in the outbound-message checker, once here), and the
215
+ sibling test `tests/unit/projects-advance-mergebase-wiring.test.ts` already documents
216
+ the identical trap for its own assertions — it strips comments before asserting,
217
+ precisely because an earlier version of it matched the helper's own explanatory prose.
218
+
219
+ **The prose was reworded; the lint was NOT touched.** Weakening a safety check to
220
+ unblock one's own change is the wrong instinct even when the check is imprecise, and
221
+ this lint's stated history (a bare catch in a 5-second loop causing a real cost
222
+ incident) earns it the benefit of the doubt. Making the lint comment-aware is a
223
+ legitimate improvement, but it belongs in its own change with its own review — not
224
+ smuggled in as a side-effect of needing this PR to go green.
225
+
226
+ **Re-verification scope, corrected.** The refusal proved my local scope was wrong: I
227
+ had run the tests covering the changed module, but this failure came from a test that
228
+ scans the whole source tree and whose subject my change never touched. The right scope
229
+ for a source change is therefore *every tree-scanning test*, located by grep rather
230
+ than guessed: 99 files, 8,370 tests, exit 0, `tsc --noEmit` clean.