tickmarkr 2.2.1 → 2.4.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.
Files changed (81) hide show
  1. package/README.md +10 -9
  2. package/dist/adapters/catalog-remote.d.ts +1 -4
  3. package/dist/adapters/catalog-remote.js +52 -42
  4. package/dist/adapters/catalog.js +5 -3
  5. package/dist/adapters/claude-code.d.ts +1 -1
  6. package/dist/adapters/claude-code.js +8 -5
  7. package/dist/adapters/model-lints.d.ts +9 -5
  8. package/dist/adapters/model-lints.js +56 -15
  9. package/dist/adapters/model-windows.js +11 -0
  10. package/dist/adapters/prompt.js +1 -0
  11. package/dist/adapters/qwen.d.ts +5 -0
  12. package/dist/adapters/qwen.js +153 -0
  13. package/dist/adapters/types.d.ts +21 -1
  14. package/dist/adapters/types.js +43 -2
  15. package/dist/cli/commands/approve.js +5 -4
  16. package/dist/cli/commands/beat.js +7 -4
  17. package/dist/cli/commands/compile.js +32 -6
  18. package/dist/cli/commands/doctor.d.ts +9 -4
  19. package/dist/cli/commands/doctor.js +87 -13
  20. package/dist/cli/commands/fleet.d.ts +4 -0
  21. package/dist/cli/commands/fleet.js +53 -14
  22. package/dist/cli/commands/init.js +36 -21
  23. package/dist/cli/commands/plan.js +45 -7
  24. package/dist/cli/commands/report.js +37 -1
  25. package/dist/cli/commands/status.d.ts +1 -0
  26. package/dist/cli/commands/status.js +45 -1
  27. package/dist/cli/commands/verify.d.ts +6 -0
  28. package/dist/cli/commands/verify.js +145 -25
  29. package/dist/cli/index.d.ts +1 -1
  30. package/dist/cli/index.js +2 -2
  31. package/dist/compile/collateral.d.ts +2 -9
  32. package/dist/compile/collateral.js +17 -18
  33. package/dist/compile/index.d.ts +4 -1
  34. package/dist/compile/index.js +41 -7
  35. package/dist/compile/native.d.ts +4 -2
  36. package/dist/compile/native.js +58 -9
  37. package/dist/compile/ownership.js +34 -9
  38. package/dist/config/config.d.ts +1 -0
  39. package/dist/config/config.js +52 -6
  40. package/dist/drivers/herdr.d.ts +1 -0
  41. package/dist/drivers/herdr.js +11 -1
  42. package/dist/drivers/index.d.ts +6 -0
  43. package/dist/drivers/index.js +19 -4
  44. package/dist/drivers/orca.d.ts +35 -1
  45. package/dist/drivers/orca.js +260 -20
  46. package/dist/drivers/subprocess.d.ts +3 -3
  47. package/dist/drivers/subprocess.js +16 -9
  48. package/dist/drivers/types.d.ts +12 -0
  49. package/dist/gates/baseline.d.ts +2 -0
  50. package/dist/gates/baseline.js +47 -11
  51. package/dist/gates/llm.d.ts +6 -0
  52. package/dist/gates/llm.js +25 -9
  53. package/dist/gates/review.d.ts +7 -3
  54. package/dist/gates/review.js +61 -22
  55. package/dist/gates/run-gates.d.ts +5 -2
  56. package/dist/gates/run-gates.js +50 -26
  57. package/dist/gates/verdict-cause.d.ts +6 -2
  58. package/dist/gates/verdict-cause.js +8 -4
  59. package/dist/route/preference.d.ts +4 -0
  60. package/dist/route/preference.js +40 -0
  61. package/dist/route/router.js +15 -2
  62. package/dist/run/consult.d.ts +1 -0
  63. package/dist/run/consult.js +39 -8
  64. package/dist/run/daemon.d.ts +16 -0
  65. package/dist/run/daemon.js +345 -74
  66. package/dist/run/git.d.ts +3 -0
  67. package/dist/run/git.js +40 -5
  68. package/dist/run/journal.d.ts +15 -2
  69. package/dist/run/journal.js +73 -12
  70. package/dist/run/supervision.d.ts +6 -0
  71. package/dist/run/supervision.js +29 -1
  72. package/dist/tui/ink/fleet-app.d.ts +4 -0
  73. package/dist/tui/ink/fleet-app.js +45 -16
  74. package/dist/tui/ink/init-app.js +4 -4
  75. package/package.json +59 -1
  76. package/skills/tickmarkr-overseer/SKILL.md +77 -18
  77. package/skills/tickmarkr-overseer/scripts/seat-send.sh +88 -18
  78. package/skills/tickmarkr-overseer/scripts/watch-artifacts.sh +36 -2
  79. package/skills/tickmarkr-overseer/scripts/watch-contamination.sh +36 -15
  80. package/skills/tickmarkr-overseer/scripts/watch-context.sh +33 -7
  81. package/skills/tickmarkr-overseer/scripts/watch-pending-input.sh +32 -8
@@ -1,5 +1,6 @@
1
+ import { execFileSync } from "node:child_process";
1
2
  import { appendFileSync, cpSync, existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from "node:fs";
2
- import { join } from "node:path";
3
+ import { join, relative } from "node:path";
3
4
  import { fileURLToPath } from "node:url";
4
5
  import { createInterface } from "node:readline/promises";
5
6
  import { parseArgs } from "node:util";
@@ -8,20 +9,20 @@ import { configTemplate, DEFAULT_CONFIG, globalConfigDir, loadConfig } from "../
8
9
  import { LEGACY_PREFIX, specTemplate } from "../../compile/native.js";
9
10
  import { BANNER, kvRow, legend, rule, statusRow, title } from "../../brand.js";
10
11
  import { tickmarkrDir } from "../../graph/graph.js";
12
+ import { orcaHostDetected } from "../../drivers/index.js";
11
13
  import { Journal } from "../../run/journal.js";
12
14
  import { doctor } from "./doctor.js";
13
15
  import { assembleFleetEditor } from "./fleet.js";
14
16
  const SCAFFOLD_SPEC = "tickmarkr.spec.md";
15
17
  // Operator-approved (2026-07-17) environments footer — no npm install for herdr (npm package
16
- // "herdr" is a reserved 0.0.0 placeholder as of that date). The orca row joins it with the v2.1
17
- // driver: it is a THIRD execution surface an operator selects outright — `auto` still resolves
18
- // herdr-else-subprocess and never picks it, so the footer names it beside the other two choices.
18
+ // "herdr" is a reserved 0.0.0 placeholder as of that date). Auto resolves herdr → orca →
19
+ // subprocess; the orca leg is environment-only, using the two markers Orca stamps in its terminals.
19
20
  const ENVIRONMENTS_FOOTER = [
20
21
  "environments:",
21
- " herdr — the full cockpit — every worker, judge, and consult is a visible pane you can watch and unblock · https://herdr.dev",
22
- " orca — visible terminals in the Orca app — an explicit driver choice: set driver: orca (auto never picks it) · https://onorca.dev",
22
+ " herdr — the full cockpit — every worker, judge, and consult is a visible pane you can watch and unblock · https://herdr.dev · auto picks it first when HERDR_ENV=1",
23
+ " orca — visible terminals in the Orca app — auto picks it inside an Orca terminal after herdr and before subprocess; set driver: orca to force it · https://onorca.dev",
23
24
  " claude code — tickmarkr init --agent installs the /tkr skills + AGENTS.md so Claude Code (or any agent CLI) drives the loop natively",
24
- " anywhere — no herdr? same fail-closed gates, headless subprocess driver",
25
+ " anywhere — no herdr or Orca terminal? same fail-closed gates, headless subprocess driver (legacy: no herdr? same fail-closed gates, headless subprocess driver when Orca markers are absent)",
25
26
  ].join("\n");
26
27
  /** Latest journal without a run-end event, if any. */
27
28
  function activeRunId(cwd) {
@@ -140,17 +141,6 @@ you were handed the whole picture — list the repository root, open every guida
140
141
  direction each moves, what makes a criterion real, and why an absence or a source-text grep is never a
141
142
  criterion. Read it before authoring or repairing acceptance items.
142
143
  - **Editing \`src/gates/\`, \`src/compile/\` or \`src/graph/\`?** Read \`docs/codebase/ARCHITECTURE.md\` first.
143
- - **EVERY fix gets a ship/no-ship decision, recorded, at the moment it is made.** Ask one question of each
144
- one: *does a user hit this defect?* If yes, the fix belongs in \`src/**\` or \`skills/**\` — the only trees
145
- the package carries (\`files: [dist, schema, skills, fixtures]\`). A script, overlay, config entry or
146
- operator-side workaround that resolves the symptom **locally is not the fix; it is a decision to leave
147
- every other user broken**, and it must say so in writing and name the condition that removes it.
148
- **The default answer is SHIP.** A local remedy is the exception and carries the burden of proof.
149
- Watch for the three shapes this hides in: a fix applied where you happened to be standing rather than
150
- where the defect lives; an observation filed with a product fix named in its own text and queued
151
- nowhere; and a local tool that quietly grows into a product feature nobody shipped. **A defect and its
152
- fix must be recorded in the same place, or the queue silently becomes a list of things everyone assumed
153
- someone else had shipped.**
154
144
  ${DOCS_END}
155
145
  `;
156
146
  // Every applicable host location gets the skills, each paired with its own repository guidance
@@ -229,7 +219,28 @@ const describeDrift = (d) => [
229
219
  // should not be asked again just because a copy drifted. Staleness is a different question with a
230
220
  // different answer (`--agent --force`), and it is reported by name on every init path below.
231
221
  const skillsInstalled = (cwd) => hostTargets(cwd).every((t) => AGENT_SKILLS.every((s) => existsSync(join(t.skillsDir, s, "SKILL.md"))));
232
- const wizardDriverDefault = () => process.env.HERDR_ENV === "1" ? "herdr" : "auto";
222
+ const wizardDriverDefault = () => process.env.HERDR_ENV === "1" ? "herdr" : orcaHostDetected() ? "orca" : "auto";
223
+ function packageName(cwd) {
224
+ try {
225
+ const parsed = JSON.parse(readFileSync(join(cwd, "package.json"), "utf8"));
226
+ return typeof parsed.name === "string" ? parsed.name : null;
227
+ }
228
+ catch {
229
+ return null;
230
+ }
231
+ }
232
+ function gitTracked(cwd, path) {
233
+ const rel = relative(cwd, path);
234
+ if (rel.startsWith(".."))
235
+ return false;
236
+ try {
237
+ execFileSync("git", ["ls-files", "--error-unmatch", "--", rel], { cwd, stdio: "ignore" });
238
+ return true;
239
+ }
240
+ catch {
241
+ return false;
242
+ }
243
+ }
233
244
  async function installAgentFiles(cwd, force, docs, notes) {
234
245
  const interactive = process.stdin.isTTY === true && process.stdout.isTTY === true;
235
246
  let prompt;
@@ -257,13 +268,14 @@ async function installAgentFiles(cwd, force, docs, notes) {
257
268
  notes.push(`kept current ${dest}`);
258
269
  continue;
259
270
  }
271
+ const tracked = exists && gitTracked(cwd, dest);
260
272
  if (exists && !force && !(await confirm(`Overwrite ${dest} (${describeDrift(stale)})?`))) {
261
- notes.push(`skipped existing ${dest} — ${describeDrift(stale)}; pass --force to overwrite it`);
273
+ notes.push(`skipped existing ${tracked ? "tracked " : ""}${dest} — ${describeDrift(stale)}; pass --force to overwrite it`);
262
274
  continue;
263
275
  }
264
276
  // whole skill dir, not just SKILL.md — the overseer ships its pane-watcher script
265
277
  cpSync(fileURLToPath(new URL(`../../../skills/${skill}`, import.meta.url)), join(skillsDir, skill), { recursive: true });
266
- notes.push(`${exists ? "overwrote" : "wrote"} ${dest}`);
278
+ notes.push(`${exists ? "overwrote" : "wrote"} ${tracked ? "tracked " : ""}${dest}`);
267
279
  }
268
280
  const current = existsSync(docPath) ? readFileSync(docPath, "utf8") : "";
269
281
  // The block is CANONICAL guidance ("stated once here, restated nowhere"), so a repo that has
@@ -341,6 +353,9 @@ export async function init(argv, cwd = process.cwd(), io = {}) {
341
353
  });
342
354
  // banner at START on the visual surface — every init path, not just the wizard, and never trailing the probe (operator report 2026-07-17)
343
355
  emitBanner();
356
+ if (packageName(cwd) === "tickmarkr") {
357
+ throw new Error("OBS-584: tickmarkr init refuses to run inside the tickmarkr source repository");
358
+ }
344
359
  const gdir = values["global-dir"] ?? globalConfigDir();
345
360
  mkdirSync(gdir, { recursive: true });
346
361
  const notes = [];
@@ -1,18 +1,20 @@
1
- import { allAdapters, discoverChannels, doctorAgeMs, modelAuthExclusions, probeAll, readDoctor, servableExclusions, servabilityLine } from "../../adapters/registry.js";
1
+ import { existsSync } from "node:fs";
2
+ import { join } from "node:path";
3
+ import { parseArgs } from "node:util";
4
+ import { allAdapters, doctorAgeMs, modelAuthExclusions, probeAll, readDoctor, rolePools, servableExclusions, servabilityLine } from "../../adapters/registry.js";
2
5
  import { formatModelAuthLine, contextWindowLints, modelLints, preferEntryLints, ttyVisual } from "../../adapters/model-lints.js";
3
6
  import { GLYPHS, dim, rule, title, warn } from "../../brand.js";
4
- import { parseArgs } from "node:util";
5
7
  import { collateralLints, sourceScopeLints } from "../../compile/collateral.js";
6
8
  import { classifyContextPath } from "../../compile/native.js";
7
9
  import { DEFAULT_CONFIG, overlayPreferShapes, ROUTING_MODES, TIER_RANK } from "../../config/config.js";
8
10
  import { loadGraph } from "../../graph/graph.js";
9
11
  import { renderAcceptanceItem } from "../../graph/schema.js";
10
12
  import { resolveRunMode } from "../../run/daemon.js";
11
- import { excludedChannels, exclusionLine } from "../../route/preference.js";
13
+ import { disallowedBy, excludedChannels, exclusionLine, routingEntrySeatLines } from "../../route/preference.js";
12
14
  import { staffLedEvidence } from "../../route/profile.js";
13
15
  import { route, RoutingError } from "../../route/router.js";
14
16
  import { auditNamedTestOracles, listVitestTests } from "../../gates/acceptance.js";
15
- import { modelId } from "../../gates/review.js";
17
+ import { modelId, modelProvider } from "../../gates/review.js";
16
18
  import { loadRoutingProfile } from "../../run/journal.js";
17
19
  import { harnessLine, resolveHarness } from "../harness.js";
18
20
  import { shq } from "../../adapters/types.js";
@@ -106,10 +108,15 @@ export async function plan(argv, cwd = process.cwd(), adapters = allAdapters(),
106
108
  }
107
109
  const g = loadGraph(cwd);
108
110
  const oracleRefusals = new Map();
111
+ const oracleAdvisories = new Map();
109
112
  if (g.tasks.some((task) => task.acceptance.some((item) => typeof item === "object" && item.oracle === "test"))) {
110
113
  const listing = await (opts.listTests ?? listVitestTests)(cwd);
111
114
  for (const task of g.tasks) {
112
115
  const refusals = [];
116
+ const advisories = [];
117
+ const authoredTest = task.files
118
+ .map((path) => path.replace(/^\.\//, ""))
119
+ .find((path) => /^tests\/.+\.test\.ts$/.test(path) && !/[?*{[]/.test(path) && !existsSync(join(cwd, path)));
113
120
  if (listing.status === "failed") {
114
121
  if (task.acceptance.some((item) => typeof item === "object" && item.oracle === "test")) {
115
122
  refusals.push(`acceptance oracle unresolved — runner listing failed: ${listing.error.split("\n")[0]}`);
@@ -117,7 +124,10 @@ export async function plan(argv, cwd = process.cwd(), adapters = allAdapters(),
117
124
  }
118
125
  else {
119
126
  for (const audit of auditNamedTestOracles(task.acceptance, listing.tests)) {
120
- if (audit.matches.length === 0) {
127
+ if (audit.matches.length === 0 && authoredTest) {
128
+ advisories.push(`acceptance oracle ${JSON.stringify(audit.criterion)} not yet written (worker authors ${authoredTest})`);
129
+ }
130
+ else if (audit.matches.length === 0) {
121
131
  refusals.push(`acceptance oracle ${JSON.stringify(audit.criterion)} matches zero runner-listed test names`);
122
132
  }
123
133
  else if (audit.matches.length > 1) {
@@ -127,6 +137,8 @@ export async function plan(argv, cwd = process.cwd(), adapters = allAdapters(),
127
137
  }
128
138
  if (refusals.length)
129
139
  oracleRefusals.set(task.id, refusals);
140
+ if (advisories.length)
141
+ oracleAdvisories.set(task.id, advisories);
130
142
  }
131
143
  }
132
144
  const inputFindings = await taskInputFindings(g.tasks, cwd);
@@ -141,7 +153,8 @@ export async function plan(argv, cwd = process.cwd(), adapters = allAdapters(),
141
153
  // readDoctor cache path: staleness line only fires here (probeAll fallback is fresh by construction).
142
154
  const cached = readDoctor(cwd);
143
155
  const health = cached ?? (await probeAll(adapters));
144
- const channels = discoverChannels(cfg, adapters, health);
156
+ const pools = rolePools(cfg, adapters, health);
157
+ const channels = pools.worker;
145
158
  // VIS-04 trust ramp (VALIDATION 13-01-11): preview:true bypasses the routing.learned:off short-circuit so
146
159
  // the static-vs-learned comparison renders even when the daemon's learned routing is off. Cold ⇒ undefined ⇒
147
160
  // output byte-identical to today.
@@ -163,6 +176,13 @@ export async function plan(argv, cwd = process.cwd(), adapters = allAdapters(),
163
176
  harnessLine(resolveHarness(harnessFrom)),
164
177
  "",
165
178
  ];
179
+ const judgeDeny = disallowedBy(cfg.judge, cfg.routing, "judge");
180
+ if (judgeDeny?.by === "deny") {
181
+ lines.push(`REFUSAL OBS-576: judge seat ${cfg.judge.adapter}:${cfg.judge.model} is removed by flat routing.deny (${judgeDeny.entry}) — move worker-only policy to routing.deny.workers`, "");
182
+ }
183
+ const entrySeats = routingEntrySeatLines(cfg);
184
+ if (entrySeats.length)
185
+ lines.push(...entrySeats, "");
166
186
  const derivation = (shape) => {
167
187
  const floor = cfg.routing.floors[shape];
168
188
  return floor ? ` floor ${floor} ← ${mode.provenance[shape] ?? "config floors"}` : null;
@@ -223,6 +243,22 @@ export async function plan(argv, cwd = process.cwd(), adapters = allAdapters(),
223
243
  lints.push(...modelLints(cfg, health, adapters, { tty: ttyVisual() })); // health may be pre-v1.5/probeAll-fallback — no-detection branch covers both
224
244
  // v1.54 T3: dead-steering sweep — advisory only, renders with the routing lints, never alters routing.
225
245
  lints.push(...preferEntryLints(cfg, health, overlayPreferShapes(cwd)));
246
+ const preferMatches = (entry, pool) => pool.some((c) => entry === c.adapter || entry === `${c.adapter}:${c.model}`);
247
+ const lintInertPrefer = (surface, entries, poolName, pool) => {
248
+ for (const entry of entries ?? []) {
249
+ if (!preferMatches(entry, pool))
250
+ lints.push(`${surface} '${entry}' resolves to no live channel of ${poolName} pool`);
251
+ }
252
+ };
253
+ for (const [shape, entry] of Object.entries(cfg.routing.map)) {
254
+ lintInertPrefer(`routing.map.${shape}.prefer`, entry.prefer, "worker", pools.worker);
255
+ }
256
+ lintInertPrefer("review.prefer", cfg.review.prefer, "review", pools.review);
257
+ lintInertPrefer("consult.prefer", cfg.consult.prefer, "consult", pools.consult);
258
+ const reviewProviders = new Set(pools.review.map((c) => modelProvider(c.model, c.vendor)));
259
+ if (pools.review.length > 0 && reviewProviders.size < 2) {
260
+ lints.push(`review pool concentration: ${pools.review.length} channel(s), ${reviewProviders.size} provider(s)`);
261
+ }
226
262
  // The guard here was `channels.length && !fleetCanCrossVendorReview(channels)`, so the review lint went
227
263
  // SILENT at zero channels — the first-run state, where review.required is set and nothing can route at all.
228
264
  // Deleting that clause outright is worse than the silence: it fires "no cross-vendor reviewer pair in fleet"
@@ -233,7 +269,7 @@ export async function plan(argv, cwd = process.cwd(), adapters = allAdapters(),
233
269
  if (cfg.review.required && channels.length === 0) {
234
270
  lints.push("review: review.required is set but no channel can route — the fleet is empty; install or authenticate an adapter");
235
271
  }
236
- else if (cfg.review.required && !fleetCanCrossVendorReview(channels)) {
272
+ else if (cfg.review.required && !fleetCanCrossVendorReview(pools.review)) {
237
273
  lints.push("review: no cross-vendor reviewer pair in fleet — set review.required: false to waive");
238
274
  }
239
275
  let cost = 0;
@@ -283,6 +319,8 @@ export async function plan(argv, cwd = process.cwd(), adapters = allAdapters(),
283
319
  lines.push(d);
284
320
  lints.push(`${t.id}: unroutable — ${msg}`);
285
321
  }
322
+ for (const advisory of oracleAdvisories.get(t.id) ?? [])
323
+ lines.push(` ! ${advisory}`);
286
324
  }
287
325
  const inputWarnings = inputFindings.filter((finding) => finding.severity === "warn");
288
326
  if (inputWarnings.length) {
@@ -4,6 +4,7 @@ import { ttyVisual } from "../../adapters/model-lints.js";
4
4
  import { addUsage } from "../../adapters/types.js";
5
5
  import { dim, rule, title } from "../../brand.js";
6
6
  import { loadConfig } from "../../config/config.js";
7
+ import { modelProvider } from "../../gates/review.js";
7
8
  import { buildProofBundle, gateDeclined } from "../../report/bundle.js";
8
9
  import { compareRuns } from "../../report/compare.js";
9
10
  import { estimateCosts } from "../../report/cost.js";
@@ -35,6 +36,22 @@ const channelLabel = (data) => {
35
36
  const { adapter, model } = a;
36
37
  return typeof adapter === "string" && typeof model === "string" ? `${adapter}:${model}` : EM;
37
38
  };
39
+ const modelFromChannel = (channel) => channel.slice(channel.indexOf(":") + 1);
40
+ const reviewerIdentity = (data) => {
41
+ const details = typeof data.details === "string" ? data.details : "";
42
+ const fromDetails = /reviewer\s+([^\s;()]+:[^\s;()]+)/i.exec(details)?.[1];
43
+ const reviewer = typeof data.reviewer === "string" ? data.reviewer : fromDetails;
44
+ if (!reviewer || !reviewer.includes(":"))
45
+ return undefined;
46
+ const vendor = typeof data.vendor === "string"
47
+ ? data.vendor
48
+ : /vendor:\s*([^;()]+)/i.exec(details)?.[1]?.trim()
49
+ ?? /\(([^;:()]+)\):/.exec(details)?.[1]?.trim();
50
+ const provider = typeof data.provider === "string"
51
+ ? data.provider
52
+ : modelProvider(modelFromChannel(reviewer), vendor ?? "unknown");
53
+ return { reviewer, provider, ...(vendor ? { vendor } : {}) };
54
+ };
38
55
  const rateBasis = (row) => {
39
56
  if (!row.rate)
40
57
  return EM;
@@ -245,6 +262,7 @@ export function renderMarkdownRecord(runId, events, prices = [], rows = []) {
245
262
  const dispatches = events.filter((e) => e.taskId === taskId && e.event === "task-dispatch");
246
263
  const channels = dispatches.map((e) => channelLabel(e.data));
247
264
  const gates = events.filter((e) => e.taskId === taskId && e.event === "gate-result");
265
+ const leg2 = events.filter((e) => e.taskId === taskId && e.event === "review-leg2");
248
266
  const consults = events.filter((e) => e.taskId === taskId && e.event === "consult-verdict");
249
267
  const deviations = events.filter((e) => e.taskId === taskId && e.event === "route-deviation");
250
268
  const merge = [...events].reverse().find((e) => e.taskId === taskId && e.event === "merge");
@@ -267,10 +285,28 @@ export function renderMarkdownRecord(runId, events, prices = [], rows = []) {
267
285
  if (gates.length) {
268
286
  for (const g of gates) {
269
287
  // T11: a declined gate never ran — report it as declined, never as a pass.
270
- const pass = gateDeclined(g.data) ? "declined" : g.data.pass === true ? "pass" : g.data.pass === false ? "fail" : EM;
288
+ let pass = gateDeclined(g.data) ? "declined" : g.data.pass === true ? "pass" : g.data.pass === false ? "fail" : EM;
271
289
  const gate = typeof g.data.gate === "string" ? g.data.gate : EM;
290
+ if (gate === "review" && pass === "pass") {
291
+ const reviewer = reviewerIdentity(g.data);
292
+ const before = events.slice(0, events.indexOf(g) + 1).reverse()
293
+ .find((e) => e.taskId === taskId && e.event === "task-dispatch");
294
+ const author = channelLabel(before?.data ?? {});
295
+ const authorProvider = author === EM ? "unknown" : modelProvider(modelFromChannel(author));
296
+ if (reviewer && authorProvider !== "unknown" && authorProvider === reviewer.provider) {
297
+ pass = "pass (same-provider — independence not established)";
298
+ }
299
+ }
272
300
  lines.push(` - ${gate}: ${pass} — ${firstLine(g.data.details)}`);
273
301
  }
302
+ for (const row of leg2) {
303
+ const pass = row.data.pass === true ? "pass" : row.data.pass === false ? "fail" : EM;
304
+ const identity = reviewerIdentity(row.data);
305
+ const identityText = identity
306
+ ? `reviewer ${identity.reviewer}${identity.vendor ? ` (vendor: ${identity.vendor}; provider: ${identity.provider})` : ` (provider: ${identity.provider})`}`
307
+ : firstLine(row.data.details);
308
+ lines.push(` - review-leg2: ${pass} (supersedes daemon review) — ${identityText}`);
309
+ }
274
310
  }
275
311
  else
276
312
  lines.push(` - ${EM}`);
@@ -21,6 +21,7 @@ export declare const GATE_KEYS: {
21
21
  readonly acceptance: "A";
22
22
  readonly review: "R";
23
23
  };
24
+ export declare const STATUS_HELP = "usage: tickmarkr status [<runId>] [--watch [--events]] [--oneline]\nWith --watch --events, stdout is one JSON document per decision event and stderr carries keepalive lines.\nDo not merge stdout and stderr (for example with 2>&1): doing so corrupts the JSON document stream.";
24
25
  export declare const decisionEventsFromJournal: (events: JournalEvent[], runId: string, stateDir?: string) => DecisionEvent[];
25
26
  export declare const taskBox: (status: TaskStatus) => string;
26
27
  export declare const gateBox: (state: "open" | "pass" | "fail" | "skip", unicode: boolean) => string;
@@ -1,4 +1,5 @@
1
1
  import { existsSync, readFileSync } from "node:fs";
2
+ import * as os from "node:os";
2
3
  import { join } from "node:path";
3
4
  import { GLYPHS, LIVE } from "../../brand.js";
4
5
  import { DEFAULT_CONFIG, loadConfig } from "../../config/config.js";
@@ -108,6 +109,11 @@ const SPINNER = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇",
108
109
  const ASCII_SPINNER = ["|", "/", "-", "\\"];
109
110
  const SAVE_TERMINAL_TITLE = "\x1b[22;0t";
110
111
  const RESTORE_TERMINAL_TITLE = "\x1b[23;0t";
112
+ const ENTER_ALTERNATE_SCREEN = "\x1b[?1049h";
113
+ const LEAVE_ALTERNATE_SCREEN = "\x1b[?1049l";
114
+ export const STATUS_HELP = `usage: tickmarkr status [<runId>] [--watch [--events]] [--oneline]
115
+ With --watch --events, stdout is one JSON document per decision event and stderr carries keepalive lines.
116
+ Do not merge stdout and stderr (for example with 2>&1): doing so corrupts the JSON document stream.`;
111
117
  const decisionEvidence = (stateDir, runId, sequence) => `${stateDir}/runs/${runId}/journal.jsonl#L${sequence}`;
112
118
  const DECISION_GATE_VERDICTS = {
113
119
  passed: "passed",
@@ -1109,7 +1115,7 @@ binaryVersion, now = Date.now(), animationFrame = 0, workerLiveness = new Map(),
1109
1115
  const tally = tipPhase
1110
1116
  ? `${progressTone(`${progressGlyph} ${done}/${total} tasks done`)}${dot}${progressTone("run not verified")}`
1111
1117
  : done === total && total > 0 ? ok(`${done}/${total} done`) : information(`${done}/${total} done`);
1112
- const live = liveness(events, daemon, now)
1118
+ const live = `${liveness(events, daemon, now)} · load1 ${os.loadavg()[0].toFixed(2)}`
1113
1119
  .replace(/\bdead\b/u, fail("dead"))
1114
1120
  .replace(/\bfinished\b/u, dim("finished"))
1115
1121
  .replace(/\balive\b/u, running("alive"))
@@ -1324,6 +1330,8 @@ const oneLine = (cwd, namedRunId) => {
1324
1330
  return sanitizeTaskText([record.runId, ...claims].join(" · "));
1325
1331
  };
1326
1332
  export async function status(argv, cwd = process.cwd(), opts = {}) {
1333
+ if (argv.some((arg) => arg === "--help" || arg === "-h"))
1334
+ return STATUS_HELP;
1327
1335
  const namedRunId = positionalRunId(argv);
1328
1336
  if (argv.includes("--oneline"))
1329
1337
  return oneLine(cwd, namedRunId);
@@ -1456,6 +1464,13 @@ export async function status(argv, cwd = process.cwd(), opts = {}) {
1456
1464
  // a dead board keeps writing and reads ARMED. That is the over-claiming direction, the one an operator
1457
1465
  // acts on, and the one this instrument exists to close.
1458
1466
  const armed = bounded ? undefined : armWatchSupervision(cwd, opts.supervisionBeatMs);
1467
+ let alternateScreen = false;
1468
+ const leaveAlternateScreen = () => {
1469
+ if (!alternateScreen)
1470
+ return;
1471
+ alternateScreen = false;
1472
+ process.stdout.write(LEAVE_ALTERNATE_SCREEN);
1473
+ };
1459
1474
  let titleSaved = false;
1460
1475
  const restoreTitle = () => {
1461
1476
  if (!titleSaved)
@@ -1478,6 +1493,28 @@ export async function status(argv, cwd = process.cwd(), opts = {}) {
1478
1493
  }
1479
1494
  process.stdout.write(`\x1b]0;⏳ ${hotPhase.taskId} ${phaseLabel(hotPhase)} ${fmtElapsed(nowMs - hotPhase.startedAt)}\x07`);
1480
1495
  };
1496
+ const signalHandlers = [];
1497
+ const exitFromSignal = (code) => () => {
1498
+ if (titleSaved) {
1499
+ process.removeListener("exit", restoreTitle);
1500
+ restoreTitle();
1501
+ }
1502
+ if (alternateScreen) {
1503
+ process.removeListener("exit", leaveAlternateScreen);
1504
+ leaveAlternateScreen();
1505
+ }
1506
+ process.exit(code);
1507
+ };
1508
+ if (!bounded && !eventStream && tty) {
1509
+ process.stdout.write(ENTER_ALTERNATE_SCREEN);
1510
+ alternateScreen = true;
1511
+ process.once("exit", leaveAlternateScreen);
1512
+ for (const [signal, code] of [["SIGINT", 130], ["SIGTERM", 143]]) {
1513
+ const handler = exitFromSignal(code);
1514
+ signalHandlers.push([signal, handler]);
1515
+ process.once(signal, handler);
1516
+ }
1517
+ }
1481
1518
  try {
1482
1519
  for (let i = 0; i < iterations; i++) {
1483
1520
  const nowMs = now();
@@ -1490,6 +1527,7 @@ export async function status(argv, cwd = process.cwd(), opts = {}) {
1490
1527
  if (bounded)
1491
1528
  eventLines.push(line);
1492
1529
  }
1530
+ process.stderr.write("status: keepalive\n");
1493
1531
  }
1494
1532
  else {
1495
1533
  frame = renderFrame(cwd, binaryVersion, nowMs, i, workerLiveness, true, namedRunId);
@@ -1525,6 +1563,12 @@ export async function status(argv, cwd = process.cwd(), opts = {}) {
1525
1563
  process.removeListener("exit", restoreTitle);
1526
1564
  restoreTitle();
1527
1565
  }
1566
+ for (const [signal, handler] of signalHandlers)
1567
+ process.removeListener(signal, handler);
1568
+ if (alternateScreen) {
1569
+ process.removeListener("exit", leaveAlternateScreen);
1570
+ leaveAlternateScreen();
1571
+ }
1528
1572
  }
1529
1573
  return bounded ? eventStream ? eventLines.join("\n") : frames.join(sep) : "";
1530
1574
  }
@@ -4,6 +4,12 @@ export declare function parseCriteria(text: string): AcceptanceItem[];
4
4
  export declare const HUMAN_CHANNEL: BillingChannel;
5
5
  export declare const HUMAN_AUTHOR: Assignment;
6
6
  export declare function verifyStateDir(cwd: string): string;
7
+ /** Linked worktrees share operational state with the repository that owns the common git dir. */
8
+ export declare function verifyStateRoot(cwd: string): Promise<string>;
9
+ /** Cache identity is repository content, never a checkout path. */
10
+ export declare function baselineCachePath(cwd: string, baseSha: string, commands: Record<string, string>): string;
11
+ export declare function excludeAuthorProvider(channels: BillingChannel[], author: BillingChannel): BillingChannel[];
12
+ export declare const VERIFY_HELP = "usage: tickmarkr verify [--base <ref>] [--criteria <file> | --task <id>] [--json]\nThe final verdict and JSON result are written to stdout; progress and diagnostics are written to stderr.\nDo not merge stdout and stderr (for example with 2>&1): doing so corrupts the verdict stream.";
7
13
  export declare function verify(argv: string[], cwd?: string): Promise<{
8
14
  out: string;
9
15
  code: number;