tickmarkr 1.85.0 → 1.87.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -2
- package/dist/adapters/catalog-remote.d.ts +64 -0
- package/dist/adapters/catalog-remote.js +287 -0
- package/dist/adapters/catalog.d.ts +108 -0
- package/dist/adapters/catalog.js +189 -0
- package/dist/adapters/claude-code.js +5 -3
- package/dist/adapters/fake.js +33 -4
- package/dist/adapters/model-lints.d.ts +25 -5
- package/dist/adapters/model-lints.js +184 -50
- package/dist/adapters/model-windows.d.ts +31 -0
- package/dist/adapters/model-windows.js +69 -0
- package/dist/adapters/prompt.d.ts +5 -1
- package/dist/adapters/prompt.js +13 -4
- package/dist/adapters/registry.d.ts +34 -27
- package/dist/adapters/registry.js +215 -112
- package/dist/adapters/types.js +15 -3
- package/dist/brand.d.ts +5 -1
- package/dist/brand.js +18 -2
- package/dist/cli/commands/doctor.d.ts +3 -0
- package/dist/cli/commands/doctor.js +43 -21
- package/dist/cli/commands/fleet.d.ts +7 -0
- package/dist/cli/commands/fleet.js +94 -74
- package/dist/cli/commands/init.js +118 -5
- package/dist/cli/commands/plan.js +11 -1
- package/dist/cli/commands/resume.js +7 -1
- package/dist/cli/commands/status.js +44 -18
- package/dist/compile/gsd.d.ts +2 -1
- package/dist/compile/gsd.js +68 -2
- package/dist/compile/native.d.ts +14 -0
- package/dist/compile/native.js +168 -12
- package/dist/config/config.d.ts +20 -5
- package/dist/config/config.js +96 -64
- package/dist/config/fleet-overlay.d.ts +25 -20
- package/dist/config/fleet-overlay.js +195 -77
- package/dist/config/fleet-why.d.ts +23 -0
- package/dist/config/fleet-why.js +42 -0
- package/dist/drivers/herdr.d.ts +1 -0
- package/dist/drivers/herdr.js +70 -19
- package/dist/gates/acceptance.js +7 -2
- package/dist/gates/llm.d.ts +0 -1
- package/dist/gates/llm.js +5 -30
- package/dist/gates/review.d.ts +2 -1
- package/dist/gates/review.js +9 -7
- package/dist/gates/run-gates.d.ts +1 -0
- package/dist/gates/run-gates.js +21 -2
- package/dist/gates/verdict-cause.d.ts +4 -0
- package/dist/gates/verdict-cause.js +63 -0
- package/dist/graph/schema.d.ts +6 -0
- package/dist/graph/schema.js +8 -5
- package/dist/route/preference.d.ts +1 -1
- package/dist/route/preference.js +8 -1
- package/dist/route/router.d.ts +0 -5
- package/dist/route/router.js +16 -20
- package/dist/run/consult.d.ts +6 -0
- package/dist/run/consult.js +49 -26
- package/dist/run/daemon.js +81 -20
- package/dist/run/journal.js +87 -7
- package/dist/tui/cockpit/capture.d.ts +12 -0
- package/dist/tui/cockpit/capture.js +37 -1
- package/dist/tui/cockpit/components.js +8 -8
- package/dist/tui/cockpit/theme.d.ts +32 -26
- package/dist/tui/cockpit/theme.js +11 -5
- package/dist/tui/ink/components.d.ts +0 -15
- package/dist/tui/ink/components.js +0 -17
- package/dist/tui/ink/fleet-app.d.ts +4 -1
- package/dist/tui/ink/fleet-app.js +134 -13
- package/fixtures/gateway-models.json +1 -0
- package/fixtures/sample.native.md +1 -1
- package/package.json +1 -1
- package/skills/tickmarkr-overseer/SKILL.md +464 -34
- package/skills/tickmarkr-overseer/scripts/watch-artifacts.sh +86 -0
- package/skills/tickmarkr-overseer/scripts/watch-panes.sh +1 -1
- package/dist/tui/ink/studio-app.d.ts +0 -59
- package/dist/tui/ink/studio-app.js +0 -320
- package/dist/tui/save.d.ts +0 -38
- package/dist/tui/save.js +0 -96
- package/dist/tui/staging.d.ts +0 -29
- package/dist/tui/staging.js +0 -78
|
@@ -9,6 +9,7 @@ import { foldActivity } from "../../run/activity.js";
|
|
|
9
9
|
import { Journal, engagementComparable, isQualityFailureParkKind, recordedTaskFailureKind, runHasEnded, } from "../../run/journal.js";
|
|
10
10
|
import { normalizeStallSnapshot } from "../../run/stall.js";
|
|
11
11
|
import { deriveRunCockpitData, } from "../../tui/cockpit/derive.js";
|
|
12
|
+
import { cellWidth, fitCells } from "../../tui/cockpit/width.js";
|
|
12
13
|
// ponytail: fixed 2s refresh; promote to config.visibility.* only when an operator asks.
|
|
13
14
|
const REFRESH_MS = 2000;
|
|
14
15
|
const NOT_COMPARABLE_NOTICE = "graph recompiled since this run — task states not comparable; resume with `--graph-changed` to audit this recompile";
|
|
@@ -132,6 +133,20 @@ const optionValue = (argv, name) => {
|
|
|
132
133
|
const value = index >= 0 ? argv[index + 1] : undefined;
|
|
133
134
|
return value && !value.startsWith("-") ? value : undefined;
|
|
134
135
|
};
|
|
136
|
+
// The one positional this command takes: an explicit run id. Everything else is a flag or a flag's
|
|
137
|
+
// value (`--webhook <url>`), so the first bare token that is not a --webhook value is the run id.
|
|
138
|
+
const positionalRunId = (argv) => {
|
|
139
|
+
for (let i = 0; i < argv.length; i++) {
|
|
140
|
+
const arg = argv[i];
|
|
141
|
+
if (arg === "--webhook") {
|
|
142
|
+
i += 1;
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
if (!arg.startsWith("-"))
|
|
146
|
+
return arg;
|
|
147
|
+
}
|
|
148
|
+
return undefined;
|
|
149
|
+
};
|
|
135
150
|
const defaultPostWebhook = (url, event) => fetch(url, {
|
|
136
151
|
method: "POST",
|
|
137
152
|
headers: { "content-type": "application/json" },
|
|
@@ -307,13 +322,18 @@ const failedSuffix = (states) => {
|
|
|
307
322
|
// cell when present: a post-approval park is not awaiting the designed gate. Plain text for column
|
|
308
323
|
// math; rendered with a dim dot + warn words. TTY-only — the non-TTY surface stays byte-pinned.
|
|
309
324
|
export const humanGateSuffix = (t, st, states) => st === "human" && t.humanGate && failedGates(states).length === 0 ? " · awaiting approval" : "";
|
|
325
|
+
// Task titles are graph-authored data. Remove terminal controls and row-breaking whitespace before
|
|
326
|
+
// either width measurement or rendering so zero-cell ECMA-48 bytes cannot escape the task column.
|
|
327
|
+
const ECMA_48_CONTROL = /(?:\u001B\][\s\S]*?(?:\u0007|\u001B\\)|\u009D[\s\S]*?(?:\u0007|\u009C)|\u001B[P^_X][\s\S]*?\u001B\\|[\u0090\u0098\u009E\u009F][\s\S]*?\u009C|(?:\u001B\[|\u009B)[0-?]*[ -/]*[@-~]|\u001B[@-_])/gu;
|
|
328
|
+
const sanitizeTaskText = (text) => text.replace(ECMA_48_CONTROL, "").replace(/\p{Cc}/gu, " ").replace(/\s+/gu, " ").trim();
|
|
310
329
|
export const shortGoal = (goal, max) => {
|
|
311
|
-
const clause = goal.split(/[,;.?!]/, 1)[0].trim();
|
|
312
|
-
|
|
330
|
+
const clause = sanitizeTaskText(goal).split(/[,;.?!]/, 1)[0].trim();
|
|
331
|
+
const cells = Math.max(0, Math.floor(max));
|
|
332
|
+
if (cellWidth(clause) <= cells)
|
|
313
333
|
return clause;
|
|
314
|
-
if (
|
|
315
|
-
return clause
|
|
316
|
-
return `${clause
|
|
334
|
+
if (cells <= 3)
|
|
335
|
+
return fitCells(clause, cells).trimEnd();
|
|
336
|
+
return `${fitCells(clause, cells - 3).trimEnd()}...`;
|
|
317
337
|
};
|
|
318
338
|
// VIS-11 (v1.13): a liveness header for renderFrame — last journal event age + whether the recorded
|
|
319
339
|
// daemon pid is still alive. Honest about unknowns: a pre-v1.13 journal with no pid renders "unknown",
|
|
@@ -487,9 +507,11 @@ const parseJournalSnapshot = (raw) => raw.split("\n").flatMap((line) => {
|
|
|
487
507
|
return [];
|
|
488
508
|
}
|
|
489
509
|
});
|
|
490
|
-
const renderFrame = (cwd, now = Date.now(), animationFrame = 0, workerLiveness = new Map(), journalRowsOnly = false) => {
|
|
510
|
+
const renderFrame = (cwd, now = Date.now(), animationFrame = 0, workerLiveness = new Map(), journalRowsOnly = false, namedRunId) => {
|
|
491
511
|
const g = loadGraph(cwd);
|
|
492
|
-
|
|
512
|
+
// An explicit <runId> is a resolution, not a hint: Journal.open below refuses an id without a
|
|
513
|
+
// readable journal, so status fails loudly naming that id instead of rendering any other run.
|
|
514
|
+
const runId = namedRunId ?? Journal.latestRunId(cwd, { withJournal: true });
|
|
493
515
|
const assignments = new Map();
|
|
494
516
|
let replayed = null;
|
|
495
517
|
let cockpit;
|
|
@@ -619,7 +641,7 @@ const renderFrame = (cwd, now = Date.now(), animationFrame = 0, workerLiveness =
|
|
|
619
641
|
const chain = gateChain(states, false);
|
|
620
642
|
const prefix = livePhase ? ` ${ASCII_SPINNER[animationFrame % ASCII_SPINNER.length]} ${t.id} ` : ` ${surfaceTaskBox(st, merged)} ${t.id} `;
|
|
621
643
|
const suffix = ` ${chain}${priorGraph ? ` ${PRIOR_GRAPH_MARKER}` : ""} ${livePhase ? "running" : surfaceStatusWord(st)}${label} ${assignCol}`;
|
|
622
|
-
return `${prefix}${shortGoal(t.
|
|
644
|
+
return `${prefix}${shortGoal(t.title, Math.max(0, width - prefix.length - suffix.length))}${suffix}`;
|
|
623
645
|
});
|
|
624
646
|
const zone = journalRowsOnly ? `${divider}zone ${localZoneLabel(zoneReference)}` : "";
|
|
625
647
|
const header = runId
|
|
@@ -668,9 +690,9 @@ const renderFrame = (cwd, now = Date.now(), animationFrame = 0, workerLiveness =
|
|
|
668
690
|
const nowLine = activity.now ? [legend(` now: ${activity.now}`)] : [];
|
|
669
691
|
const gatesLegend = legend(` gates: ${GATE_NAMES.join(" · ")}`);
|
|
670
692
|
// Two-line card per task (operator request, v1.67): line 1 carries identity + verdict — glyph,
|
|
671
|
-
// id,
|
|
672
|
-
// aligned under the
|
|
673
|
-
// and activity phrases live on line 2 only, so they can never squeeze the
|
|
693
|
+
// id, title at full width, and only SHORT status words. Line 2 carries the machinery, dim and
|
|
694
|
+
// aligned under the title: gate chain, live activity phrase (or channel), ctx. Long channel names
|
|
695
|
+
// and activity phrases live on line 2 only, so they can never squeeze the title or wrap line 1.
|
|
674
696
|
const taskVerdict = (c) => c.merged ? "pass" : c.redTier ? "fail" : c.st === "failed" || c.st === "human" ? "warn" : "neutral";
|
|
675
697
|
const statusWord = (c) => c.livePhase ? "running" : c.redTier ? "failed" : c.st === "failed" ? "warn" : surfaceStatusWord(c.st);
|
|
676
698
|
const idW = Math.max(...cells.map((c) => c.t.id.length), 2);
|
|
@@ -679,9 +701,9 @@ const renderFrame = (cwd, now = Date.now(), animationFrame = 0, workerLiveness =
|
|
|
679
701
|
+ humanGateSuffix(c.t, graphTaskStatus(c.st, c.t.status), c.states);
|
|
680
702
|
const stW = Math.max(...cells.map((c) => statusWord(c).length + suffixPlain(c).length));
|
|
681
703
|
const avail = Math.max(8, width - (5 + idW) - 2 - stW);
|
|
682
|
-
const
|
|
683
|
-
const
|
|
684
|
-
const indent = " ".repeat(idW + 5); // line 2 starts under the
|
|
704
|
+
const titles = cells.map((c) => shortGoal(c.t.title, avail));
|
|
705
|
+
const titleW = Math.max(8, ...titles.map(cellWidth));
|
|
706
|
+
const indent = " ".repeat(idW + 5); // line 2 starts under the title column
|
|
685
707
|
const rows = cells.map((c, i) => {
|
|
686
708
|
const { t, st, merged, failureKind, redTier, states, priorGraph, isStarved, phrase, channel, ctx, livePhase } = c;
|
|
687
709
|
const word = statusWord(c);
|
|
@@ -695,7 +717,8 @@ const renderFrame = (cwd, now = Date.now(), animationFrame = 0, workerLiveness =
|
|
|
695
717
|
(isStarved ? dot + fail("starved") : "") +
|
|
696
718
|
(f.length ? dot + fail(f.join(", ")) : "") +
|
|
697
719
|
(human ? dot + warn("awaiting approval") : "");
|
|
698
|
-
const
|
|
720
|
+
const taskTitle = titles[i];
|
|
721
|
+
const taskLabel = `${t.id.padEnd(idW)} ${taskTitle}${" ".repeat(titleW - cellWidth(taskTitle))} ${statusCell}`;
|
|
699
722
|
const line1 = livePhase
|
|
700
723
|
? ` ${(staleWorker ? warn : dim)(SPINNER[animationFrame % SPINNER.length])} ${taskLabel}`
|
|
701
724
|
: ` ${statusRow(taskVerdict(c), taskLabel)}`;
|
|
@@ -718,9 +741,10 @@ const renderFrame = (cwd, now = Date.now(), animationFrame = 0, workerLiveness =
|
|
|
718
741
|
};
|
|
719
742
|
};
|
|
720
743
|
export async function status(argv, cwd = process.cwd(), opts = {}) {
|
|
744
|
+
const namedRunId = positionalRunId(argv);
|
|
721
745
|
// cockpit surface: banner + frame on a TTY (doctor's pattern); pipes get the bare frame
|
|
722
746
|
if (!argv.includes("--watch")) {
|
|
723
|
-
const { content } = renderFrame(cwd, opts.now?.() ?? Date.now());
|
|
747
|
+
const { content } = renderFrame(cwd, opts.now?.() ?? Date.now(), 0, undefined, false, namedRunId);
|
|
724
748
|
return visual() ? BANNER + content : content;
|
|
725
749
|
}
|
|
726
750
|
const eventStream = argv.some((arg) => arg === "--events" || arg === "--jsonl" || arg === "--decision-events");
|
|
@@ -782,7 +806,9 @@ export async function status(argv, cwd = process.cwd(), opts = {}) {
|
|
|
782
806
|
let decisionRunId;
|
|
783
807
|
let journalCursor = 0;
|
|
784
808
|
const consumeDecisionEvents = () => {
|
|
785
|
-
|
|
809
|
+
// A named run is followed, never re-resolved: --watch <runId> keeps reporting that run even as
|
|
810
|
+
// newer runs start. Only the no-argument form tracks latest, as before.
|
|
811
|
+
const runId = namedRunId ?? Journal.latestRunId(cwd, { withJournal: true });
|
|
786
812
|
if (!runId)
|
|
787
813
|
return [];
|
|
788
814
|
if (decisionRunId !== runId) {
|
|
@@ -839,7 +865,7 @@ export async function status(argv, cwd = process.cwd(), opts = {}) {
|
|
|
839
865
|
}
|
|
840
866
|
}
|
|
841
867
|
else {
|
|
842
|
-
frame = renderFrame(cwd, nowMs, i, workerLiveness, true);
|
|
868
|
+
frame = renderFrame(cwd, nowMs, i, workerLiveness, true, namedRunId);
|
|
843
869
|
if (tty) {
|
|
844
870
|
updateTitle(frame.hotPhase, nowMs);
|
|
845
871
|
process.stdout.write(`\x1b[2J\x1b[H${BANNER}${frame.content}\n${legend(` watching · refresh ${REFRESH_MS / 1000}s · ^C to quit`)}`);
|
package/dist/compile/gsd.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
-
import { type RunGraph } from "../graph/schema.js";
|
|
1
|
+
import { type AcceptanceItem, type RunGraph } from "../graph/schema.js";
|
|
2
2
|
export declare function isGsdPhaseDir(dir: string): boolean;
|
|
3
|
+
export declare function parseTruths(file: string, raw: unknown): AcceptanceItem[] | undefined;
|
|
3
4
|
export declare function compileGsd(src: string, root?: string): RunGraph;
|
package/dist/compile/gsd.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
2
2
|
import { basename, dirname, isAbsolute, join, relative } from "node:path";
|
|
3
3
|
import { parse as parseYaml } from "yaml";
|
|
4
|
-
import { TIERS, validateGraph } from "../graph/schema.js";
|
|
4
|
+
import { AcceptanceItemSchema, TIERS, validateGraph } from "../graph/schema.js";
|
|
5
5
|
import { CompileError, assertWriteScope, inferShape, sha256 } from "./common.js";
|
|
6
6
|
// GSD artifact front-end (spec v1.3): one GSD *plan* is one tickmarkr *task* — a plan is
|
|
7
7
|
// worktree-sized; its inner <task> steps stay in the worker prompt via context[0] = the plan file.
|
|
@@ -16,6 +16,72 @@ export function isGsdPhaseDir(dir) {
|
|
|
16
16
|
}
|
|
17
17
|
}
|
|
18
18
|
const strings = (v) => (Array.isArray(v) ? v.map(String) : []);
|
|
19
|
+
// Cycle-safe by construction: a recursive YAML alias (`&a [*a]`) is a legal value the parser hands
|
|
20
|
+
// back as a circular object, and a bare JSON.stringify throws a native TypeError on it — which would
|
|
21
|
+
// REPLACE the indexed CompileError this module owes the author with an unrelated crash. Every value
|
|
22
|
+
// the YAML parser can produce renders here. (A repeated non-recursive alias also renders [circular];
|
|
23
|
+
// this is a diagnostic string, not a round-trippable encoding.)
|
|
24
|
+
const show = (v) => {
|
|
25
|
+
const seen = new WeakSet();
|
|
26
|
+
const cycleSafe = (_k, x) => {
|
|
27
|
+
if (typeof x !== "object" || x === null)
|
|
28
|
+
return x;
|
|
29
|
+
if (seen.has(x))
|
|
30
|
+
return "[circular]";
|
|
31
|
+
seen.add(x);
|
|
32
|
+
return x;
|
|
33
|
+
};
|
|
34
|
+
return JSON.stringify(v, cycleSafe) ?? String(v);
|
|
35
|
+
};
|
|
36
|
+
// A truth IS an acceptance item, not a string to coerce. strings()' `.map(String)` turned a typed
|
|
37
|
+
// object truth into the literal "[object Object]" — schema-legal in the judge-compat string form, so
|
|
38
|
+
// it passed validateGraph, reached the worker prompt verbatim and became a judge rubric item; a YAML
|
|
39
|
+
// scalar `- 2.0` silently became "2". Nothing downstream catches either. So fail closed (house
|
|
40
|
+
// precedent: the routing block in compileOne) on anything that is not text and not a typed oracle.
|
|
41
|
+
// The CONTAINER is reported, not collapsed: an absent key returns undefined (nothing was declared) and
|
|
42
|
+
// a present-but-empty list returns [] (declared, contributed nothing) — strings() flattened both, plus
|
|
43
|
+
// a non-list, into the same silent []. Only the non-list fails closed; `truths: []` is real producer
|
|
44
|
+
// output whose plan can still be carried by its <done> lines, and compileOne already rejects a task
|
|
45
|
+
// whose acceptance ends up genuinely empty.
|
|
46
|
+
// Parser liberal, template strict: a string truth stays exactly what strings() made of it — the GSD
|
|
47
|
+
// planner template teaches prose-only and lives in another repo, so this parser must never be
|
|
48
|
+
// stricter than the thing that produced its input. A non-string must satisfy AcceptanceItemSchema
|
|
49
|
+
// EXACTLY — accepted with nothing dropped — which is where the dual-key `text:` beside
|
|
50
|
+
// `command:`/`test:` is DECLARED (schema.ts:25-30); being declared is what makes it survive
|
|
51
|
+
// loadGraph's revalidation, so nothing is ever reattached after.
|
|
52
|
+
export function parseTruths(file, raw) {
|
|
53
|
+
if (raw === undefined)
|
|
54
|
+
return undefined; // key absent: nothing was declared
|
|
55
|
+
if (!Array.isArray(raw)) {
|
|
56
|
+
throw new CompileError(`${file} has a must_haves.truths that is not a list (got ${show(raw)}).\n` +
|
|
57
|
+
` remedy: write each truth as a "- " list entry, or drop the key.`);
|
|
58
|
+
}
|
|
59
|
+
return raw.map((v, i) => {
|
|
60
|
+
if (typeof v === "string")
|
|
61
|
+
return v;
|
|
62
|
+
const parsed = AcceptanceItemSchema.safeParse(v);
|
|
63
|
+
if (!parsed.success) {
|
|
64
|
+
throw new CompileError(`${file} has a must_haves.truths[${i}] that is neither prose nor a typed acceptance oracle: ${show(v)}\n` +
|
|
65
|
+
` remedy: write it as plain text, or as {oracle: command, command: <shell>} / {oracle: test, test: <name>} /\n` +
|
|
66
|
+
` {oracle: judge, text: <rubric>} — command and test may carry a "text:" beside the oracle.`);
|
|
67
|
+
}
|
|
68
|
+
// A SUCCESSFUL parse is lossy too: z.object strips undeclared keys, so {oracle, command, text,
|
|
69
|
+
// severity} validates and compiles with `severity` gone — the same silent coercion in the other
|
|
70
|
+
// direction, and invisible because nothing downstream ever saw the key. Accepting less than the
|
|
71
|
+
// author supplied is not acceptance, so fail closed on whatever validation dropped.
|
|
72
|
+
// Object.hasOwn, never `k in`: the YAML parser hands back `constructor`, `toString`,
|
|
73
|
+
// `hasOwnProperty` and `__proto__` as OWN keys, but `in` finds all four on Object.prototype of the
|
|
74
|
+
// validated output, so each read as "kept" and was stripped in silence — the exact coercion this
|
|
75
|
+
// check exists to stop.
|
|
76
|
+
const dropped = Object.keys(v).filter((k) => !Object.hasOwn(parsed.data, k));
|
|
77
|
+
if (dropped.length) {
|
|
78
|
+
throw new CompileError(`${file} has a must_haves.truths[${i}] carrying key(s) no acceptance oracle declares: ${dropped.join(", ")}\n` +
|
|
79
|
+
` remedy: drop ${dropped.length === 1 ? "that key" : "those keys"}, or fold the intent into "text:" — a\n` +
|
|
80
|
+
` compile that kept ${dropped.length === 1 ? "it" : "them"} would discard ${dropped.length === 1 ? "it" : "them"} at the next graph load, unread.`);
|
|
81
|
+
}
|
|
82
|
+
return parsed.data;
|
|
83
|
+
});
|
|
84
|
+
}
|
|
19
85
|
const WRITE_DIRECTIVE = /\b(?:Create|Write|Add|Emit|Generate)\s+`([^`\s]+)`/gi;
|
|
20
86
|
const isPathish = (s) => /^[\w./*-]+$/.test(s) && /\.[a-z]{2,4}$/i.test(s);
|
|
21
87
|
const stripFences = (s) => s.replace(/^```[\s\S]*?^```/gm, "");
|
|
@@ -89,7 +155,7 @@ function compileOne(file, storedPath) {
|
|
|
89
155
|
.trim();
|
|
90
156
|
const title = firstSentence || key;
|
|
91
157
|
const dones = [...body.matchAll(/<done>\s*([\s\S]*?)\s*<\/done>/g)].map((m) => m[1].replace(/\s+/g, " ").trim());
|
|
92
|
-
const truths =
|
|
158
|
+
const truths = parseTruths(file, fm.must_haves?.truths) ?? [];
|
|
93
159
|
const acceptance = [...dones, ...truths].filter(Boolean);
|
|
94
160
|
if (!acceptance.length) {
|
|
95
161
|
throw new CompileError(`${file} has no acceptance criteria — every tickmarkr task needs them.\n` +
|
package/dist/compile/native.d.ts
CHANGED
|
@@ -1,4 +1,18 @@
|
|
|
1
1
|
import { type RunGraph } from "../graph/schema.js";
|
|
2
|
+
/**
|
|
3
|
+
* Classify one `context:` entry against the tree a worker's worktree is built from.
|
|
4
|
+
* ok — reachable
|
|
5
|
+
* untracked — in the author's checkout, invisible to every worker. WARNS, permanently: per-file
|
|
6
|
+
* force-add is the deliberate opt-in, so this is a standing prompt, not a migration.
|
|
7
|
+
* missing — absent from both. FAILS compile; carries a repair when exactly one tracked file
|
|
8
|
+
* shares the basename (ambiguous matches suggest nothing rather than guess).
|
|
9
|
+
* RULING 2026-08-03 (.overseer/OVERSEER-RULING-20260803-context-visibility.md): OBS-170's
|
|
10
|
+
* fail-closed-on-untracked is retired — oracle upheld, policy overruled (it blocked 11 of 13).
|
|
11
|
+
*/
|
|
12
|
+
export declare function classifyContextPath(entry: string, tracked: Set<string>, repoDir: string): {
|
|
13
|
+
kind: "ok" | "untracked" | "missing";
|
|
14
|
+
suggestion?: string;
|
|
15
|
+
};
|
|
2
16
|
export declare const COLLECTABLE_TESTS = "tests/**/*.test.ts";
|
|
3
17
|
export declare const LEGACY_PREFIX: string;
|
|
4
18
|
export declare const TICKMARKR_NATIVE_MARKER: RegExp;
|
package/dist/compile/native.js
CHANGED
|
@@ -1,7 +1,61 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
1
2
|
import { existsSync, readFileSync } from "node:fs";
|
|
3
|
+
import { basename, dirname } from "node:path";
|
|
2
4
|
import picomatch from "picomatch";
|
|
3
5
|
import { GATE_NAMES, GRAPH_ROUTING_MODES, ORACLES, SHAPES, TIERS, validateGraph } from "../graph/schema.js";
|
|
4
6
|
import { CompileError, inferShape, sha256 } from "./common.js";
|
|
7
|
+
// OBS-170/OBS-184: `context:` is a promise to the worker, and nothing ever checked it could be kept.
|
|
8
|
+
// Workers run in `git worktree add <baseRef>` (run/git.ts:169-176), which materialises the base
|
|
9
|
+
// TREE and copies nothing in — so the oracle is that tree, and NEVER two things that look like it:
|
|
10
|
+
// · existsSync reads the AUTHOR's checkout. Measured on v1.85: 10 of 16 unreachable entries are
|
|
11
|
+
// plainly present there (.overseer/ is 21 tracked files out of 480 on disk).
|
|
12
|
+
// · `git ls-files` reads the INDEX. `git add -f` writes the index, so a staged-uncommitted file
|
|
13
|
+
// reports present while still being invisible to every worker — the lint would then certify
|
|
14
|
+
// exactly the failure it exists to catch (RULING 2026-08-03 rider; the absent-vs-empty shape
|
|
15
|
+
// again, cf. fileBytes). Hence the opt-in is "add AND commit", and the resolver reads HEAD.
|
|
16
|
+
// `ls-tree -r HEAD` enumerates that tree once; per-path `cat-file -e HEAD:<path>` is the same oracle
|
|
17
|
+
// one path at a time, and one spawn beats N.
|
|
18
|
+
function trackedAtHead(dir) {
|
|
19
|
+
const git = (...args) => spawnSync("git", ["-C", dir, ...args], { encoding: "utf8", maxBuffer: 1 << 28 });
|
|
20
|
+
const top = git("rev-parse", "--show-toplevel");
|
|
21
|
+
// --full-tree is load-bearing: without it `ls-tree` scopes to the CWD's subtree and prints paths
|
|
22
|
+
// relative to it, so a spec in specs/ would see only specs/-relative names and misjudge every
|
|
23
|
+
// path outside it. Caught only by a fixture whose spec is NOT at the repo root.
|
|
24
|
+
const r = git("ls-tree", "--full-tree", "-r", "--name-only", "HEAD");
|
|
25
|
+
// not a repo, or no commits yet — fail open, so non-repo fixtures and fresh inits stay silent
|
|
26
|
+
if (top.status !== 0 || r.status !== 0 || typeof r.stdout !== "string")
|
|
27
|
+
return undefined;
|
|
28
|
+
return { root: top.stdout.trim(), tracked: new Set(r.stdout.split("\n").filter(Boolean)) };
|
|
29
|
+
}
|
|
30
|
+
const GLOB_CHARS = /[*?{[]/;
|
|
31
|
+
/**
|
|
32
|
+
* Classify one `context:` entry against the tree a worker's worktree is built from.
|
|
33
|
+
* ok — reachable
|
|
34
|
+
* untracked — in the author's checkout, invisible to every worker. WARNS, permanently: per-file
|
|
35
|
+
* force-add is the deliberate opt-in, so this is a standing prompt, not a migration.
|
|
36
|
+
* missing — absent from both. FAILS compile; carries a repair when exactly one tracked file
|
|
37
|
+
* shares the basename (ambiguous matches suggest nothing rather than guess).
|
|
38
|
+
* RULING 2026-08-03 (.overseer/OVERSEER-RULING-20260803-context-visibility.md): OBS-170's
|
|
39
|
+
* fail-closed-on-untracked is retired — oracle upheld, policy overruled (it blocked 11 of 13).
|
|
40
|
+
*/
|
|
41
|
+
export function classifyContextPath(entry, tracked, repoDir) {
|
|
42
|
+
if (GLOB_CHARS.test(entry))
|
|
43
|
+
return { kind: "ok" }; // a glob is not a promise about one file
|
|
44
|
+
const q = entry.replace(/\/+$/, "");
|
|
45
|
+
if (tracked.has(q))
|
|
46
|
+
return { kind: "ok" };
|
|
47
|
+
for (const t of tracked)
|
|
48
|
+
if (t.startsWith(`${q}/`))
|
|
49
|
+
return { kind: "ok" }; // directory with tracked children
|
|
50
|
+
if (existsSync(`${repoDir}/${q}`))
|
|
51
|
+
return { kind: "untracked" };
|
|
52
|
+
// Measured: 5 of v1.85's 6 absent entries are a tracked file written without its directory
|
|
53
|
+
// (`V185-SEEDS.md` for `.overseer/V185-SEEDS.md`) — the opt-in had worked, only the check was
|
|
54
|
+
// missing. Suggest ONLY on an unambiguous basename: two candidates is a guess, not a repair.
|
|
55
|
+
const base = basename(q);
|
|
56
|
+
const hits = [...tracked].filter((t) => basename(t) === base);
|
|
57
|
+
return hits.length === 1 ? { kind: "missing", suggestion: hits[0] } : { kind: "missing" };
|
|
58
|
+
}
|
|
5
59
|
// OBS-97: mirror of the vitest.config.ts suite include — the only path class vitest collects.
|
|
6
60
|
export const COLLECTABLE_TESTS = "tests/**/*.test.ts";
|
|
7
61
|
export const LEGACY_PREFIX = ["dro", "vr"].join("");
|
|
@@ -31,7 +85,7 @@ export function compileNative(file) {
|
|
|
31
85
|
for (const line of content.split("\n")) {
|
|
32
86
|
const heading = line.match(HEAD_RE);
|
|
33
87
|
if (heading) {
|
|
34
|
-
drafts.push({ id: heading[1], title: heading[2].trim(), fields: {}, acceptance: [], gates: [], hasGates: false, list: null,
|
|
88
|
+
drafts.push({ id: heading[1], title: heading[2].trim(), fields: {}, acceptance: [], gates: [], hasGates: false, list: null, continuationField: null });
|
|
35
89
|
continue;
|
|
36
90
|
}
|
|
37
91
|
const draft = drafts.at(-1);
|
|
@@ -59,29 +113,42 @@ export function compileNative(file) {
|
|
|
59
113
|
draft.list = name;
|
|
60
114
|
if (name === "gates")
|
|
61
115
|
draft.hasGates = true;
|
|
62
|
-
draft.
|
|
116
|
+
draft.continuationField = null;
|
|
63
117
|
}
|
|
64
118
|
else {
|
|
65
119
|
if (name === "goal" && !value)
|
|
66
120
|
invalid(draft.id, field[1], "must not be empty");
|
|
67
121
|
draft.fields[name] = value;
|
|
68
|
-
// OBS-60: carry
|
|
69
|
-
|
|
122
|
+
// OBS-60/OBS-308: carry wrapped physical lines for prose and comma-separated fields. A
|
|
123
|
+
// worker must receive the whole logical field, regardless of the author's editor width.
|
|
124
|
+
draft.continuationField = name === "goal" || name === "deps" || name === "files" || name === "context"
|
|
125
|
+
? name
|
|
126
|
+
: null;
|
|
70
127
|
draft.list = null;
|
|
71
128
|
}
|
|
72
129
|
continue;
|
|
73
130
|
}
|
|
74
|
-
// OBS-60:
|
|
75
|
-
|
|
131
|
+
// OBS-60/OBS-308: indented physical lines continue the preceding logical field until the next
|
|
132
|
+
// field/list/heading. List continuations join with a space so the existing comma parser sees the
|
|
133
|
+
// exact same value it would have received on one line.
|
|
134
|
+
if (draft.continuationField) {
|
|
135
|
+
const continuation = draft.continuationField;
|
|
76
136
|
if ((line.startsWith(" ") || line.startsWith("\t")) && !NESTED_RE.test(line)) {
|
|
77
|
-
|
|
78
|
-
|
|
137
|
+
const value = line.trim();
|
|
138
|
+
if (continuation === "goal") {
|
|
139
|
+
draft.fields.goal += `\n${value}`;
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
if (value) {
|
|
143
|
+
draft.fields[continuation] += ` ${value}`;
|
|
144
|
+
continue;
|
|
145
|
+
}
|
|
79
146
|
}
|
|
80
|
-
if (!line.trim()) {
|
|
147
|
+
if (continuation === "goal" && !line.trim()) {
|
|
81
148
|
draft.fields.goal += "\n";
|
|
82
149
|
continue;
|
|
83
150
|
}
|
|
84
|
-
draft.
|
|
151
|
+
draft.continuationField = null;
|
|
85
152
|
}
|
|
86
153
|
const nested = line.match(NESTED_RE);
|
|
87
154
|
if (nested && draft.list) {
|
|
@@ -124,12 +191,16 @@ export function compileNative(file) {
|
|
|
124
191
|
}
|
|
125
192
|
// v1.62 (OBS-97): commas inside {a,b} alternatives are part of one glob entry, not separators —
|
|
126
193
|
// split only at brace depth 0 so a brace glob reaches the lint (and the scope gate) intact.
|
|
194
|
+
// OBS-170/OBS-184: the same is true of a citation annotation. `.planning/OBSERVATIONS.md (OBS-262,
|
|
195
|
+
// OBS-263)` split at the inner comma into two bullets, and workers were dispatched a context line
|
|
196
|
+
// reading `OBS-263)`. Track paren depth alongside brace depth for exactly the OBS-97 reason.
|
|
127
197
|
const splitTop = (value) => {
|
|
128
198
|
const parts = [];
|
|
129
199
|
let depth = 0;
|
|
200
|
+
let paren = 0;
|
|
130
201
|
let current = "";
|
|
131
202
|
for (const ch of value) {
|
|
132
|
-
if (ch === "," && depth === 0) {
|
|
203
|
+
if (ch === "," && depth === 0 && paren === 0) {
|
|
133
204
|
parts.push(current);
|
|
134
205
|
current = "";
|
|
135
206
|
continue;
|
|
@@ -138,11 +209,34 @@ export function compileNative(file) {
|
|
|
138
209
|
depth++;
|
|
139
210
|
else if (ch === "}" && depth > 0)
|
|
140
211
|
depth--;
|
|
212
|
+
else if (ch === "(")
|
|
213
|
+
paren++;
|
|
214
|
+
else if (ch === ")" && paren > 0)
|
|
215
|
+
paren--;
|
|
141
216
|
current += ch;
|
|
142
217
|
}
|
|
143
218
|
return [...parts, current];
|
|
144
219
|
};
|
|
145
|
-
|
|
220
|
+
// Keeping the comma is only half of it: the merged entry still carries the annotation, so it
|
|
221
|
+
// resolves no better than the fragments did (measured: 24 → 19 unresolvable, zero rescued).
|
|
222
|
+
// Strip one trailing parenthetical so the entry is the path its author meant.
|
|
223
|
+
const stripAnnotation = (item) => {
|
|
224
|
+
const trimmed = item.trim();
|
|
225
|
+
if (!trimmed.endsWith(")"))
|
|
226
|
+
return trimmed;
|
|
227
|
+
let paren = 0;
|
|
228
|
+
for (let i = trimmed.length - 1; i >= 0; i--) {
|
|
229
|
+
if (trimmed[i] === ")")
|
|
230
|
+
paren++;
|
|
231
|
+
else if (trimmed[i] === "(") {
|
|
232
|
+
paren--;
|
|
233
|
+
if (paren === 0)
|
|
234
|
+
return trimmed.slice(0, i).trim();
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return trimmed;
|
|
238
|
+
};
|
|
239
|
+
const csv = (value) => value && value.toLowerCase() !== "none" ? splitTop(value).map((item) => stripAnnotation(item.trim())).filter(Boolean) : [];
|
|
146
240
|
// OBS-97: a typed test: oracle needs a collectable home. vitest only collects COLLECTABLE_TESTS
|
|
147
241
|
// paths, so a task whose non-empty files[] cannot host one makes scope-green and acceptance-green
|
|
148
242
|
// mutually exclusive by construction — run-20260719-210434 burned two dispatch attempts before a
|
|
@@ -281,6 +375,31 @@ export function compileNative(file) {
|
|
|
281
375
|
}
|
|
282
376
|
}
|
|
283
377
|
}
|
|
378
|
+
// OBS-170/OBS-184: warn per unreachable context: entry, classified by the action its author must
|
|
379
|
+
// take. Warn-only by operator ruling (2026-08-03) — fail-closed would have refused to compile the
|
|
380
|
+
// spec that shipped green. Fails open when git cannot answer, so non-repo fixtures stay silent.
|
|
381
|
+
const repo = trackedAtHead(dirname(file));
|
|
382
|
+
if (repo) {
|
|
383
|
+
const unreachable = [];
|
|
384
|
+
for (const t of tasks) {
|
|
385
|
+
for (const entry of t.context) {
|
|
386
|
+
const { kind, suggestion } = classifyContextPath(entry, repo.tracked, repo.root);
|
|
387
|
+
if (kind === "untracked") {
|
|
388
|
+
// Permanent, by ruling: the per-file force-add IS the opt-in, so this stays a standing
|
|
389
|
+
// prompt. "and commit" is load-bearing — staging alone leaves it out of the base tree.
|
|
390
|
+
console.warn(`tickmarkr: OBS-170: task ${t.id} context ${JSON.stringify(entry)} exists in your checkout but is NOT in a worker's worktree. To make it worker context: git add -f ${entry} && git commit. Staging alone is not enough.`);
|
|
391
|
+
}
|
|
392
|
+
else if (kind === "missing") {
|
|
393
|
+
unreachable.push(` ${t.id}: ${JSON.stringify(entry)}${suggestion ? `\n → did you mean ${suggestion} ?` : "\n → not found in the repository, and not a path."}`);
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
if (unreachable.length) {
|
|
398
|
+
throw new CompileError(`context: paths that do not exist in ${file}:\n${unreachable.join("\n")}\n\n` +
|
|
399
|
+
`A context: entry is a promise the worker can read it. These resolve to nothing in the repository,\n` +
|
|
400
|
+
`so the worker would be told to read a file that is not there. Fix the paths and recompile.`);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
284
403
|
return result;
|
|
285
404
|
}
|
|
286
405
|
// Commented native spec written to tickmarkr.spec.md by `tickmarkr init`. Documented via HTML comments (which the
|
|
@@ -319,6 +438,43 @@ acceptance is required on every task (a nested list of observable outcomes).
|
|
|
319
438
|
- test: <name> (oracle: test — named test)
|
|
320
439
|
- judge: <rubric> (oracle: judge — LLM-judged, free text)
|
|
321
440
|
- <plain text> (compat: compiles as judge oracle, warns)
|
|
441
|
+
|
|
442
|
+
HARD BOUNDS — these FAIL the compile, they do not warn:
|
|
443
|
+
- at most 6 acceptance items per task (no exception path)
|
|
444
|
+
- at most 8 files[] patterns; a {a,b} brace group counts as ONE pattern
|
|
445
|
+
- acceptance items x files[] patterns must be <= 24 ("surface")
|
|
446
|
+
- at most 60 goal words per acceptance criterion ("density")
|
|
447
|
+
Density is goal words DIVIDED BY items, so REMOVING a criterion RAISES it. Compress the goal in the
|
|
448
|
+
same edit, or a task that was comfortably inside the bound breaches it while you are tidying up.
|
|
449
|
+
|
|
450
|
+
WHAT MAKES A CRITERION REAL:
|
|
451
|
+
- "test:" must name a real test asserting on recorded state, journal lines, or drawn frames, and its
|
|
452
|
+
title must match the criterion string verbatim. It also needs a collectable test path in files[].
|
|
453
|
+
- NO criterion may be satisfiable by an absence, a rename, a source-text grep, or an empty collection.
|
|
454
|
+
"no file references X" is not a criterion — it passes in a repo where the feature was never built.
|
|
455
|
+
- "goal:" is NEVER verification. Prose in the goal enforces nothing: every obligation needs an
|
|
456
|
+
acceptance item, or a NAMED independent gate that actually applies. A gate is not independent of a
|
|
457
|
+
task that owns both the thing it checks and the fixture it checks against.
|
|
458
|
+
- A source-only obligation (a comment or doc that a change makes false) has no lawful "test:" — verify
|
|
459
|
+
it with "judge:", which reads the DIFF and must cite a changed line in a file the task owns.
|
|
460
|
+
- A criterion that pins the SHAPE of a fix must also pin the CONDITIONS under which it runs, or a
|
|
461
|
+
correctly-shaped fix that runs SOMETIMES satisfies it. "cleanup is try/finally at all four sites"
|
|
462
|
+
is satisfied by \`finally { if (cond) cleanup() }\` — the shape is present and the fix is inert in
|
|
463
|
+
production. Say UNCONDITIONAL, or name the branch that may not exist.
|
|
464
|
+
- Enumerating one axis exhaustively is what hides the others. A spec that guards PARTIAL coverage
|
|
465
|
+
site-by-site, member-by-member, can be defeated wholesale by CONDITIONAL coverage, which leaves
|
|
466
|
+
every enumeration satisfied. After you enumerate, ask what a single flag would do to the whole set.
|
|
467
|
+
|
|
468
|
+
ORDERING AND OWNERSHIP:
|
|
469
|
+
- Every path has exactly ONE owning task. Two tasks writing one file must be ORDERED by deps, or the
|
|
470
|
+
loser's work is silently dropped when the integration tip advances.
|
|
471
|
+
- A file one task CREATES cannot be "context:" for another — only deps: carries it, and that extends to
|
|
472
|
+
the task that PRODUCES a value, not just the file's existence.
|
|
473
|
+
- Deleting or renaming a symbol is a cross-task contract. Sweep for consumers by symbol AND by what the
|
|
474
|
+
mechanism DOES for them; the most dangerous consumer is a file no task owns, because nothing fixes it
|
|
475
|
+
and no edge can order it.
|
|
476
|
+
|
|
477
|
+
"timeout:" IS A KILL CEILING, NOT AN ESTIMATE. Never read a sum of timeouts as a predicted duration.
|
|
322
478
|
-->
|
|
323
479
|
|
|
324
480
|
## T1: Scaffold the feature
|
package/dist/config/config.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ 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 MODEL_WINDOW_PLAUSIBILITY_FLOOR = 1024;
|
|
10
11
|
export declare const ROUTING_MODES: readonly ["partner-led", "risk-based", "staff-led"];
|
|
11
12
|
declare const ModeEnum: z.ZodEnum<{
|
|
12
13
|
"partner-led": "partner-led";
|
|
@@ -26,7 +27,7 @@ export declare const MapEntrySchema: z.ZodObject<{
|
|
|
26
27
|
}, z.core.$strip>;
|
|
27
28
|
export type MapEntry = z.infer<typeof MapEntrySchema>;
|
|
28
29
|
export declare const TierEntrySchema: z.ZodObject<{
|
|
29
|
-
vendor: z.ZodString
|
|
30
|
+
vendor: z.ZodNullable<z.ZodString>;
|
|
30
31
|
channel: z.ZodEnum<{
|
|
31
32
|
sub: "sub";
|
|
32
33
|
api: "api";
|
|
@@ -36,6 +37,13 @@ export declare const TierEntrySchema: z.ZodObject<{
|
|
|
36
37
|
mid: "mid";
|
|
37
38
|
frontier: "frontier";
|
|
38
39
|
}>>;
|
|
40
|
+
modelOverrides: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
41
|
+
vendor: z.ZodOptional<z.ZodString>;
|
|
42
|
+
channel: z.ZodOptional<z.ZodEnum<{
|
|
43
|
+
sub: "sub";
|
|
44
|
+
api: "api";
|
|
45
|
+
}>>;
|
|
46
|
+
}, z.core.$strip>>>;
|
|
39
47
|
windows: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodNumber>>;
|
|
40
48
|
}, z.core.$strip>;
|
|
41
49
|
export type TierEntry = z.infer<typeof TierEntrySchema>;
|
|
@@ -173,7 +181,7 @@ export declare const TickmarkrConfigSchema: z.ZodObject<{
|
|
|
173
181
|
}, z.core.$strip>>;
|
|
174
182
|
}, z.core.$strip>;
|
|
175
183
|
tiers: z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
176
|
-
vendor: z.ZodString
|
|
184
|
+
vendor: z.ZodNullable<z.ZodString>;
|
|
177
185
|
channel: z.ZodEnum<{
|
|
178
186
|
sub: "sub";
|
|
179
187
|
api: "api";
|
|
@@ -183,6 +191,13 @@ export declare const TickmarkrConfigSchema: z.ZodObject<{
|
|
|
183
191
|
mid: "mid";
|
|
184
192
|
frontier: "frontier";
|
|
185
193
|
}>>;
|
|
194
|
+
modelOverrides: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
|
|
195
|
+
vendor: z.ZodOptional<z.ZodString>;
|
|
196
|
+
channel: z.ZodOptional<z.ZodEnum<{
|
|
197
|
+
sub: "sub";
|
|
198
|
+
api: "api";
|
|
199
|
+
}>>;
|
|
200
|
+
}, z.core.$strip>>>;
|
|
186
201
|
windows: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodNumber>>;
|
|
187
202
|
}, z.core.$strip>>;
|
|
188
203
|
pricing: z.ZodRecord<z.ZodString, z.ZodNumber>;
|
|
@@ -301,8 +316,8 @@ export type InitConfigOverlay = {
|
|
|
301
316
|
};
|
|
302
317
|
};
|
|
303
318
|
export declare function configTemplate(overlay?: InitConfigOverlay): string;
|
|
304
|
-
export { FLEET_OVERLAY_KEYS, fleetEditableEquals, fleetRepoOverlayFromDelta,
|
|
305
|
-
export type {
|
|
319
|
+
export { FLEET_OVERLAY_KEYS, fleetEditableEquals, fleetRepoOverlayFromDelta, renderFleetOverlayWrite, repoOverlayYaml, serializeFleetOverlay, unifiedYamlDiff, } from "./fleet-overlay.js";
|
|
320
|
+
export type { FleetOverlayWrite } from "./fleet-overlay.js";
|
|
306
321
|
export type FleetTierAssignment = {
|
|
307
322
|
tier: Tier;
|
|
308
323
|
provenance?: string;
|
|
@@ -316,7 +331,7 @@ export type FleetEditable = {
|
|
|
316
331
|
};
|
|
317
332
|
export declare function repoOverlayPath(repoRoot: string): string;
|
|
318
333
|
export declare function readOverlayFile(path: string): Record<string, unknown>;
|
|
319
|
-
export declare function fleetEditableFromConfig(cfg: TickmarkrConfig
|
|
334
|
+
export declare function fleetEditableFromConfig(cfg: TickmarkrConfig): FleetEditable;
|
|
320
335
|
/** Resolve which layer last set a dotted fleet path (defaults < global < repo). */
|
|
321
336
|
export declare function fleetKeyLayer(repoRoot: string, dotted: string, opts?: {
|
|
322
337
|
globalDir?: string;
|