@lingjingai/scriptctl 0.28.3 → 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.28.4.md +13 -0
- 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 +725 -360
- 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,15 +1,37 @@
|
|
|
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 "";
|
|
11
19
|
return String(v);
|
|
12
20
|
}
|
|
21
|
+
function legacyDirectInitStage(stage) {
|
|
22
|
+
if (stage === "episode_synopsis")
|
|
23
|
+
return "episode_merge";
|
|
24
|
+
if (stage === "script_synopsis")
|
|
25
|
+
return "summary";
|
|
26
|
+
return stage;
|
|
27
|
+
}
|
|
28
|
+
function runStateDirectStage(state) {
|
|
29
|
+
const initStage = strOf(state["init_stage"]);
|
|
30
|
+
const directStage = strOf(state["direct_stage"]);
|
|
31
|
+
if (directStage && legacyDirectInitStage(directStage) === initStage)
|
|
32
|
+
return directStage;
|
|
33
|
+
return initStage;
|
|
34
|
+
}
|
|
13
35
|
function isDict(v) {
|
|
14
36
|
return typeof v === "object" && v !== null && !Array.isArray(v);
|
|
15
37
|
}
|
|
@@ -69,6 +91,14 @@ export function updateRunState(workspace, updates) {
|
|
|
69
91
|
state = {};
|
|
70
92
|
}
|
|
71
93
|
}
|
|
94
|
+
const directStage = strOf(updates["direct_stage"]);
|
|
95
|
+
const initStage = strOf(updates["init_stage"]);
|
|
96
|
+
if (directStage)
|
|
97
|
+
updates["init_stage"] = legacyDirectInitStage(directStage);
|
|
98
|
+
else if (initStage) {
|
|
99
|
+
updates["direct_stage"] = initStage;
|
|
100
|
+
updates["init_stage"] = legacyDirectInitStage(initStage);
|
|
101
|
+
}
|
|
72
102
|
Object.assign(state, updates);
|
|
73
103
|
state["updated_at"] = checkpointTimestamp();
|
|
74
104
|
writeJson(p, state);
|
|
@@ -549,7 +579,7 @@ function emitInitProgress(workspace, stageName, fallbackStatus = "running") {
|
|
|
549
579
|
});
|
|
550
580
|
}
|
|
551
581
|
function emitFailedInitProgress(workspace) {
|
|
552
|
-
const stage =
|
|
582
|
+
const stage = runStateDirectStage(readRunState(workspace));
|
|
553
583
|
if (isDirectStageName(stage))
|
|
554
584
|
emitInitProgress(workspace, stage, "error");
|
|
555
585
|
}
|
|
@@ -562,7 +592,7 @@ function buildDirectStatusSnapshot(workspace) {
|
|
|
562
592
|
const episodes = asList(episodePlan["episodes"]);
|
|
563
593
|
const batches = asList(batchPlan["batches"]);
|
|
564
594
|
const state = readRunState(workspace);
|
|
565
|
-
const rawRunningStage =
|
|
595
|
+
const rawRunningStage = runStateDirectStage(state);
|
|
566
596
|
const runningStage = rawRunningStage && rawRunningStage !== "complete" && isDirectStageName(rawRunningStage) ? rawRunningStage : null;
|
|
567
597
|
const runStatus = strOf(state["status"]);
|
|
568
598
|
const failedStage = runStatus === "init_failed" ? runningStage : null;
|
|
@@ -972,6 +1002,21 @@ function initFailedReport(workspace, opts) {
|
|
|
972
1002
|
nextSteps: opts.nextSteps,
|
|
973
1003
|
});
|
|
974
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
|
+
}
|
|
975
1020
|
// ---------------------------------------------------------------------------
|
|
976
1021
|
// Concurrency limiter (mirrors ThreadPoolExecutor)
|
|
977
1022
|
// ---------------------------------------------------------------------------
|
|
@@ -1281,7 +1326,11 @@ export async function commandInit(opts) {
|
|
|
1281
1326
|
finishInitProgress();
|
|
1282
1327
|
}
|
|
1283
1328
|
}
|
|
1284
|
-
|
|
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) {
|
|
1285
1334
|
const sourcePathArg = strOf(opts["source_path"]);
|
|
1286
1335
|
const source = sourcePathArg.startsWith("~")
|
|
1287
1336
|
? path.join(process.env.HOME ?? "", sourcePathArg.slice(1))
|
|
@@ -1305,7 +1354,7 @@ async function commandInitImpl(opts) {
|
|
|
1305
1354
|
const skipMetadata = Boolean(opts["skip_metadata"]);
|
|
1306
1355
|
const resummarize = Boolean(opts["resummarize"]);
|
|
1307
1356
|
const summaryThresholdChars = parsePositiveIntOption(opts["summary_threshold_chars"], "--summary-threshold-chars", "USAGE ERROR: Invalid summary threshold", DEFAULT_SUMMARY_REDUCE_THRESHOLD_CHARS);
|
|
1308
|
-
const concurrency = parsePositiveIntOption(opts["concurrency"], "--concurrency", "USAGE ERROR: Invalid concurrency",
|
|
1357
|
+
const concurrency = parsePositiveIntOption(opts["concurrency"], "--concurrency", "USAGE ERROR: Invalid concurrency", DEFAULT_DIRECT_CONCURRENCY);
|
|
1309
1358
|
const batchMode = strOf(opts["batch_mode"] || DEFAULT_BATCH_MODE).trim();
|
|
1310
1359
|
if (batchMode !== "episode") {
|
|
1311
1360
|
throw new CliError("USAGE ERROR: Invalid batch mode", "Invalid batch mode.", {
|
|
@@ -1326,6 +1375,66 @@ async function commandInitImpl(opts) {
|
|
|
1326
1375
|
const batchTargetLines = batchValues["batch_target_lines"];
|
|
1327
1376
|
const batchMaxChars = batchValues["batch_max_chars"];
|
|
1328
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) {
|
|
1329
1438
|
if (!exists(source) || !fs.statSync(source).isFile()) {
|
|
1330
1439
|
throw new CliError("INIT BLOCKED: Source file not found", "Source file not found.", {
|
|
1331
1440
|
exitCode: EXIT_INPUT,
|
|
@@ -1344,26 +1453,350 @@ async function commandInitImpl(opts) {
|
|
|
1344
1453
|
nextSteps: ["Use a supported source file and rerun init."],
|
|
1345
1454
|
});
|
|
1346
1455
|
}
|
|
1347
|
-
|
|
1348
|
-
|
|
1349
|
-
|
|
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, {
|
|
1350
1555
|
status: "init_running",
|
|
1351
1556
|
command: "direct init",
|
|
1352
1557
|
init_stage: "source_prepare",
|
|
1353
|
-
provider: providerName,
|
|
1354
|
-
model,
|
|
1355
|
-
concurrency,
|
|
1356
|
-
source_path: path.resolve(source),
|
|
1357
|
-
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,
|
|
1358
1563
|
});
|
|
1359
|
-
|
|
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");
|
|
1360
1793
|
let info;
|
|
1361
1794
|
try {
|
|
1362
|
-
info = await prepareSource(source, workspace);
|
|
1795
|
+
info = await prepareSource(source, options.workspace);
|
|
1363
1796
|
}
|
|
1364
1797
|
catch (exc) {
|
|
1365
1798
|
if (exc instanceof CliError) {
|
|
1366
|
-
updateRunState(
|
|
1799
|
+
ctx.updateRunState({
|
|
1367
1800
|
status: "init_failed",
|
|
1368
1801
|
init_stage: "source_prepare",
|
|
1369
1802
|
last_error: { title: exc.title, received: exc.received, failed_at: checkpointTimestamp() },
|
|
@@ -1372,7 +1805,7 @@ async function commandInitImpl(opts) {
|
|
|
1372
1805
|
}
|
|
1373
1806
|
const e = exc;
|
|
1374
1807
|
const receivedError = `${source}: ${e?.name ?? "Error"}${e?.message ? `: ${e.message}` : ""}`;
|
|
1375
|
-
updateRunState(
|
|
1808
|
+
ctx.updateRunState({
|
|
1376
1809
|
status: "init_failed",
|
|
1377
1810
|
init_stage: "source_prepare",
|
|
1378
1811
|
last_error: { title: "INIT BLOCKED: Source preparation failed", received: [receivedError], failed_at: checkpointTimestamp() },
|
|
@@ -1385,108 +1818,102 @@ async function commandInitImpl(opts) {
|
|
|
1385
1818
|
});
|
|
1386
1819
|
}
|
|
1387
1820
|
const sourceTextPath = strOf(info["sourceTextPath"]);
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
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");
|
|
1392
1832
|
let plan;
|
|
1393
1833
|
try {
|
|
1394
1834
|
plan = buildEpisodePlan(sourceText);
|
|
1395
1835
|
}
|
|
1396
1836
|
catch (exc) {
|
|
1397
|
-
|
|
1398
|
-
throw initFailedReport(workspace, {
|
|
1837
|
+
stageFailure(options.workspace, exc, {
|
|
1399
1838
|
title: "INIT FAILED: Episode planning failed",
|
|
1400
1839
|
stage: "episode_plan",
|
|
1401
1840
|
required: ["source.txt that can be split into episodes"],
|
|
1402
|
-
received: [`${e?.name ?? "Error"}: ${(e?.message ?? "").slice(0, 160)}`],
|
|
1403
1841
|
nextSteps: ["Inspect workspace/source.txt, fix the source file, and rerun init."],
|
|
1404
1842
|
});
|
|
1405
1843
|
}
|
|
1406
|
-
|
|
1407
|
-
|
|
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);
|
|
1408
1863
|
if (capabilities && !capabilities.extractBatch && !capabilities.extractEpisode) {
|
|
1409
|
-
throw
|
|
1410
|
-
title: "INIT FAILED: provider lacks batch extraction",
|
|
1411
|
-
stage: "episode_titles",
|
|
1412
|
-
required: ["provider.extractBatch or provider.extractEpisode"],
|
|
1413
|
-
received: [`--provider ${providerName}`],
|
|
1414
|
-
nextSteps: ["Use --provider anthropic or --provider mock for direct init."],
|
|
1415
|
-
});
|
|
1416
|
-
}
|
|
1417
|
-
let provider;
|
|
1418
|
-
try {
|
|
1419
|
-
provider = makeProvider(providerName, model);
|
|
1420
|
-
}
|
|
1421
|
-
catch (exc) {
|
|
1422
|
-
if (exc instanceof CliError) {
|
|
1423
|
-
updateRunState(workspace, {
|
|
1424
|
-
status: "init_failed",
|
|
1425
|
-
init_stage: "episode_titles",
|
|
1426
|
-
last_error: { title: exc.title, received: exc.received, failed_at: checkpointTimestamp() },
|
|
1427
|
-
});
|
|
1428
|
-
}
|
|
1429
|
-
throw exc;
|
|
1864
|
+
throw lacksExtraction();
|
|
1430
1865
|
}
|
|
1866
|
+
const provider = ctx.provider();
|
|
1431
1867
|
if (!provider.extractBatch && !provider.extractEpisode) {
|
|
1432
|
-
throw
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
|
|
1439
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
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);
|
|
1443
1881
|
let batchPlan;
|
|
1444
1882
|
try {
|
|
1445
1883
|
batchPlan = buildBatchPlan(sourceText, plan, {
|
|
1446
|
-
targetLines: batchTargetLines,
|
|
1447
|
-
maxChars: batchMaxChars,
|
|
1448
|
-
minLines: batchMinLines,
|
|
1449
|
-
mode: batchMode,
|
|
1884
|
+
targetLines: options.batchTargetLines,
|
|
1885
|
+
maxChars: options.batchMaxChars,
|
|
1886
|
+
minLines: options.batchMinLines,
|
|
1887
|
+
mode: options.batchMode,
|
|
1450
1888
|
});
|
|
1451
1889
|
}
|
|
1452
1890
|
catch (exc) {
|
|
1453
|
-
|
|
1454
|
-
throw initFailedReport(workspace, {
|
|
1891
|
+
stageFailure(options.workspace, exc, {
|
|
1455
1892
|
title: "INIT FAILED: Batch planning failed",
|
|
1456
1893
|
stage: "batch_plan",
|
|
1457
1894
|
required: ["episode_plan.json that can be split into natural paragraph batches"],
|
|
1458
|
-
received: [`${e?.name ?? "Error"}: ${(e?.message ?? "").slice(0, 160)}`],
|
|
1459
1895
|
nextSteps: ["Inspect workspace/source.txt and episode_plan.json, then rerun init."],
|
|
1460
1896
|
});
|
|
1461
1897
|
}
|
|
1462
|
-
|
|
1463
|
-
writeJson(
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
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.
|
|
1474
1912
|
gcOrphanUnits(episodeResultsDir, new Set(asList(plan["episodes"]).map((ep) => episodeResultKey(ep))));
|
|
1475
1913
|
gcOrphanUnits(batchResultsDir, new Set(asList(batchPlan["batches"]).map((b) => batchResultKey(b))));
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
if (!batchesByEpisode.has(epNum))
|
|
1480
|
-
batchesByEpisode.set(epNum, []);
|
|
1481
|
-
batchesByEpisode.get(epNum).push(batch);
|
|
1482
|
-
}
|
|
1483
|
-
const totalEpisodes = asList(plan["episodes"]).length;
|
|
1484
|
-
const totalBatches = asList(batchPlan["batches"]).length;
|
|
1485
|
-
// Decide which batches to run. State (classifyBatch) is shared with `direct
|
|
1486
|
-
// status` so the two never disagree: a "done" batch is reused, an "error"
|
|
1487
|
-
// batch is sticky (skipped on a plain rerun so a content filter can't loop),
|
|
1488
|
-
// a "pending" (never-run or corrupt) batch runs. The agent overrides with
|
|
1489
|
-
// 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).
|
|
1490
1917
|
const pending = [];
|
|
1491
1918
|
let reusedBatchCount = 0;
|
|
1492
1919
|
let stickyErrorCount = 0;
|
|
@@ -1494,7 +1921,7 @@ async function commandInitImpl(opts) {
|
|
|
1494
1921
|
const epNum = Number(batch["episode"]);
|
|
1495
1922
|
const key = batchResultKey(batch);
|
|
1496
1923
|
const errorPath = batchErrorPath(batchResultsDir, batch);
|
|
1497
|
-
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));
|
|
1498
1925
|
if (forced) {
|
|
1499
1926
|
const resultPath = batchResultPath(batchResultsDir, batch);
|
|
1500
1927
|
if (exists(resultPath))
|
|
@@ -1510,74 +1937,98 @@ async function commandInitImpl(opts) {
|
|
|
1510
1937
|
break;
|
|
1511
1938
|
case "error":
|
|
1512
1939
|
stickyErrorCount++;
|
|
1513
|
-
break;
|
|
1514
|
-
default: pending.push(batch);
|
|
1940
|
+
break;
|
|
1941
|
+
default: pending.push(batch);
|
|
1515
1942
|
}
|
|
1516
1943
|
}
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
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({
|
|
1523
1949
|
status: "init_running",
|
|
1524
1950
|
init_stage: "batch_extract",
|
|
1525
|
-
batch_mode: batchMode,
|
|
1526
|
-
batch_target_lines: batchTargetLines,
|
|
1527
|
-
batch_max_chars: batchMaxChars,
|
|
1528
|
-
batch_min_lines: batchMinLines,
|
|
1529
|
-
episode_total: totalEpisodes,
|
|
1530
|
-
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,
|
|
1531
1957
|
});
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
// whole pool drains. Per-unit files have no shared index, so concurrent writes
|
|
1535
|
-
// are safe — and batch_results/ fills up live, so a crash/kill mid-run keeps
|
|
1536
|
-
// every batch already extracted (the resume model depends on this).
|
|
1537
|
-
const outcomes = await pMapWithConcurrency(pending, concurrency, async (batch) => {
|
|
1958
|
+
ctx.emitProgress("batch-extract");
|
|
1959
|
+
const outcomes = await pMapWithConcurrency(pending, options.concurrency, async (batch) => {
|
|
1538
1960
|
try {
|
|
1539
1961
|
const result = await extractBatchWithRecovery(provider, sourceText, batch);
|
|
1540
1962
|
persistBatchResult(batchResultsDir, batch, result);
|
|
1541
1963
|
const errorPath = batchErrorPath(batchResultsDir, batch);
|
|
1542
1964
|
if (exists(errorPath))
|
|
1543
1965
|
deletePath(errorPath);
|
|
1544
|
-
|
|
1966
|
+
ctx.emitProgress("batch-extract");
|
|
1545
1967
|
return true;
|
|
1546
1968
|
}
|
|
1547
1969
|
catch (error) {
|
|
1548
1970
|
writeBatchFailure(batchResultsDir, batch, error);
|
|
1549
|
-
|
|
1971
|
+
ctx.emitProgress("batch-extract");
|
|
1550
1972
|
return false;
|
|
1551
1973
|
}
|
|
1552
1974
|
});
|
|
1553
1975
|
const freshOk = outcomes.filter((o) => o.ok && o.value === true).length;
|
|
1554
1976
|
const freshFail = outcomes.filter((o) => o.ok && o.value === false).length;
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
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
|
+
}
|
|
1560
2014
|
const results = [];
|
|
1561
2015
|
const incompleteEpisodes = [];
|
|
1562
|
-
updateRunState(
|
|
2016
|
+
ctx.updateRunState({
|
|
1563
2017
|
status: "init_running",
|
|
1564
2018
|
init_stage: "episode_merge",
|
|
1565
|
-
episode_total: totalEpisodes,
|
|
1566
|
-
batch_total: totalBatches,
|
|
1567
|
-
batch_completed: doneBatches,
|
|
1568
|
-
batch_failed: erroredBatches,
|
|
2019
|
+
episode_total: state.totalEpisodes,
|
|
2020
|
+
batch_total: state.totalBatches,
|
|
2021
|
+
batch_completed: state.doneBatches,
|
|
2022
|
+
batch_failed: state.erroredBatches,
|
|
1569
2023
|
});
|
|
1570
|
-
|
|
2024
|
+
ctx.emitProgress("episode-merge");
|
|
1571
2025
|
try {
|
|
1572
|
-
// Assemble every complete episode (incomplete ones are reported and skipped).
|
|
1573
2026
|
for (const episode of asList(plan["episodes"])) {
|
|
1574
2027
|
const episodeNum = Number(episode["episode"]);
|
|
1575
2028
|
const epBatches = batchesByEpisode.get(episodeNum) ?? [];
|
|
1576
2029
|
const batchResults = epBatches.map((b) => readBatchResult(batchResultsDir, b)).filter((r) => r !== null);
|
|
1577
2030
|
if (epBatches.length === 0 || batchResults.length !== epBatches.length) {
|
|
1578
2031
|
incompleteEpisodes.push(episodeNum);
|
|
1579
|
-
// Drop any stale episode_results/ep_NNN.json from a prior complete run so
|
|
1580
|
-
// the derived artifact never claims an episode is complete when it isn't.
|
|
1581
2032
|
const stale = episodeResultPath(episodeResultsDir, episode);
|
|
1582
2033
|
if (exists(stale))
|
|
1583
2034
|
deletePath(stale);
|
|
@@ -1585,194 +2036,180 @@ async function commandInitImpl(opts) {
|
|
|
1585
2036
|
}
|
|
1586
2037
|
const merged = mergeBatchResultsForEpisode(episode, batchResults);
|
|
1587
2038
|
validateEpisodeExtractionQuality(sourceText, episode, merged);
|
|
1588
|
-
results.push(merged);
|
|
2039
|
+
results.push(merged);
|
|
1589
2040
|
}
|
|
1590
|
-
emitInitProgress(workspace, "episode_merge", incompleteEpisodes.length > 0 ? "warning" : "success");
|
|
1591
|
-
// Episode-level synopsis (reduce-1): resolve each result's synopsis in place
|
|
1592
|
-
// (passthrough / reuse / skip / concurrent LLM stitch). Reads prior `syn`
|
|
1593
|
-
// BEFORE the persist loop below overwrites ep_NNN.json.
|
|
1594
|
-
updateRunState(workspace, { status: "init_running", init_stage: "episode_synopsis" });
|
|
1595
|
-
emitInitProgress(workspace, "episode_synopsis");
|
|
1596
|
-
summaryDegradedEpisodes.push(...await resolveEpisodeSynopses(provider, results, {
|
|
1597
|
-
resultsDir: episodeResultsDir,
|
|
1598
|
-
resummarize,
|
|
1599
|
-
skipSummary,
|
|
1600
|
-
changedEpisodes,
|
|
1601
|
-
concurrency,
|
|
1602
|
-
}));
|
|
1603
|
-
// Persist every assembled episode (synopsis now resolved for all of them).
|
|
1604
|
-
// merged carries `episode`, so it doubles as the path key.
|
|
1605
|
-
for (const merged of results) {
|
|
1606
|
-
writeJson(episodeResultPath(episodeResultsDir, merged), compactEpisodeResult(merged));
|
|
1607
|
-
const errPath = episodeErrorPath(episodeResultsDir, merged);
|
|
1608
|
-
if (exists(errPath))
|
|
1609
|
-
deletePath(errPath);
|
|
1610
|
-
}
|
|
1611
|
-
emitInitProgress(workspace, "episode_synopsis", summaryDegradedEpisodes.length > 0 ? "warning" : "success");
|
|
1612
2041
|
}
|
|
1613
2042
|
catch (exc) {
|
|
1614
|
-
|
|
1615
|
-
throw initFailedReport(workspace, {
|
|
2043
|
+
stageFailure(ctx.options.workspace, exc, {
|
|
1616
2044
|
title: "INIT FAILED: Episode merge failed",
|
|
1617
2045
|
stage: "episode_merge",
|
|
1618
2046
|
required: ["batch_results/*.json that can merge into episode_results/*.json"],
|
|
1619
|
-
received: [`${e?.name ?? "Error"}: ${(e?.message ?? "").slice(0, 160)}`],
|
|
1620
2047
|
nextSteps: ["Rerun init; completed batches are reused and episode merge will retry."],
|
|
1621
2048
|
});
|
|
1622
2049
|
}
|
|
1623
|
-
|
|
1624
|
-
incompleteEpisodes
|
|
1625
|
-
|
|
1626
|
-
|
|
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({
|
|
1627
2082
|
status: "init_incomplete",
|
|
1628
2083
|
init_stage: "batch_extract",
|
|
1629
|
-
episode_total: totalEpisodes,
|
|
1630
|
-
episode_completed:
|
|
1631
|
-
incomplete_episodes: incompleteEpisodes,
|
|
1632
|
-
batch_total: totalBatches,
|
|
1633
|
-
batch_completed: doneBatches,
|
|
1634
|
-
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,
|
|
1635
2090
|
last_error: { title: "INIT INCOMPLETE: Some episodes have unextracted batches", failed_at: checkpointTimestamp() },
|
|
1636
2091
|
});
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
title: "INIT INCOMPLETE: Some episodes have unextracted batches",
|
|
1640
|
-
result: [
|
|
1641
|
-
`episodes: ${results.length}/${totalEpisodes} complete`,
|
|
1642
|
-
`incomplete episodes: ${incompleteEpisodes.join(", ")}`,
|
|
1643
|
-
`content: ${doneBatches}/${totalBatches} done, ${erroredBatches} error`,
|
|
1644
|
-
...(titleDegraded.length > 0 ? [`titles degraded (deterministic): ${titleDegraded.join(", ")}`] : []),
|
|
1645
|
-
`provider: ${providerName}`,
|
|
1646
|
-
],
|
|
1647
|
-
artifacts: [
|
|
1648
|
-
path.join(workspace, "source.txt"),
|
|
1649
|
-
path.join(dd, "episode_plan.json"),
|
|
1650
|
-
path.join(dd, "batch_plan.json"),
|
|
1651
|
-
batchResultsDir,
|
|
1652
|
-
episodeResultsDir,
|
|
1653
|
-
path.join(dd, "run_state.json"),
|
|
1654
|
-
],
|
|
1655
|
-
next: [
|
|
1656
|
-
"Run `direct status` to see episode progress.",
|
|
1657
|
-
"Re-run one episode with `direct init --episodes <n>`, or all errors with `--retry-errors`.",
|
|
1658
|
-
"Errored units are left untouched on a plain rerun; select them to retry.",
|
|
1659
|
-
],
|
|
1660
|
-
};
|
|
1661
|
-
return [report, EXIT_RUNTIME];
|
|
2092
|
+
ctx.emitProgress("batch-extract", "error");
|
|
2093
|
+
throw new StageIncomplete();
|
|
1662
2094
|
}
|
|
1663
|
-
|
|
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({
|
|
1664
2102
|
status: "init_running",
|
|
1665
2103
|
init_stage: "script_merge",
|
|
1666
|
-
episode_total: totalEpisodes,
|
|
2104
|
+
episode_total: state.totalEpisodes || asList((ensureEpisodePlan(ctx))["episodes"]).length,
|
|
1667
2105
|
episode_completed: results.length,
|
|
1668
2106
|
incomplete_episodes: [],
|
|
1669
|
-
batch_total: totalBatches,
|
|
1670
|
-
batch_completed: doneBatches,
|
|
1671
|
-
batch_failed: erroredBatches,
|
|
2107
|
+
batch_total: state.totalBatches,
|
|
2108
|
+
batch_completed: state.doneBatches,
|
|
2109
|
+
batch_failed: state.erroredBatches,
|
|
1672
2110
|
last_error: null,
|
|
1673
2111
|
});
|
|
1674
|
-
|
|
1675
|
-
const assetNormalizationPath = path.join(dd, "asset_normalization.json");
|
|
1676
|
-
const assetMetadataPath = path.join(dd, "asset_metadata.json");
|
|
1677
|
-
let assetNormalization = null;
|
|
2112
|
+
ctx.emitProgress("script-merge");
|
|
1678
2113
|
try {
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
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);
|
|
1684
2118
|
}
|
|
1685
2119
|
catch (exc) {
|
|
1686
|
-
|
|
1687
|
-
throw initFailedReport(workspace, {
|
|
2120
|
+
stageFailure(options.workspace, exc, {
|
|
1688
2121
|
title: "INIT FAILED: Merge failed",
|
|
1689
2122
|
stage: "script_merge",
|
|
1690
2123
|
required: ["complete episode_results/*.json"],
|
|
1691
|
-
received: [`${e?.name ?? "Error"}: ${(e?.message ?? "").slice(0, 160)}`],
|
|
1692
2124
|
nextSteps: ["Rerun init; completed episode extraction checkpoints will be reused and merge will retry."],
|
|
1693
2125
|
updates: { episode_completed: results.length },
|
|
1694
2126
|
});
|
|
1695
2127
|
}
|
|
2128
|
+
}
|
|
2129
|
+
// --- asset-curation -------------------------------------------------------
|
|
2130
|
+
export async function stageAssetCuration(ctx) {
|
|
2131
|
+
const { ws, state, options } = ctx;
|
|
2132
|
+
const script = ensureScript(ctx);
|
|
1696
2133
|
try {
|
|
1697
|
-
updateRunState(
|
|
1698
|
-
|
|
1699
|
-
// Location merge is deterministic-only: actor/prop pruning runs from scene
|
|
1700
|
-
// counts and every location is kept. The previous provider location-merge
|
|
1701
|
-
// call was removed (single large streaming request kept dropping the gateway
|
|
1702
|
-
// connection); duplicate locations can be merged later if needed.
|
|
2134
|
+
ctx.updateRunState({ status: "init_running", init_stage: "asset_curation" });
|
|
2135
|
+
ctx.emitProgress("asset-curation");
|
|
1703
2136
|
const curation = curateScriptAssets(script);
|
|
1704
|
-
writeJson(
|
|
2137
|
+
ws.writeJson("asset_curation.json", curation);
|
|
1705
2138
|
}
|
|
1706
2139
|
catch (exc) {
|
|
1707
|
-
|
|
1708
|
-
// a malformed assembled script, not a gateway/provider problem.
|
|
1709
|
-
const e = exc;
|
|
1710
|
-
throw initFailedReport(workspace, {
|
|
2140
|
+
stageFailure(options.workspace, exc, {
|
|
1711
2141
|
title: "INIT FAILED: Asset curation failed",
|
|
1712
2142
|
stage: "asset_curation",
|
|
1713
2143
|
required: ["a well-formed assembled script for deterministic asset curation"],
|
|
1714
|
-
received: [`${e?.name ?? "Error"}: ${(e?.message ?? "").slice(0, 160)}`],
|
|
1715
2144
|
nextSteps: ["Rerun init; completed extraction checkpoints will be reused."],
|
|
1716
|
-
updates: { episode_completed:
|
|
2145
|
+
updates: { episode_completed: state.mergedResults.length },
|
|
1717
2146
|
});
|
|
1718
2147
|
}
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
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 });
|
|
1722
2158
|
if (exists(assetMetadataPath))
|
|
1723
2159
|
deletePath(assetMetadataPath);
|
|
1724
|
-
|
|
2160
|
+
ctx.emitProgress("metadata", "skipped");
|
|
2161
|
+
return;
|
|
1725
2162
|
}
|
|
1726
|
-
|
|
1727
|
-
|
|
1728
|
-
|
|
1729
|
-
|
|
1730
|
-
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
stage: "metadata_extract",
|
|
1741
|
-
exitCode: exc.exitCode,
|
|
1742
|
-
errorCode: exc.errorCode,
|
|
1743
|
-
required: exc.required.length > 0 ? exc.required : ["metadata JSON matching final script contract"],
|
|
1744
|
-
received: exc.received.length > 0 ? exc.received : [String(exc.message).slice(0, 160)],
|
|
1745
|
-
nextSteps: exc.nextSteps.length > 0 ? exc.nextSteps : ["Rerun init; extraction checkpoints will be reused and metadata will retry."],
|
|
1746
|
-
updates: { episode_completed: results.length },
|
|
1747
|
-
});
|
|
1748
|
-
}
|
|
1749
|
-
const e = exc;
|
|
1750
|
-
throw initFailedReport(workspace, {
|
|
1751
|
-
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,
|
|
1752
2177
|
stage: "metadata_extract",
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
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 },
|
|
1757
2184
|
});
|
|
1758
2185
|
}
|
|
1759
|
-
|
|
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
|
+
});
|
|
1760
2193
|
}
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
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();
|
|
1764
2201
|
let summaryFullDegraded = false;
|
|
1765
2202
|
let summaryDegradeReason = "";
|
|
1766
2203
|
try {
|
|
1767
|
-
updateRunState(
|
|
1768
|
-
|
|
2204
|
+
ctx.updateRunState({ status: "init_running", init_stage: "script_synopsis" });
|
|
2205
|
+
ctx.emitProgress("script-synopsis");
|
|
1769
2206
|
const summary = await buildScriptSynopsis(provider, script, {
|
|
1770
|
-
dd,
|
|
1771
|
-
thresholdChars: summaryThresholdChars,
|
|
2207
|
+
dd: ws.dir,
|
|
2208
|
+
thresholdChars: options.summaryThresholdChars,
|
|
1772
2209
|
groupSize: DEFAULT_SUMMARY_GROUP_SIZE,
|
|
1773
|
-
resummarize,
|
|
1774
|
-
skipSummary,
|
|
1775
|
-
concurrency,
|
|
2210
|
+
resummarize: options.resummarize,
|
|
2211
|
+
skipSummary: options.skipSummary,
|
|
2212
|
+
concurrency: options.concurrency,
|
|
1776
2213
|
});
|
|
1777
2214
|
summaryFullDegraded = summary.degraded;
|
|
1778
2215
|
summaryDegradeReason = summary.reason ?? "";
|
|
@@ -1781,116 +2218,44 @@ async function commandInitImpl(opts) {
|
|
|
1781
2218
|
summaryFullDegraded = true;
|
|
1782
2219
|
summaryDegradeReason = providerErrorSummary(exc);
|
|
1783
2220
|
}
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
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,
|
|
1790
2230
|
summary_degraded: summaryDegraded,
|
|
1791
|
-
summary_degraded_episodes: summaryDegradedEpisodes,
|
|
2231
|
+
summary_degraded_episodes: state.summaryDegradedEpisodes,
|
|
1792
2232
|
summary_degraded_reason: summaryDegradeReason || null,
|
|
1793
2233
|
});
|
|
1794
|
-
|
|
1795
|
-
|
|
1796
|
-
|
|
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");
|
|
1797
2243
|
let validation;
|
|
1798
2244
|
try {
|
|
1799
|
-
validation = validateScript(workspace, scriptPath);
|
|
2245
|
+
validation = validateScript(options.workspace, scriptPath);
|
|
1800
2246
|
}
|
|
1801
2247
|
catch (exc) {
|
|
1802
|
-
|
|
1803
|
-
throw initFailedReport(workspace, {
|
|
2248
|
+
stageFailure(options.workspace, exc, {
|
|
1804
2249
|
title: "INIT FAILED: Validation failed",
|
|
1805
2250
|
stage: "validate",
|
|
1806
2251
|
required: ["script.initial.json that can be validated"],
|
|
1807
|
-
received: [`${e?.name ?? "Error"}: ${(e?.message ?? "").slice(0, 160)}`],
|
|
1808
2252
|
nextSteps: ["Rerun init to retry validation, or inspect script.initial.json if the failure persists."],
|
|
1809
2253
|
updates: { script_path: scriptPath },
|
|
1810
2254
|
});
|
|
1811
2255
|
}
|
|
2256
|
+
state.validation = validation;
|
|
1812
2257
|
const passed = Boolean(validation["passed"]);
|
|
1813
|
-
|
|
1814
|
-
updateRunState(workspace, {
|
|
1815
|
-
status,
|
|
1816
|
-
command: "direct init",
|
|
1817
|
-
init_stage: "complete",
|
|
1818
|
-
provider: providerName,
|
|
1819
|
-
model,
|
|
1820
|
-
concurrency,
|
|
1821
|
-
batch_mode: batchMode,
|
|
1822
|
-
batch_target_lines: batchTargetLines,
|
|
1823
|
-
batch_max_chars: batchMaxChars,
|
|
1824
|
-
batch_min_lines: batchMinLines,
|
|
1825
|
-
source_path: path.resolve(source),
|
|
1826
|
-
script_path: scriptPath,
|
|
1827
|
-
validation_path: path.join(dd, "validation.json"),
|
|
1828
|
-
episode_total: totalEpisodes,
|
|
1829
|
-
episode_completed: results.length,
|
|
1830
|
-
incomplete_episodes: [],
|
|
1831
|
-
batch_total: totalBatches,
|
|
1832
|
-
batch_completed: doneBatches,
|
|
1833
|
-
batch_reused: reusedBatchCount,
|
|
1834
|
-
batch_failed: erroredBatches,
|
|
1835
|
-
summary_threshold_chars: summaryThresholdChars,
|
|
1836
|
-
summary_skipped: skipSummary,
|
|
1837
|
-
summary_degraded: summaryDegraded,
|
|
1838
|
-
summary_degraded_episodes: summaryDegradedEpisodes,
|
|
1839
|
-
summary_degraded_reason: summaryDegradeReason || null,
|
|
1840
|
-
metadata_skipped: skipMetadata,
|
|
1841
|
-
last_error: null,
|
|
1842
|
-
});
|
|
1843
|
-
emitInitProgress(workspace, "validate", passed ? "success" : "warning");
|
|
1844
|
-
const title = passed
|
|
1845
|
-
? "INIT COMPLETE: Initial script ready"
|
|
1846
|
-
: "INIT NEEDS AGENT: Initial script written with repair issues";
|
|
1847
|
-
const stats = validation["stats"] ?? {};
|
|
1848
|
-
const summaryLine = formatSummaryStatusLine(skipSummary, summaryDegraded, summaryDegradedEpisodes);
|
|
1849
|
-
const normalizationSummary = assetNormalization?.summary ?? {
|
|
1850
|
-
asset_merges: 0,
|
|
1851
|
-
location_states_from_names: 0,
|
|
1852
|
-
location_states_from_time: 0,
|
|
1853
|
-
review_items: 0,
|
|
1854
|
-
};
|
|
1855
|
-
const report = {
|
|
1856
|
-
title,
|
|
1857
|
-
result: [
|
|
1858
|
-
`episodes: ${stats["episodes"] ?? 0}`,
|
|
1859
|
-
`scenes: ${stats["scenes"] ?? 0}`,
|
|
1860
|
-
`actions: ${stats["actions"] ?? 0}`,
|
|
1861
|
-
`validation: ${passed ? "passed" : "needs repair"}`,
|
|
1862
|
-
`provider: ${providerName}`,
|
|
1863
|
-
`episodes: ${results.length}/${totalEpisodes} complete`,
|
|
1864
|
-
`content: ${doneBatches}/${totalBatches} done`,
|
|
1865
|
-
`normalization: merges=${normalizationSummary.asset_merges}, states=${normalizationSummary.location_states_from_names + normalizationSummary.location_states_from_time}, review=${normalizationSummary.review_items}`,
|
|
1866
|
-
...(skipMetadata ? ["metadata: skipped"] : []),
|
|
1867
|
-
...(titleDegraded.length > 0 ? [`titles degraded (deterministic): ${titleDegraded.join(", ")}`] : []),
|
|
1868
|
-
...(summaryLine ? [summaryLine] : []),
|
|
1869
|
-
],
|
|
1870
|
-
artifacts: [
|
|
1871
|
-
path.join(workspace, "source.txt"),
|
|
1872
|
-
path.join(dd, "source_manifest.json"),
|
|
1873
|
-
path.join(dd, "episode_plan.json"),
|
|
1874
|
-
path.join(dd, "batch_plan.json"),
|
|
1875
|
-
batchResultsDir,
|
|
1876
|
-
episodeResultsDir,
|
|
1877
|
-
assetNormalizationPath,
|
|
1878
|
-
path.join(dd, "asset_curation.json"),
|
|
1879
|
-
...(skipMetadata ? [] : [assetMetadataPath]),
|
|
1880
|
-
path.join(dd, "synopsis.json"),
|
|
1881
|
-
scriptPath,
|
|
1882
|
-
path.join(dd, "validation.json"),
|
|
1883
|
-
path.join(dd, "run_state.json"),
|
|
1884
|
-
],
|
|
1885
|
-
issues: summarizeIssues(asList(validation["issues"])),
|
|
1886
|
-
next: providerName === "mock"
|
|
1887
|
-
? [
|
|
1888
|
-
"Run `direct review` (add --episode <n> for source↔extract); edit with --draft if needed; then validate/export.",
|
|
1889
|
-
"Do not export mock-provider results for delivery.",
|
|
1890
|
-
]
|
|
1891
|
-
: ["Run `direct review` (add --episode <n> for source↔extract); edit with --draft if needed; then validate/export."],
|
|
1892
|
-
};
|
|
1893
|
-
return [report, passed ? EXIT_OK : EXIT_NEEDS_AGENT];
|
|
2258
|
+
ctx.emitProgress("validate", passed ? "success" : "warning");
|
|
1894
2259
|
}
|
|
1895
2260
|
// ---------------------------------------------------------------------------
|
|
1896
2261
|
// command_status — the agent-facing state table, derived entirely from disk: a
|