instar 0.28.49 → 0.28.51

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.
Files changed (33) hide show
  1. package/dist/commands/init.js +93 -93
  2. package/dist/commands/init.js.map +1 -1
  3. package/dist/commands/server.d.ts.map +1 -1
  4. package/dist/commands/server.js +61 -31
  5. package/dist/commands/server.js.map +1 -1
  6. package/dist/core/InputGuard.d.ts +29 -3
  7. package/dist/core/InputGuard.d.ts.map +1 -1
  8. package/dist/core/InputGuard.js +73 -45
  9. package/dist/core/InputGuard.js.map +1 -1
  10. package/dist/core/PostUpdateMigrator.d.ts +14 -0
  11. package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
  12. package/dist/core/PostUpdateMigrator.js +46 -0
  13. package/dist/core/PostUpdateMigrator.js.map +1 -1
  14. package/dist/messaging/shared/isSystemOrProxyMessage.d.ts +41 -0
  15. package/dist/messaging/shared/isSystemOrProxyMessage.d.ts.map +1 -0
  16. package/dist/messaging/shared/isSystemOrProxyMessage.js +64 -0
  17. package/dist/messaging/shared/isSystemOrProxyMessage.js.map +1 -0
  18. package/dist/monitoring/PresenceProxy.d.ts +3 -1
  19. package/dist/monitoring/PresenceProxy.d.ts.map +1 -1
  20. package/dist/monitoring/PresenceProxy.js +5 -16
  21. package/dist/monitoring/PresenceProxy.js.map +1 -1
  22. package/package.json +1 -1
  23. package/scripts/pre-push-gate.js +6 -3
  24. package/src/data/builtin-manifest.json +43 -43
  25. package/upgrades/0.28.50.md +59 -0
  26. package/upgrades/0.28.51.md +31 -0
  27. package/upgrades/0.28.52.md +82 -0
  28. package/upgrades/side-effects/0.28.49.md +90 -0
  29. package/upgrades/side-effects/0.28.50.md +104 -0
  30. package/upgrades/side-effects/0.28.51.md +145 -0
  31. package/upgrades/side-effects/0.28.52.md +276 -0
  32. package/upgrades/side-effects/pre-push-gate-ci-scope.md +104 -0
  33. package/upgrades/side-effects/skill-port-dynamic-resolution.md +104 -0
@@ -0,0 +1,276 @@
1
+ # Side-Effects Review — Compaction-recovery proxy-filter fix
2
+
3
+ **Version / slug:** `0.28.52`
4
+ **Date:** `2026-04-17`
5
+ **Author:** `echo`
6
+ **Second-pass reviewer:** `(this is a classifier-dedup fix with no Guard
7
+ surface — second pass not required per skill Phase 5)`
8
+
9
+ ## Summary of the change
10
+
11
+ Closes the topic-6795 compaction stall: `recoverCompactedSession` was
12
+ deciding "is there pending work?" by looking at the last message in the
13
+ topic without filtering out PresenceProxy standby messages or
14
+ server-emitted delivery/lifecycle acks. Those are `fromUser: false` but
15
+ they are NOT real agent responses — treating them as "agent answered" is
16
+ what let the compaction-recovery safety net decline three consecutive
17
+ re-inject attempts while the user sat with an unanswered question.
18
+
19
+ The fix hoists the classifier that `PresenceProxy.isSystemMessage()` and
20
+ `checkLogForAgentResponse()` already used into a shared module, adds a
21
+ thin `findLastRealMessage(history)` walk-back helper on top, and routes
22
+ `recoverCompactedSession` through it. Three scattered copies of the
23
+ prefix list are now one.
24
+
25
+ Files touched:
26
+
27
+ - `src/messaging/shared/isSystemOrProxyMessage.ts` — new. Exports
28
+ `isSystemOrProxyMessage(text)` (the classifier) and `findLastRealMessage(history)`
29
+ (the walk-back). Thorough header comment documenting which subsystems
30
+ consume it and the regression anchor.
31
+ - `src/commands/server.ts` — `recoverCompactedSession` now uses
32
+ `findLastRealMessage` instead of `history[history.length - 1]`. History
33
+ window widened 5 → 20. `checkLogForAgentResponse` now delegates to
34
+ `isSystemOrProxyMessage` instead of its inlined prefix list.
35
+ - `src/monitoring/PresenceProxy.ts` — `isSystemMessage()` is now a thin
36
+ wrapper over `isSystemOrProxyMessage` (instance-method signature
37
+ preserved so existing callsites are unchanged).
38
+ - `tests/unit/isSystemOrProxyMessage.test.ts` — new, 25 tests.
39
+
40
+ ## Decision-point inventory
41
+
42
+ - `recoverCompactedSession` unanswered-message check — **modify** — the
43
+ authority that decides whether a compaction re-injection fires. The
44
+ decision predicate changed from "look at last message only" to "walk
45
+ backward skipping system/proxy, check first real message". Role
46
+ (authority) unchanged; correctness of the predicate improved.
47
+ - `checkLogForAgentResponse` — **modify** — signal producer ("has the
48
+ agent responded since X?"). Logic unchanged, just dedup against shared
49
+ classifier.
50
+ - `PresenceProxy.isSystemMessage()` — **modify** — signal producer for
51
+ the race guard. Logic unchanged, just dedup against shared classifier.
52
+ - `isSystemOrProxyMessage` / `findLastRealMessage` — **new helpers** —
53
+ pure functions, no state, no side effects, no blocking authority of
54
+ their own. They are detectors; callers decide what to do with the
55
+ verdict.
56
+
57
+ ---
58
+
59
+ ## 1. Over-block
60
+
61
+ **What legitimate inputs does this change reject that it shouldn't?**
62
+
63
+ None new. The classifier's prefix list is identical to what
64
+ `PresenceProxy.isSystemMessage` and the pre-fix inlined copy in
65
+ `checkLogForAgentResponse` already used. Unit tests
66
+ (`does NOT classify a checkmark used in narrative as a delivery ack`,
67
+ `does NOT classify a message merely CONTAINING 🔭 later as proxy`) pin
68
+ down the leading-prefix contract — an agent reply that *mentions* 🔭 or
69
+ uses ✓ later in a sentence is correctly treated as a real response.
70
+
71
+ The only behavior change affecting "the agent answered" detection is that
72
+ `recoverCompactedSession` now walks PAST system/proxy entries instead of
73
+ tripping on them. The pre-fix behavior was strictly more trigger-happy
74
+ with declines; post-fix is strictly more trigger-happy with re-injections.
75
+ A re-injection on an already-answered topic costs one
76
+ `COMPACTION_RESUME_PROMPT` message plus a small session-wake — the same
77
+ path that fires on legitimate recoveries, and well-tested. Not comparable
78
+ to the silent 15-minute user-facing stall from the old behavior.
79
+
80
+ ---
81
+
82
+ ## 2. Under-block
83
+
84
+ **What failure modes does this still miss?**
85
+
86
+ 1. **Pre-change under-block (the one this fixes):** recoverCompactedSession
87
+ accepting standby-as-answer and declining recovery. Closed.
88
+ 2. **History horizon:** `telegram.getTopicHistory(topicId, 20)` reads the
89
+ last 20 topic entries. If the user's unanswered question is 21+ entries
90
+ back AND every message in between is system/proxy, the walk returns
91
+ null and declines. Pre-fix used a window of 5, so this is a
92
+ strict improvement; but it's still finite. In practice, 20 messages of
93
+ pure standby/ack traffic without a single real agent reply would itself
94
+ be a distinct pathology (agent is completely wedged, not just
95
+ compacting), and the right response is escalation to stall triage —
96
+ not a bigger history window. Documented in the module header.
97
+ 3. **New from-agent message formats:** if a future subsystem starts
98
+ emitting a new kind of "not really a response" message (say, a new
99
+ telemetry prefix), the classifier won't know about it and recovery
100
+ will decline. Mitigation: the classifier is now in one place, so
101
+ adding the new prefix is one edit and gets all three callsites for
102
+ free — versus the pre-fix state where you'd need to update three
103
+ files and remember which ones.
104
+ 4. **Trimmed-whitespace input:** classifier trims before matching, so
105
+ indentation/CRLF variations are covered. Pinned by
106
+ `trim handling` tests.
107
+
108
+ ---
109
+
110
+ ## 3. Level-of-abstraction fit
111
+
112
+ The classifier sits at the *data-shape* layer: "given a line of text,
113
+ is it a real agent response?" It has no knowledge of topics, sessions,
114
+ timestamps, or recovery policy. Callers (recoverCompactedSession,
115
+ PresenceProxy, checkLogForAgentResponse) own the *policy* — what to do
116
+ when a message is or isn't a real response.
117
+
118
+ The walk-back helper `findLastRealMessage` sits one layer up: "given a
119
+ chronological history, find the latest real entry." Still no policy. Its
120
+ only knowledge is that histories are chronologically ordered and that the
121
+ classifier distinguishes real from not-real.
122
+
123
+ This is the same shape as other shared classifiers in the repo
124
+ (`detectContextExhaustion`, `isHttpIdempotent`). Appropriate fit.
125
+ No authority is being packed into a detector, no detector is being
126
+ smeared across three files.
127
+
128
+ Alternatives considered and rejected:
129
+
130
+ - **Inline the walk-back in `recoverCompactedSession` only** — would
131
+ re-introduce the three-copy duplication problem on the next change.
132
+ Rejected.
133
+ - **Make the classifier itself decide "should recovery fire?"** — would
134
+ pack policy into the detector. Callers need different behaviors
135
+ (PresenceProxy cares about self-cancellation, recoverFn cares about
136
+ user turns, checkLog cares about any real agent message). Rejected.
137
+ - **Move the walk-back into TelegramAdapter as `getLastRealMessage`** —
138
+ would couple a generic message-shape filter to the Telegram adapter.
139
+ The same logic needs to apply to Slack histories later. Rejected in
140
+ favor of the transport-agnostic helper.
141
+
142
+ ---
143
+
144
+ ## 4. Signal vs authority compliance
145
+
146
+ **Required reference:** [docs/signal-vs-authority.md](../../docs/signal-vs-authority.md)
147
+
148
+ **Does this change hold blocking authority with brittle logic?**
149
+
150
+ - [x] No — this change moves in the correct direction on the principle.
151
+
152
+ Narrative breakdown:
153
+
154
+ - `isSystemOrProxyMessage` is a **signal producer** — a detector on
155
+ textual shape. No blocking authority, no side effects, no state. It
156
+ emits a classification; callers choose what to do.
157
+ - `findLastRealMessage` is also a signal producer — it transforms a
158
+ history into "which entry is the latest real one?" It doesn't decide
159
+ whether anything should fire.
160
+ - The blocking/non-blocking authority lives in `recoverCompactedSession`
161
+ (which decides to inject or not) and in the CompactionSentinel
162
+ (which decides to retry or finalize). Neither gained new authority;
163
+ the existing authority now consumes a less-brittle signal.
164
+
165
+ Signal strength: the classifier is prefix-based, which is fragile for
166
+ general-purpose intent classification — but here the "signal" is a
167
+ structural marker emitted by known server-code paths (PresenceProxy
168
+ emits `🔭`, the server emits `✓ Delivered` via a specific code path, etc.).
169
+ It's not inferring intent from free-text user input. Prefix matching is
170
+ the right tool for this specific job.
171
+
172
+ No brittle check is acquiring blocking authority. One authority
173
+ (recoverCompactedSession) is migrating from a brittle predicate ("last
174
+ message is from user?") to a robust one ("last REAL message is from
175
+ user?"). Principle held.
176
+
177
+ ---
178
+
179
+ ## 5. Interactions
180
+
181
+ - **With CompactionSentinel:** the Sentinel observes `recoverFn`'s
182
+ accept/reject verdict unchanged. Dedupe, retry, verify-window, and
183
+ finalize semantics are untouched. The fix changes WHEN recoverFn says
184
+ `true`, not the Sentinel's reaction to it.
185
+ - **With PresenceProxy race guard:** PresenceProxy already used the
186
+ (now shared) classifier to decide whether to self-cancel on a sibling
187
+ standby. That logic is byte-equivalent pre and post — only the source
188
+ of the prefix list changed. Verified by running
189
+ `presence-proxy-cancel-race.test.ts` (5 tests) green.
190
+ - **With checkLogForAgentResponse:** delegates the classifier call.
191
+ Function-level behavior unchanged. No new callers; same call sites
192
+ (stall-triage idle check, PresenceProxy "has the agent responded since
193
+ X"). Verified by running the broader suite green.
194
+ - **History-window widening (5 → 20):** only affects
195
+ `recoverCompactedSession`. `telegram.getTopicHistory` is already
196
+ safe up to 50+ entries in other call sites. No performance concern —
197
+ reading 20 log entries is ~1-2ms.
198
+ - **Races:** the walk is over a snapshot of the history read once per
199
+ call. No multi-step read, no TOCTOU window. Sentinel-level deduplication
200
+ already prevents concurrent recoverFn calls for the same session.
201
+ - **Feedback loops:** recovery inject → session wakes → agent emits real
202
+ response → future `checkLogForAgentResponse`/walk-back sees the real
203
+ response, not the proxy standby that preceded it. Correctly breaks the
204
+ loop that previously kept the session "stuck-looking."
205
+
206
+ ---
207
+
208
+ ## 6. External surfaces
209
+
210
+ - **Other agents on the same machine:** none. The classifier lives in
211
+ the per-agent server process.
212
+ - **Other users of the install base:** on upgrade, agents whose
213
+ compaction recovery was declining due to this bug will begin
214
+ successfully recovering. This is the intended, silent, good behavior
215
+ change. Users should not notice anything except that compaction stalls
216
+ become shorter.
217
+ - **External systems:** none. No new egress.
218
+ - **Persistent state:** none. No schema changes. No agent-state migration.
219
+ - **Log format:** `[CompactionResume]` log lines are unchanged.
220
+ `[Sentinel]` lifecycle lines are unchanged. A re-injection that
221
+ previously would have logged `recoverFn declined (no pending work or
222
+ session gone)` will now log the normal
223
+ `direct re-inject OK for topic <N>` path.
224
+ - **Timing/runtime:** walking 20 log entries through a string-prefix
225
+ classifier adds <1ms to the recoverFn hot path. Sentinel timers
226
+ unchanged.
227
+
228
+ ---
229
+
230
+ ## 7. Rollback cost
231
+
232
+ Pure code change. Revert the commit, ship a patch. No persistent state
233
+ migration. No agent state repair. On rollback, compaction recovery
234
+ returns to the pre-fix behavior — declining when the last message is a
235
+ PresenceProxy standby. No user-visible regression beyond re-exposing the
236
+ original bug.
237
+
238
+ Estimated rollback effort: one revert commit + one release bump, <10 minutes.
239
+
240
+ ---
241
+
242
+ ## Conclusion
243
+
244
+ Small-surface bug fix. The change:
245
+
246
+ 1. Closes a concrete user-visible failure (topic-6795 compaction stall,
247
+ repro-ed three times).
248
+ 2. Dedups three copies of a prefix list that were drifting.
249
+ 3. Adds regression coverage (25 unit tests including the exact
250
+ repro sequence).
251
+ 4. Moves `recoverCompactedSession` from a brittle last-message-only
252
+ predicate to a walk-back over filtered history.
253
+ 5. Holds to signal-vs-authority: no detector gains blocking power; one
254
+ authority now consumes a robust signal instead of a brittle one.
255
+
256
+ Clear to ship.
257
+
258
+ ---
259
+
260
+ ## Evidence pointers
261
+
262
+ - Reproduction: `.instar/shared-state.jsonl` entries around topic 6795 at
263
+ 16:08:14 / 16:13:xx / 16:18:xx logged the three
264
+ `recoverFn declined (no pending work or session gone)` lines; topic
265
+ history in `.instar/telegram-messages.jsonl` at each decline point
266
+ shows the preceding message was a `🔭 Echo is currently updating the
267
+ ledger spec…` PresenceProxy standby.
268
+ - Pre-fix behavior: `git show <pre-fix>:src/commands/server.ts` around
269
+ the `recoverCompactedSession` definition shows
270
+ `const lastMsg = history[history.length - 1]; if (lastMsg?.fromUser) { ... }`.
271
+ - Post-fix behavior: `findLastRealMessage(history)` returns the last
272
+ non-system/non-proxy entry; the decision predicate sees the real user
273
+ turn.
274
+ - Tests: `tests/unit/isSystemOrProxyMessage.test.ts` — 25 passing tests.
275
+ Explicit `topic-6795 repro` case asserts the helper finds the user
276
+ question past trailing proxy + ack entries.
@@ -0,0 +1,104 @@
1
+ # Side-Effects Review — pre-push gate: CI scope fix
2
+
3
+ **Version / slug:** `pre-push-gate-ci-scope`
4
+ **Date:** `2026-04-17`
5
+ **Author:** `echo`
6
+ **Second-pass reviewer:** `not required`
7
+
8
+ ## Summary of the change
9
+
10
+ Modifies `scripts/pre-push-gate.js` in two ways: (1) wraps section 5 (side-effects artifact check) in `if (!process.env.CI)`, so the check runs only when developers push locally and is skipped in GitHub Actions; (2) adds `2>/dev/null` to the `HEAD~1` stderr fallback in section 3's git diff command, stopping stderr from leaking through the try/catch into the test output in shallow-clone CI environments. No `src/` files are touched — only the gate script itself.
11
+
12
+ ## Decision-point inventory
13
+
14
+ - `scripts/pre-push-gate.js` section 5 — **modify** — narrows the scope of the side-effects artifact check from "always" to "not in CI". The check itself is unchanged; only its execution context is restricted.
15
+ - `scripts/pre-push-gate.js` section 3 — **modify** — cosmetic: suppresses stderr noise from a git fallback command. No decision logic involved.
16
+
17
+ ---
18
+
19
+ ## 1. Over-block
20
+
21
+ No block/allow surface for messages or agent actions — not applicable in the traditional sense.
22
+
23
+ Within the gate's own domain: the change *reduces* over-block. Previously the gate would reject any CI run on a contributor branch that was cut before the side-effects artifact for the current version was added to main. That's a false positive — the contributor didn't violate the process, the artifact simply hadn't been added to main yet when they branched. The fix stops those legitimate branches from being blocked.
24
+
25
+ No new rejection surface is introduced.
26
+
27
+ ---
28
+
29
+ ## 2. Under-block
30
+
31
+ The gate now allows CI runs that are missing the side-effects artifact. This is an intentional scope reduction: CI is not the enforcement point. The enforcement points are:
32
+
33
+ 1. The pre-commit hook (`scripts/instar-dev-precommit.js`) — runs per-commit on the developer's machine.
34
+ 2. The pre-push hook (this gate, section 5) — runs on the developer's machine at push time.
35
+
36
+ Both hooks run before code reaches CI. If a developer bypasses them (e.g., `--no-verify`), section 5 in CI would have caught it — and now it won't. This is a real reduction in defense depth for the `--no-verify` bypass case.
37
+
38
+ Mitigation: `--no-verify` bypasses are visible in git history (the commit won't have the artifact). The pre-push gate also re-checks at the release-cut step when NEXT.md is renamed to a versioned file — which does happen locally, not in CI. The net under-block exposure is: a developer who uses `--no-verify` and then somehow gets their branch merged without a local push. This is a narrow path that the review process (PR review, merge gating) is expected to catch.
39
+
40
+ ---
41
+
42
+ ## 3. Level-of-abstraction fit
43
+
44
+ The gate is a structural process-enforcement check, not a message-content gate. Section 5 is explicitly scoped to "push time" in its own comment. CI is not push time — it's post-push. Running a push-time check in CI creates a category mismatch that produces false failures on valid contributor branches.
45
+
46
+ The fix is at the correct layer: the `if (!process.env.CI)` guard is a simple execution-context discriminator applied directly to the check that's miscategorized for CI. No rearchitecting needed.
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
+ - [x] No — this change has no block/allow surface.
57
+
58
+ The gate operates on developer process compliance (file existence, git metadata), not on message content or agent behavior. The signal-vs-authority principle applies to decision points that evaluate messages or constrain agent information flow. A CI scope guard on a developer process check is outside that domain.
59
+
60
+ The change itself is a pure scope restriction — it *removes* an execution context from an existing check. No new brittle logic is added. No new authority is claimed.
61
+
62
+ ---
63
+
64
+ ## 5. Interactions
65
+
66
+ **Shadowing:** The pre-commit hook and the local pre-push hook both enforce section 5's requirement. This change scopes section 5 to local-only. The pre-commit hook is unchanged — it still runs on every commit. No shadowing occurs.
67
+
68
+ **Double-fire:** Section 5 currently runs both locally (pre-push) and in CI (via the test that invokes the gate script). After this change it only runs locally. No double-fire; in fact we're eliminating the accidental double-enforcement.
69
+
70
+ **Races:** No shared state involved. The check reads filesystem files (upgrade guides, side-effects dir). No concurrent access concern.
71
+
72
+ **Feedback loops:** None. The gate is a one-way exit check with no input to any system that feeds back.
73
+
74
+ ---
75
+
76
+ ## 6. External surfaces
77
+
78
+ - **Other agents:** No effect. The gate runs only in the instar repo's CI and in developer environments.
79
+ - **Install base users:** No effect. This is a developer tooling change, not a runtime change. `instar` as installed by users has no pre-push gate.
80
+ - **External systems:** No effect.
81
+ - **Persistent state:** No effect.
82
+ - **Timing/runtime:** The `CI` env var is set by GitHub Actions automatically for all runs. No timing dependency — it's present or absent at process start.
83
+
84
+ ---
85
+
86
+ ## 7. Rollback cost
87
+
88
+ Pure code change in `scripts/pre-push-gate.js`. Revert and ship a patch. No persistent state, no migration, no agent state repair. The only user-visible effect during the rollback window would be contributor PR CI runs again failing on missing side-effects artifacts — which is the exact condition we're fixing, not a new regression.
89
+
90
+ ---
91
+
92
+ ## Conclusion
93
+
94
+ The change is narrow and correct. It scopes section 5 of the pre-push gate to local developer contexts only, which matches the intent stated in the gate's own comment ("at push time"). The under-block exposure (a developer using `--no-verify` evading CI detection) is real but narrow: it requires bypassing two local enforcement hooks AND getting a PR merged without review catching the missing artifact. The pre-commit hook and PR review process are the remaining guards. The fix is clear to ship.
95
+
96
+ No design changes were made as a result of the review.
97
+
98
+ ---
99
+
100
+ ## Evidence pointers
101
+
102
+ - `tests/unit/pre-push-gate.test.ts` — all 6 tests pass locally after the change.
103
+ - `CI=true node scripts/pre-push-gate.js` — exits 0 on the current branch (which has the 0.28.49 versioned guide with fix/feature language but no fresh side-effects artifact for that version in CI context).
104
+ - Without `CI`, the gate still enforces section 5 (verified by the existing passing local test that runs the gate in a non-CI shell).
@@ -0,0 +1,104 @@
1
+ # Side-Effects Review — default skills: dynamic localhost port
2
+
3
+ **Version / slug:** `skill-port-dynamic-resolution`
4
+ **Date:** `2026-04-17`
5
+ **Author:** `dawn`
6
+ **Second-pass reviewer:** `not required`
7
+
8
+ ## Summary of the change
9
+
10
+ Two source changes. In `src/commands/init.ts`, every `http://localhost:${port}/...` URL inside `installBuiltinSkills` (and adjacent helpers that share the same file) is rewritten to emit `http://localhost:\${INSTAR_PORT:-${port}}/...`, so the generated `.claude/skills/*/SKILL.md` files contain a shell-expandable port reference instead of a number baked in at install time. In `src/core/PostUpdateMigrator.ts`, a new `migrateSkillPortHardcoding()` scans existing default-skill files for bare `http://localhost:NNNN/` URLs and rewrites them to `http://localhost:${INSTAR_PORT:-NNNN}/`, preserving the original port as the fallback default. The migration is scoped to the 14 known-default skill names and is idempotent. Test coverage: `tests/unit/PostUpdateMigrator-skillPortHardcoding.test.ts` — 6 cases.
11
+
12
+ ## Decision-point inventory
13
+
14
+ - `src/commands/init.ts` `installBuiltinSkills` — **modify** — replaces hardcoded port templating with runtime-expandable pattern. 93 occurrences, mechanical find/replace, all inside backtick template strings for shell-executed content.
15
+ - `src/core/PostUpdateMigrator.ts` `migrateSkillPortHardcoding` — **add** — new migration method. Called from `migrate()` between `migrateBuiltinSkills` and `migrateSelfKnowledgeTree`. Scoped to a fixed allowlist of 14 default skill names.
16
+ - `tests/unit/PostUpdateMigrator-skillPortHardcoding.test.ts` — **add** — regression coverage for the migration.
17
+
18
+ ---
19
+
20
+ ## 1. Over-block
21
+
22
+ No block/allow surface. The change is runtime port resolution in user-project skill files. No message content or agent action is gated.
23
+
24
+ Within the migration's own domain: the scan matches `/http:\/\/localhost:(\d+)\//g` in the default-skill set. This pattern is narrow enough that it will not false-positive on natural-language references ("localhost:4040" mentioned in prose without the URL form is untouched). Files outside the 14-name allowlist are never read, so custom skills are never modified — a principle the test suite asserts explicitly.
25
+
26
+ ---
27
+
28
+ ## 2. Under-block
29
+
30
+ No block surface existed before this change. The migration adds no new enforcement — it is a one-way content rewrite. There is nothing to under-block.
31
+
32
+ Edge case: if a user had a default-skill file with a mix of the new dynamic pattern and stray hardcoded ports (e.g., partial manual edits), the idempotency guard (`includes('${INSTAR_PORT:-')`) will cause the migration to skip the file entirely rather than finish the rewrite. That is the safe direction — migrating a partially-edited file risks corrupting the user's edits. Users in that state can manually finish the rewrite or delete the file and let `installBuiltinSkills` regenerate it.
33
+
34
+ ---
35
+
36
+ ## 3. Level-of-abstraction fit
37
+
38
+ The change is at the correct layer. The root cause was install-time templating of a value that should have been runtime-resolved. Fixing the template is the direct fix; fixing existing user files via migration is the correct catch-up mechanism. Neither change rearchitects the skill system — skills remain static markdown files, the only change is that a value inside them resolves later.
39
+
40
+ The dynamic pattern `${INSTAR_PORT:-PORT}` uses POSIX shell parameter expansion, the same primitive the rest of the Instar shell surface depends on. It is a recognized idiom inside curl-heavy bash content, not a novel construct the user has to learn.
41
+
42
+ ---
43
+
44
+ ## 4. Signal vs authority compliance
45
+
46
+ **Required reference:** [docs/signal-vs-authority.md](../../docs/signal-vs-authority.md)
47
+
48
+ **Does this change hold blocking authority with brittle logic?**
49
+
50
+ - [x] No — this change has no block/allow surface.
51
+
52
+ The change is a content rewrite inside skill files. It does not evaluate messages, gate agent actions, or constrain information flow. Signal-vs-authority applies to decision points that judge messages or block work. A port-expansion template does neither.
53
+
54
+ ---
55
+
56
+ ## 5. Interactions
57
+
58
+ **Shadowing:** `installBuiltinSkills` and `migrateSkillPortHardcoding` target overlapping surface. Order matters: `migrateBuiltinSkills` runs first (non-destructive, writes only missing files), then `migrateSkillPortHardcoding` runs (rewrites existing files). A skill newly written by `installBuiltinSkills` in the same migration pass already uses the dynamic pattern, so `migrateSkillPortHardcoding` will see the `${INSTAR_PORT:-` marker and no-op. No double-processing.
59
+
60
+ **Double-fire:** `migrateSkillPortHardcoding` is idempotent — once a file contains the dynamic marker, it is skipped. Test case `is idempotent on a second run after migration` covers this explicitly.
61
+
62
+ **Races:** `PostUpdateMigrator.migrate()` is sequential and runs once per `instar` update. No concurrent access to the same skill file is expected. If two updaters ran simultaneously, they would both read the hardcoded content, both rewrite it, and the second write would overwrite the first with identical content — no corruption.
63
+
64
+ **Feedback loops:** None. The migration is a one-shot rewrite; the rewritten content does not feed back into any system.
65
+
66
+ ---
67
+
68
+ ## 6. External surfaces
69
+
70
+ - **Other agents:** Each agent running instar will get the migration on next `instar` upgrade. Agents on non-default ports gain working skills; agents on port 4040 see no behavioral change (the fallback matches their previous hardcoded value).
71
+ - **Install base users:** Users with customized skill files (renamed default skills, heavily edited content) are protected by the allowlist and the dynamic-marker idempotency check. The migration touches only the 14 canonical default-skill files, and only if they still contain the bare-port pattern.
72
+ - **External systems:** None. The URL targets are all `localhost` — no external traffic shape changes.
73
+ - **Persistent state:** Skill files on disk are rewritten in place. No database, no config, no registry is touched. Rollback = `git checkout` of the skill file or `rm` and re-run `installBuiltinSkills`.
74
+ - **Timing/runtime:** The `${INSTAR_PORT:-NNNN}` expansion runs at shell invocation time. An agent with `INSTAR_PORT` unset gets the fallback; with it set, gets the override. Zero-cost at skill-read time; one environment variable lookup per curl.
75
+
76
+ ---
77
+
78
+ ## 7. Rollback cost
79
+
80
+ Low. Revert: `git revert` the two source commits; the emitted skills would return to hardcoded ports, matching pre-fix behavior. Users who already ran the migration would keep their dynamic-pattern skills, which continue to work (the fallback equals the previous hardcoded value). No persistent state to undo, no agent state to repair, no user communication required.
81
+
82
+ Narrow risk: if a user's `INSTAR_PORT` env var is set to an invalid value (e.g., a port the server isn't listening on), curls will fail after this change where they would have succeeded before on the hardcoded default. Mitigation: the variable is only consulted if the user explicitly exported it. The intersection of "exported `INSTAR_PORT`" and "set it wrong" is small and self-inflicted; the fix for that case is `unset INSTAR_PORT` or set it correctly.
83
+
84
+ ---
85
+
86
+ ## Conclusion
87
+
88
+ The change is narrow, well-scoped, and covered by regression tests. The template fix is mechanical and safe. The migration is scoped to a known allowlist, idempotent, and respects user customizations. The under-block surface is zero; the over-block surface is zero. The worst case in rollback is a return to the original bug, which affected only users on non-default ports and is already worked around today by hand-sed. Ship.
89
+
90
+ No design changes were made as a result of the review.
91
+
92
+ ---
93
+
94
+ ## Evidence pointers
95
+
96
+ - `tests/unit/PostUpdateMigrator-skillPortHardcoding.test.ts` — 6 tests pass:
97
+ - rewrites hardcoded ports in a default skill
98
+ - leaves already-dynamic skills untouched (idempotent)
99
+ - does not touch custom (non-default) skills
100
+ - is idempotent on a second run after migration
101
+ - skips when the skill file does not exist
102
+ - preserves the original port number in the fallback
103
+ - Live template verification: `node -e "const {installBuiltinSkills}=require('./dist/commands/init.js'); ..."` against a temp dir shows 13 of 14 default skills emit `localhost:${INSTAR_PORT:-4040}` and zero emit bare `localhost:4040` (the 14th skill, `autonomous`, is a stub that deploys separately and has no localhost URLs).
104
+ - Source-side verification: `grep -c 'localhost:${port}' src/commands/init.ts` = 0 after the rewrite (was 93).