opencode-usage-coach 0.3.4 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +58 -2
- package/agents/usage-coach-harness.md +5 -0
- package/dist/index.js +208 -35
- package/dist/tui.js +80 -106
- package/package.json +10 -3
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`).
|
|
@@ -142,6 +152,36 @@ Place in the **work directory**. Each role runs on its model, so per-model quota
|
|
|
142
152
|
| `UC_PROVIDER` | (config `provider`) | codexbar provider for the guardian |
|
|
143
153
|
| `UC_TTL_MS` | 60000 | quota cache TTL (ms) |
|
|
144
154
|
| `UC_DEBUG` | 0 | set to `1` for a diagnostic log at `~/.cache/opencode-usage-coach/coach.log` |
|
|
155
|
+
| `UC_HARNESS_AGENT` | `Usage-Coach-Harness` | comma-separated agent modes allowed to use harness tools + receive quota coaching (case-insensitive; must match the agent id, e.g. `usage-coach-harness` from `agents/usage-coach-harness.md`) |
|
|
156
|
+
|
|
157
|
+
## Agent-mode scoping
|
|
158
|
+
|
|
159
|
+
Harness tools (`generate`, `grade`, `harness_start`, …) and quota coaching are **scoped to
|
|
160
|
+
the `usage-coach-harness` agent mode**. Other modes (build, general, your custom agents) stay
|
|
161
|
+
completely clean — no harness tools in their tool list, no quota coaching injected into their
|
|
162
|
+
system prompt.
|
|
163
|
+
|
|
164
|
+
This is enforced on two independent layers (defense in depth):
|
|
165
|
+
|
|
166
|
+
1. **Agent definition** (`agents/usage-coach-harness.md`) — its `permission` allowlist names the
|
|
167
|
+
harness tools, so they only appear in this mode. Other agents' permission lists don't name
|
|
168
|
+
them, so they're hidden from those modes automatically (this is the standard opencode
|
|
169
|
+
mechanism — tool visibility is the agent definition's responsibility).
|
|
170
|
+
2. **Plugin runtime gate** (`tool.execute.before`) — even if a harness tool were somehow
|
|
171
|
+
invoked, the plugin resolves the current session's agent (`client.session.get` → `info.agent`,
|
|
172
|
+
60s-cached) and throws unless it matches `UC_HARNESS_AGENT` (default `Usage-Coach-Harness`,
|
|
173
|
+
case-insensitive). The quota system-prompt injection is gated the same way.
|
|
174
|
+
|
|
175
|
+
**To use the harness tools**, switch to the `usage-coach-harness` agent mode.
|
|
176
|
+
|
|
177
|
+
**To allow additional modes**, set `UC_HARNESS_AGENT` to a comma-separated list:
|
|
178
|
+
```bash
|
|
179
|
+
export UC_HARNESS_AGENT="usage-coach-harness,my-other-harness"
|
|
180
|
+
```
|
|
181
|
+
|
|
182
|
+
> Why not the v2 plugin API? v2 has no `tool` registration domain, so a plugin that provides
|
|
183
|
+
> custom tools (like this one) cannot be fully rewritten in v2. Agent `permission` allowlists +
|
|
184
|
+
> the v1 runtime gate is the structurally correct way to scope tool visibility.
|
|
145
185
|
|
|
146
186
|
## Architecture
|
|
147
187
|
- **Server module** (`src/index.ts`) — SENSE/DECIDE/ACT + custom harness tools. Loaded via `opencode.json`.
|
|
@@ -203,9 +243,25 @@ Recurring issues and fixes — mostly learned the hard way during development.
|
|
|
203
243
|
- Harness completion sets `active:false` → hidden from the TUI.
|
|
204
244
|
- Override the state path with `UC_STATE_DIR` (forces global state).
|
|
205
245
|
|
|
246
|
+
**Key gotcha — opencode TUI `ctx` does NOT carry `session_id`.**
|
|
247
|
+
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).
|
|
248
|
+
|
|
249
|
+
**Debugging session isolation** (if it breaks again):
|
|
250
|
+
1. Check `~/.cache/opencode-usage-coach/projects/<hash>/tui-debug.log` — is `panel` being called? What `routeSid` value?
|
|
251
|
+
2. `api.route.current.params.sessionID` — populated? (Empty → panel falls back to scanning all sessions.)
|
|
252
|
+
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.
|
|
253
|
+
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.
|
|
254
|
+
|
|
255
|
+
**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`.
|
|
256
|
+
|
|
206
257
|
## Status
|
|
207
|
-
- ✅ Quota guardian + TUI panel (per-provider coach view,
|
|
208
|
-
- ✅ Harness: agent mode
|
|
258
|
+
- ✅ Quota guardian + TUI panel (per-provider coach view, 5h/1w gauges, collapsible Alt+H)
|
|
259
|
+
- ✅ Harness: agent mode with generate/grade tools (multi-model, 1 terminal)
|
|
260
|
+
- ✅ Deterministic loop via NEXT directives (parallel PATH A / sequential PATH B)
|
|
261
|
+
- ✅ Quota-aware tools (GO/THROTTLE/STOP drive model selection + concurrency)
|
|
262
|
+
- ✅ Learning loop (record_failure → investigate → verify_diagnosis → generalize → rules.md)
|
|
263
|
+
- ✅ Domain knowledge base (graph store, investigate/generate injection)
|
|
264
|
+
- ✅ Session isolation (api.route, per-session harness state)
|
|
209
265
|
- ✅ npm packaging (`opencode plugin install opencode-usage-coach`)
|
|
210
266
|
|
|
211
267
|
License: MIT.
|
|
@@ -12,7 +12,12 @@ permission:
|
|
|
12
12
|
grep: allow
|
|
13
13
|
task: allow
|
|
14
14
|
generate: allow
|
|
15
|
+
generate_batch: allow
|
|
15
16
|
grade: allow
|
|
17
|
+
investigate: allow
|
|
18
|
+
verify_diagnosis: allow
|
|
19
|
+
generalize: allow
|
|
20
|
+
record_failure: allow
|
|
16
21
|
harness_start: allow
|
|
17
22
|
task_update: allow
|
|
18
23
|
harness_done: allow
|
package/dist/index.js
CHANGED
|
@@ -1,66 +1,142 @@
|
|
|
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
|
-
var DEBUG = process.env.UC_DEBUG === "1";
|
|
10
71
|
var TTL_MS = Number(process.env.UC_TTL_MS ?? 6e4);
|
|
11
|
-
var STATE_DIR =
|
|
12
|
-
var STATE_FILE =
|
|
13
|
-
var
|
|
14
|
-
var LOG_FILE = join(STATE_DIR, "coach.log");
|
|
72
|
+
var STATE_DIR = join2(homedir(), ".cache", "opencode-usage-coach");
|
|
73
|
+
var STATE_FILE = join2(STATE_DIR, "state.json");
|
|
74
|
+
var LOG_FILE = join2(STATE_DIR, "coach.log");
|
|
15
75
|
function projectStateDir(dir) {
|
|
16
76
|
const abs = resolve(dir || ".");
|
|
17
77
|
const h = createHash("sha1").update(abs).digest("hex").slice(0, 12);
|
|
18
|
-
return
|
|
78
|
+
return join2(homedir(), ".cache", "opencode-usage-coach", "projects", h);
|
|
19
79
|
}
|
|
20
80
|
function setStateDir(dir) {
|
|
21
81
|
STATE_DIR = process.env.UC_STATE_DIR ?? projectStateDir(dir);
|
|
22
|
-
STATE_FILE =
|
|
23
|
-
|
|
24
|
-
LOG_FILE = join(STATE_DIR, "coach.log");
|
|
82
|
+
STATE_FILE = join2(STATE_DIR, "state.json");
|
|
83
|
+
LOG_FILE = join2(STATE_DIR, "coach.log");
|
|
25
84
|
}
|
|
26
85
|
var NOOP_HOOKS = {};
|
|
27
86
|
function log(msg) {
|
|
28
87
|
try {
|
|
29
|
-
|
|
88
|
+
appendFileSync2(LOG_FILE, `${(/* @__PURE__ */ new Date()).toISOString()} ${msg}
|
|
30
89
|
`);
|
|
31
90
|
} catch {
|
|
32
91
|
}
|
|
33
92
|
}
|
|
34
93
|
function writeState(c) {
|
|
35
94
|
try {
|
|
36
|
-
|
|
95
|
+
mkdirSync2(STATE_DIR, { recursive: true });
|
|
37
96
|
writeFileSync(STATE_FILE, JSON.stringify({ ...c, updatedAt: (/* @__PURE__ */ new Date()).toISOString() }));
|
|
38
97
|
} catch {
|
|
39
98
|
}
|
|
40
99
|
}
|
|
41
100
|
function rulesFile() {
|
|
42
|
-
return
|
|
101
|
+
return join2(STATE_DIR, "rules.md");
|
|
43
102
|
}
|
|
44
103
|
function failuresFile() {
|
|
45
|
-
return
|
|
104
|
+
return join2(STATE_DIR, "failures.ndjson");
|
|
46
105
|
}
|
|
47
106
|
function readRules() {
|
|
48
107
|
try {
|
|
49
108
|
const f = rulesFile();
|
|
50
|
-
if (!
|
|
51
|
-
return
|
|
109
|
+
if (!existsSync2(f)) return "";
|
|
110
|
+
return readFileSync2(f, "utf8").trim();
|
|
52
111
|
} catch {
|
|
53
112
|
return "";
|
|
54
113
|
}
|
|
55
114
|
}
|
|
115
|
+
function extractKeywords(text) {
|
|
116
|
+
try {
|
|
117
|
+
const STOP = /* @__PURE__ */ new Set(["the", "and", "for", "with", "that", "this", "from", "into", "your", "you", "are", "was", "but", "not", "all", "any", "use", "task", "prompt"]);
|
|
118
|
+
const seen = /* @__PURE__ */ new Set();
|
|
119
|
+
const out = [];
|
|
120
|
+
for (const raw of (text ?? "").toLowerCase().split(/[^a-z0-9_]+/)) {
|
|
121
|
+
const t = raw.trim();
|
|
122
|
+
if (t.length < 3 || STOP.has(t) || seen.has(t)) continue;
|
|
123
|
+
seen.add(t);
|
|
124
|
+
out.push(t);
|
|
125
|
+
if (out.length >= 16) break;
|
|
126
|
+
}
|
|
127
|
+
return out;
|
|
128
|
+
} catch {
|
|
129
|
+
return [];
|
|
130
|
+
}
|
|
131
|
+
}
|
|
56
132
|
function harnessFile(sessionID) {
|
|
57
|
-
return
|
|
133
|
+
return join2(STATE_DIR, sessionID || "_default", "harness.json");
|
|
58
134
|
}
|
|
59
135
|
function readHarness(sessionID) {
|
|
60
136
|
try {
|
|
61
137
|
const f = harnessFile(sessionID);
|
|
62
|
-
if (!
|
|
63
|
-
return JSON.parse(
|
|
138
|
+
if (!existsSync2(f)) return null;
|
|
139
|
+
return JSON.parse(readFileSync2(f, "utf8"));
|
|
64
140
|
} catch {
|
|
65
141
|
return null;
|
|
66
142
|
}
|
|
@@ -68,7 +144,7 @@ function readHarness(sessionID) {
|
|
|
68
144
|
function writeHarness(sessionID, h) {
|
|
69
145
|
try {
|
|
70
146
|
const f = harnessFile(sessionID);
|
|
71
|
-
|
|
147
|
+
mkdirSync2(dirname(f), { recursive: true });
|
|
72
148
|
h.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
73
149
|
writeFileSync(f, JSON.stringify(h, null, 2));
|
|
74
150
|
} catch {
|
|
@@ -77,14 +153,14 @@ function writeHarness(sessionID, h) {
|
|
|
77
153
|
function readHarnessCfg(dir) {
|
|
78
154
|
const tryRead = (p) => {
|
|
79
155
|
try {
|
|
80
|
-
if (
|
|
156
|
+
if (existsSync2(p)) return JSON.parse(readFileSync2(p, "utf8"));
|
|
81
157
|
} catch {
|
|
82
158
|
}
|
|
83
159
|
return {};
|
|
84
160
|
};
|
|
85
161
|
return {
|
|
86
|
-
...tryRead(
|
|
87
|
-
...tryRead(
|
|
162
|
+
...tryRead(join2(homedir(), ".config", "opencode-usage-coach", "harness.config.json")),
|
|
163
|
+
...tryRead(join2(dir, "harness.config.json"))
|
|
88
164
|
};
|
|
89
165
|
}
|
|
90
166
|
async function runModel(client, model, prompt, directory) {
|
|
@@ -105,7 +181,12 @@ async function runModel(client, model, prompt, directory) {
|
|
|
105
181
|
const parts = resp?.data?.parts ?? resp?.parts ?? [];
|
|
106
182
|
const text = parts.filter((p) => p?.type === "text").map((p) => p?.text ?? "").join("");
|
|
107
183
|
try {
|
|
108
|
-
await client.session.
|
|
184
|
+
const summary = await client.session.summarize?.({ path: { id } });
|
|
185
|
+
log(`runModel(${model}): sub-session summary: ${JSON.stringify(summary?.data ?? summary).slice(0, 300)}`);
|
|
186
|
+
} catch {
|
|
187
|
+
}
|
|
188
|
+
try {
|
|
189
|
+
await client.session.delete?.({ path: { id } });
|
|
109
190
|
} catch {
|
|
110
191
|
}
|
|
111
192
|
log(`runModel(${model}): done ${elapsed}s, ${text.length} chars`);
|
|
@@ -116,6 +197,7 @@ async function runModel(client, model, prompt, directory) {
|
|
|
116
197
|
return `ERROR: runModel exception after ${elapsed}s: ${String(e)}`;
|
|
117
198
|
}
|
|
118
199
|
}
|
|
200
|
+
var HARNESS_AGENTS = (process.env.UC_HARNESS_AGENT ?? "Usage-Coach-Harness").split(",").map((s) => s.trim().toLowerCase()).filter(Boolean);
|
|
119
201
|
var num = (e, d) => {
|
|
120
202
|
try {
|
|
121
203
|
const v = Number(process.env[e]);
|
|
@@ -241,10 +323,30 @@ function coach(q, lighter) {
|
|
|
241
323
|
if (wk >= THR_WK) return thr(`weekly ${wk}% (${wkR})`);
|
|
242
324
|
return { decision: "GO", advice: `Comfortable \u2014 weekly ${wk}% \xB7 5h ${h5}% \xB7 monthly ${mo}%. proceed. 5h window ${h5R}.`, weekly: wk, monthly: mo, fiveHour: h5 };
|
|
243
325
|
}
|
|
326
|
+
var agentCache = /* @__PURE__ */ new Map();
|
|
327
|
+
async function resolveAgent(client, sessionID) {
|
|
328
|
+
if (!sessionID) return "";
|
|
329
|
+
const hit = agentCache.get(sessionID);
|
|
330
|
+
if (hit && Date.now() - hit.ts < 6e4) return hit.agent;
|
|
331
|
+
try {
|
|
332
|
+
const s = await client.session.get({ path: { id: sessionID } });
|
|
333
|
+
const agent = String(s?.data?.info?.agent ?? s?.data?.agent ?? s?.info?.agent ?? "");
|
|
334
|
+
agentCache.set(sessionID, { agent, ts: Date.now() });
|
|
335
|
+
return agent;
|
|
336
|
+
} catch (e) {
|
|
337
|
+
log(`resolveAgent err: ${String(e)}`);
|
|
338
|
+
return "";
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
function isHarnessAgent(agent) {
|
|
342
|
+
if (!agent) return false;
|
|
343
|
+
return HARNESS_AGENTS.includes(agent.toLowerCase());
|
|
344
|
+
}
|
|
244
345
|
var LOADING = { decision: "GO", advice: "quota loading\u2026", weekly: -1, monthly: -1, fiveHour: -1 };
|
|
245
346
|
async function UsageCoachPlugin(input) {
|
|
246
347
|
try {
|
|
247
348
|
setStateDir(input.directory);
|
|
349
|
+
initDomain(STATE_DIR);
|
|
248
350
|
const cfg0 = readHarnessCfg(input.directory);
|
|
249
351
|
const PROVIDER = process.env.UC_PROVIDER ?? cfg0.provider ?? "";
|
|
250
352
|
const LIGHTER = process.env.UC_LIGHTER_MODEL ?? cfg0.lighterModel ?? "a lighter model";
|
|
@@ -265,6 +367,10 @@ async function UsageCoachPlugin(input) {
|
|
|
265
367
|
providers = await fetchProvidersCoach();
|
|
266
368
|
} catch {
|
|
267
369
|
}
|
|
370
|
+
if (providers.length > 0 && last.weekly < 0) {
|
|
371
|
+
const p0 = providers[0];
|
|
372
|
+
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" };
|
|
373
|
+
}
|
|
268
374
|
writeState({ ...last, providers, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
269
375
|
log(`${last.decision} | weekly=${last.weekly}% 5h=${last.fiveHour}% | providers=${providers.length}`);
|
|
270
376
|
} catch (e) {
|
|
@@ -295,9 +401,17 @@ async function UsageCoachPlugin(input) {
|
|
|
295
401
|
log(`event err: ${String(e)}`);
|
|
296
402
|
}
|
|
297
403
|
},
|
|
298
|
-
// ACT(1) hard gate
|
|
404
|
+
// ACT(1) hard gate — harness tools are restricted to the configured harness
|
|
405
|
+
// agent mode AND gated by quota STOP. General tools (read/edit/bash/grep/task)
|
|
406
|
+
// are NEVER gated, in ANY mode — they don't consume model quota.
|
|
299
407
|
"tool.execute.before": async (_input) => {
|
|
300
|
-
|
|
408
|
+
const harnessTools = ["generate", "generate_batch", "grade", "investigate", "verify_diagnosis", "generalize", "harness_start", "task_update", "harness_done", "record_failure"];
|
|
409
|
+
if (!harnessTools.includes(_input.tool)) return;
|
|
410
|
+
const agent = await resolveAgent(input.client, _input.sessionID);
|
|
411
|
+
if (!isHarnessAgent(agent)) {
|
|
412
|
+
throw new Error(`[${PLUGIN_NAME}] '${_input.tool}' is restricted to agent mode ${JSON.stringify(HARNESS_AGENTS)} (current: ${JSON.stringify(agent || "unknown")}). Switch to that agent mode to use it.`);
|
|
413
|
+
}
|
|
414
|
+
let decision;
|
|
301
415
|
try {
|
|
302
416
|
decision = current().decision;
|
|
303
417
|
} catch {
|
|
@@ -307,9 +421,14 @@ async function UsageCoachPlugin(input) {
|
|
|
307
421
|
throw new Error(`[${PLUGIN_NAME}] blocked: quota limit exceeded. ${current().advice}`);
|
|
308
422
|
}
|
|
309
423
|
},
|
|
310
|
-
// ACT(2) inject coaching into system prompt
|
|
424
|
+
// ACT(2) inject coaching into system prompt — ONLY in the harness agent mode,
|
|
425
|
+
// so other modes' system prompts stay completely clean. Silent on error.
|
|
311
426
|
"experimental.chat.system.transform": async (_input, output) => {
|
|
312
427
|
try {
|
|
428
|
+
if (_input.sessionID) {
|
|
429
|
+
const agent = await resolveAgent(input.client, _input.sessionID);
|
|
430
|
+
if (!isHarnessAgent(agent)) return;
|
|
431
|
+
}
|
|
313
432
|
const c = current();
|
|
314
433
|
let instruction = "";
|
|
315
434
|
if (c.decision === "STOP") instruction = `[${PLUGIN_NAME}] QUOTA limit exceeded. ${c.advice} Stop making further tool calls, finish the in-progress work, then report the quota status to the user.`;
|
|
@@ -361,9 +480,12 @@ Then: harness_done(). Follow the [usage-coach NEXT] directive each tool returns.
|
|
|
361
480
|
model: tool.schema.string().optional()
|
|
362
481
|
},
|
|
363
482
|
async execute(args, ctx) {
|
|
483
|
+
const cfg = readHarnessCfg(ctx.directory);
|
|
364
484
|
const h = readHarness(ctx.sessionID) ?? { name: "batch", total: 0, current: 0, tasks: [], usage: {}, active: true };
|
|
365
485
|
h.tasks = h.tasks.filter((x) => x.id !== args.id);
|
|
366
|
-
|
|
486
|
+
const model = args.model || cfg.generator || "";
|
|
487
|
+
if (!model) return `ERROR: task ${args.id} has no model and no generator configured. Set "generator" in harness.config.json.`;
|
|
488
|
+
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
489
|
if (args.id > h.current) h.current = args.id;
|
|
368
490
|
writeHarness(ctx.sessionID, h);
|
|
369
491
|
return `task ${args.id} -> ${args.status}${args.score ? ` (${args.score})` : ""}`;
|
|
@@ -394,8 +516,8 @@ Then: harness_done(). Follow the [usage-coach NEXT] directive each tool returns.
|
|
|
394
516
|
async execute(args, _ctx) {
|
|
395
517
|
const rec = { ts: (/* @__PURE__ */ new Date()).toISOString(), task: args.task, prompt: args.prompt, gradeResult: args.gradeResult, model: args.model, revisions: args.revisions };
|
|
396
518
|
try {
|
|
397
|
-
|
|
398
|
-
|
|
519
|
+
mkdirSync2(STATE_DIR, { recursive: true });
|
|
520
|
+
appendFileSync2(failuresFile(), JSON.stringify(rec) + "\n");
|
|
399
521
|
} catch (e) {
|
|
400
522
|
log(`record_failure err: ${String(e)}`);
|
|
401
523
|
}
|
|
@@ -413,6 +535,25 @@ Then: harness_done(). Follow the [usage-coach NEXT] directive each tool returns.
|
|
|
413
535
|
async execute(args, ctx) {
|
|
414
536
|
const cfg = readHarnessCfg(ctx.directory);
|
|
415
537
|
if (!cfg.generator) return 'ERROR: no generator model configured. Set "generator" in harness.config.json (see harness.config.example.json).';
|
|
538
|
+
let domainPrefix = "";
|
|
539
|
+
let keywords = [];
|
|
540
|
+
let domainEmpty = true;
|
|
541
|
+
try {
|
|
542
|
+
keywords = extractKeywords(`${args.task} ${args.gradeResult}`);
|
|
543
|
+
if (keywords.length) {
|
|
544
|
+
const { nodes, edges } = queryDomain(keywords);
|
|
545
|
+
if (nodes && nodes.length || edges && edges.length) {
|
|
546
|
+
domainEmpty = false;
|
|
547
|
+
domainPrefix = `Known facts from domain DB: ${JSON.stringify({ nodes, edges })}. Use these if relevant.
|
|
548
|
+
|
|
549
|
+
---
|
|
550
|
+
|
|
551
|
+
`;
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
} catch (e) {
|
|
555
|
+
log(`investigate domain query err: ${String(e)}`);
|
|
556
|
+
}
|
|
416
557
|
const rcaPrompt = `A task failed. Analyze the ROOT CAUSE (not just the symptom).
|
|
417
558
|
Task: ${args.task}
|
|
418
559
|
What was expected (from grade): ${args.gradeResult}
|
|
@@ -421,7 +562,14 @@ Output a structured root cause:
|
|
|
421
562
|
category: (one of: constraint-violation, missing-context, tool-misuse, model-limitation, other)
|
|
422
563
|
explanation: <why it failed>
|
|
423
564
|
evidence: <file/line or specific quote>`;
|
|
424
|
-
const out = await runModel(input.client, cfg.generator, rcaPrompt, ctx.directory);
|
|
565
|
+
const out = await runModel(input.client, cfg.generator, domainPrefix + rcaPrompt, ctx.directory);
|
|
566
|
+
if (domainEmpty && keywords.length) {
|
|
567
|
+
try {
|
|
568
|
+
saveInvestigationResult(keywords, out, "investigate");
|
|
569
|
+
} catch (e) {
|
|
570
|
+
log(`investigate save err: ${String(e)}`);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
425
573
|
return out + "\n[usage-coach NEXT] call verify_diagnosis with this diagnosis.";
|
|
426
574
|
}
|
|
427
575
|
}),
|
|
@@ -473,8 +621,8 @@ Keep it concrete and actionable.`;
|
|
|
473
621
|
const rule = out;
|
|
474
622
|
try {
|
|
475
623
|
const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
476
|
-
|
|
477
|
-
|
|
624
|
+
mkdirSync2(STATE_DIR, { recursive: true });
|
|
625
|
+
appendFileSync2(rulesFile(), `## Rule (${date})
|
|
478
626
|
${rule}
|
|
479
627
|
Origin: ${args.task}
|
|
480
628
|
|
|
@@ -502,13 +650,38 @@ Origin: ${args.task}
|
|
|
502
650
|
const throttle = decision === "THROTTLE" && cfg.lighterModel;
|
|
503
651
|
const model = throttle ? cfg.lighterModel : cfg.generator;
|
|
504
652
|
const rules = readRules();
|
|
505
|
-
|
|
653
|
+
let prefix = rules ? `Lessons learned from previous failures (apply where relevant):
|
|
506
654
|
${rules}
|
|
507
655
|
|
|
508
656
|
---
|
|
509
657
|
|
|
510
658
|
` : "";
|
|
659
|
+
let keywords = [];
|
|
660
|
+
let domainEmpty = true;
|
|
661
|
+
try {
|
|
662
|
+
keywords = extractKeywords(args.prompt);
|
|
663
|
+
if (keywords.length) {
|
|
664
|
+
const { nodes, edges } = queryDomain(keywords);
|
|
665
|
+
if (nodes && nodes.length || edges && edges.length) {
|
|
666
|
+
domainEmpty = false;
|
|
667
|
+
prefix = `Known facts from domain DB: ${JSON.stringify({ nodes, edges })}. Use these if relevant.
|
|
668
|
+
|
|
669
|
+
---
|
|
670
|
+
|
|
671
|
+
` + prefix;
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
} catch (e) {
|
|
675
|
+
log(`generate domain query err: ${String(e)}`);
|
|
676
|
+
}
|
|
511
677
|
const out = await runModel(input.client, model, prefix + args.prompt, ctx.directory);
|
|
678
|
+
if (domainEmpty && keywords.length) {
|
|
679
|
+
try {
|
|
680
|
+
saveInvestigationResult(keywords, out, "generate");
|
|
681
|
+
} catch (e) {
|
|
682
|
+
log(`generate save err: ${String(e)}`);
|
|
683
|
+
}
|
|
684
|
+
}
|
|
512
685
|
return out + (throttle ? `
|
|
513
686
|
[usage-coach] quota THROTTLE \u2014 used lighter model ${cfg.lighterModel}` : "") + `
|
|
514
687
|
[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);
|
|
@@ -129,11 +131,9 @@ function initializeTui(api, disposeRoot) {
|
|
|
129
131
|
tlog(`api probe err: ${String(e)}`);
|
|
130
132
|
}
|
|
131
133
|
const [getState, setState] = createSignal(readState());
|
|
132
|
-
const [getHarness, setHarness] = createSignal(readHarness());
|
|
133
134
|
const timer = setInterval(() => {
|
|
134
135
|
try {
|
|
135
136
|
setState(readState());
|
|
136
|
-
setHarness(readHarness());
|
|
137
137
|
} catch {
|
|
138
138
|
}
|
|
139
139
|
}, 3e3);
|
|
@@ -182,7 +182,7 @@ function initializeTui(api, disposeRoot) {
|
|
|
182
182
|
return _el$;
|
|
183
183
|
})();
|
|
184
184
|
}
|
|
185
|
-
let s
|
|
185
|
+
let s;
|
|
186
186
|
try {
|
|
187
187
|
s = getState();
|
|
188
188
|
} catch {
|
|
@@ -236,7 +236,7 @@ function initializeTui(api, disposeRoot) {
|
|
|
236
236
|
_$insert(_el$12, () => p.fiveHour, _el$14);
|
|
237
237
|
_$insert(_el$12, () => p.fiveHourReset, null);
|
|
238
238
|
_$effect((_p$) => {
|
|
239
|
-
var _v$ = st("text"), _v$2 = st("
|
|
239
|
+
var _v$ = st("text"), _v$2 = st("text");
|
|
240
240
|
_v$ !== _p$.e && (_p$.e = _$setProp(_el$10, "style", _v$, _p$.e));
|
|
241
241
|
_v$2 !== _p$.t && (_p$.t = _$setProp(_el$11, "style", _v$2, _p$.t));
|
|
242
242
|
return _p$;
|
|
@@ -253,7 +253,7 @@ function initializeTui(api, disposeRoot) {
|
|
|
253
253
|
_$insertNode(_el$15, _el$19);
|
|
254
254
|
_$insertNode(_el$15, _el$20);
|
|
255
255
|
_$setProp(_el$15, "flexDirection", "row");
|
|
256
|
-
_$insertNode(_el$16, _$createTextNode(`
|
|
256
|
+
_$insertNode(_el$16, _$createTextNode(` 1w `));
|
|
257
257
|
_$insert(_el$18, () => barFill(p.weekly));
|
|
258
258
|
_$insert(_el$19, () => barEmpty(p.weekly));
|
|
259
259
|
_$insertNode(_el$20, _el$21);
|
|
@@ -261,7 +261,7 @@ function initializeTui(api, disposeRoot) {
|
|
|
261
261
|
_$insert(_el$20, () => p.weekly, _el$22);
|
|
262
262
|
_$insert(_el$20, () => p.weeklyReset, null);
|
|
263
263
|
_$effect((_p$) => {
|
|
264
|
-
var _v$3 = st("text"), _v$4 = st("
|
|
264
|
+
var _v$3 = st("text"), _v$4 = st("text");
|
|
265
265
|
_v$3 !== _p$.e && (_p$.e = _$setProp(_el$18, "style", _v$3, _p$.e));
|
|
266
266
|
_v$4 !== _p$.t && (_p$.t = _$setProp(_el$19, "style", _v$4, _p$.t));
|
|
267
267
|
return _p$;
|
|
@@ -281,7 +281,7 @@ function initializeTui(api, disposeRoot) {
|
|
|
281
281
|
}
|
|
282
282
|
} else {
|
|
283
283
|
nodes.push((() => {
|
|
284
|
-
var _el$27 = _$createElement("box"), _el$28 = _$createElement("text"), _el$30 = _$createElement("text"), _el$31 = _$createElement("text"), _el$32 = _$createElement("text")
|
|
284
|
+
var _el$27 = _$createElement("box"), _el$28 = _$createElement("text"), _el$30 = _$createElement("text"), _el$31 = _$createElement("text"), _el$32 = _$createElement("text");
|
|
285
285
|
_$insertNode(_el$27, _el$28);
|
|
286
286
|
_$insertNode(_el$27, _el$30);
|
|
287
287
|
_$insertNode(_el$27, _el$31);
|
|
@@ -290,11 +290,9 @@ function initializeTui(api, disposeRoot) {
|
|
|
290
290
|
_$insertNode(_el$28, _$createTextNode(` 5h `));
|
|
291
291
|
_$insert(_el$30, () => barFill(s.fiveHour));
|
|
292
292
|
_$insert(_el$31, () => barEmpty(s.fiveHour));
|
|
293
|
-
_$insertNode(_el$32,
|
|
294
|
-
_$insertNode(_el$32, _el$34);
|
|
295
|
-
_$insert(_el$32, () => s.fiveHour, _el$34);
|
|
293
|
+
_$insertNode(_el$32, _$createTextNode(` 0%`));
|
|
296
294
|
_$effect((_p$) => {
|
|
297
|
-
var _v$5 = st("text"), _v$6 = st("
|
|
295
|
+
var _v$5 = st("text"), _v$6 = st("text");
|
|
298
296
|
_v$5 !== _p$.e && (_p$.e = _$setProp(_el$30, "style", _v$5, _p$.e));
|
|
299
297
|
_v$6 !== _p$.t && (_p$.t = _$setProp(_el$31, "style", _v$6, _p$.t));
|
|
300
298
|
return _p$;
|
|
@@ -305,77 +303,51 @@ function initializeTui(api, disposeRoot) {
|
|
|
305
303
|
return _el$27;
|
|
306
304
|
})());
|
|
307
305
|
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);
|
|
306
|
+
var _el$34 = _$createElement("box"), _el$35 = _$createElement("text"), _el$37 = _$createElement("text"), _el$38 = _$createElement("text"), _el$39 = _$createElement("text");
|
|
307
|
+
_$insertNode(_el$34, _el$35);
|
|
308
|
+
_$insertNode(_el$34, _el$37);
|
|
309
|
+
_$insertNode(_el$34, _el$38);
|
|
310
|
+
_$insertNode(_el$34, _el$39);
|
|
311
|
+
_$setProp(_el$34, "flexDirection", "row");
|
|
312
|
+
_$insertNode(_el$35, _$createTextNode(` 1w `));
|
|
313
|
+
_$insert(_el$37, () => barFill(s.weekly));
|
|
314
|
+
_$insert(_el$38, () => barEmpty(s.weekly));
|
|
315
|
+
_$insertNode(_el$39, _$createTextNode(` 0%`));
|
|
320
316
|
_$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$
|
|
317
|
+
var _v$7 = st("text"), _v$8 = st("text");
|
|
318
|
+
_v$7 !== _p$.e && (_p$.e = _$setProp(_el$37, "style", _v$7, _p$.e));
|
|
319
|
+
_v$8 !== _p$.t && (_p$.t = _$setProp(_el$38, "style", _v$8, _p$.t));
|
|
324
320
|
return _p$;
|
|
325
321
|
}, {
|
|
326
322
|
e: void 0,
|
|
327
323
|
t: void 0
|
|
328
324
|
});
|
|
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;
|
|
325
|
+
return _el$34;
|
|
354
326
|
})());
|
|
355
327
|
}
|
|
356
328
|
} else {
|
|
357
329
|
nodes.push((() => {
|
|
358
|
-
var _el$
|
|
359
|
-
_$insertNode(_el$
|
|
360
|
-
return _el$
|
|
330
|
+
var _el$41 = _$createElement("text");
|
|
331
|
+
_$insertNode(_el$41, _$createTextNode(`usage-coach: ...`));
|
|
332
|
+
return _el$41;
|
|
361
333
|
})());
|
|
362
334
|
}
|
|
363
335
|
if (h && h.active !== false && h.tasks.length > 0) {
|
|
364
336
|
nodes.push((() => {
|
|
365
|
-
var _el$
|
|
366
|
-
_$insertNode(_el$
|
|
367
|
-
return _el$
|
|
337
|
+
var _el$43 = _$createElement("text");
|
|
338
|
+
_$insertNode(_el$43, _$createTextNode(` `));
|
|
339
|
+
return _el$43;
|
|
368
340
|
})());
|
|
369
341
|
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$
|
|
342
|
+
var _el$45 = _$createElement("text"), _el$46 = _$createTextNode(`harness: `), _el$47 = _$createTextNode(` `), _el$48 = _$createTextNode(`/`);
|
|
343
|
+
_$insertNode(_el$45, _el$46);
|
|
344
|
+
_$insertNode(_el$45, _el$47);
|
|
345
|
+
_$insertNode(_el$45, _el$48);
|
|
346
|
+
_$insert(_el$45, () => h.name, _el$47);
|
|
347
|
+
_$insert(_el$45, () => h.current, _el$48);
|
|
348
|
+
_$insert(_el$45, () => h.total, null);
|
|
349
|
+
_$effect((_$p) => _$setProp(_el$45, "style", st("textMuted"), _$p));
|
|
350
|
+
return _el$45;
|
|
379
351
|
})());
|
|
380
352
|
for (const t of h.tasks) {
|
|
381
353
|
const sKey = statusKey[t.status] ?? "text";
|
|
@@ -385,54 +357,54 @@ function initializeTui(api, disposeRoot) {
|
|
|
385
357
|
const elapsed = t.startedAt ? Math.max(0, Math.round((Date.now() - new Date(t.startedAt).getTime()) / 1e3)) : 0;
|
|
386
358
|
const elapsedStr = t.status === "completed" || t.status === "failed" ? "" : elapsed > 0 ? ` ${elapsed}s` : "";
|
|
387
359
|
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$
|
|
360
|
+
var _el$49 = _$createElement("text"), _el$50 = _$createTextNode(` \u25CF `), _el$51 = _$createTextNode(` `), _el$52 = _$createTextNode(` `);
|
|
361
|
+
_$insertNode(_el$49, _el$50);
|
|
362
|
+
_$insertNode(_el$49, _el$51);
|
|
363
|
+
_$insertNode(_el$49, _el$52);
|
|
364
|
+
_$insert(_el$49, () => t.id, _el$51);
|
|
365
|
+
_$insert(_el$49, mdl, _el$51);
|
|
366
|
+
_$insert(_el$49, lbl, _el$52);
|
|
367
|
+
_$insert(_el$49, rev, _el$52);
|
|
368
|
+
_$insert(_el$49, elapsedStr, _el$52);
|
|
369
|
+
_$insert(_el$49, () => t.title, null);
|
|
370
|
+
_$effect((_$p) => _$setProp(_el$49, "style", st(sKey), _$p));
|
|
371
|
+
return _el$49;
|
|
400
372
|
})());
|
|
401
373
|
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
|
|
374
|
+
const provCoach = pv ? s?.providers?.find((p) => p.id === pv || pv && p.id.startsWith(pv) || pv && pv.startsWith(p.id)) : s?.providers?.[0];
|
|
375
|
+
const rawPct = provCoach?.fiveHour ?? s?.fiveHour ?? -1;
|
|
404
376
|
const pct = rawPct < 0 ? 0 : rawPct;
|
|
405
377
|
const pctLabel = rawPct < 0 ? "n/a" : `${rawPct}%`;
|
|
406
378
|
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$
|
|
379
|
+
var _el$53 = _$createElement("box"), _el$54 = _$createElement("text"), _el$56 = _$createElement("text"), _el$57 = _$createElement("text"), _el$58 = _$createElement("text"), _el$59 = _$createTextNode(` `);
|
|
380
|
+
_$insertNode(_el$53, _el$54);
|
|
381
|
+
_$insertNode(_el$53, _el$56);
|
|
382
|
+
_$insertNode(_el$53, _el$57);
|
|
383
|
+
_$insertNode(_el$53, _el$58);
|
|
384
|
+
_$setProp(_el$53, "flexDirection", "row");
|
|
385
|
+
_$insertNode(_el$54, _$createTextNode(` 5h `));
|
|
386
|
+
_$insert(_el$56, () => barFill(pct));
|
|
387
|
+
_$insert(_el$57, () => barEmpty(pct));
|
|
388
|
+
_$insertNode(_el$58, _el$59);
|
|
389
|
+
_$insert(_el$58, pctLabel, null);
|
|
418
390
|
_$effect((_p$) => {
|
|
419
|
-
var _v$
|
|
420
|
-
_v$
|
|
421
|
-
_v$
|
|
391
|
+
var _v$9 = st("text"), _v$0 = st("text");
|
|
392
|
+
_v$9 !== _p$.e && (_p$.e = _$setProp(_el$56, "style", _v$9, _p$.e));
|
|
393
|
+
_v$0 !== _p$.t && (_p$.t = _$setProp(_el$57, "style", _v$0, _p$.t));
|
|
422
394
|
return _p$;
|
|
423
395
|
}, {
|
|
424
396
|
e: void 0,
|
|
425
397
|
t: void 0
|
|
426
398
|
});
|
|
427
|
-
return _el$
|
|
399
|
+
return _el$53;
|
|
428
400
|
})());
|
|
429
401
|
}
|
|
430
402
|
}
|
|
431
403
|
return (() => {
|
|
432
|
-
var _el$
|
|
433
|
-
_$setProp(_el$
|
|
434
|
-
_$insert(_el$
|
|
435
|
-
return _el$
|
|
404
|
+
var _el$60 = _$createElement("box");
|
|
405
|
+
_$setProp(_el$60, "flexDirection", "column");
|
|
406
|
+
_$insert(_el$60, nodes);
|
|
407
|
+
return _el$60;
|
|
436
408
|
})();
|
|
437
409
|
};
|
|
438
410
|
tlog("registering slots");
|
|
@@ -441,16 +413,18 @@ function initializeTui(api, disposeRoot) {
|
|
|
441
413
|
slots: {
|
|
442
414
|
sidebar_footer(ctx) {
|
|
443
415
|
tlog("sidebar_footer slot called");
|
|
416
|
+
let result;
|
|
444
417
|
try {
|
|
445
|
-
|
|
418
|
+
result = panel(ctx);
|
|
446
419
|
} catch (e) {
|
|
447
420
|
tlog(`sidebar_footer err: ${String(e)}`);
|
|
448
|
-
|
|
449
|
-
var _el$
|
|
450
|
-
_$insertNode(_el$
|
|
451
|
-
return _el$
|
|
421
|
+
result = (() => {
|
|
422
|
+
var _el$61 = _$createElement("text");
|
|
423
|
+
_$insertNode(_el$61, _$createTextNode(`usage-coach`));
|
|
424
|
+
return _el$61;
|
|
452
425
|
})();
|
|
453
426
|
}
|
|
427
|
+
return result;
|
|
454
428
|
}
|
|
455
429
|
}
|
|
456
430
|
});
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "opencode-usage-coach",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "opencode closed-loop usage coach
|
|
3
|
+
"version": "0.4.0",
|
|
4
|
+
"description": "opencode closed-loop usage coach — quota SENSE -> coaching DECIDE -> loop ACT + TUI integration",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
7
7
|
"module": "./dist/index.js",
|
|
@@ -18,6 +18,8 @@
|
|
|
18
18
|
"scripts": {
|
|
19
19
|
"build": "tsup",
|
|
20
20
|
"typecheck": "tsc --noEmit",
|
|
21
|
+
"lint": "eslint .",
|
|
22
|
+
"lint:fix": "eslint . --fix",
|
|
21
23
|
"prepack": "tsup"
|
|
22
24
|
},
|
|
23
25
|
"files": [
|
|
@@ -44,12 +46,17 @@
|
|
|
44
46
|
"solid-js": ">=1.9.12"
|
|
45
47
|
},
|
|
46
48
|
"devDependencies": {
|
|
49
|
+
"@eslint/js": "^10.0.1",
|
|
47
50
|
"@opencode-ai/plugin": "*",
|
|
48
51
|
"@opentui/core": ">=0.4.0",
|
|
49
52
|
"@opentui/solid": ">=0.4.0",
|
|
50
53
|
"esbuild-plugin-solid": "^0.6.0",
|
|
54
|
+
"eslint": "^10.6.0",
|
|
55
|
+
"eslint-plugin-solid": "^0.14.5",
|
|
56
|
+
"globals": "^17.7.0",
|
|
51
57
|
"solid-js": "^1.9",
|
|
52
58
|
"tsup": "^8.5",
|
|
53
|
-
"typescript": "^5"
|
|
59
|
+
"typescript": "^5",
|
|
60
|
+
"typescript-eslint": "^8.63.0"
|
|
54
61
|
}
|
|
55
62
|
}
|