@method-ai-hq/sdk 0.15.1 → 0.17.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 (36) hide show
  1. package/README.md +1 -1
  2. package/dist/packages/method-document/src/conversion-report.d.ts +2 -2
  3. package/dist/packages/sdk/examples/outbound-management/README.md +1 -1
  4. package/dist/packages/sdk/examples/outbound-management/outbound.method +3 -3
  5. package/dist/packages/sdk/examples/social-briefing/README.md +1 -1
  6. package/dist/packages/sdk/examples/social-briefing/social-briefing.method +1 -1
  7. package/dist/packages/sdk/package.json +2 -2
  8. package/dist/packages/sdk/src/authoring-instructions.d.ts +2 -2
  9. package/dist/packages/sdk/src/authoring-instructions.js +2 -2
  10. package/dist/packages/sdk/src/current-inspection.d.ts +2 -0
  11. package/dist/packages/sdk/src/current-inspection.js +26 -17
  12. package/dist/packages/sdk/src/current-runtime.js +6 -2
  13. package/dist/packages/sdk/src/failure.d.ts +11 -0
  14. package/dist/packages/sdk/src/failure.js +25 -0
  15. package/dist/packages/sdk/src/improve.d.ts +7 -4
  16. package/dist/packages/sdk/src/improve.js +11 -17
  17. package/dist/packages/sdk/src/inspect.js +1 -1
  18. package/dist/packages/sdk/src/judge.d.ts +20 -0
  19. package/dist/packages/sdk/src/judge.js +59 -0
  20. package/dist/packages/sdk/src/method-help.d.ts +1 -1
  21. package/dist/packages/sdk/src/method-help.js +9 -8
  22. package/dist/packages/sdk/src/method-sync.js +3 -4
  23. package/dist/packages/sdk/src/method.d.ts +0 -2
  24. package/dist/packages/sdk/src/method.js +8 -10
  25. package/dist/packages/sdk/src/model-checks.d.ts +18 -1
  26. package/dist/packages/sdk/src/model-checks.js +67 -7
  27. package/dist/packages/sdk/src/production.js +1 -1
  28. package/dist/packages/sdk/src/proposals.d.ts +4 -4
  29. package/dist/packages/sdk/src/proposals.js +29 -50
  30. package/dist/packages/sdk/src/run-saved.d.ts +1 -1
  31. package/dist/packages/sdk/src/run-saved.js +4 -2
  32. package/dist/packages/sdk/src/run-worker.js +10 -1
  33. package/dist/packages/sdk/src/worker.js +4 -3
  34. package/dist/packages/workflow-language/src/inspection.d.ts +272 -138
  35. package/dist/packages/workflow-language/src/inspection.js +30 -10
  36. package/package.json +2 -2
@@ -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" },
@@ -36,9 +36,10 @@ export const commandHelp = {
36
36
  get: { usage: "method get WORKFLOW_ID [--version VERSION_ID] [--out FILE] [--server URL]", purpose: "Read an online method; optionally make a local draft for editing.", arguments: "Default: latest version. --out must name a new file. Use a version ID to read an exact saved version.", result: "Without --out: {workflow_id,version_id,version_number,workflow}. With --out: {workflow_id,version_id,version_number,file}; writes the workflow with its id: line and the saved files.", errors: "Missing method or version; output file already exists.", example: "method get wf_example --out edit.method", remote: true },
37
37
  explain: { usage: "method explain FILE [--step ID] [--no-round-trip] [--server URL]", purpose: "Explain each script step of the saved version in a script card.", arguments: "FILE is a local Method whose current content is a saved version (run or publish it while signed in). For each run step whose script has no card, code analysis finds the hosts, secrets, environment variables, files, and commands the script uses; a hosted model writes a one-sentence summary and 3 to 8 steps; every number, quoted name, and host in that text must appear in the code (one retry). The round trip writes a program from the steps alone and replays up to 3 recorded inputs of the step from local runs through both programs without network access. A script that calls the network is not replayed. --step explains one step. --no-round-trip skips the replay. A signed-in run starts this in the background; publish waits for it.", result: "One line per step with its summary and check results. Cards upload to the version; the dashboard shows them beside each script. A failed check is shown on the card and never blocks publish.", errors: "No saved version, local files that differ from the saved version, or no hosted-model credit. Without sign-in it prints one line and makes no card.", example: "method explain leads.method --step score", remote: true },
38
38
  publish: { usage: "method publish FILE [--reason TEXT] [--accept-failing-case ID[,ID]] [--cloud|--workers] [--env NAME] [--server URL]", purpose: "Mark a version as published, for sharing, schedules, and production runs.", arguments: "Runs the Method's cases first; a version that breaks an approved case is refused unless --accept-failing-case names it, and the version records that. Uses the version of the file's current content: it finishes that upload (no time limit while bytes move), checks the cases, then marks it published with the reason (default: Published.). --accept-failing-case needs --reason. The first save of a new file creates the Method. --cloud runs the production runs of the environment on Method Cloud; --workers runs them on your workers again. --env NAME selects the environment (default production).", result: "JSON with workflow_id, version_id, url, published_at, the cases line, and the open warnings and notes (issues). With --cloud or --workers: placement. Warnings and notes never block a publish.", errors: "An error issue (nothing is published), failing case, --accept-failing-case without --reason, or no network.", example: "method publish task.method --reason 'Shorter introduction'", remote: true },
39
- improve: { usage: "method improve FILE [--case ID] [--note TEXT] [--step ID] [--server URL]", purpose: "Ask Method to propose a better version of a Method.", arguments: "--note: the correction, in your words; it is about the newest run of the Method on this computer, which the improvement reads and a suggested case records. --step: improve only this step. --case: the case the change must pass. A Method with account run data improves in your account; see the progress on the dashboard. A Method with run_data: device improves on this computer, so its run content stays here; the result is a local proposal in .method/proposals/.", result: "JSON with the improvement and the dashboard link, or the local proposal ID. Nothing is published, and the file does not change.", errors: "No id: line (run the file once while signed in), an unknown step, or an improvement that is already running.", example: "method improve task.method --step summary --note 'Name the customer in the first sentence.'", remote: true },
40
- proposals: { usage: "method proposals FILE [--wait] [--server URL]", purpose: "List the proposals of a Method.", arguments: "Lists the proposals in your account and the local proposals beside the file. --wait: when an improvement is running, wait until it ends (usually a few minutes), printing each step it reaches.", result: "JSON list of proposals with id, source, status, kind, cause, and steps, and the newest improvement that has no proposal yet, with its status. An improvement that found no safe change has outcome none, and next says what to do instead. No changes.", errors: "", example: "method proposals task.method", remote: true },
41
- apply: { usage: "method apply FILE [PROPOSAL_ID] [--resolved] [--server URL]", purpose: "Merge a proposal into the local file.", arguments: "Default: the newest accepted proposal, or else the newest local proposal. The merge is per step: a part that only the proposal changed takes the change; a part that you and the proposal both changed is a conflict. --resolved: you merged a conflict by hand; this marks the proposal applied. Commands that take a signed-in FILE apply an accepted proposal first and print one line.", result: "JSON with status applied and the changed steps; the file changes and the proposal is marked applied. Nothing is published. A conflict lists each part with the base, your, and the proposed value, changes nothing, and exits 1.", errors: "No proposal, a proposal that is not accepted, or a conflict.", example: "method apply task.method", remote: true },
39
+ judge: { usage: "method judge RUN_ID right|wrong [--step STEP_ID] [--note TEXT] [--corrected FILE] [--json] [--server URL]", purpose: "Record the person's reaction to a run's result.", arguments: "RUN_ID is the run_id of the run's start line, or the run ID on the dashboard. right: the person used or approved the result. wrong: the person corrected it, or asked to change the Method because of it. --note: the person's words (at most 4000 characters). --corrected FILE: the result that the person says is right; JSON is sent as JSON, other text as text (at most 1 MB). --step: the step whose output was wrong. A wrong judgment makes Method fix the Method in the background: it applies a tested change that the person can undo, or changes nothing. Do not ask the person to confirm a judgment.", result: "One line, for example Recorded: wrong on step choose. Method is fixing it in the background. --json prints the judgment and fixing.", errors: "An unknown run, a run of a Method that is not in your account, or no sign-in.", example: "method judge 6f1c… wrong --step summary --note 'Name the customer in the first sentence.'", remote: true },
40
+ changes: { usage: "method changes FILE [--json] [--server URL]", purpose: "List the changes that Method applied to a Method.", arguments: "Lists the changes that Method made from wrong judgments, newest first, and the local proposals applied on this computer. Commands that take a signed-in FILE merge a new change into the file first and print one line.", result: "One line per change: its date, its version, and its cause. --json prints {file, method_id, changes: [{id, source, cause, date, version_id, steps}]}. No changes.", errors: "No id: line and no local change.", example: "method changes task.method", remote: true },
41
+ improve: { usage: "method improve FILE [--case ID] [--note TEXT] [--step ID] [--server URL]", purpose: "Improve a Method that keeps its run content on this computer.", arguments: "For a Method with run_data: device, or in an organization that keeps run content on devices: runs Method's improvement on this computer, so the run content stays here. --note: the correction, in the person's words; it is about the newest run of the Method on this computer, which the improvement reads and a suggested case records. --step: improve only this step. --case: the case the change must pass. For a Method that keeps run content in the account, it starts nothing and says to use method judge: Method fixes those Methods from the person's judgments.", result: "JSON with the local proposal ID and the next step; the proposal is in .method/proposals/. Nothing is published, and the file does not change. For an account Method, one line.", errors: "An unknown step, or a step of the improvement that fails.", example: "method improve task.method --step summary --note 'Name the customer in the first sentence.'", remote: true },
42
+ apply: { usage: "method apply FILE [PROPOSAL_ID] [--resolved] [--server URL]", purpose: "Merge a local proposal from method improve into the file.", arguments: "Default: the newest open local proposal. The merge is per step: a part that only the proposal changed takes the change; a part that you and the proposal both changed is a conflict. --resolved: you merged a conflict by hand; this marks the proposal applied. When a change that Method applied conflicts with your edits, the line before the command names it; give its ID to see both versions.", result: "JSON with status applied and the changed steps; the file changes and the proposal is marked applied. Nothing is published. A conflict lists each part with the base, your, and the proposed value, changes nothing, and exits 1.", errors: "No open proposal, or a conflict.", example: "method apply task.method", remote: true },
42
43
  secret: { usage: "method secret find [FILE]\nmethod secret import FILE NAME...\nmethod secret set NAME\nmethod secret list [NAME...]", purpose: "Give this computer the values of a Method's declared secrets.", arguments: "find lists the KEY=VALUE files in this folder (3 levels down) and in the parent folder (2 levels down) with the key names in each, never values; for the Method FILE (or the only Method here) it says which declared secrets each file holds and prints the import command. import copies the named values from a KEY=VALUE file that the user names, without printing them. set opens a private form on 127.0.0.1 in the browser for one value. list shows names and where each value is found (shell, this computer, or missing), never values. Values are kept in ~/.config/method/secrets.json (mode 0600) and are never sent to Method. A value exported in the shell is used first. Every script of the Method receives all of its declared secrets.", result: "JSON with the saved names, or the list.", errors: "A name is missing from the file, or no value was entered.", example: "method secret find\nmethod secret import ../service/.env ARCHIVE_TOKEN" },
43
44
  connect: { usage: "method connect [APP_FOLDER] [--file METHOD_FILE]", purpose: "Connect an app to the Method in this folder, so the app runs it in production.", arguments: "Run it in the Method's folder. APP_FOLDER (default: this folder) is the app's folder. It makes a service key named after the app folder and writes METHOD_API_KEY to the app's .env (a new file, or a new or replaced line); it never prints the key. It adds .env to .gitignore in a git repository. When the Method has no published version, it publishes the current file. It detects Node (package.json) or Python (pyproject.toml, requirements.txt), and prints the install line and the code to add. Run it again: an app with a working key keeps it.", result: "JSON with the method_id, the published version, where the key is, install, code, secrets_to_set_on_the_host, and next.", errors: "No or several .method files (give --file), an app folder with no language marker, or a failing case at publish.", example: "method connect ../app", remote: true },
44
45
  answer: { usage: "method answer RUN_ID [--answer JSON]", purpose: "Show or answer the question of a production run that waits on an ask step.", arguments: "Without --answer it prints the question and its form (the fields of the answer). With --answer it checks the answer against the form and sends it; the run continues. A run that keeps its content on devices has a sealed question: run this where the app's METHOD_API_KEY is (the environment, or the .env of this folder), or on the laptop that started the run with its sign-in. Local runs (method run FILE) take answers with --human FILE as before.", result: "The question and form, or the run after the answer.", errors: "The run is not waiting, the answer does not match the form, or the key cannot open a sealed question (wrong_key).", example: "method answer 6f1c… --answer '{\"approved\":true}'", remote: true },
@@ -66,7 +67,7 @@ export const commandHelp = {
66
67
  --fresh: run every step; reuse nothing.
67
68
  --human FILE: answers for ask steps, as {steps: {"STEP": {outputs: {...}}}} (STEP:ITERATION, counted from 0, for a step with each or repeat).
68
69
  --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 },
70
+ 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
71
  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
72
  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
73
  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" },
@@ -299,7 +300,7 @@ const recipes = `# Recipes
299
300
  ## Corrections become cases
300
301
 
301
302
  When the user says a result was wrong:
302
- 1. Read the run (result.json, events.jsonl) and fix the Method. Run it again and show the user the new result.
303
+ 1. Record it: method judge RUN_ID wrong --note "the user's words" (--corrected FILE when they gave the right result). Do not ask them to confirm it. Then read the run (result.json, events.jsonl) and fix the Method. Run it again and show the user the new result.
303
304
  2. If the user wants the fix to stay, ask once: "Keep this as a rule: <the rule in plain words>?"
304
305
  3. On yes, record it: method case new FILE --id ID --run BAD_RUN --passing-run NEW_RUN --note "the user's words" --rubric "plain sentence that must be true" (repeat --rubric for each rule). --ref names the output to judge (default: the Method's result; a path to a file that the run saved is judged by that file's contents). When a rule compares the output with the sources, add --context with the step output that holds them, for example --context outputs.material. Make each sentence fail on the bad run; case new warns about one that does not. For an exact value, use --expect instead.
305
306
  4. Run method test FILE. Every case must pass. method publish refuses a version that breaks a case.
@@ -337,7 +338,7 @@ A save never blocks a run: a run that cannot save says "Will save when online",
337
338
  State commits after checks. A local checkpoint cannot roll back an external write. Inspect external state before an explicit retry.
338
339
  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
340
  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.
341
+ 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
342
  `;
342
343
  export const guideTopics = ["start", "concepts", "execution", "examples", "example", "recipes", "recovery", "commands"];
343
344
  export function authoringGuide(topic = "start", exampleId) {
@@ -370,7 +371,7 @@ Learn: method authoring Start the installed guide.
370
371
  Local drafts: init, show, set, remove, step add/update/remove/move,
371
372
  check set/remove, validate, diff.
372
373
  Online documents: list, get, steps, step, publish, explain.
373
- Improvements: improve, proposals, apply.
374
+ Results and improvements: judge, changes, improve, apply.
374
375
  Keys for scripts: secret find, secret import, secret set, secret list.
375
376
  Execution and evidence: run, run-status, wait, cancel, runs, logs, sync, observe.
376
377
  Effects and corrections: effect add/list, case new/retire/list, test.
@@ -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
  }
@@ -70,7 +62,9 @@ export async function methodMain(args = process.argv.slice(2), clientFactory = s
70
62
  process.stdout.write(earlyHelp);
71
63
  return;
72
64
  }
73
- if (['improve', 'proposals', 'apply'].includes(args[0] ?? ''))
65
+ if (args[0] === 'judge')
66
+ return (await import('./judge.js')).judgeCommand(args.slice(1), clientFactory);
67
+ if (['improve', 'changes', 'apply'].includes(args[0] ?? ''))
74
68
  return (await import('./proposals.js')).proposalCommand(args, clientFactory);
75
69
  await (await import('./proposals.js')).autoApply(args, clientFactory);
76
70
  if (args[0] === 'worker' || args[0] === 'keys')
@@ -99,9 +93,13 @@ export async function methodMain(args = process.argv.slice(2), clientFactory = s
99
93
  }
100
94
  catch (error) {
101
95
  // 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) };
96
+ const { setupFailure, failureLines } = await import('./failure.js');
97
+ const failure = setupFailure(error);
98
+ const failed = { status: error.code === 'needs_input' ? 'needs_input' : 'failed', run_dir: directory, failure, ...(error.missing ? { missing: error.missing } : {}) };
103
99
  if (!existsSync(join(directory, 'summary.json')))
104
100
  writePrivateJson(join(directory, 'summary.json'), failed);
101
+ if (failed.status === 'failed')
102
+ process.stderr.write(failureLines(failure));
105
103
  process.stdout.write(JSON.stringify(failed, null, 2) + '\n');
106
104
  process.exitCode = error.code === 'needs_input' ? 2 : 1;
107
105
  }
@@ -25,11 +25,21 @@ type Check = {
25
25
  /** Which part of the step the check reads; undefined when the check does not apply to the step. */
26
26
  script?: (step: Step) => string | undefined;
27
27
  applies: (step: Step) => boolean;
28
+ /** More inputs for Jev, from the Method (for example the definitions of an agent step's tools). */
29
+ context?: (step: Step, method: any) => Record<string, string>;
30
+ /** A finding (true) or none (false) that code analysis decides without a question to Jev; undefined asks Jev. */
31
+ decide?: (inputs: Record<string, string>) => boolean | undefined;
28
32
  field: string;
29
33
  message: (id: string) => string;
30
34
  fix: string;
31
35
  };
32
36
  export declare const checks: Record<string, Check>;
37
+ /**
38
+ * The script scan of a run step (explain-effects): a network host or a write is a finding with no model call. No host,
39
+ * no write, and no library that can send: no finding. A library that can send or a subprocess with no host found:
40
+ * undefined, so Jev answers.
41
+ */
42
+ export declare function scanDecision(stepYaml: string, script: string): boolean | undefined;
33
43
  /**
34
44
  * Checks shown to the user. A check moves here from shadow mode only with a results.json that shows its precision
35
45
  * on the frozen held-out set meets its level (warning >= 0.90, note >= 0.75). Run npm run eval:checks first.
@@ -78,7 +88,14 @@ export declare function askCheck(provider: ClassificationProvider, question: str
78
88
  probability: number;
79
89
  model: string;
80
90
  }>;
81
- export declare function evaluateTarget(provider: ClassificationProvider, target: Target, signal: AbortSignal): Promise<Result>;
91
+ /** The answer of code analysis, when the check has one for these inputs. */
92
+ export declare function decided(check: string, inputs: Record<string, string>): {
93
+ answer: boolean;
94
+ probability: number;
95
+ model: string;
96
+ } | undefined;
97
+ /** Answer one target: code analysis when it decides, or else Jev. scanned: code analysis already found it cannot decide. */
98
+ export declare function evaluateTarget(provider: ClassificationProvider | undefined, target: Target, signal: AbortSignal, scanned?: boolean): Promise<Result>;
82
99
  /**
83
100
  * Run the targets in parallel. Cached answers come at once; new ones get waitMs in all. The others are returned as
84
101
  * pending, so the caller can finish them in the background. A failed check is left out and is tried again next time.
@@ -8,7 +8,11 @@ import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, writeFile
8
8
  import { join } from 'node:path';
9
9
  import { serializeWorkflow } from '../../workflow-language/src/validate.js';
10
10
  import { methodCache } from './prepare.js';
11
+ import { scriptLanguage, staticEffects } from './explain-effects.js';
11
12
  const promptKinds = ['call', 'agent'];
13
+ /** A step that declares a change outside the run: a connection or service in changes, or an effect. */
14
+ const declaresWorldChange = (step) => (step.changes ?? []).some((ref) => typeof ref === 'string' && ref.startsWith('environment.')) || Object.keys(step.effects ?? {}).length > 0;
15
+ const agentWithTools = (step) => step.do?.kind === 'agent' && ((step.do.tools ?? []).length > 0 || !!step.do.browser);
12
16
  export const checks = {
13
17
  script_calls_model: {
14
18
  version: 1, level: 'warning', field: 'do',
@@ -52,7 +56,48 @@ export const checks = {
52
56
  message: id => `The prompt of ${id} appears to need information that the step does not give it.`,
53
57
  fix: 'Add the missing value to in: from an input or an earlier step, or write it in the prompt.',
54
58
  },
59
+ undeclared_change: {
60
+ version: 1, level: 'warning', field: 'do',
61
+ question: 'Can this step send, post, or change something outside its own output folder, for example send an email or a message, create or update a record in a service or an API, submit a form, or write files elsewhere, while its changes: and effects: declare no such change? Reading, searching, downloading, and returning a result are not changes.',
62
+ applies: step => !declaresWorldChange(step) && (step.do?.kind === 'run' || agentWithTools(step)),
63
+ script: step => step.do?.kind === 'run' ? step.do.entrypoint : undefined,
64
+ context: (step, method) => step.do?.kind === 'agent' && (step.do.tools ?? []).length
65
+ ? { tools: limit(serializeWorkflow(Object.fromEntries(step.do.tools.map(name => [name, method.tools?.[name] ?? null]))), 16_000) } : {},
66
+ decide: inputs => inputs.script === undefined ? undefined : scanDecision(inputs.step ?? "", inputs.script),
67
+ message: id => `${id} can send or change things that it does not declare.`,
68
+ fix: 'Declare what it changes in changes:, so Method can check where its data goes.',
69
+ },
55
70
  };
71
+ /** Libraries and calls that can send or change something, but whose target code analysis may not find. */
72
+ const senders = {
73
+ python: /^\s*(?:import|from)\s+(?:requests|httpx|aiohttp|urllib3?|http\.client|smtplib|email\.mime|boto3|slack_sdk|twilio|sendgrid|googleapiclient|subprocess)\b|\bos\.(?:system|popen)\(/m,
74
+ node: /\bfetch\(|\brequire\(\s*['"](?:node:)?(?:https?|child_process|axios|node-fetch|got|undici|nodemailer|@sendgrid\/mail|@slack\/web-api|twilio)['"]|\bfrom\s+['"](?:node:)?(?:https?|child_process|axios|node-fetch|got|undici|nodemailer|@sendgrid\/mail|@slack\/web-api|twilio)['"]/,
75
+ };
76
+ /** Temporary files are not a change in the world. */
77
+ const temporaryPath = (path) => /^(?:\/tmp\/|\/var\/folders\/|\/private\/tmp\/)/.test(path);
78
+ /**
79
+ * The script scan of a run step (explain-effects): a network host or a write is a finding with no model call. No host,
80
+ * no write, and no library that can send: no finding. A library that can send or a subprocess with no host found:
81
+ * undefined, so Jev answers.
82
+ */
83
+ export function scanDecision(stepYaml, script) {
84
+ const entrypoint = /^\s*entrypoint:\s*["']?([^\s"']+)/m.exec(stepYaml)?.[1];
85
+ const language = entrypoint ? scriptLanguage(entrypoint) : undefined;
86
+ if (!language || !script.trim())
87
+ return undefined;
88
+ let effects;
89
+ try {
90
+ effects = staticEffects(language, [{ path: entrypoint, text: script }], undefined).effects;
91
+ }
92
+ catch {
93
+ return undefined;
94
+ }
95
+ if (effects.network.length || effects.writes.some(path => !temporaryPath(path)))
96
+ return true;
97
+ if (effects.runs.length || senders[language].test(script))
98
+ return undefined;
99
+ return false;
100
+ }
56
101
  /**
57
102
  * Checks shown to the user. A check moves here from shadow mode only with a results.json that shows its precision
58
103
  * on the frozen held-out set meets its level (warning >= 0.90, note >= 0.75). Run npm run eval:checks first.
@@ -78,7 +123,7 @@ export function checkTargets(method, readText, steps) {
78
123
  if (!check.applies(step))
79
124
  continue;
80
125
  const path = check.script?.(step);
81
- const inputs = { step: stepText(id, step) };
126
+ const inputs = { step: stepText(id, step), ...check.context?.(step, method) };
82
127
  if (path)
83
128
  inputs.script = limit(readText(path) ?? '', 40_000);
84
129
  targets.push({ check: name, version: check.version, step: id, inputs, key: checkKey(name, check.version, inputs) });
@@ -86,7 +131,7 @@ export function checkTargets(method, readText, steps) {
86
131
  }
87
132
  return targets;
88
133
  }
89
- export const checkKey = (check, version, inputs) => createHash('sha256').update(JSON.stringify([check, version, inputs.step, inputs.script ?? ''])).digest('hex');
134
+ export const checkKey = (check, version, inputs) => createHash('sha256').update(JSON.stringify([check, version, inputs.step, inputs.script ?? '', ...(inputs.tools !== undefined ? [inputs.tools] : [])])).digest('hex');
90
135
  export const checksDir = () => join(methodCache(), 'checks');
91
136
  export function cached(key) {
92
137
  const file = join(checksDir(), `${key}.json`);
@@ -108,14 +153,24 @@ function store(result) {
108
153
  * probability is that of yes.
109
154
  */
110
155
  export const answerOptions = { yes: 'Yes: the question describes this step.', no: 'No: the question does not describe this step.' };
156
+ const noProvider = () => { throw Error('Model checks need sign-in.'); };
111
157
  export async function askCheck(provider, question, inputs, signal) {
112
158
  const { model } = await provider.resolve(signal);
113
159
  const answer = await provider.evaluate({ request_id: randomUUID(), model, question, options: answerOptions, inputs }, signal);
114
160
  const probability = answer.probabilities.yes ?? 0;
115
161
  return { answer: probability >= findingThreshold, probability, model };
116
162
  }
117
- export async function evaluateTarget(provider, target, signal) {
118
- const answer = await askCheck(provider, checks[target.check].question, target.inputs, signal);
163
+ /** The answer of code analysis, when the check has one for these inputs. */
164
+ export function decided(check, inputs) {
165
+ const answer = checks[check]?.decide?.(inputs);
166
+ return answer === undefined ? undefined : { answer, probability: answer ? 1 : 0, model: 'script-scan' };
167
+ }
168
+ /** Answer one target: code analysis when it decides, or else Jev. scanned: code analysis already found it cannot decide. */
169
+ export async function evaluateTarget(provider, target, signal, scanned = false) {
170
+ const answer = (scanned ? undefined : decided(target.check, target.inputs)) ?? await askCheck(provider ?? noProvider(), checks[target.check].question, target.inputs, signal);
171
+ return record(target, answer);
172
+ }
173
+ function record(target, answer) {
119
174
  const result = { check: target.check, version: target.version, step: target.step, key: target.key, answer: answer.probability >= findingThreshold,
120
175
  probability: answer.probability, model: answer.model, at: new Date().toISOString() };
121
176
  store(result);
@@ -127,15 +182,20 @@ export async function evaluateTarget(provider, target, signal) {
127
182
  */
128
183
  export async function runChecks(provider, targets, waitMs = 3000) {
129
184
  const done = [], pending = [];
130
- const open = targets.filter(target => { const hit = cached(target.key); if (hit)
131
- done.push(hit); return !hit; });
185
+ // Answers that code analysis decides need no provider and no wait.
186
+ const open = targets.filter(target => {
187
+ const hit = cached(target.key) ?? (() => { const answer = decided(target.check, target.inputs); return answer && record(target, answer); })();
188
+ if (hit)
189
+ done.push(hit);
190
+ return !hit;
191
+ });
132
192
  if (!open.length)
133
193
  return { done, pending };
134
194
  if (!provider)
135
195
  return { done, pending: open };
136
196
  const controller = new AbortController();
137
197
  const settled = new Set();
138
- const work = open.map(target => evaluateTarget(provider, target, controller.signal)
198
+ const work = open.map(target => evaluateTarget(provider, target, controller.signal, true)
139
199
  .then(result => { done.push(result); }).catch(() => { }).finally(() => settled.add(target.key)));
140
200
  let timer;
141
201
  await Promise.race([Promise.all(work), new Promise(resolve => { timer = setTimeout(resolve, waitMs); })]);
@@ -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);
@@ -58,9 +58,9 @@ type Applied = {
58
58
  /** Merge a proposal into the file. A clean merge writes the file and marks the proposal applied. Never publishes. */
59
59
  export declare function applyProposal(client: MethodClient | undefined, file: string, proposal: ProposalRecord, options?: RequestOptions): Promise<Applied>;
60
60
  /**
61
- * Before a command that takes a .method FILE: when this computer is signed in and the Method has an accepted proposal,
62
- * merge it into the file and print one line. A conflict prints what the coding agent must resolve; the command goes on.
63
- * Offline, signed out, or on any error, it does nothing.
61
+ * Before a command that takes a .method FILE: when this computer is signed in and Method applied a change to the Method
62
+ * (an accepted proposal, from the person's judgments), merge it into the file and print one line. A conflict prints
63
+ * what the coding agent must resolve; the command goes on. Offline, signed out, or on any error, it does nothing.
64
64
  */
65
65
  export declare function autoApply(args: string[], clientFactory: (server: string) => MethodClient): Promise<void>;
66
66
  /** Run folders of this Method on this computer, newest first, with the account run ID of each run that was saved to the account. */
@@ -68,6 +68,6 @@ export declare function localRuns(file: string): Promise<{
68
68
  dir: string;
69
69
  account_run_id: string | null;
70
70
  }[]>;
71
- /** `method improve`, `method proposals`, and `method apply`. */
71
+ /** `method improve`, `method changes`, and `method apply`. */
72
72
  export declare function proposalCommand(args: string[], clientFactory: (server: string) => MethodClient): Promise<boolean | undefined>;
73
73
  export {};
@@ -133,11 +133,11 @@ async function markApplied(client, file, proposal, options = {}) {
133
133
  }
134
134
  }
135
135
  const serverFlag = (args) => { const at = args.findIndex(arg => arg === '--server' || arg.startsWith('--server=')); return at < 0 ? undefined : args[at].startsWith('--server=') ? args[at].slice(9) : args[at + 1]; };
136
- const skipAutoApply = new Set(['apply', 'proposals', 'improve', 'init', 'new-id', 'diff', 'help', 'authoring', 'schema', '__worker', '__checks', 'worker', 'keys', 'secret', 'models', 'login', 'logout', 'sync', 'progress']);
136
+ const skipAutoApply = new Set(['apply', 'changes', 'judge', 'improve', 'init', 'new-id', 'diff', 'help', 'authoring', 'schema', '__worker', '__checks', 'worker', 'keys', 'secret', 'models', 'login', 'logout', 'sync', 'progress']);
137
137
  /**
138
- * Before a command that takes a .method FILE: when this computer is signed in and the Method has an accepted proposal,
139
- * merge it into the file and print one line. A conflict prints what the coding agent must resolve; the command goes on.
140
- * Offline, signed out, or on any error, it does nothing.
138
+ * Before a command that takes a .method FILE: when this computer is signed in and Method applied a change to the Method
139
+ * (an accepted proposal, from the person's judgments), merge it into the file and print one line. A conflict prints
140
+ * what the coding agent must resolve; the command goes on. Offline, signed out, or on any error, it does nothing.
141
141
  */
142
142
  export async function autoApply(args, clientFactory) {
143
143
  if (!args[0] || skipAutoApply.has(args[0]) || args.includes('--help') || process.env.METHOD_RUN_WORKER === '1')
@@ -159,12 +159,12 @@ export async function autoApply(args, clientFactory) {
159
159
  const result = await applyProposal(client, file, proposal, options);
160
160
  if (result.status === 'applied') {
161
161
  if (result.steps.length || result.warning)
162
- process.stderr.write(`Applied accepted proposal ${proposal.id} to ${target}: ${proposal.cause}${result.steps.length ? ` Changed steps: ${result.steps.join(', ')}.` : ''} Not published.${result.warning ? ` ${result.warning}` : ''}\n`);
162
+ process.stderr.write(`Method changed ${target} (${proposal.id}): ${proposal.cause}${result.steps.length ? ` Changed steps: ${result.steps.join(', ')}.` : ''}${result.warning ? ` ${result.warning}` : ''}\n`);
163
163
  }
164
164
  else
165
165
  process.stderr.write(result.status === 'conflict'
166
- ? `Accepted proposal ${proposal.id} conflicts with your edits of ${target} in ${result.conflicts.map(c => c.path).join(', ')}. The file is not changed. Run method apply ${target} ${proposal.id} to see both versions, merge them in the file, then run method apply ${target} ${proposal.id} --resolved.\n`
167
- : `Accepted proposal ${proposal.id} does not merge into a valid Method: ${result.error} The file is not changed. Run method apply ${target} ${proposal.id} to see it.\n`);
166
+ ? `Method's change ${proposal.id} conflicts with your edits of ${target} in ${result.conflicts.map(c => c.path).join(', ')}. The file is not changed. Run method apply ${target} ${proposal.id} to see both versions, merge them in the file, then run method apply ${target} ${proposal.id} --resolved.\n`
167
+ : `Method's change ${proposal.id} does not merge into a valid Method: ${result.error} The file is not changed. Run method apply ${target} ${proposal.id} to see it.\n`);
168
168
  }
169
169
  catch { /* offline or signed out: the command goes on without the proposal. */ }
170
170
  }
@@ -196,55 +196,36 @@ function manualStep(file, proposal, runFolder) {
196
196
  return 'This proposal has no change to apply.';
197
197
  }
198
198
  const print = (value) => process.stdout.write(JSON.stringify(value, null, 2) + '\n');
199
- function summary(p) {
200
- return { id: p.id, source: p.source, status: p.status, kind: p.evidence?.kind ?? null, cause: p.cause, steps: p.evidence?.steps ?? [], base_version_id: p.base_version_id, created_at: p.created_at };
199
+ const changeLine = (change) => `${change.date.slice(0, 10)} ${change.version_id ?? 'not saved as a version'} ${change.cause}`;
200
+ /** `method changes FILE`: the changes that Method applied to the Method, newest first, and those applied on this computer. */
201
+ async function changesCommand(file, target, methodId, client, json) {
202
+ const local = localProposals(file).filter(p => p.status === 'applied').map((p) => ({ id: p.id, source: 'local', cause: p.cause, date: String(p.applied_at ?? p.created_at), version_id: p.applied_version_id ?? null, steps: p.evidence?.steps ?? [] }));
203
+ let account = [];
204
+ if (methodId) {
205
+ const { proposals } = await client.request(`/api/methods/${encodeURIComponent(methodId)}/proposals?status=accepted`);
206
+ account = (proposals ?? []).map(p => ({ id: p.id, source: 'account', cause: String(p.cause ?? ''), date: String(p.created_at ?? ''), version_id: p.published_version_id ?? null, steps: p.evidence?.steps ?? [] }));
207
+ }
208
+ else if (!local.length)
209
+ throw Error(`${target} has no id: line, so Method has not changed it. Run it once while signed in.`);
210
+ const changes = [...account, ...local].sort((a, b) => b.date.localeCompare(a.date));
211
+ if (json)
212
+ return print({ file, method_id: methodId, changes });
213
+ process.stdout.write(changes.length ? changes.map(changeLine).join('\n') + '\n' : `Method has not changed ${target} yet.\n`);
201
214
  }
202
- /** `method improve`, `method proposals`, and `method apply`. */
215
+ /** `method improve`, `method changes`, and `method apply`. */
203
216
  export async function proposalCommand(args, clientFactory) {
204
217
  if (args[0] === 'improve')
205
218
  return (await import('./improve.js')).improveCommand(args.slice(1), clientFactory);
206
- const { values, positionals } = parseArgs({ args: args.slice(1), allowPositionals: true, options: { server: { type: 'string' }, resolved: { type: 'boolean' }, wait: { type: 'boolean' } } });
219
+ const { values, positionals } = parseArgs({ args: args.slice(1), allowPositionals: true, options: { server: { type: 'string' }, resolved: { type: 'boolean' }, json: { type: 'boolean' } } });
207
220
  const [target, wanted] = positionals;
208
221
  if (!target || positionals.length > (args[0] === 'apply' ? 2 : 1))
209
- throw Error(args[0] === 'apply' ? 'Use method apply FILE [PROPOSAL_ID] [--resolved].' : 'Use method proposals FILE.');
222
+ throw Error(args[0] === 'apply' ? 'Use method apply FILE [PROPOSAL_ID] [--resolved].' : 'Use method changes FILE [--json].');
210
223
  const file = authoringPath(target), id = readDocument(file).id;
211
224
  const methodId = typeof id === 'string' && methodIdPattern.test(id) ? id : null;
212
225
  const client = clientFactory(values.server ?? DEFAULT_SERVER);
213
226
  const signedIn = !!client.token();
214
- if (args[0] === 'proposals') {
215
- const local = localProposals(file).map(summary);
216
- if (!methodId || !signedIn)
217
- return print({ file, method_id: methodId, proposals: local, ...(methodId ? { next: 'Run method login to see the proposals in your account too.' } : {}) });
218
- const read = async () => {
219
- const [{ proposals }, { improvements }] = await Promise.all([
220
- client.request(`/api/methods/${encodeURIComponent(methodId)}/proposals`),
221
- client.request(`/api/methods/${encodeURIComponent(methodId)}/improvements`)
222
- ]);
223
- // The newest improvement that has no proposal: still running, failed, or ended with no safe change (outcome none).
224
- const latest = (improvements ?? []).find(i => !i.proposal_id);
225
- return { proposals, pending: latest && (latest.created_at ?? '') > String((proposals ?? [])[0]?.created_at ?? '') ? latest : undefined };
226
- };
227
- let { proposals, pending } = await read();
228
- // --wait: one line each time the improvement reaches a new step, until it ends.
229
- let shown = '';
230
- while (values.wait && pending && ['queued', 'running'].includes(pending.status)) {
231
- const step = pending.steps?.find((s) => s.status === 'running')?.step_id ?? pending.status;
232
- if (step !== shown) {
233
- process.stderr.write(`Improvement ${pending.id}: ${step}\n`);
234
- shown = step;
235
- }
236
- await new Promise(resolve => setTimeout(resolve, 5000));
237
- ({ proposals, pending } = await read());
238
- }
239
- const failedStep = pending?.steps?.find((s) => s.status === 'failed')?.step_id;
240
- const next = !pending ? undefined
241
- : ['queued', 'running'].includes(pending.status) ? `Improvement ${pending.id} is ${pending.status}. Run method proposals ${target} --wait: it returns when the proposal is ready.`
242
- : pending.outcome === 'none' ? `The improvement found no safe change. Say what is wrong in a note on a run (method improve ${target} --note TEXT), or record a case with method case new.`
243
- : pending.status === 'failed' || pending.status === 'cancelled' ? `Improvement ${pending.id} ${pending.status}${failedStep ? ` at step ${failedStep}` : ''}${pending.error?.message ? `: ${pending.error.message}` : ''}. Run method improve ${target} again, or record the correction with method case new.`
244
- : undefined;
245
- return print({ file, method_id: methodId, proposals: [...(proposals ?? []).map(p => summary({ ...p, source: 'account' })), ...local].sort((a, b) => String(b.created_at).localeCompare(String(a.created_at))),
246
- ...(pending ? { improvement: { id: pending.id, status: pending.status, ...(pending.outcome === 'none' ? { outcome: 'none' } : {}), ...(failedStep ? { failed_step: failedStep } : {}), ...(pending.error ? { error: pending.error } : {}) } } : {}), ...(next ? { next } : {}) });
247
- }
227
+ if (args[0] === 'changes')
228
+ return changesCommand(file, target, methodId, client, !!values.json);
248
229
  const local = localProposals(file);
249
230
  let proposal = wanted ? local.find(p => p.id === wanted) : undefined;
250
231
  if (!proposal && wanted) {
@@ -254,15 +235,13 @@ export async function proposalCommand(args, clientFactory) {
254
235
  if (methodId && proposal.method_id !== methodId)
255
236
  throw Error(`Proposal ${wanted} is for another Method.`);
256
237
  }
257
- if (!proposal && methodId && signedIn)
258
- proposal = await newestAccepted(client, methodId);
259
238
  proposal ??= local.find(p => p.status === 'open');
260
239
  if (!proposal)
261
- throw Error(`${target} has no accepted proposal. Run method proposals ${target}.`);
240
+ throw Error(`${target} has no open proposal. Run method improve ${target} --note TEXT first.`);
262
241
  if (proposal.status === 'applied')
263
242
  return print({ status: 'applied', file, proposal_id: proposal.id, next: 'This proposal is already applied.' });
264
243
  if (proposal.source === 'account' && proposal.status !== 'accepted')
265
- throw Error(`Proposal ${proposal.id} is ${proposal.status}. Accept it on the dashboard first: ${client.server}/methods/${proposal.method_id}`);
244
+ throw Error(`Change ${proposal.id} is ${proposal.status}, so it has nothing to merge.`);
266
245
  if (proposal.source === 'local' && proposal.status !== 'open')
267
246
  throw Error(`Proposal ${proposal.id} is ${proposal.status}.`);
268
247
  if (values.resolved)
@@ -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
  }