@dna113p/machines 0.3.4 → 0.3.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.
@@ -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
+ }
@@ -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. */
@@ -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.
@@ -220,6 +220,16 @@ 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
+
223
233
  A run hosted through `startMachineHost` from `@dna113p/machines/host` publishes
224
234
  the same record when its host asks with `publishRunStatus: true` and
225
235
  publication is on in the hosting process, as a daemon that runs tickets does. A
@@ -282,8 +292,17 @@ taken a response from its inbox, its record lists that delivery's id in
282
292
  id stays listed for at least a minute, however many responses the run takes
283
293
  after it, and never contains the response.
284
294
 
285
- Machine input, Machine output, and Human responses are never recorded. A Human
286
- prompt and its choices are written to the `0600` file while the request is
295
+ When a run finishes, `machine run`, the MCP server, and a host that asks for
296
+ publication all three publish the optional `note` field if the final output
297
+ is an object with a non-empty string `summary`. It is trimmed and limited to
298
+ 2000 characters, including a trailing `…` when shortened
299
+ at a word boundary (a single word longer than the limit is cut to fit).
300
+ Empty or whitespace-only summaries leave the field absent. The whole note,
301
+ including its line breaks, is available in JSON. This optional field keeps
302
+ `schemaVersion` at 2; readers should accept records with or without it.
303
+
304
+ Machine input, output fields other than that summary, and Human responses are
305
+ never recorded. A Human prompt and its choices are written to the `0600` file while the request is
287
306
  waiting. The standard directory is private to you; one you name instead should
288
307
  be a directory only you can read and write.
289
308
 
@@ -333,7 +352,9 @@ session and project; `--for` narrows it to
333
352
  [what one viewer shows](#drawing-your-own-widgets). `--all`, with or without `--json`, lists every published
334
353
  run: also the ones whose records remain after they finished, as `completed` or
335
354
  `failed`, or after their process disappeared, as `lost` (in JSON such a record
336
- keeps the status it last published). A run answered through its host shows
355
+ keeps the status it last published). For a completed or failed run, the first
356
+ non-empty line of its note is printed beneath the row; `--json` includes the
357
+ whole note. A run answered through its host shows
337
358
  `answered through its host` in place of a request id:
338
359
 
339
360
  ```text
@@ -444,6 +465,36 @@ for the same time, so the status line must run where it can see that process.
444
465
  Set `NO_COLOR` (to any value, even an empty one) for plain text and `COLUMNS`
445
466
  to change the 120-column truncation.
446
467
 
468
+ ### Watching runs in a browser
469
+
470
+ ```bash
471
+ machine ui [--port <port>] [--project <dir>]
472
+ ```
473
+
474
+ serves a live page on `127.0.0.1` (port 4777, or the next free one) of every
475
+ published run: what is waiting on you, what is running, and what finished, with
476
+ each run's Machine drawn as a graph, its current state marked, the states and
477
+ transitions it has taken counted, and the path it took so far. A waiting run
478
+ that takes outside responses is answered from the page, as `machine respond`
479
+ answers it; a run answered through its host, such as a daemon's, names where to
480
+ answer it instead. The page loads nothing from outside, accepts requests only
481
+ under its own host name, and answers a run only with a token it was served.
482
+
483
+ A project adds its own panels beside the runs, such as its tickets or pull
484
+ requests, in `.machines/ui.json` in the project (the nearest directory above the
485
+ current one that has a `.machines` directory, or `--project`):
486
+
487
+ ```json
488
+ { "panels": [{ "title": "Tickets", "command": ["node", "scripts/ui-panels.mjs", "tickets"], "refreshSeconds": 10 }] }
489
+ ```
490
+
491
+ Each command runs in the project directory every `refreshSeconds` (10 by
492
+ default) and prints one JSON object: `{ "summary"?, "items": [{ "text",
493
+ "detail"?, "status"?, "href"?, "at"? }] }`, where `status` is `ok`, `warn`,
494
+ `bad`, `active`, or `idle`, `href` is a web link, and `at` a time. Commands are
495
+ project code, trusted as `.machines/` Machines are; a command that fails or
496
+ prints something else shows its error in its panel.
497
+
447
498
  ### Watching other runs
448
499
 
449
500
  A session can pull in runs it did not start: a ticket that a daemon runs, or a
@@ -472,7 +523,8 @@ machine watch --for <session> org-9 # the same commands for another session's
472
523
  - An entry keeps matching runs that start later, so a ticket can be watched
473
524
  before its run exists. `machine watch` lists a matched run for as long as its
474
525
  record remains, with the status `machine runs --all` gives it, so it also
475
- says how a watched run ended.
526
+ says how a watched run ended, with the first non-empty line of its note
527
+ beneath the row when present.
476
528
  - `--for <session>`, first, makes `machine watch` and `machine unwatch` read
477
529
  and change the list of the session it names instead of this process's own.
478
530
  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.4",
3
+ "version": "0.3.5",
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
- ".": { "types": "./dist/src/index.d.ts", "import": "./dist/src/index.js" },
24
- "./acp": { "types": "./dist/src/acp.d.ts", "import": "./dist/src/acp.js" },
25
- "./agy": { "types": "./dist/src/agy.d.ts", "import": "./dist/src/agy.js" },
26
- "./chatgpt-web": { "types": "./dist/src/chatgpt-web.d.ts", "import": "./dist/src/chatgpt-web.js" },
27
- "./claude": { "types": "./dist/src/claude.d.ts", "import": "./dist/src/claude.js" },
28
- "./conversation": { "types": "./dist/src/conversation.d.ts", "import": "./dist/src/conversation.js" },
29
- "./codex": { "types": "./dist/src/codex.d.ts", "import": "./dist/src/codex.js" },
30
- "./deepseek": { "types": "./dist/src/deepseek.d.ts", "import": "./dist/src/deepseek.js" },
31
- "./decision": { "types": "./dist/src/decision.d.ts", "import": "./dist/src/decision.js" },
32
- "./jev": { "types": "./dist/src/jev.d.ts", "import": "./dist/src/jev.js" },
33
- "./openrouter": { "types": "./dist/src/openrouter.d.ts", "import": "./dist/src/openrouter.js" },
34
- "./laya": { "types": "./dist/src/laya.d.ts", "import": "./dist/src/laya.js" },
35
- "./von": { "types": "./dist/src/von.d.ts", "import": "./dist/src/von.js" },
36
- "./host": { "types": "./dist/src/host.d.ts", "import": "./dist/src/host.js" },
37
- "./launcher": { "types": "./dist/src/launcher.d.ts", "import": "./dist/src/launcher.js" },
38
- "./mcp": { "types": "./dist/mcp/server.d.ts", "import": "./dist/mcp/server.js" },
39
- "./pi-extension": { "types": "./dist/pi-extension/index.d.ts", "import": "./dist/pi-extension/index.js" },
40
- "./run-status": { "types": "./dist/src/run-viewer.d.ts", "import": "./dist/src/run-viewer.js" }
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": ["dist", "claude/statusline.mjs", "docs/architecture.md", "docs/authoring.md", "docs/integrations.md", "docs/local-decisions.md", "services/laya/server.py", "skills", "LICENSE", "README.md"],
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`.