tickmarkr 1.46.0 → 1.47.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 +19 -0
- package/dist/adapters/model-lints.js +74 -1
- package/dist/cli/commands/doctor.js +6 -2
- package/dist/cli/commands/fleet.d.ts +5 -0
- package/dist/cli/commands/fleet.js +249 -0
- package/dist/cli/commands/plan.js +7 -1
- package/dist/cli/commands/run.js +34 -17
- package/dist/cli/index.d.ts +1 -1
- package/dist/cli/index.js +3 -1
- package/dist/compile/native.js +16 -1
- package/dist/config/config.d.ts +42 -0
- package/dist/config/config.js +288 -1
- package/dist/gates/acceptance.js +7 -2
- package/dist/gates/scope.d.ts +2 -1
- package/dist/gates/scope.js +11 -1
- package/dist/route/profile.d.ts +3 -1
- package/dist/route/profile.js +5 -3
- package/dist/route/router.d.ts +9 -2
- package/dist/route/router.js +72 -14
- package/dist/run/daemon.js +1 -1
- package/package.json +1 -1
|
@@ -1,8 +1,21 @@
|
|
|
1
1
|
import { type TickmarkrConfig } from "../config/config.js";
|
|
2
|
+
import type { Task } from "../graph/schema.js";
|
|
2
3
|
import { type AuthHealth, type WorkerAdapter } from "./types.js";
|
|
3
4
|
export declare const SEED_STAMPED = "2026-07-09";
|
|
4
5
|
export declare const MODEL_STALE_DAYS = 30;
|
|
5
6
|
export declare const ttyVisual: () => boolean;
|
|
7
|
+
/** True when any tier entry declares at least one model window. */
|
|
8
|
+
export declare function hasWindowsConfig(cfg: TickmarkrConfig): boolean;
|
|
9
|
+
export declare function declaredModelWindow(cfg: TickmarkrConfig, adapter: string, model: string): number | undefined;
|
|
10
|
+
/** Best-effort plan-time payload estimate: prompt shell + context + files[] byte sizes. */
|
|
11
|
+
export declare function estimateTaskPayloadTokens(task: Task, repoRoot: string, feedback?: string): number;
|
|
12
|
+
export type RoutedAssignment = {
|
|
13
|
+
taskId: string;
|
|
14
|
+
adapter: string;
|
|
15
|
+
model: string;
|
|
16
|
+
};
|
|
17
|
+
/** Advisory only — absent windows config or undeclared model window ⇒ no lint. */
|
|
18
|
+
export declare function contextWindowLints(tasks: ReadonlyArray<Task>, assignments: ReadonlyArray<RoutedAssignment>, cfg: TickmarkrConfig, repoRoot: string): string[];
|
|
6
19
|
export declare function seedPreferLints(cfg: TickmarkrConfig, health: Record<string, AuthHealth>, adapters: WorkerAdapter[], overlayPreferShapes?: ReadonlySet<string>): string[];
|
|
7
20
|
export declare function modelLints(cfg: TickmarkrConfig, health: Record<string, AuthHealth>, adapters: WorkerAdapter[], opts?: {
|
|
8
21
|
tty?: boolean;
|
|
@@ -15,3 +28,9 @@ export declare function formatModelAuthLine(excluded: {
|
|
|
15
28
|
probedAt: string;
|
|
16
29
|
}[], tty?: boolean, stateDir?: string): string;
|
|
17
30
|
export declare function suggestOverlay(cfg: TickmarkrConfig, health: Record<string, AuthHealth>, adapters: WorkerAdapter[], stateDir?: string): string;
|
|
31
|
+
/** Unclassified models surfaced for fleet screen 2 (doctor matrix math, no tier fabrication). */
|
|
32
|
+
export declare function fleetUnclassifiedModels(cfg: TickmarkrConfig, health: Record<string, AuthHealth>, adapters: WorkerAdapter[]): {
|
|
33
|
+
adapter: string;
|
|
34
|
+
model: string;
|
|
35
|
+
detectedAt?: string;
|
|
36
|
+
}[];
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import { existsSync, statSync } from "node:fs";
|
|
2
|
+
import { join } from "node:path";
|
|
1
3
|
import { DEFAULT_CONFIG, TIER_RANK } from "../config/config.js";
|
|
4
|
+
import { buildTaskPrompt } from "./prompt.js";
|
|
2
5
|
import { MODEL_ID_RE } from "./types.js";
|
|
3
|
-
// date the tiers seeds were last live-verified (Phase 6/7 reseed) — surfaced when an adapter has no list surface.
|
|
4
6
|
export const SEED_STAMPED = "2026-07-09";
|
|
5
7
|
// knowledge past this age gets a "rerun tickmarkr doctor" nudge (BLOCKED_POLL_MS-style named constant).
|
|
6
8
|
export const MODEL_STALE_DAYS = 30;
|
|
@@ -14,6 +16,57 @@ const TTY_LINT_CAP = 3;
|
|
|
14
16
|
const DEFAULT_STATE_DIR = ".tickmarkr";
|
|
15
17
|
const doctorJsonRef = (stateDir) => ` — see ${stateDir}/doctor.json`;
|
|
16
18
|
export const ttyVisual = () => process.stdout.isTTY === true && process.env.NO_COLOR === undefined;
|
|
19
|
+
// ponytail: chars/4 token heuristic — good enough for advisory plan lint; no tokenizer dep.
|
|
20
|
+
const CHARS_PER_TOKEN = 4;
|
|
21
|
+
/** True when any tier entry declares at least one model window. */
|
|
22
|
+
export function hasWindowsConfig(cfg) {
|
|
23
|
+
return Object.values(cfg.tiers).some((t) => t.windows && Object.keys(t.windows).length > 0);
|
|
24
|
+
}
|
|
25
|
+
export function declaredModelWindow(cfg, adapter, model) {
|
|
26
|
+
return cfg.tiers[adapter]?.windows?.[model];
|
|
27
|
+
}
|
|
28
|
+
function fileBytes(repoRoot, rel) {
|
|
29
|
+
if (rel.includes("*") || rel.includes("?") || rel.includes("{"))
|
|
30
|
+
return 0; // glob — not measurable at plan time
|
|
31
|
+
try {
|
|
32
|
+
const p = join(repoRoot, rel);
|
|
33
|
+
if (!existsSync(p))
|
|
34
|
+
return 0;
|
|
35
|
+
return statSync(p).size;
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return 0;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
/** Best-effort plan-time payload estimate: prompt shell + context + files[] byte sizes. */
|
|
42
|
+
export function estimateTaskPayloadTokens(task, repoRoot, feedback = "") {
|
|
43
|
+
let bytes = buildTaskPrompt(task, feedback).length;
|
|
44
|
+
for (const p of task.context)
|
|
45
|
+
bytes += fileBytes(repoRoot, p);
|
|
46
|
+
for (const p of task.files)
|
|
47
|
+
bytes += fileBytes(repoRoot, p);
|
|
48
|
+
return Math.ceil(bytes / CHARS_PER_TOKEN);
|
|
49
|
+
}
|
|
50
|
+
/** Advisory only — absent windows config or undeclared model window ⇒ no lint. */
|
|
51
|
+
export function contextWindowLints(tasks, assignments, cfg, repoRoot) {
|
|
52
|
+
if (!hasWindowsConfig(cfg))
|
|
53
|
+
return [];
|
|
54
|
+
const byId = new Map(assignments.map((a) => [a.taskId, a]));
|
|
55
|
+
const lints = [];
|
|
56
|
+
for (const t of tasks) {
|
|
57
|
+
const a = byId.get(t.id);
|
|
58
|
+
if (!a)
|
|
59
|
+
continue;
|
|
60
|
+
const window = declaredModelWindow(cfg, a.adapter, a.model);
|
|
61
|
+
if (window === undefined)
|
|
62
|
+
continue;
|
|
63
|
+
const est = estimateTaskPayloadTokens(t, repoRoot);
|
|
64
|
+
if (est > window) {
|
|
65
|
+
lints.push(`${t.id}: payload ~${est} tokens exceeds ${a.adapter}:${a.model} window ${window}`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return lints;
|
|
69
|
+
}
|
|
17
70
|
const adapterHasAuthedChannel = (adapterId, shape, cfg, health, adapters) => {
|
|
18
71
|
const entry = cfg.routing.map[shape];
|
|
19
72
|
const minTier = entry?.tier ?? cfg.routing.floors[shape] ?? "cheap";
|
|
@@ -175,3 +228,23 @@ function referenceWarning(cfg, adapterId, model) {
|
|
|
175
228
|
refs.push("consult");
|
|
176
229
|
return refs.length ? ` # WARNING: still referenced by ${refs.join(", ")} — remap before removing` : "";
|
|
177
230
|
}
|
|
231
|
+
/** Unclassified models surfaced for fleet screen 2 (doctor matrix math, no tier fabrication). */
|
|
232
|
+
export function fleetUnclassifiedModels(cfg, health, adapters) {
|
|
233
|
+
const out = [];
|
|
234
|
+
for (const id of Object.keys(cfg.tiers)) {
|
|
235
|
+
if (!adapters.some((a) => a.id === id))
|
|
236
|
+
continue;
|
|
237
|
+
const h = health[id];
|
|
238
|
+
const detected = h?.models ?? [];
|
|
239
|
+
if (!detected.length)
|
|
240
|
+
continue;
|
|
241
|
+
const configured = new Set(Object.keys(cfg.tiers[id].models));
|
|
242
|
+
const date = h?.modelsDetectedAt?.split("T")[0];
|
|
243
|
+
for (const model of detected) {
|
|
244
|
+
if (configured.has(model) || LINT_VARIANT_RE.test(model))
|
|
245
|
+
continue;
|
|
246
|
+
out.push({ adapter: id, model, detectedAt: date });
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
return out;
|
|
250
|
+
}
|
|
@@ -3,7 +3,7 @@ import { join } from "node:path";
|
|
|
3
3
|
import { allAdapters, probeAll, probeModels, readAutoPrefer, servableExclusions, servabilityLine, writeDoctor } from "../../adapters/registry.js";
|
|
4
4
|
import { BANNER } from "../../brand.js";
|
|
5
5
|
import { tickmarkrDir, stateDirName } from "../../graph/graph.js";
|
|
6
|
-
import { modelLints, suggestOverlay, ttyVisual } from "../../adapters/model-lints.js";
|
|
6
|
+
import { declaredModelWindow, hasWindowsConfig, modelLints, suggestOverlay, ttyVisual } from "../../adapters/model-lints.js";
|
|
7
7
|
import { DEFAULT_CONFIG, loadConfig, overlayPreferShapes } from "../../config/config.js";
|
|
8
8
|
import { HerdrDriver } from "../../drivers/herdr.js";
|
|
9
9
|
import { disallowedBy, excludedChannels, exclusionLine, preferRanks } from "../../route/preference.js";
|
|
@@ -121,6 +121,7 @@ export async function doctor(_argv, cwd = process.cwd(), adapters = allAdapters(
|
|
|
121
121
|
// per adapter — they aren't routable, so they don't earn table rows.
|
|
122
122
|
const trunc = (s, n) => (s.length <= n ? s : `${s.slice(0, n - 1)}…`);
|
|
123
123
|
const dateOf = (iso) => iso.slice(0, 10);
|
|
124
|
+
const showWindows = hasWindowsConfig(cfg);
|
|
124
125
|
const modelStatus = adapters.flatMap((a) => {
|
|
125
126
|
const h = health[a.id];
|
|
126
127
|
if (!h?.installed)
|
|
@@ -138,7 +139,10 @@ export async function doctor(_argv, cwd = process.cwd(), adapters = allAdapters(
|
|
|
138
139
|
const d = disallowedBy({ adapter: a.id, model: m }, cfg.routing);
|
|
139
140
|
const denied = d?.by === "deny" ? d.entry : "—";
|
|
140
141
|
const pref = preferRanks({ adapter: a.id, model: m }, cfg).map((p) => `${p.shape}#${p.rank}`).join(",") || "—";
|
|
141
|
-
|
|
142
|
+
const windowCol = showWindows
|
|
143
|
+
? ` ${String(declaredModelWindow(cfg, a.id, m) ?? "—").padEnd(8)}`
|
|
144
|
+
: "";
|
|
145
|
+
rows.push(` ${m.padEnd(w)} ${classified[m].padEnd(8)}${windowCol} ${auth} denied=${denied} prefer=${pref}`);
|
|
142
146
|
}
|
|
143
147
|
if (unclassified.length)
|
|
144
148
|
rows.push(` (${unclassified.length} more listed, unclassified)`);
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname } from "node:path";
|
|
3
|
+
import { createInterface } from "node:readline/promises";
|
|
4
|
+
import { parseArgs } from "node:util";
|
|
5
|
+
import { allAdapters, discoverChannels, doctorAgeMs, initDoctorReuse, readAutoPrefer } from "../../adapters/registry.js";
|
|
6
|
+
import { fleetUnclassifiedModels } from "../../adapters/model-lints.js";
|
|
7
|
+
import { fleetEditableFromConfig, fleetEditableEquals, fleetRepoOverlayFromDelta, formatFleetPrint, globalConfigDir, loadConfig, overlayPreferShapes, readOverlayFile, repoOverlayPath, repoOverlayYaml, unifiedYamlDiff, } from "../../config/config.js";
|
|
8
|
+
import { SHAPES, TIERS } from "../../graph/schema.js";
|
|
9
|
+
import { route } from "../../route/router.js";
|
|
10
|
+
import { loadRoutingProfile } from "../../run/journal.js";
|
|
11
|
+
const NON_TTY_MSG = "tickmarkr fleet: interactive fleet editor requires a TTY — use `tickmarkr fleet --print` for non-interactive output";
|
|
12
|
+
const isDeniedAdapter = (id, editable) => editable.denyAdapters.includes(id);
|
|
13
|
+
const isDeniedModel = (adapter, model, editable) => editable.denyModels.includes(`${adapter}:${model}`);
|
|
14
|
+
function previewTask(shape) {
|
|
15
|
+
return {
|
|
16
|
+
id: "fleet-preview",
|
|
17
|
+
title: "fleet preview",
|
|
18
|
+
goal: "preview",
|
|
19
|
+
shape,
|
|
20
|
+
complexity: 3,
|
|
21
|
+
acceptance: ["done"],
|
|
22
|
+
deps: [],
|
|
23
|
+
files: [],
|
|
24
|
+
context: [],
|
|
25
|
+
gates: ["build", "test", "lint", "evidence", "scope", "acceptance", "review"],
|
|
26
|
+
humanGate: false,
|
|
27
|
+
status: "pending",
|
|
28
|
+
evidence: { commits: [], artifacts: [], gateResults: [] },
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
function formatAge(ageMs) {
|
|
32
|
+
if (ageMs === null)
|
|
33
|
+
return "no probe data";
|
|
34
|
+
const mins = Math.floor(ageMs / 60_000);
|
|
35
|
+
if (mins < 60)
|
|
36
|
+
return `${mins}m old`;
|
|
37
|
+
return `${Math.floor(mins / 60)}h old`;
|
|
38
|
+
}
|
|
39
|
+
function currentRepoOverlayText(repoRoot) {
|
|
40
|
+
const p = repoOverlayPath(repoRoot);
|
|
41
|
+
return existsSync(p) ? readFileSync(p, "utf8") : "";
|
|
42
|
+
}
|
|
43
|
+
function provenanceMap(editable) {
|
|
44
|
+
const out = {};
|
|
45
|
+
for (const [adapter, models] of Object.entries(editable.tiers)) {
|
|
46
|
+
for (const [model, v] of Object.entries(models)) {
|
|
47
|
+
if (v?.provenance) {
|
|
48
|
+
out[adapter] ??= {};
|
|
49
|
+
out[adapter][model] = v.provenance;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return out;
|
|
54
|
+
}
|
|
55
|
+
function proposedOverlayText(initial, editable, repoRoot) {
|
|
56
|
+
const merged = fleetRepoOverlayFromDelta(initial, editable, readOverlayFile(repoOverlayPath(repoRoot)));
|
|
57
|
+
return repoOverlayYaml(merged, provenanceMap(editable));
|
|
58
|
+
}
|
|
59
|
+
async function ask(rl, prompt) {
|
|
60
|
+
return (await rl.question(prompt)).trim();
|
|
61
|
+
}
|
|
62
|
+
export async function fleet(argv, cwd = process.cwd(), adapters = allAdapters()) {
|
|
63
|
+
const { values } = parseArgs({
|
|
64
|
+
args: argv,
|
|
65
|
+
options: {
|
|
66
|
+
print: { type: "boolean" },
|
|
67
|
+
"global-dir": { type: "string" },
|
|
68
|
+
fresh: { type: "boolean" },
|
|
69
|
+
},
|
|
70
|
+
});
|
|
71
|
+
const globalDir = values["global-dir"] ?? globalConfigDir();
|
|
72
|
+
const print = values.print ?? false;
|
|
73
|
+
const interactive = process.stdin.isTTY === true && process.stdout.isTTY === true;
|
|
74
|
+
if (print)
|
|
75
|
+
return formatFleetPrint(cwd, { globalDir });
|
|
76
|
+
if (!interactive)
|
|
77
|
+
return { out: NON_TTY_MSG, code: 1 };
|
|
78
|
+
const fresh = values.fresh ?? false;
|
|
79
|
+
const { reuse, health: cached } = initDoctorReuse(cwd, fresh);
|
|
80
|
+
if (!reuse || !cached) {
|
|
81
|
+
return {
|
|
82
|
+
out: "tickmarkr fleet: probe data missing or stale — run `tickmarkr doctor` first (fleet never re-probes; doctor is the sensor)",
|
|
83
|
+
code: 1,
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
const cfg = loadConfig(cwd, { globalDir });
|
|
87
|
+
const initial = fleetEditableFromConfig(cfg);
|
|
88
|
+
const editable = structuredClone(initial);
|
|
89
|
+
const health = cached;
|
|
90
|
+
const rl = createInterface({ input: process.stdin, output: process.stdout });
|
|
91
|
+
try {
|
|
92
|
+
// screen 0 — sensor freshness
|
|
93
|
+
const ageLine = formatAge(doctorAgeMs(cwd));
|
|
94
|
+
let step = await ask(rl, `tickmarkr fleet — scope your fleet\n probe data: ${ageLine} (.tickmarkr/doctor.json)\n [Enter] use it [r] refresh via doctor [q] quit\n> `);
|
|
95
|
+
if (/^q$/i.test(step))
|
|
96
|
+
return "fleet: quit without writing";
|
|
97
|
+
if (/^r$/i.test(step)) {
|
|
98
|
+
return "fleet: run `tickmarkr doctor` to refresh probe data, then re-run `tickmarkr fleet` (doctor is the sensor; fleet never re-probes)";
|
|
99
|
+
}
|
|
100
|
+
// screen 1 — adapter membership (deny adapters)
|
|
101
|
+
const installed = adapters.filter((a) => health[a.id]?.installed);
|
|
102
|
+
const adapterLines = installed.map((a, i) => {
|
|
103
|
+
const h = health[a.id];
|
|
104
|
+
const on = !isDeniedAdapter(a.id, editable);
|
|
105
|
+
const mark = on ? "x" : " ";
|
|
106
|
+
const auth = h?.authed ? "authed" : "unauthed";
|
|
107
|
+
return ` ${i + 1}. [${mark}] ${a.id} ${h?.version ?? "installed"} ${auth}`;
|
|
108
|
+
});
|
|
109
|
+
step = await ask(rl, `screen 1/4 — agent CLIs [number]=toggle [Enter]=next\n${adapterLines.join("\n")}\n> `);
|
|
110
|
+
if (/^q$/i.test(step))
|
|
111
|
+
return "fleet: quit without writing";
|
|
112
|
+
if (step && step !== "") {
|
|
113
|
+
const n = Number.parseInt(step, 10);
|
|
114
|
+
if (Number.isInteger(n) && n >= 1 && n <= installed.length) {
|
|
115
|
+
const id = installed[n - 1].id;
|
|
116
|
+
const idx = editable.denyAdapters.indexOf(id);
|
|
117
|
+
if (idx === -1)
|
|
118
|
+
editable.denyAdapters.push(id);
|
|
119
|
+
else
|
|
120
|
+
editable.denyAdapters.splice(idx, 1);
|
|
121
|
+
editable.denyAdapters.sort();
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
// screen 2 — models per enabled adapter
|
|
125
|
+
const enabledAdapters = installed.filter((a) => !isDeniedAdapter(a.id, editable));
|
|
126
|
+
for (const a of enabledAdapters) {
|
|
127
|
+
const classified = editable.tiers[a.id] ?? {};
|
|
128
|
+
const models = Object.keys(classified);
|
|
129
|
+
const unclassified = fleetUnclassifiedModels(cfg, health, adapters).filter((u) => u.adapter === a.id);
|
|
130
|
+
const rows = [];
|
|
131
|
+
let row = 1;
|
|
132
|
+
const indexFor = new Map();
|
|
133
|
+
for (const m of models) {
|
|
134
|
+
const tier = classified[m];
|
|
135
|
+
const denied = isDeniedModel(a.id, m, editable) ? "deny" : "allow";
|
|
136
|
+
const mark = denied === "deny" ? " " : "x";
|
|
137
|
+
rows.push(` ${row}. [${mark}] ${m} ${tier?.tier ?? "???"} ${denied === "deny" ? "denied" : "allowed"}`);
|
|
138
|
+
indexFor.set(String(row), { kind: "classified", model: m });
|
|
139
|
+
row++;
|
|
140
|
+
}
|
|
141
|
+
for (const u of unclassified) {
|
|
142
|
+
rows.push(` ${row}. ( ) ${u.model} ??? unclassified${u.detectedAt ? ` (${u.detectedAt})` : ""}`);
|
|
143
|
+
indexFor.set(String(row), { kind: "unclassified", model: u.model });
|
|
144
|
+
row++;
|
|
145
|
+
}
|
|
146
|
+
step = await ask(rl, `screen 2/4 — models · ${a.id} [number]=toggle deny [t <n> <tier>]=assign tier [Enter]=next\n${rows.join("\n")}\n> `);
|
|
147
|
+
if (/^q$/i.test(step))
|
|
148
|
+
return "fleet: quit without writing";
|
|
149
|
+
if (step.startsWith("t ")) {
|
|
150
|
+
const [, numRaw, tierRaw] = step.split(/\s+/);
|
|
151
|
+
const tier = tierRaw;
|
|
152
|
+
if (!indexFor.has(numRaw) || !TIERS.includes(tier)) {
|
|
153
|
+
return { out: "fleet: tier assignment requires a valid model number and tier (cheap|mid|frontier)", code: 1 };
|
|
154
|
+
}
|
|
155
|
+
const target = indexFor.get(numRaw);
|
|
156
|
+
if (target.kind !== "unclassified") {
|
|
157
|
+
return { out: "fleet: tier reassignment on classified models is not supported in v1 — edit config directly", code: 1 };
|
|
158
|
+
}
|
|
159
|
+
const note = await ask(rl, "benchmark provenance note (required): ");
|
|
160
|
+
if (!note.trim())
|
|
161
|
+
return { out: "fleet: assigning a tier to an unclassified model requires a typed benchmark-provenance note", code: 1 };
|
|
162
|
+
editable.tiers[a.id] ??= {};
|
|
163
|
+
editable.tiers[a.id][target.model] = { tier, provenance: note.trim() };
|
|
164
|
+
}
|
|
165
|
+
else if (step && /^\d+$/.test(step)) {
|
|
166
|
+
const target = indexFor.get(step);
|
|
167
|
+
if (target?.kind === "classified") {
|
|
168
|
+
const key = `${a.id}:${target.model}`;
|
|
169
|
+
const idx = editable.denyModels.indexOf(key);
|
|
170
|
+
if (idx === -1)
|
|
171
|
+
editable.denyModels.push(key);
|
|
172
|
+
else
|
|
173
|
+
editable.denyModels.splice(idx, 1);
|
|
174
|
+
editable.denyModels.sort();
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
// screen 3 — per-shape routing
|
|
179
|
+
const channels = discoverChannels(cfg, adapters, health);
|
|
180
|
+
const profile = loadRoutingProfile(cwd, cfg, { preview: true });
|
|
181
|
+
const autoPrefer = readAutoPrefer(cwd);
|
|
182
|
+
const overlayShapes = overlayPreferShapes(cwd, { globalDir });
|
|
183
|
+
for (let si = 0; si < SHAPES.length; si++) {
|
|
184
|
+
const shape = SHAPES[si];
|
|
185
|
+
const entry = editable.map[shape] ?? {};
|
|
186
|
+
let now = "";
|
|
187
|
+
try {
|
|
188
|
+
const r = route(previewTask(shape), { ...cfg, routing: { ...cfg.routing, map: editable.map, floors: editable.floors } }, channels, profile);
|
|
189
|
+
now = `${r.provenance}\n → ${r.assignment.adapter}:${r.assignment.model} (${r.assignment.channel}, ${r.assignment.tier})`;
|
|
190
|
+
}
|
|
191
|
+
catch (e) {
|
|
192
|
+
now = e.message;
|
|
193
|
+
}
|
|
194
|
+
const autoNote = autoPrefer?.[shape] && !overlayShapes.has(shape)
|
|
195
|
+
? `\n auto-prefer active — explicit prefer overrides it`
|
|
196
|
+
: "";
|
|
197
|
+
step = await ask(rl, `screen 3/4 — shape routing · ${shape} (${si + 1}/${SHAPES.length})\n now: ${now}\n [1] auto within tier [2] pin channel [3] edit tier [4] edit prefer [Enter]=keep${autoNote}\n> `);
|
|
198
|
+
if (/^q$/i.test(step))
|
|
199
|
+
return "fleet: quit without writing";
|
|
200
|
+
if (!step)
|
|
201
|
+
continue;
|
|
202
|
+
if (step === "1") {
|
|
203
|
+
const next = { ...entry };
|
|
204
|
+
delete next.pin;
|
|
205
|
+
editable.map[shape] = next;
|
|
206
|
+
}
|
|
207
|
+
else if (step === "2") {
|
|
208
|
+
const pin = await ask(rl, "pin adapter:model> ");
|
|
209
|
+
const i = pin.indexOf(":");
|
|
210
|
+
if (i < 1)
|
|
211
|
+
return { out: "fleet: pin must be adapter:model", code: 1 };
|
|
212
|
+
editable.map[shape] = { pin: { via: pin.slice(0, i), model: pin.slice(i + 1) } };
|
|
213
|
+
}
|
|
214
|
+
else if (step === "3") {
|
|
215
|
+
const tier = await ask(rl, `tier (${TIERS.join("|")})> `);
|
|
216
|
+
if (!TIERS.includes(tier))
|
|
217
|
+
return { out: `fleet: invalid tier ${tier}`, code: 1 };
|
|
218
|
+
const next = { ...entry, tier };
|
|
219
|
+
delete next.pin;
|
|
220
|
+
editable.map[shape] = next;
|
|
221
|
+
}
|
|
222
|
+
else if (step === "4") {
|
|
223
|
+
const pref = await ask(rl, "prefer (comma-separated adapters or adapter:model)> ");
|
|
224
|
+
const prefer = pref.split(",").map((s) => s.trim()).filter(Boolean);
|
|
225
|
+
editable.map[shape] = { ...entry, prefer };
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
// review — unified diff + confirm
|
|
229
|
+
const before = currentRepoOverlayText(cwd);
|
|
230
|
+
const after = proposedOverlayText(initial, editable, cwd);
|
|
231
|
+
if (fleetEditableEquals(initial, editable) || before === after) {
|
|
232
|
+
return "fleet: no overlay changes (empty diff)";
|
|
233
|
+
}
|
|
234
|
+
const diff = unifiedYamlDiff(before, after, repoOverlayPath(cwd));
|
|
235
|
+
process.stdout.write(`\n${diff}\n`);
|
|
236
|
+
const confirm = await ask(rl, "write overlay? [y/N] ");
|
|
237
|
+
if (!/^y(?:es)?$/i.test(confirm))
|
|
238
|
+
return "fleet: discarded overlay changes";
|
|
239
|
+
const merged = fleetRepoOverlayFromDelta(initial, editable, readOverlayFile(repoOverlayPath(cwd)));
|
|
240
|
+
const body = repoOverlayYaml(merged, provenanceMap(editable));
|
|
241
|
+
const path = repoOverlayPath(cwd);
|
|
242
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
243
|
+
writeFileSync(path, body);
|
|
244
|
+
return `fleet: wrote ${path}`;
|
|
245
|
+
}
|
|
246
|
+
finally {
|
|
247
|
+
rl.close();
|
|
248
|
+
}
|
|
249
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { allAdapters, discoverChannels, doctorAgeMs, modelAuthExclusions, probeAll, readDoctor, servableExclusions, servabilityLine } from "../../adapters/registry.js";
|
|
2
|
-
import { formatModelAuthLine, modelLints, ttyVisual } from "../../adapters/model-lints.js";
|
|
2
|
+
import { formatModelAuthLine, contextWindowLints, modelLints, ttyVisual } from "../../adapters/model-lints.js";
|
|
3
3
|
import { collateralLints } from "../../compile/collateral.js";
|
|
4
4
|
import { loadConfig } from "../../config/config.js";
|
|
5
5
|
import { loadGraph } from "../../graph/graph.js";
|
|
@@ -71,9 +71,11 @@ export async function plan(_argv, cwd = process.cwd(), adapters = allAdapters())
|
|
|
71
71
|
lints.push("review: no cross-vendor reviewer pair in fleet — set review.required: false to waive");
|
|
72
72
|
}
|
|
73
73
|
let cost = 0;
|
|
74
|
+
const routed = [];
|
|
74
75
|
for (const t of g.tasks) {
|
|
75
76
|
try {
|
|
76
77
|
const r = route(t, cfg, channels, profile);
|
|
78
|
+
routed.push({ taskId: t.id, adapter: r.assignment.adapter, model: r.assignment.model });
|
|
77
79
|
for (const l of r.lints) {
|
|
78
80
|
const suf = exclusionReason(l);
|
|
79
81
|
lints.push(suf ? `${l}${suf}` : l);
|
|
@@ -104,6 +106,10 @@ export async function plan(_argv, cwd = process.cwd(), adapters = allAdapters())
|
|
|
104
106
|
}
|
|
105
107
|
if (lints.length)
|
|
106
108
|
lines.push("", "routing lints:", ...lints.map((l) => ` ! ${l}`));
|
|
109
|
+
// v1.47 T3: advisory only — never blocks plan or --route-strict (run refuses on route().lints only)
|
|
110
|
+
const windowLints = contextWindowLints(g.tasks, routed, cfg, cwd);
|
|
111
|
+
if (windowLints.length)
|
|
112
|
+
lines.push("", "context window lints:", ...windowLints.map((l) => ` ! ${l}`));
|
|
107
113
|
// OBS-12/13/14/21: advisory only — never blocks --route-strict (run refuses on routing lints only)
|
|
108
114
|
const scopeLints = collateralLints(g.tasks, cwd);
|
|
109
115
|
if (scopeLints.length)
|
package/dist/cli/commands/run.js
CHANGED
|
@@ -4,7 +4,7 @@ import { loadConfig } from "../../config/config.js";
|
|
|
4
4
|
import { pickDriver } from "../../drivers/index.js";
|
|
5
5
|
import { loadGraph } from "../../graph/graph.js";
|
|
6
6
|
import { formatSummary, runDaemon } from "../../run/daemon.js";
|
|
7
|
-
import { route } from "../../route/router.js";
|
|
7
|
+
import { route, QUALITY_ENV, NO_EXPLORE_ENV } from "../../route/router.js";
|
|
8
8
|
import { formatJournalNarration, loadRoutingProfile } from "../../run/journal.js";
|
|
9
9
|
const summaryGreen = (s) => s.failed.length === 0 && s.human.length === 0 && s.blocked.length === 0 && s.pending.length === 0
|
|
10
10
|
&& s.tipVerify !== "failed";
|
|
@@ -15,6 +15,8 @@ export async function run(argv, cwd = process.cwd()) {
|
|
|
15
15
|
concurrency: { type: "string" },
|
|
16
16
|
driver: { type: "string" },
|
|
17
17
|
"route-strict": { type: "boolean" },
|
|
18
|
+
"no-explore": { type: "boolean" },
|
|
19
|
+
quality: { type: "boolean" },
|
|
18
20
|
},
|
|
19
21
|
});
|
|
20
22
|
if (values.concurrency !== undefined) {
|
|
@@ -23,21 +25,36 @@ export async function run(argv, cwd = process.cwd()) {
|
|
|
23
25
|
throw new Error(`--concurrency must be a positive integer (got ${values.concurrency})`);
|
|
24
26
|
}
|
|
25
27
|
const cfg = loadConfig(cwd);
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
28
|
+
const quality = !!values.quality;
|
|
29
|
+
const noExplore = !!values["no-explore"] || quality;
|
|
30
|
+
const exploreCtx = noExplore || quality ? { noExplore, quality } : undefined;
|
|
31
|
+
if (noExplore)
|
|
32
|
+
process.env[NO_EXPLORE_ENV] = "1";
|
|
33
|
+
if (quality)
|
|
34
|
+
process.env[QUALITY_ENV] = "1";
|
|
35
|
+
try {
|
|
36
|
+
if (values["route-strict"]) {
|
|
37
|
+
const adapters = allAdapters();
|
|
38
|
+
const health = readDoctor(cwd) ?? (await probeAll(adapters));
|
|
39
|
+
const channels = discoverChannels(cfg, adapters, health);
|
|
40
|
+
// no preview: the strict pre-flight routes through exactly what the daemon will use (honors the switch)
|
|
41
|
+
const profile = loadRoutingProfile(cwd, cfg);
|
|
42
|
+
const lints = loadGraph(cwd).tasks.flatMap((t) => route(t, cfg, channels, profile, undefined, undefined, exploreCtx).lints);
|
|
43
|
+
if (lints.length)
|
|
44
|
+
throw new Error(`--route-strict: routing lints present, refusing to dispatch:\n${lints.join("\n")}`);
|
|
45
|
+
}
|
|
46
|
+
const s = await runDaemon(cwd, {
|
|
47
|
+
concurrency: values.concurrency ? Number(values.concurrency) : undefined,
|
|
48
|
+
driver: pickDriver(cfg, values.driver),
|
|
49
|
+
narrate: (event) => console.log(formatJournalNarration(event)),
|
|
50
|
+
});
|
|
51
|
+
const out = `run ${s.runId} finished — ${formatSummary(s)} (merge to main is a human decision)`;
|
|
52
|
+
return { out, code: summaryGreen(s) ? 0 : 2 };
|
|
53
|
+
}
|
|
54
|
+
finally {
|
|
55
|
+
if (noExplore)
|
|
56
|
+
delete process.env[NO_EXPLORE_ENV];
|
|
57
|
+
if (quality)
|
|
58
|
+
delete process.env[QUALITY_ENV];
|
|
35
59
|
}
|
|
36
|
-
const s = await runDaemon(cwd, {
|
|
37
|
-
concurrency: values.concurrency ? Number(values.concurrency) : undefined,
|
|
38
|
-
driver: pickDriver(cfg, values.driver),
|
|
39
|
-
narrate: (event) => console.log(formatJournalNarration(event)),
|
|
40
|
-
});
|
|
41
|
-
const out = `run ${s.runId} finished — ${formatSummary(s)} (merge to main is a human decision)`;
|
|
42
|
-
return { out, code: summaryGreen(s) ? 0 : 2 };
|
|
43
60
|
}
|
package/dist/cli/index.d.ts
CHANGED
|
@@ -5,7 +5,7 @@ export type CommandResult = string | {
|
|
|
5
5
|
};
|
|
6
6
|
export type CommandMap = Record<string, (argv: string[]) => Promise<CommandResult>>;
|
|
7
7
|
export declare const COMMANDS: CommandMap;
|
|
8
|
-
export declare const USAGE = "tickmarkr \u2014 spec-driven orchestration harness for AI coding agents\nusage: tickmarkr <command>\n init guided setup + doctor; init --agent [--force] [--docs] adds agent skills/docs\n doctor re-probe adapters, herdr, auth; print capability matrix\n compile <src> spec \u2192 .tickmarkr/graph.json (fails without acceptance criteria)\n scope <intent> draft a compiled native spec beside an answered intent (--force to overwrite)\n plan dry-run routing table + cost estimate + floor lints\n run execute the graph (--concurrency N --driver herdr|subprocess --route-strict)\n status live run state\n resume <id> continue a run from its journal\n report <id> cost/quality report (--md for committable execution record)\n profile show learned routing profile (profile reset = forget history via cursor, keeps telemetry)\n unlock remove a stale/garbage run lock (refuses if the holder is alive)\n approve <id> <task> approve a parked human gate (--by <name> --reason <text>); takes effect on resume";
|
|
8
|
+
export declare const USAGE = "tickmarkr \u2014 spec-driven orchestration harness for AI coding agents\nusage: tickmarkr <command>\n init guided setup + doctor; init --agent [--force] [--docs] adds agent skills/docs\n doctor re-probe adapters, herdr, auth; print capability matrix\n fleet interactive fleet editor (fleet --print for CI drift checks)\n compile <src> spec \u2192 .tickmarkr/graph.json (fails without acceptance criteria)\n scope <intent> draft a compiled native spec beside an answered intent (--force to overwrite)\n plan dry-run routing table + cost estimate + floor lints\n run execute the graph (--concurrency N --driver herdr|subprocess --route-strict)\n status live run state\n resume <id> continue a run from its journal\n report <id> cost/quality report (--md for committable execution record)\n profile show learned routing profile (profile reset = forget history via cursor, keeps telemetry)\n unlock remove a stale/garbage run lock (refuses if the holder is alive)\n approve <id> <task> approve a parked human gate (--by <name> --reason <text>); takes effect on resume";
|
|
9
9
|
export declare function dispatch(cmd: string | undefined, argv: string[], commands?: CommandMap): Promise<{
|
|
10
10
|
out: string;
|
|
11
11
|
code: number;
|
package/dist/cli/index.js
CHANGED
|
@@ -4,6 +4,7 @@ import { pathToFileURL } from "node:url";
|
|
|
4
4
|
import { approve } from "./commands/approve.js";
|
|
5
5
|
import { compile } from "./commands/compile.js";
|
|
6
6
|
import { doctor } from "./commands/doctor.js";
|
|
7
|
+
import { fleet } from "./commands/fleet.js";
|
|
7
8
|
import { init } from "./commands/init.js";
|
|
8
9
|
import { plan } from "./commands/plan.js";
|
|
9
10
|
import { profile } from "./commands/profile.js";
|
|
@@ -16,7 +17,7 @@ import { unlock } from "./commands/unlock.js";
|
|
|
16
17
|
import { version } from "./commands/version.js";
|
|
17
18
|
const normalize = (r) => typeof r === "string" ? { out: r, code: 0 } : r;
|
|
18
19
|
export const COMMANDS = {
|
|
19
|
-
init, doctor, compile, scope, plan, run, status, resume, report, profile, unlock, approve, version,
|
|
20
|
+
init, doctor, fleet, compile, scope, plan, run, status, resume, report, profile, unlock, approve, version,
|
|
20
21
|
};
|
|
21
22
|
const VERSION_FLAGS = new Set(["version", "--version", "-v"]);
|
|
22
23
|
const HELP_CMDS = new Set(["help", "-h", "--help"]);
|
|
@@ -25,6 +26,7 @@ export const USAGE = `tickmarkr — spec-driven orchestration harness for AI cod
|
|
|
25
26
|
usage: tickmarkr <command>
|
|
26
27
|
init guided setup + doctor; init --agent [--force] [--docs] adds agent skills/docs
|
|
27
28
|
doctor re-probe adapters, herdr, auth; print capability matrix
|
|
29
|
+
fleet interactive fleet editor (fleet --print for CI drift checks)
|
|
28
30
|
compile <src> spec → .tickmarkr/graph.json (fails without acceptance criteria)
|
|
29
31
|
scope <intent> draft a compiled native spec beside an answered intent (--force to overwrite)
|
|
30
32
|
plan dry-run routing table + cost estimate + floor lints
|
package/dist/compile/native.js
CHANGED
|
@@ -23,7 +23,7 @@ export function compileNative(file) {
|
|
|
23
23
|
for (const line of content.split("\n")) {
|
|
24
24
|
const heading = line.match(HEAD_RE);
|
|
25
25
|
if (heading) {
|
|
26
|
-
drafts.push({ id: heading[1], title: heading[2].trim(), fields: {}, acceptance: [], gates: [], hasGates: false, list: null });
|
|
26
|
+
drafts.push({ id: heading[1], title: heading[2].trim(), fields: {}, acceptance: [], gates: [], hasGates: false, list: null, goalContinuation: false });
|
|
27
27
|
continue;
|
|
28
28
|
}
|
|
29
29
|
const draft = drafts.at(-1);
|
|
@@ -41,15 +41,30 @@ export function compileNative(file) {
|
|
|
41
41
|
draft.list = name;
|
|
42
42
|
if (name === "gates")
|
|
43
43
|
draft.hasGates = true;
|
|
44
|
+
draft.goalContinuation = false;
|
|
44
45
|
}
|
|
45
46
|
else {
|
|
46
47
|
if (name === "goal" && !value)
|
|
47
48
|
invalid(draft.id, field[1], "must not be empty");
|
|
48
49
|
draft.fields[name] = value;
|
|
50
|
+
// OBS-60: carry every indented continuation line after `- goal:` into the compiled goal.
|
|
51
|
+
draft.goalContinuation = name === "goal";
|
|
49
52
|
draft.list = null;
|
|
50
53
|
}
|
|
51
54
|
continue;
|
|
52
55
|
}
|
|
56
|
+
// OBS-60: multiline goals — indented prose after `- goal:` is appended until the next field.
|
|
57
|
+
if (draft.goalContinuation) {
|
|
58
|
+
if ((line.startsWith(" ") || line.startsWith("\t")) && !NESTED_RE.test(line)) {
|
|
59
|
+
draft.fields.goal += `\n${line.trim()}`;
|
|
60
|
+
continue;
|
|
61
|
+
}
|
|
62
|
+
if (!line.trim()) {
|
|
63
|
+
draft.fields.goal += "\n";
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
draft.goalContinuation = false;
|
|
67
|
+
}
|
|
53
68
|
const nested = line.match(NESTED_RE);
|
|
54
69
|
if (nested && draft.list) {
|
|
55
70
|
const value = nested[1].trim();
|
package/dist/config/config.d.ts
CHANGED
|
@@ -32,6 +32,7 @@ export declare const TierEntrySchema: z.ZodObject<{
|
|
|
32
32
|
mid: "mid";
|
|
33
33
|
frontier: "frontier";
|
|
34
34
|
}>>;
|
|
35
|
+
windows: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodNumber>>;
|
|
35
36
|
}, z.core.$strip>;
|
|
36
37
|
export type TierEntry = z.infer<typeof TierEntrySchema>;
|
|
37
38
|
export declare const ModelPricingSchema: z.ZodObject<{
|
|
@@ -85,6 +86,16 @@ export declare const TickmarkrConfigSchema: z.ZodObject<{
|
|
|
85
86
|
halfLifeRuns: z.ZodOptional<z.ZodNumber>;
|
|
86
87
|
availWeight: z.ZodOptional<z.ZodNumber>;
|
|
87
88
|
}, z.core.$strip>>;
|
|
89
|
+
explore: z.ZodOptional<z.ZodObject<{
|
|
90
|
+
mode: z.ZodOptional<z.ZodEnum<{
|
|
91
|
+
on: "on";
|
|
92
|
+
off: "off";
|
|
93
|
+
}>>;
|
|
94
|
+
excludeShapes: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
95
|
+
excludeComplexityAtOrAbove: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
96
|
+
cap: z.ZodOptional<z.ZodNumber>;
|
|
97
|
+
}, z.core.$strip>>;
|
|
98
|
+
sla: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodNumber>>;
|
|
88
99
|
allowUnverifiedModels: z.ZodBoolean;
|
|
89
100
|
allow: z.ZodOptional<z.ZodObject<{
|
|
90
101
|
adapters: z.ZodOptional<z.ZodArray<z.ZodString>>;
|
|
@@ -106,6 +117,7 @@ export declare const TickmarkrConfigSchema: z.ZodObject<{
|
|
|
106
117
|
mid: "mid";
|
|
107
118
|
frontier: "frontier";
|
|
108
119
|
}>>;
|
|
120
|
+
windows: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodNumber>>;
|
|
109
121
|
}, z.core.$strip>>;
|
|
110
122
|
pricing: z.ZodRecord<z.ZodString, z.ZodNumber>;
|
|
111
123
|
cost: z.ZodOptional<z.ZodObject<{
|
|
@@ -194,4 +206,34 @@ export type InitConfigOverlay = {
|
|
|
194
206
|
};
|
|
195
207
|
};
|
|
196
208
|
export declare function configTemplate(overlay?: InitConfigOverlay): string;
|
|
209
|
+
/** Fleet-owned overlay keys — the only config surface `tickmarkr fleet` may write. */
|
|
210
|
+
export declare const FLEET_OVERLAY_KEYS: readonly ["routing", "tiers"];
|
|
211
|
+
export type FleetTierAssignment = {
|
|
212
|
+
tier: Tier;
|
|
213
|
+
provenance?: string;
|
|
214
|
+
};
|
|
215
|
+
export type FleetEditable = {
|
|
216
|
+
denyAdapters: string[];
|
|
217
|
+
denyModels: string[];
|
|
218
|
+
tiers: Record<string, Record<string, FleetTierAssignment | null>>;
|
|
219
|
+
map: Record<string, MapEntry>;
|
|
220
|
+
floors: Record<string, Tier>;
|
|
221
|
+
};
|
|
222
|
+
export declare function repoOverlayPath(repoRoot: string): string;
|
|
223
|
+
export declare function readOverlayFile(path: string): Record<string, unknown>;
|
|
224
|
+
export declare function fleetEditableFromConfig(cfg: TickmarkrConfig): FleetEditable;
|
|
225
|
+
/** Resolve which layer last set a dotted fleet path (defaults < global < repo). */
|
|
226
|
+
export declare function fleetKeyLayer(repoRoot: string, dotted: string, opts?: {
|
|
227
|
+
globalDir?: string;
|
|
228
|
+
}): "defaults" | "global" | "repo";
|
|
229
|
+
/** Non-interactive fleet state for CI drift checks (`tickmarkr fleet --print`). */
|
|
230
|
+
export declare function formatFleetPrint(repoRoot: string, opts?: {
|
|
231
|
+
globalDir?: string;
|
|
232
|
+
}): string;
|
|
233
|
+
/** Build the repo overlay fragment fleet would write for edits since session start. */
|
|
234
|
+
export declare function fleetRepoOverlayFromDelta(initial: FleetEditable, edited: FleetEditable, existingRepo?: Record<string, unknown>): Record<string, unknown>;
|
|
235
|
+
export declare function repoOverlayYaml(overlay: Record<string, unknown>, provenance?: Record<string, Record<string, string>>): string;
|
|
236
|
+
export declare function serializeFleetOverlay(overlay: Record<string, unknown>, provenance?: Record<string, Record<string, string>>): string;
|
|
237
|
+
export declare function unifiedYamlDiff(before: string, after: string, label?: string): string;
|
|
238
|
+
export declare function fleetEditableEquals(a: FleetEditable, b: FleetEditable): boolean;
|
|
197
239
|
export {};
|