tickmarkr 2.5.3 → 2.5.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (71) hide show
  1. package/dist/adapters/registry.js +6 -1
  2. package/dist/adapters/types.d.ts +3 -0
  3. package/dist/cli/commands/approve.d.ts +1 -0
  4. package/dist/cli/commands/approve.js +54 -6
  5. package/dist/cli/commands/doctor.d.ts +4 -0
  6. package/dist/cli/commands/doctor.js +44 -29
  7. package/dist/cli/commands/fleet.js +195 -33
  8. package/dist/cli/commands/init.js +196 -6
  9. package/dist/cli/commands/plan.js +144 -3
  10. package/dist/cli/commands/resume.js +6 -2
  11. package/dist/cli/commands/run.js +21 -2
  12. package/dist/cli/commands/verify.js +1 -1
  13. package/dist/cli/help.d.ts +2 -0
  14. package/dist/cli/help.js +3 -1
  15. package/dist/compile/collateral.d.ts +2 -0
  16. package/dist/compile/collateral.js +50 -9
  17. package/dist/compile/ownership.d.ts +7 -0
  18. package/dist/compile/ownership.js +59 -22
  19. package/dist/config/config.d.ts +23 -8
  20. package/dist/config/config.js +42 -28
  21. package/dist/config/fleet-overlay.d.ts +3 -9
  22. package/dist/config/fleet-overlay.js +68 -11
  23. package/dist/config/fleet-why.d.ts +7 -0
  24. package/dist/config/fleet-why.js +5 -0
  25. package/dist/drivers/herdr.d.ts +5 -0
  26. package/dist/drivers/herdr.js +14 -3
  27. package/dist/drivers/index.d.ts +15 -1
  28. package/dist/drivers/index.js +38 -10
  29. package/dist/drivers/orca.d.ts +99 -10
  30. package/dist/drivers/orca.js +586 -97
  31. package/dist/drivers/types.d.ts +1 -0
  32. package/dist/gates/baseline.d.ts +3 -0
  33. package/dist/gates/baseline.js +2 -1
  34. package/dist/gates/cache.d.ts +100 -0
  35. package/dist/gates/cache.js +389 -0
  36. package/dist/gates/review.d.ts +35 -2
  37. package/dist/gates/review.js +64 -16
  38. package/dist/gates/run-gates.d.ts +5 -0
  39. package/dist/gates/run-gates.js +134 -18
  40. package/dist/gates/test-manifest.d.ts +99 -0
  41. package/dist/gates/test-manifest.js +389 -0
  42. package/dist/gates/test-reporter.d.ts +4 -0
  43. package/dist/gates/test-reporter.js +49 -0
  44. package/dist/route/preference.d.ts +22 -1
  45. package/dist/route/preference.js +123 -25
  46. package/dist/route/router.d.ts +13 -0
  47. package/dist/route/router.js +95 -17
  48. package/dist/run/daemon.d.ts +13 -0
  49. package/dist/run/daemon.js +398 -96
  50. package/dist/run/git.d.ts +35 -1
  51. package/dist/run/git.js +111 -10
  52. package/dist/run/journal.d.ts +14 -2
  53. package/dist/run/journal.js +78 -10
  54. package/dist/run/lease.d.ts +14 -0
  55. package/dist/run/lease.js +87 -0
  56. package/dist/run/merge.d.ts +2 -0
  57. package/dist/run/merge.js +91 -3
  58. package/dist/run/operator-state.d.ts +11 -0
  59. package/dist/run/operator-state.js +17 -3
  60. package/dist/tui/cockpit/board.d.ts +96 -0
  61. package/dist/tui/cockpit/board.js +346 -0
  62. package/dist/tui/cockpit/decision-actions.js +2 -0
  63. package/dist/tui/cockpit/layout.d.ts +5 -1
  64. package/dist/tui/cockpit/layout.js +8 -3
  65. package/dist/tui/cockpit/live-runtime.js +83 -31
  66. package/dist/tui/cockpit/run-view.d.ts +7 -5
  67. package/dist/tui/cockpit/run-view.js +12 -11
  68. package/dist/tui/ink/fleet-app.d.ts +41 -27
  69. package/dist/tui/ink/fleet-app.js +204 -31
  70. package/package.json +1 -1
  71. package/skills/tickmarkr-overseer/SKILL.md +180 -99
package/dist/run/merge.js CHANGED
@@ -2,8 +2,10 @@ import { existsSync, readFileSync, writeFileSync } from "node:fs";
2
2
  import { join } from "node:path";
3
3
  import { shq } from "../adapters/types.js";
4
4
  import { ceilingKillResult, classifyFreshRunnerOutput, classifyRunnerOutput, fileCountDeficit, waitForCalmWindow, effectiveCeilingMs, fingerprint, freshFailures, } from "../gates/baseline.js";
5
+ import { evaluateManifestedTest, isVitestTestCommand } from "../gates/test-manifest.js";
6
+ import { computeVerificationIdentity, formatReusedDetails, getVerdictStore, getWorktreeTree, resolveStateDir, isInfraResult, } from "../gates/cache.js";
5
7
  import { tickmarkrDir } from "../graph/graph.js";
6
- import { describeCapacity, gitHead, linkNodeModules, resolveIntegrationBranch, sameCapacity, sh, shGit, shGitOk, WORKTREES_DIR } from "./git.js";
8
+ import { describeCapacity, gitHead, linkNodeModules, resolveIntegrationBranch, resolvedCapacity, sameCapacity, sh, shGit, shGitOk, WORKTREES_DIR } from "./git.js";
7
9
  export function integrationBranch(cfg, runId) {
8
10
  return `${cfg.integrationBranchPrefix}${runId}`;
9
11
  }
@@ -120,6 +122,11 @@ export async function verifyIntegrationTip(intWt, commands, runDir, baseline) {
120
122
  }
121
123
  return results;
122
124
  }
125
+ const stateDir = resolveStateDir(intWt, runDir);
126
+ const store = getVerdictStore(stateDir);
127
+ // Publish only after the entire verification completes. The daemon kills a cancelled
128
+ // verifier process, so completed early gates must remain in memory until then.
129
+ const pending = new Map();
123
130
  for (const [gate, cmd] of gatesToRun) {
124
131
  if (runStartCommands !== undefined) {
125
132
  const expectedCmd = gate === "test"
@@ -150,6 +157,73 @@ export async function verifyIntegrationTip(intWt, commands, runDir, baseline) {
150
157
  const entry = gate === "test" && (hasTipCommand || (!identity.test && identity.tipTest))
151
158
  ? baseline?.commands.tipTest
152
159
  : baseline?.commands[gate];
160
+ // A distinct tipTest selects a different forgiveness entry even when the command
161
+ // converges with the task command. Include that effective entry in the identity.
162
+ const verificationBaseline = baseline ? { commands: { ...baseline.commands } } : undefined;
163
+ if (verificationBaseline) {
164
+ if (entry)
165
+ verificationBaseline.commands[gate] = entry;
166
+ else
167
+ delete verificationBaseline.commands[gate];
168
+ }
169
+ // A tip verifier may reuse only a completed green from its own scope.
170
+ const id = await computeVerificationIdentity({
171
+ worktree: intWt,
172
+ gate,
173
+ scope: "tip",
174
+ command: cmd,
175
+ baseline: verificationBaseline,
176
+ selectedSet: undefined,
177
+ capacity: resolvedCapacity(),
178
+ });
179
+ // Persistent reuse requires the run's durable journal. Without it there is no
180
+ // recorded cycle provenance to reconcile with the caller's capacity checks.
181
+ const hit = journalFound ? store.get(id) : undefined;
182
+ if (hit && id && hit.pass && !isInfraResult(hit)) {
183
+ results.push({
184
+ gate,
185
+ cmd,
186
+ pass: hit.pass,
187
+ exitCode: hit.exitCode ?? 0,
188
+ fingerprints: hit.meta?.fingerprints ?? [],
189
+ ...(hit.meta?.forgiven ? { forgiven: true } : {}),
190
+ details: formatReusedDetails(hit.details, id),
191
+ ...(hit.meta?.reportPath ? { reportPath: hit.meta.reportPath } : {}),
192
+ ...(hit.meta?.spawnedCommand ? { spawnedCommand: hit.meta.spawnedCommand } : {}),
193
+ ...(hit.meta?.artifact ? { artifact: hit.meta.artifact } : {}),
194
+ });
195
+ continue;
196
+ }
197
+ if (id && journalFound)
198
+ pending.set(gate, id);
199
+ // VL-1: tip verify and standalone verify read the SAME invocation-bound report the gate would —
200
+ // a detected vitest test command never reaches the stdout-count/file-count path below (this
201
+ // gate's own manifest is always the FULL suite; verify has no selected screen to hold). A
202
+ // scripted test command that is not the detected runner falls through unchanged.
203
+ if (gate === "test" && isVitestTestCommand(cmd, intWt)) {
204
+ const outcome = await evaluateManifestedTest(cmd, intWt, {
205
+ baselineDurations: entry?.fileDurations,
206
+ longestFile: entry?.longestFile,
207
+ overallCeilingMs: effectiveCeilingMs(entry),
208
+ artifactDir: runDir,
209
+ });
210
+ const artifact = join(runDir, `tip-verify-${gate}.log`);
211
+ if (!outcome.pass)
212
+ writeFileSync(artifact, outcome.details);
213
+ results.push({
214
+ gate,
215
+ cmd,
216
+ pass: outcome.pass,
217
+ exitCode: outcome.exitCode,
218
+ reportPath: outcome.reportPath,
219
+ spawnedCommand: outcome.meta.spawnedCommand,
220
+ fingerprints: outcome.meta.failingTests ?? [],
221
+ details: outcome.details,
222
+ ...(outcome.classification ? { cause: outcome.classification } : {}),
223
+ ...(outcome.pass ? {} : { artifact }),
224
+ });
225
+ continue;
226
+ }
153
227
  // OBS-534: the ceiling is the BATTERY's, derived by effectiveCeilingMs from the same baseline entry
154
228
  // this loop already reads for forgiveness two lines down — never the flat DEFAULT_SHELL_TIMEOUT_MS
155
229
  // `sh` defaults to. A suite whose capture measured 600007ms carries a recorded 1800021ms ceiling;
@@ -216,7 +290,7 @@ export async function verifyIntegrationTip(intWt, commands, runDir, baseline) {
216
290
  const pass = !deficit && (r.code === 0 || greenTeardown || forgiven);
217
291
  if (!pass)
218
292
  writeFileSync(artifact, raw);
219
- results.push({
293
+ const tipResult = {
220
294
  gate,
221
295
  cmd,
222
296
  pass,
@@ -233,7 +307,21 @@ export async function verifyIntegrationTip(intWt, commands, runDir, baseline) {
233
307
  ...(forgiven && !deficit ? { forgiven: true } : {}),
234
308
  ...(cause ? { cause } : {}),
235
309
  ...(pass ? {} : { artifact }),
236
- });
310
+ };
311
+ results.push(tipResult);
312
+ }
313
+ // Recheck through the Git runner before publishing: cancellation disables further
314
+ // spawns in the daemon child, and a command that changed the tree invalidates reuse.
315
+ const completedTree = pending.size ? await getWorktreeTree(intWt) : undefined;
316
+ for (const result of results) {
317
+ const id = pending.get(result.gate);
318
+ if (id && id.tree === completedTree && !isInfraResult(result)) {
319
+ store.set(id, { ...result, meta: {
320
+ fingerprints: result.fingerprints, forgiven: result.forgiven,
321
+ reportPath: result.reportPath, spawnedCommand: result.spawnedCommand,
322
+ artifact: result.artifact,
323
+ } });
324
+ }
237
325
  }
238
326
  return results;
239
327
  }
@@ -30,11 +30,19 @@ export interface OperatorTask {
30
30
  evidence?: EvidenceIdentity;
31
31
  mergeEvidence?: EvidenceIdentity;
32
32
  gates: Record<string, OperatorGate>;
33
+ /** BD-1: the board's per-task counters — latest dispatch channel, review verdicts drawn, human parks. */
34
+ channel?: string;
35
+ reviewRounds: number;
36
+ parks: number;
33
37
  }
34
38
  export interface OperatorSnapshot {
35
39
  sequence: number;
36
40
  observedAt: number;
37
41
  lastEventAt?: string;
42
+ firstEventAt?: string;
43
+ /** BD-1 header counts: run-resume and escalation rows seen. */
44
+ resumes: number;
45
+ escalations: number;
38
46
  lifecycle: "PENDING" | "RUNNING" | "PARTIAL" | "APPROVED" | "COMPLETE" | "UNKNOWN";
39
47
  label: string;
40
48
  green: boolean;
@@ -63,6 +71,9 @@ export declare class OperatorStateFold {
63
71
  private approved;
64
72
  private tipFailed;
65
73
  private lastEventAt?;
74
+ private firstEventAt?;
75
+ private resumes;
76
+ private escalations;
66
77
  private passed;
67
78
  private total;
68
79
  apply(record: OperatorRecord): void;
@@ -16,11 +16,19 @@ export class OperatorStateFold {
16
16
  approved = false;
17
17
  tipFailed = false;
18
18
  lastEventAt;
19
+ firstEventAt;
20
+ resumes = 0;
21
+ escalations = 0;
19
22
  passed = 0;
20
23
  total = 0;
21
24
  apply(record) {
22
25
  const { event: e } = record;
23
26
  this.lastEventAt = e.ts;
27
+ this.firstEventAt ??= e.ts;
28
+ if (e.event === "run-resume")
29
+ this.resumes++;
30
+ if (e.event === "escalation")
31
+ this.escalations++;
24
32
  const ref = identity(record);
25
33
  if (e.event === "run-start" || e.event === "run-resume") {
26
34
  if (e.event === "run-start" && !this.start) {
@@ -72,6 +80,9 @@ export class OperatorStateFold {
72
80
  t.path = typeof e.data.worktree === "string" ? e.data.worktree : typeof e.data.cwd === "string" ? e.data.cwd : undefined;
73
81
  t.pane = typeof e.data.pane === "string" ? e.data.pane : undefined;
74
82
  t.alarmMs = typeof e.data.alarmMs === "number" ? e.data.alarmMs : undefined;
83
+ const a = e.data.assignment;
84
+ if (a && typeof a.adapter === "string" && typeof a.model === "string")
85
+ t.channel = `${a.adapter}:${a.model}`;
75
86
  }
76
87
  if (e.event === "task-done")
77
88
  t.state = "completed";
@@ -85,6 +96,7 @@ export class OperatorStateFold {
85
96
  t.state = "failed";
86
97
  if (e.event === "task-human") {
87
98
  t.state = "human";
99
+ t.parks++;
88
100
  t.parkKind = typeof e.data.kind === "string" ? e.data.kind : undefined;
89
101
  }
90
102
  if (e.event === "task-blocked")
@@ -98,6 +110,8 @@ export class OperatorStateFold {
98
110
  if (typeof gate === "string" && GATE_NAMES.includes(gate)) {
99
111
  if (e.event === "gate-start")
100
112
  t.gates[gate] = { state: "running", evidence: ref };
113
+ if (e.event === "gate-result" && gate === "review")
114
+ t.reviewRounds++;
101
115
  if (e.event === "gate-result")
102
116
  t.gates[gate] = {
103
117
  state: e.data.disabled === true ? "disabled" : e.data.skipped === true ? "not-run" : e.data.pass === true ? "passed" : e.data.pass === false ? "failed" : "unknown", evidence: ref,
@@ -107,7 +121,7 @@ export class OperatorStateFold {
107
121
  task(id) {
108
122
  let t = this.tasks.get(id);
109
123
  if (!t) {
110
- t = { id, state: "unknown", merged: false, dispatches: 0, gates: emptyGates() };
124
+ t = { id, state: "unknown", merged: false, dispatches: 0, reviewRounds: 0, parks: 0, gates: emptyGates() };
111
125
  this.tasks.set(id, t);
112
126
  }
113
127
  return t;
@@ -117,7 +131,7 @@ export class OperatorStateFold {
117
131
  const comparable = this.comparableTo(hash);
118
132
  const ids = comparable ? [...graph.tasks.map(t => t.id), ...[...this.tasks.keys()].filter(id => !graph.tasks.some(t => t.id === id))] : [...this.tasks.keys()];
119
133
  const tasks = ids.map(id => {
120
- const t = this.tasks.get(id) ?? { id, state: "unknown", merged: false, dispatches: 0, gates: emptyGates() };
134
+ const t = this.tasks.get(id) ?? { id, state: "unknown", merged: false, dispatches: 0, reviewRounds: 0, parks: 0, gates: emptyGates() };
121
135
  return { ...t, title: comparable ? graph.tasks.find(g => g.id === id)?.title : undefined, gates: Object.fromEntries(Object.entries(t.gates).map(([g, cell]) => [g, { ...cell }])) };
122
136
  });
123
137
  const buckets = Object.fromEntries(bucketNames.map(k => [k, this.end?.buckets[k] === undefined ? undefined : [...this.end.buckets[k]]]));
@@ -134,7 +148,7 @@ export class OperatorStateFold {
134
148
  && bucketNames.every(k => buckets[k]?.length === 0);
135
149
  const lifecycle = !readable || !this.start ? "UNKNOWN" : this.active ? "RUNNING" : this.approved ? "APPROVED" : green ? "COMPLETE" : this.end ? "PARTIAL" : "PENDING";
136
150
  return {
137
- sequence, observedAt, lastEventAt: this.lastEventAt, lifecycle,
151
+ sequence, observedAt, lastEventAt: this.lastEventAt, firstEventAt: this.firstEventAt, resumes: this.resumes, escalations: this.escalations, lifecycle,
138
152
  label: this.approved ? "approved; resume required" : lifecycle,
139
153
  green, currentTip, comparable, comparison: comparable ? "matching graph" : "not comparable",
140
154
  merged,
@@ -0,0 +1,96 @@
1
+ import { type RunGraph } from "../../graph/schema.js";
2
+ import type { OperatorSnapshot } from "../../run/operator-state.js";
3
+ /** The prototype's palette (tasks-redesign.mjs:24-30): xterm-256 indices, stated once. */
4
+ export declare const BOARD_PALETTE: {
5
+ readonly chrome: 238;
6
+ readonly mute: 243;
7
+ readonly body: 252;
8
+ readonly faint: 240;
9
+ readonly accent: 75;
10
+ readonly pass: 71;
11
+ readonly fail: 174;
12
+ readonly warn: 179;
13
+ readonly park: 140;
14
+ };
15
+ /** The seven gates, declaration order; every cell is exactly CELL columns wide. */
16
+ export declare const BOARD_GATES: readonly ["build", "test", "lint", "evidence", "scope", "acceptance", "review"];
17
+ export declare const BOARD_CELL = 3;
18
+ export declare const BOARD_AREA_W = 16;
19
+ export declare const BOARD_DEPS_W = 14;
20
+ export declare const BOARD_NOTE_RESERVE = 28;
21
+ export declare const BOARD_FIXED: number;
22
+ export declare const BOARD_EFFORT_BAR_MAX = 46;
23
+ /** The board's own key hints — only the ones the Run view handles; nothing advertised is inert. */
24
+ export declare const BOARD_KEYS: readonly ["↑↓ move", "↵ task detail", "q quit"];
25
+ /** Width band the Run view draws in (RULING-231-19 §2: ≥150 full · 110–149 note wraps · <110 stack). */
26
+ export type BoardBand = "full" | "wrap" | "stacked";
27
+ export declare const boardBand: (columns: number) => BoardBand;
28
+ export declare const stripBoardAnsi: (s: string) => string;
29
+ /** ANSI-aware clip at `n` visible cells (prototype `clip`), cut on a grapheme boundary so a
30
+ * double-cell glyph never crosses the limit and an emoji is never emitted as a lone surrogate;
31
+ * a clipped line closes its own colour. */
32
+ export declare function clipBoard(s: string, n: number): string;
33
+ export declare const BOARD_AREA_ORDER: string[];
34
+ /** {codes, unmapped} — abstentions applied, set semantics, misses reported rather than dropped. */
35
+ export declare function areasFor(files: readonly string[]): {
36
+ codes: string[];
37
+ unmapped: string[];
38
+ };
39
+ export interface BoardInput {
40
+ readonly runId: string;
41
+ readonly snapshot: OperatorSnapshot;
42
+ readonly graph?: Pick<RunGraph, "tasks">;
43
+ /** The clock the header's elapsed/ago fields read. */
44
+ readonly now: number;
45
+ /** A live daemon holds this run's lock. */
46
+ readonly live?: boolean;
47
+ /** The task the focus marker sits on; absent draws the prototype's plain indent. */
48
+ readonly selection?: string;
49
+ /** Footer key hints, joined the prototype's way; `false` leaves the footer to the caller (see boardFooter). */
50
+ readonly keys?: readonly string[] | false;
51
+ readonly colour?: boolean;
52
+ }
53
+ /** One task row, in the parts the Run view recomposes per width band. */
54
+ export interface BoardRow {
55
+ readonly id: string;
56
+ /** indent (or the focus marker) + id + area + deps + title */
57
+ readonly head: string;
58
+ /** the seven gate cells */
59
+ readonly strip: string;
60
+ /** channel + attempts */
61
+ readonly tail: string;
62
+ /** `◍ parked ×n · ↻ n review rounds · ✖ gate` / `waiting on …` / `in flight`, joined by ` · ` */
63
+ readonly note: string;
64
+ /** The note's segments, so a band can wrap it across continuation rows without cutting one. */
65
+ readonly noteParts: readonly string[];
66
+ }
67
+ export interface BoardFrame {
68
+ /** Header through the gate header rule, in the prototype's order. */
69
+ readonly top: readonly string[];
70
+ readonly rows: readonly BoardRow[];
71
+ /** Header row parts, aligned to the row parts. */
72
+ readonly gateHeader: {
73
+ readonly head: string;
74
+ readonly strip: string;
75
+ readonly tail: string;
76
+ };
77
+ /** Legend, retired ids, rule, effort fold, footer. */
78
+ readonly bottom: readonly string[];
79
+ readonly width: number;
80
+ readonly lbl: number;
81
+ readonly chan: number;
82
+ }
83
+ /** The prototype's frame, sectioned. `renderBoard` joins it row for row. */
84
+ export declare function boardFrame(input: BoardInput, width: number): BoardFrame;
85
+ /** The footer row: the key hints in chrome, the prototype's leading space kept. */
86
+ export declare const boardFooter: (keys: readonly string[], colour?: boolean) => string;
87
+ /** The prototype's frame as lines, one row per task, unclipped — exactly what the design signed off. */
88
+ export declare function renderBoard(input: BoardInput, width: number): string[];
89
+ /**
90
+ * The Run view's three width bands over the same frame: full draws the prototype's rows; wrap
91
+ * puts a note that does not fit on its own second row; stacked draws the gate cells, channel and
92
+ * attempts under the row so nothing is truncated away.
93
+ */
94
+ export declare function renderBoardLines(input: BoardInput, width: number): string[];
95
+ /** Pack note segments greedily into rows of `avail` cells; a segment wider than a row splits at the cell. */
96
+ export declare function wrapNote(parts: readonly string[], sep: string, firstAvail: number, avail: number): string[];