tickmarkr 1.85.0 → 1.87.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 (78) hide show
  1. package/README.md +4 -2
  2. package/dist/adapters/catalog-remote.d.ts +64 -0
  3. package/dist/adapters/catalog-remote.js +287 -0
  4. package/dist/adapters/catalog.d.ts +108 -0
  5. package/dist/adapters/catalog.js +189 -0
  6. package/dist/adapters/claude-code.js +5 -3
  7. package/dist/adapters/fake.js +33 -4
  8. package/dist/adapters/model-lints.d.ts +25 -5
  9. package/dist/adapters/model-lints.js +184 -50
  10. package/dist/adapters/model-windows.d.ts +31 -0
  11. package/dist/adapters/model-windows.js +69 -0
  12. package/dist/adapters/prompt.d.ts +5 -1
  13. package/dist/adapters/prompt.js +13 -4
  14. package/dist/adapters/registry.d.ts +34 -27
  15. package/dist/adapters/registry.js +215 -112
  16. package/dist/adapters/types.js +15 -3
  17. package/dist/brand.d.ts +5 -1
  18. package/dist/brand.js +18 -2
  19. package/dist/cli/commands/doctor.d.ts +3 -0
  20. package/dist/cli/commands/doctor.js +43 -21
  21. package/dist/cli/commands/fleet.d.ts +7 -0
  22. package/dist/cli/commands/fleet.js +94 -74
  23. package/dist/cli/commands/init.js +118 -5
  24. package/dist/cli/commands/plan.js +11 -1
  25. package/dist/cli/commands/resume.js +7 -1
  26. package/dist/cli/commands/status.js +44 -18
  27. package/dist/compile/gsd.d.ts +2 -1
  28. package/dist/compile/gsd.js +68 -2
  29. package/dist/compile/native.d.ts +14 -0
  30. package/dist/compile/native.js +168 -12
  31. package/dist/config/config.d.ts +20 -5
  32. package/dist/config/config.js +96 -64
  33. package/dist/config/fleet-overlay.d.ts +25 -20
  34. package/dist/config/fleet-overlay.js +195 -77
  35. package/dist/config/fleet-why.d.ts +23 -0
  36. package/dist/config/fleet-why.js +42 -0
  37. package/dist/drivers/herdr.d.ts +1 -0
  38. package/dist/drivers/herdr.js +70 -19
  39. package/dist/gates/acceptance.js +7 -2
  40. package/dist/gates/llm.d.ts +0 -1
  41. package/dist/gates/llm.js +5 -30
  42. package/dist/gates/review.d.ts +2 -1
  43. package/dist/gates/review.js +9 -7
  44. package/dist/gates/run-gates.d.ts +1 -0
  45. package/dist/gates/run-gates.js +21 -2
  46. package/dist/gates/verdict-cause.d.ts +4 -0
  47. package/dist/gates/verdict-cause.js +63 -0
  48. package/dist/graph/schema.d.ts +6 -0
  49. package/dist/graph/schema.js +8 -5
  50. package/dist/route/preference.d.ts +1 -1
  51. package/dist/route/preference.js +8 -1
  52. package/dist/route/router.d.ts +0 -5
  53. package/dist/route/router.js +16 -20
  54. package/dist/run/consult.d.ts +6 -0
  55. package/dist/run/consult.js +49 -26
  56. package/dist/run/daemon.js +81 -20
  57. package/dist/run/journal.js +87 -7
  58. package/dist/tui/cockpit/capture.d.ts +12 -0
  59. package/dist/tui/cockpit/capture.js +37 -1
  60. package/dist/tui/cockpit/components.js +8 -8
  61. package/dist/tui/cockpit/theme.d.ts +32 -26
  62. package/dist/tui/cockpit/theme.js +11 -5
  63. package/dist/tui/ink/components.d.ts +0 -15
  64. package/dist/tui/ink/components.js +0 -17
  65. package/dist/tui/ink/fleet-app.d.ts +4 -1
  66. package/dist/tui/ink/fleet-app.js +134 -13
  67. package/fixtures/gateway-models.json +1 -0
  68. package/fixtures/sample.native.md +1 -1
  69. package/package.json +1 -1
  70. package/skills/tickmarkr-overseer/SKILL.md +464 -34
  71. package/skills/tickmarkr-overseer/scripts/watch-artifacts.sh +86 -0
  72. package/skills/tickmarkr-overseer/scripts/watch-panes.sh +1 -1
  73. package/dist/tui/ink/studio-app.d.ts +0 -59
  74. package/dist/tui/ink/studio-app.js +0 -320
  75. package/dist/tui/save.d.ts +0 -38
  76. package/dist/tui/save.js +0 -96
  77. package/dist/tui/staging.d.ts +0 -29
  78. package/dist/tui/staging.js +0 -78
@@ -0,0 +1,86 @@
1
+ #!/bin/bash
2
+ # Wake the overseer when spawned seats have actually DELIVERED.
3
+ #
4
+ # watch-artifacts.sh <marker> <cap-seconds> <poll-seconds> <file>...
5
+ #
6
+ # Completion is the ARTIFACT plus its TERMINAL MARKER — never an agent's `done`, which is turn end and
7
+ # fires the moment a seat finishes acknowledging you. Never file existence alone either: a seat killed
8
+ # mid-write leaves a large, plausible, truncated file. Size proves it started; the marker proves it
9
+ # finished. Measured 2026-08-06: one 10KB verdict was on disk while its seat still read `working`, and two
10
+ # 30KB/12.8KB verdicts sat COMPLETE for minutes with nothing watching, found only because the operator
11
+ # asked (OBS-369).
12
+ #
13
+ # ARM THIS IN THE SAME CALL THAT SPAWNS THE SEAT. A watcher armed later leaves an unwatched gap exactly as
14
+ # wide as however long you stay busy — and you will be busy, because you just spawned work.
15
+ #
16
+ # Prints one wake reason and EXITS. Re-arm after every wake.
17
+ #
18
+ # TKR_CLOSE_PANES="w1:p1,w1:p2" closes those panes when every artifact completes — the answer to panes
19
+ # accumulating because nobody was watching for "this seat is finished". It fires ONLY on completion,
20
+ # never on timeout. Operator-observed 2026-08-06: five consult panes in one tab left each 14 columns
21
+ # wide and unreadable, because closing was a step someone had to remember.
22
+ set -u
23
+ # macOS ships bash 3.2, where `set -u` makes "${arr[@]}" on an EMPTY array a fatal unbound-variable
24
+ # error. Every expansion below therefore uses the ${arr[@]+"${arr[@]}"} guard. Caught by the timeout
25
+ # control, not by the completion one: the happy path was green while the path that runs on almost every
26
+ # arm crashed before printing its reason — a watcher that dies without a wake reason is the exact failure
27
+ # this script exists to prevent.
28
+
29
+ MARKER="${1:?usage: watch-artifacts.sh <marker> <cap-seconds> <poll-seconds> <file>...}"
30
+ CAP="${2:?}"
31
+ POLL="${3:?}"
32
+ shift 3
33
+ [ "$#" -gt 0 ] || { echo "watch-artifacts: no files given" >&2; exit 2; }
34
+
35
+ # Cap BELOW the host's background-job kill so every arm ends by PRINTING something. A job killed at the
36
+ # limit carries no wake reason and is indistinguishable from a real wake until you read the output —
37
+ # OBS-325, re-earned by a third watcher that had not been capped because the fix was applied only to the
38
+ # two that happened to be in view at the time. Class, not instance.
39
+ END=$((SECONDS + CAP))
40
+
41
+ # A file is DONE when the marker appears in its last few lines. Anchored to the tail on purpose: a report
42
+ # that merely *mentions* its own marker mid-body has not finished, and grepping the whole file would call
43
+ # that done. This brief tells seats to end the file with the marker, so the tail is where it must be.
44
+ done_file() {
45
+ [ -s "$1" ] || return 1
46
+ tail -5 "$1" 2>/dev/null | grep -qF -- "$MARKER"
47
+ }
48
+
49
+ while :; do
50
+ pending=()
51
+ ready=()
52
+ for f in "$@"; do
53
+ if done_file "$f"; then ready+=("$f"); else pending+=("$f"); fi
54
+ done
55
+
56
+ if [ "${#pending[@]}" -eq 0 ]; then
57
+ echo "WAKE: all ${#ready[@]} artifact(s) complete with marker '$MARKER'"
58
+ for f in ${ready[@]+"${ready[@]}"}; do echo " READY $(wc -c <"$f" | tr -d ' ') bytes $f"; done
59
+ # A seat whose artifact is COMPLETE has nothing left to give: the report is the archive, the pane is
60
+ # not. Closing here is safe precisely because the marker — not `done`, not a size — is the trigger,
61
+ # so this can never reap a seat mid-write. Only on the COMPLETE path: on a timeout the seats are
62
+ # still working and closing one would destroy the work being waited for.
63
+ if [ -n "${TKR_CLOSE_PANES:-}" ]; then
64
+ for pane in ${TKR_CLOSE_PANES//,/ }; do
65
+ # `herdr pane close` exits 0 even for a pane that is already gone, so report the body rather
66
+ # than the status — an exit code here would claim a close that may never have happened.
67
+ printf ' CLOSED %s -> %s\n' "$pane" "$(herdr pane close "$pane" 2>&1 | head -c 80)"
68
+ done
69
+ fi
70
+ exit 0
71
+ fi
72
+
73
+ if [ "$SECONDS" -ge "$END" ]; then
74
+ # Timing out is NOT failure and must not read as one: it is the heartbeat that proves the watcher was
75
+ # alive and still watching. Report both sides so the state is unambiguous on arrival.
76
+ echo "WAKE: cap ${CAP}s reached — ${#ready[@]} of $# complete, re-arm"
77
+ for f in ${ready[@]+"${ready[@]}"}; do echo " READY $(wc -c <"$f" | tr -d ' ') bytes $f"; done
78
+ for f in ${pending[@]+"${pending[@]}"}; do
79
+ if [ -s "$f" ]; then echo " PARTIAL $(wc -c <"$f" | tr -d ' ') bytes, no '$MARKER' yet $f"
80
+ else echo " ABSENT $f"; fi
81
+ done
82
+ exit 0
83
+ fi
84
+
85
+ sleep "$POLL"
86
+ done
@@ -28,7 +28,7 @@ WORKER="${1:?worker pane id required}"
28
28
  ORCH="${2:?orchestrator pane id required}"
29
29
  shift 2
30
30
  FAST_BLOCKED=false
31
- CAP=14400
31
+ CAP=240 # a backgrounded Bash job is killed at 300s; a longer cap can never be reached (OBS-325)
32
32
  SETTLE=60
33
33
  while [ $# -gt 0 ]; do
34
34
  case "$1" in
@@ -1,59 +0,0 @@
1
- import { type RunGraph } from "../../graph/schema.js";
2
- import { type JournalEvent } from "../../run/journal.js";
3
- import { FleetStaging } from "../staging.js";
4
- export type StudioInkView = {
5
- id: string;
6
- label: string;
7
- render(props: {
8
- cols: number;
9
- rows: number;
10
- }): string[];
11
- key?(name: string): void;
12
- };
13
- export type StudioInkIO = {
14
- input: NodeJS.ReadStream;
15
- output: NodeJS.WriteStream;
16
- views?: StudioInkView[];
17
- repoRoot?: string;
18
- globalDir?: string;
19
- staging?: FleetStaging;
20
- runsData?: RunsCockpitData;
21
- clock?: () => number;
22
- debug?: boolean;
23
- };
24
- export type RunsCockpitData = {
25
- runId?: string;
26
- events: JournalEvent[];
27
- graph: RunGraph;
28
- prompts?: Record<string, string[]>;
29
- };
30
- type DossierVerdict = {
31
- action: "retry" | "reroute" | "decompose" | "human";
32
- reason?: string;
33
- prompt?: string;
34
- };
35
- type ConsultDossierData = {
36
- taskId: string;
37
- verdicts: DossierVerdict[];
38
- };
39
- type RunsCockpitTask = {
40
- id: string;
41
- status: string;
42
- phase?: string;
43
- elapsed?: string;
44
- consultCount: number;
45
- };
46
- export declare function foldConsultDossier(data: RunsCockpitData, taskId: string): ConsultDossierData;
47
- export declare function buildRunsCockpitTasks(data: RunsCockpitData, nowMs: number): RunsCockpitTask[];
48
- export declare function StudioApp({ views: suppliedViews, repoRoot, globalDir, staging: suppliedStaging, runsData: suppliedRunsData, clock, columns, rows, }: {
49
- views?: StudioInkView[];
50
- repoRoot?: string;
51
- globalDir?: string;
52
- staging?: FleetStaging;
53
- runsData?: RunsCockpitData;
54
- clock?: () => number;
55
- columns?: number;
56
- rows?: number;
57
- }): import("react").JSX.Element;
58
- export declare function runStudioInk({ input, output, views, repoRoot, globalDir, staging, runsData, clock, debug, }: StudioInkIO): Promise<void>;
59
- export {};
@@ -1,320 +0,0 @@
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
- }
@@ -1,38 +0,0 @@
1
- import { type FleetEditable } from "../config/config.js";
2
- export type SaveTarget = "repo" | "global";
3
- /** Notice surfaced in the save modal whenever a run lock is live. */
4
- export declare const RELOAD_GUARD_NOTICE = "reload guard: a live run is active; the running daemon will not see these changes until it reloads";
5
- export type SaveProposal = {
6
- target: SaveTarget;
7
- targetPath: string;
8
- before: string;
9
- after: string;
10
- diff: string;
11
- liveRun: boolean;
12
- loadError: string | null;
13
- };
14
- export type SaveResult = {
15
- kind: "written";
16
- path: string;
17
- } | {
18
- kind: "declined";
19
- } | {
20
- kind: "refused";
21
- reason: string;
22
- };
23
- /** Build the diff-confirm payload for the current staging delta and chosen target. */
24
- export declare function buildSaveProposal(opts: {
25
- repoRoot: string;
26
- loaded: FleetEditable;
27
- staged: FleetEditable;
28
- target: SaveTarget;
29
- globalDir?: string;
30
- }): SaveProposal;
31
- /** Confirm and execute the write for the chosen target. */
32
- export declare function confirmSave(opts: {
33
- repoRoot: string;
34
- loaded: FleetEditable;
35
- staged: FleetEditable;
36
- target: SaveTarget;
37
- globalDir?: string;
38
- }): SaveResult;
package/dist/tui/save.js DELETED
@@ -1,96 +0,0 @@
1
- import { randomUUID } from "node:crypto";
2
- import { existsSync, mkdtempSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
3
- import { tmpdir } from "node:os";
4
- import { dirname, join } from "node:path";
5
- import { fleetEditableFromConfig, fleetEditableEquals, fleetRepoOverlayFromDelta, globalConfigDir, loadConfigWithMode, readOverlayFile, repoOverlayPath, repoOverlayYaml, unifiedYamlDiff, } from "../config/config.js";
6
- import { isRunLockLive } from "../run/lock.js";
7
- /** Notice surfaced in the save modal whenever a run lock is live. */
8
- export const RELOAD_GUARD_NOTICE = "reload guard: a live run is active; the running daemon will not see these changes until it reloads";
9
- function overlayPath(repoRoot, target, globalDir) {
10
- return target === "repo"
11
- ? repoOverlayPath(repoRoot)
12
- : join(globalDir ?? globalConfigDir(), "config.yaml");
13
- }
14
- function currentOverlayText(path) {
15
- return existsSync(path) ? readFileSync(path, "utf8") : "";
16
- }
17
- function computeAfterBytes(target, loaded, staged, repoRoot, globalDir) {
18
- const path = overlayPath(repoRoot, target, globalDir);
19
- const existing = readOverlayFile(path);
20
- // Reuse the existing fleet serializer and diff renderer rather than inventing a parallel formatter.
21
- const merged = fleetRepoOverlayFromDelta(loaded, staged, existing);
22
- return repoOverlayYaml(merged);
23
- }
24
- function roundTripError(target, repoRoot, after, staged, globalDir) {
25
- try {
26
- let effective;
27
- if (target === "repo") {
28
- effective = loadConfigWithMode(repoRoot, { globalDir, repoOverlayText: after });
29
- }
30
- else {
31
- // Validate a candidate global overlay by loading with it in a scratch globalDir.
32
- const scratch = mkdtempSync(join(tmpdir(), "tickmarkr-global-rt-"));
33
- try {
34
- writeFileSync(join(scratch, "config.yaml"), after);
35
- effective = loadConfigWithMode(repoRoot, { globalDir: scratch });
36
- }
37
- finally {
38
- try {
39
- rmSync(scratch, { recursive: true, force: true });
40
- }
41
- catch { /* best-effort cleanup */ }
42
- }
43
- }
44
- const reparsed = fleetEditableFromConfig(effective.cfg);
45
- if (!fleetEditableEquals(reparsed, staged)) {
46
- return "round-trip mismatch: the written overlay does not re-parse to the staged state";
47
- }
48
- return null;
49
- }
50
- catch (e) {
51
- return e.message;
52
- }
53
- }
54
- /** Build the diff-confirm payload for the current staging delta and chosen target. */
55
- export function buildSaveProposal(opts) {
56
- const path = overlayPath(opts.repoRoot, opts.target, opts.globalDir);
57
- const before = currentOverlayText(path);
58
- const after = computeAfterBytes(opts.target, opts.loaded, opts.staged, opts.repoRoot, opts.globalDir);
59
- const diff = unifiedYamlDiff(before, after, path);
60
- const loadError = roundTripError(opts.target, opts.repoRoot, after, opts.staged, opts.globalDir);
61
- return {
62
- target: opts.target,
63
- targetPath: path,
64
- before,
65
- after,
66
- diff,
67
- liveRun: isRunLockLive(opts.repoRoot),
68
- loadError,
69
- };
70
- }
71
- function atomicWrite(path, bytes) {
72
- const dir = dirname(path);
73
- mkdirSync(dir, { recursive: true });
74
- // The sibling temp keeps rename(2) on one filesystem: readers see old or new bytes, never a partial overlay.
75
- const tmp = join(dir, `.tickmarkr-save-${randomUUID()}.tmp`);
76
- try {
77
- writeFileSync(tmp, bytes);
78
- renameSync(tmp, path);
79
- }
80
- catch (e) {
81
- rmSync(tmp, { force: true }); // a failed rename must not leave a partial candidate behind
82
- throw e;
83
- }
84
- }
85
- /** Confirm and execute the write for the chosen target. */
86
- export function confirmSave(opts) {
87
- const proposal = buildSaveProposal(opts);
88
- if (proposal.loadError) {
89
- return { kind: "refused", reason: proposal.loadError };
90
- }
91
- if (proposal.before === proposal.after) {
92
- return { kind: "declined" };
93
- }
94
- atomicWrite(proposal.targetPath, proposal.after);
95
- return { kind: "written", path: proposal.targetPath };
96
- }
@@ -1,29 +0,0 @@
1
- import { type FleetEditable } from "../config/config.js";
2
- /** One in-memory edit to the buffered fleet state. */
3
- export type FleetEdit = (buffer: FleetEditable) => void;
4
- /**
5
- * Staged-changes buffer for the Fleet Studio write path.
6
- *
7
- * - The loaded state is captured at construction and never mutated.
8
- * - Edits apply to an in-memory buffer only.
9
- * - `revert()` discards the buffer and restores the loaded state.
10
- * - The delta is computed through the existing `fleetEditableEquals` comparison.
11
- * - No code path in this module writes to disk.
12
- */
13
- export declare class FleetStaging {
14
- private readonly loaded;
15
- private buffer;
16
- constructor(loaded: FleetEditable);
17
- /** Read-only snapshot of the originally loaded state. */
18
- get loadedState(): FleetEditable;
19
- /** Current buffered (possibly edited) state. */
20
- get current(): FleetEditable;
21
- /** Apply an edit to the buffer. Never touches disk. */
22
- apply(edit: FleetEdit): void;
23
- /** Discard all buffered edits and restore the loaded state. */
24
- revert(): void;
25
- /** True when the buffer differs from the loaded state. */
26
- get isDirty(): boolean;
27
- /** Number of distinct staged changes vs the loaded state. */
28
- get changeCount(): number;
29
- }