@lingjingai/scriptctl 0.9.7 → 0.10.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/dist/cli.js +11 -23
- package/dist/cli.js.map +1 -1
- package/dist/common.d.ts +0 -3
- package/dist/common.js +0 -7
- package/dist/common.js.map +1 -1
- package/dist/domain/direct-core.d.ts +1 -1
- package/dist/domain/script-core.js +4 -4
- package/dist/domain/script-core.js.map +1 -1
- package/dist/help-text.js +35 -94
- package/dist/help-text.js.map +1 -1
- package/dist/infra/default-writing-prompt.d.ts +1 -1
- package/dist/infra/default-writing-prompt.js +1 -1
- package/dist/usecases/direct.d.ts +0 -5
- package/dist/usecases/direct.js +539 -1110
- package/dist/usecases/direct.js.map +1 -1
- package/dist/usecases/episode.js +2 -2
- package/dist/usecases/script.d.ts +1 -2
- package/dist/usecases/script.js +34 -144
- package/dist/usecases/script.js.map +1 -1
- package/package.json +2 -2
package/dist/usecases/direct.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
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, DEFAULT_CONCURRENCY, DEFAULT_MODEL, DEFAULT_PROVIDER, DIRECT_CONTRACT_VERSION, EXIT_INPUT, EXIT_NEEDS_AGENT, EXIT_OK, EXIT_RUNTIME, EXIT_USAGE,
|
|
3
|
+
import { CliError, DEFAULT_BATCH_MAX_CHARS, DEFAULT_BATCH_MIN_LINES, DEFAULT_BATCH_MODE, DEFAULT_BATCH_TARGET_LINES, DEFAULT_CONCURRENCY, DEFAULT_MODEL, DEFAULT_PROVIDER, DIRECT_CONTRACT_VERSION, EXIT_INPUT, EXIT_NEEDS_AGENT, EXIT_OK, EXIT_RUNTIME, EXIT_USAGE, SUPPORTED_EXTS, deletePath, deleteTree, directDir, exists, readJson, readText, sha256Text, writeJson, } from "../common.js";
|
|
4
4
|
import { compactBatchResult, compactEpisodeResult, buildBatchPlan, buildEpisodePlan, enrichEpisodePlanTitles, extractBatchWithRecovery, mergeEpisodeResults, normalizeEpisodeResult, normalizeInt, recoverBatchFromSource, uniqueAdd, validateBatchExtractionQuality, validateEpisodeExtractionQuality, _md_push_asset, curateScriptAssets, applyMetadataToScript, } from "../domain/direct-core.js";
|
|
5
5
|
import { validateScript } from "../domain/script-core.js";
|
|
6
6
|
import { makeProvider } from "../infra/providers.js";
|
|
@@ -66,38 +66,6 @@ function failureSignature(items) {
|
|
|
66
66
|
out.sort();
|
|
67
67
|
return out;
|
|
68
68
|
}
|
|
69
|
-
export function addInspectedTarget(workspace, target) {
|
|
70
|
-
const state = readRunState(workspace);
|
|
71
|
-
const targets = [];
|
|
72
|
-
for (const item of asList(state["inspected_targets"])) {
|
|
73
|
-
const s = strOf(item);
|
|
74
|
-
if (s)
|
|
75
|
-
targets.push(s);
|
|
76
|
-
}
|
|
77
|
-
if (!targets.includes(target))
|
|
78
|
-
targets.push(target);
|
|
79
|
-
const missing = [];
|
|
80
|
-
for (const t of REVIEW_TARGETS) {
|
|
81
|
-
if (!targets.includes(t))
|
|
82
|
-
missing.push(t);
|
|
83
|
-
}
|
|
84
|
-
const reviewStatus = missing.length === 0 ? "reviewed" : "in_progress";
|
|
85
|
-
return updateRunState(workspace, {
|
|
86
|
-
inspected_targets: targets,
|
|
87
|
-
review_status: reviewStatus,
|
|
88
|
-
review_missing: missing,
|
|
89
|
-
last_inspect_target: target,
|
|
90
|
-
});
|
|
91
|
-
}
|
|
92
|
-
export function markPatched(workspace, count) {
|
|
93
|
-
const state = readRunState(workspace);
|
|
94
|
-
const patchCount = Number(state["patch_count"] ?? 0) + count;
|
|
95
|
-
return updateRunState(workspace, {
|
|
96
|
-
patch_count: patchCount,
|
|
97
|
-
review_status: "patched",
|
|
98
|
-
review_missing: [],
|
|
99
|
-
});
|
|
100
|
-
}
|
|
101
69
|
export function markMetadataConfidenceReviewed(workspace, operations) {
|
|
102
70
|
const metadataOps = new Set(["meta.worldview.set", "asset.role.set", "asset.describe"]);
|
|
103
71
|
if (!operations.some((op) => isDict(op) && metadataOps.has(strOf(op["op"]))))
|
|
@@ -118,17 +86,6 @@ export function markMetadataConfidenceReviewed(workspace, operations) {
|
|
|
118
86
|
metadata["confidence_reviewed_at"] = checkpointTimestamp();
|
|
119
87
|
writeJson(p, metadata);
|
|
120
88
|
}
|
|
121
|
-
export function reviewBlockers(state) {
|
|
122
|
-
if (Number(state["patch_count"] ?? 0) > 0)
|
|
123
|
-
return [];
|
|
124
|
-
const inspected = new Set();
|
|
125
|
-
for (const item of asList(state["inspected_targets"])) {
|
|
126
|
-
const s = strOf(item);
|
|
127
|
-
if (s)
|
|
128
|
-
inspected.add(s);
|
|
129
|
-
}
|
|
130
|
-
return REVIEW_TARGETS.filter((t) => !inspected.has(t));
|
|
131
|
-
}
|
|
132
89
|
// ---------------------------------------------------------------------------
|
|
133
90
|
// Paths for episode/batch results
|
|
134
91
|
// ---------------------------------------------------------------------------
|
|
@@ -585,7 +542,7 @@ function writeBatchFailure(dir, batch, exc) {
|
|
|
585
542
|
function initFailedReport(workspace, opts) {
|
|
586
543
|
const payload = {
|
|
587
544
|
status: "init_failed",
|
|
588
|
-
command: "
|
|
545
|
+
command: "import init",
|
|
589
546
|
init_stage: opts.stage,
|
|
590
547
|
last_error: { title: opts.title, received: opts.received, failed_at: checkpointTimestamp() },
|
|
591
548
|
};
|
|
@@ -626,12 +583,6 @@ async function pMapWithConcurrency(items, concurrency, worker) {
|
|
|
626
583
|
// command_init
|
|
627
584
|
// ---------------------------------------------------------------------------
|
|
628
585
|
export async function commandInit(opts) {
|
|
629
|
-
const ctx = makeInitCtx(validateInitOpts(opts));
|
|
630
|
-
fs.mkdirSync(ctx.dd, { recursive: true });
|
|
631
|
-
ctx.previousState = readRunState(ctx.workspace);
|
|
632
|
-
return runInitPipeline(INIT_STEPS, ctx);
|
|
633
|
-
}
|
|
634
|
-
function validateInitOpts(opts) {
|
|
635
586
|
const sourcePathArg = strOf(opts["source_path"]);
|
|
636
587
|
const source = sourcePathArg.startsWith("~")
|
|
637
588
|
? path.join(process.env.HOME ?? "", sourcePathArg.slice(1))
|
|
@@ -721,1108 +672,586 @@ function validateInitOpts(opts) {
|
|
|
721
672
|
nextSteps: ["Use a supported source file and rerun init."],
|
|
722
673
|
});
|
|
723
674
|
}
|
|
724
|
-
|
|
725
|
-
}
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
checkpointReused: false,
|
|
740
|
-
batchCheckpointReused: false,
|
|
741
|
-
episodeResultsDir: "",
|
|
742
|
-
batchResultsDir: "",
|
|
743
|
-
results: [],
|
|
744
|
-
skipped: [],
|
|
745
|
-
skippedEpisodeBatchCount: 0,
|
|
746
|
-
skippedBatches: [],
|
|
747
|
-
batchResults: [],
|
|
748
|
-
completedBatches: 0,
|
|
749
|
-
script: {},
|
|
750
|
-
};
|
|
751
|
-
}
|
|
752
|
-
async function runInitPipeline(steps, ctx) {
|
|
753
|
-
for (const step of steps) {
|
|
754
|
-
if (step.enter) {
|
|
755
|
-
const updates = step.enter(ctx);
|
|
756
|
-
if (updates)
|
|
757
|
-
updateRunState(ctx.workspace, updates);
|
|
758
|
-
}
|
|
759
|
-
let result;
|
|
760
|
-
try {
|
|
761
|
-
result = await step.run(ctx);
|
|
762
|
-
}
|
|
763
|
-
catch (exc) {
|
|
764
|
-
if (step.onError)
|
|
765
|
-
step.onError(ctx, exc);
|
|
766
|
-
throw exc;
|
|
767
|
-
}
|
|
768
|
-
if (result.kind === "halt")
|
|
769
|
-
return [result.report, result.exitCode];
|
|
675
|
+
const dd = directDir(workspace);
|
|
676
|
+
fs.mkdirSync(dd, { recursive: true });
|
|
677
|
+
const previousStateBeforeInit = readRunState(workspace);
|
|
678
|
+
updateRunState(workspace, {
|
|
679
|
+
status: "init_running",
|
|
680
|
+
command: "import init",
|
|
681
|
+
init_stage: "source_prepare",
|
|
682
|
+
provider: providerName,
|
|
683
|
+
model,
|
|
684
|
+
concurrency,
|
|
685
|
+
source_path: path.resolve(source),
|
|
686
|
+
});
|
|
687
|
+
let info;
|
|
688
|
+
try {
|
|
689
|
+
info = await prepareSource(source, workspace);
|
|
770
690
|
}
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
}
|
|
774
|
-
function groupResultsByEpisode(batchResults) {
|
|
775
|
-
const byEpisode = new Map();
|
|
776
|
-
for (const result of batchResults) {
|
|
777
|
-
const ep = Number(result["episode"] ?? 0);
|
|
778
|
-
if (!byEpisode.has(ep))
|
|
779
|
-
byEpisode.set(ep, []);
|
|
780
|
-
byEpisode.get(ep).push(result);
|
|
781
|
-
}
|
|
782
|
-
return byEpisode;
|
|
783
|
-
}
|
|
784
|
-
// Merge one episode's batch results, validate, and persist its episode_results
|
|
785
|
-
// file + index metadata. Shared by the success path (episode_merge) and the
|
|
786
|
-
// failure path's partial merge so the two stay in lockstep.
|
|
787
|
-
function mergeEpisodeAndPersist(sourceText, episodeResultsDir, episode, batchResultsForEpisode, providerName, model) {
|
|
788
|
-
const result = mergeBatchResultsForEpisode(episode, batchResultsForEpisode);
|
|
789
|
-
validateEpisodeExtractionQuality(sourceText, episode, result);
|
|
790
|
-
writeJson(episodeResultPath(episodeResultsDir, episode), compactEpisodeResult(result));
|
|
791
|
-
updateEpisodeResultMetadata(episodeResultsDir, episode, providerName, model);
|
|
792
|
-
return result;
|
|
793
|
-
}
|
|
794
|
-
const INIT_STEPS = [
|
|
795
|
-
{
|
|
796
|
-
stage: "source_prepare",
|
|
797
|
-
enter: (ctx) => ({
|
|
798
|
-
status: "init_running",
|
|
799
|
-
command: "direct init",
|
|
800
|
-
init_stage: "source_prepare",
|
|
801
|
-
provider: ctx.providerName,
|
|
802
|
-
model: ctx.model,
|
|
803
|
-
concurrency: ctx.concurrency,
|
|
804
|
-
source_path: path.resolve(ctx.source),
|
|
805
|
-
}),
|
|
806
|
-
// Handles errors inline: distinguishes CliError vs raw error and writes a
|
|
807
|
-
// bespoke last_error.received before failing, so it omits onError.
|
|
808
|
-
run: async (ctx) => {
|
|
809
|
-
let info;
|
|
810
|
-
try {
|
|
811
|
-
info = await prepareSource(ctx.source, ctx.workspace);
|
|
812
|
-
}
|
|
813
|
-
catch (exc) {
|
|
814
|
-
if (exc instanceof CliError) {
|
|
815
|
-
updateRunState(ctx.workspace, {
|
|
816
|
-
status: "init_failed",
|
|
817
|
-
init_stage: "source_prepare",
|
|
818
|
-
last_error: { title: exc.title, received: exc.received, failed_at: checkpointTimestamp() },
|
|
819
|
-
});
|
|
820
|
-
throw exc;
|
|
821
|
-
}
|
|
822
|
-
const e = exc;
|
|
823
|
-
const receivedError = `${ctx.source}: ${e?.name ?? "Error"}${e?.message ? `: ${e.message}` : ""}`;
|
|
824
|
-
updateRunState(ctx.workspace, {
|
|
825
|
-
status: "init_failed",
|
|
826
|
-
init_stage: "source_prepare",
|
|
827
|
-
last_error: { title: "INIT BLOCKED: Source preparation failed", received: [receivedError], failed_at: checkpointTimestamp() },
|
|
828
|
-
});
|
|
829
|
-
throw new CliError("INIT BLOCKED: Source preparation failed", "Source preparation failed.", {
|
|
830
|
-
exitCode: EXIT_INPUT,
|
|
831
|
-
required: ["readable source file that can be converted to source.txt"],
|
|
832
|
-
received: [receivedError],
|
|
833
|
-
nextSteps: ["Fix or re-export the source file, then rerun init."],
|
|
834
|
-
});
|
|
835
|
-
}
|
|
836
|
-
ctx.info = info;
|
|
837
|
-
const sourceTextPath = strOf(info["sourceTextPath"]);
|
|
838
|
-
ctx.sourceText = readText(sourceTextPath);
|
|
839
|
-
ctx.manifest = makeSourceManifest(ctx.source, sourceTextPath, info);
|
|
840
|
-
return { kind: "continue" };
|
|
841
|
-
},
|
|
842
|
-
},
|
|
843
|
-
{
|
|
844
|
-
stage: "episode_plan",
|
|
845
|
-
enter: () => ({ status: "init_running", init_stage: "episode_plan" }),
|
|
846
|
-
run: (ctx) => {
|
|
847
|
-
ctx.plan = buildEpisodePlan(ctx.sourceText);
|
|
848
|
-
return { kind: "continue" };
|
|
849
|
-
},
|
|
850
|
-
onError: (ctx, exc) => {
|
|
851
|
-
const e = exc;
|
|
852
|
-
throw initFailedReport(ctx.workspace, {
|
|
853
|
-
title: "INIT FAILED: Episode planning failed",
|
|
854
|
-
stage: "episode_plan",
|
|
855
|
-
required: ["source.txt that can be split into episodes"],
|
|
856
|
-
received: [`${e?.name ?? "Error"}: ${(e?.message ?? "").slice(0, 160)}`],
|
|
857
|
-
nextSteps: ["Inspect workspace/source.txt, fix the source file, and rerun init."],
|
|
858
|
-
});
|
|
859
|
-
},
|
|
860
|
-
},
|
|
861
|
-
{
|
|
862
|
-
stage: "provider",
|
|
863
|
-
enter: () => ({ status: "init_running", init_stage: "provider" }),
|
|
864
|
-
// Handles errors inline: writes init_failed run_state only for CliError,
|
|
865
|
-
// then rethrows the original error unchanged, so it omits onError.
|
|
866
|
-
run: (ctx) => {
|
|
867
|
-
try {
|
|
868
|
-
ctx.provider = makeProvider(ctx.providerName, ctx.model);
|
|
869
|
-
}
|
|
870
|
-
catch (exc) {
|
|
871
|
-
if (exc instanceof CliError) {
|
|
872
|
-
updateRunState(ctx.workspace, {
|
|
873
|
-
status: "init_failed",
|
|
874
|
-
init_stage: "provider",
|
|
875
|
-
last_error: { title: exc.title, received: exc.received, failed_at: checkpointTimestamp() },
|
|
876
|
-
});
|
|
877
|
-
}
|
|
878
|
-
throw exc;
|
|
879
|
-
}
|
|
880
|
-
return { kind: "continue" };
|
|
881
|
-
},
|
|
882
|
-
},
|
|
883
|
-
{
|
|
884
|
-
stage: "episode_titles",
|
|
885
|
-
enter: () => ({ status: "init_running", init_stage: "episode_titles" }),
|
|
886
|
-
run: async (ctx) => {
|
|
887
|
-
ctx.plan = await enrichEpisodePlanTitles(ctx.sourceText, ctx.plan, ctx.provider);
|
|
888
|
-
return { kind: "continue" };
|
|
889
|
-
},
|
|
890
|
-
onError: (ctx, exc) => {
|
|
891
|
-
if (exc instanceof CliError) {
|
|
892
|
-
throw initFailedReport(ctx.workspace, {
|
|
893
|
-
title: exc.title,
|
|
894
|
-
stage: "episode_titles",
|
|
895
|
-
exitCode: exc.exitCode,
|
|
896
|
-
required: exc.required.length > 0 ? exc.required : ["episode titles generated from source text"],
|
|
897
|
-
received: exc.received.length > 0 ? exc.received : [String(exc.message).slice(0, 160)],
|
|
898
|
-
nextSteps: exc.nextSteps.length > 0 ? exc.nextSteps : ["Rerun init after checking source episode headers."],
|
|
899
|
-
});
|
|
900
|
-
}
|
|
901
|
-
const e = exc;
|
|
902
|
-
throw initFailedReport(ctx.workspace, {
|
|
903
|
-
title: "INIT FAILED: Episode title planning failed",
|
|
904
|
-
stage: "episode_titles",
|
|
905
|
-
required: ["episode titles generated from source text"],
|
|
906
|
-
received: [`${e?.name ?? "Error"}: ${(e?.message ?? "").slice(0, 160)}`],
|
|
907
|
-
nextSteps: ["Inspect workspace/source.txt and episode_plan.json, then rerun init."],
|
|
908
|
-
});
|
|
909
|
-
},
|
|
910
|
-
},
|
|
911
|
-
{
|
|
912
|
-
stage: "batch_plan",
|
|
913
|
-
run: (ctx) => {
|
|
914
|
-
ctx.batchPlan = buildBatchPlan(ctx.sourceText, ctx.plan, {
|
|
915
|
-
targetLines: ctx.batchTargetLines,
|
|
916
|
-
maxChars: ctx.batchMaxChars,
|
|
917
|
-
minLines: ctx.batchMinLines,
|
|
918
|
-
mode: ctx.batchMode,
|
|
919
|
-
});
|
|
920
|
-
return { kind: "continue" };
|
|
921
|
-
},
|
|
922
|
-
onError: (ctx, exc) => {
|
|
923
|
-
const e = exc;
|
|
924
|
-
throw initFailedReport(ctx.workspace, {
|
|
925
|
-
title: "INIT FAILED: Batch planning failed",
|
|
926
|
-
stage: "batch_plan",
|
|
927
|
-
required: ["episode_plan.json that can be split into natural paragraph batches"],
|
|
928
|
-
received: [`${e?.name ?? "Error"}: ${(e?.message ?? "").slice(0, 160)}`],
|
|
929
|
-
nextSteps: ["Inspect workspace/source.txt and episode_plan.json, then rerun init."],
|
|
930
|
-
});
|
|
931
|
-
},
|
|
932
|
-
},
|
|
933
|
-
{
|
|
934
|
-
stage: "checkpoint_setup",
|
|
935
|
-
run: (ctx) => {
|
|
936
|
-
const checkpoint = initCheckpoint(ctx.sourceText, ctx.plan);
|
|
937
|
-
const batchCheckpoint = initBatchCheckpoint(ctx.sourceText, ctx.batchPlan);
|
|
938
|
-
const previousState = ctx.previousState;
|
|
939
|
-
const previousCheckpoint = isDict(previousState["checkpoint"]) ? previousState["checkpoint"] : {};
|
|
940
|
-
const previousBatchCheckpoint = isDict(previousState["batch_checkpoint"]) ? previousState["batch_checkpoint"] : {};
|
|
941
|
-
const checkpointReused = checkpointSourceMatches(previousCheckpoint, checkpoint);
|
|
942
|
-
const batchCheckpointReused = checkpointReused && batchCheckpointMatches(previousBatchCheckpoint, batchCheckpoint);
|
|
943
|
-
if (!checkpointReused)
|
|
944
|
-
resetInitOutputs(ctx.dd);
|
|
945
|
-
else if (!batchCheckpointReused)
|
|
946
|
-
resetBatchOutputs(ctx.dd);
|
|
947
|
-
writeJson(path.join(ctx.dd, "source_manifest.json"), ctx.manifest);
|
|
948
|
-
writeJson(path.join(ctx.dd, "episode_plan.json"), ctx.plan);
|
|
949
|
-
writeJson(path.join(ctx.dd, "batch_plan.json"), ctx.batchPlan);
|
|
950
|
-
const episodeResultsDir = path.join(ctx.dd, "episode_results");
|
|
951
|
-
const batchResultsDir = path.join(ctx.dd, "batch_results");
|
|
952
|
-
fs.mkdirSync(episodeResultsDir, { recursive: true });
|
|
953
|
-
fs.mkdirSync(batchResultsDir, { recursive: true });
|
|
954
|
-
ctx.checkpoint = checkpoint;
|
|
955
|
-
ctx.batchCheckpoint = batchCheckpoint;
|
|
956
|
-
ctx.checkpointReused = checkpointReused;
|
|
957
|
-
ctx.batchCheckpointReused = batchCheckpointReused;
|
|
958
|
-
ctx.episodeResultsDir = episodeResultsDir;
|
|
959
|
-
ctx.batchResultsDir = batchResultsDir;
|
|
960
|
-
updateRunState(ctx.workspace, {
|
|
961
|
-
status: "init_running",
|
|
962
|
-
init_stage: "batch_extract",
|
|
963
|
-
checkpoint,
|
|
964
|
-
batch_checkpoint: batchCheckpoint,
|
|
965
|
-
checkpoint_reused: checkpointReused,
|
|
966
|
-
batch_checkpoint_reused: batchCheckpointReused,
|
|
967
|
-
batch_mode: ctx.batchMode,
|
|
968
|
-
batch_target_lines: ctx.batchTargetLines,
|
|
969
|
-
batch_max_chars: ctx.batchMaxChars,
|
|
970
|
-
batch_min_lines: ctx.batchMinLines,
|
|
971
|
-
episode_total: asList(ctx.plan["episodes"]).length,
|
|
972
|
-
batch_total: asList(ctx.batchPlan["batches"]).length,
|
|
973
|
-
});
|
|
974
|
-
return { kind: "continue" };
|
|
975
|
-
},
|
|
976
|
-
},
|
|
977
|
-
{
|
|
978
|
-
stage: "extract_episodes",
|
|
979
|
-
run: async (ctx) => {
|
|
980
|
-
const pendingBatches = [];
|
|
981
|
-
const batchesByEpisode = new Map();
|
|
982
|
-
for (const batch of asList(ctx.batchPlan["batches"])) {
|
|
983
|
-
const epNum = Number(batch["episode"]);
|
|
984
|
-
if (!batchesByEpisode.has(epNum))
|
|
985
|
-
batchesByEpisode.set(epNum, []);
|
|
986
|
-
batchesByEpisode.get(epNum).push(batch);
|
|
987
|
-
}
|
|
988
|
-
const previousProvider = strOf(ctx.previousState["provider"]).trim() || null;
|
|
989
|
-
for (const episode of asList(ctx.plan["episodes"])) {
|
|
990
|
-
const cached = ctx.checkpointReused
|
|
991
|
-
? loadCheckpointedEpisode(ctx.sourceText, ctx.episodeResultsDir, episode, ctx.providerName, ctx.model, previousProvider)
|
|
992
|
-
: null;
|
|
993
|
-
if (cached !== null) {
|
|
994
|
-
ctx.results.push(cached);
|
|
995
|
-
ctx.skipped.push(Number(episode["episode"]));
|
|
996
|
-
const cachedBatches = batchesByEpisode.get(Number(episode["episode"])) ?? [];
|
|
997
|
-
ctx.skippedEpisodeBatchCount += cachedBatches.length;
|
|
998
|
-
for (const cachedBatch of cachedBatches) {
|
|
999
|
-
if (!exists(batchResultPath(ctx.batchResultsDir, cachedBatch))) {
|
|
1000
|
-
const backfilled = recoverBatchFromSource(ctx.sourceText, cachedBatch);
|
|
1001
|
-
persistBatchResult(ctx.batchResultsDir, cachedBatch, backfilled);
|
|
1002
|
-
updateBatchResultMetadata(ctx.batchResultsDir, cachedBatch, ctx.providerName, ctx.model);
|
|
1003
|
-
}
|
|
1004
|
-
const errorPath = batchErrorPath(ctx.batchResultsDir, cachedBatch);
|
|
1005
|
-
if (exists(errorPath))
|
|
1006
|
-
deletePath(errorPath);
|
|
1007
|
-
}
|
|
1008
|
-
}
|
|
1009
|
-
else {
|
|
1010
|
-
pendingBatches.push(...(batchesByEpisode.get(Number(episode["episode"])) ?? []));
|
|
1011
|
-
}
|
|
1012
|
-
}
|
|
1013
|
-
const pending = [];
|
|
1014
|
-
for (const batch of pendingBatches) {
|
|
1015
|
-
const cachedBatch = ctx.batchCheckpointReused
|
|
1016
|
-
? loadCheckpointedBatch(ctx.sourceText, ctx.batchResultsDir, batch, ctx.providerName, ctx.model, previousProvider)
|
|
1017
|
-
: null;
|
|
1018
|
-
if (cachedBatch !== null) {
|
|
1019
|
-
cachedBatch["_batch_id"] = batchResultKey(batch);
|
|
1020
|
-
cachedBatch["_batch_part"] = Number(batch["part"]);
|
|
1021
|
-
cachedBatch["_starts_inside_scene"] = Boolean(batch["starts_inside_scene"]);
|
|
1022
|
-
ctx.batchResults.push(cachedBatch);
|
|
1023
|
-
ctx.skippedBatches.push(batchResultKey(batch));
|
|
1024
|
-
}
|
|
1025
|
-
else {
|
|
1026
|
-
pending.push(batch);
|
|
1027
|
-
}
|
|
1028
|
-
}
|
|
1029
|
-
const failures = [];
|
|
1030
|
-
const outcomes = await pMapWithConcurrency(pending, ctx.concurrency, async (batch) => {
|
|
1031
|
-
return await extractBatchWithRecovery(ctx.provider, ctx.sourceText, batch);
|
|
1032
|
-
});
|
|
1033
|
-
for (let i = 0; i < outcomes.length; i++) {
|
|
1034
|
-
const outcome = outcomes[i];
|
|
1035
|
-
const batch = pending[i];
|
|
1036
|
-
const errorPath = batchErrorPath(ctx.batchResultsDir, batch);
|
|
1037
|
-
if (outcome.ok) {
|
|
1038
|
-
const result = outcome.value;
|
|
1039
|
-
result["_batch_id"] = batchResultKey(batch);
|
|
1040
|
-
result["_batch_part"] = Number(batch["part"]);
|
|
1041
|
-
result["_starts_inside_scene"] = Boolean(batch["starts_inside_scene"]);
|
|
1042
|
-
ctx.batchResults.push(result);
|
|
1043
|
-
persistBatchResult(ctx.batchResultsDir, batch, result);
|
|
1044
|
-
updateBatchResultMetadata(ctx.batchResultsDir, batch, ctx.providerName, ctx.model);
|
|
1045
|
-
if (exists(errorPath))
|
|
1046
|
-
deletePath(errorPath);
|
|
1047
|
-
}
|
|
1048
|
-
else {
|
|
1049
|
-
failures.push(writeBatchFailure(ctx.batchResultsDir, batch, outcome.error));
|
|
1050
|
-
}
|
|
1051
|
-
}
|
|
1052
|
-
ctx.results.sort((a, b) => Number(a["episode"] ?? 0) - Number(b["episode"] ?? 0));
|
|
1053
|
-
ctx.batchResults.sort((a, b) => {
|
|
1054
|
-
const ea = Number(a["episode"] ?? 0);
|
|
1055
|
-
const eb = Number(b["episode"] ?? 0);
|
|
1056
|
-
if (ea !== eb)
|
|
1057
|
-
return ea - eb;
|
|
1058
|
-
return Number(a["_batch_part"] ?? 0) - Number(b["_batch_part"] ?? 0);
|
|
1059
|
-
});
|
|
1060
|
-
failures.sort((a, b) => {
|
|
1061
|
-
const ea = Number(a["episode"] ?? 0);
|
|
1062
|
-
const eb = Number(b["episode"] ?? 0);
|
|
1063
|
-
if (ea !== eb)
|
|
1064
|
-
return ea - eb;
|
|
1065
|
-
return Number(a["part"] ?? 0) - Number(b["part"] ?? 0);
|
|
1066
|
-
});
|
|
1067
|
-
const completedBatches = ctx.skippedEpisodeBatchCount + ctx.batchResults.length;
|
|
1068
|
-
ctx.completedBatches = completedBatches;
|
|
1069
|
-
if (failures.length > 0) {
|
|
1070
|
-
const failedEpisodes = [...new Set(failures.map((it) => Number(it["episode"])))].sort((a, b) => a - b);
|
|
1071
|
-
const failedBatches = failures.map((it) => strOf(it["batch_id"]));
|
|
1072
|
-
const currentFailureSignature = failureSignature(failedBatches);
|
|
1073
|
-
const previousFailureSignature = failureSignature(ctx.previousState["failed_batches"]);
|
|
1074
|
-
const sameFailuresRepeated = ctx.checkpointReused &&
|
|
1075
|
-
ctx.batchCheckpointReused &&
|
|
1076
|
-
currentFailureSignature.length > 0 &&
|
|
1077
|
-
currentFailureSignature.length === previousFailureSignature.length &&
|
|
1078
|
-
currentFailureSignature.every((v, idx) => v === previousFailureSignature[idx]) &&
|
|
1079
|
-
["init_incomplete", "init_stalled"].includes(strOf(ctx.previousState["status"]));
|
|
1080
|
-
const previousFailureStreak = normalizeInt(ctx.previousState["failure_streak"], 0);
|
|
1081
|
-
const failureStreak = sameFailuresRepeated ? previousFailureStreak + 1 : 1;
|
|
1082
|
-
const failureTitle = sameFailuresRepeated
|
|
1083
|
-
? "INIT STALLED: Same batches keep failing"
|
|
1084
|
-
: "INIT INCOMPLETE: Batch extraction failed";
|
|
1085
|
-
const nextSteps = sameFailuresRepeated
|
|
1086
|
-
? [
|
|
1087
|
-
"Run direct inspect --target issue to read failed batch details.",
|
|
1088
|
-
"Do not rerun the same init command again until source, batch options, provider, or failed content has changed.",
|
|
1089
|
-
]
|
|
1090
|
-
: [
|
|
1091
|
-
"Run direct inspect --target issue to review failed batches.",
|
|
1092
|
-
"Rerun the same init once if failures look transient; completed checkpoints will be reused.",
|
|
1093
|
-
];
|
|
1094
|
-
const failedEpisodeSet = new Set(failedEpisodes);
|
|
1095
|
-
const skippedSet = new Set(ctx.skipped);
|
|
1096
|
-
const batchResultsByEpisode = groupResultsByEpisode(ctx.batchResults);
|
|
1097
|
-
for (const episode of asList(ctx.plan["episodes"])) {
|
|
1098
|
-
const episodeNum = Number(episode["episode"]);
|
|
1099
|
-
if (skippedSet.has(episodeNum) || failedEpisodeSet.has(episodeNum))
|
|
1100
|
-
continue;
|
|
1101
|
-
const expectedBatches = (batchesByEpisode.get(episodeNum) ?? []).length;
|
|
1102
|
-
if (expectedBatches && (batchResultsByEpisode.get(episodeNum) ?? []).length === expectedBatches) {
|
|
1103
|
-
ctx.results.push(mergeEpisodeAndPersist(ctx.sourceText, ctx.episodeResultsDir, episode, batchResultsByEpisode.get(episodeNum) ?? [], ctx.providerName, ctx.model));
|
|
1104
|
-
}
|
|
1105
|
-
}
|
|
1106
|
-
updateRunState(ctx.workspace, {
|
|
1107
|
-
status: sameFailuresRepeated ? "init_stalled" : "init_incomplete",
|
|
1108
|
-
init_stage: "batch_extract",
|
|
1109
|
-
checkpoint: ctx.checkpoint,
|
|
1110
|
-
batch_checkpoint: ctx.batchCheckpoint,
|
|
1111
|
-
episode_total: asList(ctx.plan["episodes"]).length,
|
|
1112
|
-
episode_completed: ctx.results.length,
|
|
1113
|
-
episode_reused: ctx.skipped.length,
|
|
1114
|
-
episode_failed: failedEpisodes.length,
|
|
1115
|
-
failed_episodes: failedEpisodes,
|
|
1116
|
-
batch_total: asList(ctx.batchPlan["batches"]).length,
|
|
1117
|
-
batch_completed: completedBatches,
|
|
1118
|
-
batch_reused: ctx.skippedEpisodeBatchCount + ctx.skippedBatches.length,
|
|
1119
|
-
batch_failed: failures.length,
|
|
1120
|
-
failed_batches: failedBatches,
|
|
1121
|
-
failure_signature: currentFailureSignature,
|
|
1122
|
-
failure_streak: failureStreak,
|
|
1123
|
-
last_error: { title: failureTitle, failed_at: checkpointTimestamp() },
|
|
1124
|
-
exportable: false,
|
|
1125
|
-
});
|
|
1126
|
-
const issues = failures.slice(0, 5).map((it) => `${it["batch_id"]} episode ${it["episode"]} part ${it["part"]}: ${it["error_type"]} - ${it["message"]}`);
|
|
1127
|
-
const report = {
|
|
1128
|
-
title: failureTitle,
|
|
1129
|
-
result: [
|
|
1130
|
-
`episodes total: ${asList(ctx.plan["episodes"]).length}`,
|
|
1131
|
-
`completed: ${ctx.results.length}`,
|
|
1132
|
-
`reused: ${ctx.skipped.length}`,
|
|
1133
|
-
`failed episodes: ${failedEpisodes.length}`,
|
|
1134
|
-
`batches: ${completedBatches}/${asList(ctx.batchPlan["batches"]).length} completed, ${failures.length} failed`,
|
|
1135
|
-
`provider: ${ctx.providerName}`,
|
|
1136
|
-
],
|
|
1137
|
-
artifacts: [
|
|
1138
|
-
path.join(ctx.workspace, "source.txt"),
|
|
1139
|
-
path.join(ctx.dd, "source_manifest.json"),
|
|
1140
|
-
path.join(ctx.dd, "episode_plan.json"),
|
|
1141
|
-
path.join(ctx.dd, "batch_plan.json"),
|
|
1142
|
-
ctx.batchResultsDir,
|
|
1143
|
-
ctx.episodeResultsDir,
|
|
1144
|
-
path.join(ctx.dd, "run_state.json"),
|
|
1145
|
-
],
|
|
1146
|
-
issues,
|
|
1147
|
-
next: nextSteps,
|
|
1148
|
-
};
|
|
1149
|
-
return { kind: "halt", report, exitCode: EXIT_RUNTIME };
|
|
1150
|
-
}
|
|
1151
|
-
return { kind: "continue" };
|
|
1152
|
-
},
|
|
1153
|
-
},
|
|
1154
|
-
{
|
|
1155
|
-
stage: "episode_merge",
|
|
1156
|
-
enter: (ctx) => ({
|
|
1157
|
-
status: "init_running",
|
|
1158
|
-
init_stage: "episode_merge",
|
|
1159
|
-
checkpoint: ctx.checkpoint,
|
|
1160
|
-
batch_checkpoint: ctx.batchCheckpoint,
|
|
1161
|
-
episode_total: asList(ctx.plan["episodes"]).length,
|
|
1162
|
-
episode_completed: ctx.results.length,
|
|
1163
|
-
episode_reused: ctx.skipped.length,
|
|
1164
|
-
episode_failed: 0,
|
|
1165
|
-
failed_episodes: [],
|
|
1166
|
-
batch_total: asList(ctx.batchPlan["batches"]).length,
|
|
1167
|
-
batch_completed: ctx.completedBatches,
|
|
1168
|
-
batch_reused: ctx.skippedEpisodeBatchCount + ctx.skippedBatches.length,
|
|
1169
|
-
batch_failed: 0,
|
|
1170
|
-
failed_batches: [],
|
|
1171
|
-
failure_signature: [],
|
|
1172
|
-
failure_streak: 0,
|
|
1173
|
-
last_error: null,
|
|
1174
|
-
}),
|
|
1175
|
-
run: (ctx) => {
|
|
1176
|
-
// Error-file cleanup is best-effort cosmetics: a readdir hiccup must never
|
|
1177
|
-
// abort an otherwise-successful merge nor get misreported as a merge failure.
|
|
1178
|
-
for (const dir of [ctx.batchResultsDir, ctx.episodeResultsDir]) {
|
|
1179
|
-
if (!exists(dir))
|
|
1180
|
-
continue;
|
|
1181
|
-
let names;
|
|
1182
|
-
try {
|
|
1183
|
-
names = fs.readdirSync(dir);
|
|
1184
|
-
}
|
|
1185
|
-
catch {
|
|
1186
|
-
continue;
|
|
1187
|
-
}
|
|
1188
|
-
for (const name of names) {
|
|
1189
|
-
if (name.endsWith(".error.json")) {
|
|
1190
|
-
try {
|
|
1191
|
-
deletePath(path.join(dir, name));
|
|
1192
|
-
}
|
|
1193
|
-
catch {
|
|
1194
|
-
// ignore
|
|
1195
|
-
}
|
|
1196
|
-
}
|
|
1197
|
-
}
|
|
1198
|
-
}
|
|
1199
|
-
const batchResultsByEpisode = groupResultsByEpisode(ctx.batchResults);
|
|
1200
|
-
const skippedSet = new Set(ctx.skipped);
|
|
1201
|
-
for (const episode of asList(ctx.plan["episodes"])) {
|
|
1202
|
-
const episodeNum = Number(episode["episode"]);
|
|
1203
|
-
if (skippedSet.has(episodeNum))
|
|
1204
|
-
continue;
|
|
1205
|
-
ctx.results.push(mergeEpisodeAndPersist(ctx.sourceText, ctx.episodeResultsDir, episode, batchResultsByEpisode.get(episodeNum) ?? [], ctx.providerName, ctx.model));
|
|
1206
|
-
const errorPath = episodeErrorPath(ctx.episodeResultsDir, episode);
|
|
1207
|
-
if (exists(errorPath))
|
|
1208
|
-
deletePath(errorPath);
|
|
1209
|
-
}
|
|
1210
|
-
ctx.results.sort((a, b) => Number(a["episode"] ?? 0) - Number(b["episode"] ?? 0));
|
|
1211
|
-
return { kind: "continue" };
|
|
1212
|
-
},
|
|
1213
|
-
onError: (ctx, exc) => {
|
|
1214
|
-
const e = exc;
|
|
1215
|
-
throw initFailedReport(ctx.workspace, {
|
|
1216
|
-
title: "INIT FAILED: Episode merge failed",
|
|
1217
|
-
stage: "episode_merge",
|
|
1218
|
-
required: ["complete batch_results/*.json that can merge into episode_results/*.json"],
|
|
1219
|
-
received: [`${e?.name ?? "Error"}: ${(e?.message ?? "").slice(0, 160)}`],
|
|
1220
|
-
nextSteps: ["Rerun init; completed batch checkpoints will be reused and episode merge will retry."],
|
|
1221
|
-
updates: { checkpoint: ctx.checkpoint, batch_checkpoint: ctx.batchCheckpoint, batch_completed: ctx.completedBatches },
|
|
1222
|
-
});
|
|
1223
|
-
},
|
|
1224
|
-
},
|
|
1225
|
-
{
|
|
1226
|
-
stage: "script_merge",
|
|
1227
|
-
enter: (ctx) => ({ status: "init_running", init_stage: "script_merge", checkpoint: ctx.checkpoint, batch_checkpoint: ctx.batchCheckpoint }),
|
|
1228
|
-
run: (ctx) => {
|
|
1229
|
-
ctx.script = mergeEpisodeResults(ctx.results, strOf(ctx.info["projectName"]) || path.basename(ctx.source, path.extname(ctx.source)));
|
|
1230
|
-
return { kind: "continue" };
|
|
1231
|
-
},
|
|
1232
|
-
onError: (ctx, exc) => {
|
|
1233
|
-
const e = exc;
|
|
1234
|
-
throw initFailedReport(ctx.workspace, {
|
|
1235
|
-
title: "INIT FAILED: Merge failed",
|
|
1236
|
-
stage: "script_merge",
|
|
1237
|
-
required: ["complete episode_results/*.json"],
|
|
1238
|
-
received: [`${e?.name ?? "Error"}: ${(e?.message ?? "").slice(0, 160)}`],
|
|
1239
|
-
nextSteps: ["Rerun init; completed episode extraction checkpoints will be reused and merge will retry."],
|
|
1240
|
-
updates: { checkpoint: ctx.checkpoint, batch_checkpoint: ctx.batchCheckpoint, episode_completed: ctx.results.length },
|
|
1241
|
-
});
|
|
1242
|
-
},
|
|
1243
|
-
},
|
|
1244
|
-
{
|
|
1245
|
-
stage: "asset_curation",
|
|
1246
|
-
enter: (ctx) => ({ status: "init_running", init_stage: "asset_curation", checkpoint: ctx.checkpoint, batch_checkpoint: ctx.batchCheckpoint }),
|
|
1247
|
-
run: async (ctx) => {
|
|
1248
|
-
const rawCuration = await providerExtractAssetCurationLocal(ctx.provider, ctx.sourceText, ctx.script);
|
|
1249
|
-
const curation = curateScriptAssets(ctx.script, rawCuration);
|
|
1250
|
-
writeJson(path.join(ctx.dd, "asset_curation.json"), curation);
|
|
1251
|
-
return { kind: "continue" };
|
|
1252
|
-
},
|
|
1253
|
-
onError: (ctx, exc) => {
|
|
1254
|
-
if (exc instanceof CliError) {
|
|
1255
|
-
throw initFailedReport(ctx.workspace, {
|
|
1256
|
-
title: exc.title,
|
|
1257
|
-
stage: "asset_curation",
|
|
1258
|
-
exitCode: exc.exitCode,
|
|
1259
|
-
required: exc.required.length > 0 ? exc.required : ["asset curation JSON matching final script contract"],
|
|
1260
|
-
received: exc.received.length > 0 ? exc.received : [String(exc.message).slice(0, 160)],
|
|
1261
|
-
nextSteps: exc.nextSteps.length > 0 ? exc.nextSteps : ["Rerun init; extraction checkpoints will be reused and asset curation will retry."],
|
|
1262
|
-
updates: { checkpoint: ctx.checkpoint, batch_checkpoint: ctx.batchCheckpoint, episode_completed: ctx.results.length },
|
|
1263
|
-
});
|
|
1264
|
-
}
|
|
1265
|
-
const e = exc;
|
|
1266
|
-
throw initFailedReport(ctx.workspace, {
|
|
1267
|
-
title: "INIT FAILED: Asset curation failed",
|
|
1268
|
-
stage: "asset_curation",
|
|
1269
|
-
required: ["provider location merge decisions and deterministic asset reuse curation"],
|
|
1270
|
-
received: [`${e?.name ?? "Error"}: ${(e?.message ?? "").slice(0, 160)}`],
|
|
1271
|
-
nextSteps: ["Rerun init; extraction checkpoints will be reused and asset curation will retry."],
|
|
1272
|
-
updates: { checkpoint: ctx.checkpoint, batch_checkpoint: ctx.batchCheckpoint, episode_completed: ctx.results.length },
|
|
1273
|
-
});
|
|
1274
|
-
},
|
|
1275
|
-
},
|
|
1276
|
-
{
|
|
1277
|
-
stage: "metadata_extract",
|
|
1278
|
-
enter: (ctx) => ({ status: "init_running", init_stage: "metadata_extract", checkpoint: ctx.checkpoint, batch_checkpoint: ctx.batchCheckpoint }),
|
|
1279
|
-
run: async (ctx) => {
|
|
1280
|
-
let metadata = ctx.provider.extractMetadata ? await ctx.provider.extractMetadata(ctx.sourceText, ctx.script) : {};
|
|
1281
|
-
if (!isDict(metadata))
|
|
1282
|
-
metadata = {};
|
|
1283
|
-
writeJson(path.join(ctx.dd, "asset_metadata.json"), metadata);
|
|
1284
|
-
applyMetadataToScript(ctx.script, metadata);
|
|
1285
|
-
return { kind: "continue" };
|
|
1286
|
-
},
|
|
1287
|
-
onError: (ctx, exc) => {
|
|
1288
|
-
if (exc instanceof CliError) {
|
|
1289
|
-
throw initFailedReport(ctx.workspace, {
|
|
1290
|
-
title: exc.title,
|
|
1291
|
-
stage: "metadata_extract",
|
|
1292
|
-
exitCode: exc.exitCode,
|
|
1293
|
-
required: exc.required.length > 0 ? exc.required : ["metadata JSON matching final script contract"],
|
|
1294
|
-
received: exc.received.length > 0 ? exc.received : [String(exc.message).slice(0, 160)],
|
|
1295
|
-
nextSteps: exc.nextSteps.length > 0 ? exc.nextSteps : ["Rerun init; extraction checkpoints will be reused and metadata will retry."],
|
|
1296
|
-
updates: { checkpoint: ctx.checkpoint, batch_checkpoint: ctx.batchCheckpoint, episode_completed: ctx.results.length },
|
|
1297
|
-
});
|
|
1298
|
-
}
|
|
1299
|
-
const e = exc;
|
|
1300
|
-
throw initFailedReport(ctx.workspace, {
|
|
1301
|
-
title: "INIT FAILED: Metadata extraction failed",
|
|
1302
|
-
stage: "metadata_extract",
|
|
1303
|
-
required: ["provider metadata for worldview, role_type, and asset descriptions"],
|
|
1304
|
-
received: [`${e?.name ?? "Error"}: ${(e?.message ?? "").slice(0, 160)}`],
|
|
1305
|
-
nextSteps: ["Rerun init; extraction checkpoints will be reused and metadata will retry."],
|
|
1306
|
-
updates: { checkpoint: ctx.checkpoint, batch_checkpoint: ctx.batchCheckpoint, episode_completed: ctx.results.length },
|
|
1307
|
-
});
|
|
1308
|
-
},
|
|
1309
|
-
},
|
|
1310
|
-
{
|
|
1311
|
-
stage: "validate",
|
|
1312
|
-
// Handles errors inline: script.initial.json must be written and the
|
|
1313
|
-
// validate run_state written before validateScript runs, so a uniform
|
|
1314
|
-
// onError mapping cannot reproduce the ordering.
|
|
1315
|
-
run: (ctx) => {
|
|
1316
|
-
const { workspace, providerName, model, concurrency, source } = ctx;
|
|
1317
|
-
const scriptPath = path.join(ctx.dd, "script.initial.json");
|
|
1318
|
-
writeJson(scriptPath, ctx.script);
|
|
1319
|
-
updateRunState(workspace, { status: "init_running", init_stage: "validate", checkpoint: ctx.checkpoint, batch_checkpoint: ctx.batchCheckpoint });
|
|
1320
|
-
let validation;
|
|
1321
|
-
try {
|
|
1322
|
-
validation = validateScript(workspace, scriptPath);
|
|
1323
|
-
}
|
|
1324
|
-
catch (exc) {
|
|
1325
|
-
const e = exc;
|
|
1326
|
-
throw initFailedReport(workspace, {
|
|
1327
|
-
title: "INIT FAILED: Validation failed",
|
|
1328
|
-
stage: "validate",
|
|
1329
|
-
required: ["script.initial.json that can be validated"],
|
|
1330
|
-
received: [`${e?.name ?? "Error"}: ${(e?.message ?? "").slice(0, 160)}`],
|
|
1331
|
-
nextSteps: ["Rerun init to retry validation, or inspect script.initial.json if the failure persists."],
|
|
1332
|
-
updates: { checkpoint: ctx.checkpoint, script_path: scriptPath },
|
|
1333
|
-
});
|
|
1334
|
-
}
|
|
1335
|
-
const passed = Boolean(validation["passed"]);
|
|
1336
|
-
const status = passed ? "ready_for_agent" : "needs_agent_repair";
|
|
691
|
+
catch (exc) {
|
|
692
|
+
if (exc instanceof CliError) {
|
|
1337
693
|
updateRunState(workspace, {
|
|
1338
|
-
status,
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
checkpoint: ctx.checkpoint,
|
|
1342
|
-
batch_checkpoint: ctx.batchCheckpoint,
|
|
1343
|
-
checkpoint_reused: ctx.checkpointReused,
|
|
1344
|
-
batch_checkpoint_reused: ctx.batchCheckpointReused,
|
|
1345
|
-
provider: providerName,
|
|
1346
|
-
model,
|
|
1347
|
-
concurrency,
|
|
1348
|
-
batch_mode: ctx.batchMode,
|
|
1349
|
-
batch_target_lines: ctx.batchTargetLines,
|
|
1350
|
-
batch_max_chars: ctx.batchMaxChars,
|
|
1351
|
-
batch_min_lines: ctx.batchMinLines,
|
|
1352
|
-
source_path: path.resolve(source),
|
|
1353
|
-
script_path: scriptPath,
|
|
1354
|
-
validation_path: path.join(ctx.dd, "validation.json"),
|
|
1355
|
-
episode_total: asList(ctx.plan["episodes"]).length,
|
|
1356
|
-
episode_completed: ctx.results.length,
|
|
1357
|
-
episode_reused: ctx.skipped.length,
|
|
1358
|
-
episode_failed: 0,
|
|
1359
|
-
failed_episodes: [],
|
|
1360
|
-
batch_total: asList(ctx.batchPlan["batches"]).length,
|
|
1361
|
-
batch_completed: ctx.completedBatches,
|
|
1362
|
-
batch_reused: ctx.skippedEpisodeBatchCount + ctx.skippedBatches.length,
|
|
1363
|
-
batch_failed: 0,
|
|
1364
|
-
failed_batches: [],
|
|
1365
|
-
failure_signature: [],
|
|
1366
|
-
failure_streak: 0,
|
|
1367
|
-
last_error: null,
|
|
1368
|
-
review_status: "pending",
|
|
1369
|
-
review_missing: [...REVIEW_TARGETS],
|
|
1370
|
-
inspected_targets: [],
|
|
1371
|
-
patch_count: 0,
|
|
1372
|
-
exportable: providerName !== "mock",
|
|
694
|
+
status: "init_failed",
|
|
695
|
+
init_stage: "source_prepare",
|
|
696
|
+
last_error: { title: exc.title, received: exc.received, failed_at: checkpointTimestamp() },
|
|
1373
697
|
});
|
|
1374
|
-
|
|
1375
|
-
? "INIT COMPLETE: Initial script ready"
|
|
1376
|
-
: "INIT NEEDS AGENT: Initial script written with repair issues";
|
|
1377
|
-
const stats = validation["stats"] ?? {};
|
|
1378
|
-
const report = {
|
|
1379
|
-
title,
|
|
1380
|
-
result: [
|
|
1381
|
-
`episodes: ${stats["episodes"] ?? 0}`,
|
|
1382
|
-
`scenes: ${stats["scenes"] ?? 0}`,
|
|
1383
|
-
`actions: ${stats["actions"] ?? 0}`,
|
|
1384
|
-
`validation: ${passed ? "passed" : "needs repair"}`,
|
|
1385
|
-
`provider: ${providerName}`,
|
|
1386
|
-
`episode checkpoint reused: ${ctx.skipped.length}`,
|
|
1387
|
-
`batches: ${ctx.completedBatches}/${asList(ctx.batchPlan["batches"]).length} completed`,
|
|
1388
|
-
`batch checkpoint reused: ${ctx.skippedEpisodeBatchCount + ctx.skippedBatches.length}`,
|
|
1389
|
-
"agent_review: pending",
|
|
1390
|
-
],
|
|
1391
|
-
artifacts: [
|
|
1392
|
-
path.join(workspace, "source.txt"),
|
|
1393
|
-
path.join(ctx.dd, "source_manifest.json"),
|
|
1394
|
-
path.join(ctx.dd, "episode_plan.json"),
|
|
1395
|
-
path.join(ctx.dd, "batch_plan.json"),
|
|
1396
|
-
ctx.batchResultsDir,
|
|
1397
|
-
ctx.episodeResultsDir,
|
|
1398
|
-
path.join(ctx.dd, "asset_curation.json"),
|
|
1399
|
-
path.join(ctx.dd, "asset_metadata.json"),
|
|
1400
|
-
scriptPath,
|
|
1401
|
-
path.join(ctx.dd, "validation.json"),
|
|
1402
|
-
path.join(ctx.dd, "run_state.json"),
|
|
1403
|
-
],
|
|
1404
|
-
issues: summarizeIssues(asList(validation["issues"])),
|
|
1405
|
-
next: providerName === "mock"
|
|
1406
|
-
? [
|
|
1407
|
-
"Run inspect for issue, episode, and asset; apply patches if needed; then validate/export.",
|
|
1408
|
-
"Do not export mock-provider results for delivery.",
|
|
1409
|
-
]
|
|
1410
|
-
: ["Run inspect for issue, episode, and asset; apply patches if needed; then validate/export."],
|
|
1411
|
-
};
|
|
1412
|
-
return { kind: "halt", report, exitCode: passed ? EXIT_OK : EXIT_NEEDS_AGENT };
|
|
1413
|
-
},
|
|
1414
|
-
},
|
|
1415
|
-
];
|
|
1416
|
-
export function summarizeIssues(issues) {
|
|
1417
|
-
if (issues.length === 0)
|
|
1418
|
-
return [];
|
|
1419
|
-
const counts = {};
|
|
1420
|
-
for (const item of issues) {
|
|
1421
|
-
const sev = strOf(item["severity"]);
|
|
1422
|
-
counts[sev] = (counts[sev] ?? 0) + 1;
|
|
1423
|
-
}
|
|
1424
|
-
const parts = Object.entries(counts).sort(([a], [b]) => a.localeCompare(b)).map(([sev, c]) => `${sev}: ${c}`);
|
|
1425
|
-
const first = issues[0];
|
|
1426
|
-
return [parts.join("; "), `first: ${first["code"]} - ${first["summary"]}`];
|
|
1427
|
-
}
|
|
1428
|
-
// ---------------------------------------------------------------------------
|
|
1429
|
-
// command_validate
|
|
1430
|
-
// ---------------------------------------------------------------------------
|
|
1431
|
-
export function commandValidate(opts) {
|
|
1432
|
-
const workspace = strOf(opts["workspace_path"] || "workspace");
|
|
1433
|
-
const scriptPath = opts["script_path"] ? strOf(opts["script_path"]) : null;
|
|
1434
|
-
const validation = validateScript(workspace, scriptPath);
|
|
1435
|
-
const stats = validation["stats"] ?? {};
|
|
1436
|
-
const passed = Boolean(validation["passed"]);
|
|
1437
|
-
const report = {
|
|
1438
|
-
title: passed ? "VALIDATE PASSED: Script is ready" : "VALIDATE NEEDS AGENT: Repair issues found",
|
|
1439
|
-
result: [
|
|
1440
|
-
`episodes: ${stats["episodes"] ?? 0}`,
|
|
1441
|
-
`scenes: ${stats["scenes"] ?? 0}`,
|
|
1442
|
-
`actions: ${stats["actions"] ?? 0}`,
|
|
1443
|
-
],
|
|
1444
|
-
artifacts: [path.join(directDir(workspace), "validation.json")],
|
|
1445
|
-
issues: summarizeIssues(asList(validation["issues"])),
|
|
1446
|
-
next: [passed ? "Export script.json." : "Inspect issues and apply structured patches."],
|
|
1447
|
-
};
|
|
1448
|
-
return [report, passed ? EXIT_OK : EXIT_NEEDS_AGENT];
|
|
1449
|
-
}
|
|
1450
|
-
// ---------------------------------------------------------------------------
|
|
1451
|
-
// command_inspect (incl. review rendering)
|
|
1452
|
-
// ---------------------------------------------------------------------------
|
|
1453
|
-
function renderReviewEpisode(sourceText, episodePlan, script, episodeNum) {
|
|
1454
|
-
const epId = fmtId("ep", episodeNum);
|
|
1455
|
-
const scriptEp = asList(script["episodes"]).find((ep) => ep["episode_id"] === epId);
|
|
1456
|
-
const planEp = asList(episodePlan["episodes"]).find((ep) => Number(ep["episode"] ?? 0) === episodeNum);
|
|
1457
|
-
const lines = [];
|
|
1458
|
-
if (!planEp && !scriptEp) {
|
|
1459
|
-
lines.push(`⚠ Episode ${episodeNum} not found in episode_plan.json or script.initial.json.`);
|
|
1460
|
-
const available = [...new Set(asList(episodePlan["episodes"]).map((ep) => Number(ep["episode"] ?? 0)))].sort((a, b) => a - b);
|
|
1461
|
-
if (available.length > 0)
|
|
1462
|
-
lines.push(`Available episodes: ${available.join(", ")}`);
|
|
1463
|
-
return lines;
|
|
1464
|
-
}
|
|
1465
|
-
const title = (scriptEp?.["title"] ?? planEp?.["title"]) || "(无标题)";
|
|
1466
|
-
lines.push("=".repeat(72));
|
|
1467
|
-
lines.push(`EPISODE ${episodeNum} / ${epId} — ${title}`);
|
|
1468
|
-
lines.push("=".repeat(72));
|
|
1469
|
-
lines.push("");
|
|
1470
|
-
lines.push("--- 原文 source.txt ---");
|
|
1471
|
-
if (planEp) {
|
|
1472
|
-
const span = isDict(planEp["source_span"]) ? planEp["source_span"] : {};
|
|
1473
|
-
const start = Number(span["start"] ?? 0);
|
|
1474
|
-
const end = Number(span["end"] ?? sourceText.length);
|
|
1475
|
-
const snippet = sourceText.slice(start, end).replace(/\s+$/, "");
|
|
1476
|
-
lines.push(snippet || "(empty)");
|
|
1477
|
-
}
|
|
1478
|
-
else {
|
|
1479
|
-
lines.push("(no episode_plan entry; source span unavailable)");
|
|
1480
|
-
}
|
|
1481
|
-
lines.push("");
|
|
1482
|
-
lines.push("--- 抽取 script.initial.json ---");
|
|
1483
|
-
if (scriptEp) {
|
|
1484
|
-
const actorIdToName = new Map();
|
|
1485
|
-
for (const a of asList(script["actors"]))
|
|
1486
|
-
actorIdToName.set(strOf(a["actor_id"]), strOf(a["actor_name"]));
|
|
1487
|
-
const speakerIdToName = new Map();
|
|
1488
|
-
for (const s of asList(script["speakers"]))
|
|
1489
|
-
speakerIdToName.set(strOf(s["speaker_id"]), strOf(s["display_name"]));
|
|
1490
|
-
const locationIdToName = new Map();
|
|
1491
|
-
for (const l of asList(script["locations"]))
|
|
1492
|
-
locationIdToName.set(strOf(l["location_id"]), strOf(l["location_name"]));
|
|
1493
|
-
const propIdToName = new Map();
|
|
1494
|
-
for (const p of asList(script["props"]))
|
|
1495
|
-
propIdToName.set(strOf(p["prop_id"]), strOf(p["prop_name"]));
|
|
1496
|
-
for (const scene of asList(scriptEp["scenes"])) {
|
|
1497
|
-
const env = isDict(scene["environment"]) ? scene["environment"] : {};
|
|
1498
|
-
const space = strOf(env["space"]) || "?";
|
|
1499
|
-
const timeOfDay = strOf(env["time"]) || "?";
|
|
1500
|
-
const sceneId = strOf(scene["scene_id"]) || "?";
|
|
1501
|
-
let locName = "";
|
|
1502
|
-
for (const ref of asList(scene["locations"])) {
|
|
1503
|
-
locName = locationIdToName.get(strOf(ref["location_id"])) || locName;
|
|
1504
|
-
break;
|
|
1505
|
-
}
|
|
1506
|
-
const actorNames = [];
|
|
1507
|
-
for (const ref of asList(scene["actors"])) {
|
|
1508
|
-
const n = actorIdToName.get(strOf(ref["actor_id"]));
|
|
1509
|
-
if (n)
|
|
1510
|
-
actorNames.push(n);
|
|
1511
|
-
}
|
|
1512
|
-
const propNames = [];
|
|
1513
|
-
for (const ref of asList(scene["props"])) {
|
|
1514
|
-
const n = propIdToName.get(strOf(ref["prop_id"]));
|
|
1515
|
-
if (n)
|
|
1516
|
-
propNames.push(n);
|
|
1517
|
-
}
|
|
1518
|
-
lines.push("");
|
|
1519
|
-
lines.push(`Scene ${sceneId} [${space} ${timeOfDay}] ${locName || "(未知场景)"}`);
|
|
1520
|
-
if (actorNames.length > 0)
|
|
1521
|
-
lines.push(` Actors: ${actorNames.join(", ")}`);
|
|
1522
|
-
if (propNames.length > 0)
|
|
1523
|
-
lines.push(` Props: ${propNames.join(", ")}`);
|
|
1524
|
-
for (const action of asList(scene["actions"])) {
|
|
1525
|
-
const kind = strOf(action["type"]);
|
|
1526
|
-
const content = strOf(action["content"]).replace(/\n/g, "\n ");
|
|
1527
|
-
let tag;
|
|
1528
|
-
if (kind === "dialogue") {
|
|
1529
|
-
const speaker = actorIdToName.get(strOf(action["actor_id"])) || speakerIdToName.get(strOf(action["speaker_id"])) || strOf(action["speaker"]) || "?";
|
|
1530
|
-
const emotion = strOf(action["emotion"]);
|
|
1531
|
-
tag = `dlg|${speaker}` + (emotion ? `|${emotion}` : "");
|
|
1532
|
-
}
|
|
1533
|
-
else if (kind === "inner_thought") {
|
|
1534
|
-
const speaker = actorIdToName.get(strOf(action["actor_id"])) || speakerIdToName.get(strOf(action["speaker_id"])) || strOf(action["speaker"]) || "?";
|
|
1535
|
-
const emotion = strOf(action["emotion"]);
|
|
1536
|
-
tag = `think|${speaker}` + (emotion ? `|${emotion}` : "");
|
|
1537
|
-
}
|
|
1538
|
-
else {
|
|
1539
|
-
tag = "act";
|
|
1540
|
-
}
|
|
1541
|
-
lines.push(` [${tag}] ${content}`);
|
|
1542
|
-
}
|
|
698
|
+
throw exc;
|
|
1543
699
|
}
|
|
700
|
+
const e = exc;
|
|
701
|
+
const receivedError = `${source}: ${e?.name ?? "Error"}${e?.message ? `: ${e.message}` : ""}`;
|
|
702
|
+
updateRunState(workspace, {
|
|
703
|
+
status: "init_failed",
|
|
704
|
+
init_stage: "source_prepare",
|
|
705
|
+
last_error: { title: "INIT BLOCKED: Source preparation failed", received: [receivedError], failed_at: checkpointTimestamp() },
|
|
706
|
+
});
|
|
707
|
+
throw new CliError("INIT BLOCKED: Source preparation failed", "Source preparation failed.", {
|
|
708
|
+
exitCode: EXIT_INPUT,
|
|
709
|
+
required: ["readable source file that can be converted to source.txt"],
|
|
710
|
+
received: [receivedError],
|
|
711
|
+
nextSteps: ["Fix or re-export the source file, then rerun init."],
|
|
712
|
+
});
|
|
1544
713
|
}
|
|
1545
|
-
|
|
1546
|
-
|
|
1547
|
-
|
|
1548
|
-
|
|
1549
|
-
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
for (const scene of asList(scriptEp["scenes"])) {
|
|
1553
|
-
for (const ref of asList(scene["actors"])) {
|
|
1554
|
-
if (ref["actor_id"])
|
|
1555
|
-
actorIdsInEp.add(strOf(ref["actor_id"]));
|
|
1556
|
-
}
|
|
1557
|
-
for (const ref of asList(scene["locations"])) {
|
|
1558
|
-
if (ref["location_id"])
|
|
1559
|
-
locationIdsInEp.add(strOf(ref["location_id"]));
|
|
1560
|
-
}
|
|
1561
|
-
for (const ref of asList(scene["props"])) {
|
|
1562
|
-
if (ref["prop_id"])
|
|
1563
|
-
propIdsInEp.add(strOf(ref["prop_id"]));
|
|
1564
|
-
}
|
|
1565
|
-
for (const action of asList(scene["actions"])) {
|
|
1566
|
-
if (action["actor_id"])
|
|
1567
|
-
actorIdsInEp.add(strOf(action["actor_id"]));
|
|
1568
|
-
}
|
|
1569
|
-
}
|
|
1570
|
-
if (actorIdsInEp.size > 0 || locationIdsInEp.size > 0 || propIdsInEp.size > 0) {
|
|
1571
|
-
lines.push("");
|
|
1572
|
-
lines.push(`--- Episode ${episodeNum} 资产 ---`);
|
|
1573
|
-
}
|
|
1574
|
-
if (actorIdsInEp.size > 0) {
|
|
1575
|
-
lines.push("Actors:");
|
|
1576
|
-
for (const actor of asList(script["actors"])) {
|
|
1577
|
-
if (!actorIdsInEp.has(strOf(actor["actor_id"])))
|
|
1578
|
-
continue;
|
|
1579
|
-
const desc = strOf(actor["description"]).trim() || "(无描述)";
|
|
1580
|
-
const role = strOf(actor["role_type"]) || "?";
|
|
1581
|
-
lines.push(` - ${actor["actor_id"]} ${actor["actor_name"]} [${role}] — ${desc}`);
|
|
1582
|
-
}
|
|
1583
|
-
}
|
|
1584
|
-
if (locationIdsInEp.size > 0) {
|
|
1585
|
-
lines.push("Locations:");
|
|
1586
|
-
for (const loc of asList(script["locations"])) {
|
|
1587
|
-
if (!locationIdsInEp.has(strOf(loc["location_id"])))
|
|
1588
|
-
continue;
|
|
1589
|
-
const desc = strOf(loc["description"]).trim() || "(无描述)";
|
|
1590
|
-
lines.push(` - ${loc["location_id"]} ${loc["location_name"]} — ${desc}`);
|
|
1591
|
-
}
|
|
1592
|
-
}
|
|
1593
|
-
if (propIdsInEp.size > 0) {
|
|
1594
|
-
lines.push("Props:");
|
|
1595
|
-
for (const prop of asList(script["props"])) {
|
|
1596
|
-
if (!propIdsInEp.has(strOf(prop["prop_id"])))
|
|
1597
|
-
continue;
|
|
1598
|
-
const desc = strOf(prop["description"]).trim() || "(无描述)";
|
|
1599
|
-
lines.push(` - ${prop["prop_id"]} ${prop["prop_name"]} — ${desc}`);
|
|
1600
|
-
}
|
|
1601
|
-
}
|
|
714
|
+
const sourceTextPath = strOf(info["sourceTextPath"]);
|
|
715
|
+
const sourceText = readText(sourceTextPath);
|
|
716
|
+
const manifest = makeSourceManifest(source, sourceTextPath, info);
|
|
717
|
+
updateRunState(workspace, { status: "init_running", init_stage: "episode_plan" });
|
|
718
|
+
let plan;
|
|
719
|
+
try {
|
|
720
|
+
plan = buildEpisodePlan(sourceText);
|
|
1602
721
|
}
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
1612
|
-
`(removed ${summary["props_removed"] ?? 0}), ` +
|
|
1613
|
-
`locations ${summary["locations_before"] ?? 0}->${summary["locations_after"] ?? 0} ` +
|
|
1614
|
-
`(merged ${summary["locations_merged"] ?? 0})`,
|
|
1615
|
-
];
|
|
1616
|
-
for (const actor of asList(curation["actors"])) {
|
|
1617
|
-
if (!isDict(actor) || actor["decision"] !== "remove")
|
|
1618
|
-
continue;
|
|
1619
|
-
lines.push(`curation actor ${actor["actor_id"]}: remove ${actor["name"] || "-"} (scenes=${actor["scene_count"] ?? 0}) — ${actor["reason"] || "-"}`);
|
|
722
|
+
catch (exc) {
|
|
723
|
+
const e = exc;
|
|
724
|
+
throw initFailedReport(workspace, {
|
|
725
|
+
title: "INIT FAILED: Episode planning failed",
|
|
726
|
+
stage: "episode_plan",
|
|
727
|
+
required: ["source.txt that can be split into episodes"],
|
|
728
|
+
received: [`${e?.name ?? "Error"}: ${(e?.message ?? "").slice(0, 160)}`],
|
|
729
|
+
nextSteps: ["Inspect workspace/source.txt, fix the source file, and rerun init."],
|
|
730
|
+
});
|
|
1620
731
|
}
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
732
|
+
updateRunState(workspace, { status: "init_running", init_stage: "provider" });
|
|
733
|
+
let provider;
|
|
734
|
+
try {
|
|
735
|
+
provider = makeProvider(providerName, model);
|
|
1625
736
|
}
|
|
1626
|
-
|
|
1627
|
-
if (
|
|
1628
|
-
|
|
1629
|
-
|
|
737
|
+
catch (exc) {
|
|
738
|
+
if (exc instanceof CliError) {
|
|
739
|
+
updateRunState(workspace, {
|
|
740
|
+
status: "init_failed",
|
|
741
|
+
init_stage: "provider",
|
|
742
|
+
last_error: { title: exc.title, received: exc.received, failed_at: checkpointTimestamp() },
|
|
743
|
+
});
|
|
744
|
+
}
|
|
745
|
+
throw exc;
|
|
1630
746
|
}
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
747
|
+
updateRunState(workspace, { status: "init_running", init_stage: "episode_titles" });
|
|
748
|
+
try {
|
|
749
|
+
plan = await enrichEpisodePlanTitles(sourceText, plan, provider);
|
|
750
|
+
}
|
|
751
|
+
catch (exc) {
|
|
752
|
+
if (exc instanceof CliError) {
|
|
753
|
+
throw initFailedReport(workspace, {
|
|
754
|
+
title: exc.title,
|
|
755
|
+
stage: "episode_titles",
|
|
756
|
+
exitCode: exc.exitCode,
|
|
757
|
+
required: exc.required.length > 0 ? exc.required : ["episode titles generated from source text"],
|
|
758
|
+
received: exc.received.length > 0 ? exc.received : [String(exc.message).slice(0, 160)],
|
|
759
|
+
nextSteps: exc.nextSteps.length > 0 ? exc.nextSteps : ["Rerun init after checking source episode headers."],
|
|
760
|
+
});
|
|
761
|
+
}
|
|
762
|
+
const e = exc;
|
|
763
|
+
throw initFailedReport(workspace, {
|
|
764
|
+
title: "INIT FAILED: Episode title planning failed",
|
|
765
|
+
stage: "episode_titles",
|
|
766
|
+
required: ["episode titles generated from source text"],
|
|
767
|
+
received: [`${e?.name ?? "Error"}: ${(e?.message ?? "").slice(0, 160)}`],
|
|
768
|
+
nextSteps: ["Inspect workspace/source.txt and episode_plan.json, then rerun init."],
|
|
1646
769
|
});
|
|
1647
770
|
}
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
771
|
+
let batchPlan;
|
|
772
|
+
try {
|
|
773
|
+
batchPlan = buildBatchPlan(sourceText, plan, {
|
|
774
|
+
targetLines: batchTargetLines,
|
|
775
|
+
maxChars: batchMaxChars,
|
|
776
|
+
minLines: batchMinLines,
|
|
777
|
+
mode: batchMode,
|
|
778
|
+
});
|
|
1654
779
|
}
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
|
|
1662
|
-
|
|
780
|
+
catch (exc) {
|
|
781
|
+
const e = exc;
|
|
782
|
+
throw initFailedReport(workspace, {
|
|
783
|
+
title: "INIT FAILED: Batch planning failed",
|
|
784
|
+
stage: "batch_plan",
|
|
785
|
+
required: ["episode_plan.json that can be split into natural paragraph batches"],
|
|
786
|
+
received: [`${e?.name ?? "Error"}: ${(e?.message ?? "").slice(0, 160)}`],
|
|
787
|
+
nextSteps: ["Inspect workspace/source.txt and episode_plan.json, then rerun init."],
|
|
788
|
+
});
|
|
1663
789
|
}
|
|
1664
|
-
const
|
|
790
|
+
const checkpoint = initCheckpoint(sourceText, plan);
|
|
791
|
+
const batchCheckpoint = initBatchCheckpoint(sourceText, batchPlan);
|
|
792
|
+
const previousState = previousStateBeforeInit;
|
|
793
|
+
const previousCheckpoint = isDict(previousState["checkpoint"]) ? previousState["checkpoint"] : {};
|
|
794
|
+
const previousBatchCheckpoint = isDict(previousState["batch_checkpoint"]) ? previousState["batch_checkpoint"] : {};
|
|
795
|
+
const checkpointReused = checkpointSourceMatches(previousCheckpoint, checkpoint);
|
|
796
|
+
const batchCheckpointReused = checkpointReused && batchCheckpointMatches(previousBatchCheckpoint, batchCheckpoint);
|
|
797
|
+
if (!checkpointReused)
|
|
798
|
+
resetInitOutputs(dd);
|
|
799
|
+
else if (!batchCheckpointReused)
|
|
800
|
+
resetBatchOutputs(dd);
|
|
801
|
+
writeJson(path.join(dd, "source_manifest.json"), manifest);
|
|
802
|
+
writeJson(path.join(dd, "episode_plan.json"), plan);
|
|
803
|
+
writeJson(path.join(dd, "batch_plan.json"), batchPlan);
|
|
804
|
+
const episodeResultsDir = path.join(dd, "episode_results");
|
|
1665
805
|
const batchResultsDir = path.join(dd, "batch_results");
|
|
1666
|
-
|
|
1667
|
-
|
|
1668
|
-
|
|
1669
|
-
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
806
|
+
fs.mkdirSync(episodeResultsDir, { recursive: true });
|
|
807
|
+
fs.mkdirSync(batchResultsDir, { recursive: true });
|
|
808
|
+
updateRunState(workspace, {
|
|
809
|
+
status: "init_running",
|
|
810
|
+
init_stage: "batch_extract",
|
|
811
|
+
checkpoint,
|
|
812
|
+
batch_checkpoint: batchCheckpoint,
|
|
813
|
+
checkpoint_reused: checkpointReused,
|
|
814
|
+
batch_checkpoint_reused: batchCheckpointReused,
|
|
815
|
+
batch_mode: batchMode,
|
|
816
|
+
batch_target_lines: batchTargetLines,
|
|
817
|
+
batch_max_chars: batchMaxChars,
|
|
818
|
+
batch_min_lines: batchMinLines,
|
|
819
|
+
episode_total: asList(plan["episodes"]).length,
|
|
820
|
+
batch_total: asList(batchPlan["batches"]).length,
|
|
821
|
+
});
|
|
822
|
+
const results = [];
|
|
823
|
+
const skipped = [];
|
|
824
|
+
let skippedEpisodeBatchCount = 0;
|
|
825
|
+
const pendingBatches = [];
|
|
826
|
+
const batchesByEpisode = new Map();
|
|
827
|
+
for (const batch of asList(batchPlan["batches"])) {
|
|
828
|
+
const epNum = Number(batch["episode"]);
|
|
829
|
+
if (!batchesByEpisode.has(epNum))
|
|
830
|
+
batchesByEpisode.set(epNum, []);
|
|
831
|
+
batchesByEpisode.get(epNum).push(batch);
|
|
832
|
+
}
|
|
833
|
+
const previousProvider = strOf(previousState["provider"]).trim() || null;
|
|
834
|
+
for (const episode of asList(plan["episodes"])) {
|
|
835
|
+
const cached = checkpointReused
|
|
836
|
+
? loadCheckpointedEpisode(sourceText, episodeResultsDir, episode, providerName, model, previousProvider)
|
|
837
|
+
: null;
|
|
838
|
+
if (cached !== null) {
|
|
839
|
+
results.push(cached);
|
|
840
|
+
skipped.push(Number(episode["episode"]));
|
|
841
|
+
const cachedBatches = batchesByEpisode.get(Number(episode["episode"])) ?? [];
|
|
842
|
+
skippedEpisodeBatchCount += cachedBatches.length;
|
|
843
|
+
for (const cachedBatch of cachedBatches) {
|
|
844
|
+
if (!exists(batchResultPath(batchResultsDir, cachedBatch))) {
|
|
845
|
+
const backfilled = recoverBatchFromSource(sourceText, cachedBatch);
|
|
846
|
+
persistBatchResult(batchResultsDir, cachedBatch, backfilled);
|
|
847
|
+
updateBatchResultMetadata(batchResultsDir, cachedBatch, providerName, model);
|
|
848
|
+
}
|
|
849
|
+
const errorPath = batchErrorPath(batchResultsDir, cachedBatch);
|
|
850
|
+
if (exists(errorPath))
|
|
851
|
+
deletePath(errorPath);
|
|
1679
852
|
}
|
|
1680
853
|
}
|
|
854
|
+
else {
|
|
855
|
+
pendingBatches.push(...(batchesByEpisode.get(Number(episode["episode"])) ?? []));
|
|
856
|
+
}
|
|
1681
857
|
}
|
|
1682
|
-
const
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
lines.push(`${ep["episode_id"]}: scenes=${sceneCount}, actions=${actionCount}, batches=${batchCounts.get(rawNum) ?? 0}, failed_batches=${failed}, title=${ep["title"] || "-"}`);
|
|
1696
|
-
}
|
|
858
|
+
const batchResults = [];
|
|
859
|
+
const skippedBatches = [];
|
|
860
|
+
const pending = [];
|
|
861
|
+
for (const batch of pendingBatches) {
|
|
862
|
+
const cachedBatch = batchCheckpointReused
|
|
863
|
+
? loadCheckpointedBatch(sourceText, batchResultsDir, batch, providerName, model, previousProvider)
|
|
864
|
+
: null;
|
|
865
|
+
if (cachedBatch !== null) {
|
|
866
|
+
cachedBatch["_batch_id"] = batchResultKey(batch);
|
|
867
|
+
cachedBatch["_batch_part"] = Number(batch["part"]);
|
|
868
|
+
cachedBatch["_starts_inside_scene"] = Boolean(batch["starts_inside_scene"]);
|
|
869
|
+
batchResults.push(cachedBatch);
|
|
870
|
+
skippedBatches.push(batchResultKey(batch));
|
|
1697
871
|
}
|
|
1698
872
|
else {
|
|
1699
|
-
|
|
1700
|
-
const plan = exists(planPath) ? readJson(planPath) : { episodes: [] };
|
|
1701
|
-
for (const ep of asList(plan["episodes"])) {
|
|
1702
|
-
const epId = fmtId("ep", Number(ep["episode"] ?? 0));
|
|
1703
|
-
if (itemId && itemId !== epId && itemId !== strOf(ep["episode"]))
|
|
1704
|
-
continue;
|
|
1705
|
-
const episodeNum = Number(ep["episode"] ?? 0);
|
|
1706
|
-
const failed = (failedByEpisode.get(episodeNum) ?? []).join(",") || "-";
|
|
1707
|
-
lines.push(`${epId}: batches=${batchCounts.get(episodeNum) ?? 0}, failed_batches=${failed}, title=${ep["title"] || "-"}`);
|
|
1708
|
-
}
|
|
873
|
+
pending.push(batch);
|
|
1709
874
|
}
|
|
1710
875
|
}
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
876
|
+
const failures = [];
|
|
877
|
+
const outcomes = await pMapWithConcurrency(pending, concurrency, async (batch) => {
|
|
878
|
+
return await extractBatchWithRecovery(provider, sourceText, batch);
|
|
879
|
+
});
|
|
880
|
+
for (let i = 0; i < outcomes.length; i++) {
|
|
881
|
+
const outcome = outcomes[i];
|
|
882
|
+
const batch = pending[i];
|
|
883
|
+
const errorPath = batchErrorPath(batchResultsDir, batch);
|
|
884
|
+
if (outcome.ok) {
|
|
885
|
+
const result = outcome.value;
|
|
886
|
+
result["_batch_id"] = batchResultKey(batch);
|
|
887
|
+
result["_batch_part"] = Number(batch["part"]);
|
|
888
|
+
result["_starts_inside_scene"] = Boolean(batch["starts_inside_scene"]);
|
|
889
|
+
batchResults.push(result);
|
|
890
|
+
persistBatchResult(batchResultsDir, batch, result);
|
|
891
|
+
updateBatchResultMetadata(batchResultsDir, batch, providerName, model);
|
|
892
|
+
if (exists(errorPath))
|
|
893
|
+
deletePath(errorPath);
|
|
1717
894
|
}
|
|
1718
|
-
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
895
|
+
else {
|
|
896
|
+
failures.push(writeBatchFailure(batchResultsDir, batch, outcome.error));
|
|
897
|
+
}
|
|
898
|
+
}
|
|
899
|
+
results.sort((a, b) => Number(a["episode"] ?? 0) - Number(b["episode"] ?? 0));
|
|
900
|
+
batchResults.sort((a, b) => {
|
|
901
|
+
const ea = Number(a["episode"] ?? 0);
|
|
902
|
+
const eb = Number(b["episode"] ?? 0);
|
|
903
|
+
if (ea !== eb)
|
|
904
|
+
return ea - eb;
|
|
905
|
+
return Number(a["_batch_part"] ?? 0) - Number(b["_batch_part"] ?? 0);
|
|
906
|
+
});
|
|
907
|
+
failures.sort((a, b) => {
|
|
908
|
+
const ea = Number(a["episode"] ?? 0);
|
|
909
|
+
const eb = Number(b["episode"] ?? 0);
|
|
910
|
+
if (ea !== eb)
|
|
911
|
+
return ea - eb;
|
|
912
|
+
return Number(a["part"] ?? 0) - Number(b["part"] ?? 0);
|
|
913
|
+
});
|
|
914
|
+
const completedBatches = skippedEpisodeBatchCount + batchResults.length;
|
|
915
|
+
if (failures.length > 0) {
|
|
916
|
+
const failedEpisodes = [...new Set(failures.map((it) => Number(it["episode"])))].sort((a, b) => a - b);
|
|
917
|
+
const failedBatches = failures.map((it) => strOf(it["batch_id"]));
|
|
918
|
+
const currentFailureSignature = failureSignature(failedBatches);
|
|
919
|
+
const previousFailureSignature = failureSignature(previousState["failed_batches"]);
|
|
920
|
+
const sameFailuresRepeated = checkpointReused &&
|
|
921
|
+
batchCheckpointReused &&
|
|
922
|
+
currentFailureSignature.length > 0 &&
|
|
923
|
+
currentFailureSignature.length === previousFailureSignature.length &&
|
|
924
|
+
currentFailureSignature.every((v, idx) => v === previousFailureSignature[idx]) &&
|
|
925
|
+
["init_incomplete", "init_stalled"].includes(strOf(previousState["status"]));
|
|
926
|
+
const previousFailureStreak = normalizeInt(previousState["failure_streak"], 0);
|
|
927
|
+
const failureStreak = sameFailuresRepeated ? previousFailureStreak + 1 : 1;
|
|
928
|
+
const failureTitle = sameFailuresRepeated
|
|
929
|
+
? "INIT STALLED: Same batches keep failing"
|
|
930
|
+
: "INIT INCOMPLETE: Batch extraction failed";
|
|
931
|
+
const nextSteps = sameFailuresRepeated
|
|
932
|
+
? [
|
|
933
|
+
"Read failed batch details in draft/scriptctl/direct/batch_results/*.error.json (run_state.json lists failed_batches).",
|
|
934
|
+
"Do not rerun the same init command again until source, batch options, provider, or failed content has changed.",
|
|
935
|
+
]
|
|
936
|
+
: [
|
|
937
|
+
"Read failed batches in draft/scriptctl/direct/batch_results/*.error.json (run_state.json lists failed_batches).",
|
|
938
|
+
"Rerun the same init once if failures look transient; completed checkpoints will be reused.",
|
|
939
|
+
];
|
|
940
|
+
const failedEpisodeSet = new Set(failedEpisodes);
|
|
941
|
+
const skippedSet = new Set(skipped);
|
|
942
|
+
const batchResultsByEpisode = new Map();
|
|
943
|
+
for (const result of batchResults) {
|
|
944
|
+
const ep = Number(result["episode"] ?? 0);
|
|
945
|
+
if (!batchResultsByEpisode.has(ep))
|
|
946
|
+
batchResultsByEpisode.set(ep, []);
|
|
947
|
+
batchResultsByEpisode.get(ep).push(result);
|
|
948
|
+
}
|
|
949
|
+
for (const episode of asList(plan["episodes"])) {
|
|
950
|
+
const episodeNum = Number(episode["episode"]);
|
|
951
|
+
if (skippedSet.has(episodeNum) || failedEpisodeSet.has(episodeNum))
|
|
952
|
+
continue;
|
|
953
|
+
const expectedBatches = (batchesByEpisode.get(episodeNum) ?? []).length;
|
|
954
|
+
if (expectedBatches && (batchResultsByEpisode.get(episodeNum) ?? []).length === expectedBatches) {
|
|
955
|
+
const result = mergeBatchResultsForEpisode(episode, batchResultsByEpisode.get(episodeNum) ?? []);
|
|
956
|
+
validateEpisodeExtractionQuality(sourceText, episode, result);
|
|
957
|
+
results.push(result);
|
|
958
|
+
writeJson(episodeResultPath(episodeResultsDir, episode), compactEpisodeResult(result));
|
|
959
|
+
updateEpisodeResultMetadata(episodeResultsDir, episode, providerName, model);
|
|
1728
960
|
}
|
|
1729
961
|
}
|
|
962
|
+
updateRunState(workspace, {
|
|
963
|
+
status: sameFailuresRepeated ? "init_stalled" : "init_incomplete",
|
|
964
|
+
init_stage: "batch_extract",
|
|
965
|
+
checkpoint,
|
|
966
|
+
batch_checkpoint: batchCheckpoint,
|
|
967
|
+
episode_total: asList(plan["episodes"]).length,
|
|
968
|
+
episode_completed: results.length,
|
|
969
|
+
episode_reused: skipped.length,
|
|
970
|
+
episode_failed: failedEpisodes.length,
|
|
971
|
+
failed_episodes: failedEpisodes,
|
|
972
|
+
batch_total: asList(batchPlan["batches"]).length,
|
|
973
|
+
batch_completed: completedBatches,
|
|
974
|
+
batch_reused: skippedEpisodeBatchCount + skippedBatches.length,
|
|
975
|
+
batch_failed: failures.length,
|
|
976
|
+
failed_batches: failedBatches,
|
|
977
|
+
failure_signature: currentFailureSignature,
|
|
978
|
+
failure_streak: failureStreak,
|
|
979
|
+
last_error: { title: failureTitle, failed_at: checkpointTimestamp() },
|
|
980
|
+
exportable: false,
|
|
981
|
+
});
|
|
982
|
+
const issues = failures.slice(0, 5).map((it) => `${it["batch_id"]} episode ${it["episode"]} part ${it["part"]}: ${it["error_type"]} - ${it["message"]}`);
|
|
983
|
+
const report = {
|
|
984
|
+
title: failureTitle,
|
|
985
|
+
result: [
|
|
986
|
+
`episodes total: ${asList(plan["episodes"]).length}`,
|
|
987
|
+
`completed: ${results.length}`,
|
|
988
|
+
`reused: ${skipped.length}`,
|
|
989
|
+
`failed episodes: ${failedEpisodes.length}`,
|
|
990
|
+
`batches: ${completedBatches}/${asList(batchPlan["batches"]).length} completed, ${failures.length} failed`,
|
|
991
|
+
`provider: ${providerName}`,
|
|
992
|
+
],
|
|
993
|
+
artifacts: [
|
|
994
|
+
path.join(workspace, "source.txt"),
|
|
995
|
+
path.join(dd, "source_manifest.json"),
|
|
996
|
+
path.join(dd, "episode_plan.json"),
|
|
997
|
+
path.join(dd, "batch_plan.json"),
|
|
998
|
+
batchResultsDir,
|
|
999
|
+
episodeResultsDir,
|
|
1000
|
+
path.join(dd, "run_state.json"),
|
|
1001
|
+
],
|
|
1002
|
+
issues,
|
|
1003
|
+
next: nextSteps,
|
|
1004
|
+
};
|
|
1005
|
+
return [report, EXIT_RUNTIME];
|
|
1730
1006
|
}
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1007
|
+
updateRunState(workspace, {
|
|
1008
|
+
status: "init_running",
|
|
1009
|
+
init_stage: "episode_merge",
|
|
1010
|
+
checkpoint,
|
|
1011
|
+
batch_checkpoint: batchCheckpoint,
|
|
1012
|
+
episode_total: asList(plan["episodes"]).length,
|
|
1013
|
+
episode_completed: results.length,
|
|
1014
|
+
episode_reused: skipped.length,
|
|
1015
|
+
episode_failed: 0,
|
|
1016
|
+
failed_episodes: [],
|
|
1017
|
+
batch_total: asList(batchPlan["batches"]).length,
|
|
1018
|
+
batch_completed: completedBatches,
|
|
1019
|
+
batch_reused: skippedEpisodeBatchCount + skippedBatches.length,
|
|
1020
|
+
batch_failed: 0,
|
|
1021
|
+
failed_batches: [],
|
|
1022
|
+
failure_signature: [],
|
|
1023
|
+
failure_streak: 0,
|
|
1024
|
+
last_error: null,
|
|
1025
|
+
});
|
|
1026
|
+
for (const dir of [batchResultsDir, episodeResultsDir]) {
|
|
1027
|
+
if (!exists(dir))
|
|
1028
|
+
continue;
|
|
1029
|
+
for (const name of fs.readdirSync(dir)) {
|
|
1030
|
+
if (name.endsWith(".error.json")) {
|
|
1738
1031
|
try {
|
|
1739
|
-
|
|
1032
|
+
deletePath(path.join(dir, name));
|
|
1740
1033
|
}
|
|
1741
1034
|
catch {
|
|
1742
|
-
|
|
1035
|
+
// ignore
|
|
1743
1036
|
}
|
|
1744
|
-
if (itemId && itemId !== strOf(error["batch_id"]) && itemId !== strOf(error["episode"]) && itemId !== strOf(error["error_type"]))
|
|
1745
|
-
continue;
|
|
1746
|
-
lines.push(`error BATCH_FAILED: ${error["batch_id"]} episode ${error["episode"]} part ${error["part"]} - ${error["message"]}`);
|
|
1747
|
-
}
|
|
1748
|
-
}
|
|
1749
|
-
for (const issue of asList(validation["issues"])) {
|
|
1750
|
-
if (itemId && itemId !== strOf(issue["code"]) && itemId !== strOf(issue["severity"]))
|
|
1751
|
-
continue;
|
|
1752
|
-
const whereParts = [];
|
|
1753
|
-
for (const k of ["episode", "scene", "action_index"]) {
|
|
1754
|
-
if (issue[k] !== null && issue[k] !== undefined)
|
|
1755
|
-
whereParts.push(strOf(issue[k]));
|
|
1756
1037
|
}
|
|
1757
|
-
const where = whereParts.join(" ");
|
|
1758
|
-
lines.push(`${issue["severity"]} ${issue["code"]}: ${issue["summary"]}${where ? ` [${where}]` : ""}`);
|
|
1759
1038
|
}
|
|
1760
1039
|
}
|
|
1761
|
-
|
|
1762
|
-
const
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
|
|
1767
|
-
|
|
1768
|
-
nextSteps: ["Pass --episode 1 (single) or --episode 1,15,30 (multiple)."],
|
|
1769
|
-
});
|
|
1040
|
+
try {
|
|
1041
|
+
const batchResultsByEpisode = new Map();
|
|
1042
|
+
for (const result of batchResults) {
|
|
1043
|
+
const ep = Number(result["episode"] ?? 0);
|
|
1044
|
+
if (!batchResultsByEpisode.has(ep))
|
|
1045
|
+
batchResultsByEpisode.set(ep, []);
|
|
1046
|
+
batchResultsByEpisode.get(ep).push(result);
|
|
1770
1047
|
}
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1048
|
+
const skippedSet = new Set(skipped);
|
|
1049
|
+
for (const episode of asList(plan["episodes"])) {
|
|
1050
|
+
const episodeNum = Number(episode["episode"]);
|
|
1051
|
+
if (skippedSet.has(episodeNum))
|
|
1052
|
+
continue;
|
|
1053
|
+
const result = mergeBatchResultsForEpisode(episode, batchResultsByEpisode.get(episodeNum) ?? []);
|
|
1054
|
+
validateEpisodeExtractionQuality(sourceText, episode, result);
|
|
1055
|
+
results.push(result);
|
|
1056
|
+
writeJson(episodeResultPath(episodeResultsDir, episode), compactEpisodeResult(result));
|
|
1057
|
+
updateEpisodeResultMetadata(episodeResultsDir, episode, providerName, model);
|
|
1058
|
+
const errorPath = episodeErrorPath(episodeResultsDir, episode);
|
|
1059
|
+
if (exists(errorPath))
|
|
1060
|
+
deletePath(errorPath);
|
|
1779
1061
|
}
|
|
1780
|
-
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1062
|
+
}
|
|
1063
|
+
catch (exc) {
|
|
1064
|
+
const e = exc;
|
|
1065
|
+
throw initFailedReport(workspace, {
|
|
1066
|
+
title: "INIT FAILED: Episode merge failed",
|
|
1067
|
+
stage: "episode_merge",
|
|
1068
|
+
required: ["complete batch_results/*.json that can merge into episode_results/*.json"],
|
|
1069
|
+
received: [`${e?.name ?? "Error"}: ${(e?.message ?? "").slice(0, 160)}`],
|
|
1070
|
+
nextSteps: ["Rerun init; completed batch checkpoints will be reused and episode merge will retry."],
|
|
1071
|
+
updates: { checkpoint, batch_checkpoint: batchCheckpoint, batch_completed: completedBatches },
|
|
1072
|
+
});
|
|
1073
|
+
}
|
|
1074
|
+
results.sort((a, b) => Number(a["episode"] ?? 0) - Number(b["episode"] ?? 0));
|
|
1075
|
+
let script;
|
|
1076
|
+
try {
|
|
1077
|
+
updateRunState(workspace, { status: "init_running", init_stage: "script_merge", checkpoint, batch_checkpoint: batchCheckpoint });
|
|
1078
|
+
script = mergeEpisodeResults(results, strOf(info["projectName"]) || path.basename(source, path.extname(source)));
|
|
1079
|
+
}
|
|
1080
|
+
catch (exc) {
|
|
1081
|
+
const e = exc;
|
|
1082
|
+
throw initFailedReport(workspace, {
|
|
1083
|
+
title: "INIT FAILED: Merge failed",
|
|
1084
|
+
stage: "script_merge",
|
|
1085
|
+
required: ["complete episode_results/*.json"],
|
|
1086
|
+
received: [`${e?.name ?? "Error"}: ${(e?.message ?? "").slice(0, 160)}`],
|
|
1087
|
+
nextSteps: ["Rerun init; completed episode extraction checkpoints will be reused and merge will retry."],
|
|
1088
|
+
updates: { checkpoint, batch_checkpoint: batchCheckpoint, episode_completed: results.length },
|
|
1089
|
+
});
|
|
1090
|
+
}
|
|
1091
|
+
try {
|
|
1092
|
+
updateRunState(workspace, { status: "init_running", init_stage: "asset_curation", checkpoint, batch_checkpoint: batchCheckpoint });
|
|
1093
|
+
const rawCuration = await providerExtractAssetCurationLocal(provider, sourceText, script);
|
|
1094
|
+
const curation = curateScriptAssets(script, rawCuration);
|
|
1095
|
+
writeJson(path.join(dd, "asset_curation.json"), curation);
|
|
1096
|
+
}
|
|
1097
|
+
catch (exc) {
|
|
1098
|
+
if (exc instanceof CliError) {
|
|
1099
|
+
throw initFailedReport(workspace, {
|
|
1100
|
+
title: exc.title,
|
|
1101
|
+
stage: "asset_curation",
|
|
1102
|
+
exitCode: exc.exitCode,
|
|
1103
|
+
required: exc.required.length > 0 ? exc.required : ["asset curation JSON matching final script contract"],
|
|
1104
|
+
received: exc.received.length > 0 ? exc.received : [String(exc.message).slice(0, 160)],
|
|
1105
|
+
nextSteps: exc.nextSteps.length > 0 ? exc.nextSteps : ["Rerun init; extraction checkpoints will be reused and asset curation will retry."],
|
|
1106
|
+
updates: { checkpoint, batch_checkpoint: batchCheckpoint, episode_completed: results.length },
|
|
1786
1107
|
});
|
|
1787
1108
|
}
|
|
1788
|
-
const
|
|
1789
|
-
|
|
1790
|
-
|
|
1791
|
-
|
|
1792
|
-
|
|
1793
|
-
|
|
1794
|
-
|
|
1109
|
+
const e = exc;
|
|
1110
|
+
throw initFailedReport(workspace, {
|
|
1111
|
+
title: "INIT FAILED: Asset curation failed",
|
|
1112
|
+
stage: "asset_curation",
|
|
1113
|
+
required: ["provider location merge decisions and deterministic asset reuse curation"],
|
|
1114
|
+
received: [`${e?.name ?? "Error"}: ${(e?.message ?? "").slice(0, 160)}`],
|
|
1115
|
+
nextSteps: ["Rerun init; extraction checkpoints will be reused and asset curation will retry."],
|
|
1116
|
+
updates: { checkpoint, batch_checkpoint: batchCheckpoint, episode_completed: results.length },
|
|
1117
|
+
});
|
|
1118
|
+
}
|
|
1119
|
+
try {
|
|
1120
|
+
updateRunState(workspace, { status: "init_running", init_stage: "metadata_extract", checkpoint, batch_checkpoint: batchCheckpoint });
|
|
1121
|
+
let metadata = provider.extractMetadata ? await provider.extractMetadata(sourceText, script) : {};
|
|
1122
|
+
if (!isDict(metadata))
|
|
1123
|
+
metadata = {};
|
|
1124
|
+
writeJson(path.join(dd, "asset_metadata.json"), metadata);
|
|
1125
|
+
applyMetadataToScript(script, metadata);
|
|
1126
|
+
}
|
|
1127
|
+
catch (exc) {
|
|
1128
|
+
if (exc instanceof CliError) {
|
|
1129
|
+
throw initFailedReport(workspace, {
|
|
1130
|
+
title: exc.title,
|
|
1131
|
+
stage: "metadata_extract",
|
|
1132
|
+
exitCode: exc.exitCode,
|
|
1133
|
+
required: exc.required.length > 0 ? exc.required : ["metadata JSON matching final script contract"],
|
|
1134
|
+
received: exc.received.length > 0 ? exc.received : [String(exc.message).slice(0, 160)],
|
|
1135
|
+
nextSteps: exc.nextSteps.length > 0 ? exc.nextSteps : ["Rerun init; extraction checkpoints will be reused and metadata will retry."],
|
|
1136
|
+
updates: { checkpoint, batch_checkpoint: batchCheckpoint, episode_completed: results.length },
|
|
1795
1137
|
});
|
|
1796
1138
|
}
|
|
1797
|
-
const
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
1804
|
-
}
|
|
1805
|
-
|
|
1806
|
-
}
|
|
1139
|
+
const e = exc;
|
|
1140
|
+
throw initFailedReport(workspace, {
|
|
1141
|
+
title: "INIT FAILED: Metadata extraction failed",
|
|
1142
|
+
stage: "metadata_extract",
|
|
1143
|
+
required: ["provider metadata for worldview, role_type, and asset descriptions"],
|
|
1144
|
+
received: [`${e?.name ?? "Error"}: ${(e?.message ?? "").slice(0, 160)}`],
|
|
1145
|
+
nextSteps: ["Rerun init; extraction checkpoints will be reused and metadata will retry."],
|
|
1146
|
+
updates: { checkpoint, batch_checkpoint: batchCheckpoint, episode_completed: results.length },
|
|
1147
|
+
});
|
|
1807
1148
|
}
|
|
1808
|
-
|
|
1809
|
-
|
|
1810
|
-
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
1149
|
+
const scriptPath = path.join(dd, "script.initial.json");
|
|
1150
|
+
writeJson(scriptPath, script);
|
|
1151
|
+
updateRunState(workspace, { status: "init_running", init_stage: "validate", checkpoint, batch_checkpoint: batchCheckpoint });
|
|
1152
|
+
let validation;
|
|
1153
|
+
try {
|
|
1154
|
+
validation = validateScript(workspace, scriptPath);
|
|
1155
|
+
}
|
|
1156
|
+
catch (exc) {
|
|
1157
|
+
const e = exc;
|
|
1158
|
+
throw initFailedReport(workspace, {
|
|
1159
|
+
title: "INIT FAILED: Validation failed",
|
|
1160
|
+
stage: "validate",
|
|
1161
|
+
required: ["script.initial.json that can be validated"],
|
|
1162
|
+
received: [`${e?.name ?? "Error"}: ${(e?.message ?? "").slice(0, 160)}`],
|
|
1163
|
+
nextSteps: ["Rerun init to retry validation, or inspect script.initial.json if the failure persists."],
|
|
1164
|
+
updates: { checkpoint, script_path: scriptPath },
|
|
1814
1165
|
});
|
|
1815
1166
|
}
|
|
1816
|
-
const
|
|
1817
|
-
const
|
|
1167
|
+
const passed = Boolean(validation["passed"]);
|
|
1168
|
+
const status = passed ? "ready_for_agent" : "needs_agent_repair";
|
|
1169
|
+
updateRunState(workspace, {
|
|
1170
|
+
status,
|
|
1171
|
+
command: "import init",
|
|
1172
|
+
init_stage: "complete",
|
|
1173
|
+
checkpoint,
|
|
1174
|
+
batch_checkpoint: batchCheckpoint,
|
|
1175
|
+
checkpoint_reused: checkpointReused,
|
|
1176
|
+
batch_checkpoint_reused: batchCheckpointReused,
|
|
1177
|
+
provider: providerName,
|
|
1178
|
+
model,
|
|
1179
|
+
concurrency,
|
|
1180
|
+
batch_mode: batchMode,
|
|
1181
|
+
batch_target_lines: batchTargetLines,
|
|
1182
|
+
batch_max_chars: batchMaxChars,
|
|
1183
|
+
batch_min_lines: batchMinLines,
|
|
1184
|
+
source_path: path.resolve(source),
|
|
1185
|
+
script_path: scriptPath,
|
|
1186
|
+
validation_path: path.join(dd, "validation.json"),
|
|
1187
|
+
episode_total: asList(plan["episodes"]).length,
|
|
1188
|
+
episode_completed: results.length,
|
|
1189
|
+
episode_reused: skipped.length,
|
|
1190
|
+
episode_failed: 0,
|
|
1191
|
+
failed_episodes: [],
|
|
1192
|
+
batch_total: asList(batchPlan["batches"]).length,
|
|
1193
|
+
batch_completed: completedBatches,
|
|
1194
|
+
batch_reused: skippedEpisodeBatchCount + skippedBatches.length,
|
|
1195
|
+
batch_failed: 0,
|
|
1196
|
+
failed_batches: [],
|
|
1197
|
+
failure_signature: [],
|
|
1198
|
+
failure_streak: 0,
|
|
1199
|
+
last_error: null,
|
|
1200
|
+
// Informational signal only — no longer a push gate. The review subagent
|
|
1201
|
+
// flips this once it has self-reviewed the published DB script.
|
|
1202
|
+
review_status: "pending",
|
|
1203
|
+
exportable: providerName !== "mock",
|
|
1204
|
+
});
|
|
1205
|
+
const title = passed
|
|
1206
|
+
? "INIT COMPLETE: Initial script ready"
|
|
1207
|
+
: "INIT NEEDS AGENT: Initial script written with repair issues";
|
|
1208
|
+
const stats = validation["stats"] ?? {};
|
|
1818
1209
|
const report = {
|
|
1819
|
-
title
|
|
1820
|
-
result:
|
|
1821
|
-
|
|
1822
|
-
"
|
|
1823
|
-
|
|
1210
|
+
title,
|
|
1211
|
+
result: [
|
|
1212
|
+
`episodes: ${stats["episodes"] ?? 0}`,
|
|
1213
|
+
`scenes: ${stats["scenes"] ?? 0}`,
|
|
1214
|
+
`actions: ${stats["actions"] ?? 0}`,
|
|
1215
|
+
`validation: ${passed ? "passed" : "needs repair"}`,
|
|
1216
|
+
`provider: ${providerName}`,
|
|
1217
|
+
`episode checkpoint reused: ${skipped.length}`,
|
|
1218
|
+
`batches: ${completedBatches}/${asList(batchPlan["batches"]).length} completed`,
|
|
1219
|
+
`batch checkpoint reused: ${skippedEpisodeBatchCount + skippedBatches.length}`,
|
|
1220
|
+
"review: pending (post-push self-review)",
|
|
1824
1221
|
],
|
|
1222
|
+
artifacts: [
|
|
1223
|
+
path.join(workspace, "source.txt"),
|
|
1224
|
+
path.join(dd, "source_manifest.json"),
|
|
1225
|
+
path.join(dd, "episode_plan.json"),
|
|
1226
|
+
path.join(dd, "batch_plan.json"),
|
|
1227
|
+
batchResultsDir,
|
|
1228
|
+
episodeResultsDir,
|
|
1229
|
+
path.join(dd, "asset_curation.json"),
|
|
1230
|
+
path.join(dd, "asset_metadata.json"),
|
|
1231
|
+
scriptPath,
|
|
1232
|
+
path.join(dd, "validation.json"),
|
|
1233
|
+
path.join(dd, "run_state.json"),
|
|
1234
|
+
],
|
|
1235
|
+
issues: summarizeIssues(asList(validation["issues"])),
|
|
1236
|
+
next: providerName === "mock"
|
|
1237
|
+
? [
|
|
1238
|
+
"Do not push mock-provider results for delivery.",
|
|
1239
|
+
"Rerun init with --provider anthropic, then run scriptctl import push.",
|
|
1240
|
+
]
|
|
1241
|
+
: ["Run scriptctl import push to publish the script to the DB."],
|
|
1825
1242
|
};
|
|
1826
|
-
return [report, EXIT_OK];
|
|
1243
|
+
return [report, passed ? EXIT_OK : EXIT_NEEDS_AGENT];
|
|
1244
|
+
}
|
|
1245
|
+
export function summarizeIssues(issues) {
|
|
1246
|
+
if (issues.length === 0)
|
|
1247
|
+
return [];
|
|
1248
|
+
const counts = {};
|
|
1249
|
+
for (const item of issues) {
|
|
1250
|
+
const sev = strOf(item["severity"]);
|
|
1251
|
+
counts[sev] = (counts[sev] ?? 0) + 1;
|
|
1252
|
+
}
|
|
1253
|
+
const parts = Object.entries(counts).sort(([a], [b]) => a.localeCompare(b)).map(([sev, c]) => `${sev}: ${c}`);
|
|
1254
|
+
const first = issues[0];
|
|
1255
|
+
return [parts.join("; "), `first: ${first["code"]} - ${first["summary"]}`];
|
|
1827
1256
|
}
|
|
1828
1257
|
//# sourceMappingURL=direct.js.map
|