@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.
- package/dist/agent-config/agent-config-detector.js +0 -3
- package/dist/cli/args.js +0 -10
- package/dist/cli/cli.js +0 -8
- package/dist/cli/commands.js +4 -46
- package/dist/compaction/compactor.js +0 -24
- package/dist/config/config-loader.js +3 -29
- package/dist/config/config-writer.js +0 -5
- package/dist/config/schema.d.ts +2 -0
- package/dist/config/schema.js +1 -12
- package/dist/credentials/credentials.js +0 -14
- package/dist/credentials/defaults.js +0 -15
- package/dist/fs/atomic-write.js +0 -7
- package/dist/github/errors.js +0 -2
- package/dist/github/github-client.d.ts +7 -0
- package/dist/github/github-client.js +85 -21
- package/dist/github/schema.js +0 -2
- package/dist/index.js +0 -2
- package/dist/logger/logger.js +0 -6
- package/dist/loop/run-loop-adapter.js +4 -46
- package/dist/loop/take-over-flow.d.ts +14 -0
- package/dist/loop/take-over-flow.js +22 -43
- package/dist/loop/work-loop.js +0 -36
- package/dist/mcp/mcp-client.js +0 -13
- package/dist/mcp/schema.js +0 -15
- package/dist/openrouter/client.js +0 -4
- package/dist/openrouter/model-limits.js +0 -4
- package/dist/openrouter/server-tools.js +0 -16
- package/dist/orchestrator/orchestrator.js +0 -26
- package/dist/orchestrator/subagent-tools.js +0 -22
- package/dist/orchestrator/system-prompts.js +0 -3
- package/dist/plan/plan-graph.js +0 -9
- package/dist/plan/schema.js +0 -8
- package/dist/state/pr-context-store.d.ts +20 -0
- package/dist/state/pr-context-store.js +60 -0
- package/dist/state/schema.js +0 -5
- package/dist/state/state-store.js +0 -8
- package/dist/subagents/factory.js +0 -9
- package/dist/subagents/planner.js +0 -9
- package/dist/subagents/reviewer.js +0 -25
- package/dist/subagents/worker.d.ts +3 -1
- package/dist/subagents/worker.js +10 -28
- package/dist/testing/temp-repo.js +0 -4
- package/dist/tools/datetime.js +0 -9
- package/dist/tools/fetch-html.js +0 -24
- package/dist/tools/github-thread-tool.js +0 -15
- package/dist/tools/web-fetch.js +0 -32
- package/dist/workspace/worktree-pool.js +0 -21
- package/package.json +2 -2
- package/dist/agent-config/agent-config-detector.js.map +0 -1
- package/dist/cli/args.js.map +0 -1
- package/dist/cli/cli.js.map +0 -1
- package/dist/cli/commands.js.map +0 -1
- package/dist/compaction/compactor.js.map +0 -1
- package/dist/config/config-loader.js.map +0 -1
- package/dist/config/config-writer.js.map +0 -1
- package/dist/config/schema.js.map +0 -1
- package/dist/credentials/credentials.js.map +0 -1
- package/dist/credentials/defaults.js.map +0 -1
- package/dist/fs/atomic-write.js.map +0 -1
- package/dist/github/errors.js.map +0 -1
- package/dist/github/github-client.js.map +0 -1
- package/dist/github/schema.js.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/logger/logger.js.map +0 -1
- package/dist/loop/run-loop-adapter.js.map +0 -1
- package/dist/loop/take-over-flow.js.map +0 -1
- package/dist/loop/work-loop.js.map +0 -1
- package/dist/mcp/mcp-client.js.map +0 -1
- package/dist/mcp/schema.js.map +0 -1
- package/dist/openrouter/client.js.map +0 -1
- package/dist/openrouter/model-limits.js.map +0 -1
- package/dist/openrouter/server-tools.js.map +0 -1
- package/dist/orchestrator/orchestrator.js.map +0 -1
- package/dist/orchestrator/subagent-tools.js.map +0 -1
- package/dist/orchestrator/system-prompts.js.map +0 -1
- package/dist/plan/plan-graph.js.map +0 -1
- package/dist/plan/schema.js.map +0 -1
- package/dist/state/schema.js.map +0 -1
- package/dist/state/state-store.js.map +0 -1
- package/dist/subagents/factory.js.map +0 -1
- package/dist/subagents/planner.js.map +0 -1
- package/dist/subagents/reviewer.js.map +0 -1
- package/dist/subagents/worker.js.map +0 -1
- package/dist/testing/temp-repo.js.map +0 -1
- package/dist/tools/datetime.js.map +0 -1
- package/dist/tools/fetch-html.js.map +0 -1
- package/dist/tools/github-thread-tool.js.map +0 -1
- package/dist/tools/web-fetch.js.map +0 -1
- 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
|
-
|
|
266
|
-
|
|
267
|
-
|
|
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
|
package/dist/github/schema.js
CHANGED
|
@@ -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
|
package/dist/logger/logger.js
CHANGED
|
@@ -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
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
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
|
package/dist/loop/work-loop.js
CHANGED
|
@@ -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
|