create-claude-workspace 2.3.18 → 2.3.20
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/README.md +9 -0
- package/dist/scheduler/agents/worker-pool.mjs +2 -0
- package/dist/scheduler/git/issue-creator.mjs +54 -0
- package/dist/scheduler/git/manager.mjs +129 -1
- package/dist/scheduler/git/pr-manager.mjs +3 -4
- package/dist/scheduler/index.mjs +4 -0
- package/dist/scheduler/loop.mjs +303 -37
- package/dist/scheduler/tasks/inbox.mjs +1 -0
- package/dist/scheduler/tasks/issue-source.mjs +1 -0
- package/dist/scheduler/tasks/parser.mjs +1 -0
- package/dist/scheduler/tools/report-issue.mjs +94 -0
- package/dist/scheduler/tools/scheduler-tools.mjs +17 -0
- package/dist/scheduler/ui/tui.mjs +12 -12
- package/dist/template/.claude/agents/angular-engineer.md +5 -1
- package/dist/template/.claude/agents/backend-ts-architect.md +4 -0
- package/dist/template/.claude/agents/orchestrator.md +13 -0
- package/dist/template/.claude/agents/react-engineer.md +4 -0
- package/dist/template/.claude/agents/senior-code-reviewer.md +4 -0
- package/dist/template/.claude/agents/svelte-engineer.md +4 -0
- package/dist/template/.claude/agents/test-engineer.md +4 -0
- package/dist/template/.claude/agents/ui-engineer.md +5 -1
- package/dist/template/.claude/agents/vue-engineer.md +4 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -65,6 +65,15 @@ Write to `.claude/scheduler/inbox.json` while the scheduler is running:
|
|
|
65
65
|
|
|
66
66
|
The scheduler reads the inbox every iteration and processes messages immediately.
|
|
67
67
|
|
|
68
|
+
#### Proactive problem resolution
|
|
69
|
+
|
|
70
|
+
Agents never dismiss problems as "pre-existing." The scheduler injects a `report_issue` MCP tool into every agent. When an agent discovers an out-of-scope problem:
|
|
71
|
+
|
|
72
|
+
- **Non-blocker**: calls `report_issue(severity: 'non-blocker')` and continues working
|
|
73
|
+
- **Blocker**: calls `report_issue(severity: 'blocker')`, wraps up, and returns
|
|
74
|
+
|
|
75
|
+
The scheduler records the issue locally (`discovered.ndjson`) and creates a platform issue (GitHub/GitLab) if available. Discovered issues enter the task queue on the next iteration.
|
|
76
|
+
|
|
68
77
|
### npx Options
|
|
69
78
|
|
|
70
79
|
```bash
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
// ─── Platform issue creation (GitHub/GitLab) ───
|
|
2
|
+
// Creates issues on the remote platform for discovered problems.
|
|
3
|
+
// Non-fatal — returns null on failure (local record always exists).
|
|
4
|
+
import { execFileSync } from 'node:child_process';
|
|
5
|
+
const CLI_TIMEOUT = 30_000;
|
|
6
|
+
function run(cmd, args, cwd) {
|
|
7
|
+
return execFileSync(cmd, args, { cwd, timeout: CLI_TIMEOUT, stdio: 'pipe', encoding: 'utf-8' }).trim();
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Create an issue on the remote platform.
|
|
11
|
+
* Returns the created issue info, or null if creation failed (non-fatal).
|
|
12
|
+
*/
|
|
13
|
+
export function createPlatformIssue(cwd, platform, opts) {
|
|
14
|
+
try {
|
|
15
|
+
if (platform === 'github')
|
|
16
|
+
return createGitHubIssue(cwd, opts);
|
|
17
|
+
return createGitLabIssue(cwd, opts);
|
|
18
|
+
}
|
|
19
|
+
catch {
|
|
20
|
+
return null;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
function createGitHubIssue(cwd, opts) {
|
|
24
|
+
const args = ['issue', 'create', '--title', opts.title, '--body', opts.body];
|
|
25
|
+
for (const label of opts.labels) {
|
|
26
|
+
args.push('--label', label);
|
|
27
|
+
}
|
|
28
|
+
if (opts.milestone)
|
|
29
|
+
args.push('--milestone', opts.milestone);
|
|
30
|
+
const output = run('gh', args, cwd);
|
|
31
|
+
// gh issue create prints the URL: https://github.com/owner/repo/issues/42
|
|
32
|
+
const urlMatch = output.match(/https:\/\/github\.com\/[^/]+\/[^/]+\/issues\/(\d+)/);
|
|
33
|
+
if (!urlMatch)
|
|
34
|
+
return null;
|
|
35
|
+
return { number: parseInt(urlMatch[1], 10), url: urlMatch[0] };
|
|
36
|
+
}
|
|
37
|
+
function createGitLabIssue(cwd, opts) {
|
|
38
|
+
const args = ['issue', 'create', '--title', opts.title, '--description', opts.body];
|
|
39
|
+
if (opts.labels.length > 0) {
|
|
40
|
+
args.push('--label', opts.labels.join(','));
|
|
41
|
+
}
|
|
42
|
+
if (opts.milestone)
|
|
43
|
+
args.push('--milestone', opts.milestone);
|
|
44
|
+
const output = run('glab', args, cwd);
|
|
45
|
+
// glab outputs: Creating issue... or the URL
|
|
46
|
+
const urlMatch = output.match(/https:\/\/gitlab\.[^/]+\/[^/]+\/[^/]+\/-\/issues\/(\d+)/);
|
|
47
|
+
if (urlMatch)
|
|
48
|
+
return { number: parseInt(urlMatch[1], 10), url: urlMatch[0] };
|
|
49
|
+
// Fallback: look for issue number in output like "#42" or "issue #42"
|
|
50
|
+
const numMatch = output.match(/#(\d+)/);
|
|
51
|
+
if (numMatch)
|
|
52
|
+
return { number: parseInt(numMatch[1], 10), url: output };
|
|
53
|
+
return null;
|
|
54
|
+
}
|
|
@@ -15,7 +15,21 @@ export function createWorktree(projectDir, branchSlug, baseBranch) {
|
|
|
15
15
|
if (existsSync(worktreePath)) {
|
|
16
16
|
return worktreePath;
|
|
17
17
|
}
|
|
18
|
-
|
|
18
|
+
// If the branch already exists (locally or from remote), check it out instead of creating
|
|
19
|
+
if (branchExists(projectDir, branchSlug)) {
|
|
20
|
+
git(['worktree', 'add', worktreePath, branchSlug], projectDir);
|
|
21
|
+
}
|
|
22
|
+
else {
|
|
23
|
+
// Try fetching from remote in case it exists only on origin
|
|
24
|
+
try {
|
|
25
|
+
git(['fetch', 'origin', branchSlug], projectDir, PUSH_TIMEOUT);
|
|
26
|
+
git(['worktree', 'add', worktreePath, '-b', branchSlug, `origin/${branchSlug}`], projectDir);
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
// Branch doesn't exist anywhere — create new from base
|
|
30
|
+
git(['worktree', 'add', worktreePath, '-b', branchSlug, base], projectDir);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
19
33
|
return worktreePath;
|
|
20
34
|
}
|
|
21
35
|
export function cleanupWorktree(projectDir, worktreePath, branch) {
|
|
@@ -216,6 +230,120 @@ export function isBranchMerged(projectDir, branch) {
|
|
|
216
230
|
return false;
|
|
217
231
|
}
|
|
218
232
|
}
|
|
233
|
+
// ─── Branch cleanup ───
|
|
234
|
+
/**
|
|
235
|
+
* Find and clean up local branches that are fully merged into main
|
|
236
|
+
* and have no associated worktree. Also removes their remote counterparts.
|
|
237
|
+
* Returns the list of cleaned branch names.
|
|
238
|
+
*/
|
|
239
|
+
export function cleanMergedBranches(projectDir) {
|
|
240
|
+
const main = getMainBranch(projectDir);
|
|
241
|
+
const cleaned = [];
|
|
242
|
+
// Get all local branches except main
|
|
243
|
+
let branches;
|
|
244
|
+
try {
|
|
245
|
+
const output = git(['branch', '--format', '%(refname:short)'], projectDir);
|
|
246
|
+
branches = output.split('\n').filter(b => b && b !== main);
|
|
247
|
+
}
|
|
248
|
+
catch {
|
|
249
|
+
return [];
|
|
250
|
+
}
|
|
251
|
+
// Get worktree branches to avoid cleaning active worktrees
|
|
252
|
+
const worktreeBranches = new Set();
|
|
253
|
+
try {
|
|
254
|
+
const wtOutput = git(['worktree', 'list', '--porcelain'], projectDir);
|
|
255
|
+
for (const line of wtOutput.split('\n')) {
|
|
256
|
+
if (line.startsWith('branch refs/heads/')) {
|
|
257
|
+
worktreeBranches.add(line.replace('branch refs/heads/', '').trim());
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
catch { /* ignore */ }
|
|
262
|
+
for (const branch of branches) {
|
|
263
|
+
if (worktreeBranches.has(branch))
|
|
264
|
+
continue;
|
|
265
|
+
if (isBranchMerged(projectDir, branch)) {
|
|
266
|
+
// Delete local branch
|
|
267
|
+
try {
|
|
268
|
+
git(['branch', '-d', branch], projectDir);
|
|
269
|
+
}
|
|
270
|
+
catch {
|
|
271
|
+
try {
|
|
272
|
+
git(['branch', '-D', branch], projectDir);
|
|
273
|
+
}
|
|
274
|
+
catch {
|
|
275
|
+
continue;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
// Delete remote branch (best-effort)
|
|
279
|
+
deleteBranchRemote(projectDir, branch);
|
|
280
|
+
cleaned.push(branch);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
return cleaned;
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Clean up remote-tracking branches whose upstream has been deleted.
|
|
287
|
+
* Equivalent to `git fetch --prune`.
|
|
288
|
+
*/
|
|
289
|
+
export function pruneRemoteBranches(projectDir) {
|
|
290
|
+
try {
|
|
291
|
+
git(['fetch', '--prune'], projectDir, PUSH_TIMEOUT);
|
|
292
|
+
}
|
|
293
|
+
catch { /* best-effort */ }
|
|
294
|
+
}
|
|
295
|
+
/**
|
|
296
|
+
* Find local branches that are NOT merged into main, NOT in any worktree,
|
|
297
|
+
* and NOT the main branch itself. These are potentially abandoned/orphaned
|
|
298
|
+
* work that needs AI analysis to decide what to do with them.
|
|
299
|
+
*/
|
|
300
|
+
export function findStaleUnmergedBranches(projectDir) {
|
|
301
|
+
const main = getMainBranch(projectDir);
|
|
302
|
+
const stale = [];
|
|
303
|
+
let branches;
|
|
304
|
+
try {
|
|
305
|
+
const output = git(['branch', '--format', '%(refname:short)'], projectDir);
|
|
306
|
+
branches = output.split('\n').filter(b => b && b !== main);
|
|
307
|
+
}
|
|
308
|
+
catch {
|
|
309
|
+
return [];
|
|
310
|
+
}
|
|
311
|
+
// Get worktree branches to exclude
|
|
312
|
+
const worktreeBranches = new Set();
|
|
313
|
+
try {
|
|
314
|
+
const wtOutput = git(['worktree', 'list', '--porcelain'], projectDir);
|
|
315
|
+
for (const line of wtOutput.split('\n')) {
|
|
316
|
+
if (line.startsWith('branch refs/heads/')) {
|
|
317
|
+
worktreeBranches.add(line.replace('branch refs/heads/', '').trim());
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
catch { /* ignore */ }
|
|
322
|
+
for (const branch of branches) {
|
|
323
|
+
if (worktreeBranches.has(branch))
|
|
324
|
+
continue;
|
|
325
|
+
if (isBranchMerged(projectDir, branch))
|
|
326
|
+
continue;
|
|
327
|
+
// Get commit info
|
|
328
|
+
try {
|
|
329
|
+
const aheadOutput = git(['rev-list', '--count', `${main}..${branch}`], projectDir);
|
|
330
|
+
const commitsAhead = parseInt(aheadOutput, 10) || 0;
|
|
331
|
+
if (commitsAhead === 0)
|
|
332
|
+
continue; // No actual changes
|
|
333
|
+
const logOutput = git(['log', '-1', '--format=%s|||%aI', branch], projectDir);
|
|
334
|
+
const [lastCommitMsg, lastCommitDate] = logOutput.split('|||');
|
|
335
|
+
let hasRemote = false;
|
|
336
|
+
try {
|
|
337
|
+
git(['rev-parse', '--verify', `origin/${branch}`], projectDir);
|
|
338
|
+
hasRemote = true;
|
|
339
|
+
}
|
|
340
|
+
catch { /* no remote */ }
|
|
341
|
+
stale.push({ name: branch, commitsAhead, lastCommitMsg: lastCommitMsg ?? '', lastCommitDate: lastCommitDate ?? '', hasRemote });
|
|
342
|
+
}
|
|
343
|
+
catch { /* skip branches that can't be inspected */ }
|
|
344
|
+
}
|
|
345
|
+
return stale;
|
|
346
|
+
}
|
|
219
347
|
// ─── Git identity ───
|
|
220
348
|
export function hasGitIdentity(projectDir) {
|
|
221
349
|
try {
|
|
@@ -188,7 +188,6 @@ function getGitLabMRComments(cwd, mrIid) {
|
|
|
188
188
|
return [];
|
|
189
189
|
}
|
|
190
190
|
}
|
|
191
|
-
// ─── Merge PR ───
|
|
192
191
|
export function mergePR(cwd, platform, prNumber, method = 'merge') {
|
|
193
192
|
try {
|
|
194
193
|
if (platform === 'github') {
|
|
@@ -201,10 +200,10 @@ export function mergePR(cwd, platform, prNumber, method = 'merge') {
|
|
|
201
200
|
args.push('--squash');
|
|
202
201
|
run('glab', args, cwd, WRITE_TIMEOUT);
|
|
203
202
|
}
|
|
204
|
-
return true;
|
|
203
|
+
return { success: true };
|
|
205
204
|
}
|
|
206
|
-
catch {
|
|
207
|
-
return false;
|
|
205
|
+
catch (err) {
|
|
206
|
+
return { success: false, error: err.message?.split('\n')[0] };
|
|
208
207
|
}
|
|
209
208
|
}
|
|
210
209
|
// ─── Issue label management ───
|
package/dist/scheduler/index.mjs
CHANGED
|
@@ -14,6 +14,7 @@ import { runIteration } from './loop.mjs';
|
|
|
14
14
|
import { checkAuth } from './agents/health-checker.mjs';
|
|
15
15
|
import { pollForNewWork } from './util/idle-poll.mjs';
|
|
16
16
|
import { TUI } from './ui/tui.mjs';
|
|
17
|
+
import { DiscoveredIssueStore } from './tools/report-issue.mjs';
|
|
17
18
|
// ─── Args ───
|
|
18
19
|
export function parseSchedulerArgs(argv) {
|
|
19
20
|
const opts = { ...SCHEDULER_DEFAULTS };
|
|
@@ -281,6 +282,8 @@ export async function runScheduler(opts) {
|
|
|
281
282
|
const onMessage = (msg) => tui.handleMessage(msg);
|
|
282
283
|
const onSpawnStart = (name) => tui.pushAgent(name);
|
|
283
284
|
const onSpawnEnd = () => tui.popAgent();
|
|
285
|
+
// Discovered issue store (shared across all agent spawns)
|
|
286
|
+
const discoveredIssueStore = new DiscoveredIssueStore();
|
|
284
287
|
// Orchestrator client
|
|
285
288
|
const orchestrator = new OrchestratorClient({
|
|
286
289
|
pool,
|
|
@@ -317,6 +320,7 @@ export async function runScheduler(opts) {
|
|
|
317
320
|
state,
|
|
318
321
|
opts,
|
|
319
322
|
logger,
|
|
323
|
+
discoveredIssueStore,
|
|
320
324
|
onMessage,
|
|
321
325
|
onSpawnStart,
|
|
322
326
|
onSpawnEnd,
|
package/dist/scheduler/loop.mjs
CHANGED
|
@@ -8,13 +8,15 @@ import { fetchOpenIssues, issueToTask, updateIssueStatus } from './tasks/issue-s
|
|
|
8
8
|
import { buildGraph, getParallelBatches, isPhaseComplete, getNextPhase, isProjectComplete } from './tasks/queue.mjs';
|
|
9
9
|
import { writeState, appendEvent, createEvent, rotateLog } from './state/state.mjs';
|
|
10
10
|
import { recordSession, getSession, clearSession } from './state/session.mjs';
|
|
11
|
-
import { createWorktree, commitInWorktree, getChangedFiles, cleanupWorktree, mergeToMain, syncMain, pushWorktree, forcePushWorktree, rebaseOnMain, cleanMainForMerge, popStash, getMainBranch, listWorktrees, listOrphanedWorktrees, isBranchMerged, getCurrentBranch, hasUncommittedChanges, deleteBranchRemote, } from './git/manager.mjs';
|
|
12
|
-
import { createPR, getPRStatus, getPRComments, mergePR } from './git/pr-manager.mjs';
|
|
11
|
+
import { createWorktree, commitInWorktree, getChangedFiles, cleanupWorktree, mergeToMain, syncMain, pushWorktree, forcePushWorktree, rebaseOnMain, cleanMainForMerge, popStash, getMainBranch, listWorktrees, listOrphanedWorktrees, isBranchMerged, getCurrentBranch, hasUncommittedChanges, deleteBranchRemote, cleanMergedBranches, pruneRemoteBranches, findStaleUnmergedBranches, } from './git/manager.mjs';
|
|
12
|
+
import { createPR, getPRStatus, getPRComments, mergePR, closePR } from './git/pr-manager.mjs';
|
|
13
13
|
import { scanAgents } from './agents/health-checker.mjs';
|
|
14
14
|
import { detectCIPlatform, fetchFailureLogs } from './git/ci-watcher.mjs';
|
|
15
15
|
import { createRelease } from './git/release.mjs';
|
|
16
16
|
import { processInbox, addTaskMessageToTask } from './tasks/inbox.mjs';
|
|
17
17
|
import { buildPlanPrompt, buildImplementPrompt, buildQAPrompt, buildReviewPrompt, buildReworkPrompt, buildCIFixPrompt, buildPRCommentPrompt, } from './agents/prompt-builder.mjs';
|
|
18
|
+
import { discoveredIssueToTask } from './tools/report-issue.mjs';
|
|
19
|
+
import { createSchedulerToolServer } from './tools/scheduler-tools.mjs';
|
|
18
20
|
const MAX_REVIEW_CYCLES = 5;
|
|
19
21
|
const MAX_BUILD_FIXES = 3;
|
|
20
22
|
const MAX_CI_FIXES = 3;
|
|
@@ -28,15 +30,16 @@ export async function runIteration(deps) {
|
|
|
28
30
|
// Rotate log if needed
|
|
29
31
|
rotateLog(projectDir);
|
|
30
32
|
// Process inbox — immediate, non-blocking
|
|
33
|
+
const inboxTasks = [];
|
|
31
34
|
const inboxMessages = processInbox(projectDir);
|
|
32
35
|
for (const msg of inboxMessages) {
|
|
33
36
|
if (msg.type === 'add-task') {
|
|
34
37
|
const addMsg = msg;
|
|
35
38
|
const nextId = `inbox-${Date.now()}`;
|
|
36
39
|
const newTask = addTaskMessageToTask(addMsg, state.currentPhase, nextId);
|
|
40
|
+
inboxTasks.push(newTask);
|
|
37
41
|
logger.info(`[inbox] New task: ${newTask.title}`);
|
|
38
42
|
appendEvent(projectDir, createEvent('task_started', { taskId: nextId, detail: `inbox: ${newTask.title}` }));
|
|
39
|
-
// Task will be picked up when we load tasks below
|
|
40
43
|
}
|
|
41
44
|
else if (msg.type === 'message') {
|
|
42
45
|
const freeMsg = msg;
|
|
@@ -129,6 +132,18 @@ export async function runIteration(deps) {
|
|
|
129
132
|
}
|
|
130
133
|
}
|
|
131
134
|
}
|
|
135
|
+
// Merge inbox tasks into loaded tasks
|
|
136
|
+
if (inboxTasks.length > 0) {
|
|
137
|
+
tasks.push(...inboxTasks);
|
|
138
|
+
}
|
|
139
|
+
// Drain discovered issues from agent report_issue tool calls (previous iteration)
|
|
140
|
+
const discovered = deps.discoveredIssueStore.drain();
|
|
141
|
+
for (const issue of discovered) {
|
|
142
|
+
const task = discoveredIssueToTask(issue, state.currentPhase);
|
|
143
|
+
tasks.push(task);
|
|
144
|
+
logger.info(`[discovered] ${issue.severity}: ${issue.title} (reported by ${issue.reportedBy})`);
|
|
145
|
+
appendEvent(projectDir, createEvent('issue_discovered', { taskId: task.id, detail: `${issue.severity}: ${issue.title}` }));
|
|
146
|
+
}
|
|
132
147
|
// Reconcile task status with scheduler state (handles restart without --resume)
|
|
133
148
|
for (const task of tasks) {
|
|
134
149
|
if (state.completedTasks.includes(task.id) && task.status !== 'done') {
|
|
@@ -200,13 +215,7 @@ export async function runIteration(deps) {
|
|
|
200
215
|
const pipeline = state.pipelines[taskId];
|
|
201
216
|
const result = await checkPRWatch(taskId, pipeline, projectDir, agents, deps);
|
|
202
217
|
if (result === 'merged') {
|
|
203
|
-
|
|
204
|
-
try {
|
|
205
|
-
branch = getCurrentBranch(pipeline.worktreePath);
|
|
206
|
-
}
|
|
207
|
-
catch {
|
|
208
|
-
branch = taskId;
|
|
209
|
-
}
|
|
218
|
+
const branch = pipeline.branchSlug;
|
|
210
219
|
logger.info(`[${taskId}] PR merged via platform`);
|
|
211
220
|
appendEvent(projectDir, createEvent('pr_merged', { taskId }));
|
|
212
221
|
syncMain(projectDir);
|
|
@@ -228,10 +237,11 @@ export async function runIteration(deps) {
|
|
|
228
237
|
workDone = true;
|
|
229
238
|
}
|
|
230
239
|
else if (result === 'failed') {
|
|
231
|
-
logger.error(`[${taskId}] PR failed — skipping task`);
|
|
240
|
+
logger.error(`[${taskId}] PR failed — all recovery strategies exhausted, skipping task`);
|
|
232
241
|
pipeline.step = 'failed';
|
|
233
242
|
state.skippedTasks.push(taskId);
|
|
234
243
|
delete state.pipelines[taskId];
|
|
244
|
+
appendEvent(projectDir, createEvent('task_skipped', { taskId, detail: 'PR merge failed after all recovery attempts' }));
|
|
235
245
|
workDone = true;
|
|
236
246
|
}
|
|
237
247
|
else if (result === 'rework') {
|
|
@@ -266,6 +276,7 @@ export async function runIteration(deps) {
|
|
|
266
276
|
issueMarker: taskId,
|
|
267
277
|
kitUpgrade: false,
|
|
268
278
|
lineNumber: 0,
|
|
279
|
+
source: 'todo',
|
|
269
280
|
status: 'in-progress',
|
|
270
281
|
changelog: 'changed',
|
|
271
282
|
};
|
|
@@ -364,12 +375,16 @@ export async function runIteration(deps) {
|
|
|
364
375
|
async function runTaskPipeline(task, workerId, agents, deps) {
|
|
365
376
|
const { pool, orchestrator, state, opts, logger, onMessage, onSpawnStart, onSpawnEnd } = deps;
|
|
366
377
|
const projectDir = opts.projectDir;
|
|
378
|
+
// Create per-pipeline MCP server with scheduler tools (report_issue, etc.)
|
|
379
|
+
const ciPlatform = detectCIPlatform(projectDir);
|
|
380
|
+
const mcpServer = createSchedulerToolServer(deps.discoveredIssueStore, task.id, projectDir, ciPlatform === 'none' ? 'none' : ciPlatform);
|
|
381
|
+
const mcpServers = { 'scheduler-tools': mcpServer };
|
|
367
382
|
// Check for existing pipeline (recovered from previous run)
|
|
368
383
|
const existing = state.pipelines[task.id];
|
|
369
384
|
const resumeStep = existing?.step;
|
|
370
385
|
// Create worktree (returns existing path if already exists)
|
|
371
|
-
const slug = existing
|
|
372
|
-
const worktreePath = existing?.worktreePath ?? createWorktree(projectDir,
|
|
386
|
+
const slug = existing?.branchSlug ?? taskToSlug(task);
|
|
387
|
+
const worktreePath = existing?.worktreePath ?? createWorktree(projectDir, slug);
|
|
373
388
|
if (!existing) {
|
|
374
389
|
logger.info(`[${task.id}] Worktree created: ${taskToSlug(task)}`);
|
|
375
390
|
}
|
|
@@ -378,6 +393,7 @@ async function runTaskPipeline(task, workerId, agents, deps) {
|
|
|
378
393
|
taskId: task.id,
|
|
379
394
|
workerId,
|
|
380
395
|
worktreePath,
|
|
396
|
+
branchSlug: slug,
|
|
381
397
|
step: 'plan',
|
|
382
398
|
architectPlan: null,
|
|
383
399
|
apiContract: null,
|
|
@@ -388,6 +404,7 @@ async function runTaskPipeline(task, workerId, agents, deps) {
|
|
|
388
404
|
buildFixes: 0,
|
|
389
405
|
assignedAgent: null,
|
|
390
406
|
prState: null,
|
|
407
|
+
approvalWaitingSince: null,
|
|
391
408
|
};
|
|
392
409
|
pipeline.workerId = workerId;
|
|
393
410
|
state.pipelines[task.id] = pipeline;
|
|
@@ -417,7 +434,7 @@ async function runTaskPipeline(task, workerId, agents, deps) {
|
|
|
417
434
|
cwd: worktreePath,
|
|
418
435
|
prompt: buildPlanPrompt({ task, worktreePath, projectDir }),
|
|
419
436
|
model: getAgentModel(pipeline.assignedAgent, agents, task),
|
|
420
|
-
}, state, task.id, logger, onMessage, onSpawnStart, onSpawnEnd);
|
|
437
|
+
}, state, task.id, logger, onMessage, onSpawnStart, onSpawnEnd, mcpServers);
|
|
421
438
|
if (!planResult.success) {
|
|
422
439
|
logger.error(`[${task.id}] Planning failed: ${planResult.error}`);
|
|
423
440
|
return false;
|
|
@@ -445,7 +462,7 @@ async function runTaskPipeline(task, workerId, agents, deps) {
|
|
|
445
462
|
apiContract: pipeline.apiContract ?? undefined,
|
|
446
463
|
}),
|
|
447
464
|
model: getAgentModel(implAgent, agents, task),
|
|
448
|
-
}, state, task.id, logger, onMessage, onSpawnStart, onSpawnEnd);
|
|
465
|
+
}, state, task.id, logger, onMessage, onSpawnStart, onSpawnEnd, mcpServers);
|
|
449
466
|
if (!implResult.success) {
|
|
450
467
|
logger.error(`[${task.id}] Implementation failed: ${implResult.error}`);
|
|
451
468
|
return false;
|
|
@@ -464,7 +481,7 @@ async function runTaskPipeline(task, workerId, agents, deps) {
|
|
|
464
481
|
reviewFindings: 'This task was interrupted. The branch has existing work but may have build/lint failures, merge conflicts, or failing CI. Please:\n1. Run git fetch origin main && git rebase origin/main (resolve any conflicts)\n2. Run the build and lint scripts from package.json\n3. Fix any errors found\n4. Ensure all tests pass',
|
|
465
482
|
}),
|
|
466
483
|
model: getAgentModel(pipeline.assignedAgent, agents, task),
|
|
467
|
-
}, state, task.id, logger, onMessage, onSpawnStart, onSpawnEnd);
|
|
484
|
+
}, state, task.id, logger, onMessage, onSpawnStart, onSpawnEnd, mcpServers);
|
|
468
485
|
}
|
|
469
486
|
// STEP 3: QA (E2E tests, integration tests, acceptance criteria verification)
|
|
470
487
|
// Only for tasks that need it — skip for pure refactoring, config changes, etc.
|
|
@@ -483,7 +500,7 @@ async function runTaskPipeline(task, workerId, agents, deps) {
|
|
|
483
500
|
testingSection: pipeline.testingSection ?? undefined,
|
|
484
501
|
}),
|
|
485
502
|
model: getAgentModel(qaRouting.agent, agents, task),
|
|
486
|
-
}, state, task.id, logger, onMessage, onSpawnStart, onSpawnEnd);
|
|
503
|
+
}, state, task.id, logger, onMessage, onSpawnStart, onSpawnEnd, mcpServers);
|
|
487
504
|
if (!qaResult.success) {
|
|
488
505
|
pipeline.buildFixes++;
|
|
489
506
|
if (pipeline.buildFixes >= MAX_BUILD_FIXES) {
|
|
@@ -516,7 +533,7 @@ async function runTaskPipeline(task, workerId, agents, deps) {
|
|
|
516
533
|
testingSection: pipeline.testingSection ?? undefined,
|
|
517
534
|
}),
|
|
518
535
|
model: getAgentModel(reviewRouting.agent, agents, task),
|
|
519
|
-
}, state, task.id, logger, onMessage, onSpawnStart, onSpawnEnd);
|
|
536
|
+
}, state, task.id, logger, onMessage, onSpawnStart, onSpawnEnd, mcpServers);
|
|
520
537
|
if (reviewResult.output.includes('**PASS**') || reviewResult.output.includes('PASS')) {
|
|
521
538
|
reviewPassed = true;
|
|
522
539
|
}
|
|
@@ -538,7 +555,7 @@ async function runTaskPipeline(task, workerId, agents, deps) {
|
|
|
538
555
|
reviewFindings: pipeline.reviewFindings,
|
|
539
556
|
}),
|
|
540
557
|
model: getAgentModel(pipeline.assignedAgent, agents, task),
|
|
541
|
-
}, state, task.id, logger, onMessage, onSpawnStart, onSpawnEnd);
|
|
558
|
+
}, state, task.id, logger, onMessage, onSpawnStart, onSpawnEnd, mcpServers);
|
|
542
559
|
pipeline.step = 're-review';
|
|
543
560
|
}
|
|
544
561
|
}
|
|
@@ -563,6 +580,17 @@ async function runTaskPipeline(task, workerId, agents, deps) {
|
|
|
563
580
|
logger.warn(`[${task.id}] Nothing to commit`);
|
|
564
581
|
}
|
|
565
582
|
} // end if !shouldSkip('commit')
|
|
583
|
+
// If branch has no changes vs main, there's nothing to push/merge — skip to done
|
|
584
|
+
const changedFromMain = getChangedFiles(worktreePath);
|
|
585
|
+
if (changedFromMain.length === 0) {
|
|
586
|
+
logger.warn(`[${task.id}] Branch has no changes vs main — nothing to merge`);
|
|
587
|
+
cleanupWorktree(projectDir, worktreePath, slug);
|
|
588
|
+
deleteBranchRemote(projectDir, slug);
|
|
589
|
+
pipeline.step = 'done';
|
|
590
|
+
clearSession(state, task.id);
|
|
591
|
+
delete state.pipelines[task.id];
|
|
592
|
+
return true; // Not a failure, just nothing to do
|
|
593
|
+
}
|
|
566
594
|
const commitMsg = `feat: ${task.title}${task.issueMarker ? ` (${task.issueMarker})` : ''}`;
|
|
567
595
|
// STEP 6: Push + PR/MR flow
|
|
568
596
|
const ciPlatform = detectCIPlatform(projectDir);
|
|
@@ -582,7 +610,7 @@ async function runTaskPipeline(task, workerId, agents, deps) {
|
|
|
582
610
|
pipeline.step = 'pr-create';
|
|
583
611
|
appendEvent(projectDir, createEvent('step_changed', { taskId: task.id, step: 'pr-create' }));
|
|
584
612
|
const issueNum = extractIssueNumber(task.issueMarker);
|
|
585
|
-
const mainBranch =
|
|
613
|
+
const mainBranch = getMainBranch(projectDir);
|
|
586
614
|
try {
|
|
587
615
|
const prInfo = createPR({
|
|
588
616
|
cwd: projectDir,
|
|
@@ -668,7 +696,7 @@ async function runTaskPipeline(task, workerId, agents, deps) {
|
|
|
668
696
|
}
|
|
669
697
|
}
|
|
670
698
|
// ─── Helpers ───
|
|
671
|
-
async function spawnAgent(pool, slotId, opts, state, taskId, logger, onMessage, onSpawnStart, onSpawnEnd) {
|
|
699
|
+
async function spawnAgent(pool, slotId, opts, state, taskId, logger, onMessage, onSpawnStart, onSpawnEnd, mcpServers) {
|
|
672
700
|
const agentName = opts.agent ?? 'claude';
|
|
673
701
|
onSpawnStart?.(agentName);
|
|
674
702
|
// Check for existing session (resume on crash)
|
|
@@ -677,6 +705,7 @@ async function spawnAgent(pool, slotId, opts, state, taskId, logger, onMessage,
|
|
|
677
705
|
...opts,
|
|
678
706
|
resume: existingSession ?? undefined,
|
|
679
707
|
onMessage,
|
|
708
|
+
mcpServers,
|
|
680
709
|
});
|
|
681
710
|
onSpawnEnd?.();
|
|
682
711
|
// Record session for crash recovery
|
|
@@ -789,6 +818,7 @@ export async function recoverOrphanedWorktrees(projectDir, state, logger, _deps)
|
|
|
789
818
|
taskId,
|
|
790
819
|
workerId: -1, // Will be assigned when picked up
|
|
791
820
|
worktreePath,
|
|
821
|
+
branchSlug: branch,
|
|
792
822
|
step,
|
|
793
823
|
architectPlan: null,
|
|
794
824
|
apiContract: null,
|
|
@@ -799,6 +829,7 @@ export async function recoverOrphanedWorktrees(projectDir, state, logger, _deps)
|
|
|
799
829
|
buildFixes: 0,
|
|
800
830
|
assignedAgent: null,
|
|
801
831
|
prState: null,
|
|
832
|
+
approvalWaitingSince: null,
|
|
802
833
|
};
|
|
803
834
|
}
|
|
804
835
|
catch (err) {
|
|
@@ -809,6 +840,104 @@ export async function recoverOrphanedWorktrees(projectDir, state, logger, _deps)
|
|
|
809
840
|
if (ciPlatform !== 'none') {
|
|
810
841
|
recoverOpenMRs(projectDir, ciPlatform, state, logger);
|
|
811
842
|
}
|
|
843
|
+
// Phase 3: Clean up merged branches (local + remote) and prune stale remote refs
|
|
844
|
+
pruneRemoteBranches(projectDir);
|
|
845
|
+
const cleaned = cleanMergedBranches(projectDir);
|
|
846
|
+
if (cleaned.length > 0) {
|
|
847
|
+
logger.info(`[recovery] Cleaned ${cleaned.length} merged branch(es): ${cleaned.join(', ')}`);
|
|
848
|
+
}
|
|
849
|
+
// Phase 4: Analyze stale unmerged branches (no worktree, no MR, not merged)
|
|
850
|
+
// These need AI to decide: re-inject, delete, or ignore
|
|
851
|
+
await analyzeStaleUnmergedBranches(projectDir, state, logger, _deps);
|
|
852
|
+
}
|
|
853
|
+
/**
|
|
854
|
+
* Phase 4: Find branches with unmerged commits that have no worktree and no open MR.
|
|
855
|
+
* Ask orchestrator AI to decide what to do with each one.
|
|
856
|
+
*/
|
|
857
|
+
async function analyzeStaleUnmergedBranches(projectDir, state, logger, deps) {
|
|
858
|
+
const staleBranches = findStaleUnmergedBranches(projectDir);
|
|
859
|
+
if (staleBranches.length === 0)
|
|
860
|
+
return;
|
|
861
|
+
// Exclude branches already in pipelines
|
|
862
|
+
const pipelineBranches = new Set(Object.values(state.pipelines).map(p => p.branchSlug));
|
|
863
|
+
const unhandled = staleBranches.filter(b => !pipelineBranches.has(b.name));
|
|
864
|
+
if (unhandled.length === 0)
|
|
865
|
+
return;
|
|
866
|
+
logger.info(`[recovery] Found ${unhandled.length} stale unmerged branch(es) — consulting orchestrator`);
|
|
867
|
+
const branchSummary = unhandled.map(b => `- ${b.name}: ${b.commitsAhead} commit(s) ahead, last: "${b.lastCommitMsg}" (${b.lastCommitDate})${b.hasRemote ? ' [has remote]' : ' [local only]'}`).join('\n');
|
|
868
|
+
try {
|
|
869
|
+
const result = await deps.orchestrator.consult([
|
|
870
|
+
'The following git branches have unmerged commits but no active worktree or open MR.',
|
|
871
|
+
'For each branch, decide the action:',
|
|
872
|
+
'',
|
|
873
|
+
branchSummary,
|
|
874
|
+
'',
|
|
875
|
+
'Respond with a JSON array of objects: { "branch": "name", "action": "reinject" | "delete" | "ignore", "reason": "..." }',
|
|
876
|
+
'- "reinject": branch has valuable work that should be completed — create worktree and resume pipeline',
|
|
877
|
+
'- "delete": branch is abandoned/superseded/duplicate — safe to remove',
|
|
878
|
+
'- "ignore": leave it alone for now (e.g. user may be working on it manually)',
|
|
879
|
+
].join('\n'));
|
|
880
|
+
// Parse AI decision
|
|
881
|
+
const jsonMatch = result.output.match(/\[[\s\S]*\]/);
|
|
882
|
+
if (!jsonMatch) {
|
|
883
|
+
logger.warn('[recovery] Orchestrator did not return valid JSON for stale branches');
|
|
884
|
+
return;
|
|
885
|
+
}
|
|
886
|
+
const decisions = JSON.parse(jsonMatch[0]);
|
|
887
|
+
for (const d of decisions) {
|
|
888
|
+
const branchInfo = unhandled.find(b => b.name === d.branch);
|
|
889
|
+
if (!branchInfo)
|
|
890
|
+
continue;
|
|
891
|
+
if (d.action === 'delete') {
|
|
892
|
+
logger.info(`[recovery] Deleting stale branch "${d.branch}": ${d.reason}`);
|
|
893
|
+
try {
|
|
894
|
+
execFileSync('git', ['branch', '-D', d.branch], { cwd: projectDir, stdio: 'pipe', timeout: 10_000 });
|
|
895
|
+
}
|
|
896
|
+
catch { /* ignore */ }
|
|
897
|
+
deleteBranchRemote(projectDir, d.branch);
|
|
898
|
+
}
|
|
899
|
+
else if (d.action === 'reinject') {
|
|
900
|
+
logger.info(`[recovery] Re-injecting stale branch "${d.branch}": ${d.reason}`);
|
|
901
|
+
const issueNum = extractIssueFromBranch(d.branch);
|
|
902
|
+
const taskId = issueNum ? `#${issueNum}` : d.branch;
|
|
903
|
+
if (state.completedTasks.includes(taskId) || state.skippedTasks.includes(taskId)) {
|
|
904
|
+
logger.info(`[recovery] Task ${taskId} already completed/skipped — skipping re-injection`);
|
|
905
|
+
continue;
|
|
906
|
+
}
|
|
907
|
+
try {
|
|
908
|
+
const worktreePath = createWorktree(projectDir, d.branch);
|
|
909
|
+
const step = diagnoseStep(projectDir, d.branch, detectCIPlatform(projectDir));
|
|
910
|
+
state.pipelines[taskId] = {
|
|
911
|
+
taskId,
|
|
912
|
+
workerId: -1,
|
|
913
|
+
worktreePath,
|
|
914
|
+
branchSlug: d.branch,
|
|
915
|
+
step,
|
|
916
|
+
architectPlan: null,
|
|
917
|
+
apiContract: null,
|
|
918
|
+
reviewFindings: null,
|
|
919
|
+
testingSection: null,
|
|
920
|
+
reviewCycles: 0,
|
|
921
|
+
ciFixes: 0,
|
|
922
|
+
buildFixes: 0,
|
|
923
|
+
assignedAgent: null,
|
|
924
|
+
prState: null,
|
|
925
|
+
approvalWaitingSince: null,
|
|
926
|
+
};
|
|
927
|
+
logger.info(`[recovery] Branch "${d.branch}" re-injected at step "${step}"`);
|
|
928
|
+
}
|
|
929
|
+
catch (err) {
|
|
930
|
+
logger.warn(`[recovery] Failed to re-inject "${d.branch}": ${err.message?.split('\n')[0]}`);
|
|
931
|
+
}
|
|
932
|
+
}
|
|
933
|
+
else {
|
|
934
|
+
logger.info(`[recovery] Ignoring stale branch "${d.branch}": ${d.reason}`);
|
|
935
|
+
}
|
|
936
|
+
}
|
|
937
|
+
}
|
|
938
|
+
catch (err) {
|
|
939
|
+
logger.warn(`[recovery] Stale branch analysis failed: ${err.message?.split('\n')[0]}`);
|
|
940
|
+
}
|
|
812
941
|
}
|
|
813
942
|
/** Diagnose which pipeline step a recovered worktree should resume from */
|
|
814
943
|
export function diagnoseStep(projectDir, branch, ciPlatform) {
|
|
@@ -860,7 +989,13 @@ export function recoverOpenMRs(projectDir, platform, state, logger) {
|
|
|
860
989
|
continue;
|
|
861
990
|
const prStatus = getPRStatus(projectDir, platform, mr.branch);
|
|
862
991
|
if (prStatus.status === 'merged') {
|
|
863
|
-
logger.info(`[recovery] MR !${mr.number} already merged — closing issue`);
|
|
992
|
+
logger.info(`[recovery] MR !${mr.number} already merged — cleaning up branch + closing issue`);
|
|
993
|
+
// Clean up local branch if it exists (no worktree, but branch may linger)
|
|
994
|
+
try {
|
|
995
|
+
execFileSync('git', ['branch', '-d', mr.branch], { cwd: projectDir, stdio: 'pipe', timeout: 10_000 });
|
|
996
|
+
}
|
|
997
|
+
catch { /* may not exist */ }
|
|
998
|
+
deleteBranchRemote(projectDir, mr.branch);
|
|
864
999
|
closeRecoveredIssue(projectDir, mr.branch, logger);
|
|
865
1000
|
continue;
|
|
866
1001
|
}
|
|
@@ -881,12 +1016,12 @@ export function recoverOpenMRs(projectDir, platform, state, logger) {
|
|
|
881
1016
|
step = 'rework';
|
|
882
1017
|
logger.info(`[recovery] MR !${mr.number} (${mr.branch}) → creating worktree, pipeline at "${step}"`);
|
|
883
1018
|
try {
|
|
884
|
-
const slug = mr.branch.replace(/^feat\//, '');
|
|
885
1019
|
const worktreePath = createWorktree(projectDir, mr.branch);
|
|
886
1020
|
state.pipelines[taskId] = {
|
|
887
1021
|
taskId,
|
|
888
1022
|
workerId: -1,
|
|
889
1023
|
worktreePath,
|
|
1024
|
+
branchSlug: mr.branch,
|
|
890
1025
|
step,
|
|
891
1026
|
architectPlan: null,
|
|
892
1027
|
apiContract: null,
|
|
@@ -897,6 +1032,7 @@ export function recoverOpenMRs(projectDir, platform, state, logger) {
|
|
|
897
1032
|
buildFixes: 0,
|
|
898
1033
|
assignedAgent: null,
|
|
899
1034
|
prState: { prNumber: mr.number, url: '', issueNumber: issueNum },
|
|
1035
|
+
approvalWaitingSince: null,
|
|
900
1036
|
};
|
|
901
1037
|
}
|
|
902
1038
|
catch (err) {
|
|
@@ -937,6 +1073,7 @@ function loadTasksJson(path) {
|
|
|
937
1073
|
issueMarker: t.issueMarker ?? null,
|
|
938
1074
|
kitUpgrade: t.kitUpgrade ?? false,
|
|
939
1075
|
lineNumber: t.lineNumber ?? 0,
|
|
1076
|
+
source: t.source ?? 'todo',
|
|
940
1077
|
changelog: t.changelog ?? 'added',
|
|
941
1078
|
})));
|
|
942
1079
|
}
|
|
@@ -951,33 +1088,165 @@ async function checkPRWatch(taskId, pipeline, projectDir, agents, deps) {
|
|
|
951
1088
|
const ciPlatform = detectCIPlatform(projectDir);
|
|
952
1089
|
if (ciPlatform === 'none' || !pipeline.prState)
|
|
953
1090
|
return 'failed';
|
|
954
|
-
const branch =
|
|
1091
|
+
const branch = pipeline.branchSlug;
|
|
955
1092
|
try {
|
|
956
1093
|
const prStatus = getPRStatus(projectDir, ciPlatform, branch);
|
|
957
1094
|
if (prStatus.status === 'merged')
|
|
958
1095
|
return 'merged';
|
|
959
|
-
|
|
1096
|
+
// PR was closed (not merged) — try local merge as fallback before giving up
|
|
1097
|
+
if (prStatus.status === 'closed') {
|
|
1098
|
+
logger.warn(`[${taskId}] PR was closed without merging — attempting local merge fallback`);
|
|
1099
|
+
syncMain(projectDir);
|
|
1100
|
+
const stashed = cleanMainForMerge(projectDir);
|
|
1101
|
+
const localMerge = mergeToMain(projectDir, pipeline.branchSlug);
|
|
1102
|
+
if (localMerge.success) {
|
|
1103
|
+
logger.info(`[${taskId}] Local merge succeeded after closed PR (${localMerge.sha?.slice(0, 7)})`);
|
|
1104
|
+
if (stashed)
|
|
1105
|
+
popStash(projectDir);
|
|
1106
|
+
return 'merged';
|
|
1107
|
+
}
|
|
1108
|
+
if (stashed)
|
|
1109
|
+
popStash(projectDir);
|
|
1110
|
+
logger.error(`[${taskId}] Local merge also failed after closed PR: ${localMerge.error}`);
|
|
960
1111
|
return 'failed';
|
|
1112
|
+
}
|
|
961
1113
|
// Ready to merge
|
|
962
1114
|
if (prStatus.mergeable) {
|
|
963
1115
|
logger.info(`[${taskId}] PR mergeable — merging`);
|
|
964
|
-
const
|
|
965
|
-
|
|
1116
|
+
const mergeResult = mergePR(projectDir, ciPlatform, prStatus.number);
|
|
1117
|
+
if (mergeResult.success)
|
|
1118
|
+
return 'merged';
|
|
1119
|
+
// Platform merge failed — diagnose and try to recover
|
|
1120
|
+
logger.warn(`[${taskId}] Platform merge failed: ${mergeResult.error} — attempting recovery`);
|
|
1121
|
+
// Step 1: Try rebase onto main and force-push
|
|
1122
|
+
const rebaseResult = rebaseOnMain(pipeline.worktreePath, projectDir);
|
|
1123
|
+
if (rebaseResult.success) {
|
|
1124
|
+
logger.info(`[${taskId}] Rebased successfully — force-pushing`);
|
|
1125
|
+
forcePushWorktree(pipeline.worktreePath);
|
|
1126
|
+
return 'rework'; // Will re-check PR status next iteration
|
|
1127
|
+
}
|
|
1128
|
+
// Step 2: Rebase failed — delegate conflict resolution to agent
|
|
1129
|
+
logger.warn(`[${taskId}] Rebase conflict — delegating to agent for resolution`);
|
|
1130
|
+
const slot = pool.idleSlot();
|
|
1131
|
+
if (slot) {
|
|
1132
|
+
// Sync main and get conflict info
|
|
1133
|
+
syncMain(projectDir);
|
|
1134
|
+
const conflictFiles = rebaseResult.error ?? 'unknown conflict';
|
|
1135
|
+
const task = {
|
|
1136
|
+
id: taskId, title: `Resolve merge conflict for ${taskId}`, phase: state.currentPhase,
|
|
1137
|
+
type: 'fullstack', complexity: 'S', dependsOn: [], issueMarker: taskId,
|
|
1138
|
+
kitUpgrade: false, lineNumber: 0, source: 'todo', status: 'in-progress', changelog: 'fixed',
|
|
1139
|
+
};
|
|
1140
|
+
// Fetch the main branch into the worktree so agent can see what changed
|
|
1141
|
+
try {
|
|
1142
|
+
execFileSync('git', ['fetch', 'origin', getMainBranch(projectDir)], { cwd: pipeline.worktreePath, timeout: 60_000, stdio: 'pipe' });
|
|
1143
|
+
}
|
|
1144
|
+
catch { /* best-effort */ }
|
|
1145
|
+
const fixResult = await spawnAgent(pool, slot.id, {
|
|
1146
|
+
agent: pipeline.assignedAgent ?? undefined,
|
|
1147
|
+
cwd: pipeline.worktreePath,
|
|
1148
|
+
prompt: [
|
|
1149
|
+
`The branch "${pipeline.branchSlug}" has merge conflicts with main that must be resolved.`,
|
|
1150
|
+
`Conflict info: ${conflictFiles}`,
|
|
1151
|
+
'',
|
|
1152
|
+
'Steps:',
|
|
1153
|
+
`1. Run: git rebase origin/${getMainBranch(projectDir)}`,
|
|
1154
|
+
'2. Resolve ALL conflicts in the reported files',
|
|
1155
|
+
'3. Stage resolved files with: git add <file>',
|
|
1156
|
+
'4. Continue rebase with: git rebase --continue',
|
|
1157
|
+
'5. If rebase is too complex, abort with: git rebase --abort, then merge main into the branch instead',
|
|
1158
|
+
'',
|
|
1159
|
+
'Do NOT commit separately — the rebase/merge handles commits.',
|
|
1160
|
+
].join('\n'),
|
|
1161
|
+
model: getAgentModel(pipeline.assignedAgent, agents, task),
|
|
1162
|
+
}, state, taskId, logger, onMessage, onSpawnStart, onSpawnEnd);
|
|
1163
|
+
if (fixResult.success) {
|
|
1164
|
+
forcePushWorktree(pipeline.worktreePath);
|
|
1165
|
+
logger.info(`[${taskId}] Agent resolved conflicts — force-pushed, will re-check next iteration`);
|
|
1166
|
+
return 'rework';
|
|
1167
|
+
}
|
|
1168
|
+
logger.warn(`[${taskId}] Agent failed to resolve conflicts`);
|
|
1169
|
+
}
|
|
1170
|
+
else {
|
|
1171
|
+
logger.info(`[${taskId}] No idle worker for conflict resolution — will retry next iteration`);
|
|
1172
|
+
return 'waiting';
|
|
1173
|
+
}
|
|
1174
|
+
// Step 3: Agent failed — fall back to local merge
|
|
1175
|
+
logger.warn(`[${taskId}] Falling back to local merge`);
|
|
1176
|
+
syncMain(projectDir);
|
|
1177
|
+
const stashed = cleanMainForMerge(projectDir);
|
|
1178
|
+
const localMerge = mergeToMain(projectDir, pipeline.branchSlug);
|
|
1179
|
+
if (localMerge.success) {
|
|
1180
|
+
logger.info(`[${taskId}] Local merge succeeded (${localMerge.sha?.slice(0, 7)}) — closing PR`);
|
|
1181
|
+
if (stashed)
|
|
1182
|
+
popStash(projectDir);
|
|
1183
|
+
closePR(projectDir, ciPlatform, prStatus.number);
|
|
1184
|
+
return 'merged';
|
|
1185
|
+
}
|
|
1186
|
+
if (stashed)
|
|
1187
|
+
popStash(projectDir);
|
|
1188
|
+
logger.error(`[${taskId}] All merge strategies exhausted: ${localMerge.error}`);
|
|
1189
|
+
return 'failed';
|
|
966
1190
|
}
|
|
967
1191
|
// CI pending — keep waiting
|
|
968
1192
|
if (prStatus.ciStatus === 'pending' || prStatus.ciStatus === 'not-found') {
|
|
969
1193
|
logger.info(`[${taskId}] CI: ${prStatus.ciStatus} — waiting`);
|
|
970
1194
|
return 'waiting';
|
|
971
1195
|
}
|
|
972
|
-
// CI
|
|
1196
|
+
// CI canceled — force-push to re-trigger pipeline
|
|
1197
|
+
if (prStatus.ciStatus === 'canceled') {
|
|
1198
|
+
logger.warn(`[${taskId}] CI was canceled — force-pushing to re-trigger pipeline`);
|
|
1199
|
+
forcePushWorktree(pipeline.worktreePath);
|
|
1200
|
+
return 'rework';
|
|
1201
|
+
}
|
|
1202
|
+
// CI passed but not mergeable — waiting for approval (with timeout)
|
|
973
1203
|
if (prStatus.ciStatus === 'passed' && !prStatus.mergeable) {
|
|
974
|
-
|
|
975
|
-
|
|
1204
|
+
if (!pipeline.approvalWaitingSince) {
|
|
1205
|
+
pipeline.approvalWaitingSince = Date.now();
|
|
1206
|
+
}
|
|
1207
|
+
const waitingMs = Date.now() - pipeline.approvalWaitingSince;
|
|
1208
|
+
const APPROVAL_TIMEOUT = 30 * 60_000; // 30 minutes
|
|
1209
|
+
if (waitingMs < APPROVAL_TIMEOUT) {
|
|
1210
|
+
const waitMin = Math.round(waitingMs / 60_000);
|
|
1211
|
+
logger.info(`[${taskId}] CI passed, waiting for approval (${waitMin}min/${APPROVAL_TIMEOUT / 60_000}min)`);
|
|
1212
|
+
return 'waiting';
|
|
1213
|
+
}
|
|
1214
|
+
// Timeout reached — fall back to local merge (solo mode behavior)
|
|
1215
|
+
logger.warn(`[${taskId}] Approval timeout (${APPROVAL_TIMEOUT / 60_000}min) — falling back to local merge`);
|
|
1216
|
+
syncMain(projectDir);
|
|
1217
|
+
const stashed = cleanMainForMerge(projectDir);
|
|
1218
|
+
const localMerge = mergeToMain(projectDir, pipeline.branchSlug);
|
|
1219
|
+
if (localMerge.success) {
|
|
1220
|
+
logger.info(`[${taskId}] Local merge succeeded (${localMerge.sha?.slice(0, 7)}) — closing PR`);
|
|
1221
|
+
if (stashed)
|
|
1222
|
+
popStash(projectDir);
|
|
1223
|
+
closePR(projectDir, ciPlatform, prStatus.number);
|
|
1224
|
+
return 'merged';
|
|
1225
|
+
}
|
|
1226
|
+
if (stashed)
|
|
1227
|
+
popStash(projectDir);
|
|
1228
|
+
logger.error(`[${taskId}] Local merge failed after approval timeout: ${localMerge.error}`);
|
|
1229
|
+
return 'failed';
|
|
976
1230
|
}
|
|
1231
|
+
// Reset approval timer if CI status changed from passed
|
|
1232
|
+
pipeline.approvalWaitingSince = null;
|
|
977
1233
|
// CI failed — delegate fix to implementing agent
|
|
978
1234
|
if (prStatus.ciStatus === 'failed') {
|
|
979
1235
|
if (pipeline.ciFixes >= MAX_CI_FIXES) {
|
|
980
|
-
logger.
|
|
1236
|
+
logger.warn(`[${taskId}] CI fix limit (${MAX_CI_FIXES}) reached — falling back to local merge`);
|
|
1237
|
+
syncMain(projectDir);
|
|
1238
|
+
const stashed = cleanMainForMerge(projectDir);
|
|
1239
|
+
const localMerge = mergeToMain(projectDir, pipeline.branchSlug);
|
|
1240
|
+
if (localMerge.success) {
|
|
1241
|
+
logger.info(`[${taskId}] Local merge succeeded after CI fix limit (${localMerge.sha?.slice(0, 7)}) — closing PR`);
|
|
1242
|
+
if (stashed)
|
|
1243
|
+
popStash(projectDir);
|
|
1244
|
+
closePR(projectDir, ciPlatform, prStatus.number);
|
|
1245
|
+
return 'merged';
|
|
1246
|
+
}
|
|
1247
|
+
if (stashed)
|
|
1248
|
+
popStash(projectDir);
|
|
1249
|
+
logger.error(`[${taskId}] Local merge also failed: ${localMerge.error}`);
|
|
981
1250
|
return 'failed';
|
|
982
1251
|
}
|
|
983
1252
|
pipeline.ciFixes++;
|
|
@@ -993,7 +1262,7 @@ async function checkPRWatch(taskId, pipeline, projectDir, agents, deps) {
|
|
|
993
1262
|
const task = {
|
|
994
1263
|
id: taskId, title: `Fix CI for ${taskId}`, phase: state.currentPhase,
|
|
995
1264
|
type: 'fullstack', complexity: 'S', dependsOn: [], issueMarker: taskId,
|
|
996
|
-
kitUpgrade: false, lineNumber: 0, status: 'in-progress', changelog: 'fixed',
|
|
1265
|
+
kitUpgrade: false, lineNumber: 0, source: 'todo', status: 'in-progress', changelog: 'fixed',
|
|
997
1266
|
};
|
|
998
1267
|
const fixResult = await spawnAgent(pool, slot.id, {
|
|
999
1268
|
agent: pipeline.assignedAgent ?? undefined,
|
|
@@ -1025,7 +1294,7 @@ async function checkPRWatch(taskId, pipeline, projectDir, agents, deps) {
|
|
|
1025
1294
|
const task = {
|
|
1026
1295
|
id: taskId, title: `Address PR comments for ${taskId}`, phase: state.currentPhase,
|
|
1027
1296
|
type: 'fullstack', complexity: 'S', dependsOn: [], issueMarker: taskId,
|
|
1028
|
-
kitUpgrade: false, lineNumber: 0, status: 'in-progress', changelog: 'fixed',
|
|
1297
|
+
kitUpgrade: false, lineNumber: 0, source: 'todo', status: 'in-progress', changelog: 'fixed',
|
|
1029
1298
|
};
|
|
1030
1299
|
const result = await spawnAgent(pool, slot.id, {
|
|
1031
1300
|
agent: pipeline.assignedAgent ?? undefined,
|
|
@@ -1063,7 +1332,7 @@ async function pollAndMergePR(task, pipeline, branch, platform, projectDir, work
|
|
|
1063
1332
|
// Ready to merge — CI passed and mergeable
|
|
1064
1333
|
if (prStatus.mergeable) {
|
|
1065
1334
|
logger.info(`[${task.id}] PR mergeable — merging`);
|
|
1066
|
-
return mergePR(projectDir, platform, prStatus.number);
|
|
1335
|
+
return mergePR(projectDir, platform, prStatus.number).success;
|
|
1067
1336
|
}
|
|
1068
1337
|
// CI still running or not started — poll
|
|
1069
1338
|
if (prStatus.ciStatus === 'pending' || prStatus.ciStatus === 'not-found') {
|
|
@@ -1180,9 +1449,6 @@ export function extractIssueFromBranch(branch) {
|
|
|
1180
1449
|
const match = branch.match(/#?(\d+)/);
|
|
1181
1450
|
return match ? parseInt(match[1], 10) : null;
|
|
1182
1451
|
}
|
|
1183
|
-
function getCurrentBranchFromProject(projectDir) {
|
|
1184
|
-
return getMainBranch(projectDir);
|
|
1185
|
-
}
|
|
1186
1452
|
function sleep(ms) {
|
|
1187
1453
|
return new Promise(resolve => setTimeout(resolve, ms));
|
|
1188
1454
|
}
|
|
@@ -56,6 +56,7 @@ export function issueToTask(issue, fallbackPhase) {
|
|
|
56
56
|
issueMarker: `#${issue.issueNumber}`,
|
|
57
57
|
kitUpgrade: issue.labels.some(l => l.toLowerCase().includes('kit-upgrade')),
|
|
58
58
|
lineNumber: 0, // not applicable for platform issues
|
|
59
|
+
source: 'platform',
|
|
59
60
|
status,
|
|
60
61
|
changelog: inferChangelog(issue.title, issue.labels),
|
|
61
62
|
};
|
|
@@ -111,6 +111,7 @@ export function parseTodoMd(content) {
|
|
|
111
111
|
issueMarker: pendingTask.issueMarker,
|
|
112
112
|
kitUpgrade: meta.kitUpgrade,
|
|
113
113
|
lineNumber: pendingTask.lineNumber,
|
|
114
|
+
source: 'todo',
|
|
114
115
|
changelog: inferChangelogCategory(title, pendingTask.status),
|
|
115
116
|
});
|
|
116
117
|
pendingTask = null;
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
// ─── report_issue MCP tool for agents ───
|
|
2
|
+
// Allows agents to report discovered issues outside their current task scope.
|
|
3
|
+
// Handler runs in-process in the scheduler — records locally + creates platform issues.
|
|
4
|
+
import { appendFileSync } from 'node:fs';
|
|
5
|
+
import { resolve } from 'node:path';
|
|
6
|
+
import { tool } from '@anthropic-ai/claude-agent-sdk';
|
|
7
|
+
import { z } from 'zod/v4';
|
|
8
|
+
import { createPlatformIssue } from '../git/issue-creator.mjs';
|
|
9
|
+
// ─── In-memory store ───
|
|
10
|
+
export class DiscoveredIssueStore {
|
|
11
|
+
issues = [];
|
|
12
|
+
add(issue) {
|
|
13
|
+
this.issues.push(issue);
|
|
14
|
+
}
|
|
15
|
+
/** Returns all issues and clears the store. */
|
|
16
|
+
drain() {
|
|
17
|
+
return this.issues.splice(0);
|
|
18
|
+
}
|
|
19
|
+
/** Read without clearing. */
|
|
20
|
+
peek() {
|
|
21
|
+
return this.issues;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
// ─── Tool definition factory ───
|
|
25
|
+
const DISCOVERED_NDJSON = '.claude/scheduler/discovered.ndjson';
|
|
26
|
+
export function buildReportIssueTool(store, currentTaskId, projectDir, platform) {
|
|
27
|
+
return tool('report_issue', 'Report a discovered issue outside your current task scope. Use severity "non-blocker" for issues that don\'t prevent your work (then continue normally), or "blocker" for issues that prevent you from completing your task (then wrap up what you can and return).', {
|
|
28
|
+
title: z.string().describe('Concise issue title'),
|
|
29
|
+
type: z.enum(['frontend', 'backend', 'fullstack']).describe('Affected layer'),
|
|
30
|
+
complexity: z.enum(['S', 'M', 'L']).default('M').describe('Estimated complexity'),
|
|
31
|
+
severity: z.enum(['blocker', 'non-blocker']).describe('Whether this blocks your current task'),
|
|
32
|
+
description: z.string().describe('What is broken, where, and what the fix should look like'),
|
|
33
|
+
evidence: z.string().default('').describe('Error messages, file paths, stack traces'),
|
|
34
|
+
}, async (args) => {
|
|
35
|
+
const id = `discovered-${Date.now()}-${Math.random().toString(36).slice(2, 6)}`;
|
|
36
|
+
const issue = {
|
|
37
|
+
id,
|
|
38
|
+
title: args.title,
|
|
39
|
+
type: args.type,
|
|
40
|
+
complexity: args.complexity,
|
|
41
|
+
severity: args.severity,
|
|
42
|
+
description: args.description,
|
|
43
|
+
evidence: args.evidence,
|
|
44
|
+
reportedBy: currentTaskId,
|
|
45
|
+
timestamp: Date.now(),
|
|
46
|
+
};
|
|
47
|
+
// Record in memory (scheduler drains on next iteration)
|
|
48
|
+
store.add(issue);
|
|
49
|
+
// Persist for crash recovery
|
|
50
|
+
try {
|
|
51
|
+
const ndjsonPath = resolve(projectDir, DISCOVERED_NDJSON);
|
|
52
|
+
appendFileSync(ndjsonPath, JSON.stringify(issue) + '\n', 'utf-8');
|
|
53
|
+
}
|
|
54
|
+
catch { /* best-effort — in-memory store is primary */ }
|
|
55
|
+
// Create platform issue if available
|
|
56
|
+
if (platform !== 'none') {
|
|
57
|
+
try {
|
|
58
|
+
const labels = [`discovered`, `type::${args.type}`, `complexity::${args.complexity}`, 'status::todo'];
|
|
59
|
+
const created = createPlatformIssue(projectDir, platform, {
|
|
60
|
+
title: args.title,
|
|
61
|
+
body: `**Discovered during:** ${currentTaskId}\n**Severity:** ${args.severity}\n\n${args.description}\n\n**Evidence:**\n\`\`\`\n${args.evidence}\n\`\`\``,
|
|
62
|
+
labels,
|
|
63
|
+
});
|
|
64
|
+
if (created) {
|
|
65
|
+
issue.platformIssue = created;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
catch { /* non-fatal */ }
|
|
69
|
+
}
|
|
70
|
+
const platformNote = issue.platformIssue
|
|
71
|
+
? ` Platform issue: ${issue.platformIssue.url}`
|
|
72
|
+
: '';
|
|
73
|
+
return {
|
|
74
|
+
content: [{ type: 'text', text: `Issue recorded: ${args.title} (${id}).${platformNote}` }],
|
|
75
|
+
};
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
// ─── Convert discovered issue to Task ───
|
|
79
|
+
export function discoveredIssueToTask(issue, currentPhase) {
|
|
80
|
+
return {
|
|
81
|
+
id: issue.id,
|
|
82
|
+
title: issue.title,
|
|
83
|
+
phase: currentPhase,
|
|
84
|
+
type: issue.type,
|
|
85
|
+
complexity: issue.complexity,
|
|
86
|
+
dependsOn: [],
|
|
87
|
+
issueMarker: issue.platformIssue ? `#${issue.platformIssue.number}` : null,
|
|
88
|
+
kitUpgrade: false,
|
|
89
|
+
lineNumber: 0,
|
|
90
|
+
source: 'discovered',
|
|
91
|
+
status: 'todo',
|
|
92
|
+
changelog: 'added',
|
|
93
|
+
};
|
|
94
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// ─── In-process MCP server with scheduler-provided tools ───
|
|
2
|
+
// Creates a per-agent MCP server instance injected into query() options.
|
|
3
|
+
// Each agent gets its own server (currentTaskId differs per spawn).
|
|
4
|
+
import { createSdkMcpServer } from '@anthropic-ai/claude-agent-sdk';
|
|
5
|
+
import { buildReportIssueTool } from './report-issue.mjs';
|
|
6
|
+
/**
|
|
7
|
+
* Create an in-process MCP server with scheduler tools for a specific agent.
|
|
8
|
+
* Returns a config that can be passed to query() via options.mcpServers.
|
|
9
|
+
*/
|
|
10
|
+
export function createSchedulerToolServer(store, currentTaskId, projectDir, platform) {
|
|
11
|
+
const reportIssueTool = buildReportIssueTool(store, currentTaskId, projectDir, platform);
|
|
12
|
+
return createSdkMcpServer({
|
|
13
|
+
name: 'scheduler-tools',
|
|
14
|
+
version: '1.0.0',
|
|
15
|
+
tools: [reportIssueTool],
|
|
16
|
+
});
|
|
17
|
+
}
|
|
@@ -290,8 +290,7 @@ export class TUI {
|
|
|
290
290
|
return '';
|
|
291
291
|
const cur = this.state.agents[this.state.agents.length - 1];
|
|
292
292
|
const col = ANSI_COLORS[agentColor(cur)] || '';
|
|
293
|
-
|
|
294
|
-
return `${col}${name.padEnd(25)}${RESET}`;
|
|
293
|
+
return `${col}${cur}${RESET} `;
|
|
295
294
|
}
|
|
296
295
|
// ─── Public API ───
|
|
297
296
|
banner() {
|
|
@@ -304,10 +303,10 @@ export class TUI {
|
|
|
304
303
|
}
|
|
305
304
|
this.log('');
|
|
306
305
|
}
|
|
307
|
-
info(msg) { this.log(` ${
|
|
308
|
-
warn(msg) { this.log(` ${
|
|
309
|
-
error(msg) { this.log(` ${
|
|
310
|
-
success(msg) { this.log(` ${
|
|
306
|
+
info(msg) { this.log(` ${ANSI_COLORS.gray}${ts()}${RESET} ${this.prefix()}${ANSI_COLORS.blue}ℹ${RESET} ${msg}`); }
|
|
307
|
+
warn(msg) { this.log(` ${ANSI_COLORS.gray}${ts()}${RESET} ${this.prefix()}${ANSI_COLORS.yellow}⚠ ${msg}${RESET}`); }
|
|
308
|
+
error(msg) { this.log(` ${ANSI_COLORS.gray}${ts()}${RESET} ${this.prefix()}${ANSI_COLORS.red}✗ ${msg}${RESET}`); }
|
|
309
|
+
success(msg) { this.log(` ${ANSI_COLORS.gray}${ts()}${RESET} ${this.prefix()}${ANSI_COLORS.green}✓ ${msg}${RESET}`); }
|
|
311
310
|
setIteration(i, max) {
|
|
312
311
|
this.state.iteration = i;
|
|
313
312
|
this.state.maxIter = max;
|
|
@@ -363,7 +362,7 @@ export class TUI {
|
|
|
363
362
|
}
|
|
364
363
|
for (const block of content) {
|
|
365
364
|
if (block.type === 'text' && block.text?.trim()) {
|
|
366
|
-
this.log(` ${this.prefix()}${block.text}`, `TEXT: ${block.text}`);
|
|
365
|
+
this.log(` ${ANSI_COLORS.gray}${ts()}${RESET} ${this.prefix()}${block.text}`, `TEXT: ${block.text}`);
|
|
367
366
|
}
|
|
368
367
|
if (block.type === 'tool_use')
|
|
369
368
|
this.onToolUse(block);
|
|
@@ -383,7 +382,7 @@ export class TUI {
|
|
|
383
382
|
const col = ANSI_COLORS[agentColor(type)] || '';
|
|
384
383
|
// Don't push to agent stack — this is a sub-agent inside the SDK session.
|
|
385
384
|
// Our external spawn lifecycle is managed by onSpawnStart/onSpawnEnd callbacks.
|
|
386
|
-
this.log(` ${
|
|
385
|
+
this.log(` ${time} ${pre}${icon} ${col}${BOLD}${type}${RESET}${model ? ` ${ANSI_COLORS.gray}(${model})${RESET}` : ''} ${ANSI_COLORS.gray}${desc}${RESET}`, `AGENT: ${type} ${model} — ${desc}`);
|
|
387
386
|
return;
|
|
388
387
|
}
|
|
389
388
|
let detail;
|
|
@@ -406,7 +405,7 @@ export class TUI {
|
|
|
406
405
|
break;
|
|
407
406
|
default: detail = `${ANSI_COLORS.gray}${JSON.stringify(input)}${RESET}`;
|
|
408
407
|
}
|
|
409
|
-
this.log(` ${
|
|
408
|
+
this.log(` ${time} ${pre}${icon} ${BOLD}${name}${RESET} ${detail}`, `TOOL: ${name} ${JSON.stringify(input)}`);
|
|
410
409
|
}
|
|
411
410
|
onToolResult(msg) {
|
|
412
411
|
const content = msg.message?.content;
|
|
@@ -419,15 +418,16 @@ export class TUI {
|
|
|
419
418
|
if (!output)
|
|
420
419
|
continue;
|
|
421
420
|
const pre = this.prefix();
|
|
421
|
+
const time = `${ANSI_COLORS.gray}${ts()}${RESET}`;
|
|
422
422
|
if (block.is_error) {
|
|
423
|
-
this.log(` ${pre}
|
|
423
|
+
this.log(` ${time} ${pre}${ANSI_COLORS.red}✗ ${output}${RESET}`, `ERROR: ${output}`);
|
|
424
424
|
}
|
|
425
425
|
else {
|
|
426
426
|
const lines = output.split('\n');
|
|
427
427
|
const summary = lines.length > 3
|
|
428
428
|
? `${lines.length} lines`
|
|
429
429
|
: output.replace(/\n/g, ' ').trim();
|
|
430
|
-
this.log(` ${pre}
|
|
430
|
+
this.log(` ${time} ${pre}${ANSI_COLORS.green}✓${RESET} ${DIM}${summary}${RESET}`, `OK: ${summary}`);
|
|
431
431
|
}
|
|
432
432
|
}
|
|
433
433
|
}
|
|
@@ -456,7 +456,7 @@ export class TUI {
|
|
|
456
456
|
return;
|
|
457
457
|
}
|
|
458
458
|
if (msg.subtype === 'task_progress' && msg.description) {
|
|
459
|
-
this.log(` ${this.prefix()}
|
|
459
|
+
this.log(` ${ANSI_COLORS.gray}${ts()}${RESET} ${this.prefix()}${DIM}${msg.description}${RESET}`, `PROGRESS: ${msg.description}`);
|
|
460
460
|
return;
|
|
461
461
|
}
|
|
462
462
|
if (msg.subtype === 'api_retry') {
|
|
@@ -561,4 +561,8 @@ When reviewing Angular code, check:
|
|
|
561
561
|
- @defer for heavy below-fold components
|
|
562
562
|
- Naming: PascalCase components, lib- selectors, kebab-case files
|
|
563
563
|
- i18n: ALL user-facing text has `i18n` attribute (templates) or `$localize` (TS) — no bare text
|
|
564
|
-
- VRT: new pages/routes have corresponding `*.vrt.spec.ts` with 3-viewport coverage (375px, 768px, 1280px), baseline snapshots committed
|
|
564
|
+
- VRT: new pages/routes have corresponding `*.vrt.spec.ts` with 3-viewport coverage (375px, 768px, 1280px), baseline snapshots committed
|
|
565
|
+
|
|
566
|
+
## Out-of-Scope Issues
|
|
567
|
+
|
|
568
|
+
If you discover problems outside your current task (broken infrastructure, misconfigurations, deprecated APIs in other code), use the `report_issue` tool to create a tracked issue. Set `severity: 'non-blocker'` and continue your work, or `severity: 'blocker'` if the problem prevents you from completing your task — in that case, finish what you can and return. Never dismiss problems as "pre-existing."
|
|
@@ -136,3 +136,7 @@ When called for **direct implementation**, provide complete production-ready cod
|
|
|
136
136
|
- Explain architectural decisions and trade-offs
|
|
137
137
|
- Proactively identify potential issues
|
|
138
138
|
- No fluff — every word serves a purpose
|
|
139
|
+
|
|
140
|
+
## Out-of-Scope Issues
|
|
141
|
+
|
|
142
|
+
If you discover problems outside your current task (broken infrastructure, misconfigurations, deprecated APIs in other code), use the `report_issue` tool to create a tracked issue. Set `severity: 'non-blocker'` and continue your work, or `severity: 'blocker'` if the problem prevents you from completing your task — in that case, finish what you can and return. Never dismiss problems as "pre-existing."
|
|
@@ -870,6 +870,19 @@ If a merge conflict occurs:
|
|
|
870
870
|
4. Push: `git push origin HEAD`
|
|
871
871
|
5. Then clean up: `git worktree remove .worktrees/feat/{slug} && git branch -d feat/{slug}`
|
|
872
872
|
|
|
873
|
+
## Proactive Problem Resolution
|
|
874
|
+
|
|
875
|
+
All agents have access to a `report_issue` tool provided by the scheduler. When an agent encounters a problem outside its current task scope, it calls this tool to create a tracked issue. The scheduler picks it up on the next iteration and routes it through the normal pipeline.
|
|
876
|
+
|
|
877
|
+
**Agent behavior:**
|
|
878
|
+
- **In-scope problem** → fix it directly (it's the agent's job)
|
|
879
|
+
- **Out-of-scope, non-blocking** → call `report_issue` with `severity: 'non-blocker'`, then continue working
|
|
880
|
+
- **Out-of-scope, blocking** → call `report_issue` with `severity: 'blocker'`, finish what's possible, then return
|
|
881
|
+
- **Warnings are actionable** — unless they originate from inside a third-party package's own source code
|
|
882
|
+
- **Never dismiss** — "pre-existing" and "not in scope" are not valid reasons to ignore a problem
|
|
883
|
+
|
|
884
|
+
**Orchestrator does NOT process discovered issues.** The scheduler handles everything — creating tasks from `report_issue` calls, creating platform issues, and routing them to the right agent via the normal task queue. The orchestrator just picks tasks and delegates as usual.
|
|
885
|
+
|
|
873
886
|
## When Stuck
|
|
874
887
|
|
|
875
888
|
NEVER stay stuck. Escalation order:
|
|
@@ -393,3 +393,7 @@ When reviewing React code, check:
|
|
|
393
393
|
- No unstable nested component definitions (components defined inside render)
|
|
394
394
|
- `React.memo` only on components with measured re-render cost — not as a default
|
|
395
395
|
- VRT: new pages/routes have corresponding `*.vrt.spec.ts` with 3-viewport coverage (375px, 768px, 1280px), baseline snapshots committed
|
|
396
|
+
|
|
397
|
+
## Out-of-Scope Issues
|
|
398
|
+
|
|
399
|
+
If you discover problems outside your current task (broken infrastructure, misconfigurations, deprecated APIs in other code), use the `report_issue` tool to create a tracked issue. Set `severity: 'non-blocker'` and continue your work, or `severity: 'blocker'` if the problem prevents you from completing your task — in that case, finish what you can and return. Never dismiss problems as "pre-existing."
|
|
@@ -195,3 +195,7 @@ What is done well. Correct patterns, clean code, good decisions. Be specific.
|
|
|
195
195
|
4. **Prioritize impact** — security > correctness > performance > style
|
|
196
196
|
5. **Respect project standards** — align with CLAUDE.md conventions
|
|
197
197
|
6. **Don't offer to fix** — this is not interactive, the user cannot respond
|
|
198
|
+
|
|
199
|
+
## Out-of-Scope Issues
|
|
200
|
+
|
|
201
|
+
If you discover problems outside your current task (broken infrastructure, misconfigurations, deprecated APIs in other code), use the `report_issue` tool to create a tracked issue. Set `severity: 'non-blocker'` and continue your work, or `severity: 'blocker'` if the problem prevents you from completing your task — in that case, finish what you can and return. Never dismiss problems as "pre-existing."
|
|
@@ -394,3 +394,7 @@ When reviewing Svelte code, check:
|
|
|
394
394
|
- Callback props for outputs (not `createEventDispatcher` — that's Svelte 4)
|
|
395
395
|
- Resource cleanup in `onDestroy` or `$effect` return for manual subscriptions
|
|
396
396
|
- VRT: new pages/routes have corresponding `*.vrt.spec.ts` with 3-viewport coverage (375px, 768px, 1280px), baseline snapshots committed
|
|
397
|
+
|
|
398
|
+
## Out-of-Scope Issues
|
|
399
|
+
|
|
400
|
+
If you discover problems outside your current task (broken infrastructure, misconfigurations, deprecated APIs in other code), use the `report_issue` tool to create a tracked issue. Set `severity: 'non-blocker'` and continue your work, or `severity: 'blocker'` if the problem prevents you from completing your task — in that case, finish what you can and return. Never dismiss problems as "pre-existing."
|
|
@@ -538,3 +538,7 @@ Tests should be easy to write. If they are not, the production code needs refact
|
|
|
538
538
|
- **No flaky tests** — if a test fails intermittently, fix it or delete it
|
|
539
539
|
- **Test public API** — don't test private methods directly
|
|
540
540
|
- **Pure functions are easy to test** — prefer pure transformations over stateful operations. If a function is hard to test, it likely has too many responsibilities or side effects.
|
|
541
|
+
|
|
542
|
+
## Out-of-Scope Issues
|
|
543
|
+
|
|
544
|
+
If you discover problems outside your current task (broken infrastructure, misconfigurations, deprecated APIs in other code), use the `report_issue` tool to create a tracked issue. Set `severity: 'non-blocker'` and continue your work, or `severity: 'blocker'` if the problem prevents you from completing your task — in that case, finish what you can and return. Never dismiss problems as "pre-existing."
|
|
@@ -144,4 +144,8 @@ When called for **direct implementation**, provide:
|
|
|
144
144
|
- TypeScript interfaces and types
|
|
145
145
|
- Follow project conventions from CLAUDE.md (App Separation Principle, Onion Architecture layers)
|
|
146
146
|
- Monorepo-compatible solutions for shared functionality
|
|
147
|
-
- When uncertain about approach, ask for clarification before proceeding
|
|
147
|
+
- When uncertain about approach, ask for clarification before proceeding
|
|
148
|
+
|
|
149
|
+
## Out-of-Scope Issues
|
|
150
|
+
|
|
151
|
+
If you discover problems outside your current task (broken infrastructure, misconfigurations, deprecated APIs in other code), use the `report_issue` tool to create a tracked issue. Set `severity: 'non-blocker'` and continue your work, or `severity: 'blocker'` if the problem prevents you from completing your task — in that case, finish what you can and return. Never dismiss problems as "pre-existing."
|
|
@@ -423,3 +423,7 @@ When reviewing Vue code, check:
|
|
|
423
423
|
- Naming: PascalCase components, `Base` prefix for atoms, `use` prefix for composables
|
|
424
424
|
- i18n: ALL user-facing text goes through `$t()` / `t()` — no bare strings in templates or script
|
|
425
425
|
- VRT: new pages/routes have corresponding `*.vrt.spec.ts` with 3-viewport coverage (375px, 768px, 1280px), baseline snapshots committed
|
|
426
|
+
|
|
427
|
+
## Out-of-Scope Issues
|
|
428
|
+
|
|
429
|
+
If you discover problems outside your current task (broken infrastructure, misconfigurations, deprecated APIs in other code), use the `report_issue` tool to create a tracked issue. Set `severity: 'non-blocker'` and continue your work, or `severity: 'blocker'` if the problem prevents you from completing your task — in that case, finish what you can and return. Never dismiss problems as "pre-existing."
|