tickmarkr 2.5.3 → 2.5.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.
Files changed (71) hide show
  1. package/dist/adapters/registry.js +6 -1
  2. package/dist/adapters/types.d.ts +3 -0
  3. package/dist/cli/commands/approve.d.ts +1 -0
  4. package/dist/cli/commands/approve.js +54 -6
  5. package/dist/cli/commands/doctor.d.ts +4 -0
  6. package/dist/cli/commands/doctor.js +44 -29
  7. package/dist/cli/commands/fleet.js +195 -33
  8. package/dist/cli/commands/init.js +196 -6
  9. package/dist/cli/commands/plan.js +144 -3
  10. package/dist/cli/commands/resume.js +6 -2
  11. package/dist/cli/commands/run.js +21 -2
  12. package/dist/cli/commands/verify.js +1 -1
  13. package/dist/cli/help.d.ts +2 -0
  14. package/dist/cli/help.js +3 -1
  15. package/dist/compile/collateral.d.ts +2 -0
  16. package/dist/compile/collateral.js +50 -9
  17. package/dist/compile/ownership.d.ts +7 -0
  18. package/dist/compile/ownership.js +59 -22
  19. package/dist/config/config.d.ts +23 -8
  20. package/dist/config/config.js +42 -28
  21. package/dist/config/fleet-overlay.d.ts +3 -9
  22. package/dist/config/fleet-overlay.js +68 -11
  23. package/dist/config/fleet-why.d.ts +7 -0
  24. package/dist/config/fleet-why.js +5 -0
  25. package/dist/drivers/herdr.d.ts +5 -0
  26. package/dist/drivers/herdr.js +14 -3
  27. package/dist/drivers/index.d.ts +15 -1
  28. package/dist/drivers/index.js +38 -10
  29. package/dist/drivers/orca.d.ts +99 -10
  30. package/dist/drivers/orca.js +586 -97
  31. package/dist/drivers/types.d.ts +1 -0
  32. package/dist/gates/baseline.d.ts +3 -0
  33. package/dist/gates/baseline.js +2 -1
  34. package/dist/gates/cache.d.ts +100 -0
  35. package/dist/gates/cache.js +389 -0
  36. package/dist/gates/review.d.ts +35 -2
  37. package/dist/gates/review.js +64 -16
  38. package/dist/gates/run-gates.d.ts +5 -0
  39. package/dist/gates/run-gates.js +134 -18
  40. package/dist/gates/test-manifest.d.ts +99 -0
  41. package/dist/gates/test-manifest.js +389 -0
  42. package/dist/gates/test-reporter.d.ts +4 -0
  43. package/dist/gates/test-reporter.js +49 -0
  44. package/dist/route/preference.d.ts +22 -1
  45. package/dist/route/preference.js +123 -25
  46. package/dist/route/router.d.ts +13 -0
  47. package/dist/route/router.js +95 -17
  48. package/dist/run/daemon.d.ts +13 -0
  49. package/dist/run/daemon.js +398 -96
  50. package/dist/run/git.d.ts +35 -1
  51. package/dist/run/git.js +111 -10
  52. package/dist/run/journal.d.ts +14 -2
  53. package/dist/run/journal.js +78 -10
  54. package/dist/run/lease.d.ts +14 -0
  55. package/dist/run/lease.js +87 -0
  56. package/dist/run/merge.d.ts +2 -0
  57. package/dist/run/merge.js +91 -3
  58. package/dist/run/operator-state.d.ts +11 -0
  59. package/dist/run/operator-state.js +17 -3
  60. package/dist/tui/cockpit/board.d.ts +96 -0
  61. package/dist/tui/cockpit/board.js +346 -0
  62. package/dist/tui/cockpit/decision-actions.js +2 -0
  63. package/dist/tui/cockpit/layout.d.ts +5 -1
  64. package/dist/tui/cockpit/layout.js +8 -3
  65. package/dist/tui/cockpit/live-runtime.js +83 -31
  66. package/dist/tui/cockpit/run-view.d.ts +7 -5
  67. package/dist/tui/cockpit/run-view.js +12 -11
  68. package/dist/tui/ink/fleet-app.d.ts +41 -27
  69. package/dist/tui/ink/fleet-app.js +204 -31
  70. package/package.json +1 -1
  71. package/skills/tickmarkr-overseer/SKILL.md +180 -99
@@ -0,0 +1,389 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import { existsSync, mkdtempSync, readFileSync, realpathSync, writeFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import { isAbsolute, join, relative, sep } from "node:path";
5
+ import { TEST_REPORTER_SOURCE } from "./test-reporter.js";
6
+ import { shq } from "../adapters/types.js";
7
+ import { FORK_CAP_ENV, ROUTING_ENV_SEAMS, SUITE_PARENT_ENV, shell, resolvedCapacity } from "../run/git.js";
8
+ /**
9
+ * VL-1 (OBS-985 lineage): a test gate's completion must be the runner's OWN report, never a stdout
10
+ * count. `fileCountDeficit` (baseline.ts) reads a summary LINE — a selected screen's smaller count
11
+ * looked identical to a truncated full suite (OBS-985's shipped defect). This module is the
12
+ * independent validator: it names a MANIFEST (the files the runner is expected to collect), binds a
13
+ * report to ONE invocation with a nonce, and trusts nothing the runner did not certify per file.
14
+ */
15
+ /** Parse the supported simple shell invocation, retaining quoted argument values. Shell programs
16
+ * are deliberately outside this contract: forwarding reporter options to their last command is unsafe. */
17
+ function shellTokens(command) {
18
+ return command.match(/(?:[^\s'"\\&|;<>]+|'[^']*'|"(?:\\.|[^"\\])*"|\\.)+|[&|;<>]+/g) ?? [];
19
+ }
20
+ function words(command) {
21
+ const tokens = shellTokens(command);
22
+ return tokens.map((token) => token.replace(/'([^']*)'|"((?:\\.|[^"\\])*)"|\\(.)/g, (_all, single, double, escaped) => single ?? (double !== undefined ? double.replace(/\\(["\\$`])/g, "$1") : escaped)));
23
+ }
24
+ const namesRunner = (body) => /(?:^|[\s/\\'";&|])vitest(?:\.mjs)?(?=$|[\s'";&|])/.test(body);
25
+ function scriptInvocation(cmd, cwd) {
26
+ const argv = words(cmd);
27
+ const raw = shellTokens(cmd);
28
+ let manager = 0;
29
+ while (/^[A-Za-z_][A-Za-z_0-9]*=/.test(argv[manager] ?? ""))
30
+ manager++;
31
+ if (!["npm", "pnpm", "yarn"].includes(argv[manager]))
32
+ return undefined;
33
+ const offset = manager + (["run", "run-script"].includes(argv[manager + 1]) ? 2 : 1);
34
+ const name = ["t", "tst"].includes(argv[offset]) ? "test" : argv[offset];
35
+ try {
36
+ const body = JSON.parse(readFileSync(join(cwd, "package.json"), "utf8")).scripts?.[name];
37
+ return typeof body === "string" ? { body, trailing: raw.slice(offset + 1).filter((a) => a !== "--"), environment: raw.slice(0, manager), npm: argv[manager] === "npm" } : undefined;
38
+ }
39
+ catch {
40
+ return undefined;
41
+ }
42
+ }
43
+ export function isVitestTestCommand(cmd, cwd) {
44
+ return namesRunner(scriptInvocation(cmd, cwd)?.body ?? cmd);
45
+ }
46
+ function runnerInvocation(cmd, cwd) {
47
+ const script = scriptInvocation(cmd, cwd);
48
+ const body = script?.body ?? cmd;
49
+ if ([body, cmd].some((text) => shellTokens(text).some((token) => /^[&|;<>]+$/.test(token)) || /[`\n]|\$\(/.test(text)))
50
+ throw new Error("vitest command is compound or contains unsupported shell syntax");
51
+ const args = words(body);
52
+ const raw = shellTokens(body);
53
+ let i = 0;
54
+ while (/^[A-Za-z_][A-Za-z_0-9]*=/.test(args[i] ?? ""))
55
+ i++;
56
+ const env = [...(script?.environment ?? []), ...raw.slice(0, i)];
57
+ let runner = args[i];
58
+ const prefix = [];
59
+ if (runner === "node") {
60
+ prefix.push(runner);
61
+ runner = args[++i];
62
+ }
63
+ else if (runner === "npx") {
64
+ runner = args[++i];
65
+ }
66
+ if (!runner || !/(?:^|[/\\])vitest(?:\.mjs)?$/.test(runner))
67
+ throw new Error("vitest script body is not a supported direct runner invocation");
68
+ const binary = runner === "vitest" ? join(cwd, "node_modules/.bin/vitest") : isAbsolute(runner) ? runner : join(cwd, runner);
69
+ if (!existsSync(binary))
70
+ throw new Error(`vitest binary does not exist: ${binary}`);
71
+ const forwarded = [...raw.slice(i + 1), ...(script?.trailing ?? [])];
72
+ if (forwarded[0] === "run")
73
+ forwarded.shift();
74
+ const collection = forwarded.filter((a) => a !== "--run" && a !== "--watch" && a !== "--");
75
+ return {
76
+ listing: [...env, ...prefix.map(shq), shq(binary), "list", ...collection, "--json"].join(" "),
77
+ separator: script?.npm && !/(?:^|\s)--(?:\s|$)/.test(cmd) ? " --" : "",
78
+ };
79
+ }
80
+ /** One identity for every path this module compares: repo-relative, forward-slash. `vitest list
81
+ * --json` and `TestModule.moduleId` both hand back an absolute filesystem path already resolved
82
+ * through any symlink in it (e.g. macOS's `/var` -> `/private/var`, under which every OS temp dir —
83
+ * and so every test fixture worktree — lives); `cwd` as tickmarkr holds it may not be. Resolving cwd
84
+ * before computing the relative path is what makes the two actually comparable. Manifests built from
85
+ * a selected-test screen are already repo-relative and pass through unchanged. */
86
+ export function toManifestPath(file, cwd) {
87
+ if (!isAbsolute(file))
88
+ return file.replace(/^\.\//, "").split(sep).join("/");
89
+ let resolvedCwd = cwd;
90
+ try {
91
+ resolvedCwd = realpathSync(cwd);
92
+ }
93
+ catch { /* cwd unreadable — best effort with the given path */ }
94
+ return relative(resolvedCwd, file).split(sep).join("/");
95
+ }
96
+ function isTestReportShape(v) {
97
+ if (typeof v !== "object" || v === null)
98
+ return false;
99
+ const r = v;
100
+ if (typeof r.nonce !== "string" || !Array.isArray(r.requested) || typeof r.started !== "object" || r.started === null || typeof r.completed !== "object" || r.completed === null)
101
+ return false;
102
+ if (!r.requested.every((f) => typeof f === "string"))
103
+ return false;
104
+ if (Object.values(r.started).some((v) => typeof v !== "number"))
105
+ return false;
106
+ for (const c of Object.values(r.completed)) {
107
+ if (typeof c !== "object" || c === null)
108
+ return false;
109
+ const cc = c;
110
+ if (typeof cc.at !== "number" || (cc.status !== "passed" && cc.status !== "failed"))
111
+ return false;
112
+ }
113
+ if (r.duplicateCompletions !== undefined) {
114
+ if (!Array.isArray(r.duplicateCompletions) || !r.duplicateCompletions.every((f) => typeof f === "string"))
115
+ return false;
116
+ }
117
+ if (r.certificate !== undefined) {
118
+ if (typeof r.certificate !== "object" || r.certificate === null)
119
+ return false;
120
+ const cert = r.certificate;
121
+ if (typeof cert.at !== "number" || typeof cert.exitCode !== "number")
122
+ return false;
123
+ }
124
+ return true;
125
+ }
126
+ /** Reads and structurally validates the report; a missing or malformed file is `undefined` — never a partial parse. */
127
+ export function readTestReport(path) {
128
+ if (!existsSync(path))
129
+ return undefined;
130
+ try {
131
+ const parsed = JSON.parse(readFileSync(path, "utf8"));
132
+ return isTestReportShape(parsed) ? parsed : undefined;
133
+ }
134
+ catch {
135
+ return undefined;
136
+ }
137
+ }
138
+ /**
139
+ * The independent validator: given the manifest THIS invocation was asked to prove, its bound nonce
140
+ * and the independently observed process exit code, decide the
141
+ * verdict from the report alone. `killedFile` short-circuits every report-shaped check — a job this
142
+ * module killed for a per-file hang never reaches its report.
143
+ */
144
+ export function verifyManifestReport(opts) {
145
+ const { manifest, nonce, exitCode, report, killedFile, hangBudgetMs } = opts;
146
+ if (killedFile !== undefined) {
147
+ return {
148
+ kind: "infra",
149
+ pass: false,
150
+ details: `infra hang: "${killedFile}" started and did not complete within its ${hangBudgetMs}ms budget — killed, its process group is gone`,
151
+ meta: { classification: "infra", infra: true, kind: "hang", file: killedFile, hangBudgetMs },
152
+ };
153
+ }
154
+ if (!report) {
155
+ return {
156
+ kind: "fail-closed",
157
+ pass: false,
158
+ details: `exit ${exitCode ?? "unknown"} with no invocation-bound test report — a gate that trusts the exit code alone is exactly what this gate refuses to be; failing closed naming the missing report`,
159
+ meta: { classification: "infra", infra: true, missingReport: true },
160
+ };
161
+ }
162
+ if (report.nonce !== nonce) {
163
+ return {
164
+ kind: "fail-closed",
165
+ pass: false,
166
+ details: `test report carries nonce "${report.nonce}" — this invocation's nonce is "${nonce}"; a report from another invocation is never a verdict, failing closed`,
167
+ meta: { classification: "infra", infra: true, staleNonce: report.nonce, expectedNonce: nonce },
168
+ };
169
+ }
170
+ const requestedCounts = new Map();
171
+ for (const f of report.requested)
172
+ requestedCounts.set(f, (requestedCounts.get(f) ?? 0) + 1);
173
+ const duplicated = [...requestedCounts.entries()].find(([, n]) => n > 1)?.[0];
174
+ if (duplicated !== undefined) {
175
+ return {
176
+ kind: "fail-closed",
177
+ pass: false,
178
+ details: `test report names "${duplicated}" more than once in its requested set — a file counted twice is not a trustworthy completion record; failing closed`,
179
+ meta: { classification: "infra", infra: true, duplicateFile: duplicated },
180
+ };
181
+ }
182
+ if (report.duplicateCompletions?.length) {
183
+ const dup = report.duplicateCompletions[0];
184
+ return {
185
+ kind: "fail-closed",
186
+ pass: false,
187
+ details: `"${dup}" completed more than once in this report — a duplicate completion record (evidence the reporter preserved despite the later write overwriting it) is never a verdict; failing closed`,
188
+ meta: { classification: "infra", infra: true, duplicateCompletion: dup },
189
+ };
190
+ }
191
+ const uncorroborated = Object.keys(report.completed).find((f) => !(f in report.started) || !report.requested.includes(f));
192
+ if (uncorroborated !== undefined) {
193
+ return {
194
+ kind: "fail-closed",
195
+ pass: false,
196
+ details: `"${uncorroborated}" is marked completed without a matching requested/started record — an unattributed completion is never a verdict; failing closed`,
197
+ meta: { classification: "infra", infra: true, unattributedCompletion: uncorroborated },
198
+ };
199
+ }
200
+ if (!report.certificate) {
201
+ const pending = manifest.find((f) => f in report.started && !(f in report.completed));
202
+ return {
203
+ kind: "infra",
204
+ pass: false,
205
+ details: pending !== undefined
206
+ ? `the job was killed after its last file record but before its terminal certificate — never a verdict; last incomplete file was "${pending}"`
207
+ : `the job was killed after its last file record but before its terminal certificate — never a verdict`,
208
+ meta: { classification: "infra", infra: true, missingCertificate: true, ...(pending !== undefined ? { file: pending } : {}) },
209
+ };
210
+ }
211
+ const failures = Object.values(report.completed).filter((c) => c.status === "failed")
212
+ .flatMap((c) => c.failures?.length ? c.failures : ["<unnamed failure>"]);
213
+ if (failures.length)
214
+ return { kind: "work", pass: false,
215
+ details: `test report names failing fingerprint(s):\n${failures.join("\n")}`,
216
+ meta: { classification: "regression", failingTests: failures, processExit: exitCode } };
217
+ if (exitCode === undefined) {
218
+ return {
219
+ kind: "fail-closed",
220
+ pass: false,
221
+ details: `the process did not terminate with an exit code (likely killed by a signal) though a certificate naming exit ${report.certificate.exitCode} is present — a passing verdict requires a clean process termination; failing closed naming both`,
222
+ meta: { classification: "infra", infra: true, missingProcessExit: true, certificateExit: report.certificate.exitCode },
223
+ };
224
+ }
225
+ if (report.certificate.exitCode !== exitCode) {
226
+ return {
227
+ kind: "fail-closed",
228
+ pass: false,
229
+ details: `report certificate names exit ${report.certificate.exitCode} but the process exited ${exitCode} — a report that contradicts the exit code is never a verdict; failing closed naming both`,
230
+ meta: { classification: "infra", infra: true, certificateExit: report.certificate.exitCode, processExit: exitCode },
231
+ };
232
+ }
233
+ if (exitCode !== 0)
234
+ return {
235
+ kind: "fail-closed", pass: false,
236
+ details: `report has no failed tests but the process exited ${exitCode}; failing closed naming the successful report and nonzero exit`,
237
+ meta: { classification: "infra", infra: true, processExit: exitCode },
238
+ };
239
+ const unexpectedRequested = report.requested.find((f) => !manifest.includes(f));
240
+ const unexpectedStarted = Object.keys(report.started).find((f) => !report.requested.includes(f) || !manifest.includes(f));
241
+ if (unexpectedRequested ?? unexpectedStarted) {
242
+ const file = unexpectedRequested ?? unexpectedStarted;
243
+ return {
244
+ kind: "fail-closed", pass: false,
245
+ details: `"${file}" is outside the invocation's requested/start lifecycle — a report must certify exactly this manifest; failing closed`,
246
+ meta: { classification: "infra", infra: true, unexpectedLifecycleFile: file },
247
+ };
248
+ }
249
+ const pending = manifest.find((f) => f in report.started && !(f in report.completed));
250
+ if (pending !== undefined) {
251
+ return {
252
+ kind: "infra",
253
+ pass: false,
254
+ details: `"${pending}" started and never completed — an infra result naming the file`,
255
+ meta: { classification: "infra", infra: true, file: pending, incomplete: true },
256
+ };
257
+ }
258
+ const missing = manifest.find((f) => !(f in report.completed));
259
+ if (missing !== undefined) {
260
+ return {
261
+ kind: "infra",
262
+ pass: false,
263
+ details: `"${missing}" is in the manifest but absent from the report — an infra result naming the file`,
264
+ meta: { classification: "infra", infra: true, file: missing, missingFromReport: true },
265
+ };
266
+ }
267
+ return {
268
+ kind: "pass",
269
+ pass: true,
270
+ details: `invocation-bound test report succeeded — ${manifest.length} manifest file(s) present exactly once`,
271
+ meta: { manifestFiles: manifest.length },
272
+ };
273
+ }
274
+ /** How much longer than its baseline measurement one file may legitimately run before it is a hang. */
275
+ export const FILE_HANG_SLACK = 3;
276
+ export const DEFAULT_FILE_HANG_BUDGET_MS = 60_000;
277
+ const usable = (n) => n !== undefined && Number.isFinite(n) && n > 0;
278
+ export function fileHangBudgetMs(file, baselineDurations, ceilingMs = DEFAULT_FILE_HANG_BUDGET_MS, longestFile) {
279
+ const ceiling = usable(ceilingMs) ? ceilingMs : DEFAULT_FILE_HANG_BUDGET_MS;
280
+ const timings = baselineDurations?.filter((d) => usable(d.durationMs)) ?? [];
281
+ const longest = Math.max(0, ...timings.map((d) => d.durationMs), usable(longestFile?.durationMs) ? longestFile.durationMs : 0);
282
+ if (!longest)
283
+ return ceiling;
284
+ const known = timings.find((d) => d.file === file)?.durationMs;
285
+ return Math.min(ceiling, known === undefined ? FILE_HANG_SLACK * longest : Math.max(FILE_HANG_SLACK * known, longest));
286
+ }
287
+ /** Supervise the configured command and poll the runner's atomic lifecycle snapshots. Every
288
+ * timeout kills the detached process group, including descendants holding the output pipes. */
289
+ export function runManifestedTest(cmd, cwd, opts) {
290
+ const pollMs = opts.pollMs ?? 20;
291
+ const overallCeilingMs = usable(opts.overallCeilingMs) ? opts.overallCeilingMs : DEFAULT_FILE_HANG_BUDGET_MS;
292
+ const env = { ...(opts.env ?? process.env), TICKMARKR_TEST_REPORT: opts.reportPath, TICKMARKR_TEST_NONCE: opts.nonce };
293
+ const controller = new AbortController();
294
+ let pid;
295
+ let killedFile;
296
+ let hangBudgetMs;
297
+ let poll;
298
+ const checkHang = (atCeiling = false) => {
299
+ const report = readTestReport(opts.reportPath);
300
+ if (!report || report.nonce !== opts.nonce)
301
+ return;
302
+ const now = Date.now();
303
+ for (const file of opts.manifest) {
304
+ const startedAt = report.started[file];
305
+ if (startedAt === undefined || file in report.completed)
306
+ continue;
307
+ const budget = fileHangBudgetMs(file, opts.baselineDurations, overallCeilingMs, opts.longestFile);
308
+ if (now - startedAt >= budget || (atCeiling && budget === overallCeilingMs)) {
309
+ killedFile = file;
310
+ hangBudgetMs = budget;
311
+ if (!atCeiling)
312
+ controller.abort();
313
+ return;
314
+ }
315
+ }
316
+ };
317
+ return shell(cmd, cwd, overallCeilingMs, false, {
318
+ env, signal: controller.signal, onTimeout: () => checkHang(true),
319
+ onSpawn: (childPid) => {
320
+ pid = childPid;
321
+ // Neither the per-file clocks nor the command ceiling includes lease queue time.
322
+ clearInterval(poll);
323
+ poll = setInterval(checkHang, pollMs);
324
+ },
325
+ }).then((result) => ({
326
+ exitCode: result.signalExit ? undefined : result.code,
327
+ stdout: result.stdout, stderr: result.stderr,
328
+ report: readTestReport(opts.reportPath), killedFile, hangBudgetMs, pid,
329
+ })).finally(() => { clearInterval(poll); });
330
+ }
331
+ /** One configured runner execution, and its own collection under the same arguments and environment.
332
+ * The installed runner is trusted (R28 add.1 option B); the nonce catches stale artifacts, not forgery. */
333
+ export async function evaluateManifestedTest(cmd, cwd, opts) {
334
+ const dir = opts.artifactDir ?? mkdtempSync(join(tmpdir(), "tickmarkr-test-report-"));
335
+ const nonce = randomBytes(16).toString("hex");
336
+ const reportPath = join(dir, `test-manifest-report-${nonce}.json`);
337
+ const reporterPath = join(dir, `test-reporter-${nonce}.mjs`);
338
+ let spawnedCommand = cmd;
339
+ const env = { ...process.env, PATH: `${join(cwd, "node_modules/.bin")}:${process.env.PATH ?? ""}`,
340
+ [FORK_CAP_ENV]: String(resolvedCapacity().forkCap), [SUITE_PARENT_ENV]: String(process.pid) };
341
+ for (const key of ROUTING_ENV_SEAMS)
342
+ delete env[key];
343
+ // A gate can itself be tested under Vitest. Do not give the child the outer worker identity.
344
+ for (const key of Object.keys(env))
345
+ if (["VITEST", "TEST", "VITEST_WORKER_ID", "VITEST_POOL_ID"].includes(key))
346
+ delete env[key];
347
+ try {
348
+ const invocation = runnerInvocation(cmd, cwd);
349
+ const listed = await runManifestedTest(invocation.listing, cwd, {
350
+ manifest: [], nonce, reportPath: join(dir, `listing-${nonce}.json`), env,
351
+ overallCeilingMs: opts.overallCeilingMs ?? DEFAULT_FILE_HANG_BUDGET_MS,
352
+ });
353
+ if (listed.exitCode !== 0)
354
+ throw new Error(`vitest cannot list files (exit ${listed.exitCode ?? "signal"}): ${listed.stderr || listed.stdout}`);
355
+ let files;
356
+ try {
357
+ const rows = JSON.parse(listed.stdout.slice(listed.stdout.indexOf("[")));
358
+ if (!Array.isArray(rows) || !rows.every((r) => typeof r?.file === "string"))
359
+ throw new Error("invalid listing");
360
+ files = [...new Set(rows.map((r) => toManifestPath(r.file, cwd)))].sort();
361
+ }
362
+ catch {
363
+ throw new Error(`vitest cannot list files: invalid JSON listing: ${listed.stdout}`);
364
+ }
365
+ if (!files.length)
366
+ throw new Error("vitest cannot list files: empty manifest");
367
+ writeFileSync(reporterPath, TEST_REPORTER_SOURCE);
368
+ spawnedCommand = `${cmd}${invocation.separator} --reporter=${shq(reporterPath)} --outputFile=${shq(reportPath)}`;
369
+ const invoked = await runManifestedTest(spawnedCommand, cwd, {
370
+ manifest: files, nonce, reportPath, env,
371
+ baselineDurations: opts.baselineDurations?.map((d) => ({ ...d, file: toManifestPath(d.file, cwd) })),
372
+ longestFile: opts.longestFile,
373
+ overallCeilingMs: opts.overallCeilingMs ?? DEFAULT_FILE_HANG_BUDGET_MS,
374
+ pollMs: 20,
375
+ });
376
+ const verdict = verifyManifestReport({ manifest: files, nonce, exitCode: invoked.exitCode,
377
+ report: invoked.report, killedFile: invoked.killedFile, hangBudgetMs: invoked.hangBudgetMs });
378
+ return { pass: verdict.pass, kind: verdict.kind,
379
+ details: verdict.details + (!invoked.report && invoked.stderr ? `\nvitest reporter: ${invoked.stderr}` : ""),
380
+ classification: verdict.meta.classification,
381
+ meta: { ...verdict.meta, nonce, manifest: files, spawnedCommand, processExit: invoked.exitCode, pid: invoked.pid },
382
+ exitCode: invoked.exitCode ?? -1, reportPath };
383
+ }
384
+ catch (error) {
385
+ return { pass: false, kind: "infra", classification: "infra", exitCode: -1, reportPath,
386
+ details: error instanceof Error ? error.message : String(error),
387
+ meta: { classification: "infra", infra: true, manifestDiscoveryFailed: true, spawnedCommand } };
388
+ }
389
+ }
@@ -0,0 +1,4 @@
1
+ /** Loaded by the configured Vitest process. Atomic snapshots expose live lifecycle records to
2
+ * the supervisor; only onTestRunEnd emits the terminal record. This is a completeness protocol,
3
+ * not authentication against the installed runner (which the gate already trusts). */
4
+ export declare const TEST_REPORTER_SOURCE: string;
@@ -0,0 +1,49 @@
1
+ /** Loaded by the configured Vitest process. Atomic snapshots expose live lifecycle records to
2
+ * the supervisor; only onTestRunEnd emits the terminal record. This is a completeness protocol,
3
+ * not authentication against the installed runner (which the gate already trusts). */
4
+ export const TEST_REPORTER_SOURCE = String.raw `
5
+ import { writeFileSync, renameSync, realpathSync } from 'node:fs';
6
+ import { relative, sep } from 'node:path';
7
+ export default class TickmarkrReporter {
8
+ constructor() {
9
+ this.report = { nonce: process.env.TICKMARKR_TEST_NONCE, requested: [], started: {}, completed: {}, duplicateCompletions: [] };
10
+ this.path = process.env.TICKMARKR_TEST_REPORT;
11
+ this.cwd = realpathSync(process.cwd());
12
+ }
13
+ file(module) { return relative(this.cwd, module.moduleId).split(sep).join('/'); }
14
+ save() {
15
+ writeFileSync(this.path + '.tmp', JSON.stringify(this.report));
16
+ renameSync(this.path + '.tmp', this.path);
17
+ }
18
+ onTestRunStart(specifications) {
19
+ this.report.requested = specifications.map(s => this.file(s));
20
+ this.save();
21
+ }
22
+ onTestModuleStart(module) {
23
+ this.report.started[this.file(module)] = Date.now();
24
+ this.save();
25
+ }
26
+ onTestModuleEnd(module) {
27
+ const file = this.file(module);
28
+ const failed = module.state() === 'failed';
29
+ const failures = [];
30
+ if (failed) {
31
+ for (const test of module.children.allTests()) {
32
+ if (test.result().state === 'failed') {
33
+ const errors = test.result().errors || [];
34
+ failures.push(...(errors.length ? errors.map(e => 'FAIL ' + file + ' > ' + test.fullName + ': ' + e.message) : ['FAIL ' + file + ' > ' + test.fullName]));
35
+ }
36
+ }
37
+ if (!failures.length) failures.push('FAIL ' + file);
38
+ }
39
+ if (file in this.report.completed) this.report.duplicateCompletions.push(file);
40
+ this.report.completed[file] = { at: Date.now(), status: failed ? 'failed' : 'passed', failures };
41
+ this.save();
42
+ }
43
+ onTestRunEnd(modules, errors, reason) {
44
+ const failed = reason === 'failed' || errors.length > 0 || Object.values(this.report.completed).some(c => c.status === 'failed');
45
+ this.report.certificate = { at: Date.now(), exitCode: failed ? 1 : 0 };
46
+ this.save();
47
+ }
48
+ }
49
+ `;
@@ -28,10 +28,31 @@ export declare function preferRanks(c: {
28
28
  shape: string;
29
29
  rank: number;
30
30
  }[];
31
+ export declare function isExplicitIdOfAliasFamily(candidateModel: string, alias: string): boolean;
32
+ export declare function entryMatchesChannel(entry: string, c: {
33
+ adapter: string;
34
+ model: string;
35
+ identity?: string;
36
+ }, allowFamilyMatching?: boolean): boolean;
37
+ export interface ExclusionScope {
38
+ scope: string;
39
+ path: string;
40
+ configPath: string;
41
+ entry: string;
42
+ by: "deny" | "allow";
43
+ }
44
+ export declare function exclusionCollector(c: {
45
+ adapter: string;
46
+ model: string;
47
+ identity?: string;
48
+ }, routingOrCfg: TickmarkrConfig["routing"] | TickmarkrConfig, role?: PreferenceRole): ExclusionScope[];
49
+ export declare const collectExclusions: typeof exclusionCollector;
50
+ export declare const exclusionProvenance: typeof exclusionCollector;
31
51
  export declare function disallowedBy(c: {
32
52
  adapter: string;
33
53
  model: string;
34
- }, routing: TickmarkrConfig["routing"], role?: PreferenceRole): Disallowed | null;
54
+ identity?: string;
55
+ }, routingOrCfg: TickmarkrConfig["routing"] | TickmarkrConfig, role?: PreferenceRole): Disallowed | null;
35
56
  export interface DenyPreferCollision {
36
57
  kind: "prefer" | "pin" | "pool";
37
58
  shape: string;
@@ -5,19 +5,24 @@ const PREFERENCE_ROLES = ["worker", "judge", "review", "consult"];
5
5
  /** Provider identity comes from the served model, not a gateway adapter's stamped vendor. */
6
6
  export function modelProvider(model, fallback = "unknown") {
7
7
  const id = model.toLowerCase();
8
- const prefix = id.includes("/") ? id.slice(0, id.indexOf("/")) : "";
9
- if (prefix === "openai" || prefix === "openai-codex" || /^(?:gpt|o\d)/.test(id))
8
+ const sepIdx = id.search(/[/:\s]/);
9
+ const prefix = sepIdx !== -1 ? id.slice(0, sepIdx) : "";
10
+ const remainder = sepIdx !== -1 ? id.slice(sepIdx + 1) : id;
11
+ if (prefix === "openai" || prefix === "openai-codex" || /^(?:gpt|o\d)/.test(remainder))
10
12
  return "openai";
11
- if (prefix === "anthropic" || /^(?:claude|opus|sonnet|haiku|fable)(?:-|$)/.test(id))
13
+ if (prefix === "anthropic" || /^(?:claude|opus|sonnet|haiku|fable)(?:-|$)/.test(remainder))
12
14
  return "anthropic";
13
- if (prefix === "google" || /^gemini(?:-|$)/.test(id))
15
+ if (prefix === "google" || /^gemini(?:-|$)/.test(remainder))
14
16
  return "google";
15
- if (prefix === "xai" || /^grok(?:-|$)/.test(id))
17
+ if (prefix === "xai" || prefix === "xai-oauth" || /^(?:grok|xai)(?:-|$)/.test(remainder) || id.startsWith("xai-oauth"))
16
18
  return "xai";
17
- if (["zai", "zhipu", "zai-coding-plan"].includes(prefix) || /^glm(?:-|$)/.test(id))
19
+ if (["zai", "zhipu", "zai-coding-plan"].includes(prefix) || /^glm(?:-|$)/.test(remainder))
18
20
  return "zhipu";
19
- if (["kimi-code", "moonshot"].includes(prefix) || /^kimi(?:-|$)/.test(id))
21
+ if (["kimi-code", "moonshot"].includes(prefix) || /^kimi(?:-|$)/.test(remainder))
20
22
  return "moonshot";
23
+ const normalizedFallback = fallback.toLowerCase();
24
+ if (normalizedFallback === "xai-oauth" || normalizedFallback === "xai")
25
+ return "xai";
21
26
  return fallback;
22
27
  }
23
28
  // Router naming remains explicit while sharing the review gate's exact function object.
@@ -51,7 +56,9 @@ export function excludedChannels(cfg, adapters, health) {
51
56
  if (!health[id]?.installed || !health[id]?.authed)
52
57
  continue;
53
58
  for (const c of channelsFromConfig(id, cfg)) {
54
- const d = disallowedBy(c, cfg.routing);
59
+ const identity = health[id]?.modelAuth?.[c.model]?.identity ?? health[id]?.modelIdentities?.[c.model];
60
+ const chan = identity ? { ...c, identity } : c;
61
+ const d = disallowedBy(chan, cfg.routing);
55
62
  if (d)
56
63
  out.push({ key: channelKey(c), d });
57
64
  }
@@ -75,25 +82,116 @@ export function preferRanks(c, cfg) {
75
82
  }
76
83
  return out;
77
84
  }
78
- // entries in either list accept adapter id, model id, or adapter:model (grammar A1)
79
- export function disallowedBy(c, routing, role = "worker") {
80
- const { allow, deny } = routing;
81
- const matches = (e) => e === c.adapter || e === c.model || e === channelKey(c);
82
- const workerDeny = role === "worker" ? deny?.workers : undefined;
83
- const d = [
84
- ...(deny?.adapters ?? []),
85
- ...(deny?.models ?? []),
86
- ...(workerDeny?.adapters ?? []),
87
- ...(workerDeny?.models ?? []),
88
- ].find(matches);
89
- if (d)
90
- return { by: "deny", entry: d };
85
+ export function isExplicitIdOfAliasFamily(candidateModel, alias) {
86
+ if (candidateModel === alias)
87
+ return true;
88
+ const tokens = candidateModel.toLowerCase().split(/[^a-z0-9]+/);
89
+ return tokens.includes(alias.toLowerCase());
90
+ }
91
+ export function entryMatchesChannel(entry, c, allowFamilyMatching = false) {
92
+ if (entry === c.adapter)
93
+ return true;
94
+ if (entry === c.model)
95
+ return true;
96
+ if (entry === channelKey(c))
97
+ return true;
98
+ if (c.identity) {
99
+ if (entry === c.identity)
100
+ return true;
101
+ if (entry === `${c.adapter}:${c.identity}`)
102
+ return true;
103
+ }
104
+ if (!c.identity && allowFamilyMatching) {
105
+ const colon = entry.indexOf(":");
106
+ if (colon !== -1) {
107
+ const entryAdapter = entry.slice(0, colon);
108
+ const entryModel = entry.slice(colon + 1);
109
+ if (entryAdapter === c.adapter && isExplicitIdOfAliasFamily(entryModel, c.model)) {
110
+ return true;
111
+ }
112
+ }
113
+ else {
114
+ if (isExplicitIdOfAliasFamily(entry, c.model)) {
115
+ return true;
116
+ }
117
+ }
118
+ }
119
+ return false;
120
+ }
121
+ export function exclusionCollector(c, routingOrCfg, role = "worker") {
122
+ const routing = "routing" in routingOrCfg ? routingOrCfg.routing : routingOrCfg;
123
+ const out = [];
124
+ const { allow, deny } = routing ?? {};
125
+ for (const entry of deny?.adapters ?? []) {
126
+ if (entryMatchesChannel(entry, c, true)) {
127
+ out.push({
128
+ scope: "routing.deny.adapters",
129
+ path: "routing.deny.adapters",
130
+ configPath: "routing.deny.adapters",
131
+ entry,
132
+ by: "deny",
133
+ });
134
+ }
135
+ }
136
+ for (const entry of deny?.models ?? []) {
137
+ if (entryMatchesChannel(entry, c, true)) {
138
+ out.push({
139
+ scope: "routing.deny.models",
140
+ path: "routing.deny.models",
141
+ configPath: "routing.deny.models",
142
+ entry,
143
+ by: "deny",
144
+ });
145
+ }
146
+ }
147
+ if (role === "worker") {
148
+ for (const entry of deny?.workers?.adapters ?? []) {
149
+ if (entryMatchesChannel(entry, c, true)) {
150
+ out.push({
151
+ scope: "routing.deny.workers.adapters",
152
+ path: "routing.deny.workers.adapters",
153
+ configPath: "routing.deny.workers.adapters",
154
+ entry,
155
+ by: "deny",
156
+ });
157
+ }
158
+ }
159
+ for (const entry of deny?.workers?.models ?? []) {
160
+ if (entryMatchesChannel(entry, c, true)) {
161
+ out.push({
162
+ scope: "routing.deny.workers.models",
163
+ path: "routing.deny.workers.models",
164
+ configPath: "routing.deny.workers.models",
165
+ entry,
166
+ by: "deny",
167
+ });
168
+ }
169
+ }
170
+ }
91
171
  if (allow) {
92
- const entries = [...(allow.adapters ?? []), ...(allow.models ?? [])];
93
- if (!entries.some(matches))
94
- return { by: "allow", entry: entries.join(", ") || "(empty allowlist)" };
172
+ const allAllow = [...(allow.adapters ?? []), ...(allow.models ?? [])];
173
+ const admitted = allAllow.some((e) => entryMatchesChannel(e, c, false));
174
+ if (!admitted) {
175
+ out.push({
176
+ scope: "routing.allow",
177
+ path: "routing.allow",
178
+ configPath: "routing.allow",
179
+ entry: allAllow.join(", ") || "(empty allowlist)",
180
+ by: "allow",
181
+ });
182
+ }
95
183
  }
96
- return null;
184
+ return out;
185
+ }
186
+ export const collectExclusions = exclusionCollector;
187
+ export const exclusionProvenance = exclusionCollector;
188
+ // entries in either list accept adapter id, model id, or adapter:model (grammar A1)
189
+ export function disallowedBy(c, routingOrCfg, role = "worker") {
190
+ const exclusions = exclusionCollector(c, routingOrCfg, role);
191
+ if (exclusions.length === 0)
192
+ return null;
193
+ const first = exclusions[0];
194
+ return { by: first.by, entry: first.entry };
97
195
  }
98
196
  // Delegates to route()'s preflightPrefer — doctor/resume preflight reuses the router grammar,
99
197
  // not a second colon/tier/model parser of the same config.