@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.
Files changed (43) hide show
  1. package/README.md +57 -41
  2. package/dist/commands/agent.js +111 -59
  3. package/dist/commands/config.js +20 -8
  4. package/dist/commands/execute.d.ts +6 -0
  5. package/dist/commands/execute.js +30 -14
  6. package/dist/commands/init.d.ts +2 -0
  7. package/dist/commands/init.js +91 -87
  8. package/dist/commands/prepare.d.ts +1 -0
  9. package/dist/commands/prepare.js +14 -0
  10. package/dist/commands/repair.d.ts +1 -0
  11. package/dist/commands/repair.js +22 -0
  12. package/dist/commands/service.js +5 -4
  13. package/dist/commands/sessions.d.ts +24 -1
  14. package/dist/commands/sessions.js +165 -13
  15. package/dist/commands/skill.js +51 -32
  16. package/dist/commands/watch.d.ts +2 -1
  17. package/dist/commands/watch.js +130 -67
  18. package/dist/commands/workspace-registry.js +10 -1
  19. package/dist/display.js +21 -9
  20. package/dist/errors.d.ts +3 -0
  21. package/dist/errors.js +6 -0
  22. package/dist/exits.d.ts +10 -0
  23. package/dist/exits.js +26 -0
  24. package/dist/follow-log.d.ts +29 -0
  25. package/dist/follow-log.js +172 -0
  26. package/dist/fs-utils.d.ts +3 -0
  27. package/dist/fs-utils.js +26 -0
  28. package/dist/index.js +5 -1
  29. package/dist/migrate.js +2 -17
  30. package/dist/program.js +388 -168
  31. package/dist/session-row.d.ts +20 -0
  32. package/dist/session-row.js +21 -0
  33. package/dist/signals.d.ts +3 -0
  34. package/dist/signals.js +56 -0
  35. package/dist/templates/migent/SKILL.md +159 -50
  36. package/dist/templates/migent/references/schema-config.yaml +35 -0
  37. package/dist/templates/migent/references/schema-workflow.yaml +61 -0
  38. package/dist/text.d.ts +4 -0
  39. package/dist/text.js +13 -0
  40. package/dist/version.d.ts +1 -0
  41. package/dist/version.js +3 -0
  42. package/dist/workspace.js +5 -12
  43. package/package.json +4 -4
package/dist/program.js CHANGED
@@ -1,138 +1,61 @@
1
- import { createRequire } from "node:module";
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 { listSessions } from "./commands/sessions.js";
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 { version } = createRequire(import.meta.url)("../package.json");
18
- const START_EXITS = [
19
- "Exit 0 when the session is completed.",
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(version, "-V, --version", "Print the Migent version.")
29
+ .version(PACKAGE_VERSION, "-V, --version", "Print the Migent version.")
49
30
  .exitOverride();
50
- const start = program
51
- .command("start")
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
- .action((options) => settle(status, () => {
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.command("config").description("Update models and roles in .migent/config.yaml.");
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>", "Point the roles at this model.")
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>", "Model id for this role only. Pass with --provider.")
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
- function addDispatchOptions(command) {
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("--prior <session>", "Completed session whose workflow and decision match requires.")
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 addAgentOptions(command) {
317
- command
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: String(options.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 optionalText(value) {
407
- return text(typeof value === "string" ? value : null);
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
  }