agentwrangler 0.1.0 → 0.1.1-next.2f6c1df
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 -94
- package/dist/apply/jobs.js +6 -2
- package/dist/daemon/index.js +9 -3
- package/dist/daemon/readiness.js +26 -1
- package/dist/daemon/router.js +4 -2
- package/dist/db/migrations/016_ingest_offset_file_version.sql +8 -0
- package/dist/db/migrations/017_ingest_metric_events.sql +22 -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 +3 -2
- package/dist/hook/precompact-checkpoint-hook.mjs +74 -25
- package/dist/ingest/ingestor.js +151 -62
- package/dist/ingest/pricing.js +15 -0
- package/dist/ingest/tail.js +55 -11
- package/dist/query/api/cost-per-success.js +13 -1
- package/dist/query/api/delivery.js +25 -3
- package/dist/query/api/effectiveness.js +30 -5
- package/dist/query/api/efficiency-headroom.js +12 -14
- package/dist/query/api/outcomes.js +2 -1
- package/dist/query/api/overview.js +90 -6
- package/dist/query/settings-store.js +2 -0
- package/dist/query/spend.js +64 -2
- package/dist/ui/assets/BarChart-BC-ciS9d.js +3 -0
- package/dist/ui/assets/BriefsPage-Dls6npt7.js +2 -0
- package/dist/ui/assets/CacheWriteSpikesChart-ncVwVoud.js +1 -0
- package/dist/ui/assets/CartesianChart-CRV6h2et.js +84 -0
- package/dist/ui/assets/Chip-EizRm13x.js +1 -0
- package/dist/ui/assets/ComposedChart-CrOxp6Ym.js +1 -0
- package/dist/ui/assets/EmptyState-9kcoimjj.js +1 -0
- package/dist/ui/assets/FlavorDecomposition-DRZxH0vQ.js +1 -0
- package/dist/ui/assets/FrictionCell-BGYB9UKh.js +2 -0
- package/dist/ui/assets/GlossaryPage-CRyQb1Hn.js +1 -0
- package/dist/ui/assets/HotSessionsPage-DfE90Kxi.js +1 -0
- package/dist/ui/assets/InfoTip-CmcYFNQt.js +1 -0
- package/dist/ui/assets/Legend-BCXsSaEM.js +5 -0
- package/dist/ui/assets/Line-2rUzs15v.js +1 -0
- package/dist/ui/assets/OverviewPage-Bm5KvXqa.js +2 -0
- package/dist/ui/assets/RecommendationsPage-CvSl3lzp.js +2 -0
- package/dist/ui/assets/Scatter-DpIZt_y5.js +1 -0
- package/dist/ui/assets/SessionDetailPage-w_lSgite.js +1 -0
- package/dist/ui/assets/SettingsPage-CfUoP8lB.js +25 -0
- package/dist/ui/assets/Skeleton-DjuxPm_D.js +1 -0
- package/dist/ui/assets/SpendPercentileChip-DYun7Wzh.js +1 -0
- package/dist/ui/assets/TrendChart-gyhHK9H_.js +1 -0
- package/dist/ui/assets/WorkspaceDetailPage-DacPKaKA.js +1 -0
- package/dist/ui/assets/WorkspacesPage-DiNQaAYG.js +1 -0
- package/dist/ui/assets/chart-theme-DnpNuyRt.js +1 -0
- package/dist/ui/assets/graphicalItemSelectors-CFhpQjLz.js +1 -0
- package/dist/ui/assets/index-D_IJsvkJ.css +1 -0
- package/dist/ui/assets/index-hbaLYHUX.js +41 -0
- package/dist/ui/assets/prompt-templates-CVzMex9L.js +13 -0
- package/dist/ui/assets/rec-sessions-ggz9MYgP.js +1 -0
- package/dist/ui/assets/useExperimentalActions-B7J8gIkb.js +1 -0
- package/dist/ui/assets/workspace-label-CysPJNc0.js +1 -0
- package/dist/ui/index.html +2 -2
- package/package.json +21 -5
- package/dist/ui/assets/index-DnRKgc21.css +0 -1
- package/dist/ui/assets/index-h1Q1wWq5.js +0 -168
package/README.md
CHANGED
|
@@ -1,116 +1,104 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
actually merged or closed** — entirely on your machine — and ships **installable guardrails** (a
|
|
5
|
-
dangerous-command block, a pre-compaction checkpoint, a context-budget warning) you turn on from the
|
|
6
|
-
dashboard. See where your agent budget goes, whether the work shipped, and cut the waste.
|
|
7
|
-
|
|
8
|
-
## Local-only — your data never leaves your machine
|
|
9
|
-
|
|
10
|
-
AgentWrangler runs entirely on your computer: a daemon bound to **`127.0.0.1`** and a dashboard in
|
|
11
|
-
your browser that talks only to that loopback address. **No cloud backend, no telemetry, nothing
|
|
12
|
-
phones home.** It reads your local Claude Code transcripts, stores only aggregates in a local SQLite
|
|
13
|
-
file (`~/.agentwrangler/db.sqlite`), and never persists raw transcript or PR content — only counts,
|
|
14
|
-
ids, and structural anchors (the SEC-101 privacy invariant). An optional GitHub token, used only for
|
|
15
|
-
the outcomes feature, is read locally, never logged, and never leaves your machine.
|
|
1
|
+
<p align="center">
|
|
2
|
+
<img src="https://raw.githubusercontent.com/Doogit/AgentWrangler/main/docs/assets/logo.png" alt="AgentWrangler logo" width="200">
|
|
3
|
+
</p>
|
|
16
4
|
|
|
17
|
-
|
|
18
|
-
content leaves the machine. It calls the Claude API with your local Claude Code OAuth credential to
|
|
19
|
-
adjudicate G2 deferral findings, and runs only when `g2_claude_judge_opt_in` is enabled; otherwise it
|
|
20
|
-
refuses to run. No rationale text is persisted.
|
|
21
|
-
|
|
22
|
-

|
|
5
|
+
# AgentWrangler
|
|
23
6
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
Regenerate with the ce-demo-reel skill against `npx vite --mode test`.
|
|
28
|
-
-->
|
|
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.
|
|
29
10
|
|
|
30
11
|
## Quick start
|
|
31
12
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
```sh
|
|
35
|
-
git clone https://github.com/Doogit/AgentWrangler && cd AgentWrangler
|
|
36
|
-
npm ci
|
|
37
|
-
npm run build:ui # build the dashboard
|
|
38
|
-
npm run daemon # starts the daemon and opens your browser
|
|
39
|
-
```
|
|
40
|
-
|
|
41
|
-
Then open **http://127.0.0.1:47821** (the daemon opens it for you unless `AW_NO_OPEN=1`).
|
|
42
|
-
|
|
43
|
-
After installing dependencies you can also do it in one step:
|
|
13
|
+
Requires **Node 22-24 and npm**, plus Claude Code transcripts for populated charts.
|
|
44
14
|
|
|
45
15
|
```sh
|
|
46
|
-
|
|
47
|
-
npx agentwrangler # builds the UI if needed, then launches the daemon + browser
|
|
16
|
+
npx agentwrangler@latest
|
|
48
17
|
```
|
|
49
18
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
## Optional setup
|
|
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.
|
|
55
23
|
|
|
56
|
-
|
|
57
|
-
GitHub PAT via the `AW_GITHUB_TOKEN` environment variable (works on all platforms). On Windows you
|
|
58
|
-
may instead store it in Credential Manager as `AgentWrangler-GithubToken`. Without a token the
|
|
59
|
-
outcomes feature stays inert and Settings tells you so — nothing fails silently.
|
|
60
|
-
- **Usage reader** — reads your Claude Code OAuth credentials locally to calibrate the weekly limit
|
|
61
|
-
and burn forecast. Sign in through Claude Code as usual; Settings shows the reader status.
|
|
62
|
-
- **Context-budget hook** — an optional PreToolUse hook that warns before long sessions balloon.
|
|
63
|
-
Install it from the Settings page (or `npm run install-hook`).
|
|
24
|
+
[Install from source, configure scan roots, or troubleshoot](https://github.com/Doogit/AgentWrangler/blob/main/docs/getting-started.md).
|
|
64
25
|
|
|
65
|
-
##
|
|
26
|
+
## Your first five minutes
|
|
66
27
|
|
|
67
|
-
|
|
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.
|
|
68
43
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
| `AW_SCAN_ROOT` | Transcript corpus to scan | `~/.claude/projects` |
|
|
74
|
-
| `AW_UI_ROOT` | Directory the built UI is served from | `<repo>/dist/ui` |
|
|
75
|
-
| `AW_GITHUB_TOKEN` | Read-only GitHub PAT for outcomes sync | *(unset)* |
|
|
76
|
-
| `AW_NO_OPEN` | Set to `1` to not auto-open the browser | *(unset)* |
|
|
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.
|
|
77
48
|
|
|
78
|
-
|
|
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).
|
|
79
50
|
|
|
80
|
-
|
|
81
|
-
findings (e.g. long sessions never `/clear`ed, limit-burn risk), and — with a GitHub token — outcome
|
|
82
|
-
linkage between sessions and the work they shipped. Numbers are cap-weighted and honesty-tiered: the
|
|
83
|
-
UI is explicit about what is measured versus estimated, and never invents a proxy it can't ground.
|
|
84
|
-
Definitions live in [`docs/planning/AgentWrangler_Data_Model_and_Metrics_v2.md`](docs/planning/AgentWrangler_Data_Model_and_Metrics_v2.md).
|
|
51
|
+
## Dashboard preview
|
|
85
52
|
|
|
86
|
-
|
|
53
|
+

|
|
87
54
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
to that format upstream can require an ingestion update.
|
|
91
|
-
- **Outcomes credential sources.** The cross-platform path is `AW_GITHUB_TOKEN`; the OS credential
|
|
92
|
-
store integration currently covers Windows Credential Manager only (macOS/Linux keychain is a
|
|
93
|
-
follow-on).
|
|
94
|
-
- **Single local user.** It observes one machine's Claude Code history; there is no multi-user or
|
|
95
|
-
team-aggregation mode by design.
|
|
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.
|
|
96
57
|
|
|
97
|
-
##
|
|
58
|
+
## Pick a question
|
|
98
59
|
|
|
99
|
-
|
|
60
|
+
| I want to... | Start here |
|
|
61
|
+
|---|---|
|
|
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) |
|
|
100
68
|
|
|
101
|
-
|
|
102
|
-
vulnerability, and [CODE_OF_CONDUCT.md](CODE_OF_CONDUCT.md).
|
|
69
|
+
## Installable guardrails — local checks inside Claude Code, before the waste
|
|
103
70
|
|
|
104
|
-
|
|
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.
|
|
105
73
|
|
|
106
|
-
|
|
|
107
|
-
|
|
108
|
-
|
|
|
109
|
-
|
|
|
110
|
-
|
|
|
111
|
-
|
|
|
112
|
-
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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;
|
package/dist/daemon/index.js
CHANGED
|
@@ -35,11 +35,11 @@ import { linkSessions } from "../outcomes/linker.js";
|
|
|
35
35
|
import { syncAllWorkspaces } from "../outcomes/sync.js";
|
|
36
36
|
import { generateWeeklyReport } from "../query/api/reports.js";
|
|
37
37
|
import { setQueryDb } from "../query/db-context.js";
|
|
38
|
-
import { setHealthInstance, setRuntimeResetHook } from "../query/settings-store.js";
|
|
38
|
+
import { getSettingsData, setHealthInstance, setRuntimeResetHook, } from "../query/settings-store.js";
|
|
39
39
|
import { loadConfig } from "./config.js";
|
|
40
40
|
import { createServer } from "./http.js";
|
|
41
41
|
import { createOutcomesPassRunner } from "./outcomes-pass.js";
|
|
42
|
-
import { setReady } from "./readiness.js";
|
|
42
|
+
import { setReady, setScanRoots, setScanState } from "./readiness.js";
|
|
43
43
|
const VERSION = "0.1.0";
|
|
44
44
|
/** Parse a named CLI argument: --name value or --name=value. */
|
|
45
45
|
function getArg(name) {
|
|
@@ -110,6 +110,8 @@ if (isSmoke) {
|
|
|
110
110
|
"apply_jobs",
|
|
111
111
|
"analysis_runs",
|
|
112
112
|
"ingest_quarantine",
|
|
113
|
+
"ingest_metric_events",
|
|
114
|
+
"ingest_metric_baselines",
|
|
113
115
|
"ingest_offsets",
|
|
114
116
|
"schema_migrations",
|
|
115
117
|
"user_config",
|
|
@@ -208,7 +210,9 @@ function kickBootScan() {
|
|
|
208
210
|
}
|
|
209
211
|
async function runBootScan() {
|
|
210
212
|
try {
|
|
211
|
-
const
|
|
213
|
+
const scanRoots = getSettingsData(db).scan_roots;
|
|
214
|
+
setScanRoots(scanRoots);
|
|
215
|
+
const ingestor = new Ingestor(db, scanRoots, {
|
|
212
216
|
onNewMappings: (count) => {
|
|
213
217
|
console.log(`Discovery mapped ${count} new repo(s) — scheduling outcomes pass`);
|
|
214
218
|
runOutcomesPass().catch((e) => {
|
|
@@ -218,10 +222,12 @@ async function runBootScan() {
|
|
|
218
222
|
});
|
|
219
223
|
setHealthInstance(ingestor.health);
|
|
220
224
|
handle = await ingestor.startTailBatched();
|
|
225
|
+
setScanState("complete");
|
|
221
226
|
setRuntimeResetHook(() => ingestor.clearRuntimeState());
|
|
222
227
|
console.log(`Ingestion: initial scan complete — health ${JSON.stringify(ingestor.healthSnapshot())}`);
|
|
223
228
|
}
|
|
224
229
|
catch (e) {
|
|
230
|
+
setScanState("failed");
|
|
225
231
|
console.error(`Ingestion failed to start — serving dashboard in degraded mode: ${e instanceof Error ? e.message : String(e)}`);
|
|
226
232
|
}
|
|
227
233
|
// After the initial back-scan, workspaces are discovered — re-probe so per-workspace
|
package/dist/daemon/readiness.js
CHANGED
|
@@ -4,7 +4,32 @@
|
|
|
4
4
|
* Set to true by index.ts after the initial back-scan completes.
|
|
5
5
|
* Read by http.ts to decide whether to serve the loading page or the real SPA.
|
|
6
6
|
*/
|
|
7
|
-
|
|
7
|
+
import * as fs from "node:fs";
|
|
8
|
+
const state = { ready: false, scan: "scanning", scanRoots: [] };
|
|
9
|
+
/** Snapshot the roots actually used by the running ingestor, not pending settings. */
|
|
10
|
+
export function setScanRoots(roots) {
|
|
11
|
+
state.scanRoots = [...roots];
|
|
12
|
+
}
|
|
13
|
+
export function setScanState(scan) {
|
|
14
|
+
state.scan = scan;
|
|
15
|
+
}
|
|
16
|
+
/** Aggregate-only diagnostics; never exposes a local path or filesystem error. */
|
|
17
|
+
export function getScanStatus() {
|
|
18
|
+
let invalid = 0;
|
|
19
|
+
for (const root of state.scanRoots) {
|
|
20
|
+
try {
|
|
21
|
+
if (!fs.statSync(root).isDirectory()) {
|
|
22
|
+
invalid += 1;
|
|
23
|
+
continue;
|
|
24
|
+
}
|
|
25
|
+
fs.accessSync(root, fs.constants.R_OK);
|
|
26
|
+
}
|
|
27
|
+
catch {
|
|
28
|
+
invalid += 1;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return { scan_state: state.scan, invalid_scan_root_count: invalid };
|
|
32
|
+
}
|
|
8
33
|
/** Returns true after the initial back-scan + detector pass have finished. */
|
|
9
34
|
export function isReady() {
|
|
10
35
|
return state.ready;
|
package/dist/daemon/router.js
CHANGED
|
@@ -26,7 +26,7 @@ import { getSessionSpendPercentile, getWeeklySelfPercentile, } from "../query/ap
|
|
|
26
26
|
import { getFlavorDecomposition } from "../query/api/spend-flavor.js";
|
|
27
27
|
import { getCacheWriteTrend, getTrends } from "../query/api/trends.js";
|
|
28
28
|
import { getSettingsData } from "../query/settings-store.js";
|
|
29
|
-
import { isReady } from "./readiness.js";
|
|
29
|
+
import { getScanStatus, isReady } from "./readiness.js";
|
|
30
30
|
function sendJson(res, status, body) {
|
|
31
31
|
const payload = JSON.stringify(body);
|
|
32
32
|
res.writeHead(status, {
|
|
@@ -264,7 +264,7 @@ export function handleApiRequest(_db, req, res, method, url) {
|
|
|
264
264
|
sendJson(res, 200, getPractices(_db, { from, to }));
|
|
265
265
|
return;
|
|
266
266
|
}
|
|
267
|
-
// GET /api/efficiency-headroom (
|
|
267
|
+
// GET /api/efficiency-headroom (individual weekly estimates; separate selected-window spend)
|
|
268
268
|
if (method === "GET" && pathname === "/api/efficiency-headroom") {
|
|
269
269
|
const { from, to } = resolveWindow(parseWindowFilter(url));
|
|
270
270
|
sendJson(res, 200, getEfficiencyHeadroom(_db, { from, to }));
|
|
@@ -741,6 +741,8 @@ export function handleApiRequest(_db, req, res, method, url) {
|
|
|
741
741
|
sessions,
|
|
742
742
|
files_seen: parser_health.files_seen,
|
|
743
743
|
files_parsed: parser_health.files_parsed,
|
|
744
|
+
lines_quarantined: parser_health.lines_quarantined,
|
|
745
|
+
...getScanStatus(),
|
|
744
746
|
});
|
|
745
747
|
return;
|
|
746
748
|
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
-- Nullable metadata lets existing offsets establish a baseline on their next read.
|
|
2
|
+
-- No transcript content is stored here. Identity detects path replacement; timestamps
|
|
3
|
+
-- detect equal-size in-place rewrites beyond the existing 256-byte head fingerprint.
|
|
4
|
+
ALTER TABLE ingest_offsets ADD COLUMN file_size INTEGER;
|
|
5
|
+
ALTER TABLE ingest_offsets ADD COLUMN file_dev TEXT;
|
|
6
|
+
ALTER TABLE ingest_offsets ADD COLUMN file_ino TEXT;
|
|
7
|
+
ALTER TABLE ingest_offsets ADD COLUMN file_mtime_ms REAL;
|
|
8
|
+
ALTER TABLE ingest_offsets ADD COLUMN file_ctime_ms REAL;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
-- Persist structural metric events so transcript replay cannot inflate session counters.
|
|
2
|
+
-- event_id is a SHA-256 digest computed in-process; transcript content is never stored.
|
|
3
|
+
CREATE TABLE ingest_metric_events (
|
|
4
|
+
event_id TEXT PRIMARY KEY,
|
|
5
|
+
session_id TEXT NOT NULL,
|
|
6
|
+
user_turn_ts TEXT,
|
|
7
|
+
is_user_turn INTEGER NOT NULL CHECK (is_user_turn IN (0, 1)),
|
|
8
|
+
is_compact_summary INTEGER NOT NULL CHECK (is_compact_summary IN (0, 1)),
|
|
9
|
+
is_api_error INTEGER NOT NULL CHECK (is_api_error IN (0, 1)),
|
|
10
|
+
is_interrupt INTEGER NOT NULL CHECK (is_interrupt IN (0, 1))
|
|
11
|
+
);
|
|
12
|
+
|
|
13
|
+
CREATE INDEX idx_ingest_metric_events_session_user_ts
|
|
14
|
+
ON ingest_metric_events(session_id, user_turn_ts)
|
|
15
|
+
WHERE is_user_turn = 1;
|
|
16
|
+
|
|
17
|
+
-- Existing offsets predate the event ledger. Their consumed prefixes are seeded into
|
|
18
|
+
-- the ledger without incrementing counters, then this durable boundary is advanced.
|
|
19
|
+
CREATE TABLE ingest_metric_baselines (
|
|
20
|
+
file_path TEXT PRIMARY KEY,
|
|
21
|
+
seeded_offset INTEGER NOT NULL CHECK (seeded_offset >= 0)
|
|
22
|
+
);
|
|
@@ -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
|
|
98
|
+
title: `Review tool catalog: ${Math.round(catalogTokens / 1000)}K estimated 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: "The tool catalog exceeds its size target; actual loaded context is not measured",
|
|
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 current
|
|
34
|
-
"
|
|
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",
|
|
35
35
|
];
|
|
36
36
|
case "MEMORY":
|
|
37
37
|
return [
|
|
38
38
|
`Review memory files under ${fileRef}`,
|
|
39
|
-
"
|
|
40
|
-
"
|
|
41
|
-
"
|
|
39
|
+
"Remove old or duplicate memories",
|
|
40
|
+
"Combine overlapping facts into concise entries",
|
|
41
|
+
"Check the size again after the next local scan",
|
|
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 changelogs and history out of CLAUDE.md; keep current rules and links.";
|
|
66
66
|
case "MEMORY":
|
|
67
|
-
return "
|
|
67
|
+
return "Remove old or duplicate memories and combine 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 "Shorten this content that is loaded into every conversation.";
|
|
72
72
|
}
|
|
73
73
|
}
|
|
74
74
|
export const d1Detector = {
|
|
@@ -81,12 +81,12 @@ export const d2Detector = {
|
|
|
81
81
|
scopeKey: `D2|global|${formula.model}`,
|
|
82
82
|
category: "CONTEXT",
|
|
83
83
|
scope_workspace_id: null,
|
|
84
|
-
lever: "/clear between unrelated tasks
|
|
84
|
+
lever: "Use /clear between unrelated tasks, split long work into separate sessions, and avoid automatic compaction while you are working.",
|
|
85
85
|
target_metric: "avg_context_per_turn",
|
|
86
86
|
modeled_savings_u_per_wk: savingsU,
|
|
87
87
|
modeled_formula: formula,
|
|
88
88
|
evidence: {
|
|
89
|
-
title: `
|
|
89
|
+
title: `Split ${n} long session${n === 1 ? "" : "s"} this week`,
|
|
90
90
|
qualifying_session_count: qualifying.length,
|
|
91
91
|
qualifying_turn_count: qualifyingTurnCount,
|
|
92
92
|
session_ids: sessionIds,
|
|
@@ -192,7 +192,7 @@ export const d4Detector = {
|
|
|
192
192
|
? {
|
|
193
193
|
withheld: true,
|
|
194
194
|
withheld_reason: `Sonnet weekly cap is the binding constraint (Sonnet util ${bindingSonnet.utilization} >= all-models ${perModelSnapshot.seven_day_util}) — routing Opus->Sonnet would worsen it`,
|
|
195
|
-
title: `
|
|
195
|
+
title: `Model change is not recommended: ${mismatchPct}% of Opus turns have large context and little output`,
|
|
196
196
|
}
|
|
197
197
|
: perModelSnapshot && sonnetEntries && sonnetEntries.length > 0
|
|
198
198
|
? { cap_attribution: "all_models_or_opus_binds" }
|
|
@@ -202,7 +202,7 @@ export const d4Detector = {
|
|
|
202
202
|
category: "MODEL",
|
|
203
203
|
scope_workspace_id: workspace_id,
|
|
204
204
|
// Advisory gate (W0.3): which cap binds is NOT inferable from JSONL. Conditional lever.
|
|
205
|
-
lever: "If your
|
|
205
|
+
lever: "Check /usage first. If your overall, Opus, or 5-hour limit is filling, these large-context Opus turns with little output may be suitable for Sonnet. Do not switch if your Sonnet weekly limit is the one filling.",
|
|
206
206
|
target_metric: "model_mix_opus_fraction",
|
|
207
207
|
// Advisory gate: suppress the crisp $/wk headline until live /usage cap-attribution exists.
|
|
208
208
|
modeled_savings_u_per_wk: null,
|
|
@@ -210,7 +210,7 @@ export const d4Detector = {
|
|
|
210
210
|
// Destructure out result_usd_per_wk so the advisory formula carries no crisp $/wk figure.
|
|
211
211
|
modeled_formula: (({ result_usd_per_wk: _, ...rest }) => ({ ...rest, kind: "ADVISORY" }))(formula),
|
|
212
212
|
evidence: {
|
|
213
|
-
title: `
|
|
213
|
+
title: `Review model choice: ${mismatchPct}% of Opus turns have large context and little output`,
|
|
214
214
|
workspace_id,
|
|
215
215
|
total_opus_turns_per_week: totalOpus,
|
|
216
216
|
mismatch_turns_per_week: mismatchCount,
|
|
@@ -262,7 +262,7 @@ export const d6Detector = {
|
|
|
262
262
|
modeled_savings_u_per_wk: modeledSavingsU,
|
|
263
263
|
modeled_formula: formula,
|
|
264
264
|
evidence: {
|
|
265
|
-
title: `
|
|
265
|
+
title: `Reduce large tool results: ${Math.round(bloatShare * 100)}% of this session's context`,
|
|
266
266
|
session_id: row.session_id,
|
|
267
267
|
workspace_id: row.workspace_id,
|
|
268
268
|
tool_result_bytes: row.tool_result_bytes,
|
|
@@ -299,7 +299,7 @@ export const d7Detector = {
|
|
|
299
299
|
expression: "cap-weighted exposure of turns owning repeat-excess events; not an avoidable-token or USD savings estimate",
|
|
300
300
|
},
|
|
301
301
|
evidence: {
|
|
302
|
-
title: `
|
|
302
|
+
title: `Stop repeated attempts: ${flaggedTurns.size} affected turn${flaggedTurns.size === 1 ? "" : "s"} in this session`,
|
|
303
303
|
session_id: sessionId,
|
|
304
304
|
workspace_id: session.workspaceId,
|
|
305
305
|
loop_flagged_event_count: flaggedEventIds.size,
|
|
@@ -125,7 +125,7 @@ export const d8Detector = {
|
|
|
125
125
|
// TTL-regime facet: is this session dominated by 5m-tier creation?
|
|
126
126
|
const fiveMinCreation = events.reduce((s, e) => s + e.cache_write_5m, 0);
|
|
127
127
|
const regime5m = totalCreation > 0 && fiveMinCreation / totalCreation >= D8_REGIME_5M_SHARE;
|
|
128
|
-
const baseLever = "
|
|
128
|
+
const baseLever = "Before a long break, use /clear or resume from a summary. Make instruction-file changes between sessions so they do not rebuild the cache while you work.";
|
|
129
129
|
const lever = regime5m
|
|
130
130
|
? `${baseLever} This session's creation is mostly 5m-tier — enable the 1h cache regime (ENABLE_PROMPT_CACHING_1H) where long pauses are unavoidable.`
|
|
131
131
|
: baseLever;
|
|
@@ -138,7 +138,7 @@ export const d8Detector = {
|
|
|
138
138
|
modeled_savings_u_per_wk: savingsU,
|
|
139
139
|
modeled_formula: formula,
|
|
140
140
|
evidence: {
|
|
141
|
-
title: `
|
|
141
|
+
title: `Avoid repeated cache rebuilds: ${events.length} after a long pause`,
|
|
142
142
|
session_id: sessionId,
|
|
143
143
|
workspace_id,
|
|
144
144
|
churn_event_count: events.length,
|
|
@@ -62,7 +62,7 @@ export const d9Detector = {
|
|
|
62
62
|
modeled_savings_u_per_wk: null,
|
|
63
63
|
modeled_formula: d9Formula(sidechainCap, D9_UNPRODUCTIVE_FRACTION),
|
|
64
64
|
evidence: {
|
|
65
|
-
title: `Review background
|
|
65
|
+
title: `Review background-agent work: ${Math.round(share * 100)}% of estimated limit use`,
|
|
66
66
|
workspace_id: row.workspace_id,
|
|
67
67
|
sidechain_cap_weighted_tokens: sidechainCap,
|
|
68
68
|
total_cap_weighted_tokens: totalCap,
|
|
@@ -20,6 +20,7 @@
|
|
|
20
20
|
*
|
|
21
21
|
* Injected `now: Date` everywhere — never call new Date() in this module.
|
|
22
22
|
*/
|
|
23
|
+
import { PREMIUM_MODEL_SQL } from "../ingest/pricing.js";
|
|
23
24
|
import { GLOBAL_WORKSPACE_ID } from "./context-probe.js";
|
|
24
25
|
import { isD1SourceBackedRecommendation, parseD1SourceIdentity } from "./d1-source-identity.js";
|
|
25
26
|
/** Wait ≥1 day after adoption before entering MEASURING. */
|
|
@@ -82,14 +83,14 @@ function avgFloorContext(db, scopeWorkspaceId, fromIso, toIso) {
|
|
|
82
83
|
/**
|
|
83
84
|
* D4 signal: routing-adherence score = ROUND(100 × (1 − premium_share)) over
|
|
84
85
|
* non-sidechain reconciled turns in [fromIso, toIso), where premium = model
|
|
85
|
-
*
|
|
86
|
+
* matches PREMIUM_MODEL_SQL (Opus/Fable/Mythos). The data-model's "mechanical turn" classifier is not yet
|
|
86
87
|
* shipped, so all reconciled turns are the mechanical set (conservative proxy —
|
|
87
88
|
* documented in the W4 build digest).
|
|
88
89
|
*/
|
|
89
90
|
function routingAdherenceScore(db, scopeWorkspaceId, fromIso, toIso) {
|
|
90
91
|
const scoped = scopeWorkspaceId !== null;
|
|
91
92
|
const sql = `SELECT COUNT(*) AS n,
|
|
92
|
-
AVG(CASE WHEN
|
|
93
|
+
AVG(CASE WHEN ${PREMIUM_MODEL_SQL} THEN 1.0 ELSE 0.0 END) AS premium_share
|
|
93
94
|
FROM turns
|
|
94
95
|
WHERE is_sidechain = 0 AND provisional = 0 AND ts >= ? AND ts < ? ${scoped ? "AND workspace_id = ?" : ""}`;
|
|
95
96
|
const params = scoped ? [fromIso, toIso, scopeWorkspaceId] : [fromIso, toIso];
|