tickmarkr 1.72.0 → 1.74.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 (52) hide show
  1. package/dist/cli/commands/approve.js +21 -3
  2. package/dist/cli/commands/resume.js +10 -2
  3. package/dist/cli/commands/status.d.ts +2 -0
  4. package/dist/cli/commands/status.js +260 -41
  5. package/dist/cli/commands/ui.d.ts +6 -2
  6. package/dist/cli/commands/ui.js +2 -4
  7. package/dist/config/config.d.ts +4 -0
  8. package/dist/config/config.js +6 -1
  9. package/dist/drivers/herdr.d.ts +5 -0
  10. package/dist/drivers/herdr.js +100 -10
  11. package/dist/drivers/subprocess.d.ts +3 -3
  12. package/dist/drivers/subprocess.js +10 -5
  13. package/dist/gates/acceptance.js +19 -1
  14. package/dist/gates/llm.d.ts +4 -0
  15. package/dist/gates/llm.js +16 -3
  16. package/dist/gates/run-gates.d.ts +1 -1
  17. package/dist/gates/run-gates.js +25 -3
  18. package/dist/route/preference.d.ts +2 -1
  19. package/dist/route/preference.js +8 -2
  20. package/dist/run/daemon.d.ts +1 -0
  21. package/dist/run/daemon.js +139 -4
  22. package/dist/run/git.d.ts +2 -0
  23. package/dist/run/git.js +55 -0
  24. package/dist/run/journal.d.ts +21 -1
  25. package/dist/run/journal.js +83 -5
  26. package/dist/tui/ink/components.d.ts +18 -0
  27. package/dist/tui/ink/components.js +30 -2
  28. package/dist/tui/ink/studio-app.d.ts +59 -0
  29. package/dist/tui/ink/studio-app.js +320 -0
  30. package/package.json +1 -1
  31. package/dist/tui/app.d.ts +0 -72
  32. package/dist/tui/app.js +0 -334
  33. package/dist/tui/engine.d.ts +0 -52
  34. package/dist/tui/engine.js +0 -92
  35. package/dist/tui/frame.d.ts +0 -22
  36. package/dist/tui/frame.js +0 -56
  37. package/dist/tui/input.d.ts +0 -41
  38. package/dist/tui/input.js +0 -100
  39. package/dist/tui/views/consult-dossier.d.ts +0 -49
  40. package/dist/tui/views/consult-dossier.js +0 -169
  41. package/dist/tui/views/diff-modal.d.ts +0 -9
  42. package/dist/tui/views/diff-modal.js +0 -19
  43. package/dist/tui/views/fleet-view.d.ts +0 -91
  44. package/dist/tui/views/fleet-view.js +0 -337
  45. package/dist/tui/views/preview-view.d.ts +0 -7
  46. package/dist/tui/views/preview-view.js +0 -80
  47. package/dist/tui/views/profile-view.d.ts +0 -15
  48. package/dist/tui/views/profile-view.js +0 -93
  49. package/dist/tui/views/routing-view.d.ts +0 -48
  50. package/dist/tui/views/routing-view.js +0 -234
  51. package/dist/tui/views/runs-view.d.ts +0 -70
  52. package/dist/tui/views/runs-view.js +0 -387
@@ -0,0 +1,320 @@
1
+ import { jsx as _jsx } from "react/jsx-runtime";
2
+ import { render, useApp, useInput } from "ink";
3
+ import { useEffect, useRef, useState } from "react";
4
+ import { allAdapters, doctorAgeMs, readDoctor } from "../../adapters/registry.js";
5
+ import { fleetEditableFromConfig, loadConfig } from "../../config/config.js";
6
+ import { loadGraph } from "../../graph/graph.js";
7
+ import { Journal, readAllTelemetry, RUNS_WINDOW, } from "../../run/journal.js";
8
+ import { buildSaveProposal, confirmSave } from "../save.js";
9
+ import { FleetStaging } from "../staging.js";
10
+ import { DiffConfirmationScreen, StudioScreen, } from "./components.js";
11
+ const STATUS = {
12
+ "task-dispatch": "running", "task-done": "done", "task-failed": "failed",
13
+ "task-human": "human", "task-approved": "pending",
14
+ };
15
+ const HELP = [
16
+ "Key bindings",
17
+ "1-5 switch view",
18
+ "tab cycle views",
19
+ "? show this help",
20
+ "esc / q quit",
21
+ "u revert staged changes",
22
+ "s save staged changes",
23
+ ];
24
+ function loadFleetData(repoRoot) {
25
+ try {
26
+ const cfg = loadConfig(repoRoot);
27
+ const health = readDoctor(repoRoot);
28
+ if (!health)
29
+ return undefined;
30
+ const editable = fleetEditableFromConfig(cfg);
31
+ const adapters = allAdapters().flatMap((adapter) => {
32
+ if (!health[adapter.id]?.installed)
33
+ return [];
34
+ const models = adapter.channels(cfg).map((channel) => ({
35
+ model: channel.model,
36
+ tier: channel.tier,
37
+ channel: channel.channel,
38
+ }));
39
+ return models.length ? [{ adapter: adapter.id, models }] : [];
40
+ });
41
+ return {
42
+ adapters,
43
+ health,
44
+ doctorAgeMs: doctorAgeMs(repoRoot),
45
+ denyAdapters: editable.denyAdapters,
46
+ denyModels: editable.denyModels,
47
+ telemetry: readAllTelemetry(repoRoot, RUNS_WINDOW),
48
+ };
49
+ }
50
+ catch {
51
+ // Best-effort read surface: line-mode commands remain the diagnostic path when config is unavailable.
52
+ return undefined;
53
+ }
54
+ }
55
+ function emptyStaging() {
56
+ return new FleetStaging({
57
+ denyAdapters: [],
58
+ denyModels: [],
59
+ tiers: {},
60
+ map: {},
61
+ floors: {},
62
+ });
63
+ }
64
+ function loadStaging(repoRoot) {
65
+ try {
66
+ return new FleetStaging(fleetEditableFromConfig(loadConfig(repoRoot)));
67
+ }
68
+ catch {
69
+ return emptyStaging();
70
+ }
71
+ }
72
+ const componentView = (id, label, line) => ({ id, label, render: () => [line] });
73
+ const createView = (id, label) => (..._) => componentView(id, label, `${label} view`);
74
+ const createFleetView = createView("fleet", "Fleet"), createRoutingView = createView("routing", "Routing"), createPreviewView = createView("preview", "Preview"), createProfileView = createView("profile", "Profile"), createRunsView = createView("runs", "Runs");
75
+ function defaultViews(repoRoot, staging) {
76
+ return [
77
+ createFleetView(loadFleetData(repoRoot), staging),
78
+ createRoutingView({ repoRoot, staging }),
79
+ createPreviewView({ cwd: repoRoot }),
80
+ createProfileView({ repoRoot }),
81
+ createRunsView(undefined, { repoRoot }),
82
+ ];
83
+ }
84
+ const emptyGraph = () => ({ version: 1, spec: { source: "prd", paths: [], hash: "" }, tasks: [] });
85
+ function loadRunsData(repoRoot) {
86
+ const runId = Journal.latestRunId(repoRoot, { withJournal: true }) ?? undefined;
87
+ let graph = emptyGraph();
88
+ try {
89
+ graph = loadGraph(repoRoot);
90
+ }
91
+ catch { /* no graph loaded */ }
92
+ return { runId, events: runId ? Journal.open(repoRoot, runId).read() : [], graph };
93
+ }
94
+ export function foldConsultDossier(data, taskId) {
95
+ let prompt = 0;
96
+ const verdicts = data.events.flatMap((event) => {
97
+ if (event.taskId !== taskId || event.event !== "consult-verdict")
98
+ return [];
99
+ const action = event.data.action;
100
+ if (!["retry", "reroute", "decompose", "human"].includes(action))
101
+ return [];
102
+ return [{
103
+ action,
104
+ reason: typeof event.data.reason === "string" ? event.data.reason : undefined,
105
+ prompt: data.prompts?.[taskId]?.[prompt++],
106
+ }];
107
+ });
108
+ return { taskId, verdicts };
109
+ }
110
+ export function buildRunsCockpitTasks(data, nowMs) {
111
+ return data.graph.tasks.map((task) => {
112
+ const events = data.events.filter((event) => event.taskId === task.id);
113
+ const state = [...events].reverse().find((event) => STATUS[event.event]);
114
+ const status = STATUS[state?.event ?? ""] ?? task.status;
115
+ const phaseStart = [...events].reverse().find((event) => event.event === "phase-start");
116
+ const start = phaseStart ?? (state?.event === "task-dispatch" ? state : undefined);
117
+ const phase = typeof phaseStart?.data.phase === "string" ? phaseStart.data.phase : undefined;
118
+ const running = status === "running" && start !== undefined;
119
+ const elapsedMs = running ? Math.max(0, nowMs - Date.parse(start.ts)) : 0;
120
+ return {
121
+ id: task.id, status,
122
+ ...(running ? { phase: phase ?? "worker",
123
+ elapsed: `${Math.floor(elapsedMs / 1000)}s` } : {}),
124
+ consultCount: foldConsultDossier(data, task.id).verdicts.length,
125
+ };
126
+ });
127
+ }
128
+ function dossierLines(data) {
129
+ if (!data)
130
+ return ["d opens consult dossier"];
131
+ if (!data.verdicts.length)
132
+ return [`no consult verdicts recorded for ${data.taskId}`];
133
+ return [`Consult dossier — ${data.taskId}`, "d back", ...data.verdicts.flatMap((verdict, index) => [
134
+ `❯ ${index + 1}. ${verdict.action}${verdict.reason ? ` — ${verdict.reason}` : ""}`,
135
+ ` prompt ${index + 1}`,
136
+ ...(verdict.prompt ?? "(prompt unavailable)").split("\n")
137
+ .map((line) => ` ${line.replace(/^#{1,6}\s+/, "")}`),
138
+ ])];
139
+ }
140
+ function runsLines(runId, tasks, cursor) {
141
+ if (!runId)
142
+ return ["Runs cockpit", "no run loaded"];
143
+ return [`Runs cockpit — ${runId}`, "d consult dossier", ...tasks.flatMap((task, index) => [
144
+ `${index === cursor ? "❯ " : " "}${task.id} — ${task.status}`,
145
+ ` ${task.phase ? `${task.phase} · ${task.elapsed}` : "waiting"}${task.consultCount ? ` · ${task.consultCount} consult verdict` : ""}`,
146
+ ])];
147
+ }
148
+ export function StudioApp({ views: suppliedViews, repoRoot = process.cwd(), globalDir, staging: suppliedStaging, runsData: suppliedRunsData, clock = Date.now, columns = 80, rows = 24, }) {
149
+ const { exit } = useApp();
150
+ const stagingRef = useRef(undefined);
151
+ stagingRef.current ??= suppliedStaging ?? loadStaging(repoRoot);
152
+ const staging = stagingRef.current;
153
+ const viewsRef = useRef(undefined);
154
+ viewsRef.current ??= suppliedViews ?? defaultViews(repoRoot, staging);
155
+ const views = viewsRef.current;
156
+ const [active, setActive] = useState(0);
157
+ const [help, setHelp] = useState(false);
158
+ const [saveProposal, setSaveProposal] = useState(null);
159
+ const [notice, setNotice] = useState(null);
160
+ const [runsData, setRunsData] = useState(() => suppliedRunsData ?? loadRunsData(repoRoot));
161
+ const [nowMs, setNowMs] = useState(clock);
162
+ const [dossierOpen, setDossierOpen] = useState(false);
163
+ const [, repaint] = useState(0);
164
+ const nativeRuns = suppliedViews === undefined && views[active]?.id === "runs";
165
+ const runTasks = buildRunsCockpitTasks(runsData, nowMs);
166
+ const runsCursor = Math.max(0, runTasks.findIndex((task) => task.consultCount > 0));
167
+ const selectedRunTask = runTasks[runsCursor];
168
+ const dossier = selectedRunTask ? foldConsultDossier(runsData, selectedRunTask.id) : undefined;
169
+ useEffect(() => {
170
+ if (!nativeRuns)
171
+ return;
172
+ const timer = setInterval(() => {
173
+ setNowMs(clock());
174
+ if (!suppliedRunsData)
175
+ setRunsData(loadRunsData(repoRoot));
176
+ }, 250);
177
+ return () => clearInterval(timer);
178
+ }, [clock, nativeRuns, repoRoot, suppliedRunsData]);
179
+ useInput((input, key) => {
180
+ if (key.escape) {
181
+ if (help)
182
+ setHelp(false);
183
+ else if (saveProposal)
184
+ setSaveProposal(null);
185
+ else
186
+ exit();
187
+ return;
188
+ }
189
+ if (key.ctrl && input === "c") {
190
+ exit();
191
+ return;
192
+ }
193
+ if (saveProposal) {
194
+ if (input === "n") {
195
+ setSaveProposal(null);
196
+ }
197
+ else if (input === "t") {
198
+ const target = saveProposal.target === "repo" ? "global" : "repo";
199
+ setSaveProposal(buildSaveProposal({
200
+ repoRoot,
201
+ loaded: staging.loadedState,
202
+ staged: staging.current,
203
+ target,
204
+ globalDir,
205
+ }));
206
+ }
207
+ else if (input === "y") {
208
+ const result = confirmSave({
209
+ repoRoot,
210
+ loaded: staging.loadedState,
211
+ staged: staging.current,
212
+ target: saveProposal.target,
213
+ globalDir,
214
+ });
215
+ if (result.kind === "written") {
216
+ const nextStaging = new FleetStaging(staging.current);
217
+ stagingRef.current = nextStaging;
218
+ if (!suppliedViews)
219
+ viewsRef.current = defaultViews(repoRoot, nextStaging);
220
+ setNotice(`wrote ${result.path}`);
221
+ }
222
+ else if (result.kind === "refused") {
223
+ setNotice(`save refused: ${result.reason}`);
224
+ }
225
+ setSaveProposal(null);
226
+ }
227
+ return;
228
+ }
229
+ if (input === "q") {
230
+ if (!help)
231
+ exit();
232
+ return;
233
+ }
234
+ if (input === "?") {
235
+ setHelp(true);
236
+ return;
237
+ }
238
+ if (help)
239
+ return;
240
+ if (nativeRuns && input === "d" && (dossierOpen || selectedRunTask)) {
241
+ setDossierOpen((open) => !open);
242
+ return;
243
+ }
244
+ if (input === "s") {
245
+ if (!staging.isDirty) {
246
+ setNotice("no staged changes");
247
+ }
248
+ else {
249
+ setNotice(null);
250
+ setSaveProposal(buildSaveProposal({
251
+ repoRoot,
252
+ loaded: staging.loadedState,
253
+ staged: staging.current,
254
+ target: "repo",
255
+ globalDir,
256
+ }));
257
+ }
258
+ return;
259
+ }
260
+ if (input === "u") {
261
+ staging.revert();
262
+ setNotice(staging.isDirty ? null : "no staged changes");
263
+ repaint((revision) => revision + 1);
264
+ return;
265
+ }
266
+ if (key.tab) {
267
+ setActive((index) => (index + 1) % views.length);
268
+ setNotice(null);
269
+ setDossierOpen(false);
270
+ return;
271
+ }
272
+ if (/^[1-9]$/.test(input)) {
273
+ const index = Number(input) - 1;
274
+ if (index < views.length) {
275
+ setActive(index);
276
+ setNotice(null);
277
+ setDossierOpen(false);
278
+ }
279
+ return;
280
+ }
281
+ const name = key.upArrow ? "up" : key.downArrow ? "down" : null;
282
+ if (name) {
283
+ views[active]?.key?.(name);
284
+ repaint((revision) => revision + 1);
285
+ }
286
+ });
287
+ const view = views[active];
288
+ const lines = help
289
+ ? HELP
290
+ : nativeRuns
291
+ ? dossierOpen
292
+ ? dossierLines(dossier)
293
+ : runsLines(runsData.runId, runTasks, runsCursor)
294
+ : [
295
+ ...view.render({ cols: columns, rows: Math.max(rows - 2, 1) }),
296
+ ...(notice ? [notice] : []),
297
+ ];
298
+ const count = staging.changeCount;
299
+ const status = staging.isDirty
300
+ ? `● ${count} staged change${count === 1 ? "" : "s"}`
301
+ : "no staged changes";
302
+ return (_jsx(StudioScreen, { tabs: views.map((candidate) => candidate.label), active: active, lines: lines, status: status, children: saveProposal
303
+ ? (_jsx(DiffConfirmationScreen, { target: saveProposal.target, targetPath: saveProposal.targetPath, diff: saveProposal.diff, liveRun: saveProposal.liveRun }))
304
+ : undefined }));
305
+ }
306
+ export async function runStudioInk({ input, output, views, repoRoot, globalDir, staging, runsData, clock, debug = false, }) {
307
+ const app = render(_jsx(StudioApp, { views: views, repoRoot: repoRoot, globalDir: globalDir, staging: staging, runsData: runsData, clock: clock, columns: output.columns, rows: output.rows }), {
308
+ stdin: input,
309
+ stdout: output,
310
+ exitOnCtrlC: false,
311
+ patchConsole: false,
312
+ debug,
313
+ });
314
+ try {
315
+ await app.waitUntilExit();
316
+ }
317
+ finally {
318
+ app.unmount();
319
+ }
320
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tickmarkr",
3
- "version": "1.72.0",
3
+ "version": "1.74.0",
4
4
  "description": "Spec in, verified work out.",
5
5
  "type": "module",
6
6
  "license": "MIT",
package/dist/tui/app.d.ts DELETED
@@ -1,72 +0,0 @@
1
- import { type OutputStream } from "../tui/engine.js";
2
- import type { InputStream } from "../tui/input.js";
3
- import type { FleetEditable } from "../config/config.js";
4
- import { type FleetEdit } from "./staging.js";
5
- export type View = {
6
- id: string;
7
- label: string;
8
- render(props: {
9
- cols: number;
10
- rows: number;
11
- }): string[];
12
- /** Optional per-view key handler (e.g. up/down cursor movement). */
13
- key?(name: string): void;
14
- };
15
- export type StudioOptions = {
16
- input: InputStream;
17
- output: OutputStream;
18
- /** Loaded fleet state the buffer wraps. Defaults to an empty editable for the no-arg shell path. */
19
- loaded?: FleetEditable;
20
- /** Repository root used for overlay paths and live-run detection. Defaults to cwd. */
21
- repoRoot?: string;
22
- /** Global config directory override. */
23
- globalDir?: string;
24
- };
25
- export declare class StudioApp {
26
- private engine;
27
- private views;
28
- private active;
29
- private showingHelp;
30
- private confirmingQuit;
31
- private showingSave;
32
- private saveTarget;
33
- private saveProposal;
34
- private notice;
35
- private staging;
36
- private repoRoot;
37
- private globalDir;
38
- private _lines;
39
- private resolveExit;
40
- readonly exited: Promise<void>;
41
- constructor(opts: StudioOptions);
42
- /** Current line model last rendered or prepared. */
43
- get lines(): string[];
44
- /** Labels of every registered view, in order. */
45
- get viewLabels(): string[];
46
- /** Enter the alternate screen and start routing input. */
47
- start(): void;
48
- /** Restore the terminal and release the input stream. */
49
- stop(): void;
50
- /** Apply an edit to the staging buffer and repaint. Intended as the test seam and future edit hook. */
51
- stageEdit(edit: FleetEdit): void;
52
- private get isModalOpen();
53
- private bindKeys;
54
- private handleEscape;
55
- private handleQuit;
56
- private handleSaveOpen;
57
- private handleSaveToggle;
58
- private handleConfirm;
59
- private handleCancel;
60
- private executeSave;
61
- private closeSave;
62
- private handleRevert;
63
- private setView;
64
- private forwardViewKey;
65
- private showNotice;
66
- private paint;
67
- private buildFrame;
68
- private renderTabBar;
69
- private renderStatusBar;
70
- private renderHelp;
71
- private renderQuitConfirm;
72
- }