opencode-usage-coach 0.3.3 → 0.3.4
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/dist/index.js +113 -1
- package/dist/tui.js +47 -19
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -41,6 +41,9 @@ function writeState(c) {
|
|
|
41
41
|
function rulesFile() {
|
|
42
42
|
return join(STATE_DIR, "rules.md");
|
|
43
43
|
}
|
|
44
|
+
function failuresFile() {
|
|
45
|
+
return join(STATE_DIR, "failures.ndjson");
|
|
46
|
+
}
|
|
44
47
|
function readRules() {
|
|
45
48
|
try {
|
|
46
49
|
const f = rulesFile();
|
|
@@ -379,6 +382,110 @@ Then: harness_done(). Follow the [usage-coach NEXT] directive each tool returns.
|
|
|
379
382
|
return "Harness complete.";
|
|
380
383
|
}
|
|
381
384
|
}),
|
|
385
|
+
record_failure: tool({
|
|
386
|
+
description: "Stage 1 (RECORD) of the learning loop. Append a failure record to failures.ndjson for later root-cause analysis.",
|
|
387
|
+
args: {
|
|
388
|
+
task: tool.schema.string(),
|
|
389
|
+
prompt: tool.schema.string(),
|
|
390
|
+
gradeResult: tool.schema.string(),
|
|
391
|
+
model: tool.schema.string().optional(),
|
|
392
|
+
revisions: tool.schema.number().optional()
|
|
393
|
+
},
|
|
394
|
+
async execute(args, _ctx) {
|
|
395
|
+
const rec = { ts: (/* @__PURE__ */ new Date()).toISOString(), task: args.task, prompt: args.prompt, gradeResult: args.gradeResult, model: args.model, revisions: args.revisions };
|
|
396
|
+
try {
|
|
397
|
+
mkdirSync(STATE_DIR, { recursive: true });
|
|
398
|
+
appendFileSync(failuresFile(), JSON.stringify(rec) + "\n");
|
|
399
|
+
} catch (e) {
|
|
400
|
+
log(`record_failure err: ${String(e)}`);
|
|
401
|
+
}
|
|
402
|
+
return `Failure recorded. [usage-coach NEXT] call investigate({failure: ${JSON.stringify(rec)}}) to find the root cause.`;
|
|
403
|
+
}
|
|
404
|
+
}),
|
|
405
|
+
investigate: tool({
|
|
406
|
+
description: "Stage 2 (INVESTIGATE) of the learning loop. Run the generator to analyze the ROOT CAUSE of a failure (not just the symptom).",
|
|
407
|
+
args: {
|
|
408
|
+
task: tool.schema.string(),
|
|
409
|
+
prompt: tool.schema.string(),
|
|
410
|
+
gradeResult: tool.schema.string(),
|
|
411
|
+
model: tool.schema.string().optional()
|
|
412
|
+
},
|
|
413
|
+
async execute(args, ctx) {
|
|
414
|
+
const cfg = readHarnessCfg(ctx.directory);
|
|
415
|
+
if (!cfg.generator) return 'ERROR: no generator model configured. Set "generator" in harness.config.json (see harness.config.example.json).';
|
|
416
|
+
const rcaPrompt = `A task failed. Analyze the ROOT CAUSE (not just the symptom).
|
|
417
|
+
Task: ${args.task}
|
|
418
|
+
What was expected (from grade): ${args.gradeResult}
|
|
419
|
+
Read relevant files in the directory if needed.
|
|
420
|
+
Output a structured root cause:
|
|
421
|
+
category: (one of: constraint-violation, missing-context, tool-misuse, model-limitation, other)
|
|
422
|
+
explanation: <why it failed>
|
|
423
|
+
evidence: <file/line or specific quote>`;
|
|
424
|
+
const out = await runModel(input.client, cfg.generator, rcaPrompt, ctx.directory);
|
|
425
|
+
return out + "\n[usage-coach NEXT] call verify_diagnosis with this diagnosis.";
|
|
426
|
+
}
|
|
427
|
+
}),
|
|
428
|
+
verify_diagnosis: tool({
|
|
429
|
+
description: "Stage 3 (VERIFY) of the learning loop. Run the grader to check whether a diagnosis is CORRECT and ACTIONABLE (leads to a useful rule). Returns PASS/FAIL + a [usage-coach NEXT] directive.",
|
|
430
|
+
args: {
|
|
431
|
+
diagnosis: tool.schema.string(),
|
|
432
|
+
task: tool.schema.string(),
|
|
433
|
+
gradeResult: tool.schema.string()
|
|
434
|
+
},
|
|
435
|
+
async execute(args, ctx) {
|
|
436
|
+
const cfg = readHarnessCfg(ctx.directory);
|
|
437
|
+
const model = cfg.grader ?? cfg.generator;
|
|
438
|
+
if (!model) return "FAIL\n(ERROR: no grader/generator model configured.)\n[usage-coach NEXT] configure grader in harness.config.json, then retry verify_diagnosis.";
|
|
439
|
+
const verifyPrompt = `Verify this root-cause analysis for a failure.
|
|
440
|
+
Task: ${args.task}
|
|
441
|
+
Grade feedback: ${args.gradeResult}
|
|
442
|
+
Diagnosis: ${args.diagnosis}
|
|
443
|
+
Is the diagnosis CORRECT and ACTIONABLE (leads to a useful rule)?
|
|
444
|
+
Output PASS (the diagnosis is right) or FAIL (re-investigate needed), then reason.`;
|
|
445
|
+
const out = await runModel(input.client, model, verifyPrompt, ctx.directory);
|
|
446
|
+
let verdict = "FAIL";
|
|
447
|
+
if (!out.startsWith("ERROR:")) {
|
|
448
|
+
const f = (out.split("\n").find((l) => l.trim()) ?? "").trim();
|
|
449
|
+
if (/^pass\b/i.test(f)) verdict = "PASS";
|
|
450
|
+
else verdict = "FAIL";
|
|
451
|
+
}
|
|
452
|
+
const next = verdict === "PASS" ? `
|
|
453
|
+
[usage-coach NEXT] call generalize with this verified diagnosis.` : `
|
|
454
|
+
[usage-coach NEXT] FAIL \u2014 re-investigate the root cause.`;
|
|
455
|
+
return out + "\n" + next;
|
|
456
|
+
}
|
|
457
|
+
}),
|
|
458
|
+
generalize: tool({
|
|
459
|
+
description: "Stage 4 (GENERALIZE) of the learning loop. Run the generator to turn a verified root cause into a reusable rule and append it to rules.md, so the next generate call includes it. Returns the rule text and a [usage-coach NEXT] directive.",
|
|
460
|
+
args: {
|
|
461
|
+
diagnosis: tool.schema.string(),
|
|
462
|
+
task: tool.schema.string()
|
|
463
|
+
},
|
|
464
|
+
async execute(args, ctx) {
|
|
465
|
+
const cfg = readHarnessCfg(ctx.directory);
|
|
466
|
+
if (!cfg.generator) return 'ERROR: no generator model configured. Set "generator" in harness.config.json (see harness.config.example.json).';
|
|
467
|
+
const genPrompt = `Turn this verified root cause into a GENERAL, REUSABLE rule for future tasks of this kind.
|
|
468
|
+
Diagnosis: ${args.diagnosis}
|
|
469
|
+
Failed task: ${args.task}
|
|
470
|
+
Output a single rule in the form: 'For <task-type> tasks, always <check/do X> because <reason>.'
|
|
471
|
+
Keep it concrete and actionable.`;
|
|
472
|
+
const out = await runModel(input.client, cfg.generator, genPrompt, ctx.directory);
|
|
473
|
+
const rule = out;
|
|
474
|
+
try {
|
|
475
|
+
const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
476
|
+
mkdirSync(STATE_DIR, { recursive: true });
|
|
477
|
+
appendFileSync(rulesFile(), `## Rule (${date})
|
|
478
|
+
${rule}
|
|
479
|
+
Origin: ${args.task}
|
|
480
|
+
|
|
481
|
+
`);
|
|
482
|
+
} catch (e) {
|
|
483
|
+
log(`generalize err: ${String(e)}`);
|
|
484
|
+
}
|
|
485
|
+
return `${rule}
|
|
486
|
+
[usage-coach NEXT] rule saved to rules.md. The next generate call will include it. Call task_update for the original failed task -> failed, then proceed.`;
|
|
487
|
+
}
|
|
488
|
+
}),
|
|
382
489
|
// Per-role model execution (config-driven, quota-aware, same server, no deadlock).
|
|
383
490
|
// P1: quota decision drives model selection + concurrency.
|
|
384
491
|
generate: tool({
|
|
@@ -453,7 +560,12 @@ ${rules}
|
|
|
453
560
|
}
|
|
454
561
|
const next = verdict === "PASS" ? `
|
|
455
562
|
[usage-coach NEXT] PASS -> call task_update(i, title, "completed", "PASS"), then proceed to next task (or harness_done if last).` : `
|
|
456
|
-
[usage-coach NEXT] FAIL -> if revisions < 2: task_update(i, title, "revising", revisions+1) + generate({prompt: "Apply feedback:\\n{grade result}\\nTask: {title}"}); else:
|
|
563
|
+
[usage-coach NEXT] FAIL -> if revisions < 2: task_update(i, title, "revising", revisions+1) + generate({prompt: "Apply feedback:\\n{grade result}\\nTask: {title}"}); else: run the learning loop before failing \u2014
|
|
564
|
+
1. record_failure({task, prompt, gradeResult, model, revisions})
|
|
565
|
+
2. investigate({task, prompt, gradeResult}) -> diagnosis
|
|
566
|
+
3. verify_diagnosis({diagnosis, task, gradeResult}) -> if PASS: generalize({diagnosis, task}) (saves rule to rules.md)
|
|
567
|
+
4. task_update(i, title, "failed", "FAIL") -> next task.
|
|
568
|
+
The next generate call will automatically include the new rule.`;
|
|
457
569
|
return out + "\n" + next;
|
|
458
570
|
}
|
|
459
571
|
})
|
package/dist/tui.js
CHANGED
|
@@ -5,7 +5,7 @@ import { effect as _$effect } from "@opentui/solid";
|
|
|
5
5
|
import { createTextNode as _$createTextNode } from "@opentui/solid";
|
|
6
6
|
import { insertNode as _$insertNode } from "@opentui/solid";
|
|
7
7
|
import { createElement as _$createElement } from "@opentui/solid";
|
|
8
|
-
import { readFileSync, existsSync, writeFileSync, mkdirSync, readdirSync, statSync } from "fs";
|
|
8
|
+
import { readFileSync, existsSync, writeFileSync, mkdirSync, readdirSync, statSync, appendFileSync } from "fs";
|
|
9
9
|
import { createHash } from "crypto";
|
|
10
10
|
import { homedir } from "os";
|
|
11
11
|
import { join, resolve } from "path";
|
|
@@ -91,21 +91,43 @@ function barFill(p) {
|
|
|
91
91
|
function barEmpty(p) {
|
|
92
92
|
return "\u2591".repeat(10 - Math.max(0, Math.min(10, Math.round(p / 10))));
|
|
93
93
|
}
|
|
94
|
-
function short(s, n) {
|
|
95
|
-
return s.length <= n ? s : s.slice(0, n - 1) + "\u2026";
|
|
96
|
-
}
|
|
97
94
|
function initializeTui(api, disposeRoot) {
|
|
98
95
|
STATE_DIR = process.env.UC_STATE_DIR ?? projectStateDir(api.state.path.directory);
|
|
99
96
|
STATE_FILE = join(STATE_DIR, "state.json");
|
|
100
97
|
HARNESS_FILE = join(STATE_DIR, "harness.json");
|
|
101
98
|
MARKER = join(STATE_DIR, "tui-loaded.txt");
|
|
99
|
+
const TUI_LOG = join(STATE_DIR, "tui-debug.log");
|
|
100
|
+
const tlog = (msg) => {
|
|
101
|
+
try {
|
|
102
|
+
appendFileSync(MARKER, `${(/* @__PURE__ */ new Date()).toISOString()} ${msg}
|
|
103
|
+
`);
|
|
104
|
+
appendFileSync(TUI_LOG, `${(/* @__PURE__ */ new Date()).toISOString()} ${msg}
|
|
105
|
+
`);
|
|
106
|
+
} catch (e) {
|
|
107
|
+
try {
|
|
108
|
+
appendFileSync(MARKER, `TLOG ERR: ${String(e)}
|
|
109
|
+
`);
|
|
110
|
+
} catch {
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
};
|
|
102
114
|
try {
|
|
103
115
|
mkdirSync(STATE_DIR, {
|
|
104
116
|
recursive: true
|
|
105
117
|
});
|
|
106
|
-
writeFileSync(MARKER, `loaded ${(/* @__PURE__ */ new Date()).toISOString()} @ ${api.state.path.directory}`);
|
|
118
|
+
writeFileSync(MARKER, `loaded-v2 ${(/* @__PURE__ */ new Date()).toISOString()} @ ${api.state.path.directory}`);
|
|
107
119
|
} catch {
|
|
108
120
|
}
|
|
121
|
+
tlog(`init start | dir=${api.state.path.directory} | STATE_DIR=${STATE_DIR}`);
|
|
122
|
+
try {
|
|
123
|
+
tlog(`api keys=${Object.keys(api).join(",")}`);
|
|
124
|
+
tlog(`api.state=${JSON.stringify(api.state).slice(0, 400)}`);
|
|
125
|
+
tlog(`api.state.path=${JSON.stringify(api.state?.path).slice(0, 300)}`);
|
|
126
|
+
const r = api.route;
|
|
127
|
+
tlog(`api.route type=${typeof r} keys=${r && typeof r === "object" ? Object.keys(r).join(",") : "?"} val=${JSON.stringify(r).slice(0, 400)}`);
|
|
128
|
+
} catch (e) {
|
|
129
|
+
tlog(`api probe err: ${String(e)}`);
|
|
130
|
+
}
|
|
109
131
|
const [getState, setState] = createSignal(readState());
|
|
110
132
|
const [getHarness, setHarness] = createSignal(readHarness());
|
|
111
133
|
const timer = setInterval(() => {
|
|
@@ -168,7 +190,8 @@ function initializeTui(api, disposeRoot) {
|
|
|
168
190
|
}
|
|
169
191
|
let h = null;
|
|
170
192
|
try {
|
|
171
|
-
const
|
|
193
|
+
const routeSid = api.route?.current?.params?.sessionID ?? "";
|
|
194
|
+
const sid = routeSid || (ctx.session_id ?? "");
|
|
172
195
|
if (sid) {
|
|
173
196
|
const hf = join(STATE_DIR, sid, "harness.json");
|
|
174
197
|
if (existsSync(hf)) h = JSON.parse(readFileSync(hf, "utf8"));
|
|
@@ -371,15 +394,17 @@ function initializeTui(api, disposeRoot) {
|
|
|
371
394
|
_$insert(_el$59, lbl, _el$62);
|
|
372
395
|
_$insert(_el$59, rev, _el$62);
|
|
373
396
|
_$insert(_el$59, elapsedStr, _el$62);
|
|
374
|
-
_$insert(_el$59, () =>
|
|
397
|
+
_$insert(_el$59, () => t.title, null);
|
|
375
398
|
_$effect((_$p) => _$setProp(_el$59, "style", st(sKey), _$p));
|
|
376
399
|
return _el$59;
|
|
377
400
|
})());
|
|
378
401
|
const pv = t.model ? (t.model.split("/")[0] ?? "").split("-")[0] : "";
|
|
379
402
|
const provCoach = s?.providers?.find((p) => p.id === pv || pv && p.id.startsWith(pv) || pv && pv.startsWith(p.id));
|
|
380
|
-
const
|
|
403
|
+
const rawPct = provCoach ? provCoach.fiveHour : s?.fiveHour ?? 0;
|
|
404
|
+
const pct = rawPct < 0 ? 0 : rawPct;
|
|
405
|
+
const pctLabel = rawPct < 0 ? "n/a" : `${rawPct}%`;
|
|
381
406
|
nodes.push((() => {
|
|
382
|
-
var _el$63 = _$createElement("box"), _el$64 = _$createElement("text"), _el$66 = _$createElement("text"), _el$67 = _$createElement("text"), _el$68 = _$createElement("text"), _el$69 = _$createTextNode(` `)
|
|
407
|
+
var _el$63 = _$createElement("box"), _el$64 = _$createElement("text"), _el$66 = _$createElement("text"), _el$67 = _$createElement("text"), _el$68 = _$createElement("text"), _el$69 = _$createTextNode(` `);
|
|
383
408
|
_$insertNode(_el$63, _el$64);
|
|
384
409
|
_$insertNode(_el$63, _el$66);
|
|
385
410
|
_$insertNode(_el$63, _el$67);
|
|
@@ -389,8 +414,7 @@ function initializeTui(api, disposeRoot) {
|
|
|
389
414
|
_$insert(_el$66, () => barFill(pct));
|
|
390
415
|
_$insert(_el$67, () => barEmpty(pct));
|
|
391
416
|
_$insertNode(_el$68, _el$69);
|
|
392
|
-
_$
|
|
393
|
-
_$insert(_el$68, pct, _el$70);
|
|
417
|
+
_$insert(_el$68, pctLabel, null);
|
|
394
418
|
_$effect((_p$) => {
|
|
395
419
|
var _v$1 = st("text"), _v$10 = st("textMuted");
|
|
396
420
|
_v$1 !== _p$.e && (_p$.e = _$setProp(_el$66, "style", _v$1, _p$.e));
|
|
@@ -405,28 +429,32 @@ function initializeTui(api, disposeRoot) {
|
|
|
405
429
|
}
|
|
406
430
|
}
|
|
407
431
|
return (() => {
|
|
408
|
-
var _el$
|
|
409
|
-
_$setProp(_el$
|
|
410
|
-
_$insert(_el$
|
|
411
|
-
return _el$
|
|
432
|
+
var _el$70 = _$createElement("box");
|
|
433
|
+
_$setProp(_el$70, "flexDirection", "column");
|
|
434
|
+
_$insert(_el$70, nodes);
|
|
435
|
+
return _el$70;
|
|
412
436
|
})();
|
|
413
437
|
};
|
|
438
|
+
tlog("registering slots");
|
|
414
439
|
api.slots.register({
|
|
415
440
|
order: 80,
|
|
416
441
|
slots: {
|
|
417
442
|
sidebar_footer(ctx) {
|
|
443
|
+
tlog("sidebar_footer slot called");
|
|
418
444
|
try {
|
|
419
445
|
return panel(ctx);
|
|
420
|
-
} catch {
|
|
446
|
+
} catch (e) {
|
|
447
|
+
tlog(`sidebar_footer err: ${String(e)}`);
|
|
421
448
|
return (() => {
|
|
422
|
-
var _el$
|
|
423
|
-
_$insertNode(_el$
|
|
424
|
-
return _el$
|
|
449
|
+
var _el$71 = _$createElement("text");
|
|
450
|
+
_$insertNode(_el$71, _$createTextNode(`usage-coach`));
|
|
451
|
+
return _el$71;
|
|
425
452
|
})();
|
|
426
453
|
}
|
|
427
454
|
}
|
|
428
455
|
}
|
|
429
456
|
});
|
|
457
|
+
tlog("slots registered, init complete");
|
|
430
458
|
}
|
|
431
459
|
var tui = async (api) => {
|
|
432
460
|
createRoot((disposeRoot) => initializeTui(api, disposeRoot));
|
package/package.json
CHANGED