opencode-usage-coach 0.2.1 → 0.2.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js 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
  }
@@ -68,6 +73,7 @@ function readHarnessCfg(dir) {
68
73
  };
69
74
  }
70
75
  async function runModel(client, model, prompt, directory) {
76
+ const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
71
77
  try {
72
78
  const slash = model.indexOf("/");
73
79
  const providerID = slash >= 0 ? model.slice(0, slash) : model;
@@ -75,11 +81,20 @@ async function runModel(client, model, prompt, directory) {
75
81
  const s = await client.session.create({ body: { title: "uc-harness-sub" }, query: { directory } });
76
82
  const id = s?.data?.info?.id;
77
83
  if (!id) return null;
78
- const r = await client.session.prompt({
84
+ await client.session.prompt({
79
85
  path: { id },
80
86
  body: { model: { providerID, modelID }, parts: [{ type: "text", text: prompt }] }
81
87
  });
82
- const parts = r?.data?.parts ?? [];
88
+ for (let i = 0; i < 600; i++) {
89
+ await sleep(1e3);
90
+ const st = await client.session.status({ path: { id } });
91
+ const status = st?.data?.[id]?.status ?? st?.data?.status;
92
+ if (status === "idle" || status === "completed" || !status) break;
93
+ }
94
+ const msgs = await client.session.messages({ path: { id } });
95
+ const all = msgs?.data ?? [];
96
+ const lastAssistant = all.filter((m) => m?.info?.role === "assistant").pop();
97
+ const parts = lastAssistant?.parts ?? [];
83
98
  const text = parts.filter((p) => p?.type === "text").map((p) => p?.text ?? "").join("");
84
99
  try {
85
100
  await client.session.remove?.({ path: { id } });
@@ -298,8 +313,8 @@ async function UsageCoachPlugin(input) {
298
313
  harness_start: tool({
299
314
  description: "Start the harness: register the total task count on the panel. Call once when the harness loop begins.",
300
315
  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() });
316
+ async execute(args, ctx) {
317
+ writeHarness(ctx.sessionID, { name: args.name, total: args.total, current: 0, tasks: [], usage: {}, active: true, startedAt: (/* @__PURE__ */ new Date()).toISOString() });
303
318
  return `Harness '${args.name}' started (${args.total} tasks). Now report each task's status via task_update and run the loop.`;
304
319
  }
305
320
  }),
@@ -313,31 +328,31 @@ async function UsageCoachPlugin(input) {
313
328
  score: tool.schema.string().optional().describe("PASS | FAIL"),
314
329
  model: tool.schema.string().optional()
315
330
  },
316
- async execute(args) {
317
- const h = readHarness() ?? { name: "batch", total: 0, current: 0, tasks: [], usage: {} };
331
+ async execute(args, ctx) {
332
+ const h = readHarness(ctx.sessionID) ?? { name: "batch", total: 0, current: 0, tasks: [], usage: {}, active: true };
318
333
  h.tasks = h.tasks.filter((x) => x.id !== args.id);
319
334
  h.tasks.push({ id: args.id, title: args.title, status: args.status, model: args.model ?? "", revisions: args.revisions ?? 0, score: args.score ?? null });
320
335
  if (args.id > h.current) h.current = args.id;
321
- writeHarness(h);
336
+ writeHarness(ctx.sessionID, h);
322
337
  return `task ${args.id} -> ${args.status}${args.score ? ` (${args.score})` : ""}`;
323
338
  }
324
339
  }),
325
340
  harness_done: tool({
326
341
  description: "Mark the harness as complete \u2014 call when the loop ends.",
327
342
  args: {},
328
- async execute() {
329
- const h = readHarness();
343
+ async execute(_args, ctx) {
344
+ const h = readHarness(ctx.sessionID);
330
345
  if (h) {
331
346
  h.current = h.total;
332
- writeHarness(h);
347
+ h.active = false;
348
+ writeHarness(ctx.sessionID, h);
333
349
  }
334
350
  return "Harness complete.";
335
351
  }
336
352
  }),
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.
353
+ // Per-role model execution (config-driven, same server, no deadlock).
339
354
  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.",
355
+ description: "Run the GENERATOR model on a prompt. The generator can use tools (write/edit files). Returns the model's text response.",
341
356
  args: { prompt: tool.schema.string() },
342
357
  async execute(args, ctx) {
343
358
  const cfg = readHarnessCfg(ctx.directory);
@@ -346,8 +361,21 @@ async function UsageCoachPlugin(input) {
346
361
  return out ?? `(generator ${model} produced no text)`;
347
362
  }
348
363
  }),
364
+ generate_batch: tool({
365
+ 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).",
366
+ args: { tasks: tool.schema.array(tool.schema.object({ id: tool.schema.number(), prompt: tool.schema.string() })) },
367
+ async execute(args, ctx) {
368
+ const cfg = readHarnessCfg(ctx.directory);
369
+ const model = cfg.generator ?? "zai-coding-plan/glm-5.1";
370
+ const results = await Promise.all(args.tasks.map(async (t) => {
371
+ const out = await runModel(input.client, model, t.prompt, ctx.directory);
372
+ return `[task ${t.id}] ${out ?? "(no output)"}`;
373
+ }));
374
+ return results.join("\n\n");
375
+ }
376
+ }),
349
377
  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.",
378
+ 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
379
  args: { prompt: tool.schema.string() },
352
380
  async execute(args, ctx) {
353
381
  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
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-usage-coach",
3
- "version": "0.2.1",
3
+ "version": "0.2.3",
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",