@method-ai-hq/sdk 0.18.2 → 0.20.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 (48) hide show
  1. package/README.md +1 -1
  2. package/dist/packages/sdk/package.json +7 -2
  3. package/dist/packages/sdk/src/authoring-instructions.d.ts +1 -1
  4. package/dist/packages/sdk/src/authoring-instructions.js +1 -1
  5. package/dist/packages/sdk/src/current-inspection.js +3 -0
  6. package/dist/packages/sdk/src/current-runtime.d.ts +6 -1
  7. package/dist/packages/sdk/src/current-runtime.js +47 -2
  8. package/dist/packages/sdk/src/fix-lines.d.ts +82 -0
  9. package/dist/packages/sdk/src/fix-lines.js +63 -0
  10. package/dist/packages/sdk/src/fix-outcomes.d.ts +72 -0
  11. package/dist/packages/sdk/src/fix-outcomes.js +201 -0
  12. package/dist/packages/sdk/src/home.d.ts +30 -26
  13. package/dist/packages/sdk/src/home.js +164 -60
  14. package/dist/packages/sdk/src/improve.d.ts +146 -24
  15. package/dist/packages/sdk/src/improve.js +665 -105
  16. package/dist/packages/sdk/src/judge.d.ts +23 -10
  17. package/dist/packages/sdk/src/judge.js +110 -26
  18. package/dist/packages/sdk/src/learned.d.ts +43 -0
  19. package/dist/packages/sdk/src/learned.js +97 -0
  20. package/dist/packages/sdk/src/local-cli.js +8 -3
  21. package/dist/packages/sdk/src/local-judgments.d.ts +2 -0
  22. package/dist/packages/sdk/src/merge.d.ts +38 -0
  23. package/dist/packages/sdk/src/merge.js +65 -0
  24. package/dist/packages/sdk/src/method-client.js +1 -1
  25. package/dist/packages/sdk/src/method-files.d.ts +3 -2
  26. package/dist/packages/sdk/src/method-files.js +7 -3
  27. package/dist/packages/sdk/src/method-help.d.ts +1 -1
  28. package/dist/packages/sdk/src/method-help.js +25 -16
  29. package/dist/packages/sdk/src/method-sync.d.ts +7 -0
  30. package/dist/packages/sdk/src/method-sync.js +33 -0
  31. package/dist/packages/sdk/src/method.d.ts +5 -0
  32. package/dist/packages/sdk/src/method.js +169 -15
  33. package/dist/packages/sdk/src/outbox.d.ts +28 -6
  34. package/dist/packages/sdk/src/outbox.js +13 -6
  35. package/dist/packages/sdk/src/proposals.d.ts +0 -28
  36. package/dist/packages/sdk/src/proposals.js +2 -129
  37. package/dist/packages/sdk/src/quality.d.ts +65 -5
  38. package/dist/packages/sdk/src/quality.js +82 -17
  39. package/dist/packages/sdk/src/run-saved.d.ts +4 -0
  40. package/dist/packages/sdk/src/run-saved.js +33 -2
  41. package/dist/packages/sdk/src/undo.d.ts +50 -0
  42. package/dist/packages/sdk/src/undo.js +108 -0
  43. package/dist/packages/sdk/src/versions.js +5 -3
  44. package/dist/packages/sdk/src/watch.d.ts +20 -0
  45. package/dist/packages/sdk/src/watch.js +86 -0
  46. package/dist/packages/workflow-language/src/inspection.d.ts +30 -0
  47. package/dist/packages/workflow-language/src/inspection.js +3 -1
  48. package/package.json +7 -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 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" },
16
+ wait: { usage: "method wait RUN_DIRECTORY\nmethod wait [JUDGMENT_ID] [--timeout SECONDS] [--json]", purpose: "Reconnect to a local run, or get what a fix did with a correction.", arguments: "With a run directory: reconnects to the same local worker; closing this output connection leaves the worker active. With a judgment ID (from method judge --json), or with nothing: waits for the fix of that correction, or of every correction that this computer is fixing, for up to --timeout seconds (default 100), and prints what it did. A wait that ends first says so in one line; the fix goes on, and method wait can run again.", result: "For a run: 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. For a fix: one plain line per correction for the person (Fixed, No fix found yet, Could not test a fix safely, a question, ...). After a fix it shows what the fixed agent gives now, from the test; nothing was sent. After a question, one more line says how to send the person's answer. --json prints {outcomes: [{judgment, status, outcome, lines, next?}]}.", errors: "Stopped process or missing run, or a judgment that this computer is not fixing.", example: "method wait .runs/example\nmethod wait\nmethod wait jdg_6f1c…" },
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,11 +36,13 @@ 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
- 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. A wrong one fixes the Method on this computer.", 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; it is recorded only. wrong: the person corrected it, or asked to change the Method because of it. --note: the person's words (at most 4000 characters), even when they are vague. --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 then fixes the Method in the foreground, with your own Claude Code or Codex (hosted models only when neither is signed in): it records the correction as a case in cases/ next to the Method file, finds the step that causes it, tests up to three small changes against this run, every earlier corrected or approved run, and every case, and changes the file only when a change fixes this run and breaks no earlier correction. The change is saved as a new version; restoring the version before it on the Method page undoes it. It takes a few minutes. Corrections that wait (from the dashboard, or a fix that stopped) are fixed in the same loop. For a run whose content stays on this computer (run_data: device), the account gets only the verdict and the step; the note and the corrected result go in .method/judgments.jsonl next to the Method file. Run it in the folder of the Method file when the run came from an older CLI. Do not ask the person to confirm a judgment, and do not edit the Method yourself for the same correction.", result: "One line: what changed and how many earlier corrections it passes, with how to undo it; or No safe change found and the reason. A subscription limit or another failure of the model service is an error, and the correction waits for the next method judge or method run. --json prints the judgment and the fix (kind change or none, cause, steps, cases made, version, usage with null dollars on a subscription, what the replay caps left out, and the log file).", errors: "An unknown run, a run of a Method that is not in your account, no sign-in, or a failure of the model service.", example: "method judge 6f1c… wrong --note \"it's not even redlined\"", remote: true },
40
- changes: { usage: "method changes FILE [--json] [--server URL]", purpose: "List the changes that Method applied to a Method.", arguments: "Lists, newest first, the changes that method judge applied on this computer and the changes that Method made in the account. Commands that take a signed-in FILE merge a change made in the account 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 change made on this computer.", example: "method changes task.method", remote: true },
39
+ judge: { usage: "method judge RUN_ID right|wrong [--step STEP_ID] [--note TEXT] [--corrected FILE] [--json] [--server URL]\nmethod judge RUN_ID keep JUDGMENT_ID", purpose: "Record the person's reaction to a run's result. A wrong one starts a fix on this computer and returns at once.", 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; it is recorded only. wrong: the person corrected it, or asked to change the Method because of it. --note: the person's words (at most 4000 characters), even when they are vague. --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 starts the fix in its own process, with your own Claude Code or Codex (hosted models only when neither is signed in), and prints one line at once. The fix records the correction as a case in cases/ next to the Method file, judged on what the person saw (the result, the files the run wrote, the messages it sent), and tests changes on a private copy of the run, with every message or change held back. It applies a change only when the change fixes this run reliably and breaks no earlier correction, or adds the note as a learned rule when it cannot test one safely. method wait prints what it did, and so does the next method run of the Method. method undo restores the Method as it was before. The fix starts from the newest version of the Method, as method run does. Two people who judge the same step of a run in opposite ways are held: the output shows both, and method judge RUN_ID keep JUDGMENT_ID keeps one and sets the other aside. For a run whose content stays on this computer (run_data: device), the account gets only the verdict and the step; the note and the corrected result go in .method/judgments.jsonl next to the Method file. Do not ask the person to confirm a judgment, and do not edit the Method yourself for the same correction.", result: "One line: Working on it. --json prints the judgment and {fix: {status: working, wait}}; method wait JUDGMENT_ID gives the outcome.", errors: "An unknown run, a run of a Method that is not in your account, or no sign-in.", example: "method judge 6f1c… wrong --note \"it's not even redlined\"\nmethod wait", remote: true },
40
+ learned: { usage: "method learned FILE [--remove NUMBER] [--json]", purpose: "List the rules that the agent learned from corrections, or remove one.", arguments: "The rules are in learned.json beside the Method file; a step uses a rule when its condition holds. Each line says the rule in plain words, who taught it, when, and how often it applied in this computer's runs. --remove NUMBER (from the list, or the rule's id from --json) retires one rule and saves a new version; method undo brings it back.", result: "One line per rule, or the rule that was removed. --json prints {rules: [{id, step, when, then, by, created, applied, text}]}.", errors: "No such rule, or a learned.json that is not valid.", example: "method learned nda.method\nmethod learned nda.method --remove 2", remote: true },
41
+ undo: { usage: "method undo [FILE] [--json]", purpose: "Undo Method's last change to a Method: a fix, a pruning change, or a removed learned rule.", arguments: "FILE defaults to the only Method file in this folder, else the Method of the last change on this computer. Restores the Method file and the files the change wrote, and saves that as a new version. A file that changed after Method's change is not overwritten. Run it again to undo the change before.", result: "One line: what is undone.", errors: "Nothing to undo, or the file changed after the change.", example: "method undo", remote: true },
42
+ changes: { usage: "method changes FILE [--json] [--server URL]", purpose: "List the changes that Method applied to a Method.", arguments: "Lists, newest first, the changes that method judge applied on this computer and the changes that Method made in the account. A change made in the account is a new version: the next method run of the file takes it.", 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 change made on this computer.", example: "method changes task.method", remote: true },
41
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" },
42
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 },
43
- 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 },
45
+ answer: { usage: "method answer RUN_ID [--answer JSON]\nmethod answer JUDGMENT_ID \"THE PERSON'S WORDS\"", purpose: "Answer the question of a production run that waits on an ask step, or the question that a fix asked about a correction.", arguments: "With a run ID: 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, and 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. With a judgment ID: send the person's own words, word for word, and the fix starts again with them in its own process; method wait gives what it did. Never answer a fix's question yourself. For a Method whose run content stays on this computer, the answer stays next to the Method file.", result: "The question and form, or the run after the answer; for a fix, one line.", errors: "The run is not waiting, the answer does not match the form, the key cannot open a sealed question (wrong_key), or the correction has no open question.", example: "method answer 6f1c… --answer '{\"approved\":true}'\nmethod answer jdg_6f1c… \"Every counterparty NDA must come back redlined.\"", remote: true },
44
46
  worker: { usage: "method worker [--method ID]... [--concurrency N] [--server URL]", purpose: "Run production runs that an app started with the runs API.", arguments: "Uses METHOD_API_KEY, or the sign-in of this computer. Use the app's key: runs that keep content on devices open only with the key that started them. The worker claims queued runs of the organization (or only of each --method), renews each run's lease, downloads the run's saved version by ID, prepares its dependencies once per version, reads declared secrets from the environment, and sends the run records to the dashboard. An ask step makes the run wait: the worker saves the run folder, reports the question, and lets go of the run; after the answer, a worker continues it and reuses the finished steps. --concurrency runs that many at the same time (default 1). Ctrl+C stops after the current runs; a second Ctrl+C stops them and queues them again. The libraries start a worker in the app's process with method.run(), so this command is for separate worker processes.", result: "One JSON line for each finished run: {run_id, method_id, version_id, status, run_data, result?, error?, run_dir}, and one for each run that starts to wait: {run_id, status: waiting, question}. A run_data: device result is printed only here.", errors: "Missing or revoked key. A run whose lease expires goes back to the queue and runs again (at most 3 attempts).", example: "METHOD_API_KEY=mk_live_... method worker --method wf_example", remote: true },
45
47
  keys: { usage: "method keys create --name NAME\nmethod keys list\nmethod keys revoke KEY_ID", purpose: "Manage the organization's service keys for production runs.", arguments: "A service key (mk_live_...) lets an app start runs and lets a worker run them. method connect makes one for an app and writes it to the app's .env. Keep the key in the app's secret store as METHOD_API_KEY; never put it in a Method or in chat.", result: "create prints the key and its webhook_secret once; the server keeps only a hash of the key. list shows names, prefixes, and last use, never keys.", errors: "Requires a person's sign-in (method login), not a service key.", example: "method keys create --name 'Cuties backend'", remote: true },
46
48
  status: { usage: "method status [--server URL]", purpose: "Check installation and sign-in without listing Methods or starting login.", arguments: "No required arguments. Checks the selected server with the current credential when one exists.", result: "JSON {installed:true,server,signed_in}. A missing or expired credential returns signed_in:false. Does not print account details.", errors: "Network and server errors exit 1; they are not reported as signed out.", example: "method status", remote: true },
@@ -54,7 +56,7 @@ export const commandHelp = {
54
56
  runs: { usage: "method runs [--method WORKFLOW_ID] [--server URL]", purpose: "Read saved run summaries.", arguments: "Default: all methods. --method filters the list.", result: "Server JSON containing runs. No changes.", errors: "", example: "method runs --method wf_example", remote: true },
55
57
  logs: { usage: "method logs RUN_ID [--server URL]", purpose: "Read saved run evidence before a repair.", arguments: "Use a run ID from method runs. Saved logs may be incomplete if upload failed.", result: "Server JSON containing the run, inputs, outputs, checks and events. No execution or changes.", errors: "Missing run.", example: "method logs run_example > run.json", remote: true },
56
58
  sync: { usage: "method sync RUN_DIRECTORY [--server URL]", purpose: "Retry upload of records from an existing local run.", arguments: "Directory must contain method-sync.json. Default destination is the saved run's server. Do not use a new business run to repair an upload.", result: "Upload status text. Updates dashboard records and local sync metadata. Does not execute steps.", errors: "Missing run/sync records; access or network error. Keep the original run directory and retry.", example: "method sync .method-runs/wf_example/saved-run", remote: true },
57
- run: { usage: "method run FILE.method [OPTIONS]\nmethod run WORKFLOW_ID [--version VERSION_ID] [--server URL] [OPTIONS]", purpose: "Run a local .method file, or a saved Method by ID. When this computer is signed in, a run of a local file starts at once and saves in the background: the version of the file's content and the run's records go to the dashboard through the outbox (~/.cache/method/outbox), and the last line says Saved as version N, or Will save when online; without sign-in the records stay in .method-runs. Steps whose definition and inputs match an earlier accepted run on this computer are reused.", arguments: `Models come from the Method's models: (or the account default). --workspace selects a different helper folder. See method authoring execution. Optional --state FILE initializes state for a new run. Resume with --resume --run-dir DIR; authorize unfinished work with --retry STEP:ITERATION.
59
+ run: { usage: "method run FILE.method [OPTIONS]\nmethod run WORKFLOW_ID [--version VERSION_ID] [--server URL] [OPTIONS]\nmethod run NAME [OPTIONS]", purpose: "Run a local .method file, or a saved Method by ID or by its name in your workspace. When this computer is signed in, a run of a local file starts at once and saves in the background: the version of the file's content and the run's records go to the dashboard through the outbox (~/.cache/method/outbox), and the last line says Saved as version N, or Will save when online; without sign-in the records stay in .method-runs. Steps whose definition and inputs match an earlier accepted run on this computer are reused.", arguments: `Models come from the Method's models: (or the account default). --workspace selects a different helper folder. See method authoring execution. Optional --state FILE initializes state for a new run. Resume with --resume --run-dir DIR; authorize unfinished work with --retry STEP:ITERATION.
58
60
 
59
61
  --inputs FILE: JSON input values.
60
62
  --inputs-from RUN_ID: use the inputs of that run; --inputs FILE overrides single values.
@@ -64,12 +66,17 @@ export const commandHelp = {
64
66
  --fresh: run every step; reuse nothing.
65
67
  --human FILE: answers for ask steps, as {steps: {"STEP": {outputs: {...}}}} (STEP:ITERATION, counted from 0, for a step with each or repeat).
66
68
  --verbose: print runtime events.
67
- Before a signed-in run of a FILE, corrections of the Method that wait (from the dashboard, or a fix that stopped) are fixed first, as method judge fixes them, with one line on stderr.
69
+ NAME: the name of a Method in your workspace (not case-sensitive), run as method run ID runs it: the newest version. When several Methods have the name, the run does not start; the result lists their names and IDs (exit 2).
70
+ Before a signed-in run of a FILE, the file takes the newest version of its Method. When a teammate saved a newer version, an unchanged file takes it, and a changed file combines it step by step and file by file; one line on stderr says whose version it took and which of your changes are kept. When you and a teammate changed the same step or the same script file, nothing runs and no file changes: the result has status needs_you and both texts of each part (exit 2). Combine them in the file, then run again. Offline, the file runs as it is, with one line. --resume does not change the file.
71
+ First, the outcomes of fixes of this Method that nobody has seen yet are printed, one line each. Then corrections of the Method that wait (from the dashboard, an answer, or a fix that stopped) start a fix in the background, as method judge does, with one line on stderr; the run does not wait for it. After a run that completed, its output is checked in the background against the corrections that fixes of the last 30 days answered; nothing runs again, and a problem that comes back twice is reported on the next run.
72
+ When the same run (the same version and the same inputs) is already going in your workspace, one line says who started it and links to it; this run starts too. To find it, each run record has a sha256 of its inputs; a run whose content stays on devices (run_data: device) sends none, because a hash of short inputs can show them.
73
+ A Method that uses shared state (method state) waits while another person's run holds the state, with one line, and starts when it is free. A run that stopped while it held the state stops this run, and the line names the person who must check it.
74
+ A Method whose declared secrets are not on this computer stops before the first step with the list of missing keys, each with its method secret set command. A step that stops because a file is not on this computer names the file and who saved this version.
68
75
  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 },
69
76
  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" },
70
- test: { usage: "method test FILE [--case ID]... [--baseline OLD_FILE] [--new ID]... [--cases DIR]", 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" },
77
+ test: { usage: "method test FILE [--case ID]... [--baseline OLD_FILE] [--new ID]... [--cases DIR] [--once | --runs N [--min-pass N]] [--live STEP]... [--no-judge-cache] [--keep-dir DIR]", 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; each attempt works on its own copy of the files the recorded run read, so a changed step that writes files runs safely; a Method tool that sends or changes something returns a stand-in instead. --baseline also runs each case on the old version, to show what the change fixed or broke. --once runs each case once; --runs N and --min-pass N replace each case's own counts. --live STEP runs that step live even when it matches the recording. --no-judge-cache judges again instead of reusing earlier judgments. --keep-dir keeps each attempt's run folder there.", result: "JSON report with a verdict per case and passed:true when no case blocks the change. Exit 0 or 1.", errors: "Unknown case or step, invalid Method, or setup errors.", example: "method test task.method --baseline task-before.method --new bounce-reported" },
71
78
  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" },
72
- "case new": { usage: "method case new FILE --id ID --note TEXT (--run BAD_RUN | --passing-run GOOD_RUN | both) (--rubric SENTENCE... | --expect FILE) [--ref outputs.NAME] [--context REF]... [--accept-conflict TEXT]", purpose: "Turn a correction into a recorded case.", arguments: "--run is the run that went wrong; the case must fail on it. --passing-run is the run the person accepted after the fix; the case must pass on it. With only --passing-run, the case pins behaviour that is already right. --rubric is a plain sentence that must be true of the output (repeatable); a model judges it with quotes. --ref selects the output to judge; the default is the Method's result. --context REF gives the judge other values to check against, such as the sources or the person's words (outputs.NAME or inputs.NAME); they are read, not judged. --expect FILE gives exact checks instead: {kind: equals, ref: outputs.NAME, value}, {kind: status, in: [STATUS]}, {kind: effect, effect: STEP/ITERATION/NAME, verdict: [VERDICT]}, or {kind: predicate, runtime: node, entrypoint: check.mjs}. --redact FILE maps recorded text to replacements. --accept-conflict TEXT keeps a case that conflicts with an instruction of the Method, with the person's answer.", result: "The saved case, with its result on each run. A case that the bad run already meets is refused: it does not capture the problem, or the note does not match the run. A case that can pass only by breaking an instruction of the Method's model steps is refused (case_conflicts): the error names the rule and the instruction.", errors: "A case that does not fail on the bad run, or does not pass on the passing run; a case that conflicts with an instruction; an existing ID; an unreadable run.", example: "method case new report.method --id sources-named --run .method-runs/bad --passing-run .method-runs/fixed --note 'Say which source backs each point.' --rubric 'Every point names the source file that supports it.'" },
79
+ "case new": { usage: "method case new FILE --id ID --note TEXT (--run BAD_RUN | --passing-run GOOD_RUN | both) (--rubric SENTENCE... | --expect FILE) [--ref outputs.NAME] [--context REF]... [--accept-conflict TEXT]", purpose: "Turn a correction into a recorded case.", arguments: "--run is the run that went wrong; the case must fail on it. --passing-run is the run the person accepted after the fix; the case must pass on it. With only --passing-run, the case pins behaviour that is already right. --rubric is a plain sentence that must be true of the output (repeatable); a model judges it with quotes. --ref selects the output to judge; the default is the Method's result. --context REF gives the judge other values to check against, such as the sources or the person's words (outputs.NAME or inputs.NAME); they are read, not judged. --expect FILE gives exact checks instead: {kind: equals, ref: outputs.NAME, value}, {kind: status, in: [STATUS]}, {kind: effect, effect: STEP/ITERATION/NAME, verdict: [VERDICT]}, or {kind: predicate, runtime: node, entrypoint: check.mjs}. --redact FILE maps recorded text to replacements. --accept-conflict TEXT keeps a case that conflicts with an instruction of the Method, with the person's answer.", result: "The saved case, with its result on each run. Its author is the signed-in person, so a refusal and the run notice name whose case it is. A case that the bad run already meets is refused: it does not capture the problem, or the note does not match the run. A case that can pass only by breaking an instruction of the Method's model steps is refused (case_conflicts): the error names the rule and the instruction.", errors: "A case that does not fail on the bad run, or does not pass on the passing run; a case that conflicts with an instruction; an existing ID; an unreadable run.", example: "method case new report.method --id sources-named --run .method-runs/bad --passing-run .method-runs/fixed --note 'Say which source backs each point.' --rubric 'Every point names the source file that supports it.'" },
73
80
  "case retire": { usage: "method case retire FILE ID --reason TEXT [--by NEW_ID]", purpose: "Retire a case whose rule no longer applies.", arguments: "The case stays on disk with its reason and is no longer run. Use it when a policy changed, not to make a failing change pass.", result: "JSON {retired}.", errors: "Unknown or already retired case.", example: "method case retire task.method old-terms --by new-terms --reason 'Payment terms changed on 1 October.'" },
74
81
  "case list": { usage: "method case list FILE", purpose: "List the cases of a Method.", arguments: "Reads cases/ beside the Method unless --cases DIR is given.", result: "JSON list of cases with status and note.", errors: "", example: "method case list task.method" },
75
82
  effect: { usage: "method effect add|list ...", purpose: "Use a reviewed observer for an external change.", arguments: "Use method help effect add or method help effect list.", result: "See the subcommand.", errors: "Unknown action. Choose add or list.", example: "method effect list" },
@@ -298,12 +305,14 @@ const recipes = `# Recipes
298
305
  ## Corrections become cases
299
306
 
300
307
  When the user says a result was wrong:
301
- 1. Record it: method judge RUN_ID wrong --note "the user's words" (--corrected FILE when they gave the right result). Use their words even when they are vague. Do not ask them to confirm it, and do not edit the Method for the same correction: method judge records the correction as a case, finds a change with the user's own agent, and applies it only when it fixes the run and breaks no earlier correction. It takes a few minutes.
302
- 2. Tell the user the line it prints in plain words, then run the Method again and show the new result.
303
- 3. When it prints No safe change found, tell the user the reason. If the user's words do not say what the right result is, ask before you change anything. Name the readings that the run and the Method's instructions allow, and what each one would change, in one question. A complaint about a missing part of the output can mean "produce it from what the inputs give", "mark what is missing", or "ask for it"; a complaint about wrong content can mean a wrong source, a wrong rule, or a wrong reading of the input. Do not choose a reading yourself, and never one that breaks the user's instructions. Then record the user's answer with method judge RUN_ID wrong --note "their answer".
304
- 4. For a rule the user states outside a run, record it: method case new FILE --id ID --run BAD_RUN --passing-run GOOD_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
- 5. case new refuses a case that can pass only by breaking one of the Method's instructions (case_conflicts), for example a rule that needs a fact that the inputs do not give when an instruction says not to invent facts, or a rule that requires what an instruction forbids. Show the user the rule and the instruction, and ask which one is right. When the user says the case is right, change the instruction first and record the case again. Add --accept-conflict "the user's answer" only when the user wants both.
306
- 6. After any edit by hand, run method test FILE. Every case must pass. method publish refuses a version that breaks a case.
308
+ 1. Record it: method judge RUN_ID wrong --note "the user's words" (--corrected FILE when they gave the right result). Use their words even when they are vague. Do not ask them to confirm it, and do not edit the Method for the same correction. method judge returns at once with one line and tests a fix in the background, on a private copy of the run, with every message held back.
309
+ 2. Get the result with method wait. It waits up to 100 seconds; when it says it is still working, run it again. Tell the user the line it prints, in their words.
310
+ 3. After a fix, show the user what the fixed agent gives now: method wait prints the output from the test. Do not run the Method again to show it, above all when it sends or changes things: that would send it again.
311
+ 4. When method wait prints a question, show it to the user word for word and never answer it yourself. Send their own words with the command it prints: method answer JUDGMENT_ID "their words". Then run method wait again.
312
+ 5. When the user wants to see or remove what the agent learned from corrections, run method learned FILE, and method learned FILE --remove NUMBER. To go back to how the agent worked before Method's last change, run method undo.
313
+ 6. For a rule the user states outside a run, record it: method case new FILE --id ID --run BAD_RUN --passing-run GOOD_RUN --note "the user's words" --rubric "plain sentence that must be true" (repeat --rubric for each rule). --ref names what to judge (default: the Method's result; result, outputs.NAME, file:PATH for a file the run wrote, or effect:NAME for what a tool or effect sent). 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.
314
+ 7. case new refuses a case that can pass only by breaking one of the Method's instructions (case_conflicts). Show the user the rule and the instruction, and ask which one is right. When the user says the case is right, change the instruction first and record the case again. Add --accept-conflict "the user's answer" only when the user wants both.
315
+ 8. After any edit by hand, run method test FILE. Every case must pass. method publish refuses a version that breaks a case.
307
316
  For a one-time preference, edit and run again; a case is not needed.
308
317
 
309
318
  ## Checked on every run, or on every new version
@@ -336,7 +345,7 @@ To fix a failed or wrong step, change it and run the Method again. The new run r
336
345
  For ask, supply --human FILE containing {steps: {"STEP": {outputs: {NAME: VALUE}}}} (STEP:ITERATION, counted from 0, for a step with each or repeat). Use the user's actual answer. Checks still run.
337
346
  A save never blocks a run: a run that cannot save says "Will save when online", and any later method command sends the outbox.
338
347
  State commits after checks. A local checkpoint cannot roll back an external write. Inspect external state before an explicit retry.
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.
348
+ 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. Before a run, a fix or a publish, the file takes the newest version of its Method: an unchanged file takes it, a changed file combines it step by step and file by file, and a step or a script file that you and a teammate changed in different ways stops the command with both texts, until you combine them in the file. A save that a teammate's newer save overtook is kept beside it, and the next run combines the two.
340
349
  Use method sync RUN_DIRECTORY to repair a dashboard upload without executing the method again.
341
350
  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.
342
351
  `;
@@ -371,7 +380,7 @@ Learn: method authoring Start the installed guide.
371
380
  Local drafts: init, show, set, remove, step add/update/remove/move,
372
381
  check set/remove, validate, diff.
373
382
  Online documents: list, get, steps, step, publish, explain.
374
- Results and improvements: judge, changes.
383
+ Results and improvements: judge, wait, answer, learned, undo, changes.
375
384
  Keys for scripts: secret find, secret import, secret set, secret list.
376
385
  Execution and evidence: run, run-status, wait, cancel, runs, logs, sync, observe.
377
386
  Effects and corrections: effect add/list, case new/retire/list, test.
@@ -41,6 +41,13 @@ export declare class MethodSync {
41
41
  /** Send the final local record of a run. Never runs business actions. */
42
42
  static retry(directory: string, client?: MethodClient): Promise<void>;
43
43
  }
44
+ /** The hash of a run's requested inputs: sha256 of their canonical JSON. */
45
+ export declare const inputsSha256: (inputs: unknown) => string;
46
+ /**
47
+ * The line that says that the same run is going: a running run of this version with the same inputs, from anyone in the
48
+ * workspace. Undefined when there is none, or the list cannot be read (offline). Never blocks the run.
49
+ */
50
+ export declare function sameRunNotice(client: Pick<MethodClient, 'server' | 'request' | 'token'>, methodId: string, versionId: string, inputs: unknown): Promise<string | undefined>;
44
51
  /** The dashboard page of the run in a run folder, when the run is linked to an account. */
45
52
  export declare function runDashboard(directory: string): string | undefined;
46
53
  /**
@@ -8,6 +8,7 @@ import { inspectRun } from "./inspect.js";
8
8
  import { setupFailure, unreadableRecord } from "./failure.js";
9
9
  import { writePrivateJson } from "./files.js";
10
10
  import { MethodClient } from "./method-client.js";
11
+ import { canonicalJson, sha256 } from "../../contracts/src/identity.js";
11
12
  import { InspectionSchema, deviceOnly, } from "../../workflow-language/src/inspection.js";
12
13
  const SyncSchema = z.object({
13
14
  schema: z.literal("method-sync/1"),
@@ -19,6 +20,8 @@ const SyncSchema = z.object({
19
20
  dashboard_id: z.string().optional(),
20
21
  // The run's content stays on this computer, also when the sync is retried later.
21
22
  device: z.literal(true).optional(),
23
+ // sha256 of the requested inputs, sent so that a second start of the same run can say so. Never for a device run.
24
+ inputs_sha256: z.string().optional(),
22
25
  });
23
26
  export class MethodSync {
24
27
  client;
@@ -91,6 +94,8 @@ export class MethodSync {
91
94
  version_id: this.state.version_id,
92
95
  sequence: ++this.state.sequence,
93
96
  inspection,
97
+ // A hash of short inputs can show them, so a run whose content stays on devices sends none.
98
+ ...(!this.device && this.state.inputs_sha256 ? { inputs_sha256: this.state.inputs_sha256 } : {}),
94
99
  };
95
100
  this.save();
96
101
  if (inspection.files?.length) {
@@ -116,6 +121,11 @@ export class MethodSync {
116
121
  try {
117
122
  if (this.live)
118
123
  return;
124
+ // The first start has the requested inputs; the runtime's start adds defaults.
125
+ if (!this.state.inputs_sha256 && !this.device) {
126
+ this.state.inputs_sha256 = inputsSha256(inputs);
127
+ this.save();
128
+ }
119
129
  this.initial = {
120
130
  schema: "workflow-inspection/2",
121
131
  workflow,
@@ -268,6 +278,29 @@ export class MethodSync {
268
278
  sync.outbox.remove(sync.item);
269
279
  }
270
280
  }
281
+ /** The hash of a run's requested inputs: sha256 of their canonical JSON. */
282
+ export const inputsSha256 = (inputs) => sha256(canonicalJson(inputs ?? {}));
283
+ /**
284
+ * The line that says that the same run is going: a running run of this version with the same inputs, from anyone in the
285
+ * workspace. Undefined when there is none, or the list cannot be read (offline). Never blocks the run.
286
+ */
287
+ export async function sameRunNotice(client, methodId, versionId, inputs) {
288
+ try {
289
+ if (!client.token())
290
+ return undefined;
291
+ const hash = inputsSha256(inputs);
292
+ const { runs } = await client.request(`/api/workspace/runs?workflow_id=${encodeURIComponent(methodId)}`, 'GET', undefined, true, { signal: AbortSignal.timeout(3000), stallMs: 3000 });
293
+ const same = (runs ?? []).filter(r => r?.status === 'running' && r.workflow_version_id === versionId && r.inputs_sha256 === hash)
294
+ .sort((a, b) => String(b.created_at).localeCompare(String(a.created_at)))[0];
295
+ if (!same)
296
+ return undefined;
297
+ const minutes = Math.max(0, Math.round((Date.now() - Date.parse(same.created_at)) / 60_000));
298
+ return `${same.started_by ?? 'A teammate'} started this same run ${minutes} minute${minutes === 1 ? '' : 's'} ago: ${client.server}/runs/${same.id}. This run starts too.`;
299
+ }
300
+ catch {
301
+ return undefined;
302
+ }
303
+ }
271
304
  /** The dashboard page of a run: its saved ID, or before the first record, the ID that this computer gave it. */
272
305
  function dashboardUrl(state) {
273
306
  return `${state.server}/runs/${state.dashboard_id ?? state.id}`;
@@ -1,3 +1,8 @@
1
1
  #!/usr/bin/env node
2
2
  import { MethodClient } from "./method-client.js";
3
+ /**
4
+ * Bring a Method file to the newest version of its Method before a run or a publish, with one line on stderr. Returns
5
+ * the catch-up status; conflict when the person must combine two texts first: then nothing runs and no file changes.
6
+ */
7
+ export declare function catchUpFile(client: MethodClient | undefined, file: string, doing?: string): Promise<"skipped" | "conflict" | "current" | "offline" | "updated">;
3
8
  export declare function methodMain(args?: string[], clientFactory?: (server: string) => MethodClient): Promise<boolean | void>;
@@ -4,7 +4,7 @@ import runtimeInfo from '@withmethod/runtime/package.json' with { type: 'json' }
4
4
  import { startRunWorker, waitForRun, cancelRun, workerAlive } from './run-worker.js';
5
5
  import { methodCache } from './prepare.js';
6
6
  import { restorePackage } from './method-files.js';
7
- import { publish, methodContent } from './versions.js';
7
+ import { publish, methodContent, methodIdPattern } from './versions.js';
8
8
  import { runSaved } from './run-saved.js';
9
9
  import { bindInput, bindConnection } from './bindings.js';
10
10
  import { existsSync, readFileSync, realpathSync } from "node:fs";
@@ -62,14 +62,104 @@ async function dispatchRunWorker(args, flags, extra = [], savedClient) {
62
62
  await startRunWorker(workerArgs, directory, flags.background, server);
63
63
  return true;
64
64
  }
65
+ /** Print a result that stops a command before it starts: the JSON on stdout, the three failure lines on stderr. Exit code 2. */
66
+ async function stopForPerson(result) {
67
+ process.stderr.write((await import('./failure.js')).failureLines(result.failure));
68
+ process.stdout.write(JSON.stringify(result, null, 2) + '\n');
69
+ process.exitCode = 2;
70
+ }
71
+ /**
72
+ * Bring a Method file to the newest version of its Method before a run or a publish, with one line on stderr. Returns
73
+ * the catch-up status; conflict when the person must combine two texts first: then nothing runs and no file changes.
74
+ */
75
+ export async function catchUpFile(client, file, doing = 'running') {
76
+ if (!client)
77
+ return 'skipped';
78
+ const result = await (await import('./home.js')).catchUp(client, file, doing);
79
+ if (result.status === 'conflict')
80
+ await stopForPerson(result.result);
81
+ else if ('line' in result)
82
+ process.stderr.write(result.line + '\n');
83
+ return result.status;
84
+ }
85
+ /** The line that says the same run (same version, same inputs) is already going. Never blocks; nothing for device runs. */
86
+ async function sameRunLine(client, methodId, versionId, flags, device) {
87
+ if (!client || device || process.env.METHOD_RUN_WORKER === '1' || flags.resume || flags['inputs-from'])
88
+ return;
89
+ let inputs = {};
90
+ try {
91
+ if (flags.inputs)
92
+ inputs = JSON.parse(readFileSync(resolve(flags.inputs), 'utf8'));
93
+ }
94
+ catch {
95
+ return;
96
+ }
97
+ const line = await (await import('./method-sync.js')).sameRunNotice(client, methodId, versionId, inputs);
98
+ if (line)
99
+ process.stderr.write(line + '\n');
100
+ }
101
+ /**
102
+ * Before method run FILE in the parent process: the outcomes of fixes not yet shown are printed, the file takes the
103
+ * newest version of its Method, corrections that wait start a fix, and the same-run line is printed. Returns false when the run must not start.
104
+ */
105
+ async function beforeFileRun(args, clientFactory) {
106
+ if (process.env.METHOD_RUN_WORKER === '1' || args.includes('--resume') || !existsSync(args[1]))
107
+ return true;
108
+ const server = parseArgs({ args: args.slice(2), strict: false, options: { server: { type: 'string' } } }).values.server;
109
+ let client;
110
+ try {
111
+ client = clientFactory(typeof server === 'string' ? server : DEFAULT_SERVER);
112
+ if (!client.token())
113
+ client = undefined;
114
+ }
115
+ catch {
116
+ client = undefined;
117
+ }
118
+ const file = authoringPath(args[1]);
119
+ // The outcomes of fixes that nobody has seen yet come first.
120
+ const id = (() => { try {
121
+ return readDocument(file).id;
122
+ }
123
+ catch {
124
+ return undefined;
125
+ } })();
126
+ if (typeof id === 'string' && methodIdPattern.test(id))
127
+ for (const line of (await import('./fix-outcomes.js')).unshownLines(id))
128
+ process.stderr.write(line + '\n');
129
+ const caught = await catchUpFile(client, file);
130
+ if (caught === 'conflict')
131
+ return false;
132
+ await fixWaitingCorrections(args, clientFactory);
133
+ const version = client && caught !== 'offline' ? await (await import('./versions.js')).currentVersion(file).catch(() => undefined) : undefined;
134
+ if (version) {
135
+ let flags;
136
+ try {
137
+ flags = parse(args.slice(2)).values;
138
+ }
139
+ catch {
140
+ return true;
141
+ }
142
+ await sameRunLine(client, version.method_id, version.version_id, flags, readDocument(file).run_data === 'device');
143
+ }
144
+ return true;
145
+ }
65
146
  /**
66
- * Before method run FILE: corrections that wait for this Method (from the dashboard, or a loop that stopped) are fixed
67
- * first, with one line about them. A failure of the loop is one line too; the run goes on. Signed out, offline, a
68
- * resumed run, or the run worker: nothing.
147
+ * Before method run FILE: corrections that wait for this Method (from the dashboard, an answer on the dashboard, or a
148
+ * fix that stopped) start a fix in its own process, with one line; the run does not wait for it. Signed out, offline,
149
+ * a resumed run, or the run worker: nothing.
69
150
  */
70
151
  async function fixWaitingCorrections(args, clientFactory) {
71
152
  if (process.env.METHOD_RUN_WORKER === '1' || args.includes('--resume') || !existsSync(args[1]))
72
153
  return;
154
+ const file = authoringPath(args[1]), id = (() => { try {
155
+ return readDocument(file).id;
156
+ }
157
+ catch {
158
+ return undefined;
159
+ } })();
160
+ if (typeof id !== 'string' || !methodIdPattern.test(id))
161
+ return;
162
+ const outcomes = await import('./fix-outcomes.js');
73
163
  const server = parseArgs({ args: args.slice(2), strict: false, options: { server: { type: 'string' } } }).values.server;
74
164
  let client;
75
165
  try {
@@ -80,17 +170,44 @@ async function fixWaitingCorrections(args, clientFactory) {
80
170
  catch {
81
171
  return;
82
172
  }
83
- const { fixCorrections, waitingLine } = await import('./improve.js');
173
+ const { fixRunning, waitingCorrections } = await import('./improve.js');
174
+ // A fix that runs now takes the waiting corrections in its next pass.
175
+ if (fixRunning(id))
176
+ return;
84
177
  try {
85
- const fix = await fixCorrections(client, authoringPath(args[1]), { waitMs: 3000 });
86
- if (fix)
87
- process.stderr.write(waitingLine(fix) + '\n');
178
+ const waiting = (await waitingCorrections(client, file, id, 3000)).map(j => j.id);
179
+ if (!waiting.length)
180
+ return;
181
+ outcomes.markWorking(id, file, waiting);
182
+ await outcomes.fixStarter.start(file, id, waiting, client.server);
183
+ process.stderr.write((await import('./fix-lines.js')).waitingLine(waiting.length) + '\n');
88
184
  }
89
185
  catch (error) {
90
186
  if (!/^(fetch failed|401:|404:)|abort|timed? ?out/i.test(error.message))
91
187
  process.stderr.write(`The waiting corrections are not fixed yet: ${error.message}\n`);
92
188
  }
93
189
  }
190
+ /**
191
+ * The ID of the workspace's Method with this name (case-insensitive, archived Methods left out), or null when none has
192
+ * it. Several matches list their names and IDs and run nothing (exit code 2): then undefined.
193
+ */
194
+ async function methodByName(client, name) {
195
+ const found = [];
196
+ for (let cursor = null, first = true; first || cursor; first = false) {
197
+ const page = await client.request(`/api/methods?limit=500${cursor ? `&cursor=${encodeURIComponent(cursor)}` : ''}`);
198
+ found.push(...(page.workflows ?? []).filter(w => !w.archived_at && typeof w.name === 'string' && w.name.trim().toLowerCase() === name.trim().toLowerCase()));
199
+ cursor = page.next_cursor ?? null;
200
+ }
201
+ if (found.length === 1)
202
+ return found[0].id;
203
+ if (!found.length)
204
+ return null;
205
+ const message = `${found.length} Methods are named "${name}". Run one by its ID: ${found.map(w => `method run ${w.id} (${w.name})`).join(', ')}.`;
206
+ process.stderr.write(message + '\n');
207
+ process.stdout.write(JSON.stringify({ status: 'needs_you', message, matches: found.map(w => ({ name: w.name, id: w.id })) }, null, 2) + '\n');
208
+ process.exitCode = 2;
209
+ return undefined;
210
+ }
94
211
  export async function methodMain(args = process.argv.slice(2), clientFactory = server => new MethodClient(server)) {
95
212
  const resumed = (await import('./resume-release.js')).resumeWithOriginalRelease(args);
96
213
  if (resumed !== undefined) {
@@ -106,11 +223,27 @@ export async function methodMain(args = process.argv.slice(2), clientFactory = s
106
223
  }
107
224
  if (args[0] === 'judge')
108
225
  return (await import('./judge.js')).judgeCommand(args.slice(1), clientFactory);
226
+ if (args[0] === 'answer' && /^jdg_/.test(args[1] ?? ''))
227
+ return (await import('./judge.js')).answerJudgmentCommand(args.slice(1), clientFactory);
228
+ if (args[0] === 'wait' && (!args[1] || /^jdg_|^--/.test(args[1])))
229
+ return (await import('./fix-outcomes.js')).waitCommand(args.slice(1));
230
+ if (args[0] === 'learned')
231
+ return (await import('./learned.js')).learnedCommand(args.slice(1), clientFactory);
232
+ if (args[0] === 'undo')
233
+ return (await import('./undo.js')).undoCommand(args.slice(1), clientFactory);
234
+ if (args[0] === '__fix' || args[0] === '__watch') {
235
+ const p = parseArgs({ args: args.slice(1), allowPositionals: true, options: { server: { type: 'string' }, judgment: { type: 'string', multiple: true }, prune: { type: 'boolean' } } });
236
+ const client = clientFactory(p.values.server ?? DEFAULT_SERVER), file = authoringPath(p.positionals[0]);
237
+ if (args[0] === '__watch') {
238
+ await (await import('./watch.js')).watchRun(client, file, resolve(p.positionals[1]));
239
+ return;
240
+ }
241
+ return (await import('./improve.js')).backgroundFix(client, file, p.values.judgment ?? [], { prune: !!p.values.prune });
242
+ }
109
243
  if (args[0] === 'changes')
110
244
  return (await import('./proposals.js')).changesMain(args.slice(1), clientFactory);
111
- await (await import('./proposals.js')).autoApply(args, clientFactory);
112
- if (args[0] === 'run' && /\.method$/i.test(args[1] ?? '') && !args.includes('--help'))
113
- await fixWaitingCorrections(args, clientFactory);
245
+ if (args[0] === 'run' && /\.method$/i.test(args[1] ?? '') && !args.includes('--help') && !await beforeFileRun(args, clientFactory))
246
+ return;
114
247
  if (args[0] === 'worker' || args[0] === 'keys')
115
248
  return (await import('./worker.js')).productionCommand(args, clientFactory);
116
249
  if (args[0] === '__run-data')
@@ -230,7 +363,8 @@ export async function methodMain(args = process.argv.slice(2), clientFactory = s
230
363
  else
231
364
  rest.push(args[i]);
232
365
  }
233
- const [command, target, step] = rest;
366
+ const [command, , step] = rest;
367
+ let target = rest[1];
234
368
  if (!command || command === "--help" || command === "help") {
235
369
  process.stdout.write(help);
236
370
  return;
@@ -361,16 +495,36 @@ export async function methodMain(args = process.argv.slice(2), clientFactory = s
361
495
  if (command === "publish") {
362
496
  if (!authorFile)
363
497
  throw Error("Use method publish FILE [--reason TEXT] [--cloud|--workers] [--env NAME].");
498
+ if (await catchUpFile(client, safePath(authorFile), 'publishing') === 'conflict')
499
+ return;
364
500
  print(await (await import("./cloud.js")).publishWithPlacement(client, rest, () => publish(client, safePath(authorFile), values.reason, (values["accept-failing-case"] ?? "").split(",").map(id => id.trim()).filter(Boolean))));
365
501
  return;
366
502
  }
367
503
  if (!target)
368
504
  throw Error(help);
505
+ // method run NAME: the Method of the workspace with this name, run as method run ID runs it.
506
+ let named;
507
+ if (command === "run" && !methodIdPattern.test(target) && !existsSync(target)) {
508
+ const found = await methodByName(client, target);
509
+ if (found === undefined)
510
+ return;
511
+ if (found) {
512
+ const at = args.indexOf(target);
513
+ args = [...args.slice(0, at), found, ...args.slice(at + 1)];
514
+ target = found;
515
+ }
516
+ else
517
+ named = target;
518
+ }
369
519
  const path = `/api/cli/methods/${encodeURIComponent(target)}`;
370
520
  if (!["get", "steps", "step", "run"].includes(command))
371
521
  throw Error(help);
372
522
  const saved = await client.request(path +
373
- (values.version ? `?version=${encodeURIComponent(values.version)}` : ""));
523
+ (values.version ? `?version=${encodeURIComponent(values.version)}` : "")).catch(error => {
524
+ if (named && /^40[34]:/.test(error.message))
525
+ throw Error(`No Method named "${named}" in your workspace, and no file or Method ID ${named}. Run method list to see the names.`);
526
+ throw error;
527
+ });
374
528
  const workflow = command === "get" ? saved.workflow : loadWorkflow(saved.workflow);
375
529
  if (command === "steps") {
376
530
  print({ version_id: saved.version_id, steps: workflow.steps });
@@ -399,12 +553,11 @@ export async function methodMain(args = process.argv.slice(2), clientFactory = s
399
553
  // An older saved version carries runtime.json; the file gets it in the Method, as in format 3.4.
400
554
  (await import("./legacy.js")).convertLegacyOrThrow(out);
401
555
  writeMethodId(out, saved.workflow_id);
402
- (await import("./outbox.js")).recordCheckout(client.server, saved.workflow_id, saved.version_id, saved.version_number, methodContent(workflow));
556
+ (await import("./outbox.js")).recordCheckout(client.server, saved.workflow_id, saved.version_id, saved.version_number, methodContent(workflow), { saved_by: saved.saved_by ?? null });
403
557
  // The new file becomes the Method's home on this computer unless another file already is.
404
558
  const home = await import("./home.js");
405
559
  try {
406
560
  home.claimHome(client.server, saved.workflow_id, out);
407
- home.recordHome(client.server, saved.workflow_id, out, { version_id: saved.version_id, files: home.writtenFiles(out, saved.package?.files ?? []) });
408
561
  }
409
562
  catch { /* a copy for reading */ }
410
563
  print({
@@ -419,6 +572,7 @@ export async function methodMain(args = process.argv.slice(2), clientFactory = s
419
572
  return;
420
573
  }
421
574
  const flags = parse(rest.slice(2)).values;
575
+ await sameRunLine(client, saved.workflow_id, saved.version_id, flags, saved.workflow?.run_data === 'device');
422
576
  if (await dispatchRunWorker(args, flags, values.version ? [] : ['--version', saved.version_id], client))
423
577
  return;
424
578
  await runSaved(saved, flags, client);
@@ -28,6 +28,7 @@ export type VersionItem = {
28
28
  digest: string;
29
29
  };
30
30
  parent_version_id?: string;
31
+ merged_version_id?: string;
31
32
  reason: string;
32
33
  };
33
34
  };
@@ -39,23 +40,44 @@ export type RunItem = {
39
40
  pid: number;
40
41
  };
41
42
  export type Item = FileItem | VersionItem | RunItem;
43
+ /** beside: the version was saved beside a newer one (by that person, or null); the next run combines them. */
42
44
  type Saved = {
43
45
  method_id: string;
44
46
  version_number: number;
47
+ beside?: string | null;
48
+ };
49
+ /**
50
+ * The version that this computer last saved or got of a Method: the base of the next combine and the parent of the next
51
+ * save. side_version_id: a later save of this computer that was kept beside a newer version. merged_version_id: the
52
+ * side version that the file now combines, sent with the next save. saved_by: who saved version_id. conflict: a combine
53
+ * with the newer version theirs stopped, and this computer's text of each part that both sides changed (a step's
54
+ * definition, or a file's sha256): a part whose text changed since then is combined by the person.
55
+ */
56
+ export type CombineStop = {
57
+ theirs: string;
58
+ parts: Record<string, unknown>;
59
+ files: Record<string, string | null>;
60
+ };
61
+ export type LastSaved = {
62
+ version_id?: string;
63
+ version_number?: number;
64
+ workflow?: any;
65
+ side_version_id?: string;
66
+ merged_version_id?: string;
67
+ saved_by?: string | null;
68
+ conflict?: CombineStop;
45
69
  };
46
70
  export declare const outboxDir: () => string;
47
71
  /** Versions this computer knows the server has: version_id -> {method_id, version_number}. */
48
72
  export declare function savedVersions(server: string): Record<string, Saved>;
49
73
  /** The last version this computer saved of a Method, for the parent and the change reason of the next one. */
50
- export declare function lastSaved(server: string, methodId: string): {
51
- version_id: string;
52
- version_number?: number;
53
- workflow: any;
54
- } | undefined;
74
+ export declare function lastSaved(server: string, methodId: string): LastSaved | undefined;
55
75
  /** Note a version that the server is known to have (a saved Method run by ID). */
56
76
  export declare function recordKnownVersion(server: string, methodId: string, versionId: string, versionNumber: number): void;
57
77
  /** A checkout of a saved version: the next save of this Method on this computer has it as its parent. */
58
- export declare function recordCheckout(server: string, methodId: string, versionId: string, versionNumber: number, workflow: unknown): void;
78
+ export declare function recordCheckout(server: string, methodId: string, versionId: string, versionNumber: number, workflow: unknown, extra?: Pick<LastSaved, 'merged_version_id' | 'saved_by'>): void;
79
+ /** A combine that stopped: the parts that both sides changed, as they were on this computer. */
80
+ export declare function recordCombineStop(server: string, methodId: string, conflict: CombineStop): void;
59
81
  /** Errors that a retry can fix: no connection, a stalled transfer, a server error, or too many requests. */
60
82
  export declare function retryable(error: any): boolean;
61
83
  export declare class Outbox {