@dna113p/machines 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (50) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +186 -0
  3. package/dist/codex/machines/ui/machines.html +286 -0
  4. package/dist/examples/agent.d.ts +1 -0
  5. package/dist/examples/agent.js +19 -0
  6. package/dist/examples/choices.d.ts +1 -0
  7. package/dist/examples/choices.js +17 -0
  8. package/dist/examples/host.d.ts +3 -0
  9. package/dist/examples/host.js +32 -0
  10. package/dist/examples/human.d.ts +1 -0
  11. package/dist/examples/human.js +17 -0
  12. package/dist/examples/named-agents.d.ts +1 -0
  13. package/dist/examples/named-agents.js +38 -0
  14. package/dist/examples/operation.d.ts +1 -0
  15. package/dist/examples/operation.js +10 -0
  16. package/dist/examples/pi.d.ts +1 -0
  17. package/dist/examples/pi.js +29 -0
  18. package/dist/mcp/server.d.ts +17 -0
  19. package/dist/mcp/server.js +313 -0
  20. package/dist/pi-extension/index.d.ts +52 -0
  21. package/dist/pi-extension/index.js +390 -0
  22. package/dist/src/acp.d.ts +7 -0
  23. package/dist/src/acp.js +184 -0
  24. package/dist/src/agent-bindings.d.ts +15 -0
  25. package/dist/src/agent-bindings.js +113 -0
  26. package/dist/src/catalog.d.ts +31 -0
  27. package/dist/src/catalog.js +68 -0
  28. package/dist/src/cli.d.ts +2 -0
  29. package/dist/src/cli.js +392 -0
  30. package/dist/src/discovery-worker.d.ts +3 -0
  31. package/dist/src/discovery-worker.js +52 -0
  32. package/dist/src/discovery.d.ts +8 -0
  33. package/dist/src/discovery.js +94 -0
  34. package/dist/src/host.d.ts +25 -0
  35. package/dist/src/host.js +357 -0
  36. package/dist/src/index.d.ts +210 -0
  37. package/dist/src/index.js +204 -0
  38. package/dist/src/launcher.d.ts +19 -0
  39. package/dist/src/launcher.js +98 -0
  40. package/dist/src/machine-module.d.ts +3 -0
  41. package/dist/src/machine-module.js +55 -0
  42. package/dist/src/session.d.ts +40 -0
  43. package/dist/src/session.js +165 -0
  44. package/dist/src/terminal-human.d.ts +2 -0
  45. package/dist/src/terminal-human.js +100 -0
  46. package/docs/architecture.md +111 -0
  47. package/docs/authoring.md +193 -0
  48. package/docs/integrations.md +142 -0
  49. package/package.json +58 -0
  50. package/skills/machine-builder/SKILL.md +34 -0
@@ -0,0 +1,165 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { startMachineHost } from "./host.js";
3
+ /** Owns runs from startup to disposal. Presentation belongs to its subscribers. */
4
+ export class MachineSession {
5
+ #runs = new Map();
6
+ #starting = new Set();
7
+ #listeners = new Set();
8
+ #closed = false;
9
+ subscribe(listener) {
10
+ this.#listeners.add(listener);
11
+ return () => this.#listeners.delete(listener);
12
+ }
13
+ async start(options) {
14
+ if (this.#closed)
15
+ throw new Error("Machine session is closed");
16
+ const cancellation = new AbortController();
17
+ this.#starting.add(cancellation);
18
+ const id = randomUUID();
19
+ const startedAt = Date.now();
20
+ const update = (type, change) => {
21
+ const run = this.#runs.get(id);
22
+ if (run === undefined || !isActive(run.status))
23
+ return;
24
+ change(run);
25
+ run.updatedAt = Date.now();
26
+ this.#emit(type, run);
27
+ };
28
+ let host;
29
+ try {
30
+ host = await startMachineHost({
31
+ ...options,
32
+ signal: cancellation.signal,
33
+ onState: (state) => update("updated", (run) => {
34
+ if (JSON.stringify(run.state) !== JSON.stringify(state))
35
+ run.agent = undefined;
36
+ run.state = state;
37
+ }),
38
+ onAgentUpdate: (agent) => {
39
+ if (agent.type === "identity")
40
+ update("updated", (run) => { run.agent = agent; });
41
+ },
42
+ onHumanRequest: (request) => update("human", (run) => {
43
+ run.status = "waiting";
44
+ run.human = request;
45
+ }),
46
+ });
47
+ }
48
+ finally {
49
+ this.#starting.delete(cancellation);
50
+ }
51
+ if (this.#closed) {
52
+ host.terminate();
53
+ throw new Error("Machine session is closed");
54
+ }
55
+ const run = {
56
+ id,
57
+ cwd: options.cwd ?? process.cwd(),
58
+ host,
59
+ startedAt,
60
+ updatedAt: Date.now(),
61
+ status: "running",
62
+ };
63
+ this.#runs.set(id, run);
64
+ void host.result.then(({ state }) => this.#finish(run, { state }), (cause) => this.#finish(run, { error: errorMessage(cause) }));
65
+ this.#emit("updated", run);
66
+ return snapshot(run);
67
+ }
68
+ status(runId) {
69
+ const runs = runId === undefined
70
+ ? [...this.#runs.values()].sort((left, right) => right.updatedAt - left.updatedAt)
71
+ : [this.#requireRun(runId)];
72
+ return runs.map(snapshot);
73
+ }
74
+ async respond(input) {
75
+ const run = this.#requireRun(input.runId);
76
+ if (run.status !== "waiting" || run.human === undefined) {
77
+ throw new Error(`Machine run "${input.runId}" is not waiting for Human input`);
78
+ }
79
+ if (run.human.requestId !== input.requestId) {
80
+ throw new Error("Machine Human request is stale; read the current requestId");
81
+ }
82
+ if (run.human.choices !== undefined && !run.human.choices.includes(input.response)) {
83
+ throw new Error(`Expected one of: ${run.human.choices.join(", ")}`);
84
+ }
85
+ // Claim synchronously in the session as well as in the host. New requests or
86
+ // terminal events arriving during IPC must never be overwritten by this reply.
87
+ run.status = "running";
88
+ run.human = undefined;
89
+ run.updatedAt = Date.now();
90
+ const sent = run.host.respond(input.response, input.requestId);
91
+ this.#emit("updated", run);
92
+ await sent;
93
+ return snapshot(run);
94
+ }
95
+ close() {
96
+ if (this.#closed)
97
+ return;
98
+ this.#closed = true;
99
+ this.#listeners.clear();
100
+ for (const cancellation of this.#starting)
101
+ cancellation.abort();
102
+ this.#starting.clear();
103
+ for (const run of this.#runs.values())
104
+ run.host.terminate();
105
+ this.#runs.clear();
106
+ }
107
+ #requireRun(id) {
108
+ const run = this.#runs.get(id);
109
+ if (run === undefined)
110
+ throw new Error(`Machine run "${id}" not found in this session`);
111
+ return run;
112
+ }
113
+ #finish(run, outcome) {
114
+ if (this.#closed || !isActive(run.status))
115
+ return;
116
+ run.status = "state" in outcome ? "completed" : "failed";
117
+ if ("state" in outcome)
118
+ run.state = outcome.state;
119
+ else
120
+ run.error = outcome.error;
121
+ run.human = undefined;
122
+ run.agent = undefined;
123
+ run.updatedAt = Date.now();
124
+ const terminal = [...this.#runs.values()]
125
+ .filter((item) => !isActive(item.status))
126
+ .sort((left, right) => right.updatedAt - left.updatedAt);
127
+ for (const item of terminal.slice(5))
128
+ this.#runs.delete(item.id);
129
+ this.#emit("finished", run);
130
+ }
131
+ #emit(type, run) {
132
+ for (const listener of this.#listeners) {
133
+ try {
134
+ listener({ type, run: snapshot(run) });
135
+ }
136
+ catch (cause) {
137
+ // Rendering and notifications cannot change the outcome of an owned run.
138
+ process.emitWarning(errorMessage(cause), { type: "MachineSessionObserverError" });
139
+ }
140
+ }
141
+ }
142
+ }
143
+ export function isActive(status) {
144
+ return status === "running" || status === "waiting";
145
+ }
146
+ function snapshot(run) {
147
+ return {
148
+ id: run.id,
149
+ machine: run.host.name,
150
+ description: run.host.description,
151
+ path: run.host.path,
152
+ cwd: run.cwd,
153
+ status: run.status,
154
+ ...(run.state === undefined ? {} : { state: structuredClone(run.state) }),
155
+ startedAt: new Date(run.startedAt).toISOString(),
156
+ updatedAt: new Date(run.updatedAt).toISOString(),
157
+ elapsedSeconds: Math.floor(((isActive(run.status) ? Date.now() : run.updatedAt) - run.startedAt) / 1_000),
158
+ ...(run.agent === undefined ? {} : { agent: { ...run.agent } }),
159
+ ...(run.human === undefined ? {} : { human: structuredClone(run.human) }),
160
+ ...(run.error === undefined ? {} : { error: run.error }),
161
+ };
162
+ }
163
+ function errorMessage(cause) {
164
+ return cause instanceof Error ? cause.message : String(cause);
165
+ }
@@ -0,0 +1,2 @@
1
+ import type { HumanRequest } from "./index.ts";
2
+ export declare function terminalHuman(request: HumanRequest): Promise<string>;
@@ -0,0 +1,100 @@
1
+ import { clearScreenDown, createInterface, cursorTo, emitKeypressEvents, moveCursor, } from "node:readline";
2
+ import { cyan, dim, green } from "yoctocolors";
3
+ export function terminalHuman(request) {
4
+ const values = request.choices ?? request.suggestions ?? [];
5
+ const allowOther = request.suggestions !== undefined;
6
+ if (values.length > 0
7
+ && process.stdin.isTTY
8
+ && process.stdout.isTTY
9
+ && typeof process.stdin.setRawMode === "function") {
10
+ return selectHumanInput(request.prompt, values, allowOther);
11
+ }
12
+ return readFreeformInput(request.prompt, values, allowOther ? "Suggestions" : "Choices");
13
+ }
14
+ function readFreeformInput(prompt, values = [], heading = "Suggestions") {
15
+ const terminal = createInterface({ input: process.stdin, output: process.stdout });
16
+ const suggestionText = values.length === 0
17
+ ? ""
18
+ : `\n${heading}:\n${values.map((value) => `- ${value}`).join("\n")}`;
19
+ return new Promise((resolve, reject) => {
20
+ let answered = false;
21
+ terminal.once("close", () => {
22
+ if (!answered)
23
+ reject(new Error("Terminal input closed before a response"));
24
+ });
25
+ terminal.question(`${prompt}${suggestionText}\n> `, (answer) => {
26
+ answered = true;
27
+ terminal.close();
28
+ resolve(answer);
29
+ });
30
+ });
31
+ }
32
+ function selectHumanInput(prompt, values, allowOther) {
33
+ return new Promise((resolve, reject) => {
34
+ const choices = allowOther ? [...values, "Other…"] : [...values];
35
+ const initialRawMode = process.stdin.isRaw;
36
+ let selected = 0;
37
+ let renderedLines = 0;
38
+ function clearMenu() {
39
+ if (renderedLines === 0)
40
+ return;
41
+ moveCursor(process.stdout, 0, -renderedLines);
42
+ cursorTo(process.stdout, 0);
43
+ clearScreenDown(process.stdout);
44
+ renderedLines = 0;
45
+ }
46
+ function render() {
47
+ clearMenu();
48
+ const lines = choices.map((choice, index) => (index === selected ? `${cyan("❯")} ${cyan(choice)}` : ` ${choice}`));
49
+ lines.push(dim("↑↓ select · enter confirm"));
50
+ process.stdout.write(`${lines.join("\n")}\n`);
51
+ renderedLines = lines.length;
52
+ }
53
+ function cleanup() {
54
+ process.stdin.off("keypress", onKeypress);
55
+ process.stdin.off("end", onInputClosed);
56
+ process.stdin.off("close", onInputClosed);
57
+ process.stdin.setRawMode(initialRawMode ?? false);
58
+ if (!initialRawMode)
59
+ process.stdin.pause();
60
+ }
61
+ function onInputClosed() {
62
+ cleanup();
63
+ reject(new Error("Terminal input closed before a response"));
64
+ }
65
+ function onKeypress(_character, key) {
66
+ if (key.ctrl && key.name === "c") {
67
+ clearMenu();
68
+ cleanup();
69
+ process.kill(process.pid, "SIGINT");
70
+ }
71
+ else if (key.name === "up" || key.name === "down") {
72
+ const direction = key.name === "up" ? -1 : 1;
73
+ selected = (selected + direction + choices.length) % choices.length;
74
+ render();
75
+ }
76
+ else if (key.name === "return" || key.name === "enter") {
77
+ const answer = choices[selected];
78
+ if (answer === undefined)
79
+ return;
80
+ clearMenu();
81
+ cleanup();
82
+ if (!allowOther || selected < values.length) {
83
+ process.stdout.write(`${green("✔")} ${answer}\n`);
84
+ resolve(answer);
85
+ }
86
+ else {
87
+ void readFreeformInput("Feedback").then(resolve, reject);
88
+ }
89
+ }
90
+ }
91
+ process.stdout.write(`${prompt}\n\n`);
92
+ emitKeypressEvents(process.stdin);
93
+ process.stdin.setRawMode(true);
94
+ process.stdin.on("keypress", onKeypress);
95
+ process.stdin.once("end", onInputClosed);
96
+ process.stdin.once("close", onInputClosed);
97
+ process.stdin.resume();
98
+ render();
99
+ });
100
+ }
@@ -0,0 +1,111 @@
1
+ # Architecture
2
+
3
+ Machines separates workflow decisions from execution and presentation. A Machine
4
+ can say “review after implementation” without knowing whether an Agent uses ACP,
5
+ a Human uses a terminal, or a run is displayed in Codex.
6
+
7
+ ## Vocabulary and ownership
8
+
9
+ | Concept | Responsibility |
10
+ | --- | --- |
11
+ | Machine | Finite-state policy mapping returned events to the next state |
12
+ | State | Named position invoking one Agent, Human, or Operation, or a final state |
13
+ | Agent | Bounded request implemented by an `AgentRunner` |
14
+ | Operation | Trusted local function returning an event |
15
+ | Human | Input request returning a `submitted` event with the response |
16
+ | Run | One execution of a Machine |
17
+ | Adapter | Mechanism connecting a primitive or run to an external interface |
18
+
19
+ XState owns transitions and actor lifecycle. Each primitive returns a semantic
20
+ event; the active state declares its accepted event types. Adapters return events
21
+ and report observations. Destination states stay in the Machine definition.
22
+
23
+ Workflow-specific retries, escalation, approval, merging, and cleanup belong in
24
+ Machines. Add shared runtime behavior when multiple real workflows require the
25
+ same mechanism and cannot reasonably own it themselves.
26
+
27
+ ## Module boundaries
28
+
29
+ ```text
30
+ CLI ──────────────────────→ launcher → runtime → AgentRunner / HumanRunner
31
+ Pi extension ──┐ ↑
32
+ ├─→ session → child host ─┘
33
+ MCP server ────┘ ↑
34
+ └── snapshots and notifications → adapter presentation
35
+ ```
36
+
37
+ | Module | Owns |
38
+ | --- | --- |
39
+ | `src/index.ts` | Typed primitives, Machine validation, XState execution, observer contracts |
40
+ | `src/terminal-human.ts` | Default terminal Human input, selectors, raw-mode restoration |
41
+ | `src/machine-module.ts` | Shared module contract and metadata validation |
42
+ | `src/catalog.ts` | Definition and preset catalog reads |
43
+ | `src/acp.ts` | ACP process/session protocol, bounded Agent result, normalized activity |
44
+ | `src/discovery.ts` | Project/global definition locations and precedence |
45
+ | `src/launcher.ts` | Definition metadata, Agent bindings, preflight, single-use launch preparation |
46
+ | `src/discovery-worker.ts` | Fresh metadata imports in disposable workers |
47
+ | `src/host.ts` | One child run, IPC, Human request identity, owned process termination |
48
+ | `src/session.ts` | Shared Pi/MCP run lifecycle, snapshots, response handling, retention, shutdown |
49
+ | `src/cli.ts` | Command parsing, terminal progress, source inspection |
50
+ | `pi-extension/index.ts` | Pi tools, widgets, notifications, native input presentation |
51
+ | `mcp/server.ts` | MCP schemas, tool results, and the optional HTML resource |
52
+ | `codex/machines/ui/machines.html` | Status rendering and submission of the current Human request |
53
+
54
+ ## Discovery and launch
55
+
56
+ Discovery combines the nearest project directory with the global directory. Each
57
+ listing uses a disposable worker so edits to definitions, presets, and imported
58
+ helpers are visible without restarting the interface. An invalid definition has
59
+ an `error` in its listing; valid definitions remain available. The CLI displays
60
+ all entries and exits unsuccessfully if any definition is invalid.
61
+
62
+ Workers isolate module caches and ordinary output, not file or network permissions.
63
+ Importing metadata still executes code. Definition factories run during launch
64
+ preparation, so side effects that require a successful preflight belong inside an
65
+ Operation rather than the factory.
66
+
67
+ The launcher resolves required semantic Agent roles and configured presets before
68
+ starting the actor. Missing bindings, invalid overrides, or invalid definitions
69
+ fail preflight. The resulting prepared run can be started once. Long-lived hosted
70
+ runs get a fresh child process; direct runtime users own their own definitions.
71
+
72
+ ## Session and process lifecycle
73
+
74
+ Pi and MCP delegate to the same session module. The session owns hosts from the
75
+ start of launch, keeps authoritative snapshots, and closes permanently. Presentation
76
+ adapters subscribe to changes and format them for their own surfaces.
77
+
78
+ Each pending Human prompt has a unique request ID. A response names both the run
79
+ and that request. Claiming a request before asynchronous delivery prevents two
80
+ answers from being accepted, and the identity prevents a stale card or dialog from
81
+ answering a later prompt. Restricted choices are validated for every interface.
82
+
83
+ Closing a session terminates active and starting hosts. Host termination includes
84
+ ordinary descendants in the host's owned process group on POSIX and process-tree
85
+ termination on Windows. Deliberately detached processes can escape normal ownership;
86
+ Machines should await work they own and avoid launching untracked background jobs.
87
+ Abrupt operating-system termination cannot guarantee cleanup. Stopping processes
88
+ also cannot reverse a completed external action.
89
+ The automated process-lifecycle checks run on Linux; the Windows termination path
90
+ has not been exercised in this environment.
91
+
92
+ Human drafts and focus belong to the presentation adapter while the request ID is
93
+ unchanged. The card polls authoritative status; it does not decide transitions or
94
+ persist a second copy of the workflow state.
95
+
96
+ ## Distribution
97
+
98
+ Development runs source TypeScript on Node 24. `npm run build` emits JavaScript and
99
+ declarations under `dist/`, rewrites relative TypeScript import extensions, and
100
+ copies UI assets at the same relative paths used by the server. The npm package
101
+ exports built entry points so runtime loading works inside `node_modules`.
102
+
103
+ `npm run build:plugin` assembles a self-contained directory with built code, UI,
104
+ authoring instructions, and production dependencies. It needs Node on PATH but
105
+ has no dependency on the source checkout after copying. Packaging smoke tests
106
+ exercise a real installed tarball and a relocated plugin, including discovery,
107
+ the MCP widget resource, and a hosted Human response.
108
+
109
+ The earlier implementation plans are retained in the repository's
110
+ [history directory](https://github.com/dna113p/machines/tree/main/docs/history) for rationale.
111
+ Their phase ordering and manual proof gates describe past work.
@@ -0,0 +1,193 @@
1
+ # Authoring Machines
2
+
3
+ ## Definition contract
4
+
5
+ Store a workflow in `.machines/name.ts` or `.machines/name/index.ts` in the project.
6
+ The [README quickstart](../README.md#run-your-own-workflows) is a complete minimal
7
+ definition. Export a non-empty, one-line `description` and a default factory:
8
+
9
+ ```ts
10
+ import type { MachinePrimitives } from "@dna113p/machines";
11
+
12
+ export const description = "Prints a supplied message after a Human confirms it.";
13
+
14
+ export default function printMessage(
15
+ { machine, operation, human, final }: MachinePrimitives,
16
+ input: string,
17
+ ) {
18
+ return machine({
19
+ initial: "confirm",
20
+ states: {
21
+ confirm: human(`Print ${JSON.stringify(input)}?`, {
22
+ submitted: "print",
23
+ }, { choices: ["print"] }),
24
+ print: operation(() => {
25
+ console.log(input);
26
+ return { type: "completed" };
27
+ }, { completed: "done" }),
28
+ done: final(),
29
+ },
30
+ });
31
+ }
32
+ ```
33
+
34
+ Factories receive primitives, so workflow execution need not resolve the runtime
35
+ package from a global `~/.machines/` folder. A type-only import is erased by Node;
36
+ installing the local package makes those types available to your editor.
37
+
38
+ Keep top-level evaluation free of side effects. Discovery imports the definition
39
+ and its helpers to inspect metadata. Keep effects inside states: the launcher
40
+ calls the factory before it finishes Agent preflight.
41
+
42
+ ## Events and Human input
43
+
44
+ An Operation or Agent returns `{ type: "eventName", ...data }`. Its state maps
45
+ that type to a target. An unexpected event fails the run. Human returns
46
+ `{ type: "submitted", value: response }`; use an XState transition action or guard
47
+ to inspect `event.value`.
48
+
49
+ ```ts
50
+ human("Feedback?", { submitted: "revise" });
51
+ human("Approve or give feedback", { submitted: "decide" }, {
52
+ suggestions: ["approve"],
53
+ });
54
+ human("Choose the outcome", { submitted: "decide" }, {
55
+ choices: ["approve", "deny"],
56
+ });
57
+ ```
58
+
59
+ Suggestions preserve free-form input. Explicit choices are restrictive and are
60
+ validated by the runtime across terminal, injected, Pi, and MCP Human runners.
61
+ Both properties cannot be supplied together. Terminal selectors support arrow
62
+ keys; piped input stays plain text.
63
+
64
+ Here is a complete Agent/Human feedback loop. Save as `.machines/review-task.ts`:
65
+
66
+ ```ts
67
+ import type { Event, MachinePrimitives } from "@dna113p/machines";
68
+
69
+ export const description = "Implements a task and revises it until the Human approves.";
70
+ export const agentRoles = { implementer: "Implements the task and review feedback" };
71
+
72
+ export default function reviewTask(
73
+ { machine, agent, human, final }: MachinePrimitives,
74
+ input: string,
75
+ ) {
76
+ let feedback = "";
77
+ return machine({
78
+ initial: "implement",
79
+ states: {
80
+ implement: agent(
81
+ () => `${input}\n${feedback}`,
82
+ { completed: "review" },
83
+ { using: "implementer", cwd: process.cwd() },
84
+ ),
85
+ review: human("Approve or describe changes", {
86
+ submitted: [
87
+ { guard: ({ event }: { event: Event }) => event.value === "approve", target: "done" },
88
+ {
89
+ target: "implement",
90
+ actions: ({ event }: { event: Event }) => { feedback = String(event.value); },
91
+ },
92
+ ],
93
+ }, { suggestions: ["approve"] }),
94
+ done: final(),
95
+ },
96
+ });
97
+ }
98
+ ```
99
+
100
+ The Machine owns this loop. An Agent runner only returns an allowed outcome; it
101
+ does not decide destination states. Add verification Operations where the workflow
102
+ has a concrete condition to check.
103
+
104
+ ## Agent presets
105
+
106
+ Named Agent states select a semantic role with `using`. Declare each named role in
107
+ `agentRoles` with a one-line description. Unnamed states use `default`.
108
+
109
+ The launcher provides `default` through `npx -y pi-acp`. For deterministic local
110
+ practice, this `.machines/agents.ts` preset satisfies the example role without a
111
+ model or credentials:
112
+
113
+ ```ts
114
+ export default () => ({
115
+ implementer: {
116
+ description: "Demonstrates the review loop without changing files",
117
+ runner: async () => ({ type: "completed" }),
118
+ },
119
+ });
120
+ ```
121
+
122
+ Run `machine agents`, then `machine run review-task "Demonstrate the loop"`.
123
+ Supply feedback once, then `approve`, to exercise both transitions.
124
+
125
+ To use a real ACP harness, replace the preset with:
126
+
127
+ ```ts
128
+ export default ({ acpAgent }: { acpAgent: typeof import("@dna113p/machines/acp").acpAgent }) => ({
129
+ implementer: {
130
+ description: "Uses the locally configured Pi Agent to implement changes",
131
+ harness: "pi-acp",
132
+ runner: acpAgent("npx", ["-y", "pi-acp"], {
133
+ harness: "pi-acp",
134
+ output: "capture",
135
+ }),
136
+ },
137
+ });
138
+ ```
139
+
140
+ Install/configure the selected harness and its model credentials first. `acpAgent`
141
+ accepts any compatible ACP command and argument array; the Pi adapter uses ACP as
142
+ its supported execution path. The former `machines/pi` export has been retired;
143
+ use `acpAgent` from `machines/acp` for Pi execution.
144
+
145
+ Global presets live in `~/.machines/agents.ts`; project presets replace matching
146
+ global names. Each preset requires a description and runner. Optional `harness`,
147
+ `model`, and `thinking` fields are discovery labels, while the runner reports its
148
+ actual identity during execution. Harness configuration belongs in the runner
149
+ command, arguments, or `env` option; labels do not change model settings.
150
+
151
+ Use `machine agents` to inspect configured names. Rebind a role for one run:
152
+
153
+ ```bash
154
+ machine run review-task --agent implementer=anotherConfiguredPreset -- "Implement the task"
155
+ ```
156
+
157
+ Repeat `--agent role=preset` for multiple overrides. `--` ends launcher options.
158
+ The launcher rejects unknown roles/presets, duplicate overrides, and missing roles
159
+ before starting the actor. `machine list` refreshes metadata on each call and
160
+ reports missing bindings or invalid definitions.
161
+ `ready` describes valid metadata and resolved roles; it does not verify that a
162
+ real harness is installed, authenticated, or able to run the requested model.
163
+
164
+ ## Programmatic execution and verification
165
+
166
+ Direct runtime users supply runners through `run(definition, { agents, human })`.
167
+ An `AgentRunner` receives `prompt`, allowed `outcomes`, and optional `cwd`, plus an
168
+ optional reporter for `identity`, `output`, and `tool` updates. It returns one event.
169
+ A `HumanRunner` receives the prompt, suggestions, or choices and returns a string.
170
+
171
+ For example, exercise the feedback loop in an installed project:
172
+
173
+ ```ts
174
+ import { prepareMachineRun } from "@dna113p/machines/launcher";
175
+
176
+ const answers = ["Please revise", "approve"];
177
+ const prepared = await prepareMachineRun({
178
+ cwd: process.cwd(),
179
+ machine: "review-task",
180
+ input: "Demonstrate the loop",
181
+ });
182
+ const result = await prepared.start({ human: () => answers.shift() ?? "approve" });
183
+ console.log(result.value); // done
184
+ ```
185
+
186
+ Use the fake preset above for deterministic verification. A prepared run is
187
+ single-use. Omit `human` to retain terminal input. For direct runtime examples,
188
+ see the built package's `dist/examples/operation.js`, `human.js`, and
189
+ `named-agents.js`; source equivalents live in the checkout's `examples/`.
190
+
191
+ Verify the final state and the expected effect, including a feedback/error branch
192
+ when it changes behavior. Real Agent execution is a separate check that requires
193
+ the configured harness and any external authorization for the task.