tickmarkr 1.84.0 → 1.85.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -14,16 +14,76 @@ const PASS_LINE_RE = /^\s*(?:(?:[\w@./-]+:\s*)*[✓✔]|(?:\[tickmarkr\]\s+)?(?:
14
14
  // output to headline it. \s is fine in a TS regex — the BSD [[:space:]] rule binds shell grep only.
15
15
  // OBS-42: vitest's diagnostic failure headings are shared anchors for baseline and tip verification.
16
16
  const FAIL_ANCHOR_RE = /^\s*(?:FAIL\s+|[^\w]*(?:Unhandled Errors|Uncaught Exception)\b)/;
17
- const SUMMARY_FAIL_RE = /^\s*Tests?\s+(?:Files?\s+)?\d+\s+failed/; // " Tests N failed | M passed (T)"
17
+ // OBS-278: a failure fingerprint is a SHAPE — the way a runner reports a failure — never error/fail
18
+ // vocabulary. A status surface that draws those words (zone +N, run attempt N, "1 failed") used to
19
+ // fingerprint as a fresh failure on every attempt, rejecting a task for rendering what it was chartered
20
+ // to render. Digits are written (?:\d+|#) so a shape matches both raw and digit-normalized lines.
21
+ // Run summaries: " Tests N failed | M passed (T)" (vitest), "# fail N" (TAP / node:test),
22
+ // "ℹ fail N" (node:test's spec reporter) and "test result: FAILED. …" (cargo / libtest).
23
+ const SUMMARY_FAIL_RE = /^\s*(?:Tests?\s+(?:Files?\s+)?(?:\d+|#)\s+failed|#\s+fail\s+(?!0\b)(?:\d+|#)\b|ℹ\s+fail\s+(?!0\b)(?:\d+|#)\b|test result:\s+FAILED\b)/;
24
+ const ERROR_ANCHOR_RE = /^\s*(?:Error|[A-Za-z_$][\w$]*Error):\s+\S/;
25
+ const TSC_ERROR_RE = /^\s*\S.*\((?:\d+|#),(?:\d+|#)\):\s+error\s+[A-Z]+(?:\d+|#):/i; // tsc
26
+ const LINTER_ERROR_RE = /^\s*(?:\d+|#):(?:\d+|#)\s+error\s+\S/; // eslint stylish
27
+ // The failure shapes non-Vitest runners name their tests with: pytest's short summary
28
+ // (`FAILED tests/t.py::test_x - AssertionError`, `ERROR tests/t.py::fixture`), go test
29
+ // (`--- FAIL: TestFoo (0.00s)`), TAP / node:test (`not ok 1 - name`, whose summary line lands in
30
+ // SUMMARY_FAIL_RE) and python unittest's failure-block header (`FAIL: test_x (mod.Class.test_x)`,
31
+ // verbatim capture, python3 -m unittest -v). Each anchors the runner's token at line START; the
32
+ // pytest/go forms also require a file/test identifier after it (`::`, a path separator, or an
33
+ // extension) — so a surface drawing "FAILED" mid-line, or a plain sentence like "FAILED to reach
34
+ // the zone", still matches nothing. Without these a genuinely new pytest/go/TAP/unittest failure on
35
+ // an already-red baseline was forgiven as pre-existing, because only shaped lines can reject.
36
+ const RUNNER_FAIL_RE = /^\s*(?:(?:FAILED|ERROR)\s+\S*(?:::|[/\\]|\.[A-Za-z]\w*\b)|FAIL:\s+\S|---\s+FAIL:\s+\S|not ok\b)/;
37
+ // The other half of the position rule: runners that put the verdict LAST. cargo/libtest names each
38
+ // failing test as `test tests::name ... FAILED` (verbatim capture, cargo 1.95.0) and details it with
39
+ // `thread 'tests::name' (81651804) panicked at src/lib.rs:7:24:`; python unittest -v writes
40
+ // `test_x (mod.Class.test_x) ... FAIL` (and `... ERROR`) — the same rule with the runner's
41
+ // qualified-name token between identifier and separator, and the short verdict spelling (verbatim
42
+ // capture, python3 -m unittest -v). Recognition is positional, not a vendor name: an identifier, the
43
+ // runner's own separator, then the verdict ENDING the line. A drawn status strip ("│ ✗ tip-verify
44
+ // FAILED · zone +3 │") has the verdict mid-line inside chrome, so it matches nothing here — which is
45
+ // why this can generalize without re-opening OBS-278.
46
+ const TRAILING_FAIL_RE = /^\s*(?:test\s+)?\S+(?:\s+\([^)]*\))?\s+(?:\.{3}|-{3,})\s+(?:FAIL(?:ED)?|ERROR)\s*$|^\s*thread\s+'[^']*'\s+.*panicked at\s+\S/;
47
+ // node:test's spec reporter speaks glyphs, not words: a failure is named by a leading ✖ (verbatim
48
+ // capture, Node v22 `--test-reporter=spec`: `✖ old failure (0.585875ms)`, totalled as `ℹ fail 1` in
49
+ // SUMMARY_FAIL_RE) — the exact counterpart of the ✓/✔ pass markers PASS_LINE_RE drops, with the same
50
+ // optional label prefixes. Recognition is positional — the runner's own marker at line start — so any
51
+ // runner sharing the glyph protocol is read without being enumerated; a status strip drawing ✖
52
+ // mid-line inside chrome matches nothing, the same position rule as the other shapes.
53
+ const GLYPH_FAIL_RE = /^\s*(?:(?:[\w@./-]+:\s*)*)✖\s+\S/;
54
+ // Lines that NAME a failing test — the ones worth headlining to the operator. One list, so recognition
55
+ // and reporting cannot drift apart (a shape that blocks but never gets named cost 3 attempts once).
56
+ const namesFailure = (l) => FAIL_ANCHOR_RE.test(l) || RUNNER_FAIL_RE.test(l) || TRAILING_FAIL_RE.test(l) || GLYPH_FAIL_RE.test(l);
57
+ const isFailureShaped = (l) => namesFailure(l) || SUMMARY_FAIL_RE.test(l) || ERROR_ANCHOR_RE.test(l)
58
+ || TSC_ERROR_RE.test(l) || LINTER_ERROR_RE.test(l);
59
+ const VOCAB_RE = /\b(?:error|fail(?:ed|ure|ing)?)\b/i;
18
60
  const normalizeLine = (l) => l.replace(/\d+/g, "#").replace(/\s+/g, " ").trim();
61
+ // A failing command whose output holds no shape any runner here names. The marker is content-free and
62
+ // constant: downstream consumers (tip verify journals fingerprint counts) still see that the command
63
+ // failed, while no line of the output — narration, status strip, stack trace — can enter the set. Being
64
+ // constant it is identical on every attempt, so it can never surface as a fresh fingerprint either.
65
+ export const UNRECOGNIZED_FAILURE = "<unrecognized failure output>";
66
+ // OBS-278 dies here: only a failure SHAPE is fingerprintable. Vocabulary is not a shape — a surface that
67
+ // renders "zone +3 · run attempt 2 · 1 failed" (what T9 is chartered to draw) contributes nothing, with
68
+ // or without box glyphs, so it cannot manufacture a fresh-fingerprint rejection on any future attempt.
19
69
  export function fingerprint(output) {
20
70
  const lines = output
21
71
  .split("\n")
22
72
  .map((l) => l.replace(ANSI_RE, ""))
23
- .filter((l) => !PASS_LINE_RE.test(l) && (FAIL_ANCHOR_RE.test(l) || /\b(error|fail(ed|ure|ing)?)\b/i.test(l)))
24
- .map(normalizeLine);
25
- return [...new Set(lines)];
73
+ .filter((l) => !PASS_LINE_RE.test(l));
74
+ const shaped = lines.filter(isFailureShaped);
75
+ if (!shaped.length)
76
+ return lines.some((l) => l.trim()) ? [UNRECOGNIZED_FAILURE] : [];
77
+ return [...new Set(shaped.map(normalizeLine))];
26
78
  }
79
+ // The diagnostic channel for a runner we cannot read: raw output lines, never fingerprints, never a
80
+ // verdict. They exist so the operator sees SOMETHING when a green command turns red unrecognizably.
81
+ const unrecognizedEvidence = (raw) => raw
82
+ .split("\n")
83
+ .map((l) => l.replace(ANSI_RE, "").trimEnd())
84
+ .filter((l) => VOCAB_RE.test(l))
85
+ .slice(0, 10)
86
+ .join("\n");
27
87
  // stored baselines may predate ANSI/pass-marker hardening — renormalize at compare time so existing
28
88
  // on-disk baseline.json files stay comparable without recapture (compat invariant, CLAUDE.md)
29
89
  const renormalize = (fp) => normalizeLine(fp.replace(ANSI_RE, ""));
@@ -68,7 +128,9 @@ export async function captureBaseline(cwd, commands) {
68
128
  // ponytail: strip the executing cwd so repo-root capture and worktree compare fingerprint identically; /private-vs-/tmp symlink variance is out of scope
69
129
  base.commands[name] = {
70
130
  exitCode: r.code,
71
- fingerprints: fingerprint((r.stdout + "\n" + r.stderr).split(cwd).join("")),
131
+ // a command that exits 0 has no failures to fingerprint — recording any would be a lie the
132
+ // compare step then has to forgive
133
+ fingerprints: r.code === 0 ? [] : fingerprint((r.stdout + "\n" + r.stderr).split(cwd).join("")),
72
134
  missingCommand: missingConfiguredCommand(cmd, r),
73
135
  };
74
136
  }
@@ -119,12 +181,12 @@ function headlineDetails(raw, fresh) {
119
181
  const headlines = raw
120
182
  .split("\n")
121
183
  .map((l) => l.replace(ANSI_RE, ""))
122
- .filter((l) => FAIL_ANCHOR_RE.test(l) || SUMMARY_FAIL_RE.test(l));
184
+ .filter((l) => namesFailure(l) || SUMMARY_FAIL_RE.test(l));
123
185
  if (!headlines.length)
124
186
  return { details: `new failures vs baseline:\n${fresh.join("\n")}` };
125
187
  return {
126
188
  details: `failing tests:\n${headlines.join("\n")}\n\nnew failure fingerprints vs baseline (secondary):\n${fresh.join("\n")}`,
127
- meta: { failingTests: headlines.filter((l) => FAIL_ANCHOR_RE.test(l)) },
189
+ meta: { failingTests: headlines.filter(namesFailure) },
128
190
  };
129
191
  }
130
192
  export async function compareToBaseline(cwd, commands, baseline, enabled) {
@@ -145,18 +207,34 @@ export async function compareToBaseline(cwd, commands, baseline, enabled) {
145
207
  const raw = (r.stdout + "\n" + r.stderr).split(cwd).join("");
146
208
  const known = new Set((baseline.commands[name]?.fingerprints ?? []).map(renormalize));
147
209
  // OBS-42: diagnostic headings enrich fingerprints but cannot invalidate legacy baselines.
148
- const fresh = fingerprint(raw).filter((f) => !known.has(f) && (!FAIL_ANCHOR_RE.test(f) || f.startsWith("FAIL ")));
149
- if (!fresh.length && (baseline.commands[name]?.exitCode ?? 1) === 0) {
210
+ const current = fingerprint(raw);
211
+ const fresh = current.filter((f) => !known.has(f) && (!FAIL_ANCHOR_RE.test(f) || f.startsWith("FAIL ")));
212
+ // OBS-278: only a failure SHAPE is a verdict — everything fingerprint() keeps is one, except the
213
+ // unrecognized-output marker, which is evidence for the operator and never grounds to reject.
214
+ // ponytail: ceiling — a runner whose failure output holds no shape above and whose baseline is
215
+ // already red has its new failures forgiven, so forgiveness that rests on the marker SAYS so
216
+ // below rather than reading as a verified green. Raise the ceiling by teaching isFailureShaped
217
+ // that runner's position rule (leading verdict + identifier, or identifier + separator + trailing
218
+ // verdict); loosening back to vocabulary re-opens OBS-278.
219
+ const unreadable = current.includes(UNRECOGNIZED_FAILURE);
220
+ const failing = fresh.filter((f) => f !== UNRECOGNIZED_FAILURE);
221
+ if (!failing.length && (baseline.commands[name]?.exitCode ?? 1) === 0) {
222
+ const closed = `command was green at baseline but now exits ${r.code} with no recognizable failure lines — failing closed`;
223
+ const evidence = unrecognizedEvidence(raw);
150
224
  results.push({
151
225
  gate: name,
152
226
  pass: false,
153
- details: `command was green at baseline but now exits ${r.code} with no recognizable failure lines — failing closed`,
227
+ details: evidence ? `${closed}\nunrecognized output:\n${evidence}` : closed,
154
228
  });
155
229
  continue;
156
230
  }
157
- results.push(fresh.length
158
- ? { gate: name, pass: false, ...headlineDetails(raw, fresh) }
159
- : { gate: name, pass: true, details: `exit ${r.code} but only pre-existing failures (forgiven)` });
231
+ results.push(failing.length
232
+ ? { gate: name, pass: false, ...headlineDetails(raw, failing) }
233
+ : {
234
+ gate: name,
235
+ pass: true,
236
+ details: `exit ${r.code} but only pre-existing failures (forgiven)${unreadable ? " — no failure shape recognized in this output, so a new failure from this runner is invisible to the baseline gate" : ""}`,
237
+ });
160
238
  }
161
239
  return results;
162
240
  }
@@ -27,6 +27,13 @@ export declare function isProtectedEvidence(path: string): boolean;
27
27
  export declare function setAsideReceiptPath(section: string): string | null;
28
28
  /** Replace the content of every section confined to the regenerable frame corpora with a receipt. */
29
29
  export declare function setAsideRegenerableCaptures(diff: string): string;
30
+ /**
31
+ * The paths this task's diff ACTUALLY touched. `-z` so a path carrying spaces or non-ASCII bytes is
32
+ * never mangled by git's quoting, `--no-renames` so a rename reports BOTH sides: a file renamed OUT of
33
+ * the leaf class must be visible to the promotion test, and rename detection would hide the old side.
34
+ */
35
+ export declare function changedPaths(worktree: string, baseRef: string): Promise<string[]>;
36
+ export declare function mirrorsVersionOnly(worktree: string, baseRef: string, path: string): Promise<boolean>;
30
37
  export declare function fetchTaskDiff(worktree: string, baseRef: string): Promise<{
31
38
  full: string;
32
39
  forCap: string;
@@ -1,7 +1,7 @@
1
1
  import { writeFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
- import { channelKey } from "../adapters/types.js";
4
- import { DEFAULT_DIFF_CAP, TIER_RANK } from "../config/config.js";
3
+ import { channelKey, shq } from "../adapters/types.js";
4
+ import { criticalPathHits, DEFAULT_DIFF_CAP, DEFAULT_REVIEW_CRITICAL_PATHS, declaredReviewPolicy, isReviewLeafPath, raiseReviewPolicy, REVIEW_VERSION_MIRRORS, TIER_RANK, } from "../config/config.js";
5
5
  import { renderAcceptanceItem } from "../graph/schema.js";
6
6
  import { getAdapter } from "../adapters/registry.js";
7
7
  import { shOk } from "../run/git.js";
@@ -211,6 +211,34 @@ export function setAsideRegenerableCaptures(diff) {
211
211
  const kindOnly = kindOnlyPaths(sections);
212
212
  return sections.map((s) => setAsideSection(s, kindOnly)).join("");
213
213
  }
214
+ /**
215
+ * The paths this task's diff ACTUALLY touched. `-z` so a path carrying spaces or non-ASCII bytes is
216
+ * never mangled by git's quoting, `--no-renames` so a rename reports BOTH sides: a file renamed OUT of
217
+ * the leaf class must be visible to the promotion test, and rename detection would hide the old side.
218
+ */
219
+ export async function changedPaths(worktree, baseRef) {
220
+ const out = await shOk(`git diff --name-only --no-renames -z '${baseRef}..HEAD'`, worktree);
221
+ return [...new Set(out.split("\0").map((p) => p.trim()).filter(Boolean))].sort();
222
+ }
223
+ /**
224
+ * A root manifest is a version MIRROR only when the bump is all it changed. `package.json` carries the
225
+ * gate commands and the dependency set, so a diff that moves `scripts` or `dependencies` is executable
226
+ * behaviour wearing a leaf-class path — the one thing a path predicate can never see for itself. Every
227
+ * added or removed line must be a `"version":` line (a lockfile bump rewrites several of them); a
228
+ * manifest whose diff cannot be read at all fails closed, out of the class.
229
+ */
230
+ const VERSION_FIELD_LINE_RE = /^[+-]\s*"version":\s*"[^"]*",?\s*$/;
231
+ export async function mirrorsVersionOnly(worktree, baseRef, path) {
232
+ let diff;
233
+ try {
234
+ diff = await shOk(`git diff -U0 '${baseRef}..HEAD' -- ${shq(path)}`, worktree);
235
+ }
236
+ catch {
237
+ return false;
238
+ }
239
+ const changed = diff.split("\n").filter((l) => /^[+-]/.test(l) && !/^(?:\+\+\+|---)/.test(l));
240
+ return changed.length > 0 && changed.every((l) => VERSION_FIELD_LINE_RE.test(l));
241
+ }
214
242
  export async function fetchTaskDiff(worktree, baseRef) {
215
243
  const full = setAsideRegenerableCaptures(await shOk(`git diff '${baseRef}..HEAD'`, worktree));
216
244
  const forCap = setAsideRegenerableCaptures(await shOk(`git diff -U0 '${baseRef}..HEAD'`, worktree));
@@ -269,9 +297,74 @@ export async function reviewGate(task, worktree, baseRef, author, channels, adap
269
297
  // OBS-196: run dir for raw-output persistence on an unparseable verdict; absent (older callers,
270
298
  // direct tests) skips persistence and changes nothing else.
271
299
  artifactDir) {
272
- if (task.complexity < cfg.review.complexityThreshold) {
273
- return { gate: "review", pass: true, details: `skipped — complexity ${task.complexity} < threshold ${cfg.review.complexityThreshold}`, meta: { skipped: true } };
300
+ // R3 (OBS-186): participation is keyed on PATHS. The compiler's assignment comes from the DECLARED
301
+ // files[]; the operator's floor may RAISE it to full and can never lower it. `complexityThreshold` is
302
+ // retired — the branch that returned a green skip on a complexity comparison is gone, and with it the
303
+ // "a law caps complexity at 3, the gate starts at 7" unreachability OBS-186 measured.
304
+ //
305
+ // COLLATERAL this rescoped task closed: the run-gates/daemon participation assertions are rewritten
306
+ // path-keyed, the NamedFake review fixtures carry their own nonce trailer (llm.ts's injection is
307
+ // scoped to adapter id "fake" and a renamed fake is a different adapter to it — the check is not
308
+ // weakened, the fixture is fixed), the merge decision reads `gateSatisfied`, and the daemon writes a
309
+ // parallel round's gate-result rows in GATE_NAMES order (src/run/daemon.ts).
310
+ //
311
+ // That last one is why: retiring the switch makes fixtures that used to SKIP review journal TWO
312
+ // verdict rows per round instead of one, and judge ‖ review publish in COMPLETION order — so the
313
+ // three journal-determinism oracles (tests/run/narration.test.ts's byte comparison and its
314
+ // throwing-sink event-order check, tests/run/notify-identity.test.ts's two-run equality, and this
315
+ // repo's own phase-start/gate-result pairing in tests/run/daemon.test.ts) start seeing a race.
316
+ // LATENT, not introduced: the operator's config has run `complexityThreshold: 0` since 2026-07-31,
317
+ // so production rounds have journaled both siblings all along — only the fixtures were blind to it.
318
+ // Fixed in the ledger rather than in the oracles, because determinism run-to-run is a property of
319
+ // the journal, not of three test files that happen to assert it.
320
+ const declaredPolicy = declaredReviewPolicy(task.files);
321
+ const policy = raiseReviewPolicy(declaredPolicy, cfg.review.policy);
322
+ // PROMOTION: the declared assignment is a claim about paths, and the diff is the evidence. A
323
+ // judge-only task whose diff left the leaf class is reviewed in full — the claim never outranks
324
+ // what actually happened, and an empty diff promotes too (a skip earned by an absence is not earned).
325
+ let promotedBy = null;
326
+ if (policy === "judge-only") {
327
+ const touched = await changedPaths(worktree, baseRef);
328
+ // Two ways a path leaves the leaf class. It is not a member (`docs/tool.ts`, `docs/Makefile`) — or
329
+ // it is a root version mirror whose diff moved more than the version field, which no path predicate
330
+ // can see. `package.json` carries the gate commands, so a scripts edit hiding behind a leaf-class
331
+ // path is exactly the promotion this buys.
332
+ const nonMembers = touched.filter((p) => !isReviewLeafPath(p));
333
+ const impostorMirrors = (await Promise.all(touched.filter((p) => REVIEW_VERSION_MIRRORS.has(p))
334
+ .map(async (p) => await mirrorsVersionOnly(worktree, baseRef, p) ? null : p))).filter((p) => p !== null);
335
+ // The fail-closed backstop for the compile lint. reviewParticipationErrors runs at a repo root the
336
+ // compile seam cannot always name (collateral.ts); THIS gate is handed the run's real config, so a
337
+ // critical path that reached dispatch is reviewed here whatever the lint saw. The shipped defaults
338
+ // are unioned in for the same reason they are there: a config that names none still has a floor.
339
+ const criticalHits = criticalPathHits([...task.files, ...touched], [...new Set([...DEFAULT_REVIEW_CRITICAL_PATHS, ...(cfg.review.criticalPaths ?? [])])]);
340
+ const escaped = [...new Set([...nonMembers, ...impostorMirrors, ...criticalHits])].sort();
341
+ if (touched.length > 0 && escaped.length === 0) {
342
+ // A declined review makes NO green claim. types.ts's T11 note ("pass stays true so enforcement is
343
+ // unchanged") describes the baseline skips — a build/test/lint command the repo never configured.
344
+ // R3 overrules it here: a cross-vendor review that did not run cannot report a pass, so the record
345
+ // carries verdict "skipped" with the policy that declined it and the reason, and pass is never
346
+ // true. The merge-predicate seam this opens is named in the collateral note above.
347
+ return {
348
+ gate: "review",
349
+ pass: false,
350
+ details: `skipped — reviewPolicy judge-only: every declared path is docs/CHANGELOG/RELEASING/version-mirror leaf work and the diff stayed in that class (${touched.join(", ")})`,
351
+ meta: {
352
+ skipped: true,
353
+ verdict: "skipped",
354
+ policy: "judge-only",
355
+ reason: "every declared path and every path this diff touched is provably leaf-class work",
356
+ paths: touched,
357
+ },
358
+ };
359
+ }
360
+ promotedBy = escaped.length > 0 ? escaped : [];
274
361
  }
362
+ // Every verdict this gate reports from here on was produced under `full` — either declared full, or
363
+ // promoted here. `promotedBy` names the paths that bought the promotion, so the record shows WHY.
364
+ const policyMeta = {
365
+ policy: "full",
366
+ ...(promotedBy ? { promotedFrom: declaredPolicy, promotedBy } : {}),
367
+ };
275
368
  const reviewer = pickReviewer(author, channels, excludeReviewers ?? [], cfg.review.prefer ?? []);
276
369
  if (!reviewer) {
277
370
  // meta.noEligibleReviewer lets run-gates' review-retry keep the ORIGINAL unparseable result when
@@ -343,7 +436,7 @@ The top-level comments array is optional. Use it only for actionable line-anchor
343
436
  gate: "review",
344
437
  pass: false,
345
438
  details: `review output unparseable (reviewer ${reviewer.adapter}:${reviewer.model}; cause: ${cause}${saved ? `; raw saved: ${saved}` : ""}) — failing closed`,
346
- meta: { reviewer: channelKey(reviewer), unparseable: true, cause },
439
+ meta: { ...policyMeta, reviewer: channelKey(reviewer), unparseable: true, cause },
347
440
  };
348
441
  }
349
442
  const decided = findings !== null
@@ -354,6 +447,6 @@ The top-level comments array is optional. Use it only for actionable line-anchor
354
447
  gate: "review",
355
448
  pass: decided.pass,
356
449
  details: appendAnchoredReview(prose, v),
357
- meta: { reviewer: channelKey(reviewer) },
450
+ meta: { ...policyMeta, reviewer: channelKey(reviewer) },
358
451
  };
359
452
  }
@@ -9,6 +9,7 @@ export type GateEvent = {
9
9
  gate: GateName;
10
10
  index: number;
11
11
  total: number;
12
+ parentAt?: number;
12
13
  } | {
13
14
  phase: "end";
14
15
  gate: GateName;
@@ -27,8 +28,16 @@ export interface GateContext {
27
28
  via?: GateVia;
28
29
  excludeReviewers?: string[];
29
30
  artifactDir?: string;
31
+ pipeline?: "v185" | "legacy";
32
+ selectTests?: boolean;
30
33
  onGate?: (e: GateEvent) => void | Promise<void>;
31
34
  }
35
+ /**
36
+ * The configured test command narrowed to these files. Mirrors testFiltered's `--` rule (acceptance.ts:104):
37
+ * npm/yarn/pnpm/npx script wrappers need one `--` to forward positional filters to the underlying runner;
38
+ * a command that already has `--` takes them directly. Every path is quoted — config flows into a shell.
39
+ */
40
+ export declare function testCommandForFiles(testCmd: string, files: string[]): string;
32
41
  export declare function runGates(task: Task, ctx: GateContext): Promise<{
33
42
  results: GateResult[];
34
43
  commits: string[];