@nodeint/migent 0.1.4 → 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 +30 -14
  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 +1 -0
  7. package/dist/commands/init.js +70 -63
  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 +386 -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/README.md CHANGED
@@ -49,7 +49,7 @@ the process, while each provider CLI remains responsible for authentication,
49
49
  agent execution, and its own tools and options.
50
50
 
51
51
  ```mermaid
52
- flowchart LR
52
+ flowchart TD
53
53
  A[Task] --> B[Planner - Codex]
54
54
  B -->|plan.md| C[Reviewer - Grok]
55
55
  C -->|revise| B
@@ -61,11 +61,12 @@ flowchart LR
61
61
  Migent drives each provider through its headless CLI. Install and sign in to the
62
62
  ones your configuration names.
63
63
 
64
- | Provider | CLI |
65
- | -------- | -------- |
66
- | Claude | `claude` |
67
- | Codex | `codex` |
68
- | Grok | `grok` |
64
+ | Provider | CLI |
65
+ | -------------------| -------- |
66
+ | Google Antigravity | `agy` |
67
+ | Claude | `claude` |
68
+ | Codex | `codex` |
69
+ | Grok | `grok` |
69
70
 
70
71
  ## Quickstart
71
72
 
@@ -114,18 +115,32 @@ Commit `config.yaml` and `workflows/`. To add the skill later, run
114
115
  From the project or any subdirectory:
115
116
 
116
117
  ```bash
117
- migent start plan --task "Plan the account settings redesign"
118
+ migent session run --workflow plan --task "Plan the account settings redesign"
118
119
  ```
119
120
 
120
- Migent runs each eligible stage until the workflow completes or stops. Progress
121
- goes to stderr; stdout is a single JSON result. Then inspect or resume:
121
+ `--task` is one line and is stored in the session as `task.md`. For a longer
122
+ task, create the session first, write the file, then start it:
122
123
 
123
124
  ```bash
124
- migent sessions
125
- migent watch <session-id>
126
- migent continue <session-id>
125
+ migent session prepare wave-3 --workflow plan
126
+ migent session run --session wave-3 --workflow plan
127
127
  ```
128
128
 
129
+ Migent runs each eligible stage until the workflow completes or stops. Omit
130
+ `--workflow` and pass `--role` to run one role instead. A workflow run prints
131
+ one JSON result with the session id, status, stop reason, decision, and output
132
+ paths. A role run prints a table unless `--json`. Stderr prints one line when
133
+ a stage starts and one when it finishes. Then inspect or resume:
134
+
135
+ ```bash
136
+ migent session list
137
+ migent session list --latest --json
138
+ migent session follow <session-id>
139
+ migent session continue <session-id>
140
+ ```
141
+
142
+ `session list --json` and a JSON snapshot omit the task text.
143
+
129
144
  ### Next
130
145
 
131
146
  - [Providers and model configuration](https://github.com/nodeint/migent/blob/main/docs/providers.md)
@@ -147,8 +162,9 @@ A workflow connects stages through dependencies and explicit decision routes.
147
162
  - A **session** records executions, events, decisions, and artifacts so work can
148
163
  be inspected or resumed.
149
164
 
150
- Use `depends_on` to give a stage upstream artifacts. Use `session.resume_from`
151
- when the stage must also continue a provider's prior conversational session.
165
+ Use `depends_on` to give a stage upstream artifacts. Every visit of a stage
166
+ continues that stage's provider agent. Use `session.resume_from` when a stage
167
+ must continue a different stage's provider session.
152
168
 
153
169
  ## Current scope
154
170
 
@@ -1,7 +1,9 @@
1
- import { AgentToolRunner, CancellationRequested, ConfigurationError, createAgentSession, log, raiseCancellation, requireAgentSession, resolveRoleTarget, SessionStore, } from "@nodeint/migent-kernel";
1
+ import path from "node:path";
2
+ import { AgentToolRunner, CancellationRequested, ConfigurationError, createAgentSession, loadRoleInstructions, loadYamlMapping, log, prependRoleInstructions, requireAgentSession, isSessionLockLost, repairSessionUnlocked, SessionLock, resolveRoleTarget, SessionStore, } from "@nodeint/migent-kernel";
2
3
  import { printAgent, printError, printNotice } from "../display.js";
3
4
  import { cliHooks } from "../hooks.js";
4
5
  import { requireCurrentSchema } from "../migrate.js";
6
+ import { bindCancellationSignals } from "../signals.js";
5
7
  import { adapterMap, parseOptions } from "../provider.js";
6
8
  import { resolvePrompt } from "../workspace.js";
7
9
  export async function dispatchAgent(workspace, args) {
@@ -13,76 +15,126 @@ export async function dispatchAgent(workspace, args) {
13
15
  options: Object.keys(parsed).length > 0 ? parsed : null,
14
16
  });
15
17
  const [resolvedSessionId, createdSession] = resolveSession(workspace, args.sessionId ?? null, args.role, args.task ?? null);
16
- let resumeExecutionId = null;
17
- if (!createdSession) {
18
- const previous = new SessionStore(workspace).latestExecution(resolvedSessionId);
19
- if (previous !== null)
20
- resumeExecutionId = previous.executionId;
18
+ if (createdSession)
19
+ log(`session_id: ${resolvedSessionId}`);
20
+ let lock;
21
+ try {
22
+ lock = SessionLock.acquire(path.join(workspace, ".migent", "sessions", resolvedSessionId));
21
23
  }
22
- const optionLabel = Object.entries(target.options)
23
- .map(([key, value]) => `${key}: ${value}`)
24
- .join(", ") || "default";
25
- printNotice(`agent ${args.role} · ${target.provider}/${target.model} (${optionLabel}).`);
26
- const onSigint = () => {
27
- try {
28
- raiseCancellation(2);
29
- }
30
- catch {
31
- // The runner observes the pending cancellation.
32
- }
33
- };
34
- const onSigterm = () => {
35
- try {
36
- raiseCancellation(15);
37
- }
38
- catch {
39
- // The runner observes the pending cancellation.
24
+ catch (error) {
25
+ if (error instanceof Error) {
26
+ printError(error.message);
27
+ return 2;
40
28
  }
41
- };
42
- process.on("SIGINT", onSigint);
43
- process.on("SIGTERM", onSigterm);
44
- let result;
29
+ throw error;
30
+ }
45
31
  try {
46
- const runner = args.runner ?? cliHooks.runner ?? new AgentToolRunner(adapterMap());
47
- result = await runner.run({
48
- provider: target.provider,
49
- model: target.model,
50
- options: { ...target.options },
51
- prompt: resolvePrompt({
52
- prompt: args.prompt ?? null,
53
- promptFile: args.promptFile ?? null,
54
- promptStdin: args.promptStdin ?? false,
55
- }),
56
- workspace,
57
- sessionId: resolvedSessionId,
58
- resumeExecutionId,
59
- stageId: args.role,
60
- });
32
+ return await runLocked();
61
33
  }
62
34
  catch (error) {
63
- if (error instanceof CancellationRequested) {
64
- log(error.message);
65
- return 130;
66
- }
67
- if (error instanceof ConfigurationError)
68
- throw error;
69
- if (error instanceof Error) {
35
+ if (isSessionLockLost(error)) {
70
36
  printError(error.message);
71
37
  return 2;
72
38
  }
73
39
  throw error;
74
40
  }
75
41
  finally {
76
- process.off("SIGINT", onSigint);
77
- process.off("SIGTERM", onSigterm);
42
+ lock.release();
43
+ }
44
+ async function runLocked() {
45
+ let resumeExecutionId = null;
46
+ let resumeCancelled = false;
47
+ if (!createdSession) {
48
+ repairSessionUnlocked(workspace, resolvedSessionId);
49
+ const store = new SessionStore(workspace);
50
+ if (store.readSessionStatus(resolvedSessionId).status === "cancelled") {
51
+ resumeCancelled = true;
52
+ }
53
+ else {
54
+ const previous = store.latestExecution(resolvedSessionId);
55
+ if (previous !== null)
56
+ resumeExecutionId = previous.executionId;
57
+ }
58
+ }
59
+ const optionLabel = Object.entries(target.options)
60
+ .map(([key, value]) => `${key}: ${value}`)
61
+ .join(", ") || "default";
62
+ printNotice(`agent ${args.role} · ${target.provider}/${target.model} (${optionLabel}).`);
63
+ const unbind = bindCancellationSignals();
64
+ let result;
65
+ let resumedMany = null;
66
+ try {
67
+ const runner = args.runner ?? cliHooks.runner ?? new AgentToolRunner(adapterMap());
68
+ if (resumeCancelled) {
69
+ const reopened = new SessionStore(workspace).reopenCancelledExecutions(resolvedSessionId);
70
+ for (const item of reopened) {
71
+ log(`session ${resolvedSessionId} was cancelled during ${item.executionId}. Resuming that execution.`);
72
+ }
73
+ const settled = await Promise.allSettled(reopened.map((item) => runner.continueCancelled(workspace, resolvedSessionId, item.executionId)));
74
+ const rejected = settled.find((item) => item.status === "rejected");
75
+ if (rejected && rejected.status === "rejected")
76
+ throw rejected.reason;
77
+ const continued = settled.flatMap((item) => (item.status === "fulfilled" ? [item.value] : []));
78
+ if (continued.length === 0) {
79
+ throw new ConfigurationError(`Session ${resolvedSessionId} is cancelled without an execution.`);
80
+ }
81
+ if (continued.length === 1)
82
+ result = continued[0];
83
+ else
84
+ resumedMany = continued;
85
+ }
86
+ else {
87
+ result = await runner.run({
88
+ provider: target.provider,
89
+ model: target.model,
90
+ options: { ...target.options },
91
+ prompt: prependRoleInstructions(loadRoleInstructions(loadYamlMapping(path.join(workspace, ".migent", "config.yaml")), args.role), resolvePrompt({
92
+ prompt: args.prompt ?? null,
93
+ promptFile: args.promptFile ?? null,
94
+ promptStdin: args.promptStdin ?? false,
95
+ })),
96
+ workspace,
97
+ sessionId: resolvedSessionId,
98
+ resumeExecutionId,
99
+ stageId: args.role,
100
+ });
101
+ }
102
+ }
103
+ catch (error) {
104
+ if (error instanceof CancellationRequested) {
105
+ log(error.message);
106
+ return 130;
107
+ }
108
+ if (error instanceof ConfigurationError)
109
+ throw error;
110
+ if (error instanceof Error) {
111
+ printError(error.message);
112
+ return 2;
113
+ }
114
+ throw error;
115
+ }
116
+ finally {
117
+ unbind();
118
+ }
119
+ if (resumedMany !== null) {
120
+ if (args.asJson)
121
+ process.stdout.write(`${JSON.stringify(resumedMany)}\n`);
122
+ else {
123
+ for (const item of resumedMany)
124
+ printAgent({ ...item }, args.role, target.provider, target.model);
125
+ }
126
+ if (resumedMany.some((item) => item.failure_reason === "verify_exhausted"))
127
+ return 6;
128
+ return resumedMany.every((item) => item.outcome_status === "complete") ? 0 : 1;
129
+ }
130
+ if (result === undefined)
131
+ throw new Error("Agent run produced no result.");
132
+ if (args.asJson)
133
+ process.stdout.write(`${JSON.stringify(result)}\n`);
134
+ else
135
+ printAgent({ ...result }, args.role, target.provider, target.model);
136
+ return result.outcome_status === "complete" ? 0 : 1;
78
137
  }
79
- if (createdSession)
80
- printNotice(`session_id: ${result.session_id}`);
81
- if (args.asJson)
82
- process.stdout.write(`${JSON.stringify(result)}\n`);
83
- else
84
- printAgent({ ...result }, args.role, target.provider, target.model);
85
- return result.outcome_status === "complete" ? 0 : 1;
86
138
  }
87
139
  function resolveSession(workspace, sessionId, role, task) {
88
140
  if (sessionId !== null && typeof task === "string" && task.trim() !== "") {
@@ -3,6 +3,7 @@ import path from "node:path";
3
3
  import { confirm as clackConfirm, isCancel, select as clackSelect, text as clackText } from "@clack/prompts";
4
4
  import { ConfigEditError, ConfigurationError, assignRoles, describeChange, diagnose, inspectConfig, loadModelTarget, loadWorkflowCatalog, loadYamlMapping, matchingModelName, patchConfigText, placeRoleModel, resolveStageSpec, setRoleOptions, upsertModel, whichSync, } from "@nodeint/migent-kernel";
5
5
  import { printError } from "../display.js";
6
+ import { PromptCancelled } from "../errors.js";
6
7
  import { cliHooks } from "../hooks.js";
7
8
  import { requireCurrentSchema } from "../migrate.js";
8
9
  import { acceptedOptions, adapterMap, asCatalog, detected, knownModel, loadCatalog, loadOptionValues, parseOptions, requireAdapter, runProviderCommand, } from "../provider.js";
@@ -25,6 +26,8 @@ export async function runConfigModel(workspace, options = {}) {
25
26
  });
26
27
  }
27
28
  catch (error) {
29
+ if (error instanceof PromptCancelled)
30
+ throw error;
28
31
  if (error instanceof ConfigurationError || error instanceof ConfigEditError || error instanceof Error) {
29
32
  printError(error.message);
30
33
  return 2;
@@ -43,6 +46,8 @@ export async function runConfigRole(workspace, options = {}) {
43
46
  });
44
47
  }
45
48
  catch (error) {
49
+ if (error instanceof PromptCancelled)
50
+ throw error;
46
51
  if (error instanceof ConfigurationError || error instanceof ConfigEditError || error instanceof Error) {
47
52
  printError(error.message);
48
53
  return 2;
@@ -382,7 +387,7 @@ function optionUpdatesOf(options, clearOptions) {
382
387
  async function selectProvider(prompter, adapters, which, message, defaultValue) {
383
388
  const rows = detected(adapters, which);
384
389
  if (rows.length === 0)
385
- throw new Error("No providers are available.");
390
+ throw new ConfigurationError("No providers are available.");
386
391
  const names = new Set(rows.map(([name]) => name));
387
392
  return prompter.select(message, rows.map(([name, found]) => [`${name} ${found ?? "not found"}`, name]), defaultValue !== null && names.has(defaultValue) ? defaultValue : rows[0]?.[0]);
388
393
  }
@@ -486,6 +491,7 @@ function roleJson(role) {
486
491
  provider: role.provider,
487
492
  model: role.model,
488
493
  options: { ...role.options },
494
+ instructions: [...role.instructions],
489
495
  shared_with: [...role.sharedWith],
490
496
  stages: role.stages.map((stage) => ({
491
497
  workflow: stage.workflow,
@@ -586,7 +592,7 @@ function isRecord(value) {
586
592
  class ClackConfigPrompter {
587
593
  async select(message, choices, defaultValue) {
588
594
  if (choices.length === 0)
589
- throw new Error("Choose a value.");
595
+ throw new ConfigurationError("Choose a value.");
590
596
  const values = new Set(choices.map(([, value]) => value));
591
597
  const initial = defaultValue != null && values.has(defaultValue) ? defaultValue : choices[0]?.[1];
592
598
  const result = await clackSelect({
@@ -594,20 +600,26 @@ class ClackConfigPrompter {
594
600
  options: choices.map(([label, value]) => ({ label, value })),
595
601
  initialValue: initial,
596
602
  });
597
- if (isCancel(result) || typeof result !== "string" || !values.has(result))
598
- throw new Error("Choose a value.");
603
+ if (isCancel(result))
604
+ throw new PromptCancelled();
605
+ if (typeof result !== "string" || !values.has(result))
606
+ throw new ConfigurationError("Choose a value.");
599
607
  return result;
600
608
  }
601
609
  async confirm(message) {
602
610
  const result = await clackConfirm({ message, initialValue: true });
603
- if (isCancel(result) || typeof result !== "boolean")
604
- throw new Error("Answer yes or no.");
611
+ if (isCancel(result))
612
+ throw new PromptCancelled();
613
+ if (typeof result !== "boolean")
614
+ throw new ConfigurationError("Answer yes or no.");
605
615
  return result;
606
616
  }
607
617
  async text(message, defaultValue = "") {
608
618
  const result = await clackText({ message, initialValue: defaultValue });
609
- if (isCancel(result) || typeof result !== "string")
610
- throw new Error("Enter a value.");
619
+ if (isCancel(result))
620
+ throw new PromptCancelled();
621
+ if (typeof result !== "string")
622
+ throw new ConfigurationError("Enter a value.");
611
623
  return result;
612
624
  }
613
625
  }
@@ -7,6 +7,12 @@ export interface ExecuteWorkflowOptions {
7
7
  priorSessionId?: string | null;
8
8
  maxAttempts?: number | null;
9
9
  maxDispatches?: number | null;
10
+ stageId?: string | null;
10
11
  runner?: AgentToolRunner | null;
12
+ verbose?: boolean;
13
+ itemProgress?: boolean;
14
+ startPrepared?: boolean;
15
+ refreshWorkflow?: boolean;
16
+ reply?: string | null;
11
17
  }
12
18
  export declare function executeWorkflow(workspace: string, options: ExecuteWorkflowOptions): Promise<number>;
@@ -2,38 +2,54 @@ import { AgentToolRunner, DEFAULT_MAX_DISPATCHES, executeNamedWorkflow, } from "
2
2
  import { cliHooks } from "../hooks.js";
3
3
  import { requireCurrentSchema } from "../migrate.js";
4
4
  import { adapterMap } from "../provider.js";
5
+ import { text } from "../text.js";
5
6
  export async function executeWorkflow(workspace, options) {
6
7
  requireCurrentSchema(workspace);
7
8
  const once = options.action === "stage";
8
- const creating = options.action === "start" || (once && text(options.sessionId) === null);
9
+ const startPrepared = options.startPrepared === true;
10
+ const sessionId = text(options.sessionId);
11
+ const creating = !startPrepared && (options.action === "start" || (once && sessionId === null));
9
12
  const maxAttempts = options.maxAttempts === undefined ? 3 : options.maxAttempts;
10
13
  const maxDispatches = options.maxDispatches === undefined ? DEFAULT_MAX_DISPATCHES : options.maxDispatches;
11
14
  const runner = options.runner ?? cliHooks.runner ?? new AgentToolRunner(adapterMap());
15
+ runner.itemProgress = options.itemProgress === true;
12
16
  const result = await executeNamedWorkflow(workspace, {
13
- workflowId: creating ? (options.workflowId ?? null) : null,
17
+ workflowId: creating || startPrepared ? (options.workflowId ?? null) : null,
14
18
  task: creating ? (options.task ?? null) : null,
15
- sessionId: creating ? null : (options.sessionId ?? null),
19
+ sessionId: creating ? null : sessionId,
16
20
  priorSessionId: options.priorSessionId ?? null,
21
+ startPrepared,
17
22
  maxAttempts: once ? null : maxAttempts,
18
23
  maxDispatches: once ? null : maxDispatches,
24
+ stageId: options.stageId ?? null,
19
25
  once,
20
26
  resetRevisions: options.action === "continue",
27
+ refreshWorkflow: options.refreshWorkflow === true,
28
+ reply: options.reply ?? null,
21
29
  }, runner);
22
- printResult(result);
30
+ printResult(result, options.verbose === true);
31
+ if (result.stopReason === "retry_exhausted" ||
32
+ result.stopReason === "revisions_exhausted" ||
33
+ result.stopReason === "dispatch_limit" ||
34
+ result.stopReason === "verify_exhausted") {
35
+ process.stderr.write(`Resume with: migent session continue ${result.sessionId}\n`);
36
+ }
23
37
  return result.code;
24
38
  }
25
- function printResult(result) {
26
- process.stdout.write(`${JSON.stringify({
39
+ const INSPECT_EXIT_CODES = new Set([1, 3, 6]);
40
+ function printResult(result, verbose) {
41
+ const payload = {
27
42
  session_id: result.sessionId,
28
43
  session_status: result.status,
29
44
  stop_reason: result.stopReason,
45
+ latest_decision: result.latestDecision,
30
46
  outputs: result.outputs,
31
- stages: result.stages,
32
- })}\n`);
33
- }
34
- function text(value) {
35
- if (typeof value !== "string")
36
- return null;
37
- const stripped = value.trim();
38
- return stripped === "" ? null : stripped;
47
+ };
48
+ if (INSPECT_EXIT_CODES.has(result.code) && result.latestExecutionId) {
49
+ payload.latest_execution_id = result.latestExecutionId;
50
+ payload.response_path = `executions/${result.latestExecutionId}/response.md`;
51
+ }
52
+ if (verbose)
53
+ payload.stages = result.stages;
54
+ process.stdout.write(`${JSON.stringify(payload)}\n`);
39
55
  }
@@ -18,6 +18,7 @@ export declare function runInit(workspace: string, options?: {
18
18
  model?: string | null;
19
19
  options?: readonly string[];
20
20
  preset?: string;
21
+ presetExplicit?: boolean;
21
22
  skill?: boolean;
22
23
  interactive?: boolean;
23
24
  adapters?: Record<string, AgentAdapter> | null;