@nodeint/migent 0.1.4 → 0.1.6

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