instar 1.3.993 → 1.3.995
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/channelRegistry.d.ts +17 -0
- package/dist/core/channelRegistry.d.ts.map +1 -1
- package/dist/core/channelRegistry.js +7 -1
- package/dist/core/channelRegistry.js.map +1 -1
- package/dist/core/instarChannels.d.ts.map +1 -1
- package/dist/core/instarChannels.js +4 -0
- package/dist/core/instarChannels.js.map +1 -1
- package/dist/core/userChannels.d.ts +73 -0
- package/dist/core/userChannels.d.ts.map +1 -0
- package/dist/core/userChannels.js +138 -0
- package/dist/core/userChannels.js.map +1 -0
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/routes.js +27 -52
- 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.994.md +69 -0
- package/upgrades/1.3.995.md +75 -0
- package/upgrades/side-effects/round-completion-verifies-merged.md +137 -0
- package/upgrades/side-effects/user-channel-liveness.md +116 -0
|
@@ -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.
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# Side-effects review — user-channel liveness in the channel registry
|
|
2
|
+
|
|
3
|
+
**Change:** the two direct USER channels (Telegram, Slack) join the peer channel registry, with
|
|
4
|
+
liveness probes that read live adapter state instead of configuration. `ChannelDefinition` and
|
|
5
|
+
`ChannelReport` gain an `audience: 'peer' | 'user'` field.
|
|
6
|
+
|
|
7
|
+
**Decision point touched:** none that blocks. This is a read surface — it informs a routing choice a
|
|
8
|
+
caller makes, and holds no authority over it. `advisory: true` is already on the response.
|
|
9
|
+
|
|
10
|
+
## 1. Over-block — what legitimate inputs does this reject that it shouldn't?
|
|
11
|
+
|
|
12
|
+
Nothing is rejected; nothing is gated. The nearest analogue is reporting a *usable* channel as
|
|
13
|
+
unusable, which would cause a caller to avoid a working path.
|
|
14
|
+
|
|
15
|
+
One case deliberately risks that and should be named: a Telegram that is **not polling with no
|
|
16
|
+
recorded reason** reports `unknown`, not `working`. If the adapter was stopped deliberately and could
|
|
17
|
+
still send, this understates it. That direction is chosen on purpose — over-reporting liveness is the
|
|
18
|
+
defect being fixed, and `unknown` carries its reason rather than pretending to a verdict.
|
|
19
|
+
|
|
20
|
+
Transient errors do NOT downgrade a live channel: still-polling with a non-zero consecutive error
|
|
21
|
+
count stays `working`, with the count named in the detail. Marking that broken would be the opposite
|
|
22
|
+
over-block.
|
|
23
|
+
|
|
24
|
+
## 2. Under-block — what does it still miss?
|
|
25
|
+
|
|
26
|
+
- **A live reading is not a promise.** `working` means the loop was polling when asked. It can die a
|
|
27
|
+
second later. No probe can fix that; the response carries `generatedAt`.
|
|
28
|
+
- **No round-trip is attempted.** Telegram `working` means the poll loop is up, not that a message to
|
|
29
|
+
a specific topic would land (a topic could be deleted, a user could have blocked the bot). The
|
|
30
|
+
peer registry's `mutual-ssh` entry already makes the same distinction in its own detail text, and
|
|
31
|
+
this follows that precedent rather than overstating.
|
|
32
|
+
- **Slack is one workspace.** `isConnected()` is per-adapter; a multi-workspace setup is not modelled.
|
|
33
|
+
- **Other user surfaces are absent** — WhatsApp and iMessage adapters exist in the tree. They are not
|
|
34
|
+
in this change, and the registry will therefore not claim anything about them. Under the registry's
|
|
35
|
+
own "absence is impossible" property this is a real limit: a channel with no row cannot report that
|
|
36
|
+
it is missing. Called out rather than quietly scoped away.
|
|
37
|
+
|
|
38
|
+
## 3. Level-of-abstraction fit
|
|
39
|
+
|
|
40
|
+
The user definitions live in a NEW file (`src/core/userChannels.ts`) rather than being added to
|
|
41
|
+
`src/core/instarChannels.ts`, whose header declares an explicit "PEER-TO-PEER only" scope discipline
|
|
42
|
+
and justifies two prior exclusions. Widening that file would have silently discarded a deliberate
|
|
43
|
+
decision by its author.
|
|
44
|
+
|
|
45
|
+
The two lists are composed at the route and resolved by the same `resolveChannels`, so the registry's
|
|
46
|
+
invariants (one row per definition, bounded probes, `unknown` on failure) apply identically to both
|
|
47
|
+
without being reimplemented.
|
|
48
|
+
|
|
49
|
+
`audience` is data on the channel, not two registries, because the peer-vs-user choice is itself a
|
|
50
|
+
routing decision a caller must be able to weigh. Two surfaces would require the caller to already
|
|
51
|
+
know which to consult — the arbitrariness the registry exists to remove.
|
|
52
|
+
|
|
53
|
+
## 4. Signal vs authority
|
|
54
|
+
|
|
55
|
+
Pure signal. The registry reports; it never routes, blocks, or sends. The response is already flagged
|
|
56
|
+
`advisory: true`. The mapping functions (`telegramStateFrom`, `slackStateFrom`) are exported and pure
|
|
57
|
+
precisely so the verdict logic can be pinned by tests without constructing an adapter — the mapping
|
|
58
|
+
is where a wrong verdict would originate.
|
|
59
|
+
|
|
60
|
+
## 5. Interactions
|
|
61
|
+
|
|
62
|
+
- **`/capabilities`** — unchanged. It keeps reporting `telegram: { configured: true }`, which remains
|
|
63
|
+
correct for what it measures (configuration). This adds the missing state reading; it does not
|
|
64
|
+
correct or replace the config reading, and the two answer different questions.
|
|
65
|
+
- **Peer channels** — behaviour unchanged; they gain an `audience: 'peer'` tag. Because `audience` is
|
|
66
|
+
required on `ChannelDefinition`, a future channel cannot be added untagged: it is a compile error,
|
|
67
|
+
not a silent default.
|
|
68
|
+
- **No double-fire / no races** — read-only, no writes, no timers of its own. Probes are bounded by
|
|
69
|
+
the registry's existing 3s timeout.
|
|
70
|
+
|
|
71
|
+
## 6. External surfaces
|
|
72
|
+
|
|
73
|
+
`GET /channels` gains two rows and every row gains an `audience` field. Additive: existing consumers
|
|
74
|
+
reading `id`/`state`/`detail` are unaffected. No new route, no config key, no user-visible string.
|
|
75
|
+
|
|
76
|
+
## 7. Multi-machine posture
|
|
77
|
+
|
|
78
|
+
**Machine-local BY DESIGN**, `machine-local-justification: physical-credential-locality` — a Telegram
|
|
79
|
+
bot token and its long-poll loop, and a Slack Socket Mode connection, live in one process on one
|
|
80
|
+
machine. "Is my Telegram polling?" is only meaningful about the machine asked; replicating another
|
|
81
|
+
machine's answer would assert liveness this process cannot observe. This matches the peer registry,
|
|
82
|
+
which is machine-local for the same reason (its relay/SSH probes read local runtime state).
|
|
83
|
+
|
|
84
|
+
## 8. Rollback cost
|
|
85
|
+
|
|
86
|
+
Low. One new file, one required field on two interfaces, one route composing two lists. No data
|
|
87
|
+
migration, no config, no persisted state. Reverting restores the prior behaviour exactly — including
|
|
88
|
+
the gap.
|
|
89
|
+
|
|
90
|
+
## Refusals demonstrated (command + output)
|
|
91
|
+
|
|
92
|
+
Falsification 1 — make the Telegram probe read EXISTENCE instead of liveness (`if (status !== null)`
|
|
93
|
+
in place of `if (status.started)`), i.e. exactly what `/capabilities` does:
|
|
94
|
+
|
|
95
|
+
```
|
|
96
|
+
× THE FIX: a configured-but-DEAD Telegram is never reported as working
|
|
97
|
+
× a missing bot token is a credential verdict, not a network one
|
|
98
|
+
× a network death is broken — distinct from a credential problem
|
|
99
|
+
× stopped for NO recorded reason is unknown — never working, never a confident broken
|
|
100
|
+
Tests 4 failed | 12 passed (16)
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
Falsification 2 — give Slack "ever connected" semantics (`if (enabled)` in place of
|
|
104
|
+
`if (connected)`), the exact trap its own source warns about:
|
|
105
|
+
|
|
106
|
+
```
|
|
107
|
+
× THE TRAP: enabled with the socket DOWN is broken, not working
|
|
108
|
+
Tests 1 failed | 15 passed (16)
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Restored: `Tests 35 passed (35)` across `channel-registry`, `channel-registry-claims` and
|
|
112
|
+
`user-channel-liveness`; `npx tsc --noEmit` exit 0.
|
|
113
|
+
|
|
114
|
+
Two source ratchets are included so the probes cannot drift back: one asserts `userChannels.ts` never
|
|
115
|
+
reads a `.configured` property or a `config.*` value, the other asserts the route wiring uses
|
|
116
|
+
`isConnected()` and never `slackAdapter.started`.
|