opencode-usage-coach 0.8.1 → 0.8.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.
@@ -50,14 +50,15 @@ The user's message is the task source. If it has multiple distinct parts, decomp
50
50
  DEPENDENT tasks (B needs A) → always sequential `generate` calls, regardless of quota.
51
51
 
52
52
  1. Call `harness_start(name, N)` to register the run on the panel.
53
- 2. **PRE-FLIGHT — unknown_scan:** Call `unknown_scan({prompt, tasks: [{id, title}, ...]})`.
53
+ 2. **DIAGNOSIS GATE — unknown_scan (REQUIRED, not optional):** Call `unknown_scan({prompt, tasks: [{id, title}, ...]})`.
54
+ This is enforced: if you skip it, `generate` will inject a ⚠ warning into the sub-session prompt.
54
55
  Review the report:
55
56
  - If QUESTIONS are flagged → ask the user concisely, then adjust tasks.
56
57
  - If TASK REFINEMENTS are suggested → apply via `task_update` (split/add/remove).
57
- - If UNKNOWN UNKNOWNS with high impact are found → acknowledge them in the
58
- generate prompts (the domain DB injection will help, but explicitly call them out).
59
- This step prevents wasted steps from wrong assumptions. Skip only if the
60
- task is trivially clear.
58
+ - If UNKNOWN UNKNOWNS with high impact are found → they will be auto-injected into
59
+ generate prompts via scanSummary, but you should explicitly acknowledge them.
60
+ You may skip unknown_scan ONLY for: revisions (applying grade feedback), trivial
61
+ single-file edits, or empty directories. Skipping must be a conscious choice.
61
62
  3. For each task i (1..N):
62
63
  a. `task_update(i, title, "generating")`.
63
64
  b. **Generate** — call `generate({ prompt: "Task: {title}. Perform it for real in the current directory (write/edit files)." })`. The generator model runs in a sub-session and writes files directly — its return value is a summary, NOT the work itself.
@@ -72,6 +73,7 @@ DEPENDENT tasks (B needs A) → always sequential `generate` calls, regardless o
72
73
  4. When all tasks are done → `harness_done()`.
73
74
 
74
75
  ## Rules
76
+ - **Diagnose before acting.** Never implement a fix based on a problem description without verifying what actually happened. Read logs, check source code, reproduce the issue. If you find yourself writing code within 60 seconds of reading a problem, STOP and verify your assumptions first.
75
77
  - **Follow the [usage-coach NEXT] directive each tool returns.** `harness_start`, `generate`, and `grade` all append a `NEXT` line telling you exactly what to call next. This makes the loop deterministic — do not improvise the sequence, follow `NEXT`.
76
78
  - In the loop, do NOT do the work yourself — call `generate`/`grade` (they run the configured models). You orchestrate. (Outside the loop, for trivial requests, act directly.)
77
79
  - Call `task_update` on every state transition — the sidebar panel reads it for live visibility.
package/dist/index.js CHANGED
@@ -837,11 +837,48 @@ function writeUnknownScan(sessionID, result) {
837
837
  const h = readHarness(sessionID);
838
838
  if (h) {
839
839
  h.unknownScan = result;
840
+ h.scanDone = true;
841
+ h.scanSummary = buildScanSummary(result);
840
842
  writeHarness(sessionID, h);
841
843
  }
842
844
  } catch {
843
845
  }
844
846
  }
847
+ function buildScanSummary(r) {
848
+ const lines = [];
849
+ if (r.unknownUnknowns?.length) {
850
+ lines.push(`Unknown Unknowns (${r.unknownUnknowns.length}):`);
851
+ for (const uu of r.unknownUnknowns.slice(0, 5)) {
852
+ lines.push(` [${uu.impact?.toUpperCase() ?? "?"}] ${uu.finding}${uu.mitigation ? ` \u2192 ${uu.mitigation}` : ""}`);
853
+ }
854
+ }
855
+ if (r.unknownKnowns?.length) {
856
+ lines.push(`Implicit knowledge (${r.unknownKnowns.length}):`);
857
+ for (const uk of r.unknownKnowns.slice(0, 5)) {
858
+ lines.push(` \u2139 ${uk.finding}`);
859
+ }
860
+ }
861
+ if (r.questions?.length) {
862
+ lines.push(`Pending questions (${r.questions.length}):`);
863
+ for (const q of r.questions.slice(0, 5)) {
864
+ lines.push(` [Q] ${q.question}`);
865
+ }
866
+ }
867
+ return lines.join("\n");
868
+ }
869
+ function checkScanGate(sessionID) {
870
+ try {
871
+ const h = readHarness(sessionID);
872
+ if (!h || !h.scanRequired) return { warning: null, summary: null };
873
+ if (h.scanDone) return { warning: null, summary: h.scanSummary ?? null };
874
+ return {
875
+ warning: `\u26A0 DIAGNOSIS GATE: unknown_scan was NOT called before this generate. You are generating without pre-flight gap analysis. Blind spots (unknown unknowns) may cause wrong assumptions and waste steps. Call unknown_scan first, OR proceed consciously accepting the risk.`,
876
+ summary: null
877
+ };
878
+ } catch {
879
+ return { warning: null, summary: null };
880
+ }
881
+ }
845
882
  function readHarnessCfg(dir) {
846
883
  const tryRead = (p) => {
847
884
  try {
@@ -1275,12 +1312,15 @@ async function UsageCoachPlugin(input) {
1275
1312
  description: "Start the harness: register the total task count on the panel. Call once when the harness loop begins. IMPORTANT: each generate/generate_batch sub-session is step-limited (default 30). If any task seems too large, split it into smaller subtasks BEFORE starting \u2014 oversized tasks will timeout.",
1276
1313
  args: { name: tool.schema.string(), total: tool.schema.number() },
1277
1314
  async execute(args, ctx) {
1278
- writeHarness(ctx.sessionID, { name: args.name, total: args.total, current: 0, tasks: [], usage: {}, active: true, startedAt: (/* @__PURE__ */ new Date()).toISOString() });
1315
+ writeHarness(ctx.sessionID, { name: args.name, total: args.total, current: 0, tasks: [], usage: {}, active: true, scanRequired: true, startedAt: (/* @__PURE__ */ new Date()).toISOString() });
1279
1316
  return `Harness '${args.name}' started (${args.total} tasks).
1280
1317
 
1281
- PRE-FLIGHT (unknown_scan): Before starting generate, call unknown_scan to check for blind spots. This scans the codebase against your tasks and finds gaps (unknown unknowns) that could waste steps if discovered late.
1318
+ \u26A0 DIAGNOSIS GATE \u2014 unknown_scan is REQUIRED before generate/generate_batch.
1282
1319
  unknown_scan({ prompt: "<user request>", tasks: [{id:1, title:"..."}, ...] })
1283
- Review the report: if questions are flagged, ask the user first. If task splits are suggested, adjust via task_update. THEN proceed to the loop below.
1320
+ If you skip it, generate will inject a \u26A0 warning into the sub-session prompt.
1321
+ Review the report: if QUESTIONS are flagged \u2192 ask the user first. If TASK
1322
+ REFINEMENTS are suggested \u2192 apply via task_update. Unknown unknowns found will
1323
+ be automatically injected into generate prompts as context.
1284
1324
 
1285
1325
  STEP LIMIT (default ${DEFAULT_MAX_STEPS}): each generate call creates a sub-session that is automatically aborted if it exceeds ${DEFAULT_MAX_STEPS} assistant steps. Before starting the loop, review each task: can it be completed in a focused, single-pass effort? If a task seems too broad (multiple files, multiple features, open-ended research), SPLIT it now into 2-3 smaller subtasks. A timeout wastes quota \u2014 split upfront.
1286
1326
 
@@ -1643,6 +1683,22 @@ ${priorNotes}
1643
1683
  log(`generate impl-notes read err: ${String(e)}`);
1644
1684
  }
1645
1685
  prefix += IMPL_NOTE_INSTRUCTION;
1686
+ const gate = checkScanGate(ctx.sessionID);
1687
+ if (gate.warning) {
1688
+ prefix = `${gate.warning}
1689
+
1690
+ ---
1691
+
1692
+ ` + prefix;
1693
+ }
1694
+ if (gate.summary) {
1695
+ prefix = `Pre-flight scan findings (from unknown_scan \u2014 heed these):
1696
+ ${gate.summary}
1697
+
1698
+ ---
1699
+
1700
+ ` + prefix;
1701
+ }
1646
1702
  const genTaskId = findActiveTaskId(ctx.sessionID, "generating");
1647
1703
  const maxSteps = args.max_steps ?? DEFAULT_MAX_STEPS;
1648
1704
  const out = await runModel(
@@ -1699,8 +1755,20 @@ ${priorNotes}
1699
1755
  const rules = readRules();
1700
1756
  const priorNotes = readImplNotes(5);
1701
1757
  const maxSteps = args.max_steps ?? DEFAULT_MAX_STEPS;
1758
+ const gate = checkScanGate(ctx.sessionID);
1759
+ const gatePrefix = gate.warning ? `${gate.warning}
1760
+
1761
+ ---
1762
+
1763
+ ` : gate.summary ? `Pre-flight scan findings (from unknown_scan \u2014 heed these):
1764
+ ${gate.summary}
1765
+
1766
+ ---
1767
+
1768
+ ` : "";
1702
1769
  const runOne = async (t) => {
1703
- let prefix = rules ? `Lessons learned from previous failures (apply where relevant):
1770
+ let prefix = gatePrefix;
1771
+ prefix += rules ? `Lessons learned from previous failures (apply where relevant):
1704
1772
  ${rules}
1705
1773
 
1706
1774
  ---
package/dist/tui.js CHANGED
@@ -83,8 +83,11 @@ var TLABEL = {
83
83
  completed: "done",
84
84
  failed: "fail",
85
85
  timed_out: "timeout",
86
- halted_quota: "quota-halt"
86
+ halted_quota: "quota-halt",
87
+ stale: "STALE"
87
88
  };
89
+ var STALE_MS = 5 * 6e4;
90
+ var HIDE_MS = 30 * 6e4;
88
91
  function barFill(p) {
89
92
  const n = p <= 0 ? 0 : Math.max(1, Math.min(10, Math.round(p / 10)));
90
93
  return "\u2588".repeat(n);
@@ -365,79 +368,89 @@ function initializeTui(api, disposeRoot) {
365
368
  return _el$46;
366
369
  })());
367
370
  }
368
- if (h && h.tasks.length > 0) {
369
- nodes.push((() => {
370
- var _el$48 = _$createElement("text");
371
- _$insertNode(_el$48, _$createTextNode(` `));
372
- return _el$48;
373
- })());
374
- nodes.push((() => {
375
- var _el$50 = _$createElement("text"), _el$51 = _$createTextNode(`harness: `), _el$52 = _$createTextNode(` `), _el$53 = _$createTextNode(`/`);
376
- _$insertNode(_el$50, _el$51);
377
- _$insertNode(_el$50, _el$52);
378
- _$insertNode(_el$50, _el$53);
379
- _$insert(_el$50, () => h.name, _el$52);
380
- _$insert(_el$50, () => h.current, _el$53);
381
- _$insert(_el$50, () => h.total, null);
382
- _$effect((_$p) => _$setProp(_el$50, "style", st("textMuted"), _$p));
383
- return _el$50;
384
- })());
385
- for (const t of h.tasks) {
386
- const sKey = statusKey[t.status] ?? "text";
387
- const lbl = TLABEL[t.status] ?? t.status;
388
- const rev = t.revisions > 0 && t.status === "revising" ? `(${t.revisions})` : "";
389
- const mdl = t.model ? ` ${t.model.split("/").pop() ?? t.model}` : "";
390
- const hasSub = !!t.subSessionId;
391
- const subStepStr = hasSub && t.subStep !== void 0 && t.subStep > 0 ? ` step:${t.subStep}` : "";
392
- const subEl = hasSub && t.subElapsed !== void 0 ? ` ${t.subElapsed}s` : "";
393
- const subWarn = hasSub && (t.subElapsed ?? 0) > 300;
394
- const elapsed = t.startedAt ? Math.max(0, Math.round((Date.now() - new Date(t.startedAt).getTime()) / 1e3)) : 0;
395
- const taskEl = t.status === "completed" || t.status === "failed" ? "" : elapsed > 0 ? ` ${elapsed}s` : "";
396
- const displayEl = hasSub ? subEl : taskEl;
397
- const lineKey = subWarn ? "warning" : sKey;
371
+ if (h && h.tasks.length > 0 && h.active !== false) {
372
+ const hAge = h.updatedAt ? Date.now() - new Date(h.updatedAt).getTime() : 0;
373
+ const hasActiveSub = h.tasks.some((t) => !!t.subSessionId);
374
+ const isStale = !hasActiveSub && hAge > STALE_MS;
375
+ const shouldHide = hAge > HIDE_MS && !hasActiveSub;
376
+ if (shouldHide) {
377
+ } else {
398
378
  nodes.push((() => {
399
- var _el$54 = _$createElement("text"), _el$55 = _$createTextNode(` \u25CF `), _el$56 = _$createTextNode(` `), _el$57 = _$createTextNode(` `);
400
- _$insertNode(_el$54, _el$55);
401
- _$insertNode(_el$54, _el$56);
402
- _$insertNode(_el$54, _el$57);
403
- _$insert(_el$54, () => t.id, _el$56);
404
- _$insert(_el$54, mdl, _el$56);
405
- _$insert(_el$54, lbl, _el$57);
406
- _$insert(_el$54, rev, _el$57);
407
- _$insert(_el$54, subStepStr, _el$57);
408
- _$insert(_el$54, displayEl, _el$57);
409
- _$insert(_el$54, () => t.title, null);
410
- _$effect((_$p) => _$setProp(_el$54, "style", st(lineKey), _$p));
411
- return _el$54;
379
+ var _el$48 = _$createElement("text");
380
+ _$insertNode(_el$48, _$createTextNode(` `));
381
+ return _el$48;
412
382
  })());
413
- const pv = t.model ? (t.model.split("/")[0] ?? "").split("-")[0] : "";
414
- const provCoach = pv ? s?.providers?.find((p) => p.id === pv || pv && p.id.startsWith(pv) || pv && pv.startsWith(p.id)) : s?.providers?.[0];
415
- const rawPct = provCoach?.fiveHour ?? s?.fiveHour ?? -1;
416
- const pct = rawPct < 0 ? 0 : rawPct;
417
- const pctLabel = rawPct < 0 ? "n/a" : `${rawPct}%`;
418
383
  nodes.push((() => {
419
- var _el$58 = _$createElement("box"), _el$59 = _$createElement("text"), _el$61 = _$createElement("text"), _el$62 = _$createElement("text"), _el$63 = _$createElement("text"), _el$64 = _$createTextNode(` `);
420
- _$insertNode(_el$58, _el$59);
421
- _$insertNode(_el$58, _el$61);
422
- _$insertNode(_el$58, _el$62);
423
- _$insertNode(_el$58, _el$63);
424
- _$setProp(_el$58, "flexDirection", "row");
425
- _$insertNode(_el$59, _$createTextNode(` 5h `));
426
- _$insert(_el$61, () => barFill(pct));
427
- _$insert(_el$62, () => barEmpty(pct));
428
- _$insertNode(_el$63, _el$64);
429
- _$insert(_el$63, pctLabel, null);
430
- _$effect((_p$) => {
431
- var _v$11 = st("text"), _v$12 = st("text");
432
- _v$11 !== _p$.e && (_p$.e = _$setProp(_el$61, "style", _v$11, _p$.e));
433
- _v$12 !== _p$.t && (_p$.t = _$setProp(_el$62, "style", _v$12, _p$.t));
434
- return _p$;
435
- }, {
436
- e: void 0,
437
- t: void 0
438
- });
439
- return _el$58;
384
+ var _el$50 = _$createElement("text"), _el$51 = _$createTextNode(`harness: `), _el$52 = _$createTextNode(` `), _el$53 = _$createTextNode(`/`);
385
+ _$insertNode(_el$50, _el$51);
386
+ _$insertNode(_el$50, _el$52);
387
+ _$insertNode(_el$50, _el$53);
388
+ _$insert(_el$50, () => h.name, _el$52);
389
+ _$insert(_el$50, () => h.current, _el$53);
390
+ _$insert(_el$50, () => h.total, null);
391
+ _$insert(_el$50, isStale ? " (stale)" : "", null);
392
+ _$effect((_$p) => _$setProp(_el$50, "style", st("textMuted"), _$p));
393
+ return _el$50;
440
394
  })());
395
+ for (const t of h.tasks) {
396
+ const TERMINAL = /* @__PURE__ */ new Set(["completed", "failed", "timed_out", "halted_quota"]);
397
+ const displayStatus = isStale && !TERMINAL.has(t.status) ? "stale" : t.status;
398
+ const sKey = statusKey[displayStatus] ?? "text";
399
+ const lbl = TLABEL[displayStatus] ?? displayStatus;
400
+ const rev = t.revisions > 0 && t.status === "revising" ? `(${t.revisions})` : "";
401
+ const mdl = t.model ? ` ${t.model.split("/").pop() ?? t.model}` : "";
402
+ const hasSub = !!t.subSessionId;
403
+ const subStepStr = hasSub && t.subStep !== void 0 && t.subStep > 0 ? ` step:${t.subStep}` : "";
404
+ const subEl = hasSub && t.subElapsed !== void 0 ? ` ${t.subElapsed}s` : "";
405
+ const subWarn = hasSub && (t.subElapsed ?? 0) > 300;
406
+ const elapsed = t.startedAt ? Math.max(0, Math.round((Date.now() - new Date(t.startedAt).getTime()) / 1e3)) : 0;
407
+ const taskEl = t.status === "completed" || t.status === "failed" ? "" : elapsed > 0 ? ` ${elapsed}s` : "";
408
+ const displayEl = hasSub ? subEl : taskEl;
409
+ const lineKey = subWarn ? "warning" : sKey;
410
+ nodes.push((() => {
411
+ var _el$54 = _$createElement("text"), _el$55 = _$createTextNode(` \u25CF `), _el$56 = _$createTextNode(` `), _el$57 = _$createTextNode(` `);
412
+ _$insertNode(_el$54, _el$55);
413
+ _$insertNode(_el$54, _el$56);
414
+ _$insertNode(_el$54, _el$57);
415
+ _$insert(_el$54, () => t.id, _el$56);
416
+ _$insert(_el$54, mdl, _el$56);
417
+ _$insert(_el$54, lbl, _el$57);
418
+ _$insert(_el$54, rev, _el$57);
419
+ _$insert(_el$54, subStepStr, _el$57);
420
+ _$insert(_el$54, displayEl, _el$57);
421
+ _$insert(_el$54, () => t.title, null);
422
+ _$effect((_$p) => _$setProp(_el$54, "style", st(lineKey), _$p));
423
+ return _el$54;
424
+ })());
425
+ const pv = t.model ? (t.model.split("/")[0] ?? "").split("-")[0] : "";
426
+ const provCoach = pv ? s?.providers?.find((p) => p.id === pv || pv && p.id.startsWith(pv) || pv && pv.startsWith(p.id)) : s?.providers?.[0];
427
+ const rawPct = provCoach?.fiveHour ?? s?.fiveHour ?? -1;
428
+ const pct = rawPct < 0 ? 0 : rawPct;
429
+ const pctLabel = rawPct < 0 ? "n/a" : `${rawPct}%`;
430
+ nodes.push((() => {
431
+ var _el$58 = _$createElement("box"), _el$59 = _$createElement("text"), _el$61 = _$createElement("text"), _el$62 = _$createElement("text"), _el$63 = _$createElement("text"), _el$64 = _$createTextNode(` `);
432
+ _$insertNode(_el$58, _el$59);
433
+ _$insertNode(_el$58, _el$61);
434
+ _$insertNode(_el$58, _el$62);
435
+ _$insertNode(_el$58, _el$63);
436
+ _$setProp(_el$58, "flexDirection", "row");
437
+ _$insertNode(_el$59, _$createTextNode(` 5h `));
438
+ _$insert(_el$61, () => barFill(pct));
439
+ _$insert(_el$62, () => barEmpty(pct));
440
+ _$insertNode(_el$63, _el$64);
441
+ _$insert(_el$63, pctLabel, null);
442
+ _$effect((_p$) => {
443
+ var _v$11 = st("text"), _v$12 = st("text");
444
+ _v$11 !== _p$.e && (_p$.e = _$setProp(_el$61, "style", _v$11, _p$.e));
445
+ _v$12 !== _p$.t && (_p$.t = _$setProp(_el$62, "style", _v$12, _p$.t));
446
+ return _p$;
447
+ }, {
448
+ e: void 0,
449
+ t: void 0
450
+ });
451
+ return _el$58;
452
+ })());
453
+ }
441
454
  }
442
455
  }
443
456
  return (() => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-usage-coach",
3
- "version": "0.8.1",
3
+ "version": "0.8.3",
4
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",