instar 1.3.971 → 1.3.973

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.
@@ -0,0 +1,51 @@
1
+ # Reviving the observer that has never once observed — Plain-English Overview
2
+
3
+ > The one-line version: a watcher that was supposed to notice when I claim something is "done" has been completely dead since the day it shipped, and it failed in the one way nobody notices — quietly.
4
+
5
+ ## The problem in one breath
6
+
7
+ There's a small watcher that runs after each of my replies. Its job is to notice when I claim I've finished or fixed something, and to record that claim so it can later be checked against what I actually did. It has **never recorded a single thing**.
8
+
9
+ Not "recorded a few and stopped". Never. Not once. And because the watcher is designed to be silent when there's nothing to report, a watcher that's totally broken looks exactly like a watcher that's working fine on a quiet day.
10
+
11
+ ## What already exists
12
+
13
+ - **The watcher** — runs after each reply, reads a bounded slice of the conversation locally, and sends a small summary of the *shape* of the reply (never the text, never commands, never file paths).
14
+ - **The analysis behind it** — the part that would compare "I said I fixed it" against evidence. It works; it has simply never had any input from the watcher.
15
+ - **The update mechanism** — this watcher is regenerated from scratch on every agent update, so fixing the template fixes it everywhere automatically.
16
+
17
+ ## What this adds
18
+
19
+ Two separate faults, each fatal on its own:
20
+
21
+ **Fault one: it crashed while addressing the envelope.** The watcher builds an ID for each observation. That ID-builder reached for a tool that wasn't available where it was standing — a scoping mistake, where the name it used pointed at a similar-but-different object with no such function. The killer detail: the ID is generated *inside the act of composing the message it was about to send*. So it crashed mid-compose, and the message was never sent at all. Then its safety net caught the error and exited quietly, reporting success.
22
+
23
+ **Fault two: it was looking in the wrong filing cabinet.** The watcher only reads conversation files from one specific folder, for safety. But this agent stores its conversations in a *differently named* folder — a supported setup that the watcher didn't know about. So even with fault one fixed, every single conversation would have been turned away at the door, silently.
24
+
25
+ That second one matters more than it sounds: **fixing only the reported bug would have produced a fix that changed nothing**, while the ticket got closed as done. That's the exact trap this family of bugs keeps setting.
26
+
27
+ ## The safeguards
28
+
29
+ **The fix was tested by watching, not by hoping.** I pointed the watcher at a mailbox I controlled instead of the real one, so nothing else could muddy the result. The old version delivered **nothing**. The new version delivered exactly one message. That's the difference between "the code looks right" and "the thing arrives".
30
+
31
+ **Why a controlled mailbox at all:** my first attempt counted records in the real system before and after. The count moved on its own between checks — other activity was writing at the same time — so the numbers couldn't prove anything either way. Rather than read a comforting number and call it proof, I isolated the test.
32
+
33
+ **The scoping trap can't come back through a side door.** The ID-builder now uses a tool that's always available and behaves identically regardless of how the file is loaded. An earlier incident in this codebase involved exactly this kind of load-style mismatch breaking a different hook, so the fix avoids reintroducing that dependency at all.
34
+
35
+ **The tests run the real thing.** Rather than checking whether the code *mentions* the right function, the test pulls the actual generated ID-builder out of the actual generated watcher and *runs* it — in both loading styles. Pointed at the old version, it fails with the exact original error.
36
+
37
+ ## What ships when
38
+
39
+ All together: both fixes plus the tests that keep them fixed.
40
+
41
+ ## What this unblocks
42
+
43
+ A previously approved improvement was stuck waiting on real measurements from this watcher — measurements that couldn't exist while it was dead. Those can now start accumulating.
44
+
45
+ ## One thing I did not fix
46
+
47
+ The watcher sends its message and exits immediately without waiting for delivery confirmation. If it exits fast enough, an occasional observation can be lost. That's how it was already built, it's unrelated to these two faults, and changing when a watcher is allowed to exit is its own decision with its own risks. I've written it down rather than quietly bundling it in.
48
+
49
+ ## If it goes wrong
50
+
51
+ Undo it; agents pick up the reversal on their next update. Nothing stored, nothing migrated. Worst case is a return to a silent watcher that does nothing — which is exactly today.
@@ -0,0 +1,149 @@
1
+ # Side-Effects Review — Verify-Before-Done observer hook was 100% inert (ACT-966)
2
+
3
+ **Version / slug:** `act966-observer-hook-revival`
4
+ **Date:** `2026-07-25`
5
+ **Author:** `Echo (instar-dev agent)`
6
+ **Second-pass reviewer:** `not required`
7
+
8
+ ## Summary of the change
9
+
10
+ The `completion-claim-observe` Stop hook has never recorded a single hook-originated observation. Two independent defects, each sufficient on its own to kill it, and both silent by construction — the hook's `catch` exits 0, so a total failure is indistinguishable from "nothing to report".
11
+
12
+ 1. **`uuidv7()` threw on every invocation.** The function sits at MODULE scope while `const crypto = await import('node:crypto')` lives inside the stdin `'end'` callback, so a bare `crypto` resolved to the global WebCrypto object — which has `getRandomValues` but not `randomBytes`. Critically, `uuidv7()` is called *inside the fetch body construction*, so the throw happened **while building the POST**: the request was never sent.
13
+ 2. **The transcript guard hardcoded `~/.claude/projects`.** An agent running with a custom `CLAUDE_CONFIG_DIR` (this one uses `~/.claude-followme-adriana`) keeps transcripts under *that* directory, so every transcript failed the containment check and the hook exited 0 before doing anything.
14
+
15
+ Fixes: `uuidv7()` now uses `globalThis.crypto.getRandomValues` (no import, identical under ESM and CJS) with manual hex formatting; the transcript guard accepts the `CLAUDE_CONFIG_DIR`-derived projects root **and** the default one.
16
+
17
+ This unblocks EVO-005, whose parent spec requires measured soak data that could not exist while the observer was inert.
18
+
19
+ ## Decision-point inventory
20
+
21
+ - `completion-claim-observe` Stop hook — **modify** — signal-only observer. It has no authority: it never blocks, delays, or rewrites a turn, and every path ends in `process.exit(0)`.
22
+ - Transcript containment guard — **modify** — a safety boundary on what the hook may READ. Widened from one Claude projects root to two, both still Claude projects trees.
23
+ - `tests/unit/completion-claim-observe-uuidv7.test.ts` — **add** — CI regression test, build-time only.
24
+
25
+ ---
26
+
27
+ ## 1. Over-block
28
+
29
+ The containment guard is the only rejecting surface, and this change makes it reject **less**: it previously rejected every transcript on any agent with a custom config dir — a 100% false-reject that was the entire second defect.
30
+
31
+ Residual over-block: an agent whose transcripts live somewhere that is neither `$CLAUDE_CONFIG_DIR/projects` nor `~/.claude/projects` is still rejected. That is intended containment, not an accident.
32
+
33
+ ---
34
+
35
+ ## 2. Under-block
36
+
37
+ This is the important direction, since the guard governs what the hook may read.
38
+
39
+ - **The guard is widened, so it accepts more paths than before.** Concretely it now also accepts anything under `$CLAUDE_CONFIG_DIR/projects`. `CLAUDE_CONFIG_DIR` is set by the Claude Code host, not by transcript content or hook input, so this is not attacker-controlled from the message side. An actor who can already set that env var for the hook process can run arbitrary code as that process anyway — so it grants no new capability.
40
+ - Both roots are `path.resolve`d and matched with an explicit `root + path.sep` prefix check, so `~/.claude/projects-evil` does not satisfy the `~/.claude/projects` root. Traversal (`../`) is normalised by `resolve` before comparison.
41
+ - Unchanged and still true: the hook sends **structural metadata only** — never the transcript path, commands, tool results, or raw inputs. Widening which transcript it may read does not widen what it transmits.
42
+ - Still missed: the hook does not verify the transcript belongs to the *current* session, only that it sits in a Claude projects tree. Pre-existing, unchanged by this fix.
43
+
44
+ ---
45
+
46
+ ## 3. Level-of-abstraction fit
47
+
48
+ Both fixes sit at the defect's own level — a scope bug fixed in the function that had it, and a path assumption fixed in the guard that made it. Neither is worked around at a higher layer.
49
+
50
+ `getRandomValues` over re-importing `node:crypto` inside `uuidv7()` is deliberate: an import inside the function would work, but it re-creates the ESM/CJS coupling that the 2026 `hook-event-reporter` incident already burned us on (a CJS-only hook broke on an ESM host). The global needs no import and behaves identically under both, so the trap cannot return by a different door.
51
+
52
+ ---
53
+
54
+ ## 4. Signal vs authority compliance
55
+
56
+ **Required reference:** [docs/signal-vs-authority.md](../../docs/signal-vs-authority.md)
57
+
58
+ - [x] No — this change produces a signal consumed by an existing smart gate.
59
+
60
+ The hook is the observe-only arm of Verify-Before-Done. It records observations for later analysis and holds no blocking authority; the change restores its ability to emit a signal, and grants it nothing else. The added test is CI-only.
61
+
62
+ ---
63
+
64
+ ## 4b. Judgment-point check (Judgment Within Floors standard)
65
+
66
+ **No new static heuristic at a competing-signals decision point.** "Is this path inside an allowed root?" is a containment invariant over a finite enumerated set of roots — deterministic by design, and a safety guard on a read boundary. No competing live signals are weighed.
67
+
68
+ ---
69
+
70
+ ## 5. Interactions
71
+
72
+ - **Shadowing:** none. The hook is one of several Stop hooks and does not gate the others; it exits 0 unconditionally.
73
+ - **Double-fire:** the hook may now post where it previously always failed, so observation volume goes from zero to non-zero. The server-side pass is already bounded (a single evaluation per authored response) and ships in dryRun, so this is the intended volume, not a duplicate.
74
+ - **Races:** the POST is fire-and-forget (`void fetch(...)`) followed by `process.exit(0)`. If the process exits before the request flushes, the observation is lost. **Pre-existing and unchanged** — observed during verification when a piped invocation terminated early. Not addressed here because changing exit semantics of a Stop hook is a separate behavioural change with its own review surface; noted so it is not mistaken for a new defect.
75
+ - **Feedback loops:** observations feed the Verify-Before-Done analysis, which is exactly what EVO-005 needs. Nothing feeds back into the hook.
76
+ - **Migration parity:** built-in hooks under `.instar/hooks/instar/` are **always overwritten** on every migration run (`PostUpdateMigrator` writes this file unconditionally), so deployed agents receive the fix on their next update with no additional migration. Verified at the writing callsite.
77
+
78
+ ---
79
+
80
+ ## 6. External surfaces
81
+
82
+ - **Other users of the install base:** every agent with the feature enabled starts producing observations where it produced none. Volume is bounded per authored response and the pipeline ships in dryRun.
83
+ - **External systems:** none — the POST is to `127.0.0.1`.
84
+ - **Persistent state:** observation records begin accumulating in the existing audit store. That store already existed and already had records from direct API calls; this only restores the hook-originated path.
85
+ - **Privacy:** unchanged. Structural metadata only; no transcript content, path, or command text is transmitted. Verified by reading the payload construction.
86
+ - **Operator surface:** none added or touched.
87
+
88
+ ---
89
+
90
+ ## 6b. Operator-surface quality (Operator-Surface Quality standard)
91
+
92
+ **No operator surface — not applicable.** No dashboard renderer, approval page, or grant/secret form is touched.
93
+
94
+ ---
95
+
96
+ ## 7. Multi-machine posture (Cross-Machine Coherence)
97
+
98
+ **Machine-local BY DESIGN.** The hook observes the turns of the session running on *this* machine, reading that machine's transcript files and posting to that machine's own `127.0.0.1` server. Transcripts are inherently machine-local artifacts, and `CLAUDE_CONFIG_DIR` is a per-machine environment fact — the whole point of fix #2 is that this path legitimately differs per machine. Replicating observations would be wrong at the emit layer; any pool-wide view belongs to the analysis surface, not the hook.
99
+
100
+ - **User-facing notices:** none. The hook is silent by design.
101
+ - **Durable state on topic transfer:** none held by the hook.
102
+ - **Generated URLs:** none.
103
+
104
+ ---
105
+
106
+ ## 8. Rollback cost
107
+
108
+ - **Hot-fix release:** revert; agents pick up the reverted hook on their next update via the always-overwrite path.
109
+ - **Data migration:** none. Observation records already accumulate from other paths.
110
+ - **Agent state repair:** none.
111
+ - **User visibility during rollback:** reverting restores silent inertness — no crash, no user-visible regression. Same benign asymmetry as the other fixes in this family.
112
+
113
+ ---
114
+
115
+ ## Conclusion
116
+
117
+ The review's substantive finding is that fixing the reported cause alone would have produced a **false fix**. ACT-966 named the `uuidv7` scope bug, and that diagnosis was correct — but on this very agent the transcript guard would still have rejected every transcript, so the observer would have stayed at zero observations while the ticket read "fixed". That is the exact proxy-signal failure the EVO-004/005/006 family exists to stop, and it was caught only by running the real hook against a real transcript instead of trusting the ticket.
118
+
119
+ Verification was deliberately made noise-free: pointing the hook at a listener under my control (via a temp project dir whose `config.json` names that port) isolated the test from concurrent background writes to the live audit store, which had been moving on their own and made a naive before/after count meaningless. Against that listener the old hook produces **zero** requests and the new one produces exactly one 623-byte POST to `/completion-claim/observe`.
120
+
121
+ The change is clear to ship. One honest limit: that A/B demonstrates old-fails/new-works but does not isolate the two fixes from each other, because the transcript used only passes the new path guard. They were verified independently — the scope bug by executing the extracted function at module scope under both ESM and CJS, the path bug by confirming the live `CLAUDE_CONFIG_DIR` value and the actual transcript locations.
122
+
123
+ ---
124
+
125
+ ## Second-pass review (if required)
126
+
127
+ **Reviewer:** not required.
128
+
129
+ Phase 5 triggers on block/allow decisions over messaging or dispatch, session lifecycle, context/compaction, coherence gates, trust levels, or sentinel/guard/watchdog components. This hook is an observe-only recorder that cannot block, delay, or alter a turn. It does touch a containment *guard*, but that guard governs which local file the hook may read, not any agent action — and the change to it is a widening from one Claude projects root to two, analysed in §2.
130
+
131
+ ---
132
+
133
+ ## Evidence pointers
134
+
135
+ - **Isolated A/B (noise-free):** with the hook pointed at a controlled listener, the pre-fix hook produced `[]` and the post-fix hook produced `[{"url":"/completion-claim/observe","bytes":623}]`.
136
+ - **Why the old hook could not post:** `uuidv7()` is invoked inside the `fetch` body argument, so the `TypeError` fired while constructing the request — the POST was never issued. Confirmed by character-offset ordering in the generated hook.
137
+ - **Scope bug reproduced:** the extracted `uuidv7` run at module scope in a real node process throws `TypeError: crypto.randomBytes is not a function` pre-fix; post-fix it returns a valid UUIDv7 (version nibble `7`, variant `8|9|a|b`, unique, time-ordered) under both `.js` and `.mjs`.
138
+ - **Path bug confirmed:** `CLAUDE_CONFIG_DIR=/Users/justin_instar_1/.claude-followme-adriana`; this agent's transcripts live under that tree, never under `~/.claude/projects`.
139
+ - **Regression test bites:** run against the pre-fix migrator, 4 of 6 tests fail with the production error; all 6 pass post-fix.
140
+ - `tsc --noEmit` clean.
141
+
142
+ ---
143
+
144
+ ## Class-Closure Declaration (display-only mirror)
145
+
146
+ - **`defectClass`** — `proxy-signal-substitution` (same family as PRs #1636/#1637): "the hook is installed and exits 0" was treated as evidence the observer worked, when the terminal state is "an observation was recorded".
147
+ - **`closure`** — `guard`.
148
+ - **`guardEvidence`** — `{enforcementType: ratchet, citation: tests/unit/completion-claim-observe-uuidv7.test.ts#"produces a valid UUIDv7 when run as a CJS file" + #"honours CLAUDE_CONFIG_DIR when confining transcript reads", howCaught: the test extracts uuidv7 from the REAL generated hook and executes it at module scope in a separate node process under both ESM and CJS, reproducing the exact TypeError pre-fix, and asserts the transcript guard consults CLAUDE_CONFIG_DIR while retaining the default root — so neither cause can silently return}`.
149
+ - **`gap`** — none for these two causes. The fire-and-forget POST race noted in §5 is pre-existing and out of scope; it is described there rather than claimed as closed.
@@ -0,0 +1,95 @@
1
+ # Side-Effects Review — resolve the GitHub CLI by path, so a project item can be recorded as merged
2
+
3
+ ## Summary of the change
4
+
5
+ The server is launched by launchd with a minimal PATH that does not include
6
+ `/opt/homebrew/bin`. Every in-process `execFileSync('gh', …)` therefore died with a raw
7
+ `spawnSync gh ENOENT`. The visible consequence: `building → merged` on a project item could
8
+ never succeed on this install, because the stage gate correctly refuses to take the caller's word
9
+ and verifies the PR with `gh` — a missing binary read as "cannot verify", so the item stayed open.
10
+
11
+ Adds `src/core/resolveGhBinary.ts` (override → cached → common absolute locations → `which`),
12
+ mirroring the established in-repo `BitwardenProvider.findBw` pattern, and uses it at the
13
+ `ghPrView` callsite. A missing binary now raises a NAMED diagnostic naming the override env var,
14
+ instead of an opaque spawn error.
15
+
16
+ ## Decision-point inventory
17
+
18
+ None added. `resolveGhBinary()` returns a path or null and makes no decision about whether an
19
+ operation may proceed. The existing authority (`StageTransitionValidator`) keeps its judgement
20
+ unchanged — it still refuses the transition when the PR cannot be verified.
21
+
22
+ ## 1. Over-block
23
+
24
+ Nothing newly blocked. The transition already failed in this situation; it now fails with a
25
+ useful reason, or succeeds where it previously could not. The resolver cannot cause a refusal that
26
+ was not already happening.
27
+
28
+ ## 2. Under-block
29
+
30
+ The gate still trusts whatever `gh` it resolves. A hostile `gh` earlier on the candidate list, or
31
+ a malicious `INSTAR_GH_PATH`, would be trusted — unchanged from the previous behaviour, which
32
+ trusted whatever `gh` PATH produced. Named rather than silently accepted; not addressed here
33
+ because it is the pre-existing trust model for every CLI this repo shells out to.
34
+
35
+ ## 3. Level-of-abstraction fit
36
+
37
+ Correct layer. Binary resolution belongs beside the callsite that shells out, not inside the
38
+ validator (which should stay a pure decision function) and not in process startup (which cannot
39
+ know which binaries a given route will need). The repo already solves this exact problem this
40
+ exact way for `bw`.
41
+
42
+ ## 4. Signal vs authority compliance
43
+
44
+ Compliant. The resolver is a pure signal — a path or null. It holds no blocking authority and
45
+ cannot pass anything. The one behavioural change at the authority is that "cannot verify" now
46
+ carries a named reason; the refusal itself is unchanged, and it still fails toward refusing rather
47
+ than assuming a merge happened.
48
+
49
+ ## 4b. Judgment-point check
50
+
51
+ No LLM judgement. Deterministic filesystem existence checks.
52
+
53
+ ## 5. Interactions
54
+
55
+ Scoped to the `ghPrView` callsite. Thirteen other `gh` invocations exist across the tree; most run
56
+ in CLI/script contexts that inherit a normal user PATH and are not known to be broken. They are
57
+ NOT swept here — a blind sweep would widen blast radius past what is verified. Tracked for
58
+ assessment under ACT-1269 <!-- tracked: ACT-1269 -->.
59
+
60
+ ## 6. External surfaces
61
+
62
+ No API shape change. The failure message on an unverifiable merge becomes human-readable and names
63
+ the override variable. No new external calls: `gh pr view` was already being invoked.
64
+
65
+ ## 6b. Operator-surface quality
66
+
67
+ The new message says what could not be done, why, and the one lever that fixes it, in plain
68
+ English — and states that the transition is refused rather than assumed, so the reader knows the
69
+ system did not guess.
70
+
71
+ ## 7. Multi-machine posture (Cross-Machine Coherence)
72
+
73
+ **Machine-local by design.** Binary location is a property of one machine's filesystem; there is
74
+ nothing to replicate and a peer's answer would be actively wrong. `INSTAR_GH_PATH` is per-machine
75
+ by nature. No durable state, no generated URL, no user-facing notice, so no one-voice gating and
76
+ nothing to strand on a topic transfer.
77
+
78
+ ## 8. Rollback cost
79
+
80
+ Revert the commit. The resolver is additive and its only consumer is the one callsite; reverting
81
+ restores the previous `execFileSync('gh', …)` exactly. No data, no migration, no stored state.
82
+
83
+ ## Conclusion
84
+
85
+ Restores a transition that has been unreachable on this install. Worth recording that this is the
86
+ SECOND time `building → merged` has been impossible for an environmental reason — the #866 note at
87
+ the same callsite records the 2026-06-06 instance, where the helper simply was not injected. Same
88
+ symptom, different cause, which is why this fix targets resolution rather than that one callsite.
89
+
90
+ Proof, under the exact minimal PATH that caused the failure (`PATH=/usr/bin:/bin`):
91
+ bare `gh` → `ENOENT (the old failure)`; `resolveGhBinary()` → `/opt/homebrew/bin/gh`.
92
+
93
+ **Second-pass review: not required.** No runtime agent-behaviour gate is touched — no message
94
+ block/allow, no session lifecycle, no compaction, no coherence gate or trust level. The PR is the
95
+ review surface per Tier 1.