scene-capability-engine 3.5.2 → 3.6.2

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,46 +1,532 @@
1
- /**
2
- * Task Command Group
3
- *
4
- * Manages task claiming and status updates
1
+ /**
2
+ * Task command group
3
+ *
4
+ * Supports task claiming plus hierarchical task reference operations.
5
5
  */
6
6
 
7
+ const path = require('path');
8
+ const { spawnSync } = require('child_process');
9
+ const fs = require('fs-extra');
7
10
  const chalk = require('chalk');
8
11
  const TaskClaimer = require('../task/task-claimer');
9
12
  const WorkspaceManager = require('../workspace/workspace-manager');
13
+ const { TaskRefRegistry } = require('../task/task-ref-registry');
14
+
15
+ function normalizeString(value) {
16
+ if (typeof value !== 'string') {
17
+ return '';
18
+ }
19
+ return value.trim();
20
+ }
21
+
22
+ function toRelativePosix(projectPath, absolutePath) {
23
+ return path.relative(projectPath, absolutePath).replace(/\\/g, '/');
24
+ }
25
+
26
+ function resolveStudioStageFromTaskKey(taskKey) {
27
+ const normalized = normalizeString(taskKey);
28
+ if (!normalized.startsWith('studio:')) {
29
+ return '';
30
+ }
31
+ return normalizeString(normalized.slice('studio:'.length));
32
+ }
33
+
34
+ function isStudioTaskRef(lookup = {}) {
35
+ const source = normalizeString(lookup.source);
36
+ if (source === 'studio-stage') {
37
+ return true;
38
+ }
39
+ return Boolean(resolveStudioStageFromTaskKey(lookup.task_key));
40
+ }
41
+
42
+ async function loadTaskFromSpec(projectPath, specId, taskId, fileSystem = fs) {
43
+ const tasksPath = path.join(projectPath, '.sce', 'specs', specId, 'tasks.md');
44
+ const exists = await fileSystem.pathExists(tasksPath);
45
+ if (!exists) {
46
+ return null;
47
+ }
48
+
49
+ const claimer = new TaskClaimer();
50
+ const tasks = await claimer.parseTasks(tasksPath);
51
+ const task = tasks.find((item) => item.taskId === taskId);
52
+ if (!task) {
53
+ return null;
54
+ }
55
+
56
+ return {
57
+ task,
58
+ tasksPath
59
+ };
60
+ }
61
+
62
+ async function resetTaskInSpec(projectPath, specId, taskId, fileSystem = fs) {
63
+ const loaded = await loadTaskFromSpec(projectPath, specId, taskId, fileSystem);
64
+ if (!loaded) {
65
+ return {
66
+ success: false,
67
+ error: `Task not found: ${taskId}`
68
+ };
69
+ }
70
+
71
+ const { task, tasksPath } = loaded;
72
+ const content = await fileSystem.readFile(tasksPath, 'utf8');
73
+ const lines = content.split(/\r?\n/);
74
+ if (lines[task.lineNumber] !== task.originalLine) {
75
+ return {
76
+ success: false,
77
+ error: 'Task line changed during rerun reset; retry after reload'
78
+ };
79
+ }
80
+
81
+ const optionalMarker = task.isOptional ? '*' : '';
82
+ const linePrefix = task.linePrefix || '- ';
83
+ const nextLine = `${linePrefix}[ ]${optionalMarker} ${taskId} ${task.title}`;
84
+ lines[task.lineNumber] = nextLine;
85
+ await fileSystem.writeFile(tasksPath, lines.join('\n'), 'utf8');
86
+
87
+ return {
88
+ success: true,
89
+ spec_id: specId,
90
+ task_id: taskId,
91
+ title: task.title,
92
+ previous_status: task.status,
93
+ tasks_path: toRelativePosix(projectPath, tasksPath)
94
+ };
95
+ }
96
+
97
+ async function loadStudioJob(projectPath, jobId, fileSystem = fs) {
98
+ if (!jobId) {
99
+ return null;
100
+ }
101
+
102
+ const jobPath = path.join(projectPath, '.sce', 'studio', 'jobs', `${jobId}.json`);
103
+ if (!await fileSystem.pathExists(jobPath)) {
104
+ return null;
105
+ }
106
+
107
+ try {
108
+ return await fileSystem.readJson(jobPath);
109
+ } catch (_error) {
110
+ return null;
111
+ }
112
+ }
113
+
114
+ async function readLatestStudioJobId(projectPath, fileSystem = fs) {
115
+ const latestPath = path.join(projectPath, '.sce', 'studio', 'latest-job.json');
116
+ if (!await fileSystem.pathExists(latestPath)) {
117
+ return '';
118
+ }
119
+
120
+ try {
121
+ const latest = await fileSystem.readJson(latestPath);
122
+ return normalizeString(latest && latest.job_id);
123
+ } catch (_error) {
124
+ return '';
125
+ }
126
+ }
127
+
128
+ async function findLatestStudioJob(projectPath, sceneId, specId, fileSystem = fs) {
129
+ const latestJobId = await readLatestStudioJobId(projectPath, fileSystem);
130
+ if (latestJobId) {
131
+ const latestJob = await loadStudioJob(projectPath, latestJobId, fileSystem);
132
+ if (latestJob) {
133
+ const latestScene = normalizeString(latestJob?.scene?.id);
134
+ const latestSpec = normalizeString(latestJob?.scene?.spec_id) || normalizeString(latestJob?.source?.spec_id);
135
+ if (latestScene === normalizeString(sceneId) && latestSpec === normalizeString(specId)) {
136
+ return latestJob;
137
+ }
138
+ }
139
+ }
140
+
141
+ const jobsDir = path.join(projectPath, '.sce', 'studio', 'jobs');
142
+ if (!await fileSystem.pathExists(jobsDir)) {
143
+ return null;
144
+ }
145
+
146
+ const entries = await fileSystem.readdir(jobsDir);
147
+ const jobs = [];
148
+ for (const entry of entries) {
149
+ if (!entry.endsWith('.json')) {
150
+ continue;
151
+ }
152
+
153
+ const absolutePath = path.join(jobsDir, entry);
154
+ try {
155
+ const payload = await fileSystem.readJson(absolutePath);
156
+ const payloadScene = normalizeString(payload?.scene?.id);
157
+ const payloadSpec = normalizeString(payload?.scene?.spec_id) || normalizeString(payload?.source?.spec_id);
158
+ if (payloadScene !== normalizeString(sceneId) || payloadSpec !== normalizeString(specId)) {
159
+ continue;
160
+ }
161
+
162
+ const stat = await fileSystem.stat(absolutePath);
163
+ jobs.push({
164
+ payload,
165
+ mtime: stat.mtimeMs
166
+ });
167
+ } catch (_error) {
168
+ // Ignore malformed job files.
169
+ }
170
+ }
171
+
172
+ if (jobs.length === 0) {
173
+ return null;
174
+ }
175
+
176
+ jobs.sort((a, b) => b.mtime - a.mtime);
177
+ return jobs[0].payload;
178
+ }
179
+
180
+ async function resolveStudioJobForRef(projectPath, lookup, options = {}, fileSystem = fs) {
181
+ const explicitJobId = normalizeString(options.job) || normalizeString(options.jobId);
182
+ if (explicitJobId) {
183
+ const explicitJob = await loadStudioJob(projectPath, explicitJobId, fileSystem);
184
+ if (!explicitJob) {
185
+ throw new Error(`Studio job not found: ${explicitJobId}`);
186
+ }
187
+ return explicitJob;
188
+ }
189
+
190
+ const metaJobId = normalizeString(lookup?.metadata?.job_id);
191
+ if (metaJobId) {
192
+ const metaJob = await loadStudioJob(projectPath, metaJobId, fileSystem);
193
+ if (metaJob) {
194
+ return metaJob;
195
+ }
196
+ }
197
+
198
+ const fallback = await findLatestStudioJob(projectPath, lookup.scene_id, lookup.spec_id, fileSystem);
199
+ return fallback;
200
+ }
201
+
202
+ function buildStudioRerunArgs(stage, lookup, job, options = {}) {
203
+ const normalizedStage = normalizeString(stage);
204
+ const sceneId = normalizeString(lookup.scene_id);
205
+ const specId = normalizeString(lookup.spec_id);
206
+
207
+ if (normalizedStage === 'plan') {
208
+ const fromChat = normalizeString(options.fromChat)
209
+ || normalizeString(job?.source?.from_chat)
210
+ || normalizeString(job?.source?.chat_session)
211
+ || normalizeString(job?.session?.scene_session_id);
212
+
213
+ if (!fromChat) {
214
+ return {
215
+ ok: false,
216
+ error: 'studio plan rerun requires from-chat context; provide --from-chat'
217
+ };
218
+ }
219
+
220
+ const args = ['studio', 'plan', '--scene', sceneId, '--from-chat', fromChat];
221
+ if (specId) {
222
+ args.push('--spec', specId);
223
+ }
224
+ const goal = normalizeString(options.goal) || normalizeString(job?.source?.goal);
225
+ if (goal) {
226
+ args.push('--goal', goal);
227
+ }
228
+ args.push('--json');
229
+ return { ok: true, args };
230
+ }
231
+
232
+ const stageRequiresJob = new Set(['generate', 'apply', 'verify', 'release', 'rollback']);
233
+ if (!stageRequiresJob.has(normalizedStage)) {
234
+ return {
235
+ ok: false,
236
+ error: `Unsupported studio stage for rerun: ${normalizedStage}`
237
+ };
238
+ }
239
+
240
+ const jobId = normalizeString(options.job)
241
+ || normalizeString(options.jobId)
242
+ || normalizeString(job?.job_id)
243
+ || normalizeString(lookup?.metadata?.job_id);
244
+
245
+ if (!jobId) {
246
+ return {
247
+ ok: false,
248
+ error: 'studio rerun requires a job id; provide --job or run studio plan first'
249
+ };
250
+ }
251
+
252
+ const args = ['studio', normalizedStage, '--job', jobId];
253
+ if (normalizedStage === 'verify') {
254
+ const profile = normalizeString(options.profile) || 'standard';
255
+ args.push('--profile', profile);
256
+ }
257
+ if (normalizedStage === 'release') {
258
+ const profile = normalizeString(options.profile) || 'standard';
259
+ const channel = normalizeString(options.channel) || 'dev';
260
+ args.push('--profile', profile, '--channel', channel);
261
+ }
262
+ args.push('--json');
263
+ return { ok: true, args };
264
+ }
265
+
266
+ function stringifySceArgs(args = []) {
267
+ return ['sce', ...args].join(' ').trim();
268
+ }
269
+
270
+ function printTaskRefPayload(payload, options = {}) {
271
+ if (options.json) {
272
+ console.log(JSON.stringify(payload, null, 2));
273
+ return;
274
+ }
275
+
276
+ console.log(chalk.blue(`Task reference: ${payload.task_ref}`));
277
+ console.log(` Scene: ${payload.scene_id}`);
278
+ console.log(` Spec: ${payload.spec_id}`);
279
+ console.log(` Task: ${payload.task_key}`);
280
+ console.log(` Source: ${payload.source}`);
281
+ }
282
+
283
+ function printTaskShowPayload(payload, options = {}) {
284
+ if (options.json) {
285
+ console.log(JSON.stringify(payload, null, 2));
286
+ return;
287
+ }
288
+
289
+ console.log(chalk.blue(`Task show: ${payload.task_ref}`));
290
+ console.log(` Scene: ${payload.target.scene_id}`);
291
+ console.log(` Spec: ${payload.target.spec_id}`);
292
+ console.log(` Task key: ${payload.target.task_key}`);
293
+ console.log(` Source: ${payload.target.source}`);
294
+ if (payload.task) {
295
+ console.log(` Status: ${payload.task.status}`);
296
+ console.log(` Title: ${payload.task.title}`);
297
+ }
298
+ if (payload.rerun_command) {
299
+ console.log(` Rerun: ${payload.rerun_command}`);
300
+ }
301
+ }
302
+
303
+ function printTaskRerunPayload(payload, options = {}) {
304
+ if (options.json) {
305
+ console.log(JSON.stringify(payload, null, 2));
306
+ return;
307
+ }
308
+
309
+ console.log(chalk.blue(`Task rerun: ${payload.task_ref}`));
310
+ console.log(` Type: ${payload.rerun_type}`);
311
+ console.log(` Dry run: ${payload.dry_run ? 'yes' : 'no'}`);
312
+ if (payload.command) {
313
+ console.log(` Command: ${payload.command}`);
314
+ }
315
+ if (payload.reset) {
316
+ console.log(` Reset status: ${payload.reset.previous_status} -> not-started`);
317
+ console.log(` File: ${payload.reset.tasks_path}`);
318
+ }
319
+ if (payload.exit_code != null) {
320
+ console.log(` Exit code: ${payload.exit_code}`);
321
+ }
322
+ }
323
+
324
+ async function runTaskRefCommand(options = {}, dependencies = {}) {
325
+ const projectPath = dependencies.projectPath || process.cwd();
326
+ const fileSystem = dependencies.fileSystem || fs;
327
+ const sceneId = normalizeString(options.scene);
328
+ const specId = normalizeString(options.spec);
329
+ const taskId = normalizeString(options.task || options.taskId);
330
+
331
+ if (!sceneId || !specId || !taskId) {
332
+ throw new Error('--scene, --spec, and --task are required');
333
+ }
334
+
335
+ const registry = dependencies.taskRefRegistry || new TaskRefRegistry(projectPath, { fileSystem });
336
+ const resolved = await registry.resolveOrCreateRef({
337
+ sceneId,
338
+ specId,
339
+ taskKey: taskId,
340
+ source: 'spec-task',
341
+ metadata: {
342
+ task_id: taskId
343
+ }
344
+ });
345
+
346
+ const payload = {
347
+ mode: 'task-ref',
348
+ success: true,
349
+ ...resolved
350
+ };
351
+
352
+ printTaskRefPayload(payload, options);
353
+ return payload;
354
+ }
355
+
356
+ async function runTaskShowCommand(options = {}, dependencies = {}) {
357
+ const projectPath = dependencies.projectPath || process.cwd();
358
+ const fileSystem = dependencies.fileSystem || fs;
359
+ const taskRef = normalizeString(options.ref);
360
+ if (!taskRef) {
361
+ throw new Error('--ref is required');
362
+ }
363
+
364
+ const registry = dependencies.taskRefRegistry || new TaskRefRegistry(projectPath, { fileSystem });
365
+ const lookup = await registry.lookupByRef(taskRef);
366
+ if (!lookup) {
367
+ throw new Error(`Task ref not found: ${taskRef}`);
368
+ }
369
+
370
+ const payload = {
371
+ mode: 'task-show',
372
+ success: true,
373
+ task_ref: lookup.task_ref,
374
+ target: {
375
+ scene_id: lookup.scene_id,
376
+ spec_id: lookup.spec_id,
377
+ task_key: lookup.task_key,
378
+ source: lookup.source,
379
+ metadata: lookup.metadata || {}
380
+ },
381
+ registry_path: lookup.registry_path,
382
+ task: null,
383
+ rerun_command: null
384
+ };
385
+
386
+ if (isStudioTaskRef(lookup)) {
387
+ const stage = resolveStudioStageFromTaskKey(lookup.task_key);
388
+ const job = await resolveStudioJobForRef(projectPath, lookup, options, fileSystem);
389
+ const rerunPlan = buildStudioRerunArgs(stage, lookup, job, options);
390
+ payload.task = {
391
+ kind: 'studio-stage',
392
+ stage,
393
+ job_id: normalizeString(job?.job_id) || normalizeString(lookup?.metadata?.job_id) || null
394
+ };
395
+ if (rerunPlan.ok) {
396
+ payload.rerun_command = stringifySceArgs(rerunPlan.args);
397
+ }
398
+ } else {
399
+ const loaded = await loadTaskFromSpec(projectPath, lookup.spec_id, lookup.task_key, fileSystem);
400
+ if (loaded) {
401
+ payload.task = {
402
+ kind: 'spec-task',
403
+ task_id: loaded.task.taskId,
404
+ title: loaded.task.title,
405
+ status: loaded.task.status,
406
+ claimed_by: loaded.task.claimedBy || null,
407
+ claimed_at: loaded.task.claimedAt || null,
408
+ is_optional: loaded.task.isOptional === true,
409
+ tasks_path: toRelativePosix(projectPath, loaded.tasksPath)
410
+ };
411
+ } else {
412
+ payload.task = {
413
+ kind: 'spec-task',
414
+ task_id: lookup.task_key,
415
+ status: 'unknown'
416
+ };
417
+ }
418
+ payload.rerun_command = `sce task rerun --ref ${lookup.task_ref}`;
419
+ }
420
+
421
+ printTaskShowPayload(payload, options);
422
+ return payload;
423
+ }
424
+
425
+ async function runTaskRerunCommand(options = {}, dependencies = {}) {
426
+ const projectPath = dependencies.projectPath || process.cwd();
427
+ const fileSystem = dependencies.fileSystem || fs;
428
+ const taskRef = normalizeString(options.ref);
429
+ if (!taskRef) {
430
+ throw new Error('--ref is required');
431
+ }
432
+
433
+ const dryRun = options.dryRun === true;
434
+ const registry = dependencies.taskRefRegistry || new TaskRefRegistry(projectPath, { fileSystem });
435
+ const lookup = await registry.lookupByRef(taskRef);
436
+ if (!lookup) {
437
+ throw new Error(`Task ref not found: ${taskRef}`);
438
+ }
439
+
440
+ if (isStudioTaskRef(lookup)) {
441
+ const stage = resolveStudioStageFromTaskKey(lookup.task_key);
442
+ const job = await resolveStudioJobForRef(projectPath, lookup, options, fileSystem);
443
+ const rerunPlan = buildStudioRerunArgs(stage, lookup, job, options);
444
+ if (!rerunPlan.ok) {
445
+ throw new Error(rerunPlan.error || 'Failed to build studio rerun command');
446
+ }
447
+
448
+ const payload = {
449
+ mode: 'task-rerun',
450
+ success: true,
451
+ task_ref: lookup.task_ref,
452
+ rerun_type: 'studio-stage',
453
+ stage,
454
+ job_id: normalizeString(job?.job_id) || null,
455
+ dry_run: dryRun,
456
+ command: stringifySceArgs(rerunPlan.args)
457
+ };
458
+
459
+ if (!dryRun) {
460
+ const cliEntry = path.resolve(__dirname, '..', '..', 'bin', 'sce.js');
461
+ const execution = spawnSync(process.execPath, [cliEntry, ...rerunPlan.args], {
462
+ cwd: projectPath,
463
+ env: process.env,
464
+ encoding: 'utf8',
465
+ windowsHide: true,
466
+ maxBuffer: 1024 * 1024 * 20
467
+ });
468
+
469
+ payload.exit_code = Number.isInteger(execution.status) ? execution.status : 1;
470
+ payload.stdout = `${execution.stdout || ''}`;
471
+ payload.stderr = `${execution.stderr || ''}`;
472
+ payload.success = payload.exit_code === 0;
473
+
474
+ if (!payload.success) {
475
+ throw new Error(`studio rerun command failed (exit=${payload.exit_code})`);
476
+ }
477
+ }
478
+
479
+ printTaskRerunPayload(payload, options);
480
+ return payload;
481
+ }
482
+
483
+ const payload = {
484
+ mode: 'task-rerun',
485
+ success: true,
486
+ task_ref: lookup.task_ref,
487
+ rerun_type: 'spec-task',
488
+ dry_run: dryRun,
489
+ command: `sce task claim ${lookup.spec_id} ${lookup.task_key}`
490
+ };
491
+
492
+ if (!dryRun) {
493
+ const reset = await resetTaskInSpec(projectPath, lookup.spec_id, lookup.task_key, fileSystem);
494
+ if (!reset.success) {
495
+ throw new Error(reset.error || 'Failed to reset task status for rerun');
496
+ }
497
+ payload.reset = reset;
498
+ }
499
+
500
+ printTaskRerunPayload(payload, options);
501
+ return payload;
502
+ }
10
503
 
11
504
  /**
12
- * Claim a task
13
- *
14
- * @param {string} specName - Spec name
15
- * @param {string} taskId - Task ID
16
- * @param {Object} options - Command options
17
- * @param {string} options.user - Override username
18
- * @param {boolean} options.force - Force claim even if already claimed
19
- * @returns {Promise<void>}
505
+ * Claim a task.
20
506
  */
21
507
  async function claimTask(specName, taskId, options = {}) {
22
508
  const projectPath = process.cwd();
23
509
  const taskClaimer = new TaskClaimer();
24
510
  const workspaceManager = new WorkspaceManager();
25
-
26
- console.log(chalk.red('🔥') + ' Claiming Task');
511
+
512
+ console.log(chalk.blue('Task claim'));
27
513
  console.log();
28
-
514
+
29
515
  try {
30
516
  const username = options.user || await workspaceManager.detectUsername();
31
-
517
+
32
518
  if (!username) {
33
- console.log(chalk.red('Could not detect username'));
519
+ console.log(chalk.red('Could not detect username'));
34
520
  console.log();
35
521
  console.log('Please configure git or use --user flag');
36
522
  return;
37
523
  }
38
-
524
+
39
525
  console.log(`Spec: ${chalk.cyan(specName)}`);
40
526
  console.log(`Task: ${chalk.cyan(taskId)}`);
41
527
  console.log(`User: ${chalk.cyan(username)}`);
42
528
  console.log();
43
-
529
+
44
530
  const result = await taskClaimer.claimTask(
45
531
  projectPath,
46
532
  specName,
@@ -48,119 +534,107 @@ async function claimTask(specName, taskId, options = {}) {
48
534
  username,
49
535
  options.force
50
536
  );
51
-
537
+
52
538
  if (result.success) {
53
- console.log(chalk.green('Task claimed successfully'));
539
+ console.log(chalk.green('Task claimed successfully'));
54
540
  console.log();
55
- console.log(`Task: ${result.taskTitle}`);
541
+ console.log(`Task: ${result.taskTitle || taskId}`);
56
542
  console.log(`Claimed by: ${chalk.cyan(username)}`);
57
543
  console.log(`Claimed at: ${chalk.gray(result.claimedAt)}`);
58
-
544
+
59
545
  if (result.previousClaim) {
60
546
  console.log();
61
- console.log(chalk.yellow('⚠️ Previous claim overridden:'));
547
+ console.log(chalk.yellow('Previous claim overridden:'));
62
548
  console.log(` User: ${result.previousClaim.username}`);
63
- console.log(` Time: ${result.previousClaim.timestamp}`);
549
+ console.log(` Time: ${result.previousClaim.timestamp || result.previousClaim.claimedAt}`);
64
550
  }
65
551
  } else {
66
- console.log(chalk.red('Failed to claim task'));
552
+ console.log(chalk.red('Failed to claim task'));
67
553
  console.log();
68
554
  console.log(`Error: ${result.error}`);
69
-
70
- if (result.existingClaim) {
555
+
556
+ const currentClaim = result.currentClaim || result.existingClaim;
557
+ if (currentClaim) {
71
558
  console.log();
72
559
  console.log('Task is already claimed by:');
73
- console.log(` User: ${chalk.cyan(result.existingClaim.username)}`);
74
- console.log(` Time: ${chalk.gray(result.existingClaim.timestamp)}`);
560
+ console.log(` User: ${chalk.cyan(currentClaim.username)}`);
561
+ console.log(` Time: ${chalk.gray(currentClaim.claimedAt || currentClaim.timestamp || 'n/a')}`);
75
562
  console.log();
76
563
  console.log('Use ' + chalk.cyan('--force') + ' to override the claim');
77
564
  }
78
565
  }
79
566
  } catch (error) {
80
- console.log(chalk.red('Error:'), error.message);
567
+ console.log(chalk.red('Error:'), error.message);
81
568
  }
82
569
  }
83
570
 
84
571
  /**
85
- * Unclaim a task
86
- *
87
- * @param {string} specName - Spec name
88
- * @param {string} taskId - Task ID
89
- * @param {Object} options - Command options
90
- * @param {string} options.user - Override username
91
- * @returns {Promise<void>}
572
+ * Unclaim a task.
92
573
  */
93
574
  async function unclaimTask(specName, taskId, options = {}) {
94
575
  const projectPath = process.cwd();
95
576
  const taskClaimer = new TaskClaimer();
96
577
  const workspaceManager = new WorkspaceManager();
97
-
98
- console.log(chalk.red('🔥') + ' Unclaiming Task');
578
+
579
+ console.log(chalk.blue('Task unclaim'));
99
580
  console.log();
100
-
581
+
101
582
  try {
102
583
  const username = options.user || await workspaceManager.detectUsername();
103
-
584
+
104
585
  if (!username) {
105
- console.log(chalk.red('Could not detect username'));
586
+ console.log(chalk.red('Could not detect username'));
106
587
  return;
107
588
  }
108
-
589
+
109
590
  console.log(`Spec: ${chalk.cyan(specName)}`);
110
591
  console.log(`Task: ${chalk.cyan(taskId)}`);
111
592
  console.log(`User: ${chalk.cyan(username)}`);
112
593
  console.log();
113
-
594
+
114
595
  const result = await taskClaimer.unclaimTask(
115
596
  projectPath,
116
597
  specName,
117
598
  taskId,
118
599
  username
119
600
  );
120
-
601
+
121
602
  if (result.success) {
122
- console.log(chalk.green('Task unclaimed successfully'));
603
+ console.log(chalk.green('Task unclaimed successfully'));
123
604
  console.log();
124
- console.log(`Task: ${result.taskTitle}`);
605
+ console.log(`Task: ${result.taskTitle || taskId}`);
125
606
  } else {
126
- console.log(chalk.red('Failed to unclaim task'));
607
+ console.log(chalk.red('Failed to unclaim task'));
127
608
  console.log();
128
609
  console.log(`Error: ${result.error}`);
129
610
  }
130
611
  } catch (error) {
131
- console.log(chalk.red('Error:'), error.message);
612
+ console.log(chalk.red('Error:'), error.message);
132
613
  }
133
614
  }
134
615
 
135
616
  /**
136
- * List claimed tasks
137
- *
138
- * @param {string} specName - Spec name (optional)
139
- * @param {Object} options - Command options
140
- * @param {string} options.user - Filter by username
141
- * @returns {Promise<void>}
617
+ * List claimed tasks.
142
618
  */
143
619
  async function listClaimedTasks(specName, options = {}) {
144
620
  const projectPath = process.cwd();
145
621
  const taskClaimer = new TaskClaimer();
146
-
147
- console.log(chalk.red('🔥') + ' Claimed Tasks');
622
+
623
+ console.log(chalk.blue('Claimed tasks'));
148
624
  console.log();
149
-
625
+
150
626
  try {
151
627
  if (specName) {
152
- // List claimed tasks for specific spec
153
628
  const tasks = await taskClaimer.getClaimedTasks(projectPath, specName);
154
-
629
+
155
630
  if (tasks.length === 0) {
156
631
  console.log(chalk.gray('No claimed tasks found'));
157
632
  return;
158
633
  }
159
-
634
+
160
635
  console.log(`Spec: ${chalk.cyan(specName)}`);
161
636
  console.log();
162
-
163
- // Group by user
637
+
164
638
  const byUser = {};
165
639
  for (const task of tasks) {
166
640
  if (!byUser[task.claimedBy]) {
@@ -168,12 +642,12 @@ async function listClaimedTasks(specName, options = {}) {
168
642
  }
169
643
  byUser[task.claimedBy].push(task);
170
644
  }
171
-
645
+
172
646
  for (const [user, userTasks] of Object.entries(byUser)) {
173
647
  if (options.user && user !== options.user) {
174
648
  continue;
175
649
  }
176
-
650
+
177
651
  console.log(chalk.cyan(`${user} (${userTasks.length} task(s))`));
178
652
  for (const task of userTasks) {
179
653
  const staleMarker = task.isStale ? chalk.yellow(' [STALE]') : '';
@@ -188,12 +662,100 @@ async function listClaimedTasks(specName, options = {}) {
188
662
  console.log('Usage: ' + chalk.cyan('sce task list <spec-name>'));
189
663
  }
190
664
  } catch (error) {
191
- console.log(chalk.red('Error:'), error.message);
665
+ console.log(chalk.red('Error:'), error.message);
192
666
  }
193
667
  }
194
668
 
669
+ function registerTaskCommands(program) {
670
+ const task = program
671
+ .command('task')
672
+ .description('Manage task claims, references, and reruns');
673
+
674
+ task
675
+ .command('claim <spec-name> <task-id>')
676
+ .description('Claim a task for current user')
677
+ .option('--user <username>', 'Override current username')
678
+ .option('--force', 'Force claim when already claimed')
679
+ .action(async (specName, taskId, options) => {
680
+ await claimTask(specName, taskId, options);
681
+ });
682
+
683
+ task
684
+ .command('unclaim <spec-name> <task-id>')
685
+ .description('Release a claimed task')
686
+ .option('--user <username>', 'Override current username')
687
+ .action(async (specName, taskId, options) => {
688
+ await unclaimTask(specName, taskId, options);
689
+ });
690
+
691
+ task
692
+ .command('list <spec-name>')
693
+ .alias('status')
694
+ .description('List claimed tasks for a spec')
695
+ .option('--user <username>', 'Filter by username')
696
+ .action(async (specName, options) => {
697
+ await listClaimedTasks(specName, options);
698
+ });
699
+
700
+ task
701
+ .command('ref')
702
+ .description('Resolve or create hierarchical task reference (SS.PP.TT)')
703
+ .requiredOption('--scene <scene-id>', 'Scene identifier')
704
+ .requiredOption('--spec <spec-id>', 'Spec identifier')
705
+ .requiredOption('--task <task-id>', 'Task identifier')
706
+ .option('--json', 'Print machine-readable JSON output')
707
+ .action(async (options) => {
708
+ try {
709
+ await runTaskRefCommand(options);
710
+ } catch (error) {
711
+ console.error(chalk.red(`Task ref failed: ${error.message}`));
712
+ process.exitCode = 1;
713
+ }
714
+ });
715
+
716
+ task
717
+ .command('show')
718
+ .description('Show task target by hierarchical task reference')
719
+ .requiredOption('--ref <task-ref>', 'Task reference (SS.PP.TT)')
720
+ .option('--from-chat <session>', 'Override session for studio plan rerun hints')
721
+ .option('--job <job-id>', 'Override studio job id for rerun hints')
722
+ .option('--json', 'Print machine-readable JSON output')
723
+ .action(async (options) => {
724
+ try {
725
+ await runTaskShowCommand(options);
726
+ } catch (error) {
727
+ console.error(chalk.red(`Task show failed: ${error.message}`));
728
+ process.exitCode = 1;
729
+ }
730
+ });
731
+
732
+ task
733
+ .command('rerun')
734
+ .description('Rerun task by hierarchical reference')
735
+ .requiredOption('--ref <task-ref>', 'Task reference (SS.PP.TT)')
736
+ .option('--dry-run', 'Preview rerun command without executing')
737
+ .option('--from-chat <session>', 'Override session for studio plan rerun')
738
+ .option('--job <job-id>', 'Override studio job id')
739
+ .option('--profile <profile>', 'Override profile for studio verify/release rerun')
740
+ .option('--channel <channel>', 'Override channel for studio release rerun')
741
+ .option('--goal <goal>', 'Override goal for studio plan rerun')
742
+ .option('--json', 'Print machine-readable JSON output')
743
+ .action(async (options) => {
744
+ try {
745
+ await runTaskRerunCommand(options);
746
+ } catch (error) {
747
+ console.error(chalk.red(`Task rerun failed: ${error.message}`));
748
+ process.exitCode = 1;
749
+ }
750
+ });
751
+ }
752
+
195
753
  module.exports = {
196
754
  claimTask,
197
755
  unclaimTask,
198
- listClaimedTasks
756
+ listClaimedTasks,
757
+ runTaskRefCommand,
758
+ runTaskShowCommand,
759
+ runTaskRerunCommand,
760
+ registerTaskCommands
199
761
  };