@dna113p/machines 0.2.0 → 0.3.1

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 (79) hide show
  1. package/README.md +115 -11
  2. package/claude/statusline.mjs +277 -0
  3. package/dist/examples/chatgpt-web.d.ts +1 -0
  4. package/dist/examples/chatgpt-web.js +36 -0
  5. package/dist/examples/claude.d.ts +1 -0
  6. package/dist/examples/claude.js +30 -0
  7. package/dist/examples/codex.d.ts +1 -0
  8. package/dist/examples/codex.js +30 -0
  9. package/dist/examples/decision.d.ts +1 -0
  10. package/dist/examples/decision.js +66 -0
  11. package/dist/examples/deepseek.d.ts +1 -0
  12. package/dist/examples/deepseek.js +27 -0
  13. package/dist/mcp/server.d.ts +1 -0
  14. package/dist/mcp/server.js +74 -11
  15. package/dist/pi-extension/index.js +16 -9
  16. package/dist/src/acp.js +2 -1
  17. package/dist/src/agent-bindings.d.ts +6 -0
  18. package/dist/src/agent-bindings.js +35 -5
  19. package/dist/src/agent-limit.d.ts +25 -0
  20. package/dist/src/agent-limit.js +34 -0
  21. package/dist/src/agent-limits.d.ts +37 -0
  22. package/dist/src/agent-limits.js +212 -0
  23. package/dist/src/agent-protocol.js +3 -1
  24. package/dist/src/catalog.d.ts +3 -0
  25. package/dist/src/catalog.js +84 -12
  26. package/dist/src/chatgpt-web.d.ts +143 -0
  27. package/dist/src/chatgpt-web.js +946 -0
  28. package/dist/src/claude.d.ts +35 -0
  29. package/dist/src/claude.js +294 -0
  30. package/dist/src/cli.js +282 -16
  31. package/dist/src/codex.d.ts +16 -0
  32. package/dist/src/codex.js +176 -0
  33. package/dist/src/conversation.d.ts +74 -0
  34. package/dist/src/conversation.js +46 -0
  35. package/dist/src/coordinator-guidance.d.ts +3 -0
  36. package/dist/src/coordinator-guidance.js +17 -0
  37. package/dist/src/decision.d.ts +29 -0
  38. package/dist/src/decision.js +67 -0
  39. package/dist/src/deepseek.d.ts +12 -0
  40. package/dist/src/deepseek.js +16 -0
  41. package/dist/src/host.d.ts +11 -0
  42. package/dist/src/host.js +153 -16
  43. package/dist/src/index.d.ts +39 -2
  44. package/dist/src/index.js +62 -5
  45. package/dist/src/jev.d.ts +15 -0
  46. package/dist/src/jev.js +90 -0
  47. package/dist/src/json.d.ts +6 -0
  48. package/dist/src/json.js +28 -0
  49. package/dist/src/launcher.d.ts +11 -1
  50. package/dist/src/launcher.js +72 -3
  51. package/dist/src/laya.d.ts +15 -0
  52. package/dist/src/laya.js +13 -0
  53. package/dist/src/openrouter.d.ts +16 -0
  54. package/dist/src/openrouter.js +97 -0
  55. package/dist/src/run-inbox.d.ts +49 -0
  56. package/dist/src/run-inbox.js +233 -0
  57. package/dist/src/run-status.d.ts +121 -0
  58. package/dist/src/run-status.js +336 -0
  59. package/dist/src/run-watch.d.ts +24 -0
  60. package/dist/src/run-watch.js +112 -0
  61. package/dist/src/session.d.ts +15 -2
  62. package/dist/src/session.js +47 -8
  63. package/dist/src/system-one-http.d.ts +25 -0
  64. package/dist/src/system-one-http.js +146 -0
  65. package/dist/src/terminal-human.d.ts +8 -1
  66. package/dist/src/terminal-human.js +36 -8
  67. package/dist/src/von.d.ts +15 -0
  68. package/dist/src/von.js +13 -0
  69. package/docs/architecture.md +74 -3
  70. package/docs/authoring.md +809 -1
  71. package/docs/integrations.md +322 -3
  72. package/docs/local-decisions.md +212 -0
  73. package/package.json +21 -4
  74. package/services/laya/server.py +230 -0
  75. package/skills/machine-builder/SKILL.md +56 -6
  76. package/skills/machine-delegation/SKILL.md +128 -0
  77. package/skills/machine-delegation/references/delegation-machine.md +90 -0
  78. package/skills/machine-project-setup/SKILL.md +181 -0
  79. package/skills/machine-project-setup/references/project-workflows.md +50 -0
@@ -0,0 +1,146 @@
1
+ import * as v from "valibot";
2
+ const nonBlank = v.pipe(v.string(), v.check(value => value.trim().length > 0));
3
+ const probability = v.pipe(v.number(), v.minValue(0), v.maxValue(1));
4
+ const responseSchema = v.object({
5
+ model: nonBlank,
6
+ answers: v.object({
7
+ decision: v.object({
8
+ type: v.literal("choice"),
9
+ choice: nonBlank,
10
+ probabilities: v.pipe(v.unknown(), v.check(value => !Array.isArray(value)), v.record(v.string(), probability)),
11
+ confidence: probability,
12
+ }),
13
+ }),
14
+ });
15
+ const maxResponseBytes = 1_048_576;
16
+ /** Internal plumbing shared by Von and Laya, not a new workflow primitive. */
17
+ export function systemOneHttpProvider(service, options) {
18
+ const { label, envPrefix } = service;
19
+ const timeoutMs = options.timeoutMs ?? 30_000;
20
+ if (!Number.isInteger(timeoutMs) || timeoutMs <= 0 || timeoutMs > 2_147_483_647) {
21
+ throw new Error(`${label} timeoutMs must be a positive 32-bit integer`);
22
+ }
23
+ return {
24
+ name: service.name,
25
+ async decide(request) {
26
+ // Resolve all environment configuration lazily: discovery never contacts a
27
+ // service, validates credentials, starts Python, or downloads model weights.
28
+ const endpoint = serviceEndpoint(options.baseUrl ?? process.env[`${envPrefix}_BASE_URL`] ?? service.baseUrl, label);
29
+ const apiKey = options.apiKey ?? process.env[`${envPrefix}_API_KEY`];
30
+ if (apiKey !== undefined && (typeof apiKey !== "string" || !/^[\x21-\x7e]+$/.test(apiKey))) {
31
+ throw new Error(`${label} apiKey must be a non-empty ASCII Bearer token`);
32
+ }
33
+ const model = options.model ?? process.env[`${envPrefix}_MODEL`] ?? service.model;
34
+ if (typeof model !== "string" || !model.trim())
35
+ throw new Error(`${label} model must be non-empty`);
36
+ const choices = Object.entries(request.choices);
37
+ if (choices.length < service.minChoices) {
38
+ throw new Error(`${label} Choice requires at least ${service.minChoices} outcome(s)`);
39
+ }
40
+ if (!request.question?.trim() || !request.input?.trim()
41
+ || choices.some(([key, value]) => !key.trim() || key !== key.trim()
42
+ || (value !== null && (typeof value !== "string" || !value.trim())))) {
43
+ throw new Error(`${label} requires non-empty evidence, question, and valid choices`);
44
+ }
45
+ const controller = new AbortController();
46
+ const timeoutError = () => new Error(`${label} request timed out after ${timeoutMs}ms`);
47
+ let timer;
48
+ const timeout = new Promise((_resolve, reject) => {
49
+ timer = setTimeout(() => { controller.abort(); reject(timeoutError()); }, timeoutMs);
50
+ });
51
+ const execute = async () => {
52
+ let response;
53
+ try {
54
+ response = await (options.fetch ?? globalThis.fetch)(endpoint, {
55
+ method: "POST", redirect: "error", signal: controller.signal,
56
+ headers: {
57
+ "Content-Type": "application/json",
58
+ ...(apiKey === undefined ? {} : { Authorization: `Bearer ${apiKey}` }),
59
+ },
60
+ body: JSON.stringify({
61
+ model, state: request.input,
62
+ questions: {
63
+ decision: { type: "choice", instructions: request.question, criteria: request.choices },
64
+ },
65
+ }),
66
+ });
67
+ }
68
+ catch {
69
+ throw controller.signal.aborted ? timeoutError() : new Error(`${label} network request failed`);
70
+ }
71
+ if (controller.signal.aborted || !response.ok) {
72
+ // Do not wait on untrusted error streams, or expose their contents.
73
+ void response.body?.cancel().catch(() => { });
74
+ if (controller.signal.aborted)
75
+ throw timeoutError();
76
+ throw new Error(`${label} request failed (HTTP ${response.status}); no automatic retry`);
77
+ }
78
+ let body;
79
+ const reader = response.body?.getReader();
80
+ const cancel = () => { void reader?.cancel().catch(() => { }); };
81
+ controller.signal.addEventListener("abort", cancel, { once: true });
82
+ try {
83
+ const chunks = [];
84
+ let bytes = 0;
85
+ if (reader) {
86
+ while (true) {
87
+ const { value, done } = await reader.read();
88
+ if (done)
89
+ break;
90
+ bytes += value.byteLength;
91
+ if (bytes > maxResponseBytes) {
92
+ cancel();
93
+ throw new RangeError("Response size limit");
94
+ }
95
+ chunks.push(value);
96
+ }
97
+ }
98
+ body = JSON.parse(Buffer.concat(chunks).toString("utf8"));
99
+ }
100
+ catch (error) {
101
+ if (controller.signal.aborted)
102
+ throw timeoutError();
103
+ throw new Error(error instanceof RangeError
104
+ ? `${label} response exceeds 1 MiB` : `${label} returned invalid JSON`);
105
+ }
106
+ finally {
107
+ controller.signal.removeEventListener("abort", cancel);
108
+ reader?.releaseLock();
109
+ }
110
+ if (controller.signal.aborted)
111
+ throw timeoutError();
112
+ const parsed = v.safeParse(responseSchema, body);
113
+ if (!parsed.success)
114
+ throw new Error(`${label} returned an invalid Choice response`);
115
+ const { choice, probabilities, confidence } = parsed.output.answers.decision;
116
+ if (!Object.hasOwn(probabilities, choice)
117
+ || Object.values(probabilities).some(value => value > probabilities[choice])) {
118
+ throw new Error(`${label} choice must have the highest probability`);
119
+ }
120
+ return { choice, probabilities, confidence, model: parsed.output.model };
121
+ };
122
+ try {
123
+ return await Promise.race([execute(), timeout]);
124
+ }
125
+ finally {
126
+ clearTimeout(timer);
127
+ }
128
+ },
129
+ };
130
+ }
131
+ function serviceEndpoint(value, label) {
132
+ try {
133
+ if (typeof value !== "string" || !value.trim() || value !== value.trim())
134
+ throw new Error();
135
+ const url = new URL(value);
136
+ if (!["http:", "https:"].includes(url.protocol) || url.username || url.password || url.search || url.hash) {
137
+ throw new Error();
138
+ }
139
+ url.pathname = `${url.pathname.replace(/\/+$/, "")}/v1/systemone`;
140
+ return url.href;
141
+ }
142
+ catch {
143
+ // A malformed URL may contain a password; never include it in the error.
144
+ throw new Error(`${label} baseUrl must be an HTTP(S) server root without credentials, query, or fragment`);
145
+ }
146
+ }
@@ -1,2 +1,9 @@
1
1
  import type { HumanRequest } from "./index.ts";
2
- export declare function terminalHuman(request: HumanRequest): Promise<string>;
2
+ export declare class TerminalInputClosed extends Error {
3
+ constructor();
4
+ }
5
+ /**
6
+ * Asks on the process's own terminal. Aborting the signal withdraws the
7
+ * question, restores the terminal, and rejects with the signal's reason.
8
+ */
9
+ export declare function terminalHuman(request: HumanRequest, signal?: AbortSignal): Promise<string>;
@@ -1,27 +1,47 @@
1
1
  import { clearScreenDown, createInterface, cursorTo, emitKeypressEvents, moveCursor, } from "node:readline";
2
2
  import { cyan, dim, green } from "yoctocolors";
3
- export function terminalHuman(request) {
3
+ export class TerminalInputClosed extends Error {
4
+ constructor() {
5
+ super("Terminal input closed before a response");
6
+ }
7
+ }
8
+ /**
9
+ * Asks on the process's own terminal. Aborting the signal withdraws the
10
+ * question, restores the terminal, and rejects with the signal's reason.
11
+ */
12
+ export function terminalHuman(request, signal) {
13
+ if (signal?.aborted)
14
+ return Promise.reject(signal.reason);
4
15
  const values = request.choices ?? request.suggestions ?? [];
5
16
  const allowOther = request.suggestions !== undefined;
6
17
  if (values.length > 0
7
18
  && process.stdin.isTTY
8
19
  && process.stdout.isTTY
9
20
  && typeof process.stdin.setRawMode === "function") {
10
- return selectHumanInput(request.prompt, values, allowOther);
21
+ return selectHumanInput(request.prompt, values, allowOther, signal);
11
22
  }
12
- return readFreeformInput(request.prompt, values, allowOther ? "Suggestions" : "Choices");
23
+ return readFreeformInput(request.prompt, values, allowOther ? "Suggestions" : "Choices", signal);
13
24
  }
14
- function readFreeformInput(prompt, values = [], heading = "Suggestions") {
25
+ function readFreeformInput(prompt, values = [], heading = "Suggestions", signal) {
15
26
  const terminal = createInterface({ input: process.stdin, output: process.stdout });
16
27
  const suggestionText = values.length === 0
17
28
  ? ""
18
29
  : `\n${heading}:\n${values.map((value) => `- ${value}`).join("\n")}`;
19
30
  return new Promise((resolve, reject) => {
20
31
  let answered = false;
32
+ function onAbort() {
33
+ answered = true;
34
+ terminal.close();
35
+ // Leave the unanswered prompt line behind.
36
+ process.stdout.write("\n");
37
+ reject(signal?.reason);
38
+ }
21
39
  terminal.once("close", () => {
40
+ signal?.removeEventListener("abort", onAbort);
22
41
  if (!answered)
23
- reject(new Error("Terminal input closed before a response"));
42
+ reject(new TerminalInputClosed());
24
43
  });
44
+ signal?.addEventListener("abort", onAbort, { once: true });
25
45
  terminal.question(`${prompt}${suggestionText}\n> `, (answer) => {
26
46
  answered = true;
27
47
  terminal.close();
@@ -29,7 +49,7 @@ function readFreeformInput(prompt, values = [], heading = "Suggestions") {
29
49
  });
30
50
  });
31
51
  }
32
- function selectHumanInput(prompt, values, allowOther) {
52
+ function selectHumanInput(prompt, values, allowOther, signal) {
33
53
  return new Promise((resolve, reject) => {
34
54
  const choices = allowOther ? [...values, "Other…"] : [...values];
35
55
  const initialRawMode = process.stdin.isRaw;
@@ -54,13 +74,20 @@ function selectHumanInput(prompt, values, allowOther) {
54
74
  process.stdin.off("keypress", onKeypress);
55
75
  process.stdin.off("end", onInputClosed);
56
76
  process.stdin.off("close", onInputClosed);
77
+ signal?.removeEventListener("abort", onAbort);
57
78
  process.stdin.setRawMode(initialRawMode ?? false);
58
79
  if (!initialRawMode)
59
80
  process.stdin.pause();
60
81
  }
61
82
  function onInputClosed() {
62
83
  cleanup();
63
- reject(new Error("Terminal input closed before a response"));
84
+ reject(new TerminalInputClosed());
85
+ }
86
+ function onAbort() {
87
+ clearMenu();
88
+ cleanup();
89
+ process.stdout.write(`${dim("answered elsewhere")}\n`);
90
+ reject(signal?.reason);
64
91
  }
65
92
  function onKeypress(_character, key) {
66
93
  if (key.ctrl && key.name === "c") {
@@ -84,7 +111,7 @@ function selectHumanInput(prompt, values, allowOther) {
84
111
  resolve(answer);
85
112
  }
86
113
  else {
87
- void readFreeformInput("Feedback").then(resolve, reject);
114
+ void readFreeformInput("Feedback", [], undefined, signal).then(resolve, reject);
88
115
  }
89
116
  }
90
117
  }
@@ -94,6 +121,7 @@ function selectHumanInput(prompt, values, allowOther) {
94
121
  process.stdin.on("keypress", onKeypress);
95
122
  process.stdin.once("end", onInputClosed);
96
123
  process.stdin.once("close", onInputClosed);
124
+ signal?.addEventListener("abort", onAbort, { once: true });
97
125
  process.stdin.resume();
98
126
  render();
99
127
  });
@@ -0,0 +1,15 @@
1
+ import { type DecisionAgentOptions, type DecisionProvider } from "./decision.ts";
2
+ import { type SystemOneHttpOptions } from "./system-one-http.ts";
3
+ export interface VonProviderOptions extends SystemOneHttpOptions {
4
+ /** Overrides VON_BASE_URL, then http://127.0.0.1:8000. */
5
+ readonly baseUrl?: string;
6
+ /** Overrides VON_API_KEY. No TypeSafe/OpenRouter credential fallback. */
7
+ readonly apiKey?: string;
8
+ /** Overrides VON_MODEL, then von-1.0.0. Does NOT select the server backend. */
9
+ readonly model?: string;
10
+ }
11
+ export type VonAgentOptions = VonProviderOptions & DecisionAgentOptions;
12
+ /** One Choice request to a separately managed Von server; no Python dependency. */
13
+ export declare function vonProvider(options?: VonProviderOptions): DecisionProvider;
14
+ /** Convenience binding. Transitions, confidence gates, and retries stay in Machines. */
15
+ export declare function vonAgent(options?: VonAgentOptions): (request: import("./index.ts").AgentRequest, report?: import("./index.ts").AgentReporter) => Promise<import("./decision.ts").DecisionEvent>;
@@ -0,0 +1,13 @@
1
+ import { decisionAgent } from "./decision.js";
2
+ import { systemOneHttpProvider } from "./system-one-http.js";
3
+ /** One Choice request to a separately managed Von server; no Python dependency. */
4
+ export function vonProvider(options = {}) {
5
+ return systemOneHttpProvider({
6
+ name: "von", label: "Von", envPrefix: "VON",
7
+ baseUrl: "http://127.0.0.1:8000", model: "von-1.0.0", minChoices: 1,
8
+ }, options);
9
+ }
10
+ /** Convenience binding. Transitions, confidence gates, and retries stay in Machines. */
11
+ export function vonAgent(options = {}) {
12
+ return decisionAgent(vonProvider(options), options);
13
+ }
@@ -12,7 +12,7 @@ a Human uses a terminal, or a run is displayed in Codex.
12
12
  | State | Named position invoking one Agent, Human, or Operation, or a final state |
13
13
  | Agent | Bounded request implemented by an `AgentRunner` |
14
14
  | Operation | Trusted local function returning an event |
15
- | Human | Input request returning a `submitted` event with the response |
15
+ | Human | Input request returning `submitted`, or an explicitly enabled `question` event |
16
16
  | Run | One execution of a Machine |
17
17
  | Adapter | Mechanism connecting a primitive or run to an external interface |
18
18
 
@@ -37,16 +37,29 @@ MCP server ────┘ ↑
37
37
  | Module | Owns |
38
38
  | --- | --- |
39
39
  | `src/index.ts` | Typed primitives, Machine validation, XState execution, observer contracts |
40
+ | `src/conversation.ts` | Compound Human/clarification state; the owner supplies the original session responder |
40
41
  | `src/terminal-human.ts` | Default terminal Human input, selectors, raw-mode restoration |
41
42
  | `src/machine-module.ts` | Shared module contract and metadata validation |
42
43
  | `src/catalog.ts` | Definition and preset catalog reads |
43
44
  | `src/acp.ts` | ACP process/session protocol, bounded Agent result, normalized activity |
44
45
  | `src/agy.ts` | Antigravity CLI (agy) process protocol, bounded Agent result, normalized activity |
46
+ | `src/chatgpt-web.ts` | ChatGPT web conversation over the DevTools protocol, rig-bridge workspace prompt, bridge activity |
47
+ | `src/claude.ts` | Claude Code CLI headless execution, final-result decoding, permission options, normalized activity |
48
+ | `src/codex.ts` | Codex CLI execution, final-response decoding, sandbox options, normalized activity |
49
+ | `src/deepseek.ts` | DeepSeek Harness command/profile configuration over the shared ACP runner |
50
+ | `src/decision.ts` | Provider-neutral classification contract, validated outcome events, normalized observations |
51
+ | `src/jev.ts` | TypeSafe Choice HTTP transport, authentication, model selection, and bounded requests |
52
+ | `src/openrouter.ts` | OpenRouter Decisions Choice transport, independent credentials, optional result metadata |
53
+ | `src/laya.ts`, `src/von.ts` | Self-hosted Choice providers with independent configuration |
54
+ | `src/system-one-http.ts` | Shared bounded HTTP transport for the self-hosted providers |
55
+ | `services/laya/server.py` | Opt-in, externally managed single-checkpoint Python Choice bridge |
45
56
  | `src/agent-protocol.ts` | Shared Agent prompt instructions and Machines event decoding |
57
+ | `src/agent-limit.ts` | Typed provider usage-limit error and its recognition across module copies |
58
+ | `src/agent-limits.ts` | Limit-aware runner: fallback order, handoff note, reset waiting, per-run memory of limited presets, report-only requests |
46
59
  | `src/discovery.ts` | Project/global definition locations and precedence |
47
60
  | `src/launcher.ts` | Definition metadata, Agent bindings, preflight, single-use launch preparation |
48
61
  | `src/discovery-worker.ts` | Fresh metadata imports in disposable workers |
49
- | `src/host.ts` | One child run, IPC, Human request identity, owned process termination |
62
+ | `src/host.ts` | One child run, IPC, Human request identity, owned process termination, run-status publication for the run |
50
63
  | `src/session.ts` | Shared Pi/MCP run lifecycle, snapshots, response handling, retention, shutdown |
51
64
  | `src/cli.ts` | Command parsing, terminal progress, source inspection |
52
65
  | `pi-extension/index.ts` | Pi tools, widgets, notifications, native input presentation |
@@ -68,7 +81,12 @@ Operation rather than the factory.
68
81
 
69
82
  The launcher resolves required semantic Agent roles and configured presets before
70
83
  starting the actor. Missing bindings, invalid overrides, or invalid definitions
71
- fail preflight. The resulting prepared run can be started once. Long-lived hosted
84
+ fail preflight. It then wraps each role's runner so that a usage limit swaps to
85
+ the bound preset's allowed fallbacks or waits for the reset; the runtime still
86
+ receives one runner per role. An Agent state that names an `onLimit` event keeps
87
+ that policy in the Machine: the runtime asks the runner to report the limit and
88
+ delivers it as the event, and the wrapper neither swaps nor waits for that
89
+ request. The resulting prepared run can be started once. Long-lived hosted
72
90
  runs get a fresh child process; direct runtime users own their own definitions.
73
91
 
74
92
  ## Session and process lifecycle
@@ -82,6 +100,21 @@ and that request. Claiming a request before asynchronous delivery prevents two
82
100
  answers from being accepted, and the identity prevents a stale card or dialog from
83
101
  answering a later prompt. Restricted choices are validated for every interface.
84
102
 
103
+ `machine run` asks on its own terminal. With run-status publication on it also
104
+ publishes the pending request and its request ID, and takes the first valid
105
+ response from either the terminal or a private per-run inbox file written by
106
+ `machine respond`; the MCP session takes inbox responses through the same
107
+ session `respond` path as its tool, and inbox questions through the session's
108
+ `ask`. The inbox is a second way to deliver a response to the owning run, not a
109
+ second owner: the run still validates the request ID and choices, and a response
110
+ to any other request is discarded. A run hosted directly through
111
+ `startMachineHost` publishes its status without an inbox: its host remains the
112
+ only way to answer it, its record says so, and `machine respond` refuses it. See
113
+ [Integrations](integrations.md#answering-a-waiting-run-from-outside).
114
+
115
+ A session's watch list changes only which published runs its status line shows.
116
+ It gives no ownership of a run and no way to answer one.
117
+
85
118
  Closing a session terminates active and starting hosts. Host termination includes
86
119
  ordinary descendants in the host's owned process group on POSIX and process-tree
87
120
  termination on Windows. Deliberately detached processes can escape normal ownership;
@@ -95,6 +128,31 @@ Human drafts and focus belong to the presentation adapter while the request ID i
95
128
  unchanged. The card polls authoritative status; it does not decide transitions or
96
129
  persist a second copy of the workflow state.
97
130
 
131
+ ## Discussion-capable Human states
132
+
133
+ Existing Human runners may still return strings. An opt-in request with
134
+ `discussion: true` additionally permits `{ type: "question", text }`; its Human
135
+ state must explicitly handle `question`. Questions never bypass restricted
136
+ choices or become implicit approvals. The public host's `ask(question, requestId)`
137
+ claims the exact current request separately from `respond(answer, requestId)`.
138
+ The child emits the Machine event; the adapter never chooses a destination state.
139
+
140
+ `@dna113p/machines/conversation` exports `conversation(options, on)`, authoring
141
+ sugar for a bounded Human -> owner-supplied clarification -> Human loop. It keeps
142
+ one pending decision, accepts a final explicit answer, and delegates its outcome
143
+ to the surrounding Machine. Its reply callback owns the agent/session reference;
144
+ the helper does not create an assistant or promise that another call to the same
145
+ Agent role resumes the original native session. For arbitrary clarification
146
+ policies, use ordinary `human`, `agent`, and `operation` states instead.
147
+
148
+ Drip uses this public host/runtime boundary to show every Machine run and route
149
+ its Human steps through a shared web/CLI focus. Existing Pi/MCP presentation
150
+ adapters remain answer-only unless they explicitly implement the question channel.
151
+ The default terminal Human selector is also answer-only; Drip's `chat` command
152
+ provides the discussion-capable terminal surface, and `machine respond --question`
153
+ sends a question to a published run, started by `machine run` or hosted by the
154
+ MCP server, whose request allows discussion.
155
+
98
156
  ## Distribution
99
157
 
100
158
  Development runs source TypeScript on Node 24. `npm run build` emits JavaScript and
@@ -111,3 +169,16 @@ the MCP widget resource, and a hosted Human response.
111
169
  The earlier implementation plans are retained in the repository's
112
170
  [history directory](https://github.com/dna113p/machines/tree/main/docs/history) for rationale.
113
171
  Their phase ordering and manual proof gates describe past work.
172
+
173
+ ## External execution owners
174
+
175
+ The supported `@dna113p/machines/host` export exposes `startMachineHost`, the
176
+ run's id, its observations, correlated Human responses, termination, and final
177
+ state/output.
178
+ External applications can own hosted runs without importing private modules or
179
+ parsing terminal output. Hosted input and optional output are JSON values.
180
+
181
+ Ticket discovery, persistence, scheduling, and tracker updates belong to the
182
+ separate `machines-dispatch` application. Machines has no dependency on ticketing.
183
+ External owners decide how to interpret output; a final state alone does not
184
+ prove that a ticket should close.