create-claude-workspace 2.3.27 → 2.3.29

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.
@@ -1,5 +1,66 @@
1
1
  // ─── Minimal prompt construction for each pipeline step ───
2
2
  // No workflow instructions. Just task + context + expected output format.
3
+ // ─── Shared helpers ───
4
+ function descriptionBlock(task) {
5
+ if (!task.description)
6
+ return [];
7
+ return [``, `## Issue Description`, task.description];
8
+ }
9
+ // ─── Triage prompts ───
10
+ export function buildPOTriagePrompt(ctx) {
11
+ return [
12
+ `You are a product owner evaluating a feature request.`,
13
+ ``,
14
+ `## Feature Request`,
15
+ `**${ctx.task.title}**`,
16
+ ...descriptionBlock(ctx.task),
17
+ ``,
18
+ `## Working Directory`,
19
+ ctx.worktreePath,
20
+ ``,
21
+ `## Project Root`,
22
+ ctx.projectDir,
23
+ ``,
24
+ `## Instructions`,
25
+ `Read PRODUCT.md (if available) and the codebase to understand the product context.`,
26
+ `Evaluate this feature request and respond with a structured analysis.`,
27
+ ``,
28
+ `## Required Output Format`,
29
+ `Respond with JSON:`,
30
+ '```json',
31
+ `{`,
32
+ ` "decision": "accept" | "reject",`,
33
+ ` "risks": ["risk 1", "risk 2"],`,
34
+ ` "scope": "Your refined scope description — what exactly should be implemented",`,
35
+ ` "priority": "critical" | "high" | "medium" | "low",`,
36
+ ` "reason": "Brief explanation of your decision",`,
37
+ ` "suggestedType": "frontend" | "backend" | "fullstack",`,
38
+ ` "suggestedComplexity": "S" | "M" | "L"`,
39
+ `}`,
40
+ '```',
41
+ ``,
42
+ `## Decision Criteria`,
43
+ `- ACCEPT if: aligns with product vision, reasonable scope, clear value`,
44
+ `- REJECT if: out of scope, too risky, contradicts product direction, duplicate of existing functionality`,
45
+ `- When accepting, refine the scope to be specific and achievable`,
46
+ `- Estimate type and complexity based on what you see in the codebase`,
47
+ ].join('\n');
48
+ }
49
+ export function buildITAnalystTriagePrompt(ctx, poOutput) {
50
+ const kind = ctx.task.issueKind ?? 'feature';
51
+ const lines = [
52
+ `You are an IT analyst creating a detailed technical specification for a ${kind} issue.`,
53
+ ``,
54
+ `## Issue`,
55
+ `**${ctx.task.title}**`,
56
+ ...descriptionBlock(ctx.task),
57
+ ];
58
+ if (poOutput) {
59
+ lines.push(``, `## Product Owner's Brief`, poOutput);
60
+ }
61
+ lines.push(``, `## Working Directory`, ctx.worktreePath, ``, `## Project Root`, ctx.projectDir, ``, `## Instructions`, `Read the codebase to understand existing patterns, architecture, and conventions.`, `Create a detailed specification for this ${kind}.`, ``, `## Required Output Format`, ``, `### Description`, `Clear explanation of what needs to be done and why.`, ``, `### Acceptance Criteria`, `- [ ] Testable criterion 1`, `- [ ] Testable criterion 2`, `- [ ] (at least 3 criteria)`, ``, `### Technical Notes`, `Architecture decisions, existing patterns to follow, reuse opportunities.`, ``, `### Edge Cases`, `- Boundary condition 1`, `- Boundary condition 2`, ``, `### Affected Components`, `List file paths and modules that will need changes.`, ``, `### Metadata`, `Respond with a JSON block at the end:`, '```json', `{`, ` "type": "frontend" | "backend" | "fullstack",`, ` "complexity": "S" | "M" | "L",`, ` "dependencies": ["#N", ...]`, `}`, '```');
62
+ return lines.join('\n');
63
+ }
3
64
  // ─── Planning prompts ───
4
65
  export function buildPlanPrompt(ctx) {
5
66
  const lines = [
@@ -7,6 +68,7 @@ export function buildPlanPrompt(ctx) {
7
68
  ``,
8
69
  `## Task`,
9
70
  `**${ctx.task.title}** (${ctx.task.type}, Complexity: ${ctx.task.complexity})`,
71
+ ...descriptionBlock(ctx.task),
10
72
  ``,
11
73
  `## Working Directory`,
12
74
  ctx.worktreePath,
@@ -30,6 +92,7 @@ export function buildImplementPrompt(ctx) {
30
92
  ``,
31
93
  `## Task`,
32
94
  `**${ctx.task.title}** (${ctx.task.type}, Complexity: ${ctx.task.complexity})`,
95
+ ...descriptionBlock(ctx.task),
33
96
  ``,
34
97
  `## Working Directory`,
35
98
  ctx.worktreePath,
@@ -54,6 +117,7 @@ export function buildQAPrompt(ctx) {
54
117
  ``,
55
118
  `## Task`,
56
119
  `**${ctx.task.title}** (${ctx.task.type})`,
120
+ ...descriptionBlock(ctx.task),
57
121
  ``,
58
122
  `## Working Directory`,
59
123
  ctx.worktreePath,
@@ -74,6 +138,7 @@ export function buildReviewPrompt(ctx) {
74
138
  ``,
75
139
  `## Task`,
76
140
  `**${ctx.task.title}** (${ctx.task.type})`,
141
+ ...descriptionBlock(ctx.task),
77
142
  ``,
78
143
  `## Working Directory`,
79
144
  ctx.worktreePath,
@@ -111,6 +176,7 @@ export function buildDecomposePrompt(task) {
111
176
  ``,
112
177
  `## Task`,
113
178
  `**${task.title}** (${task.type}, Phase ${task.phase})`,
179
+ ...descriptionBlock(task),
114
180
  ``,
115
181
  `## Instructions`,
116
182
  `Split this task into 2-4 smaller tasks (Complexity S or M).`,
@@ -136,6 +202,7 @@ export function buildRoutingPrompt(task, step, agents) {
136
202
  ``,
137
203
  `## Task`,
138
204
  `**${task.title}** (${task.type}, Complexity: ${task.complexity})`,
205
+ ...descriptionBlock(task),
139
206
  ``,
140
207
  `## Pipeline Step`,
141
208
  step,
@@ -118,7 +118,7 @@ export class WorkerPool {
118
118
  };
119
119
  }
120
120
  async killAll() {
121
- for (const [slotId, q] of this.queries) {
121
+ const kills = [...this.queries.entries()].map(async ([slotId, q]) => {
122
122
  try {
123
123
  await q.return(undefined);
124
124
  }
@@ -127,7 +127,8 @@ export class WorkerPool {
127
127
  slot.status = 'idle';
128
128
  slot.taskId = null;
129
129
  slot.pid = null;
130
- }
130
+ });
131
+ await Promise.allSettled(kills);
131
132
  this.queries.clear();
132
133
  }
133
134
  reset() {
@@ -66,6 +66,10 @@ export function parseSchedulerArgs(argv) {
66
66
  mutable.resume = true;
67
67
  continue;
68
68
  }
69
+ if (arg === '--reset-iterations') {
70
+ mutable.resetIterations = true;
71
+ continue;
72
+ }
69
73
  if (arg === '--interactive' || arg === '-i') {
70
74
  mutable.interactive = true;
71
75
  continue;
@@ -178,6 +182,7 @@ Options:
178
182
  --project-dir <path> Project directory (default: cwd)
179
183
  --skip-permissions Bypass all permission checks
180
184
  --resume Resume from saved state
185
+ --reset-iterations Reset iteration counter (use with --resume)
181
186
  --resume-session <id> Resume specific Claude session
182
187
  --notify-command <cmd> Shell command on critical events
183
188
  --log-file <path> Log file path
@@ -248,6 +253,12 @@ export async function runScheduler(opts) {
248
253
  // Clear in-progress pipelines from previous run (can't resume mid-pipeline)
249
254
  if (!opts.resume) {
250
255
  state.pipelines = {};
256
+ // Reset iteration counter on fresh start (not --resume)
257
+ state.iteration = 0;
258
+ }
259
+ // Explicit --reset-iterations flag (works with or without --resume)
260
+ if (opts.resetIterations) {
261
+ state.iteration = 0;
251
262
  }
252
263
  // Always re-run recovery on fresh scheduler start
253
264
  state._recoveryDone = false;
@@ -256,6 +267,12 @@ export async function runScheduler(opts) {
256
267
  else {
257
268
  state = emptyState(opts.concurrency);
258
269
  }
270
+ // Warn if iteration limit already reached (e.g. --resume without --reset-iterations)
271
+ if (state.iteration >= opts.maxIterations) {
272
+ logger.warn(`Iteration limit reached (${state.iteration}/${opts.maxIterations}). Use --reset-iterations or --max-iterations ${state.iteration + 50} to continue.`);
273
+ tui.destroy();
274
+ return;
275
+ }
259
276
  // Ensure platform CLI is available before task mode detection
260
277
  const detectedPlatform = detectCIPlatform(opts.projectDir);
261
278
  if (detectedPlatform !== 'none') {
@@ -326,12 +343,26 @@ export async function runScheduler(opts) {
326
343
  let stopping = false;
327
344
  const stoppingRef = { value: false };
328
345
  const cleanup = () => {
346
+ if (stopping) {
347
+ // Second signal — force exit immediately
348
+ logger.warn('Force exit.');
349
+ tui.destroy();
350
+ process.exit(1);
351
+ }
329
352
  stopping = true;
330
353
  stoppingRef.value = true;
331
354
  writeState(opts.projectDir, state);
332
355
  appendEvent(opts.projectDir, createEvent('health_check', { detail: 'Interrupted — state saved' }));
333
- logger.warn('Interrupted. State saved.');
356
+ logger.warn('Interrupted. State saved. Press Ctrl+C again to force exit.');
357
+ // Kill workers with 5s timeout — guarantee exit even if SDK hangs
358
+ const forceTimer = setTimeout(() => {
359
+ logger.warn('Kill timeout — force exit.');
360
+ tui.destroy();
361
+ process.exit(1);
362
+ }, 5_000);
363
+ forceTimer.unref();
334
364
  pool.killAll().finally(() => {
365
+ clearTimeout(forceTimer);
335
366
  tui.destroy();
336
367
  process.exit(0);
337
368
  });
@@ -4,7 +4,7 @@ import { readFileSync, writeFileSync, existsSync, mkdirSync } from 'node:fs';
4
4
  import { execFileSync } from 'node:child_process';
5
5
  import { resolve } from 'node:path';
6
6
  import { parseTodoMd, updateTaskCheckbox } from './tasks/parser.mjs';
7
- import { fetchOpenIssues, issueToTask, updateIssueStatus } from './tasks/issue-source.mjs';
7
+ import { fetchOpenIssues, issueToTask, updateIssueStatus, isTriaged, updateIssueBody, addIssueLabels, closeIssue } from './tasks/issue-source.mjs';
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';
@@ -14,7 +14,7 @@ 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
- import { buildPlanPrompt, buildImplementPrompt, buildQAPrompt, buildReviewPrompt, buildReworkPrompt, buildCIFixPrompt, buildPRCommentPrompt, } from './agents/prompt-builder.mjs';
17
+ import { buildPlanPrompt, buildImplementPrompt, buildQAPrompt, buildReviewPrompt, buildReworkPrompt, buildCIFixPrompt, buildPRCommentPrompt, buildPOTriagePrompt, buildITAnalystTriagePrompt, } from './agents/prompt-builder.mjs';
18
18
  import { discoveredIssueToTask } from './tools/report-issue.mjs';
19
19
  import { createSchedulerToolServer } from './tools/scheduler-tools.mjs';
20
20
  const MAX_REVIEW_CYCLES = 5;
@@ -405,15 +405,102 @@ async function runTaskPipeline(task, workerId, agents, deps) {
405
405
  assignedAgent: null,
406
406
  prState: null,
407
407
  approvalWaitingSince: null,
408
+ triageOutput: null,
408
409
  };
409
410
  pipeline.workerId = workerId;
410
411
  state.pipelines[task.id] = pipeline;
411
412
  // Determine which steps to skip (for resumed pipelines)
412
- const skipTo = resumeStep ?? 'plan';
413
- const stepOrder = ['plan', 'implement', 'test', 'review', 'rework', 'commit', 'pr-create', 'pr-watch', 'merge'];
413
+ const skipTo = resumeStep ?? 'triage';
414
+ const stepOrder = ['triage', 'plan', 'implement', 'test', 'review', 'rework', 'commit', 'pr-create', 'pr-watch', 'merge'];
414
415
  const skipToIndex = stepOrder.indexOf(skipTo);
415
416
  const shouldSkip = (step) => stepOrder.indexOf(step) < skipToIndex;
416
417
  try {
418
+ // STEP 0: Triage (platform issues only — features go through PO, bugs go to IT analyst)
419
+ if (!shouldSkip('triage')) {
420
+ if (task.source === 'platform' && !isTriaged(task)) {
421
+ pipeline.step = 'triage';
422
+ appendEvent(projectDir, createEvent('step_changed', { taskId: task.id, step: 'triage' }));
423
+ const ciPlatformTriage = detectCIPlatform(projectDir);
424
+ const issueNum = extractIssueNumber(task.issueMarker);
425
+ const isBug = task.issueKind === 'bug';
426
+ let poOutput;
427
+ // Features: product owner evaluates first
428
+ if (!isBug) {
429
+ logger.info(`[${task.id}] Triage: feature request — consulting product owner`);
430
+ const poResult = await spawnAgent(pool, workerId, {
431
+ agent: 'product-owner',
432
+ cwd: worktreePath,
433
+ prompt: buildPOTriagePrompt({ task, worktreePath, projectDir }),
434
+ model: 'opus',
435
+ }, state, task.id, logger, onMessage, onSpawnStart, onSpawnEnd);
436
+ if (poResult.success) {
437
+ poOutput = poResult.output;
438
+ // Parse PO decision
439
+ const jsonMatch = poResult.output.match(/\{[\s\S]*"decision"[\s\S]*\}/);
440
+ if (jsonMatch) {
441
+ try {
442
+ const poDecision = JSON.parse(jsonMatch[0]);
443
+ if (poDecision.decision === 'reject') {
444
+ logger.info(`[${task.id}] Product owner rejected: ${poDecision.reason}`);
445
+ // Close the issue with explanation
446
+ if (ciPlatformTriage !== 'none' && issueNum) {
447
+ closeIssue(projectDir, ciPlatformTriage, issueNum, `**Rejected by product analysis:**\n\n${poDecision.reason}\n\nRisks: ${(poDecision.risks ?? []).join(', ')}`);
448
+ }
449
+ cleanupWorktree(projectDir, worktreePath, slug);
450
+ pipeline.step = 'done';
451
+ delete state.pipelines[task.id];
452
+ task.status = 'skipped';
453
+ state.skippedTasks.push(task.id);
454
+ appendEvent(projectDir, createEvent('task_skipped', { taskId: task.id, detail: `PO rejected: ${poDecision.reason}` }));
455
+ return false;
456
+ }
457
+ }
458
+ catch { /* parse error — continue with IT analyst */ }
459
+ }
460
+ }
461
+ }
462
+ // IT analyst creates detailed spec
463
+ logger.info(`[${task.id}] Triage: ${isBug ? 'bug' : 'accepted feature'} — creating spec via IT analyst`);
464
+ const analystResult = await spawnAgent(pool, workerId, {
465
+ agent: 'it-analyst',
466
+ cwd: worktreePath,
467
+ prompt: buildITAnalystTriagePrompt({ task, worktreePath, projectDir }, poOutput),
468
+ model: 'sonnet',
469
+ }, state, task.id, logger, onMessage, onSpawnStart, onSpawnEnd);
470
+ if (analystResult.success) {
471
+ pipeline.triageOutput = analystResult.output;
472
+ // Update task description with triage output
473
+ task.description = analystResult.output;
474
+ // Parse metadata from analyst output
475
+ const metaMatch = analystResult.output.match(/```json\s*(\{[\s\S]*?\})\s*```/);
476
+ if (metaMatch) {
477
+ try {
478
+ const meta = JSON.parse(metaMatch[1]);
479
+ if (meta.type)
480
+ task.type = meta.type;
481
+ if (meta.complexity)
482
+ task.complexity = meta.complexity;
483
+ }
484
+ catch { /* ignore parse errors */ }
485
+ }
486
+ // Update issue on platform with structured spec + labels
487
+ if (ciPlatformTriage !== 'none' && issueNum) {
488
+ try {
489
+ updateIssueBody(projectDir, ciPlatformTriage, issueNum, analystResult.output);
490
+ const newLabels = [`type::${task.type}`, `complexity::${task.complexity}`, 'status::todo'];
491
+ addIssueLabels(projectDir, ciPlatformTriage, issueNum, newLabels);
492
+ logger.info(`[${task.id}] Issue updated with spec + labels`);
493
+ }
494
+ catch (err) {
495
+ logger.warn(`[${task.id}] Failed to update issue: ${err.message?.split('\n')[0]}`);
496
+ }
497
+ }
498
+ }
499
+ else {
500
+ logger.warn(`[${task.id}] IT analyst triage failed — proceeding with original description`);
501
+ }
502
+ }
503
+ }
417
504
  // Route task to agent
418
505
  const routing = await orchestrator.routeTask(task, shouldSkip('plan') ? skipTo : 'plan', agents);
419
506
  pipeline.assignedAgent = routing.agent;
@@ -839,6 +926,7 @@ export async function recoverOrphanedWorktrees(projectDir, state, logger, _deps)
839
926
  assignedAgent: null,
840
927
  prState: null,
841
928
  approvalWaitingSince: null,
929
+ triageOutput: null,
842
930
  };
843
931
  }
844
932
  catch (err) {
@@ -932,6 +1020,7 @@ async function analyzeStaleUnmergedBranches(projectDir, state, logger, deps) {
932
1020
  assignedAgent: null,
933
1021
  prState: null,
934
1022
  approvalWaitingSince: null,
1023
+ triageOutput: null,
935
1024
  };
936
1025
  logger.info(`[recovery] Branch "${d.branch}" re-injected at step "${step}"`);
937
1026
  }
@@ -1042,6 +1131,7 @@ export function recoverOpenMRs(projectDir, platform, state, logger) {
1042
1131
  assignedAgent: null,
1043
1132
  prState: { prNumber: mr.number, url: '', issueNumber: issueNum },
1044
1133
  approvalWaitingSince: null,
1134
+ triageOutput: null,
1045
1135
  };
1046
1136
  }
1047
1137
  catch (err) {
@@ -42,8 +42,14 @@ const PHASE_RE = /^Phase\s+(\d+)/i;
42
42
  const DEPENDS_ON_RE = /depends[- ]on:\s*(.+)/im;
43
43
  export function issueToTask(issue, fallbackPhase) {
44
44
  const phase = extractPhase(issue.milestone) ?? fallbackPhase;
45
- const complexity = extractLabel(issue.labels, 'complexity::', 'M');
46
- const type = extractLabel(issue.labels, 'type::', 'fullstack');
45
+ const hasTypeLabel = issue.labels.some(l => l.startsWith('type::'));
46
+ const hasComplexityLabel = issue.labels.some(l => l.startsWith('complexity::'));
47
+ const type = hasTypeLabel
48
+ ? extractLabel(issue.labels, 'type::', 'fullstack')
49
+ : inferType(issue.title, issue.body);
50
+ const complexity = hasComplexityLabel
51
+ ? extractLabel(issue.labels, 'complexity::', 'M')
52
+ : inferComplexity(issue.title, issue.body);
47
53
  const status = extractStatus(issue.labels);
48
54
  const dependsOn = extractDependencies(issue.body, issue.labels);
49
55
  return {
@@ -55,8 +61,10 @@ export function issueToTask(issue, fallbackPhase) {
55
61
  dependsOn,
56
62
  issueMarker: `#${issue.issueNumber}`,
57
63
  kitUpgrade: issue.labels.some(l => l.toLowerCase().includes('kit-upgrade')),
58
- lineNumber: 0, // not applicable for platform issues
64
+ lineNumber: 0,
59
65
  source: 'platform',
66
+ description: truncateBody(issue.body, 4000),
67
+ issueKind: classifyIssueKind(issue.title, issue.labels),
60
68
  status,
61
69
  changelog: inferChangelog(issue.title, issue.labels),
62
70
  };
@@ -123,6 +131,109 @@ function inferChangelog(title, labels) {
123
131
  return 'changed';
124
132
  return 'added';
125
133
  }
134
+ // ─── Body truncation ───
135
+ export function truncateBody(body, maxChars) {
136
+ if (!body || !body.trim())
137
+ return undefined;
138
+ if (body.length <= maxChars)
139
+ return body;
140
+ // Truncate at the last paragraph break before maxChars
141
+ const truncated = body.slice(0, maxChars);
142
+ const lastBreak = truncated.lastIndexOf('\n\n');
143
+ const cutPoint = lastBreak > maxChars * 0.5 ? lastBreak : maxChars;
144
+ return truncated.slice(0, cutPoint) + '\n\n[... truncated]';
145
+ }
146
+ // ─── Issue classification ───
147
+ const BUG_KEYWORDS = ['bug', 'fix', 'hotfix', 'broken', 'crash', 'error', 'regression', 'defect'];
148
+ export function classifyIssueKind(title, labels) {
149
+ // Labels take priority
150
+ if (labels.some(l => l === 'bug' || l === 'type::bug'))
151
+ return 'bug';
152
+ if (labels.some(l => l === 'feature' || l === 'enhancement' || l === 'type::feature'))
153
+ return 'feature';
154
+ // Keyword detection from title
155
+ const lower = title.toLowerCase();
156
+ if (BUG_KEYWORDS.some(kw => lower.includes(kw)))
157
+ return 'bug';
158
+ return 'feature';
159
+ }
160
+ /**
161
+ * An issue is considered "triaged" if it has both status:: and type:: and complexity:: labels.
162
+ * Triaged issues skip the triage step and go directly to planning.
163
+ */
164
+ export function isTriaged(task) {
165
+ // Platform tasks that were created by the setup pipeline (it-analyst/technical-planner)
166
+ // already have proper labels. We check if the source data had explicit labels.
167
+ // The simplest heuristic: if the task has a non-default status label AND description
168
+ // contains structured sections (acceptance criteria), it's been triaged.
169
+ // But for now, we use a simpler approach: issues with all 3 metadata labels are triaged.
170
+ return task.source === 'platform' && task.status === 'todo'
171
+ && task.type !== 'fullstack' // has explicit type (not fallback)
172
+ && task.complexity !== 'M'; // has explicit complexity (not fallback)
173
+ }
174
+ // ─── Heuristic inference (fallback when labels missing) ───
175
+ const FRONTEND_SIGNALS = ['component', 'ui', 'css', 'scss', 'template', 'form', 'button', 'page', 'modal', 'dialog', 'layout', 'responsive', 'animation'];
176
+ const BACKEND_SIGNALS = ['api', 'endpoint', 'database', 'migration', 'auth', 'middleware', 'worker', 'cron', 'server', 'query', 'schema'];
177
+ export function inferType(title, body) {
178
+ const text = `${title} ${body}`.toLowerCase();
179
+ const fScore = FRONTEND_SIGNALS.filter(s => text.includes(s)).length;
180
+ const bScore = BACKEND_SIGNALS.filter(s => text.includes(s)).length;
181
+ if (fScore > 0 && bScore > 0)
182
+ return 'fullstack';
183
+ if (fScore > bScore)
184
+ return 'frontend';
185
+ if (bScore > fScore)
186
+ return 'backend';
187
+ return 'fullstack';
188
+ }
189
+ export function inferComplexity(title, body) {
190
+ const text = `${title} ${body}`;
191
+ const criteriaCount = (text.match(/^[-*]\s/gm) ?? []).length;
192
+ const wordCount = text.split(/\s+/).length;
193
+ if (criteriaCount >= 8 || wordCount >= 500)
194
+ return 'L';
195
+ if (criteriaCount >= 3 || wordCount >= 150)
196
+ return 'M';
197
+ return 'S';
198
+ }
199
+ // ─── Update issue on platform ───
200
+ export function updateIssueBody(cwd, platform, issueNumber, body) {
201
+ const num = String(issueNumber);
202
+ if (platform === 'github') {
203
+ run('gh', ['issue', 'edit', num, '--body', body], cwd);
204
+ }
205
+ else {
206
+ run('glab', ['issue', 'update', num, '--description', body], cwd);
207
+ }
208
+ }
209
+ export function addIssueLabels(cwd, platform, issueNumber, labels) {
210
+ if (labels.length === 0)
211
+ return;
212
+ const num = String(issueNumber);
213
+ if (platform === 'github') {
214
+ run('gh', ['issue', 'edit', num, '--add-label', labels.join(',')], cwd);
215
+ }
216
+ else {
217
+ run('glab', ['issue', 'update', num, '--label', labels.join(',')], cwd);
218
+ }
219
+ }
220
+ export function closeIssue(cwd, platform, issueNumber, comment) {
221
+ const num = String(issueNumber);
222
+ if (comment) {
223
+ if (platform === 'github') {
224
+ run('gh', ['issue', 'comment', num, '--body', comment], cwd);
225
+ }
226
+ else {
227
+ run('glab', ['issue', 'note', num, '--message', comment], cwd);
228
+ }
229
+ }
230
+ if (platform === 'github') {
231
+ run('gh', ['issue', 'close', num], cwd);
232
+ }
233
+ else {
234
+ run('glab', ['issue', 'close', num], cwd);
235
+ }
236
+ }
126
237
  // ─── Update issue status on platform ───
127
238
  export function updateIssueStatus(cwd, platform, issueNumber, status) {
128
239
  const statusLabel = `status::${status}`;
@@ -6,6 +6,7 @@ export const SCHEDULER_DEFAULTS = {
6
6
  maxTurns: 50,
7
7
  skipPermissions: true,
8
8
  resume: false,
9
+ resetIterations: false,
9
10
  resumeSession: null,
10
11
  logFile: '.claude/scheduler/scheduler.log',
11
12
  noPull: false,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-claude-workspace",
3
- "version": "2.3.27",
3
+ "version": "2.3.29",
4
4
  "author": "",
5
5
  "repository": {
6
6
  "type": "git",