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.
- package/README.md +162 -82
- package/dist/apply/jobs.js +3 -13
- package/dist/daemon/http.js +0 -2
- package/dist/daemon/index.js +6 -8
- package/dist/daemon/router.js +6 -63
- package/dist/detector/context-history-retention.js +0 -49
- package/dist/detector/detectors/d10_catalog_footprint.js +2 -2
- package/dist/detector/detectors/d1_ctx_always_loaded.js +9 -9
- package/dist/detector/detectors/d2_session_long_full_context.js +2 -2
- package/dist/detector/detectors/d4_model_mismatch.js +3 -3
- package/dist/detector/detectors/d6_tool_result_bloat.js +1 -1
- package/dist/detector/detectors/d7_loop_retry_waste.js +1 -1
- package/dist/detector/detectors/d8_cache_write_churn.js +2 -2
- package/dist/detector/detectors/d9_idle_background_session.js +1 -1
- package/dist/detector/measurement.js +3 -14
- package/dist/hook/precompact-checkpoint-hook.mjs +13 -26
- package/dist/ingest/pricing.js +0 -15
- package/dist/query/api/cost-per-success.js +1 -13
- package/dist/query/api/delivery.js +3 -25
- package/dist/query/api/effectiveness.js +5 -30
- package/dist/query/api/efficiency-headroom.js +14 -12
- package/dist/query/api/outcomes.js +1 -2
- package/dist/query/api/overview.js +6 -90
- package/dist/query/api/recommendations-ledger.js +14 -9
- package/dist/query/api/recommendations.js +1 -21
- package/dist/query/settings-store.js +0 -4
- package/dist/query/spend.js +2 -64
- package/dist/ui/assets/{BarChart-CIjCNfbI.js → BarChart-p1usctxJ.js} +3 -3
- package/dist/ui/assets/BriefsPage-CneqLVGC.js +2 -0
- package/dist/ui/assets/CacheWriteSpikesChart-zNRZbzy9.js +1 -0
- package/dist/ui/assets/Chip-Bu6udQGq.js +1 -0
- package/dist/ui/assets/ComposedChart-8D5E-Po_.js +1 -0
- package/dist/ui/assets/{EmptyState-B8vpug3_.js → EmptyState-CvQeJjjG.js} +1 -1
- package/dist/ui/assets/FlavorDecomposition-xcMLA50f.js +1 -0
- package/dist/ui/assets/FrictionCell-OafGLGxT.js +2 -0
- package/dist/ui/assets/GlossaryPage-BmOP3Syj.js +1 -0
- package/dist/ui/assets/HotSessionsPage-FoxuuDLF.js +1 -0
- package/dist/ui/assets/{InfoTip-CL8RC4vz.js → InfoTip-Dx7BHpBu.js} +1 -1
- package/dist/ui/assets/{Legend-CB7sqqJH.js → Legend--q6-rQGk.js} +3 -3
- package/dist/ui/assets/{Line-B9XY30uT.js → Line-DzVtUwF4.js} +1 -1
- package/dist/ui/assets/OverviewPage-BjSQYNvS.js +2 -0
- package/dist/ui/assets/RecommendationsPage-CRTpzcHs.js +2 -0
- package/dist/ui/assets/{Scatter-BSWm_yIZ.js → Scatter-Cr_hCkjG.js} +1 -1
- package/dist/ui/assets/SessionDetailPage-Dd3u8kVI.js +1 -0
- package/dist/ui/assets/SettingsPage-BPVQyjlc.js +25 -0
- package/dist/ui/assets/{Skeleton-Dp_m79B8.js → Skeleton-CcD61x7q.js} +1 -1
- package/dist/ui/assets/SpendPercentileChip-BaQhJQAn.js +1 -0
- package/dist/ui/assets/TrendChart-Baz3GGgE.js +1 -0
- package/dist/ui/assets/WorkspaceDetailPage-yk4z_EtQ.js +1 -0
- package/dist/ui/assets/WorkspacesPage-BDtclXJP.js +1 -0
- package/dist/ui/assets/{CartesianChart-DEnDOGKj.js → chart-theme-JH5aQymO.js} +36 -36
- package/dist/ui/assets/graphicalItemSelectors-CCbl8AKC.js +1 -0
- package/dist/ui/assets/{index-DhFiA8Ch.js → index-DRhoA51c.js} +10 -10
- package/dist/ui/assets/{index-D_IJsvkJ.css → index-SrfNUeBZ.css} +1 -1
- package/dist/ui/assets/{prompt-templates-CVzMex9L.js → prompt-templates-DhR4Qoy9.js} +4 -4
- package/dist/ui/assets/{useExperimentalActions-aI00AFZv.js → useExperimentalActions-BPDdswCN.js} +1 -1
- package/dist/ui/index.html +2 -2
- package/package.json +1 -1
- package/dist/daemon/effect-pass.js +0 -24
- package/dist/db/migrations/018_effect_cycles.sql +0 -109
- package/dist/effects/api-contract.js +0 -1
- package/dist/effects/engine.js +0 -574
- package/dist/effects/gate.js +0 -2
- package/dist/effects/index.js +0 -5
- package/dist/effects/legacy.js +0 -28
- package/dist/effects/registry.js +0 -152
- package/dist/effects/sql-observer.js +0 -238
- package/dist/effects/store.js +0 -181
- package/dist/effects/types.js +0 -2
- package/dist/query/api/effect-service.js +0 -308
- package/dist/query/api/esf-observations.js +0 -254
- package/dist/ui/assets/BriefsPage-DIU8iFjV.js +0 -2
- package/dist/ui/assets/CacheWriteSpikesChart-1Nfnf7OX.js +0 -1
- package/dist/ui/assets/Chip-s-32Dh3o.js +0 -1
- package/dist/ui/assets/ComposedChart-DUw8Y79v.js +0 -1
- package/dist/ui/assets/FlavorDecomposition-CkHX3M-U.js +0 -1
- package/dist/ui/assets/FrictionCell-X75fKRBK.js +0 -2
- package/dist/ui/assets/GlossaryPage-D1tYumqL.js +0 -1
- package/dist/ui/assets/HotSessionsPage-8HlRe0VB.js +0 -1
- package/dist/ui/assets/OverviewPage-DL4joBBE.js +0 -2
- package/dist/ui/assets/RecommendationsPage-NkfH17eB.js +0 -2
- package/dist/ui/assets/SessionDetailPage-Ceqt7poO.js +0 -1
- package/dist/ui/assets/SettingsPage-ZK4ii5h_.js +0 -25
- package/dist/ui/assets/SpendPercentileChip-CzY6h1gP.js +0 -1
- package/dist/ui/assets/TrendChart-BSLwlBiq.js +0 -1
- package/dist/ui/assets/WorkspaceDetailPage-DFOdEB39.js +0 -1
- package/dist/ui/assets/WorkspacesPage-Doct6meK.js +0 -1
- package/dist/ui/assets/chart-theme-DYgw_Tac.js +0 -1
- package/dist/ui/assets/graphicalItemSelectors-0-Ue_y1f.js +0 -1
- 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="
|
|
2
|
+
<img src="docs/assets/logo.png" alt="AgentWrangler logo" width="260">
|
|
3
3
|
</p>
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
<h1 align="center">AgentWrangler</h1>
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
|
|
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
|
+

|
|
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
|
-
|
|
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
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
+

|
|
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
|
+

|
|
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
|
+

|
|
105
|
+
|
|
106
|
+
<details>
|
|
107
|
+
<summary>Session detail view</summary>
|
|
108
|
+
|
|
109
|
+

|
|
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
|
+

|
|
119
|
+
|
|
120
|
+
<details>
|
|
121
|
+
<summary>Workspace detail view</summary>
|
|
122
|
+
|
|
123
|
+

|
|
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
|
-
[
|
|
131
|
+

|
|
25
132
|
|
|
26
|
-
|
|
133
|
+
### Honest numbers, labeled as such
|
|
27
134
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
151
|
+
Full details, including exactly what is and isn't stored: **[Privacy model →](docs/privacy.md)**
|
|
52
152
|
|
|
53
|
-
|
|
153
|
+
## Configuration
|
|
54
154
|
|
|
55
|
-
|
|
56
|
-
|
|
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
|
-
##
|
|
159
|
+
## Documentation
|
|
59
160
|
|
|
60
|
-
|
|
|
161
|
+
| Page | What's in it |
|
|
61
162
|
|---|---|
|
|
62
|
-
|
|
|
63
|
-
|
|
|
64
|
-
|
|
|
65
|
-
|
|
|
66
|
-
|
|
|
67
|
-
|
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
[
|
|
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).
|
package/dist/apply/jobs.js
CHANGED
|
@@ -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
|
-
|
|
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);
|
package/dist/daemon/http.js
CHANGED
|
@@ -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
|
/**
|
package/dist/daemon/index.js
CHANGED
|
@@ -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
|
-
//
|
|
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
|
-
//
|
|
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
|
-
|
|
164
|
+
runMeasurementPass(probeDb, now);
|
|
167
165
|
});
|
|
168
166
|
runProbePass("boot");
|
|
169
167
|
// ── Outcomes bootstrap (WP5 — best-effort, never crash daemon) ────────────────
|
package/dist/daemon/router.js
CHANGED
|
@@ -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
|
|
38
|
+
function readBody(req) {
|
|
40
39
|
return new Promise((resolve, reject) => {
|
|
41
40
|
const chunks = [];
|
|
42
|
-
|
|
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 (
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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: "
|
|
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
|
|
33
|
-
"Keep
|
|
34
|
-
"
|
|
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
|
-
"
|
|
40
|
-
"
|
|
41
|
-
"
|
|
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
|
|
65
|
+
return "Move changelog/history prose out of CLAUDE.md; keep current-state + pointers.";
|
|
66
66
|
case "MEMORY":
|
|
67
|
-
return "
|
|
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 "
|
|
71
|
+
return "Trim always-loaded context source to the per-source target.";
|
|
72
72
|
}
|
|
73
73
|
}
|
|
74
74
|
export const d1Detector = {
|