scene-capability-engine 3.5.2 → 3.6.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.
- package/CHANGELOG.md +19 -0
- package/README.md +12 -3
- package/README.zh.md +12 -3
- package/bin/scene-capability-engine.js +2 -0
- package/docs/command-reference.md +22 -3
- package/lib/commands/studio.js +113 -58
- package/lib/commands/task.js +630 -68
- package/lib/state/sce-state-store.js +594 -0
- package/lib/task/task-ref-registry.js +139 -0
- package/package.json +1 -1
package/lib/commands/task.js
CHANGED
|
@@ -1,46 +1,532 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Task
|
|
3
|
-
*
|
|
4
|
-
*
|
|
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.
|
|
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('
|
|
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('
|
|
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('
|
|
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('
|
|
552
|
+
console.log(chalk.red('Failed to claim task'));
|
|
67
553
|
console.log();
|
|
68
554
|
console.log(`Error: ${result.error}`);
|
|
69
|
-
|
|
70
|
-
|
|
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(
|
|
74
|
-
console.log(` Time: ${chalk.gray(
|
|
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('
|
|
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.
|
|
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('
|
|
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('
|
|
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('
|
|
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('
|
|
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.
|
|
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('
|
|
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
|
};
|