@lingjingai/scriptctl 0.28.4 → 0.29.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/changes/0.29.0.md +17 -0
- package/dist/cli.js +25 -3
- package/dist/cli.js.map +1 -1
- package/dist/common.d.ts +1 -0
- package/dist/common.js +3 -0
- package/dist/common.js.map +1 -1
- package/dist/domain/direct/runner.d.ts +23 -0
- package/dist/domain/direct/runner.js +72 -0
- package/dist/domain/direct/runner.js.map +1 -0
- package/dist/domain/direct/stage.d.ts +86 -0
- package/dist/domain/direct/stage.js +131 -0
- package/dist/domain/direct/stage.js.map +1 -0
- package/dist/domain/direct/stages/asset-curation.d.ts +2 -0
- package/dist/domain/direct/stages/asset-curation.js +9 -0
- package/dist/domain/direct/stages/asset-curation.js.map +1 -0
- package/dist/domain/direct/stages/batch-extract.d.ts +2 -0
- package/dist/domain/direct/stages/batch-extract.js +12 -0
- package/dist/domain/direct/stages/batch-extract.js.map +1 -0
- package/dist/domain/direct/stages/batch-plan.d.ts +2 -0
- package/dist/domain/direct/stages/batch-plan.js +9 -0
- package/dist/domain/direct/stages/batch-plan.js.map +1 -0
- package/dist/domain/direct/stages/episode-merge.d.ts +2 -0
- package/dist/domain/direct/stages/episode-merge.js +11 -0
- package/dist/domain/direct/stages/episode-merge.js.map +1 -0
- package/dist/domain/direct/stages/episode-plan.d.ts +2 -0
- package/dist/domain/direct/stages/episode-plan.js +9 -0
- package/dist/domain/direct/stages/episode-plan.js.map +1 -0
- package/dist/domain/direct/stages/episode-synopsis.d.ts +2 -0
- package/dist/domain/direct/stages/episode-synopsis.js +12 -0
- package/dist/domain/direct/stages/episode-synopsis.js.map +1 -0
- package/dist/domain/direct/stages/episode-titles.d.ts +2 -0
- package/dist/domain/direct/stages/episode-titles.js +12 -0
- package/dist/domain/direct/stages/episode-titles.js.map +1 -0
- package/dist/domain/direct/stages/index.d.ts +17 -0
- package/dist/domain/direct/stages/index.js +37 -0
- package/dist/domain/direct/stages/index.js.map +1 -0
- package/dist/domain/direct/stages/metadata.d.ts +2 -0
- package/dist/domain/direct/stages/metadata.js +11 -0
- package/dist/domain/direct/stages/metadata.js.map +1 -0
- package/dist/domain/direct/stages/script-merge.d.ts +2 -0
- package/dist/domain/direct/stages/script-merge.js +15 -0
- package/dist/domain/direct/stages/script-merge.js.map +1 -0
- package/dist/domain/direct/stages/script-synopsis.d.ts +2 -0
- package/dist/domain/direct/stages/script-synopsis.js +12 -0
- package/dist/domain/direct/stages/script-synopsis.js.map +1 -0
- package/dist/domain/direct/stages/source-prepare.d.ts +2 -0
- package/dist/domain/direct/stages/source-prepare.js +9 -0
- package/dist/domain/direct/stages/source-prepare.js.map +1 -0
- package/dist/domain/direct/stages/validate.d.ts +2 -0
- package/dist/domain/direct/stages/validate.js +10 -0
- package/dist/domain/direct/stages/validate.js.map +1 -0
- package/dist/domain/direct-core.js +15 -1
- package/dist/domain/direct-core.js.map +1 -1
- package/dist/help-text.js +1 -1
- package/dist/infra/local-script-output-store.d.ts +2 -1
- package/dist/infra/local-script-output-store.js +11 -0
- package/dist/infra/local-script-output-store.js.map +1 -1
- package/dist/infra/providers.js +13 -4
- package/dist/infra/providers.js.map +1 -1
- package/dist/infra/script-output-api.d.ts +27 -4
- package/dist/infra/script-output-api.js +130 -106
- package/dist/infra/script-output-api.js.map +1 -1
- package/dist/infra/script-output-store.d.ts +24 -0
- package/dist/infra/script-output-store.js.map +1 -1
- package/dist/usecases/direct.d.ts +14 -0
- package/dist/usecases/direct.js +701 -358
- package/dist/usecases/direct.js.map +1 -1
- package/dist/usecases/parse.js +2 -2
- package/dist/usecases/parse.js.map +1 -1
- package/dist/usecases/script/actions.js.map +1 -1
- package/dist/usecases/script/actor.js.map +1 -1
- package/dist/usecases/script/actors.js.map +1 -1
- package/dist/usecases/script/add-actor.js.map +1 -1
- package/dist/usecases/script/add-episode.js.map +1 -1
- package/dist/usecases/script/add-location.js.map +1 -1
- package/dist/usecases/script/add-prop.js.map +1 -1
- package/dist/usecases/script/add-speaker.js.map +1 -1
- package/dist/usecases/script/alias.js.map +1 -1
- package/dist/usecases/script/assets.js.map +1 -1
- package/dist/usecases/script/context.js.map +1 -1
- package/dist/usecases/script/create.js.map +1 -1
- package/dist/usecases/script/delete.js.map +1 -1
- package/dist/usecases/script/describe.js.map +1 -1
- package/dist/usecases/script/dialogue.js.map +1 -1
- package/dist/usecases/script/do.js.map +1 -1
- package/dist/usecases/script/episodes.js.map +1 -1
- package/dist/usecases/script/export.js.map +1 -1
- package/dist/usecases/script/insert.js.map +1 -1
- package/dist/usecases/script/issues.js.map +1 -1
- package/dist/usecases/script/lib.js.map +1 -1
- package/dist/usecases/script/locations.js.map +1 -1
- package/dist/usecases/script/merge.js.map +1 -1
- package/dist/usecases/script/move.js.map +1 -1
- package/dist/usecases/script/overlap.js.map +1 -1
- package/dist/usecases/script/props.js.map +1 -1
- package/dist/usecases/script/refs.js.map +1 -1
- package/dist/usecases/script/rename.js.map +1 -1
- package/dist/usecases/script/replace.js.map +1 -1
- package/dist/usecases/script/role.js.map +1 -1
- package/dist/usecases/script/scenes.js.map +1 -1
- package/dist/usecases/script/script-patch.js.map +1 -1
- package/dist/usecases/script/script-validate.js.map +1 -1
- package/dist/usecases/script/session.d.ts +1 -0
- package/dist/usecases/script/session.js +15 -4
- package/dist/usecases/script/session.js.map +1 -1
- package/dist/usecases/script/speakers.js.map +1 -1
- package/dist/usecases/script/split.js.map +1 -1
- package/dist/usecases/script/state-add.js.map +1 -1
- package/dist/usecases/script/state-change.js.map +1 -1
- package/dist/usecases/script/state-delete.js.map +1 -1
- package/dist/usecases/script/state-rename.js.map +1 -1
- package/dist/usecases/script/summary.js.map +1 -1
- package/dist/usecases/script/transition.js.map +1 -1
- package/dist/usecases/script/type.js.map +1 -1
- package/dist/usecases/script/worldview.js.map +1 -1
- package/package.json +2 -1
package/dist/usecases/direct.js
CHANGED
|
@@ -1,10 +1,18 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
-
import { CliError, DEFAULT_BATCH_MAX_CHARS, DEFAULT_BATCH_MIN_LINES, DEFAULT_BATCH_MODE, DEFAULT_BATCH_TARGET_LINES,
|
|
4
|
-
import { compactBatchResult, compactEpisodeResult, buildBatchPlan, buildEpisodePlan, DEFAULT_TITLE_CHUNK_SIZE, chunkEpisodesNeedingTitles, applyEpisodeTitlesToPlan, cleanSynopsisList, episodeTitleResponseMap, extractBatchWithRecovery, mergeEpisodeResultsWithReport, normalizeEpisodeResult, planSynopsisReduction, readCarriedSynopsis, sortDeep, uniqueAdd, validateEpisodeExtractionQuality, _md_push_asset, curateScriptAssets, applyMetadataToScript, } from "../domain/direct-core.js";
|
|
3
|
+
import { CliError, DEFAULT_BATCH_MAX_CHARS, DEFAULT_BATCH_MIN_LINES, DEFAULT_BATCH_MODE, DEFAULT_BATCH_TARGET_LINES, DEFAULT_DIRECT_CONCURRENCY, DEFAULT_MODEL, DEFAULT_PROVIDER, DEFAULT_SUMMARY_GROUP_SIZE, DEFAULT_SUMMARY_REDUCE_THRESHOLD_CHARS, EXIT_INPUT, EXIT_NEEDS_AGENT, EXIT_OK, EXIT_RUNTIME, EXIT_USAGE, SUPPORTED_EXTS, deletePath, directDir, exists, fmtId, readJson, readText, sha256Text, writeJson, } from "../common.js";
|
|
4
|
+
import { compactBatchResult, compactEpisodeResult, expandCompactEpisodeResult, buildBatchPlan, buildEpisodePlan, DEFAULT_TITLE_CHUNK_SIZE, chunkEpisodesNeedingTitles, applyEpisodeTitlesToPlan, cleanSynopsisList, episodeTitleResponseMap, extractBatchWithRecovery, mergeEpisodeResultsWithReport, normalizeEpisodeResult, planSynopsisReduction, readCarriedSynopsis, sortDeep, uniqueAdd, validateEpisodeExtractionQuality, _md_push_asset, curateScriptAssets, applyMetadataToScript, } from "../domain/direct-core.js";
|
|
5
5
|
import { validateScript } from "../domain/script/validate.js";
|
|
6
6
|
import { makeProvider, providerCapabilities } from "../infra/providers.js";
|
|
7
7
|
import { makeSourceManifest, prepareSource, } from "../infra/converters.js";
|
|
8
|
+
import { Workspace, StageIncomplete, emptyRunState, stageRunStateName, } from "../domain/direct/stage.js";
|
|
9
|
+
// Circular import: runner.ts → stages/index.ts → each stage file → THIS module
|
|
10
|
+
// (the stageXxx impls live here). It's safe because every reference below
|
|
11
|
+
// (runStages / runOne / STAGES) is used only inside function bodies that run at
|
|
12
|
+
// CALL time (commandInitImpl, commandRun), never at module-load time — so the
|
|
13
|
+
// cycle is fully resolved before any of them executes.
|
|
14
|
+
import { runStages, runOne } from "../domain/direct/runner.js";
|
|
15
|
+
import { STAGES } from "../domain/direct/stages/index.js";
|
|
8
16
|
function strOf(v) {
|
|
9
17
|
if (v === null || v === undefined)
|
|
10
18
|
return "";
|
|
@@ -994,6 +1002,21 @@ function initFailedReport(workspace, opts) {
|
|
|
994
1002
|
nextSteps: opts.nextSteps,
|
|
995
1003
|
});
|
|
996
1004
|
}
|
|
1005
|
+
// Catch→initFailedReport for the deterministic stages: format the raw exception
|
|
1006
|
+
// into a single-line `received` and write the failure run_state, so the cast +
|
|
1007
|
+
// `slice(0,160)` + updates boilerplate lives here instead of being hand-copied
|
|
1008
|
+
// into every stage's catch block.
|
|
1009
|
+
function stageFailure(workspace, exc, opts) {
|
|
1010
|
+
const e = exc;
|
|
1011
|
+
throw initFailedReport(workspace, {
|
|
1012
|
+
title: opts.title,
|
|
1013
|
+
stage: opts.stage,
|
|
1014
|
+
required: opts.required,
|
|
1015
|
+
received: [`${e?.name ?? "Error"}: ${(e?.message ?? "").slice(0, 160)}`],
|
|
1016
|
+
nextSteps: opts.nextSteps,
|
|
1017
|
+
...(opts.updates ? { updates: opts.updates } : {}),
|
|
1018
|
+
});
|
|
1019
|
+
}
|
|
997
1020
|
// ---------------------------------------------------------------------------
|
|
998
1021
|
// Concurrency limiter (mirrors ThreadPoolExecutor)
|
|
999
1022
|
// ---------------------------------------------------------------------------
|
|
@@ -1303,7 +1326,11 @@ export async function commandInit(opts) {
|
|
|
1303
1326
|
finishInitProgress();
|
|
1304
1327
|
}
|
|
1305
1328
|
}
|
|
1306
|
-
|
|
1329
|
+
// Parse `direct init` / `direct run` options into the typed DirectRunOptions the
|
|
1330
|
+
// stage pipeline consumes. Single source of truth for flag parsing + validation
|
|
1331
|
+
// so init and run can't drift on defaults (--concurrency / batch sizing / the
|
|
1332
|
+
// selection flags). Throws the same USAGE/INPUT CliErrors the god-command did.
|
|
1333
|
+
function parseDirectRunOptions(opts) {
|
|
1307
1334
|
const sourcePathArg = strOf(opts["source_path"]);
|
|
1308
1335
|
const source = sourcePathArg.startsWith("~")
|
|
1309
1336
|
? path.join(process.env.HOME ?? "", sourcePathArg.slice(1))
|
|
@@ -1327,7 +1354,7 @@ async function commandInitImpl(opts) {
|
|
|
1327
1354
|
const skipMetadata = Boolean(opts["skip_metadata"]);
|
|
1328
1355
|
const resummarize = Boolean(opts["resummarize"]);
|
|
1329
1356
|
const summaryThresholdChars = parsePositiveIntOption(opts["summary_threshold_chars"], "--summary-threshold-chars", "USAGE ERROR: Invalid summary threshold", DEFAULT_SUMMARY_REDUCE_THRESHOLD_CHARS);
|
|
1330
|
-
const concurrency = parsePositiveIntOption(opts["concurrency"], "--concurrency", "USAGE ERROR: Invalid concurrency",
|
|
1357
|
+
const concurrency = parsePositiveIntOption(opts["concurrency"], "--concurrency", "USAGE ERROR: Invalid concurrency", DEFAULT_DIRECT_CONCURRENCY);
|
|
1331
1358
|
const batchMode = strOf(opts["batch_mode"] || DEFAULT_BATCH_MODE).trim();
|
|
1332
1359
|
if (batchMode !== "episode") {
|
|
1333
1360
|
throw new CliError("USAGE ERROR: Invalid batch mode", "Invalid batch mode.", {
|
|
@@ -1348,6 +1375,66 @@ async function commandInitImpl(opts) {
|
|
|
1348
1375
|
const batchTargetLines = batchValues["batch_target_lines"];
|
|
1349
1376
|
const batchMaxChars = batchValues["batch_max_chars"];
|
|
1350
1377
|
const batchMinLines = Math.min(batchValues["batch_min_lines"], batchTargetLines);
|
|
1378
|
+
return {
|
|
1379
|
+
source,
|
|
1380
|
+
workspace,
|
|
1381
|
+
providerName,
|
|
1382
|
+
model,
|
|
1383
|
+
concurrency,
|
|
1384
|
+
batchMode,
|
|
1385
|
+
batchTargetLines,
|
|
1386
|
+
batchMaxChars,
|
|
1387
|
+
batchMinLines,
|
|
1388
|
+
selEpisodes,
|
|
1389
|
+
selBatches,
|
|
1390
|
+
retryErrors,
|
|
1391
|
+
runAll,
|
|
1392
|
+
skipSummary,
|
|
1393
|
+
skipMetadata,
|
|
1394
|
+
resummarize,
|
|
1395
|
+
summaryThresholdChars,
|
|
1396
|
+
};
|
|
1397
|
+
}
|
|
1398
|
+
// Build the per-stage progress emitter + run_state updater + lazy provider that
|
|
1399
|
+
// every stage shares. Extracted so commandInit and commandRun assemble an
|
|
1400
|
+
// identical StageContext (same progress rendering, same run_state side effects).
|
|
1401
|
+
function buildStageContext(options) {
|
|
1402
|
+
const ws = new Workspace(options.workspace);
|
|
1403
|
+
let provider = null;
|
|
1404
|
+
const ctx = {
|
|
1405
|
+
ws,
|
|
1406
|
+
options,
|
|
1407
|
+
state: emptyRunState(),
|
|
1408
|
+
provider() {
|
|
1409
|
+
if (provider)
|
|
1410
|
+
return provider;
|
|
1411
|
+
try {
|
|
1412
|
+
provider = makeProvider(options.providerName, options.model);
|
|
1413
|
+
}
|
|
1414
|
+
catch (exc) {
|
|
1415
|
+
if (exc instanceof CliError) {
|
|
1416
|
+
updateRunState(options.workspace, {
|
|
1417
|
+
status: "init_failed",
|
|
1418
|
+
init_stage: "episode_titles",
|
|
1419
|
+
last_error: { title: exc.title, received: exc.received, failed_at: checkpointTimestamp() },
|
|
1420
|
+
});
|
|
1421
|
+
}
|
|
1422
|
+
throw exc;
|
|
1423
|
+
}
|
|
1424
|
+
return provider;
|
|
1425
|
+
},
|
|
1426
|
+
emitProgress(stage, status = "running") {
|
|
1427
|
+
emitInitProgress(options.workspace, stageRunStateName(stage), status);
|
|
1428
|
+
},
|
|
1429
|
+
updateRunState(updates) {
|
|
1430
|
+
updateRunState(options.workspace, updates);
|
|
1431
|
+
},
|
|
1432
|
+
};
|
|
1433
|
+
return ctx;
|
|
1434
|
+
}
|
|
1435
|
+
// Validate --source-path (existence + supported extension) before any stage
|
|
1436
|
+
// runs. Kept as the init-only entry guard the god-command performed up front.
|
|
1437
|
+
function assertInitSource(source) {
|
|
1351
1438
|
if (!exists(source) || !fs.statSync(source).isFile()) {
|
|
1352
1439
|
throw new CliError("INIT BLOCKED: Source file not found", "Source file not found.", {
|
|
1353
1440
|
exitCode: EXIT_INPUT,
|
|
@@ -1366,26 +1453,350 @@ async function commandInitImpl(opts) {
|
|
|
1366
1453
|
nextSteps: ["Use a supported source file and rerun init."],
|
|
1367
1454
|
});
|
|
1368
1455
|
}
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
|
|
1456
|
+
}
|
|
1457
|
+
// Build the final INIT report from the completed run state. Reads the same
|
|
1458
|
+
// artifacts/state the god-command's trailing block read, so the report lines,
|
|
1459
|
+
// artifacts list and exit code are byte-identical.
|
|
1460
|
+
function buildInitReport(ctx) {
|
|
1461
|
+
const { ws, options, state } = ctx;
|
|
1462
|
+
const dd = ws.dir;
|
|
1463
|
+
const validation = state.validation ?? {};
|
|
1464
|
+
const passed = Boolean(validation["passed"]);
|
|
1465
|
+
const title = passed
|
|
1466
|
+
? "INIT COMPLETE: Initial script ready"
|
|
1467
|
+
: "INIT NEEDS AGENT: Initial script written with repair issues";
|
|
1468
|
+
const stats = validation["stats"] ?? {};
|
|
1469
|
+
const summaryDegraded = state.summaryFullDegraded || state.summaryDegradedEpisodes.length > 0;
|
|
1470
|
+
const summaryLine = formatSummaryStatusLine(options.skipSummary, summaryDegraded, state.summaryDegradedEpisodes);
|
|
1471
|
+
const normalization = isDict(state.assetNormalization) ? state.assetNormalization : null;
|
|
1472
|
+
const normSummary = normalization && isDict(normalization["summary"]) ? normalization["summary"] : {};
|
|
1473
|
+
const merges = Number(normSummary["asset_merges"] ?? 0);
|
|
1474
|
+
const statesFromNames = Number(normSummary["location_states_from_names"] ?? 0);
|
|
1475
|
+
const statesFromTime = Number(normSummary["location_states_from_time"] ?? 0);
|
|
1476
|
+
const reviewItems = Number(normSummary["review_items"] ?? 0);
|
|
1477
|
+
const report = {
|
|
1478
|
+
title,
|
|
1479
|
+
result: [
|
|
1480
|
+
`episodes: ${stats["episodes"] ?? 0}`,
|
|
1481
|
+
`scenes: ${stats["scenes"] ?? 0}`,
|
|
1482
|
+
`actions: ${stats["actions"] ?? 0}`,
|
|
1483
|
+
`validation: ${passed ? "passed" : "needs repair"}`,
|
|
1484
|
+
`provider: ${options.providerName}`,
|
|
1485
|
+
`episodes: ${state.mergedResults.length}/${state.totalEpisodes} complete`,
|
|
1486
|
+
`content: ${state.doneBatches}/${state.totalBatches} done`,
|
|
1487
|
+
`normalization: merges=${merges}, states=${statesFromNames + statesFromTime}, review=${reviewItems}`,
|
|
1488
|
+
...(options.skipMetadata ? ["metadata: skipped"] : []),
|
|
1489
|
+
...(state.titleDegraded.length > 0 ? [`titles degraded (deterministic): ${state.titleDegraded.join(", ")}`] : []),
|
|
1490
|
+
...(summaryLine ? [summaryLine] : []),
|
|
1491
|
+
],
|
|
1492
|
+
artifacts: [
|
|
1493
|
+
ws.rootPathOf("source.txt"),
|
|
1494
|
+
path.join(dd, "source_manifest.json"),
|
|
1495
|
+
path.join(dd, "episode_plan.json"),
|
|
1496
|
+
path.join(dd, "batch_plan.json"),
|
|
1497
|
+
path.join(dd, "batch_results"),
|
|
1498
|
+
path.join(dd, "episode_results"),
|
|
1499
|
+
path.join(dd, "asset_normalization.json"),
|
|
1500
|
+
path.join(dd, "asset_curation.json"),
|
|
1501
|
+
...(options.skipMetadata ? [] : [path.join(dd, "asset_metadata.json")]),
|
|
1502
|
+
path.join(dd, "synopsis.json"),
|
|
1503
|
+
path.join(dd, "script.initial.json"),
|
|
1504
|
+
path.join(dd, "validation.json"),
|
|
1505
|
+
path.join(dd, "run_state.json"),
|
|
1506
|
+
],
|
|
1507
|
+
issues: summarizeIssues(asList(validation["issues"])),
|
|
1508
|
+
next: options.providerName === "mock"
|
|
1509
|
+
? [
|
|
1510
|
+
"Run `direct review` (add --episode <n> for source↔extract); edit with --draft if needed; then validate/export.",
|
|
1511
|
+
"Do not export mock-provider results for delivery.",
|
|
1512
|
+
]
|
|
1513
|
+
: ["Run `direct review` (add --episode <n> for source↔extract); edit with --draft if needed; then validate/export."],
|
|
1514
|
+
};
|
|
1515
|
+
return [report, passed ? EXIT_OK : EXIT_NEEDS_AGENT];
|
|
1516
|
+
}
|
|
1517
|
+
// Build the INIT INCOMPLETE report (some episode still has unextracted batches).
|
|
1518
|
+
// Mirrors the god-command's early-return block; EXIT_RUNTIME signals automation.
|
|
1519
|
+
function buildIncompleteReport(ctx) {
|
|
1520
|
+
const { ws, options, state } = ctx;
|
|
1521
|
+
const dd = ws.dir;
|
|
1522
|
+
const report = {
|
|
1523
|
+
title: "INIT INCOMPLETE: Some episodes have unextracted batches",
|
|
1524
|
+
result: [
|
|
1525
|
+
`episodes: ${state.mergedResults.length}/${state.totalEpisodes} complete`,
|
|
1526
|
+
`incomplete episodes: ${state.incompleteEpisodes.join(", ")}`,
|
|
1527
|
+
`content: ${state.doneBatches}/${state.totalBatches} done, ${state.erroredBatches} error`,
|
|
1528
|
+
...(state.titleDegraded.length > 0 ? [`titles degraded (deterministic): ${state.titleDegraded.join(", ")}`] : []),
|
|
1529
|
+
`provider: ${options.providerName}`,
|
|
1530
|
+
],
|
|
1531
|
+
artifacts: [
|
|
1532
|
+
ws.rootPathOf("source.txt"),
|
|
1533
|
+
path.join(dd, "episode_plan.json"),
|
|
1534
|
+
path.join(dd, "batch_plan.json"),
|
|
1535
|
+
path.join(dd, "batch_results"),
|
|
1536
|
+
path.join(dd, "episode_results"),
|
|
1537
|
+
path.join(dd, "run_state.json"),
|
|
1538
|
+
],
|
|
1539
|
+
next: [
|
|
1540
|
+
"Run `direct status` to see episode progress.",
|
|
1541
|
+
"Re-run one episode with `direct init --episodes <n>`, or all errors with `--retry-errors`.",
|
|
1542
|
+
"Errored units are left untouched on a plain rerun; select them to retry.",
|
|
1543
|
+
],
|
|
1544
|
+
};
|
|
1545
|
+
return [report, EXIT_RUNTIME];
|
|
1546
|
+
}
|
|
1547
|
+
async function commandInitImpl(opts) {
|
|
1548
|
+
const options = parseDirectRunOptions(opts);
|
|
1549
|
+
assertInitSource(options.source);
|
|
1550
|
+
const ctx = buildStageContext(options);
|
|
1551
|
+
ctx.ws.ensure();
|
|
1552
|
+
// Seed run_state with the init header before the first stage runs (provider /
|
|
1553
|
+
// model / source_path / metadata flag) — the god-command set these up front.
|
|
1554
|
+
updateRunState(options.workspace, {
|
|
1372
1555
|
status: "init_running",
|
|
1373
1556
|
command: "direct init",
|
|
1374
1557
|
init_stage: "source_prepare",
|
|
1375
|
-
provider: providerName,
|
|
1376
|
-
model,
|
|
1377
|
-
concurrency,
|
|
1378
|
-
source_path: path.resolve(source),
|
|
1379
|
-
metadata_skipped: skipMetadata,
|
|
1558
|
+
provider: options.providerName,
|
|
1559
|
+
model: options.model,
|
|
1560
|
+
concurrency: options.concurrency,
|
|
1561
|
+
source_path: path.resolve(options.source),
|
|
1562
|
+
metadata_skipped: options.skipMetadata,
|
|
1380
1563
|
});
|
|
1381
|
-
|
|
1564
|
+
try {
|
|
1565
|
+
// `direct init` runs EVERY stage (force) — exactly like the former god
|
|
1566
|
+
// command, which had no whole-stage skip. The reuse model lives INSIDE the
|
|
1567
|
+
// stages (per-batch file presence in batch-extract; per-episode `syn` reuse
|
|
1568
|
+
// in the synopsis stages), so a deterministic stage must still re-derive its
|
|
1569
|
+
// output from the (possibly reused/edited) upstream artifacts. Whole-stage
|
|
1570
|
+
// skip-on-present is a `direct run <stage>` convenience only.
|
|
1571
|
+
await runStages(ctx, STAGES, { force: true });
|
|
1572
|
+
}
|
|
1573
|
+
catch (exc) {
|
|
1574
|
+
// The resumable "incomplete" stop is not a failure — surface the INCOMPLETE
|
|
1575
|
+
// report + EXIT_RUNTIME, exactly as the god-command's early return did.
|
|
1576
|
+
if (exc instanceof StageIncomplete)
|
|
1577
|
+
return buildIncompleteReport(ctx);
|
|
1578
|
+
throw exc;
|
|
1579
|
+
}
|
|
1580
|
+
// Pipeline complete: finalize run_state (the trailing god-command block).
|
|
1581
|
+
const validation = ctx.state.validation ?? {};
|
|
1582
|
+
const passed = Boolean(validation["passed"]);
|
|
1583
|
+
const status = passed ? "ready_for_agent" : "needs_agent_repair";
|
|
1584
|
+
const summaryDegraded = ctx.state.summaryFullDegraded || ctx.state.summaryDegradedEpisodes.length > 0;
|
|
1585
|
+
updateRunState(options.workspace, {
|
|
1586
|
+
status,
|
|
1587
|
+
command: "direct init",
|
|
1588
|
+
init_stage: "complete",
|
|
1589
|
+
provider: options.providerName,
|
|
1590
|
+
model: options.model,
|
|
1591
|
+
concurrency: options.concurrency,
|
|
1592
|
+
batch_mode: options.batchMode,
|
|
1593
|
+
batch_target_lines: options.batchTargetLines,
|
|
1594
|
+
batch_max_chars: options.batchMaxChars,
|
|
1595
|
+
batch_min_lines: options.batchMinLines,
|
|
1596
|
+
source_path: path.resolve(options.source),
|
|
1597
|
+
script_path: ctx.ws.pathOf("script.initial.json"),
|
|
1598
|
+
validation_path: ctx.ws.pathOf("validation.json"),
|
|
1599
|
+
episode_total: ctx.state.totalEpisodes,
|
|
1600
|
+
episode_completed: ctx.state.mergedResults.length,
|
|
1601
|
+
incomplete_episodes: [],
|
|
1602
|
+
batch_total: ctx.state.totalBatches,
|
|
1603
|
+
batch_completed: ctx.state.doneBatches,
|
|
1604
|
+
batch_reused: ctx.state.reusedBatchCount,
|
|
1605
|
+
batch_failed: ctx.state.erroredBatches,
|
|
1606
|
+
summary_threshold_chars: options.summaryThresholdChars,
|
|
1607
|
+
summary_skipped: options.skipSummary,
|
|
1608
|
+
summary_degraded: summaryDegraded,
|
|
1609
|
+
summary_degraded_episodes: ctx.state.summaryDegradedEpisodes,
|
|
1610
|
+
summary_degraded_reason: ctx.state.summaryDegradeReason || null,
|
|
1611
|
+
metadata_skipped: options.skipMetadata,
|
|
1612
|
+
last_error: null,
|
|
1613
|
+
});
|
|
1614
|
+
return buildInitReport(ctx);
|
|
1615
|
+
}
|
|
1616
|
+
// ---------------------------------------------------------------------------
|
|
1617
|
+
// command_run — single-stage runner against an existing workspace. Lets the
|
|
1618
|
+
// agent re-run one step (e.g. `direct run metadata`, `direct run episode-synopsis`)
|
|
1619
|
+
// without re-driving the whole pipeline. The stage rehydrates the prior
|
|
1620
|
+
// artifacts it needs from disk, so it stands alone.
|
|
1621
|
+
// ---------------------------------------------------------------------------
|
|
1622
|
+
export async function commandRun(opts) {
|
|
1623
|
+
const stageName = strOf(asList(opts["_args"])[0] || opts["stage"]).trim();
|
|
1624
|
+
if (!stageName) {
|
|
1625
|
+
throw new CliError("DIRECT RUN BLOCKED: missing stage", "No stage name given.", {
|
|
1626
|
+
exitCode: EXIT_USAGE,
|
|
1627
|
+
required: [`one of: ${STAGES.map((s) => s.name).join(", ")}`],
|
|
1628
|
+
received: ["<no stage>"],
|
|
1629
|
+
nextSteps: ["Pass a stage name, e.g. `direct run metadata`."],
|
|
1630
|
+
});
|
|
1631
|
+
}
|
|
1632
|
+
// Reject an unknown stage up front (both code paths below would otherwise
|
|
1633
|
+
// silently no-op the `only`-filtered runStages on a typo).
|
|
1634
|
+
if (!STAGES.some((s) => s.name === stageName)) {
|
|
1635
|
+
throw new CliError("DIRECT RUN BLOCKED: unknown stage", `Unknown stage "${stageName}".`, {
|
|
1636
|
+
exitCode: EXIT_USAGE,
|
|
1637
|
+
required: [`one of: ${STAGES.map((s) => s.name).join(", ")}`],
|
|
1638
|
+
received: [stageName],
|
|
1639
|
+
nextSteps: ["Pass a valid stage name to `direct run`."],
|
|
1640
|
+
});
|
|
1641
|
+
}
|
|
1642
|
+
const options = parseDirectRunOptions(opts);
|
|
1643
|
+
const ctx = buildStageContext(options);
|
|
1644
|
+
const force = Boolean(opts["force"]);
|
|
1645
|
+
try {
|
|
1646
|
+
if (force) {
|
|
1647
|
+
// --force re-runs even when the output artifact is already present.
|
|
1648
|
+
await runOne(ctx, stageName);
|
|
1649
|
+
}
|
|
1650
|
+
else {
|
|
1651
|
+
// Without --force, honor skip-on-present so `direct run <stage>` is a no-op
|
|
1652
|
+
// when the stage's output already exists (mirrors runStages' resume).
|
|
1653
|
+
await runStages(ctx, STAGES, { only: new Set([stageName]) });
|
|
1654
|
+
}
|
|
1655
|
+
}
|
|
1656
|
+
catch (exc) {
|
|
1657
|
+
if (exc instanceof StageIncomplete) {
|
|
1658
|
+
finishInitProgress();
|
|
1659
|
+
return buildIncompleteReport(ctx);
|
|
1660
|
+
}
|
|
1661
|
+
emitFailedInitProgress(options.workspace);
|
|
1662
|
+
finishInitProgress();
|
|
1663
|
+
throw exc;
|
|
1664
|
+
}
|
|
1665
|
+
finishInitProgress();
|
|
1666
|
+
const report = {
|
|
1667
|
+
title: `DIRECT RUN: ${stageName}`,
|
|
1668
|
+
result: [
|
|
1669
|
+
`stage: ${stageName}`,
|
|
1670
|
+
`workspace: ${options.workspace}`,
|
|
1671
|
+
"ran one stage; run `direct status` for the full picture.",
|
|
1672
|
+
],
|
|
1673
|
+
artifacts: [ctx.ws.pathOf("run_state.json")],
|
|
1674
|
+
next: ["Run `direct status` to see updated progress, or `direct run <stage>` for another step."],
|
|
1675
|
+
};
|
|
1676
|
+
return [report, EXIT_OK];
|
|
1677
|
+
}
|
|
1678
|
+
// ===========================================================================
|
|
1679
|
+
// Per-stage implementations. Each function below is ONE segment of the former
|
|
1680
|
+
// commandInitImpl, lifted verbatim onto a StageContext so it can run either as
|
|
1681
|
+
// part of the full pipeline (runStages) or on its own (`direct run <stage>`).
|
|
1682
|
+
// They call the SAME helpers/pure functions the god-command used — no algorithm
|
|
1683
|
+
// is duplicated; only the cross-stage locals moved onto ctx.state. The thin
|
|
1684
|
+
// Stage wrappers in src/domain/direct/stages/*.ts just name + order these.
|
|
1685
|
+
// ===========================================================================
|
|
1686
|
+
// A stage that needs a prior artifact missing from ctx.state (because the run
|
|
1687
|
+
// started at a later stage via `direct run`) lazily rehydrates it from disk.
|
|
1688
|
+
function ensureEpisodePlan(ctx) {
|
|
1689
|
+
if (ctx.state.episodePlan)
|
|
1690
|
+
return ctx.state.episodePlan;
|
|
1691
|
+
const plan = ctx.ws.readJsonSafe("episode_plan.json");
|
|
1692
|
+
if (!plan) {
|
|
1693
|
+
throw new CliError("DIRECT RUN BLOCKED: episode_plan.json missing", "episode_plan.json missing.", {
|
|
1694
|
+
exitCode: EXIT_INPUT,
|
|
1695
|
+
required: ["episode_plan.json (run the episode-plan stage first)"],
|
|
1696
|
+
received: ["episode_plan.json missing"],
|
|
1697
|
+
nextSteps: ["Run `direct init` or `direct run episode-plan` first."],
|
|
1698
|
+
});
|
|
1699
|
+
}
|
|
1700
|
+
ctx.state.episodePlan = plan;
|
|
1701
|
+
return plan;
|
|
1702
|
+
}
|
|
1703
|
+
function ensureBatchPlan(ctx) {
|
|
1704
|
+
if (ctx.state.batchPlan)
|
|
1705
|
+
return ctx.state.batchPlan;
|
|
1706
|
+
const plan = ctx.ws.readJsonSafe("batch_plan.json");
|
|
1707
|
+
if (!plan) {
|
|
1708
|
+
throw new CliError("DIRECT RUN BLOCKED: batch_plan.json missing", "batch_plan.json missing.", {
|
|
1709
|
+
exitCode: EXIT_INPUT,
|
|
1710
|
+
required: ["batch_plan.json (run the batch-plan stage first)"],
|
|
1711
|
+
received: ["batch_plan.json missing"],
|
|
1712
|
+
nextSteps: ["Run `direct init` or `direct run batch-plan` first."],
|
|
1713
|
+
});
|
|
1714
|
+
}
|
|
1715
|
+
ctx.state.batchPlan = plan;
|
|
1716
|
+
return plan;
|
|
1717
|
+
}
|
|
1718
|
+
function ensureSourceText(ctx) {
|
|
1719
|
+
if (ctx.state.sourceText)
|
|
1720
|
+
return ctx.state.sourceText;
|
|
1721
|
+
const p = ctx.ws.rootPathOf("source.txt");
|
|
1722
|
+
if (!exists(p)) {
|
|
1723
|
+
throw new CliError("DIRECT RUN BLOCKED: source.txt missing", "source.txt missing.", {
|
|
1724
|
+
exitCode: EXIT_INPUT,
|
|
1725
|
+
required: ["source.txt (run the source-prepare stage first)"],
|
|
1726
|
+
received: ["source.txt missing"],
|
|
1727
|
+
nextSteps: ["Run `direct init` or `direct run source-prepare` first."],
|
|
1728
|
+
});
|
|
1729
|
+
}
|
|
1730
|
+
ctx.state.sourceText = readText(p);
|
|
1731
|
+
return ctx.state.sourceText;
|
|
1732
|
+
}
|
|
1733
|
+
function ensureScript(ctx) {
|
|
1734
|
+
if (ctx.state.script)
|
|
1735
|
+
return ctx.state.script;
|
|
1736
|
+
const script = ctx.ws.readJsonSafe("script.initial.json");
|
|
1737
|
+
if (!script) {
|
|
1738
|
+
throw new CliError("DIRECT RUN BLOCKED: script.initial.json missing", "script.initial.json missing.", {
|
|
1739
|
+
exitCode: EXIT_INPUT,
|
|
1740
|
+
required: ["script.initial.json (run the script-merge stage first)"],
|
|
1741
|
+
received: ["script.initial.json missing"],
|
|
1742
|
+
nextSteps: ["Run `direct init` or `direct run script-merge` first."],
|
|
1743
|
+
});
|
|
1744
|
+
}
|
|
1745
|
+
ctx.state.script = script;
|
|
1746
|
+
return script;
|
|
1747
|
+
}
|
|
1748
|
+
// Reassemble ctx.state.mergedResults from episode_results/ when a later stage is
|
|
1749
|
+
// run standalone (e.g. `direct run script-merge`). Mirrors the assembly the
|
|
1750
|
+
// episode-merge stage does in-memory, but reads the persisted results.
|
|
1751
|
+
function ensureMergedResults(ctx) {
|
|
1752
|
+
if (ctx.state.mergedResults.length > 0)
|
|
1753
|
+
return ctx.state.mergedResults;
|
|
1754
|
+
const plan = ensureEpisodePlan(ctx);
|
|
1755
|
+
const episodeResultsDir = ctx.ws.pathOf("episode_results");
|
|
1756
|
+
const results = [];
|
|
1757
|
+
for (const episode of asList(plan["episodes"])) {
|
|
1758
|
+
const p = episodeResultPath(episodeResultsDir, episode);
|
|
1759
|
+
const data = exists(p) ? readJsonFile(p, {}) : null;
|
|
1760
|
+
if (data && isDict(data) && Object.keys(data).length > 0) {
|
|
1761
|
+
results.push(expandCompactEpisodeResult(data, episode));
|
|
1762
|
+
}
|
|
1763
|
+
}
|
|
1764
|
+
results.sort((a, b) => Number(a["episode"] ?? 0) - Number(b["episode"] ?? 0));
|
|
1765
|
+
ctx.state.mergedResults = results;
|
|
1766
|
+
return results;
|
|
1767
|
+
}
|
|
1768
|
+
// Recover the project name when a script-producing stage runs standalone (no
|
|
1769
|
+
// source-prepare set state.projectName, and `direct run` has no --source-path).
|
|
1770
|
+
// Order: in-memory → persisted manifest (source_name minus ext) → existing
|
|
1771
|
+
// script title → source basename. Without this, `direct run script-merge` would
|
|
1772
|
+
// rebuild the script with an empty title (state.projectName='' && source='').
|
|
1773
|
+
function ensureProjectName(ctx) {
|
|
1774
|
+
if (ctx.state.projectName)
|
|
1775
|
+
return ctx.state.projectName;
|
|
1776
|
+
const manifest = ctx.ws.readJsonSafe("source_manifest.json");
|
|
1777
|
+
const fromManifest = manifest
|
|
1778
|
+
? strOf(manifest["source_name"]).replace(/\.[^.]+$/, "")
|
|
1779
|
+
|| (strOf(manifest["source_path"]) ? path.basename(strOf(manifest["source_path"]), path.extname(strOf(manifest["source_path"]))) : "")
|
|
1780
|
+
: "";
|
|
1781
|
+
const fromScript = strOf(ctx.ws.readJsonSafe("script.initial.json")?.["title"]);
|
|
1782
|
+
const fromSource = ctx.options.source ? path.basename(ctx.options.source, path.extname(ctx.options.source)) : "";
|
|
1783
|
+
const name = fromManifest || fromScript || fromSource;
|
|
1784
|
+
ctx.state.projectName = name;
|
|
1785
|
+
return name;
|
|
1786
|
+
}
|
|
1787
|
+
// --- source-prepare -------------------------------------------------------
|
|
1788
|
+
export async function stageSourcePrepare(ctx) {
|
|
1789
|
+
const { options, ws, state } = ctx;
|
|
1790
|
+
const source = options.source;
|
|
1791
|
+
ctx.updateRunState({ status: "init_running", command: "direct init", init_stage: "source_prepare" });
|
|
1792
|
+
ctx.emitProgress("source-prepare");
|
|
1382
1793
|
let info;
|
|
1383
1794
|
try {
|
|
1384
|
-
info = await prepareSource(source, workspace);
|
|
1795
|
+
info = await prepareSource(source, options.workspace);
|
|
1385
1796
|
}
|
|
1386
1797
|
catch (exc) {
|
|
1387
1798
|
if (exc instanceof CliError) {
|
|
1388
|
-
updateRunState(
|
|
1799
|
+
ctx.updateRunState({
|
|
1389
1800
|
status: "init_failed",
|
|
1390
1801
|
init_stage: "source_prepare",
|
|
1391
1802
|
last_error: { title: exc.title, received: exc.received, failed_at: checkpointTimestamp() },
|
|
@@ -1394,7 +1805,7 @@ async function commandInitImpl(opts) {
|
|
|
1394
1805
|
}
|
|
1395
1806
|
const e = exc;
|
|
1396
1807
|
const receivedError = `${source}: ${e?.name ?? "Error"}${e?.message ? `: ${e.message}` : ""}`;
|
|
1397
|
-
updateRunState(
|
|
1808
|
+
ctx.updateRunState({
|
|
1398
1809
|
status: "init_failed",
|
|
1399
1810
|
init_stage: "source_prepare",
|
|
1400
1811
|
last_error: { title: "INIT BLOCKED: Source preparation failed", received: [receivedError], failed_at: checkpointTimestamp() },
|
|
@@ -1407,108 +1818,102 @@ async function commandInitImpl(opts) {
|
|
|
1407
1818
|
});
|
|
1408
1819
|
}
|
|
1409
1820
|
const sourceTextPath = strOf(info["sourceTextPath"]);
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1821
|
+
state.sourceText = readText(sourceTextPath);
|
|
1822
|
+
state.projectName = strOf(info["projectName"]) || path.basename(source, path.extname(source));
|
|
1823
|
+
state.manifest = makeSourceManifest(source, sourceTextPath, info);
|
|
1824
|
+
ws.writeJson("source_manifest.json", state.manifest);
|
|
1825
|
+
}
|
|
1826
|
+
// --- episode-plan ---------------------------------------------------------
|
|
1827
|
+
export async function stageEpisodePlan(ctx) {
|
|
1828
|
+
const { ws, state, options } = ctx;
|
|
1829
|
+
const sourceText = ensureSourceText(ctx);
|
|
1830
|
+
ctx.updateRunState({ status: "init_running", init_stage: "episode_plan" });
|
|
1831
|
+
ctx.emitProgress("episode-plan");
|
|
1414
1832
|
let plan;
|
|
1415
1833
|
try {
|
|
1416
1834
|
plan = buildEpisodePlan(sourceText);
|
|
1417
1835
|
}
|
|
1418
1836
|
catch (exc) {
|
|
1419
|
-
|
|
1420
|
-
throw initFailedReport(workspace, {
|
|
1837
|
+
stageFailure(options.workspace, exc, {
|
|
1421
1838
|
title: "INIT FAILED: Episode planning failed",
|
|
1422
1839
|
stage: "episode_plan",
|
|
1423
1840
|
required: ["source.txt that can be split into episodes"],
|
|
1424
|
-
received: [`${e?.name ?? "Error"}: ${(e?.message ?? "").slice(0, 160)}`],
|
|
1425
1841
|
nextSteps: ["Inspect workspace/source.txt, fix the source file, and rerun init."],
|
|
1426
1842
|
});
|
|
1427
1843
|
}
|
|
1428
|
-
|
|
1429
|
-
|
|
1844
|
+
state.episodePlan = plan;
|
|
1845
|
+
ws.writeJson("episode_plan.json", plan);
|
|
1846
|
+
}
|
|
1847
|
+
// --- episode-titles -------------------------------------------------------
|
|
1848
|
+
export async function stageEpisodeTitles(ctx) {
|
|
1849
|
+
const { state, options } = ctx;
|
|
1850
|
+
const sourceText = ensureSourceText(ctx);
|
|
1851
|
+
const plan = ensureEpisodePlan(ctx);
|
|
1852
|
+
// Provider capability gate (was inline in commandInitImpl before titles).
|
|
1853
|
+
// Checked twice — declared capabilities, then the constructed provider — but
|
|
1854
|
+
// both fail the same way, so share one report builder.
|
|
1855
|
+
const lacksExtraction = () => initFailedReport(options.workspace, {
|
|
1856
|
+
title: "INIT FAILED: provider lacks batch extraction",
|
|
1857
|
+
stage: "episode_titles",
|
|
1858
|
+
required: ["provider.extractBatch or provider.extractEpisode"],
|
|
1859
|
+
received: [`--provider ${options.providerName}`],
|
|
1860
|
+
nextSteps: ["Use --provider anthropic or --provider mock for direct init."],
|
|
1861
|
+
});
|
|
1862
|
+
const capabilities = providerCapabilities(options.providerName);
|
|
1430
1863
|
if (capabilities && !capabilities.extractBatch && !capabilities.extractEpisode) {
|
|
1431
|
-
throw
|
|
1432
|
-
title: "INIT FAILED: provider lacks batch extraction",
|
|
1433
|
-
stage: "episode_titles",
|
|
1434
|
-
required: ["provider.extractBatch or provider.extractEpisode"],
|
|
1435
|
-
received: [`--provider ${providerName}`],
|
|
1436
|
-
nextSteps: ["Use --provider anthropic or --provider mock for direct init."],
|
|
1437
|
-
});
|
|
1438
|
-
}
|
|
1439
|
-
let provider;
|
|
1440
|
-
try {
|
|
1441
|
-
provider = makeProvider(providerName, model);
|
|
1442
|
-
}
|
|
1443
|
-
catch (exc) {
|
|
1444
|
-
if (exc instanceof CliError) {
|
|
1445
|
-
updateRunState(workspace, {
|
|
1446
|
-
status: "init_failed",
|
|
1447
|
-
init_stage: "episode_titles",
|
|
1448
|
-
last_error: { title: exc.title, received: exc.received, failed_at: checkpointTimestamp() },
|
|
1449
|
-
});
|
|
1450
|
-
}
|
|
1451
|
-
throw exc;
|
|
1864
|
+
throw lacksExtraction();
|
|
1452
1865
|
}
|
|
1866
|
+
const provider = ctx.provider();
|
|
1453
1867
|
if (!provider.extractBatch && !provider.extractEpisode) {
|
|
1454
|
-
throw
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
const
|
|
1868
|
+
throw lacksExtraction();
|
|
1869
|
+
}
|
|
1870
|
+
ctx.updateRunState({ status: "init_running", init_stage: "episode_titles" });
|
|
1871
|
+
ctx.emitProgress("episode-titles");
|
|
1872
|
+
state.titleDegraded = await enrichEpisodeTitlesChunked(options.workspace, sourceText, plan, provider, options.concurrency);
|
|
1873
|
+
// Titles mutate the plan in place — persist it so the artifact reflects them.
|
|
1874
|
+
ctx.ws.writeJson("episode_plan.json", plan);
|
|
1875
|
+
}
|
|
1876
|
+
// --- batch-plan -----------------------------------------------------------
|
|
1877
|
+
export async function stageBatchPlan(ctx) {
|
|
1878
|
+
const { ws, state, options } = ctx;
|
|
1879
|
+
const sourceText = ensureSourceText(ctx);
|
|
1880
|
+
const plan = ensureEpisodePlan(ctx);
|
|
1465
1881
|
let batchPlan;
|
|
1466
1882
|
try {
|
|
1467
1883
|
batchPlan = buildBatchPlan(sourceText, plan, {
|
|
1468
|
-
targetLines: batchTargetLines,
|
|
1469
|
-
maxChars: batchMaxChars,
|
|
1470
|
-
minLines: batchMinLines,
|
|
1471
|
-
mode: batchMode,
|
|
1884
|
+
targetLines: options.batchTargetLines,
|
|
1885
|
+
maxChars: options.batchMaxChars,
|
|
1886
|
+
minLines: options.batchMinLines,
|
|
1887
|
+
mode: options.batchMode,
|
|
1472
1888
|
});
|
|
1473
1889
|
}
|
|
1474
1890
|
catch (exc) {
|
|
1475
|
-
|
|
1476
|
-
throw initFailedReport(workspace, {
|
|
1891
|
+
stageFailure(options.workspace, exc, {
|
|
1477
1892
|
title: "INIT FAILED: Batch planning failed",
|
|
1478
1893
|
stage: "batch_plan",
|
|
1479
1894
|
required: ["episode_plan.json that can be split into natural paragraph batches"],
|
|
1480
|
-
received: [`${e?.name ?? "Error"}: ${(e?.message ?? "").slice(0, 160)}`],
|
|
1481
1895
|
nextSteps: ["Inspect workspace/source.txt and episode_plan.json, then rerun init."],
|
|
1482
1896
|
});
|
|
1483
1897
|
}
|
|
1484
|
-
|
|
1485
|
-
writeJson(
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
1493
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1898
|
+
state.batchPlan = batchPlan;
|
|
1899
|
+
ws.writeJson("batch_plan.json", batchPlan);
|
|
1900
|
+
ctx.emitProgress("batch-plan", "success");
|
|
1901
|
+
}
|
|
1902
|
+
// --- batch-extract --------------------------------------------------------
|
|
1903
|
+
export async function stageBatchExtract(ctx) {
|
|
1904
|
+
const { ws, state, options } = ctx;
|
|
1905
|
+
const sourceText = ensureSourceText(ctx);
|
|
1906
|
+
const plan = ensureEpisodePlan(ctx);
|
|
1907
|
+
const batchPlan = ensureBatchPlan(ctx);
|
|
1908
|
+
const provider = ctx.provider();
|
|
1909
|
+
const episodeResultsDir = ws.mkdir("episode_results");
|
|
1910
|
+
const batchResultsDir = ws.mkdir("batch_results");
|
|
1911
|
+
// Non-destructive GC for units the current plan no longer references.
|
|
1496
1912
|
gcOrphanUnits(episodeResultsDir, new Set(asList(plan["episodes"]).map((ep) => episodeResultKey(ep))));
|
|
1497
1913
|
gcOrphanUnits(batchResultsDir, new Set(asList(batchPlan["batches"]).map((b) => batchResultKey(b))));
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
1501
|
-
if (!batchesByEpisode.has(epNum))
|
|
1502
|
-
batchesByEpisode.set(epNum, []);
|
|
1503
|
-
batchesByEpisode.get(epNum).push(batch);
|
|
1504
|
-
}
|
|
1505
|
-
const totalEpisodes = asList(plan["episodes"]).length;
|
|
1506
|
-
const totalBatches = asList(batchPlan["batches"]).length;
|
|
1507
|
-
// Decide which batches to run. State (classifyBatch) is shared with `direct
|
|
1508
|
-
// status` so the two never disagree: a "done" batch is reused, an "error"
|
|
1509
|
-
// batch is sticky (skipped on a plain rerun so a content filter can't loop),
|
|
1510
|
-
// a "pending" (never-run or corrupt) batch runs. The agent overrides with
|
|
1511
|
-
// explicit selection.
|
|
1914
|
+
state.totalEpisodes = asList(plan["episodes"]).length;
|
|
1915
|
+
state.totalBatches = asList(batchPlan["batches"]).length;
|
|
1916
|
+
// Decide which batches to run (force / reuse-done / sticky-error / pending).
|
|
1512
1917
|
const pending = [];
|
|
1513
1918
|
let reusedBatchCount = 0;
|
|
1514
1919
|
let stickyErrorCount = 0;
|
|
@@ -1516,7 +1921,7 @@ async function commandInitImpl(opts) {
|
|
|
1516
1921
|
const epNum = Number(batch["episode"]);
|
|
1517
1922
|
const key = batchResultKey(batch);
|
|
1518
1923
|
const errorPath = batchErrorPath(batchResultsDir, batch);
|
|
1519
|
-
const forced = runAll || selEpisodes.has(epNum) || selBatches.has(key) || (retryErrors && exists(errorPath));
|
|
1924
|
+
const forced = options.runAll || options.selEpisodes.has(epNum) || options.selBatches.has(key) || (options.retryErrors && exists(errorPath));
|
|
1520
1925
|
if (forced) {
|
|
1521
1926
|
const resultPath = batchResultPath(batchResultsDir, batch);
|
|
1522
1927
|
if (exists(resultPath))
|
|
@@ -1532,74 +1937,98 @@ async function commandInitImpl(opts) {
|
|
|
1532
1937
|
break;
|
|
1533
1938
|
case "error":
|
|
1534
1939
|
stickyErrorCount++;
|
|
1535
|
-
break;
|
|
1536
|
-
default: pending.push(batch);
|
|
1940
|
+
break;
|
|
1941
|
+
default: pending.push(batch);
|
|
1537
1942
|
}
|
|
1538
1943
|
}
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1544
|
-
updateRunState(workspace, {
|
|
1944
|
+
state.pending = pending;
|
|
1945
|
+
state.reusedBatchCount = reusedBatchCount;
|
|
1946
|
+
state.stickyErrorCount = stickyErrorCount;
|
|
1947
|
+
state.changedEpisodes = new Set(pending.map((b) => Number(b["episode"])));
|
|
1948
|
+
ctx.updateRunState({
|
|
1545
1949
|
status: "init_running",
|
|
1546
1950
|
init_stage: "batch_extract",
|
|
1547
|
-
batch_mode: batchMode,
|
|
1548
|
-
batch_target_lines: batchTargetLines,
|
|
1549
|
-
batch_max_chars: batchMaxChars,
|
|
1550
|
-
batch_min_lines: batchMinLines,
|
|
1551
|
-
episode_total: totalEpisodes,
|
|
1552
|
-
batch_total: totalBatches,
|
|
1951
|
+
batch_mode: options.batchMode,
|
|
1952
|
+
batch_target_lines: options.batchTargetLines,
|
|
1953
|
+
batch_max_chars: options.batchMaxChars,
|
|
1954
|
+
batch_min_lines: options.batchMinLines,
|
|
1955
|
+
episode_total: state.totalEpisodes,
|
|
1956
|
+
batch_total: state.totalBatches,
|
|
1553
1957
|
});
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
// whole pool drains. Per-unit files have no shared index, so concurrent writes
|
|
1557
|
-
// are safe — and batch_results/ fills up live, so a crash/kill mid-run keeps
|
|
1558
|
-
// every batch already extracted (the resume model depends on this).
|
|
1559
|
-
const outcomes = await pMapWithConcurrency(pending, concurrency, async (batch) => {
|
|
1958
|
+
ctx.emitProgress("batch-extract");
|
|
1959
|
+
const outcomes = await pMapWithConcurrency(pending, options.concurrency, async (batch) => {
|
|
1560
1960
|
try {
|
|
1561
1961
|
const result = await extractBatchWithRecovery(provider, sourceText, batch);
|
|
1562
1962
|
persistBatchResult(batchResultsDir, batch, result);
|
|
1563
1963
|
const errorPath = batchErrorPath(batchResultsDir, batch);
|
|
1564
1964
|
if (exists(errorPath))
|
|
1565
1965
|
deletePath(errorPath);
|
|
1566
|
-
|
|
1966
|
+
ctx.emitProgress("batch-extract");
|
|
1567
1967
|
return true;
|
|
1568
1968
|
}
|
|
1569
1969
|
catch (error) {
|
|
1570
1970
|
writeBatchFailure(batchResultsDir, batch, error);
|
|
1571
|
-
|
|
1971
|
+
ctx.emitProgress("batch-extract");
|
|
1572
1972
|
return false;
|
|
1573
1973
|
}
|
|
1574
1974
|
});
|
|
1575
1975
|
const freshOk = outcomes.filter((o) => o.ok && o.value === true).length;
|
|
1576
1976
|
const freshFail = outcomes.filter((o) => o.ok && o.value === false).length;
|
|
1577
|
-
|
|
1578
|
-
|
|
1579
|
-
|
|
1580
|
-
|
|
1581
|
-
|
|
1977
|
+
state.doneBatches = reusedBatchCount + freshOk;
|
|
1978
|
+
state.erroredBatches = stickyErrorCount + freshFail;
|
|
1979
|
+
}
|
|
1980
|
+
// --- episode-merge --------------------------------------------------------
|
|
1981
|
+
export async function stageEpisodeMerge(ctx) {
|
|
1982
|
+
const { ws, state } = ctx;
|
|
1983
|
+
const sourceText = ensureSourceText(ctx);
|
|
1984
|
+
const plan = ensureEpisodePlan(ctx);
|
|
1985
|
+
const batchPlan = ensureBatchPlan(ctx);
|
|
1986
|
+
const episodeResultsDir = ws.mkdir("episode_results");
|
|
1987
|
+
const batchResultsDir = ws.mkdir("batch_results");
|
|
1988
|
+
// Recompute the bookkeeping a standalone `direct run episode-merge` needs when
|
|
1989
|
+
// batch-extract didn't run this invocation.
|
|
1990
|
+
if (state.totalEpisodes === 0)
|
|
1991
|
+
state.totalEpisodes = asList(plan["episodes"]).length;
|
|
1992
|
+
if (state.totalBatches === 0)
|
|
1993
|
+
state.totalBatches = asList(batchPlan["batches"]).length;
|
|
1994
|
+
const batchesByEpisode = new Map();
|
|
1995
|
+
for (const batch of asList(batchPlan["batches"])) {
|
|
1996
|
+
const epNum = Number(batch["episode"]);
|
|
1997
|
+
if (!batchesByEpisode.has(epNum))
|
|
1998
|
+
batchesByEpisode.set(epNum, []);
|
|
1999
|
+
batchesByEpisode.get(epNum).push(batch);
|
|
2000
|
+
}
|
|
2001
|
+
if (state.doneBatches === 0 && state.erroredBatches === 0) {
|
|
2002
|
+
let done = 0;
|
|
2003
|
+
let errored = 0;
|
|
2004
|
+
for (const batch of asList(batchPlan["batches"])) {
|
|
2005
|
+
const s = classifyBatch(batchResultsDir, batch);
|
|
2006
|
+
if (s === "done")
|
|
2007
|
+
done++;
|
|
2008
|
+
else if (s === "error")
|
|
2009
|
+
errored++;
|
|
2010
|
+
}
|
|
2011
|
+
state.doneBatches = done;
|
|
2012
|
+
state.erroredBatches = errored;
|
|
2013
|
+
}
|
|
1582
2014
|
const results = [];
|
|
1583
2015
|
const incompleteEpisodes = [];
|
|
1584
|
-
updateRunState(
|
|
2016
|
+
ctx.updateRunState({
|
|
1585
2017
|
status: "init_running",
|
|
1586
2018
|
init_stage: "episode_merge",
|
|
1587
|
-
episode_total: totalEpisodes,
|
|
1588
|
-
batch_total: totalBatches,
|
|
1589
|
-
batch_completed: doneBatches,
|
|
1590
|
-
batch_failed: erroredBatches,
|
|
2019
|
+
episode_total: state.totalEpisodes,
|
|
2020
|
+
batch_total: state.totalBatches,
|
|
2021
|
+
batch_completed: state.doneBatches,
|
|
2022
|
+
batch_failed: state.erroredBatches,
|
|
1591
2023
|
});
|
|
1592
|
-
|
|
2024
|
+
ctx.emitProgress("episode-merge");
|
|
1593
2025
|
try {
|
|
1594
|
-
// Assemble every complete episode (incomplete ones are reported and skipped).
|
|
1595
2026
|
for (const episode of asList(plan["episodes"])) {
|
|
1596
2027
|
const episodeNum = Number(episode["episode"]);
|
|
1597
2028
|
const epBatches = batchesByEpisode.get(episodeNum) ?? [];
|
|
1598
2029
|
const batchResults = epBatches.map((b) => readBatchResult(batchResultsDir, b)).filter((r) => r !== null);
|
|
1599
2030
|
if (epBatches.length === 0 || batchResults.length !== epBatches.length) {
|
|
1600
2031
|
incompleteEpisodes.push(episodeNum);
|
|
1601
|
-
// Drop any stale episode_results/ep_NNN.json from a prior complete run so
|
|
1602
|
-
// the derived artifact never claims an episode is complete when it isn't.
|
|
1603
2032
|
const stale = episodeResultPath(episodeResultsDir, episode);
|
|
1604
2033
|
if (exists(stale))
|
|
1605
2034
|
deletePath(stale);
|
|
@@ -1607,194 +2036,180 @@ async function commandInitImpl(opts) {
|
|
|
1607
2036
|
}
|
|
1608
2037
|
const merged = mergeBatchResultsForEpisode(episode, batchResults);
|
|
1609
2038
|
validateEpisodeExtractionQuality(sourceText, episode, merged);
|
|
1610
|
-
results.push(merged);
|
|
1611
|
-
}
|
|
1612
|
-
emitInitProgress(workspace, "episode_merge", incompleteEpisodes.length > 0 ? "warning" : "success");
|
|
1613
|
-
// Episode-level synopsis (reduce-1): resolve each result's synopsis in place
|
|
1614
|
-
// (passthrough / reuse / skip / concurrent LLM stitch). Reads prior `syn`
|
|
1615
|
-
// BEFORE the persist loop below overwrites ep_NNN.json.
|
|
1616
|
-
updateRunState(workspace, { status: "init_running", init_stage: "episode_synopsis" });
|
|
1617
|
-
emitInitProgress(workspace, "episode_synopsis");
|
|
1618
|
-
summaryDegradedEpisodes.push(...await resolveEpisodeSynopses(provider, results, {
|
|
1619
|
-
resultsDir: episodeResultsDir,
|
|
1620
|
-
resummarize,
|
|
1621
|
-
skipSummary,
|
|
1622
|
-
changedEpisodes,
|
|
1623
|
-
concurrency,
|
|
1624
|
-
}));
|
|
1625
|
-
// Persist every assembled episode (synopsis now resolved for all of them).
|
|
1626
|
-
// merged carries `episode`, so it doubles as the path key.
|
|
1627
|
-
for (const merged of results) {
|
|
1628
|
-
writeJson(episodeResultPath(episodeResultsDir, merged), compactEpisodeResult(merged));
|
|
1629
|
-
const errPath = episodeErrorPath(episodeResultsDir, merged);
|
|
1630
|
-
if (exists(errPath))
|
|
1631
|
-
deletePath(errPath);
|
|
2039
|
+
results.push(merged);
|
|
1632
2040
|
}
|
|
1633
|
-
emitInitProgress(workspace, "episode_synopsis", summaryDegradedEpisodes.length > 0 ? "warning" : "success");
|
|
1634
2041
|
}
|
|
1635
2042
|
catch (exc) {
|
|
1636
|
-
|
|
1637
|
-
throw initFailedReport(workspace, {
|
|
2043
|
+
stageFailure(ctx.options.workspace, exc, {
|
|
1638
2044
|
title: "INIT FAILED: Episode merge failed",
|
|
1639
2045
|
stage: "episode_merge",
|
|
1640
2046
|
required: ["batch_results/*.json that can merge into episode_results/*.json"],
|
|
1641
|
-
received: [`${e?.name ?? "Error"}: ${(e?.message ?? "").slice(0, 160)}`],
|
|
1642
2047
|
nextSteps: ["Rerun init; completed batches are reused and episode merge will retry."],
|
|
1643
2048
|
});
|
|
1644
2049
|
}
|
|
1645
|
-
|
|
1646
|
-
incompleteEpisodes
|
|
1647
|
-
|
|
1648
|
-
|
|
2050
|
+
state.mergedResults = results;
|
|
2051
|
+
state.incompleteEpisodes = incompleteEpisodes;
|
|
2052
|
+
ctx.emitProgress("episode-merge", incompleteEpisodes.length > 0 ? "warning" : "success");
|
|
2053
|
+
}
|
|
2054
|
+
// --- episode-synopsis -----------------------------------------------------
|
|
2055
|
+
export async function stageEpisodeSynopsis(ctx) {
|
|
2056
|
+
const { ws, state, options } = ctx;
|
|
2057
|
+
const provider = ctx.provider();
|
|
2058
|
+
const episodeResultsDir = ws.mkdir("episode_results");
|
|
2059
|
+
const results = ensureMergedResults(ctx);
|
|
2060
|
+
ctx.updateRunState({ status: "init_running", init_stage: "episode_synopsis" });
|
|
2061
|
+
ctx.emitProgress("episode-synopsis");
|
|
2062
|
+
state.summaryDegradedEpisodes.push(...await resolveEpisodeSynopses(provider, results, {
|
|
2063
|
+
resultsDir: episodeResultsDir,
|
|
2064
|
+
resummarize: options.resummarize,
|
|
2065
|
+
skipSummary: options.skipSummary,
|
|
2066
|
+
changedEpisodes: state.changedEpisodes,
|
|
2067
|
+
concurrency: options.concurrency,
|
|
2068
|
+
}));
|
|
2069
|
+
for (const merged of results) {
|
|
2070
|
+
writeJson(episodeResultPath(episodeResultsDir, merged), compactEpisodeResult(merged));
|
|
2071
|
+
const errPath = episodeErrorPath(episodeResultsDir, merged);
|
|
2072
|
+
if (exists(errPath))
|
|
2073
|
+
deletePath(errPath);
|
|
2074
|
+
}
|
|
2075
|
+
ctx.emitProgress("episode-synopsis", state.summaryDegradedEpisodes.length > 0 ? "warning" : "success");
|
|
2076
|
+
// Incomplete episodes are a resumable, non-error stop. The full-pipeline
|
|
2077
|
+
// runner converts this into the INIT INCOMPLETE report + EXIT_RUNTIME; a
|
|
2078
|
+
// standalone `direct run episode-synopsis` surfaces it the same way.
|
|
2079
|
+
if (state.incompleteEpisodes.length > 0) {
|
|
2080
|
+
state.incompleteEpisodes.sort((a, b) => a - b);
|
|
2081
|
+
ctx.updateRunState({
|
|
1649
2082
|
status: "init_incomplete",
|
|
1650
2083
|
init_stage: "batch_extract",
|
|
1651
|
-
episode_total: totalEpisodes,
|
|
1652
|
-
episode_completed:
|
|
1653
|
-
incomplete_episodes: incompleteEpisodes,
|
|
1654
|
-
batch_total: totalBatches,
|
|
1655
|
-
batch_completed: doneBatches,
|
|
1656
|
-
batch_failed: erroredBatches,
|
|
2084
|
+
episode_total: state.totalEpisodes,
|
|
2085
|
+
episode_completed: state.mergedResults.length,
|
|
2086
|
+
incomplete_episodes: state.incompleteEpisodes,
|
|
2087
|
+
batch_total: state.totalBatches,
|
|
2088
|
+
batch_completed: state.doneBatches,
|
|
2089
|
+
batch_failed: state.erroredBatches,
|
|
1657
2090
|
last_error: { title: "INIT INCOMPLETE: Some episodes have unextracted batches", failed_at: checkpointTimestamp() },
|
|
1658
2091
|
});
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
title: "INIT INCOMPLETE: Some episodes have unextracted batches",
|
|
1662
|
-
result: [
|
|
1663
|
-
`episodes: ${results.length}/${totalEpisodes} complete`,
|
|
1664
|
-
`incomplete episodes: ${incompleteEpisodes.join(", ")}`,
|
|
1665
|
-
`content: ${doneBatches}/${totalBatches} done, ${erroredBatches} error`,
|
|
1666
|
-
...(titleDegraded.length > 0 ? [`titles degraded (deterministic): ${titleDegraded.join(", ")}`] : []),
|
|
1667
|
-
`provider: ${providerName}`,
|
|
1668
|
-
],
|
|
1669
|
-
artifacts: [
|
|
1670
|
-
path.join(workspace, "source.txt"),
|
|
1671
|
-
path.join(dd, "episode_plan.json"),
|
|
1672
|
-
path.join(dd, "batch_plan.json"),
|
|
1673
|
-
batchResultsDir,
|
|
1674
|
-
episodeResultsDir,
|
|
1675
|
-
path.join(dd, "run_state.json"),
|
|
1676
|
-
],
|
|
1677
|
-
next: [
|
|
1678
|
-
"Run `direct status` to see episode progress.",
|
|
1679
|
-
"Re-run one episode with `direct init --episodes <n>`, or all errors with `--retry-errors`.",
|
|
1680
|
-
"Errored units are left untouched on a plain rerun; select them to retry.",
|
|
1681
|
-
],
|
|
1682
|
-
};
|
|
1683
|
-
return [report, EXIT_RUNTIME];
|
|
2092
|
+
ctx.emitProgress("batch-extract", "error");
|
|
2093
|
+
throw new StageIncomplete();
|
|
1684
2094
|
}
|
|
1685
|
-
|
|
2095
|
+
state.mergedResults.sort((a, b) => Number(a["episode"] ?? 0) - Number(b["episode"] ?? 0));
|
|
2096
|
+
}
|
|
2097
|
+
// --- script-merge ---------------------------------------------------------
|
|
2098
|
+
export async function stageScriptMerge(ctx) {
|
|
2099
|
+
const { ws, state, options } = ctx;
|
|
2100
|
+
const results = ensureMergedResults(ctx);
|
|
2101
|
+
ctx.updateRunState({
|
|
1686
2102
|
status: "init_running",
|
|
1687
2103
|
init_stage: "script_merge",
|
|
1688
|
-
episode_total: totalEpisodes,
|
|
2104
|
+
episode_total: state.totalEpisodes || asList((ensureEpisodePlan(ctx))["episodes"]).length,
|
|
1689
2105
|
episode_completed: results.length,
|
|
1690
2106
|
incomplete_episodes: [],
|
|
1691
|
-
batch_total: totalBatches,
|
|
1692
|
-
batch_completed: doneBatches,
|
|
1693
|
-
batch_failed: erroredBatches,
|
|
2107
|
+
batch_total: state.totalBatches,
|
|
2108
|
+
batch_completed: state.doneBatches,
|
|
2109
|
+
batch_failed: state.erroredBatches,
|
|
1694
2110
|
last_error: null,
|
|
1695
2111
|
});
|
|
1696
|
-
|
|
1697
|
-
const assetNormalizationPath = path.join(dd, "asset_normalization.json");
|
|
1698
|
-
const assetMetadataPath = path.join(dd, "asset_metadata.json");
|
|
1699
|
-
let assetNormalization = null;
|
|
2112
|
+
ctx.emitProgress("script-merge");
|
|
1700
2113
|
try {
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
writeJson(assetNormalizationPath, assetNormalization);
|
|
2114
|
+
const merged = mergeEpisodeResultsWithReport(results, ensureProjectName(ctx));
|
|
2115
|
+
state.script = merged.script;
|
|
2116
|
+
state.assetNormalization = merged.assetNormalization;
|
|
2117
|
+
ws.writeJson("asset_normalization.json", state.assetNormalization);
|
|
1706
2118
|
}
|
|
1707
2119
|
catch (exc) {
|
|
1708
|
-
|
|
1709
|
-
throw initFailedReport(workspace, {
|
|
2120
|
+
stageFailure(options.workspace, exc, {
|
|
1710
2121
|
title: "INIT FAILED: Merge failed",
|
|
1711
2122
|
stage: "script_merge",
|
|
1712
2123
|
required: ["complete episode_results/*.json"],
|
|
1713
|
-
received: [`${e?.name ?? "Error"}: ${(e?.message ?? "").slice(0, 160)}`],
|
|
1714
2124
|
nextSteps: ["Rerun init; completed episode extraction checkpoints will be reused and merge will retry."],
|
|
1715
2125
|
updates: { episode_completed: results.length },
|
|
1716
2126
|
});
|
|
1717
2127
|
}
|
|
2128
|
+
}
|
|
2129
|
+
// --- asset-curation -------------------------------------------------------
|
|
2130
|
+
export async function stageAssetCuration(ctx) {
|
|
2131
|
+
const { ws, state, options } = ctx;
|
|
2132
|
+
const script = ensureScript(ctx);
|
|
1718
2133
|
try {
|
|
1719
|
-
updateRunState(
|
|
1720
|
-
|
|
1721
|
-
// Location merge is deterministic-only: actor/prop pruning runs from scene
|
|
1722
|
-
// counts and every location is kept. The previous provider location-merge
|
|
1723
|
-
// call was removed (single large streaming request kept dropping the gateway
|
|
1724
|
-
// connection); duplicate locations can be merged later if needed.
|
|
2134
|
+
ctx.updateRunState({ status: "init_running", init_stage: "asset_curation" });
|
|
2135
|
+
ctx.emitProgress("asset-curation");
|
|
1725
2136
|
const curation = curateScriptAssets(script);
|
|
1726
|
-
writeJson(
|
|
2137
|
+
ws.writeJson("asset_curation.json", curation);
|
|
1727
2138
|
}
|
|
1728
2139
|
catch (exc) {
|
|
1729
|
-
|
|
1730
|
-
// a malformed assembled script, not a gateway/provider problem.
|
|
1731
|
-
const e = exc;
|
|
1732
|
-
throw initFailedReport(workspace, {
|
|
2140
|
+
stageFailure(options.workspace, exc, {
|
|
1733
2141
|
title: "INIT FAILED: Asset curation failed",
|
|
1734
2142
|
stage: "asset_curation",
|
|
1735
2143
|
required: ["a well-formed assembled script for deterministic asset curation"],
|
|
1736
|
-
received: [`${e?.name ?? "Error"}: ${(e?.message ?? "").slice(0, 160)}`],
|
|
1737
2144
|
nextSteps: ["Rerun init; completed extraction checkpoints will be reused."],
|
|
1738
|
-
updates: { episode_completed:
|
|
2145
|
+
updates: { episode_completed: state.mergedResults.length },
|
|
1739
2146
|
});
|
|
1740
2147
|
}
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
2148
|
+
ctx.emitProgress("asset-curation", "success");
|
|
2149
|
+
}
|
|
2150
|
+
// --- metadata -------------------------------------------------------------
|
|
2151
|
+
export async function stageMetadata(ctx) {
|
|
2152
|
+
const { ws, state, options } = ctx;
|
|
2153
|
+
const script = ensureScript(ctx);
|
|
2154
|
+
const sourceText = ensureSourceText(ctx);
|
|
2155
|
+
const assetMetadataPath = ws.pathOf("asset_metadata.json");
|
|
2156
|
+
if (options.skipMetadata) {
|
|
2157
|
+
ctx.updateRunState({ status: "init_running", init_stage: "metadata_extract", metadata_skipped: true });
|
|
1744
2158
|
if (exists(assetMetadataPath))
|
|
1745
2159
|
deletePath(assetMetadataPath);
|
|
1746
|
-
|
|
2160
|
+
ctx.emitProgress("metadata", "skipped");
|
|
2161
|
+
return;
|
|
1747
2162
|
}
|
|
1748
|
-
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
stage: "metadata_extract",
|
|
1763
|
-
exitCode: exc.exitCode,
|
|
1764
|
-
errorCode: exc.errorCode,
|
|
1765
|
-
required: exc.required.length > 0 ? exc.required : ["metadata JSON matching final script contract"],
|
|
1766
|
-
received: exc.received.length > 0 ? exc.received : [String(exc.message).slice(0, 160)],
|
|
1767
|
-
nextSteps: exc.nextSteps.length > 0 ? exc.nextSteps : ["Rerun init; extraction checkpoints will be reused and metadata will retry."],
|
|
1768
|
-
updates: { episode_completed: results.length },
|
|
1769
|
-
});
|
|
1770
|
-
}
|
|
1771
|
-
const e = exc;
|
|
1772
|
-
throw initFailedReport(workspace, {
|
|
1773
|
-
title: "INIT FAILED: Metadata extraction failed",
|
|
2163
|
+
const provider = ctx.provider();
|
|
2164
|
+
try {
|
|
2165
|
+
ctx.updateRunState({ status: "init_running", init_stage: "metadata_extract", metadata_skipped: false });
|
|
2166
|
+
ctx.emitProgress("metadata");
|
|
2167
|
+
let metadata = provider.extractMetadata ? await provider.extractMetadata(sourceText, script) : {};
|
|
2168
|
+
if (!isDict(metadata))
|
|
2169
|
+
metadata = {};
|
|
2170
|
+
ws.writeJson("asset_metadata.json", metadata);
|
|
2171
|
+
applyMetadataToScript(script, metadata);
|
|
2172
|
+
}
|
|
2173
|
+
catch (exc) {
|
|
2174
|
+
if (exc instanceof CliError) {
|
|
2175
|
+
throw initFailedReport(options.workspace, {
|
|
2176
|
+
title: exc.title,
|
|
1774
2177
|
stage: "metadata_extract",
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
2178
|
+
exitCode: exc.exitCode,
|
|
2179
|
+
errorCode: exc.errorCode,
|
|
2180
|
+
required: exc.required.length > 0 ? exc.required : ["metadata JSON matching final script contract"],
|
|
2181
|
+
received: exc.received.length > 0 ? exc.received : [String(exc.message).slice(0, 160)],
|
|
2182
|
+
nextSteps: exc.nextSteps.length > 0 ? exc.nextSteps : ["Rerun init; extraction checkpoints will be reused and metadata will retry."],
|
|
2183
|
+
updates: { episode_completed: state.mergedResults.length },
|
|
1779
2184
|
});
|
|
1780
2185
|
}
|
|
1781
|
-
|
|
2186
|
+
stageFailure(options.workspace, exc, {
|
|
2187
|
+
title: "INIT FAILED: Metadata extraction failed",
|
|
2188
|
+
stage: "metadata_extract",
|
|
2189
|
+
required: ["provider metadata for worldview, role_type, and asset descriptions"],
|
|
2190
|
+
nextSteps: ["Rerun init; extraction checkpoints will be reused and metadata will retry."],
|
|
2191
|
+
updates: { episode_completed: state.mergedResults.length },
|
|
2192
|
+
});
|
|
1782
2193
|
}
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
2194
|
+
ctx.emitProgress("metadata", "success");
|
|
2195
|
+
}
|
|
2196
|
+
// --- script-synopsis ------------------------------------------------------
|
|
2197
|
+
export async function stageScriptSynopsis(ctx) {
|
|
2198
|
+
const { ws, state, options } = ctx;
|
|
2199
|
+
const script = ensureScript(ctx);
|
|
2200
|
+
const provider = ctx.provider();
|
|
1786
2201
|
let summaryFullDegraded = false;
|
|
1787
2202
|
let summaryDegradeReason = "";
|
|
1788
2203
|
try {
|
|
1789
|
-
updateRunState(
|
|
1790
|
-
|
|
2204
|
+
ctx.updateRunState({ status: "init_running", init_stage: "script_synopsis" });
|
|
2205
|
+
ctx.emitProgress("script-synopsis");
|
|
1791
2206
|
const summary = await buildScriptSynopsis(provider, script, {
|
|
1792
|
-
dd,
|
|
1793
|
-
thresholdChars: summaryThresholdChars,
|
|
2207
|
+
dd: ws.dir,
|
|
2208
|
+
thresholdChars: options.summaryThresholdChars,
|
|
1794
2209
|
groupSize: DEFAULT_SUMMARY_GROUP_SIZE,
|
|
1795
|
-
resummarize,
|
|
1796
|
-
skipSummary,
|
|
1797
|
-
concurrency,
|
|
2210
|
+
resummarize: options.resummarize,
|
|
2211
|
+
skipSummary: options.skipSummary,
|
|
2212
|
+
concurrency: options.concurrency,
|
|
1798
2213
|
});
|
|
1799
2214
|
summaryFullDegraded = summary.degraded;
|
|
1800
2215
|
summaryDegradeReason = summary.reason ?? "";
|
|
@@ -1803,116 +2218,44 @@ async function commandInitImpl(opts) {
|
|
|
1803
2218
|
summaryFullDegraded = true;
|
|
1804
2219
|
summaryDegradeReason = providerErrorSummary(exc);
|
|
1805
2220
|
}
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
2221
|
+
state.summaryFullDegraded = summaryFullDegraded;
|
|
2222
|
+
state.summaryDegradeReason = summaryDegradeReason;
|
|
2223
|
+
state.summaryDegradedEpisodes.sort((a, b) => a - b);
|
|
2224
|
+
const summaryDegraded = summaryFullDegraded || state.summaryDegradedEpisodes.length > 0;
|
|
2225
|
+
// Persist script.initial.json now that the overview fields are assigned — the
|
|
2226
|
+
// same write point as the god-command (right after the synopsis stage).
|
|
2227
|
+
ws.writeJson("script.initial.json", script);
|
|
2228
|
+
ctx.updateRunState({
|
|
2229
|
+
summary_skipped: options.skipSummary,
|
|
1812
2230
|
summary_degraded: summaryDegraded,
|
|
1813
|
-
summary_degraded_episodes: summaryDegradedEpisodes,
|
|
2231
|
+
summary_degraded_episodes: state.summaryDegradedEpisodes,
|
|
1814
2232
|
summary_degraded_reason: summaryDegradeReason || null,
|
|
1815
2233
|
});
|
|
1816
|
-
|
|
1817
|
-
|
|
1818
|
-
|
|
2234
|
+
ctx.emitProgress("script-synopsis", summaryDegraded ? "warning" : options.skipSummary ? "skipped" : "success");
|
|
2235
|
+
}
|
|
2236
|
+
// --- validate -------------------------------------------------------------
|
|
2237
|
+
export async function stageValidate(ctx) {
|
|
2238
|
+
const { ws, state, options } = ctx;
|
|
2239
|
+
ensureScript(ctx); // ensures script.initial.json exists on disk for validateScript
|
|
2240
|
+
const scriptPath = ws.pathOf("script.initial.json");
|
|
2241
|
+
ctx.updateRunState({ status: "init_running", init_stage: "validate" });
|
|
2242
|
+
ctx.emitProgress("validate");
|
|
1819
2243
|
let validation;
|
|
1820
2244
|
try {
|
|
1821
|
-
validation = validateScript(workspace, scriptPath);
|
|
2245
|
+
validation = validateScript(options.workspace, scriptPath);
|
|
1822
2246
|
}
|
|
1823
2247
|
catch (exc) {
|
|
1824
|
-
|
|
1825
|
-
throw initFailedReport(workspace, {
|
|
2248
|
+
stageFailure(options.workspace, exc, {
|
|
1826
2249
|
title: "INIT FAILED: Validation failed",
|
|
1827
2250
|
stage: "validate",
|
|
1828
2251
|
required: ["script.initial.json that can be validated"],
|
|
1829
|
-
received: [`${e?.name ?? "Error"}: ${(e?.message ?? "").slice(0, 160)}`],
|
|
1830
2252
|
nextSteps: ["Rerun init to retry validation, or inspect script.initial.json if the failure persists."],
|
|
1831
2253
|
updates: { script_path: scriptPath },
|
|
1832
2254
|
});
|
|
1833
2255
|
}
|
|
2256
|
+
state.validation = validation;
|
|
1834
2257
|
const passed = Boolean(validation["passed"]);
|
|
1835
|
-
|
|
1836
|
-
updateRunState(workspace, {
|
|
1837
|
-
status,
|
|
1838
|
-
command: "direct init",
|
|
1839
|
-
init_stage: "complete",
|
|
1840
|
-
provider: providerName,
|
|
1841
|
-
model,
|
|
1842
|
-
concurrency,
|
|
1843
|
-
batch_mode: batchMode,
|
|
1844
|
-
batch_target_lines: batchTargetLines,
|
|
1845
|
-
batch_max_chars: batchMaxChars,
|
|
1846
|
-
batch_min_lines: batchMinLines,
|
|
1847
|
-
source_path: path.resolve(source),
|
|
1848
|
-
script_path: scriptPath,
|
|
1849
|
-
validation_path: path.join(dd, "validation.json"),
|
|
1850
|
-
episode_total: totalEpisodes,
|
|
1851
|
-
episode_completed: results.length,
|
|
1852
|
-
incomplete_episodes: [],
|
|
1853
|
-
batch_total: totalBatches,
|
|
1854
|
-
batch_completed: doneBatches,
|
|
1855
|
-
batch_reused: reusedBatchCount,
|
|
1856
|
-
batch_failed: erroredBatches,
|
|
1857
|
-
summary_threshold_chars: summaryThresholdChars,
|
|
1858
|
-
summary_skipped: skipSummary,
|
|
1859
|
-
summary_degraded: summaryDegraded,
|
|
1860
|
-
summary_degraded_episodes: summaryDegradedEpisodes,
|
|
1861
|
-
summary_degraded_reason: summaryDegradeReason || null,
|
|
1862
|
-
metadata_skipped: skipMetadata,
|
|
1863
|
-
last_error: null,
|
|
1864
|
-
});
|
|
1865
|
-
emitInitProgress(workspace, "validate", passed ? "success" : "warning");
|
|
1866
|
-
const title = passed
|
|
1867
|
-
? "INIT COMPLETE: Initial script ready"
|
|
1868
|
-
: "INIT NEEDS AGENT: Initial script written with repair issues";
|
|
1869
|
-
const stats = validation["stats"] ?? {};
|
|
1870
|
-
const summaryLine = formatSummaryStatusLine(skipSummary, summaryDegraded, summaryDegradedEpisodes);
|
|
1871
|
-
const normalizationSummary = assetNormalization?.summary ?? {
|
|
1872
|
-
asset_merges: 0,
|
|
1873
|
-
location_states_from_names: 0,
|
|
1874
|
-
location_states_from_time: 0,
|
|
1875
|
-
review_items: 0,
|
|
1876
|
-
};
|
|
1877
|
-
const report = {
|
|
1878
|
-
title,
|
|
1879
|
-
result: [
|
|
1880
|
-
`episodes: ${stats["episodes"] ?? 0}`,
|
|
1881
|
-
`scenes: ${stats["scenes"] ?? 0}`,
|
|
1882
|
-
`actions: ${stats["actions"] ?? 0}`,
|
|
1883
|
-
`validation: ${passed ? "passed" : "needs repair"}`,
|
|
1884
|
-
`provider: ${providerName}`,
|
|
1885
|
-
`episodes: ${results.length}/${totalEpisodes} complete`,
|
|
1886
|
-
`content: ${doneBatches}/${totalBatches} done`,
|
|
1887
|
-
`normalization: merges=${normalizationSummary.asset_merges}, states=${normalizationSummary.location_states_from_names + normalizationSummary.location_states_from_time}, review=${normalizationSummary.review_items}`,
|
|
1888
|
-
...(skipMetadata ? ["metadata: skipped"] : []),
|
|
1889
|
-
...(titleDegraded.length > 0 ? [`titles degraded (deterministic): ${titleDegraded.join(", ")}`] : []),
|
|
1890
|
-
...(summaryLine ? [summaryLine] : []),
|
|
1891
|
-
],
|
|
1892
|
-
artifacts: [
|
|
1893
|
-
path.join(workspace, "source.txt"),
|
|
1894
|
-
path.join(dd, "source_manifest.json"),
|
|
1895
|
-
path.join(dd, "episode_plan.json"),
|
|
1896
|
-
path.join(dd, "batch_plan.json"),
|
|
1897
|
-
batchResultsDir,
|
|
1898
|
-
episodeResultsDir,
|
|
1899
|
-
assetNormalizationPath,
|
|
1900
|
-
path.join(dd, "asset_curation.json"),
|
|
1901
|
-
...(skipMetadata ? [] : [assetMetadataPath]),
|
|
1902
|
-
path.join(dd, "synopsis.json"),
|
|
1903
|
-
scriptPath,
|
|
1904
|
-
path.join(dd, "validation.json"),
|
|
1905
|
-
path.join(dd, "run_state.json"),
|
|
1906
|
-
],
|
|
1907
|
-
issues: summarizeIssues(asList(validation["issues"])),
|
|
1908
|
-
next: providerName === "mock"
|
|
1909
|
-
? [
|
|
1910
|
-
"Run `direct review` (add --episode <n> for source↔extract); edit with --draft if needed; then validate/export.",
|
|
1911
|
-
"Do not export mock-provider results for delivery.",
|
|
1912
|
-
]
|
|
1913
|
-
: ["Run `direct review` (add --episode <n> for source↔extract); edit with --draft if needed; then validate/export."],
|
|
1914
|
-
};
|
|
1915
|
-
return [report, passed ? EXIT_OK : EXIT_NEEDS_AGENT];
|
|
2258
|
+
ctx.emitProgress("validate", passed ? "success" : "warning");
|
|
1916
2259
|
}
|
|
1917
2260
|
// ---------------------------------------------------------------------------
|
|
1918
2261
|
// command_status — the agent-facing state table, derived entirely from disk: a
|