tickmarkr 1.30.0
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 +347 -0
- package/dist/adapters/claude-code.d.ts +4 -0
- package/dist/adapters/claude-code.js +191 -0
- package/dist/adapters/codex.d.ts +8 -0
- package/dist/adapters/codex.js +257 -0
- package/dist/adapters/cursor-agent.d.ts +4 -0
- package/dist/adapters/cursor-agent.js +62 -0
- package/dist/adapters/fake.d.ts +45 -0
- package/dist/adapters/fake.js +115 -0
- package/dist/adapters/grok.d.ts +4 -0
- package/dist/adapters/grok.js +122 -0
- package/dist/adapters/model-lints.d.ts +6 -0
- package/dist/adapters/model-lints.js +130 -0
- package/dist/adapters/opencode.d.ts +3 -0
- package/dist/adapters/opencode.js +108 -0
- package/dist/adapters/pi.d.ts +3 -0
- package/dist/adapters/pi.js +191 -0
- package/dist/adapters/prompt.d.ts +6 -0
- package/dist/adapters/prompt.js +84 -0
- package/dist/adapters/registry.d.ts +31 -0
- package/dist/adapters/registry.js +165 -0
- package/dist/adapters/types.d.ts +98 -0
- package/dist/adapters/types.js +56 -0
- package/dist/cli/commands/approve.d.ts +1 -0
- package/dist/cli/commands/approve.js +72 -0
- package/dist/cli/commands/compile.d.ts +1 -0
- package/dist/cli/commands/compile.js +23 -0
- package/dist/cli/commands/doctor.d.ts +2 -0
- package/dist/cli/commands/doctor.js +103 -0
- package/dist/cli/commands/init.d.ts +1 -0
- package/dist/cli/commands/init.js +32 -0
- package/dist/cli/commands/plan.d.ts +2 -0
- package/dist/cli/commands/plan.js +101 -0
- package/dist/cli/commands/profile.d.ts +1 -0
- package/dist/cli/commands/profile.js +56 -0
- package/dist/cli/commands/report.d.ts +4 -0
- package/dist/cli/commands/report.js +306 -0
- package/dist/cli/commands/resume.d.ts +1 -0
- package/dist/cli/commands/resume.js +11 -0
- package/dist/cli/commands/run.d.ts +1 -0
- package/dist/cli/commands/run.js +35 -0
- package/dist/cli/commands/scope.d.ts +3 -0
- package/dist/cli/commands/scope.js +22 -0
- package/dist/cli/commands/status.d.ts +9 -0
- package/dist/cli/commands/status.js +162 -0
- package/dist/cli/commands/unlock.d.ts +1 -0
- package/dist/cli/commands/unlock.js +11 -0
- package/dist/cli/index.d.ts +8 -0
- package/dist/cli/index.js +65 -0
- package/dist/compile/collateral.d.ts +6 -0
- package/dist/compile/collateral.js +128 -0
- package/dist/compile/common.d.ts +12 -0
- package/dist/compile/common.js +56 -0
- package/dist/compile/gsd.d.ts +3 -0
- package/dist/compile/gsd.js +222 -0
- package/dist/compile/index.d.ts +4 -0
- package/dist/compile/index.js +33 -0
- package/dist/compile/native.d.ts +4 -0
- package/dist/compile/native.js +196 -0
- package/dist/compile/prd.d.ts +2 -0
- package/dist/compile/prd.js +64 -0
- package/dist/compile/speckit.d.ts +2 -0
- package/dist/compile/speckit.js +66 -0
- package/dist/config/config.d.ts +184 -0
- package/dist/config/config.js +329 -0
- package/dist/drivers/herdr.d.ts +44 -0
- package/dist/drivers/herdr.js +394 -0
- package/dist/drivers/index.d.ts +3 -0
- package/dist/drivers/index.js +13 -0
- package/dist/drivers/subprocess.d.ts +25 -0
- package/dist/drivers/subprocess.js +106 -0
- package/dist/drivers/types.d.ts +61 -0
- package/dist/drivers/types.js +77 -0
- package/dist/gates/acceptance.d.ts +19 -0
- package/dist/gates/acceptance.js +92 -0
- package/dist/gates/baseline.d.ts +12 -0
- package/dist/gates/baseline.js +97 -0
- package/dist/gates/evidence.d.ts +4 -0
- package/dist/gates/evidence.js +12 -0
- package/dist/gates/llm.d.ts +25 -0
- package/dist/gates/llm.js +101 -0
- package/dist/gates/review.d.ts +12 -0
- package/dist/gates/review.js +80 -0
- package/dist/gates/run-gates.d.ts +34 -0
- package/dist/gates/run-gates.js +105 -0
- package/dist/gates/scope.d.ts +8 -0
- package/dist/gates/scope.js +32 -0
- package/dist/gates/types.d.ts +6 -0
- package/dist/gates/types.js +1 -0
- package/dist/graph/graph.d.ts +20 -0
- package/dist/graph/graph.js +148 -0
- package/dist/graph/schema.d.ts +185 -0
- package/dist/graph/schema.js +141 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/plan/prompt.d.ts +4 -0
- package/dist/plan/prompt.js +31 -0
- package/dist/plan/scope.d.ts +17 -0
- package/dist/plan/scope.js +141 -0
- package/dist/report/cost.d.ts +24 -0
- package/dist/report/cost.js +89 -0
- package/dist/route/preference.d.ts +27 -0
- package/dist/route/preference.js +49 -0
- package/dist/route/profile.d.ts +65 -0
- package/dist/route/profile.js +197 -0
- package/dist/route/router.d.ts +26 -0
- package/dist/route/router.js +200 -0
- package/dist/run/consult.d.ts +23 -0
- package/dist/run/consult.js +80 -0
- package/dist/run/daemon.d.ts +23 -0
- package/dist/run/daemon.js +728 -0
- package/dist/run/git.d.ts +12 -0
- package/dist/run/git.js +56 -0
- package/dist/run/journal.d.ts +91 -0
- package/dist/run/journal.js +287 -0
- package/dist/run/lock.d.ts +29 -0
- package/dist/run/lock.js +182 -0
- package/dist/run/merge.d.ts +12 -0
- package/dist/run/merge.js +44 -0
- package/dist/run/reconcile.d.ts +4 -0
- package/dist/run/reconcile.js +95 -0
- package/package.json +39 -0
- package/schema/rungraph.schema.json +306 -0
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { allAdapters, discoverChannels, doctorAgeMs, modelAuthExclusions, modelAuthLine, probeAll, readDoctor, servableExclusions, servabilityLine } from "../../adapters/registry.js";
|
|
2
|
+
import { modelLints } from "../../adapters/model-lints.js";
|
|
3
|
+
import { collateralLints } from "../../compile/collateral.js";
|
|
4
|
+
import { loadConfig } from "../../config/config.js";
|
|
5
|
+
import { loadGraph } from "../../graph/graph.js";
|
|
6
|
+
import { excludedChannels, exclusionLine } from "../../route/preference.js";
|
|
7
|
+
import { route, RoutingError } from "../../route/router.js";
|
|
8
|
+
import { loadRoutingProfile } from "../../run/journal.js";
|
|
9
|
+
export async function plan(_argv, cwd = process.cwd(), adapters = allAdapters()) {
|
|
10
|
+
// ponytail: hardcoded 24h TTL — promote to config when an operator asks. mtime is the signal because
|
|
11
|
+
// doctor.json has no probe timestamp and a schema field would break the existing-files compat invariant.
|
|
12
|
+
const DOCTOR_STALE_MS = 24 * 60 * 60 * 1000;
|
|
13
|
+
const cfg = loadConfig(cwd);
|
|
14
|
+
const g = loadGraph(cwd);
|
|
15
|
+
// readDoctor cache path: staleness line only fires here (probeAll fallback is fresh by construction).
|
|
16
|
+
const cached = readDoctor(cwd);
|
|
17
|
+
const health = cached ?? (await probeAll(adapters));
|
|
18
|
+
const channels = discoverChannels(cfg, adapters, health);
|
|
19
|
+
// VIS-04 trust ramp (VALIDATION 13-01-11): preview:true bypasses the routing.learned:off short-circuit so
|
|
20
|
+
// the static-vs-learned column renders even when the daemon's learned routing is off. Cold ⇒ undefined ⇒
|
|
21
|
+
// output byte-identical to today.
|
|
22
|
+
const profile = loadRoutingProfile(cwd, cfg, { preview: true });
|
|
23
|
+
let deviations = 0;
|
|
24
|
+
const lines = [`drovr plan — dry run (${channels.length} channels available)`, ""];
|
|
25
|
+
const excluded = excludedChannels(cfg, adapters, health);
|
|
26
|
+
if (excluded.length)
|
|
27
|
+
lines.push(exclusionLine(excluded), "");
|
|
28
|
+
const servable = servableExclusions(cfg, adapters, health);
|
|
29
|
+
if (servable.length)
|
|
30
|
+
lines.push(servabilityLine(servable), "");
|
|
31
|
+
// T2 (2026-07-13): per-model unauthed verdicts from doctor — one lint per exclusion (reason + date).
|
|
32
|
+
const modelUnauthed = modelAuthExclusions(cfg, adapters, health);
|
|
33
|
+
if (modelUnauthed.length)
|
|
34
|
+
lines.push(modelAuthLine(modelUnauthed), "");
|
|
35
|
+
const unauthed = adapters.filter((a) => health[a.id]?.installed && !health[a.id]?.authed).map((a) => a.id);
|
|
36
|
+
if (unauthed.length)
|
|
37
|
+
lines.push(`installed but unauthed: ${unauthed.join(", ")} — channels excluded from routing`, "");
|
|
38
|
+
if (cached) {
|
|
39
|
+
const age = doctorAgeMs(cwd);
|
|
40
|
+
if (age !== null && age > DOCTOR_STALE_MS) {
|
|
41
|
+
lines.push(`doctor.json is ${Math.floor(age / 3_600_000)}h old — run 'drovr doctor' to refresh (servability/auth may have changed)`, "");
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
// HYG-07(a)/T2 pin honesty: append the drop reason when a pin/miss message names a filtered channel —
|
|
45
|
+
// servable (not served) or model-auth (unauthed). Floor-failure errors name only surviving channels, so
|
|
46
|
+
// these never fire there; the standalone exclusion lines above carry that attribution.
|
|
47
|
+
const exclusionReason = (msg) => {
|
|
48
|
+
const s = servable.find((x) => msg.includes(x.key));
|
|
49
|
+
if (s)
|
|
50
|
+
return ` — ${s.key} is unservable (not in ${s.adapter}'s served model list)`;
|
|
51
|
+
const m = modelUnauthed.find((x) => msg.includes(x.key));
|
|
52
|
+
if (m)
|
|
53
|
+
return ` — ${m.key} is unauthed (${m.reason}, probed ${m.probedAt.split("T")[0]})`;
|
|
54
|
+
return "";
|
|
55
|
+
};
|
|
56
|
+
const lints = [];
|
|
57
|
+
for (const [role, sel] of [["judge", cfg.judge], ["consult", cfg.consult]]) {
|
|
58
|
+
if (!health[sel.adapter]?.installed)
|
|
59
|
+
lints.push(`${role}: ${sel.adapter}:${sel.model} not installed — that gate/consult will fail closed`);
|
|
60
|
+
}
|
|
61
|
+
lints.push(...modelLints(cfg, health, adapters)); // health may be pre-v1.5/probeAll-fallback — no-detection branch covers both
|
|
62
|
+
let cost = 0;
|
|
63
|
+
for (const t of g.tasks) {
|
|
64
|
+
try {
|
|
65
|
+
const r = route(t, cfg, channels, profile);
|
|
66
|
+
for (const l of r.lints) {
|
|
67
|
+
const suf = exclusionReason(l);
|
|
68
|
+
lints.push(suf ? `${l}${suf}` : l);
|
|
69
|
+
}
|
|
70
|
+
const est = r.assignment.channel === "sub" ? 0 : cfg.pricing[r.assignment.tier];
|
|
71
|
+
cost += est;
|
|
72
|
+
lines.push(` ${t.id.padEnd(6)} ${t.shape.padEnd(10)} c${String(t.complexity).padEnd(3)}→ ${r.assignment.adapter}:${r.assignment.model} [${r.assignment.channel}/${r.assignment.tier}]${t.humanGate ? " (human gate)" : ""}${est ? ` ~$${est.toFixed(2)}` : ""} — ${r.provenance}`);
|
|
73
|
+
if (r.deviation) {
|
|
74
|
+
deviations++;
|
|
75
|
+
const d = r.deviation;
|
|
76
|
+
lines.push(` ⇄ static would pick ${d.static} — learned picked ${d.chosen} (score ${d.score.toFixed(3)} n=${d.n} vs ${d.staticScore.toFixed(3)})`);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
catch (e) {
|
|
80
|
+
if (!(e instanceof RoutingError))
|
|
81
|
+
throw e;
|
|
82
|
+
const msg = `${e.message}${exclusionReason(e.message)}`;
|
|
83
|
+
lines.push(` ${t.id.padEnd(6)} ${t.shape.padEnd(10)} !! ${msg}`);
|
|
84
|
+
lints.push(`${t.id}: unroutable — ${msg}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
lines.push("", `est. cost (API channels only, rough): ~$${cost.toFixed(2)} + judge/review/consult calls`);
|
|
88
|
+
// VIS-04: summary only when a profile is active AND something deviates. Labeled by the switch — off = preview.
|
|
89
|
+
if (profile && deviations) {
|
|
90
|
+
lines.push(cfg.routing.learned === "off"
|
|
91
|
+
? `learned routing (preview — currently OFF): ${deviations}/${g.tasks.length} tasks would deviate from static (set routing.learned: on to adopt)`
|
|
92
|
+
: `learned routing: ${deviations}/${g.tasks.length} tasks deviate from static (routing.learned: off to disable)`);
|
|
93
|
+
}
|
|
94
|
+
if (lints.length)
|
|
95
|
+
lines.push("", "routing lints:", ...lints.map((l) => ` ! ${l}`));
|
|
96
|
+
// OBS-12/13/14/21: advisory only — never blocks --route-strict (run refuses on routing lints only)
|
|
97
|
+
const scopeLints = collateralLints(g.tasks, cwd);
|
|
98
|
+
if (scopeLints.length)
|
|
99
|
+
lines.push("", "scope lints:", ...scopeLints.map((l) => ` ! ${l}`));
|
|
100
|
+
return lines.join("\n");
|
|
101
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function profile(argv: string[], cwd?: string): Promise<string>;
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import { writeFileSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { loadConfig } from "../../config/config.js";
|
|
4
|
+
import { drovrDir } from "../../graph/graph.js";
|
|
5
|
+
import { learnedScore, MIN_SAMPLES, cellsOf } from "../../route/profile.js";
|
|
6
|
+
import { Journal, loadRoutingProfile, readProfileCursor, RUNS_WINDOW } from "../../run/journal.js";
|
|
7
|
+
// drovr profile — inspect (show) and forget (reset) the derived learned-routing profile (VIS-03).
|
|
8
|
+
// Read-only class like plan/status/report: no run lock. `reset` writes ONE cursor scalar, nothing else.
|
|
9
|
+
export async function profile(argv, cwd = process.cwd()) {
|
|
10
|
+
return argv[0] === "reset" ? reset(cwd) : show(cwd);
|
|
11
|
+
}
|
|
12
|
+
// Non-destructive reset (T-13-06): a one-line runId cutoff at .drovr/profile-since. NEVER deletes,
|
|
13
|
+
// truncates, or rotates any telemetry — the profile is derived (PROF-01), so there is nothing to delete;
|
|
14
|
+
// the cursor only bounds loadRoutingProfile's telemetry window. drovrDir guarantees .drovr exists and
|
|
15
|
+
// blanket-gitignores it, so the cursor is never git-addable. Opaque string: used only in a runId > compare.
|
|
16
|
+
function reset(cwd) {
|
|
17
|
+
const cursor = Journal.latestRunId(cwd) ?? ""; // empty repo ⇒ empty cursor ⇒ readProfileCursor === undefined
|
|
18
|
+
const path = join(drovrDir(cwd), "profile-since");
|
|
19
|
+
writeFileSync(path, cursor + "\n");
|
|
20
|
+
return [
|
|
21
|
+
cursor
|
|
22
|
+
? `profile reset — learned routing now forgets runs at or before ${cursor}.`
|
|
23
|
+
: `profile reset — no runs yet; wrote an empty cursor.`,
|
|
24
|
+
` wrote .drovr/profile-since (telemetry is UNTOUCHED — report/resume still see every run).`,
|
|
25
|
+
` to un-reset: delete .drovr/profile-since.`,
|
|
26
|
+
].join("\n");
|
|
27
|
+
}
|
|
28
|
+
// Inspection surface: preview:true bypasses the routing.learned:off short-circuit so `show` renders the
|
|
29
|
+
// profile even under the default off — same trust ramp as `drovr plan`. The routing path stays inert.
|
|
30
|
+
function show(cwd) {
|
|
31
|
+
const cfg = loadConfig(cwd);
|
|
32
|
+
const cursor = readProfileCursor(cwd);
|
|
33
|
+
const p = loadRoutingProfile(cwd, cfg, { preview: true });
|
|
34
|
+
const header = [
|
|
35
|
+
`drovr profile`,
|
|
36
|
+
` routing.learned: ${cfg.routing.learned}${cfg.routing.learned === "off" ? " (preview — routing is inert)" : ""}`,
|
|
37
|
+
` runs window: ${RUNS_WINDOW}`,
|
|
38
|
+
...(cursor ? [` reset cursor: ${cursor}`] : []),
|
|
39
|
+
];
|
|
40
|
+
if (!p || p.cells.size === 0) {
|
|
41
|
+
// preview bypasses the switch ⇒ undefined here means data, not policy: no telemetry, or all behind the cursor.
|
|
42
|
+
header.push("", cursor
|
|
43
|
+
? ` empty profile — no telemetry after the reset cursor (delete .drovr/profile-since to see earlier runs).`
|
|
44
|
+
: ` empty profile — no telemetry yet.`);
|
|
45
|
+
return header.join("\n");
|
|
46
|
+
}
|
|
47
|
+
const rows = [...cellsOf(p)].map(({ shape, chKey, channel, cell }) => {
|
|
48
|
+
const quality = cell.n > 0 ? (cell.qSum / cell.n).toFixed(2) : "-";
|
|
49
|
+
const median = cell.doneMedianMs === undefined ? "-" : `${Math.round(cell.doneMedianMs)}ms`;
|
|
50
|
+
// VIS-04/ROUTE-15 — preview must match routing's tuning, never the bare module default.
|
|
51
|
+
const score = learnedScore(p, shape, chKey, channel, { availWeight: cfg.routing.learnedTuning?.availWeight }).toFixed(3);
|
|
52
|
+
const cold = cell.n < MIN_SAMPLES ? ` cold (n<${MIN_SAMPLES})` : "";
|
|
53
|
+
return ` ${shape.padEnd(10)} ${chKey.padEnd(28)} ${channel.padEnd(4)} n=${String(cell.n).padEnd(3)} q=${quality.padEnd(5)} ${median.padEnd(9)} disp=${String(cell.dispatches).padEnd(3)} score=${score}${cold}`;
|
|
54
|
+
});
|
|
55
|
+
return [...header, "", " shape channel class obs quality median dispatch score", ...rows].join("\n");
|
|
56
|
+
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { type ChannelCost } from "../../report/cost.js";
|
|
2
|
+
import { type JournalEvent, type TelemetryRow } from "../../run/journal.js";
|
|
3
|
+
export declare function renderMarkdownRecord(runId: string, events: JournalEvent[], prices?: ChannelCost[], rows?: TelemetryRow[]): string;
|
|
4
|
+
export declare function report(argv: string[], cwd?: string): Promise<string>;
|
|
@@ -0,0 +1,306 @@
|
|
|
1
|
+
import { parseArgs } from "node:util";
|
|
2
|
+
import { addUsage } from "../../adapters/types.js";
|
|
3
|
+
import { loadConfig } from "../../config/config.js";
|
|
4
|
+
import { estimateCosts } from "../../report/cost.js";
|
|
5
|
+
import { cellsOf, cellSummary } from "../../route/profile.js";
|
|
6
|
+
import { Journal, loadRoutingProfile } from "../../run/journal.js";
|
|
7
|
+
const n = (x) => x.toLocaleString("en-US"); // explicit locale — CI/darwin flake guard
|
|
8
|
+
const EM = "—";
|
|
9
|
+
// TokenUsage fields that are actually present — filtered, never coalesced to zero (absent ⇒ unmetered).
|
|
10
|
+
const fields = (u) => {
|
|
11
|
+
const parts = [`in ${n(u.input)}`, `out ${n(u.output)}`];
|
|
12
|
+
if (u.cacheRead !== undefined && u.cacheWrite !== undefined)
|
|
13
|
+
parts.push(`cache r/w ${n(u.cacheRead)}/${n(u.cacheWrite)}`);
|
|
14
|
+
if (u.reasoning !== undefined)
|
|
15
|
+
parts.push(`reasoning ${n(u.reasoning)}`);
|
|
16
|
+
return parts.join(" ");
|
|
17
|
+
};
|
|
18
|
+
const total = (u) => [u.input, u.output, u.cacheRead, u.cacheWrite, u.reasoning].filter((x) => x !== undefined).reduce((a, b) => a + b, 0);
|
|
19
|
+
const firstLine = (s) => {
|
|
20
|
+
if (typeof s !== "string" || !s)
|
|
21
|
+
return EM;
|
|
22
|
+
const i = s.indexOf("\n");
|
|
23
|
+
return (i < 0 ? s : s.slice(0, i)) || EM;
|
|
24
|
+
};
|
|
25
|
+
const channelLabel = (data) => {
|
|
26
|
+
const a = data.assignment;
|
|
27
|
+
if (!a || typeof a !== "object")
|
|
28
|
+
return EM;
|
|
29
|
+
const { adapter, model } = a;
|
|
30
|
+
return typeof adapter === "string" && typeof model === "string" ? `${adapter}:${model}` : EM;
|
|
31
|
+
};
|
|
32
|
+
const rateBasis = (row) => {
|
|
33
|
+
if (!row.rate)
|
|
34
|
+
return EM;
|
|
35
|
+
const cache = row.rate.cacheReadPerMtok === undefined ? "" : `; cache-read $${row.rate.cacheReadPerMtok}/Mtok`;
|
|
36
|
+
const date = row.rate.rateDate === undefined ? "" : `; rate date ${row.rate.rateDate}`;
|
|
37
|
+
return `in/out $${row.rate.inPerMtok}/$${row.rate.outPerMtok}/Mtok${cache}${date}`;
|
|
38
|
+
};
|
|
39
|
+
const priceLine = (row) => {
|
|
40
|
+
const windows = row.channel === "sub" && row.subPlan ? `windows: ${n(row.attempts)}` : `attempts/windows: ${n(row.attempts)}`;
|
|
41
|
+
const tokenText = row.tokens
|
|
42
|
+
? `tokens: ${row.partialMetering ? "≥ " : ""}${fields(row.tokens)} (${n(total(row.tokens))} tokens)`
|
|
43
|
+
: "tokens: unmetered";
|
|
44
|
+
const prices = [];
|
|
45
|
+
const bases = [];
|
|
46
|
+
if (row.apiUsd !== undefined)
|
|
47
|
+
prices.push(`price: $${row.apiUsd.toFixed(6)}`);
|
|
48
|
+
if (row.amortizedUsd !== undefined && row.subPlan !== undefined) {
|
|
49
|
+
const [low, high] = row.amortizedUsd;
|
|
50
|
+
prices.push(`price: $${low.toFixed(6)}–$${high.toFixed(6)} amortized`);
|
|
51
|
+
bases.push(`${n(row.attempts)} windows × $${row.subPlan.planMonthly}/month ÷ ${row.subPlan.windowsPerMonthHigh}–${row.subPlan.windowsPerMonthLow} windows/month`);
|
|
52
|
+
}
|
|
53
|
+
if (row.counterfactualUsd !== undefined)
|
|
54
|
+
prices.push(`API-equivalent: $${row.counterfactualUsd.toFixed(6)}`);
|
|
55
|
+
if (row.rate)
|
|
56
|
+
bases.push(rateBasis(row));
|
|
57
|
+
if (!prices.length)
|
|
58
|
+
prices.push("price: not measurable");
|
|
59
|
+
if (!bases.length)
|
|
60
|
+
bases.push(row.reason || "not recorded");
|
|
61
|
+
return `- **${row.adapter}:${row.model}** — ${windows}; ${tokenText}; ${prices.join("; ")}; basis: ${bases.join("; ")}`;
|
|
62
|
+
};
|
|
63
|
+
const journalUsage = (events, known) => {
|
|
64
|
+
const groups = new Map();
|
|
65
|
+
for (const e of events) {
|
|
66
|
+
if (e.event !== "task-dispatch")
|
|
67
|
+
continue;
|
|
68
|
+
const channel = typeof e.data.assignment?.channel === "string"
|
|
69
|
+
? e.data.assignment.channel
|
|
70
|
+
: EM;
|
|
71
|
+
const label = channelLabel(e.data);
|
|
72
|
+
if (channel === EM || label === EM)
|
|
73
|
+
continue;
|
|
74
|
+
const key = `${channel}:${label}`;
|
|
75
|
+
if (known.has(key))
|
|
76
|
+
continue;
|
|
77
|
+
const group = groups.get(key);
|
|
78
|
+
if (group)
|
|
79
|
+
group.attempts++;
|
|
80
|
+
else
|
|
81
|
+
groups.set(key, { channel, label, attempts: 1 });
|
|
82
|
+
}
|
|
83
|
+
return [...groups.values()].map((group) => `- **${group.label}** — attempts/windows: ${n(group.attempts)}; tokens: not measurable; price: not measurable; basis: no telemetry row`);
|
|
84
|
+
};
|
|
85
|
+
const wallClock = (start, end) => {
|
|
86
|
+
const from = Date.parse(start?.ts || "");
|
|
87
|
+
const to = Date.parse(end?.ts || "");
|
|
88
|
+
if (!Number.isFinite(from) || !Number.isFinite(to) || to < from)
|
|
89
|
+
return "not measurable";
|
|
90
|
+
const seconds = Math.round((to - from) / 1_000);
|
|
91
|
+
const minutes = Math.floor(seconds / 60);
|
|
92
|
+
return minutes ? `${minutes}m ${seconds % 60}s` : `${seconds}s`;
|
|
93
|
+
};
|
|
94
|
+
const detail = (value) => typeof value === "string" || typeof value === "number" ? String(value) : EM;
|
|
95
|
+
const taskIds = (events) => {
|
|
96
|
+
const seen = new Set();
|
|
97
|
+
const out = [];
|
|
98
|
+
for (const e of events) {
|
|
99
|
+
if (!e.taskId || seen.has(e.taskId))
|
|
100
|
+
continue;
|
|
101
|
+
seen.add(e.taskId);
|
|
102
|
+
out.push(e.taskId);
|
|
103
|
+
}
|
|
104
|
+
return out;
|
|
105
|
+
};
|
|
106
|
+
const outcomeFor = (events, taskId, runEnd) => {
|
|
107
|
+
for (let i = events.length - 1; i >= 0; i--) {
|
|
108
|
+
const e = events[i];
|
|
109
|
+
if (e.taskId !== taskId)
|
|
110
|
+
continue;
|
|
111
|
+
if (e.event === "task-done")
|
|
112
|
+
return "unqualified opinion";
|
|
113
|
+
if (e.event === "task-failed")
|
|
114
|
+
return "qualified opinion";
|
|
115
|
+
if (e.event === "task-human")
|
|
116
|
+
return "human";
|
|
117
|
+
}
|
|
118
|
+
if (runEnd) {
|
|
119
|
+
const d = runEnd.data;
|
|
120
|
+
if (Array.isArray(d.done) && d.done.includes(taskId))
|
|
121
|
+
return "unqualified opinion";
|
|
122
|
+
if (Array.isArray(d.failed) && d.failed.includes(taskId))
|
|
123
|
+
return "qualified opinion";
|
|
124
|
+
if (Array.isArray(d.human) && d.human.includes(taskId))
|
|
125
|
+
return "human";
|
|
126
|
+
}
|
|
127
|
+
return "not recorded";
|
|
128
|
+
};
|
|
129
|
+
// VIS-07 / REC-01: derived only from the run journal, telemetry, and local configuration.
|
|
130
|
+
export function renderMarkdownRecord(runId, events, prices = [], rows = []) {
|
|
131
|
+
const runStart = events.find((e) => e.event === "run-start");
|
|
132
|
+
const runEnd = [...events].reverse().find((e) => e.event === "run-end");
|
|
133
|
+
const baseRef = typeof runStart?.data.baseRef === "string" ? runStart.data.baseRef : EM;
|
|
134
|
+
const branch = typeof runEnd?.data.branch === "string" ? runEnd.data.branch : EM;
|
|
135
|
+
const count = (key) => {
|
|
136
|
+
const v = runEnd?.data[key];
|
|
137
|
+
return Array.isArray(v) ? String(v.length) : EM;
|
|
138
|
+
};
|
|
139
|
+
const gateFailures = new Map();
|
|
140
|
+
for (const event of events) {
|
|
141
|
+
if (event.event !== "gate-result" || event.data.pass !== false || typeof event.data.gate !== "string")
|
|
142
|
+
continue;
|
|
143
|
+
gateFailures.set(event.data.gate, (gateFailures.get(event.data.gate) || 0) + 1);
|
|
144
|
+
}
|
|
145
|
+
const firstAttemptRows = rows.filter((row) => row.firstAttemptOk !== undefined);
|
|
146
|
+
const firstAttemptRate = firstAttemptRows.length
|
|
147
|
+
? `${firstAttemptRows.filter((row) => row.firstAttemptOk).length}/${firstAttemptRows.length} (${Math.round((100 * firstAttemptRows.filter((row) => row.firstAttemptOk).length) / firstAttemptRows.length)}%)`
|
|
148
|
+
: "not measurable";
|
|
149
|
+
const usageLines = prices.map(priceLine);
|
|
150
|
+
usageLines.push(...journalUsage(events, new Set(prices.map((row) => `${row.channel}:${row.adapter}:${row.model}`))));
|
|
151
|
+
if (!usageLines.length)
|
|
152
|
+
usageLines.push("- **not recorded:** attempts/windows: not measurable; tokens: not measurable; price: not measurable");
|
|
153
|
+
const lines = [
|
|
154
|
+
`# tickmarkr engagement`,
|
|
155
|
+
"",
|
|
156
|
+
`- **runId:** ${runId}`,
|
|
157
|
+
`- **base ref:** ${baseRef}`,
|
|
158
|
+
`- **branch:** ${branch}`,
|
|
159
|
+
`- **done:** ${count("done")}`,
|
|
160
|
+
`- **failed:** ${count("failed")}`,
|
|
161
|
+
`- **human:** ${count("human")}`,
|
|
162
|
+
"",
|
|
163
|
+
"## Usage & efficiency",
|
|
164
|
+
"",
|
|
165
|
+
...usageLines,
|
|
166
|
+
`- **wall-clock:** ${wallClock(runStart, runEnd)}`,
|
|
167
|
+
`- **first-attempt rate:** ${firstAttemptRate}`,
|
|
168
|
+
`- **gate failures:** ${[...gateFailures.entries()].map(([gate, failures]) => `${gate}: ${failures}`).join(", ") || "none recorded"}`,
|
|
169
|
+
`- **consults:** ${events.filter((e) => e.event === "consult-verdict").length}`,
|
|
170
|
+
`- **escalations:** ${events.filter((e) => e.event === "escalation").length}`,
|
|
171
|
+
"",
|
|
172
|
+
];
|
|
173
|
+
lines.push("## Audit trail", "");
|
|
174
|
+
for (const taskId of taskIds(events)) {
|
|
175
|
+
const dispatches = events.filter((e) => e.taskId === taskId && e.event === "task-dispatch");
|
|
176
|
+
const channels = dispatches.map((e) => channelLabel(e.data));
|
|
177
|
+
const gates = events.filter((e) => e.taskId === taskId && e.event === "gate-result");
|
|
178
|
+
const consults = events.filter((e) => e.taskId === taskId && e.event === "consult-verdict");
|
|
179
|
+
const deviations = events.filter((e) => e.taskId === taskId && e.event === "route-deviation");
|
|
180
|
+
const merge = [...events].reverse().find((e) => e.taskId === taskId && e.event === "merge");
|
|
181
|
+
const provenance = dispatches
|
|
182
|
+
.map((e) => typeof e.data.provenance === "string" ? e.data.provenance : "")
|
|
183
|
+
.filter(Boolean);
|
|
184
|
+
lines.push(`## ${taskId}`, "");
|
|
185
|
+
lines.push(`- **opinion:** ${outcomeFor(events, taskId, runEnd)}`);
|
|
186
|
+
lines.push(`- **attempts:** ${dispatches.length || EM}`);
|
|
187
|
+
lines.push(`- **channels tried:** ${channels.length ? channels.join(", ") : EM}`);
|
|
188
|
+
lines.push(`- **routing:** ${provenance.length ? provenance.join(" | ") : EM}`);
|
|
189
|
+
if (deviations.length) {
|
|
190
|
+
for (const deviation of deviations) {
|
|
191
|
+
lines.push(`- **route deviation:** ${detail(deviation.data.chosen)} learned score ${detail(deviation.data.score)} (n=${detail(deviation.data.n)}) vs static ${detail(deviation.data.static)} ${detail(deviation.data.staticScore)}`);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
else
|
|
195
|
+
lines.push(`- **route deviation:** ${EM}`);
|
|
196
|
+
lines.push("- **tickmarks:**");
|
|
197
|
+
if (gates.length) {
|
|
198
|
+
for (const g of gates) {
|
|
199
|
+
const pass = g.data.pass === true ? "pass" : g.data.pass === false ? "fail" : EM;
|
|
200
|
+
const gate = typeof g.data.gate === "string" ? g.data.gate : EM;
|
|
201
|
+
lines.push(` - ${gate}: ${pass} — ${firstLine(g.data.details)}`);
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
else
|
|
205
|
+
lines.push(` - ${EM}`);
|
|
206
|
+
lines.push("- **National Office:**");
|
|
207
|
+
if (consults.length) {
|
|
208
|
+
for (const c of consults) {
|
|
209
|
+
const action = typeof c.data.action === "string" ? c.data.action : EM;
|
|
210
|
+
lines.push(` - ${action} — ${firstLine(c.data.notes)}`);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
else
|
|
214
|
+
lines.push(` - ${EM}`);
|
|
215
|
+
const mergedBranch = typeof merge?.data.branch === "string" ? merge.data.branch : EM;
|
|
216
|
+
const mergedCommit = typeof merge?.data.commit === "string" ? merge.data.commit : EM;
|
|
217
|
+
lines.push(`- **consolidation branch:** ${mergedBranch}`);
|
|
218
|
+
lines.push(`- **consolidation commit:** ${mergedCommit}`);
|
|
219
|
+
lines.push("");
|
|
220
|
+
}
|
|
221
|
+
return lines.join("\n").trimEnd() + "\n";
|
|
222
|
+
}
|
|
223
|
+
function textReport(runId, events, rows, cwd) {
|
|
224
|
+
// one group per adapter:model, carrying channel + the folded usage across its rows
|
|
225
|
+
const groups = new Map();
|
|
226
|
+
for (const r of rows) {
|
|
227
|
+
const k = `${r.adapter}:${r.model}`;
|
|
228
|
+
const g = groups.get(k) ?? { channel: r.channel, rows: [] };
|
|
229
|
+
g.rows.push(r);
|
|
230
|
+
groups.set(k, g);
|
|
231
|
+
}
|
|
232
|
+
// TOKENS axis (channel-agnostic): tokens present ⇒ measured, absent ⇒ unmetered (never 0).
|
|
233
|
+
const tokenLines = [...groups.entries()].map(([k, g]) => {
|
|
234
|
+
const usage = g.rows.filter((r) => r.tokens).reduce((a, r) => addUsage(a, r.tokens), undefined);
|
|
235
|
+
if (!usage)
|
|
236
|
+
return ` ${k.padEnd(24)} unmetered (adapter reports no usage)`;
|
|
237
|
+
// exact iff EVERY row is metered to completion; a degraded (tokens but meteredAttempts undefined) row ⇒ floor
|
|
238
|
+
const exact = g.rows.every((r) => r.tokens && r.meteredAttempts === r.attempts);
|
|
239
|
+
if (exact) {
|
|
240
|
+
const tasks = g.rows.length;
|
|
241
|
+
return ` ${k.padEnd(24)} ${fields(usage)} (${n(total(usage))} tokens, ${tasks} task${tasks === 1 ? "" : "s"})`;
|
|
242
|
+
}
|
|
243
|
+
const metered = g.rows.reduce((s, r) => (r.meteredAttempts === undefined ? s : s + r.meteredAttempts), 0);
|
|
244
|
+
const attempts = g.rows.reduce((s, r) => s + r.attempts, 0);
|
|
245
|
+
return ` ${k.padEnd(24)} ≥ ${fields(usage)} (floor: ${metered}/${attempts} attempts metered)`;
|
|
246
|
+
});
|
|
247
|
+
// MONEY axis (orthogonal — branches on channel, NOT tokens): sub ⇒ subscription (no price ever),
|
|
248
|
+
// api ⇒ operator price × tokens; no prices configured ⇒ `price unset`, never a dollar figure.
|
|
249
|
+
const subs = [...groups].filter(([, g]) => g.channel === "sub").map(([k]) => k);
|
|
250
|
+
const apis = [...groups].filter(([, g]) => g.channel === "api").map(([k]) => k);
|
|
251
|
+
const apiLine = apis.length ? apis.map((k) => `${k} — price unset`).join(" · ") : "none configured";
|
|
252
|
+
// VIS-05 learning axis: render the PREVIEW-mode profile (preview bypasses routing.learned:off) so the
|
|
253
|
+
// operator audits per-cell confidence before ever flipping learning on. profile.ts owns every number
|
|
254
|
+
// (cellSummary); this section only formats. probes = this run's exploratory route-deviation events.
|
|
255
|
+
const cfg = loadConfig(cwd);
|
|
256
|
+
const off = cfg.routing.learned === "off";
|
|
257
|
+
const p = loadRoutingProfile(cwd, cfg, { preview: true });
|
|
258
|
+
const probes = events.filter((e) => e.event === "route-deviation" && e.data.explore === true).length;
|
|
259
|
+
const learningLines = !p || p.cells.size === 0
|
|
260
|
+
? [" no telemetry yet"]
|
|
261
|
+
: [...cellsOf(p)].map(({ shape, chKey, channel, cell }) => {
|
|
262
|
+
const s = cellSummary(cell);
|
|
263
|
+
return ` ${shape.padEnd(10)} ${chKey.padEnd(28)} ${channel.padEnd(4)} raw=${s.nRaw} n_eff=${s.nEff} disp=${s.dispatches} q=${s.quality === undefined ? "-" : s.quality.toFixed(2)} quota=${s.quotaHits} explore-left=${s.exploreRemaining}${s.cold ? " cold (neutral)" : ""}`;
|
|
264
|
+
});
|
|
265
|
+
const gateResults = events.filter((e) => e.event === "gate-result");
|
|
266
|
+
const gatePass = gateResults.filter((e) => e.data.pass).length;
|
|
267
|
+
const escalations = events.filter((e) => e.event === "escalation").length;
|
|
268
|
+
const consults = events.filter((e) => e.event === "consult-verdict").length;
|
|
269
|
+
const failovers = events.filter((e) => e.event === "quota-failover").length;
|
|
270
|
+
return [
|
|
271
|
+
`tickmarkr engagement — ${runId}`,
|
|
272
|
+
"",
|
|
273
|
+
"engagement summary — audit trail:",
|
|
274
|
+
...[...groups.entries()].map(([k, g]) => ` ${k.padEnd(30)} tasks ${g.rows.length}, attempts ${g.rows.reduce((s, r) => s + r.attempts, 0)}, done ${g.rows.filter((r) => r.outcome === "done").length}`),
|
|
275
|
+
"",
|
|
276
|
+
`tickmark rate: ${gateResults.length ? Math.round((100 * gatePass) / gateResults.length) : 0}% (${gatePass}/${gateResults.length})`,
|
|
277
|
+
`escalations: ${escalations} · National Office consults: ${consults} · quota failovers: ${failovers}`,
|
|
278
|
+
"",
|
|
279
|
+
"spend — tokens (measured where observed):",
|
|
280
|
+
...tokenLines,
|
|
281
|
+
"spend — money:",
|
|
282
|
+
` subscription channels (no marginal spend): ${subs.length ? subs.join(", ") : "none"}`,
|
|
283
|
+
` api channels: ${apiLine}`,
|
|
284
|
+
"",
|
|
285
|
+
`learning (routing.learned: ${cfg.routing.learned}${off ? " — preview" : ""}):`,
|
|
286
|
+
...learningLines,
|
|
287
|
+
` probes this run (route-deviation explore): ${probes}`,
|
|
288
|
+
].join("\n");
|
|
289
|
+
}
|
|
290
|
+
export async function report(argv, cwd = process.cwd()) {
|
|
291
|
+
const { values, positionals } = parseArgs({
|
|
292
|
+
args: argv,
|
|
293
|
+
options: { md: { type: "boolean" } },
|
|
294
|
+
allowPositionals: true,
|
|
295
|
+
});
|
|
296
|
+
const runId = positionals[0] ?? Journal.latestRunId(cwd);
|
|
297
|
+
if (!runId)
|
|
298
|
+
throw new Error("no runs found — usage: drovr report <run-id> [--md]");
|
|
299
|
+
const j = Journal.open(cwd, runId);
|
|
300
|
+
const events = j.read();
|
|
301
|
+
if (values.md) {
|
|
302
|
+
const rows = j.readTelemetry();
|
|
303
|
+
return renderMarkdownRecord(runId, events, estimateCosts(rows, loadConfig(cwd).cost), rows);
|
|
304
|
+
}
|
|
305
|
+
return textReport(runId, events, j.readTelemetry(), cwd);
|
|
306
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function resume(argv: string[], cwd?: string): Promise<string>;
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { loadConfig } from "../../config/config.js";
|
|
2
|
+
import { pickDriver } from "../../drivers/index.js";
|
|
3
|
+
import { formatSummary, runDaemon } from "../../run/daemon.js";
|
|
4
|
+
import { formatJournalNarration } from "../../run/journal.js";
|
|
5
|
+
export async function resume(argv, cwd = process.cwd()) {
|
|
6
|
+
const runId = argv[0];
|
|
7
|
+
if (!runId)
|
|
8
|
+
throw new Error("usage: drovr resume <run-id>");
|
|
9
|
+
const s = await runDaemon(cwd, { runId, resume: true, driver: pickDriver(loadConfig(cwd)), narrate: (event) => console.log(formatJournalNarration(event)) });
|
|
10
|
+
return `resumed ${s.runId} — ${formatSummary(s)}`;
|
|
11
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export declare function run(argv: string[], cwd?: string): Promise<string>;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { parseArgs } from "node:util";
|
|
2
|
+
import { allAdapters, discoverChannels, probeAll, readDoctor } from "../../adapters/registry.js";
|
|
3
|
+
import { loadConfig } from "../../config/config.js";
|
|
4
|
+
import { pickDriver } from "../../drivers/index.js";
|
|
5
|
+
import { loadGraph } from "../../graph/graph.js";
|
|
6
|
+
import { formatSummary, runDaemon } from "../../run/daemon.js";
|
|
7
|
+
import { route } from "../../route/router.js";
|
|
8
|
+
import { formatJournalNarration, loadRoutingProfile } from "../../run/journal.js";
|
|
9
|
+
export async function run(argv, cwd = process.cwd()) {
|
|
10
|
+
const { values } = parseArgs({
|
|
11
|
+
args: argv,
|
|
12
|
+
options: {
|
|
13
|
+
concurrency: { type: "string" },
|
|
14
|
+
driver: { type: "string" },
|
|
15
|
+
"route-strict": { type: "boolean" },
|
|
16
|
+
},
|
|
17
|
+
});
|
|
18
|
+
const cfg = loadConfig(cwd);
|
|
19
|
+
if (values["route-strict"]) {
|
|
20
|
+
const adapters = allAdapters();
|
|
21
|
+
const health = readDoctor(cwd) ?? (await probeAll(adapters));
|
|
22
|
+
const channels = discoverChannels(cfg, adapters, health);
|
|
23
|
+
// no preview: the strict pre-flight routes through exactly what the daemon will use (honors the switch)
|
|
24
|
+
const profile = loadRoutingProfile(cwd, cfg);
|
|
25
|
+
const lints = loadGraph(cwd).tasks.flatMap((t) => route(t, cfg, channels, profile).lints);
|
|
26
|
+
if (lints.length)
|
|
27
|
+
throw new Error(`--route-strict: routing lints present, refusing to dispatch:\n${lints.join("\n")}`);
|
|
28
|
+
}
|
|
29
|
+
const s = await runDaemon(cwd, {
|
|
30
|
+
concurrency: values.concurrency ? Number(values.concurrency) : undefined,
|
|
31
|
+
driver: pickDriver(cfg, values.driver),
|
|
32
|
+
narrate: (event) => console.log(formatJournalNarration(event)),
|
|
33
|
+
});
|
|
34
|
+
return `run ${s.runId} finished — ${formatSummary(s)} (merge to main is a human decision)`;
|
|
35
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { isAbsolute, join, relative } from "node:path";
|
|
2
|
+
import { parseArgs } from "node:util";
|
|
3
|
+
import { allAdapters } from "../../adapters/registry.js";
|
|
4
|
+
import { loadConfig } from "../../config/config.js";
|
|
5
|
+
import { scopeIntent } from "../../plan/scope.js";
|
|
6
|
+
export async function scope(argv, cwd = process.cwd(), adapters = allAdapters(), driver) {
|
|
7
|
+
const { values, positionals } = parseArgs({
|
|
8
|
+
args: argv,
|
|
9
|
+
options: { force: { type: "boolean" } },
|
|
10
|
+
allowPositionals: true,
|
|
11
|
+
});
|
|
12
|
+
if (positionals.length !== 1)
|
|
13
|
+
throw new Error("usage: drovr scope <intent-file> [--force]");
|
|
14
|
+
const source = positionals[0];
|
|
15
|
+
const intentFile = isAbsolute(source) ? source : join(cwd, source);
|
|
16
|
+
const result = await scopeIntent(intentFile, cwd, {
|
|
17
|
+
cfg: loadConfig(cwd), adapters, driver, force: values.force,
|
|
18
|
+
});
|
|
19
|
+
const tasks = `${result.tasks} task${result.tasks === 1 ? "" : "s"}`;
|
|
20
|
+
const calls = `${result.attempts} LLM call${result.attempts === 1 ? "" : "s"}`;
|
|
21
|
+
return `scoped ${source} → ${relative(cwd, result.specFile)} (${tasks}, ${calls})`;
|
|
22
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type Task } from "../../graph/schema.js";
|
|
2
|
+
import { type JournalEvent } from "../../run/journal.js";
|
|
3
|
+
export type StatusOpts = {
|
|
4
|
+
iterations?: number;
|
|
5
|
+
sleep?: (ms: number) => Promise<void>;
|
|
6
|
+
};
|
|
7
|
+
/** Derive the live phase column for one task. Counts only that task's events since its last dispatch/escalation. */
|
|
8
|
+
export declare const derivePhase: (task: Task, events: JournalEvent[], channel?: string) => string;
|
|
9
|
+
export declare function status(argv: string[], cwd?: string, opts?: StatusOpts): Promise<string>;
|