@method-ai-hq/sdk 0.16.0 → 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 CHANGED
@@ -3,7 +3,7 @@
3
3
  MIT-licensed JavaScript API and the `method` CLI. Source and releases: https://github.com/method-ai-hq/method-sdk
4
4
 
5
5
  ```sh
6
- npm install https://github.com/method-ai-hq/method-sdk/releases/download/v0.16.0/method-ai-hq-sdk-0.16.0.tgz
6
+ npm install https://github.com/method-ai-hq/method-sdk/releases/download/v0.17.0/method-ai-hq-sdk-0.17.0.tgz
7
7
  npx method --version
8
8
  ```
9
9
 
@@ -24,7 +24,7 @@ The `draft_tasks` prompt shows two tasks written the way they should be, for
24
24
  contacts that are not in the CRM: a reply to a pricing question and an
25
25
  introduction request. Replace them with two drafts that you sent and liked.
26
26
 
27
- The three browser steps accept the `untrusted_content_can_act` warning: each
27
+ The three browser steps accept the `untrusted_reaches_action` warning: each
28
28
  agent reads pages with a signed-in browser that could also send, and the
29
29
  `accept:` line on each step gives the reason to keep it. Nothing is sent until
30
30
  you review the tasks.
@@ -58,7 +58,7 @@ steps:
58
58
  name: Read campaign email
59
59
  no_effect_reason: Reads the mailbox in the browser; sends, replies to, and deletes nothing.
60
60
  accept:
61
- untrusted_content_can_act: The agent needs the signed-in mailbox to read campaign email. The prompt asks it only to search and read; nothing is sent until a person reviews the tasks.
61
+ untrusted_reaches_action: The agent needs the signed-in mailbox to read campaign email. The prompt asks it only to search and read; nothing is sent until a person reviews the tasks.
62
62
  changes: [environment.browser]
63
63
  in:
64
64
  mailbox_url: target.mailbox_url
@@ -94,7 +94,7 @@ steps:
94
94
  name: Find prospects in Happenstance
95
95
  no_effect_reason: Searches Happenstance in the browser; sends no messages or introduction requests.
96
96
  accept:
97
- untrusted_content_can_act: The agent needs the signed-in Happenstance account to search the network. The prompt asks it only to search; no message or introduction request is sent.
97
+ untrusted_reaches_action: The agent needs the signed-in Happenstance account to search the network. The prompt asks it only to search; no message or introduction request is sent.
98
98
  changes: [environment.browser]
99
99
  after: read_email
100
100
  in:
@@ -132,7 +132,7 @@ steps:
132
132
  name: Check each prospect's pages
133
133
  no_effect_reason: Reads company and LinkedIn pages in the browser; sends no messages or connection requests.
134
134
  accept:
135
- untrusted_content_can_act: The agent needs the signed-in browser to read LinkedIn profiles. The prompt asks it only to read pages; no message or connection request is sent.
135
+ untrusted_reaches_action: The agent needs the signed-in browser to read LinkedIn profiles. The prompt asks it only to read pages; no message or connection request is sent.
136
136
  changes: [environment.browser]
137
137
  after: find_prospects
138
138
  each: {prospect: prospects}
@@ -29,7 +29,7 @@ default connection copies the last-used Chrome profile and runs headless. Run
29
29
 
30
30
  - `no_effect_reason`: the browser step only reads. It sends, posts, and submits
31
31
  nothing, so there is no change to read back.
32
- - `accept: {untrusted_content_can_act: ...}`: `validate` warns when an agent that
32
+ - `accept: {untrusted_reaches_action: ...}`: `validate` warns when an agent that
33
33
  reads web pages could also change things with the same browser. Here the agent
34
34
  needs the signed-in browser to read, and a person reads the briefing before it
35
35
  is shared, so the warning is accepted with that reason.
@@ -18,7 +18,7 @@ steps:
18
18
  gather_sources:
19
19
  no_effect_reason: Reads posts and papers in the browser; sends, posts and submits nothing.
20
20
  accept:
21
- untrusted_content_can_act: The agent needs the signed-in browser to read Grok, alphaXiv, and LinkedIn. The prompt asks it only to search and read, and a person reads the briefing before it is shared.
21
+ untrusted_reaches_action: The agent needs the signed-in browser to read Grok, alphaXiv, and LinkedIn. The prompt asks it only to search and read, and a person reads the briefing before it is shared.
22
22
  name: Find and read sources
23
23
  changes:
24
24
  - environment.browser
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@method-ai-hq/sdk",
3
- "version": "0.16.0",
3
+ "version": "0.17.0",
4
4
  "description": "Build and run reusable .method files with checked steps and saved results.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -35,7 +35,7 @@
35
35
  "prepack": "npm --prefix ../.. run build"
36
36
  },
37
37
  "dependencies": {
38
- "@withmethod/runtime": "https://codeload.github.com/method-ai-hq/method-spec/tar.gz/6020ed9208670972df60a27a602885bcad947c8c",
38
+ "@withmethod/runtime": "https://codeload.github.com/method-ai-hq/method-spec/tar.gz/54461cb387d8f10813db05bb0cea29c0a3a53504",
39
39
  "fflate": "^0.8.3",
40
40
  "yaml": "^2.9.0",
41
41
  "zod": "^3.25.76"
@@ -16,5 +16,5 @@ export declare const costRules = "Before describing execution cost, check how th
16
16
  /** What to watch after a Method works. */
17
17
  export declare const watchRules = "# Watch it\n\n- Look at failed and unconfirmed runs on the dashboard. A run that needs attention says which step failed and why.\n- For classify, watch how often the choice is `unclear` or below your threshold. A rise means the inputs changed.\n- For slow effects such as email delivery, schedule `method observe --pending`.\n";
18
18
  /** What to do after a Method works: production, improvement, and script cards. */
19
- export declare const afterRules = "# After it works\n\n- **Put it in an app:** `method connect APP_FOLDER` publishes it and prints the code to add.\n- **Improve the results:** when the user wants the results changed, `method improve FILE --note TEXT` makes a proposal from the recent runs; `method apply FILE` merges it.\n- **Explain the scripts:** `method explain FILE` writes a card for each script step.\n";
19
+ export declare const afterRules = "# After it works\n\n- **Put it in an app:** `method connect APP_FOLDER` publishes it and prints the code to add.\n- **Record each reaction:** after you show a result, run `method judge RUN_ID wrong --note \"THEIR WORDS\"` (with `--corrected FILE` when they gave the right result) when the user corrects it or asks to change the Method because of it, and `method judge RUN_ID right` when they use or approve it. Do not ask them to confirm it. Method fixes the Method from wrong judgments; `method changes FILE` lists what it changed. For a Method that keeps run content on this computer, `method improve FILE --note TEXT` makes a local proposal and `method apply FILE` merges it.\n- **Explain the scripts:** `method explain FILE` writes a card for each script step.\n";
20
20
  export declare const exampleSelection = "After choosing the step types, read the examples that fit the design. Use their syntax; choose the steps for this task yourself.";
@@ -150,7 +150,7 @@ export const watchRules = `# Watch it
150
150
  export const afterRules = `# After it works
151
151
 
152
152
  - **Put it in an app:** \`method connect APP_FOLDER\` publishes it and prints the code to add.
153
- - **Improve the results:** when the user wants the results changed, \`method improve FILE --note TEXT\` makes a proposal from the recent runs; \`method apply FILE\` merges it.
153
+ - **Record each reaction:** after you show a result, run \`method judge RUN_ID wrong --note "THEIR WORDS"\` (with \`--corrected FILE\` when they gave the right result) when the user corrects it or asks to change the Method because of it, and \`method judge RUN_ID right\` when they use or approve it. Do not ask them to confirm it. Method fixes the Method from wrong judgments; \`method changes FILE\` lists what it changed. For a Method that keeps run content on this computer, \`method improve FILE --note TEXT\` makes a local proposal and \`method apply FILE\` merges it.
154
154
  - **Explain the scripts:** \`method explain FILE\` writes a card for each script step.
155
155
  `;
156
156
  export const exampleSelection = "After choosing the step types, read the examples that fit the design. Use their syntax; choose the steps for this task yourself.";
@@ -1,5 +1,5 @@
1
1
  import { type MethodClient } from './method-client.js';
2
- /** run_id: the run that the note is about; an account run ID for an account improvement, a run folder for one on this computer. */
2
+ /** run_id: the run folder on this computer that the note is about. */
3
3
  export type ImproveRequest = {
4
4
  step_id?: string;
5
5
  note?: string;
@@ -27,11 +27,14 @@ type ProposalKind = 'change' | 'case' | 'rubric' | 'script' | 'none';
27
27
  * proposed workflow; a case or rubric fix has case_suggestion; a script change has script_change; none has neither.
28
28
  */
29
29
  export declare function readImproveResult(result: unknown): ImproveProposal;
30
+ export declare const judgmentsLine = "Method fixes this Method from your judgments. Record the person's reaction: method judge RUN_ID wrong --note \"...\".";
30
31
  /**
31
- * `method improve FILE`. A Method with account run data starts an improvement run in the account. A Method with device
32
- * run data runs improve.method on this computer, so its run content stays here, and saves a local proposal.
32
+ * `method improve FILE`. A Method that keeps run content in the account is fixed from the person's judgments
33
+ * (method judge), so this prints that and starts nothing. A Method with device run data (run_data: device, or an
34
+ * organization that keeps run content on devices) runs improve.method on this computer, so its run content stays here,
35
+ * and saves a local proposal.
33
36
  */
34
- export declare function improveCommand(args: string[], clientFactory: (server: string) => MethodClient, run?: RunFile): Promise<boolean>;
37
+ export declare function improveCommand(args: string[], clientFactory: (server: string) => MethodClient, run?: RunFile): Promise<boolean | undefined>;
35
38
  /** Download the published improve.method and its files into the Method cache, once per version. */
36
39
  export declare function cachedImproveMethod(client: MethodClient): Promise<{
37
40
  file: string;
@@ -9,7 +9,7 @@ import { DEFAULT_SERVER } from './method-client.js';
9
9
  import { fileHash } from './method-files.js';
10
10
  import { methodCache } from './prepare.js';
11
11
  import { writePrivateJson } from './files.js';
12
- import { currentVersion, methodContent, methodIdPattern } from './versions.js';
12
+ import { currentVersion, methodContent } from './versions.js';
13
13
  import { localRuns, saveLocalProposal } from './proposals.js';
14
14
  const kinds = ['change', 'case', 'rubric', 'script', 'none'];
15
15
  /**
@@ -50,9 +50,12 @@ const nextStep = (file, id, kind) => ({
50
50
  none: 'No safe change was found.',
51
51
  })[kind];
52
52
  const print = (value) => process.stdout.write(JSON.stringify(value, null, 2) + '\n');
53
+ export const judgmentsLine = 'Method fixes this Method from your judgments. Record the person\'s reaction: method judge RUN_ID wrong --note "...".';
53
54
  /**
54
- * `method improve FILE`. A Method with account run data starts an improvement run in the account. A Method with device
55
- * run data runs improve.method on this computer, so its run content stays here, and saves a local proposal.
55
+ * `method improve FILE`. A Method that keeps run content in the account is fixed from the person's judgments
56
+ * (method judge), so this prints that and starts nothing. A Method with device run data (run_data: device, or an
57
+ * organization that keeps run content on devices) runs improve.method on this computer, so its run content stays here,
58
+ * and saves a local proposal.
56
59
  */
57
60
  export async function improveCommand(args, clientFactory, run) {
58
61
  const { values, positionals } = parseArgs({ args, allowPositionals: true, options: { case: { type: 'string' }, note: { type: 'string' }, step: { type: 'string' }, server: { type: 'string' } } });
@@ -66,24 +69,15 @@ export async function improveCommand(args, clientFactory, run) {
66
69
  if (values.note !== undefined && !values.note.trim())
67
70
  throw Error('Write the correction after --note.');
68
71
  const request = { ...(values.step ? { step_id: values.step } : {}), ...(values.note ? { note: values.note.trim() } : {}), ...(values.case ? { case_id: values.case } : {}) };
69
- // A note is about the newest run of the Method on this computer.
70
- const newest = request.note ? (await localRuns(file))[0] : undefined;
71
72
  const client = clientFactory(values.server ?? DEFAULT_SERVER);
72
73
  if (!client.token())
73
74
  await client.login();
74
- if (document.run_data !== 'device') {
75
- const id = document.id;
76
- if (typeof id !== 'string' || !methodIdPattern.test(id))
77
- throw Error(`${positionals[0]} has no id: line, so your account has no version of it. Run method run ${positionals[0]} once while signed in, then run method improve again.`);
78
- try {
79
- const { improvement } = await client.request(`/api/methods/${encodeURIComponent(id)}/improvements`, 'POST', { ...request, ...(newest?.account_run_id ? { run_id: newest.account_run_id } : {}) });
80
- return print({ improvement, url: `${client.server}/methods/${id}`, next: `Method's improvement agent is working; it takes a few minutes. Run method proposals ${positionals[0]} --wait (give the command 10 minutes): it returns with the proposal. Then show it to the user.` });
81
- }
82
- catch (error) {
83
- if (error.code !== 'device_data')
84
- throw error;
85
- }
75
+ if (document.run_data !== 'device' && !await (await import('./method-sync.js')).organizationKeepsContent(client)) {
76
+ process.stdout.write(judgmentsLine + '\n');
77
+ return;
86
78
  }
79
+ // A note is about the newest run of the Method on this computer.
80
+ const newest = request.note ? (await localRuns(file))[0] : undefined;
87
81
  return print(await improveOnDevice(client, file, { ...request, ...(newest ? { run_id: newest.dir } : {}) }, run));
88
82
  }
89
83
  /** Download the published improve.method and its files into the Method cache, once per version. */
@@ -0,0 +1,20 @@
1
+ import { type MethodClient } from './method-client.js';
2
+ export declare const maxNoteLength = 4000;
3
+ export declare const maxCorrectedBytes: number;
4
+ export type Judgment = {
5
+ id: string;
6
+ run_id: string;
7
+ method_id: string;
8
+ verdict: 'right' | 'wrong';
9
+ step_id: string | null;
10
+ created_at: string;
11
+ };
12
+ /** The corrected result in a file: its JSON value when it parses as JSON, or else its text. */
13
+ export declare function correctedValue(path: string): unknown;
14
+ /** The one line that says what was recorded. */
15
+ export declare function judgmentLine(judgment: Pick<Judgment, 'verdict' | 'step_id'>, fixing: boolean): string;
16
+ /**
17
+ * `method judge RUN_ID right|wrong [--step ID] [--note TEXT] [--corrected FILE] [--json]`: record the person's reaction
18
+ * to a run's result. RUN_ID is the dashboard run ID or this computer's run ID; the server resolves both.
19
+ */
20
+ export declare function judgeCommand(args: string[], clientFactory: (server: string) => MethodClient): Promise<void>;
@@ -0,0 +1,59 @@
1
+ import { readFileSync, statSync } from 'node:fs';
2
+ import { parseArgs } from 'node:util';
3
+ import { DEFAULT_SERVER } from './method-client.js';
4
+ export const maxNoteLength = 4000;
5
+ export const maxCorrectedBytes = 1024 * 1024;
6
+ /** The corrected result in a file: its JSON value when it parses as JSON, or else its text. */
7
+ export function correctedValue(path) {
8
+ let size;
9
+ try {
10
+ size = statSync(path).size;
11
+ }
12
+ catch {
13
+ throw Error(`Cannot read ${path}.`);
14
+ }
15
+ if (size > maxCorrectedBytes)
16
+ throw Error(`${path} is larger than 1 MB. Give a shorter corrected result.`);
17
+ const text = readFileSync(path, 'utf8');
18
+ try {
19
+ return JSON.parse(text);
20
+ }
21
+ catch {
22
+ return text;
23
+ }
24
+ }
25
+ /** The one line that says what was recorded. */
26
+ export function judgmentLine(judgment, fixing) {
27
+ return `Recorded: ${judgment.verdict}${judgment.step_id ? ` on step ${judgment.step_id}` : ''}.${fixing ? ' Method is fixing it in the background.' : ''}`;
28
+ }
29
+ /**
30
+ * `method judge RUN_ID right|wrong [--step ID] [--note TEXT] [--corrected FILE] [--json]`: record the person's reaction
31
+ * to a run's result. RUN_ID is the dashboard run ID or this computer's run ID; the server resolves both.
32
+ */
33
+ export async function judgeCommand(args, clientFactory) {
34
+ const usage = 'Use method judge RUN_ID right|wrong [--step STEP_ID] [--note TEXT] [--corrected FILE].';
35
+ const { values, positionals } = parseArgs({ args, allowPositionals: true, options: {
36
+ step: { type: 'string' }, note: { type: 'string' }, corrected: { type: 'string' }, json: { type: 'boolean' }, server: { type: 'string' }
37
+ } });
38
+ const [runId, verdict] = positionals;
39
+ if (!runId || !verdict || positionals.length !== 2)
40
+ throw Error(usage);
41
+ if (verdict !== 'right' && verdict !== 'wrong')
42
+ throw Error(`Judge the result right or wrong, not ${verdict}. ${usage}`);
43
+ const note = values.note?.trim();
44
+ if (values.note !== undefined && !note)
45
+ throw Error('Write the person\'s words after --note.');
46
+ if (note && note.length > maxNoteLength)
47
+ throw Error(`The note has ${note.length} characters; the most is ${maxNoteLength}.`);
48
+ if (values.step !== undefined && !values.step.trim())
49
+ throw Error('Write the step ID after --step.');
50
+ const body = { verdict, ...(values.step ? { step_id: values.step.trim() } : {}), ...(note ? { note } : {}),
51
+ ...(values.corrected !== undefined ? { corrected: correctedValue(values.corrected) } : {}) };
52
+ const client = clientFactory(values.server ?? DEFAULT_SERVER);
53
+ const response = await client.request(`/api/cli/runs/${encodeURIComponent(runId)}/judgments`, 'POST', body);
54
+ if (values.json) {
55
+ process.stdout.write(JSON.stringify(response, null, 2) + '\n');
56
+ return;
57
+ }
58
+ process.stdout.write(judgmentLine(response.judgment, !!response.fixing) + '\n');
59
+ }
@@ -12,7 +12,7 @@ export declare const commandHelp: Record<string, Command>;
12
12
  export declare function renderCommand(name: string, includeCommon?: boolean): string;
13
13
  export declare const guideTopics: readonly ["start", "concepts", "execution", "examples", "example", "recipes", "recovery", "commands"];
14
14
  export declare function authoringGuide(topic?: string, exampleId?: string): string;
15
- export declare const overviewHelp = "Method \u2014 author, save and run methods with your coding agent.\n\nBefore creating, editing, or proposing a Method, run `method authoring` and read its guidance. Do this before choosing an existing Method as a reference.\n\nLearn: method authoring Start the installed guide.\n method authoring all Read the full manual offline.\n method COMMAND --help Read arguments, results and examples.\n method help step add Read help for a subcommand.\n method schema [TYPE] Read a formal JSON schema.\n\nLocal drafts: init, show, set, remove, step add/update/remove/move,\n check set/remove, validate, diff.\nOnline documents: list, get, steps, step, publish, explain.\nImprovements: improve, proposals, apply.\nKeys for scripts: secret find, secret import, secret set, secret list.\nExecution and evidence: run, run-status, wait, cancel, runs, logs, sync, observe.\nEffects and corrections: effect add/list, case new/retire/list, test.\nInputs and shared state: bind, state.\nBrowser and computer setup: browser connect, config.\nAccount access: status, login, logout, devices, revoke.\nProduction runs: connect, answer, keys create/list/revoke, worker.\n\nSign in with method login first. Read the authoring guide, give each prompt its own step, write the files, validate, and run. Each signed-in run saves a version when the file changed. Publish a version when the user wants to share or schedule it.\nUse method run task.method to run it.\nOnline server: https://app.withmethod.ai. Use --server only to select another\nserver. Sign-in uses browser approval. Never send credentials in chat.\n";
15
+ export declare const overviewHelp = "Method \u2014 author, save and run methods with your coding agent.\n\nBefore creating, editing, or proposing a Method, run `method authoring` and read its guidance. Do this before choosing an existing Method as a reference.\n\nLearn: method authoring Start the installed guide.\n method authoring all Read the full manual offline.\n method COMMAND --help Read arguments, results and examples.\n method help step add Read help for a subcommand.\n method schema [TYPE] Read a formal JSON schema.\n\nLocal drafts: init, show, set, remove, step add/update/remove/move,\n check set/remove, validate, diff.\nOnline documents: list, get, steps, step, publish, explain.\nResults and improvements: judge, changes, improve, apply.\nKeys for scripts: secret find, secret import, secret set, secret list.\nExecution and evidence: run, run-status, wait, cancel, runs, logs, sync, observe.\nEffects and corrections: effect add/list, case new/retire/list, test.\nInputs and shared state: bind, state.\nBrowser and computer setup: browser connect, config.\nAccount access: status, login, logout, devices, revoke.\nProduction runs: connect, answer, keys create/list/revoke, worker.\n\nSign in with method login first. Read the authoring guide, give each prompt its own step, write the files, validate, and run. Each signed-in run saves a version when the file changed. Publish a version when the user wants to share or schedule it.\nUse method run task.method to run it.\nOnline server: https://app.withmethod.ai. Use --server only to select another\nserver. Sign-in uses browser approval. Never send credentials in chat.\n";
16
16
  /** Resolve help before parsing flags, creating a client, or touching any draft. */
17
17
  export declare function methodHelp(args: string[]): string | undefined;
18
18
  /** The CLI and published reference use this same command inventory. */
@@ -36,9 +36,10 @@ export const commandHelp = {
36
36
  get: { usage: "method get WORKFLOW_ID [--version VERSION_ID] [--out FILE] [--server URL]", purpose: "Read an online method; optionally make a local draft for editing.", arguments: "Default: latest version. --out must name a new file. Use a version ID to read an exact saved version.", result: "Without --out: {workflow_id,version_id,version_number,workflow}. With --out: {workflow_id,version_id,version_number,file}; writes the workflow with its id: line and the saved files.", errors: "Missing method or version; output file already exists.", example: "method get wf_example --out edit.method", remote: true },
37
37
  explain: { usage: "method explain FILE [--step ID] [--no-round-trip] [--server URL]", purpose: "Explain each script step of the saved version in a script card.", arguments: "FILE is a local Method whose current content is a saved version (run or publish it while signed in). For each run step whose script has no card, code analysis finds the hosts, secrets, environment variables, files, and commands the script uses; a hosted model writes a one-sentence summary and 3 to 8 steps; every number, quoted name, and host in that text must appear in the code (one retry). The round trip writes a program from the steps alone and replays up to 3 recorded inputs of the step from local runs through both programs without network access. A script that calls the network is not replayed. --step explains one step. --no-round-trip skips the replay. A signed-in run starts this in the background; publish waits for it.", result: "One line per step with its summary and check results. Cards upload to the version; the dashboard shows them beside each script. A failed check is shown on the card and never blocks publish.", errors: "No saved version, local files that differ from the saved version, or no hosted-model credit. Without sign-in it prints one line and makes no card.", example: "method explain leads.method --step score", remote: true },
38
38
  publish: { usage: "method publish FILE [--reason TEXT] [--accept-failing-case ID[,ID]] [--cloud|--workers] [--env NAME] [--server URL]", purpose: "Mark a version as published, for sharing, schedules, and production runs.", arguments: "Runs the Method's cases first; a version that breaks an approved case is refused unless --accept-failing-case names it, and the version records that. Uses the version of the file's current content: it finishes that upload (no time limit while bytes move), checks the cases, then marks it published with the reason (default: Published.). --accept-failing-case needs --reason. The first save of a new file creates the Method. --cloud runs the production runs of the environment on Method Cloud; --workers runs them on your workers again. --env NAME selects the environment (default production).", result: "JSON with workflow_id, version_id, url, published_at, the cases line, and the open warnings and notes (issues). With --cloud or --workers: placement. Warnings and notes never block a publish.", errors: "An error issue (nothing is published), failing case, --accept-failing-case without --reason, or no network.", example: "method publish task.method --reason 'Shorter introduction'", remote: true },
39
- improve: { usage: "method improve FILE [--case ID] [--note TEXT] [--step ID] [--server URL]", purpose: "Ask Method to propose a better version of a Method.", arguments: "--note: the correction, in your words; it is about the newest run of the Method on this computer, which the improvement reads and a suggested case records. --step: improve only this step. --case: the case the change must pass. A Method with account run data improves in your account; see the progress on the dashboard. A Method with run_data: device improves on this computer, so its run content stays here; the result is a local proposal in .method/proposals/.", result: "JSON with the improvement and the dashboard link, or the local proposal ID. Nothing is published, and the file does not change.", errors: "No id: line (run the file once while signed in), an unknown step, or an improvement that is already running.", example: "method improve task.method --step summary --note 'Name the customer in the first sentence.'", remote: true },
40
- proposals: { usage: "method proposals FILE [--wait] [--server URL]", purpose: "List the proposals of a Method.", arguments: "Lists the proposals in your account and the local proposals beside the file. --wait: when an improvement is running, wait until it ends (usually a few minutes), printing each step it reaches.", result: "JSON list of proposals with id, source, status, kind, cause, and steps, and the newest improvement that has no proposal yet, with its status. An improvement that found no safe change has outcome none, and next says what to do instead. No changes.", errors: "", example: "method proposals task.method", remote: true },
41
- apply: { usage: "method apply FILE [PROPOSAL_ID] [--resolved] [--server URL]", purpose: "Merge a proposal into the local file.", arguments: "Default: the newest accepted proposal, or else the newest local proposal. The merge is per step: a part that only the proposal changed takes the change; a part that you and the proposal both changed is a conflict. --resolved: you merged a conflict by hand; this marks the proposal applied. Commands that take a signed-in FILE apply an accepted proposal first and print one line.", result: "JSON with status applied and the changed steps; the file changes and the proposal is marked applied. Nothing is published. A conflict lists each part with the base, your, and the proposed value, changes nothing, and exits 1.", errors: "No proposal, a proposal that is not accepted, or a conflict.", example: "method apply task.method", remote: true },
39
+ judge: { usage: "method judge RUN_ID right|wrong [--step STEP_ID] [--note TEXT] [--corrected FILE] [--json] [--server URL]", purpose: "Record the person's reaction to a run's result.", arguments: "RUN_ID is the run_id of the run's start line, or the run ID on the dashboard. right: the person used or approved the result. wrong: the person corrected it, or asked to change the Method because of it. --note: the person's words (at most 4000 characters). --corrected FILE: the result that the person says is right; JSON is sent as JSON, other text as text (at most 1 MB). --step: the step whose output was wrong. A wrong judgment makes Method fix the Method in the background: it applies a tested change that the person can undo, or changes nothing. Do not ask the person to confirm a judgment.", result: "One line, for example Recorded: wrong on step choose. Method is fixing it in the background. --json prints the judgment and fixing.", errors: "An unknown run, a run of a Method that is not in your account, or no sign-in.", example: "method judge 6f1c… wrong --step summary --note 'Name the customer in the first sentence.'", remote: true },
40
+ changes: { usage: "method changes FILE [--json] [--server URL]", purpose: "List the changes that Method applied to a Method.", arguments: "Lists the changes that Method made from wrong judgments, newest first, and the local proposals applied on this computer. Commands that take a signed-in FILE merge a new change into the file first and print one line.", result: "One line per change: its date, its version, and its cause. --json prints {file, method_id, changes: [{id, source, cause, date, version_id, steps}]}. No changes.", errors: "No id: line and no local change.", example: "method changes task.method", remote: true },
41
+ improve: { usage: "method improve FILE [--case ID] [--note TEXT] [--step ID] [--server URL]", purpose: "Improve a Method that keeps its run content on this computer.", arguments: "For a Method with run_data: device, or in an organization that keeps run content on devices: runs Method's improvement on this computer, so the run content stays here. --note: the correction, in the person's words; it is about the newest run of the Method on this computer, which the improvement reads and a suggested case records. --step: improve only this step. --case: the case the change must pass. For a Method that keeps run content in the account, it starts nothing and says to use method judge: Method fixes those Methods from the person's judgments.", result: "JSON with the local proposal ID and the next step; the proposal is in .method/proposals/. Nothing is published, and the file does not change. For an account Method, one line.", errors: "An unknown step, or a step of the improvement that fails.", example: "method improve task.method --step summary --note 'Name the customer in the first sentence.'", remote: true },
42
+ apply: { usage: "method apply FILE [PROPOSAL_ID] [--resolved] [--server URL]", purpose: "Merge a local proposal from method improve into the file.", arguments: "Default: the newest open local proposal. The merge is per step: a part that only the proposal changed takes the change; a part that you and the proposal both changed is a conflict. --resolved: you merged a conflict by hand; this marks the proposal applied. When a change that Method applied conflicts with your edits, the line before the command names it; give its ID to see both versions.", result: "JSON with status applied and the changed steps; the file changes and the proposal is marked applied. Nothing is published. A conflict lists each part with the base, your, and the proposed value, changes nothing, and exits 1.", errors: "No open proposal, or a conflict.", example: "method apply task.method", remote: true },
42
43
  secret: { usage: "method secret find [FILE]\nmethod secret import FILE NAME...\nmethod secret set NAME\nmethod secret list [NAME...]", purpose: "Give this computer the values of a Method's declared secrets.", arguments: "find lists the KEY=VALUE files in this folder (3 levels down) and in the parent folder (2 levels down) with the key names in each, never values; for the Method FILE (or the only Method here) it says which declared secrets each file holds and prints the import command. import copies the named values from a KEY=VALUE file that the user names, without printing them. set opens a private form on 127.0.0.1 in the browser for one value. list shows names and where each value is found (shell, this computer, or missing), never values. Values are kept in ~/.config/method/secrets.json (mode 0600) and are never sent to Method. A value exported in the shell is used first. Every script of the Method receives all of its declared secrets.", result: "JSON with the saved names, or the list.", errors: "A name is missing from the file, or no value was entered.", example: "method secret find\nmethod secret import ../service/.env ARCHIVE_TOKEN" },
43
44
  connect: { usage: "method connect [APP_FOLDER] [--file METHOD_FILE]", purpose: "Connect an app to the Method in this folder, so the app runs it in production.", arguments: "Run it in the Method's folder. APP_FOLDER (default: this folder) is the app's folder. It makes a service key named after the app folder and writes METHOD_API_KEY to the app's .env (a new file, or a new or replaced line); it never prints the key. It adds .env to .gitignore in a git repository. When the Method has no published version, it publishes the current file. It detects Node (package.json) or Python (pyproject.toml, requirements.txt), and prints the install line and the code to add. Run it again: an app with a working key keeps it.", result: "JSON with the method_id, the published version, where the key is, install, code, secrets_to_set_on_the_host, and next.", errors: "No or several .method files (give --file), an app folder with no language marker, or a failing case at publish.", example: "method connect ../app", remote: true },
44
45
  answer: { usage: "method answer RUN_ID [--answer JSON]", purpose: "Show or answer the question of a production run that waits on an ask step.", arguments: "Without --answer it prints the question and its form (the fields of the answer). With --answer it checks the answer against the form and sends it; the run continues. A run that keeps its content on devices has a sealed question: run this where the app's METHOD_API_KEY is (the environment, or the .env of this folder), or on the laptop that started the run with its sign-in. Local runs (method run FILE) take answers with --human FILE as before.", result: "The question and form, or the run after the answer.", errors: "The run is not waiting, the answer does not match the form, or the key cannot open a sealed question (wrong_key).", example: "method answer 6f1c… --answer '{\"approved\":true}'", remote: true },
@@ -299,7 +300,7 @@ const recipes = `# Recipes
299
300
  ## Corrections become cases
300
301
 
301
302
  When the user says a result was wrong:
302
- 1. Read the run (result.json, events.jsonl) and fix the Method. Run it again and show the user the new result.
303
+ 1. Record it: method judge RUN_ID wrong --note "the user's words" (--corrected FILE when they gave the right result). Do not ask them to confirm it. Then read the run (result.json, events.jsonl) and fix the Method. Run it again and show the user the new result.
303
304
  2. If the user wants the fix to stay, ask once: "Keep this as a rule: <the rule in plain words>?"
304
305
  3. On yes, record it: method case new FILE --id ID --run BAD_RUN --passing-run NEW_RUN --note "the user's words" --rubric "plain sentence that must be true" (repeat --rubric for each rule). --ref names the output to judge (default: the Method's result; a path to a file that the run saved is judged by that file's contents). When a rule compares the output with the sources, add --context with the step output that holds them, for example --context outputs.material. Make each sentence fail on the bad run; case new warns about one that does not. For an exact value, use --expect instead.
305
306
  4. Run method test FILE. Every case must pass. method publish refuses a version that breaks a case.
@@ -370,7 +371,7 @@ Learn: method authoring Start the installed guide.
370
371
  Local drafts: init, show, set, remove, step add/update/remove/move,
371
372
  check set/remove, validate, diff.
372
373
  Online documents: list, get, steps, step, publish, explain.
373
- Improvements: improve, proposals, apply.
374
+ Results and improvements: judge, changes, improve, apply.
374
375
  Keys for scripts: secret find, secret import, secret set, secret list.
375
376
  Execution and evidence: run, run-status, wait, cancel, runs, logs, sync, observe.
376
377
  Effects and corrections: effect add/list, case new/retire/list, test.
@@ -62,7 +62,9 @@ export async function methodMain(args = process.argv.slice(2), clientFactory = s
62
62
  process.stdout.write(earlyHelp);
63
63
  return;
64
64
  }
65
- if (['improve', 'proposals', 'apply'].includes(args[0] ?? ''))
65
+ if (args[0] === 'judge')
66
+ return (await import('./judge.js')).judgeCommand(args.slice(1), clientFactory);
67
+ if (['improve', 'changes', 'apply'].includes(args[0] ?? ''))
66
68
  return (await import('./proposals.js')).proposalCommand(args, clientFactory);
67
69
  await (await import('./proposals.js')).autoApply(args, clientFactory);
68
70
  if (args[0] === 'worker' || args[0] === 'keys')
@@ -25,11 +25,21 @@ type Check = {
25
25
  /** Which part of the step the check reads; undefined when the check does not apply to the step. */
26
26
  script?: (step: Step) => string | undefined;
27
27
  applies: (step: Step) => boolean;
28
+ /** More inputs for Jev, from the Method (for example the definitions of an agent step's tools). */
29
+ context?: (step: Step, method: any) => Record<string, string>;
30
+ /** A finding (true) or none (false) that code analysis decides without a question to Jev; undefined asks Jev. */
31
+ decide?: (inputs: Record<string, string>) => boolean | undefined;
28
32
  field: string;
29
33
  message: (id: string) => string;
30
34
  fix: string;
31
35
  };
32
36
  export declare const checks: Record<string, Check>;
37
+ /**
38
+ * The script scan of a run step (explain-effects): a network host or a write is a finding with no model call. No host,
39
+ * no write, and no library that can send: no finding. A library that can send or a subprocess with no host found:
40
+ * undefined, so Jev answers.
41
+ */
42
+ export declare function scanDecision(stepYaml: string, script: string): boolean | undefined;
33
43
  /**
34
44
  * Checks shown to the user. A check moves here from shadow mode only with a results.json that shows its precision
35
45
  * on the frozen held-out set meets its level (warning >= 0.90, note >= 0.75). Run npm run eval:checks first.
@@ -78,7 +88,14 @@ export declare function askCheck(provider: ClassificationProvider, question: str
78
88
  probability: number;
79
89
  model: string;
80
90
  }>;
81
- export declare function evaluateTarget(provider: ClassificationProvider, target: Target, signal: AbortSignal): Promise<Result>;
91
+ /** The answer of code analysis, when the check has one for these inputs. */
92
+ export declare function decided(check: string, inputs: Record<string, string>): {
93
+ answer: boolean;
94
+ probability: number;
95
+ model: string;
96
+ } | undefined;
97
+ /** Answer one target: code analysis when it decides, or else Jev. scanned: code analysis already found it cannot decide. */
98
+ export declare function evaluateTarget(provider: ClassificationProvider | undefined, target: Target, signal: AbortSignal, scanned?: boolean): Promise<Result>;
82
99
  /**
83
100
  * Run the targets in parallel. Cached answers come at once; new ones get waitMs in all. The others are returned as
84
101
  * pending, so the caller can finish them in the background. A failed check is left out and is tried again next time.
@@ -8,7 +8,11 @@ import { existsSync, mkdirSync, readFileSync, readdirSync, renameSync, writeFile
8
8
  import { join } from 'node:path';
9
9
  import { serializeWorkflow } from '../../workflow-language/src/validate.js';
10
10
  import { methodCache } from './prepare.js';
11
+ import { scriptLanguage, staticEffects } from './explain-effects.js';
11
12
  const promptKinds = ['call', 'agent'];
13
+ /** A step that declares a change outside the run: a connection or service in changes, or an effect. */
14
+ const declaresWorldChange = (step) => (step.changes ?? []).some((ref) => typeof ref === 'string' && ref.startsWith('environment.')) || Object.keys(step.effects ?? {}).length > 0;
15
+ const agentWithTools = (step) => step.do?.kind === 'agent' && ((step.do.tools ?? []).length > 0 || !!step.do.browser);
12
16
  export const checks = {
13
17
  script_calls_model: {
14
18
  version: 1, level: 'warning', field: 'do',
@@ -52,7 +56,48 @@ export const checks = {
52
56
  message: id => `The prompt of ${id} appears to need information that the step does not give it.`,
53
57
  fix: 'Add the missing value to in: from an input or an earlier step, or write it in the prompt.',
54
58
  },
59
+ undeclared_change: {
60
+ version: 1, level: 'warning', field: 'do',
61
+ question: 'Can this step send, post, or change something outside its own output folder, for example send an email or a message, create or update a record in a service or an API, submit a form, or write files elsewhere, while its changes: and effects: declare no such change? Reading, searching, downloading, and returning a result are not changes.',
62
+ applies: step => !declaresWorldChange(step) && (step.do?.kind === 'run' || agentWithTools(step)),
63
+ script: step => step.do?.kind === 'run' ? step.do.entrypoint : undefined,
64
+ context: (step, method) => step.do?.kind === 'agent' && (step.do.tools ?? []).length
65
+ ? { tools: limit(serializeWorkflow(Object.fromEntries(step.do.tools.map(name => [name, method.tools?.[name] ?? null]))), 16_000) } : {},
66
+ decide: inputs => inputs.script === undefined ? undefined : scanDecision(inputs.step ?? "", inputs.script),
67
+ message: id => `${id} can send or change things that it does not declare.`,
68
+ fix: 'Declare what it changes in changes:, so Method can check where its data goes.',
69
+ },
55
70
  };
71
+ /** Libraries and calls that can send or change something, but whose target code analysis may not find. */
72
+ const senders = {
73
+ python: /^\s*(?:import|from)\s+(?:requests|httpx|aiohttp|urllib3?|http\.client|smtplib|email\.mime|boto3|slack_sdk|twilio|sendgrid|googleapiclient|subprocess)\b|\bos\.(?:system|popen)\(/m,
74
+ node: /\bfetch\(|\brequire\(\s*['"](?:node:)?(?:https?|child_process|axios|node-fetch|got|undici|nodemailer|@sendgrid\/mail|@slack\/web-api|twilio)['"]|\bfrom\s+['"](?:node:)?(?:https?|child_process|axios|node-fetch|got|undici|nodemailer|@sendgrid\/mail|@slack\/web-api|twilio)['"]/,
75
+ };
76
+ /** Temporary files are not a change in the world. */
77
+ const temporaryPath = (path) => /^(?:\/tmp\/|\/var\/folders\/|\/private\/tmp\/)/.test(path);
78
+ /**
79
+ * The script scan of a run step (explain-effects): a network host or a write is a finding with no model call. No host,
80
+ * no write, and no library that can send: no finding. A library that can send or a subprocess with no host found:
81
+ * undefined, so Jev answers.
82
+ */
83
+ export function scanDecision(stepYaml, script) {
84
+ const entrypoint = /^\s*entrypoint:\s*["']?([^\s"']+)/m.exec(stepYaml)?.[1];
85
+ const language = entrypoint ? scriptLanguage(entrypoint) : undefined;
86
+ if (!language || !script.trim())
87
+ return undefined;
88
+ let effects;
89
+ try {
90
+ effects = staticEffects(language, [{ path: entrypoint, text: script }], undefined).effects;
91
+ }
92
+ catch {
93
+ return undefined;
94
+ }
95
+ if (effects.network.length || effects.writes.some(path => !temporaryPath(path)))
96
+ return true;
97
+ if (effects.runs.length || senders[language].test(script))
98
+ return undefined;
99
+ return false;
100
+ }
56
101
  /**
57
102
  * Checks shown to the user. A check moves here from shadow mode only with a results.json that shows its precision
58
103
  * on the frozen held-out set meets its level (warning >= 0.90, note >= 0.75). Run npm run eval:checks first.
@@ -78,7 +123,7 @@ export function checkTargets(method, readText, steps) {
78
123
  if (!check.applies(step))
79
124
  continue;
80
125
  const path = check.script?.(step);
81
- const inputs = { step: stepText(id, step) };
126
+ const inputs = { step: stepText(id, step), ...check.context?.(step, method) };
82
127
  if (path)
83
128
  inputs.script = limit(readText(path) ?? '', 40_000);
84
129
  targets.push({ check: name, version: check.version, step: id, inputs, key: checkKey(name, check.version, inputs) });
@@ -86,7 +131,7 @@ export function checkTargets(method, readText, steps) {
86
131
  }
87
132
  return targets;
88
133
  }
89
- export const checkKey = (check, version, inputs) => createHash('sha256').update(JSON.stringify([check, version, inputs.step, inputs.script ?? ''])).digest('hex');
134
+ export const checkKey = (check, version, inputs) => createHash('sha256').update(JSON.stringify([check, version, inputs.step, inputs.script ?? '', ...(inputs.tools !== undefined ? [inputs.tools] : [])])).digest('hex');
90
135
  export const checksDir = () => join(methodCache(), 'checks');
91
136
  export function cached(key) {
92
137
  const file = join(checksDir(), `${key}.json`);
@@ -108,14 +153,24 @@ function store(result) {
108
153
  * probability is that of yes.
109
154
  */
110
155
  export const answerOptions = { yes: 'Yes: the question describes this step.', no: 'No: the question does not describe this step.' };
156
+ const noProvider = () => { throw Error('Model checks need sign-in.'); };
111
157
  export async function askCheck(provider, question, inputs, signal) {
112
158
  const { model } = await provider.resolve(signal);
113
159
  const answer = await provider.evaluate({ request_id: randomUUID(), model, question, options: answerOptions, inputs }, signal);
114
160
  const probability = answer.probabilities.yes ?? 0;
115
161
  return { answer: probability >= findingThreshold, probability, model };
116
162
  }
117
- export async function evaluateTarget(provider, target, signal) {
118
- const answer = await askCheck(provider, checks[target.check].question, target.inputs, signal);
163
+ /** The answer of code analysis, when the check has one for these inputs. */
164
+ export function decided(check, inputs) {
165
+ const answer = checks[check]?.decide?.(inputs);
166
+ return answer === undefined ? undefined : { answer, probability: answer ? 1 : 0, model: 'script-scan' };
167
+ }
168
+ /** Answer one target: code analysis when it decides, or else Jev. scanned: code analysis already found it cannot decide. */
169
+ export async function evaluateTarget(provider, target, signal, scanned = false) {
170
+ const answer = (scanned ? undefined : decided(target.check, target.inputs)) ?? await askCheck(provider ?? noProvider(), checks[target.check].question, target.inputs, signal);
171
+ return record(target, answer);
172
+ }
173
+ function record(target, answer) {
119
174
  const result = { check: target.check, version: target.version, step: target.step, key: target.key, answer: answer.probability >= findingThreshold,
120
175
  probability: answer.probability, model: answer.model, at: new Date().toISOString() };
121
176
  store(result);
@@ -127,15 +182,20 @@ export async function evaluateTarget(provider, target, signal) {
127
182
  */
128
183
  export async function runChecks(provider, targets, waitMs = 3000) {
129
184
  const done = [], pending = [];
130
- const open = targets.filter(target => { const hit = cached(target.key); if (hit)
131
- done.push(hit); return !hit; });
185
+ // Answers that code analysis decides need no provider and no wait.
186
+ const open = targets.filter(target => {
187
+ const hit = cached(target.key) ?? (() => { const answer = decided(target.check, target.inputs); return answer && record(target, answer); })();
188
+ if (hit)
189
+ done.push(hit);
190
+ return !hit;
191
+ });
132
192
  if (!open.length)
133
193
  return { done, pending };
134
194
  if (!provider)
135
195
  return { done, pending: open };
136
196
  const controller = new AbortController();
137
197
  const settled = new Set();
138
- const work = open.map(target => evaluateTarget(provider, target, controller.signal)
198
+ const work = open.map(target => evaluateTarget(provider, target, controller.signal, true)
139
199
  .then(result => { done.push(result); }).catch(() => { }).finally(() => settled.add(target.key)));
140
200
  let timer;
141
201
  await Promise.race([Promise.all(work), new Promise(resolve => { timer = setTimeout(resolve, waitMs); })]);
@@ -58,9 +58,9 @@ type Applied = {
58
58
  /** Merge a proposal into the file. A clean merge writes the file and marks the proposal applied. Never publishes. */
59
59
  export declare function applyProposal(client: MethodClient | undefined, file: string, proposal: ProposalRecord, options?: RequestOptions): Promise<Applied>;
60
60
  /**
61
- * Before a command that takes a .method FILE: when this computer is signed in and the Method has an accepted proposal,
62
- * merge it into the file and print one line. A conflict prints what the coding agent must resolve; the command goes on.
63
- * Offline, signed out, or on any error, it does nothing.
61
+ * Before a command that takes a .method FILE: when this computer is signed in and Method applied a change to the Method
62
+ * (an accepted proposal, from the person's judgments), merge it into the file and print one line. A conflict prints
63
+ * what the coding agent must resolve; the command goes on. Offline, signed out, or on any error, it does nothing.
64
64
  */
65
65
  export declare function autoApply(args: string[], clientFactory: (server: string) => MethodClient): Promise<void>;
66
66
  /** Run folders of this Method on this computer, newest first, with the account run ID of each run that was saved to the account. */
@@ -68,6 +68,6 @@ export declare function localRuns(file: string): Promise<{
68
68
  dir: string;
69
69
  account_run_id: string | null;
70
70
  }[]>;
71
- /** `method improve`, `method proposals`, and `method apply`. */
71
+ /** `method improve`, `method changes`, and `method apply`. */
72
72
  export declare function proposalCommand(args: string[], clientFactory: (server: string) => MethodClient): Promise<boolean | undefined>;
73
73
  export {};
@@ -133,11 +133,11 @@ async function markApplied(client, file, proposal, options = {}) {
133
133
  }
134
134
  }
135
135
  const serverFlag = (args) => { const at = args.findIndex(arg => arg === '--server' || arg.startsWith('--server=')); return at < 0 ? undefined : args[at].startsWith('--server=') ? args[at].slice(9) : args[at + 1]; };
136
- const skipAutoApply = new Set(['apply', 'proposals', 'improve', 'init', 'new-id', 'diff', 'help', 'authoring', 'schema', '__worker', '__checks', 'worker', 'keys', 'secret', 'models', 'login', 'logout', 'sync', 'progress']);
136
+ const skipAutoApply = new Set(['apply', 'changes', 'judge', 'improve', 'init', 'new-id', 'diff', 'help', 'authoring', 'schema', '__worker', '__checks', 'worker', 'keys', 'secret', 'models', 'login', 'logout', 'sync', 'progress']);
137
137
  /**
138
- * Before a command that takes a .method FILE: when this computer is signed in and the Method has an accepted proposal,
139
- * merge it into the file and print one line. A conflict prints what the coding agent must resolve; the command goes on.
140
- * Offline, signed out, or on any error, it does nothing.
138
+ * Before a command that takes a .method FILE: when this computer is signed in and Method applied a change to the Method
139
+ * (an accepted proposal, from the person's judgments), merge it into the file and print one line. A conflict prints
140
+ * what the coding agent must resolve; the command goes on. Offline, signed out, or on any error, it does nothing.
141
141
  */
142
142
  export async function autoApply(args, clientFactory) {
143
143
  if (!args[0] || skipAutoApply.has(args[0]) || args.includes('--help') || process.env.METHOD_RUN_WORKER === '1')
@@ -159,12 +159,12 @@ export async function autoApply(args, clientFactory) {
159
159
  const result = await applyProposal(client, file, proposal, options);
160
160
  if (result.status === 'applied') {
161
161
  if (result.steps.length || result.warning)
162
- process.stderr.write(`Applied accepted proposal ${proposal.id} to ${target}: ${proposal.cause}${result.steps.length ? ` Changed steps: ${result.steps.join(', ')}.` : ''} Not published.${result.warning ? ` ${result.warning}` : ''}\n`);
162
+ process.stderr.write(`Method changed ${target} (${proposal.id}): ${proposal.cause}${result.steps.length ? ` Changed steps: ${result.steps.join(', ')}.` : ''}${result.warning ? ` ${result.warning}` : ''}\n`);
163
163
  }
164
164
  else
165
165
  process.stderr.write(result.status === 'conflict'
166
- ? `Accepted proposal ${proposal.id} conflicts with your edits of ${target} in ${result.conflicts.map(c => c.path).join(', ')}. The file is not changed. Run method apply ${target} ${proposal.id} to see both versions, merge them in the file, then run method apply ${target} ${proposal.id} --resolved.\n`
167
- : `Accepted proposal ${proposal.id} does not merge into a valid Method: ${result.error} The file is not changed. Run method apply ${target} ${proposal.id} to see it.\n`);
166
+ ? `Method's change ${proposal.id} conflicts with your edits of ${target} in ${result.conflicts.map(c => c.path).join(', ')}. The file is not changed. Run method apply ${target} ${proposal.id} to see both versions, merge them in the file, then run method apply ${target} ${proposal.id} --resolved.\n`
167
+ : `Method's change ${proposal.id} does not merge into a valid Method: ${result.error} The file is not changed. Run method apply ${target} ${proposal.id} to see it.\n`);
168
168
  }
169
169
  catch { /* offline or signed out: the command goes on without the proposal. */ }
170
170
  }
@@ -196,55 +196,36 @@ function manualStep(file, proposal, runFolder) {
196
196
  return 'This proposal has no change to apply.';
197
197
  }
198
198
  const print = (value) => process.stdout.write(JSON.stringify(value, null, 2) + '\n');
199
- function summary(p) {
200
- return { id: p.id, source: p.source, status: p.status, kind: p.evidence?.kind ?? null, cause: p.cause, steps: p.evidence?.steps ?? [], base_version_id: p.base_version_id, created_at: p.created_at };
199
+ const changeLine = (change) => `${change.date.slice(0, 10)} ${change.version_id ?? 'not saved as a version'} ${change.cause}`;
200
+ /** `method changes FILE`: the changes that Method applied to the Method, newest first, and those applied on this computer. */
201
+ async function changesCommand(file, target, methodId, client, json) {
202
+ const local = localProposals(file).filter(p => p.status === 'applied').map((p) => ({ id: p.id, source: 'local', cause: p.cause, date: String(p.applied_at ?? p.created_at), version_id: p.applied_version_id ?? null, steps: p.evidence?.steps ?? [] }));
203
+ let account = [];
204
+ if (methodId) {
205
+ const { proposals } = await client.request(`/api/methods/${encodeURIComponent(methodId)}/proposals?status=accepted`);
206
+ account = (proposals ?? []).map(p => ({ id: p.id, source: 'account', cause: String(p.cause ?? ''), date: String(p.created_at ?? ''), version_id: p.published_version_id ?? null, steps: p.evidence?.steps ?? [] }));
207
+ }
208
+ else if (!local.length)
209
+ throw Error(`${target} has no id: line, so Method has not changed it. Run it once while signed in.`);
210
+ const changes = [...account, ...local].sort((a, b) => b.date.localeCompare(a.date));
211
+ if (json)
212
+ return print({ file, method_id: methodId, changes });
213
+ process.stdout.write(changes.length ? changes.map(changeLine).join('\n') + '\n' : `Method has not changed ${target} yet.\n`);
201
214
  }
202
- /** `method improve`, `method proposals`, and `method apply`. */
215
+ /** `method improve`, `method changes`, and `method apply`. */
203
216
  export async function proposalCommand(args, clientFactory) {
204
217
  if (args[0] === 'improve')
205
218
  return (await import('./improve.js')).improveCommand(args.slice(1), clientFactory);
206
- const { values, positionals } = parseArgs({ args: args.slice(1), allowPositionals: true, options: { server: { type: 'string' }, resolved: { type: 'boolean' }, wait: { type: 'boolean' } } });
219
+ const { values, positionals } = parseArgs({ args: args.slice(1), allowPositionals: true, options: { server: { type: 'string' }, resolved: { type: 'boolean' }, json: { type: 'boolean' } } });
207
220
  const [target, wanted] = positionals;
208
221
  if (!target || positionals.length > (args[0] === 'apply' ? 2 : 1))
209
- throw Error(args[0] === 'apply' ? 'Use method apply FILE [PROPOSAL_ID] [--resolved].' : 'Use method proposals FILE.');
222
+ throw Error(args[0] === 'apply' ? 'Use method apply FILE [PROPOSAL_ID] [--resolved].' : 'Use method changes FILE [--json].');
210
223
  const file = authoringPath(target), id = readDocument(file).id;
211
224
  const methodId = typeof id === 'string' && methodIdPattern.test(id) ? id : null;
212
225
  const client = clientFactory(values.server ?? DEFAULT_SERVER);
213
226
  const signedIn = !!client.token();
214
- if (args[0] === 'proposals') {
215
- const local = localProposals(file).map(summary);
216
- if (!methodId || !signedIn)
217
- return print({ file, method_id: methodId, proposals: local, ...(methodId ? { next: 'Run method login to see the proposals in your account too.' } : {}) });
218
- const read = async () => {
219
- const [{ proposals }, { improvements }] = await Promise.all([
220
- client.request(`/api/methods/${encodeURIComponent(methodId)}/proposals`),
221
- client.request(`/api/methods/${encodeURIComponent(methodId)}/improvements`)
222
- ]);
223
- // The newest improvement that has no proposal: still running, failed, or ended with no safe change (outcome none).
224
- const latest = (improvements ?? []).find(i => !i.proposal_id);
225
- return { proposals, pending: latest && (latest.created_at ?? '') > String((proposals ?? [])[0]?.created_at ?? '') ? latest : undefined };
226
- };
227
- let { proposals, pending } = await read();
228
- // --wait: one line each time the improvement reaches a new step, until it ends.
229
- let shown = '';
230
- while (values.wait && pending && ['queued', 'running'].includes(pending.status)) {
231
- const step = pending.steps?.find((s) => s.status === 'running')?.step_id ?? pending.status;
232
- if (step !== shown) {
233
- process.stderr.write(`Improvement ${pending.id}: ${step}\n`);
234
- shown = step;
235
- }
236
- await new Promise(resolve => setTimeout(resolve, 5000));
237
- ({ proposals, pending } = await read());
238
- }
239
- const failedStep = pending?.steps?.find((s) => s.status === 'failed')?.step_id;
240
- const next = !pending ? undefined
241
- : ['queued', 'running'].includes(pending.status) ? `Improvement ${pending.id} is ${pending.status}. Run method proposals ${target} --wait: it returns when the proposal is ready.`
242
- : pending.outcome === 'none' ? `The improvement found no safe change. Say what is wrong in a note on a run (method improve ${target} --note TEXT), or record a case with method case new.`
243
- : pending.status === 'failed' || pending.status === 'cancelled' ? `Improvement ${pending.id} ${pending.status}${failedStep ? ` at step ${failedStep}` : ''}${pending.error?.message ? `: ${pending.error.message}` : ''}. Run method improve ${target} again, or record the correction with method case new.`
244
- : undefined;
245
- return print({ file, method_id: methodId, proposals: [...(proposals ?? []).map(p => summary({ ...p, source: 'account' })), ...local].sort((a, b) => String(b.created_at).localeCompare(String(a.created_at))),
246
- ...(pending ? { improvement: { id: pending.id, status: pending.status, ...(pending.outcome === 'none' ? { outcome: 'none' } : {}), ...(failedStep ? { failed_step: failedStep } : {}), ...(pending.error ? { error: pending.error } : {}) } } : {}), ...(next ? { next } : {}) });
247
- }
227
+ if (args[0] === 'changes')
228
+ return changesCommand(file, target, methodId, client, !!values.json);
248
229
  const local = localProposals(file);
249
230
  let proposal = wanted ? local.find(p => p.id === wanted) : undefined;
250
231
  if (!proposal && wanted) {
@@ -254,15 +235,13 @@ export async function proposalCommand(args, clientFactory) {
254
235
  if (methodId && proposal.method_id !== methodId)
255
236
  throw Error(`Proposal ${wanted} is for another Method.`);
256
237
  }
257
- if (!proposal && methodId && signedIn)
258
- proposal = await newestAccepted(client, methodId);
259
238
  proposal ??= local.find(p => p.status === 'open');
260
239
  if (!proposal)
261
- throw Error(`${target} has no accepted proposal. Run method proposals ${target}.`);
240
+ throw Error(`${target} has no open proposal. Run method improve ${target} --note TEXT first.`);
262
241
  if (proposal.status === 'applied')
263
242
  return print({ status: 'applied', file, proposal_id: proposal.id, next: 'This proposal is already applied.' });
264
243
  if (proposal.source === 'account' && proposal.status !== 'accepted')
265
- throw Error(`Proposal ${proposal.id} is ${proposal.status}. Accept it on the dashboard first: ${client.server}/methods/${proposal.method_id}`);
244
+ throw Error(`Change ${proposal.id} is ${proposal.status}, so it has nothing to merge.`);
266
245
  if (proposal.source === 'local' && proposal.status !== 'open')
267
246
  throw Error(`Proposal ${proposal.id} is ${proposal.status}.`);
268
247
  if (values.resolved)
@@ -84,16 +84,16 @@ export declare const ObservedEffectsSchema: z.ZodUnion<[z.ZodObject<{
84
84
  }, "strict", z.ZodTypeAny, {
85
85
  runs: string[];
86
86
  network: string[];
87
+ env: string[];
87
88
  reads: string[];
88
89
  writes: string[];
89
- env: string[];
90
90
  truncated?: true | undefined;
91
91
  }, {
92
92
  runs: string[];
93
93
  network: string[];
94
+ env: string[];
94
95
  reads: string[];
95
96
  writes: string[];
96
- env: string[];
97
97
  truncated?: true | undefined;
98
98
  }>, z.ZodObject<{
99
99
  unavailable: z.ZodString;
@@ -641,16 +641,16 @@ export declare const InspectionSchema: z.ZodObject<{
641
641
  }, "strict", z.ZodTypeAny, {
642
642
  runs: string[];
643
643
  network: string[];
644
+ env: string[];
644
645
  reads: string[];
645
646
  writes: string[];
646
- env: string[];
647
647
  truncated?: true | undefined;
648
648
  }, {
649
649
  runs: string[];
650
650
  network: string[];
651
+ env: string[];
651
652
  reads: string[];
652
653
  writes: string[];
653
- env: string[];
654
654
  truncated?: true | undefined;
655
655
  }>, z.ZodObject<{
656
656
  unavailable: z.ZodString;
@@ -661,6 +661,7 @@ export declare const InspectionSchema: z.ZodObject<{
661
661
  }>]>>;
662
662
  }, "strict", z.ZodTypeAny, {
663
663
  status: "running" | "waiting_for_you" | "failed" | "returned" | "passed" | "skipped" | "stopped";
664
+ changes: Record<string, Json>;
664
665
  checks: {
665
666
  method: string;
666
667
  id: string;
@@ -700,7 +701,6 @@ export declare const InspectionSchema: z.ZodObject<{
700
701
  attempts?: number | undefined;
701
702
  }[];
702
703
  step_id: string;
703
- changes: Record<string, Json>;
704
704
  inputs?: Record<string, Json> | undefined;
705
705
  verification?: "checked" | "unchecked" | undefined;
706
706
  outputs?: Record<string, Json> | undefined;
@@ -713,15 +713,16 @@ export declare const InspectionSchema: z.ZodObject<{
713
713
  observed_effects?: {
714
714
  runs: string[];
715
715
  network: string[];
716
+ env: string[];
716
717
  reads: string[];
717
718
  writes: string[];
718
- env: string[];
719
719
  truncated?: true | undefined;
720
720
  } | {
721
721
  unavailable: string;
722
722
  } | undefined;
723
723
  }, {
724
724
  status: "running" | "waiting_for_you" | "failed" | "returned" | "passed" | "skipped" | "stopped";
725
+ changes: Record<string, Json>;
725
726
  checks: {
726
727
  method: string;
727
728
  id: string;
@@ -761,7 +762,6 @@ export declare const InspectionSchema: z.ZodObject<{
761
762
  attempts?: number | undefined;
762
763
  }[];
763
764
  step_id: string;
764
- changes: Record<string, Json>;
765
765
  inputs?: Record<string, Json> | undefined;
766
766
  verification?: "checked" | "unchecked" | undefined;
767
767
  outputs?: Record<string, Json> | undefined;
@@ -774,9 +774,9 @@ export declare const InspectionSchema: z.ZodObject<{
774
774
  observed_effects?: {
775
775
  runs: string[];
776
776
  network: string[];
777
+ env: string[];
777
778
  reads: string[];
778
779
  writes: string[];
779
- env: string[];
780
780
  truncated?: true | undefined;
781
781
  } | {
782
782
  unavailable: string;
@@ -795,6 +795,7 @@ export declare const InspectionSchema: z.ZodObject<{
795
795
  }>;
796
796
  invocations: Record<string, {
797
797
  status: "running" | "waiting_for_you" | "failed" | "returned" | "passed" | "skipped" | "stopped";
798
+ changes: Record<string, Json>;
798
799
  checks: {
799
800
  method: string;
800
801
  id: string;
@@ -834,7 +835,6 @@ export declare const InspectionSchema: z.ZodObject<{
834
835
  attempts?: number | undefined;
835
836
  }[];
836
837
  step_id: string;
837
- changes: Record<string, Json>;
838
838
  inputs?: Record<string, Json> | undefined;
839
839
  verification?: "checked" | "unchecked" | undefined;
840
840
  outputs?: Record<string, Json> | undefined;
@@ -847,9 +847,9 @@ export declare const InspectionSchema: z.ZodObject<{
847
847
  observed_effects?: {
848
848
  runs: string[];
849
849
  network: string[];
850
+ env: string[];
850
851
  reads: string[];
851
852
  writes: string[];
852
- env: string[];
853
853
  truncated?: true | undefined;
854
854
  } | {
855
855
  unavailable: string;
@@ -951,6 +951,7 @@ export declare const InspectionSchema: z.ZodObject<{
951
951
  }>;
952
952
  invocations: Record<string, {
953
953
  status: "running" | "waiting_for_you" | "failed" | "returned" | "passed" | "skipped" | "stopped";
954
+ changes: Record<string, Json>;
954
955
  checks: {
955
956
  method: string;
956
957
  id: string;
@@ -990,7 +991,6 @@ export declare const InspectionSchema: z.ZodObject<{
990
991
  attempts?: number | undefined;
991
992
  }[];
992
993
  step_id: string;
993
- changes: Record<string, Json>;
994
994
  inputs?: Record<string, Json> | undefined;
995
995
  verification?: "checked" | "unchecked" | undefined;
996
996
  outputs?: Record<string, Json> | undefined;
@@ -1003,9 +1003,9 @@ export declare const InspectionSchema: z.ZodObject<{
1003
1003
  observed_effects?: {
1004
1004
  runs: string[];
1005
1005
  network: string[];
1006
+ env: string[];
1006
1007
  reads: string[];
1007
1008
  writes: string[];
1008
- env: string[];
1009
1009
  truncated?: true | undefined;
1010
1010
  } | {
1011
1011
  unavailable: string;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@method-ai-hq/sdk",
3
- "version": "0.16.0",
3
+ "version": "0.17.0",
4
4
  "description": "Build and run reusable .method files with checked steps and saved results.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -35,7 +35,7 @@
35
35
  "prepack": "npm --prefix ../.. run build"
36
36
  },
37
37
  "dependencies": {
38
- "@withmethod/runtime": "https://codeload.github.com/method-ai-hq/method-spec/tar.gz/6020ed9208670972df60a27a602885bcad947c8c",
38
+ "@withmethod/runtime": "https://codeload.github.com/method-ai-hq/method-spec/tar.gz/54461cb387d8f10813db05bb0cea29c0a3a53504",
39
39
  "fflate": "^0.8.3",
40
40
  "yaml": "^2.9.0",
41
41
  "zod": "^3.25.76"