opencode-usage-coach 0.2.0 → 0.2.2

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 CHANGED
@@ -13,15 +13,16 @@ the loop** — and ships a harness agent mode + a deterministic orchestrator.
13
13
  - Senses z.ai quota windows via the `codexbar` CLI.
14
14
  - On STOP threshold: blocks tool calls (`tool.execute.before` throws) → the agent self-stops.
15
15
  - Injects coaching (how to use right now) into the system prompt (double defense).
16
- - Surfaces a sidebar panel: quota meters + harness task states + per-model token usage.
16
+ - Surfaces a sidebar panel: per-provider quota meters + harness task states + coaching.
17
17
  - Defensive: any plugin error never breaks opencode.
18
18
 
19
- **Harness (two ways):**
20
- - **Agent mode** (`usage-coach-harness`): triages each request — trivial → do directly;
21
- unclear → clarify; substantive → generate(delegate)→grade(delegate)→revise→advance.
22
- Conversation-driven, no file required. Adaptive (best-effort).
23
- - **Deterministic script** (`harness.ts`): `tasks.txt` + `rubric.md` guaranteed loop.
24
- Reliable for batch jobs. Tracks per-model tokens, auto-splits on timeout.
19
+ **Harness (agent mode, 1 terminal):**
20
+ - The `usage-coach-harness` agent triages each request — trivial → do directly;
21
+ unclear → clarify; substantive → generate→grade→revise→advance.
22
+ - Multi-model via plugin tools `generate`/`grade`: run the configured generator/grader model
23
+ (from `harness.config.json`) in a new session on the same server — no second terminal.
24
+ e.g. GLM generation + free mimo grading in one opencode session.
25
+ - Reports progress via `task_update` → the sidebar panel shows live task states.
25
26
 
26
27
  ## Requirements
27
28
  - opencode (tested on 1.17.13) with a z.ai coding-plan provider configured.
@@ -118,8 +119,8 @@ opencode's bundled solid); TUI plugins must be compiled + loaded via `tui.json`
118
119
  `codexbar` must be called via `spawn` (the `$` BunShell leaks output to the TUI).
119
120
 
120
121
  ## Status
121
- - ✅ M0–M2 quota guardian + TUI panel
122
- - ✅ M3 harness: agent mode (triage) + deterministic script (per-model tokens, auto-split)
123
- - M4 npm packaging
122
+ - ✅ M0–M2 quota guardian + TUI panel (per-provider coach view, colors, collapsible Alt+H)
123
+ - ✅ M3 harness: agent mode (triage) with generate/grade model-specific tools (1 terminal, multi-model)
124
+ - M4 npm packaging (`opencode plugin install opencode-usage-coach`)
124
125
 
125
126
  License: MIT.
package/dist/index.js CHANGED
@@ -3,7 +3,7 @@ import { mkdirSync, writeFileSync, appendFileSync, readFileSync, existsSync } fr
3
3
  import { spawn } from "child_process";
4
4
  import { createHash } from "crypto";
5
5
  import { homedir } from "os";
6
- import { join, resolve } from "path";
6
+ import { join, resolve, dirname } from "path";
7
7
  import { tool } from "@opencode-ai/plugin";
8
8
  var PLUGIN_NAME = "opencode-usage-coach";
9
9
  var DEBUG = process.env.UC_DEBUG === "1";
@@ -38,19 +38,24 @@ function writeState(c) {
38
38
  } catch {
39
39
  }
40
40
  }
41
- function readHarness() {
41
+ function harnessFile(sessionID) {
42
+ return join(STATE_DIR, sessionID || "_default", "harness.json");
43
+ }
44
+ function readHarness(sessionID) {
42
45
  try {
43
- if (!existsSync(HARNESS_FILE)) return null;
44
- return JSON.parse(readFileSync(HARNESS_FILE, "utf8"));
46
+ const f = harnessFile(sessionID);
47
+ if (!existsSync(f)) return null;
48
+ return JSON.parse(readFileSync(f, "utf8"));
45
49
  } catch {
46
50
  return null;
47
51
  }
48
52
  }
49
- function writeHarness(h) {
53
+ function writeHarness(sessionID, h) {
50
54
  try {
51
- mkdirSync(STATE_DIR, { recursive: true });
55
+ const f = harnessFile(sessionID);
56
+ mkdirSync(dirname(f), { recursive: true });
52
57
  h.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
53
- writeFileSync(HARNESS_FILE, JSON.stringify(h, null, 2));
58
+ writeFileSync(f, JSON.stringify(h, null, 2));
54
59
  } catch {
55
60
  }
56
61
  }
@@ -298,8 +303,8 @@ async function UsageCoachPlugin(input) {
298
303
  harness_start: tool({
299
304
  description: "Start the harness: register the total task count on the panel. Call once when the harness loop begins.",
300
305
  args: { name: tool.schema.string(), total: tool.schema.number() },
301
- async execute(args) {
302
- writeHarness({ name: args.name, total: args.total, current: 0, tasks: [], usage: {}, startedAt: (/* @__PURE__ */ new Date()).toISOString() });
306
+ async execute(args, ctx) {
307
+ writeHarness(ctx.sessionID, { name: args.name, total: args.total, current: 0, tasks: [], usage: {}, active: true, startedAt: (/* @__PURE__ */ new Date()).toISOString() });
303
308
  return `Harness '${args.name}' started (${args.total} tasks). Now report each task's status via task_update and run the loop.`;
304
309
  }
305
310
  }),
@@ -313,31 +318,31 @@ async function UsageCoachPlugin(input) {
313
318
  score: tool.schema.string().optional().describe("PASS | FAIL"),
314
319
  model: tool.schema.string().optional()
315
320
  },
316
- async execute(args) {
317
- const h = readHarness() ?? { name: "batch", total: 0, current: 0, tasks: [], usage: {} };
321
+ async execute(args, ctx) {
322
+ const h = readHarness(ctx.sessionID) ?? { name: "batch", total: 0, current: 0, tasks: [], usage: {}, active: true };
318
323
  h.tasks = h.tasks.filter((x) => x.id !== args.id);
319
324
  h.tasks.push({ id: args.id, title: args.title, status: args.status, model: args.model ?? "", revisions: args.revisions ?? 0, score: args.score ?? null });
320
325
  if (args.id > h.current) h.current = args.id;
321
- writeHarness(h);
326
+ writeHarness(ctx.sessionID, h);
322
327
  return `task ${args.id} -> ${args.status}${args.score ? ` (${args.score})` : ""}`;
323
328
  }
324
329
  }),
325
330
  harness_done: tool({
326
331
  description: "Mark the harness as complete \u2014 call when the loop ends.",
327
332
  args: {},
328
- async execute() {
329
- const h = readHarness();
333
+ async execute(_args, ctx) {
334
+ const h = readHarness(ctx.sessionID);
330
335
  if (h) {
331
336
  h.current = h.total;
332
- writeHarness(h);
337
+ h.active = false;
338
+ writeHarness(ctx.sessionID, h);
333
339
  }
334
340
  return "Harness complete.";
335
341
  }
336
342
  }),
337
- // Per-role model execution (config-driven). Runs a NEW session with the configured model
338
- // on the same server — enables multi-model (e.g. GLM generate + free mimo grade) in 1 terminal.
343
+ // Per-role model execution (config-driven, same server, no deadlock).
339
344
  generate: tool({
340
- description: "Run the GENERATOR model (from harness.config.json) on a prompt. Returns the model's text response. The generator can use tools (write/edit files) in the directory. Use for the 'do the work' step.",
345
+ description: "Run the GENERATOR model on a prompt. The generator can use tools (write/edit files). Returns the model's text response.",
341
346
  args: { prompt: tool.schema.string() },
342
347
  async execute(args, ctx) {
343
348
  const cfg = readHarnessCfg(ctx.directory);
@@ -346,8 +351,21 @@ async function UsageCoachPlugin(input) {
346
351
  return out ?? `(generator ${model} produced no text)`;
347
352
  }
348
353
  }),
354
+ generate_batch: tool({
355
+ description: "Run the GENERATOR model on MULTIPLE tasks in PARALLEL. Pass an array of {id, prompt}. Returns all results at once. Use for INDEPENDENT tasks (much faster than sequential generate calls).",
356
+ args: { tasks: tool.schema.array(tool.schema.object({ id: tool.schema.number(), prompt: tool.schema.string() })) },
357
+ async execute(args, ctx) {
358
+ const cfg = readHarnessCfg(ctx.directory);
359
+ const model = cfg.generator ?? "zai-coding-plan/glm-5.1";
360
+ const results = await Promise.all(args.tasks.map(async (t) => {
361
+ const out = await runModel(input.client, model, t.prompt, ctx.directory);
362
+ return `[task ${t.id}] ${out ?? "(no output)"}`;
363
+ }));
364
+ return results.join("\n\n");
365
+ }
366
+ }),
349
367
  grade: tool({
350
- description: "Run the GRADER model (from harness.config.json) on a prompt. Returns the model's text response (expect PASS/FAIL on the first line). Use for the 'evaluate the result' step. Falls back to the generator if the grader has no quota.",
368
+ description: "Run the GRADER model on a prompt. Returns PASS/FAIL on the first line. Falls back to generator if grader quota is out.",
351
369
  args: { prompt: tool.schema.string() },
352
370
  async execute(args, ctx) {
353
371
  const cfg = readHarnessCfg(ctx.directory);
package/dist/tui.js CHANGED
@@ -132,7 +132,11 @@ function initializeTui(api, disposeRoot) {
132
132
  }
133
133
  let h = null;
134
134
  try {
135
- h = getHarness();
135
+ const sid = ctx.session_id ?? "";
136
+ if (sid) {
137
+ const hf = join(STATE_DIR, sid, "harness.json");
138
+ if (existsSync(hf)) h = JSON.parse(readFileSync(hf, "utf8"));
139
+ }
136
140
  } catch {
137
141
  h = null;
138
142
  }
@@ -379,17 +383,6 @@ function initializeTui(api, disposeRoot) {
379
383
  return _el$72;
380
384
  })();
381
385
  }
382
- },
383
- home_footer(ctx) {
384
- try {
385
- return panel(ctx);
386
- } catch {
387
- return (() => {
388
- var _el$74 = _$createElement("text");
389
- _$insertNode(_el$74, _$createTextNode(`usage-coach`));
390
- return _el$74;
391
- })();
392
- }
393
386
  }
394
387
  }
395
388
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-usage-coach",
3
- "version": "0.2.0",
3
+ "version": "0.2.2",
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",