tickmarkr 1.50.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/cli/commands/fleet.js +84 -23
- package/dist/cli/commands/plan.d.ts +1 -1
- package/dist/cli/commands/plan.js +49 -6
- package/dist/cli/commands/run.js +31 -12
- package/dist/compile/native.js +18 -2
- package/dist/config/config.d.ts +28 -0
- package/dist/config/config.js +103 -8
- 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 +55 -11
- package/package.json +1 -1
|
@@ -17,6 +17,7 @@ export type RoutedAssignment = {
|
|
|
17
17
|
/** Advisory only — absent windows config or undeclared model window ⇒ no lint. */
|
|
18
18
|
export declare function contextWindowLints(tasks: ReadonlyArray<Task>, assignments: ReadonlyArray<RoutedAssignment>, cfg: TickmarkrConfig, repoRoot: string): string[];
|
|
19
19
|
export declare function seedPreferLints(cfg: TickmarkrConfig, health: Record<string, AuthHealth>, adapters: WorkerAdapter[], overlayPreferShapes?: ReadonlySet<string>): string[];
|
|
20
|
+
export declare function mapTierLints(cfg: TickmarkrConfig): string[];
|
|
20
21
|
export declare function modelLints(cfg: TickmarkrConfig, health: Record<string, AuthHealth>, adapters: WorkerAdapter[], opts?: {
|
|
21
22
|
tty?: boolean;
|
|
22
23
|
stateDir?: string;
|
|
@@ -95,6 +95,14 @@ export function seedPreferLints(cfg, health, adapters, overlayPreferShapes = new
|
|
|
95
95
|
}
|
|
96
96
|
return lints;
|
|
97
97
|
}
|
|
98
|
+
// v1.51 T3: built-in defaults no longer carry map tiers, so any tier surviving the merge came from an
|
|
99
|
+
// overlay. Deprecated — routing.floors is the single band authority; MapEntrySchema.tier removal itself
|
|
100
|
+
// is deferred to the next version. One lint per shape, identical text on every surface (plan + doctor).
|
|
101
|
+
export function mapTierLints(cfg) {
|
|
102
|
+
return Object.entries(cfg.routing.map)
|
|
103
|
+
.filter(([, entry]) => entry.tier !== undefined)
|
|
104
|
+
.map(([shape, entry]) => `routing.map.${shape}.tier: ${entry.tier} is deprecated — move this value to routing.floors.${shape} (single band authority; tier removal deferred to next version)`);
|
|
105
|
+
}
|
|
98
106
|
// Diffs detected models (doctor.json) against configured tiers, both directions, per adapter id in cfg.tiers.
|
|
99
107
|
// No ` ! ` prefix here — the consumer (doctor rows / plan lints) owns that. Pre-v1.5 doctor.json (models:[], no
|
|
100
108
|
// modelsDetectedAt) is the compat baseline: `?.`/`?? []` everywhere, no zod (would reject old files).
|
|
@@ -137,6 +145,7 @@ export function modelLints(cfg, health, adapters, opts) {
|
|
|
137
145
|
}
|
|
138
146
|
}
|
|
139
147
|
lints.push(...seedPreferLints(cfg, health, adapters, opts?.overlayPreferShapes));
|
|
148
|
+
lints.push(...mapTierLints(cfg)); // v1.51 T3: shared here so plan and doctor draw the exact same message
|
|
140
149
|
return lints;
|
|
141
150
|
}
|
|
142
151
|
// T2/T6: one lint per exclusion, naming the probe reason and date. TTY truncates reasons to 60 chars and
|
|
@@ -6,9 +6,10 @@ import { parseArgs } from "node:util";
|
|
|
6
6
|
import { allAdapters, discoverChannels, doctorAgeMs, initDoctorReuse, readAutoPrefer } from "../../adapters/registry.js";
|
|
7
7
|
import { fleetUnclassifiedModels } from "../../adapters/model-lints.js";
|
|
8
8
|
import { GLYPHS, bold, legend as legendText, toggleActive, toggleInactive } from "../../brand.js";
|
|
9
|
-
import { fleetEditableFromConfig, fleetEditableEquals, fleetRepoOverlayFromDelta, formatFleetPrint, globalConfigDir,
|
|
9
|
+
import { fleetEditableFromConfig, fleetEditableEquals, fleetRepoOverlayFromDelta, formatFleetPrint, globalConfigDir, overlayPreferShapes, readOverlayFile, repoOverlayPath, repoOverlayYaml, ROUTING_MODES, unifiedYamlDiff, } from "../../config/config.js";
|
|
10
10
|
import { SHAPES, TIERS } from "../../graph/schema.js";
|
|
11
11
|
import { route } from "../../route/router.js";
|
|
12
|
+
import { resolveRunMode } from "../../run/daemon.js";
|
|
12
13
|
import { loadRoutingProfile } from "../../run/journal.js";
|
|
13
14
|
const NON_TTY_MSG = "tickmarkr fleet: interactive fleet editor requires a TTY — use `tickmarkr fleet --print` for non-interactive output";
|
|
14
15
|
const QUIT = "fleet: quit without writing";
|
|
@@ -55,10 +56,21 @@ function provenanceMap(editable) {
|
|
|
55
56
|
}
|
|
56
57
|
return out;
|
|
57
58
|
}
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
59
|
+
// v1.51 T4: serializeFleetOverlay predates routing.mode — splice the mode line under routing:
|
|
60
|
+
// so a repo-declared mode survives fleet writes and a mode selection lands as routing.mode.
|
|
61
|
+
function withModeLine(yaml, mode) {
|
|
62
|
+
if (!mode)
|
|
63
|
+
return yaml;
|
|
64
|
+
if (/^routing:$/m.test(yaml))
|
|
65
|
+
return yaml.replace(/^routing:$/m, `routing:\n mode: ${mode}`);
|
|
66
|
+
return `routing:\n mode: ${mode}\n${yaml}`;
|
|
61
67
|
}
|
|
68
|
+
// v1.51 T4: one gloss per routing mode on the fleet mode screen — mirrors the preset compiler.
|
|
69
|
+
const MODE_GLOSS = {
|
|
70
|
+
"partner-led": "every shape frontier · explore off",
|
|
71
|
+
"risk-based": "risk-tiered default floors",
|
|
72
|
+
"staff-led": "implement/refactor one band down · integrity shapes hold frontier",
|
|
73
|
+
};
|
|
62
74
|
const isAbort = (k) => k.name === "escape" || k.name === "q" || (k.ctrl === true && k.name === "c");
|
|
63
75
|
const isNext = (k) => k.name === "return" || k.name === "enter";
|
|
64
76
|
const isToggle = (k) => k.name === "space";
|
|
@@ -154,8 +166,14 @@ export async function fleet(argv, cwd = process.cwd(), adapters = allAdapters(),
|
|
|
154
166
|
const input = io.input ?? process.stdin;
|
|
155
167
|
const output = io.output ?? process.stdout;
|
|
156
168
|
const interactive = input.isTTY === true && output.isTTY === true;
|
|
157
|
-
if (print)
|
|
158
|
-
|
|
169
|
+
if (print) {
|
|
170
|
+
// v1.51 T4: the print surface names the mode and its source layer right under the header —
|
|
171
|
+
// comment-prefixed so the YAML body stays machine-parseable and regex-stable.
|
|
172
|
+
const rm = resolveRunMode(cwd, { globalDir });
|
|
173
|
+
const body = formatFleetPrint(cwd, { globalDir });
|
|
174
|
+
const nl = body.indexOf("\n");
|
|
175
|
+
return `${body.slice(0, nl)}\n# mode: ${rm.mode.mode} (${rm.source})${body.slice(nl)}`;
|
|
176
|
+
}
|
|
159
177
|
if (!interactive)
|
|
160
178
|
return { out: NON_TTY_MSG, code: 1 };
|
|
161
179
|
const fresh = values.fresh ?? false;
|
|
@@ -166,15 +184,16 @@ export async function fleet(argv, cwd = process.cwd(), adapters = allAdapters(),
|
|
|
166
184
|
code: 1,
|
|
167
185
|
};
|
|
168
186
|
}
|
|
169
|
-
const
|
|
187
|
+
const rm = resolveRunMode(cwd, { globalDir });
|
|
188
|
+
const cfg = rm.cfg;
|
|
170
189
|
const initial = fleetEditableFromConfig(cfg);
|
|
171
190
|
const editable = structuredClone(initial);
|
|
172
191
|
const health = cached;
|
|
173
192
|
const term = openTerm(input, output);
|
|
174
193
|
try {
|
|
175
|
-
// step 1/
|
|
194
|
+
// step 1/5 — probe data
|
|
176
195
|
const ageLine = formatAge(doctorAgeMs(cwd));
|
|
177
|
-
term.frame(listFrame("step 1/
|
|
196
|
+
term.frame(listFrame("step 1/5 · probe data", "enter continue · r refresh via doctor · esc/q quit", [`probe data: ${ageLine} (.tickmarkr/doctor.json)`], 0));
|
|
178
197
|
for (;;) {
|
|
179
198
|
const k = await term.key();
|
|
180
199
|
if (isAbort(k))
|
|
@@ -185,7 +204,7 @@ export async function fleet(argv, cwd = process.cwd(), adapters = allAdapters(),
|
|
|
185
204
|
if (isNext(k))
|
|
186
205
|
break;
|
|
187
206
|
}
|
|
188
|
-
// step 2/
|
|
207
|
+
// step 2/5 — agent CLIs (space toggles adapter deny)
|
|
189
208
|
const installed = adapters.filter((a) => health[a.id]?.installed);
|
|
190
209
|
const adapterRows = () => installed.map((a) => {
|
|
191
210
|
const h = health[a.id];
|
|
@@ -194,7 +213,7 @@ export async function fleet(argv, cwd = process.cwd(), adapters = allAdapters(),
|
|
|
194
213
|
});
|
|
195
214
|
const listLegend = "↑↓/jk move · space toggle · enter next · esc/q quit";
|
|
196
215
|
let aCursor = 0;
|
|
197
|
-
term.frame(listFrame("step 2/
|
|
216
|
+
term.frame(listFrame("step 2/5 · agent CLIs", listLegend, adapterRows(), aCursor));
|
|
198
217
|
for (;;) {
|
|
199
218
|
const k = await term.key();
|
|
200
219
|
if (isAbort(k))
|
|
@@ -218,9 +237,9 @@ export async function fleet(argv, cwd = process.cwd(), adapters = allAdapters(),
|
|
|
218
237
|
changed = true;
|
|
219
238
|
}
|
|
220
239
|
if (changed)
|
|
221
|
-
term.frame(listFrame("step 2/
|
|
240
|
+
term.frame(listFrame("step 2/5 · agent CLIs", listLegend, adapterRows(), aCursor));
|
|
222
241
|
}
|
|
223
|
-
// step 3/
|
|
242
|
+
// step 3/5 — models per enabled adapter (space toggles deny, t assigns tier from the row)
|
|
224
243
|
const enabledAdapters = installed.filter((a) => !isDeniedAdapter(a.id, editable));
|
|
225
244
|
for (const a of enabledAdapters) {
|
|
226
245
|
const unclassifiedAll = fleetUnclassifiedModels(cfg, health, adapters).filter((u) => u.adapter === a.id);
|
|
@@ -238,7 +257,7 @@ export async function fleet(argv, cwd = process.cwd(), adapters = allAdapters(),
|
|
|
238
257
|
const denied = isDeniedModel(a.id, r.model, editable);
|
|
239
258
|
return `${denied ? toggleInactive() : toggleActive()} ${r.model} ${tier?.tier ?? "???"} ${denied ? "denied" : "allowed"}`;
|
|
240
259
|
});
|
|
241
|
-
const title = `step 3/
|
|
260
|
+
const title = `step 3/5 · models · ${a.id}`;
|
|
242
261
|
const modelsLegend = "↑↓/jk move · space toggle · t tier · enter next · esc/q quit";
|
|
243
262
|
let mCursor = 0;
|
|
244
263
|
term.frame(listFrame(title, modelsLegend, renderRows(rowsData()), mCursor));
|
|
@@ -283,7 +302,44 @@ export async function fleet(argv, cwd = process.cwd(), adapters = allAdapters(),
|
|
|
283
302
|
term.frame(listFrame(title, modelsLegend, renderRows(rowsData()), mCursor));
|
|
284
303
|
}
|
|
285
304
|
}
|
|
286
|
-
// step 4/
|
|
305
|
+
// step 4/5 — routing mode (selection is in-memory; the write happens only through the diff confirm).
|
|
306
|
+
// Candidate floor tables come from the ONE preset compiler via resolveRunMode — no mode math here.
|
|
307
|
+
const modeCfgs = Object.fromEntries(ROUTING_MODES.map((m) => [m, m === rm.mode.mode ? rm : resolveRunMode(cwd, { flag: m, globalDir })]));
|
|
308
|
+
const modeRows = () => ROUTING_MODES.map((m) => `${m === rm.mode.mode ? toggleActive() : " "} ${m.padEnd(11)} ${MODE_GLOSS[m]}`);
|
|
309
|
+
// preview: the highlighted mode's resolved floor table diffed against the current mode
|
|
310
|
+
const floorPreview = (cand) => {
|
|
311
|
+
if (cand === rm.mode.mode)
|
|
312
|
+
return [];
|
|
313
|
+
const cur = cfg.routing.floors;
|
|
314
|
+
const next = modeCfgs[cand].cfg.routing.floors;
|
|
315
|
+
const changed = SHAPES.filter((s) => cur[s] !== next[s]);
|
|
316
|
+
return [
|
|
317
|
+
` floors vs ${rm.mode.mode}:`,
|
|
318
|
+
...(changed.length ? changed.map((s) => ` ${s}: ${cur[s]} → ${next[s]}`) : [" (no floor changes)"]),
|
|
319
|
+
];
|
|
320
|
+
};
|
|
321
|
+
const modeTitle = "step 4/5 · routing mode";
|
|
322
|
+
const modeLegend = "↑↓/jk move · enter select · esc/q quit";
|
|
323
|
+
let modeCursor = ROUTING_MODES.indexOf(rm.mode.mode);
|
|
324
|
+
let selectedMode = rm.mode.mode;
|
|
325
|
+
const modeFrame = () => [...listFrame(modeTitle, modeLegend, modeRows(), modeCursor), ...floorPreview(ROUTING_MODES[modeCursor])];
|
|
326
|
+
term.frame(modeFrame());
|
|
327
|
+
for (;;) {
|
|
328
|
+
const k = await term.key();
|
|
329
|
+
if (isAbort(k))
|
|
330
|
+
return QUIT;
|
|
331
|
+
if (isNext(k)) {
|
|
332
|
+
selectedMode = ROUTING_MODES[modeCursor];
|
|
333
|
+
break;
|
|
334
|
+
}
|
|
335
|
+
const moved = moveCursor(k, modeCursor, ROUTING_MODES.length);
|
|
336
|
+
if (moved !== modeCursor) {
|
|
337
|
+
modeCursor = moved;
|
|
338
|
+
term.frame(modeFrame());
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
// step 5/5 — shape routing (typed entry for pin/tier/prefer from the highlighted row);
|
|
342
|
+
// previews route under the SELECTED mode's resolved floors, never a floor edit of its own
|
|
287
343
|
const channels = discoverChannels(cfg, adapters, health);
|
|
288
344
|
const profile = loadRoutingProfile(cwd, cfg, { preview: true });
|
|
289
345
|
const autoPrefer = readAutoPrefer(cwd);
|
|
@@ -291,7 +347,7 @@ export async function fleet(argv, cwd = process.cwd(), adapters = allAdapters(),
|
|
|
291
347
|
const shapeRows = () => SHAPES.map((shape) => {
|
|
292
348
|
let now;
|
|
293
349
|
try {
|
|
294
|
-
const r = route(previewTask(shape), { ...cfg, routing: { ...cfg.routing, map: editable.map, floors:
|
|
350
|
+
const r = route(previewTask(shape), { ...cfg, routing: { ...cfg.routing, map: editable.map, floors: modeCfgs[selectedMode].cfg.routing.floors } }, channels, profile);
|
|
295
351
|
now = `${r.assignment.adapter}:${r.assignment.model} (${r.assignment.channel}, ${r.assignment.tier})`;
|
|
296
352
|
}
|
|
297
353
|
catch (e) {
|
|
@@ -300,7 +356,7 @@ export async function fleet(argv, cwd = process.cwd(), adapters = allAdapters(),
|
|
|
300
356
|
const auto = autoPrefer?.[shape] && !overlayShapes.has(shape) ? " (auto-prefer active)" : "";
|
|
301
357
|
return `${shape} → ${now}${auto}`;
|
|
302
358
|
});
|
|
303
|
-
const shapesTitle = "step
|
|
359
|
+
const shapesTitle = "step 5/5 · shape routing";
|
|
304
360
|
const shapesLegend = "↑↓/jk move · a auto · p pin · t tier · f prefer · enter next · esc/q quit";
|
|
305
361
|
let sCursor = 0;
|
|
306
362
|
term.frame(listFrame(shapesTitle, shapesLegend, shapeRows(), sCursor));
|
|
@@ -349,10 +405,17 @@ export async function fleet(argv, cwd = process.cwd(), adapters = allAdapters(),
|
|
|
349
405
|
if (changed)
|
|
350
406
|
term.frame(listFrame(shapesTitle, shapesLegend, shapeRows(), sCursor));
|
|
351
407
|
}
|
|
352
|
-
// review — unified diff + typed confirm
|
|
408
|
+
// review — unified diff + typed confirm (the ONLY write path; the mode screen funnels through here)
|
|
353
409
|
const before = currentRepoOverlayText(cwd);
|
|
354
|
-
const
|
|
355
|
-
|
|
410
|
+
const existing = readOverlayFile(repoOverlayPath(cwd));
|
|
411
|
+
const modeChanged = selectedMode !== rm.mode.mode;
|
|
412
|
+
// preserve a repo-declared mode line verbatim; write a new one only when the selection changed it
|
|
413
|
+
const writeMode = modeChanged ? selectedMode : existing.routing?.mode;
|
|
414
|
+
const merged = fleetEditableEquals(initial, editable)
|
|
415
|
+
? structuredClone(existing)
|
|
416
|
+
: fleetRepoOverlayFromDelta(initial, editable, existing);
|
|
417
|
+
const after = withModeLine(repoOverlayYaml(merged, provenanceMap(editable)), writeMode);
|
|
418
|
+
if ((!modeChanged && fleetEditableEquals(initial, editable)) || before === after) {
|
|
356
419
|
return "fleet: no overlay changes (empty diff)";
|
|
357
420
|
}
|
|
358
421
|
const diff = unifiedYamlDiff(before, after, repoOverlayPath(cwd));
|
|
@@ -360,11 +423,9 @@ export async function fleet(argv, cwd = process.cwd(), adapters = allAdapters(),
|
|
|
360
423
|
const confirm = await term.askTyped("write overlay? [y/N] ");
|
|
361
424
|
if (!/^y(?:es)?$/i.test(confirm))
|
|
362
425
|
return "fleet: discarded overlay changes";
|
|
363
|
-
const merged = fleetRepoOverlayFromDelta(initial, editable, readOverlayFile(repoOverlayPath(cwd)));
|
|
364
|
-
const body = repoOverlayYaml(merged, provenanceMap(editable));
|
|
365
426
|
const path = repoOverlayPath(cwd);
|
|
366
427
|
mkdirSync(dirname(path), { recursive: true });
|
|
367
|
-
writeFileSync(path,
|
|
428
|
+
writeFileSync(path, after);
|
|
368
429
|
return `fleet: wrote ${path}`;
|
|
369
430
|
}
|
|
370
431
|
finally {
|
|
@@ -1,2 +1,2 @@
|
|
|
1
1
|
import type { WorkerAdapter } from "../../adapters/types.js";
|
|
2
|
-
export declare function plan(
|
|
2
|
+
export declare function plan(argv: string[], cwd?: string, adapters?: WorkerAdapter[]): Promise<string>;
|
|
@@ -1,11 +1,14 @@
|
|
|
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
3
|
import { GLYPHS, dim, rule, title, warn } from "../../brand.js";
|
|
4
|
+
import { parseArgs } from "node:util";
|
|
4
5
|
import { collateralLints } from "../../compile/collateral.js";
|
|
5
|
-
import {
|
|
6
|
+
import { DEFAULT_CONFIG, ROUTING_MODES, TIER_RANK } from "../../config/config.js";
|
|
6
7
|
import { loadGraph } from "../../graph/graph.js";
|
|
8
|
+
import { resolveRunMode } from "../../run/daemon.js";
|
|
7
9
|
import { excludedChannels, exclusionLine } from "../../route/preference.js";
|
|
8
|
-
import {
|
|
10
|
+
import { staffLedEvidence } from "../../route/profile.js";
|
|
11
|
+
import { QUALITY_ENV, route, RoutingError } from "../../route/router.js";
|
|
9
12
|
import { modelId } from "../../gates/review.js";
|
|
10
13
|
import { loadRoutingProfile } from "../../run/journal.js";
|
|
11
14
|
// T4 (v1.50): TTY-only brand pass — the title helper frames the routing table, lint/unroutable
|
|
@@ -31,12 +34,19 @@ const fleetCanCrossVendorReview = (channels) => {
|
|
|
31
34
|
return true;
|
|
32
35
|
return false;
|
|
33
36
|
};
|
|
34
|
-
export async function plan(
|
|
37
|
+
export async function plan(argv, cwd = process.cwd(), adapters = allAdapters()) {
|
|
35
38
|
// ponytail: hardcoded 24h TTL — promote to config when an operator asks. mtime is the signal because
|
|
36
39
|
// doctor.json has no probe timestamp and a schema field would break the existing-files compat invariant.
|
|
37
40
|
const DOCTOR_STALE_MS = 24 * 60 * 60 * 1000;
|
|
38
|
-
|
|
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];
|
|
39
48
|
const g = loadGraph(cwd);
|
|
49
|
+
const { cfg, mode, source } = resolveRunMode(cwd, { flag: values.mode, spec: g.mode });
|
|
40
50
|
// readDoctor cache path: staleness line only fires here (probeAll fallback is fresh by construction).
|
|
41
51
|
const cached = readDoctor(cwd);
|
|
42
52
|
const health = cached ?? (await probeAll(adapters));
|
|
@@ -46,7 +56,17 @@ export async function plan(_argv, cwd = process.cwd(), adapters = allAdapters())
|
|
|
46
56
|
// output byte-identical to today.
|
|
47
57
|
const profile = loadRoutingProfile(cwd, cfg, { preview: true });
|
|
48
58
|
let deviations = 0;
|
|
49
|
-
|
|
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
|
+
};
|
|
50
70
|
const excluded = excludedChannels(cfg, adapters, health);
|
|
51
71
|
if (excluded.length)
|
|
52
72
|
lines.push(exclusionLine(excluded), "");
|
|
@@ -78,7 +98,24 @@ export async function plan(_argv, cwd = process.cwd(), adapters = allAdapters())
|
|
|
78
98
|
return ` — ${m.key} is unauthed (${m.reason}, probed ${m.probedAt.split("T")[0]})`;
|
|
79
99
|
return "";
|
|
80
100
|
};
|
|
81
|
-
|
|
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
|
+
}
|
|
82
119
|
for (const [role, sel] of [["judge", cfg.judge], ["consult", cfg.consult]]) {
|
|
83
120
|
if (!health[sel.adapter]?.installed)
|
|
84
121
|
lints.push(`${role}: ${sel.adapter}:${sel.model} not installed — that gate/consult will fail closed`);
|
|
@@ -100,6 +137,9 @@ export async function plan(_argv, cwd = process.cwd(), adapters = allAdapters())
|
|
|
100
137
|
const est = r.assignment.channel === "sub" ? 0 : cfg.pricing[r.assignment.tier];
|
|
101
138
|
cost += est;
|
|
102
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);
|
|
103
143
|
if (r.deviation) {
|
|
104
144
|
deviations++;
|
|
105
145
|
const d = r.deviation;
|
|
@@ -111,6 +151,9 @@ export async function plan(_argv, cwd = process.cwd(), adapters = allAdapters())
|
|
|
111
151
|
throw e;
|
|
112
152
|
const msg = `${e.message}${exclusionReason(e.message)}`;
|
|
113
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);
|
|
114
157
|
lints.push(`${t.id}: unroutable — ${msg}`);
|
|
115
158
|
}
|
|
116
159
|
}
|
package/dist/cli/commands/run.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
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
9
|
import { ttyVisual } from "../../adapters/model-lints.js";
|
|
10
10
|
import { statusRow } from "../../brand.js";
|
|
@@ -24,6 +24,11 @@ export const narrationLine = (event) => {
|
|
|
24
24
|
};
|
|
25
25
|
const summaryGreen = (s) => s.failed.length === 0 && s.human.length === 0 && s.blocked.length === 0 && s.pending.length === 0
|
|
26
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";
|
|
27
32
|
export async function run(argv, cwd = process.cwd()) {
|
|
28
33
|
const { values } = parseArgs({
|
|
29
34
|
args: argv,
|
|
@@ -32,6 +37,7 @@ export async function run(argv, cwd = process.cwd()) {
|
|
|
32
37
|
driver: { type: "string" },
|
|
33
38
|
"route-strict": { type: "boolean" },
|
|
34
39
|
"no-explore": { type: "boolean" },
|
|
40
|
+
mode: { type: "string" },
|
|
35
41
|
quality: { type: "boolean" },
|
|
36
42
|
},
|
|
37
43
|
});
|
|
@@ -40,14 +46,28 @@ export async function run(argv, cwd = process.cwd()) {
|
|
|
40
46
|
if (!Number.isInteger(n) || n <= 0)
|
|
41
47
|
throw new Error(`--concurrency must be a positive integer (got ${values.concurrency})`);
|
|
42
48
|
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
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;
|
|
47
69
|
if (noExplore)
|
|
48
70
|
process.env[NO_EXPLORE_ENV] = "1";
|
|
49
|
-
if (quality)
|
|
50
|
-
process.env[QUALITY_ENV] = "1";
|
|
51
71
|
try {
|
|
52
72
|
if (values["route-strict"]) {
|
|
53
73
|
const adapters = allAdapters();
|
|
@@ -55,13 +75,14 @@ export async function run(argv, cwd = process.cwd()) {
|
|
|
55
75
|
const channels = discoverChannels(cfg, adapters, health);
|
|
56
76
|
// no preview: the strict pre-flight routes through exactly what the daemon will use (honors the switch)
|
|
57
77
|
const profile = loadRoutingProfile(cwd, cfg);
|
|
58
|
-
const lints =
|
|
78
|
+
const lints = graph.tasks.flatMap((t) => route(t, cfg, channels, profile, undefined, undefined, exploreCtx).lints);
|
|
59
79
|
if (lints.length)
|
|
60
80
|
throw new Error(`--route-strict: routing lints present, refusing to dispatch:\n${lints.join("\n")}`);
|
|
61
81
|
}
|
|
62
82
|
const s = await runDaemon(cwd, {
|
|
63
83
|
concurrency: values.concurrency ? Number(values.concurrency) : undefined,
|
|
64
84
|
driver: pickDriver(cfg, values.driver),
|
|
85
|
+
mode: flagMode,
|
|
65
86
|
narrate: (event) => console.log(narrationLine(event)),
|
|
66
87
|
});
|
|
67
88
|
const out = `run ${s.runId} finished — ${formatSummary(s)} (merge to main is a human decision)`;
|
|
@@ -70,7 +91,5 @@ export async function run(argv, cwd = process.cwd()) {
|
|
|
70
91
|
finally {
|
|
71
92
|
if (noExplore)
|
|
72
93
|
delete process.env[NO_EXPLORE_ENV];
|
|
73
|
-
if (quality)
|
|
74
|
-
delete process.env[QUALITY_ENV];
|
|
75
94
|
}
|
|
76
95
|
}
|
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;
|
package/dist/config/config.js
CHANGED
|
@@ -10,6 +10,16 @@ const TierEnum = z.enum(TIERS, {
|
|
|
10
10
|
});
|
|
11
11
|
export const TIER_RANK = { cheap: 0, mid: 1, frontier: 2 };
|
|
12
12
|
export const DEFAULT_DIFF_CAP = 60_000;
|
|
13
|
+
// v1.51 T1: routing.mode — a preset COMPILED INTO FLOORS at config load. The router never sees the
|
|
14
|
+
// mode; it receives resolved floors only (the structural defense against the quality-silently-loses
|
|
15
|
+
// class — no fourth runtime authority, no new precedence key).
|
|
16
|
+
export const ROUTING_MODES = ["partner-led", "risk-based", "staff-led"];
|
|
17
|
+
const ModeEnum = z.enum(ROUTING_MODES, {
|
|
18
|
+
error: (iss) => `Invalid option: expected one of "partner-led"|"risk-based"|"staff-led" (got ${JSON.stringify(iss.input)})`,
|
|
19
|
+
});
|
|
20
|
+
// Integrity set (weak-oracle shapes, OBS-68..70 class): no mode may resolve these below frontier.
|
|
21
|
+
// Only an explicit operator floor line can — and it draws a standing plan lint every load.
|
|
22
|
+
export const INTEGRITY_FLOOR_SHAPES = ["plan", "spec", "migration", "ui"];
|
|
13
23
|
export const MapEntrySchema = z.object({
|
|
14
24
|
pin: z.object({ via: z.string(), model: z.string() }).optional(),
|
|
15
25
|
tier: TierEnum.optional(),
|
|
@@ -80,6 +90,8 @@ export const TickmarkrConfigSchema = z.object({
|
|
|
80
90
|
contextWarnTokens: z.number().int().positive(),
|
|
81
91
|
setup: z.string().optional(),
|
|
82
92
|
routing: z.object({
|
|
93
|
+
// v1.51 T1: preset expanded into floors by loadConfig — absent ⇒ risk-based ⇒ byte-identical routing.
|
|
94
|
+
mode: ModeEnum.optional(),
|
|
83
95
|
map: z.record(z.string(), MapEntrySchema),
|
|
84
96
|
floors: z.record(z.string(), TierEnum),
|
|
85
97
|
learned: z.enum(["on", "off"]), // v1.6 ROUTE-09 kill switch; a typo (offf) fails loud via safeParse
|
|
@@ -155,12 +167,16 @@ export const DEFAULT_CONFIG = {
|
|
|
155
167
|
taskTimeoutMinutes: 30,
|
|
156
168
|
contextWarnTokens: 170_000, // v1.23 T2: overseer ctx-watch.sh proven threshold
|
|
157
169
|
routing: {
|
|
170
|
+
// v1.51 T3 (round-2 consult): map entries carry preferences/pins only — band policy lives in
|
|
171
|
+
// routing.floors, the single tier authority. The dropped tiers (implement mid, tests cheap,
|
|
172
|
+
// docs cheap) were byte-equal to the floors below, so routing is unchanged. Overlay map tiers
|
|
173
|
+
// still parse but draw a move-to-floors lint in plan/doctor; MapEntrySchema.tier removal itself
|
|
174
|
+
// is deferred to the next version (tombstone grammar: tier: null).
|
|
158
175
|
map: {
|
|
159
176
|
plan: { pin: { via: "claude-code", model: "fable" } },
|
|
160
177
|
spec: { pin: { via: "claude-code", model: "fable" } },
|
|
161
|
-
implement: {
|
|
162
|
-
tests: {
|
|
163
|
-
docs: { tier: "cheap" },
|
|
178
|
+
implement: { prefer: ["cursor-agent", "codex"] },
|
|
179
|
+
tests: { prefer: ["opencode"] },
|
|
164
180
|
},
|
|
165
181
|
floors: {
|
|
166
182
|
plan: "frontier", spec: "frontier", migration: "frontier",
|
|
@@ -304,14 +320,88 @@ export function overlayPreferShapes(repoRoot, opts = {}) {
|
|
|
304
320
|
}
|
|
305
321
|
return shapes;
|
|
306
322
|
}
|
|
307
|
-
|
|
323
|
+
const lowerTier = (t) => (t === "frontier" ? "mid" : "cheap");
|
|
324
|
+
const maxTier = (a, b) => (TIER_RANK[a] >= TIER_RANK[b] ? a : b);
|
|
325
|
+
const integrityMin = (shape) => INTEGRITY_FLOOR_SHAPES.includes(shape) ? "frontier" : "cheap";
|
|
326
|
+
// partner-led = frontier everywhere; staff-led = one band down, integrity-clamped (net effect on the
|
|
327
|
+
// defaults: implement/refactor → cheap, ui → frontier); risk-based = identity, so an absent mode key
|
|
328
|
+
// resolves byte-identically to pre-v1.51 routing.
|
|
329
|
+
function presetFloor(mode, shape, dflt) {
|
|
330
|
+
if (mode === "partner-led")
|
|
331
|
+
return "frontier";
|
|
332
|
+
if (mode === "staff-led")
|
|
333
|
+
return maxTier(lowerTier(dflt), integrityMin(shape));
|
|
334
|
+
return dflt;
|
|
335
|
+
}
|
|
336
|
+
/** Which floor shapes the operator wrote in an overlay layer (explicit beats mode; null tombstones stay removed). */
|
|
337
|
+
function overlayFloorEdits(layers) {
|
|
338
|
+
const explicit = new Set();
|
|
339
|
+
const tombstoned = new Set();
|
|
340
|
+
for (const layer of layers) {
|
|
341
|
+
const floors = layer?.routing?.floors;
|
|
342
|
+
if (!floors || typeof floors !== "object" || Array.isArray(floors))
|
|
343
|
+
continue;
|
|
344
|
+
for (const [shape, v] of Object.entries(floors)) {
|
|
345
|
+
if (v === null) {
|
|
346
|
+
explicit.delete(shape);
|
|
347
|
+
tombstoned.add(shape);
|
|
348
|
+
}
|
|
349
|
+
else {
|
|
350
|
+
tombstoned.delete(shape);
|
|
351
|
+
explicit.add(shape);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
return { explicit, tombstoned };
|
|
356
|
+
}
|
|
357
|
+
// Mutates cfg.routing.floors (and explore under partner-led) to the mode-resolved values.
|
|
358
|
+
function resolveRoutingMode(cfg, layers) {
|
|
359
|
+
const mode = cfg.routing.mode ?? "risk-based";
|
|
360
|
+
const { explicit, tombstoned } = overlayFloorEdits(layers);
|
|
361
|
+
const provenance = {};
|
|
362
|
+
const lints = [];
|
|
363
|
+
for (const shape of SHAPES) {
|
|
364
|
+
if (tombstoned.has(shape))
|
|
365
|
+
continue; // operator removed the floor — a mode never resurrects it
|
|
366
|
+
const dflt = DEFAULT_CONFIG.routing.floors[shape];
|
|
367
|
+
const moded = presetFloor(mode, shape, dflt);
|
|
368
|
+
if (explicit.has(shape)) {
|
|
369
|
+
const val = cfg.routing.floors[shape];
|
|
370
|
+
provenance[shape] = "config floors";
|
|
371
|
+
if (moded !== val && moded !== dflt) {
|
|
372
|
+
lints.push(`floors.${shape}: ${val} (config floors) overrides mode ${mode} — shadowed delta: ${shape} ${dflt}→${moded}`);
|
|
373
|
+
}
|
|
374
|
+
if (TIER_RANK[val] < TIER_RANK[integrityMin(shape)]) {
|
|
375
|
+
lints.push(`floors.${shape}: ${val} is below integrity minimum frontier — integrity class ${INTEGRITY_FLOOR_SHAPES.join("/")} holds regardless of mode`);
|
|
376
|
+
}
|
|
377
|
+
continue;
|
|
378
|
+
}
|
|
379
|
+
cfg.routing.floors[shape] = moded;
|
|
380
|
+
provenance[shape] = `mode ${mode}`;
|
|
381
|
+
}
|
|
382
|
+
for (const shape of Object.keys(cfg.routing.floors)) {
|
|
383
|
+
if (!(shape in provenance))
|
|
384
|
+
provenance[shape] = "config floors"; // overlay floors on non-shape keys
|
|
385
|
+
}
|
|
386
|
+
// partner-led resolves exploration off (probes are the wrong spend under a premium declaration);
|
|
387
|
+
// staff-led and risk-based leave explore untouched — the round-2 fence rides through as configured.
|
|
388
|
+
if (mode === "partner-led")
|
|
389
|
+
cfg.routing.explore = { ...cfg.routing.explore, mode: "off" };
|
|
390
|
+
return { mode, provenance, lints };
|
|
391
|
+
}
|
|
392
|
+
/** loadConfig plus the mode-resolution record (floor provenance + plan lints). The resolved floors are
|
|
393
|
+
* already applied to cfg.routing.floors — route() consumes floors only and never sees the mode. */
|
|
394
|
+
export function loadConfigWithMode(repoRoot, opts = {}) {
|
|
308
395
|
const globalCfg = readYaml(join(opts.globalDir ?? globalConfigDir(), "config.yaml"));
|
|
309
396
|
const repoCfg = readYaml(join(repoRoot, stateDirName(repoRoot), "config.yaml"));
|
|
310
397
|
const merged = deepMerge(deepMerge(structuredClone(DEFAULT_CONFIG), globalCfg), repoCfg);
|
|
311
398
|
const r = TickmarkrConfigSchema.safeParse(merged);
|
|
312
399
|
if (!r.success)
|
|
313
400
|
throw new ConfigError(z.prettifyError(r.error));
|
|
314
|
-
return r.data;
|
|
401
|
+
return { cfg: r.data, mode: resolveRoutingMode(r.data, [globalCfg, repoCfg]) };
|
|
402
|
+
}
|
|
403
|
+
export function loadConfig(repoRoot, opts = {}) {
|
|
404
|
+
return loadConfigWithMode(repoRoot, opts).cfg;
|
|
315
405
|
}
|
|
316
406
|
export function configTemplate(overlay) {
|
|
317
407
|
const base = `# tickmarkr config overlay — merges over built-in defaults (repo beats global beats defaults)
|
|
@@ -321,10 +411,15 @@ export function configTemplate(overlay) {
|
|
|
321
411
|
# contextWarnTokens: 170000 # v1.23: journal+notify once per attempt when live worker context crosses this (status shows the sample)
|
|
322
412
|
# setup: npm ci --prefer-offline # run in each fresh task worktree before dispatch
|
|
323
413
|
# routing:
|
|
324
|
-
#
|
|
325
|
-
#
|
|
414
|
+
# mode: risk-based # v1.51: partner-led | risk-based | staff-led — a preset compiled into floors at
|
|
415
|
+
# # load (partner-led: every shape frontier + explore off; staff-led: implement/refactor
|
|
416
|
+
# # cheap; integrity set plan/spec/migration/ui never resolves below frontier under any
|
|
417
|
+
# # mode). Absent = risk-based = byte-identical routing. Explicit floors below beat mode deltas (linted).
|
|
418
|
+
# map: # per-shape preferences/pins; band policy lives in routing.floors
|
|
419
|
+
# implement: { prefer: [cursor-agent, codex] }
|
|
326
420
|
# migration: { pin: { via: claude-code, model: fable } }
|
|
327
|
-
#
|
|
421
|
+
# tests: { tier: null } # tombstone: null removes a legacy map tier (deprecated — move the band to routing.floors.tests; schema removal lands next version)
|
|
422
|
+
# floors: # tier authority — advisory minimum bands; 'tickmarkr plan' lints violations
|
|
328
423
|
# migration: frontier
|
|
329
424
|
# learned: on # default ON (ROUTE-14); cold profile = exact v1.5 static routing, warms per workspace. Set 'off' to pin static routing; preview with 'tickmarkr plan'
|
|
330
425
|
# learnedTuning: { halfLifeRuns: 5, availWeight: 0.05 } # optional; defaults byte-identical
|
package/dist/graph/schema.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
export declare const SHAPES: readonly ["plan", "spec", "implement", "tests", "docs", "migration", "ui", "refactor", "chore"];
|
|
3
|
+
export declare const GRAPH_ROUTING_MODES: readonly ["partner-led", "risk-based", "staff-led"];
|
|
3
4
|
export declare const STATUSES: readonly ["pending", "running", "gated", "failed", "done", "human"];
|
|
4
5
|
export declare const GATE_NAMES: readonly ["build", "test", "lint", "evidence", "scope", "acceptance", "review"];
|
|
5
6
|
export declare const TIERS: readonly ["cheap", "mid", "frontier"];
|
|
@@ -94,6 +95,11 @@ export declare const TaskSchema: z.ZodObject<{
|
|
|
94
95
|
}, z.core.$strip>;
|
|
95
96
|
export declare const RunGraphSchema: z.ZodObject<{
|
|
96
97
|
version: z.ZodLiteral<1>;
|
|
98
|
+
mode: z.ZodOptional<z.ZodEnum<{
|
|
99
|
+
"partner-led": "partner-led";
|
|
100
|
+
"risk-based": "risk-based";
|
|
101
|
+
"staff-led": "staff-led";
|
|
102
|
+
}>>;
|
|
97
103
|
spec: z.ZodObject<{
|
|
98
104
|
source: z.ZodEnum<{
|
|
99
105
|
speckit: "speckit";
|
package/dist/graph/schema.js
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
export const SHAPES = ["plan", "spec", "implement", "tests", "docs", "migration", "ui", "refactor", "chore"];
|
|
3
|
+
// v1.51 T2: spec-declared routing mode. Mirrors config ROUTING_MODES literally — config.ts imports this
|
|
4
|
+
// module, so a runtime import back would be circular; parity is pinned by test (tests/cli/mode-sources).
|
|
5
|
+
export const GRAPH_ROUTING_MODES = ["partner-led", "risk-based", "staff-led"];
|
|
3
6
|
export const STATUSES = ["pending", "running", "gated", "failed", "done", "human"];
|
|
4
7
|
export const GATE_NAMES = ["build", "test", "lint", "evidence", "scope", "acceptance", "review"];
|
|
5
8
|
const MANDATORY_GATES = ["build", "test", "lint", "evidence", "scope"];
|
|
@@ -88,6 +91,9 @@ export const TaskSchema = z.object({
|
|
|
88
91
|
export const RunGraphSchema = z
|
|
89
92
|
.object({
|
|
90
93
|
version: z.literal(1),
|
|
94
|
+
// v1.51 T2: spec front-matter mode — the spec author's routing-mode declaration. Source
|
|
95
|
+
// precedence: run flag > this > repo config > global config > default (risk-based).
|
|
96
|
+
mode: z.enum(GRAPH_ROUTING_MODES).optional(),
|
|
91
97
|
spec: z.object({
|
|
92
98
|
source: z.enum(["speckit", "gsd", "prd", "native", "taskmaster"]),
|
|
93
99
|
paths: z.array(z.string()),
|
package/dist/route/profile.d.ts
CHANGED
|
@@ -83,6 +83,18 @@ export declare function learnedScoreTerms(profile: RoutingProfile | undefined, s
|
|
|
83
83
|
availWeight?: number;
|
|
84
84
|
slaMinutes?: number;
|
|
85
85
|
}): LearnedScoreTerms;
|
|
86
|
+
export declare const STAFF_LED_MARGIN = 0.1;
|
|
87
|
+
export interface StaffLedEvidence {
|
|
88
|
+
cheapBest: number;
|
|
89
|
+
midBest: number;
|
|
90
|
+
n: number;
|
|
91
|
+
}
|
|
92
|
+
export declare function staffLedEvidence(profile: RoutingProfile | undefined, shape: string, channels: readonly {
|
|
93
|
+
adapter: string;
|
|
94
|
+
model: string;
|
|
95
|
+
channel: string;
|
|
96
|
+
tier: string;
|
|
97
|
+
}[]): StaffLedEvidence | null;
|
|
86
98
|
export declare function learnedScore(profile: RoutingProfile | undefined, shape: string, chKey: string, channel: string, opts?: {
|
|
87
99
|
availWeight?: number;
|
|
88
100
|
slaMinutes?: number;
|
package/dist/route/profile.js
CHANGED
|
@@ -217,6 +217,37 @@ export function learnedScoreTerms(profile, shape, chKey, channel, opts = {}) {
|
|
|
217
217
|
const overrun = cell.dispatches < MIN_SAMPLES ? 0 : -OVERRUN_WEIGHT * ((cell.overruns ?? 0) / cell.dispatches); // ∈ [−OVERRUN_WEIGHT, 0]
|
|
218
218
|
return { quality, perf, avail, overrun };
|
|
219
219
|
}
|
|
220
|
+
// v1.51 T5 staff-led economics guard (round-3 Fable §3.3): ONE comparison over existing warm cells,
|
|
221
|
+
// consumed by plan as an ADVISORY lint only. Evidence display, never a floor input — no routing path
|
|
222
|
+
// reads this (anticipatory raises are refused house law: bands move on declarations, structural
|
|
223
|
+
// signals, or the failure ladder — never on the profile's prediction).
|
|
224
|
+
export const STAFF_LED_MARGIN = 0.1; // "materially below" = two quality quanta (2 × the 0.05 term weights)
|
|
225
|
+
// Best WARM learned score per band (cold cells — n < MIN_SAMPLES — are not evidence; the warm best is
|
|
226
|
+
// the band's incumbent proxy). Null unless BOTH bands are warm AND the mid incumbent leads by at least
|
|
227
|
+
// STAFF_LED_MARGIN: silence is the default, so a cold profile can never fire the lint.
|
|
228
|
+
export function staffLedEvidence(profile, shape, channels) {
|
|
229
|
+
if (!profile)
|
|
230
|
+
return null;
|
|
231
|
+
const best = (tier) => {
|
|
232
|
+
let top = null;
|
|
233
|
+
for (const c of channels) {
|
|
234
|
+
if (c.tier !== tier)
|
|
235
|
+
continue;
|
|
236
|
+
const cell = cellOf(profile, shape, channelKey(c), c.channel);
|
|
237
|
+
if (!cell || cell.n < MIN_SAMPLES)
|
|
238
|
+
continue;
|
|
239
|
+
const score = learnedScore(profile, shape, channelKey(c), c.channel);
|
|
240
|
+
if (!top || score > top.score)
|
|
241
|
+
top = { score, nRaw: cell.nRaw ?? 0 };
|
|
242
|
+
}
|
|
243
|
+
return top;
|
|
244
|
+
};
|
|
245
|
+
const cheap = best("cheap");
|
|
246
|
+
const mid = best("mid");
|
|
247
|
+
if (!cheap || !mid || mid.score - cheap.score < STAFF_LED_MARGIN)
|
|
248
|
+
return null;
|
|
249
|
+
return { cheapBest: cheap.score, midBest: mid.score, n: cheap.nRaw + mid.nRaw };
|
|
250
|
+
}
|
|
220
251
|
// Total by construction: every miss returns the explicit NEUTRAL constant; every denominator is a
|
|
221
252
|
// compile-time-positive constant sum; no Record indexing by row strings (the TIER_RANK[typo] scar).
|
|
222
253
|
// Score 0 defers to the static sort keys, which already encode the benchmark-dated tier seeds — a
|
package/dist/run/daemon.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { type WorkerAdapter } from "../adapters/types.js";
|
|
2
|
+
import { type ModeResolution, type RoutingMode, type TickmarkrConfig } from "../config/config.js";
|
|
2
3
|
import { type ExecutorDriver } from "../drivers/types.js";
|
|
3
4
|
import { type JournalEvent } from "./journal.js";
|
|
4
5
|
export interface RunOptions {
|
|
@@ -9,8 +10,23 @@ export interface RunOptions {
|
|
|
9
10
|
driver?: ExecutorDriver;
|
|
10
11
|
adapters?: WorkerAdapter[];
|
|
11
12
|
globalDir?: string;
|
|
13
|
+
mode?: RoutingMode;
|
|
12
14
|
narrate?: (event: JournalEvent) => void;
|
|
13
15
|
}
|
|
16
|
+
export type ModeSource = "run flag" | "spec" | "repo config" | "global config" | "default";
|
|
17
|
+
export interface ResolvedRunMode {
|
|
18
|
+
cfg: TickmarkrConfig;
|
|
19
|
+
/** effective mode + per-floor provenance + standing lints, from the ONE preset compiler in config.ts */
|
|
20
|
+
mode: ModeResolution;
|
|
21
|
+
source: ModeSource;
|
|
22
|
+
/** set when the run flag picked a mode below the spec-declared mode (loud warn; --route-strict refuses) */
|
|
23
|
+
conflict?: string;
|
|
24
|
+
}
|
|
25
|
+
export declare function resolveRunMode(repoRoot: string, opts?: {
|
|
26
|
+
flag?: RoutingMode;
|
|
27
|
+
spec?: RoutingMode;
|
|
28
|
+
globalDir?: string;
|
|
29
|
+
}): ResolvedRunMode;
|
|
14
30
|
export interface RunSummary {
|
|
15
31
|
runId: string;
|
|
16
32
|
branch: string;
|
package/dist/run/daemon.js
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { randomBytes } from "node:crypto";
|
|
2
2
|
import { shq } from "../adapters/types.js";
|
|
3
|
-
import { existsSync, readFileSync, writeFileSync } from "node:fs";
|
|
3
|
+
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { tmpdir } from "node:os";
|
|
4
5
|
import { join } from "node:path";
|
|
6
|
+
import { stringify } from "yaml";
|
|
5
7
|
import { trailerPattern, writePrompt } from "../adapters/prompt.js";
|
|
6
8
|
import { allAdapters, discoverChannels, getAdapter, probeAll, readDoctor } from "../adapters/registry.js";
|
|
7
9
|
import { addUsage, channelKey, matchesTrustDialog, QUOTA_RE } from "../adapters/types.js";
|
|
8
10
|
import { bannerShell } from "../brand.js";
|
|
9
|
-
import {
|
|
11
|
+
import { globalConfigDir, loadConfigWithMode, readOverlayFile, repoOverlayPath, } from "../config/config.js";
|
|
10
12
|
import { herdrSealShellPrefix, SubprocessDriver } from "../drivers/subprocess.js";
|
|
11
13
|
import { formatOwnedName } from "../drivers/types.js";
|
|
12
14
|
import { captureBaseline, detectGateCommands } from "../gates/baseline.js";
|
|
@@ -17,8 +19,41 @@ import { cleanupRunWorktrees, gitHead, linkNodeModules, sh, shGit, WORKTREE_LAYO
|
|
|
17
19
|
import { classifyWorkerResultCause, engagementComparable, Journal, loadRoutingProfile, newRunId } from "./journal.js";
|
|
18
20
|
import { acquireRunLock, releaseRunLock } from "./lock.js";
|
|
19
21
|
import { ensureIntegration, integrationBranch, integrationHead, mergeTask, verifyIntegrationTip } from "./merge.js";
|
|
20
|
-
import { nextChannel, route } from "../route/router.js";
|
|
22
|
+
import { nextChannel, QUALITY_ENV, route } from "../route/router.js";
|
|
21
23
|
import { desiredPanes } from "./reconcile.js";
|
|
24
|
+
const MODE_RANK = { "staff-led": 0, "risk-based": 1, "partner-led": 2 };
|
|
25
|
+
// An override (flag/spec) re-resolves through loadConfigWithMode itself, via a synthesized repo overlay
|
|
26
|
+
// carrying routing.mode — floors, explore, lints, and provenance all come from config.ts's preset
|
|
27
|
+
// compiler, never duplicated mode math here (the quality-silently-loses defense holds by construction).
|
|
28
|
+
function withOverlayMode(repoRoot, mode, globalDir) {
|
|
29
|
+
const overlay = readOverlayFile(repoOverlayPath(repoRoot));
|
|
30
|
+
const tmp = mkdtempSync(join(tmpdir(), "tickmarkr-mode-"));
|
|
31
|
+
try {
|
|
32
|
+
mkdirSync(join(tmp, ".tickmarkr"), { recursive: true });
|
|
33
|
+
writeFileSync(join(tmp, ".tickmarkr", "config.yaml"), stringify({ ...overlay, routing: { ...overlay.routing, mode } }));
|
|
34
|
+
return loadConfigWithMode(tmp, { globalDir });
|
|
35
|
+
}
|
|
36
|
+
finally {
|
|
37
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
export function resolveRunMode(repoRoot, opts = {}) {
|
|
41
|
+
const overlayMode = (path) => readOverlayFile(path).routing?.mode;
|
|
42
|
+
const source = opts.flag !== undefined ? "run flag"
|
|
43
|
+
: opts.spec !== undefined ? "spec"
|
|
44
|
+
: overlayMode(repoOverlayPath(repoRoot)) !== undefined ? "repo config"
|
|
45
|
+
: overlayMode(join(opts.globalDir ?? globalConfigDir(), "config.yaml")) !== undefined ? "global config"
|
|
46
|
+
: "default";
|
|
47
|
+
const override = opts.flag ?? opts.spec;
|
|
48
|
+
const base = loadConfigWithMode(repoRoot, { globalDir: opts.globalDir });
|
|
49
|
+
const resolved = override === undefined || override === base.mode.mode
|
|
50
|
+
? base
|
|
51
|
+
: withOverlayMode(repoRoot, override, opts.globalDir);
|
|
52
|
+
const conflict = opts.flag !== undefined && opts.spec !== undefined && MODE_RANK[opts.flag] < MODE_RANK[opts.spec]
|
|
53
|
+
? `mode conflict: run flag ${opts.flag} selects a mode below the spec-declared ${opts.spec} — the run flag wins this run`
|
|
54
|
+
: undefined;
|
|
55
|
+
return { cfg: resolved.cfg, mode: resolved.mode, source, ...(conflict ? { conflict } : {}) };
|
|
56
|
+
}
|
|
22
57
|
// VIS-01: one formatter, four readers (run-end journal event, run/resume CLI, run-end notify).
|
|
23
58
|
// Parity by construction — every caller renders the same complete bucket line.
|
|
24
59
|
export function formatSummary(s) {
|
|
@@ -54,14 +89,11 @@ async function cherryPickCommits(wt, commits) {
|
|
|
54
89
|
return carried;
|
|
55
90
|
}
|
|
56
91
|
export async function runDaemon(repoRoot, opts = {}) {
|
|
57
|
-
|
|
92
|
+
// v1.51 T2: retired --quality env seam. Mode resolution owns premium routing now; an exported
|
|
93
|
+
// TICKMARKR_QUALITY must not resurrect the old one-band floor raise in daemon dispatches.
|
|
94
|
+
delete process.env[QUALITY_ENV];
|
|
58
95
|
const adapters = opts.adapters ?? allAdapters();
|
|
59
96
|
const health = readDoctor(repoRoot) ?? (await probeAll(adapters));
|
|
60
|
-
const channels = discoverChannels(cfg, adapters, health);
|
|
61
|
-
// v1.6 ROUTE-06: build the learned profile ONCE at startup (never per task, never in the comparator).
|
|
62
|
-
// No preview — the daemon honors routing.learned:off and gets undefined; this snapshot is immutable
|
|
63
|
-
// for the run, so this run's own telemetry never feeds back into its own routing.
|
|
64
|
-
const profile = loadRoutingProfile(repoRoot, cfg);
|
|
65
97
|
const driver = opts.driver ?? new SubprocessDriver();
|
|
66
98
|
// HARD-01/02: hold the run lock across the whole read-modify-write of graph.json. Acquire
|
|
67
99
|
// BEFORE loadGraph; release in the finally below (every exit path, incl. throws).
|
|
@@ -72,6 +104,18 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
72
104
|
const lock = acquireRunLock(repoRoot, runId);
|
|
73
105
|
try {
|
|
74
106
|
let graph = loadGraph(repoRoot);
|
|
107
|
+
// v1.51 T2: the routing mode resolves BEFORE any routing input is built — run flag > spec front-matter
|
|
108
|
+
// > repo > global > default. The resolved cfg carries mode-compiled floors; route() never sees the mode.
|
|
109
|
+
const rm = resolveRunMode(repoRoot, { flag: opts.mode, spec: graph.mode, globalDir: opts.globalDir });
|
|
110
|
+
const cfg = rm.cfg;
|
|
111
|
+
// v1.51 T4: every dispatch provenance line begins with the mode and its source; when a pin won
|
|
112
|
+
// the route (the final "→ " segment is a pin, not a degraded-to-auto tail) it names the mode it bypassed.
|
|
113
|
+
const dispatchProvenance = (p) => `mode ${rm.mode.mode} (${rm.source})${p.split("→ ").pop().startsWith("pin ") ? ` — pin bypasses mode ${rm.mode.mode}` : ""} · ${p}`;
|
|
114
|
+
const channels = discoverChannels(cfg, adapters, health);
|
|
115
|
+
// v1.6 ROUTE-06: build the learned profile ONCE at startup (never per task, never in the comparator).
|
|
116
|
+
// No preview — the daemon honors routing.learned:off and gets undefined; this snapshot is immutable
|
|
117
|
+
// for the run, so this run's own telemetry never feeds back into its own routing.
|
|
118
|
+
const profile = loadRoutingProfile(repoRoot, cfg);
|
|
75
119
|
const journal = opts.resume ? Journal.open(repoRoot, runId, opts.narrate) : Journal.create(repoRoot, runId, opts.narrate);
|
|
76
120
|
const branchEvent = opts.resume
|
|
77
121
|
? [...journal.read()].reverse().find((e) => (e.event === "run-start" || e.event === "run-end" || e.event === "merge") && typeof e.data.branch === "string")
|
|
@@ -131,7 +175,7 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
131
175
|
baseRef = await gitHead(repoRoot);
|
|
132
176
|
baseline = await captureBaseline(repoRoot, commands);
|
|
133
177
|
writeFileSync(join(journal.dir, "baseline.json"), JSON.stringify(baseline, null, 2));
|
|
134
|
-
journal.append("run-start", undefined, { pid: process.pid, baseRef, commands, channels: channels.map(channelKey), branch, graphDefinitionHash: graphDefinitionHash(graph) }); // graphDefinitionHash: T3 engagement identity (status+resume share it); pid: v1.13 (VIS-11) liveness
|
|
178
|
+
journal.append("run-start", undefined, { pid: process.pid, baseRef, commands, channels: channels.map(channelKey), branch, graphDefinitionHash: graphDefinitionHash(graph), mode: rm.mode.mode, modeSource: rm.source }); // graphDefinitionHash: T3 engagement identity (status+resume share it); pid: v1.13 (VIS-11) liveness; mode/modeSource: v1.51 T2
|
|
135
179
|
}
|
|
136
180
|
// T6: open the narrator AFTER run-start/run-resume is journaled so the watch surface has a run to
|
|
137
181
|
// show. driver.narrator is undefined on subprocess → no-op (subprocess spawns nothing). Swallowed:
|
|
@@ -389,7 +433,7 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
389
433
|
lastContextTokens = undefined;
|
|
390
434
|
graph = setStatus(graph, t.id, "running");
|
|
391
435
|
saveGraph(repoRoot, graph);
|
|
392
|
-
journal.append("task-dispatch", t.id, { assignment, attempt, provenance: r.provenance, retryMode });
|
|
436
|
+
journal.append("task-dispatch", t.id, { assignment, attempt, provenance: dispatchProvenance(r.provenance), retryMode });
|
|
393
437
|
const taskBase = await integrationHead(intWt); // deps are merged → visible to this task
|
|
394
438
|
const taskBranch = `${branch}--${t.id}`; // "--": a ref can't nest under the existing integration branch (locked decision 10)
|
|
395
439
|
const priorWt = worktreePath(repoRoot, taskBranch);
|