tickmarkr 1.49.0 → 1.51.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/dist/adapters/model-lints.d.ts +1 -0
- package/dist/adapters/model-lints.js +9 -0
- package/dist/brand.d.ts +62 -0
- package/dist/brand.js +74 -2
- package/dist/cli/commands/doctor.js +33 -46
- package/dist/cli/commands/fleet.js +89 -30
- package/dist/cli/commands/init.js +22 -2
- package/dist/cli/commands/plan.d.ts +1 -1
- package/dist/cli/commands/plan.js +67 -7
- package/dist/cli/commands/report.js +13 -1
- package/dist/cli/commands/run.d.ts +2 -0
- package/dist/cli/commands/run.js +48 -13
- package/dist/cli/commands/status.js +38 -41
- package/dist/compile/native.js +18 -2
- package/dist/config/config.d.ts +28 -0
- package/dist/config/config.js +103 -8
- package/dist/drivers/herdr.js +6 -2
- package/dist/graph/schema.d.ts +6 -0
- package/dist/graph/schema.js +6 -0
- package/dist/route/profile.d.ts +12 -0
- package/dist/route/profile.js +31 -0
- package/dist/run/daemon.d.ts +16 -0
- package/dist/run/daemon.js +61 -13
- package/package.json +1 -1
|
@@ -1,12 +1,32 @@
|
|
|
1
1
|
import { allAdapters, discoverChannels, doctorAgeMs, modelAuthExclusions, probeAll, readDoctor, servableExclusions, servabilityLine } from "../../adapters/registry.js";
|
|
2
2
|
import { formatModelAuthLine, contextWindowLints, modelLints, ttyVisual } from "../../adapters/model-lints.js";
|
|
3
|
+
import { GLYPHS, dim, rule, title, warn } from "../../brand.js";
|
|
4
|
+
import { parseArgs } from "node:util";
|
|
3
5
|
import { collateralLints } from "../../compile/collateral.js";
|
|
4
|
-
import {
|
|
6
|
+
import { DEFAULT_CONFIG, ROUTING_MODES, TIER_RANK } from "../../config/config.js";
|
|
5
7
|
import { loadGraph } from "../../graph/graph.js";
|
|
8
|
+
import { resolveRunMode } from "../../run/daemon.js";
|
|
6
9
|
import { excludedChannels, exclusionLine } from "../../route/preference.js";
|
|
7
|
-
import {
|
|
10
|
+
import { staffLedEvidence } from "../../route/profile.js";
|
|
11
|
+
import { QUALITY_ENV, route, RoutingError } from "../../route/router.js";
|
|
8
12
|
import { modelId } from "../../gates/review.js";
|
|
9
13
|
import { loadRoutingProfile } from "../../run/journal.js";
|
|
14
|
+
// T4 (v1.50): TTY-only brand pass — the title helper frames the routing table, lint/unroutable
|
|
15
|
+
// markers carry the attention glyph, section labels dim to chrome (the doctor/status system).
|
|
16
|
+
// Gated on ttyVisual(): the non-TTY surface returns untouched (byte-pinned, machine-consumable).
|
|
17
|
+
const stylizePlan = (out) => {
|
|
18
|
+
if (!ttyVisual())
|
|
19
|
+
return out;
|
|
20
|
+
return out.split("\n").map((line, i) => {
|
|
21
|
+
if (i === 0)
|
|
22
|
+
return `${title(line)}\n${rule()}`;
|
|
23
|
+
if (/^(routing|context window|scope) lints:$/.test(line))
|
|
24
|
+
return dim(line);
|
|
25
|
+
return line
|
|
26
|
+
.replace(/^(\s+)! /, (_, s) => `${s}${warn(GLYPHS.attention)} `)
|
|
27
|
+
.replace(/^( \S+\s+\S+\s+)!! /, (_, p) => `${p}${warn("!!")} `);
|
|
28
|
+
}).join("\n");
|
|
29
|
+
};
|
|
10
30
|
const fleetCanCrossVendorReview = (channels) => {
|
|
11
31
|
for (let i = 0; i < channels.length; i++)
|
|
12
32
|
for (let j = 0; j < channels.length; j++)
|
|
@@ -14,12 +34,19 @@ const fleetCanCrossVendorReview = (channels) => {
|
|
|
14
34
|
return true;
|
|
15
35
|
return false;
|
|
16
36
|
};
|
|
17
|
-
export async function plan(
|
|
37
|
+
export async function plan(argv, cwd = process.cwd(), adapters = allAdapters()) {
|
|
18
38
|
// ponytail: hardcoded 24h TTL — promote to config when an operator asks. mtime is the signal because
|
|
19
39
|
// doctor.json has no probe timestamp and a schema field would break the existing-files compat invariant.
|
|
20
40
|
const DOCTOR_STALE_MS = 24 * 60 * 60 * 1000;
|
|
21
|
-
|
|
41
|
+
// v1.51 T2: plan previews any mode without a config edit — same source precedence as run
|
|
42
|
+
// (flag > spec front-matter > repo > global > default), same preset compiler.
|
|
43
|
+
const { values } = parseArgs({ args: argv, options: { mode: { type: "string" } }, allowPositionals: true });
|
|
44
|
+
if (values.mode !== undefined && !ROUTING_MODES.includes(values.mode)) {
|
|
45
|
+
throw new Error(`--mode must be one of ${ROUTING_MODES.join(" | ")} (got ${values.mode})`);
|
|
46
|
+
}
|
|
47
|
+
delete process.env[QUALITY_ENV];
|
|
22
48
|
const g = loadGraph(cwd);
|
|
49
|
+
const { cfg, mode, source } = resolveRunMode(cwd, { flag: values.mode, spec: g.mode });
|
|
23
50
|
// readDoctor cache path: staleness line only fires here (probeAll fallback is fresh by construction).
|
|
24
51
|
const cached = readDoctor(cwd);
|
|
25
52
|
const health = cached ?? (await probeAll(adapters));
|
|
@@ -29,7 +56,17 @@ export async function plan(_argv, cwd = process.cwd(), adapters = allAdapters())
|
|
|
29
56
|
// output byte-identical to today.
|
|
30
57
|
const profile = loadRoutingProfile(cwd, cfg, { preview: true });
|
|
31
58
|
let deviations = 0;
|
|
32
|
-
|
|
59
|
+
// v1.51 T4: the mode is never invisible — the header names the resolved mode, its winning
|
|
60
|
+
// source, and the explore posture; each task row carries a floor-derivation line below.
|
|
61
|
+
const lines = [
|
|
62
|
+
`tickmarkr plan — dry run (${channels.length} channels available)`,
|
|
63
|
+
`mode: ${mode.mode} (${source}) · explore ${cfg.routing.explore?.mode ?? "on"}`,
|
|
64
|
+
"",
|
|
65
|
+
];
|
|
66
|
+
const derivation = (shape) => {
|
|
67
|
+
const floor = cfg.routing.floors[shape];
|
|
68
|
+
return floor ? ` floor ${floor} ← ${mode.provenance[shape] ?? "config floors"}` : null;
|
|
69
|
+
};
|
|
33
70
|
const excluded = excludedChannels(cfg, adapters, health);
|
|
34
71
|
if (excluded.length)
|
|
35
72
|
lines.push(exclusionLine(excluded), "");
|
|
@@ -61,7 +98,24 @@ export async function plan(_argv, cwd = process.cwd(), adapters = allAdapters())
|
|
|
61
98
|
return ` — ${m.key} is unauthed (${m.reason}, probed ${m.probedAt.split("T")[0]})`;
|
|
62
99
|
return "";
|
|
63
100
|
};
|
|
64
|
-
|
|
101
|
+
// v1.51 T1: mode-resolution lints (shadowed deltas, below-integrity operator floors) surface here every run.
|
|
102
|
+
const lints = [...mode.lints];
|
|
103
|
+
// v1.51 T5: staff-led economics guard — when the MODE (never an explicit operator line) lowered a
|
|
104
|
+
// shape's floor and warm evidence shows the cheap band materially under the mid incumbent, say so.
|
|
105
|
+
// Advisory only: no floor is raised here or anywhere on prediction — raises stay evidence-triggered
|
|
106
|
+
// (task-hint floors, the in-run retry ladder), each journaled with provenance.
|
|
107
|
+
if (mode.mode === "staff-led") {
|
|
108
|
+
const fmtScore = (s) => `${s < 0 ? "" : "+"}${s.toFixed(3)}`;
|
|
109
|
+
for (const [shape, floor] of Object.entries(cfg.routing.floors)) {
|
|
110
|
+
const dflt = DEFAULT_CONFIG.routing.floors[shape];
|
|
111
|
+
if (mode.provenance[shape] !== "mode staff-led" || !dflt || TIER_RANK[floor] >= TIER_RANK[dflt])
|
|
112
|
+
continue;
|
|
113
|
+
const ev = staffLedEvidence(profile, shape, channels);
|
|
114
|
+
if (ev) {
|
|
115
|
+
lints.push(`staff-led may cost more than risk-based on ${shape} (cheap best ${fmtScore(ev.cheapBest)} vs mid ${fmtScore(ev.midBest)}, n=${ev.n}) — advisory only, floor stays ${floor}`);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
}
|
|
65
119
|
for (const [role, sel] of [["judge", cfg.judge], ["consult", cfg.consult]]) {
|
|
66
120
|
if (!health[sel.adapter]?.installed)
|
|
67
121
|
lints.push(`${role}: ${sel.adapter}:${sel.model} not installed — that gate/consult will fail closed`);
|
|
@@ -83,6 +137,9 @@ export async function plan(_argv, cwd = process.cwd(), adapters = allAdapters())
|
|
|
83
137
|
const est = r.assignment.channel === "sub" ? 0 : cfg.pricing[r.assignment.tier];
|
|
84
138
|
cost += est;
|
|
85
139
|
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.timeoutMinutes !== undefined ? ` (timeout ${t.timeoutMinutes}m)` : ""}${t.humanGate ? " (human gate)" : ""}${est ? ` ~$${est.toFixed(2)}` : ""} — ${r.provenance}`);
|
|
140
|
+
const d = derivation(t.shape);
|
|
141
|
+
if (d)
|
|
142
|
+
lines.push(d);
|
|
86
143
|
if (r.deviation) {
|
|
87
144
|
deviations++;
|
|
88
145
|
const d = r.deviation;
|
|
@@ -94,6 +151,9 @@ export async function plan(_argv, cwd = process.cwd(), adapters = allAdapters())
|
|
|
94
151
|
throw e;
|
|
95
152
|
const msg = `${e.message}${exclusionReason(e.message)}`;
|
|
96
153
|
lines.push(` ${t.id.padEnd(6)} ${t.shape.padEnd(10)} !! ${msg}`);
|
|
154
|
+
const d = derivation(t.shape);
|
|
155
|
+
if (d)
|
|
156
|
+
lines.push(d);
|
|
97
157
|
lints.push(`${t.id}: unroutable — ${msg}`);
|
|
98
158
|
}
|
|
99
159
|
}
|
|
@@ -114,5 +174,5 @@ export async function plan(_argv, cwd = process.cwd(), adapters = allAdapters())
|
|
|
114
174
|
const scopeLints = collateralLints(g.tasks, cwd);
|
|
115
175
|
if (scopeLints.length)
|
|
116
176
|
lines.push("", "scope lints:", ...scopeLints.map((l) => ` ! ${l}`));
|
|
117
|
-
return lines.join("\n");
|
|
177
|
+
return stylizePlan(lines.join("\n"));
|
|
118
178
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { parseArgs } from "node:util";
|
|
2
|
+
import { ttyVisual } from "../../adapters/model-lints.js";
|
|
2
3
|
import { addUsage } from "../../adapters/types.js";
|
|
4
|
+
import { dim, rule, title } from "../../brand.js";
|
|
3
5
|
import { loadConfig } from "../../config/config.js";
|
|
4
6
|
import { estimateCosts } from "../../report/cost.js";
|
|
5
7
|
import { cellsOf, cellSummary } from "../../route/profile.js";
|
|
@@ -287,6 +289,16 @@ function textReport(runId, events, rows, cwd) {
|
|
|
287
289
|
` probes this run (route-deviation explore): ${probes}`,
|
|
288
290
|
].join("\n");
|
|
289
291
|
}
|
|
292
|
+
// T4 (v1.50): TTY-only brand pass over the text report — title frame + dim section chrome; row
|
|
293
|
+
// text and alignment untouched (the doctor/status system). Gated on ttyVisual(): the non-TTY
|
|
294
|
+
// surface returns untouched, and --md never styles (the record is a document surface).
|
|
295
|
+
const stylizeReport = (out) => {
|
|
296
|
+
if (!ttyVisual())
|
|
297
|
+
return out;
|
|
298
|
+
return out
|
|
299
|
+
.replace(/^.*$/m, (first) => `${title(first)}\n${rule()}`) // non-global /m ⇒ first line only
|
|
300
|
+
.replace(/^(engagement summary — audit trail:|spend — tokens[^\n]*|spend — money:|learning \([^\n]*)$/gm, (l) => dim(l));
|
|
301
|
+
};
|
|
290
302
|
export async function report(argv, cwd = process.cwd()) {
|
|
291
303
|
const { values, positionals } = parseArgs({
|
|
292
304
|
args: argv,
|
|
@@ -302,5 +314,5 @@ export async function report(argv, cwd = process.cwd()) {
|
|
|
302
314
|
const rows = j.readTelemetry();
|
|
303
315
|
return renderMarkdownRecord(runId, events, estimateCosts(rows, loadConfig(cwd).cost), rows);
|
|
304
316
|
}
|
|
305
|
-
return textReport(runId, events, j.readTelemetry(), cwd);
|
|
317
|
+
return stylizeReport(textReport(runId, events, j.readTelemetry(), cwd));
|
|
306
318
|
}
|
package/dist/cli/commands/run.js
CHANGED
|
@@ -1,13 +1,34 @@
|
|
|
1
1
|
import { parseArgs } from "node:util";
|
|
2
2
|
import { allAdapters, discoverChannels, probeAll, readDoctor } from "../../adapters/registry.js";
|
|
3
|
-
import {
|
|
3
|
+
import { ROUTING_MODES } from "../../config/config.js";
|
|
4
4
|
import { pickDriver } from "../../drivers/index.js";
|
|
5
5
|
import { loadGraph } from "../../graph/graph.js";
|
|
6
|
-
import { formatSummary, runDaemon } from "../../run/daemon.js";
|
|
7
|
-
import { route,
|
|
6
|
+
import { formatSummary, resolveRunMode, runDaemon } from "../../run/daemon.js";
|
|
7
|
+
import { route, NO_EXPLORE_ENV, QUALITY_ENV } from "../../route/router.js";
|
|
8
8
|
import { formatJournalNarration, loadRoutingProfile } from "../../run/journal.js";
|
|
9
|
+
import { ttyVisual } from "../../adapters/model-lints.js";
|
|
10
|
+
import { statusRow } from "../../brand.js";
|
|
11
|
+
// T4 (v1.50): lifecycle verdict glyphs on the live narration stream — glyph-first, message text
|
|
12
|
+
// unchanged (the doctor/status visual system). TTY-gated: the piped narration surface stays
|
|
13
|
+
// byte-identical to formatJournalNarration.
|
|
14
|
+
const NARRATION_VERDICTS = {
|
|
15
|
+
"task-dispatch": "neutral",
|
|
16
|
+
"task-done": "pass",
|
|
17
|
+
"task-failed": "fail",
|
|
18
|
+
"task-human": "warn",
|
|
19
|
+
};
|
|
20
|
+
export const narrationLine = (event) => {
|
|
21
|
+
const line = formatJournalNarration(event);
|
|
22
|
+
const verdict = NARRATION_VERDICTS[event.event];
|
|
23
|
+
return verdict !== undefined && ttyVisual() ? statusRow(verdict, line) : line;
|
|
24
|
+
};
|
|
9
25
|
const summaryGreen = (s) => s.failed.length === 0 && s.human.length === 0 && s.blocked.length === 0 && s.pending.length === 0
|
|
10
26
|
&& s.tipVerify !== "failed";
|
|
27
|
+
// v1.51 T2: --quality is a pure compatibility alias for `--mode partner-led` (this run only). It
|
|
28
|
+
// carries no one-band floor raise of its own: the flag never sets ExploreContext.quality or the
|
|
29
|
+
// TICKMARKR_QUALITY env, so no downstream code raises a floor on its behalf (proven in mode-sources).
|
|
30
|
+
const QUALITY_ALIAS_NOTICE = "tickmarkr: --quality is a compatibility alias for --mode partner-led (this run only) — "
|
|
31
|
+
+ "the v1.47 one-band floor raise is retired (deprecated); use --mode partner-led";
|
|
11
32
|
export async function run(argv, cwd = process.cwd()) {
|
|
12
33
|
const { values } = parseArgs({
|
|
13
34
|
args: argv,
|
|
@@ -16,6 +37,7 @@ export async function run(argv, cwd = process.cwd()) {
|
|
|
16
37
|
driver: { type: "string" },
|
|
17
38
|
"route-strict": { type: "boolean" },
|
|
18
39
|
"no-explore": { type: "boolean" },
|
|
40
|
+
mode: { type: "string" },
|
|
19
41
|
quality: { type: "boolean" },
|
|
20
42
|
},
|
|
21
43
|
});
|
|
@@ -24,14 +46,28 @@ export async function run(argv, cwd = process.cwd()) {
|
|
|
24
46
|
if (!Number.isInteger(n) || n <= 0)
|
|
25
47
|
throw new Error(`--concurrency must be a positive integer (got ${values.concurrency})`);
|
|
26
48
|
}
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
49
|
+
if (values.quality && values.mode !== undefined) {
|
|
50
|
+
throw new Error("--quality is a compatibility alias for --mode partner-led and cannot be combined with an explicit --mode — pass one or the other");
|
|
51
|
+
}
|
|
52
|
+
if (values.mode !== undefined && !ROUTING_MODES.includes(values.mode)) {
|
|
53
|
+
throw new Error(`--mode must be one of ${ROUTING_MODES.join(" | ")} (got ${values.mode})`);
|
|
54
|
+
}
|
|
55
|
+
if (values.quality)
|
|
56
|
+
console.warn(QUALITY_ALIAS_NOTICE);
|
|
57
|
+
const flagMode = values.mode ?? (values.quality ? "partner-led" : undefined);
|
|
58
|
+
delete process.env[QUALITY_ENV];
|
|
59
|
+
const graph = loadGraph(cwd);
|
|
60
|
+
const { cfg, conflict } = resolveRunMode(cwd, { flag: flagMode, spec: graph.mode });
|
|
61
|
+
if (conflict) {
|
|
62
|
+
// Loud, never silent: live intent (the flag) may override compiled intent (the spec) — strict refuses.
|
|
63
|
+
if (values["route-strict"])
|
|
64
|
+
throw new Error(`--route-strict: refusing to dispatch — ${conflict}`);
|
|
65
|
+
console.warn(`tickmarkr: !! ${conflict}`);
|
|
66
|
+
}
|
|
67
|
+
const noExplore = !!values["no-explore"];
|
|
68
|
+
const exploreCtx = noExplore ? { noExplore } : undefined;
|
|
31
69
|
if (noExplore)
|
|
32
70
|
process.env[NO_EXPLORE_ENV] = "1";
|
|
33
|
-
if (quality)
|
|
34
|
-
process.env[QUALITY_ENV] = "1";
|
|
35
71
|
try {
|
|
36
72
|
if (values["route-strict"]) {
|
|
37
73
|
const adapters = allAdapters();
|
|
@@ -39,14 +75,15 @@ export async function run(argv, cwd = process.cwd()) {
|
|
|
39
75
|
const channels = discoverChannels(cfg, adapters, health);
|
|
40
76
|
// no preview: the strict pre-flight routes through exactly what the daemon will use (honors the switch)
|
|
41
77
|
const profile = loadRoutingProfile(cwd, cfg);
|
|
42
|
-
const lints =
|
|
78
|
+
const lints = graph.tasks.flatMap((t) => route(t, cfg, channels, profile, undefined, undefined, exploreCtx).lints);
|
|
43
79
|
if (lints.length)
|
|
44
80
|
throw new Error(`--route-strict: routing lints present, refusing to dispatch:\n${lints.join("\n")}`);
|
|
45
81
|
}
|
|
46
82
|
const s = await runDaemon(cwd, {
|
|
47
83
|
concurrency: values.concurrency ? Number(values.concurrency) : undefined,
|
|
48
84
|
driver: pickDriver(cfg, values.driver),
|
|
49
|
-
|
|
85
|
+
mode: flagMode,
|
|
86
|
+
narrate: (event) => console.log(narrationLine(event)),
|
|
50
87
|
});
|
|
51
88
|
const out = `run ${s.runId} finished — ${formatSummary(s)} (merge to main is a human decision)`;
|
|
52
89
|
return { out, code: summaryGreen(s) ? 0 : 2 };
|
|
@@ -54,7 +91,5 @@ export async function run(argv, cwd = process.cwd()) {
|
|
|
54
91
|
finally {
|
|
55
92
|
if (noExplore)
|
|
56
93
|
delete process.env[NO_EXPLORE_ENV];
|
|
57
|
-
if (quality)
|
|
58
|
-
delete process.env[QUALITY_ENV];
|
|
59
94
|
}
|
|
60
95
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { BANNER } from "../../brand.js";
|
|
1
|
+
import { BANNER, GLYPHS, dim, fail, legend, ok, rule, statusRow, title, warn } from "../../brand.js";
|
|
2
2
|
import { blockedTasks, graphDefinitionHash, loadGraph, pendingTasks } from "../../graph/graph.js";
|
|
3
3
|
import { GATE_NAMES } from "../../graph/schema.js";
|
|
4
4
|
import { Journal, engagementComparable } from "../../run/journal.js";
|
|
@@ -19,17 +19,8 @@ const attemptStartIdx = (events, taskId) => {
|
|
|
19
19
|
return idx;
|
|
20
20
|
};
|
|
21
21
|
const visual = () => process.stdout.isTTY === true && process.env.NO_COLOR === undefined;
|
|
22
|
-
|
|
23
|
-
const taskBox = (status
|
|
24
|
-
if (unicode) {
|
|
25
|
-
if (status === "done")
|
|
26
|
-
return "✓";
|
|
27
|
-
if (status === "failed")
|
|
28
|
-
return "✗";
|
|
29
|
-
if (status === "human")
|
|
30
|
-
return "⏸";
|
|
31
|
-
return "☐";
|
|
32
|
-
}
|
|
22
|
+
// non-TTY machine surface only — the TTY frame draws task verdicts via statusRow
|
|
23
|
+
const taskBox = (status) => {
|
|
33
24
|
if (status === "done")
|
|
34
25
|
return "[x]";
|
|
35
26
|
if (status === "failed" || status === "human")
|
|
@@ -37,8 +28,10 @@ const taskBox = (status, unicode) => {
|
|
|
37
28
|
return "[ ]";
|
|
38
29
|
};
|
|
39
30
|
const gateBox = (state, unicode) => {
|
|
40
|
-
if (unicode)
|
|
41
|
-
|
|
31
|
+
if (unicode) {
|
|
32
|
+
// shared glyph vocabulary: pass/fail verdicts, dash for skip, dim circle for not-yet-run
|
|
33
|
+
return state === "pass" ? GLYPHS.pass : state === "fail" ? GLYPHS.fail : state === "skip" ? GLYPHS.neutral : GLYPHS.toggleInactive;
|
|
34
|
+
}
|
|
42
35
|
return state === "pass" ? "[x]" : state === "fail" ? "[!]" : state === "skip" ? "." : "[ ]";
|
|
43
36
|
};
|
|
44
37
|
const defaultGateStates = (task) => GATE_NAMES.map((gate) => task.gates.includes(gate) ? "open" : "skip");
|
|
@@ -59,9 +52,12 @@ const gateStates = (task, events) => {
|
|
|
59
52
|
}
|
|
60
53
|
return GATE_NAMES.map((gate) => task.gates.includes(gate) ? outcomes.get(gate) ?? "open" : "skip");
|
|
61
54
|
};
|
|
62
|
-
// verdict semantics only: pass green, fail red, skip/open dim chrome — everything else stays quiet
|
|
63
|
-
const
|
|
64
|
-
const gateChain = (states, unicode) => GATE_NAMES.map((gate, i) =>
|
|
55
|
+
// verdict semantics only: pass brand green, fail red, skip/open dim chrome — everything else stays quiet
|
|
56
|
+
const GATE_STATE_TOKEN = { pass: ok, fail, skip: dim, open: dim };
|
|
57
|
+
const gateChain = (states, unicode) => GATE_NAMES.map((gate, i) => {
|
|
58
|
+
const chip = `${GATE_KEYS[gate]}${gateBox(states[i], unicode)}`;
|
|
59
|
+
return unicode ? GATE_STATE_TOKEN[states[i]](chip) : chip;
|
|
60
|
+
}).join(" ");
|
|
65
61
|
// plain (uncolored) chip width for column math — ANSI codes have zero display width
|
|
66
62
|
const gateChainWidth = (unicode) => GATE_NAMES.reduce((w, gate) => w + GATE_KEYS[gate].length + (unicode ? 1 : 3) + 1, -1);
|
|
67
63
|
const shortGoal = (goal, max) => {
|
|
@@ -159,61 +155,62 @@ const renderFrame = (cwd) => {
|
|
|
159
155
|
const assignCol = contexts.has(t.id) ? `${channel}${divider}ctx ${contexts.get(t.id)}` : channel;
|
|
160
156
|
return { t, st, label, assignCol, states: comparable ? gateStates(t, events) : defaultGateStates(t) };
|
|
161
157
|
});
|
|
162
|
-
const statusColor = (st) => st === "done" ? 32 : st === "failed" ? 31 : st === "human" ? 33 : 36;
|
|
163
158
|
if (!unicode) {
|
|
164
159
|
// machine/CI surface — layout unchanged (pipes, greps, and the golden pins depend on it)
|
|
165
160
|
const rows = cells.map(({ t, st, label, assignCol, states }) => {
|
|
166
161
|
const chain = gateChain(states, false);
|
|
167
|
-
const prefix = ` ${taskBox(st
|
|
162
|
+
const prefix = ` ${taskBox(st)} ${t.id} `;
|
|
168
163
|
const suffix = ` ${chain} ${String(st)}${label} ${assignCol}`;
|
|
169
164
|
return `${prefix}${shortGoal(t.goal, Math.max(0, width - prefix.length - suffix.length))}${suffix}`;
|
|
170
165
|
});
|
|
171
166
|
const header = runId
|
|
172
167
|
? `tickmarkr status${divider}run ${runId}${!comparable ? `${divider}${NOT_COMPARABLE_NOTICE}` : ""}${divider}${liveness(events).replaceAll(" · ", divider)}${divider}${done}/${g.tasks.length} done`
|
|
173
168
|
: `tickmarkr status${divider}no runs yet${divider}${done}/${g.tasks.length} done`;
|
|
174
|
-
const
|
|
175
|
-
return [header,
|
|
169
|
+
const legendLine = ` gates: ${GATE_NAMES.map((gate) => `${GATE_KEYS[gate]} ${gate}`).join(divider)}`;
|
|
170
|
+
return [header, legendLine, ...rows].join("\n");
|
|
176
171
|
}
|
|
177
|
-
// TTY:
|
|
178
|
-
//
|
|
179
|
-
// zero display width and would corrupt
|
|
180
|
-
|
|
172
|
+
// TTY: cockpit frame composed through src/brand.ts (CLI-DESIGN.md) — dominant run title,
|
|
173
|
+
// dim chrome, semantic color only on verdicts, completion gauge, and column-aligned status
|
|
174
|
+
// rows (pad plain text FIRST, colorize after: ANSI has zero display width and would corrupt
|
|
175
|
+
// padEnd math)
|
|
181
176
|
const dot = dim(" · ");
|
|
182
177
|
const anyFailed = cells.some((c) => c.st === "failed");
|
|
183
178
|
const gaugeCells = 10;
|
|
184
179
|
const fill = g.tasks.length ? Math.round((done / g.tasks.length) * gaugeCells) : 0;
|
|
185
|
-
const gauge = (fill ?
|
|
180
|
+
const gauge = (fill ? (anyFailed ? fail : ok)("█".repeat(fill)) : "") + (fill < gaugeCells ? dim("░".repeat(gaugeCells - fill)) : "");
|
|
186
181
|
const live = liveness(events)
|
|
187
|
-
.replace(/\bdead\b/,
|
|
188
|
-
.replace(/\bfinished\b/,
|
|
189
|
-
.replace(/\balive\b/,
|
|
182
|
+
.replace(/\bdead\b/, fail("dead"))
|
|
183
|
+
.replace(/\bfinished\b/, dim("finished"))
|
|
184
|
+
.replace(/\balive\b/, ok("alive"))
|
|
190
185
|
.replaceAll(" · ", dot);
|
|
191
186
|
const tally = `${done}/${g.tasks.length} done`;
|
|
192
|
-
const header = ` ${
|
|
187
|
+
const header = ` ${title(runId ? `run ${runId}` : "tickmarkr")}${dot}` +
|
|
193
188
|
(runId
|
|
194
|
-
?
|
|
189
|
+
? `${!comparable ? `${warn(NOT_COMPARABLE_NOTICE)}${dot}` : ""}${live}${dot}`
|
|
195
190
|
: `no runs yet${dot}`) +
|
|
196
|
-
`${gauge} ${done === g.tasks.length && g.tasks.length > 0 ?
|
|
197
|
-
const
|
|
198
|
-
const
|
|
191
|
+
`${gauge} ${done === g.tasks.length && g.tasks.length > 0 ? ok(tally) : tally}`;
|
|
192
|
+
const hr = rule(Math.min(width, 100));
|
|
193
|
+
const gatesLegend = legend(` gates: ${GATE_NAMES.map((gate) => `${GATE_KEYS[gate]} ${gate}`).join(" · ")}`);
|
|
194
|
+
const taskVerdict = (st) => st === "done" ? "pass" : st === "failed" ? "fail" : st === "human" ? "warn" : "neutral";
|
|
199
195
|
const idW = Math.max(...cells.map((c) => c.t.id.length), 2);
|
|
200
196
|
const stW = Math.max(...cells.map((c) => (String(c.st) + c.label).length));
|
|
201
197
|
const chainW = gateChainWidth(true);
|
|
202
198
|
const assignW = Math.max(...cells.map((c) => c.assignCol.length));
|
|
203
199
|
const goalW = Math.max(8, width - (5 + idW) - 2 - chainW - 2 - stW - 2 - assignW);
|
|
204
200
|
const rows = cells.map(({ t, st, label, assignCol, states }) => {
|
|
205
|
-
const box = color(taskBox(st, true), st === "done" ? 32 : st === "failed" ? 31 : st === "human" ? 33 : 2, true);
|
|
206
201
|
const goal = shortGoal(t.goal, goalW).padEnd(goalW);
|
|
207
|
-
const
|
|
208
|
-
|
|
202
|
+
const stWord = st === "done" ? ok(String(st)) : st === "failed" ? fail(String(st)) : st === "human" ? warn(String(st)) : String(st);
|
|
203
|
+
const statusCell = stWord +
|
|
204
|
+
(label ? (label === " starved" ? fail(label) : dim(label)) : "") +
|
|
209
205
|
" ".repeat(stW - (String(st) + label).length);
|
|
210
|
-
return ` ${
|
|
206
|
+
return ` ${statusRow(taskVerdict(st), `${t.id.padEnd(idW)} ${goal} ${gateChain(states, true)} ${statusCell} ${dim(assignCol)}`)}`;
|
|
211
207
|
});
|
|
212
|
-
return [header,
|
|
208
|
+
return [header, hr, gatesLegend, ...rows].join("\n");
|
|
213
209
|
};
|
|
214
210
|
export async function status(argv, cwd = process.cwd(), opts = {}) {
|
|
211
|
+
// cockpit surface: banner + frame on a TTY (doctor's pattern); pipes get the bare frame
|
|
215
212
|
if (!argv.includes("--watch"))
|
|
216
|
-
return renderFrame(cwd);
|
|
213
|
+
return visual() ? BANNER + renderFrame(cwd) : renderFrame(cwd);
|
|
217
214
|
const iterations = opts.iterations ?? Infinity;
|
|
218
215
|
const sleep = opts.sleep ?? defaultSleep;
|
|
219
216
|
const bounded = Number.isFinite(iterations);
|
|
@@ -223,7 +220,7 @@ export async function status(argv, cwd = process.cwd(), opts = {}) {
|
|
|
223
220
|
for (let i = 0; i < iterations; i++) {
|
|
224
221
|
const frame = renderFrame(cwd);
|
|
225
222
|
if (tty)
|
|
226
|
-
process.stdout.write(`\x1b[2J\x1b[H${BANNER}${frame}\n
|
|
223
|
+
process.stdout.write(`\x1b[2J\x1b[H${BANNER}${frame}\n${legend(` watching · refresh ${REFRESH_MS / 1000}s · ^C to quit`)}`);
|
|
227
224
|
else
|
|
228
225
|
process.stdout.write(frame + sep);
|
|
229
226
|
if (bounded)
|
package/dist/compile/native.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { existsSync, readFileSync } from "node:fs";
|
|
2
|
-
import { GATE_NAMES, ORACLES, SHAPES, TIERS, validateGraph } from "../graph/schema.js";
|
|
2
|
+
import { GATE_NAMES, GRAPH_ROUTING_MODES, ORACLES, SHAPES, TIERS, validateGraph } from "../graph/schema.js";
|
|
3
3
|
import { CompileError, inferShape, sha256 } from "./common.js";
|
|
4
4
|
export const LEGACY_PREFIX = ["dro", "vr"].join("");
|
|
5
5
|
export const TICKMARKR_NATIVE_MARKER = /^<!--\s*tickmarkr:spec(?:\s+v1)?\s*-->\s*$/m;
|
|
@@ -20,6 +20,7 @@ export function compileNative(file) {
|
|
|
20
20
|
const content = readFileSync(file, "utf8");
|
|
21
21
|
const drafts = [];
|
|
22
22
|
let plainCount = 0; // v1.19: plain-string acceptance items compiled as judge oracles (compat) — warn once
|
|
23
|
+
let specMode;
|
|
23
24
|
for (const line of content.split("\n")) {
|
|
24
25
|
const heading = line.match(HEAD_RE);
|
|
25
26
|
if (heading) {
|
|
@@ -27,8 +28,18 @@ export function compileNative(file) {
|
|
|
27
28
|
continue;
|
|
28
29
|
}
|
|
29
30
|
const draft = drafts.at(-1);
|
|
30
|
-
if (!draft)
|
|
31
|
+
if (!draft) {
|
|
32
|
+
// v1.51 T2: spec front-matter — a top-level `mode: <name>` line before the first task heading
|
|
33
|
+
// declares the engagement's routing mode (loses only to an explicit run flag).
|
|
34
|
+
const fm = line.match(/^mode:\s*(\S+)\s*$/);
|
|
35
|
+
if (fm) {
|
|
36
|
+
if (!GRAPH_ROUTING_MODES.includes(fm[1])) {
|
|
37
|
+
throw new CompileError(`spec front-matter mode must be one of ${GRAPH_ROUTING_MODES.join(", ")} (got ${JSON.stringify(fm[1])})`);
|
|
38
|
+
}
|
|
39
|
+
specMode = fm[1];
|
|
40
|
+
}
|
|
31
41
|
continue;
|
|
42
|
+
}
|
|
32
43
|
const field = line.match(FIELD_RE);
|
|
33
44
|
if (field) {
|
|
34
45
|
const name = field[1].toLowerCase();
|
|
@@ -151,6 +162,7 @@ export function compileNative(file) {
|
|
|
151
162
|
});
|
|
152
163
|
const result = validateGraph({
|
|
153
164
|
version: 1,
|
|
165
|
+
...(specMode ? { mode: specMode } : {}),
|
|
154
166
|
spec: { source: "native", paths: [file], hash: sha256(content) },
|
|
155
167
|
tasks,
|
|
156
168
|
});
|
|
@@ -184,6 +196,10 @@ Each task is a "## Tn: Title" heading with "- field: value" bullets.
|
|
|
184
196
|
acceptance is required on every task (a nested list of observable outcomes).
|
|
185
197
|
|
|
186
198
|
<!--
|
|
199
|
+
Spec front-matter (top-level, before the first task heading):
|
|
200
|
+
mode: partner-led | risk-based | staff-led — this engagement's routing mode
|
|
201
|
+
(loses only to an explicit \`run --mode\` flag)
|
|
202
|
+
|
|
187
203
|
Fields available per task:
|
|
188
204
|
goal: outcome the task must achieve (defaults to the title if omitted)
|
|
189
205
|
shape: plan | spec | implement | tests | docs | migration | ui | refactor | chore
|
package/dist/config/config.d.ts
CHANGED
|
@@ -7,6 +7,14 @@ declare const TierEnum: z.ZodEnum<{
|
|
|
7
7
|
export type Tier = z.infer<typeof TierEnum>;
|
|
8
8
|
export declare const TIER_RANK: Record<Tier, number>;
|
|
9
9
|
export declare const DEFAULT_DIFF_CAP = 60000;
|
|
10
|
+
export declare const ROUTING_MODES: readonly ["partner-led", "risk-based", "staff-led"];
|
|
11
|
+
declare const ModeEnum: z.ZodEnum<{
|
|
12
|
+
"partner-led": "partner-led";
|
|
13
|
+
"risk-based": "risk-based";
|
|
14
|
+
"staff-led": "staff-led";
|
|
15
|
+
}>;
|
|
16
|
+
export type RoutingMode = z.infer<typeof ModeEnum>;
|
|
17
|
+
export declare const INTEGRITY_FLOOR_SHAPES: readonly ["plan", "spec", "migration", "ui"];
|
|
10
18
|
export declare const MapEntrySchema: z.ZodObject<{
|
|
11
19
|
pin: z.ZodOptional<z.ZodObject<{
|
|
12
20
|
via: z.ZodString;
|
|
@@ -60,6 +68,11 @@ export declare const TickmarkrConfigSchema: z.ZodObject<{
|
|
|
60
68
|
contextWarnTokens: z.ZodNumber;
|
|
61
69
|
setup: z.ZodOptional<z.ZodString>;
|
|
62
70
|
routing: z.ZodObject<{
|
|
71
|
+
mode: z.ZodOptional<z.ZodEnum<{
|
|
72
|
+
"partner-led": "partner-led";
|
|
73
|
+
"risk-based": "risk-based";
|
|
74
|
+
"staff-led": "staff-led";
|
|
75
|
+
}>>;
|
|
63
76
|
map: z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
64
77
|
pin: z.ZodOptional<z.ZodObject<{
|
|
65
78
|
via: z.ZodString;
|
|
@@ -195,6 +208,21 @@ export declare function globalConfigDir(): string;
|
|
|
195
208
|
export declare function overlayPreferShapes(repoRoot: string, opts?: {
|
|
196
209
|
globalDir?: string;
|
|
197
210
|
}): ReadonlySet<string>;
|
|
211
|
+
export type ModeResolution = {
|
|
212
|
+
mode: RoutingMode;
|
|
213
|
+
/** floor shape → "mode <name>" | "config floors" */
|
|
214
|
+
provenance: Record<string, string>;
|
|
215
|
+
/** plan lints: shadowed mode deltas + operator floors below the integrity minimum (standing) */
|
|
216
|
+
lints: string[];
|
|
217
|
+
};
|
|
218
|
+
/** loadConfig plus the mode-resolution record (floor provenance + plan lints). The resolved floors are
|
|
219
|
+
* already applied to cfg.routing.floors — route() consumes floors only and never sees the mode. */
|
|
220
|
+
export declare function loadConfigWithMode(repoRoot: string, opts?: {
|
|
221
|
+
globalDir?: string;
|
|
222
|
+
}): {
|
|
223
|
+
cfg: TickmarkrConfig;
|
|
224
|
+
mode: ModeResolution;
|
|
225
|
+
};
|
|
198
226
|
export declare function loadConfig(repoRoot: string, opts?: {
|
|
199
227
|
globalDir?: string;
|
|
200
228
|
}): TickmarkrConfig;
|