tickmarkr 2.5.7 → 2.5.8

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.
@@ -1,5 +1,7 @@
1
1
  import { HerdrDriver } from "./herdr.js";
2
- import { OrcaDriver } from "./orca.js";
2
+ import { shq } from "../adapters/types.js";
3
+ import { sh } from "../run/git.js";
4
+ import { canonicalWorktreePath, OrcaDriver, OrcaError, parseEnvelope, resolveOrcaCliBinary, terminalWorktree } from "./orca.js";
3
5
  import { SubprocessDriver } from "./subprocess.js";
4
6
  export const DRIVER_CHOICES = ["auto", "herdr", "subprocess", "orca"];
5
7
  const overrideByDriver = new WeakMap();
@@ -20,18 +22,64 @@ export function classifyHost(env = process.env) {
20
22
  return "orca";
21
23
  return "none";
22
24
  }
25
+ const UNMANAGED_CHECKOUT = "checkout is not Orca-managed; run from an orca worktree create checkout or pass --driver subprocess";
26
+ /**
27
+ * OBS-1061: the ONE admission probe — `worktree current` asked from the run root. Like the driver's
28
+ * own per-checkout query it accepts an enclosing tracked checkout after path canonicalisation. An
29
+ * untracked checkout refuses with both remedies; any other failure refuses naming its reason, never
30
+ * read as managed.
31
+ */
32
+ async function admitOrcaCheckout(runRoot, env) {
33
+ const bin = resolveOrcaCliBinary(runRoot, { env }) ?? "orca";
34
+ const refuse = (reason) => new Error(`refusing driver 'orca': Orca admission probe of ${runRoot} failed — ${reason}`);
35
+ let reported;
36
+ // The transport's own diagnostics (timeout, exit, stderr): a stderr-only failure such as a missing
37
+ // executable parses as 'empty response', which alone names no cause.
38
+ let transport = "";
39
+ try {
40
+ // Config values flow into a shell here: every argv element is quoted, always.
41
+ const r = await sh([bin, "worktree", "current", "--json"].map(shq).join(" "), runRoot);
42
+ const raw = [r.stdout, r.stderr ? `STDERR: ${r.stderr}` : ""].filter(Boolean).join("\n");
43
+ transport = [r.timedOut ? "probe timed out" : r.code !== 0 ? `orca exited ${r.code}` : "", r.stderr.trim() ? `stderr: ${r.stderr.trim()}` : ""]
44
+ .filter(Boolean).join("; ");
45
+ const envelope = parseEnvelope("worktree-current", r.stdout, raw);
46
+ // An ok:true body on a nonzero exit is a transport failure (stale stdout), as in the driver.
47
+ if (r.code !== 0)
48
+ throw new OrcaError("worktree-current", `orca exited ${r.code}`, raw);
49
+ const worktree = envelope.result.worktree;
50
+ reported = typeof worktree === "object" && worktree !== null && !Array.isArray(worktree)
51
+ ? terminalWorktree(worktree)
52
+ : undefined;
53
+ if (!reported)
54
+ throw new OrcaError("worktree-current", "response carries no worktree path", raw);
55
+ }
56
+ catch (e) {
57
+ if (e instanceof OrcaError && e.code === "selector_not_found")
58
+ throw new Error(`refusing driver 'orca': ${UNMANAGED_CHECKOUT}`);
59
+ const reason = e instanceof OrcaError ? e.reason : e.message;
60
+ throw refuse(transport.startsWith(reason) ? transport : [reason, transport].filter(Boolean).join("; "));
61
+ }
62
+ const tracked = canonicalWorktreePath(reported);
63
+ const root = canonicalWorktreePath(runRoot);
64
+ if (tracked !== root && !root.startsWith(`${tracked}/`))
65
+ throw new Error(`refusing driver 'orca': ${UNMANAGED_CHECKOUT}`);
66
+ }
23
67
  /**
24
68
  * A config driver of herdr or orca whose host is not the classified one is refused naming the
25
69
  * detected host, the config line and the --driver remedy. Any explicit --driver value bypasses
26
- * this; auto and subprocess are never refused.
70
+ * that; auto and subprocess are never refused. Then, when the RESOLVED driver is orca (config,
71
+ * auto on an Orca host, or an explicit flag), the run root is admitted by one probe before any run
72
+ * directory, journal row or dispatch exists. Subprocess and herdr selections probe nothing.
27
73
  */
28
- export function preflightHostDriver(cfg, driverOverride, host) {
29
- if (driverOverride !== undefined)
30
- return;
31
- if ((cfg.driver === "herdr" || cfg.driver === "orca") && cfg.driver !== host) {
74
+ export async function preflightHostDriver(cfg, driverOverride, host, runRoot, env = process.env) {
75
+ if (driverOverride === undefined && (cfg.driver === "herdr" || cfg.driver === "orca") && cfg.driver !== host) {
32
76
  const remedy = host === "none" ? "subprocess" : host;
33
77
  throw new Error(`refusing driver '${cfg.driver}' (config line 'driver: ${cfg.driver}'): detected host is ${host}; use --driver ${remedy} to override`);
34
78
  }
79
+ const want = parseDriverOverride(driverOverride) ?? cfg.driver;
80
+ const resolved = want === "auto" ? (host === "none" ? "subprocess" : host) : want;
81
+ if (resolved === "orca")
82
+ await admitOrcaCheckout(runRoot, env);
35
83
  }
36
84
  /**
37
85
  * Orca authors both markers on every terminal it creates. Requiring the pair avoids treating an
@@ -65,6 +65,8 @@ export interface GateContext {
65
65
  demotedReviewers?: Set<string>;
66
66
  reviewNoVerdicts?: Map<string, string[]>;
67
67
  recheck?: boolean;
68
+ /** Explicit worker funding requires fresh red measurements, never a gate waiver. */
69
+ cachedRedBypass?: "operator-rerun";
68
70
  carriedAuthors?: readonly string[];
69
71
  reviewHistory?: string[];
70
72
  priorReviewers?: PriorReviewer[];
@@ -310,9 +310,11 @@ export async function runGates(task, ctx) {
310
310
  // OBS-1055: on a recheck a cached red is the answer the operator just said was wrongly given; it is
311
311
  // journaled as discarded and the gate runs. Returns true when the hit must NOT be reused.
312
312
  const discardCachedRed = async (gate, hit) => {
313
- if (!ctx.recheck || hit.pass)
313
+ const reason = ctx.recheck ? "recheck" : ctx.cachedRedBypass;
314
+ if (!reason || hit.pass)
314
315
  return false;
315
- await ctx.onGate?.({ phase: "note", gate, name: "recheck-rerun", payload: { gate, reason: "cached-red-discarded" }, result: hit });
316
+ await ctx.onGate?.({ phase: "note", gate, name: reason === "recheck" ? "recheck-rerun" : "gate-rerun",
317
+ payload: { gate, reason: "cached-red-discarded", ...(reason === "recheck" ? {} : { bypass: reason }) }, result: hit });
316
318
  return true;
317
319
  };
318
320
  const shapeGates = ctx.cfg.gates.byShape?.[task.shape];
@@ -20,6 +20,15 @@ export interface TestReportCompletion {
20
20
  failed: number;
21
21
  skipped: number;
22
22
  };
23
+ /** Bounded (4096 bytes) runner evidence per failed test — diff, actual/expected or stack head.
24
+ * Never part of the failure's identity: `failures` alone feeds details and fingerprints. */
25
+ evidence?: FailureEvidence[];
26
+ }
27
+ export interface FailureEvidence {
28
+ test: string;
29
+ text: string;
30
+ truncated?: true;
31
+ unavailable?: true;
23
32
  }
24
33
  /** The runner's own machine report — requested/started/completed are the runner's claims about ITSELF. */
25
34
  export interface TestReport {
@@ -103,6 +103,12 @@ export function toManifestPath(file, cwd) {
103
103
  catch { /* cwd unreadable — best effort with the given path */ }
104
104
  return relative(resolvedCwd, file).split(sep).join("/");
105
105
  }
106
+ function isFailureEvidence(v) {
107
+ if (typeof v !== "object" || v === null)
108
+ return false;
109
+ const e = v;
110
+ return typeof e.test === "string" && typeof e.text === "string";
111
+ }
106
112
  function isTestReportShape(v) {
107
113
  if (typeof v !== "object" || v === null)
108
114
  return false;
@@ -228,10 +234,12 @@ export function verifyManifestReport(opts) {
228
234
  const failedCompletions = Object.entries(report.completed).filter(([, c]) => c.status === "failed");
229
235
  const failingFiles = failedCompletions.map(([file]) => file).sort();
230
236
  const failures = failedCompletions.flatMap(([, c]) => c.failures?.length ? c.failures : ["<unnamed failure>"]);
237
+ // Parsed defensively: a malformed entry is dropped, never a verdict change — evidence is diagnostics only.
238
+ const failureEvidence = failedCompletions.flatMap(([, c]) => Array.isArray(c.evidence) ? c.evidence.filter(isFailureEvidence) : []);
231
239
  if (failures.length)
232
240
  return { kind: "work", pass: false,
233
241
  details: `test report names failing fingerprint(s):\n${failures.join("\n")}`,
234
- meta: { classification: "regression", failingTests: failures, failingFiles, processExit: exitCode } };
242
+ meta: { classification: "regression", failingTests: failures, failingFiles, processExit: exitCode, failureEvidence } };
235
243
  if (exitCode === undefined) {
236
244
  return {
237
245
  kind: "fail-closed",
@@ -11,6 +11,24 @@ export default class TickmarkrReporter {
11
11
  this.cwd = realpathSync(process.cwd());
12
12
  }
13
13
  file(module) { return relative(this.cwd, module.moduleId).split(sep).join('/'); }
14
+ // Bounded assertion evidence, kept BESIDE the failure fingerprint and never inside it: the
15
+ // fingerprint is the failure's identity (the repeated-failure cap compares it), the evidence is
16
+ // what the runner knew and the message elided. ONE 4096-byte budget per failed test, shared by
17
+ // all of its errors (expect.soft yields several); never invented.
18
+ evidence(test, errors) {
19
+ const parts = [];
20
+ for (const e of errors) {
21
+ if (e && typeof e.diff === 'string' && e.diff) parts.push(e.diff);
22
+ else for (const k of ['actual', 'expected']) if (e && e[k] !== undefined && e[k] !== null) parts.push(k + ': ' + (typeof e[k] === 'string' ? e[k] : JSON.stringify(e[k])));
23
+ if (e && typeof e.stack === 'string' && e.stack) parts.push(e.stack.split('\n').slice(0, 8).join('\n'));
24
+ }
25
+ if (!parts.length) return { test, text: '', unavailable: true };
26
+ const full = parts.join('\n').replace(/\x1b\[[0-9;]*m/g, '');
27
+ if (Buffer.byteLength(full) <= 4096) return { test, text: full };
28
+ let text = full.slice(0, 4096);
29
+ while (Buffer.byteLength(text) > 4096) text = text.slice(0, -1);
30
+ return { test, text, truncated: true };
31
+ }
14
32
  save() {
15
33
  writeFileSync(this.path + '.tmp', JSON.stringify(this.report));
16
34
  renameSync(this.path + '.tmp', this.path);
@@ -27,6 +45,7 @@ export default class TickmarkrReporter {
27
45
  const file = this.file(module);
28
46
  const failed = module.state() === 'failed';
29
47
  const failures = [];
48
+ const evidence = [];
30
49
  // R41: count test bodies by their own state so a module whose every test was skipped (a
31
50
  // describe.skipIf gate) is recorded as SKIPPED — present in the lifecycle, but never as
32
51
  // executed test-body success. 'passed'/'failed' executed; anything else did not run.
@@ -37,6 +56,8 @@ export default class TickmarkrReporter {
37
56
  tests.failed++;
38
57
  if (failed) {
39
58
  const errors = test.result().errors || [];
59
+ const name = file + ' > ' + test.fullName;
60
+ evidence.push(this.evidence(name, errors));
40
61
  failures.push(...(errors.length ? errors.map(e => 'FAIL ' + file + ' > ' + test.fullName + ': ' + e.message) : ['FAIL ' + file + ' > ' + test.fullName]));
41
62
  }
42
63
  } else if (state === 'passed') tests.passed++;
@@ -45,7 +66,7 @@ export default class TickmarkrReporter {
45
66
  if (failed && !failures.length) failures.push('FAIL ' + file);
46
67
  const status = failed ? 'failed' : tests.passed + tests.failed === 0 ? 'skipped' : 'passed';
47
68
  if (file in this.report.completed) this.report.duplicateCompletions.push(file);
48
- this.report.completed[file] = { at: Date.now(), status, failures, tests };
69
+ this.report.completed[file] = { at: Date.now(), status, failures, tests, ...(evidence.length ? { evidence } : {}) };
49
70
  this.save();
50
71
  }
51
72
  onTestRunEnd(modules, errors, reason) {
@@ -19,12 +19,22 @@ export declare const AcceptanceItemSchema: z.ZodUnion<readonly [z.ZodString, z.Z
19
19
  oracle: z.ZodLiteral<"test">;
20
20
  test: z.ZodString;
21
21
  text: z.ZodOptional<z.ZodString>;
22
+ landing: z.ZodOptional<z.ZodString>;
22
23
  }, z.core.$strip>, z.ZodObject<{
23
24
  oracle: z.ZodLiteral<"judge">;
24
25
  text: z.ZodString;
25
26
  }, z.core.$strip>]>;
26
27
  export type AcceptanceItem = z.infer<typeof AcceptanceItemSchema>;
27
28
  export declare function renderAcceptanceItem(item: AcceptanceItem): string;
29
+ export declare const PinSchema: z.ZodDiscriminatedUnion<[z.ZodObject<{
30
+ kind: z.ZodLiteral<"literal">;
31
+ text: z.ZodString;
32
+ glob: z.ZodString;
33
+ }, z.core.$strip>, z.ZodObject<{
34
+ kind: z.ZodLiteral<"fixture">;
35
+ paths: z.ZodArray<z.ZodString>;
36
+ }, z.core.$strip>], "kind">;
37
+ export type Pin = z.infer<typeof PinSchema>;
28
38
  export declare const TaskSchema: z.ZodObject<{
29
39
  id: z.ZodString;
30
40
  title: z.ZodString;
@@ -52,6 +62,7 @@ export declare const TaskSchema: z.ZodObject<{
52
62
  oracle: z.ZodLiteral<"test">;
53
63
  test: z.ZodString;
54
64
  text: z.ZodOptional<z.ZodString>;
65
+ landing: z.ZodOptional<z.ZodString>;
55
66
  }, z.core.$strip>, z.ZodObject<{
56
67
  oracle: z.ZodLiteral<"judge">;
57
68
  text: z.ZodString;
@@ -61,6 +72,14 @@ export declare const TaskSchema: z.ZodObject<{
61
72
  confidence: z.ZodOptional<z.ZodNumber>;
62
73
  reason: z.ZodOptional<z.ZodString>;
63
74
  }, z.core.$strip>>>;
75
+ pins: z.ZodOptional<z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
76
+ kind: z.ZodLiteral<"literal">;
77
+ text: z.ZodString;
78
+ glob: z.ZodString;
79
+ }, z.core.$strip>, z.ZodObject<{
80
+ kind: z.ZodLiteral<"fixture">;
81
+ paths: z.ZodArray<z.ZodString>;
82
+ }, z.core.$strip>], "kind">>>;
64
83
  gates: z.ZodDefault<z.ZodArray<z.ZodEnum<{
65
84
  build: "build";
66
85
  test: "test";
@@ -144,6 +163,7 @@ export declare const RunGraphSchema: z.ZodObject<{
144
163
  oracle: z.ZodLiteral<"test">;
145
164
  test: z.ZodString;
146
165
  text: z.ZodOptional<z.ZodString>;
166
+ landing: z.ZodOptional<z.ZodString>;
147
167
  }, z.core.$strip>, z.ZodObject<{
148
168
  oracle: z.ZodLiteral<"judge">;
149
169
  text: z.ZodString;
@@ -153,6 +173,14 @@ export declare const RunGraphSchema: z.ZodObject<{
153
173
  confidence: z.ZodOptional<z.ZodNumber>;
154
174
  reason: z.ZodOptional<z.ZodString>;
155
175
  }, z.core.$strip>>>;
176
+ pins: z.ZodOptional<z.ZodArray<z.ZodDiscriminatedUnion<[z.ZodObject<{
177
+ kind: z.ZodLiteral<"literal">;
178
+ text: z.ZodString;
179
+ glob: z.ZodString;
180
+ }, z.core.$strip>, z.ZodObject<{
181
+ kind: z.ZodLiteral<"fixture">;
182
+ paths: z.ZodArray<z.ZodString>;
183
+ }, z.core.$strip>], "kind">>>;
156
184
  gates: z.ZodDefault<z.ZodArray<z.ZodEnum<{
157
185
  build: "build";
158
186
  test: "test";
@@ -19,7 +19,9 @@ export const ORACLES = ["command", "test", "judge"];
19
19
  export const AcceptanceItemSchema = z.union([
20
20
  z.string().min(1),
21
21
  z.object({ oracle: z.literal("command"), command: z.string().min(1), text: z.string().min(1).optional() }),
22
- z.object({ oracle: z.literal("test"), test: z.string().min(1), text: z.string().min(1).optional() }),
22
+ // v2.5.8 T8 (OBS-1064): landing = the collectable suite path a test criterion lands in, declared
23
+ // beside the verbatim title (never inside it). Optional; compile enforces the collectable glob.
24
+ z.object({ oracle: z.literal("test"), test: z.string().min(1), text: z.string().min(1).optional(), landing: z.string().min(1).optional() }),
23
25
  z.object({ oracle: z.literal("judge"), text: z.string().min(1) }),
24
26
  ]);
25
27
  // Shared text rendering of one acceptance item — every consumer (worker prompt, acceptance gate,
@@ -33,6 +35,15 @@ export function renderAcceptanceItem(item) {
33
35
  return item.text ?? `test: ${item.test}`;
34
36
  return item.text; // judge — bare text, byte-identical to a plain-string judge criterion
35
37
  }
38
+ // v2.5.8 T7 (agreement C2): declared pin obligations — a LIMITED AUTHORING CONTRACT, not an assertion
39
+ // analyzer. literal = an exact text plus the glob it is pinned in (every matching file holding the text
40
+ // is obligated); fixture = a path set whose every matching file is itself obligated (byte-pinned output
41
+ // the change will move) and which carries no literal text. Declared here because z.object strips
42
+ // undeclared keys on every load.
43
+ export const PinSchema = z.discriminatedUnion("kind", [
44
+ z.object({ kind: z.literal("literal"), text: z.string().min(1), glob: z.string().min(1) }),
45
+ z.object({ kind: z.literal("fixture"), paths: z.array(z.string().min(1)).min(1) }),
46
+ ]);
36
47
  export const TaskSchema = z.object({
37
48
  // ids land in git branch names and herdr pane names — branch-safe characters only, bounded
38
49
  // length (refs hit filesystem limits), and never "--" (the task-branch separator, locked
@@ -59,6 +70,7 @@ export const TaskSchema = z.object({
59
70
  reason: z.string().optional(),
60
71
  }))
61
72
  .optional(),
73
+ pins: z.array(PinSchema).optional(),
62
74
  gates: z
63
75
  .array(z.enum(GATE_NAMES))
64
76
  .default(["build", "test", "lint", "evidence", "scope", "acceptance", "review"])
@@ -4,6 +4,7 @@ import { type DriverChoice } from "../drivers/index.js";
4
4
  import { type ExecutorDriver, type Slot } from "../drivers/types.js";
5
5
  import { type Baseline } from "../gates/baseline.js";
6
6
  import type { GateResult } from "../gates/types.js";
7
+ import { type RunGraph } from "../graph/schema.js";
7
8
  import { Journal, type JournalEvent } from "./journal.js";
8
9
  export declare function closeLiveSlot(liveSlots: Set<Slot>, driver: Pick<ExecutorDriver, "close">, slot: Slot): Promise<void>;
9
10
  /** Optional authoritative transport receipt. Screen text can prove execution, never nonacceptance. */
@@ -58,6 +59,8 @@ export interface RunSummary {
58
59
  pending: string[];
59
60
  blocked: string[];
60
61
  tipVerify?: "passed" | "failed";
62
+ /** additive beside the legacy tipVerify enum: HOW the close's latest verification cycle earned its verdict */
63
+ tipProof?: TipProof;
61
64
  lastMergedTask?: string;
62
65
  /** T14: did every approval this run accepted actually get enacted, or did the run end over one? */
63
66
  approvalDisposition?: "complete" | "outstanding";
@@ -165,6 +168,21 @@ export declare function resetHarvestSilentMsForTests(): void;
165
168
  export declare const HARVESTED_RESULT_SUMMARY = "harvested: the worktree carries committed work; the worker emitted no TICKMARKR_RESULT trailer";
166
169
  /** T4 (OBS-266): identity of the command SET a tip verify ran — a changed command is a different verify. */
167
170
  export declare function commandsHash(commands: Record<string, string>): string;
171
+ export interface TipProof {
172
+ kind: "fresh" | "reused" | "failed" | "incomplete";
173
+ /** the commit the cycle's start row spoke for */
174
+ tip?: string;
175
+ }
176
+ /**
177
+ * OBS-1077 close rider: what the engagement's LATEST verification cycle proved — its
178
+ * `tip-verify-start` row and what followed, never a commit comparison. Exactly one kind per close:
179
+ * fresh (every tip command ran AND passed), reused (an eligible cached cycle carried forward),
180
+ * failed, or incomplete (cancelled, cut short, mixed or undelimited). Nothing before the start row
181
+ * is read, so an unfinished cycle inherits nothing from an earlier green one.
182
+ */
183
+ export declare function runEndTipProof(events: readonly JournalEvent[]): TipProof;
184
+ /** The close notification's statement of the proof — one clause per kind. */
185
+ export declare function formatTipProof(p: TipProof): string;
168
186
  /**
169
187
  * OBS-34's strict tip verify, but it stops re-paying for an unmoved tip (~334m corpus-wide; 69.5m in
170
188
  * one park-heavy run whose 18 resume cycles merged nothing new). The verify journals the SHA it
@@ -194,4 +212,5 @@ export declare function liveSuiteCount(repoRoot: string): Promise<number>;
194
212
  /** Test seam — exercise the production observer's total read bound with a small real tree. */
195
213
  export declare function setObserveBudgetBytesForTests(bytes: number): void;
196
214
  export declare function resetObserveBudgetBytesForTests(): void;
215
+ export declare function recordFatalRunEnd(journal: Journal, runId: string, branch: string, err: unknown, graph?: RunGraph, phase?: string): TipProof | undefined;
197
216
  export declare function runDaemon(repoRoot: string, opts?: RunOptions): Promise<RunSummary>;