tickmarkr 2.4.2 → 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 (70) hide show
  1. package/README.md +110 -24
  2. package/dist/cli/commands/approve.d.ts +42 -0
  3. package/dist/cli/commands/approve.js +80 -13
  4. package/dist/cli/commands/doctor.d.ts +234 -0
  5. package/dist/cli/commands/doctor.js +139 -5
  6. package/dist/cli/commands/report.js +15 -44
  7. package/dist/cli/commands/scope.js +36 -6
  8. package/dist/cli/commands/stats.d.ts +2 -0
  9. package/dist/cli/commands/stats.js +42 -23
  10. package/dist/cli/commands/status.js +20 -4
  11. package/dist/cli/commands/ui.js +42 -53
  12. package/dist/cli/commands/unlock.d.ts +1 -1
  13. package/dist/cli/commands/unlock.js +59 -9
  14. package/dist/cli/help.d.ts +219 -0
  15. package/dist/cli/help.js +212 -0
  16. package/dist/cli/index.d.ts +42 -2
  17. package/dist/cli/index.js +23 -8
  18. package/dist/drivers/herdr.d.ts +7 -2
  19. package/dist/drivers/herdr.js +78 -48
  20. package/dist/drivers/orca.d.ts +3 -2
  21. package/dist/drivers/orca.js +43 -4
  22. package/dist/drivers/subprocess.d.ts +1 -0
  23. package/dist/drivers/subprocess.js +3 -0
  24. package/dist/drivers/types.d.ts +14 -0
  25. package/dist/gates/artifact-manifest.d.ts +50 -0
  26. package/dist/gates/artifact-manifest.js +23 -0
  27. package/dist/plan/scope.d.ts +25 -0
  28. package/dist/plan/scope.js +92 -12
  29. package/dist/report/operator-record.d.ts +49 -0
  30. package/dist/report/operator-record.js +137 -0
  31. package/dist/run/daemon.d.ts +3 -4
  32. package/dist/run/daemon.js +18 -10
  33. package/dist/run/lock.d.ts +59 -2
  34. package/dist/run/lock.js +184 -26
  35. package/dist/run/operator-state.d.ts +86 -0
  36. package/dist/run/operator-state.js +165 -0
  37. package/dist/run/supervision.d.ts +32 -0
  38. package/dist/run/supervision.js +138 -17
  39. package/dist/tui/cockpit/capture.d.ts +19 -0
  40. package/dist/tui/cockpit/capture.js +89 -1
  41. package/dist/tui/cockpit/components.d.ts +15 -1
  42. package/dist/tui/cockpit/components.js +79 -9
  43. package/dist/tui/cockpit/decision-actions.d.ts +147 -0
  44. package/dist/tui/cockpit/decision-actions.js +315 -0
  45. package/dist/tui/cockpit/derive.d.ts +1 -1
  46. package/dist/tui/cockpit/derive.js +2 -0
  47. package/dist/tui/cockpit/evidence-view.d.ts +119 -0
  48. package/dist/tui/cockpit/evidence-view.js +210 -0
  49. package/dist/tui/cockpit/home-view.d.ts +88 -0
  50. package/dist/tui/cockpit/home-view.js +240 -0
  51. package/dist/tui/cockpit/keys.d.ts +125 -0
  52. package/dist/tui/cockpit/keys.js +31 -0
  53. package/dist/tui/cockpit/layout.d.ts +14 -0
  54. package/dist/tui/cockpit/layout.js +15 -0
  55. package/dist/tui/cockpit/live-runtime.d.ts +46 -0
  56. package/dist/tui/cockpit/live-runtime.js +683 -0
  57. package/dist/tui/cockpit/live-store.d.ts +289 -0
  58. package/dist/tui/cockpit/live-store.js +308 -0
  59. package/dist/tui/cockpit/live.d.ts +21 -1
  60. package/dist/tui/cockpit/live.js +12 -1
  61. package/dist/tui/cockpit/run-view.d.ts +100 -0
  62. package/dist/tui/cockpit/run-view.js +202 -0
  63. package/dist/tui/cockpit/shell.d.ts +50 -0
  64. package/dist/tui/cockpit/shell.js +74 -0
  65. package/dist/tui/cockpit/theme.d.ts +27 -0
  66. package/dist/tui/cockpit/theme.js +21 -0
  67. package/package.json +1 -1
  68. package/skills/tickmarkr-auto/SKILL.md +10 -1
  69. package/skills/tickmarkr-loop/SKILL.md +72 -1
  70. package/skills/tickmarkr-overseer/SKILL.md +12 -0
@@ -0,0 +1,147 @@
1
+ import { approve, type ApprovalDisposition, type ApprovalRunOwner, type DecisionVerb, type NewestPark } from "../../cli/commands/approve.js";
2
+ import type { RunGraph } from "../../graph/schema.js";
3
+ import { Journal, type JournalEvent } from "../../run/journal.js";
4
+ /** One parked task and the verbs FINAL §3.3 lets the operator name on it. */
5
+ export interface RunDecision {
6
+ readonly taskId: string;
7
+ readonly park: NewestPark;
8
+ /** Count of recorded task-dispatch events — never max(attempt)+1. */
9
+ readonly attempts: number;
10
+ readonly verbs: readonly DecisionVerb[];
11
+ /** Present exactly when `verbs` is empty: why no verb exists, stated instead of invented. */
12
+ readonly diagnostic?: string;
13
+ /** Graph tasks whose deps name this park, when a comparable graph is supplied. */
14
+ readonly blocks: readonly string[];
15
+ }
16
+ /**
17
+ * The parks the journal's own status replay leaves on a human, in first-named order. The replay is
18
+ * the production one so this surface and `tickmarkr status` can never disagree about what is parked.
19
+ */
20
+ export declare function deriveRunDecisions(journal: Journal, graph?: RunGraph): readonly RunDecision[];
21
+ /** A decision the operator has named but not yet confirmed. */
22
+ export interface DecisionCommand {
23
+ readonly verb: DecisionVerb;
24
+ readonly taskId: string;
25
+ readonly reason?: string;
26
+ /** `--review-rounds`: the review-round ceiling the release carries. */
27
+ readonly reviewRounds?: number;
28
+ }
29
+ /** The exact argv the production command receives — built only from the closed verb set. */
30
+ export declare function decisionArgv(command: DecisionCommand, { runId, by }: {
31
+ runId: string;
32
+ by: string;
33
+ }): string[];
34
+ /** The confirm inset's facts, read at preview time. `observedEvents` pins the journal the preview saw. */
35
+ export interface DecisionPreview {
36
+ readonly runId: string;
37
+ readonly command: DecisionCommand;
38
+ readonly park: NewestPark;
39
+ readonly actor: string;
40
+ readonly argv: readonly string[];
41
+ readonly release: string | undefined;
42
+ readonly disposition: ApprovalDisposition;
43
+ readonly run: ApprovalRunOwner;
44
+ readonly journalFile: string;
45
+ readonly observedEvents: number;
46
+ }
47
+ export type DecisionPreviewResult = {
48
+ ok: true;
49
+ preview: DecisionPreview;
50
+ } | {
51
+ ok: false;
52
+ refusal: string;
53
+ };
54
+ export declare const journalFileFor: (cwd: string, runId: string) => string;
55
+ /**
56
+ * Predict the write before naming the one key that can make it. Refuses — appending nothing — when
57
+ * the named verb is outside the park's table, so a confirm inset can never promise a decision the
58
+ * command would refuse. The enactor is read from the run lock now, and read again at the receipt.
59
+ */
60
+ export declare function previewDecision(command: DecisionCommand, { cwd, runId, by }: {
61
+ cwd: string;
62
+ runId: string;
63
+ by: string;
64
+ }): DecisionPreviewResult;
65
+ /** Who enacts it — the three lock states, each stated by the command's own sentence. */
66
+ export declare function enactmentLines(run: ApprovalRunOwner, disposition: ApprovalDisposition): string[];
67
+ /** The confirm inset: run/task, the newest park line, actor/reason, exact argv, consequence, enactor. */
68
+ export declare function decisionConfirmLines(preview: DecisionPreview): readonly string[];
69
+ /** The task-approved row the file gained, located by its physical line. */
70
+ export interface AppendedDecision {
71
+ line: number;
72
+ event: JournalEvent;
73
+ }
74
+ export type DecisionReceipt = {
75
+ readonly ok: true;
76
+ readonly preview: DecisionPreview;
77
+ /** The exact string the production command returned. */
78
+ readonly message: string;
79
+ readonly appended: AppendedDecision;
80
+ readonly release: string | undefined;
81
+ readonly disposition: ApprovalDisposition;
82
+ /** The lock read again after the write — the enactor at receipt time. */
83
+ readonly run: ApprovalRunOwner;
84
+ } | {
85
+ readonly ok: false;
86
+ readonly preview: DecisionPreview;
87
+ /** The command's own refusal, or this boundary's — verbatim, and nothing was shown as success. */
88
+ readonly refusal: string;
89
+ /** True when the journal moved past the preview: refresh and preview again. */
90
+ readonly stale?: true;
91
+ };
92
+ /**
93
+ * THE write. Re-validates the preview against the journal as it is now (a release appended by another
94
+ * actor after preview refuses instead of appending twice), invokes the production command with the
95
+ * previewed argv, then reads the file back and reports only the task-approved row it actually gained.
96
+ * A successful-looking command whose append cannot be read back is a refusal, never a receipt.
97
+ */
98
+ export declare function executeDecision(preview: DecisionPreview, { cwd, command }: {
99
+ cwd: string;
100
+ command?: typeof approve;
101
+ }): Promise<DecisionReceipt>;
102
+ /** The receipt: what the file gained, verified, and who enacts it — read after the write. */
103
+ export declare function decisionReceiptLines(receipt: DecisionReceipt): readonly string[];
104
+ export interface DecisionMenu {
105
+ readonly taskId: string;
106
+ readonly verbs: readonly DecisionVerb[];
107
+ readonly selection: number;
108
+ }
109
+ /** Observable state of the decision flow. Every field is drawn; a state that cannot be drawn cannot exist. */
110
+ export interface DecisionSession {
111
+ readonly menu: DecisionMenu | null;
112
+ readonly confirming: DecisionPreview | null;
113
+ readonly receipt: DecisionReceipt | null;
114
+ readonly notice: string | null;
115
+ }
116
+ export declare const initialDecisionSession: () => DecisionSession;
117
+ export interface DecisionKeyEvent {
118
+ readonly input: string;
119
+ readonly key: {
120
+ readonly upArrow?: boolean;
121
+ readonly downArrow?: boolean;
122
+ readonly return?: boolean;
123
+ readonly escape?: boolean;
124
+ };
125
+ }
126
+ export interface DecisionKeyResult {
127
+ readonly session: DecisionSession;
128
+ /** The verb picked from the menu — the caller previews it (a read) and stores the preview. */
129
+ readonly open?: DecisionCommand;
130
+ /** Present only when the confirm key landed on a preview — the one moment a write may happen. */
131
+ readonly confirm?: DecisionPreview;
132
+ /** True when this key was consumed by the decision flow. */
133
+ readonly handled: boolean;
134
+ }
135
+ /**
136
+ * The reducer never writes and never reads a file; it names commands. `a` opens the menu for the
137
+ * selected park (a diagnostic park opens a menu with no verbs, stating why); ↑↓ move; Enter picks a
138
+ * verb — which OPENS the confirm inset, never confirms; `y` on a confirm names the write; `n`/Esc
139
+ * cancel the deepest layer. Enter on a confirm is inert by construction.
140
+ */
141
+ export declare function applyDecisionKey(session: DecisionSession, event: DecisionKeyEvent, selected: RunDecision | undefined): DecisionKeyResult;
142
+ /** Store the preview a picked verb produced — or its refusal as the notice. */
143
+ export declare function withDecisionPreview(session: DecisionSession, result: DecisionPreviewResult): DecisionSession;
144
+ /** Store the receipt of the one write — a refusal is a receipt too, drawn as such. */
145
+ export declare function withDecisionReceipt(session: DecisionSession, receipt: DecisionReceipt): DecisionSession;
146
+ /** The verbs the keybar may advertise for the current layer — nothing the flow cannot honour. */
147
+ export declare function decisionKeybar(session: DecisionSession, selected: RunDecision | undefined): string;
@@ -0,0 +1,315 @@
1
+ import { join } from "node:path";
2
+ import { APPROVAL_ENACTS, approvalDispositionForRelease, approvalEnactment, approvalRunOwner, approve, DECISION_VERBS, newestPark, permittedDecisionVerbs, readJournalEvents, releaseForDecision, } from "../../cli/commands/approve.js";
3
+ import { stateDirName } from "../../graph/graph.js";
4
+ import { Journal } from "../../run/journal.js";
5
+ const parkDiagnostic = (park) => {
6
+ if (park.tombstone)
7
+ return "tombstone — permanent by design; no verb releases it";
8
+ if (park.kind === "gate-fail" && park.failedGate === undefined) {
9
+ return "parked on gate-fail with no failed gate result on the newest park — refusing to infer one";
10
+ }
11
+ return undefined;
12
+ };
13
+ /**
14
+ * The parks the journal's own status replay leaves on a human, in first-named order. The replay is
15
+ * the production one so this surface and `tickmarkr status` can never disagree about what is parked.
16
+ */
17
+ export function deriveRunDecisions(journal, graph) {
18
+ const { events, sourceIndexes } = readJournalEvents(journal);
19
+ const decisions = [];
20
+ for (const [taskId, status] of journal.replayStatuses()) {
21
+ if (status !== "human")
22
+ continue;
23
+ const park = newestPark(events, taskId, sourceIndexes);
24
+ if (!park)
25
+ continue; // a "human" status with no task-human row is not a park this surface can name
26
+ const verbs = permittedDecisionVerbs(park);
27
+ const diagnostic = parkDiagnostic(park);
28
+ decisions.push({
29
+ taskId,
30
+ park,
31
+ attempts: events.filter((e) => e.event === "task-dispatch" && e.taskId === taskId).length,
32
+ verbs,
33
+ ...(diagnostic === undefined ? {} : { diagnostic }),
34
+ blocks: graph?.tasks.filter((t) => t.deps.includes(taskId)).map((t) => t.id) ?? [],
35
+ });
36
+ }
37
+ return decisions;
38
+ }
39
+ /** The exact argv the production command receives — built only from the closed verb set. */
40
+ export function decisionArgv(command, { runId, by }) {
41
+ if (!DECISION_VERBS.includes(command.verb)) {
42
+ throw new Error(`"${String(command.verb)}" is not a decision verb — permitted: ${DECISION_VERBS.join(", ")}`);
43
+ }
44
+ if (command.reviewRounds !== undefined
45
+ && (!Number.isSafeInteger(command.reviewRounds) || command.reviewRounds < 1)) {
46
+ throw new Error("--review-rounds must be a positive integer");
47
+ }
48
+ const flag = { approve: [], waive: ["--waive"], uphold: ["--uphold"], recheck: ["--recheck"] }[command.verb];
49
+ return [
50
+ runId, command.taskId, ...flag,
51
+ ...(command.reviewRounds === undefined ? [] : ["--review-rounds", String(command.reviewRounds)]),
52
+ "--by", by,
53
+ ...(command.reason === undefined ? [] : ["--reason", command.reason]),
54
+ ];
55
+ }
56
+ const dispositionForVerb = (verb, park) => approvalDispositionForRelease(releaseForDecision(verb, park));
57
+ export const journalFileFor = (cwd, runId) => join(stateDirName(cwd), "runs", runId, "journal.jsonl");
58
+ /**
59
+ * Predict the write before naming the one key that can make it. Refuses — appending nothing — when
60
+ * the named verb is outside the park's table, so a confirm inset can never promise a decision the
61
+ * command would refuse. The enactor is read from the run lock now, and read again at the receipt.
62
+ */
63
+ export function previewDecision(command, { cwd, runId, by }) {
64
+ let argv;
65
+ try {
66
+ argv = decisionArgv(command, { runId, by });
67
+ }
68
+ catch (e) {
69
+ return { ok: false, refusal: e.message };
70
+ }
71
+ let events;
72
+ let sourceIndexes;
73
+ try {
74
+ ({ events, sourceIndexes } = readJournalEvents(Journal.open(cwd, runId)));
75
+ }
76
+ catch (e) {
77
+ return { ok: false, refusal: e.message };
78
+ }
79
+ const park = newestPark(events, command.taskId, sourceIndexes);
80
+ if (!park)
81
+ return { ok: false, refusal: `task ${command.taskId} has no park in run ${runId} — nothing to decide` };
82
+ const verbs = permittedDecisionVerbs(park);
83
+ if (!verbs.includes(command.verb)) {
84
+ const why = parkDiagnostic(park) ?? `permitted: ${verbs.join(", ")}`;
85
+ return { ok: false, refusal: `${command.verb} is not a decision for ${command.taskId}'s ${park.kind ?? "unknown"} park (${why})` };
86
+ }
87
+ const released = events.slice(park.index + 1).find((e) => e.event === "task-approved" && e.taskId === command.taskId);
88
+ if (released) {
89
+ return { ok: false, refusal: `task ${command.taskId} was already released at #L${sourceIndexes[events.indexOf(released)] + 1} by ${String(released.data.by ?? "unknown")} — nothing to confirm` };
90
+ }
91
+ const release = releaseForDecision(command.verb, park);
92
+ return {
93
+ ok: true,
94
+ preview: {
95
+ runId, command, park, actor: by, argv, release, disposition: dispositionForVerb(command.verb, park),
96
+ run: approvalRunOwner(cwd, runId), journalFile: journalFileFor(cwd, runId), observedEvents: events.length,
97
+ },
98
+ };
99
+ }
100
+ const parkLine = (park) => `#L${park.line} ${park.kind ?? "unknown kind"}${park.failedGate ? ` · failed gate ${park.failedGate}` : ""}${park.reason ? ` · ${park.reason}` : ""}`;
101
+ const quoteArg = (arg) => (/^[\w.:/@=-]+$/u.test(arg) ? arg : JSON.stringify(arg));
102
+ /** What a confirmed write buys, from the command's own table — never a phrase of this file's own. */
103
+ function consequenceLines(verb, park, release, disposition) {
104
+ const lines = [`consequence disposition ${disposition}; appends one task-approved${release ? ` with release ${release}` : " with no release"}`];
105
+ if (verb === "waive")
106
+ lines.push(` marks gate ${park.failedGate} satisfied; it does NOT mark the task done`);
107
+ if (verb === "recheck")
108
+ lines.push(" marks no gate satisfied; the whole declared battery re-runs");
109
+ if (verb === "uphold")
110
+ lines.push(" sides with the reviewer; funds one fixed attempt carrying the findings");
111
+ lines.push(` it will ${APPROVAL_ENACTS[disposition]}`);
112
+ return lines;
113
+ }
114
+ /** Who enacts it — the three lock states, each stated by the command's own sentence. */
115
+ export function enactmentLines(run, disposition) {
116
+ const owner = run.live
117
+ ? "enactment matching live daemon — pending daemon enactment at its next task boundary"
118
+ : run.blockingRunId
119
+ ? `enactment other live run \`${run.blockingRunId}\` holds the repository lock — recorded, not dispatched`
120
+ : `enactment no live owner — recorded, not dispatched; resume required: tickmarkr resume ${run.runId}`;
121
+ return [owner, ` ${approvalEnactment(disposition, run)}`];
122
+ }
123
+ /** The confirm inset: run/task, the newest park line, actor/reason, exact argv, consequence, enactor. */
124
+ export function decisionConfirmLines(preview) {
125
+ return [
126
+ `run ${preview.runId}`,
127
+ `task ${preview.command.taskId}`,
128
+ `park ${parkLine(preview.park)}`,
129
+ `actor ${preview.actor}`,
130
+ `reason ${preview.command.reason ?? "none given"}`,
131
+ `argv tickmarkr approve ${preview.argv.map(quoteArg).join(" ")}`,
132
+ ...consequenceLines(preview.command.verb, preview.park, preview.release, preview.disposition),
133
+ ...enactmentLines(preview.run, preview.disposition),
134
+ `file ${preview.journalFile} · append only; cannot be undone`,
135
+ `y ${preview.command.verb} · n cancel · Enter never confirms`,
136
+ ];
137
+ }
138
+ /**
139
+ * THE write. Re-validates the preview against the journal as it is now (a release appended by another
140
+ * actor after preview refuses instead of appending twice), invokes the production command with the
141
+ * previewed argv, then reads the file back and reports only the task-approved row it actually gained.
142
+ * A successful-looking command whose append cannot be read back is a refusal, never a receipt.
143
+ */
144
+ export async function executeDecision(preview, { cwd, command = approve }) {
145
+ const { runId, command: named } = preview;
146
+ let before;
147
+ let beforeSourceIndexes;
148
+ try {
149
+ ({ events: before, sourceIndexes: beforeSourceIndexes } = readJournalEvents(Journal.open(cwd, runId)));
150
+ }
151
+ catch (e) {
152
+ return { ok: false, preview, refusal: e.message };
153
+ }
154
+ const park = newestPark(before, named.taskId, beforeSourceIndexes);
155
+ if (!park || park.index !== preview.park.index || park.line !== preview.park.line || park.kind !== preview.park.kind) {
156
+ return { ok: false, preview, stale: true, refusal: `task ${named.taskId}'s park changed since preview (${park ? `now #L${park.line} ${park.kind ?? "unknown"}` : "no park now"}) — refresh and preview again` };
157
+ }
158
+ const released = before.slice(park.index + 1).find((e) => e.event === "task-approved" && e.taskId === named.taskId);
159
+ if (released) {
160
+ return { ok: false, preview, stale: true, refusal: `task ${named.taskId} was released at #L${beforeSourceIndexes[before.indexOf(released)] + 1} by ${String(released.data.by ?? "unknown")} after this preview — no second decision appended; refresh` };
161
+ }
162
+ if (!permittedDecisionVerbs(park).includes(named.verb)) {
163
+ return { ok: false, preview, refusal: `${named.verb} is not a decision for ${named.taskId}'s ${park.kind ?? "unknown"} park` };
164
+ }
165
+ // The argv that runs is rebuilt from the validated command; a preview whose argv says otherwise
166
+ // (a permitted verb in `command`, a different flag in `argv`) is refused before any append.
167
+ let argv;
168
+ try {
169
+ argv = decisionArgv(named, { runId, by: preview.actor });
170
+ }
171
+ catch (e) {
172
+ return { ok: false, preview, refusal: e.message };
173
+ }
174
+ if (argv.length !== preview.argv.length || argv.some((arg, i) => arg !== preview.argv[i])) {
175
+ return { ok: false, preview, refusal: `preview argv [${preview.argv.join(" ")}] does not match the confirmed ${named.verb} [${argv.join(" ")}] — nothing appended` };
176
+ }
177
+ let message;
178
+ try {
179
+ message = await command(argv, cwd);
180
+ }
181
+ catch (e) {
182
+ return { ok: false, preview, refusal: e instanceof Error ? e.message : String(e) };
183
+ }
184
+ let after;
185
+ let afterSourceIndexes;
186
+ try {
187
+ ({ events: after, sourceIndexes: afterSourceIndexes } = readJournalEvents(Journal.open(cwd, runId)));
188
+ }
189
+ catch (e) {
190
+ return { ok: false, preview, refusal: `approve returned but the journal could not be read back: ${e.message}` };
191
+ }
192
+ const beforeLastSourceIndex = beforeSourceIndexes.at(-1) ?? -1;
193
+ const gained = after
194
+ .map((event, i) => ({ line: afterSourceIndexes[i] + 1, sourceIndex: afterSourceIndexes[i], event }))
195
+ .filter(({ sourceIndex }) => sourceIndex > beforeLastSourceIndex)
196
+ .filter(({ event }) => event.event === "task-approved" && event.taskId === named.taskId);
197
+ const appended = gained[0];
198
+ if (gained.length !== 1 || !appended) {
199
+ return { ok: false, preview, refusal: `approve returned "${message}" but the journal gained ${gained.length} task-approved row(s) for ${named.taskId} after #L${beforeLastSourceIndex + 1} — not shown as success` };
200
+ }
201
+ const release = typeof appended.event.data.release === "string" ? appended.event.data.release : undefined;
202
+ const expected = {
203
+ release: releaseForDecision(named.verb, park),
204
+ by: preview.actor,
205
+ reason: named.reason,
206
+ via: "cli",
207
+ gate: named.verb === "waive" ? park.failedGate : named.verb === "uphold" ? "review" : undefined,
208
+ reviewRoundCeiling: named.reviewRounds,
209
+ };
210
+ const actual = {
211
+ release,
212
+ by: appended.event.data.by,
213
+ reason: appended.event.data.reason,
214
+ via: appended.event.data.via,
215
+ gate: appended.event.data.gate,
216
+ reviewRoundCeiling: appended.event.data.reviewRoundCeiling,
217
+ };
218
+ const mismatch = Object.keys(expected)
219
+ .find((field) => actual[field] !== expected[field]);
220
+ if (mismatch !== undefined) {
221
+ return { ok: false, preview, refusal: `the appended decision at #L${appended.line} has ${mismatch} ${String(actual[mismatch] ?? "none")}, expected ${String(expected[mismatch] ?? "none")} for confirmed ${named.verb} — not shown as success` };
222
+ }
223
+ return {
224
+ ok: true, preview, message, appended, release,
225
+ disposition: approvalDispositionForRelease(release), run: approvalRunOwner(cwd, runId),
226
+ };
227
+ }
228
+ /** The receipt: what the file gained, verified, and who enacts it — read after the write. */
229
+ export function decisionReceiptLines(receipt) {
230
+ const { preview } = receipt;
231
+ if (!receipt.ok) {
232
+ return [
233
+ `refused ${receipt.refusal}`,
234
+ `task ${preview.command.taskId} · park ${parkLine(preview.park)}`,
235
+ `argv tickmarkr approve ${preview.argv.map(quoteArg).join(" ")}`,
236
+ "appended nothing",
237
+ ];
238
+ }
239
+ const appendedVerb = receipt.release === "gate-satisfied"
240
+ ? "waive"
241
+ : receipt.release === "review-upheld"
242
+ ? "uphold"
243
+ : receipt.release === "recheck"
244
+ ? "recheck"
245
+ : "approve";
246
+ const appendedPark = receipt.appended.event.data.gate === undefined
247
+ ? preview.park
248
+ : { ...preview.park, failedGate: String(receipt.appended.event.data.gate) };
249
+ return [
250
+ `appended #L${receipt.appended.line} task-approved ${preview.command.taskId} · release ${receipt.release ?? "none"} · disposition ${receipt.disposition} · read back from ${preview.journalFile}`,
251
+ `run ${preview.runId}`,
252
+ `park ${parkLine(preview.park)}`,
253
+ `actor ${String(receipt.appended.event.data.by)} · reason ${typeof receipt.appended.event.data.reason === "string" ? receipt.appended.event.data.reason : "none given"}`,
254
+ `argv tickmarkr approve ${preview.argv.map(quoteArg).join(" ")}`,
255
+ ...consequenceLines(appendedVerb, appendedPark, receipt.release, receipt.disposition),
256
+ ...enactmentLines(receipt.run, receipt.disposition),
257
+ `state ${receipt.run.live ? "pending daemon enactment" : "approved; resume required"} — permission recorded, no work dispatched, nothing green`,
258
+ `command ${receipt.message.split("\n")[0]}`,
259
+ ];
260
+ }
261
+ export const initialDecisionSession = () => ({ menu: null, confirming: null, receipt: null, notice: null });
262
+ /**
263
+ * The reducer never writes and never reads a file; it names commands. `a` opens the menu for the
264
+ * selected park (a diagnostic park opens a menu with no verbs, stating why); ↑↓ move; Enter picks a
265
+ * verb — which OPENS the confirm inset, never confirms; `y` on a confirm names the write; `n`/Esc
266
+ * cancel the deepest layer. Enter on a confirm is inert by construction.
267
+ */
268
+ export function applyDecisionKey(session, event, selected) {
269
+ if (session.confirming !== null) {
270
+ if (event.input === "y")
271
+ return { session: { ...session, confirming: null }, confirm: session.confirming, handled: true };
272
+ if (event.input === "n" || event.key.escape === true)
273
+ return { session: { ...session, confirming: null }, handled: true };
274
+ return { session, handled: true };
275
+ }
276
+ if (session.menu !== null) {
277
+ const { menu } = session;
278
+ if (event.key.escape === true || event.input === "n")
279
+ return { session: { ...session, menu: null }, handled: true };
280
+ if (event.key.upArrow === true || event.key.downArrow === true) {
281
+ const delta = event.key.upArrow === true ? -1 : 1;
282
+ const selection = Math.max(0, Math.min(menu.verbs.length - 1, menu.selection + delta));
283
+ return { session: { ...session, menu: { ...menu, selection } }, handled: true };
284
+ }
285
+ if (event.key.return === true) {
286
+ const verb = menu.verbs[menu.selection];
287
+ if (verb === undefined)
288
+ return { session, handled: true };
289
+ return { session: { ...session, menu: null }, open: { verb, taskId: menu.taskId }, handled: true };
290
+ }
291
+ return { session, handled: true };
292
+ }
293
+ if (event.input === "a") {
294
+ if (selected === undefined)
295
+ return { session: { ...session, notice: "no park selected — nothing to decide" }, handled: true };
296
+ return { session: { ...session, menu: { taskId: selected.taskId, verbs: selected.verbs, selection: 0 }, receipt: null, notice: null }, handled: true };
297
+ }
298
+ return { session, handled: false };
299
+ }
300
+ /** Store the preview a picked verb produced — or its refusal as the notice. */
301
+ export function withDecisionPreview(session, result) {
302
+ return result.ok ? { ...session, confirming: result.preview, notice: null } : { ...session, notice: result.refusal };
303
+ }
304
+ /** Store the receipt of the one write — a refusal is a receipt too, drawn as such. */
305
+ export function withDecisionReceipt(session, receipt) {
306
+ return { ...session, receipt, confirming: null, notice: receipt.ok ? null : receipt.refusal };
307
+ }
308
+ /** The verbs the keybar may advertise for the current layer — nothing the flow cannot honour. */
309
+ export function decisionKeybar(session, selected) {
310
+ if (session.confirming !== null)
311
+ return `y Confirm ${session.confirming.command.verb} · n Cancel`;
312
+ if (session.menu !== null)
313
+ return session.menu.verbs.length === 0 ? "Esc Close" : "↑↓ Choose · Enter Preview · Esc Close";
314
+ return selected === undefined ? "" : "a Actions";
315
+ }
@@ -1,3 +1,4 @@
1
+ export { readOperatorState as deriveOperatorState } from "../../run/operator-state.js";
1
2
  import type { ComponentState, JournalRow, StatusStripItem } from "./components.js";
2
3
  import type { RunViewId } from "./views.js";
3
4
  export declare const SPARKLINE_BUCKET_WINDOW = 12;
@@ -135,4 +136,3 @@ export type TaskState = "completed" | "done" | "failed" | "human" | "pending" |
135
136
  */
136
137
  export declare function runViewRowIdentities(data: RunCockpitData, viewId: RunViewId, filterQuery?: string): readonly string[];
137
138
  export declare function deriveRunCockpitData(source: RunCockpitSource, binaryVersion: string, options?: RunCockpitDeriveOptions): RunCockpitData;
138
- export {};
@@ -1,3 +1,5 @@
1
+ // Consolidated C1/C6 readers use the incremental lifecycle contract; legacy captures keep their API.
2
+ export { readOperatorState as deriveOperatorState } from "../../run/operator-state.js";
1
3
  import { channelKey } from "../../adapters/types.js";
2
4
  import { isPidLive } from "../../run/lock.js";
3
5
  export const SPARKLINE_BUCKET_WINDOW = 12;
@@ -0,0 +1,119 @@
1
+ import { type ReactElement } from "react";
2
+ import type { ChannelCost } from "../../report/cost.js";
3
+ import { type OperatorRecordRow } from "../../report/operator-record.js";
4
+ import type { EvidenceIdentity } from "../../run/operator-state.js";
5
+ import { type JournalEvent } from "../../run/journal.js";
6
+ import { type ComponentState } from "./components.js";
7
+ /** A journal row the shell/store tracks — physical line identity travels with the row. */
8
+ export interface TrackedJournalRow {
9
+ readonly line: number;
10
+ readonly source?: string;
11
+ readonly id?: string;
12
+ readonly event?: JournalEvent;
13
+ readonly raw?: string;
14
+ readonly error?: string;
15
+ }
16
+ /** One row the Journal/Channels/Report tabs are all read from — the true journal `#L`, never a
17
+ * filtered on-screen ordinal, travels with the row for its whole life. */
18
+ export interface EvidenceRow {
19
+ readonly evidence: EvidenceIdentity;
20
+ readonly time: string;
21
+ readonly state: ComponentState;
22
+ /** One-line narration for the Journal list. */
23
+ readonly text: string;
24
+ /** The complete recorded detail (a review verdict can run to hundreds of lines) — never truncated. */
25
+ readonly fullText: string;
26
+ /** Durable artifact locations this row's detail names (e.g. a saved raw-review capture); empty
27
+ * when none were recorded — rendered as "missing", never silently omitted. */
28
+ readonly artifacts: readonly string[];
29
+ readonly taskId?: string;
30
+ readonly gate?: string;
31
+ }
32
+ /** Durable artifact locations named inside a gate/review's recorded detail text. */
33
+ export declare function parseArtifactLocations(details: string): string[];
34
+ /** Pure C2-journal-to-Evidence-rows mapping: the original journal position IS the evidence identity
35
+ * — assigned once here, from the full unfiltered event list, never recomputed from a display slice. */
36
+ export declare function deriveEvidenceJournal(eventsOrRows: readonly (JournalEvent | TrackedJournalRow)[], source?: string): EvidenceRow[];
37
+ /** The most recent review verdict recorded for a task — by original journal position, so a merged
38
+ * task's review opens the row it actually happened on rather than any on-screen index. */
39
+ export declare function selectTaskReview(journal: readonly EvidenceRow[], taskId: string): EvidenceRow | undefined;
40
+ export interface EvidenceViewInput {
41
+ readonly events?: readonly JournalEvent[];
42
+ readonly rows?: readonly TrackedJournalRow[];
43
+ readonly source?: string;
44
+ readonly costs?: readonly ChannelCost[];
45
+ /** Caller-supplied Report-tab text (e.g. from `renderMarkdownRecord`/`textReport`) — kept as a pure
46
+ * input so this leaf never reads config/telemetry itself. */
47
+ readonly reportLines?: readonly string[];
48
+ readonly statsLines?: readonly string[];
49
+ readonly learningPreview?: readonly string[];
50
+ }
51
+ export interface EvidenceViewModel {
52
+ readonly journal: readonly EvidenceRow[];
53
+ readonly channels: readonly OperatorRecordRow[];
54
+ readonly reportLines: readonly string[];
55
+ readonly statsLines: readonly string[];
56
+ readonly learningPreview: readonly string[];
57
+ }
58
+ export declare function deriveEvidenceView(input: EvidenceViewInput): EvidenceViewModel;
59
+ export interface EvidenceExportTarget {
60
+ readonly runId: string;
61
+ /** Every spec path recorded for the run; beside-spec naming only fires when exactly one exists. */
62
+ readonly specPaths: readonly string[];
63
+ readonly specDir?: string;
64
+ }
65
+ /** The default beside-spec destination, or undefined when the source spec is ambiguous or unknown —
66
+ * an ambiguous/missing source never guesses a path; the caller must ask for explicit choice. */
67
+ export declare function defaultExportPath(target: EvidenceExportTarget): string | undefined;
68
+ export interface EvidenceExportPlan {
69
+ readonly path: string;
70
+ /** true ⇒ the caller must obtain explicit overwrite confirmation before writing. */
71
+ readonly exists: boolean;
72
+ }
73
+ export declare function planEvidenceExport(path: string, io: {
74
+ existsSync: (p: string) => boolean;
75
+ }): EvidenceExportPlan;
76
+ export type EvidenceExportOutcome = {
77
+ readonly ok: true;
78
+ readonly path: string;
79
+ readonly bytes: number;
80
+ } | {
81
+ readonly ok: false;
82
+ readonly reason: string;
83
+ };
84
+ /** Writes only once a confirmed path is in hand; reports success — with the real byte count of what
85
+ * landed — only after the write actually completes. A write that throws changes nothing this
86
+ * function reports, and the caller never sees a path/byte count before this returns.
87
+ *
88
+ * Confirmation is enforced HERE, not merely documented: when the destination already exists this
89
+ * refuses (never calling `writeFileSync`) unless `confirmedOverwrite` is explicitly true — a caller
90
+ * cannot silently replace a file just by skipping `planEvidenceExport`. */
91
+ export interface EvidenceExportIO {
92
+ existsSync: (p: string) => boolean;
93
+ writeFileSync: (p: string, data: string, options?: {
94
+ flag?: string;
95
+ }) => void;
96
+ renameSync?: (oldPath: string, newPath: string) => void;
97
+ unlinkSync?: (p: string) => void;
98
+ statSync?: (p: string) => {
99
+ size: number;
100
+ };
101
+ }
102
+ export declare function writeEvidenceExport(path: string, record: string, io: EvidenceExportIO, confirmedOverwrite?: boolean): EvidenceExportOutcome;
103
+ export interface EvidenceViewProps {
104
+ readonly model: EvidenceViewModel;
105
+ readonly width?: number | string;
106
+ readonly focused?: boolean;
107
+ /** C1's navigation target (e.g. Home/Run's "open evidence" action, or `selectTaskReview`'s row for
108
+ * a merged task) — opens directly on this original journal `#L` with Follow off. Changing this
109
+ * prop's identity re-targets the selection without a remount; omitted, the view defaults to the
110
+ * tail with Follow on, as before. */
111
+ readonly focusEvidence?: EvidenceIdentity;
112
+ readonly onSelect?: (evidence: EvidenceIdentity) => void;
113
+ readonly onExport?: () => void;
114
+ }
115
+ /** The exported Evidence body: C1 mounts this against a live EvidenceViewModel. Selection is held by
116
+ * original journal line in local state — never recomputed from the array's current length — so a
117
+ * newly appended row can never steal it while Follow is off; toggling Follow on snaps back to the
118
+ * tail. */
119
+ export declare function EvidenceView({ model, width, focused, focusEvidence, onSelect, onExport }: EvidenceViewProps): ReactElement;