mira-harness 0.2.2 → 0.2.4
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 +8 -1
- package/dist/catalog.d.ts +57 -0
- package/dist/cli.js +148 -54
- package/dist/index.js +14 -0
- package/dist/mcp.js +14 -0
- package/llms.txt +3 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -101,12 +101,14 @@ a "typing…" fallback for a slow bot — replies run 5–62s) and capture, per
|
|
|
101
101
|
| `login` | One-time interactive login → prints `TG_SESSION` |
|
|
102
102
|
| `doctor` | Check `.env` / session / connectivity / @mira resolution (read-only) |
|
|
103
103
|
| `send [message...]` | One probe → full reply as JSON (message via arg or stdin). `--quiet --settle --timeout --no-log` |
|
|
104
|
-
| `loop` | Run the catalog paced; grades `expect` probes (exit 1 on failure). `--category --max --confirm --peer --gap --settle --timeout --list --catalog --no-fail --quiet` |
|
|
104
|
+
| `loop` | Run the catalog paced; grades `expect` probes (exit 1 on failure). `--category --max --confirm --peer --gap --settle --timeout --list --catalog --grep --no-fail --quiet` |
|
|
105
105
|
| `catalog` | List the catalog (no sends). `--category --catalog --json` |
|
|
106
106
|
| `watch` | Live-tail @mira's messages (observe-only). `--peer` |
|
|
107
107
|
| `report` | Distill the run log into Markdown. `--in --out --category` |
|
|
108
108
|
| `stats` | At-a-glance dashboard: totals, latency records, sparkline. `--in --category --json` |
|
|
109
109
|
| `diff` | Compare two run logs for @mira behavioral drift (exit 1 on a regression). `--json --no-fail` |
|
|
110
|
+
| `assert` | Re-grade a saved run log against a catalog's `expect`, offline (exit 1 on failure). `--in --catalog --category --json --no-fail` |
|
|
111
|
+
| `schema` | Print the JSON Schema for a custom catalog file (editor autocomplete / validation). `--out` |
|
|
110
112
|
|
|
111
113
|
Run `mira-harness --help` (or `<command> --help`) for full options.
|
|
112
114
|
|
|
@@ -160,6 +162,11 @@ Give a probe an optional `expect` block and `loop` grades it ✓/✗. The checks
|
|
|
160
162
|
Probes without `expect` stay observe-only (informational). `loop` **exits non-zero** if any
|
|
161
163
|
graded probe fails — so it drops straight into CI. Add `--no-fail` to report without failing.
|
|
162
164
|
|
|
165
|
+
`assert` re-grades a **saved** run log against the catalog's `expect` (offline, no @mira) — the
|
|
166
|
+
fast loop for *developing* assertions (capture once, then tune `expect` and re-grade instantly),
|
|
167
|
+
and a way to gate a committed run-log fixture in CI **without a Telegram session**. Run a subset
|
|
168
|
+
of probes by id with `loop --grep <regex>`; emit a catalog JSON Schema for your editor with `schema`.
|
|
169
|
+
|
|
163
170
|
### Drift detection
|
|
164
171
|
|
|
165
172
|
`diff` compares two run logs and flags how @mira's behavior **changed** (structural, not
|
package/dist/catalog.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
1
2
|
import { type Expect } from "./assert.js";
|
|
2
3
|
/** Built-in categories. Custom catalogs may use any category string. */
|
|
3
4
|
export type ProbeCategory = "core" | "skills" | "generation" | "wallet";
|
|
@@ -20,6 +21,59 @@ export interface Probe {
|
|
|
20
21
|
expect?: Expect;
|
|
21
22
|
}
|
|
22
23
|
export declare const CATEGORIES: ProbeCategory[];
|
|
24
|
+
export declare const ProbeSchema: z.ZodObject<{
|
|
25
|
+
id: z.ZodString;
|
|
26
|
+
category: z.ZodDefault<z.ZodString>;
|
|
27
|
+
hypothesis: z.ZodDefault<z.ZodString>;
|
|
28
|
+
send: z.ZodString;
|
|
29
|
+
slow: z.ZodOptional<z.ZodBoolean>;
|
|
30
|
+
confirm: z.ZodOptional<z.ZodBoolean>;
|
|
31
|
+
note: z.ZodOptional<z.ZodString>;
|
|
32
|
+
expect: z.ZodOptional<z.ZodObject<{
|
|
33
|
+
replies: z.ZodOptional<z.ZodBoolean>;
|
|
34
|
+
textMatches: z.ZodOptional<z.ZodString>;
|
|
35
|
+
minButtons: z.ZodOptional<z.ZodNumber>;
|
|
36
|
+
minLinks: z.ZodOptional<z.ZodNumber>;
|
|
37
|
+
hasWebApp: z.ZodOptional<z.ZodBoolean>;
|
|
38
|
+
media: z.ZodOptional<z.ZodEnum<{
|
|
39
|
+
photo: "photo";
|
|
40
|
+
video: "video";
|
|
41
|
+
audio: "audio";
|
|
42
|
+
document: "document";
|
|
43
|
+
webpage: "webpage";
|
|
44
|
+
other: "other";
|
|
45
|
+
}>>;
|
|
46
|
+
maxFirstReplyMs: z.ZodOptional<z.ZodNumber>;
|
|
47
|
+
json: z.ZodOptional<z.ZodBoolean>;
|
|
48
|
+
}, z.core.$strip>>;
|
|
49
|
+
}, z.core.$strip>;
|
|
50
|
+
/** A whole catalog file = an array of probes. */
|
|
51
|
+
export declare const CatalogSchema: z.ZodArray<z.ZodObject<{
|
|
52
|
+
id: z.ZodString;
|
|
53
|
+
category: z.ZodDefault<z.ZodString>;
|
|
54
|
+
hypothesis: z.ZodDefault<z.ZodString>;
|
|
55
|
+
send: z.ZodString;
|
|
56
|
+
slow: z.ZodOptional<z.ZodBoolean>;
|
|
57
|
+
confirm: z.ZodOptional<z.ZodBoolean>;
|
|
58
|
+
note: z.ZodOptional<z.ZodString>;
|
|
59
|
+
expect: z.ZodOptional<z.ZodObject<{
|
|
60
|
+
replies: z.ZodOptional<z.ZodBoolean>;
|
|
61
|
+
textMatches: z.ZodOptional<z.ZodString>;
|
|
62
|
+
minButtons: z.ZodOptional<z.ZodNumber>;
|
|
63
|
+
minLinks: z.ZodOptional<z.ZodNumber>;
|
|
64
|
+
hasWebApp: z.ZodOptional<z.ZodBoolean>;
|
|
65
|
+
media: z.ZodOptional<z.ZodEnum<{
|
|
66
|
+
photo: "photo";
|
|
67
|
+
video: "video";
|
|
68
|
+
audio: "audio";
|
|
69
|
+
document: "document";
|
|
70
|
+
webpage: "webpage";
|
|
71
|
+
other: "other";
|
|
72
|
+
}>>;
|
|
73
|
+
maxFirstReplyMs: z.ZodOptional<z.ZodNumber>;
|
|
74
|
+
json: z.ZodOptional<z.ZodBoolean>;
|
|
75
|
+
}, z.core.$strip>>;
|
|
76
|
+
}, z.core.$strip>>;
|
|
23
77
|
/**
|
|
24
78
|
* Load a custom probe catalog from a JSON file (an array of probe objects).
|
|
25
79
|
* Throws a clear error on a missing file or an entry that fails validation.
|
|
@@ -28,3 +82,6 @@ export declare function loadCatalog(file: string): Probe[];
|
|
|
28
82
|
export declare const CATALOG: Probe[];
|
|
29
83
|
/** Filter probes by category. `source` defaults to the built-in CATALOG. */
|
|
30
84
|
export declare function probesFor(category?: string, source?: Probe[]): Probe[];
|
|
85
|
+
/** Keep probes whose `id` matches the pattern (regex, case-insensitive; falls
|
|
86
|
+
* back to substring on an invalid regex). Used by `loop --grep` to run a subset. */
|
|
87
|
+
export declare function grepProbes(probes: Probe[], pattern: string): Probe[];
|
package/dist/cli.js
CHANGED
|
@@ -88,6 +88,7 @@ var ProbeSchema = z2.object({
|
|
|
88
88
|
note: z2.string().optional(),
|
|
89
89
|
expect: ExpectSchema.optional()
|
|
90
90
|
});
|
|
91
|
+
var CatalogSchema = z2.array(ProbeSchema);
|
|
91
92
|
function loadCatalog(file) {
|
|
92
93
|
const path = resolve(process.cwd(), file);
|
|
93
94
|
const raw = readFileSync(path, "utf8");
|
|
@@ -293,6 +294,33 @@ function probesFor(category, source = CATALOG) {
|
|
|
293
294
|
return source;
|
|
294
295
|
return source.filter((p) => p.category === category);
|
|
295
296
|
}
|
|
297
|
+
function grepProbes(probes, pattern) {
|
|
298
|
+
let compiled;
|
|
299
|
+
try {
|
|
300
|
+
compiled = new RegExp(pattern, "i");
|
|
301
|
+
} catch {
|
|
302
|
+
compiled = undefined;
|
|
303
|
+
}
|
|
304
|
+
const rx = compiled;
|
|
305
|
+
if (rx)
|
|
306
|
+
return probes.filter((p) => rx.test(p.id));
|
|
307
|
+
const needle = pattern.toLowerCase();
|
|
308
|
+
return probes.filter((p) => p.id.toLowerCase().includes(needle));
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
// src/commands/assert.ts
|
|
312
|
+
import { resolve as resolve4 } from "node:path";
|
|
313
|
+
|
|
314
|
+
// src/log.ts
|
|
315
|
+
import { appendFile, mkdir } from "node:fs/promises";
|
|
316
|
+
import { dirname, resolve as resolve2 } from "node:path";
|
|
317
|
+
var RUNS_FILE = resolve2(process.cwd(), process.env.MIRA_RUNS_FILE ?? "mira-runs.jsonl");
|
|
318
|
+
async function appendRun(result, meta = {}) {
|
|
319
|
+
await mkdir(dirname(RUNS_FILE), { recursive: true });
|
|
320
|
+
const record = { ...meta, ...result };
|
|
321
|
+
await appendFile(RUNS_FILE, `${JSON.stringify(record)}
|
|
322
|
+
`, "utf8");
|
|
323
|
+
}
|
|
296
324
|
|
|
297
325
|
// src/ui.ts
|
|
298
326
|
import pc from "picocolors";
|
|
@@ -401,49 +429,6 @@ function clearTitle() {
|
|
|
401
429
|
process.stderr.write("\x1B]0;\x07");
|
|
402
430
|
}
|
|
403
431
|
|
|
404
|
-
// src/commands/catalog.ts
|
|
405
|
-
function listCatalog(opts = {}) {
|
|
406
|
-
const source = opts.catalog ? loadCatalog(opts.catalog) : CATALOG;
|
|
407
|
-
if (!opts.catalog && opts.category && !CATEGORIES.includes(opts.category)) {
|
|
408
|
-
note(c.red(`unknown category "${opts.category}" (one of: ${CATEGORIES.join(", ")})`));
|
|
409
|
-
process.exit(1);
|
|
410
|
-
}
|
|
411
|
-
let probes = probesFor(opts.category, source);
|
|
412
|
-
if (opts.max !== undefined)
|
|
413
|
-
probes = probes.slice(0, opts.max);
|
|
414
|
-
if (opts.json) {
|
|
415
|
-
console.log(JSON.stringify(probes.map((p) => ({
|
|
416
|
-
id: p.id,
|
|
417
|
-
category: p.category,
|
|
418
|
-
hypothesis: p.hypothesis,
|
|
419
|
-
send: p.send,
|
|
420
|
-
slow: Boolean(p.slow),
|
|
421
|
-
confirm: Boolean(p.confirm),
|
|
422
|
-
expect: p.expect ?? null
|
|
423
|
-
})), null, 2));
|
|
424
|
-
return;
|
|
425
|
-
}
|
|
426
|
-
note(c.bold(`${probes.length} probe(s)${opts.category ? ` [${opts.category}]` : ""}`));
|
|
427
|
-
for (const p of probes) {
|
|
428
|
-
const tags = [p.slow ? c.yellow("slow") : "", p.confirm ? c.magenta("confirm") : ""].filter(Boolean).join(" ");
|
|
429
|
-
note(` ${c.cyan(p.id.padEnd(16))} ${c.dim(p.category.padEnd(11))} ${p.hypothesis} ${tags}`);
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
|
|
433
|
-
// src/commands/diff.ts
|
|
434
|
-
import { resolve as resolve4 } from "node:path";
|
|
435
|
-
|
|
436
|
-
// src/log.ts
|
|
437
|
-
import { appendFile, mkdir } from "node:fs/promises";
|
|
438
|
-
import { dirname, resolve as resolve2 } from "node:path";
|
|
439
|
-
var RUNS_FILE = resolve2(process.cwd(), process.env.MIRA_RUNS_FILE ?? "mira-runs.jsonl");
|
|
440
|
-
async function appendRun(result, meta = {}) {
|
|
441
|
-
await mkdir(dirname(RUNS_FILE), { recursive: true });
|
|
442
|
-
const record = { ...meta, ...result };
|
|
443
|
-
await appendFile(RUNS_FILE, `${JSON.stringify(record)}
|
|
444
|
-
`, "utf8");
|
|
445
|
-
}
|
|
446
|
-
|
|
447
432
|
// src/commands/report.ts
|
|
448
433
|
import { readFileSync as readFileSync2, writeFileSync } from "node:fs";
|
|
449
434
|
import { resolve as resolve3 } from "node:path";
|
|
@@ -568,7 +553,89 @@ function report(opts) {
|
|
|
568
553
|
}
|
|
569
554
|
}
|
|
570
555
|
|
|
556
|
+
// src/commands/assert.ts
|
|
557
|
+
function assertLog(opts = {}) {
|
|
558
|
+
let records;
|
|
559
|
+
try {
|
|
560
|
+
records = loadRunRecords(opts.in, opts.category);
|
|
561
|
+
} catch (e) {
|
|
562
|
+
if (e?.code === "ENOENT") {
|
|
563
|
+
const path = opts.in ? resolve4(process.cwd(), opts.in) : RUNS_FILE;
|
|
564
|
+
console.error(`no run log at ${path} — run \`mira-harness send\` or \`mira-harness loop\` first.`);
|
|
565
|
+
} else {
|
|
566
|
+
console.error(e instanceof Error ? e.message : String(e));
|
|
567
|
+
}
|
|
568
|
+
process.exit(1);
|
|
569
|
+
}
|
|
570
|
+
const source = opts.catalog ? loadCatalog(opts.catalog) : CATALOG;
|
|
571
|
+
const byId = new Map(source.map((p) => [p.id, p]));
|
|
572
|
+
const graded = [];
|
|
573
|
+
for (const r of records) {
|
|
574
|
+
if (!r.probeId)
|
|
575
|
+
continue;
|
|
576
|
+
const p = byId.get(r.probeId);
|
|
577
|
+
if (!p?.expect)
|
|
578
|
+
continue;
|
|
579
|
+
graded.push({ id: r.probeId, verdict: evaluate(p.expect, r) });
|
|
580
|
+
}
|
|
581
|
+
const failed = graded.filter((g) => !g.verdict.ok);
|
|
582
|
+
if (opts.json) {
|
|
583
|
+
console.log(JSON.stringify({ graded: graded.length, passed: graded.length - failed.length, results: graded }, null, 2));
|
|
584
|
+
if (failed.length && !opts.noFail)
|
|
585
|
+
process.exit(1);
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
note(c.bold(`assert ${opts.catalog ?? "(built-in catalog)"} × ${opts.in ?? "(run log)"}`));
|
|
589
|
+
if (!graded.length) {
|
|
590
|
+
note(c.dim(" no records matched a probe with `expect` — nothing to grade."));
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
for (const g of graded) {
|
|
594
|
+
note(` ${g.verdict.ok ? c.green("✓") : c.red("✗")} ${c.cyan(g.id)}`);
|
|
595
|
+
if (!g.verdict.ok) {
|
|
596
|
+
for (const ch of g.verdict.checks.filter((x) => !x.ok))
|
|
597
|
+
note(c.dim(` ✗ ${ch.name}: ${ch.detail}`));
|
|
598
|
+
}
|
|
599
|
+
}
|
|
600
|
+
note((failed.length ? c.red : c.green)(`
|
|
601
|
+
${graded.length - failed.length}/${graded.length} assertions passed.`));
|
|
602
|
+
if (failed.length && !opts.noFail)
|
|
603
|
+
process.exit(1);
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
// src/commands/catalog.ts
|
|
607
|
+
function listCatalog(opts = {}) {
|
|
608
|
+
const source = opts.catalog ? loadCatalog(opts.catalog) : CATALOG;
|
|
609
|
+
if (!opts.catalog && opts.category && !CATEGORIES.includes(opts.category)) {
|
|
610
|
+
note(c.red(`unknown category "${opts.category}" (one of: ${CATEGORIES.join(", ")})`));
|
|
611
|
+
process.exit(1);
|
|
612
|
+
}
|
|
613
|
+
let probes = probesFor(opts.category, source);
|
|
614
|
+
if (opts.grep)
|
|
615
|
+
probes = grepProbes(probes, opts.grep);
|
|
616
|
+
if (opts.max !== undefined)
|
|
617
|
+
probes = probes.slice(0, opts.max);
|
|
618
|
+
if (opts.json) {
|
|
619
|
+
console.log(JSON.stringify(probes.map((p) => ({
|
|
620
|
+
id: p.id,
|
|
621
|
+
category: p.category,
|
|
622
|
+
hypothesis: p.hypothesis,
|
|
623
|
+
send: p.send,
|
|
624
|
+
slow: Boolean(p.slow),
|
|
625
|
+
confirm: Boolean(p.confirm),
|
|
626
|
+
expect: p.expect ?? null
|
|
627
|
+
})), null, 2));
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
630
|
+
note(c.bold(`${probes.length} probe(s)${opts.category ? ` [${opts.category}]` : ""}`));
|
|
631
|
+
for (const p of probes) {
|
|
632
|
+
const tags = [p.slow ? c.yellow("slow") : "", p.confirm ? c.magenta("confirm") : ""].filter(Boolean).join(" ");
|
|
633
|
+
note(` ${c.cyan(p.id.padEnd(16))} ${c.dim(p.category.padEnd(11))} ${p.hypothesis} ${tags}`);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
|
|
571
637
|
// src/commands/diff.ts
|
|
638
|
+
import { resolve as resolve5 } from "node:path";
|
|
572
639
|
function lastByProbe(records) {
|
|
573
640
|
const m = new Map;
|
|
574
641
|
for (const r of records) {
|
|
@@ -640,7 +707,7 @@ function load(path, label) {
|
|
|
640
707
|
try {
|
|
641
708
|
return loadRunRecords(path);
|
|
642
709
|
} catch (e) {
|
|
643
|
-
const shown = path ?
|
|
710
|
+
const shown = path ? resolve5(process.cwd(), path) : RUNS_FILE;
|
|
644
711
|
if (e?.code === "ENOENT")
|
|
645
712
|
console.error(`${label}: no run log at ${shown}`);
|
|
646
713
|
else
|
|
@@ -859,7 +926,7 @@ function buildCollector(peer, client, t, sent, opts) {
|
|
|
859
926
|
const collected = new Map;
|
|
860
927
|
const start = Date.now();
|
|
861
928
|
let firstReplyMs = null;
|
|
862
|
-
return new Promise((
|
|
929
|
+
return new Promise((resolve6) => {
|
|
863
930
|
let settleTimer;
|
|
864
931
|
let firstTimer;
|
|
865
932
|
let hardTimer;
|
|
@@ -889,7 +956,7 @@ function buildCollector(peer, client, t, sent, opts) {
|
|
|
889
956
|
const finish = () => {
|
|
890
957
|
cleanup();
|
|
891
958
|
const messages = [...collected.values()].sort((a, b) => a.msg.id - b.msg.id).map(({ msg, editCount }) => extractMessage(msg, editCount));
|
|
892
|
-
|
|
959
|
+
resolve6({
|
|
893
960
|
peer,
|
|
894
961
|
sent,
|
|
895
962
|
messages,
|
|
@@ -1106,7 +1173,7 @@ async function loop(opts) {
|
|
|
1106
1173
|
process.exit(1);
|
|
1107
1174
|
}
|
|
1108
1175
|
if (opts.list) {
|
|
1109
|
-
listCatalog({ category: opts.category, max: opts.max, catalog: opts.catalog });
|
|
1176
|
+
listCatalog({ category: opts.category, max: opts.max, catalog: opts.catalog, grep: opts.grep });
|
|
1110
1177
|
return;
|
|
1111
1178
|
}
|
|
1112
1179
|
const source = opts.catalog ? loadCatalog(opts.catalog) : CATALOG;
|
|
@@ -1125,9 +1192,10 @@ async function loop(opts) {
|
|
|
1125
1192
|
console.error("TG_SESSION is empty — run `mira-harness login` first, then put it in .env.");
|
|
1126
1193
|
process.exit(1);
|
|
1127
1194
|
}
|
|
1128
|
-
const
|
|
1195
|
+
const matched = opts.grep ? grepProbes(probesFor(opts.category, source), opts.grep) : probesFor(opts.category, source);
|
|
1196
|
+
const probes = matched.slice(0, opts.max);
|
|
1129
1197
|
if (!probes.length) {
|
|
1130
|
-
console.error("no probes selected.");
|
|
1198
|
+
console.error(opts.grep ? `no probes match --grep "${opts.grep}".` : "no probes selected.");
|
|
1131
1199
|
process.exit(1);
|
|
1132
1200
|
}
|
|
1133
1201
|
const gap = opts.gap ?? DEFAULT_GAP_MS;
|
|
@@ -1196,6 +1264,22 @@ async function loop(opts) {
|
|
|
1196
1264
|
}
|
|
1197
1265
|
}
|
|
1198
1266
|
|
|
1267
|
+
// src/commands/schema.ts
|
|
1268
|
+
import { writeFileSync as writeFileSync2 } from "node:fs";
|
|
1269
|
+
import { resolve as resolve6 } from "node:path";
|
|
1270
|
+
import { z as z3 } from "zod";
|
|
1271
|
+
function schema(opts = {}) {
|
|
1272
|
+
const json = JSON.stringify(z3.toJSONSchema(CatalogSchema), null, 2);
|
|
1273
|
+
if (opts.out) {
|
|
1274
|
+
const dest = resolve6(process.cwd(), opts.out);
|
|
1275
|
+
writeFileSync2(dest, `${json}
|
|
1276
|
+
`, "utf8");
|
|
1277
|
+
console.error(`wrote catalog JSON Schema to ${opts.out}`);
|
|
1278
|
+
} else {
|
|
1279
|
+
console.log(json);
|
|
1280
|
+
}
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1199
1283
|
// src/commands/send.ts
|
|
1200
1284
|
import { existsSync as existsSync2 } from "node:fs";
|
|
1201
1285
|
var STOP_FILE2 = "STOP_MIRA";
|
|
@@ -1243,7 +1327,7 @@ async function send(rawMessage, opts = {}) {
|
|
|
1243
1327
|
}
|
|
1244
1328
|
|
|
1245
1329
|
// src/commands/stats.ts
|
|
1246
|
-
import { resolve as
|
|
1330
|
+
import { resolve as resolve7 } from "node:path";
|
|
1247
1331
|
var SPARK = "▁▂▃▄▅▆▇█";
|
|
1248
1332
|
function sparkline(values) {
|
|
1249
1333
|
if (!values.length)
|
|
@@ -1282,7 +1366,7 @@ function stats(opts = {}) {
|
|
|
1282
1366
|
records = loadRunRecords(opts.in, opts.category);
|
|
1283
1367
|
} catch (e) {
|
|
1284
1368
|
if (e?.code === "ENOENT") {
|
|
1285
|
-
const path = opts.in ?
|
|
1369
|
+
const path = opts.in ? resolve7(process.cwd(), opts.in) : RUNS_FILE;
|
|
1286
1370
|
console.error(`no run log at ${path} — run \`mira-harness send\` or \`mira-harness loop\` first.`);
|
|
1287
1371
|
} else {
|
|
1288
1372
|
console.error(e instanceof Error ? e.message : String(e));
|
|
@@ -1373,11 +1457,11 @@ async function watch(opts = {}) {
|
|
|
1373
1457
|
const client = await connect(session);
|
|
1374
1458
|
const unsubscribe = await subscribe(client, peer, (m, kind) => note(line(peer, m, kind)));
|
|
1375
1459
|
note(c.bold(`watching @${peer} — Ctrl-C to stop`));
|
|
1376
|
-
await new Promise((
|
|
1460
|
+
await new Promise((resolve8) => {
|
|
1377
1461
|
process.once("SIGINT", () => {
|
|
1378
1462
|
note(c.dim(`
|
|
1379
1463
|
stopping…`));
|
|
1380
|
-
|
|
1464
|
+
resolve8();
|
|
1381
1465
|
});
|
|
1382
1466
|
});
|
|
1383
1467
|
unsubscribe();
|
|
@@ -1433,7 +1517,7 @@ program.command("send").description("Send one probe to @mira and print the full
|
|
|
1433
1517
|
noLog: opts.log === false
|
|
1434
1518
|
});
|
|
1435
1519
|
});
|
|
1436
|
-
program.command("loop").description("Run the experiment catalog against @mira at a human pace").option("-c, --category <category>", `only this category (${CATEGORIES.join(" | ")})`).option("-m, --max <n>", "max probes this run", "6").option("--confirm", "press a safe ✅ Confirm on generation probes (spends credits)", false).option("--peer <peer>", "'experiment'|'group' for TG_EXPERIMENT_CHAT, or a literal allowlisted peer").option("--gap <ms>", "delay between sends").option("--settle <ms>", "quiet window before concluding a reply").option("--timeout <ms>", "give up if no reply by this many ms").option("--list", "list the probes that would run, then exit (no sends)", false).option("--catalog <file>", "custom catalog JSON file (instead of the built-in catalog)").option("--no-fail", "report failed assertions but still exit 0 (default: exit 1 on failure)").option("-q, --quiet", "suppress progress spinners", false).action(async (opts) => {
|
|
1520
|
+
program.command("loop").description("Run the experiment catalog against @mira at a human pace").option("-c, --category <category>", `only this category (${CATEGORIES.join(" | ")})`).option("-m, --max <n>", "max probes this run", "6").option("--confirm", "press a safe ✅ Confirm on generation probes (spends credits)", false).option("--peer <peer>", "'experiment'|'group' for TG_EXPERIMENT_CHAT, or a literal allowlisted peer").option("--gap <ms>", "delay between sends").option("--settle <ms>", "quiet window before concluding a reply").option("--timeout <ms>", "give up if no reply by this many ms").option("--list", "list the probes that would run, then exit (no sends)", false).option("--catalog <file>", "custom catalog JSON file (instead of the built-in catalog)").option("--grep <pattern>", "run only probes whose id matches this regex (case-insensitive)").option("--no-fail", "report failed assertions but still exit 0 (default: exit 1 on failure)").option("-q, --quiet", "suppress progress spinners", false).action(async (opts) => {
|
|
1437
1521
|
await loop({
|
|
1438
1522
|
category: opts.category,
|
|
1439
1523
|
max: posInt("max", opts.max),
|
|
@@ -1444,6 +1528,7 @@ program.command("loop").description("Run the experiment catalog against @mira at
|
|
|
1444
1528
|
timeout: ms("timeout", opts.timeout),
|
|
1445
1529
|
list: opts.list,
|
|
1446
1530
|
catalog: opts.catalog,
|
|
1531
|
+
grep: opts.grep,
|
|
1447
1532
|
noFail: opts.fail === false,
|
|
1448
1533
|
quiet: opts.quiet
|
|
1449
1534
|
});
|
|
@@ -1461,6 +1546,12 @@ program.command("stats").description("At-a-glance run-log dashboard: totals, lat
|
|
|
1461
1546
|
program.command("diff").description("Compare two run logs for @mira behavioral drift (exit 1 on a regression)").argument("<baseline>", "baseline run log (JSONL)").argument("[current]", "current run log (JSONL); defaults to the run log").option("--json", "output the drift as JSON", false).option("--no-fail", "report regressions but still exit 0").action((baseline, current, opts) => {
|
|
1462
1547
|
diff({ baseline, current, json: opts.json, noFail: !opts.fail });
|
|
1463
1548
|
});
|
|
1549
|
+
program.command("assert").description("Re-grade a saved run log against a catalog's `expect` (offline; exit 1 on failure)").option("--in <file>", "input JSONL (default: the run log)").option("--catalog <file>", "catalog with expectations (default: the built-in catalog)").option("-c, --category <category>", "only include probes from this category").option("--json", "output the results as JSON", false).option("--no-fail", "report failures but still exit 0").action((opts) => {
|
|
1550
|
+
assertLog({ in: opts.in, catalog: opts.catalog, category: opts.category, json: opts.json, noFail: !opts.fail });
|
|
1551
|
+
});
|
|
1552
|
+
program.command("schema").description("Print the JSON Schema for a custom catalog file (editor autocomplete / validation)").option("--out <file>", "write to a file instead of stdout").action((opts) => {
|
|
1553
|
+
schema({ out: opts.out });
|
|
1554
|
+
});
|
|
1464
1555
|
program.addHelpText("after", `
|
|
1465
1556
|
Examples:
|
|
1466
1557
|
$ mira-harness login
|
|
@@ -1476,6 +1567,9 @@ Examples:
|
|
|
1476
1567
|
$ mira-harness report --category core --out report.md
|
|
1477
1568
|
$ mira-harness stats
|
|
1478
1569
|
$ mira-harness diff baseline.jsonl
|
|
1570
|
+
$ mira-harness loop --grep core-json
|
|
1571
|
+
$ mira-harness assert
|
|
1572
|
+
$ mira-harness schema > catalog.schema.json
|
|
1479
1573
|
`);
|
|
1480
1574
|
if (process.argv.length <= 2) {
|
|
1481
1575
|
banner(version);
|
package/dist/index.js
CHANGED
|
@@ -174,6 +174,7 @@ var ProbeSchema = z2.object({
|
|
|
174
174
|
note: z2.string().optional(),
|
|
175
175
|
expect: ExpectSchema.optional()
|
|
176
176
|
});
|
|
177
|
+
var CatalogSchema = z2.array(ProbeSchema);
|
|
177
178
|
function loadCatalog(file) {
|
|
178
179
|
const path = resolve(process.cwd(), file);
|
|
179
180
|
const raw = readFileSync(path, "utf8");
|
|
@@ -379,6 +380,19 @@ function probesFor(category, source = CATALOG) {
|
|
|
379
380
|
return source;
|
|
380
381
|
return source.filter((p) => p.category === category);
|
|
381
382
|
}
|
|
383
|
+
function grepProbes(probes, pattern) {
|
|
384
|
+
let compiled;
|
|
385
|
+
try {
|
|
386
|
+
compiled = new RegExp(pattern, "i");
|
|
387
|
+
} catch {
|
|
388
|
+
compiled = undefined;
|
|
389
|
+
}
|
|
390
|
+
const rx = compiled;
|
|
391
|
+
if (rx)
|
|
392
|
+
return probes.filter((p) => rx.test(p.id));
|
|
393
|
+
const needle = pattern.toLowerCase();
|
|
394
|
+
return probes.filter((p) => p.id.toLowerCase().includes(needle));
|
|
395
|
+
}
|
|
382
396
|
// src/client.ts
|
|
383
397
|
import { Api, TelegramClient } from "telegram";
|
|
384
398
|
import { EditedMessage } from "telegram/events/EditedMessage.js";
|
package/dist/mcp.js
CHANGED
|
@@ -91,6 +91,7 @@ var ProbeSchema = z2.object({
|
|
|
91
91
|
note: z2.string().optional(),
|
|
92
92
|
expect: ExpectSchema.optional()
|
|
93
93
|
});
|
|
94
|
+
var CatalogSchema = z2.array(ProbeSchema);
|
|
94
95
|
function loadCatalog(file) {
|
|
95
96
|
const path = resolve(process.cwd(), file);
|
|
96
97
|
const raw = readFileSync(path, "utf8");
|
|
@@ -296,6 +297,19 @@ function probesFor(category, source = CATALOG) {
|
|
|
296
297
|
return source;
|
|
297
298
|
return source.filter((p) => p.category === category);
|
|
298
299
|
}
|
|
300
|
+
function grepProbes(probes, pattern) {
|
|
301
|
+
let compiled;
|
|
302
|
+
try {
|
|
303
|
+
compiled = new RegExp(pattern, "i");
|
|
304
|
+
} catch {
|
|
305
|
+
compiled = undefined;
|
|
306
|
+
}
|
|
307
|
+
const rx = compiled;
|
|
308
|
+
if (rx)
|
|
309
|
+
return probes.filter((p) => rx.test(p.id));
|
|
310
|
+
const needle = pattern.toLowerCase();
|
|
311
|
+
return probes.filter((p) => p.id.toLowerCase().includes(needle));
|
|
312
|
+
}
|
|
299
313
|
|
|
300
314
|
// src/client.ts
|
|
301
315
|
import { Api, TelegramClient } from "telegram";
|
package/llms.txt
CHANGED
|
@@ -19,12 +19,14 @@ so the only programmatic path is a userbot (a real user account over MTProto / G
|
|
|
19
19
|
## CLI commands
|
|
20
20
|
- `doctor` — env / session / connectivity / @mira resolution check (read-only). Run this first.
|
|
21
21
|
- `send [message...]` — one probe → full reply as JSON. Message via arg or stdin. Flags: `--settle <ms> --timeout <ms> --quiet --no-log`.
|
|
22
|
-
- `loop` — run the catalog, paced. Flags: `--category <core|skills|generation|wallet> --max <n> --peer <experiment> --gap <ms> --settle <ms> --timeout <ms> --list --catalog <file.json> --no-fail --quiet`. Observe-only unless `--confirm`. Grades probes that declare `expect` (PASS/FAIL) and **exits non-zero** if any graded probe fails (`--no-fail` to report without failing) — drops straight into CI.
|
|
22
|
+
- `loop` — run the catalog, paced. Flags: `--category <core|skills|generation|wallet> --max <n> --peer <experiment> --gap <ms> --settle <ms> --timeout <ms> --list --catalog <file.json> --grep <regex> --no-fail --quiet`. Observe-only unless `--confirm`. Grades probes that declare `expect` (PASS/FAIL) and **exits non-zero** if any graded probe fails (`--no-fail` to report without failing) — drops straight into CI. `--grep <regex>` runs only probes whose id matches.
|
|
23
23
|
- `catalog` — list the catalog, no network. Flags: `--category --catalog <file.json> --json`.
|
|
24
24
|
- `watch` — live-tail @mira's messages, observe-only. Flag: `--peer`.
|
|
25
25
|
- `report` — distill the JSONL run log into Markdown. Flags: `--in <file> --out <file> --category`.
|
|
26
26
|
- `stats` — at-a-glance run-log dashboard: totals, first-reply latency records (fastest / median / p95 / slowest), an ASCII sparkline, and a per-category breakdown. Flags: `--in <file> --category --json`.
|
|
27
27
|
- `diff <baseline> [current]` — compare two run logs for @mira behavioral drift (matches probes by id; structural, not exact-text). Regressions (assertion ✓→✗, new timeout, >2x latency) **exit non-zero**; surface changes / improvements are reported. `current` defaults to the run log. Flags: `--json --no-fail`.
|
|
28
|
+
- `assert` — re-grade a SAVED run log against a catalog's `expect`, offline (no network). The fast loop for developing assertions, and CI-able **without a Telegram session** (grade a committed run-log fixture). **Exits non-zero** on failure. Flags: `--in <file> --catalog <file.json> --category --json --no-fail`.
|
|
29
|
+
- `schema` — print the JSON Schema for a custom catalog file (an array of probes), derived from the loader's zod schema. Wire into an editor for autocomplete/validation. Flag: `--out <file>`.
|
|
28
30
|
- Help: `mira-harness --help` or `<command> --help`.
|
|
29
31
|
|
|
30
32
|
## MCP server (bin: mira-harness-mcp, entry dist/mcp.js)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mira-harness",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.4",
|
|
4
4
|
"description": "A CLI + MCP dev-tool for communicating with the @mira Telegram bot — capture its full reply (buttons/links/media) and run a self-driving experiment catalog.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|