instar 1.2.83 → 1.3.1
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.
- package/dist/commands/init.js +14 -3
- package/dist/commands/init.js.map +1 -1
- package/dist/commands/server.d.ts.map +1 -1
- package/dist/commands/server.js +86 -4
- package/dist/commands/server.js.map +1 -1
- package/dist/core/PostUpdateMigrator.d.ts +2 -1
- package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
- package/dist/core/PostUpdateMigrator.js +279 -12
- package/dist/core/PostUpdateMigrator.js.map +1 -1
- package/dist/core/installCodexHooks.d.ts.map +1 -1
- package/dist/core/installCodexHooks.js +3 -2
- package/dist/core/installCodexHooks.js.map +1 -1
- package/dist/scaffold/templates.d.ts.map +1 -1
- package/dist/scaffold/templates.js +1 -8
- package/dist/scaffold/templates.js.map +1 -1
- package/dist/server/routes.d.ts.map +1 -1
- package/dist/server/routes.js +28 -67
- package/dist/server/routes.js.map +1 -1
- package/dist/server/stopGate.d.ts +8 -2
- package/dist/server/stopGate.d.ts.map +1 -1
- package/dist/server/stopGate.js +42 -2
- package/dist/server/stopGate.js.map +1 -1
- package/dist/threadline/CollaborationSurfacer.d.ts.map +1 -1
- package/dist/threadline/CollaborationSurfacer.js +7 -3
- package/dist/threadline/CollaborationSurfacer.js.map +1 -1
- package/dist/threadline/hubCommands.d.ts +67 -0
- package/dist/threadline/hubCommands.d.ts.map +1 -0
- package/dist/threadline/hubCommands.js +126 -0
- package/dist/threadline/hubCommands.js.map +1 -0
- package/package.json +1 -1
- package/playbook-scripts/build-state.py +39 -1
- package/scripts/analyze-release.js +16 -8
- package/scripts/generate-builtin-manifest.cjs +2 -1
- package/src/data/builtin-manifest.json +74 -65
- package/src/scaffold/templates.ts +1 -8
- package/src/templates/hooks/build-stop-hook.sh +62 -0
- package/src/templates/hooks/settings-template.json +10 -0
- package/upgrades/1.3.0.md +27 -0
- package/upgrades/1.3.1.md +27 -0
- package/upgrades/side-effects/build-stop-hook-session-scoping.md +133 -0
- package/upgrades/side-effects/fresh-session-stop-gate-shadow-wiring.md +35 -0
- package/upgrades/side-effects/threadline-open-this-deterministic.md +45 -0
|
@@ -135,6 +135,16 @@
|
|
|
135
135
|
}
|
|
136
136
|
],
|
|
137
137
|
"Stop": [
|
|
138
|
+
{
|
|
139
|
+
"matcher": "",
|
|
140
|
+
"hooks": [
|
|
141
|
+
{
|
|
142
|
+
"type": "command",
|
|
143
|
+
"command": "node .instar/hooks/instar/stop-gate-router.js",
|
|
144
|
+
"timeout": 5000
|
|
145
|
+
}
|
|
146
|
+
]
|
|
147
|
+
},
|
|
138
148
|
{
|
|
139
149
|
"matcher": "",
|
|
140
150
|
"hooks": [
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Upgrade Guide — NEXT
|
|
2
|
+
|
|
3
|
+
<!-- bump: patch -->
|
|
4
|
+
<!-- Valid values: patch, minor, major -->
|
|
5
|
+
<!-- patch = bug fixes, refactors, test additions, doc updates -->
|
|
6
|
+
<!-- minor = new features, new APIs, new capabilities (backwards-compatible) -->
|
|
7
|
+
<!-- major = breaking changes to existing APIs or behavior -->
|
|
8
|
+
|
|
9
|
+
## What Changed
|
|
10
|
+
|
|
11
|
+
**The `/build` stop-hook is now session-scoped — it only nags the session that actually owns the build.** Before, the hook that keeps a build from quitting half-done had no idea *which* of your concurrent sessions started the build, so it fired its "keep working" block into every session — trapping unrelated ones and, worse, spending the owning build's reinforcement budget on each misfire (when that budget hit its cap, the hook stopped protecting the real builder too).
|
|
12
|
+
|
|
13
|
+
Now `build-state.py` stamps the owning session (its tmux session name, and optionally the Claude session UUID) at build start, and the hook blocks **only** the proven owner. Every other session approve-exits without touching the owner's budget. A build with no owner stamp (legacy state) gets a conservative no-adopt: the hook goes quiet rather than guessing — it never traps a session and never claims ownership.
|
|
14
|
+
|
|
15
|
+
The hook ships via the always-overwrite path (the inline `getBuildStopHook()` twin in `PostUpdateMigrator`, kept byte-identical to `src/templates/hooks/build-stop-hook.sh` and asserted by a drift test), so every agent gets it on update.
|
|
16
|
+
|
|
17
|
+
## What to Tell Your User
|
|
18
|
+
|
|
19
|
+
- **No action needed; this just stops cross-talk between your sessions.** If you run more than one session at once, a build in one of them will no longer pester the others or drain its own "keep going" budget. You won't notice anything unless you run concurrent sessions, in which case it gets quieter.
|
|
20
|
+
|
|
21
|
+
## Summary of New Capabilities
|
|
22
|
+
|
|
23
|
+
| Capability | How to Use |
|
|
24
|
+
|-----------|-----------|
|
|
25
|
+
| Session-scoped build stop-hook | Automatic. `build-state.py init` stamps `owner.{tmux,session,stampedAt}`; the hook blocks only the owner. |
|
|
26
|
+
| Owner-stamp flags on `build-state.py init` | `--owner-session "$CLAUDE_CODE_SESSION_ID"` (precision; SKILL wiring is a fast-follow), `--owner-tmux` (override seam). Tmux name is auto-resolved by default. |
|
|
27
|
+
| Conservative no-adopt for un-stamped builds | Automatic. No owner stamp → hook approves without claiming ownership (never traps, never drains). |
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# Unreleased
|
|
2
|
+
|
|
3
|
+
## What Changed
|
|
4
|
+
|
|
5
|
+
"Open this" in the Threadline hub is now a deterministic, structural action instead of something the agent has to interpret (CMT-529). Previously the agent could — and did — ramble a reply instead of creating a topic.
|
|
6
|
+
|
|
7
|
+
- **Structural intercept.** When a message in the Threadline hub topic is exactly "open this" / "tie this to <topic>", the system catches it at the one inbound seam BOTH message paths converge on (`telegram.onTopicMessage`) and binds the conversation to a topic before any agent interprets it. Ordinary hub chat falls through to the agent unchanged. FAIL-OPEN.
|
|
8
|
+
- **Bare "open this" auto-picks the most-recent** unbound conversation (the one you're looking at) instead of asking "which one?". The legacy-state ordering bug that made "most recent" unreliable is fixed (migrated hub entries now preserve their original order instead of all sharing one timestamp).
|
|
9
|
+
- **Readable topic names.** A newly-opened topic is named from what the conversation is about (capped, charset-scrubbed, and a safe fallback if the content looks like a secret) instead of a cryptic `peer · threadId`.
|
|
10
|
+
- The `POST /threadline/hub/bind` API is unchanged (it still returns 409 on ambiguity for scripted callers); the route and the intercept now share one `bindHubConversation` helper.
|
|
11
|
+
|
|
12
|
+
## What to Tell Your User
|
|
13
|
+
|
|
14
|
+
When you're looking at the Threadline topic and say "open this", I now reliably spin up a topic for that conversation — no more rambling, no judgment call on my end. It opens the one you're looking at and names it after what it's about. You can also say "tie this to <one of my topics>" to file it into an existing topic.
|
|
15
|
+
|
|
16
|
+
## Summary of New Capabilities
|
|
17
|
+
|
|
18
|
+
- Deterministic "open this" / "tie this to X" hub commands (intercepted structurally, both inbound paths).
|
|
19
|
+
- Auto-pick most-recent-unbound on bare "open this"; readable, scrubbed topic names.
|
|
20
|
+
- Shared `bindHubConversation` helper behind both the intercept and the API route.
|
|
21
|
+
- Legacy hub-state ordering fix so "most recent" is trustworthy.
|
|
22
|
+
|
|
23
|
+
## Evidence
|
|
24
|
+
|
|
25
|
+
- Verified against v1.3.0; design converged by two reviewers — the decisive catch was the dual-path trap (intercept must sit at `onTopicMessage`, the convergence both lifeline-forward and server-polling reach, not just `/internal/telegram-forward` where it'd be dead code for polling agents). Auto-pick, no-double-post, and topic-name privacy also folded in.
|
|
26
|
+
- 3-tier tests: unit for `parseHubCommand` (both sides of the boundary), `bindHubConversation` (open/tie/404/409/autoPick/readable-name), and the legacy-ordering fix; integration parity for `POST /threadline/hub/bind`; full threadline suite green (1569 unit/integration + 329 e2e, zero regressions). Typecheck + build clean.
|
|
27
|
+
- Independent second-pass review of the diff; live test-as-self on Codey (type "open this" into the hub → a readable-named topic is created + bound, no ramble).
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
# Side-Effects Review — Build Stop-Hook Session-Scoping
|
|
2
|
+
|
|
3
|
+
**Slug:** `build-stop-hook-session-scoping`
|
|
4
|
+
**Date:** `2026-05-26`
|
|
5
|
+
**Author:** Echo
|
|
6
|
+
**Spec:** `docs/specs/BUILD-STOP-HOOK-SESSION-SCOPING-SPEC.md` (converged round 1, approved by Justin via Telegram topic 13352)
|
|
7
|
+
**Second-pass reviewer:** independent general-purpose review agent (3 findings, all incorporated — see spec §"Review Findings Incorporated")
|
|
8
|
+
|
|
9
|
+
## Summary of the change
|
|
10
|
+
|
|
11
|
+
The `/build` Stop hook had no notion of which session owns a build. With one
|
|
12
|
+
shared `build-state.json` and one hook in a checkout, a build started by session
|
|
13
|
+
A fired its "keep working" block into every concurrent session of the same agent
|
|
14
|
+
— trapping unrelated sessions and, on every misfire, incrementing the shared
|
|
15
|
+
`reinforcementsUsed` counter, which drains the owning build's protection budget
|
|
16
|
+
to zero.
|
|
17
|
+
|
|
18
|
+
This change stamps the owning session at `/build` start (`build-state.py init`
|
|
19
|
+
writes `owner.{tmux,session,stampedAt}`) and teaches the hook to block **only**
|
|
20
|
+
the proven owner. Any other session approve-exits **without** incrementing the
|
|
21
|
+
counter. A build with no owner stamp gets a conservative no-adopt (approve,
|
|
22
|
+
never claim ownership) — it never traps a session and never inverts ownership.
|
|
23
|
+
|
|
24
|
+
## Files changed (in gate scope = behavior)
|
|
25
|
+
|
|
26
|
+
- `src/core/PostUpdateMigrator.ts` — the inline `getBuildStopHook()` (the
|
|
27
|
+
shipping artifact; written to `.instar/hooks/instar/build-stop-hook.sh` on every
|
|
28
|
+
migration via always-overwrite, and by `init.ts` via `getHookContent`). Added
|
|
29
|
+
the ownership block between the terminal-phase early-exit and the counter
|
|
30
|
+
mutation.
|
|
31
|
+
- `src/templates/hooks/build-stop-hook.sh` — the canonical reference template +
|
|
32
|
+
builtin-manifest fingerprint. Kept byte-identical to the inline twin (asserted
|
|
33
|
+
by a new drift test).
|
|
34
|
+
|
|
35
|
+
Out of gate scope but part of the change:
|
|
36
|
+
- `playbook-scripts/build-state.py` — `cmd_init` stamps `owner`; new
|
|
37
|
+
`--owner-session` / `--owner-tmux` flags; `resolve_owner_tmux()` helper.
|
|
38
|
+
- `tests/unit/build-stop-hook-session-scoping.test.ts` (new, 12 tests),
|
|
39
|
+
`tests/unit/PostUpdateMigrator-buildStopHook.test.ts` (+1 drift test).
|
|
40
|
+
- `docs/specs/*`, `upgrades/NEXT.md` (docs / release note).
|
|
41
|
+
|
|
42
|
+
## Decision-point inventory
|
|
43
|
+
|
|
44
|
+
- **Added**: hook ownership gate — between terminal-phase exit and counter
|
|
45
|
+
mutation. Decides block (owner) vs approve-no-increment (non-owner / unknown /
|
|
46
|
+
un-stamped). This is the new decision boundary.
|
|
47
|
+
- **Added**: `build-state.py` owner stamp at init (records identity; no runtime
|
|
48
|
+
decision, pure data).
|
|
49
|
+
- **Unchanged**: no-state-file exit, terminal-phase exit, and the
|
|
50
|
+
counter/reinforcement block logic itself (the owner path falls through to the
|
|
51
|
+
exact pre-existing code).
|
|
52
|
+
|
|
53
|
+
## Over-block / under-block analysis
|
|
54
|
+
|
|
55
|
+
- **Over-block risk (trapping a non-owner):** eliminated. A non-owner returns
|
|
56
|
+
`approve` before reaching the counter. The only block path requires a positive
|
|
57
|
+
owner match (tmux or session). Tested: non-owner tmux, non-owner session,
|
|
58
|
+
identity-unknown, and legacy/un-stamped all return approve.
|
|
59
|
+
- **Under-block risk (owner not protected):** bounded and acceptable. The owner
|
|
60
|
+
is protected whenever `owner.tmux` matches the live tmux (the load-bearing
|
|
61
|
+
path, proven live) or `owner.session` matches stdin `session_id`. The only
|
|
62
|
+
under-protection case is an **un-stamped** build (legacy state, or an
|
|
63
|
+
environment where stamping didn't run) — by deliberate design the hook goes
|
|
64
|
+
quiet there rather than guess. Forfeiting protection for a stale build is the
|
|
65
|
+
correct trade vs. trapping the wrong session (spec §"Why conservative-no-adopt").
|
|
66
|
+
- **Bootstrap inversion (the rejected alternative):** an earlier draft let the
|
|
67
|
+
first session to Stop adopt ownership. The independent review showed this
|
|
68
|
+
inverts ownership in the real incident pattern (busy owner never stops first).
|
|
69
|
+
Removed entirely; replaced with conservative no-adopt. Tested: un-stamped state
|
|
70
|
+
yields approve with `owner` NOT written.
|
|
71
|
+
|
|
72
|
+
## Level-of-abstraction fit
|
|
73
|
+
|
|
74
|
+
The fix lives at the same layer as the bug: the Stop hook and the state writer.
|
|
75
|
+
It mirrors the already-shipped autonomous stop-hook's session-scoping ladder
|
|
76
|
+
(tmux-name primary, session-UUID backstop, fail-open) without merging the two
|
|
77
|
+
(explicit non-goal — bash hooks don't share code cleanly; premature abstraction
|
|
78
|
+
avoided). No new module, no new service.
|
|
79
|
+
|
|
80
|
+
## Signal-vs-authority compliance
|
|
81
|
+
|
|
82
|
+
The hook is a low-context filter making a binary ownership decision from
|
|
83
|
+
locally-verifiable identifiers (tmux `#S`, stdin `session_id`). It does not
|
|
84
|
+
arrogate higher-level judgment — it only declines to block a session it cannot
|
|
85
|
+
prove it owns. Conservative-by-construction: every ambiguous case resolves to
|
|
86
|
+
`approve` (release), never to `block` (trap). It emits no user-facing messages.
|
|
87
|
+
|
|
88
|
+
## Interactions
|
|
89
|
+
|
|
90
|
+
- **Reinforcement counter:** the owner path is byte-for-byte the prior logic, so
|
|
91
|
+
graduated protection (3/5/10) is unchanged for the owner. Non-owners no longer
|
|
92
|
+
touch the counter at all.
|
|
93
|
+
- **Restart reconcile:** writes `owner.session` ONLY on a confirmed tmux-owner
|
|
94
|
+
match with a rotated UUID. Gated strictly behind the tmux match — a non-owner
|
|
95
|
+
can never clobber `owner.session` (tested explicitly).
|
|
96
|
+
- **stdin consumption:** the hook now reads stdin (`cat`). Stop hooks deliver and
|
|
97
|
+
close stdin in production (the autonomous hook relies on this), so no hang.
|
|
98
|
+
Even with no stdin/session, tmux-scoping alone is sufficient (proven live).
|
|
99
|
+
- **Worktree topology:** ownership is keyed on the cwd-independent tmux name, so
|
|
100
|
+
it is correct whether the owner launched at the main root and `cd`'d into a
|
|
101
|
+
worktree or launched rooted inside the worktree. The old, fragile `$PWD`-based
|
|
102
|
+
stopgap is NOT carried forward.
|
|
103
|
+
- **Migration parity:** inline twin is always-overwritten → every agent gets the
|
|
104
|
+
new hook on update. `build-state.py` rides the repo checkout (the only place
|
|
105
|
+
the bug occurs — see spec §Migration Parity 2). Drift test prevents
|
|
106
|
+
template/inline divergence.
|
|
107
|
+
|
|
108
|
+
## Rollback cost
|
|
109
|
+
|
|
110
|
+
Low and clean. Revert the two src files (and optionally build-state.py); the
|
|
111
|
+
always-overwrite migration restores the prior hook on next update. The added
|
|
112
|
+
`owner` block in state is additive JSON ignored by the old hook — no destructive
|
|
113
|
+
schema migration. No data loss path.
|
|
114
|
+
|
|
115
|
+
## Tracked deferral
|
|
116
|
+
|
|
117
|
+
The SKILL change to pass `--owner-session "$CLAUDE_CODE_SESSION_ID"` (session-UUID
|
|
118
|
+
precision; + its dedicated PostUpdateMigrator migration per Migration Parity §5)
|
|
119
|
+
is a deliberate fast-follow per the approved phasing (Justin approved one-PR-now +
|
|
120
|
+
tiny-follow-up). The flag is already plumbed and tested in `build-state.py`; only
|
|
121
|
+
the SKILL invocation + migration remain. This is tracked, not orphaned.
|
|
122
|
+
|
|
123
|
+
## Verification
|
|
124
|
+
|
|
125
|
+
- 3-tier behavior tests (12) drive the **real shipping hook** (from
|
|
126
|
+
`getHookContent`) against the **real `build-state.py`** with real stdin/tmux
|
|
127
|
+
seams — covering owner-block, non-owner-no-drain, repeated-non-owner,
|
|
128
|
+
session-only owner, identity-unknown fail-open, legacy no-adopt, restart
|
|
129
|
+
reconcile, anti-clobber, terminal-phase. Plus build-state stamp tests (3) and
|
|
130
|
+
the template/inline drift test (1).
|
|
131
|
+
- Live test-as-self: ran the shipping hook with **real** `tmux display-message`
|
|
132
|
+
resolution (no seam) in this session (`echo-build-stop-hook-session-scoping`) —
|
|
133
|
+
confirmed owner→block, non-owner→approve with zero counter drain.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# Side-effects review — fresh-session stop-gate shadow wiring
|
|
2
|
+
|
|
3
|
+
**Scope**: Complete the conservative first rollout for the fresh-session stop-gate: wire the existing server authority/database, install a Stop-hook router, and default to observe-only shadow mode when the gate is healthy.
|
|
4
|
+
|
|
5
|
+
**Files touched**:
|
|
6
|
+
- `src/commands/server.ts` — constructs `StopGateDb` and `UnjustifiedStopGate`, persists mode state, passes both into `AgentServer`.
|
|
7
|
+
- `src/server/stopGate.ts` — persists mode flips to `server-data/stop-gate-mode.json`.
|
|
8
|
+
- `src/server/routes.ts` — records SessionStart rows in `StopGateDb` when hook events arrive.
|
|
9
|
+
- `src/core/PostUpdateMigrator.ts` — installs `stop-gate-router.js` and patches `.claude/settings.json` Stop hooks.
|
|
10
|
+
- `src/commands/init.ts` and `src/templates/hooks/settings-template.json` — include the router on fresh installs.
|
|
11
|
+
- `src/core/installCodexHooks.ts` — mirrors the Stop router into Codex hook config.
|
|
12
|
+
- Focused tests cover route mode persistence, hook behavior, Codex registration, and update migration.
|
|
13
|
+
|
|
14
|
+
**Under-block**: Intentional for this PR. Default mode is `shadow` only when the authority and SQLite log initialize successfully; otherwise the gate is `off`. In shadow mode the hook submits evaluations but always lets the agent exit. Enforcement is reserved for a later explicit operator flip.
|
|
15
|
+
|
|
16
|
+
**Over-block**: Minimal. The router only emits `{decision:"block"}` when the server is already in `enforce` mode and the server authority returns `continue` with a reminder. All network errors, malformed hook payloads, missing config, hot-path failures, compaction-in-flight, kill-switch, and degraded initialization paths fail open.
|
|
17
|
+
|
|
18
|
+
**Signal vs authority**: Compliant. The hook collects evidence metadata and simple signals, but never decides whether a Stop is unjustified. The server-side `UnjustifiedStopGate` remains the sole authority for `continue`; the hook is a transport/router.
|
|
19
|
+
|
|
20
|
+
**External surfaces**:
|
|
21
|
+
- New installed hook file: `.instar/hooks/instar/stop-gate-router.js`.
|
|
22
|
+
- Existing routes become live for real Stop events: `GET /internal/stop-gate/hot-path` and `POST /internal/stop-gate/evaluate`.
|
|
23
|
+
- New persisted mode file: `server-data/stop-gate-mode.json`.
|
|
24
|
+
- Existing SQLite event log: `server-data/stop-gate.db`.
|
|
25
|
+
|
|
26
|
+
**Migration parity**:
|
|
27
|
+
- Post-update migration writes the router and patches existing Claude settings.
|
|
28
|
+
- Fresh `instar init` installs the router template.
|
|
29
|
+
- Codex hook installer places the router first in the Stop chain before the existing review trio.
|
|
30
|
+
|
|
31
|
+
**Rollback cost**: Revert this change set. Existing `stop-gate-mode.json` and `stop-gate.db` files can remain on disk; without the router/server wiring they are inert. Emergency runtime rollback without code revert: `instar gate mode off` or set the kill-switch.
|
|
32
|
+
|
|
33
|
+
**Tests**:
|
|
34
|
+
- `npm test -- --run tests/unit/routes-stopGate.test.ts tests/unit/stop-gate-router-hook.test.ts tests/unit/installCodexHooks.test.ts tests/unit/PostUpdateMigrator-codexHooks.test.ts`
|
|
35
|
+
- `npx tsc --noEmit`
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# Side-Effects Review — Deterministic "open this" (CMT-529)
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `threadline-open-this-deterministic`
|
|
4
|
+
**Date:** 2026-05-26
|
|
5
|
+
**Author:** Echo
|
|
6
|
+
**Second-pass reviewer:** (pending — required; message-routing intercept)
|
|
7
|
+
|
|
8
|
+
## Summary of the change
|
|
9
|
+
|
|
10
|
+
Makes "open this" / "tie this to <topic>" in the Threadline hub topic a DETERMINISTIC structural intercept instead of agent-interpreted (which failed — the agent rambled instead of binding). New `src/threadline/hubCommands.ts`: `parseHubCommand` (pure, tightly-anchored) + `bindHubConversation` (shared logic extracted from the route; discriminated result; readable+scrubbed topic name; `autoPick`). The intercept lands in `telegram.onTopicMessage` (`wireTelegramRouting`, src/commands/server.ts) — the convergence point BOTH inbound paths reach — via a late-bound `getHubDeps()` accessor (deps constructed after wiring). `POST /threadline/hub/bind` refactored to call the same helper (autoPick=false → 409 preserved for the API). `CollaborationSurfacer.load()` legacy migration now stamps `surfacedAt` by index (was epoch) so ordering works. Decision point: the hub-command intercept (routing, not block/allow).
|
|
11
|
+
|
|
12
|
+
## Decision-point inventory
|
|
13
|
+
|
|
14
|
+
- `telegram.onTopicMessage` hub-command intercept — **add** — for the hub topic + a matched command, bind structurally + return before session injection.
|
|
15
|
+
- `POST /threadline/hub/bind` — **modify** (refactor to shared helper; behavior unchanged for the API).
|
|
16
|
+
- `CollaborationSurfacer.load()` legacy `surfacedAt` — **modify** — index-based ordering.
|
|
17
|
+
|
|
18
|
+
## 1. Over-block
|
|
19
|
+
No block/allow surface. The intercept only fires when `topicId === hub` AND `parseHubCommand` matches a tightly-anchored command (`/^open(?:\s+this)?\s*[.!]?$/i`, `/^(?:tie|bind)\s+this\s+to\s+.../i`). Ordinary hub chat ("can you open this and explain?") returns null → falls through to the agent. FAIL-OPEN: any intercept error logs + falls through.
|
|
20
|
+
|
|
21
|
+
## 2. Under-block
|
|
22
|
+
A creatively-phrased command ("open it please") won't match → falls through to the agent (who still has the #392 CLAUDE.md guidance as backstop). Acceptable; common forms covered.
|
|
23
|
+
|
|
24
|
+
## 3. Level-of-abstraction fit
|
|
25
|
+
Correct: the intercept sits at `onTopicMessage` alongside the existing `/new`/slash/fix-command interceptions — the single seam both the lifeline-forward and server-polling paths converge on (avoids the dual-path dead-code trap that bit the sentinel + warrants-reply gate). `bindHubConversation` composes existing primitives (ConversationStore.mutate, findOrCreateForumTopic, CommitmentTracker.mutate, surfacer.markBound/noteInHub) — no re-implementation.
|
|
26
|
+
|
|
27
|
+
## 4. Signal vs authority compliance
|
|
28
|
+
No new blocking authority. The intercept is a deterministic router (match → bind → return), the bind is an authoritative state mutation on explicit operator action, parseHubCommand is a pure classifier. Per `docs/signal-vs-authority.md`: routers/sinks, not gates.
|
|
29
|
+
|
|
30
|
+
## 5. Interactions
|
|
31
|
+
- **No double-bind/post:** `bindHubConversation` posts to the new topic + one `noteInHub`; the intercept does NOT add a third message (reuses the helper's confirmation), and `return`s so no session injection. One bind per command (markBound makes a re-issued "open this" pick the next unbound).
|
|
32
|
+
- **Order vs other intercepts:** sits after `/new` + slash + (and, on the forward path, the sentinel) — emergency-stop still wins. No shadowing.
|
|
33
|
+
- **autoPick split:** intercept (human) autoPick=true → most-recent; API path autoPick=false → 409. Distinct, intentional.
|
|
34
|
+
|
|
35
|
+
## 6. External surfaces
|
|
36
|
+
New `getHubDeps` param on `wireTelegramRouting` (internal). Hub stays silent. Topic name from gist is **capped ~40 chars, charset-scrubbed, and falls back to `<peer> · <threadId8>` on empty/credential-like gist** (a cold first message could contain a secret — never splash it into a chat-list-visible title). Template + `migrateClaudeMd` note that "open this" is now structural (Agent Awareness + Migration Parity).
|
|
37
|
+
|
|
38
|
+
## 7. Rollback cost
|
|
39
|
+
Localized: new hubCommands module, one route refactor, one intercept block + a param threaded to two call sites, one load() timestamp line, the naming helper. Clean `git revert`. The legacy `surfacedAt` change is read-time + idempotent (no data migration). `getHubDeps` is an optional param (older callers unaffected).
|
|
40
|
+
|
|
41
|
+
## Second-pass review
|
|
42
|
+
|
|
43
|
+
**Concur with the review** (independent reviewer, 2026-05-26). All seven checks verified against the diff: (1) `getHubDeps` late-bind is TDZ/null-safe — the closure only runs at message-time, long after the `const` deps initialize; the `&& telegram` guard narrows `TelegramAdapter|undefined`; `commitmentTracker` is unconditionally constructed + null-guarded internally; tsc clean. (2) Intercept gates on `getHubTopicId()` match, falls through otherwise, whole block try/catch fail-open. (3) `parseHubCommand` anchoring verified empirically across 15 inputs — "open this"/"open"/"Open This." fire; "can you open this and explain?" / "open this conversation please" fall through. (4) `bindHubConversation` returns a discriminated result, never touches res/req; route maps 400/404/409/500. (5) Early `return` skips no essential side-effect — consistent with the adjacent `/`/`/new` intercepts (no message logging in this handler). (6) `topicNameFor` caps 40 / scrubs / credential-fallback. (7) The CMT-529 migrator re-patch is idempotent + correctly scoped (matches only OLD CMT-519 agents; non-greedy anchored regex; 2nd run no-op).
|
|
44
|
+
|
|
45
|
+
Non-blocking notes (no action needed): an 18-digit "tie this to <huge number>" would be treated as a name not an id (unrealistic, harmless); legacy `surfacedAt=new Date(index+1)` ISO strings stay lexicographically monotonic well past realistic array sizes.
|