@developerz.ai/aitm 0.0.3 → 0.0.5

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.
@@ -0,0 +1,224 @@
1
+ import { readFile } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ import { ZodError } from 'zod';
4
+ import { atomicWrite } from "../fs/atomic-write.js";
5
+ import { PROVIDER_PRESETS } from "./provider-presets.js";
6
+ import { ConfigFileSchema } from "./schema.js";
7
+ const GLOBAL_FILE = '.aitm.json';
8
+ export class ProfileManager {
9
+ homeDir;
10
+ constructor(homeDir) {
11
+ this.homeDir = homeDir;
12
+ }
13
+ async list() {
14
+ const validated = await this.readValidated();
15
+ return {
16
+ activeProfile: validated.activeProfile,
17
+ profiles: validated.profiles ?? {},
18
+ };
19
+ }
20
+ async use(name) {
21
+ const file = await this.readRaw();
22
+ const profiles = asObject(file.profiles);
23
+ if (!(name in profiles)) {
24
+ throw new Error(unknownProfileMessage(name, Object.keys(profiles)));
25
+ }
26
+ file.activeProfile = name;
27
+ await this.persist(file);
28
+ }
29
+ async add(name, input = {}) {
30
+ if (name.trim() === '')
31
+ throw new Error('Profile name must be non-empty.');
32
+ const file = await this.readRaw();
33
+ const profiles = ensureObject(file, 'profiles');
34
+ if (name in profiles) {
35
+ throw new Error(`Profile "${name}" already exists. Use \`aitm profile set ${name} <key> <value>\` to modify it.`);
36
+ }
37
+ const profile = input.preset
38
+ ? jsonClone(PROVIDER_PRESETS[input.preset])
39
+ : {};
40
+ if (input.baseURL !== undefined)
41
+ profile.baseURL = input.baseURL;
42
+ if (input.apiKey !== undefined)
43
+ profile.openrouterApiKey = input.apiKey;
44
+ profiles[name] = profile;
45
+ if (file.activeProfile === undefined)
46
+ file.activeProfile = name;
47
+ const validated = await this.persist(file);
48
+ return validated.profiles?.[name] ?? {};
49
+ }
50
+ async set(name, key, value) {
51
+ const file = await this.readRaw();
52
+ const profiles = asObject(file.profiles);
53
+ if (!(name in profiles)) {
54
+ throw new Error(unknownProfileMessage(name, Object.keys(profiles)));
55
+ }
56
+ const profile = asObject(profiles[name]);
57
+ setDotted(profile, splitKey(key), parseValue(value));
58
+ profiles[name] = profile;
59
+ file.profiles = profiles;
60
+ const validated = await this.persist(file);
61
+ return validated.profiles?.[name] ?? {};
62
+ }
63
+ async get(name, key) {
64
+ const { profiles } = await this.list();
65
+ const profile = profiles[name];
66
+ if (profile === undefined) {
67
+ throw new Error(unknownProfileMessage(name, Object.keys(profiles)));
68
+ }
69
+ return getDotted(profile, splitKey(key));
70
+ }
71
+ async remove(name) {
72
+ const file = await this.readRaw();
73
+ const profiles = asObject(file.profiles);
74
+ if (!(name in profiles)) {
75
+ throw new Error(unknownProfileMessage(name, Object.keys(profiles)));
76
+ }
77
+ delete profiles[name];
78
+ if (file.activeProfile === name)
79
+ delete file.activeProfile;
80
+ await this.persist(file);
81
+ }
82
+ async show(name) {
83
+ const { activeProfile, profiles } = await this.list();
84
+ const target = name ?? activeProfile;
85
+ if (target === undefined) {
86
+ throw new Error('No profile specified and no active profile set. Pass a name or run `aitm profile use <name>`.');
87
+ }
88
+ const profile = profiles[target];
89
+ if (profile === undefined) {
90
+ throw new Error(unknownProfileMessage(target, Object.keys(profiles)));
91
+ }
92
+ return { name: target, profile };
93
+ }
94
+ filePath() {
95
+ return join(this.homeDir, GLOBAL_FILE);
96
+ }
97
+ async readRaw() {
98
+ const path = this.filePath();
99
+ let raw;
100
+ try {
101
+ raw = await readFile(path, 'utf8');
102
+ }
103
+ catch (err) {
104
+ if (isNotFound(err))
105
+ return {};
106
+ throw err;
107
+ }
108
+ let parsed;
109
+ try {
110
+ parsed = JSON.parse(raw);
111
+ }
112
+ catch (err) {
113
+ const msg = err instanceof Error ? err.message : String(err);
114
+ throw new Error(`${path}: invalid JSON — ${msg}`);
115
+ }
116
+ if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) {
117
+ throw new Error(`${path}: expected a JSON object at the top level`);
118
+ }
119
+ return parsed;
120
+ }
121
+ async readValidated() {
122
+ return this.validate(await this.readRaw());
123
+ }
124
+ validate(file) {
125
+ try {
126
+ return ConfigFileSchema.parse(file);
127
+ }
128
+ catch (err) {
129
+ if (err instanceof ZodError)
130
+ throw new Error(`${this.filePath()}: ${formatZodError(err)}`);
131
+ throw err;
132
+ }
133
+ }
134
+ async persist(file) {
135
+ const validated = this.validate(file);
136
+ await atomicWrite(this.filePath(), `${JSON.stringify(validated, null, 2)}\n`);
137
+ return validated;
138
+ }
139
+ }
140
+ function asObject(value) {
141
+ return value !== null && typeof value === 'object' && !Array.isArray(value)
142
+ ? value
143
+ : {};
144
+ }
145
+ function ensureObject(parent, key) {
146
+ const next = asObject(parent[key]);
147
+ parent[key] = next;
148
+ return next;
149
+ }
150
+ function jsonClone(value) {
151
+ return JSON.parse(JSON.stringify(value));
152
+ }
153
+ const ALLOWED_PROFILE_ROOT_KEYS = new Set([
154
+ 'openrouterApiKey',
155
+ 'baseURL',
156
+ 'models',
157
+ ]);
158
+ const FORBIDDEN_KEY_SEGMENTS = new Set([
159
+ '__proto__',
160
+ 'prototype',
161
+ 'constructor',
162
+ ]);
163
+ const KEY_SURFACE_HINT = 'Allowed keys: openrouterApiKey, baseURL, models.<tier>.';
164
+ function splitKey(key) {
165
+ const parts = key.split('.');
166
+ if (parts.length === 0 || parts.some((p) => p === '')) {
167
+ throw new Error(`Invalid profile key: "${key}". ${KEY_SURFACE_HINT}`);
168
+ }
169
+ if (parts.some((p) => FORBIDDEN_KEY_SEGMENTS.has(p))) {
170
+ throw new Error(`Invalid profile key: "${key}" — reserved segment. ${KEY_SURFACE_HINT}`);
171
+ }
172
+ const [first, ...rest] = parts;
173
+ if (first === undefined || !ALLOWED_PROFILE_ROOT_KEYS.has(first)) {
174
+ throw new Error(`Invalid profile key: "${key}". ${KEY_SURFACE_HINT}`);
175
+ }
176
+ if (first === 'models' ? rest.length !== 1 : rest.length !== 0) {
177
+ throw new Error(`Invalid profile key: "${key}". ${KEY_SURFACE_HINT}`);
178
+ }
179
+ return [first, ...rest];
180
+ }
181
+ function setDotted(obj, parts, value) {
182
+ const [first, ...rest] = parts;
183
+ if (first === undefined)
184
+ return;
185
+ if (rest.length === 0) {
186
+ obj[first] = value;
187
+ return;
188
+ }
189
+ const sub = asObject(obj[first]);
190
+ obj[first] = sub;
191
+ setDotted(sub, rest, value);
192
+ }
193
+ function getDotted(obj, parts) {
194
+ let cur = obj;
195
+ for (const p of parts) {
196
+ if (cur === null || typeof cur !== 'object' || Array.isArray(cur))
197
+ return undefined;
198
+ cur = cur[p];
199
+ }
200
+ return cur;
201
+ }
202
+ function parseValue(v) {
203
+ if (typeof v !== 'string')
204
+ return v;
205
+ try {
206
+ return JSON.parse(v);
207
+ }
208
+ catch {
209
+ return v;
210
+ }
211
+ }
212
+ function unknownProfileMessage(name, available) {
213
+ const list = available.length > 0 ? available.slice().sort().join(', ') : '(none)';
214
+ return `Unknown profile "${name}". Available: ${list}. Create it with \`aitm profile add ${name}\`.`;
215
+ }
216
+ function isNotFound(err) {
217
+ return (typeof err === 'object' &&
218
+ err !== null &&
219
+ 'code' in err &&
220
+ err.code === 'ENOENT');
221
+ }
222
+ function formatZodError(err) {
223
+ return err.issues.map((i) => `${i.path.join('.') || '<root>'}: ${i.message}`).join('; ');
224
+ }
@@ -0,0 +1,5 @@
1
+ import type { Profile } from './schema.ts';
2
+ export type PresetName = 'openrouter' | 'zai';
3
+ export declare const PROVIDER_PRESETS: Readonly<Record<PresetName, Profile>>;
4
+ export declare const PRESET_NAMES: readonly PresetName[];
5
+ export declare function isPresetName(s: string): s is PresetName;
@@ -0,0 +1,18 @@
1
+ export const PROVIDER_PRESETS = {
2
+ openrouter: {
3
+ baseURL: 'https://openrouter.ai/api/v1',
4
+ },
5
+ zai: {
6
+ baseURL: 'https://api.z.ai/api/coding/paas/v4',
7
+ models: {
8
+ generic: 'glm-5.2',
9
+ smart: 'glm-5.2',
10
+ coding: 'glm-5.2',
11
+ fast: 'glm-5-turbo',
12
+ },
13
+ },
14
+ };
15
+ export const PRESET_NAMES = Object.keys(PROVIDER_PRESETS);
16
+ export function isPresetName(s) {
17
+ return Object.hasOwn(PROVIDER_PRESETS, s);
18
+ }
@@ -18,8 +18,31 @@ export declare const MergeMethodSchema: z.ZodEnum<{
18
18
  merge: "merge";
19
19
  rebase: "rebase";
20
20
  }>;
21
+ export declare const ProfileSchema: z.ZodObject<{
22
+ openrouterApiKey: z.ZodOptional<z.ZodString>;
23
+ baseURL: z.ZodOptional<z.ZodURL>;
24
+ models: z.ZodOptional<z.ZodObject<{
25
+ generic: z.ZodOptional<z.ZodString>;
26
+ smart: z.ZodOptional<z.ZodString>;
27
+ coding: z.ZodOptional<z.ZodString>;
28
+ fast: z.ZodOptional<z.ZodString>;
29
+ }, z.core.$loose>>;
30
+ }, z.core.$loose>;
31
+ export type Profile = z.infer<typeof ProfileSchema>;
21
32
  export declare const ConfigFileSchema: z.ZodObject<{
22
33
  openrouterApiKey: z.ZodOptional<z.ZodString>;
34
+ activeProfile: z.ZodOptional<z.ZodString>;
35
+ profiles: z.ZodOptional<z.ZodRecord<z.ZodString, z.ZodObject<{
36
+ openrouterApiKey: z.ZodOptional<z.ZodString>;
37
+ baseURL: z.ZodOptional<z.ZodURL>;
38
+ models: z.ZodOptional<z.ZodObject<{
39
+ generic: z.ZodOptional<z.ZodString>;
40
+ smart: z.ZodOptional<z.ZodString>;
41
+ coding: z.ZodOptional<z.ZodString>;
42
+ fast: z.ZodOptional<z.ZodString>;
43
+ }, z.core.$loose>>;
44
+ }, z.core.$loose>>>;
45
+ baseURL: z.ZodOptional<z.ZodURL>;
23
46
  models: z.ZodOptional<z.ZodObject<{
24
47
  generic: z.ZodOptional<z.ZodString>;
25
48
  smart: z.ZodOptional<z.ZodString>;
@@ -35,6 +58,7 @@ export declare const ConfigFileSchema: z.ZodObject<{
35
58
  rebase: "rebase";
36
59
  }>>;
37
60
  stylePath: z.ZodOptional<z.ZodNullable<z.ZodString>>;
61
+ formatCommand: z.ZodOptional<z.ZodString>;
38
62
  logLevel: z.ZodOptional<z.ZodEnum<{
39
63
  debug: "debug";
40
64
  info: "info";
@@ -70,13 +94,16 @@ export type CliOverrides = {
70
94
  };
71
95
  export type ResolvedConfig = {
72
96
  openrouterApiKey: string;
73
- apiKeySource: 'project' | 'global' | 'env';
97
+ apiKeySource: 'project' | 'global' | 'env' | 'profile';
98
+ activeProfile?: string | undefined;
99
+ baseURL?: string | undefined;
74
100
  models: Required<Pick<CapabilityModels, 'generic' | 'smart' | 'coding' | 'fast'>>;
75
101
  maxPrs: number;
76
102
  maxSessions: number | null;
77
103
  autoMerge: boolean;
78
104
  mergeMethod: 'squash' | 'merge' | 'rebase';
79
105
  stylePath: string | null;
106
+ formatCommand: string | null;
80
107
  logLevel: 'debug' | 'info' | 'warn' | 'error';
81
108
  concurrency: number;
82
109
  mcpServers: import('../mcp/schema.ts').McpServers;
@@ -10,15 +10,26 @@ export const CapabilityModelsSchema = z
10
10
  .passthrough();
11
11
  export const LogLevelSchema = z.enum(['debug', 'info', 'warn', 'error']);
12
12
  export const MergeMethodSchema = z.enum(['squash', 'merge', 'rebase']);
13
+ export const ProfileSchema = z
14
+ .object({
15
+ openrouterApiKey: z.string().optional(),
16
+ baseURL: z.url().optional(),
17
+ models: CapabilityModelsSchema.optional(),
18
+ })
19
+ .passthrough();
13
20
  export const ConfigFileSchema = z
14
21
  .object({
15
22
  openrouterApiKey: z.string().optional(),
23
+ activeProfile: z.string().optional(),
24
+ profiles: z.record(z.string(), ProfileSchema).optional(),
25
+ baseURL: z.url().optional(),
16
26
  models: CapabilityModelsSchema.optional(),
17
27
  maxPrs: z.number().int().positive().optional(),
18
28
  maxSessions: z.number().int().positive().nullable().optional(),
19
29
  autoMerge: z.boolean().optional(),
20
30
  mergeMethod: MergeMethodSchema.optional(),
21
31
  stylePath: z.string().nullable().optional(),
32
+ formatCommand: z.string().optional(),
22
33
  logLevel: LogLevelSchema.optional(),
23
34
  concurrency: z.number().int().positive().optional(),
24
35
  mcpServers: McpServersSchema.optional(),
@@ -3,6 +3,10 @@ import type { Capability, ResolvedConfig } from '../config/schema.ts';
3
3
  export type Role = 'planner' | 'worker' | 'reviewer' | 'orchestrator';
4
4
  export declare const ROLE_CAPABILITY: Readonly<Record<Role, Capability>>;
5
5
  export type ModelHandles = Record<Role, LanguageModel>;
6
+ export declare function providerSettings(resolved: ResolvedConfig): {
7
+ apiKey: string;
8
+ baseURL?: string;
9
+ };
6
10
  export declare class Credentials {
7
11
  private readonly resolved;
8
12
  private providerInstance;
@@ -6,6 +6,12 @@ export const ROLE_CAPABILITY = {
6
6
  reviewer: 'smart',
7
7
  orchestrator: 'fast',
8
8
  };
9
+ export function providerSettings(resolved) {
10
+ return {
11
+ apiKey: resolved.openrouterApiKey,
12
+ ...(resolved.baseURL ? { baseURL: resolved.baseURL } : {}),
13
+ };
14
+ }
9
15
  export class Credentials {
10
16
  resolved;
11
17
  providerInstance;
@@ -37,7 +43,7 @@ export class Credentials {
37
43
  provider() {
38
44
  if (!this.providerInstance) {
39
45
  Credentials.assertApiKeyPresent(this.resolved);
40
- this.providerInstance = createOpenRouter({ apiKey: this.resolved.openrouterApiKey });
46
+ this.providerInstance = createOpenRouter(providerSettings(this.resolved));
41
47
  }
42
48
  return this.providerInstance;
43
49
  }
@@ -33,6 +33,13 @@ export declare class GitHubClient {
33
33
  getPrForBranch(branch: string): Promise<PullRequest | null>;
34
34
  createPr(input: CreatePrInput): Promise<PullRequest>;
35
35
  waitForChecks(pr: number): Promise<CheckStatus>;
36
+ getFailedCiLogs(pr: number): Promise<Array<{
37
+ check: string;
38
+ logs: string;
39
+ }>>;
40
+ private failedRunIds;
41
+ private failedJobs;
42
+ private jobLogs;
36
43
  listUnresolvedThreads(pr: number): Promise<ReviewThread[]>;
37
44
  private paginateReviewThreads;
38
45
  private paginateThreadComments;
@@ -121,6 +121,67 @@ export class GitHubClient {
121
121
  delay = Math.min(delay * 2, CHECKS_MAX_DELAY_MS);
122
122
  }
123
123
  }
124
+ async getFailedCiLogs(pr) {
125
+ const head = await this.runCmd('gh', ['pr', 'view', String(pr), '--json', 'headRefName,headRefOid'], { cwd: this.cwd });
126
+ if (head.exitCode !== 0)
127
+ return [];
128
+ const parsedHead = safeJson(head.stdout);
129
+ const branch = isRecord(parsedHead) ? parsedHead.headRefName : undefined;
130
+ const sha = isRecord(parsedHead) ? parsedHead.headRefOid : undefined;
131
+ if (typeof branch !== 'string')
132
+ return [];
133
+ const runIds = await this.failedRunIds(branch, typeof sha === 'string' ? sha : undefined);
134
+ if (runIds.length === 0)
135
+ return [];
136
+ const { owner, name } = await this.repoMeta();
137
+ const out = [];
138
+ for (const runId of runIds) {
139
+ for (const job of await this.failedJobs(owner, name, runId)) {
140
+ const logs = await this.jobLogs(owner, name, job.id);
141
+ if (logs.trim())
142
+ out.push({ check: job.name, logs });
143
+ }
144
+ }
145
+ return out;
146
+ }
147
+ async failedRunIds(branch, sha) {
148
+ const r = await this.runCmd('gh', [
149
+ 'run',
150
+ 'list',
151
+ '--branch',
152
+ branch,
153
+ '--json',
154
+ 'databaseId,headSha,conclusion',
155
+ '--limit',
156
+ '30',
157
+ ], { cwd: this.cwd });
158
+ if (r.exitCode !== 0)
159
+ return [];
160
+ const parsed = safeJson(r.stdout);
161
+ const rows = WorkflowRunsSchema.safeParse(parsed);
162
+ if (!rows.success)
163
+ return [];
164
+ const failed = rows.data.filter((run) => FAILED_CONCLUSIONS.has(run.conclusion ?? ''));
165
+ const forSha = sha ? failed.filter((run) => run.headSha === sha) : [];
166
+ return (forSha.length > 0 ? forSha : failed).map((run) => run.databaseId);
167
+ }
168
+ async failedJobs(owner, name, runId) {
169
+ const r = await this.runCmd('gh', ['api', `repos/${owner}/${name}/actions/runs/${runId}/jobs`], { cwd: this.cwd });
170
+ if (r.exitCode !== 0)
171
+ return [];
172
+ const parsed = JobsResponseSchema.safeParse(safeJson(r.stdout));
173
+ if (!parsed.success)
174
+ return [];
175
+ return parsed.data.jobs
176
+ .filter((job) => FAILED_CONCLUSIONS.has(job.conclusion ?? ''))
177
+ .map((job) => ({ id: job.id, name: job.name }));
178
+ }
179
+ async jobLogs(owner, name, jobId) {
180
+ const r = await this.runCmd('gh', ['api', `repos/${owner}/${name}/actions/jobs/${jobId}/logs`], {
181
+ cwd: this.cwd,
182
+ });
183
+ return r.exitCode === 0 ? r.stdout : '';
184
+ }
124
185
  async listUnresolvedThreads(pr) {
125
186
  const { owner, name } = await this.repoMeta();
126
187
  const threads = await this.paginateReviewThreads(owner, name, pr);
@@ -249,6 +310,30 @@ export class GitHubClient {
249
310
  return { ok: r.exitCode === 0, scopes };
250
311
  }
251
312
  }
313
+ const FAILED_CONCLUSIONS = new Set(['failure', 'timed_out', 'startup_failure', 'action_required']);
314
+ const WorkflowRunsSchema = z.array(z.object({
315
+ databaseId: z.number(),
316
+ headSha: z.string().optional(),
317
+ conclusion: z.string().nullable().optional(),
318
+ }));
319
+ const JobsResponseSchema = z.object({
320
+ jobs: z.array(z.object({
321
+ id: z.number(),
322
+ name: z.string(),
323
+ conclusion: z.string().nullable().optional(),
324
+ })),
325
+ });
326
+ function safeJson(s) {
327
+ try {
328
+ return JSON.parse(s);
329
+ }
330
+ catch {
331
+ return null;
332
+ }
333
+ }
334
+ function isRecord(v) {
335
+ return typeof v === 'object' && v !== null;
336
+ }
252
337
  function isPrNotFoundStderr(stderr) {
253
338
  return /no pull requests? found|could not resolve to a pullrequest|no open pull requests/i.test(stderr);
254
339
  }
@@ -1,5 +1,5 @@
1
1
  import { resolve as resolvePath } from 'node:path';
2
- import { bashTool, composeSystemPrompt, editFileTool, globTool, grepTool, multiEditTool, readFileTool, writeFileTool, } from '@developerz.ai/ai-claude-compat';
2
+ import { bashTool, composeSystemPrompt, editFileTool, globTool, grepTool, multiBashTool, multiEditTool, readFileTool, writeFileTool, } from '@developerz.ai/ai-claude-compat';
3
3
  import { tool } from 'ai';
4
4
  import { execa } from 'execa';
5
5
  import { z } from 'zod';
@@ -20,6 +20,7 @@ export function localEditTools(cwd) {
20
20
  grep: grepTool({ cwd }),
21
21
  glob: globTool({ cwd }),
22
22
  bash: bashTool({ cwd }),
23
+ multiBash: multiBashTool({ cwd }),
23
24
  };
24
25
  }
25
26
  export function localReadTools(cwd) {
@@ -156,6 +157,7 @@ function defaultMakeOrchestrator(ctx) {
156
157
  baseBranch,
157
158
  styleContents: style,
158
159
  rollingContext,
160
+ ...(input.resolved.formatCommand ? { formatCommand: input.resolved.formatCommand } : {}),
159
161
  });
160
162
  },
161
163
  finalizeCommit: (group, delivery, worktreePath) => orch.finalizeCommit(group, delivery, worktreePath),
@@ -191,6 +193,7 @@ function resolveWorkerTools(set, cwd) {
191
193
  grep: set.grep ?? local.grep,
192
194
  glob: set.glob ?? local.glob,
193
195
  bash: set.bash ?? local.bash,
196
+ multiBash: set.multiBash ?? local.multiBash,
194
197
  };
195
198
  }
196
199
  function resolveReviewerTools(set, cwd, github) {
@@ -11,6 +11,18 @@ export type TakeOverGithub = {
11
11
  mergePr(pr: number, method: MergeMethod): Promise<void>;
12
12
  replyToThread(threadId: string, body: string): Promise<void>;
13
13
  resolveThread(threadId: string): Promise<void>;
14
+ getFailedCiLogs?(pr: number): Promise<Array<{
15
+ check: string;
16
+ logs: string;
17
+ }>>;
18
+ };
19
+ export type PrContextPort = {
20
+ clear(pr: number): Promise<void>;
21
+ saveCiFailures(pr: number, failures: ReadonlyArray<{
22
+ check: string;
23
+ logs: string;
24
+ }>): Promise<string | null>;
25
+ saveComments(pr: number, threads: readonly ReviewThread[]): Promise<string | null>;
14
26
  };
15
27
  export type TakeOverSubagents = {
16
28
  reviewerModel: LanguageModel;
@@ -18,6 +30,7 @@ export type TakeOverSubagents = {
18
30
  workerModel: LanguageModel;
19
31
  workerTools: WorkerTools;
20
32
  styleContents: string;
33
+ formatCommand?: string;
21
34
  runReviewerOverride?: (input: {
22
35
  pr: number;
23
36
  threads: ReviewThread[];
@@ -38,6 +51,7 @@ export type TakeOverFlowInput = {
38
51
  baseBranch: string;
39
52
  github: TakeOverGithub;
40
53
  subagents: TakeOverSubagents;
54
+ prContext?: PrContextPort;
41
55
  mergeMethod: MergeMethod;
42
56
  maxIterations?: number;
43
57
  cooldownMs?: number;
@@ -19,9 +19,24 @@ export async function runTakeOverFlow(input) {
19
19
  if (ciStatus === 'success' && threads.length === 0) {
20
20
  break;
21
21
  }
22
+ let ciLogsDir = null;
23
+ if (input.prContext) {
24
+ await input.prContext.clear(input.pr);
25
+ if ((ciStatus === 'failure' || ciStatus === 'cancelled') && input.github.getFailedCiLogs) {
26
+ const failures = await input.github.getFailedCiLogs(input.pr);
27
+ ciLogsDir = await input.prContext.saveCiFailures(input.pr, failures);
28
+ log?.info('take-over: downloaded ci logs', {
29
+ pr: input.pr,
30
+ checks: failures.length,
31
+ dir: ciLogsDir,
32
+ });
33
+ }
34
+ if (threads.length > 0)
35
+ await input.prContext.saveComments(input.pr, threads);
36
+ }
22
37
  let pushedSomething = false;
23
38
  if (ciStatus === 'failure' || ciStatus === 'cancelled') {
24
- const fixed = await runWorkerCiFix(input);
39
+ const fixed = await runWorkerCiFix(input, ciLogsDir);
25
40
  if (fixed.kind === 'blocked') {
26
41
  return { kind: 'blocked', reason: fixed.reason, iterations: iteration };
27
42
  }
@@ -104,14 +119,14 @@ async function runReviewerThreads(input, threads) {
104
119
  styleContents: input.subagents.styleContents,
105
120
  });
106
121
  }
107
- async function runWorkerCiFix(input) {
122
+ async function runWorkerCiFix(input, ciLogsDir) {
123
+ const readTask = ciLogsDir
124
+ ? `Read the downloaded CI failure logs in ${ciLogsDir} (one file per failed check, full untruncated logs) with your shell/read tools, then fix every failure those logs report.`
125
+ : `Read the CI logs (via gh) and fix every failing check on PR #${input.pr}.`;
108
126
  const group = {
109
127
  id: `takeover-ci-${input.pr}`,
110
128
  title: `Fix CI failures on PR #${input.pr}`,
111
- tasks: [
112
- `Read the CI logs (via gh) and fix every failing check on PR #${input.pr}.`,
113
- 'Run the project test/lint commands locally to verify, then stage fixes.',
114
- ],
129
+ tasks: [readTask, 'Run the project test/lint commands locally to verify, then stage fixes.'],
115
130
  dependsOn: [],
116
131
  branch: null,
117
132
  pr: input.pr,
@@ -137,6 +152,7 @@ async function runWorkerCiFix(input) {
137
152
  baseBranch: input.baseBranch,
138
153
  styleContents: input.subagents.styleContents,
139
154
  rollingContext: '',
155
+ ...(input.subagents.formatCommand ? { formatCommand: input.subagents.formatCommand } : {}),
140
156
  });
141
157
  }
142
158
  function defaultSleep(ms) {
@@ -1,4 +1,5 @@
1
- import { generateText, hasToolCall, Output, stepCountIs, ToolLoopAgent } from 'ai';
1
+ import { submittedOutput } from '@developerz.ai/ai-claude-compat';
2
+ import { generateText, hasToolCall, stepCountIs, ToolLoopAgent, tool } from 'ai';
2
3
  import { ExecaError, execa } from 'execa';
3
4
  import { z } from 'zod';
4
5
  import { makePlannerTool, makeReviewerTool, makeWorkerTool, } from "./subagent-tools.js";
@@ -133,15 +134,26 @@ export class Orchestrator {
133
134
  const result = await generateText({
134
135
  model: this.init.credentials.modelFor('orchestrator'),
135
136
  prompt: this.buildPrPrompt(group, delivery),
136
- output: Output.object({ schema: PrCompositionSchema, name: 'PrComposition' }),
137
+ tools: {
138
+ submit: tool({
139
+ description: 'Submit the composed pull-request title and body (the PrComposition schema).',
140
+ inputSchema: PrCompositionSchema,
141
+ execute: async (composition) => composition,
142
+ }),
143
+ },
144
+ toolChoice: { type: 'tool', toolName: 'submit' },
137
145
  });
138
- return result.experimental_output;
146
+ const out = submittedOutput(result, PrCompositionSchema);
147
+ if (!out) {
148
+ throw new Error('orchestrator did not submit a PR composition');
149
+ }
150
+ return out;
139
151
  }
140
152
  buildPrPrompt(group, delivery) {
141
153
  return [
142
154
  this.buildSystemPrompt(),
143
155
  '',
144
- 'Compose the pull-request title and body for this PR group. Return JSON.',
156
+ 'Compose the pull-request title and body for this PR group, then call the submit tool with it.',
145
157
  '- title: conventional-commit style, ≤72 chars',
146
158
  '- body: short summary + bulleted file changes + relevant rolling context',
147
159
  '',
@@ -0,0 +1,20 @@
1
+ import type { ReviewThread } from '../github/schema.ts';
2
+ export type CiFailure = {
3
+ check: string;
4
+ logs: string;
5
+ };
6
+ export type PrContextSummary = {
7
+ prDir: string;
8
+ ciDir: string | null;
9
+ commentsDir: string | null;
10
+ ciCount: number;
11
+ commentCount: number;
12
+ };
13
+ export declare class PrContextStore {
14
+ private readonly stateDir;
15
+ constructor(stateDir: string);
16
+ prDir(pr: number): string;
17
+ clear(pr: number): Promise<void>;
18
+ saveCiFailures(pr: number, failures: readonly CiFailure[]): Promise<string | null>;
19
+ saveComments(pr: number, threads: readonly ReviewThread[]): Promise<string | null>;
20
+ }