tickmarkr 2.4.2 → 2.5.1
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.
- package/README.md +110 -24
- package/dist/adapters/fake.d.ts +1 -0
- package/dist/adapters/fake.js +2 -1
- package/dist/adapters/opencode.d.ts +1 -0
- package/dist/adapters/opencode.js +4 -1
- package/dist/adapters/types.d.ts +14 -0
- package/dist/adapters/types.js +25 -0
- package/dist/cli/commands/approve.d.ts +42 -0
- package/dist/cli/commands/approve.js +80 -13
- package/dist/cli/commands/doctor.d.ts +234 -0
- package/dist/cli/commands/doctor.js +139 -5
- package/dist/cli/commands/init.d.ts +2 -0
- package/dist/cli/commands/init.js +45 -5
- package/dist/cli/commands/plan.js +58 -6
- package/dist/cli/commands/report.js +18 -45
- package/dist/cli/commands/run.js +3 -0
- package/dist/cli/commands/scope.js +36 -6
- package/dist/cli/commands/stats.d.ts +2 -0
- package/dist/cli/commands/stats.js +42 -23
- package/dist/cli/commands/status.js +20 -4
- package/dist/cli/commands/ui.js +42 -53
- package/dist/cli/commands/unlock.d.ts +1 -1
- package/dist/cli/commands/unlock.js +59 -9
- package/dist/cli/help.d.ts +219 -0
- package/dist/cli/help.js +212 -0
- package/dist/cli/index.d.ts +42 -2
- package/dist/cli/index.js +23 -8
- package/dist/drivers/herdr.d.ts +7 -2
- package/dist/drivers/herdr.js +78 -48
- package/dist/drivers/orca.d.ts +3 -2
- package/dist/drivers/orca.js +43 -4
- package/dist/drivers/subprocess.d.ts +1 -0
- package/dist/drivers/subprocess.js +3 -0
- package/dist/drivers/types.d.ts +14 -0
- package/dist/gates/artifact-manifest.d.ts +50 -0
- package/dist/gates/artifact-manifest.js +23 -0
- package/dist/gates/llm.d.ts +5 -0
- package/dist/gates/llm.js +188 -3
- package/dist/gates/review.d.ts +6 -3
- package/dist/gates/review.js +79 -33
- package/dist/gates/run-gates.d.ts +3 -0
- package/dist/gates/run-gates.js +14 -4
- package/dist/plan/scope.d.ts +25 -0
- package/dist/plan/scope.js +92 -12
- package/dist/report/operator-record.d.ts +49 -0
- package/dist/report/operator-record.js +137 -0
- package/dist/run/daemon.d.ts +5 -5
- package/dist/run/daemon.js +166 -44
- package/dist/run/lock.d.ts +59 -2
- package/dist/run/lock.js +184 -26
- package/dist/run/operator-state.d.ts +86 -0
- package/dist/run/operator-state.js +165 -0
- package/dist/run/supervision.d.ts +32 -0
- package/dist/run/supervision.js +138 -17
- package/dist/tui/cockpit/capture.d.ts +19 -0
- package/dist/tui/cockpit/capture.js +89 -1
- package/dist/tui/cockpit/components.d.ts +15 -1
- package/dist/tui/cockpit/components.js +79 -9
- package/dist/tui/cockpit/decision-actions.d.ts +147 -0
- package/dist/tui/cockpit/decision-actions.js +315 -0
- package/dist/tui/cockpit/derive.d.ts +1 -1
- package/dist/tui/cockpit/derive.js +2 -0
- package/dist/tui/cockpit/evidence-view.d.ts +119 -0
- package/dist/tui/cockpit/evidence-view.js +210 -0
- package/dist/tui/cockpit/home-view.d.ts +88 -0
- package/dist/tui/cockpit/home-view.js +240 -0
- package/dist/tui/cockpit/keys.d.ts +125 -0
- package/dist/tui/cockpit/keys.js +31 -0
- package/dist/tui/cockpit/layout.d.ts +14 -0
- package/dist/tui/cockpit/layout.js +15 -0
- package/dist/tui/cockpit/live-runtime.d.ts +46 -0
- package/dist/tui/cockpit/live-runtime.js +683 -0
- package/dist/tui/cockpit/live-store.d.ts +289 -0
- package/dist/tui/cockpit/live-store.js +308 -0
- package/dist/tui/cockpit/live.d.ts +21 -1
- package/dist/tui/cockpit/live.js +12 -1
- package/dist/tui/cockpit/run-view.d.ts +100 -0
- package/dist/tui/cockpit/run-view.js +202 -0
- package/dist/tui/cockpit/shell.d.ts +50 -0
- package/dist/tui/cockpit/shell.js +74 -0
- package/dist/tui/cockpit/theme.d.ts +27 -0
- package/dist/tui/cockpit/theme.js +21 -0
- package/dist/tui/ink/init-app.d.ts +4 -0
- package/dist/tui/ink/init-app.js +18 -7
- package/package.json +1 -1
- package/skills/tickmarkr-auto/SKILL.md +10 -1
- package/skills/tickmarkr-loop/SKILL.md +72 -1
- package/skills/tickmarkr-overseer/SKILL.md +12 -0
- package/skills/tickmarkr-overseer/scripts/watch-context.sh +13 -2
|
@@ -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
|
+
}
|
|
@@ -1,12 +1,16 @@
|
|
|
1
1
|
import type { InitConfigOverlay } from "../../config/config.js";
|
|
2
2
|
type Driver = NonNullable<InitConfigOverlay["driver"]>;
|
|
3
3
|
type VisibilityLlm = NonNullable<NonNullable<InitConfigOverlay["visibility"]>["llm"]>;
|
|
4
|
+
export type DetectedHost = "herdr" | "orca" | "subprocess";
|
|
5
|
+
export declare function detectedHost(env?: NodeJS.ProcessEnv): DetectedHost;
|
|
6
|
+
export declare function driverDescription(driver: Driver, host?: DetectedHost): string;
|
|
4
7
|
export type InitWizardFields = {
|
|
5
8
|
driver: Driver;
|
|
6
9
|
concurrency: number;
|
|
7
10
|
visibilityLlm: VisibilityLlm;
|
|
8
11
|
offerSkills: boolean;
|
|
9
12
|
skillsDefault: boolean;
|
|
13
|
+
detectedHost?: DetectedHost;
|
|
10
14
|
};
|
|
11
15
|
export type InitWizardResult = {
|
|
12
16
|
kind: "continue";
|
package/dist/tui/ink/init-app.js
CHANGED
|
@@ -1,23 +1,33 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import { Box, render, Text, useApp, useInput } from "ink";
|
|
3
3
|
import { useRef, useState } from "react";
|
|
4
|
+
import { orcaHostDetected } from "../../drivers/index.js";
|
|
4
5
|
import { ToggleMark } from "./components.js";
|
|
5
6
|
import { clip, INK, inkInput, inkOutput, KeyBar, padCell, Pointer } from "./frame.js";
|
|
6
7
|
const DRIVERS = ["auto", "herdr", "subprocess", "orca"];
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
8
|
+
export function detectedHost(env = process.env) {
|
|
9
|
+
if (env.HERDR_ENV === "1")
|
|
10
|
+
return "herdr";
|
|
11
|
+
if (orcaHostDetected(env))
|
|
12
|
+
return "orca";
|
|
13
|
+
return "subprocess";
|
|
14
|
+
}
|
|
10
15
|
const DRIVER_DESC = {
|
|
11
|
-
auto: "auto
|
|
16
|
+
auto: "auto — resolves to subprocess here",
|
|
12
17
|
herdr: "herdr: every worker runs in a visible pane you can watch and unblock",
|
|
13
18
|
subprocess: "subprocess: headless child processes — no cockpit, same fail-closed gates",
|
|
14
19
|
orca: "orca: visible terminals in the Orca app — picked by auto inside one; name it elsewhere",
|
|
15
20
|
};
|
|
21
|
+
export function driverDescription(driver, host = detectedHost()) {
|
|
22
|
+
if (driver === "auto")
|
|
23
|
+
return `auto — resolves to ${host} here`;
|
|
24
|
+
return DRIVER_DESC[driver];
|
|
25
|
+
}
|
|
16
26
|
const VISIBILITY = ["pane", "headless"];
|
|
17
27
|
// Three fields, one toggle, one action — a descriptor array, deliberately not a forms framework.
|
|
18
|
-
function buildRows(offerSkills, driver) {
|
|
28
|
+
function buildRows(offerSkills, driver, host) {
|
|
19
29
|
const rows = [
|
|
20
|
-
{ id: "driver", section: "Run", label: "Driver", desc:
|
|
30
|
+
{ id: "driver", section: "Run", label: "Driver", desc: driverDescription(driver, host) },
|
|
21
31
|
{ id: "concurrency", section: "Run", label: "Concurrency", desc: "parallel task batteries per run — min 1; an empty or zero entry reverts on leave" },
|
|
22
32
|
{
|
|
23
33
|
id: "visibility",
|
|
@@ -45,6 +55,7 @@ function buildRows(offerSkills, driver) {
|
|
|
45
55
|
}
|
|
46
56
|
export function InitWizardApp({ fields, frameColumns = 74 }) {
|
|
47
57
|
const { exit } = useApp();
|
|
58
|
+
const host = fields.detectedHost ?? detectedHost();
|
|
48
59
|
const [cursor, setCursor] = useState(0);
|
|
49
60
|
const cursorRef = useRef(0);
|
|
50
61
|
const driverRef = useRef(fields.driver);
|
|
@@ -56,7 +67,7 @@ export function InitWizardApp({ fields, frameColumns = 74 }) {
|
|
|
56
67
|
const typedRef = useRef(false);
|
|
57
68
|
const doneRef = useRef(false);
|
|
58
69
|
// Built per render so the description row follows the driver the cursor last cycled to.
|
|
59
|
-
const rows = buildRows(fields.offerSkills, driverRef.current);
|
|
70
|
+
const rows = buildRows(fields.offerSkills, driverRef.current, host);
|
|
60
71
|
const [, setRevision] = useState(0);
|
|
61
72
|
const bump = () => setRevision((n) => n + 1);
|
|
62
73
|
// Leaving the row (or pressing Enter on it) is the commit point: a parse ≥ 1 becomes the new
|
package/package.json
CHANGED
|
@@ -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** —
|
|
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.
|