tickmarkr 1.45.0 → 1.47.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,13 +1,21 @@
1
- import { writeFileSync } from "node:fs";
1
+ import { readFileSync, writeFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { loadConfig } from "../../config/config.js";
4
4
  import { tickmarkrDir, stateDirName } from "../../graph/graph.js";
5
- import { learnedScore, MIN_SAMPLES, cellsOf } from "../../route/profile.js";
6
- import { Journal, loadRoutingProfile, readProfileCursor, RUNS_WINDOW } from "../../run/journal.js";
7
- // tickmarkr profile — inspect (show) and forget (reset) the derived learned-routing profile (VIS-03).
8
- // Read-only class like plan/status/report: no run lock. `reset` writes ONE cursor scalar, nothing else.
5
+ import { cellOf, cellSummary, explorationBonus, EXPLORE_CAP, learnedScore, learnedScoreTerms, MIN_SAMPLES, PRIOR_K, REF_MS, cellsOf, } from "../../route/profile.js";
6
+ import { Journal, loadRoutingProfile, parseRunId, readProfileCursor, readProfileDiscounts, appendProfileDiscount, profileDiscountsPath, RUNS_WINDOW, } from "../../run/journal.js";
7
+ // tickmarkr profile — inspect (show), forget (reset), and discount poisoned evidence (v1.46 T5).
8
+ // Read-only class like plan/status/report except discount append. `reset` writes ONE cursor scalar.
9
9
  export async function profile(argv, cwd = process.cwd()) {
10
- return argv[0] === "reset" ? reset(cwd) : show(cwd);
10
+ if (argv[0] === "reset")
11
+ return reset(cwd);
12
+ if (argv[0] === "discount")
13
+ return discount(argv.slice(1), cwd);
14
+ if (argv[0] === "discounts")
15
+ return listDiscounts(cwd);
16
+ if (argv[0] === "--explain" || argv.includes("--explain"))
17
+ return explain(argv, cwd);
18
+ return show(cwd);
11
19
  }
12
20
  // Non-destructive reset (T-13-06): a one-line runId cutoff at .tickmarkr/profile-since. NEVER deletes,
13
21
  // truncates, or rotates any telemetry — the profile is derived (PROF-01), so there is nothing to delete;
@@ -26,6 +34,119 @@ function reset(cwd) {
26
34
  ` to un-reset: delete ${stateDir}/profile-since.`,
27
35
  ].join("\n");
28
36
  }
37
+ function parseDiscountArgs(argv) {
38
+ let runId;
39
+ let taskId;
40
+ let weight;
41
+ let reason;
42
+ for (let i = 0; i < argv.length; i++) {
43
+ if (argv[i] === "--weight" && argv[i + 1]) {
44
+ const w = argv[++i];
45
+ if (w !== "0" && w !== "0.5")
46
+ throw new Error(`profile discount --weight must be 0 or 0.5 (got ${w})`);
47
+ weight = w === "0" ? 0 : 0.5;
48
+ }
49
+ else if (argv[i] === "--reason" && argv[i + 1]) {
50
+ reason = argv[++i];
51
+ }
52
+ else if (!argv[i].startsWith("--") && runId === undefined) {
53
+ runId = parseRunId(argv[i]);
54
+ }
55
+ else if (!argv[i].startsWith("--") && taskId === undefined) {
56
+ taskId = argv[i];
57
+ }
58
+ }
59
+ if (!runId)
60
+ throw new Error("profile discount requires <runId>");
61
+ if (weight === undefined)
62
+ throw new Error("profile discount requires --weight 0|0.5");
63
+ if (!reason?.trim())
64
+ throw new Error("profile discount requires --reason (a discount is an evidence claim)");
65
+ return { runId, taskId, weight, reason: reason.trim() };
66
+ }
67
+ function discount(argv, cwd) {
68
+ const mark = parseDiscountArgs(argv);
69
+ appendProfileDiscount(cwd, mark);
70
+ const stateDir = stateDirName(cwd);
71
+ const scope = mark.taskId ? `${mark.runId} task ${mark.taskId}` : mark.runId;
72
+ return [
73
+ `profile discount — marked ${scope} at weight ${mark.weight}.`,
74
+ ` reason: ${mark.reason}`,
75
+ ` wrote ${stateDir}/profile-discounts (append-only).`,
76
+ ` list: tickmarkr profile discounts`,
77
+ ].join("\n");
78
+ }
79
+ function listDiscounts(cwd) {
80
+ const marks = readProfileDiscounts(cwd);
81
+ const path = profileDiscountsPath(cwd);
82
+ if (!marks.length) {
83
+ return existsDiscountsFile(cwd)
84
+ ? `profile discounts — ${path}\n (no valid marks)`
85
+ : `profile discounts — no ${stateDirName(cwd)}/profile-discounts file yet.`;
86
+ }
87
+ const lines = marks.map((m) => {
88
+ const scope = m.taskId ? `${m.runId} ${m.taskId}` : m.runId;
89
+ return ` ${scope.padEnd(28)} weight=${m.weight} # ${m.reason}`;
90
+ });
91
+ return [`profile discounts — ${path}`, ...lines].join("\n");
92
+ }
93
+ function existsDiscountsFile(cwd) {
94
+ try {
95
+ readFileSync(profileDiscountsPath(cwd));
96
+ return true;
97
+ }
98
+ catch {
99
+ return false;
100
+ }
101
+ }
102
+ const fmtSigned = (n) => (n >= 0 ? `+${n.toFixed(3)}` : n.toFixed(3));
103
+ function explain(argv, cwd) {
104
+ const rest = argv[0] === "--explain" ? argv.slice(1) : argv.filter((a) => a !== "--explain");
105
+ const shape = rest[0];
106
+ const chKey = rest[1];
107
+ const channel = rest[2] ?? "sub";
108
+ if (!shape || !chKey)
109
+ throw new Error("profile --explain requires <shape> <channel>");
110
+ const cfg = loadConfig(cwd);
111
+ const p = loadRoutingProfile(cwd, cfg, { preview: true });
112
+ const tuning = { availWeight: cfg.routing.learnedTuning?.availWeight };
113
+ const cell = cellOf(p, shape, chKey, channel);
114
+ const terms = learnedScoreTerms(p, shape, chKey, channel, tuning);
115
+ const score = learnedScore(p, shape, chKey, channel, tuning);
116
+ const header = `${shape} × ${chKey} (${channel})`;
117
+ if (!cell) {
118
+ return [
119
+ header,
120
+ ` quality ${fmtSigned(terms.quality)} (no cell — neutral)`,
121
+ ` perf ${fmtSigned(terms.perf)}`,
122
+ ` avail ${fmtSigned(terms.avail)}`,
123
+ ` overrun ${fmtSigned(terms.overrun)}`,
124
+ ` score ${fmtSigned(score)}`,
125
+ ].join("\n");
126
+ }
127
+ const s = cellSummary(cell);
128
+ const disc = s.discounted > 0 ? `, ${s.discounted} rows discounted` : "";
129
+ const qualityDetail = s.cold
130
+ ? `n_eff ${s.nEff} < ${MIN_SAMPLES} (cold)`
131
+ : `q̂=(qSum ${cell.qSum} + ${PRIOR_K})/(n ${cell.n} + ${2 * PRIOR_K}) − 0.5 n_raw ${s.nRaw}${disc}`;
132
+ const perfDetail = cell.doneMedianMs === undefined || s.cold
133
+ ? `cold`
134
+ : `median ${Math.round(cell.doneMedianMs / 60_000)}m vs ref ${REF_MS / 60_000}m done=${cell.doneCount}`;
135
+ const availDetail = `quotaHits ${cell.quotaHits} / dispatches ${cell.dispatches}`;
136
+ const overrunDetail = `overruns ${cell.overruns ?? 0} / dispatches ${cell.dispatches}`;
137
+ const explore = cell.dispatches >= EXPLORE_CAP
138
+ ? `spent dispatches ${cell.dispatches} ≥ cap ${EXPLORE_CAP}`
139
+ : `remaining ${s.exploreRemaining} bonus ${explorationBonus(cell).toFixed(3)}`;
140
+ return [
141
+ header,
142
+ ` quality ${fmtSigned(terms.quality)} ${qualityDetail}`,
143
+ ` perf ${fmtSigned(terms.perf)} ${perfDetail}`,
144
+ ` avail ${fmtSigned(terms.avail)} ${availDetail}`,
145
+ ` overrun ${fmtSigned(terms.overrun)} ${overrunDetail}`,
146
+ ` score ${fmtSigned(score)} (deciding only below prefer/cost/tier — ROUTE-08)`,
147
+ ` explore ${explore}`,
148
+ ].join("\n");
149
+ }
29
150
  // Inspection surface: preview:true bypasses the routing.learned:off short-circuit so `show` renders the
30
151
  // profile even under the default off — same trust ramp as `tickmarkr plan`. The routing path stays inert.
31
152
  function show(cwd) {
@@ -51,7 +172,8 @@ function show(cwd) {
51
172
  // VIS-04/ROUTE-15 — preview must match routing's tuning, never the bare module default.
52
173
  const score = learnedScore(p, shape, chKey, channel, { availWeight: cfg.routing.learnedTuning?.availWeight }).toFixed(3);
53
174
  const cold = cell.n < MIN_SAMPLES ? ` cold (n<${MIN_SAMPLES})` : "";
54
- return ` ${shape.padEnd(10)} ${chKey.padEnd(28)} ${channel.padEnd(4)} n=${String(cell.n).padEnd(3)} q=${quality.padEnd(5)} ${median.padEnd(9)} disp=${String(cell.dispatches).padEnd(3)} score=${score}${cold}`;
175
+ const disc = (cell.discounted ?? 0) > 0 ? ` disc=${cell.discounted}` : "";
176
+ return ` ${shape.padEnd(10)} ${chKey.padEnd(28)} ${channel.padEnd(4)} n=${String(cell.n).padEnd(3)} q=${quality.padEnd(5)} ${median.padEnd(9)} disp=${String(cell.dispatches).padEnd(3)} score=${score}${disc}${cold}`;
55
177
  });
56
178
  return [...header, "", " shape channel class obs quality median dispatch score", ...rows].join("\n");
57
179
  }
@@ -4,7 +4,7 @@ import { loadConfig } from "../../config/config.js";
4
4
  import { pickDriver } from "../../drivers/index.js";
5
5
  import { loadGraph } from "../../graph/graph.js";
6
6
  import { formatSummary, runDaemon } from "../../run/daemon.js";
7
- import { route } from "../../route/router.js";
7
+ import { route, QUALITY_ENV, NO_EXPLORE_ENV } from "../../route/router.js";
8
8
  import { formatJournalNarration, loadRoutingProfile } from "../../run/journal.js";
9
9
  const summaryGreen = (s) => s.failed.length === 0 && s.human.length === 0 && s.blocked.length === 0 && s.pending.length === 0
10
10
  && s.tipVerify !== "failed";
@@ -15,6 +15,8 @@ export async function run(argv, cwd = process.cwd()) {
15
15
  concurrency: { type: "string" },
16
16
  driver: { type: "string" },
17
17
  "route-strict": { type: "boolean" },
18
+ "no-explore": { type: "boolean" },
19
+ quality: { type: "boolean" },
18
20
  },
19
21
  });
20
22
  if (values.concurrency !== undefined) {
@@ -23,21 +25,36 @@ export async function run(argv, cwd = process.cwd()) {
23
25
  throw new Error(`--concurrency must be a positive integer (got ${values.concurrency})`);
24
26
  }
25
27
  const cfg = loadConfig(cwd);
26
- if (values["route-strict"]) {
27
- const adapters = allAdapters();
28
- const health = readDoctor(cwd) ?? (await probeAll(adapters));
29
- const channels = discoverChannels(cfg, adapters, health);
30
- // no preview: the strict pre-flight routes through exactly what the daemon will use (honors the switch)
31
- const profile = loadRoutingProfile(cwd, cfg);
32
- const lints = loadGraph(cwd).tasks.flatMap((t) => route(t, cfg, channels, profile).lints);
33
- if (lints.length)
34
- throw new Error(`--route-strict: routing lints present, refusing to dispatch:\n${lints.join("\n")}`);
28
+ const quality = !!values.quality;
29
+ const noExplore = !!values["no-explore"] || quality;
30
+ const exploreCtx = noExplore || quality ? { noExplore, quality } : undefined;
31
+ if (noExplore)
32
+ process.env[NO_EXPLORE_ENV] = "1";
33
+ if (quality)
34
+ process.env[QUALITY_ENV] = "1";
35
+ try {
36
+ if (values["route-strict"]) {
37
+ const adapters = allAdapters();
38
+ const health = readDoctor(cwd) ?? (await probeAll(adapters));
39
+ const channels = discoverChannels(cfg, adapters, health);
40
+ // no preview: the strict pre-flight routes through exactly what the daemon will use (honors the switch)
41
+ const profile = loadRoutingProfile(cwd, cfg);
42
+ const lints = loadGraph(cwd).tasks.flatMap((t) => route(t, cfg, channels, profile, undefined, undefined, exploreCtx).lints);
43
+ if (lints.length)
44
+ throw new Error(`--route-strict: routing lints present, refusing to dispatch:\n${lints.join("\n")}`);
45
+ }
46
+ const s = await runDaemon(cwd, {
47
+ concurrency: values.concurrency ? Number(values.concurrency) : undefined,
48
+ driver: pickDriver(cfg, values.driver),
49
+ narrate: (event) => console.log(formatJournalNarration(event)),
50
+ });
51
+ const out = `run ${s.runId} finished — ${formatSummary(s)} (merge to main is a human decision)`;
52
+ return { out, code: summaryGreen(s) ? 0 : 2 };
53
+ }
54
+ finally {
55
+ if (noExplore)
56
+ delete process.env[NO_EXPLORE_ENV];
57
+ if (quality)
58
+ delete process.env[QUALITY_ENV];
35
59
  }
36
- const s = await runDaemon(cwd, {
37
- concurrency: values.concurrency ? Number(values.concurrency) : undefined,
38
- driver: pickDriver(cfg, values.driver),
39
- narrate: (event) => console.log(formatJournalNarration(event)),
40
- });
41
- const out = `run ${s.runId} finished — ${formatSummary(s)} (merge to main is a human decision)`;
42
- return { out, code: summaryGreen(s) ? 0 : 2 };
43
60
  }
@@ -5,7 +5,7 @@ export type CommandResult = string | {
5
5
  };
6
6
  export type CommandMap = Record<string, (argv: string[]) => Promise<CommandResult>>;
7
7
  export declare const COMMANDS: CommandMap;
8
- export declare const USAGE = "tickmarkr \u2014 spec-driven orchestration harness for AI coding agents\nusage: tickmarkr <command>\n init guided setup + doctor; init --agent [--force] [--docs] adds agent skills/docs\n doctor re-probe adapters, herdr, auth; print capability matrix\n compile <src> spec \u2192 .tickmarkr/graph.json (fails without acceptance criteria)\n scope <intent> draft a compiled native spec beside an answered intent (--force to overwrite)\n plan dry-run routing table + cost estimate + floor lints\n run execute the graph (--concurrency N --driver herdr|subprocess --route-strict)\n status live run state\n resume <id> continue a run from its journal\n report <id> cost/quality report (--md for committable execution record)\n profile show learned routing profile (profile reset = forget history via cursor, keeps telemetry)\n unlock remove a stale/garbage run lock (refuses if the holder is alive)\n approve <id> <task> approve a parked human gate (--by <name> --reason <text>); takes effect on resume";
8
+ export declare const USAGE = "tickmarkr \u2014 spec-driven orchestration harness for AI coding agents\nusage: tickmarkr <command>\n init guided setup + doctor; init --agent [--force] [--docs] adds agent skills/docs\n doctor re-probe adapters, herdr, auth; print capability matrix\n fleet interactive fleet editor (fleet --print for CI drift checks)\n compile <src> spec \u2192 .tickmarkr/graph.json (fails without acceptance criteria)\n scope <intent> draft a compiled native spec beside an answered intent (--force to overwrite)\n plan dry-run routing table + cost estimate + floor lints\n run execute the graph (--concurrency N --driver herdr|subprocess --route-strict)\n status live run state\n resume <id> continue a run from its journal\n report <id> cost/quality report (--md for committable execution record)\n profile show learned routing profile (profile reset = forget history via cursor, keeps telemetry)\n unlock remove a stale/garbage run lock (refuses if the holder is alive)\n approve <id> <task> approve a parked human gate (--by <name> --reason <text>); takes effect on resume";
9
9
  export declare function dispatch(cmd: string | undefined, argv: string[], commands?: CommandMap): Promise<{
10
10
  out: string;
11
11
  code: number;
package/dist/cli/index.js CHANGED
@@ -4,6 +4,7 @@ import { pathToFileURL } from "node:url";
4
4
  import { approve } from "./commands/approve.js";
5
5
  import { compile } from "./commands/compile.js";
6
6
  import { doctor } from "./commands/doctor.js";
7
+ import { fleet } from "./commands/fleet.js";
7
8
  import { init } from "./commands/init.js";
8
9
  import { plan } from "./commands/plan.js";
9
10
  import { profile } from "./commands/profile.js";
@@ -16,7 +17,7 @@ import { unlock } from "./commands/unlock.js";
16
17
  import { version } from "./commands/version.js";
17
18
  const normalize = (r) => typeof r === "string" ? { out: r, code: 0 } : r;
18
19
  export const COMMANDS = {
19
- init, doctor, compile, scope, plan, run, status, resume, report, profile, unlock, approve, version,
20
+ init, doctor, fleet, compile, scope, plan, run, status, resume, report, profile, unlock, approve, version,
20
21
  };
21
22
  const VERSION_FLAGS = new Set(["version", "--version", "-v"]);
22
23
  const HELP_CMDS = new Set(["help", "-h", "--help"]);
@@ -25,6 +26,7 @@ export const USAGE = `tickmarkr — spec-driven orchestration harness for AI cod
25
26
  usage: tickmarkr <command>
26
27
  init guided setup + doctor; init --agent [--force] [--docs] adds agent skills/docs
27
28
  doctor re-probe adapters, herdr, auth; print capability matrix
29
+ fleet interactive fleet editor (fleet --print for CI drift checks)
28
30
  compile <src> spec → .tickmarkr/graph.json (fails without acceptance criteria)
29
31
  scope <intent> draft a compiled native spec beside an answered intent (--force to overwrite)
30
32
  plan dry-run routing table + cost estimate + floor lints
@@ -1,5 +1,6 @@
1
1
  import { readdirSync, readFileSync, statSync } from "node:fs";
2
2
  import { extname, join, relative } from "node:path";
3
+ import picomatch from "picomatch";
3
4
  // Advisory plan-time scan only (OBS-12/13/14/21). NEVER expands files[], fails compile, or
4
5
  // feeds the scope gate — a warning the author acts on. Plain-text (no AST), capped + sorted.
5
6
  /** Max test files walked under tests/ (sorted walk; rest ignored). */
@@ -99,7 +100,8 @@ export function collateralLints(tasks, repoRoot) {
99
100
  };
100
101
  const lines = [];
101
102
  for (const t of tasks) {
102
- const scoped = new Set(t.files.map((f) => f.replace(/^\.\//, "")));
103
+ // OBS-22: scopeGate accepts picomatch globs; advisory collateral warnings must agree.
104
+ const scoped = picomatch(t.files.map((f) => f.replace(/^\.\//, "")), { dot: true });
103
105
  const srcFiles = t.files.map((f) => f.replace(/^\.\//, "")).filter(isSrcPath);
104
106
  if (!srcFiles.length)
105
107
  continue;
@@ -107,7 +109,7 @@ export function collateralLints(tasks, repoRoot) {
107
109
  const needles = [...new Set(srcFiles.flatMap(needlesFor))];
108
110
  const hits = [];
109
111
  for (const tf of testFiles) {
110
- if (scoped.has(tf))
112
+ if (scoped(tf))
111
113
  continue;
112
114
  const text = read(tf);
113
115
  if (text === null)
@@ -23,7 +23,7 @@ export function compileNative(file) {
23
23
  for (const line of content.split("\n")) {
24
24
  const heading = line.match(HEAD_RE);
25
25
  if (heading) {
26
- drafts.push({ id: heading[1], title: heading[2].trim(), fields: {}, acceptance: [], gates: [], hasGates: false, list: null });
26
+ drafts.push({ id: heading[1], title: heading[2].trim(), fields: {}, acceptance: [], gates: [], hasGates: false, list: null, goalContinuation: false });
27
27
  continue;
28
28
  }
29
29
  const draft = drafts.at(-1);
@@ -41,15 +41,30 @@ export function compileNative(file) {
41
41
  draft.list = name;
42
42
  if (name === "gates")
43
43
  draft.hasGates = true;
44
+ draft.goalContinuation = false;
44
45
  }
45
46
  else {
46
47
  if (name === "goal" && !value)
47
48
  invalid(draft.id, field[1], "must not be empty");
48
49
  draft.fields[name] = value;
50
+ // OBS-60: carry every indented continuation line after `- goal:` into the compiled goal.
51
+ draft.goalContinuation = name === "goal";
49
52
  draft.list = null;
50
53
  }
51
54
  continue;
52
55
  }
56
+ // OBS-60: multiline goals — indented prose after `- goal:` is appended until the next field.
57
+ if (draft.goalContinuation) {
58
+ if ((line.startsWith(" ") || line.startsWith("\t")) && !NESTED_RE.test(line)) {
59
+ draft.fields.goal += `\n${line.trim()}`;
60
+ continue;
61
+ }
62
+ if (!line.trim()) {
63
+ draft.fields.goal += "\n";
64
+ continue;
65
+ }
66
+ draft.goalContinuation = false;
67
+ }
53
68
  const nested = line.match(NESTED_RE);
54
69
  if (nested && draft.list) {
55
70
  const value = nested[1].trim();
@@ -32,6 +32,7 @@ export declare const TierEntrySchema: z.ZodObject<{
32
32
  mid: "mid";
33
33
  frontier: "frontier";
34
34
  }>>;
35
+ windows: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodNumber>>;
35
36
  }, z.core.$strip>;
36
37
  export type TierEntry = z.infer<typeof TierEntrySchema>;
37
38
  export declare const ModelPricingSchema: z.ZodObject<{
@@ -85,6 +86,16 @@ export declare const TickmarkrConfigSchema: z.ZodObject<{
85
86
  halfLifeRuns: z.ZodOptional<z.ZodNumber>;
86
87
  availWeight: z.ZodOptional<z.ZodNumber>;
87
88
  }, z.core.$strip>>;
89
+ explore: z.ZodOptional<z.ZodObject<{
90
+ mode: z.ZodOptional<z.ZodEnum<{
91
+ on: "on";
92
+ off: "off";
93
+ }>>;
94
+ excludeShapes: z.ZodOptional<z.ZodArray<z.ZodString>>;
95
+ excludeComplexityAtOrAbove: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
96
+ cap: z.ZodOptional<z.ZodNumber>;
97
+ }, z.core.$strip>>;
98
+ sla: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodNumber>>;
88
99
  allowUnverifiedModels: z.ZodBoolean;
89
100
  allow: z.ZodOptional<z.ZodObject<{
90
101
  adapters: z.ZodOptional<z.ZodArray<z.ZodString>>;
@@ -106,6 +117,7 @@ export declare const TickmarkrConfigSchema: z.ZodObject<{
106
117
  mid: "mid";
107
118
  frontier: "frontier";
108
119
  }>>;
120
+ windows: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodNumber>>;
109
121
  }, z.core.$strip>>;
110
122
  pricing: z.ZodRecord<z.ZodString, z.ZodNumber>;
111
123
  cost: z.ZodOptional<z.ZodObject<{
@@ -194,4 +206,34 @@ export type InitConfigOverlay = {
194
206
  };
195
207
  };
196
208
  export declare function configTemplate(overlay?: InitConfigOverlay): string;
209
+ /** Fleet-owned overlay keys — the only config surface `tickmarkr fleet` may write. */
210
+ export declare const FLEET_OVERLAY_KEYS: readonly ["routing", "tiers"];
211
+ export type FleetTierAssignment = {
212
+ tier: Tier;
213
+ provenance?: string;
214
+ };
215
+ export type FleetEditable = {
216
+ denyAdapters: string[];
217
+ denyModels: string[];
218
+ tiers: Record<string, Record<string, FleetTierAssignment | null>>;
219
+ map: Record<string, MapEntry>;
220
+ floors: Record<string, Tier>;
221
+ };
222
+ export declare function repoOverlayPath(repoRoot: string): string;
223
+ export declare function readOverlayFile(path: string): Record<string, unknown>;
224
+ export declare function fleetEditableFromConfig(cfg: TickmarkrConfig): FleetEditable;
225
+ /** Resolve which layer last set a dotted fleet path (defaults < global < repo). */
226
+ export declare function fleetKeyLayer(repoRoot: string, dotted: string, opts?: {
227
+ globalDir?: string;
228
+ }): "defaults" | "global" | "repo";
229
+ /** Non-interactive fleet state for CI drift checks (`tickmarkr fleet --print`). */
230
+ export declare function formatFleetPrint(repoRoot: string, opts?: {
231
+ globalDir?: string;
232
+ }): string;
233
+ /** Build the repo overlay fragment fleet would write for edits since session start. */
234
+ export declare function fleetRepoOverlayFromDelta(initial: FleetEditable, edited: FleetEditable, existingRepo?: Record<string, unknown>): Record<string, unknown>;
235
+ export declare function repoOverlayYaml(overlay: Record<string, unknown>, provenance?: Record<string, Record<string, string>>): string;
236
+ export declare function serializeFleetOverlay(overlay: Record<string, unknown>, provenance?: Record<string, Record<string, string>>): string;
237
+ export declare function unifiedYamlDiff(before: string, after: string, label?: string): string;
238
+ export declare function fleetEditableEquals(a: FleetEditable, b: FleetEditable): boolean;
197
239
  export {};