instar 1.3.1304 → 1.3.1306

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 (46) hide show
  1. package/dashboard/feedback-readiness-authority.js +155 -0
  2. package/dashboard/index.html +34 -0
  3. package/dist/commands/server.d.ts.map +1 -1
  4. package/dist/commands/server.js +1 -0
  5. package/dist/commands/server.js.map +1 -1
  6. package/dist/core/IntelligenceRouter.d.ts +19 -0
  7. package/dist/core/IntelligenceRouter.d.ts.map +1 -1
  8. package/dist/core/IntelligenceRouter.js +37 -0
  9. package/dist/core/IntelligenceRouter.js.map +1 -1
  10. package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
  11. package/dist/core/PostUpdateMigrator.js +30 -0
  12. package/dist/core/PostUpdateMigrator.js.map +1 -1
  13. package/dist/core/TopicIntentCapture.d.ts +4 -1
  14. package/dist/core/TopicIntentCapture.d.ts.map +1 -1
  15. package/dist/core/TopicIntentCapture.js +13 -0
  16. package/dist/core/TopicIntentCapture.js.map +1 -1
  17. package/dist/data/standards-guard-index.json +1 -1
  18. package/dist/data/standards-guard-index.meta.json +2 -2
  19. package/dist/data/standards-registry.meta.json +1 -1
  20. package/dist/feedback-factory/drain/readinessAuthorityProposal.d.ts +70 -0
  21. package/dist/feedback-factory/drain/readinessAuthorityProposal.d.ts.map +1 -0
  22. package/dist/feedback-factory/drain/readinessAuthorityProposal.js +93 -0
  23. package/dist/feedback-factory/drain/readinessAuthorityProposal.js.map +1 -0
  24. package/dist/monitoring/WorktreeMonitor.d.ts +15 -0
  25. package/dist/monitoring/WorktreeMonitor.d.ts.map +1 -1
  26. package/dist/monitoring/WorktreeMonitor.js +90 -14
  27. package/dist/monitoring/WorktreeMonitor.js.map +1 -1
  28. package/dist/scaffold/templates.d.ts.map +1 -1
  29. package/dist/scaffold/templates.js +11 -0
  30. package/dist/scaffold/templates.js.map +1 -1
  31. package/dist/server/AgentServer.d.ts.map +1 -1
  32. package/dist/server/AgentServer.js +3 -1
  33. package/dist/server/AgentServer.js.map +1 -1
  34. package/dist/server/routes.d.ts +5 -0
  35. package/dist/server/routes.d.ts.map +1 -1
  36. package/dist/server/routes.js +44 -1
  37. package/dist/server/routes.js.map +1 -1
  38. package/package.json +1 -1
  39. package/src/data/builtin-manifest.json +64 -64
  40. package/src/data/standards-guard-index.json +1 -1
  41. package/src/data/standards-guard-index.meta.json +2 -2
  42. package/src/data/standards-registry.meta.json +1 -1
  43. package/src/scaffold/templates.ts +11 -0
  44. package/upgrades/1.3.1306.md +125 -0
  45. package/upgrades/side-effects/feedback-readiness-authority-card.md +105 -0
  46. package/upgrades/side-effects/worktree-monitor-dedupe.md +133 -0
@@ -0,0 +1,125 @@
1
+ # Upgrade Guide — vNEXT
2
+
3
+ <!-- assembled-by: assemble-next-md -->
4
+ <!-- bump: patch -->
5
+
6
+ ## What Changed
7
+
8
+ The operated feedback drain refuses every tick with 403 "current registered
9
+ readiness agent required" until an operator registers the readiness
10
+ authority, which is the record that says which model may decide which
11
+ feedback reports become work items. Registering was possible only through
12
+ the PIN-gated `POST /feedback-factory/readiness-authorities` with about a
13
+ dozen technical fields, and there was no screen for it. On the Mac Studio
14
+ this left 1,001 reports waiting.
15
+
16
+ - New read-only `GET /feedback-factory/readiness-authorities/proposal`
17
+ (Bearer). The server computes every field the runtime later compares: the
18
+ agent id (`projectName`, which the drain job sends), the drain's owner
19
+ machine and epoch, the provider and model that the intelligence router
20
+ would actually pick for the readiness call, and the prompt, schema and
21
+ decision-point ids. Only the envelope can be edited: batch size (default
22
+ 50), daily spend cap (default $5), and token limit (default 1200). The
23
+ response also carries the status (none / active / proposal-only /
24
+ revoked), any plain-language blockers, and which action an Approve tap
25
+ would perform.
26
+ - The existing PIN route now accepts `useProposal: true` for create and
27
+ replace. When it is set, the server fills the binding fields from its own
28
+ proposal and takes only the envelope from the request. It stays PIN-only
29
+ and same-origin, and the agent still cannot register itself.
30
+ - New `IntelligenceRouter.previewPrimary()`, a read-only preview of the
31
+ primary (framework, model) that `evaluate()` would choose. It follows the
32
+ same enforced-nature or category logic.
33
+ - New dashboard card on the **Feedback Drain** tab: "Who decides which
34
+ feedback becomes work". It shows a one-line status, a plain sentence
35
+ ("…up to 50 reports per batch, at most $5 per day. Anything outside that
36
+ comes to you."), an "Adjust limits" drawer, a PIN box, and Approve (or
37
+ Restore / Save new limits) and Revoke buttons.
38
+ - CLAUDE.md template and migration: new "Feedback Readiness Authority
39
+ (operator approval)" section, also mirrored to Codex/Gemini agents.
40
+
41
+ The worktree monitor posted the same two notices ("Worktree activity detected
42
+ after session …" and "Stale worktrees detected") after almost every job
43
+ session and every five-minute scan. On 29 September that was 549 of 724
44
+ outbound messages, all about the same unmerged branches. It now remembers the
45
+ set of branches it last announced for each notice and posts only when that set
46
+ changes: a branch appears, or one resolves. A set that stays open is repeated
47
+ once a day. The remembered set is saved in
48
+ `state/worktree-monitor/announced.json`, so a restart does not post it again.
49
+ A failed send is not recorded, so the next scan retries it.
50
+
51
+ The topic-intent capture loop no longer sends `provenance: automation`
52
+ messages to the extractor LLM. That covers scheduler job sends and monitor,
53
+ sentinel and system notices. Those messages used about 64% of the extractor's
54
+ input tokens, and most of what they stored was repeated "unmerged branches"
55
+ facts. The check uses the structural provenance stamp set when a message is
56
+ sent, never the message text. User and agent conversation is extracted as
57
+ before, and so are unstamped legacy rows. The rolling topic summary still
58
+ includes automated messages, so the next conversational extraction still sees
59
+ them as context.
60
+
61
+ ## What to Tell Your User
62
+
63
+ The feedback drain has been waiting for your go-ahead before it sorts
64
+ reports into work items. You can now give it from your phone: open the
65
+ dashboard, go to the Feedback Drain tab, and find the card "Who decides
66
+ which feedback becomes work". It says in one sentence what you're agreeing
67
+ to (by default, up to 50 reports per batch and at most $5 a day). Enter
68
+ your PIN and tap Approve. You can revoke it from the same card at any time.
69
+ I can't approve it myself.
70
+
71
+ You'll see far fewer worktree reminders. A notice now means something changed,
72
+ and a branch that is still open gets one reminder a day.
73
+
74
+ ## Summary of New Capabilities
75
+
76
+ - Dashboard → Feedback Drain → "Who decides which feedback becomes work":
77
+ approve, change limits, restore or revoke the readiness authority with
78
+ the PIN.
79
+ - `GET /feedback-factory/readiness-authorities/proposal`: the read-only
80
+ answer to "what exactly would be approved, and why can't it be yet?"
81
+
82
+ - Worktree notices are sent only when the flagged set changes, with one reminder a day; `/hooks/worktrees/last-report` shows when an unchanged finding was suppressed.
83
+ - Automated messages are no longer run through topic-intent extraction.
84
+
85
+ ## Evidence
86
+
87
+ - `tests/integration/feedback-readiness-authority-routes.test.ts`, run
88
+ against a real AgentServer over HTTP. The test reads the proposal, checks
89
+ that a tick 403s, and checks that Bearer without a PIN and a wrong PIN are
90
+ both refused. It then approves with the PIN while also sending
91
+ attacker-supplied `agentId`/`provider`, which are ignored. The next tick
92
+ returns 202 and the run succeeds. The arbiter is called once and its
93
+ model check passes, so the authority stays `active`. It then revokes,
94
+ confirms the tick 403s again, and confirms an out-of-range batch is
95
+ refused.
96
+ - `tests/unit/intelligence-router-preview-primary.test.ts`: the preview
97
+ equals what the real `evaluate()` reports through `onModel` in four modes:
98
+ unconfigured, category override, enforced nature plan, and dryRun nature
99
+ plan. It returns null when the routed framework is unavailable.
100
+ - `tests/unit/feedback-readiness-authority-proposal.test.ts` covers every
101
+ status→action branch, the blockers and the envelope bounds. It also
102
+ replays real recorded shapes from the Mac Studio's `feature-metrics.db`
103
+ (221 codex-cli `gpt-6-astra` `capable` calls). An authority built from the
104
+ proposal passes the real arbiter on that shape and is refused on the
105
+ `gpt-5.6-sol` retirement-fallback shape.
106
+ - `tests/unit/feedback-readiness-authority-ui.test.ts`: covers the card's
107
+ buttons per state, the request bodies (no binding fields are sent), the
108
+ PIN and envelope submitted on Approve, and that values are rendered with
109
+ textContent only. It also checks that the page mounts the card, imports
110
+ the module, and renders the card on tab open but not on the 15-second
111
+ poll.
112
+ - `tests/e2e/feedback-factory-drain-lifecycle.test.ts`: on the production
113
+ init path the route answers 200, tracks the post-failover owner epoch,
114
+ offers no Approve when no routed model exists, and refuses the new path
115
+ without a PIN.
116
+
117
+ - A replay of the real 29 September message log through the new
118
+ `captureTurn`: 549 worktree notices and 64 other automated rows skipped; all
119
+ 111 agent and 19 user rows extracted. That is 130 extractor calls instead of
120
+ 743.
121
+ - A replay of the same day's worktree notice texts under the new rule: 11 of
122
+ 549 would be sent, each because the flagged set changed.
123
+ - Unit, integration (`/hooks/worktrees/last-report`, `/telegram/reply` →
124
+ capture metrics) and e2e tests cover both sides. The new tests fail when
125
+ the new logic is disabled.
@@ -0,0 +1,105 @@
1
+ # Side-Effects Review — Readiness authority approval from the dashboard
2
+
3
+ **Version / slug:** `feedback-readiness-authority-card`
4
+ **Date:** `2026-09-30`
5
+ **Author:** `Echo`
6
+ **Second-pass reviewer:** `independent reviewer subagent (see below)`
7
+
8
+ ## Summary of the change
9
+
10
+ The operated feedback drain on the Mac Studio is live-healthy with 1,001 unprocessed reports, but every tick 403s ("current registered readiness agent required"). The readiness authority can only be registered through the PIN-gated `POST /feedback-factory/readiness-authorities`, which has no human surface and needs a dozen technical fields. The operator (Justin, topic 95267, 2026-09-30 10:26 PDT) approved registering it with maxBatch 50 and maxDailySpendUsd 5. This change adds the operator surface:
11
+
12
+ - `src/feedback-factory/drain/readinessAuthorityProposal.ts` (new, pure): builds the record the runtime will accept. It also returns status, blockers, the Approve action and a plain summary.
13
+ - `src/core/IntelligenceRouter.ts`: new read-only `previewPrimary()` that mirrors `evaluateRouted`'s primary selection.
14
+ - `src/server/routes.ts`: new `GET /feedback-factory/readiness-authorities/proposal` (Bearer, read-only); `useProposal: true` on the existing PIN route makes the server fill the binding fields.
15
+ - `src/server/AgentServer.ts`: exposes `authorityBinding()`, the owner machine and epoch that `canAgentMutateReadiness` compares.
16
+ - `dashboard/feedback-readiness-authority.js` (new) + `dashboard/index.html`: the card on the Feedback Drain tab.
17
+ - CLAUDE.md template, migration, and shadow marker for Agent Awareness.
18
+
19
+ ## Decision-point inventory
20
+
21
+ - `POST /feedback-factory/readiness-authorities` authorization (PIN + X-Instar-Request + same-origin): **pass-through**. It is unchanged and still runs before any new code.
22
+ - `FeedbackDrainStore.mutateAuthority` validation: **pass-through**. The proposal mirrors its envelope bounds for display, and the store remains the authority.
23
+ - `FeedbackDrainService.canAgentMutateReadiness` and `FeedbackReadinessArbiter.decideBatch` provider/model/prompt checks: **pass-through**. Nothing loosens them; the proposal computes values that satisfy them.
24
+
25
+ ---
26
+
27
+ ## 1. Over-block
28
+
29
+ No new block/allow surface. The proposal's `blockers` only decide whether the card offers Approve, and the PIN route re-checks the same conditions and refuses with 409 when `useProposal` cannot build a record. One case: an operator who deliberately wants to register while no routed model exists (for example, before enabling a framework) cannot use the card. The raw-field PIN route still works for that expert path.
30
+
31
+ ## 2. Under-block
32
+
33
+ - The proposal predicts the primary route. If the call later lands elsewhere (the Codex retirement self-heal reports `gpt-5.6-sol`; a failure swap to another framework), the arbiter's existing check refuses and demotes the authority to `proposal-only`. The card then shows "Paused by a safety brake" and offers Approve (replace). This is the designed fail-safe direction and is replayed in `feedback-readiness-authority-proposal.test.ts`. The operator may need to approve again after a model retirement.
34
+ - Found during this build and not fixed here: the live `Config.ts` loader drops `sessions.natureRouting`, so the Studio runs nature routing observe-only even though its config says `dryRun:false`. `previewPrimary` reads the same runtime closure, so the proposal matches the real call (codex-cli / `gpt-6-astra`). If that loader bug is fixed, the next tick after a routing change demotes the authority, and the card offers a re-approval bound to the new route. Fixing the loader would make MessageSentinel's FAST chain fail closed (pi-cli is not enabled), so it is a separate decision. It is reported to the operator in the PR summary.
35
+
36
+ ## 3. Level-of-abstraction fit
37
+
38
+ This is the right layer. The server already owns the facts: the owner binding in AgentServer, routing in IntelligenceRouter, and the prompt/schema ids in the arbiter module. Computing them server-side and letting the operator choose only the envelope keeps the human decision where it belongs. `previewPrimary` sits on the router beside `for()` (the existing diagnostic resolver) instead of re-implementing routing in the route handler.
39
+
40
+ ## 4. Signal vs authority compliance
41
+
42
+ - [x] No — this change has no block/allow surface.
43
+
44
+ The proposal is advisory display. Authority stays with the PIN (human), the store's validation, and the arbiter's runtime checks, none of which changed.
45
+
46
+ ## 4b. Judgment-point check
47
+
48
+ No new static heuristic at a competing-signals decision point. The status→action mapping follows the store's own state machine: a revoked authority must be restored before it can be replaced, and create is refused if a record already exists.
49
+
50
+ ## 5. Interactions
51
+
52
+ - **Shadowing:** `useProposal` is read only after `feedbackMutationIntentValid` passes. Revoke and restore ignore it and reuse the stored record as before.
53
+ - **Double-fire:** the card renders on tab open and after an action, not on the 15-second status poll, so a PIN being typed is never wiped. The status poll is unchanged.
54
+ - **Races:** two approvals at once are serialized by the store's `.immediate()` transaction. The second create gets 409 "authority already exists", which the card shows verbatim.
55
+ - **Feedback loops:** none. The proposal reads state and writes nothing.
56
+
57
+ ## 6. External surfaces
58
+
59
+ - **Operator surface (Mobile-Complete):** this change adds the missing phone surface. It works from the tunnel URL with the dashboard PIN, and the POST is same-origin.
60
+ - Other agents and users: the new route is Bearer-only and read-only. Without the drain (fleet default) it returns 503, like its siblings.
61
+ - Persistent state: nothing new. Approvals write the same `authority_records` and `authority_audit` rows as before, with an `operatorDecisionRef` of the form `dashboard:<timestamp>:<random>`.
62
+
63
+ ## 6b. Operator-surface quality
64
+
65
+ 1. **Leads with the primary action?** Yes. The card shows its title, a one-line status, the plain sentence of what is being approved, the PIN box, then Approve. The numeric limits sit in a collapsed "Adjust limits" drawer because the approved defaults (50 / $5) are prefilled.
66
+ 2. **Zero raw internals as primary content?** Yes. The machine id, epoch and model appear only as a muted "Bound to:" line inside the collapsed drawer. Statuses read "Not set up yet", "Active (version 1): up to 50 reports per batch…", "Paused by a safety brake", "Revoked". No enum values are shown.
67
+ 3. **Destructive actions de-emphasized?** Yes. Revoke is a plain button after the green Approve / Save new limits, never above it, and appears only when there is something to revoke.
68
+ 4. **Plain language + phone width?** Plain language is checked in the UI tests. The card reuses the Spend tab's mobile-first classes: 44px tap targets, 16px inputs (no iOS zoom), full-width stacked fields. I did not take a separate phone-width screenshot of this card.
69
+
70
+ ## 7. Multi-machine posture
71
+
72
+ **Machine-local by design, proxied on the owner.** The authority binds to the drain's canonical owner machine and epoch, and `authorityBinding()` reports this server's view: the configured `operatedHostMachineId` and the coordinator lease epoch. On a non-owner machine, `ownerMachineId` is still the configured owner, so a record approved there binds to the owner. The store is per machine, though, so the approval must be made on the owner's dashboard; the tick proxy already routes ticks to the owner. The card emits no user notices and generates no URLs. Its only durable state is the existing authority tables, which are already in the drain backup/restore path.
73
+
74
+ ## 8. Rollback cost
75
+
76
+ This is a pure code change. Reverting removes the card, the proposal route, `useProposal` and `previewPrimary`. Records approved through the card are ordinary authority generations and keep working, and they can be revoked through the raw PIN route. No migration is needed. The CLAUDE.md section would remain in existing agents' files, pointing at a card that no longer exists, until a follow-up migration removes it by the same marker.
77
+
78
+ ## Conclusion
79
+
80
+ The review changed the design in three ways. Approval uses `useProposal` (server-derived fields) rather than posting client-side copies of technical fields. The card renders outside the 15-second poll. The model binding comes from a router preview that is proven equal to the real `evaluate()` selection, rather than from the static routing map. The static map would have proposed `gpt-5.5`, a retired id that the live call never uses, and the first tick would have demoted the authority. The change is clear to ship. The natureRouting loader drop is flagged to the operator as a separate finding.
81
+
82
+ ---
83
+
84
+ ## Second-pass review (if required)
85
+
86
+ **Reviewer:** independent reviewer subagent
87
+ **Independent read of the artifact: concur**
88
+
89
+ The reviewer found no path for a Bearer-only caller to register, widen or restore. `useProposal` is read only after the PIN, origin and header checks pass, and the store's envelope bounds still apply. They confirmed that `previewPrimary` matches `evaluateRouted` on the enforced, fail-closed, no-route and category paths. They also confirmed that the binding equals what `canAgentMutateReadiness` and the tick job compare. They raised three minor points:
90
+ - Restore ignores the edited limits. Fixed: the card now says Restore brings back the last approved limits, and a UI test covers it.
91
+ - An emptied limit field turned into 0. Fixed: empty fields are omitted, so the server default applies (JS and GET), with a UI test.
92
+ - A `claude-code` subscription-path agent reports `interactive-pool` as its model, so the arbiter would pause the authority (a false pause, fail-safe). This is noted here, and it does not affect the Studio route.
93
+
94
+ ---
95
+
96
+ ## Evidence pointers
97
+
98
+ - `tests/integration/feedback-readiness-authority-routes.test.ts`: before approval the tick 403s; after the PIN approval it returns 202 with a succeeded run and the authority stays active.
99
+ - `tests/unit/intelligence-router-preview-primary.test.ts`: the preview equals the real `onModel` in four routing modes.
100
+ - `tests/unit/feedback-readiness-authority-proposal.test.ts`: replays the recorded live shape codex-cli/`gpt-6-astra` (Mac Studio `.instar/server-data/feature-metrics.db`, 221 rows) and the `gpt-5.6-sol` retirement-fallback shape.
101
+ - `tests/e2e/feedback-factory-drain-lifecycle.test.ts`: the route is alive on the production init path.
102
+
103
+ ## Class-Closure Declaration (display-only mirror)
104
+
105
+ No agent-authored-artifact defect — not applicable. No self-triggered controller is added.
@@ -0,0 +1,133 @@
1
+ # Side-Effects Review — Worktree notices on change; automated sends skip intent extraction
2
+
3
+ **Version / slug:** `worktree-monitor-dedupe`
4
+ **Date:** `2026-09-30`
5
+ **Author:** `Echo`
6
+ **Second-pass reviewer:** `not required`
7
+
8
+ ## Summary of the change
9
+
10
+ Two small changes, both driven by offline measurements on 29 Sep traffic
11
+ (docs/research/jev/field-notes/2026-09-30-idea1-intent-skip.md and
12
+ 2026-09-30-idea4-notification-tiering.md, in the agent home). (1)
13
+ `src/monitoring/WorktreeMonitor.ts`: each notice kind (`session` = post-session
14
+ unmerged/orphan notice, `stale` = periodic stale-worktree notice) keeps a
15
+ fingerprint of the item set it last announced, persisted in
16
+ `state/worktree-monitor/announced.json`. A notice is sent only when the set
17
+ differs, or when `reminderIntervalMs` (default 24h) has passed. An empty set
18
+ clears the fingerprint, and a failed send is not recorded. (2)
19
+ `src/core/TopicIntentCapture.ts`: `captureTurn` returns `skipped-automation`
20
+ for `provenance === 'automation'` before any LLM call, counted under
21
+ `prefilter_skipped`. `src/commands/server.ts` forwards `entry.provenance` from
22
+ `onMessageLogged` into the loop. `src/scaffold/templates.ts` and
23
+ `src/core/PostUpdateMigrator.ts` add a short "Worktree Notices" CLAUDE.md
24
+ section (template, add-if-absent migration, and framework-shadow marker).
25
+
26
+ ## Decision-point inventory
27
+
28
+ - `WorktreeMonitor.onSessionComplete` / `periodicScan`: whether to send a notice — **modify**. It was "any finding → send". It is now "finding set changed, or 24h since last announcement → send".
29
+ - `captureTurn` pre-filter: whether a turn reaches the extractor LLM — **modify**. It adds a structural skip for `automation` provenance.
30
+ - `migrateClaudeMd`: whether to add the section — **add**. It is content-sniffed on the heading.
31
+
32
+ ---
33
+
34
+ ## 1. Over-block
35
+
36
+ - Worktree: a finding whose *details* change while the branch set does not (for example, more commits on the same unmerged branch) is not re-announced until the daily reminder. This is intentional. The branch was already flagged, and commit-count churn was part of the spam.
37
+ - Intent: an automated message that carries a real decision (the idea-1 note found a few, such as "framework switch deferred until the build is idle") is no longer extracted directly. Mitigation: TopicSummarizer builds the rolling summary from every message regardless of provenance (`getMessagesSinceSummary` has no provenance filter), and that summary is passed to the next conversational extraction as context. Justin's own messages are stamped `user` and are never skipped.
38
+
39
+ ---
40
+
41
+ ## 2. Under-block
42
+
43
+ - A set that flaps (a branch that keeps being created and deleted) re-announces on each change. That is correct, since each flap is a real change, but it is not rate-limited beyond that.
44
+ - Automated sends that reach the log without a provenance stamp (legacy rows, any path that does not stamp) are still extracted. This fails open on purpose.
45
+ - Other LLM consumers of the same messages (Usher, ArcCheck) are unchanged. They are outside this change.
46
+
47
+ ---
48
+
49
+ ## 3. Level-of-abstraction fit
50
+
51
+ The dedupe sits at the source, the monitor that decides to speak, rather than in NotificationBatcher, because only the monitor knows the item set. The intent skip uses the existing structural provenance authority, stamped at the send seam (`/telegram/reply` derives it from messageKind/isSystemTemplate/proxy, and `sendToTopic` defaults to `automation`), rather than re-deriving automation from text.
52
+
53
+ ---
54
+
55
+ ## 4. Signal vs authority compliance
56
+
57
+ **Required reference:** [docs/signal-vs-authority.md](../../docs/signal-vs-authority.md)
58
+
59
+ - [x] No — this change has no block/allow surface.
60
+
61
+ Neither change blocks a user or agent action. The monitor chooses whether to emit its own informational notice. The capture loop chooses whether to spend an LLM call on a best-effort background extraction, and it was already fail-open and never on the delivery path.
62
+
63
+ ---
64
+
65
+ ## 4b. Judgment-point check (Judgment Within Floors standard)
66
+
67
+ No new static heuristic at a competing-signals decision point. "Has the flagged set changed since the last announcement" is an enumerable set comparison. "Is this message automation-provenance" reads an existing structural stamp and does not weigh competing live signals.
68
+
69
+ ---
70
+
71
+ ## 5. Interactions
72
+
73
+ - **Shadowing:** the provenance skip runs before `isSubstantiveTurn`. Both skips count as `prefilter_skipped`, and the new status `skipped-automation` distinguishes them for callers. User turns still bump the turn counter first, because the provenance check comes after `bumpTurn`, and automation is never `fromUser`.
74
+ - **Double-fire:** the post-session and periodic notices keep separate fingerprints, as they did before. The dedupe cannot suppress one because of the other.
75
+ - **Races:** `onSessionComplete` and `periodicScan` can overlap. The worst case is one duplicate notice in the overlap window: both see "due" before either records. The state write is synchronous and last-writer-wins, and both write the same fingerprint.
76
+ - **Feedback loops:** this removes one. Worktree notices fed the extractor, which filed "unmerged branches" facts into the intent store on every notice.
77
+
78
+ ---
79
+
80
+ ## 6. External surfaces
81
+
82
+ - Telegram: far fewer worktree notices in the attention/system topic (replay of 29 Sep notices: 549 → 11).
83
+ - Persistent state: a new small file `state/worktree-monitor/announced.json`, holding at most two entries.
84
+ - CLAUDE.md: a new "Worktree Notices" section for new and existing agents, plus the Codex/Gemini shadow marker.
85
+ - Operator surface: no operator-facing actions.
86
+
87
+ ---
88
+
89
+ ## 6b. Operator-surface quality (Operator-Surface Quality standard)
90
+
91
+ No operator surface — not applicable.
92
+
93
+ ---
94
+
95
+ ## 7. Multi-machine posture (Cross-Machine Coherence)
96
+
97
+ **Machine-local by design.** The worktree monitor scans the local git checkout. Worktrees are per-machine filesystem state, so the announced-set memory belongs with the scan that produced it. Two machines with different worktrees correctly announce their own findings. This change does not alter the existing notice routing, including one-voice gating. It only reduces how often the notice fires. The intent skip is a pure function of each logged row, so its behaviour is identical on every machine.
98
+
99
+ ---
100
+
101
+ ## 8. Rollback cost
102
+
103
+ Pure code change. Revert and ship a patch. `announced.json` is ignored by older code and harmless to leave. After rollback, the old behaviour (a notice every session) returns immediately. The CLAUDE.md section would stay on migrated agents but describes behaviour that would no longer hold. A rollback should drop it with a follow-up migration.
104
+
105
+ ---
106
+
107
+ ## Conclusion
108
+
109
+ Both changes are deterministic and reduce volume. They were measured on real 29 Sep data before building. The review added two design points: a failed send is not recorded (otherwise one Telegram outage would suppress the finding for a day), and an empty set clears the fingerprint (so a finding that resolves and later returns is announced). Clear to ship.
110
+
111
+ ---
112
+
113
+ ## Second-pass review (if required)
114
+
115
+ Not required (Tier 1).
116
+
117
+ ---
118
+
119
+ ## Evidence pointers
120
+
121
+ - Unit: `tests/unit/worktree-monitor.test.ts` (announce on change only: 8 cases), `tests/unit/TopicIntentCapture.test.ts` (automation skipped; agent/user/unstamped captured), `tests/unit/PostUpdateMigrator-worktreeNotices.test.ts`.
122
+ - Integration: `tests/integration/worktree-monitor-announce-routes.test.ts`, `tests/integration/topic-intent-capture-automation-skip.test.ts`.
123
+ - E2E: `tests/e2e/worktree-monitor-announce-lifecycle.test.ts` (10 sessions → 1 notice; restart; changed set), `tests/e2e/topic-intent-capture-lifecycle.test.ts`.
124
+ - Wrong-side proof: with the new logic disabled, all new integration/e2e assertions fail (8 failures).
125
+ - Real-shape replay (read-only, live telegram-messages.jsonl, 29 Sep): 743 rows → 130 extractor calls; automation skipped (e.g. ids 115564, 115575, 115608), agent captured (115622, 115667), user captured (115774, 115822).
126
+
127
+ ---
128
+
129
+ ## Class-Closure Declaration (display-only mirror)
130
+
131
+ - **`defectClass`**: `unbounded-self-action`
132
+ - **`closure`**: `n/a`
133
+ - **Reason**: this change adds a settling brake to an existing self-triggered notify. It does not add a new action. Under a sustained, unchanged finding, the notice rate falls from one per session or scan (unbounded; 549 a day observed) to at most one per `reminderIntervalMs` (24h) per notice kind. A change in the set re-arms it once. `tests/e2e/worktree-monitor-announce-lifecycle.test.ts` proves 10 sessions → 1 notice.