@method-ai-hq/sdk 0.18.1 → 0.19.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/packages/sdk/package.json +6 -1
- package/dist/packages/sdk/src/current-runtime.d.ts +6 -1
- package/dist/packages/sdk/src/current-runtime.js +50 -5
- package/dist/packages/sdk/src/home.d.ts +30 -26
- package/dist/packages/sdk/src/home.js +164 -60
- package/dist/packages/sdk/src/improve.d.ts +25 -0
- package/dist/packages/sdk/src/improve.js +256 -10
- package/dist/packages/sdk/src/judge.d.ts +14 -2
- package/dist/packages/sdk/src/judge.js +73 -13
- package/dist/packages/sdk/src/local-cli.js +4 -6
- package/dist/packages/sdk/src/merge.d.ts +38 -0
- package/dist/packages/sdk/src/merge.js +65 -0
- package/dist/packages/sdk/src/method-client.js +1 -1
- package/dist/packages/sdk/src/method-help.js +11 -6
- package/dist/packages/sdk/src/method-sync.d.ts +7 -0
- package/dist/packages/sdk/src/method-sync.js +33 -0
- package/dist/packages/sdk/src/method.d.ts +5 -0
- package/dist/packages/sdk/src/method.js +139 -14
- package/dist/packages/sdk/src/outbox.d.ts +28 -6
- package/dist/packages/sdk/src/outbox.js +13 -6
- package/dist/packages/sdk/src/proposals.d.ts +0 -28
- package/dist/packages/sdk/src/proposals.js +2 -129
- package/dist/packages/sdk/src/quality.d.ts +10 -1
- package/dist/packages/sdk/src/quality.js +34 -4
- package/dist/packages/sdk/src/run-saved.d.ts +4 -0
- package/dist/packages/sdk/src/run-saved.js +45 -5
- package/dist/packages/sdk/src/versions.js +5 -3
- package/package.json +6 -1
|
@@ -36,8 +36,8 @@ 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.
|
|
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 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. The fix starts from the newest version of the Method, as method run does. A correction of a step that someone changed after the run is not fixed, and the line names who changed it. A change that fixes one person's correction only by breaking another person's case is held, not applied, and the line names both. Two people who judge the same step of a run in opposite ways are held too: 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. 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. 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
41
|
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
42
|
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
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 },
|
|
@@ -54,7 +54,7 @@ export const commandHelp = {
|
|
|
54
54
|
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
55
|
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
56
|
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.
|
|
57
|
+
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
58
|
|
|
59
59
|
--inputs FILE: JSON input values.
|
|
60
60
|
--inputs-from RUN_ID: use the inputs of that run; --inputs FILE overrides single values.
|
|
@@ -64,12 +64,17 @@ export const commandHelp = {
|
|
|
64
64
|
--fresh: run every step; reuse nothing.
|
|
65
65
|
--human FILE: answers for ask steps, as {steps: {"STEP": {outputs: {...}}}} (STEP:ITERATION, counted from 0, for a step with each or repeat).
|
|
66
66
|
--verbose: print runtime events.
|
|
67
|
-
|
|
67
|
+
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).
|
|
68
|
+
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.
|
|
69
|
+
Then 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.
|
|
70
|
+
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.
|
|
71
|
+
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.
|
|
72
|
+
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
73
|
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
74
|
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
75
|
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" },
|
|
71
76
|
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.'" },
|
|
77
|
+
"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
78
|
"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
79
|
"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
80
|
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" },
|
|
@@ -336,7 +341,7 @@ To fix a failed or wrong step, change it and run the Method again. The new run r
|
|
|
336
341
|
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
342
|
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
343
|
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.
|
|
344
|
+
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
345
|
Use method sync RUN_DIRECTORY to repair a dashboard upload without executing the method again.
|
|
341
346
|
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
347
|
`;
|
|
@@ -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 } 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";
|
|
@@ -24,6 +24,18 @@ import { methodHelp, overviewHelp as help } from "./method-help.js";
|
|
|
24
24
|
function safePath(path) {
|
|
25
25
|
return authoringPath(path);
|
|
26
26
|
}
|
|
27
|
+
/** A run target that can save a version: a saved Method (ID or link), or a Method file that loads. */
|
|
28
|
+
function loads(target) {
|
|
29
|
+
if (!target || !existsSync(target))
|
|
30
|
+
return true;
|
|
31
|
+
try {
|
|
32
|
+
loadWorkflow(methodContent(readDocument(target)));
|
|
33
|
+
return true;
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
27
39
|
async function dispatchRunWorker(args, flags, extra = [], savedClient) {
|
|
28
40
|
if (process.env.METHOD_RUN_WORKER === '1' || (!/[/\\]method\.(?:js|ts)$/.test(process.argv[1] ?? '') && !flags.background))
|
|
29
41
|
return false;
|
|
@@ -44,14 +56,87 @@ async function dispatchRunWorker(args, flags, extra = [], savedClient) {
|
|
|
44
56
|
const resolved = await withInputsFrom(flags, directory, /\.method$/i.test(args[1] ?? '') ? args[1] : undefined, client);
|
|
45
57
|
workerArgs = [...withoutOptions(workerArgs, ['inputs-from', 'inputs']), '--inputs', resolved.inputs];
|
|
46
58
|
}
|
|
47
|
-
// A new run of a signed-in computer is synced; a resumed run keeps the link that it has.
|
|
48
|
-
|
|
59
|
+
// A new run of a signed-in computer is synced; a resumed run keeps the link that it has. A Method file that does not
|
|
60
|
+
// load saves no version, so its run sends no record and gets no link.
|
|
61
|
+
const server = !flags.resume && client?.token() && loads(args[1]) ? client.server : undefined;
|
|
49
62
|
await startRunWorker(workerArgs, directory, flags.background, server);
|
|
50
63
|
return true;
|
|
51
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 file takes the newest version of its Method, corrections that wait
|
|
103
|
+
* are fixed, 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
|
+
const caught = await catchUpFile(client, file);
|
|
120
|
+
if (caught === 'conflict')
|
|
121
|
+
return false;
|
|
122
|
+
await fixWaitingCorrections(args, clientFactory);
|
|
123
|
+
const version = client && caught !== 'offline' ? await (await import('./versions.js')).currentVersion(file).catch(() => undefined) : undefined;
|
|
124
|
+
if (version) {
|
|
125
|
+
let flags;
|
|
126
|
+
try {
|
|
127
|
+
flags = parse(args.slice(2)).values;
|
|
128
|
+
}
|
|
129
|
+
catch {
|
|
130
|
+
return true;
|
|
131
|
+
}
|
|
132
|
+
await sameRunLine(client, version.method_id, version.version_id, flags, readDocument(file).run_data === 'device');
|
|
133
|
+
}
|
|
134
|
+
return true;
|
|
135
|
+
}
|
|
52
136
|
/**
|
|
53
137
|
* Before method run FILE: corrections that wait for this Method (from the dashboard, or a loop that stopped) are fixed
|
|
54
|
-
* first, with one line about them.
|
|
138
|
+
* first, with one line about them. When a fix of the Method already runs on this computer, the run does not wait: that
|
|
139
|
+
* fix takes the waiting corrections. A failure of the loop is one line too; the run goes on. Signed out, offline, a
|
|
55
140
|
* resumed run, or the run worker: nothing.
|
|
56
141
|
*/
|
|
57
142
|
async function fixWaitingCorrections(args, clientFactory) {
|
|
@@ -67,10 +152,9 @@ async function fixWaitingCorrections(args, clientFactory) {
|
|
|
67
152
|
catch {
|
|
68
153
|
return;
|
|
69
154
|
}
|
|
70
|
-
const {
|
|
155
|
+
const { fixWaiting, waitingLine } = await import('./improve.js');
|
|
71
156
|
try {
|
|
72
|
-
const fix
|
|
73
|
-
if (fix)
|
|
157
|
+
for (const fix of await fixWaiting(client, authoringPath(args[1]), { waitMs: 3000 }) ?? [])
|
|
74
158
|
process.stderr.write(waitingLine(fix) + '\n');
|
|
75
159
|
}
|
|
76
160
|
catch (error) {
|
|
@@ -78,6 +162,27 @@ async function fixWaitingCorrections(args, clientFactory) {
|
|
|
78
162
|
process.stderr.write(`The waiting corrections are not fixed yet: ${error.message}\n`);
|
|
79
163
|
}
|
|
80
164
|
}
|
|
165
|
+
/**
|
|
166
|
+
* The ID of the workspace's Method with this name (case-insensitive, archived Methods left out), or null when none has
|
|
167
|
+
* it. Several matches list their names and IDs and run nothing (exit code 2): then undefined.
|
|
168
|
+
*/
|
|
169
|
+
async function methodByName(client, name) {
|
|
170
|
+
const found = [];
|
|
171
|
+
for (let cursor = null, first = true; first || cursor; first = false) {
|
|
172
|
+
const page = await client.request(`/api/methods?limit=500${cursor ? `&cursor=${encodeURIComponent(cursor)}` : ''}`);
|
|
173
|
+
found.push(...(page.workflows ?? []).filter(w => !w.archived_at && typeof w.name === 'string' && w.name.trim().toLowerCase() === name.trim().toLowerCase()));
|
|
174
|
+
cursor = page.next_cursor ?? null;
|
|
175
|
+
}
|
|
176
|
+
if (found.length === 1)
|
|
177
|
+
return found[0].id;
|
|
178
|
+
if (!found.length)
|
|
179
|
+
return null;
|
|
180
|
+
const message = `${found.length} Methods are named "${name}". Run one by its ID: ${found.map(w => `method run ${w.id} (${w.name})`).join(', ')}.`;
|
|
181
|
+
process.stderr.write(message + '\n');
|
|
182
|
+
process.stdout.write(JSON.stringify({ status: 'needs_you', message, matches: found.map(w => ({ name: w.name, id: w.id })) }, null, 2) + '\n');
|
|
183
|
+
process.exitCode = 2;
|
|
184
|
+
return undefined;
|
|
185
|
+
}
|
|
81
186
|
export async function methodMain(args = process.argv.slice(2), clientFactory = server => new MethodClient(server)) {
|
|
82
187
|
const resumed = (await import('./resume-release.js')).resumeWithOriginalRelease(args);
|
|
83
188
|
if (resumed !== undefined) {
|
|
@@ -95,9 +200,8 @@ export async function methodMain(args = process.argv.slice(2), clientFactory = s
|
|
|
95
200
|
return (await import('./judge.js')).judgeCommand(args.slice(1), clientFactory);
|
|
96
201
|
if (args[0] === 'changes')
|
|
97
202
|
return (await import('./proposals.js')).changesMain(args.slice(1), clientFactory);
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
await fixWaitingCorrections(args, clientFactory);
|
|
203
|
+
if (args[0] === 'run' && /\.method$/i.test(args[1] ?? '') && !args.includes('--help') && !await beforeFileRun(args, clientFactory))
|
|
204
|
+
return;
|
|
101
205
|
if (args[0] === 'worker' || args[0] === 'keys')
|
|
102
206
|
return (await import('./worker.js')).productionCommand(args, clientFactory);
|
|
103
207
|
if (args[0] === '__run-data')
|
|
@@ -217,7 +321,8 @@ export async function methodMain(args = process.argv.slice(2), clientFactory = s
|
|
|
217
321
|
else
|
|
218
322
|
rest.push(args[i]);
|
|
219
323
|
}
|
|
220
|
-
const [command,
|
|
324
|
+
const [command, , step] = rest;
|
|
325
|
+
let target = rest[1];
|
|
221
326
|
if (!command || command === "--help" || command === "help") {
|
|
222
327
|
process.stdout.write(help);
|
|
223
328
|
return;
|
|
@@ -348,16 +453,36 @@ export async function methodMain(args = process.argv.slice(2), clientFactory = s
|
|
|
348
453
|
if (command === "publish") {
|
|
349
454
|
if (!authorFile)
|
|
350
455
|
throw Error("Use method publish FILE [--reason TEXT] [--cloud|--workers] [--env NAME].");
|
|
456
|
+
if (await catchUpFile(client, safePath(authorFile), 'publishing') === 'conflict')
|
|
457
|
+
return;
|
|
351
458
|
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))));
|
|
352
459
|
return;
|
|
353
460
|
}
|
|
354
461
|
if (!target)
|
|
355
462
|
throw Error(help);
|
|
463
|
+
// method run NAME: the Method of the workspace with this name, run as method run ID runs it.
|
|
464
|
+
let named;
|
|
465
|
+
if (command === "run" && !methodIdPattern.test(target) && !existsSync(target)) {
|
|
466
|
+
const found = await methodByName(client, target);
|
|
467
|
+
if (found === undefined)
|
|
468
|
+
return;
|
|
469
|
+
if (found) {
|
|
470
|
+
const at = args.indexOf(target);
|
|
471
|
+
args = [...args.slice(0, at), found, ...args.slice(at + 1)];
|
|
472
|
+
target = found;
|
|
473
|
+
}
|
|
474
|
+
else
|
|
475
|
+
named = target;
|
|
476
|
+
}
|
|
356
477
|
const path = `/api/cli/methods/${encodeURIComponent(target)}`;
|
|
357
478
|
if (!["get", "steps", "step", "run"].includes(command))
|
|
358
479
|
throw Error(help);
|
|
359
480
|
const saved = await client.request(path +
|
|
360
|
-
(values.version ? `?version=${encodeURIComponent(values.version)}` : ""))
|
|
481
|
+
(values.version ? `?version=${encodeURIComponent(values.version)}` : "")).catch(error => {
|
|
482
|
+
if (named && /^40[34]:/.test(error.message))
|
|
483
|
+
throw Error(`No Method named "${named}" in your workspace, and no file or Method ID ${named}. Run method list to see the names.`);
|
|
484
|
+
throw error;
|
|
485
|
+
});
|
|
361
486
|
const workflow = command === "get" ? saved.workflow : loadWorkflow(saved.workflow);
|
|
362
487
|
if (command === "steps") {
|
|
363
488
|
print({ version_id: saved.version_id, steps: workflow.steps });
|
|
@@ -386,12 +511,11 @@ export async function methodMain(args = process.argv.slice(2), clientFactory = s
|
|
|
386
511
|
// An older saved version carries runtime.json; the file gets it in the Method, as in format 3.4.
|
|
387
512
|
(await import("./legacy.js")).convertLegacyOrThrow(out);
|
|
388
513
|
writeMethodId(out, saved.workflow_id);
|
|
389
|
-
(await import("./outbox.js")).recordCheckout(client.server, saved.workflow_id, saved.version_id, saved.version_number, methodContent(workflow));
|
|
514
|
+
(await import("./outbox.js")).recordCheckout(client.server, saved.workflow_id, saved.version_id, saved.version_number, methodContent(workflow), { saved_by: saved.saved_by ?? null });
|
|
390
515
|
// The new file becomes the Method's home on this computer unless another file already is.
|
|
391
516
|
const home = await import("./home.js");
|
|
392
517
|
try {
|
|
393
518
|
home.claimHome(client.server, saved.workflow_id, out);
|
|
394
|
-
home.recordHome(client.server, saved.workflow_id, out, { version_id: saved.version_id, files: home.writtenFiles(out, saved.package?.files ?? []) });
|
|
395
519
|
}
|
|
396
520
|
catch { /* a copy for reading */ }
|
|
397
521
|
print({
|
|
@@ -406,6 +530,7 @@ export async function methodMain(args = process.argv.slice(2), clientFactory = s
|
|
|
406
530
|
return;
|
|
407
531
|
}
|
|
408
532
|
const flags = parse(rest.slice(2)).values;
|
|
533
|
+
await sameRunLine(client, saved.workflow_id, saved.version_id, flags, saved.workflow?.run_data === 'device');
|
|
409
534
|
if (await dispatchRunWorker(args, flags, values.version ? [] : ['--version', saved.version_id], client))
|
|
410
535
|
return;
|
|
411
536
|
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 {
|
|
@@ -32,13 +32,20 @@ export function recordKnownVersion(server, methodId, versionId, versionNumber) {
|
|
|
32
32
|
writePrivateJson(savedFile(server), { ...savedVersions(server), [versionId]: { method_id: methodId, version_number: versionNumber } });
|
|
33
33
|
}
|
|
34
34
|
/** A checkout of a saved version: the next save of this Method on this computer has it as its parent. */
|
|
35
|
-
export function recordCheckout(server, methodId, versionId, versionNumber, workflow) {
|
|
35
|
+
export function recordCheckout(server, methodId, versionId, versionNumber, workflow, extra = {}) {
|
|
36
36
|
recordKnownVersion(server, methodId, versionId, versionNumber);
|
|
37
|
-
writePrivateJson(methodFile(server, methodId), { version_id: versionId, version_number: versionNumber, workflow });
|
|
37
|
+
writePrivateJson(methodFile(server, methodId), { version_id: versionId, version_number: versionNumber, workflow, ...extra });
|
|
38
38
|
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
writePrivateJson(methodFile(
|
|
39
|
+
/** A combine that stopped: the parts that both sides changed, as they were on this computer. */
|
|
40
|
+
export function recordCombineStop(server, methodId, conflict) {
|
|
41
|
+
writePrivateJson(methodFile(server, methodId), { ...lastSaved(server, methodId), conflict });
|
|
42
|
+
}
|
|
43
|
+
/** A save that the server kept beside a newer version stays off the base: the next run combines it with the newer one. */
|
|
44
|
+
function recordSaved(item, saved) {
|
|
45
|
+
const beside = saved.newest === false;
|
|
46
|
+
writePrivateJson(savedFile(item.server), { ...savedVersions(item.server), [item.version_id]: { method_id: item.method_id, version_number: saved.version_number, ...(beside ? { beside: saved.newer?.saved_by ?? null } : {}) } });
|
|
47
|
+
writePrivateJson(methodFile(item.server, item.method_id), beside ? { ...lastSaved(item.server, item.method_id), side_version_id: item.version_id }
|
|
48
|
+
: { version_id: item.version_id, version_number: saved.version_number, workflow: item.body.workflow });
|
|
42
49
|
}
|
|
43
50
|
/** Errors that a retry can fix: no connection, a stalled transfer, a server error, or too many requests. */
|
|
44
51
|
export function retryable(error) {
|
|
@@ -274,7 +281,7 @@ export class Outbox {
|
|
|
274
281
|
const saved = await this.client.request(`/api/cli/methods/${encodeURIComponent(item.method_id)}/versions/${encodeURIComponent(item.version_id)}`, 'PUT', item.body, true, { stallMs, onProgress });
|
|
275
282
|
if (saved.version_id !== item.version_id)
|
|
276
283
|
throw Object.assign(Error('The server saved another version ID than this content has.'), { status: 409 });
|
|
277
|
-
recordSaved(item, saved
|
|
284
|
+
recordSaved(item, saved);
|
|
278
285
|
return;
|
|
279
286
|
}
|
|
280
287
|
if (!existsSync(join(item.run_dir, 'method-sync.json')))
|
|
@@ -12,27 +12,6 @@ export type ProposalRecord = {
|
|
|
12
12
|
base_workflow: unknown;
|
|
13
13
|
proposed_workflow: unknown | null;
|
|
14
14
|
};
|
|
15
|
-
export type MergeConflict = {
|
|
16
|
-
path: string;
|
|
17
|
-
base: unknown;
|
|
18
|
-
ours: unknown;
|
|
19
|
-
theirs: unknown;
|
|
20
|
-
};
|
|
21
|
-
export type MergeResult = {
|
|
22
|
-
clean: true;
|
|
23
|
-
workflow: any;
|
|
24
|
-
} | {
|
|
25
|
-
clean: false;
|
|
26
|
-
conflicts: MergeConflict[];
|
|
27
|
-
};
|
|
28
|
-
/**
|
|
29
|
-
* Three-way merge of Method documents, per step and per top-level key. base is the version the proposal started from,
|
|
30
|
-
* ours is the local file, and theirs is the proposed version. A part that only one side changed takes that change; a part
|
|
31
|
-
* that both sides changed in different ways is a conflict.
|
|
32
|
-
*/
|
|
33
|
-
export declare function mergeMethod(base: any, ours: any, theirs: any): MergeResult;
|
|
34
|
-
/** The steps whose definition differs between two documents. */
|
|
35
|
-
export declare function changedSteps(before: any, after: any): string[];
|
|
36
15
|
/** Write Method content to a file and keep its id: line. A JSON file stays JSON. */
|
|
37
16
|
export declare function writeMethodFile(file: string, id: string | undefined, content: any): void;
|
|
38
17
|
export declare const localProposalFolder: (file: string) => string;
|
|
@@ -41,13 +20,6 @@ export declare function saveLocalProposal(file: string, record: Omit<ProposalRec
|
|
|
41
20
|
export declare function localProposals(file: string): (ProposalRecord & {
|
|
42
21
|
path: string;
|
|
43
22
|
})[];
|
|
44
|
-
/**
|
|
45
|
-
* Before a command that takes a .method FILE: when this computer is signed in and Method changed the Method in the
|
|
46
|
-
* account (keeping run data in the account, or a fix of a Method that had not run on a computer), merge the change into
|
|
47
|
-
* the file and print one line. A conflict leaves the file unchanged and writes both versions of each conflicting part
|
|
48
|
-
* to a file for the coding agent to merge; the command goes on. Offline, signed out, or on any error, it does nothing.
|
|
49
|
-
*/
|
|
50
|
-
export declare function autoApply(args: string[], clientFactory: (server: string) => MethodClient): Promise<void>;
|
|
51
23
|
/** Run folders of this Method on this computer, newest first, with the account run ID of each run that was saved to the account. */
|
|
52
24
|
export declare function localRuns(file: string): Promise<{
|
|
53
25
|
dir: string;
|