@nodeint/migent 0.1.3 → 0.1.5
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 +57 -41
- package/dist/commands/agent.js +111 -59
- package/dist/commands/config.js +20 -8
- package/dist/commands/execute.d.ts +6 -0
- package/dist/commands/execute.js +30 -14
- package/dist/commands/init.d.ts +2 -0
- package/dist/commands/init.js +91 -87
- package/dist/commands/prepare.d.ts +1 -0
- package/dist/commands/prepare.js +14 -0
- package/dist/commands/repair.d.ts +1 -0
- package/dist/commands/repair.js +22 -0
- package/dist/commands/service.js +5 -4
- package/dist/commands/sessions.d.ts +24 -1
- package/dist/commands/sessions.js +165 -13
- package/dist/commands/skill.js +51 -32
- package/dist/commands/watch.d.ts +2 -1
- package/dist/commands/watch.js +130 -67
- package/dist/commands/workspace-registry.js +10 -1
- package/dist/display.js +21 -9
- package/dist/errors.d.ts +3 -0
- package/dist/errors.js +6 -0
- package/dist/exits.d.ts +10 -0
- package/dist/exits.js +26 -0
- package/dist/follow-log.d.ts +29 -0
- package/dist/follow-log.js +172 -0
- package/dist/fs-utils.d.ts +3 -0
- package/dist/fs-utils.js +26 -0
- package/dist/index.js +5 -1
- package/dist/migrate.js +2 -17
- package/dist/program.js +388 -168
- package/dist/session-row.d.ts +20 -0
- package/dist/session-row.js +21 -0
- package/dist/signals.d.ts +3 -0
- package/dist/signals.js +56 -0
- package/dist/templates/migent/SKILL.md +159 -50
- package/dist/templates/migent/references/schema-config.yaml +35 -0
- package/dist/templates/migent/references/schema-workflow.yaml +61 -0
- package/dist/text.d.ts +4 -0
- package/dist/text.js +13 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +3 -0
- package/dist/workspace.js +5 -12
- package/package.json +4 -4
package/dist/program.js
CHANGED
|
@@ -1,138 +1,61 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
2
|
import { Command } from "commander";
|
|
3
3
|
import { ConfigurationError, DEFAULT_MAX_DISPATCHES } from "@nodeint/migent-kernel";
|
|
4
4
|
import { dispatchAgent } from "./commands/agent.js";
|
|
5
5
|
import { runConfigModel, runConfigRole, runConfigShow } from "./commands/config.js";
|
|
6
6
|
import { runDoctor } from "./commands/doctor.js";
|
|
7
7
|
import { executeWorkflow } from "./commands/execute.js";
|
|
8
|
+
import { prepareWorkflowSession } from "./commands/prepare.js";
|
|
9
|
+
import { repairWorkflowSession } from "./commands/repair.js";
|
|
8
10
|
import { runInit } from "./commands/init.js";
|
|
9
|
-
import {
|
|
11
|
+
import { runSessionMetrics, runSessions } from "./commands/sessions.js";
|
|
10
12
|
import { parsePort, runServiceStart, runServiceStatus, runServiceStop } from "./commands/service.js";
|
|
11
13
|
import { installSkill, updateSkill } from "./commands/skill.js";
|
|
12
|
-
import { watchSession } from "./commands/watch.js";
|
|
13
14
|
import { runRegister, runRemove, runWorkspaceList } from "./commands/workspace-registry.js";
|
|
14
15
|
import { printError } from "./display.js";
|
|
16
|
+
import { LOOP_EXITS, STAGE_EXITS } from "./exits.js";
|
|
15
17
|
import { runMigrate } from "./migrate.js";
|
|
18
|
+
import { optionalText, rawText, text } from "./text.js";
|
|
19
|
+
import { PACKAGE_VERSION } from "./version.js";
|
|
16
20
|
import { findWorkspace, invocationCwdPath } from "./workspace.js";
|
|
17
|
-
const
|
|
18
|
-
const
|
|
19
|
-
|
|
20
|
-
"Exit 1 when the session is blocked.",
|
|
21
|
-
"Exit 2 on a configuration error.",
|
|
22
|
-
"Exit 3 when --attempts is spent. The session stays active.",
|
|
23
|
-
"Exit 4 when a route would exceed max_revisions. The session stays active.",
|
|
24
|
-
"Exit 5 when --max-dispatches is spent. The session stays active.",
|
|
25
|
-
"Exit 130 when cancelled.",
|
|
26
|
-
];
|
|
27
|
-
const CONTINUE_EXITS = [
|
|
28
|
-
"Exit 0 when the session is completed.",
|
|
29
|
-
"Exit 1 when the session is blocked.",
|
|
30
|
-
"Exit 2 on a configuration error.",
|
|
31
|
-
"Exit 3 when --attempts is spent. The session stays active.",
|
|
32
|
-
"Exit 4 when a route would exceed max_revisions. The session stays active.",
|
|
33
|
-
"Exit 5 when --max-dispatches is spent. The session stays active.",
|
|
34
|
-
"Exit 130 when cancelled.",
|
|
35
|
-
];
|
|
36
|
-
const STAGE_EXITS = [
|
|
37
|
-
"Exit 0 when the stage outcome is complete, or the session is already completed.",
|
|
38
|
-
"Exit 1 when the stage is not complete, or the session is blocked.",
|
|
39
|
-
"Exit 2 on a configuration error.",
|
|
40
|
-
"Exit 4 when a route would exceed max_revisions. The session stays active.",
|
|
41
|
-
"Exit 130 when cancelled.",
|
|
42
|
-
];
|
|
21
|
+
const PRIOR_HELP = "Completed session with the workflow id and decision named in requires. Checked before outputs are injected.";
|
|
22
|
+
const PROGRESS_HELP = "Print each tool call when it finishes. The default prints one line when a stage starts and one when it finishes.";
|
|
23
|
+
const VERBOSE_HELP = "Include every stage and its response text in the result JSON.";
|
|
43
24
|
export function createProgram(status) {
|
|
44
25
|
const program = new Command();
|
|
45
26
|
program
|
|
46
27
|
.name("migent")
|
|
47
28
|
.description("Run named workflows and standalone roles from .migent/.")
|
|
48
|
-
.version(
|
|
29
|
+
.version(PACKAGE_VERSION, "-V, --version", "Print the Migent version.")
|
|
49
30
|
.exitOverride();
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
.description("Create a session and continue until it stops.")
|
|
53
|
-
.argument("<workflow>", "Workflow id, or a path to a workflow file.")
|
|
54
|
-
.requiredOption("--task <task>", "Task recorded on the new session.");
|
|
55
|
-
addDispatchOptions(start);
|
|
56
|
-
start.addHelpText("after", exitText(START_EXITS)).action((workflow, options) => settle(status, () => {
|
|
57
|
-
const ceiling = dispatchCeiling(options);
|
|
58
|
-
if (typeof ceiling === "string")
|
|
59
|
-
return 2;
|
|
60
|
-
return executeWorkflow(findWorkspace(invocationCwdPath()), {
|
|
61
|
-
action: "start",
|
|
62
|
-
workflowId: workflow,
|
|
63
|
-
task: String(options.task),
|
|
64
|
-
priorSessionId: optionalText(options.prior),
|
|
65
|
-
maxAttempts: attemptsOf(options),
|
|
66
|
-
maxDispatches: ceiling,
|
|
67
|
-
});
|
|
68
|
-
}));
|
|
69
|
-
const continueCommand = program
|
|
70
|
-
.command("continue")
|
|
71
|
-
.description("Resume a session until it stops.")
|
|
72
|
-
.argument("<session>", "Session to resume.");
|
|
73
|
-
addDispatchOptions(continueCommand);
|
|
74
|
-
continueCommand.addHelpText("after", exitText(CONTINUE_EXITS)).action((session, options) => settle(status, () => {
|
|
75
|
-
const ceiling = dispatchCeiling(options);
|
|
76
|
-
if (typeof ceiling === "string")
|
|
77
|
-
return 2;
|
|
78
|
-
return executeWorkflow(findWorkspace(invocationCwdPath()), {
|
|
79
|
-
action: "continue",
|
|
80
|
-
sessionId: session,
|
|
81
|
-
priorSessionId: optionalText(options.prior),
|
|
82
|
-
maxAttempts: attemptsOf(options),
|
|
83
|
-
maxDispatches: ceiling,
|
|
84
|
-
});
|
|
85
|
-
}));
|
|
86
|
-
program
|
|
87
|
-
.command("stage")
|
|
88
|
-
.description("Advance one stage. Dispatch the next eligible stage once.")
|
|
89
|
-
.argument("[session]", "Session to advance. Omit when passing --workflow.")
|
|
90
|
-
.option("--workflow <workflow>", "Workflow id or path. Creates a session and dispatches its next stage once.")
|
|
91
|
-
.option("--task <task>", "Task recorded on the new session. Required with --workflow.")
|
|
92
|
-
.option("--prior <session>", "Completed session whose workflow and decision match requires.")
|
|
93
|
-
.addHelpText("after", exitText(STAGE_EXITS))
|
|
94
|
-
.action((session, options) => settle(status, () => {
|
|
95
|
-
const sessionId = text(session);
|
|
96
|
-
const workflow = optionalText(options.workflow);
|
|
97
|
-
const task = optionalText(options.task);
|
|
98
|
-
if (sessionId !== null && (workflow !== null || task !== null)) {
|
|
99
|
-
printError("Pass SESSION, or --workflow and --task, not both.");
|
|
100
|
-
return 2;
|
|
101
|
-
}
|
|
102
|
-
if (sessionId === null && (workflow === null || task === null)) {
|
|
103
|
-
printError("Pass SESSION, or both --workflow and --task.");
|
|
104
|
-
return 2;
|
|
105
|
-
}
|
|
106
|
-
return executeWorkflow(findWorkspace(invocationCwdPath()), {
|
|
107
|
-
action: "stage",
|
|
108
|
-
workflowId: workflow,
|
|
109
|
-
task,
|
|
110
|
-
sessionId,
|
|
111
|
-
priorSessionId: optionalText(options.prior),
|
|
112
|
-
maxAttempts: null,
|
|
113
|
-
maxDispatches: null,
|
|
114
|
-
});
|
|
115
|
-
}));
|
|
116
|
-
program
|
|
31
|
+
registerSessionCommand(program, status);
|
|
32
|
+
const init = program
|
|
117
33
|
.command("init")
|
|
118
34
|
.description("Create a minimal config, one workflow, and a sessions gitignore.")
|
|
119
35
|
.option("--provider <provider>", "Provider for the default model. Pass with --model.")
|
|
120
36
|
.option("--model <model>", "Model id for the default model. Pass with --provider.")
|
|
121
37
|
.option("--option <pair>", "KEY=VALUE stored under models.*.options.", collect, [])
|
|
122
38
|
.option("--preset <preset>", "Workflow to create: implement or plan. Default: implement.", "implement")
|
|
123
|
-
.
|
|
39
|
+
.option("--skill", "Install the Migent skill at .agents/skills/migent.")
|
|
40
|
+
.option("--no-input", "Do not prompt. Same as MIGENT_NO_INPUT=1.");
|
|
41
|
+
init.action((options) => settle(status, () => {
|
|
124
42
|
const provider = optionalText(options.provider);
|
|
125
43
|
const model = optionalText(options.model);
|
|
126
|
-
const interactive = provider === null && Boolean(process.stdin.isTTY);
|
|
44
|
+
const interactive = !inputDisabled(options) && provider === null && Boolean(process.stdin.isTTY);
|
|
127
45
|
return runInit(invocationCwdPath(), {
|
|
128
46
|
provider,
|
|
129
47
|
model,
|
|
130
48
|
options: stringList(options.option),
|
|
131
49
|
preset: typeof options.preset === "string" ? options.preset : "implement",
|
|
50
|
+
presetExplicit: init.getOptionValueSource("preset") === "cli",
|
|
51
|
+
skill: options.skill === true,
|
|
132
52
|
interactive,
|
|
133
53
|
});
|
|
134
54
|
}));
|
|
135
|
-
const config = program
|
|
55
|
+
const config = program
|
|
56
|
+
.command("config")
|
|
57
|
+
.description("Print .migent/config.yaml, or update models and roles.");
|
|
58
|
+
config.action(() => settle(status, () => runConfigShow(findWorkspace(invocationCwdPath()))));
|
|
136
59
|
config
|
|
137
60
|
.command("show")
|
|
138
61
|
.description("Print .migent/config.yaml.")
|
|
@@ -146,13 +69,14 @@ export function createProgram(status) {
|
|
|
146
69
|
.option("--option <pair>", "KEY=VALUE stored under this model's options.", collect, [])
|
|
147
70
|
.option("--clear-option <key>", "Remove one key from this model's options.", collect, [])
|
|
148
71
|
.option("--json", "Print one JSON object.")
|
|
72
|
+
.option("--no-input", "Do not prompt. Same as MIGENT_NO_INPUT=1. With no changes, list models.")
|
|
149
73
|
.action((name, options) => settle(status, () => {
|
|
150
74
|
const provider = optionalText(options.provider);
|
|
151
75
|
const model = optionalText(options.model);
|
|
152
76
|
const optionValues = stringList(options.option);
|
|
153
77
|
const clearOptions = stringList(options.clearOption);
|
|
154
78
|
const mutating = provider !== null || model !== null || optionValues.length > 0 || clearOptions.length > 0;
|
|
155
|
-
const interactive = Boolean(process.stdin.isTTY) && options.json !== true && !mutating;
|
|
79
|
+
const interactive = !inputDisabled(options) && Boolean(process.stdin.isTTY) && options.json !== true && !mutating;
|
|
156
80
|
return runConfigModel(findWorkspace(invocationCwdPath()), {
|
|
157
81
|
key: text(name),
|
|
158
82
|
provider,
|
|
@@ -167,12 +91,13 @@ export function createProgram(status) {
|
|
|
167
91
|
.command("role")
|
|
168
92
|
.description("List roles, or point roles at a model.")
|
|
169
93
|
.argument("[role...]", "Role to show or update.")
|
|
170
|
-
.option("--model-name <name>", "
|
|
94
|
+
.option("--model-name <name>", "models: name to point the roles at. This is not a provider model id.")
|
|
171
95
|
.option("--provider <provider>", "Provider for this role only. Pass with --model.")
|
|
172
|
-
.option("--model <model>", "
|
|
96
|
+
.option("--model <model>", "Provider model id for this role only. Pass with --provider.")
|
|
173
97
|
.option("--option <pair>", "KEY=VALUE stored under this role's options.", collect, [])
|
|
174
98
|
.option("--clear-option <key>", "Remove one key from this role's options.", collect, [])
|
|
175
99
|
.option("--json", "Print one JSON object.")
|
|
100
|
+
.option("--no-input", "Do not prompt. Same as MIGENT_NO_INPUT=1. With no changes, list roles.")
|
|
176
101
|
.action((roles, options) => settle(status, () => {
|
|
177
102
|
const unique = roles
|
|
178
103
|
.map((role) => text(role))
|
|
@@ -184,7 +109,7 @@ export function createProgram(status) {
|
|
|
184
109
|
const optionValues = stringList(options.option);
|
|
185
110
|
const clearOptions = stringList(options.clearOption);
|
|
186
111
|
const mutating = modelName !== null || provider !== null || model !== null || optionValues.length > 0 || clearOptions.length > 0;
|
|
187
|
-
const interactive = Boolean(process.stdin.isTTY) && options.json !== true && !mutating;
|
|
112
|
+
const interactive = !inputDisabled(options) && Boolean(process.stdin.isTTY) && options.json !== true && !mutating;
|
|
188
113
|
return runConfigRole(findWorkspace(invocationCwdPath()), {
|
|
189
114
|
roles: unique,
|
|
190
115
|
modelName,
|
|
@@ -196,55 +121,17 @@ export function createProgram(status) {
|
|
|
196
121
|
interactive,
|
|
197
122
|
});
|
|
198
123
|
}));
|
|
199
|
-
showHelpWhenBare(status, config);
|
|
200
124
|
program
|
|
201
125
|
.command("doctor")
|
|
202
126
|
.description("Check config, workflows, and provider CLIs.")
|
|
203
127
|
.option("--json", "Print the checks as one JSON object.")
|
|
128
|
+
.addHelpText("after", exitText(["Exit 0 when every check passes.", "Exit 1 when a check fails."]))
|
|
204
129
|
.action((options) => settle(status, () => runDoctor(findWorkspace(invocationCwdPath()), { asJson: options.json === true })));
|
|
205
130
|
program
|
|
206
131
|
.command("migrate")
|
|
207
132
|
.description("Upgrade config and workflows to the current schema version.")
|
|
208
133
|
.option("-y, --yes", "Upgrade without asking. Use this when no terminal is available.")
|
|
209
134
|
.action((options) => settle(status, () => runMigrate(findWorkspace(invocationCwdPath()), { assumeYes: options.yes === true })));
|
|
210
|
-
program
|
|
211
|
-
.command("sessions")
|
|
212
|
-
.description("List sessions and their latest status.")
|
|
213
|
-
.option("--json", "Print the same rows as a JSON array.")
|
|
214
|
-
.action((options) => settle(status, () => listSessions(findWorkspace(invocationCwdPath()), { asJson: options.json === true })));
|
|
215
|
-
program
|
|
216
|
-
.command("watch")
|
|
217
|
-
.description("Follow a session.")
|
|
218
|
-
.argument("<session>", "Session to follow.")
|
|
219
|
-
.option("--execution <execution>", "Limit followed events and --until to this execution.")
|
|
220
|
-
.option("--json", "With --once, print one JSON snapshot. While following, print JSON lines.")
|
|
221
|
-
.option("--cursor <offset>", "Replay events.jsonl from this byte offset. 0 replays the whole log.")
|
|
222
|
-
.option("--once", "Print the snapshot and events cursor, then exit.")
|
|
223
|
-
.option("--until", "Exit when --execution reaches completed, failed, or cancelled.")
|
|
224
|
-
.action((session, options) => settle(status, () => {
|
|
225
|
-
const cursor = options.cursor === undefined ? null : Number(options.cursor);
|
|
226
|
-
if (options.cursor !== undefined && (!Number.isInteger(cursor) || (cursor ?? 0) < 0)) {
|
|
227
|
-
throw new ConfigurationError("--cursor must be >= 0.");
|
|
228
|
-
}
|
|
229
|
-
return watchSession(findWorkspace(invocationCwdPath()), session, {
|
|
230
|
-
once: options.once === true,
|
|
231
|
-
executionId: optionalText(options.execution),
|
|
232
|
-
untilTerminal: options.until === true,
|
|
233
|
-
jsonLines: options.json === true,
|
|
234
|
-
cursor,
|
|
235
|
-
});
|
|
236
|
-
}));
|
|
237
|
-
const agent = program.command("agent").description("Run one role outside a workflow.");
|
|
238
|
-
const agentStart = agent.command("start").description("Create a session and dispatch one turn.");
|
|
239
|
-
addAgentOptions(agentStart);
|
|
240
|
-
agentStart.action((options) => settle(status, () => runAgent(options, null)));
|
|
241
|
-
const agentContinue = agent
|
|
242
|
-
.command("continue")
|
|
243
|
-
.description("Append a turn to a session.")
|
|
244
|
-
.argument("<session>", "Session to continue.");
|
|
245
|
-
addAgentOptions(agentContinue);
|
|
246
|
-
agentContinue.action((session, options) => settle(status, () => runAgent(options, session)));
|
|
247
|
-
showHelpWhenBare(status, agent);
|
|
248
135
|
const skill = program.command("skill").description("Install or update the Migent skill in this project.");
|
|
249
136
|
skill
|
|
250
137
|
.command("install")
|
|
@@ -268,12 +155,13 @@ export function createProgram(status) {
|
|
|
268
155
|
}));
|
|
269
156
|
service
|
|
270
157
|
.command("stop")
|
|
271
|
-
.description("Stop the registry service when its process identity matches.")
|
|
158
|
+
.description("Stop the registry service when its process identity matches. Stop is idempotent and exits 0 when the service is already absent.")
|
|
272
159
|
.action(() => settle(status, () => runServiceStop()));
|
|
273
160
|
service
|
|
274
161
|
.command("status")
|
|
275
|
-
.description("Report whether the registry service is running.")
|
|
162
|
+
.description("Report whether the registry service is running. Status is a query and exits 1 when the service is stopped.")
|
|
276
163
|
.option("--json", "Print one JSON object.")
|
|
164
|
+
.addHelpText("after", exitText(["Exit 0 when the service is running.", "Exit 1 when the service is stopped."]))
|
|
277
165
|
.action((options) => settle(status, () => runServiceStatus(options.json === true)));
|
|
278
166
|
showHelpWhenBare(status, service);
|
|
279
167
|
const workspace = program.command("workspace").description("Register this machine's Migent workspaces.");
|
|
@@ -306,24 +194,339 @@ export function createProgram(status) {
|
|
|
306
194
|
});
|
|
307
195
|
return program;
|
|
308
196
|
}
|
|
309
|
-
|
|
197
|
+
const ROLE_EXITS = [
|
|
198
|
+
"Exit 0 when the outcome is complete.",
|
|
199
|
+
"Exit 1 when the outcome is not complete.",
|
|
200
|
+
"Exit 2 on a configuration error.",
|
|
201
|
+
"Exit 130 when cancelled. A later run of this session resumes that execution.",
|
|
202
|
+
];
|
|
203
|
+
const RUN_SPLIT = "Pass --workflow for a workflow run, or --role for an agent run.";
|
|
204
|
+
function registerSessionCommand(program, status) {
|
|
205
|
+
const session = program.command("session").description("Create, run, resume, and inspect sessions.");
|
|
206
|
+
const run = session
|
|
207
|
+
.command("run")
|
|
208
|
+
.description("Run a workflow, or one role when --workflow is omitted.")
|
|
209
|
+
.option("--workflow <workflow>", "Workflow id, or a path to a workflow file. Omit to run one role.")
|
|
210
|
+
.option("--task <task>", "Workflow: one-line task stored in task.md. Role: label for a new session.")
|
|
211
|
+
.option("--session <session>", "Workflow: prepared session to start, with --workflow. Role: session to continue.");
|
|
212
|
+
addDispatchOptions(run, "Dispatch the next eligible stage once. Requires --workflow.");
|
|
213
|
+
run
|
|
214
|
+
.option("--role <role>", "Role to run. Omit --workflow.")
|
|
215
|
+
.option("--provider <provider>", "Provider for this role. Pass with --model, or omit both.")
|
|
216
|
+
.option("--model <model>", "Native model id for this role. Pass with --provider, or omit both.")
|
|
217
|
+
.option("--option <pair>", "KEY=VALUE overriding one provider option for this turn.", collect, [])
|
|
218
|
+
.option("--prompt <prompt>", "Prompt text for a role run.")
|
|
219
|
+
.option("--file <file>", "Read the role prompt from this file.")
|
|
220
|
+
.option("--stdin", "Read the role prompt from stdin.")
|
|
221
|
+
.option("--json", "Print the role result as one JSON object. A workflow run is already JSON.")
|
|
222
|
+
.addHelpText("after", `With --workflow:\n${exitText(LOOP_EXITS)}With --workflow and --once:\n${exitText(STAGE_EXITS)}Without --workflow:\n${exitText(ROLE_EXITS)}Examples:\n migent session run --workflow plan --task "<one line>"\n migent session run --session wave-3 --workflow plan\n migent session run --role reviewer --prompt "<prompt>"\n`)
|
|
223
|
+
.action((options) => settle(status, () => runSessionCommand(options)));
|
|
224
|
+
session
|
|
225
|
+
.command("prepare")
|
|
226
|
+
.description("Create a prepared session and exit. Write task.md, then session run --session <id> --workflow <workflow>.")
|
|
227
|
+
.argument("<id>", "Session id: a lowercase slug such as wave-3.")
|
|
228
|
+
.requiredOption("--workflow <workflow>", "Workflow id, or a path to a workflow file.")
|
|
229
|
+
.option("--prior <session>", PRIOR_HELP)
|
|
230
|
+
.addHelpText("after", `${exitText(["Exit 0 when the session is prepared.", "Exit 2 on a configuration error."])}Examples:\n migent session prepare wave-3 --workflow plan\n`)
|
|
231
|
+
.action((id, options) => settle(status, () => prepareWorkflowSession(findWorkspace(invocationCwdPath()), id, String(options.workflow), optionalText(options.prior))));
|
|
232
|
+
const continueCommand = session
|
|
233
|
+
.command("continue")
|
|
234
|
+
.description("Resume an active or blocked workflow session until it stops. Resets revision counts. --once dispatches one stage and does not reset them.")
|
|
235
|
+
.argument("<session>", "Session to resume.");
|
|
236
|
+
addDispatchOptions(continueCommand, "Dispatch the next eligible stage once and do not reset revision counts. Without --once, continue resets them.");
|
|
237
|
+
continueCommand
|
|
238
|
+
.option("--reply <text>", "Reply for the blocked stage. The new execution prompt body is this text.")
|
|
239
|
+
.option("--reply-file <file>", "Read the blocked stage's reply from this file.")
|
|
240
|
+
.addHelpText("after", `${exitText(LOOP_EXITS)}With --once:\n${exitText(STAGE_EXITS)}Examples:\n migent session continue <session> --once\n migent session continue <session> --reply "<reply>"\n migent session continue <session> --stage plan --reply-file answer.md\n`)
|
|
241
|
+
.action((id, options) => settle(status, () => continueSession(id, options)));
|
|
242
|
+
session
|
|
243
|
+
.command("repair")
|
|
244
|
+
.description("Rebuild status.json from the session directory.")
|
|
245
|
+
.argument("<session>", "Session to repair.")
|
|
246
|
+
.option("--accept-task", "Store the hash of the current task.md when status.json does not parse.")
|
|
247
|
+
.addHelpText("after", `${exitText([
|
|
248
|
+
"Exit 0 after status.json is rebuilt.",
|
|
249
|
+
"Exit 2 when the session cannot be repaired, or another process holds the session lock.",
|
|
250
|
+
])}Examples:\n migent session repair <session>\n migent session repair <session> --accept-task\n`)
|
|
251
|
+
.action((id, options) => settle(status, () => repairWorkflowSession(findWorkspace(invocationCwdPath()), id, options.acceptTask === true)));
|
|
252
|
+
session
|
|
253
|
+
.command("list")
|
|
254
|
+
.description("List sessions, newest first.")
|
|
255
|
+
.option("--json", "Print a JSON array without the task. With --latest, print one object, or null.")
|
|
256
|
+
.option("--workflow <id>", "List sessions for this workflow id.")
|
|
257
|
+
.option("--status <status>", "List sessions with this status: active, prepared, completed, blocked, cancelled, or corrupt.")
|
|
258
|
+
.option("--limit <n>", "List the newest n sessions. At least 1. Cannot be combined with --latest.")
|
|
259
|
+
.option("--latest", "Print the newest matching session. Cannot be combined with --limit.")
|
|
260
|
+
.addHelpText("after", `${exitText(["Exit 0 after a list, including an empty list.", "Exit 2 on a configuration error."])}Examples:\n migent session list\n migent session list --latest --json\n migent session list --workflow <id> --status completed --json\n`)
|
|
261
|
+
.action((options) => settle(status, () => runSessions(findWorkspace(invocationCwdPath()), undefined, {
|
|
262
|
+
asJson: options.json === true,
|
|
263
|
+
follow: false,
|
|
264
|
+
execution: null,
|
|
265
|
+
until: false,
|
|
266
|
+
cursor: undefined,
|
|
267
|
+
exitWhenDone: false,
|
|
268
|
+
workflow: typeof options.workflow === "string" ? options.workflow : null,
|
|
269
|
+
status: typeof options.status === "string" ? options.status : null,
|
|
270
|
+
limit: options.limit === undefined ? null : String(options.limit),
|
|
271
|
+
latest: options.latest === true,
|
|
272
|
+
})));
|
|
273
|
+
session
|
|
274
|
+
.command("show")
|
|
275
|
+
.description("Print one session snapshot.")
|
|
276
|
+
.argument("<session>", "Session to inspect.")
|
|
277
|
+
.option("--json", "Print one JSON object without the task.")
|
|
278
|
+
.addHelpText("after", `${exitText(["Exit 0 after a snapshot, including a blocked session.", "Exit 2 on a configuration error."])}Examples:\n migent session show <session>\n migent session show <session> --json\n`)
|
|
279
|
+
.action((id, options) => settle(status, () => runSessions(findWorkspace(invocationCwdPath()), id, {
|
|
280
|
+
asJson: options.json === true,
|
|
281
|
+
follow: false,
|
|
282
|
+
execution: null,
|
|
283
|
+
until: false,
|
|
284
|
+
cursor: undefined,
|
|
285
|
+
exitWhenDone: false,
|
|
286
|
+
workflow: null,
|
|
287
|
+
status: null,
|
|
288
|
+
limit: null,
|
|
289
|
+
latest: false,
|
|
290
|
+
})));
|
|
291
|
+
session
|
|
292
|
+
.command("metrics")
|
|
293
|
+
.description("Print derived usage, time, and revision counts.")
|
|
294
|
+
.argument("[session]", "Session to measure. Omit to list sessions.")
|
|
295
|
+
.option("--json", "Print one JSON object, or one object per line for a list.")
|
|
296
|
+
.option("--workflow <id>", "With a list, keep sessions for this workflow.")
|
|
297
|
+
.option("--status <status>", "With a list, keep sessions in this status.")
|
|
298
|
+
.option("--limit <n>", "With a list, keep the newest n sessions.")
|
|
299
|
+
.addHelpText("after", `${exitText([
|
|
300
|
+
"Exit 0 after one session object, or one object per line. An empty list prints nothing.",
|
|
301
|
+
"Exit 2 when --json is missing, the session has no events.jsonl, or a list flag is invalid.",
|
|
302
|
+
])}Examples:\n migent session metrics <session> --json\n migent session metrics --json --workflow <id> --status completed --limit 10\n`)
|
|
303
|
+
.action((id, options) => settle(status, () => runSessionMetrics(findWorkspace(invocationCwdPath()), id, {
|
|
304
|
+
asJson: options.json === true,
|
|
305
|
+
workflow: typeof options.workflow === "string" ? options.workflow : null,
|
|
306
|
+
status: typeof options.status === "string" ? options.status : null,
|
|
307
|
+
limit: options.limit === undefined ? null : String(options.limit),
|
|
308
|
+
})));
|
|
309
|
+
session
|
|
310
|
+
.command("follow")
|
|
311
|
+
.description("Follow a session's events until a signal.")
|
|
312
|
+
.argument("<session>", "Session to follow.")
|
|
313
|
+
.option("--json", "Print a snapshot object, then JSON lines. With --cursor, only JSON lines.")
|
|
314
|
+
.option("--execution <execution>", "Limit followed events and --until to this execution.")
|
|
315
|
+
.option("--cursor <offset>", "Byte offset in events.jsonl. 0 replays the whole log and every execution stream. Any other offset replays events from that byte, and stream lines whose timestamp is at or after the first event there. Skips the snapshot.")
|
|
316
|
+
.option("--until", "Exit when --execution reaches completed, failed, or cancelled. Requires --execution.")
|
|
317
|
+
.option("--exit-when-done", "Exit when the session is completed (0), blocked (1), or cancelled (130).")
|
|
318
|
+
.addHelpText("after", `${exitText([
|
|
319
|
+
"Exit 0 when --exit-when-done and the session is completed.",
|
|
320
|
+
"Exit 1 when --exit-when-done and the session is blocked.",
|
|
321
|
+
"Exit 130 when --exit-when-done and the session is cancelled, or the follow is cancelled.",
|
|
322
|
+
"Exit 2 on a configuration error.",
|
|
323
|
+
])}Examples:\n migent session follow <session>\n migent session follow <session> --exit-when-done\n`)
|
|
324
|
+
.action((id, options) => settle(status, () => runSessions(findWorkspace(invocationCwdPath()), id, {
|
|
325
|
+
asJson: options.json === true,
|
|
326
|
+
follow: true,
|
|
327
|
+
execution: optionalText(options.execution),
|
|
328
|
+
until: options.until === true,
|
|
329
|
+
cursor: options.cursor === undefined ? undefined : String(options.cursor),
|
|
330
|
+
exitWhenDone: options.exitWhenDone === true,
|
|
331
|
+
workflow: null,
|
|
332
|
+
status: null,
|
|
333
|
+
limit: null,
|
|
334
|
+
latest: false,
|
|
335
|
+
})));
|
|
336
|
+
showHelpWhenBare(status, session);
|
|
337
|
+
}
|
|
338
|
+
function runSessionCommand(options) {
|
|
339
|
+
if (options.stage !== undefined && optionalText(options.stage) === null) {
|
|
340
|
+
printError("--stage is empty.");
|
|
341
|
+
return 2;
|
|
342
|
+
}
|
|
343
|
+
const workflowProvided = options.workflow !== undefined;
|
|
344
|
+
const workflowId = optionalText(options.workflow);
|
|
345
|
+
if (workflowProvided && workflowId === null) {
|
|
346
|
+
printError("--workflow is empty.");
|
|
347
|
+
return 2;
|
|
348
|
+
}
|
|
349
|
+
const roleMode = roleFlagsPresent(options);
|
|
350
|
+
const workflowMode = workflowFlagsPresent(options);
|
|
351
|
+
if ((workflowId !== null && roleMode) || (workflowId === null && workflowMode)) {
|
|
352
|
+
printError(RUN_SPLIT);
|
|
353
|
+
return 2;
|
|
354
|
+
}
|
|
355
|
+
if (workflowId !== null) {
|
|
356
|
+
const sessionId = optionalText(options.session);
|
|
357
|
+
const task = rawText(options.task);
|
|
358
|
+
const prepared = sessionId !== null;
|
|
359
|
+
if (prepared && task !== null) {
|
|
360
|
+
printError("Pass --workflow and --task, or --session and --workflow, not both.");
|
|
361
|
+
return 2;
|
|
362
|
+
}
|
|
363
|
+
if (!prepared && task === null) {
|
|
364
|
+
printError("Pass --task.");
|
|
365
|
+
return 2;
|
|
366
|
+
}
|
|
367
|
+
return runWorkflowCommand({
|
|
368
|
+
workflowId,
|
|
369
|
+
task,
|
|
370
|
+
sessionId,
|
|
371
|
+
prepared,
|
|
372
|
+
once: options.once === true,
|
|
373
|
+
options,
|
|
374
|
+
});
|
|
375
|
+
}
|
|
376
|
+
if (!roleMode) {
|
|
377
|
+
printError(RUN_SPLIT);
|
|
378
|
+
return 2;
|
|
379
|
+
}
|
|
380
|
+
return runAgent(options, optionalText(options.session));
|
|
381
|
+
}
|
|
382
|
+
function roleFlagsPresent(options) {
|
|
383
|
+
return (options.role !== undefined ||
|
|
384
|
+
options.provider !== undefined ||
|
|
385
|
+
options.model !== undefined ||
|
|
386
|
+
stringList(options.option).length > 0 ||
|
|
387
|
+
options.prompt !== undefined ||
|
|
388
|
+
options.file !== undefined ||
|
|
389
|
+
options.stdin === true ||
|
|
390
|
+
options.json === true);
|
|
391
|
+
}
|
|
392
|
+
function workflowFlagsPresent(options) {
|
|
393
|
+
return (options.once === true ||
|
|
394
|
+
options.stage !== undefined ||
|
|
395
|
+
options.prior !== undefined ||
|
|
396
|
+
options.attempts !== undefined ||
|
|
397
|
+
options.maxDispatches !== undefined ||
|
|
398
|
+
options.unlimitedDispatches === true ||
|
|
399
|
+
options.progress === true ||
|
|
400
|
+
options.verbose === true ||
|
|
401
|
+
options.refreshWorkflow === true);
|
|
402
|
+
}
|
|
403
|
+
function runWorkflowCommand(args) {
|
|
404
|
+
if (rejectOnce(args.options))
|
|
405
|
+
return 2;
|
|
406
|
+
const workflowId = args.workflowId;
|
|
407
|
+
const task = args.prepared ? null : args.task;
|
|
408
|
+
const sessionId = args.prepared ? args.sessionId : null;
|
|
409
|
+
const stageId = optionalText(args.options.stage);
|
|
410
|
+
if (!args.once && stageId === null) {
|
|
411
|
+
const ceiling = dispatchCeiling(args.options);
|
|
412
|
+
if (typeof ceiling === "string")
|
|
413
|
+
return 2;
|
|
414
|
+
return executeWorkflow(findWorkspace(invocationCwdPath()), {
|
|
415
|
+
action: "start",
|
|
416
|
+
workflowId,
|
|
417
|
+
task,
|
|
418
|
+
sessionId,
|
|
419
|
+
startPrepared: args.prepared,
|
|
420
|
+
priorSessionId: optionalText(args.options.prior),
|
|
421
|
+
maxAttempts: attemptsOf(args.options),
|
|
422
|
+
maxDispatches: ceiling,
|
|
423
|
+
refreshWorkflow: args.options.refreshWorkflow === true,
|
|
424
|
+
...outputFlags(args.options),
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
return executeWorkflow(findWorkspace(invocationCwdPath()), {
|
|
428
|
+
action: "stage",
|
|
429
|
+
workflowId,
|
|
430
|
+
task,
|
|
431
|
+
sessionId,
|
|
432
|
+
stageId,
|
|
433
|
+
startPrepared: args.prepared,
|
|
434
|
+
priorSessionId: optionalText(args.options.prior),
|
|
435
|
+
maxAttempts: null,
|
|
436
|
+
maxDispatches: null,
|
|
437
|
+
refreshWorkflow: args.options.refreshWorkflow === true,
|
|
438
|
+
...outputFlags(args.options),
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
function continueSession(session, options) {
|
|
442
|
+
if (options.stage !== undefined && optionalText(options.stage) === null) {
|
|
443
|
+
printError("--stage is empty.");
|
|
444
|
+
return 2;
|
|
445
|
+
}
|
|
446
|
+
if (rejectOnce(options))
|
|
447
|
+
return 2;
|
|
448
|
+
const reply = readReply(options);
|
|
449
|
+
if (reply === "invalid")
|
|
450
|
+
return 2;
|
|
451
|
+
const stageId = optionalText(options.stage);
|
|
452
|
+
if (options.once === true || stageId !== null) {
|
|
453
|
+
return executeWorkflow(findWorkspace(invocationCwdPath()), {
|
|
454
|
+
action: "stage",
|
|
455
|
+
sessionId: session,
|
|
456
|
+
stageId,
|
|
457
|
+
priorSessionId: optionalText(options.prior),
|
|
458
|
+
maxAttempts: null,
|
|
459
|
+
maxDispatches: null,
|
|
460
|
+
refreshWorkflow: options.refreshWorkflow === true,
|
|
461
|
+
reply,
|
|
462
|
+
...outputFlags(options),
|
|
463
|
+
});
|
|
464
|
+
}
|
|
465
|
+
const ceiling = dispatchCeiling(options);
|
|
466
|
+
if (typeof ceiling === "string")
|
|
467
|
+
return 2;
|
|
468
|
+
return executeWorkflow(findWorkspace(invocationCwdPath()), {
|
|
469
|
+
action: "continue",
|
|
470
|
+
sessionId: session,
|
|
471
|
+
priorSessionId: optionalText(options.prior),
|
|
472
|
+
maxAttempts: attemptsOf(options),
|
|
473
|
+
maxDispatches: ceiling,
|
|
474
|
+
refreshWorkflow: options.refreshWorkflow === true,
|
|
475
|
+
reply,
|
|
476
|
+
...outputFlags(options),
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
function readReply(options) {
|
|
480
|
+
const hasReply = options.reply !== undefined;
|
|
481
|
+
const hasFile = options.replyFile !== undefined;
|
|
482
|
+
if (!hasReply && !hasFile)
|
|
483
|
+
return null;
|
|
484
|
+
if (hasReply && hasFile) {
|
|
485
|
+
printError("Pass only one of --reply or --reply-file.");
|
|
486
|
+
return "invalid";
|
|
487
|
+
}
|
|
488
|
+
if (hasFile) {
|
|
489
|
+
const file = optionalText(options.replyFile);
|
|
490
|
+
if (file === null) {
|
|
491
|
+
printError("--reply-file is empty.");
|
|
492
|
+
return "invalid";
|
|
493
|
+
}
|
|
494
|
+
let text;
|
|
495
|
+
try {
|
|
496
|
+
text = readFileSync(file, "utf8");
|
|
497
|
+
}
|
|
498
|
+
catch (error) {
|
|
499
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
500
|
+
printError(`Unable to read reply file: ${file} (${reason})`);
|
|
501
|
+
return "invalid";
|
|
502
|
+
}
|
|
503
|
+
if (text.trim() === "") {
|
|
504
|
+
printError("--reply is empty.");
|
|
505
|
+
return "invalid";
|
|
506
|
+
}
|
|
507
|
+
return text;
|
|
508
|
+
}
|
|
509
|
+
const reply = rawText(options.reply);
|
|
510
|
+
if (reply === null || reply.trim() === "") {
|
|
511
|
+
printError("--reply is empty.");
|
|
512
|
+
return "invalid";
|
|
513
|
+
}
|
|
514
|
+
return reply;
|
|
515
|
+
}
|
|
516
|
+
function addDispatchOptions(command, onceDescription) {
|
|
310
517
|
command
|
|
311
|
-
.option("--
|
|
518
|
+
.option("--once", onceDescription)
|
|
519
|
+
.option("--stage <stage>", "Dispatch this ready stage once. It does not have to be the first ready stage.")
|
|
520
|
+
.option("--prior <session>", PRIOR_HELP)
|
|
312
521
|
.option("--attempts <count>", "Failed executions allowed for one stage. Default: 3.")
|
|
313
522
|
.option("--max-dispatches <count>", `Stage dispatches allowed in this command. Default: ${DEFAULT_MAX_DISPATCHES}. Pass --unlimited-dispatches to remove the ceiling.`)
|
|
314
|
-
.option("--unlimited-dispatches", "Remove the dispatch ceiling for this command.")
|
|
523
|
+
.option("--unlimited-dispatches", "Remove the dispatch ceiling for this command.")
|
|
524
|
+
.option("--progress", PROGRESS_HELP)
|
|
525
|
+
.option("--verbose", VERBOSE_HELP)
|
|
526
|
+
.option("--refresh-workflow", "Use the current workflow, models, and role instructions for this session. Requires an existing session that has a frozen workflow.");
|
|
315
527
|
}
|
|
316
|
-
function
|
|
317
|
-
|
|
318
|
-
.requiredOption("--role <role>", "Key under roles in .migent/config.yaml.")
|
|
319
|
-
.option("--task <task>", "Label stored on the new session. Defaults to the role key.")
|
|
320
|
-
.option("--provider <provider>", "Provider for this role. Pass with --model, or omit both.")
|
|
321
|
-
.option("--model <model>", "Native model id for this role. Pass with --provider, or omit both.")
|
|
322
|
-
.option("--option <pair>", "KEY=VALUE overriding one provider option for this turn.", collect, [])
|
|
323
|
-
.option("--prompt <prompt>", "Prompt text.")
|
|
324
|
-
.option("--file <file>", "Read the prompt from this file.")
|
|
325
|
-
.option("--stdin", "Read the prompt from stdin.")
|
|
326
|
-
.option("--json", "Print the result as one JSON object.");
|
|
528
|
+
function outputFlags(options) {
|
|
529
|
+
return { verbose: options.verbose === true, itemProgress: options.progress === true };
|
|
327
530
|
}
|
|
328
531
|
function showHelpWhenBare(status, command) {
|
|
329
532
|
command.action(() => {
|
|
@@ -351,6 +554,11 @@ function stringList(value) {
|
|
|
351
554
|
return value.filter((item) => typeof item === "string");
|
|
352
555
|
}
|
|
353
556
|
async function runAgent(options, sessionId) {
|
|
557
|
+
const role = optionalText(options.role);
|
|
558
|
+
if (role === null) {
|
|
559
|
+
printError("Pass --role.");
|
|
560
|
+
return 2;
|
|
561
|
+
}
|
|
354
562
|
const provider = optionalText(options.provider);
|
|
355
563
|
const model = optionalText(options.model);
|
|
356
564
|
if ((provider === null) !== (model === null)) {
|
|
@@ -366,7 +574,7 @@ async function runAgent(options, sessionId) {
|
|
|
366
574
|
return 2;
|
|
367
575
|
}
|
|
368
576
|
return dispatchAgent(findWorkspace(invocationCwdPath()), {
|
|
369
|
-
role
|
|
577
|
+
role,
|
|
370
578
|
task: optionalText(options.task),
|
|
371
579
|
sessionId,
|
|
372
580
|
provider,
|
|
@@ -378,6 +586,24 @@ async function runAgent(options, sessionId) {
|
|
|
378
586
|
asJson: options.json === true,
|
|
379
587
|
});
|
|
380
588
|
}
|
|
589
|
+
function rejectOnce(options) {
|
|
590
|
+
if (options.once !== true && options.stage === undefined)
|
|
591
|
+
return false;
|
|
592
|
+
const flag = options.once === true ? "--once" : "--stage";
|
|
593
|
+
if (options.attempts !== undefined) {
|
|
594
|
+
printError(`${flag} cannot be combined with --attempts.`);
|
|
595
|
+
return true;
|
|
596
|
+
}
|
|
597
|
+
if (options.maxDispatches !== undefined) {
|
|
598
|
+
printError(`${flag} cannot be combined with --max-dispatches.`);
|
|
599
|
+
return true;
|
|
600
|
+
}
|
|
601
|
+
if (options.unlimitedDispatches === true) {
|
|
602
|
+
printError(`${flag} cannot be combined with --unlimited-dispatches.`);
|
|
603
|
+
return true;
|
|
604
|
+
}
|
|
605
|
+
return false;
|
|
606
|
+
}
|
|
381
607
|
function dispatchCeiling(options) {
|
|
382
608
|
const maxDispatches = optionalText(options.maxDispatches);
|
|
383
609
|
const unlimited = options.unlimitedDispatches === true;
|
|
@@ -403,12 +629,6 @@ function integerFlag(raw, flag) {
|
|
|
403
629
|
}
|
|
404
630
|
return Number(raw);
|
|
405
631
|
}
|
|
406
|
-
function
|
|
407
|
-
return
|
|
408
|
-
}
|
|
409
|
-
function text(value) {
|
|
410
|
-
if (typeof value !== "string")
|
|
411
|
-
return null;
|
|
412
|
-
const stripped = value.trim();
|
|
413
|
-
return stripped === "" ? null : stripped;
|
|
632
|
+
function inputDisabled(options) {
|
|
633
|
+
return options.noInput === true || process.env.MIGENT_NO_INPUT === "1";
|
|
414
634
|
}
|