instar 1.3.992 → 1.3.994
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/dist/core/ProjectRoundExecution.d.ts +48 -6
- package/dist/core/ProjectRoundExecution.d.ts.map +1 -1
- package/dist/core/ProjectRoundExecution.js +134 -30
- package/dist/core/ProjectRoundExecution.js.map +1 -1
- package/dist/core/RecurrenceActuator.d.ts +79 -0
- package/dist/core/RecurrenceActuator.d.ts.map +1 -0
- package/dist/core/RecurrenceActuator.js +116 -0
- package/dist/core/RecurrenceActuator.js.map +1 -0
- package/dist/core/recurrenceLoop.d.ts +66 -0
- package/dist/core/recurrenceLoop.d.ts.map +1 -0
- package/dist/core/recurrenceLoop.js +76 -0
- package/dist/core/recurrenceLoop.js.map +1 -0
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/routes.js +1 -51
- package/dist/server/routes.js.map +1 -1
- package/package.json +1 -1
- package/src/data/builtin-manifest.json +46 -46
- package/upgrades/1.3.993.md +73 -0
- package/upgrades/1.3.994.md +69 -0
- package/upgrades/side-effects/recurrence-actuator.md +130 -0
- package/upgrades/side-effects/recurrence-loop.md +112 -0
- package/upgrades/side-effects/round-completion-verifies-merged.md +137 -0
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
# Side-Effects Review — RecurrenceActuator (Tier 2 item 4, plan-only)
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `recurrence-actuator`
|
|
4
|
+
**Date:** `2026-07-27`
|
|
5
|
+
**Author:** `Echo (instar-dev agent)`
|
|
6
|
+
|
|
7
|
+
## Summary
|
|
8
|
+
|
|
9
|
+
`RecurrenceReader` makes recurrence visible. Visibility was never the goal — the project's diagnosis
|
|
10
|
+
is that instar notices constantly and closes almost nothing (≈30:1). A reader nobody acts on is that
|
|
11
|
+
ratio with better typography.
|
|
12
|
+
|
|
13
|
+
Operator directive 2026-07-26 20:08Z: *"the synthesis itself must lead to ACTION and a fully closed
|
|
14
|
+
loop"*, minimal user dependence.
|
|
15
|
+
|
|
16
|
+
`planActuation()` returns a PLAN: for clusters that genuinely recur AND are untracked, propose work on
|
|
17
|
+
the EXISTING evolution action queue. Pure — the caller performs the write, so the write path and its
|
|
18
|
+
gating stay exactly where they already are.
|
|
19
|
+
|
|
20
|
+
**Live dry-run, 2026-07-27:** 836 clusters considered → **3 proposed, 17 deferred by cap**
|
|
21
|
+
(278x idle-timeout, 238x escalation-suppressed, 177x credential rebalancer — all `high`).
|
|
22
|
+
|
|
23
|
+
## Refusal evidence (constraint 2)
|
|
24
|
+
|
|
25
|
+
```
|
|
26
|
+
REFUSAL 1 — actions store unreadable ⇒ propose NOTHING
|
|
27
|
+
refused.reason : actions-store-unreadable
|
|
28
|
+
detail : "…so 'has anyone already committed to this?' is unanswerable for every
|
|
29
|
+
cluster. Proposing work now would duplicate whatever is already tracked."
|
|
30
|
+
propose : [] consideredClusters: 1 ← still honest about scope
|
|
31
|
+
|
|
32
|
+
DELIBERATELY NOT SYMMETRIC — attention/sentinel unreadable ⇒ it DOES act
|
|
33
|
+
Those only UNDERSTATE counts, so a cluster still clearing the bar genuinely clears it.
|
|
34
|
+
Treating all partial reads alike would be lazy symmetry that blocks safe action.
|
|
35
|
+
|
|
36
|
+
REFUSAL 2 — per-run cap: 10 qualifying clusters ⇒ propose 3, deferredByCap 7
|
|
37
|
+
REFUSAL 3 — below threshold: seen 3x with minCount 10 ⇒ no-qualifying-clusters
|
|
38
|
+
REFUSAL 4 — already tracked: a member from the action queue ⇒ propose nothing
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
Tests **10 passed (10)**; combined with the reader **21 passed (21)**; `tsc --noEmit` exit 0.
|
|
42
|
+
|
|
43
|
+
## A test caught the documented over-merge risk, live
|
|
44
|
+
|
|
45
|
+
My first cap test built clusters titled `problem 0`…`problem 9` and asserted 3 proposals. It got 1:
|
|
46
|
+
the recurrence key normalizes digits to `N`, so all ten collapsed to one cluster. **The fixture was
|
|
47
|
+
naive, not the code** — and it is a live demonstration of the over-merge trade the reader's
|
|
48
|
+
side-effects review names as its weakest point. Fixed with word-titles and the reason recorded in
|
|
49
|
+
the test.
|
|
50
|
+
|
|
51
|
+
## Decision-point inventory
|
|
52
|
+
|
|
53
|
+
| point | classification |
|
|
54
|
+
|---|---|
|
|
55
|
+
| actions-unreadable ⇒ refuse | `invariant` — the load-bearing rule |
|
|
56
|
+
| untracked + minCount filter | `invariant` — deterministic thresholds |
|
|
57
|
+
| per-run cap, densest-first | `invariant` |
|
|
58
|
+
| priority from volume | `invariant` — fixed bands, no model |
|
|
59
|
+
| `externalKey` from cluster key | `invariant` — stable, idempotent |
|
|
60
|
+
|
|
61
|
+
No judgment points, no LLM.
|
|
62
|
+
|
|
63
|
+
## 1. Over-block
|
|
64
|
+
|
|
65
|
+
Refuses entirely when the actions store is unreadable — deliberately, since acting blind creates
|
|
66
|
+
duplicates. Cost: on a broken actions store, nothing is proposed until it is fixed. Correct trade.
|
|
67
|
+
|
|
68
|
+
The `minCount` default of 10 will skip genuine problems seen 4–9 times. Accepted: a work item per
|
|
69
|
+
seen-twice observation is how the queue got to 371 open in the first place.
|
|
70
|
+
|
|
71
|
+
## 2. Under-block
|
|
72
|
+
|
|
73
|
+
**Nothing prevents the CALLER from ignoring the plan or writing it badly.** This module returns data;
|
|
74
|
+
the write is the caller's. That is the right seam (the write path keeps its own gating) but it means
|
|
75
|
+
"the loop closes" is only true once a caller is wired. No caller ships here — that is the next step
|
|
76
|
+
and is not claimed.
|
|
77
|
+
|
|
78
|
+
**Cancelled actions are not re-proposed-proof.** If a human cancels a proposed action, the cluster
|
|
79
|
+
remains untracked, so a later run could propose it again. The `externalKey` makes it the same row
|
|
80
|
+
rather than a new one, but a "dismissed, stop asking" state belongs to the action store, not here.
|
|
81
|
+
Named as a real gap. <!-- tracked: ACT-1311 -->
|
|
82
|
+
|
|
83
|
+
## 3. Level-of-abstraction fit
|
|
84
|
+
|
|
85
|
+
Plan-only, pure over the report. It cannot flood, cannot notify, cannot write. The one thing it
|
|
86
|
+
must never become — a fourth place that notices things — is structurally impossible: it has no
|
|
87
|
+
output channel.
|
|
88
|
+
|
|
89
|
+
## 4. Signal vs authority compliance
|
|
90
|
+
|
|
91
|
+
It proposes; it holds no authority. Creating a tracked action QUEUES work for a human or agent to
|
|
92
|
+
judge — it does not close, prioritise beyond a fixed volume rule, escalate, or act.
|
|
93
|
+
|
|
94
|
+
## 5. Interactions
|
|
95
|
+
|
|
96
|
+
Consumes `RecurrenceReport` only. No writes, no schema change, no existing caller. Depends on
|
|
97
|
+
`RecurrenceReader` (same branch, PR #1662) — this is stacked on it.
|
|
98
|
+
|
|
99
|
+
## 6. External surfaces
|
|
100
|
+
|
|
101
|
+
**None.** No route, no config, no persisted state, no user-visible behaviour in this increment.
|
|
102
|
+
|
|
103
|
+
## 7. Multi-machine posture
|
|
104
|
+
|
|
105
|
+
**Posture: `machine-local`.** `machine-local-justification: physical-credential-locality` — it plans
|
|
106
|
+
over one machine's stores, whose observation titles carry that machine's ids, topics and account
|
|
107
|
+
emails. Cross-machine synthesis would mean replicating those records; the correct route is the
|
|
108
|
+
existing pool-scope fan-out, serving each machine's data from that machine.
|
|
109
|
+
|
|
110
|
+
## 8. Rollback cost
|
|
111
|
+
|
|
112
|
+
**Zero.** One module, one test file, no callers. Delete removes the feature.
|
|
113
|
+
|
|
114
|
+
## Phase 5 — Second-pass review
|
|
115
|
+
|
|
116
|
+
No gate/sentinel/watchdog, no block/allow authority, no session lifecycle, no LLM. Author lenses:
|
|
117
|
+
|
|
118
|
+
**Adversarial — "how would I make this useless?"** Let it propose blind when the actions store is
|
|
119
|
+
down (duplicates existing work), or let it propose for everything at once (new backlog). Both are
|
|
120
|
+
asserted refusals.
|
|
121
|
+
|
|
122
|
+
**"Would it have caught the incident?"** The incident is 69 untracked recurrers. It selects exactly
|
|
123
|
+
that class and would open the top three today.
|
|
124
|
+
|
|
125
|
+
**"Symptom or cause?"** Cause for the never-gets-picked-up half. NOT for the recurrence itself —
|
|
126
|
+
proposing work does not fix the 278x idle-timeout; it makes someone decide about it. Claiming
|
|
127
|
+
otherwise would be filing-as-progress.
|
|
128
|
+
|
|
129
|
+
**Weakest point:** no caller is wired, so "the loop closes" is a designed property, not yet a
|
|
130
|
+
demonstrated one. The dry-run shows what it WOULD propose; nothing has been written.
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# Side-Effects Review — recurrenceLoop (the caller that closes the loop)
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `recurrence-loop` · **Date:** `2026-07-27` · **Author:** `Echo (instar-dev agent)`
|
|
4
|
+
|
|
5
|
+
## Summary
|
|
6
|
+
|
|
7
|
+
`RecurrenceReader` sees; `RecurrenceActuator` decides. Both are pure, which left **"the loop closes"
|
|
8
|
+
a DESIGNED property, not a demonstrated one** — the weakest point named in the actuator's own review.
|
|
9
|
+
This closes it: read three stores → group → plan → create, via a caller-supplied `createAction`.
|
|
10
|
+
|
|
11
|
+
It is deliberately the ONLY I/O in the feature, so every read failure has exactly one reporting site
|
|
12
|
+
and cannot be swallowed mid-pipeline.
|
|
13
|
+
|
|
14
|
+
**Live run, writes intercepted, 2026-07-27:** coverage `complete` (attention, actions, sentinel);
|
|
15
|
+
836 problems; **would create 3, deferred 17, 0 write failures, no refusal.**
|
|
16
|
+
|
|
17
|
+
## Refusal evidence (constraint 2)
|
|
18
|
+
|
|
19
|
+
```
|
|
20
|
+
REFUSAL — action store unreadable ⇒ createAction NEVER CALLED
|
|
21
|
+
create spy : not called created: 0
|
|
22
|
+
refused.reason : actions-store-unreadable writeFailures: 0
|
|
23
|
+
|
|
24
|
+
A FAILED WRITE IS NOT A REFUSAL (the distinction this module exists to protect)
|
|
25
|
+
createAction throws '503 action store unavailable'
|
|
26
|
+
refused : undefined ← an outage must NOT present as judgement
|
|
27
|
+
writeFailures : [{ error: '503 …' }]
|
|
28
|
+
created : 0
|
|
29
|
+
|
|
30
|
+
one failure among three ⇒ created 2, writeFailures 1 (others not abandoned)
|
|
31
|
+
|
|
32
|
+
UNREADABLE STORE IS DATA, NOT AN EXCEPTION
|
|
33
|
+
sentinel throws ⇒ coverage.unreadable names it, completeness 'partial',
|
|
34
|
+
and it STILL creates 1 (sentinel only understates counts)
|
|
35
|
+
all three throw ⇒ verdict undefined, created 0, no crash
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
Tests **9 passed (9)**; whole feature **30 passed (30)**; `tsc --noEmit` exit 0.
|
|
39
|
+
|
|
40
|
+
## Decision-point inventory
|
|
41
|
+
|
|
42
|
+
| point | classification |
|
|
43
|
+
|---|---|
|
|
44
|
+
| read failure → coverage gap | `invariant` — try/catch → named entry, never a throw |
|
|
45
|
+
| refusal vs write-failure separation | `invariant` — distinct fields, the load-bearing rule |
|
|
46
|
+
| write loop continues past a failure | `invariant` |
|
|
47
|
+
|
|
48
|
+
No judgment points, no LLM, no authority: `createAction` is supplied by the caller, so the write path
|
|
49
|
+
keeps whatever gating it already has. This module never constructs an HTTP call.
|
|
50
|
+
|
|
51
|
+
## 1. Over-block
|
|
52
|
+
|
|
53
|
+
Refuses to write when the actions store is unreadable — inherited from the actuator and correct: it
|
|
54
|
+
cannot tell what is already owned. Cost: a broken actions store stops proposals until fixed.
|
|
55
|
+
|
|
56
|
+
## 2. Under-block
|
|
57
|
+
|
|
58
|
+
**No scheduler/route calls it.** Running it is deliberate. So this demonstrates the loop CAN close,
|
|
59
|
+
not that it closes *unattended*. Wiring a cadence is a separate increment with its own risk, and is
|
|
60
|
+
not claimed here.
|
|
61
|
+
|
|
62
|
+
**No dismissal memory.** A cancelled action leaves the cluster untracked, so a later run can propose
|
|
63
|
+
it again (same `externalKey`, so one row, not many). A "dismissed, stop asking" state belongs to the
|
|
64
|
+
action store. <!-- tracked: ACT-1311 -->
|
|
65
|
+
|
|
66
|
+
**`createAction` failures are reported, not retried.** Deliberate: retry policy belongs to the write
|
|
67
|
+
path, and a silent retry here would obscure the outage the separate field exists to surface.
|
|
68
|
+
|
|
69
|
+
## 3. Level-of-abstraction fit
|
|
70
|
+
|
|
71
|
+
All I/O in one place, everything else pure. The alternative — reads scattered through reader and
|
|
72
|
+
actuator — is exactly how a failed read becomes an empty result that reads as "nothing found".
|
|
73
|
+
|
|
74
|
+
## 4. Signal vs authority
|
|
75
|
+
|
|
76
|
+
Holds none. It executes a plan produced by deterministic rules and writes through a function the
|
|
77
|
+
caller owns.
|
|
78
|
+
|
|
79
|
+
## 5. Interactions
|
|
80
|
+
|
|
81
|
+
Consumes `RecurrenceReader` + `RecurrenceActuator` (same stack, PRs #1662 / actuator branch). Writes
|
|
82
|
+
only via the injected function. No schema change.
|
|
83
|
+
|
|
84
|
+
## 6. External surfaces
|
|
85
|
+
|
|
86
|
+
**None.** No route, no config, no persisted state of its own.
|
|
87
|
+
|
|
88
|
+
## 7. Multi-machine posture
|
|
89
|
+
|
|
90
|
+
**Posture: `machine-local`.** `machine-local-justification: physical-credential-locality` — it reads
|
|
91
|
+
one machine's stores, whose observation titles carry that machine's ids, topics and account emails,
|
|
92
|
+
and writes to that machine's action queue. Cross-machine synthesis would mean replicating those
|
|
93
|
+
records; the existing pool-scope fan-out is the correct route.
|
|
94
|
+
|
|
95
|
+
## 8. Rollback cost
|
|
96
|
+
|
|
97
|
+
**Zero.** One module, one test file, no callers.
|
|
98
|
+
|
|
99
|
+
## Phase 5 — Second-pass review
|
|
100
|
+
|
|
101
|
+
No gate/sentinel/watchdog, no block/allow authority, no LLM. Lenses:
|
|
102
|
+
|
|
103
|
+
**Adversarial — "how would I make this useless?"** Report a write outage as a refusal, so breakage
|
|
104
|
+
reads as judgement. Asserted against directly.
|
|
105
|
+
|
|
106
|
+
**"Would it have caught the incident?"** It IS the incident's remedy: 69 untracked recurrers, top
|
|
107
|
+
three actionable today.
|
|
108
|
+
|
|
109
|
+
**"Symptom or cause?"** Cause for never-picked-up. Not for the recurrence itself — creating an item
|
|
110
|
+
makes someone decide about the 278x idle-timeout; it does not fix it.
|
|
111
|
+
|
|
112
|
+
**Weakest point:** nothing schedules it. "Closes unattended" remains unclaimed.
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
# Side-effects review — round completion verifies merged state
|
|
2
|
+
|
|
3
|
+
**Change:** `runRound`'s `verifyMergedItems` seam no longer defaults to a no-op that reports nothing
|
|
4
|
+
verified. It defaults to the real git-backed `verifyMergedItemsViaGit`, the seam carries the
|
|
5
|
+
three-state `MergedVerificationResult`, and a new `unverifiable` outcome records **no** round status.
|
|
6
|
+
|
|
7
|
+
**Decision point touched:** yes — two of them. (a) whether a round is complete; (b) whether to spawn
|
|
8
|
+
an autonomous child. Both previously ran on a verifier that could only ever say "nothing verified".
|
|
9
|
+
|
|
10
|
+
## 1. Over-block — what legitimate inputs does this reject that it shouldn't?
|
|
11
|
+
|
|
12
|
+
The one real risk is stalling a round that should proceed. `verifyMergedItemsViaGit` reports
|
|
13
|
+
`unverifiable` for an item with **no `mergeCommitOid`**, which is the ordinary state of a round nobody
|
|
14
|
+
has worked yet. A naive "any unverifiable ⇒ don't spawn" would deadlock **every fresh round** — a far
|
|
15
|
+
worse defect than the one being fixed.
|
|
16
|
+
|
|
17
|
+
Handled by splitting on **evidence**: only an item that *records* a merge commit and cannot be checked
|
|
18
|
+
counts as genuinely uncheckable. An item with no recorded commit is not-done, and the child spawns.
|
|
19
|
+
Pinned by `an item with NO merge commit recorded is NOT-DONE, not unknown — the child still spawns`.
|
|
20
|
+
|
|
21
|
+
This test earned its place: the first draft applied the evidence rule at the pre-spawn check only,
|
|
22
|
+
and the post-exit check kept the old conflation. The test failed, and the predicate is now defined
|
|
23
|
+
once and used at both sites.
|
|
24
|
+
|
|
25
|
+
## 2. Under-block — what does it still miss?
|
|
26
|
+
|
|
27
|
+
- **CI-green is still not checked.** Verification remains merge-base reachability of a recorded
|
|
28
|
+
commit. An item merged with red CI verifies. Unchanged by this PR; `StageTransitionValidator`
|
|
29
|
+
performs the stronger check on the `/advance` path.
|
|
30
|
+
- **No scheduling.** Nothing here makes rounds run; it makes a run able to conclude.
|
|
31
|
+
- **`resolveCanonicalMainRef` is best-effort.** If `gh` is absent it falls back to `origin/main`,
|
|
32
|
+
which on a fork-origin home under-verifies (items read as regressed → a spawn, i.e. redoing work).
|
|
33
|
+
That is the pre-existing conservative default, now applied to this path too rather than left to a
|
|
34
|
+
caller to remember.
|
|
35
|
+
|
|
36
|
+
## 3. Level-of-abstraction fit
|
|
37
|
+
|
|
38
|
+
`resolveCanonicalMainRef` moved from `src/server/routes.ts` into `src/core/ProjectRoundExecution.ts`
|
|
39
|
+
and is imported back. Both consumers — the lazy reconciler and this runner — need identical
|
|
40
|
+
resolution, and a core module must not import from `server/`. Net: one definition, two callers,
|
|
41
|
+
correct direction. The two source-grep tests in `merged-record-carries-its-evidence.test.ts` assert
|
|
42
|
+
the *call* in `routes.ts`, not the definition, and still pass.
|
|
43
|
+
|
|
44
|
+
## 4. Signal vs authority
|
|
45
|
+
|
|
46
|
+
The verifier stays a **signal** — it reports three states and holds no blocking authority. `runRound`
|
|
47
|
+
is the authority and now consumes all three rather than flattening them. The specific improvement:
|
|
48
|
+
the authority can no longer be handed a value ("empty set") that is indistinguishable from a real
|
|
49
|
+
negative reading. Per `docs/signal-vs-authority.md`, the failure was not a brittle check holding
|
|
50
|
+
authority; it was an authority whose input could not express uncertainty.
|
|
51
|
+
|
|
52
|
+
## 5. Interactions
|
|
53
|
+
|
|
54
|
+
- **`/projects/:id/advance`** — untouched. Item-level stage transitions still go through
|
|
55
|
+
`StageTransitionValidator`; this only affects round-level outcome.
|
|
56
|
+
- **`ProjectAutoAdvancePoller`** — unchanged; it clears `autoAdvanceAt` and counts unacked advances.
|
|
57
|
+
It now sees rounds that can actually reach `complete`.
|
|
58
|
+
- **`ProjectDigestCache`** — unchanged, and this is the visible effect: the session-start
|
|
59
|
+
`N of M done` line can move off zero for the first time via the poller path.
|
|
60
|
+
- **Double-fire / races** — none added. `unverifiable` performs strictly *fewer* writes than any
|
|
61
|
+
other outcome (it writes nothing), so it cannot race the tracker's OCC.
|
|
62
|
+
|
|
63
|
+
## 6. External surfaces
|
|
64
|
+
|
|
65
|
+
`RoundOutcome` gains `'unverifiable'` — a TypeScript-level addition on an exported union. In-repo
|
|
66
|
+
consumers are `ProjectRoundExecution` itself and the tests; `recordOutcome` maps
|
|
67
|
+
`Exclude<RoundOutcome,'unverifiable'>` so the compiler enforces the exhaustiveness rather than a
|
|
68
|
+
runtime default swallowing it. No route, no config key, no user-visible string.
|
|
69
|
+
|
|
70
|
+
## 7. Multi-machine posture
|
|
71
|
+
|
|
72
|
+
**Machine-local BY DESIGN**, `machine-local-justification: hardware-bound-resource` — the check runs
|
|
73
|
+
`git merge-base` against a working checkout on local disk, and the round runner already holds a
|
|
74
|
+
host-local `ProjectRoundLock` (`.instar/local/round-runner.lock`). Round execution is bound to the
|
|
75
|
+
machine holding the checkout; nothing here introduces state another machine would need to read. No
|
|
76
|
+
notice, no durable cross-machine record, no generated URL.
|
|
77
|
+
|
|
78
|
+
## 8. Rollback cost
|
|
79
|
+
|
|
80
|
+
Low and local. One file's behavior plus a moved helper; no data migration, no config, no persisted
|
|
81
|
+
schema. Reverting restores the prior behavior exactly — including the defect. A round left in
|
|
82
|
+
`pending` by an `unverifiable` outcome is a normal pending round; nothing to repair.
|
|
83
|
+
|
|
84
|
+
## Refusals demonstrated (command + output)
|
|
85
|
+
|
|
86
|
+
Falsification 1 — neutralise the evidence predicate (`ids.filter(() => false && …)`):
|
|
87
|
+
|
|
88
|
+
```
|
|
89
|
+
× an item that RECORDS a merge commit but cannot be checked → no respawn, and NO round verdict
|
|
90
|
+
× child exits 0 but the shortfall is entirely uncheckable → unverifiable, NOT partially-complete
|
|
91
|
+
Tests 2 failed | 8 passed (10)
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Falsification 2 — re-introduce a default returning an unconditional empty verdict:
|
|
95
|
+
|
|
96
|
+
```
|
|
97
|
+
× the seam cannot default to silence again > has no default verifier that returns an unconditional empty set
|
|
98
|
+
Tests 1 failed | 9 passed (10)
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
Restored: `Tests 36 passed (36)` across the four affected files, `tsc --noEmit` exit 0.
|
|
102
|
+
|
|
103
|
+
Falsification 3 — **a guard I did not anticipate, which changed the code.** The full suite failed on
|
|
104
|
+
`lint-sync-subprocess-chokepoint`:
|
|
105
|
+
|
|
106
|
+
```
|
|
107
|
+
src/core/ProjectRoundExecution.ts:569 — raw sync spawn (execFileSync('gh', …)).
|
|
108
|
+
A sync blocking op must funnel through withSyncOp() so the in-flight marker sees it.
|
|
109
|
+
lint-sync-subprocess-chokepoint: 1 new violation(s).
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
In `routes.ts` that callsite sat on the lint's **frozen baseline** — grandfathered, not blessed.
|
|
113
|
+
Moving it into core made it a NEW violation. The refusal is the useful part: `runRound` is called
|
|
114
|
+
from the server's auto-advance poller, so a blocking `gh repo view` there stalls the event loop.
|
|
115
|
+
Now funnelled through `withSyncOp` so the in-flight marker classes the stall instead of it
|
|
116
|
+
presenting as an unexplained freeze. Lint after: `clean — 97 raw sync spawn(s), all grandfathered
|
|
117
|
+
(142 baselined)`.
|
|
118
|
+
|
|
119
|
+
Worth noting the lint is a deliberately **lexical, same-line** matcher (`/\bwithSyncOp\s*\(/`) and
|
|
120
|
+
says so in its own header — it cannot prove runtime wrapping, which is the marker's unit test's job.
|
|
121
|
+
My first wrap was semantically correct across three lines and still flagged. That is signal-vs-
|
|
122
|
+
authority working as designed: a cheap check that names a hazard, not a proof.
|
|
123
|
+
|
|
124
|
+
## Known-failing locally, verified NOT caused by this change
|
|
125
|
+
|
|
126
|
+
`tests/e2e/dev-preflight-cli.test.ts` fails in this worktree. The cause is inside preflight's lint
|
|
127
|
+
step, which shells out to `pnpm install`; reproduced standalone:
|
|
128
|
+
|
|
129
|
+
```
|
|
130
|
+
pnpm install → PNPM_EXIT=1
|
|
131
|
+
[ERR_PNPM_IGNORED_BUILDS] Ignored build scripts: baileys, better-sqlite3, esbuild, sharp, …
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
That is a dependency build-script approval issue in a worktree installed with `npm ci`, naming only
|
|
135
|
+
third-party packages and reading none of the changed files. Not run to completion deliberately —
|
|
136
|
+
`pnpm install` would restructure the `node_modules` the 36 passing tests were validated against. CI
|
|
137
|
+
installs the project its own way and is the arbiter; flagged here rather than quietly omitted.
|