instar 1.0.13 → 1.1.0

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 (57) hide show
  1. package/README.md +1 -1
  2. package/dist/cli.js +52 -2
  3. package/dist/cli.js.map +1 -1
  4. package/dist/commands/init.d.ts +17 -0
  5. package/dist/commands/init.d.ts.map +1 -1
  6. package/dist/commands/init.js +156 -64
  7. package/dist/commands/init.js.map +1 -1
  8. package/dist/commands/server.d.ts.map +1 -1
  9. package/dist/commands/server.js +68 -4
  10. package/dist/commands/server.js.map +1 -1
  11. package/dist/commands/setup.d.ts +3 -1
  12. package/dist/commands/setup.d.ts.map +1 -1
  13. package/dist/commands/setup.js +67 -28
  14. package/dist/commands/setup.js.map +1 -1
  15. package/dist/commands/worktree.d.ts +17 -4
  16. package/dist/commands/worktree.d.ts.map +1 -1
  17. package/dist/commands/worktree.js +28 -4
  18. package/dist/commands/worktree.js.map +1 -1
  19. package/dist/core/AgentWorktreeDetector.d.ts +63 -0
  20. package/dist/core/AgentWorktreeDetector.d.ts.map +1 -0
  21. package/dist/core/AgentWorktreeDetector.js +329 -0
  22. package/dist/core/AgentWorktreeDetector.js.map +1 -0
  23. package/dist/core/InstarWorktreeManager.d.ts +99 -0
  24. package/dist/core/InstarWorktreeManager.d.ts.map +1 -0
  25. package/dist/core/InstarWorktreeManager.js +447 -0
  26. package/dist/core/InstarWorktreeManager.js.map +1 -0
  27. package/dist/core/MachineIdentity.d.ts.map +1 -1
  28. package/dist/core/MachineIdentity.js +2 -0
  29. package/dist/core/MachineIdentity.js.map +1 -1
  30. package/dist/core/PostUpdateMigrator.d.ts +25 -0
  31. package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
  32. package/dist/core/PostUpdateMigrator.js +258 -0
  33. package/dist/core/PostUpdateMigrator.js.map +1 -1
  34. package/dist/monitoring/PromptGate.d.ts +12 -0
  35. package/dist/monitoring/PromptGate.d.ts.map +1 -1
  36. package/dist/monitoring/PromptGate.js +44 -0
  37. package/dist/monitoring/PromptGate.js.map +1 -1
  38. package/dist/scaffold/templates.d.ts.map +1 -1
  39. package/dist/scaffold/templates.js +17 -0
  40. package/dist/scaffold/templates.js.map +1 -1
  41. package/package.json +1 -1
  42. package/scripts/lint-no-direct-destructive.js +7 -0
  43. package/scripts/resolve-release-tier.mjs +192 -0
  44. package/src/data/builtin-manifest.json +56 -48
  45. package/src/scaffold/templates.ts +17 -0
  46. package/src/templates/scripts/instar-watchdog.sh +169 -6
  47. package/src/templates/scripts/instar-worktree-create.sh +79 -0
  48. package/upgrades/1.1.0.md +320 -0
  49. package/upgrades/side-effects/agent-worktree-convention-layer-1-2-5.md +280 -0
  50. package/upgrades/side-effects/agent-worktree-convention-layer-3-4.md +285 -0
  51. package/upgrades/side-effects/feat-fix-parent-framework-intercept.md +59 -0
  52. package/upgrades/side-effects/feat-install-claude-gating.md +67 -0
  53. package/upgrades/side-effects/feat-install-framework-flag.md +65 -0
  54. package/upgrades/side-effects/feat-portability-shadow-capabilities.md +63 -0
  55. package/upgrades/side-effects/feat-setup-framework-flag.md +65 -0
  56. package/upgrades/side-effects/lockdown-layer-2.md +123 -0
  57. package/upgrades/side-effects/watchdog-bind-failure-probe.md +174 -0
@@ -0,0 +1,67 @@
1
+ # Side-effects review — Codex-only init zero .claude/ (PR 2 of 4)
2
+
3
+ Per L6. Seven dimensions.
4
+
5
+ ## 1. Over-block / under-block
6
+
7
+ Before: UNDER — `--framework codex-cli` was expressed but ignored; all
8
+ `.claude/` writes fired anyway. After: precisely gated. Default (no flag,
9
+ or `claude-code`, or `both`) is byte-identical to v1.0.15. Only an explicit
10
+ codex-only choice changes anything.
11
+
12
+ ## 2. Level-of-abstraction fit
13
+
14
+ `claudeEnabled` is derived once per init function (or per refresh helper)
15
+ from the canonical `enabledFrameworks` value. The pure
16
+ `resolveEnabledFrameworks(choice)` helper (PR 1) is the single resolver
17
+ for flag-provided values; `refreshHooksAndSettings` and `refreshScripts`
18
+ read the persisted config directly with the same default semantics. No
19
+ brittle inline re-derivation.
20
+
21
+ ## 3. Signal vs Authority compliance
22
+
23
+ The flag (PR 1) and the persisted `enabledFrameworks` are the SIGNAL of
24
+ operator intent. Each init/refresh function's local `claudeEnabled`
25
+ variable is the single AUTHORITY for that function's gating. No leak of
26
+ the framework concept into the installer functions themselves
27
+ (`installClaudeSettings`, `installSmartFetch`, etc.) — they remain
28
+ unchanged.
29
+
30
+ ## 4. Interactions with adjacent systems
31
+
32
+ - **PR 1 (`--framework` flag)** — built on top. PR 2 makes the flag's
33
+ persisted value actually do something at install/refresh time.
34
+ - **`PostUpdateMigrator.getEnabledFrameworks()`** (v1.0.11) — uses the
35
+ same default semantics. The two readers are consistent.
36
+ - **`IdentityRenderer.renderNonClaudeIdentityShadows`** — unchanged, still
37
+ runs unconditionally for AGENTS.md/GEMINI.md.
38
+ - **`installHooks(stateDir)`** — unchanged; it writes to `.instar/hooks/`,
39
+ framework-neutral.
40
+ - **`migrateScripts` in PostUpdateMigrator** — already framework-aware for
41
+ the relay script (Gap 4 / v1.0.10). The `.claude/scripts/` writes in
42
+ `refreshScripts` are now consistently gated.
43
+
44
+ ## 5. Rollback cost
45
+
46
+ Low. Five gates in init.ts (all near-identical `if (claudeEnabled)` wraps)
47
+ + one new test file. `git revert` restores prior behavior. Existing
48
+ configs with or without `enabledFrameworks` continue to be readable by
49
+ older versions.
50
+
51
+ ## 6. Backwards compatibility / drift surface
52
+
53
+ Fully backward-compatible. Default behavior preserved. Drift surface:
54
+ none new — five gate sites read from the same persisted field with the
55
+ same default.
56
+
57
+ ## 7. Authorization / Trust posture
58
+
59
+ No new authority. The gates only *prevent* writes for codex-only installs;
60
+ they never grant new writes. Unreadable config defaults to `['claude-code']`
61
+ (status quo). Cannot escalate, cannot read additional resources.
62
+
63
+ ## Outcome
64
+
65
+ Ship. Operator-scoped, default-safe, fully tested (the codex-only zero-
66
+ `.claude/` guarantee is pinned by an isolated-fs end-to-end test). PR 2 of
67
+ 4 of the install/wizard portability series.
@@ -0,0 +1,65 @@
1
+ # Side-effects review — install --framework flag (PR 1 of 4)
2
+
3
+ Per L6. Seven dimensions.
4
+
5
+ ## 1. Over-block / under-block
6
+
7
+ Before: UNDER — no UI for framework choice; even with all downstream
8
+ machinery in place (PostUpdateMigrator, ParitySentinel, IdentityRenderer)
9
+ the install always produced Claude-shaped scaffolding. After: no
10
+ over-block. Default (no flag) is `['claude-code']` — byte-identical
11
+ to today's behavior. Only an explicit `--framework codex-cli` or `both`
12
+ changes the persisted config.
13
+
14
+ ## 2. Level-of-abstraction fit
15
+
16
+ CLI option lives in `cli.ts`. The flag value resolution is a pure helper
17
+ (`resolveEnabledFrameworks`) — testable in isolation, no side effects.
18
+ Config-write integration is at the three init paths' existing config
19
+ object literals. Correct altitude — option parsing at CLI boundary,
20
+ business logic in pure function, persistence at the existing write sites.
21
+
22
+ ## 3. Signal vs Authority compliance
23
+
24
+ The `--framework` flag is the SIGNAL of operator intent.
25
+ `resolveEnabledFrameworks()` is the single AUTHORITY for the persisted
26
+ shape. PostUpdateMigrator's `getEnabledFrameworks()` (added v1.0.11)
27
+ is the single reader. No brittle inline duplication.
28
+
29
+ ## 4. Interactions with adjacent systems
30
+
31
+ - **`PostUpdateMigrator.getEnabledFrameworks()`** (v1.0.11) already
32
+ reads this field with the same default. Now the field actually gets
33
+ written at install time. Consistent.
34
+ - **`FrameworkParitySentinel`** has its own `enabledFrameworks` config
35
+ (different object); names intentionally mirror, no shared mutable
36
+ state, no coupling introduced.
37
+ - **PRs 2-4** depend on this field being written; this PR makes them
38
+ possible without itself changing any gating.
39
+ - **Existing Claude-only installs** are unaffected (default `['claude-code']`
40
+ preserves all current writes).
41
+
42
+ ## 5. Rollback cost
43
+
44
+ Low. One CLI option add + one type field + one helper + three identical
45
+ config-literal additions + one test file. `git revert` restores prior
46
+ behavior; existing configs that have `enabledFrameworks` are still
47
+ readable by older versions (the field is optional everywhere).
48
+
49
+ ## 6. Backwards compatibility / drift surface
50
+
51
+ Fully backward-compatible: the flag is optional; the helper defaults
52
+ to historical behavior. Configs created without the flag have no
53
+ `enabledFrameworks` field; PostUpdateMigrator's helper already defaults
54
+ to `['claude-code']` when the field is unset. Drift surface: none — one
55
+ helper, one config field, three call sites using identical syntax.
56
+
57
+ ## 7. Authorization / Trust posture
58
+
59
+ No new authority. The flag only changes what's persisted at install
60
+ time. Cannot escalate privileges, cannot read additional resources.
61
+ Invalid flag values exit with error at parse time.
62
+
63
+ ## Outcome
64
+
65
+ Ship. Foundation for PRs 2-4. Default-safe, drift-reducing, tested.
@@ -0,0 +1,63 @@
1
+ # Side-effects review — Shadow capability mirror (Gap 6)
2
+
3
+ Per L6. Seven dimensions.
4
+
5
+ ## 1. Over-block / under-block
6
+
7
+ Before: UNDER — Codex/Gemini agents lacked capability instructions. After:
8
+ no over-block. Mirror runs only when a non-Claude shadow exists; appends
9
+ only sections absent from the shadow; never modifies CLAUDE.md.
10
+
11
+ ## 2. Level-of-abstraction fit
12
+
13
+ Sits alongside `migrateClaudeMd` in PostUpdateMigrator, runs immediately
14
+ after it. Same altitude. Slices from the live CLAUDE.md rather than
15
+ extracting section bodies into a shared array — a deliberate scope choice
16
+ per the operator's "minimal shim" decision.
17
+
18
+ ## 3. Signal vs Authority compliance
19
+
20
+ CLAUDE.md (just patched by `migrateClaudeMd`) is the AUTHORITY for section
21
+ content; the marker list is the SIGNAL of which sections exist. The shim
22
+ never invents content — it copies bytes from CLAUDE.md.
23
+
24
+ ## 4. Interactions with adjacent systems
25
+
26
+ - **migrateClaudeMd** — untouched; runs first, the shim reads what it
27
+ produced.
28
+ - **IdentityRenderer** (Gap 1) — untouched; runs at a different layer
29
+ (identity vs capability). The shim appends only capability sections, so
30
+ the AGENT.md-derived identity content at the top of AGENTS.md/GEMINI.md
31
+ is preserved (tested).
32
+ - **migrateFrameworkShadowCapabilities ordering** — runs before
33
+ migrateScripts and others; section appending is pure file I/O on the
34
+ shadows and does not block downstream steps.
35
+ - **Claude-only installs** — no shadow exists, no-op. Verified.
36
+
37
+ ## 5. Rollback cost
38
+
39
+ Low. One new private method + one new call site + one new test. `git
40
+ revert` restores prior behavior; on-disk shadows keep the appended
41
+ sections (inert; they'd just look like extra documentation). No state
42
+ migration.
43
+
44
+ ## 6. Backwards compatibility / drift surface
45
+
46
+ Claude installs: byte-identical. Non-Claude shadows: strictly better (now
47
+ have capability instructions). Drift surface: **reduced** — section
48
+ bodies live in exactly one place (CLAUDE.md, written by migrateClaudeMd)
49
+ and are sliced from there into shadows; there is no separate source array
50
+ to drift.
51
+
52
+ ## 7. Authorization / Trust posture
53
+
54
+ No new authority. Reads CLAUDE.md and the existing shadows the migrator
55
+ already owns. Writes only when appending material that came from
56
+ CLAUDE.md. Failure paths surface as captured errors in the MigrationResult,
57
+ never throw out of migrate().
58
+
59
+ ## Outcome
60
+
61
+ Ship. Operator-scoped, source-of-truth preserved (no duplicated section
62
+ bodies), idempotent, identity-preserving, Claude-only-safe, six-case
63
+ tested. Closes the v1.0.0 cross-framework portability audit at 6/6.
@@ -0,0 +1,65 @@
1
+ # Side-effects review — instar setup --framework + wizard launch (PR 3+4)
2
+
3
+ Per L6. Seven dimensions.
4
+
5
+ ## 1. Over-block / under-block
6
+
7
+ Before: UNDER — `setup.ts` exited if Claude wasn't installed, even for
8
+ Codex operators. After: precisely targeted. Default (no flag) is
9
+ 'claude-code', byte-identical to v1.0.16. Only an explicit
10
+ `--framework codex-cli` changes detection and spawn behavior.
11
+
12
+ ## 2. Level-of-abstraction fit
13
+
14
+ `runSetup({ framework? })` accepts the choice; `checkFrameworkPrerequisite`
15
+ from `src/core/Config.ts` is the single AUTHORITY for "is the chosen
16
+ runtime's binary installed?" — reused, not duplicated. Spawn shape is
17
+ two parallel branches (Claude and Codex) sharing the same wizard prompt
18
+ content. Correct altitude.
19
+
20
+ ## 3. Signal vs Authority compliance
21
+
22
+ The `--framework` flag is the operator-intent SIGNAL. `checkFrameworkPrerequisite`
23
+ is the single AUTHORITY for binary detection across the whole codebase
24
+ (also used by Config.load runtime startup). No new brittle inline checks.
25
+
26
+ ## 4. Interactions with adjacent systems
27
+
28
+ - **PR 1 + PR 2** — built on top. The flag now drives both install-time
29
+ scaffolding (PR 1+2) and wizard-time launch (PR 3+4) consistently.
30
+ - **`detectClaudePath` / `detectCodexPath`** — both already exported from
31
+ `src/core/Config.ts`. Used here without modification.
32
+ - **`checkFrameworkPrerequisite`** — existing function, used as-is.
33
+ - **Wizard skill content** (`.claude/skills/setup-wizard/SKILL.md`,
34
+ `secret-setup/SKILL.md`) — unchanged. Both runtimes are pointed at the
35
+ same files.
36
+ - **Playwright Telegram-setup flow** inside the wizard — unchanged. The
37
+ flow was already runtime-portable; this PR makes the entry path work
38
+ for Codex too.
39
+
40
+ ## 5. Rollback cost
41
+
42
+ Low. One function-signature change, one detection swap, two spawn-arg
43
+ branches, two CLI flag additions. `git revert` restores prior Claude-only
44
+ behavior. Existing Claude users are unaffected even mid-rollback.
45
+
46
+ ## 6. Backwards compatibility / drift surface
47
+
48
+ Fully backward-compatible. The optional `framework` parameter defaults
49
+ to `'claude-code'`. The bareword `npx instar` and `instar setup` without
50
+ the flag run exactly as v1.0.16 did. Drift surface: none — wizard
51
+ content remains in one place.
52
+
53
+ ## 7. Authorization / Trust posture
54
+
55
+ No new authority. The flag only changes which binary is spawned; both
56
+ existing spawn paths use `--dangerously-skip-permissions` (Claude) or
57
+ `--dangerously-bypass-approvals-and-sandbox` (Codex) — these are the same
58
+ sandbox-bypass postures the runtime-spawn builders already use elsewhere
59
+ in the codebase (`src/core/frameworkSessionLaunch.ts`). No new privilege.
60
+
61
+ ## Outcome
62
+
63
+ Ship. Operator-scoped, default-safe, no Claude-user regression risk,
64
+ the cohesive end-to-end Codex setup story finally works. End-to-end
65
+ verification follows as the next task (smoke test on this machine).
@@ -0,0 +1,123 @@
1
+ # Side-effects review — Deployment Lockdown Layer 2 (release-tier gate)
2
+
3
+ Per L6. Seven dimensions.
4
+
5
+ ## 1. Over-block / under-block
6
+
7
+ **Before.** UNDER: the publish workflow had no concept of a holding pattern.
8
+ Any merge to main with a non-template NEXT.md auto-published. There was no
9
+ expressible "no deploy" state in code — the operator could only say it in
10
+ chat, and chat does not feed the workflow. This is the exact gap that allowed
11
+ the 2026-05-19 misalignment (four v1.0.x PRs shipped as v0.28.x patches
12
+ during a session marked "no deploy").
13
+
14
+ **After.** No new over-block of routine maintenance: when an operator flips
15
+ `.instar/release-tier.json` to `tier: patch`, behavior is byte-for-byte
16
+ identical to v1.0.8's post-Layer-1 path. The new path is `tier: hold`, which
17
+ is precisely the previously-unexpressible "no deploy" state, plus `tier:
18
+ minor` and `tier: major`, which enforce that the package.json bump actually
19
+ matches the declared tier rather than letting any leap silently ship.
20
+
21
+ The single deliberate over-block is `tier: major`, which refuses publishes
22
+ until Layer 5 (multi-signature) ships. This is intentional — the spec
23
+ requires both `LOCAL.major > NPM.major` AND signature verification before a
24
+ major publish. Shipping a major with only single-agent authority would
25
+ defeat the purpose of the lockdown.
26
+
27
+ ## 2. Level-of-abstraction fit
28
+
29
+ The resolution policy lives in `scripts/resolve-release-tier.mjs` — pure
30
+ functions (`validateTierConfig`, `readTierConfig`, `resolveReleaseTier`)
31
+ with a thin CLI shim. The workflow calls it and routes on the printed
32
+ decision. Decision logic in testable code, orchestration in the workflow.
33
+ No logic buried in inline bash.
34
+
35
+ The committed `.instar/release-tier.json` file is the operator's authority;
36
+ the script is the deterministic reader; the workflow is the consumer. Three
37
+ roles, three files, no conflation.
38
+
39
+ ## 3. Signal vs Authority compliance
40
+
41
+ `.instar/release-tier.json` is the AUTHORITATIVE declaration of the active
42
+ release line. The script never blocks of its own initiative — it reads the
43
+ operator's committed authority and reports the corresponding decision. The
44
+ workflow honors that decision. This is signal-vs-authority done right: a
45
+ deterministic reader translating a committed operator signal into a
46
+ deterministic outcome.
47
+
48
+ No new authority is created by this layer. The publish authority (NPM_TOKEN)
49
+ is unchanged and still gated by branch protection + CI required checks. The
50
+ gate runs strictly before any side-effectful step, so a `skip` decision
51
+ prevents `npm publish`, `npm version`, the NEXT.md rename, and the
52
+ release-commit push from running at all.
53
+
54
+ ## 4. Interactions with adjacent systems
55
+
56
+ - **Layer 1 (`resolve-publish-version.mjs`).** Untouched. Layer 2 runs
57
+ BEFORE Layer 1 in the workflow: if the tier blocks, the version-bump step
58
+ never runs. If the tier allows, Layer 1 proceeds with its existing
59
+ package.json-as-authority logic.
60
+ - **NEXT.md skip-gate.** Untouched. The existing template-detection logic
61
+ still runs first; Layer 2's gate is only consulted when NEXT.md has
62
+ content. Tier=hold + template NEXT.md = both gates skip (overdetermined,
63
+ safe).
64
+ - **`check-upgrade-guide.js`.** Untouched. Only runs when both Layer 2 and
65
+ Layer 1 have allowed publish.
66
+ - **Pre-push hook / Husky.** Untouched. This is build-time infra in a
67
+ workflow YAML, not in a code path the pre-push hook examines.
68
+ - **Agent runtime.** No agent runtime code is modified. The lockdown is
69
+ infrastructure-only.
70
+
71
+ ## 5. Rollback cost
72
+
73
+ Low. Four files:
74
+
75
+ - `scripts/resolve-release-tier.mjs` (new)
76
+ - `tests/unit/resolve-release-tier.test.ts` (new)
77
+ - `.instar/release-tier.json` (new)
78
+ - `.github/workflows/publish.yml` (one added step, four downstream `if:`
79
+ conditions extended)
80
+
81
+ `git revert <merge-sha>` restores the pre-Layer-2 behavior. No state
82
+ migration. No deployed-agent impact. If the workflow YAML edit ever needs
83
+ to be undone without a revert, removing the `tier-gate` step and the four
84
+ extended `if:` clauses restores the prior path.
85
+
86
+ Operator can also unblock publishes immediately by changing
87
+ `.instar/release-tier.json` from `hold` to `patch` — a one-line commit, no
88
+ code changes required.
89
+
90
+ ## 6. Backwards compatibility / drift surface
91
+
92
+ The script defaults to `tier: patch` (pre-Layer-2 behavior) when
93
+ `.instar/release-tier.json` is absent. This means an older checkout, a
94
+ cherry-pick to a branch without the file, or any environment that loses
95
+ the file mid-flight degrades gracefully to the existing behavior — never
96
+ to a blocked state.
97
+
98
+ Drift surface: small. The tier file is the only new persistent input. It's
99
+ JSON with four allowed values; the validator rejects anything else with a
100
+ clear error. The `setAt` / `setBy` / `reason` fields are informational only
101
+ and the script does not use them for decisions, so format drift on those
102
+ fields is non-blocking.
103
+
104
+ ## 7. Authorization / Trust posture
105
+
106
+ No new authority is granted. The script reads a committed file and prints
107
+ a string. It cannot publish, cannot mutate state, cannot escalate. The
108
+ operator's authority is expressed via a normal git commit to
109
+ `.instar/release-tier.json` — reviewable, traceable, revertable, and
110
+ governed by the same branch protection that protects every other code
111
+ change.
112
+
113
+ The "hold" default in this release means the lockdown's headline guarantee
114
+ is engaged immediately on merge — no further auto-publish can occur until
115
+ an operator deliberately changes the tier. This is the spec's
116
+ operator-intent-first posture: opt out of hold rather than opt in.
117
+
118
+ ## Outcome
119
+
120
+ Ship. Minimal, incident-driven, fully unit-tested. The committed `hold`
121
+ tier delivers the headline "no chance of accidental major-feature
122
+ deployment" guarantee immediately on merge. Routine maintenance resumes
123
+ with a one-line tier change. Layers 3–7 ship as separate PRs.
@@ -0,0 +1,174 @@
1
+ # Side-Effects Review — Fleet watchdog bind-failure probe
2
+
3
+ **Version / slug:** `watchdog-bind-failure-probe`
4
+ **Date:** 2026-05-19
5
+ **Author:** echo
6
+ **Second-pass reviewer:** subagent (high-risk: touches "watchdog" trigger)
7
+ **Spec:** [docs/specs/watchdog-bind-failure-probe.md](../../docs/specs/watchdog-bind-failure-probe.md)
8
+ **ELI16:** [docs/specs/watchdog-bind-failure-probe.eli16.md](../../docs/specs/watchdog-bind-failure-probe.eli16.md)
9
+
10
+ ## Summary of the change
11
+
12
+ Adds a `probe_server_identity` function to the fleet watchdog. For every agent that launchd reports as loaded-and-running, the probe asks the agent's `/health` endpoint to confirm the server on its configured port actually belongs to it. If the port is unreachable or returns a project name belonging to a different agent, the watchdog logs a `BIND-FAIL` signal and runs it through the same `try_self_heal` → consecutive-fail-counter → `escalate_via_peer` pipeline as a launchd crash-loop.
13
+
14
+ When escalation fires for a BIND-FAIL with a known conflicting party, the alert payload uses a more specific summary: *"AI Guy hasn't been able to start its server because codex-server-smoke is using its port. My repair attempts haven't fixed it."* The fallback (no conflict context) is the existing generic "offline for about X minutes" copy. Both variants pass the existing B12-B14 health-alert ruleset; the `/attention` route's safe-template retry from PR #245 still backstops any tone-gate rejection.
15
+
16
+ Decision points the change touches:
17
+ - **`probe_server_identity`** (new) — structural detector, emits a signal, no blocking authority.
18
+ - **`handle_bind_fail`** (new) — routes the signal through the existing heal+escalate pipeline. No new authority surface.
19
+ - **`escalate_via_peer`** (modified) — reads optional conflict context to build a richer summary. The summary still goes through `MessagingToneGate` via `/attention`; the gate remains the only authority.
20
+ - **`reset_fail_counter`** (modified) — also clears the new `bind-fail-conflict` state file.
21
+
22
+ ## Decision-point inventory
23
+
24
+ - `probe_server_identity` — **add** — structural detector returning 0/2/3 + structured stdout.
25
+ - `handle_bind_fail` — **add** — routes BIND-FAIL into the existing heal+escalate pipeline.
26
+ - Main loop's healthy-PID branch — **modify** — now calls the probe before treating PID-alive as healthy.
27
+ - `escalate_via_peer` — **modify** — picks up optional conflict context for a more specific summary.
28
+ - `reset_fail_counter` — **modify** — also clears `bind-fail-conflict` marker.
29
+
30
+ ---
31
+
32
+ ## 1. Over-block
33
+
34
+ **What legitimate inputs does this change reject that it shouldn't?**
35
+
36
+ - **Probe — "wrong-project" false positive.** If the agent's label-derived expected project name doesn't match the `project` field in /health (e.g. someone renamed an agent without updating the launchd label, or an old instar version reports project under a different field name), the probe flags BIND-FAIL on a perfectly healthy agent. Mitigation: the probe FAILS OPEN when `project` is empty in the response (old instar versions don't expose it). Documented in test `returns 0 when project field is absent`. The label-mismatch case is harder — if you legitimately renamed an agent, you must update the launchd label too. The alert text would still be coherent ("X is offline because Y is using its port" — Y is the agent's actual project name, the user can act on it).
37
+ - **Probe — "unreachable" false positive during legitimate maintenance.** If a user manually stops an agent's server (e.g. for an upgrade) while the lifeline stays alive, the probe will report BIND-FAIL. After 3 cycles (~15 min) the user gets a Telegram alert. Acceptable — the user knows they stopped it and can ignore. Not common enough to justify a per-agent "muted" flag in this PR (deferrable to v3 Remediator).
38
+ - **Probe — `--max-time 5` curl timeout.** A server doing legitimate slow work that takes >5s to respond to `/health` would be flagged unreachable. The `/health` endpoint is intentionally fast (no DB hits, no LLM calls); 5s is comfortable. If a future change makes `/health` slower, this test fails first.
39
+ - **No false alarms for lifeline-only agents.** Agents with no `port` in config.json skip the probe entirely (return 0). Test confirms.
40
+
41
+ ---
42
+
43
+ ## 2. Under-block
44
+
45
+ **What failure modes does this still miss?**
46
+
47
+ - **Slow port-collision.** If two agents race for the same port at boot, the probe will only see the winner. The loser's lifeline is still up. The probe correctly catches this (the loser's expected project doesn't match the winner's). But if both processes alternate (one fails immediately, the other binds, then they swap on next launchd respawn), the probe sees inconsistent states across cycles and may take longer to escalate. Acceptable — the consecutive-fail counter handles the noise.
48
+ - **Wrong port in config.json.** If config.json says port 5050 but the lifeline actually starts the server on a different port (config drift), the probe will hit the wrong port and false-alarm. This shouldn't happen in practice — the lifeline reads the same config.json the probe reads. But if a future change adds runtime port mutation, this needs revisiting.
49
+ - **Server identifies itself with a project name that's not the label minus prefix.** If the agent's `agentName` in config differs from its launchd label (e.g. user customized one), the probe false-alarms. Mitigation deferred — current convention is they match, and a sentinel test could enforce this convention going forward.
50
+ - **Tone gate rejects both attempts (initial + safe template).** Same backstop as PR #245: counter preserved, retried next cycle. No regression from this PR.
51
+
52
+ ---
53
+
54
+ ## 3. Level-of-abstraction fit
55
+
56
+ **Is this at the right layer?**
57
+
58
+ - **Probe in the watchdog (this PR).** The watchdog is the OUTSIDE-the-lifeline observer with the cross-agent visibility (it sees all plists + can correlate `expected agent X on port P` against `who's actually on port P`). The lifeline can't do this cross-agent view from inside itself. So watchdog is the right layer.
59
+ - **In-supervisor bind-detection (PR #111) remains.** That layer catches the per-agent case where the server's own preflight detects bind failure. This PR catches the failure mode where the supervisor's alerts are being SUPPRESSED as duplicates (the exact failure that bit AI Guy). Two layers, two signals, same authority for the user-facing alert.
60
+ - **Not at the v3 Remediator layer yet.** v3 will absorb both layers under a unified probe registry + runbook system. This PR is the minimum plumbing until v3 ships. Both specs explicitly agree on the absorption point.
61
+
62
+ ---
63
+
64
+ ## 4. Signal vs authority compliance
65
+
66
+ **Required reference:** [docs/signal-vs-authority.md](../../docs/signal-vs-authority.md)
67
+
68
+ **Does this change hold blocking authority with brittle logic?**
69
+
70
+ - [x] **No — this change produces a signal consumed by an existing smart gate.**
71
+
72
+ Detailed:
73
+ - `probe_server_identity` is a structural detector: file existence (config.json), HTTP code, JSON field equality. No judgment, no blocking. Returns numeric codes + structured stdout.
74
+ - The consecutive-fail counter is a deterministic threshold (mechanic, explicitly exempt per the principle's §"When this principle does NOT apply").
75
+ - `escalate_via_peer` builds a CANDIDATE message. The decision to ship that candidate, reshape it, or fall back to the SAFE template rests entirely with `MessagingToneGate` via the `/attention` route — the existing authority. This PR does not modify the gate; it just supplies a different candidate.
76
+ - The new conflict-aware summary copy was hand-crafted to pass B12 (no jargon: no "lifeline", "launchd", "pid", "crash-loop", "shadow-install"). A unit test screens the payload heredoc for these terms; future drift would fail the test.
77
+
78
+ This PR adds NO new blocking authority.
79
+
80
+ ---
81
+
82
+ ## 5. Interactions
83
+
84
+ **Does this interact with existing checks, recovery paths, or infrastructure?**
85
+
86
+ - **Shadowing:**
87
+ - The bind-probe runs INSIDE the healthy-PID branch. Before: PID exists → OK. After: PID exists + probe passes → OK; PID exists + probe fails → BIND-FAIL → heal pipeline. The OK path is now slower (one curl per agent per cycle, capped at 5s). Acceptable: cycles run every 5 min, so an extra ~30ms per agent is negligible.
88
+ - The crash-loop branch (exit_status != 0) is unchanged. PR #245's path is preserved exactly.
89
+ - **Double-fire with supervisor's internal bind-detection (PR #111):**
90
+ - The supervisor's in-process detection emits a `DegradationReporter` event INSIDE the lifeline. That event tries to route through ToneGate → /attention on the SAME agent's server. But the failing agent's server is locked out — so the in-process route is broken precisely when it's needed. The watchdog's external probe + peer-escalation is what actually surfaces the failure. The two paths can coexist; the peer-routed POST will win (it actually reaches Telegram). Idempotency-by-id in `/attention` prevents duplicate topic creation if both ever land.
91
+ - **Races:**
92
+ - State files (`*.consecutive-heal-fails`, `*.bind-fail-conflict`) are per-label. Concurrent watchdog runs are prevented by launchd's single-instance default. Manual invocation could race, but the script's writes are atomic single-line files.
93
+ - The probe-vs-healing race: if heal succeeds between probe-time and the launchd reload, the next cycle's probe sees healthy state and resets counters. No persistent stuck state.
94
+ - **Feedback loops:**
95
+ - When the user replies to the escalation Telegram topic, the reply lands at the PEER that escalated, not the failing agent. Documented in PR #245's review (same surface, no new behavior here).
96
+ - The bind-fail-conflict marker is cleared on (a) probe-pass, (b) escalation success, (c) crash-loop reset. No way for stale conflict context to leak into future escalations.
97
+
98
+ ---
99
+
100
+ ## 6. External surfaces
101
+
102
+ **Does this change anything visible outside the immediate code path?**
103
+
104
+ - **Other agents on the same machine:** YES — adds one `curl /health` per agent per 5-minute cycle. Negligible load, no auth required for /health.
105
+ - **Other users of the install base:** YES — ships in next release via PostUpdateMigrator. macOS-only effect.
106
+ - **External systems:**
107
+ - Telegram: a new failure-mode class (port collision) now produces a Telegram topic. Same authority gates it.
108
+ - `/attention` route: unchanged behaviorally; this PR is a new caller.
109
+ - **Persistent state:**
110
+ - New marker file `bind-fail-conflict` in `~/.instar/watchdog-state/`. Single-line, agent label, harmless.
111
+ - Existing `consecutive-heal-fails` markers are now shared between crash-loop and bind-fail paths — they both increment / reset the same counter. Test confirms reset_fail_counter clears both.
112
+ - **Timing:** Probe curl adds ~30ms-5s per agent per cycle (capped at 5s). For a 10-agent machine, max overhead is 50s of a 300s cycle. Acceptable.
113
+
114
+ ---
115
+
116
+ ## 7. Rollback cost
117
+
118
+ **If this turns out wrong in production, what's the back-out?**
119
+
120
+ - **Hot-fix:** revert `src/templates/scripts/instar-watchdog.sh` to the PR-#245 state. The next agent update overwrites the user-level script via `PostUpdateMigrator.migrateFleetWatchdog`. Ship as a patch release.
121
+ - **Data migration:** none. `bind-fail-conflict` marker files in `~/.instar/watchdog-state/` are harmless if left behind (auto-pruned by the existing 24-hour cleanup that already covers `last-heal` files; the regex could be widened later if needed).
122
+ - **Agent state repair:** none required.
123
+ - **User visibility during rollback:** minimal. The watchdog reverts to its pre-this-PR behavior — no bind-fail detection, no false alarms either.
124
+ - **Estimated total:** ~30 min revert + release cycle.
125
+
126
+ ---
127
+
128
+ ## Addendum 2026-05-19 — Reviewer findings + fixes
129
+
130
+ Second-pass reviewer found one critical defect plus two minors. All addressed:
131
+
132
+ **Critical — auth gating on `/health.project`.** The probe called `/health` without an Authorization header. The route only sets `base.project` inside `if (isAuthed)` (`src/server/routes.ts:1157`), so production probes would have received responses with no `project` field, hitting the fail-open branch, and the wrong-project detection — the entire reason for this PR — would never have fired in production. Fix: probe now reads `authToken` alongside `port` from `config.json` and sends `Authorization: Bearer <token>` with the `/health` request. Two new tests cover this (one shell-level asserting the header lands in the actual HTTP request; one template-content asserting the script references `authToken` + `Authorization: Bearer`).
133
+
134
+ **Minor 1 — state-file cleanup regex.** The existing 24h cleanup matched only `*.last-heal`. Widened to `*.last-heal | *.consecutive-heal-fails | *.bind-fail-conflict` so uninstalled-agent state doesn't accumulate. Documented inline.
135
+
136
+ **Minor 2 — uninstalled-peer cleanup path not tested.** Deferred. The widened regex is exercised by the existing watchdog-cycle path; standalone test of the cleanup is mechanical and adds little value given the simple find expression.
137
+
138
+ **Confirmed sound (no change needed):** signal-vs-authority compliance, PR #245 idempotency-by-id holds via `TelegramAdapter.createAttentionItem`'s `attentionItems.has(item.id)` short-circuit, 5s curl timeout is appropriate for macOS-only production target, migration parity covered by `PostUpdateMigrator.migrateFleetWatchdog`.
139
+
140
+ ## Conclusion
141
+
142
+ This PR adds the second layer of bind-failure detection — outside the lifeline, with cross-agent visibility — that closes the failure-mode that kept AI Guy offline for two days post-PR-#245. No new authority over message flow; the probe is a structural detector that feeds the existing tone-gated escalation pipeline. The conflict-aware summary copy was crafted to pass B12 and is unit-tested for jargon. Two new state markers, both harmless on rollback.
143
+
144
+ 17 new tests added (11 template-content unit tests + 4 darwin-gated behavioural unit tests + 2 darwin-gated integration tests covering the full pipeline). Cross-platform template assertions ensure the production-code paths stay consistent across Linux CI runs.
145
+
146
+ Clear to ship.
147
+
148
+ ---
149
+
150
+ ## Second-pass review (if required)
151
+
152
+ **Reviewer:** second-pass subagent (Opus 4.7 1M, 2026-05-19)
153
+ **Independent read of the artifact: concern**
154
+
155
+ Signal-vs-authority compliance and rollback cost check out. The probe is structurally a detector, the counter is mechanic, and the candidate message goes through `MessagingToneGate` via `/attention` — same authority, no new blocker. PR #245 idempotency-by-id is real: `TelegramAdapter.createAttentionItem` at `src/messaging/TelegramAdapter.ts:2949` short-circuits on `attentionItems.has(item.id)`, so a double-fire from PR #111's in-process path and this PR's external path cannot duplicate the topic. However:
156
+
157
+ - **(CRITICAL) Wrong-project detection is dead-on-arrival in production.** The `/health` route only includes `base.project` inside the `isAuthed` branch (`src/server/routes.ts:1140` opens `if (isAuthed)`, `1157` sets `base.project`). The probe's curl at `src/templates/scripts/instar-watchdog.sh:444` sends no `Authorization` header, so every peer agent's `/health` response will have `project` absent → probe hits the fail-open branch at `instar-watchdog.sh:460-464` (`project_field == ""` → `return 0`). The original AI-Guy-behind-codex-server-smoke incident was exactly a wrong-project case (200 response, foreign project name), so this PR would NOT have caught it. The unit test passes because the mock at `tests/unit/watchdog-bind-probe.test.ts:160` returns `project` unconditionally regardless of auth. **Recommended resolution:** either (a) read `authToken` from the agent's own `config.json` (we already read `port` from there) and send `Authorization: Bearer $auth` on the probe, or (b) expose `project` in the unauthenticated `/health` body — `projectName` is already public via launchd plist paths, so this is not a secret. (a) is the smaller change; (b) is cleaner. Either way, add a darwin-gated probe test that exercises the actual route handler (or a fixture that mirrors its auth-gated shape) instead of a mock that hand-waves the auth.
158
+
159
+ - **(MINOR) Stale state-file claim under-counts the gap.** Artifact §7 says `bind-fail-conflict` markers are "auto-pruned by the existing 24-hour cleanup that already covers `last-heal` files." `instar-watchdog.sh:623` is `find ... -name "*.last-heal" -mmin +1440 -delete` — it does not match `*.bind-fail-conflict` or `*.consecutive-heal-fails`. In practice `reset_fail_counter` clears both on every successful probe/escalation, so growth is bounded for active agents. But a launched-then-deleted agent (plist gone, marker left behind) will leak both files until a human cleans `~/.instar/watchdog-state/`. **Recommended resolution:** widen the regex to `\( -name "*.last-heal" -o -name "*.bind-fail-conflict" -o -name "*.consecutive-heal-fails" \)`, or document the leak as accepted with an issue link.
160
+
161
+ - **(MINOR) Coverage gap — uninstalled-peer leak.** No test asserts that markers for a label whose plist no longer exists eventually get cleaned. Combined with the regex above, this is the realistic stale-state path. Add a unit test that creates a marker, removes the plist, runs a cycle, and asserts the marker is gone (after fixing the regex).
162
+
163
+ Counter exclusion to surface: 5s timeout is fine for macOS-only production; `/health` is intentionally cheap (no DB/LLM) and the cached session count already protects against event-loop stalls (`routes.ts:1097`). No concern there.
164
+
165
+ Once the auth header (or unauth `project` exposure) is wired, this PR delivers what the spec promises. Until then, it ships a probe that emits the right log line in dev fixtures and silently fails open in production.
166
+
167
+ ---
168
+
169
+ ## Evidence pointers
170
+
171
+ - Spec: `docs/specs/watchdog-bind-failure-probe.md` (with ELI16 companion).
172
+ - Tests: `tests/unit/watchdog-bind-probe.test.ts` (15 tests), `tests/integration/watchdog-bind-fail-escalation.test.ts` (2 tests).
173
+ - Incident reference: AI Guy port collision with codex-server-smoke, 2026-05-17 → 2026-05-19, topic 5447. Lifeline supervisor logged "Suppressing duplicate server down notification (4163 suppressed this outage)" — captured in `~/Documents/Projects/ai-guy/.instar/logs/lifeline-launchd.log`.
174
+ - Recovery: manual unload of codex-server-smoke plist + restart of ai-guy launchd job. After this PR ships, the same configuration error would have surfaced to the user via Telegram within ~15 min instead of staying silent for 2 days.