instar 1.3.1152 → 1.3.1154

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.
@@ -2,7 +2,7 @@
2
2
  "schemaVersion": 1,
3
3
  "generatedFrom": "source-tree",
4
4
  "registrySha256": "81b53363a440e832672618965540b3e507ae0d93adcc67ec2b93daf7933b3ab4",
5
- "packageVersion": "1.3.1152",
5
+ "packageVersion": "1.3.1154",
6
6
  "guards": [
7
7
  {
8
8
  "ref": "docs/audits/phase-b/f10-triage.md",
@@ -1,5 +1,5 @@
1
1
  {
2
- "sha256": "cb5b39009ce485f47b3010b819bd2a9c0dc4e2294204deaef41bf69db200fd41",
2
+ "sha256": "49844097b7495749a584409f6b28788b61127591a3f707a03ef9622e69bf1dd1",
3
3
  "registrySha256": "81b53363a440e832672618965540b3e507ae0d93adcc67ec2b93daf7933b3ab4",
4
- "packageVersion": "1.3.1152"
4
+ "packageVersion": "1.3.1154"
5
5
  }
@@ -2,5 +2,5 @@
2
2
  "sha256": "81b53363a440e832672618965540b3e507ae0d93adcc67ec2b93daf7933b3ab4",
3
3
  "articleCount": 88,
4
4
  "generatedFrom": "docs/STANDARDS-REGISTRY.md",
5
- "packageVersion": "1.3.1152"
5
+ "packageVersion": "1.3.1154"
6
6
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "instar",
3
- "version": "1.3.1152",
3
+ "version": "1.3.1154",
4
4
  "description": "Coherence infrastructure for self-evolving AI agents — on the Claude Code or Codex subscription you already have.",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -78,6 +78,61 @@ const ALLOW = /lint-allow-sync-spawn:/;
78
78
  const FUNNELED = /\bwithSyncOp\s*\(/;
79
79
 
80
80
  const inScanDir = (p) => SCAN_DIRS.some((d) => p === d || p.startsWith(d + '/'));
81
+
82
+ /**
83
+ * Names this file has bound to a raw sync spawn, so `ex(...)` is seen the same
84
+ * as `execFileSync(...)`. Two forms, both ordinary code rather than evasions:
85
+ *
86
+ * import { execFileSync as run } from 'node:child_process'; // renamed import
87
+ * const ex = execFileSync; // local alias
88
+ *
89
+ * Measured before this was added: BOTH walked past the check while the plain
90
+ * form was caught, and NEITHER appears anywhere in the scanned directories
91
+ * today — so this is a pure forward ratchet with no baseline to grow.
92
+ *
93
+ * DELIBERATELY NOT COLLECTED: `const ex = <something>.execFileSync`. The
94
+ * VIOLATION regex excludes a dot-prefixed name on purpose, and that exclusion
95
+ * was measured to be RIGHT: all 14 namespace-form occurrences in the scanned
96
+ * dirs are either calls through `SafeGitExecutor` (the audited git funnel, 13
97
+ * of them) or sit inside a generated hook script's template literal, which runs
98
+ * in its own process and cannot block this event loop. Widening to dot-prefixed
99
+ * names would flag the funnel itself. Recorded here so it is not re-litigated.
100
+ */
101
+ function collectSyncSpawnAliases(content) {
102
+ const names = new Set();
103
+ const SPAWNS = '(?:spawnSync|execSync|execFileSync)';
104
+
105
+ // import { execFileSync as run } from 'node:child_process'
106
+ const importRe = new RegExp(
107
+ String.raw`import\s*\{([^}]*)\}\s*from\s*['"\`](?:node:)?child_process['"\`]`,
108
+ 'g'
109
+ );
110
+ let m;
111
+ while ((m = importRe.exec(content)) !== null) {
112
+ for (const part of m[1].split(',')) {
113
+ const bit = part.trim().match(new RegExp(String.raw`^${SPAWNS}\s+as\s+([A-Za-z_$][\w$]*)$`));
114
+ if (bit) names.add(bit[1]);
115
+ }
116
+ }
117
+
118
+ // const ex = execFileSync; (bare RHS only — see the dot note above)
119
+ const aliasRe = new RegExp(
120
+ String.raw`\b(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*${SPAWNS}\s*(?=[;,\n)])`,
121
+ 'g'
122
+ );
123
+ while ((m = aliasRe.exec(content)) !== null) names.add(m[1]);
124
+
125
+ return names;
126
+ }
127
+
128
+ /** A call-shape matcher for the collected names, or null when there are none. */
129
+ function aliasCallRegex(names) {
130
+ if (!names.size) return null;
131
+ const alt = [...names].map((n) => n.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')).join('|');
132
+ // Same dot-exclusion as VIOLATION: `obj.ex(...)` is a method on something
133
+ // else, not the bound spawn.
134
+ return new RegExp(String.raw`(?<![.\w])(?:${alt})\s*\(`);
135
+ }
81
136
  const normalize = (p) => p.split(path.sep).join('/');
82
137
 
83
138
  function listFiles() {
@@ -133,12 +188,15 @@ function collectHits() {
133
188
  continue;
134
189
  }
135
190
  const lines = content.split('\n');
191
+ // Names this file has bound to a raw sync spawn. Collected up-front so a
192
+ // binding that appears BELOW the function using it is still resolved.
193
+ const aliasRe = aliasCallRegex(collectSyncSpawnAliases(content));
136
194
  const seenLineText = new Map(); // trimmed-line-text → occurrence count so far
137
195
  for (let i = 0; i < lines.length; i++) {
138
196
  const raw = lines[i];
139
197
  const trimmed = raw.trimStart();
140
198
  if (/^(\/\/|\*|\/\*)/.test(trimmed)) continue; // comment-only mention
141
- if (!VIOLATION.test(raw)) continue;
199
+ if (!VIOLATION.test(raw) && !(aliasRe && aliasRe.test(raw))) continue;
142
200
  // FUNNELED: a sync spawn wrapped by withSyncOp(...) on the same line is the required
143
201
  // pattern (the marker sees it) — allowed unconditionally, never grandfathered/baselined.
144
202
  if (FUNNELED.test(raw)) continue;
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "$schema": "./builtin-manifest.schema.json",
3
3
  "schemaVersion": 1,
4
- "generatedAt": "2026-08-15T01:02:42.043Z",
5
- "instarVersion": "1.3.1152",
4
+ "generatedAt": "2026-08-15T02:06:17.465Z",
5
+ "instarVersion": "1.3.1154",
6
6
  "entryCount": 202,
7
7
  "entries": {
8
8
  "hook:session-start": {
@@ -2,7 +2,7 @@
2
2
  "schemaVersion": 1,
3
3
  "generatedFrom": "source-tree",
4
4
  "registrySha256": "81b53363a440e832672618965540b3e507ae0d93adcc67ec2b93daf7933b3ab4",
5
- "packageVersion": "1.3.1152",
5
+ "packageVersion": "1.3.1154",
6
6
  "guards": [
7
7
  {
8
8
  "ref": "docs/audits/phase-b/f10-triage.md",
@@ -1,5 +1,5 @@
1
1
  {
2
- "sha256": "cb5b39009ce485f47b3010b819bd2a9c0dc4e2294204deaef41bf69db200fd41",
2
+ "sha256": "49844097b7495749a584409f6b28788b61127591a3f707a03ef9622e69bf1dd1",
3
3
  "registrySha256": "81b53363a440e832672618965540b3e507ae0d93adcc67ec2b93daf7933b3ab4",
4
- "packageVersion": "1.3.1152"
4
+ "packageVersion": "1.3.1154"
5
5
  }
@@ -2,5 +2,5 @@
2
2
  "sha256": "81b53363a440e832672618965540b3e507ae0d93adcc67ec2b93daf7933b3ab4",
3
3
  "articleCount": 88,
4
4
  "generatedFrom": "docs/STANDARDS-REGISTRY.md",
5
- "packageVersion": "1.3.1152"
5
+ "packageVersion": "1.3.1154"
6
6
  }
@@ -0,0 +1,62 @@
1
+ # Upgrade Guide — vNEXT
2
+
3
+ <!-- assembled-by: assemble-next-md -->
4
+ <!-- bump: patch -->
5
+
6
+ ## What Changed
7
+
8
+ The atomic-writes consistency test could not detect the defect it exists to detect.
9
+
10
+ Measured rather than argued: a bare `fs.writeFileSync` of durable session state, inserted into
11
+ `saveSession` — a DECLARED method of a DECLARED module — passed all 21 of its assertions.
12
+
13
+ Three causes, all fixed:
14
+
15
+ 1. **Scoping.** `inSaveMethod` was set when a method NAME appeared on a line and never reset, while
16
+ `hasWriteFile`/`hasRename` were re-zeroed at each occurrence. Only the window from the LAST name
17
+ mention to EOF reached the assertion — 125 of 617 lines (20%) in `StateManager.ts`, leaving three
18
+ of its four declared methods structurally unreachable. Bodies are now brace-matched per method.
19
+ 2. **File-scope substring checks.** `source.includes('renameSync')` and `source.includes('.tmp')` are
20
+ satisfied by one occurrence anywhere in the file, comments included. Pairing is now per body.
21
+ 3. **Silent declaration rot.** A missing file was `it.skip`ped and a missing method never set the
22
+ flag, so a rename dropped a module out of coverage without a sound. Both are failures now — and
23
+ enabling that found two immediately: `saveState` has ZERO occurrences in `StateManager.ts` and in
24
+ `QuotaTracker.ts`. QuotaTracker's real writer, `updateState()`, is atomic and had never been
25
+ verified by this test. The declared list is corrected here.
26
+
27
+ **Delegation.** `StateManager` funnels every write through a private `atomicWrite()`. A naive
28
+ per-method rule would have failed the best-written module in the set for being well written, so one
29
+ level of `this.helper()` delegation is resolved and the funnel is what gets verified — which means
30
+ `StateManager`'s writes are now genuinely checked, where before nothing checked them.
31
+
32
+ Added `tests/helpers/atomicWriteScope.ts` (`stripComments`, `methodBodies`, `delegateTargets`,
33
+ `classifyMethod`) and `tests/unit/atomic-write-scope.test.ts`.
34
+
35
+ **Declared open in the source:** the module list is curated at 7 entries and says nothing about the
36
+ hundreds of other files under `src/` that call `writeFileSync`; delegation resolves one level within
37
+ one file; only `writeFileSync`/`renameSync` are recognised.
38
+
39
+ **The production code is atomic** everywhere the check now looks. This fixes a weak instrument, not a
40
+ live corruption bug.
41
+
42
+ ## What to Tell Your User
43
+
44
+ None — internal change (no user-facing surface).
45
+
46
+ ## Summary of New Capabilities
47
+
48
+ None — internal change (no user-facing surface).
49
+
50
+ ## Evidence
51
+
52
+ - `tests/unit/atomic-write-scope.test.ts` — 18/18 green (6 defect cases, 6 over-block controls, 6 primitives).
53
+ - `tests/unit/atomic-writes-consistency.test.ts` — 32/32 green against the real tree.
54
+ - **Both-directions proof in ONE worktree**, shipped check restored from git alongside the new one so
55
+ subject and control share a boundary: shipped → **21/21 PASSED** against the mutation; new →
56
+ **1 failed / 31 passed**, naming file, method, deciding body and line. `src/core/StateManager.ts`
57
+ restored byte-exact (sha match, zero markers, zero stray files).
58
+ - Six over-block controls pass under BOTH behaviours — genuine funnel delegation, in-body
59
+ tmp-then-rename, a method that writes nothing, a commented-out write, a call site vs a declaration,
60
+ and worst-verdict-wins across duplicate declarations.
61
+ - `tsc --noEmit` exit 0 — with the boundary stated: `tsconfig.json` excludes `tests`, so that run says
62
+ nothing about these files.
@@ -0,0 +1,65 @@
1
+ # Upgrade Guide — vNEXT
2
+
3
+ <!-- assembled-by: assemble-next-md -->
4
+ <!-- bump: patch -->
5
+
6
+ ## What Changed
7
+
8
+ `scripts/lint-sync-subprocess-chokepoint.js` — the forward ratchet that keeps raw
9
+ synchronous subprocess spawns out of the runtime hot path, so a blocked-but-alive
10
+ server is never mistaken for a dead one — now resolves bound names.
11
+
12
+ It matched the spawn NAME on the call line, so two ordinary forms walked past
13
+ while the plain call was caught. Measured with a positive control firing in the
14
+ same run:
15
+
16
+ ```ts
17
+ import { execFileSync as run } from 'node:child_process'; run(...); // exit 0 — EVADED
18
+ const ex = execFileSync; ex(...); // exit 0 — EVADED
19
+ ```
20
+
21
+ A renamed import is not an evasion; it is how a name collision gets resolved.
22
+
23
+ **Neither form appears anywhere in the scanned directories today** (0 local
24
+ aliases, 0 renamed imports, against a control of 53 files carrying plain named
25
+ imports), so this is a pure forward ratchet: the frozen baseline does not grow
26
+ and nothing existing can break.
27
+
28
+ **Scope reversed by measurement.** `VIOLATION` also excludes a DOT-prefixed name,
29
+ and there are 14 namespace-form occurrences in the scanned dirs — "14 invisible
30
+ blocking spawns" would have been the headline. Counting what they *are*:
31
+ **13 are `SafeGitExecutor.execSync(`**, i.e. calls THROUGH the audited git funnel
32
+ (flagging them would report correct use of the funnel as a bypass of it), and the
33
+ **1 remaining sits inside a generated hook script's template literal**, which runs
34
+ in its own process and cannot block this event loop. All 14 exclusions are
35
+ correct; the dot-exclusion is left alone and pinned by two tests so it is not
36
+ "fixed" later.
37
+
38
+ Added `collectSyncSpawnAliases()` (renamed imports from `(node:)child_process`,
39
+ and bare local aliases) and `aliasCallRegex()` (carrying the same dot-exclusion as
40
+ the original rule). `VIOLATION`, `FUNNELED`, `ALLOW`, the baseline format and the
41
+ exit codes are unchanged.
42
+
43
+ ## What to Tell Your User
44
+
45
+ None — internal change (no user-facing surface).
46
+
47
+ ## Summary of New Capabilities
48
+
49
+ None — internal change (no user-facing surface).
50
+
51
+ ## Evidence
52
+
53
+ - `tests/unit/sync-spawn-alias-resolution.test.ts` — 12/12 green.
54
+ - **Negative control: 4 of 12 fail** against the shipped lint (exactly the four
55
+ defect cases). The other 8 pass both ways and are the controls. Script restored
56
+ byte-exact after the control.
57
+ - Six anti-over-block controls, because this lint fails builds — the two that
58
+ matter most: an aliased spawn wrapped by `withSyncOp` is still NOT flagged (the
59
+ funnel is the required pattern; overriding it would punish the code the rule
60
+ exists to produce), and an aliased spawn carrying an allow-comment is still NOT
61
+ flagged.
62
+ - Real tree: `exit 0` before AND after. `tsc --noEmit` exit 0. Full `npm run lint`
63
+ chain exit 0.
64
+ - Declared open in the source: dot-prefixed names (measured correct), cross-module
65
+ aliases, and `const ex = <ns>.execFileSync`.
@@ -0,0 +1,152 @@
1
+ # Side-Effects Review — atomic-writes check scopes to method bodies
2
+
3
+ **Version / slug:** `atomic-writes-method-scope`
4
+ **Date:** `2026-08-15`
5
+ **Author:** `echo`
6
+ **Second-pass reviewer:** `not required — Tier 1. Test-only change; no file under src/, scripts/, .husky/ or skills/ is touched, so there is no runtime path and no shipped behaviour. The change makes an existing check stricter and adds no authority.`
7
+
8
+ ## Summary of the change
9
+
10
+ `tests/unit/atomic-writes-consistency.test.ts` verifies that state-writing modules use
11
+ write-to-tmp-then-rename, so a crash mid-write cannot leave a truncated state file. **It could not
12
+ detect the defect it exists to detect.**
13
+
14
+ Measured, not argued — a bare `fs.writeFileSync` of durable session state inserted into
15
+ `saveSession`, a DECLARED method of a DECLARED module:
16
+
17
+ | check | verdict against that mutation |
18
+ |---|---|
19
+ | shipped | **21/21 PASSED** |
20
+ | this change | **1 failed / 31 passed**, naming file, method, deciding body, line |
21
+
22
+ Both runs were made in the SAME worktree against the SAME mutated source, with the shipped check
23
+ restored from git alongside the new one — subject and control share a boundary rather than being
24
+ compared across two checkouts.
25
+
26
+ Three causes, all fixed:
27
+
28
+ 1. **Scoping.** `inSaveMethod` was set when a method NAME appeared on a line and never reset, while
29
+ `hasWriteFile`/`hasRename` were re-zeroed at each occurrence. Only the window from the LAST name
30
+ mention to EOF survived to the assertion — measured at **125 of 617 lines (20%)** in
31
+ `StateManager.ts`, leaving three of its four declared methods structurally unreachable. Within
32
+ that window the booleans were file-scope, so a rename in one method vouched for a write in another.
33
+ 2. **File-scope substring checks.** `source.includes('renameSync')` and `source.includes('.tmp')` are
34
+ satisfied by one occurrence anywhere, comments included.
35
+ 3. **Silent declaration rot.** A missing file was `it.skip`ped; a missing method simply never set the
36
+ flag. Both are failures now.
37
+
38
+ ## What enabling (3) immediately found
39
+
40
+ **Two of the ten declared (module, method) pairs name methods that do not exist.** `saveState` has
41
+ ZERO occurrences in `src/core/StateManager.ts` and ZERO in `src/monitoring/QuotaTracker.ts` — verified
42
+ by grep with a control (`saveSession(`, `appendEvent(`, `persistUsers(` all found). QuotaTracker's
43
+ real writer is `updateState()`, which is correctly atomic and had never been verified by this test.
44
+ The declared list is corrected in the same change.
45
+
46
+ ## Decision-point inventory
47
+
48
+ - `tests/helpers/atomicWriteScope.ts` — ADD. `stripComments` (quote-aware, line-count preserving),
49
+ `methodBodies` (brace-matched, string-aware, declaration-vs-call aware), `delegateTargets`,
50
+ `classifyMethod`.
51
+ - `tests/unit/atomic-writes-consistency.test.ts` — REWRITTEN to per-method assertions; declared list
52
+ corrected; missing file/method now fail; anti-vacuity assertion added.
53
+ - `tests/unit/atomic-write-scope.test.ts` — ADD. 18 tests pinning the primitives and both directions.
54
+ - No file under `src/`, `scripts/`, `.husky/` or `skills/` is touched. No runtime decision added.
55
+
56
+ ## 1. Over-block
57
+
58
+ **The dominant risk, and it nearly bit me.** The obvious fix — require a rename in each declared
59
+ method's own body — would have FAILED on `StateManager`, the best-written module in the set, because
60
+ it routes every write through a private `atomicWrite()` funnel and its save methods contain no write
61
+ call at all. Failing the single-funnel pattern this codebase argues for everywhere else would be a
62
+ false red on exemplary code.
63
+
64
+ So one level of `this.helper()` delegation is resolved and the funnel is what gets verified — which
65
+ also means `StateManager`'s writes are now genuinely checked, where previously nothing checked them.
66
+
67
+ Six controls, each with a test, all passing under BOTH the old and new behaviour:
68
+
69
+ - delegation to a genuine atomic funnel → `atomic-via-funnel`, not a violation;
70
+ - in-body tmp-then-rename → `atomic-inline`;
71
+ - a method that legitimately writes nothing → `no-write`, no invented violation;
72
+ - a commented-out write is not a write;
73
+ - a CALL site (`this.saveSession({...})` inside another method) is not a declaration — treating it as
74
+ one is precisely how the old flag conflated two methods;
75
+ - an unbalanced brace yields no body rather than a wrong region, so a syntax error elsewhere cannot
76
+ become a false verdict here.
77
+
78
+ **Verified against the real tree: 32/32 green.** The production code is atomic everywhere the check
79
+ now looks, including through the funnel.
80
+
81
+ **A defect in my own helper, caught by these controls before it shipped:** the first `methodBodies`
82
+ required a declaration at line start. That works on real source (which indents declarations) and
83
+ returned `found: false` for every hand-written fixture — so five tests failed loudly rather than
84
+ passing vacuously. The matcher now decides by the PRECEDING token (start / `{` / `}` / `;` after
85
+ skipping modifiers), which rejects `this.save(` and `helper(save(1))` as calls while accepting a
86
+ declaration that does not begin its own line.
87
+
88
+ ## 2. Under-block
89
+
90
+ Stated in the source rather than implied:
91
+
92
+ - **Population.** The module list is CURATED and holds 7 entries. Hundreds of files under `src/` call
93
+ `writeFileSync`; this test says nothing about any of them. A heuristic sweep suggested a state-writing
94
+ population in the low hundreds, but that heuristic missed 2 of the 7 KNOWN-good modules, so it is not
95
+ a defect count and is not quoted as one. Widening the population is separate work with real
96
+ over-block risk and is deliberately not attempted here.
97
+ - **Delegation depth.** One level, one file. A helper calling another helper, or an imported writer,
98
+ is not resolved — that needs a symbol graph, not text.
99
+ - **Write vocabulary.** Only `writeFileSync`/`renameSync`. A module writing via a stream, `fs.promises`,
100
+ or a third-party helper is invisible.
101
+
102
+ ## 3. Level-of-abstraction fit
103
+
104
+ Same layer as the existing check — source-text analysis in a unit test, no AST, no type information,
105
+ no new dependency. The brace matcher is the minimum needed to answer "which method is this line in?",
106
+ which is the question the original flag was trying and failing to answer.
107
+
108
+ ## 4. Signal vs authority compliance
109
+
110
+ A test, not a runtime authority. It gates CI only. It gained teeth (it can now fail) but no new
111
+ decision-making power over agent behaviour.
112
+
113
+ ## 5. Interactions
114
+
115
+ - Runs in the existing unit shards; no new script, no lint-chain entry, no CI wiring change.
116
+ - `tsc --noEmit` exit 0 — **but stated honestly: `tsconfig.json` excludes `tests`, so that run says
117
+ nothing about these files.** Their correctness is evidenced by the suite passing, not by the compiler.
118
+ - No source module, route, config key, or state file touched.
119
+
120
+ ## 6. External surfaces
121
+
122
+ None. Developer tooling. The Agent Awareness Standard does not apply — no agent capability is added.
123
+
124
+ ## 7. Multi-machine posture (Cross-Machine Coherence)
125
+
126
+ **Machine-local by design, and correct.** A unit test reads files in one checkout and returns an exit
127
+ code. No durable state, no user-facing notice, no generated URL, no runtime decision — nothing to
128
+ replicate, merge on read, or strand on a topic transfer. Every machine runs it over its own checkout
129
+ of the same tracked source and reaches the same verdict; determinism comes from the source tree, not
130
+ from coordination.
131
+
132
+ ## 8. Rollback cost
133
+
134
+ `git revert` of three test files. No migration, no state, no deployed artifact, no runtime impact.
135
+
136
+ ## Conclusion
137
+
138
+ Ship. A check that could not fail on its own subject now fails on it, two stale declarations are
139
+ repaired, the best-written module in the set is verified for the first time, and the limits are named
140
+ in the source rather than implied.
141
+
142
+ ## Evidence pointers
143
+
144
+ - `tests/unit/atomic-write-scope.test.ts` — 18/18 green (6 defect cases, 6 over-block controls, 6 primitives).
145
+ - `tests/unit/atomic-writes-consistency.test.ts` — 32/32 green against the real tree.
146
+ - **Both-directions proof in ONE worktree:** shipped check vs the mutation → 21/21 PASSED; new check
147
+ vs the SAME mutation → 1 failed / 31 passed. `src/core/StateManager.ts` restored byte-exact
148
+ (sha match, zero probe markers, zero stray files).
149
+ - Stale declarations verified by grep with a control that fired.
150
+ - `tsc --noEmit` exit 0 (boundary stated above: tests are excluded from that config).
151
+ - Tier **1** declared: `classifyTier` reports riskFloor 1 with no safety-invariant match, and `tests/`
152
+ is outside `inScope()`, so the size heuristic contributes nothing either.
@@ -0,0 +1,166 @@
1
+ # Side-Effects Review — sync-spawn ratchet resolves bound names
2
+
3
+ **Version / slug:** `sync-spawn-alias-resolution`
4
+ **Date:** `2026-08-15`
5
+ **Author:** `echo`
6
+ **Second-pass reviewer:** `not required — Tier 1 (CI-only lint script, no runtime path). The rule, the funnel, the allow-comment escape and the frozen baseline are all unchanged; the check now resolves two more ways of naming the same banned call.`
7
+
8
+ ## Summary of the change
9
+
10
+ `scripts/lint-sync-subprocess-chokepoint.js` is the forward ratchet for tmux
11
+ event-loop resilience: a synchronous subprocess spawn blocks the single-threaded
12
+ event loop for the child's whole lifetime, so outside the `withSyncOp` marker
13
+ funnel a raw sync spawn is banned. The incident behind it — a blocked-but-alive
14
+ server that looked dead to its supervisor and was restarted for being busy.
15
+
16
+ It matched the spawn NAME on the call line. Measured against the shipped lint
17
+ with a positive control (plain `execFileSync(...)`) firing in the same run:
18
+
19
+ | form | shipped |
20
+ |---|---|
21
+ | `execFileSync('tmux', …)` — POSITIVE CONTROL | exit 1 (caught) |
22
+ | **`import { execFileSync as run } …; run(…)`** | **exit 0 — EVADES** |
23
+ | **`const ex = execFileSync; ex(…)`** | **exit 0 — EVADES** |
24
+
25
+ A renamed import is not an evasion; it is how a name collision gets resolved.
26
+
27
+ **Neither form appears anywhere in the scanned directories today** (measured: 0
28
+ local aliases, 0 renamed imports, against a control of 53 files carrying plain
29
+ named imports). So this is a pure forward ratchet — nothing is added to the
30
+ frozen baseline and nothing existing can break.
31
+
32
+ ## The scope decision, which measurement reversed
33
+
34
+ `VIOLATION` also excludes a DOT-prefixed name, and my first read was that this
35
+ was the same class of hole. There are **14** namespace-form occurrences in the
36
+ scanned directories, and "14 invisible blocking spawns" would have been the
37
+ headline.
38
+
39
+ Counting what they *are* rather than how many:
40
+
41
+ - **13 are `SafeGitExecutor.execSync(`** — calls THROUGH the audited git funnel.
42
+ Flagging them would invert the rule, reporting correct use of the funnel as a
43
+ bypass of it.
44
+ - **1 is `childProcess.execFileSync(` inside `getStopGateRouterHook()`** — which
45
+ returns a template literal for a generated hook script. That text runs in its
46
+ own short-lived process and cannot block this event loop.
47
+
48
+ **All 14 exclusions are correct. The dot-exclusion is left alone**, and two tests
49
+ pin it so a future reader does not "fix" it and break the funnel. The header
50
+ records the measurement for the same reason.
51
+
52
+ **A probe of mine returned the flattering answer and was wrong.** To test whether
53
+ the 14th sat inside a template literal I counted unescaped backticks before its
54
+ line — in a 16,000-line file, where backticks inside strings and comments corrupt
55
+ the count. It reported "not inside a template", which supported the bigger
56
+ finding. Reading the enclosing function signature settled it in one line.
57
+
58
+ ## Decision-point inventory
59
+
60
+ - `collectSyncSpawnAliases(content)` — ADD. Per-file: renamed imports from
61
+ `(node:)child_process`, and `const|let|var X = <bare spawn name>`.
62
+ - `aliasCallRegex(names)` — ADD. Call-shape matcher carrying the SAME
63
+ dot-exclusion as `VIOLATION`; returns null when there are no names.
64
+ - The per-file loop — CHANGED: `if (!VIOLATION.test(raw) && !(aliasRe && aliasRe.test(raw))) continue;`
65
+ - `VIOLATION`, `FUNNELED`, `ALLOW`, `SCAN_DIRS`, `EXTENSIONS`, the baseline
66
+ format, the baseline file and the exit codes — UNCHANGED.
67
+ - No runtime block/allow decision added or modified. CI-time only.
68
+
69
+ ## 1. Over-block
70
+
71
+ The dominant risk — this lint fails builds. Six controls, each with a test, all
72
+ passing under BOTH old and new behaviour:
73
+
74
+ - **an aliased spawn wrapped by `withSyncOp` is not flagged.** The most important
75
+ one: the funnel is the REQUIRED pattern, and if resolution overrode it the fix
76
+ would punish exactly the code the rule exists to produce.
77
+ - **an aliased spawn carrying `lint-allow-sync-spawn:` is not flagged** — the
78
+ existing escape for genuinely pre-runtime calls still works.
79
+ - an unrelated identifier that merely shares the name is not flagged — only a
80
+ name actually bound to a spawn is collected.
81
+ - a method call on another object (`helper.ex(...)`) is not flagged — the alias
82
+ matcher carries the same dot-exclusion as the original rule.
83
+ - a file with no sync spawn is not flagged.
84
+ - the two dot-exclusion pins above (`SafeGitExecutor.execSync`, `cp.execFileSync`).
85
+
86
+ **Real tree: exit 0 before AND after.** Full `npm run lint` chain exit 0.
87
+
88
+ Residual over-block risk, stated: a very short alias (`run`, `ex`) shadowed later
89
+ in the same file by an unrelated binding of the same name would be flagged. Not
90
+ observed anywhere today, and the failure is loud and one line to fix, unlike the
91
+ silent miss it replaces.
92
+
93
+ ## 2. Under-block
94
+
95
+ Stated in the source:
96
+
97
+ - **Dot-prefixed names** — deliberately excluded, measured correct (above).
98
+ - **Cross-module aliases** — a wrapper exported from another file.
99
+ - **`const ex = <ns>.execFileSync`** — not collected, because collecting it would
100
+ require resolving the namespace, which is the dot case.
101
+ - The header's pre-existing honesty stands: this is a static line regex and
102
+ cannot prove a flagged line is actually wrapped at runtime — that is the
103
+ marker unit tests' job.
104
+
105
+ ## 3. Level-of-abstraction fit
106
+
107
+ Same layer as the existing check — line regex over file text, no AST, no new
108
+ dependency. Alias collection is the smallest addition that answers the question
109
+ the rule already asks ("is this line a raw sync spawn?") for names the file
110
+ creates itself.
111
+
112
+ ## 4. Signal vs authority compliance
113
+
114
+ A CI ratchet, not a runtime authority. It gained reach over two more spellings of
115
+ a violation it already forbade, and no new decision-making power. The funnel, the
116
+ escape and the baseline are untouched.
117
+
118
+ ## 5. Interactions
119
+
120
+ - Already in the `lint` chain CI runs; chain exit 0 with this change.
121
+ - The frozen baseline is untouched and does not grow — the newly-reachable forms
122
+ have zero existing instances.
123
+ - Alias collection is one extra regex pass per file; no perceptible change in
124
+ chain duration.
125
+ - No source module, route, config key, or state file touched.
126
+
127
+ ## 6. External surfaces
128
+
129
+ None. Developer tooling. The Agent Awareness Standard does not apply.
130
+
131
+ ## 7. Multi-machine posture (Cross-Machine Coherence)
132
+
133
+ **Machine-local by design, and correct.** A CI-time source scan: reads files in
134
+ one checkout, returns an exit code. No durable state, no user-facing notice, no
135
+ generated URL, no runtime decision — nothing to replicate, merge on read, or
136
+ strand on a topic transfer. Every machine runs it over its own checkout of the
137
+ same tracked source and reaches the same verdict; alias collection is explicitly
138
+ per-file, so it cannot depend on the rest of the checkout, let alone another
139
+ machine.
140
+
141
+ ## 8. Rollback cost
142
+
143
+ `git revert` of one script plus the added test file. No migration, no state, no
144
+ deployed artifact, no runtime impact, no baseline change to undo.
145
+
146
+ ## Conclusion
147
+
148
+ Ship. Two ordinary ways of naming a banned blocking call are now seen, the
149
+ existing funnel and escape still win over the new reach, the deliberate
150
+ dot-exclusion is measured-correct and pinned rather than widened, and the real
151
+ tree is verified clean in both directions.
152
+
153
+ ## Evidence pointers
154
+
155
+ - `tests/unit/sync-spawn-alias-resolution.test.ts` — **12/12 green**.
156
+ - **Negative control: 4 of 12 fail** against the shipped lint (exactly the four
157
+ defect cases). The other 8 pass **both ways** — one positive control, two
158
+ escape-still-wins, three over-block, two dot-exclusion pins. Script restored
159
+ **byte-exact** after the control (sha match).
160
+ - Reproduced by hand FIRST with a positive control in the same run.
161
+ - Zero existing instances of either newly-reached form (control: 53 files with
162
+ plain named imports), so the frozen baseline does not grow.
163
+ - Real-tree verdict: exit 0 before and after. `tsc --noEmit` exit 0. Full chain
164
+ exit 0.
165
+ - Tier **1** declared: CI-only script, no runtime path, no authority, no
166
+ capability.