opencode-usage-coach 0.3.4 → 0.3.5
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 +28 -2
- package/dist/index.js +181 -33
- package/dist/tui.js +75 -101
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -28,6 +28,16 @@ the loop** — and ships a harness agent mode.
|
|
|
28
28
|
explicitly ask "run this through the harness" or "use the harness for this". The harness
|
|
29
29
|
tools are only available when the harness agent mode is active (see Install).
|
|
30
30
|
|
|
31
|
+
**Learning from failures (learning loop):**
|
|
32
|
+
- When `grade` returns FAIL, the harness enters a learning cycle: `record_failure` → `investigate` (root-cause analysis) → `verify_diagnosis` → `generalize` (extract a reusable rule).
|
|
33
|
+
- Rules accumulate in `rules.md` → the next `generate` call automatically includes them → the harness avoids repeating the same mistake.
|
|
34
|
+
- Tools: `record_failure`, `investigate`, `verify_diagnosis`, `generalize`.
|
|
35
|
+
|
|
36
|
+
**Domain knowledge base:**
|
|
37
|
+
- `investigate` and `generate` query a local domain DB before running — known facts are injected into the prompt ("Known facts from domain DB: ...").
|
|
38
|
+
- Unknown domains are investigated (webfetch/docs) and stored as graph nodes/edges → accumulates over time → evidence-based judgments instead of speculation.
|
|
39
|
+
- Storage: `nodes.ndjson` + `edges.ndjson` under the project state dir.
|
|
40
|
+
|
|
31
41
|
## Requirements
|
|
32
42
|
- opencode (tested on 1.17.13) with a quota-metered provider configured.
|
|
33
43
|
- `codexbar` CLI with your provider key wired (e.g. `codexbar config set-api-key --provider zai --stdin`).
|
|
@@ -203,9 +213,25 @@ Recurring issues and fixes — mostly learned the hard way during development.
|
|
|
203
213
|
- Harness completion sets `active:false` → hidden from the TUI.
|
|
204
214
|
- Override the state path with `UC_STATE_DIR` (forces global state).
|
|
205
215
|
|
|
216
|
+
**Key gotcha — opencode TUI `ctx` does NOT carry `session_id`.**
|
|
217
|
+
The slot context passed to `panel(ctx)` contains only `{ theme }`. There is no `session_id`/`sessionID` field. The current session ID lives in **`api.route.current.params.sessionID`** instead — the panel reads it from there. If you ever see harnesses from other sessions leaking in, the cause is almost certainly that `sid` resolved to empty (→ fallback broad scan).
|
|
218
|
+
|
|
219
|
+
**Debugging session isolation** (if it breaks again):
|
|
220
|
+
1. Check `~/.cache/opencode-usage-coach/projects/<hash>/tui-debug.log` — is `panel` being called? What `routeSid` value?
|
|
221
|
+
2. `api.route.current.params.sessionID` — populated? (Empty → panel falls back to scanning all sessions.)
|
|
222
|
+
3. New TUI code loaded? `tui-loaded.txt` (MARKER) should show `loaded-v2 ...`. If it still says `loaded`, the new dist isn't being picked up.
|
|
223
|
+
4. `appendFileSync` imported in `src/tui.tsx`? If missing, **all TUI debug logging silently fails** (ReferenceError swallowed by try/catch) — this wasted a lot of debugging time once.
|
|
224
|
+
|
|
225
|
+
**Past issue (fixed v0.3.4):** panel read `ctx.session_id` which was always `undefined` → fallback scanned every session → another session's active harness leaked in. Fixed by reading `api.route.current.params.sessionID`.
|
|
226
|
+
|
|
206
227
|
## Status
|
|
207
|
-
- ✅ Quota guardian + TUI panel (per-provider coach view,
|
|
208
|
-
- ✅ Harness: agent mode
|
|
228
|
+
- ✅ Quota guardian + TUI panel (per-provider coach view, 5h/1w gauges, collapsible Alt+H)
|
|
229
|
+
- ✅ Harness: agent mode with generate/grade tools (multi-model, 1 terminal)
|
|
230
|
+
- ✅ Deterministic loop via NEXT directives (parallel PATH A / sequential PATH B)
|
|
231
|
+
- ✅ Quota-aware tools (GO/THROTTLE/STOP drive model selection + concurrency)
|
|
232
|
+
- ✅ Learning loop (record_failure → investigate → verify_diagnosis → generalize → rules.md)
|
|
233
|
+
- ✅ Domain knowledge base (graph store, investigate/generate injection)
|
|
234
|
+
- ✅ Session isolation (api.route, per-session harness state)
|
|
209
235
|
- ✅ npm packaging (`opencode plugin install opencode-usage-coach`)
|
|
210
236
|
|
|
211
237
|
License: MIT.
|
package/dist/index.js
CHANGED
|
@@ -1,66 +1,145 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
|
-
import { mkdirSync, writeFileSync, appendFileSync, readFileSync, existsSync } from "fs";
|
|
2
|
+
import { mkdirSync as mkdirSync2, writeFileSync, appendFileSync as appendFileSync2, readFileSync as readFileSync2, existsSync as existsSync2 } from "fs";
|
|
3
3
|
import { spawn } from "child_process";
|
|
4
4
|
import { createHash } from "crypto";
|
|
5
5
|
import { homedir } from "os";
|
|
6
|
-
import { join, resolve, dirname } from "path";
|
|
6
|
+
import { join as join2, resolve, dirname } from "path";
|
|
7
7
|
import { tool } from "@opencode-ai/plugin";
|
|
8
|
+
|
|
9
|
+
// src/domain.ts
|
|
10
|
+
import { mkdirSync, appendFileSync, readFileSync, existsSync } from "fs";
|
|
11
|
+
import { join } from "path";
|
|
12
|
+
var BASE_DIR = "";
|
|
13
|
+
function initDomain(stateDir) {
|
|
14
|
+
BASE_DIR = stateDir;
|
|
15
|
+
}
|
|
16
|
+
var nodesFile = () => join(BASE_DIR, "nodes.ndjson");
|
|
17
|
+
var edgesFile = () => join(BASE_DIR, "edges.ndjson");
|
|
18
|
+
function readNdjson(path) {
|
|
19
|
+
try {
|
|
20
|
+
if (!existsSync(path)) return [];
|
|
21
|
+
return readFileSync(path, "utf8").split("\n").filter(Boolean).map((l) => JSON.parse(l));
|
|
22
|
+
} catch {
|
|
23
|
+
return [];
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
function readNodes() {
|
|
27
|
+
return readNdjson(nodesFile());
|
|
28
|
+
}
|
|
29
|
+
function readEdges() {
|
|
30
|
+
return readNdjson(edgesFile());
|
|
31
|
+
}
|
|
32
|
+
function uid(prefix) {
|
|
33
|
+
return `${prefix}_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
|
|
34
|
+
}
|
|
35
|
+
function addDomainNode(node) {
|
|
36
|
+
const full = { ...node, id: uid("node"), ts: (/* @__PURE__ */ new Date()).toISOString() };
|
|
37
|
+
try {
|
|
38
|
+
mkdirSync(BASE_DIR, { recursive: true });
|
|
39
|
+
appendFileSync(nodesFile(), JSON.stringify(full) + "\n");
|
|
40
|
+
} catch {
|
|
41
|
+
}
|
|
42
|
+
return full.id;
|
|
43
|
+
}
|
|
44
|
+
function queryDomain(keywords) {
|
|
45
|
+
const lc = keywords.map((k) => k.toLowerCase());
|
|
46
|
+
const nodes = readNodes();
|
|
47
|
+
const matched = nodes.filter((n) => {
|
|
48
|
+
const hay = (n.name + " " + JSON.stringify(n.props)).toLowerCase();
|
|
49
|
+
return lc.some((k) => k && hay.includes(k));
|
|
50
|
+
});
|
|
51
|
+
const ids = new Set(matched.map((n) => n.id));
|
|
52
|
+
const edges = readEdges().filter((e) => ids.has(e.from) || ids.has(e.to));
|
|
53
|
+
return { nodes: matched, edges };
|
|
54
|
+
}
|
|
55
|
+
function saveInvestigationResult(keywords, result, source) {
|
|
56
|
+
try {
|
|
57
|
+
return addDomainNode({
|
|
58
|
+
type: "fact",
|
|
59
|
+
name: keywords.join(" "),
|
|
60
|
+
props: { result },
|
|
61
|
+
source: source || "investigation",
|
|
62
|
+
confidence: 0.7
|
|
63
|
+
});
|
|
64
|
+
} catch {
|
|
65
|
+
return "";
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
// src/index.ts
|
|
8
70
|
var PLUGIN_NAME = "opencode-usage-coach";
|
|
9
71
|
var DEBUG = process.env.UC_DEBUG === "1";
|
|
10
72
|
var TTL_MS = Number(process.env.UC_TTL_MS ?? 6e4);
|
|
11
|
-
var STATE_DIR =
|
|
12
|
-
var STATE_FILE =
|
|
13
|
-
var HARNESS_FILE =
|
|
14
|
-
var LOG_FILE =
|
|
73
|
+
var STATE_DIR = join2(homedir(), ".cache", "opencode-usage-coach");
|
|
74
|
+
var STATE_FILE = join2(STATE_DIR, "state.json");
|
|
75
|
+
var HARNESS_FILE = join2(STATE_DIR, "harness.json");
|
|
76
|
+
var LOG_FILE = join2(STATE_DIR, "coach.log");
|
|
15
77
|
function projectStateDir(dir) {
|
|
16
78
|
const abs = resolve(dir || ".");
|
|
17
79
|
const h = createHash("sha1").update(abs).digest("hex").slice(0, 12);
|
|
18
|
-
return
|
|
80
|
+
return join2(homedir(), ".cache", "opencode-usage-coach", "projects", h);
|
|
19
81
|
}
|
|
20
82
|
function setStateDir(dir) {
|
|
21
83
|
STATE_DIR = process.env.UC_STATE_DIR ?? projectStateDir(dir);
|
|
22
|
-
STATE_FILE =
|
|
23
|
-
HARNESS_FILE =
|
|
24
|
-
LOG_FILE =
|
|
84
|
+
STATE_FILE = join2(STATE_DIR, "state.json");
|
|
85
|
+
HARNESS_FILE = join2(STATE_DIR, "harness.json");
|
|
86
|
+
LOG_FILE = join2(STATE_DIR, "coach.log");
|
|
25
87
|
}
|
|
26
88
|
var NOOP_HOOKS = {};
|
|
27
89
|
function log(msg) {
|
|
28
90
|
try {
|
|
29
|
-
|
|
91
|
+
appendFileSync2(LOG_FILE, `${(/* @__PURE__ */ new Date()).toISOString()} ${msg}
|
|
30
92
|
`);
|
|
31
93
|
} catch {
|
|
32
94
|
}
|
|
33
95
|
}
|
|
34
96
|
function writeState(c) {
|
|
35
97
|
try {
|
|
36
|
-
|
|
98
|
+
mkdirSync2(STATE_DIR, { recursive: true });
|
|
37
99
|
writeFileSync(STATE_FILE, JSON.stringify({ ...c, updatedAt: (/* @__PURE__ */ new Date()).toISOString() }));
|
|
38
100
|
} catch {
|
|
39
101
|
}
|
|
40
102
|
}
|
|
41
103
|
function rulesFile() {
|
|
42
|
-
return
|
|
104
|
+
return join2(STATE_DIR, "rules.md");
|
|
43
105
|
}
|
|
44
106
|
function failuresFile() {
|
|
45
|
-
return
|
|
107
|
+
return join2(STATE_DIR, "failures.ndjson");
|
|
46
108
|
}
|
|
47
109
|
function readRules() {
|
|
48
110
|
try {
|
|
49
111
|
const f = rulesFile();
|
|
50
|
-
if (!
|
|
51
|
-
return
|
|
112
|
+
if (!existsSync2(f)) return "";
|
|
113
|
+
return readFileSync2(f, "utf8").trim();
|
|
52
114
|
} catch {
|
|
53
115
|
return "";
|
|
54
116
|
}
|
|
55
117
|
}
|
|
118
|
+
function extractKeywords(text) {
|
|
119
|
+
try {
|
|
120
|
+
const STOP = /* @__PURE__ */ new Set(["the", "and", "for", "with", "that", "this", "from", "into", "your", "you", "are", "was", "but", "not", "all", "any", "use", "task", "prompt"]);
|
|
121
|
+
const seen = /* @__PURE__ */ new Set();
|
|
122
|
+
const out = [];
|
|
123
|
+
for (const raw of (text ?? "").toLowerCase().split(/[^a-z0-9_]+/)) {
|
|
124
|
+
const t = raw.trim();
|
|
125
|
+
if (t.length < 3 || STOP.has(t) || seen.has(t)) continue;
|
|
126
|
+
seen.add(t);
|
|
127
|
+
out.push(t);
|
|
128
|
+
if (out.length >= 16) break;
|
|
129
|
+
}
|
|
130
|
+
return out;
|
|
131
|
+
} catch {
|
|
132
|
+
return [];
|
|
133
|
+
}
|
|
134
|
+
}
|
|
56
135
|
function harnessFile(sessionID) {
|
|
57
|
-
return
|
|
136
|
+
return join2(STATE_DIR, sessionID || "_default", "harness.json");
|
|
58
137
|
}
|
|
59
138
|
function readHarness(sessionID) {
|
|
60
139
|
try {
|
|
61
140
|
const f = harnessFile(sessionID);
|
|
62
|
-
if (!
|
|
63
|
-
return JSON.parse(
|
|
141
|
+
if (!existsSync2(f)) return null;
|
|
142
|
+
return JSON.parse(readFileSync2(f, "utf8"));
|
|
64
143
|
} catch {
|
|
65
144
|
return null;
|
|
66
145
|
}
|
|
@@ -68,7 +147,7 @@ function readHarness(sessionID) {
|
|
|
68
147
|
function writeHarness(sessionID, h) {
|
|
69
148
|
try {
|
|
70
149
|
const f = harnessFile(sessionID);
|
|
71
|
-
|
|
150
|
+
mkdirSync2(dirname(f), { recursive: true });
|
|
72
151
|
h.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
73
152
|
writeFileSync(f, JSON.stringify(h, null, 2));
|
|
74
153
|
} catch {
|
|
@@ -77,14 +156,14 @@ function writeHarness(sessionID, h) {
|
|
|
77
156
|
function readHarnessCfg(dir) {
|
|
78
157
|
const tryRead = (p) => {
|
|
79
158
|
try {
|
|
80
|
-
if (
|
|
159
|
+
if (existsSync2(p)) return JSON.parse(readFileSync2(p, "utf8"));
|
|
81
160
|
} catch {
|
|
82
161
|
}
|
|
83
162
|
return {};
|
|
84
163
|
};
|
|
85
164
|
return {
|
|
86
|
-
...tryRead(
|
|
87
|
-
...tryRead(
|
|
165
|
+
...tryRead(join2(homedir(), ".config", "opencode-usage-coach", "harness.config.json")),
|
|
166
|
+
...tryRead(join2(dir, "harness.config.json"))
|
|
88
167
|
};
|
|
89
168
|
}
|
|
90
169
|
async function runModel(client, model, prompt, directory) {
|
|
@@ -105,7 +184,12 @@ async function runModel(client, model, prompt, directory) {
|
|
|
105
184
|
const parts = resp?.data?.parts ?? resp?.parts ?? [];
|
|
106
185
|
const text = parts.filter((p) => p?.type === "text").map((p) => p?.text ?? "").join("");
|
|
107
186
|
try {
|
|
108
|
-
await client.session.
|
|
187
|
+
const summary = await client.session.summarize?.({ path: { id } });
|
|
188
|
+
log(`runModel(${model}): sub-session summary: ${JSON.stringify(summary?.data ?? summary).slice(0, 300)}`);
|
|
189
|
+
} catch {
|
|
190
|
+
}
|
|
191
|
+
try {
|
|
192
|
+
await client.session.delete?.({ path: { id } });
|
|
109
193
|
} catch {
|
|
110
194
|
}
|
|
111
195
|
log(`runModel(${model}): done ${elapsed}s, ${text.length} chars`);
|
|
@@ -245,6 +329,7 @@ var LOADING = { decision: "GO", advice: "quota loading\u2026", weekly: -1, month
|
|
|
245
329
|
async function UsageCoachPlugin(input) {
|
|
246
330
|
try {
|
|
247
331
|
setStateDir(input.directory);
|
|
332
|
+
initDomain(STATE_DIR);
|
|
248
333
|
const cfg0 = readHarnessCfg(input.directory);
|
|
249
334
|
const PROVIDER = process.env.UC_PROVIDER ?? cfg0.provider ?? "";
|
|
250
335
|
const LIGHTER = process.env.UC_LIGHTER_MODEL ?? cfg0.lighterModel ?? "a lighter model";
|
|
@@ -265,6 +350,10 @@ async function UsageCoachPlugin(input) {
|
|
|
265
350
|
providers = await fetchProvidersCoach();
|
|
266
351
|
} catch {
|
|
267
352
|
}
|
|
353
|
+
if (providers.length > 0 && last.weekly < 0) {
|
|
354
|
+
const p0 = providers[0];
|
|
355
|
+
last = { ...last, weekly: p0.weekly, fiveHour: p0.fiveHour, monthly: p0.weekly >= 0 ? 0 : -1, advice: p0.advice, decision: p0.weekly >= STOP_WK ? "STOP" : p0.weekly >= THR_WK ? "THROTTLE" : "GO" };
|
|
356
|
+
}
|
|
268
357
|
writeState({ ...last, providers, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
269
358
|
log(`${last.decision} | weekly=${last.weekly}% 5h=${last.fiveHour}% | providers=${providers.length}`);
|
|
270
359
|
} catch (e) {
|
|
@@ -295,7 +384,9 @@ async function UsageCoachPlugin(input) {
|
|
|
295
384
|
log(`event err: ${String(e)}`);
|
|
296
385
|
}
|
|
297
386
|
},
|
|
298
|
-
// ACT(1) hard gate
|
|
387
|
+
// ACT(1) hard gate — ONLY for harness tools (generate/grade/etc.) that consume quota.
|
|
388
|
+
// General tools (read/edit/bash/grep/task) are NEVER blocked — they don't consume model quota.
|
|
389
|
+
// This ensures Agent-Factory-Coordinator and other modes work freely even at STOP.
|
|
299
390
|
"tool.execute.before": async (_input) => {
|
|
300
391
|
let decision = "GO";
|
|
301
392
|
try {
|
|
@@ -304,7 +395,10 @@ async function UsageCoachPlugin(input) {
|
|
|
304
395
|
decision = "GO";
|
|
305
396
|
}
|
|
306
397
|
if (decision === "STOP") {
|
|
307
|
-
|
|
398
|
+
const harnessTools = ["generate", "generate_batch", "grade", "investigate", "verify_diagnosis", "generalize"];
|
|
399
|
+
if (harnessTools.includes(_input.tool)) {
|
|
400
|
+
throw new Error(`[${PLUGIN_NAME}] blocked: quota limit exceeded. ${current().advice}`);
|
|
401
|
+
}
|
|
308
402
|
}
|
|
309
403
|
},
|
|
310
404
|
// ACT(2) inject coaching into system prompt (double defense). Silent on error.
|
|
@@ -361,9 +455,12 @@ Then: harness_done(). Follow the [usage-coach NEXT] directive each tool returns.
|
|
|
361
455
|
model: tool.schema.string().optional()
|
|
362
456
|
},
|
|
363
457
|
async execute(args, ctx) {
|
|
458
|
+
const cfg = readHarnessCfg(ctx.directory);
|
|
364
459
|
const h = readHarness(ctx.sessionID) ?? { name: "batch", total: 0, current: 0, tasks: [], usage: {}, active: true };
|
|
365
460
|
h.tasks = h.tasks.filter((x) => x.id !== args.id);
|
|
366
|
-
|
|
461
|
+
const model = args.model || cfg.generator || "";
|
|
462
|
+
if (!model) return `ERROR: task ${args.id} has no model and no generator configured. Set "generator" in harness.config.json.`;
|
|
463
|
+
h.tasks.push({ id: args.id, title: args.title, status: args.status, model, revisions: args.revisions ?? 0, score: args.score ?? null, startedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
367
464
|
if (args.id > h.current) h.current = args.id;
|
|
368
465
|
writeHarness(ctx.sessionID, h);
|
|
369
466
|
return `task ${args.id} -> ${args.status}${args.score ? ` (${args.score})` : ""}`;
|
|
@@ -394,8 +491,8 @@ Then: harness_done(). Follow the [usage-coach NEXT] directive each tool returns.
|
|
|
394
491
|
async execute(args, _ctx) {
|
|
395
492
|
const rec = { ts: (/* @__PURE__ */ new Date()).toISOString(), task: args.task, prompt: args.prompt, gradeResult: args.gradeResult, model: args.model, revisions: args.revisions };
|
|
396
493
|
try {
|
|
397
|
-
|
|
398
|
-
|
|
494
|
+
mkdirSync2(STATE_DIR, { recursive: true });
|
|
495
|
+
appendFileSync2(failuresFile(), JSON.stringify(rec) + "\n");
|
|
399
496
|
} catch (e) {
|
|
400
497
|
log(`record_failure err: ${String(e)}`);
|
|
401
498
|
}
|
|
@@ -413,6 +510,25 @@ Then: harness_done(). Follow the [usage-coach NEXT] directive each tool returns.
|
|
|
413
510
|
async execute(args, ctx) {
|
|
414
511
|
const cfg = readHarnessCfg(ctx.directory);
|
|
415
512
|
if (!cfg.generator) return 'ERROR: no generator model configured. Set "generator" in harness.config.json (see harness.config.example.json).';
|
|
513
|
+
let domainPrefix = "";
|
|
514
|
+
let keywords = [];
|
|
515
|
+
let domainEmpty = true;
|
|
516
|
+
try {
|
|
517
|
+
keywords = extractKeywords(`${args.task} ${args.gradeResult}`);
|
|
518
|
+
if (keywords.length) {
|
|
519
|
+
const { nodes, edges } = queryDomain(keywords);
|
|
520
|
+
if (nodes && nodes.length || edges && edges.length) {
|
|
521
|
+
domainEmpty = false;
|
|
522
|
+
domainPrefix = `Known facts from domain DB: ${JSON.stringify({ nodes, edges })}. Use these if relevant.
|
|
523
|
+
|
|
524
|
+
---
|
|
525
|
+
|
|
526
|
+
`;
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
} catch (e) {
|
|
530
|
+
log(`investigate domain query err: ${String(e)}`);
|
|
531
|
+
}
|
|
416
532
|
const rcaPrompt = `A task failed. Analyze the ROOT CAUSE (not just the symptom).
|
|
417
533
|
Task: ${args.task}
|
|
418
534
|
What was expected (from grade): ${args.gradeResult}
|
|
@@ -421,7 +537,14 @@ Output a structured root cause:
|
|
|
421
537
|
category: (one of: constraint-violation, missing-context, tool-misuse, model-limitation, other)
|
|
422
538
|
explanation: <why it failed>
|
|
423
539
|
evidence: <file/line or specific quote>`;
|
|
424
|
-
const out = await runModel(input.client, cfg.generator, rcaPrompt, ctx.directory);
|
|
540
|
+
const out = await runModel(input.client, cfg.generator, domainPrefix + rcaPrompt, ctx.directory);
|
|
541
|
+
if (domainEmpty && keywords.length) {
|
|
542
|
+
try {
|
|
543
|
+
saveInvestigationResult(keywords, out, "investigate");
|
|
544
|
+
} catch (e) {
|
|
545
|
+
log(`investigate save err: ${String(e)}`);
|
|
546
|
+
}
|
|
547
|
+
}
|
|
425
548
|
return out + "\n[usage-coach NEXT] call verify_diagnosis with this diagnosis.";
|
|
426
549
|
}
|
|
427
550
|
}),
|
|
@@ -473,8 +596,8 @@ Keep it concrete and actionable.`;
|
|
|
473
596
|
const rule = out;
|
|
474
597
|
try {
|
|
475
598
|
const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
476
|
-
|
|
477
|
-
|
|
599
|
+
mkdirSync2(STATE_DIR, { recursive: true });
|
|
600
|
+
appendFileSync2(rulesFile(), `## Rule (${date})
|
|
478
601
|
${rule}
|
|
479
602
|
Origin: ${args.task}
|
|
480
603
|
|
|
@@ -502,13 +625,38 @@ Origin: ${args.task}
|
|
|
502
625
|
const throttle = decision === "THROTTLE" && cfg.lighterModel;
|
|
503
626
|
const model = throttle ? cfg.lighterModel : cfg.generator;
|
|
504
627
|
const rules = readRules();
|
|
505
|
-
|
|
628
|
+
let prefix = rules ? `Lessons learned from previous failures (apply where relevant):
|
|
506
629
|
${rules}
|
|
507
630
|
|
|
508
631
|
---
|
|
509
632
|
|
|
510
633
|
` : "";
|
|
634
|
+
let keywords = [];
|
|
635
|
+
let domainEmpty = true;
|
|
636
|
+
try {
|
|
637
|
+
keywords = extractKeywords(args.prompt);
|
|
638
|
+
if (keywords.length) {
|
|
639
|
+
const { nodes, edges } = queryDomain(keywords);
|
|
640
|
+
if (nodes && nodes.length || edges && edges.length) {
|
|
641
|
+
domainEmpty = false;
|
|
642
|
+
prefix = `Known facts from domain DB: ${JSON.stringify({ nodes, edges })}. Use these if relevant.
|
|
643
|
+
|
|
644
|
+
---
|
|
645
|
+
|
|
646
|
+
` + prefix;
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
} catch (e) {
|
|
650
|
+
log(`generate domain query err: ${String(e)}`);
|
|
651
|
+
}
|
|
511
652
|
const out = await runModel(input.client, model, prefix + args.prompt, ctx.directory);
|
|
653
|
+
if (domainEmpty && keywords.length) {
|
|
654
|
+
try {
|
|
655
|
+
saveInvestigationResult(keywords, out, "generate");
|
|
656
|
+
} catch (e) {
|
|
657
|
+
log(`generate save err: ${String(e)}`);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
512
660
|
return out + (throttle ? `
|
|
513
661
|
[usage-coach] quota THROTTLE \u2014 used lighter model ${cfg.lighterModel}` : "") + `
|
|
514
662
|
[usage-coach NEXT] call task_update(i, title, "grading"), then grade to evaluate this work.`;
|
package/dist/tui.js
CHANGED
|
@@ -86,10 +86,12 @@ var TLABEL = {
|
|
|
86
86
|
halted_quota: "quota-halt"
|
|
87
87
|
};
|
|
88
88
|
function barFill(p) {
|
|
89
|
-
|
|
89
|
+
const n = p <= 0 ? 0 : Math.max(1, Math.min(10, Math.round(p / 10)));
|
|
90
|
+
return "\u2588".repeat(n);
|
|
90
91
|
}
|
|
91
92
|
function barEmpty(p) {
|
|
92
|
-
|
|
93
|
+
const n = p <= 0 ? 0 : Math.max(1, Math.min(10, Math.round(p / 10)));
|
|
94
|
+
return "\u2591".repeat(10 - n);
|
|
93
95
|
}
|
|
94
96
|
function initializeTui(api, disposeRoot) {
|
|
95
97
|
STATE_DIR = process.env.UC_STATE_DIR ?? projectStateDir(api.state.path.directory);
|
|
@@ -236,7 +238,7 @@ function initializeTui(api, disposeRoot) {
|
|
|
236
238
|
_$insert(_el$12, () => p.fiveHour, _el$14);
|
|
237
239
|
_$insert(_el$12, () => p.fiveHourReset, null);
|
|
238
240
|
_$effect((_p$) => {
|
|
239
|
-
var _v$ = st("text"), _v$2 = st("
|
|
241
|
+
var _v$ = st("text"), _v$2 = st("text");
|
|
240
242
|
_v$ !== _p$.e && (_p$.e = _$setProp(_el$10, "style", _v$, _p$.e));
|
|
241
243
|
_v$2 !== _p$.t && (_p$.t = _$setProp(_el$11, "style", _v$2, _p$.t));
|
|
242
244
|
return _p$;
|
|
@@ -253,7 +255,7 @@ function initializeTui(api, disposeRoot) {
|
|
|
253
255
|
_$insertNode(_el$15, _el$19);
|
|
254
256
|
_$insertNode(_el$15, _el$20);
|
|
255
257
|
_$setProp(_el$15, "flexDirection", "row");
|
|
256
|
-
_$insertNode(_el$16, _$createTextNode(`
|
|
258
|
+
_$insertNode(_el$16, _$createTextNode(` 1w `));
|
|
257
259
|
_$insert(_el$18, () => barFill(p.weekly));
|
|
258
260
|
_$insert(_el$19, () => barEmpty(p.weekly));
|
|
259
261
|
_$insertNode(_el$20, _el$21);
|
|
@@ -261,7 +263,7 @@ function initializeTui(api, disposeRoot) {
|
|
|
261
263
|
_$insert(_el$20, () => p.weekly, _el$22);
|
|
262
264
|
_$insert(_el$20, () => p.weeklyReset, null);
|
|
263
265
|
_$effect((_p$) => {
|
|
264
|
-
var _v$3 = st("text"), _v$4 = st("
|
|
266
|
+
var _v$3 = st("text"), _v$4 = st("text");
|
|
265
267
|
_v$3 !== _p$.e && (_p$.e = _$setProp(_el$18, "style", _v$3, _p$.e));
|
|
266
268
|
_v$4 !== _p$.t && (_p$.t = _$setProp(_el$19, "style", _v$4, _p$.t));
|
|
267
269
|
return _p$;
|
|
@@ -281,7 +283,7 @@ function initializeTui(api, disposeRoot) {
|
|
|
281
283
|
}
|
|
282
284
|
} else {
|
|
283
285
|
nodes.push((() => {
|
|
284
|
-
var _el$27 = _$createElement("box"), _el$28 = _$createElement("text"), _el$30 = _$createElement("text"), _el$31 = _$createElement("text"), _el$32 = _$createElement("text")
|
|
286
|
+
var _el$27 = _$createElement("box"), _el$28 = _$createElement("text"), _el$30 = _$createElement("text"), _el$31 = _$createElement("text"), _el$32 = _$createElement("text");
|
|
285
287
|
_$insertNode(_el$27, _el$28);
|
|
286
288
|
_$insertNode(_el$27, _el$30);
|
|
287
289
|
_$insertNode(_el$27, _el$31);
|
|
@@ -290,11 +292,9 @@ function initializeTui(api, disposeRoot) {
|
|
|
290
292
|
_$insertNode(_el$28, _$createTextNode(` 5h `));
|
|
291
293
|
_$insert(_el$30, () => barFill(s.fiveHour));
|
|
292
294
|
_$insert(_el$31, () => barEmpty(s.fiveHour));
|
|
293
|
-
_$insertNode(_el$32,
|
|
294
|
-
_$insertNode(_el$32, _el$34);
|
|
295
|
-
_$insert(_el$32, () => s.fiveHour, _el$34);
|
|
295
|
+
_$insertNode(_el$32, _$createTextNode(` 0%`));
|
|
296
296
|
_$effect((_p$) => {
|
|
297
|
-
var _v$5 = st("text"), _v$6 = st("
|
|
297
|
+
var _v$5 = st("text"), _v$6 = st("text");
|
|
298
298
|
_v$5 !== _p$.e && (_p$.e = _$setProp(_el$30, "style", _v$5, _p$.e));
|
|
299
299
|
_v$6 !== _p$.t && (_p$.t = _$setProp(_el$31, "style", _v$6, _p$.t));
|
|
300
300
|
return _p$;
|
|
@@ -305,77 +305,51 @@ function initializeTui(api, disposeRoot) {
|
|
|
305
305
|
return _el$27;
|
|
306
306
|
})());
|
|
307
307
|
nodes.push((() => {
|
|
308
|
-
var _el$
|
|
309
|
-
_$insertNode(_el$
|
|
310
|
-
_$insertNode(_el$
|
|
311
|
-
_$insertNode(_el$
|
|
312
|
-
_$insertNode(_el$
|
|
313
|
-
_$setProp(_el$
|
|
314
|
-
_$insertNode(_el$
|
|
315
|
-
_$insert(_el$
|
|
316
|
-
_$insert(_el$
|
|
317
|
-
_$insertNode(_el$
|
|
318
|
-
_$insertNode(_el$40, _el$42);
|
|
319
|
-
_$insert(_el$40, () => s.weekly, _el$42);
|
|
308
|
+
var _el$34 = _$createElement("box"), _el$35 = _$createElement("text"), _el$37 = _$createElement("text"), _el$38 = _$createElement("text"), _el$39 = _$createElement("text");
|
|
309
|
+
_$insertNode(_el$34, _el$35);
|
|
310
|
+
_$insertNode(_el$34, _el$37);
|
|
311
|
+
_$insertNode(_el$34, _el$38);
|
|
312
|
+
_$insertNode(_el$34, _el$39);
|
|
313
|
+
_$setProp(_el$34, "flexDirection", "row");
|
|
314
|
+
_$insertNode(_el$35, _$createTextNode(` 1w `));
|
|
315
|
+
_$insert(_el$37, () => barFill(s.weekly));
|
|
316
|
+
_$insert(_el$38, () => barEmpty(s.weekly));
|
|
317
|
+
_$insertNode(_el$39, _$createTextNode(` 0%`));
|
|
320
318
|
_$effect((_p$) => {
|
|
321
|
-
var _v$7 = st("text"), _v$8 = st("
|
|
322
|
-
_v$7 !== _p$.e && (_p$.e = _$setProp(_el$
|
|
323
|
-
_v$8 !== _p$.t && (_p$.t = _$setProp(_el$
|
|
319
|
+
var _v$7 = st("text"), _v$8 = st("text");
|
|
320
|
+
_v$7 !== _p$.e && (_p$.e = _$setProp(_el$37, "style", _v$7, _p$.e));
|
|
321
|
+
_v$8 !== _p$.t && (_p$.t = _$setProp(_el$38, "style", _v$8, _p$.t));
|
|
324
322
|
return _p$;
|
|
325
323
|
}, {
|
|
326
324
|
e: void 0,
|
|
327
325
|
t: void 0
|
|
328
326
|
});
|
|
329
|
-
return _el$
|
|
330
|
-
})());
|
|
331
|
-
nodes.push((() => {
|
|
332
|
-
var _el$43 = _$createElement("box"), _el$44 = _$createElement("text"), _el$46 = _$createElement("text"), _el$47 = _$createElement("text"), _el$48 = _$createElement("text"), _el$49 = _$createTextNode(` `), _el$50 = _$createTextNode(`%`);
|
|
333
|
-
_$insertNode(_el$43, _el$44);
|
|
334
|
-
_$insertNode(_el$43, _el$46);
|
|
335
|
-
_$insertNode(_el$43, _el$47);
|
|
336
|
-
_$insertNode(_el$43, _el$48);
|
|
337
|
-
_$setProp(_el$43, "flexDirection", "row");
|
|
338
|
-
_$insertNode(_el$44, _$createTextNode(` mo `));
|
|
339
|
-
_$insert(_el$46, () => barFill(s.monthly));
|
|
340
|
-
_$insert(_el$47, () => barEmpty(s.monthly));
|
|
341
|
-
_$insertNode(_el$48, _el$49);
|
|
342
|
-
_$insertNode(_el$48, _el$50);
|
|
343
|
-
_$insert(_el$48, () => s.monthly, _el$50);
|
|
344
|
-
_$effect((_p$) => {
|
|
345
|
-
var _v$9 = st("text"), _v$0 = st("textMuted");
|
|
346
|
-
_v$9 !== _p$.e && (_p$.e = _$setProp(_el$46, "style", _v$9, _p$.e));
|
|
347
|
-
_v$0 !== _p$.t && (_p$.t = _$setProp(_el$47, "style", _v$0, _p$.t));
|
|
348
|
-
return _p$;
|
|
349
|
-
}, {
|
|
350
|
-
e: void 0,
|
|
351
|
-
t: void 0
|
|
352
|
-
});
|
|
353
|
-
return _el$43;
|
|
327
|
+
return _el$34;
|
|
354
328
|
})());
|
|
355
329
|
}
|
|
356
330
|
} else {
|
|
357
331
|
nodes.push((() => {
|
|
358
|
-
var _el$
|
|
359
|
-
_$insertNode(_el$
|
|
360
|
-
return _el$
|
|
332
|
+
var _el$41 = _$createElement("text");
|
|
333
|
+
_$insertNode(_el$41, _$createTextNode(`usage-coach: ...`));
|
|
334
|
+
return _el$41;
|
|
361
335
|
})());
|
|
362
336
|
}
|
|
363
337
|
if (h && h.active !== false && h.tasks.length > 0) {
|
|
364
338
|
nodes.push((() => {
|
|
365
|
-
var _el$
|
|
366
|
-
_$insertNode(_el$
|
|
367
|
-
return _el$
|
|
339
|
+
var _el$43 = _$createElement("text");
|
|
340
|
+
_$insertNode(_el$43, _$createTextNode(` `));
|
|
341
|
+
return _el$43;
|
|
368
342
|
})());
|
|
369
343
|
nodes.push((() => {
|
|
370
|
-
var _el$
|
|
371
|
-
_$insertNode(_el$
|
|
372
|
-
_$insertNode(_el$
|
|
373
|
-
_$insertNode(_el$
|
|
374
|
-
_$insert(_el$
|
|
375
|
-
_$insert(_el$
|
|
376
|
-
_$insert(_el$
|
|
377
|
-
_$effect((_$p) => _$setProp(_el$
|
|
378
|
-
return _el$
|
|
344
|
+
var _el$45 = _$createElement("text"), _el$46 = _$createTextNode(`harness: `), _el$47 = _$createTextNode(` `), _el$48 = _$createTextNode(`/`);
|
|
345
|
+
_$insertNode(_el$45, _el$46);
|
|
346
|
+
_$insertNode(_el$45, _el$47);
|
|
347
|
+
_$insertNode(_el$45, _el$48);
|
|
348
|
+
_$insert(_el$45, () => h.name, _el$47);
|
|
349
|
+
_$insert(_el$45, () => h.current, _el$48);
|
|
350
|
+
_$insert(_el$45, () => h.total, null);
|
|
351
|
+
_$effect((_$p) => _$setProp(_el$45, "style", st("textMuted"), _$p));
|
|
352
|
+
return _el$45;
|
|
379
353
|
})());
|
|
380
354
|
for (const t of h.tasks) {
|
|
381
355
|
const sKey = statusKey[t.status] ?? "text";
|
|
@@ -385,54 +359,54 @@ function initializeTui(api, disposeRoot) {
|
|
|
385
359
|
const elapsed = t.startedAt ? Math.max(0, Math.round((Date.now() - new Date(t.startedAt).getTime()) / 1e3)) : 0;
|
|
386
360
|
const elapsedStr = t.status === "completed" || t.status === "failed" ? "" : elapsed > 0 ? ` ${elapsed}s` : "";
|
|
387
361
|
nodes.push((() => {
|
|
388
|
-
var _el$
|
|
389
|
-
_$insertNode(_el$
|
|
390
|
-
_$insertNode(_el$
|
|
391
|
-
_$insertNode(_el$
|
|
392
|
-
_$insert(_el$
|
|
393
|
-
_$insert(_el$
|
|
394
|
-
_$insert(_el$
|
|
395
|
-
_$insert(_el$
|
|
396
|
-
_$insert(_el$
|
|
397
|
-
_$insert(_el$
|
|
398
|
-
_$effect((_$p) => _$setProp(_el$
|
|
399
|
-
return _el$
|
|
362
|
+
var _el$49 = _$createElement("text"), _el$50 = _$createTextNode(` \u25CF `), _el$51 = _$createTextNode(` `), _el$52 = _$createTextNode(` `);
|
|
363
|
+
_$insertNode(_el$49, _el$50);
|
|
364
|
+
_$insertNode(_el$49, _el$51);
|
|
365
|
+
_$insertNode(_el$49, _el$52);
|
|
366
|
+
_$insert(_el$49, () => t.id, _el$51);
|
|
367
|
+
_$insert(_el$49, mdl, _el$51);
|
|
368
|
+
_$insert(_el$49, lbl, _el$52);
|
|
369
|
+
_$insert(_el$49, rev, _el$52);
|
|
370
|
+
_$insert(_el$49, elapsedStr, _el$52);
|
|
371
|
+
_$insert(_el$49, () => t.title, null);
|
|
372
|
+
_$effect((_$p) => _$setProp(_el$49, "style", st(sKey), _$p));
|
|
373
|
+
return _el$49;
|
|
400
374
|
})());
|
|
401
375
|
const pv = t.model ? (t.model.split("/")[0] ?? "").split("-")[0] : "";
|
|
402
|
-
const provCoach = s?.providers?.find((p) => p.id === pv || pv && p.id.startsWith(pv) || pv && pv.startsWith(p.id));
|
|
403
|
-
const rawPct = provCoach
|
|
376
|
+
const provCoach = pv ? s?.providers?.find((p) => p.id === pv || pv && p.id.startsWith(pv) || pv && pv.startsWith(p.id)) : s?.providers?.[0];
|
|
377
|
+
const rawPct = provCoach?.fiveHour ?? s?.fiveHour ?? -1;
|
|
404
378
|
const pct = rawPct < 0 ? 0 : rawPct;
|
|
405
379
|
const pctLabel = rawPct < 0 ? "n/a" : `${rawPct}%`;
|
|
406
380
|
nodes.push((() => {
|
|
407
|
-
var _el$
|
|
408
|
-
_$insertNode(_el$
|
|
409
|
-
_$insertNode(_el$
|
|
410
|
-
_$insertNode(_el$
|
|
411
|
-
_$insertNode(_el$
|
|
412
|
-
_$setProp(_el$
|
|
413
|
-
_$insertNode(_el$
|
|
414
|
-
_$insert(_el$
|
|
415
|
-
_$insert(_el$
|
|
416
|
-
_$insertNode(_el$
|
|
417
|
-
_$insert(_el$
|
|
381
|
+
var _el$53 = _$createElement("box"), _el$54 = _$createElement("text"), _el$56 = _$createElement("text"), _el$57 = _$createElement("text"), _el$58 = _$createElement("text"), _el$59 = _$createTextNode(` `);
|
|
382
|
+
_$insertNode(_el$53, _el$54);
|
|
383
|
+
_$insertNode(_el$53, _el$56);
|
|
384
|
+
_$insertNode(_el$53, _el$57);
|
|
385
|
+
_$insertNode(_el$53, _el$58);
|
|
386
|
+
_$setProp(_el$53, "flexDirection", "row");
|
|
387
|
+
_$insertNode(_el$54, _$createTextNode(` 5h `));
|
|
388
|
+
_$insert(_el$56, () => barFill(pct));
|
|
389
|
+
_$insert(_el$57, () => barEmpty(pct));
|
|
390
|
+
_$insertNode(_el$58, _el$59);
|
|
391
|
+
_$insert(_el$58, pctLabel, null);
|
|
418
392
|
_$effect((_p$) => {
|
|
419
|
-
var _v$
|
|
420
|
-
_v$
|
|
421
|
-
_v$
|
|
393
|
+
var _v$9 = st("text"), _v$0 = st("text");
|
|
394
|
+
_v$9 !== _p$.e && (_p$.e = _$setProp(_el$56, "style", _v$9, _p$.e));
|
|
395
|
+
_v$0 !== _p$.t && (_p$.t = _$setProp(_el$57, "style", _v$0, _p$.t));
|
|
422
396
|
return _p$;
|
|
423
397
|
}, {
|
|
424
398
|
e: void 0,
|
|
425
399
|
t: void 0
|
|
426
400
|
});
|
|
427
|
-
return _el$
|
|
401
|
+
return _el$53;
|
|
428
402
|
})());
|
|
429
403
|
}
|
|
430
404
|
}
|
|
431
405
|
return (() => {
|
|
432
|
-
var _el$
|
|
433
|
-
_$setProp(_el$
|
|
434
|
-
_$insert(_el$
|
|
435
|
-
return _el$
|
|
406
|
+
var _el$60 = _$createElement("box");
|
|
407
|
+
_$setProp(_el$60, "flexDirection", "column");
|
|
408
|
+
_$insert(_el$60, nodes);
|
|
409
|
+
return _el$60;
|
|
436
410
|
})();
|
|
437
411
|
};
|
|
438
412
|
tlog("registering slots");
|
|
@@ -446,9 +420,9 @@ function initializeTui(api, disposeRoot) {
|
|
|
446
420
|
} catch (e) {
|
|
447
421
|
tlog(`sidebar_footer err: ${String(e)}`);
|
|
448
422
|
return (() => {
|
|
449
|
-
var _el$
|
|
450
|
-
_$insertNode(_el$
|
|
451
|
-
return _el$
|
|
423
|
+
var _el$61 = _$createElement("text");
|
|
424
|
+
_$insertNode(_el$61, _$createTextNode(`usage-coach`));
|
|
425
|
+
return _el$61;
|
|
452
426
|
})();
|
|
453
427
|
}
|
|
454
428
|
}
|
package/package.json
CHANGED