create-claude-workspace 2.1.11 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (29) hide show
  1. package/dist/scheduler/agents/prompt-builder.mjs +21 -0
  2. package/dist/scheduler/git/manager.mjs +78 -0
  3. package/dist/scheduler/git/manager.spec.js +63 -1
  4. package/dist/scheduler/git/pr-manager.mjs +214 -0
  5. package/dist/scheduler/git/pr-manager.spec.js +34 -0
  6. package/dist/scheduler/index.mjs +41 -1
  7. package/dist/scheduler/integration.spec.js +1 -0
  8. package/dist/scheduler/loop.mjs +239 -42
  9. package/dist/scheduler/loop.spec.js +2 -0
  10. package/dist/scheduler/state/state.mjs +1 -0
  11. package/dist/scheduler/state/state.spec.js +1 -0
  12. package/dist/scheduler/tasks/issue-source.mjs +156 -0
  13. package/dist/scheduler/tasks/issue-source.spec.js +104 -0
  14. package/dist/scheduler/types.mjs +1 -0
  15. package/dist/scheduler/util/idle-poll.mjs +27 -2
  16. package/dist/template/.claude/CLAUDE.md +26 -61
  17. package/dist/template/.claude/agents/backend-ts-architect.md +2 -2
  18. package/dist/template/.claude/agents/deployment-engineer.md +3 -3
  19. package/dist/template/.claude/agents/devops-integrator.md +13 -10
  20. package/dist/template/.claude/agents/it-analyst.md +108 -0
  21. package/dist/template/.claude/agents/orchestrator.md +118 -153
  22. package/dist/template/.claude/agents/product-owner.md +5 -9
  23. package/dist/template/.claude/agents/project-initializer.md +297 -342
  24. package/dist/template/.claude/agents/senior-code-reviewer.md +1 -1
  25. package/dist/template/.claude/agents/technical-planner.md +7 -12
  26. package/dist/template/.claude/agents/test-engineer.md +1 -1
  27. package/dist/template/.claude/agents/ui-engineer.md +3 -3
  28. package/dist/template/.claude/templates/claude-md.md +2 -1
  29. package/package.json +1 -1
@@ -178,6 +178,27 @@ export function buildCIFixPrompt(ctx, ciLogs) {
178
178
  `- Run build/lint/tests locally to verify the fix`,
179
179
  ].join('\n');
180
180
  }
181
+ // ─── PR comment resolution prompt ───
182
+ export function buildPRCommentPrompt(ctx, comments) {
183
+ return [
184
+ `Address the following code review comments on your PR/MR.`,
185
+ ``,
186
+ `## Task`,
187
+ `**${ctx.task.title}**`,
188
+ ``,
189
+ `## Working Directory`,
190
+ ctx.worktreePath,
191
+ ``,
192
+ `## Review Comments`,
193
+ comments,
194
+ ``,
195
+ `## Instructions`,
196
+ `- Address each comment by making the requested changes`,
197
+ `- If a comment is incorrect or unnecessary, add a brief explanation in the code`,
198
+ `- Run build/lint/tests locally to verify your changes`,
199
+ `- Do NOT dismiss or ignore valid feedback`,
200
+ ].join('\n');
201
+ }
181
202
  // ─── Phase transition prompt (for product-owner re-evaluation) ───
182
203
  export function buildPhaseTransitionPrompt(completedPhase, nextPhase) {
183
204
  return [
@@ -219,6 +219,84 @@ export function pushTags(projectDir) {
219
219
  return false;
220
220
  }
221
221
  }
222
+ export function rebaseOnMain(worktreePath, projectDir) {
223
+ try {
224
+ const main = getMainBranch(projectDir);
225
+ git(`fetch origin "${main}"`, worktreePath, PUSH_TIMEOUT);
226
+ git(`rebase "origin/${main}"`, worktreePath, PUSH_TIMEOUT);
227
+ return { success: true, conflict: false };
228
+ }
229
+ catch (err) {
230
+ // Check if rebase is in progress (conflict)
231
+ try {
232
+ const status = git('status --porcelain', worktreePath);
233
+ if (status.includes('UU ') || status.includes('AA ') || status.includes('DD ')) {
234
+ git('rebase --abort', worktreePath);
235
+ return { success: false, conflict: true, error: err.message };
236
+ }
237
+ }
238
+ catch { /* ignore */ }
239
+ // Abort any in-progress rebase
240
+ try {
241
+ git('rebase --abort', worktreePath);
242
+ }
243
+ catch { /* ignore */ }
244
+ return { success: false, conflict: false, error: err.message };
245
+ }
246
+ }
247
+ // ─── Stash operations ───
248
+ export function stashChanges(dir) {
249
+ const status = git('status --porcelain', dir);
250
+ if (!status)
251
+ return false;
252
+ try {
253
+ git('stash --include-untracked', dir);
254
+ return true;
255
+ }
256
+ catch {
257
+ return false;
258
+ }
259
+ }
260
+ export function popStash(dir) {
261
+ try {
262
+ git('stash pop', dir);
263
+ return true;
264
+ }
265
+ catch {
266
+ return false;
267
+ }
268
+ }
269
+ // ─── Dirty main evaluation ───
270
+ const TRACKING_FILES = new Set(['TODO.md', 'MEMORY.md']);
271
+ export function evaluateDirtyMain(projectDir) {
272
+ const status = git('status --porcelain', projectDir);
273
+ if (!status)
274
+ return { trackingFiles: [], userFiles: [] };
275
+ const trackingFiles = [];
276
+ const userFiles = [];
277
+ for (const line of status.split('\n')) {
278
+ // Format: "XY filename" where XY is 2-char status
279
+ const file = line.slice(3).trim();
280
+ if (!file)
281
+ continue;
282
+ if (TRACKING_FILES.has(file)) {
283
+ trackingFiles.push(file);
284
+ }
285
+ else {
286
+ userFiles.push(file);
287
+ }
288
+ }
289
+ return { trackingFiles, userFiles };
290
+ }
291
+ export function discardFiles(dir, files) {
292
+ if (files.length === 0)
293
+ return;
294
+ const fileArgs = files.map(f => `"${f}"`).join(' ');
295
+ try {
296
+ git(`checkout -- ${fileArgs}`, dir);
297
+ }
298
+ catch { /* file may not exist in index */ }
299
+ }
222
300
  // ─── Helpers ───
223
301
  function escapeMsg(msg) {
224
302
  return msg.replace(/"/g, '\\"').replace(/\n/g, '\\n');
@@ -4,7 +4,7 @@ import { mkdtempSync, writeFileSync, existsSync } from 'node:fs';
4
4
  import { resolve, join } from 'node:path';
5
5
  import { tmpdir } from 'node:os';
6
6
  import { rmSync } from 'node:fs';
7
- import { createWorktree, cleanupWorktree, listWorktrees, listOrphanedWorktrees, commitInWorktree, getChangedFiles, hasUncommittedChanges, mergeToMain, getMainBranch, getCurrentBranch, branchExists, isBranchMerged, hasGitIdentity, createTag, getLatestTag, } from './manager.mjs';
7
+ import { createWorktree, cleanupWorktree, listWorktrees, listOrphanedWorktrees, commitInWorktree, getChangedFiles, hasUncommittedChanges, mergeToMain, getMainBranch, getCurrentBranch, branchExists, isBranchMerged, hasGitIdentity, createTag, getLatestTag, rebaseOnMain, stashChanges, popStash, evaluateDirtyMain, discardFiles, } from './manager.mjs';
8
8
  let repoDir;
9
9
  function gitCmd(args, cwd) {
10
10
  return execSync(`git ${args}`, { cwd: cwd ?? repoDir, encoding: 'utf-8', stdio: 'pipe' }).trim();
@@ -196,3 +196,65 @@ describe('createTag / getLatestTag', () => {
196
196
  expect(getLatestTag(repoDir)).toBe('v0.2.0');
197
197
  });
198
198
  });
199
+ describe('rebaseOnMain', () => {
200
+ it('rebases worktree onto main successfully', () => {
201
+ // Create worktree with a commit
202
+ const wtPath = createWorktree(repoDir, 'feat/rebase-test');
203
+ writeFileSync(resolve(wtPath, 'feature.ts'), 'export const x = 1;');
204
+ commitInWorktree(wtPath, 'feat: add feature');
205
+ // Add a commit on main to create divergence
206
+ writeFileSync(resolve(repoDir, 'main-change.ts'), 'export const y = 2;');
207
+ gitCmd('add .', repoDir);
208
+ gitCmd('commit -m "main: parallel change"', repoDir);
209
+ const result = rebaseOnMain(wtPath, repoDir);
210
+ // May fail without remote — test the interface
211
+ expect(result).toHaveProperty('success');
212
+ expect(result).toHaveProperty('conflict');
213
+ });
214
+ });
215
+ describe('stashChanges / popStash', () => {
216
+ it('stashes and pops changes', () => {
217
+ writeFileSync(resolve(repoDir, 'dirty.txt'), 'uncommitted');
218
+ expect(stashChanges(repoDir)).toBe(true);
219
+ expect(hasUncommittedChanges(repoDir)).toBe(false);
220
+ expect(popStash(repoDir)).toBe(true);
221
+ expect(hasUncommittedChanges(repoDir)).toBe(true);
222
+ // Cleanup
223
+ gitCmd('checkout -- .', repoDir);
224
+ });
225
+ it('returns false when nothing to stash', () => {
226
+ expect(stashChanges(repoDir)).toBe(false);
227
+ });
228
+ });
229
+ describe('evaluateDirtyMain', () => {
230
+ it('returns empty when clean', () => {
231
+ const result = evaluateDirtyMain(repoDir);
232
+ expect(result.trackingFiles).toEqual([]);
233
+ expect(result.userFiles).toEqual([]);
234
+ });
235
+ it('separates tracking files from user files', () => {
236
+ writeFileSync(resolve(repoDir, 'TODO.md'), '# TODO');
237
+ writeFileSync(resolve(repoDir, 'MEMORY.md'), '# Memory');
238
+ writeFileSync(resolve(repoDir, 'custom.ts'), 'code');
239
+ gitCmd('add .', repoDir);
240
+ const result = evaluateDirtyMain(repoDir);
241
+ expect(result.trackingFiles).toContain('TODO.md');
242
+ expect(result.trackingFiles).toContain('MEMORY.md');
243
+ expect(result.userFiles).toContain('custom.ts');
244
+ // Cleanup
245
+ gitCmd('reset HEAD .', repoDir);
246
+ gitCmd('checkout -- .', repoDir);
247
+ });
248
+ });
249
+ describe('discardFiles', () => {
250
+ it('discards specific files', () => {
251
+ writeFileSync(resolve(repoDir, 'README.md'), '# Modified');
252
+ expect(hasUncommittedChanges(repoDir)).toBe(true);
253
+ discardFiles(repoDir, ['README.md']);
254
+ expect(hasUncommittedChanges(repoDir)).toBe(false);
255
+ });
256
+ it('does nothing for empty file list', () => {
257
+ discardFiles(repoDir, []);
258
+ expect(hasUncommittedChanges(repoDir)).toBe(false);
259
+ });
260
+ });
@@ -0,0 +1,214 @@
1
+ // ─── PR/MR lifecycle management ───
2
+ // Deterministic TS, zero AI tokens. Uses gh/glab CLI.
3
+ import { execSync } from 'node:child_process';
4
+ const READ_TIMEOUT = 30_000;
5
+ const WRITE_TIMEOUT = 60_000;
6
+ function cli(cmd, cwd, timeout = READ_TIMEOUT) {
7
+ return execSync(cmd, { cwd, timeout, stdio: 'pipe', encoding: 'utf-8' }).trim();
8
+ }
9
+ export function createPR(opts) {
10
+ const body = opts.issueNumber
11
+ ? `${opts.body}\n\nCloses #${opts.issueNumber}`
12
+ : opts.body;
13
+ // Escape body for shell — write to temp approach avoided, use stdin-like quoting
14
+ const escapedBody = body.replace(/"/g, '\\"').replace(/\n/g, '\\n');
15
+ const escapedTitle = opts.title.replace(/"/g, '\\"');
16
+ if (opts.platform === 'github') {
17
+ const output = cli(`gh pr create --title "${escapedTitle}" --body "${escapedBody}" --base "${opts.baseBranch}" --head "${opts.branch}" --json number,url`, opts.cwd, WRITE_TIMEOUT);
18
+ const pr = JSON.parse(output);
19
+ return {
20
+ number: pr.number,
21
+ url: pr.url,
22
+ status: 'open',
23
+ ciStatus: 'pending',
24
+ approvals: 0,
25
+ mergeable: false,
26
+ comments: [],
27
+ };
28
+ }
29
+ // GitLab
30
+ const output = cli(`glab mr create --title "${escapedTitle}" --description "${escapedBody}" --target-branch "${opts.baseBranch}" --source-branch "${opts.branch}" --no-editor --json iid,web_url`, opts.cwd, WRITE_TIMEOUT);
31
+ const mr = JSON.parse(output);
32
+ return {
33
+ number: mr.iid,
34
+ url: mr.web_url,
35
+ status: 'open',
36
+ ciStatus: 'pending',
37
+ approvals: 0,
38
+ mergeable: false,
39
+ comments: [],
40
+ };
41
+ }
42
+ // ─── PR status ───
43
+ export function getPRStatus(cwd, platform, branch) {
44
+ if (platform === 'github') {
45
+ return getGitHubPRStatus(cwd, branch);
46
+ }
47
+ return getGitLabMRStatus(cwd, branch);
48
+ }
49
+ function getGitHubPRStatus(cwd, branch) {
50
+ const output = cli(`gh pr view "${branch}" --json number,url,state,mergeable,reviewDecision,statusCheckRollup,comments`, cwd);
51
+ const pr = JSON.parse(output);
52
+ const ciStatus = resolveGitHubCIStatus(pr.statusCheckRollup ?? []);
53
+ const approvals = pr.reviewDecision === 'APPROVED' ? 1 : 0;
54
+ const mergeable = pr.mergeable === 'MERGEABLE' && ciStatus === 'passed';
55
+ return {
56
+ number: pr.number,
57
+ url: pr.url,
58
+ status: pr.state === 'MERGED' ? 'merged' : pr.state === 'CLOSED' ? 'closed' : 'open',
59
+ ciStatus,
60
+ approvals,
61
+ mergeable,
62
+ comments: parseGitHubComments(pr.comments ?? []),
63
+ };
64
+ }
65
+ function resolveGitHubCIStatus(checks) {
66
+ if (checks.length === 0)
67
+ return 'not-found';
68
+ const states = checks.map(c => c.state);
69
+ if (states.every(s => s === 'SUCCESS'))
70
+ return 'passed';
71
+ if (states.some(s => s === 'FAILURE' || s === 'ERROR'))
72
+ return 'failed';
73
+ if (states.some(s => s === 'PENDING' || s === 'EXPECTED'))
74
+ return 'pending';
75
+ return 'passed';
76
+ }
77
+ function parseGitHubComments(comments) {
78
+ return comments.map(c => ({
79
+ id: c.id,
80
+ author: c.author.login,
81
+ body: c.body,
82
+ resolved: false, // GitHub PR comments don't have resolved state (only review threads do)
83
+ }));
84
+ }
85
+ function getGitLabMRStatus(cwd, branch) {
86
+ const output = cli(`glab mr view "${branch}" --json iid,web_url,state,merge_status,head_pipeline,user_notes_count`, cwd);
87
+ const mr = JSON.parse(output);
88
+ const ciStatus = resolveGitLabCIStatus(mr.head_pipeline);
89
+ const mergeable = mr.merge_status === 'can_be_merged' && ciStatus === 'passed';
90
+ return {
91
+ number: mr.iid,
92
+ url: mr.web_url,
93
+ status: mr.state === 'merged' ? 'merged' : mr.state === 'closed' ? 'closed' : 'open',
94
+ ciStatus,
95
+ approvals: 0, // Would need separate API call; scheduler handles via mergeable check
96
+ mergeable,
97
+ comments: [], // Fetched separately via getPRComments
98
+ };
99
+ }
100
+ function resolveGitLabCIStatus(pipeline) {
101
+ if (!pipeline)
102
+ return 'not-found';
103
+ switch (pipeline.status) {
104
+ case 'success': return 'passed';
105
+ case 'failed': return 'failed';
106
+ case 'canceled': return 'canceled';
107
+ default: return 'pending';
108
+ }
109
+ }
110
+ // ─── PR comments ───
111
+ export function getPRComments(cwd, platform, prNumber) {
112
+ if (platform === 'github') {
113
+ return getGitHubPRComments(cwd, prNumber);
114
+ }
115
+ return getGitLabMRComments(cwd, prNumber);
116
+ }
117
+ function getGitHubPRComments(cwd, prNumber) {
118
+ try {
119
+ const output = cli(`gh api repos/{owner}/{repo}/pulls/${prNumber}/reviews --jq '.[] | select(.state != "APPROVED") | {id: .id, user: .user.login, body: .body, state: .state}'`, cwd);
120
+ if (!output)
121
+ return [];
122
+ return output.split('\n').filter(Boolean).map(line => {
123
+ const review = JSON.parse(line);
124
+ return {
125
+ id: review.id,
126
+ author: review.user,
127
+ body: review.body,
128
+ resolved: review.state === 'DISMISSED',
129
+ };
130
+ });
131
+ }
132
+ catch {
133
+ return [];
134
+ }
135
+ }
136
+ function getGitLabMRComments(cwd, mrIid) {
137
+ try {
138
+ const output = cli(`glab api projects/:id/merge_requests/${mrIid}/notes --paginate`, cwd);
139
+ if (!output)
140
+ return [];
141
+ const notes = JSON.parse(output);
142
+ return notes
143
+ .filter(n => n.resolvable)
144
+ .map(n => ({
145
+ id: n.id,
146
+ author: n.author.username,
147
+ body: n.body,
148
+ resolved: n.resolved,
149
+ }));
150
+ }
151
+ catch {
152
+ return [];
153
+ }
154
+ }
155
+ // ─── Merge PR ───
156
+ export function mergePR(cwd, platform, prNumber, method = 'merge') {
157
+ try {
158
+ if (platform === 'github') {
159
+ const flag = method === 'squash' ? '--squash' : method === 'rebase' ? '--rebase' : '--merge';
160
+ cli(`gh pr merge ${prNumber} ${flag} --delete-branch`, cwd, WRITE_TIMEOUT);
161
+ }
162
+ else {
163
+ const flag = method === 'squash' ? '--squash' : '';
164
+ cli(`glab mr merge ${mrIid(prNumber)} ${flag} --remove-source-branch --yes`, cwd, WRITE_TIMEOUT);
165
+ }
166
+ return true;
167
+ }
168
+ catch {
169
+ return false;
170
+ }
171
+ }
172
+ function mrIid(n) {
173
+ return String(n);
174
+ }
175
+ // ─── Issue label management ───
176
+ export function updateIssueLabels(cwd, platform, issueNumber, add, remove) {
177
+ try {
178
+ if (platform === 'github') {
179
+ if (remove.length > 0) {
180
+ for (const label of remove) {
181
+ try {
182
+ cli(`gh issue edit ${issueNumber} --remove-label "${label}"`, cwd);
183
+ }
184
+ catch { /* label may not exist */ }
185
+ }
186
+ }
187
+ if (add.length > 0) {
188
+ cli(`gh issue edit ${issueNumber} --add-label "${add.join(',')}"`, cwd);
189
+ }
190
+ }
191
+ else {
192
+ // GitLab: comma-separated labels
193
+ if (remove.length > 0) {
194
+ cli(`glab issue update ${issueNumber} --unlabel "${remove.join(',')}"`, cwd);
195
+ }
196
+ if (add.length > 0) {
197
+ cli(`glab issue update ${issueNumber} --label "${add.join(',')}"`, cwd);
198
+ }
199
+ }
200
+ }
201
+ catch { /* best-effort label update */ }
202
+ }
203
+ // ─── Close PR ───
204
+ export function closePR(cwd, platform, prNumber) {
205
+ try {
206
+ if (platform === 'github') {
207
+ cli(`gh pr close ${prNumber}`, cwd);
208
+ }
209
+ else {
210
+ cli(`glab mr close ${prNumber}`, cwd);
211
+ }
212
+ }
213
+ catch { /* best-effort */ }
214
+ }
@@ -0,0 +1,34 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ // These tests verify the internal parsing/mapping logic.
3
+ // CLI-dependent functions (createPR, mergePR, etc.) require a real remote
4
+ // and are tested via integration tests.
5
+ // Since the module uses execSync directly, we test the exported types and behavior
6
+ // that can be validated without a real remote. For full coverage, see integration tests.
7
+ describe('PR manager types', () => {
8
+ it('PRInfo has expected shape', async () => {
9
+ // Verify type compatibility
10
+ const info = {
11
+ number: 42,
12
+ url: 'https://github.com/owner/repo/pull/42',
13
+ status: 'open',
14
+ ciStatus: 'passed',
15
+ approvals: 1,
16
+ mergeable: true,
17
+ comments: [],
18
+ };
19
+ expect(info.number).toBe(42);
20
+ expect(info.status).toBe('open');
21
+ });
22
+ it('PRComment has expected shape', async () => {
23
+ const comment = {
24
+ id: 1,
25
+ author: 'reviewer',
26
+ body: 'Fix this',
27
+ resolved: false,
28
+ path: 'src/auth.ts',
29
+ line: 42,
30
+ };
31
+ expect(comment.resolved).toBe(false);
32
+ expect(comment.path).toBe('src/auth.ts');
33
+ });
34
+ });
@@ -5,6 +5,8 @@ import { resolve } from 'node:path';
5
5
  import { existsSync, mkdirSync } from 'node:fs';
6
6
  import { config as dotenvConfig } from '@dotenvx/dotenvx';
7
7
  import { SCHEDULER_DEFAULTS } from './types.mjs';
8
+ import { detectCIPlatform } from './git/ci-watcher.mjs';
9
+ import { fetchOpenIssues } from './tasks/issue-source.mjs';
8
10
  import { emptyState, readState, writeState, appendEvent, createEvent } from './state/state.mjs';
9
11
  import { WorkerPool } from './agents/worker-pool.mjs';
10
12
  import { OrchestratorClient } from './agents/orchestrator.mjs';
@@ -122,6 +124,16 @@ export function parseSchedulerArgs(argv) {
122
124
  i++;
123
125
  continue;
124
126
  }
127
+ if (arg === '--task-mode') {
128
+ const val = strFlag(arg, i);
129
+ if (val !== 'local' && val !== 'platform') {
130
+ console.error(`--task-mode must be 'local' or 'platform'`);
131
+ process.exit(1);
132
+ }
133
+ mutable.taskMode = val;
134
+ i++;
135
+ continue;
136
+ }
125
137
  if (arg.startsWith('--')) {
126
138
  console.error(`Unknown option: ${arg}`);
127
139
  process.exit(1);
@@ -129,6 +141,19 @@ export function parseSchedulerArgs(argv) {
129
141
  }
130
142
  return opts;
131
143
  }
144
+ function detectTaskMode(projectDir) {
145
+ const platform = detectCIPlatform(projectDir);
146
+ if (platform === 'none')
147
+ return 'local';
148
+ try {
149
+ const issues = fetchOpenIssues(projectDir, platform);
150
+ const hasStatusLabels = issues.some(i => i.labels.some(l => l.startsWith('status::')));
151
+ if (hasStatusLabels)
152
+ return 'platform';
153
+ }
154
+ catch { /* CLI not available or no issues */ }
155
+ return 'local';
156
+ }
132
157
  function printSchedulerHelp() {
133
158
  console.log(`
134
159
  Multi-agent scheduler for autonomous development.
@@ -220,6 +245,21 @@ export async function runScheduler(opts) {
220
245
  else {
221
246
  state = emptyState(opts.concurrency);
222
247
  }
248
+ // Task mode detection
249
+ if (opts.taskMode) {
250
+ state.taskMode = opts.taskMode;
251
+ }
252
+ else if (!existing) {
253
+ // Auto-detect: platform mode if remote has issues with status:: labels
254
+ state.taskMode = detectTaskMode(opts.projectDir);
255
+ logger.info(`Task mode auto-detected: ${state.taskMode}`);
256
+ }
257
+ // If resuming existing state, keep the stored taskMode
258
+ // Platform mode: shorter idle poll interval (30s vs 5min)
259
+ const mutableOpts = opts;
260
+ if (state.taskMode === 'platform' && !opts.taskMode && opts.idlePollInterval === SCHEDULER_DEFAULTS.idlePollInterval) {
261
+ mutableOpts.idlePollInterval = 30_000;
262
+ }
223
263
  // Worker pool
224
264
  const pool = new WorkerPool({
225
265
  concurrency: opts.concurrency,
@@ -276,7 +316,7 @@ export async function runScheduler(opts) {
276
316
  logger.info('No work available. Entering idle polling...');
277
317
  const idleStart = Date.now();
278
318
  while (!stopping) {
279
- const poll = await pollForNewWork(opts.projectDir, logger);
319
+ const poll = await pollForNewWork(opts.projectDir, logger, state.taskMode, state.completedTasks);
280
320
  if (poll.hasWork) {
281
321
  logger.info(`New work detected (${poll.source}). Resuming...`);
282
322
  break;
@@ -54,6 +54,7 @@ describe('State + Session integration', () => {
54
54
  ciFixes: 0,
55
55
  buildFixes: 0,
56
56
  assignedAgent: 'backend-ts-architect',
57
+ prState: null,
57
58
  };
58
59
  writeState(testDir, state);
59
60
  const loaded = readState(testDir);