@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
@@ -1,16 +1,20 @@
1
- import { mkdirSync, readdirSync, statSync, writeFileSync } from "node:fs";
1
+ import { mkdirSync, readdirSync, writeFileSync } from "node:fs";
2
2
  import path from "node:path";
3
3
  import { confirm as clackConfirm, isCancel, select as clackSelect } from "@clack/prompts";
4
- import { diagnose, whichSync, yamlScalar } from "@nodeint/migent-kernel";
4
+ import { ConfigurationError, diagnose, whichSync, yamlScalar, } from "@nodeint/migent-kernel";
5
+ import { PromptCancelled } from "../errors.js";
6
+ import { exists, isDirectory, isFile } from "../fs-utils.js";
5
7
  import { printAlreadyInitialized, printCreated, printError, printInitConflicts, } from "../display.js";
6
8
  import { cliHooks } from "../hooks.js";
7
9
  import { runDoctor } from "./doctor.js";
10
+ import { installSkill } from "./skill.js";
8
11
  import { acceptedOptions, adapterMap, asCatalog, detected, knownModel, loadCatalog, loadOptionValues, parseOptions, requireAdapter, runProviderCommand, } from "../provider.js";
9
12
  export const PRESETS = ["implement", "plan"];
10
13
  export async function runInit(workspace, options = {}) {
11
14
  const provider = options.provider ?? null;
12
15
  const model = options.model ?? null;
13
16
  const preset = options.preset ?? "implement";
17
+ const skill = options.skill === true;
14
18
  const interactive = options.interactive ?? false;
15
19
  const chosen = options.adapters ?? adapterMap();
16
20
  const resolve = options.which ?? cliHooks.which ?? whichSync;
@@ -20,11 +24,23 @@ export async function runInit(workspace, options = {}) {
20
24
  printError("preset must be implement or plan.");
21
25
  return 2;
22
26
  }
27
+ const optionPairs = options.options ?? [];
28
+ const classified = classify(workspace, chosen, resolve);
29
+ if (classified.state === "ready") {
30
+ if (provider !== null || model !== null || optionPairs.length > 0) {
31
+ printError("Already initialized; ignoring --provider/--model/--option.");
32
+ }
33
+ if (options.presetExplicit === true)
34
+ printError("Already initialized; ignoring --preset.");
35
+ printAlreadyInitialized();
36
+ if (skill)
37
+ return installSkill(workspace);
38
+ return 0;
39
+ }
23
40
  if ((provider === null) !== (model === null)) {
24
41
  printError("Pass both --provider and --model, or omit both.");
25
42
  return 2;
26
43
  }
27
- const optionPairs = options.options ?? [];
28
44
  if (optionPairs.length > 0 && provider === null) {
29
45
  printError("Pass --provider and --model with --option.");
30
46
  return 2;
@@ -33,34 +49,58 @@ export async function runInit(workspace, options = {}) {
33
49
  printError("Pass --provider and --model when init is not interactive.");
34
50
  return 2;
35
51
  }
36
- const classified = classify(workspace, chosen, resolve);
37
- if (classified.state === "ready") {
38
- printAlreadyInitialized();
39
- return 0;
40
- }
41
52
  if (classified.state === "conflict") {
42
53
  printInitConflicts(classified.conflicts);
43
54
  return 1;
44
55
  }
45
56
  let primary;
46
- let reviewer;
47
57
  try {
48
- [primary, reviewer] = await choices(chosen, resolve, prompt, {
58
+ primary = await choices(chosen, resolve, prompt, {
49
59
  provider,
50
60
  model,
51
61
  options: parseOptions(optionPairs),
52
- preset,
53
- interactive,
54
62
  runCommand: execute,
55
63
  });
56
64
  }
57
65
  catch (error) {
66
+ if (error instanceof PromptCancelled)
67
+ throw error;
58
68
  printError(error instanceof Error ? error.message : String(error));
59
69
  return 2;
60
70
  }
61
- const paths = writeProject(workspace, preset, primary, reviewer);
62
- printCreated(paths, `migent start ${preset} --task "..."`);
63
- return runDoctor(workspace, { which: resolve });
71
+ let install = skill;
72
+ if (!install && interactive) {
73
+ try {
74
+ install = await prompt.confirm("Install the Migent skill in .agents/skills/migent?");
75
+ }
76
+ catch (error) {
77
+ if (error instanceof PromptCancelled)
78
+ throw error;
79
+ printError(error instanceof Error ? error.message : String(error));
80
+ return 2;
81
+ }
82
+ }
83
+ if (install && installSkill(workspace) !== 0)
84
+ return 1;
85
+ const paths = writeProject(workspace, preset, primary);
86
+ printCreated(paths, `migent session run --workflow ${preset} --task "..."`);
87
+ if (interactive) {
88
+ const doctorCode = await runDoctor(workspace, { which: resolve });
89
+ if (doctorCode !== 0)
90
+ writeDoctorProblems(workspace, chosen, resolve);
91
+ }
92
+ else {
93
+ writeDoctorProblems(workspace, chosen, resolve);
94
+ }
95
+ return 0;
96
+ }
97
+ function writeDoctorProblems(workspace, adapters, which) {
98
+ const problems = diagnose(workspace, { adapters, which }).flatMap((check) => check.problems);
99
+ if (problems.length === 0)
100
+ return;
101
+ process.stderr.write("Doctor reported problems:\n");
102
+ for (const problem of problems)
103
+ process.stderr.write(`${problem}\n`);
64
104
  }
65
105
  async function choices(adapters, which, prompt, args) {
66
106
  let provider = args.provider;
@@ -71,7 +111,7 @@ async function choices(adapters, which, prompt, args) {
71
111
  provider = await prompt.selectProvider(rows, "Default provider");
72
112
  const adapter = adapters[provider];
73
113
  if (adapter === undefined)
74
- throw new Error(`Unknown provider: ${provider}.`);
114
+ throw new ConfigurationError(`Unknown provider: ${provider}.`);
75
115
  const catalog = await loadCatalog(adapter, which, args.runCommand);
76
116
  model = await prompt.selectModel(provider, catalog, "Model");
77
117
  stored = await promptOptions(adapter, model, catalog, which, args.runCommand, prompt);
@@ -79,27 +119,15 @@ async function choices(adapters, which, prompt, args) {
79
119
  else {
80
120
  const adapter = requireAdapter(adapters, provider);
81
121
  if (model === null)
82
- throw new Error("Model is required.");
122
+ throw new ConfigurationError("Model is required.");
83
123
  const catalog = await loadCatalog(adapter, which, args.runCommand);
84
124
  model = knownModel(model, catalog);
85
125
  stored = await acceptedOptions(adapter, model, catalog, which, args.runCommand, args.options);
86
126
  }
87
127
  if (model.trim() === "")
88
- throw new Error("Model is required.");
128
+ throw new ConfigurationError("Model is required.");
89
129
  const primary = { provider, model: model.trim(), options: stored };
90
- if (args.preset !== "plan" || !args.interactive)
91
- return [primary, null];
92
- if (await prompt.confirm("Use the default model for both planner and reviewer?"))
93
- return [primary, null];
94
- const rows = detected(adapters, which);
95
- const reviewerProvider = await prompt.selectProvider(rows, "Reviewer provider");
96
- const reviewerAdapter = adapters[reviewerProvider];
97
- if (reviewerAdapter === undefined)
98
- throw new Error(`Unknown provider: ${reviewerProvider}.`);
99
- const reviewerCatalog = await loadCatalog(reviewerAdapter, which, args.runCommand);
100
- const reviewerModel = (await prompt.selectModel(reviewerProvider, reviewerCatalog, "Reviewer model")).trim();
101
- const reviewerOptions = await promptOptions(reviewerAdapter, reviewerModel, reviewerCatalog, which, args.runCommand, prompt);
102
- return [primary, { provider: reviewerProvider, model: reviewerModel, options: reviewerOptions }];
130
+ return primary;
103
131
  }
104
132
  function classify(workspace, adapters, which) {
105
133
  const root = path.join(workspace, ".migent");
@@ -133,28 +161,24 @@ function classify(workspace, adapters, which) {
133
161
  }
134
162
  return { state: "conflict", conflicts: owned };
135
163
  }
136
- function writeProject(workspace, preset, primary, reviewer) {
164
+ function writeProject(workspace, preset, primary) {
137
165
  const root = path.join(workspace, ".migent");
138
166
  const workflows = path.join(root, "workflows");
139
167
  mkdirSync(workflows, { recursive: true });
140
- writeFileSync(path.join(root, "config.yaml"), renderConfig(preset, primary, reviewer), "utf8");
168
+ writeFileSync(path.join(root, "config.yaml"), renderConfig(preset, primary), "utf8");
141
169
  const workflowName = `${preset}.yaml`;
142
170
  writeFileSync(path.join(workflows, workflowName), renderWorkflow(preset), "utf8");
143
171
  writeFileSync(path.join(root, ".gitignore"), "sessions/\n", "utf8");
144
172
  return [".migent/config.yaml", `.migent/workflows/${workflowName}`, ".migent/.gitignore"];
145
173
  }
146
- function renderConfig(preset, primary, reviewer) {
174
+ function renderConfig(preset, primary) {
147
175
  const models = [["default", primary]];
148
176
  let roles;
149
177
  if (preset === "implement")
150
178
  roles = [["developer", "default"]];
151
- else if (reviewer === null)
179
+ else
152
180
  roles = [["planner", "default"], ["reviewer", "default"]];
153
- else {
154
- models.push(["review", reviewer]);
155
- roles = [["planner", "default"], ["reviewer", "review"]];
156
- }
157
- const lines = ["schema_version: 1", "models:"];
181
+ const lines = ["schema_version: 2", "models:"];
158
182
  for (const [name, choice] of models) {
159
183
  lines.push(` ${name}:`, ` provider: ${yamlScalar(choice.provider)}`, ` model: ${yamlScalar(choice.model)}`);
160
184
  const entries = Object.entries(choice.options);
@@ -171,7 +195,7 @@ function renderConfig(preset, primary, reviewer) {
171
195
  }
172
196
  function renderWorkflow(preset) {
173
197
  if (preset === "implement") {
174
- return ("schema_version: 1\n" +
198
+ return ("schema_version: 2\n" +
175
199
  "id: implement\n" +
176
200
  "stages:\n" +
177
201
  " - id: implement\n" +
@@ -179,11 +203,9 @@ function renderWorkflow(preset) {
179
203
  " depends_on: []\n" +
180
204
  " instructions:\n" +
181
205
  " - Implement the requested change.\n" +
182
- " - Verify the result with relevant checks or tests.\n" +
183
- "completion:\n" +
184
- " stage: implement\n");
206
+ " - Verify the result with relevant checks or tests.\n");
185
207
  }
186
- return ("schema_version: 1\n" +
208
+ return ("schema_version: 2\n" +
187
209
  "id: plan\n" +
188
210
  "stages:\n" +
189
211
  " - id: plan\n" +
@@ -192,6 +214,7 @@ function renderWorkflow(preset) {
192
214
  " max_revisions: 3\n" +
193
215
  " instructions:\n" +
194
216
  " - Analyze the task and write an implementation plan.\n" +
217
+ " - When revising, edit the existing plan.md in place. Do not replace the whole file.\n" +
195
218
  " produces:\n" +
196
219
  " artifact: plan.md\n" +
197
220
  " - id: review-plan\n" +
@@ -200,6 +223,7 @@ function renderWorkflow(preset) {
200
223
  " - plan\n" +
201
224
  " instructions:\n" +
202
225
  " - Review the plan for correctness, missing risks, and unnecessary scope.\n" +
226
+ " - Report every gap of the same kind in one response.\n" +
203
227
  " decision:\n" +
204
228
  " values:\n" +
205
229
  " - approved\n" +
@@ -207,13 +231,10 @@ function renderWorkflow(preset) {
207
231
  " routes:\n" +
208
232
  " approved: complete\n" +
209
233
  " revise: plan\n" +
210
- "completion:\n" +
211
- " stage: review-plan\n" +
212
- " decision: approved\n" +
213
- " outputs:\n" +
214
- " - name: plan\n" +
215
- " from_stage: plan\n" +
216
- " artifact: plan.md\n");
234
+ "outputs:\n" +
235
+ " - name: plan\n" +
236
+ " from_stage: plan\n" +
237
+ " artifact: plan.md\n");
217
238
  }
218
239
  async function promptOptions(adapter, model, catalog, which, runCommand, prompt) {
219
240
  const stored = {};
@@ -235,28 +256,32 @@ function providerOptions(adapter) {
235
256
  class ClackPrompter {
236
257
  async selectProvider(rows, message) {
237
258
  if (rows.length === 0)
238
- throw new Error("No providers are available.");
259
+ throw new ConfigurationError("No providers are available.");
239
260
  const names = new Set(rows.map(([name]) => name));
240
261
  const result = await clackSelect({
241
262
  message,
242
263
  options: rows.map(([name, found]) => ({ value: name, label: `${name} ${found ?? "not found"}` })),
243
264
  initialValue: rows[0]?.[0],
244
265
  });
245
- if (isCancel(result) || typeof result !== "string" || !names.has(result))
246
- throw new Error("Provider is required.");
266
+ if (isCancel(result))
267
+ throw new PromptCancelled();
268
+ if (typeof result !== "string" || !names.has(result))
269
+ throw new ConfigurationError("Provider is required.");
247
270
  return result;
248
271
  }
249
272
  async selectModel(_provider, catalog, message) {
250
273
  if (catalog.ids.length === 0)
251
- throw new Error("Model is required.");
274
+ throw new ConfigurationError("Model is required.");
252
275
  const initial = catalog.defaultId && catalog.ids.includes(catalog.defaultId) ? catalog.defaultId : catalog.ids[0];
253
276
  const result = await clackSelect({
254
277
  message,
255
278
  options: catalog.ids.map((id) => ({ value: id, label: id })),
256
279
  initialValue: initial,
257
280
  });
258
- if (isCancel(result) || !catalog.ids.includes(String(result)))
259
- throw new Error(`Unknown model: ${String(result)}.`);
281
+ if (isCancel(result))
282
+ throw new PromptCancelled();
283
+ if (!catalog.ids.includes(String(result)))
284
+ throw new ConfigurationError(`Unknown model: ${String(result)}.`);
260
285
  return String(result);
261
286
  }
262
287
  async selectOption(name, values, allowDefault) {
@@ -272,40 +297,19 @@ class ClackPrompter {
272
297
  initialValue: allowDefault ? "" : values[0],
273
298
  });
274
299
  const allowed = new Set(allowDefault ? ["", ...values] : values);
275
- if (isCancel(result) || !allowed.has(String(result ?? ""))) {
276
- throw new Error(`Unknown ${name} value: ${String(result)}.`);
300
+ if (isCancel(result))
301
+ throw new PromptCancelled();
302
+ if (!allowed.has(String(result ?? ""))) {
303
+ throw new ConfigurationError(`Unknown ${name} value: ${String(result)}.`);
277
304
  }
278
305
  return result == null ? "" : String(result);
279
306
  }
280
307
  async confirm(message) {
281
308
  const result = await clackConfirm({ message, initialValue: true });
282
- if (isCancel(result) || typeof result !== "boolean")
283
- throw new Error("Answer yes or no.");
309
+ if (isCancel(result))
310
+ throw new PromptCancelled();
311
+ if (typeof result !== "boolean")
312
+ throw new ConfigurationError("Answer yes or no.");
284
313
  return result;
285
314
  }
286
315
  }
287
- function exists(filePath) {
288
- try {
289
- statSync(filePath);
290
- return true;
291
- }
292
- catch {
293
- return false;
294
- }
295
- }
296
- function isFile(filePath) {
297
- try {
298
- return statSync(filePath).isFile();
299
- }
300
- catch {
301
- return false;
302
- }
303
- }
304
- function isDirectory(filePath) {
305
- try {
306
- return statSync(filePath).isDirectory();
307
- }
308
- catch {
309
- return false;
310
- }
311
- }
@@ -0,0 +1 @@
1
+ export declare function prepareWorkflowSession(workspace: string, sessionId: string, workflowId: string, priorSessionId: string | null): number;
@@ -0,0 +1,14 @@
1
+ import { createPreparedSession, log, relativeTaskPath } from "@nodeint/migent-kernel";
2
+ import { requireCurrentSchema } from "../migrate.js";
3
+ export function prepareWorkflowSession(workspace, sessionId, workflowId, priorSessionId) {
4
+ requireCurrentSchema(workspace);
5
+ const id = createPreparedSession(workspace, sessionId, workflowId, priorSessionId);
6
+ const payload = {
7
+ session_id: id,
8
+ status: "prepared",
9
+ task_path: relativeTaskPath(id),
10
+ };
11
+ process.stdout.write(`${JSON.stringify(payload)}\n`);
12
+ log(`session_id: ${id}`);
13
+ return 0;
14
+ }
@@ -0,0 +1 @@
1
+ export declare function repairWorkflowSession(workspace: string, sessionId: string, acceptTask: boolean): number;
@@ -0,0 +1,22 @@
1
+ import { isSessionLockLost, repairSession, SessionLockHeldError } from "@nodeint/migent-kernel";
2
+ import { printError } from "../display.js";
3
+ import { requireCurrentSchema } from "../migrate.js";
4
+ export function repairWorkflowSession(workspace, sessionId, acceptTask) {
5
+ requireCurrentSchema(workspace);
6
+ try {
7
+ const status = repairSession(workspace, sessionId, { acceptTask });
8
+ const payload = {
9
+ session_id: sessionId,
10
+ status: typeof status.status === "string" ? status.status : null,
11
+ };
12
+ process.stdout.write(`${JSON.stringify(payload)}\n`);
13
+ return 0;
14
+ }
15
+ catch (error) {
16
+ if (error instanceof SessionLockHeldError || isSessionLockLost(error)) {
17
+ printError(error.message);
18
+ return 2;
19
+ }
20
+ throw error;
21
+ }
22
+ }
@@ -34,6 +34,7 @@ export async function runServiceStop() {
34
34
  printError(result.message || "Registry service did not stop.");
35
35
  return 1;
36
36
  }
37
+ process.stderr.write(result.outcome === "absent" ? "Not running.\n" : "Stopped.\n");
37
38
  return 0;
38
39
  }
39
40
  export async function runServiceStatus(asJson) {
@@ -48,13 +49,13 @@ export async function runServiceStatus(asJson) {
48
49
  else {
49
50
  process.stdout.write("stopped\n");
50
51
  }
51
- return 0;
52
+ return report.state === "running" ? 0 : 1;
52
53
  }
53
54
  export function parsePort(raw) {
54
55
  if (!/^[0-9]+$/.test(raw))
55
- throw new ConfigurationError("--port must be an integer.");
56
+ throw new ConfigurationError("--port must be an integer from 1 to 65535.");
56
57
  const port = Number(raw);
57
- if (port > 65535)
58
- throw new ConfigurationError("--port must be an integer.");
58
+ if (port < 1 || port > 65535)
59
+ throw new ConfigurationError("--port must be an integer from 1 to 65535.");
59
60
  return port;
60
61
  }
@@ -1,3 +1,26 @@
1
- export declare function listSessions(workspace: string, options?: {
1
+ export interface SessionListOptions {
2
2
  asJson?: boolean;
3
+ workflow?: string | null;
4
+ status?: string | null;
5
+ limit?: number | null;
6
+ latest?: boolean;
7
+ }
8
+ export declare function listSessions(workspace: string, options?: SessionListOptions): number;
9
+ export declare function runSessionMetrics(workspace: string, session: string | undefined, options: {
10
+ asJson: boolean;
11
+ workflow: string | null;
12
+ status: string | null;
13
+ limit: string | null;
3
14
  }): number;
15
+ export declare function runSessions(workspace: string, session: string | undefined, options: {
16
+ asJson: boolean;
17
+ follow: boolean;
18
+ execution: string | null;
19
+ until: boolean;
20
+ cursor: string | undefined;
21
+ exitWhenDone: boolean;
22
+ workflow: string | null;
23
+ status: string | null;
24
+ limit: string | null;
25
+ latest: boolean;
26
+ }): Promise<number>;
@@ -1,20 +1,172 @@
1
- import { listSessionRows } from "@nodeint/migent-kernel";
2
- import { printSessions } from "../display.js";
1
+ import { existsSync } from "node:fs";
2
+ import path from "node:path";
3
+ import { listSessionMetrics, listSessionRows, sessionMetrics, ConfigurationError } from "@nodeint/migent-kernel";
4
+ import { printError, printSessions } from "../display.js";
5
+ import { adapterMap } from "../provider.js";
6
+ import { SESSION_COLUMN, sessionFields } from "../session-row.js";
7
+ import { optionalText } from "../text.js";
8
+ import { followSession } from "./watch.js";
9
+ const SESSION_STATUSES = new Set(["active", "prepared", "completed", "blocked", "cancelled", "corrupt"]);
3
10
  export function listSessions(workspace, options = {}) {
4
- const rows = listSessionRows(workspace);
11
+ const rows = selectSessionRows(listSessionRows(workspace), options);
12
+ if (options.latest) {
13
+ const row = rows[0];
14
+ if (options.asJson) {
15
+ process.stdout.write(`${JSON.stringify(row === undefined ? null : sessionJson(row))}\n`);
16
+ return 0;
17
+ }
18
+ printSessions(row === undefined ? [] : [row]);
19
+ return 0;
20
+ }
5
21
  if (options.asJson) {
6
- const payload = rows.map((row) => ({
7
- session: row[1] ?? "",
8
- workflow: row[2] ?? "",
9
- status: row[3] ?? "",
10
- execution: row[4] ?? "",
11
- decision: row[5] ?? "",
12
- outputs: row[6] ?? "",
13
- task: row[7] ?? "",
14
- }));
15
- process.stdout.write(`${JSON.stringify(payload)}\n`);
22
+ process.stdout.write(`${JSON.stringify(rows.map((row) => sessionJson(row)))}\n`);
16
23
  return 0;
17
24
  }
18
25
  printSessions(rows);
19
26
  return 0;
20
27
  }
28
+ export function runSessionMetrics(workspace, session, options) {
29
+ if (!options.asJson) {
30
+ printError("session metrics requires --json.");
31
+ return 2;
32
+ }
33
+ const selection = parseListSelection({ ...options, latest: false });
34
+ const sessionId = optionalText(session);
35
+ if (sessionId !== null && hasListFilter(selection)) {
36
+ printError("List filters cannot be combined with a session.");
37
+ return 2;
38
+ }
39
+ const adapters = adapterMap();
40
+ if (sessionId === null) {
41
+ const rows = listSessionMetrics(workspace, adapters, {
42
+ workflow: selection.workflow ?? null,
43
+ status: selection.status ?? null,
44
+ limit: selection.limit ?? null,
45
+ });
46
+ for (const row of rows)
47
+ process.stdout.write(`${JSON.stringify(row)}\n`);
48
+ return 0;
49
+ }
50
+ const eventsPath = path.join(workspace, ".migent", "sessions", sessionId, "events.jsonl");
51
+ if (!existsSync(eventsPath)) {
52
+ printError(`No events found for session: ${sessionId}`);
53
+ return 2;
54
+ }
55
+ process.stdout.write(`${JSON.stringify(sessionMetrics(workspace, sessionId, adapters))}\n`);
56
+ return 0;
57
+ }
58
+ export async function runSessions(workspace, session, options) {
59
+ const selection = parseListSelection(options);
60
+ const sessionId = optionalText(session);
61
+ if (sessionId !== null && hasListFilter(selection)) {
62
+ printError("List filters cannot be combined with a session.");
63
+ return 2;
64
+ }
65
+ if (sessionId === null)
66
+ return listOnly(workspace, options, selection);
67
+ if (options.until && !options.follow) {
68
+ printError("--until requires --follow.");
69
+ return 2;
70
+ }
71
+ if (options.cursor !== undefined && !options.follow) {
72
+ printError("--cursor requires --follow.");
73
+ return 2;
74
+ }
75
+ if (options.exitWhenDone && !options.follow) {
76
+ printError("--exit-when-done requires --follow.");
77
+ return 2;
78
+ }
79
+ if (options.until && options.execution === null) {
80
+ printError("--until requires --execution.");
81
+ return 2;
82
+ }
83
+ const cursor = parseCursor(options.cursor);
84
+ return followSession(workspace, sessionId, {
85
+ once: !options.follow,
86
+ executionId: options.execution,
87
+ untilTerminal: options.until,
88
+ jsonLines: options.asJson,
89
+ cursor,
90
+ exitWhenDone: options.exitWhenDone,
91
+ });
92
+ }
93
+ function listOnly(workspace, options, selection) {
94
+ if (options.follow) {
95
+ printError("--follow requires a session.");
96
+ return 2;
97
+ }
98
+ if (options.until) {
99
+ printError("--until requires --follow.");
100
+ return 2;
101
+ }
102
+ if (options.cursor !== undefined) {
103
+ printError("--cursor requires --follow.");
104
+ return 2;
105
+ }
106
+ if (options.exitWhenDone) {
107
+ printError("--exit-when-done requires --follow.");
108
+ return 2;
109
+ }
110
+ if (options.execution !== null) {
111
+ printError("--execution requires a session.");
112
+ return 2;
113
+ }
114
+ return listSessions(workspace, { ...selection, asJson: options.asJson });
115
+ }
116
+ function selectSessionRows(rows, options) {
117
+ let selected = [...rows];
118
+ if (options.workflow != null) {
119
+ selected = selected.filter((row) => row[SESSION_COLUMN.workflow] === options.workflow);
120
+ }
121
+ if (options.status != null) {
122
+ selected = selected.filter((row) => row[SESSION_COLUMN.status] === options.status);
123
+ }
124
+ const limit = options.latest ? 1 : (options.limit ?? null);
125
+ if (limit !== null)
126
+ return selected.slice(0, limit);
127
+ return selected;
128
+ }
129
+ function sessionJson(row) {
130
+ const fields = sessionFields(row);
131
+ return {
132
+ session: fields.session,
133
+ workflow: fields.workflow,
134
+ status: fields.status,
135
+ execution: fields.execution,
136
+ decision: fields.decision,
137
+ outputs: fields.outputs,
138
+ };
139
+ }
140
+ function hasListFilter(selection) {
141
+ return selection.latest === true || selection.workflow != null || selection.status != null || selection.limit != null;
142
+ }
143
+ function parseListSelection(options) {
144
+ const workflow = optionalText(options.workflow);
145
+ if (options.workflow !== null && workflow === null)
146
+ throw new ConfigurationError("--workflow is empty.");
147
+ const status = optionalText(options.status);
148
+ if (options.status !== null && status === null)
149
+ throw new ConfigurationError("--status is empty.");
150
+ if (status !== null && !SESSION_STATUSES.has(status)) {
151
+ throw new ConfigurationError("--status must be active, prepared, completed, blocked, cancelled, or corrupt.");
152
+ }
153
+ let limit = null;
154
+ if (options.limit !== null) {
155
+ if (!/^[0-9]+$/.test(options.limit))
156
+ throw new ConfigurationError("--limit must be an integer >= 1.");
157
+ limit = Number(options.limit);
158
+ if (limit < 1)
159
+ throw new ConfigurationError("--limit must be an integer >= 1.");
160
+ }
161
+ if (options.latest && limit !== null)
162
+ throw new ConfigurationError("--latest cannot be combined with --limit.");
163
+ return { workflow, status, limit, latest: options.latest };
164
+ }
165
+ function parseCursor(raw) {
166
+ if (raw === undefined)
167
+ return null;
168
+ const cursor = Number(raw);
169
+ if (!Number.isInteger(cursor) || cursor < 0)
170
+ throw new ConfigurationError("--cursor must be >= 0.");
171
+ return cursor;
172
+ }