tickmarkr 2.4.1 → 2.5.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 (75) hide show
  1. package/README.md +110 -24
  2. package/dist/adapters/claude-code.js +7 -2
  3. package/dist/adapters/codex.d.ts +1 -0
  4. package/dist/adapters/codex.js +66 -5
  5. package/dist/adapters/types.d.ts +4 -0
  6. package/dist/adapters/types.js +33 -0
  7. package/dist/cli/commands/approve.d.ts +42 -0
  8. package/dist/cli/commands/approve.js +80 -13
  9. package/dist/cli/commands/doctor.d.ts +234 -0
  10. package/dist/cli/commands/doctor.js +139 -5
  11. package/dist/cli/commands/report.js +15 -44
  12. package/dist/cli/commands/scope.js +36 -6
  13. package/dist/cli/commands/stats.d.ts +2 -0
  14. package/dist/cli/commands/stats.js +42 -23
  15. package/dist/cli/commands/status.js +20 -4
  16. package/dist/cli/commands/ui.js +42 -53
  17. package/dist/cli/commands/unlock.d.ts +1 -1
  18. package/dist/cli/commands/unlock.js +59 -9
  19. package/dist/cli/help.d.ts +219 -0
  20. package/dist/cli/help.js +212 -0
  21. package/dist/cli/index.d.ts +42 -2
  22. package/dist/cli/index.js +23 -8
  23. package/dist/drivers/herdr.d.ts +7 -2
  24. package/dist/drivers/herdr.js +78 -48
  25. package/dist/drivers/orca.d.ts +3 -2
  26. package/dist/drivers/orca.js +43 -4
  27. package/dist/drivers/subprocess.d.ts +1 -0
  28. package/dist/drivers/subprocess.js +3 -0
  29. package/dist/drivers/types.d.ts +14 -0
  30. package/dist/gates/artifact-manifest.d.ts +50 -0
  31. package/dist/gates/artifact-manifest.js +23 -0
  32. package/dist/plan/scope.d.ts +25 -0
  33. package/dist/plan/scope.js +92 -12
  34. package/dist/report/operator-record.d.ts +49 -0
  35. package/dist/report/operator-record.js +137 -0
  36. package/dist/run/daemon.d.ts +3 -4
  37. package/dist/run/daemon.js +18 -10
  38. package/dist/run/lock.d.ts +59 -2
  39. package/dist/run/lock.js +184 -26
  40. package/dist/run/operator-state.d.ts +86 -0
  41. package/dist/run/operator-state.js +165 -0
  42. package/dist/run/supervision.d.ts +32 -0
  43. package/dist/run/supervision.js +138 -17
  44. package/dist/tui/cockpit/capture.d.ts +19 -0
  45. package/dist/tui/cockpit/capture.js +89 -1
  46. package/dist/tui/cockpit/components.d.ts +15 -1
  47. package/dist/tui/cockpit/components.js +79 -9
  48. package/dist/tui/cockpit/decision-actions.d.ts +147 -0
  49. package/dist/tui/cockpit/decision-actions.js +315 -0
  50. package/dist/tui/cockpit/derive.d.ts +1 -1
  51. package/dist/tui/cockpit/derive.js +2 -0
  52. package/dist/tui/cockpit/evidence-view.d.ts +119 -0
  53. package/dist/tui/cockpit/evidence-view.js +210 -0
  54. package/dist/tui/cockpit/home-view.d.ts +88 -0
  55. package/dist/tui/cockpit/home-view.js +240 -0
  56. package/dist/tui/cockpit/keys.d.ts +125 -0
  57. package/dist/tui/cockpit/keys.js +31 -0
  58. package/dist/tui/cockpit/layout.d.ts +14 -0
  59. package/dist/tui/cockpit/layout.js +15 -0
  60. package/dist/tui/cockpit/live-runtime.d.ts +46 -0
  61. package/dist/tui/cockpit/live-runtime.js +683 -0
  62. package/dist/tui/cockpit/live-store.d.ts +289 -0
  63. package/dist/tui/cockpit/live-store.js +308 -0
  64. package/dist/tui/cockpit/live.d.ts +21 -1
  65. package/dist/tui/cockpit/live.js +12 -1
  66. package/dist/tui/cockpit/run-view.d.ts +100 -0
  67. package/dist/tui/cockpit/run-view.js +202 -0
  68. package/dist/tui/cockpit/shell.d.ts +50 -0
  69. package/dist/tui/cockpit/shell.js +74 -0
  70. package/dist/tui/cockpit/theme.d.ts +27 -0
  71. package/dist/tui/cockpit/theme.js +21 -0
  72. package/package.json +1 -1
  73. package/skills/tickmarkr-auto/SKILL.md +10 -1
  74. package/skills/tickmarkr-loop/SKILL.md +72 -1
  75. package/skills/tickmarkr-overseer/SKILL.md +12 -0
@@ -0,0 +1,100 @@
1
+ import type { ReactElement } from "react";
2
+ import type { ApprovalRunOwner } from "../../cli/commands/approve.js";
3
+ import { type GateName, type RunGraph } from "../../graph/schema.js";
4
+ import type { JournalEvent } from "../../run/journal.js";
5
+ import { type GateOutcome } from "../../run/outcome.js";
6
+ import type { OperatorGateState, OperatorSnapshot, OperatorTask } from "../../run/operator-state.js";
7
+ import { type DecisionCommand, type DecisionPreview, type DecisionSession, type RunDecision } from "./decision-actions.js";
8
+ /** A journal line as C2's store exposes it — physical line, parsed event when readable. */
9
+ export interface RunEvidenceRow {
10
+ readonly line: number;
11
+ readonly event?: JournalEvent;
12
+ readonly error?: string;
13
+ }
14
+ export type EvidencePage = (firstLine: number, count?: number) => readonly RunEvidenceRow[];
15
+ export interface EvidenceLookup {
16
+ (line: number): RunEvidenceRow | undefined;
17
+ /** All known later rows, filling the evicted gap through the store's page accessor. */
18
+ later(line: number): readonly RunEvidenceRow[];
19
+ }
20
+ /** Bounded history first, then the store's page accessor for an evicted line. */
21
+ export declare function evidenceLookup(rows: readonly RunEvidenceRow[], page?: EvidencePage): EvidenceLookup;
22
+ export declare const GATE_CELL_LETTERS: Record<OperatorGateState, string>;
23
+ /** The outcome selector's vocabulary — a classification of the row, never a word search. */
24
+ export declare const OUTCOME_FILTERS: readonly ["all", "infra failure", "work failure", "pass", "unknown"];
25
+ export type OutcomeFilter = (typeof OUTCOME_FILTERS)[number];
26
+ export type OutcomeClass = Exclude<OutcomeFilter, "all"> | "not a verdict";
27
+ export interface RunGateCell {
28
+ readonly gate: GateName;
29
+ readonly state: OperatorGateState;
30
+ readonly letter: string;
31
+ readonly line?: number;
32
+ readonly outcome?: GateOutcome;
33
+ readonly outcomeClass: OutcomeClass;
34
+ readonly labels: readonly string[];
35
+ /** The verdict text the evidence row carries, split into lines for paging. */
36
+ readonly verdict: readonly string[];
37
+ }
38
+ /**
39
+ * The current attempt's seven cells in declaration order. `rows` supplies the later journal rows
40
+ * that give a cell its inherited/satisfied label; only rows for this task after the evidence line count.
41
+ */
42
+ export declare function runGateCells(task: OperatorTask, evidence: EvidenceLookup, rows?: readonly RunEvidenceRow[]): readonly RunGateCell[];
43
+ export declare const VERDICT_WINDOW = 10;
44
+ export interface RunViewSession {
45
+ readonly selection: number;
46
+ /** Index into GATE_NAMES of the gate whose verdict is open. */
47
+ readonly verdictGate: number;
48
+ readonly verdictOffset: number;
49
+ readonly outcomeFilter: OutcomeFilter;
50
+ readonly decisions: DecisionSession;
51
+ }
52
+ export declare const initialRunViewSession: () => RunViewSession;
53
+ export interface RunViewKeyEvent {
54
+ readonly input: string;
55
+ readonly key: {
56
+ readonly upArrow?: boolean;
57
+ readonly downArrow?: boolean;
58
+ readonly leftArrow?: boolean;
59
+ readonly rightArrow?: boolean;
60
+ readonly return?: boolean;
61
+ readonly escape?: boolean;
62
+ readonly pageUp?: boolean;
63
+ readonly pageDown?: boolean;
64
+ };
65
+ }
66
+ export interface RunViewKeyResult {
67
+ readonly session: RunViewSession;
68
+ readonly open?: DecisionCommand;
69
+ readonly confirm?: DecisionPreview;
70
+ }
71
+ /**
72
+ * ↑↓ select a task, ←→ choose the verdict gate, PageUp/PageDown page the verdict, x cycles the
73
+ * outcome selector, and a/Enter/y/n/Esc are the decision flow's. A digit is never a view switch here.
74
+ */
75
+ export declare function applyRunViewKey(session: RunViewSession, event: RunViewKeyEvent, ctx: {
76
+ tasks: readonly OperatorTask[];
77
+ decisions: readonly RunDecision[];
78
+ verdictLines: number;
79
+ }): RunViewKeyResult;
80
+ /**
81
+ * The one-line lifecycle summary drawn above the matrix. Unresolved members come from the tasks'
82
+ * current recorded states (the run-end buckets are C2's green authority, invalidated by a resume);
83
+ * a closed run whose record lacks a bucket says so rather than reading absence as empty.
84
+ */
85
+ export declare function runSummaryLine(s: OperatorSnapshot): string;
86
+ /** One matrix row: id, state, the seven letters in declaration order, the recorded attempt. */
87
+ export declare function runTaskRow(t: OperatorTask, cells: readonly Pick<RunGateCell, "letter">[]): string;
88
+ export interface RunViewProps {
89
+ readonly snapshot: OperatorSnapshot;
90
+ /** Journal rows the store retains (C2 `journal.history`); evidence outside them reaches `page`. */
91
+ readonly rows: readonly RunEvidenceRow[];
92
+ readonly page?: EvidencePage;
93
+ readonly graph?: RunGraph;
94
+ readonly decisions: readonly RunDecision[];
95
+ readonly session: RunViewSession;
96
+ readonly columns: number;
97
+ readonly run: ApprovalRunOwner;
98
+ }
99
+ /** The Run body. Every line is measured through the width module; nothing overflows its column. */
100
+ export declare function RunView({ snapshot, rows, page, graph, decisions, session, columns, run }: RunViewProps): ReactElement;
@@ -0,0 +1,202 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { Box } from "ink";
3
+ import { GLYPHS } from "../../brand.js";
4
+ import { GATE_NAMES } from "../../graph/schema.js";
5
+ import { normalizeGateOutcome } from "../../run/outcome.js";
6
+ import { BodyText, Panel } from "./components.js";
7
+ import { applyDecisionKey, decisionConfirmLines, decisionKeybar, decisionReceiptLines, initialDecisionSession, } from "./decision-actions.js";
8
+ import { fitCells, wrapCells } from "./width.js";
9
+ /** Bounded history first, then the store's page accessor for an evicted line. */
10
+ export function evidenceLookup(rows, page) {
11
+ const retained = [...rows].sort((a, b) => a.line - b.line);
12
+ const recovered = new Map();
13
+ const safelyPage = (firstLine, count) => {
14
+ try {
15
+ return page?.(firstLine, count) ?? [];
16
+ }
17
+ catch {
18
+ return [];
19
+ }
20
+ };
21
+ const lookup = ((line) => retained.find((row) => row.line === line) ?? recovered.get(line) ?? safelyPage(line, 1)[0]);
22
+ lookup.later = (line) => {
23
+ const later = retained.filter((row) => row.line > line);
24
+ const firstRetained = later[0]?.line;
25
+ // If the evidence itself is retained, every later row is retained too: history is a tail.
26
+ // Otherwise page only the evicted gap. This recovers durable reuse/approval markers without
27
+ // retaining the journal or repeatedly scanning the live tail on ordinary renders.
28
+ if (!page || retained.some((row) => row.line === line))
29
+ return later;
30
+ let candidate = line + 1;
31
+ while (firstRetained === undefined || candidate < firstRetained) {
32
+ const count = firstRetained === undefined ? 256 : Math.min(256, firstRetained - candidate);
33
+ const batch = safelyPage(candidate, count);
34
+ if (batch.length === 0)
35
+ break;
36
+ for (const row of batch)
37
+ recovered.set(row.line, row);
38
+ const next = batch[batch.length - 1].line + 1;
39
+ if (next <= candidate)
40
+ break;
41
+ candidate = next;
42
+ }
43
+ return [...recovered.values(), ...later]
44
+ .filter((row) => row.line > line)
45
+ .sort((a, b) => a.line - b.line);
46
+ };
47
+ return lookup;
48
+ }
49
+ export const GATE_CELL_LETTERS = {
50
+ passed: "P", failed: "F", running: "R", "not-run": "-", disabled: "D", unknown: "?",
51
+ };
52
+ /** The outcome selector's vocabulary — a classification of the row, never a word search. */
53
+ export const OUTCOME_FILTERS = ["all", "infra failure", "work failure", "pass", "unknown"];
54
+ const BASELINE_GATES = new Set(["build", "test", "lint"]);
55
+ const str = (v) => (typeof v === "string" && v.trim() !== "" ? v : undefined);
56
+ function outcomeClassOf(outcome, state) {
57
+ if (!outcome)
58
+ return state === "passed" || state === "failed" ? "unknown" : "not a verdict";
59
+ switch (outcome.kind) {
60
+ case "infra": return "infra failure";
61
+ case "failed": return "work failure";
62
+ case "passed": return "pass";
63
+ case "unavailable": return "unknown";
64
+ default: return "not a verdict";
65
+ }
66
+ }
67
+ function outcomeLabel(outcome) {
68
+ switch (outcome.kind) {
69
+ case "passed": return "passed";
70
+ case "failed": return "failed (work)";
71
+ case "infra": return `infra failure — ${outcome.reason} (${outcome.retryable ? "retryable" : "terminal"})`;
72
+ case "skipped": return `skipped — ${outcome.reason}`;
73
+ case "declined": return `declined — ${outcome.reason}`;
74
+ case "held": return `held — ${outcome.reason}`;
75
+ case "unavailable": return `unknown — ${outcome.reason}`;
76
+ }
77
+ }
78
+ /**
79
+ * The current attempt's seven cells in declaration order. `rows` supplies the later journal rows
80
+ * that give a cell its inherited/satisfied label; only rows for this task after the evidence line count.
81
+ */
82
+ export function runGateCells(task, evidence, rows = []) {
83
+ return GATE_NAMES.map((gate) => {
84
+ const cell = task.gates[gate] ?? { state: "unknown" };
85
+ const line = cell.evidence?.line;
86
+ const row = line === undefined ? undefined : evidence(line);
87
+ const data = row?.event?.data;
88
+ const labels = [];
89
+ let outcome;
90
+ if (data === undefined) {
91
+ labels.push(line === undefined ? { "not-run": "not run", disabled: "disabled by policy", running: "running", unknown: "unknown", passed: "passed", failed: "failed" }[cell.state] : `evidence #L${line} unavailable`);
92
+ }
93
+ else if (data.disabled === true) {
94
+ labels.push("disabled by policy");
95
+ }
96
+ else if (row?.event?.event === "gate-start") {
97
+ labels.push("running");
98
+ }
99
+ else {
100
+ outcome = normalizeGateOutcome(data);
101
+ labels.push(outcomeLabel(outcome));
102
+ // Baseline forgiveness is recorded only in the battery's own details text (baseline.ts); read it
103
+ // from that row alone, for the three gates that battery runs, never from any other prose.
104
+ if (BASELINE_GATES.has(gate) && data.pass === true && /\(forgiven\)/u.test(String(data.details ?? "")))
105
+ labels.push("baseline-forgiven (pre-existing failures)");
106
+ if (data.replayMeasurement === true)
107
+ labels.push("replayed measurement");
108
+ if (gate === "review") {
109
+ labels.push(`reviewer ${str(data.reviewer) ?? "unknown"}`, `reviewer tier ${str(data.reviewerTier) ?? "unknown"}`, `author tier ${str(data.authorTier) ?? "unknown"}`, `reviewer floor ${str(data.reviewerFloor) ?? "unknown"}`);
110
+ }
111
+ }
112
+ for (const later of line === undefined ? rows : evidence.later(line)) {
113
+ if (line === undefined || later.line <= line || later.event?.taskId !== task.id)
114
+ continue;
115
+ const e = later.event;
116
+ if (e.event === "gate-reused" && e.data.gate === gate)
117
+ labels.push(`inherited from ${str(e.data.commit) ?? "unknown commit"} · #L${later.line}`);
118
+ if (e.event === "task-approved" && e.data.release === "gate-satisfied" && e.data.gate === gate)
119
+ labels.push(`satisfied by approval #L${later.line}`);
120
+ }
121
+ const verdict = typeof data?.details === "string" ? data.details.split("\n") : [];
122
+ return { gate, state: cell.state, letter: GATE_CELL_LETTERS[cell.state], ...(line === undefined ? {} : { line }), ...(outcome ? { outcome } : {}), outcomeClass: outcomeClassOf(outcome, cell.state), labels, verdict };
123
+ });
124
+ }
125
+ export const VERDICT_WINDOW = 10;
126
+ export const initialRunViewSession = () => ({ selection: 0, verdictGate: GATE_NAMES.indexOf("review"), verdictOffset: 0, outcomeFilter: "all", decisions: initialDecisionSession() });
127
+ const clamp = (v, lo, hi) => Math.max(lo, Math.min(hi, v));
128
+ /**
129
+ * ↑↓ select a task, ←→ choose the verdict gate, PageUp/PageDown page the verdict, x cycles the
130
+ * outcome selector, and a/Enter/y/n/Esc are the decision flow's. A digit is never a view switch here.
131
+ */
132
+ export function applyRunViewKey(session, event, ctx) {
133
+ const task = ctx.tasks[session.selection];
134
+ const selected = task === undefined ? undefined : ctx.decisions.find((d) => d.taskId === task.id);
135
+ const decided = applyDecisionKey(session.decisions, event, selected);
136
+ if (decided.handled)
137
+ return { session: { ...session, decisions: decided.session }, ...(decided.open ? { open: decided.open } : {}), ...(decided.confirm ? { confirm: decided.confirm } : {}) };
138
+ if (event.key.upArrow === true || event.key.downArrow === true) {
139
+ const selection = clamp(session.selection + (event.key.upArrow === true ? -1 : 1), 0, Math.max(0, ctx.tasks.length - 1));
140
+ return { session: { ...session, selection, verdictOffset: 0 } };
141
+ }
142
+ if (event.key.leftArrow === true || event.key.rightArrow === true) {
143
+ return { session: { ...session, verdictGate: clamp(session.verdictGate + (event.key.leftArrow === true ? -1 : 1), 0, GATE_NAMES.length - 1), verdictOffset: 0 } };
144
+ }
145
+ if (event.key.pageUp === true || event.key.pageDown === true) {
146
+ const last = Math.max(0, ctx.verdictLines - VERDICT_WINDOW);
147
+ return { session: { ...session, verdictOffset: clamp(session.verdictOffset + (event.key.pageUp === true ? -VERDICT_WINDOW : VERDICT_WINDOW), 0, last) } };
148
+ }
149
+ if (event.input === "x") {
150
+ return { session: { ...session, outcomeFilter: OUTCOME_FILTERS[(OUTCOME_FILTERS.indexOf(session.outcomeFilter) + 1) % OUTCOME_FILTERS.length] } };
151
+ }
152
+ return { session };
153
+ }
154
+ const UNRESOLVED = ["failed", "human", "blocked", "pending"];
155
+ /**
156
+ * The one-line lifecycle summary drawn above the matrix. Unresolved members come from the tasks'
157
+ * current recorded states (the run-end buckets are C2's green authority, invalidated by a resume);
158
+ * a closed run whose record lacks a bucket says so rather than reading absence as empty.
159
+ */
160
+ export function runSummaryLine(s) {
161
+ const members = UNRESOLVED.map((k) => { const ids = s.tasks.filter((t) => t.state === k).map((t) => t.id); return ids.length === 0 ? "" : `${k} ${ids.join(",")}`; }).filter(Boolean);
162
+ const unknownBuckets = s.latestRunEnd === undefined ? [] : UNRESOLVED.filter((k) => s.buckets[k] === undefined);
163
+ return [
164
+ `${s.merged}/${s.planned ?? "?"} merged`,
165
+ ...members,
166
+ ...(unknownBuckets.length === 0 ? [] : [`run-end bucket ${unknownBuckets.join("/")} unknown`]),
167
+ `current-cycle tip ${s.currentTip.toUpperCase()}`,
168
+ s.label, s.comparison, `snapshot #${s.sequence}`,
169
+ ].join(" | ");
170
+ }
171
+ const attemptLabel = (t) => (t.attempt !== undefined ? `attempt ${t.attempt}` : t.dispatches === 0 ? "never dispatched" : "attempt unknown");
172
+ const parkOf = (t) => (t.state === "human" ? `human · ${t.parkKind ?? "unknown kind"}` : t.state);
173
+ /** One matrix row: id, state, the seven letters in declaration order, the recorded attempt. */
174
+ export function runTaskRow(t, cells) {
175
+ return `${t.id.padEnd(4)} ${parkOf(t).padEnd(20)} ${cells.map((c) => c.letter).join(" ")} ${attemptLabel(t)}`;
176
+ }
177
+ const MATRIX_HEADER = `${"Task".padEnd(4)} ${"state".padEnd(20)} ${GATE_NAMES.map((g) => g.slice(0, 2)).join(" ")} latest attempt`;
178
+ const MATRIX_LEGEND = "P pass · F fail · R running · - not run · D disabled · ? unknown | declaration order, not a timeline · acceptance/review concurrent, optional";
179
+ /** The Run body. Every line is measured through the width module; nothing overflows its column. */
180
+ export function RunView({ snapshot, rows, page, graph, decisions, session, columns, run }) {
181
+ const inner = Math.max(1, Math.floor(columns) - 4);
182
+ const fit = (text) => fitCells(text, inner);
183
+ const decisionLines = (lines) => lines.flatMap((line) => wrapCells(line, inner));
184
+ const lookup = evidenceLookup(rows, page);
185
+ const tasks = snapshot.tasks;
186
+ const selection = Math.min(session.selection, Math.max(0, tasks.length - 1));
187
+ const task = tasks[selection];
188
+ const cells = task === undefined ? [] : runGateCells(task, lookup, rows);
189
+ const shown = cells.filter((c) => session.outcomeFilter === "all" || c.outcomeClass === session.outcomeFilter);
190
+ const verdictCell = cells[session.verdictGate];
191
+ const verdict = verdictCell?.verdict ?? [];
192
+ const from = Math.min(session.verdictOffset, Math.max(0, verdict.length - 1));
193
+ const window = verdict.slice(from, from + VERDICT_WINDOW);
194
+ const decision = task === undefined ? undefined : decisions.find((d) => d.taskId === task.id);
195
+ const { menu, confirming, receipt, notice } = session.decisions;
196
+ const blocked = task === undefined ? [] : graph?.tasks.filter((g) => g.deps.includes(task.id)).map((g) => g.id) ?? decision?.blocks ?? [];
197
+ return (_jsxs(Box, { flexDirection: "column", width: columns, children: [_jsxs(Panel, { title: `RUN / ${snapshot.lifecycle}`, focused: true, children: [_jsx(BodyText, { emphasis: "dim", children: fit(runSummaryLine(snapshot)) }), _jsx(BodyText, { emphasis: "dim", children: fit(` ${MATRIX_HEADER}`) }), tasks.map((t, i) => (_jsx(BodyText, { emphasis: i === selection ? "strong" : "normal", children: fit(`${i === selection ? `${GLYPHS.pointer} ` : " "}${runTaskRow(t, GATE_NAMES.map((gate) => ({ letter: GATE_CELL_LETTERS[t.gates[gate].state] })))}`) }, t.id))), tasks.length === 0 && _jsx(BodyText, { children: fit("no tasks recorded — no plan") }), _jsx(BodyText, { emphasis: "dim", children: fit(MATRIX_LEGEND) })] }), task !== undefined && (_jsxs(Panel, { title: `SELECTED / ${task.id}${task.title ? ` · ${task.title}` : ""}`, children: [_jsx(BodyText, { children: fit(`state ${parkOf(task)} · ${attemptLabel(task)} · dispatches ${task.dispatches} · path ${task.path ?? "unknown"} · pane ${task.pane ?? "unknown"} · alarm ${task.alarmMs === undefined ? "unknown" : `${task.alarmMs}ms`}${task.mergeEvidence ? ` · merged #L${task.mergeEvidence.line}` : ""}`) }), _jsx(BodyText, { emphasis: "dim", children: fit(`outcome filter ${session.outcomeFilter} (x cycles) · ${shown.length} of ${cells.length} gates shown`) }), shown.map((c) => (_jsx(BodyText, { children: fit(`${c.letter} ${c.gate.padEnd(10)} ${c.labels.join(" · ")}${c.line === undefined ? "" : ` · #L${c.line}`}`) }, c.gate)))] })), verdictCell !== undefined && (_jsxs(Panel, { title: `VERDICT / ${verdictCell.gate}${verdictCell.line === undefined ? "" : ` #L${verdictCell.line}`} · ${verdict.length === 0 ? "no verdict text" : `lines ${from + 1}–${from + window.length} of ${verdict.length}`}`, children: [window.map((line, i) => _jsx(BodyText, { children: fit(`${String(from + i + 1).padStart(4)} ${line}`) }, `${from + i}:${line}`)), _jsx(BodyText, { emphasis: "dim", children: fit("←→ gate · PageUp/PageDown page") })] })), task?.state === "human" && (_jsx(Panel, { title: `PARK / ${task.id}`, children: decision === undefined
198
+ ? _jsx(BodyText, { children: fit("park evidence unavailable — refresh before deciding") })
199
+ : (_jsxs(_Fragment, { children: [_jsx(BodyText, { children: fit(`#L${decision.park.line} ${decision.park.kind ?? "unknown kind"}${decision.park.failedGate ? ` · failed gate ${decision.park.failedGate}` : ""} · ${decision.park.reason ?? "no reason recorded"}`) }), _jsx(BodyText, { children: fit(`blocks ${blocked.length === 0 ? "nothing" : blocked.join(", ")} · attempts ${decision.attempts}`) }), _jsx(BodyText, { children: fit(decision.verbs.length === 0 ? `diagnostic: ${decision.diagnostic ?? "no verb"}` : `decisions: ${decision.verbs.join(" · ")} (a Actions)`) }), _jsx(BodyText, { emphasis: "dim", children: fit(run.live ? "matching live daemon enacts a release at its next task boundary" : run.blockingRunId ? `other live run ${run.blockingRunId} holds the lock; resume after it ends` : "no live owner: a release records permission; resume dispatches") })] })) })), menu !== null && (_jsx(Panel, { title: `ACTIONS / ${menu.taskId}`, focused: true, children: menu.verbs.length === 0
200
+ ? _jsx(BodyText, { children: fit(`no decision — ${decision?.diagnostic ?? "no verb for this park"}`) })
201
+ : menu.verbs.map((verb, i) => _jsx(BodyText, { emphasis: i === menu.selection ? "strong" : "normal", children: fit(`${i === menu.selection ? `${GLYPHS.pointer} ` : " "}${verb}`) }, verb)) })), confirming !== null && (_jsx(Panel, { title: `CONFIRM ${confirming.command.verb.toUpperCase()}`, focused: true, children: decisionLines(decisionConfirmLines(confirming)).map((line, i) => _jsx(BodyText, { children: line }, `${i}:${line}`)) })), receipt !== null && (_jsx(Panel, { title: receipt.ok ? "RECEIPT · appended" : "RECEIPT · refused", children: decisionLines(decisionReceiptLines(receipt)).map((line, i) => _jsx(BodyText, { children: line }, `${i}:${line}`)) })), notice !== null && _jsx(BodyText, { children: notice }), _jsx(BodyText, { emphasis: "dim", children: fitCells(["↑↓ Task", "←→ Verdict gate", "PgUp/PgDn Page", "x Outcome", decisionKeybar(session.decisions, decision)].filter(Boolean).join(" · "), Math.max(1, Math.floor(columns))) })] }));
202
+ }
@@ -0,0 +1,50 @@
1
+ import { type ReactNode } from "react";
2
+ import { type ShellPlan } from "./layout.js";
3
+ import { SHELL_BINDINGS } from "./keys.js";
4
+ import type { LiveStoreSnapshot } from "./live-store.js";
5
+ export type ShellView = "home" | "run" | "evidence";
6
+ export type ShellFocus = ShellPlan["focus"][number];
7
+ export interface ShellState {
8
+ view: ShellView;
9
+ focus: ShellFocus;
10
+ scroll: number;
11
+ help: boolean;
12
+ evidenceSection?: number;
13
+ canOpen?: boolean;
14
+ shortcutColumns?: number;
15
+ canResize?: boolean;
16
+ editorKind?: "filter" | "export";
17
+ editor?: string;
18
+ query: string;
19
+ overlay?: readonly string[];
20
+ overlayOffset: number;
21
+ quit: boolean;
22
+ staged: readonly string[];
23
+ reviewing: boolean;
24
+ }
25
+ export declare const initialShellState: () => ShellState;
26
+ export declare const shellHelp: (bindings?: readonly (typeof SHELL_BINDINGS)[number][]) => string[];
27
+ export interface ShellPaintRow {
28
+ evidenceId?: string;
29
+ text: string;
30
+ column: number;
31
+ row: number;
32
+ columns: number;
33
+ rows: number;
34
+ }
35
+ export type ShellCommit = ShellPlan & {
36
+ paintedRows: readonly ShellPaintRow[];
37
+ contentRows: number;
38
+ scroll: number;
39
+ };
40
+ /** Chrome remains outside the clipped, paged leaf at every measured size. */
41
+ export declare function CockpitShell({ snapshot, state, columns, rows, version, children, onCommit, colour }: {
42
+ snapshot: LiveStoreSnapshot;
43
+ state: ShellState;
44
+ columns: number;
45
+ rows: number;
46
+ version: string;
47
+ children: ReactNode;
48
+ onCommit?: (plan: ShellCommit) => void;
49
+ colour?: "truecolor" | "reduced" | "none";
50
+ }): import("react").JSX.Element;
@@ -0,0 +1,74 @@
1
+ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
2
+ import { Box, Text } from "ink";
3
+ import { useLayoutEffect, useRef, useState } from "react";
4
+ import { ShellPresentation, ShellTheme } from "./components.js";
5
+ import { planShell } from "./layout.js";
6
+ import { SHELL_BINDINGS, shellBindings } from "./keys.js";
7
+ import { SHELL_PALETTE, SHELL_REDUCED_PALETTE } from "./theme.js";
8
+ import { fitCells } from "./width.js";
9
+ export const initialShellState = () => ({ view: "home", focus: "content", scroll: 0, help: false, query: "", overlayOffset: 0, quit: false, staged: [], reviewing: false });
10
+ export const shellHelp = (bindings = SHELL_BINDINGS) => [
11
+ ...bindings.map(b => `${b.key} ${b.label}`),
12
+ "Shift-Tab reverses focus; folded regions are skipped.",
13
+ "Esc closes deepest overlay; it never quits.",
14
+ "Text: q1? is literal; Enter applies; Esc cancels.",
15
+ "Confirm: y alone confirms; n/Esc cancels.",
16
+ "Tab now moves focus; view numbers are 1/4/5.",
17
+ "Fleet: tickmarkr fleet", "Plan: tickmarkr plan", "Health: tickmarkr doctor",
18
+ ];
19
+ /** Read the Yoga geometry that actually painted, including clipped/scrolled leaves.
20
+ * Targets are resolved from these rows, never from guessed header offsets. */
21
+ function paintedRows(root) {
22
+ const result = [];
23
+ const textOf = (node) => node.nodeName === "#text"
24
+ ? node.nodeValue : node.childNodes.map(textOf).join("");
25
+ const visit = (node, x, y, inheritedEvidence) => {
26
+ if (node.style.display === "none")
27
+ return;
28
+ const evidenceId = typeof node.attributes.shellEvidence === "string" ? node.attributes.shellEvidence : inheritedEvidence;
29
+ const column = x + (node.yogaNode?.getComputedLeft() ?? 0);
30
+ const row = y + (node.yogaNode?.getComputedTop() ?? 0);
31
+ if (node.nodeName === "ink-text")
32
+ result.push({ evidenceId, text: textOf(node), column, row,
33
+ columns: node.yogaNode?.getComputedWidth() ?? 0, rows: node.yogaNode?.getComputedHeight() ?? 0 });
34
+ else
35
+ for (const child of node.childNodes)
36
+ if (child.nodeName !== "#text")
37
+ visit(child, column, row, evidenceId);
38
+ };
39
+ visit(root, 0, 0);
40
+ return result;
41
+ }
42
+ /** Chrome remains outside the clipped, paged leaf at every measured size. */
43
+ export function CockpitShell({ snapshot, state, columns, rows, version, children, onCommit, colour = "truecolor" }) {
44
+ const plan = planShell(columns, rows, state.shortcutColumns);
45
+ const root = useRef(null);
46
+ const leaf = useRef(null);
47
+ const [contentRows, setContentRows] = useState(0);
48
+ const scroll = Math.min(state.scroll, Math.max(0, contentRows - plan.bodyRows));
49
+ useLayoutEffect(() => {
50
+ const measured = Math.ceil(leaf.current?.yogaNode?.getComputedHeight() ?? 0);
51
+ if (!state.overlay && !state.help && state.editor === undefined)
52
+ setContentRows(measured);
53
+ if (plan.refused)
54
+ onCommit?.({ ...plan, paintedRows: [], contentRows: 0, scroll: 0 });
55
+ else if (root.current)
56
+ onCommit?.({ ...plan, paintedRows: paintedRows(root.current), contentRows: overlay?.length ?? measured, scroll: displayOffset });
57
+ });
58
+ const palette = colour === "reduced" ? SHELL_REDUCED_PALETTE : SHELL_PALETTE;
59
+ const ink = (color) => colour === "none" ? undefined : color;
60
+ const line = (text, width = columns, color = palette.text) => _jsx(Text, { color: ink(color), wrap: "truncate-end", children: fitCells(text.replace(/[\x00-\x1f\x7f]/g, " "), width) });
61
+ if (plan.refused)
62
+ return _jsxs(Box, { flexDirection: "column", width: columns, backgroundColor: ink(palette.surface), children: [line("Need at least 40x14; resize or q Quit"), line("q Quit | Ctrl-C Quit")] });
63
+ const op = snapshot.operator;
64
+ const bindings = shellBindings(state);
65
+ const overlay = state.editor !== undefined ? [`${state.editorKind === "export" ? "Export destination" : "Filter"}: ${state.editor}`, "Enter Apply | Esc Cancel"] : state.overlay ?? (state.help ? shellHelp(bindings) : undefined);
66
+ const totalRows = overlay?.length ?? contentRows;
67
+ const displayOffset = overlay ? Math.min(state.overlayOffset, Math.max(0, totalRows - plan.bodyRows)) : scroll;
68
+ const position = `${totalRows ? displayOffset + 1 : 0}–${Math.min(totalRows, displayOffset + plan.bodyRows)}/${totalRows} | ${Math.max(0, totalRows - plan.bodyRows)} hidden`;
69
+ const keybar = bindings.filter(b => ["focus", "open", "actions", "help", "quit"].includes(b.action) && (columns >= 60 || b.action !== "focus")).map(b => `${b.key} ${columns < 60 && b.action === "actions" ? "Act" : b.label}`).join(" | ");
70
+ const status = columns < 60 ? `${op.label} | tip ${op.currentTip.toUpperCase()}` : `${op.label} | MERGED ${op.merged}/${op.planned ?? "?"} | CURRENT TIP ${op.currentTip.toUpperCase()} | ${snapshot.freshness}`;
71
+ return _jsx(ShellPresentation.Provider, { value: true, children: _jsx(ShellTheme.Provider, { value: { mode: colour, palette }, children: _jsxs(Box, { ref: root, flexDirection: "column", width: columns, height: rows, flexShrink: 0, backgroundColor: ink(palette.surface), children: [line(`tickmarkr ${version} | ${state.view.toUpperCase()} | ${snapshot.journal.source.split("/").at(-2) ?? "run identity unknown"}`), line(columns < 60
72
+ ? `1H 4R 5E | ${position}`
73
+ : `1 Home 4 Run 5 Evidence | ${state.focus} | ${position}`), line("─".repeat(columns), columns, palette.chrome), _jsxs(Box, { height: plan.bodyRows, flexShrink: 0, flexDirection: "row", overflow: "hidden", children: [_jsx(Box, { width: 1, flexDirection: "column", children: Array.from({ length: plan.bodyRows }, (_, i) => _jsx(Text, { color: ink(palette.chrome), children: "\u2502" }, i)) }), plan.rail > 0 && _jsxs(_Fragment, { children: [_jsx(Box, { width: plan.rail, flexDirection: "column", children: ["1 Home", "4 Run", "5 Evidence", "", "CLI:", "fleet", "plan", "doctor"].map((t, i) => _jsx(Text, { color: ink(palette.chrome), children: fitCells(t, plan.rail) }, i)) }), _jsx(Box, { width: 1, flexDirection: "column", children: Array.from({ length: plan.bodyRows }, (_, i) => _jsx(Text, { color: ink(palette.chrome), children: "\u2502" }, i)) })] }), _jsxs(Box, { width: plan.bodyColumns, height: plan.bodyRows, flexDirection: "column", overflow: "hidden", flexShrink: 0, children: [overlay?.slice(displayOffset, displayOffset + plan.bodyRows).map((t, i) => _jsx(Text, { wrap: "truncate-end", color: ink(palette.text), children: fitCells(t, plan.bodyColumns) }, i)), _jsx(Box, { ref: leaf, display: overlay ? "none" : "flex", flexDirection: "column", flexShrink: 0, marginTop: -scroll, children: children })] }), plan.shortcuts > 0 && _jsxs(_Fragment, { children: [_jsx(Box, { width: 1, flexDirection: "column", children: Array.from({ length: plan.bodyRows }, (_, i) => _jsx(Text, { color: ink(palette.chrome), children: "\u2502" }, i)) }), _jsx(Box, { width: plan.shortcuts, flexDirection: "column", children: bindings.map(b => _jsx(Text, { color: ink(palette.chrome), wrap: "truncate-end", children: fitCells(`${b.key} ${b.label}`, plan.shortcuts) }, b.key)) })] }), _jsx(Box, { width: 1, flexDirection: "column", children: Array.from({ length: plan.bodyRows }, (_, i) => _jsx(Text, { color: ink(palette.chrome), children: "\u2502" }, i)) })] }), line("─".repeat(columns), columns, palette.chrome), line(status), line(keybar), line("─".repeat(columns), columns, palette.chrome)] }) }) });
74
+ }
@@ -165,3 +165,30 @@ export type PaletteGuardVerdict = {
165
165
  * are intentionally ignored: only parsed colour values can earn a verdict.
166
166
  */
167
167
  export declare function guardDataRamp(candidate: readonly ColourValue[], surface?: HexColour): PaletteGuardVerdict;
168
+ /** FINAL live palette. Printed TOKENS and historical evidence remain independent. */
169
+ export declare const SHELL_PALETTE: {
170
+ readonly surface: "#1a1a19";
171
+ readonly text: "#E6FDFF";
172
+ readonly chrome: "#D9D7DD";
173
+ readonly passed: "#90C4A4";
174
+ readonly running: "#5A76AE";
175
+ readonly human: "#ffaf00";
176
+ readonly failure: "#B07BAC";
177
+ readonly unknown: "#D9D7DD";
178
+ readonly disabled: "#D9D7DD";
179
+ };
180
+ export declare const SHELL_REDUCED_PALETTE: {
181
+ readonly surface: "black";
182
+ readonly text: "whiteBright";
183
+ readonly chrome: "white";
184
+ readonly passed: "green";
185
+ readonly running: "blue";
186
+ readonly human: "yellow";
187
+ readonly failure: "magenta";
188
+ readonly unknown: "white";
189
+ readonly disabled: "white";
190
+ };
191
+ export type ShellColourMode = "truecolor" | "reduced" | "none";
192
+ export type ShellPalette = typeof SHELL_PALETTE | typeof SHELL_REDUCED_PALETTE;
193
+ /** Resolve the real terminal environment once, at mount time. */
194
+ export declare function resolveShellColourMode(environment?: NodeJS.ProcessEnv): ShellColourMode;
@@ -174,3 +174,24 @@ export function guardDataRamp(candidate, surface = COCKPIT_SURFACE) {
174
174
  issues,
175
175
  };
176
176
  }
177
+ /** FINAL live palette. Printed TOKENS and historical evidence remain independent. */
178
+ export const SHELL_PALETTE = {
179
+ surface: "#1a1a19", text: "#E6FDFF", chrome: "#D9D7DD",
180
+ passed: "#90C4A4", running: "#5A76AE", human: "#ffaf00",
181
+ failure: "#B07BAC", unknown: "#D9D7DD", disabled: "#D9D7DD",
182
+ };
183
+ export const SHELL_REDUCED_PALETTE = {
184
+ surface: "black", text: "whiteBright", chrome: "white",
185
+ passed: "green", running: "blue", human: "yellow",
186
+ failure: "magenta", unknown: "white", disabled: "white",
187
+ };
188
+ /** Resolve the real terminal environment once, at mount time. */
189
+ export function resolveShellColourMode(environment = process.env) {
190
+ if (Object.hasOwn(environment, "NO_COLOR"))
191
+ return "none";
192
+ const colourTerm = environment.COLORTERM?.toLowerCase() ?? "";
193
+ const term = environment.TERM?.toLowerCase() ?? "";
194
+ return /^(truecolor|24bit)$/u.test(colourTerm) || /(truecolor|24bit|direct)/u.test(term)
195
+ ? "truecolor"
196
+ : "reduced";
197
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tickmarkr",
3
- "version": "2.4.1",
3
+ "version": "2.5.0",
4
4
  "description": "Spec in, verified work out.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -8,6 +8,15 @@ argument-hint: "[spec-or-directory ...]"
8
8
 
9
9
  Use this to execute a requested sequence of repository specs. It is SDD-agnostic: each target can be any file or directory accepted by `tickmarkr compile`.
10
10
 
11
+ Use the canonical loop skill's [cockpit, parked decisions and printed twins](../tickmarkr-loop/SKILL.md#cockpit-parked-decisions-and-printed-twins)
12
+ for each target: delivered views are **1 Home, 4 Run, 5 Evidence**; Fleet/Bootstrap and
13
+ Plan/Health remain follow-ons with CLI entries. The recorded 1/3 merged, human T2, blocked
14
+ T3 case stays PARTIAL despite tip pass. Review the Run confirmation, read back the appended
15
+ `task-approved` receipt, and explicitly `tickmarkr resume <runId>` when no live owner exists;
16
+ approval records permission and never dispatches work itself. Non-TTY readers keep `status`,
17
+ `report` and default-watch line output, with `--watch --plain` as the TTY fallback.
18
+ Keep `name: tickmarkr-auto` and the installed links to canonical `skills/` intact.
19
+
11
20
  ## Two-tier by default — role check before the loop
12
21
 
13
22
  When working in a multi-agent terminal environment, decide your role before starting:
@@ -85,5 +94,5 @@ After sending, **confirm delivery** by reading the target pane and verifying the
85
94
  3. **Plan** — run `tickmarkr plan`. Review routes, capability-floor warnings, and human gates before execution.
86
95
  4. **Run** — run `tickmarkr run`. Watch the run journal for its terminal events rather than polling agents, using the shipped watcher — `.claude/skills/tickmarkr-overseer/scripts/watch-journal.sh <state-dir>/runs 20 28800` — which takes a line baseline at arm time, then wakes ONCE on `run-end`, `task-human`, `task-failed` or `consult-verdict` and grades the run-end summary against every green clause for you. Re-arm after every wake. ⛔ Never `tail -F | grep -m1` (run-end is the journal's last line, so tail never notices the broken pipe and the watcher hangs forever) and never a pane-level done wait (it fires on every agent turn end, not mission end). ⚠ A bare whole-file `grep -q '"event":"run-end"'` is the trap the watcher exists to avoid: on a resume it matches the PREVIOUS run's run-end and returns instantly, so a re-armed watcher reads as coverage that does not exist. Resolve blocked interactions in the relevant agent session.
87
96
  5. **Verify and consolidate** — continue only after a green run. A run is green when the run-end event exists in the journal, the tip verify is not "failed", and the summary's `failed`, `human`, `blocked` and `pending` buckets are all empty — a run with a parked task is partial, not green. Tickmarkr consolidates accepted work on `tickmarkr/<runId>` and never signs off to the main branch. A human controls any later release merge.
88
- 6. **Record** — write `tickmarkr report <runId> --md` beside the source spec and commit the execution record when the repository tracks those records.
97
+ 6. **Record** — `tickmarkr report <runId> --md` prints Markdown to stdout; redirect explicitly beside the spec (for example `tickmarkr report <runId> --md > feature.record.md`) and commit the execution record when the repository tracks those records.
89
98
  7. **Continue** — move to the next requested target. If a target fails or is parked, stop with the journal evidence rather than silently skipping it.
@@ -90,4 +90,75 @@ When spawning consultants (agents gathering synthesis input for decisions like S
90
90
  3. **Plan** — run `tickmarkr plan`. Review the routing table, capability-floor warnings, and every human gate, including work that each gate blocks.
91
91
  4. **Run** — run `tickmarkr run`. Watch the run journal for its terminal events rather than polling agents, using the shipped watcher — `.claude/skills/tickmarkr-overseer/scripts/watch-journal.sh <state-dir>/runs 20 28800` — which takes a line baseline at arm time, then wakes ONCE on `run-end`, `task-human`, `task-failed` or `consult-verdict` and grades the run-end summary against every green clause for you. Re-arm after every wake. ⛔ Never `tail -F | grep -m1` (run-end is the journal's last line, so tail never notices the broken pipe and the watcher hangs forever) and never a pane-level done wait (it fires on every agent turn end, not mission end). ⚠ A bare whole-file `grep -q '"event":"run-end"'` is the trap the watcher exists to avoid: on a resume it matches the PREVIOUS run's run-end and returns instantly, so a re-armed watcher reads as coverage that does not exist. Resolve blocked interactions in the agent session; do not turn them into proxy questions.
92
92
  5. **Verify and consolidate** — accept only a green run. A run is green when the run-end event exists in the journal, the tip verify is not "failed", and the summary's `failed`, `human`, `blocked` and `pending` buckets are all empty — a run with a parked task is partial, not green. Tickmarkr consolidates accepted task work on `tickmarkr/<runId>`; it never signs off to the main branch. A human may later merge that integration branch through the repository's normal release process.
93
- 6. **Record** — write `tickmarkr report <runId> --md` beside the source spec and commit the execution record when the repository tracks those records. Then [stand down](#stand-down-mission-end-and-retirement).
93
+ 6. **Record** — `tickmarkr report <runId> --md` prints Markdown to stdout. Redirect it explicitly beside the source spec (for example `tickmarkr report <runId> --md > feature.record.md`) and commit the execution record when the repository tracks those records. Then [stand down](#stand-down-mission-end-and-retirement).
94
+
95
+ ## Cockpit, parked decisions and printed twins
96
+
97
+ The shipped cockpit is **1 Home, 4 Run, 5 Evidence**: `tickmarkr ui [runId]` defaults to
98
+ Home on the latest journal, or empty Home with no run. `tickmarkr ui <runId> --view run`
99
+ and `--view evidence` select delivered views; `tickmarkr ui --setup <runId>` opens Run
100
+ Parks for that run. Fleet/Bootstrap and Plan/Health are follow-ons; keys 2/3/6 are not
101
+ installed. Continue using `fleet`, `init`, `plan` and `doctor` as CLI workflows.
102
+
103
+ Walk the recorded partial-human-park case before declaring green: **1/3 merged**, human
104
+ T2, blocked T3, tip passed is **PARTIAL**. T2 is a `humanGate: true` park before dispatch.
105
+ In Run select T2, press `a` Actions, choose Approve with Enter, then read the confirmation's
106
+ run/task, original park `#L`, actor/reason, exact argv, consequence and enactor. Only `y`
107
+ confirms; `n`/Esc cancel; Enter never confirms. Read the receipt's appended `task-approved`
108
+ line and actor/reason back from the journal. Approval records permission, never dispatch,
109
+ a passed gate or task completion. With no live owner it says **approved; resume required**:
110
+ exit the observer and run `tickmarkr resume <runId>` explicitly. A matching live daemon
111
+ enacts at its next task boundary; if a different live run owns the repository lock, wait
112
+ for that run to end before resuming this one.
113
+
114
+ For a non-TTY decision, the same command is
115
+ `tickmarkr approve <runId> T2 --by operator --reason 'ready to proceed'`; check its receipt
116
+ and explicitly resume. Preserve resume refusals and repair the named source/config issue
117
+ (including deny/prefer conflicts); never edit the compiled graph to force a result. Resume
118
+ makes CURRENT TIP PENDING; historical GATES RAN does not prove completion. The completed
119
+ case has 3/3 recorded merges, a latest run-end, a nonfailed known tip result and empty
120
+ `failed`, `human`, `blocked`, `pending` buckets. A mismatched graph is “not comparable.”
121
+
122
+ Run offers only validated park verbs: human/attempt-cap/other non-gate parks allow approve;
123
+ infra allows approve or `--recheck`; review gate-fail allows `--waive`, `--uphold` or
124
+ `--recheck`; other gate-fail allows waive/recheck. Waive satisfies only the identified
125
+ failed gate, uphold funds a fixed attempt carrying review findings, and recheck reruns the
126
+ declared battery without satisfying a gate. Attempt-cap approval resets the budget while
127
+ retaining routing exclusions. Tombstones and failures without identified gate evidence
128
+ are diagnostic-only. Decisions cannot be undone; stale or duplicate decisions refuse.
129
+
130
+ Use `tickmarkr status <runId>` and `tickmarkr status <runId> --oneline` for preserved
131
+ snapshots. `tickmarkr status <runId> --watch` opens Run on a TTY and uses line output on
132
+ non-TTY; `--watch --plain` keeps the line/ANSI fallback even on a TTY. `--watch --events`
133
+ replays and follows projected decision JSON documents on stdout; `--jsonl` and
134
+ `--decision-events` are aliases. Keep stderr keepalives separate, never `2>&1`; raw
135
+ `journal.jsonl` remains distinct. Webhooks require explicit opt-in. `report` retains text,
136
+ learning preview and comparison warnings; `report --md` stays stdout, `--compare
137
+ <baseline-runId>` compares records, `--bundle <path>` writes a proof bundle, and `stats`
138
+ reports all runs without a run ID. Keep `fleet --print` and `fleet --why` separate.
139
+ Absent usage/cost evidence remains “not measurable.”
140
+
141
+ Manual UI keeps a receipt at run-end and observes a later resume of that same run. The
142
+ daemon-owned board by default gracefully stops its own watch presence before closing its owned pane;
143
+ unconfirmed cleanup is reported; `visibility.keepPanes: forever` preserves panes. It preserves Herdr grouping, short titles and placement
144
+ beside the caller without taking focus. `q`, Ctrl-C and SIGTERM restore raw/pointer/title/
145
+ alternate-screen state and release only this observer's presence. `?` opens shortcuts,
146
+ Tab/Shift-Tab traverse visible focus, and Esc closes the deepest overlay. Text input keeps
147
+ `q1?` literal. Evidence retains original `#L` pointers and full verdict paging with stable
148
+ selection while Follow is off; Run's `o` focuses only a verified owned pane, otherwise
149
+ showing its evidence diagnostic. Missing historical reviewer-floor metadata stays unknown.
150
+
151
+ For inert guidance use `tickmarkr ui --help`, `tickmarkr eval --help`,
152
+ `tickmarkr unlock --help` and `tickmarkr profile reset --help`. Before `--`, help returns
153
+ without UI/probes/writes; after `--`, arguments are literal data. Displayed examples do
154
+ not execute. Recovery uses `unlock <runId>` for a matching dead lock or `unlock --garbage`
155
+ for malformed bytes, with confirmation (`--yes` for non-TTY) and identity recheck; live,
156
+ inaccessible or changed holders refuse. `doctor --cached`, `--probe-preflight`,
157
+ `--fix-only` and `--refresh-catalog` distinguish cached reads, budget disclosure, local
158
+ repair and catalog refresh; default doctor and `--fix` still probe. `scope <intent-file>
159
+ --preview` performs no probes, model calls or writes; authoring requires confirmation or
160
+ `--yes`. These actions remain CLI entries until their follow-on views ship.
161
+
162
+ Keep this skill's canonical identity `tickmarkr-loop`; sibling skills link to this
163
+ walkthrough. In this repository `skills/` is canonical and installed `.claude/skills/`
164
+ files resolve to it; change the canonical source, never create a second help skill.
@@ -12,6 +12,18 @@ to the user with evidence.
12
12
  The mission is the skill argument. If empty, ask the user what to run end-to-end before doing anything else.
13
13
  Requires `HERDR_ENV=1`; if unset, say so and stop.
14
14
 
15
+ Use the canonical loop skill's [cockpit, parked decisions and printed twins](../tickmarkr-loop/SKILL.md#cockpit-parked-decisions-and-printed-twins)
16
+ when reading or relaying operator evidence. Delivered views are **1 Home, 4 Run, 5 Evidence**;
17
+ Fleet/Bootstrap and Plan/Health remain follow-ons with existing CLI entries. A recorded
18
+ 1/3 merged, human T2, blocked T3 run is PARTIAL despite tip pass. The orchestrator owns
19
+ the Run confirmation and read-back of the appended `task-approved` receipt after a ruling;
20
+ permission is not dispatch, and a closed run needs explicit `tickmarkr resume <runId>`.
21
+ Manual UI retains the receipt; the daemon-owned board gracefully stands down only its own
22
+ presence before closing its owned pane. Non-TTY supervision keeps `status`, `report` and
23
+ default-watch line output (`--watch --plain` is also available on a TTY). Keep the canonical
24
+ `tickmarkr-overseer` identity and installed links to `skills/`; the linked walkthrough is
25
+ the shared guidance source, not a new help skill.
26
+
15
27
  **THE ENGINE IS THE DEFAULT EXECUTOR.** A mission that names a milestone, a phase, or a spec runs the
16
28
  loop: `tickmarkr compile` → `plan` → `run` → `report`, and the JOURNAL is the record. `compile` ingests
17
29
  GSD phase plans (`src/compile/gsd.ts`), so *"this repo uses GSD"* is not a reason to bypass it. The