@dna113p/machines 0.3.4 → 0.3.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -2
- package/claude/statusline.mjs +2 -0
- package/dist/mcp/server.js +2 -2
- package/dist/src/cli.js +58 -4
- package/dist/src/codex.d.ts +2 -0
- package/dist/src/codex.js +2 -0
- package/dist/src/host.d.ts +6 -0
- package/dist/src/host.js +7 -2
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.js +1 -0
- package/dist/src/launcher.d.ts +3 -0
- package/dist/src/launcher.js +3 -0
- package/dist/src/run-graph.d.ts +39 -0
- package/dist/src/run-graph.js +91 -0
- package/dist/src/run-status.d.ts +16 -2
- package/dist/src/run-status.js +56 -3
- package/dist/src/run-ui-page.d.ts +6 -0
- package/dist/src/run-ui-page.js +690 -0
- package/dist/src/run-ui.d.ts +62 -0
- package/dist/src/run-ui.js +258 -0
- package/dist/src/run-viewer.d.ts +2 -0
- package/dist/src/run-viewer.js +1 -0
- package/docs/authoring.md +13 -0
- package/docs/integrations.md +62 -4
- package/package.json +88 -21
- package/skills/machine-builder/SKILL.md +31 -0
- package/skills/machine-delegation/SKILL.md +23 -0
- package/skills/machine-project-setup/SKILL.md +23 -5
- package/skills/machine-project-setup/references/project-workflows.md +4 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { type RunStatusRecord } from "./run-status.ts";
|
|
2
|
+
/**
|
|
3
|
+
* `machine ui`: a local page that shows every published run as it happens,
|
|
4
|
+
* with its Machine's graph, and the project's own panels beside them. It reads
|
|
5
|
+
* the run-status directory and answers a waiting run through its inbox, as
|
|
6
|
+
* `machine respond` does; nothing else in the page changes anything.
|
|
7
|
+
*/
|
|
8
|
+
export interface RunUiOptions {
|
|
9
|
+
/** The run-status directory. */
|
|
10
|
+
readonly directory: string;
|
|
11
|
+
/** The project whose `.machines/ui.json` names panels; its name heads the page. */
|
|
12
|
+
readonly projectDir: string;
|
|
13
|
+
readonly port?: number;
|
|
14
|
+
/** How often the directory is read for changes. */
|
|
15
|
+
readonly pollMilliseconds?: number;
|
|
16
|
+
}
|
|
17
|
+
export interface RunUi {
|
|
18
|
+
readonly url: string;
|
|
19
|
+
close(): Promise<void>;
|
|
20
|
+
}
|
|
21
|
+
/** A run as the page shows it: `lost` when its record says running or waiting and its process is gone. */
|
|
22
|
+
export type UiRun = Omit<RunStatusRecord, "status"> & {
|
|
23
|
+
readonly status: RunStatusRecord["status"] | "lost";
|
|
24
|
+
};
|
|
25
|
+
/** What a panel command prints: a few lines a person can take in at a glance. */
|
|
26
|
+
export interface UiPanel {
|
|
27
|
+
readonly title: string;
|
|
28
|
+
readonly summary?: string;
|
|
29
|
+
readonly items: readonly UiPanelItem[];
|
|
30
|
+
readonly error?: string;
|
|
31
|
+
readonly checkedAt?: string;
|
|
32
|
+
}
|
|
33
|
+
export interface UiPanelItem {
|
|
34
|
+
readonly text: string;
|
|
35
|
+
readonly detail?: string;
|
|
36
|
+
/** Colours the item: `bad` and `warn` draw the eye, `active` is work under way. */
|
|
37
|
+
readonly status?: "ok" | "warn" | "bad" | "active" | "idle";
|
|
38
|
+
readonly href?: string;
|
|
39
|
+
readonly at?: string;
|
|
40
|
+
}
|
|
41
|
+
interface PanelSource {
|
|
42
|
+
readonly title: string;
|
|
43
|
+
readonly command: readonly string[];
|
|
44
|
+
readonly refreshSeconds: number;
|
|
45
|
+
}
|
|
46
|
+
export declare const defaultUiPort = 4777;
|
|
47
|
+
/** The nearest directory at or above `start` that has a `.machines` directory, or `start` itself. */
|
|
48
|
+
export declare function uiProjectDir(start: string): string;
|
|
49
|
+
/** Every published run, with runs whose process is gone shown as lost. */
|
|
50
|
+
export declare function uiRuns(directory: string, isAlive?: (pid: number) => boolean): UiRun[];
|
|
51
|
+
/**
|
|
52
|
+
* Reads `.machines/ui.json`: `{ "panels": [{ "title", "command": [...], "refreshSeconds"? }] }`.
|
|
53
|
+
* A missing file is no panels; a malformed one is one panel saying what is wrong.
|
|
54
|
+
*/
|
|
55
|
+
export declare function readPanelSources(projectDir: string): {
|
|
56
|
+
sources: PanelSource[];
|
|
57
|
+
error?: string;
|
|
58
|
+
};
|
|
59
|
+
/** Turns a panel command's output into a panel, or says why it could not. */
|
|
60
|
+
export declare function parsePanelOutput(title: string, stdout: string): UiPanel;
|
|
61
|
+
export declare function startRunUi(options: RunUiOptions): Promise<RunUi>;
|
|
62
|
+
export {};
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import { execFile } from "node:child_process";
|
|
2
|
+
import { randomBytes } from "node:crypto";
|
|
3
|
+
import { existsSync, readFileSync, statSync } from "node:fs";
|
|
4
|
+
import { createServer } from "node:http";
|
|
5
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
6
|
+
import { respondToRun } from "./run-inbox.js";
|
|
7
|
+
import { runUiPage } from "./run-ui-page.js";
|
|
8
|
+
import { isProcessAlive, readRunStatuses } from "./run-status.js";
|
|
9
|
+
export const defaultUiPort = 4777;
|
|
10
|
+
const panelTimeoutMilliseconds = 30_000;
|
|
11
|
+
const maxPanelItems = 50;
|
|
12
|
+
/** The nearest directory at or above `start` that has a `.machines` directory, or `start` itself. */
|
|
13
|
+
export function uiProjectDir(start) {
|
|
14
|
+
let current = resolve(start);
|
|
15
|
+
while (true) {
|
|
16
|
+
if (existsSync(join(current, ".machines")) && statSync(join(current, ".machines")).isDirectory())
|
|
17
|
+
return current;
|
|
18
|
+
const parent = dirname(current);
|
|
19
|
+
if (parent === current)
|
|
20
|
+
return resolve(start);
|
|
21
|
+
current = parent;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
/** Every published run, with runs whose process is gone shown as lost. */
|
|
25
|
+
export function uiRuns(directory, isAlive = isProcessAlive) {
|
|
26
|
+
return readRunStatuses(directory)
|
|
27
|
+
.map((record) => ((record.status === "running" || record.status === "waiting") && !isAlive(record.pid)
|
|
28
|
+
? { ...record, status: "lost" }
|
|
29
|
+
: record))
|
|
30
|
+
.sort((left, right) => Date.parse(right.updatedAt) - Date.parse(left.updatedAt));
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Reads `.machines/ui.json`: `{ "panels": [{ "title", "command": [...], "refreshSeconds"? }] }`.
|
|
34
|
+
* A missing file is no panels; a malformed one is one panel saying what is wrong.
|
|
35
|
+
*/
|
|
36
|
+
export function readPanelSources(projectDir) {
|
|
37
|
+
const file = join(projectDir, ".machines", "ui.json");
|
|
38
|
+
if (!existsSync(file))
|
|
39
|
+
return { sources: [] };
|
|
40
|
+
try {
|
|
41
|
+
const parsed = JSON.parse(readFileSync(file, "utf8"));
|
|
42
|
+
if (!Array.isArray(parsed.panels))
|
|
43
|
+
throw new Error("panels must be an array");
|
|
44
|
+
const sources = parsed.panels.map((panel, index) => {
|
|
45
|
+
const { title, command, refreshSeconds } = (panel ?? {});
|
|
46
|
+
if (typeof title !== "string" || title.trim() === "")
|
|
47
|
+
throw new Error(`panels[${index}].title must be a non-empty string`);
|
|
48
|
+
if (!Array.isArray(command) || command.length === 0 || !command.every((part) => typeof part === "string")) {
|
|
49
|
+
throw new Error(`panels[${index}].command must be a non-empty array of strings`);
|
|
50
|
+
}
|
|
51
|
+
if (refreshSeconds !== undefined && (typeof refreshSeconds !== "number" || !(refreshSeconds >= 1))) {
|
|
52
|
+
throw new Error(`panels[${index}].refreshSeconds must be a number of at least 1`);
|
|
53
|
+
}
|
|
54
|
+
return { title, command, refreshSeconds: refreshSeconds ?? 10 };
|
|
55
|
+
});
|
|
56
|
+
return { sources };
|
|
57
|
+
}
|
|
58
|
+
catch (cause) {
|
|
59
|
+
return { sources: [], error: `${file}: ${cause instanceof Error ? cause.message : String(cause)}` };
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/** Turns a panel command's output into a panel, or says why it could not. */
|
|
63
|
+
export function parsePanelOutput(title, stdout) {
|
|
64
|
+
const parsed = JSON.parse(stdout);
|
|
65
|
+
if (parsed === null || typeof parsed !== "object" || !Array.isArray(parsed.items)) {
|
|
66
|
+
throw new Error("the command must print a JSON object with an items array");
|
|
67
|
+
}
|
|
68
|
+
const statuses = new Set(["ok", "warn", "bad", "active", "idle"]);
|
|
69
|
+
const text = (value, limit) => typeof value === "string" && value.trim() !== "" ? value.slice(0, limit) : undefined;
|
|
70
|
+
const items = parsed.items.slice(0, maxPanelItems).flatMap((item) => {
|
|
71
|
+
const entry = (item ?? {});
|
|
72
|
+
const line = text(entry.text, 300);
|
|
73
|
+
if (line === undefined)
|
|
74
|
+
return [];
|
|
75
|
+
const href = text(entry.href, 2000);
|
|
76
|
+
return [{
|
|
77
|
+
text: line,
|
|
78
|
+
...(text(entry.detail, 600) === undefined ? {} : { detail: text(entry.detail, 600) }),
|
|
79
|
+
...(typeof entry.status === "string" && statuses.has(entry.status) ? { status: entry.status } : {}),
|
|
80
|
+
// Only web links: a panel never gets the page to open anything else.
|
|
81
|
+
...(href !== undefined && /^https?:\/\//u.test(href) ? { href } : {}),
|
|
82
|
+
...(text(entry.at, 40) !== undefined && !Number.isNaN(Date.parse(entry.at)) ? { at: entry.at } : {}),
|
|
83
|
+
}];
|
|
84
|
+
});
|
|
85
|
+
return { title, ...(text(parsed.summary, 300) === undefined ? {} : { summary: text(parsed.summary, 300) }), items };
|
|
86
|
+
}
|
|
87
|
+
export async function startRunUi(options) {
|
|
88
|
+
const { directory, projectDir } = options;
|
|
89
|
+
const token = randomBytes(24).toString("hex");
|
|
90
|
+
const poll = options.pollMilliseconds ?? 1_000;
|
|
91
|
+
const clients = new Set();
|
|
92
|
+
const panels = new Map();
|
|
93
|
+
let port = 0;
|
|
94
|
+
let last = "";
|
|
95
|
+
const refreshPanels = () => {
|
|
96
|
+
const { sources, error } = readPanelSources(projectDir);
|
|
97
|
+
if (error !== undefined) {
|
|
98
|
+
panels.set("\0config", { panel: { title: "ui.json", items: [], error }, checkedAt: Date.now(), running: false });
|
|
99
|
+
}
|
|
100
|
+
else
|
|
101
|
+
panels.delete("\0config");
|
|
102
|
+
const wanted = new Set(sources.map((source) => source.title));
|
|
103
|
+
for (const title of panels.keys())
|
|
104
|
+
if (title !== "\0config" && !wanted.has(title))
|
|
105
|
+
panels.delete(title);
|
|
106
|
+
for (const source of sources) {
|
|
107
|
+
const known = panels.get(source.title);
|
|
108
|
+
if (known?.running || (known !== undefined && Date.now() - known.checkedAt < source.refreshSeconds * 1_000))
|
|
109
|
+
continue;
|
|
110
|
+
const entry = { panel: known?.panel ?? { title: source.title, items: [] }, checkedAt: Date.now(), running: true };
|
|
111
|
+
panels.set(source.title, entry);
|
|
112
|
+
execFile(source.command[0], source.command.slice(1), {
|
|
113
|
+
cwd: projectDir, timeout: panelTimeoutMilliseconds, maxBuffer: 4 * 1024 * 1024, encoding: "utf8",
|
|
114
|
+
}, (failure, stdout, stderr) => {
|
|
115
|
+
let panel;
|
|
116
|
+
try {
|
|
117
|
+
if (failure)
|
|
118
|
+
throw new Error((stderr.trim() || failure.message).split("\n").slice(-3).join(" "));
|
|
119
|
+
panel = parsePanelOutput(source.title, stdout);
|
|
120
|
+
}
|
|
121
|
+
catch (cause) {
|
|
122
|
+
panel = { ...entry.panel, error: cause instanceof Error ? cause.message : String(cause) };
|
|
123
|
+
}
|
|
124
|
+
panels.set(source.title, { panel: { ...panel, checkedAt: new Date().toISOString() }, checkedAt: Date.now(), running: false });
|
|
125
|
+
broadcast();
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
};
|
|
129
|
+
const snapshot = () => ({
|
|
130
|
+
project: { name: basename(projectDir), dir: projectDir },
|
|
131
|
+
directory,
|
|
132
|
+
runs: uiRuns(directory),
|
|
133
|
+
panels: [...panels.values()].map((entry) => entry.panel),
|
|
134
|
+
});
|
|
135
|
+
const broadcast = (force = false) => {
|
|
136
|
+
const state = snapshot();
|
|
137
|
+
const text = JSON.stringify(state);
|
|
138
|
+
if (!force && text === last)
|
|
139
|
+
return;
|
|
140
|
+
last = text;
|
|
141
|
+
for (const client of clients)
|
|
142
|
+
client.write(`data: ${text}\n\n`);
|
|
143
|
+
};
|
|
144
|
+
const timer = setInterval(() => {
|
|
145
|
+
refreshPanels();
|
|
146
|
+
broadcast();
|
|
147
|
+
}, poll);
|
|
148
|
+
timer.unref();
|
|
149
|
+
// A quiet connection is closed by some proxies and browsers; a comment keeps it open.
|
|
150
|
+
const heartbeat = setInterval(() => { for (const client of clients)
|
|
151
|
+
client.write(": ping\n\n"); }, 15_000);
|
|
152
|
+
heartbeat.unref();
|
|
153
|
+
const server = createServer((request, response) => {
|
|
154
|
+
void handle(request, response).catch((cause) => {
|
|
155
|
+
if (!response.headersSent)
|
|
156
|
+
send(response, 500, { error: cause instanceof Error ? cause.message : String(cause) });
|
|
157
|
+
else
|
|
158
|
+
response.end();
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
async function handle(request, response) {
|
|
162
|
+
// Another site can make the browser send to this port, but not with this host name: a page served
|
|
163
|
+
// from a name that resolves here (DNS rebinding) is refused, and only the page itself holds the token.
|
|
164
|
+
const host = request.headers.host ?? "";
|
|
165
|
+
if (host !== `127.0.0.1:${port}` && host !== `localhost:${port}`)
|
|
166
|
+
return send(response, 421, { error: "unexpected host" });
|
|
167
|
+
const url = new URL(request.url ?? "/", `http://${host}`);
|
|
168
|
+
if (request.method === "GET" && url.pathname === "/") {
|
|
169
|
+
response.writeHead(200, {
|
|
170
|
+
"content-type": "text/html; charset=utf-8",
|
|
171
|
+
"cache-control": "no-store",
|
|
172
|
+
"content-security-policy": "default-src 'none'; script-src 'unsafe-inline'; style-src 'unsafe-inline'; connect-src 'self'; img-src data:; base-uri 'none'; form-action 'none'; frame-ancestors 'none'",
|
|
173
|
+
"x-content-type-options": "nosniff",
|
|
174
|
+
});
|
|
175
|
+
response.end(runUiPage(token));
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
if (request.method === "GET" && url.pathname === "/api/state")
|
|
179
|
+
return send(response, 200, snapshot());
|
|
180
|
+
if (request.method === "GET" && url.pathname === "/api/events") {
|
|
181
|
+
response.writeHead(200, { "content-type": "text/event-stream", "cache-control": "no-store", connection: "keep-alive" });
|
|
182
|
+
clients.add(response);
|
|
183
|
+
response.write(`data: ${JSON.stringify(snapshot())}\n\n`);
|
|
184
|
+
request.on("close", () => clients.delete(response));
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
if (request.method === "POST" && url.pathname === "/api/respond") {
|
|
188
|
+
if (request.headers["x-machines-ui"] !== token)
|
|
189
|
+
return send(response, 403, { error: "missing or wrong page token" });
|
|
190
|
+
const body = await readBody(request);
|
|
191
|
+
const { runId, requestId, response: text, question } = body;
|
|
192
|
+
if (typeof runId !== "string" || typeof requestId !== "string" || typeof text !== "string" || text.trim() === "") {
|
|
193
|
+
return send(response, 400, { error: "runId, requestId, and a non-empty response are required" });
|
|
194
|
+
}
|
|
195
|
+
const answer = question === true ? { type: "question", text } : text;
|
|
196
|
+
try {
|
|
197
|
+
await respondToRun({ directory, runId, requestId, response: answer });
|
|
198
|
+
}
|
|
199
|
+
catch (cause) {
|
|
200
|
+
return send(response, 409, { error: cause instanceof Error ? cause.message : String(cause) });
|
|
201
|
+
}
|
|
202
|
+
broadcast(true);
|
|
203
|
+
return send(response, 200, { ok: true });
|
|
204
|
+
}
|
|
205
|
+
send(response, 404, { error: "not found" });
|
|
206
|
+
}
|
|
207
|
+
const start = options.port ?? defaultUiPort;
|
|
208
|
+
// The first free port from the one asked for: a second page for another project is not an error.
|
|
209
|
+
for (let candidate = start;; candidate += 1) {
|
|
210
|
+
try {
|
|
211
|
+
await new Promise((resolveListen, rejectListen) => {
|
|
212
|
+
server.once("error", rejectListen);
|
|
213
|
+
server.listen(candidate, "127.0.0.1", () => {
|
|
214
|
+
server.off("error", rejectListen);
|
|
215
|
+
resolveListen();
|
|
216
|
+
});
|
|
217
|
+
});
|
|
218
|
+
port = candidate;
|
|
219
|
+
break;
|
|
220
|
+
}
|
|
221
|
+
catch (cause) {
|
|
222
|
+
if (cause.code !== "EADDRINUSE" || candidate - start >= 20 || options.port === 0)
|
|
223
|
+
throw cause;
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
const address = server.address();
|
|
227
|
+
if (address !== null && typeof address === "object")
|
|
228
|
+
port = address.port;
|
|
229
|
+
refreshPanels();
|
|
230
|
+
return {
|
|
231
|
+
url: `http://127.0.0.1:${port}/`,
|
|
232
|
+
close: () => new Promise((resolveClose) => {
|
|
233
|
+
clearInterval(timer);
|
|
234
|
+
clearInterval(heartbeat);
|
|
235
|
+
for (const client of clients)
|
|
236
|
+
client.end();
|
|
237
|
+
server.close(() => resolveClose());
|
|
238
|
+
}),
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
function send(response, status, body) {
|
|
242
|
+
response.writeHead(status, { "content-type": "application/json; charset=utf-8", "cache-control": "no-store" });
|
|
243
|
+
response.end(JSON.stringify(body));
|
|
244
|
+
}
|
|
245
|
+
async function readBody(request) {
|
|
246
|
+
let text = "";
|
|
247
|
+
for await (const chunk of request) {
|
|
248
|
+
text += String(chunk);
|
|
249
|
+
if (text.length > 64 * 1024)
|
|
250
|
+
throw new Error("request body too large");
|
|
251
|
+
}
|
|
252
|
+
try {
|
|
253
|
+
return JSON.parse(text);
|
|
254
|
+
}
|
|
255
|
+
catch {
|
|
256
|
+
return {};
|
|
257
|
+
}
|
|
258
|
+
}
|
package/dist/src/run-viewer.d.ts
CHANGED
|
@@ -2,6 +2,8 @@ import { type RunStatusRecord } from "./run-status.ts";
|
|
|
2
2
|
import { type RunWatch } from "./run-watch.ts";
|
|
3
3
|
export { readRunStatuses, runSessionId, runStatusDirectory } from "./run-status.ts";
|
|
4
4
|
export type { RunStatusAgent, RunStatusHuman, RunStatusLimit, RunStatusRecord } from "./run-status.ts";
|
|
5
|
+
export { statePaths } from "./run-graph.ts";
|
|
6
|
+
export type { RunGraph, RunGraphEdge, RunGraphState, RunTrailStep } from "./run-graph.ts";
|
|
5
7
|
export { readRunWatches } from "./run-watch.ts";
|
|
6
8
|
export type { RunWatch } from "./run-watch.ts";
|
|
7
9
|
/** How long a settled or lost run stays with its viewers before it drops out. */
|
package/dist/src/run-viewer.js
CHANGED
|
@@ -4,6 +4,7 @@ import { isWithinDirectory, runWatchMatches } from "./run-watch.js";
|
|
|
4
4
|
// application needs to draw its own widgets for published runs. It only reads;
|
|
5
5
|
// the publisher and the watch-list writer stay in the runtime.
|
|
6
6
|
export { readRunStatuses, runSessionId, runStatusDirectory } from "./run-status.js";
|
|
7
|
+
export { statePaths } from "./run-graph.js";
|
|
7
8
|
export { readRunWatches } from "./run-watch.js";
|
|
8
9
|
/** How long a settled or lost run stays with its viewers before it drops out. */
|
|
9
10
|
export const settledRunVisibleMilliseconds = 30_000;
|
package/docs/authoring.md
CHANGED
|
@@ -101,6 +101,10 @@ The Machine owns this loop. An Agent runner only returns an allowed outcome; it
|
|
|
101
101
|
does not decide destination states. Add verification Operations where the workflow
|
|
102
102
|
has a concrete condition to check.
|
|
103
103
|
|
|
104
|
+
When designing verification and repair routes, follow
|
|
105
|
+
[Checks the Machine owns](../skills/machine-builder/SKILL.md#checks-the-machine-owns)
|
|
106
|
+
for the checks and evidence the workflow must own.
|
|
107
|
+
|
|
104
108
|
### Handling a usage limit in the Machine
|
|
105
109
|
|
|
106
110
|
By default an Agent state never sees its provider's usage limit: the launcher
|
|
@@ -471,6 +475,15 @@ Options:
|
|
|
471
475
|
`MACHINES_AGENT_MODEL` / `MACHINES_AGENT_EFFORT`. The `env` option overrides
|
|
472
476
|
matching parent variables. With no setting, Codex selects its configured values;
|
|
473
477
|
effort must be supported by the selected model.
|
|
478
|
+
- `serviceTier`: selects a Codex service tier, for example
|
|
479
|
+
`codexAgent("codex", [], { serviceTier: "fast" })` for Fast mode or
|
|
480
|
+
`codexAgent("codex", [], { serviceTier: "ultrafast" })` for Ultrafast. Overrides
|
|
481
|
+
`CODEX_SERVICE_TIER` (including values supplied through `env`); when neither
|
|
482
|
+
is set, the runner leaves Codex's configured tier unchanged. The built-in
|
|
483
|
+
preset also accepts `CODEX_SERVICE_TIER=fast` or `CODEX_SERVICE_TIER=ultrafast`.
|
|
484
|
+
Both are opt-in; Machines does not enable either tier by default. Supported
|
|
485
|
+
tiers depend on the installed CLI, model, and account. See the
|
|
486
|
+
[Codex configuration reference](https://developers.openai.com/codex/config-reference#service_tier).
|
|
474
487
|
- `output`: `stream` (default) writes agent messages and stderr to the terminal;
|
|
475
488
|
`capture` keeps them off the terminal and includes final prose in the event's
|
|
476
489
|
`message`. Both modes report activity to observers.
|
package/docs/integrations.md
CHANGED
|
@@ -220,6 +220,22 @@ choice to publish that text. `machine run` exports its run id as
|
|
|
220
220
|
`MACHINES_RUN_PARENT`, so a Machine started by one of its Operations or Agents
|
|
221
221
|
records that id as `parent`; the status line shows only top-level runs.
|
|
222
222
|
|
|
223
|
+
`graph` is the Machine's states and transitions, published once when the run
|
|
224
|
+
starts: `initial`, `states` (each with an `id`, a `kind` of `agent`, `human`,
|
|
225
|
+
`operation`, `final`, `group`, or `state`, an Agent's `role`, and the `parent`
|
|
226
|
+
group of a nested state), and `edges` (`from`, `to`, and the `event` taken). Ids
|
|
227
|
+
are state paths joined by dots, so a nested state is `review.round`. A Machine
|
|
228
|
+
with more than 300 states or 1000 transitions publishes no graph. `trail` is the
|
|
229
|
+
states the run entered, oldest first, each with the time it was entered, as the
|
|
230
|
+
same dotted paths; it keeps the latest 200. Both are additions to version 2:
|
|
231
|
+
a reader that does not know them ignores them.
|
|
232
|
+
|
|
233
|
+
The graph does not depend on publication. `machineGraph(definition)` from
|
|
234
|
+
`@dna113p/machines` returns it for any Machine definition a program holds, or
|
|
235
|
+
undefined for one too large to draw. The value `startMachineHost` resolves to
|
|
236
|
+
carries the run's `graph` whether or not the host publishes, and its `onState`
|
|
237
|
+
callback reports each state a trail is made of.
|
|
238
|
+
|
|
223
239
|
A run hosted through `startMachineHost` from `@dna113p/machines/host` publishes
|
|
224
240
|
the same record when its host asks with `publishRunStatus: true` and
|
|
225
241
|
publication is on in the hosting process, as a daemon that runs tickets does. A
|
|
@@ -282,8 +298,17 @@ taken a response from its inbox, its record lists that delivery's id in
|
|
|
282
298
|
id stays listed for at least a minute, however many responses the run takes
|
|
283
299
|
after it, and never contains the response.
|
|
284
300
|
|
|
285
|
-
|
|
286
|
-
|
|
301
|
+
When a run finishes, `machine run`, the MCP server, and a host that asks for
|
|
302
|
+
publication all three publish the optional `note` field if the final output
|
|
303
|
+
is an object with a non-empty string `summary`. It is trimmed and limited to
|
|
304
|
+
2000 characters, including a trailing `…` when shortened
|
|
305
|
+
at a word boundary (a single word longer than the limit is cut to fit).
|
|
306
|
+
Empty or whitespace-only summaries leave the field absent. The whole note,
|
|
307
|
+
including its line breaks, is available in JSON. This optional field keeps
|
|
308
|
+
`schemaVersion` at 2; readers should accept records with or without it.
|
|
309
|
+
|
|
310
|
+
Machine input, output fields other than that summary, and Human responses are
|
|
311
|
+
never recorded. A Human prompt and its choices are written to the `0600` file while the request is
|
|
287
312
|
waiting. The standard directory is private to you; one you name instead should
|
|
288
313
|
be a directory only you can read and write.
|
|
289
314
|
|
|
@@ -333,7 +358,9 @@ session and project; `--for` narrows it to
|
|
|
333
358
|
[what one viewer shows](#drawing-your-own-widgets). `--all`, with or without `--json`, lists every published
|
|
334
359
|
run: also the ones whose records remain after they finished, as `completed` or
|
|
335
360
|
`failed`, or after their process disappeared, as `lost` (in JSON such a record
|
|
336
|
-
keeps the status it last published).
|
|
361
|
+
keeps the status it last published). For a completed or failed run, the first
|
|
362
|
+
non-empty line of its note is printed beneath the row; `--json` includes the
|
|
363
|
+
whole note. A run answered through its host shows
|
|
337
364
|
`answered through its host` in place of a request id:
|
|
338
365
|
|
|
339
366
|
```text
|
|
@@ -444,6 +471,36 @@ for the same time, so the status line must run where it can see that process.
|
|
|
444
471
|
Set `NO_COLOR` (to any value, even an empty one) for plain text and `COLUMNS`
|
|
445
472
|
to change the 120-column truncation.
|
|
446
473
|
|
|
474
|
+
### Watching runs in a browser
|
|
475
|
+
|
|
476
|
+
```bash
|
|
477
|
+
machine ui [--port <port>] [--project <dir>]
|
|
478
|
+
```
|
|
479
|
+
|
|
480
|
+
serves a live page on `127.0.0.1` (port 4777, or the next free one) of every
|
|
481
|
+
published run: what is waiting on you, what is running, and what finished, with
|
|
482
|
+
each run's Machine drawn as a graph, its current state marked, the states and
|
|
483
|
+
transitions it has taken counted, and the path it took so far. A waiting run
|
|
484
|
+
that takes outside responses is answered from the page, as `machine respond`
|
|
485
|
+
answers it; a run answered through its host, such as a daemon's, names where to
|
|
486
|
+
answer it instead. The page loads nothing from outside, accepts requests only
|
|
487
|
+
under its own host name, and answers a run only with a token it was served.
|
|
488
|
+
|
|
489
|
+
A project adds its own panels beside the runs, such as its tickets or pull
|
|
490
|
+
requests, in `.machines/ui.json` in the project (the nearest directory above the
|
|
491
|
+
current one that has a `.machines` directory, or `--project`):
|
|
492
|
+
|
|
493
|
+
```json
|
|
494
|
+
{ "panels": [{ "title": "Tickets", "command": ["node", "scripts/ui-panels.mjs", "tickets"], "refreshSeconds": 10 }] }
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
Each command runs in the project directory every `refreshSeconds` (10 by
|
|
498
|
+
default) and prints one JSON object: `{ "summary"?, "items": [{ "text",
|
|
499
|
+
"detail"?, "status"?, "href"?, "at"? }] }`, where `status` is `ok`, `warn`,
|
|
500
|
+
`bad`, `active`, or `idle`, `href` is a web link, and `at` a time. Commands are
|
|
501
|
+
project code, trusted as `.machines/` Machines are; a command that fails or
|
|
502
|
+
prints something else shows its error in its panel.
|
|
503
|
+
|
|
447
504
|
### Watching other runs
|
|
448
505
|
|
|
449
506
|
A session can pull in runs it did not start: a ticket that a daemon runs, or a
|
|
@@ -472,7 +529,8 @@ machine watch --for <session> org-9 # the same commands for another session's
|
|
|
472
529
|
- An entry keeps matching runs that start later, so a ticket can be watched
|
|
473
530
|
before its run exists. `machine watch` lists a matched run for as long as its
|
|
474
531
|
record remains, with the status `machine runs --all` gives it, so it also
|
|
475
|
-
says how a watched run ended
|
|
532
|
+
says how a watched run ended, with the first non-empty line of its note
|
|
533
|
+
beneath the row when present.
|
|
476
534
|
- `--for <session>`, first, makes `machine watch` and `machine unwatch` read
|
|
477
535
|
and change the list of the session it names instead of this process's own.
|
|
478
536
|
A program that draws [its own widgets](#drawing-your-own-widgets) for a
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dna113p/machines",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.6",
|
|
4
4
|
"description": "Reusable workflows coordinating agents, local operations, and human decisions.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"repository": {
|
|
@@ -20,30 +20,96 @@
|
|
|
20
20
|
},
|
|
21
21
|
"types": "./dist/src/index.d.ts",
|
|
22
22
|
"exports": {
|
|
23
|
-
".": {
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
"./
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
"./
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
"./
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
"./
|
|
40
|
-
|
|
23
|
+
".": {
|
|
24
|
+
"types": "./dist/src/index.d.ts",
|
|
25
|
+
"import": "./dist/src/index.js"
|
|
26
|
+
},
|
|
27
|
+
"./acp": {
|
|
28
|
+
"types": "./dist/src/acp.d.ts",
|
|
29
|
+
"import": "./dist/src/acp.js"
|
|
30
|
+
},
|
|
31
|
+
"./agy": {
|
|
32
|
+
"types": "./dist/src/agy.d.ts",
|
|
33
|
+
"import": "./dist/src/agy.js"
|
|
34
|
+
},
|
|
35
|
+
"./chatgpt-web": {
|
|
36
|
+
"types": "./dist/src/chatgpt-web.d.ts",
|
|
37
|
+
"import": "./dist/src/chatgpt-web.js"
|
|
38
|
+
},
|
|
39
|
+
"./claude": {
|
|
40
|
+
"types": "./dist/src/claude.d.ts",
|
|
41
|
+
"import": "./dist/src/claude.js"
|
|
42
|
+
},
|
|
43
|
+
"./conversation": {
|
|
44
|
+
"types": "./dist/src/conversation.d.ts",
|
|
45
|
+
"import": "./dist/src/conversation.js"
|
|
46
|
+
},
|
|
47
|
+
"./codex": {
|
|
48
|
+
"types": "./dist/src/codex.d.ts",
|
|
49
|
+
"import": "./dist/src/codex.js"
|
|
50
|
+
},
|
|
51
|
+
"./deepseek": {
|
|
52
|
+
"types": "./dist/src/deepseek.d.ts",
|
|
53
|
+
"import": "./dist/src/deepseek.js"
|
|
54
|
+
},
|
|
55
|
+
"./decision": {
|
|
56
|
+
"types": "./dist/src/decision.d.ts",
|
|
57
|
+
"import": "./dist/src/decision.js"
|
|
58
|
+
},
|
|
59
|
+
"./jev": {
|
|
60
|
+
"types": "./dist/src/jev.d.ts",
|
|
61
|
+
"import": "./dist/src/jev.js"
|
|
62
|
+
},
|
|
63
|
+
"./openrouter": {
|
|
64
|
+
"types": "./dist/src/openrouter.d.ts",
|
|
65
|
+
"import": "./dist/src/openrouter.js"
|
|
66
|
+
},
|
|
67
|
+
"./laya": {
|
|
68
|
+
"types": "./dist/src/laya.d.ts",
|
|
69
|
+
"import": "./dist/src/laya.js"
|
|
70
|
+
},
|
|
71
|
+
"./von": {
|
|
72
|
+
"types": "./dist/src/von.d.ts",
|
|
73
|
+
"import": "./dist/src/von.js"
|
|
74
|
+
},
|
|
75
|
+
"./host": {
|
|
76
|
+
"types": "./dist/src/host.d.ts",
|
|
77
|
+
"import": "./dist/src/host.js"
|
|
78
|
+
},
|
|
79
|
+
"./launcher": {
|
|
80
|
+
"types": "./dist/src/launcher.d.ts",
|
|
81
|
+
"import": "./dist/src/launcher.js"
|
|
82
|
+
},
|
|
83
|
+
"./mcp": {
|
|
84
|
+
"types": "./dist/mcp/server.d.ts",
|
|
85
|
+
"import": "./dist/mcp/server.js"
|
|
86
|
+
},
|
|
87
|
+
"./pi-extension": {
|
|
88
|
+
"types": "./dist/pi-extension/index.d.ts",
|
|
89
|
+
"import": "./dist/pi-extension/index.js"
|
|
90
|
+
},
|
|
91
|
+
"./run-status": {
|
|
92
|
+
"types": "./dist/src/run-viewer.d.ts",
|
|
93
|
+
"import": "./dist/src/run-viewer.js"
|
|
94
|
+
}
|
|
41
95
|
},
|
|
42
|
-
"files": [
|
|
96
|
+
"files": [
|
|
97
|
+
"dist",
|
|
98
|
+
"claude/statusline.mjs",
|
|
99
|
+
"docs/architecture.md",
|
|
100
|
+
"docs/authoring.md",
|
|
101
|
+
"docs/integrations.md",
|
|
102
|
+
"docs/local-decisions.md",
|
|
103
|
+
"services/laya/server.py",
|
|
104
|
+
"skills",
|
|
105
|
+
"LICENSE",
|
|
106
|
+
"README.md"
|
|
107
|
+
],
|
|
43
108
|
"scripts": {
|
|
44
109
|
"build": "node scripts/build.mjs",
|
|
45
110
|
"build:plugin": "npm run build && node scripts/build-plugin.mjs",
|
|
46
|
-
"check": "npm run typecheck && npm test",
|
|
111
|
+
"check": "npm run lint && npm run typecheck && npm test",
|
|
112
|
+
"lint": "oxlint --deny-warnings",
|
|
47
113
|
"example": "node examples/operation.ts",
|
|
48
114
|
"example:agents": "node examples/named-agents.ts",
|
|
49
115
|
"example:agy": "node examples/agy.ts",
|
|
@@ -73,6 +139,7 @@
|
|
|
73
139
|
},
|
|
74
140
|
"devDependencies": {
|
|
75
141
|
"@types/node": "^24.0.0",
|
|
142
|
+
"oxlint": "^1.87.0",
|
|
76
143
|
"typescript": "^7.0.2"
|
|
77
144
|
}
|
|
78
145
|
}
|
|
@@ -31,6 +31,11 @@ needs. Existing Machines and examples are starting points, not a fixed menu.
|
|
|
31
31
|
Make consequential checks and decisions explicit in states when they need to
|
|
32
32
|
control progress. Choose the complexity needed to complete the task reliably.
|
|
33
33
|
|
|
34
|
+
Machine Shop is an optional starter library of common software-task Machines,
|
|
35
|
+
kept in its own repository at `https://github.com/dna113p/machine-shop`. Nothing
|
|
36
|
+
here requires it; clone it to try a default or adapt one, as
|
|
37
|
+
[machine-project-setup](../machine-project-setup/SKILL.md) describes.
|
|
38
|
+
|
|
34
39
|
Look for common task sequences and recurring manual workflows while doing the
|
|
35
40
|
work. Convert them into Machines when explicit steps would improve repeatability
|
|
36
41
|
or code quality. Parameterize assignment details so the workflow can serve the
|
|
@@ -82,3 +87,29 @@ Agent runners or injected Human input where possible. A successful run reaches
|
|
|
82
87
|
the intended final state and produces the requested artifact or effect. Run real
|
|
83
88
|
external work only within the user's authorized scope. Report what was verified
|
|
84
89
|
and any branch that still requires a configured harness or human interaction.
|
|
90
|
+
|
|
91
|
+
## Checks the Machine owns
|
|
92
|
+
|
|
93
|
+
Design the workflow so these checks control progress and leave evidence for the
|
|
94
|
+
next repair round or person taking over:
|
|
95
|
+
|
|
96
|
+
- Run every gate, not only up to the first failure, so one repair round sees
|
|
97
|
+
all failures.
|
|
98
|
+
- Keep the project's gate list in one place that every route reads.
|
|
99
|
+
- For new behaviour, write tests first; have an Operation see them fail before
|
|
100
|
+
implementation, then pass after implementation.
|
|
101
|
+
- After each editing Agent, run an Operation that compares the changed paths
|
|
102
|
+
against the paths the assignment allows.
|
|
103
|
+
- Ask for a structured review: each acceptance criterion met or not, findings
|
|
104
|
+
tagged by severity. Let an Operation, not the reviewer's verdict word, decide
|
|
105
|
+
what happens next.
|
|
106
|
+
- Match a reviewer's findings across rounds by their place in the conversation,
|
|
107
|
+
never by their wording.
|
|
108
|
+
- End stuck unattended runs with work kept and a written note of what a person
|
|
109
|
+
must decide, instead of waiting in a Human state. Every stop keeps both so a later
|
|
110
|
+
run or a person can continue from them.
|
|
111
|
+
- Before implementation, have a read-only Agent try to show the assignment is
|
|
112
|
+
wrong, already done, or underspecified; require options and a recommended answer
|
|
113
|
+
for each question it raises.
|
|
114
|
+
- Tell editing Agents that checks they cannot run in their environment are the
|
|
115
|
+
workflow's to run, so a sandboxed Agent returns its work instead of `blocked`.
|