@method-ai-hq/sdk 0.19.0 → 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.
- package/README.md +1 -1
- package/dist/packages/sdk/package.json +2 -2
- package/dist/packages/sdk/src/authoring-instructions.d.ts +1 -1
- package/dist/packages/sdk/src/authoring-instructions.js +1 -1
- package/dist/packages/sdk/src/current-inspection.js +3 -0
- package/dist/packages/sdk/src/fix-lines.d.ts +82 -0
- package/dist/packages/sdk/src/fix-lines.js +63 -0
- package/dist/packages/sdk/src/fix-outcomes.d.ts +72 -0
- package/dist/packages/sdk/src/fix-outcomes.js +201 -0
- package/dist/packages/sdk/src/improve.d.ts +131 -34
- package/dist/packages/sdk/src/improve.js +489 -175
- package/dist/packages/sdk/src/judge.d.ts +12 -11
- package/dist/packages/sdk/src/judge.js +63 -39
- package/dist/packages/sdk/src/learned.d.ts +43 -0
- package/dist/packages/sdk/src/learned.js +97 -0
- package/dist/packages/sdk/src/local-cli.js +8 -1
- package/dist/packages/sdk/src/local-judgments.d.ts +2 -0
- package/dist/packages/sdk/src/method-files.d.ts +3 -2
- package/dist/packages/sdk/src/method-files.js +7 -3
- package/dist/packages/sdk/src/method-help.d.ts +1 -1
- package/dist/packages/sdk/src/method-help.js +16 -12
- package/dist/packages/sdk/src/method.js +51 -9
- package/dist/packages/sdk/src/quality.d.ts +62 -11
- package/dist/packages/sdk/src/quality.js +56 -21
- package/dist/packages/sdk/src/undo.d.ts +50 -0
- package/dist/packages/sdk/src/undo.js +108 -0
- package/dist/packages/sdk/src/watch.d.ts +20 -0
- package/dist/packages/sdk/src/watch.js +86 -0
- package/dist/packages/workflow-language/src/inspection.d.ts +30 -0
- package/dist/packages/workflow-language/src/inspection.js +3 -1
- package/package.json +2 -2
|
@@ -13,7 +13,7 @@ export const commandHelp = {
|
|
|
13
13
|
"new-id": { usage: "method new-id FILE", purpose: "Make a copied Method file a new Method.", arguments: "Writes a new id: line. A copy that keeps its id: saves to the same Method.", result: "JSON with file and method_id.", errors: "Missing file.", example: "method new-id copy.method" },
|
|
14
14
|
bind: { usage: "method bind FILE_OR_ID NAME (--file FOLDER | --connection URL) [--upload]", purpose: "Save a named input location or connection.", arguments: "Default: this computer only, in ~/.config/method/computer.json under the Method's ID (a FILE gets its id: line). A folder named NAME beside the Method needs no binding. --upload saves only the selected input folder or connection URL in the account. It never scans your disk. Keep credentials in the service's normal sign-in store.", result: "Saved binding and scope.", errors: "Missing input, invalid name, or unsafe path.", example: "method bind METHOD_ID prepared_day --file day-records --upload", remote: true },
|
|
15
15
|
state: { usage: "method state ID [--enable --file state.json | --release RUN_ID]", purpose: "Read or explicitly enable shared account state.", arguments: "Accepted state updates use revision checks. Inspect a stopped run's external actions before releasing its ownership.", result: "Current revision, value, and owner run.", errors: "Concurrent ownership or stale revision. Existing state is never silently overwritten.", example: "method state METHOD_ID --enable --file initial-state.json", remote: true },
|
|
16
|
-
wait: { usage: "method wait RUN_DIRECTORY", purpose: "Reconnect to
|
|
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]\nmethod judge RUN_ID keep JUDGMENT_ID", purpose: "Record the person's reaction to a run's result. A wrong one
|
|
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 },
|
|
40
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: "
|
|
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 },
|
|
@@ -66,13 +68,13 @@ export const commandHelp = {
|
|
|
66
68
|
--verbose: print runtime events.
|
|
67
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).
|
|
68
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.
|
|
69
|
-
Then corrections of the Method that wait (from the dashboard, or a fix that stopped)
|
|
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.
|
|
70
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.
|
|
71
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.
|
|
72
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.
|
|
73
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 },
|
|
74
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" },
|
|
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
|
|
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" },
|
|
76
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" },
|
|
77
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.'" },
|
|
78
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.'" },
|
|
@@ -303,12 +305,14 @@ const recipes = `# Recipes
|
|
|
303
305
|
## Corrections become cases
|
|
304
306
|
|
|
305
307
|
When the user says a result was wrong:
|
|
306
|
-
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
|
|
307
|
-
2.
|
|
308
|
-
3.
|
|
309
|
-
4.
|
|
310
|
-
5.
|
|
311
|
-
6.
|
|
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.
|
|
312
316
|
For a one-time preference, edit and run again; a case is not needed.
|
|
313
317
|
|
|
314
318
|
## Checked on every run, or on every new version
|
|
@@ -376,7 +380,7 @@ Learn: method authoring Start the installed guide.
|
|
|
376
380
|
Local drafts: init, show, set, remove, step add/update/remove/move,
|
|
377
381
|
check set/remove, validate, diff.
|
|
378
382
|
Online documents: list, get, steps, step, publish, explain.
|
|
379
|
-
Results and improvements: judge, changes.
|
|
383
|
+
Results and improvements: judge, wait, answer, learned, undo, changes.
|
|
380
384
|
Keys for scripts: secret find, secret import, secret set, secret list.
|
|
381
385
|
Execution and evidence: run, run-status, wait, cancel, runs, logs, sync, observe.
|
|
382
386
|
Effects and corrections: effect add/list, case new/retire/list, test.
|
|
@@ -99,8 +99,8 @@ async function sameRunLine(client, methodId, versionId, flags, device) {
|
|
|
99
99
|
process.stderr.write(line + '\n');
|
|
100
100
|
}
|
|
101
101
|
/**
|
|
102
|
-
* Before method run FILE in the parent process: the
|
|
103
|
-
*
|
|
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
104
|
*/
|
|
105
105
|
async function beforeFileRun(args, clientFactory) {
|
|
106
106
|
if (process.env.METHOD_RUN_WORKER === '1' || args.includes('--resume') || !existsSync(args[1]))
|
|
@@ -116,6 +116,16 @@ async function beforeFileRun(args, clientFactory) {
|
|
|
116
116
|
client = undefined;
|
|
117
117
|
}
|
|
118
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');
|
|
119
129
|
const caught = await catchUpFile(client, file);
|
|
120
130
|
if (caught === 'conflict')
|
|
121
131
|
return false;
|
|
@@ -134,14 +144,22 @@ async function beforeFileRun(args, clientFactory) {
|
|
|
134
144
|
return true;
|
|
135
145
|
}
|
|
136
146
|
/**
|
|
137
|
-
* Before method run FILE: corrections that wait for this Method (from the dashboard,
|
|
138
|
-
*
|
|
139
|
-
*
|
|
140
|
-
* 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.
|
|
141
150
|
*/
|
|
142
151
|
async function fixWaitingCorrections(args, clientFactory) {
|
|
143
152
|
if (process.env.METHOD_RUN_WORKER === '1' || args.includes('--resume') || !existsSync(args[1]))
|
|
144
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');
|
|
145
163
|
const server = parseArgs({ args: args.slice(2), strict: false, options: { server: { type: 'string' } } }).values.server;
|
|
146
164
|
let client;
|
|
147
165
|
try {
|
|
@@ -152,10 +170,17 @@ async function fixWaitingCorrections(args, clientFactory) {
|
|
|
152
170
|
catch {
|
|
153
171
|
return;
|
|
154
172
|
}
|
|
155
|
-
const {
|
|
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;
|
|
156
177
|
try {
|
|
157
|
-
|
|
158
|
-
|
|
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');
|
|
159
184
|
}
|
|
160
185
|
catch (error) {
|
|
161
186
|
if (!/^(fetch failed|401:|404:)|abort|timed? ?out/i.test(error.message))
|
|
@@ -198,6 +223,23 @@ export async function methodMain(args = process.argv.slice(2), clientFactory = s
|
|
|
198
223
|
}
|
|
199
224
|
if (args[0] === 'judge')
|
|
200
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
|
+
}
|
|
201
243
|
if (args[0] === 'changes')
|
|
202
244
|
return (await import('./proposals.js')).changesMain(args.slice(1), clientFactory);
|
|
203
245
|
if (args[0] === 'run' && /\.method$/i.test(args[1] ?? '') && !args.includes('--help') && !await beforeFileRun(args, clientFactory))
|
|
@@ -17,26 +17,77 @@ export declare const defaultRunRoots: () => string[];
|
|
|
17
17
|
export declare function observeCommand(args: string[]): Promise<void>;
|
|
18
18
|
/** A fingerprint of a Method and the files it runs, to tell whether its cases were checked since the last change. */
|
|
19
19
|
export declare function methodFingerprint(file: string): string;
|
|
20
|
-
/**
|
|
21
|
-
export declare const caseName: (c: {
|
|
22
|
-
id: string;
|
|
23
|
-
author?: string | null;
|
|
24
|
-
note?: unknown;
|
|
25
|
-
}) => string;
|
|
26
|
-
/** One line for method run when the Method changed since its cases last passed. Cases with an author are named. */
|
|
20
|
+
/** One line for method run when the Method changed since its earlier corrections last passed. Their authors are named. */
|
|
27
21
|
export declare function casesNotice(file: string): Promise<string | undefined>;
|
|
22
|
+
/**
|
|
23
|
+
* The judge profiles for a rubric: the configured judge, and when it is the user's own Claude Code or Codex and the
|
|
24
|
+
* other one is installed and signed in too, that one as well. A judge that the Method names is the only judge.
|
|
25
|
+
*/
|
|
26
|
+
export declare function judgeProfiles(config: any): Promise<any[]>;
|
|
27
|
+
/**
|
|
28
|
+
* Evaluate a case's expectations with every judge profile (the runtime's judges option). A criterion passes only when
|
|
29
|
+
* every judge passes it; when the judges disagree it fails as unsure.
|
|
30
|
+
*/
|
|
31
|
+
export declare function evaluateWithJudges(expect: any[], outcome: any, context: {
|
|
32
|
+
caseDir: string;
|
|
33
|
+
config: any;
|
|
34
|
+
options?: any;
|
|
35
|
+
}, profiles: any[]): Promise<{
|
|
36
|
+
status: string;
|
|
37
|
+
results: {
|
|
38
|
+
criteria?: ({
|
|
39
|
+
id: string;
|
|
40
|
+
text: string;
|
|
41
|
+
judge: string;
|
|
42
|
+
probability: any;
|
|
43
|
+
pass: boolean;
|
|
44
|
+
reason: string;
|
|
45
|
+
votes?: undefined;
|
|
46
|
+
unsure?: undefined;
|
|
47
|
+
} | {
|
|
48
|
+
id: string;
|
|
49
|
+
text: string;
|
|
50
|
+
judge: string;
|
|
51
|
+
votes: any[];
|
|
52
|
+
pass: boolean;
|
|
53
|
+
unsure: boolean;
|
|
54
|
+
reason: string;
|
|
55
|
+
probability?: undefined;
|
|
56
|
+
} | {
|
|
57
|
+
id: string;
|
|
58
|
+
text: string;
|
|
59
|
+
judge: string;
|
|
60
|
+
votes: any[];
|
|
61
|
+
pass: boolean;
|
|
62
|
+
reason: any;
|
|
63
|
+
probability?: undefined;
|
|
64
|
+
unsure?: undefined;
|
|
65
|
+
})[];
|
|
66
|
+
judge_calls?: number;
|
|
67
|
+
kind: any;
|
|
68
|
+
status: string;
|
|
69
|
+
reason: string;
|
|
70
|
+
text?: any;
|
|
71
|
+
}[];
|
|
72
|
+
}>;
|
|
28
73
|
/** The name of the signed-in person, for the author of a new case; null when signed out or offline. Asked once per sign-in in a process. */
|
|
29
74
|
export declare function signedInName(client?: Pick<MethodClient, "server" | "request" | "token">): Promise<string | null>;
|
|
30
75
|
/** Run the cases of a Method with the same preparation as method run. */
|
|
31
|
-
export
|
|
32
|
-
workspace?: string;
|
|
33
|
-
}, options?: {
|
|
76
|
+
export type CaseRunOptions = {
|
|
34
77
|
casesDir?: string | undefined;
|
|
35
78
|
ids?: string[] | undefined;
|
|
36
79
|
baseline?: string | undefined;
|
|
37
80
|
newIds?: string[];
|
|
38
81
|
once?: boolean;
|
|
39
|
-
|
|
82
|
+
runs?: number | undefined;
|
|
83
|
+
minPass?: number | undefined;
|
|
84
|
+
live?: string[] | undefined;
|
|
85
|
+
judgeCache?: boolean | undefined;
|
|
86
|
+
keepDir?: string | undefined;
|
|
87
|
+
};
|
|
88
|
+
export declare function runCases(file: string, flags: {
|
|
89
|
+
workspace?: string;
|
|
90
|
+
}, options?: CaseRunOptions): Promise<{
|
|
40
91
|
counts: {
|
|
41
92
|
[k: string]: number;
|
|
42
93
|
};
|
|
@@ -1,16 +1,17 @@
|
|
|
1
1
|
/** Effect observation, recorded cases, the save gate, and library observers. */
|
|
2
2
|
import { cpSync, existsSync, mkdirSync, readFileSync, readdirSync } from "node:fs";
|
|
3
|
-
import { dirname, join, relative, resolve } from "node:path";
|
|
3
|
+
import { basename, dirname, join, relative, resolve } from "node:path";
|
|
4
4
|
import { execFileSync } from "node:child_process";
|
|
5
5
|
import { fileURLToPath } from "node:url";
|
|
6
6
|
import { createHash } from "node:crypto";
|
|
7
7
|
import { parseArgs } from "node:util";
|
|
8
8
|
import { observeRun, pendingRuns } from "@withmethod/runtime/observe.js";
|
|
9
|
-
import { createCase, retireCase, listCases, testSuite } from "@withmethod/runtime/cases.js";
|
|
9
|
+
import { createCase, retireCase, listCases, testSuite, evaluate } from "@withmethod/runtime/cases.js";
|
|
10
10
|
import { authoringPath, editDocument, readDocument } from "./authoring.js";
|
|
11
11
|
import { localSetup } from "./local-setup.js";
|
|
12
12
|
import { resolveAgentProfiles, checkAgents, accountNeeds } from "./capabilities.js";
|
|
13
|
-
import { localAgent, resolveModels } from "@withmethod/runtime/agents.js";
|
|
13
|
+
import { agentSignedIn, localAgent, resolveModels } from "@withmethod/runtime/agents.js";
|
|
14
|
+
import { executable } from "@withmethod/runtime/io.js";
|
|
14
15
|
import { hostedModelsFor, managedModels } from "./hosted-models.js";
|
|
15
16
|
import { managedClassification } from "./classification-client.js";
|
|
16
17
|
import { MethodClient } from "./method-client.js";
|
|
@@ -58,6 +59,8 @@ export async function preparedConfig(file, flags, options = {}) {
|
|
|
58
59
|
config.classification = await classification.resolve(new AbortController().signal);
|
|
59
60
|
await checkAgents(config.models);
|
|
60
61
|
const prepared = await prepareRuntime(setup.sourceRoot, config, method);
|
|
62
|
+
// A case's check script runs on Node, which the CLI itself runs on.
|
|
63
|
+
prepared.config.runtimes = { node: { command: process.execPath, version: process.version }, ...prepared.config.runtimes };
|
|
61
64
|
const hostedModels = Object.values(prepared.config.models ?? {}).some((profile) => profile.backend === 'method') ? await hostedModelsFor(client) : undefined;
|
|
62
65
|
return { config: prepared.config, sourceRoot: setup.sourceRoot, processPath: prepared.processPath, prepareBundle: prepared.prepareBundle, ...(hostedModels ? { hostedModels } : {}), ...(classification ? { classification } : {}) };
|
|
63
66
|
}
|
|
@@ -100,7 +103,7 @@ export async function observeCommand(args) {
|
|
|
100
103
|
export function methodFingerprint(file) {
|
|
101
104
|
const folder = dirname(file), method = readDocument(file);
|
|
102
105
|
const parts = [readFileSync(file, "utf8")];
|
|
103
|
-
const paths = new Set(method.files ?? []);
|
|
106
|
+
const paths = new Set([...(method.files ?? []), ...(existsSync(join(folder, "learned.json")) ? ["learned.json"] : [])]);
|
|
104
107
|
for (const step of Object.values(method.steps ?? {})) {
|
|
105
108
|
for (const exec of [step.do, step.check])
|
|
106
109
|
if (exec?.kind === "run")
|
|
@@ -125,9 +128,7 @@ function recordChecked(file, report) {
|
|
|
125
128
|
mkdirSync(dirname(checkedFile(file)), { recursive: true, mode: 0o700 });
|
|
126
129
|
writePrivateJson(checkedFile(file), { file, fingerprint: methodFingerprint(file), passed: report.passed, cases: report.cases.length, at: new Date().toISOString() });
|
|
127
130
|
}
|
|
128
|
-
/**
|
|
129
|
-
export const caseName = (c) => `${c.author}'s case ${c.id}: ${String(c.note ?? "").slice(0, 200).replace(/[.\s]+$/, "")}`;
|
|
130
|
-
/** One line for method run when the Method changed since its cases last passed. Cases with an author are named. */
|
|
131
|
+
/** One line for method run when the Method changed since its earlier corrections last passed. Their authors are named. */
|
|
131
132
|
export async function casesNotice(file) {
|
|
132
133
|
const cases = (await listCases(file)).filter((c) => c.status === "active");
|
|
133
134
|
if (!cases.length)
|
|
@@ -139,8 +140,34 @@ export async function casesNotice(file) {
|
|
|
139
140
|
catch { /* never checked */ }
|
|
140
141
|
if (checked?.passed && checked.fingerprint === methodFingerprint(file))
|
|
141
142
|
return undefined;
|
|
142
|
-
const
|
|
143
|
-
return
|
|
143
|
+
const { uncheckedLine, correctionName } = await import("./fix-lines.js");
|
|
144
|
+
return uncheckedLine(cases.length, cases.filter((c) => c.author).map(correctionName), `method test ${basename(file)}`);
|
|
145
|
+
}
|
|
146
|
+
/**
|
|
147
|
+
* The judge profiles for a rubric: the configured judge, and when it is the user's own Claude Code or Codex and the
|
|
148
|
+
* other one is installed and signed in too, that one as well. A judge that the Method names is the only judge.
|
|
149
|
+
*/
|
|
150
|
+
export async function judgeProfiles(config) {
|
|
151
|
+
const first = config?.models?.judge;
|
|
152
|
+
if (!first)
|
|
153
|
+
return [];
|
|
154
|
+
if (!["claude", "codex"].includes(first.backend) || first.command || first.model)
|
|
155
|
+
return [first];
|
|
156
|
+
const other = first.backend === "claude" ? "codex" : "claude";
|
|
157
|
+
try {
|
|
158
|
+
await executable(other);
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
return [first];
|
|
162
|
+
}
|
|
163
|
+
return await agentSignedIn(other) ? [first, { backend: other }] : [first];
|
|
164
|
+
}
|
|
165
|
+
/**
|
|
166
|
+
* Evaluate a case's expectations with every judge profile (the runtime's judges option). A criterion passes only when
|
|
167
|
+
* every judge passes it; when the judges disagree it fails as unsure.
|
|
168
|
+
*/
|
|
169
|
+
export async function evaluateWithJudges(expect, outcome, context, profiles) {
|
|
170
|
+
return evaluate(expect, outcome, profiles.length < 2 ? context : { ...context, options: { ...context.options, judges: profiles } });
|
|
144
171
|
}
|
|
145
172
|
const signedIn = new Map();
|
|
146
173
|
/** The name of the signed-in person, for the author of a new case; null when signed out or offline. Asked once per sign-in in a process. */
|
|
@@ -172,22 +199,30 @@ async function declaredSecrets(file) {
|
|
|
172
199
|
const { resolveSecrets } = await import("./secrets.js");
|
|
173
200
|
return resolveSecrets(Object.keys(readDocument(file)?.secrets ?? {}));
|
|
174
201
|
}
|
|
175
|
-
/** Run the cases of a Method with the same preparation as method run. */
|
|
176
202
|
export async function runCases(file, flags, options = {}) {
|
|
177
203
|
const prepared = await preparedConfig(file, flags, { judge: true });
|
|
178
|
-
|
|
204
|
+
// Claude Code and Codex both judge when both are signed in; a disagreement fails the criterion as unsure.
|
|
205
|
+
const judges = await judgeProfiles(prepared.config);
|
|
206
|
+
const report = await testSuite(file, prepared.config, { ...options, ...(judges.length > 1 ? { judges } : {}),
|
|
179
207
|
runOptions: { processPath: prepared.processPath, prepareBundle: prepared.prepareBundle, secrets: await declaredSecrets(file), ...(prepared.classification ? { classification: prepared.classification } : {}), ...(prepared.hostedModels ? { hostedModels: prepared.hostedModels } : {}) } });
|
|
180
|
-
if (!options.ids && !options.casesDir && !options.once)
|
|
208
|
+
if (!options.ids && !options.casesDir && !options.once && !options.runs && !options.live?.length)
|
|
181
209
|
recordChecked(file, report);
|
|
182
210
|
return report;
|
|
183
211
|
}
|
|
184
212
|
export async function testCommand(args) {
|
|
185
|
-
const { values, positionals } = parseArgs({ args, allowPositionals: true, options: { case: { type: "string", multiple: true }, new: { type: "string", multiple: true }, baseline: { type: "string" }, cases: { type: "string" }, workspace: { type: "string" }, once: { type: "boolean" }
|
|
213
|
+
const { values, positionals } = parseArgs({ args, allowPositionals: true, options: { case: { type: "string", multiple: true }, new: { type: "string", multiple: true }, baseline: { type: "string" }, cases: { type: "string" }, workspace: { type: "string" }, once: { type: "boolean" },
|
|
214
|
+
runs: { type: "string" }, "min-pass": { type: "string" }, live: { type: "string", multiple: true }, "no-judge-cache": { type: "boolean" }, "keep-dir": { type: "string" } } });
|
|
186
215
|
const file = positionals[0];
|
|
216
|
+
const usage = "Use method test FILE [--case ID]... [--baseline OLD_FILE] [--new ID]... [--once | --runs N [--min-pass N]] [--live STEP]... [--no-judge-cache] [--keep-dir DIR]";
|
|
187
217
|
if (!file || positionals.length !== 1)
|
|
188
|
-
throw Error(
|
|
218
|
+
throw Error(usage);
|
|
219
|
+
const count = (value, name) => { if (value === undefined)
|
|
220
|
+
return undefined; const n = Number(value); if (!Number.isSafeInteger(n) || n < 1 || n > 20)
|
|
221
|
+
throw Error(`${name} must be a number from 1 to 20. ${usage}`); return n; };
|
|
189
222
|
const report = await runCases(authoringPath(file), values, { casesDir: values.cases ? authoringPath(values.cases) : undefined, ids: values.case,
|
|
190
|
-
baseline: values.baseline ? authoringPath(values.baseline) : undefined, newIds: values.new ?? [], once: !!values.once
|
|
223
|
+
baseline: values.baseline ? authoringPath(values.baseline) : undefined, newIds: values.new ?? [], once: !!values.once,
|
|
224
|
+
runs: count(values.runs, "--runs"), minPass: count(values["min-pass"], "--min-pass"), live: values.live?.flatMap(step => step.split(",")).map(step => step.trim()).filter(Boolean),
|
|
225
|
+
...(values["no-judge-cache"] ? { judgeCache: false } : {}), keepDir: values["keep-dir"] ? authoringPath(values["keep-dir"]) : undefined });
|
|
191
226
|
print(report);
|
|
192
227
|
process.exitCode = report.passed ? 0 : 1;
|
|
193
228
|
}
|
|
@@ -200,16 +235,16 @@ export async function caseGate(file, accept = [], reason) {
|
|
|
200
235
|
if (!cases.length)
|
|
201
236
|
return { line: undefined, report: undefined };
|
|
202
237
|
// A save checks every case; say so first, because live model steps and judges take time and cost money.
|
|
203
|
-
process.stderr.write(`Checking ${cases.length}
|
|
238
|
+
process.stderr.write(`Checking ${cases.length} earlier correction${cases.length === 1 ? "" : "s"} before saving. Parts of the agent that changed run again, which can take a few minutes.\n`);
|
|
204
239
|
const report = await runCases(file, {});
|
|
205
|
-
|
|
206
|
-
process.stderr.write(`Cases checked in ${Math.round(t.duration_ms / 1000)} s: ${t.live_model_steps} live model step${t.live_model_steps === 1 ? "" : "s"}, ${t.judge_calls} judge call${t.judge_calls === 1 ? "" : "s"}.\n`);
|
|
240
|
+
process.stderr.write(`Checked in ${Math.round(report.totals.duration_ms / 1000)} s.\n`);
|
|
207
241
|
const failing = report.cases.filter((c) => !["pass", "fixed"].includes(c.verdict));
|
|
208
242
|
const unaccepted = failing.filter((c) => !accept.includes(c.id));
|
|
209
|
-
const
|
|
210
|
-
const
|
|
243
|
+
const byId = new Map(cases.map((c) => [c.id, c]));
|
|
244
|
+
const { correctionName } = await import("./fix-lines.js");
|
|
245
|
+
const shown = (c) => correctionName(byId.get(c.id) ?? c);
|
|
211
246
|
if (unaccepted.length)
|
|
212
|
-
throw Object.assign(Error(`This
|
|
247
|
+
throw Object.assign(Error(`This change breaks ${unaccepted.length} earlier correction${unaccepted.length === 1 ? "" : "s"}: ${unaccepted.map(shown).join("; ")}. Fix the Method and run method test, or retire a correction whose rule no longer applies with method case retire. To publish anyway, add --accept-failing-case ${unaccepted.map((c) => c.id).join(",")} with --reason.`), { code: "cases_failed" });
|
|
213
248
|
if (failing.length && !reason)
|
|
214
249
|
throw Error("Give --reason for accepting a failing case.");
|
|
215
250
|
return { report, line: failing.length ? `Cases: ${report.cases.length - failing.length} of ${report.cases.length} passed; accepted failing ${failing.map((c) => c.id).join(", ")}.` : `Cases: ${report.cases.length} of ${report.cases.length} passed.` };
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { type MethodClient } from './method-client.js';
|
|
2
|
+
export type UndoRecord = {
|
|
3
|
+
id: string;
|
|
4
|
+
file: string;
|
|
5
|
+
kind: 'fix' | 'prune' | 'learned';
|
|
6
|
+
created_at: string;
|
|
7
|
+
cause: string;
|
|
8
|
+
/** What the person reads after undo: what is no longer in effect. */
|
|
9
|
+
what: string;
|
|
10
|
+
before: {
|
|
11
|
+
text: string;
|
|
12
|
+
files: Record<string, string | null>;
|
|
13
|
+
};
|
|
14
|
+
after: {
|
|
15
|
+
sha256: string;
|
|
16
|
+
files: Record<string, string | null>;
|
|
17
|
+
};
|
|
18
|
+
undone_at?: string;
|
|
19
|
+
};
|
|
20
|
+
/** Take what the file and the named files are now, before a change is written. */
|
|
21
|
+
export declare function snapshot(file: string, paths: string[]): {
|
|
22
|
+
text: string;
|
|
23
|
+
files: {
|
|
24
|
+
[k: string]: string | null;
|
|
25
|
+
};
|
|
26
|
+
};
|
|
27
|
+
/** Record a change after it was written, so method undo can restore the state before it. */
|
|
28
|
+
export declare function recordChange(file: string, kind: UndoRecord['kind'], cause: string, what: string, before: UndoRecord['before']): UndoRecord;
|
|
29
|
+
/** The changes of this file that can be undone, newest first. */
|
|
30
|
+
export declare function undoRecords(file: string): (UndoRecord & {
|
|
31
|
+
path: string;
|
|
32
|
+
})[];
|
|
33
|
+
/**
|
|
34
|
+
* Restore the Method file, and the files the change wrote, as they were before the last change Method made (or before
|
|
35
|
+
* the change `id`, which must be the last one). A file that changed after that change is not overwritten. Returns the
|
|
36
|
+
* undone record; the caller saves the restored file as a new version.
|
|
37
|
+
*/
|
|
38
|
+
export declare function undoLast(file: string, id?: string): UndoRecord & {
|
|
39
|
+
path: string;
|
|
40
|
+
};
|
|
41
|
+
/**
|
|
42
|
+
* `method undo [FILE] [--json]`: restore the Method file, and the files the change wrote, as they were before the last
|
|
43
|
+
* fix, pruning change, or removed learned rule. The restored file is saved as a new version.
|
|
44
|
+
*/
|
|
45
|
+
export declare function undoCommand(args: string[], clientFactory: (server: string) => MethodClient): Promise<void>;
|
|
46
|
+
/** Save a changed file as a new version when this computer is signed in. Undefined when it cannot be saved now. */
|
|
47
|
+
export declare function saveVersion(client: MethodClient, file: string, reason: string): Promise<{
|
|
48
|
+
version_id: string;
|
|
49
|
+
url: string;
|
|
50
|
+
} | undefined>;
|