@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,46 +1,50 @@
1
1
  /**
2
2
  * Each Method has one home file on this computer: the file its versions are saved from. A copy in a temporary folder,
3
3
  * or in another folder while the home file still exists, is not saved, so one Method never grows two histories.
4
- * Git worktrees and clones of one repository share a home. The home record also holds what the last save or checkout
5
- * wrote, so an unchanged home file can take a newer version from the account.
4
+ * Git worktrees and clones of one repository share a home.
6
5
  */
7
6
  export type Home = {
8
7
  path: string;
9
8
  key: string;
10
- version_id?: string;
11
- files?: Record<string, string>;
12
9
  };
13
10
  export declare function readHome(server: string, methodId: string): Home | undefined;
14
11
  export declare const temporary: (path: string) => boolean;
15
12
  /** The same file in every checkout of one repository has the same key. */
16
13
  export declare function homeKey(file: string): string;
17
- /** What a save or checkout wrote: the Method file and each package file, by hash. */
18
- export declare function writtenFiles(file: string, packageFiles: Array<{
19
- path: string;
20
- sha256: string;
21
- }>): {
22
- [x: string]: string;
23
- };
24
- export declare function recordHome(server: string, methodId: string, file: string, version?: {
25
- version_id: string;
26
- files: Record<string, string>;
27
- }): void;
14
+ export declare function recordHome(server: string, methodId: string, file: string): void;
28
15
  /**
29
16
  * Throws, with the home file named, when this file may not save the Method. Otherwise this file is the home after
30
17
  * the call. A home that is gone, holds another Method, or is a temporary copy gives way to a file outside a
31
18
  * temporary folder.
32
19
  */
33
20
  export declare function claimHome(server: string, methodId: string, file: string): void;
34
- /** True when every file that the last save or checkout wrote is still unchanged in the home folder. */
35
- export declare function unchangedSinceSave(home: Home): boolean;
21
+ type Client = Pick<import('./method-client.js').MethodClient, 'server' | 'token' | 'request' | 'transfer'>;
22
+ /** Both texts of one part that this computer and a teammate changed in different ways. */
23
+ export type CombineConflict = {
24
+ part: string;
25
+ file: string;
26
+ yours: unknown;
27
+ theirs: unknown;
28
+ };
29
+ export type CatchUp = {
30
+ status: 'skipped' | 'current';
31
+ } | {
32
+ status: 'offline' | 'updated';
33
+ line: string;
34
+ } | {
35
+ status: 'conflict';
36
+ result: {
37
+ status: 'needs_you';
38
+ failure: import('../../workflow-language/src/inspection.js').RunFailure;
39
+ conflicts: CombineConflict[];
40
+ };
41
+ };
36
42
  /**
37
- * Before a run of the home file: when nothing in it changed since the last save or checkout on this computer and the
38
- * account has a newer version (saved from another computer or folder), write that version here first. Says nothing;
39
- * any problem leaves the files as they are.
43
+ * Before a run, a fix or a publish of a Method file: bring the file to the newest version of its Method. base is the
44
+ * version this computer last saved or got, ours the file, theirs the newest version. An unchanged file takes theirs;
45
+ * a changed one combines step by step and file by file (an empty base when this computer has none). A part that both
46
+ * sides changed in different ways is a conflict: nothing changes, and the result has both texts. Offline, the file
47
+ * runs as it is. Signed out, a file with no id: line, or a Method with no version yet: nothing.
40
48
  */
41
- export declare function followAccount(client: {
42
- server: string;
43
- token(): string | null | undefined;
44
- request<T>(path: string): Promise<T>;
45
- transfer(path: string): Promise<Uint8Array>;
46
- }, file: string): Promise<boolean>;
49
+ export declare function catchUp(client: Client, file: string, doing?: string): Promise<CatchUp>;
50
+ export {};
@@ -43,13 +43,8 @@ export function homeKey(file) {
43
43
  const path = real(authoringPath(file)), repo = repository(path);
44
44
  return repo ? `${repo.main}:${relative(repo.root, path)}` : path;
45
45
  }
46
- /** What a save or checkout wrote: the Method file and each package file, by hash. */
47
- export function writtenFiles(file, packageFiles) {
48
- const path = authoringPath(file);
49
- return { [relative(dirname(path), path)]: fileHash(readFileSync(path)), ...Object.fromEntries(packageFiles.map(f => [f.path, f.sha256])) };
50
- }
51
- export function recordHome(server, methodId, file, version) {
52
- writePrivateJson(homeFile(server, methodId), { path: authoringPath(file), key: homeKey(file), ...version });
46
+ export function recordHome(server, methodId, file) {
47
+ writePrivateJson(homeFile(server, methodId), { path: authoringPath(file), key: homeKey(file) });
53
48
  }
54
49
  /**
55
50
  * Throws, with the home file named, when this file may not save the Method. Otherwise this file is the home after
@@ -60,7 +55,7 @@ export function claimHome(server, methodId, file) {
60
55
  const home = readHome(server, methodId), key = homeKey(file);
61
56
  if (home && home.key === key) {
62
57
  if (home.path !== authoringPath(file))
63
- recordHome(server, methodId, file, home.version_id ? { version_id: home.version_id, files: home.files ?? {} } : undefined);
58
+ recordHome(server, methodId, file);
64
59
  return;
65
60
  }
66
61
  const homeLives = (() => { try {
@@ -73,65 +68,174 @@ export function claimHome(server, methodId, file) {
73
68
  throw Object.assign(Error(`This Method is saved from ${home.path}. Make the change in that file and run it there; this copy was not run or saved.`), { code: 'other_home', home: home.path });
74
69
  recordHome(server, methodId, file);
75
70
  }
76
- /** True when every file that the last save or checkout wrote is still unchanged in the home folder. */
77
- export function unchangedSinceSave(home) {
78
- if (!home.version_id || !home.files)
79
- return false;
80
- const root = dirname(home.path);
81
- return Object.entries(home.files).every(([name, hash]) => { try {
82
- return fileHash(readFileSync(join(root, name))) === hash;
83
- }
84
- catch {
85
- return false;
86
- } });
87
- }
71
+ const list = (word, items) => items.length ? `${word}${items.length === 1 ? '' : 's'} ${items.join(', ')}` : '';
72
+ const parts = (steps, files) => [list('step', steps), list('file', files)].filter(Boolean).join(' and ');
73
+ const clock = (at) => { const date = at ? new Date(at) : undefined; return date && !isNaN(+date) ? ` from ${String(date.getHours()).padStart(2, '0')}:${String(date.getMinutes()).padStart(2, '0')}` : ''; };
74
+ const clip = (text) => text.length > 20_000 ? `${text.slice(0, 20_000)}…` : text;
75
+ const unreachable = (error) => error?.code === 'network' || error?.code === 'stalled' || error?.name === 'TimeoutError' || error?.name === 'AbortError' || error?.status >= 500 || error?.status === 429;
88
76
  /**
89
- * Before a run of the home file: when nothing in it changed since the last save or checkout on this computer and the
90
- * account has a newer version (saved from another computer or folder), write that version here first. Says nothing;
91
- * any problem leaves the files as they are.
77
+ * Before a run, a fix or a publish of a Method file: bring the file to the newest version of its Method. base is the
78
+ * version this computer last saved or got, ours the file, theirs the newest version. An unchanged file takes theirs;
79
+ * a changed one combines step by step and file by file (an empty base when this computer has none). A part that both
80
+ * sides changed in different ways is a conflict: nothing changes, and the result has both texts. Offline, the file
81
+ * runs as it is. Signed out, a file with no id: line, or a Method with no version yet: nothing.
92
82
  */
93
- export async function followAccount(client, file) {
94
- const path = authoringPath(file);
83
+ export async function catchUp(client, file, doing = 'running') {
84
+ const path = authoringPath(file), root = dirname(path);
85
+ const { methodContent, methodIdPattern, versionIdFor } = await import('./versions.js');
86
+ let document;
87
+ try {
88
+ document = readDocument(path);
89
+ if (typeof document.id !== 'string' || !methodIdPattern.test(document.id) || !client.token())
90
+ return { status: 'skipped' };
91
+ }
92
+ catch {
93
+ return { status: 'skipped' };
94
+ }
95
+ const id = document.id, options = { signal: AbortSignal.timeout(5000), stallMs: 5000 };
96
+ const methodPath = `/api/cli/methods/${encodeURIComponent(id)}`;
97
+ const { lastSaved, recordCheckout } = await import('./outbox.js');
98
+ const record = lastSaved(client.server, id);
99
+ // merge_base: the newest version that the server has as an ancestor of both this computer's version and the newest one.
100
+ let theirs;
101
+ try {
102
+ theirs = await client.request(record?.version_id ? `${methodPath}?base=${encodeURIComponent(record.version_id)}` : methodPath, 'GET', undefined, true, options);
103
+ }
104
+ catch (error) {
105
+ return unreachable(error) ? { status: 'offline', line: `Could not check for a newer version; ${doing} this file.` } : { status: 'skipped' };
106
+ }
107
+ const { loadWorkflow } = await import('../../workflow-language/src/validate.js');
108
+ const { buildPackage } = await import('./method-files.js');
109
+ // A file that does not load fails its run with its own error.
110
+ let ours;
95
111
  try {
96
- const id = (await import('./authoring.js')).readDocument(path).id;
97
- if (typeof id !== 'string' || !client.token())
98
- return false;
99
- const home = readHome(client.server, id);
100
- if (!home || home.key !== homeKey(path) || !unchangedSinceSave(home))
101
- return false;
102
- const latest = await client.request(`/api/cli/methods/${encodeURIComponent(id)}`);
103
- if (!latest?.version_id || latest.version_id === home.version_id)
104
- return false;
105
- const { mkdtempSync, rmSync, copyFileSync, mkdirSync } = await import('node:fs');
106
- const { restorePackage } = await import('./method-files.js');
107
- const staging = mkdtempSync(join(methodCache(), 'follow-'));
112
+ const workflow = loadWorkflow(methodContent(document)), { pack } = await buildPackage(path, workflow);
113
+ ours = { workflow, files: pack.files, version_id: versionIdFor(id, pack.digest) };
114
+ }
115
+ catch {
116
+ return { status: 'skipped' };
117
+ }
118
+ const savedBy = theirs.saved_by ?? null;
119
+ if (ours.version_id === theirs.version_id) {
120
+ if (record?.version_id !== theirs.version_id)
121
+ recordCheckout(client.server, id, theirs.version_id, theirs.version_number, theirs.workflow, { saved_by: savedBy });
122
+ return { status: 'current' };
123
+ }
124
+ if (record?.version_id === theirs.version_id)
125
+ return { status: 'current' };
126
+ let base = { workflow: {}, files: [] };
127
+ // An older server sends no merge_base: then the version this computer last saved or got is the base.
128
+ const baseId = theirs.merge_base !== undefined ? theirs.merge_base : record?.version_id;
129
+ // This computer's version was kept beside the newest one (an earlier CLI recorded it as its last save): it is combined now.
130
+ const beside = record?.version_id && baseId !== record.version_id ? record.version_id : undefined;
131
+ if (record?.version_id && baseId) {
108
132
  try {
109
- await restorePackage(latest, staging, client);
110
- const root = dirname(path), incoming = latest.package?.files ?? [];
111
- // A local file that the last save did not write, and that differs, is the user's: then nothing changes.
112
- for (const f of incoming) {
113
- const local = join(root, f.path);
114
- if (existsSync(local) && !home.files?.[f.path] && fileHash(readFileSync(local)) !== f.sha256)
115
- return false;
116
- }
117
- for (const f of incoming) {
118
- mkdirSync(dirname(join(root, f.path)), { recursive: true, mode: 0o700 });
119
- copyFileSync(join(staging, f.path), join(root, f.path));
120
- }
133
+ const saved = await client.request(`${methodPath}?version=${encodeURIComponent(baseId)}`, 'GET', undefined, true, options);
134
+ base = { workflow: saved.workflow, files: saved.package?.files ?? [] };
121
135
  }
122
- finally {
123
- rmSync(staging, { recursive: true, force: true });
136
+ catch (error) {
137
+ if (unreachable(error))
138
+ return { status: 'offline', line: `Could not check for a newer version; ${doing} this file.` };
139
+ base = { workflow: record.workflow ?? {}, files: [] };
124
140
  }
125
- const { writeDocument } = await import('./authoring.js');
126
- const { methodContent, writeMethodId } = await import('./versions.js');
127
- writeDocument(path, methodContent(latest.workflow));
128
- (await import('./legacy.js')).convertLegacy(path);
129
- writeMethodId(path, id);
130
- (await import('./outbox.js')).recordCheckout(client.server, id, latest.version_id, latest.version_number, methodContent(latest.workflow));
131
- recordHome(client.server, id, path, { version_id: latest.version_id, files: writtenFiles(path, latest.package?.files ?? []) });
132
- return true;
141
+ }
142
+ const { mergeMethod, mergeFiles, changedSteps } = await import('./merge.js');
143
+ const theirFiles = theirs.package?.files ?? [];
144
+ const steps = mergeMethod(base.workflow, ours.workflow, theirs.workflow), files = mergeFiles(base.files, ours.files, theirFiles);
145
+ const ourHashes = new Map(ours.files.map(f => [f.path, f.sha256]));
146
+ // Files that come from theirs. A file here that no save wrote and that differs is the person's own: a conflict too.
147
+ const incoming = files.files.filter(f => ourHashes.get(f.path) !== f.sha256);
148
+ const local = (name) => { try {
149
+ return fileHash(readFileSync(join(root, name)));
133
150
  }
134
151
  catch {
135
- return false;
152
+ return undefined;
153
+ } };
154
+ const { isDeepStrictEqual } = await import('node:util');
155
+ // After a combine stopped, a part whose text changed on this computer since then is combined by the person: theirs is used.
156
+ const stop = record?.conflict?.theirs === theirs.version_id ? record.conflict : undefined;
157
+ const combinedStep = (at, now) => !!stop && at in stop.parts && !isDeepStrictEqual(now, stop.parts[at]);
158
+ const combinedFile = (at) => !!stop && at in stop.files && (local(at) ?? null) !== stop.files[at];
159
+ const fileConflicts = [...files.conflicts.map(c => c.path), ...incoming.filter(f => !ourHashes.has(f.path) && local(f.path) !== undefined && local(f.path) !== f.sha256).map(f => f.path)];
160
+ const stepConflicts = steps.clean ? [] : steps.conflicts;
161
+ const openSteps = stepConflicts.filter(c => !combinedStep(c.path, c.ours)), openFiles = fileConflicts.filter(at => !combinedFile(at));
162
+ const keepLocal = new Set(fileConflicts.filter(combinedFile));
163
+ const take = incoming.filter(f => !keepLocal.has(f.path));
164
+ const download = async (f) => {
165
+ const bytes = await client.transfer(`${methodPath}/files/${f.sha256}?version=${encodeURIComponent(theirs.version_id)}`);
166
+ if (fileHash(bytes) !== f.sha256)
167
+ throw Error(`The newer version's file ${f.path} does not match its checksum.`);
168
+ return bytes;
169
+ };
170
+ const who = savedBy ?? 'A teammate', name = relative(root, path);
171
+ if (openSteps.length || openFiles.length) {
172
+ // Remember this computer's text of each part, so that the next run sees which parts the person combined.
173
+ const { recordCombineStop } = await import('./outbox.js');
174
+ recordCombineStop(client.server, id, {
175
+ theirs: theirs.version_id,
176
+ parts: Object.fromEntries(stepConflicts.map(c => [c.path, combinedStep(c.path, c.ours) ? stop.parts[c.path] : c.ours])),
177
+ files: Object.fromEntries(fileConflicts.map(at => [at, combinedFile(at) ? stop.files[at] : local(at) ?? null])),
178
+ });
179
+ const conflicts = [];
180
+ for (const c of openSteps)
181
+ conflicts.push({ part: c.path.replace(/^steps\./, 'step '), file: name, yours: c.ours, theirs: c.theirs });
182
+ for (const at of openFiles) {
183
+ const theirFile = theirFiles.find(f => f.path === at);
184
+ const text = async () => { try {
185
+ return clip(Buffer.from(await download(theirFile)).toString('utf8'));
186
+ }
187
+ catch (error) {
188
+ return `(not read: ${error.message})`;
189
+ } };
190
+ conflicts.push({ part: `file ${at}`, file: at, yours: (() => { try {
191
+ return clip(readFileSync(join(root, at), 'utf8'));
192
+ }
193
+ catch {
194
+ return null;
195
+ } })(), theirs: theirFile ? await text() : null });
196
+ }
197
+ const where = [...new Set(conflicts.map(c => c.file))];
198
+ const headline = `${who} also changed ${conflicts.map(c => c.part).join(', ')}. Combine the two texts in ${where.join(' and ')}, then run again.`;
199
+ return { status: 'conflict', result: { status: 'needs_you', failure: combineFailure(headline, conflicts, who), conflicts } };
136
200
  }
201
+ try {
202
+ loadWorkflow(steps.workflow);
203
+ }
204
+ catch (error) {
205
+ const headline = `Your changes and ${savedBy ? `${savedBy}'s` : 'the newest'} version combine into a Method that does not load: ${error.message} Combine them in ${name} by hand, then run again.`;
206
+ return { status: 'conflict', result: { status: 'needs_you', failure: combineFailure(headline, [], who), conflicts: [] } };
207
+ }
208
+ // Every file is read before any file changes, so a failed download changes nothing.
209
+ let written;
210
+ try {
211
+ written = await Promise.all(take.map(async (f) => ({ path: f.path, bytes: await download(f) })));
212
+ }
213
+ catch (error) {
214
+ return unreachable(error) ? { status: 'offline', line: `Could not check for a newer version; ${doing} this file.` } : { status: 'skipped' };
215
+ }
216
+ const { mkdirSync, renameSync, writeFileSync } = await import('node:fs');
217
+ const { packagePath } = await import('../../contracts/src/method-package.js');
218
+ for (const f of written) {
219
+ const target = resolve(root, packagePath(f.path));
220
+ mkdirSync(dirname(target), { recursive: true, mode: 0o700 });
221
+ writeFileSync(`${target}.download`, f.bytes, { mode: 0o600 });
222
+ renameSync(`${target}.download`, target);
223
+ }
224
+ if (!isDeepStrictEqual(steps.workflow, ours.workflow))
225
+ (await import('./proposals.js')).writeMethodFile(path, id, steps.workflow);
226
+ (await import('./legacy.js')).convertLegacy(path);
227
+ const fileChanges = (from, to) => [...new Set([...from, ...to].map(f => f.path))].filter(at => from.find(f => f.path === at)?.sha256 !== to.find(f => f.path === at)?.sha256);
228
+ const yours = parts(changedSteps(base.workflow, ours.workflow), fileChanges(base.files, ours.files));
229
+ const kept = !isDeepStrictEqual(ours.workflow, base.workflow) || fileChanges(base.files, ours.files).length > 0;
230
+ // theirs is the parent of the next save; a side version of this computer that the file now combines goes with it.
231
+ recordCheckout(client.server, id, theirs.version_id, theirs.version_number, theirs.workflow, { saved_by: savedBy, ...(kept && (record?.side_version_id ?? beside) ? { merged_version_id: (record?.side_version_id ?? beside) } : {}) });
232
+ const changed = parts(changedSteps(base.workflow, theirs.workflow), fileChanges(base.files, theirFiles));
233
+ return { status: 'updated', line: `Updated to ${savedBy ? `${savedBy}'s` : 'the newest'} version${clock(theirs.saved_at)}${changed ? ` (changed ${changed})` : ''}.${kept ? ` Your changes${yours ? ` to ${yours}` : ''} are kept.` : ''}` };
234
+ }
235
+ /** The failure record of a combine that needs the person: needs_you, with both texts of each part as evidence. */
236
+ function combineFailure(headline, conflicts, who) {
237
+ const text = (value) => typeof value === 'string' ? value : JSON.stringify(value);
238
+ return { owner: 'needs_you', code: 'combine_versions', headline, step: null, step_name: null, iteration: null, failed_iterations: [], total: null, phase: 'setup', check_name: null,
239
+ next: { action: 'fix_method', text: 'Edit the file so that each part keeps both changes, then run again. No run started and no file changed.' },
240
+ evidence: conflicts.flatMap(c => [`Your ${c.part}: ${text(c.yours)}`, `${who}'s ${c.part}: ${text(c.theirs)}`]) };
137
241
  }
@@ -1,4 +1,5 @@
1
1
  import type { MethodClient } from './method-client.js';
2
+ import type { CorrectionOutcome } from './fix-lines.js';
2
3
  /** A judgment as improve.method reads it. new marks the corrections to fix now; run is the account record of a run with no folder here. */
3
4
  export type ImproveJudgment = {
4
5
  id: string;
@@ -13,7 +14,12 @@ export type ImproveJudgment = {
13
14
  created_at: string;
14
15
  new?: boolean;
15
16
  run?: unknown;
17
+ noted_by?: string | null;
18
+ /** The question that an earlier fix asked about this correction, and the person's answer. */
19
+ question?: string;
20
+ answer?: string;
16
21
  };
22
+ export type { CorrectionOutcome };
17
23
  /** Runs a local Method file; the default is the runtime that `method run FILE` uses. */
18
24
  export type RunFile = (file: string, flags: Record<string, unknown>, syncFactory: undefined, onEvent: undefined, client: MethodClient, baseConfig: undefined, secrets: Record<string, string>) => Promise<{
19
25
  status: string;
@@ -35,12 +41,17 @@ export type ImproveProposal = {
35
41
  [key: string]: unknown;
36
42
  };
37
43
  };
44
+ export type ImproveResult = ImproveProposal & {
45
+ capped: string;
46
+ outcomes: CorrectionOutcome[];
47
+ background_cases: string[];
48
+ summary: string;
49
+ };
38
50
  export type FixOutcome = {
39
51
  file: string;
40
- corrections: number;
41
- from_dashboard: number;
52
+ judgments: string[];
42
53
  kind: 'change' | 'none';
43
- line: string;
54
+ applied: boolean;
44
55
  cause: string;
45
56
  steps: string[];
46
57
  cases: {
@@ -50,11 +61,9 @@ export type FixOutcome = {
50
61
  reason: string;
51
62
  }[];
52
63
  };
53
- earlier_passed: number;
54
64
  version?: {
55
65
  version_id: string;
56
66
  url: string;
57
- undo: string;
58
67
  } | undefined;
59
68
  usage: {
60
69
  model_calls: number;
@@ -63,15 +72,31 @@ export type FixOutcome = {
63
72
  } | null;
64
73
  capped: string;
65
74
  log: string;
66
- changed?: string;
75
+ outcomes: CorrectionOutcome[];
76
+ /** The cases that run after an applied change, and the undo record of that change. */
77
+ background_cases: string[];
78
+ undo_id?: string | undefined;
79
+ /** The corrections that the applied change answered, with their notes. */
80
+ fixed: {
81
+ id: string;
82
+ note: string | null;
83
+ }[];
67
84
  };
68
85
  /**
69
- * Read the result of improve.method: {proposal: <SubmitProposal as JSON text>, kind, proposal_id, capped}. A change has
70
- * the tested workflow (and a changed package file for a script change); none has no workflow.
86
+ * One outcome of improve.method in the form the lines read: {id, outcome, detail, reason?, question?, held_with?,
87
+ * learned_rule?, again?, tests?: {case, base, candidate: {passes, runs}, guards: []}, tried?}. A fixed outcome's detail
88
+ * says what is different now; a held outcome's detail is about the loop, so the line names only the other person.
71
89
  */
72
- export declare function readImproveResult(result: unknown): ImproveProposal & {
73
- capped: string;
74
- };
90
+ export declare function loopOutcome(o: any): CorrectionOutcome;
91
+ /**
92
+ * Read the result of improve.method: {proposal: <SubmitProposal as JSON text>, kind, proposal_id, outcomes: <JSON text,
93
+ * one outcome per new correction>, background_cases: [case ids], summary: one plain line}. A change has the tested
94
+ * workflow and the changed package files (a script, learned.json). A pruning sweep (mode prune) has no outcomes; its
95
+ * summary says what it removed, and what it left because nothing checks it.
96
+ */
97
+ export declare function readImproveResult(result: unknown, options?: {
98
+ prune?: boolean;
99
+ }): ImproveResult;
75
100
  /**
76
101
  * Download the published improve.method and its files into the Method cache, once per version. For developers of
77
102
  * improve.method: METHOD_IMPROVE_METHOD=PATH runs that local improve.method file instead, from its own folder.
@@ -82,13 +107,12 @@ export declare function cachedImproveMethod(client: MethodClient): Promise<{
82
107
  version_id: string;
83
108
  }>;
84
109
  /**
85
- * Run improve.method on this computer. Its model steps and its scripts' model requests follow the rule of every run:
110
+ * Run improve.method on this computer (mode prune: the pruning pass). Its model steps and its scripts' model requests follow the rule of every run:
86
111
  * the user's own Claude Code or Codex first, hosted models only without one. The run gets the signed-in key and server
87
112
  * as its IMPROVE_API_KEY and IMPROVE_SERVER secrets, for this run only. Its output goes to a log file.
88
113
  */
89
- export declare function runImprovement(client: MethodClient, file: string, judgments: ImproveJudgment[], runFolders: string[], run?: RunFile): Promise<{
114
+ export declare function runImprovement(client: MethodClient, file: string, judgments: ImproveJudgment[], runFolders: string[], run?: RunFile, mode?: 'prune'): Promise<{
90
115
  log: string;
91
- summary: any;
92
116
  cause: string;
93
117
  workflow: unknown | null;
94
118
  files: {
@@ -102,31 +126,129 @@ export declare function runImprovement(client: MethodClient, file: string, judgm
102
126
  [key: string]: unknown;
103
127
  };
104
128
  capped: string;
129
+ outcomes: CorrectionOutcome[];
130
+ background_cases: string[];
131
+ summary: string;
105
132
  }>;
106
133
  /** The ID of the regression case that a correction makes: one per judgment, so a later loop finds it. */
107
134
  export declare const correctionCaseId: (judgmentId: string) => string;
135
+ /** One thing that the person saw: the result, a step output, a file the run wrote, or a call or effect it made. */
136
+ type Seen = {
137
+ ref: string;
138
+ label: string;
139
+ value: unknown;
140
+ };
141
+ /** What the person saw of a run, with the ref that a case uses for each thing (runtime 0.16.0 refs). */
142
+ export declare function seenOfRun(outcome: any): Seen[];
108
143
  /**
109
- * Make the regression case of a correction, even from vague words such as "it's not even redlined": a model writes 1 to
110
- * 3 plain sentences from the note and the run's result, and method case new keeps them only when they fail on the run.
111
- * A rule that fails nothing gets one more try with the reason. The person's words decide a conflict with an instruction.
144
+ * Make the regression case of a correction, even from vague words such as "it's not even redlined". A model reads what
145
+ * the person saw (the result, the files the run wrote, and the calls and effects it made) and writes 1 to 3 plain
146
+ * sentences, each about one of those things; for a note about form or a missing action, also a small check script. A
147
+ * sentence is kept only when it fails on the corrected run and passes on every approved run of the same part, with two
148
+ * judges when both Claude Code and Codex are signed in. Sentences that do not fail get one more try with the reason.
112
149
  */
113
150
  export declare function caseFromCorrection(file: string, judgment: {
114
151
  id: string;
115
152
  note: string | null;
116
153
  corrected?: unknown;
117
- }, runDir: string): Promise<{
154
+ noted_by?: string | null;
155
+ answer?: string | null;
156
+ }, runDir: string, options?: {
157
+ approvedRuns?: string[];
158
+ }): Promise<{
118
159
  id: string;
119
160
  made: boolean;
120
161
  }>;
162
+ /** The corrections of a Method that wait for a fix. */
163
+ export declare function waitingCorrections(client: MethodClient, file: string, methodId: string, waitMs?: number): Promise<{
164
+ note: string | null;
165
+ corrected: {} | null;
166
+ answer: string | null;
167
+ run_dir: string | undefined;
168
+ id: string;
169
+ run_id: string;
170
+ step_id: string | null;
171
+ verdict: "right" | "wrong";
172
+ source: string;
173
+ created_at: string;
174
+ pending: boolean;
175
+ noted_by?: string | null;
176
+ outcome?: string | null;
177
+ handled_by?: string | null;
178
+ held_with?: string | null;
179
+ question?: string | null;
180
+ }[]>;
121
181
  /**
122
- * Fix the pending corrections of a Method file on this computer: the loop. Returns undefined when nothing waits. It
123
- * takes the pending corrections in the account first, so two computers never fix one correction twice, and gives them
124
- * back when the loop stops before it decides. A subscription limit or another failure of the model service is a plain
125
- * error.
182
+ * Fix the pending corrections of a Method file on this computer: one pass of the loop. Returns undefined when nothing
183
+ * waits. It takes the pending corrections in the account first, so two computers never fix one correction twice, and
184
+ * gives them back when the loop stops before it decides. Each correction gets its own outcome. A subscription limit or
185
+ * another failure of the model service is a plain error.
126
186
  */
127
187
  export declare function fixCorrections(client: MethodClient, file: string, options?: {
128
188
  run?: RunFile;
129
189
  waitMs?: number;
190
+ skip?: Set<string>;
130
191
  }): Promise<FixOutcome | undefined>;
131
- /** The one line for corrections that waited, printed before method run or method judge goes on. */
132
- export declare function waitingLine(outcome: FixOutcome): string;
192
+ /** Whether a fix of the Method runs on this computer now. */
193
+ export declare function fixRunning(methodId: string): boolean;
194
+ /**
195
+ * Fix the waiting corrections of a Method file while holding its lock, pass after pass until none waits, so the
196
+ * corrections that arrive during a pass join the next. Each correction's outcome is saved for method wait. After a pass
197
+ * that changed the file, the other cases run (improve.method's background_cases): a failure undoes that change. Then a
198
+ * pruning pass runs when it is due. Returns undefined when another fix of the Method holds the lock.
199
+ */
200
+ export declare function fixWaiting(client: MethodClient, file: string, options?: {
201
+ run?: RunFile;
202
+ waitMs?: number;
203
+ runCases?: RunCases;
204
+ }): Promise<FixOutcome[] | undefined>;
205
+ /** Runs cases of a Method file (quality.ts runCases); tests replace it. */
206
+ export type RunCases = (file: string, flags: Record<string, never>, options: {
207
+ ids: string[];
208
+ }) => Promise<{
209
+ cases: {
210
+ id: string;
211
+ verdict: string;
212
+ }[];
213
+ }>;
214
+ /**
215
+ * The later check of an applied change: the Method's other cases (improve.method's background_cases) run on the changed
216
+ * file. A failing case that belongs to a retired learned rule (its from.case) brings that rule back. Any other failure
217
+ * undoes the change, saves the restored file as a new version, and opens the corrections it answered again, so the next
218
+ * method run fixes them anew. Each of these says one line on the next method run or method wait.
219
+ */
220
+ export declare function checkBackground(client: MethodClient, file: string, fix: Pick<FixOutcome, 'background_cases' | 'undo_id' | 'fixed' | 'cause'>, options?: {
221
+ runCases?: RunCases;
222
+ }): Promise<{
223
+ failed: string[];
224
+ restored: string[];
225
+ undone: boolean;
226
+ reopened: string[];
227
+ }>;
228
+ /**
229
+ * The pruning pass (improve.method with mode prune): it retires learned rules and deletes prompt text that tests show
230
+ * no longer change any result, and reports text that no test covers without deleting it. The change applies at once,
231
+ * with undo; the next method run or method wait shows the sweep's summary line.
232
+ */
233
+ export declare function pruneMethod(client: MethodClient, file: string, options?: {
234
+ run?: RunFile;
235
+ }): Promise<{
236
+ changed: boolean;
237
+ } | undefined>;
238
+ /**
239
+ * Fix the Method for one correction: run the fix, or, when a fix of the Method already runs on this computer, wait for
240
+ * the fix that takes this correction. Returns the stored outcome, or undefined when no fix here took it.
241
+ */
242
+ export declare function fixCorrection(client: MethodClient, file: string, judgmentId: string, options?: {
243
+ run?: RunFile;
244
+ pollMs?: number;
245
+ }): Promise<import("./fix-outcomes.js").StoredOutcome | undefined>;
246
+ /**
247
+ * `method __fix FILE --judgment ID... [--prune]`: the fix of method judge, method answer and method run, in its own
248
+ * process. Every correction it was started for ends with a stored outcome or error.
249
+ */
250
+ export declare function backgroundFix(client: MethodClient, file: string, ids: string[], options?: {
251
+ run?: RunFile;
252
+ prune?: boolean;
253
+ pollMs?: number;
254
+ }): Promise<void>;