instar 0.28.58 → 0.28.60
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/dashboard/index.html +21 -17
- package/package.json +2 -1
- package/src/data/builtin-manifest.json +2 -2
- package/upgrades/0.28.59.md +85 -0
- package/upgrades/0.28.60.md +80 -0
- package/upgrades/side-effects/0.28.57.md +33 -0
- package/upgrades/side-effects/0.28.58.md +24 -0
- package/upgrades/side-effects/0.28.59.md +20 -0
- package/upgrades/side-effects/ci-shard-unit-tests.md +108 -0
- package/upgrades/side-effects/dashboard-resume-live-scroll.md +132 -0
- package/upgrades/side-effects/pre-push-smoke-tier.md +235 -0
package/dashboard/index.html
CHANGED
|
@@ -3221,6 +3221,7 @@
|
|
|
3221
3221
|
|
|
3222
3222
|
activeSession = tmuxSession;
|
|
3223
3223
|
userIsFollowing = true; // Reset scroll tracking on session switch
|
|
3224
|
+
hideResumeButton(); // Carry-over button from prior session would be misleading
|
|
3224
3225
|
historyLinesLoaded = 2000; // Reset history state for new session
|
|
3225
3226
|
historyLoading = false;
|
|
3226
3227
|
historyExhausted = false;
|
|
@@ -3334,15 +3335,20 @@
|
|
|
3334
3335
|
if (atBottom && !userIsFollowing) {
|
|
3335
3336
|
// User scrolled back to bottom — resume following and apply pending output
|
|
3336
3337
|
userIsFollowing = true;
|
|
3338
|
+
hideResumeButton();
|
|
3337
3339
|
if (pendingOutputData) {
|
|
3338
3340
|
const data = pendingOutputData;
|
|
3339
3341
|
pendingOutputData = null;
|
|
3340
3342
|
term.clear();
|
|
3341
|
-
term.write
|
|
3342
|
-
term.scrollToBottom();
|
|
3343
|
+
// term.write is async — scroll after write completes so the buffer is populated.
|
|
3344
|
+
term.write(data, () => { term.scrollToBottom(); });
|
|
3343
3345
|
}
|
|
3344
|
-
} else if (!atBottom) {
|
|
3346
|
+
} else if (!atBottom && userIsFollowing) {
|
|
3347
|
+
// User scrolled up — pause follow and surface the resume button
|
|
3348
|
+
// even if no new output is pending. The button is the always-available
|
|
3349
|
+
// "take me back to live" control.
|
|
3345
3350
|
userIsFollowing = false;
|
|
3351
|
+
showResumeButton();
|
|
3346
3352
|
}
|
|
3347
3353
|
|
|
3348
3354
|
// Infinite scroll: load more history when scrolled near the top
|
|
@@ -3370,12 +3376,13 @@
|
|
|
3370
3376
|
const atBottom = buf.baseY === 0 || (buf.baseY - buf.viewportY) <= 5;
|
|
3371
3377
|
if (atBottom) {
|
|
3372
3378
|
userIsFollowing = true;
|
|
3379
|
+
hideResumeButton();
|
|
3373
3380
|
if (pendingOutputData) {
|
|
3374
3381
|
const data = pendingOutputData;
|
|
3375
3382
|
pendingOutputData = null;
|
|
3376
3383
|
term.clear();
|
|
3377
|
-
term.write
|
|
3378
|
-
term.scrollToBottom();
|
|
3384
|
+
// term.write is async — scroll after write completes so the buffer is populated.
|
|
3385
|
+
term.write(data, () => { term.scrollToBottom(); });
|
|
3379
3386
|
}
|
|
3380
3387
|
}
|
|
3381
3388
|
}
|
|
@@ -3449,23 +3456,20 @@
|
|
|
3449
3456
|
btn.style.cssText = 'position:absolute;bottom:12px;left:50%;transform:translateX(-50%);z-index:10;background:var(--accent);color:#000;border:none;padding:6px 16px;border-radius:4px;font-size:12px;cursor:pointer;font-weight:600;opacity:0.95;box-shadow:0 2px 8px rgba(0,0,0,0.3);';
|
|
3450
3457
|
btn.onclick = () => {
|
|
3451
3458
|
userIsFollowing = true;
|
|
3459
|
+
const snapToBottom = () => {
|
|
3460
|
+
term.scrollToBottom();
|
|
3461
|
+
const container = document.getElementById('terminalContainer');
|
|
3462
|
+
if (container) container.scrollIntoView({ block: 'end', behavior: 'instant' });
|
|
3463
|
+
};
|
|
3452
3464
|
if (pendingOutputData) {
|
|
3453
3465
|
const data = pendingOutputData;
|
|
3454
3466
|
pendingOutputData = null;
|
|
3455
3467
|
term.clear();
|
|
3456
|
-
term.write
|
|
3457
|
-
//
|
|
3458
|
-
|
|
3459
|
-
term.scrollToBottom();
|
|
3460
|
-
// Also scroll the browser viewport to show the terminal bottom
|
|
3461
|
-
const container = document.getElementById('terminalContainer');
|
|
3462
|
-
if (container) container.scrollIntoView({ block: 'end', behavior: 'instant' });
|
|
3463
|
-
});
|
|
3468
|
+
// term.write is async — use its completion callback so scroll runs
|
|
3469
|
+
// after the buffer is populated, not against a stale viewport.
|
|
3470
|
+
term.write(data, snapToBottom);
|
|
3464
3471
|
} else {
|
|
3465
|
-
|
|
3466
|
-
term.scrollToBottom();
|
|
3467
|
-
const container = document.getElementById('terminalContainer');
|
|
3468
|
-
if (container) container.scrollIntoView({ block: 'end', behavior: 'instant' });
|
|
3472
|
+
snapToBottom();
|
|
3469
3473
|
}
|
|
3470
3474
|
hideResumeButton();
|
|
3471
3475
|
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "instar",
|
|
3
|
-
"version": "0.28.
|
|
3
|
+
"version": "0.28.60",
|
|
4
4
|
"description": "Persistent autonomy infrastructure for AI agents",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
"dev": "tsc --watch",
|
|
21
21
|
"test": "vitest run",
|
|
22
22
|
"test:push": "vitest run --config vitest.push.config.ts",
|
|
23
|
+
"test:smoke": "vitest run --config vitest.push.config.ts --changed origin/main",
|
|
23
24
|
"test:flaky": "vitest run tests/unit/relationship-routes.test.ts tests/integration/messaging-routes.test.ts tests/integration/whatsapp-routes.test.ts tests/unit/server.test.ts tests/e2e/semantic-memory-lifecycle.test.ts tests/e2e/system-reviewer-e2e.test.ts tests/e2e/working-memory-lifecycle.test.ts tests/e2e/messaging-multi-agent.test.ts",
|
|
24
25
|
"test:watch": "vitest",
|
|
25
26
|
"test:integration": "vitest run --config vitest.integration.config.ts",
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "./builtin-manifest.schema.json",
|
|
3
3
|
"schemaVersion": 1,
|
|
4
|
-
"generatedAt": "2026-04-
|
|
5
|
-
"instarVersion": "0.28.
|
|
4
|
+
"generatedAt": "2026-04-19T05:19:43.879Z",
|
|
5
|
+
"instarVersion": "0.28.60",
|
|
6
6
|
"entryCount": 186,
|
|
7
7
|
"entries": {
|
|
8
8
|
"hook:session-start": {
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
# Upgrade Guide — Dashboard "Resume live output" reliability + always-visible control
|
|
2
|
+
|
|
3
|
+
## What Changed
|
|
4
|
+
|
|
5
|
+
Fixed two long-standing rough edges in the dashboard's terminal view for the
|
|
6
|
+
"▼ Resume live output" control.
|
|
7
|
+
|
|
8
|
+
### Scroll-to-bottom now actually lands at the bottom
|
|
9
|
+
|
|
10
|
+
Clicking "▼ Resume live output" after scrolling up in a session view used to
|
|
11
|
+
sometimes leave the viewport mid-history instead of snapping to the tail of
|
|
12
|
+
live output — defeating the purpose of the button. The same stale-scroll
|
|
13
|
+
happened on the two sibling auto-resume paths that fire when the user
|
|
14
|
+
scrolls back to the bottom or wheels down into it.
|
|
15
|
+
|
|
16
|
+
Root cause: `term.write(data)` in xterm is asynchronous. The old code called
|
|
17
|
+
`term.scrollToBottom()` immediately after the write, or inside a
|
|
18
|
+
`requestAnimationFrame` — neither guarantees the buffer has been populated,
|
|
19
|
+
so the scroll operated on stale viewport state and landed above the new
|
|
20
|
+
output.
|
|
21
|
+
|
|
22
|
+
Fix: all three resume paths now use xterm's write-completion callback —
|
|
23
|
+
`term.write(data, () => term.scrollToBottom())`. The scroll runs after the
|
|
24
|
+
buffer is populated, so the viewport lands at the new bottom.
|
|
25
|
+
|
|
26
|
+
### The button is always visible when paused
|
|
27
|
+
|
|
28
|
+
Previously the "▼ Resume live output" button only appeared when new output
|
|
29
|
+
happened to arrive while the user was scrolled up. In idle sessions —
|
|
30
|
+
nothing streaming, user just scrolled up to read history — the button never
|
|
31
|
+
surfaced, and there was no UI control to jump back to live.
|
|
32
|
+
|
|
33
|
+
Fix: button visibility now mirrors `!userIsFollowing` from every code path
|
|
34
|
+
that flips the flag. Scroll up in a session view (idle or streaming) and the
|
|
35
|
+
button appears. Scroll back to the bottom, click the button, wheel down into
|
|
36
|
+
the tail, or switch sessions, and the button hides.
|
|
37
|
+
|
|
38
|
+
## What to Tell Your User
|
|
39
|
+
|
|
40
|
+
The "Resume live output" button in your dashboard's session view now works
|
|
41
|
+
the way you'd expect: it shows up any time you've scrolled up (even in an
|
|
42
|
+
idle session where nothing is streaming), and clicking it reliably drops
|
|
43
|
+
you at the bottom of the live output instead of leaving you mid-history.
|
|
44
|
+
|
|
45
|
+
## Summary of New Capabilities
|
|
46
|
+
|
|
47
|
+
| Capability | How to Use |
|
|
48
|
+
|-----------|-----------|
|
|
49
|
+
| Reliable auto-follow resume in the dashboard terminal | Click "▼ Resume live output" after scrolling up in any session view; viewport snaps to the bottom |
|
|
50
|
+
| Button always available while paused | Scroll up in any session — the button surfaces automatically; scroll back down or click it to resume |
|
|
51
|
+
|
|
52
|
+
## Evidence
|
|
53
|
+
|
|
54
|
+
The fix was reproduced from Justin's report (topic 6309 on echo agent): the
|
|
55
|
+
button left him mid-history, and in idle sessions there was no button at
|
|
56
|
+
all.
|
|
57
|
+
|
|
58
|
+
Regression test (`tests/unit/dashboard-resumeLive.test.ts`, 10 tests) locks
|
|
59
|
+
in both invariants at the HTML-inspection layer:
|
|
60
|
+
|
|
61
|
+
- Scroll-after-write: all three resume paths use the xterm write callback;
|
|
62
|
+
the old `term.write(data); requestAnimationFrame(() => term.scrollToBottom())`
|
|
63
|
+
shape is explicitly banned.
|
|
64
|
+
- Button visibility: `term.onScroll` shows the button on scroll-up and
|
|
65
|
+
hides it on scroll-to-bottom; the wheel-down resume path hides the
|
|
66
|
+
button; session-switch reset hides any carry-over button.
|
|
67
|
+
|
|
68
|
+
Full dashboard test suite green across `dashboard-*.test.ts`.
|
|
69
|
+
|
|
70
|
+
Side-effects review:
|
|
71
|
+
`upgrades/side-effects/dashboard-resume-live-scroll.md` — concludes no
|
|
72
|
+
decision-point touched (pure UI timing + visibility fix), no over/under-block
|
|
73
|
+
risk, trivial rollback (git revert).
|
|
74
|
+
|
|
75
|
+
## Deployment Notes
|
|
76
|
+
|
|
77
|
+
No operator action required on update. The dashboard is static HTML served
|
|
78
|
+
from the installed instar package — clients pick up the fix on their next
|
|
79
|
+
page load after `instar` updates to 0.28.59.
|
|
80
|
+
|
|
81
|
+
## Rollback
|
|
82
|
+
|
|
83
|
+
Downgrade reverts the three resume paths to the prior (racy) code and
|
|
84
|
+
returns the button to its contingent-on-new-output visibility. No schema
|
|
85
|
+
changes, no state-file changes, no API changes.
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# Upgrade Guide — Pre-push smoke tier (fast local test gate)
|
|
2
|
+
|
|
3
|
+
## What Changed
|
|
4
|
+
|
|
5
|
+
The local pre-push test gate now runs only the tests affected by files
|
|
6
|
+
you changed, instead of the full suite. On a typical small push this
|
|
7
|
+
drops from ~10 minutes to tens of seconds. The full suite still runs in
|
|
8
|
+
CI on the PR across 8 sharded runners, and CI is the authority for
|
|
9
|
+
merge.
|
|
10
|
+
|
|
11
|
+
### Before
|
|
12
|
+
|
|
13
|
+
Every `git push` ran `npm run test:push` — the full suite minus the
|
|
14
|
+
known-flaky exclude list — serially (no in-process parallelism because
|
|
15
|
+
tests collide on ports, SQLite, npm). Wall-clock ~9–10 min on every
|
|
16
|
+
push, including pushes that only touched docs or config.
|
|
17
|
+
|
|
18
|
+
### After
|
|
19
|
+
|
|
20
|
+
Every `git push` now runs `npm run test:smoke` — the same test set,
|
|
21
|
+
gated by vitest's `--changed origin/main` mode, so only tests whose
|
|
22
|
+
files (or their transitive imports) appear in your diff execute. The
|
|
23
|
+
exclude list and `fileParallelism: false` are preserved; the change is
|
|
24
|
+
which files are included, not how they run.
|
|
25
|
+
|
|
26
|
+
Escape hatches, for the cases where you want the old behavior:
|
|
27
|
+
|
|
28
|
+
| Variable | Effect |
|
|
29
|
+
|----------|--------|
|
|
30
|
+
| `INSTAR_PRE_PUSH_FULL=1 git push` | Run the full push suite locally (old behavior, ~10 min) |
|
|
31
|
+
| `INSTAR_PRE_PUSH_SKIP=1 git push` | Skip pre-push tests entirely; CI is the only gate |
|
|
32
|
+
|
|
33
|
+
The NEXT.md / version / side-effects pre-push gate still runs first on
|
|
34
|
+
every push — that's independent of the test tier and stays as-is.
|
|
35
|
+
|
|
36
|
+
## What to Tell Your User
|
|
37
|
+
|
|
38
|
+
Pushing local changes now takes seconds instead of minutes in the
|
|
39
|
+
common case. The exhaustive test run still happens on GitHub before
|
|
40
|
+
anything merges, so you won't ship a broken change — you just won't
|
|
41
|
+
wait for the full suite on your laptop.
|
|
42
|
+
|
|
43
|
+
## Summary of New Capabilities
|
|
44
|
+
|
|
45
|
+
| Capability | How to Use |
|
|
46
|
+
|-----------|-----------|
|
|
47
|
+
| Fast local pre-push gate | Nothing to do — `git push` now runs only tests touched by your diff |
|
|
48
|
+
| Force full local suite | `INSTAR_PRE_PUSH_FULL=1 git push` |
|
|
49
|
+
| Skip local tests entirely | `INSTAR_PRE_PUSH_SKIP=1 git push` (CI still runs the full suite) |
|
|
50
|
+
|
|
51
|
+
## Evidence
|
|
52
|
+
|
|
53
|
+
- Local run of `npm run test:smoke` on this change (which only touches
|
|
54
|
+
`.husky/pre-push` and `package.json` scripts) runs 0 tests — the
|
|
55
|
+
diff doesn't cover any source files, so `--changed` has no targets.
|
|
56
|
+
That's the intended fast-path.
|
|
57
|
+
- Local run of `npm run test:push` unchanged — ~10 min as before, now
|
|
58
|
+
opt-in via `INSTAR_PRE_PUSH_FULL=1`.
|
|
59
|
+
- CI's 8-shard matrix on `ci.yml` is unchanged and continues to run
|
|
60
|
+
the full push suite on every PR — that's the authority; pre-push is
|
|
61
|
+
a signal.
|
|
62
|
+
|
|
63
|
+
Side-effects review:
|
|
64
|
+
`upgrades/side-effects/pre-push-smoke-tier.md` — covers over/under-block,
|
|
65
|
+
level-of-abstraction fit (signal-vs-authority: pre-push is a signal, CI
|
|
66
|
+
is the authority), interactions with the NEXT.md gate and the retry
|
|
67
|
+
loop, external surfaces, rollback cost.
|
|
68
|
+
|
|
69
|
+
## Deployment Notes
|
|
70
|
+
|
|
71
|
+
No operator action required on update. The change is to the
|
|
72
|
+
contributor-side git hook, which is installed by `npm install` via
|
|
73
|
+
husky. Contributors pick it up automatically on their next `npm ci`.
|
|
74
|
+
|
|
75
|
+
## Rollback
|
|
76
|
+
|
|
77
|
+
Revert this commit. `.husky/pre-push` returns to running
|
|
78
|
+
`npm run test:push` (full suite) on every push, `test:smoke` script
|
|
79
|
+
stays harmless in package.json until removed. No schema changes, no
|
|
80
|
+
state-file changes, no API changes.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# Side-Effects Review — 0.28.57 (release aggregate)
|
|
2
|
+
|
|
3
|
+
**Version:** `0.28.57`
|
|
4
|
+
**Date:** `2026-04-19`
|
|
5
|
+
**Author:** `echo` (backfill)
|
|
6
|
+
**Second-pass reviewer:** `not required`
|
|
7
|
+
|
|
8
|
+
## Summary
|
|
9
|
+
|
|
10
|
+
Release 0.28.57 bundles two changes that were each reviewed individually at merge time. The publish workflow's file-renamer skipped the corresponding `upgrades/side-effects/` artifact during the 0.28.57 finalization (it renames `upgrades/NEXT.md → upgrades/{version}.md` but not the side-effects counterpart), so this file is a minimal aggregator that points at the two component reviews already on disk. Captured as a follow-up in the pre-push gate: teach `publish.yml` or `check-upgrade-guide.js` to also rename the matching side-effects artifact so this backfill isn't needed on future releases.
|
|
11
|
+
|
|
12
|
+
## Components
|
|
13
|
+
|
|
14
|
+
### 1. TelegramLifeline sends auth on `/internal/*` forwards
|
|
15
|
+
|
|
16
|
+
Landed via PR #64. Surgical fix in `src/lifeline/TelegramLifeline.ts` to add `Authorization: Bearer <authToken>` to `forwardToServer()` and `handleCallbackQuery()`. Reasoning: the 0.28.53 server-side tightening that required auth on `/internal/*` had no matching client-side update in the lifeline — every inbound Telegram message was 401ing.
|
|
17
|
+
|
|
18
|
+
- No runtime decision points introduced; a signal-less, surface-narrow header addition.
|
|
19
|
+
- Over-block / under-block: not applicable — no block/allow surface.
|
|
20
|
+
- Level-of-abstraction: header is constructed exactly where the fetch is built; no new indirection.
|
|
21
|
+
- Signal-vs-authority: not applicable — hard-invariant transport-layer auth.
|
|
22
|
+
- Interactions: none with other gates; backwards-compatible on servers that don't require the header.
|
|
23
|
+
- Rollback: revert the single file.
|
|
24
|
+
|
|
25
|
+
### 2. Drop duplicate CI gate from `publish.yml` (PR #67)
|
|
26
|
+
|
|
27
|
+
Full review: `upgrades/side-effects/publish-drop-duplicate-ci-gate.md` (in this directory). Summary: removed the `ci` job from `publish.yml` that duplicated `ci.yml` on the same `push: branches: [main]` event. Publish wall-clock measured 11m → 55s on the first post-merge run. Branch-protection-at-PR-time remains the actual authority.
|
|
28
|
+
|
|
29
|
+
## Notes
|
|
30
|
+
|
|
31
|
+
This aggregator file exists to satisfy the `pre-push-gate.js` requirement that `upgrades/side-effects/{version}.md` exists whenever `upgrades/{version}.md` exists and claims fix/feature content. The finer-grained artifacts for each component are the authoritative reviews; this file only provides the version-named pointer.
|
|
32
|
+
|
|
33
|
+
Follow-up captured: either (a) teach `publish.yml` to also rename the matching side-effects artifact during NEXT → version finalization, or (b) relax `pre-push-gate.js` to accept any side-effects artifact dated within the release window rather than requiring a version-exact filename.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Side-Effects Review — 0.28.58 (release aggregate)
|
|
2
|
+
|
|
3
|
+
**Version:** `0.28.58`
|
|
4
|
+
**Date:** `2026-04-19`
|
|
5
|
+
**Author:** `echo` (backfill)
|
|
6
|
+
**Second-pass reviewer:** `not required`
|
|
7
|
+
|
|
8
|
+
## Summary
|
|
9
|
+
|
|
10
|
+
Release 0.28.58 shipped the Initiative Tracker recovery merge (PR #68). The two component reviews exist in this directory under slug names because `publish.yml`'s finalization renames `upgrades/NEXT.md → upgrades/{version}.md` but does not rename the corresponding side-effects artifact. This file is a minimal aggregator that satisfies `pre-push-gate.js`'s requirement that `upgrades/side-effects/{version}.md` exists whenever `upgrades/{version}.md` exists and claims fix/feature content.
|
|
11
|
+
|
|
12
|
+
## Components
|
|
13
|
+
|
|
14
|
+
### Initiative Tracker — core and API
|
|
15
|
+
|
|
16
|
+
Full review: `upgrades/side-effects/initiative-tracker-core-and-api.md`. Persisted multi-phase work tracker with phase / blocker / last-touched state; backed by a JSON store; 28 core tests + 15 route tests added.
|
|
17
|
+
|
|
18
|
+
### Initiative Tracker — dashboard tab
|
|
19
|
+
|
|
20
|
+
Full review: `upgrades/side-effects/initiative-tracker-dashboard-tab.md`. New "Initiatives" tab in the dashboard rendering the tracker state; 9 dashboard smoke tests added.
|
|
21
|
+
|
|
22
|
+
## Notes
|
|
23
|
+
|
|
24
|
+
Same follow-up as noted in `0.28.57.md` aggregator: either (a) teach `publish.yml` to also rename the matching side-effects artifact during NEXT → version finalization, or (b) relax `pre-push-gate.js` to accept any side-effects artifact dated within the release window rather than requiring a version-exact filename. Until one of those lands, each release will need a backfill aggregator like this one.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Side-Effects Review — 0.28.59 (release aggregate)
|
|
2
|
+
|
|
3
|
+
**Version:** `0.28.59`
|
|
4
|
+
**Date:** `2026-04-19`
|
|
5
|
+
**Author:** `echo` (backfill)
|
|
6
|
+
**Second-pass reviewer:** `not required`
|
|
7
|
+
|
|
8
|
+
## Summary
|
|
9
|
+
|
|
10
|
+
Release 0.28.59 shipped the dashboard "Resume live output" scroll-reliability + always-visible control fix (PR #63). The component review exists in this directory under its slug name (`dashboard-resume-live-scroll.md`) because `publish.yml`'s finalization renames `upgrades/NEXT.md → upgrades/{version}.md` but does not rename the corresponding side-effects artifact. This file is a minimal aggregator that satisfies `pre-push-gate.js`'s requirement that `upgrades/side-effects/{version}.md` exists whenever `upgrades/{version}.md` exists and claims fix/feature content.
|
|
11
|
+
|
|
12
|
+
## Components
|
|
13
|
+
|
|
14
|
+
### Dashboard "Resume live output" reliability + always-visible control
|
|
15
|
+
|
|
16
|
+
Full review: `upgrades/side-effects/dashboard-resume-live-scroll.md`. Pure UI timing + visibility fix — all three resume paths now use xterm's write-completion callback so `scrollToBottom()` runs after the buffer is populated; button visibility now mirrors `!userIsFollowing` from every code path that flips the flag. 10 regression tests added at `tests/unit/dashboard-resumeLive.test.ts`.
|
|
17
|
+
|
|
18
|
+
## Notes
|
|
19
|
+
|
|
20
|
+
Same follow-up as captured in `0.28.57.md` and `0.28.58.md` aggregators: either (a) teach `publish.yml` to also rename the matching side-effects artifact during NEXT → version finalization, or (b) relax `pre-push-gate.js` to accept any side-effects artifact dated within the release window rather than requiring a version-exact filename. Until one of those lands, each release will need a backfill aggregator like this one.
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
# Side-Effects Review — Shard CI unit tests across 4 parallel runners
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `ci-shard-unit-tests`
|
|
4
|
+
**Date:** `2026-04-19`
|
|
5
|
+
**Author:** `echo`
|
|
6
|
+
**Second-pass reviewer:** `not required`
|
|
7
|
+
|
|
8
|
+
## Summary of the change
|
|
9
|
+
|
|
10
|
+
`.github/workflows/ci.yml` unit-test job is extended to run the pre-push test suite across 4 vitest shards per Node version, using vitest's built-in `--shard=N/M` file-partitioning. The matrix changes from `{ node-version: [20, 22] }` (2 runners, each ~9½ min serial) to `{ node-version: [20, 22], shard: [1,2,3,4] }` (8 runners, each ~2½ min serial inside its shard). `fileParallelism: false` in `vitest.push.config.ts` stays untouched — each shard still runs its files one-at-a-time to preserve the port / SQLite / npm isolation that commit `002a463` established. `fail-fast: false` is added so one flaky shard doesn't kill the others. Expected impact on CI wall-clock: unit-test phase 9½ min → ~3 min, which pulls overall PR-CI time from ~12 min toward ~5-6 min (next bottleneck becomes integration/e2e). Only file touched: `.github/workflows/ci.yml`.
|
|
11
|
+
|
|
12
|
+
## Decision-point inventory
|
|
13
|
+
|
|
14
|
+
- `ci.yml.unit` — **modify** — job matrix gains a `shard` dimension; test runner invoked with `--shard=${{ matrix.shard }}/4`.
|
|
15
|
+
|
|
16
|
+
No runtime / agent-behavior decision points touched.
|
|
17
|
+
|
|
18
|
+
---
|
|
19
|
+
|
|
20
|
+
## 1. Over-block
|
|
21
|
+
|
|
22
|
+
**What legitimate inputs does this change reject that it shouldn't?**
|
|
23
|
+
|
|
24
|
+
No block/allow surface on message flow — over-block not applicable in the runtime sense.
|
|
25
|
+
|
|
26
|
+
In the CI sense: the change runs *exactly the same* set of tests as before, just partitioned across 4 runners per Node version. Vitest's sharding is deterministic and disjoint — the union of `--shard=1/4 … 4/4` is the full include set. A commit that passed unsharded would pass sharded. No new rejection surface.
|
|
27
|
+
|
|
28
|
+
---
|
|
29
|
+
|
|
30
|
+
## 2. Under-block
|
|
31
|
+
|
|
32
|
+
**What failure modes does this still miss?**
|
|
33
|
+
|
|
34
|
+
Same as before: anything `test:push` doesn't cover (the flaky-exclusion list in `vitest.push.config.ts` is unchanged — the same ~30 files that were excluded as flaky remain excluded; full suite still runs separately via `npm test` and is not part of CI gating). No new under-block introduced by this change.
|
|
35
|
+
|
|
36
|
+
One theoretical concern: if the shard hash assignment is not stable across vitest versions, a file could disappear from all shards after a vitest upgrade. This is vanishingly unlikely in practice because vitest's shard algorithm is documented and stable; the union is enforced by the CLI. If we ever suspect it, a simple sanity check is to run with `--shard=` omitted and compare the test count.
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## 3. Level-of-abstraction fit
|
|
41
|
+
|
|
42
|
+
**Is this at the right layer?**
|
|
43
|
+
|
|
44
|
+
Yes. File-level parallelism was disabled in `vitest.push.config.ts` because tests spawn HTTP servers, SQLite DBs, and real npm operations that collide when running in the same process pool (see commit `002a463`). Sharding at the CI-runner level (one process per shard on its own VM) sidesteps that collision entirely — each shard has its own ports, own filesystem, own npm cache — without re-enabling in-process parallelism. This is the right layer: the isolation problem was resource-contention across a single machine's pool; moving to multiple machines solves the resource contention without touching the isolation invariant.
|
|
45
|
+
|
|
46
|
+
Signal/authority lens: not applicable. CI test-pass is still an authority-grade signal computed over the same logical test set; we're just distributing the computation.
|
|
47
|
+
|
|
48
|
+
---
|
|
49
|
+
|
|
50
|
+
## 4. Signal vs authority compliance
|
|
51
|
+
|
|
52
|
+
**Required reference:** [docs/signal-vs-authority.md](../../docs/signal-vs-authority.md)
|
|
53
|
+
|
|
54
|
+
**Does this change hold blocking authority with brittle logic?**
|
|
55
|
+
|
|
56
|
+
- [ ] No — this change produces a signal consumed by an existing smart gate.
|
|
57
|
+
- [x] No — this change has no block/allow surface on message flow or agent behavior.
|
|
58
|
+
- [ ] Yes — but the logic is a smart gate with full conversational context.
|
|
59
|
+
- [ ] ⚠️ Yes, with brittle logic.
|
|
60
|
+
|
|
61
|
+
CI sharding is a runtime-distribution change. The test suite itself is unchanged — same authority (the full union of tests), same verdict logic (all must pass), just faster. Signal-vs-authority domain untouched.
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## 5. Interactions
|
|
66
|
+
|
|
67
|
+
**Does this interact with existing checks, recovery paths, or infrastructure?**
|
|
68
|
+
|
|
69
|
+
- **Shadowing:** `unit` is a `needs:` target for `integration`, `e2e`, `contract`, and `build`. GitHub Actions' default behavior when a `needs:` target is a matrix is to wait for **all** combinations to succeed before downstream jobs run. With 8 combinations (2 node × 4 shard), downstream jobs now wait for all 8 instead of 2. This is a stricter gate, not weaker. No shadowing introduced.
|
|
70
|
+
- **Double-fire:** n/a — the old 2-runner unit matrix was the only thing that ran tests. The new 8-runner matrix replaces it; total test-run count remains "once per Node × shard" (1 per runner).
|
|
71
|
+
- **Races:** each shard runs in its own ephemeral GitHub-hosted runner VM. No shared filesystem, no shared ports, no shared npm cache across shards. No race surface.
|
|
72
|
+
- **Feedback loops:** none. CI result feeds into PR-level required status checks, unchanged by this.
|
|
73
|
+
|
|
74
|
+
One behavioral nuance worth noting: with `fail-fast: false`, a PR that has genuinely broken tests will now consume 8 runners (all reporting the same failure) instead of 2. Cost is small (GitHub-hosted free minutes; we're not anywhere near the cap), and the upside is clearer diagnostic signal — if only shard-3 fails, the problem is isolated; if all 8 fail, the problem is universal.
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## 6. External surfaces
|
|
79
|
+
|
|
80
|
+
**Does this change anything visible outside the immediate code path?**
|
|
81
|
+
|
|
82
|
+
- **Other agents on the same machine:** no.
|
|
83
|
+
- **Other users of the install base:** no runtime behavior change. The shipped package is identical byte-for-byte.
|
|
84
|
+
- **External systems:** GitHub Actions runs more matrix combinations. Cost impact for public repos on GitHub-hosted free runners: negligible — Actions minutes are free for public repos. If a private fork is on a paid plan, the extra 6 runners per CI run will show up in billing; flag to callers who fork.
|
|
85
|
+
- **Persistent state:** none touched.
|
|
86
|
+
- **Status check names:** the job name becomes `Unit Tests (node 20, shard 1/4)` etc. instead of `Unit Tests (20)`. Only required status check in the branch ruleset today is `verify`; `Unit Tests` matrix jobs are **not** required checks (confirmed via `gh api repos/JKHeadley/instar/rules/branches/main` on 2026-04-19). So the rename does not break branch protection. If a watcher tool or dashboard specifically hardcoded `Unit Tests (20)` / `Unit Tests (22)` as an expected name, it would need updating — unlikely, but captured here.
|
|
87
|
+
|
|
88
|
+
---
|
|
89
|
+
|
|
90
|
+
## 7. Rollback cost
|
|
91
|
+
|
|
92
|
+
**If this turns out wrong in production, what's the back-out?**
|
|
93
|
+
|
|
94
|
+
Trivial. Revert the single `.github/workflows/ci.yml` change — restore the 2-runner matrix and the original `name:` / `run:` lines. No persistent state. No downstream code affected. Worst realistic "wrong" outcome: the shard hash algorithm has a pathological case that leaves one shard with most of the heavy tests, so the wall-clock win is smaller than projected. That's an optimization miss, not a correctness failure — still faster than pre-change, and the remediation is increasing shard count (6 or 8) rather than reverting. Full revert cost: one commit, no migration, no user impact.
|
|
95
|
+
|
|
96
|
+
---
|
|
97
|
+
|
|
98
|
+
## Conclusion
|
|
99
|
+
|
|
100
|
+
Pure CI distribution change with no runtime surface, no decision-point surface, no signal/authority interaction. Preserves the isolation invariant from commit `002a463` by sharding at the runner level instead of re-enabling in-process parallelism. Expected wall-clock win on unit-test phase: 9½ min → ~3 min; overall CI: ~12 min → ~5-6 min (next bottleneck likely integration/e2e, captured as follow-up). Rollback is one revert. Cleared to ship.
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## Evidence pointers
|
|
105
|
+
|
|
106
|
+
- Root cause of current serial execution: commit `002a463` (2026-02-28) — "fix(test): disable file parallelism to prevent lock contention". Confirms the isolation problem is real and resource-contention-based; sharding at the VM level is the right remediation.
|
|
107
|
+
- Shard algorithm: [vitest docs — CLI `--shard`](https://vitest.dev/guide/cli.html) — deterministic file-path hash distribution; union of all shards equals the full include set.
|
|
108
|
+
- Required-check topology: `gh api repos/JKHeadley/instar/rules/branches/main` (2026-04-19) shows only `verify` as a required context. Unit-test matrix jobs are observed green on recent PRs but are not ruleset-required.
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
# Side-Effects Review — Dashboard "Resume live output" reliability + always-visible while paused
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `dashboard-resume-live-scroll`
|
|
4
|
+
**Date:** `2026-04-18`
|
|
5
|
+
**Author:** `echo`
|
|
6
|
+
**Second-pass reviewer:** `not required — pure UI timing fix, no gate/sentinel/watchdog/session-lifecycle surface`
|
|
7
|
+
|
|
8
|
+
## Summary of the change
|
|
9
|
+
|
|
10
|
+
The dashboard's "Resume live output" button (and the two sibling resume paths that
|
|
11
|
+
fire on scroll-to-bottom and wheel-down) did not reliably snap the xterm viewport
|
|
12
|
+
to the bottom after being clicked. The user still landed mid-history and had to
|
|
13
|
+
scroll manually to see live output — defeating the purpose of the button.
|
|
14
|
+
|
|
15
|
+
Root cause: `term.write(data)` is asynchronous — xterm parses and renders the
|
|
16
|
+
write on a microtask. The old code called `term.scrollToBottom()` either
|
|
17
|
+
immediately after the write or inside a `requestAnimationFrame`. Neither
|
|
18
|
+
guarantees the buffer has been populated, so `scrollToBottom()` snapped against
|
|
19
|
+
a stale `baseY` and landed above the new output.
|
|
20
|
+
|
|
21
|
+
Fix: use xterm's write-completion callback — `term.write(data, () => { term.scrollToBottom(); })`.
|
|
22
|
+
The callback fires after the write is flushed, so the scroll operates on the
|
|
23
|
+
final buffer state.
|
|
24
|
+
|
|
25
|
+
## Secondary change — button visibility mirrors follow state
|
|
26
|
+
|
|
27
|
+
The prior behavior only showed the "▼ Resume live output" button when new
|
|
28
|
+
output happened to arrive while the user was scrolled up — via `showResumeButton()`
|
|
29
|
+
inside `renderTerminalOutput`'s not-following branch. If the user scrolled up in
|
|
30
|
+
an idle session (no new output arriving), the button never appeared, and there
|
|
31
|
+
was no UI control to jump back to live.
|
|
32
|
+
|
|
33
|
+
Fix: `term.onScroll` now surfaces the button the moment the user scrolls up
|
|
34
|
+
(`!atBottom && userIsFollowing` → flip false + `showResumeButton()`), and hides
|
|
35
|
+
it the moment they return to the bottom by any means (`atBottom && !userIsFollowing`
|
|
36
|
+
→ flip true + `hideResumeButton()`). The wheel-down resume path hides the button
|
|
37
|
+
for the same reason. Session-switch reset calls `hideResumeButton()` so a
|
|
38
|
+
carry-over button from a prior session doesn't mislead.
|
|
39
|
+
|
|
40
|
+
The button is now the always-available "take me back to live" control, not a
|
|
41
|
+
contingent reaction to incoming output.
|
|
42
|
+
|
|
43
|
+
Files touched:
|
|
44
|
+
|
|
45
|
+
- `dashboard/index.html` — two overlapping changes:
|
|
46
|
+
- Three resume paths use xterm's write-completion callback:
|
|
47
|
+
1. The button `onclick` handler.
|
|
48
|
+
2. The `term.onScroll` handler's auto-resume branch.
|
|
49
|
+
3. The `xtermViewport` wheel handler's auto-resume branch.
|
|
50
|
+
The button handler retains `container.scrollIntoView({block:'end'})` in a
|
|
51
|
+
shared `snapToBottom` closure so both the pending-data and no-pending-data
|
|
52
|
+
branches do the same thing.
|
|
53
|
+
- Button visibility now mirrors `!userIsFollowing` from every path that
|
|
54
|
+
flips the flag: scroll-up in `onScroll`, scroll-to-bottom in `onScroll`,
|
|
55
|
+
wheel-down resume, and session-switch reset.
|
|
56
|
+
- `tests/unit/dashboard-resumeLive.test.ts` — new. HTML-inspection regression
|
|
57
|
+
test, consistent with the existing `dashboard-*.test.ts` pattern. Ten tests,
|
|
58
|
+
split across two describe blocks: (a) scroll-after-write invariants — locks
|
|
59
|
+
in the callback form on all three paths and bans the old
|
|
60
|
+
`term.write(data); requestAnimationFrame(() => term.scrollToBottom())` shape;
|
|
61
|
+
(b) button-visibility invariants — asserts show on scroll-up, hide on
|
|
62
|
+
scroll-to-bottom / wheel-resume / session switch.
|
|
63
|
+
|
|
64
|
+
## 1. Over-block
|
|
65
|
+
|
|
66
|
+
N/A — no block/allow decision exists in this change. The only behavior altered
|
|
67
|
+
is the timing of a DOM scroll call relative to an xterm write.
|
|
68
|
+
|
|
69
|
+
## 2. Under-block
|
|
70
|
+
|
|
71
|
+
N/A — see Over-block.
|
|
72
|
+
|
|
73
|
+
## 3. Level-of-abstraction fit
|
|
74
|
+
|
|
75
|
+
The fix lives at the exact layer where the bug lives: dashboard JavaScript
|
|
76
|
+
interacting with xterm.js. xterm's documented API for scheduling work after
|
|
77
|
+
a write is the write callback; using `requestAnimationFrame` was a workaround
|
|
78
|
+
that guessed at timing instead of using the library's native hook. The fix
|
|
79
|
+
moves us from guessing to the library-native pattern.
|
|
80
|
+
|
|
81
|
+
There is no higher-level authority that should own terminal-viewport scrolling
|
|
82
|
+
— this is a leaf UI behavior. There is no lower layer to delegate to — xterm
|
|
83
|
+
itself is the layer.
|
|
84
|
+
|
|
85
|
+
## 4. Signal-vs-authority compliance
|
|
86
|
+
|
|
87
|
+
No decision point touched. This is not a gate, filter, sentinel, watchdog, or
|
|
88
|
+
dispatcher. No brittle logic holds blocking authority. The principle does not
|
|
89
|
+
apply. Documented in Phase 1.
|
|
90
|
+
|
|
91
|
+
## 5. Interactions
|
|
92
|
+
|
|
93
|
+
- **`renderTerminalOutput` (the steady-state write path):** Still uses the
|
|
94
|
+
bare `term.write(data); term.scrollToBottom();` form. That path runs only
|
|
95
|
+
when `userIsFollowing === true` — xterm's default behavior is to auto-track
|
|
96
|
+
the bottom when the viewport is already there, so the follow-up
|
|
97
|
+
`scrollToBottom()` is essentially redundant and harmless. Not touched.
|
|
98
|
+
- **`term.onScroll` flag-flipping:** The scroll event fires during
|
|
99
|
+
`term.write()` as the buffer grows. If the write callback runs after
|
|
100
|
+
`scrollToBottom()`, the onScroll handler sees `atBottom === true` and
|
|
101
|
+
keeps `userIsFollowing === true`. No race with flag-flipping introduced.
|
|
102
|
+
- **Concurrent WS output:** If a new frame arrives via WebSocket during the
|
|
103
|
+
click handler, it calls `renderTerminalOutput` with `userIsFollowing === true`
|
|
104
|
+
(we just set it). That path does its own `term.clear()` + `term.write` +
|
|
105
|
+
`scrollToBottom()`, which will overwrite whatever our click handler was
|
|
106
|
+
mid-flight. Net effect: user ends up at bottom either way. No deadlock, no
|
|
107
|
+
torn state.
|
|
108
|
+
- **Infinite-scroll history loader:** Unaffected — uses `buf.viewportY <= 10`,
|
|
109
|
+
which is a separate concern from the bottom-snap logic.
|
|
110
|
+
|
|
111
|
+
## 6. External surfaces
|
|
112
|
+
|
|
113
|
+
None. This is client-side JavaScript in the dashboard HTML, served to the
|
|
114
|
+
user's browser. No other agents, no other users, no other systems observe the
|
|
115
|
+
change. No server API touched. No persisted state touched.
|
|
116
|
+
|
|
117
|
+
## 7. Rollback cost
|
|
118
|
+
|
|
119
|
+
Trivial: `git revert <commit>`. No data migration, no state repair, no release
|
|
120
|
+
coordination. Dashboard is static HTML served by the instar server; a revert
|
|
121
|
+
commit + server restart puts the old code back in front of the user
|
|
122
|
+
immediately.
|
|
123
|
+
|
|
124
|
+
## Verification
|
|
125
|
+
|
|
126
|
+
- HTML-inspection regression test passing (`tests/unit/dashboard-resumeLive.test.ts`, 6 tests).
|
|
127
|
+
- Full dashboard test suite passing (`dashboard-*.test.ts`, 20 tests total).
|
|
128
|
+
- Manual browser verification: deferred to user acceptance — reproducing the
|
|
129
|
+
bug end-to-end requires a live session with pending output and a user
|
|
130
|
+
scrolled up, which is Justin's original reporting path. The mechanical fix
|
|
131
|
+
is narrow enough (swap `term.write(data); scroll` → `term.write(data, scroll)`)
|
|
132
|
+
that the HTML regression is the load-bearing guarantee.
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
# Side-Effects Review — Pre-push smoke tier (changed-files only)
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `pre-push-smoke-tier`
|
|
4
|
+
**Date:** `2026-04-19`
|
|
5
|
+
**Author:** `echo`
|
|
6
|
+
**Second-pass reviewer:** `not required`
|
|
7
|
+
|
|
8
|
+
## Summary of the change
|
|
9
|
+
|
|
10
|
+
`.husky/pre-push` now runs `npm run test:smoke` (new script:
|
|
11
|
+
`vitest run --config vitest.push.config.ts --changed origin/main`) instead of
|
|
12
|
+
`npm run test:push`. The new script executes the same excluded/included set
|
|
13
|
+
and same `fileParallelism: false` isolation — the only difference is that
|
|
14
|
+
vitest's `--changed origin/main` filter restricts the run to tests whose
|
|
15
|
+
files (or transitive imports) are in the diff vs. origin/main. The
|
|
16
|
+
pre-push gate (`scripts/pre-push-gate.js` — NEXT.md / version / side-effects
|
|
17
|
+
artifact / contract-evidence / source-without-tests checks) runs first,
|
|
18
|
+
unchanged. Two escape hatches: `INSTAR_PRE_PUSH_FULL=1` (run full push
|
|
19
|
+
suite locally) and `INSTAR_PRE_PUSH_SKIP=1` (skip tests entirely; CI is
|
|
20
|
+
the only gate). Full suite continues to run in CI across 8 sharded
|
|
21
|
+
runners on every PR; CI remains the authority for merge.
|
|
22
|
+
|
|
23
|
+
Files touched:
|
|
24
|
+
|
|
25
|
+
- `package.json` — adds `test:smoke` script; bumps version 0.28.59 → 0.28.60.
|
|
26
|
+
- `.husky/pre-push` — switches to `test:smoke` by default, adds env-var
|
|
27
|
+
escape hatches, keeps the 2-attempt retry loop and the pre-push gate
|
|
28
|
+
exactly as before.
|
|
29
|
+
- `upgrades/NEXT.md` — upgrade guide (new file at release time).
|
|
30
|
+
- `upgrades/side-effects/pre-push-smoke-tier.md` — this review.
|
|
31
|
+
|
|
32
|
+
## Decision-point inventory
|
|
33
|
+
|
|
34
|
+
- `.husky/pre-push` — **modify** — chooses which test script runs based on
|
|
35
|
+
env vars and falls back to smoke tier by default.
|
|
36
|
+
- `package.json scripts.test:smoke` — **add** — new script, thin wrapper
|
|
37
|
+
over existing push config with `--changed origin/main`.
|
|
38
|
+
|
|
39
|
+
No runtime / agent-behavior decision points touched. Strictly contributor-side
|
|
40
|
+
git hook behavior.
|
|
41
|
+
|
|
42
|
+
---
|
|
43
|
+
|
|
44
|
+
## 1. Over-block
|
|
45
|
+
|
|
46
|
+
**What legitimate inputs does this change reject that it shouldn't?**
|
|
47
|
+
|
|
48
|
+
On the runtime / message surface: no block/allow change. The pre-push gate
|
|
49
|
+
is a contributor-side hook, not a runtime gate, so over-block doesn't apply
|
|
50
|
+
in the agent-behavior sense.
|
|
51
|
+
|
|
52
|
+
On the contributor surface: the smoke tier runs *fewer* tests than the full
|
|
53
|
+
push suite, so it will **accept pushes that the full suite would have
|
|
54
|
+
rejected** — the opposite of over-blocking. See Under-block below.
|
|
55
|
+
|
|
56
|
+
There is one narrow over-block possibility: if `origin/main` is stale (user
|
|
57
|
+
hasn't fetched recently), the `--changed` diff may include files that are
|
|
58
|
+
already merged, running more tests than strictly needed. We mitigate with
|
|
59
|
+
`git fetch --quiet origin main` inside the hook. If the fetch fails (offline
|
|
60
|
+
push), the diff falls back to whatever the local `origin/main` ref points
|
|
61
|
+
at — worst case runs a few extra tests, still fast.
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
## 2. Under-block
|
|
66
|
+
|
|
67
|
+
**What failure modes does this still miss?**
|
|
68
|
+
|
|
69
|
+
This is the real surface to review. Moving from "full suite" to "tests
|
|
70
|
+
affected by changed files" trades thoroughness for speed. Specifically:
|
|
71
|
+
|
|
72
|
+
1. **Regression in unchanged code** — if your change to file A breaks
|
|
73
|
+
file B via some runtime-only coupling that vitest's module graph
|
|
74
|
+
doesn't see (e.g., a serialization format that both sides implement
|
|
75
|
+
independently), the smoke tier will miss it. CI's full suite on PR
|
|
76
|
+
will catch it before merge. Net risk: the contributor experiences
|
|
77
|
+
"passed locally, failed in CI" more often. Cost: one extra CI round
|
|
78
|
+
trip. Benefit: ~9 minutes saved per push across all pushes that don't
|
|
79
|
+
break anything (i.e., the vast majority).
|
|
80
|
+
|
|
81
|
+
2. **Config/global-state files** — if a change touches a file imported by
|
|
82
|
+
most tests (e.g., a vitest global setup, a shared fixture builder),
|
|
83
|
+
`--changed` will correctly include the full test set. No extra risk
|
|
84
|
+
here; the mechanism is self-balancing.
|
|
85
|
+
|
|
86
|
+
3. **No-change pushes** — pure doc or comment changes produce 0 tests,
|
|
87
|
+
which is correct. The pre-push gate still enforces NEXT.md presence
|
|
88
|
+
and side-effects artifact presence independently, so doc pushes that
|
|
89
|
+
claim a fix/feature still require the corresponding artifact.
|
|
90
|
+
|
|
91
|
+
4. **Stale local `origin/main`** — if the user hasn't fetched for days,
|
|
92
|
+
the diff could be larger than reality, but never smaller. Under-block
|
|
93
|
+
is bounded: you cannot *miss* a file this way, only over-include.
|
|
94
|
+
We still proactively `git fetch --quiet origin main` inside the hook
|
|
95
|
+
to keep it tight.
|
|
96
|
+
|
|
97
|
+
The accepted residual risk is (1). Mitigation: CI is the merge authority
|
|
98
|
+
(per `docs/signal-vs-authority.md`); the pre-push gate is downgraded from
|
|
99
|
+
"implicit authority" to "explicit signal" by this change, which matches
|
|
100
|
+
the architectural principle.
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## 3. Level-of-abstraction fit
|
|
105
|
+
|
|
106
|
+
**Is this at the right layer?**
|
|
107
|
+
|
|
108
|
+
Yes. The correct layer is the pre-push hook itself (the boundary where
|
|
109
|
+
we trade speed for confidence on the contributor side). Alternatives:
|
|
110
|
+
|
|
111
|
+
- **Change vitest.push.config.ts to be smaller** — wrong layer. That
|
|
112
|
+
config defines "the push suite"; the smoke tier is a *different use*
|
|
113
|
+
of that same config, selected per-push based on diff size. Keeping
|
|
114
|
+
two scripts that share one config is cleaner than two configs that
|
|
115
|
+
overlap.
|
|
116
|
+
- **Change CI to run less** — wrong direction. CI is the authority; it
|
|
117
|
+
must remain exhaustive or the invariant breaks.
|
|
118
|
+
- **Tag some tests as "smoke" and run only those** — brittle taxonomy
|
|
119
|
+
that humans would have to maintain. Vitest's `--changed` computes
|
|
120
|
+
affected tests from the module graph automatically; no taxonomy drift.
|
|
121
|
+
|
|
122
|
+
Signal-vs-authority reference: `docs/signal-vs-authority.md`. The new
|
|
123
|
+
pre-push hook is a signal consumed by the contributor (and, if they
|
|
124
|
+
push anyway via `INSTAR_PRE_PUSH_SKIP=1`, CI is the binding authority).
|
|
125
|
+
The old hook *was* acting as authority by blocking pushes that CI
|
|
126
|
+
would have caught anyway — same verdict, earlier but slower. Moving
|
|
127
|
+
the authority to CI and keeping a fast signal locally is the correct
|
|
128
|
+
decomposition.
|
|
129
|
+
|
|
130
|
+
---
|
|
131
|
+
|
|
132
|
+
## 4. Signal vs authority compliance
|
|
133
|
+
|
|
134
|
+
**Required reference:** [docs/signal-vs-authority.md](../../docs/signal-vs-authority.md)
|
|
135
|
+
|
|
136
|
+
**Does this change hold blocking authority with brittle logic?**
|
|
137
|
+
|
|
138
|
+
- [x] No — this change produces a signal consumed by an existing smart gate.
|
|
139
|
+
- [ ] No — this change has no block/allow surface on message flow or agent behavior.
|
|
140
|
+
- [ ] Yes — but the logic is a smart gate with full conversational context.
|
|
141
|
+
- [ ] ⚠️ Yes, with brittle logic.
|
|
142
|
+
|
|
143
|
+
The pre-push hook is now explicitly a *signal* — it fails the push fast
|
|
144
|
+
when a clearly-affected test regressed, and otherwise lets CI act as
|
|
145
|
+
authority. The `INSTAR_PRE_PUSH_SKIP=1` escape hatch formalizes this:
|
|
146
|
+
contributors can bypass the signal; they cannot bypass CI on the PR.
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
## 5. Interactions
|
|
151
|
+
|
|
152
|
+
**Does this interact with existing checks, recovery paths, or infrastructure?**
|
|
153
|
+
|
|
154
|
+
- **Pre-push gate (NEXT.md / version / side-effects / contract evidence):**
|
|
155
|
+
runs *before* the test tier and is unchanged. Smoke tier substitution
|
|
156
|
+
happens after that gate passes. No interaction risk — orthogonal
|
|
157
|
+
concerns.
|
|
158
|
+
- **Retry loop:** the 2-attempt retry is preserved, now wrapped around
|
|
159
|
+
`test:smoke`. Same behavior on flaky failures; just shorter because
|
|
160
|
+
the retry suite is smaller.
|
|
161
|
+
- **CI (`ci.yml`):** unchanged. Still runs 8 sharded unit-test matrix +
|
|
162
|
+
integration + e2e + build + type-check on every PR. CI remains the
|
|
163
|
+
authority on merge-readiness.
|
|
164
|
+
- **Shadowing:** does not exist. CI is not a `needs:` target of the
|
|
165
|
+
pre-push hook or vice versa; they operate on different events
|
|
166
|
+
(contributor push vs. GitHub Actions PR event).
|
|
167
|
+
- **Double-fire:** if a contributor has the smoke tier pass and pushes,
|
|
168
|
+
CI re-runs the full suite — intentional double-gate (fast local
|
|
169
|
+
signal, authoritative remote). Not wasted work; that's the design.
|
|
170
|
+
- **Races:** none. Contributor-side hook, synchronous, no shared state.
|
|
171
|
+
- **Feedback loops:** if `origin/main` ref moves mid-push, the diff
|
|
172
|
+
window shifts but the push is a single event; the diff is snapshotted
|
|
173
|
+
at hook-start time.
|
|
174
|
+
|
|
175
|
+
---
|
|
176
|
+
|
|
177
|
+
## 6. External surfaces
|
|
178
|
+
|
|
179
|
+
**Does this change anything visible outside the immediate code path?**
|
|
180
|
+
|
|
181
|
+
- **Other agents on the same machine:** no. The git hook runs only in
|
|
182
|
+
the contributor's shell during `git push`.
|
|
183
|
+
- **Other users of the install base:** no runtime change. The shipped
|
|
184
|
+
npm package is unchanged. Only the contributor-side hook installed
|
|
185
|
+
by `npm install` is affected, and only for people working *in* the
|
|
186
|
+
instar repo — not consumers of the `instar` package.
|
|
187
|
+
- **External systems:** no.
|
|
188
|
+
- **Persistent state:** none touched.
|
|
189
|
+
- **CI minutes consumption:** unchanged — CI runs the same matrix.
|
|
190
|
+
If anything, fewer "contributor runs full suite locally, then CI
|
|
191
|
+
runs it again" iterations means slightly less duplicated load on
|
|
192
|
+
developer machines.
|
|
193
|
+
|
|
194
|
+
---
|
|
195
|
+
|
|
196
|
+
## 7. Rollback cost
|
|
197
|
+
|
|
198
|
+
**If this turns out wrong in production, what's the back-out?**
|
|
199
|
+
|
|
200
|
+
Trivial. Revert the commit that changes `.husky/pre-push` and
|
|
201
|
+
`package.json`. `test:push` is unchanged, so the pre-push hook
|
|
202
|
+
reverts to running the full suite exactly as it did before. No
|
|
203
|
+
migration, no state cleanup, no user-visible impact (since the
|
|
204
|
+
npm package is unaffected).
|
|
205
|
+
|
|
206
|
+
Partial rollback also available without reverting: set the env var
|
|
207
|
+
`INSTAR_PRE_PUSH_FULL=1` globally (e.g., in the user's shell rc) to
|
|
208
|
+
restore the old blocking behavior without changing any code.
|
|
209
|
+
|
|
210
|
+
---
|
|
211
|
+
|
|
212
|
+
## Conclusion
|
|
213
|
+
|
|
214
|
+
Moves the pre-push test gate from "implicit authority" (slow, exhaustive,
|
|
215
|
+
blocks every push) to "fast signal" (runs only tests affected by the
|
|
216
|
+
diff, CI remains the merge authority). Matches the signal-vs-authority
|
|
217
|
+
architectural principle. Preserves the exclude list, the isolation
|
|
218
|
+
invariant (`fileParallelism: false`), the NEXT.md / version / side-effects
|
|
219
|
+
gate, the retry loop, and the full suite in CI — all authorities stay
|
|
220
|
+
authoritative. Expected wall-clock on a typical small push: ~9 min → <1
|
|
221
|
+
min. Escape hatches for the edge cases. Rollback is one revert. Cleared
|
|
222
|
+
to ship.
|
|
223
|
+
|
|
224
|
+
---
|
|
225
|
+
|
|
226
|
+
## Evidence pointers
|
|
227
|
+
|
|
228
|
+
- `docs/signal-vs-authority.md` — the architectural principle this change
|
|
229
|
+
aligns the hook with.
|
|
230
|
+
- Vitest `--changed` mode docs:
|
|
231
|
+
<https://vitest.dev/guide/cli.html#changed> — deterministic, based on
|
|
232
|
+
module graph, safe to compose with `--config`.
|
|
233
|
+
- CI authority chain: `.github/workflows/ci.yml` — 8-shard unit matrix
|
|
234
|
+
+ integration + e2e + build + type-check; required by branch
|
|
235
|
+
protection on main (per the ruleset update landed with PR #69).
|