@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,20 +1,21 @@
1
1
  import { createHash } from "node:crypto";
2
- import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync } from "node:fs";
2
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
3
3
  import path from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
5
  import { printError } from "../display.js";
6
+ import { isDirectory, isFile } from "../fs-utils.js";
6
7
  const SKILL_RELATIVE = path.join(".agents", "skills", "migent");
7
8
  const STATE_FILENAME = ".migent-template.json";
8
9
  const TEMPLATE_VERSION = 1;
10
+ const MANAGED = ["SKILL.md", "references/schema-config.yaml", "references/schema-workflow.yaml"];
9
11
  export function installSkill(workspace) {
10
12
  const skillDirectory = path.join(workspace, SKILL_RELATIVE);
11
13
  if (existsSync(skillDirectory)) {
12
14
  printError(`${displayPath(skillDirectory, workspace)} already exists; use \`migent skill update\`.`);
13
15
  return 1;
14
16
  }
15
- const content = templateContent();
16
17
  mkdirSync(skillDirectory, { recursive: true });
17
- writeManagedSkill(skillDirectory, content);
18
+ writeManagedSkill(skillDirectory, readTemplate());
18
19
  process.stdout.write(`Installed ${displayPath(skillDirectory, workspace)}\n`);
19
20
  return 0;
20
21
  }
@@ -31,32 +32,56 @@ export function updateSkill(workspace, options = {}) {
31
32
  printError("Existing Migent skill is not managed by Migent; move it aside and run `migent skill install`.");
32
33
  return 1;
33
34
  }
34
- const current = readFileSync(skillPath, "utf8");
35
35
  const installedDigest = state.content_sha256;
36
- const currentDigest = digest(current);
37
- if (installedDigest !== currentDigest && !options.force) {
36
+ if (typeof installedDigest !== "string") {
37
+ printError("Existing Migent skill is not managed by Migent; move it aside and run `migent skill install`.");
38
+ return 1;
39
+ }
40
+ const installed = readInstalled(skillDirectory);
41
+ if (hasLocalChanges(installed, installedDigest) && !options.force) {
38
42
  printError("Migent skill has local changes; rerun with `migent skill update --force` to replace them.");
39
43
  return 1;
40
44
  }
41
- const content = templateContent();
42
- if (current === content && installedDigest === currentDigest && state.template_version === TEMPLATE_VERSION) {
45
+ const template = readTemplate();
46
+ if (bundleDigest(installed) === bundleDigest(template) && state.template_version === TEMPLATE_VERSION) {
43
47
  process.stdout.write(`Already up to date: ${displayPath(skillDirectory, workspace)}\n`);
44
48
  return 0;
45
49
  }
46
- writeManagedSkill(skillDirectory, content);
50
+ writeManagedSkill(skillDirectory, template);
47
51
  process.stdout.write(`Updated ${displayPath(skillDirectory, workspace)}\n`);
48
52
  return 0;
49
53
  }
50
- function templateContent() {
51
- const nextToModule = fileURLToPath(new URL("../templates/migent/SKILL.md", import.meta.url));
52
- return readFileSync(nextToModule, "utf8");
54
+ function readTemplate() {
55
+ const root = fileURLToPath(new URL("../templates/migent/", import.meta.url));
56
+ return MANAGED.map((name) => [name, readFileSync(path.join(root, name), "utf8")]);
53
57
  }
54
- function writeManagedSkill(skillDirectory, content) {
55
- writeFileSync(path.join(skillDirectory, "SKILL.md"), content, "utf8");
58
+ function readInstalled(skillDirectory) {
59
+ const entries = [];
60
+ for (const name of MANAGED) {
61
+ const filePath = path.join(skillDirectory, name);
62
+ if (!isFile(filePath))
63
+ continue;
64
+ entries.push([name, readFileSync(filePath, "utf8")]);
65
+ }
66
+ return entries;
67
+ }
68
+ function hasLocalChanges(installed, installedDigest) {
69
+ if (bundleDigest(installed) === installedDigest)
70
+ return false;
71
+ const skill = installed.find(([name]) => name === "SKILL.md");
72
+ const references = installed.filter(([name]) => name !== "SKILL.md");
73
+ return !(skill !== undefined && references.length === 0 && skillOnlyDigest(skill[1]) === installedDigest);
74
+ }
75
+ function writeManagedSkill(skillDirectory, entries) {
76
+ for (const [name, content] of entries) {
77
+ const filePath = path.join(skillDirectory, name);
78
+ mkdirSync(path.dirname(filePath), { recursive: true });
79
+ writeFileSync(filePath, content, "utf8");
80
+ }
56
81
  const state = {
57
82
  schema_version: 1,
58
83
  template_version: TEMPLATE_VERSION,
59
- content_sha256: digest(content),
84
+ content_sha256: bundleDigest(entries),
60
85
  };
61
86
  writeFileSync(path.join(skillDirectory, STATE_FILENAME), `${JSON.stringify(state, null, 2)}\n`, "utf8");
62
87
  }
@@ -71,7 +96,17 @@ function readState(filePath) {
71
96
  return null;
72
97
  }
73
98
  }
74
- function digest(content) {
99
+ function bundleDigest(entries) {
100
+ const hash = createHash("sha256");
101
+ for (const [name, content] of entries) {
102
+ hash.update(name);
103
+ hash.update("\0");
104
+ hash.update(content);
105
+ hash.update("\0");
106
+ }
107
+ return hash.digest("hex");
108
+ }
109
+ function skillOnlyDigest(content) {
75
110
  return createHash("sha256").update(content, "utf8").digest("hex");
76
111
  }
77
112
  function displayPath(filePath, workspace) {
@@ -80,19 +115,3 @@ function displayPath(filePath, workspace) {
80
115
  function isRecord(value) {
81
116
  return typeof value === "object" && value !== null && !Array.isArray(value);
82
117
  }
83
- function isFile(filePath) {
84
- try {
85
- return statSync(filePath).isFile();
86
- }
87
- catch {
88
- return false;
89
- }
90
- }
91
- function isDirectory(filePath) {
92
- try {
93
- return statSync(filePath).isDirectory();
94
- }
95
- catch {
96
- return false;
97
- }
98
- }
@@ -1,7 +1,8 @@
1
- export declare function watchSession(workspace: string, sessionId: string, options?: {
1
+ export declare function followSession(workspace: string, sessionId: string, options?: {
2
2
  once?: boolean;
3
3
  executionId?: string | null;
4
4
  untilTerminal?: boolean;
5
5
  jsonLines?: boolean;
6
6
  cursor?: number | null;
7
+ exitWhenDone?: boolean;
7
8
  }): Promise<number>;
@@ -1,27 +1,43 @@
1
- import { openSync, readSync, closeSync, statSync } from "node:fs";
1
+ import { openSync, readFileSync, readSync, closeSync, statSync } from "node:fs";
2
2
  import path from "node:path";
3
- import { CancellationRequested, ConfigurationError, SessionStore, log, raiseCancellation, readExecutionFile, watchSnapshot, } from "@nodeint/migent-kernel";
3
+ import { CancellationRequested, ConfigurationError, corruptStatusMessage, SessionLock, SessionStore, log, readStatusFileState, readExecutionFile, statusDriftMessage, watchSnapshot, } from "@nodeint/migent-kernel";
4
4
  import { printNotice, printWatchEvent, printWatchSnapshot } from "../display.js";
5
+ import { EXIT_BLOCKED, EXIT_CANCELLED, EXIT_OK } from "../exits.js";
6
+ import { compareFollowLines, includeStreamLine, listStreamFiles, streamFollowStart, } from "../follow-log.js";
7
+ import { isFile } from "../fs-utils.js";
8
+ import { bindCancellationSignals, delay, throwIfCliCancelled } from "../signals.js";
9
+ const SESSION_DONE_EXITS = {
10
+ completed: EXIT_OK,
11
+ blocked: EXIT_BLOCKED,
12
+ cancelled: EXIT_CANCELLED,
13
+ };
5
14
  const TERMINAL_EXECUTION_STATUSES = new Set(["completed", "failed", "cancelled"]);
6
- export async function watchSession(workspace, sessionId, options = {}) {
15
+ export async function followSession(workspace, sessionId, options = {}) {
7
16
  const once = options.once ?? false;
8
17
  const untilTerminal = options.untilTerminal ?? false;
9
18
  const jsonLines = options.jsonLines ?? false;
10
19
  const executionId = options.executionId ?? null;
11
20
  const cursor = options.cursor === undefined ? null : options.cursor;
12
- if (once && untilTerminal)
13
- throw new ConfigurationError("--once cannot be combined with --until.");
14
- if (once && cursor !== null)
15
- throw new ConfigurationError("--once cannot be combined with --cursor.");
16
- if (untilTerminal && !executionId)
17
- throw new ConfigurationError("--until requires --execution.");
18
- if (cursor !== null && cursor < 0)
19
- throw new ConfigurationError("--cursor must be >= 0.");
21
+ const exitWhenDone = options.exitWhenDone === true;
20
22
  const sessionDirectory = path.join(workspace, ".migent", "sessions", sessionId);
21
23
  const eventsPath = path.join(sessionDirectory, "events.jsonl");
22
- if (!isFile(eventsPath))
24
+ const statusPath = path.join(sessionDirectory, "status.json");
25
+ if (readStatusFileState(statusPath).state === "corrupt") {
26
+ throw new ConfigurationError(corruptStatusMessage(statusPath));
27
+ }
28
+ if (!isFile(eventsPath) && !(once && isFile(path.join(sessionDirectory, "status.json")))) {
23
29
  throw new ConfigurationError(`No events found for session: ${sessionId}`);
24
- new SessionStore(workspace).reapOrphanedExecutions(sessionId, "ignore");
30
+ }
31
+ // Reaping writes, so it needs the session lock. A live holder keeps the session; follow then tails read-only.
32
+ const reapLock = SessionLock.tryAcquire(sessionDirectory);
33
+ if (reapLock !== null) {
34
+ try {
35
+ new SessionStore(workspace).reapOrphanedExecutions(sessionId, "ignore");
36
+ }
37
+ finally {
38
+ reapLock.release();
39
+ }
40
+ }
25
41
  let executionPath = null;
26
42
  if (executionId !== null) {
27
43
  const resolved = path.join(sessionDirectory, "executions", executionId, "execution.json");
@@ -30,56 +46,54 @@ export async function watchSession(workspace, sessionId, options = {}) {
30
46
  throw new ConfigurationError(`No execution found: ${executionId}`);
31
47
  }
32
48
  if (once) {
49
+ const drift = statusDriftMessage(workspace, sessionId);
50
+ if (drift)
51
+ printNotice(drift);
33
52
  printSnapshot(sessionDirectory, executionId, jsonLines);
34
53
  return 0;
35
54
  }
36
- let followCursor = cursor;
37
- if (followCursor === null) {
38
- const snapshot = printSnapshot(sessionDirectory, executionId, jsonLines);
39
- followCursor = Number(snapshot.events_cursor ?? 0);
55
+ const tailOnly = cursor === null;
56
+ let followCursor = cursor ?? 0;
57
+ let streamOffsets = null;
58
+ if (cursor === null) {
59
+ // Snapshot and the notice can block. Capture offsets first so lines written then are still tailed.
60
+ streamOffsets = new Map(listStreamFiles(sessionDirectory).map((stream) => [stream, fileSize(stream)]));
61
+ followCursor = fileSize(eventsPath);
62
+ printSnapshot(sessionDirectory, executionId, jsonLines, followCursor);
40
63
  }
41
64
  printNotice(`Watching session ${sessionId}.`);
42
- const onSigint = () => {
43
- try {
44
- raiseCancellation(2);
45
- }
46
- catch {
47
- // The command loop observes the pending cancellation.
48
- }
49
- };
50
- const onSigterm = () => {
51
- try {
52
- raiseCancellation(15);
53
- }
54
- catch {
55
- // The command loop observes the pending cancellation.
56
- }
57
- };
58
- process.on("SIGINT", onSigint);
59
- process.on("SIGTERM", onSigterm);
60
- const fd = openSync(eventsPath, "r");
65
+ const unbind = bindCancellationSignals();
66
+ const logs = openFollowLogs(sessionDirectory, eventsPath, followCursor, tailOnly, streamOffsets);
61
67
  try {
62
- let position = followCursor;
63
- let pending = "";
64
68
  for (;;) {
65
- const read = readCompleteLines(fd, position, pending);
66
- position = read.position;
67
- pending = read.pending;
68
- for (const line of read.lines)
69
- emitWatchEvent(line, executionId, jsonLines);
70
- if (read.lines.length > 0)
69
+ throwIfCliCancelled();
70
+ discoverStreams(sessionDirectory, logs);
71
+ const batch = readBatch(logs);
72
+ for (const item of batch)
73
+ emitWatchEvent(item.line, executionId, jsonLines);
74
+ if (batch.length > 0)
71
75
  continue;
72
76
  if (untilTerminal) {
73
77
  if (executionPath === null)
74
78
  throw new ConfigurationError("--until requires --execution.");
75
79
  const record = readExecutionFile(executionPath);
76
80
  if (record !== null && TERMINAL_EXECUTION_STATUSES.has(String(record.status))) {
77
- const drained = readCompleteLines(fd, position, pending);
78
- for (const line of drained.lines)
79
- emitWatchEvent(line, executionId, jsonLines);
81
+ const drained = readBatch(logs);
82
+ for (const item of drained)
83
+ emitWatchEvent(item.line, executionId, jsonLines);
80
84
  return untilTerminalExitCode(record);
81
85
  }
82
86
  }
87
+ if (exitWhenDone) {
88
+ const status = String(watchSnapshot(sessionDirectory, null).status ?? "");
89
+ const done = SESSION_DONE_EXITS[status];
90
+ if (done !== undefined) {
91
+ const drained = readBatch(logs);
92
+ for (const item of drained)
93
+ emitWatchEvent(item.line, executionId, jsonLines);
94
+ return done;
95
+ }
96
+ }
83
97
  await delay(250);
84
98
  }
85
99
  }
@@ -91,19 +105,26 @@ export async function watchSession(workspace, sessionId, options = {}) {
91
105
  throw error;
92
106
  }
93
107
  finally {
94
- closeSync(fd);
95
- process.off("SIGINT", onSigint);
96
- process.off("SIGTERM", onSigterm);
108
+ for (const log of logs)
109
+ closeSync(log.fd);
110
+ unbind();
97
111
  }
98
112
  }
99
- function printSnapshot(sessionDirectory, executionId, jsonLines) {
113
+ function printSnapshot(sessionDirectory, executionId, jsonLines, eventsCursor) {
100
114
  const snapshot = watchSnapshot(sessionDirectory, executionId);
115
+ if (eventsCursor !== undefined)
116
+ snapshot.events_cursor = eventsCursor;
101
117
  if (jsonLines)
102
- process.stdout.write(`${JSON.stringify(snapshot)}\n`);
118
+ process.stdout.write(`${JSON.stringify(snapshotJson(snapshot))}\n`);
103
119
  else
104
120
  printWatchSnapshot(snapshot);
105
121
  return snapshot;
106
122
  }
123
+ function snapshotJson(snapshot) {
124
+ const body = { ...snapshot };
125
+ delete body.task;
126
+ return body;
127
+ }
107
128
  function emitWatchEvent(line, executionId, jsonLines) {
108
129
  let event;
109
130
  try {
@@ -129,10 +150,65 @@ function untilTerminalExitCode(record) {
129
150
  return 0;
130
151
  return 1;
131
152
  }
132
- if (record.status === "failed" || record.status === "cancelled")
133
- return 1;
134
153
  return 1;
135
154
  }
155
+ function openFollowLogs(sessionDirectory, eventsPath, cursor, tailOnly, streamOffsets) {
156
+ const eventsText = !tailOnly && cursor > 0 ? readText(eventsPath) : "";
157
+ const logs = [openLog(eventsPath, cursor, null, "events")];
158
+ for (const stream of listStreamFiles(sessionDirectory)) {
159
+ const start = tailOnly
160
+ ? { offset: tailOffset(stream, streamOffsets), minTimestampMs: null }
161
+ : streamFollowStart(cursor, eventsText, fileSize(stream));
162
+ logs.push(openLog(stream, start.offset, start.minTimestampMs, "stream"));
163
+ }
164
+ return logs;
165
+ }
166
+ function tailOffset(stream, streamOffsets) {
167
+ if (streamOffsets === null)
168
+ return fileSize(stream);
169
+ return streamOffsets.get(stream) ?? 0;
170
+ }
171
+ function discoverStreams(sessionDirectory, logs) {
172
+ const known = new Set(logs.map((log) => log.path));
173
+ for (const stream of listStreamFiles(sessionDirectory)) {
174
+ if (known.has(stream))
175
+ continue;
176
+ logs.push(openLog(stream, 0, null, "stream"));
177
+ }
178
+ }
179
+ function readBatch(logs) {
180
+ const batch = [];
181
+ for (const log of logs) {
182
+ const read = readCompleteLines(log.fd, log.offset, log.pending);
183
+ log.offset = read.position;
184
+ log.pending = read.pending;
185
+ // A later poll can finish a torn line that is older than the cursor anchor.
186
+ const lines = read.lines.filter((line) => includeStreamLine(line, log.minTimestampMs));
187
+ for (const line of lines)
188
+ batch.push({ line, source: log.source, path: log.path });
189
+ }
190
+ batch.sort(compareFollowLines);
191
+ return batch;
192
+ }
193
+ function openLog(filePath, offset, minTimestampMs, source) {
194
+ return { path: filePath, fd: openSync(filePath, "r"), offset, pending: "", minTimestampMs, source };
195
+ }
196
+ function fileSize(filePath) {
197
+ try {
198
+ return statSync(filePath).size;
199
+ }
200
+ catch {
201
+ return 0;
202
+ }
203
+ }
204
+ function readText(filePath) {
205
+ try {
206
+ return readFileSync(filePath, "utf8");
207
+ }
208
+ catch {
209
+ return "";
210
+ }
211
+ }
136
212
  function readCompleteLines(fd, position, pending) {
137
213
  const chunks = [];
138
214
  const buffer = Buffer.alloc(64 * 1024);
@@ -151,19 +227,6 @@ function readCompleteLines(fd, position, pending) {
151
227
  const rest = pieces.pop() ?? "";
152
228
  return { lines: pieces, pending: rest, position: offset };
153
229
  }
154
- function delay(ms) {
155
- return new Promise((resolve) => {
156
- setTimeout(resolve, ms);
157
- });
158
- }
159
- function isFile(filePath) {
160
- try {
161
- return statSync(filePath).isFile();
162
- }
163
- catch {
164
- return false;
165
- }
166
- }
167
230
  function isRecord(value) {
168
231
  return typeof value === "object" && value !== null && !Array.isArray(value);
169
232
  }
@@ -2,6 +2,7 @@ import { realpathSync } from "node:fs";
2
2
  import path from "node:path";
3
3
  import { ServiceError, callService, endpointFromHome, ensureRegistryHome, startDetached } from "@nodeint/migent-registry";
4
4
  import { printError } from "../display.js";
5
+ import { PACKAGE_VERSION } from "../version.js";
5
6
  import { findWorkspace, invocationCwdPath } from "../workspace.js";
6
7
  export const workspaceApi = {
7
8
  startDetached,
@@ -25,7 +26,7 @@ export async function runRegister(workspacePath, name) {
25
26
  }
26
27
  throw error;
27
28
  }
28
- const body = { path: root, tool: { name: "migent", version: "0.1.0" } };
29
+ const body = { path: root, tool: { name: "migent", version: PACKAGE_VERSION } };
29
30
  if (name !== null)
30
31
  body.name = name;
31
32
  if (!path.isAbsolute(String(body.path))) {
@@ -88,6 +89,14 @@ async function finish(method, requestPath, endpoint, body) {
88
89
  }
89
90
  if (status === 200 || status === 201) {
90
91
  process.stdout.write(`${JSON.stringify(payload)}\n`);
92
+ if (method === "POST") {
93
+ const id = typeof payload.id === "string" && payload.id !== "" ? payload.id : "workspace";
94
+ process.stderr.write(`Registered ${id}.\n`);
95
+ }
96
+ else if (method === "DELETE") {
97
+ const id = requestPath.split("/").pop() ?? "workspace";
98
+ process.stderr.write(`Removed ${id}.\n`);
99
+ }
91
100
  return 0;
92
101
  }
93
102
  return statusExit(status, payload);
package/dist/display.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { createRequire } from "node:module";
2
+ import { sessionFields } from "./session-row.js";
2
3
  const require = createRequire(import.meta.url);
3
4
  const pc = require("picocolors");
4
5
  export function printError(message) {
@@ -45,8 +46,8 @@ export function printSessions(rows) {
45
46
  return;
46
47
  }
47
48
  for (const row of rows) {
48
- const [, sessionId, workflowId, status, executionId, decision, outputs, task] = row;
49
- process.stdout.write(`${sessionId ?? ""} ${workflowId ?? ""} ${status ?? ""} ${executionId ?? ""} ${decision ?? ""} ${outputs ?? ""} ${task ?? ""}\n`);
49
+ const fields = sessionFields(row);
50
+ process.stdout.write(`${fields.session} ${fields.workflow} ${fields.status} ${fields.execution} ${fields.decision} ${fields.outputs} ${fields.task}\n`);
50
51
  }
51
52
  }
52
53
  export function printAgent(result, role, provider, model) {
@@ -74,13 +75,24 @@ export function printWatchSnapshot(snapshot) {
74
75
  }
75
76
  if (typeof snapshot.task === "string" && snapshot.task)
76
77
  lines.push(["task", snapshot.task]);
77
- const executionId = snapshot.execution_id;
78
- const executionStatus = snapshot.execution_status ?? snapshot.latest_execution_status;
79
- if (typeof executionId === "string" && executionId) {
80
- const value = typeof executionStatus === "string" && executionStatus
81
- ? `${executionId} · ${executionStatus}`
82
- : executionId;
83
- lines.push(["execution", value]);
78
+ const running = Array.isArray(snapshot.running_stages) ? snapshot.running_stages.filter(isRecord) : [];
79
+ if (running.length > 0) {
80
+ for (const stage of running) {
81
+ const stageId = typeof stage.stage_id === "string" && stage.stage_id ? stage.stage_id : "stage";
82
+ const executionId = typeof stage.execution_id === "string" ? stage.execution_id : "";
83
+ const summary = typeof stage.summary === "string" && stage.summary ? ` · ${stage.summary}` : "";
84
+ lines.push(["running", `${stageId} · ${executionId}${summary}`]);
85
+ }
86
+ }
87
+ else {
88
+ const executionId = snapshot.execution_id;
89
+ const executionStatus = snapshot.execution_status ?? snapshot.latest_execution_status;
90
+ if (typeof executionId === "string" && executionId) {
91
+ const value = typeof executionStatus === "string" && executionStatus
92
+ ? `${executionId} · ${executionStatus}`
93
+ : executionId;
94
+ lines.push(["execution", value]);
95
+ }
84
96
  }
85
97
  const outcome = snapshot.outcome;
86
98
  if (isRecord(outcome) && typeof outcome.status === "string") {
@@ -0,0 +1,3 @@
1
+ export declare class PromptCancelled extends Error {
2
+ constructor();
3
+ }
package/dist/errors.js ADDED
@@ -0,0 +1,6 @@
1
+ export class PromptCancelled extends Error {
2
+ constructor() {
3
+ super("Prompt cancelled.");
4
+ this.name = "PromptCancelled";
5
+ }
6
+ }
@@ -0,0 +1,10 @@
1
+ export declare const EXIT_OK = 0;
2
+ export declare const EXIT_BLOCKED = 1;
3
+ export declare const EXIT_CONFIG = 2;
4
+ export declare const EXIT_ATTEMPTS = 3;
5
+ export declare const EXIT_REVISIONS = 4;
6
+ export declare const EXIT_DISPATCHES = 5;
7
+ export declare const EXIT_VERIFY = 6;
8
+ export declare const EXIT_CANCELLED = 130;
9
+ export declare const LOOP_EXITS: string[];
10
+ export declare const STAGE_EXITS: string[];
package/dist/exits.js ADDED
@@ -0,0 +1,26 @@
1
+ export const EXIT_OK = 0;
2
+ export const EXIT_BLOCKED = 1;
3
+ export const EXIT_CONFIG = 2;
4
+ export const EXIT_ATTEMPTS = 3;
5
+ export const EXIT_REVISIONS = 4;
6
+ export const EXIT_DISPATCHES = 5;
7
+ export const EXIT_VERIFY = 6;
8
+ export const EXIT_CANCELLED = 130;
9
+ export const LOOP_EXITS = [
10
+ `Exit ${EXIT_OK} when the session is completed.`,
11
+ `Exit ${EXIT_BLOCKED} when the session is blocked. session continue runs that stage again.`,
12
+ `Exit ${EXIT_CONFIG} on a configuration error.`,
13
+ `Exit ${EXIT_ATTEMPTS} when --attempts is spent. The session stays active.`,
14
+ `Exit ${EXIT_REVISIONS} when a route would exceed max_revisions. The session stays active.`,
15
+ `Exit ${EXIT_DISPATCHES} when --max-dispatches is spent. The session stays active.`,
16
+ `Exit ${EXIT_VERIFY} when verify fails max_verify times. The session stays active.`,
17
+ `Exit ${EXIT_CANCELLED} when cancelled. session continue resumes that execution.`,
18
+ ];
19
+ export const STAGE_EXITS = [
20
+ `Exit ${EXIT_OK} when the stage outcome is complete, or the session is already completed.`,
21
+ `Exit ${EXIT_BLOCKED} when the stage is not complete, or the session is blocked.`,
22
+ `Exit ${EXIT_CONFIG} on a configuration error.`,
23
+ `Exit ${EXIT_REVISIONS} when a route would exceed max_revisions. The session stays active.`,
24
+ `Exit ${EXIT_VERIFY} when verify fails max_verify times. The session stays active.`,
25
+ `Exit ${EXIT_CANCELLED} when cancelled. session continue resumes that execution.`,
26
+ ];
@@ -0,0 +1,29 @@
1
+ export interface FollowLine {
2
+ line: string;
3
+ source: "events" | "stream";
4
+ path: string;
5
+ }
6
+ export interface StreamFollowStart {
7
+ offset: number;
8
+ /** Skip stream lines whose timestamp parses earlier than this. The floor stays for later polls. */
9
+ minTimestampMs: number | null;
10
+ }
11
+ /**
12
+ * Where to start reading one execution stream.
13
+ * `--cursor` is a byte offset into events.jsonl. Stream lines are selected by timestamp.
14
+ * A stream created after follow starts is not passed here: the caller reads it from offset 0.
15
+ */
16
+ export declare function streamFollowStart(cursor: number | null, eventsText: string, streamSize: number): StreamFollowStart;
17
+ export declare function includeStreamLine(line: string, minTimestampMs: number | null): boolean;
18
+ export declare function compareFollowLines(left: FollowLine, right: FollowLine): number;
19
+ /** Lines follow would emit for this cursor, including a stream that appeared after the start. */
20
+ export declare function replayFollowLines(input: {
21
+ cursor: number | null;
22
+ eventsText: string;
23
+ streams: Array<{
24
+ path: string;
25
+ text: string;
26
+ appearedAfterStart?: boolean;
27
+ }>;
28
+ }): string[];
29
+ export declare function listStreamFiles(sessionDirectory: string): string[];