@method-ai-hq/sdk 0.19.0 → 0.20.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 (31) hide show
  1. package/README.md +1 -1
  2. package/dist/packages/sdk/package.json +2 -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/fix-lines.d.ts +82 -0
  7. package/dist/packages/sdk/src/fix-lines.js +63 -0
  8. package/dist/packages/sdk/src/fix-outcomes.d.ts +72 -0
  9. package/dist/packages/sdk/src/fix-outcomes.js +201 -0
  10. package/dist/packages/sdk/src/improve.d.ts +132 -34
  11. package/dist/packages/sdk/src/improve.js +523 -176
  12. package/dist/packages/sdk/src/judge.d.ts +12 -11
  13. package/dist/packages/sdk/src/judge.js +63 -39
  14. package/dist/packages/sdk/src/learned.d.ts +43 -0
  15. package/dist/packages/sdk/src/learned.js +97 -0
  16. package/dist/packages/sdk/src/local-cli.js +8 -1
  17. package/dist/packages/sdk/src/local-judgments.d.ts +2 -0
  18. package/dist/packages/sdk/src/method-files.d.ts +3 -2
  19. package/dist/packages/sdk/src/method-files.js +7 -3
  20. package/dist/packages/sdk/src/method-help.d.ts +1 -1
  21. package/dist/packages/sdk/src/method-help.js +16 -12
  22. package/dist/packages/sdk/src/method.js +51 -9
  23. package/dist/packages/sdk/src/quality.d.ts +62 -11
  24. package/dist/packages/sdk/src/quality.js +56 -21
  25. package/dist/packages/sdk/src/undo.d.ts +50 -0
  26. package/dist/packages/sdk/src/undo.js +108 -0
  27. package/dist/packages/sdk/src/watch.d.ts +20 -0
  28. package/dist/packages/sdk/src/watch.js +86 -0
  29. package/dist/packages/workflow-language/src/inspection.d.ts +30 -0
  30. package/dist/packages/workflow-language/src/inspection.js +3 -1
  31. package/package.json +2 -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,21 +11,17 @@ 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. When a fix of
24
- * the same Method already runs on this computer, the correction joins it: that fix takes it in one more pass, and this
25
- * command waits for it. It prints one 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.
26
21
  */
27
- export declare function judgeCommand(args: string[], clientFactory: (server: string) => MethodClient, options?: {
28
- run?: RunFile;
29
- }): 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;
30
25
  /** Two judgments of the same run and step by two people with opposite verdicts: neither is fixed until one is kept. */
31
26
  export type HeldPair = {
32
27
  run_id: string;
@@ -38,3 +33,9 @@ export type HeldPair = {
38
33
  note: string | null;
39
34
  }>;
40
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 { fixCorrection, waitingLine } from './improve.js';
7
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,30 +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. When a fix of
40
- * the same Method already runs on this computer, the correction joins it: that fix takes it in one more pass, and this
41
- * command waits for it. It prints one 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.
42
39
  */
43
- export async function judgeCommand(args, clientFactory, options = {}) {
44
- // The output stream at the start: a fix that runs in this process sends its run's own output to a log meanwhile.
45
- const print = process.stdout.write.bind(process.stdout);
40
+ export async function judgeCommand(args, clientFactory) {
46
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.';
47
42
  const { values, positionals } = parseArgs({ args, allowPositionals: true, options: {
48
43
  step: { type: 'string' }, note: { type: 'string' }, corrected: { type: 'string' }, json: { type: 'boolean' }, server: { type: 'string' }
49
44
  } });
50
45
  const [runId, verdict] = positionals;
51
46
  if (runId && verdict === 'keep' && positionals.length === 3)
52
- return keepCommand(runId, positionals[2], values, clientFactory, options);
47
+ return keepCommand(runId, positionals[2], values, clientFactory);
53
48
  if (!runId || !verdict || positionals.length !== 2)
54
49
  throw Error(usage);
55
50
  if (verdict !== 'right' && verdict !== 'wrong')
@@ -77,41 +72,45 @@ export async function judgeCommand(args, clientFactory, options = {}) {
77
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,
78
73
  file: basename(runFile), step_id: step ?? null, verdict, note: note ?? null, ...(corrected !== undefined ? { corrected } : {}), created_at: response.judgment.created_at,
79
74
  });
80
- const recorded = judgmentLine(response.judgment);
81
75
  // A judgment that contradicts another person's judgment of the same step is held: show both, with the two choices.
82
76
  const pairs = response.held ? await heldPairs(client, response.judgment.method_id, response.judgment.id) : [];
83
77
  if (verdict === 'right' || response.held) {
84
- print(values.json ? JSON.stringify({ judgment: response.judgment, ...(pairs.length ? { held: pairs } : {}) }, null, 2) + '\n' : [recorded, ...pairs.map(heldLine)].join('\n') + '\n');
78
+ print(values.json ? { judgment: response.judgment, ...(pairs.length ? { held: pairs } : {}) } : [verdict === 'right' && !response.held ? approvedLine : 'Noted.', ...pairs.flatMap(heldLines)]);
85
79
  return;
86
80
  }
87
- return fixAfter(client, response.judgment, recorded, runFile, values, options, pairs, print);
81
+ return startFix(client, response.judgment, runFile, values);
88
82
  }
83
+ const print = (value) => process.stdout.write(Array.isArray(value) ? value.join('\n') + '\n' : JSON.stringify(value, null, 2) + '\n');
89
84
  /** The run's Method file fixes the correction: the file that ran the run, else the Method's home file on this computer. */
90
- async function fixAfter(client, judgment, recorded, runFile, values, options, pairs, print = process.stdout.write.bind(process.stdout)) {
91
- const home = judgment.method_id ? readHome(client.server, judgment.method_id)?.path : undefined;
92
- const file = runFile ?? (home && existsSync(home) ? home : undefined);
93
- const held = pairs.map(heldLine), extra = pairs.length ? { held: pairs } : {};
94
- if (!file) {
95
- const line = `${recorded} The Method file is not on this computer, so the next method run of it on a computer fixes it.`;
96
- print(values.json ? JSON.stringify({ judgment, fix: null, ...extra }, null, 2) + '\n' : [line, ...held].join('\n') + '\n');
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]);
97
94
  return;
98
95
  }
99
96
  // The fix starts from the newest version; a teammate's change of the same step stops it until the person combines them.
100
97
  const caught = await catchUp(client, file, 'fixing');
101
98
  if (caught.status === 'conflict') {
102
- print(values.json ? JSON.stringify({ judgment, fix: null, stopped: caught.result, ...extra }, null, 2) + '\n' : [`${recorded} ${caught.result.failure.headline}`, ...held].join('\n') + '\n');
99
+ print(values.json ? { judgment, fix: null, stopped: caught.result } : [caught.result.failure.headline]);
103
100
  process.exitCode = 2;
104
101
  return;
105
102
  }
106
103
  if ('line' in caught)
107
104
  process.stderr.write(caught.line + '\n');
108
- const fix = await fixCorrection(client, file, judgment.id, options);
109
- if (values.json) {
110
- print(JSON.stringify({ judgment, fix: fix ?? null, ...extra }, null, 2) + '\n');
111
- return;
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
112
  }
113
- const line = !fix ? `${recorded} Another loop is fixing it.` : fix.corrections > 1 ? `${recorded} ${waitingLine(fix)}` : `${recorded} ${fix.line}`;
114
- print([line, ...held].join('\n') + '\n');
113
+ print(values.json ? { judgment, fix: { status: 'working', wait: `method wait ${judgment.id}` } } : [line]);
115
114
  }
116
115
  /** The held pairs of a Method, from its judgments in the account. Nothing when they cannot be read. */
117
116
  async function heldPairs(client, methodId, mine) {
@@ -136,20 +135,45 @@ async function heldPairs(client, methodId, mine) {
136
135
  }
137
136
  return pairs;
138
137
  }
139
- const heldLine = (pair) => {
140
- const said = (j) => `${j.noted_by ?? 'Someone'} says it is ${j.verdict}${j.note ? ` ("${j.note.slice(0, 200)}")` : ''}`;
141
- return `Held: ${pair.step_id ? `step ${pair.step_id} of run ${pair.run_id}` : `run ${pair.run_id}`}. ${pair.judgments.map(said).join('; ')}. Nothing is fixed until one is kept: ${pair.judgments.map(j => `method judge ${pair.run_id} keep ${j.id} (${j.noted_by ?? 'this one'}'s)`).join(' or ')}.`;
142
- };
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 ')}.`];
143
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. */
144
- async function keepCommand(runId, judgmentId, values, clientFactory, options) {
142
+ async function keepCommand(runId, judgmentId, values, clientFactory) {
145
143
  const client = clientFactory(values.server ?? DEFAULT_SERVER);
146
144
  const response = await client.request(`/api/judgments/${encodeURIComponent(judgmentId)}/settle`, 'POST', { keep: true });
147
145
  const kept = response.judgment;
148
- const recorded = `Kept ${kept?.noted_by ? `${kept.noted_by}'s judgment` : 'this judgment'}${kept?.verdict ? ` (${kept.verdict})` : ''}; the other one is set aside.`;
146
+ const recorded = `Kept ${kept?.noted_by ? `${kept.noted_by}'s view` : 'this view'}: the result is ${kept?.verdict ?? 'as judged'}.`;
149
147
  if (kept?.verdict !== 'wrong' || !kept.method_id) {
150
- process.stdout.write(values.json ? JSON.stringify({ judgment: kept ?? { id: judgmentId } }, null, 2) + '\n' : recorded + '\n');
148
+ print(values.json ? { judgment: kept ?? { id: judgmentId } } : [recorded]);
151
149
  return;
152
150
  }
153
151
  const runDir = findRunFolder(runId);
154
- return fixAfter(client, kept, recorded, runDir ? methodFileOfRun(runDir) : undefined, values, options, []);
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);
155
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
+ }
@@ -49,8 +49,9 @@ export async function run(target, flags, syncFactory) {
49
49
  flags = await withInputsFrom(flags, flags["run-dir"], path, client);
50
50
  }
51
51
  const account = syncFactory ? undefined : await accountSync(path, flags);
52
+ let result;
52
53
  try {
53
- await runCurrentFile(path, flags, syncFactory ?? account?.sync);
54
+ result = await runCurrentFile(path, flags, syncFactory ?? account?.sync);
54
55
  }
55
56
  finally {
56
57
  if (account) {
@@ -60,6 +61,12 @@ export async function run(target, flags, syncFactory) {
60
61
  // Script cards are made after the run, so that this run's inputs can be replayed. They never delay or fail it.
61
62
  if (savedVersions(account.client.server)[account.version.version_id])
62
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
+ }
63
70
  }
64
71
  }
65
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";
@@ -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. */