agentwrangler 0.1.1-next.bca85ea → 0.1.1-next.fbbbe7f

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 (90) hide show
  1. package/README.md +162 -82
  2. package/dist/apply/jobs.js +3 -13
  3. package/dist/daemon/http.js +0 -2
  4. package/dist/daemon/index.js +6 -8
  5. package/dist/daemon/router.js +6 -63
  6. package/dist/detector/context-history-retention.js +0 -49
  7. package/dist/detector/detectors/d10_catalog_footprint.js +2 -2
  8. package/dist/detector/detectors/d1_ctx_always_loaded.js +9 -9
  9. package/dist/detector/detectors/d2_session_long_full_context.js +2 -2
  10. package/dist/detector/detectors/d4_model_mismatch.js +3 -3
  11. package/dist/detector/detectors/d6_tool_result_bloat.js +1 -1
  12. package/dist/detector/detectors/d7_loop_retry_waste.js +1 -1
  13. package/dist/detector/detectors/d8_cache_write_churn.js +2 -2
  14. package/dist/detector/detectors/d9_idle_background_session.js +1 -1
  15. package/dist/detector/measurement.js +3 -14
  16. package/dist/hook/precompact-checkpoint-hook.mjs +13 -26
  17. package/dist/ingest/pricing.js +0 -15
  18. package/dist/query/api/cost-per-success.js +1 -13
  19. package/dist/query/api/delivery.js +3 -25
  20. package/dist/query/api/effectiveness.js +5 -30
  21. package/dist/query/api/efficiency-headroom.js +14 -12
  22. package/dist/query/api/outcomes.js +1 -2
  23. package/dist/query/api/overview.js +6 -90
  24. package/dist/query/api/recommendations-ledger.js +14 -9
  25. package/dist/query/api/recommendations.js +1 -21
  26. package/dist/query/settings-store.js +0 -4
  27. package/dist/query/spend.js +2 -64
  28. package/dist/ui/assets/{BarChart-CIjCNfbI.js → BarChart-p1usctxJ.js} +3 -3
  29. package/dist/ui/assets/BriefsPage-CneqLVGC.js +2 -0
  30. package/dist/ui/assets/CacheWriteSpikesChart-zNRZbzy9.js +1 -0
  31. package/dist/ui/assets/Chip-Bu6udQGq.js +1 -0
  32. package/dist/ui/assets/ComposedChart-8D5E-Po_.js +1 -0
  33. package/dist/ui/assets/{EmptyState-B8vpug3_.js → EmptyState-CvQeJjjG.js} +1 -1
  34. package/dist/ui/assets/FlavorDecomposition-xcMLA50f.js +1 -0
  35. package/dist/ui/assets/FrictionCell-OafGLGxT.js +2 -0
  36. package/dist/ui/assets/GlossaryPage-BmOP3Syj.js +1 -0
  37. package/dist/ui/assets/HotSessionsPage-FoxuuDLF.js +1 -0
  38. package/dist/ui/assets/{InfoTip-CL8RC4vz.js → InfoTip-Dx7BHpBu.js} +1 -1
  39. package/dist/ui/assets/{Legend-CB7sqqJH.js → Legend--q6-rQGk.js} +3 -3
  40. package/dist/ui/assets/{Line-B9XY30uT.js → Line-DzVtUwF4.js} +1 -1
  41. package/dist/ui/assets/OverviewPage-BjSQYNvS.js +2 -0
  42. package/dist/ui/assets/RecommendationsPage-CRTpzcHs.js +2 -0
  43. package/dist/ui/assets/{Scatter-BSWm_yIZ.js → Scatter-Cr_hCkjG.js} +1 -1
  44. package/dist/ui/assets/SessionDetailPage-Dd3u8kVI.js +1 -0
  45. package/dist/ui/assets/SettingsPage-BPVQyjlc.js +25 -0
  46. package/dist/ui/assets/{Skeleton-Dp_m79B8.js → Skeleton-CcD61x7q.js} +1 -1
  47. package/dist/ui/assets/SpendPercentileChip-BaQhJQAn.js +1 -0
  48. package/dist/ui/assets/TrendChart-Baz3GGgE.js +1 -0
  49. package/dist/ui/assets/WorkspaceDetailPage-yk4z_EtQ.js +1 -0
  50. package/dist/ui/assets/WorkspacesPage-BDtclXJP.js +1 -0
  51. package/dist/ui/assets/{CartesianChart-DEnDOGKj.js → chart-theme-JH5aQymO.js} +36 -36
  52. package/dist/ui/assets/graphicalItemSelectors-CCbl8AKC.js +1 -0
  53. package/dist/ui/assets/{index-DhFiA8Ch.js → index-DRhoA51c.js} +10 -10
  54. package/dist/ui/assets/{index-D_IJsvkJ.css → index-SrfNUeBZ.css} +1 -1
  55. package/dist/ui/assets/{prompt-templates-CVzMex9L.js → prompt-templates-DhR4Qoy9.js} +4 -4
  56. package/dist/ui/assets/{useExperimentalActions-aI00AFZv.js → useExperimentalActions-BPDdswCN.js} +1 -1
  57. package/dist/ui/index.html +2 -2
  58. package/package.json +1 -1
  59. package/dist/daemon/effect-pass.js +0 -24
  60. package/dist/db/migrations/018_effect_cycles.sql +0 -109
  61. package/dist/effects/api-contract.js +0 -1
  62. package/dist/effects/engine.js +0 -574
  63. package/dist/effects/gate.js +0 -2
  64. package/dist/effects/index.js +0 -5
  65. package/dist/effects/legacy.js +0 -28
  66. package/dist/effects/registry.js +0 -152
  67. package/dist/effects/sql-observer.js +0 -238
  68. package/dist/effects/store.js +0 -181
  69. package/dist/effects/types.js +0 -2
  70. package/dist/query/api/effect-service.js +0 -308
  71. package/dist/query/api/esf-observations.js +0 -254
  72. package/dist/ui/assets/BriefsPage-DIU8iFjV.js +0 -2
  73. package/dist/ui/assets/CacheWriteSpikesChart-1Nfnf7OX.js +0 -1
  74. package/dist/ui/assets/Chip-s-32Dh3o.js +0 -1
  75. package/dist/ui/assets/ComposedChart-DUw8Y79v.js +0 -1
  76. package/dist/ui/assets/FlavorDecomposition-CkHX3M-U.js +0 -1
  77. package/dist/ui/assets/FrictionCell-X75fKRBK.js +0 -2
  78. package/dist/ui/assets/GlossaryPage-D1tYumqL.js +0 -1
  79. package/dist/ui/assets/HotSessionsPage-8HlRe0VB.js +0 -1
  80. package/dist/ui/assets/OverviewPage-DL4joBBE.js +0 -2
  81. package/dist/ui/assets/RecommendationsPage-NkfH17eB.js +0 -2
  82. package/dist/ui/assets/SessionDetailPage-Ceqt7poO.js +0 -1
  83. package/dist/ui/assets/SettingsPage-ZK4ii5h_.js +0 -25
  84. package/dist/ui/assets/SpendPercentileChip-CzY6h1gP.js +0 -1
  85. package/dist/ui/assets/TrendChart-BSLwlBiq.js +0 -1
  86. package/dist/ui/assets/WorkspaceDetailPage-DFOdEB39.js +0 -1
  87. package/dist/ui/assets/WorkspacesPage-Doct6meK.js +0 -1
  88. package/dist/ui/assets/chart-theme-DYgw_Tac.js +0 -1
  89. package/dist/ui/assets/graphicalItemSelectors-0-Ue_y1f.js +0 -1
  90. package/dist/ui/assets/rec-sessions-ggz9MYgP.js +0 -1
package/README.md CHANGED
@@ -1,104 +1,184 @@
1
1
  <p align="center">
2
- <img src="https://raw.githubusercontent.com/Doogit/AgentWrangler/main/docs/assets/logo.png" alt="AgentWrangler logo" width="200">
2
+ <img src="docs/assets/logo.png" alt="AgentWrangler logo" width="260">
3
3
  </p>
4
4
 
5
- # AgentWrangler
5
+ <h1 align="center">AgentWrangler</h1>
6
6
 
7
- See where your Claude Code tokens go, inspect costly sessions, and track whether a change helped.
8
- AgentWrangler reads local Claude Code transcripts and serves a dashboard on your machine.
9
- No account or cloud backend is required.
7
+ <p align="center">
8
+ <b>See where your Claude Code tokens go — and whether the work actually shipped.</b><br>
9
+ Local-first observability for Claude Code: token spend, session outcomes, waste detection,
10
+ and installable guardrails. Runs entirely on your machine.
11
+ </p>
10
12
 
11
- ## Quick start
13
+ <p align="center">
14
+ <a href="https://www.npmjs.com/package/agentwrangler"><img src="https://img.shields.io/npm/v/agentwrangler" alt="npm version"></a>
15
+ <a href="https://github.com/Doogit/AgentWrangler/actions/workflows/ci.yml"><img src="https://github.com/Doogit/AgentWrangler/actions/workflows/ci.yml/badge.svg" alt="CI"></a>
16
+ <a href="https://img.shields.io/node/v/agentwrangler"><img src="https://img.shields.io/node/v/agentwrangler" alt="node version"></a>
17
+ <a href="LICENSE"><img src="https://img.shields.io/badge/license-Apache--2.0-blue" alt="license"></a>
18
+ </p>
19
+
20
+ ![AgentWrangler dashboard — spend verdict, model mix, recommendations, and per-repo efficiency](docs/assets/dashboard.gif)
21
+
22
+ <!--
23
+ All screenshots and the demo GIF in this README are captured from a SANITIZED instance
24
+ (Vite test-mode fixtures — anonymized names, no live data) so no real workspace/repository
25
+ names or paths are committed (SEC-101). Regenerate against `npx vite --mode test`.
26
+ -->
27
+
28
+ ## Why
29
+
30
+ Claude Code tells you almost nothing about where your token budget goes. Sessions balloon,
31
+ caches miss, background agents idle — and you find out when you hit the rate limit.
32
+ AgentWrangler reads the transcripts Claude Code already writes to your disk and answers three
33
+ questions:
34
+
35
+ - **Where did the tokens go?** Per-model, per-workspace, per-session spend with cache economics.
36
+ - **Did the work ship?** Sessions are linked to the pull requests they merged or closed.
37
+ - **What should I change?** Ranked waste-source detectors with modeled savings — and one-click
38
+ guardrail hooks that warn *inside* Claude Code before waste happens.
12
39
 
13
- Requires **Node 22-24 and npm**, plus Claude Code transcripts for populated charts.
40
+ No cloud backend, no telemetry, no account. A daemon on `127.0.0.1`, a dashboard in your
41
+ browser, and a SQLite file in your home directory.
42
+
43
+ ## Quick start
14
44
 
15
45
  ```sh
16
46
  npx agentwrangler@latest
17
47
  ```
18
48
 
19
- Open **http://127.0.0.1:47821** if the browser does not open automatically. Keep the terminal
20
- running; press **Ctrl+C** to stop. The first scan runs in the background. An empty history is
21
- valid: use Claude Code, then return after ingestion. If a daemon is already running, stop it
22
- before starting another on the same port.
49
+ That's it — requires Node **22–24**. The daemon starts on `http://127.0.0.1:47821`, opens your
50
+ browser, and scans your `~/.claude/projects` transcripts in the background; the dashboard
51
+ appears immediately and fills in as the scan completes.
52
+
53
+ More options (install from source, GitHub outcomes sync, environment variables):
54
+ **[Getting started →](docs/getting-started.md)**
55
+
56
+ ## Features
57
+
58
+ ### Overview — verdict first, details on demand
59
+
60
+ One screen answers "how bad is it this week": a spend verdict with trend, your top waste
61
+ source with a copyable fix prompt, live rate-limit gauges (5-hour and 7-day), a burn forecast
62
+ against your calibrated weekly limit, hot sessions, cache efficiency, and per-model
63
+ context-per-turn tiles.
64
+
65
+ ![Overview tab — at-a-glance verdict, rate limits, burn forecast, hot sessions](docs/assets/overview.png)
66
+
67
+ ### Recommendations — waste-source detectors, ranked by impact
68
+
69
+ Ten detector families watch your sessions for the patterns that actually burn tokens: cache
70
+ misses (the biggest single lever), session hygiene, retry/redundant-read loops, tool-result
71
+ bloat, model routing, idle background sessions, and more. Each recommendation shows modeled
72
+ weekly savings, a confidence tier, and a concrete action — install a hook, copy a config
73
+ snippet, or copy a guided prompt straight into Claude Code. Adopted changes flow into an
74
+ **impact ledger** that tracks the measured effect, and modeled savings are never counted as
75
+ achieved.
76
+
77
+ ![Recommendations tab — ranked detector families with modeled savings and one-click actions](docs/assets/recommendations.png)
78
+
79
+ ### Installable guardrails — local checks inside Claude Code, before the waste
80
+
81
+ Five small hooks you can install from the dashboard (directly, or via a copyable prompt that
82
+ Claude Code applies itself):
83
+
84
+ | Guardrail | What it does |
85
+ |---|---|
86
+ | **Context-budget warning** | Warns when a session's context crosses your soft/hard thresholds |
87
+ | **Loop guard** | Flags repeated identical tool failures before they spiral |
88
+ | **Burn alert** | Catches idle sessions still burning tokens in the background |
89
+ | **Pre-compaction checkpoint** | Copies the raw local transcript before an automatic compaction, subject to a local retention cap |
90
+ | **Dangerous-command guard** | Asks before risky shell commands and denies a small catastrophe list |
91
+
92
+ The context-budget and burn hooks warn. The loop guard warns before it denies repeated identical
93
+ failures, and the dangerous-command guard can ask or deny. Direct install enables all five hooks;
94
+ the copied install prompt enables the context-budget, loop, and burn hooks only. Thresholds are
95
+ tunable from Settings, and direct uninstall removes every AgentWrangler hook.
96
+
97
+ ### Sessions — who spent it, and on what
98
+
99
+ The highest-cost sessions ranked with their output-to-context split, model, friction band
100
+ (API errors, tool failures, compactions, interrupts), and a "top X% by spend" self-percentile
101
+ chip. Drill into any session for a turn-by-turn timeline, its cost drivers (which detectors
102
+ fired and how hard), and a guided fix prompt built only from measured numbers.
103
+
104
+ ![Sessions tab — highest-cost sessions with friction bands and spend percentiles](docs/assets/sessions.png)
105
+
106
+ <details>
107
+ <summary>Session detail view</summary>
108
+
109
+ ![Session detail — per-session KPIs, cost drivers, and a measured-context fix prompt](docs/assets/session-detail.png)
110
+ </details>
111
+
112
+ ### Workspaces — spend efficiency by repository
113
+
114
+ Every repo you run Claude Code in, with spend share, trend, context-per-turn, cache-write
115
+ share, Opus share, and $/turn. With a GitHub token configured, sessions are linked to the PRs
116
+ and commits they produced — so you can see cost-per-merged-PR, not just cost.
117
+
118
+ ![Workspaces tab — per-repository spend, efficiency, and outcome linkage](docs/assets/workspaces.png)
119
+
120
+ <details>
121
+ <summary>Workspace detail view</summary>
122
+
123
+ ![Workspace detail — top sessions, context composition, and outcomes](docs/assets/workspace-detail.png)
124
+ </details>
125
+
126
+ ### Weekly brief — one page, three decisions
127
+
128
+ The week in one screen: spend verdict, what changed vs. last week, the top actions to take —
129
+ with a **Copy as Markdown** button so the whole brief drops into a standup note or a message.
23
130
 
24
- [Install from source, configure scan roots, or troubleshoot](https://github.com/Doogit/AgentWrangler/blob/main/docs/getting-started.md).
131
+ ![Briefs tab — weekly verdict, week-over-week deltas, and top actions](docs/assets/briefs.png)
25
132
 
26
- ## Your first five minutes
133
+ ### Honest numbers, labeled as such
27
134
 
28
- 1. **Check the scan.** In Overview, read the onboarding status. A completed scan with no
29
- recommendations is a valid result. For an unexpected empty history, inspect scan roots
30
- and parser health in Settings; saved scan-root changes require a daemon restart.
31
- 2. **Find one expensive session.** Select a date window, open Workspaces, choose a workspace,
32
- then open one of its sessions. Compare context, cache writes, and output before deciding
33
- what to change.
34
- 3. **Inspect one recommendation.** Open Recommendations and **Show details** on an instance.
35
- Read its evidence and caveats. A modeled amount is a projection; directional advice may
36
- have no dollar estimate. If nothing fires, there is nothing to adopt just to finish setup.
37
- 4. **Make one deliberate change.** A copied prompt is an artifact to review and run yourself.
38
- It does not edit files. After completing a supported change, use **I completed the change**
39
- when offered, then **Track this change** to record a baseline.
40
- 5. **Return to the Impact ledger.** Tracking starts observation, not a savings claim. Eligible
41
- signals use a 14-day observation window and may finish inconclusive. You can finish this
42
- first visit without a GitHub token, calibration, or installed hooks.
135
+ Every metric carries an honesty-tier chip `EXACT`, `LIST_EQUIV`, `MODELED`, `PROXY`,
136
+ `DIRECTIONAL`, `EXPERIMENTAL` so you always know what is measured versus estimated. Dollar
137
+ figures are list-price *equivalents* (subscription plans aren't billed per token; tokens drive
138
+ rate limits), and the built-in glossary ("How to read this dashboard") defines every
139
+ key metric in plain language. The full tour: **[Dashboard tour →](docs/dashboard-tour.md)**
43
140
 
44
- Dollar figures are **list-price equivalents**, not your subscription bill. Modeled savings
45
- are not achieved savings, and observed improvement does not prove the change caused it.
46
- GitHub linkage adds outcome metadata; calibration enables the burn forecast. Local spend,
47
- session inspection, and supported recommendation tracking work without either.
141
+ ## Privacy local-only by design
48
142
 
49
- [Worked example: trim always-loaded context and inspect its effect](https://github.com/Doogit/AgentWrangler/blob/main/docs/getting-started.md#example-trim-always-loaded-context).
143
+ - The daemon binds to **`127.0.0.1`** only. There is no cloud backend, telemetry, or account.
144
+ - Most dashboard data is **aggregates, ids, counts, and structural anchors**. Local command text
145
+ and filesystem paths can also be retained in SQLite; treat the database as sensitive. The optional
146
+ direct-install PreCompact hook can separately copy full raw transcripts locally.
147
+ - The optional GitHub token is read locally, never logged, never persisted to the DB.
148
+ - Usage refreshes can call Anthropic with an existing Claude Code sign-in. GitHub outcomes sync
149
+ calls GitHub only when a token is configured; calibration and G2 judging are separate opt-ins.
50
150
 
51
- ## Dashboard preview
151
+ Full details, including exactly what is and isn't stored: **[Privacy model →](docs/privacy.md)**
52
152
 
53
- ![Synthetic Overview: spend for the selected window, model mix, and links to sessions and recommendations](https://raw.githubusercontent.com/Doogit/AgentWrangler/main/docs/assets/overview.png)
153
+ ## Configuration
54
154
 
55
- This static preview uses synthetic data. [The dashboard tour](https://github.com/Doogit/AgentWrangler/blob/main/docs/dashboard-tour.md)
56
- contains the other views and an optional animated preview.
155
+ Everything is optional with sensible defaults port, DB path, scan roots, GitHub token, and
156
+ more are environment variables documented in [Getting started](docs/getting-started.md#configuration)
157
+ and [`.env.example`](.env.example).
57
158
 
58
- ## Pick a question
159
+ ## Documentation
59
160
 
60
- | I want to... | Start here |
161
+ | Page | What's in it |
61
162
  |---|---|
62
- | Find where my tokens went | [Workspaces and Sessions](https://github.com/Doogit/AgentWrangler/blob/main/docs/dashboard-tour.md#workspaces) |
63
- | Turn a recommendation into a change | [Worked example](https://github.com/Doogit/AgentWrangler/blob/main/docs/getting-started.md#example-trim-always-loaded-context) |
64
- | Understand the dollars and verdicts | [Metric vocabulary](https://github.com/Doogit/AgentWrangler/blob/main/docs/dashboard-tour.md#glossary-how-to-read-this-dashboard) |
65
- | Fix an empty dashboard | [First launch](https://github.com/Doogit/AgentWrangler/blob/main/docs/getting-started.md#first-launch) |
66
- | Connect GitHub or calibrate limits | [Optional setup](https://github.com/Doogit/AgentWrangler/blob/main/docs/getting-started.md#optional-setup) |
67
- | Read a weekly summary | [Briefs](https://github.com/Doogit/AgentWrangler/blob/main/docs/dashboard-tour.md#briefs) |
68
-
69
- ## Installable guardrails — local checks inside Claude Code, before the waste
70
-
71
- Hooks are optional and require an explicit install. Settings **Install directly** installs
72
- five hooks; **Copy install prompt** prepares instructions for three. Copying alone installs nothing.
73
-
74
- | Hook | Behavior | Install path |
75
- |---|---|---|
76
- | Context-budget | Warns when context crosses configured thresholds | Direct or copied prompt |
77
- | Loop guard | Warns, then can deny repeated identical failures | Direct or copied prompt |
78
- | Burn alert | Warns about session budget consumption | Direct or copied prompt |
79
- | Dangerous-command | Can ask or deny risky commands | Direct only |
80
- | PreCompact checkpoint | Copies raw transcripts locally, with retention limits | Direct only |
81
-
82
- [Installation, removal, and the checkpoint privacy details](https://github.com/Doogit/AgentWrangler/blob/main/docs/getting-started.md#in-session-guardrails).
83
-
84
- ## Privacy and limits
85
-
86
- - The daemon binds to **127.0.0.1**. There is no telemetry or hosted product backend.
87
- - SQLite contains aggregates and structural data, and can also contain local command text
88
- and filesystem paths. Treat it as sensitive. The optional PreCompact hook makes separate
89
- raw transcript copies on your machine.
90
- - Usage refresh can contact Anthropic using your existing Claude Code sign-in. GitHub
91
- outcomes sync requires a configured token. [Privacy details](https://github.com/Doogit/AgentWrangler/blob/main/docs/privacy.md).
92
- - Claude Code format changes can require parser updates. This is a single-user tool.
93
- - Windows local validation and Linux/macOS CI smoke coverage do not establish full native
94
- accessibility or credential-store compatibility on every platform.
95
-
96
- ## Deeper documentation
97
-
98
- [Configuration](https://github.com/Doogit/AgentWrangler/blob/main/docs/getting-started.md#configuration) |
99
- [Architecture](https://github.com/Doogit/AgentWrangler/blob/main/docs/planning/AgentWrangler_Technical_Architecture_v4_5_0.md) |
100
- [Data model and metrics](https://github.com/Doogit/AgentWrangler/blob/main/docs/planning/AgentWrangler_Data_Model_and_Metrics_v2.md) |
101
- [Contributing](https://github.com/Doogit/AgentWrangler/blob/main/.github/CONTRIBUTING.md) |
102
- [Security policy](https://github.com/Doogit/AgentWrangler/blob/main/.github/SECURITY.md)
103
-
104
- [Apache 2.0](https://github.com/Doogit/AgentWrangler/blob/main/LICENSE).
163
+ | [Getting started](docs/getting-started.md) | Install paths, optional setup, configuration, troubleshooting |
164
+ | [Dashboard tour](docs/dashboard-tour.md) | Every tab in depth, plus the metric vocabulary |
165
+ | [Privacy model](docs/privacy.md) | Local storage, raw-checkpoint exception, and network integrations |
166
+ | [Architecture](docs/planning/AgentWrangler_Technical_Architecture_v4_5_0.md) | Daemon, ingestion, detector, and query design |
167
+ | [Data model & metrics](docs/planning/AgentWrangler_Data_Model_and_Metrics_v2.md) | SQLite schema and metric definitions |
168
+ | [Contributing](.github/CONTRIBUTING.md) | Dev setup, checks, PR expectations |
169
+ | [Security policy](.github/SECURITY.md) | Threat model and how to report a vulnerability |
170
+
171
+ ## Limitations
172
+
173
+ - Reads Claude Code's JSONL transcript format; an upstream format change can require an
174
+ ingestion update.
175
+ - Tested on Windows; macOS/Linux are believed working — reports welcome.
176
+ - Single local user by design — no multi-user or team-aggregation mode.
177
+ - Outcome linkage needs a read-only GitHub token; without one the feature stays inert (and
178
+ Settings says so nothing fails silently).
179
+
180
+ ## License
181
+
182
+ [Apache 2.0](LICENSE) © 2026 AgentWrangler contributors.
183
+ See [CONTRIBUTING.md](.github/CONTRIBUTING.md), [SECURITY.md](.github/SECURITY.md), and
184
+ [CODE_OF_CONDUCT.md](.github/CODE_OF_CONDUCT.md).
@@ -200,7 +200,6 @@ function runClaudePhase(db, job, rec, phase, settingsPath, rt) {
200
200
  const changedPaths = [];
201
201
  let finalized = false;
202
202
  let fatalExitMessage = null;
203
- let killTimer;
204
203
  const cleanup = () => {
205
204
  fs.unlink(settingsPath, () => { });
206
205
  };
@@ -252,11 +251,9 @@ function runClaudePhase(db, job, rec, phase, settingsPath, rt) {
252
251
  }
253
252
  });
254
253
  const timer = setTimeout(() => {
255
- // Keep the job exclusive until the child has actually closed. Publishing
256
- // FAILED here lets a retry (or workspace cleanup) race the dying process.
257
- fatalExitMessage ??= "job timed out";
258
254
  proc.kill("SIGTERM");
259
- killTimer = setTimeout(() => proc.kill("SIGKILL"), 2000);
255
+ setTimeout(() => proc.kill("SIGKILL"), 2000);
256
+ markFailed("job timed out");
260
257
  }, rt.timeoutMs);
261
258
  proc.on("error", (err) => {
262
259
  clearTimeout(timer);
@@ -264,7 +261,6 @@ function runClaudePhase(db, job, rec, phase, settingsPath, rt) {
264
261
  });
265
262
  proc.on("close", (code) => {
266
263
  clearTimeout(timer);
267
- clearTimeout(killTimer);
268
264
  if (finalized)
269
265
  return;
270
266
  finalized = true;
@@ -302,10 +298,7 @@ function completeApplyPhase(db, job, rec, result, rt) {
302
298
  let runId = null;
303
299
  const completedAt = rt.now();
304
300
  try {
305
- adoptRecommendation(job.rec_id, completedAt.getTime(), {
306
- machineConfirmed: true,
307
- actionRevision: job.job_id,
308
- });
301
+ adoptRecommendation(job.rec_id, completedAt.getTime());
309
302
  runId = randomUUID();
310
303
  const evidencePackHash = createHash("sha256")
311
304
  .update(JSON.stringify(rec.evidence))
@@ -420,9 +413,6 @@ export function rollbackApplyJob(jobId) {
420
413
  if (row.status !== "APPLIED" && row.status !== "CONFIRMING") {
421
414
  throw new Error("job cannot be rolled back from this state");
422
415
  }
423
- if (db.prepare("SELECT 1 FROM effect_cycles WHERE rec_id=? LIMIT 1").get(row.rec_id)) {
424
- throw new Error("Whole-file backup rollback is not a supported effect inverse. Revert manually and attest the rollback in the effect ledger.");
425
- }
426
416
  if (row.backup_path === null)
427
417
  throw new Error("no backup available");
428
418
  fs.copyFileSync(row.backup_path, row.file_ref);
@@ -138,8 +138,6 @@ function requiresSessionToken(pathname) {
138
138
  return true;
139
139
  if (/^\/api\/recommendations\/jobs\/[^/]+\/(confirm|rollback)$/.test(pathname))
140
140
  return true;
141
- if (/^\/api\/esf\/effects\/(track|stop|close|rollback|attest-rollback)$/.test(pathname))
142
- return true;
143
141
  return false;
144
142
  }
145
143
  /**
@@ -22,6 +22,7 @@ import { openDb } from "../db/open.js";
22
22
  import { configGet as bptConfigGet, calibrateBytesPerToken } from "../detector/calibration.js";
23
23
  import { runContextProbe } from "../detector/context-probe.js";
24
24
  import { runDetectors } from "../detector/index.js";
25
+ import { runMeasurementPass } from "../detector/measurement.js";
25
26
  import { installHook, uninstallHook } from "../hook/install.js";
26
27
  import { collectSessionChurn } from "../ingest/churn-collector.js";
27
28
  import { runPostProbeHook, setPostIngestHook, setPostProbeHook } from "../ingest/detector-hook.js";
@@ -36,7 +37,6 @@ import { generateWeeklyReport } from "../query/api/reports.js";
36
37
  import { setQueryDb } from "../query/db-context.js";
37
38
  import { getSettingsData, setHealthInstance, setRuntimeResetHook, } from "../query/settings-store.js";
38
39
  import { loadConfig } from "./config.js";
39
- import { runEffectMeasurementPass } from "./effect-pass.js";
40
40
  import { createServer } from "./http.js";
41
41
  import { createOutcomesPassRunner } from "./outcomes-pass.js";
42
42
  import { setReady, setScanRoots, setScanState } from "./readiness.js";
@@ -107,10 +107,6 @@ if (isSmoke) {
107
107
  "review_findings",
108
108
  "recommendations",
109
109
  "recommendation_effects",
110
- "effect_cycles",
111
- "effect_guardrail_results",
112
- "effect_rollback_operations",
113
- "effect_mutation_keys",
114
110
  "apply_jobs",
115
111
  "analysis_runs",
116
112
  "ingest_quarantine",
@@ -154,16 +150,18 @@ function runProbePass(label) {
154
150
  const now = new Date();
155
151
  const { rows } = runContextProbe(db, now);
156
152
  console.log(`ContextProbe[${label}]: ${rows} row(s) upserted`);
157
- // Best-effort measurement after each probe, using the same observation clock.
153
+ // W4: measurement pass after each probe (best-effort; the seam never throws
154
+ // through, and runMeasurementPass itself is guarded/log-not-throw).
158
155
  runPostProbeHook(db, now);
159
156
  }
160
157
  catch (e) {
161
158
  console.log(`ContextProbe[${label}]: skipped — ${e instanceof Error ? e.message : String(e)}`);
162
159
  }
163
160
  }
164
- // Versioned measurement owns new cycles; W4 history stays read-only under the code gate.
161
+ // Wire the W4 Impact Ledger measurement pass onto the post-probe seam. The pass
162
+ // receives the probe's clock (deterministic; NFR-107), is throttled internally.
165
163
  setPostProbeHook((probeDb, now) => {
166
- runEffectMeasurementPass(probeDb, now);
164
+ runMeasurementPass(probeDb, now);
167
165
  });
168
166
  runProbePass("boot");
169
167
  // ── Outcomes bootstrap (WP5 — best-effort, never crash daemon) ────────────────
@@ -14,7 +14,6 @@ import { endSession, getAgentsLiveness } from "../query/api/agents-liveness.js";
14
14
  import { getBurnStatus } from "../query/api/burn-status.js";
15
15
  import { getCostPerSuccess } from "../query/api/cost-per-success.js";
16
16
  import { getDeliveryMetrics } from "../query/api/delivery.js";
17
- import { EffectRequestError, listEffectEvidence, mutateEffectCycle, trackCompletedChange, } from "../query/api/effect-service.js";
18
17
  import { getClosureProxy } from "../query/api/effectiveness.js";
19
18
  import { getEfficiencyHeadroom } from "../query/api/efficiency-headroom.js";
20
19
  import { getHeadroomTrend } from "../query/api/headroom-trend.js";
@@ -36,19 +35,10 @@ function sendJson(res, status, body) {
36
35
  });
37
36
  res.end(payload);
38
37
  }
39
- function readBody(req, maxBytes) {
38
+ function readBody(req) {
40
39
  return new Promise((resolve, reject) => {
41
40
  const chunks = [];
42
- let size = 0;
43
- req.on("data", (chunk) => {
44
- size += chunk.length;
45
- if (maxBytes !== undefined && size > maxBytes) {
46
- reject(new EffectRequestError(400, "Request body is too large."));
47
- req.resume();
48
- return;
49
- }
50
- chunks.push(chunk);
51
- });
41
+ req.on("data", (chunk) => chunks.push(chunk));
52
42
  req.on("end", () => resolve(Buffer.concat(chunks).toString("utf-8")));
53
43
  req.on("error", reject);
54
44
  });
@@ -138,47 +128,6 @@ export function handleApiRequest(_db, req, res, method, url) {
138
128
  // Strip query string for routing.
139
129
  const pathname = url.split("?")[0] ?? url;
140
130
  try {
141
- // GET /api/esf/effects — independently keyset-paged versioned and legacy evidence.
142
- if (method === "GET" && pathname === "/api/esf/effects") {
143
- const params = new URLSearchParams(url.split("?")[1] ?? "");
144
- const workspaceId = params.get("workspace_id");
145
- sendJson(res, 200, listEffectEvidence(_db, workspaceId, params.get("rec_id") ?? undefined, params.get("limit") ?? undefined, params.get("cycle_cursor") ?? undefined, params.get("legacy_cursor") ?? undefined));
146
- return;
147
- }
148
- const effectAction = pathname.match(/^\/api\/esf\/effects\/(track|stop|close|rollback|attest-rollback)$/)?.[1];
149
- if (method === "POST" && effectAction !== undefined) {
150
- readBody(req, 4096)
151
- .then((raw) => {
152
- let body;
153
- try {
154
- body = JSON.parse(raw);
155
- }
156
- catch {
157
- sendJson(res, 400, { error: "Invalid JSON body." });
158
- return;
159
- }
160
- try {
161
- const result = effectAction === "track"
162
- ? trackCompletedChange(_db, body)
163
- : mutateEffectCycle(_db, effectAction, body);
164
- sendJson(res, 200, result);
165
- }
166
- catch (error) {
167
- const status = error instanceof EffectRequestError ? error.status : 409;
168
- sendJson(res, status, {
169
- error: error instanceof EffectRequestError
170
- ? error.message
171
- : "Effect request conflicts with the current cycle.",
172
- });
173
- }
174
- })
175
- .catch((error) => sendJson(res, error instanceof EffectRequestError ? error.status : 500, {
176
- error: error instanceof EffectRequestError
177
- ? error.message
178
- : "Internal error reading request body",
179
- }));
180
- return;
181
- }
182
131
  // GET /api/overview
183
132
  if (method === "GET" && pathname === "/api/overview") {
184
133
  sendJson(res, 200, getGlobalOverview(parseWindowFilter(url)));
@@ -315,7 +264,7 @@ export function handleApiRequest(_db, req, res, method, url) {
315
264
  sendJson(res, 200, getPractices(_db, { from, to }));
316
265
  return;
317
266
  }
318
- // GET /api/efficiency-headroom (individual weekly estimates; separate selected-window spend)
267
+ // GET /api/efficiency-headroom (BM2 modeled savings vs trailing-window spend)
319
268
  if (method === "GET" && pathname === "/api/efficiency-headroom") {
320
269
  const { from, to } = resolveWindow(parseWindowFilter(url));
321
270
  sendJson(res, 200, getEfficiencyHeadroom(_db, { from, to }));
@@ -557,15 +506,13 @@ export function handleApiRequest(_db, req, res, method, url) {
557
506
  sendJson(res, 400, { error: "Invalid JSON body" });
558
507
  return;
559
508
  }
560
- const { rec_id, completed_change } = body;
509
+ const { rec_id } = body;
561
510
  if (typeof rec_id !== "string" || rec_id.length === 0) {
562
511
  sendJson(res, 400, { error: "rec_id is required" });
563
512
  return;
564
513
  }
565
514
  try {
566
- sendJson(res, 200, adoptRecommendation(rec_id, Date.now(), {
567
- completedChange: completed_change === true,
568
- }));
515
+ sendJson(res, 200, adoptRecommendation(rec_id));
569
516
  }
570
517
  catch (e) {
571
518
  sendJson(res, 400, { error: e instanceof Error ? e.message : "Adopt failed" });
@@ -802,11 +749,7 @@ export function handleApiRequest(_db, req, res, method, url) {
802
749
  // 404 for everything else.
803
750
  sendJson(res, 404, { error: "Not found", path: pathname });
804
751
  }
805
- catch (error) {
806
- if (error instanceof EffectRequestError) {
807
- sendJson(res, error.status, { error: error.message });
808
- return;
809
- }
752
+ catch {
810
753
  // Do not leak internal exception text into a cross-origin-readable 500 body
811
754
  // (GET routes are not CSRF-gated). Validation errors on POST routes still
812
755
  // return their message via the 400 paths above.
@@ -1,6 +1,4 @@
1
- import { createHash } from "node:crypto";
2
1
  import { performance } from "node:perf_hooks";
3
- import { cycleFromRow } from "../effects/store.js";
4
2
  import { GLOBAL_WORKSPACE_ID } from "./context-probe.js";
5
3
  import { isD1SourceBackedRecommendation, parseD1SourceIdentity } from "./d1-source-identity.js";
6
4
  import { AFTER_WINDOW_DAYS } from "./measurement.js";
@@ -80,15 +78,10 @@ function validateHistoryRows(db) {
80
78
  });
81
79
  }
82
80
  function readOpenEffects(db) {
83
- const hasVersioned = db.prepare("SELECT 1 FROM sqlite_master WHERE name='effect_cycles'").get();
84
- const versionedExclusion = hasVersioned
85
- ? "AND NOT EXISTS (SELECT 1 FROM effect_cycles c WHERE c.rec_id=recommendations.rec_id)"
86
- : "";
87
81
  const recs = db
88
82
  .prepare(`SELECT rec_id, detector_id, scope_workspace_id, evidence_json, target_metric, adopted_at
89
83
  FROM recommendations
90
84
  WHERE state IN ('ADOPTED', 'MEASURING')
91
- ${versionedExclusion}
92
85
  ORDER BY rec_id ASC`)
93
86
  .all();
94
87
  const sourceBacked = recs.filter(isD1SourceBackedRecommendation);
@@ -147,48 +140,6 @@ function readOpenEffects(db) {
147
140
  }
148
141
  if (malformedN > 0)
149
142
  throw new RetentionPlanError("invalid_open_effect", malformedN);
150
- if (hasVersioned) {
151
- const rows = db
152
- .prepare(`SELECT c.*,r.evidence_json,r.scope_workspace_id
153
- FROM effect_cycles c JOIN recommendations r ON r.rec_id=c.rec_id
154
- WHERE c.detector_id='D1' AND c.state IN ('OPEN_SETTLING','OPEN_MEASURING')`)
155
- .all();
156
- for (const row of rows) {
157
- const cycle = cycleFromRow(row);
158
- const identity = parseD1SourceIdentity(String(row.evidence_json));
159
- const workspaceId = row.scope_workspace_id === null ? GLOBAL_WORKSPACE_ID : String(row.scope_workspace_id);
160
- const beforeToMs = canonicalTimestamp(cycle.baselineTo);
161
- const afterFromMs = canonicalTimestamp(cycle.observationFrom);
162
- const afterToMs = canonicalTimestamp(cycle.scheduledObservationTo);
163
- if (!identity ||
164
- !COMPONENTS.has(identity.component) ||
165
- !identity.fileRef ||
166
- cycle.versionStatus !== "SUPPORTED" ||
167
- beforeToMs === null ||
168
- afterFromMs === null ||
169
- afterToMs === null ||
170
- beforeToMs !== afterFromMs ||
171
- afterToMs < afterFromMs) {
172
- throw new RetentionPlanError("invalid_open_effect", 1);
173
- }
174
- // Same opaque tuple encoding as the service; a moved/reassigned source is not reinterpreted.
175
- const digest = createHash("sha256")
176
- .update(JSON.stringify({
177
- workspaceId,
178
- component: identity.component,
179
- fileRef: identity.fileRef,
180
- }))
181
- .digest("hex");
182
- if (digest !== cycle.scope.sourceIdentity)
183
- throw new RetentionPlanError("invalid_open_effect", 1);
184
- effects.push({
185
- sourceKey: sourceKey(workspaceId, identity.component, identity.fileRef),
186
- beforeToMs,
187
- afterFromMs,
188
- afterToMs,
189
- });
190
- }
191
- }
192
143
  return effects;
193
144
  }
194
145
  function emptySummary(mode, startedAt, failureClass, malformedOpenEffectN = 0) {
@@ -95,7 +95,7 @@ export const d10Detector = {
95
95
  const latestProbedAt = rows.reduce((latest, row) => (latest === null || row.probed_at > latest ? row.probed_at : latest), null);
96
96
  const refs = rows.map((row) => row.file_ref);
97
97
  const evidence = {
98
- title: `Review tool catalog: ${Math.round(catalogTokens / 1000)}K estimated tokens`,
98
+ title: `Review ${state.effective_catalog_state} tool catalog: ${Math.round(catalogTokens / 1000)}K tokens`,
99
99
  component: "MCP_SCHEMAS",
100
100
  file_ref: refs.length === 1 ? refs[0] : null,
101
101
  file_refs: refs,
@@ -120,7 +120,7 @@ export const d10Detector = {
120
120
  scopeKey: "D10|global|MCP_SCHEMAS",
121
121
  category: "TOOLING",
122
122
  scope_workspace_id: null,
123
- lever: "The tool catalog exceeds its size target; actual loaded context is not measured",
123
+ lever: "Too many connected tools, plugins, and skills",
124
124
  target_metric: "catalog_context_tokens",
125
125
  // R11 is required before catalog size can become a freed-headroom claim.
126
126
  modeled_savings_u_per_wk: null,
@@ -29,16 +29,16 @@ function stepsFor(component, fileRef) {
29
29
  case "CLAUDE_MD":
30
30
  return [
31
31
  `Open ${fileRef}`,
32
- "Move changelogs, history, and background explanations to a linked document",
33
- "Keep only current rules and links here",
34
- "Check the size again after the next local scan",
32
+ "Move changelog/history/rationale prose to a linked doc",
33
+ "Keep current-state rules + pointers only",
34
+ "Re-measure: probe re-sizes on next daemon pass",
35
35
  ];
36
36
  case "MEMORY":
37
37
  return [
38
38
  `Review memory files under ${fileRef}`,
39
- "Remove old or duplicate memories",
40
- "Combine overlapping facts into concise entries",
41
- "Check the size again after the next local scan",
39
+ "Delete stale or duplicate memories",
40
+ "Consolidate overlapping facts into concise entries",
41
+ "Re-measure: probe re-sizes on next daemon pass",
42
42
  ];
43
43
  case "MCP_SCHEMAS":
44
44
  return [
@@ -62,13 +62,13 @@ function titleFor(component, tokens, target) {
62
62
  function leverFor(component) {
63
63
  switch (component) {
64
64
  case "CLAUDE_MD":
65
- return "Move changelogs and history out of CLAUDE.md; keep current rules and links.";
65
+ return "Move changelog/history prose out of CLAUDE.md; keep current-state + pointers.";
66
66
  case "MEMORY":
67
- return "Remove old or duplicate memories and combine overlapping facts.";
67
+ return "Prune stale/duplicate memories; consolidate overlapping facts.";
68
68
  case "MCP_SCHEMAS":
69
69
  return "Identify rarely-used skills/plugins; extract to on-demand or disable.";
70
70
  default:
71
- return "Shorten this content that is loaded into every conversation.";
71
+ return "Trim always-loaded context source to the per-source target.";
72
72
  }
73
73
  }
74
74
  export const d1Detector = {