instar 1.3.753 → 1.3.755

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 (39) hide show
  1. package/dist/commands/server.d.ts +30 -1
  2. package/dist/commands/server.d.ts.map +1 -1
  3. package/dist/commands/server.js +238 -19
  4. package/dist/commands/server.js.map +1 -1
  5. package/dist/config/ConfigDefaults.d.ts.map +1 -1
  6. package/dist/config/ConfigDefaults.js +40 -0
  7. package/dist/config/ConfigDefaults.js.map +1 -1
  8. package/dist/core/ProfileIntentClassifier.d.ts +167 -0
  9. package/dist/core/ProfileIntentClassifier.d.ts.map +1 -0
  10. package/dist/core/ProfileIntentClassifier.js +421 -0
  11. package/dist/core/ProfileIntentClassifier.js.map +1 -0
  12. package/dist/core/componentCategories.d.ts.map +1 -1
  13. package/dist/core/componentCategories.js +11 -0
  14. package/dist/core/componentCategories.js.map +1 -1
  15. package/dist/core/devGatedFeatures.d.ts.map +1 -1
  16. package/dist/core/devGatedFeatures.js +12 -0
  17. package/dist/core/devGatedFeatures.js.map +1 -1
  18. package/dist/core/topicProfileIngress.d.ts +14 -2
  19. package/dist/core/topicProfileIngress.d.ts.map +1 -1
  20. package/dist/core/topicProfileIngress.js +15 -44
  21. package/dist/core/topicProfileIngress.js.map +1 -1
  22. package/dist/data/llmBenchCoverage.d.ts.map +1 -1
  23. package/dist/data/llmBenchCoverage.js +15 -0
  24. package/dist/data/llmBenchCoverage.js.map +1 -1
  25. package/dist/threadline/HubIntentClassifier.d.ts +150 -0
  26. package/dist/threadline/HubIntentClassifier.d.ts.map +1 -0
  27. package/dist/threadline/HubIntentClassifier.js +322 -0
  28. package/dist/threadline/HubIntentClassifier.js.map +1 -0
  29. package/dist/threadline/hubCommands.d.ts +16 -12
  30. package/dist/threadline/hubCommands.d.ts.map +1 -1
  31. package/dist/threadline/hubCommands.js +16 -30
  32. package/dist/threadline/hubCommands.js.map +1 -1
  33. package/package.json +1 -1
  34. package/src/data/builtin-manifest.json +2 -2
  35. package/src/data/llmBenchCoverage.ts +16 -0
  36. package/upgrades/1.3.754.md +46 -0
  37. package/upgrades/1.3.755.md +75 -0
  38. package/upgrades/side-effects/keyword-intent-conversion-3-hubcommands.md +170 -0
  39. package/upgrades/side-effects/topicprofile-intent-llm-rebuild.md +87 -0
@@ -0,0 +1,75 @@
1
+ # Upgrade Guide — vNEXT
2
+
3
+ <!-- assembled-by: assemble-next-md -->
4
+ <!-- bump: patch -->
5
+
6
+ ## What Changed
7
+
8
+ The recognizer that decides whether a mid-conversation message means *"change this topic's
9
+ framework / model / thinking"* — e.g. "use codex here", "switch this topic to gemini", "set high
10
+ thinking on this topic" — was rebuilt from keyword regexes into an LLM classifier
11
+ (`src/core/ProfileIntentClassifier.ts`). The old code (`parseProfileTrigger`'s
12
+ `/^use (codex|claude|gemini|pi…) …$/`, `/^switch this topic to …$/`, `THINKING_WORDS`, and the
13
+ `pin this topic to <id>$/` forms) was offender #1 of the 2026-07-03 keyword-intent audit: a
14
+ keyword/regex list DECIDING what a human meant. Those regexes were tightly anchored (so few false
15
+ positives), but they are brittle in the MISS direction — a valid paraphrase ("let's run this topic
16
+ on claude") had no anchor — and, more fundamentally, they are the exact anti-pattern the
17
+ constitutional standard *"Intelligence Infers, Keywords Only Guard"* forbids. The new classifier
18
+ infers the change intent from the message **and** a bounded window of recent conversation, and
19
+ constrains the value to a closed enum (framework ∈ configured frameworks, model ∈ known ids/tiers,
20
+ thinking ∈ off/low/medium/high/max) via **code-side validation of the model's emitted field** —
21
+ never string-matching the model's prose. It is conversion #1 under that standard, mirroring the
22
+ merged move-intent exemplar (PR #1367).
23
+
24
+ It **fails open**: on any uncertainty — no provider, breaker open, timeout, unparseable/schema-
25
+ violating output, value-not-in-enum, value-not-grounded-in-the-latest-message, or low confidence —
26
+ the message passes straight through to the agent, never actuating a respawn. A **grounding guard**
27
+ (`valueGroundedInLatestMessage`) additionally requires the resolved value to appear in the LATEST
28
+ message, so a value inferred purely from stale prior context ("yeah go with that") never actuates —
29
+ closing a confirm-slot-bypass the context window would otherwise open. The LLM call is `gating:true`
30
+ (swap-provider-before-fail, no silent heuristic drop). It ships **dev-gated dark on the fleet +
31
+ dry-run first on a development agent** (it logs would-actuate vs would-pass to
32
+ `logs/profile-intent.jsonl` — no raw message content — and actuates nothing until a deliberate
33
+ `dryRun:false`). The framework/model/thinking regexes are removed from `parseProfileTrigger`; the
34
+ command kinds (readout / undo / clear / reapply / switch-now / confirm) and the explicit
35
+ `effort` + `escalationOverride` forms remain (structural / explicit-mandate, not framework/model/
36
+ thinking intent). The `TopicProfileWriteSurface` authority + confirm-slot ordering guards are
37
+ unchanged.
38
+
39
+ ## What to Tell Your User
40
+
41
+ Nothing user-facing right now — this ships dark on the fleet and dry-run on a development agent, so
42
+ no behavior changes until it's deliberately graduated (and the whole topic-profile WRITE layer is
43
+ itself already dev-gated + dry-run). If asked why the agent might once have missed a phrasing like
44
+ "let's run this topic on claude", or why "should we use codex here?" should never flip the setting:
45
+ the old recognizer was a brittle regex list, now replaced by an LLM that judges intent from the
46
+ message and its conversation context and errs toward *not* changing your topic when unsure.
47
+
48
+ ## Summary of New Capabilities
49
+
50
+ - `src/core/ProfileIntentClassifier.ts` — LLM-with-context framework/model/thinking intent
51
+ recognizer (`classifyProfileIntent` + `toProfilePatch`); structured-output enum guardrail +
52
+ latest-message grounding guard validated in code; fail-open on all uncertainty; `gating:true`.
53
+ - Config `topicProfiles.intentClassifier` (`enabled` dev-gated; `dryRun:true`, `minConfidence:0.85`,
54
+ `timeoutMs:4000`, `contextWindowTurns:6`, `modelTier:'fast'`); registered in `DEV_GATED_FEATURES`;
55
+ attributed `gate` in `componentCategories`; row in `docs/LLM-ROUTING-REGISTRY.md`; bench coverage
56
+ queued (`llmBenchCoverage.ts`, wave-3).
57
+ - `logs/profile-intent.jsonl` — machine-local dry-run soak log (LLM-engaged decisions only; enum
58
+ fields + message length, NO raw content).
59
+ - Committed discrimination corpus + opt-in real-model benchmark (`INSTAR_LIVE_PROFILE_INTENT=1`),
60
+ the graduation gate before `dryRun:false` (≥200 decisions, <1% false-actuation, zero
61
+ context-resolved actuations).
62
+
63
+ ## Evidence
64
+
65
+ Not reproducible in dev as a live false-positive: the removed regexes were `^…$`-anchored, so
66
+ they rarely MIS-fired today ("should we use codex here?" never matched `^use …$`). This converts
67
+ the banned anti-pattern (a keyword/regex list DECIDING natural-language intent) pre-emptively —
68
+ the sibling offender #2 (`NicknameCommand`) is the one that produced the actual 2026-07-03 hijack;
69
+ offender #1 is the same CLASS, converted before it can misfire. Observed before/after is proven by
70
+ the committed discrimination corpus (`tests/unit/profile-intent-discrimination.test.ts`): the old
71
+ class fires on the words, not the meaning — the corpus asserts, both directions, that "use codex
72
+ here" actuates while "should we use codex here?", "codex here keeps failing", and a stale-context
73
+ "yeah go with that" all pass through, and that a valid paraphrase the anchored regex MISSED
74
+ ("let's run this topic on claude") is now recognized. The deterministic harness runs in CI; the
75
+ opt-in `INSTAR_LIVE_PROFILE_INTENT=1` real-model harness is the graduation gate before `dryRun:false`.
@@ -0,0 +1,170 @@
1
+ # Side-Effects Review — Hub-intent recognizer: regex → LLM-with-context (Conversion #3)
2
+
3
+ **Version / slug:** `keyword-intent-conversion-3-hubcommands`
4
+ **Date:** `2026-07-03`
5
+ **Author:** `echo`
6
+ **Second-pass reviewer:** `subagent (fresh reviewer) — see appended verdict`
7
+
8
+ ## Summary of the change
9
+
10
+ Replaces the keyword/regex DECISION in `src/threadline/hubCommands.ts` `parseHubCommand()` (anchored
11
+ whole-message regexes for "open this" / "tie this to <topic>") with an LLM-with-context classifier,
12
+ `src/threadline/HubIntentClassifier.ts`. The recognizer runs at the `telegram.onTopicMessage` hub
13
+ intercept in `src/commands/server.ts`, where a positive decision SWALLOWS the message before the agent
14
+ sees it and performs a bind — so a misread silently EATS a real message. The new classifier infers
15
+ open/tie intent from the message + a bounded recent-conversation window, constrains a `tie` target to a
16
+ structured enum of real existing topics (validated in code by numeric id membership, never prose-match),
17
+ and FAILS OPEN on every uncertainty (message passes through, never swallowed). The authoritative binder
18
+ `bindHubConversation` is unchanged. Ships dev-gated dark (config `threadline.hubIntent`, `enabled` omitted
19
+ → `resolveDevAgentGate`) + dry-run first (`logs/hub-intent.jsonl`). Files: `HubIntentClassifier.ts` (new),
20
+ `hubCommands.ts` (regex removed), `server.ts` (wiring + `resolveHubClassifierDeps`), `ConfigDefaults.ts`,
21
+ `devGatedFeatures.ts`, `componentCategories.ts`, `LLM-ROUTING-REGISTRY.md`, three test tiers + updated
22
+ `hubCommands.test.ts` + recomputed dark-gate line-map. Follows the proven `MoveIntentClassifier` exemplar
23
+ (PR #1367).
24
+
25
+ ## Decision-point inventory
26
+
27
+ - `hubCommands.parseHubCommand` (`src/threadline/hubCommands.ts`) — **remove** — the anchored-regex "is
28
+ this a hub bind command?" decision is deleted.
29
+ - `HubIntentClassifier.classifyHubIntent` (`src/threadline/HubIntentClassifier.ts`) — **add** — the LLM
30
+ decision that replaces it (open/tie/null + enum-validated target + confidence).
31
+ - `onTopicMessage` hub intercept (`src/commands/server.ts`) — **modify** — swaps regex call for classifier
32
+ + dev-gate + dry-run gate + audit; still returns (swallows) only on a real, non-dry-run command.
33
+ - `bindHubConversation` (`src/threadline/hubCommands.ts`) — **pass-through** — unchanged binder.
34
+
35
+ ---
36
+
37
+ ## 1. Over-block
38
+
39
+ Over-block here = wrongly SWALLOWING a legitimate message the user did not mean as a bind command (the
40
+ exact harm the old regex caused, e.g. "should I open this?" / "open this in a new tab"). The new decision
41
+ is LLM-with-context, so these are correctly classified as discussion (covered by the discrimination
42
+ corpus). Structural protections against over-block: (a) FAIL-OPEN — any uncertainty passes through;
43
+ (b) a high confidence bar (0.85) to swallow; (c) a `tie` whose target isn't a real topic id passes
44
+ through; (d) the whole path is DARK on the fleet and DRY-RUN on dev, so nothing is swallowed until a
45
+ deliberate `dryRun:false` gated on a live accuracy benchmark. Net: over-block strictly decreases vs the
46
+ shipped regex.
47
+
48
+ ## 2. Under-block
49
+
50
+ Under-block = failing to recognize a genuine bind command (message reaches the agent instead of
51
+ auto-binding). This is the SAFE direction here (a missed auto-bind costs a restate; the agent can still
52
+ bind via the API). Sources: (a) the cheap pre-filter skips messages with no bind-ish stem word, so a
53
+ paraphrase like "put this under the roadmap topic" is not auto-bound; (b) fail-open passes through on
54
+ provider failure; (c) while dark/dry-run nothing is bound. All acceptable — the design deliberately trades
55
+ missed auto-binds for never eating a message.
56
+
57
+ ## 3. Level-of-abstraction fit
58
+
59
+ Correct layer: an AUTHORITY (context-rich LLM reasoning), not a brittle detector. It replaces a brittle
60
+ detector (regex) that wrongly held swallow authority. It routes through the shared `IntelligenceProvider`
61
+ (the same smart-gate substrate `CoherenceGate` uses) rather than re-implementing an LLM path, and reuses
62
+ the existing `bindHubConversation` binder rather than duplicating bind logic. The cheap pre-filter is a
63
+ low-level primitive used ONLY to drop toward pass-through (never to decide a positive), exactly as the
64
+ standard permits.
65
+
66
+ ## 4. Signal vs authority compliance
67
+
68
+ **Required reference:** docs/signal-vs-authority.md
69
+
70
+ - [x] Yes — the logic is a smart gate with full conversational context (LLM-backed with recent history).
71
+
72
+ The classifier holds the swallow decision, but it is an LLM reasoning over the message + a bounded recent
73
+ conversation window (not brittle logic), and it fails open. The only string-matching in the module is the
74
+ pre-filter, which can never DECIDE a positive command — it only drops obvious non-commands toward
75
+ pass-through. Enum validation of the model's emitted `targetTopicId` is numeric-id membership, not
76
+ prose-matching.
77
+
78
+ ## 5. Interactions
79
+
80
+ - **Shadowing:** the hub intercept runs inside `onTopicMessage` before normal session routing, exactly
81
+ where the regex ran. When dark/dry-run OR fail-open OR not-a-command, it returns `handled:false`
82
+ (falls through) so downstream routing (fix-commands, session dispatch) is unaffected — it can only
83
+ shadow routing when it genuinely swallows a real command, which was the old behavior too.
84
+ - **Double-fire:** only one intercept owns hub-command recognition; there is no second recognizer to
85
+ double-fire with. The `POST /threadline/hub/bind` API route is a separate, explicit caller (no text
86
+ decision) and is unchanged.
87
+ - **Races:** none new — the classifier is stateless; the audit append is best-effort and wrapped so it
88
+ never gates the message. `bindHubConversation`'s existing CAS mutate is unchanged.
89
+ - **Feedback loops:** none — the classifier reads recent history but does not write to it.
90
+
91
+ ## 6. External surfaces
92
+
93
+ - **Other agents / users:** none while dark on the fleet. On a dev agent (dry-run) the only new surface is
94
+ the machine-local `logs/hub-intent.jsonl` (80-char scrubbed preview, LLM-engaged decisions only).
95
+ - **External systems:** one bounded fast-tier LLM call per candidate hub message through the shared
96
+ provider (spawn-cap + breaker), attributed `HubIntentClassifier`. No new network egress beyond that.
97
+ - **Persistent state:** none beyond the append-only dry-run log.
98
+ - **Operator surface (Mobile-Complete):** no operator-facing actions added — the only control is a config
99
+ flag (`threadline.hubIntent.dryRun/enabled`), same as every dev-gated feature. Not applicable.
100
+
101
+ ## 6b. Operator-surface quality
102
+
103
+ No operator surface — not applicable. This change touches no dashboard renderer, approval page, or
104
+ grant/revoke/secret form.
105
+
106
+ ## 7. Multi-machine posture
107
+
108
+ **machine-local BY DESIGN.** The hub intercept and its dry-run log are per-machine inbound-message
109
+ processing on whichever machine owns the hub conversation; there is no cross-machine state to replicate
110
+ and no URL generated. It emits NO user-facing notices (it either swallows a command and lets the existing
111
+ binder post the hub confirmation, or passes through), so no one-voice gating is needed. It holds no durable
112
+ state that could strand on topic transfer (the audit log is local observability). The config flag resolves
113
+ per-machine via `resolveDevAgentGate`, consistent with every other dev-gated feature; on the fleet it is
114
+ uniformly dark.
115
+
116
+ ## 8. Rollback cost
117
+
118
+ Pure code change — revert and ship a patch. No persistent state needing cleanup (the dry-run log is
119
+ append-only machine-local observability that can be ignored/deleted). No agent-state repair, no
120
+ user-visible regression during the rollback window (the feature is dark on the fleet, so a revert is a
121
+ no-op for fleet agents). The rollback lever short of a revert is `threadline.hubIntent` staying
122
+ dark/dry-run, or `enabled:false` to force-dark even a dev agent.
123
+
124
+ ---
125
+
126
+ ## Second-pass review (Phase 5)
127
+
128
+ **Reviewer:** fresh subagent (independent audit). **Verdict: Concur with the review.**
129
+
130
+ The reviewer independently traced every `return` in `classifyHubIntent` and confirmed only two paths
131
+ yield `isCommand:true` (a high-confidence `open`, or a high-confidence `tie` with an enum-resolved
132
+ target), both behind the confidence gate; every other path (empty text, no bind signal, no provider,
133
+ throw/timeout, unparseable/schema-violation, intent null, below confidence, tie-target-not-in-enum)
134
+ returns pass-through. The wiring's `willAct = isCommand && !dryRun` gate means the message is swallowed
135
+ ONLY on a real command with a resolved target and `dryRun:false`; the dark gate (`if clsDeps?.enabled`)
136
+ and dry-run default both skip swallowing; the audit write is wrapped so it can never break the path, and
137
+ the outer try/catch fails open to normal routing. Enum validation is numeric-id membership (never
138
+ prose-match); the pre-filter can only drop toward pass-through. The classifier never throws.
139
+
140
+ **One non-blocking observation:** while dark on the fleet, the "open this"/"tie this" structural
141
+ interception no longer fires (no regex fallback), so those messages reach the agent — whereas the fleet
142
+ CLAUDE.md still says the agent will NOT see "open this." This is the intentional dark-rollout tradeoff
143
+ already recorded in §2 (Under-block) / §5 (Shadowing); it is the SAFE direction (pass-through, never
144
+ swallow) and not a safety concern. It is a product-completeness item to close at graduation (flip the
145
+ gate, or update the fleet CLAUDE.md guidance for the dark window) — not a blocker for this change.
146
+
147
+ ---
148
+
149
+ **CI follow-up (2026-07-04):** the new `HubIntentClassifier` gate is registered in
150
+ `COMPONENT_CATEGORY`, which the untrusted-input-classification ratchet requires to carry an explicit
151
+ `LLM_UNTRUSTED_INPUT` classification. It judges an inbound hub message's bind-intent (untrusted user
152
+ text), so it is classified `true` in `src/data/llmBenchCoverage.ts`. Mechanical consequence of the
153
+ component registration; no behavior change.
154
+
155
+ ---
156
+
157
+ ## Post-rebase addendum (main now carries #1367)
158
+
159
+ After the move-intent exemplar (PR #1367) merged, `main` gained the keyword-intent classification-ratchet family. Rebasing this branch onto it required registering `HubIntentClassifier` (a new `COMPONENT_CATEGORY` key) across every classification map, exactly mirroring how #1367 registered `MoveIntentClassifier`:
160
+
161
+ - `src/data/llmBenchCoverage.ts`:
162
+ - `LLM_BENCH_COVERAGE`: `{ exempt }` — ships its own discrimination benchmark (`tests/unit/hub-intent-discrimination.test.ts` + opt-in `INSTAR_LIVE_HUB_INTENT=1`), the co-located benchmark IS the benchmark (same argument as MoveIntentClassifier / InteractivePoolCanaryJudge).
163
+ - `LLM_JUDGES_CLAIMS`: bare `false` — classifies a USER's bind-intent, not an agent/session completion/health/credit claim.
164
+ - `LLM_PARSER_CONTRACT`: `{ pending: 'contract-wave-2' }` — parses a closed intent(open/tie/null) + targetTopicId-enum + confidence verdict.
165
+ - `LLM_UNTRUSTED_INPUT`: `true` — judges untrusted inbound hub text (landed in the prior fix commit).
166
+ - Pinned shrink-only baselines updated (each a visible, reviewed act): `EXEMPT_BASELINE` (`tests/unit/llm-bench-coverage-ratchet.test.ts`) and `PENDING_BASELINE` (`tests/unit/parser-contract-classification-ratchet.test.ts`) each gain `HubIntentClassifier`.
167
+ - `tests/unit/keyword-intent-decision-ratchet.test.ts`: `threadline/hubCommands.ts` removed from `EXPECTED_OFFENDERS` and `BASELINE` dropped 5→4 (the converted file no longer keyword-decides intent; the detector confirms exactly 4 remaining offenders). `topicProfileIngress` #1 remains for its own conversion.
168
+ - Merge conflicts in `componentCategories.ts`, `devGatedFeatures.ts`, `LLM-ROUTING-REGISTRY.md`, and `lint-dev-agent-dark-gate.test.ts` resolved keeping BOTH #1367's and this change's registrations; the dark-gate line-map recomputed against the merged `ConfigDefaults` (hubIntent +20 lines and moveIntent +18 lines both present, neither adds an attributed path).
169
+
170
+ No behavior change from this addendum — all registrations are ratchet metadata / test baselines. The classifier, wiring, config, and fail-open contract are unchanged from the reviewed version above.
@@ -0,0 +1,87 @@
1
+ # Side-Effects Review — topicprofile-intent-llm-rebuild
2
+
3
+ **Change:** Replace the FRAMEWORK/MODEL/THINKING keyword regexes in
4
+ `parseProfileTrigger` with an LLM-with-context recognizer (`ProfileIntentClassifier`),
5
+ per the standard "Intelligence Infers, Keywords Only Guard" (keyword-intent audit offender #1).
6
+ Dev-gated dark + dry-run-first; fail-open.
7
+
8
+ **Spec:** `docs/specs/topicprofile-intent-llm-rebuild.md` (converged, approved)
9
+
10
+ ## Phase 1 — Principle check (signal vs authority)
11
+
12
+ Yes — this is a decision point (it gates whether an inbound message actuates a topic-profile write
13
+ vs passes through to the agent). It is designed as a **signal producer**: the classifier's only
14
+ power is to move a message from "actuate a write" toward "pass through" (fail-open). The authority —
15
+ `TopicProfileWriteSurface.applyWrite` — is unchanged and independently re-validates every field
16
+ against the closed enums. No brittle check holds blocking authority; the classifier can never
17
+ actuate a value the write surface would reject.
18
+
19
+ ## The 8 questions
20
+
21
+ 1. **Over-block (rejects legitimate inputs it shouldn't).** The grounding guard requires the
22
+ resolved value to appear in the LATEST message, so a purely context-resolved command ("yeah go
23
+ with that") is intentionally NOT actuated — it passes through to the agent. This is a deliberate
24
+ safety choice (a respawn off stale context is the harm). The cost is cheap: the operator restates
25
+ ("switch to gemini") or the agent handles it. Not a defect.
26
+
27
+ 2. **Under-block (failure modes it still misses).** A genuinely-intended change the LLM classifies
28
+ as `isChange:false` is a MISS — cheap by design (fail-open; the operator restates). The prefilter
29
+ is drop-only and inclusive, so it does not cause misses beyond the LLM's own judgment. A cross-
30
+ framework model id the classifier passes is refused at the write surface with a named reason (no
31
+ wrong respawn).
32
+
33
+ 3. **Level-of-abstraction fit.** Correct layer: the recognizer's DECISION moves from regex to LLM;
34
+ the actuator (write surface, validation, respawn debounce, confirm slots) is untouched. It feeds
35
+ the existing authority rather than paralleling it. The command kinds + effort/escalation stay in
36
+ `parseProfileTrigger` (structural / explicit-mandate forms, not framework/model/thinking intent).
37
+
38
+ 4. **Signal vs authority compliance.** Compliant — see Phase 1. Fail-open (never actuates on doubt),
39
+ `gating:true` (swap-provider-before-fail, no silent heuristic drop), independent write-surface
40
+ re-validation.
41
+
42
+ 5. **Interactions (shadow / shadowed / double-fire / race).** Runs ONLY when `parseProfileTrigger`
43
+ returns null, so it never shadows or double-fires with the deterministic command kinds (readout /
44
+ undo / clear / reapply / switch-now / confirm) or the retained effort/escalation writes. Bare
45
+ affirmatives ("yes", "do it") are consumed by `parseProfileTrigger`'s `confirm` kind and the
46
+ armed-slot machinery BEFORE the classifier — the classifier never competes with the confirm-slot
47
+ ordering/TTL guards. The grounding guard specifically prevents the classifier from re-introducing
48
+ the confirm-slot-bypass an unconstrained context window would open.
49
+
50
+ 6. **External surfaces.** Adds no HTTP route. Writes a dev-agent-only, machine-local soak log
51
+ (`logs/profile-intent.jsonl`) with NO raw message content (enum fields + length only). The LLM
52
+ call is one bounded fast-tier call per candidate message, attributed to `/metrics/features` as
53
+ `ProfileIntentClassifier` (gate). On the fleet it is dark (no external surface changes at all).
54
+
55
+ 7. **Multi-machine posture (Cross-Machine Coherence).** Machine-local BY DESIGN
56
+ (`physical-credential-locality`): the recognizer runs on the topic-owning machine's Telegram
57
+ inbound path and gates a session that lives on that machine; there is no shared state to
58
+ replicate. `unified` would be infeasible (nothing to replicate). The one operational note:
59
+ graduation evidence is read from the auto-aggregated `/metrics/features` surface, not by hand-
60
+ unioning per-machine logs. No user-facing notice (one-voice) concern — the only user-facing
61
+ output is the write surface's existing disclosure reply, unchanged. No durable state strands on
62
+ topic transfer (the classifier holds none). No generated URLs.
63
+
64
+ 8. **Rollback cost.** Trivial and doubly-inert: `enabled` is omitted (dev-gate dark on the fleet),
65
+ `dryRun:true` default (never actuates), and the whole topic-profile WRITE layer is separately
66
+ dev-gated + dryRun. Flipping `topicProfiles.intentClassifier.enabled:false` (or reverting the
67
+ commit) fully removes it; no data migration, no state repair. On the fleet, framework/model/
68
+ thinking conversational pins simply pass through to the agent, as they would with the feature
69
+ absent.
70
+
71
+ ## Phase 5 — Second-pass review (required: inbound-message gate)
72
+
73
+ This change gates inbound messaging, so a dedicated adversarial + integration review was run
74
+ (two independent reviewer passes + two cross-model external passes) during spec convergence. The
75
+ one MATERIAL adversarial finding — context-referential affirmatives bypassing the confirm-slot
76
+ guards to actuate directly off stale context — was found and CLOSED with the deterministic
77
+ `valueGroundedInLatestMessage` grounding guard (the value must be named in the latest message to
78
+ actuate), proven by the `guard-context-only-value` corpus case and unit tests. All minor findings
79
+ (symmetric untrusted-data delimiting, scrubbed log, cleared timeout, context dedupe, `gating:true`
80
+ swap-first) were folded in. The multi-machine reviewer confirmed machine-local-by-design is
81
+ correct.
82
+
83
+ **Concur with the review.** The final design holds no brittle blocking authority, fails open to the
84
+ more-capable agent on every uncertainty, and the write-surface authority + confirm-slot ordering
85
+ guards are unchanged. The single new actuation trigger (a paraphrase the old anchored regex missed)
86
+ is strictly gated by the grounding guard + enum guardrail + confidence gate + dev-gate + dryRun.
87
+ No residual concern.