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,146 @@
1
+ /**
2
+ * src/daemon/static.ts — hardened static file serving for the built SPA.
3
+ *
4
+ * Security properties:
5
+ *
6
+ * 1. safeResolve(root, input): the single choke-point for any request-derived
7
+ * filesystem path. Defends against:
8
+ * - Path traversal (../../, URL-encoded, double-encoded).
9
+ * - Null-byte injection.
10
+ * - Absolute paths (Unix / and Windows drive letters).
11
+ * - UNC paths (\\server\share, //server/share).
12
+ * - Symlinks that escape root (checked via realpathSync).
13
+ * - Sibling-prefix attacks (<root>-evil/...) — blocked by the `root + sep`
14
+ * prefix check after realpathSync.
15
+ *
16
+ * 2. sirv with dotfiles:false blocks .env and other dotfiles.
17
+ *
18
+ * 3. The static handler is only mounted when uiRoot exists; it is not exposed
19
+ * unless the SPA has been built.
20
+ */
21
+ import * as fs from "node:fs";
22
+ import * as path from "node:path";
23
+ import sirv from "sirv";
24
+ /**
25
+ * Resolve `input` (from a request URL segment) safely within `root`.
26
+ *
27
+ * Returns the absolute, real path if it is within `root`, or `null` if:
28
+ * - `input` contains a null byte.
29
+ * - `input` is or resolves to an absolute path.
30
+ * - `input` is a UNC path.
31
+ * - The real path after symlink resolution escapes `root`.
32
+ *
33
+ * The URL-decode loop handles single-, double-, and triple-encoded sequences.
34
+ */
35
+ export function safeResolve(root, input) {
36
+ // 1. URL-decode loop until stable (handles %25-encoding, etc.).
37
+ let decoded = input;
38
+ for (let i = 0; i < 16; i++) {
39
+ let next;
40
+ try {
41
+ next = decodeURIComponent(decoded);
42
+ }
43
+ catch {
44
+ // Malformed percent-encoding — reject.
45
+ return null;
46
+ }
47
+ if (next === decoded)
48
+ break;
49
+ decoded = next;
50
+ }
51
+ // 2. Reject null bytes (bypass attempts via null-byte injection).
52
+ if (decoded.includes("\0"))
53
+ return null;
54
+ // 3. Reject UNC paths (\\server\share or //server/share).
55
+ if (decoded.startsWith("\\\\") || decoded.startsWith("//"))
56
+ return null;
57
+ // 4. Reject Windows drive letters (C:\, D:/, etc.).
58
+ if (/^[a-zA-Z]:/.test(decoded))
59
+ return null;
60
+ // 5. Reject absolute Unix paths.
61
+ if (path.isAbsolute(decoded))
62
+ return null;
63
+ // 6. Resolve relative to root.
64
+ const resolved = path.resolve(root, decoded);
65
+ // 7. RealpathSync: follow symlinks and get the canonical path.
66
+ // For non-existent paths (ENOENT/ENOTDIR), no symlink can exist, so
67
+ // the path.resolve result is already canonical — fall through with it.
68
+ // This lets createStaticHandler pass unknown paths to sirv's single:true
69
+ // SPA fallback (serving index.html) rather than returning a spurious 404.
70
+ let realResolved;
71
+ try {
72
+ realResolved = fs.realpathSync(resolved);
73
+ }
74
+ catch (err) {
75
+ const code = err.code;
76
+ if (code !== "ENOENT" && code !== "ENOTDIR") {
77
+ // Inaccessible for reasons other than "doesn't exist" — reject.
78
+ return null;
79
+ }
80
+ // File does not exist; use the path.resolve result for the prefix check.
81
+ realResolved = resolved;
82
+ }
83
+ // 8. Get the canonical root path (once; callers may cache this).
84
+ let realRoot;
85
+ try {
86
+ realRoot = fs.realpathSync(root);
87
+ }
88
+ catch {
89
+ return null;
90
+ }
91
+ // 9. Guard: real path must be root itself or begin with root + separator.
92
+ // The `+ sep` prevents sibling-prefix attacks: /var/www/app vs /var/www/app-evil.
93
+ if (realResolved !== realRoot && !realResolved.startsWith(realRoot + path.sep)) {
94
+ return null;
95
+ }
96
+ return realResolved;
97
+ }
98
+ /**
99
+ * Create a sirv-based static file handler for `uiRoot`.
100
+ *
101
+ * sirv is configured with:
102
+ * - dotfiles: false — blocks .env and other hidden files.
103
+ * - dev: false — production mode (ETags, caching headers).
104
+ * - single: true — falls back to index.html for SPA client-side routing.
105
+ *
106
+ * The returned function is a Node HTTP middleware: (req, res, next) => void.
107
+ * Call `next()` when sirv cannot serve the path (404 fallback in http.ts).
108
+ */
109
+ export function createStaticHandler(uiRoot) {
110
+ // Verify root exists before constructing the handler.
111
+ if (!fs.existsSync(uiRoot)) {
112
+ // Return a no-op that always calls next — UI not built yet.
113
+ return (_req, _res, next) => next();
114
+ }
115
+ const serve = sirv(uiRoot, {
116
+ dotfiles: false,
117
+ dev: false,
118
+ single: true, // SPA: fall back to index.html on 404.
119
+ etag: true,
120
+ });
121
+ return (req, res, next) => {
122
+ // Intercept the path for an extra safety check before handing off to sirv.
123
+ const rawPath = (req.url ?? "/").split("?")[0] ?? "/";
124
+ // Strip leading slash for safeResolve (it expects a relative input).
125
+ const relative = rawPath.startsWith("/") ? rawPath.slice(1) : rawPath;
126
+ if (relative.length > 0) {
127
+ // Block dotfiles explicitly (sirv's single: true would otherwise fall
128
+ // back to index.html instead of 404 for paths like /.env).
129
+ const segments = relative.split(/[/\\]/);
130
+ if (segments.some((seg) => seg.length > 0 && seg.startsWith("."))) {
131
+ res.writeHead(404, { "Content-Type": "text/plain" });
132
+ res.end("404 Not Found");
133
+ return;
134
+ }
135
+ const safe = safeResolve(uiRoot, relative);
136
+ if (safe === null) {
137
+ // Traversal or other rejection — short-circuit with 404.
138
+ res.writeHead(404, { "Content-Type": "text/plain" });
139
+ res.end("404 Not Found");
140
+ return;
141
+ }
142
+ }
143
+ // Path is within root — hand off to sirv.
144
+ serve(req, res, next);
145
+ };
146
+ }
@@ -0,0 +1,72 @@
1
+ /**
2
+ * src/db/migrate.ts — migration runner for AgentWrangler.
3
+ *
4
+ * Discovers SQL migration files in `src/db/migrations/` ordered by filename,
5
+ * records each applied migration in the `schema_migrations` table, and is
6
+ * idempotent: re-running applies nothing if all migrations are already recorded.
7
+ *
8
+ * Convention: migration filenames must be `NNN_<slug>.sql` (e.g. `001_observe.sql`).
9
+ * The version key stored is the bare filename without the `.sql` extension.
10
+ *
11
+ * Boot-strapping note: schema_migrations is created by 001_observe.sql itself.
12
+ * The runner checks sqlite_master directly to determine whether schema_migrations
13
+ * already exists, avoiding the circular dependency of creating it first.
14
+ *
15
+ * Each migration is applied in its own transaction. If a migration fails, the
16
+ * transaction rolls back and the error propagates — leaving the DB at the last
17
+ * successfully applied migration.
18
+ */
19
+ import * as fs from "node:fs";
20
+ import * as path from "node:path";
21
+ import { fileURLToPath } from "node:url";
22
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
23
+ const MIGRATIONS_DIR = path.join(__dirname, "migrations");
24
+ /** Return true if the schema_migrations table exists in the DB. */
25
+ function migrationsTableExists(db) {
26
+ const row = db
27
+ .prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name='schema_migrations'")
28
+ .get();
29
+ return row !== undefined;
30
+ }
31
+ /** Return the set of already-applied migration versions. */
32
+ function appliedVersions(db) {
33
+ const rows = db.prepare("SELECT version FROM schema_migrations").all();
34
+ return new Set(rows.map((r) => r.version));
35
+ }
36
+ /** Discover migration files, sorted lexicographically (ascending). */
37
+ function discoverMigrations() {
38
+ const files = fs
39
+ .readdirSync(MIGRATIONS_DIR)
40
+ .filter((f) => f.endsWith(".sql"))
41
+ .sort();
42
+ return files.map((f) => ({
43
+ version: f.slice(0, -4), // strip .sql
44
+ filePath: path.join(MIGRATIONS_DIR, f),
45
+ }));
46
+ }
47
+ /**
48
+ * Run pending migrations against `db`, optionally stopping at `throughVersion`.
49
+ * Returns the list of newly-applied version strings (empty when already up-to-date).
50
+ *
51
+ * The first migration (001_observe.sql) is responsible for creating the
52
+ * schema_migrations table. Subsequent calls find the table already present.
53
+ */
54
+ export function runMigrations(db, throughVersion) {
55
+ // Determine which migrations have already been applied (if the bookkeeping
56
+ // table exists) or treat the list as empty (first ever run).
57
+ const hasTable = migrationsTableExists(db);
58
+ const applied = hasTable ? appliedVersions(db) : new Set();
59
+ const pending = discoverMigrations().filter((m) => !applied.has(m.version) && (throughVersion === undefined || m.version <= throughVersion));
60
+ const applied_now = [];
61
+ for (const { version, filePath } of pending) {
62
+ const sql = fs.readFileSync(filePath, "utf-8");
63
+ // Run the migration SQL + record the version atomically.
64
+ db.transaction(() => {
65
+ db.exec(sql);
66
+ // schema_migrations must now exist (created by the SQL if it wasn't before).
67
+ db.prepare("INSERT INTO schema_migrations (version, applied_at) VALUES (?, ?)").run(version, new Date().toISOString());
68
+ })();
69
+ applied_now.push(version);
70
+ }
71
+ return applied_now;
72
+ }
@@ -0,0 +1,196 @@
1
+ -- AgentWrangler Data Model v2 DDL — verbatim from AgentWrangler_Data_Model_and_Metrics_v2.md §1
2
+ -- metric_definition_version = 'observe-1'
3
+ -- Applied by: src/db/migrate.ts
4
+
5
+ CREATE TABLE workspaces (
6
+ workspace_id TEXT PRIMARY KEY,
7
+ project_slug TEXT NOT NULL UNIQUE, -- ~/.claude/projects/<slug>
8
+ repo_path TEXT, -- local checkout, if registered
9
+ repo_owner TEXT, repo_name TEXT, -- canonical GitHub identity
10
+ registered_at TEXT NOT NULL
11
+ );
12
+
13
+ CREATE TABLE sessions (
14
+ session_id TEXT PRIMARY KEY, -- sessionId or filename stem
15
+ workspace_id TEXT NOT NULL REFERENCES workspaces(workspace_id),
16
+ file_path TEXT NOT NULL,
17
+ first_turn_at TEXT, last_turn_at TEXT,
18
+ state TEXT NOT NULL CHECK (state IN ('LIVE','RECONCILED')),
19
+ turn_count INTEGER NOT NULL DEFAULT 0,
20
+ cost_equiv_u INTEGER NOT NULL DEFAULT 0, -- rollforward; reconciled at close
21
+ hygiene_flags TEXT NOT NULL DEFAULT '[]' -- JSON: LONG_FULL_CONTEXT, COMPACT_MID_TASK...
22
+ );
23
+
24
+ CREATE TABLE turns ( -- one row per assistant message (deduped)
25
+ message_id TEXT PRIMARY KEY, -- message.id, fallback uuid
26
+ session_id TEXT NOT NULL REFERENCES sessions(session_id),
27
+ workspace_id TEXT NOT NULL,
28
+ ts TEXT NOT NULL,
29
+ model TEXT NOT NULL, -- '<synthetic>' rows excluded upstream
30
+ is_sidechain INTEGER NOT NULL DEFAULT 0, -- subagent marker
31
+ input_tokens INTEGER NOT NULL DEFAULT 0,
32
+ output_tokens INTEGER NOT NULL DEFAULT 0,
33
+ cache_read_tokens INTEGER NOT NULL DEFAULT 0,
34
+ cache_write_5m INTEGER NOT NULL DEFAULT 0,
35
+ cache_write_1h INTEGER NOT NULL DEFAULT 0,
36
+ cache_write_other INTEGER NOT NULL DEFAULT 0,
37
+ context_tokens AS (input_tokens + cache_read_tokens
38
+ + cache_write_5m + cache_write_1h + cache_write_other) STORED,
39
+ tool_result_bytes INTEGER, -- SUM over all tool_result blocks in the turn; size only, never content
40
+ pricing_snapshot_id TEXT REFERENCES pricing_snapshots(snapshot_id),
41
+ cost_equiv_u INTEGER, -- NULL = unpriceable
42
+ cost_claim TEXT NOT NULL DEFAULT 'LIST_EQUIV'
43
+ CHECK (cost_claim IN ('LIST_EQUIV','LIST_EQUIV_STALE','BILLED')),
44
+ provisional INTEGER NOT NULL DEFAULT 0,
45
+ effort TEXT, -- extended thinking effort label; nullable, tolerated since 2026-08-14
46
+ parser_version TEXT NOT NULL
47
+ );
48
+ CREATE INDEX idx_turns_ws_ts ON turns(workspace_id, ts);
49
+ CREATE INDEX idx_turns_session ON turns(session_id, ts);
50
+ CREATE INDEX idx_turns_model ON turns(model, ts);
51
+
52
+ CREATE TABLE tool_events ( -- names/sizes/sequence only
53
+ event_id TEXT PRIMARY KEY,
54
+ session_id TEXT NOT NULL REFERENCES sessions(session_id),
55
+ ts TEXT NOT NULL,
56
+ tool_name TEXT NOT NULL,
57
+ input_bytes INTEGER, result_bytes INTEGER,
58
+ input_hash TEXT, -- for loop/near-duplicate detection
59
+ exit_class TEXT, -- OK | ERROR | TEST_FAIL | ...
60
+ commit_sha TEXT -- harvested when the event exposes one
61
+ );
62
+ CREATE INDEX idx_tool_session ON tool_events(session_id, ts);
63
+ CREATE INDEX idx_tool_sha ON tool_events(commit_sha) WHERE commit_sha IS NOT NULL;
64
+
65
+ CREATE TABLE pricing_snapshots (
66
+ snapshot_id TEXT PRIMARY KEY,
67
+ model_tier TEXT NOT NULL, -- opus|sonnet|haiku|fable...
68
+ unit_prices_json TEXT NOT NULL, -- [in, out, cacheRead, cw5m, cw1h] $/MTok
69
+ captured_at TEXT NOT NULL, stale_after TEXT NOT NULL
70
+ );
71
+
72
+ CREATE TABLE context_inventory ( -- always-loaded attribution time series
73
+ probe_id TEXT PRIMARY KEY,
74
+ workspace_id TEXT NOT NULL REFERENCES workspaces(workspace_id),
75
+ probed_at TEXT NOT NULL,
76
+ component TEXT NOT NULL CHECK (component IN
77
+ ('CLAUDE_MD','RULES','MCP_SCHEMAS','SETTINGS_SYSTEM','MEMORY','OTHER')),
78
+ file_ref TEXT NOT NULL, -- path, not content
79
+ file_hash TEXT NOT NULL,
80
+ tokens INTEGER NOT NULL,
81
+ attribution_version TEXT NOT NULL
82
+ -- NOTE (v1 qualification): system-prompt and MCP-schema tokens (~1-3% of context) are NOT
83
+ -- attributable from local files in v1; the attributed total may undercount (SG-S5-03 / FW-04).
84
+ );
85
+
86
+ CREATE TABLE work_items ( -- PRs
87
+ work_item_id TEXT PRIMARY KEY, -- gh:<owner>/<repo>#<number>
88
+ workspace_id TEXT NOT NULL REFERENCES workspaces(workspace_id),
89
+ number INTEGER NOT NULL,
90
+ state TEXT NOT NULL, -- OPEN|MERGED|CLOSED
91
+ final_commit TEXT,
92
+ checks_conclusion TEXT, -- SUCCESS|FAILURE|PENDING|NONE
93
+ opened_at TEXT, merged_at TEXT, closed_at TEXT,
94
+ synced_at TEXT NOT NULL
95
+ );
96
+
97
+ CREATE TABLE session_work_links (
98
+ session_id TEXT NOT NULL REFERENCES sessions(session_id),
99
+ work_item_id TEXT NOT NULL REFERENCES work_items(work_item_id),
100
+ confidence REAL NOT NULL, -- SHA-overlap strength
101
+ method TEXT NOT NULL, -- SHA_OVERLAP | BRANCH | MANUAL
102
+ PRIMARY KEY (session_id, work_item_id)
103
+ );
104
+
105
+ CREATE TABLE observed_outcomes (
106
+ work_item_id TEXT PRIMARY KEY REFERENCES work_items(work_item_id),
107
+ outcome TEXT NOT NULL CHECK (outcome IN
108
+ ('OBSERVED_SUCCESS','OBSERVED_SUCCESS_WITH_DEFERRALS',
109
+ 'OBSERVED_FAILURE','IN_PROGRESS')),
110
+ derived_at TEXT NOT NULL,
111
+ methodology_version TEXT NOT NULL
112
+ );
113
+
114
+ CREATE TABLE review_findings (
115
+ finding_id TEXT PRIMARY KEY,
116
+ work_item_id TEXT NOT NULL REFERENCES work_items(work_item_id),
117
+ source TEXT NOT NULL CHECK (source IN
118
+ ('UNRESOLVED_THREAD','DEFERRAL_SECTION','DIFF_MARKER','LLM')), -- LLM = P0.5 seam
119
+ severity TEXT CHECK (severity IN ('HIGH','MEDIUM','LOW','UNKNOWN')),
120
+ status TEXT NOT NULL CHECK (status IN ('ADDRESSED','DEFERRED','UNKNOWN')),
121
+ evidence_ref TEXT NOT NULL, -- thread id / body anchor / file:line
122
+ confidence REAL, -- NULL for deterministic sources
123
+ human_state TEXT CHECK (human_state IN ('CONFIRMED','REJECTED')), -- LLM findings only
124
+ raised_at TEXT NOT NULL,
125
+ cleared_at TEXT,
126
+ cleared_by TEXT, -- resolving commit SHA or work_item_id
127
+ extractor_version TEXT NOT NULL
128
+ );
129
+ CREATE INDEX idx_findings_open ON review_findings(status, cleared_at);
130
+
131
+ CREATE TABLE recommendations (
132
+ rec_id TEXT PRIMARY KEY,
133
+ provenance TEXT NOT NULL CHECK (provenance IN ('RULE','CLAUDE_ANALYZED')),
134
+ detector_id TEXT, -- RULE provenance
135
+ analysis_run_id TEXT REFERENCES analysis_runs(run_id), -- CLAUDE provenance
136
+ category TEXT NOT NULL,
137
+ scope_workspace_id TEXT, -- NULL = global
138
+ lever TEXT NOT NULL,
139
+ modeled_savings_u_per_wk INTEGER,
140
+ modeled_formula_json TEXT NOT NULL, -- inputs so the model is reproducible
141
+ evidence_json TEXT NOT NULL, -- citations: metric ids + values + row ids
142
+ target_metric TEXT NOT NULL, -- what effect measurement watches
143
+ state TEXT NOT NULL CHECK (state IN
144
+ ('PROPOSED','ADOPTED','DISMISSED','MEASURING',
145
+ 'MEASURED_EFFECTIVE','MEASURED_NO_EFFECT')),
146
+ created_at TEXT NOT NULL, adopted_at TEXT, dismissed_until TEXT
147
+ );
148
+
149
+ CREATE TABLE recommendation_effects (
150
+ rec_id TEXT NOT NULL REFERENCES recommendations(rec_id),
151
+ measured_at TEXT NOT NULL, -- adoption-cycle grain; composite PK with rec_id
152
+ before_from TEXT NOT NULL, before_to TEXT NOT NULL,
153
+ after_from TEXT NOT NULL, after_to TEXT NOT NULL,
154
+ before_value REAL, after_value REAL,
155
+ before_n INTEGER, after_n INTEGER,
156
+ delta_pct REAL,
157
+ verdict TEXT CHECK (verdict IN ('EFFECTIVE','NO_EFFECT','INCONCLUSIVE')),
158
+ PRIMARY KEY (rec_id, measured_at) -- composite: effects re-measurable across cycles
159
+ );
160
+
161
+ CREATE TABLE analysis_runs ( -- Tier 2 provenance + metering
162
+ run_id TEXT PRIMARY KEY,
163
+ scope TEXT NOT NULL, -- GLOBAL | workspace_id | session_id
164
+ model TEXT NOT NULL,
165
+ prompt_version TEXT NOT NULL,
166
+ evidence_pack_hash TEXT NOT NULL,
167
+ content_included INTEGER NOT NULL DEFAULT 0, -- explicit opt-in only (SEC-104)
168
+ input_tokens INTEGER, output_tokens INTEGER,
169
+ cost_equiv_u INTEGER,
170
+ contract_valid INTEGER, -- schema + citation resolution result
171
+ ran_at TEXT NOT NULL
172
+ );
173
+
174
+ CREATE TABLE ingest_quarantine (
175
+ q_id TEXT PRIMARY KEY,
176
+ file_path TEXT NOT NULL, line_no INTEGER NOT NULL,
177
+ error_class TEXT NOT NULL, parser_version TEXT NOT NULL,
178
+ seen_at TEXT NOT NULL -- pointer only; no content (SEC-107)
179
+ );
180
+
181
+ CREATE TABLE ingest_offsets (
182
+ file_path TEXT PRIMARY KEY,
183
+ byte_offset INTEGER NOT NULL,
184
+ file_hash_head TEXT, -- rotation/truncation detection
185
+ updated_at TEXT NOT NULL
186
+ );
187
+
188
+ CREATE TABLE schema_migrations (version TEXT PRIMARY KEY, applied_at TEXT NOT NULL);
189
+
190
+ CREATE TABLE user_config ( -- daemon-side persisted config + alert state
191
+ key TEXT PRIMARY KEY, -- e.g. 'limit_tokens', 'last_warned_jd'
192
+ value TEXT, -- stored as text; callers cast to required type
193
+ updated_at TEXT NOT NULL
194
+ -- Enables Burn-Forecast persistence (FW-06) and alerting detector D5 (FW-07).
195
+ -- Seed rows: ('limit_tokens', NULL, ...) and ('last_warned_jd', NULL, ...).
196
+ );
@@ -0,0 +1,6 @@
1
+ -- AgentWrangler migration 002 — additional indexes for high-frequency FK lookups.
2
+ -- Applied by: src/db/migrate.ts
3
+
4
+ CREATE INDEX IF NOT EXISTS idx_work_items_workspace ON work_items(workspace_id);
5
+ CREATE INDEX IF NOT EXISTS idx_session_work_links_work_item ON session_work_links(work_item_id);
6
+ CREATE INDEX IF NOT EXISTS idx_review_findings_work_item ON review_findings(work_item_id);
@@ -0,0 +1,20 @@
1
+ -- AgentWrangler migration 003 — context_inventory_history for realized-savings measurement.
2
+ -- Applied by: src/db/migrate.ts (auto-discovered by filename order)
3
+ --
4
+ -- Sibling to context_inventory: stores one row per distinct file_hash version, appended
5
+ -- by the probe whenever file_hash changes from the last recorded value. This gives a
6
+ -- bounded change-log (one row per version, not one per probe) so byte-deltas are measurable.
7
+
8
+ CREATE TABLE context_inventory_history (
9
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
10
+ workspace_id TEXT NOT NULL REFERENCES workspaces(workspace_id),
11
+ component TEXT NOT NULL CHECK (component IN
12
+ ('CLAUDE_MD','RULES','MCP_SCHEMAS','SETTINGS_SYSTEM','MEMORY','OTHER')),
13
+ file_ref TEXT NOT NULL,
14
+ file_hash TEXT NOT NULL,
15
+ tokens INTEGER NOT NULL,
16
+ attribution_version TEXT NOT NULL,
17
+ observed_at TEXT NOT NULL
18
+ );
19
+ CREATE INDEX idx_ctx_hist_workspace ON context_inventory_history(workspace_id, observed_at);
20
+ CREATE INDEX idx_ctx_hist_file_ref ON context_inventory_history(workspace_id, component, file_ref, observed_at);
@@ -0,0 +1,17 @@
1
+ CREATE TABLE apply_jobs (
2
+ job_id TEXT PRIMARY KEY,
3
+ rec_id TEXT NOT NULL REFERENCES recommendations(rec_id),
4
+ run_id TEXT REFERENCES analysis_runs(run_id),
5
+ status TEXT NOT NULL CHECK (status IN
6
+ ('PENDING','DRY_RUNNING','DRY_DONE','CONFIRMING','APPLIED','FAILED','ROLLED_BACK')),
7
+ file_ref TEXT NOT NULL,
8
+ workspace_cwd TEXT NOT NULL,
9
+ diff_preview TEXT,
10
+ diff_applied TEXT,
11
+ backup_path TEXT,
12
+ error_msg TEXT,
13
+ created_at TEXT NOT NULL,
14
+ updated_at TEXT NOT NULL
15
+ );
16
+
17
+ CREATE INDEX idx_apply_jobs_rec ON apply_jobs(rec_id, created_at DESC);
@@ -0,0 +1,17 @@
1
+ -- D7 ingestion foundation: privacy-safe target identity and structural ordering.
2
+ -- Raw tool input/output and raw tool file paths are intentionally excluded.
3
+
4
+ CREATE TABLE tool_event_metadata (
5
+ event_id TEXT PRIMARY KEY REFERENCES tool_events(event_id) ON DELETE CASCADE,
6
+ file_path_hash TEXT,
7
+ owner_message_id TEXT,
8
+ block_index INTEGER NOT NULL,
9
+ is_test_command INTEGER NOT NULL CHECK (is_test_command IN (0, 1))
10
+ );
11
+
12
+ CREATE INDEX idx_tool_meta_path_order
13
+ ON tool_event_metadata(file_path_hash, block_index)
14
+ WHERE file_path_hash IS NOT NULL;
15
+
16
+ CREATE INDEX idx_tool_meta_owner_event
17
+ ON tool_event_metadata(owner_message_id, event_id);
@@ -0,0 +1,9 @@
1
+ -- Keep D7's trailing-window scans bounded by time as the corpus grows.
2
+ -- This is separate from migration 005 because 005 may already have been
3
+ -- applied by pre-commit smoke runs during D7 development.
4
+
5
+ CREATE INDEX idx_tool_ts_session_event
6
+ ON tool_events(ts, session_id, event_id);
7
+
8
+ CREATE INDEX idx_turns_ts_provisional_session
9
+ ON turns(ts, provisional, session_id);
@@ -0,0 +1,11 @@
1
+ CREATE TABLE work_item_branch_keys (
2
+ work_item_id TEXT PRIMARY KEY REFERENCES work_items(work_item_id) ON DELETE CASCADE,
3
+ head_ref_key TEXT NOT NULL
4
+ CHECK (length(head_ref_key) = 64
5
+ AND head_ref_key NOT GLOB '*[^0-9a-f]*'),
6
+ normalization_version TEXT NOT NULL CHECK (normalization_version = 'branch-v1'),
7
+ synced_at TEXT NOT NULL
8
+ );
9
+
10
+ CREATE INDEX idx_work_item_branch_key
11
+ ON work_item_branch_keys(head_ref_key, work_item_id);
@@ -0,0 +1 @@
1
+ ALTER TABLE turns ADD COLUMN thinking_tokens INTEGER;
@@ -0,0 +1 @@
1
+ ALTER TABLE sessions ADD COLUMN user_turn_count INTEGER NOT NULL DEFAULT 0;
@@ -0,0 +1 @@
1
+ ALTER TABLE workspaces ADD COLUMN discovered_cwd TEXT;
@@ -0,0 +1 @@
1
+ CREATE TABLE reports (report_id TEXT PRIMARY KEY, kind TEXT NOT NULL, period_start TEXT NOT NULL, period_end TEXT NOT NULL, generated_at TEXT NOT NULL, content_json TEXT NOT NULL, UNIQUE(kind, period_start));
@@ -0,0 +1,2 @@
1
+ CREATE INDEX IF NOT EXISTS idx_turns_session_ctx ON turns(session_id, context_tokens);
2
+ CREATE INDEX IF NOT EXISTS idx_tool_session_name_hash ON tool_events(session_id, tool_name, input_hash);
@@ -0,0 +1,5 @@
1
+ -- RV2a: L1 Friction substrate — per-session counters for friction signal ingestion.
2
+ -- SEC-101: counts only; never message content.
3
+ ALTER TABLE sessions ADD COLUMN compaction_count INTEGER NOT NULL DEFAULT 0;
4
+ ALTER TABLE sessions ADD COLUMN api_error_count INTEGER NOT NULL DEFAULT 0;
5
+ ALTER TABLE sessions ADD COLUMN interrupt_count INTEGER NOT NULL DEFAULT 0;
@@ -0,0 +1,11 @@
1
+ CREATE TABLE session_churn (
2
+ session_id TEXT PRIMARY KEY REFERENCES sessions(session_id),
3
+ status TEXT NOT NULL,
4
+ window_days INTEGER NOT NULL,
5
+ authored_lines INTEGER NOT NULL DEFAULT 0,
6
+ churned_lines INTEGER NOT NULL DEFAULT 0,
7
+ churn_ratio REAL,
8
+ commit_count INTEGER NOT NULL DEFAULT 0,
9
+ commit_shas TEXT NOT NULL DEFAULT '[]',
10
+ measured_at TEXT NOT NULL
11
+ );
@@ -0,0 +1,6 @@
1
+ -- EF3: per-session inter-user-turn gap aggregates (SEC-101: durations in seconds only).
2
+ -- Backfilled on next full re-scan (RV2a stance); historical rows keep defaults.
3
+ ALTER TABLE sessions ADD COLUMN gap_median_s REAL;
4
+ ALTER TABLE sessions ADD COLUMN gap_p90_s REAL;
5
+ ALTER TABLE sessions ADD COLUMN long_gap_count INTEGER NOT NULL DEFAULT 0;
6
+ ALTER TABLE sessions ADD COLUMN gap_n INTEGER NOT NULL DEFAULT 0;
@@ -0,0 +1,30 @@
1
+ /**
2
+ * src/db/open.ts — open a better-sqlite3 database with required pragmas.
3
+ *
4
+ * Enforces:
5
+ * - WAL journal mode (on-disk only; in-memory stays 'memory', which is expected).
6
+ * - foreign_keys=ON (SEC constraint; enforced at every open).
7
+ *
8
+ * Never open :memory: in production paths — this module is for on-disk DBs.
9
+ */
10
+ import * as fs from "node:fs";
11
+ import * as path from "node:path";
12
+ import Database from "better-sqlite3";
13
+ /**
14
+ * Open an on-disk SQLite database at `dbPath`, creating parent directories as needed.
15
+ * Applies WAL journal mode and foreign_keys=ON before returning.
16
+ */
17
+ export function openDb(dbPath) {
18
+ const dir = path.dirname(dbPath);
19
+ fs.mkdirSync(dir, { recursive: true });
20
+ const db = new Database(dbPath);
21
+ db.pragma("journal_mode = WAL");
22
+ db.pragma("synchronous = NORMAL");
23
+ db.pragma("foreign_keys = ON");
24
+ const fk = db.pragma("foreign_keys")[0]?.foreign_keys;
25
+ if (fk !== 1) {
26
+ db.close();
27
+ throw new Error("FATAL: foreign_keys pragma failed to enable");
28
+ }
29
+ return db;
30
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * src/detector/benchmark-anchors.ts — BM4 public benchmark anchor constants.
3
+ *
4
+ * Static, cited reference points sourced from public Anthropic data.
5
+ * These are display-only constants — NEVER a score input, NEVER stored.
6
+ *
7
+ * Re-verify when touched — $6→$13 is the precedent that these rot.
8
+ *
9
+ * SEC-101: only public aggregate/claim strings, no user data.
10
+ */
11
+ /** $13/day average (enterprise API-billed). Supersedes the old $6/day figure. */
12
+ export const DAILY_AVG_USD = {
13
+ daily_usd: 13,
14
+ label: "avg/day",
15
+ source_url: "https://code.claude.com/docs/en/costs",
16
+ fetched_date: "2026-09-02",
17
+ };
18
+ /** $30/day p90 (enterprise API-billed). */
19
+ export const DAILY_P90_USD = {
20
+ daily_usd: 30,
21
+ label: "p90/day",
22
+ source_url: "https://code.claude.com/docs/en/costs",
23
+ fetched_date: "2026-09-02",
24
+ };
25
+ /** Rate-limit cap-impact claim from official Anthropic post. */
26
+ export const CAP_IMPACT_CLAIM = {
27
+ claim: "<5% of subscribers",
28
+ source: "official Anthropic X post, 2025-07-28",
29
+ };
30
+ /**
31
+ * Mandatory caveat: anchor population is enterprise API-billed;
32
+ * our own figure is a LIST_EQUIV subscription estimate — a reference
33
+ * point, NOT like-for-like.
34
+ */
35
+ export const ANCHOR_CAVEAT = "Benchmark figures are from enterprise API-billed users. " +
36
+ "Your figure is a subscription-plan estimate (LIST_EQUIV) — a reference point, not a like-for-like comparison.";