@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.
- package/README.md +57 -41
- package/dist/commands/agent.js +111 -59
- package/dist/commands/config.js +20 -8
- package/dist/commands/execute.d.ts +6 -0
- package/dist/commands/execute.js +30 -14
- package/dist/commands/init.d.ts +2 -0
- package/dist/commands/init.js +91 -87
- package/dist/commands/prepare.d.ts +1 -0
- package/dist/commands/prepare.js +14 -0
- package/dist/commands/repair.d.ts +1 -0
- package/dist/commands/repair.js +22 -0
- package/dist/commands/service.js +5 -4
- package/dist/commands/sessions.d.ts +24 -1
- package/dist/commands/sessions.js +165 -13
- package/dist/commands/skill.js +51 -32
- package/dist/commands/watch.d.ts +2 -1
- package/dist/commands/watch.js +130 -67
- package/dist/commands/workspace-registry.js +10 -1
- package/dist/display.js +21 -9
- package/dist/errors.d.ts +3 -0
- package/dist/errors.js +6 -0
- package/dist/exits.d.ts +10 -0
- package/dist/exits.js +26 -0
- package/dist/follow-log.d.ts +29 -0
- package/dist/follow-log.js +172 -0
- package/dist/fs-utils.d.ts +3 -0
- package/dist/fs-utils.js +26 -0
- package/dist/index.js +5 -1
- package/dist/migrate.js +2 -17
- package/dist/program.js +388 -168
- package/dist/session-row.d.ts +20 -0
- package/dist/session-row.js +21 -0
- package/dist/signals.d.ts +3 -0
- package/dist/signals.js +56 -0
- package/dist/templates/migent/SKILL.md +159 -50
- package/dist/templates/migent/references/schema-config.yaml +35 -0
- package/dist/templates/migent/references/schema-workflow.yaml +61 -0
- package/dist/text.d.ts +4 -0
- package/dist/text.js +13 -0
- package/dist/version.d.ts +1 -0
- package/dist/version.js +3 -0
- package/dist/workspace.js +5 -12
- package/package.json +4 -4
package/dist/commands/skill.js
CHANGED
|
@@ -1,20 +1,21 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
|
-
import { existsSync, mkdirSync, readFileSync,
|
|
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,
|
|
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
|
-
|
|
37
|
-
|
|
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
|
|
42
|
-
if (
|
|
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,
|
|
50
|
+
writeManagedSkill(skillDirectory, template);
|
|
47
51
|
process.stdout.write(`Updated ${displayPath(skillDirectory, workspace)}\n`);
|
|
48
52
|
return 0;
|
|
49
53
|
}
|
|
50
|
-
function
|
|
51
|
-
const
|
|
52
|
-
return readFileSync(
|
|
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
|
|
55
|
-
|
|
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:
|
|
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
|
|
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
|
-
}
|
package/dist/commands/watch.d.ts
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
|
-
export declare function
|
|
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>;
|
package/dist/commands/watch.js
CHANGED
|
@@ -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,
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
|
43
|
-
|
|
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
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
for (const
|
|
69
|
-
emitWatchEvent(line, executionId, jsonLines);
|
|
70
|
-
if (
|
|
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 =
|
|
78
|
-
for (const
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
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:
|
|
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
|
|
49
|
-
process.stdout.write(`${
|
|
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
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
:
|
|
83
|
-
|
|
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") {
|
package/dist/errors.d.ts
ADDED
package/dist/errors.js
ADDED
package/dist/exits.d.ts
ADDED
|
@@ -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[];
|