fix: replan when persisted runtime intent is invalid

This commit is contained in:
Ma
2026-03-25 17:35:49 +08:00
parent 5910a4ded3
commit 9a448dbc5e
2 changed files with 96 additions and 1 deletions

View File

@@ -991,6 +991,88 @@ describe("PipelineRunner", () => {
}
});
it("re-plans instead of reusing a persisted invalid intent artifact in v2 mode", async () => {
const { root, runner, state, bookId } = await createRunnerFixture({
inputGovernanceMode: "v2",
});
const storyDir = join(state.bookDir(bookId), "story");
const runtimeDir = join(storyDir, "runtime");
await mkdir(runtimeDir, { recursive: true });
await Promise.all([
writeFile(join(storyDir, "current_focus.md"), "# Current Focus\n\nBring focus back to the mentor conflict.\n", "utf-8"),
writeFile(
join(storyDir, "volume_outline.md"),
[
"# Volume Outline",
"",
"### Golden First Three Chapters Rule",
"",
"**Chapter 1:**",
"Track the merchant guild trail.",
"",
].join("\n"),
"utf-8",
),
writeFile(join(storyDir, "current_state.md"), "# Current State\n\n- Lin Yue still hides the broken oath token.\n", "utf-8"),
writeFile(join(storyDir, "story_bible.md"), "# Story Bible\n\n- The jade seal cannot be destroyed.\n", "utf-8"),
writeFile(join(storyDir, "pending_hooks.md"), "# Pending Hooks\n\n- Why the mentor vanished after the trial.\n", "utf-8"),
writeFile(
join(runtimeDir, "chapter-0001.intent.md"),
[
"# Chapter Intent",
"",
"## Goal",
"**",
"",
"## Outline Node",
"**",
"",
"## Must Keep",
"- none",
"",
"## Must Avoid",
"- none",
"",
"## Style Emphasis",
"- none",
"",
"## Conflicts",
"- none",
"",
].join("\n"),
"utf-8",
),
]);
const planChapter = vi.spyOn(PlannerAgent.prototype, "planChapter");
const writeChapter = vi.spyOn(WriterAgent.prototype, "writeChapter").mockResolvedValue(
createWriterOutput({
chapterNumber: 1,
content: "Governed pipeline draft.",
wordCount: "Governed pipeline draft.".length,
}),
);
vi.spyOn(ContinuityAuditor.prototype, "auditChapter").mockResolvedValue(
createAuditResult({
passed: true,
issues: [],
summary: "clean",
}),
);
try {
await runner.writeNextChapter(bookId, 220);
expect(planChapter).toHaveBeenCalledTimes(1);
const writeInput = writeChapter.mock.calls[0]?.[0];
expect(writeInput?.chapterIntent).toContain("Track the merchant guild trail.");
expect(writeInput?.chapterIntent).not.toContain("\n**\n");
} finally {
await rm(root, { recursive: true, force: true });
}
});
it("logs explicit stage messages during writeNextChapter", async () => {
const { logger, infos } = createCaptureLogger();
const { root, runner, state, bookId } = await createRunnerFixture({

View File

@@ -2289,9 +2289,12 @@ ${matrix}`,
const intentMarkdown = await readFile(runtimePath, "utf-8");
const sections = this.parseIntentSections(intentMarkdown);
const goal = this.readIntentScalar(sections, "Goal");
if (!goal) return null;
if (!goal || this.isInvalidPersistedIntentScalar(goal)) return null;
const outlineNode = this.readIntentScalar(sections, "Outline Node");
if (outlineNode && outlineNode !== "(not found)" && this.isInvalidPersistedIntentScalar(outlineNode)) {
return null;
}
const conflicts = this.readIntentList(sections, "Conflicts")
.map((line) => {
const separator = line.indexOf(":");
@@ -2354,6 +2357,16 @@ ${matrix}`,
.map((line) => line.replace(/^-\s*/, ""));
}
private isInvalidPersistedIntentScalar(value: string): boolean {
const normalized = value.trim();
if (!normalized) return true;
if (/^[*_`~:|.-]+$/.test(normalized)) return true;
return (
/^\((describe|briefly describe|write)\b[\s\S]*\)$/i.test(normalized)
|| /^(?:|||)[\s\S]*$/u.test(normalized)
);
}
private relativeToBookDir(bookDir: string, absolutePath: string): string {
const prefix = `${bookDir}/`;
return absolutePath.startsWith(prefix) ? absolutePath.slice(prefix.length) : absolutePath;