@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.
- package/README.md +52 -28
- package/dist/builtins/autodoc.workflow.d.ts +4 -4
- package/dist/builtins/autoimplement.workflow.d.ts +369 -73
- package/dist/builtins/autoplan.workflow.d.ts +126 -13
- package/dist/builtins/autoplan.workflow.js +200 -29
- package/dist/builtins/autoplan.workflow.js.map +1 -1
- package/dist/builtins/catalog.js +6 -4
- package/dist/builtins/catalog.js.map +1 -1
- package/dist/builtins/index.d.ts +2 -1
- package/dist/builtins/index.js +1 -0
- package/dist/builtins/index.js.map +1 -1
- package/dist/builtins/monitor.workflow.d.ts +56 -21
- package/dist/builtins/monitor.workflow.js +497 -216
- package/dist/builtins/monitor.workflow.js.map +1 -1
- package/dist/builtins/plain-summary.workflow.d.ts +32 -0
- package/dist/builtins/plain-summary.workflow.js +134 -0
- package/dist/builtins/plain-summary.workflow.js.map +1 -0
- package/dist/builtins/plan-change.workflow.d.ts +170 -22
- package/dist/builtins/sanity-check.workflow.d.ts +45 -3
- package/dist/builtins/sanity-check.workflow.js +45 -7
- package/dist/builtins/sanity-check.workflow.js.map +1 -1
- package/dist/controllers/effects.d.ts +3 -2
- package/dist/controllers/effects.js +8 -1
- package/dist/controllers/effects.js.map +1 -1
- package/dist/controllers/index.d.ts +1 -1
- package/dist/controllers/index.js +1 -1
- package/dist/controllers/index.js.map +1 -1
- package/dist/controllers/manager.d.ts +1 -0
- package/dist/controllers/manager.js +41 -17
- package/dist/controllers/manager.js.map +1 -1
- package/dist/controllers/sqlite.d.ts +107 -85
- package/dist/controllers/sqlite.js +1611 -1486
- package/dist/controllers/sqlite.js.map +1 -1
- package/dist/controllers/store.d.ts +9 -8
- package/dist/controllers/store.js +1 -31
- package/dist/controllers/store.js.map +1 -1
- package/dist/controllers/types.d.ts +3 -0
- package/dist/controllers/workflow-engine-scheduler.d.ts +1 -1
- package/dist/controllers/workflow-engine-scheduler.js +3 -5
- package/dist/controllers/workflow-engine-scheduler.js.map +1 -1
- package/dist/controllers/workflows.d.ts +2 -2
- package/dist/controllers/workflows.js +11 -10
- package/dist/controllers/workflows.js.map +1 -1
- package/dist/extension/controller-host.js +2 -2
- package/dist/extension/controller-host.js.map +1 -1
- package/dist/extension/decision-channels.d.ts +2 -0
- package/dist/extension/decision-channels.js +311 -118
- package/dist/extension/decision-channels.js.map +1 -1
- package/dist/extension/executor.d.ts +9 -2
- package/dist/extension/executor.js +89 -2
- package/dist/extension/executor.js.map +1 -1
- package/dist/extension/herdr-viewer.d.ts +0 -1
- package/dist/extension/herdr-viewer.js +0 -4
- package/dist/extension/herdr-viewer.js.map +1 -1
- package/dist/extension/index.js +156 -103
- package/dist/extension/index.js.map +1 -1
- package/dist/extension/recorder.d.ts +3 -10
- package/dist/extension/recorder.js +13 -22
- package/dist/extension/recorder.js.map +1 -1
- package/dist/extension/step-message.d.ts +7 -2
- package/dist/extension/step-message.js +94 -2
- package/dist/extension/step-message.js.map +1 -1
- package/dist/extension/widget.js +6 -0
- package/dist/extension/widget.js.map +1 -1
- package/dist/host/rpc-executor.d.ts +3 -0
- package/dist/host/rpc-executor.js +2 -0
- package/dist/host/rpc-executor.js.map +1 -1
- package/dist/host/runner.d.ts +3 -6
- package/dist/host/runner.js +83 -32
- package/dist/host/runner.js.map +1 -1
- package/dist/render/graph-render.js +14 -8
- package/dist/render/graph-render.js.map +1 -1
- package/dist/state/database.d.ts +35 -0
- package/dist/state/database.js +287 -0
- package/dist/state/database.js.map +1 -0
- package/dist/state/index.d.ts +4 -0
- package/dist/state/index.js +5 -0
- package/dist/state/index.js.map +1 -0
- package/dist/state/json.d.ts +6 -0
- package/dist/state/json.js +38 -0
- package/dist/state/json.js.map +1 -0
- package/dist/state/mutation.d.ts +57 -0
- package/dist/state/mutation.js +255 -0
- package/dist/state/mutation.js.map +1 -0
- package/dist/state/schema.d.ts +6 -0
- package/dist/state/schema.js +473 -0
- package/dist/state/schema.js.map +1 -0
- package/dist/viewer/cli.d.ts +2 -2
- package/dist/viewer/cli.js +142 -122
- package/dist/viewer/cli.js.map +1 -1
- package/dist/viewer/render.d.ts +4 -4
- package/dist/viewer/render.js +2 -17
- package/dist/viewer/render.js.map +1 -1
- package/dist/viewer/session-reducer.d.ts +1 -1
- package/dist/viewer/session-reducer.js +1 -1
- package/dist/viewer/tui.d.ts +3 -3
- package/dist/viewer/tui.js +15 -13
- package/dist/viewer/tui.js.map +1 -1
- package/dist/viewer/watch.d.ts +2 -5
- package/dist/viewer/watch.js +13 -13
- package/dist/viewer/watch.js.map +1 -1
- package/dist/workflows/composition.js +12 -2
- package/dist/workflows/composition.js.map +1 -1
- package/dist/workflows/definition.d.ts +6 -2
- package/dist/workflows/definition.js +17 -0
- package/dist/workflows/definition.js.map +1 -1
- package/dist/workflows/engine.d.ts +6 -6
- package/dist/workflows/engine.js +183 -103
- package/dist/workflows/engine.js.map +1 -1
- package/dist/workflows/errors.d.ts +3 -3
- package/dist/workflows/errors.js +3 -3
- package/dist/workflows/human-decision.d.ts +29 -4
- package/dist/workflows/human-decision.js +547 -283
- package/dist/workflows/human-decision.js.map +1 -1
- package/dist/workflows/index.d.ts +4 -5
- package/dist/workflows/index.js +3 -4
- package/dist/workflows/index.js.map +1 -1
- package/dist/workflows/schema.js +17 -1
- package/dist/workflows/schema.js.map +1 -1
- package/dist/workflows/store.d.ts +83 -103
- package/dist/workflows/store.js +902 -951
- package/dist/workflows/store.js.map +1 -1
- package/dist/workflows/types.d.ts +55 -81
- package/docs/CONTROLLERS.md +10 -8
- package/docs/DEFERRED_TURNS.md +2 -2
- package/docs/DESIGN_PHILOSOPHY.md +4 -2
- package/docs/HUMAN_DECISIONS.md +13 -14
- package/docs/MONITOR.md +179 -231
- package/docs/SQLITE_STATE.md +219 -0
- package/docs/WORKFLOW_COMPOSITION.md +19 -19
- package/docs/WORKFLOW_STEP_MESSAGES.md +20 -18
- package/docs/WORKFLOW_UPDATES.md +10 -9
- package/docs/development.md +9 -9
- package/docs/live-replay-protocol.md +28 -37
- package/docs/plans/2026-08-16-workflow-updates-plan.md +1 -1
- package/docs/plans/2026-08-19-human-decision-gates-plan.md +1 -1
- package/docs/plans/2026-08-21-autoimplement-timeout-fallback-plan.md +1 -1
- package/docs/plans/2026-08-21-plan-change-approval-policy-plan.md +1 -1
- package/docs/plans/2026-08-21-sanity-check-plan.md +30 -16
- package/docs/plans/2026-08-22-goal-finishing-monitor-plan.md +200 -0
- package/docs/plans/2026-08-23-assistant-agent-completion-plan.md +701 -0
- package/docs/plans/2026-08-23-sqlite-state-plan.md +970 -0
- package/docs/plans/replayable-run-bundles-implementation-plan.md +2 -2
- package/docs/plans/session-event-replay-implementation-plan.md +1 -1
- package/docs/plans/tui-viewer-implementation-plan.md +1 -1
- package/docs/session-event-journal.md +63 -434
- package/docs/tui-viewer.md +13 -15
- package/docs/workflows.md +104 -50
- package/examples/workflows/plain-summary.workflow.ts +1 -0
- package/herdr-plugin.toml +1 -1
- package/package.json +1 -1
- package/plugins/herdr/viewer.mjs +1 -11
- package/skills/autoplan/SKILL.md +5 -4
- package/skills/monitor/SKILL.md +91 -163
- package/skills/pi-workflows/SKILL.md +7 -7
- package/skills/sanity-check/SKILL.md +1 -1
- package/src/builtins/autoplan.workflow.ts +290 -40
- package/src/builtins/catalog.ts +6 -4
- package/src/builtins/index.ts +7 -1
- package/src/builtins/monitor.workflow.ts +663 -249
- package/src/builtins/plain-summary.workflow.ts +185 -0
- package/src/builtins/sanity-check.workflow.ts +62 -7
- package/src/controllers/effects.ts +7 -0
- package/src/controllers/index.ts +0 -6
- package/src/controllers/manager.ts +72 -32
- package/src/controllers/sqlite.ts +2420 -1948
- package/src/controllers/store.ts +17 -38
- package/src/controllers/types.ts +3 -0
- package/src/controllers/workflow-engine-scheduler.ts +4 -8
- package/src/controllers/workflows.ts +45 -21
- package/src/extension/controller-host.ts +1 -2
- package/src/extension/decision-channels.ts +427 -144
- package/src/extension/executor.ts +105 -2
- package/src/extension/herdr-viewer.ts +0 -5
- package/src/extension/index.ts +180 -118
- package/src/extension/recorder.ts +13 -22
- package/src/extension/step-message.ts +106 -4
- package/src/extension/widget.ts +8 -0
- package/src/host/rpc-executor.ts +4 -0
- package/src/host/runner.ts +93 -41
- package/src/render/graph-render.ts +11 -4
- package/src/state/database.ts +375 -0
- package/src/state/index.ts +29 -0
- package/src/state/json.ts +42 -0
- package/src/state/mutation.ts +435 -0
- package/src/state/schema.ts +475 -0
- package/src/viewer/cli.ts +151 -144
- package/src/viewer/render.ts +12 -24
- package/src/viewer/session-reducer.ts +1 -1
- package/src/viewer/tui.ts +18 -17
- package/src/viewer/watch.ts +13 -17
- package/src/workflows/composition.ts +12 -3
- package/src/workflows/definition.ts +36 -3
- package/src/workflows/engine.ts +212 -116
- package/src/workflows/errors.ts +3 -3
- package/src/workflows/human-decision.ts +795 -340
- package/src/workflows/index.ts +15 -19
- package/src/workflows/schema.ts +19 -1
- package/src/workflows/store.ts +1295 -1163
- package/src/workflows/types.ts +61 -81
- package/dist/workflows/artifacts.d.ts +0 -40
- package/dist/workflows/artifacts.js +0 -155
- package/dist/workflows/artifacts.js.map +0 -1
- package/dist/workflows/migrate-sources.d.ts +0 -42
- package/dist/workflows/migrate-sources.js +0 -133
- package/dist/workflows/migrate-sources.js.map +0 -1
- package/docs/run-bundles.md +0 -481
- package/src/workflows/artifacts.ts +0 -188
- 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 {
|
|
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] [--
|
|
23
|
-
pi-workflows runs
|
|
24
|
-
pi-workflows controllers
|
|
25
|
-
pi-workflows controller <controller> <key>
|
|
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
|
-
|
|
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
|
-
|
|
53
|
-
|
|
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 === "--
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
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 ||
|
|
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]
|
|
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]
|
|
128
|
-
dir,
|
|
129
|
-
controllerDir,
|
|
108
|
+
...(positionals[0] === undefined ? {} : { runId: positionals[0] }),
|
|
130
109
|
once,
|
|
131
110
|
json,
|
|
132
111
|
};
|
|
133
112
|
}
|
|
134
113
|
|
|
135
|
-
|
|
136
|
-
const
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
const
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
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
|
-
|
|
153
|
-
const
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
throw new Error(`Run not found: ${runId}`);
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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(
|
|
171
|
-
const store = openControllerStore(
|
|
150
|
+
function printControllers(databasePath: string): void {
|
|
151
|
+
const store = openControllerStore(databasePath);
|
|
172
152
|
if (store === undefined) {
|
|
173
|
-
process.stdout.write(
|
|
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(
|
|
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(
|
|
200
|
-
const store = openControllerStore(
|
|
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
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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
|
|
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
|
-
|
|
219
|
+
printRuns(databasePath);
|
|
237
220
|
return 0;
|
|
238
221
|
}
|
|
239
222
|
if (args.command === "controllers") {
|
|
240
|
-
printControllers(
|
|
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 === "
|
|
252
|
-
|
|
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
|
-
|
|
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(
|
|
294
|
-
|
|
295
|
-
|
|
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
|
|
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;
|
package/src/viewer/render.ts
CHANGED
|
@@ -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 {
|
|
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 =
|
|
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:
|
|
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(
|
|
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 :
|
|
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:
|
|
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:
|
|
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
|
|
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 {
|
|
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 {
|
|
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";
|
|
14
|
+
type ViewerMode = { view: "list" } | { view: "detail"; runId: string };
|
|
16
15
|
|
|
17
16
|
export type ViewerOptions = {
|
|
18
|
-
|
|
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
|
|
33
|
-
*
|
|
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:
|
|
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 =
|
|
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",
|
|
50
|
+
mode = { view: "detail", runId: match.runId };
|
|
51
51
|
}
|
|
52
52
|
|
|
53
53
|
const draw = async () => {
|
|
54
|
-
bundles =
|
|
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.
|
|
60
|
+
: await renderDetail(mode.runId, size);
|
|
61
61
|
process.stdout.write(CLEAR + lines.join("\n"));
|
|
62
62
|
};
|
|
63
63
|
|
|
64
|
-
const renderDetail = async (
|
|
65
|
-
const bundle =
|
|
64
|
+
const renderDetail = async (runId: string, size: ViewportSize): Promise<string[]> => {
|
|
65
|
+
const bundle = store.readRun(runId, { includeTrace: true });
|
|
66
66
|
if (!bundle) {
|
|
67
|
-
return ["
|
|
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 =
|
|
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",
|
|
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
|
}
|