@method-ai-hq/sdk 0.17.3 → 0.18.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/README.md +2 -2
  2. package/dist/packages/sdk/package.json +2 -2
  3. package/dist/packages/sdk/src/authoring-instructions.d.ts +4 -4
  4. package/dist/packages/sdk/src/authoring-instructions.js +5 -5
  5. package/dist/packages/sdk/src/authoring.js +8 -6
  6. package/dist/packages/sdk/src/capabilities.d.ts +9 -6
  7. package/dist/packages/sdk/src/capabilities.js +15 -32
  8. package/dist/packages/sdk/src/computer-settings.d.ts +2 -5
  9. package/dist/packages/sdk/src/computer-settings.js +4 -11
  10. package/dist/packages/sdk/src/current-runtime.js +19 -8
  11. package/dist/packages/sdk/src/improve.d.ts +104 -29
  12. package/dist/packages/sdk/src/improve.js +240 -70
  13. package/dist/packages/sdk/src/judge.d.ts +12 -4
  14. package/dist/packages/sdk/src/judge.js +46 -10
  15. package/dist/packages/sdk/src/local-cli.d.ts +0 -1
  16. package/dist/packages/sdk/src/local-cli.js +16 -15
  17. package/dist/packages/sdk/src/local-judgments.d.ts +40 -0
  18. package/dist/packages/sdk/src/local-judgments.js +144 -0
  19. package/dist/packages/sdk/src/local-setup.d.ts +0 -2
  20. package/dist/packages/sdk/src/local-setup.js +1 -1
  21. package/dist/packages/sdk/src/method-client.d.ts +2 -0
  22. package/dist/packages/sdk/src/method-client.js +6 -2
  23. package/dist/packages/sdk/src/method-help.d.ts +1 -1
  24. package/dist/packages/sdk/src/method-help.js +20 -23
  25. package/dist/packages/sdk/src/method.js +35 -2
  26. package/dist/packages/sdk/src/model-call.d.ts +73 -0
  27. package/dist/packages/sdk/src/model-call.js +74 -0
  28. package/dist/packages/sdk/src/outbox.d.ts +1 -1
  29. package/dist/packages/sdk/src/outbox.js +6 -5
  30. package/dist/packages/sdk/src/own-model-key.d.ts +1 -1
  31. package/dist/packages/sdk/src/own-model-key.js +1 -1
  32. package/dist/packages/sdk/src/proposals.d.ts +10 -26
  33. package/dist/packages/sdk/src/proposals.js +32 -64
  34. package/dist/packages/sdk/src/quality.d.ts +1 -2
  35. package/dist/packages/sdk/src/quality.js +25 -16
  36. package/package.json +2 -2
@@ -0,0 +1,73 @@
1
+ /**
2
+ * One model request that follows the same model rule as a run (the runtime's resolveModels): the model that the
3
+ * request names in `models`, else the user's own signed-in Claude Code or Codex (the calling agent first), else a
4
+ * hosted model on account credit. Each request is a fresh session.
5
+ */
6
+ export type ModelRequest = {
7
+ system: string;
8
+ user: string;
9
+ schema: JsonSchema;
10
+ /** A model name of `models`, or default. */
11
+ model?: string;
12
+ models?: Record<string, unknown>;
13
+ };
14
+ export type ModelAnswer = {
15
+ value: any;
16
+ agent: string;
17
+ usd: number | null;
18
+ duration_ms: number;
19
+ };
20
+ type JsonSchema = {
21
+ type?: string;
22
+ properties?: Record<string, JsonSchema>;
23
+ items?: JsonSchema;
24
+ enum?: string[];
25
+ description?: string;
26
+ };
27
+ /** A Method type for a JSON schema: object → record, array → list, string → text. */
28
+ export declare function methodType(schema: JsonSchema): any;
29
+ /** The one-step Method of a request. Instructions with prompt braces go in the inputs, so nothing in them is read as a variable. */
30
+ export declare function requestMethod(request: ModelRequest): {
31
+ inputs: {
32
+ instructions?: {
33
+ type: string;
34
+ description: string;
35
+ };
36
+ request: {
37
+ type: string;
38
+ description: string;
39
+ };
40
+ };
41
+ steps: {
42
+ answer: {
43
+ name: string;
44
+ in: {
45
+ instructions?: string;
46
+ request: string;
47
+ };
48
+ do: {
49
+ kind: string;
50
+ model: string;
51
+ prompt: string;
52
+ };
53
+ out: {
54
+ [k: string]: any;
55
+ };
56
+ };
57
+ };
58
+ result: {
59
+ [k: string]: string;
60
+ };
61
+ models?: Record<string, unknown>;
62
+ format: string;
63
+ name: string;
64
+ goal: string;
65
+ };
66
+ /** Run one model request through the runtime, with the preparation of method run. */
67
+ export declare function callModel(request: ModelRequest): Promise<ModelAnswer>;
68
+ /**
69
+ * `method __model`: one model request from a script of a Method (improve.method), as JSON on stdin; the answer as
70
+ * JSON on stdout. A failure prints {error, code, owner} and exits 1.
71
+ */
72
+ export declare function modelCallCommand(): Promise<void>;
73
+ export {};
@@ -0,0 +1,74 @@
1
+ import { mkdirSync, rmSync, writeFileSync } from 'node:fs';
2
+ import { join } from 'node:path';
3
+ import { randomUUID } from 'node:crypto';
4
+ import { methodCache } from './prepare.js';
5
+ /** A Method type for a JSON schema: object → record, array → list, string → text. */
6
+ export function methodType(schema) {
7
+ const described = (value) => schema.description || schema.enum ? { ...value, description: [schema.description, schema.enum ? `One of: ${schema.enum.join(', ')}.` : ''].filter(Boolean).join(' ') } : value;
8
+ if (schema.type === 'object')
9
+ return described({ type: 'record', fields: fieldsOf(schema) });
10
+ if (schema.type === 'array')
11
+ return described(schema.items?.type === 'object' ? { type: 'list', fields: fieldsOf(schema.items) } : { type: 'list', items: methodType(schema.items ?? { type: 'string' }) });
12
+ if (schema.type === 'number' || schema.type === 'integer')
13
+ return described({ type: 'number' });
14
+ if (schema.type === 'boolean')
15
+ return described({ type: 'boolean' });
16
+ return described({ type: 'text' });
17
+ }
18
+ const fieldsOf = (schema) => Object.fromEntries(Object.entries(schema.properties ?? {}).map(([name, value]) => [name, methodType(value)]));
19
+ /** The one-step Method of a request. Instructions with prompt braces go in the inputs, so nothing in them is read as a variable. */
20
+ export function requestMethod(request) {
21
+ if (request.schema.type !== 'object')
22
+ throw Error('A model request asks for a JSON object.');
23
+ const braces = /\{\{|\}\}/.test(request.system);
24
+ const model = request.model ?? 'default';
25
+ const fields = fieldsOf(request.schema);
26
+ return {
27
+ format: 'method/3.4', name: 'Model request', goal: 'Answer one request.',
28
+ ...(request.models ? { models: request.models } : {}),
29
+ inputs: { request: { type: 'text', description: 'The request.' }, ...(braces ? { instructions: { type: 'text', description: 'The instructions.' } } : {}) },
30
+ steps: { answer: { name: 'Answer', in: { request: 'inputs.request', ...(braces ? { instructions: 'inputs.instructions' } : {}) },
31
+ do: { kind: 'call', model, prompt: braces ? 'Follow the instructions in the step inputs, then answer the request.' : request.system }, out: fields } },
32
+ result: Object.fromEntries(Object.keys(fields).map(name => [name, name])),
33
+ };
34
+ }
35
+ /** Run one model request through the runtime, with the preparation of method run. */
36
+ export async function callModel(request) {
37
+ const dir = join(methodCache(), 'model-requests', randomUUID());
38
+ mkdirSync(dir, { recursive: true, mode: 0o700 });
39
+ try {
40
+ const file = join(dir, 'request.method');
41
+ writeFileSync(file, JSON.stringify(requestMethod(request)), { mode: 0o600 });
42
+ const { preparedConfig } = await import('./quality.js');
43
+ const { runMethod } = await import('@withmethod/runtime/runner.js');
44
+ const prepared = await preparedConfig(file, {});
45
+ const started = performance.now();
46
+ const result = await runMethod(file, prepared.config, { runDir: join(dir, 'run'), inputs: { request: request.user, ...(/\{\{|\}\}/.test(request.system) ? { instructions: request.system } : {}) },
47
+ processPath: prepared.processPath, prepareBundle: prepared.prepareBundle, ...(prepared.hostedModels ? { hostedModels: prepared.hostedModels } : {}) });
48
+ if (result.status !== 'completed')
49
+ throw Object.assign(Error(result.failure?.headline ?? `The model request ended with status ${result.status}.`), { code: result.failure?.code ?? 'model_failed', owner: result.failure?.owner });
50
+ const profile = prepared.config.models?.[request.model ?? 'default'];
51
+ const hosted = profile?.backend === 'method';
52
+ return { value: result.result, agent: hosted ? `hosted:${profile.model}` : profile?.backend ?? 'unknown', usd: hosted && typeof result.usage?.cost_usd === 'number' ? result.usage.cost_usd : null,
53
+ duration_ms: Math.round(performance.now() - started) };
54
+ }
55
+ finally {
56
+ rmSync(dir, { recursive: true, force: true });
57
+ }
58
+ }
59
+ /**
60
+ * `method __model`: one model request from a script of a Method (improve.method), as JSON on stdin; the answer as
61
+ * JSON on stdout. A failure prints {error, code, owner} and exits 1.
62
+ */
63
+ export async function modelCallCommand() {
64
+ const chunks = [];
65
+ for await (const chunk of process.stdin)
66
+ chunks.push(chunk);
67
+ try {
68
+ process.stdout.write(JSON.stringify(await callModel(JSON.parse(Buffer.concat(chunks).toString('utf8')))) + '\n');
69
+ }
70
+ catch (error) {
71
+ process.stdout.write(JSON.stringify({ error: error.message, code: error.code ?? 'model_failed', owner: error.owner ?? null }) + '\n');
72
+ process.exitCode = 1;
73
+ }
74
+ }
@@ -56,7 +56,7 @@ export declare function lastSaved(server: string, methodId: string): {
56
56
  export declare function recordKnownVersion(server: string, methodId: string, versionId: string, versionNumber: number): void;
57
57
  /** A checkout of a saved version: the next save of this Method on this computer has it as its parent. */
58
58
  export declare function recordCheckout(server: string, methodId: string, versionId: string, versionNumber: number, workflow: unknown): void;
59
- /** Errors that a retry can fix: no response, a stalled transfer, a server error, or too many requests. */
59
+ /** Errors that a retry can fix: no connection, a stalled transfer, a server error, or too many requests. */
60
60
  export declare function retryable(error: any): boolean;
61
61
  export declare class Outbox {
62
62
  readonly client: MethodClient;
@@ -40,12 +40,13 @@ function recordSaved(item, versionNumber) {
40
40
  writePrivateJson(savedFile(item.server), { ...savedVersions(item.server), [item.version_id]: { method_id: item.method_id, version_number: versionNumber } });
41
41
  writePrivateJson(methodFile(item.server, item.method_id), { version_id: item.version_id, version_number: versionNumber, workflow: item.body.workflow });
42
42
  }
43
- /** Errors that a retry can fix: no response, a stalled transfer, a server error, or too many requests. */
43
+ /** Errors that a retry can fix: no connection, a stalled transfer, a server error, or too many requests. */
44
44
  export function retryable(error) {
45
45
  const status = error?.status ?? Number(/^(\d{3}):/.exec(String(error?.message))?.[1]);
46
- return !status || status >= 500 || status === 429 || status === 408;
46
+ return status ? status >= 500 || status === 429 || status === 408 : unreachable(error) || error?.code === 'stalled';
47
47
  }
48
- const networkError = (error) => !(error?.status ?? /^\d{3}:/.test(String(error?.message))) && error?.code !== 'stalled';
48
+ /** The server cannot be reached. An error on this computer (an unreadable run folder) is not one. */
49
+ const unreachable = (error) => error?.code === 'network';
49
50
  export class Outbox {
50
51
  client;
51
52
  options;
@@ -221,7 +222,7 @@ export class Outbox {
221
222
  }
222
223
  // No response, a server error, or too many requests: after 3 in a row, stop and keep the items for a later
223
224
  // command. A finished run never waits for a server that keeps failing.
224
- if (networkError(error) || status >= 500 || status === 429)
225
+ if (unreachable(error) || status >= 500 || status === 429)
225
226
  this.networkFailures++;
226
227
  const attempt = (this.retryAt.get(key)?.attempt ?? 0) + 1;
227
228
  this.retryAt.set(key, { at: Date.now() + Math.min(30_000, 500 * 2 ** (attempt - 1)), attempt });
@@ -256,7 +257,7 @@ export class Outbox {
256
257
  }
257
258
  catch (error) {
258
259
  const status = error?.status ?? Number(/^(\d{3}):/.exec(String(error?.message))?.[1]);
259
- if (networkError(error) || status >= 500 || status === 429)
260
+ if (unreachable(error) || status >= 500 || status === 429)
260
261
  this.networkFailures++;
261
262
  }
262
263
  }
@@ -1,6 +1,6 @@
1
1
  import type { HostedModels } from '@withmethod/runtime';
2
2
  export declare const ownKeyName = "OPENROUTER_API_KEY";
3
- /** The name of this computer's own model key, or undefined. A local agent (--agent) replaces it for model steps. */
3
+ /** The name of this computer's own model key, or undefined. */
4
4
  export declare const ownKeyEnv: () => string | undefined;
5
5
  /** method config model-key [--off]: open the private form for the key, then use it for model and classify steps. */
6
6
  export declare function modelKeyCommand(args: string[], set?: (name: string) => Promise<unknown>): Promise<void>;
@@ -7,7 +7,7 @@ import { typesafeModel } from '@withmethod/runtime/classification.js';
7
7
  import { readComputerSettings, updateComputerSettings } from './computer-settings.js';
8
8
  import { findSecrets, importSecrets, resolveSecrets, setSecret } from './secrets.js';
9
9
  export const ownKeyName = 'OPENROUTER_API_KEY';
10
- /** The name of this computer's own model key, or undefined. A local agent (--agent) replaces it for model steps. */
10
+ /** The name of this computer's own model key, or undefined. */
11
11
  export const ownKeyEnv = () => readComputerSettings().model_key_env;
12
12
  /** method config model-key [--off]: open the private form for the key, then use it for model and classify steps. */
13
13
  export async function modelKeyCommand(args, set = setSecret) {
@@ -1,5 +1,5 @@
1
1
  import { type MethodClient } from './method-client.js';
2
- /** A proposed change to a Method, from the account (a server proposal) or from `method improve` on this computer. */
2
+ /** A change to a Method: from the account (a change Method made there), or from the improvement loop on this computer. */
3
3
  export type ProposalRecord = {
4
4
  id: string;
5
5
  source: 'account' | 'local';
@@ -33,34 +33,19 @@ export type MergeResult = {
33
33
  export declare function mergeMethod(base: any, ours: any, theirs: any): MergeResult;
34
34
  /** The steps whose definition differs between two documents. */
35
35
  export declare function changedSteps(before: any, after: any): string[];
36
+ /** Write Method content to a file and keep its id: line. A JSON file stays JSON. */
37
+ export declare function writeMethodFile(file: string, id: string | undefined, content: any): void;
36
38
  export declare const localProposalFolder: (file: string) => string;
37
- /** Save a local proposal as a private file. The .method folder is not for Git: its proposals can hold run content. */
39
+ /** Record what the improvement loop on this computer did (applied, or none) as a private file in the .method folder: it can hold run content. */
38
40
  export declare function saveLocalProposal(file: string, record: Omit<ProposalRecord, 'source'> & Record<string, unknown>): string;
39
41
  export declare function localProposals(file: string): (ProposalRecord & {
40
42
  path: string;
41
43
  })[];
42
- type RequestOptions = {
43
- signal?: AbortSignal;
44
- stallMs?: number;
45
- };
46
- type Applied = {
47
- status: 'applied';
48
- steps: string[];
49
- version_id: string | undefined;
50
- warning?: string;
51
- } | {
52
- status: 'conflict';
53
- conflicts: MergeConflict[];
54
- } | {
55
- status: 'invalid';
56
- error: string;
57
- };
58
- /** Merge a proposal into the file. A clean merge writes the file and marks the proposal applied. Never publishes. */
59
- export declare function applyProposal(client: MethodClient | undefined, file: string, proposal: ProposalRecord, options?: RequestOptions): Promise<Applied>;
60
44
  /**
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.
45
+ * Before a command that takes a .method FILE: when this computer is signed in and Method changed the Method in the
46
+ * account (keeping run data in the account, or a fix of a Method that had not run on a computer), merge the change into
47
+ * the file and print one line. A conflict leaves the file unchanged and writes both versions of each conflicting part
48
+ * to a file for the coding agent to merge; the command goes on. Offline, signed out, or on any error, it does nothing.
64
49
  */
65
50
  export declare function autoApply(args: string[], clientFactory: (server: string) => MethodClient): Promise<void>;
66
51
  /** 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 +53,5 @@ export declare function localRuns(file: string): Promise<{
68
53
  dir: string;
69
54
  account_run_id: string | null;
70
55
  }[]>;
71
- /** `method improve`, `method changes`, and `method apply`. */
72
- export declare function proposalCommand(args: string[], clientFactory: (server: string) => MethodClient): Promise<boolean | undefined>;
73
- export {};
56
+ /** `method changes FILE [--json]`. */
57
+ export declare function changesMain(args: string[], clientFactory: (server: string) => MethodClient): Promise<boolean | undefined>;
@@ -6,6 +6,7 @@ import { authoringPath, readDocument, writeDocument } from './authoring.js';
6
6
  import { DEFAULT_SERVER } from './method-client.js';
7
7
  import { writePrivateJson } from './files.js';
8
8
  import { currentVersion, methodContent, methodIdPattern } from './versions.js';
9
+ import { localMethodFolder } from './local-judgments.js';
9
10
  const isObject = (value) => !!value && typeof value === 'object' && !Array.isArray(value);
10
11
  /** Keys of ours in their order; a key that only theirs has goes after the key before it in theirs. */
11
12
  function keyOrder(ours, theirs) {
@@ -56,7 +57,7 @@ export function changedSteps(before, after) {
56
57
  return [...new Set([...Object.keys(before?.steps ?? {}), ...Object.keys(after?.steps ?? {})])].filter(id => !isDeepStrictEqual(before?.steps?.[id], after?.steps?.[id]));
57
58
  }
58
59
  /** Write Method content to a file and keep its id: line. A JSON file stays JSON. */
59
- function writeMethodFile(file, id, content) {
60
+ export function writeMethodFile(file, id, content) {
60
61
  const { format, ...rest } = content;
61
62
  const document = { ...(format !== undefined ? { format } : {}), ...(id ? { id } : {}), ...rest };
62
63
  if (/^\s*\{/.test(readFileSync(file, 'utf8'))) {
@@ -74,13 +75,11 @@ function writeMethodFile(file, id, content) {
74
75
  writeDocument(file, document);
75
76
  }
76
77
  export const localProposalFolder = (file) => join(dirname(file), '.method', 'proposals');
77
- /** Save a local proposal as a private file. The .method folder is not for Git: its proposals can hold run content. */
78
+ /** Record what the improvement loop on this computer did (applied, or none) as a private file in the .method folder: it can hold run content. */
78
79
  export function saveLocalProposal(file, record) {
79
- const folder = localProposalFolder(file), ignore = join(dirname(folder), '.gitignore');
80
- const path = join(folder, `${record.id}.json`);
80
+ localMethodFolder(file);
81
+ const path = join(localProposalFolder(file), `${record.id}.json`);
81
82
  writePrivateJson(path, { ...record, source: 'local', file: basename(file) });
82
- if (!existsSync(ignore))
83
- writeFileSync(ignore, '*\n', { mode: 0o600 });
84
83
  return path;
85
84
  }
86
85
  export function localProposals(file) {
@@ -101,8 +100,8 @@ async function newestAccepted(client, methodId, options = {}) {
101
100
  const accepted = (proposals ?? []).filter(p => p?.status === 'accepted').sort((a, b) => String(b.created_at).localeCompare(String(a.created_at)));
102
101
  return accepted[0] ? accountProposal(client, accepted[0].id, options) : undefined;
103
102
  }
104
- /** Merge a proposal into the file. A clean merge writes the file and marks the proposal applied. Never publishes. */
105
- export async function applyProposal(client, file, proposal, options = {}) {
103
+ /** Merge a change Method made in the account into the file. A clean merge writes the file and records the change as applied. Never publishes. */
104
+ async function applyProposal(client, file, proposal, options = {}) {
106
105
  const document = readDocument(file), ours = methodContent(document);
107
106
  const merged = mergeMethod(proposal.base_workflow, ours, proposal.proposed_workflow);
108
107
  if (!merged.clean)
@@ -119,11 +118,6 @@ export async function applyProposal(client, file, proposal, options = {}) {
119
118
  }
120
119
  async function markApplied(client, file, proposal, options = {}) {
121
120
  const version_id = (await currentVersion(file).catch(() => undefined))?.version_id;
122
- if (proposal.source === 'local') {
123
- const path = join(localProposalFolder(file), `${proposal.id}.json`), record = JSON.parse(readFileSync(path, 'utf8'));
124
- writePrivateJson(path, { ...record, status: 'applied', applied_at: new Date().toISOString(), ...(version_id ? { applied_version_id: version_id } : {}) });
125
- return { version_id };
126
- }
127
121
  try {
128
122
  await client.request(`/api/proposals/${encodeURIComponent(proposal.id)}/applied`, 'POST', version_id ? { version_id } : {}, true, options);
129
123
  return { version_id };
@@ -133,11 +127,12 @@ async function markApplied(client, file, proposal, options = {}) {
133
127
  }
134
128
  }
135
129
  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', 'changes', 'judge', 'improve', 'init', 'new-id', 'diff', 'help', 'authoring', 'schema', '__worker', '__checks', 'worker', 'keys', 'secret', 'models', 'login', 'logout', 'sync', 'progress']);
130
+ const skipAutoApply = new Set(['changes', 'judge', 'init', 'new-id', 'diff', 'help', 'authoring', 'schema', '__worker', '__checks', '__model', 'worker', 'keys', 'secret', 'models', 'login', 'logout', 'sync', 'progress']);
137
131
  /**
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.
132
+ * Before a command that takes a .method FILE: when this computer is signed in and Method changed the Method in the
133
+ * account (keeping run data in the account, or a fix of a Method that had not run on a computer), merge the change into
134
+ * the file and print one line. A conflict leaves the file unchanged and writes both versions of each conflicting part
135
+ * to a file for the coding agent to merge; the command goes on. Offline, signed out, or on any error, it does nothing.
141
136
  */
142
137
  export async function autoApply(args, clientFactory) {
143
138
  if (!args[0] || skipAutoApply.has(args[0]) || args.includes('--help') || process.env.METHOD_RUN_WORKER === '1')
@@ -160,13 +155,18 @@ export async function autoApply(args, clientFactory) {
160
155
  if (result.status === 'applied') {
161
156
  if (result.steps.length || result.warning)
162
157
  process.stderr.write(`Method changed ${target} (${proposal.id}): ${proposal.cause}${result.steps.length ? ` Changed steps: ${result.steps.join(', ')}.` : ''}${result.warning ? ` ${result.warning}` : ''}\n`);
158
+ return;
163
159
  }
164
- else
165
- process.stderr.write(result.status === 'conflict'
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`);
160
+ // The agent merges by hand from the saved versions; the change is recorded as applied, so it is not merged again.
161
+ localMethodFolder(file);
162
+ const path = join(dirname(file), '.method', `conflict-${proposal.id}.json`);
163
+ writePrivateJson(path, { proposal_id: proposal.id, cause: proposal.cause, ...(result.status === 'conflict' ? { conflicts: result.conflicts } : { error: result.error }), proposed_workflow: proposal.proposed_workflow });
164
+ await markApplied(client, file, proposal, options).catch(() => undefined);
165
+ process.stderr.write(result.status === 'conflict'
166
+ ? `Method's change ${proposal.id} (${proposal.cause}) conflicts with your edits of ${target} in ${result.conflicts.map(c => c.path).join(', ')}. The file is not changed. Both versions of each part are in ${path}: edit ${target} so each part keeps both changes.\n`
167
+ : `Method's change ${proposal.id} does not merge into a valid Method: ${result.error} The file is not changed. The changed version is in ${path}.\n`);
168
168
  }
169
- catch { /* offline or signed out: the command goes on without the proposal. */ }
169
+ catch { /* offline or signed out: the command goes on without the change. */ }
170
170
  }
171
171
  /** Run folders of this Method on this computer, newest first, with the account run ID of each run that was saved to the account. */
172
172
  export async function localRuns(file) {
@@ -186,7 +186,7 @@ export async function localRuns(file) {
186
186
  }
187
187
  const print = (value) => process.stdout.write(JSON.stringify(value, null, 2) + '\n');
188
188
  const changeLine = (change) => `${change.date.slice(0, 10)} ${change.version_id ?? 'not saved as a version'} ${change.cause}`;
189
- /** `method changes FILE`: the changes that Method applied to the Method, newest first, and those applied on this computer. */
189
+ /** `method changes FILE`: the changes that Method applied in the account and on this computer, newest first. */
190
190
  async function changesCommand(file, target, methodId, client, json) {
191
191
  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 ?? [] }));
192
192
  let account = [];
@@ -201,48 +201,16 @@ async function changesCommand(file, target, methodId, client, json) {
201
201
  return print({ file, method_id: methodId, changes });
202
202
  process.stdout.write(changes.length ? changes.map(changeLine).join('\n') + '\n' : `Method has not changed ${target} yet.\n`);
203
203
  }
204
- /** `method improve`, `method changes`, and `method apply`. */
205
- export async function proposalCommand(args, clientFactory) {
206
- if (args[0] === 'improve')
207
- return (await import('./improve.js')).improveCommand(args.slice(1), clientFactory);
208
- const { values, positionals } = parseArgs({ args: args.slice(1), allowPositionals: true, options: { server: { type: 'string' }, resolved: { type: 'boolean' }, json: { type: 'boolean' } } });
209
- const [target, wanted] = positionals;
210
- if (!target || positionals.length > (args[0] === 'apply' ? 2 : 1))
211
- throw Error(args[0] === 'apply' ? 'Use method apply FILE [PROPOSAL_ID] [--resolved].' : 'Use method changes FILE [--json].');
204
+ /** `method changes FILE [--json]`. */
205
+ export async function changesMain(args, clientFactory) {
206
+ const { values, positionals } = parseArgs({ args, allowPositionals: true, options: { server: { type: 'string' }, json: { type: 'boolean' } } });
207
+ const [target] = positionals;
208
+ if (!target || positionals.length !== 1)
209
+ throw Error('Use method changes FILE [--json].');
212
210
  const file = authoringPath(target), id = readDocument(file).id;
213
211
  const methodId = typeof id === 'string' && methodIdPattern.test(id) ? id : null;
214
212
  const client = clientFactory(values.server ?? DEFAULT_SERVER);
215
- const signedIn = !!client.token();
216
- if (args[0] === 'changes')
217
- return changesCommand(file, target, methodId, client, !!values.json);
218
- const local = localProposals(file);
219
- let proposal = wanted ? local.find(p => p.id === wanted) : undefined;
220
- if (!proposal && wanted) {
221
- if (!signedIn)
222
- await client.login();
223
- proposal = await accountProposal(client, wanted);
224
- if (methodId && proposal.method_id !== methodId)
225
- throw Error(`Proposal ${wanted} is for another Method.`);
226
- }
227
- proposal ??= local.find(p => p.status === 'open');
228
- if (!proposal)
229
- throw Error(`${target} has no open proposal. Run method improve ${target} --note TEXT first.`);
230
- if (proposal.status === 'applied')
231
- return print({ status: 'applied', file, proposal_id: proposal.id, next: 'This proposal is already applied.' });
232
- if (proposal.source === 'account' && proposal.status !== 'accepted')
233
- throw Error(`Change ${proposal.id} is ${proposal.status}, so it has nothing to merge.`);
234
- if (proposal.source === 'local' && proposal.status !== 'open')
235
- throw Error(`Proposal ${proposal.id} is ${proposal.status}.`);
236
- if (values.resolved)
237
- return print({ status: 'applied', file, proposal_id: proposal.id, ...await markApplied(client, file, proposal), next: `Run method test ${target}. Publish it when it is right.` });
238
- if (!proposal.proposed_workflow)
239
- return print({ status: 'none', file, proposal_id: proposal.id, cause: proposal.cause, next: 'No change passed the tests; there is nothing to apply.' });
240
- const result = await applyProposal(client, file, proposal);
241
- if (result.status === 'applied')
242
- return print({ ...result, file, proposal_id: proposal.id, cause: proposal.cause, next: `Run method test ${target}. Publish it when it is right.` });
243
- process.exitCode = 1;
244
- if (result.status === 'invalid')
245
- return print({ status: 'invalid', file, proposal_id: proposal.id, error: result.error, next: 'The file is not changed. Merge the proposed version by hand.' });
246
- print({ status: 'conflict', file, proposal_id: proposal.id, cause: proposal.cause, conflicts: result.conflicts,
247
- next: `The file is not changed. Your edits and the proposal both change each listed part. Edit ${target} so that each part keeps both changes, then run method apply ${target} ${proposal.id} --resolved.` });
213
+ if (methodId && !client.token())
214
+ await client.login();
215
+ return changesCommand(file, target, methodId, client, !!values.json);
248
216
  }
@@ -1,7 +1,6 @@
1
1
  /** The same runtime preparation as method run, without starting a run. Hosted models are used when signed in. */
2
2
  export declare function preparedConfig(file: string, flags: {
3
3
  workspace?: string;
4
- agent?: string;
5
4
  }, options?: {
6
5
  judge?: boolean;
7
6
  }): Promise<{
@@ -22,12 +21,12 @@ export declare function casesNotice(file: string): Promise<string | undefined>;
22
21
  /** Run the cases of a Method with the same preparation as method run. */
23
22
  export declare function runCases(file: string, flags: {
24
23
  workspace?: string;
25
- agent?: string;
26
24
  }, options?: {
27
25
  casesDir?: string | undefined;
28
26
  ids?: string[] | undefined;
29
27
  baseline?: string | undefined;
30
28
  newIds?: string[];
29
+ once?: boolean;
31
30
  }): Promise<{
32
31
  counts: {
33
32
  [k: string]: number;
@@ -10,6 +10,7 @@ import { createCase, retireCase, listCases, testSuite } from "@withmethod/runtim
10
10
  import { authoringPath, editDocument, readDocument } from "./authoring.js";
11
11
  import { localSetup } from "./local-setup.js";
12
12
  import { resolveAgentProfiles, checkAgents, accountNeeds } from "./capabilities.js";
13
+ import { localAgent, resolveModels } from "@withmethod/runtime/agents.js";
13
14
  import { hostedModelsFor, managedModels } from "./hosted-models.js";
14
15
  import { managedClassification } from "./classification-client.js";
15
16
  import { MethodClient } from "./method-client.js";
@@ -30,21 +31,29 @@ export async function preparedConfig(file, flags, options = {}) {
30
31
  catch {
31
32
  return false;
32
33
  } })();
33
- // A rubric judge is a model step too: without a local agent, a signed-in computer judges with the account's model.
34
- const judge = options.judge && !setup.agent && !config.models?.judge;
35
- if ((accountNeeds(method, config, setup.agent).models || judge) && signedIn) {
34
+ // A rubric judge is a model step too: it follows the same rule (the user's own local agent, else hosted).
35
+ const judge = options.judge && !config.models?.judge;
36
+ // The hosted model is asked for only when no local agent can run the model steps.
37
+ if ((accountNeeds(method, config).models || judge) && signedIn && !await localAgent()) {
36
38
  try {
37
39
  hostedModel = await managedModels(client).model();
38
40
  }
39
41
  catch (error) {
40
- process.stderr.write(`Hosted models are unavailable (${error.message}). Using a local agent.\n`);
42
+ process.stderr.write(`Hosted models are unavailable (${error.message}).\n`);
43
+ }
44
+ }
45
+ config.models = await resolveAgentProfiles(method, config, hostedModel);
46
+ if (judge) {
47
+ try {
48
+ config.models.judge = (await resolveModels({ steps: { judge: { do: { kind: 'call', model: 'judge' } } } }, {}, { hostedModel })).judge;
49
+ }
50
+ catch (error) {
51
+ if (error.code !== 'needs_input')
52
+ throw error;
41
53
  }
42
54
  }
43
- config.models = await resolveAgentProfiles(method, config, setup.agent, hostedModel);
44
- if (judge && hostedModel)
45
- config.models.judge = { backend: "method", model: hostedModel };
46
55
  // Classify steps that run live use the account's classifier, as method run does.
47
- const classification = accountNeeds(method, config, setup.agent).classification && signedIn ? managedClassification(client) : undefined;
56
+ const classification = accountNeeds(method, config).classification && signedIn ? managedClassification(client) : undefined;
48
57
  if (classification)
49
58
  config.classification = await classification.resolve(new AbortController().signal);
50
59
  await checkAgents(config.models);
@@ -139,18 +148,18 @@ async function declaredSecrets(file) {
139
148
  export async function runCases(file, flags, options = {}) {
140
149
  const prepared = await preparedConfig(file, flags, { judge: true });
141
150
  const report = await testSuite(file, prepared.config, { ...options,
142
- runOptions: { processPath: prepared.processPath, prepareBundle: prepared.prepareBundle, secrets: await declaredSecrets(file), ...(prepared.classification ? { classification: prepared.classification } : {}), ...(prepared.hostedModels ? { hostedModels: prepared.hostedModels } : {}), ...(flags.agent ? { agent: flags.agent } : {}) } });
143
- if (!options.ids && !options.casesDir)
151
+ runOptions: { processPath: prepared.processPath, prepareBundle: prepared.prepareBundle, secrets: await declaredSecrets(file), ...(prepared.classification ? { classification: prepared.classification } : {}), ...(prepared.hostedModels ? { hostedModels: prepared.hostedModels } : {}) } });
152
+ if (!options.ids && !options.casesDir && !options.once)
144
153
  recordChecked(file, report);
145
154
  return report;
146
155
  }
147
156
  export async function testCommand(args) {
148
- const { values, positionals } = parseArgs({ args, allowPositionals: true, options: { case: { type: "string", multiple: true }, new: { type: "string", multiple: true }, baseline: { type: "string" }, cases: { type: "string" }, workspace: { type: "string" }, agent: { type: "string" } } });
157
+ const { values, positionals } = parseArgs({ args, allowPositionals: true, options: { case: { type: "string", multiple: true }, new: { type: "string", multiple: true }, baseline: { type: "string" }, cases: { type: "string" }, workspace: { type: "string" }, once: { type: "boolean" } } });
149
158
  const file = positionals[0];
150
159
  if (!file || positionals.length !== 1)
151
- throw Error("Use method test FILE [--case ID]... [--baseline OLD_FILE] [--new ID]...");
160
+ throw Error("Use method test FILE [--case ID]... [--baseline OLD_FILE] [--new ID]... [--once]");
152
161
  const report = await runCases(authoringPath(file), values, { casesDir: values.cases ? authoringPath(values.cases) : undefined, ids: values.case,
153
- baseline: values.baseline ? authoringPath(values.baseline) : undefined, newIds: values.new ?? [] });
162
+ baseline: values.baseline ? authoringPath(values.baseline) : undefined, newIds: values.new ?? [], once: !!values.once });
154
163
  print(report);
155
164
  process.exitCode = report.passed ? 0 : 1;
156
165
  }
@@ -178,7 +187,7 @@ export async function caseGate(file, accept = [], reason) {
178
187
  export async function caseCommand(args) {
179
188
  const text = { type: "string" };
180
189
  const { values, positionals } = parseArgs({ args, allowPositionals: true, options: { run: text, "passing-run": text, id: text, note: text, rubric: { type: "string", multiple: true }, ref: text, context: { type: "string", multiple: true }, expect: text,
181
- observations: text, redact: text, runs: text, "min-pass": text, author: text, reason: text, by: text, cases: text, workspace: text, agent: text,
190
+ observations: text, redact: text, runs: text, "min-pass": text, author: text, reason: text, by: text, cases: text, workspace: text,
182
191
  "retention-days": text, supersedes: { type: "string", multiple: true }, "accept-conflict": text } });
183
192
  const [action, file, id] = positionals;
184
193
  if (!file || !["new", "retire", "list"].includes(action ?? ""))
@@ -200,7 +209,7 @@ export async function caseCommand(args) {
200
209
  throw Error('Supply --id, --note "the person\'s words", and --run (the run that went wrong) or --passing-run (a run that was right).');
201
210
  if (!values.rubric?.length && !values.expect)
202
211
  throw Error('Say what must be true with --rubric "plain sentence" (repeatable), or give --expect FILE.');
203
- const prepared = await preparedConfig(methodFile, { ...(values.workspace ? { workspace: values.workspace } : {}), ...(values.agent ? { agent: values.agent } : {}) }, { judge: true });
212
+ const prepared = await preparedConfig(methodFile, { ...(values.workspace ? { workspace: values.workspace } : {}) }, { judge: true });
204
213
  const privacy = casesPrivacyWarning(methodFile, casesDir);
205
214
  if (privacy)
206
215
  process.stderr.write(privacy + "\n");
@@ -208,7 +217,7 @@ export async function caseCommand(args) {
208
217
  id: values.id, note: values.note, author: values.author ?? null, rubric: values.rubric ?? [], ref: values.ref, context: values.context ?? [], expect: json(values.expect) ?? [],
209
218
  observations: json(values.observations), redact: json(values.redact), runs: integer(values.runs, "--runs"), minPass: integer(values["min-pass"], "--min-pass"),
210
219
  retentionDays: integer(values["retention-days"], "--retention-days"), supersedes: values.supersedes ?? [], casesDir, acceptConflict: values["accept-conflict"], config: prepared.config,
211
- options: { runOptions: { processPath: prepared.processPath, secrets: await declaredSecrets(methodFile), ...(prepared.classification ? { classification: prepared.classification } : {}), ...(prepared.hostedModels ? { hostedModels: prepared.hostedModels } : {}), ...(values.agent ? { agent: values.agent } : {}) } } }));
220
+ options: { runOptions: { processPath: prepared.processPath, secrets: await declaredSecrets(methodFile), ...(prepared.classification ? { classification: prepared.classification } : {}), ...(prepared.hostedModels ? { hostedModels: prepared.hostedModels } : {}) } } }));
212
221
  }
213
222
  /**
214
223
  * A case copies run data (inputs, outputs, written files) into cases/. When that folder would be committed to a Git
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@method-ai-hq/sdk",
3
- "version": "0.17.3",
3
+ "version": "0.18.1",
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/5d9183f52b64af45d677a69d718bda9e1e0077ab",
38
+ "@withmethod/runtime": "https://codeload.github.com/method-ai-hq/method-spec/tar.gz/v0.15.0",
39
39
  "fflate": "^0.8.3",
40
40
  "yaml": "^2.9.0",
41
41
  "zod": "^3.25.76"