instar 1.3.970 → 1.3.972
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/core/CartographerSweepEngine.js +1 -1
- package/dist/core/CartographerSweepEngine.js.map +1 -1
- package/dist/core/CartographerTree.d.ts +5 -2
- package/dist/core/CartographerTree.d.ts.map +1 -1
- package/dist/core/CartographerTree.js +6 -3
- package/dist/core/CartographerTree.js.map +1 -1
- package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
- package/dist/core/PostUpdateMigrator.js +27 -4
- package/dist/core/PostUpdateMigrator.js.map +1 -1
- package/dist/core/cartographerDetect.d.ts +1 -1
- package/dist/core/cartographerDetect.d.ts.map +1 -1
- package/dist/core/cartographerDetect.js +2 -2
- package/dist/core/cartographerDetect.js.map +1 -1
- package/dist/server/routes.js +1 -1
- package/dist/server/routes.js.map +1 -1
- package/package.json +1 -1
- package/scripts/cartographer-freshness.mjs +18 -3
- package/src/data/builtin-manifest.json +63 -63
- package/upgrades/1.3.971.md +57 -0
- package/upgrades/1.3.972.md +39 -0
- package/upgrades/side-effects/act966-observer-hook-revival.eli16.md +51 -0
- package/upgrades/side-effects/act966-observer-hook-revival.md +149 -0
- package/upgrades/side-effects/honest-denominators.md +135 -0
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# Upgrade Guide — vNEXT
|
|
2
|
+
|
|
3
|
+
<!-- assembled-by: assemble-next-md -->
|
|
4
|
+
<!-- bump: patch -->
|
|
5
|
+
|
|
6
|
+
## What Changed
|
|
7
|
+
|
|
8
|
+
The `completion-claim-observe` Stop hook — the observe-only arm of Verify-Before-Done — has never recorded a single hook-originated observation since it shipped. Two independent defects, each fatal alone, and both silent because the hook's `catch` exits 0: a total failure is indistinguishable from "nothing to report".
|
|
9
|
+
|
|
10
|
+
1. **`uuidv7()` threw on every invocation.** It sits at MODULE scope while `const crypto = await import('node:crypto')` lives inside the stdin `'end'` callback, so a bare `crypto` resolved to global WebCrypto — which has `getRandomValues` but not `randomBytes`. Because `uuidv7()` is called *inside the fetch body construction*, the throw happened while composing the request: the POST was never issued.
|
|
11
|
+
2. **The transcript guard hardcoded `~/.claude/projects`.** An agent running with a custom `CLAUDE_CONFIG_DIR` keeps transcripts under that directory instead, so every transcript failed containment and the hook exited before doing anything.
|
|
12
|
+
|
|
13
|
+
Fault 2 matters beyond itself: fixing only the reported cause (fault 1) would have shipped a **fix that changed nothing** on any agent with a custom config dir, while the ticket closed as done.
|
|
14
|
+
|
|
15
|
+
Fixes: `uuidv7()` now uses `globalThis.crypto.getRandomValues` with manual hex formatting — no import, identical under ESM and CJS, so the scope trap cannot return via a re-import (the 2026 `hook-event-reporter` ESM/CJS lesson). The transcript guard accepts the `CLAUDE_CONFIG_DIR`-derived projects root **and** the default one, so agents with and without the variable both work.
|
|
16
|
+
|
|
17
|
+
Built-in hooks are always overwritten on every migration run, so deployed agents receive this on their next update with no additional migration.
|
|
18
|
+
|
|
19
|
+
This unblocks EVO-005, whose parent spec requires measured soak data that could not exist while the observer was inert.
|
|
20
|
+
|
|
21
|
+
## What to Tell Your User
|
|
22
|
+
|
|
23
|
+
A background watcher that was supposed to notice completion claims has been dead since it shipped — silently, because it was designed to say nothing when there's nothing to report. It now works. No user action is needed; nothing was lost that can be recovered, since nothing was ever recorded.
|
|
24
|
+
|
|
25
|
+
## Summary of New Capabilities
|
|
26
|
+
|
|
27
|
+
No new capability. An existing observe-only watcher goes from 100% inert to functional, and two regression guards prevent both causes from returning.
|
|
28
|
+
|
|
29
|
+
## Evidence
|
|
30
|
+
|
|
31
|
+
- **Isolated A/B (noise-free):** pointing the hook at a controlled listener via a temp project dir, the pre-fix hook produced `[]` and the post-fix hook produced `[{"url":"/completion-claim/observe","bytes":623}]`.
|
|
32
|
+
- A first attempt counted records in the live audit store before/after, but the count moved on its own from concurrent background writes (6 → 8 between polls) and records are scrubbed of attribution — so that comparison could not prove anything and was replaced with the isolated listener rather than reported as proof.
|
|
33
|
+
- **Why the old hook could not post:** `uuidv7()` is invoked inside the `fetch` body argument, so the `TypeError` fired while constructing the request. Confirmed by character-offset ordering in the generated hook.
|
|
34
|
+
- **Scope bug reproduced:** the extracted `uuidv7`, executed 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`.
|
|
35
|
+
- **Path bug confirmed:** `CLAUDE_CONFIG_DIR` on the dev agent is `~/.claude-followme-adriana`; its transcripts live under that tree and never under `~/.claude/projects`.
|
|
36
|
+
- **Regression test bites:** against the pre-fix migrator, 4 of 6 tests fail with the production error; all 6 pass post-fix.
|
|
37
|
+
- `tsc --noEmit` clean.
|
|
38
|
+
|
|
39
|
+
Known and unchanged: the hook posts fire-and-forget (`void fetch(...)`) then exits, so an observation can occasionally be lost if the process exits before the request flushes. Pre-existing, unrelated to these two causes, and documented in the side-effects artifact rather than bundled in.
|
|
@@ -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,135 @@
|
|
|
1
|
+
# Side-Effects Review — honest denominators: a gate that discards its verdict, and a ratio that reports 1 over nothing
|
|
2
|
+
|
|
3
|
+
## Summary of the change
|
|
4
|
+
|
|
5
|
+
Two related honesty defects, both found by the convergence-towards-coherence audit (2026-07-25).
|
|
6
|
+
|
|
7
|
+
**A. `.husky/pre-commit` discarded its own blocking verdict.** The hook was a plain command
|
|
8
|
+
sequence with no `set -e`, so its exit status was the LAST command's.
|
|
9
|
+
`scripts/instar-dev-precommit.js` exits 1 and prints a full "commit BLOCKED" banner; three checks
|
|
10
|
+
then run after it, each exiting 0, so git saw 0 and accepted the commit. Verified by landing a
|
|
11
|
+
commit through a printed block (`b2efded2f`, reverted). Every blocking check in the hook except
|
|
12
|
+
the final one was advisory. Fix: `set -e`, with the reasoning recorded inline.
|
|
13
|
+
|
|
14
|
+
**B. `freshRatio` reported a perfect `1` when there was nothing to divide by.** Explicit in code
|
|
15
|
+
(`ratioDenom === 0 ? 1`), not an oversight. The CI ratchet compares `freshRatio < FLOOR`, so 1
|
|
16
|
+
passed every possible floor — the guard was structurally unable to fail on an empty map, the exact
|
|
17
|
+
case it exists to catch. Fix: all five producers return `null` with no denominator; the ratchet
|
|
18
|
+
distinguishes *no authored state* (legitimate CI case → still exit 0, but prints
|
|
19
|
+
`NOT ASSESSED — this check gated on nothing`) from *an authored index with zero nodes*
|
|
20
|
+
(→ hard failure).
|
|
21
|
+
|
|
22
|
+
## Decision-point inventory
|
|
23
|
+
|
|
24
|
+
- `.husky/pre-commit` — a commit-time authority. Change makes its existing verdict effective; it
|
|
25
|
+
adds no new verdict of its own.
|
|
26
|
+
- `scripts/cartographer-freshness.mjs --check` — a CI-time authority. Change adds one failure
|
|
27
|
+
condition (authored index with zero nodes) and one explicit non-assessment notice.
|
|
28
|
+
- The five `freshRatio` producers are pure reporters; no decision logic changed.
|
|
29
|
+
|
|
30
|
+
## 1. Over-block
|
|
31
|
+
|
|
32
|
+
**A.** `set -e` means the FIRST failing check aborts, so later checks do not run and a developer
|
|
33
|
+
sees one problem at a time rather than all of them. Accepted: that is standard hook behaviour and
|
|
34
|
+
strictly better than the previous state (no check could block at all). Every command in the
|
|
35
|
+
sequence is intended to block; the only conditional (`if node -e … fi`) is a shell condition,
|
|
36
|
+
which `set -e` deliberately does not trip on.
|
|
37
|
+
|
|
38
|
+
**B.** The narrow risk was failing a legitimately-not-yet-assessable index. Caught during build:
|
|
39
|
+
my first version failed a freshly-scaffolded tree whose nodes are all within grace, breaking two
|
|
40
|
+
pre-existing tests. Narrowed to `nodeCount === 0`. A scaffold within grace is a pass, and the
|
|
41
|
+
existing `neverAuthoredPastGrace` ceiling still guards the backlog case.
|
|
42
|
+
|
|
43
|
+
## 2. Under-block
|
|
44
|
+
|
|
45
|
+
**A.** `--no-verify` still bypasses everything — unchanged and out of scope; the skill already
|
|
46
|
+
names bypass as an anti-pattern. Server-side CI remains the backstop.
|
|
47
|
+
|
|
48
|
+
**B.** A *populated but stale* map is still governed only by the configured floor, which ships at
|
|
49
|
+
`0` by default and therefore still gates weakly. This change does not raise that floor — doing so
|
|
50
|
+
is a separate judgement about desired strictness, and bundling it would hide which change caused
|
|
51
|
+
which effect. Named here rather than silently left.
|
|
52
|
+
|
|
53
|
+
## 3. Level-of-abstraction fit
|
|
54
|
+
|
|
55
|
+
Both fixes are at the layer that already owns the decision. **A** repairs how an existing verdict
|
|
56
|
+
is propagated — it does not move authority. **B** repairs what a reporter reports and how its
|
|
57
|
+
existing consumer reads "no evidence". Neither adds a parallel checker beside a smarter one.
|
|
58
|
+
|
|
59
|
+
## 4. Signal vs authority compliance
|
|
60
|
+
|
|
61
|
+
Compliant, and the change moves *toward* the principle.
|
|
62
|
+
|
|
63
|
+
- **A** adds no new authority. The authority (`instar-dev-precommit.js`) already existed and
|
|
64
|
+
already computed the right answer; the defect was that its signal was discarded. Making an
|
|
65
|
+
existing verdict effective is the opposite of granting brittle logic new blocking power.
|
|
66
|
+
- **B** the producers remain pure signals (`number | null`). The one authority (the CI ratchet)
|
|
67
|
+
gains a failure condition that is *deterministic and evidence-based* — it fires only on the
|
|
68
|
+
objective state "an index exists and contains zero nodes". Crucially it fails toward "cannot
|
|
69
|
+
assess", never toward a fabricated pass, which is the direction the principle asks for.
|
|
70
|
+
|
|
71
|
+
## 4b. Judgment-point check
|
|
72
|
+
|
|
73
|
+
No LLM judgement involved. Both are deterministic checks over objective state (exit codes; node
|
|
74
|
+
counts). No string-matching heuristic gains authority.
|
|
75
|
+
|
|
76
|
+
## 5. Interactions
|
|
77
|
+
|
|
78
|
+
- **A** interacts with every check in the hook: `npm run lint`,
|
|
79
|
+
`lint-migration-consumer-completeness`, `instar-dev-precommit`, `check-rule3-coverage`,
|
|
80
|
+
`protect-migration-guarantee`, `check-e2e-pairing`. All six become genuinely blocking. Verified
|
|
81
|
+
each currently exits 0 on a clean tree with real dependencies installed, so this does not turn a
|
|
82
|
+
currently-green repo red. **This was checked only after a false alarm**: I first measured a lint
|
|
83
|
+
failure and reported that the fix would block every developer — that failure was an artifact of
|
|
84
|
+
my own worktree pointing at an 85-commit-stale `node_modules` that predated the `undici`
|
|
85
|
+
dependency. With a real `npm ci`, lint exits 0. The false alarm is recorded because the
|
|
86
|
+
generalisation-from-local-artifact is the more instructive error.
|
|
87
|
+
- **B** does not shadow the `neverAuthoredPastGrace` or `authorFailed` ceilings; they are
|
|
88
|
+
evaluated independently and unchanged.
|
|
89
|
+
|
|
90
|
+
## 6. External surfaces
|
|
91
|
+
|
|
92
|
+
`GET /cartographer/health` now returns `freshness.freshRatio: null` instead of `1` in the
|
|
93
|
+
no-data case. Any external consumer doing arithmetic on it must handle null. Type-checked across
|
|
94
|
+
the tree: zero errors, because the only pipeline consumer is the `.mjs` ratchet, which is updated
|
|
95
|
+
here. The dashboard renders the freshness block; `null` displays as absent rather than as a
|
|
96
|
+
fabricated 100%, which is the intended user-visible effect.
|
|
97
|
+
|
|
98
|
+
## 6b. Operator-surface quality
|
|
99
|
+
|
|
100
|
+
The ratchet's new no-assessment line is plain English and states the consequence:
|
|
101
|
+
`NOT ASSESSED — no authored cartographer state; this check gated on nothing.` The failure line
|
|
102
|
+
says what could not be done and explicitly that it is not a pass, rather than emitting a bare
|
|
103
|
+
code. No file paths or config syntax are surfaced to a user; both lines are developer/CI-facing.
|
|
104
|
+
|
|
105
|
+
## 7. Multi-machine posture (Cross-Machine Coherence)
|
|
106
|
+
|
|
107
|
+
**Machine-local by design, both.** `.husky/pre-commit` is a per-checkout git hook — there is no
|
|
108
|
+
cross-machine surface and no replication path is meaningful. The cartographer index is per-machine
|
|
109
|
+
state and its freshness is a property of that machine's checkout; `GET /cartographer/health` is
|
|
110
|
+
already a machine-local read. No durable state strands on topic transfer, no generated URL crosses
|
|
111
|
+
a machine boundary, and no user-facing notice is emitted that would need one-voice gating.
|
|
112
|
+
|
|
113
|
+
## 8. Rollback cost
|
|
114
|
+
|
|
115
|
+
Cheap and total, for both. **A**: delete one line (`set -e`) — restores the previous behaviour
|
|
116
|
+
exactly. **B**: revert the commit; the producers return to `1` and the ratchet to its prior
|
|
117
|
+
comparison. No data migration, no agent state repair, no released artifact depends on the shape.
|
|
118
|
+
The `null` value is additive to a type, not a schema change with stored records behind it.
|
|
119
|
+
|
|
120
|
+
## Conclusion
|
|
121
|
+
|
|
122
|
+
Both changes make an existing mechanism do what it already claimed to do. Neither adds authority;
|
|
123
|
+
one restores authority that was being silently discarded, the other stops a reporter from
|
|
124
|
+
manufacturing a passing grade out of an absence of evidence.
|
|
125
|
+
|
|
126
|
+
The honest note for reviewers: I got the second one wrong twice during the build (over-broad
|
|
127
|
+
failure condition caught by pre-existing tests; a false blast-radius claim caught by checking my
|
|
128
|
+
own environment), and one existing E2E assertion had to be corrected because it was protecting
|
|
129
|
+
the old behaviour. That history is in the commit message and the test comments so it cannot be
|
|
130
|
+
quietly reverted.
|
|
131
|
+
|
|
132
|
+
**Second-pass review: not required.** Per the skill's Phase 5 list, this touches no runtime
|
|
133
|
+
agent-behaviour gate — no outbound/inbound message block/allow, no session lifecycle, no
|
|
134
|
+
compaction/respawn, no coherence gate or trust level. It touches a git hook and a CI script, both
|
|
135
|
+
build-time. The PR is the review surface, per Tier 1.
|