@method-ai-hq/sdk 0.18.2 → 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/README.md +1 -1
  2. package/dist/packages/sdk/package.json +7 -2
  3. package/dist/packages/sdk/src/authoring-instructions.d.ts +1 -1
  4. package/dist/packages/sdk/src/authoring-instructions.js +1 -1
  5. package/dist/packages/sdk/src/current-inspection.js +3 -0
  6. package/dist/packages/sdk/src/current-runtime.d.ts +6 -1
  7. package/dist/packages/sdk/src/current-runtime.js +47 -2
  8. package/dist/packages/sdk/src/fix-lines.d.ts +82 -0
  9. package/dist/packages/sdk/src/fix-lines.js +63 -0
  10. package/dist/packages/sdk/src/fix-outcomes.d.ts +72 -0
  11. package/dist/packages/sdk/src/fix-outcomes.js +201 -0
  12. package/dist/packages/sdk/src/home.d.ts +30 -26
  13. package/dist/packages/sdk/src/home.js +164 -60
  14. package/dist/packages/sdk/src/improve.d.ts +146 -24
  15. package/dist/packages/sdk/src/improve.js +665 -105
  16. package/dist/packages/sdk/src/judge.d.ts +23 -10
  17. package/dist/packages/sdk/src/judge.js +110 -26
  18. package/dist/packages/sdk/src/learned.d.ts +43 -0
  19. package/dist/packages/sdk/src/learned.js +97 -0
  20. package/dist/packages/sdk/src/local-cli.js +8 -3
  21. package/dist/packages/sdk/src/local-judgments.d.ts +2 -0
  22. package/dist/packages/sdk/src/merge.d.ts +38 -0
  23. package/dist/packages/sdk/src/merge.js +65 -0
  24. package/dist/packages/sdk/src/method-client.js +1 -1
  25. package/dist/packages/sdk/src/method-files.d.ts +3 -2
  26. package/dist/packages/sdk/src/method-files.js +7 -3
  27. package/dist/packages/sdk/src/method-help.d.ts +1 -1
  28. package/dist/packages/sdk/src/method-help.js +25 -16
  29. package/dist/packages/sdk/src/method-sync.d.ts +7 -0
  30. package/dist/packages/sdk/src/method-sync.js +33 -0
  31. package/dist/packages/sdk/src/method.d.ts +5 -0
  32. package/dist/packages/sdk/src/method.js +169 -15
  33. package/dist/packages/sdk/src/outbox.d.ts +28 -6
  34. package/dist/packages/sdk/src/outbox.js +13 -6
  35. package/dist/packages/sdk/src/proposals.d.ts +0 -28
  36. package/dist/packages/sdk/src/proposals.js +2 -129
  37. package/dist/packages/sdk/src/quality.d.ts +65 -5
  38. package/dist/packages/sdk/src/quality.js +82 -17
  39. package/dist/packages/sdk/src/run-saved.d.ts +4 -0
  40. package/dist/packages/sdk/src/run-saved.js +33 -2
  41. package/dist/packages/sdk/src/undo.d.ts +50 -0
  42. package/dist/packages/sdk/src/undo.js +108 -0
  43. package/dist/packages/sdk/src/versions.js +5 -3
  44. package/dist/packages/sdk/src/watch.d.ts +20 -0
  45. package/dist/packages/sdk/src/watch.js +86 -0
  46. package/dist/packages/workflow-language/src/inspection.d.ts +30 -0
  47. package/dist/packages/workflow-language/src/inspection.js +3 -1
  48. package/package.json +7 -2
@@ -1,5 +1,4 @@
1
1
  import { type MethodClient } from './method-client.js';
2
- import { type RunFile } from './improve.js';
3
2
  export declare const maxNoteLength = 4000;
4
3
  export declare const maxCorrectedBytes: number;
5
4
  export type Judgment = {
@@ -12,17 +11,31 @@ export type Judgment = {
12
11
  };
13
12
  /** The corrected result in a file: its JSON value when it parses as JSON, or else its text. */
14
13
  export declare function correctedValue(path: string): unknown;
15
- /** The line that says what was recorded. */
16
- export declare function judgmentLine(judgment: Pick<Judgment, 'verdict' | 'step_id'>): string;
17
14
  /**
18
15
  * `method judge RUN_ID right|wrong [--step ID] [--note TEXT] [--corrected FILE] [--json]`: record the person's reaction
19
16
  * to a run's result. RUN_ID is the dashboard run ID or this computer's run ID; the server resolves both. When the run's
20
17
  * content stays on this computer (run_data: device), the account gets the verdict and step only, and the judgment with
21
- * its note and corrected result goes in .method/judgments.jsonl next to the Method file. A wrong judgment then fixes the
22
- * Method here, in the foreground: method judge makes a regression case from the note, finds a change with the user's
23
- * own Claude Code or Codex, and applies it only when it fixes the run and breaks no earlier correction. It prints one
24
- * line: what changed, or why nothing did.
18
+ * its note and corrected result goes in .method/judgments.jsonl next to the Method file. A wrong judgment then starts
19
+ * the fix on this computer in its own process and returns at once with one line; method wait prints what the fix did,
20
+ * and so does the next method run of the Method.
25
21
  */
26
- export declare function judgeCommand(args: string[], clientFactory: (server: string) => MethodClient, options?: {
27
- run?: RunFile;
28
- }): Promise<void>;
22
+ export declare function judgeCommand(args: string[], clientFactory: (server: string) => MethodClient): Promise<void>;
23
+ /** The run's Method file fixes the correction: the file that ran the run, else the Method's home file on this computer. */
24
+ export declare function fileForJudgment(server: string, judgment: Pick<Judgment, 'method_id'>, runFile: string | undefined): string | undefined;
25
+ /** Two judgments of the same run and step by two people with opposite verdicts: neither is fixed until one is kept. */
26
+ export type HeldPair = {
27
+ run_id: string;
28
+ step_id: string | null;
29
+ judgments: Array<{
30
+ id: string;
31
+ verdict: string;
32
+ noted_by: string | null;
33
+ note: string | null;
34
+ }>;
35
+ };
36
+ /**
37
+ * `method answer JUDGMENT_ID "THEIR WORDS"`: send the person's answer to the question that a fix asked, and start the
38
+ * fix again with it. For a Method whose run content stays on this computer, the account keeps no answer, so the words
39
+ * also stay next to the Method file.
40
+ */
41
+ export declare function answerJudgmentCommand(args: string[], clientFactory: (server: string) => MethodClient): Promise<void>;
@@ -3,8 +3,11 @@ import { basename } from 'node:path';
3
3
  import { parseArgs } from 'node:util';
4
4
  import { DEFAULT_SERVER } from './method-client.js';
5
5
  import { deviceRun, findRunFolder, methodFileOfRun, saveLocalJudgment } from './local-judgments.js';
6
- import { fixCorrections, waitingLine } from './improve.js';
7
- import { readHome } from './home.js';
6
+ import { catchUp, readHome } from './home.js';
7
+ import { readDocument } from './authoring.js';
8
+ import { localJudgments } from './local-judgments.js';
9
+ import { fixStarter, markWorking, saveError, storedOutcomes } from './fix-outcomes.js';
10
+ import { approvedLine, heldPairLine, notHereLine, workingAnswerLine, workingLine } from './fix-lines.js';
8
11
  export const maxNoteLength = 4000;
9
12
  export const maxCorrectedBytes = 1024 * 1024;
10
13
  /** The corrected result in a file: its JSON value when it parses as JSON, or else its text. */
@@ -26,25 +29,22 @@ export function correctedValue(path) {
26
29
  return text;
27
30
  }
28
31
  }
29
- /** The line that says what was recorded. */
30
- export function judgmentLine(judgment) {
31
- return `Recorded: ${judgment.verdict}${judgment.step_id ? ` on step ${judgment.step_id}` : ''}.`;
32
- }
33
32
  /**
34
33
  * `method judge RUN_ID right|wrong [--step ID] [--note TEXT] [--corrected FILE] [--json]`: record the person's reaction
35
34
  * to a run's result. RUN_ID is the dashboard run ID or this computer's run ID; the server resolves both. When the run's
36
35
  * content stays on this computer (run_data: device), the account gets the verdict and step only, and the judgment with
37
- * its note and corrected result goes in .method/judgments.jsonl next to the Method file. A wrong judgment then fixes the
38
- * Method here, in the foreground: method judge makes a regression case from the note, finds a change with the user's
39
- * own Claude Code or Codex, and applies it only when it fixes the run and breaks no earlier correction. It prints one
40
- * line: what changed, or why nothing did.
36
+ * its note and corrected result goes in .method/judgments.jsonl next to the Method file. A wrong judgment then starts
37
+ * the fix on this computer in its own process and returns at once with one line; method wait prints what the fix did,
38
+ * and so does the next method run of the Method.
41
39
  */
42
- export async function judgeCommand(args, clientFactory, options = {}) {
43
- const usage = 'Use method judge RUN_ID right|wrong [--step STEP_ID] [--note TEXT] [--corrected FILE].';
40
+ export async function judgeCommand(args, clientFactory) {
41
+ const usage = 'Use method judge RUN_ID right|wrong [--step STEP_ID] [--note TEXT] [--corrected FILE], or method judge RUN_ID keep JUDGMENT_ID.';
44
42
  const { values, positionals } = parseArgs({ args, allowPositionals: true, options: {
45
43
  step: { type: 'string' }, note: { type: 'string' }, corrected: { type: 'string' }, json: { type: 'boolean' }, server: { type: 'string' }
46
44
  } });
47
45
  const [runId, verdict] = positionals;
46
+ if (runId && verdict === 'keep' && positionals.length === 3)
47
+ return keepCommand(runId, positionals[2], values, clientFactory);
48
48
  if (!runId || !verdict || positionals.length !== 2)
49
49
  throw Error(usage);
50
50
  if (verdict !== 'right' && verdict !== 'wrong')
@@ -72,24 +72,108 @@ export async function judgeCommand(args, clientFactory, options = {}) {
72
72
  id: response.judgment.id, run_id: runId, account_run_id: response.judgment.run_id ?? null, run_dir: runDir, method_id: response.judgment.method_id ?? null,
73
73
  file: basename(runFile), step_id: step ?? null, verdict, note: note ?? null, ...(corrected !== undefined ? { corrected } : {}), created_at: response.judgment.created_at,
74
74
  });
75
- const recorded = judgmentLine(response.judgment);
76
- if (verdict === 'right') {
77
- process.stdout.write(values.json ? JSON.stringify({ judgment: response.judgment }, null, 2) + '\n' : recorded + '\n');
75
+ // A judgment that contradicts another person's judgment of the same step is held: show both, with the two choices.
76
+ const pairs = response.held ? await heldPairs(client, response.judgment.method_id, response.judgment.id) : [];
77
+ if (verdict === 'right' || response.held) {
78
+ print(values.json ? { judgment: response.judgment, ...(pairs.length ? { held: pairs } : {}) } : [verdict === 'right' && !response.held ? approvedLine : 'Noted.', ...pairs.flatMap(heldLines)]);
78
79
  return;
79
80
  }
80
- // The Method file: the one that ran the run, else the Method's home file on this computer.
81
- const home = response.judgment.method_id ? readHome(client.server, response.judgment.method_id)?.path : undefined;
82
- const file = runFile ?? (home && existsSync(home) ? home : undefined);
83
- if (!file) {
84
- const line = `${recorded} The Method file is not on this computer, so the next method run of it on a computer fixes it.`;
85
- process.stdout.write(values.json ? JSON.stringify({ judgment: response.judgment, fix: null }, null, 2) + '\n' : line + '\n');
81
+ return startFix(client, response.judgment, runFile, values);
82
+ }
83
+ const print = (value) => process.stdout.write(Array.isArray(value) ? value.join('\n') + '\n' : JSON.stringify(value, null, 2) + '\n');
84
+ /** The run's Method file fixes the correction: the file that ran the run, else the Method's home file on this computer. */
85
+ export function fileForJudgment(server, judgment, runFile) {
86
+ const home = judgment.method_id ? readHome(server, judgment.method_id)?.path : undefined;
87
+ return runFile ?? (home && existsSync(home) ? home : undefined);
88
+ }
89
+ /** Start the fix of a correction in its own process and return: method wait gives its outcome. */
90
+ async function startFix(client, judgment, runFile, values, line = workingLine) {
91
+ const file = fileForJudgment(client.server, judgment, runFile);
92
+ if (!file || !judgment.method_id) {
93
+ print(values.json ? { judgment, fix: null } : [notHereLine]);
86
94
  return;
87
95
  }
88
- const fix = await fixCorrections(client, file, options);
89
- if (values.json) {
90
- process.stdout.write(JSON.stringify({ judgment: response.judgment, fix: fix ?? null }, null, 2) + '\n');
96
+ // The fix starts from the newest version; a teammate's change of the same step stops it until the person combines them.
97
+ const caught = await catchUp(client, file, 'fixing');
98
+ if (caught.status === 'conflict') {
99
+ print(values.json ? { judgment, fix: null, stopped: caught.result } : [caught.result.failure.headline]);
100
+ process.exitCode = 2;
91
101
  return;
92
102
  }
93
- const line = !fix ? `${recorded} Another loop is fixing it.` : fix.corrections > 1 ? `${recorded} ${waitingLine(fix)}` : `${recorded} ${fix.line}`;
94
- process.stdout.write(line + '\n');
103
+ if ('line' in caught)
104
+ process.stderr.write(caught.line + '\n');
105
+ markWorking(judgment.method_id, file, [judgment.id]);
106
+ try {
107
+ await fixStarter.start(file, judgment.method_id, [judgment.id], client.server);
108
+ }
109
+ catch (error) {
110
+ saveError(judgment.method_id, file, judgment.id, error.message);
111
+ throw error;
112
+ }
113
+ print(values.json ? { judgment, fix: { status: 'working', wait: `method wait ${judgment.id}` } } : [line]);
114
+ }
115
+ /** The held pairs of a Method, from its judgments in the account. Nothing when they cannot be read. */
116
+ async function heldPairs(client, methodId, mine) {
117
+ if (!methodId)
118
+ return [];
119
+ let judgments;
120
+ try {
121
+ judgments = (await client.request(`/api/cli/methods/${encodeURIComponent(methodId)}/judgments`)).judgments ?? [];
122
+ }
123
+ catch {
124
+ return [];
125
+ }
126
+ const byId = new Map(judgments.map(j => [j.id, j])), pairs = [], seen = new Set();
127
+ for (const j of judgments) {
128
+ const other = j.outcome === 'held' && j.held_with ? byId.get(j.held_with) : undefined;
129
+ if (!other || seen.has(j.id) || seen.has(other.id))
130
+ continue;
131
+ seen.add(j.id);
132
+ seen.add(other.id);
133
+ const [first, second] = mine === other.id ? [other, j] : [j, other];
134
+ pairs.push({ run_id: j.run_id, step_id: j.step_id ?? null, judgments: [first, second].map(x => ({ id: x.id, verdict: x.verdict, noted_by: x.noted_by ?? null, note: x.note ?? null })) });
135
+ }
136
+ return pairs;
137
+ }
138
+ /** The person's line of a held pair, then the agent's line with the two commands. */
139
+ const heldLines = (pair) => [heldPairLine(pair.judgments),
140
+ `Ask the person which one is right, then keep it: ${pair.judgments.map(j => `method judge ${pair.run_id} keep ${j.id} (${j.noted_by ?? 'this one'}'s)`).join(' or ')}.`];
141
+ /** `method judge RUN keep JUDGMENT_ID`: keep this judgment of a held pair; the other one is set aside. A kept wrong judgment is fixed. */
142
+ async function keepCommand(runId, judgmentId, values, clientFactory) {
143
+ const client = clientFactory(values.server ?? DEFAULT_SERVER);
144
+ const response = await client.request(`/api/judgments/${encodeURIComponent(judgmentId)}/settle`, 'POST', { keep: true });
145
+ const kept = response.judgment;
146
+ const recorded = `Kept ${kept?.noted_by ? `${kept.noted_by}'s view` : 'this view'}: the result is ${kept?.verdict ?? 'as judged'}.`;
147
+ if (kept?.verdict !== 'wrong' || !kept.method_id) {
148
+ print(values.json ? { judgment: kept ?? { id: judgmentId } } : [recorded]);
149
+ return;
150
+ }
151
+ const runDir = findRunFolder(runId);
152
+ return startFix(client, kept, runDir ? methodFileOfRun(runDir) : undefined, values, `${recorded} ${workingLine}`);
153
+ }
154
+ /**
155
+ * `method answer JUDGMENT_ID "THEIR WORDS"`: send the person's answer to the question that a fix asked, and start the
156
+ * fix again with it. For a Method whose run content stays on this computer, the account keeps no answer, so the words
157
+ * also stay next to the Method file.
158
+ */
159
+ export async function answerJudgmentCommand(args, clientFactory) {
160
+ const { values, positionals } = parseArgs({ args, allowPositionals: true, options: { json: { type: 'boolean' }, server: { type: 'string' } } });
161
+ const [id, ...words] = positionals, answer = words.join(' ').trim();
162
+ if (!id || !answer)
163
+ throw Error('Use method answer JUDGMENT_ID "THE PERSON\'S WORDS".');
164
+ if (answer.length > maxNoteLength)
165
+ throw Error(`The answer has ${answer.length} characters; the most is ${maxNoteLength}.`);
166
+ const stored = storedOutcomes().find(r => r.judgment === id);
167
+ const file = stored && existsSync(stored.file) ? stored.file : undefined;
168
+ const device = !!file && readDocument(file).run_data === 'device';
169
+ const client = clientFactory(values.server ?? DEFAULT_SERVER);
170
+ // The account keeps no answer for a device Method; the words stay next to the Method file.
171
+ const response = await client.request(`/api/cli/judgments/${encodeURIComponent(id)}/answer`, 'POST', { answer });
172
+ const judgment = response.judgment ?? { id, method_id: stored?.method_id };
173
+ if (device && file) {
174
+ const before = localJudgments(file).filter(j => j.id === id).at(-1);
175
+ if (before)
176
+ saveLocalJudgment(file, { ...before, answer });
177
+ }
178
+ return startFix(client, { id, method_id: judgment.method_id ?? stored?.method_id ?? '' }, file, values, workingAnswerLine);
95
179
  }
@@ -0,0 +1,43 @@
1
+ import { type MethodClient } from './method-client.js';
2
+ export type LearnedRule = {
3
+ id: string;
4
+ when: string | null;
5
+ then: string;
6
+ by: string | null;
7
+ from: {
8
+ judgment?: string;
9
+ case?: string;
10
+ } | null;
11
+ status: 'active' | 'retired';
12
+ created: string;
13
+ retired_at: string | null;
14
+ retired_reason: string | null;
15
+ /** What the fix loop keeps about a rule: base_failures, base_runs, dormant, merged_from. */
16
+ meta?: {
17
+ base_failures?: number;
18
+ base_runs?: number;
19
+ dormant?: boolean;
20
+ merged_from?: string[];
21
+ };
22
+ };
23
+ export type Learned = {
24
+ format: 'method-learned/1';
25
+ steps: Record<string, LearnedRule[]>;
26
+ };
27
+ export declare const learnedFile: (file: string) => string;
28
+ export declare function readLearned(file: string): Learned;
29
+ /** The active rules of a Method in a stable order: by step, then by date. */
30
+ export declare function activeRules(learned: Learned): {
31
+ step: string;
32
+ rule: LearnedRule;
33
+ }[];
34
+ /** How often each rule applied, from the learned.applied events of this Method's runs on this computer. */
35
+ export declare function appliedCounts(file: string): Promise<Map<string, number>>;
36
+ /** A rule in plain words: "When the client is Acme, accept five years." */
37
+ export declare const ruleText: (rule: Pick<LearnedRule, "when" | "then">) => string;
38
+ /**
39
+ * `method learned FILE [--remove NUMBER|ID] [--json]`: the rules the agent learned from corrections, in plain words,
40
+ * with who taught each one and how often it applied. --remove retires one rule (by its number in the list or its ID);
41
+ * the change is saved as a new version, and method undo brings it back.
42
+ */
43
+ export declare function learnedCommand(args: string[], clientFactory: (server: string) => MethodClient): Promise<void>;
@@ -0,0 +1,97 @@
1
+ import { existsSync, readFileSync, writeFileSync } from 'node:fs';
2
+ import { basename, dirname, join } from 'node:path';
3
+ import { parseArgs } from 'node:util';
4
+ import { authoringPath } from './authoring.js';
5
+ import { DEFAULT_SERVER } from './method-client.js';
6
+ import { recordChange, saveVersion, snapshot } from './undo.js';
7
+ import { removedRuleLine } from './fix-lines.js';
8
+ export const learnedFile = (file) => join(dirname(file), 'learned.json');
9
+ export function readLearned(file) {
10
+ const path = learnedFile(file);
11
+ if (!existsSync(path))
12
+ return { format: 'method-learned/1', steps: {} };
13
+ const value = JSON.parse(readFileSync(path, 'utf8'));
14
+ if (value?.format !== 'method-learned/1' || typeof value.steps !== 'object')
15
+ throw Error(`${basename(path)} is not a learned rules file.`);
16
+ return value;
17
+ }
18
+ /** The active rules of a Method in a stable order: by step, then by date. */
19
+ export function activeRules(learned) {
20
+ return Object.entries(learned.steps).flatMap(([step, rules]) => rules.filter(r => r.status === 'active').map(rule => ({ step, rule })))
21
+ .sort((a, b) => a.rule.created.localeCompare(b.rule.created));
22
+ }
23
+ /** How often each rule applied, from the learned.applied events of this Method's runs on this computer. */
24
+ export async function appliedCounts(file) {
25
+ const { localRuns } = await import('./proposals.js');
26
+ const counts = new Map();
27
+ for (const run of await localRuns(file)) {
28
+ let text = '';
29
+ try {
30
+ text = readFileSync(join(run.dir, 'events.jsonl'), 'utf8');
31
+ }
32
+ catch {
33
+ continue;
34
+ }
35
+ for (const line of text.split('\n')) {
36
+ if (!line.includes('learned.applied'))
37
+ continue;
38
+ try {
39
+ const event = JSON.parse(line);
40
+ if (event.event === 'learned.applied')
41
+ for (const id of event.applied ?? [])
42
+ counts.set(id, (counts.get(id) ?? 0) + 1);
43
+ }
44
+ catch { /* a partial line */ }
45
+ }
46
+ }
47
+ return counts;
48
+ }
49
+ const day = (iso) => { const date = new Date(iso); return Number.isNaN(date.getTime()) ? '' : date.toLocaleDateString('en-GB', { day: 'numeric', month: 'short', timeZone: 'UTC' }); };
50
+ /** A rule in plain words: "When the client is Acme, accept five years." */
51
+ export const ruleText = (rule) => {
52
+ const then = rule.then.trim().replace(/[.\s]+$/, '');
53
+ // A condition reads as part of the sentence: "The email is in German." becomes "When the email is in German, ...".
54
+ const when = rule.when?.trim().replace(/^when\s+/i, '').replace(/[.,\s]+$/, '').replace(/^[A-Z](?=[a-z])/, c => c.toLowerCase());
55
+ return when ? `When ${when}, ${then.charAt(0).toLowerCase()}${then.slice(1)}.` : `${then}.`;
56
+ };
57
+ const usedText = (n) => n === 0 ? 'not used yet' : n === 1 ? 'used once' : `used ${n} times`;
58
+ /**
59
+ * `method learned FILE [--remove NUMBER|ID] [--json]`: the rules the agent learned from corrections, in plain words,
60
+ * with who taught each one and how often it applied. --remove retires one rule (by its number in the list or its ID);
61
+ * the change is saved as a new version, and method undo brings it back.
62
+ */
63
+ export async function learnedCommand(args, clientFactory) {
64
+ const { values, positionals } = parseArgs({ args, allowPositionals: true, options: { remove: { type: 'string' }, json: { type: 'boolean' }, server: { type: 'string' } } });
65
+ if (positionals.length !== 1)
66
+ throw Error('Use method learned FILE [--remove NUMBER].');
67
+ const file = authoringPath(positionals[0]);
68
+ const learned = readLearned(file), rules = activeRules(learned);
69
+ if (values.remove !== undefined) {
70
+ const wanted = values.remove.trim(), index = /^\d+$/.test(wanted) ? Number(wanted) - 1 : rules.findIndex(r => r.rule.id === wanted);
71
+ const chosen = rules[index];
72
+ if (!chosen)
73
+ throw Error(`There is no learned rule ${wanted}. List them with method learned ${basename(file)}.`);
74
+ const client = clientFactory(values.server ?? DEFAULT_SERVER);
75
+ const { signedInName } = await import('./quality.js');
76
+ const name = await signedInName(client);
77
+ const before = snapshot(file, ['learned.json']);
78
+ Object.assign(chosen.rule, { status: 'retired', retired_at: new Date().toISOString(), retired_reason: `Removed by ${name ?? 'the person'}.` });
79
+ writeFileSync(learnedFile(file), JSON.stringify(learned, null, 2) + '\n');
80
+ const text = ruleText(chosen.rule);
81
+ recordChange(file, 'learned', `Removed the learned rule "${text}"`, `the rule "${text}" is back`, before);
82
+ await saveVersion(client, file, `Removed a learned rule: ${text}`.slice(0, 500));
83
+ process.stdout.write(values.json ? JSON.stringify({ removed: chosen.rule }, null, 2) + '\n' : removedRuleLine(text) + '\n');
84
+ return;
85
+ }
86
+ const counts = await appliedCounts(file);
87
+ if (values.json) {
88
+ process.stdout.write(JSON.stringify({ rules: rules.map(({ step, rule }) => ({ ...rule, step, applied: counts.get(rule.id) ?? 0, text: ruleText(rule) })) }, null, 2) + '\n');
89
+ return;
90
+ }
91
+ if (!rules.length) {
92
+ process.stdout.write('This agent has not learned any rules from corrections yet.\n');
93
+ return;
94
+ }
95
+ const lines = rules.map(({ rule }, i) => `${i + 1}. ${ruleText(rule)} (${[rule.by, day(rule.created)].filter(Boolean).join(', ')}${rule.by || day(rule.created) ? '; ' : ''}${usedText(counts.get(rule.id) ?? 0)})`);
96
+ process.stdout.write(['What this agent learned from corrections:', ...lines, `Remove one: method learned ${basename(file)} --remove NUMBER`].join('\n') + '\n');
97
+ }
@@ -33,8 +33,6 @@ export function parse(args) {
33
33
  export async function run(target, flags, syncFactory) {
34
34
  const path = isWorkflowLink(target) ? (await saveWorkflowLink(target, process.cwd())).path : resolve(target);
35
35
  (await import("./legacy.js")).convertLegacyOrThrow(path);
36
- if (!syncFactory)
37
- await (await import("./home.js")).followAccount(new MethodClient(flags.server), path);
38
36
  loadWorkflow(methodContent(readDocument(path)));
39
37
  const notice = await (await import("./quality.js")).casesNotice(path);
40
38
  if (notice)
@@ -51,8 +49,9 @@ export async function run(target, flags, syncFactory) {
51
49
  flags = await withInputsFrom(flags, flags["run-dir"], path, client);
52
50
  }
53
51
  const account = syncFactory ? undefined : await accountSync(path, flags);
52
+ let result;
54
53
  try {
55
- await runCurrentFile(path, flags, syncFactory ?? account?.sync);
54
+ result = await runCurrentFile(path, flags, syncFactory ?? account?.sync);
56
55
  }
57
56
  finally {
58
57
  if (account) {
@@ -62,6 +61,12 @@ export async function run(target, flags, syncFactory) {
62
61
  // Script cards are made after the run, so that this run's inputs can be replayed. They never delay or fail it.
63
62
  if (savedVersions(account.client.server)[account.version.version_id])
64
63
  (await import("./explain.js")).startBackgroundExplain(path, account.version.workflow, account.version.version_id, account.client.server);
64
+ // The output of this run is judged against the corrections that recent fixes answered, in the background.
65
+ if (result?.status === "completed" && !flags.resume) {
66
+ const { watchedCorrections } = await import("./watch.js");
67
+ if (watchedCorrections(path).length)
68
+ (await import("./fix-outcomes.js")).fixStarter.watch(path, flags["run-dir"], account.client.server);
69
+ }
65
70
  }
66
71
  }
67
72
  }
@@ -15,6 +15,8 @@ export type LocalJudgment = {
15
15
  note: string | null;
16
16
  corrected?: unknown;
17
17
  created_at: string;
18
+ /** The person's answer to the fix's question; a later line of the same judgment adds it. */
19
+ answer?: string;
18
20
  };
19
21
  /** The file that a run folder records: the Method file that method run ran. */
20
22
  export declare const methodFileRecord = "method-file.json";
@@ -0,0 +1,38 @@
1
+ export type MergeConflict = {
2
+ path: string;
3
+ base: unknown;
4
+ ours: unknown;
5
+ theirs: unknown;
6
+ };
7
+ /** Not clean: workflow keeps ours at each conflict. */
8
+ export type MergeResult = {
9
+ clean: true;
10
+ workflow: any;
11
+ } | {
12
+ clean: false;
13
+ conflicts: MergeConflict[];
14
+ workflow: any;
15
+ };
16
+ /**
17
+ * Three-way merge of Method documents, per step and per top-level key. base is the version both sides started from
18
+ * (an empty base when there is none), ours is the local file, and theirs is the other version.
19
+ */
20
+ export declare function mergeMethod(base: any, ours: any, theirs: any): MergeResult;
21
+ export type PackageFile = {
22
+ path: string;
23
+ sha256: string;
24
+ };
25
+ export type FilesMergeResult<T extends PackageFile = PackageFile> = {
26
+ clean: boolean;
27
+ files: T[];
28
+ conflicts: {
29
+ path: string;
30
+ }[];
31
+ };
32
+ /**
33
+ * Three-way merge of a Method's package files, by path and sha256. A file that only one side added, changed or removed
34
+ * takes that change; a file that both sides changed in different ways is a conflict (ours stays in files).
35
+ */
36
+ export declare function mergeFiles<T extends PackageFile>(base: T[], ours: T[], theirs: T[]): FilesMergeResult<T>;
37
+ /** The steps whose definition differs between two documents. */
38
+ export declare function changedSteps(before: any, after: any): string[];
@@ -0,0 +1,65 @@
1
+ import { isDeepStrictEqual } from 'node:util';
2
+ const isObject = (value) => !!value && typeof value === 'object' && !Array.isArray(value);
3
+ /** Keys of ours in their order; a key that only theirs has goes after the key before it in theirs. */
4
+ function keyOrder(ours, theirs) {
5
+ const keys = Object.keys(ours), theirKeys = Object.keys(theirs);
6
+ theirKeys.forEach((key, index) => {
7
+ if (keys.includes(key))
8
+ return;
9
+ const before = theirKeys.slice(0, index).reverse().find(other => keys.includes(other));
10
+ keys.splice(before === undefined ? 0 : keys.indexOf(before) + 1, 0, key);
11
+ });
12
+ return keys;
13
+ }
14
+ /**
15
+ * Three-way merge of Method documents, per step and per top-level key. base is the version both sides started from
16
+ * (an empty base when there is none), ours is the local file, and theirs is the other version.
17
+ */
18
+ export function mergeMethod(base, ours, theirs) {
19
+ if (isDeepStrictEqual(ours, base))
20
+ return { clean: true, workflow: theirs };
21
+ if (isDeepStrictEqual(theirs, base) || isDeepStrictEqual(ours, theirs))
22
+ return { clean: true, workflow: ours };
23
+ const conflicts = [];
24
+ const pick = (path, b, o, t) => {
25
+ if (isDeepStrictEqual(o, t) || isDeepStrictEqual(b, t))
26
+ return o;
27
+ if (isDeepStrictEqual(b, o))
28
+ return t;
29
+ conflicts.push({ path, base: b, ours: o, theirs: t });
30
+ return o;
31
+ };
32
+ const merge = (path, b, o, t, nested) => {
33
+ const result = {};
34
+ for (const key of keyOrder(o, t)) {
35
+ const at = path ? `${path}.${key}` : key;
36
+ const value = nested(key) && isObject(o[key]) && isObject(t[key]) && isObject(b?.[key] ?? {})
37
+ ? merge(at, b?.[key] ?? {}, o[key], t[key], () => false) : pick(at, b?.[key], o[key], t[key]);
38
+ if (value !== undefined)
39
+ result[key] = value;
40
+ }
41
+ return result;
42
+ };
43
+ const workflow = merge('', isObject(base) ? base : {}, ours, theirs, key => key === 'steps');
44
+ return conflicts.length ? { clean: false, conflicts, workflow } : { clean: true, workflow };
45
+ }
46
+ /**
47
+ * Three-way merge of a Method's package files, by path and sha256. A file that only one side added, changed or removed
48
+ * takes that change; a file that both sides changed in different ways is a conflict (ours stays in files).
49
+ */
50
+ export function mergeFiles(base, ours, theirs) {
51
+ const byPath = (list) => new Map(list.map(f => [f.path, f]));
52
+ const b = byPath(base), o = byPath(ours), t = byPath(theirs);
53
+ const files = [], conflicts = [];
54
+ for (const path of [...new Set([...o.keys(), ...t.keys(), ...b.keys()])].sort()) {
55
+ const [bs, os, ts] = [b.get(path)?.sha256, o.get(path)?.sha256, t.get(path)?.sha256];
56
+ const kept = os === ts || bs === ts ? o.get(path) : bs === os ? t.get(path) : (conflicts.push({ path }), o.get(path));
57
+ if (kept)
58
+ files.push(kept);
59
+ }
60
+ return { clean: !conflicts.length, files, conflicts };
61
+ }
62
+ /** The steps whose definition differs between two documents. */
63
+ export function changedSteps(before, after) {
64
+ return [...new Set([...Object.keys(before?.steps ?? {}), ...Object.keys(after?.steps ?? {})])].filter(id => !isDeepStrictEqual(before?.steps?.[id], after?.steps?.[id]));
65
+ }
@@ -90,7 +90,7 @@ export class MethodClient {
90
90
  throw Object.assign(Error(`${status}: ${title || statusText || "Method returned a non-JSON response"} (${method} ${path}). Local run files are preserved.`), { status });
91
91
  }
92
92
  if (!ok)
93
- throw Object.assign(Error(`${status}: ${String(result.message ?? result.error ?? "Method request failed").slice(0, 1000)}`), { status, code: typeof result.code === 'string' ? result.code.slice(0, 100) : 'method_request_failed' });
93
+ throw Object.assign(Error(`${status}: ${String(result.message ?? result.error ?? "Method request failed").slice(0, 1000)}`), { status, code: typeof result.code === 'string' ? result.code.slice(0, 100) : 'method_request_failed', body: result });
94
94
  return result;
95
95
  }
96
96
  /** Upload (data) or download bytes. Retries a lost connection or a server error twice; stops only when no bytes move. */
@@ -3,12 +3,13 @@ import type { MethodClient } from './method-client.js';
3
3
  export declare const runtimeVersion: string;
4
4
  export declare const fileHash: (data: Uint8Array) => string;
5
5
  export declare function contained(root: string, name: string): string;
6
- /** The files that a saved package holds: declared files, script entrypoints, and dependency lockfiles. */
6
+ /** The files that a saved package holds: declared files, script entrypoints, dependency lockfiles, and learned rules. */
7
7
  export declare function packageFileNames(root: string, workflow: any): string[];
8
8
  /**
9
9
  * The files of the Method's cases/ folder. A Method with account run data saves its cases with each version; a Method
10
10
  * with device run data keeps them on this computer. Hidden files and links are not included. A file over the package
11
- * limit of 20 MB is not included; its name goes to tooLarge, so the save can say so.
11
+ * limit of 20 MB is not included; its name goes to tooLarge, so the save can say so. A case's snapshot/ folder (copies of
12
+ * the files its run read) is never included: it stays on this computer.
12
13
  */
13
14
  export declare const maxPackageFileBytes = 20000000;
14
15
  export declare function caseFileNames(root: string, tooLarge?: string[]): string[];
@@ -13,17 +13,18 @@ export function contained(root, name) {
13
13
  throw Error(`File leaves the Method folder: ${name}`);
14
14
  return path;
15
15
  }
16
- /** The files that a saved package holds: declared files, script entrypoints, and dependency lockfiles. */
16
+ /** The files that a saved package holds: declared files, script entrypoints, dependency lockfiles, and learned rules. */
17
17
  export function packageFileNames(root, workflow) {
18
18
  const executions = [...Object.values(workflow.steps).flatMap((s) => [s.do, s.check, ...Object.values(s.effects ?? {}).flatMap((e) => [e.observe, e.judge])]),
19
19
  ...Object.values(workflow.tools ?? {}).map((t) => t.run)].filter(Boolean);
20
20
  return [...new Set([...(workflow.files ?? []), ...executions.filter(e => e.kind === 'run').map(e => e.entrypoint),
21
- ...['package.json', 'package-lock.json', 'pyproject.toml', 'uv.lock'].filter(name => existsSync(resolve(root, name)))])].sort();
21
+ ...['package.json', 'package-lock.json', 'pyproject.toml', 'uv.lock', 'learned.json'].filter(name => existsSync(resolve(root, name)))])].sort();
22
22
  }
23
23
  /**
24
24
  * The files of the Method's cases/ folder. A Method with account run data saves its cases with each version; a Method
25
25
  * with device run data keeps them on this computer. Hidden files and links are not included. A file over the package
26
- * limit of 20 MB is not included; its name goes to tooLarge, so the save can say so.
26
+ * limit of 20 MB is not included; its name goes to tooLarge, so the save can say so. A case's snapshot/ folder (copies of
27
+ * the files its run read) is never included: it stays on this computer.
27
28
  */
28
29
  export const maxPackageFileBytes = 20_000_000;
29
30
  export function caseFileNames(root, tooLarge = []) {
@@ -36,6 +37,9 @@ export function caseFileNames(root, tooLarge = []) {
36
37
  const name = `${relativeDir}/${entry.name}`;
37
38
  if (entry.name.startsWith('.'))
38
39
  continue;
40
+ // cases/ID/snapshot/ holds copies of what a run read (client documents): it stays on this computer.
41
+ if (entry.isDirectory() && entry.name === 'snapshot' && /^cases\/[^/]+$/.test(relativeDir))
42
+ continue;
39
43
  try {
40
44
  packagePath(name);
41
45
  }
@@ -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.\nResults and improvements: judge, changes.\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, wait, answer, learned, undo, changes.\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. */