outsrc 0.2.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 (49) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +188 -0
  3. package/config.example.toml +46 -0
  4. package/dist/adapters.d.ts +110 -0
  5. package/dist/adapters.js +246 -0
  6. package/dist/cli.d.ts +2 -0
  7. package/dist/cli.js +302 -0
  8. package/dist/config.d.ts +46 -0
  9. package/dist/config.js +87 -0
  10. package/dist/engines.d.ts +44 -0
  11. package/dist/engines.js +69 -0
  12. package/dist/fs-home.d.ts +7 -0
  13. package/dist/fs-home.js +28 -0
  14. package/dist/git.d.ts +9 -0
  15. package/dist/git.js +27 -0
  16. package/dist/init.d.ts +54 -0
  17. package/dist/init.js +322 -0
  18. package/dist/job.d.ts +19 -0
  19. package/dist/job.js +112 -0
  20. package/dist/limits.d.ts +13 -0
  21. package/dist/limits.js +10 -0
  22. package/dist/mailbox.d.ts +122 -0
  23. package/dist/mailbox.js +503 -0
  24. package/dist/migrate.d.ts +11 -0
  25. package/dist/migrate.js +122 -0
  26. package/dist/paths.d.ts +7 -0
  27. package/dist/paths.js +36 -0
  28. package/dist/permissions.d.ts +2 -0
  29. package/dist/permissions.js +29 -0
  30. package/dist/plugin-contract.d.ts +12 -0
  31. package/dist/plugin-contract.js +79 -0
  32. package/dist/plugins.d.ts +44 -0
  33. package/dist/plugins.js +215 -0
  34. package/dist/prompt.d.ts +2 -0
  35. package/dist/prompt.js +20 -0
  36. package/dist/server.d.ts +12 -0
  37. package/dist/server.js +127 -0
  38. package/dist/settings.d.ts +247 -0
  39. package/dist/settings.js +214 -0
  40. package/dist/state.d.ts +79 -0
  41. package/dist/state.js +61 -0
  42. package/dist/types.d.ts +181 -0
  43. package/dist/types.js +35 -0
  44. package/dist/workspace.d.ts +18 -0
  45. package/dist/workspace.js +66 -0
  46. package/dist/wrapper.d.ts +2 -0
  47. package/dist/wrapper.js +246 -0
  48. package/engines.lock.json +14 -0
  49. package/package.json +59 -0
@@ -0,0 +1,247 @@
1
+ export declare const SETTINGS_HELP: readonly [{
2
+ readonly key: "limits.max_jobs";
3
+ readonly values: 'positive whole number or "unlimited"';
4
+ readonly default: 4;
5
+ readonly meaning: "Most jobs working at once, across every caller.";
6
+ }, {
7
+ readonly key: "limits.max_run_minutes";
8
+ readonly values: 'positive whole number or "unlimited"';
9
+ readonly default: 120;
10
+ readonly meaning: "A run is stopped after this long.";
11
+ }, {
12
+ readonly key: "repos.<alias>";
13
+ readonly values: '{"path": "/absolute/path"}';
14
+ readonly default: null;
15
+ readonly meaning: "Add a repository agents may work in. unset removes it.";
16
+ }, {
17
+ readonly key: "repos.<alias>.path";
18
+ readonly values: "absolute path to a git checkout";
19
+ readonly default: null;
20
+ readonly meaning: "Where the repository lives.";
21
+ }, {
22
+ readonly key: "repos.<alias>.setup";
23
+ readonly values: 'JSON list of commands, e.g. [["npm","ci"]]';
24
+ readonly default: readonly [];
25
+ readonly meaning: "Runs once in a new task worktree before the first run. Never runs for reviews.";
26
+ }, {
27
+ readonly key: "repos.<alias>.auto_commit";
28
+ readonly values: "true or false";
29
+ readonly default: false;
30
+ readonly meaning: "Commit a successful task's changes on the agent branch.";
31
+ }, {
32
+ readonly key: "repos.<alias>.retention_days";
33
+ readonly values: "positive number";
34
+ readonly default: null;
35
+ readonly meaning: "outsrc prune removes finished worktrees older than this. Unset keeps them.";
36
+ }, {
37
+ readonly key: "targets.<name>";
38
+ readonly values: 'JSON object, e.g. {"adapter":"claude","command":"claude"}';
39
+ readonly default: null;
40
+ readonly meaning: "Add an agent outsrc can start. unset removes it.";
41
+ }, {
42
+ readonly key: "targets.<name>.permissions";
43
+ readonly values: '"auto" or "ask"';
44
+ readonly default: "auto";
45
+ readonly meaning: "auto lets tasks edit and run commands without approval. Reviews always run as ask.";
46
+ }, {
47
+ readonly key: "targets.<name>.effort.default";
48
+ readonly values: "one of effort.allowed";
49
+ readonly default: null;
50
+ readonly meaning: "Effort used when a request names none.";
51
+ }, {
52
+ readonly key: "targets.<name>.effort.allowed";
53
+ readonly values: "comma list or JSON list";
54
+ readonly default: null;
55
+ readonly meaning: "Efforts a request may choose.";
56
+ }, {
57
+ readonly key: "targets.<name>.models.default";
58
+ readonly values: "one of models.allowed";
59
+ readonly default: null;
60
+ readonly meaning: "Model used when a request names none. Without models, the CLI picks.";
61
+ }, {
62
+ readonly key: "targets.<name>.models.allowed";
63
+ readonly values: "comma list or JSON list";
64
+ readonly default: null;
65
+ readonly meaning: "Models a request may choose.";
66
+ }, {
67
+ readonly key: "targets.<name>.description";
68
+ readonly values: "text";
69
+ readonly default: "";
70
+ readonly meaning: "Shown to callers in list_targets.";
71
+ }, {
72
+ readonly key: "targets.<name>.cost_note";
73
+ readonly values: "text";
74
+ readonly default: "";
75
+ readonly meaning: "Shown to callers in list_targets.";
76
+ }, {
77
+ readonly key: "targets.<name>.adapter";
78
+ readonly values: "claude, codex, grok, codex-plugin, grok-plugin or custom";
79
+ readonly default: null;
80
+ readonly meaning: "How outsrc drives the agent.";
81
+ }, {
82
+ readonly key: "targets.<name>.command";
83
+ readonly values: "program path";
84
+ readonly default: null;
85
+ readonly meaning: "The program outsrc runs, as you.";
86
+ }, {
87
+ readonly key: "targets.<name>.args";
88
+ readonly values: "JSON list of strings";
89
+ readonly default: readonly [];
90
+ readonly meaning: "Extra arguments for the program.";
91
+ }];
92
+ export declare const RISKS: {
93
+ readonly "limits.max_jobs": "No cap on jobs working at once. A looping or confused bot can start many agents together: the computer slows down, usage and cost climb quickly, and providers may rate-limit the account.";
94
+ readonly "limits.max_run_minutes": "No time limit on a run. A stuck agent (waiting for input it cannot get, a test runner in watch mode, a loop) keeps running and spending until someone stops it.";
95
+ readonly "permissions.auto": "Tasks on this target edit files and run commands without asking. That is how unattended work gets done, and it is also why only the owner should choose the repositories.";
96
+ readonly command: "outsrc will run this program as you, with your files and accounts.";
97
+ };
98
+ type Key = {
99
+ kind: "limit";
100
+ name: "max_jobs" | "max_run_minutes";
101
+ } | {
102
+ kind: "repo";
103
+ alias: string;
104
+ field: string | null;
105
+ } | {
106
+ kind: "target";
107
+ name: string;
108
+ field: string[] | null;
109
+ };
110
+ export declare function parseKey(key: string): Key;
111
+ export type SettingChange = {
112
+ ok: true;
113
+ key: string;
114
+ value: unknown;
115
+ warning: string | null;
116
+ } | {
117
+ ok: false;
118
+ error: string;
119
+ };
120
+ /** Set (or with value undefined, remove) one setting. The file is only written if the whole config stays valid. */
121
+ export declare function changeSetting(configPath: string, key: string, raw: string | undefined): SettingChange;
122
+ /** Every setting with its effective value, plus the keys an owner can change. */
123
+ export declare function listSettings(configPath: string): {
124
+ config_path: string;
125
+ limits: {
126
+ max_jobs: string | number;
127
+ max_run_minutes: string | number;
128
+ };
129
+ repos: {
130
+ alias: string;
131
+ path: string;
132
+ setup: string[][];
133
+ auto_commit: boolean;
134
+ retention_days: number | null;
135
+ }[];
136
+ targets: {
137
+ [k: string]: {
138
+ adapter: string;
139
+ command: string;
140
+ args: string[];
141
+ permissions: import("./types.js").PermissionMode;
142
+ models: {
143
+ default: string;
144
+ allowed: string[];
145
+ } | null;
146
+ effort: {
147
+ default: string;
148
+ allowed: string[];
149
+ } | null;
150
+ description: string;
151
+ cost_note: string;
152
+ };
153
+ };
154
+ change_with: string;
155
+ settings: readonly [{
156
+ readonly key: "limits.max_jobs";
157
+ readonly values: 'positive whole number or "unlimited"';
158
+ readonly default: 4;
159
+ readonly meaning: "Most jobs working at once, across every caller.";
160
+ }, {
161
+ readonly key: "limits.max_run_minutes";
162
+ readonly values: 'positive whole number or "unlimited"';
163
+ readonly default: 120;
164
+ readonly meaning: "A run is stopped after this long.";
165
+ }, {
166
+ readonly key: "repos.<alias>";
167
+ readonly values: '{"path": "/absolute/path"}';
168
+ readonly default: null;
169
+ readonly meaning: "Add a repository agents may work in. unset removes it.";
170
+ }, {
171
+ readonly key: "repos.<alias>.path";
172
+ readonly values: "absolute path to a git checkout";
173
+ readonly default: null;
174
+ readonly meaning: "Where the repository lives.";
175
+ }, {
176
+ readonly key: "repos.<alias>.setup";
177
+ readonly values: 'JSON list of commands, e.g. [["npm","ci"]]';
178
+ readonly default: readonly [];
179
+ readonly meaning: "Runs once in a new task worktree before the first run. Never runs for reviews.";
180
+ }, {
181
+ readonly key: "repos.<alias>.auto_commit";
182
+ readonly values: "true or false";
183
+ readonly default: false;
184
+ readonly meaning: "Commit a successful task's changes on the agent branch.";
185
+ }, {
186
+ readonly key: "repos.<alias>.retention_days";
187
+ readonly values: "positive number";
188
+ readonly default: null;
189
+ readonly meaning: "outsrc prune removes finished worktrees older than this. Unset keeps them.";
190
+ }, {
191
+ readonly key: "targets.<name>";
192
+ readonly values: 'JSON object, e.g. {"adapter":"claude","command":"claude"}';
193
+ readonly default: null;
194
+ readonly meaning: "Add an agent outsrc can start. unset removes it.";
195
+ }, {
196
+ readonly key: "targets.<name>.permissions";
197
+ readonly values: '"auto" or "ask"';
198
+ readonly default: "auto";
199
+ readonly meaning: "auto lets tasks edit and run commands without approval. Reviews always run as ask.";
200
+ }, {
201
+ readonly key: "targets.<name>.effort.default";
202
+ readonly values: "one of effort.allowed";
203
+ readonly default: null;
204
+ readonly meaning: "Effort used when a request names none.";
205
+ }, {
206
+ readonly key: "targets.<name>.effort.allowed";
207
+ readonly values: "comma list or JSON list";
208
+ readonly default: null;
209
+ readonly meaning: "Efforts a request may choose.";
210
+ }, {
211
+ readonly key: "targets.<name>.models.default";
212
+ readonly values: "one of models.allowed";
213
+ readonly default: null;
214
+ readonly meaning: "Model used when a request names none. Without models, the CLI picks.";
215
+ }, {
216
+ readonly key: "targets.<name>.models.allowed";
217
+ readonly values: "comma list or JSON list";
218
+ readonly default: null;
219
+ readonly meaning: "Models a request may choose.";
220
+ }, {
221
+ readonly key: "targets.<name>.description";
222
+ readonly values: "text";
223
+ readonly default: "";
224
+ readonly meaning: "Shown to callers in list_targets.";
225
+ }, {
226
+ readonly key: "targets.<name>.cost_note";
227
+ readonly values: "text";
228
+ readonly default: "";
229
+ readonly meaning: "Shown to callers in list_targets.";
230
+ }, {
231
+ readonly key: "targets.<name>.adapter";
232
+ readonly values: "claude, codex, grok, codex-plugin, grok-plugin or custom";
233
+ readonly default: null;
234
+ readonly meaning: "How outsrc drives the agent.";
235
+ }, {
236
+ readonly key: "targets.<name>.command";
237
+ readonly values: "program path";
238
+ readonly default: null;
239
+ readonly meaning: "The program outsrc runs, as you.";
240
+ }, {
241
+ readonly key: "targets.<name>.args";
242
+ readonly values: "JSON list of strings";
243
+ readonly default: readonly [];
244
+ readonly meaning: "Extra arguments for the program.";
245
+ }];
246
+ };
247
+ export {};
@@ -0,0 +1,214 @@
1
+ import { execFileSync } from "node:child_process";
2
+ import { copyFileSync, existsSync, readFileSync } from "node:fs";
3
+ import { dirname, isAbsolute } from "node:path";
4
+ import { parse, stringify } from "smol-toml";
5
+ import { configFromDocument } from "./config.js";
6
+ import { ensureOutsrcHome, writeHomeFile } from "./fs-home.js";
7
+ import { DEFAULT_LIMITS, resolveLimits } from "./limits.js";
8
+ // Every setting an owner can change, with its default and what it means. Keys use the config.toml names.
9
+ export const SETTINGS_HELP = [
10
+ { key: "limits.max_jobs", values: 'positive whole number or "unlimited"', default: DEFAULT_LIMITS.max_jobs, meaning: "Most jobs working at once, across every caller." },
11
+ { key: "limits.max_run_minutes", values: 'positive whole number or "unlimited"', default: DEFAULT_LIMITS.max_run_minutes, meaning: "A run is stopped after this long." },
12
+ { key: "repos.<alias>", values: '{"path": "/absolute/path"}', default: null, meaning: "Add a repository agents may work in. unset removes it." },
13
+ { key: "repos.<alias>.path", values: "absolute path to a git checkout", default: null, meaning: "Where the repository lives." },
14
+ { key: "repos.<alias>.setup", values: 'JSON list of commands, e.g. [["npm","ci"]]', default: [], meaning: "Runs once in a new task worktree before the first run. Never runs for reviews." },
15
+ { key: "repos.<alias>.auto_commit", values: "true or false", default: false, meaning: "Commit a successful task's changes on the agent branch." },
16
+ { key: "repos.<alias>.retention_days", values: "positive number", default: null, meaning: "outsrc prune removes finished worktrees older than this. Unset keeps them." },
17
+ { key: "targets.<name>", values: 'JSON object, e.g. {"adapter":"claude","command":"claude"}', default: null, meaning: "Add an agent outsrc can start. unset removes it." },
18
+ { key: "targets.<name>.permissions", values: '"auto" or "ask"', default: "auto", meaning: "auto lets tasks edit and run commands without approval. Reviews always run as ask." },
19
+ { key: "targets.<name>.effort.default", values: "one of effort.allowed", default: null, meaning: "Effort used when a request names none." },
20
+ { key: "targets.<name>.effort.allowed", values: "comma list or JSON list", default: null, meaning: "Efforts a request may choose." },
21
+ { key: "targets.<name>.models.default", values: "one of models.allowed", default: null, meaning: "Model used when a request names none. Without models, the CLI picks." },
22
+ { key: "targets.<name>.models.allowed", values: "comma list or JSON list", default: null, meaning: "Models a request may choose." },
23
+ { key: "targets.<name>.description", values: "text", default: "", meaning: "Shown to callers in list_targets." },
24
+ { key: "targets.<name>.cost_note", values: "text", default: "", meaning: "Shown to callers in list_targets." },
25
+ { key: "targets.<name>.adapter", values: "claude, codex, grok, codex-plugin, grok-plugin or custom", default: null, meaning: "How outsrc drives the agent." },
26
+ { key: "targets.<name>.command", values: "program path", default: null, meaning: "The program outsrc runs, as you." },
27
+ { key: "targets.<name>.args", values: "JSON list of strings", default: [], meaning: "Extra arguments for the program." },
28
+ ];
29
+ export const RISKS = {
30
+ "limits.max_jobs": "No cap on jobs working at once. A looping or confused bot can start many agents together: the computer slows down, usage and cost climb quickly, and providers may rate-limit the account.",
31
+ "limits.max_run_minutes": "No time limit on a run. A stuck agent (waiting for input it cannot get, a test runner in watch mode, a loop) keeps running and spending until someone stops it.",
32
+ "permissions.auto": "Tasks on this target edit files and run commands without asking. That is how unattended work gets done, and it is also why only the owner should choose the repositories.",
33
+ command: "outsrc will run this program as you, with your files and accounts.",
34
+ };
35
+ const REPO_FIELDS = ["path", "setup", "auto_commit", "retention_days"];
36
+ const TARGET_FIELDS = ["models.default", "models.allowed", "effort.default", "effort.allowed", "models", "effort",
37
+ "adapter", "command", "args", "permissions", "description", "cost_note"];
38
+ // Aliases may contain dots (a repository named outsrc.ing), so the field is matched from the end.
39
+ export function parseKey(key) {
40
+ if (key === "limits.max_jobs" || key === "limits.max_run_minutes")
41
+ return { kind: "limit", name: key.slice("limits.".length) };
42
+ for (const [prefix, fields, kind] of [["repos.", REPO_FIELDS, "repo"], ["targets.", TARGET_FIELDS, "target"]]) {
43
+ if (!key.startsWith(prefix) || key.length === prefix.length)
44
+ continue;
45
+ const rest = key.slice(prefix.length);
46
+ const field = fields.find((candidate) => rest.endsWith(`.${candidate}`) && rest.length > candidate.length + 1);
47
+ const name = field ? rest.slice(0, -(field.length + 1)) : rest;
48
+ if (kind === "repo")
49
+ return { kind, alias: name, field: field ?? null };
50
+ return { kind, name, field: field ? field.split(".") : null };
51
+ }
52
+ throw new Error(`unknown setting: ${key}. Run outsrc config to see every setting`);
53
+ }
54
+ function parseValue(key, raw) {
55
+ let value;
56
+ try {
57
+ value = JSON.parse(raw);
58
+ }
59
+ catch {
60
+ value = raw;
61
+ }
62
+ if (key.kind === "target" && key.field?.at(-1) === "allowed" && typeof value === "string")
63
+ return value.split(",").map((item) => item.trim()).filter(Boolean);
64
+ return value;
65
+ }
66
+ function readDocument(path) {
67
+ return existsSync(path) ? parse(readFileSync(path, "utf8")) : {};
68
+ }
69
+ function gitTop(path) {
70
+ if (!isAbsolute(path))
71
+ throw new Error(`repository path must be absolute: ${path}`);
72
+ try {
73
+ return execFileSync("git", ["-C", path, "rev-parse", "--show-toplevel"], { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim();
74
+ }
75
+ catch {
76
+ throw new Error(`not a git repository: ${path}`);
77
+ }
78
+ }
79
+ function apply(document, key, value) {
80
+ if (key.kind === "limit") {
81
+ const limits = { ...(document.limits ?? {}) };
82
+ if (value === undefined)
83
+ delete limits[key.name];
84
+ else
85
+ limits[key.name] = value;
86
+ document.limits = limits;
87
+ return;
88
+ }
89
+ if (key.kind === "repo") {
90
+ const repos = [...(document.repos ?? [])];
91
+ const index = repos.findIndex((repo) => repo.alias === key.alias);
92
+ if (key.field === null) {
93
+ if (value === undefined) {
94
+ if (index < 0)
95
+ throw new Error(`unknown repository: ${key.alias}`);
96
+ repos.splice(index, 1);
97
+ }
98
+ else {
99
+ if (typeof value !== "object" || value === null || Array.isArray(value))
100
+ throw new Error(`repos.${key.alias} takes a JSON object such as {"path": "/absolute/path"}`);
101
+ const entry = { ...(index >= 0 ? repos[index] : {}), ...value, alias: key.alias };
102
+ if (typeof entry.path === "string")
103
+ entry.path = gitTop(entry.path);
104
+ if (index >= 0)
105
+ repos[index] = entry;
106
+ else
107
+ repos.push(entry);
108
+ }
109
+ }
110
+ else {
111
+ if (index < 0 && !(key.field === "path" && value !== undefined))
112
+ throw new Error(`unknown repository: ${key.alias}`);
113
+ const entry = { ...(index >= 0 ? repos[index] : { alias: key.alias }) };
114
+ if (value === undefined)
115
+ delete entry[key.field];
116
+ else
117
+ entry[key.field] = value;
118
+ if (key.field === "path" && typeof entry.path === "string")
119
+ entry.path = gitTop(entry.path);
120
+ if (index >= 0)
121
+ repos[index] = entry;
122
+ else
123
+ repos.push(entry);
124
+ }
125
+ document.repos = repos;
126
+ return;
127
+ }
128
+ const targets = { ...(document.targets ?? {}) };
129
+ if (key.field === null) {
130
+ if (value === undefined) {
131
+ if (!(key.name in targets))
132
+ throw new Error(`unknown target: ${key.name}`);
133
+ delete targets[key.name];
134
+ }
135
+ else {
136
+ if (typeof value !== "object" || value === null || Array.isArray(value))
137
+ throw new Error(`targets.${key.name} takes a JSON object`);
138
+ targets[key.name] = { ...(targets[key.name] ?? {}), ...value };
139
+ }
140
+ }
141
+ else {
142
+ if (!(key.name in targets))
143
+ throw new Error(`unknown target: ${key.name}. Add it first with outsrc config set targets.${key.name} '{"adapter": ..., "command": ...}'`);
144
+ const target = structuredClone(targets[key.name] ?? {});
145
+ const [head = "", sub] = key.field;
146
+ if (sub === undefined) {
147
+ if (value === undefined)
148
+ delete target[head];
149
+ else
150
+ target[head] = value;
151
+ }
152
+ else {
153
+ const nested = { ...(target[head] ?? {}) };
154
+ if (value === undefined)
155
+ delete nested[sub];
156
+ else
157
+ nested[sub] = value;
158
+ if (Object.keys(nested).length)
159
+ target[head] = nested;
160
+ else
161
+ delete target[head];
162
+ }
163
+ targets[key.name] = target;
164
+ }
165
+ document.targets = targets;
166
+ }
167
+ function risk(key, value) {
168
+ if ((key === "limits.max_jobs" || key === "limits.max_run_minutes") && value === "unlimited")
169
+ return RISKS[key];
170
+ if (key.endsWith(".permissions") && value === "auto")
171
+ return RISKS["permissions.auto"];
172
+ if (key.endsWith(".command"))
173
+ return RISKS.command;
174
+ return null;
175
+ }
176
+ /** Set (or with value undefined, remove) one setting. The file is only written if the whole config stays valid. */
177
+ export function changeSetting(configPath, key, raw) {
178
+ try {
179
+ const parsed = parseKey(key);
180
+ const value = raw === undefined ? undefined : parseValue(parsed, raw);
181
+ if (value === "off" && parsed.kind === "repo" && parsed.field === "retention_days")
182
+ return changeSetting(configPath, key, undefined);
183
+ const document = readDocument(configPath);
184
+ apply(document, parsed, value);
185
+ configFromDocument(document, configPath);
186
+ ensureOutsrcHome(dirname(configPath));
187
+ if (existsSync(configPath))
188
+ copyFileSync(configPath, `${configPath}.bak`);
189
+ writeHomeFile(configPath, stringify(document));
190
+ return { ok: true, key, value: value ?? null, warning: risk(key, value) };
191
+ }
192
+ catch (error) {
193
+ const message = error instanceof Error ? error.message : String(error);
194
+ return { ok: false, error: message.startsWith("[") ? `invalid value for ${key}: ${message}` : message };
195
+ }
196
+ }
197
+ /** Every setting with its effective value, plus the keys an owner can change. */
198
+ export function listSettings(configPath) {
199
+ const config = configFromDocument(readDocument(configPath), configPath);
200
+ const limits = resolveLimits(config.limits);
201
+ return {
202
+ config_path: configPath,
203
+ limits: { max_jobs: limits.maxJobs ?? "unlimited", max_run_minutes: limits.maxRunMinutes ?? "unlimited" },
204
+ repos: config.repos.map((repo) => ({
205
+ alias: repo.alias, path: repo.path, setup: repo.setup ?? [], auto_commit: repo.autoCommit ?? false, retention_days: repo.retentionDays ?? null,
206
+ })),
207
+ targets: Object.fromEntries(Object.entries(config.targets).map(([name, target]) => [name, {
208
+ adapter: target.adapter ?? name, command: target.command, args: target.args, permissions: target.permissions ?? "auto",
209
+ models: target.models ?? null, effort: target.effort ?? null, description: target.description ?? "", cost_note: target.costNote ?? "",
210
+ }])),
211
+ change_with: "outsrc config set <key> <value>; outsrc config unset <key> restores the default",
212
+ settings: SETTINGS_HELP,
213
+ };
214
+ }
@@ -0,0 +1,79 @@
1
+ import { z } from "zod";
2
+ export declare const ThreadSchema: z.ZodObject<{
3
+ version: z.ZodLiteral<2>;
4
+ id: z.ZodPipe<z.ZodString, z.ZodTransform<import("./types.js").ThreadId, string>>;
5
+ caller: z.ZodDefault<z.ZodString>;
6
+ repo: z.ZodPipe<z.ZodString, z.ZodTransform<import("./types.js").Alias, string>>;
7
+ target: z.ZodPipe<z.ZodString, z.ZodTransform<import("./types.js").TargetName, string>>;
8
+ model: z.ZodNullable<z.ZodString>;
9
+ effort: z.ZodNullable<z.ZodString>;
10
+ taskKind: z.ZodEnum<{
11
+ adversarial_review: "adversarial_review";
12
+ review: "review";
13
+ task: "task";
14
+ }>;
15
+ base: z.ZodDefault<z.ZodNullable<z.ZodString>>;
16
+ worktree: z.ZodString;
17
+ branch: z.ZodString;
18
+ baseCommit: z.ZodString;
19
+ createdAt: z.ZodString;
20
+ latestRunId: z.ZodString;
21
+ }, z.core.$strip>;
22
+ export declare const RunSchema: z.ZodObject<{
23
+ id: z.ZodPipe<z.ZodString, z.ZodTransform<import("./types.js").RunId, string>>;
24
+ threadId: z.ZodPipe<z.ZodString, z.ZodTransform<import("./types.js").ThreadId, string>>;
25
+ requestId: z.ZodNullable<z.ZodString>;
26
+ message: z.ZodString;
27
+ createdAt: z.ZodString;
28
+ sessionId: z.ZodNullable<z.ZodString>;
29
+ }, z.core.$strip>;
30
+ export declare const ResultSchema: z.ZodObject<{
31
+ kind: z.ZodEnum<{
32
+ cancelled: "cancelled";
33
+ completed: "completed";
34
+ failed: "failed";
35
+ needs_input: "needs_input";
36
+ }>;
37
+ message: z.ZodString;
38
+ sessionId: z.ZodNullable<z.ZodString>;
39
+ findings: z.ZodArray<z.ZodObject<{
40
+ priority: z.ZodEnum<{
41
+ P0: "P0";
42
+ P1: "P1";
43
+ P2: "P2";
44
+ P3: "P3";
45
+ }>;
46
+ title: z.ZodString;
47
+ body: z.ZodString;
48
+ path: z.ZodNullable<z.ZodString>;
49
+ line: z.ZodNullable<z.ZodNumber>;
50
+ }, z.core.$strip>>;
51
+ exitCode: z.ZodNullable<z.ZodNumber>;
52
+ finishedAt: z.ZodString;
53
+ diffstat: z.ZodString;
54
+ commit: z.ZodNullable<z.ZodString>;
55
+ }, z.core.$strip>;
56
+ export declare const ProcessSchema: z.ZodObject<{
57
+ pid: z.ZodNumber;
58
+ pgid: z.ZodNumber;
59
+ startedAt: z.ZodString;
60
+ }, z.core.$strip>;
61
+ export declare function writeJson(file: string, value: unknown): void;
62
+ export declare function readJson(file: string): unknown;
63
+ export declare function runDirectory(threadDirectory: string, runId: string): string;
64
+ export declare function readResult(directory: string): {
65
+ kind: "cancelled" | "completed" | "failed" | "needs_input";
66
+ message: string;
67
+ sessionId: string | null;
68
+ findings: {
69
+ priority: "P0" | "P1" | "P2" | "P3";
70
+ title: string;
71
+ body: string;
72
+ path: string | null;
73
+ line: number | null;
74
+ }[];
75
+ exitCode: number | null;
76
+ finishedAt: string;
77
+ diffstat: string;
78
+ commit: string | null;
79
+ } | null;
package/dist/state.js ADDED
@@ -0,0 +1,61 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import { existsSync, readFileSync, renameSync } from "node:fs";
3
+ import { writeHomeFile } from "./fs-home.js";
4
+ import { join } from "node:path";
5
+ import { z } from "zod";
6
+ import { FindingSchema } from "./adapters.js";
7
+ import { parseAlias, parseRunId, parseTargetName, parseThreadId, STORAGE_ID } from "./types.js";
8
+ export const ThreadSchema = z.object({
9
+ version: z.literal(2),
10
+ id: z.string().transform(parseThreadId),
11
+ caller: z.string().default("local"),
12
+ repo: z.string().transform(parseAlias),
13
+ target: z.string().transform(parseTargetName),
14
+ model: z.string().nullable(),
15
+ effort: z.string().nullable(),
16
+ taskKind: z.enum(["task", "review", "adversarial_review"]),
17
+ base: z.string().nullable().default(null),
18
+ worktree: z.string(),
19
+ branch: z.string(),
20
+ baseCommit: z.string(),
21
+ createdAt: z.string(),
22
+ latestRunId: z.string().refine((value) => value === "" || STORAGE_ID.test(value), "latest run id is not a storage id"),
23
+ });
24
+ export const RunSchema = z.object({
25
+ id: z.string().transform(parseRunId),
26
+ threadId: z.string().transform(parseThreadId),
27
+ requestId: z.string().nullable(),
28
+ message: z.string(),
29
+ createdAt: z.string(),
30
+ sessionId: z.string().nullable(),
31
+ });
32
+ export const ResultSchema = z.object({
33
+ kind: z.enum(["completed", "needs_input", "failed", "cancelled"]),
34
+ message: z.string(),
35
+ sessionId: z.string().nullable(),
36
+ findings: z.array(FindingSchema),
37
+ exitCode: z.number().nullable(),
38
+ finishedAt: z.string(),
39
+ diffstat: z.string(),
40
+ commit: z.string().nullable(),
41
+ });
42
+ export const ProcessSchema = z.object({
43
+ pid: z.number().int().gte(2),
44
+ pgid: z.number().int().gte(2),
45
+ startedAt: z.string().min(1),
46
+ }).refine((value) => value.pid === value.pgid, "process group must be the wrapper process");
47
+ export function writeJson(file, value) {
48
+ const temporary = `${file}.${randomBytes(6).toString("hex")}.tmp`;
49
+ writeHomeFile(temporary, JSON.stringify(value, null, 2));
50
+ renameSync(temporary, file);
51
+ }
52
+ export function readJson(file) {
53
+ return JSON.parse(readFileSync(file, "utf8"));
54
+ }
55
+ export function runDirectory(threadDirectory, runId) {
56
+ return join(threadDirectory, "runs", runId);
57
+ }
58
+ export function readResult(directory) {
59
+ const file = join(directory, "result.json");
60
+ return existsSync(file) ? ResultSchema.parse(readJson(file)) : null;
61
+ }