agentwrangler 0.1.1-next.b96c996 → 0.1.1-next.bdce596
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 +5 -3
- package/dist/daemon/http.js +167 -34
- package/dist/daemon/index.js +4 -0
- package/dist/daemon/router.js +106 -1
- package/dist/db/migrations/020_local_command_retention.sql +50 -0
- package/dist/db/migrations/021_recommendation_feedback.sql +25 -0
- package/dist/ingest/boot-progress.js +76 -0
- package/dist/ingest/ingestor.js +32 -11
- package/dist/ingest/parser.js +19 -3
- package/dist/ingest/types.js +1 -1
- package/dist/query/api/decision-ranker-types.js +6 -0
- package/dist/query/api/decision-ranker.js +393 -0
- package/dist/query/api/evidence-packet-types.js +3 -0
- package/dist/query/api/evidence-packet.js +316 -0
- package/dist/query/api/feedback-store.js +286 -0
- package/dist/query/api/index.js +1 -1
- package/dist/query/api/opportunity-composer-types.js +3 -0
- package/dist/query/api/opportunity-composer.js +303 -0
- package/dist/query/api/overview.js +27 -1
- package/dist/query/settings-store.js +2 -0
- package/dist/ui/assets/{BarChart-Dl53SZ6e.js → BarChart-C9vtAcml.js} +1 -1
- package/dist/ui/assets/{BriefsPage-8lfHwiet.js → BriefsPage-DLp1pInz.js} +2 -2
- package/dist/ui/assets/{CacheWriteSpikesChart-BsSxJ9SV.js → CacheWriteSpikesChart-BlflM-xW.js} +1 -1
- package/dist/ui/assets/{CartesianChart-DupEWmgD.js → CartesianChart-0_RUWlzc.js} +1 -1
- package/dist/ui/assets/{Chip-CCd9WDMt.js → Chip-BQ4UlaAL.js} +1 -1
- package/dist/ui/assets/{ComposedChart-0oXVebKl.js → ComposedChart-CIG9CS0j.js} +1 -1
- package/dist/ui/assets/{EmptyState-CAYDLcoW.js → EmptyState-CjBh_Y06.js} +1 -1
- package/dist/ui/assets/{FlavorDecomposition-D-PGCDPt.js → FlavorDecomposition-Cb1Et9e_.js} +1 -1
- package/dist/ui/assets/{FrictionCell-tbVMqCgq.js → FrictionCell-BAcKJbGq.js} +1 -1
- package/dist/ui/assets/{GlossaryPage-DdPUdfKZ.js → GlossaryPage-C0nFEAdc.js} +1 -1
- package/dist/ui/assets/HotSessionsPage-C6obf1m9.js +1 -0
- package/dist/ui/assets/InfoTip-Dc4F2ylc.js +1 -0
- package/dist/ui/assets/{Legend-DTGaSuvn.js → Legend-Ck2QXclg.js} +1 -1
- package/dist/ui/assets/{Line-DRi_yMDT.js → Line-CSuEmMnn.js} +1 -1
- package/dist/ui/assets/{Modal-ZYooa7gF.js → Modal-C5VeGRjW.js} +1 -1
- package/dist/ui/assets/{ObservationEvidence-Cy92xibu.css → ObservationEvidence-Bhnk-xkX.css} +1 -1
- package/dist/ui/assets/ObservationEvidence-CP8cvr_5.js +1 -0
- package/dist/ui/assets/OverviewPage-CCyQ2Oxn.js +2 -0
- package/dist/ui/assets/RecommendationsPage-Bs1B1AJ0.js +2 -0
- package/dist/ui/assets/{Scatter-DsFXWqth.js → Scatter-BiTrTZ1e.js} +1 -1
- package/dist/ui/assets/SectionHeader-BcQQCuZA.js +1 -0
- package/dist/ui/assets/SessionDetailPage-CcPL36Cv.js +1 -0
- package/dist/ui/assets/SettingsPage-CzAe6Xf1.js +25 -0
- package/dist/ui/assets/Skeleton-D1ey4h0q.js +1 -0
- package/dist/ui/assets/SpendPercentileChip-DiTZ1xoI.js +1 -0
- package/dist/ui/assets/TrendChart-B51LSzds.js +1 -0
- package/dist/ui/assets/WorkspaceDetailPage-B4LWopuo.js +1 -0
- package/dist/ui/assets/WorkspacesPage-m4Km-zOE.js +1 -0
- package/dist/ui/assets/chart-theme-BAFdtBab.js +1 -0
- package/dist/ui/assets/{graphicalItemSelectors-Z3Izh0Fw.js → graphicalItemSelectors-yTOJ8llA.js} +1 -1
- package/dist/ui/assets/index-DTmPINeX.js +41 -0
- package/dist/ui/assets/index-qkEcvt6k.css +1 -0
- package/dist/ui/assets/{prompt-templates-CVzMex9L.js → prompt-templates-CVAAu0IZ.js} +1 -1
- package/dist/ui/assets/short-id-C12NbqLn.js +1 -0
- package/dist/ui/assets/{useExperimentalActions-x8q0qp8Z.js → useExperimentalActions-BVXB2b0W.js} +1 -1
- package/dist/ui/assets/workspace-names-DW0HTwW6.js +1 -0
- package/dist/ui/index.html +2 -2
- package/package.json +3 -3
- package/dist/ui/assets/HotSessionsPage-D4HAqHz7.js +0 -1
- package/dist/ui/assets/InfoTip-D0FXyNUo.js +0 -1
- package/dist/ui/assets/ObservationEvidence-DD-B7CBZ.js +0 -1
- package/dist/ui/assets/OverviewPage-DOVfPJzI.js +0 -2
- package/dist/ui/assets/RecommendationsPage-D8RphMJF.js +0 -2
- package/dist/ui/assets/SessionDetailPage-CtsfFubC.js +0 -1
- package/dist/ui/assets/SettingsPage-DFr7Husi.js +0 -25
- package/dist/ui/assets/Skeleton-DkfxY-OD.js +0 -1
- package/dist/ui/assets/SpendPercentileChip-CwS6UIlD.js +0 -1
- package/dist/ui/assets/TrendChart-CjE_ilNQ.js +0 -1
- package/dist/ui/assets/WorkspaceDetailPage-CiNzhaLj.js +0 -1
- package/dist/ui/assets/WorkspacesPage-D2gI9O2J.js +0 -1
- package/dist/ui/assets/chart-theme-C_vHEuaA.js +0 -1
- package/dist/ui/assets/index-CkFwfN3_.css +0 -1
- package/dist/ui/assets/index-DP7as9pl.js +0 -41
- package/dist/ui/assets/workspace-label-CysPJNc0.js +0 -1
- package/dist/ui/assets/workspace-names-870o7-Mo.js +0 -1
package/README.md
CHANGED
|
@@ -84,9 +84,11 @@ five hooks; **Copy install prompt** prepares instructions for three. Copying alo
|
|
|
84
84
|
## Privacy and limits
|
|
85
85
|
|
|
86
86
|
- The daemon binds to **127.0.0.1**. There is no telemetry or hosted product backend.
|
|
87
|
-
- SQLite contains aggregates and structural data,
|
|
88
|
-
|
|
89
|
-
|
|
87
|
+
- SQLite contains aggregates and structural data, plus filesystem paths. Command rows keep
|
|
88
|
+
only `/compact`, `/clear`, or an unclassified marker — other commands and arguments are
|
|
89
|
+
discarded, though databases or backups from before the upgrade can still hold raw command
|
|
90
|
+
text (sanitation is logical, not physical erasure). Treat it as sensitive. The optional
|
|
91
|
+
PreCompact hook makes separate raw transcript copies on your machine.
|
|
90
92
|
- Usage refresh can contact Anthropic using your existing Claude Code sign-in. GitHub
|
|
91
93
|
outcomes sync requires a configured token. [Privacy details](https://github.com/Doogit/AgentWrangler/blob/main/docs/privacy.md).
|
|
92
94
|
- Claude Code format changes can require parser updates. This is a single-user tool.
|
package/dist/daemon/http.js
CHANGED
|
@@ -27,53 +27,178 @@ import * as http from "node:http";
|
|
|
27
27
|
import { isReady } from "./readiness.js";
|
|
28
28
|
import { handleApiRequest } from "./router.js";
|
|
29
29
|
import { createStaticHandler } from "./static.js";
|
|
30
|
-
/**
|
|
30
|
+
/**
|
|
31
|
+
* Inline loading page served while the daemon's initial back-scan is running
|
|
32
|
+
* (BOOT-2, Option E): docked terminal panel streaming boot_scan_events on top,
|
|
33
|
+
* three live counters + a determinate progress bar below, fade into the app on
|
|
34
|
+
* ready (no white-flash hard reload). Self-contained by design — the SPA's
|
|
35
|
+
* assets are not servable pre-ready, so design-token VALUES are copied from
|
|
36
|
+
* src/ui/styles.css (dark theme) with that file as the source of truth.
|
|
37
|
+
* Falls back to the plain "Scanning transcripts — N of M files" line when the
|
|
38
|
+
* BOOT-1 fields are absent (older daemon mid-upgrade).
|
|
39
|
+
*/
|
|
31
40
|
const LOADING_HTML = `<!doctype html>
|
|
32
41
|
<html lang="en">
|
|
33
42
|
<head>
|
|
34
43
|
<meta charset="utf-8">
|
|
35
44
|
<meta name="viewport" content="width=device-width,initial-scale=1">
|
|
45
|
+
<meta name="color-scheme" content="dark">
|
|
36
46
|
<title>AgentWrangler — Starting up</title>
|
|
37
47
|
<style>
|
|
48
|
+
/* Token values copied from src/ui/styles.css (dark theme) — keep in sync. */
|
|
49
|
+
:root{
|
|
50
|
+
--bg:#0b0f1a;--panel:#111827;--panel2:#1a2233;--line:rgba(255,255,255,.07);
|
|
51
|
+
--text:#e2e8f0;--text2:#94a3b8;--muted:#9aa9bd;
|
|
52
|
+
--cyan:#22d3ee;--violet:#a78bfa;--green:#34d399;
|
|
53
|
+
--sans:system-ui,-apple-system,"Segoe UI",sans-serif;
|
|
54
|
+
--mono:ui-monospace,Consolas,Menlo,monospace;
|
|
55
|
+
}
|
|
38
56
|
*{box-sizing:border-box;margin:0;padding:0}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
.
|
|
46
|
-
|
|
57
|
+
html{background:var(--bg)}
|
|
58
|
+
body{
|
|
59
|
+
background:var(--bg);
|
|
60
|
+
color:var(--text);font-family:var(--sans);min-height:100vh;
|
|
61
|
+
display:grid;place-content:center;gap:28px;padding:24px 0;
|
|
62
|
+
}
|
|
63
|
+
body.leaving .log,body.leaving .hero{opacity:0}
|
|
64
|
+
.log,.hero{opacity:1;transition:opacity .4s ease}
|
|
65
|
+
.log{
|
|
66
|
+
background:#0a0e16;margin:0 auto;width:min(480px,calc(100vw - 36px));
|
|
67
|
+
border:1px solid var(--line);border-radius:8px;overflow:hidden;
|
|
68
|
+
display:grid;grid-template-rows:26px 1fr;height:158px;
|
|
69
|
+
}
|
|
70
|
+
.log-head{
|
|
71
|
+
display:flex;align-items:center;gap:8px;padding:0 12px;
|
|
72
|
+
background:var(--panel);border-bottom:1px solid var(--line);
|
|
73
|
+
font-family:var(--mono);font-size:10px;color:var(--muted);
|
|
74
|
+
letter-spacing:.08em;text-transform:uppercase;
|
|
75
|
+
}
|
|
76
|
+
.log-head .dot{width:7px;height:7px;border-radius:50%;background:var(--green)}
|
|
77
|
+
.log-body{
|
|
78
|
+
padding:8px 12px;font-family:var(--mono);font-size:10.5px;line-height:1.7;
|
|
79
|
+
color:var(--text2);overflow:hidden;display:flex;flex-direction:column;justify-content:flex-end;
|
|
80
|
+
}
|
|
81
|
+
.log-body .ln{white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
|
|
82
|
+
.log-body .ok{color:var(--green)}
|
|
83
|
+
.log-body .hl{color:var(--cyan)}
|
|
84
|
+
.hero{display:grid;place-content:center;gap:16px;text-align:center;padding:0 16px}
|
|
85
|
+
.headline{font-size:15px;font-weight:600}
|
|
86
|
+
.headline span{color:var(--cyan)}
|
|
87
|
+
.counters{display:flex;gap:34px;justify-content:center;flex-wrap:wrap}
|
|
88
|
+
.counter .cv{font-family:var(--mono);font-size:30px;font-weight:600;font-variant-numeric:tabular-nums;letter-spacing:-.01em}
|
|
89
|
+
.counter:nth-child(1) .cv{color:var(--cyan)}
|
|
90
|
+
.counter:nth-child(2) .cv{color:var(--violet)}
|
|
91
|
+
.counter:nth-child(3) .cv{color:var(--green)}
|
|
92
|
+
.counter .cl{font-size:10.5px;color:var(--muted);text-transform:uppercase;letter-spacing:.1em;margin-top:2px}
|
|
93
|
+
.pbar{width:300px;max-width:80vw;height:6px;background:var(--panel2);border-radius:99px;overflow:hidden;margin:0 auto}
|
|
94
|
+
.pbar i{display:block;height:100%;width:0;background:linear-gradient(90deg,var(--cyan),var(--violet));border-radius:99px;transition:width .25s ease-out}
|
|
95
|
+
.status{font-family:var(--mono);font-size:11.5px;color:var(--muted);min-height:1.2em;font-variant-numeric:tabular-nums}
|
|
96
|
+
@media (prefers-reduced-motion:reduce){
|
|
97
|
+
.log,.hero,.pbar i{transition:none}
|
|
98
|
+
}
|
|
47
99
|
</style>
|
|
48
100
|
</head>
|
|
49
101
|
<body>
|
|
50
|
-
<div class="
|
|
51
|
-
<
|
|
52
|
-
<
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
<
|
|
102
|
+
<div class="log" id="log" hidden>
|
|
103
|
+
<div class="log-head"><span class="dot"></span>boot scan · <span id="rate">— files/s</span></div>
|
|
104
|
+
<div class="log-body" id="logbody"></div>
|
|
105
|
+
</div>
|
|
106
|
+
<div class="hero">
|
|
107
|
+
<div class="headline">Preparing your <span>Claude Code</span> data…</div>
|
|
108
|
+
<div class="counters">
|
|
109
|
+
<div class="counter"><div class="cv" id="c-files">0</div><div class="cl">files scanned</div></div>
|
|
110
|
+
<div class="counter"><div class="cv" id="c-sess">0</div><div class="cl">sessions found</div></div>
|
|
111
|
+
<div class="counter"><div class="cv" id="c-tok">0</div><div class="cl">tokens counted</div></div>
|
|
112
|
+
</div>
|
|
113
|
+
<div class="pbar"><i id="fill"></i></div>
|
|
114
|
+
<div class="status" id="status" aria-live="polite">Scanning transcript roots…</div>
|
|
56
115
|
</div>
|
|
57
116
|
<script>
|
|
58
|
-
(function
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
117
|
+
(function(){
|
|
118
|
+
var reduced=false;
|
|
119
|
+
try{reduced=matchMedia('(prefers-reduced-motion: reduce)').matches;}catch(e){}
|
|
120
|
+
var lastSeq=0,prevFiles=null,prevT=null,rateEma=null,leaving=false;
|
|
121
|
+
function $(id){return document.getElementById(id);}
|
|
122
|
+
function fmt(n){return Number(n||0).toLocaleString('en-US');}
|
|
123
|
+
function fmtTok(n){
|
|
124
|
+
n=Number(n||0);
|
|
125
|
+
if(n>=1e9)return (n/1e9).toFixed(1)+'B';
|
|
126
|
+
if(n>=1e6)return (n/1e6).toFixed(1)+'M';
|
|
127
|
+
if(n>=1e3)return (n/1e3).toFixed(1)+'K';
|
|
128
|
+
return String(n);
|
|
129
|
+
}
|
|
130
|
+
function finish(){
|
|
131
|
+
if(leaving)return;
|
|
132
|
+
leaving=true;
|
|
133
|
+
// Fade out, then swap to the app behind the transition — no white flash.
|
|
134
|
+
document.body.className='leaving';
|
|
135
|
+
setTimeout(function(){location.reload();},reduced?0:450);
|
|
136
|
+
}
|
|
137
|
+
function renderLog(events){
|
|
138
|
+
var body=$('logbody');
|
|
139
|
+
for(var i=0;i<events.length;i++){
|
|
140
|
+
var ev=events[i];
|
|
141
|
+
if(!ev||typeof ev.seq!=='number'||ev.seq<=lastSeq)continue;
|
|
142
|
+
lastSeq=ev.seq;
|
|
143
|
+
var el=document.createElement('div');
|
|
144
|
+
el.className='ln'+(ev.kind==='stage'?' hl':ev.kind==='root'?' ok':'');
|
|
145
|
+
el.textContent=String(ev.text||'');
|
|
146
|
+
body.appendChild(el);
|
|
147
|
+
while(body.children.length>6)body.removeChild(body.firstChild);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
function render(s){
|
|
151
|
+
if(!s)return;
|
|
152
|
+
var files=typeof s.files_parsed==='number'?s.files_parsed:0;
|
|
153
|
+
var seen=typeof s.files_seen==='number'?s.files_seen:0;
|
|
154
|
+
var hasBoot=Array.isArray(s.boot_scan_events);
|
|
155
|
+
// Progress denominator: discovery's up-front total. files_seen only counts
|
|
156
|
+
// files visited so far, which would pin a determinate bar at ~100%.
|
|
157
|
+
var total=hasBoot&&typeof s.boot_files_total==='number'&&s.boot_files_total>0?s.boot_files_total:seen;
|
|
158
|
+
$('c-files').textContent=fmt(files);
|
|
159
|
+
$('c-sess').textContent=fmt(typeof s.boot_sessions_found==='number'?s.boot_sessions_found:(s.sessions||0));
|
|
160
|
+
$('c-tok').textContent=hasBoot?fmtTok(s.boot_tokens_counted):'—';
|
|
161
|
+
var pct=total>0?Math.min(100,Math.round(100*files/total)):0;
|
|
162
|
+
$('fill').style.width=pct+'%';
|
|
163
|
+
// Client-side ETA from the parse-rate delta between polls (EMA-smoothed).
|
|
164
|
+
var now=Date.now();
|
|
165
|
+
if(prevFiles!==null&&now>prevT){
|
|
166
|
+
var r=(files-prevFiles)/((now-prevT)/1000);
|
|
167
|
+
if(r>0)rateEma=rateEma===null?r:(rateEma*0.7+r*0.3);
|
|
168
|
+
}
|
|
169
|
+
prevFiles=files;prevT=now;
|
|
170
|
+
if(hasBoot){
|
|
171
|
+
$('log').hidden=false;
|
|
172
|
+
renderLog(s.boot_scan_events);
|
|
173
|
+
var rr=typeof s.boot_files_per_sec==='number'?s.boot_files_per_sec:0;
|
|
174
|
+
$('rate').textContent=(rr>0?rr:'—')+' files/s';
|
|
175
|
+
if(total===0){
|
|
176
|
+
$('status').textContent='Scanning transcript roots…';
|
|
177
|
+
}else if(pct<100){
|
|
178
|
+
var eta=rateEma&&rateEma>0?Math.max(1,Math.round((total-files)/rateEma)):null;
|
|
179
|
+
$('status').textContent='Parsing sessions'+(eta!==null?' — '+eta+'s remaining':'…');
|
|
180
|
+
}else{
|
|
181
|
+
$('status').textContent='Opening your dashboard…';
|
|
182
|
+
}
|
|
183
|
+
}else if(seen>0){
|
|
184
|
+
// Fallback for an older daemon without BOOT-1 fields: keep the plain
|
|
185
|
+
// first-run counter line ("N of M files"), never a blank page.
|
|
186
|
+
$('status').textContent='Scanning transcripts — '+files+' of '+seen+' files';
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
(function poll(){
|
|
190
|
+
fetch('/api/ready')
|
|
191
|
+
.then(function(r){return r.json();})
|
|
192
|
+
.then(function(d){
|
|
193
|
+
if(d.ready){finish();return;}
|
|
194
|
+
fetch('/api/status')
|
|
195
|
+
.then(function(r){return r.json();})
|
|
196
|
+
.then(render)
|
|
197
|
+
.catch(function(){})
|
|
198
|
+
.then(function(){setTimeout(poll,1000);});
|
|
199
|
+
})
|
|
200
|
+
.catch(function(){setTimeout(poll,1000);});
|
|
201
|
+
})();
|
|
77
202
|
})();
|
|
78
203
|
</script>
|
|
79
204
|
</body>
|
|
@@ -132,6 +257,14 @@ function requiresSessionToken(pathname) {
|
|
|
132
257
|
if (pathname === "/api/recommendations/adopt" || pathname === "/api/recommendations/dismiss") {
|
|
133
258
|
return true;
|
|
134
259
|
}
|
|
260
|
+
if (pathname === "/api/recommendations/feedback")
|
|
261
|
+
return true;
|
|
262
|
+
if (pathname === "/api/recommendations/feedback/undo")
|
|
263
|
+
return true;
|
|
264
|
+
if (pathname === "/api/recommendations/goal")
|
|
265
|
+
return true;
|
|
266
|
+
if (pathname === "/api/recommendations/goal/reset")
|
|
267
|
+
return true;
|
|
135
268
|
if (/^\/api\/recommendations\/[^/]+\/apply$/.test(pathname))
|
|
136
269
|
return true;
|
|
137
270
|
if (/^\/api\/recommendations\/[^/]+\/open-terminal$/.test(pathname))
|
package/dist/daemon/index.js
CHANGED
|
@@ -23,6 +23,7 @@ import { configGet as bptConfigGet, calibrateBytesPerToken } from "../detector/c
|
|
|
23
23
|
import { runContextProbe } from "../detector/context-probe.js";
|
|
24
24
|
import { runDetectors } from "../detector/index.js";
|
|
25
25
|
import { installHook, uninstallHook } from "../hook/install.js";
|
|
26
|
+
import { getBootProgress, pushBootEvent } from "../ingest/boot-progress.js";
|
|
26
27
|
import { collectSessionChurn } from "../ingest/churn-collector.js";
|
|
27
28
|
import { runPostProbeHook, setPostIngestHook, setPostProbeHook } from "../ingest/detector-hook.js";
|
|
28
29
|
import { Ingestor } from "../ingest/index.js";
|
|
@@ -224,6 +225,7 @@ async function runBootScan() {
|
|
|
224
225
|
setHealthInstance(ingestor.health);
|
|
225
226
|
handle = await ingestor.startTailBatched();
|
|
226
227
|
setScanState("complete");
|
|
228
|
+
pushBootEvent("stage", `parse complete — ${getBootProgress().boot_sessions_found} sessions found`);
|
|
227
229
|
setRuntimeResetHook(() => ingestor.clearRuntimeState());
|
|
228
230
|
console.log(`Ingestion: initial scan complete — health ${JSON.stringify(ingestor.healthSnapshot())}`);
|
|
229
231
|
}
|
|
@@ -236,6 +238,7 @@ async function runBootScan() {
|
|
|
236
238
|
// boot probe ran before any workspace existed and could only size the global sources.
|
|
237
239
|
// Then re-evaluate detectors so D1 surfaces its per-source context recs on the first run
|
|
238
240
|
// instead of waiting for the next 10-minute poll.
|
|
241
|
+
pushBootEvent("stage", "building aggregates…");
|
|
239
242
|
runProbePass("post-scan");
|
|
240
243
|
try {
|
|
241
244
|
runDetectors(db, { now: new Date() });
|
|
@@ -245,6 +248,7 @@ async function runBootScan() {
|
|
|
245
248
|
}
|
|
246
249
|
// Signal readiness BEFORE the outcomes pass — outcomes is a slow ~97s I/O
|
|
247
250
|
// pass that yields on `gh` subprocess I/O; the loading page must not wait on it.
|
|
251
|
+
pushBootEvent("stage", "ready — opening dashboard");
|
|
248
252
|
setReady();
|
|
249
253
|
// Outcomes bootstrap — deferred until after the back-scan so the event loop
|
|
250
254
|
// is free and readGithubToken() execFile (10 s timeout) cannot be starved.
|
package/dist/daemon/router.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { getReportedWork } from "../query/api/reported-work.js";
|
|
2
2
|
import { getPractices } from "../detector/practice-registry.js";
|
|
3
|
+
import { getBootProgress } from "../ingest/boot-progress.js";
|
|
3
4
|
import { getOAuthStatus } from "../oauth/credentials.js";
|
|
4
5
|
import { getGithubTokenStatus } from "../outcomes/github/credential.js";
|
|
5
6
|
import { manualLink, manualUnlink } from "../outcomes/linker.js";
|
|
@@ -11,9 +12,10 @@ import { EffectRequestError, listEffectEvidence, mutateEffectCycle, trackComplet
|
|
|
11
12
|
import { getClosureProxy } from "../query/api/effectiveness.js";
|
|
12
13
|
import { getEfficiencyHeadroom } from "../query/api/efficiency-headroom.js";
|
|
13
14
|
import { getEsfObservations, getSessionEsfObservations } from "../query/api/esf-observations.js";
|
|
15
|
+
import { FeedbackError, deleteFeedbackRoute, getGoalRoute, listFeedbackRoute, resetGoalRoute, setFeedbackRoute, setGoalRoute, undoFeedbackRoute, } from "../query/api/feedback-store.js";
|
|
14
16
|
import { getHeadroomTrend } from "../query/api/headroom-trend.js";
|
|
15
17
|
import { getHotSessions } from "../query/api/hot-sessions.js";
|
|
16
|
-
import { adoptRecommendation, calibrateBytesPerToken, calibrateLimit, confirmApplyJob, dismissRecommendation, getApplyJob, getContextBudget, getContextComposition, getGlobalOverview, getHookConfig, getIdleSessions, getLinkageRate, getLoopGuard, getSession, getSessionDrivers, getSettings, getSuccessRate, getTurnTimeline, getWorkspaceOutcomeDetail, installHookRoute, listLedger, listLiveSessions, listRecommendations, listSessions, listWorkspaceOutcomes, listWorkspaces, openTerminalForRec, resetDatabase, rollbackApplyJob, startApplyJob, uninstallHookRoute, updateHookConfig, updateSettings, } from "../query/api/index.js";
|
|
18
|
+
import { adoptRecommendation, calibrateBytesPerToken, calibrateLimit, confirmApplyJob, dismissRecommendation, getApplyJob, getContextBudget, getContextComposition, getGlobalOverview, getHookConfig, getIdleSessions, getLinkageRate, getLoopGuard, getSession, getSessionDrivers, getSettings, getSuccessRate, getTurnTimeline, getWorkspaceOutcomeDetail, installHookRoute, listLedger, listLiveSessions, listRecommendations, listSessions, listWorkspaceNames, listWorkspaceOutcomes, listWorkspaces, openTerminalForRec, resetDatabase, rollbackApplyJob, startApplyJob, uninstallHookRoute, updateHookConfig, updateSettings, } from "../query/api/index.js";
|
|
17
19
|
import { getOffloadShare } from "../query/api/offload-share.js";
|
|
18
20
|
import { getReport, listReports } from "../query/api/reports.js";
|
|
19
21
|
import { getSelfChurn } from "../query/api/self-churn.js";
|
|
@@ -31,6 +33,39 @@ function sendJson(res, status, body) {
|
|
|
31
33
|
});
|
|
32
34
|
res.end(payload);
|
|
33
35
|
}
|
|
36
|
+
function sendFeedbackError(res, error) {
|
|
37
|
+
if (error instanceof FeedbackError) {
|
|
38
|
+
sendJson(res, error.status, { error: error.message, code: error.code });
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
// Oversized body (readBody) rejects with EffectRequestError(400, …).
|
|
42
|
+
if (error instanceof EffectRequestError) {
|
|
43
|
+
sendJson(res, error.status, { error: error.message, code: "INVALID_REQUEST" });
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
sendJson(res, 500, { error: "Internal error" });
|
|
47
|
+
}
|
|
48
|
+
/** Read + JSON-parse a write body, then run `run` and send its enveloped result. */
|
|
49
|
+
function withJsonBody(req, res, run) {
|
|
50
|
+
readBody(req, 4096)
|
|
51
|
+
.then((raw) => {
|
|
52
|
+
let body;
|
|
53
|
+
try {
|
|
54
|
+
body = JSON.parse(raw);
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
sendJson(res, 400, { error: "Invalid JSON body", code: "INVALID_REQUEST" });
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
try {
|
|
61
|
+
sendJson(res, 200, run(body));
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
sendFeedbackError(res, error);
|
|
65
|
+
}
|
|
66
|
+
})
|
|
67
|
+
.catch((error) => sendFeedbackError(res, error));
|
|
68
|
+
}
|
|
34
69
|
function readBody(req, maxBytes) {
|
|
35
70
|
return new Promise((resolve, reject) => {
|
|
36
71
|
const chunks = [];
|
|
@@ -133,6 +168,18 @@ function workRecordId(pathname) {
|
|
|
133
168
|
const match = pathname.match(/^\/api\/work-records\/([^/]+)$/);
|
|
134
169
|
return match?.[1] === undefined ? null : decodeURIComponent(match[1]);
|
|
135
170
|
}
|
|
171
|
+
function listSavedCostReports(db, workspaceId) {
|
|
172
|
+
return db
|
|
173
|
+
.prepare(`SELECT r.allocation_revision_id, r.created_at, r.cohort_from, r.cohort_to,
|
|
174
|
+
SUM(CASE WHEN a.disposition = 'OWNED' THEN 1 ELSE 0 END) AS allocated_session_count,
|
|
175
|
+
COUNT(a.session_id) AS eligible_session_count
|
|
176
|
+
FROM work_allocation_revisions r
|
|
177
|
+
LEFT JOIN work_session_allocations a ON a.allocation_revision_id = r.allocation_revision_id
|
|
178
|
+
WHERE r.workspace_id = ?
|
|
179
|
+
GROUP BY r.allocation_revision_id
|
|
180
|
+
ORDER BY r.created_at DESC, r.allocation_revision_id DESC`)
|
|
181
|
+
.all(workspaceId);
|
|
182
|
+
}
|
|
136
183
|
function workRecordAction(pathname) {
|
|
137
184
|
const match = pathname.match(/^\/api\/work-records\/([^/]+)\/(edit|closeout|archive|reopen|sessions|contexts|delete)(?:\/([^/]+)\/detach)?$/);
|
|
138
185
|
if (match?.[1] === undefined || match[2] === undefined)
|
|
@@ -264,6 +311,16 @@ export function handleApiRequest(_db, req, res, method, url) {
|
|
|
264
311
|
sendJson(res, 200, workRecordResponse(records, { n: records.length, workspaceId }));
|
|
265
312
|
return;
|
|
266
313
|
}
|
|
314
|
+
if (method === "GET" && pathname === "/api/work-records/allocations") {
|
|
315
|
+
const workspaceId = new URLSearchParams(url.split("?")[1] ?? "").get("workspace_id");
|
|
316
|
+
if (workspaceId === null || workspaceId.length === 0) {
|
|
317
|
+
sendJson(res, 400, { error: "workspace_id is required" });
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
const reports = listSavedCostReports(_db, workspaceId);
|
|
321
|
+
sendJson(res, 200, workRecordResponse(reports, { n: reports.length, workspaceId }));
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
267
324
|
if (method === "GET" && pathname.startsWith("/api/work-records/allocations/")) {
|
|
268
325
|
const allocationId = pathname.slice("/api/work-records/allocations/".length);
|
|
269
326
|
if (allocationId.length === 0 || allocationId.includes("/")) {
|
|
@@ -500,6 +557,11 @@ export function handleApiRequest(_db, req, res, method, url) {
|
|
|
500
557
|
sendJson(res, 200, listWorkspaces(parseWindowFilter(url)));
|
|
501
558
|
return;
|
|
502
559
|
}
|
|
560
|
+
// GET /api/workspace-names (unwindowed naming metadata for all workspaces — UIR-8)
|
|
561
|
+
if (method === "GET" && pathname === "/api/workspace-names") {
|
|
562
|
+
sendJson(res, 200, listWorkspaceNames());
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
503
565
|
// GET /api/delivery (delivery proxy metrics across all workspaces)
|
|
504
566
|
if (method === "GET" && pathname === "/api/delivery") {
|
|
505
567
|
const { from, to } = resolveWindow(parseWindowFilter(url));
|
|
@@ -799,6 +861,46 @@ export function handleApiRequest(_db, req, res, method, url) {
|
|
|
799
861
|
.catch(() => sendJson(res, 500, { error: "Internal error reading request body" }));
|
|
800
862
|
return;
|
|
801
863
|
}
|
|
864
|
+
// ── RIQ3 relevance feedback + goal preference (migration 021) ──────────
|
|
865
|
+
// Local, enum-only, SEC-101. Writes are CSRF + session-token gated in http.ts.
|
|
866
|
+
if (method === "GET" && pathname === "/api/recommendations/feedback") {
|
|
867
|
+
const scopeKey = new URLSearchParams(url.split("?")[1] ?? "").get("scope_key");
|
|
868
|
+
if (!scopeKey) {
|
|
869
|
+
sendJson(res, 400, { error: "scope_key is required", code: "INVALID_REQUEST" });
|
|
870
|
+
return;
|
|
871
|
+
}
|
|
872
|
+
sendJson(res, 200, listFeedbackRoute(_db, scopeKey));
|
|
873
|
+
return;
|
|
874
|
+
}
|
|
875
|
+
if (method === "POST" && pathname === "/api/recommendations/feedback") {
|
|
876
|
+
withJsonBody(req, res, (body) => setFeedbackRoute(_db, body));
|
|
877
|
+
return;
|
|
878
|
+
}
|
|
879
|
+
if (method === "POST" && pathname === "/api/recommendations/feedback/undo") {
|
|
880
|
+
withJsonBody(req, res, (body) => undoFeedbackRoute(_db, body));
|
|
881
|
+
return;
|
|
882
|
+
}
|
|
883
|
+
if (method === "DELETE" && pathname === "/api/recommendations/feedback") {
|
|
884
|
+
withJsonBody(req, res, (body) => deleteFeedbackRoute(_db, body));
|
|
885
|
+
return;
|
|
886
|
+
}
|
|
887
|
+
if (method === "GET" && pathname === "/api/recommendations/goal") {
|
|
888
|
+
const scopeKey = new URLSearchParams(url.split("?")[1] ?? "").get("scope_key");
|
|
889
|
+
if (!scopeKey) {
|
|
890
|
+
sendJson(res, 400, { error: "scope_key is required", code: "INVALID_REQUEST" });
|
|
891
|
+
return;
|
|
892
|
+
}
|
|
893
|
+
sendJson(res, 200, getGoalRoute(_db, scopeKey));
|
|
894
|
+
return;
|
|
895
|
+
}
|
|
896
|
+
if (method === "POST" && pathname === "/api/recommendations/goal") {
|
|
897
|
+
withJsonBody(req, res, (body) => setGoalRoute(_db, body));
|
|
898
|
+
return;
|
|
899
|
+
}
|
|
900
|
+
if (method === "POST" && pathname === "/api/recommendations/goal/reset") {
|
|
901
|
+
withJsonBody(req, res, (body) => resetGoalRoute(_db, body));
|
|
902
|
+
return;
|
|
903
|
+
}
|
|
802
904
|
// GET /api/settings
|
|
803
905
|
if (method === "GET" && pathname === "/api/settings") {
|
|
804
906
|
sendJson(res, 200, getSettings());
|
|
@@ -1021,6 +1123,9 @@ export function handleApiRequest(_db, req, res, method, url) {
|
|
|
1021
1123
|
files_parsed: parser_health.files_parsed,
|
|
1022
1124
|
lines_quarantined: parser_health.lines_quarantined,
|
|
1023
1125
|
...getScanStatus(),
|
|
1126
|
+
// BOOT-1: boot-progress fields exist ONLY pre-ready; the steady-state
|
|
1127
|
+
// /api/status shape is unchanged (UI fixtures must not require them).
|
|
1128
|
+
...(isReady() ? {} : getBootProgress()),
|
|
1024
1129
|
});
|
|
1025
1130
|
return;
|
|
1026
1131
|
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
-- 020_local_command_retention.sql — SEC-4 local command retention.
|
|
2
|
+
-- Spec: docs/plans/spec-local-command-retention.md §"Persistence invariant and legacy migration".
|
|
3
|
+
--
|
|
4
|
+
-- Legacy marker namespace M(row): tool_name = 'local_command' AND event_id is
|
|
5
|
+
-- 'cmd-' + 20 lowercase hex (length 24). Valid input_hash inside M is SQL NULL
|
|
6
|
+
-- or exact binary TEXT '/compact' or '/clear'; everything else is command
|
|
7
|
+
-- content and is removed. Genuine tool-use events named 'local_command' with
|
|
8
|
+
-- IDs outside M keep their SHA-256 hashes.
|
|
9
|
+
--
|
|
10
|
+
-- Logical retention reduction only: no physical erasure of WAL frames, free
|
|
11
|
+
-- pages, backups, or source transcripts is promised.
|
|
12
|
+
|
|
13
|
+
-- 1. Data change: NULL every invalid M input_hash in place. No row deletion,
|
|
14
|
+
-- no rekeying, no other column touched.
|
|
15
|
+
UPDATE tool_events
|
|
16
|
+
SET input_hash = NULL
|
|
17
|
+
WHERE tool_name = 'local_command'
|
|
18
|
+
AND length(event_id) = 24
|
|
19
|
+
AND substr(event_id, 1, 4) = 'cmd-'
|
|
20
|
+
AND substr(event_id, 5) NOT GLOB '*[^0-9a-f]*'
|
|
21
|
+
AND input_hash IS NOT NULL
|
|
22
|
+
AND NOT (typeof(input_hash) = 'text' AND input_hash IN ('/compact', '/clear'));
|
|
23
|
+
|
|
24
|
+
-- 2. Schema change: BEFORE INSERT/UPDATE guards so an older writer cannot
|
|
25
|
+
-- restore raw command values. Fixed error text; never interpolate values.
|
|
26
|
+
-- The UPDATE guard checks every update (including name/ID changes into M),
|
|
27
|
+
-- not just UPDATE OF input_hash.
|
|
28
|
+
CREATE TRIGGER trg_sec4_command_marker_guard_insert
|
|
29
|
+
BEFORE INSERT ON tool_events
|
|
30
|
+
BEGIN
|
|
31
|
+
SELECT CASE WHEN NEW.tool_name = 'local_command'
|
|
32
|
+
AND length(NEW.event_id) = 24
|
|
33
|
+
AND substr(NEW.event_id, 1, 4) = 'cmd-'
|
|
34
|
+
AND substr(NEW.event_id, 5) NOT GLOB '*[^0-9a-f]*'
|
|
35
|
+
AND NEW.input_hash IS NOT NULL
|
|
36
|
+
AND NOT (typeof(NEW.input_hash) = 'text' AND NEW.input_hash IN ('/compact', '/clear'))
|
|
37
|
+
THEN RAISE(ABORT, 'SEC4_COMMAND_MARKER_INVALID') END;
|
|
38
|
+
END;
|
|
39
|
+
|
|
40
|
+
CREATE TRIGGER trg_sec4_command_marker_guard_update
|
|
41
|
+
BEFORE UPDATE ON tool_events
|
|
42
|
+
BEGIN
|
|
43
|
+
SELECT CASE WHEN NEW.tool_name = 'local_command'
|
|
44
|
+
AND length(NEW.event_id) = 24
|
|
45
|
+
AND substr(NEW.event_id, 1, 4) = 'cmd-'
|
|
46
|
+
AND substr(NEW.event_id, 5) NOT GLOB '*[^0-9a-f]*'
|
|
47
|
+
AND NEW.input_hash IS NOT NULL
|
|
48
|
+
AND NOT (typeof(NEW.input_hash) = 'text' AND NEW.input_hash IN ('/compact', '/clear'))
|
|
49
|
+
THEN RAISE(ABORT, 'SEC4_COMMAND_MARKER_INVALID') END;
|
|
50
|
+
END;
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
-- 021_recommendation_feedback.sql — RIQ3 additive relevance-feedback + goal-preference storage.
|
|
2
|
+
-- SEC-101: enums, ids, scope keys, counts and timestamps only. No free-text, no raw content.
|
|
3
|
+
|
|
4
|
+
CREATE TABLE recommendation_feedback (
|
|
5
|
+
feedback_id TEXT PRIMARY KEY,
|
|
6
|
+
scope_key TEXT NOT NULL,
|
|
7
|
+
rec_identity TEXT NOT NULL,
|
|
8
|
+
feedback TEXT NOT NULL CHECK (feedback IN
|
|
9
|
+
('NOT_APPLICABLE', 'ALREADY_DONE', 'ACCEPTED_TRADEOFF', 'NOT_USEFUL')),
|
|
10
|
+
cooldown_until TEXT,
|
|
11
|
+
dismissed_materiality_band TEXT CHECK (dismissed_materiality_band IS NULL
|
|
12
|
+
OR dismissed_materiality_band IN ('SMALL', 'MODERATE', 'LARGE', 'UNKNOWN')),
|
|
13
|
+
created_at TEXT NOT NULL,
|
|
14
|
+
updated_at TEXT NOT NULL,
|
|
15
|
+
deleted_at TEXT
|
|
16
|
+
);
|
|
17
|
+
CREATE UNIQUE INDEX idx_rec_feedback_active
|
|
18
|
+
ON recommendation_feedback(scope_key, rec_identity) WHERE deleted_at IS NULL;
|
|
19
|
+
|
|
20
|
+
CREATE TABLE recommendation_goal_preference (
|
|
21
|
+
scope_key TEXT PRIMARY KEY,
|
|
22
|
+
goal TEXT NOT NULL CHECK (goal IN
|
|
23
|
+
('PRESERVE_QUALITY', 'REDUCE_RESOURCE_USE', 'INVESTIGATE_FRICTION')),
|
|
24
|
+
updated_at TEXT NOT NULL
|
|
25
|
+
);
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/ingest/boot-progress.ts — in-memory boot-scan progress for the loading page (BOOT-1).
|
|
3
|
+
*
|
|
4
|
+
* Running counters + a small event ring buffer published by the initial
|
|
5
|
+
* back-scan and surfaced on /api/status ONLY while the daemon is not ready
|
|
6
|
+
* (the router gates on isReady()). Everything here is process-memory; no DB,
|
|
7
|
+
* no filesystem access (readiness.ts already stat-loops roots per poll —
|
|
8
|
+
* this module must never add per-event fs work).
|
|
9
|
+
*
|
|
10
|
+
* SEC-101: event text carries workspace ids/slugs and counts only — callers
|
|
11
|
+
* must never pass file paths or transcript content.
|
|
12
|
+
*/
|
|
13
|
+
const MAX_EVENTS = 50;
|
|
14
|
+
const RATE_WINDOW_MS = 5_000;
|
|
15
|
+
const state = {
|
|
16
|
+
sessionsFound: 0,
|
|
17
|
+
tokensCounted: 0,
|
|
18
|
+
filesTotal: 0,
|
|
19
|
+
nextSeq: 1,
|
|
20
|
+
events: [],
|
|
21
|
+
/** Epoch-ms timestamps of recent fileParsed calls (pruned to the rate window). */
|
|
22
|
+
parseTicks: [],
|
|
23
|
+
};
|
|
24
|
+
function pruneTicks(nowMs) {
|
|
25
|
+
const cutoff = nowMs - RATE_WINDOW_MS;
|
|
26
|
+
let drop = 0;
|
|
27
|
+
while (drop < state.parseTicks.length && state.parseTicks[drop] < cutoff)
|
|
28
|
+
drop++;
|
|
29
|
+
if (drop > 0)
|
|
30
|
+
state.parseTicks.splice(0, drop);
|
|
31
|
+
}
|
|
32
|
+
/** A session row was inserted for the first time during the scan. */
|
|
33
|
+
export function bootSessionFound() {
|
|
34
|
+
state.sessionsFound++;
|
|
35
|
+
}
|
|
36
|
+
/** Tokens attributed by a newly ingested turn (all token classes summed). */
|
|
37
|
+
export function bootTokensCounted(tokens) {
|
|
38
|
+
if (Number.isFinite(tokens) && tokens > 0)
|
|
39
|
+
state.tokensCounted += tokens;
|
|
40
|
+
}
|
|
41
|
+
/** Total files discovered at boot-scan start (the progress denominator). */
|
|
42
|
+
export function bootFilesTotal(total) {
|
|
43
|
+
if (Number.isFinite(total) && total >= 0)
|
|
44
|
+
state.filesTotal = Math.floor(total);
|
|
45
|
+
}
|
|
46
|
+
/** A file finished parsing; feeds the derived files/s rate. */
|
|
47
|
+
export function bootFileParsed(nowMs = Date.now()) {
|
|
48
|
+
pruneTicks(nowMs);
|
|
49
|
+
state.parseTicks.push(nowMs);
|
|
50
|
+
}
|
|
51
|
+
/** Append a scan event to the ring buffer (oldest entries drop past 50). */
|
|
52
|
+
export function pushBootEvent(kind, text, ts = new Date().toISOString()) {
|
|
53
|
+
state.events.push({ seq: state.nextSeq++, ts, kind, text });
|
|
54
|
+
if (state.events.length > MAX_EVENTS)
|
|
55
|
+
state.events.splice(0, state.events.length - MAX_EVENTS);
|
|
56
|
+
}
|
|
57
|
+
/** Snapshot for the pre-ready /api/status surface. */
|
|
58
|
+
export function getBootProgress(nowMs = Date.now()) {
|
|
59
|
+
pruneTicks(nowMs);
|
|
60
|
+
return {
|
|
61
|
+
boot_sessions_found: state.sessionsFound,
|
|
62
|
+
boot_tokens_counted: state.tokensCounted,
|
|
63
|
+
boot_files_per_sec: Math.round((state.parseTicks.length / (RATE_WINDOW_MS / 1000)) * 10) / 10,
|
|
64
|
+
boot_files_total: state.filesTotal,
|
|
65
|
+
boot_scan_events: [...state.events],
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
/** Test-only reset. */
|
|
69
|
+
export function resetBootProgress() {
|
|
70
|
+
state.sessionsFound = 0;
|
|
71
|
+
state.tokensCounted = 0;
|
|
72
|
+
state.filesTotal = 0;
|
|
73
|
+
state.nextSeq = 1;
|
|
74
|
+
state.events = [];
|
|
75
|
+
state.parseTicks = [];
|
|
76
|
+
}
|