tickmarkr 2.6.2 → 2.6.4
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 +12 -2
- package/dist/adapters/codex.d.ts +2 -1
- package/dist/adapters/codex.js +26 -5
- package/dist/adapters/model-lints.d.ts +1 -0
- package/dist/adapters/model-lints.js +48 -1
- package/dist/adapters/model-windows.d.ts +2 -1
- package/dist/adapters/model-windows.js +18 -7
- package/dist/adapters/types.d.ts +6 -0
- package/dist/cli/commands/approve.d.ts +6 -3
- package/dist/cli/commands/approve.js +43 -6
- package/dist/cli/commands/doctor.d.ts +8 -2
- package/dist/cli/commands/doctor.js +6 -1
- package/dist/cli/commands/fleet.js +264 -59
- package/dist/cli/commands/init.js +5 -3
- package/dist/cli/commands/plan.js +13 -8
- package/dist/cli/commands/report.d.ts +40 -2
- package/dist/cli/commands/report.js +278 -11
- 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 +49 -4
- package/dist/cli/commands/verify.js +332 -121
- package/dist/compile/native.js +137 -0
- package/dist/config/config.d.ts +40 -9
- package/dist/config/config.js +133 -15
- package/dist/config/fleet-overlay.d.ts +13 -3
- package/dist/config/fleet-overlay.js +12 -8
- package/dist/drivers/herdr.d.ts +12 -0
- package/dist/drivers/herdr.js +51 -0
- package/dist/drivers/orca.d.ts +9 -1
- package/dist/drivers/orca.js +29 -7
- package/dist/drivers/types.d.ts +2 -0
- package/dist/drivers/types.js +2 -2
- package/dist/gates/acceptance.d.ts +7 -0
- package/dist/gates/acceptance.js +27 -5
- package/dist/gates/baseline.d.ts +20 -1
- package/dist/gates/baseline.js +100 -20
- package/dist/gates/cache.d.ts +8 -0
- package/dist/gates/cache.js +12 -2
- package/dist/gates/llm.d.ts +6 -0
- package/dist/gates/llm.js +27 -8
- package/dist/gates/review.d.ts +6 -1
- package/dist/gates/review.js +122 -32
- package/dist/gates/run-gates.d.ts +54 -3
- package/dist/gates/run-gates.js +331 -45
- package/dist/gates/test-manifest.d.ts +42 -0
- package/dist/gates/test-manifest.js +69 -10
- package/dist/route/router.d.ts +23 -1
- package/dist/route/router.js +54 -16
- package/dist/run/consult.d.ts +3 -1
- package/dist/run/consult.js +4 -2
- package/dist/run/daemon.d.ts +2 -1
- package/dist/run/daemon.js +349 -79
- package/dist/run/interactive-seed.d.ts +4 -0
- package/dist/run/interactive-seed.js +35 -9
- package/dist/run/journal.d.ts +123 -1
- package/dist/run/journal.js +480 -17
- package/dist/run/lease.d.ts +13 -0
- package/dist/run/lease.js +45 -0
- package/dist/run/protocol.d.ts +15 -0
- package/dist/run/protocol.js +11 -1
- package/dist/run/receipt-resolver.d.ts +22 -0
- package/dist/run/receipt-resolver.js +40 -1
- package/dist/run/repair-selection.d.ts +11 -1
- package/dist/run/repair-selection.js +17 -9
- package/dist/run/wall-budget.d.ts +48 -0
- package/dist/run/wall-budget.js +280 -0
- package/dist/tui/cockpit/live-store.d.ts +6 -0
- package/dist/tui/cockpit/live-store.js +36 -11
- package/dist/tui/cockpit/run-cockpit.js +2 -2
- package/dist/tui/cockpit/run-view.d.ts +2 -1
- package/dist/tui/cockpit/run-view.js +13 -9
- package/dist/tui/cockpit/setup-cockpit.d.ts +2 -0
- package/dist/tui/cockpit/setup-cockpit.js +4 -0
- package/package.json +2 -1
- package/schema/config.schema.json +8 -1
- package/skills/tickmarkr-auto/SKILL.md +2 -2
- package/skills/tickmarkr-loop/SKILL.md +17 -5
- package/skills/tickmarkr-overseer/scripts/classify-vitest-log.sh +4 -1
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));
|
|
@@ -911,6 +1041,13 @@ acceptance is required on every task (a nested list of observable outcomes).
|
|
|
911
1041
|
- Enumerating one axis exhaustively is what hides the others. A spec that guards PARTIAL coverage
|
|
912
1042
|
site-by-site, member-by-member, can be defeated wholesale by CONDITIONAL coverage, which leaves
|
|
913
1043
|
every enumeration satisfied. After you enumerate, ask what a single flag would do to the whole set.
|
|
1044
|
+
- A GOAL STATING AN INVARIANT OVER "every", "never" OR "any" CARRIES ITS CLOSED CASE TABLE AS A
|
|
1045
|
+
CRITERION: the in-scope consumers, bridges, operations and event sequences it ranges over, each a
|
|
1046
|
+
row a test exercises. An unenumerated invariant is repaired one edge per review round while every
|
|
1047
|
+
round re-judges the accumulated diff (OBS-1019 add.2). The table binds author and reviewer, never a
|
|
1048
|
+
worker's claim: each review material binds its declared class to the goal clause or criterion it
|
|
1049
|
+
violates through input-to-consequence evidence marked executed, static or blocked. A worker's own
|
|
1050
|
+
table alone never silences a real finding, and blocked evidence is never a pass.
|
|
914
1051
|
- EVERY CRITERION NAMES THE PAIR IT DISCRIMINATES: the correct case that MUST PASS, and the
|
|
915
1052
|
neighbouring plausible-wrong or false-clean case that MUST FAIL. Two easy examples that both pass are
|
|
916
1053
|
not discrimination — they are two ways of being green, and the wrong half is the whole point: it is
|
package/dist/config/config.d.ts
CHANGED
|
@@ -181,13 +181,13 @@ export declare function effectiveReviewPolicy(files: ReadonlyArray<string>, revi
|
|
|
181
181
|
export declare function criticalPathHits(files: ReadonlyArray<string>, criticalPaths?: ReadonlyArray<string>): string[];
|
|
182
182
|
export declare const ExecutionPolicySchema: z.ZodObject<{
|
|
183
183
|
boundedInfrastructure: z.ZodDefault<z.ZodBoolean>;
|
|
184
|
-
repairSelection: z.
|
|
184
|
+
repairSelection: z.ZodOptional<z.ZodBoolean>;
|
|
185
185
|
taskExecutionLimitMs: z.ZodNumber;
|
|
186
186
|
}, z.core.$strict>;
|
|
187
187
|
export declare const TickmarkrConfigSchema: z.ZodObject<{
|
|
188
188
|
executionPolicy: z.ZodOptional<z.ZodObject<{
|
|
189
189
|
boundedInfrastructure: z.ZodDefault<z.ZodBoolean>;
|
|
190
|
-
repairSelection: z.
|
|
190
|
+
repairSelection: z.ZodOptional<z.ZodBoolean>;
|
|
191
191
|
taskExecutionLimitMs: z.ZodNumber;
|
|
192
192
|
}, z.core.$strict>>;
|
|
193
193
|
concurrency: z.ZodNumber;
|
|
@@ -309,6 +309,8 @@ export declare const TickmarkrConfigSchema: z.ZodObject<{
|
|
|
309
309
|
tipTest: z.ZodOptional<z.ZodString>;
|
|
310
310
|
lint: z.ZodOptional<z.ZodString>;
|
|
311
311
|
diffCap: z.ZodOptional<z.ZodNumber>;
|
|
312
|
+
evidenceQuotaBytes: z.ZodOptional<z.ZodNumber>;
|
|
313
|
+
repairSelection: z.ZodOptional<z.ZodBoolean>;
|
|
312
314
|
byShape: z.ZodOptional<z.ZodOptional<z.ZodRecord<z.ZodEnum<{
|
|
313
315
|
plan: "plan";
|
|
314
316
|
spec: "spec";
|
|
@@ -374,6 +376,7 @@ export type TickmarkrConfig = z.infer<typeof TickmarkrConfigSchema>;
|
|
|
374
376
|
export declare class ConfigError extends Error {
|
|
375
377
|
constructor(message: string);
|
|
376
378
|
}
|
|
379
|
+
export declare const DEFAULT_EVIDENCE_QUOTA_BYTES: number;
|
|
377
380
|
export declare const DEFAULT_CONFIG: TickmarkrConfig;
|
|
378
381
|
export declare function globalConfigDir(): string;
|
|
379
382
|
export declare function overlayPreferShapes(repoRoot: string, opts?: {
|
|
@@ -391,24 +394,41 @@ export type ModeResolution = {
|
|
|
391
394
|
export declare function loadConfigWithMode(repoRoot: string, opts?: {
|
|
392
395
|
globalDir?: string;
|
|
393
396
|
repoOverlayText?: string;
|
|
397
|
+
userOverlayText?: string;
|
|
394
398
|
}): {
|
|
395
399
|
cfg: TickmarkrConfig;
|
|
396
400
|
mode: ModeResolution;
|
|
397
401
|
};
|
|
398
|
-
/** OBS-1182: tiers.<adapter>.modelOverrides as the layers under the
|
|
399
|
-
|
|
400
|
-
|
|
402
|
+
/** OBS-1182: tiers.<adapter>.modelOverrides as the layers under the written overlay resolve them. */
|
|
403
|
+
export type LowerLayerModelOverrides = Record<string, Record<string, Record<string, unknown>>>;
|
|
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.
|
|
409
|
+
* OBS-1188: an unparseable global file, or a non-map where this path expects a map, is an error —
|
|
410
|
+
* never an empty lower layer, which would silently drop the metadata a write must re-mask. */
|
|
401
411
|
export declare function lowerLayerModelOverrides(opts?: {
|
|
402
412
|
globalDir?: string;
|
|
403
|
-
|
|
413
|
+
below?: "repo" | "user";
|
|
414
|
+
}): {
|
|
415
|
+
ok: true;
|
|
416
|
+
overrides: LowerLayerModelOverrides;
|
|
417
|
+
} | {
|
|
418
|
+
ok: false;
|
|
419
|
+
error: string;
|
|
420
|
+
};
|
|
404
421
|
export declare function loadConfig(repoRoot: string, opts?: {
|
|
405
422
|
globalDir?: string;
|
|
406
423
|
}): TickmarkrConfig;
|
|
407
|
-
/** v1.52 T2 write-time reload guard: run candidate
|
|
408
|
-
* loader path every later command uses (parse → merge → schema → mode
|
|
409
|
-
* 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. */
|
|
410
428
|
export declare function overlayBytesLoadError(repoRoot: string, bytes: string, opts?: {
|
|
411
429
|
globalDir?: string;
|
|
430
|
+
layer?: "repo" | "user";
|
|
431
|
+
repoOverlayText?: string;
|
|
412
432
|
}): string | null;
|
|
413
433
|
export type InitConfigOverlay = {
|
|
414
434
|
concurrency?: number;
|
|
@@ -461,6 +481,17 @@ export declare function fleetEditableFromConfig(cfg: TickmarkrConfig, universe?:
|
|
|
461
481
|
export declare function fleetKeyLayer(repoRoot: string, dotted: string, opts?: {
|
|
462
482
|
globalDir?: string;
|
|
463
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
|
+
}>;
|
|
464
495
|
/** Non-interactive fleet state for CI drift checks (`tickmarkr fleet --print`). */
|
|
465
496
|
export declare function formatFleetPrint(repoRoot: string, opts?: {
|
|
466
497
|
globalDir?: string;
|
package/dist/config/config.js
CHANGED
|
@@ -325,7 +325,8 @@ const ShapeGateParticipationSchema = z
|
|
|
325
325
|
});
|
|
326
326
|
export const ExecutionPolicySchema = z.object({
|
|
327
327
|
boundedInfrastructure: z.boolean().default(false),
|
|
328
|
-
|
|
328
|
+
// OBS-1199: absent inherits the effective default (on); an explicit false stays off.
|
|
329
|
+
repairSelection: z.boolean().optional(),
|
|
329
330
|
taskExecutionLimitMs: z.number().int().positive(),
|
|
330
331
|
}).strict();
|
|
331
332
|
export const TickmarkrConfigSchema = z.object({
|
|
@@ -408,6 +409,12 @@ export const TickmarkrConfigSchema = z.object({
|
|
|
408
409
|
tipTest: z.string(),
|
|
409
410
|
lint: z.string(),
|
|
410
411
|
diffCap: z.number().int().positive(),
|
|
412
|
+
// OBS-1140: per-run gate-evidence byte quota; the oldest artifacts are evicted (and tombstoned)
|
|
413
|
+
// past it. Zero retains zero artifact bytes. Evidence retention never changes a gate's pass bit.
|
|
414
|
+
evidenceQuotaBytes: z.number().int().nonnegative(),
|
|
415
|
+
// OBS-1199: repair rounds screen known failing files plus affected tests before the full suite
|
|
416
|
+
// that still decides every merge. On unless false here or in executionPolicy, on every driver.
|
|
417
|
+
repairSelection: z.boolean(),
|
|
411
418
|
byShape: z.partialRecord(z.enum(SHAPES), ShapeGateParticipationSchema).optional(),
|
|
412
419
|
}).partial(),
|
|
413
420
|
scope: z.object({
|
|
@@ -488,6 +495,7 @@ function assertSeededModelWindows() {
|
|
|
488
495
|
}
|
|
489
496
|
}
|
|
490
497
|
}
|
|
498
|
+
export const DEFAULT_EVIDENCE_QUOTA_BYTES = 8 * 1024 * 1024;
|
|
491
499
|
export const DEFAULT_CONFIG = {
|
|
492
500
|
concurrency: 3,
|
|
493
501
|
driver: "auto",
|
|
@@ -537,10 +545,27 @@ export const DEFAULT_CONFIG = {
|
|
|
537
545
|
// retired id. Installed codex 0.144.1 lists it (models_cache.json client_version 0.144.0, fetched
|
|
538
546
|
// 2026-07-10) AND a live runtime probe (`codex exec --model gpt-5.6-sol` → "OK", 2026-07-10) confirmed
|
|
539
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.
|
|
540
559
|
codex: {
|
|
541
560
|
vendor: "openai", channel: "sub",
|
|
542
|
-
models: {
|
|
543
|
-
|
|
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
|
+
},
|
|
544
569
|
},
|
|
545
570
|
// grok-4.5 (xAI, released 2026-07-08) → mid: AA Intelligence 54 (#4), Terminal-Bench 2.1 83.3%
|
|
546
571
|
// (≈ GPT-5.5 83.4, Fable 5 84.3), SWE-bench Pro 64.7%, ~4.2× more token-efficient than Opus 4.8
|
|
@@ -660,7 +685,7 @@ export const DEFAULT_CONFIG = {
|
|
|
660
685
|
},
|
|
661
686
|
},
|
|
662
687
|
pricing: { cheap: 0.1, mid: 0.5, frontier: 2.5 },
|
|
663
|
-
gates: { diffCap: DEFAULT_DIFF_CAP },
|
|
688
|
+
gates: { diffCap: DEFAULT_DIFF_CAP, evidenceQuotaBytes: DEFAULT_EVIDENCE_QUOTA_BYTES },
|
|
664
689
|
judge: { adapter: "claude-code", model: "fable" },
|
|
665
690
|
// R3: no `policy` floor — the neutral floor leaves the compiler's per-task assignment standing, so
|
|
666
691
|
// the path-keyed rule is reachable out of the box rather than raised to full by construction.
|
|
@@ -801,7 +826,11 @@ function resolveRoutingMode(cfg, layers) {
|
|
|
801
826
|
/** loadConfig plus the mode-resolution record (floor provenance + plan lints). The resolved floors are
|
|
802
827
|
* already applied to cfg.routing.floors — route() consumes floors only and never sees the mode. */
|
|
803
828
|
export function loadConfigWithMode(repoRoot, opts = {}) {
|
|
804
|
-
|
|
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);
|
|
805
834
|
// v1.52 T2: repoOverlayText substitutes candidate bytes for the on-disk repo layer — the fleet
|
|
806
835
|
// write guard validates EXACTLY what it is about to write through this one loader path.
|
|
807
836
|
const repoCfg = opts.repoOverlayText === undefined
|
|
@@ -816,24 +845,60 @@ export function loadConfigWithMode(repoRoot, opts = {}) {
|
|
|
816
845
|
assertSeededModelWindows();
|
|
817
846
|
return { cfg: r.data, mode: resolveRoutingMode(r.data, [globalCfg, repoCfg]) };
|
|
818
847
|
}
|
|
819
|
-
/** OBS-1182: tiers.<adapter>.modelOverrides as the layers under the
|
|
820
|
-
*
|
|
821
|
-
*
|
|
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.
|
|
853
|
+
* OBS-1188: an unparseable global file, or a non-map where this path expects a map, is an error —
|
|
854
|
+
* never an empty lower layer, which would silently drop the metadata a write must re-mask. */
|
|
822
855
|
export function lowerLayerModelOverrides(opts = {}) {
|
|
823
856
|
const isMap = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
|
|
824
|
-
const
|
|
825
|
-
|
|
826
|
-
|
|
857
|
+
const path = join(opts.globalDir ?? globalConfigDir(), "config.yaml");
|
|
858
|
+
let global;
|
|
859
|
+
try {
|
|
860
|
+
global = opts.below === "user" ? undefined : readYaml(path);
|
|
861
|
+
}
|
|
862
|
+
catch (error) {
|
|
863
|
+
return { ok: false, error: `lower config layer ${path} is malformed YAML: ${error.message.replace(/\s+/g, " ").trim()}` };
|
|
864
|
+
}
|
|
865
|
+
const malformed = (at) => ({ ok: false, error: `lower config layer ${path} is malformed: ${at} is not a map` });
|
|
866
|
+
// deepMerge replaces a map with any non-map the global declares, so the merged shape is the global's
|
|
867
|
+
const merged = deepMerge(structuredClone(DEFAULT_CONFIG), global);
|
|
868
|
+
if (!isMap(merged))
|
|
869
|
+
return malformed("the document");
|
|
870
|
+
const tiers = merged.tiers ?? {}; // a global `tiers: null` tombstone leaves no lower tiers
|
|
871
|
+
if (!isMap(tiers))
|
|
872
|
+
return malformed("tiers");
|
|
873
|
+
const overrides = {};
|
|
874
|
+
for (const [adapter, entry] of Object.entries(tiers)) {
|
|
875
|
+
if (!isMap(entry))
|
|
876
|
+
return malformed(`tiers.${adapter}`);
|
|
877
|
+
const models = entry.modelOverrides ?? {};
|
|
878
|
+
if (!isMap(models))
|
|
879
|
+
return malformed(`tiers.${adapter}.modelOverrides`);
|
|
880
|
+
for (const [model, override] of Object.entries(models)) {
|
|
881
|
+
if (!isMap(override))
|
|
882
|
+
return malformed(`tiers.${adapter}.modelOverrides.${model}`);
|
|
883
|
+
}
|
|
884
|
+
overrides[adapter] = models;
|
|
885
|
+
}
|
|
886
|
+
return { ok: true, overrides };
|
|
827
887
|
}
|
|
828
888
|
export function loadConfig(repoRoot, opts = {}) {
|
|
829
889
|
return loadConfigWithMode(repoRoot, opts).cfg;
|
|
830
890
|
}
|
|
831
|
-
/** v1.52 T2 write-time reload guard: run candidate
|
|
832
|
-
* loader path every later command uses (parse → merge → schema → mode
|
|
833
|
-
* 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. */
|
|
834
895
|
export function overlayBytesLoadError(repoRoot, bytes, opts = {}) {
|
|
835
896
|
try {
|
|
836
|
-
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
|
+
});
|
|
837
902
|
return null;
|
|
838
903
|
}
|
|
839
904
|
catch (e) {
|
|
@@ -1048,6 +1113,59 @@ export function fleetKeyLayer(repoRoot, dotted, opts = {}) {
|
|
|
1048
1113
|
return "global";
|
|
1049
1114
|
return "defaults";
|
|
1050
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
|
+
}
|
|
1051
1169
|
/** Non-interactive fleet state for CI drift checks (`tickmarkr fleet --print`). */
|
|
1052
1170
|
export function formatFleetPrint(repoRoot, opts = {}) {
|
|
1053
1171
|
const gdir = opts.globalDir ?? globalConfigDir();
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { type FleetEditable, type FleetUniverseRow, type RoutingMode } from "./config.js";
|
|
1
|
+
import { type FleetEditable, type FleetUniverseRow, type LowerLayerModelOverrides, 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 = {
|
|
5
5
|
initial: FleetEditable;
|
|
6
6
|
edited: FleetEditable;
|
|
7
7
|
mode?: RoutingMode;
|
|
@@ -20,8 +20,17 @@ export type FleetOverlayWrite = {
|
|
|
20
20
|
};
|
|
21
21
|
};
|
|
22
22
|
universe?: FleetUniverseRow[];
|
|
23
|
-
lowerOverrides?: Record<string, Record<string, Record<string, unknown>>>;
|
|
24
23
|
};
|
|
24
|
+
type EffortFree = FleetEditable & {
|
|
25
|
+
efforts?: undefined;
|
|
26
|
+
};
|
|
27
|
+
export type FleetOverlayWrite = FleetOverlayWriteFields & ({
|
|
28
|
+
lowerOverrides: LowerLayerModelOverrides;
|
|
29
|
+
} | {
|
|
30
|
+
initial: EffortFree;
|
|
31
|
+
edited: EffortFree;
|
|
32
|
+
lowerOverrides?: undefined;
|
|
33
|
+
});
|
|
25
34
|
export type FleetFirstTouch = {
|
|
26
35
|
vendor: string;
|
|
27
36
|
channel: "sub" | "api";
|
|
@@ -36,3 +45,4 @@ export declare function repoOverlayYaml(overlay: Record<string, unknown>): strin
|
|
|
36
45
|
export declare function serializeFleetOverlay(overlay: Record<string, unknown>): string;
|
|
37
46
|
export declare function unifiedYamlDiff(before: string, after: string, label?: string): string;
|
|
38
47
|
export declare function fleetEditableEquals(a: FleetEditable, b: FleetEditable): boolean;
|
|
48
|
+
export {};
|
|
@@ -368,27 +368,32 @@ export function renderFleetOverlayWrite(priorBytes, write) {
|
|
|
368
368
|
const after = efforts(edited)[adapter]?.[model];
|
|
369
369
|
if (efforts(initial)[adapter]?.[model] === after)
|
|
370
370
|
continue;
|
|
371
|
+
// OBS-1188: the type already demands lower state here; this refuses a cast/JS caller before the
|
|
372
|
+
// bytes are returned, so a blind effort write never reaches disk.
|
|
373
|
+
const lowerAll = write.lowerOverrides;
|
|
374
|
+
if (!lowerAll) {
|
|
375
|
+
throw new Error(`fleet write: the effort edit on ${adapter}:${model} needs the lower config layers' model overrides — refusing to write it blind`);
|
|
376
|
+
}
|
|
371
377
|
const override = ["tiers", adapter, "modelOverrides", model];
|
|
372
378
|
if (after !== undefined) {
|
|
373
379
|
// Setting beneath a tombstone (the override, or modelOverrides above it) lifts it, and
|
|
374
380
|
// deepMerge would restore what it masked: re-mask each lower-layer key it covered — sibling
|
|
375
|
-
// models, then this override's
|
|
381
|
+
// models, then this override's own keys.
|
|
376
382
|
// ponytail: tiers.<adapter> itself tombstoned is unreachable here — its models, and so a tier, are masked too.
|
|
377
383
|
const tombstoned = (depth) => {
|
|
378
384
|
const node = doc.getIn(override.slice(0, depth), true);
|
|
379
385
|
return node !== undefined && !isMap(node);
|
|
380
386
|
};
|
|
381
|
-
const lowerModels =
|
|
387
|
+
const lowerModels = lowerAll[adapter] ?? {};
|
|
382
388
|
const remask = [];
|
|
383
389
|
if (tombstoned(3))
|
|
384
390
|
for (const other of Object.keys(lowerModels))
|
|
385
391
|
if (other !== model)
|
|
386
392
|
remask.push([...override.slice(0, 3), other]);
|
|
387
393
|
if (tombstoned(3) || tombstoned(4)) {
|
|
388
|
-
for (const key of
|
|
394
|
+
for (const key of Object.keys(lowerModels[model] ?? {}))
|
|
389
395
|
if (key !== "effort")
|
|
390
396
|
remask.push([...override, key]);
|
|
391
|
-
}
|
|
392
397
|
}
|
|
393
398
|
setScalarPreservingComment(doc, [...override, "effort"], after);
|
|
394
399
|
for (const path of remask)
|
|
@@ -396,11 +401,10 @@ export function renderFleetOverlayWrite(priorBytes, write) {
|
|
|
396
401
|
continue;
|
|
397
402
|
}
|
|
398
403
|
// Clearing means no effort in the MERGED config: drop the repo key, then mask an effort the
|
|
399
|
-
// lower layers still declare
|
|
400
|
-
const held = doc.getIn([...override, "effort"]) !== undefined;
|
|
404
|
+
// lower layers still declare.
|
|
401
405
|
deleteAt(doc, [...override, "effort"]);
|
|
402
|
-
const lower =
|
|
403
|
-
const maskEffort =
|
|
406
|
+
const lower = lowerAll[adapter]?.[model];
|
|
407
|
+
const maskEffort = lower?.effort !== undefined;
|
|
404
408
|
const repoNode = doc.getIn(override, true);
|
|
405
409
|
const { effort: _masked, ...rest } = { ...lower, ...(isMap(repoNode) ? repoNode.toJSON() : {}) };
|
|
406
410
|
if (Object.values(rest).some((v) => v !== null && v !== undefined)) {
|
package/dist/drivers/herdr.d.ts
CHANGED
|
@@ -62,6 +62,8 @@ export declare class HerdrDriver implements ExecutorDriver {
|
|
|
62
62
|
private deliverySerial;
|
|
63
63
|
private dispatchLeases;
|
|
64
64
|
private deliveredPanes;
|
|
65
|
+
/** OBS-1203: slots bound by adopt() — their reads are harvest evidence, so a failed read throws. */
|
|
66
|
+
private adoptedSlots;
|
|
65
67
|
private inputBoxes;
|
|
66
68
|
private bootstraps;
|
|
67
69
|
private journalRoots;
|
|
@@ -93,6 +95,16 @@ export declare class HerdrDriver implements ExecutorDriver {
|
|
|
93
95
|
private paneId;
|
|
94
96
|
private paneWidth;
|
|
95
97
|
slot(cwd: string, name: string, opts?: SlotOpts): Promise<Slot>;
|
|
98
|
+
/**
|
|
99
|
+
* OBS-1203: bind THIS instance, READ-ONLY, to the pane an earlier daemon created for an owned slot,
|
|
100
|
+
* so a resumed daemon can read the interrupted attempt's trailer. slot() is never a substitute: it
|
|
101
|
+
* allocates a tab, reclaimStaleLabel closes the very pane holding the evidence, and it holds a
|
|
102
|
+
* dispatch lease only run() releases. Binding needs ownership evidence, never a label alone: exactly
|
|
103
|
+
* one pane carries the FULL owned label, herdr reports that pane's cwd as the journaled task
|
|
104
|
+
* checkout, and that path is a git checkout whose own top level it is. Zero, ambiguous or foreign
|
|
105
|
+
* candidates throw — the caller's decline. `pane list` and `git rev-parse` are the only calls made.
|
|
106
|
+
*/
|
|
107
|
+
adopt(slot: Slot): Promise<Slot>;
|
|
96
108
|
private tabSlot;
|
|
97
109
|
private reclaimStaleLabel;
|
|
98
110
|
private staleLabelPanes;
|