@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.
@@ -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,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
- 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
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). A run answered through its host shows
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.4",
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
- ".": { "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`.