tickmarkr 2.1.6 → 2.1.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,7 +1,7 @@
1
1
  import { mkdirSync, renameSync, rmSync, writeFileSync } from "node:fs";
2
2
  import { dirname } from "node:path";
3
3
  import { tickmarkrDir } from "../../graph/graph.js";
4
- import { SUPERVISION_BEAT_MS, SUPERVISION_STALE_MS, SUPERVISION_TIERS, beatSupervision, supervisionStandDownPath, } from "../../run/supervision.js";
4
+ import { SUPERVISION_BEAT_MS, SUPERVISION_DEFAULT_THRESHOLD_PCT, SUPERVISION_STALE_MS, SUPERVISION_TIERS, beatSupervision, supervisionStandDownPath, } from "../../run/supervision.js";
5
5
  // SUP-04: the writer side of supervision, as a VERB. `beatSupervision` and `SUPERVISION_BEAT_MS` shipped
6
6
  // with exactly one in-repo caller — the daemon, on one tier — so `status` printed
7
7
  // `orchestrator ARMED / overseer ABSENT / watch ABSENT` while a real overseer worked the run: two thirds
@@ -18,16 +18,22 @@ import { SUPERVISION_BEAT_MS, SUPERVISION_STALE_MS, SUPERVISION_TIERS, beatSuper
18
18
  // reads UNREADABLE. Here the writes are unguarded: a failure leaves the process with a non-zero exit
19
19
  // and a message instead of a claim. The LOOP belongs to the caller, which is what makes the beat
20
20
  // evidence: stop calling it and the tier ages into STALE on its own.
21
+ const VALUE_OPTIONS = ["--seat", "--arm-id", "--pct", "--threshold-pct"];
21
22
  export async function beat(argv, cwd = process.cwd()) {
22
23
  const standDown = argv.includes("--stand-down");
23
24
  const seat = seatOf(argv);
24
- const named = argv.find((a, i) => !a.startsWith("--") && argv[i - 1] !== "--seat");
25
+ const pct = percentageOf(argv, "--pct");
26
+ const thresholdPct = percentageOf(argv, "--threshold-pct") ?? SUPERVISION_DEFAULT_THRESHOLD_PCT;
27
+ const armId = optionOf(argv, "--arm-id");
28
+ const named = argv.find((a, i) => !a.startsWith("--") && !VALUE_OPTIONS.includes(argv[i - 1] ?? ""));
25
29
  if (!isTier(named)) {
26
- throw new Error(`usage: tickmarkr beat <${SUPERVISION_TIERS.join("|")}> --seat <identity> [--stand-down] — got ${named ? `\`${named}\`` : "no tier"}`);
30
+ throw new Error(`usage: tickmarkr beat <${SUPERVISION_TIERS.join("|")}> --seat <identity> ` +
31
+ `[--arm-id <identity> --pct <0..100> --threshold-pct <0..100>] [--stand-down] — ` +
32
+ `got ${named ? `\`${named}\`` : "no tier"}`);
27
33
  }
28
34
  // SUP-05: NO SEAT, NO BEAT — and the refusal comes before any write, so a refused invocation leaves
29
- // the tier exactly as it found it. This verb is one-shot: the pid it would otherwise record has
30
- // already exited by the time anyone reads the record, so tier + pid + instant is a beat nobody can
35
+ // the tier exactly as it found it. This verb is one-shot: the exitedWriterPid it records has
36
+ // already exited by the time anyone reads the record, so tier + writer + instant is a beat nobody can
31
37
  // attribute to a seat. Measured 2026-08-26: a consult seat ran the documented loop verbatim and the
32
38
  // board read that tier ARMED with no seat of that tier having armed anything, and a seatless ARMED
33
39
  // reads as coverage — worse than ABSENT, because ABSENT sends someone to look.
@@ -42,16 +48,35 @@ export async function beat(argv, cwd = process.cwd()) {
42
48
  // uncleared marker would render DISARMED while this verb claimed ARMED, so the removal is unguarded
43
49
  // too — `force` makes the ordinary "no marker" case a no-op, and anything else is a real failure.
44
50
  rmSync(supervisionStandDownPath(cwd, named), { force: true, recursive: true });
45
- beatSupervision(cwd, named, seat);
51
+ beatSupervision(cwd, named, seat, pct === undefined ? undefined : { armId: armId ?? seat, pct, thresholdPct });
46
52
  return `${named} ARMED as ${seat} — beat again every ${SUPERVISION_BEAT_MS / 1_000}s; the tier reads STALE ${SUPERVISION_STALE_MS / 1_000}s after the last beat`;
47
53
  }
48
54
  /** `--seat <identity>` or `--seat=<identity>`; blank and missing are the same answer — none. */
49
55
  function seatOf(argv) {
50
- const inline = argv.find((a) => a.startsWith("--seat="))?.slice("--seat=".length);
51
- const spaced = argv[argv.indexOf("--seat") + 1];
52
- const seat = (inline ?? (argv.includes("--seat") ? spaced : undefined))?.trim();
56
+ const seat = optionOf(argv, "--seat")?.trim();
53
57
  return seat && !seat.startsWith("--") ? seat : undefined;
54
58
  }
59
+ /** A `--name value` or `--name=value` option, excluding a missing value or the next flag. */
60
+ function optionOf(argv, name) {
61
+ const inline = argv.find((a) => a.startsWith(`${name}=`))?.slice(name.length + 1).trim();
62
+ const spaced = argv[argv.indexOf(name) + 1]?.trim();
63
+ const value = inline ?? (argv.includes(name) ? spaced : undefined);
64
+ return value && !value.startsWith("--") ? value : undefined;
65
+ }
66
+ function percentageOf(argv, name) {
67
+ const raw = optionOf(argv, name);
68
+ if (raw === undefined) {
69
+ if (argv.includes(name) || argv.some((a) => a.startsWith(`${name}=`))) {
70
+ throw new Error(`${name} needs a number from 0 through 100`);
71
+ }
72
+ return undefined;
73
+ }
74
+ const value = Number(raw);
75
+ if (!Number.isFinite(value) || value < 0 || value > 100) {
76
+ throw new Error(`${name} must be a number from 0 through 100`);
77
+ }
78
+ return value;
79
+ }
55
80
  // Stand-down is a RECORDED act, not a silence: the marker tells a reader this watcher left on purpose,
56
81
  // so the tier reads DISARMED rather than ageing out as a death. Published atomically — written aside,
57
82
  // renamed over — because a torn marker is rejected by the reader, and a rejected stand-down reports a
@@ -63,7 +88,9 @@ function standDownTier(repoRoot, tier, seat) {
63
88
  mkdirSync(dirname(p), { recursive: true });
64
89
  // The marker names the seat for the same reason the beat does: "someone stood this tier down" is not
65
90
  // a hand-off anyone can act on, and on a seat tier the reader rejects an anonymous one outright.
66
- writeFileSync(tmp, JSON.stringify({ tier, seat, pid: process.pid, disarmedAt: new Date().toISOString() }) + "\n");
91
+ writeFileSync(tmp, JSON.stringify({
92
+ tier, seat, exitedWriterPid: process.pid, disarmedAt: new Date().toISOString(),
93
+ }) + "\n");
67
94
  renameSync(tmp, p);
68
95
  return `${tier} DISARMED — ${seat} handed off; status reads it stood down, not dead`;
69
96
  }
@@ -3,6 +3,7 @@ import type { WorkerAdapter } from "../../adapters/types.js";
3
3
  import { type KimiDoctorTurnResult } from "../../adapters/kimi.js";
4
4
  import { type CatalogReadResult } from "../../adapters/catalog-remote.js";
5
5
  import { type ShResult } from "../../run/git.js";
6
+ import { type VitestListResult } from "../../gates/acceptance.js";
6
7
  /** Where a newer `table_<date>.csv` is discovered — the deployed site builds filenames by
7
8
  * concatenation and publishes no index, so the release listing is the only enumerable surface. */
8
9
  export declare const LIVEBENCH_RELEASES_URL = "https://api.github.com/repos/LiveBench/livebench.github.io/contents/public";
@@ -20,6 +21,8 @@ export type DoctorOpts = {
20
21
  orcaStatusProbe?: (cwd: string, binary: string) => Promise<ShResult>;
21
22
  /** Test seam for shell-path discovery; absence remains a normal doctor row, never an exception. */
22
23
  resolveOrcaBinary?: (cwd: string) => string | undefined;
24
+ /** Test seam for the runner-owned JSON listing used by the acceptance-oracle report row. */
25
+ listTests?: (cwd: string) => Promise<VitestListResult>;
23
26
  };
24
27
  type OrcaCapability = {
25
28
  verdict: "pass" | "fail";
@@ -8,7 +8,7 @@ import { allAdapters, binaryShadowWarnings, detectCandidateClis, flagDriftWarnin
8
8
  import { CLAUDE_ALIAS_IDENTITY_STAMPS, claudeCode, resolveClaudeAliasIdentity } from "../../adapters/claude-code.js";
9
9
  import { shq } from "../../adapters/types.js";
10
10
  import { BANNER, compactTokens, dim, fail, kvRow, legend, ok, rule, statusRow, title } from "../../brand.js";
11
- import { tickmarkrDir, stateDirName } from "../../graph/graph.js";
11
+ import { graphPath, loadGraph, tickmarkrDir, stateDirName } from "../../graph/graph.js";
12
12
  import { catalogModelAdvisory, catalogTierRanking, declaredModelWindow, hasWindowsConfig, modelLints, suggestOverlay, ttyVisual } from "../../adapters/model-lints.js";
13
13
  import { loadConfig, overlayPreferShapes } from "../../config/config.js";
14
14
  import { HerdrDriver } from "../../drivers/herdr.js";
@@ -17,6 +17,7 @@ import { kimi, probeKimiDoctorTurn } from "../../adapters/kimi.js";
17
17
  import { denyPreferCollisionLine, denyPreferCollisions, disallowedBy, excludedChannels, exclusionLine, preferRanks } from "../../route/preference.js";
18
18
  import { LIVEBENCH_TABLE_DATE, readCachedCatalog, refreshCatalogCommand } from "../../adapters/catalog-remote.js";
19
19
  import { sh } from "../../run/git.js";
20
+ import { auditNamedTestOracles, listVitestTests } from "../../gates/acceptance.js";
20
21
  /** Where a newer `table_<date>.csv` is discovered — the deployed site builds filenames by
21
22
  * concatenation and publishes no index, so the release listing is the only enumerable surface. */
22
23
  export const LIVEBENCH_RELEASES_URL = "https://api.github.com/repos/LiveBench/livebench.github.io/contents/public";
@@ -362,6 +363,27 @@ export async function doctor(_argv, cwd = process.cwd(), adapters = allAdapters(
362
363
  const healthy = h.installed && (a.id !== kimi.id || h.authed);
363
364
  return alignedStatusRow(healthy ? "pass" : "fail", a.id, state);
364
365
  });
366
+ if (existsSync(graphPath(cwd))) {
367
+ try {
368
+ const graph = loadGraph(cwd);
369
+ const items = graph.tasks.flatMap((task) => task.acceptance);
370
+ if (items.some((item) => typeof item === "object" && item.oracle === "test")) {
371
+ const listing = await (opts.listTests ?? listVitestTests)(cwd);
372
+ if (listing.status === "failed") {
373
+ rows.push(alignedStatusRow("fail", "acceptance-oracles", `runner listing failed — ${listing.error.split("\n")[0]}`));
374
+ }
375
+ else {
376
+ const audited = auditNamedTestOracles(items, listing.tests);
377
+ const resolved = audited.filter((row) => row.matches.length === 1).length;
378
+ const verdict = resolved === audited.length ? "pass" : "fail";
379
+ rows.push(alignedStatusRow(verdict, "acceptance-oracles", `${resolved}/${audited.length} resolved — each resolved oracle's shipped filter matches exactly one runner-listed test`));
380
+ }
381
+ }
382
+ }
383
+ catch (error) {
384
+ rows.push(alignedStatusRow("fail", "acceptance-oracles", `graph unreadable — ${error instanceof Error ? error.message : String(error)}`));
385
+ }
386
+ }
365
387
  if (catalog.warning) {
366
388
  rows.push(attentionRow(`model catalog cache unreadable — ${catalog.warning}; using vendored fallback (advisory — routing unchanged)`));
367
389
  }
@@ -1,2 +1,6 @@
1
- import type { WorkerAdapter } from "../../adapters/types.js";
2
- export declare function plan(argv: string[], cwd?: string, adapters?: WorkerAdapter[], harnessFrom?: string | undefined): Promise<string>;
1
+ import { type VitestListResult } from "../../gates/acceptance.js";
2
+ import { type WorkerAdapter } from "../../adapters/types.js";
3
+ export type PlanOpts = {
4
+ listTests?: (cwd: string) => Promise<VitestListResult>;
5
+ };
6
+ export declare function plan(argv: string[], cwd?: string, adapters?: WorkerAdapter[], harnessFrom?: string | undefined, opts?: PlanOpts): Promise<string>;
@@ -3,15 +3,20 @@ import { formatModelAuthLine, contextWindowLints, modelLints, preferEntryLints,
3
3
  import { GLYPHS, dim, rule, title, warn } from "../../brand.js";
4
4
  import { parseArgs } from "node:util";
5
5
  import { collateralLints, sourceScopeLints } from "../../compile/collateral.js";
6
+ import { classifyContextPath } from "../../compile/native.js";
6
7
  import { DEFAULT_CONFIG, overlayPreferShapes, ROUTING_MODES, TIER_RANK } from "../../config/config.js";
7
8
  import { loadGraph } from "../../graph/graph.js";
9
+ import { renderAcceptanceItem } from "../../graph/schema.js";
8
10
  import { resolveRunMode } from "../../run/daemon.js";
9
11
  import { excludedChannels, exclusionLine } from "../../route/preference.js";
10
12
  import { staffLedEvidence } from "../../route/profile.js";
11
13
  import { route, RoutingError } from "../../route/router.js";
14
+ import { auditNamedTestOracles, listVitestTests } from "../../gates/acceptance.js";
12
15
  import { modelId } from "../../gates/review.js";
13
16
  import { loadRoutingProfile } from "../../run/journal.js";
14
17
  import { harnessLine, resolveHarness } from "../harness.js";
18
+ import { shq } from "../../adapters/types.js";
19
+ import { shGit } from "../../run/git.js";
15
20
  // T4 (v1.50): TTY-only brand pass — the title helper frames the routing table, lint/unroutable
16
21
  // markers carry the attention glyph, section labels dim to chrome (the doctor/status system).
17
22
  // Gated on ttyVisual(): the non-TTY surface returns untouched (byte-pinned, machine-consumable).
@@ -35,12 +40,60 @@ const fleetCanCrossVendorReview = (channels) => {
35
40
  return true;
36
41
  return false;
37
42
  };
43
+ const NAMED_CRITERION_PATH = /(?:^|[\s("'`])((?:src|tests|fixtures|scripts)\/[A-Za-z0-9_@{}*?.,/+-]+\.(?:tsx|ts|jsx|json|js|mjs|cjs|md|txt))(?![A-Za-z0-9])/g;
44
+ async function taskInputFindings(tasks, cwd) {
45
+ const pathsByTask = tasks.map((task) => {
46
+ const paths = new Set(task.files.map((path) => path.replace(/^\.\//, "")));
47
+ for (const item of task.acceptance) {
48
+ const texts = typeof item === "string"
49
+ ? [item]
50
+ : [renderAcceptanceItem(item), ...Object.values(item).filter((value) => typeof value === "string")];
51
+ for (const text of texts) {
52
+ for (const match of text.matchAll(NAMED_CRITERION_PATH))
53
+ paths.add(match[1]);
54
+ }
55
+ }
56
+ return { task, paths: [...paths] };
57
+ });
58
+ if (pathsByTask.every(({ paths }) => paths.length === 0))
59
+ return [];
60
+ const [tree, diff] = await Promise.all([
61
+ shGit("git ls-tree --full-tree -r --name-only -z HEAD", cwd),
62
+ shGit("git diff --name-only -z HEAD --", cwd),
63
+ ]);
64
+ if (tree.code !== 0 || diff.code !== 0)
65
+ return [];
66
+ const tracked = new Set(tree.stdout.split("\0").filter(Boolean));
67
+ const changed = new Set(diff.stdout.split("\0").filter(Boolean));
68
+ const findings = [];
69
+ for (const { task, paths } of pathsByTask) {
70
+ for (const path of paths) {
71
+ const state = classifyContextPath(path, tracked, cwd);
72
+ if (state.kind === "untracked") {
73
+ findings.push({
74
+ taskId: task.id,
75
+ severity: "refuse",
76
+ detail: `task path ${JSON.stringify(path)} exists in the working tree but no commit holds it`,
77
+ });
78
+ }
79
+ else if (state.kind === "ok"
80
+ && (changed.has(path) || [...changed].some((candidate) => candidate.startsWith(`${path}/`)))) {
81
+ findings.push({
82
+ taskId: task.id,
83
+ severity: "warn",
84
+ detail: `tracked path ${JSON.stringify(path)} differs from HEAD — run git restore -- ${shq(path)} to discard the divergence, or commit it before dispatch`,
85
+ });
86
+ }
87
+ }
88
+ }
89
+ return findings;
90
+ }
38
91
  // v1.89 T4: harnessFrom is the resolver's INPUT — a caller (the byte-pinned goldens) fixes the location
39
92
  // and keeps this machine's absolute paths out of a fixture. The default is the INVOKED entrypoint,
40
93
  // `process.argv[1]`: the bin symlink a global install puts on PATH, which resolves to dist/cli/index.js.
41
94
  // It is NOT `import.meta.url` — that names dist/cli/commands/plan.js, an internal module of the harness
42
95
  // rather than the harness that was invoked, so the banner would identify the wrong file entirely.
43
- export async function plan(argv, cwd = process.cwd(), adapters = allAdapters(), harnessFrom = process.argv[1]) {
96
+ export async function plan(argv, cwd = process.cwd(), adapters = allAdapters(), harnessFrom = process.argv[1], opts = {}) {
44
97
  // ponytail: hardcoded 24h TTL — promote to config when an operator asks. mtime is the signal because
45
98
  // doctor.json has no probe timestamp and a schema field would break the existing-files compat invariant.
46
99
  const DOCTOR_STALE_MS = 24 * 60 * 60 * 1000;
@@ -51,6 +104,37 @@ export async function plan(argv, cwd = process.cwd(), adapters = allAdapters(),
51
104
  throw new Error(`--mode must be one of ${ROUTING_MODES.join(" | ")} (got ${values.mode})`);
52
105
  }
53
106
  const g = loadGraph(cwd);
107
+ const oracleRefusals = new Map();
108
+ if (g.tasks.some((task) => task.acceptance.some((item) => typeof item === "object" && item.oracle === "test"))) {
109
+ const listing = await (opts.listTests ?? listVitestTests)(cwd);
110
+ for (const task of g.tasks) {
111
+ const refusals = [];
112
+ if (listing.status === "failed") {
113
+ if (task.acceptance.some((item) => typeof item === "object" && item.oracle === "test")) {
114
+ refusals.push(`acceptance oracle unresolved — runner listing failed: ${listing.error.split("\n")[0]}`);
115
+ }
116
+ }
117
+ else {
118
+ for (const audit of auditNamedTestOracles(task.acceptance, listing.tests)) {
119
+ if (audit.matches.length === 0) {
120
+ refusals.push(`acceptance oracle ${JSON.stringify(audit.criterion)} matches zero runner-listed test names`);
121
+ }
122
+ else if (audit.matches.length > 1) {
123
+ refusals.push(`acceptance oracle ${JSON.stringify(audit.criterion)} matches ${audit.matches.length} runner-listed test names`);
124
+ }
125
+ }
126
+ }
127
+ if (refusals.length)
128
+ oracleRefusals.set(task.id, refusals);
129
+ }
130
+ }
131
+ const inputFindings = await taskInputFindings(g.tasks, cwd);
132
+ const inputRefusals = new Map();
133
+ for (const finding of inputFindings) {
134
+ if (finding.severity !== "refuse")
135
+ continue;
136
+ inputRefusals.set(finding.taskId, [...(inputRefusals.get(finding.taskId) ?? []), finding.detail]);
137
+ }
54
138
  const { cfg, mode, source } = resolveRunMode(cwd, { flag: values.mode, spec: g.mode });
55
139
  // readDoctor cache path: staleness line only fires here (probeAll fallback is fresh by construction).
56
140
  const cached = readDoctor(cwd);
@@ -152,6 +236,11 @@ export async function plan(argv, cwd = process.cwd(), adapters = allAdapters(),
152
236
  let cost = 0;
153
237
  const routed = [];
154
238
  for (const t of g.tasks) {
239
+ const refusals = [...(oracleRefusals.get(t.id) ?? []), ...(inputRefusals.get(t.id) ?? [])];
240
+ if (refusals.length) {
241
+ lines.push(` ${t.id.padEnd(6)} ${t.shape.padEnd(10)} !! pre-dispatch refusal — ${refusals.join("; ")}`);
242
+ continue;
243
+ }
155
244
  try {
156
245
  const r = route(t, cfg, channels, dispatchProfile);
157
246
  routed.push({ taskId: t.id, adapter: r.assignment.adapter, model: r.assignment.model });
@@ -192,6 +281,10 @@ export async function plan(argv, cwd = process.cwd(), adapters = allAdapters(),
192
281
  lints.push(`${t.id}: unroutable — ${msg}`);
193
282
  }
194
283
  }
284
+ const inputWarnings = inputFindings.filter((finding) => finding.severity === "warn");
285
+ if (inputWarnings.length) {
286
+ lines.push("", "input warnings:", ...inputWarnings.map((finding) => ` ! ${finding.taskId}: ${finding.detail}`));
287
+ }
195
288
  lines.push("", `est. cost (API channels only, rough): ~$${cost.toFixed(2)} + judge/review/consult calls`);
196
289
  // VIS-04: summary only when a profile is active AND something deviates. Labeled by the switch — off = preview.
197
290
  if (profile && deviations) {
@@ -167,7 +167,7 @@ const railTone = (event, fallback) => {
167
167
  // `gate-result` is the one event whose data IS a gate result; every other row carrying a `gate`
168
168
  // names a gate without reporting one (a start, a reuse, a retry) and keeps its declared tone.
169
169
  if (event.event === "gate-result")
170
- return GATE_OUTCOME_TONES[normalizeGateOutcome(event.data).kind] ?? fallback;
170
+ return GATE_OUTCOME_TONES[normalizeGateOutcome(event.data).kind];
171
171
  const verdict = event.data.pass ?? event.data.ok;
172
172
  return verdict === true ? "pass" : verdict === false ? "fail" : fallback;
173
173
  };
@@ -226,7 +226,24 @@ const railSalient = (data) => RAIL_SALIENT.flatMap(({ key, render }) => {
226
226
  * This is one detail vocabulary stated twice, so `brand-surfaces.test.ts` pins the two against each
227
227
  * other over the whole event corpus: the pipe's bytes must equal event/taskId/THIS detail put through
228
228
  * the formatter's legacy squeeze-and-slice. A ladder that drifts fails there rather than in a tab.
229
+ * Gate-result verdict words are the deliberate exception: the pipe is byte-frozen, while the TTY rail
230
+ * names the normalized outcome so a held selected-test screen is not narrated as a pass.
229
231
  */
232
+ const gateResultDetail = (data) => {
233
+ if (typeof data.gate !== "string")
234
+ return undefined;
235
+ switch (normalizeGateOutcome(data).kind) {
236
+ case "passed":
237
+ return `${data.gate} passed`;
238
+ case "failed":
239
+ return `${data.gate} failed`;
240
+ case "held":
241
+ // The kind is already normalized; the selected-test list only chooses this surface noun.
242
+ return Array.isArray(data.selectedTests) ? `${data.gate} selected-test screen` : `${data.gate} held`;
243
+ default:
244
+ return data.gate;
245
+ }
246
+ };
230
247
  const formatterDetail = ({ event, data }) => {
231
248
  const assignment = data.assignment;
232
249
  const direct = [data.summary, data.reason, data.error, data.step, data.action, data.lint, data.branch, data.from]
@@ -240,7 +257,9 @@ const formatterDetail = ({ event, data }) => {
240
257
  }
241
258
  if (event === "tip-verify")
242
259
  return `${data.gate} passed`;
243
- return `${data.gate}${data.pass === true ? " passed" : data.pass === false ? " failed" : ""}`;
260
+ if (event === "gate-result")
261
+ return gateResultDetail(data);
262
+ return `${data.gate}`;
244
263
  }
245
264
  if (typeof data.code === "number")
246
265
  return `exit ${data.code}`;
@@ -1,8 +1,8 @@
1
1
  import { existsSync, readdirSync } from "node:fs";
2
2
  import { join } from "node:path";
3
- import { classifyFailureOutput } from "../../gates/baseline.js";
4
3
  import { stateDirName } from "../../graph/graph.js";
5
4
  import { Journal } from "../../run/journal.js";
5
+ import { normalizeGateOutcome } from "../../run/outcome.js";
6
6
  // The operator-facing schema is deliberately closed. Adding a fact to the table therefore requires
7
7
  // adding it here as a conscious compatibility change, rather than letting incidental journal fields
8
8
  // leak into an analytics surface.
@@ -35,14 +35,6 @@ const reviewerFrom = (data) => {
35
35
  return undefined;
36
36
  return /\breviewer(?:\s+|:\s*)([\w@./+-]+:[\w@./+-]+)/iu.exec(data.details)?.[1];
37
37
  };
38
- const redEvidence = (data) => {
39
- const fingerprints = Array.isArray(data.fingerprints)
40
- ? data.fingerprints.filter((value) => typeof value === "string")
41
- : [];
42
- const prose = [data.details, data.error, data.reason]
43
- .filter((value) => typeof value === "string");
44
- return [...fingerprints, ...prose].join("\n");
45
- };
46
38
  const historyFor = (tasks, taskId) => {
47
39
  let history = tasks.get(taskId);
48
40
  if (!history) {
@@ -143,12 +135,10 @@ export function collectChannelStats(cwd = process.cwd()) {
143
135
  if (reviewer)
144
136
  channel.reviewers.add(reviewer);
145
137
  }
146
- if (event.data.pass !== false)
147
- continue;
148
- const infra = event.data.infra === true || classifyFailureOutput(redEvidence(event.data)) === "infra";
149
- if (infra)
138
+ const outcome = normalizeGateOutcome(event.data);
139
+ if (outcome.kind === "infra")
150
140
  channel.infraReds += 1;
151
- else
141
+ else if (outcome.kind === "failed")
152
142
  channel.realReds += 1;
153
143
  }
154
144
  // A rescue is task-matched, not attempt-matched: one edge says the failed author and delivering
@@ -1,4 +1,4 @@
1
- import { readFileSync } from "node:fs";
1
+ import { existsSync, readFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { GLYPHS, LIVE } from "../../brand.js";
4
4
  import { DEFAULT_CONFIG, loadConfig } from "../../config/config.js";
@@ -7,12 +7,12 @@ import { formatOwnedName, parseOwnedName, } from "../../drivers/types.js";
7
7
  import { blockedTasks, graphDefinitionHash, loadGraph, stateDirName } from "../../graph/graph.js";
8
8
  import { GATE_NAMES } from "../../graph/schema.js";
9
9
  import { foldActivity } from "../../run/activity.js";
10
- import { Journal, engagementComparable, isQualityFailureParkKind, preservedRefsByTask, recordedTaskFailureKind, runHasEnded, upheldFeedbackByTask, } from "../../run/journal.js";
11
- import { isPidLive } from "../../run/lock.js";
10
+ import { Journal, engagementComparable, isQualityFailureParkKind, parseRunId, preservedRefsByTask, recordedTaskFailureKind, runHasEnded, upheldFeedbackByTask, } from "../../run/journal.js";
11
+ import { isPidLive, runLockOwner } from "../../run/lock.js";
12
12
  import { normalizeGateOutcome } from "../../run/outcome.js";
13
13
  import { desiredPanes } from "../../run/reconcile.js";
14
14
  import { normalizeStallSnapshot } from "../../run/stall.js";
15
- import { armSupervision, readSupervision, supervisionText } from "../../run/supervision.js";
15
+ import { armWatchSupervision, readSupervision, supervisionText } from "../../run/supervision.js";
16
16
  import { deriveRunCockpitData, } from "../../tui/cockpit/derive.js";
17
17
  import { COCKPIT_COLUMN_FLOOR } from "../../tui/cockpit/layout.js";
18
18
  import { cellWidth, fitCells, wrapCells } from "../../tui/cockpit/width.js";
@@ -109,6 +109,15 @@ const ASCII_SPINNER = ["|", "/", "-", "\\"];
109
109
  const SAVE_TERMINAL_TITLE = "\x1b[22;0t";
110
110
  const RESTORE_TERMINAL_TITLE = "\x1b[23;0t";
111
111
  const decisionEvidence = (stateDir, runId, sequence) => `${stateDir}/runs/${runId}/journal.jsonl#L${sequence}`;
112
+ const DECISION_GATE_VERDICTS = {
113
+ passed: "passed",
114
+ failed: "failed",
115
+ skipped: "skipped",
116
+ declined: "skipped",
117
+ held: "unknown",
118
+ unavailable: "unknown",
119
+ infra: "unknown",
120
+ };
112
121
  // v1.79 T4: a deterministic JSONL projection over journal truth. Sequence/evidence come from the
113
122
  // append-only line position, timestamps and claims come from the row, and no watcher-local clock or
114
123
  // filesystem write participates. Re-reading the same bytes therefore returns the same event bytes.
@@ -126,13 +135,7 @@ export const decisionEventsFromJournal = (events, runId, stateDir = ".tickmarkr"
126
135
  return [{ ...base, type: "phase-change", tier: "routine", phase: event.data.phase }];
127
136
  }
128
137
  if (event.event === "gate-result" && typeof event.data.gate === "string") {
129
- const verdict = event.data.skipped === true
130
- ? "skipped"
131
- : event.data.pass === true
132
- ? "passed"
133
- : event.data.pass === false
134
- ? "failed"
135
- : "unknown";
138
+ const verdict = DECISION_GATE_VERDICTS[normalizeGateOutcome(event.data).kind];
136
139
  return [{
137
140
  ...base,
138
141
  type: "gate-verdict",
@@ -381,6 +384,13 @@ export const gateBox = (state, unicode) => {
381
384
  return state === "pass" ? "[x]" : state === "fail" ? "[!]" : state === "skip" ? "." : "[ ]";
382
385
  };
383
386
  export const defaultGateStates = (task) => GATE_NAMES.map((gate) => task.gates.includes(gate) ? "open" : "skip");
387
+ const GATE_OUTCOME_STATES = {
388
+ // The board state is a rendering vocabulary over the normalized outcome, not another field reader.
389
+ passed: "pass",
390
+ failed: "fail",
391
+ skipped: "skip",
392
+ declined: "skip",
393
+ };
384
394
  const gateSnapshot = (task, events, rehashAt) => {
385
395
  const outcomes = new Map();
386
396
  const start = attemptStartIdx(events, task.id);
@@ -389,12 +399,9 @@ const gateSnapshot = (task, events, rehashAt) => {
389
399
  const e = events[eventIndex];
390
400
  if (e.taskId !== task.id || e.event !== "gate-result" || typeof e.data.gate !== "string")
391
401
  continue;
392
- if (e.data.skipped === true)
393
- outcomes.set(e.data.gate, { state: "skip", eventIndex });
394
- else if (e.data.pass === true)
395
- outcomes.set(e.data.gate, { state: "pass", eventIndex });
396
- else if (e.data.pass === false)
397
- outcomes.set(e.data.gate, { state: "fail", eventIndex });
402
+ const state = GATE_OUTCOME_STATES[normalizeGateOutcome(e.data).kind];
403
+ if (state)
404
+ outcomes.set(e.data.gate, { state, eventIndex });
398
405
  }
399
406
  }
400
407
  return {
@@ -519,7 +526,7 @@ const foldTaskEffort = (tasks, events) => {
519
526
  task.dispatches += 1;
520
527
  else if (event.event === "gate-result" && event.data.gate === "review"
521
528
  && event.data.replayMeasurement !== true
522
- && REVIEW_VERDICTS.has(normalizeGateOutcome(event.data.outcome ?? event.data).kind))
529
+ && REVIEW_VERDICTS.has(normalizeGateOutcome(event.data).kind))
523
530
  task.reviews += 1;
524
531
  else if (event.event === "task-human")
525
532
  task.parks += 1;
@@ -843,20 +850,56 @@ const parseJournalSnapshot = (raw) => raw.split("\n").flatMap((line) => {
843
850
  return [];
844
851
  }
845
852
  });
853
+ // runLockOwner is the single lock payload/liveness reader. Its current path helper also initializes
854
+ // .tickmarkr metadata; an engine-written lock necessarily passed through that initializer already, so
855
+ // status avoids invoking it in stripped reader-purity fixtures where no engine lock can exist.
856
+ const canReadRunLockOwner = (cwd) => existsSync(join(cwd, stateDirName(cwd), ".gitignore"));
857
+ const reportableLockRunId = (owner) => {
858
+ if (!owner?.live || owner.runId === undefined)
859
+ return undefined;
860
+ try {
861
+ return parseRunId(owner.runId);
862
+ }
863
+ catch {
864
+ // Repository-wide locks can be held for non-run work such as compile; those are not a status run.
865
+ return undefined;
866
+ }
867
+ };
868
+ const readRunJournalRaw = (cwd, runId, allowMissingJournal) => {
869
+ const dir = join(cwd, stateDirName(cwd), "runs", runId);
870
+ try {
871
+ return readFileSync(join(dir, "journal.jsonl"), "utf8");
872
+ }
873
+ catch (error) {
874
+ if (error.code === "ENOENT") {
875
+ if (allowMissingJournal)
876
+ return "";
877
+ throw new Error(`no journal for ${runId} at ${dir}`);
878
+ }
879
+ throw error;
880
+ }
881
+ };
846
882
  /**
847
883
  * ONE journal read per answer, and every claim about the run folded from THOSE bytes — the board's
848
884
  * task rows, activity, phases, gates, liveness and tip verification, and the compact one-line form
849
885
  * alike. A second read is what lets two snapshots be presented as one state: a line the daemon
850
886
  * appends between them pairs a task count from one instant with a verdict from another.
851
887
  *
852
- * An explicit <runId> is a resolution, not a hint: `Journal.open` refuses an id without a readable
853
- * journal, so status fails loudly naming that id instead of rendering any other run.
888
+ * An explicit <runId> is a resolution, not a hint: it refuses an id without a readable journal, so
889
+ * status fails loudly naming that id instead of rendering any other run. The implicit form first
890
+ * asks the repository lock accessor which run is live, then falls back to the newest journalled run;
891
+ * a lock-selected run may still be in the directory-before-first-journal window, which renders as an
892
+ * empty snapshot for that run rather than borrowing the previous run's journal.
854
893
  */
855
894
  const readRunRecord = (cwd, graph, namedRunId) => {
856
- const runId = namedRunId ?? Journal.latestRunId(cwd, { withJournal: true });
895
+ const explicitRunId = namedRunId === undefined ? undefined : parseRunId(namedRunId);
896
+ const lockedRunId = explicitRunId === undefined && canReadRunLockOwner(cwd)
897
+ ? reportableLockRunId(runLockOwner(cwd))
898
+ : undefined;
899
+ const runId = explicitRunId ?? lockedRunId ?? Journal.latestRunId(cwd, { withJournal: true });
857
900
  if (!runId)
858
901
  return undefined;
859
- const raw = readFileSync(join(Journal.open(cwd, runId).dir, "journal.jsonl"), "utf8");
902
+ const raw = readRunJournalRaw(cwd, runId, lockedRunId === runId);
860
903
  const events = parseJournalSnapshot(raw);
861
904
  // The resume comparator is the fail-closed baseline; a matching graph-rehash is the daemon's
862
905
  // append-only audit that authorizes this status replay after stop-amend-resume.
@@ -1373,15 +1416,19 @@ export async function status(argv, cwd = process.cwd(), opts = {}) {
1373
1416
  let journalCursor = 0;
1374
1417
  const consumeDecisionEvents = () => {
1375
1418
  // A named run is followed, never re-resolved: --watch <runId> keeps reporting that run even as
1376
- // newer runs start. Only the no-argument form tracks latest, as before.
1377
- const runId = namedRunId ?? Journal.latestRunId(cwd, { withJournal: true });
1419
+ // newer runs start. The no-argument form tracks the live lock first, then latest journal.
1420
+ const explicitRunId = namedRunId === undefined ? undefined : parseRunId(namedRunId);
1421
+ const lockedRunId = explicitRunId === undefined && canReadRunLockOwner(cwd)
1422
+ ? reportableLockRunId(runLockOwner(cwd))
1423
+ : undefined;
1424
+ const runId = explicitRunId ?? lockedRunId ?? Journal.latestRunId(cwd, { withJournal: true });
1378
1425
  if (!runId)
1379
1426
  return [];
1380
1427
  if (decisionRunId !== runId) {
1381
1428
  decisionRunId = runId;
1382
1429
  journalCursor = 0;
1383
1430
  }
1384
- const journalEvents = Journal.open(cwd, runId).read();
1431
+ const journalEvents = parseJournalSnapshot(readRunJournalRaw(cwd, runId, lockedRunId === runId));
1385
1432
  if (journalEvents.length < journalCursor)
1386
1433
  journalCursor = 0;
1387
1434
  const fresh = decisionEventsFromJournal(journalEvents, runId, stateDirName(cwd))
@@ -1408,7 +1455,7 @@ export async function status(argv, cwd = process.cwd(), opts = {}) {
1408
1455
  // it INVERTS it: armSupervision's interval outlives the board in any host that outlives one board, so
1409
1456
  // a dead board keeps writing and reads ARMED. That is the over-claiming direction, the one an operator
1410
1457
  // acts on, and the one this instrument exists to close.
1411
- const armed = bounded ? undefined : armSupervision(cwd, "watch", opts.supervisionBeatMs);
1458
+ const armed = bounded ? undefined : armWatchSupervision(cwd, opts.supervisionBeatMs);
1412
1459
  let titleSaved = false;
1413
1460
  const restoreTitle = () => {
1414
1461
  if (!titleSaved)
@@ -10,6 +10,6 @@ export type PlanIR = {
10
10
  tasks: RunGraph["tasks"];
11
11
  };
12
12
  export type PlanFinalizationHook = (plan: PlanIR) => PlanIR;
13
- export declare function finalizePlan(plan: PlanIR, src: string): RunGraph;
13
+ export declare function finalizePlan(plan: PlanIR, src: string, repoRoot?: string): RunGraph;
14
14
  export declare function compileSource(src: string, type?: SourceType, root?: string, beforeFinalize?: PlanFinalizationHook): RunGraph;
15
15
  export {};
@@ -2,6 +2,7 @@ import { existsSync, readFileSync, statSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { validateGraph } from "../graph/schema.js";
4
4
  import { taskUnitContractErrors } from "./collateral.js";
5
+ import { blocksCompile, ownershipFindings, renderOwnershipFinding } from "./ownership.js";
5
6
  import { CompileError } from "./common.js";
6
7
  import { compileGsd, isGsdPhaseDir } from "./gsd.js";
7
8
  import { compileNative, TICKMARKR_NATIVE_MARKER } from "./native.js";
@@ -40,8 +41,8 @@ function enforceTaskUnitContract(g, src) {
40
41
  }
41
42
  return g;
42
43
  }
43
- export function finalizePlan(plan, src) {
44
- const graph = validateGraph({
44
+ export function finalizePlan(plan, src, repoRoot) {
45
+ const graph = enforceTaskUnitContract(validateGraph({
45
46
  version: plan.version,
46
47
  ...(plan.mode !== undefined ? { mode: plan.mode } : {}),
47
48
  spec: {
@@ -51,8 +52,26 @@ export function finalizePlan(plan, src) {
51
52
  ...(plan.base !== undefined ? { base: plan.base } : {}),
52
53
  },
53
54
  tasks: plan.tasks,
54
- });
55
- return enforceTaskUnitContract(graph, src);
55
+ }), src);
56
+ // overseer-217 removal condition, now paid: on this milestone's authored graph the conventional
57
+ // name map emitted 21 raw unowned-test findings; review found 1 real and 20 false, while intersecting
58
+ // with a direct import or command-entry spawn retained the real one and left 0 false positives. That
59
+ // is a precision measurement, NOT a recall claim. The prior halted semantic-contract class has no
60
+ // name/import/ownership relation (one of its three members has no matching literal anywhere), so this
61
+ // rule could not have caught it and does not claim to. The promoted rule already earned a true positive
62
+ // during authoring: assigning the plan command to oracle-preflight left two dedicated plan tests unowned.
63
+ if (repoRoot) {
64
+ const findings = ownershipFindings(graph.tasks, repoRoot);
65
+ const blocking = findings.filter(blocksCompile);
66
+ for (const finding of findings.filter((item) => !blocksCompile(item))) {
67
+ console.warn(renderOwnershipFinding(finding));
68
+ }
69
+ if (blocking.length > 0) {
70
+ throw new CompileError(`${src} violates cross-task test ownership (${blocking.length} error${blocking.length === 1 ? "" : "s"}):\n`
71
+ + blocking.map((finding) => ` - ${renderOwnershipFinding(finding)}`).join("\n"));
72
+ }
73
+ }
74
+ return graph;
56
75
  }
57
76
  function compilePlan(src, type, root) {
58
77
  const kind = type ?? detect(src);
@@ -75,5 +94,5 @@ function compilePlan(src, type, root) {
75
94
  };
76
95
  }
77
96
  export function compileSource(src, type, root, beforeFinalize = (plan) => plan) {
78
- return finalizePlan(beforeFinalize(compilePlan(src, type, root)), src);
97
+ return finalizePlan(beforeFinalize(compilePlan(src, type, root)), src, root);
79
98
  }