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.
@@ -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.