agentwrangler 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (157) hide show
  1. package/LICENSE +191 -0
  2. package/README.md +116 -0
  3. package/dist/apply/jobs.js +429 -0
  4. package/dist/apply/open-terminal-child.mjs +98 -0
  5. package/dist/apply/open-terminal.js +221 -0
  6. package/dist/apply/settings-gen.js +35 -0
  7. package/dist/cli/agentwrangler.js +18 -0
  8. package/dist/daemon/config.js +51 -0
  9. package/dist/daemon/http.js +258 -0
  10. package/dist/daemon/index.js +372 -0
  11. package/dist/daemon/outcomes-pass.js +82 -0
  12. package/dist/daemon/readiness.js +15 -0
  13. package/dist/daemon/router.js +756 -0
  14. package/dist/daemon/static.js +146 -0
  15. package/dist/db/migrate.js +72 -0
  16. package/dist/db/migrations/001_observe.sql +196 -0
  17. package/dist/db/migrations/002_indexes.sql +6 -0
  18. package/dist/db/migrations/003_context_inventory_history.sql +20 -0
  19. package/dist/db/migrations/004_apply_jobs.sql +17 -0
  20. package/dist/db/migrations/005_tool_event_metadata.sql +17 -0
  21. package/dist/db/migrations/006_d7_query_indexes.sql +9 -0
  22. package/dist/db/migrations/007_work_item_branch_keys.sql +11 -0
  23. package/dist/db/migrations/008_thinking_tokens.sql +1 -0
  24. package/dist/db/migrations/009_user_turn_count.sql +1 -0
  25. package/dist/db/migrations/010_workspace_cwd.sql +1 -0
  26. package/dist/db/migrations/011_reports.sql +1 -0
  27. package/dist/db/migrations/012_reconcile_indexes.sql +2 -0
  28. package/dist/db/migrations/013_friction_fields.sql +5 -0
  29. package/dist/db/migrations/014_session_churn.sql +11 -0
  30. package/dist/db/migrations/015_gap_aggregates.sql +6 -0
  31. package/dist/db/open.js +30 -0
  32. package/dist/detector/benchmark-anchors.js +36 -0
  33. package/dist/detector/calibration.js +302 -0
  34. package/dist/detector/context-history-retention.js +312 -0
  35. package/dist/detector/context-probe.js +574 -0
  36. package/dist/detector/d1-source-identity.js +25 -0
  37. package/dist/detector/detectors/d10_catalog_footprint.js +146 -0
  38. package/dist/detector/detectors/d1_ctx_always_loaded.js +203 -0
  39. package/dist/detector/detectors/d2_session_long_full_context.js +119 -0
  40. package/dist/detector/detectors/d4_model_mismatch.js +258 -0
  41. package/dist/detector/detectors/d5_limit_burn_forecast.js +138 -0
  42. package/dist/detector/detectors/d6_tool_result_bloat.js +301 -0
  43. package/dist/detector/detectors/d7_loop_retry_waste.js +345 -0
  44. package/dist/detector/detectors/d8_cache_write_churn.js +201 -0
  45. package/dist/detector/detectors/d9_idle_background_session.js +101 -0
  46. package/dist/detector/engine.js +88 -0
  47. package/dist/detector/index.js +17 -0
  48. package/dist/detector/measurement.js +426 -0
  49. package/dist/detector/practice-registry.js +259 -0
  50. package/dist/detector/registry.js +32 -0
  51. package/dist/detector/savings.js +249 -0
  52. package/dist/detector/types.js +14 -0
  53. package/dist/evidence/common/approved-input.js +632 -0
  54. package/dist/evidence/common/boundary.js +84 -0
  55. package/dist/evidence/common/canonical.js +55 -0
  56. package/dist/evidence/common/redaction.js +321 -0
  57. package/dist/evidence/common/sqlite.js +25 -0
  58. package/dist/evidence/common/state.js +29 -0
  59. package/dist/evidence/cond1/cli.js +289 -0
  60. package/dist/evidence/cond1/packet.js +407 -0
  61. package/dist/evidence/cond1/prepare.js +295 -0
  62. package/dist/evidence/cond1/score.js +349 -0
  63. package/dist/evidence/cond1/types.js +1 -0
  64. package/dist/evidence/create-approval.js +365 -0
  65. package/dist/evidence/create-scratch.js +542 -0
  66. package/dist/evidence/d7/cli.js +113 -0
  67. package/dist/evidence/d7/measure.js +193 -0
  68. package/dist/evidence/d7/types.js +1 -0
  69. package/dist/evidence/discover-approval.js +492 -0
  70. package/dist/evidence/g2/adjudicate.js +20 -0
  71. package/dist/evidence/g2/cli.js +207 -0
  72. package/dist/evidence/g2/kappa.js +39 -0
  73. package/dist/evidence/g2/pipeline.js +92 -0
  74. package/dist/evidence/g2/store.js +14 -0
  75. package/dist/evidence/github/client.js +1 -0
  76. package/dist/evidence/github/gh-cli-client.js +301 -0
  77. package/dist/evidence/r3/cli.js +209 -0
  78. package/dist/evidence/r3/evaluate.js +417 -0
  79. package/dist/evidence/r3/packet.js +162 -0
  80. package/dist/evidence/r3/prepare.js +405 -0
  81. package/dist/evidence/r3/score.js +341 -0
  82. package/dist/evidence/r3/transcript.js +155 -0
  83. package/dist/evidence/r3/types.js +4 -0
  84. package/dist/hook/context-budget-hook.mjs +138 -0
  85. package/dist/hook/danger-guard-denylist.json +27 -0
  86. package/dist/hook/danger-guard-hook.mjs +167 -0
  87. package/dist/hook/install.js +0 -0
  88. package/dist/hook/limit-burn-hook.mjs +127 -0
  89. package/dist/hook/loop-guard-hook.mjs +104 -0
  90. package/dist/hook/precompact-checkpoint-hook.mjs +123 -0
  91. package/dist/ingest/churn-collector.js +122 -0
  92. package/dist/ingest/detector-hook.js +52 -0
  93. package/dist/ingest/discovery.js +207 -0
  94. package/dist/ingest/health.js +43 -0
  95. package/dist/ingest/index.js +28 -0
  96. package/dist/ingest/ingestor.js +509 -0
  97. package/dist/ingest/parser.js +344 -0
  98. package/dist/ingest/pricing.js +153 -0
  99. package/dist/ingest/reconcile.js +52 -0
  100. package/dist/ingest/tail.js +152 -0
  101. package/dist/ingest/types.js +24 -0
  102. package/dist/ingest/workspace-mapping.js +114 -0
  103. package/dist/oauth/anthropic-api-key.js +88 -0
  104. package/dist/oauth/count-tokens.js +86 -0
  105. package/dist/oauth/credentials.js +171 -0
  106. package/dist/oauth/judge-g2-client.js +154 -0
  107. package/dist/oauth/usage.js +167 -0
  108. package/dist/outcomes/branch-key.js +49 -0
  109. package/dist/outcomes/conclusions.js +45 -0
  110. package/dist/outcomes/derive.js +94 -0
  111. package/dist/outcomes/finding-extractors.js +131 -0
  112. package/dist/outcomes/findings.js +237 -0
  113. package/dist/outcomes/github/client.js +367 -0
  114. package/dist/outcomes/github/credential.js +195 -0
  115. package/dist/outcomes/github/gh-cli-client.js +340 -0
  116. package/dist/outcomes/linker.js +486 -0
  117. package/dist/outcomes/pool.js +24 -0
  118. package/dist/outcomes/sync.js +276 -0
  119. package/dist/query/api/agents-liveness.js +182 -0
  120. package/dist/query/api/burn-status.js +50 -0
  121. package/dist/query/api/context-budget.js +114 -0
  122. package/dist/query/api/context-composition.js +67 -0
  123. package/dist/query/api/cost-per-success.js +104 -0
  124. package/dist/query/api/delivery.js +92 -0
  125. package/dist/query/api/effectiveness.js +254 -0
  126. package/dist/query/api/efficiency-headroom.js +74 -0
  127. package/dist/query/api/headroom-trend.js +105 -0
  128. package/dist/query/api/hook-config.js +75 -0
  129. package/dist/query/api/hook-install.js +8 -0
  130. package/dist/query/api/hot-sessions.js +17 -0
  131. package/dist/query/api/idle-sessions.js +52 -0
  132. package/dist/query/api/index.js +40 -0
  133. package/dist/query/api/loop-guard.js +90 -0
  134. package/dist/query/api/offload-share.js +41 -0
  135. package/dist/query/api/outcomes.js +218 -0
  136. package/dist/query/api/overview.js +535 -0
  137. package/dist/query/api/rec-prompt.js +138 -0
  138. package/dist/query/api/recommendations-ledger.js +111 -0
  139. package/dist/query/api/recommendations.js +514 -0
  140. package/dist/query/api/reports.js +78 -0
  141. package/dist/query/api/self-churn.js +77 -0
  142. package/dist/query/api/self-percentiles.js +109 -0
  143. package/dist/query/api/session-drivers.js +153 -0
  144. package/dist/query/api/settings.js +85 -0
  145. package/dist/query/api/spend-flavor.js +234 -0
  146. package/dist/query/api/trends.js +155 -0
  147. package/dist/query/cap-weighted.js +119 -0
  148. package/dist/query/db-context.js +42 -0
  149. package/dist/query/envelope.js +71 -0
  150. package/dist/query/forecast.js +191 -0
  151. package/dist/query/settings-store.js +441 -0
  152. package/dist/query/spend.js +171 -0
  153. package/dist/query/trends.js +194 -0
  154. package/dist/ui/assets/index-DnRKgc21.css +1 -0
  155. package/dist/ui/assets/index-h1Q1wWq5.js +168 -0
  156. package/dist/ui/index.html +39 -0
  157. package/package.json +59 -0
@@ -0,0 +1,52 @@
1
+ /**
2
+ * src/ingest/detector-hook.ts — post-ingest hook seam.
3
+ *
4
+ * Mirrors the setRuntimeResetHook pattern (query/settings-store.ts): a
5
+ * module-level callback the daemon wires at boot so the Ingestor can trigger a
6
+ * detector pass WITHOUT `ingest/**` statically importing `detector/**` or the
7
+ * query layer (no ingest→query import cycle). Zero-op when unset (tests that do
8
+ * not wire it, e.g. the WP1 ingestion suite, are unaffected).
9
+ *
10
+ * Determinism (NFR-107 / Review F4): the hook is invoked with the ingestor's
11
+ * injected clock, never new Date() — see Ingestor.runPostIngest().
12
+ */
13
+ let _hook = null;
14
+ /** Wire the post-ingest hook (daemon boot / tests). */
15
+ export function setPostIngestHook(fn) {
16
+ _hook = fn;
17
+ }
18
+ /** Clear the post-ingest hook (tests, between cases). */
19
+ export function clearPostIngestHook() {
20
+ _hook = null;
21
+ }
22
+ /** Invoke the hook if wired; a thrown hook never breaks the ingest loop. */
23
+ export function runPostIngestHook(db, now) {
24
+ if (_hook === null)
25
+ return;
26
+ try {
27
+ _hook(db, now);
28
+ }
29
+ catch (e) {
30
+ console.warn(`post-ingest hook failed: ${e instanceof Error ? e.message : String(e)}`);
31
+ }
32
+ }
33
+ let _probeHook = null;
34
+ /** Wire the post-probe hook (daemon boot / tests). */
35
+ export function setPostProbeHook(fn) {
36
+ _probeHook = fn;
37
+ }
38
+ /** Clear the post-probe hook (tests, between cases). */
39
+ export function clearPostProbeHook() {
40
+ _probeHook = null;
41
+ }
42
+ /** Invoke the post-probe hook if wired; a thrown hook never breaks the probe pass. */
43
+ export function runPostProbeHook(db, now) {
44
+ if (_probeHook === null)
45
+ return;
46
+ try {
47
+ _probeHook(db, now);
48
+ }
49
+ catch (e) {
50
+ console.warn(`post-probe hook failed: ${e instanceof Error ? e.message : String(e)}`);
51
+ }
52
+ }
@@ -0,0 +1,207 @@
1
+ /**
2
+ * src/ingest/discovery.ts — transcript-file discovery + workspace registration.
3
+ *
4
+ * Each subdirectory of a scan root is a candidate workspace (project_slug);
5
+ * every *.jsonl beneath it (recursively, including nested subagent transcripts)
6
+ * is a session file. New slugs auto-register as unmapped
7
+ * workspaces so spend is visible immediately (repo mapping is a later WP4 step).
8
+ *
9
+ * Discovery is poll-based (default 30s cadence), distinct from the faster tail
10
+ * cadence. Filesystem watch is an optional optimisation not required for
11
+ * correctness — offsets + dedupe make a full re-scan cheap and idempotent.
12
+ */
13
+ import * as fs from "node:fs";
14
+ import * as path from "node:path";
15
+ /** Recursively collect every *.jsonl under `dir`, tagging each with `slug`. */
16
+ function collectJsonl(dir, slug, out) {
17
+ let entries;
18
+ try {
19
+ entries = fs.readdirSync(dir, { withFileTypes: true });
20
+ }
21
+ catch {
22
+ return;
23
+ }
24
+ // Emit files at this level BEFORE descending, so a session's own transcript
25
+ // (<slug>/<uuid>.jsonl) is discovered before its nested subagent files
26
+ // (<slug>/<uuid>/subagents/agent-*.jsonl). Both carry the same parent sessionId
27
+ // and ensureSession records file_path first-seen — files-before-dirs keeps the
28
+ // session's file_path pointing at the top-level transcript, not a subagent file.
29
+ for (const e of entries) {
30
+ if (e.isFile() && e.name.endsWith(".jsonl")) {
31
+ out.push({ filePath: path.join(dir, e.name), projectSlug: slug });
32
+ }
33
+ }
34
+ for (const e of entries) {
35
+ if (e.isDirectory()) {
36
+ collectJsonl(path.join(dir, e.name), slug, out);
37
+ }
38
+ }
39
+ }
40
+ /** Create an empty cache; the first refresh performs one full walk. */
41
+ export function createDiscoveryCache() {
42
+ return { files: [], roots: new Map(), initialized: false };
43
+ }
44
+ function directoryMtimeMs(dir) {
45
+ try {
46
+ const stat = fs.statSync(dir);
47
+ return stat.isDirectory() ? stat.mtimeMs : null;
48
+ }
49
+ catch {
50
+ return null;
51
+ }
52
+ }
53
+ function readSlugDirectories(root) {
54
+ let entries;
55
+ try {
56
+ entries = fs.readdirSync(root, { withFileTypes: true });
57
+ }
58
+ catch {
59
+ return null;
60
+ }
61
+ const slugDirs = new Map();
62
+ for (const entry of entries) {
63
+ if (!entry.isDirectory())
64
+ continue;
65
+ const dir = path.join(root, entry.name);
66
+ const mtimeMs = directoryMtimeMs(dir);
67
+ if (mtimeMs === null)
68
+ continue;
69
+ slugDirs.set(dir, { projectSlug: entry.name, mtimeMs });
70
+ }
71
+ return slugDirs;
72
+ }
73
+ function isInside(dir, filePath) {
74
+ const relative = path.relative(dir, filePath);
75
+ return (relative !== "" &&
76
+ relative !== ".." &&
77
+ !relative.startsWith(`..${path.sep}`) &&
78
+ !path.isAbsolute(relative));
79
+ }
80
+ function filesForSlug(discovered, dir, projectSlug) {
81
+ return discovered.filter((file) => file.projectSlug === projectSlug && isInside(dir, file.filePath));
82
+ }
83
+ function walkSlugFiles(dir, projectSlug) {
84
+ const files = [];
85
+ collectJsonl(dir, projectSlug, files);
86
+ return files;
87
+ }
88
+ function replaceFullCache(roots, cache, discovered) {
89
+ const nextRoots = new Map();
90
+ for (const root of roots) {
91
+ const rootMtimeMs = directoryMtimeMs(root);
92
+ if (rootMtimeMs === null)
93
+ continue;
94
+ const slugDirs = readSlugDirectories(root);
95
+ if (slugDirs === null)
96
+ continue;
97
+ const cachedSlugDirs = new Map();
98
+ for (const [dir, slug] of slugDirs) {
99
+ cachedSlugDirs.set(dir, {
100
+ projectSlug: slug.projectSlug,
101
+ mtimeMs: slug.mtimeMs,
102
+ files: filesForSlug(discovered, dir, slug.projectSlug),
103
+ });
104
+ }
105
+ nextRoots.set(root, { mtimeMs: rootMtimeMs, slugDirs: cachedSlugDirs });
106
+ }
107
+ cache.files = discovered;
108
+ cache.roots = nextRoots;
109
+ cache.initialized = true;
110
+ }
111
+ /** Refresh only changed slug directories, or force a periodic full walk. */
112
+ export function refreshDiscoveryCache(roots, cache, forceRefresh = false) {
113
+ if (forceRefresh || !cache.initialized) {
114
+ replaceFullCache(roots, cache, discoverFiles(roots));
115
+ return cache.files;
116
+ }
117
+ const activeRoots = new Set(roots);
118
+ for (const root of cache.roots.keys()) {
119
+ if (!activeRoots.has(root))
120
+ cache.roots.delete(root);
121
+ }
122
+ for (const root of roots) {
123
+ const rootMtimeMs = directoryMtimeMs(root);
124
+ if (rootMtimeMs === null) {
125
+ cache.roots.delete(root);
126
+ continue;
127
+ }
128
+ let cachedRoot = cache.roots.get(root);
129
+ const isNewRoot = cachedRoot === undefined;
130
+ if (cachedRoot === undefined) {
131
+ cachedRoot = { mtimeMs: rootMtimeMs, slugDirs: new Map() };
132
+ cache.roots.set(root, cachedRoot);
133
+ }
134
+ const rootChanged = isNewRoot || cachedRoot.mtimeMs !== rootMtimeMs;
135
+ if (rootChanged) {
136
+ const currentSlugDirs = readSlugDirectories(root);
137
+ if (currentSlugDirs !== null) {
138
+ const nextSlugDirs = new Map();
139
+ for (const [dir, slug] of currentSlugDirs) {
140
+ const previous = cachedRoot.slugDirs.get(dir);
141
+ nextSlugDirs.set(dir, previous !== undefined && previous.mtimeMs === slug.mtimeMs
142
+ ? previous
143
+ : {
144
+ projectSlug: slug.projectSlug,
145
+ mtimeMs: slug.mtimeMs,
146
+ files: walkSlugFiles(dir, slug.projectSlug),
147
+ });
148
+ }
149
+ cachedRoot.slugDirs = nextSlugDirs;
150
+ }
151
+ cachedRoot.mtimeMs = rootMtimeMs;
152
+ continue;
153
+ }
154
+ for (const [dir, cachedSlug] of cachedRoot.slugDirs) {
155
+ const mtimeMs = directoryMtimeMs(dir);
156
+ if (mtimeMs === null) {
157
+ cachedRoot.slugDirs.delete(dir);
158
+ continue;
159
+ }
160
+ if (mtimeMs === cachedSlug.mtimeMs)
161
+ continue;
162
+ const files = walkSlugFiles(dir, cachedSlug.projectSlug);
163
+ cachedSlug.mtimeMs = mtimeMs;
164
+ cachedSlug.files = files;
165
+ }
166
+ }
167
+ cache.files = [...cache.roots.values()].flatMap((root) => [...root.slugDirs.values()].flatMap((slug) => slug.files));
168
+ return cache.files;
169
+ }
170
+ /** Walk each root; return every *.jsonl file with its owning project slug. */
171
+ export function discoverFiles(roots) {
172
+ const out = [];
173
+ for (const root of roots) {
174
+ if (!fs.existsSync(root))
175
+ continue;
176
+ let dirents;
177
+ try {
178
+ dirents = fs.readdirSync(root, { withFileTypes: true });
179
+ }
180
+ catch {
181
+ continue;
182
+ }
183
+ for (const d of dirents) {
184
+ if (!d.isDirectory())
185
+ continue;
186
+ // projectSlug is always the top-level slug dir; recurse so nested subagent
187
+ // transcripts (<slug>/<session>/subagents/agent-*.jsonl) are discovered and
188
+ // attributed to the same workspace as their parent session.
189
+ collectJsonl(path.join(root, d.name), d.name, out);
190
+ }
191
+ }
192
+ return out;
193
+ }
194
+ /**
195
+ * Register a workspace for `slug` if not already present, returning its
196
+ * workspace_id. The slug is used as the workspace_id (deterministic, keeps
197
+ * rebuild-equality stable). Idempotent via INSERT OR IGNORE.
198
+ */
199
+ export function registerWorkspace(db, slug) {
200
+ db.prepare(`INSERT OR IGNORE INTO workspaces (workspace_id, project_slug, registered_at)
201
+ VALUES (?,?,?)`).run(slug, slug, new Date().toISOString());
202
+ return slug;
203
+ }
204
+ /** The session id used when a record carries none: the file's basename stem. */
205
+ export function sessionStemFor(filePath) {
206
+ return path.basename(filePath, ".jsonl");
207
+ }
@@ -0,0 +1,43 @@
1
+ /**
2
+ * src/ingest/health.ts — parser-health counters (NFR-106, first-class metric).
3
+ *
4
+ * Drift in any of these is the early-warning that a Claude Code update changed
5
+ * the transcript schema. Counters are in-memory on the running ingestor and
6
+ * surfaced in Settings (WP4). No content; counts only.
7
+ */
8
+ import { newHealthCounters } from "./types.js";
9
+ export class Health {
10
+ c = newHealthCounters();
11
+ fileSeen() {
12
+ this.c.filesSeen++;
13
+ }
14
+ fileParsed() {
15
+ this.c.filesParsed++;
16
+ }
17
+ quarantined() {
18
+ this.c.linesQuarantined++;
19
+ }
20
+ synthetic() {
21
+ this.c.syntheticExcluded++;
22
+ }
23
+ duplicateDrop() {
24
+ this.c.duplicateDrops++;
25
+ }
26
+ turnIngested(parserVersion) {
27
+ this.c.turnsIngested++;
28
+ this.c.parserVersionMix[parserVersion] = (this.c.parserVersionMix[parserVersion] ?? 0) + 1;
29
+ }
30
+ unknownFields(fields) {
31
+ for (const f of fields) {
32
+ this.c.unknownFieldKinds[f] = (this.c.unknownFieldKinds[f] ?? 0) + 1;
33
+ }
34
+ }
35
+ /** Return an immutable snapshot of the current counters. */
36
+ snapshot() {
37
+ return {
38
+ ...this.c,
39
+ unknownFieldKinds: { ...this.c.unknownFieldKinds },
40
+ parserVersionMix: { ...this.c.parserVersionMix },
41
+ };
42
+ }
43
+ }
@@ -0,0 +1,28 @@
1
+ /**
2
+ * src/ingest/index.ts — WP1 ingestion pipeline public surface.
3
+ *
4
+ * The daemon orchestrator (src/daemon/index.ts) wires these at boot; this module
5
+ * exposes clean importable entrypoints and does NOT touch daemon internals.
6
+ *
7
+ * runBackscan(db, roots) — one full idempotent scan of all transcript files,
8
+ * then reconcile; returns parser-health counters.
9
+ * startTail(db, roots) — initial pass, then discovery (30s) + tail (2s)
10
+ * loops with periodic reconciliation; returns a
11
+ * handle whose stop() clears the timers.
12
+ */
13
+ import { DEFAULT_INGESTOR_OPTIONS, Ingestor, } from "./ingestor.js";
14
+ export { Ingestor, DEFAULT_INGESTOR_OPTIONS };
15
+ export { PARSER_VERSION } from "./types.js";
16
+ export { PricingSnapshotStore, seedListPrices, LIST_PRICES } from "./pricing.js";
17
+ export { reconcileSessions, DEFAULT_RECONCILE_OPTIONS } from "./reconcile.js";
18
+ export { discoverFiles, registerWorkspace } from "./discovery.js";
19
+ /** Run a single full back-scan of `roots` into `db`. Returns health counters. */
20
+ export function runBackscan(db, roots, options = {}) {
21
+ return new Ingestor(db, roots, options).runBackscan();
22
+ }
23
+ /** Start incremental tailing of `roots` into `db`. Returns a stop handle. */
24
+ export function startTail(db, roots, options = {}) {
25
+ const ingestor = new Ingestor(db, roots, options);
26
+ const handle = ingestor.startTail();
27
+ return { handle, ingestor };
28
+ }