opencode-usage-coach 0.3.3 → 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.
Files changed (4) hide show
  1. package/README.md +28 -2
  2. package/dist/index.js +288 -28
  3. package/dist/tui.js +111 -109
  4. 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, colors, collapsible Alt+H)
208
- - ✅ Harness: agent mode (triage) with generate/grade model-specific tools (1 terminal, multi-model)
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,63 +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 = join(homedir(), ".cache", "opencode-usage-coach");
12
- var STATE_FILE = join(STATE_DIR, "state.json");
13
- var HARNESS_FILE = join(STATE_DIR, "harness.json");
14
- var LOG_FILE = join(STATE_DIR, "coach.log");
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 join(homedir(), ".cache", "opencode-usage-coach", "projects", h);
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 = join(STATE_DIR, "state.json");
23
- HARNESS_FILE = join(STATE_DIR, "harness.json");
24
- LOG_FILE = join(STATE_DIR, "coach.log");
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
- appendFileSync(LOG_FILE, `${(/* @__PURE__ */ new Date()).toISOString()} ${msg}
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
- mkdirSync(STATE_DIR, { recursive: true });
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 join(STATE_DIR, "rules.md");
104
+ return join2(STATE_DIR, "rules.md");
105
+ }
106
+ function failuresFile() {
107
+ return join2(STATE_DIR, "failures.ndjson");
43
108
  }
44
109
  function readRules() {
45
110
  try {
46
111
  const f = rulesFile();
47
- if (!existsSync(f)) return "";
48
- return readFileSync(f, "utf8").trim();
112
+ if (!existsSync2(f)) return "";
113
+ return readFileSync2(f, "utf8").trim();
49
114
  } catch {
50
115
  return "";
51
116
  }
52
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
+ }
53
135
  function harnessFile(sessionID) {
54
- return join(STATE_DIR, sessionID || "_default", "harness.json");
136
+ return join2(STATE_DIR, sessionID || "_default", "harness.json");
55
137
  }
56
138
  function readHarness(sessionID) {
57
139
  try {
58
140
  const f = harnessFile(sessionID);
59
- if (!existsSync(f)) return null;
60
- return JSON.parse(readFileSync(f, "utf8"));
141
+ if (!existsSync2(f)) return null;
142
+ return JSON.parse(readFileSync2(f, "utf8"));
61
143
  } catch {
62
144
  return null;
63
145
  }
@@ -65,7 +147,7 @@ function readHarness(sessionID) {
65
147
  function writeHarness(sessionID, h) {
66
148
  try {
67
149
  const f = harnessFile(sessionID);
68
- mkdirSync(dirname(f), { recursive: true });
150
+ mkdirSync2(dirname(f), { recursive: true });
69
151
  h.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
70
152
  writeFileSync(f, JSON.stringify(h, null, 2));
71
153
  } catch {
@@ -74,14 +156,14 @@ function writeHarness(sessionID, h) {
74
156
  function readHarnessCfg(dir) {
75
157
  const tryRead = (p) => {
76
158
  try {
77
- if (existsSync(p)) return JSON.parse(readFileSync(p, "utf8"));
159
+ if (existsSync2(p)) return JSON.parse(readFileSync2(p, "utf8"));
78
160
  } catch {
79
161
  }
80
162
  return {};
81
163
  };
82
164
  return {
83
- ...tryRead(join(homedir(), ".config", "opencode-usage-coach", "harness.config.json")),
84
- ...tryRead(join(dir, "harness.config.json"))
165
+ ...tryRead(join2(homedir(), ".config", "opencode-usage-coach", "harness.config.json")),
166
+ ...tryRead(join2(dir, "harness.config.json"))
85
167
  };
86
168
  }
87
169
  async function runModel(client, model, prompt, directory) {
@@ -102,7 +184,12 @@ async function runModel(client, model, prompt, directory) {
102
184
  const parts = resp?.data?.parts ?? resp?.parts ?? [];
103
185
  const text = parts.filter((p) => p?.type === "text").map((p) => p?.text ?? "").join("");
104
186
  try {
105
- await client.session.remove?.({ path: { id } });
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 } });
106
193
  } catch {
107
194
  }
108
195
  log(`runModel(${model}): done ${elapsed}s, ${text.length} chars`);
@@ -242,6 +329,7 @@ var LOADING = { decision: "GO", advice: "quota loading\u2026", weekly: -1, month
242
329
  async function UsageCoachPlugin(input) {
243
330
  try {
244
331
  setStateDir(input.directory);
332
+ initDomain(STATE_DIR);
245
333
  const cfg0 = readHarnessCfg(input.directory);
246
334
  const PROVIDER = process.env.UC_PROVIDER ?? cfg0.provider ?? "";
247
335
  const LIGHTER = process.env.UC_LIGHTER_MODEL ?? cfg0.lighterModel ?? "a lighter model";
@@ -262,6 +350,10 @@ async function UsageCoachPlugin(input) {
262
350
  providers = await fetchProvidersCoach();
263
351
  } catch {
264
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
+ }
265
357
  writeState({ ...last, providers, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
266
358
  log(`${last.decision} | weekly=${last.weekly}% 5h=${last.fiveHour}% | providers=${providers.length}`);
267
359
  } catch (e) {
@@ -292,7 +384,9 @@ async function UsageCoachPlugin(input) {
292
384
  log(`event err: ${String(e)}`);
293
385
  }
294
386
  },
295
- // ACT(1) hard gate: only intentional STOP throws. Our own bugs never block.
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.
296
390
  "tool.execute.before": async (_input) => {
297
391
  let decision = "GO";
298
392
  try {
@@ -301,7 +395,10 @@ async function UsageCoachPlugin(input) {
301
395
  decision = "GO";
302
396
  }
303
397
  if (decision === "STOP") {
304
- throw new Error(`[${PLUGIN_NAME}] blocked: quota limit exceeded. ${current().advice}`);
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
+ }
305
402
  }
306
403
  },
307
404
  // ACT(2) inject coaching into system prompt (double defense). Silent on error.
@@ -358,9 +455,12 @@ Then: harness_done(). Follow the [usage-coach NEXT] directive each tool returns.
358
455
  model: tool.schema.string().optional()
359
456
  },
360
457
  async execute(args, ctx) {
458
+ const cfg = readHarnessCfg(ctx.directory);
361
459
  const h = readHarness(ctx.sessionID) ?? { name: "batch", total: 0, current: 0, tasks: [], usage: {}, active: true };
362
460
  h.tasks = h.tasks.filter((x) => x.id !== args.id);
363
- h.tasks.push({ id: args.id, title: args.title, status: args.status, model: args.model ?? "", revisions: args.revisions ?? 0, score: args.score ?? null, startedAt: (/* @__PURE__ */ new Date()).toISOString() });
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() });
364
464
  if (args.id > h.current) h.current = args.id;
365
465
  writeHarness(ctx.sessionID, h);
366
466
  return `task ${args.id} -> ${args.status}${args.score ? ` (${args.score})` : ""}`;
@@ -379,6 +479,136 @@ Then: harness_done(). Follow the [usage-coach NEXT] directive each tool returns.
379
479
  return "Harness complete.";
380
480
  }
381
481
  }),
482
+ record_failure: tool({
483
+ description: "Stage 1 (RECORD) of the learning loop. Append a failure record to failures.ndjson for later root-cause analysis.",
484
+ args: {
485
+ task: tool.schema.string(),
486
+ prompt: tool.schema.string(),
487
+ gradeResult: tool.schema.string(),
488
+ model: tool.schema.string().optional(),
489
+ revisions: tool.schema.number().optional()
490
+ },
491
+ async execute(args, _ctx) {
492
+ const rec = { ts: (/* @__PURE__ */ new Date()).toISOString(), task: args.task, prompt: args.prompt, gradeResult: args.gradeResult, model: args.model, revisions: args.revisions };
493
+ try {
494
+ mkdirSync2(STATE_DIR, { recursive: true });
495
+ appendFileSync2(failuresFile(), JSON.stringify(rec) + "\n");
496
+ } catch (e) {
497
+ log(`record_failure err: ${String(e)}`);
498
+ }
499
+ return `Failure recorded. [usage-coach NEXT] call investigate({failure: ${JSON.stringify(rec)}}) to find the root cause.`;
500
+ }
501
+ }),
502
+ investigate: tool({
503
+ description: "Stage 2 (INVESTIGATE) of the learning loop. Run the generator to analyze the ROOT CAUSE of a failure (not just the symptom).",
504
+ args: {
505
+ task: tool.schema.string(),
506
+ prompt: tool.schema.string(),
507
+ gradeResult: tool.schema.string(),
508
+ model: tool.schema.string().optional()
509
+ },
510
+ async execute(args, ctx) {
511
+ const cfg = readHarnessCfg(ctx.directory);
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
+ }
532
+ const rcaPrompt = `A task failed. Analyze the ROOT CAUSE (not just the symptom).
533
+ Task: ${args.task}
534
+ What was expected (from grade): ${args.gradeResult}
535
+ Read relevant files in the directory if needed.
536
+ Output a structured root cause:
537
+ category: (one of: constraint-violation, missing-context, tool-misuse, model-limitation, other)
538
+ explanation: <why it failed>
539
+ evidence: <file/line or specific quote>`;
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
+ }
548
+ return out + "\n[usage-coach NEXT] call verify_diagnosis with this diagnosis.";
549
+ }
550
+ }),
551
+ verify_diagnosis: tool({
552
+ 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.",
553
+ args: {
554
+ diagnosis: tool.schema.string(),
555
+ task: tool.schema.string(),
556
+ gradeResult: tool.schema.string()
557
+ },
558
+ async execute(args, ctx) {
559
+ const cfg = readHarnessCfg(ctx.directory);
560
+ const model = cfg.grader ?? cfg.generator;
561
+ 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.";
562
+ const verifyPrompt = `Verify this root-cause analysis for a failure.
563
+ Task: ${args.task}
564
+ Grade feedback: ${args.gradeResult}
565
+ Diagnosis: ${args.diagnosis}
566
+ Is the diagnosis CORRECT and ACTIONABLE (leads to a useful rule)?
567
+ Output PASS (the diagnosis is right) or FAIL (re-investigate needed), then reason.`;
568
+ const out = await runModel(input.client, model, verifyPrompt, ctx.directory);
569
+ let verdict = "FAIL";
570
+ if (!out.startsWith("ERROR:")) {
571
+ const f = (out.split("\n").find((l) => l.trim()) ?? "").trim();
572
+ if (/^pass\b/i.test(f)) verdict = "PASS";
573
+ else verdict = "FAIL";
574
+ }
575
+ const next = verdict === "PASS" ? `
576
+ [usage-coach NEXT] call generalize with this verified diagnosis.` : `
577
+ [usage-coach NEXT] FAIL \u2014 re-investigate the root cause.`;
578
+ return out + "\n" + next;
579
+ }
580
+ }),
581
+ generalize: tool({
582
+ 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.",
583
+ args: {
584
+ diagnosis: tool.schema.string(),
585
+ task: tool.schema.string()
586
+ },
587
+ async execute(args, ctx) {
588
+ const cfg = readHarnessCfg(ctx.directory);
589
+ if (!cfg.generator) return 'ERROR: no generator model configured. Set "generator" in harness.config.json (see harness.config.example.json).';
590
+ const genPrompt = `Turn this verified root cause into a GENERAL, REUSABLE rule for future tasks of this kind.
591
+ Diagnosis: ${args.diagnosis}
592
+ Failed task: ${args.task}
593
+ Output a single rule in the form: 'For <task-type> tasks, always <check/do X> because <reason>.'
594
+ Keep it concrete and actionable.`;
595
+ const out = await runModel(input.client, cfg.generator, genPrompt, ctx.directory);
596
+ const rule = out;
597
+ try {
598
+ const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
599
+ mkdirSync2(STATE_DIR, { recursive: true });
600
+ appendFileSync2(rulesFile(), `## Rule (${date})
601
+ ${rule}
602
+ Origin: ${args.task}
603
+
604
+ `);
605
+ } catch (e) {
606
+ log(`generalize err: ${String(e)}`);
607
+ }
608
+ return `${rule}
609
+ [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.`;
610
+ }
611
+ }),
382
612
  // Per-role model execution (config-driven, quota-aware, same server, no deadlock).
383
613
  // P1: quota decision drives model selection + concurrency.
384
614
  generate: tool({
@@ -395,13 +625,38 @@ Then: harness_done(). Follow the [usage-coach NEXT] directive each tool returns.
395
625
  const throttle = decision === "THROTTLE" && cfg.lighterModel;
396
626
  const model = throttle ? cfg.lighterModel : cfg.generator;
397
627
  const rules = readRules();
398
- const prefix = rules ? `Lessons learned from previous failures (apply where relevant):
628
+ let prefix = rules ? `Lessons learned from previous failures (apply where relevant):
399
629
  ${rules}
400
630
 
401
631
  ---
402
632
 
403
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
+ }
404
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
+ }
405
660
  return out + (throttle ? `
406
661
  [usage-coach] quota THROTTLE \u2014 used lighter model ${cfg.lighterModel}` : "") + `
407
662
  [usage-coach NEXT] call task_update(i, title, "grading"), then grade to evaluate this work.`;
@@ -453,7 +708,12 @@ ${rules}
453
708
  }
454
709
  const next = verdict === "PASS" ? `
455
710
  [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: task_update(i, title, "failed", "FAIL") -> next task.`;
711
+ [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
712
+ 1. record_failure({task, prompt, gradeResult, model, revisions})
713
+ 2. investigate({task, prompt, gradeResult}) -> diagnosis
714
+ 3. verify_diagnosis({diagnosis, task, gradeResult}) -> if PASS: generalize({diagnosis, task}) (saves rule to rules.md)
715
+ 4. task_update(i, title, "failed", "FAIL") -> next task.
716
+ The next generate call will automatically include the new rule.`;
457
717
  return out + "\n" + next;
458
718
  }
459
719
  })
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";
@@ -86,26 +86,50 @@ var TLABEL = {
86
86
  halted_quota: "quota-halt"
87
87
  };
88
88
  function barFill(p) {
89
- return "\u2588".repeat(Math.max(0, Math.min(10, Math.round(p / 10))));
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
- return "\u2591".repeat(10 - Math.max(0, Math.min(10, Math.round(p / 10))));
93
- }
94
- function short(s, n) {
95
- return s.length <= n ? s : s.slice(0, n - 1) + "\u2026";
93
+ const n = p <= 0 ? 0 : Math.max(1, Math.min(10, Math.round(p / 10)));
94
+ return "\u2591".repeat(10 - n);
96
95
  }
97
96
  function initializeTui(api, disposeRoot) {
98
97
  STATE_DIR = process.env.UC_STATE_DIR ?? projectStateDir(api.state.path.directory);
99
98
  STATE_FILE = join(STATE_DIR, "state.json");
100
99
  HARNESS_FILE = join(STATE_DIR, "harness.json");
101
100
  MARKER = join(STATE_DIR, "tui-loaded.txt");
101
+ const TUI_LOG = join(STATE_DIR, "tui-debug.log");
102
+ const tlog = (msg) => {
103
+ try {
104
+ appendFileSync(MARKER, `${(/* @__PURE__ */ new Date()).toISOString()} ${msg}
105
+ `);
106
+ appendFileSync(TUI_LOG, `${(/* @__PURE__ */ new Date()).toISOString()} ${msg}
107
+ `);
108
+ } catch (e) {
109
+ try {
110
+ appendFileSync(MARKER, `TLOG ERR: ${String(e)}
111
+ `);
112
+ } catch {
113
+ }
114
+ }
115
+ };
102
116
  try {
103
117
  mkdirSync(STATE_DIR, {
104
118
  recursive: true
105
119
  });
106
- writeFileSync(MARKER, `loaded ${(/* @__PURE__ */ new Date()).toISOString()} @ ${api.state.path.directory}`);
120
+ writeFileSync(MARKER, `loaded-v2 ${(/* @__PURE__ */ new Date()).toISOString()} @ ${api.state.path.directory}`);
107
121
  } catch {
108
122
  }
123
+ tlog(`init start | dir=${api.state.path.directory} | STATE_DIR=${STATE_DIR}`);
124
+ try {
125
+ tlog(`api keys=${Object.keys(api).join(",")}`);
126
+ tlog(`api.state=${JSON.stringify(api.state).slice(0, 400)}`);
127
+ tlog(`api.state.path=${JSON.stringify(api.state?.path).slice(0, 300)}`);
128
+ const r = api.route;
129
+ tlog(`api.route type=${typeof r} keys=${r && typeof r === "object" ? Object.keys(r).join(",") : "?"} val=${JSON.stringify(r).slice(0, 400)}`);
130
+ } catch (e) {
131
+ tlog(`api probe err: ${String(e)}`);
132
+ }
109
133
  const [getState, setState] = createSignal(readState());
110
134
  const [getHarness, setHarness] = createSignal(readHarness());
111
135
  const timer = setInterval(() => {
@@ -168,7 +192,8 @@ function initializeTui(api, disposeRoot) {
168
192
  }
169
193
  let h = null;
170
194
  try {
171
- const sid = ctx.session_id ?? "";
195
+ const routeSid = api.route?.current?.params?.sessionID ?? "";
196
+ const sid = routeSid || (ctx.session_id ?? "");
172
197
  if (sid) {
173
198
  const hf = join(STATE_DIR, sid, "harness.json");
174
199
  if (existsSync(hf)) h = JSON.parse(readFileSync(hf, "utf8"));
@@ -213,7 +238,7 @@ function initializeTui(api, disposeRoot) {
213
238
  _$insert(_el$12, () => p.fiveHour, _el$14);
214
239
  _$insert(_el$12, () => p.fiveHourReset, null);
215
240
  _$effect((_p$) => {
216
- var _v$ = st("text"), _v$2 = st("textMuted");
241
+ var _v$ = st("text"), _v$2 = st("text");
217
242
  _v$ !== _p$.e && (_p$.e = _$setProp(_el$10, "style", _v$, _p$.e));
218
243
  _v$2 !== _p$.t && (_p$.t = _$setProp(_el$11, "style", _v$2, _p$.t));
219
244
  return _p$;
@@ -230,7 +255,7 @@ function initializeTui(api, disposeRoot) {
230
255
  _$insertNode(_el$15, _el$19);
231
256
  _$insertNode(_el$15, _el$20);
232
257
  _$setProp(_el$15, "flexDirection", "row");
233
- _$insertNode(_el$16, _$createTextNode(` wk `));
258
+ _$insertNode(_el$16, _$createTextNode(` 1w `));
234
259
  _$insert(_el$18, () => barFill(p.weekly));
235
260
  _$insert(_el$19, () => barEmpty(p.weekly));
236
261
  _$insertNode(_el$20, _el$21);
@@ -238,7 +263,7 @@ function initializeTui(api, disposeRoot) {
238
263
  _$insert(_el$20, () => p.weekly, _el$22);
239
264
  _$insert(_el$20, () => p.weeklyReset, null);
240
265
  _$effect((_p$) => {
241
- var _v$3 = st("text"), _v$4 = st("textMuted");
266
+ var _v$3 = st("text"), _v$4 = st("text");
242
267
  _v$3 !== _p$.e && (_p$.e = _$setProp(_el$18, "style", _v$3, _p$.e));
243
268
  _v$4 !== _p$.t && (_p$.t = _$setProp(_el$19, "style", _v$4, _p$.t));
244
269
  return _p$;
@@ -258,7 +283,7 @@ function initializeTui(api, disposeRoot) {
258
283
  }
259
284
  } else {
260
285
  nodes.push((() => {
261
- var _el$27 = _$createElement("box"), _el$28 = _$createElement("text"), _el$30 = _$createElement("text"), _el$31 = _$createElement("text"), _el$32 = _$createElement("text"), _el$33 = _$createTextNode(` `), _el$34 = _$createTextNode(`%`);
286
+ var _el$27 = _$createElement("box"), _el$28 = _$createElement("text"), _el$30 = _$createElement("text"), _el$31 = _$createElement("text"), _el$32 = _$createElement("text");
262
287
  _$insertNode(_el$27, _el$28);
263
288
  _$insertNode(_el$27, _el$30);
264
289
  _$insertNode(_el$27, _el$31);
@@ -267,11 +292,9 @@ function initializeTui(api, disposeRoot) {
267
292
  _$insertNode(_el$28, _$createTextNode(` 5h `));
268
293
  _$insert(_el$30, () => barFill(s.fiveHour));
269
294
  _$insert(_el$31, () => barEmpty(s.fiveHour));
270
- _$insertNode(_el$32, _el$33);
271
- _$insertNode(_el$32, _el$34);
272
- _$insert(_el$32, () => s.fiveHour, _el$34);
295
+ _$insertNode(_el$32, _$createTextNode(` 0%`));
273
296
  _$effect((_p$) => {
274
- var _v$5 = st("text"), _v$6 = st("textMuted");
297
+ var _v$5 = st("text"), _v$6 = st("text");
275
298
  _v$5 !== _p$.e && (_p$.e = _$setProp(_el$30, "style", _v$5, _p$.e));
276
299
  _v$6 !== _p$.t && (_p$.t = _$setProp(_el$31, "style", _v$6, _p$.t));
277
300
  return _p$;
@@ -282,77 +305,51 @@ function initializeTui(api, disposeRoot) {
282
305
  return _el$27;
283
306
  })());
284
307
  nodes.push((() => {
285
- var _el$35 = _$createElement("box"), _el$36 = _$createElement("text"), _el$38 = _$createElement("text"), _el$39 = _$createElement("text"), _el$40 = _$createElement("text"), _el$41 = _$createTextNode(` `), _el$42 = _$createTextNode(`%`);
286
- _$insertNode(_el$35, _el$36);
287
- _$insertNode(_el$35, _el$38);
288
- _$insertNode(_el$35, _el$39);
289
- _$insertNode(_el$35, _el$40);
290
- _$setProp(_el$35, "flexDirection", "row");
291
- _$insertNode(_el$36, _$createTextNode(` wk `));
292
- _$insert(_el$38, () => barFill(s.weekly));
293
- _$insert(_el$39, () => barEmpty(s.weekly));
294
- _$insertNode(_el$40, _el$41);
295
- _$insertNode(_el$40, _el$42);
296
- _$insert(_el$40, () => s.weekly, _el$42);
297
- _$effect((_p$) => {
298
- var _v$7 = st("text"), _v$8 = st("textMuted");
299
- _v$7 !== _p$.e && (_p$.e = _$setProp(_el$38, "style", _v$7, _p$.e));
300
- _v$8 !== _p$.t && (_p$.t = _$setProp(_el$39, "style", _v$8, _p$.t));
301
- return _p$;
302
- }, {
303
- e: void 0,
304
- t: void 0
305
- });
306
- return _el$35;
307
- })());
308
- nodes.push((() => {
309
- 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(`%`);
310
- _$insertNode(_el$43, _el$44);
311
- _$insertNode(_el$43, _el$46);
312
- _$insertNode(_el$43, _el$47);
313
- _$insertNode(_el$43, _el$48);
314
- _$setProp(_el$43, "flexDirection", "row");
315
- _$insertNode(_el$44, _$createTextNode(` mo `));
316
- _$insert(_el$46, () => barFill(s.monthly));
317
- _$insert(_el$47, () => barEmpty(s.monthly));
318
- _$insertNode(_el$48, _el$49);
319
- _$insertNode(_el$48, _el$50);
320
- _$insert(_el$48, () => s.monthly, _el$50);
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%`));
321
318
  _$effect((_p$) => {
322
- var _v$9 = st("text"), _v$0 = st("textMuted");
323
- _v$9 !== _p$.e && (_p$.e = _$setProp(_el$46, "style", _v$9, _p$.e));
324
- _v$0 !== _p$.t && (_p$.t = _$setProp(_el$47, "style", _v$0, _p$.t));
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));
325
322
  return _p$;
326
323
  }, {
327
324
  e: void 0,
328
325
  t: void 0
329
326
  });
330
- return _el$43;
327
+ return _el$34;
331
328
  })());
332
329
  }
333
330
  } else {
334
331
  nodes.push((() => {
335
- var _el$51 = _$createElement("text");
336
- _$insertNode(_el$51, _$createTextNode(`usage-coach: ...`));
337
- return _el$51;
332
+ var _el$41 = _$createElement("text");
333
+ _$insertNode(_el$41, _$createTextNode(`usage-coach: ...`));
334
+ return _el$41;
338
335
  })());
339
336
  }
340
337
  if (h && h.active !== false && h.tasks.length > 0) {
341
338
  nodes.push((() => {
342
- var _el$53 = _$createElement("text");
343
- _$insertNode(_el$53, _$createTextNode(` `));
344
- return _el$53;
339
+ var _el$43 = _$createElement("text");
340
+ _$insertNode(_el$43, _$createTextNode(` `));
341
+ return _el$43;
345
342
  })());
346
343
  nodes.push((() => {
347
- var _el$55 = _$createElement("text"), _el$56 = _$createTextNode(`harness: `), _el$57 = _$createTextNode(` `), _el$58 = _$createTextNode(`/`);
348
- _$insertNode(_el$55, _el$56);
349
- _$insertNode(_el$55, _el$57);
350
- _$insertNode(_el$55, _el$58);
351
- _$insert(_el$55, () => h.name, _el$57);
352
- _$insert(_el$55, () => h.current, _el$58);
353
- _$insert(_el$55, () => h.total, null);
354
- _$effect((_$p) => _$setProp(_el$55, "style", st("textMuted"), _$p));
355
- return _el$55;
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;
356
353
  })());
357
354
  for (const t of h.tasks) {
358
355
  const sKey = statusKey[t.status] ?? "text";
@@ -362,71 +359,76 @@ function initializeTui(api, disposeRoot) {
362
359
  const elapsed = t.startedAt ? Math.max(0, Math.round((Date.now() - new Date(t.startedAt).getTime()) / 1e3)) : 0;
363
360
  const elapsedStr = t.status === "completed" || t.status === "failed" ? "" : elapsed > 0 ? ` ${elapsed}s` : "";
364
361
  nodes.push((() => {
365
- var _el$59 = _$createElement("text"), _el$60 = _$createTextNode(` \u25CF `), _el$61 = _$createTextNode(` `), _el$62 = _$createTextNode(` `);
366
- _$insertNode(_el$59, _el$60);
367
- _$insertNode(_el$59, _el$61);
368
- _$insertNode(_el$59, _el$62);
369
- _$insert(_el$59, () => t.id, _el$61);
370
- _$insert(_el$59, mdl, _el$61);
371
- _$insert(_el$59, lbl, _el$62);
372
- _$insert(_el$59, rev, _el$62);
373
- _$insert(_el$59, elapsedStr, _el$62);
374
- _$insert(_el$59, () => short(t.title, 12), null);
375
- _$effect((_$p) => _$setProp(_el$59, "style", st(sKey), _$p));
376
- return _el$59;
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;
377
374
  })());
378
375
  const pv = t.model ? (t.model.split("/")[0] ?? "").split("-")[0] : "";
379
- const provCoach = s?.providers?.find((p) => p.id === pv || pv && p.id.startsWith(pv) || pv && pv.startsWith(p.id));
380
- const pct = provCoach ? provCoach.fiveHour : s?.fiveHour ?? 0;
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;
378
+ const pct = rawPct < 0 ? 0 : rawPct;
379
+ const pctLabel = rawPct < 0 ? "n/a" : `${rawPct}%`;
381
380
  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(` `), _el$70 = _$createTextNode(`%`);
383
- _$insertNode(_el$63, _el$64);
384
- _$insertNode(_el$63, _el$66);
385
- _$insertNode(_el$63, _el$67);
386
- _$insertNode(_el$63, _el$68);
387
- _$setProp(_el$63, "flexDirection", "row");
388
- _$insertNode(_el$64, _$createTextNode(` 5h `));
389
- _$insert(_el$66, () => barFill(pct));
390
- _$insert(_el$67, () => barEmpty(pct));
391
- _$insertNode(_el$68, _el$69);
392
- _$insertNode(_el$68, _el$70);
393
- _$insert(_el$68, pct, _el$70);
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);
394
392
  _$effect((_p$) => {
395
- var _v$1 = st("text"), _v$10 = st("textMuted");
396
- _v$1 !== _p$.e && (_p$.e = _$setProp(_el$66, "style", _v$1, _p$.e));
397
- _v$10 !== _p$.t && (_p$.t = _$setProp(_el$67, "style", _v$10, _p$.t));
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));
398
396
  return _p$;
399
397
  }, {
400
398
  e: void 0,
401
399
  t: void 0
402
400
  });
403
- return _el$63;
401
+ return _el$53;
404
402
  })());
405
403
  }
406
404
  }
407
405
  return (() => {
408
- var _el$71 = _$createElement("box");
409
- _$setProp(_el$71, "flexDirection", "column");
410
- _$insert(_el$71, nodes);
411
- return _el$71;
406
+ var _el$60 = _$createElement("box");
407
+ _$setProp(_el$60, "flexDirection", "column");
408
+ _$insert(_el$60, nodes);
409
+ return _el$60;
412
410
  })();
413
411
  };
412
+ tlog("registering slots");
414
413
  api.slots.register({
415
414
  order: 80,
416
415
  slots: {
417
416
  sidebar_footer(ctx) {
417
+ tlog("sidebar_footer slot called");
418
418
  try {
419
419
  return panel(ctx);
420
- } catch {
420
+ } catch (e) {
421
+ tlog(`sidebar_footer err: ${String(e)}`);
421
422
  return (() => {
422
- var _el$72 = _$createElement("text");
423
- _$insertNode(_el$72, _$createTextNode(`usage-coach`));
424
- return _el$72;
423
+ var _el$61 = _$createElement("text");
424
+ _$insertNode(_el$61, _$createTextNode(`usage-coach`));
425
+ return _el$61;
425
426
  })();
426
427
  }
427
428
  }
428
429
  }
429
430
  });
431
+ tlog("slots registered, init complete");
430
432
  }
431
433
  var tui = async (api) => {
432
434
  createRoot((disposeRoot) => initializeTui(api, disposeRoot));
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-usage-coach",
3
- "version": "0.3.3",
3
+ "version": "0.3.5",
4
4
  "description": "opencode closed-loop usage coach \u2014 quota SENSE -> coaching DECIDE -> loop ACT + TUI integration",
5
5
  "type": "module",
6
6
  "main": "./dist/index.js",