agentwrangler 0.1.1-next.fbbbe7f → 0.1.1-next.fbffef0
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 +82 -162
- package/dist/apply/jobs.js +13 -3
- package/dist/daemon/effect-pass.js +24 -0
- package/dist/daemon/http.js +10 -0
- package/dist/daemon/index.js +48 -87
- package/dist/daemon/lifecycle.js +122 -0
- package/dist/daemon/router.js +296 -14
- package/dist/db/migrations/018_effect_cycles.sql +109 -0
- package/dist/db/migrations/019_work_records.sql +291 -0
- package/dist/detector/context-history-retention.js +49 -0
- 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 +14 -3
- package/dist/effects/api-contract.js +1 -0
- package/dist/effects/engine.js +574 -0
- package/dist/effects/gate.js +2 -0
- package/dist/effects/index.js +5 -0
- package/dist/effects/legacy.js +28 -0
- package/dist/effects/registry.js +152 -0
- package/dist/effects/sql-observer.js +238 -0
- package/dist/effects/store.js +181 -0
- package/dist/effects/types.js +2 -0
- package/dist/hook/precompact-checkpoint-hook.mjs +26 -13
- package/dist/ingest/ingestor.js +322 -73
- package/dist/ingest/pricing.js +15 -0
- package/dist/ingest/tail.js +130 -20
- package/dist/query/api/cost-per-success.js +13 -1
- package/dist/query/api/delivery.js +25 -3
- package/dist/query/api/effect-service.js +308 -0
- package/dist/query/api/effectiveness.js +30 -5
- package/dist/query/api/efficiency-headroom.js +12 -14
- package/dist/query/api/esf-observations.js +285 -0
- package/dist/query/api/outcomes.js +2 -1
- package/dist/query/api/overview.js +105 -6
- package/dist/query/api/recommendations-ledger.js +9 -14
- package/dist/query/api/recommendations.js +21 -1
- package/dist/query/api/reported-work.js +46 -0
- package/dist/query/api/spend-flavor.js +22 -15
- package/dist/query/api/trends.js +86 -6
- package/dist/query/api/work-records.js +85 -0
- package/dist/query/settings-store.js +13 -0
- package/dist/query/spend.js +91 -15
- package/dist/query/trends.js +27 -0
- package/dist/ui/assets/{BarChart-p1usctxJ.js → BarChart-BOecTQgC.js} +3 -3
- package/dist/ui/assets/BriefsPage-CjvXspiw.js +2 -0
- package/dist/ui/assets/CacheWriteSpikesChart-5Pn1sUZC.js +1 -0
- package/dist/ui/assets/{chart-theme-JH5aQymO.js → CartesianChart-56rkZQt4.js} +36 -36
- package/dist/ui/assets/Chip-BZTcuGOm.js +1 -0
- package/dist/ui/assets/ComposedChart-C8PPdkFl.js +1 -0
- package/dist/ui/assets/{EmptyState-CvQeJjjG.js → EmptyState-BfD86Pv3.js} +1 -1
- package/dist/ui/assets/FlavorDecomposition-BsOuw4CF.js +1 -0
- package/dist/ui/assets/FrictionCell-DQBJsot-.js +2 -0
- package/dist/ui/assets/GlossaryPage-DutLIAlv.js +1 -0
- package/dist/ui/assets/HotSessionsPage-DLWlyBfb.js +1 -0
- package/dist/ui/assets/{InfoTip-Dx7BHpBu.js → InfoTip-BrYyvCNl.js} +1 -1
- package/dist/ui/assets/{Legend--q6-rQGk.js → Legend-DzSAmt8-.js} +3 -3
- package/dist/ui/assets/{Line-DzVtUwF4.js → Line-1eksYo3m.js} +1 -1
- package/dist/ui/assets/Modal-DX-JN5Ul.js +1 -0
- package/dist/ui/assets/ObservationEvidence-Bbsc-DCW.js +1 -0
- package/dist/ui/assets/ObservationEvidence-Cy92xibu.css +1 -0
- package/dist/ui/assets/OverviewPage-CWgzRIqk.js +2 -0
- package/dist/ui/assets/RecommendationsPage-sRHEku70.js +2 -0
- package/dist/ui/assets/{Scatter-Cr_hCkjG.js → Scatter-Ys7Lbhjf.js} +1 -1
- package/dist/ui/assets/SessionDetailPage-L02ituTQ.js +1 -0
- package/dist/ui/assets/SettingsPage-DJjWicvx.js +25 -0
- package/dist/ui/assets/{Skeleton-CcD61x7q.js → Skeleton-ab1u7ntu.js} +1 -1
- package/dist/ui/assets/SpendPercentileChip-DnRJWBUP.js +1 -0
- package/dist/ui/assets/TrendChart-_nohASRq.js +1 -0
- package/dist/ui/assets/WorkspaceDetailPage-BQ3XA3Hf.js +1 -0
- package/dist/ui/assets/WorkspacesPage-DKdpBgC9.js +1 -0
- package/dist/ui/assets/chart-theme-0POFlal8.js +1 -0
- package/dist/ui/assets/graphicalItemSelectors-B97PsJDp.js +1 -0
- package/dist/ui/assets/{index-DRhoA51c.js → index-DAAmee2m.js} +10 -10
- package/dist/ui/assets/{index-SrfNUeBZ.css → index-D_IJsvkJ.css} +1 -1
- package/dist/ui/assets/{prompt-templates-DhR4Qoy9.js → prompt-templates-CVzMex9L.js} +4 -4
- package/dist/ui/assets/rec-sessions-ggz9MYgP.js +1 -0
- package/dist/ui/assets/{useExperimentalActions-BPDdswCN.js → useExperimentalActions-avkWaDmQ.js} +1 -1
- package/dist/ui/index.html +2 -2
- package/dist/work-records/allocation.js +242 -0
- package/dist/work-records/ids.js +80 -0
- package/dist/work-records/index.js +4 -0
- package/dist/work-records/store.js +376 -0
- package/dist/work-records/types.js +29 -0
- package/dist/work-records/validation.js +71 -0
- package/package.json +2 -1
- package/dist/ui/assets/BriefsPage-CneqLVGC.js +0 -2
- package/dist/ui/assets/CacheWriteSpikesChart-zNRZbzy9.js +0 -1
- package/dist/ui/assets/Chip-Bu6udQGq.js +0 -1
- package/dist/ui/assets/ComposedChart-8D5E-Po_.js +0 -1
- package/dist/ui/assets/FlavorDecomposition-xcMLA50f.js +0 -1
- package/dist/ui/assets/FrictionCell-OafGLGxT.js +0 -2
- package/dist/ui/assets/GlossaryPage-BmOP3Syj.js +0 -1
- package/dist/ui/assets/HotSessionsPage-FoxuuDLF.js +0 -1
- package/dist/ui/assets/OverviewPage-BjSQYNvS.js +0 -2
- package/dist/ui/assets/RecommendationsPage-CRTpzcHs.js +0 -2
- package/dist/ui/assets/SessionDetailPage-Dd3u8kVI.js +0 -1
- package/dist/ui/assets/SettingsPage-BPVQyjlc.js +0 -25
- package/dist/ui/assets/SpendPercentileChip-BaQhJQAn.js +0 -1
- package/dist/ui/assets/TrendChart-Baz3GGgE.js +0 -1
- package/dist/ui/assets/WorkspaceDetailPage-yk4z_EtQ.js +0 -1
- package/dist/ui/assets/WorkspacesPage-BDtclXJP.js +0 -1
- package/dist/ui/assets/graphicalItemSelectors-CCbl8AKC.js +0 -1
package/README.md
CHANGED
|
@@ -1,184 +1,104 @@
|
|
|
1
1
|
<p align="center">
|
|
2
|
-
<img src="docs/assets/logo.png" alt="AgentWrangler logo" width="
|
|
2
|
+
<img src="https://raw.githubusercontent.com/Doogit/AgentWrangler/main/docs/assets/logo.png" alt="AgentWrangler logo" width="200">
|
|
3
3
|
</p>
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
# AgentWrangler
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
and installable guardrails. Runs entirely on your machine.
|
|
11
|
-
</p>
|
|
12
|
-
|
|
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.
|
|
39
|
-
|
|
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.
|
|
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.
|
|
42
10
|
|
|
43
11
|
## Quick start
|
|
44
12
|
|
|
13
|
+
Requires **Node 22-24 and npm**, plus Claude Code transcripts for populated charts.
|
|
14
|
+
|
|
45
15
|
```sh
|
|
46
16
|
npx agentwrangler@latest
|
|
47
17
|
```
|
|
48
18
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
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.
|
|
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.
|
|
130
23
|
|
|
131
|
-
|
|
24
|
+
[Install from source, configure scan roots, or troubleshoot](https://github.com/Doogit/AgentWrangler/blob/main/docs/getting-started.md).
|
|
132
25
|
|
|
133
|
-
|
|
26
|
+
## Your first five minutes
|
|
134
27
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
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.
|
|
140
43
|
|
|
141
|
-
|
|
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.
|
|
142
48
|
|
|
143
|
-
-
|
|
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.
|
|
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).
|
|
150
50
|
|
|
151
|
-
|
|
51
|
+
## Dashboard preview
|
|
152
52
|
|
|
153
|
-
|
|
53
|
+

|
|
154
54
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
and [`.env.example`](.env.example).
|
|
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.
|
|
158
57
|
|
|
159
|
-
##
|
|
58
|
+
## Pick a question
|
|
160
59
|
|
|
161
|
-
|
|
|
60
|
+
| I want to... | Start here |
|
|
162
61
|
|---|---|
|
|
163
|
-
| [
|
|
164
|
-
| [
|
|
165
|
-
| [
|
|
166
|
-
| [
|
|
167
|
-
|
|
|
168
|
-
| [
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
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).
|
package/dist/apply/jobs.js
CHANGED
|
@@ -200,6 +200,7 @@ 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;
|
|
203
204
|
const cleanup = () => {
|
|
204
205
|
fs.unlink(settingsPath, () => { });
|
|
205
206
|
};
|
|
@@ -251,9 +252,11 @@ function runClaudePhase(db, job, rec, phase, settingsPath, rt) {
|
|
|
251
252
|
}
|
|
252
253
|
});
|
|
253
254
|
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";
|
|
254
258
|
proc.kill("SIGTERM");
|
|
255
|
-
setTimeout(() => proc.kill("SIGKILL"), 2000);
|
|
256
|
-
markFailed("job timed out");
|
|
259
|
+
killTimer = setTimeout(() => proc.kill("SIGKILL"), 2000);
|
|
257
260
|
}, rt.timeoutMs);
|
|
258
261
|
proc.on("error", (err) => {
|
|
259
262
|
clearTimeout(timer);
|
|
@@ -261,6 +264,7 @@ function runClaudePhase(db, job, rec, phase, settingsPath, rt) {
|
|
|
261
264
|
});
|
|
262
265
|
proc.on("close", (code) => {
|
|
263
266
|
clearTimeout(timer);
|
|
267
|
+
clearTimeout(killTimer);
|
|
264
268
|
if (finalized)
|
|
265
269
|
return;
|
|
266
270
|
finalized = true;
|
|
@@ -298,7 +302,10 @@ function completeApplyPhase(db, job, rec, result, rt) {
|
|
|
298
302
|
let runId = null;
|
|
299
303
|
const completedAt = rt.now();
|
|
300
304
|
try {
|
|
301
|
-
adoptRecommendation(job.rec_id, completedAt.getTime()
|
|
305
|
+
adoptRecommendation(job.rec_id, completedAt.getTime(), {
|
|
306
|
+
machineConfirmed: true,
|
|
307
|
+
actionRevision: job.job_id,
|
|
308
|
+
});
|
|
302
309
|
runId = randomUUID();
|
|
303
310
|
const evidencePackHash = createHash("sha256")
|
|
304
311
|
.update(JSON.stringify(rec.evidence))
|
|
@@ -413,6 +420,9 @@ export function rollbackApplyJob(jobId) {
|
|
|
413
420
|
if (row.status !== "APPLIED" && row.status !== "CONFIRMING") {
|
|
414
421
|
throw new Error("job cannot be rolled back from this state");
|
|
415
422
|
}
|
|
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
|
+
}
|
|
416
426
|
if (row.backup_path === null)
|
|
417
427
|
throw new Error("no backup available");
|
|
418
428
|
fs.copyFileSync(row.backup_path, row.file_ref);
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { runMeasurementPass } from "../detector/measurement.js";
|
|
2
|
+
import { ESF_EFFECT_WRITER_ENABLED } from "../effects/gate.js";
|
|
3
|
+
import { effectEngineForDb } from "../query/api/effect-service.js";
|
|
4
|
+
const INTERVAL_MS = 60 * 60 * 1000;
|
|
5
|
+
const lastPass = new WeakMap();
|
|
6
|
+
/** Probe-clock scheduling; restarting can safely replay the bounded, idempotent engine pass. */
|
|
7
|
+
export function runEffectMeasurementPass(db, now) {
|
|
8
|
+
if (!ESF_EFFECT_WRITER_ENABLED) {
|
|
9
|
+
runMeasurementPass(db, now);
|
|
10
|
+
return;
|
|
11
|
+
}
|
|
12
|
+
const previous = lastPass.get(db);
|
|
13
|
+
const current = now.getTime();
|
|
14
|
+
if (previous !== undefined && current >= previous && current - previous < INTERVAL_MS)
|
|
15
|
+
return;
|
|
16
|
+
lastPass.set(db, current);
|
|
17
|
+
try {
|
|
18
|
+
effectEngineForDb(db).runPass(now);
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
// Never expose source identities or DB contents in a background error.
|
|
22
|
+
console.warn("Effect measurement pass failed; retrying on the next scheduled pass.");
|
|
23
|
+
}
|
|
24
|
+
}
|
package/dist/daemon/http.js
CHANGED
|
@@ -138,6 +138,16 @@ 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
|
+
if (pathname === "/api/work-records" || pathname === "/api/work-records/ids")
|
|
144
|
+
return true;
|
|
145
|
+
if (/^\/api\/work-records\/[^/]+\/(edit|closeout|archive|reopen|sessions|contexts|delete)$/.test(pathname))
|
|
146
|
+
return true;
|
|
147
|
+
if (/^\/api\/work-records\/[^/]+\/(sessions|contexts)\/[^/]+\/detach$/.test(pathname))
|
|
148
|
+
return true;
|
|
149
|
+
if (pathname === "/api/work-records/allocations/recompute")
|
|
150
|
+
return true;
|
|
141
151
|
return false;
|
|
142
152
|
}
|
|
143
153
|
/**
|
package/dist/daemon/index.js
CHANGED
|
@@ -22,7 +22,6 @@ 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";
|
|
26
25
|
import { installHook, uninstallHook } from "../hook/install.js";
|
|
27
26
|
import { collectSessionChurn } from "../ingest/churn-collector.js";
|
|
28
27
|
import { runPostProbeHook, setPostIngestHook, setPostProbeHook } from "../ingest/detector-hook.js";
|
|
@@ -37,7 +36,9 @@ import { generateWeeklyReport } from "../query/api/reports.js";
|
|
|
37
36
|
import { setQueryDb } from "../query/db-context.js";
|
|
38
37
|
import { getSettingsData, setHealthInstance, setRuntimeResetHook, } from "../query/settings-store.js";
|
|
39
38
|
import { loadConfig } from "./config.js";
|
|
39
|
+
import { runEffectMeasurementPass } from "./effect-pass.js";
|
|
40
40
|
import { createServer } from "./http.js";
|
|
41
|
+
import { createDaemonLifecycle } from "./lifecycle.js";
|
|
41
42
|
import { createOutcomesPassRunner } from "./outcomes-pass.js";
|
|
42
43
|
import { setReady, setScanRoots, setScanState } from "./readiness.js";
|
|
43
44
|
const VERSION = "0.1.0";
|
|
@@ -107,6 +108,10 @@ if (isSmoke) {
|
|
|
107
108
|
"review_findings",
|
|
108
109
|
"recommendations",
|
|
109
110
|
"recommendation_effects",
|
|
111
|
+
"effect_cycles",
|
|
112
|
+
"effect_guardrail_results",
|
|
113
|
+
"effect_rollback_operations",
|
|
114
|
+
"effect_mutation_keys",
|
|
110
115
|
"apply_jobs",
|
|
111
116
|
"analysis_runs",
|
|
112
117
|
"ingest_quarantine",
|
|
@@ -150,18 +155,16 @@ function runProbePass(label) {
|
|
|
150
155
|
const now = new Date();
|
|
151
156
|
const { rows } = runContextProbe(db, now);
|
|
152
157
|
console.log(`ContextProbe[${label}]: ${rows} row(s) upserted`);
|
|
153
|
-
//
|
|
154
|
-
// through, and runMeasurementPass itself is guarded/log-not-throw).
|
|
158
|
+
// Best-effort measurement after each probe, using the same observation clock.
|
|
155
159
|
runPostProbeHook(db, now);
|
|
156
160
|
}
|
|
157
161
|
catch (e) {
|
|
158
162
|
console.log(`ContextProbe[${label}]: skipped — ${e instanceof Error ? e.message : String(e)}`);
|
|
159
163
|
}
|
|
160
164
|
}
|
|
161
|
-
//
|
|
162
|
-
// receives the probe's clock (deterministic; NFR-107), is throttled internally.
|
|
165
|
+
// Versioned measurement owns new cycles; W4 history stays read-only under the code gate.
|
|
163
166
|
setPostProbeHook((probeDb, now) => {
|
|
164
|
-
|
|
167
|
+
runEffectMeasurementPass(probeDb, now);
|
|
165
168
|
});
|
|
166
169
|
runProbePass("boot");
|
|
167
170
|
// ── Outcomes bootstrap (WP5 — best-effort, never crash daemon) ────────────────
|
|
@@ -169,10 +172,6 @@ runProbePass("boot");
|
|
|
169
172
|
// The runner (src/daemon/outcomes-pass.ts) adds a hard 15-min per-pass deadline:
|
|
170
173
|
// a hung pass can no longer wedge the poll cadence forever — the running flag
|
|
171
174
|
// always clears and the next poll resumes.
|
|
172
|
-
let outcomesTimer = null;
|
|
173
|
-
let reportsTimer = null;
|
|
174
|
-
let reportsRunning = false;
|
|
175
|
-
let bptCalibrationTimer = null;
|
|
176
175
|
const outcomesRunner = createOutcomesPassRunner({
|
|
177
176
|
db,
|
|
178
177
|
probe: () => runProbePass("poll"),
|
|
@@ -186,29 +185,31 @@ const outcomesRunner = createOutcomesPassRunner({
|
|
|
186
185
|
function runOutcomesPass() {
|
|
187
186
|
return outcomesRunner.run();
|
|
188
187
|
}
|
|
188
|
+
// R12 bytes→token calibration — best-effort, never crash the daemon.
|
|
189
|
+
// Runs on boot (deferred) and weekly when: opt-in enabled AND ratio is absent
|
|
190
|
+
// or older than 30 days. Mirrors the outcomes bootstrap pattern.
|
|
191
|
+
const THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1000;
|
|
192
|
+
function runBptCalibrationIfStale() {
|
|
193
|
+
const enabled = bptConfigGet(db, "bytes_per_token_calibration_enabled");
|
|
194
|
+
if (enabled !== "true")
|
|
195
|
+
return;
|
|
196
|
+
const measuredAt = bptConfigGet(db, "bytes_per_token_measured_at");
|
|
197
|
+
if (measuredAt !== null && Date.now() - new Date(measuredAt).getTime() < THIRTY_DAYS_MS)
|
|
198
|
+
return;
|
|
199
|
+
calibrateBytesPerToken(db).catch((e) => {
|
|
200
|
+
console.error(`Bytes-per-token calibration failed (non-fatal): ${e instanceof Error ? e.message : String(e)}`);
|
|
201
|
+
});
|
|
202
|
+
}
|
|
189
203
|
// ── Deferred boot scan (runs AFTER the HTTP port is bound) ───────────────────
|
|
190
204
|
// startTailBatched() performs a yielding initial back-scan (ingestAllKnown +
|
|
191
205
|
// reconcile), then arms the tail/discovery timers. We do NOT
|
|
192
206
|
// also call runBackscan — that would spin up a second Ingestor whose separate
|
|
193
207
|
// Health instance is the one surfaced in Settings. One Ingestor, one Health.
|
|
194
208
|
// Bounded: if ingestion fails to start, still serve the dashboard (degraded).
|
|
195
|
-
|
|
196
|
-
//
|
|
197
|
-
// requests or timers fire kickBootScan concurrently.
|
|
198
|
-
let scanKicked = false;
|
|
199
|
-
function kickBootScan() {
|
|
200
|
-
if (scanKicked)
|
|
201
|
-
return;
|
|
202
|
-
scanKicked = true;
|
|
203
|
-
// Use setImmediate so the triggering HTTP response flushes before boot work
|
|
204
|
-
// starts. The scan itself also yields between bounded batches.
|
|
205
|
-
setImmediate(() => {
|
|
206
|
-
runBootScan().catch((e) => {
|
|
207
|
-
console.error(`Boot scan failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
208
|
-
});
|
|
209
|
-
});
|
|
210
|
-
}
|
|
209
|
+
// The one-shot kick guard, the daemon-owned intervals, and shutdown ordering
|
|
210
|
+
// live in the lifecycle harness (src/daemon/lifecycle.ts).
|
|
211
211
|
async function runBootScan() {
|
|
212
|
+
let handle = null;
|
|
212
213
|
try {
|
|
213
214
|
const scanRoots = getSettingsData(db).scan_roots;
|
|
214
215
|
setScanRoots(scanRoots);
|
|
@@ -261,21 +262,8 @@ async function runBootScan() {
|
|
|
261
262
|
console.error(`Self-churn collection failed (non-fatal): ${e instanceof Error ? e.message : String(e)}`);
|
|
262
263
|
}
|
|
263
264
|
});
|
|
264
|
-
//
|
|
265
|
-
//
|
|
266
|
-
// or older than 30 days. Mirrors the outcomes bootstrap pattern.
|
|
267
|
-
const THIRTY_DAYS_MS = 30 * 24 * 60 * 60 * 1000;
|
|
268
|
-
function runBptCalibrationIfStale() {
|
|
269
|
-
const enabled = bptConfigGet(db, "bytes_per_token_calibration_enabled");
|
|
270
|
-
if (enabled !== "true")
|
|
271
|
-
return;
|
|
272
|
-
const measuredAt = bptConfigGet(db, "bytes_per_token_measured_at");
|
|
273
|
-
if (measuredAt !== null && Date.now() - new Date(measuredAt).getTime() < THIRTY_DAYS_MS)
|
|
274
|
-
return;
|
|
275
|
-
calibrateBytesPerToken(db).catch((e) => {
|
|
276
|
-
console.error(`Bytes-per-token calibration failed (non-fatal): ${e instanceof Error ? e.message : String(e)}`);
|
|
277
|
-
});
|
|
278
|
-
}
|
|
265
|
+
// Deferred boot calibration check (the weekly interval is armed by the
|
|
266
|
+
// lifecycle harness after this scan resolves).
|
|
279
267
|
setImmediate(() => {
|
|
280
268
|
try {
|
|
281
269
|
runBptCalibrationIfStale();
|
|
@@ -284,42 +272,31 @@ async function runBootScan() {
|
|
|
284
272
|
console.error(`Bytes-per-token calibration boot failed (non-fatal): ${e instanceof Error ? e.message : String(e)}`);
|
|
285
273
|
}
|
|
286
274
|
});
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
outcomesTimer = setInterval(() => {
|
|
275
|
+
return handle;
|
|
276
|
+
}
|
|
277
|
+
// ── Lifecycle harness: boot-kick guard, daemon intervals, shutdown ordering ──
|
|
278
|
+
// closeServer closes over `server` (declared below); shutdown only ever runs
|
|
279
|
+
// after listen, so the reference is initialized by then.
|
|
280
|
+
const lifecycle = createDaemonLifecycle({
|
|
281
|
+
bootScan: runBootScan,
|
|
282
|
+
calibrationTick: runBptCalibrationIfStale,
|
|
283
|
+
outcomesTick: () => {
|
|
297
284
|
runOutcomesPass().catch((e) => {
|
|
298
285
|
console.error(`Outcomes poll failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
299
286
|
});
|
|
300
|
-
},
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
await Promise.resolve(generateWeeklyReport(db, new Date()));
|
|
307
|
-
}
|
|
308
|
-
catch (e) {
|
|
309
|
-
console.error(`Weekly report poll failed: ${e instanceof Error ? e.message : String(e)}`);
|
|
310
|
-
}
|
|
311
|
-
finally {
|
|
312
|
-
reportsRunning = false;
|
|
313
|
-
}
|
|
314
|
-
}, 7 * 24 * 60 * 60 * 1000);
|
|
315
|
-
}
|
|
287
|
+
},
|
|
288
|
+
reportsTick: () => generateWeeklyReport(db, new Date()),
|
|
289
|
+
closeServer: (onClosed) => server.close(onClosed),
|
|
290
|
+
closeDb: () => db.close(),
|
|
291
|
+
onClosed: () => process.exit(0),
|
|
292
|
+
});
|
|
316
293
|
// ── 4. Start HTTP server ──────────────────────────────────────────────────────
|
|
317
294
|
// Bind the port BEFORE the back-scan so the loading page is delivered
|
|
318
295
|
// immediately while the initial scan yields between file batches.
|
|
319
296
|
// Generate a random in-memory session token for the CSRF token gate.
|
|
320
297
|
// NEVER log it, NEVER persist it — it lives only in process memory.
|
|
321
298
|
const sessionToken = randomUUID();
|
|
322
|
-
const server = createServer(db, config.port, config.uiRoot, sessionToken, kickBootScan);
|
|
299
|
+
const server = createServer(db, config.port, config.uiRoot, sessionToken, lifecycle.kickBootScan);
|
|
323
300
|
server.listen(config.port, "127.0.0.1", () => {
|
|
324
301
|
const url = `http://127.0.0.1:${config.port}`;
|
|
325
302
|
console.log(`Listening on ${url}`);
|
|
@@ -332,7 +309,7 @@ server.listen(config.port, "127.0.0.1", () => {
|
|
|
332
309
|
// Fallback: kick the back-scan after 3 s even if no browser request arrives
|
|
333
310
|
// (headless mode, --no-open, CI). The one-shot guard in kickBootScan() makes
|
|
334
311
|
// whichever trigger fires first the only one that runs the scan.
|
|
335
|
-
|
|
312
|
+
lifecycle.armBootFallback();
|
|
336
313
|
});
|
|
337
314
|
/** Open `url` in the system default browser (best-effort; never crashes the daemon). */
|
|
338
315
|
function openBrowser(url) {
|
|
@@ -355,24 +332,8 @@ function openBrowser(url) {
|
|
|
355
332
|
// ── Graceful shutdown ─────────────────────────────────────────────────────────
|
|
356
333
|
process.on("SIGINT", () => {
|
|
357
334
|
console.log("Shutting down…");
|
|
358
|
-
|
|
359
|
-
if (outcomesTimer !== null)
|
|
360
|
-
clearInterval(outcomesTimer);
|
|
361
|
-
if (reportsTimer !== null)
|
|
362
|
-
clearInterval(reportsTimer);
|
|
363
|
-
server.close(() => {
|
|
364
|
-
db.close();
|
|
365
|
-
process.exit(0);
|
|
366
|
-
});
|
|
335
|
+
lifecycle.shutdown();
|
|
367
336
|
});
|
|
368
337
|
process.on("SIGTERM", () => {
|
|
369
|
-
|
|
370
|
-
if (outcomesTimer !== null)
|
|
371
|
-
clearInterval(outcomesTimer);
|
|
372
|
-
if (reportsTimer !== null)
|
|
373
|
-
clearInterval(reportsTimer);
|
|
374
|
-
server.close(() => {
|
|
375
|
-
db.close();
|
|
376
|
-
process.exit(0);
|
|
377
|
-
});
|
|
338
|
+
lifecycle.shutdown();
|
|
378
339
|
});
|