@osolmaz/pi-workflows 0.12.1 → 0.13.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 (209) hide show
  1. package/README.md +52 -28
  2. package/dist/builtins/autodoc.workflow.d.ts +4 -4
  3. package/dist/builtins/autoimplement.workflow.d.ts +369 -73
  4. package/dist/builtins/autoplan.workflow.d.ts +126 -13
  5. package/dist/builtins/autoplan.workflow.js +200 -29
  6. package/dist/builtins/autoplan.workflow.js.map +1 -1
  7. package/dist/builtins/catalog.js +6 -4
  8. package/dist/builtins/catalog.js.map +1 -1
  9. package/dist/builtins/index.d.ts +2 -1
  10. package/dist/builtins/index.js +1 -0
  11. package/dist/builtins/index.js.map +1 -1
  12. package/dist/builtins/monitor.workflow.d.ts +56 -21
  13. package/dist/builtins/monitor.workflow.js +497 -216
  14. package/dist/builtins/monitor.workflow.js.map +1 -1
  15. package/dist/builtins/plain-summary.workflow.d.ts +32 -0
  16. package/dist/builtins/plain-summary.workflow.js +134 -0
  17. package/dist/builtins/plain-summary.workflow.js.map +1 -0
  18. package/dist/builtins/plan-change.workflow.d.ts +170 -22
  19. package/dist/builtins/sanity-check.workflow.d.ts +45 -3
  20. package/dist/builtins/sanity-check.workflow.js +45 -7
  21. package/dist/builtins/sanity-check.workflow.js.map +1 -1
  22. package/dist/controllers/effects.d.ts +3 -2
  23. package/dist/controllers/effects.js +8 -1
  24. package/dist/controllers/effects.js.map +1 -1
  25. package/dist/controllers/index.d.ts +1 -1
  26. package/dist/controllers/index.js +1 -1
  27. package/dist/controllers/index.js.map +1 -1
  28. package/dist/controllers/manager.d.ts +1 -0
  29. package/dist/controllers/manager.js +41 -17
  30. package/dist/controllers/manager.js.map +1 -1
  31. package/dist/controllers/sqlite.d.ts +107 -85
  32. package/dist/controllers/sqlite.js +1611 -1486
  33. package/dist/controllers/sqlite.js.map +1 -1
  34. package/dist/controllers/store.d.ts +9 -8
  35. package/dist/controllers/store.js +1 -31
  36. package/dist/controllers/store.js.map +1 -1
  37. package/dist/controllers/types.d.ts +3 -0
  38. package/dist/controllers/workflow-engine-scheduler.d.ts +1 -1
  39. package/dist/controllers/workflow-engine-scheduler.js +3 -5
  40. package/dist/controllers/workflow-engine-scheduler.js.map +1 -1
  41. package/dist/controllers/workflows.d.ts +2 -2
  42. package/dist/controllers/workflows.js +11 -10
  43. package/dist/controllers/workflows.js.map +1 -1
  44. package/dist/extension/controller-host.js +2 -2
  45. package/dist/extension/controller-host.js.map +1 -1
  46. package/dist/extension/decision-channels.d.ts +2 -0
  47. package/dist/extension/decision-channels.js +311 -118
  48. package/dist/extension/decision-channels.js.map +1 -1
  49. package/dist/extension/executor.d.ts +9 -2
  50. package/dist/extension/executor.js +89 -2
  51. package/dist/extension/executor.js.map +1 -1
  52. package/dist/extension/herdr-viewer.d.ts +0 -1
  53. package/dist/extension/herdr-viewer.js +0 -4
  54. package/dist/extension/herdr-viewer.js.map +1 -1
  55. package/dist/extension/index.js +156 -103
  56. package/dist/extension/index.js.map +1 -1
  57. package/dist/extension/recorder.d.ts +3 -10
  58. package/dist/extension/recorder.js +13 -22
  59. package/dist/extension/recorder.js.map +1 -1
  60. package/dist/extension/step-message.d.ts +7 -2
  61. package/dist/extension/step-message.js +94 -2
  62. package/dist/extension/step-message.js.map +1 -1
  63. package/dist/extension/widget.js +6 -0
  64. package/dist/extension/widget.js.map +1 -1
  65. package/dist/host/rpc-executor.d.ts +3 -0
  66. package/dist/host/rpc-executor.js +2 -0
  67. package/dist/host/rpc-executor.js.map +1 -1
  68. package/dist/host/runner.d.ts +3 -6
  69. package/dist/host/runner.js +83 -32
  70. package/dist/host/runner.js.map +1 -1
  71. package/dist/render/graph-render.js +14 -8
  72. package/dist/render/graph-render.js.map +1 -1
  73. package/dist/state/database.d.ts +35 -0
  74. package/dist/state/database.js +287 -0
  75. package/dist/state/database.js.map +1 -0
  76. package/dist/state/index.d.ts +4 -0
  77. package/dist/state/index.js +5 -0
  78. package/dist/state/index.js.map +1 -0
  79. package/dist/state/json.d.ts +6 -0
  80. package/dist/state/json.js +38 -0
  81. package/dist/state/json.js.map +1 -0
  82. package/dist/state/mutation.d.ts +57 -0
  83. package/dist/state/mutation.js +255 -0
  84. package/dist/state/mutation.js.map +1 -0
  85. package/dist/state/schema.d.ts +6 -0
  86. package/dist/state/schema.js +473 -0
  87. package/dist/state/schema.js.map +1 -0
  88. package/dist/viewer/cli.d.ts +2 -2
  89. package/dist/viewer/cli.js +142 -122
  90. package/dist/viewer/cli.js.map +1 -1
  91. package/dist/viewer/render.d.ts +4 -4
  92. package/dist/viewer/render.js +2 -17
  93. package/dist/viewer/render.js.map +1 -1
  94. package/dist/viewer/session-reducer.d.ts +1 -1
  95. package/dist/viewer/session-reducer.js +1 -1
  96. package/dist/viewer/tui.d.ts +3 -3
  97. package/dist/viewer/tui.js +15 -13
  98. package/dist/viewer/tui.js.map +1 -1
  99. package/dist/viewer/watch.d.ts +2 -5
  100. package/dist/viewer/watch.js +13 -13
  101. package/dist/viewer/watch.js.map +1 -1
  102. package/dist/workflows/composition.js +12 -2
  103. package/dist/workflows/composition.js.map +1 -1
  104. package/dist/workflows/definition.d.ts +6 -2
  105. package/dist/workflows/definition.js +17 -0
  106. package/dist/workflows/definition.js.map +1 -1
  107. package/dist/workflows/engine.d.ts +6 -6
  108. package/dist/workflows/engine.js +183 -103
  109. package/dist/workflows/engine.js.map +1 -1
  110. package/dist/workflows/errors.d.ts +3 -3
  111. package/dist/workflows/errors.js +3 -3
  112. package/dist/workflows/human-decision.d.ts +29 -4
  113. package/dist/workflows/human-decision.js +547 -283
  114. package/dist/workflows/human-decision.js.map +1 -1
  115. package/dist/workflows/index.d.ts +4 -5
  116. package/dist/workflows/index.js +3 -4
  117. package/dist/workflows/index.js.map +1 -1
  118. package/dist/workflows/schema.js +17 -1
  119. package/dist/workflows/schema.js.map +1 -1
  120. package/dist/workflows/store.d.ts +83 -103
  121. package/dist/workflows/store.js +902 -951
  122. package/dist/workflows/store.js.map +1 -1
  123. package/dist/workflows/types.d.ts +55 -81
  124. package/docs/CONTROLLERS.md +10 -8
  125. package/docs/DEFERRED_TURNS.md +2 -2
  126. package/docs/DESIGN_PHILOSOPHY.md +4 -2
  127. package/docs/HUMAN_DECISIONS.md +13 -14
  128. package/docs/MONITOR.md +179 -231
  129. package/docs/SQLITE_STATE.md +219 -0
  130. package/docs/WORKFLOW_COMPOSITION.md +19 -19
  131. package/docs/WORKFLOW_STEP_MESSAGES.md +20 -18
  132. package/docs/WORKFLOW_UPDATES.md +10 -9
  133. package/docs/development.md +9 -9
  134. package/docs/live-replay-protocol.md +28 -37
  135. package/docs/plans/2026-08-16-workflow-updates-plan.md +1 -1
  136. package/docs/plans/2026-08-19-human-decision-gates-plan.md +1 -1
  137. package/docs/plans/2026-08-21-autoimplement-timeout-fallback-plan.md +1 -1
  138. package/docs/plans/2026-08-21-plan-change-approval-policy-plan.md +1 -1
  139. package/docs/plans/2026-08-21-sanity-check-plan.md +30 -16
  140. package/docs/plans/2026-08-22-goal-finishing-monitor-plan.md +200 -0
  141. package/docs/plans/2026-08-23-assistant-agent-completion-plan.md +701 -0
  142. package/docs/plans/2026-08-23-sqlite-state-plan.md +970 -0
  143. package/docs/plans/replayable-run-bundles-implementation-plan.md +2 -2
  144. package/docs/plans/session-event-replay-implementation-plan.md +1 -1
  145. package/docs/plans/tui-viewer-implementation-plan.md +1 -1
  146. package/docs/session-event-journal.md +63 -434
  147. package/docs/tui-viewer.md +13 -15
  148. package/docs/workflows.md +104 -50
  149. package/examples/workflows/plain-summary.workflow.ts +1 -0
  150. package/herdr-plugin.toml +1 -1
  151. package/package.json +1 -1
  152. package/plugins/herdr/viewer.mjs +1 -11
  153. package/skills/autoplan/SKILL.md +5 -4
  154. package/skills/monitor/SKILL.md +91 -163
  155. package/skills/pi-workflows/SKILL.md +7 -7
  156. package/skills/sanity-check/SKILL.md +1 -1
  157. package/src/builtins/autoplan.workflow.ts +290 -40
  158. package/src/builtins/catalog.ts +6 -4
  159. package/src/builtins/index.ts +7 -1
  160. package/src/builtins/monitor.workflow.ts +663 -249
  161. package/src/builtins/plain-summary.workflow.ts +185 -0
  162. package/src/builtins/sanity-check.workflow.ts +62 -7
  163. package/src/controllers/effects.ts +7 -0
  164. package/src/controllers/index.ts +0 -6
  165. package/src/controllers/manager.ts +72 -32
  166. package/src/controllers/sqlite.ts +2420 -1948
  167. package/src/controllers/store.ts +17 -38
  168. package/src/controllers/types.ts +3 -0
  169. package/src/controllers/workflow-engine-scheduler.ts +4 -8
  170. package/src/controllers/workflows.ts +45 -21
  171. package/src/extension/controller-host.ts +1 -2
  172. package/src/extension/decision-channels.ts +427 -144
  173. package/src/extension/executor.ts +105 -2
  174. package/src/extension/herdr-viewer.ts +0 -5
  175. package/src/extension/index.ts +180 -118
  176. package/src/extension/recorder.ts +13 -22
  177. package/src/extension/step-message.ts +106 -4
  178. package/src/extension/widget.ts +8 -0
  179. package/src/host/rpc-executor.ts +4 -0
  180. package/src/host/runner.ts +93 -41
  181. package/src/render/graph-render.ts +11 -4
  182. package/src/state/database.ts +375 -0
  183. package/src/state/index.ts +29 -0
  184. package/src/state/json.ts +42 -0
  185. package/src/state/mutation.ts +435 -0
  186. package/src/state/schema.ts +475 -0
  187. package/src/viewer/cli.ts +151 -144
  188. package/src/viewer/render.ts +12 -24
  189. package/src/viewer/session-reducer.ts +1 -1
  190. package/src/viewer/tui.ts +18 -17
  191. package/src/viewer/watch.ts +13 -17
  192. package/src/workflows/composition.ts +12 -3
  193. package/src/workflows/definition.ts +36 -3
  194. package/src/workflows/engine.ts +212 -116
  195. package/src/workflows/errors.ts +3 -3
  196. package/src/workflows/human-decision.ts +795 -340
  197. package/src/workflows/index.ts +15 -19
  198. package/src/workflows/schema.ts +19 -1
  199. package/src/workflows/store.ts +1295 -1163
  200. package/src/workflows/types.ts +61 -81
  201. package/dist/workflows/artifacts.d.ts +0 -40
  202. package/dist/workflows/artifacts.js +0 -155
  203. package/dist/workflows/artifacts.js.map +0 -1
  204. package/dist/workflows/migrate-sources.d.ts +0 -42
  205. package/dist/workflows/migrate-sources.js +0 -133
  206. package/dist/workflows/migrate-sources.js.map +0 -1
  207. package/docs/run-bundles.md +0 -481
  208. package/src/workflows/artifacts.ts +0 -188
  209. package/src/workflows/migrate-sources.ts +0 -178
package/src/viewer/cli.ts CHANGED
@@ -3,10 +3,10 @@ import fs, { realpathSync } from "node:fs";
3
3
  import path from "node:path";
4
4
  import { fileURLToPath, pathToFileURL } from "node:url";
5
5
  import { SqliteControllerStore } from "../controllers/sqlite.js";
6
- import { projectControllerStoreBaseDir } from "../controllers/store.js";
7
6
  import { syncHerdrPlugin } from "../herdr/setup.js";
8
7
  import { sanitizeText } from "../render/ansi.js";
9
- import { listRunBundles, readRunBundle, workflowRunsBaseDir } from "../workflows/store.js";
8
+ import { StateDatabase } from "../state/database.js";
9
+ import { WorkflowRunStore, workflowStateDatabasePath } from "../workflows/store.js";
10
10
  import {
11
11
  formatDuration,
12
12
  renderRunDetailLines,
@@ -19,28 +19,17 @@ import { runViewer } from "./tui.js";
19
19
  const USAGE = `pi-workflows — workflow runs and controller resources
20
20
 
21
21
  Usage:
22
- pi-workflows view [runId] [--dir <runsDir>] [--once]
23
- pi-workflows runs [--dir <runsDir>]
24
- pi-workflows controllers [--controller-dir <dir>]
25
- pi-workflows controller <controller> <key> [--controller-dir <dir>]
22
+ pi-workflows view [runId] [--once]
23
+ pi-workflows runs
24
+ pi-workflows controllers
25
+ pi-workflows controller <controller> <key>
26
+ pi-workflows state status|verify
27
+ pi-workflows state backup <destination>
26
28
  pi-workflows host [--project <dir>] [-- <extra pi args>]
27
29
  pi-workflows herdr sync [--json]
28
30
  pi-workflows herdr setup [--json]
29
31
 
30
- Commands:
31
- view Open the live workflow TUI. With --once, print a snapshot.
32
- runs List recent workflow runs.
33
- controllers List durable controller resources.
34
- controller Show one resource, its effects, child workflows, and events.
35
- host Run the always-on workflow host in the foreground.
36
- herdr Synchronize the bundled Herdr plugin. setup is an alias for sync.
37
-
38
- Options:
39
- --dir <runsDir> Runs directory (default: ~/.pi/agent/workflows/runs)
40
- --controller-dir <dir> Controller directory (default: project-scoped local store)
41
- --once Render once without the interactive TUI
42
- --project <dir> Project directory for the host (default: cwd)
43
- --json Print a versioned JSON result for herdr sync
32
+ All commands use ~/.pi/agent/workflows/state.sqlite.
44
33
  `;
45
34
 
46
35
  export type CliArgs = {
@@ -49,8 +38,8 @@ export type CliArgs = {
49
38
  controllerName?: string;
50
39
  resourceKey?: string;
51
40
  herdrAction?: string;
52
- dir: string;
53
- controllerDir: string;
41
+ stateAction?: "status" | "verify" | "backup";
42
+ backupDestination?: string;
54
43
  once: boolean;
55
44
  json: boolean;
56
45
  project?: string | undefined;
@@ -60,123 +49,114 @@ export type CliArgs = {
60
49
  export function parseCliArgs(argv: string[]): CliArgs {
61
50
  const args = [...argv];
62
51
  const command = args[0] && !args[0].startsWith("-") ? (args.shift() as string) : "view";
63
- let dir = workflowRunsBaseDir();
64
- let controllerDir = projectControllerStoreBaseDir(process.cwd());
65
52
  let once = false;
66
53
  let json = false;
67
- const positionals: string[] = [];
68
54
  let project: string | undefined;
55
+ const positionals: string[] = [];
69
56
  const piArgs: string[] = [];
70
57
 
71
58
  while (args.length > 0) {
72
59
  const arg = args.shift() as string;
73
- if (arg === "--dir") {
74
- dir = requiredValue(args, "--dir");
75
- } else if (arg === "--controller-dir") {
76
- controllerDir = requiredValue(args, "--controller-dir");
77
- } else if (arg === "--project") {
78
- project = requiredValue(args, "--project");
79
- } else if (arg === "--once") {
80
- once = true;
81
- } else if (arg === "--json") {
82
- json = true;
83
- } else if (arg === "--help" || arg === "-h") {
84
- return { command: "help", dir, controllerDir, once, json };
85
- } else if (arg === "--") {
86
- piArgs.push(...args.splice(0));
87
- } else if (arg.startsWith("-")) {
88
- throw new Error(`Unknown argument: ${arg}`);
89
- } else {
90
- positionals.push(arg);
91
- }
92
- }
93
-
94
- if (command !== "herdr" && json) {
95
- throw new Error("--json is available only for herdr sync");
96
- }
97
-
98
- if (command === "host") {
99
- return { command, dir, controllerDir, once, json, project, piArgs };
60
+ if (arg === "--project") project = requiredValue(args, "--project");
61
+ else if (arg === "--once") once = true;
62
+ else if (arg === "--json") json = true;
63
+ else if (arg === "--help" || arg === "-h") return { command: "help", once, json };
64
+ else if (arg === "--") piArgs.push(...args.splice(0));
65
+ else if (arg.startsWith("-")) throw new Error(`Unknown argument: ${arg}`);
66
+ else positionals.push(arg);
100
67
  }
101
68
 
69
+ if (command !== "herdr" && json) throw new Error("--json is available only for herdr sync");
70
+ if (command === "host") return { command, once, json, project, piArgs };
102
71
  if (command === "controller") {
103
- if (positionals.length !== 2) {
104
- throw new Error("controller requires <controller> and <key>");
105
- }
72
+ if (positionals.length !== 2) throw new Error("controller requires <controller> and <key>");
106
73
  return {
107
74
  command,
108
75
  controllerName: positionals[0] as string,
109
76
  resourceKey: positionals[1] as string,
110
- dir,
111
- controllerDir,
112
77
  once,
113
78
  json,
114
79
  };
115
80
  }
81
+ if (command === "state") {
82
+ const action = positionals[0];
83
+ if (action !== "status" && action !== "verify" && action !== "backup") {
84
+ throw new Error("state requires status, verify, or backup");
85
+ }
86
+ if (action === "backup") {
87
+ if (positionals.length !== 2) throw new Error("state backup requires <destination>");
88
+ return {
89
+ command,
90
+ stateAction: action,
91
+ backupDestination: positionals[1] as string,
92
+ once,
93
+ json,
94
+ };
95
+ }
96
+ if (positionals.length !== 1) throw new Error(`state ${action} accepts no other arguments`);
97
+ return { command, stateAction: action, once, json };
98
+ }
116
99
  if (command === "herdr") {
117
- if (positionals.length !== 1 || (positionals[0] !== "sync" && positionals[0] !== "setup")) {
100
+ if (positionals.length !== 1 || !["sync", "setup"].includes(positionals[0] as string)) {
118
101
  throw new Error("herdr requires the sync action");
119
102
  }
120
- return { command, herdrAction: positionals[0], dir, controllerDir, once, json };
121
- }
122
- if (positionals.length > 1) {
123
- throw new Error(`Unexpected argument: ${positionals[1]}`);
103
+ return { command, herdrAction: positionals[0] as string, once, json };
124
104
  }
105
+ if (positionals.length > 1) throw new Error(`Unexpected argument: ${positionals[1]}`);
125
106
  return {
126
107
  command,
127
- ...(positionals[0] !== undefined ? { runId: positionals[0] } : {}),
128
- dir,
129
- controllerDir,
108
+ ...(positionals[0] === undefined ? {} : { runId: positionals[0] }),
130
109
  once,
131
110
  json,
132
111
  };
133
112
  }
134
113
 
135
- async function printRuns(dir: string): Promise<void> {
136
- const bundles = await listRunBundles(dir);
137
- if (bundles.length === 0) {
138
- process.stdout.write(`No workflow runs found in ${dir}\n`);
139
- return;
140
- }
141
- for (const bundle of bundles) {
142
- const state = bundle.state;
143
- const title = state.runTitle ? ` — ${sanitizeText(state.runTitle)}` : "";
144
- process.stdout.write(
145
- `${statusLabel(state.status)} ${sanitizeText(state.workflowName)}${title} ${state.runId} ${formatDuration(
146
- runElapsedMs(state),
147
- )}\n`,
148
- );
114
+ function printRuns(databasePath: string): void {
115
+ const store = new WorkflowRunStore(databasePath, { readOnly: true });
116
+ try {
117
+ const runs = store.listRuns();
118
+ if (runs.length === 0) {
119
+ process.stdout.write("No workflow runs found.\n");
120
+ return;
121
+ }
122
+ for (const run of runs) {
123
+ const state = run.state;
124
+ const title = state.runTitle ? ` — ${sanitizeText(state.runTitle)}` : "";
125
+ process.stdout.write(
126
+ `${statusLabel(state.status)} ${sanitizeText(state.workflowName)}${title} ${state.runId} ${formatDuration(runElapsedMs(state))}\n`,
127
+ );
128
+ }
129
+ } finally {
130
+ store.close();
149
131
  }
150
132
  }
151
133
 
152
- async function printOnce(dir: string, runId: string | undefined): Promise<void> {
153
- const bundles = await listRunBundles(dir);
154
- const size = { width: process.stdout.columns ?? 100, height: 1_000 };
155
- if (runId === undefined) {
156
- process.stdout.write(`${renderRunListLines(bundles, 0, size).join("\n")}\n`);
157
- return;
158
- }
159
- const match = bundles.find((bundle) => bundle.state.runId === runId);
160
- if (!match) {
161
- throw new Error(`Run not found: ${runId}`);
162
- }
163
- const bundle = await readRunBundle(match.runDir, { includeTrace: true });
164
- if (!bundle) {
165
- throw new Error(`Run bundle unreadable: ${match.runDir}`);
134
+ function printOnce(databasePath: string, runId: string | undefined): void {
135
+ const store = new WorkflowRunStore(databasePath, { readOnly: true });
136
+ try {
137
+ const size = { width: process.stdout.columns ?? 100, height: 1_000 };
138
+ if (runId === undefined) {
139
+ process.stdout.write(`${renderRunListLines(store.listRuns(), 0, size).join("\n")}\n`);
140
+ return;
141
+ }
142
+ const run = store.readRun(runId, { includeTrace: true });
143
+ if (run === null) throw new Error(`Run not found: ${runId}`);
144
+ process.stdout.write(`${renderRunDetailLines(run, size).join("\n")}\n`);
145
+ } finally {
146
+ store.close();
166
147
  }
167
- process.stdout.write(`${renderRunDetailLines(bundle, size).join("\n")}\n`);
168
148
  }
169
149
 
170
- function printControllers(controllerDir: string): void {
171
- const store = openControllerStore(controllerDir);
150
+ function printControllers(databasePath: string): void {
151
+ const store = openControllerStore(databasePath);
172
152
  if (store === undefined) {
173
- process.stdout.write(`No controller resources found in ${controllerDir}\n`);
153
+ process.stdout.write("No controller resources found.\n");
174
154
  return;
175
155
  }
176
156
  try {
177
157
  const resources = store.listResources();
178
158
  if (resources.length === 0) {
179
- process.stdout.write(`No controller resources found in ${controllerDir}\n`);
159
+ process.stdout.write("No controller resources found.\n");
180
160
  return;
181
161
  }
182
162
  for (const resource of resources) {
@@ -196,29 +176,31 @@ function printControllers(controllerDir: string): void {
196
176
  }
197
177
  }
198
178
 
199
- function printController(controllerDir: string, controller: string, key: string): void {
200
- const store = openControllerStore(controllerDir);
201
- if (store === undefined) {
202
- throw new Error(`Controller store not found in ${controllerDir}`);
203
- }
179
+ function printController(databasePath: string, controller: string, key: string): void {
180
+ const store = openControllerStore(databasePath);
181
+ if (store === undefined) throw new Error("Controller state database not found");
204
182
  try {
205
183
  const resource = store.getResource({ controller, key });
206
- if (resource === undefined) {
184
+ if (resource === undefined)
207
185
  throw new Error(`Controller resource not found: ${controller}/${key}`);
208
- }
209
- const value = {
210
- resource,
211
- effects: store.listEffects(resource.metadata.uid),
212
- workflows: store.listWorkflows(resource.metadata.uid),
213
- events: store.listEvents({ controller, key, limit: 50 }),
214
- };
215
- process.stdout.write(`${JSON.stringify(value, null, 2)}\n`);
186
+ process.stdout.write(
187
+ `${JSON.stringify(
188
+ {
189
+ resource,
190
+ effects: store.listEffects(resource.metadata.uid),
191
+ workflows: store.listWorkflows(resource.metadata.uid),
192
+ events: store.listEvents({ controller, key, limit: 50 }),
193
+ },
194
+ null,
195
+ 2,
196
+ )}\n`,
197
+ );
216
198
  } finally {
217
199
  store.close();
218
200
  }
219
201
  }
220
202
 
221
- export async function main(argv: string[] = process.argv.slice(2)): Promise<number> {
203
+ export async function main(argv = process.argv.slice(2)): Promise<number> {
222
204
  let args: CliArgs;
223
205
  try {
224
206
  args = parseCliArgs(argv);
@@ -226,42 +208,38 @@ export async function main(argv: string[] = process.argv.slice(2)): Promise<numb
226
208
  process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n\n${USAGE}`);
227
209
  return 2;
228
210
  }
211
+ if (args.command === "help") {
212
+ process.stdout.write(USAGE);
213
+ return 0;
214
+ }
229
215
 
216
+ const databasePath = workflowStateDatabasePath();
230
217
  try {
231
- if (args.command === "help") {
232
- process.stdout.write(USAGE);
233
- return 0;
234
- }
235
218
  if (args.command === "runs") {
236
- await printRuns(args.dir);
219
+ printRuns(databasePath);
237
220
  return 0;
238
221
  }
239
222
  if (args.command === "controllers") {
240
- printControllers(args.controllerDir);
223
+ printControllers(databasePath);
241
224
  return 0;
242
225
  }
243
226
  if (args.command === "controller") {
244
- printController(
245
- args.controllerDir,
246
- args.controllerName as string,
247
- args.resourceKey as string,
248
- );
227
+ printController(databasePath, args.controllerName as string, args.resourceKey as string);
249
228
  return 0;
250
229
  }
251
- if (args.command === "host") {
252
- return await runHost(args.project ?? process.cwd(), args.piArgs);
230
+ if (args.command === "state") {
231
+ await runStateCommand(databasePath, args);
232
+ return 0;
253
233
  }
234
+ if (args.command === "host") return await runHost(args.project ?? process.cwd(), args.piArgs);
254
235
  if (args.command === "herdr") {
255
236
  const result = syncHerdrPlugin(packageRoot());
256
237
  process.stdout.write(args.json ? `${JSON.stringify(result)}\n` : `${result.message}\n`);
257
238
  return 0;
258
239
  }
259
240
  if (args.command === "view") {
260
- if (args.once || !process.stdout.isTTY) {
261
- await printOnce(args.dir, args.runId);
262
- return 0;
263
- }
264
- await runViewer({ runsDir: args.dir, runId: args.runId });
241
+ if (args.once || !process.stdout.isTTY) printOnce(databasePath, args.runId);
242
+ else await runViewer({ databasePath, runId: args.runId });
265
243
  return 0;
266
244
  }
267
245
  process.stderr.write(`Unknown command: ${args.command}\n\n${USAGE}`);
@@ -272,6 +250,42 @@ export async function main(argv: string[] = process.argv.slice(2)): Promise<numb
272
250
  }
273
251
  }
274
252
 
253
+ async function runStateCommand(databasePath: string, args: CliArgs): Promise<void> {
254
+ if (args.stateAction === "backup") {
255
+ const state = new StateDatabase({ filePath: databasePath });
256
+ try {
257
+ await state.backup(path.resolve(args.backupDestination as string));
258
+ process.stdout.write(`Backup verified: ${path.resolve(args.backupDestination as string)}\n`);
259
+ } finally {
260
+ state.close();
261
+ }
262
+ return;
263
+ }
264
+ const state = new StateDatabase({ filePath: databasePath, mode: "read-only" });
265
+ try {
266
+ state.integrityCheck();
267
+ if (args.stateAction === "verify") {
268
+ process.stdout.write("SQLite state is valid.\n");
269
+ return;
270
+ }
271
+ const counts = state.connection
272
+ .prepare(
273
+ `SELECT
274
+ (SELECT count(*) FROM runs) AS runs,
275
+ (SELECT count(*) FROM controller_resources) AS controllers,
276
+ (SELECT count(*) FROM human_decisions) AS decisions,
277
+ (SELECT count(*) FROM effects WHERE status IN ('pending', 'applying', 'ambiguous')) AS unsettledEffects,
278
+ (SELECT count(*) FROM leases WHERE owner_id IS NOT NULL) AS activeLeases`,
279
+ )
280
+ .get();
281
+ process.stdout.write(
282
+ `Database: ${databasePath}\nSize: ${fs.statSync(databasePath).size} bytes\n${JSON.stringify(counts)}\n`,
283
+ );
284
+ } finally {
285
+ state.close();
286
+ }
287
+ }
288
+
275
289
  async function runHost(project: string, piArgs: string[] | undefined): Promise<number> {
276
290
  const { WorkflowHost } = await import("../host/runner.js");
277
291
  const host = new WorkflowHost({
@@ -281,21 +295,16 @@ async function runHost(project: string, piArgs: string[] | undefined): Promise<n
281
295
  });
282
296
  await host.start();
283
297
  await new Promise<void>((resolve) => {
284
- const shutdown = () => {
285
- void host.stop().then(() => resolve());
286
- };
298
+ const shutdown = () => void host.stop().then(resolve);
287
299
  process.once("SIGTERM", shutdown);
288
300
  process.once("SIGINT", shutdown);
289
301
  });
290
302
  return 0;
291
303
  }
292
304
 
293
- function openControllerStore(controllerDir: string): SqliteControllerStore | undefined {
294
- const file = path.join(controllerDir, "controller.sqlite");
295
- if (!fs.existsSync(file)) {
296
- return undefined;
297
- }
298
- return new SqliteControllerStore(file, { readOnly: true });
305
+ function openControllerStore(databasePath: string): SqliteControllerStore | undefined {
306
+ if (!fs.existsSync(databasePath)) return undefined;
307
+ return new SqliteControllerStore(databasePath, { readOnly: true });
299
308
  }
300
309
 
301
310
  function packageRoot(): string {
@@ -304,14 +313,12 @@ function packageRoot(): string {
304
313
 
305
314
  function requiredValue(args: string[], option: string): string {
306
315
  const value = args.shift();
307
- if (!value) {
308
- throw new Error(`${option} requires a path`);
309
- }
316
+ if (!value) throw new Error(`${option} requires a path`);
310
317
  return value;
311
318
  }
312
319
 
313
320
  const entryPath = process.argv[1];
314
- const resolvedEntry = entryPath !== undefined ? realpathSyncSafe(entryPath) : undefined;
321
+ const resolvedEntry = entryPath === undefined ? undefined : realpathSyncSafe(entryPath);
315
322
  if (resolvedEntry !== undefined && import.meta.url === pathToFileURL(resolvedEntry).href) {
316
323
  main().then((code) => {
317
324
  process.exitCode = code;
@@ -1,7 +1,6 @@
1
1
  import { ansi, fitWidth, sanitizeText } from "../render/ansi.js";
2
2
  import { formatDuration, runElapsedMs } from "../render/format.js";
3
3
  import { renderGraphLines } from "../render/graph-render.js";
4
- import { decodeValueWith } from "../workflows/artifacts.js";
5
4
  import {
6
5
  decisionDocumentSegments,
7
6
  decisionPresentationFingerprint,
@@ -12,7 +11,7 @@ import {
12
11
  progressRecordsFromTrace,
13
12
  progressTracksFromRecords,
14
13
  } from "../workflows/progress.js";
15
- import type { LoadedRunBundle } from "../workflows/store.js";
14
+ import type { LoadedWorkflowRun } from "../workflows/store.js";
16
15
  import type {
17
16
  HumanDecisionRequest,
18
17
  WorkflowRunStatus,
@@ -21,22 +20,6 @@ import type {
21
20
 
22
21
  export { formatDuration, runElapsedMs };
23
22
 
24
- /**
25
- * Replace `$artifact` references with a compact placeholder for display. The
26
- * terminal viewer shows summaries; full artifact contents are for replay
27
- * tooling.
28
- */
29
- function withArtifactPlaceholders(value: unknown): unknown {
30
- return decodeValueWith(value, (ref) => `«artifact ${formatBytes(ref.bytes)} ${ref.path}»`);
31
- }
32
-
33
- function formatBytes(bytes: number): string {
34
- if (bytes < 1024) {
35
- return `${bytes}B`;
36
- }
37
- return `${(bytes / 1024).toFixed(1)}KB`;
38
- }
39
-
40
23
  export type ViewportSize = {
41
24
  width: number;
42
25
  height: number;
@@ -59,7 +42,7 @@ function previewValue(rawValue: unknown, maxLength: number): string {
59
42
  if (rawValue === undefined) {
60
43
  return "";
61
44
  }
62
- const value = withArtifactPlaceholders(rawValue);
45
+ const value = rawValue;
63
46
  const text = typeof value === "string" ? value : JSON.stringify(value);
64
47
  // Model-controlled values must not carry escape sequences into the terminal.
65
48
  const singleLine = sanitizeText(text ?? "")
@@ -70,7 +53,7 @@ function previewValue(rawValue: unknown, maxLength: number): string {
70
53
 
71
54
  /** One line per run for the run picker. */
72
55
  export function renderRunListLines(
73
- bundles: LoadedRunBundle[],
56
+ bundles: LoadedWorkflowRun[],
74
57
  selectedIndex: number,
75
58
  size: ViewportSize,
76
59
  now: Date = new Date(),
@@ -126,7 +109,12 @@ function stepLine(
126
109
  }
127
110
 
128
111
  /** Fallback node status list for bundles without a definition snapshot. */
129
- function nodeStatusLine(bundle: LoadedRunBundle, nodeId: string, width: number, now: Date): string {
112
+ function nodeStatusLine(
113
+ bundle: LoadedWorkflowRun,
114
+ nodeId: string,
115
+ width: number,
116
+ now: Date,
117
+ ): string {
130
118
  const state = bundle.state;
131
119
  const nodeType = bundle.snapshot?.nodes[nodeId]?.nodeType ?? "?";
132
120
  const result = state.results[nodeId];
@@ -187,7 +175,7 @@ function inspectorLines(step: WorkflowStepRecord, width: number): string[] {
187
175
  );
188
176
  return lines;
189
177
  }
190
- const body = step.error !== undefined ? step.error : withArtifactPlaceholders(step.output);
178
+ const body = step.error !== undefined ? step.error : step.output;
191
179
  const rendered =
192
180
  typeof body === "string" && step.error !== undefined ? body : JSON.stringify(body, null, 2);
193
181
  for (const raw of (rendered ?? "null").split("\n")) {
@@ -221,7 +209,7 @@ function humanDecisionRequest(value: unknown): HumanDecisionRequest | null {
221
209
  * scrubs the replay position (defaults to the latest step, i.e. live).
222
210
  */
223
211
  export function renderRunDetailLines(
224
- bundle: LoadedRunBundle,
212
+ bundle: LoadedWorkflowRun,
225
213
  size: ViewportSize,
226
214
  now: Date = new Date(),
227
215
  scroll = 0,
@@ -320,7 +308,7 @@ export function renderRunDetailLines(
320
308
 
321
309
  /** Highest useful `scroll` value for the detail view of `bundle`. */
322
310
  export function maxDetailScroll(
323
- bundle: LoadedRunBundle,
311
+ bundle: LoadedWorkflowRun,
324
312
  size: ViewportSize,
325
313
  selectedStepIndex: number | null = null,
326
314
  ): number {
@@ -296,7 +296,7 @@ export function reduceSessionEvents(
296
296
 
297
297
  /**
298
298
  * In-memory seek index. Checkpoints are viewer-only cache state and are never
299
- * persisted into a run bundle.
299
+ * persisted into a SQLite run state.
300
300
  */
301
301
  export class SessionReplayIndex {
302
302
  private readonly entries: WorkflowSessionEntryRecord[];
package/src/viewer/tui.ts CHANGED
@@ -1,21 +1,20 @@
1
- import { listRunBundles, readRunBundle } from "../workflows/store.js";
2
- import type { LoadedRunBundle } from "../workflows/store.js";
1
+ import { WorkflowRunStore, type LoadedWorkflowRun } from "../workflows/store.js";
3
2
  import {
4
3
  maxDetailScroll,
5
4
  renderRunDetailLines,
6
5
  renderRunListLines,
7
6
  type ViewportSize,
8
7
  } from "./render.js";
9
- import { watchRunsDir } from "./watch.js";
8
+ import { watchStateDatabase } from "./watch.js";
10
9
 
11
10
  const ALT_SCREEN_ON = "\u001b[?1049h\u001b[?25l";
12
11
  const ALT_SCREEN_OFF = "\u001b[?25h\u001b[?1049l";
13
12
  const CLEAR = "\u001b[2J\u001b[H";
14
13
 
15
- type ViewerMode = { view: "list" } | { view: "detail"; runDir: string };
14
+ type ViewerMode = { view: "list" } | { view: "detail"; runId: string };
16
15
 
17
16
  export type ViewerOptions = {
18
- runsDir: string;
17
+ databasePath: string;
19
18
  runId?: string | undefined;
20
19
  /** Redraw interval for elapsed timers while a run is active. */
21
20
  tickMs?: number;
@@ -29,12 +28,13 @@ function viewportSize(): ViewportSize {
29
28
  }
30
29
 
31
30
  /**
32
- * Interactive live viewer. Watches the runs directory and re-renders as run
33
- * bundles change on disk. Returns when the user quits.
31
+ * Interactive live viewer. Watches the SQLite database and re-renders after
32
+ * committed run changes. Returns when the user quits.
34
33
  */
35
34
  export async function runViewer(options: ViewerOptions): Promise<void> {
35
+ const store = new WorkflowRunStore(options.databasePath, { readOnly: true });
36
36
  let mode: ViewerMode = { view: "list" };
37
- let bundles: LoadedRunBundle[] = [];
37
+ let bundles: LoadedWorkflowRun[] = [];
38
38
  let selectedIndex = 0;
39
39
  let detailScroll = 0;
40
40
  /** Replay position; null follows the latest step live. */
@@ -42,29 +42,29 @@ export async function runViewer(options: ViewerOptions): Promise<void> {
42
42
  let detailStepCount = 0;
43
43
 
44
44
  if (options.runId) {
45
- bundles = await listRunBundles(options.runsDir);
45
+ bundles = store.listRuns();
46
46
  const match = bundles.find((bundle) => bundle.state.runId === options.runId);
47
47
  if (!match) {
48
48
  throw new Error(`Run not found: ${options.runId}`);
49
49
  }
50
- mode = { view: "detail", runDir: match.runDir };
50
+ mode = { view: "detail", runId: match.runId };
51
51
  }
52
52
 
53
53
  const draw = async () => {
54
- bundles = await listRunBundles(options.runsDir);
54
+ bundles = store.listRuns();
55
55
  selectedIndex = Math.min(selectedIndex, Math.max(0, bundles.length - 1));
56
56
  const size = viewportSize();
57
57
  const lines =
58
58
  mode.view === "list"
59
59
  ? renderRunListLines(bundles, selectedIndex, size)
60
- : await renderDetail(mode.runDir, size);
60
+ : await renderDetail(mode.runId, size);
61
61
  process.stdout.write(CLEAR + lines.join("\n"));
62
62
  };
63
63
 
64
- const renderDetail = async (runDir: string, size: ViewportSize): Promise<string[]> => {
65
- const bundle = await readRunBundle(runDir, { includeTrace: true });
64
+ const renderDetail = async (runId: string, size: ViewportSize): Promise<string[]> => {
65
+ const bundle = store.readRun(runId, { includeTrace: true });
66
66
  if (!bundle) {
67
- return ["Run bundle disappeared. Press q to go back."];
67
+ return ["SQLite run state disappeared. Press q to go back."];
68
68
  }
69
69
  detailStepCount = bundle.state.steps.length;
70
70
  if (selectedStep !== null && selectedStep >= detailStepCount - 1) {
@@ -76,7 +76,7 @@ export async function runViewer(options: ViewerOptions): Promise<void> {
76
76
  };
77
77
 
78
78
  process.stdout.write(ALT_SCREEN_ON);
79
- const stopWatching = watchRunsDir(options.runsDir, () => {
79
+ const stopWatching = watchStateDatabase(options.databasePath, () => {
80
80
  void draw();
81
81
  });
82
82
  const ticker = setInterval(() => {
@@ -136,7 +136,7 @@ export async function runViewer(options: ViewerOptions): Promise<void> {
136
136
  } else if (key === "\r" || key === "\n") {
137
137
  const selected = bundles[selectedIndex];
138
138
  if (selected) {
139
- mode = { view: "detail", runDir: selected.runDir };
139
+ mode = { view: "detail", runId: selected.runId };
140
140
  detailScroll = 0;
141
141
  selectedStep = null;
142
142
  void draw();
@@ -150,6 +150,7 @@ export async function runViewer(options: ViewerOptions): Promise<void> {
150
150
  } finally {
151
151
  clearInterval(ticker);
152
152
  stopWatching();
153
+ store.close();
153
154
  if (rawModeSupported) {
154
155
  process.stdin.setRawMode(false);
155
156
  }