instar 1.3.726 → 1.3.728

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 (32) hide show
  1. package/dist/commands/server.d.ts.map +1 -1
  2. package/dist/commands/server.js +53 -4
  3. package/dist/commands/server.js.map +1 -1
  4. package/dist/core/IntelligenceRouter.d.ts +57 -0
  5. package/dist/core/IntelligenceRouter.d.ts.map +1 -1
  6. package/dist/core/IntelligenceRouter.js +135 -20
  7. package/dist/core/IntelligenceRouter.js.map +1 -1
  8. package/dist/core/types.d.ts +39 -0
  9. package/dist/core/types.d.ts.map +1 -1
  10. package/dist/core/types.js.map +1 -1
  11. package/dist/messaging/slack/SlackAdapter.d.ts +15 -0
  12. package/dist/messaging/slack/SlackAdapter.d.ts.map +1 -1
  13. package/dist/messaging/slack/SlackAdapter.js +20 -2
  14. package/dist/messaging/slack/SlackAdapter.js.map +1 -1
  15. package/dist/messaging/slack/types.d.ts +38 -0
  16. package/dist/messaging/slack/types.d.ts.map +1 -1
  17. package/dist/messaging/slack/types.js +4 -0
  18. package/dist/messaging/slack/types.js.map +1 -1
  19. package/dist/permissions/TestWorkspacePrincipalSource.d.ts +133 -0
  20. package/dist/permissions/TestWorkspacePrincipalSource.d.ts.map +1 -0
  21. package/dist/permissions/TestWorkspacePrincipalSource.js +189 -0
  22. package/dist/permissions/TestWorkspacePrincipalSource.js.map +1 -0
  23. package/dist/permissions/index.d.ts +1 -0
  24. package/dist/permissions/index.d.ts.map +1 -1
  25. package/dist/permissions/index.js +1 -0
  26. package/dist/permissions/index.js.map +1 -1
  27. package/package.json +1 -1
  28. package/src/data/builtin-manifest.json +2 -2
  29. package/upgrades/1.3.728.md +215 -0
  30. package/upgrades/side-effects/per-target-swap-timeout.md +120 -0
  31. package/upgrades/side-effects/slack-test-workspace-principal-source.md +227 -0
  32. package/upgrades/1.3.726.md +0 -61
@@ -0,0 +1,189 @@
1
+ /**
2
+ * TestWorkspacePrincipalSource — the sanctioned, workspace-scoped principal source
3
+ * for the Slack live-test scenario cast (roadmap 0.3 entry condition).
4
+ *
5
+ * WHY THIS EXISTS (the 2026-07-01 lesson): the June scenario drives seeded the
6
+ * five-seat test cast into the production user registry (`users.json`). The
7
+ * silent-loss registry rebuild removed them, and the fixture-identity guard
8
+ * ("Test Identity Never Enters Production State", silent-loss-refusal-conservation
9
+ * §2.D) now — correctly — refuses to let them back in. Principal resolution for
10
+ * the live-test workspace therefore needs a home that is NOT the production
11
+ * registry. This module is that home.
12
+ *
13
+ * THE PARTITION INVARIANT (the load-bearing design decision):
14
+ * - The production registry REFUSES fixture identities (the existing guard,
15
+ * untouched by this module).
16
+ * - This source ACCEPTS ONLY fixture identities — every cast entry's
17
+ * slackUserId must match the SAME single matcher the production guard uses
18
+ * (`matchesTestIdentityToken` from `users/testIdentityMarkers.ts`). A
19
+ * non-fixture UID is refused at load, loudly.
20
+ * By construction an identity can live in exactly one of the two stores —
21
+ * a real employee's UID can never be smuggled into the cast to gain a role
22
+ * outside the audited registration path, and standing up a NEW test cast
23
+ * structurally forces the fixture-marker list to be updated first (which is
24
+ * what keeps the production guard aware of the new fixtures). The single
25
+ * matcher can never drift into two lists.
26
+ *
27
+ * THE SELF-DECLARATION MARKER (fail-closed to ignoring the cast): the config
28
+ * object MUST self-declare `testWorkspace: true`. Without that marker the source
29
+ * refuses to load a SINGLE principal — it disables itself, emits ONE loud log
30
+ * line, and every lookup returns null (production resolution is byte-identical to
31
+ * having no cast at all). The marker exists so a cast config can never be
32
+ * activated by accident: sanctioning a live-test workspace is a deliberate,
33
+ * self-evident opt-in, never an implicit side effect of the block existing.
34
+ *
35
+ * SCOPING (fail-closed): the cast resolves ONLY while the Slack adapter's
36
+ * VERIFIED connected team id (captured from Slack's own `auth.test` response at
37
+ * adapter start — never from config alone) equals the configured test
38
+ * `workspaceId`. Before verification, on verification failure, or when the
39
+ * adapter is connected to any other workspace, every lookup returns null and
40
+ * the resolver falls through to its existing safe default (unregistered guest).
41
+ *
42
+ * READ-ONLY BY DESIGN: this source never writes anything — not users.json, not
43
+ * a state file. It is consulted ONLY by the permission gate's principal
44
+ * resolver (production-registry-first via {@link ChainedUserLookup}); it is
45
+ * invisible to UserManager, sender auth (`authorizedUserIds`), cross-machine
46
+ * replication, and every other identity surface.
47
+ *
48
+ * Note on imports: `testIdentityMarkers.ts` is dependency-light (node builtins +
49
+ * a type-only core import), so reusing its matcher keeps the permission module
50
+ * free of runtime core dependencies — and keeps ONE fixture matcher, not two.
51
+ *
52
+ * Design: docs/specs/SLACK-ORG-INTEGRATION-SPEC.md §6.2.1;
53
+ * runbook: docs/specs/SLACK-ORG-TEST-WORKSPACE-RUNBOOK.md.
54
+ */
55
+ import { matchesTestIdentityToken } from '../users/testIdentityMarkers.js';
56
+ import { ORG_ROLES } from './types.js';
57
+ /**
58
+ * Hard ceiling on cast seats. The scenario cast is five seats today; the cap
59
+ * exists so the test-cast path can never quietly become a shadow user registry.
60
+ */
61
+ export const MAX_TEST_CAST_SEATS = 12;
62
+ export class TestWorkspacePrincipalSource {
63
+ bySlackId = new Map();
64
+ workspaceId;
65
+ getVerifiedWorkspaceId;
66
+ /** Entries refused at load (surfaced loudly by the wiring site). */
67
+ rejected = [];
68
+ /**
69
+ * True when the whole source refused to load (the self-declaration marker was
70
+ * absent). A disabled source holds ZERO principals and resolves everything to
71
+ * null — production resolution is byte-identical to having no cast configured.
72
+ */
73
+ disabled = false;
74
+ /** Why the source disabled itself (only set when {@link disabled} is true). */
75
+ disabledReason;
76
+ constructor(opts) {
77
+ if (!opts.workspaceId || typeof opts.workspaceId !== 'string' || !opts.workspaceId.trim()) {
78
+ throw new Error('TestWorkspacePrincipalSource requires a non-empty workspaceId (the sanctioned live-test workspace)');
79
+ }
80
+ // Stored TRIMMED (second-pass review note): Slack's own `team_id` never
81
+ // carries whitespace, so a config value with a stray space would otherwise
82
+ // make the scope equality permanently false — an inert cast (fail-closed,
83
+ // but a silent config foot-gun the boot log alone would have to catch).
84
+ this.workspaceId = opts.workspaceId.trim();
85
+ this.getVerifiedWorkspaceId = opts.getVerifiedWorkspaceId;
86
+ // THE SELF-DECLARATION GATE (fail-closed): without an explicit
87
+ // `testWorkspace: true` the cast is IGNORED entirely — zero principals
88
+ // loaded, one loud line, no throw, no production impact. The loud line lives
89
+ // HERE (not only at the wiring site) so the fail-closed guarantee holds for
90
+ // EVERY caller — Structure > Willpower.
91
+ if (opts.testWorkspace !== true) {
92
+ this.disabled = true;
93
+ this.disabledReason = 'missing-testWorkspace-marker';
94
+ console.warn(`[slack] TestWorkspacePrincipalSource IGNORED for workspace ${this.workspaceId} — the testCast config is ` +
95
+ `missing its required "testWorkspace: true" self-declaration. Loading ZERO cast principals (fail-closed; the ` +
96
+ `production user registry is unaffected). Add "testWorkspace": true to sanction the live-test cast.`);
97
+ return; // no principals admitted
98
+ }
99
+ for (const entry of opts.principals ?? []) {
100
+ const slackUserId = typeof entry?.slackUserId === 'string' ? entry.slackUserId.trim() : '';
101
+ if (!slackUserId) {
102
+ this.rejected.push({ slackUserId: String(entry?.slackUserId ?? ''), reason: 'missing-slack-user-id' });
103
+ continue;
104
+ }
105
+ // THE PARTITION INVARIANT: only fixture-marker identities are admitted.
106
+ // Same single matcher as the production guard — never a second list.
107
+ if (!matchesTestIdentityToken(slackUserId)) {
108
+ this.rejected.push({ slackUserId, reason: 'not-a-fixture-identity' });
109
+ continue;
110
+ }
111
+ if (!entry.orgRole || !ORG_ROLES.includes(entry.orgRole)) {
112
+ this.rejected.push({ slackUserId, reason: 'invalid-role' });
113
+ continue;
114
+ }
115
+ if (this.bySlackId.has(slackUserId)) {
116
+ this.rejected.push({ slackUserId, reason: 'duplicate' });
117
+ continue;
118
+ }
119
+ if (this.bySlackId.size >= MAX_TEST_CAST_SEATS) {
120
+ this.rejected.push({ slackUserId, reason: 'cast-cap-exceeded' });
121
+ continue;
122
+ }
123
+ this.bySlackId.set(slackUserId, {
124
+ // Namespaced id so a cast principal is visibly a test seat in every
125
+ // ledger row / audit surface it appears in.
126
+ id: `test-cast:${slackUserId}`,
127
+ name: entry.name?.trim() || slackUserId,
128
+ permissions: [entry.orgRole],
129
+ orgRole: entry.orgRole,
130
+ });
131
+ }
132
+ }
133
+ /** Number of admitted cast seats. */
134
+ get size() {
135
+ return this.bySlackId.size;
136
+ }
137
+ /**
138
+ * Resolve a cast seat — ONLY while the verified connected workspace equals the
139
+ * sanctioned test workspace. Any uncertainty (no verified id yet, supplier
140
+ * throw, mismatch) resolves null: fail-closed to unregistered guest.
141
+ */
142
+ resolveFromSlackUserId(slackUserId) {
143
+ // A disabled source (missing marker) holds no principals — this is redundant
144
+ // with the empty map but makes the fail-closed contract explicit.
145
+ if (this.disabled)
146
+ return null;
147
+ let verified;
148
+ try {
149
+ verified = this.getVerifiedWorkspaceId();
150
+ }
151
+ catch {
152
+ // Fail-closed: an errored verification supplier keeps the cast inert.
153
+ return null;
154
+ }
155
+ if (!verified || verified !== this.workspaceId)
156
+ return null;
157
+ return this.bySlackId.get(slackUserId) ?? null;
158
+ }
159
+ }
160
+ /**
161
+ * ChainedUserLookup — production-registry-first principal resolution.
162
+ *
163
+ * Sources are consulted in order; the first non-null record wins. The wiring
164
+ * site puts the production registry FIRST, so a genuinely registered user can
165
+ * never be shadowed or role-escalated by a cast entry, and the cast is only
166
+ * ever a fallback for identities the production registry does not know.
167
+ * A throwing source is skipped (resolution must never break the message path).
168
+ */
169
+ export class ChainedUserLookup {
170
+ sources;
171
+ constructor(sources) {
172
+ this.sources = sources;
173
+ }
174
+ resolveFromSlackUserId(slackUserId) {
175
+ for (const source of this.sources) {
176
+ try {
177
+ const record = source.resolveFromSlackUserId(slackUserId);
178
+ if (record)
179
+ return record;
180
+ }
181
+ catch {
182
+ // A faulty source must never break principal resolution — fall through
183
+ // to the next source (ultimately the unregistered-guest default).
184
+ }
185
+ }
186
+ return null;
187
+ }
188
+ }
189
+ //# sourceMappingURL=TestWorkspacePrincipalSource.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"TestWorkspacePrincipalSource.js","sourceRoot":"","sources":["../../src/permissions/TestWorkspacePrincipalSource.ts"],"names":[],"mappings":"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"}
@@ -16,4 +16,5 @@ export * from './MandateBackedGrantStore.js';
16
16
  export * from './SlackPrincipalResolver.js';
17
17
  export * from './SlackPermissionObserver.js';
18
18
  export * from './SlackUserRegistry.js';
19
+ export * from './TestWorkspacePrincipalSource.js';
19
20
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/permissions/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,cAAc,YAAY,CAAC;AAC3B,cAAc,iBAAiB,CAAC;AAChC,cAAc,uBAAuB,CAAC;AACtC,cAAc,0BAA0B,CAAC;AACzC,cAAc,8BAA8B,CAAC;AAC7C,cAAc,oBAAoB,CAAC;AACnC,cAAc,gCAAgC,CAAC;AAC/C,cAAc,gCAAgC,CAAC;AAC/C,cAAc,+BAA+B,CAAC;AAC9C,cAAc,0BAA0B,CAAC;AACzC,cAAc,8BAA8B,CAAC;AAC7C,cAAc,6BAA6B,CAAC;AAC5C,cAAc,8BAA8B,CAAC;AAC7C,cAAc,wBAAwB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/permissions/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,cAAc,YAAY,CAAC;AAC3B,cAAc,iBAAiB,CAAC;AAChC,cAAc,uBAAuB,CAAC;AACtC,cAAc,0BAA0B,CAAC;AACzC,cAAc,8BAA8B,CAAC;AAC7C,cAAc,oBAAoB,CAAC;AACnC,cAAc,gCAAgC,CAAC;AAC/C,cAAc,gCAAgC,CAAC;AAC/C,cAAc,+BAA+B,CAAC;AAC9C,cAAc,0BAA0B,CAAC;AACzC,cAAc,8BAA8B,CAAC;AAC7C,cAAc,6BAA6B,CAAC;AAC5C,cAAc,8BAA8B,CAAC;AAC7C,cAAc,wBAAwB,CAAC;AACvC,cAAc,mCAAmC,CAAC"}
@@ -16,4 +16,5 @@ export * from './MandateBackedGrantStore.js';
16
16
  export * from './SlackPrincipalResolver.js';
17
17
  export * from './SlackPermissionObserver.js';
18
18
  export * from './SlackUserRegistry.js';
19
+ export * from './TestWorkspacePrincipalSource.js';
19
20
  //# sourceMappingURL=index.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/permissions/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,cAAc,YAAY,CAAC;AAC3B,cAAc,iBAAiB,CAAC;AAChC,cAAc,uBAAuB,CAAC;AACtC,cAAc,0BAA0B,CAAC;AACzC,cAAc,8BAA8B,CAAC;AAC7C,cAAc,oBAAoB,CAAC;AACnC,cAAc,gCAAgC,CAAC;AAC/C,cAAc,gCAAgC,CAAC;AAC/C,cAAc,+BAA+B,CAAC;AAC9C,cAAc,0BAA0B,CAAC;AACzC,cAAc,8BAA8B,CAAC;AAC7C,cAAc,6BAA6B,CAAC;AAC5C,cAAc,8BAA8B,CAAC;AAC7C,cAAc,wBAAwB,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/permissions/index.ts"],"names":[],"mappings":"AAAA;;;GAGG;AACH,cAAc,YAAY,CAAC;AAC3B,cAAc,iBAAiB,CAAC;AAChC,cAAc,uBAAuB,CAAC;AACtC,cAAc,0BAA0B,CAAC;AACzC,cAAc,8BAA8B,CAAC;AAC7C,cAAc,oBAAoB,CAAC;AACnC,cAAc,gCAAgC,CAAC;AAC/C,cAAc,gCAAgC,CAAC;AAC/C,cAAc,+BAA+B,CAAC;AAC9C,cAAc,0BAA0B,CAAC;AACzC,cAAc,8BAA8B,CAAC;AAC7C,cAAc,6BAA6B,CAAC;AAC5C,cAAc,8BAA8B,CAAC;AAC7C,cAAc,wBAAwB,CAAC;AACvC,cAAc,mCAAmC,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "instar",
3
- "version": "1.3.726",
3
+ "version": "1.3.728",
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",
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "$schema": "./builtin-manifest.schema.json",
3
3
  "schemaVersion": 1,
4
- "generatedAt": "2026-07-03T01:15:56.453Z",
5
- "instarVersion": "1.3.726",
4
+ "generatedAt": "2026-07-03T01:23:17.069Z",
5
+ "instarVersion": "1.3.728",
6
6
  "entryCount": 202,
7
7
  "entries": {
8
8
  "hook:session-start": {
@@ -0,0 +1,215 @@
1
+ # Upgrade Guide — vNEXT
2
+
3
+ <!-- assembled-by: assemble-next-md -->
4
+ <!-- bump: minor -->
5
+
6
+ ## What Changed
7
+
8
+ The IntelligenceRouter failure-swap loop bounded EVERY swap attempt with one
9
+ global cap (`intelligence.swapAttemptTimeoutMs`, default 5000ms). Measured
10
+ provider latency (llm-pathway-bench, N=30, uncontended) shows gemini-flash's
11
+ p50 is 8,538ms — ABOVE the 5s cap — so whenever gemini was a swap target it was
12
+ SIGTERMed before it could answer on the majority of attempts: it reliably
13
+ burned a full 5s swap slot and then failed, poisoning the failover tail
14
+ (the R4 gemini-swap-timeout finding).
15
+
16
+ Per `docs/specs/per-target-swap-timeout-spec.md` (review-converged 2026-07-01,
17
+ approved), the cap now resolves PER TARGET FRAMEWORK with three new optional
18
+ `intelligence` config fields, all defaulting UNSET (dark ship — routing/timeout
19
+ behavior is byte-identical to today until an operator opts in):
20
+
21
+ - `swapAttemptTimeoutMsByFramework` — per-target caps. Resolution per swap
22
+ target: `byFramework[target]` (valid = finite number > 0) → the global
23
+ `swapAttemptTimeoutMs` → no cap. An INVALID value (0, negative, NaN,
24
+ non-number) falls through to the global — never "no cap", never a 0ms kill
25
+ (FD5: per-framework config cannot express "unbounded"; only the global's
26
+ ≤0/unset does). Recommended opt-in package:
27
+ `{ "claude-code": 8000, "pi-cli": 9000, "gemini-cli": 18000, "codex-cli": 45000 }`.
28
+ - `swapAttemptTimeoutMsMax` — clamp on any single resolved cap (invalid/unset
29
+ ⇒ 120s) so a typo'd huge value cannot pin a host spawn-cap slot unbounded.
30
+ - `swapTotalBudgetMs` — wall-clock TOTAL budget over the whole swap tail
31
+ (recommended 40000, set together with the per-target caps). When set, each
32
+ attempt's effective cap is `min(resolvedCap, budgetRemaining)` on a MONOTONIC
33
+ clock (never Date.now()), and the loop stops and falls closed once ≤250ms
34
+ remains — so the worst-case swap tail is literally ≤ the budget. Unset ⇒ no
35
+ enforcement (today's semantics).
36
+
37
+ Also: the swap timeout timer is now cleared on settle (`withSwapTimeout`
38
+ helper) so a fast success no longer leaks a pending timer per call; a stray/
39
+ misspelled `byFramework` key warns once and is ignored (falls through to the
40
+ global). The `swap-attempt-timeout:` degrade-reason format is unchanged.
41
+ `failureSwap` ORDER remains latency-load-bearing (sequential loop) — order
42
+ fastest-first; keep caps ≤ the circuit breaker's failure sensitivity.
43
+
44
+ Rollback: remove the config block → instant revert to the global cap. No
45
+ migrateConfig entry (absence is the default state, codexExecJson precedent).
46
+
47
+ `AgentWorktreeReaper.start()` scheduled ONLY a 24h `setInterval` — no initial
48
+ pass. Agent servers restart far more often than daily (auto-updates, sleep/wake
49
+ supervisor bounces), so the interval timer reset on every restart and an
50
+ **enabled + armed** reaper never ran a single pass. Measured live on 2026-07-02:
51
+ 86 worktrees / 25GB accumulated on a machine with `enabled: true, dryRun: false`
52
+ and `lastPassAt: 0` — the feature was on and structurally inert (root cause of
53
+ that day's fseventsd/reboot resource incident).
54
+
55
+ The fix:
56
+
57
+ - New `initialPassDelayMs` config (default 15 min; `<= 0` disables — the exact
58
+ legacy behavior as a no-release rollback lever) on
59
+ `monitoring.agentWorktreeReaper`.
60
+ - `start()` now schedules a ONE-TIME initial pass after that delay (unref'd
61
+ timer, cleared by `stop()`), then the unchanged 24h cadence. The delay keeps
62
+ the pass off the busy post-boot window.
63
+ - The pass runs through the same `reap()` as always: same KEEP gates (in-use /
64
+ dirty / unmerged / detached), same dry-run gating, same `maxReapsPerPass`
65
+ blast-radius cap, same per-path reclaim-failure breaker. Nothing about WHAT
66
+ may be deleted changed — only WHEN passes run.
67
+ - `GET /worktrees/agent-reaper` snapshot additively reports
68
+ `initialPassPending`.
69
+ - CLAUDE.md template + PostUpdateMigrator addendum so deployed agents learn the
70
+ new behavior (idempotent, content-sniffed on `initialPassDelayMs`).
71
+
72
+ Fixes the roadmap-0.3 blocker found in the Slack permission-gate false-positive
73
+ review (`docs/audits/slack-permission-fp-review-2026-07.md`, row 29): after the
74
+ 2026-07-01 silent-loss registry rebuild, the Slack live-test scenario cast could no
75
+ longer resolve, so every Slack sender — including the workspace owner — resolved as an
76
+ unregistered guest. Root cause: the gate derives roles from the production user registry
77
+ (`users.json`), the rebuild removed the five Slack test-cast principals, and the
78
+ fixture-identity guard (correctly) refuses to let test identities back into the
79
+ production registry.
80
+
81
+ The fix is a SEPARATE, test-workspace-scoped principal source rather than a registry
82
+ edit:
83
+
84
+ - **New `TestWorkspacePrincipalSource` + `ChainedUserLookup`** (`src/permissions/`).
85
+ The gate's `SlackPrincipalResolver` now reads production-registry-FIRST, then the
86
+ cast as a fallback. A production-registered user can never be shadowed or
87
+ role-escalated by a cast entry.
88
+ - **Config-carried cast** — `permissionGate.testCast` in the Slack messaging config
89
+ (`{ testWorkspace, workspaceId, principals[] }`). It lives beside the workspace
90
+ tokens, so a `users.json` rebuild can never silently drop it again.
91
+ - **Code-enforced workspace scoping** — the cast resolves ONLY while the adapter's
92
+ VERIFIED connected team id (captured from Slack's own `auth.test` at start, exposed
93
+ via `SlackAdapter.getConnectedTeamId()`) EXACTLY equals `workspaceId`. Any other
94
+ workspace → the source is structurally invisible (production behavior byte-identical
95
+ to no cast). An unlisted uid in the test workspace → today's unregistered-guest default.
96
+ - **Partition invariant** — every cast `slackUserId` must match the SAME fixture-marker
97
+ matcher (`users/testIdentityMarkers.ts`) the production guard uses to refuse fixtures;
98
+ a non-fixture id is refused at load. One identity, two disjoint homes.
99
+ - **Fail-closed self-declaration** — the block must set `testWorkspace: true`; without
100
+ it the whole cast is ignored (zero principals, one loud log line, no production
101
+ effect). A cast can never be activated by accident.
102
+ - **Authority scope (KYP)** — the source feeds permission-gate role resolution ONLY. It
103
+ takes no state dir and has no write surface, so it cannot create user-registry
104
+ entries, cannot feed operator binding, and cannot affect message authorization /
105
+ sender validation.
106
+
107
+ The runbook (`docs/specs/SLACK-ORG-TEST-WORKSPACE-RUNBOOK.md`) is patched: the old
108
+ "seed the cast in users.json" step is replaced with the `testCast` config block plus a
109
+ re-provision checklist so a registry rebuild can't silently lose principal resolution
110
+ again. Dark by default — nothing activates without an explicit `testCast` block.
111
+
112
+ ## What to Tell Your User
113
+
114
+ <!-- audience: agent-only, maturity: preview -->
115
+ - **Backup AI routes now get fair deadlines (off until you opt in)**: when my
116
+ main AI route fails mid-decision, I try backups — but every backup shared one
117
+ five-second deadline, and one of my backups (gemini) usually needs about nine
118
+ seconds, so it was being cut off before it could ever answer. I can now give
119
+ each backup its own deadline matched to how fast it really is, plus an
120
+ overall time limit so a chain of backups can never keep you waiting too long.
121
+ Nothing changes until we turn it on — if you'd like, I can enable the
122
+ recommended settings for you and undo it just as easily.
123
+
124
+ <!-- audience: user, maturity: stable -->
125
+
126
+ If you switched on the stale-worktree auto-cleaner, it now genuinely runs: one
127
+ cleaning pass about 15 minutes after each server start, then daily as designed.
128
+ Previously a quirk meant frequent server restarts kept pushing its first run
129
+ into the future forever, so leftover work folders could quietly pile up into
130
+ tens of gigabytes even with the cleaner enabled. Its safety rules are untouched:
131
+ it still only removes folders whose work is fully merged and saved, still does
132
+ dry-run first, and it stays entirely off unless you enabled it.
133
+
134
+ - **Slack permission demo is more robust**: the test harness that proves the Slack
135
+ "who's allowed to do what" feature now keeps its demo cast in a safe place that a
136
+ routine cleanup of your real people-list can't wipe out. This is behind-the-scenes
137
+ test infrastructure — nothing changes in how I work for you day to day, and the
138
+ pretend demo roles are locked to the throwaway test workspace so they can never leak
139
+ into a real one.
140
+
141
+ ## Summary of New Capabilities
142
+
143
+ | Capability | How to Use |
144
+ |-----------|-----------|
145
+ | Per-target swap-attempt caps | `intelligence.swapAttemptTimeoutMsByFramework` in `.instar/config.json` (unset = today's global cap) |
146
+ | Per-attempt clamp | `intelligence.swapAttemptTimeoutMsMax` (invalid/unset ⇒ 120s) |
147
+ | Total swap-tail budget | `intelligence.swapTotalBudgetMs` (unset = no budget enforcement) |
148
+
149
+ - The stale-worktree reaper actually fires on real deployments: a one-time
150
+ cleaning pass ~15 minutes after boot (tunable/disableable via
151
+ `monitoring.agentWorktreeReaper.initialPassDelayMs`), unchanged daily cadence
152
+ thereafter, and `initialPassPending` visibility on the existing report route.
153
+
154
+ | Capability | How to Use |
155
+ |-----------|-----------|
156
+ | Test-workspace-scoped Slack principal cast | Add a `testCast` block (with `testWorkspace: true`, `workspaceId`, `principals`) under the Slack `permissionGate` config — dark unless configured |
157
+
158
+ ## Evidence
159
+
160
+ - Live measurement (the bug): llm-pathway-bench R4 characterization, N=30
161
+ uncontended — gemini-flash p50 = 8,538ms, p95 = 15,726ms vs the 5,000ms flat
162
+ cap: every median-speed gemini swap attempt was killed at the cap before it
163
+ could answer (observed as `swap-attempt-timeout: gemini-cli` degrade reasons;
164
+ the attempt burned the full 5s slot and produced nothing). Other measured
165
+ targets for scale: claude ~3s p50 / ~6s p95, pi ~4.6s / ~7s, codex ~18s / ~43s
166
+ — no single cap fits all four.
167
+ - After (mechanism, exercised end-to-end in-process): with the recommended 18s
168
+ gemini cap, an 8.5s-latency gemini target is SERVED on swap
169
+ (`tests/integration/per-target-swap-timeout.test.ts`, wired with the server's
170
+ exact threading expressions); with no per-framework config the same target is
171
+ abandoned at exactly 5s and the call fails closed — byte-identical to before
172
+ (regression tests pin the cap value the provider subprocess receives).
173
+ - The field fix requires the operator opt-in (dark ship); the delivery
174
+ follow-through is tracked as commitment CMT-1889 (surface the recommended
175
+ values as a one-tap go/no-go).
176
+
177
+ - `tests/unit/agent-worktree-reaper.test.ts`: 47/47 green — 7 new tests (initial
178
+ pass fires exactly once at the configured delay and before the interval;
179
+ respects dry-run; disabled reaper sets no timers; `<= 0` restores
180
+ interval-only; `stop()` cancels the pending pass; 24h cadence unchanged after
181
+ the initial pass; snapshot reports `initialPassPending` honestly).
182
+ - Live incident data (2026-07-02, topic 30379): reaper enabled+armed with 25
183
+ reap-eligible worktrees and `lastPassAt: 0`; server uptime history shows no
184
+ 24h-continuous window for weeks.
185
+
186
+ Root cause is a live datapoint captured in `docs/audits/slack-permission-fp-review-2026-07.md`:
187
+ row 29 (2026-07-02 18:15:19, the only organic decision row) shows the workspace OWNER
188
+ seat's Slack UID resolving as `role: guest, registered: false` — the same UID that
189
+ resolved as `role: owner, registered: true` in every June scenario row, because the
190
+ 2026-07-01 rebuild removed the cast from `users.json`.
191
+
192
+ The fix is verified by `tests/unit/slack-test-workspace-principal-source.test.ts` (22
193
+ tests, all passing), which exercises BOTH sides of every boundary:
194
+
195
+ - matching workspace + listed uid → registered role; matching workspace + unlisted uid
196
+ → guest.
197
+ - non-matching workspace + listed uid → source invisible, and the resolved principal is
198
+ asserted EQUAL to the no-cast baseline (the scoping proof that roles can't leak into a
199
+ production workspace).
200
+ - missing `testWorkspace: true` → source disabled, zero seats, loud log line, and
201
+ byte-identical to no cast.
202
+ - non-fixture uid refused at load; cast cap enforced; production-registry-first
203
+ precedence; a throwing source skipped.
204
+ - authority-scope assertions: the source exposes only the read contract (no
205
+ write/registry/operator methods) and takes no state dir.
206
+
207
+ `tests/integration/slack-testcast-principal-pipeline.test.ts` (5 tests, all passing)
208
+ additionally drives the EXACT server.ts composition through the real inbound chokepoint
209
+ (`SlackAdapter._handleMessage`) and asserts the durable decision-ledger rows: the owner
210
+ seat resolves `owner, registered:true` through the live path (the row-29 regression),
211
+ the same seat is an unregistered guest when connected to any other workspace or before
212
+ `auth.test` verifies the connection, an unlisted uid stays a guest, and a production
213
+ record beats the cast for the same uid.
214
+
215
+ `npx tsc --noEmit` exits 0.
@@ -0,0 +1,120 @@
1
+ # Side-Effects Review — Per-Target Failure-Swap Timeout (fixes gemini-swap-timeout)
2
+
3
+ **Version / slug:** `per-target-swap-timeout`
4
+ **Date:** `2026-07-02`
5
+ **Author:** `echo (instar-dev agent)`
6
+ **Second-pass reviewer:** `required — the change modifies timeout resolution on the failure-swap path that serves GATING calls (the tone gate rides it)`
7
+
8
+ ## Summary of the change
9
+
10
+ The IntelligenceRouter failure-swap loop used ONE global per-attempt timeout (`intelligence.swapAttemptTimeoutMs`, default 5s) for every swap target. Measured provider latency (llm-pathway-bench, N=30) shows gemini's p50 is ~8.5s — the 5s cap sits BELOW gemini's median, so gemini as a swap target was reliably killed before it could answer, wasting a full swap slot ("poisoning the failover tail"). This change makes the cap resolvable PER TARGET FRAMEWORK (`intelligence.swapAttemptTimeoutMsByFramework`), adds a per-attempt clamp (`swapAttemptTimeoutMsMax`, invalid/unset ⇒ 120s), and adds an optional wall-clock TOTAL swap budget (`swapTotalBudgetMs`) that clamps each in-flight attempt to the remaining budget on a MONOTONIC clock, bounding the whole fail-closed tail to literally ≤ budget. All three fields default UNSET — routing/timeout behavior is byte-identical to today until an operator opts in (dark ship). Files touched: `src/core/IntelligenceRouter.ts` (resolveSwapCap, withSwapTimeout, per-target loop resolution, unknown-key hygiene), `src/core/types.ts` (three optional config fields), `src/commands/server.ts` (threads the three fields), `tests/unit/per-target-swap-timeout.test.ts`, `tests/integration/per-target-swap-timeout.test.ts`, spec + ELI16 + convergence report under `docs/specs/`. Spec: `docs/specs/per-target-swap-timeout-spec.md` (review-converged 2026-07-01, approved).
11
+
12
+ ## Decision-point inventory
13
+
14
+ - `IntelligenceRouter failure-swap loop` (`src/core/IntelligenceRouter.ts` `evaluate()`) — **modify** — a routing/degrade path (which fallback provider gets how long), NOT a block/allow gate. No new gate is introduced or removed; the swap still fires only for `attribution.gating`/`deferrable` calls with configured `failureSwap` targets (scope unchanged). What changed is only HOW LONG each target is given (per-target vs one flat cap) and a new optional total-budget stop that falls CLOSED (the safe direction for gating calls).
15
+ - `Gating ladder deadline` (`gatingLadderBudgetMs`) — **pass-through** — still checked first at each loop iteration, unchanged.
16
+
17
+ **Phase 1 principle-check answer (recorded):** the change touches a decision point only in the routing/degrade sense — it does not gate information flow by content, block actions by meaning, or filter messages. Timeout resolution is deterministic mechanics (a wall-clock bound on a subprocess attempt), in the signal-vs-authority carve-out class of structural/transport mechanics, not a judgment decision. No brittle check gains blocking authority.
18
+
19
+ ---
20
+
21
+ ## 1. Over-block
22
+
23
+ **What legitimate inputs does this change reject that it shouldn't?**
24
+
25
+ At the default (all three fields unset): none — resolution degenerates to exactly today's single global cap; the regression tests assert the 5s behavior byte-for-byte (same cap value passed through as `timeoutMs`, same fail-closed rethrow).
26
+
27
+ When an operator opts in: two deliberate "rejections" exist and both are the spec's explicit fail-SAFE choices, not accidents. (a) A `swapTotalBudgetMs` set below the 250ms floor disables swapping on the first attempt — an absurd misconfig fails CLOSED (no swap) rather than open (unbounded swap). (b) When the remaining budget is ≤ 250ms, a viable fast target later in the tail is NOT admitted — the loop falls closed. Both are unit-tested. A per-framework value that is invalid (0, negative, NaN, non-number) can never over-block: it falls through to the global, never to "no cap" and never to a 0ms instant kill (FD5, tested for every invalid class).
28
+
29
+ ---
30
+
31
+ ## 2. Under-block
32
+
33
+ **What failure modes does this still miss?**
34
+
35
+ - **The dark default does not fix the field bug.** Gemini keeps dying at 5s until an operator sets the recommended values — this is the spec's own "ships-inert" finding. Mitigation is a DELIVERY obligation (FD8): surface the recommended values as a one-tap go/no-go and track the opt-in as a commitment (registered: CMT — see Conclusion). Tracked, not silently dropped.
36
+ - **A provider that ignores `timeoutMs` can overrun its cap as an orphaned subprocess.** The `Promise.race` timer still resolves the swap decision at the cap (the loop advances on time), but the subprocess kill is the provider's own responsibility — unchanged by this change and already true of the existing global cap (spec: provider timeout contract).
37
+ - **Operator misconfig within valid ranges:** a per-target cap set above the circuit breaker's failure sensitivity could keep a chronically-slow target alive without tripping the breaker; a slow target ordered FIRST in `failureSwap` delays faster ones up to its cap (order is latency-load-bearing). Both are documented operator constraints in the spec and the types.ts JSDoc — static caps remain blind to live latency drift by design (auto-tuning is explicitly out of scope, FD3 <!-- tracked: CMT-1889 -->).
38
+
39
+ ---
40
+
41
+ ## 3. Level-of-abstraction fit
42
+
43
+ **Is this at the right layer?**
44
+
45
+ Yes. The failure-swap loop in `IntelligenceRouter` is the ONLY place that owns swap-attempt timing, and the change modifies exactly that resolution point (cap resolution moved inside the per-target loop). It does not re-implement any primitive: it reuses the existing `Promise.race` pattern (now wrapped in `withSwapTimeout`, which only adds timer-clear-on-settle), the existing `timeoutMs` provider pass-through contract, and the existing `onDegrade` observability channel (`swap-attempt-timeout:` reason format preserved byte-for-byte so DegradationReporter/metrics consumers are unaffected). The config lives in the existing `intelligence` block alongside the global cap it extends. No higher layer (LlmQueue, circuit breaker, gating ladder) owns per-attempt timing; no lower layer duplicates it.
46
+
47
+ ---
48
+
49
+ ## 4. Signal vs authority compliance
50
+
51
+ **Required reference:** [docs/signal-vs-authority.md](../../docs/signal-vs-authority.md)
52
+
53
+ - [x] No — this change has no block/allow surface.
54
+
55
+ The change has no judgment-based block/allow surface: it is timeout mechanics on a routing/degrade path (the signal-vs-authority carve-out class: structural bounds, not content judgment). The one nuance — acknowledged in the spec's foundation note — is that a static cap holds kill-authority over whether a provider ATTEMPT survives, blind to real latency; that mis-fit is the root shape of the original incident. This change REDUCES the mis-fit (caps sized per target to measured p50/p95) without adding any new authority, and the residual (static caps can drift from live latency) is explicitly flagged as a recurrence vector for the auto-tuning follow-up (FD3, out of scope <!-- tracked: CMT-1889 -->). Timed-out attempts still count toward the per-framework circuit breaker (unchanged), so the breaker — the existing authority over provider health — keeps its signal supply.
56
+
57
+ ---
58
+
59
+ ## 5. Interactions
60
+
61
+ - **Shadowing:** the gating-ladder deadline (`gatingLadderBudgetMs`) is checked FIRST at each loop iteration, before the new budget check — an active ladder still wins (unchanged precedence). The new total budget only ADDS a stop; it cannot extend the ladder's deadline. Verified in code order.
62
+ - **Double-fire:** the per-attempt timer and the provider's own `timeoutMs` SIGTERM fire at the SAME bound (the cap is passed through as `timeoutMs`, exactly as before) — by design, not a race: the race timer decides the swap, the subprocess bound kills the process. The budget-clamped cap keeps them identical (the clamped value is what flows through).
63
+ - **Races:** `withSwapTimeout` preserves the shipped crash-safe `Promise.race` semantics (per-input settlement handlers; late reject swallowed, late resolve ignored — both re-covered by the existing N1 tests, which still pass unmodified). The only addition is `clearTimeout` in a `finally`, which cannot race the rejection (a fired timer's clear is a no-op).
64
+ - **Feedback loops:** timed-out swap attempts still count toward the target's circuit breaker (unchanged), so a chronically-timing-out target still trips its breaker and gets skipped fast on later calls — no new loop. The spec documents the operator constraint that caps SHOULD stay ≤ the breaker's failure sensitivity.
65
+ - **Config layering:** the three new fields ride `IntelligenceRouterOptions`, NOT `ComponentFrameworksConfig` — so the §4.6 computed-default/live-override layering in `resolveConfig` is untouched (verified: the layered-resolveConfig tests pass unmodified).
66
+
67
+ ---
68
+
69
+ ## 6. External surfaces
70
+
71
+ - **Other agents / install base:** none until opt-in — all three fields default unset and there is no `migrateConfig` entry (absence is the default state, codexExecJson precedent), so no deployed agent's config changes on update.
72
+ - **External systems:** none. No new network calls, no new subprocess kinds — only how long an existing swap subprocess is given.
73
+ - **Persistent state:** none. Per-call resolution from live opts; no ledger, no file, no store.
74
+ - **Timing/runtime conditions:** the budget uses a MONOTONIC clock (`performance.now()`), never `Date.now()`, so an NTP step/DST jump cannot warp it (round-3 external finding; the gating-ladder's existing `Date.now()` deadline is unchanged — out of this change's scope). Observability: a cap firing emits the same `swap-attempt-timeout:` degrade reason as today (format preserved), visible in DegradationReporter and `/metrics/features`.
75
+ - **Operator surface (Mobile-Complete Operator Actions):** no operator-facing action is added — the knobs are config fields. The FD8 delivery obligation (one-tap go/no-go for the recommended values) is where the operator surface will live; it is a tracked commitment (see Conclusion), deliberately outside this spec's code change per the approved spec.
76
+
77
+ ---
78
+
79
+ ## 6b. Operator-surface quality (Operator-Surface Quality standard)
80
+
81
+ No operator surface — not applicable (no dashboard/approval/grant/revoke/secret-drop file is staged; the change is config + router internals + tests).
82
+
83
+ ---
84
+
85
+ ## 7. Multi-machine posture (Cross-Machine Coherence)
86
+
87
+ **Machine-local BY DESIGN** (spec FD4). The swap cap is a per-call routing parameter read from each machine's local config at call time. It holds no durable cross-machine state (nothing to strand on topic transfer), emits no user-facing notice (no one-voice gating needed), and generates no URL (nothing to survive machine boundaries). Each machine's providers have that machine's latency characteristics, so per-machine values are the correct scope; an operator who wants uniform values sets them per machine's config. No replication path needed.
88
+
89
+ ---
90
+
91
+ ## 8. Rollback cost
92
+
93
+ Pure code change with a dark default — two independent rollback levers, both cheap:
94
+
95
+ - **Operator-level (instant, no release):** remove the `swapAttemptTimeoutMsByFramework` / `swapAttemptTimeoutMsMax` / `swapTotalBudgetMs` block from `.instar/config.json` → instant revert to the global-cap behavior (spec: "Rollback: remove the config block → instant revert").
96
+ - **Code-level:** revert the commit, ship as next patch. No persistent state, no data migration, no agent state repair, no user-visible regression during the rollback window (the default path is byte-identical for routing/timeout semantics; the only internal difference is the timer-clear-on-settle hygiene, which is behavior-neutral for routing).
97
+
98
+ ---
99
+
100
+ ## Conclusion
101
+
102
+ The review confirms the change is a localized, additive, dark-shipped modification of the failure-swap timeout resolution with no new block/allow authority, no persistent state, no external surface until opt-in, and a two-lever rollback. The design decisions that the review leaned on hardest — FD5 (invalid per-framework values fall through to the global, closing the accidental-uncap footgun), FD6 (the total budget clamps each IN-FLIGHT attempt so the tail is literally ≤ budget, and falls CLOSED at the floor), FD7 (maxCap clamp + validated knobs + timer hygiene) — are all unit-tested on both sides of every boundary (valid vs each invalid class). Two items are flagged, both already tracked: (1) the FD8 delivery obligation (the dark ship fixes nothing until the operator opts in — the recommended-values go/no-go + commitment is registered as CMT-1889 and is delivery work outside this spec's code change per the approved spec); (2) static caps remain blind to latency drift — the auto-tuning follow-up <!-- tracked: CMT-1889 --> (FD3) rides the same tracked commitment's context. Clear to ship pending second-pass concurrence (required: the change sits on the path that serves gating calls).
103
+
104
+ ---
105
+
106
+ ## Second-pass review (if required)
107
+
108
+ **Reviewer:** claude (independent second-pass subagent)
109
+
110
+ **Independent read of the artifact: concur** — Verified against the working-tree diff (`src/core/IntelligenceRouter.ts`, `src/core/types.ts`, `src/commands/server.ts`) and all 56 tests (25 + 3 new; the 11 + 17 pre-existing pass unmodified): the code matches the spec's resolveCap contract exactly (isFinite && >0 validation, byFramework→global→undefined fall-through, maxCap clamp with invalid→120s default, in-flight `min(cap, remaining)` budget clamp on `performance.now()`, 250ms-floor fall-closed, budget-unset ⇒ no enforcement), no brittle check gains blocking authority (the unknown-key check is warn-once signal-only; the budget stop is a deterministic resource bound in the signal-vs-authority carve-out class), and the ladder-deadline-before-budget precedence, breaker counting, Promise.race crash-safety, finally-clause timer clear, and `swap-attempt-timeout:` prefix format are all true in the code as claimed — with one non-blocking precision note: the "byte-identical at default for ANY global cap" claim is overbroad for the exotic-but-JSON-reachable case of a pre-existing `swapAttemptTimeoutMs > 120000`, which the (spec-mandated, FD7-intended) default maxCap now clamps to 120s even with all three new fields unset — a fail-safe-direction change worth knowing in forensics, not a ship blocker.
111
+
112
+ ---
113
+
114
+ ## Evidence pointers
115
+
116
+ - Unit: `tests/unit/per-target-swap-timeout.test.ts` (25 tests — resolveSwapCap contract incl. every invalid class, the gemini 8.5s fix + 5s regression, FD6 budget in-flight clamp/floor/unset/invalid/uncapped-bound, FD7 timer clear, wiring incl. server threading scan, unknown-key warn-once).
117
+ - Integration: `tests/integration/per-target-swap-timeout.test.ts` (3 tests — server-exact wiring: the FD8 recommended-values package serves gemini at 8.5s under an 18s cap; no-config and absent-intelligence-block regressions pin the 5s global byte-for-byte).
118
+ - Pre-existing behavior locked: `tests/unit/provider-fallback-swap-timeout.test.ts` (11 tests) and `tests/unit/intelligence-router.test.ts` (17 tests) pass UNMODIFIED against the new code.
119
+ - Tier-3 carve-out: no HTTP route is added (internal routing-timeout knob), so per the Testing-Integrity Standard's no-route carve-out there is no "feature-alive/200" e2e; Tiers 1 + 2 apply (stated in the spec's Testing section).
120
+ - Measured basis: llm-pathway-bench N=30 (gemini p50 8,538ms / p95 15,726ms; claude ~3s/~6s; pi ~4.6s/~7s; codex ~18s/~43s).