@developerz.ai/aitm 0.0.2 → 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.
Files changed (89) hide show
  1. package/dist/agent-config/agent-config-detector.js +0 -3
  2. package/dist/cli/args.js +0 -10
  3. package/dist/cli/cli.js +0 -8
  4. package/dist/cli/commands.js +4 -46
  5. package/dist/compaction/compactor.js +0 -24
  6. package/dist/config/config-loader.js +3 -29
  7. package/dist/config/config-writer.js +0 -5
  8. package/dist/config/schema.d.ts +2 -0
  9. package/dist/config/schema.js +1 -12
  10. package/dist/credentials/credentials.js +0 -14
  11. package/dist/credentials/defaults.js +0 -15
  12. package/dist/fs/atomic-write.js +0 -7
  13. package/dist/github/errors.js +0 -2
  14. package/dist/github/github-client.d.ts +7 -0
  15. package/dist/github/github-client.js +85 -21
  16. package/dist/github/schema.js +0 -2
  17. package/dist/index.js +0 -2
  18. package/dist/logger/logger.js +0 -6
  19. package/dist/loop/run-loop-adapter.js +4 -46
  20. package/dist/loop/take-over-flow.d.ts +14 -0
  21. package/dist/loop/take-over-flow.js +22 -43
  22. package/dist/loop/work-loop.js +0 -36
  23. package/dist/mcp/mcp-client.js +0 -13
  24. package/dist/mcp/schema.js +0 -15
  25. package/dist/openrouter/client.js +0 -4
  26. package/dist/openrouter/model-limits.js +0 -4
  27. package/dist/openrouter/server-tools.js +0 -16
  28. package/dist/orchestrator/orchestrator.js +0 -26
  29. package/dist/orchestrator/subagent-tools.js +0 -22
  30. package/dist/orchestrator/system-prompts.js +0 -3
  31. package/dist/plan/plan-graph.js +0 -9
  32. package/dist/plan/schema.js +0 -8
  33. package/dist/state/pr-context-store.d.ts +20 -0
  34. package/dist/state/pr-context-store.js +60 -0
  35. package/dist/state/schema.js +0 -5
  36. package/dist/state/state-store.js +0 -8
  37. package/dist/subagents/factory.js +0 -9
  38. package/dist/subagents/planner.js +0 -9
  39. package/dist/subagents/reviewer.js +0 -25
  40. package/dist/subagents/worker.d.ts +3 -1
  41. package/dist/subagents/worker.js +10 -28
  42. package/dist/testing/temp-repo.js +0 -4
  43. package/dist/tools/datetime.js +0 -9
  44. package/dist/tools/fetch-html.js +0 -24
  45. package/dist/tools/github-thread-tool.js +0 -15
  46. package/dist/tools/web-fetch.js +0 -32
  47. package/dist/workspace/worktree-pool.js +0 -21
  48. package/package.json +2 -2
  49. package/dist/agent-config/agent-config-detector.js.map +0 -1
  50. package/dist/cli/args.js.map +0 -1
  51. package/dist/cli/cli.js.map +0 -1
  52. package/dist/cli/commands.js.map +0 -1
  53. package/dist/compaction/compactor.js.map +0 -1
  54. package/dist/config/config-loader.js.map +0 -1
  55. package/dist/config/config-writer.js.map +0 -1
  56. package/dist/config/schema.js.map +0 -1
  57. package/dist/credentials/credentials.js.map +0 -1
  58. package/dist/credentials/defaults.js.map +0 -1
  59. package/dist/fs/atomic-write.js.map +0 -1
  60. package/dist/github/errors.js.map +0 -1
  61. package/dist/github/github-client.js.map +0 -1
  62. package/dist/github/schema.js.map +0 -1
  63. package/dist/index.js.map +0 -1
  64. package/dist/logger/logger.js.map +0 -1
  65. package/dist/loop/run-loop-adapter.js.map +0 -1
  66. package/dist/loop/take-over-flow.js.map +0 -1
  67. package/dist/loop/work-loop.js.map +0 -1
  68. package/dist/mcp/mcp-client.js.map +0 -1
  69. package/dist/mcp/schema.js.map +0 -1
  70. package/dist/openrouter/client.js.map +0 -1
  71. package/dist/openrouter/model-limits.js.map +0 -1
  72. package/dist/openrouter/server-tools.js.map +0 -1
  73. package/dist/orchestrator/orchestrator.js.map +0 -1
  74. package/dist/orchestrator/subagent-tools.js.map +0 -1
  75. package/dist/orchestrator/system-prompts.js.map +0 -1
  76. package/dist/plan/plan-graph.js.map +0 -1
  77. package/dist/plan/schema.js.map +0 -1
  78. package/dist/state/schema.js.map +0 -1
  79. package/dist/state/state-store.js.map +0 -1
  80. package/dist/subagents/factory.js.map +0 -1
  81. package/dist/subagents/planner.js.map +0 -1
  82. package/dist/subagents/reviewer.js.map +0 -1
  83. package/dist/subagents/worker.js.map +0 -1
  84. package/dist/testing/temp-repo.js.map +0 -1
  85. package/dist/tools/datetime.js.map +0 -1
  86. package/dist/tools/fetch-html.js.map +0 -1
  87. package/dist/tools/github-thread-tool.js.map +0 -1
  88. package/dist/tools/web-fetch.js.map +0 -1
  89. package/dist/workspace/worktree-pool.js.map +0 -1
@@ -1,5 +1,3 @@
1
- // docs/github-integration.md, docs/auth.md §"GitHub"
2
- // Only module allowed to shell out to gh. Uses execa (docs/runtime.md — Bun.$ forbidden in src/).
3
1
  import { ExecaError, execa } from 'execa';
4
2
  import { z } from 'zod';
5
3
  import { CiFailed, MergeConflict } from "./errors.js";
@@ -34,8 +32,6 @@ export class GitHubClient {
34
32
  cwd;
35
33
  runCmd;
36
34
  sleep;
37
- // Capability matrix — docs/github-integration.md §"Capabilities".
38
- // Backoff — docs/github-integration.md §"Rate limits" (1s, doubling, 60s cap).
39
35
  constructor(cwd, runCmd = defaultRunCmd, sleep = defaultSleep) {
40
36
  this.cwd = cwd;
41
37
  this.runCmd = runCmd;
@@ -94,9 +90,6 @@ export class GitHubClient {
94
90
  args.push('--draft');
95
91
  for (const label of labels)
96
92
  args.push('--label', label);
97
- // `gh pr create --label X` fails if X doesn't exist yet — which it won't on a fresh repo the
98
- // first time aitm opens a PR. Ensure each label exists first (idempotent via --force; the
99
- // result is intentionally not checked so a labels-permission gap doesn't block PR creation).
100
93
  for (const label of labels) {
101
94
  await this.runCmd('gh', ['label', 'create', label, '--force'], { cwd: this.cwd });
102
95
  }
@@ -104,7 +97,6 @@ export class GitHubClient {
104
97
  if (r.exitCode !== 0) {
105
98
  throw new Error(`gh pr create failed: ${r.stderr.trim() || r.stdout.trim()}`);
106
99
  }
107
- // gh prints the PR URL to stdout; we re-fetch to get the full typed shape.
108
100
  const pr = await this.getPrForBranch(input.head);
109
101
  if (!pr) {
110
102
  throw new Error(`gh pr create succeeded for ${input.head} but PR lookup returned null (stdout: ${r.stdout.trim()})`);
@@ -115,8 +107,6 @@ export class GitHubClient {
115
107
  let delay = CHECKS_INITIAL_DELAY_MS;
116
108
  while (true) {
117
109
  const r = await this.runCmd('gh', ['pr', 'checks', String(pr), '--json', 'bucket,name,state'], { cwd: this.cwd });
118
- // `gh pr checks` exits 8 when any check fails but still emits JSON on stdout. Treat any
119
- // exit code as "command ran" if stdout parses; otherwise propagate the failure.
120
110
  const rows = tryParseChecks(r.stdout);
121
111
  if (!rows) {
122
112
  throw new Error(`gh pr checks failed: ${r.stderr.trim() || r.stdout.trim()}`);
@@ -131,10 +121,69 @@ export class GitHubClient {
131
121
  delay = Math.min(delay * 2, CHECKS_MAX_DELAY_MS);
132
122
  }
133
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
+ }
134
185
  async listUnresolvedThreads(pr) {
135
186
  const { owner, name } = await this.repoMeta();
136
- // GitHub caps connections at 100 nodes per page — page through threads and
137
- // their comments to avoid silently dropping data on large PRs.
138
187
  const threads = await this.paginateReviewThreads(owner, name, pr);
139
188
  const unresolved = threads.filter((t) => !t.isResolved);
140
189
  for (const thread of unresolved) {
@@ -256,19 +305,38 @@ export class GitHubClient {
256
305
  const r = await this.runCmd('gh', ['auth', 'status', '--hostname', 'github.com'], {
257
306
  cwd: this.cwd,
258
307
  });
259
- // `gh auth status` writes its human-readable summary to stderr; stdout is usually empty.
260
308
  const text = `${r.stderr}\n${r.stdout}`;
261
309
  const scopes = parseScopes(text);
262
310
  return { ok: r.exitCode === 0, scopes };
263
311
  }
264
312
  }
265
- // `gh pr view` exits non-zero with messages like:
266
- // "no pull requests found for branch <name>"
267
- // "GraphQL: Could not resolve to a PullRequest..."
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
+ }
268
337
  function isPrNotFoundStderr(stderr) {
269
338
  return /no pull requests? found|could not resolve to a pullrequest|no open pull requests/i.test(stderr);
270
339
  }
271
- // `gh auth status` line shape: " - Token scopes: 'repo', 'workflow', 'read:org'"
272
340
  function parseScopes(text) {
273
341
  const match = text.match(/Token scopes:\s*([^\n]+)/i);
274
342
  if (!match?.[1])
@@ -281,8 +349,6 @@ function parseScopes(text) {
281
349
  }
282
350
  return scopes;
283
351
  }
284
- // Wire shapes for `gh pr checks --json bucket,name,state`. The bucket field is the gh CLI's
285
- // normalized status across providers (Actions, Circle, etc.); CheckStatus is our domain.
286
352
  const CheckBucketSchema = z.enum(['pass', 'fail', 'pending', 'cancel', 'skipping']);
287
353
  const CheckRowSchema = z.object({
288
354
  bucket: CheckBucketSchema,
@@ -331,7 +397,6 @@ function summarizeFailures(rows) {
331
397
  return 'unknown';
332
398
  return bad.map((r) => `${r.name}=${r.bucket}`).join(', ');
333
399
  }
334
- // `gh repo view --json owner,name` returns `{ owner: { login }, name }`.
335
400
  const RepoOwnerNameSchema = z.object({
336
401
  owner: z.object({ login: z.string() }),
337
402
  name: z.string(),
@@ -414,4 +479,3 @@ const GqlThreadCommentsResponseSchema = z.object({
414
479
  }),
415
480
  }),
416
481
  });
417
- //# sourceMappingURL=github-client.js.map
@@ -1,4 +1,3 @@
1
- // docs/github-integration.md — JSON shapes returned by `gh` (parsed through Zod).
2
1
  import { z } from 'zod';
3
2
  export const PrStateSchema = z.enum(['OPEN', 'CLOSED', 'MERGED']);
4
3
  export const PullRequestSchema = z.object({
@@ -20,4 +19,3 @@ export const ReviewThreadSchema = z.object({
20
19
  path: z.string().nullable(),
21
20
  comments: z.array(ReviewCommentSchema),
22
21
  });
23
- //# sourceMappingURL=schema.js.map
package/dist/index.js CHANGED
@@ -1,4 +1,3 @@
1
- // Public surface. Keep this list narrow — most internals are not stable yet.
2
1
  export { AgentConfigDetector } from "./agent-config/agent-config-detector.js";
3
2
  export { main } from "./cli/cli.js";
4
3
  export { Compactor } from "./compaction/compactor.js";
@@ -19,4 +18,3 @@ export { datetimeTool } from "./tools/datetime.js";
19
18
  export { DEFAULT_IMPERSONATE_TARGETS, fetchHtmlTool, isFetchHtmlAvailable, } from "./tools/fetch-html.js";
20
19
  export { DEFAULT_STEALTH_HEADERS, webFetchTool } from "./tools/web-fetch.js";
21
20
  export { WorktreePool } from "./workspace/worktree-pool.js";
22
- //# sourceMappingURL=index.js.map
@@ -1,6 +1,3 @@
1
- // docs/state.md (logs dir), docs/auth.md §Security (redaction policy)
2
- // Structured logs to .ai-task-master/logs/run-{ts}.log; user-facing status to stdout.
3
- // Redact /key|token|secret|authorization/i before serializing.
4
1
  import { appendFile, mkdir } from 'node:fs/promises';
5
2
  import { dirname } from 'node:path';
6
3
  const LEVEL_RANK = {
@@ -42,7 +39,6 @@ export class Logger {
42
39
  const out = redactValue(fields);
43
40
  return out;
44
41
  }
45
- // Flush pending file writes — tests and shutdown hooks await this.
46
42
  async flush() {
47
43
  await this.writeTail;
48
44
  if (this.lastError) {
@@ -86,7 +82,6 @@ export class Logger {
86
82
  await appendFile(file, line);
87
83
  }
88
84
  catch (err) {
89
- // Surface failures via flush() but never crash callers.
90
85
  this.lastError = err instanceof Error ? err : new Error(String(err));
91
86
  }
92
87
  });
@@ -120,4 +115,3 @@ function redactValue(value, seen = new WeakSet()) {
120
115
  function bigintReplacer(_key, value) {
121
116
  return typeof value === 'bigint' ? value.toString() : value;
122
117
  }
123
- //# sourceMappingURL=logger.js.map
@@ -1,19 +1,5 @@
1
- // Production wiring for `aitm start`. Composes the WorkLoop's structural ports out of the
2
- // real Planner, Orchestrator, WorktreePool, PlanGraph, MCP tools and GitHubClient.
3
- //
4
- // Symmetric counterpart to the merge-pr flow: `runStart` injects this as its `runLoop` seam
5
- // (see src/cli/commands.ts `defaultRunLoop`). Every external dependency is reachable through
6
- // a seam so the four WorkLoopResult branches are unit-testable without spawning subagents,
7
- // git, or `gh` — the integration suite (test/integration/) covers the real stack.
8
- //
9
- // Flow:
10
- // 1. Resume detection — if state already holds prGroups, reuse them; else run the Planner.
11
- // 2. Persist the plan into RunState (status → working).
12
- // 3. Build a *live* PlanGraph that re-reads the mirrored prGroups on every ready()/isComplete()
13
- // so status transitions written by the loop are visible (PlanGraph snapshots at construction).
14
- // 4. Bridge the Orchestrator/subagents into the WorkLoopOrchestrator port and run the loop.
15
1
  import { resolve as resolvePath } from 'node:path';
16
- 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';
17
3
  import { tool } from 'ai';
18
4
  import { execa } from 'execa';
19
5
  import { z } from 'zod';
@@ -25,10 +11,6 @@ import { createReviewerAgent, REVIEWER_SYSTEM_PREFIX, runReviewer as runReviewer
25
11
  import { createWorkerAgent, runWorker as runWorkerSubagent, WORKER_SYSTEM_PREFIX, } from "../subagents/worker.js";
26
12
  import { WorktreePool } from "../workspace/worktree-pool.js";
27
13
  import { WorkLoop, } from "./work-loop.js";
28
- // Worktree-scoped Claude-Code-style tools the Worker/Reviewer fall back to when no MCP server
29
- // supplies them. aitm is MCP-first, but a bare `aitm start` (no `mcpServers` configured) must
30
- // still be able to read, search, edit, commit and open a PR — so it uses the compat lib's
31
- // tools, scoped to the active worktree.
32
14
  export function localEditTools(cwd) {
33
15
  return {
34
16
  readFile: readFileTool({ cwd }),
@@ -38,9 +20,9 @@ export function localEditTools(cwd) {
38
20
  grep: grepTool({ cwd }),
39
21
  glob: globTool({ cwd }),
40
22
  bash: bashTool({ cwd }),
23
+ multiBash: multiBashTool({ cwd }),
41
24
  };
42
25
  }
43
- // Read-only subset for the Planner — survey the repo without write/edit/bash.
44
26
  export function localReadTools(cwd) {
45
27
  return {
46
28
  readFile: readFileTool({ cwd }),
@@ -50,8 +32,6 @@ export function localReadTools(cwd) {
50
32
  }
51
33
  export async function runLoopAdapter(input, seams = {}) {
52
34
  const state = seams.state ?? input.state;
53
- // MCP is only needed when a real Planner / Orchestrator default runs. When both are stubbed
54
- // (unit tests), we never connect, so no transport is spawned.
55
35
  const usesMcp = !seams.planGroups || !seams.makeOrchestrator;
56
36
  const mcp = seams.makeMcp
57
37
  ? seams.makeMcp(input)
@@ -64,7 +44,6 @@ export async function runLoopAdapter(input, seams = {}) {
64
44
  }
65
45
  const current = await state.read();
66
46
  const rollingContext = (await state.readContext?.()) ?? '';
67
- // ---- Plan (fresh) or resume (prior prGroups present) -------------------
68
47
  let groups;
69
48
  if (current.prGroups.length > 0) {
70
49
  groups = current.prGroups;
@@ -84,9 +63,6 @@ export async function runLoopAdapter(input, seams = {}) {
84
63
  groups = outcome.groups;
85
64
  await state.update((s) => ({ ...s, status: 'working', prGroups: groups }));
86
65
  }
87
- // ---- Live graph + state proxy ------------------------------------------
88
- // PlanGraph captures its groups at construction, so rebuild it per call against the
89
- // mirror that workLoopState keeps in sync after every persisted update.
90
66
  let liveGroups = groups;
91
67
  const graph = {
92
68
  ready: () => new PlanGraph([...liveGroups]).ready(),
@@ -99,7 +75,6 @@ export async function runLoopAdapter(input, seams = {}) {
99
75
  return next;
100
76
  },
101
77
  };
102
- // ---- Remaining deps ----------------------------------------------------
103
78
  const pool = seams.makePool?.(input) ??
104
79
  new WorktreePool(input.cwd, resolvePath(input.cwd, '.ai-task-master'), input.resolved.concurrency);
105
80
  const github = seams.makeGithub?.(input) ?? input.github;
@@ -128,7 +103,6 @@ export async function runLoopAdapter(input, seams = {}) {
128
103
  }
129
104
  }
130
105
  }
131
- // ---- Plan ------------------------------------------------------------------
132
106
  export function planToPrGroups(plan) {
133
107
  return plan.groups.map((g) => ({
134
108
  id: g.id,
@@ -159,7 +133,6 @@ async function defaultPlanGroups(input, mcp) {
159
133
  return { kind: 'blocked', reason: result.reason };
160
134
  return { kind: 'error', error: result.error };
161
135
  }
162
- // ---- Orchestrator bridge ---------------------------------------------------
163
136
  function defaultMakeOrchestrator(ctx) {
164
137
  const { input, mcp, rollingContext } = ctx;
165
138
  const style = input.agentConfig.contents;
@@ -172,8 +145,6 @@ function defaultMakeOrchestrator(ctx) {
172
145
  });
173
146
  return {
174
147
  runWorker: async ({ group, worktree, baseBranch }) => {
175
- // Prefer MCP-supplied tools; partial-fill any the server omits from the local set so a
176
- // bare `aitm start` (no mcpServers configured) can still edit, commit and open a PR.
177
148
  const tools = resolveWorkerTools(mcp.toolsForRole('worker'), worktree.path);
178
149
  const agent = createWorkerAgent({
179
150
  model: input.credentials.modelFor('worker'),
@@ -186,20 +157,17 @@ function defaultMakeOrchestrator(ctx) {
186
157
  baseBranch,
187
158
  styleContents: style,
188
159
  rollingContext,
160
+ ...(input.resolved.formatCommand ? { formatCommand: input.resolved.formatCommand } : {}),
189
161
  });
190
162
  },
191
163
  finalizeCommit: (group, delivery, worktreePath) => orch.finalizeCommit(group, delivery, worktreePath),
192
164
  openPr: async (group, delivery, baseBranch) => {
193
- // The Worker's commits live on the group branch in a linked worktree (shared object
194
- // store). Push it to origin first — `gh pr create` won't open a PR for a branch that
195
- // isn't on the remote ("No commits between … / Head ref must be a branch").
196
165
  const head = group.branch ?? `aitm/${group.id}`;
197
166
  await execa('git', ['push', '-u', 'origin', head], { cwd: input.cwd });
198
167
  return orch.openPr(group, delivery, baseBranch);
199
168
  },
200
169
  runReviewer: async ({ pr, threads, worktree }) => {
201
170
  const github = githubThreadTool(input.github);
202
- // Same partial-fill as the Worker, plus the local `github` thread tool.
203
171
  const tools = resolveReviewerTools(mcp.toolsForRole('reviewer'), worktree.path, github);
204
172
  const agent = createReviewerAgent({
205
173
  model: input.credentials.modelFor('reviewer'),
@@ -215,10 +183,6 @@ function defaultMakeOrchestrator(ctx) {
215
183
  },
216
184
  };
217
185
  }
218
- // MCP exposes a dynamically-typed ToolSet. The subagents need a statically-shaped tool record.
219
- // Rather than fail closed when a server only exports the legacy readFile/writeFile/bash, we
220
- // partial-fill: prefer the MCP tool for each name, falling back to the local worktree-scoped
221
- // tool for any the server omits. The shape is asserted once at this boundary.
222
186
  function resolveWorkerTools(set, cwd) {
223
187
  const local = localEditTools(cwd);
224
188
  return {
@@ -229,15 +193,12 @@ function resolveWorkerTools(set, cwd) {
229
193
  grep: set.grep ?? local.grep,
230
194
  glob: set.glob ?? local.glob,
231
195
  bash: set.bash ?? local.bash,
196
+ multiBash: set.multiBash ?? local.multiBash,
232
197
  };
233
198
  }
234
199
  function resolveReviewerTools(set, cwd, github) {
235
200
  return { ...resolveWorkerTools(set, cwd), github };
236
201
  }
237
- // The Planner gets only the read-only subset, partial-filled the same way. This is also the fix
238
- // for the latent no-MCP bug: previously the Planner was handed the raw MCP ToolSet with no local
239
- // fallback, so a bare `aitm start` left it with zero tools despite its prompt promising
240
- // readFile/grep/glob.
241
202
  function resolvePlannerTools(set, cwd) {
242
203
  const local = localReadTools(cwd);
243
204
  return {
@@ -246,8 +207,6 @@ function resolvePlannerTools(set, cwd) {
246
207
  glob: set.glob ?? local.glob,
247
208
  };
248
209
  }
249
- // Flat object, not a discriminatedUnion → no `oneOf` in the tool params (rejected by some
250
- // OpenRouter-routed providers). `body` applies to replyToThread only.
251
210
  const githubToolInputSchema = z.object({
252
211
  action: z.enum(['replyToThread', 'resolveThread']),
253
212
  threadId: z.string().min(1),
@@ -267,4 +226,3 @@ export function githubThreadTool(github) {
267
226
  },
268
227
  });
269
228
  }
270
- //# sourceMappingURL=run-loop-adapter.js.map
@@ -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;
@@ -1,23 +1,3 @@
1
- // Take-over merge flow. Drives an externally-built PR (Claude Code, gh pr create, etc.)
2
- // to merge: waits for CI, runs Reviewer to address unresolved review threads (CodeRabbit
3
- // + human reviewers), pushes fixes, and merges. Mirrors the claude-task-master
4
- // `merge_pr()` shape from src/claude_task_master/cli_commands/fix_pr.py:
5
- //
6
- // for iteration in 0..maxIterations:
7
- // status = waitForChecks(pr)
8
- // threads = listUnresolvedThreads(pr)
9
- // if status == success and threads.empty: break
10
- // if status == failure: runWorker (CI-fix path, optional)
11
- // if threads.any: runReviewer per thread, push commits
12
- // sleep(cooldown) # let CI restart
13
- // mergePr(pr)
14
- //
15
- // Unlike WorkLoop.autoMergeFlow, this does NOT acquire a `git worktree`. The user is
16
- // expected to be on the PR branch in their cwd; everything happens in-place. That's
17
- // the simpler model and matches how a human reviewer would handle it.
18
- //
19
- // docs/vendor/ai-sdk/chunk-09.md §"Subagents" — Reviewer/Worker are built ad-hoc per loop
20
- // iteration because their tool bindings (worktree, threads) change each iteration.
21
1
  import { composeSystemPrompt } from '@developerz.ai/ai-claude-compat';
22
2
  import { CiFailed } from "../github/errors.js";
23
3
  import { createReviewerAgent, REVIEWER_SYSTEM_PREFIX, runReviewer, } from "../subagents/reviewer.js";
@@ -29,26 +9,34 @@ export async function runTakeOverFlow(input) {
29
9
  const cooldownMs = input.cooldownMs ?? DEFAULT_COOLDOWN_MS;
30
10
  const sleep = input.sleep ?? defaultSleep;
31
11
  const log = input.logger;
32
- // Hoisted so the post-loop merge can report how many iterations actually ran: on an
33
- // early `break` it holds the break index, on natural exhaustion it equals maxIterations.
34
12
  let iteration = 0;
35
13
  for (; iteration < maxIterations; iteration++) {
36
14
  log?.info('take-over: iteration start', { pr: input.pr, iteration });
37
- // 1. Wait for CI to settle. waitForChecks throws CiFailed on hard failure; we treat
38
- // that as "Worker should try to fix" rather than a fatal error.
39
15
  const ciStatus = await observeCheckStatus(input.github, input.pr);
40
16
  log?.info('take-over: ci status', { pr: input.pr, ciStatus });
41
- // 2. Pull review threads. Always runs — even on CI failure, threads may exist and
42
- // fixing them might happen to fix CI too.
43
17
  const threads = await input.github.listUnresolvedThreads(input.pr);
44
18
  log?.info('take-over: threads', { pr: input.pr, count: threads.length });
45
19
  if (ciStatus === 'success' && threads.length === 0) {
46
- // Happy path: nothing left to do. Merge.
47
20
  break;
48
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
+ }
49
37
  let pushedSomething = false;
50
38
  if (ciStatus === 'failure' || ciStatus === 'cancelled') {
51
- const fixed = await runWorkerCiFix(input);
39
+ const fixed = await runWorkerCiFix(input, ciLogsDir);
52
40
  if (fixed.kind === 'blocked') {
53
41
  return { kind: 'blocked', reason: fixed.reason, iterations: iteration };
54
42
  }
@@ -69,7 +57,6 @@ export async function runTakeOverFlow(input) {
69
57
  iterations: iteration,
70
58
  };
71
59
  }
72
- // Reviewer commits per-thread fixes via the bash tool; we still need to push them.
73
60
  if (reviewed.resolutions.some((r) => r.kind === 'fixed')) {
74
61
  pushedSomething = true;
75
62
  }
@@ -78,12 +65,9 @@ export async function runTakeOverFlow(input) {
78
65
  await input.push(input.worktreePath);
79
66
  log?.info('take-over: pushed fixes', { pr: input.pr });
80
67
  }
81
- // Sleep so the next iteration's waitForChecks sees fresh CI state, not the stale
82
- // success/failure from before our push triggered a new run.
83
68
  if (cooldownMs > 0)
84
69
  await sleep(cooldownMs);
85
70
  }
86
- // Final state check — make sure we didn't fall through the loop with a hung iteration.
87
71
  const finalStatus = await observeCheckStatus(input.github, input.pr);
88
72
  const finalThreads = await input.github.listUnresolvedThreads(input.pr);
89
73
  if (finalStatus !== 'success') {
@@ -104,8 +88,6 @@ export async function runTakeOverFlow(input) {
104
88
  log?.info('take-over: merged', { pr: input.pr });
105
89
  return { kind: 'merged', pr: input.pr, iterations: iteration };
106
90
  }
107
- // Convert waitForChecks' throw-on-failure semantics into a status return so the loop can
108
- // treat CI failure as a recoverable state.
109
91
  async function observeCheckStatus(github, pr) {
110
92
  try {
111
93
  return await github.waitForChecks(pr);
@@ -137,17 +119,14 @@ async function runReviewerThreads(input, threads) {
137
119
  styleContents: input.subagents.styleContents,
138
120
  });
139
121
  }
140
- // Worker CI-fix path. Build a synthetic PR group whose only task is "fix CI on this PR",
141
- // then run the regular Worker. Worker emits a FileManifest and runs per-file editors —
142
- // suitable for "test failed, fix it" if Worker has enough context from the worktree.
143
- 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}.`;
144
126
  const group = {
145
127
  id: `takeover-ci-${input.pr}`,
146
128
  title: `Fix CI failures on PR #${input.pr}`,
147
- tasks: [
148
- `Read the CI logs (via gh) and fix every failing check on PR #${input.pr}.`,
149
- 'Run the project test/lint commands locally to verify, then stage fixes.',
150
- ],
129
+ tasks: [readTask, 'Run the project test/lint commands locally to verify, then stage fixes.'],
151
130
  dependsOn: [],
152
131
  branch: null,
153
132
  pr: input.pr,
@@ -173,6 +152,7 @@ async function runWorkerCiFix(input) {
173
152
  baseBranch: input.baseBranch,
174
153
  styleContents: input.subagents.styleContents,
175
154
  rollingContext: '',
155
+ ...(input.subagents.formatCommand ? { formatCommand: input.subagents.formatCommand } : {}),
176
156
  });
177
157
  }
178
158
  function defaultSleep(ms) {
@@ -180,4 +160,3 @@ function defaultSleep(ms) {
180
160
  setTimeout(resolve, ms);
181
161
  });
182
162
  }
183
- //# sourceMappingURL=take-over-flow.js.map
@@ -1,21 +1,5 @@
1
- // docs/architecture.md (WorkLoop row), docs/commands/start.md §Flow
2
- // Drives the Orchestrator group-by-group. Extended for concurrent execution:
3
- //
4
- // while !plan.isComplete():
5
- // ready = planGraph.ready()
6
- // batch = ready.slice(0, free worker slots)
7
- // await Promise.all(batch.map(g => runGroup(g)))
8
- //
9
- // Each runGroup acquires a WorktreePool slot, runs Worker, and on PR open hands off to
10
- // the merge-pr flow (CI wait + Reviewer + GitHubClient.mergePr).
11
- //
12
- // Deps are structural ports — concrete classes (Orchestrator, GitHubClient, StateStore,
13
- // WorktreePool, PlanGraph) satisfy them at runtime; tests pass literal stubs.
14
1
  import { CiFailed } from "../github/errors.js";
15
2
  const DEFAULT_MERGE_METHOD = 'squash';
16
- // Thrown when a state-write fails *after* an external side effect (openPr/mergePr) already
17
- // succeeded. Carries the real outcome so runGroup doesn't roll the group back to 'blocked'
18
- // and cause a retry to reopen/re-merge work that already landed.
19
3
  class StateWriteAfterSuccess extends Error {
20
4
  outcome;
21
5
  cause;
@@ -52,7 +36,6 @@ export class WorkLoop {
52
36
  }
53
37
  return this.finalResult();
54
38
  }
55
- // Run a single group end-to-end: worktree → Worker → (optionally) merge-pr inline.
56
39
  async runGroup(group) {
57
40
  const branch = group.branch ?? `aitm/${group.id}`;
58
41
  let acquired = false;
@@ -71,18 +54,13 @@ export class WorkLoop {
71
54
  }
72
55
  catch (err) {
73
56
  if (acquired) {
74
- // best-effort release if processGroup itself threw before the inner finally ran;
75
- // the inner finally would have run already in normal flow, so this is defensive.
76
57
  try {
77
58
  await this.deps.pool.release(group.id);
78
59
  }
79
60
  catch {
80
- /* swallow */
81
61
  }
82
62
  }
83
63
  if (err instanceof StateWriteAfterSuccess) {
84
- // External side effect (openPr/mergePr) already succeeded; persist failed.
85
- // Keep the real outcome so a retry doesn't reopen or re-merge.
86
64
  this.outcomes.push(err.outcome);
87
65
  return;
88
66
  }
@@ -91,7 +69,6 @@ export class WorkLoop {
91
69
  await this.markStatus(group.id, 'blocked');
92
70
  }
93
71
  catch {
94
- /* swallow secondary failures */
95
72
  }
96
73
  this.outcomes.push({ groupId: group.id, status: 'blocked', reason });
97
74
  }
@@ -108,20 +85,15 @@ export class WorkLoop {
108
85
  const delivery = workerResult.delivery;
109
86
  await orchestrator.finalizeCommit(group, delivery, worktree.path);
110
87
  const pr = await orchestrator.openPr(group, delivery, baseBranch);
111
- // openPr already landed externally; if persistence fails here, surface the real
112
- // outcome via StateWriteAfterSuccess so the outer catch doesn't flip us to 'blocked'.
113
88
  await this.persistAfterSideEffect({ groupId: group.id, status: 'awaiting-pr', pr: pr.number }, () => this.markStatus(group.id, 'awaiting-pr', { pr: pr.number }));
114
89
  if (!this.deps.autoMerge) {
115
90
  this.outcomes.push({ groupId: group.id, status: 'awaiting-pr', pr: pr.number });
116
91
  return;
117
92
  }
118
93
  await this.autoMergeFlow(group, pr, worktree, baseBranch);
119
- // mergePr already landed externally; same guard as above.
120
94
  await this.persistAfterSideEffect({ groupId: group.id, status: 'merged', pr: pr.number }, () => this.markStatus(group.id, 'merged'));
121
95
  this.outcomes.push({ groupId: group.id, status: 'merged', pr: pr.number });
122
96
  }
123
- // Run a state write that follows a successful external side effect. If the write throws,
124
- // wrap the error in StateWriteAfterSuccess so callers don't roll the outcome back.
125
97
  async persistAfterSideEffect(outcome, write) {
126
98
  try {
127
99
  await write();
@@ -132,7 +104,6 @@ export class WorkLoop {
132
104
  }
133
105
  async autoMergeFlow(group, pr, worktree, baseBranch) {
134
106
  const { orchestrator, github } = this.deps;
135
- // CI: wait for checks. On failure, ask Worker to fix and re-check.
136
107
  try {
137
108
  await github.waitForChecks(pr.number);
138
109
  }
@@ -147,7 +118,6 @@ export class WorkLoop {
147
118
  await orchestrator.finalizeCommit(group, fix.delivery, worktree.path);
148
119
  await github.waitForChecks(pr.number);
149
120
  }
150
- // Review: resolve any unresolved threads via Reviewer.
151
121
  const threads = await github.listUnresolvedThreads(pr.number);
152
122
  if (threads.length > 0) {
153
123
  const review = await orchestrator.runReviewer({ pr: pr.number, threads, worktree });
@@ -166,16 +136,11 @@ export class WorkLoop {
166
136
  return Math.min(concurrency, readyCount, remaining);
167
137
  }
168
138
  async markStatus(id, status, patch = {}) {
169
- // Status transitions do not bump sessionCount — that's owned by incrementSessionCount,
170
- // which fires once per batch dispatch so the in-memory and persisted counters agree.
171
139
  await this.deps.state.update((s) => ({
172
140
  ...s,
173
141
  prGroups: s.prGroups.map((g) => (g.id === id ? { ...g, ...patch, status } : g)),
174
142
  }));
175
143
  }
176
- // Single source of truth for session counting: bump both the in-memory counter (used by
177
- // run() to enforce maxSessions) and the persisted counter (used by reporting/resume) in
178
- // one call. Drops in-memory if persistence fails so the two stay aligned.
179
144
  async incrementSessionCount(by) {
180
145
  if (by <= 0)
181
146
  return;
@@ -208,4 +173,3 @@ export class WorkLoop {
208
173
  return { kind: 'success', outcomes: this.outcomes.slice() };
209
174
  }
210
175
  }
211
- //# sourceMappingURL=work-loop.js.map