@lingjingai/scriptctl 0.27.2 → 0.28.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/README.md +1 -1
- package/changes/unreleased.md +3 -2
- package/dist/help-text.js +17 -5
- package/dist/help-text.js.map +1 -1
- package/dist/usecases/direct.js +673 -92
- package/dist/usecases/direct.js.map +1 -1
- package/dist/usecases/update.js +6 -6
- package/dist/usecases/update.js.map +1 -1
- package/package.json +1 -1
- package/changes/0.27.2.md +0 -13
package/dist/usecases/direct.js
CHANGED
|
@@ -171,6 +171,29 @@ function readBatchResult(dir, batch) {
|
|
|
171
171
|
return null;
|
|
172
172
|
}
|
|
173
173
|
}
|
|
174
|
+
function isDirectStageName(value) {
|
|
175
|
+
switch (value) {
|
|
176
|
+
case "source_prepare":
|
|
177
|
+
case "episode_plan":
|
|
178
|
+
case "batch_plan":
|
|
179
|
+
case "episode_titles":
|
|
180
|
+
case "batch_extract":
|
|
181
|
+
case "episode_merge":
|
|
182
|
+
case "episode_synopsis":
|
|
183
|
+
case "script_merge":
|
|
184
|
+
case "asset_curation":
|
|
185
|
+
case "metadata_extract":
|
|
186
|
+
case "script_synopsis":
|
|
187
|
+
case "validate":
|
|
188
|
+
return true;
|
|
189
|
+
default:
|
|
190
|
+
return false;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
let progressFrame = 0;
|
|
194
|
+
let activeProgressFrame = null;
|
|
195
|
+
let progressTimer = null;
|
|
196
|
+
let progressBlockLines = 0;
|
|
174
197
|
// Single source of truth for a batch's on-disk state, shared by init (what to
|
|
175
198
|
// (re)run) and status (what to report) so the two can never disagree. "done"
|
|
176
199
|
// requires a result that actually PARSES — a present-but-corrupt result is
|
|
@@ -182,6 +205,579 @@ function classifyBatch(dir, batch) {
|
|
|
182
205
|
return "error";
|
|
183
206
|
return "pending";
|
|
184
207
|
}
|
|
208
|
+
function statusFromBatchState(state) {
|
|
209
|
+
return state === "done" ? "success" : state;
|
|
210
|
+
}
|
|
211
|
+
function stageSnapshot(name, status, completed, total, message) {
|
|
212
|
+
const boundedTotal = Math.max(0, total);
|
|
213
|
+
const boundedCompleted = Math.max(0, Math.min(completed, boundedTotal));
|
|
214
|
+
const percent = boundedTotal > 0 ? Math.round((boundedCompleted / boundedTotal) * 100) : status === "success" ? 100 : 0;
|
|
215
|
+
return {
|
|
216
|
+
name,
|
|
217
|
+
status,
|
|
218
|
+
completed: boundedCompleted,
|
|
219
|
+
total: boundedTotal,
|
|
220
|
+
percent,
|
|
221
|
+
pending: status === "skipped" ? 0 : Math.max(0, boundedTotal - boundedCompleted),
|
|
222
|
+
warning: status === "warning" ? 1 : 0,
|
|
223
|
+
error: status === "error" ? 1 : 0,
|
|
224
|
+
message: message ?? "",
|
|
225
|
+
};
|
|
226
|
+
}
|
|
227
|
+
function fileStage(name, filePath, runningStage) {
|
|
228
|
+
if (exists(filePath))
|
|
229
|
+
return stageSnapshot(name, "success", 1, 1);
|
|
230
|
+
return stageSnapshot(name, runningStage === name ? "running" : "pending", 0, 1);
|
|
231
|
+
}
|
|
232
|
+
function readJsonFile(filePath, fallback) {
|
|
233
|
+
if (!exists(filePath))
|
|
234
|
+
return fallback;
|
|
235
|
+
return readJson(filePath);
|
|
236
|
+
}
|
|
237
|
+
function readableJsonFile(filePath) {
|
|
238
|
+
if (!exists(filePath))
|
|
239
|
+
return false;
|
|
240
|
+
try {
|
|
241
|
+
readJson(filePath);
|
|
242
|
+
return true;
|
|
243
|
+
}
|
|
244
|
+
catch {
|
|
245
|
+
return false;
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
function stageUnit(name) {
|
|
249
|
+
if (name === "batch_extract")
|
|
250
|
+
return "progress";
|
|
251
|
+
if (name === "episode_merge" || name === "episode_synopsis" || name === "episode_titles")
|
|
252
|
+
return "items";
|
|
253
|
+
return "step";
|
|
254
|
+
}
|
|
255
|
+
function stageLabel(name) {
|
|
256
|
+
switch (name) {
|
|
257
|
+
case "source_prepare": return "准备素材";
|
|
258
|
+
case "episode_plan": return "阅读剧本";
|
|
259
|
+
case "batch_plan": return "理解剧情";
|
|
260
|
+
case "episode_titles": return "整理内容";
|
|
261
|
+
case "batch_extract": return "整理内容";
|
|
262
|
+
case "episode_merge": return "生成剧本";
|
|
263
|
+
case "episode_synopsis": return "补充信息";
|
|
264
|
+
case "script_merge": return "生成剧本";
|
|
265
|
+
case "asset_curation": return "补充信息";
|
|
266
|
+
case "metadata_extract": return "补充信息";
|
|
267
|
+
case "script_synopsis": return "补充信息";
|
|
268
|
+
case "validate": return "检查结果";
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
function statusText(status) {
|
|
272
|
+
switch (status) {
|
|
273
|
+
case "success": return "完成";
|
|
274
|
+
case "running": return "进行中";
|
|
275
|
+
case "pending": return "等待中";
|
|
276
|
+
case "warning": return "需留意";
|
|
277
|
+
case "error": return "出错";
|
|
278
|
+
case "skipped": return "已跳过";
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
function statusPriority(status) {
|
|
282
|
+
switch (status) {
|
|
283
|
+
case "error": return 5;
|
|
284
|
+
case "warning": return 4;
|
|
285
|
+
case "running": return 3;
|
|
286
|
+
case "pending": return 2;
|
|
287
|
+
case "skipped": return 1;
|
|
288
|
+
default: return 0;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
function userStageRows(stages) {
|
|
292
|
+
const byLabel = new Map();
|
|
293
|
+
for (const stage of stages) {
|
|
294
|
+
const label = stageLabel(stage.name);
|
|
295
|
+
byLabel.set(label, [...(byLabel.get(label) ?? []), stage]);
|
|
296
|
+
}
|
|
297
|
+
const rows = [];
|
|
298
|
+
for (const items of byLabel.values()) {
|
|
299
|
+
const total = items.reduce((sum, item) => sum + stageWeight(item.name), 0);
|
|
300
|
+
const completed = items.reduce((sum, item) => sum + stageWeight(item.name) * (item.status === "skipped" ? 1 : item.percent / 100), 0);
|
|
301
|
+
const status = items.reduce((max, item) => statusPriority(item.status) > statusPriority(max) ? item.status : max, "success");
|
|
302
|
+
rows.push({
|
|
303
|
+
name: items[0].name,
|
|
304
|
+
status,
|
|
305
|
+
completed: Math.round(completed),
|
|
306
|
+
total,
|
|
307
|
+
percent: total > 0 ? Math.round((completed / total) * 100) : 0,
|
|
308
|
+
pending: items.reduce((sum, item) => sum + item.pending, 0),
|
|
309
|
+
warning: items.reduce((sum, item) => sum + item.warning, 0),
|
|
310
|
+
error: items.reduce((sum, item) => sum + item.error, 0),
|
|
311
|
+
message: items.map((item) => item.message).filter(Boolean).join("; "),
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
return rows;
|
|
315
|
+
}
|
|
316
|
+
function stageWeight(name) {
|
|
317
|
+
switch (name) {
|
|
318
|
+
case "source_prepare": return 3;
|
|
319
|
+
case "episode_plan": return 7;
|
|
320
|
+
case "batch_plan": return 5;
|
|
321
|
+
case "episode_titles": return 5;
|
|
322
|
+
case "batch_extract": return 45;
|
|
323
|
+
case "episode_merge": return 8;
|
|
324
|
+
case "episode_synopsis": return 5;
|
|
325
|
+
case "script_merge": return 7;
|
|
326
|
+
case "asset_curation": return 5;
|
|
327
|
+
case "metadata_extract": return 3;
|
|
328
|
+
case "script_synopsis": return 5;
|
|
329
|
+
case "validate": return 2;
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
function snapshotProgressPercent(stages) {
|
|
333
|
+
let done = 0;
|
|
334
|
+
let total = 0;
|
|
335
|
+
for (const stage of stages) {
|
|
336
|
+
const weight = stageWeight(stage.name);
|
|
337
|
+
total += weight;
|
|
338
|
+
done += weight * (stage.status === "skipped" ? 1 : stage.percent / 100);
|
|
339
|
+
}
|
|
340
|
+
return total > 0 ? Math.max(0, Math.min(100, Math.round((done / total) * 100))) : 0;
|
|
341
|
+
}
|
|
342
|
+
function useProgressColor() {
|
|
343
|
+
return Boolean(process.stderr.isTTY) && process.env.NO_COLOR === undefined;
|
|
344
|
+
}
|
|
345
|
+
function ansi(code, text) {
|
|
346
|
+
return useProgressColor() ? `\x1b[${code}m${text}\x1b[0m` : text;
|
|
347
|
+
}
|
|
348
|
+
function colorStatus(status, text) {
|
|
349
|
+
switch (status) {
|
|
350
|
+
case "success": return ansi(32, text);
|
|
351
|
+
case "error": return ansi(31, text);
|
|
352
|
+
case "warning": return ansi(33, text);
|
|
353
|
+
case "running": return ansi(36, text);
|
|
354
|
+
case "pending": return ansi(34, text);
|
|
355
|
+
case "skipped": return ansi(90, text);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
function dim(text) {
|
|
359
|
+
return ansi(2, text);
|
|
360
|
+
}
|
|
361
|
+
function bold(text) {
|
|
362
|
+
return ansi(1, text);
|
|
363
|
+
}
|
|
364
|
+
function progressBar(stage) {
|
|
365
|
+
const width = 24;
|
|
366
|
+
const ratio = stage.percent / 100;
|
|
367
|
+
const filled = Math.round(ratio * width);
|
|
368
|
+
const bar = `${"█".repeat(filled)}${"░".repeat(width - filled)}`;
|
|
369
|
+
return colorStatus(stage.status, bar);
|
|
370
|
+
}
|
|
371
|
+
function statusMark(stage) {
|
|
372
|
+
if (stage.status === "running") {
|
|
373
|
+
const frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
374
|
+
const frame = frames[progressFrame % frames.length];
|
|
375
|
+
progressFrame += 1;
|
|
376
|
+
return ansi(36, frame);
|
|
377
|
+
}
|
|
378
|
+
if (stage.status === "success")
|
|
379
|
+
return ansi(32, "✓");
|
|
380
|
+
if (stage.status === "error")
|
|
381
|
+
return ansi(31, "✖");
|
|
382
|
+
if (stage.status === "warning")
|
|
383
|
+
return ansi(33, "!");
|
|
384
|
+
if (stage.status === "skipped")
|
|
385
|
+
return ansi(90, "-");
|
|
386
|
+
return ansi(34, "•");
|
|
387
|
+
}
|
|
388
|
+
function renderProgressLine(stage) {
|
|
389
|
+
const status = colorStatus(stage.status, stage.status.padEnd(7));
|
|
390
|
+
const percent = `${stage.percent}%`;
|
|
391
|
+
const bits = [
|
|
392
|
+
statusMark(stage),
|
|
393
|
+
bold(stageLabel(stage.name)),
|
|
394
|
+
status,
|
|
395
|
+
progressBar(stage),
|
|
396
|
+
`${percent} ${stageUnit(stage.name)}`,
|
|
397
|
+
];
|
|
398
|
+
if (stage.error > 0)
|
|
399
|
+
bits.push(ansi(31, `error ${stage.error}`));
|
|
400
|
+
if (stage.warning > 0)
|
|
401
|
+
bits.push(ansi(33, `warning ${stage.warning}`));
|
|
402
|
+
if (stage.status === "pending")
|
|
403
|
+
bits.push(ansi(34, statusText("pending")));
|
|
404
|
+
if (stage.message)
|
|
405
|
+
bits.push(dim(`- ${stage.message}`));
|
|
406
|
+
return bits.join(" ");
|
|
407
|
+
}
|
|
408
|
+
function renderStageDetail(stage) {
|
|
409
|
+
const counts = [`${stage.percent}%`];
|
|
410
|
+
if (stage.error > 0)
|
|
411
|
+
counts.push(ansi(31, `error ${stage.error}`));
|
|
412
|
+
if (stage.warning > 0)
|
|
413
|
+
counts.push(ansi(33, `warning ${stage.warning}`));
|
|
414
|
+
if (stage.status === "pending")
|
|
415
|
+
counts.push(ansi(34, statusText("pending")));
|
|
416
|
+
return ` ${dim("stage")} ${stageLabel(stage.name)} ${colorStatus(stage.status, statusText(stage.status))} ${counts.join(", ")}`;
|
|
417
|
+
}
|
|
418
|
+
function renderEpisodeDetail(episode) {
|
|
419
|
+
const percent = episode.batches_total > 0 ? Math.round((episode.batches_done / episode.batches_total) * 100) : 0;
|
|
420
|
+
const errors = episode.error_batches.length > 0 ? ansi(31, " has errors") : "";
|
|
421
|
+
return ` ${dim("episode")} ${episode.episode_id} ${colorStatus(episode.status, statusText(episode.status))} ${percent}%, story ${colorStatus(episode.synopsis_status, statusText(episode.synopsis_status))}${errors}`;
|
|
422
|
+
}
|
|
423
|
+
function renderBatchDetail(batch) {
|
|
424
|
+
const reason = batch.error_code || batch.error_type || batch.message;
|
|
425
|
+
return ` ${dim("detail")} ${batch.batch_id} ${colorStatus(batch.status, statusText(batch.status))} ep_${pad3(batch.episode)}${reason ? ` - ${reason}` : ""}`;
|
|
426
|
+
}
|
|
427
|
+
function capDetails(lines, total, label) {
|
|
428
|
+
if (total <= lines.length)
|
|
429
|
+
return lines;
|
|
430
|
+
return [...lines, ` ${dim(`${total - lines.length} more ${label}`)}`];
|
|
431
|
+
}
|
|
432
|
+
function renderProgressDetails(snapshot, stageName) {
|
|
433
|
+
if (stageName === "batch_extract") {
|
|
434
|
+
const episodes = snapshot.episodes.filter((episode) => episode.status !== "success");
|
|
435
|
+
return [
|
|
436
|
+
...capDetails(episodes.slice(0, 4).map((episode) => renderEpisodeDetail(episode)), episodes.length, "episodes"),
|
|
437
|
+
];
|
|
438
|
+
}
|
|
439
|
+
if (stageName === "episode_titles" || stageName === "episode_merge" || stageName === "episode_synopsis") {
|
|
440
|
+
const episodes = snapshot.episodes.filter((episode) => episode.status !== "success" || episode.synopsis_status !== "success");
|
|
441
|
+
return capDetails(episodes.slice(0, 6).map((episode) => renderEpisodeDetail(episode)), episodes.length, "episodes");
|
|
442
|
+
}
|
|
443
|
+
const stages = snapshot.stages.filter((stage) => stage.status === "running" || stage.status === "pending" || stage.status === "warning" || stage.status === "error");
|
|
444
|
+
return capDetails(stages.slice(0, 5).map((stage) => renderStageDetail(stage)), stages.length, "stages");
|
|
445
|
+
}
|
|
446
|
+
function renderProgressFrameText(frame) {
|
|
447
|
+
return [renderProgressLine(frame.stage), ...frame.details].join("\n");
|
|
448
|
+
}
|
|
449
|
+
function renderProgressFrame() {
|
|
450
|
+
if (activeProgressFrame)
|
|
451
|
+
updateProgressBlock(renderProgressFrameText(activeProgressFrame));
|
|
452
|
+
}
|
|
453
|
+
function progressStageWithStatus(stage, status) {
|
|
454
|
+
if (status === "success")
|
|
455
|
+
return { ...stage, status, completed: stage.total, percent: 100, pending: 0, warning: 0, error: 0 };
|
|
456
|
+
if (status === "warning")
|
|
457
|
+
return { ...stage, status, completed: stage.total, percent: 100, pending: 0, warning: Math.max(1, stage.warning), error: 0 };
|
|
458
|
+
if (status === "skipped")
|
|
459
|
+
return { ...stage, status, percent: 100, pending: 0, warning: 0, error: 0 };
|
|
460
|
+
if (status === "error")
|
|
461
|
+
return { ...stage, status, error: Math.max(1, stage.error) };
|
|
462
|
+
return { ...stage, status };
|
|
463
|
+
}
|
|
464
|
+
function stopProgressTimer() {
|
|
465
|
+
if (!progressTimer)
|
|
466
|
+
return;
|
|
467
|
+
clearInterval(progressTimer);
|
|
468
|
+
progressTimer = null;
|
|
469
|
+
}
|
|
470
|
+
function ensureProgressTimer() {
|
|
471
|
+
if (!process.stderr.isTTY || progressTimer !== null)
|
|
472
|
+
return;
|
|
473
|
+
progressTimer = setInterval(renderProgressFrame, 80);
|
|
474
|
+
}
|
|
475
|
+
function eraseProgressBlock() {
|
|
476
|
+
if (progressBlockLines === 0)
|
|
477
|
+
return;
|
|
478
|
+
process.stderr.write(`\x1b[${progressBlockLines}A`);
|
|
479
|
+
for (let i = 0; i < progressBlockLines; i++) {
|
|
480
|
+
process.stderr.write("\x1b[2K");
|
|
481
|
+
if (i < progressBlockLines - 1)
|
|
482
|
+
process.stderr.write("\x1b[1B");
|
|
483
|
+
}
|
|
484
|
+
process.stderr.write("\r");
|
|
485
|
+
if (progressBlockLines > 1)
|
|
486
|
+
process.stderr.write(`\x1b[${progressBlockLines - 1}A`);
|
|
487
|
+
progressBlockLines = 0;
|
|
488
|
+
}
|
|
489
|
+
function updateProgressBlock(text) {
|
|
490
|
+
eraseProgressBlock();
|
|
491
|
+
process.stderr.write(text + "\n");
|
|
492
|
+
progressBlockLines = text.split("\n").length;
|
|
493
|
+
}
|
|
494
|
+
function persistProgressLine(text) {
|
|
495
|
+
eraseProgressBlock();
|
|
496
|
+
process.stderr.write(text + "\n");
|
|
497
|
+
}
|
|
498
|
+
function emitProgressStage(stage, details = []) {
|
|
499
|
+
const line = renderProgressLine(stage);
|
|
500
|
+
if (!process.stderr.isTTY) {
|
|
501
|
+
process.stderr.write(line + "\n");
|
|
502
|
+
return;
|
|
503
|
+
}
|
|
504
|
+
activeProgressFrame = { stage, details: stage.status === "running" ? details : [] };
|
|
505
|
+
if (stage.status === "running") {
|
|
506
|
+
ensureProgressTimer();
|
|
507
|
+
updateProgressBlock(renderProgressFrameText(activeProgressFrame));
|
|
508
|
+
return;
|
|
509
|
+
}
|
|
510
|
+
stopProgressTimer();
|
|
511
|
+
persistProgressLine(line);
|
|
512
|
+
activeProgressFrame = null;
|
|
513
|
+
}
|
|
514
|
+
function finishInitProgress() {
|
|
515
|
+
stopProgressTimer();
|
|
516
|
+
if (process.stderr.isTTY)
|
|
517
|
+
eraseProgressBlock();
|
|
518
|
+
}
|
|
519
|
+
function emitInitProgress(workspace, stageName, fallbackStatus = "running") {
|
|
520
|
+
const dd = directDir(workspace);
|
|
521
|
+
const episodePlanPath = path.join(dd, "episode_plan.json");
|
|
522
|
+
const batchPlanPath = path.join(dd, "batch_plan.json");
|
|
523
|
+
if (exists(episodePlanPath) && exists(batchPlanPath)) {
|
|
524
|
+
const snapshot = buildDirectStatusSnapshot(workspace);
|
|
525
|
+
const stage = snapshot.stages.find((item) => item.name === stageName);
|
|
526
|
+
if (stage) {
|
|
527
|
+
const renderedStage = fallbackStatus === "running" ? stage : progressStageWithStatus(stage, fallbackStatus);
|
|
528
|
+
emitProgressStage(renderedStage, renderProgressDetails(snapshot, stageName));
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
emitProgressStage({
|
|
533
|
+
name: stageName,
|
|
534
|
+
status: fallbackStatus,
|
|
535
|
+
completed: fallbackStatus === "success" ? 1 : 0,
|
|
536
|
+
total: 1,
|
|
537
|
+
percent: fallbackStatus === "success" ? 100 : 0,
|
|
538
|
+
pending: fallbackStatus === "running" || fallbackStatus === "pending" ? 1 : 0,
|
|
539
|
+
warning: fallbackStatus === "warning" ? 1 : 0,
|
|
540
|
+
error: fallbackStatus === "error" ? 1 : 0,
|
|
541
|
+
message: "",
|
|
542
|
+
});
|
|
543
|
+
}
|
|
544
|
+
function emitFailedInitProgress(workspace) {
|
|
545
|
+
const stage = strOf(readRunState(workspace)["init_stage"]);
|
|
546
|
+
if (isDirectStageName(stage))
|
|
547
|
+
emitInitProgress(workspace, stage, "error");
|
|
548
|
+
}
|
|
549
|
+
function buildDirectStatusSnapshot(workspace) {
|
|
550
|
+
const dd = directDir(workspace);
|
|
551
|
+
const episodePlanPath = path.join(dd, "episode_plan.json");
|
|
552
|
+
const batchPlanPath = path.join(dd, "batch_plan.json");
|
|
553
|
+
const episodePlan = readJsonFile(episodePlanPath, {});
|
|
554
|
+
const batchPlan = readJsonFile(batchPlanPath, {});
|
|
555
|
+
const episodes = asList(episodePlan["episodes"]);
|
|
556
|
+
const batches = asList(batchPlan["batches"]);
|
|
557
|
+
const state = readRunState(workspace);
|
|
558
|
+
const rawRunningStage = strOf(state["init_stage"]);
|
|
559
|
+
const runningStage = rawRunningStage && rawRunningStage !== "complete" ? rawRunningStage : null;
|
|
560
|
+
const runStatus = strOf(state["status"]);
|
|
561
|
+
const failedStage = runStatus === "init_failed" ? runningStage : null;
|
|
562
|
+
const batchResultsDir = path.join(dd, "batch_results");
|
|
563
|
+
const episodeResultsDir = path.join(dd, "episode_results");
|
|
564
|
+
const batchesByEpisode = new Map();
|
|
565
|
+
const batchDetails = [];
|
|
566
|
+
let doneBatches = 0;
|
|
567
|
+
let errorBatches = 0;
|
|
568
|
+
for (const batch of batches) {
|
|
569
|
+
const batchId = batchResultKey(batch);
|
|
570
|
+
const status = statusFromBatchState(classifyBatch(batchResultsDir, batch));
|
|
571
|
+
if (status === "success")
|
|
572
|
+
doneBatches++;
|
|
573
|
+
if (status === "error")
|
|
574
|
+
errorBatches++;
|
|
575
|
+
const detail = {
|
|
576
|
+
batch_id: batchId,
|
|
577
|
+
episode: Number(batch["episode"]),
|
|
578
|
+
part: Number(batch["part"] ?? 0),
|
|
579
|
+
status,
|
|
580
|
+
};
|
|
581
|
+
if (status === "error") {
|
|
582
|
+
try {
|
|
583
|
+
const err = readJson(batchErrorPath(batchResultsDir, batch));
|
|
584
|
+
const errorCode = strOf(err["error_code"]);
|
|
585
|
+
const errorType = strOf(err["error_type"]);
|
|
586
|
+
const message = strOf(err["message"]);
|
|
587
|
+
if (errorCode)
|
|
588
|
+
detail.error_code = errorCode;
|
|
589
|
+
if (errorType)
|
|
590
|
+
detail.error_type = errorType;
|
|
591
|
+
if (message)
|
|
592
|
+
detail.message = message;
|
|
593
|
+
}
|
|
594
|
+
catch {
|
|
595
|
+
detail.error_type = "Error";
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
batchDetails.push(detail);
|
|
599
|
+
if (!batchesByEpisode.has(detail.episode))
|
|
600
|
+
batchesByEpisode.set(detail.episode, []);
|
|
601
|
+
batchesByEpisode.get(detail.episode).push(detail);
|
|
602
|
+
}
|
|
603
|
+
const pendingBatches = batches.length - doneBatches - errorBatches;
|
|
604
|
+
const degradedSynopsisEpisodes = new Set(asList(state["summary_degraded_episodes"]).map((n) => Number(n)));
|
|
605
|
+
const episodeSnapshots = [];
|
|
606
|
+
let completeEpisodes = 0;
|
|
607
|
+
let episodeResults = 0;
|
|
608
|
+
let episodeSynopses = 0;
|
|
609
|
+
for (const episode of episodes) {
|
|
610
|
+
const episodeNum = Number(episode["episode"]);
|
|
611
|
+
const epBatches = batchesByEpisode.get(episodeNum) ?? [];
|
|
612
|
+
const batchesDone = epBatches.filter((b) => b.status === "success").length;
|
|
613
|
+
const batchesError = epBatches.filter((b) => b.status === "error").length;
|
|
614
|
+
const batchesPending = epBatches.length - batchesDone - batchesError;
|
|
615
|
+
const epResultPath = episodeResultPath(episodeResultsDir, episode);
|
|
616
|
+
const hasEpisodeResult = readableJsonFile(epResultPath);
|
|
617
|
+
if (hasEpisodeResult)
|
|
618
|
+
episodeResults++;
|
|
619
|
+
const synopsis = readEpisodeSynopsis(episodeResultsDir, episode);
|
|
620
|
+
if (synopsis)
|
|
621
|
+
episodeSynopses++;
|
|
622
|
+
const status = batchesError > 0
|
|
623
|
+
? "error"
|
|
624
|
+
: epBatches.length > 0 && batchesDone === epBatches.length && hasEpisodeResult
|
|
625
|
+
? "success"
|
|
626
|
+
: runningStage === "batch_extract" || runningStage === "episode_merge"
|
|
627
|
+
? "running"
|
|
628
|
+
: "pending";
|
|
629
|
+
if (status === "success")
|
|
630
|
+
completeEpisodes++;
|
|
631
|
+
const synopsisStatus = degradedSynopsisEpisodes.has(episodeNum)
|
|
632
|
+
? "warning"
|
|
633
|
+
: synopsis
|
|
634
|
+
? "success"
|
|
635
|
+
: Boolean(state["summary_skipped"]) && !exists(path.join(dd, "synopsis.json"))
|
|
636
|
+
? "skipped"
|
|
637
|
+
: "pending";
|
|
638
|
+
episodeSnapshots.push({
|
|
639
|
+
episode: episodeNum,
|
|
640
|
+
episode_id: episodeResultKey(episode),
|
|
641
|
+
status,
|
|
642
|
+
batches_done: batchesDone,
|
|
643
|
+
batches_total: epBatches.length,
|
|
644
|
+
batches_error: batchesError,
|
|
645
|
+
batches_pending: batchesPending,
|
|
646
|
+
synopsis_status: synopsisStatus,
|
|
647
|
+
error_batches: epBatches.filter((b) => b.status === "error").map((b) => b.batch_id),
|
|
648
|
+
});
|
|
649
|
+
}
|
|
650
|
+
const sourceStage = exists(path.join(workspace, "source.txt"))
|
|
651
|
+
? stageSnapshot("source_prepare", "success", 1, 1)
|
|
652
|
+
: stageSnapshot("source_prepare", runningStage === "source_prepare" ? "running" : "pending", 0, 1);
|
|
653
|
+
const episodePlanStage = episodes.length > 0
|
|
654
|
+
? stageSnapshot("episode_plan", "success", episodes.length, episodes.length)
|
|
655
|
+
: stageSnapshot("episode_plan", runningStage === "episode_plan" ? "running" : "pending", 0, 1);
|
|
656
|
+
const batchPlanStage = batches.length > 0
|
|
657
|
+
? stageSnapshot("batch_plan", "success", batches.length, batches.length)
|
|
658
|
+
: stageSnapshot("batch_plan", runningStage === "batch_plan" ? "running" : "pending", 0, 1);
|
|
659
|
+
const titleDeg = isDict(state["title_degraded"]) ? state["title_degraded"] : null;
|
|
660
|
+
const episodeTitlesStage = stageSnapshot("episode_titles", episodes.length === 0
|
|
661
|
+
? runningStage === "episode_titles" ? "running" : "pending"
|
|
662
|
+
: titleDeg && Number(titleDeg["count"] ?? 0) > 0 ? "warning" : "success", episodes.length > 0 ? episodes.length : 0, episodes.length > 0 ? episodes.length : 1, titleDeg && Number(titleDeg["count"] ?? 0) > 0
|
|
663
|
+
? `deterministic titles: ${asList(titleDeg["episodes"]).join(", ")}`
|
|
664
|
+
: "");
|
|
665
|
+
const batchStatus = errorBatches > 0
|
|
666
|
+
? "error"
|
|
667
|
+
: doneBatches === batches.length && batches.length > 0
|
|
668
|
+
? "success"
|
|
669
|
+
: runningStage === "batch_extract"
|
|
670
|
+
? "running"
|
|
671
|
+
: "pending";
|
|
672
|
+
const batchStage = {
|
|
673
|
+
...stageSnapshot("batch_extract", batchStatus, doneBatches, batches.length),
|
|
674
|
+
pending: pendingBatches,
|
|
675
|
+
error: errorBatches,
|
|
676
|
+
};
|
|
677
|
+
const episodeMergeStatus = errorBatches > 0
|
|
678
|
+
? "error"
|
|
679
|
+
: episodeResults === episodes.length && episodes.length > 0
|
|
680
|
+
? "success"
|
|
681
|
+
: runningStage === "episode_merge"
|
|
682
|
+
? "running"
|
|
683
|
+
: "pending";
|
|
684
|
+
const episodeMergeStage = stageSnapshot("episode_merge", episodeMergeStatus, episodeResults, episodes.length);
|
|
685
|
+
const synopsisWarning = degradedSynopsisEpisodes.size;
|
|
686
|
+
const episodeSynopsisStatus = synopsisWarning > 0
|
|
687
|
+
? "warning"
|
|
688
|
+
: episodeSynopses === episodes.length && episodes.length > 0
|
|
689
|
+
? "success"
|
|
690
|
+
: runningStage === "episode_synopsis"
|
|
691
|
+
? "running"
|
|
692
|
+
: "pending";
|
|
693
|
+
const episodeSynopsisStage = {
|
|
694
|
+
...stageSnapshot("episode_synopsis", episodeSynopsisStatus, episodeSynopses, episodes.length),
|
|
695
|
+
warning: synopsisWarning,
|
|
696
|
+
};
|
|
697
|
+
const scriptStage = fileStage("script_merge", path.join(dd, "script.initial.json"), runningStage ?? "");
|
|
698
|
+
const curationStage = fileStage("asset_curation", path.join(dd, "asset_curation.json"), runningStage ?? "");
|
|
699
|
+
const metadataStage = fileStage("metadata_extract", path.join(dd, "asset_metadata.json"), runningStage ?? "");
|
|
700
|
+
const synopsisPath = path.join(dd, "synopsis.json");
|
|
701
|
+
const summaryLine = formatSummaryStatusLine(Boolean(state["summary_skipped"]), Boolean(state["summary_degraded"]), asList(state["summary_degraded_episodes"]).map((n) => Number(n)));
|
|
702
|
+
const scriptSynopsisStatus = exists(synopsisPath)
|
|
703
|
+
? Boolean(state["summary_degraded"]) ? "warning" : "success"
|
|
704
|
+
: Boolean(state["summary_skipped"])
|
|
705
|
+
? "skipped"
|
|
706
|
+
: runningStage === "script_synopsis"
|
|
707
|
+
? "running"
|
|
708
|
+
: "pending";
|
|
709
|
+
const scriptSynopsisStage = stageSnapshot("script_synopsis", scriptSynopsisStatus, exists(synopsisPath) ? 1 : 0, 1, summaryLine ?? "");
|
|
710
|
+
const validationPath = path.join(dd, "validation.json");
|
|
711
|
+
let validationStage = fileStage("validate", validationPath, runningStage ?? "");
|
|
712
|
+
if (exists(validationPath)) {
|
|
713
|
+
const validation = readJson(validationPath);
|
|
714
|
+
validationStage = stageSnapshot("validate", Boolean(validation["passed"]) ? "success" : "warning", 1, 1);
|
|
715
|
+
}
|
|
716
|
+
const stages = [
|
|
717
|
+
sourceStage,
|
|
718
|
+
episodePlanStage,
|
|
719
|
+
batchPlanStage,
|
|
720
|
+
episodeTitlesStage,
|
|
721
|
+
batchStage,
|
|
722
|
+
episodeMergeStage,
|
|
723
|
+
episodeSynopsisStage,
|
|
724
|
+
scriptStage,
|
|
725
|
+
curationStage,
|
|
726
|
+
metadataStage,
|
|
727
|
+
scriptSynopsisStage,
|
|
728
|
+
validationStage,
|
|
729
|
+
].map((stage) => failedStage === stage.name ? { ...stage, status: "error", error: Math.max(1, stage.error) } : stage);
|
|
730
|
+
let overallStatus = "pending";
|
|
731
|
+
const maxStage = stages.reduce((max, stage) => statusPriority(stage.status) > statusPriority(max) ? stage.status : max, "success");
|
|
732
|
+
if (runStatus === "init_failed" || runStatus === "init_incomplete" || maxStage === "error")
|
|
733
|
+
overallStatus = "error";
|
|
734
|
+
else if (runStatus === "init_running" || maxStage === "running")
|
|
735
|
+
overallStatus = "running";
|
|
736
|
+
else if (maxStage === "warning")
|
|
737
|
+
overallStatus = "warning";
|
|
738
|
+
else if (stages.filter((s) => s.name !== "script_synopsis").every((s) => s.status === "success" || s.status === "warning"))
|
|
739
|
+
overallStatus = "success";
|
|
740
|
+
return {
|
|
741
|
+
overall_status: overallStatus,
|
|
742
|
+
running_stage: runningStage,
|
|
743
|
+
progress_percent: snapshotProgressPercent(stages),
|
|
744
|
+
stages,
|
|
745
|
+
episodes: episodeSnapshots,
|
|
746
|
+
batches: batchDetails,
|
|
747
|
+
};
|
|
748
|
+
}
|
|
749
|
+
function renderDirectStatusResult(snapshot) {
|
|
750
|
+
const completedEpisodes = snapshot.episodes.filter((episode) => episode.status === "success").length;
|
|
751
|
+
const result = [
|
|
752
|
+
"Overall",
|
|
753
|
+
`overall: ${statusText(snapshot.overall_status)}; current: ${snapshot.running_stage && isDirectStageName(snapshot.running_stage) ? stageLabel(snapshot.running_stage) : "-"}; progress: ${snapshot.progress_percent}%`,
|
|
754
|
+
snapshot.episodes.length > 0 ? `episodes: ${Math.round((completedEpisodes / snapshot.episodes.length) * 100)}% 完成` : "episodes: 等待中",
|
|
755
|
+
"Stages",
|
|
756
|
+
];
|
|
757
|
+
for (const stage of userStageRows(snapshot.stages)) {
|
|
758
|
+
result.push(`${stageLabel(stage.name)}: ${statusText(stage.status)} ${stage.percent}%${stage.error ? `, error ${stage.error}` : ""}${stage.warning ? `, warning ${stage.warning}` : ""}${stage.status === "pending" ? ", 等待中" : ""}${stage.message ? ` - ${stage.message}` : ""}`);
|
|
759
|
+
}
|
|
760
|
+
result.push("Episodes");
|
|
761
|
+
for (const episode of snapshot.episodes) {
|
|
762
|
+
const percent = episode.batches_total > 0 ? Math.round((episode.batches_done / episode.batches_total) * 100) : 0;
|
|
763
|
+
const errors = episode.error_batches.length > 0 ? `, error ids: ${episode.error_batches.join(",")}` : "";
|
|
764
|
+
result.push(`${episode.episode_id}: ${statusText(episode.status)} ${percent}%, story ${statusText(episode.synopsis_status)}${errors}`);
|
|
765
|
+
}
|
|
766
|
+
result.push("Details");
|
|
767
|
+
const noisyBatches = snapshot.batches.filter((batch) => batch.status !== "success");
|
|
768
|
+
if (noisyBatches.length === 0) {
|
|
769
|
+
result.push("no error/warning/pending details");
|
|
770
|
+
}
|
|
771
|
+
else {
|
|
772
|
+
for (const batch of noisyBatches.slice(0, 50)) {
|
|
773
|
+
const reason = batch.error_code || batch.error_type || batch.message;
|
|
774
|
+
result.push(`${batch.batch_id}: ${statusText(batch.status)} ep_${pad3(batch.episode)}${reason ? ` - ${reason}` : ""}`);
|
|
775
|
+
}
|
|
776
|
+
if (noisyBatches.length > 50)
|
|
777
|
+
result.push(`${noisyBatches.length - 50} more non-success details not shown`);
|
|
778
|
+
}
|
|
779
|
+
return result;
|
|
780
|
+
}
|
|
185
781
|
// Delete result/meta/error/markdown files whose unit key is no longer in the
|
|
186
782
|
// current plan (e.g. the source shed an episode). Pure function of the plan —
|
|
187
783
|
// it never inspects hashes, content, or run_state, so it can only remove units
|
|
@@ -659,6 +1255,19 @@ async function buildScriptSynopsis(provider, script, opts) {
|
|
|
659
1255
|
// command_init
|
|
660
1256
|
// ---------------------------------------------------------------------------
|
|
661
1257
|
export async function commandInit(opts) {
|
|
1258
|
+
const workspace = strOf(opts["workspace_path"] || "workspace");
|
|
1259
|
+
try {
|
|
1260
|
+
return await commandInitImpl(opts);
|
|
1261
|
+
}
|
|
1262
|
+
catch (exc) {
|
|
1263
|
+
emitFailedInitProgress(workspace);
|
|
1264
|
+
throw exc;
|
|
1265
|
+
}
|
|
1266
|
+
finally {
|
|
1267
|
+
finishInitProgress();
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
async function commandInitImpl(opts) {
|
|
662
1271
|
const sourcePathArg = strOf(opts["source_path"]);
|
|
663
1272
|
const source = sourcePathArg.startsWith("~")
|
|
664
1273
|
? path.join(process.env.HOME ?? "", sourcePathArg.slice(1))
|
|
@@ -731,6 +1340,7 @@ export async function commandInit(opts) {
|
|
|
731
1340
|
concurrency,
|
|
732
1341
|
source_path: path.resolve(source),
|
|
733
1342
|
});
|
|
1343
|
+
emitInitProgress(workspace, "source_prepare");
|
|
734
1344
|
let info;
|
|
735
1345
|
try {
|
|
736
1346
|
info = await prepareSource(source, workspace);
|
|
@@ -762,6 +1372,7 @@ export async function commandInit(opts) {
|
|
|
762
1372
|
const sourceText = readText(sourceTextPath);
|
|
763
1373
|
const manifest = makeSourceManifest(source, sourceTextPath, info);
|
|
764
1374
|
updateRunState(workspace, { status: "init_running", init_stage: "episode_plan" });
|
|
1375
|
+
emitInitProgress(workspace, "episode_plan");
|
|
765
1376
|
let plan;
|
|
766
1377
|
try {
|
|
767
1378
|
plan = buildEpisodePlan(sourceText);
|
|
@@ -776,12 +1387,12 @@ export async function commandInit(opts) {
|
|
|
776
1387
|
nextSteps: ["Inspect workspace/source.txt, fix the source file, and rerun init."],
|
|
777
1388
|
});
|
|
778
1389
|
}
|
|
779
|
-
updateRunState(workspace, { status: "init_running", init_stage: "
|
|
1390
|
+
updateRunState(workspace, { status: "init_running", init_stage: "episode_titles" });
|
|
780
1391
|
const capabilities = providerCapabilities(providerName);
|
|
781
1392
|
if (capabilities && !capabilities.extractBatch && !capabilities.extractEpisode) {
|
|
782
1393
|
throw initFailedReport(workspace, {
|
|
783
1394
|
title: "INIT FAILED: provider lacks batch extraction",
|
|
784
|
-
stage: "
|
|
1395
|
+
stage: "episode_titles",
|
|
785
1396
|
required: ["provider.extractBatch or provider.extractEpisode"],
|
|
786
1397
|
received: [`--provider ${providerName}`],
|
|
787
1398
|
nextSteps: ["Use --provider anthropic or --provider mock for direct init."],
|
|
@@ -795,7 +1406,7 @@ export async function commandInit(opts) {
|
|
|
795
1406
|
if (exc instanceof CliError) {
|
|
796
1407
|
updateRunState(workspace, {
|
|
797
1408
|
status: "init_failed",
|
|
798
|
-
init_stage: "
|
|
1409
|
+
init_stage: "episode_titles",
|
|
799
1410
|
last_error: { title: exc.title, received: exc.received, failed_at: checkpointTimestamp() },
|
|
800
1411
|
});
|
|
801
1412
|
}
|
|
@@ -804,13 +1415,14 @@ export async function commandInit(opts) {
|
|
|
804
1415
|
if (!provider.extractBatch && !provider.extractEpisode) {
|
|
805
1416
|
throw initFailedReport(workspace, {
|
|
806
1417
|
title: "INIT FAILED: provider lacks batch extraction",
|
|
807
|
-
stage: "
|
|
1418
|
+
stage: "episode_titles",
|
|
808
1419
|
required: ["provider.extractBatch or provider.extractEpisode"],
|
|
809
1420
|
received: [`--provider ${providerName}`],
|
|
810
1421
|
nextSteps: ["Use --provider anthropic or --provider mock for direct init."],
|
|
811
1422
|
});
|
|
812
1423
|
}
|
|
813
1424
|
updateRunState(workspace, { status: "init_running", init_stage: "episode_titles" });
|
|
1425
|
+
emitInitProgress(workspace, "episode_titles");
|
|
814
1426
|
const titleDegraded = await enrichEpisodeTitlesChunked(workspace, sourceText, plan, provider, concurrency);
|
|
815
1427
|
let batchPlan;
|
|
816
1428
|
try {
|
|
@@ -834,6 +1446,7 @@ export async function commandInit(opts) {
|
|
|
834
1446
|
writeJson(path.join(dd, "source_manifest.json"), manifest);
|
|
835
1447
|
writeJson(path.join(dd, "episode_plan.json"), plan);
|
|
836
1448
|
writeJson(path.join(dd, "batch_plan.json"), batchPlan);
|
|
1449
|
+
emitInitProgress(workspace, "batch_plan", "success");
|
|
837
1450
|
const episodeResultsDir = path.join(dd, "episode_results");
|
|
838
1451
|
const batchResultsDir = path.join(dd, "batch_results");
|
|
839
1452
|
fs.mkdirSync(episodeResultsDir, { recursive: true });
|
|
@@ -900,6 +1513,7 @@ export async function commandInit(opts) {
|
|
|
900
1513
|
episode_total: totalEpisodes,
|
|
901
1514
|
batch_total: totalBatches,
|
|
902
1515
|
});
|
|
1516
|
+
emitInitProgress(workspace, "batch_extract");
|
|
903
1517
|
// Persist each batch the moment it finishes (inside the worker), NOT after the
|
|
904
1518
|
// whole pool drains. Per-unit files have no shared index, so concurrent writes
|
|
905
1519
|
// are safe — and batch_results/ fills up live, so a crash/kill mid-run keeps
|
|
@@ -911,10 +1525,12 @@ export async function commandInit(opts) {
|
|
|
911
1525
|
const errorPath = batchErrorPath(batchResultsDir, batch);
|
|
912
1526
|
if (exists(errorPath))
|
|
913
1527
|
deletePath(errorPath);
|
|
1528
|
+
emitInitProgress(workspace, "batch_extract");
|
|
914
1529
|
return true;
|
|
915
1530
|
}
|
|
916
1531
|
catch (error) {
|
|
917
1532
|
writeBatchFailure(batchResultsDir, batch, error);
|
|
1533
|
+
emitInitProgress(workspace, "batch_extract");
|
|
918
1534
|
return false;
|
|
919
1535
|
}
|
|
920
1536
|
});
|
|
@@ -927,6 +1543,15 @@ export async function commandInit(opts) {
|
|
|
927
1543
|
// reported; assembling them is left to an explicit re-run.
|
|
928
1544
|
const results = [];
|
|
929
1545
|
const incompleteEpisodes = [];
|
|
1546
|
+
updateRunState(workspace, {
|
|
1547
|
+
status: "init_running",
|
|
1548
|
+
init_stage: "episode_merge",
|
|
1549
|
+
episode_total: totalEpisodes,
|
|
1550
|
+
batch_total: totalBatches,
|
|
1551
|
+
batch_completed: doneBatches,
|
|
1552
|
+
batch_failed: erroredBatches,
|
|
1553
|
+
});
|
|
1554
|
+
emitInitProgress(workspace, "episode_merge");
|
|
930
1555
|
try {
|
|
931
1556
|
// Assemble every complete episode (incomplete ones are reported and skipped).
|
|
932
1557
|
for (const episode of asList(plan["episodes"])) {
|
|
@@ -946,9 +1571,12 @@ export async function commandInit(opts) {
|
|
|
946
1571
|
validateEpisodeExtractionQuality(sourceText, episode, merged);
|
|
947
1572
|
results.push(merged); // complete episodes only; carries `episode` for the write below
|
|
948
1573
|
}
|
|
1574
|
+
emitInitProgress(workspace, "episode_merge", incompleteEpisodes.length > 0 ? "warning" : "success");
|
|
949
1575
|
// Episode-level synopsis (reduce-1): resolve each result's synopsis in place
|
|
950
1576
|
// (passthrough / reuse / skip / concurrent LLM stitch). Reads prior `syn`
|
|
951
1577
|
// BEFORE the persist loop below overwrites ep_NNN.json.
|
|
1578
|
+
updateRunState(workspace, { status: "init_running", init_stage: "episode_synopsis" });
|
|
1579
|
+
emitInitProgress(workspace, "episode_synopsis");
|
|
952
1580
|
summaryDegradedEpisodes.push(...await resolveEpisodeSynopses(provider, results, {
|
|
953
1581
|
resultsDir: episodeResultsDir,
|
|
954
1582
|
resummarize,
|
|
@@ -964,6 +1592,7 @@ export async function commandInit(opts) {
|
|
|
964
1592
|
if (exists(errPath))
|
|
965
1593
|
deletePath(errPath);
|
|
966
1594
|
}
|
|
1595
|
+
emitInitProgress(workspace, "episode_synopsis", summaryDegradedEpisodes.length > 0 ? "warning" : "success");
|
|
967
1596
|
}
|
|
968
1597
|
catch (exc) {
|
|
969
1598
|
const e = exc;
|
|
@@ -989,12 +1618,13 @@ export async function commandInit(opts) {
|
|
|
989
1618
|
batch_failed: erroredBatches,
|
|
990
1619
|
last_error: { title: "INIT INCOMPLETE: Some episodes have unextracted batches", failed_at: checkpointTimestamp() },
|
|
991
1620
|
});
|
|
1621
|
+
emitInitProgress(workspace, "batch_extract", "error");
|
|
992
1622
|
const report = {
|
|
993
1623
|
title: "INIT INCOMPLETE: Some episodes have unextracted batches",
|
|
994
1624
|
result: [
|
|
995
1625
|
`episodes: ${results.length}/${totalEpisodes} complete`,
|
|
996
1626
|
`incomplete episodes: ${incompleteEpisodes.join(", ")}`,
|
|
997
|
-
`
|
|
1627
|
+
`content progress: ${totalBatches > 0 ? Math.round((doneBatches / totalBatches) * 100) : 0}% done, ${erroredBatches} error`,
|
|
998
1628
|
...(titleDegraded.length > 0 ? [`titles degraded (deterministic): ${titleDegraded.join(", ")}`] : []),
|
|
999
1629
|
`provider: ${providerName}`,
|
|
1000
1630
|
],
|
|
@@ -1007,16 +1637,16 @@ export async function commandInit(opts) {
|
|
|
1007
1637
|
path.join(dd, "run_state.json"),
|
|
1008
1638
|
],
|
|
1009
1639
|
next: [
|
|
1010
|
-
"Run `direct status` to see
|
|
1011
|
-
"Re-run one episode with `direct init --episodes <n>`,
|
|
1012
|
-
"Errored
|
|
1640
|
+
"Run `direct status` to see episode progress.",
|
|
1641
|
+
"Re-run one episode with `direct init --episodes <n>`, or all errors with `--retry-errors`.",
|
|
1642
|
+
"Errored units are left untouched on a plain rerun; select them to retry.",
|
|
1013
1643
|
],
|
|
1014
1644
|
};
|
|
1015
1645
|
return [report, EXIT_RUNTIME];
|
|
1016
1646
|
}
|
|
1017
1647
|
updateRunState(workspace, {
|
|
1018
1648
|
status: "init_running",
|
|
1019
|
-
init_stage: "
|
|
1649
|
+
init_stage: "script_merge",
|
|
1020
1650
|
episode_total: totalEpisodes,
|
|
1021
1651
|
episode_completed: results.length,
|
|
1022
1652
|
incomplete_episodes: [],
|
|
@@ -1027,7 +1657,7 @@ export async function commandInit(opts) {
|
|
|
1027
1657
|
});
|
|
1028
1658
|
let script;
|
|
1029
1659
|
try {
|
|
1030
|
-
|
|
1660
|
+
emitInitProgress(workspace, "script_merge");
|
|
1031
1661
|
script = mergeEpisodeResults(results, strOf(info["projectName"]) || path.basename(source, path.extname(source)));
|
|
1032
1662
|
}
|
|
1033
1663
|
catch (exc) {
|
|
@@ -1043,6 +1673,7 @@ export async function commandInit(opts) {
|
|
|
1043
1673
|
}
|
|
1044
1674
|
try {
|
|
1045
1675
|
updateRunState(workspace, { status: "init_running", init_stage: "asset_curation" });
|
|
1676
|
+
emitInitProgress(workspace, "asset_curation");
|
|
1046
1677
|
// Location merge is deterministic-only: actor/prop pruning runs from scene
|
|
1047
1678
|
// counts and every location is kept. The previous provider location-merge
|
|
1048
1679
|
// call was removed (single large streaming request kept dropping the gateway
|
|
@@ -1063,8 +1694,10 @@ export async function commandInit(opts) {
|
|
|
1063
1694
|
updates: { episode_completed: results.length },
|
|
1064
1695
|
});
|
|
1065
1696
|
}
|
|
1697
|
+
emitInitProgress(workspace, "asset_curation", "success");
|
|
1066
1698
|
try {
|
|
1067
1699
|
updateRunState(workspace, { status: "init_running", init_stage: "metadata_extract" });
|
|
1700
|
+
emitInitProgress(workspace, "metadata_extract");
|
|
1068
1701
|
let metadata = provider.extractMetadata ? await provider.extractMetadata(sourceText, script) : {};
|
|
1069
1702
|
if (!isDict(metadata))
|
|
1070
1703
|
metadata = {};
|
|
@@ -1094,13 +1727,15 @@ export async function commandInit(opts) {
|
|
|
1094
1727
|
updates: { episode_completed: results.length },
|
|
1095
1728
|
});
|
|
1096
1729
|
}
|
|
1730
|
+
emitInitProgress(workspace, "metadata_extract", "success");
|
|
1097
1731
|
// Whole-script overview (reduce-2). Soft: a non-essential concept enhancement,
|
|
1098
1732
|
// the opposite of batch extraction's hard-fail policy — failures degrade the
|
|
1099
1733
|
// overview (recorded as summary_degraded + a sanitized reason) and NEVER block init.
|
|
1100
1734
|
let summaryFullDegraded = false;
|
|
1101
1735
|
let summaryDegradeReason = "";
|
|
1102
1736
|
try {
|
|
1103
|
-
updateRunState(workspace, { status: "init_running", init_stage: "
|
|
1737
|
+
updateRunState(workspace, { status: "init_running", init_stage: "script_synopsis" });
|
|
1738
|
+
emitInitProgress(workspace, "script_synopsis");
|
|
1104
1739
|
const summary = await buildScriptSynopsis(provider, script, {
|
|
1105
1740
|
dd,
|
|
1106
1741
|
thresholdChars: summaryThresholdChars,
|
|
@@ -1120,7 +1755,15 @@ export async function commandInit(opts) {
|
|
|
1120
1755
|
const summaryDegraded = summaryFullDegraded || summaryDegradedEpisodes.length > 0;
|
|
1121
1756
|
const scriptPath = path.join(dd, "script.initial.json");
|
|
1122
1757
|
writeJson(scriptPath, script);
|
|
1758
|
+
updateRunState(workspace, {
|
|
1759
|
+
summary_skipped: skipSummary,
|
|
1760
|
+
summary_degraded: summaryDegraded,
|
|
1761
|
+
summary_degraded_episodes: summaryDegradedEpisodes,
|
|
1762
|
+
summary_degraded_reason: summaryDegradeReason || null,
|
|
1763
|
+
});
|
|
1764
|
+
emitInitProgress(workspace, "script_synopsis", summaryDegraded ? "warning" : skipSummary ? "skipped" : "success");
|
|
1123
1765
|
updateRunState(workspace, { status: "init_running", init_stage: "validate" });
|
|
1766
|
+
emitInitProgress(workspace, "validate");
|
|
1124
1767
|
let validation;
|
|
1125
1768
|
try {
|
|
1126
1769
|
validation = validateScript(workspace, scriptPath);
|
|
@@ -1166,6 +1809,7 @@ export async function commandInit(opts) {
|
|
|
1166
1809
|
summary_degraded_reason: summaryDegradeReason || null,
|
|
1167
1810
|
last_error: null,
|
|
1168
1811
|
});
|
|
1812
|
+
emitInitProgress(workspace, "validate", passed ? "success" : "warning");
|
|
1169
1813
|
const title = passed
|
|
1170
1814
|
? "INIT COMPLETE: Initial script ready"
|
|
1171
1815
|
: "INIT NEEDS AGENT: Initial script written with repair issues";
|
|
@@ -1180,7 +1824,7 @@ export async function commandInit(opts) {
|
|
|
1180
1824
|
`validation: ${passed ? "passed" : "needs repair"}`,
|
|
1181
1825
|
`provider: ${providerName}`,
|
|
1182
1826
|
`episodes: ${results.length}/${totalEpisodes} complete`,
|
|
1183
|
-
`
|
|
1827
|
+
`content progress: ${totalBatches > 0 ? Math.round((doneBatches / totalBatches) * 100) : 0}% done`,
|
|
1184
1828
|
...(titleDegraded.length > 0 ? [`titles degraded (deterministic): ${titleDegraded.join(", ")}`] : []),
|
|
1185
1829
|
...(summaryLine ? [summaryLine] : []),
|
|
1186
1830
|
],
|
|
@@ -1219,7 +1863,8 @@ export function commandStatus(opts) {
|
|
|
1219
1863
|
const dd = directDir(workspace);
|
|
1220
1864
|
const episodePlanPath = path.join(dd, "episode_plan.json");
|
|
1221
1865
|
const batchPlanPath = path.join(dd, "batch_plan.json");
|
|
1222
|
-
|
|
1866
|
+
const hasTrace = exists(path.join(workspace, "source.txt")) || Object.keys(readRunState(workspace)).length > 0;
|
|
1867
|
+
if (!hasTrace && (!exists(episodePlanPath) || !exists(batchPlanPath))) {
|
|
1223
1868
|
throw new CliError("STATUS BLOCKED: Plan not found", "Plan not found.", {
|
|
1224
1869
|
exitCode: EXIT_INPUT,
|
|
1225
1870
|
required: ["episode_plan.json and batch_plan.json"],
|
|
@@ -1227,90 +1872,26 @@ export function commandStatus(opts) {
|
|
|
1227
1872
|
nextSteps: ["Run scriptctl direct init first."],
|
|
1228
1873
|
});
|
|
1229
1874
|
}
|
|
1230
|
-
const
|
|
1231
|
-
const
|
|
1232
|
-
const
|
|
1233
|
-
const
|
|
1234
|
-
|
|
1235
|
-
let errorTotal = 0;
|
|
1236
|
-
for (const batch of batches) {
|
|
1237
|
-
const epNum = Number(batch["episode"]);
|
|
1238
|
-
if (!byEp.has(epNum))
|
|
1239
|
-
byEp.set(epNum, { total: 0, done: 0, error: 0, pending: 0, errorKeys: [], reasons: new Set() });
|
|
1240
|
-
const st = byEp.get(epNum);
|
|
1241
|
-
st.total++;
|
|
1242
|
-
const state = classifyBatch(batchResultsDir, batch);
|
|
1243
|
-
if (state === "done") {
|
|
1244
|
-
st.done++;
|
|
1245
|
-
doneTotal++;
|
|
1246
|
-
continue;
|
|
1247
|
-
}
|
|
1248
|
-
if (state === "error") {
|
|
1249
|
-
st.error++;
|
|
1250
|
-
errorTotal++;
|
|
1251
|
-
st.errorKeys.push(batchResultKey(batch));
|
|
1252
|
-
try {
|
|
1253
|
-
const err = readJson(batchErrorPath(batchResultsDir, batch));
|
|
1254
|
-
st.reasons.add(strOf(err["error_code"] || err["error_type"] || "Error"));
|
|
1255
|
-
}
|
|
1256
|
-
catch {
|
|
1257
|
-
st.reasons.add("Error");
|
|
1258
|
-
}
|
|
1259
|
-
}
|
|
1260
|
-
else {
|
|
1261
|
-
st.pending++;
|
|
1262
|
-
}
|
|
1263
|
-
}
|
|
1264
|
-
const lines = [];
|
|
1265
|
-
let completeEpisodes = 0;
|
|
1266
|
-
const errorEpisodes = [];
|
|
1267
|
-
for (const ep of episodes) {
|
|
1268
|
-
const epNum = Number(ep["episode"]);
|
|
1269
|
-
const st = byEp.get(epNum) ?? { total: 0, done: 0, error: 0, pending: 0, errorKeys: [], reasons: new Set() };
|
|
1270
|
-
let label;
|
|
1271
|
-
if (st.total > 0 && st.done === st.total) {
|
|
1272
|
-
label = "done";
|
|
1273
|
-
completeEpisodes++;
|
|
1274
|
-
}
|
|
1275
|
-
else if (st.error > 0) {
|
|
1276
|
-
label = `ERROR (${st.done}/${st.total} done, ${st.error} err: ${[...st.reasons].join(",")}) [${st.errorKeys.join(",")}]`;
|
|
1277
|
-
errorEpisodes.push(epNum);
|
|
1278
|
-
}
|
|
1279
|
-
else {
|
|
1280
|
-
label = `pending (${st.done}/${st.total} done)`;
|
|
1281
|
-
}
|
|
1282
|
-
lines.push(`${episodeResultKey(ep)} ${label}`);
|
|
1283
|
-
}
|
|
1284
|
-
// Surface degraded titles (deterministic fallback) recorded by init — they are
|
|
1285
|
-
// not a failure but the agent should know.
|
|
1286
|
-
const state = readRunState(workspace);
|
|
1287
|
-
const titleDeg = isDict(state["title_degraded"]) ? state["title_degraded"] : null;
|
|
1288
|
-
const result = [
|
|
1289
|
-
`episodes: ${completeEpisodes}/${episodes.length} done`,
|
|
1290
|
-
`batches: ${doneTotal}/${batches.length} done, ${errorTotal} error`,
|
|
1291
|
-
`error episodes: ${errorEpisodes.length === 0 ? "-" : errorEpisodes.join(", ")}`,
|
|
1292
|
-
];
|
|
1293
|
-
if (titleDeg && Number(titleDeg["count"] ?? 0) > 0) {
|
|
1294
|
-
result.push(`titles degraded (deterministic): ${asList(titleDeg["episodes"]).join(", ")}`);
|
|
1295
|
-
}
|
|
1296
|
-
// Surface synopsis state recorded by init: skipped or degraded summaries mean
|
|
1297
|
-
// the overview / some episode synopses are missing or fell back to joined
|
|
1298
|
-
// beats — an agent gating on status should know it isn't a complete summary.
|
|
1299
|
-
// Same formatter as init's report so the two never word it differently.
|
|
1300
|
-
const summaryLine = formatSummaryStatusLine(Boolean(state["summary_skipped"]), Boolean(state["summary_degraded"]), asList(state["summary_degraded_episodes"]).map((n) => Number(n)));
|
|
1301
|
-
if (summaryLine)
|
|
1302
|
-
result.push(summaryLine);
|
|
1303
|
-
result.push(...lines);
|
|
1304
|
-
const allDone = completeEpisodes === episodes.length;
|
|
1875
|
+
const snapshot = buildDirectStatusSnapshot(workspace);
|
|
1876
|
+
const result = renderDirectStatusResult(snapshot);
|
|
1877
|
+
const blockingStatuses = new Set(["error", "pending", "running"]);
|
|
1878
|
+
const allDone = !snapshot.stages.some((stage) => blockingStatuses.has(stage.status));
|
|
1879
|
+
const firstErrorEpisode = snapshot.episodes.find((episode) => episode.status === "error");
|
|
1305
1880
|
const report = {
|
|
1306
1881
|
title: "DIRECT STATUS",
|
|
1307
1882
|
result,
|
|
1308
|
-
artifacts: [
|
|
1309
|
-
|
|
1310
|
-
|
|
1883
|
+
artifacts: [path.join(dd, "batch_results"), path.join(dd, "episode_results"), path.join(dd, "run_state.json")],
|
|
1884
|
+
overall_status: snapshot.overall_status,
|
|
1885
|
+
running_stage: snapshot.running_stage,
|
|
1886
|
+
progress_percent: snapshot.progress_percent,
|
|
1887
|
+
stages: snapshot.stages,
|
|
1888
|
+
episodes: snapshot.episodes,
|
|
1889
|
+
batches: snapshot.batches,
|
|
1890
|
+
next: firstErrorEpisode
|
|
1891
|
+
? [`Re-run an errored episode with: direct init --episodes ${firstErrorEpisode.episode} (or --retry-errors).`]
|
|
1311
1892
|
: allDone
|
|
1312
1893
|
? ["All episodes complete."]
|
|
1313
|
-
: ["Pending
|
|
1894
|
+
: ["Pending work remains — re-run: direct init (resume) or --episodes <N>."],
|
|
1314
1895
|
};
|
|
1315
1896
|
// Non-zero when the workspace is not fully extracted, so automation can gate
|
|
1316
1897
|
// on the exit code instead of scraping stdout.
|