instar 1.3.1153 → 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.1153",
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": "c9fe9271930f52c79239f9b43e4d3c873717dc1dd98744525290c8ccba4f07bf",
2
+ "sha256": "49844097b7495749a584409f6b28788b61127591a3f707a03ef9622e69bf1dd1",
3
3
  "registrySha256": "81b53363a440e832672618965540b3e507ae0d93adcc67ec2b93daf7933b3ab4",
4
- "packageVersion": "1.3.1153"
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.1153"
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.1153",
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:31:14.741Z",
5
- "instarVersion": "1.3.1153",
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.1153",
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": "c9fe9271930f52c79239f9b43e4d3c873717dc1dd98744525290c8ccba4f07bf",
2
+ "sha256": "49844097b7495749a584409f6b28788b61127591a3f707a03ef9622e69bf1dd1",
3
3
  "registrySha256": "81b53363a440e832672618965540b3e507ae0d93adcc67ec2b93daf7933b3ab4",
4
- "packageVersion": "1.3.1153"
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.1153"
5
+ "packageVersion": "1.3.1154"
6
6
  }
@@ -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,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.