opencode-usage-coach 0.11.4 → 0.12.1
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 +237 -60
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -1,20 +1,28 @@
|
|
|
1
1
|
// src/index.ts
|
|
2
|
-
import { mkdirSync as
|
|
2
|
+
import { mkdirSync as mkdirSync3, writeFileSync as writeFileSync2, appendFileSync as appendFileSync3, readFileSync as readFileSync2, existsSync as existsSync2 } from "fs";
|
|
3
3
|
import { spawn, spawnSync } from "child_process";
|
|
4
4
|
import { createHash } from "crypto";
|
|
5
|
-
import { homedir } from "os";
|
|
6
|
-
import { join as
|
|
5
|
+
import { homedir as homedir2 } from "os";
|
|
6
|
+
import { join as join3, resolve, dirname as dirname2 } from "path";
|
|
7
7
|
import { tool } from "@opencode-ai/plugin";
|
|
8
8
|
|
|
9
9
|
// src/domain.ts
|
|
10
10
|
import { mkdirSync, appendFileSync, readFileSync, existsSync, writeFileSync } from "fs";
|
|
11
|
-
import { join } from "path";
|
|
11
|
+
import { join, dirname, basename } from "path";
|
|
12
12
|
var BASE_DIR = "";
|
|
13
|
+
var SHARED_DIR = "";
|
|
13
14
|
function initDomain(stateDir) {
|
|
14
15
|
BASE_DIR = stateDir;
|
|
16
|
+
if (basename(dirname(stateDir)) === "projects") {
|
|
17
|
+
SHARED_DIR = join(dirname(dirname(stateDir)), "shared");
|
|
18
|
+
} else {
|
|
19
|
+
SHARED_DIR = join(stateDir, "_shared");
|
|
20
|
+
}
|
|
15
21
|
}
|
|
16
22
|
var nodesFile = () => join(BASE_DIR, "nodes.ndjson");
|
|
17
23
|
var edgesFile = () => join(BASE_DIR, "edges.ndjson");
|
|
24
|
+
var sharedNodesFile = () => join(SHARED_DIR, "nodes.ndjson");
|
|
25
|
+
var sharedEdgesFile = () => join(SHARED_DIR, "edges.ndjson");
|
|
18
26
|
function readNdjson(path) {
|
|
19
27
|
try {
|
|
20
28
|
if (!existsSync(path)) return [];
|
|
@@ -24,23 +32,14 @@ function readNdjson(path) {
|
|
|
24
32
|
}
|
|
25
33
|
}
|
|
26
34
|
function readNodes() {
|
|
27
|
-
return readNdjson(nodesFile());
|
|
35
|
+
return [...readNdjson(nodesFile()), ...readNdjson(sharedNodesFile())];
|
|
28
36
|
}
|
|
29
37
|
function readEdges() {
|
|
30
|
-
return readNdjson(edgesFile());
|
|
38
|
+
return [...readNdjson(edgesFile()), ...readNdjson(sharedEdgesFile())];
|
|
31
39
|
}
|
|
32
40
|
function uid(prefix) {
|
|
33
41
|
return `${prefix}_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
|
|
34
42
|
}
|
|
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
43
|
function addDomainEdge(edge) {
|
|
45
44
|
const full = { ...edge, ts: (/* @__PURE__ */ new Date()).toISOString() };
|
|
46
45
|
try {
|
|
@@ -49,6 +48,9 @@ function addDomainEdge(edge) {
|
|
|
49
48
|
} catch {
|
|
50
49
|
}
|
|
51
50
|
}
|
|
51
|
+
function readProjectNodes() {
|
|
52
|
+
return readNdjson(nodesFile());
|
|
53
|
+
}
|
|
52
54
|
function writeNodes(nodes) {
|
|
53
55
|
try {
|
|
54
56
|
mkdirSync(BASE_DIR, { recursive: true });
|
|
@@ -72,7 +74,7 @@ function queryDomain(keywords) {
|
|
|
72
74
|
function touchNodes(ids) {
|
|
73
75
|
if (ids.size === 0) return;
|
|
74
76
|
try {
|
|
75
|
-
const nodes =
|
|
77
|
+
const nodes = readProjectNodes();
|
|
76
78
|
let changed = false;
|
|
77
79
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
78
80
|
for (const n of nodes) {
|
|
@@ -88,7 +90,7 @@ function touchNodes(ids) {
|
|
|
88
90
|
}
|
|
89
91
|
function evictStale(maxAgeDays = 30, maxNodes = 1e3) {
|
|
90
92
|
try {
|
|
91
|
-
const nodes =
|
|
93
|
+
const nodes = readProjectNodes();
|
|
92
94
|
if (nodes.length === 0) return { removed: 0, kept: 0 };
|
|
93
95
|
const now = Date.now();
|
|
94
96
|
const ageMs = maxAgeDays * 864e5;
|
|
@@ -167,19 +169,70 @@ function queryDomainGraph(keywords, maxDepth = 2, opts = {}) {
|
|
|
167
169
|
}
|
|
168
170
|
function saveInvestigationResult(keywords, result, source, confidence = 0.7) {
|
|
169
171
|
try {
|
|
170
|
-
|
|
172
|
+
const nodeId = uid("node");
|
|
173
|
+
const full = {
|
|
174
|
+
id: nodeId,
|
|
171
175
|
type: "fact",
|
|
172
176
|
name: keywords.join(" "),
|
|
173
|
-
props: { result },
|
|
177
|
+
props: { result, keywords: [...new Set(keywords)] },
|
|
174
178
|
source: source || "investigation",
|
|
175
|
-
confidence
|
|
176
|
-
|
|
179
|
+
confidence,
|
|
180
|
+
ts: (/* @__PURE__ */ new Date()).toISOString()
|
|
181
|
+
};
|
|
182
|
+
try {
|
|
183
|
+
mkdirSync(SHARED_DIR, { recursive: true });
|
|
184
|
+
appendFileSync(sharedNodesFile(), JSON.stringify(full) + "\n");
|
|
185
|
+
} catch {
|
|
186
|
+
}
|
|
187
|
+
autoLinkKeywords(nodeId, keywords);
|
|
188
|
+
return nodeId;
|
|
177
189
|
} catch {
|
|
178
190
|
return "";
|
|
179
191
|
}
|
|
180
192
|
}
|
|
193
|
+
function autoLinkKeywords(nodeId, keywords, minOverlap = 2, maxLinks = 8) {
|
|
194
|
+
if (keywords.length < minOverlap) return;
|
|
195
|
+
try {
|
|
196
|
+
const candidates = queryDomain(keywords);
|
|
197
|
+
let linked = 0;
|
|
198
|
+
for (const node of candidates.nodes) {
|
|
199
|
+
if (node.id === nodeId) continue;
|
|
200
|
+
const nodeKw = Array.isArray(node.props?.keywords) ? node.props.keywords : (node.name || "").toLowerCase().split(/[^a-z0-9_-]+/).filter((w) => w.length >= 3);
|
|
201
|
+
const overlap = keywords.filter((k) => nodeKw.includes(k));
|
|
202
|
+
if (overlap.length >= minOverlap) {
|
|
203
|
+
const edge = {
|
|
204
|
+
from: nodeId,
|
|
205
|
+
to: node.id,
|
|
206
|
+
rel: "related-to",
|
|
207
|
+
note: `auto: ${overlap.length} shared (${overlap.slice(0, 5).join(",")})`,
|
|
208
|
+
ts: (/* @__PURE__ */ new Date()).toISOString()
|
|
209
|
+
};
|
|
210
|
+
try {
|
|
211
|
+
mkdirSync(SHARED_DIR, { recursive: true });
|
|
212
|
+
appendFileSync(sharedEdgesFile(), JSON.stringify(edge) + "\n");
|
|
213
|
+
} catch {
|
|
214
|
+
}
|
|
215
|
+
linked++;
|
|
216
|
+
if (linked >= maxLinks) break;
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
} catch {
|
|
220
|
+
}
|
|
221
|
+
}
|
|
181
222
|
|
|
182
223
|
// src/web-search.ts
|
|
224
|
+
import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync2 } from "fs";
|
|
225
|
+
import { join as join2 } from "path";
|
|
226
|
+
import { homedir } from "os";
|
|
227
|
+
var WS_LOG_FILE = process.env.UC_STATE_DIR ? join2(process.env.UC_STATE_DIR, "web-search.log") : join2(homedir(), ".cache", "opencode-usage-coach", "web-search.log");
|
|
228
|
+
function wsLog(msg) {
|
|
229
|
+
try {
|
|
230
|
+
mkdirSync2(join2(WS_LOG_FILE, ".."), { recursive: true });
|
|
231
|
+
appendFileSync2(WS_LOG_FILE, `${(/* @__PURE__ */ new Date()).toISOString()} ${msg}
|
|
232
|
+
`);
|
|
233
|
+
} catch {
|
|
234
|
+
}
|
|
235
|
+
}
|
|
183
236
|
var FRAMEWORK_DOCS = {
|
|
184
237
|
"react": { name: "React", docs: "https://react.dev", githubOrg: "facebook" },
|
|
185
238
|
"solid-js": { name: "Solid.js", docs: "https://solidjs.com", githubOrg: "solidjs" },
|
|
@@ -242,7 +295,7 @@ async function ghFetch(url, signal) {
|
|
|
242
295
|
const errs = ghError?.errors;
|
|
243
296
|
const errMsg = ghError?.message ?? ghErrorText.slice(0, 300);
|
|
244
297
|
const detail = errs ? errs.map((e) => typeof e === "string" ? e : `${e?.field ?? "?"}: ${e?.message ?? e?.code ?? JSON.stringify(e)}`).join("; ") : "";
|
|
245
|
-
|
|
298
|
+
wsLog(JSON.stringify({
|
|
246
299
|
level: "error",
|
|
247
300
|
module: "web-search",
|
|
248
301
|
event: "gh-fetch-error",
|
|
@@ -278,7 +331,9 @@ async function tier1OfficialDocs(query, fws, results, docRefs, seen, signal) {
|
|
|
278
331
|
const entry = FRAMEWORK_DOCS[fw];
|
|
279
332
|
if (!entry?.githubOrg) continue;
|
|
280
333
|
try {
|
|
281
|
-
const
|
|
334
|
+
const orgPart = ` org:${entry.githubOrg}`;
|
|
335
|
+
const truncated = cleanQuery.slice(0, GH_QUERY_MAX - orgPart.length);
|
|
336
|
+
const fullQ = `${truncated}${orgPart}`;
|
|
282
337
|
const url = `https://api.github.com/search/issues?q=${encodeURIComponent(fullQ)}&per_page=3`;
|
|
283
338
|
const data = await ghFetch(url, signal);
|
|
284
339
|
for (const item of data.items ?? []) {
|
|
@@ -291,7 +346,7 @@ async function tier1OfficialDocs(query, fws, results, docRefs, seen, signal) {
|
|
|
291
346
|
}
|
|
292
347
|
} catch (e) {
|
|
293
348
|
const m = errMessage(e);
|
|
294
|
-
|
|
349
|
+
wsLog(`[web-search] tier1 ${fw}: ${m}`);
|
|
295
350
|
errors.push(`tier1:${fw}:${m.slice(0, 80)}`);
|
|
296
351
|
}
|
|
297
352
|
}
|
|
@@ -314,7 +369,7 @@ async function tier2GitHubIssues(query, results, seen, signal) {
|
|
|
314
369
|
}
|
|
315
370
|
} catch (e) {
|
|
316
371
|
const m = errMessage(e);
|
|
317
|
-
|
|
372
|
+
wsLog(`[web-search] tier2: ${m}`);
|
|
318
373
|
errors.push(`tier2:${m.slice(0, 80)}`);
|
|
319
374
|
}
|
|
320
375
|
return errors;
|
|
@@ -338,7 +393,7 @@ async function tier3GitHubCode(query, results, seen, signal) {
|
|
|
338
393
|
}
|
|
339
394
|
} catch (e) {
|
|
340
395
|
const m = errMessage(e);
|
|
341
|
-
|
|
396
|
+
wsLog(`[web-search] tier3: ${m}`);
|
|
342
397
|
errors.push(`tier3:${m.slice(0, 80)}`);
|
|
343
398
|
}
|
|
344
399
|
return errors;
|
|
@@ -364,7 +419,7 @@ async function searchContext(query, frameworks, keyDeps, timeoutMs) {
|
|
|
364
419
|
}
|
|
365
420
|
} catch (e) {
|
|
366
421
|
const m = errMessage(e);
|
|
367
|
-
|
|
422
|
+
wsLog(`[web-search] unexpected error: ${m}`);
|
|
368
423
|
errors.push(`unexpected:${m.slice(0, 80)}`);
|
|
369
424
|
} finally {
|
|
370
425
|
clearTimeout(timer);
|
|
@@ -382,49 +437,49 @@ var DEFAULT_MAX_STEPS = Number(process.env.UC_MAX_STEPS ?? 30) || 30;
|
|
|
382
437
|
var WATCHDOG_POLL_MS = Math.max(1e3, Number(process.env.UC_WATCHDOG_POLL_MS ?? 3e3) || 3e3);
|
|
383
438
|
var WALL_TIMEOUT_MS = Math.max(1, Number(process.env.UC_WALL_TIMEOUT_MIN ?? 30) || 30) * 60 * 1e3;
|
|
384
439
|
var DEFAULT_MAX_QUESTIONS = Math.max(1, Math.round(Number(process.env.UC_MAX_QUESTIONS ?? 7)) || 7);
|
|
385
|
-
var PIPE_LOG =
|
|
440
|
+
var PIPE_LOG = join3(homedir2(), ".cache", "opencode-usage-coach", "pipeline.log");
|
|
386
441
|
function pipeLog(msg) {
|
|
387
442
|
try {
|
|
388
|
-
|
|
389
|
-
|
|
443
|
+
mkdirSync3(dirname2(PIPE_LOG), { recursive: true });
|
|
444
|
+
appendFileSync3(PIPE_LOG, `[SERVER] ${(/* @__PURE__ */ new Date()).toISOString()} ${msg}
|
|
390
445
|
`);
|
|
391
446
|
} catch {
|
|
392
447
|
}
|
|
393
448
|
}
|
|
394
449
|
pipeLog(`MODULE LOADED | node=${process.version} | pid=${process.pid}`);
|
|
395
|
-
var STATE_DIR =
|
|
396
|
-
var STATE_FILE =
|
|
397
|
-
var LOG_FILE =
|
|
450
|
+
var STATE_DIR = join3(homedir2(), ".cache", "opencode-usage-coach");
|
|
451
|
+
var STATE_FILE = join3(STATE_DIR, "state.json");
|
|
452
|
+
var LOG_FILE = join3(STATE_DIR, "coach.log");
|
|
398
453
|
function projectStateDir(dir) {
|
|
399
454
|
const abs = resolve(dir || ".");
|
|
400
455
|
const h = createHash("sha1").update(abs).digest("hex").slice(0, 12);
|
|
401
|
-
return
|
|
456
|
+
return join3(homedir2(), ".cache", "opencode-usage-coach", "projects", h);
|
|
402
457
|
}
|
|
403
458
|
function setStateDir(dir) {
|
|
404
459
|
STATE_DIR = process.env.UC_STATE_DIR ?? projectStateDir(dir);
|
|
405
|
-
STATE_FILE =
|
|
406
|
-
LOG_FILE =
|
|
460
|
+
STATE_FILE = join3(STATE_DIR, "state.json");
|
|
461
|
+
LOG_FILE = join3(STATE_DIR, "coach.log");
|
|
407
462
|
}
|
|
408
463
|
var NOOP_HOOKS = {};
|
|
409
464
|
function log(msg) {
|
|
410
465
|
try {
|
|
411
|
-
|
|
466
|
+
appendFileSync3(LOG_FILE, `${(/* @__PURE__ */ new Date()).toISOString()} ${msg}
|
|
412
467
|
`);
|
|
413
468
|
} catch {
|
|
414
469
|
}
|
|
415
470
|
}
|
|
416
471
|
function writeState(c) {
|
|
417
472
|
try {
|
|
418
|
-
|
|
473
|
+
mkdirSync3(STATE_DIR, { recursive: true });
|
|
419
474
|
writeFileSync2(STATE_FILE, JSON.stringify({ ...c, updatedAt: (/* @__PURE__ */ new Date()).toISOString() }));
|
|
420
475
|
} catch {
|
|
421
476
|
}
|
|
422
477
|
}
|
|
423
478
|
function rulesFile() {
|
|
424
|
-
return
|
|
479
|
+
return join3(STATE_DIR, "rules.md");
|
|
425
480
|
}
|
|
426
481
|
function failuresFile() {
|
|
427
|
-
return
|
|
482
|
+
return join3(STATE_DIR, "failures.ndjson");
|
|
428
483
|
}
|
|
429
484
|
function readRules() {
|
|
430
485
|
try {
|
|
@@ -436,7 +491,7 @@ function readRules() {
|
|
|
436
491
|
}
|
|
437
492
|
}
|
|
438
493
|
function implNotesFile() {
|
|
439
|
-
return
|
|
494
|
+
return join3(STATE_DIR, "impl-notes.md");
|
|
440
495
|
}
|
|
441
496
|
var IMPL_NOTE_INSTRUCTION = `
|
|
442
497
|
## Implementation Notes (important!)
|
|
@@ -483,8 +538,8 @@ ${notes}
|
|
|
483
538
|
Source: generate task "${shortTask}"
|
|
484
539
|
|
|
485
540
|
`;
|
|
486
|
-
|
|
487
|
-
|
|
541
|
+
mkdirSync3(STATE_DIR, { recursive: true });
|
|
542
|
+
appendFileSync3(implNotesFile(), entry);
|
|
488
543
|
} catch (e) {
|
|
489
544
|
log(`appendImplNotes err: ${String(e)}`);
|
|
490
545
|
}
|
|
@@ -537,15 +592,137 @@ function readImplNotesByGraph(keywords, limit = 5) {
|
|
|
537
592
|
}
|
|
538
593
|
function extractKeywords(text) {
|
|
539
594
|
try {
|
|
540
|
-
const STOP = /* @__PURE__ */ new Set([
|
|
595
|
+
const STOP = /* @__PURE__ */ new Set([
|
|
596
|
+
// articles / prepositions / conjunctions
|
|
597
|
+
"the",
|
|
598
|
+
"and",
|
|
599
|
+
"for",
|
|
600
|
+
"with",
|
|
601
|
+
"that",
|
|
602
|
+
"this",
|
|
603
|
+
"from",
|
|
604
|
+
"into",
|
|
605
|
+
"your",
|
|
606
|
+
"you",
|
|
607
|
+
"are",
|
|
608
|
+
"was",
|
|
609
|
+
"but",
|
|
610
|
+
"not",
|
|
611
|
+
"all",
|
|
612
|
+
"any",
|
|
613
|
+
"use",
|
|
614
|
+
"task",
|
|
615
|
+
"prompt",
|
|
616
|
+
// generic verbs
|
|
617
|
+
"apply",
|
|
618
|
+
"add",
|
|
619
|
+
"make",
|
|
620
|
+
"set",
|
|
621
|
+
"get",
|
|
622
|
+
"run",
|
|
623
|
+
"try",
|
|
624
|
+
"put",
|
|
625
|
+
"let",
|
|
626
|
+
"new",
|
|
627
|
+
"will",
|
|
628
|
+
"can",
|
|
629
|
+
"has",
|
|
630
|
+
"had",
|
|
631
|
+
"have",
|
|
632
|
+
"been",
|
|
633
|
+
"were",
|
|
634
|
+
"they",
|
|
635
|
+
"them",
|
|
636
|
+
"when",
|
|
637
|
+
"then",
|
|
638
|
+
"than",
|
|
639
|
+
"also",
|
|
640
|
+
"just",
|
|
641
|
+
"like",
|
|
642
|
+
"what",
|
|
643
|
+
"which",
|
|
644
|
+
"how",
|
|
645
|
+
"should",
|
|
646
|
+
"would",
|
|
647
|
+
"could",
|
|
648
|
+
"must",
|
|
649
|
+
"does",
|
|
650
|
+
"doing",
|
|
651
|
+
"done",
|
|
652
|
+
"via",
|
|
653
|
+
// generic nouns / adjectives
|
|
654
|
+
"some",
|
|
655
|
+
"more",
|
|
656
|
+
"most",
|
|
657
|
+
"such",
|
|
658
|
+
"each",
|
|
659
|
+
"other",
|
|
660
|
+
"very",
|
|
661
|
+
"much",
|
|
662
|
+
"here",
|
|
663
|
+
"there",
|
|
664
|
+
"where",
|
|
665
|
+
"while",
|
|
666
|
+
"about",
|
|
667
|
+
"after",
|
|
668
|
+
"before",
|
|
669
|
+
"main",
|
|
670
|
+
"call",
|
|
671
|
+
"file",
|
|
672
|
+
"code",
|
|
673
|
+
"data",
|
|
674
|
+
"line",
|
|
675
|
+
"name",
|
|
676
|
+
"type",
|
|
677
|
+
"true",
|
|
678
|
+
"false",
|
|
679
|
+
"null",
|
|
680
|
+
"void",
|
|
681
|
+
"return",
|
|
682
|
+
"function",
|
|
683
|
+
"const",
|
|
684
|
+
"note",
|
|
685
|
+
"notes",
|
|
686
|
+
"result",
|
|
687
|
+
"output",
|
|
688
|
+
"input",
|
|
689
|
+
"detail",
|
|
690
|
+
"reason",
|
|
691
|
+
"reasons",
|
|
692
|
+
"improve",
|
|
693
|
+
"fix",
|
|
694
|
+
"fixed",
|
|
695
|
+
"error",
|
|
696
|
+
"issue",
|
|
697
|
+
"thing",
|
|
698
|
+
"things",
|
|
699
|
+
"stuff",
|
|
700
|
+
"case",
|
|
701
|
+
"cases",
|
|
702
|
+
"way",
|
|
703
|
+
"ways",
|
|
704
|
+
"first",
|
|
705
|
+
"second",
|
|
706
|
+
"third",
|
|
707
|
+
"last",
|
|
708
|
+
"next",
|
|
709
|
+
"prev",
|
|
710
|
+
"previous",
|
|
711
|
+
"following",
|
|
712
|
+
"above",
|
|
713
|
+
"below",
|
|
714
|
+
"since",
|
|
715
|
+
"until",
|
|
716
|
+
"without"
|
|
717
|
+
]);
|
|
541
718
|
const seen = /* @__PURE__ */ new Set();
|
|
542
719
|
const out = [];
|
|
543
|
-
for (const raw of (text ?? "").toLowerCase().split(/[^a-z0-9_]+/)) {
|
|
720
|
+
for (const raw of (text ?? "").toLowerCase().split(/[^a-z0-9_-]+/)) {
|
|
544
721
|
const t = raw.trim();
|
|
545
722
|
if (t.length < 3 || STOP.has(t) || seen.has(t)) continue;
|
|
546
723
|
seen.add(t);
|
|
547
724
|
out.push(t);
|
|
548
|
-
if (out.length >=
|
|
725
|
+
if (out.length >= 8) break;
|
|
549
726
|
}
|
|
550
727
|
return out;
|
|
551
728
|
} catch {
|
|
@@ -553,7 +730,7 @@ function extractKeywords(text) {
|
|
|
553
730
|
}
|
|
554
731
|
}
|
|
555
732
|
function harnessFile(sessionID) {
|
|
556
|
-
return
|
|
733
|
+
return join3(STATE_DIR, sessionID || "_default", "harness.json");
|
|
557
734
|
}
|
|
558
735
|
function readHarness(sessionID) {
|
|
559
736
|
try {
|
|
@@ -567,7 +744,7 @@ function readHarness(sessionID) {
|
|
|
567
744
|
function writeHarness(sessionID, h) {
|
|
568
745
|
try {
|
|
569
746
|
const f = harnessFile(sessionID);
|
|
570
|
-
|
|
747
|
+
mkdirSync3(dirname2(f), { recursive: true });
|
|
571
748
|
h.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
572
749
|
writeFileSync2(f, JSON.stringify(h, null, 2));
|
|
573
750
|
} catch {
|
|
@@ -590,7 +767,7 @@ function mutateHarness(sessionID, fn) {
|
|
|
590
767
|
return next;
|
|
591
768
|
}
|
|
592
769
|
function interviewFile(sessionID) {
|
|
593
|
-
return
|
|
770
|
+
return join3(STATE_DIR, sessionID || "_default", "interview.json");
|
|
594
771
|
}
|
|
595
772
|
function readInterview(sessionID) {
|
|
596
773
|
try {
|
|
@@ -604,7 +781,7 @@ function readInterview(sessionID) {
|
|
|
604
781
|
function writeInterview(sessionID, s) {
|
|
605
782
|
try {
|
|
606
783
|
const f = interviewFile(sessionID);
|
|
607
|
-
|
|
784
|
+
mkdirSync3(dirname2(f), { recursive: true });
|
|
608
785
|
s.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
609
786
|
writeFileSync2(f, JSON.stringify(s, null, 2));
|
|
610
787
|
} catch {
|
|
@@ -802,7 +979,7 @@ function parseFileList(rawList, baseDir) {
|
|
|
802
979
|
let testPattern;
|
|
803
980
|
let testFramework;
|
|
804
981
|
for (const mf of manifestFiles) {
|
|
805
|
-
const full =
|
|
982
|
+
const full = join3(baseDir, mf);
|
|
806
983
|
if (existsSync2(full)) {
|
|
807
984
|
try {
|
|
808
985
|
const content = JSON.parse(readFileSync2(full, "utf8"));
|
|
@@ -1132,15 +1309,15 @@ function readHarnessCfg(dir) {
|
|
|
1132
1309
|
return {};
|
|
1133
1310
|
};
|
|
1134
1311
|
return {
|
|
1135
|
-
...tryRead(
|
|
1136
|
-
...tryRead(
|
|
1312
|
+
...tryRead(join3(homedir2(), ".config", "opencode-usage-coach", "harness.config.json")),
|
|
1313
|
+
...tryRead(join3(dir, "harness.config.json"))
|
|
1137
1314
|
};
|
|
1138
1315
|
}
|
|
1139
1316
|
function writeHarnessCfg(updates) {
|
|
1140
|
-
const configDir =
|
|
1141
|
-
const configPath =
|
|
1317
|
+
const configDir = join3(homedir2(), ".config", "opencode-usage-coach");
|
|
1318
|
+
const configPath = join3(configDir, "harness.config.json");
|
|
1142
1319
|
try {
|
|
1143
|
-
|
|
1320
|
+
mkdirSync3(configDir, { recursive: true });
|
|
1144
1321
|
const existing = (() => {
|
|
1145
1322
|
try {
|
|
1146
1323
|
return JSON.parse(readFileSync2(configPath, "utf8"));
|
|
@@ -1868,8 +2045,8 @@ Then: harness_done(). Follow the [usage-coach NEXT] directive each tool returns.
|
|
|
1868
2045
|
async execute(args, _ctx) {
|
|
1869
2046
|
const rec = { ts: (/* @__PURE__ */ new Date()).toISOString(), task: args.task, prompt: args.prompt, gradeResult: args.gradeResult, model: args.model, revisions: args.revisions };
|
|
1870
2047
|
try {
|
|
1871
|
-
|
|
1872
|
-
|
|
2048
|
+
mkdirSync3(STATE_DIR, { recursive: true });
|
|
2049
|
+
appendFileSync3(failuresFile(), JSON.stringify(rec) + "\n");
|
|
1873
2050
|
} catch (e) {
|
|
1874
2051
|
log(`record_failure err: ${String(e)}`);
|
|
1875
2052
|
}
|
|
@@ -1994,8 +2171,8 @@ Keep it concrete and actionable.`;
|
|
|
1994
2171
|
const rule = out;
|
|
1995
2172
|
try {
|
|
1996
2173
|
const date = (/* @__PURE__ */ new Date()).toISOString().slice(0, 10);
|
|
1997
|
-
|
|
1998
|
-
|
|
2174
|
+
mkdirSync3(STATE_DIR, { recursive: true });
|
|
2175
|
+
appendFileSync3(rulesFile(), `## Rule (${date})
|
|
1999
2176
|
${rule}
|
|
2000
2177
|
Origin: ${args.task}
|
|
2001
2178
|
|
package/package.json
CHANGED