@developerz.ai/aitm 0.0.3 → 0.0.4

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.
@@ -370,9 +370,11 @@ async function defaultRunMergeFlow(input) {
370
370
  const { runTakeOverFlow } = await import("../loop/take-over-flow.js");
371
371
  const { execa } = await import('execa');
372
372
  const { githubThreadTool } = await import("../tools/github-thread-tool.js");
373
+ const { PrContextStore } = await import("../state/pr-context-store.js");
373
374
  const worktreePath = input.cwd;
374
375
  const baseBranch = await input.github.defaultBranch();
375
376
  const styleContents = input.agentConfig.contents;
377
+ const prContext = new PrContextStore(resolvePath(input.cwd, '.ai-task-master'));
376
378
  const workerTools = localEditTools(worktreePath);
377
379
  const github = githubThreadTool({ github: input.github });
378
380
  const result = await runTakeOverFlow({
@@ -380,6 +382,7 @@ async function defaultRunMergeFlow(input) {
380
382
  worktreePath,
381
383
  baseBranch,
382
384
  github: input.github,
385
+ prContext,
383
386
  mergeMethod: input.runState.options.mergeMethod,
384
387
  push: async (cwd) => {
385
388
  const r = await execa('git', ['push'], { cwd });
@@ -393,6 +396,7 @@ async function defaultRunMergeFlow(input) {
393
396
  workerModel: input.credentials.modelFor('worker'),
394
397
  workerTools,
395
398
  styleContents,
399
+ ...(input.resolved.formatCommand ? { formatCommand: input.resolved.formatCommand } : {}),
396
400
  },
397
401
  });
398
402
  if (result.kind === 'merged') {
@@ -19,6 +19,7 @@ const KNOWN_KEYS = new Set([
19
19
  'autoMerge',
20
20
  'mergeMethod',
21
21
  'stylePath',
22
+ 'formatCommand',
22
23
  'logLevel',
23
24
  'concurrency',
24
25
  'mcpServers',
@@ -29,6 +30,7 @@ const DEFAULTS = {
29
30
  autoMerge: true,
30
31
  mergeMethod: 'squash',
31
32
  stylePath: null,
33
+ formatCommand: null,
32
34
  logLevel: 'info',
33
35
  concurrency: 1,
34
36
  };
@@ -68,6 +70,7 @@ export class ConfigLoader {
68
70
  autoMerge: pick(cliOverrides.autoMerge, project?.autoMerge, global?.autoMerge, DEFAULTS.autoMerge),
69
71
  mergeMethod: pick(cliOverrides.mergeMethod, project?.mergeMethod, global?.mergeMethod, DEFAULTS.mergeMethod),
70
72
  stylePath: pickNullable(cliOverrides.stylePath, project?.stylePath, global?.stylePath, DEFAULTS.stylePath),
73
+ formatCommand: pickNullable(undefined, project?.formatCommand, global?.formatCommand, DEFAULTS.formatCommand),
71
74
  logLevel: pick(undefined, project?.logLevel, global?.logLevel, DEFAULTS.logLevel),
72
75
  concurrency: pick(cliOverrides.concurrency, project?.concurrency, global?.concurrency, DEFAULTS.concurrency),
73
76
  mcpServers,
@@ -35,6 +35,7 @@ export declare const ConfigFileSchema: z.ZodObject<{
35
35
  rebase: "rebase";
36
36
  }>>;
37
37
  stylePath: z.ZodOptional<z.ZodNullable<z.ZodString>>;
38
+ formatCommand: z.ZodOptional<z.ZodString>;
38
39
  logLevel: z.ZodOptional<z.ZodEnum<{
39
40
  debug: "debug";
40
41
  info: "info";
@@ -77,6 +78,7 @@ export type ResolvedConfig = {
77
78
  autoMerge: boolean;
78
79
  mergeMethod: 'squash' | 'merge' | 'rebase';
79
80
  stylePath: string | null;
81
+ formatCommand: string | null;
80
82
  logLevel: 'debug' | 'info' | 'warn' | 'error';
81
83
  concurrency: number;
82
84
  mcpServers: import('../mcp/schema.ts').McpServers;
@@ -19,6 +19,7 @@ export const ConfigFileSchema = z
19
19
  autoMerge: z.boolean().optional(),
20
20
  mergeMethod: MergeMethodSchema.optional(),
21
21
  stylePath: z.string().nullable().optional(),
22
+ formatCommand: z.string().optional(),
22
23
  logLevel: LogLevelSchema.optional(),
23
24
  concurrency: z.number().int().positive().optional(),
24
25
  mcpServers: McpServersSchema.optional(),
@@ -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) {
@@ -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
+ }
@@ -0,0 +1,60 @@
1
+ import { mkdir, rm, writeFile } from 'node:fs/promises';
2
+ import { join } from 'node:path';
3
+ export class PrContextStore {
4
+ stateDir;
5
+ constructor(stateDir) {
6
+ this.stateDir = stateDir;
7
+ }
8
+ prDir(pr) {
9
+ return join(this.stateDir, 'debugging', 'pr', String(pr));
10
+ }
11
+ async clear(pr) {
12
+ await rm(this.prDir(pr), { recursive: true, force: true });
13
+ }
14
+ async saveCiFailures(pr, failures) {
15
+ if (failures.length === 0)
16
+ return null;
17
+ const ciDir = join(this.prDir(pr), 'ci');
18
+ await mkdir(ciDir, { recursive: true });
19
+ const used = new Map();
20
+ for (const { check, logs } of failures) {
21
+ const base = sanitize(check);
22
+ const n = used.get(base) ?? 0;
23
+ used.set(base, n + 1);
24
+ const file = n === 0 ? `failed_${base}.txt` : `failed_${base}_${n}.txt`;
25
+ const header = `CI check failed: ${check}\nPR: #${pr}\n${'='.repeat(60)}\n\n`;
26
+ await writeFile(join(ciDir, file), header + logs);
27
+ }
28
+ await writeFile(join(ciDir, 'summary.txt'), [
29
+ `PR #${pr} — ${failures.length} failed check(s):`,
30
+ ...failures.map((f) => ` - ${f.check}`),
31
+ ].join('\n'));
32
+ return ciDir;
33
+ }
34
+ async saveComments(pr, threads) {
35
+ if (threads.length === 0)
36
+ return null;
37
+ const commentsDir = join(this.prDir(pr), 'comments');
38
+ await mkdir(commentsDir, { recursive: true });
39
+ let i = 0;
40
+ for (const thread of threads) {
41
+ i += 1;
42
+ const path = thread.path ?? 'general';
43
+ const body = thread.comments
44
+ .map((c) => `@${c.author}:\n${c.body}`)
45
+ .join(`\n${'-'.repeat(40)}\n`);
46
+ const header = `Review thread on ${path} (thread ${thread.id})\nPR: #${pr}\n${'='.repeat(60)}\n\n`;
47
+ await writeFile(join(commentsDir, `${String(i).padStart(3, '0')}_${sanitize(path)}.txt`), header + body);
48
+ }
49
+ const paths = [...new Set(threads.map((t) => t.path ?? 'general'))].sort();
50
+ await writeFile(join(commentsDir, 'summary.txt'), [
51
+ `PR #${pr} — ${threads.length} unresolved review thread(s).`,
52
+ 'Files with comments:',
53
+ ...paths.map((p) => ` - ${p}`),
54
+ ].join('\n'));
55
+ return commentsDir;
56
+ }
57
+ }
58
+ function sanitize(s) {
59
+ return s.replace(/[^a-zA-Z0-9._-]+/g, '_').replace(/^_+|_+$/g, '') || 'unnamed';
60
+ }
@@ -1,4 +1,4 @@
1
- import type { BashInput, BashOutput, EditFileInput, EditFileOutput, GlobInput, GlobOutput, GrepInput, GrepOutput, MultiEditInput, MultiEditOutput, ReadFileInput, ReadFileOutput, WriteFileInput, WriteFileOutput } from '@developerz.ai/ai-claude-compat';
1
+ import type { BashInput, BashOutput, EditFileInput, EditFileOutput, GlobInput, GlobOutput, GrepInput, GrepOutput, MultiBashInput, MultiBashOutput, MultiEditInput, MultiEditOutput, ReadFileInput, ReadFileOutput, WriteFileInput, WriteFileOutput } from '@developerz.ai/ai-claude-compat';
2
2
  import { type DeepPartial, Output, type Tool, type ToolLoopAgent } from 'ai';
3
3
  import { z } from 'zod';
4
4
  import type { PrGroup } from '../state/schema.ts';
@@ -11,6 +11,7 @@ export type WorkerTools = {
11
11
  grep: Tool<GrepInput, GrepOutput>;
12
12
  glob: Tool<GlobInput, GlobOutput>;
13
13
  bash: Tool<BashInput, BashOutput>;
14
+ multiBash: Tool<MultiBashInput, MultiBashOutput>;
14
15
  };
15
16
  export declare const FileManifestEntrySchema: z.ZodObject<{
16
17
  path: z.ZodString;
@@ -43,6 +44,7 @@ export type WorkerInput = {
43
44
  baseBranch: string;
44
45
  styleContents: string;
45
46
  rollingContext: string;
47
+ formatCommand?: string;
46
48
  };
47
49
  export type FileChange = {
48
50
  path: string;
@@ -36,6 +36,9 @@ const EDITOR_SYSTEM_PREFIX = [
36
36
  ' replacement) or `multiEdit` (several replacements applied atomically). Use `writeFile` only',
37
37
  ' for a full rewrite.',
38
38
  '- To DELETE a file, use `bash` with `rm -f <path>`.',
39
+ '- For a dependent sequence of shell steps (e.g. `mkdir … && generate && test`), prefer',
40
+ ' `multiBash` with an ordered `commands` array — it stops at the first failure, so you',
41
+ ' see exactly which step broke without chaining `&&` by hand.',
39
42
  'You may issue multiple tool calls in parallel.',
40
43
  '',
41
44
  "IMPORTANT: your final assistant message is returned to the outer Worker as this file's",
@@ -65,7 +68,10 @@ export async function runWorker(agent, input) {
65
68
  try {
66
69
  const manifest = await planManifest(agent, input);
67
70
  if (manifest.files.length === 0) {
68
- return { kind: 'blocked', reason: 'worker produced an empty file manifest' };
71
+ return {
72
+ kind: 'blocked',
73
+ reason: 'The Worker returned an empty file manifest — the configured coding model produced no files to change for this PR group. This usually means the model is not capable enough to plan the work; try a more capable coding model (set `models.coding` in .ai-task-master/config.json or pass a stronger --model).',
74
+ };
69
75
  }
70
76
  const changes = await Promise.all(manifest.files.map((file) => runEditor(init, file, input)));
71
77
  await commitOnBranch(init.tools.bash, input, branch, manifest.draftCommitMessage);
@@ -133,6 +139,9 @@ async function commitOnBranch(bash, input, branch, message) {
133
139
  }
134
140
  const wt = shQuote(input.worktreePath);
135
141
  await runBash(exec, `git -C ${wt} checkout -B ${shQuote(branch)}`);
142
+ if (input.formatCommand) {
143
+ await runBash(exec, `cd ${wt} && ${input.formatCommand}`);
144
+ }
136
145
  await runBash(exec, `git -C ${wt} add -A`);
137
146
  await runBash(exec, `git -C ${wt} commit -m ${shQuote(message)}`);
138
147
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@developerz.ai/aitm",
3
- "version": "0.0.3",
3
+ "version": "0.0.4",
4
4
  "description": "Autonomous task orchestrator. Goal in, merged PRs out.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -42,7 +42,7 @@
42
42
  },
43
43
  "dependencies": {
44
44
  "@ai-sdk/mcp": "^1.0.42",
45
- "@developerz.ai/ai-claude-compat": "0.0.3",
45
+ "@developerz.ai/ai-claude-compat": "0.0.4",
46
46
  "@openrouter/ai-sdk-provider": "^2.9.0",
47
47
  "ai": "^6.0.182",
48
48
  "execa": "^9.6.1",