@method-ai-hq/sdk 0.15.1 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/packages/method-document/src/conversion-report.d.ts +2 -2
- package/dist/packages/sdk/examples/outbound-management/README.md +1 -1
- package/dist/packages/sdk/examples/outbound-management/outbound.method +3 -3
- package/dist/packages/sdk/examples/social-briefing/README.md +1 -1
- package/dist/packages/sdk/examples/social-briefing/social-briefing.method +1 -1
- package/dist/packages/sdk/package.json +2 -2
- package/dist/packages/sdk/src/authoring-instructions.d.ts +2 -2
- package/dist/packages/sdk/src/authoring-instructions.js +2 -2
- package/dist/packages/sdk/src/current-inspection.d.ts +2 -0
- package/dist/packages/sdk/src/current-inspection.js +26 -17
- package/dist/packages/sdk/src/current-runtime.js +6 -2
- package/dist/packages/sdk/src/failure.d.ts +11 -0
- package/dist/packages/sdk/src/failure.js +25 -0
- package/dist/packages/sdk/src/improve.d.ts +7 -4
- package/dist/packages/sdk/src/improve.js +11 -17
- package/dist/packages/sdk/src/inspect.js +1 -1
- package/dist/packages/sdk/src/judge.d.ts +20 -0
- package/dist/packages/sdk/src/judge.js +59 -0
- package/dist/packages/sdk/src/method-help.d.ts +1 -1
- package/dist/packages/sdk/src/method-help.js +9 -8
- package/dist/packages/sdk/src/method-sync.js +3 -4
- package/dist/packages/sdk/src/method.d.ts +0 -2
- package/dist/packages/sdk/src/method.js +8 -10
- package/dist/packages/sdk/src/model-checks.d.ts +18 -1
- package/dist/packages/sdk/src/model-checks.js +67 -7
- package/dist/packages/sdk/src/production.js +1 -1
- package/dist/packages/sdk/src/proposals.d.ts +4 -4
- package/dist/packages/sdk/src/proposals.js +29 -50
- package/dist/packages/sdk/src/run-saved.d.ts +1 -1
- package/dist/packages/sdk/src/run-saved.js +4 -2
- package/dist/packages/sdk/src/run-worker.js +10 -1
- package/dist/packages/sdk/src/worker.js +4 -3
- package/dist/packages/workflow-language/src/inspection.d.ts +272 -138
- package/dist/packages/workflow-language/src/inspection.js +30 -10
- package/package.json +2 -2
|
@@ -13,7 +13,7 @@ export const commandHelp = {
|
|
|
13
13
|
"new-id": { usage: "method new-id FILE", purpose: "Make a copied Method file a new Method.", arguments: "Writes a new id: line. A copy that keeps its id: saves to the same Method.", result: "JSON with file and method_id.", errors: "Missing file.", example: "method new-id copy.method" },
|
|
14
14
|
bind: { usage: "method bind FILE_OR_ID NAME (--file FOLDER | --connection URL) [--upload]", purpose: "Save a named input location or connection.", arguments: "Default: this computer only, in ~/.config/method/computer.json under the Method's ID (a FILE gets its id: line). A folder named NAME beside the Method needs no binding. --upload saves only the selected input folder or connection URL in the account. It never scans your disk. Keep credentials in the service's normal sign-in store.", result: "Saved binding and scope.", errors: "Missing input, invalid name, or unsafe path.", example: "method bind METHOD_ID prepared_day --file day-records --upload", remote: true },
|
|
15
15
|
state: { usage: "method state ID [--enable --file state.json | --release RUN_ID]", purpose: "Read or explicitly enable shared account state.", arguments: "Accepted state updates use revision checks. Inspect a stopped run's external actions before releasing its ownership.", result: "Current revision, value, and owner run.", errors: "Concurrent ownership or stale revision. Existing state is never silently overwritten.", example: "method state METHOD_ID --enable --file initial-state.json", remote: true },
|
|
16
|
-
wait: { usage: "method wait RUN_DIRECTORY", purpose: "Reconnect to the same local worker.", arguments: "Use the directory returned by run. Closing this output connection leaves the worker active.", result: "Live output, then
|
|
16
|
+
wait: { usage: "method wait RUN_DIRECTORY", purpose: "Reconnect to the same local worker.", arguments: "Use the directory returned by run. Closing this output connection leaves the worker active.", result: "Live output, then a final line with the run status (succeeded, unconfirmed, waiting_for_you, or failed), the headline of a failure, and the dashboard link of a synced run.", errors: "Stopped process or missing run.", example: "method wait .runs/example" },
|
|
17
17
|
'run-status': { usage: "method run-status RUN_DIRECTORY", purpose: "Read worker status without waiting.", arguments: "Local run directory.", result: "Worker status and whether its process is active.", errors: "Unreadable run directory.", example: "method run-status .runs/example" },
|
|
18
18
|
cancel: { usage: "method cancel RUN_DIRECTORY", purpose: "Stop a local run process.", arguments: "Inspect uncertain external actions before any explicit retry.", result: "Cancellation request. The checkpoint remains available.", errors: "Unreadable run directory.", example: "method cancel .runs/example" },
|
|
19
19
|
progress: { usage: "method progress --message TEXT [--completed N --total N --unit NAME] [--child NAME]\nmethod progress --codex --child NAME", purpose: "Report public progress from a running script or relay a child Codex JSON stream.", arguments: "METHOD_PROGRESS_FD is supplied by the executor. --codex reads JSON lines from stdin; --message sends one message. Do not include secrets or source contents.", result: "Writes to the separate progress pipe. No stdout output. No-op outside a Method process.", errors: "Invalid arguments. Malformed Codex events are ignored.", example: "method progress --message 'Rendered 12 of 40 pages' --completed 12 --total 40 --unit pages" },
|
|
@@ -36,9 +36,10 @@ export const commandHelp = {
|
|
|
36
36
|
get: { usage: "method get WORKFLOW_ID [--version VERSION_ID] [--out FILE] [--server URL]", purpose: "Read an online method; optionally make a local draft for editing.", arguments: "Default: latest version. --out must name a new file. Use a version ID to read an exact saved version.", result: "Without --out: {workflow_id,version_id,version_number,workflow}. With --out: {workflow_id,version_id,version_number,file}; writes the workflow with its id: line and the saved files.", errors: "Missing method or version; output file already exists.", example: "method get wf_example --out edit.method", remote: true },
|
|
37
37
|
explain: { usage: "method explain FILE [--step ID] [--no-round-trip] [--server URL]", purpose: "Explain each script step of the saved version in a script card.", arguments: "FILE is a local Method whose current content is a saved version (run or publish it while signed in). For each run step whose script has no card, code analysis finds the hosts, secrets, environment variables, files, and commands the script uses; a hosted model writes a one-sentence summary and 3 to 8 steps; every number, quoted name, and host in that text must appear in the code (one retry). The round trip writes a program from the steps alone and replays up to 3 recorded inputs of the step from local runs through both programs without network access. A script that calls the network is not replayed. --step explains one step. --no-round-trip skips the replay. A signed-in run starts this in the background; publish waits for it.", result: "One line per step with its summary and check results. Cards upload to the version; the dashboard shows them beside each script. A failed check is shown on the card and never blocks publish.", errors: "No saved version, local files that differ from the saved version, or no hosted-model credit. Without sign-in it prints one line and makes no card.", example: "method explain leads.method --step score", remote: true },
|
|
38
38
|
publish: { usage: "method publish FILE [--reason TEXT] [--accept-failing-case ID[,ID]] [--cloud|--workers] [--env NAME] [--server URL]", purpose: "Mark a version as published, for sharing, schedules, and production runs.", arguments: "Runs the Method's cases first; a version that breaks an approved case is refused unless --accept-failing-case names it, and the version records that. Uses the version of the file's current content: it finishes that upload (no time limit while bytes move), checks the cases, then marks it published with the reason (default: Published.). --accept-failing-case needs --reason. The first save of a new file creates the Method. --cloud runs the production runs of the environment on Method Cloud; --workers runs them on your workers again. --env NAME selects the environment (default production).", result: "JSON with workflow_id, version_id, url, published_at, the cases line, and the open warnings and notes (issues). With --cloud or --workers: placement. Warnings and notes never block a publish.", errors: "An error issue (nothing is published), failing case, --accept-failing-case without --reason, or no network.", example: "method publish task.method --reason 'Shorter introduction'", remote: true },
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
39
|
+
judge: { usage: "method judge RUN_ID right|wrong [--step STEP_ID] [--note TEXT] [--corrected FILE] [--json] [--server URL]", purpose: "Record the person's reaction to a run's result.", arguments: "RUN_ID is the run_id of the run's start line, or the run ID on the dashboard. right: the person used or approved the result. wrong: the person corrected it, or asked to change the Method because of it. --note: the person's words (at most 4000 characters). --corrected FILE: the result that the person says is right; JSON is sent as JSON, other text as text (at most 1 MB). --step: the step whose output was wrong. A wrong judgment makes Method fix the Method in the background: it applies a tested change that the person can undo, or changes nothing. Do not ask the person to confirm a judgment.", result: "One line, for example Recorded: wrong on step choose. Method is fixing it in the background. --json prints the judgment and fixing.", errors: "An unknown run, a run of a Method that is not in your account, or no sign-in.", example: "method judge 6f1c… wrong --step summary --note 'Name the customer in the first sentence.'", remote: true },
|
|
40
|
+
changes: { usage: "method changes FILE [--json] [--server URL]", purpose: "List the changes that Method applied to a Method.", arguments: "Lists the changes that Method made from wrong judgments, newest first, and the local proposals applied on this computer. Commands that take a signed-in FILE merge a new change into the file first and print one line.", result: "One line per change: its date, its version, and its cause. --json prints {file, method_id, changes: [{id, source, cause, date, version_id, steps}]}. No changes.", errors: "No id: line and no local change.", example: "method changes task.method", remote: true },
|
|
41
|
+
improve: { usage: "method improve FILE [--case ID] [--note TEXT] [--step ID] [--server URL]", purpose: "Improve a Method that keeps its run content on this computer.", arguments: "For a Method with run_data: device, or in an organization that keeps run content on devices: runs Method's improvement on this computer, so the run content stays here. --note: the correction, in the person's words; it is about the newest run of the Method on this computer, which the improvement reads and a suggested case records. --step: improve only this step. --case: the case the change must pass. For a Method that keeps run content in the account, it starts nothing and says to use method judge: Method fixes those Methods from the person's judgments.", result: "JSON with the local proposal ID and the next step; the proposal is in .method/proposals/. Nothing is published, and the file does not change. For an account Method, one line.", errors: "An unknown step, or a step of the improvement that fails.", example: "method improve task.method --step summary --note 'Name the customer in the first sentence.'", remote: true },
|
|
42
|
+
apply: { usage: "method apply FILE [PROPOSAL_ID] [--resolved] [--server URL]", purpose: "Merge a local proposal from method improve into the file.", arguments: "Default: the newest open local proposal. The merge is per step: a part that only the proposal changed takes the change; a part that you and the proposal both changed is a conflict. --resolved: you merged a conflict by hand; this marks the proposal applied. When a change that Method applied conflicts with your edits, the line before the command names it; give its ID to see both versions.", result: "JSON with status applied and the changed steps; the file changes and the proposal is marked applied. Nothing is published. A conflict lists each part with the base, your, and the proposed value, changes nothing, and exits 1.", errors: "No open proposal, or a conflict.", example: "method apply task.method", remote: true },
|
|
42
43
|
secret: { usage: "method secret find [FILE]\nmethod secret import FILE NAME...\nmethod secret set NAME\nmethod secret list [NAME...]", purpose: "Give this computer the values of a Method's declared secrets.", arguments: "find lists the KEY=VALUE files in this folder (3 levels down) and in the parent folder (2 levels down) with the key names in each, never values; for the Method FILE (or the only Method here) it says which declared secrets each file holds and prints the import command. import copies the named values from a KEY=VALUE file that the user names, without printing them. set opens a private form on 127.0.0.1 in the browser for one value. list shows names and where each value is found (shell, this computer, or missing), never values. Values are kept in ~/.config/method/secrets.json (mode 0600) and are never sent to Method. A value exported in the shell is used first. Every script of the Method receives all of its declared secrets.", result: "JSON with the saved names, or the list.", errors: "A name is missing from the file, or no value was entered.", example: "method secret find\nmethod secret import ../service/.env ARCHIVE_TOKEN" },
|
|
43
44
|
connect: { usage: "method connect [APP_FOLDER] [--file METHOD_FILE]", purpose: "Connect an app to the Method in this folder, so the app runs it in production.", arguments: "Run it in the Method's folder. APP_FOLDER (default: this folder) is the app's folder. It makes a service key named after the app folder and writes METHOD_API_KEY to the app's .env (a new file, or a new or replaced line); it never prints the key. It adds .env to .gitignore in a git repository. When the Method has no published version, it publishes the current file. It detects Node (package.json) or Python (pyproject.toml, requirements.txt), and prints the install line and the code to add. Run it again: an app with a working key keeps it.", result: "JSON with the method_id, the published version, where the key is, install, code, secrets_to_set_on_the_host, and next.", errors: "No or several .method files (give --file), an app folder with no language marker, or a failing case at publish.", example: "method connect ../app", remote: true },
|
|
44
45
|
answer: { usage: "method answer RUN_ID [--answer JSON]", purpose: "Show or answer the question of a production run that waits on an ask step.", arguments: "Without --answer it prints the question and its form (the fields of the answer). With --answer it checks the answer against the form and sends it; the run continues. A run that keeps its content on devices has a sealed question: run this where the app's METHOD_API_KEY is (the environment, or the .env of this folder), or on the laptop that started the run with its sign-in. Local runs (method run FILE) take answers with --human FILE as before.", result: "The question and form, or the run after the answer.", errors: "The run is not waiting, the answer does not match the form, or the key cannot open a sealed question (wrong_key).", example: "method answer 6f1c… --answer '{\"approved\":true}'", remote: true },
|
|
@@ -66,7 +67,7 @@ export const commandHelp = {
|
|
|
66
67
|
--fresh: run every step; reuse nothing.
|
|
67
68
|
--human FILE: answers for ask steps, as {steps: {"STEP": {outputs: {...}}}} (STEP:ITERATION, counted from 0, for a step with each or repeat).
|
|
68
69
|
--verbose: print runtime events.
|
|
69
|
-
Use method doctor to check the installed Node and configured tools.`, result: "When signed in, the start line and the final result have dashboard: the link to the run's page; give it to the user when the run starts. Progress
|
|
70
|
+
Use method doctor to check the installed Node and configured tools.`, result: "When signed in, the start line and the final result have dashboard: the link to the run's page; give it to the user when the run starts. Progress, then the final result as JSON on stdout; a run that stopped has failure (owner, headline, step, next), and a failed run also prints Failed, Whose problem, and Next lines on stderr. Local result.json and run evidence. The runtime executes the method's declared scripts, calls, agents, and checks. Executes trusted local processes; changes declarations do not enforce permissions. Runs exit 0 on completion, 1 on failure, 2 when human input is needed, and 3 when an observer could not confirm an external change (unconfirmed).", errors: "Missing inputs/access, failed check, timeout, unsafe resume/version mismatch, upload failure. See recovery. Never retry a business write without inspecting its saved changes.", example: "method run wf_example --version v_example --inputs inputs.json", remote: true },
|
|
70
71
|
observe: { usage: "method observe [RUN_DIRECTORY...] [--pending ROOT]...", purpose: "Make the effect observations that are due for finished runs.", arguments: "Without directories, checks runs with open effects under .method-runs and the Method cache. Runs only observers and judges; never repeats an action. Schedule it, for example hourly, so late evidence such as a bounce reaches the run.", result: "JSON with each run's new verdicts, status, and next observation time. A synced run whose status changed is uploaded again.", errors: "Locked run, changed bundle, or a missing observer credential.", example: "method observe --pending .method-runs" },
|
|
71
72
|
test: { usage: "method test FILE [--case ID]... [--baseline OLD_FILE] [--new ID]... [--cases DIR] [--agent codex|claude]", purpose: "Replay recorded cases against this version of a Method. Every case must pass.", arguments: "Cases are in cases/ beside the Method. Unchanged steps return their recorded outputs; changed steps run; files connections are scratch folders, so a changed step that writes files runs safely. A changed step that asks a person or acts on a service makes the case unverifiable. --baseline also runs each case on the old version, to show what the change fixed or broke.", result: "JSON report with a verdict per case and passed:true when no case blocks the change. Exit 0 or 1.", errors: "Unknown case, invalid Method, or setup errors.", example: "method test task.method --baseline task-before.method --new bounce-reported" },
|
|
72
73
|
case: { usage: "method case new|retire|list FILE ...", purpose: "Keep a correction as a recorded case that every later version must pass.", arguments: "Use method help case new, method help case retire, or method help case list.", result: "See the subcommand.", errors: "Unknown action. Choose new, retire, or list.", example: "method help case new" },
|
|
@@ -299,7 +300,7 @@ const recipes = `# Recipes
|
|
|
299
300
|
## Corrections become cases
|
|
300
301
|
|
|
301
302
|
When the user says a result was wrong:
|
|
302
|
-
1.
|
|
303
|
+
1. Record it: method judge RUN_ID wrong --note "the user's words" (--corrected FILE when they gave the right result). Do not ask them to confirm it. Then read the run (result.json, events.jsonl) and fix the Method. Run it again and show the user the new result.
|
|
303
304
|
2. If the user wants the fix to stay, ask once: "Keep this as a rule: <the rule in plain words>?"
|
|
304
305
|
3. On yes, record it: method case new FILE --id ID --run BAD_RUN --passing-run NEW_RUN --note "the user's words" --rubric "plain sentence that must be true" (repeat --rubric for each rule). --ref names the output to judge (default: the Method's result; a path to a file that the run saved is judged by that file's contents). When a rule compares the output with the sources, add --context with the step output that holds them, for example --context outputs.material. Make each sentence fail on the bad run; case new warns about one that does not. For an exact value, use --expect instead.
|
|
305
306
|
4. Run method test FILE. Every case must pass. method publish refuses a version that breaks a case.
|
|
@@ -337,7 +338,7 @@ A save never blocks a run: a run that cannot save says "Will save when online",
|
|
|
337
338
|
State commits after checks. A local checkpoint cannot roll back an external write. Inspect external state before an explicit retry.
|
|
338
339
|
Two computers can save different content of one Method; each becomes a version with its parent. Going back to earlier content finds its existing version. The id: line keeps a renamed or moved file on its Method; method new-id FILE makes a copy a new Method. On one computer a Method is saved from one file (every checkout of one Git repository counts as one): a copy in another folder or a temporary folder does not run while that file exists, and the error names the file to change. When that file is unchanged since its last save and the account has a newer version, a run takes that version first.
|
|
339
340
|
Use method sync RUN_DIRECTORY to repair a dashboard upload without executing the method again.
|
|
340
|
-
After a failed action with effects,
|
|
341
|
+
After a failed action with effects, failure.next.text lists what the observers saw. A confirmed effect means the change happened: do not retry the action. A run that is unconfirmed finished all its steps; inspect effects.jsonl and the external system before relying on its result.
|
|
341
342
|
`;
|
|
342
343
|
export const guideTopics = ["start", "concepts", "execution", "examples", "example", "recipes", "recovery", "commands"];
|
|
343
344
|
export function authoringGuide(topic = "start", exampleId) {
|
|
@@ -370,7 +371,7 @@ Learn: method authoring Start the installed guide.
|
|
|
370
371
|
Local drafts: init, show, set, remove, step add/update/remove/move,
|
|
371
372
|
check set/remove, validate, diff.
|
|
372
373
|
Online documents: list, get, steps, step, publish, explain.
|
|
373
|
-
|
|
374
|
+
Results and improvements: judge, changes, improve, apply.
|
|
374
375
|
Keys for scripts: secret find, secret import, secret set, secret list.
|
|
375
376
|
Execution and evidence: run, run-status, wait, cancel, runs, logs, sync, observe.
|
|
376
377
|
Effects and corrections: effect add/list, case new/retire/list, test.
|
|
@@ -5,6 +5,7 @@ import { join } from "node:path";
|
|
|
5
5
|
import { randomUUID } from "node:crypto";
|
|
6
6
|
import { z } from "zod";
|
|
7
7
|
import { inspectRun } from "./inspect.js";
|
|
8
|
+
import { setupFailure, unreadableRecord } from "./failure.js";
|
|
8
9
|
import { writePrivateJson } from "./files.js";
|
|
9
10
|
import { MethodClient } from "./method-client.js";
|
|
10
11
|
import { InspectionSchema, deviceOnly, } from "../../workflow-language/src/inspection.js";
|
|
@@ -216,10 +217,8 @@ export class MethodSync {
|
|
|
216
217
|
inspection = {
|
|
217
218
|
...this.initial,
|
|
218
219
|
events: existsSync(join(this.directory, "setup.json")) ? JSON.parse(readFileSync(join(this.directory, "setup.json"), "utf8")) : this.initial.events,
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
? failure.message
|
|
222
|
-
: String(failure ?? error),
|
|
220
|
+
// A run that failed before its first step has the failure record of its setup error; otherwise Method could not read the record.
|
|
221
|
+
...((record => ({ status: record.code === "needs_input" ? "waiting_for_you" : "failed", failure: record }))(failure ? setupFailure(failure) : unreadableRecord(error))),
|
|
223
222
|
};
|
|
224
223
|
// A run that failed before its first step keeps this record for a later sync.
|
|
225
224
|
writePrivateJson(join(this.directory, "setup-inspection.json"), inspection);
|
|
@@ -1,5 +1,3 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import { MethodClient } from "./method-client.js";
|
|
3
|
-
/** The next step for a failure before the first step. */
|
|
4
|
-
export declare function setupFix(error: any): any;
|
|
5
3
|
export declare function methodMain(args?: string[], clientFactory?: (server: string) => MethodClient): Promise<boolean | void>;
|
|
@@ -20,15 +20,7 @@ import { parse, localMain } from "./local-cli.js";
|
|
|
20
20
|
import { writePrivateJson } from "./files.js";
|
|
21
21
|
import { loadWorkflow } from "../../workflow-language/src/validate.js";
|
|
22
22
|
import { methodHelp, overviewHelp as help } from "./method-help.js";
|
|
23
|
-
import { fixFor } from "@withmethod/runtime/runner.js";
|
|
24
23
|
/** The next step for a failure before the first step. */
|
|
25
|
-
export function setupFix(error) {
|
|
26
|
-
if (error.code === 'missing_secret') {
|
|
27
|
-
const names = (error.missing ?? []).join(' ');
|
|
28
|
-
return `No step ran. Run method secret find to see which nearby files hold ${names}, then method secret import FILE ${names}; or ask the user to enter each one with method secret set NAME (a private form in their browser). Never open or print a key file, and never ask for a value in chat.`;
|
|
29
|
-
}
|
|
30
|
-
return fixFor(error);
|
|
31
|
-
}
|
|
32
24
|
function safePath(path) {
|
|
33
25
|
return authoringPath(path);
|
|
34
26
|
}
|
|
@@ -70,7 +62,9 @@ export async function methodMain(args = process.argv.slice(2), clientFactory = s
|
|
|
70
62
|
process.stdout.write(earlyHelp);
|
|
71
63
|
return;
|
|
72
64
|
}
|
|
73
|
-
if (
|
|
65
|
+
if (args[0] === 'judge')
|
|
66
|
+
return (await import('./judge.js')).judgeCommand(args.slice(1), clientFactory);
|
|
67
|
+
if (['improve', 'changes', 'apply'].includes(args[0] ?? ''))
|
|
74
68
|
return (await import('./proposals.js')).proposalCommand(args, clientFactory);
|
|
75
69
|
await (await import('./proposals.js')).autoApply(args, clientFactory);
|
|
76
70
|
if (args[0] === 'worker' || args[0] === 'keys')
|
|
@@ -99,9 +93,13 @@ export async function methodMain(args = process.argv.slice(2), clientFactory = s
|
|
|
99
93
|
}
|
|
100
94
|
catch (error) {
|
|
101
95
|
// A run that fails before its first step still prints one result that says why and what to do.
|
|
102
|
-
const
|
|
96
|
+
const { setupFailure, failureLines } = await import('./failure.js');
|
|
97
|
+
const failure = setupFailure(error);
|
|
98
|
+
const failed = { status: error.code === 'needs_input' ? 'needs_input' : 'failed', run_dir: directory, failure, ...(error.missing ? { missing: error.missing } : {}) };
|
|
103
99
|
if (!existsSync(join(directory, 'summary.json')))
|
|
104
100
|
writePrivateJson(join(directory, 'summary.json'), failed);
|
|
101
|
+
if (failed.status === 'failed')
|
|
102
|
+
process.stderr.write(failureLines(failure));
|
|
105
103
|
process.stdout.write(JSON.stringify(failed, null, 2) + '\n');
|
|
106
104
|
process.exitCode = error.code === 'needs_input' ? 2 : 1;
|
|
107
105
|
}
|
|
@@ -25,11 +25,21 @@ type Check = {
|
|
|
25
25
|
/** Which part of the step the check reads; undefined when the check does not apply to the step. */
|
|
26
26
|
script?: (step: Step) => string | undefined;
|
|
27
27
|
applies: (step: Step) => boolean;
|
|
28
|
+
/** More inputs for Jev, from the Method (for example the definitions of an agent step's tools). */
|
|
29
|
+
context?: (step: Step, method: any) => Record<string, string>;
|
|
30
|
+
/** A finding (true) or none (false) that code analysis decides without a question to Jev; undefined asks Jev. */
|
|
31
|
+
decide?: (inputs: Record<string, string>) => boolean | undefined;
|
|
28
32
|
field: string;
|
|
29
33
|
message: (id: string) => string;
|
|
30
34
|
fix: string;
|
|
31
35
|
};
|
|
32
36
|
export declare const checks: Record<string, Check>;
|
|
37
|
+
/**
|
|
38
|
+
* The script scan of a run step (explain-effects): a network host or a write is a finding with no model call. No host,
|
|
39
|
+
* no write, and no library that can send: no finding. A library that can send or a subprocess with no host found:
|
|
40
|
+
* undefined, so Jev answers.
|
|
41
|
+
*/
|
|
42
|
+
export declare function scanDecision(stepYaml: string, script: string): boolean | undefined;
|
|
33
43
|
/**
|
|
34
44
|
* Checks shown to the user. A check moves here from shadow mode only with a results.json that shows its precision
|
|
35
45
|
* on the frozen held-out set meets its level (warning >= 0.90, note >= 0.75). Run npm run eval:checks first.
|
|
@@ -78,7 +88,14 @@ export declare function askCheck(provider: ClassificationProvider, question: str
|
|
|
78
88
|
probability: number;
|
|
79
89
|
model: string;
|
|
80
90
|
}>;
|
|
81
|
-
|
|
91
|
+
/** The answer of code analysis, when the check has one for these inputs. */
|
|
92
|
+
export declare function decided(check: string, inputs: Record<string, string>): {
|
|
93
|
+
answer: boolean;
|
|
94
|
+
probability: number;
|
|
95
|
+
model: string;
|
|
96
|
+
} | undefined;
|
|
97
|
+
/** Answer one target: code analysis when it decides, or else Jev. scanned: code analysis already found it cannot decide. */
|
|
98
|
+
export declare function evaluateTarget(provider: ClassificationProvider | undefined, target: Target, signal: AbortSignal, scanned?: boolean): Promise<Result>;
|
|
82
99
|
/**
|
|
83
100
|
* Run the targets in parallel. Cached answers come at once; new ones get waitMs in all. The others are returned as
|
|
84
101
|
* pending, so the caller can finish them in the background. A failed check is left out and is tried again next time.
|
|
@@ -8,7 +8,11 @@ import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, writeFile
|
|
|
8
8
|
import { join } from 'node:path';
|
|
9
9
|
import { serializeWorkflow } from '../../workflow-language/src/validate.js';
|
|
10
10
|
import { methodCache } from './prepare.js';
|
|
11
|
+
import { scriptLanguage, staticEffects } from './explain-effects.js';
|
|
11
12
|
const promptKinds = ['call', 'agent'];
|
|
13
|
+
/** A step that declares a change outside the run: a connection or service in changes, or an effect. */
|
|
14
|
+
const declaresWorldChange = (step) => (step.changes ?? []).some((ref) => typeof ref === 'string' && ref.startsWith('environment.')) || Object.keys(step.effects ?? {}).length > 0;
|
|
15
|
+
const agentWithTools = (step) => step.do?.kind === 'agent' && ((step.do.tools ?? []).length > 0 || !!step.do.browser);
|
|
12
16
|
export const checks = {
|
|
13
17
|
script_calls_model: {
|
|
14
18
|
version: 1, level: 'warning', field: 'do',
|
|
@@ -52,7 +56,48 @@ export const checks = {
|
|
|
52
56
|
message: id => `The prompt of ${id} appears to need information that the step does not give it.`,
|
|
53
57
|
fix: 'Add the missing value to in: from an input or an earlier step, or write it in the prompt.',
|
|
54
58
|
},
|
|
59
|
+
undeclared_change: {
|
|
60
|
+
version: 1, level: 'warning', field: 'do',
|
|
61
|
+
question: 'Can this step send, post, or change something outside its own output folder, for example send an email or a message, create or update a record in a service or an API, submit a form, or write files elsewhere, while its changes: and effects: declare no such change? Reading, searching, downloading, and returning a result are not changes.',
|
|
62
|
+
applies: step => !declaresWorldChange(step) && (step.do?.kind === 'run' || agentWithTools(step)),
|
|
63
|
+
script: step => step.do?.kind === 'run' ? step.do.entrypoint : undefined,
|
|
64
|
+
context: (step, method) => step.do?.kind === 'agent' && (step.do.tools ?? []).length
|
|
65
|
+
? { tools: limit(serializeWorkflow(Object.fromEntries(step.do.tools.map(name => [name, method.tools?.[name] ?? null]))), 16_000) } : {},
|
|
66
|
+
decide: inputs => inputs.script === undefined ? undefined : scanDecision(inputs.step ?? "", inputs.script),
|
|
67
|
+
message: id => `${id} can send or change things that it does not declare.`,
|
|
68
|
+
fix: 'Declare what it changes in changes:, so Method can check where its data goes.',
|
|
69
|
+
},
|
|
55
70
|
};
|
|
71
|
+
/** Libraries and calls that can send or change something, but whose target code analysis may not find. */
|
|
72
|
+
const senders = {
|
|
73
|
+
python: /^\s*(?:import|from)\s+(?:requests|httpx|aiohttp|urllib3?|http\.client|smtplib|email\.mime|boto3|slack_sdk|twilio|sendgrid|googleapiclient|subprocess)\b|\bos\.(?:system|popen)\(/m,
|
|
74
|
+
node: /\bfetch\(|\brequire\(\s*['"](?:node:)?(?:https?|child_process|axios|node-fetch|got|undici|nodemailer|@sendgrid\/mail|@slack\/web-api|twilio)['"]|\bfrom\s+['"](?:node:)?(?:https?|child_process|axios|node-fetch|got|undici|nodemailer|@sendgrid\/mail|@slack\/web-api|twilio)['"]/,
|
|
75
|
+
};
|
|
76
|
+
/** Temporary files are not a change in the world. */
|
|
77
|
+
const temporaryPath = (path) => /^(?:\/tmp\/|\/var\/folders\/|\/private\/tmp\/)/.test(path);
|
|
78
|
+
/**
|
|
79
|
+
* The script scan of a run step (explain-effects): a network host or a write is a finding with no model call. No host,
|
|
80
|
+
* no write, and no library that can send: no finding. A library that can send or a subprocess with no host found:
|
|
81
|
+
* undefined, so Jev answers.
|
|
82
|
+
*/
|
|
83
|
+
export function scanDecision(stepYaml, script) {
|
|
84
|
+
const entrypoint = /^\s*entrypoint:\s*["']?([^\s"']+)/m.exec(stepYaml)?.[1];
|
|
85
|
+
const language = entrypoint ? scriptLanguage(entrypoint) : undefined;
|
|
86
|
+
if (!language || !script.trim())
|
|
87
|
+
return undefined;
|
|
88
|
+
let effects;
|
|
89
|
+
try {
|
|
90
|
+
effects = staticEffects(language, [{ path: entrypoint, text: script }], undefined).effects;
|
|
91
|
+
}
|
|
92
|
+
catch {
|
|
93
|
+
return undefined;
|
|
94
|
+
}
|
|
95
|
+
if (effects.network.length || effects.writes.some(path => !temporaryPath(path)))
|
|
96
|
+
return true;
|
|
97
|
+
if (effects.runs.length || senders[language].test(script))
|
|
98
|
+
return undefined;
|
|
99
|
+
return false;
|
|
100
|
+
}
|
|
56
101
|
/**
|
|
57
102
|
* Checks shown to the user. A check moves here from shadow mode only with a results.json that shows its precision
|
|
58
103
|
* on the frozen held-out set meets its level (warning >= 0.90, note >= 0.75). Run npm run eval:checks first.
|
|
@@ -78,7 +123,7 @@ export function checkTargets(method, readText, steps) {
|
|
|
78
123
|
if (!check.applies(step))
|
|
79
124
|
continue;
|
|
80
125
|
const path = check.script?.(step);
|
|
81
|
-
const inputs = { step: stepText(id, step) };
|
|
126
|
+
const inputs = { step: stepText(id, step), ...check.context?.(step, method) };
|
|
82
127
|
if (path)
|
|
83
128
|
inputs.script = limit(readText(path) ?? '', 40_000);
|
|
84
129
|
targets.push({ check: name, version: check.version, step: id, inputs, key: checkKey(name, check.version, inputs) });
|
|
@@ -86,7 +131,7 @@ export function checkTargets(method, readText, steps) {
|
|
|
86
131
|
}
|
|
87
132
|
return targets;
|
|
88
133
|
}
|
|
89
|
-
export const checkKey = (check, version, inputs) => createHash('sha256').update(JSON.stringify([check, version, inputs.step, inputs.script ?? ''])).digest('hex');
|
|
134
|
+
export const checkKey = (check, version, inputs) => createHash('sha256').update(JSON.stringify([check, version, inputs.step, inputs.script ?? '', ...(inputs.tools !== undefined ? [inputs.tools] : [])])).digest('hex');
|
|
90
135
|
export const checksDir = () => join(methodCache(), 'checks');
|
|
91
136
|
export function cached(key) {
|
|
92
137
|
const file = join(checksDir(), `${key}.json`);
|
|
@@ -108,14 +153,24 @@ function store(result) {
|
|
|
108
153
|
* probability is that of yes.
|
|
109
154
|
*/
|
|
110
155
|
export const answerOptions = { yes: 'Yes: the question describes this step.', no: 'No: the question does not describe this step.' };
|
|
156
|
+
const noProvider = () => { throw Error('Model checks need sign-in.'); };
|
|
111
157
|
export async function askCheck(provider, question, inputs, signal) {
|
|
112
158
|
const { model } = await provider.resolve(signal);
|
|
113
159
|
const answer = await provider.evaluate({ request_id: randomUUID(), model, question, options: answerOptions, inputs }, signal);
|
|
114
160
|
const probability = answer.probabilities.yes ?? 0;
|
|
115
161
|
return { answer: probability >= findingThreshold, probability, model };
|
|
116
162
|
}
|
|
117
|
-
|
|
118
|
-
|
|
163
|
+
/** The answer of code analysis, when the check has one for these inputs. */
|
|
164
|
+
export function decided(check, inputs) {
|
|
165
|
+
const answer = checks[check]?.decide?.(inputs);
|
|
166
|
+
return answer === undefined ? undefined : { answer, probability: answer ? 1 : 0, model: 'script-scan' };
|
|
167
|
+
}
|
|
168
|
+
/** Answer one target: code analysis when it decides, or else Jev. scanned: code analysis already found it cannot decide. */
|
|
169
|
+
export async function evaluateTarget(provider, target, signal, scanned = false) {
|
|
170
|
+
const answer = (scanned ? undefined : decided(target.check, target.inputs)) ?? await askCheck(provider ?? noProvider(), checks[target.check].question, target.inputs, signal);
|
|
171
|
+
return record(target, answer);
|
|
172
|
+
}
|
|
173
|
+
function record(target, answer) {
|
|
119
174
|
const result = { check: target.check, version: target.version, step: target.step, key: target.key, answer: answer.probability >= findingThreshold,
|
|
120
175
|
probability: answer.probability, model: answer.model, at: new Date().toISOString() };
|
|
121
176
|
store(result);
|
|
@@ -127,15 +182,20 @@ export async function evaluateTarget(provider, target, signal) {
|
|
|
127
182
|
*/
|
|
128
183
|
export async function runChecks(provider, targets, waitMs = 3000) {
|
|
129
184
|
const done = [], pending = [];
|
|
130
|
-
|
|
131
|
-
|
|
185
|
+
// Answers that code analysis decides need no provider and no wait.
|
|
186
|
+
const open = targets.filter(target => {
|
|
187
|
+
const hit = cached(target.key) ?? (() => { const answer = decided(target.check, target.inputs); return answer && record(target, answer); })();
|
|
188
|
+
if (hit)
|
|
189
|
+
done.push(hit);
|
|
190
|
+
return !hit;
|
|
191
|
+
});
|
|
132
192
|
if (!open.length)
|
|
133
193
|
return { done, pending };
|
|
134
194
|
if (!provider)
|
|
135
195
|
return { done, pending: open };
|
|
136
196
|
const controller = new AbortController();
|
|
137
197
|
const settled = new Set();
|
|
138
|
-
const work = open.map(target => evaluateTarget(provider, target, controller.signal)
|
|
198
|
+
const work = open.map(target => evaluateTarget(provider, target, controller.signal, true)
|
|
139
199
|
.then(result => { done.push(result); }).catch(() => { }).finally(() => settled.add(target.key)));
|
|
140
200
|
let timer;
|
|
141
201
|
await Promise.race([Promise.all(work), new Promise(resolve => { timer = setTimeout(resolve, waitMs); })]);
|
|
@@ -155,7 +155,7 @@ export class Method {
|
|
|
155
155
|
throw new MethodApiError(409, "not_waiting", `The run is ${run.status}; it has no question to answer.`);
|
|
156
156
|
const asked = this.openQuestion(run);
|
|
157
157
|
try {
|
|
158
|
-
assertSchema(asked.form, answer, "Answer");
|
|
158
|
+
assertSchema(asked.form, answer, "Answer", { producer: "the answer", owner: "needs_you" });
|
|
159
159
|
}
|
|
160
160
|
catch (error) {
|
|
161
161
|
throw new MethodApiError(422, "invalid_answer", error.message);
|
|
@@ -58,9 +58,9 @@ type Applied = {
|
|
|
58
58
|
/** Merge a proposal into the file. A clean merge writes the file and marks the proposal applied. Never publishes. */
|
|
59
59
|
export declare function applyProposal(client: MethodClient | undefined, file: string, proposal: ProposalRecord, options?: RequestOptions): Promise<Applied>;
|
|
60
60
|
/**
|
|
61
|
-
* Before a command that takes a .method FILE: when this computer is signed in and
|
|
62
|
-
* merge it into the file and print one line. A conflict prints
|
|
63
|
-
* Offline, signed out, or on any error, it does nothing.
|
|
61
|
+
* Before a command that takes a .method FILE: when this computer is signed in and Method applied a change to the Method
|
|
62
|
+
* (an accepted proposal, from the person's judgments), merge it into the file and print one line. A conflict prints
|
|
63
|
+
* what the coding agent must resolve; the command goes on. Offline, signed out, or on any error, it does nothing.
|
|
64
64
|
*/
|
|
65
65
|
export declare function autoApply(args: string[], clientFactory: (server: string) => MethodClient): Promise<void>;
|
|
66
66
|
/** Run folders of this Method on this computer, newest first, with the account run ID of each run that was saved to the account. */
|
|
@@ -68,6 +68,6 @@ export declare function localRuns(file: string): Promise<{
|
|
|
68
68
|
dir: string;
|
|
69
69
|
account_run_id: string | null;
|
|
70
70
|
}[]>;
|
|
71
|
-
/** `method improve`, `method
|
|
71
|
+
/** `method improve`, `method changes`, and `method apply`. */
|
|
72
72
|
export declare function proposalCommand(args: string[], clientFactory: (server: string) => MethodClient): Promise<boolean | undefined>;
|
|
73
73
|
export {};
|
|
@@ -133,11 +133,11 @@ async function markApplied(client, file, proposal, options = {}) {
|
|
|
133
133
|
}
|
|
134
134
|
}
|
|
135
135
|
const serverFlag = (args) => { const at = args.findIndex(arg => arg === '--server' || arg.startsWith('--server=')); return at < 0 ? undefined : args[at].startsWith('--server=') ? args[at].slice(9) : args[at + 1]; };
|
|
136
|
-
const skipAutoApply = new Set(['apply', '
|
|
136
|
+
const skipAutoApply = new Set(['apply', 'changes', 'judge', 'improve', 'init', 'new-id', 'diff', 'help', 'authoring', 'schema', '__worker', '__checks', 'worker', 'keys', 'secret', 'models', 'login', 'logout', 'sync', 'progress']);
|
|
137
137
|
/**
|
|
138
|
-
* Before a command that takes a .method FILE: when this computer is signed in and
|
|
139
|
-
* merge it into the file and print one line. A conflict prints
|
|
140
|
-
* Offline, signed out, or on any error, it does nothing.
|
|
138
|
+
* Before a command that takes a .method FILE: when this computer is signed in and Method applied a change to the Method
|
|
139
|
+
* (an accepted proposal, from the person's judgments), merge it into the file and print one line. A conflict prints
|
|
140
|
+
* what the coding agent must resolve; the command goes on. Offline, signed out, or on any error, it does nothing.
|
|
141
141
|
*/
|
|
142
142
|
export async function autoApply(args, clientFactory) {
|
|
143
143
|
if (!args[0] || skipAutoApply.has(args[0]) || args.includes('--help') || process.env.METHOD_RUN_WORKER === '1')
|
|
@@ -159,12 +159,12 @@ export async function autoApply(args, clientFactory) {
|
|
|
159
159
|
const result = await applyProposal(client, file, proposal, options);
|
|
160
160
|
if (result.status === 'applied') {
|
|
161
161
|
if (result.steps.length || result.warning)
|
|
162
|
-
process.stderr.write(`
|
|
162
|
+
process.stderr.write(`Method changed ${target} (${proposal.id}): ${proposal.cause}${result.steps.length ? ` Changed steps: ${result.steps.join(', ')}.` : ''}${result.warning ? ` ${result.warning}` : ''}\n`);
|
|
163
163
|
}
|
|
164
164
|
else
|
|
165
165
|
process.stderr.write(result.status === 'conflict'
|
|
166
|
-
? `
|
|
167
|
-
: `
|
|
166
|
+
? `Method's change ${proposal.id} conflicts with your edits of ${target} in ${result.conflicts.map(c => c.path).join(', ')}. The file is not changed. Run method apply ${target} ${proposal.id} to see both versions, merge them in the file, then run method apply ${target} ${proposal.id} --resolved.\n`
|
|
167
|
+
: `Method's change ${proposal.id} does not merge into a valid Method: ${result.error} The file is not changed. Run method apply ${target} ${proposal.id} to see it.\n`);
|
|
168
168
|
}
|
|
169
169
|
catch { /* offline or signed out: the command goes on without the proposal. */ }
|
|
170
170
|
}
|
|
@@ -196,55 +196,36 @@ function manualStep(file, proposal, runFolder) {
|
|
|
196
196
|
return 'This proposal has no change to apply.';
|
|
197
197
|
}
|
|
198
198
|
const print = (value) => process.stdout.write(JSON.stringify(value, null, 2) + '\n');
|
|
199
|
-
|
|
200
|
-
|
|
199
|
+
const changeLine = (change) => `${change.date.slice(0, 10)} ${change.version_id ?? 'not saved as a version'} ${change.cause}`;
|
|
200
|
+
/** `method changes FILE`: the changes that Method applied to the Method, newest first, and those applied on this computer. */
|
|
201
|
+
async function changesCommand(file, target, methodId, client, json) {
|
|
202
|
+
const local = localProposals(file).filter(p => p.status === 'applied').map((p) => ({ id: p.id, source: 'local', cause: p.cause, date: String(p.applied_at ?? p.created_at), version_id: p.applied_version_id ?? null, steps: p.evidence?.steps ?? [] }));
|
|
203
|
+
let account = [];
|
|
204
|
+
if (methodId) {
|
|
205
|
+
const { proposals } = await client.request(`/api/methods/${encodeURIComponent(methodId)}/proposals?status=accepted`);
|
|
206
|
+
account = (proposals ?? []).map(p => ({ id: p.id, source: 'account', cause: String(p.cause ?? ''), date: String(p.created_at ?? ''), version_id: p.published_version_id ?? null, steps: p.evidence?.steps ?? [] }));
|
|
207
|
+
}
|
|
208
|
+
else if (!local.length)
|
|
209
|
+
throw Error(`${target} has no id: line, so Method has not changed it. Run it once while signed in.`);
|
|
210
|
+
const changes = [...account, ...local].sort((a, b) => b.date.localeCompare(a.date));
|
|
211
|
+
if (json)
|
|
212
|
+
return print({ file, method_id: methodId, changes });
|
|
213
|
+
process.stdout.write(changes.length ? changes.map(changeLine).join('\n') + '\n' : `Method has not changed ${target} yet.\n`);
|
|
201
214
|
}
|
|
202
|
-
/** `method improve`, `method
|
|
215
|
+
/** `method improve`, `method changes`, and `method apply`. */
|
|
203
216
|
export async function proposalCommand(args, clientFactory) {
|
|
204
217
|
if (args[0] === 'improve')
|
|
205
218
|
return (await import('./improve.js')).improveCommand(args.slice(1), clientFactory);
|
|
206
|
-
const { values, positionals } = parseArgs({ args: args.slice(1), allowPositionals: true, options: { server: { type: 'string' }, resolved: { type: 'boolean' },
|
|
219
|
+
const { values, positionals } = parseArgs({ args: args.slice(1), allowPositionals: true, options: { server: { type: 'string' }, resolved: { type: 'boolean' }, json: { type: 'boolean' } } });
|
|
207
220
|
const [target, wanted] = positionals;
|
|
208
221
|
if (!target || positionals.length > (args[0] === 'apply' ? 2 : 1))
|
|
209
|
-
throw Error(args[0] === 'apply' ? 'Use method apply FILE [PROPOSAL_ID] [--resolved].' : 'Use method
|
|
222
|
+
throw Error(args[0] === 'apply' ? 'Use method apply FILE [PROPOSAL_ID] [--resolved].' : 'Use method changes FILE [--json].');
|
|
210
223
|
const file = authoringPath(target), id = readDocument(file).id;
|
|
211
224
|
const methodId = typeof id === 'string' && methodIdPattern.test(id) ? id : null;
|
|
212
225
|
const client = clientFactory(values.server ?? DEFAULT_SERVER);
|
|
213
226
|
const signedIn = !!client.token();
|
|
214
|
-
if (args[0] === '
|
|
215
|
-
|
|
216
|
-
if (!methodId || !signedIn)
|
|
217
|
-
return print({ file, method_id: methodId, proposals: local, ...(methodId ? { next: 'Run method login to see the proposals in your account too.' } : {}) });
|
|
218
|
-
const read = async () => {
|
|
219
|
-
const [{ proposals }, { improvements }] = await Promise.all([
|
|
220
|
-
client.request(`/api/methods/${encodeURIComponent(methodId)}/proposals`),
|
|
221
|
-
client.request(`/api/methods/${encodeURIComponent(methodId)}/improvements`)
|
|
222
|
-
]);
|
|
223
|
-
// The newest improvement that has no proposal: still running, failed, or ended with no safe change (outcome none).
|
|
224
|
-
const latest = (improvements ?? []).find(i => !i.proposal_id);
|
|
225
|
-
return { proposals, pending: latest && (latest.created_at ?? '') > String((proposals ?? [])[0]?.created_at ?? '') ? latest : undefined };
|
|
226
|
-
};
|
|
227
|
-
let { proposals, pending } = await read();
|
|
228
|
-
// --wait: one line each time the improvement reaches a new step, until it ends.
|
|
229
|
-
let shown = '';
|
|
230
|
-
while (values.wait && pending && ['queued', 'running'].includes(pending.status)) {
|
|
231
|
-
const step = pending.steps?.find((s) => s.status === 'running')?.step_id ?? pending.status;
|
|
232
|
-
if (step !== shown) {
|
|
233
|
-
process.stderr.write(`Improvement ${pending.id}: ${step}\n`);
|
|
234
|
-
shown = step;
|
|
235
|
-
}
|
|
236
|
-
await new Promise(resolve => setTimeout(resolve, 5000));
|
|
237
|
-
({ proposals, pending } = await read());
|
|
238
|
-
}
|
|
239
|
-
const failedStep = pending?.steps?.find((s) => s.status === 'failed')?.step_id;
|
|
240
|
-
const next = !pending ? undefined
|
|
241
|
-
: ['queued', 'running'].includes(pending.status) ? `Improvement ${pending.id} is ${pending.status}. Run method proposals ${target} --wait: it returns when the proposal is ready.`
|
|
242
|
-
: pending.outcome === 'none' ? `The improvement found no safe change. Say what is wrong in a note on a run (method improve ${target} --note TEXT), or record a case with method case new.`
|
|
243
|
-
: pending.status === 'failed' || pending.status === 'cancelled' ? `Improvement ${pending.id} ${pending.status}${failedStep ? ` at step ${failedStep}` : ''}${pending.error?.message ? `: ${pending.error.message}` : ''}. Run method improve ${target} again, or record the correction with method case new.`
|
|
244
|
-
: undefined;
|
|
245
|
-
return print({ file, method_id: methodId, proposals: [...(proposals ?? []).map(p => summary({ ...p, source: 'account' })), ...local].sort((a, b) => String(b.created_at).localeCompare(String(a.created_at))),
|
|
246
|
-
...(pending ? { improvement: { id: pending.id, status: pending.status, ...(pending.outcome === 'none' ? { outcome: 'none' } : {}), ...(failedStep ? { failed_step: failedStep } : {}), ...(pending.error ? { error: pending.error } : {}) } } : {}), ...(next ? { next } : {}) });
|
|
247
|
-
}
|
|
227
|
+
if (args[0] === 'changes')
|
|
228
|
+
return changesCommand(file, target, methodId, client, !!values.json);
|
|
248
229
|
const local = localProposals(file);
|
|
249
230
|
let proposal = wanted ? local.find(p => p.id === wanted) : undefined;
|
|
250
231
|
if (!proposal && wanted) {
|
|
@@ -254,15 +235,13 @@ export async function proposalCommand(args, clientFactory) {
|
|
|
254
235
|
if (methodId && proposal.method_id !== methodId)
|
|
255
236
|
throw Error(`Proposal ${wanted} is for another Method.`);
|
|
256
237
|
}
|
|
257
|
-
if (!proposal && methodId && signedIn)
|
|
258
|
-
proposal = await newestAccepted(client, methodId);
|
|
259
238
|
proposal ??= local.find(p => p.status === 'open');
|
|
260
239
|
if (!proposal)
|
|
261
|
-
throw Error(`${target} has no
|
|
240
|
+
throw Error(`${target} has no open proposal. Run method improve ${target} --note TEXT first.`);
|
|
262
241
|
if (proposal.status === 'applied')
|
|
263
242
|
return print({ status: 'applied', file, proposal_id: proposal.id, next: 'This proposal is already applied.' });
|
|
264
243
|
if (proposal.source === 'account' && proposal.status !== 'accepted')
|
|
265
|
-
throw Error(`
|
|
244
|
+
throw Error(`Change ${proposal.id} is ${proposal.status}, so it has nothing to merge.`);
|
|
266
245
|
if (proposal.source === 'local' && proposal.status !== 'open')
|
|
267
246
|
throw Error(`Proposal ${proposal.id} is ${proposal.status}.`);
|
|
268
247
|
if (values.resolved)
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { MethodClient } from './method-client.js';
|
|
2
2
|
import type { parse } from './local-cli.js';
|
|
3
3
|
/** A run whose steps all finished: completed, or unconfirmed (an external change could not be confirmed). */
|
|
4
|
-
export declare const finishedRun: (status: string) => status is "
|
|
4
|
+
export declare const finishedRun: (status: string) => status is "unconfirmed" | "completed";
|
|
5
5
|
export declare function runSaved(saved: any, flags: ReturnType<typeof parse>['values'], client: MethodClient): Promise<any>;
|
|
@@ -12,8 +12,9 @@ import { writePrivateJson } from './files.js';
|
|
|
12
12
|
import { workerRunId } from './run-worker.js';
|
|
13
13
|
import { readDocument } from './authoring.js';
|
|
14
14
|
import { withInputsFrom } from './inputs-from.js';
|
|
15
|
+
import { setupFailure } from './failure.js';
|
|
15
16
|
// Older versions are listed only after their saved-package tests pass.
|
|
16
|
-
const supportedPackageRuntimes = new Set([runtimeVersion, '0.11.1', '0.11.0', '0.10.0', '0.9.5', '0.9.4', '0.9.3', '0.7.0', '0.7.1', '0.7.2', '0.8.0', '0.8.1', '0.8.2', '0.8.3']);
|
|
17
|
+
const supportedPackageRuntimes = new Set([runtimeVersion, '0.12.0', '0.11.1', '0.11.0', '0.10.0', '0.9.5', '0.9.4', '0.9.3', '0.7.0', '0.7.1', '0.7.2', '0.8.0', '0.8.1', '0.8.2', '0.8.3']);
|
|
17
18
|
/** A run whose steps all finished: completed, or unconfirmed (an external change could not be confirmed). */
|
|
18
19
|
export const finishedRun = (status) => status === 'completed' || status === 'unconfirmed';
|
|
19
20
|
export async function runSaved(saved, flags, client) {
|
|
@@ -124,7 +125,8 @@ export async function runSaved(saved, flags, client) {
|
|
|
124
125
|
return { run_id: request.run_id, directory, ...result };
|
|
125
126
|
}
|
|
126
127
|
catch (error) {
|
|
127
|
-
|
|
128
|
+
// The run stopped before its first step: setup-error.json holds its failure record.
|
|
129
|
+
writePrivateJson(join(directory, 'setup-error.json'), setupFailure(error));
|
|
128
130
|
setupEvents.push({ at: new Date().toISOString(), type: 'setup_failed', detail: error.message });
|
|
129
131
|
writePrivateJson(setupFile, setupEvents);
|
|
130
132
|
await sync.finish(error);
|
|
@@ -5,6 +5,8 @@ import { resolve, join } from 'node:path';
|
|
|
5
5
|
import { fileURLToPath } from 'node:url';
|
|
6
6
|
import { writePrivateJson } from './files.js';
|
|
7
7
|
import { runDashboard } from './method-sync.js';
|
|
8
|
+
import { runStatus } from './current-inspection.js';
|
|
9
|
+
import { unreadableRecord } from './failure.js';
|
|
8
10
|
/** server: the signed-in Method server, so that the start line links to the run's dashboard page. */
|
|
9
11
|
export async function startRunWorker(args, directory, background = false, server) {
|
|
10
12
|
directory = resolve(directory);
|
|
@@ -117,8 +119,15 @@ export async function waitForRun(directory) {
|
|
|
117
119
|
const status = JSON.parse(readFileSync(file, 'utf8'));
|
|
118
120
|
const dashboard = runDashboard(directory), link = dashboard ? { dashboard } : {};
|
|
119
121
|
if (status.status !== 'running') {
|
|
122
|
+
// The final line gives the run's own status, and the headline of a failure.
|
|
120
123
|
process.exitCode = status.exit_code ?? 1;
|
|
121
|
-
|
|
124
|
+
const summary = existsSync(join(directory, 'summary.json')) ? JSON.parse(readFileSync(join(directory, 'summary.json'), 'utf8')) : undefined;
|
|
125
|
+
const result = runStatus(summary?.status);
|
|
126
|
+
// A worker command that is not a run (no run record) and exits 0 only finished.
|
|
127
|
+
if (!result && process.exitCode === 0)
|
|
128
|
+
return { ...status, ...link };
|
|
129
|
+
const failure = result ? summary.failure : unreadableRecord(Error('The run has no readable summary.json.'));
|
|
130
|
+
return { status: result ?? 'failed', run_dir: directory, exit_code: process.exitCode, ...(failure && result !== 'succeeded' ? { headline: failure.headline } : {}), ...link };
|
|
122
131
|
}
|
|
123
132
|
if (!workerAlive(directory)) {
|
|
124
133
|
process.exitCode = 1;
|
|
@@ -359,13 +359,14 @@ export function cliExecutor(options = {}) {
|
|
|
359
359
|
catch { /* Default. */ }
|
|
360
360
|
return { status: "waiting", question, ...(typeof timeoutMs === "number" ? { timeoutMs } : {}) };
|
|
361
361
|
}
|
|
362
|
-
return { status: "failed", error: { code: "needs_input", message: String(summary.
|
|
362
|
+
return { status: "failed", error: { code: "needs_input", message: String(summary.failure?.headline ?? "The run needs input that a production worker cannot give.") } };
|
|
363
363
|
}
|
|
364
|
+
// Production keeps its own run statuses; the failure record gives the code and the headline.
|
|
364
365
|
if (summary?.status === "failed")
|
|
365
|
-
return { status: "failed", error: { code: summary.code ?? "execution_failed", message: String(summary.
|
|
366
|
+
return { status: "failed", error: { code: summary.failure?.code ?? "execution_failed", message: String(summary.failure?.headline ?? "The run failed.") } };
|
|
366
367
|
const setup = read("setup-error.json");
|
|
367
368
|
if (setup)
|
|
368
|
-
return { status: "failed", error: { code: setup.code, message: String(setup.
|
|
369
|
+
return { status: "failed", error: { code: setup.code, message: String(setup.headline) }, retryable: !finalCodes.has(setup.code) };
|
|
369
370
|
return { status: "failed", error: { code: "worker_failed", message: `The run process stopped (exit ${code}). See ${join(dir, "worker.stderr.log")}.` }, retryable: true };
|
|
370
371
|
};
|
|
371
372
|
}
|