tickmarkr 2.6.3 → 2.6.5
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 +10 -2
- package/dist/adapters/catalog-remote.d.ts +23 -0
- package/dist/adapters/catalog-remote.js +38 -0
- package/dist/adapters/claude-code.d.ts +1 -1
- package/dist/adapters/claude-code.js +4 -1
- package/dist/adapters/codex-commit-check.d.ts +25 -0
- package/dist/adapters/codex-commit-check.js +108 -0
- package/dist/adapters/codex.d.ts +2 -1
- package/dist/adapters/codex.js +26 -5
- package/dist/adapters/model-discovery-check.d.ts +33 -0
- package/dist/adapters/model-discovery-check.js +123 -0
- package/dist/adapters/model-lints.d.ts +2 -1
- package/dist/adapters/model-lints.js +52 -1
- package/dist/adapters/model-windows.d.ts +2 -1
- package/dist/adapters/model-windows.js +18 -7
- package/dist/adapters/prompt.js +3 -2
- package/dist/adapters/types.d.ts +13 -0
- package/dist/cli/commands/approve.js +37 -6
- package/dist/cli/commands/doctor.d.ts +4 -0
- package/dist/cli/commands/doctor.js +20 -3
- package/dist/cli/commands/fleet.d.ts +10 -1
- package/dist/cli/commands/fleet.js +279 -95
- package/dist/cli/commands/init.js +10 -4
- package/dist/cli/commands/plan.js +65 -4
- package/dist/cli/commands/report.d.ts +39 -2
- package/dist/cli/commands/report.js +227 -10
- package/dist/cli/commands/resume.js +2 -4
- package/dist/cli/commands/run.d.ts +11 -0
- package/dist/cli/commands/run.js +33 -5
- package/dist/cli/commands/status.js +30 -3
- package/dist/cli/commands/verify.d.ts +1 -1
- package/dist/cli/commands/verify.js +356 -123
- package/dist/compile/native.js +130 -0
- package/dist/config/config.d.ts +25 -7
- package/dist/config/config.js +92 -11
- package/dist/config/fleet-overlay.d.ts +2 -1
- package/dist/config/fleet-overlay.js +77 -4
- package/dist/drivers/orca.d.ts +19 -1
- package/dist/drivers/orca.js +161 -31
- package/dist/gates/cache.d.ts +4 -0
- package/dist/gates/cache.js +10 -2
- package/dist/gates/llm.d.ts +4 -1
- package/dist/gates/llm.js +6 -1
- package/dist/gates/review.js +8 -3
- package/dist/gates/run-gates.d.ts +3 -2
- package/dist/gates/run-gates.js +91 -50
- package/dist/route/router.d.ts +11 -0
- package/dist/route/router.js +29 -8
- package/dist/run/daemon.js +158 -22
- package/dist/run/git.js +9 -0
- package/dist/run/journal.d.ts +101 -1
- package/dist/run/journal.js +352 -2
- package/dist/run/launchservices-check.d.ts +56 -0
- package/dist/run/launchservices-check.js +117 -0
- package/dist/run/operator-state.d.ts +22 -2
- package/dist/run/operator-state.js +20 -3
- package/dist/run/title-environment.d.ts +45 -0
- package/dist/run/title-environment.js +127 -0
- package/dist/run/title-preload.cjs +38 -0
- package/dist/run/title-preload.d.cts +1 -0
- package/dist/tui/cockpit/board.js +12 -6
- package/dist/tui/cockpit/keys.d.ts +8 -0
- package/dist/tui/cockpit/keys.js +4 -1
- package/dist/tui/cockpit/layout.d.ts +2 -2
- package/dist/tui/cockpit/layout.js +2 -2
- package/dist/tui/cockpit/live-runtime.js +46 -20
- package/dist/tui/cockpit/live-store.d.ts +30 -1
- package/dist/tui/cockpit/live-store.js +134 -18
- package/dist/tui/cockpit/log-view.d.ts +28 -0
- package/dist/tui/cockpit/log-view.js +60 -0
- package/dist/tui/cockpit/run-view.js +4 -0
- package/dist/tui/cockpit/shell.d.ts +2 -1
- package/dist/tui/cockpit/shell.js +6 -4
- package/dist/tui/ink/fleet-app.d.ts +12 -6
- package/dist/tui/ink/fleet-app.js +17 -11
- package/package.json +1 -1
- package/skills/tickmarkr-auto/SKILL.md +33 -3
- package/skills/tickmarkr-loop/SKILL.md +55 -5
- package/skills/tickmarkr-overseer/SKILL.md +83 -8
package/dist/compile/native.js
CHANGED
|
@@ -58,6 +58,121 @@ export function classifyContextPath(entry, tracked, repoDir) {
|
|
|
58
58
|
const hits = [...tracked].filter((t) => basename(t) === base);
|
|
59
59
|
return hits.length === 1 ? { kind: "missing", suggestion: hits[0] } : { kind: "missing" };
|
|
60
60
|
}
|
|
61
|
+
// v2.6.4 T9 (E): picomatch (the files[] matcher, graph/files-glob.ts) expands a brace group only when
|
|
62
|
+
// it holds a top-level comma or a `..` range, which compiles to a character class — faithful only
|
|
63
|
+
// between single characters: `{A..z}` is `[A-z]`, but `{1..10}` is the class `[1-10]`. Any other
|
|
64
|
+
// unescaped group stays LITERAL braces: `docs/{a}.md` scopes a file named `docs/{a}.md`, never
|
|
65
|
+
// `docs/a.md` (2.6.3 T10: six scope reds). Like picomatch, a `[...]` class (opened only when a `]`
|
|
66
|
+
// follows; a `]` right after `[` or `[^` is a member) swallows braces and commas: `docs/{[a,b]}.md`
|
|
67
|
+
// stays literal, `docs/[{a}].md` holds no group. A POSIX `[:name:]` inside a class is replayed as
|
|
68
|
+
// picomatch's parser does it (the `:]` is consumed, the class stays open), so `docs/{[[:alpha:],]}.md`
|
|
69
|
+
// stays literal and `docs/[[:alpha:]{a}].md` holds no group. Outside a class, picomatch reads `"..."` as
|
|
70
|
+
// quoted literal text (an escape still applies inside it), so a quoted `[`, `{` or `,` opens nothing:
|
|
71
|
+
// `docs/"["/{a}].md` still holds the group `{a}`, `docs/"{a}".md` holds none. A class picomatch never
|
|
72
|
+
// closes (its only `]` is a member: `docs/[]/{a}.md`) has its opener escaped at the end, and everything
|
|
73
|
+
// after that opener stays literal text — no quotes, no classes, braces literal — so that tail is
|
|
74
|
+
// rescanned for groups with only escapes honoured: `docs/[]/{a}.md` holds `{a}`. Returns each literal
|
|
75
|
+
// group and the pattern with those braces dropped — the literal path its author almost certainly meant.
|
|
76
|
+
// A range is read with picomatch's escapes: its lexer drops a backslash before `.`, `/` or `;` (the char
|
|
77
|
+
// stays live, so `{1\..3}` IS the range `{1..3}`) and keeps any other `\X` as one literal character. Each
|
|
78
|
+
// end must be one such character, and the matcher itself must then match both ends: `{\d..z}` compiles
|
|
79
|
+
// to the class `[\d-z]`, which never matches "d".
|
|
80
|
+
const RANGE_END = String.raw `(\\[\s\S]|[^\\])`;
|
|
81
|
+
const RANGE = new RegExp(`^${RANGE_END}\\.\\.${RANGE_END}$`);
|
|
82
|
+
function supportedRange(group) {
|
|
83
|
+
const ends = RANGE.exec(group.replace(/\\([\s\S])/g, (pair, ch) => "./;".includes(ch) ? ch : pair));
|
|
84
|
+
if (!ends)
|
|
85
|
+
return false;
|
|
86
|
+
const match = filesGlob(`{${group}}`);
|
|
87
|
+
return ends.slice(1).every((end) => match(end.slice(-1)));
|
|
88
|
+
}
|
|
89
|
+
const POSIX_CLASS = picomatch.constants.POSIX_REGEX_SOURCE;
|
|
90
|
+
function singletonBraces(pattern) {
|
|
91
|
+
const open = [];
|
|
92
|
+
const groups = [];
|
|
93
|
+
const drop = new Set();
|
|
94
|
+
let cls; // the open character class's text as picomatch accumulates it, undefined outside one
|
|
95
|
+
let clsAt = -1; // the open class's opener
|
|
96
|
+
let quoted = false; // inside a "..." run outside any class
|
|
97
|
+
let raw = false; // past an unterminated class's opener: literal text, only escapes and braces count
|
|
98
|
+
for (let i = 0; i < pattern.length; i++) {
|
|
99
|
+
const ch = pattern[i];
|
|
100
|
+
if (cls !== undefined) {
|
|
101
|
+
const at = cls.lastIndexOf("[");
|
|
102
|
+
if (ch === "\\")
|
|
103
|
+
cls += pattern.slice(i, ++i + 1);
|
|
104
|
+
else if (ch === "]" && cls !== "[" && cls !== "[^")
|
|
105
|
+
cls = undefined;
|
|
106
|
+
else if (ch === ":" && at > 0 && cls.includes(":", 1) && Object.hasOwn(POSIX_CLASS, cls.slice(at + 2))) {
|
|
107
|
+
cls = cls.slice(0, at) + POSIX_CLASS[cls.slice(at + 2)];
|
|
108
|
+
i++; // picomatch consumes the char after the closing `:` (the `]` of `[:name:]`) without closing the class
|
|
109
|
+
}
|
|
110
|
+
else
|
|
111
|
+
cls += ch;
|
|
112
|
+
}
|
|
113
|
+
else if (ch === "\\")
|
|
114
|
+
i++; // escaped: `\{a\}` is a literal filename
|
|
115
|
+
else if (ch === '"' && !raw)
|
|
116
|
+
quoted = !quoted;
|
|
117
|
+
else if (quoted)
|
|
118
|
+
continue;
|
|
119
|
+
else if (ch === "[" && !raw && pattern.includes("]", i + 1))
|
|
120
|
+
[cls, clsAt] = ["[", i];
|
|
121
|
+
else if (ch === "{")
|
|
122
|
+
open.push({ at: i, comma: false });
|
|
123
|
+
else if (ch === "," && open.length)
|
|
124
|
+
open[open.length - 1].comma = true;
|
|
125
|
+
else if (ch === "}" && open.length) {
|
|
126
|
+
const { at, comma } = open.pop();
|
|
127
|
+
if (comma || supportedRange(pattern.slice(at + 1, i)))
|
|
128
|
+
continue;
|
|
129
|
+
groups.push(pattern.slice(at, i + 1));
|
|
130
|
+
drop.add(at).add(i);
|
|
131
|
+
}
|
|
132
|
+
if (cls !== undefined && i + 1 >= pattern.length)
|
|
133
|
+
[i, cls, raw] = [clsAt, undefined, true]; // unterminated: rescan its tail raw
|
|
134
|
+
}
|
|
135
|
+
return { groups, bare: pattern.split("").filter((_, i) => !drop.has(i)).join("") };
|
|
136
|
+
}
|
|
137
|
+
// v2.6.4 T9 (E): a files[] path the SELECTED git tree (spec base, else HEAD — what a worker's worktree
|
|
138
|
+
// is built from) stores as a symlink, mode 120000, scopes the link's own bytes: its stored target
|
|
139
|
+
// text. An edit made THROUGH the link writes the file it points to, which the scope gate refuses. The
|
|
140
|
+
// checkout is never the oracle — its links may be untracked or replaced. Advisory only; with no
|
|
141
|
+
// repository (or no files[]) there is no tree to inspect and git is never spawned.
|
|
142
|
+
function symlinkScopeWarnings(file, tasks, base) {
|
|
143
|
+
const scoped = tasks.filter((task) => task.files.length > 0)
|
|
144
|
+
.map((task) => ({ id: task.id, match: filesGlob(task.files.map((entry) => entry.replace(/^\.\//, ""))) }));
|
|
145
|
+
if (!scoped.length || !repositoryRoot(file))
|
|
146
|
+
return [];
|
|
147
|
+
const tree = base ?? "HEAD";
|
|
148
|
+
const git = (...args) => spawnSync("git", ["-C", dirname(file), ...args], { encoding: "utf8", maxBuffer: 1 << 28 });
|
|
149
|
+
const failed = (r) => r.error || r.signal || r.status !== 0 || typeof r.stdout !== "string";
|
|
150
|
+
const why = (r) => r.signal ? `signal ${r.signal}` : r.status === null ? "git did not run" : `git exit ${r.status}`;
|
|
151
|
+
const advisory = (what, r) => `tickmarkr: advisory: files[] symlink inspection could not read ${what} in the selected git tree ${JSON.stringify(tree)} (${why(r)}) — symlink scope is unverified for this compile.`;
|
|
152
|
+
const ls = git("ls-tree", "--full-tree", "-r", "-z", "--end-of-options", tree); // a base is never an option
|
|
153
|
+
if (failed(ls))
|
|
154
|
+
return [advisory("the tree", ls)];
|
|
155
|
+
const warnings = [];
|
|
156
|
+
for (const row of ls.stdout.split("\0")) {
|
|
157
|
+
const link = /^120000 blob ([0-9a-f]+)\t([\s\S]+)$/.exec(row);
|
|
158
|
+
if (!link)
|
|
159
|
+
continue;
|
|
160
|
+
const owners = scoped.filter((task) => task.match(link[2]));
|
|
161
|
+
if (!owners.length)
|
|
162
|
+
continue;
|
|
163
|
+
// ponytail: one cat-file per matched link; switch to `cat-file --batch` if a scope matches hundreds
|
|
164
|
+
const target = git("cat-file", "blob", link[1]);
|
|
165
|
+
if (failed(target)) {
|
|
166
|
+
warnings.push(advisory(`the stored target of ${JSON.stringify(link[2])}`, target));
|
|
167
|
+
continue;
|
|
168
|
+
}
|
|
169
|
+
for (const { id } of owners) {
|
|
170
|
+
warnings.push(`tickmarkr: task ${id} files[] path ${JSON.stringify(link[2])} is a symlink in the selected git tree ${JSON.stringify(tree)} storing target ${JSON.stringify(target.stdout)} — `
|
|
171
|
+
+ `files[] owns only that stored target text; an edit made through the link changes the file it points to, which needs its own files[] entry.`);
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
return warnings;
|
|
175
|
+
}
|
|
61
176
|
// OBS-97: mirror of the vitest.config.ts suite include — the only path class vitest collects.
|
|
62
177
|
export const COLLECTABLE_TESTS = "tests/**/*.test.ts";
|
|
63
178
|
export const LEGACY_PREFIX = ["dro", "vr"].join("");
|
|
@@ -611,6 +726,19 @@ export function compileNative(file, options = {}) {
|
|
|
611
726
|
// A fixture pin's path set is split only — never annotation-stripped: "fixtures/output (old)" is a
|
|
612
727
|
// filename, and csv() would silently retarget the declared obligation to "fixtures/output".
|
|
613
728
|
const pinPaths = (value) => splitTop(value).map((item) => item.trim()).filter(Boolean);
|
|
729
|
+
// v2.6.4 T9 (E): refused BEFORE the hosting probe below, which expands every brace group — so it
|
|
730
|
+
// would certify `tests/{native}.test.ts` as a test home while the scope gate never matches it.
|
|
731
|
+
for (const draft of drafts) {
|
|
732
|
+
for (const entry of csv(draft.fields.files)) {
|
|
733
|
+
const { groups, bare } = singletonBraces(entry);
|
|
734
|
+
if (groups.length) {
|
|
735
|
+
invalid(draft.id, "files", `entry ${JSON.stringify(entry)} holds single-item brace group${groups.length === 1 ? "" : "s"} ${groups.join(", ")}: `
|
|
736
|
+
+ `the scope gate matches ${groups.length === 1 ? "it" : "them"} as literal braces while test hosting expands ${groups.length === 1 ? "it" : "them"}, `
|
|
737
|
+
+ `so the declared scope disagrees with itself. Write the bare path ${JSON.stringify(bare)}, list two or more alternatives as {a,b}, `
|
|
738
|
+
+ `or escape the braces (\\{ \\}) if they are part of the filename (a range expands only between single characters, e.g. {1..3} or {a..c}).`);
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
}
|
|
614
742
|
// OBS-97: a typed test: oracle needs a collectable home. vitest only collects COLLECTABLE_TESTS
|
|
615
743
|
// paths, so a task whose non-empty files[] cannot host one makes scope-green and acceptance-green
|
|
616
744
|
// mutually exclusive by construction — run-20260719-210434 burned two dispatch attempts before a
|
|
@@ -744,6 +872,8 @@ export function compileNative(file, options = {}) {
|
|
|
744
872
|
},
|
|
745
873
|
tasks,
|
|
746
874
|
});
|
|
875
|
+
for (const warning of symlinkScopeWarnings(file, result.tasks, specBase))
|
|
876
|
+
console.warn(warning);
|
|
747
877
|
const authoringFindings = authoringLintFindings(result.tasks, file);
|
|
748
878
|
const blockingCodes = new Set(["criterion-scope", "fence-symbol-absent"]);
|
|
749
879
|
const blockingFindings = authoringFindings.filter((finding) => options.strict || blockingCodes.has(finding.code));
|
package/dist/config/config.d.ts
CHANGED
|
@@ -394,19 +394,23 @@ export type ModeResolution = {
|
|
|
394
394
|
export declare function loadConfigWithMode(repoRoot: string, opts?: {
|
|
395
395
|
globalDir?: string;
|
|
396
396
|
repoOverlayText?: string;
|
|
397
|
+
userOverlayText?: string;
|
|
397
398
|
}): {
|
|
398
399
|
cfg: TickmarkrConfig;
|
|
399
400
|
mode: ModeResolution;
|
|
400
401
|
};
|
|
401
|
-
/** OBS-1182: tiers.<adapter>.modelOverrides as the layers under the
|
|
402
|
+
/** OBS-1182: tiers.<adapter>.modelOverrides as the layers under the written overlay resolve them. */
|
|
402
403
|
export type LowerLayerModelOverrides = Record<string, Record<string, Record<string, unknown>>>;
|
|
403
|
-
/** OBS-1182: tiers.<adapter>.modelOverrides as the layers under the
|
|
404
|
-
*
|
|
405
|
-
*
|
|
404
|
+
/** OBS-1182: tiers.<adapter>.modelOverrides as the layers under the written overlay merge them —
|
|
405
|
+
* defaults + global under the repo overlay; B2 `below: "user"`: the defaults alone under the USER
|
|
406
|
+
* overlay, which is never read as its own lower layer. Read raw, never schema-validated: a lower
|
|
407
|
+
* layer may only validate beside upper fields (global `vendor: null` completed by a repo vendor)
|
|
408
|
+
* and its override metadata is still inherited.
|
|
406
409
|
* OBS-1188: an unparseable global file, or a non-map where this path expects a map, is an error —
|
|
407
410
|
* never an empty lower layer, which would silently drop the metadata a write must re-mask. */
|
|
408
411
|
export declare function lowerLayerModelOverrides(opts?: {
|
|
409
412
|
globalDir?: string;
|
|
413
|
+
below?: "repo" | "user";
|
|
410
414
|
}): {
|
|
411
415
|
ok: true;
|
|
412
416
|
overrides: LowerLayerModelOverrides;
|
|
@@ -417,11 +421,14 @@ export declare function lowerLayerModelOverrides(opts?: {
|
|
|
417
421
|
export declare function loadConfig(repoRoot: string, opts?: {
|
|
418
422
|
globalDir?: string;
|
|
419
423
|
}): TickmarkrConfig;
|
|
420
|
-
/** v1.52 T2 write-time reload guard: run candidate
|
|
421
|
-
* loader path every later command uses (parse → merge → schema → mode
|
|
422
|
-
* when the bytes load, else the loader's failure message — the caller
|
|
424
|
+
/** v1.52 T2 write-time reload guard: run candidate overlay bytes (the repo layer, or B2's USER layer)
|
|
425
|
+
* through the SAME production loader path every later command uses (parse → merge → schema → mode
|
|
426
|
+
* resolution). Returns null when the bytes load, else the loader's failure message — the caller
|
|
427
|
+
* must refuse the write. */
|
|
423
428
|
export declare function overlayBytesLoadError(repoRoot: string, bytes: string, opts?: {
|
|
424
429
|
globalDir?: string;
|
|
430
|
+
layer?: "repo" | "user";
|
|
431
|
+
repoOverlayText?: string;
|
|
425
432
|
}): string | null;
|
|
426
433
|
export type InitConfigOverlay = {
|
|
427
434
|
concurrency?: number;
|
|
@@ -474,6 +481,17 @@ export declare function fleetEditableFromConfig(cfg: TickmarkrConfig, universe?:
|
|
|
474
481
|
export declare function fleetKeyLayer(repoRoot: string, dotted: string, opts?: {
|
|
475
482
|
globalDir?: string;
|
|
476
483
|
}): "defaults" | "global" | "repo";
|
|
484
|
+
/** B2: the user-overlay keys a write changes that the repo overlay (merged above it) decides in this
|
|
485
|
+
* repository — it declares the path, or a non-map (value, list, null tombstone) at a prefix that
|
|
486
|
+
* replaces or deletes the user's subtree, or its merge otherwise moves the value (a repo pin/pool/
|
|
487
|
+
* prefer replaces the user's whole routing.map slot atomically) — each with the value that still
|
|
488
|
+
* applies here. Bytes the loader refuses yield none: the write guard refuses them anyway. */
|
|
489
|
+
export declare function repoShadowedUserKeys(repoRoot: string, beforeUser: string, afterUser: string, opts?: {
|
|
490
|
+
globalDir?: string;
|
|
491
|
+
}): Array<{
|
|
492
|
+
path: string;
|
|
493
|
+
value: unknown;
|
|
494
|
+
}>;
|
|
477
495
|
/** Non-interactive fleet state for CI drift checks (`tickmarkr fleet --print`). */
|
|
478
496
|
export declare function formatFleetPrint(repoRoot: string, opts?: {
|
|
479
497
|
globalDir?: string;
|
package/dist/config/config.js
CHANGED
|
@@ -545,10 +545,27 @@ export const DEFAULT_CONFIG = {
|
|
|
545
545
|
// retired id. Installed codex 0.144.1 lists it (models_cache.json client_version 0.144.0, fetched
|
|
546
546
|
// 2026-07-10) AND a live runtime probe (`codex exec --model gpt-5.6-sol` → "OK", 2026-07-10) confirmed
|
|
547
547
|
// it runs. Reseeding away would be a regression — the CLI upgrade closed the gate. Seed stays.
|
|
548
|
+
// v2.6.4 T3 (B1b, D-717): gpt-6-astra / gpt-6-sol / gpt-6-luna seeded AHEAD of the retained gpt-5.x
|
|
549
|
+
// ids from the installed CLI's models_cache.json snapshot (codex 0.159.0, fetched 2026-09-29T16:09Z).
|
|
550
|
+
// Tiers are ASSUMPTIONS, not measurements — no benchmark was run or cited for any of the three:
|
|
551
|
+
// astra=frontier and luna=cheap follow the provider's own descriptions in the local cache (read
|
|
552
|
+
// 2026-09-29: "Frontier intelligence for the most demanding work" / "Fast and affordable model for
|
|
553
|
+
// easier tasks"); sol=frontier keeps the shipped workhorse-family mapping (gpt-5.6-sol above) by
|
|
554
|
+
// continuity. Learned routing or an overlay may retier on evidence. gpt-5.5 stays seeded until its
|
|
555
|
+
// CLI-announced retirement (2026-10-14T19:00:00Z → gpt-5.6-sol); gpt-5.6-sol is not retiring.
|
|
556
|
+
// Windows: the same snapshot lists context_window=272000 for all seven — the budget the CLI launches
|
|
557
|
+
// with, and the authority for these seeds. The API's published 1,050,000 is a different surface and is
|
|
558
|
+
// not contradicted. A later cache listing more ids (gpt-6.1-sol) does not widen this sealed set.
|
|
548
559
|
codex: {
|
|
549
560
|
vendor: "openai", channel: "sub",
|
|
550
|
-
models: {
|
|
551
|
-
|
|
561
|
+
models: {
|
|
562
|
+
"gpt-6-astra": "frontier", "gpt-6-sol": "frontier", "gpt-6-luna": "cheap",
|
|
563
|
+
"gpt-5.6-sol": "frontier", "gpt-5.5": "frontier", "gpt-5.6-terra": "mid", "gpt-5.6-luna": "cheap",
|
|
564
|
+
},
|
|
565
|
+
windows: {
|
|
566
|
+
"gpt-6-astra": 272_000, "gpt-6-sol": 272_000, "gpt-6-luna": 272_000,
|
|
567
|
+
"gpt-5.6-sol": 272_000, "gpt-5.5": 272_000, "gpt-5.6-terra": 272_000, "gpt-5.6-luna": 272_000,
|
|
568
|
+
},
|
|
552
569
|
},
|
|
553
570
|
// grok-4.5 (xAI, released 2026-07-08) → mid: AA Intelligence 54 (#4), Terminal-Bench 2.1 83.3%
|
|
554
571
|
// (≈ GPT-5.5 83.4, Fable 5 84.3), SWE-bench Pro 64.7%, ~4.2× more token-efficient than Opus 4.8
|
|
@@ -809,7 +826,11 @@ function resolveRoutingMode(cfg, layers) {
|
|
|
809
826
|
/** loadConfig plus the mode-resolution record (floor provenance + plan lints). The resolved floors are
|
|
810
827
|
* already applied to cfg.routing.floors — route() consumes floors only and never sees the mode. */
|
|
811
828
|
export function loadConfigWithMode(repoRoot, opts = {}) {
|
|
812
|
-
|
|
829
|
+
// B2: userOverlayText substitutes candidate bytes for the on-disk USER layer — the fleet's save
|
|
830
|
+
// destination — so a preview and the write guard load exactly the bytes a save would land.
|
|
831
|
+
const globalCfg = opts.userOverlayText === undefined
|
|
832
|
+
? readYaml(join(opts.globalDir ?? globalConfigDir(), "config.yaml"))
|
|
833
|
+
: parse(opts.userOverlayText);
|
|
813
834
|
// v1.52 T2: repoOverlayText substitutes candidate bytes for the on-disk repo layer — the fleet
|
|
814
835
|
// write guard validates EXACTLY what it is about to write through this one loader path.
|
|
815
836
|
const repoCfg = opts.repoOverlayText === undefined
|
|
@@ -824,9 +845,11 @@ export function loadConfigWithMode(repoRoot, opts = {}) {
|
|
|
824
845
|
assertSeededModelWindows();
|
|
825
846
|
return { cfg: r.data, mode: resolveRoutingMode(r.data, [globalCfg, repoCfg]) };
|
|
826
847
|
}
|
|
827
|
-
/** OBS-1182: tiers.<adapter>.modelOverrides as the layers under the
|
|
828
|
-
*
|
|
829
|
-
*
|
|
848
|
+
/** OBS-1182: tiers.<adapter>.modelOverrides as the layers under the written overlay merge them —
|
|
849
|
+
* defaults + global under the repo overlay; B2 `below: "user"`: the defaults alone under the USER
|
|
850
|
+
* overlay, which is never read as its own lower layer. Read raw, never schema-validated: a lower
|
|
851
|
+
* layer may only validate beside upper fields (global `vendor: null` completed by a repo vendor)
|
|
852
|
+
* and its override metadata is still inherited.
|
|
830
853
|
* OBS-1188: an unparseable global file, or a non-map where this path expects a map, is an error —
|
|
831
854
|
* never an empty lower layer, which would silently drop the metadata a write must re-mask. */
|
|
832
855
|
export function lowerLayerModelOverrides(opts = {}) {
|
|
@@ -834,7 +857,7 @@ export function lowerLayerModelOverrides(opts = {}) {
|
|
|
834
857
|
const path = join(opts.globalDir ?? globalConfigDir(), "config.yaml");
|
|
835
858
|
let global;
|
|
836
859
|
try {
|
|
837
|
-
global = readYaml(path);
|
|
860
|
+
global = opts.below === "user" ? undefined : readYaml(path);
|
|
838
861
|
}
|
|
839
862
|
catch (error) {
|
|
840
863
|
return { ok: false, error: `lower config layer ${path} is malformed YAML: ${error.message.replace(/\s+/g, " ").trim()}` };
|
|
@@ -865,12 +888,17 @@ export function lowerLayerModelOverrides(opts = {}) {
|
|
|
865
888
|
export function loadConfig(repoRoot, opts = {}) {
|
|
866
889
|
return loadConfigWithMode(repoRoot, opts).cfg;
|
|
867
890
|
}
|
|
868
|
-
/** v1.52 T2 write-time reload guard: run candidate
|
|
869
|
-
* loader path every later command uses (parse → merge → schema → mode
|
|
870
|
-
* when the bytes load, else the loader's failure message — the caller
|
|
891
|
+
/** v1.52 T2 write-time reload guard: run candidate overlay bytes (the repo layer, or B2's USER layer)
|
|
892
|
+
* through the SAME production loader path every later command uses (parse → merge → schema → mode
|
|
893
|
+
* resolution). Returns null when the bytes load, else the loader's failure message — the caller
|
|
894
|
+
* must refuse the write. */
|
|
871
895
|
export function overlayBytesLoadError(repoRoot, bytes, opts = {}) {
|
|
872
896
|
try {
|
|
873
|
-
loadConfigWithMode(repoRoot, {
|
|
897
|
+
loadConfigWithMode(repoRoot, {
|
|
898
|
+
globalDir: opts.globalDir,
|
|
899
|
+
// B2: a user-layer check may substitute the repo layer too ("" = none: the bytes on their own)
|
|
900
|
+
...(opts.layer === "user" ? { userOverlayText: bytes, repoOverlayText: opts.repoOverlayText } : { repoOverlayText: bytes }),
|
|
901
|
+
});
|
|
874
902
|
return null;
|
|
875
903
|
}
|
|
876
904
|
catch (e) {
|
|
@@ -1085,6 +1113,59 @@ export function fleetKeyLayer(repoRoot, dotted, opts = {}) {
|
|
|
1085
1113
|
return "global";
|
|
1086
1114
|
return "defaults";
|
|
1087
1115
|
}
|
|
1116
|
+
const isPlainMap = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
1117
|
+
/** B2: every leaf path an overlay write changes — a map on either side (the other absent) is
|
|
1118
|
+
* walked, anything else (scalar, list, null tombstone) is one leaf. */
|
|
1119
|
+
function changedLeafPaths(before, after, path = []) {
|
|
1120
|
+
if (JSON.stringify(before) === JSON.stringify(after))
|
|
1121
|
+
return [];
|
|
1122
|
+
const walkable = (v) => v === undefined || isPlainMap(v);
|
|
1123
|
+
if (!walkable(before) || !walkable(after))
|
|
1124
|
+
return [path];
|
|
1125
|
+
const b = (before ?? {});
|
|
1126
|
+
const a = (after ?? {});
|
|
1127
|
+
return [...new Set([...Object.keys(b), ...Object.keys(a)])].flatMap((key) => changedLeafPaths(b[key], a[key], [...path, key]));
|
|
1128
|
+
}
|
|
1129
|
+
/** B2: the user-overlay keys a write changes that the repo overlay (merged above it) decides in this
|
|
1130
|
+
* repository — it declares the path, or a non-map (value, list, null tombstone) at a prefix that
|
|
1131
|
+
* replaces or deletes the user's subtree, or its merge otherwise moves the value (a repo pin/pool/
|
|
1132
|
+
* prefer replaces the user's whole routing.map slot atomically) — each with the value that still
|
|
1133
|
+
* applies here. Bytes the loader refuses yield none: the write guard refuses them anyway. */
|
|
1134
|
+
export function repoShadowedUserKeys(repoRoot, beforeUser, afterUser, opts = {}) {
|
|
1135
|
+
let effective;
|
|
1136
|
+
let paths;
|
|
1137
|
+
try {
|
|
1138
|
+
effective = loadConfigWithMode(repoRoot, { globalDir: opts.globalDir, userOverlayText: afterUser }).cfg;
|
|
1139
|
+
paths = changedLeafPaths(parse(beforeUser) ?? undefined, parse(afterUser) ?? undefined);
|
|
1140
|
+
}
|
|
1141
|
+
catch {
|
|
1142
|
+
return [];
|
|
1143
|
+
}
|
|
1144
|
+
// the same bytes without the repo layer: any changed path whose value differs here is the repo's merge
|
|
1145
|
+
let alone;
|
|
1146
|
+
try {
|
|
1147
|
+
alone = loadConfigWithMode(repoRoot, { globalDir: opts.globalDir, userOverlayText: afterUser, repoOverlayText: "" }).cfg;
|
|
1148
|
+
}
|
|
1149
|
+
catch {
|
|
1150
|
+
alone = undefined;
|
|
1151
|
+
}
|
|
1152
|
+
const at = (cfg, path) => path.reduce((cur, key) => (isPlainMap(cur) ? cur[key] : undefined), cfg);
|
|
1153
|
+
const repo = readOverlayFile(repoOverlayPath(repoRoot));
|
|
1154
|
+
const decides = (path) => {
|
|
1155
|
+
if (alone !== undefined && JSON.stringify(at(alone, path)) !== JSON.stringify(at(effective, path)))
|
|
1156
|
+
return true;
|
|
1157
|
+
let cur = repo;
|
|
1158
|
+
for (const key of path) {
|
|
1159
|
+
if (!isPlainMap(cur))
|
|
1160
|
+
return cur !== repo;
|
|
1161
|
+
if (!(key in cur))
|
|
1162
|
+
return false;
|
|
1163
|
+
cur = cur[key];
|
|
1164
|
+
}
|
|
1165
|
+
return true;
|
|
1166
|
+
};
|
|
1167
|
+
return paths.filter(decides).map((path) => ({ path: path.join("."), value: at(effective, path) }));
|
|
1168
|
+
}
|
|
1088
1169
|
/** Non-interactive fleet state for CI drift checks (`tickmarkr fleet --print`). */
|
|
1089
1170
|
export function formatFleetPrint(repoRoot, opts = {}) {
|
|
1090
1171
|
const gdir = opts.globalDir ?? globalConfigDir();
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type FleetEditable, type FleetUniverseRow, type LowerLayerModelOverrides, type RoutingMode } from "./config.js";
|
|
1
|
+
import { type FleetEditable, type FleetUniverseRow, type LowerLayerModelOverrides, type MapEntry, type 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
4
|
type FleetOverlayWriteFields = {
|
|
@@ -20,6 +20,7 @@ type FleetOverlayWriteFields = {
|
|
|
20
20
|
};
|
|
21
21
|
};
|
|
22
22
|
universe?: FleetUniverseRow[];
|
|
23
|
+
lowerMap?: Record<string, MapEntry>;
|
|
23
24
|
};
|
|
24
25
|
type EffortFree = FleetEditable & {
|
|
25
26
|
efforts?: undefined;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// Fleet-overlay mutation, serialization, and diff rendering for the `tickmarkr fleet` write path.
|
|
2
|
-
import { isMap, isScalar, isSeq, parseDocument, stringify, visit } from "yaml";
|
|
2
|
+
import { isAlias, isMap, isNode, isScalar, isSeq, parseDocument, stringify, visit } from "yaml";
|
|
3
3
|
import { DENY_SCOPES, universeCovers, universeEntryMatches } from "./config.js";
|
|
4
4
|
// OBS-1099 add.1: the writer ranges over the schema-derived scopes. The flat (all-seats) scopes
|
|
5
5
|
// take part in the membership/allow-form write; every nested scope (routing.deny.workers.*) is a
|
|
@@ -161,7 +161,71 @@ function setStringSequencePreservingComments(doc, path, values) {
|
|
|
161
161
|
// Total forms: a delete treats the masked subtree as absent (the operator's tombstone already
|
|
162
162
|
// denies it, and survives untouched); a set clears the scalar and rebuilds what fleet owns —
|
|
163
163
|
// the routing.allow precedent below.
|
|
164
|
+
// T8 repair: an alias is no tombstone — `spec: *empty` resolves to a mapping. Each alias on the path
|
|
165
|
+
// is replaced by an un-anchored copy of its target, so what it resolves to decides, and a write under
|
|
166
|
+
// it never edits the anchored node its other aliases share.
|
|
167
|
+
function materializeAliases(doc, path) {
|
|
168
|
+
for (let depth = 1; depth < path.length; depth++) {
|
|
169
|
+
const node = doc.getIn(path.slice(0, depth), true);
|
|
170
|
+
if (!isAlias(node))
|
|
171
|
+
continue;
|
|
172
|
+
const target = node.resolve(doc);
|
|
173
|
+
if (target === undefined)
|
|
174
|
+
return;
|
|
175
|
+
doc.setIn(path.slice(0, depth), detachedCopy(doc, node, target, new Set()));
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
const nodesOf = (root) => {
|
|
179
|
+
const out = [];
|
|
180
|
+
visit(root, (_key, node) => {
|
|
181
|
+
if (isNode(node))
|
|
182
|
+
out.push(node);
|
|
183
|
+
});
|
|
184
|
+
return out;
|
|
185
|
+
};
|
|
186
|
+
// D-821: a copy carries NO anchor (a nested `&empty` would rebind `plan: *empty` and every later
|
|
187
|
+
// alias to the edited copy), and each inner alias must still name what it named in the original —
|
|
188
|
+
// one whose anchor is redefined before `at` is replaced by a detached copy of its original target.
|
|
189
|
+
function detachedCopy(doc, at, target, expanding) {
|
|
190
|
+
if (expanding.has(target))
|
|
191
|
+
throw new Error(`fleet cannot detach the recursive YAML alias *${at.source}`);
|
|
192
|
+
const visible = new Map();
|
|
193
|
+
visit(doc, (_key, node) => {
|
|
194
|
+
if (node === at)
|
|
195
|
+
return visit.BREAK;
|
|
196
|
+
if (isNode(node) && !isAlias(node) && node.anchor)
|
|
197
|
+
visible.set(node.anchor, node);
|
|
198
|
+
});
|
|
199
|
+
const copy = target.clone();
|
|
200
|
+
const originals = nodesOf(target);
|
|
201
|
+
const replace = new Map();
|
|
202
|
+
nodesOf(copy).forEach((node, index) => {
|
|
203
|
+
if (!isAlias(node)) {
|
|
204
|
+
node.anchor = undefined;
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
const resolved = originals[index].resolve(doc);
|
|
208
|
+
if (resolved !== undefined && visible.get(node.source) !== resolved) {
|
|
209
|
+
replace.set(node, detachedCopy(doc, at, resolved, new Set([...expanding, target])));
|
|
210
|
+
}
|
|
211
|
+
});
|
|
212
|
+
if (!replace.size)
|
|
213
|
+
return copy;
|
|
214
|
+
visit(copy, (_key, node) => (isNode(node) ? replace.get(node) : undefined));
|
|
215
|
+
return copy;
|
|
216
|
+
}
|
|
217
|
+
function underScalar(doc, path) {
|
|
218
|
+
for (let depth = 1; depth < path.length; depth++) {
|
|
219
|
+
const node = doc.getIn(path.slice(0, depth), true);
|
|
220
|
+
if (node === undefined)
|
|
221
|
+
return false;
|
|
222
|
+
if (!isMap(node))
|
|
223
|
+
return true;
|
|
224
|
+
}
|
|
225
|
+
return false;
|
|
226
|
+
}
|
|
164
227
|
function deleteAt(doc, path) {
|
|
228
|
+
materializeAliases(doc, path); // an aliased mapping is no tombstone: Auto under `spec: *pinned` deletes the pin
|
|
165
229
|
for (let depth = 1; depth < path.length; depth++) {
|
|
166
230
|
if (!isMap(doc.getIn(path.slice(0, depth), true)))
|
|
167
231
|
return;
|
|
@@ -171,6 +235,7 @@ function deleteAt(doc, path) {
|
|
|
171
235
|
doc.deleteIn(path);
|
|
172
236
|
}
|
|
173
237
|
function setAt(doc, path, value) {
|
|
238
|
+
materializeAliases(doc, path); // edit a copy of an aliased mapping, never drop what it resolved to
|
|
174
239
|
for (let depth = 1; depth < path.length; depth++) {
|
|
175
240
|
const node = doc.getIn(path.slice(0, depth), true);
|
|
176
241
|
if (node === undefined)
|
|
@@ -281,11 +346,19 @@ export function renderFleetOverlayWrite(priorBytes, write) {
|
|
|
281
346
|
for (const shape of new Set([...Object.keys(initial.map), ...Object.keys(edited.map)])) {
|
|
282
347
|
const before = initial.map[shape];
|
|
283
348
|
const after = edited.map[shape];
|
|
284
|
-
|
|
349
|
+
const pinPath = ["routing", "map", shape, "pin"];
|
|
350
|
+
const declares = after?.pin !== undefined || after?.pool !== undefined || after?.prefer !== undefined;
|
|
351
|
+
if (!declares && write.lowerMap?.[shape]?.pin !== undefined && JSON.stringify(before) !== JSON.stringify(after)) {
|
|
352
|
+
// pin: null tombstone — masks the lower-layer pin; a scalar tombstone above it already masks the entry
|
|
353
|
+
materializeAliases(doc, pinPath);
|
|
354
|
+
if (!underScalar(doc, pinPath))
|
|
355
|
+
setScalarPreservingComment(doc, pinPath, null);
|
|
356
|
+
}
|
|
357
|
+
else if (JSON.stringify(before?.pin) !== JSON.stringify(after?.pin)) {
|
|
285
358
|
if (after?.pin === undefined)
|
|
286
|
-
deleteAt(doc,
|
|
359
|
+
deleteAt(doc, pinPath);
|
|
287
360
|
else
|
|
288
|
-
setAt(doc,
|
|
361
|
+
setAt(doc, pinPath, doc.createNode(after.pin));
|
|
289
362
|
}
|
|
290
363
|
if (JSON.stringify(before?.pool) !== JSON.stringify(after?.pool)) {
|
|
291
364
|
const path = ["routing", "map", shape, "pool"];
|
package/dist/drivers/orca.d.ts
CHANGED
|
@@ -31,16 +31,18 @@ export declare class OrcaError extends Error {
|
|
|
31
31
|
readonly raw: string;
|
|
32
32
|
readonly code?: string;
|
|
33
33
|
readonly runtimeId?: string;
|
|
34
|
+
readonly launchCause?: SeatLaunchCause;
|
|
34
35
|
constructor(family: string, reason: string, raw: string, opts?: {
|
|
35
36
|
code?: string;
|
|
36
37
|
runtimeId?: string;
|
|
38
|
+
launchCause?: SeatLaunchCause;
|
|
37
39
|
});
|
|
38
40
|
}
|
|
39
41
|
/** The slot cannot be addressed: dead/unknown terminal record, or a handle that cannot be recovered
|
|
40
42
|
* to exactly one owned terminal in the slot's own worktree. Never a silent false or empty string. */
|
|
41
43
|
export declare class OrcaUnavailableError extends OrcaError {
|
|
42
44
|
readonly terminalStatus?: string | undefined;
|
|
43
|
-
constructor(family: string, reason: string, raw: string, terminalStatus?: string | undefined);
|
|
45
|
+
constructor(family: string, reason: string, raw: string, terminalStatus?: string | undefined, launchCause?: SeatLaunchCause);
|
|
44
46
|
}
|
|
45
47
|
export interface OrcaEnvelope {
|
|
46
48
|
result: Record<string, unknown>;
|
|
@@ -83,6 +85,10 @@ export declare function terminalWorktree(term: Record<string, unknown>): string
|
|
|
83
85
|
export declare function canonicalWorktreePath(path: string): string;
|
|
84
86
|
/** The proof line a worker terminal prints first: create, recovery and focus read it back. */
|
|
85
87
|
export declare const CHECKOUT_MARK = "TICKMARKR_CHECKOUT";
|
|
88
|
+
/** E1: the one typed launch cause a driver attaches to its launch failure. `checkout-proof-timeout`
|
|
89
|
+
* means the terminal printed NO proof frame by the ceiling — a slow host, not a misplaced seat. A
|
|
90
|
+
* foreign or malformed frame is placement evidence and carries no cause (never a timeout exemption). */
|
|
91
|
+
export type SeatLaunchCause = "checkout-proof-timeout";
|
|
86
92
|
/** The exact bytes the create command prints as its first line. */
|
|
87
93
|
export declare function checkoutProofLine(checkout: string): string;
|
|
88
94
|
/** Every complete frame in a scrollback, decoded and canonicalized; whether an incomplete one was seen. */
|
|
@@ -250,6 +256,18 @@ export declare class OrcaDriver implements ExecutorDriver {
|
|
|
250
256
|
* that supplied the ownership listing — else another terminal's or another runtime's bytes were
|
|
251
257
|
* answered and nothing is proven. Proven means provesCheckout: exact canonical full-path equality
|
|
252
258
|
* of a complete frame, no other checkout named, no incomplete frame.
|
|
259
|
+
* E1: with a `deadline` (create's window) there is ONE absolute deadline: a read is issued only while
|
|
260
|
+
* budget is left and is handed exactly that budget, and a page that still lands past it is late —
|
|
261
|
+
* nothing late proves the checkout. `seen` is what the scrollback showed, for the typed timeout:
|
|
262
|
+
* `misplaced` — a frame naming another checkout, a cut frame or an unparseable mark in anything read
|
|
263
|
+
* (the anchor's tail and every page included, even when paging stopped or a later read threw);
|
|
264
|
+
* `absent` — ONLY when every page down to the start of the scrollback was read before the ceiling
|
|
265
|
+
* and none carries a trace of the mark; `unread` — every other exit: PROOF_PAGES reached, a limited
|
|
266
|
+
* read with no (or the same) next cursor or a truncated anchor without one, a read that threw, a read
|
|
267
|
+
* the ceiling cut off or never let start (the anchor included), or a page that landed late (even one
|
|
268
|
+
* carrying the right proof), a malformed page (its tail, cursors or paging flags), or a blind stream
|
|
269
|
+
* page (isBlindStreamPage). Unread output is unknown — never an empty page, never absence.
|
|
270
|
+
* Without a deadline (reconcile, recovery) only `proven` is consumed.
|
|
253
271
|
*/
|
|
254
272
|
private checkoutProven;
|
|
255
273
|
private recover;
|