@method-ai-hq/sdk 0.15.1 → 0.16.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.
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  MIT-licensed JavaScript API and the `method` CLI. Source and releases: https://github.com/method-ai-hq/method-sdk
4
4
 
5
5
  ```sh
6
- npm install https://github.com/method-ai-hq/method-sdk/releases/download/v0.15.1/method-ai-hq-sdk-0.15.1.tgz
6
+ npm install https://github.com/method-ai-hq/method-sdk/releases/download/v0.16.0/method-ai-hq-sdk-0.16.0.tgz
7
7
  npx method --version
8
8
  ```
9
9
 
@@ -70,11 +70,11 @@ export declare const ConversionReportSchema: z.ZodObject<{
70
70
  }, "strict", z.ZodTypeAny, {
71
71
  status: "ready" | "needs_review";
72
72
  schema: "workflow-conversion/1";
73
+ attempts: number;
73
74
  references: {
74
75
  sha256: string;
75
76
  name: string;
76
77
  }[];
77
- attempts: number;
78
78
  source_sha256: string;
79
79
  workflow_sha256: string;
80
80
  selected_attempt: number;
@@ -101,11 +101,11 @@ export declare const ConversionReportSchema: z.ZodObject<{
101
101
  }, {
102
102
  status: "ready" | "needs_review";
103
103
  schema: "workflow-conversion/1";
104
+ attempts: number;
104
105
  references: {
105
106
  sha256: string;
106
107
  name: string;
107
108
  }[];
108
- attempts: number;
109
109
  source_sha256: string;
110
110
  workflow_sha256: string;
111
111
  selected_attempt: number;
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@method-ai-hq/sdk",
3
- "version": "0.15.1",
3
+ "version": "0.16.0",
4
4
  "description": "Build and run reusable .method files with checked steps and saved results.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -35,7 +35,7 @@
35
35
  "prepack": "npm --prefix ../.. run build"
36
36
  },
37
37
  "dependencies": {
38
- "@withmethod/runtime": "https://codeload.github.com/method-ai-hq/method-spec/tar.gz/c45c5ba1a3432b8d4e90ff07a7457eef5ea0f371",
38
+ "@withmethod/runtime": "https://codeload.github.com/method-ai-hq/method-spec/tar.gz/6020ed9208670972df60a27a602885bcad947c8c",
39
39
  "fflate": "^0.8.3",
40
40
  "yaml": "^2.9.0",
41
41
  "zod": "^3.25.76"
@@ -5,7 +5,7 @@ export declare const designProcedure = "# Choose the design\n\n| Requirement | S
5
5
  /** The full check rules; in concepts. */
6
6
  export declare const checkRules = "# Checks\n\nAdd a check only when it catches a concrete failure that matters to the result. Output types and the runtime already validate shapes; do not repeat them. Do not check wording, headings, keywords, lengths, or counts unless the task requires them: \"write accurate prose\" is not a reason to match strings.\n\nUse the simplest check that proves the fact: a built-in equals, count, present, or file check, a script, or an agent check. Local file writes need no check: the runtime observes files connections itself. A change to a service, an API, email, or a browser that sends needs an effect that reads the result back, because a receipt or a 200 status shows only that the request was accepted. Use a built-in observer (http, sqlite, file) when one fits. When a change cannot or need not be observed, such as a browser step that only reads, write no_effect_reason instead.\n\nExisting checks and tests are implementation choices, not user requirements. Remove a check that is unnecessary, repeats other validation, or enforces a requirement that nobody asked for. Delete the tests and instructions that exist only for it, and do not replace it. Do not change useful output only to satisfy a check.\n";
7
7
  /** The rules that make a first Method work quickly. They come before every other part of the guide. */
8
- export declare const firstMethodRules = "# Build a Method\n\n1. **Sign in first.** Run `method status`. If it is not signed in, run `method login` and let the user approve in the browser. Signed in, model and classification steps need no keys, each run saves a version when the file changed, and runs appear on the dashboard.\n2. **Each model or classifier request is its own step**: `call`, `agent`, or `classify`, with its prompt in the Method, so the user can change one prompt, run again, and compare. A script never calls a model API. When the user already has code that does the work, keep its fixed logic as `run` steps and move each prompt and rubric into its own step; do not wrap the code. \"The same thing\" means the same behavior in steps. If a request cannot become a step because Method lacks a feature, tell the user what is missing.\n3. **Show the design, then build it in the same turn.** Show each step with its type, purpose, and output, and a table of the prompts and rubrics in the user's existing work, each with the step that holds it. Do not wait for approval unless the user asked to approve first. Ask only for information that you cannot find and that would change the design.\n4. **Run early and often.** Write the first steps, validate, run, then add the next steps. Fix errors. Fix each warning, or accept it with the user's reason. Never add a check only to remove a warning. A new run reuses every step whose definition and inputs did not change, so each run executes only what changed. `--rerun STEP` runs a step again anyway; `--fresh` runs every step.\n5. **Keep the inputs of the existing code.** If the code takes an ID and looks up the record, the Method takes the same ID and looks it up the same way.\n6. **Use the defaults without asking.** Model steps use the account's hosted models and `classify` uses Method's classifier, so no model or classifier keys are needed. To keep the model that existing code uses, add `models: {writer: provider/model}` to the Method and use `model: writer` in the steps (or `model: provider/model` on the step); it needs no key. Keep the models that the Method or the existing code names: when a model step fails, show the error and ask before you change its model. Run records go to the user's account. When the user asks that run data stay on this computer or not be uploaded, set `run_data: device`: the run's inputs, outputs, and files stay here, and the account keeps only each step's status and timing. Model steps still send each step's input to the model to answer it; nothing is kept. Tell the user that in one line, then run again.\n7. **Keys stay out of chat.** Declare each key that a script needs under `secrets:` with its purpose; every script of the Method receives all of its declared secrets. Run `method secret find` in the Method's folder: it lists the key files nearby and the names in each, never the values, and prints the import command. Run that command and tell the user which file each key came from: the key stays on this computer, so there is nothing to ask. If it finds nothing, ask the user to run `method secret set NAME`, which opens a private form in their browser. Never open, print, or search a key file (`cat`, `grep`, an editor), including Method's own secret store, and never ask for a value in chat: the values would go into the chat. When a run says Method's model credit is used up, run `method config model-key` yourself: it takes the user's own OpenRouter key from the one nearby key file that holds it, or opens a private form for it. Then run again.\n8. **Choose a sample yourself** from the user's data, and check that it has the sources the Method needs. Ask only when there is no good sample.\n9. **Iterate.** `method run` prints a `dashboard` link when the run starts and again with its result. Give the user the link as soon as the run starts. After a good run, show the result and the link, and ask what to change. Change the step and run again. Show only results that a run made: never write or edit a result by hand, and fix a wrong result in the Method, not in the data it reads. If you cannot run, say so.\n10. **Publish** with `method publish FILE --reason TEXT` when the user wants to share or schedule a version. It runs the Method's cases first.\n11. **Talk about the work, not about Method.** Unless the user asks, do not mention Method's versions, formats, settings files, or other internal files, and do not ask the user about them, about models, or about which saved version to use. Method updates itself and converts older Method files when it runs them. When the user asks whether a Method works, or asks you to make it work, run it with the inputs of a recent run (`--inputs-from RUN_ID`) and show the result and the dashboard link in a few sentences. Say that it works only after a run completed.\n12. **One file for each Method.** Change and run a Method in its own file. Do not copy it to a temporary or other folder to change it: Method does not run or save the copy while the Method's file exists, and names that file.\n13. **Questions in a run are for the user.** When a run stops with `needs_input`, ask the user its `question` in plain words, wait for the answer, and continue the run with it as the output says. Never answer it yourself.\n14. **Make each run easy to find and to run again.** Give the Method a `run_label` that tells its runs apart, such as `inputs.topic`. When an input can be empty and a default picks the value, point `run_label` at the step output that holds the resolved value, not at the input. Put the readable output, such as the report text or the message, in `result`, not only in files. Write a `run_prompt` that says where each input comes from (for example \"as_of is today's date in YYYY-MM-DD\") and what to show at the end. Leave SDK, runtime, and version details out of it.\n15. **Run again from the dashboard.** When the user pastes a \"Run again\" prompt from the dashboard, run with `--inputs-from RUN_ID` and change only what the prompt names: put those values in a file and add `--inputs FILE`.\n\nWhen a run fails, read its `fix` and `diagnostics`, change the step, and run again.\n";
8
+ export declare const firstMethodRules = "# Build a Method\n\n1. **Sign in first.** Run `method status`. If it is not signed in, run `method login` and let the user approve in the browser. Signed in, model and classification steps need no keys, each run saves a version when the file changed, and runs appear on the dashboard.\n2. **Each model or classifier request is its own step**: `call`, `agent`, or `classify`, with its prompt in the Method, so the user can change one prompt, run again, and compare. A script never calls a model API. When the user already has code that does the work, keep its fixed logic as `run` steps and move each prompt and rubric into its own step; do not wrap the code. \"The same thing\" means the same behavior in steps. If a request cannot become a step because Method lacks a feature, tell the user what is missing.\n3. **Show the design, then build it in the same turn.** Show each step with its type, purpose, and output, and a table of the prompts and rubrics in the user's existing work, each with the step that holds it. Do not wait for approval unless the user asked to approve first. Ask only for information that you cannot find and that would change the design.\n4. **Run early and often.** Write the first steps, validate, run, then add the next steps. Fix errors. Fix each warning, or accept it with the user's reason. Never add a check only to remove a warning. A new run reuses every step whose definition and inputs did not change, so each run executes only what changed. `--rerun STEP` runs a step again anyway; `--fresh` runs every step.\n5. **Keep the inputs of the existing code.** If the code takes an ID and looks up the record, the Method takes the same ID and looks it up the same way.\n6. **Use the defaults without asking.** Model steps use the account's hosted models and `classify` uses Method's classifier, so no model or classifier keys are needed. To keep the model that existing code uses, add `models: {writer: provider/model}` to the Method and use `model: writer` in the steps (or `model: provider/model` on the step); it needs no key. Keep the models that the Method or the existing code names: when a model step fails, show the error and ask before you change its model. Run records go to the user's account. When the user asks that run data stay on this computer or not be uploaded, set `run_data: device`: the run's inputs, outputs, and files stay here, and the account keeps only each step's status and timing. Model steps still send each step's input to the model to answer it; nothing is kept. Tell the user that in one line, then run again.\n7. **Keys stay out of chat.** Declare each key that a script needs under `secrets:` with its purpose; every script of the Method receives all of its declared secrets. Run `method secret find` in the Method's folder: it lists the key files nearby and the names in each, never the values, and prints the import command. Run that command and tell the user which file each key came from: the key stays on this computer, so there is nothing to ask. If it finds nothing, ask the user to run `method secret set NAME`, which opens a private form in their browser. Never open, print, or search a key file (`cat`, `grep`, an editor), including Method's own secret store, and never ask for a value in chat: the values would go into the chat. When a run says Method's model credit is used up, run `method config model-key` yourself: it takes the user's own OpenRouter key from the one nearby key file that holds it, or opens a private form for it. Then run again.\n8. **Choose a sample yourself** from the user's data, and check that it has the sources the Method needs. Ask only when there is no good sample.\n9. **Iterate.** `method run` prints a `dashboard` link when the run starts and again with its result. Give the user the link as soon as the run starts. After a good run, show the result and the link, and ask what to change. Change the step and run again. Show only results that a run made: never write or edit a result by hand, and fix a wrong result in the Method, not in the data it reads. If you cannot run, say so.\n10. **Publish** with `method publish FILE --reason TEXT` when the user wants to share or schedule a version. It runs the Method's cases first.\n11. **Talk about the work, not about Method.** Unless the user asks, do not mention Method's versions, formats, settings files, or other internal files, and do not ask the user about them, about models, or about which saved version to use. Method updates itself and converts older Method files when it runs them. When the user asks whether a Method works, or asks you to make it work, run it with the inputs of a recent run (`--inputs-from RUN_ID`) and show the result and the dashboard link in a few sentences. Say that it works only after a run completed.\n12. **One file for each Method.** Change and run a Method in its own file. Do not copy it to a temporary or other folder to change it: Method does not run or save the copy while the Method's file exists, and names that file.\n13. **Questions in a run are for the user.** When a run stops with `needs_input`, ask the user its `question` in plain words, wait for the answer, and continue the run with it as the output says. Never answer it yourself.\n14. **Make each run easy to find and to run again.** Give the Method a `run_label` that tells its runs apart, such as `inputs.topic`. When an input can be empty and a default picks the value, point `run_label` at the step output that holds the resolved value, not at the input. Put the readable output, such as the report text or the message, in `result`, not only in files. Write a `run_prompt` that says where each input comes from (for example \"as_of is today's date in YYYY-MM-DD\") and what to show at the end. Leave SDK, runtime, and version details out of it.\n15. **Run again from the dashboard.** When the user pastes a \"Run again\" prompt from the dashboard, run with `--inputs-from RUN_ID` and change only what the prompt names: put those values in a file and add `--inputs FILE`.\n\nWhen a run fails, read failure.headline and failure.next, change what they name, and run again.\n";
9
9
  /** How to write the prompt inside a call, agent, or classify step. */
10
10
  export declare const promptRules = "# Write each prompt\n\n- **One task per prompt.** If a prompt says \"then\" or has several jobs, make several steps.\n- **Say what to do.** Leave out background that does not change the output.\n- **Show 1 to 3 examples of good output.** Take them from the user's work: examples in an existing prompt, a past output that the user approved, or the first run that the user accepts. Pass a long example as an input, not as text in the prompt.\n- **Put the output shape in `out`**, not in the prompt.\n- **Say what to do when the input does not have the answer**, for example \"leave due empty\". For classify, add an option such as `unclear`.\n\nBefore:\n\n```text\nYou are an expert support analyst with years of experience. Our company values clear\ncommunication. Read the tickets, decide which ones are urgent, write a summary, and\nreturn JSON with a \"summary\" key. Make it good.\n```\n\nAfter (urgency is a classify step before this one; the shape is in `out`):\n\n```text\nWrite a five-sentence summary of this week's support tickets for the team.\nStart with the high-urgency tickets. Quote no customer names.\nWrite it in the style of this summary from an earlier week:\n\n{{example}}\n```\n";
11
11
  /** Enough of the format to write a Method without reading the JSON schema. */
@@ -45,7 +45,7 @@ export const firstMethodRules = `# Build a Method
45
45
  14. **Make each run easy to find and to run again.** Give the Method a \`run_label\` that tells its runs apart, such as \`inputs.topic\`. When an input can be empty and a default picks the value, point \`run_label\` at the step output that holds the resolved value, not at the input. Put the readable output, such as the report text or the message, in \`result\`, not only in files. Write a \`run_prompt\` that says where each input comes from (for example "as_of is today's date in YYYY-MM-DD") and what to show at the end. Leave SDK, runtime, and version details out of it.
46
46
  15. **Run again from the dashboard.** When the user pastes a "Run again" prompt from the dashboard, run with \`--inputs-from RUN_ID\` and change only what the prompt names: put those values in a file and add \`--inputs FILE\`.
47
47
 
48
- When a run fails, read its \`fix\` and \`diagnostics\`, change the step, and run again.
48
+ When a run fails, read failure.headline and failure.next, change what they name, and run again.
49
49
  `;
50
50
  /** How to write the prompt inside a call, agent, or classify step. */
51
51
  export const promptRules = `# Write each prompt
@@ -1,3 +1,5 @@
1
1
  import { type RunInspection } from "../../workflow-language/src/inspection.js";
2
+ /** The runtime's status of a finished run, in the dashboard's words; undefined for a status that is not known. */
3
+ export declare function runStatus(status: unknown): RunInspection["status"] | undefined;
2
4
  /** Convert the public trace into the dashboard's stable inspection contract. */
3
5
  export declare function inspectCurrentRun(root: string, activeSnapshot?: boolean, includeFiles?: boolean | "references"): RunInspection;
@@ -5,6 +5,10 @@ import { join, sep, basename } from "node:path";
5
5
  import { createHash } from "node:crypto";
6
6
  import { loadWorkflow } from "../../workflow-language/src/validate.js";
7
7
  import { InspectionSchema } from "../../workflow-language/src/inspection.js";
8
+ const finalEvents = new Set(["run.completed", "run.failed"]);
9
+ const statuses = { completed: "succeeded", needs_input: "waiting_for_you", unconfirmed: "unconfirmed", failed: "failed" };
10
+ /** The runtime's status of a finished run, in the dashboard's words; undefined for a status that is not known. */
11
+ export function runStatus(status) { return typeof status === "string" && Object.hasOwn(statuses, status) ? statuses[status] : undefined; }
8
12
  /** Convert the public trace into the dashboard's stable inspection contract. */
9
13
  export function inspectCurrentRun(root, activeSnapshot = false, includeFiles = false) {
10
14
  const path = (name) => {
@@ -40,22 +44,24 @@ export function inspectCurrentRun(root, activeSnapshot = false, includeFiles = f
40
44
  ...(typeof event.operation_id === 'string' ? { operation_id: event.operation_id } : {}),
41
45
  ...(typeof event.confidence === 'number' ? { confidence: event.confidence } : {}),
42
46
  ...(event.kind === 'classify' && event.usage !== undefined ? { usage: event.usage } : {}),
43
- ...(typeof event.duration_ms === 'number' ? { duration_ms: event.duration_ms } : event.event.startsWith('run.') && typeof event.elapsed_ms === 'number' ? { duration_ms: event.elapsed_ms } : {}),
47
+ // Only the final run event gives the run its duration.
48
+ ...(typeof event.duration_ms === 'number' ? { duration_ms: event.duration_ms } : finalEvents.has(event.event) && typeof event.elapsed_ms === 'number' ? { duration_ms: event.elapsed_ms } : {}),
44
49
  at: event.at, type: event.event === "human.required" ? "human_input_required" : event.event.replaceAll(".", "_"),
45
- ...(event.check ? { detail: `${event.check.status}: ${event.check.reason}` } : event.message || event.error ? { detail: String(event.message ?? event.error).slice(0, 4000) } : event.code ? { detail: String(event.code) } : {}),
50
+ // The runtime writes one plain sentence for each event that users see.
51
+ ...(event.message || event.error ? { detail: String(event.message ?? event.error).slice(0, 4000) } : event.code ? { detail: String(event.code) } : {}),
46
52
  ...(Number.isInteger(event.sequence) ? { sequence: event.sequence } : {}),
47
53
  ...(["action", "check"].includes(event.phase) ? { phase: event.phase } : {}),
48
54
  ...(typeof event.tool === "string" ? { tool: event.tool } : {}),
49
55
  ...(typeof event.call_id === "string" ? { call_id: event.call_id } : {}),
50
56
  ...(typeof event.child === "string" ? { child: event.child } : {}),
51
- ...(Number.isSafeInteger(event.completed) && Number.isSafeInteger(event.total) ? { completed: event.completed, total: event.total, ...(typeof event.unit === "string" ? { unit: event.unit } : {}) } : {}),
57
+ ...(Number.isSafeInteger(event.completed) && Number.isSafeInteger(event.total) ? { completed: event.completed } : {}),
58
+ ...(Number.isSafeInteger(event.total) ? { total: event.total, ...(typeof event.unit === "string" ? { unit: event.unit } : {}) } : {}),
59
+ ...(typeof event.key === "string" ? { key: event.key } : {}),
60
+ ...(Number.isSafeInteger(event.attempt) ? { attempt: event.attempt } : {}), ...(Number.isSafeInteger(event.attempts) ? { attempts: event.attempts } : {}),
52
61
  ...(event.entrypoint ? { command: [event.runtime, event.entrypoint, ...(event.args ?? [])].filter(Boolean).join(" ") } : typeof event.command === "string" ? { command: event.command } : {}),
53
62
  ...(event.exit_code !== undefined ? { exit_code: event.exit_code } : {}),
54
63
  ...(typeof event.diagnostics === "string" && event.diagnostics ? { diagnostics: event.diagnostics.length <= 4000 ? event.diagnostics : event.diagnostics.slice(0, 1000) + "\n[middle omitted]\n" + event.diagnostics.slice(-3000) } : {}),
55
64
  };
56
- // Effect verdicts are run events, so the dashboard shows them without a new inspection field.
57
- if (event.event === "effect.observed")
58
- recorded.detail = `${event.effect}: ${event.verdict}${event.final ? "" : " (observation continues)"}. ${String(event.reason ?? "")}`.slice(0, 4000);
59
65
  if (event.event === "run.status_changed")
60
66
  recorded.detail = `Status changed from ${event.from} to ${event.to} after a later observation.`;
61
67
  if (event.event === "effects.waived")
@@ -94,7 +100,6 @@ export function inspectCurrentRun(root, activeSnapshot = false, includeFiles = f
94
100
  row.changes = {};
95
101
  delete row.prompts;
96
102
  delete row.verification;
97
- delete row.failure;
98
103
  delete row.observed_effects;
99
104
  }
100
105
  row.events.push(recorded);
@@ -105,7 +110,6 @@ export function inspectCurrentRun(root, activeSnapshot = false, includeFiles = f
105
110
  row.inputs = event.inputs;
106
111
  row.checks = [];
107
112
  delete row.outputs;
108
- delete row.error;
109
113
  }
110
114
  if (event.event === "step.candidate") {
111
115
  const { state, ...outputs } = event.candidate;
@@ -127,20 +131,25 @@ export function inspectCurrentRun(root, activeSnapshot = false, includeFiles = f
127
131
  if (event.event === "step.skipped")
128
132
  row.status = "skipped";
129
133
  if (event.event === "human.required")
130
- row.status = "needs_attention";
134
+ row.status = "waiting_for_you";
135
+ }
136
+ const status = active ? "running" : runStatus(summary.status);
137
+ if (!status)
138
+ throw Error(`The run record has an unknown status: ${String(summary.status)}.`);
139
+ const failure = active ? undefined : summary.failure;
140
+ if (!active && !["completed", "unconfirmed"].includes(summary.status)) {
141
+ // The failed step's failed tasks are failed. Other unfinished tasks stopped with the run; the failure is not theirs.
142
+ const failed = summary.status === "failed" && failure?.step ? new Set((failure.failed_iterations?.length ? failure.failed_iterations : [failure.iteration ?? 0]).map((i) => `${failure.step}:${i}`)) : new Set();
143
+ for (const [id, row] of Object.entries(invocations))
144
+ if (["running", "returned"].includes(row.status))
145
+ row.status = failed.has(id) ? "failed" : "stopped";
131
146
  }
132
- if (!active && summary.status !== "completed")
133
- for (const row of Object.values(invocations))
134
- if (["running", "returned"].includes(row.status)) {
135
- row.status = "needs_attention";
136
- row.error = summary.error ?? "Run interrupted";
137
- }
138
147
  const files = includeFiles ? attachResultFiles(workflow, invocations, [join(root, "artifacts")], join(root, "artifacts"), includeFiles === "references") : undefined;
139
148
  const lastAccepted = events.filter(e => e.event === "step.accepted").at(-1);
140
149
  return InspectionSchema.parse({ schema: "workflow-inspection/2", workflow, run_id: basename(root), local_run_directory: root,
141
150
  ...(typeof (summary.started_at ?? start?.at) === 'string' ? { started_at: summary.started_at ?? start.at } : {}),
142
151
  ...(typeof (summary.device_name ?? start?.device_name) === 'string' ? { device_name: summary.device_name ?? start.device_name } : {}),
143
- events: runEvents, ...(includeFiles ? { files } : {}), status: active ? "running" : summary.status === "completed" ? "succeeded" : "needs_attention",
144
- ...(summary.error ? { error: summary.error } : {}), inputs: start?.inputs ?? {}, state: lastAccepted?.state ?? start?.initial_state ?? {},
152
+ events: runEvents, ...(includeFiles ? { files } : {}), status,
153
+ ...(failure ? { failure } : {}), inputs: start?.inputs ?? {}, state: lastAccepted?.state ?? start?.initial_state ?? {},
145
154
  resources: Object.fromEntries(Object.entries(start?.config?.environment ?? {}).map(([key, value]) => [key, { description: workflow.environment?.[key]?.description ?? key, path: value }])), invocations });
146
155
  }
@@ -13,6 +13,7 @@ import { writePrivateJson } from './files.js';
13
13
  import { runMethod as executeMethod } from "@withmethod/runtime/runner.js";
14
14
  import { localSetup } from "./local-setup.js";
15
15
  import { authoringPath } from "./authoring.js";
16
+ import { failureLines } from "./failure.js";
16
17
  export { executeMethod as runCurrentMethod };
17
18
  /** Recent run folders on this computer, newest first. The runtime reuses only iterations whose key matches. */
18
19
  export function recentRuns(exclude, limit = 20) {
@@ -148,8 +149,11 @@ export async function runCurrentFile(file, flags, syncFactory, onEvent, client =
148
149
  let display = synced ? { ...result, dashboard: sync.dashboard, dashboard_sync: synced } : result;
149
150
  // A run that waits for a person shows the question itself, so the agent can ask the user and pass on the answer.
150
151
  const question = result.status === 'needs_input' ? (await import('./worker.js')).lastQuestion(authoringPath(flags['run-dir'])) : null;
151
- if (question)
152
- display = { ...display, question, fix: `Ask the user this question in plain words and wait for the answer; do not answer it yourself. Then write {"steps": {"${question.step}": {"outputs": {...}}}} with the user's answer (the fields are in question.form) to a file, and run the same command again with --resume --run-dir ${JSON.stringify(flags['run-dir'])} --human FILE.` };
152
+ if (question && display.failure)
153
+ display = { ...display, question, failure: { ...display.failure, next: { ...display.failure.next, text: `Ask the user this question in plain words and wait for the answer; do not answer it yourself. Then write {"steps": {"${question.step}": {"outputs": {...}}}} with the user's answer (the fields are in question.form) to a file, and run the same command again with --resume --run-dir ${JSON.stringify(flags['run-dir'])} --human FILE.` } } };
154
+ // People read three plain lines on stderr; agents, apps, and tests parse the JSON on stdout.
155
+ if (result.status === "failed" && display.failure)
156
+ process.stderr.write(failureLines(display.failure) + (display.dashboard ? `Dashboard: ${display.dashboard}\n` : ""));
153
157
  process.stdout.write(JSON.stringify(display, null, 2) + "\n");
154
158
  // 3: every step finished, but an observer could not confirm an external change.
155
159
  if (result.status !== "completed")
@@ -0,0 +1,11 @@
1
+ import { type RunFailure } from "../../workflow-language/src/inspection.js";
2
+ /**
3
+ * The failure record of a run that stopped before its first step. A runtime error carries its own record; an SDK error
4
+ * gets one from the runtime's templates: an SDK setup error has a code or is a plain Error, and any other error type
5
+ * is a bug in Method.
6
+ */
7
+ export declare function setupFailure(error: any): RunFailure;
8
+ /** The record of a run whose own record Method could not read. */
9
+ export declare function unreadableRecord(error: unknown): RunFailure;
10
+ /** The three plain lines that a failed run prints on stderr. */
11
+ export declare function failureLines(failure: RunFailure): string;
@@ -0,0 +1,25 @@
1
+ import { failureRecord } from "@withmethod/runtime/failure.js";
2
+ import { FailureOwnerSchema, FailureSchema, ownerText } from "../../workflow-language/src/inspection.js";
3
+ /**
4
+ * The failure record of a run that stopped before its first step. A runtime error carries its own record; an SDK error
5
+ * gets one from the runtime's templates: an SDK setup error has a code or is a plain Error, and any other error type
6
+ * is a bug in Method.
7
+ */
8
+ export function setupFailure(error) {
9
+ if (FailureSchema.safeParse(error?.failure).success)
10
+ return error.failure;
11
+ const owner = FailureOwnerSchema.safeParse(error?.owner).success ? error.owner
12
+ : error?.code === "needs_input" ? "needs_you"
13
+ : error?.code || error?.constructor === Error ? "setup" : "method_platform";
14
+ const message = error instanceof Error ? error.message : String(error);
15
+ return FailureSchema.parse(failureRecord({ message, owner, ...(error?.code ? { code: error.code } : {}), ...(error?.missing ? { missing: error.missing } : {}) }, { phase: "setup" }));
16
+ }
17
+ /** The record of a run whose own record Method could not read. */
18
+ export function unreadableRecord(error) {
19
+ const message = error instanceof Error ? error.message : String(error);
20
+ return { ...FailureSchema.parse(failureRecord({ message, owner: "method_platform", code: "unreadable_record" })), headline: "Method could not read this run's record." };
21
+ }
22
+ /** The three plain lines that a failed run prints on stderr. */
23
+ export function failureLines(failure) {
24
+ return `Failed: ${failure.headline}\nWhose problem: ${ownerText(failure.owner)}\nNext: ${failure.next.text}\n`;
25
+ }
@@ -9,7 +9,7 @@ export function inspectRun(directory, options = {}) {
9
9
  if (!existsSync(join(root, "method.json")) && existsSync(join(root, "setup-inspection.json")) && existsSync(join(root, "request.json"))) {
10
10
  const initial = JSON.parse(readFileSync(join(root, "setup-inspection.json"), "utf8"));
11
11
  const failure = existsSync(join(root, "setup-error.json")) ? JSON.parse(readFileSync(join(root, "setup-error.json"), "utf8")) : null;
12
- return InspectionSchema.parse({ ...initial, local_run_directory: root, ...(failure ? { status: 'needs_attention', error: failure.error } : {}), events: existsSync(join(root, "setup.json")) ? JSON.parse(readFileSync(join(root, "setup.json"), "utf8")) : [] });
12
+ return InspectionSchema.parse({ ...initial, local_run_directory: root, ...(failure ? { status: failure.code === 'needs_input' ? 'waiting_for_you' : 'failed', failure } : {}), events: existsSync(join(root, "setup.json")) ? JSON.parse(readFileSync(join(root, "setup.json"), "utf8")) : [] });
13
13
  }
14
14
  if (existsSync(join(root, "method.json")))
15
15
  return inspectCurrentRun(root, options.activeSnapshot, options.includeFiles);
@@ -13,7 +13,7 @@ export const commandHelp = {
13
13
  "new-id": { usage: "method new-id FILE", purpose: "Make a copied Method file a new Method.", arguments: "Writes a new id: line. A copy that keeps its id: saves to the same Method.", result: "JSON with file and method_id.", errors: "Missing file.", example: "method new-id copy.method" },
14
14
  bind: { usage: "method bind FILE_OR_ID NAME (--file FOLDER | --connection URL) [--upload]", purpose: "Save a named input location or connection.", arguments: "Default: this computer only, in ~/.config/method/computer.json under the Method's ID (a FILE gets its id: line). A folder named NAME beside the Method needs no binding. --upload saves only the selected input folder or connection URL in the account. It never scans your disk. Keep credentials in the service's normal sign-in store.", result: "Saved binding and scope.", errors: "Missing input, invalid name, or unsafe path.", example: "method bind METHOD_ID prepared_day --file day-records --upload", remote: true },
15
15
  state: { usage: "method state ID [--enable --file state.json | --release RUN_ID]", purpose: "Read or explicitly enable shared account state.", arguments: "Accepted state updates use revision checks. Inspect a stopped run's external actions before releasing its ownership.", result: "Current revision, value, and owner run.", errors: "Concurrent ownership or stale revision. Existing state is never silently overwritten.", example: "method state METHOD_ID --enable --file initial-state.json", remote: true },
16
- wait: { usage: "method wait RUN_DIRECTORY", purpose: "Reconnect to the same local worker.", arguments: "Use the directory returned by run. Closing this output connection leaves the worker active.", result: "Live output, then the final worker status with the dashboard link of a synced run.", errors: "Stopped process or missing run.", example: "method wait .runs/example" },
16
+ wait: { usage: "method wait RUN_DIRECTORY", purpose: "Reconnect to the same local worker.", arguments: "Use the directory returned by run. Closing this output connection leaves the worker active.", result: "Live output, then a final line with the run status (succeeded, unconfirmed, waiting_for_you, or failed), the headline of a failure, and the dashboard link of a synced run.", errors: "Stopped process or missing run.", example: "method wait .runs/example" },
17
17
  'run-status': { usage: "method run-status RUN_DIRECTORY", purpose: "Read worker status without waiting.", arguments: "Local run directory.", result: "Worker status and whether its process is active.", errors: "Unreadable run directory.", example: "method run-status .runs/example" },
18
18
  cancel: { usage: "method cancel RUN_DIRECTORY", purpose: "Stop a local run process.", arguments: "Inspect uncertain external actions before any explicit retry.", result: "Cancellation request. The checkpoint remains available.", errors: "Unreadable run directory.", example: "method cancel .runs/example" },
19
19
  progress: { usage: "method progress --message TEXT [--completed N --total N --unit NAME] [--child NAME]\nmethod progress --codex --child NAME", purpose: "Report public progress from a running script or relay a child Codex JSON stream.", arguments: "METHOD_PROGRESS_FD is supplied by the executor. --codex reads JSON lines from stdin; --message sends one message. Do not include secrets or source contents.", result: "Writes to the separate progress pipe. No stdout output. No-op outside a Method process.", errors: "Invalid arguments. Malformed Codex events are ignored.", example: "method progress --message 'Rendered 12 of 40 pages' --completed 12 --total 40 --unit pages" },
@@ -66,7 +66,7 @@ export const commandHelp = {
66
66
  --fresh: run every step; reuse nothing.
67
67
  --human FILE: answers for ask steps, as {steps: {"STEP": {outputs: {...}}}} (STEP:ITERATION, counted from 0, for a step with each or repeat).
68
68
  --verbose: print runtime events.
69
- Use method doctor to check the installed Node and configured tools.`, result: "When signed in, the start line and the final result have dashboard: the link to the run's page; give it to the user when the run starts. Progress and final status text; local result.json and run evidence. The runtime executes the method's declared scripts, calls, agents, and checks. Executes trusted local processes; changes declarations do not enforce permissions. Runs exit 0 on completion, 1 on failure, 2 when human input is needed, and 3 when an observer could not confirm an external change (unconfirmed).", errors: "Missing inputs/access, failed check, timeout, unsafe resume/version mismatch, upload failure. See recovery. Never retry a business write without inspecting its saved changes.", example: "method run wf_example --version v_example --inputs inputs.json", remote: true },
69
+ Use method doctor to check the installed Node and configured tools.`, result: "When signed in, the start line and the final result have dashboard: the link to the run's page; give it to the user when the run starts. Progress, then the final result as JSON on stdout; a run that stopped has failure (owner, headline, step, next), and a failed run also prints Failed, Whose problem, and Next lines on stderr. Local result.json and run evidence. The runtime executes the method's declared scripts, calls, agents, and checks. Executes trusted local processes; changes declarations do not enforce permissions. Runs exit 0 on completion, 1 on failure, 2 when human input is needed, and 3 when an observer could not confirm an external change (unconfirmed).", errors: "Missing inputs/access, failed check, timeout, unsafe resume/version mismatch, upload failure. See recovery. Never retry a business write without inspecting its saved changes.", example: "method run wf_example --version v_example --inputs inputs.json", remote: true },
70
70
  observe: { usage: "method observe [RUN_DIRECTORY...] [--pending ROOT]...", purpose: "Make the effect observations that are due for finished runs.", arguments: "Without directories, checks runs with open effects under .method-runs and the Method cache. Runs only observers and judges; never repeats an action. Schedule it, for example hourly, so late evidence such as a bounce reaches the run.", result: "JSON with each run's new verdicts, status, and next observation time. A synced run whose status changed is uploaded again.", errors: "Locked run, changed bundle, or a missing observer credential.", example: "method observe --pending .method-runs" },
71
71
  test: { usage: "method test FILE [--case ID]... [--baseline OLD_FILE] [--new ID]... [--cases DIR] [--agent codex|claude]", purpose: "Replay recorded cases against this version of a Method. Every case must pass.", arguments: "Cases are in cases/ beside the Method. Unchanged steps return their recorded outputs; changed steps run; files connections are scratch folders, so a changed step that writes files runs safely. A changed step that asks a person or acts on a service makes the case unverifiable. --baseline also runs each case on the old version, to show what the change fixed or broke.", result: "JSON report with a verdict per case and passed:true when no case blocks the change. Exit 0 or 1.", errors: "Unknown case, invalid Method, or setup errors.", example: "method test task.method --baseline task-before.method --new bounce-reported" },
72
72
  case: { usage: "method case new|retire|list FILE ...", purpose: "Keep a correction as a recorded case that every later version must pass.", arguments: "Use method help case new, method help case retire, or method help case list.", result: "See the subcommand.", errors: "Unknown action. Choose new, retire, or list.", example: "method help case new" },
@@ -337,7 +337,7 @@ A save never blocks a run: a run that cannot save says "Will save when online",
337
337
  State commits after checks. A local checkpoint cannot roll back an external write. Inspect external state before an explicit retry.
338
338
  Two computers can save different content of one Method; each becomes a version with its parent. Going back to earlier content finds its existing version. The id: line keeps a renamed or moved file on its Method; method new-id FILE makes a copy a new Method. On one computer a Method is saved from one file (every checkout of one Git repository counts as one): a copy in another folder or a temporary folder does not run while that file exists, and the error names the file to change. When that file is unchanged since its last save and the account has a newer version, a run takes that version first.
339
339
  Use method sync RUN_DIRECTORY to repair a dashboard upload without executing the method again.
340
- After a failed action with effects, the failure lists what the observers saw. A confirmed effect means the change happened: do not retry the action. A run that is unconfirmed finished all its steps; inspect effects.jsonl and the external system before relying on its result.
340
+ After a failed action with effects, failure.next.text lists what the observers saw. A confirmed effect means the change happened: do not retry the action. A run that is unconfirmed finished all its steps; inspect effects.jsonl and the external system before relying on its result.
341
341
  `;
342
342
  export const guideTopics = ["start", "concepts", "execution", "examples", "example", "recipes", "recovery", "commands"];
343
343
  export function authoringGuide(topic = "start", exampleId) {
@@ -5,6 +5,7 @@ import { join } from "node:path";
5
5
  import { randomUUID } from "node:crypto";
6
6
  import { z } from "zod";
7
7
  import { inspectRun } from "./inspect.js";
8
+ import { setupFailure, unreadableRecord } from "./failure.js";
8
9
  import { writePrivateJson } from "./files.js";
9
10
  import { MethodClient } from "./method-client.js";
10
11
  import { InspectionSchema, deviceOnly, } from "../../workflow-language/src/inspection.js";
@@ -216,10 +217,8 @@ export class MethodSync {
216
217
  inspection = {
217
218
  ...this.initial,
218
219
  events: existsSync(join(this.directory, "setup.json")) ? JSON.parse(readFileSync(join(this.directory, "setup.json"), "utf8")) : this.initial.events,
219
- status: "needs_attention",
220
- error: failure instanceof Error
221
- ? failure.message
222
- : String(failure ?? error),
220
+ // A run that failed before its first step has the failure record of its setup error; otherwise Method could not read the record.
221
+ ...((record => ({ status: record.code === "needs_input" ? "waiting_for_you" : "failed", failure: record }))(failure ? setupFailure(failure) : unreadableRecord(error))),
223
222
  };
224
223
  // A run that failed before its first step keeps this record for a later sync.
225
224
  writePrivateJson(join(this.directory, "setup-inspection.json"), inspection);
@@ -1,5 +1,3 @@
1
1
  #!/usr/bin/env node
2
2
  import { MethodClient } from "./method-client.js";
3
- /** The next step for a failure before the first step. */
4
- export declare function setupFix(error: any): any;
5
3
  export declare function methodMain(args?: string[], clientFactory?: (server: string) => MethodClient): Promise<boolean | void>;
@@ -20,15 +20,7 @@ import { parse, localMain } from "./local-cli.js";
20
20
  import { writePrivateJson } from "./files.js";
21
21
  import { loadWorkflow } from "../../workflow-language/src/validate.js";
22
22
  import { methodHelp, overviewHelp as help } from "./method-help.js";
23
- import { fixFor } from "@withmethod/runtime/runner.js";
24
23
  /** The next step for a failure before the first step. */
25
- export function setupFix(error) {
26
- if (error.code === 'missing_secret') {
27
- const names = (error.missing ?? []).join(' ');
28
- return `No step ran. Run method secret find to see which nearby files hold ${names}, then method secret import FILE ${names}; or ask the user to enter each one with method secret set NAME (a private form in their browser). Never open or print a key file, and never ask for a value in chat.`;
29
- }
30
- return fixFor(error);
31
- }
32
24
  function safePath(path) {
33
25
  return authoringPath(path);
34
26
  }
@@ -99,9 +91,13 @@ export async function methodMain(args = process.argv.slice(2), clientFactory = s
99
91
  }
100
92
  catch (error) {
101
93
  // A run that fails before its first step still prints one result that says why and what to do.
102
- const failed = { status: error.code === 'needs_input' ? 'needs_input' : 'failed', run_dir: directory, code: error.code ?? 'setup_failed', error: error.message, ...(error.missing ? { missing: error.missing } : {}), fix: setupFix(error) };
94
+ const { setupFailure, failureLines } = await import('./failure.js');
95
+ const failure = setupFailure(error);
96
+ const failed = { status: error.code === 'needs_input' ? 'needs_input' : 'failed', run_dir: directory, failure, ...(error.missing ? { missing: error.missing } : {}) };
103
97
  if (!existsSync(join(directory, 'summary.json')))
104
98
  writePrivateJson(join(directory, 'summary.json'), failed);
99
+ if (failed.status === 'failed')
100
+ process.stderr.write(failureLines(failure));
105
101
  process.stdout.write(JSON.stringify(failed, null, 2) + '\n');
106
102
  process.exitCode = error.code === 'needs_input' ? 2 : 1;
107
103
  }
@@ -155,7 +155,7 @@ export class Method {
155
155
  throw new MethodApiError(409, "not_waiting", `The run is ${run.status}; it has no question to answer.`);
156
156
  const asked = this.openQuestion(run);
157
157
  try {
158
- assertSchema(asked.form, answer, "Answer");
158
+ assertSchema(asked.form, answer, "Answer", { producer: "the answer", owner: "needs_you" });
159
159
  }
160
160
  catch (error) {
161
161
  throw new MethodApiError(422, "invalid_answer", error.message);
@@ -1,5 +1,5 @@
1
1
  import { MethodClient } from './method-client.js';
2
2
  import type { parse } from './local-cli.js';
3
3
  /** A run whose steps all finished: completed, or unconfirmed (an external change could not be confirmed). */
4
- export declare const finishedRun: (status: string) => status is "completed" | "unconfirmed";
4
+ export declare const finishedRun: (status: string) => status is "unconfirmed" | "completed";
5
5
  export declare function runSaved(saved: any, flags: ReturnType<typeof parse>['values'], client: MethodClient): Promise<any>;
@@ -12,8 +12,9 @@ import { writePrivateJson } from './files.js';
12
12
  import { workerRunId } from './run-worker.js';
13
13
  import { readDocument } from './authoring.js';
14
14
  import { withInputsFrom } from './inputs-from.js';
15
+ import { setupFailure } from './failure.js';
15
16
  // Older versions are listed only after their saved-package tests pass.
16
- const supportedPackageRuntimes = new Set([runtimeVersion, '0.11.1', '0.11.0', '0.10.0', '0.9.5', '0.9.4', '0.9.3', '0.7.0', '0.7.1', '0.7.2', '0.8.0', '0.8.1', '0.8.2', '0.8.3']);
17
+ const supportedPackageRuntimes = new Set([runtimeVersion, '0.12.0', '0.11.1', '0.11.0', '0.10.0', '0.9.5', '0.9.4', '0.9.3', '0.7.0', '0.7.1', '0.7.2', '0.8.0', '0.8.1', '0.8.2', '0.8.3']);
17
18
  /** A run whose steps all finished: completed, or unconfirmed (an external change could not be confirmed). */
18
19
  export const finishedRun = (status) => status === 'completed' || status === 'unconfirmed';
19
20
  export async function runSaved(saved, flags, client) {
@@ -124,7 +125,8 @@ export async function runSaved(saved, flags, client) {
124
125
  return { run_id: request.run_id, directory, ...result };
125
126
  }
126
127
  catch (error) {
127
- writePrivateJson(join(directory, 'setup-error.json'), { code: error.code ?? 'setup_failed', error: error.message, ...(error.missing ? { missing: error.missing } : {}) });
128
+ // The run stopped before its first step: setup-error.json holds its failure record.
129
+ writePrivateJson(join(directory, 'setup-error.json'), setupFailure(error));
128
130
  setupEvents.push({ at: new Date().toISOString(), type: 'setup_failed', detail: error.message });
129
131
  writePrivateJson(setupFile, setupEvents);
130
132
  await sync.finish(error);
@@ -5,6 +5,8 @@ import { resolve, join } from 'node:path';
5
5
  import { fileURLToPath } from 'node:url';
6
6
  import { writePrivateJson } from './files.js';
7
7
  import { runDashboard } from './method-sync.js';
8
+ import { runStatus } from './current-inspection.js';
9
+ import { unreadableRecord } from './failure.js';
8
10
  /** server: the signed-in Method server, so that the start line links to the run's dashboard page. */
9
11
  export async function startRunWorker(args, directory, background = false, server) {
10
12
  directory = resolve(directory);
@@ -117,8 +119,15 @@ export async function waitForRun(directory) {
117
119
  const status = JSON.parse(readFileSync(file, 'utf8'));
118
120
  const dashboard = runDashboard(directory), link = dashboard ? { dashboard } : {};
119
121
  if (status.status !== 'running') {
122
+ // The final line gives the run's own status, and the headline of a failure.
120
123
  process.exitCode = status.exit_code ?? 1;
121
- return { ...status, ...link };
124
+ const summary = existsSync(join(directory, 'summary.json')) ? JSON.parse(readFileSync(join(directory, 'summary.json'), 'utf8')) : undefined;
125
+ const result = runStatus(summary?.status);
126
+ // A worker command that is not a run (no run record) and exits 0 only finished.
127
+ if (!result && process.exitCode === 0)
128
+ return { ...status, ...link };
129
+ const failure = result ? summary.failure : unreadableRecord(Error('The run has no readable summary.json.'));
130
+ return { status: result ?? 'failed', run_dir: directory, exit_code: process.exitCode, ...(failure && result !== 'succeeded' ? { headline: failure.headline } : {}), ...link };
122
131
  }
123
132
  if (!workerAlive(directory)) {
124
133
  process.exitCode = 1;
@@ -359,13 +359,14 @@ export function cliExecutor(options = {}) {
359
359
  catch { /* Default. */ }
360
360
  return { status: "waiting", question, ...(typeof timeoutMs === "number" ? { timeoutMs } : {}) };
361
361
  }
362
- return { status: "failed", error: { code: "needs_input", message: String(summary.error ?? "The run needs input that a production worker cannot give.") } };
362
+ return { status: "failed", error: { code: "needs_input", message: String(summary.failure?.headline ?? "The run needs input that a production worker cannot give.") } };
363
363
  }
364
+ // Production keeps its own run statuses; the failure record gives the code and the headline.
364
365
  if (summary?.status === "failed")
365
- return { status: "failed", error: { code: summary.code ?? "execution_failed", message: String(summary.error ?? "The run failed.") } };
366
+ return { status: "failed", error: { code: summary.failure?.code ?? "execution_failed", message: String(summary.failure?.headline ?? "The run failed.") } };
366
367
  const setup = read("setup-error.json");
367
368
  if (setup)
368
- return { status: "failed", error: { code: setup.code, message: String(setup.error) }, retryable: !finalCodes.has(setup.code) };
369
+ return { status: "failed", error: { code: setup.code, message: String(setup.headline) }, retryable: !finalCodes.has(setup.code) };
369
370
  return { status: "failed", error: { code: "worker_failed", message: `The run process stopped (exit ${code}). See ${join(dir, "worker.stderr.log")}.` }, retryable: true };
370
371
  };
371
372
  }