tickmarkr 1.84.0 → 1.86.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.
Files changed (87) hide show
  1. package/README.md +4 -2
  2. package/dist/adapters/catalog-remote.d.ts +64 -0
  3. package/dist/adapters/catalog-remote.js +287 -0
  4. package/dist/adapters/catalog.d.ts +96 -0
  5. package/dist/adapters/catalog.js +176 -0
  6. package/dist/adapters/claude-code.d.ts +1 -0
  7. package/dist/adapters/claude-code.js +59 -1
  8. package/dist/adapters/fake.js +42 -4
  9. package/dist/adapters/model-lints.d.ts +25 -5
  10. package/dist/adapters/model-lints.js +184 -50
  11. package/dist/adapters/model-windows.d.ts +31 -0
  12. package/dist/adapters/model-windows.js +69 -0
  13. package/dist/adapters/prompt.d.ts +5 -1
  14. package/dist/adapters/prompt.js +13 -4
  15. package/dist/adapters/registry.d.ts +25 -26
  16. package/dist/adapters/registry.js +173 -110
  17. package/dist/adapters/types.d.ts +3 -0
  18. package/dist/adapters/types.js +36 -3
  19. package/dist/brand.d.ts +5 -1
  20. package/dist/brand.js +18 -2
  21. package/dist/cli/commands/doctor.d.ts +3 -0
  22. package/dist/cli/commands/doctor.js +43 -21
  23. package/dist/cli/commands/fleet.d.ts +7 -0
  24. package/dist/cli/commands/fleet.js +94 -74
  25. package/dist/cli/commands/init.js +118 -5
  26. package/dist/cli/commands/status.js +202 -46
  27. package/dist/compile/collateral.d.ts +86 -2
  28. package/dist/compile/collateral.js +294 -3
  29. package/dist/compile/gsd.d.ts +2 -1
  30. package/dist/compile/gsd.js +68 -2
  31. package/dist/compile/native.d.ts +14 -0
  32. package/dist/compile/native.js +161 -12
  33. package/dist/config/config.d.ts +82 -5
  34. package/dist/config/config.js +253 -66
  35. package/dist/config/fleet-overlay.d.ts +25 -20
  36. package/dist/config/fleet-overlay.js +195 -77
  37. package/dist/config/fleet-why.d.ts +23 -0
  38. package/dist/config/fleet-why.js +42 -0
  39. package/dist/drivers/herdr.d.ts +21 -3
  40. package/dist/drivers/herdr.js +344 -110
  41. package/dist/gates/acceptance.js +7 -2
  42. package/dist/gates/baseline.d.ts +1 -0
  43. package/dist/gates/baseline.js +91 -13
  44. package/dist/gates/llm.d.ts +0 -1
  45. package/dist/gates/llm.js +5 -30
  46. package/dist/gates/review.d.ts +9 -1
  47. package/dist/gates/review.js +105 -10
  48. package/dist/gates/run-gates.d.ts +9 -0
  49. package/dist/gates/run-gates.js +285 -41
  50. package/dist/gates/verdict-cause.d.ts +4 -0
  51. package/dist/gates/verdict-cause.js +63 -0
  52. package/dist/graph/schema.d.ts +6 -0
  53. package/dist/graph/schema.js +8 -5
  54. package/dist/route/router.d.ts +0 -5
  55. package/dist/route/router.js +16 -20
  56. package/dist/run/consult.d.ts +6 -0
  57. package/dist/run/consult.js +35 -25
  58. package/dist/run/daemon.d.ts +48 -2
  59. package/dist/run/daemon.js +1488 -330
  60. package/dist/run/journal.d.ts +56 -3
  61. package/dist/run/journal.js +358 -4
  62. package/dist/run/stall.d.ts +35 -1
  63. package/dist/run/stall.js +118 -8
  64. package/dist/tui/cockpit/capture.d.ts +12 -0
  65. package/dist/tui/cockpit/capture.js +37 -1
  66. package/dist/tui/cockpit/components.d.ts +2 -0
  67. package/dist/tui/cockpit/components.js +8 -8
  68. package/dist/tui/cockpit/derive.d.ts +29 -2
  69. package/dist/tui/cockpit/derive.js +219 -23
  70. package/dist/tui/cockpit/run-cockpit.js +128 -27
  71. package/dist/tui/cockpit/theme.d.ts +32 -26
  72. package/dist/tui/cockpit/theme.js +11 -5
  73. package/dist/tui/ink/components.d.ts +0 -15
  74. package/dist/tui/ink/components.js +0 -17
  75. package/dist/tui/ink/fleet-app.d.ts +4 -1
  76. package/dist/tui/ink/fleet-app.js +134 -13
  77. package/fixtures/sample.native.md +1 -1
  78. package/package.json +1 -1
  79. package/skills/tickmarkr-overseer/SKILL.md +354 -34
  80. package/skills/tickmarkr-overseer/scripts/watch-artifacts.sh +70 -0
  81. package/skills/tickmarkr-overseer/scripts/watch-panes.sh +1 -1
  82. package/dist/tui/ink/studio-app.d.ts +0 -59
  83. package/dist/tui/ink/studio-app.js +0 -320
  84. package/dist/tui/save.d.ts +0 -38
  85. package/dist/tui/save.js +0 -96
  86. package/dist/tui/staging.d.ts +0 -29
  87. package/dist/tui/staging.js +0 -78
@@ -1,8 +1,10 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
2
  import { homedir } from "node:os";
3
3
  import { join } from "node:path";
4
- import { parse, stringify } from "yaml";
4
+ import picomatch from "picomatch";
5
+ import { Document, parse } from "yaml";
5
6
  import { z } from "zod";
7
+ import { CITED_MODEL_WINDOWS } from "../adapters/model-windows.js";
6
8
  import { stateDirName } from "../graph/graph.js";
7
9
  import { SHAPES, TIERS } from "../graph/schema.js";
8
10
  const TierEnum = z.enum(TIERS, {
@@ -10,6 +12,8 @@ const TierEnum = z.enum(TIERS, {
10
12
  });
11
13
  export const TIER_RANK = { cheap: 0, mid: 1, frontier: 2 };
12
14
  export const DEFAULT_DIFF_CAP = 60_000;
15
+ // This is deliberately only a rejection guard. T20's cited table, not this floor, supplies each claim.
16
+ export const MODEL_WINDOW_PLAUSIBILITY_FLOOR = 1_024;
13
17
  // v1.51 T1: routing.mode — a preset COMPILED INTO FLOORS at config load. The router never sees the
14
18
  // mode; it receives resolved floors only (the structural defense against the quality-silently-loses
15
19
  // class — no fourth runtime authority, no new precedence key).
@@ -38,12 +42,36 @@ export const MapEntrySchema = z.object({
38
42
  prefer: z.array(z.string()).optional(),
39
43
  escalate: z.boolean().optional(),
40
44
  });
45
+ const DeclaredVendorSchema = z.string().trim().min(1, "vendor must be nonempty");
46
+ const ModelOverrideSchema = z
47
+ .object({
48
+ vendor: DeclaredVendorSchema.optional(),
49
+ channel: z.enum(["sub", "api"]).optional(),
50
+ })
51
+ .refine((override) => override.vendor !== undefined || override.channel !== undefined, {
52
+ message: "a model override must declare vendor, channel, or both",
53
+ });
41
54
  export const TierEntrySchema = z.object({
42
- vendor: z.string(),
55
+ vendor: DeclaredVendorSchema.nullable(),
43
56
  channel: z.enum(["sub", "api"]),
44
57
  models: z.record(z.string(), TierEnum),
58
+ // T19: keep model values scalar for existing consumers. Optional sibling metadata resolves before
59
+ // parent metadata in channelsFromConfig; neither provider prefixes nor adapter ids manufacture it.
60
+ modelOverrides: z.record(z.string(), ModelOverrideSchema).optional(),
45
61
  // v1.47 T3: optional per-model context-window sizes (tokens). Absent block ⇒ no doctor column, no plan lint.
46
62
  windows: z.record(z.string(), z.number().int().positive()).optional(),
63
+ }).superRefine((entry, ctx) => {
64
+ if (entry.vendor !== null)
65
+ return;
66
+ for (const model of Object.keys(entry.models)) {
67
+ if (entry.modelOverrides?.[model]?.vendor)
68
+ continue;
69
+ ctx.addIssue({
70
+ code: "custom",
71
+ path: ["modelOverrides", model, "vendor"],
72
+ message: "vendor: null requires every configured model to declare a nonempty vendor override",
73
+ });
74
+ }
47
75
  });
48
76
  // v1.10 FLEET-06: optional routing.allow/deny fleet preference; absent blocks ⇒ byte-identical routing/discovery.
49
77
  // deny wins on conflict; presence of allow (even {} / empty arrays) activates allowlist (fail-closed).
@@ -73,6 +101,143 @@ export const SubPricingSchema = z
73
101
  windowsPerMonthHigh: z.number().int().positive(),
74
102
  })
75
103
  .refine((s) => s.windowsPerMonthLow <= s.windowsPerMonthHigh, "windowsPerMonthLow must be ≤ windowsPerMonthHigh");
104
+ // ── Review participation (R3 of OVERSEER-RULING-20260731-velocity; OBS-186) ────────────────────
105
+ // Participation is keyed on PATHS — what a task DECLARES at compile time and what its diff ACTUALLY
106
+ // touches at gate time — never on complexity. `complexityThreshold` capped participation at a number
107
+ // the authoring law forbade tasks from reaching, so cross-vendor review was unreachable by
108
+ // construction while `required: true` claimed the opposite. Paths cannot drift out from under the
109
+ // gate that way: the same predicate decides both ends.
110
+ export const REVIEW_POLICIES = ["full", "judge-only"];
111
+ /**
112
+ * The leaf class: work a judge alone can carry because there is no behaviour to review. It is CLOSED
113
+ * and enumerated here — documentation pages, the changelog, the release runbook, and the version
114
+ * mirrors a release bump rewrites. Everything else, including tests and scripts, is full-review work.
115
+ */
116
+ export const REVIEW_LEAF_GLOBS = [
117
+ "docs/**", "CHANGELOG.md", "RELEASING.md", "package.json", "package-lock.json",
118
+ ];
119
+ /**
120
+ * Membership is a POSITIVE rule, not a blacklist. A blacklist of source extensions is a list of the
121
+ * formats someone thought of, and everything nobody thought of — `docs/widget.vue`, `docs/site.svelte`,
122
+ * `docs/Makefile`, `docs/package.json`, an extensionless script — falls into the leaf class and skips
123
+ * its review. That direction of failure is the whole defect OBS-186 names, so the rule is inverted:
124
+ * under `docs/` a path is leaf work only when its extension is a DOCUMENTATION format. An unknown
125
+ * format is behaviour until proven otherwise, which is the fail-closed direction.
126
+ */
127
+ const REVIEW_DOC_EXT_RE = /\.(?:md|mdx|markdown|txt|rst|adoc|asciidoc)$/i;
128
+ /**
129
+ * The version mirrors a release bump rewrites — and ONLY at the repo root: `docs/package.json` is a
130
+ * real manifest for something, not a mirror. Membership here is still a claim about paths, and a
131
+ * manifest diff that moves more than the version field leaves the class; the gate re-tests that
132
+ * against the actual diff content (src/gates/review.ts) because a path cannot say it by itself.
133
+ */
134
+ export const REVIEW_VERSION_MIRRORS = new Set(["package.json", "package-lock.json"]);
135
+ /** The changelog and the release runbook, at the repo root, by exact name. */
136
+ const REVIEW_LEAF_FILES = new Set(["CHANGELOG.md", "RELEASING.md"]);
137
+ /**
138
+ * Paths whose review may never be skipped (R3): the gate/run/driver/adapter surface — lifecycle,
139
+ * demux, and everything that reaches a shell. Shipped as the default so the compile lint that
140
+ * enforces them is live in a repo that never configures anything.
141
+ */
142
+ export const DEFAULT_REVIEW_CRITICAL_PATHS = [
143
+ "src/gates/**", "src/run/**", "src/drivers/**", "src/adapters/**",
144
+ ];
145
+ const GLOB_META_RE = /[*?[\]{}]/;
146
+ const normPath = (p) => p.replace(/^\.\//, "").trim();
147
+ /**
148
+ * True when `path` — an ACTUAL path a diff touched — is provably leaf-class work. Enumerated, closed,
149
+ * and positive: the two named root files, the two root version mirrors, and documentation formats
150
+ * under `docs/`. Everything else, including a `.vue`/`.svelte`/`Makefile`/`package.json` parked under
151
+ * `docs/`, is behaviour and draws a full review.
152
+ */
153
+ export function isReviewLeafPath(path) {
154
+ const p = normPath(path);
155
+ if (!p.length)
156
+ return false;
157
+ if (REVIEW_LEAF_FILES.has(p) || REVIEW_VERSION_MIRRORS.has(p))
158
+ return true;
159
+ return p.startsWith("docs/") && REVIEW_DOC_EXT_RE.test(p);
160
+ }
161
+ /**
162
+ * True when a DECLARED pattern may claim leaf work. A declaration is a claim about a directory, not
163
+ * a file: `docs/**` is how a documentation task declares itself and has to stay claimable, so a glob
164
+ * rooted at `docs/` reads leaf here while `docs/tool.ts` does not read leaf above. The claim is never
165
+ * the last word — reviewGate re-tests it against what the diff actually touched and promotes when the
166
+ * diff leaves the class. A glob rooted anywhere else can claim nothing.
167
+ */
168
+ export function isReviewLeafDeclaration(pattern) {
169
+ const p = normPath(pattern);
170
+ if (!p.length)
171
+ return false;
172
+ return isReviewLeafPath(p) || (GLOB_META_RE.test(p) && p.startsWith("docs/"));
173
+ }
174
+ /** full beats judge-only. The join is monotone by construction: nothing here can LOWER a policy. */
175
+ export function raiseReviewPolicy(...policies) {
176
+ return policies.includes("full") ? "full" : "judge-only";
177
+ }
178
+ /**
179
+ * The compiler's assignment, from the task's DECLARED files[]. judge-only only when EVERY declared
180
+ * path is provably leaf work — one non-leaf pattern (or an empty files[]) makes it full.
181
+ */
182
+ export function declaredReviewPolicy(files) {
183
+ const declared = files.map(normPath).filter((f) => f.length > 0);
184
+ return declared.length > 0 && declared.every(isReviewLeafDeclaration) ? "judge-only" : "full";
185
+ }
186
+ /** The declared assignment after the operator's floor is applied — config may raise it, never lower it. */
187
+ export function effectiveReviewPolicy(files, review) {
188
+ return raiseReviewPolicy(declaredReviewPolicy(files), review.policy);
189
+ }
190
+ /** The literal head of a pattern — everything before its first glob metacharacter. */
191
+ const literalPrefix = (p) => {
192
+ const i = p.search(GLOB_META_RE);
193
+ return i < 0 ? p : p.slice(0, i);
194
+ };
195
+ /** The literal tail of a pattern — everything after its last glob metacharacter. */
196
+ const literalSuffix = (p) => p.replace(/^.*[*?[\]{}]/s, "");
197
+ const eitherWay = (a, b, rel) => rel(a, b) || rel(b, a);
198
+ /**
199
+ * Do two GLOBS have a path in common? Exact glob-vs-glob intersection is undecidable in general, so
200
+ * this fails CLOSED: the pair is treated as intersecting unless their literal heads or their literal
201
+ * tails prove they cannot meet. `docs/**\/README.md` vs `docs/security/*.md` share the head `docs/`
202
+ * and the tail `.md` is a tail of `/README.md`, so `docs/security/README.md` is correctly flagged;
203
+ * `src/**\/*.ts` vs `src/**\/*.md` share a head but neither tail contains the other, so they are not.
204
+ */
205
+ const globsMayIntersect = (a, b) => eitherWay(literalPrefix(a), literalPrefix(b), (x, y) => x.startsWith(y))
206
+ && eitherWay(literalSuffix(a), literalSuffix(b), (x, y) => x.endsWith(y));
207
+ /**
208
+ * Declared patterns that intersect a critical path. Directional containment is the certain half — a
209
+ * critical `src/gates/**` contains a declared `src/gates/review.ts`, and a declared `docs/**` contains
210
+ * a critical `docs/security/**`. It is NOT intersection: two globs can overlap without either matching
211
+ * the other as a literal string, which is how a judge-only task declaring `docs/**\/README.md` slipped
212
+ * past a critical `docs/security/*.md`. When BOTH sides are globs the conservative overlap test above
213
+ * decides, and it decides toward flagging — an over-flagged task is narrowed by its author, an
214
+ * under-flagged one skips the only gate that exists for it.
215
+ */
216
+ export function criticalPathHits(files, criticalPaths = []) {
217
+ const hits = new Set();
218
+ for (const raw of files) {
219
+ const f = normPath(raw);
220
+ if (!f)
221
+ continue;
222
+ for (const rawCritical of criticalPaths) {
223
+ const critical = normPath(rawCritical);
224
+ if (!critical)
225
+ continue;
226
+ if (f === critical || picomatch(critical, { dot: true })(f) || picomatch(f, { dot: true })(critical)
227
+ || (GLOB_META_RE.test(f) && GLOB_META_RE.test(critical) && globsMayIntersect(f, critical))) {
228
+ hits.add(f);
229
+ }
230
+ }
231
+ }
232
+ return [...hits].sort();
233
+ }
234
+ // R3 (OBS-186): `review: false` here is a SECOND participation switch, and unlike review.policy it is
235
+ // not monotone — it can omit the review gate from a task the path rules assigned `full`. Participation
236
+ // is decided by paths, so the two are JOINED at the one seam that can still refuse the graph:
237
+ // reviewParticipationErrors (src/compile/collateral.ts) fails compile when a shape's `review: false`
238
+ // would demote a full-review task or silence a critical path. The key itself stays parseable (a run's
239
+ // own config, the setup cockpit and the shipped template all round-trip it); what it can no longer do
240
+ // is take effect behind the policy's back.
76
241
  const ShapeGateParticipationSchema = z
77
242
  .object({ acceptance: z.boolean().optional(), review: z.boolean().optional() })
78
243
  .passthrough()
@@ -157,7 +322,22 @@ export const TickmarkrConfigSchema = z.object({
157
322
  judge: z.object({ adapter: z.string(), model: z.string() }),
158
323
  // v1.53 T2: prefer — ordered reviewer preference (entry grammar: adapter | adapter:model, same as
159
324
  // routing.map.prefer). Reorders diversity-eligible channels only; never widens or narrows eligibility.
160
- review: z.object({ complexityThreshold: z.number(), required: z.boolean(), prefer: z.array(z.string()).optional() }),
325
+ review: z.object({
326
+ // RETIRED as a participation switch (R3 / OBS-186, v1.85): review participation is decided by the
327
+ // paths a task declares and the paths its diff touches — never by complexity. The key survives
328
+ // because routing hints and the setup cockpit still read it as a complexity landmark; NOTHING in
329
+ // src/gates/review.ts reads it any more, and a value here can no longer disable a review.
330
+ complexityThreshold: z.number(),
331
+ required: z.boolean(),
332
+ prefer: z.array(z.string()).optional(),
333
+ // R3: the operator's participation floor. "full" forces a cross-vendor review on every task;
334
+ // "judge-only" is the neutral floor (it leaves the compiler's per-task assignment standing).
335
+ // Config may RAISE a task's policy and can never lower one — see raiseReviewPolicy.
336
+ policy: z.enum(REVIEW_POLICIES).optional(),
337
+ // R3: globs no task may skip review on (input/demux/lifecycle, gates/run/drivers/adapters, anything
338
+ // reaching a shell). A judge-only task intersecting one of these fails COMPILE, never silently skips.
339
+ criticalPaths: z.array(z.string()).optional(),
340
+ }),
161
341
  // v1.54 T1: prefer — ranked consult seat failover. Entries MUST be adapter:model (unlike
162
342
  // review.prefer's adapter|adapter:model grammar): a consult seat has no channel to inherit a
163
343
  // model from, so a bare adapter is meaningless and fails config load fail-closed. The pinned
@@ -188,6 +368,24 @@ export class ConfigError extends Error {
188
368
  this.name = "ConfigError";
189
369
  }
190
370
  }
371
+ function assertSeededModelWindows() {
372
+ const cited = new Map(CITED_MODEL_WINDOWS.map((claim) => [claim.modelId, claim.window]));
373
+ for (const [adapter, entry] of Object.entries(DEFAULT_CONFIG.tiers)) {
374
+ for (const [model, window] of Object.entries(entry.windows ?? {})) {
375
+ const path = `tiers.${adapter}.windows.${model}`;
376
+ if (window < MODEL_WINDOW_PLAUSIBILITY_FLOOR) {
377
+ throw new ConfigError(`${path} declares window ${window}, below plausibility floor ${MODEL_WINDOW_PLAUSIBILITY_FLOOR} (non-authoritative rejection guard)`);
378
+ }
379
+ const citedWindow = cited.get(model);
380
+ if (citedWindow === undefined) {
381
+ throw new ConfigError(`${path} has no cited model-window entry in T20's vendored table`);
382
+ }
383
+ if (window !== citedWindow) {
384
+ throw new ConfigError(`${path} declares ${window} but does not match cited window ${citedWindow} in T20's vendored table`);
385
+ }
386
+ }
387
+ }
388
+ }
191
389
  export const DEFAULT_CONFIG = {
192
390
  concurrency: 3,
193
391
  driver: "auto",
@@ -221,10 +419,13 @@ export const DEFAULT_CONFIG = {
221
419
  allowUnverifiedModels: false,
222
420
  },
223
421
  // Seed table (spec §13). New models = edit this (or your config.yaml), never code.
422
+ // Windows are copied rather than derived from T20's table so config load can reject disagreement
423
+ // between two independently owned artifacts instead of letting one side certify itself.
224
424
  tiers: {
225
425
  "claude-code": {
226
426
  vendor: "anthropic", channel: "sub",
227
427
  models: { fable: "frontier", opus: "frontier", sonnet: "mid", haiku: "cheap" },
428
+ windows: { fable: 1_000_000, opus: 1_000_000, sonnet: 1_000_000, haiku: 200_000 },
228
429
  },
229
430
  // ids verified against installed CLI models_cache.json (codex 0.144.0, fetched 2026-07-09);
230
431
  // gpt-5.4 / gpt-5.4-mini removed — OpenAI retirement 2026-07-23; sol=frontier / terra=mid / luna=cheap per OpenAI tier framing.
@@ -236,6 +437,7 @@ export const DEFAULT_CONFIG = {
236
437
  codex: {
237
438
  vendor: "openai", channel: "sub",
238
439
  models: { "gpt-5.6-sol": "frontier", "gpt-5.5": "frontier", "gpt-5.6-terra": "mid", "gpt-5.6-luna": "cheap" },
440
+ windows: { "gpt-5.6-sol": 1_050_000, "gpt-5.5": 1_050_000, "gpt-5.6-terra": 1_050_000, "gpt-5.6-luna": 1_050_000 },
239
441
  },
240
442
  // grok-4.5 (xAI, released 2026-07-08) → mid: AA Intelligence 54 (#4), Terminal-Bench 2.1 83.3%
241
443
  // (≈ GPT-5.5 83.4, Fable 5 84.3), SWE-bench Pro 64.7%, ~4.2× more token-efficient than Opus 4.8
@@ -256,6 +458,7 @@ export const DEFAULT_CONFIG = {
256
458
  // stop burning its mid. Operator-approved 2026-07-16.
257
459
  "composer-2.5-fast": "cheap",
258
460
  },
461
+ windows: { "composer-2.5": 200_000, "composer-2.5-fast": 200_000 },
259
462
  },
260
463
  // GLM-5.2 → mid per benchmark policy (2026-07): SWE-bench Pro 62.1 (> GPT-5.5 58.6), FrontierSWE 74.4 ≈ Opus 4.8;
261
464
  // no independent Terminal-Bench score → conservative mid, overlays may raise.
@@ -266,6 +469,7 @@ export const DEFAULT_CONFIG = {
266
469
  opencode: {
267
470
  vendor: "mixed", channel: "sub",
268
471
  models: { "zai-coding-plan/glm-5.2": "mid" },
472
+ windows: { "zai-coding-plan/glm-5.2": 1_000_000 },
269
473
  },
270
474
  // GLM-5.2 via pi/ZAI Coding Plan (sub, flat-rate). mid per benchmark policy (2026-07):
271
475
  // SWE-bench Pro 62.1, FrontierSWE 74.4 — same rationale as the opencode glm-5.2 seed above.
@@ -277,6 +481,7 @@ export const DEFAULT_CONFIG = {
277
481
  pi: {
278
482
  vendor: "zhipu", channel: "sub",
279
483
  models: { "zai/glm-5.2": "mid" },
484
+ windows: { "zai/glm-5.2": 1_000_000 },
280
485
  },
281
486
  // Native grok CLI (Phase 40). grok-4.5 → mid: same benchmark provenance as the retired cursor-agent
282
487
  // grok-4.5-xhigh seed above (AA 54, TB2.1 83.3%, SWE-b Pro 64.7% — researched 2026-07-10).
@@ -286,6 +491,7 @@ export const DEFAULT_CONFIG = {
286
491
  grok: {
287
492
  vendor: "xai", channel: "sub",
288
493
  models: { "grok-4.5": "mid", "grok-composer-2.5-fast": "cheap" },
494
+ windows: { "grok-4.5": 500_000, "grok-composer-2.5-fast": 200_000 },
289
495
  },
290
496
  // Native kimi CLI (Phase 48). kimi-code/k3 → frontier: 81.2 FrontierSWE, 88.3 Terminal-Bench 2.1,
291
497
  // top-3 across six coding benchmarks (research 2026-07-17, K3 released 2026-07-16).
@@ -298,18 +504,25 @@ export const DEFAULT_CONFIG = {
298
504
  "kimi-code/kimi-for-coding": "mid",
299
505
  "kimi-code/kimi-for-coding-highspeed": "cheap",
300
506
  },
507
+ windows: {
508
+ "kimi-code/k3": 1_048_576,
509
+ "kimi-code/kimi-for-coding": 262_144,
510
+ "kimi-code/kimi-for-coding-highspeed": 262_144,
511
+ },
301
512
  },
302
513
  },
303
514
  pricing: { cheap: 0.1, mid: 0.5, frontier: 2.5 },
304
515
  gates: { diffCap: DEFAULT_DIFF_CAP },
305
516
  judge: { adapter: "claude-code", model: "fable" },
306
- review: { complexityThreshold: 7, required: true },
517
+ // R3: no `policy` floor — the neutral floor leaves the compiler's per-task assignment standing, so
518
+ // the path-keyed rule is reachable out of the box rather than raised to full by construction.
519
+ review: { complexityThreshold: 7, required: true, criticalPaths: [...DEFAULT_REVIEW_CRITICAL_PATHS] },
307
520
  consult: { adapter: "claude-code", model: "fable", stallMinutes: 15 },
308
521
  // v1.4: gate LLM calls (judge/review/consult) run headless by default; pane opts back into visible agents.
309
522
  // v1.2: workers are the real agent TUI in the pane; "print" restores the -p-rendered-in-pane path.
310
523
  visibility: { llm: "headless", keepPanes: "run", worker: "interactive", workersPerTab: 3 },
311
524
  };
312
- function deepMerge(base, over) {
525
+ function deepMerge(base, over, path = []) {
313
526
  if (over === undefined || over === null)
314
527
  return base;
315
528
  if (Array.isArray(base) || Array.isArray(over) || typeof base !== "object" || typeof over !== "object" || base === null) {
@@ -318,12 +531,18 @@ function deepMerge(base, over) {
318
531
  const out = { ...base };
319
532
  for (const [k, v] of Object.entries(over)) {
320
533
  if (v === null) {
321
- delete out[k]; // v1.1 tombstone: an explicit null in an overlay removes the key (e.g. stale tiers model ids)
534
+ // T19: null is a declared multi-vendor state only at tiers.<adapter>.vendor. Every other null
535
+ // remains the v1.1 overlay tombstone (for example a stale tiers model id).
536
+ if (path.length === 2 && path[0] === "tiers" && k === "vendor")
537
+ out[k] = null;
538
+ else
539
+ delete out[k];
322
540
  continue;
323
541
  }
324
542
  // OBS-75 class: merge fresh-landing objects onto {} so nested tombstone nulls are pruned even when
325
543
  // no lower layer set the key (a fleet-cleared deny writes {adapters: null} — the schema rejects raw null)
326
- out[k] = k in out ? deepMerge(out[k], v) : deepMerge({}, v);
544
+ const childPath = [...path, k];
545
+ out[k] = k in out ? deepMerge(out[k], v, childPath) : deepMerge({}, v, childPath);
327
546
  }
328
547
  return out;
329
548
  }
@@ -401,7 +620,7 @@ function resolveRoutingMode(cfg, layers) {
401
620
  lints.push(`floors.${shape}: ${val} (config floors) overrides mode ${mode} — shadowed delta: ${shape} ${dflt}→${moded}`);
402
621
  }
403
622
  if (TIER_RANK[val] < TIER_RANK[integrityMin(shape)]) {
404
- lints.push(`floors.${shape}: ${val} is below integrity minimum frontier — integrity class ${INTEGRITY_FLOOR_SHAPES.join("/")} holds regardless of mode`);
623
+ lints.push(`floors.${shape}: ${val} is below integrity minimum frontier — integrity class ${INTEGRITY_FLOOR_SHAPES.join("/")} is advisory to explicit floors and map pins; map pins are supreme`);
405
624
  }
406
625
  continue;
407
626
  }
@@ -431,6 +650,9 @@ export function loadConfigWithMode(repoRoot, opts = {}) {
431
650
  const r = TickmarkrConfigSchema.safeParse(merged);
432
651
  if (!r.success)
433
652
  throw new ConfigError(z.prettifyError(r.error));
653
+ // The vendored table attests Tickmarkr's built-in seed artifact. Operator overlays remain free to
654
+ // declare private/future models and deliberately smaller test windows without pretending T20 cited them.
655
+ assertSeededModelWindows();
434
656
  return { cfg: r.data, mode: resolveRoutingMode(r.data, [globalCfg, repoCfg]) };
435
657
  }
436
658
  export function loadConfig(repoRoot, opts = {}) {
@@ -487,7 +709,7 @@ export function configTemplate(overlay) {
487
709
  # llm: headless # headless (default): judge/review/consult run silently | pane: visible agents
488
710
  # keepPanes: run # run (default): ephemeral judge/review/consult panes close when read; a merged task's worker pane closes on done; other worker panes persist until run end | attempt | forever (keep everything for debugging)
489
711
  # worker: interactive # interactive (default): workers run the real agent TUI | print
490
- # workersPerTab: 3 # cap concurrent worker panes per WORKERS tab; overflow opens WORKERS-2 (VIS-09). Default 3
712
+ # workersPerTab: 3 # cap worker panes sharing one tab; gate panes ride with their task and never count. Default 3
491
713
  # scope:
492
714
  # allowDeviations: [] # globs an operator permits out-of-scope edits into, e.g. ["package-lock.json"]
493
715
  # gates: # override auto-detected commands; per-shape participation may only skip LLM gates
@@ -541,9 +763,8 @@ export function configTemplate(overlay) {
541
763
  const nl = base.indexOf("\n");
542
764
  return `${base.slice(0, nl + 1)}${lines.join("\n")}\n${base.slice(nl + 1)}`;
543
765
  }
544
- // v1.61 seed 8: the fleet-overlay provenance cluster (harvesting, serialization, diff rendering)
545
- // moved to fleet-overlay.ts as a pure move — re-exported here so prior import paths keep working.
546
- export { FLEET_OVERLAY_KEYS, fleetEditableEquals, fleetRepoOverlayFromDelta, harvestFleetProvenance, repoOverlayYaml, serializeFleetOverlay, unifiedYamlDiff, } from "./fleet-overlay.js";
766
+ // Fleet overlay mutation, legacy serialization, and diff rendering live together in fleet-overlay.ts.
767
+ export { FLEET_OVERLAY_KEYS, fleetEditableEquals, fleetRepoOverlayFromDelta, renderFleetOverlayWrite, repoOverlayYaml, serializeFleetOverlay, unifiedYamlDiff, } from "./fleet-overlay.js";
547
768
  export function repoOverlayPath(repoRoot) {
548
769
  return join(repoRoot, stateDirName(repoRoot), "config.yaml");
549
770
  }
@@ -555,15 +776,12 @@ export function readOverlayFile(path) {
555
776
  throw new ConfigError(`invalid overlay at ${path}`);
556
777
  return raw;
557
778
  }
558
- export function fleetEditableFromConfig(cfg, provenance = {}) {
779
+ export function fleetEditableFromConfig(cfg) {
559
780
  const tiers = {};
560
781
  for (const [adapter, entry] of Object.entries(cfg.tiers)) {
561
782
  tiers[adapter] = {};
562
783
  for (const [model, tier] of Object.entries(entry.models)) {
563
- // OBS-88: notes harvested from the overlay ride the session state from load, so a later
564
- // write knows about every existing note — not only the ones this session typed itself
565
- const note = provenance[adapter]?.[model];
566
- tiers[adapter][model] = note ? { tier, provenance: note } : { tier };
784
+ tiers[adapter][model] = { tier };
567
785
  }
568
786
  }
569
787
  return {
@@ -599,55 +817,24 @@ export function fleetKeyLayer(repoRoot, dotted, opts = {}) {
599
817
  export function formatFleetPrint(repoRoot, opts = {}) {
600
818
  const gdir = opts.globalDir ?? globalConfigDir();
601
819
  const effective = loadConfig(repoRoot, { globalDir: gdir });
602
- const editable = fleetEditableFromConfig(effective);
603
- const lines = ["# tickmarkr fleet — effective state (repo > global > defaults)"];
604
- const annotate = (dotted, yaml) => `${yaml} # ${fleetKeyLayer(repoRoot, dotted, opts)}`;
605
- const denyAdapters = editable.denyAdapters;
606
- const denyModels = editable.denyModels;
607
- if (denyAdapters.length || denyModels.length) {
608
- lines.push("routing:");
609
- lines.push(" deny:");
610
- if (denyAdapters.length)
611
- lines.push(annotate("routing.deny.adapters", ` adapters: ${stringify(denyAdapters).trim()}`));
612
- if (denyModels.length)
613
- lines.push(annotate("routing.deny.models", ` models: ${stringify(denyModels).trim()}`));
614
- }
615
- const mapKeys = Object.keys(editable.map).sort();
616
- if (mapKeys.length) {
617
- if (!lines.some((l) => l === "routing:"))
618
- lines.push("routing:");
619
- lines.push(" map:");
620
- for (const shape of mapKeys) {
621
- const entry = editable.map[shape];
622
- const body = stringify(entry, { indent: 4 }).trim().split("\n").map((l) => ` ${l}`).join("\n");
623
- lines.push(annotate(`routing.map.${shape}`, ` ${shape}:`));
624
- lines.push(body.split("\n").slice(1).join("\n"));
625
- }
626
- }
627
- const floorKeys = Object.keys(editable.floors).sort();
628
- if (floorKeys.length) {
629
- if (!lines.some((l) => l === "routing:"))
630
- lines.push("routing:");
631
- lines.push(annotate("routing.floors", ` floors: ${stringify(Object.fromEntries(floorKeys.map((k) => [k, editable.floors[k]]))).trim().replace(/^/gm, " ")}`));
632
- }
633
- const tierAdapters = Object.keys(editable.tiers).sort();
634
- if (tierAdapters.length) {
635
- lines.push("tiers:");
636
- for (const adapter of tierAdapters) {
637
- const models = editable.tiers[adapter];
638
- const modelIds = Object.keys(models).sort();
639
- if (!modelIds.length)
640
- continue;
641
- lines.push(` ${adapter}:`);
642
- lines.push(" models:");
643
- for (const model of modelIds) {
644
- const v = models[model];
645
- if (v === null)
646
- lines.push(` ${model}: null`);
647
- else
648
- lines.push(annotate(`tiers.${adapter}.models.${model}`, ` ${model}: ${v.tier}`));
649
- }
820
+ const doc = new Document({ routing: effective.routing, tiers: effective.tiers });
821
+ doc.commentBefore = " tickmarkr fleet — effective state (repo > global > defaults)";
822
+ const annotate = (path, dotted = path.join(".")) => {
823
+ const node = doc.getIn(path, true);
824
+ if (node && typeof node === "object" && "comment" in node) {
825
+ node.comment = ` ${fleetKeyLayer(repoRoot, dotted, opts)}`;
650
826
  }
827
+ };
828
+ if (effective.routing.deny?.adapters !== undefined)
829
+ annotate(["routing", "deny", "adapters"]);
830
+ if (effective.routing.deny?.models !== undefined)
831
+ annotate(["routing", "deny", "models"]);
832
+ for (const shape of Object.keys(effective.routing.map))
833
+ annotate(["routing", "map", shape]);
834
+ annotate(["routing", "floors"]);
835
+ for (const [adapter, entry] of Object.entries(effective.tiers)) {
836
+ for (const model of Object.keys(entry.models))
837
+ annotate(["tiers", adapter, "models", model]);
651
838
  }
652
- return `${lines.join("\n")}\n`;
839
+ return String(doc);
653
840
  }
@@ -1,27 +1,32 @@
1
- import type { FleetEditable } from "./config.js";
1
+ import type { FleetEditable, RoutingMode } from "./config.js";
2
2
  /** Fleet-owned overlay keys — the only config surface `tickmarkr fleet` may write. */
3
3
  export declare const FLEET_OVERLAY_KEYS: readonly ["routing", "tiers"];
4
- /** Trailing `# note` comments on per-entry lines an operator may have hand-written or a prior
5
- * fleet write stamped: model tier lines (including null tombstones) and deny list items. */
6
- export type HarvestedProvenance = {
7
- tiers: Record<string, Record<string, string>>;
8
- denyAdapters: Record<string, string>;
9
- denyModels: Record<string, string>;
4
+ export type FleetOverlayWrite = {
5
+ initial: FleetEditable;
6
+ edited: FleetEditable;
7
+ mode?: RoutingMode;
8
+ steering?: {
9
+ initial: {
10
+ review?: string[];
11
+ consult?: string[];
12
+ };
13
+ edited: {
14
+ review?: string[];
15
+ consult?: string[];
16
+ };
17
+ };
10
18
  };
11
- /** OBS-88: harvest existing provenance comments from raw repo-overlay bytes at fleet-session
12
- * load. yaml.parse discards comments, so before this every fleet write re-serialized the file
13
- * knowing only the current session's own notes and silently stripped all prior ones — a typed
14
- * benchmark-provenance note survived exactly one write. Fail-open to empty: an unreadable
15
- * overlay is the loader's problem to reject, never the harvester's. */
16
- export declare function harvestFleetProvenance(overlayText: string): HarvestedProvenance;
17
- /** Harvested deny-entry notes keyed by the exact entry string, re-attached at serialize time. */
18
- export type FleetDenyNotes = {
19
- adapters?: Record<string, string>;
20
- models?: Record<string, string>;
19
+ export type FleetFirstTouch = {
20
+ vendor: string;
21
+ channel: "sub" | "api";
21
22
  };
23
+ export declare function fleetFirstTouchProvenance(note: string, firstTouch: FleetFirstTouch): string;
24
+ /** Apply only fields fleet authored to the parsed YAML document. Untouched nodes retain their
25
+ * keys, ordering, scalar style, and comments; fresh provenance is written directly on its tier node. */
26
+ export declare function renderFleetOverlayWrite(priorBytes: string, write: FleetOverlayWrite): string;
22
27
  /** Build the repo overlay fragment fleet would write for edits since session start. */
23
- export declare function fleetRepoOverlayFromDelta(initial: FleetEditable, edited: FleetEditable, existingRepo?: Record<string, unknown>): Record<string, unknown>;
24
- export declare function repoOverlayYaml(overlay: Record<string, unknown>, provenance?: Record<string, Record<string, string>>, denyNotes?: FleetDenyNotes): string;
25
- export declare function serializeFleetOverlay(overlay: Record<string, unknown>, provenance?: Record<string, Record<string, string>>, denyNotes?: FleetDenyNotes): string;
28
+ export declare function fleetRepoOverlayFromDelta(initial: FleetEditable, edited: FleetEditable, existingRepo?: Record<string, unknown>, firstTouches?: Readonly<Record<string, FleetFirstTouch>>): Record<string, unknown>;
29
+ export declare function repoOverlayYaml(overlay: Record<string, unknown>): string;
30
+ export declare function serializeFleetOverlay(overlay: Record<string, unknown>): string;
26
31
  export declare function unifiedYamlDiff(before: string, after: string, label?: string): string;
27
32
  export declare function fleetEditableEquals(a: FleetEditable, b: FleetEditable): boolean;