tickmarkr 2.6.3 → 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 +10 -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.js +27 -2
- package/dist/cli/commands/doctor.d.ts +2 -0
- package/dist/cli/commands/doctor.js +6 -1
- package/dist/cli/commands/fleet.js +241 -74
- package/dist/cli/commands/init.js +5 -3
- 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.js +332 -121
- package/dist/compile/native.js +130 -0
- package/dist/config/config.d.ts +25 -7
- package/dist/config/config.js +92 -11
- package/dist/route/router.d.ts +11 -0
- package/dist/route/router.js +29 -8
- package/dist/run/daemon.js +8 -3
- package/dist/run/journal.d.ts +97 -1
- package/dist/run/journal.js +337 -2
- package/dist/tui/cockpit/live-store.d.ts +6 -0
- package/dist/tui/cockpit/live-store.js +36 -11
- package/package.json +1 -1
- package/skills/tickmarkr-auto/SKILL.md +2 -2
- package/skills/tickmarkr-loop/SKILL.md +10 -4
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();
|
package/dist/route/router.d.ts
CHANGED
|
@@ -55,6 +55,17 @@ export declare const taskPool: (task: Task, cfg: TickmarkrConfig) => {
|
|
|
55
55
|
/** Park-reason suffix naming the pool an exhausted re-dispatch stayed inside; empty when unpooled. */
|
|
56
56
|
export declare const poolExhaustion: (task: Task, cfg: TickmarkrConfig) => string;
|
|
57
57
|
export declare const inTaskPool: (task: Task, cfg: TickmarkrConfig, key: string) => boolean;
|
|
58
|
+
/**
|
|
59
|
+
* F (D-718): the one exclusion meaning route(), nextChannel() and climbChannel() share — explicit
|
|
60
|
+
* exclusions, demotions, tried and escalation history, and climb skips — exported so resume can test a
|
|
61
|
+
* restored assignment the same way. An excluded raw channel key always excludes itself (the fallback).
|
|
62
|
+
* The exclusion extends to another offered channel only when BOTH offered channels carry a probed
|
|
63
|
+
* identity and the two are equal. A missing identity is neutral: it never matches and never blocks.
|
|
64
|
+
* Residual, by ruling: an unprobed alias of an excluded model stays eligible by its raw key — this is
|
|
65
|
+
* not complete alias exclusion; reopen on an executed re-dispatch of a tried model through an alias
|
|
66
|
+
* with no probed identity. The daemon's nextChannel-null exhaustion fallback is not this policy's.
|
|
67
|
+
*/
|
|
68
|
+
export declare function channelExclusion(offered: readonly BillingChannel[], excluded: Iterable<string>): (seat: ChannelSeat) => boolean;
|
|
58
69
|
export declare function route(task: Task, cfg: TickmarkrConfig, channels: BillingChannel[], profile?: RoutingProfile, preferCtx?: RoutingPreferContext, exclude?: ReadonlySet<string>, exploreCtx?: ExploreContext): Route;
|
|
59
70
|
export declare function nextChannel(current: Assignment, task: Task, cfg: TickmarkrConfig, channels: BillingChannel[], tried: string[], profile?: RoutingProfile, exclude?: ReadonlySet<string>): Assignment | null;
|
|
60
71
|
export type ClimbPick = Assignment & {
|
package/dist/route/router.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { channelKey, channelsFromConfig } from "../adapters/types.js";
|
|
2
2
|
import { TIER_RANK } from "../config/config.js";
|
|
3
|
-
import {
|
|
3
|
+
import { disallowedBy, routingModelProvider } from "./preference.js";
|
|
4
4
|
import { cellOf, EXPLORE_CAP, explorationBonus, learnedScore, MIN_SAMPLES } from "./profile.js";
|
|
5
5
|
export const NO_EXPLORE_ENV = "TICKMARKR_NO_EXPLORE";
|
|
6
6
|
// OBS-89 (v1.60): the TICKMARKR_QUALITY variable is RETIRED — nothing in src reads it anymore and
|
|
@@ -124,11 +124,33 @@ function spreadFrontierTies(sorted, task, prefer, tied) {
|
|
|
124
124
|
}
|
|
125
125
|
return out;
|
|
126
126
|
}
|
|
127
|
+
/**
|
|
128
|
+
* F (D-718): the one exclusion meaning route(), nextChannel() and climbChannel() share — explicit
|
|
129
|
+
* exclusions, demotions, tried and escalation history, and climb skips — exported so resume can test a
|
|
130
|
+
* restored assignment the same way. An excluded raw channel key always excludes itself (the fallback).
|
|
131
|
+
* The exclusion extends to another offered channel only when BOTH offered channels carry a probed
|
|
132
|
+
* identity and the two are equal. A missing identity is neutral: it never matches and never blocks.
|
|
133
|
+
* Residual, by ruling: an unprobed alias of an excluded model stays eligible by its raw key — this is
|
|
134
|
+
* not complete alias exclusion; reopen on an executed re-dispatch of a tried model through an alias
|
|
135
|
+
* with no probed identity. The daemon's nextChannel-null exhaustion fallback is not this policy's.
|
|
136
|
+
*/
|
|
137
|
+
export function channelExclusion(offered, excluded) {
|
|
138
|
+
const keys = new Set(excluded);
|
|
139
|
+
const identityOf = new Map(offered.flatMap((c) => (c.identity ? [[channelKey(c), c.identity]] : [])));
|
|
140
|
+
const identities = new Set([...keys].flatMap((k) => identityOf.get(k) ?? []));
|
|
141
|
+
return (seat) => {
|
|
142
|
+
const key = channelKey(seat);
|
|
143
|
+
const identity = identityOf.get(key);
|
|
144
|
+
return keys.has(key) || (identity !== undefined && identities.has(identity));
|
|
145
|
+
};
|
|
146
|
+
}
|
|
127
147
|
function withoutExcluded(channels, exclude) {
|
|
128
148
|
if (!exclude?.size)
|
|
129
149
|
return channels;
|
|
130
150
|
// OBS-57: in-run demotion after consecutive no-trailer windows — route around poisoned channels for the rest of the run.
|
|
131
|
-
|
|
151
|
+
// D-718: a demoted channel's probed-identical alias is routed around too; an unprobed one is not.
|
|
152
|
+
const excluded = channelExclusion(channels, exclude);
|
|
153
|
+
return channels.filter((c) => !excluded(c));
|
|
132
154
|
}
|
|
133
155
|
// Pools spread providers before tier, independently within each marginal-cost run. Stable
|
|
134
156
|
// cost sorting preserves declaration order when forming groups; gateways share the served
|
|
@@ -394,19 +416,18 @@ export function route(task, cfg, channels, profile, preferCtx, exclude, exploreC
|
|
|
394
416
|
return { assignment: toAssignment(eligible[0]), ladder: ladderFor(task, entry), lints, provenance: `${degraded}${bound}, marginal-cost auto (${chosenBy})`, ...(deviation ? { deviation } : {}) };
|
|
395
417
|
}
|
|
396
418
|
function candidatePool(current, task, cfg, channels, tried, tierPredicate, exclude) {
|
|
397
|
-
|
|
419
|
+
// D-718: tried/escalated/demoted keys share channelExclusion's meaning, resolved against every offered
|
|
420
|
+
// channel; the provider-outage rule below still reads the raw-key remainder exactly as before.
|
|
421
|
+
const excluded = channelExclusion(channels, [...(exclude ?? []), ...tried]);
|
|
422
|
+
channels = channels.filter((c) => !exclude?.has(channelKey(c)) && inTaskPool(task, cfg, channelKey(c)));
|
|
398
423
|
const triedKeys = new Set(tried);
|
|
399
|
-
const triedIdentities = new Set(tried.map((key) => {
|
|
400
|
-
const channel = channels.find((c) => channelKey(c) === key);
|
|
401
|
-
return channel ? modelRouteIdentity(channel.model, channel.vendor) : channelRouteIdentity(key);
|
|
402
|
-
}));
|
|
403
424
|
// excludeAdapter is expanded by the daemon into every channel key of the failed adapter. Once that
|
|
404
425
|
// complete set is present, the outage follows the current served provider across gateway aliases.
|
|
405
426
|
const currentAdapterExcluded = channels.some((c) => c.adapter === current.adapter)
|
|
406
427
|
&& channels.filter((c) => c.adapter === current.adapter).every((c) => triedKeys.has(channelKey(c)));
|
|
407
428
|
const currentChannel = channels.find((c) => c.adapter === current.adapter && c.model === current.model);
|
|
408
429
|
const excludedProvider = currentAdapterExcluded ? routingModelProvider(current.model, currentChannel?.vendor) : undefined;
|
|
409
|
-
return channels.filter((c) => !
|
|
430
|
+
return channels.filter((c) => !excluded(c)
|
|
410
431
|
&& (!excludedProvider || routingModelProvider(c.model, c.vendor) !== excludedProvider)
|
|
411
432
|
&& tierPredicate(c.tier));
|
|
412
433
|
}
|
package/dist/run/daemon.js
CHANGED
|
@@ -39,7 +39,7 @@ import { applyScopeAmendments, activeRetryBan, interruptedAttempt, RESUME_HARVES
|
|
|
39
39
|
import { gateReviewerFloor, isDiffCapPark, isGarbageReview, pickReviewer } from "../gates/review.js";
|
|
40
40
|
import { acquireApprovalSerialization, acquireRunLock, isPidLive, releaseRunLock } from "./lock.js";
|
|
41
41
|
import { ensureIntegration, integrationBranch, integrationHead, mergeTask, reusedTipEvidence, verifyIntegrationTip } from "./merge.js";
|
|
42
|
-
import { climbChannel, inTaskPool, marginalCostRank, nextChannel, poolExhaustion, route, taskPool } from "../route/router.js";
|
|
42
|
+
import { channelExclusion, climbChannel, inTaskPool, marginalCostRank, nextChannel, poolExhaustion, route, taskPool } from "../route/router.js";
|
|
43
43
|
import { desiredPanes } from "./reconcile.js";
|
|
44
44
|
import { readTierLiveness, readWatchBoard, supervisionPresencePath } from "./supervision.js";
|
|
45
45
|
import { harvestCpuFlatWindowMs, NUDGEABLE_ADAPTERS, PANE_READ_ROWS, StallProgressTracker, stallSnapshotBannerRows, WorkerTreeCpuAccountant, reapOwnedProcessGroup, readOwnedProcessGroup, } from "./stall.js";
|
|
@@ -3215,10 +3215,12 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
3215
3215
|
// and a lastAssignment at zero attempts is a first dispatch whose capacity requeue was taken back:
|
|
3216
3216
|
// the seat is still in force, so restore it instead of failing over its own tried[] entry early.
|
|
3217
3217
|
// OBS-1187: a restored seat must still be a member of the task's pool (the operator may have closed it).
|
|
3218
|
+
// F/W (D-718): and not excluded under the router's one meaning — its raw key, or a probed identity
|
|
3219
|
+
// it shares with an excluded channel; an unprobed seat is excluded by its raw key alone.
|
|
3218
3220
|
if (!hintsChanged && rs?.lastAssignment
|
|
3219
3221
|
&& channels.some((c) => channelKey(c) === channelKey(rs.lastAssignment))
|
|
3220
3222
|
&& inTaskPool(t, cfg, channelKey(rs.lastAssignment))
|
|
3221
|
-
&& !demotedChannels
|
|
3223
|
+
&& !channelExclusion(channels, demotedChannels)(rs.lastAssignment)) {
|
|
3222
3224
|
assignment = rs.lastAssignment; // restore the consult-chosen assignment (bypasses route()'s static re-pick)
|
|
3223
3225
|
}
|
|
3224
3226
|
else if (!hintsChanged && rs && rs.tried.length && !t.routingHints?.pin) {
|
|
@@ -4016,7 +4018,10 @@ export async function runDaemon(repoRoot, opts = {}) {
|
|
|
4016
4018
|
const followsApproved = GATE_NAMES.indexOf(gate) > satisfiedIndex;
|
|
4017
4019
|
const otherRed = prior?.pass === false;
|
|
4018
4020
|
const needsFullSuite = gate === "test" && prior?.fullSuite !== true;
|
|
4019
|
-
|
|
4021
|
+
// W: a red evidence/scope screen ends the round before the battery, so a gate that PRECEDES
|
|
4022
|
+
// the waived one may never have run: no prior-round row is no verdict — run it as declared.
|
|
4023
|
+
const unobserved = prior === undefined;
|
|
4024
|
+
return followsApproved || otherRed || needsFullSuite || unobserved;
|
|
4020
4025
|
});
|
|
4021
4026
|
}
|
|
4022
4027
|
else {
|
package/dist/run/journal.d.ts
CHANGED
|
@@ -435,8 +435,8 @@ export declare const TelemetryRowSchema: z.ZodObject<{
|
|
|
435
435
|
quotaFailover: z.ZodOptional<z.ZodLiteral<true>>;
|
|
436
436
|
overrun: z.ZodOptional<z.ZodLiteral<true>>;
|
|
437
437
|
retryMode: z.ZodOptional<z.ZodEnum<{
|
|
438
|
-
repair: "repair";
|
|
439
438
|
resume: "resume";
|
|
439
|
+
repair: "repair";
|
|
440
440
|
fresh: "fresh";
|
|
441
441
|
}>>;
|
|
442
442
|
signalQuality: z.ZodOptional<z.ZodUnion<readonly [z.ZodLiteral<0>, z.ZodLiteral<0.25>, z.ZodLiteral<0.5>, z.ZodLiteral<0.75>, z.ZodLiteral<1>]>>;
|
|
@@ -593,4 +593,100 @@ export declare class Journal {
|
|
|
593
593
|
readTelemetry(): TelemetryRow[];
|
|
594
594
|
readJudgeTelemetry(): TelemetryRow[];
|
|
595
595
|
}
|
|
596
|
+
export declare const OWED_CHECK_VERSION: 1;
|
|
597
|
+
export declare const OWED_DISCHARGE_EVENT: "owed-check-discharged";
|
|
598
|
+
export interface OwedCheck {
|
|
599
|
+
version: typeof OWED_CHECK_VERSION;
|
|
600
|
+
id: string;
|
|
601
|
+
runId: string;
|
|
602
|
+
taskId: string;
|
|
603
|
+
gate: GateName;
|
|
604
|
+
/** The immutable range the waived gate measured, and its canonical subject (daemon gateCommitSubject). */
|
|
605
|
+
base: string;
|
|
606
|
+
head: string;
|
|
607
|
+
subject: string;
|
|
608
|
+
/** Verbatim (whitespace-sensitive) patch identity of the whole range and of each commit, in order — the integration mapping key. */
|
|
609
|
+
patch: string;
|
|
610
|
+
patches: string[];
|
|
611
|
+
criteria: string;
|
|
612
|
+
/** The task's file scope at waive time: a discharge gates exactly this allowlist, never a wider one. */
|
|
613
|
+
files: string[];
|
|
614
|
+
authors: string[];
|
|
615
|
+
/** The channels this repository's adapters declared under its config at waive time (key → vendor):
|
|
616
|
+
* the only seats a review discharge's reviewer and author channels may resolve to. */
|
|
617
|
+
declared: ChannelFact[];
|
|
618
|
+
cause: string;
|
|
619
|
+
evidence: string;
|
|
620
|
+
disposition: "accepted-risk";
|
|
621
|
+
known: boolean;
|
|
622
|
+
unknownReason?: string;
|
|
623
|
+
}
|
|
624
|
+
export interface OwedFold {
|
|
625
|
+
known: boolean;
|
|
626
|
+
debt: number | "unknown";
|
|
627
|
+
outstanding: OwedCheck[];
|
|
628
|
+
acceptedRisk: OwedCheck[];
|
|
629
|
+
discharged: string[];
|
|
630
|
+
unknown: Array<{
|
|
631
|
+
taskId?: string;
|
|
632
|
+
gate?: string;
|
|
633
|
+
reason: string;
|
|
634
|
+
}>;
|
|
635
|
+
}
|
|
636
|
+
export declare const owedCriteria: (acceptance: unknown) => string;
|
|
637
|
+
/** The daemon's canonical gate subject for base..head (daemon.ts gateCommitSubject), byte for byte. */
|
|
638
|
+
export declare const owedSubject: (cwd: string, base: string, head: string) => string;
|
|
639
|
+
/** Ordered verbatim patch ids of the commits in base..head; an empty commit authored no patch. */
|
|
640
|
+
export declare const rangePatches: (cwd: string, base: string, head: string) => string[];
|
|
641
|
+
/** Verbatim patch identity of the whole change base..head introduces. */
|
|
642
|
+
export declare const rangePatch: (cwd: string, base: string, head: string) => string;
|
|
643
|
+
/**
|
|
644
|
+
* `head` is the task's own recorded integration merge of the obligation's range — exactly. Its first
|
|
645
|
+
* parent is `mergeBase`; its second parent replays the waived history source tree for source tree from
|
|
646
|
+
* a base with the waived base's tree (a carry may mint new commit objects, never new content — and
|
|
647
|
+
* patch ids alone ignore where a hunk lands); its tree is git's own clean merge of the two (no
|
|
648
|
+
* resolution, no amend); and it introduced the same ordered verbatim patch series and total patch.
|
|
649
|
+
* Squashed, reordered, relocated, re-indented, reverted or conflict-resolved history fails one of
|
|
650
|
+
* these and is refused as ambiguous; containment never counts.
|
|
651
|
+
* Both verify (before discharging) and the fold (re-reading every discharge) decide through here.
|
|
652
|
+
*/
|
|
653
|
+
export declare function integrationMapped(cwd: string, events: readonly JournalEvent[], o: OwedCheck, mergeBase: unknown, head: unknown): boolean;
|
|
654
|
+
/**
|
|
655
|
+
* Lifetime patch authors of base..head, read from the run: the daemon's subjectAuthors fold
|
|
656
|
+
* (daemon.ts, not exported and not this task's to edit) restated synchronously so the waive row is
|
|
657
|
+
* written without yielding. Recreation rows name SOURCE hashes, so ownership joins by stable patch
|
|
658
|
+
* identity across changed commit objects; a gate-only restore (resume-restore's seat, a recreation
|
|
659
|
+
* after work started) never becomes an author. Unreadable history names an unknown author.
|
|
660
|
+
*/
|
|
661
|
+
export declare function owedAuthors(events: readonly JournalEvent[], taskId: string, cwd: string, base: string, head: string): string[];
|
|
662
|
+
/**
|
|
663
|
+
* The obligation a waive of `gate` leaves on the task branch's current range. Synchronous on purpose:
|
|
664
|
+
* approve writes it into the waive row under the approval serialization without yielding, so a live
|
|
665
|
+
* daemon's boundary sees the same single append it always did. Anything not bound exactly — the
|
|
666
|
+
* waived row measured another subject, unreadable refs, unresolved authors, unreadable criteria — is
|
|
667
|
+
* recorded, never guessed, as known: false.
|
|
668
|
+
*/
|
|
669
|
+
export declare function captureOwedCheck(input: {
|
|
670
|
+
cwd: string;
|
|
671
|
+
runId: string;
|
|
672
|
+
taskId: string;
|
|
673
|
+
gate: GateName;
|
|
674
|
+
cause: string;
|
|
675
|
+
acceptance: unknown;
|
|
676
|
+
files: unknown;
|
|
677
|
+
declared: readonly ChannelFact[] | undefined;
|
|
678
|
+
taskRef: string;
|
|
679
|
+
integrationRef: string;
|
|
680
|
+
events: readonly JournalEvent[];
|
|
681
|
+
}): OwedCheck;
|
|
682
|
+
export type ChannelFact = {
|
|
683
|
+
key: string;
|
|
684
|
+
vendor: string;
|
|
685
|
+
};
|
|
686
|
+
/**
|
|
687
|
+
* The CURRENT owed-check fold: accepted-risk history, outstanding debt, and whether it is known.
|
|
688
|
+
* `cwd` is any checkout of the run's repository (integration discharges are re-proved from git).
|
|
689
|
+
* It never throws: a fold that cannot be computed is unknown debt, not a crash and never zero.
|
|
690
|
+
*/
|
|
691
|
+
export declare function foldOwedChecks(events: readonly JournalEvent[], cwd: string): OwedFold;
|
|
596
692
|
export {};
|