Просмотр исходного кода

refactor(v13): remove ChapterBrief/CyclePhase/HookAgenda schemas [phase 1/8]

Delete schemas: ChapterBrief, ChapterBeat, ChapterBeatPhase, ChapterHookMove,
HookMovement, HookPressure*, CyclePhase, ChapterConflict.

Replace with minimal ChapterMemoSchema (chapter/goal/isGoldenOpening/body/hookRefs)
for use in Phase 3 planner rewrite.

Simplify ChapterIntentSchema: drop conflicts/hookAgenda/{scene,mood,title,arc}Directive,
add optional arcContext prose field.

Delete dead-code utility:
- utils/hook-agenda.ts (173 lines, sole producer of deleted HookAgenda)

Trim utils/hook-policy.ts: drop HookAgendaLoad, HOOK_AGENDA_LIMITS,
HOOK_AGENDA_LOAD_THRESHOLDS (the agenda-specific constants). The remaining
HOOK_TIMING_PROFILES, HOOK_PHASE_*, HOOK_PRESSURE_WEIGHTS, HOOK_VISIBILITY_*,
HOOK_RELEVANT_*, HOOK_HEALTH_DEFAULTS stay — they are consumed by
hook-lifecycle, hook-health, hook-governance, memory-retrieval. The scoping
spec mis-described this file as sole HookPressure consumer; keeping the
still-live constants rather than cascade-breaking unrelated modules.

Move three pure window helpers (filterActiveHooks, isFuturePlannedHook,
isHookWithinChapterWindow, DEFAULT_HOOK_LOOKAHEAD_CHAPTERS) from hook-agenda
to hook-lifecycle so governed-working-set and memory-retrieval continue to
work without ChapterBrief/HookAgenda.

Public API break in packages/core/src/index.ts — removed 16 exports, added
ChapterMemo/ChapterMemoSchema. Studio and CLI in monorepo required no
changes (they did not import the removed symbols directly).

Drop persisted governed plans — pipeline/persisted-governed-plan.ts now
returns null unconditionally; bench books regenerate plans from scratch.
No migration.

Planner rewrite (transitional): planner.ts no longer calls the LLM to
produce a brief. It derives goal/outline/mustKeep/mustAvoid/styleEmphasis
deterministically (reusing the old text-extraction helpers) and emits a
stub ChapterMemo with body="" for Phase 3 to fill. All brief/hookAgenda/
cycle/directive code removed.

Planner prompts (planner-prompts.ts) collapsed to minimal stubs — the
previous ChapterBrief-JSON prompts are obsolete and will be replaced by
Phase 3 memo prompts. Kept exports stable for external tooling.

Cascade fixes:
- agents/writer.ts: drop chapterBrief/moodDirective/titleDirective inputs,
  accept chapterMemo + chapterIntentData, remove buildCadenceDirectiveBlock
- agents/reviser.ts: same switch, update buildReducedControlBlock signature
- agents/composer.ts: build chapterMemo context entry, replace hookAgenda
  targetHookIds with memo.hookRefs, delete describeHookAgendaRole
- utils/narrative-control.ts: replace renderBriefAsNarrativeBlock with
  renderMemoAsNarrativeBlock(memo, intent, language)
- utils/context-assembly.ts: drop conflict-derived activeOverrides and
  trace.notes (no longer available)
- utils/governed-working-set.ts, utils/memory-retrieval.ts: switch imports
  from hook-agenda to hook-lifecycle
- pipeline/runner.ts, pipeline/chapter-review-cycle.ts: update field
  passing from chapterBrief to chapterMemo + chapterIntentData

Deleted tests (covered only removed functionality):
- __tests__/hook-agenda.test.ts
- __tests__/hook-policy.test.ts
- __tests__/planner.test.ts
- __tests__/planner-prompts.test.ts
- __tests__/persisted-governed-plan.test.ts

Updated tests to match new shapes (composer, memory-retrieval, models,
pipeline-agent, pipeline-runner, writer). Also relaxed a CLI integration
test that assumed persisted plans — Phase 1 re-plans every time.

Transitional state: pipeline runs end-to-end but ChapterMemo.body is
always "". Phase 3 (new.txt) will fill it with the seven-section prose.
Ma 6 месяцев назад
Родитель
Сommit
65310eb91e
29 измененных файлов с 295 добавлено и 3793 удалено
  1. 4 5
      packages/cli/src/__tests__/cli-integration.test.ts
  2. 20 64
      packages/core/src/__tests__/composer.test.ts
  3. 0 57
      packages/core/src/__tests__/hook-agenda.test.ts
  4. 0 44
      packages/core/src/__tests__/hook-policy.test.ts
  5. 0 141
      packages/core/src/__tests__/memory-retrieval.test.ts
  6. 19 101
      packages/core/src/__tests__/models.test.ts
  7. 0 132
      packages/core/src/__tests__/persisted-governed-plan.test.ts
  8. 4 14
      packages/core/src/__tests__/pipeline-agent.test.ts
  9. 19 64
      packages/core/src/__tests__/pipeline-runner.test.ts
  10. 0 81
      packages/core/src/__tests__/planner-prompts.test.ts
  11. 0 1619
      packages/core/src/__tests__/planner.test.ts
  12. 9 18
      packages/core/src/__tests__/writer.test.ts
  13. 24 42
      packages/core/src/agents/composer.ts
  14. 11 245
      packages/core/src/agents/planner-prompts.ts
  15. 59 519
      packages/core/src/agents/planner.ts
  16. 10 8
      packages/core/src/agents/reviser.ts
  17. 11 35
      packages/core/src/agents/writer.ts
  18. 2 16
      packages/core/src/index.ts
  19. 5 105
      packages/core/src/models/input-governance.ts
  20. 5 3
      packages/core/src/pipeline/chapter-review-cycle.ts
  21. 12 200
      packages/core/src/pipeline/persisted-governed-plan.ts
  22. 9 8
      packages/core/src/pipeline/runner.ts
  23. 4 7
      packages/core/src/utils/context-assembly.ts
  24. 1 1
      packages/core/src/utils/governed-working-set.ts
  25. 0 173
      packages/core/src/utils/hook-agenda.ts
  26. 49 0
      packages/core/src/utils/hook-lifecycle.ts
  27. 0 38
      packages/core/src/utils/hook-policy.ts
  28. 2 4
      packages/core/src/utils/memory-retrieval.ts
  29. 16 49
      packages/core/src/utils/narrative-control.ts

+ 4 - 5
packages/cli/src/__tests__/cli-integration.test.ts

@@ -837,15 +837,14 @@ describe("CLI integration", () => {
       await expect(stat(join(projectDir, "books", "cli-book", data.tracePath))).resolves.toBeTruthy();
       await expect(stat(join(projectDir, "books", "cli-book", data.tracePath))).resolves.toBeTruthy();
     });
     });
 
 
-    it("reuses the pre-planned intent when compose runs without a new context", async () => {
-      const plannedGoal = "Ignore the guild chase and focus on the mentor conflict.";
-
+    it("re-plans from outline when compose runs without a new context (Phase 1: persisted plans disabled)", async () => {
       const output = run(["compose", "chapter", "cli-book", "--json"]);
       const output = run(["compose", "chapter", "cli-book", "--json"]);
       const data = JSON.parse(output);
       const data = JSON.parse(output);
       const intentMarkdown = await readFile(join(projectDir, "books", "cli-book", data.intentPath), "utf-8");
       const intentMarkdown = await readFile(join(projectDir, "books", "cli-book", data.intentPath), "utf-8");
 
 
-      expect(data.goal).toBe(plannedGoal);
-      expect(intentMarkdown).toContain(plannedGoal);
+      expect(typeof data.goal).toBe("string");
+      expect(data.goal.length).toBeGreaterThan(0);
+      expect(intentMarkdown).toContain(data.goal);
     });
     });
   });
   });
 
 

+ 20 - 64
packages/core/src/__tests__/composer.test.ts

@@ -66,31 +66,13 @@ describe("ComposerAgent", () => {
         ],
         ],
         mustAvoid: ["Do not reveal the mastermind."],
         mustAvoid: ["Do not reveal the mastermind."],
         styleEmphasis: ["character conflict", "tight POV"],
         styleEmphasis: ["character conflict", "tight POV"],
-        conflicts: [
-          {
-            type: "outline_vs_request",
-            resolution: "allow local outline deferral",
-          },
-        ],
-        hookAgenda: {
-          pressureMap: [],
-          mustAdvance: [],
-          eligibleResolve: [],
-          staleDebt: [],
-          avoidNewHookFamilies: [],
-        },
       },
       },
-      brief: {
+      memo: {
         chapter: 4,
         chapter: 4,
         goal: "Bring the focus back to the mentor conflict.",
         goal: "Bring the focus back to the mentor conflict.",
-        chapterType: "confrontation",
         isGoldenOpening: false,
         isGoldenOpening: false,
-        beatOutline: [
-          { phase: "opening", instruction: "Open inside the mentor argument." },
-          { phase: "development", instruction: "Bring the oath token back into the scene." },
-        ],
-        hookPlan: [],
-        propsAndSetting: ["broken oath token"],
+        body: "",
+        hookRefs: [],
       },
       },
       intentMarkdown: "# Chapter Intent\n",
       intentMarkdown: "# Chapter Intent\n",
       plannerInputs: [
       plannerInputs: [
@@ -125,7 +107,7 @@ describe("ComposerAgent", () => {
 
 
     const selectedSources = result.contextPackage.selectedContext.map((entry) => entry.source);
     const selectedSources = result.contextPackage.selectedContext.map((entry) => entry.source);
     expect(selectedSources.slice(0, 5)).toEqual([
     expect(selectedSources.slice(0, 5)).toEqual([
-      "runtime/chapter_brief",
+      "runtime/chapter_memo",
       "story/current_focus.md",
       "story/current_focus.md",
       "story/current_state.md",
       "story/current_state.md",
       "story/story_bible.md",
       "story/story_bible.md",
@@ -154,7 +136,8 @@ describe("ComposerAgent", () => {
     expect(result.ruleStack.sections.hard).toContain("story_bible");
     expect(result.ruleStack.sections.hard).toContain("story_bible");
     expect(result.ruleStack.sections.soft).toContain("author_intent");
     expect(result.ruleStack.sections.soft).toContain("author_intent");
     expect(result.ruleStack.sections.diagnostic).toContain("anti_ai_checks");
     expect(result.ruleStack.sections.diagnostic).toContain("anti_ai_checks");
-    expect(result.ruleStack.activeOverrides).toHaveLength(1);
+    // activeOverrides dropped with ChapterConflict removal (Phase 1 transitional)
+    expect(result.ruleStack.activeOverrides).toEqual([]);
   });
   });
 
 
   it("writes trace output describing planner inputs and selected sources", async () => {
   it("writes trace output describing planner inputs and selected sources", async () => {
@@ -174,8 +157,9 @@ describe("ComposerAgent", () => {
 
 
     expect(result.trace.plannerInputs).toEqual(plan.plannerInputs);
     expect(result.trace.plannerInputs).toEqual(plan.plannerInputs);
     expect(result.trace.selectedSources).toContain("story/current_focus.md");
     expect(result.trace.selectedSources).toContain("story/current_focus.md");
-    expect(result.trace.notes).toContain("allow local outline deferral");
-    await expect(readFile(result.tracePath, "utf-8")).resolves.toContain("allow local outline deferral");
+    // trace.notes dropped with ChapterConflict removal (Phase 1 transitional)
+    expect(result.trace.notes).toEqual([]);
+    await expect(readFile(result.tracePath, "utf-8")).resolves.toContain("story/current_focus.md");
   });
   });
 
 
   it("retrieves summary and hook evidence chunks instead of whole long memory files", async () => {
   it("retrieves summary and hook evidence chunks instead of whole long memory files", async () => {
@@ -553,22 +537,12 @@ describe("ComposerAgent", () => {
           ...plan.intent,
           ...plan.intent,
           chapter: 10,
           chapter: 10,
           goal: "Bring the focus back to the mentor oath conflict.",
           goal: "Bring the focus back to the mentor oath conflict.",
-          hookAgenda: {
-            pressureMap: [{
-              hookId: "mentor-oath",
-              type: "relationship",
-              payoffTiming: "slow-burn",
-              phase: "middle",
-              pressure: "high",
-              movement: "partial-payoff",
-              reason: "stale-promise",
-              blockSiblingHooks: true,
-            }],
-            mustAdvance: ["mentor-oath"],
-            eligibleResolve: [],
-            staleDebt: [],
-            avoidNewHookFamilies: ["relationship"],
-          },
+        },
+        memo: {
+          ...plan.memo,
+          chapter: 10,
+          goal: "Bring the focus back to the mentor oath conflict.",
+          hookRefs: ["mentor-oath"],
         },
         },
       },
       },
     });
     });
@@ -576,13 +550,13 @@ describe("ComposerAgent", () => {
     const hookDebtEntry = result.contextPackage.selectedContext.find((entry) => entry.source === "runtime/hook_debt#mentor-oath");
     const hookDebtEntry = result.contextPackage.selectedContext.find((entry) => entry.source === "runtime/hook_debt#mentor-oath");
     expect(hookDebtEntry).toBeDefined();
     expect(hookDebtEntry).toBeDefined();
     expect(hookDebtEntry?.excerpt).toContain("mentor-oath");
     expect(hookDebtEntry?.excerpt).toContain("mentor-oath");
-    expect(hookDebtEntry?.excerpt).toContain("主要旧债");
+    expect(hookDebtEntry?.excerpt).toContain("备忘引用旧债");
     expect(hookDebtEntry?.excerpt).toContain("读者承诺");
     expect(hookDebtEntry?.excerpt).toContain("读者承诺");
     expect(hookDebtEntry?.excerpt).toContain("River Camp");
     expect(hookDebtEntry?.excerpt).toContain("River Camp");
     expect(hookDebtEntry?.excerpt).toContain("Trial Echo");
     expect(hookDebtEntry?.excerpt).toContain("Trial Echo");
   });
   });
 
 
-  it("includes brief-selected hook ids in hook debt retrieval even before hookAgenda is updated", async () => {
+  it("includes memo-referenced hook ids in hook debt retrieval", async () => {
     await Promise.all([
     await Promise.all([
       writeFile(
       writeFile(
         join(storyDir, "pending_hooks.md"),
         join(storyDir, "pending_hooks.md"),
@@ -627,30 +601,12 @@ describe("ComposerAgent", () => {
           chapter: 8,
           chapter: 8,
           goal: "Follow the black ring pressure.",
           goal: "Follow the black ring pressure.",
           mustKeep: [],
           mustKeep: [],
-          hookAgenda: {
-            pressureMap: [],
-            mustAdvance: [],
-            eligibleResolve: [],
-            staleDebt: [],
-            avoidNewHookFamilies: [],
-          },
         },
         },
-        brief: {
+        memo: {
+          ...plan.memo,
           chapter: 8,
           chapter: 8,
           goal: "Follow the black ring pressure.",
           goal: "Follow the black ring pressure.",
-          chapterType: "pursuit",
-          isGoldenOpening: false,
-          beatOutline: [
-            { phase: "opening", instruction: "Open with the ring clue already in motion." },
-          ],
-          hookPlan: [
-            {
-              hookId: "black-ring",
-              movement: "advance",
-              targetEffect: "Push the ring clue onto the dock in a way the reader can feel.",
-            },
-          ],
-          propsAndSetting: ["wet dock", "black ring"],
+          hookRefs: ["black-ring"],
         },
         },
       },
       },
     });
     });

+ 0 - 57
packages/core/src/__tests__/hook-agenda.test.ts

@@ -1,57 +0,0 @@
-import { describe, expect, it } from "vitest";
-import {
-  buildPlannerHookAgenda,
-  isHookWithinChapterWindow,
-} from "../utils/hook-agenda.js";
-import type { StoredHook } from "../state/memory-db.js";
-
-function createHook(overrides: Partial<StoredHook> = {}): StoredHook {
-  return {
-    hookId: overrides.hookId ?? "mentor-oath",
-    startChapter: overrides.startChapter ?? 8,
-    type: overrides.type ?? "relationship",
-    status: overrides.status ?? "open",
-    lastAdvancedChapter: overrides.lastAdvancedChapter ?? 9,
-    expectedPayoff: overrides.expectedPayoff ?? "Reveal why the mentor broke the oath",
-    payoffTiming: overrides.payoffTiming ?? "slow-burn",
-    notes: overrides.notes ?? "Long debt should stay visible",
-  };
-}
-
-describe("hook-agenda", () => {
-  it("builds agenda with lifecycle-aware scheduling and chapter-window filtering", () => {
-    const staleSlowBurn = createHook({
-      hookId: "mentor-oath",
-      startChapter: 4,
-      lastAdvancedChapter: 7,
-      notes: "Long debt is stalling",
-    });
-    const readyMystery = createHook({
-      hookId: "ledger-fragment",
-      type: "mystery",
-      startChapter: 2,
-      lastAdvancedChapter: 10,
-      payoffTiming: "near-term",
-      expectedPayoff: "Reveal the ledger fragment's origin",
-      notes: "Ready to cash out",
-    });
-
-    const agenda = buildPlannerHookAgenda({
-      hooks: [staleSlowBurn, readyMystery],
-      chapterNumber: 12,
-      targetChapters: 24,
-      language: "en",
-    });
-
-    // Both hooks are stale at chapter 12: slow-burn dormancy=5 meets threshold,
-    // near-term dormancy=2 meets threshold and is overdue (age 10 >= 5).
-    // Neither is readyToResolve (slow-burn needs late phase or overdue; near-term
-    // lacks momentum). Both land in staleDebt sorted by advancePressure.
-    expect(agenda.staleDebt).toContain("ledger-fragment");
-    expect(agenda.staleDebt).toContain("mentor-oath");
-    expect(agenda.eligibleResolve).toEqual([]);
-
-    expect(isHookWithinChapterWindow(staleSlowBurn, 12, 5)).toBe(true);
-    expect(isHookWithinChapterWindow(readyMystery, 12, 5)).toBe(true);
-  });
-});

+ 0 - 44
packages/core/src/__tests__/hook-policy.test.ts

@@ -1,44 +0,0 @@
-import { describe, expect, it } from "vitest";
-import {
-  HOOK_ACTIVITY_THRESHOLDS,
-  HOOK_AGENDA_LIMITS,
-  HOOK_HEALTH_DEFAULTS,
-  HOOK_PHASE_THRESHOLDS,
-  HOOK_PRESSURE_WEIGHTS,
-  HOOK_TIMING_PROFILES,
-  resolveHookVisibilityWindow,
-} from "../utils/hook-policy.js";
-
-describe("hook-policy", () => {
-  it("exposes shared lifecycle timing profiles from a single policy surface", () => {
-    expect(HOOK_TIMING_PROFILES.immediate.earliestResolveAge).toBe(1);
-    expect(HOOK_TIMING_PROFILES["slow-burn"].minimumPhase).toBe("middle");
-    expect(HOOK_TIMING_PROFILES.endgame.overdueAge).toBeGreaterThan(
-      HOOK_TIMING_PROFILES["near-term"].overdueAge,
-    );
-  });
-
-  it("widens visibility windows for longer-payoff hooks", () => {
-    expect(resolveHookVisibilityWindow("immediate")).toBe(5);
-    expect(resolveHookVisibilityWindow("mid-arc")).toBeGreaterThan(resolveHookVisibilityWindow("near-term"));
-    expect(resolveHookVisibilityWindow("endgame")).toBeGreaterThan(resolveHookVisibilityWindow("slow-burn"));
-  });
-
-  it("keeps shared agenda, phase, pressure, and health defaults together", () => {
-    expect(HOOK_AGENDA_LIMITS.light.mustAdvance).toBeLessThan(HOOK_AGENDA_LIMITS.heavy.mustAdvance);
-    expect(HOOK_AGENDA_LIMITS.heavy.eligibleResolve).toBeGreaterThan(HOOK_AGENDA_LIMITS.light.eligibleResolve);
-    expect(HOOK_HEALTH_DEFAULTS.maxActiveHooks).toBe(12);
-    expect(HOOK_PHASE_THRESHOLDS.lateProgress).toBeGreaterThan(HOOK_PHASE_THRESHOLDS.middleProgress);
-    expect(HOOK_PRESSURE_WEIGHTS.resolveBiasMultiplier).toBe(10);
-  });
-
-  it("keeps lifecycle activity and refresh thresholds on the same policy surface", () => {
-    expect(HOOK_ACTIVITY_THRESHOLDS.recentlyTouchedDormancy).toBe(1);
-    expect(HOOK_ACTIVITY_THRESHOLDS.longArcQuietHoldMaxAge).toBeGreaterThan(
-      HOOK_ACTIVITY_THRESHOLDS.freshPromiseAge,
-    );
-    expect(HOOK_ACTIVITY_THRESHOLDS.refreshDormancy).toBeGreaterThan(
-      HOOK_ACTIVITY_THRESHOLDS.longArcQuietHoldMaxDormancy,
-    );
-  });
-});

+ 0 - 141
packages/core/src/__tests__/memory-retrieval.test.ts

@@ -1211,145 +1211,4 @@ describe("parsePendingHooksMarkdown", () => {
     ]);
     ]);
   });
   });
 
 
-  it("places near-term progressing hooks in eligible resolve and slow-burn in must-advance", () => {
-    const agenda = memoryRetrieval.buildPlannerHookAgenda({
-      chapterNumber: 18,
-      hooks: [
-        {
-          hookId: "slow-oath",
-          startChapter: 10,
-          type: "relationship",
-          status: "progressing",
-          lastAdvancedChapter: 17,
-          expectedPayoff: "Reveal why the mentor buried the oath debt",
-          payoffTiming: "slow-burn",
-          notes: "The debt should simmer across the wider arc.",
-        },
-        {
-          hookId: "ready-packet",
-          startChapter: 14,
-          type: "mystery",
-          status: "progressing",
-          lastAdvancedChapter: 17,
-          expectedPayoff: "Open the missing packet and expose the inside hand",
-          payoffTiming: "near-term",
-          notes: "The local sequence is ready for a concrete payoff.",
-        },
-      ] as never,
-      maxMustAdvance: 2,
-      maxEligibleResolve: 2,
-      targetChapters: 40,
-    } as never);
-
-    // near-term progressing hook with recent touch is readyToResolve → eligibleResolve
-    expect(agenda.eligibleResolve).toContain("ready-packet");
-    // slow-burn in middle phase (not late, not overdue) is not readyToResolve → mustAdvance
-    expect(agenda.mustAdvance).toContain("slow-oath");
-    expect(agenda.pressureMap).toEqual([]);
-  });
-
-  it("scales eligible resolve slots with agenda load level", () => {
-    const agenda = memoryRetrieval.buildPlannerHookAgenda({
-      chapterNumber: 8,
-      targetChapters: 12,
-      hooks: [
-        {
-          hookId: "packet-drop",
-          startChapter: 5,
-          type: "mystery",
-          status: "progressing",
-          lastAdvancedChapter: 7,
-          expectedPayoff: "Open the dropped packet and expose who planted it",
-          payoffTiming: "near-term",
-          notes: "The packet has been foregrounded for two chapters already.",
-        },
-        {
-          hookId: "seal-crack",
-          startChapter: 4,
-          type: "artifact",
-          status: "progressing",
-          lastAdvancedChapter: 7,
-          expectedPayoff: "Reveal what the cracked seal is hiding",
-          payoffTiming: "immediate",
-          notes: "The cracked seal should pay off in the current local sequence.",
-        },
-        {
-          hookId: "witness-turn",
-          startChapter: 5,
-          type: "relationship",
-          status: "progressing",
-          lastAdvancedChapter: 7,
-          expectedPayoff: "Force the silent witness to choose a side",
-          payoffTiming: "near-term",
-          notes: "The witness line is ready for a concrete turn now.",
-        },
-      ] as never,
-    } as never);
-
-    // All three hooks are readyToResolve (progressing + recentlyTouched),
-    // pushing readyCount to 3 which triggers heavy load (eligibleResolve=2).
-    expect(agenda.eligibleResolve.length).toBe(2);
-    expect(agenda.pressureMap).toEqual([]);
-  });
-
-  it("routes all stale/overdue hooks to staleDebt by advancePressure, with remainder in mustAdvance", () => {
-    const agenda = memoryRetrieval.buildPlannerHookAgenda({
-      chapterNumber: 15,
-      targetChapters: 30,
-      hooks: [
-        {
-          hookId: "mentor-oath-a",
-          startChapter: 1,
-          type: "relationship",
-          status: "open",
-          lastAdvancedChapter: 2,
-          expectedPayoff: "Explain the first layer of the mentor oath debt",
-          payoffTiming: "mid-arc",
-          notes: "Old relationship debt keeps surfacing without movement.",
-        },
-        {
-          hookId: "mentor-oath-b",
-          startChapter: 2,
-          type: "relationship",
-          status: "open",
-          lastAdvancedChapter: 3,
-          expectedPayoff: "Show what the second oath witness is hiding",
-          payoffTiming: "mid-arc",
-          notes: "Another branch of the same relationship family is also stale.",
-        },
-        {
-          hookId: "mentor-oath-c",
-          startChapter: 3,
-          type: "relationship",
-          status: "open",
-          lastAdvancedChapter: 4,
-          expectedPayoff: "Reveal why the oath cannot be spoken aloud",
-          payoffTiming: "mid-arc",
-          notes: "The third relationship branch is still hanging.",
-        },
-        {
-          hookId: "kiln-key",
-          startChapter: 4,
-          type: "artifact",
-          status: "open",
-          lastAdvancedChapter: 5,
-          expectedPayoff: "Show what the kiln key unlocks",
-          payoffTiming: "mid-arc",
-          notes: "Artifact debt is also stale and should not vanish behind relationship debt.",
-        },
-      ] as never,
-    } as never);
-
-    // All four are stale+overdue (mid-arc, dormancy >> 4, age >> 8).
-    // Heavy load (staleCount=4): staleDebt limit=3 captures highest advancePressure.
-    // The fourth-highest (kiln-key) lands in mustAdvance.
-    expect(agenda.staleDebt).toEqual(["mentor-oath-a", "mentor-oath-b", "mentor-oath-c"]);
-    expect(agenda.mustAdvance).toEqual(["kiln-key"]);
-    expect(agenda.eligibleResolve).toEqual([]);
-    // avoidNewHookFamilies includes both stale families
-    expect(agenda.avoidNewHookFamilies).toEqual(expect.arrayContaining([
-      "relationship",
-      "artifact",
-    ]));
-  });
 });
 });

+ 19 - 101
packages/core/src/__tests__/models.test.ts

@@ -13,8 +13,8 @@ import {
   InputGovernanceModeSchema,
   InputGovernanceModeSchema,
 } from "../models/project.js";
 } from "../models/project.js";
 import {
 import {
-  ChapterBriefSchema,
   ChapterIntentSchema,
   ChapterIntentSchema,
+  ChapterMemoSchema,
   ContextPackageSchema,
   ContextPackageSchema,
   RuleStackSchema,
   RuleStackSchema,
   ChapterTraceSchema,
   ChapterTraceSchema,
@@ -498,67 +498,17 @@ describe("ChapterIntentSchema", () => {
       chapter: 12,
       chapter: 12,
       goal: "Pull focus back to the mentor conflict",
       goal: "Pull focus back to the mentor conflict",
       outlineNode: "Volume 2 / Chapter 12",
       outlineNode: "Volume 2 / Chapter 12",
-      sceneDirective: "Break the repeated investigation-room rhythm with a location change.",
-      arcDirective: "Advance toward the next concrete arc beat instead of replaying the fallback setup.",
-      moodDirective: "Release pressure for one chapter before the next escalation.",
-      titleDirective: "Avoid another ledger title and use a new concrete image.",
+      arcContext: "Volume arc: mentor confrontation, post-betrayal.",
       mustKeep: ["Protagonist remains injured"],
       mustKeep: ["Protagonist remains injured"],
       mustAvoid: ["Do not reveal the mastermind"],
       mustAvoid: ["Do not reveal the mastermind"],
       styleEmphasis: ["dialogue tension", "character conflict"],
       styleEmphasis: ["dialogue tension", "character conflict"],
-      conflicts: [
-        {
-          type: "outline_vs_focus",
-          resolution: "allow local outline deferral",
-        },
-      ],
-      hookAgenda: {
-        pressureMap: [
-          {
-            hookId: "H019",
-            type: "relationship",
-            payoffTiming: "slow-burn",
-            phase: "middle",
-            pressure: "high",
-            movement: "partial-payoff",
-            reason: "stale-promise",
-            blockSiblingHooks: true,
-          },
-        ],
-        mustAdvance: ["H019"],
-        eligibleResolve: ["H045"],
-        staleDebt: ["H023", "H027"],
-        avoidNewHookFamilies: [
-          "anonymous-source-restatement",
-          "mechanism-restatement",
-        ],
-      },
     });
     });
 
 
     expect(result.chapter).toBe(12);
     expect(result.chapter).toBe(12);
     expect(result.goal).toContain("mentor conflict");
     expect(result.goal).toContain("mentor conflict");
-    expect(result.sceneDirective).toContain("location change");
-    expect(result.arcDirective).toContain("arc beat");
-    expect(result.moodDirective).toContain("Release pressure");
-    expect(result.titleDirective).toContain("ledger title");
-    expect(result.conflicts).toHaveLength(1);
-    expect(result.hookAgenda.pressureMap).toEqual([
-      expect.objectContaining({
-        hookId: "H019",
-        type: "relationship",
-        phase: "middle",
-        movement: "partial-payoff",
-        pressure: "high",
-        payoffTiming: "slow-burn",
-        reason: "stale-promise",
-      }),
-    ]);
-    expect(result.hookAgenda.mustAdvance).toEqual(["H019"]);
-    expect(result.hookAgenda.eligibleResolve).toEqual(["H045"]);
-    expect(result.hookAgenda.staleDebt).toEqual(["H023", "H027"]);
-    expect(result.hookAgenda.avoidNewHookFamilies).toEqual([
-      "anonymous-source-restatement",
-      "mechanism-restatement",
-    ]);
+    expect(result.outlineNode).toBe("Volume 2 / Chapter 12");
+    expect(result.arcContext).toContain("Volume arc");
+    expect(result.mustKeep).toContain("Protagonist remains injured");
   });
   });
 
 
   it("defaults optional arrays to empty", () => {
   it("defaults optional arrays to empty", () => {
@@ -567,19 +517,11 @@ describe("ChapterIntentSchema", () => {
       goal: "Establish the protagonist's first setback",
       goal: "Establish the protagonist's first setback",
     });
     });
 
 
-    expect(result.sceneDirective).toBeUndefined();
-    expect(result.arcDirective).toBeUndefined();
-    expect(result.moodDirective).toBeUndefined();
-    expect(result.titleDirective).toBeUndefined();
+    expect(result.outlineNode).toBeUndefined();
+    expect(result.arcContext).toBeUndefined();
     expect(result.mustKeep).toEqual([]);
     expect(result.mustKeep).toEqual([]);
     expect(result.mustAvoid).toEqual([]);
     expect(result.mustAvoid).toEqual([]);
     expect(result.styleEmphasis).toEqual([]);
     expect(result.styleEmphasis).toEqual([]);
-    expect(result.conflicts).toEqual([]);
-    expect(result.hookAgenda.pressureMap).toEqual([]);
-    expect(result.hookAgenda.mustAdvance).toEqual([]);
-    expect(result.hookAgenda.eligibleResolve).toEqual([]);
-    expect(result.hookAgenda.staleDebt).toEqual([]);
-    expect(result.hookAgenda.avoidNewHookFamilies).toEqual([]);
   });
   });
 
 
   it("rejects invalid chapter numbers", () => {
   it("rejects invalid chapter numbers", () => {
@@ -592,55 +534,31 @@ describe("ChapterIntentSchema", () => {
   });
   });
 });
 });
 
 
-describe("ChapterBriefSchema", () => {
-  it("accepts a structured chapter brief without mustKeep", () => {
-    const result = ChapterBriefSchema.parse({
+describe("ChapterMemoSchema", () => {
+  it("accepts a memo with body prose", () => {
+    const result = ChapterMemoSchema.parse({
       chapter: 12,
       chapter: 12,
       goal: "Force the heroine to choose between the ledger and the mentor debt.",
       goal: "Force the heroine to choose between the ledger and the mentor debt.",
-      chapterType: "confrontation",
       isGoldenOpening: true,
       isGoldenOpening: true,
-      beatOutline: [
-        { phase: "opening", instruction: "Open inside the warehouse argument, not with recap." },
-        { phase: "development", instruction: "Expose the ledger discrepancy through action, not explanation." },
-        { phase: "hook", instruction: "End on the unsigned transfer sheet changing hands." },
-      ],
-      hookPlan: [
-        {
-          hookId: "H019",
-          movement: "advance",
-          targetEffect: "Move the mentor-debt line from abstract guilt to immediate cost.",
-        },
-      ],
-      dormantReason: "Hold the black-ring line for the next pressure spike.",
-      propsAndSetting: ["warehouse transfer sheet", "wet loading dock", "A Sheng"],
+      body: "Seven-section memo body (Phase 3 fills this with prose).",
+      hookRefs: ["H019", "H045"],
     });
     });
 
 
-    expect(result.chapterType).toBe("confrontation");
     expect(result.isGoldenOpening).toBe(true);
     expect(result.isGoldenOpening).toBe(true);
-    expect(result.beatOutline).toHaveLength(3);
-    expect(result.hookPlan).toEqual([
-      expect.objectContaining({
-        hookId: "H019",
-        movement: "advance",
-      }),
-    ]);
-    expect(result.propsAndSetting).toContain("wet loading dock");
+    expect(result.body).toContain("memo body");
+    expect(result.hookRefs).toEqual(["H019", "H045"]);
   });
   });
 
 
-  it("defaults optional brief arrays to empty", () => {
-    const result = ChapterBriefSchema.parse({
+  it("accepts a stub memo with empty body", () => {
+    const result = ChapterMemoSchema.parse({
       chapter: 3,
       chapter: 3,
       goal: "Lock the protagonist into the first irreversible choice.",
       goal: "Lock the protagonist into the first irreversible choice.",
-      chapterType: "opening",
-      beatOutline: [
-        { phase: "opening", instruction: "Open on the conflict." },
-      ],
+      body: "",
     });
     });
 
 
     expect(result.isGoldenOpening).toBe(false);
     expect(result.isGoldenOpening).toBe(false);
-    expect(result.hookPlan).toEqual([]);
-    expect(result.propsAndSetting).toEqual([]);
-    expect(result.dormantReason).toBeUndefined();
+    expect(result.body).toBe("");
+    expect(result.hookRefs).toEqual([]);
   });
   });
 });
 });
 
 

+ 0 - 132
packages/core/src/__tests__/persisted-governed-plan.test.ts

@@ -1,132 +0,0 @@
-import { describe, expect, it } from "vitest";
-import { mkdtemp, mkdir, rm, writeFile } from "node:fs/promises";
-import { tmpdir } from "node:os";
-import { join } from "node:path";
-import { loadPersistedPlan, relativeToBookDir } from "../pipeline/persisted-governed-plan.js";
-
-describe("persisted governed plan helpers", () => {
-  it("parses a persisted intent markdown file into a reusable plan", async () => {
-    const bookDir = await mkdtemp(join(tmpdir(), "inkos-persisted-plan-"));
-    const runtimeDir = join(bookDir, "story", "runtime");
-    await mkdir(runtimeDir, { recursive: true });
-
-    const runtimePath = join(runtimeDir, "chapter-0007.intent.md");
-    await writeFile(
-      runtimePath,
-      [
-        "# Chapter Intent",
-        "",
-        "## Goal",
-        "Bring the focus back to the mentor oath conflict.",
-        "",
-        "## Outline Node",
-        "Track the mentor oath fallout.",
-        "",
-        "## Chapter Brief",
-        "- chapterType: confrontation",
-        "- isGoldenOpening: true",
-        "- dormantReason: Hold the guild-route line for the next chapter.",
-        "",
-        "### Beat Outline",
-        "- opening: Open inside the oath confrontation.",
-        "- development: Push the token back into the argument.",
-        "- hook: End when the witness changes sides.",
-        "",
-        "### Hook Plan",
-        "- mentor-oath: advance -> Turn the oath debt into an immediate cost.",
-        "",
-        "### Props And Setting",
-        "- broken oath token",
-        "- rain-slick shrine steps",
-        "",
-        "## Must Keep",
-        "- Lin Yue keeps the oath token hidden.",
-        "- Mentor debt stays unresolved.",
-        "",
-        "## Must Avoid",
-        "- Open a new guild-route mystery.",
-        "",
-        "## Style Emphasis",
-        "- restrained prose",
-        "",
-        "## Conflicts",
-        "- duty: repay the oath without exposing the token",
-        "- trust: keep the mentor debt personal",
-        "",
-      ].join("\n"),
-      "utf-8",
-    );
-
-    try {
-      const plan = await loadPersistedPlan(bookDir, 7);
-
-      expect(plan).not.toBeNull();
-      expect(plan?.runtimePath).toBe(runtimePath);
-      expect(plan?.intent.goal).toBe("Bring the focus back to the mentor oath conflict.");
-      expect(plan?.intent.outlineNode).toBe("Track the mentor oath fallout.");
-      expect(plan?.intent.mustKeep).toEqual([
-        "Lin Yue keeps the oath token hidden.",
-        "Mentor debt stays unresolved.",
-      ]);
-      expect(plan?.intent.mustAvoid).toEqual(["Open a new guild-route mystery."]);
-      expect(plan?.intent.styleEmphasis).toEqual(["restrained prose"]);
-      expect(plan?.brief).toEqual(expect.objectContaining({
-        chapterType: "confrontation",
-        isGoldenOpening: true,
-        dormantReason: "Hold the guild-route line for the next chapter.",
-      }));
-      expect(plan?.brief?.beatOutline).toEqual(expect.arrayContaining([
-        expect.objectContaining({ phase: "opening" }),
-        expect.objectContaining({ phase: "hook" }),
-      ]));
-      expect(plan?.brief?.hookPlan).toEqual([
-        expect.objectContaining({
-          hookId: "mentor-oath",
-          movement: "advance",
-        }),
-      ]);
-      expect(plan?.brief?.propsAndSetting).toEqual([
-        "broken oath token",
-        "rain-slick shrine steps",
-      ]);
-      expect(plan?.intent.conflicts).toEqual([
-        { type: "duty", resolution: "repay the oath without exposing the token" },
-        { type: "trust", resolution: "keep the mentor debt personal" },
-      ]);
-      expect(plan?.plannerInputs).toEqual([runtimePath]);
-    } finally {
-      await rm(bookDir, { recursive: true, force: true });
-    }
-  });
-
-  it("rejects persisted intents whose goal is still a placeholder", async () => {
-    const bookDir = await mkdtemp(join(tmpdir(), "inkos-persisted-plan-invalid-"));
-    const runtimeDir = join(bookDir, "story", "runtime");
-    await mkdir(runtimeDir, { recursive: true });
-
-    await writeFile(
-      join(runtimeDir, "chapter-0003.intent.md"),
-      [
-        "# Chapter Intent",
-        "",
-        "## Goal",
-        "(describe the goal here)",
-        "",
-      ].join("\n"),
-      "utf-8",
-    );
-
-    try {
-      await expect(loadPersistedPlan(bookDir, 3)).resolves.toBeNull();
-    } finally {
-      await rm(bookDir, { recursive: true, force: true });
-    }
-  });
-
-  it("normalizes persisted artifact paths relative to the book directory", () => {
-    expect(relativeToBookDir(
-      "/tmp/book",
-      "/tmp/book/story/runtime/chapter-0001.intent.md",
-    )).toBe("story/runtime/chapter-0001.intent.md");
-  });
-});

+ 4 - 14
packages/core/src/__tests__/pipeline-agent.test.ts

@@ -66,14 +66,12 @@ describe("agent pipeline tools", () => {
           goal = overrideMatch[1].trim();
           goal = overrideMatch[1].trim();
         }
         }
       } catch { /* ignore missing file */ }
       } catch { /* ignore missing file */ }
-      const brief = {
+      const memo = {
         chapter: chapterNumber,
         chapter: chapterNumber,
         goal,
         goal,
-        chapterType: "推进" as const,
         isGoldenOpening: false,
         isGoldenOpening: false,
-        beatOutline: [{ phase: "opening" as const, instruction: "test" }],
-        hookPlan: [] as Array<{ hookId: string; movement: "quiet-hold" | "refresh" | "advance" | "partial-payoff" | "full-payoff"; targetEffect: string }>,
-        propsAndSetting: [] as string[],
+        body: "",
+        hookRefs: [] as string[],
       };
       };
       const intentMarkdown = [
       const intentMarkdown = [
         "# Chapter Intent",
         "# Chapter Intent",
@@ -93,16 +91,8 @@ describe("agent pipeline tools", () => {
           mustKeep: [],
           mustKeep: [],
           mustAvoid: [],
           mustAvoid: [],
           styleEmphasis: [],
           styleEmphasis: [],
-          conflicts: [],
-          hookAgenda: {
-            pressureMap: [],
-            mustAdvance: [],
-            eligibleResolve: [],
-            staleDebt: [],
-            avoidNewHookFamilies: [],
-          },
         },
         },
-        brief,
+        memo,
         intentMarkdown,
         intentMarkdown,
         plannerInputs: [runtimePath],
         plannerInputs: [runtimePath],
         runtimePath,
         runtimePath,

+ 19 - 64
packages/core/src/__tests__/pipeline-runner.test.ts

@@ -204,14 +204,12 @@ describe("PipelineRunner", () => {
     vi.spyOn(PlannerAgent.prototype, "planChapter").mockImplementation(async (input) => {
     vi.spyOn(PlannerAgent.prototype, "planChapter").mockImplementation(async (input) => {
       const chapterNumber = input.chapterNumber;
       const chapterNumber = input.chapterNumber;
       const goal = input.externalContext ?? "test goal";
       const goal = input.externalContext ?? "test goal";
-      const brief = {
+      const memo = {
         chapter: chapterNumber,
         chapter: chapterNumber,
         goal,
         goal,
-        chapterType: "推进" as const,
         isGoldenOpening: false,
         isGoldenOpening: false,
-        beatOutline: [{ phase: "opening" as const, instruction: "test" }],
-        hookPlan: [] as Array<{ hookId: string; movement: "quiet-hold" | "refresh" | "advance" | "partial-payoff" | "full-payoff"; targetEffect: string }>,
-        propsAndSetting: [] as string[],
+        body: "",
+        hookRefs: [] as string[],
       };
       };
       const intentMarkdown = [
       const intentMarkdown = [
         "# Chapter Intent",
         "# Chapter Intent",
@@ -231,22 +229,6 @@ describe("PipelineRunner", () => {
         "## Style Emphasis",
         "## Style Emphasis",
         "- none",
         "- none",
         "",
         "",
-        "## Conflicts",
-        "- none",
-        "",
-        "## Chapter Brief",
-        `- chapterType: ${brief.chapterType}`,
-        `- isGoldenOpening: ${brief.isGoldenOpening}`,
-        "",
-        "### Beat Outline",
-        "- opening: test",
-        "",
-        "### Hook Plan",
-        "- none",
-        "",
-        "### Props And Setting",
-        "- none",
-        "",
       ].join("\n");
       ].join("\n");
       const runtimeDir = join(input.bookDir, "story", "runtime");
       const runtimeDir = join(input.bookDir, "story", "runtime");
       const { mkdir: mkdirFs, writeFile: writeFileFs } = await import("node:fs/promises");
       const { mkdir: mkdirFs, writeFile: writeFileFs } = await import("node:fs/promises");
@@ -260,16 +242,8 @@ describe("PipelineRunner", () => {
           mustKeep: [],
           mustKeep: [],
           mustAvoid: [],
           mustAvoid: [],
           styleEmphasis: [],
           styleEmphasis: [],
-          conflicts: [],
-          hookAgenda: {
-            pressureMap: [],
-            mustAdvance: [],
-            eligibleResolve: [],
-            staleDebt: [],
-            avoidNewHookFamilies: [],
-          },
         },
         },
-        brief,
+        memo,
         intentMarkdown,
         intentMarkdown,
         plannerInputs: [runtimePath],
         plannerInputs: [runtimePath],
         runtimePath,
         runtimePath,
@@ -603,14 +577,12 @@ describe("PipelineRunner", () => {
       const runtimeDir = join(input.bookDir, "story", "runtime");
       const runtimeDir = join(input.bookDir, "story", "runtime");
       await mkdir(runtimeDir, { recursive: true });
       await mkdir(runtimeDir, { recursive: true });
       const goal = "Ignore the guild chase and bring focus back to mentor conflict.";
       const goal = "Ignore the guild chase and bring focus back to mentor conflict.";
-      const brief = {
+      const memo = {
         chapter: input.chapterNumber,
         chapter: input.chapterNumber,
         goal,
         goal,
-        chapterType: "confrontation" as const,
         isGoldenOpening: true,
         isGoldenOpening: true,
-        beatOutline: [{ phase: "opening" as const, instruction: "Open on the conflict." }],
-        hookPlan: [] as Array<{ hookId: string; movement: "quiet-hold" | "refresh" | "advance" | "partial-payoff" | "full-payoff"; targetEffect: string }>,
-        propsAndSetting: ["broken oath token"],
+        body: "",
+        hookRefs: [] as string[],
       };
       };
       const intentMarkdown = [
       const intentMarkdown = [
         "# Chapter Intent",
         "# Chapter Intent",
@@ -657,16 +629,8 @@ describe("PipelineRunner", () => {
           mustKeep: ["Lin Yue still hides the broken oath token."],
           mustKeep: ["Lin Yue still hides the broken oath token."],
           mustAvoid: [],
           mustAvoid: [],
           styleEmphasis: [],
           styleEmphasis: [],
-          conflicts: [{ type: "outline_vs_request", resolution: "allow local outline deferral" }],
-          hookAgenda: {
-            pressureMap: [],
-            mustAdvance: [],
-            eligibleResolve: [],
-            staleDebt: [],
-            avoidNewHookFamilies: [],
-          },
         },
         },
-        brief,
+        memo,
         intentMarkdown,
         intentMarkdown,
         plannerInputs: [runtimePath],
         plannerInputs: [runtimePath],
         runtimePath,
         runtimePath,
@@ -690,11 +654,10 @@ describe("PipelineRunner", () => {
       const writeInput = writeChapter.mock.calls[0]?.[0];
       const writeInput = writeChapter.mock.calls[0]?.[0];
       expect(writeInput?.externalContext).toBeUndefined();
       expect(writeInput?.externalContext).toBeUndefined();
       expect(writeInput?.chapterIntent).toContain("# Chapter Intent");
       expect(writeInput?.chapterIntent).toContain("# Chapter Intent");
-      expect(writeInput?.chapterBrief).toEqual(expect.objectContaining({
+      expect(writeInput?.chapterMemo).toEqual(expect.objectContaining({
         chapter: 1,
         chapter: 1,
       }));
       }));
       expect(writeInput?.contextPackage?.selectedContext.length).toBeGreaterThan(0);
       expect(writeInput?.contextPackage?.selectedContext.length).toBeGreaterThan(0);
-      expect(writeInput?.ruleStack?.activeOverrides).toHaveLength(1);
 
 
       const runtimeDir = join(state.bookDir(bookId), "story", "runtime");
       const runtimeDir = join(state.bookDir(bookId), "story", "runtime");
       await expect(stat(join(runtimeDir, "chapter-0001.intent.md"))).resolves.toBeTruthy();
       await expect(stat(join(runtimeDir, "chapter-0001.intent.md"))).resolves.toBeTruthy();
@@ -777,10 +740,10 @@ describe("PipelineRunner", () => {
     try {
     try {
       await runner.writeDraft(bookId);
       await runner.writeDraft(bookId);
 
 
-      expect(planChapter).not.toHaveBeenCalled();
+      // Phase 1: persisted plans disabled, planner is always invoked.
+      expect(planChapter).toHaveBeenCalledTimes(1);
       const writeInput = writeChapter.mock.calls[0]?.[0];
       const writeInput = writeChapter.mock.calls[0]?.[0];
-      expect(writeInput?.chapterIntent).toContain("Bring the focus back to the mentor conflict.");
-      expect(writeInput?.ruleStack?.activeOverrides).toHaveLength(1);
+      expect(writeInput?.chapterIntent).toBeDefined();
     } finally {
     } finally {
       await rm(root, { recursive: true, force: true });
       await rm(root, { recursive: true, force: true });
     }
     }
@@ -1288,16 +1251,12 @@ describe("PipelineRunner", () => {
       const result = await originalPlanChapter.call(this, input);
       const result = await originalPlanChapter.call(this, input);
       return {
       return {
         ...result,
         ...result,
-        brief: {
+        memo: {
           chapter: input.chapterNumber,
           chapter: input.chapterNumber,
           goal: result.intent.goal,
           goal: result.intent.goal,
-          chapterType: "confrontation",
           isGoldenOpening: true,
           isGoldenOpening: true,
-          beatOutline: [
-            { phase: "opening", instruction: "Open on the conflict." },
-          ],
-          hookPlan: [],
-          propsAndSetting: ["broken oath token"],
+          body: "",
+          hookRefs: [],
         },
         },
       };
       };
     });
     });
@@ -1324,7 +1283,7 @@ describe("PipelineRunner", () => {
       expect(composeChapter).toHaveBeenCalledTimes(1);
       expect(composeChapter).toHaveBeenCalledTimes(1);
       const writeInput = writeChapter.mock.calls[0]?.[0];
       const writeInput = writeChapter.mock.calls[0]?.[0];
       expect(writeInput?.chapterIntent).toContain("# Chapter Intent");
       expect(writeInput?.chapterIntent).toContain("# Chapter Intent");
-      expect(writeInput?.chapterBrief).toEqual(expect.objectContaining({
+      expect(writeInput?.chapterMemo).toEqual(expect.objectContaining({
         chapter: 1,
         chapter: 1,
       }));
       }));
       expect(writeInput?.contextPackage?.selectedContext.length).toBeGreaterThan(0);
       expect(writeInput?.contextPackage?.selectedContext.length).toBeGreaterThan(0);
@@ -1435,17 +1394,13 @@ describe("PipelineRunner", () => {
           mustKeep: [],
           mustKeep: [],
           mustAvoid: [],
           mustAvoid: [],
           styleEmphasis: [],
           styleEmphasis: [],
-          conflicts: [],
-          hookAgenda: { pressureMap: [], mustAdvance: [], eligibleResolve: [], staleDebt: [], avoidNewHookFamilies: [] },
         },
         },
-        brief: {
+        memo: {
           chapter: input.chapterNumber,
           chapter: input.chapterNumber,
           goal,
           goal,
-          chapterType: "推进",
           isGoldenOpening: false,
           isGoldenOpening: false,
-          beatOutline: [{ phase: "opening" as const, instruction: "test" }],
-          hookPlan: [],
-          propsAndSetting: [],
+          body: "",
+          hookRefs: [],
         },
         },
         intentMarkdown: intentMd,
         intentMarkdown: intentMd,
         plannerInputs: [runtimePath],
         plannerInputs: [runtimePath],

+ 0 - 81
packages/core/src/__tests__/planner-prompts.test.ts

@@ -1,81 +0,0 @@
-import { describe, expect, it } from "vitest";
-import {
-  buildPlannerSystemPrompt,
-  buildPlannerUserPrompt,
-} from "../agents/planner-prompts.js";
-import type { PlanningMaterials } from "../utils/planning-materials.js";
-
-const MATERIALS: PlanningMaterials = {
-  storyDir: "/tmp/project/books/harbor/story",
-  authorIntent: "# Author Intent\n\nKeep the mentor debt line central.\n",
-  currentFocus: "# Current Focus\n\nReturn to the warehouse confrontation.\n",
-  storyBible: "# Story Bible\n\n- The jade seal cannot be destroyed.\n",
-  volumeOutline: "# Volume Outline\n\n## Chapter 4\nForce the warehouse confrontation.\n",
-  bookRulesRaw: "---\nprohibitions:\n  - Do not reveal the mastermind\n---",
-  currentState: "# Current State\n\n- Lin Yue is still injured.\n",
-  chapterSummariesRaw: "# Chapter Summaries",
-  outlineNode: "Force the warehouse confrontation.",
-  recentSummaries: [
-    {
-      chapter: 3,
-      title: "Canal Shadow",
-      characters: "Lin Yue,A Sheng",
-      events: "Ledger route points to the warehouse.",
-      stateChanges: "Harbor pressure rises.",
-      hookActivity: "The unsigned transfer sheet becomes urgent.",
-      mood: "tight",
-      chapterType: "pursuit",
-    },
-  ],
-  activeHooks: [
-    {
-      hookId: "H019",
-      startChapter: 2,
-      type: "mentor-debt",
-      status: "active",
-      lastAdvancedChapter: 3,
-      expectedPayoff: "Debt becomes immediate cost",
-      payoffTiming: "mid-arc",
-      notes: "Pressure should move on-page soon.",
-    },
-  ],
-  previousEndingHook: "The unsigned transfer sheet becomes urgent.",
-  previousEndingExcerpt: "Lin Yue folded the transfer sheet and realized the warehouse door had already been opened from inside.",
-  memorySelection: {
-    summaries: [],
-    hooks: [],
-    activeHooks: [],
-    facts: [],
-    volumeSummaries: [],
-  },
-  plannerInputs: [],
-};
-
-describe("planner prompt builders", () => {
-  it("includes an explicit meta-language ban list in the system prompt", () => {
-    const prompt = buildPlannerSystemPrompt("zh");
-
-    expect(prompt).toContain("ChapterBrief");
-    expect(prompt).toContain("第一章");
-    expect(prompt).toContain("小爽点");
-  });
-
-  it("builds a chronicle-driven user prompt without mustKeep", () => {
-    const prompt = buildPlannerUserPrompt({
-      chapterNumber: 4,
-      targetChapters: 20,
-      genreName: "xuanhuan",
-      language: "zh",
-      materials: MATERIALS,
-    });
-
-    expect(prompt).toContain("## Previous Ending Hook");
-    expect(prompt).toContain("unsigned transfer sheet");
-    expect(prompt).toContain("## Previous Ending Excerpt");
-    expect(prompt).toContain("warehouse door");
-    expect(prompt).toContain("## Recent Chronicle");
-    expect(prompt).toContain("## Active Hooks");
-    expect(prompt).not.toContain("mustKeep");
-    expect(prompt).not.toContain("本章必须");
-  });
-});

+ 0 - 1619
packages/core/src/__tests__/planner.test.ts

@@ -1,1619 +0,0 @@
-import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
-import { mkdtemp, mkdir, readFile, rm, writeFile } from "node:fs/promises";
-import { tmpdir } from "node:os";
-import { join } from "node:path";
-import type { BookConfig } from "../models/book.js";
-import { PlannerAgent } from "../agents/planner.js";
-
-describe("PlannerAgent", () => {
-  let root: string;
-  let bookDir: string;
-  let storyDir: string;
-  let book: BookConfig;
-
-  beforeEach(async () => {
-    root = await mkdtemp(join(tmpdir(), "inkos-planner-test-"));
-    bookDir = join(root, "books", "planner-book");
-    storyDir = join(bookDir, "story");
-    await mkdir(join(storyDir, "runtime"), { recursive: true });
-
-    book = {
-      id: "planner-book",
-      title: "Planner Book",
-      platform: "tomato",
-      genre: "xuanhuan",
-      status: "active",
-      targetChapters: 20,
-      chapterWordCount: 3000,
-      createdAt: "2026-03-22T00:00:00.000Z",
-      updatedAt: "2026-03-22T00:00:00.000Z",
-    };
-
-    await Promise.all([
-      writeFile(
-        join(storyDir, "author_intent.md"),
-        "# Author Intent\n\nKeep the book emotionally centered on the mentor-student bond.\n",
-        "utf-8",
-      ),
-      writeFile(
-        join(storyDir, "current_focus.md"),
-        "# Current Focus\n\nBring the focus back to the mentor conflict before opening new subplots.\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, "volume_outline.md"),
-        "# Volume Outline\n\n## Chapter 3\nTrack the merchant guild's escape route.\n",
-        "utf-8",
-      ),
-      writeFile(
-        join(storyDir, "book_rules.md"),
-        "---\nprohibitions:\n  - Do not reveal the mastermind\n---\n\n# Book Rules\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, "pending_hooks.md"),
-        "# Pending Hooks\n\n- Why the mentor vanished after the trial.\n",
-        "utf-8",
-      ),
-      writeFile(
-        join(storyDir, "chapter_summaries.md"),
-        "# Chapter Summaries\n\n| 2 | Trial fallout | Mentor left without explanation |\n",
-        "utf-8",
-      ),
-    ]);
-  });
-
-  /**
-   * Default chat mock installed on the prototype: echoes the derived goal
-   * from the planner user prompt as the brief's goal, so legacy
-   * goal-derivation tests keep working. Individual tests can override
-   * with their own vi.spyOn on a specific instance.
-   */
-  beforeEach(() => {
-    vi.spyOn(
-      PlannerAgent.prototype as unknown as { chat: (...args: unknown[]) => Promise<{ content: string }> },
-      "chat",
-    // eslint-disable-next-line @typescript-eslint/no-explicit-any
-    ).mockImplementation(async (...args: unknown[]) => {
-      const messages = args[0] as Array<{ role: string; content: string }>;
-      const userMsg = messages.find((m) => m.role === "user");
-      const content = userMsg?.content ?? "";
-
-      function isPlaceholder(line: string): boolean {
-        return /^\(.*\)$/.test(line) || /^\(文件尚未创建\)/.test(line);
-      }
-
-      // Extract first substantive line from a prompt section
-      function extractFirstContent(section: string): string | undefined {
-        const re = new RegExp(`## ${section}\\n([\\s\\S]*?)(?=\\n## |$)`);
-        const block = content.match(re)?.[1];
-        if (!block) return undefined;
-        const lines = block.split("\n").map((l) => l.trim()).filter(
-          (l) => l.length > 0 && !l.startsWith("#") && !isPlaceholder(l),
-        );
-        // Prefer non-bullet text, but fall back to first bullet stripped of "- "
-        const plain = lines.find((l) => !l.startsWith("-"));
-        if (plain) return plain;
-        // Join multiple bullet items for richer goal extraction (mimics deriveGoal combining)
-        return lines
-          .filter((l) => l.startsWith("- "))
-          .map((l) => l.slice(2))
-          .slice(0, 3)
-          .join(";") || undefined;
-      }
-
-      const goalSeed = extractFirstContent("Goal Seed");
-      const focus = extractFirstContent("Current Focus");
-      const authorIntent = extractFirstContent("Author Intent");
-      const goal = (goalSeed && goalSeed !== "(no matched outline slice)" ? goalSeed : undefined)
-        ?? focus
-        ?? authorIntent
-        ?? "advance the story";
-      const chapterMatch = content.match(/## Chapter\n(\d+)/);
-      const chapter = chapterMatch ? Number(chapterMatch[1]) : 3;
-
-      return {
-        content: JSON.stringify({
-          chapter,
-          goal,
-          chapterType: "推进",
-          isGoldenOpening: chapter <= 3,
-          beatOutline: [
-            { phase: "opening", instruction: "open scene" },
-            { phase: "development", instruction: "develop conflict" },
-            { phase: "hook", instruction: "end with hook" },
-          ],
-          hookPlan: [],
-          propsAndSetting: [],
-        }),
-      };
-    });
-  });
-
-  afterEach(async () => {
-    await rm(root, { recursive: true, force: true });
-  });
-
-  it("uses current focus as the chapter goal when no outline node is available", async () => {
-    await writeFile(
-      join(storyDir, "volume_outline.md"),
-      "# Volume Outline\n",
-      "utf-8",
-    );
-
-    const planner = new PlannerAgent({
-      client: {} as ConstructorParameters<typeof PlannerAgent>[0]["client"],
-      model: "test-model",
-      projectRoot: root,
-      bookId: book.id,
-    });
-
-    const result = await planner.planChapter({
-      book,
-      bookDir,
-      chapterNumber: 3,
-    });
-
-    expect(result.intent.goal).toContain("mentor conflict");
-    await expect(readFile(result.runtimePath, "utf-8")).resolves.toContain("mentor conflict");
-  });
-
-  it("prefers a matched outline node over ordinary current focus text", async () => {
-    await Promise.all([
-      writeFile(
-        join(storyDir, "current_focus.md"),
-        [
-          "# Current Focus",
-          "",
-          "## Active Focus",
-          "",
-          "Pull the next chapter back toward the mentor fallout instead of the guild route.",
-          "",
-        ].join("\n"),
-        "utf-8",
-      ),
-      writeFile(
-        join(storyDir, "volume_outline.md"),
-        [
-          "# Volume Outline",
-          "",
-          "## Chapter 3",
-          "Track the merchant guild's escape route through the western canal.",
-          "",
-        ].join("\n"),
-        "utf-8",
-      ),
-    ]);
-
-    const planner = new PlannerAgent({
-      client: {} as ConstructorParameters<typeof PlannerAgent>[0]["client"],
-      model: "test-model",
-      projectRoot: root,
-      bookId: book.id,
-    });
-
-    const result = await planner.planChapter({
-      book,
-      bookDir,
-      chapterNumber: 3,
-    });
-
-    expect(result.intent.outlineNode).toContain("merchant guild's escape route");
-    expect(result.intent.goal).toContain("merchant guild's escape route");
-    expect(result.intent.goal).not.toContain("mentor fallout");
-    expect(result.intent.conflicts).toEqual([]);
-  });
-
-  it("lets explicit local override focus beat the matched outline node", async () => {
-    await Promise.all([
-      writeFile(
-        join(storyDir, "current_focus.md"),
-        [
-          "# Current Focus",
-          "",
-          "## Active Focus",
-          "",
-          "Keep pressure on the guild route in the background.",
-          "",
-          "## Local Override",
-          "",
-          "Stay inside the mentor debt confrontation first and delay the canal pursuit by one chapter.",
-          "",
-        ].join("\n"),
-        "utf-8",
-      ),
-      writeFile(
-        join(storyDir, "volume_outline.md"),
-        [
-          "# Volume Outline",
-          "",
-          "## Chapter 3",
-          "Track the merchant guild's escape route through the western canal.",
-          "",
-        ].join("\n"),
-        "utf-8",
-      ),
-    ]);
-
-    const planner = new PlannerAgent({
-      client: {} as ConstructorParameters<typeof PlannerAgent>[0]["client"],
-      model: "test-model",
-      projectRoot: root,
-      bookId: book.id,
-    });
-
-    const result = await planner.planChapter({
-      book,
-      bookDir,
-      chapterNumber: 3,
-    });
-
-    expect(result.intent.outlineNode).toContain("merchant guild's escape route");
-    // goal is now LLM-derived from brief; verify conflict detection still works
-    expect(result.intent.conflicts).toEqual(expect.arrayContaining([
-      expect.objectContaining({
-        type: "outline_vs_current_focus",
-        resolution: "allow explicit current focus override",
-      }),
-    ]));
-    expect(result.brief).toBeDefined();
-  });
-
-  it("keeps external context above both outline anchors and current focus", async () => {
-    await Promise.all([
-      writeFile(
-        join(storyDir, "current_focus.md"),
-        [
-          "# Current Focus",
-          "",
-          "## Active Focus",
-          "",
-          "Pull the next chapter back toward the mentor fallout instead of the guild route.",
-          "",
-        ].join("\n"),
-        "utf-8",
-      ),
-      writeFile(
-        join(storyDir, "volume_outline.md"),
-        [
-          "# Volume Outline",
-          "",
-          "## Chapter 3",
-          "Track the merchant guild's escape route through the western canal.",
-          "",
-        ].join("\n"),
-        "utf-8",
-      ),
-    ]);
-
-    const planner = new PlannerAgent({
-      client: {} as ConstructorParameters<typeof PlannerAgent>[0]["client"],
-      model: "test-model",
-      projectRoot: root,
-      bookId: book.id,
-    });
-
-    const result = await planner.planChapter({
-      book,
-      bookDir,
-      chapterNumber: 3,
-      externalContext: "Ignore the canal pursuit for now and force the next chapter into the mentor debt confrontation.",
-    });
-
-    // goal is now LLM-derived; verify conflict detection for external override
-    expect(result.intent.conflicts).toEqual(expect.arrayContaining([
-      expect.objectContaining({
-        type: "outline_vs_request",
-        resolution: "allow local outline deferral",
-      }),
-    ]));
-    expect(result.brief).toBeDefined();
-  });
-
-  it("emits structured directives when fallback planning, chapter type repetition, and title collapse stack up", async () => {
-    book = {
-      ...book,
-      genre: "other",
-      language: "en",
-    };
-
-    await Promise.all([
-      writeFile(
-        join(storyDir, "author_intent.md"),
-        "# Author Intent\n\n(Describe the long-horizon vision for this book here.)\n",
-        "utf-8",
-      ),
-      writeFile(
-        join(storyDir, "current_focus.md"),
-        [
-          "# Current Focus",
-          "",
-          "## Active Focus",
-          "",
-          "(Describe what the next 1-3 chapters should prioritize.)",
-          "",
-        ].join("\n"),
-        "utf-8",
-      ),
-      writeFile(
-        join(storyDir, "volume_outline.md"),
-        [
-          "# Volume Outline",
-          "",
-          "## Chapter 8",
-          "Expose the registry clerk's hidden ledger in the floodgate archive.",
-          "",
-        ].join("\n"),
-        "utf-8",
-      ),
-      writeFile(
-        join(storyDir, "chapter_summaries.md"),
-        [
-          "# Chapter Summaries",
-          "",
-          "| chapter | title | characters | events | stateChanges | hookActivity | mood | chapterType |",
-          "| --- | --- | --- | --- | --- | --- | --- | --- |",
-          "| 1 | Ledger in Rain | Taryn | Taryn checks the first false folio | None | hook advanced | tight | investigation |",
-          "| 2 | Ledger at Dusk | Taryn | Taryn questions the dock clerk | None | hook advanced | tight | investigation |",
-          "| 3 | Ledger Below | Taryn | Taryn searches the under-archive | None | hook advanced | tight | investigation |",
-          "",
-        ].join("\n"),
-        "utf-8",
-      ),
-    ]);
-
-    const planner = new PlannerAgent({
-      client: {} as ConstructorParameters<typeof PlannerAgent>[0]["client"],
-      model: "test-model",
-      projectRoot: root,
-      bookId: book.id,
-    });
-
-    const result = await planner.planChapter({
-      book,
-      bookDir,
-      chapterNumber: 4,
-    });
-
-    // With LLM brief path, arc/scene directives are derived from the brief
-    expect(result.intent.arcDirective).toBeDefined();
-    expect(result.intent.sceneDirective).toBeDefined();
-    expect(result.brief).toBeDefined();
-    expect(result.brief.beatOutline.length).toBeGreaterThanOrEqual(1);
-  });
-
-  it("emits a cycle directive when recent chapters are all high-tension", async () => {
-    book = {
-      ...book,
-      genre: "other",
-      language: "zh",
-    };
-
-    await Promise.all([
-      writeFile(
-        join(storyDir, "volume_outline.md"),
-        "# Volume Outline\n\n## Chapter 5\n进入新的地点。\n",
-        "utf-8",
-      ),
-      writeFile(
-        join(storyDir, "chapter_summaries.md"),
-        [
-          "# Chapter Summaries",
-          "",
-          "| chapter | title | characters | events | stateChanges | hookActivity | mood | chapterType |",
-          "| --- | --- | --- | --- | --- | --- | --- | --- |",
-          "| 1 | 暗巷追踪 | 周谨川 | 追踪目标 | None | none | 紧张、压抑 | 悬念验证章 |",
-          "| 2 | 旧楼对峙 | 周谨川 | 对峙 | None | none | 冷硬、逼仄 | 冲突章 |",
-          "| 3 | 夜色围堵 | 周谨川 | 围堵 | None | none | 肃杀、凝重 | 追击章 |",
-          "| 4 | 地下通道 | 周谨川 | 逃脱 | None | none | 压迫、窒息 | 逃亡章 |",
-          "",
-        ].join("\n"),
-        "utf-8",
-      ),
-    ]);
-
-    const planner = new PlannerAgent({
-      client: {} as ConstructorParameters<typeof PlannerAgent>[0]["client"],
-      model: "test-model",
-      projectRoot: root,
-      bookId: book.id,
-    });
-
-    const result = await planner.planChapter({
-      book,
-      bookDir,
-      chapterNumber: 5,
-    });
-
-    // With objective cycle analysis, the moodDirective now contains
-    // cycle-phase guidance instead of the old mood-based cooldown
-    expect(result.intent.moodDirective).toBeDefined();
-    expect(result.intent.moodDirective).toContain("本章");
-  });
-
-  it("emits a cycle directive for varied moods based on chapter type patterns", async () => {
-    book = {
-      ...book,
-      genre: "other",
-      language: "en",
-    };
-
-    await Promise.all([
-      writeFile(
-        join(storyDir, "volume_outline.md"),
-        "# Volume Outline\n\n## Chapter 5\nMove to the harbor.\n",
-        "utf-8",
-      ),
-      writeFile(
-        join(storyDir, "chapter_summaries.md"),
-        [
-          "# Chapter Summaries",
-          "",
-          "| chapter | title | characters | events | stateChanges | hookActivity | mood | chapterType |",
-          "| --- | --- | --- | --- | --- | --- | --- | --- |",
-          "| 1 | Morning Calm | Taryn | A quiet walk | None | none | warm, gentle | slice-of-life |",
-          "| 2 | Sudden Rain | Taryn | Storm arrives | None | none | tense, ominous | tension |",
-          "| 3 | Harbor Light | Taryn | Finds shelter | None | none | hopeful, light | transition |",
-          "| 4 | The Letter | Taryn | Reads bad news | None | none | melancholy, reflective | introspection |",
-          "",
-        ].join("\n"),
-        "utf-8",
-      ),
-    ]);
-
-    const planner = new PlannerAgent({
-      client: {} as ConstructorParameters<typeof PlannerAgent>[0]["client"],
-      model: "test-model",
-      projectRoot: root,
-      bookId: book.id,
-    });
-
-    const result = await planner.planChapter({
-      book,
-      bookDir,
-      chapterNumber: 5,
-    });
-
-    // With the new cycle system, a cycle directive is always emitted
-    // based on chapter type patterns rather than mood analysis
-    expect(result.intent.moodDirective).toBeDefined();
-    expect(result.intent.moodDirective).toContain("chapter");
-  });
-
-  it("ignores the default current_focus placeholder and falls back to author intent when no chapter outline is available", async () => {
-    await Promise.all([
-      writeFile(
-        join(storyDir, "current_focus.md"),
-        [
-          "# Current Focus",
-          "",
-          "## Active Focus",
-          "",
-          "(Describe what the next 1-3 chapters should prioritize.)",
-          "",
-        ].join("\n"),
-        "utf-8",
-      ),
-      writeFile(
-        join(storyDir, "volume_outline.md"),
-        "# Volume Outline\n",
-        "utf-8",
-      ),
-    ]);
-
-    const planner = new PlannerAgent({
-      client: {} as ConstructorParameters<typeof PlannerAgent>[0]["client"],
-      model: "test-model",
-      projectRoot: root,
-      bookId: book.id,
-    });
-
-    const result = await planner.planChapter({
-      book,
-      bookDir,
-      chapterNumber: 3,
-    });
-
-    expect(result.intent.goal).toContain("mentor-student bond");
-    expect(result.intent.goal).not.toContain("Describe what the next 1-3 chapters should prioritize");
-  });
-
-  it("uses bullet-style volume outline chapter nodes as the fallback goal when control docs are placeholders", async () => {
-    await Promise.all([
-      writeFile(
-        join(storyDir, "author_intent.md"),
-        "# Author Intent\n\n(Describe the long-horizon vision for this book here.)\n",
-        "utf-8",
-      ),
-      writeFile(
-        join(storyDir, "current_focus.md"),
-        [
-          "# Current Focus",
-          "",
-          "## Active Focus",
-          "",
-          "(Describe what the next 1-3 chapters should prioritize.)",
-          "",
-        ].join("\n"),
-        "utf-8",
-      ),
-      writeFile(
-        join(storyDir, "volume_outline.md"),
-        [
-          "## Volume 1",
-          "**Chapter range:** 1-8",
-          "",
-          "**Key turning points:**",
-          "- **Chapter 3:** Track the merchant guild's escape route through the western canal.",
-          "",
-        ].join("\n"),
-        "utf-8",
-      ),
-    ]);
-
-    const planner = new PlannerAgent({
-      client: {} as ConstructorParameters<typeof PlannerAgent>[0]["client"],
-      model: "test-model",
-      projectRoot: root,
-      bookId: book.id,
-    });
-
-    const result = await planner.planChapter({
-      book,
-      bookDir,
-      chapterNumber: 3,
-    });
-
-    expect(result.intent.outlineNode).toContain("merchant guild's escape route");
-    expect(result.intent.goal).toContain("merchant guild's escape route");
-    expect(result.intent.goal).not.toContain("Advance chapter 3 with clear narrative focus.");
-  });
-
-  it("uses the next paragraph for bold standalone English chapter labels instead of capturing markdown markers", async () => {
-    book = {
-      ...book,
-      genre: "other",
-      language: "en",
-    };
-
-    await Promise.all([
-      writeFile(
-        join(storyDir, "current_focus.md"),
-        [
-          "# Current Focus",
-          "",
-          "## Active Focus",
-          "",
-          "(Describe what the next 1-3 chapters should prioritize.)",
-          "",
-        ].join("\n"),
-        "utf-8",
-      ),
-      writeFile(
-        join(storyDir, "volume_outline.md"),
-        [
-          "## Volume 1 - The Dead Examiner",
-          "**Chapter Range:** 1-12",
-          "",
-          "**Key Turning Points:**",
-          "- Ch1: Renn dies after summoning Taryn to review irregular treaty folios.",
-          "",
-          "### Golden First Three Chapters Rule",
-          "",
-          "**Chapter 2:**",
-          "Show Taryn's edge through action, not exposition. He uses registry numbering logic to identify which folios are decoys and which conceal a ledger fragment.",
-          "",
-        ].join("\n"),
-        "utf-8",
-      ),
-    ]);
-
-    const planner = new PlannerAgent({
-      client: {} as ConstructorParameters<typeof PlannerAgent>[0]["client"],
-      model: "test-model",
-      projectRoot: root,
-      bookId: book.id,
-    });
-
-    const result = await planner.planChapter({
-      book,
-      bookDir,
-      chapterNumber: 2,
-    });
-
-    expect(result.intent.outlineNode).toContain("Show Taryn's edge through action");
-    expect(result.intent.outlineNode).not.toBe("**");
-    expect(result.intent.goal).toContain("Show Taryn's edge through action");
-    expect(result.intent.goal).not.toBe("**");
-  });
-
-  it("does not confuse Chapter 1 with Chapter 10 when matching exact English chapter labels", async () => {
-    book = {
-      ...book,
-      genre: "other",
-      language: "en",
-    };
-
-    await Promise.all([
-      writeFile(
-        join(storyDir, "current_focus.md"),
-        [
-          "# Current Focus",
-          "",
-          "## Active Focus",
-          "",
-          "(Describe what the next 1-3 chapters should prioritize.)",
-          "",
-        ].join("\n"),
-        "utf-8",
-      ),
-      writeFile(
-        join(storyDir, "volume_outline.md"),
-        [
-          "# Volume Outline",
-          "",
-          "### Chapter 10",
-          "This late-volume node should not be selected for chapter one.",
-          "",
-          "### Chapter 1",
-          "Open with the dead examiner and the sealed folio dispute.",
-          "",
-        ].join("\n"),
-        "utf-8",
-      ),
-    ]);
-
-    const planner = new PlannerAgent({
-      client: {} as ConstructorParameters<typeof PlannerAgent>[0]["client"],
-      model: "test-model",
-      projectRoot: root,
-      bookId: book.id,
-    });
-
-    const result = await planner.planChapter({
-      book,
-      bookDir,
-      chapterNumber: 1,
-    });
-
-    expect(result.intent.outlineNode).toContain("dead examiner");
-    expect(result.intent.outlineNode).not.toContain("late-volume");
-    expect(result.intent.goal).toContain("dead examiner");
-  });
-
-  it("uses inline Chinese exact chapter labels with a title suffix", async () => {
-    await Promise.all([
-      writeFile(
-        join(storyDir, "author_intent.md"),
-        "# Author Intent\n\n(Describe the long-horizon vision for this book here.)\n",
-        "utf-8",
-      ),
-      writeFile(
-        join(storyDir, "current_focus.md"),
-        [
-          "# Current Focus",
-          "",
-          "## Active Focus",
-          "",
-          "(Describe what the next 1-3 chapters should prioritize.)",
-          "",
-        ].join("\n"),
-        "utf-8",
-      ),
-      writeFile(
-        join(storyDir, "volume_outline.md"),
-        [
-          "# Volume Outline",
-          "",
-          "第 7 章:在码头接头并截住逃跑账房。",
-          "",
-        ].join("\n"),
-        "utf-8",
-      ),
-    ]);
-
-    const planner = new PlannerAgent({
-      client: {} as ConstructorParameters<typeof PlannerAgent>[0]["client"],
-      model: "test-model",
-      projectRoot: root,
-      bookId: book.id,
-    });
-
-    const result = await planner.planChapter({
-      book,
-      bookDir,
-      chapterNumber: 7,
-    });
-
-    expect(result.intent.outlineNode).toContain("在码头接头");
-    expect(result.intent.goal).toContain("在码头接头");
-    expect(result.intent.goal).not.toContain("Describe the long-horizon vision");
-  });
-
-  it("uses standalone Chinese chapter-range labels when the chapter falls inside the range", async () => {
-    await Promise.all([
-      writeFile(
-        join(storyDir, "author_intent.md"),
-        "# Author Intent\n\n(Describe the long-horizon vision for this book here.)\n",
-        "utf-8",
-      ),
-      writeFile(
-        join(storyDir, "current_focus.md"),
-        [
-          "# Current Focus",
-          "",
-          "## Active Focus",
-          "",
-          "(Describe what the next 1-3 chapters should prioritize.)",
-          "",
-        ].join("\n"),
-        "utf-8",
-      ),
-      writeFile(
-        join(storyDir, "volume_outline.md"),
-        [
-          "# Volume Outline",
-          "",
-          "第1-6章",
-          "Stay with the early city setup and mentor fallout.",
-          "",
-          "第7-20章",
-          "Track the merchant guild's escape route through the western canal.",
-          "",
-        ].join("\n"),
-        "utf-8",
-      ),
-    ]);
-
-    const planner = new PlannerAgent({
-      client: {} as ConstructorParameters<typeof PlannerAgent>[0]["client"],
-      model: "test-model",
-      projectRoot: root,
-      bookId: book.id,
-    });
-
-    const result = await planner.planChapter({
-      book,
-      bookDir,
-      chapterNumber: 7,
-    });
-
-    expect(result.intent.outlineNode).toContain("merchant guild's escape route");
-    expect(result.intent.goal).toContain("merchant guild's escape route");
-    expect(result.intent.goal).not.toContain("Describe the long-horizon vision");
-  });
-
-  it("uses standalone English chapter-range labels at the start of the range", async () => {
-    book = {
-      ...book,
-      genre: "other",
-      language: "en",
-    };
-
-    await Promise.all([
-      writeFile(
-        join(storyDir, "author_intent.md"),
-        "# Author Intent\n\n(Describe the long-horizon vision for this book here.)\n",
-        "utf-8",
-      ),
-      writeFile(
-        join(storyDir, "current_focus.md"),
-        [
-          "# Current Focus",
-          "",
-          "## Active Focus",
-          "",
-          "(Describe what the next 1-3 chapters should prioritize.)",
-          "",
-        ].join("\n"),
-        "utf-8",
-      ),
-      writeFile(
-        join(storyDir, "volume_outline.md"),
-        [
-          "# Volume Outline",
-          "",
-          "Chapter 1-3",
-          "Keep the opening pressure on the first examiner.",
-          "",
-          "Chapter 4-6",
-          "Recover the sealed ledger before dawn.",
-          "",
-        ].join("\n"),
-        "utf-8",
-      ),
-    ]);
-
-    const planner = new PlannerAgent({
-      client: {} as ConstructorParameters<typeof PlannerAgent>[0]["client"],
-      model: "test-model",
-      projectRoot: root,
-      bookId: book.id,
-    });
-
-    const result = await planner.planChapter({
-      book,
-      bookDir,
-      chapterNumber: 4,
-    });
-
-    expect(result.intent.outlineNode).toContain("sealed ledger");
-    expect(result.intent.goal).toContain("sealed ledger");
-    expect(result.intent.outlineNode).not.toContain("6");
-    expect(result.intent.goal).not.toContain("Describe the long-horizon vision");
-  });
-
-  it("uses the next paragraph for bold standalone English chapter-range labels instead of bleeding into the next range", async () => {
-    book = {
-      ...book,
-      genre: "other",
-      language: "en",
-    };
-
-    await Promise.all([
-      writeFile(
-        join(storyDir, "author_intent.md"),
-        "# Author Intent\n\n(Describe the long-horizon vision for this book here.)\n",
-        "utf-8",
-      ),
-      writeFile(
-        join(storyDir, "current_focus.md"),
-        [
-          "# Current Focus",
-          "",
-          "## Active Focus",
-          "",
-          "(Describe what the next 1-3 chapters should prioritize.)",
-          "",
-        ].join("\n"),
-        "utf-8",
-      ),
-      writeFile(
-        join(storyDir, "volume_outline.md"),
-        [
-          "# Volume Outline",
-          "",
-          "**Chapter 1-3:**",
-          "Keep the opening pressure on the first examiner.",
-          "",
-          "**Chapter 4-6:**",
-          "Recover the sealed ledger before dawn.",
-          "",
-          "**Chapter 7-9:**",
-          "Trigger the registry fire and expose the false witness.",
-          "",
-        ].join("\n"),
-        "utf-8",
-      ),
-    ]);
-
-    const planner = new PlannerAgent({
-      client: {} as ConstructorParameters<typeof PlannerAgent>[0]["client"],
-      model: "test-model",
-      projectRoot: root,
-      bookId: book.id,
-    });
-
-    const result = await planner.planChapter({
-      book,
-      bookDir,
-      chapterNumber: 4,
-    });
-
-    expect(result.intent.outlineNode).toContain("sealed ledger");
-    expect(result.intent.outlineNode).not.toContain("registry fire");
-    expect(result.intent.goal).toContain("sealed ledger");
-  });
-
-  it("falls back to the first outline directive when no standalone range matches", async () => {
-    await Promise.all([
-      writeFile(
-        join(storyDir, "author_intent.md"),
-        "# Author Intent\n\n(Describe the long-horizon vision for this book here.)\n",
-        "utf-8",
-      ),
-      writeFile(
-        join(storyDir, "current_focus.md"),
-        [
-          "# Current Focus",
-          "",
-          "## Active Focus",
-          "",
-          "(Describe what the next 1-3 chapters should prioritize.)",
-          "",
-        ].join("\n"),
-        "utf-8",
-      ),
-      writeFile(
-        join(storyDir, "volume_outline.md"),
-        [
-          "# Volume Outline",
-          "",
-          "第1-6章",
-          "Stay with the early city setup and mentor fallout.",
-          "",
-          "第7-20章",
-          "Track the merchant guild's escape route through the western canal.",
-          "",
-        ].join("\n"),
-        "utf-8",
-      ),
-    ]);
-
-    const planner = new PlannerAgent({
-      client: {} as ConstructorParameters<typeof PlannerAgent>[0]["client"],
-      model: "test-model",
-      projectRoot: root,
-      bookId: book.id,
-    });
-
-    const result = await planner.planChapter({
-      book,
-      bookDir,
-      chapterNumber: 25,
-    });
-
-    expect(result.intent.outlineNode).toContain("Stay with the early city setup");
-    expect(result.intent.goal).toContain("Stay with the early city setup");
-    expect(result.intent.outlineNode).not.toBe("第1-6章");
-    expect(result.intent.goal).not.toContain("merchant guild's escape route");
-  });
-
-  it("preserves hard facts from state and canon in mustKeep", async () => {
-    const planner = new PlannerAgent({
-      client: {} as ConstructorParameters<typeof PlannerAgent>[0]["client"],
-      model: "test-model",
-      projectRoot: root,
-      bookId: book.id,
-    });
-
-    const result = await planner.planChapter({
-      book,
-      bookDir,
-      chapterNumber: 3,
-    });
-
-    expect(result.intent.mustKeep).toContain("Lin Yue still hides the broken oath token.");
-    expect(result.intent.mustKeep).toContain("The jade seal cannot be destroyed.");
-  });
-
-  it("records conflicts when the external request diverges from the outline", async () => {
-    const planner = new PlannerAgent({
-      client: {} as ConstructorParameters<typeof PlannerAgent>[0]["client"],
-      model: "test-model",
-      projectRoot: root,
-      bookId: book.id,
-    });
-
-    const result = await planner.planChapter({
-      book,
-      bookDir,
-      chapterNumber: 3,
-      externalContext: "Ignore the guild chase and bring the focus back to mentor conflict.",
-    });
-
-    expect(result.intent.conflicts).toHaveLength(1);
-    expect(result.intent.conflicts[0]?.type).toBe("outline_vs_request");
-    await expect(readFile(result.runtimePath, "utf-8")).resolves.toContain("outline_vs_request");
-  });
-
-  it("writes compact memory snapshots instead of inlining the full history", async () => {
-    await Promise.all([
-      writeFile(
-        join(storyDir, "pending_hooks.md"),
-        [
-          "# Pending Hooks",
-          "",
-          "| hook_id | 起始章节 | 类型 | 状态 | 最近推进 | 预期回收 | 备注 |",
-          "| --- | --- | --- | --- | --- | --- | --- |",
-          "| guild-route | 1 | mystery | open | 2 | 6 | Merchant guild trail |",
-          "| mentor-oath | 8 | relationship | open | 9 | 11 | Mentor oath debt with Lin Yue |",
-          "| old-seal | 3 | artifact | resolved | 3 | 3 | Jade seal already recovered |",
-          "",
-        ].join("\n"),
-        "utf-8",
-      ),
-      writeFile(
-        join(storyDir, "chapter_summaries.md"),
-        [
-          "# Chapter Summaries",
-          "",
-          "| 1 | Guild Trail | Merchant guild flees west | Route clues only | None | guild-route seeded | tense | action |",
-          "| 2 | City Watch | Patrols sweep the market | Search widens | None | guild-route advanced | urgent | investigation |",
-          "| 3 | Seal Vault | Lin Yue finds the seal vault | The jade seal returns | Seal secured | old-seal resolved | solemn | reveal |",
-          "| 4 | Empty Road | The group loses the convoy | Doubts grow | Travel fatigue | none | grim | travel |",
-          "| 5 | Burned Shrine | Shrine clues point nowhere | Friction rises | Lin Yue distrusts allies | none | bitter | setback |",
-          "| 6 | Quiet Ledger | Merchant records stay hidden | No breakthrough | Cash runs thin | none | weary | transition |",
-          "| 7 | Broken Letter | A torn letter mentions the mentor | Suspicion returns | Lin Yue reopens the old oath | mentor-oath seeded | uneasy | mystery |",
-          "| 8 | River Camp | Lin Yue meets old witnesses | Mentor debt becomes personal | Lin Yue cannot let go | mentor-oath advanced | raw | confrontation |",
-          "| 9 | Trial Echo | The trial fallout resurfaces | Mentor left without explanation | Oath token matters again | mentor-oath advanced | aching | fallout |",
-          "| 10 | Locked Gate | Lin Yue chooses the mentor line over the guild line | Mentor conflict takes priority | Oath token is still hidden | mentor-oath advanced | focused | decision |",
-          "",
-        ].join("\n"),
-        "utf-8",
-      ),
-    ]);
-
-    const planner = new PlannerAgent({
-      client: {} as ConstructorParameters<typeof PlannerAgent>[0]["client"],
-      model: "test-model",
-      projectRoot: root,
-      bookId: book.id,
-    });
-
-    const result = await planner.planChapter({
-      book,
-      bookDir,
-      chapterNumber: 11,
-      externalContext: "Bring the focus back to the mentor oath conflict with Lin Yue.",
-    });
-
-    const intentMarkdown = await readFile(result.runtimePath, "utf-8");
-    expect(intentMarkdown).toContain("mentor-oath");
-    expect(intentMarkdown).toContain("| 10 | Locked Gate |");
-    expect(intentMarkdown).not.toContain("| 1 | Guild Trail |");
-    expect(intentMarkdown).not.toContain("| old-seal | 3 | artifact | resolved |");
-  });
-
-  it("renders English memory snapshot headers for English books", async () => {
-    book = {
-      ...book,
-      genre: "other",
-      language: "en",
-    };
-    await Promise.all([
-      writeFile(
-        join(storyDir, "pending_hooks.md"),
-        [
-          "# Pending Hooks",
-          "",
-          "| hook_id | start_chapter | type | status | last_advanced | expected_payoff | notes |",
-          "| --- | --- | --- | --- | --- | --- | --- |",
-          "| mentor-oath | 8 | relationship | open | 9 | 11 | Mentor oath debt with Lin Yue |",
-          "",
-        ].join("\n"),
-        "utf-8",
-      ),
-      writeFile(
-        join(storyDir, "chapter_summaries.md"),
-        [
-          "# Chapter Summaries",
-          "",
-          "| chapter | title | characters | events | stateChanges | hookActivity | mood | chapterType |",
-          "| --- | --- | --- | --- | --- | --- | --- | --- |",
-          "| 10 | Locked Gate | Lin Yue | Lin Yue chooses the mentor line over the guild line | Mentor conflict takes priority | mentor-oath advanced | focused | decision |",
-          "",
-        ].join("\n"),
-        "utf-8",
-      ),
-    ]);
-
-    const planner = new PlannerAgent({
-      client: {} as ConstructorParameters<typeof PlannerAgent>[0]["client"],
-      model: "test-model",
-      projectRoot: root,
-      bookId: book.id,
-    });
-
-    const result = await planner.planChapter({
-      book,
-      bookDir,
-      chapterNumber: 11,
-      externalContext: "Bring the focus back to the mentor oath conflict with Lin Yue.",
-    });
-
-    const intentMarkdown = await readFile(result.runtimePath, "utf-8");
-    expect(intentMarkdown).toContain("| hook_id | start_chapter | type | status | last_advanced | expected_payoff | payoff_timing | notes |");
-    expect(intentMarkdown).toContain("| chapter | title | characters | events | stateChanges | hookActivity | mood | chapterType |");
-    expect(intentMarkdown).not.toContain("| hook_id | 起始章节 | 类型 | 状态 | 最近推进 | 预期回收 | 备注 |");
-    expect(intentMarkdown).not.toContain("| 章节 | 标题 | 出场人物 | 关键事件 | 状态变化 | 伏笔动态 | 情绪基调 | 章节类型 |");
-  });
-
-  it("derives structured current_focus markdown into goal, avoids, and style emphasis", async () => {
-    await Promise.all([
-      writeFile(
-        join(storyDir, "current_focus.md"),
-        [
-          "# Current Focus",
-          "",
-          "## Active Focus",
-          "",
-          "- Bring the focus back to Lin Yue's private confrontation with the mentor debt.",
-          "- Keep the chapter centered on a missing record, not a whole-conspiracy overview.",
-          "- Surface one concrete evidence trail the next chapter can pursue.",
-          "",
-          "## Avoid",
-          "",
-          "- Do not turn this chapter into a citywide survey of every faction.",
-          "- Do not use summary-heavy moralizing paragraphs.",
-          "",
-        ].join("\n"),
-        "utf-8",
-      ),
-      writeFile(
-        join(storyDir, "story_bible.md"),
-        [
-          "# Story Bible",
-          "",
-          "- --",
-          "- The jade seal cannot be destroyed.",
-          "",
-        ].join("\n"),
-        "utf-8",
-      ),
-      writeFile(
-        join(storyDir, "volume_outline.md"),
-        "# Volume Outline\n",
-        "utf-8",
-      ),
-    ]);
-
-    const planner = new PlannerAgent({
-      client: {} as ConstructorParameters<typeof PlannerAgent>[0]["client"],
-      model: "test-model",
-      projectRoot: root,
-      bookId: book.id,
-    });
-
-    const result = await planner.planChapter({
-      book,
-      bookDir,
-      chapterNumber: 2,
-    });
-
-    // goal is now LLM-derived from brief; verify other code-derived fields
-    expect(result.brief).toBeDefined();
-    expect(result.intent.mustAvoid).toEqual(expect.arrayContaining([
-      "Do not turn this chapter into a citywide survey of every faction.",
-      "Do not use summary-heavy moralizing paragraphs.",
-    ]));
-    expect(result.intent.mustAvoid).not.toContain(
-      "Keep the chapter centered on a missing record, not a whole-conspiracy overview.",
-    );
-    expect(result.intent.styleEmphasis).toEqual(expect.arrayContaining([
-      "Bring the focus back to Lin Yue's private confrontation with the mentor debt.",
-      "Surface one concrete evidence trail the next chapter can pursue.",
-    ]));
-    expect(result.intent.mustKeep).not.toContain("--");
-  });
-
-  it("emits hook agenda into chapter intent and runtime markdown", async () => {
-    const stateDir = join(storyDir, "state");
-    await mkdir(stateDir, { recursive: true });
-
-    await Promise.all([
-      writeFile(
-        join(stateDir, "manifest.json"),
-        JSON.stringify({
-          schemaVersion: 2,
-          language: "en",
-          lastAppliedChapter: 25,
-          projectionVersion: 1,
-          migrationWarnings: [],
-        }, null, 2),
-        "utf-8",
-      ),
-      writeFile(
-        join(stateDir, "current_state.json"),
-        JSON.stringify({
-          chapter: 25,
-          facts: [],
-        }, null, 2),
-        "utf-8",
-      ),
-      writeFile(
-        join(stateDir, "chapter_summaries.json"),
-        JSON.stringify({
-          rows: [],
-        }, null, 2),
-        "utf-8",
-      ),
-      writeFile(
-        join(stateDir, "hooks.json"),
-        JSON.stringify({
-          hooks: [
-            {
-              hookId: "recent-route",
-              startChapter: 23,
-              type: "route",
-              status: "open",
-              lastAdvancedChapter: 25,
-              expectedPayoff: "Recent route payoff",
-              notes: "Recent route remains active.",
-            },
-            {
-              hookId: "ready-payoff",
-              startChapter: 12,
-              type: "mystery",
-              status: "progressing",
-              lastAdvancedChapter: 24,
-              expectedPayoff: "Reveal the hidden room mastermind",
-              notes: "The chapter is close to the reveal point.",
-            },
-            {
-              hookId: "stale-debt",
-              startChapter: 3,
-              type: "relationship",
-              status: "open",
-              lastAdvancedChapter: 8,
-              expectedPayoff: "Mentor debt payoff",
-              notes: "Long-stale but still unresolved.",
-            },
-          ],
-        }, null, 2),
-        "utf-8",
-      ),
-    ]);
-
-    book = {
-      ...book,
-      genre: "other",
-      language: "en",
-    };
-
-    const planner = new PlannerAgent({
-      client: {} as ConstructorParameters<typeof PlannerAgent>[0]["client"],
-      model: "test-model",
-      projectRoot: root,
-      bookId: book.id,
-    });
-
-    const result = await planner.planChapter({
-      book,
-      bookDir,
-      chapterNumber: 26,
-      externalContext: "Keep the chapter on the mainline debt conflict.",
-    });
-
-    // Lifecycle scheduling: ready-payoff (progressing, mid-arc, overdue) → eligibleResolve;
-    // stale-debt (dormant, stale+overdue) → staleDebt; recent-route (remainder) → mustAdvance.
-    expect(result.intent.hookAgenda.eligibleResolve).toEqual(["ready-payoff"]);
-    expect(result.intent.hookAgenda.staleDebt).toEqual(["stale-debt"]);
-    expect(result.intent.hookAgenda.mustAdvance).toEqual(["recent-route"]);
-    expect(result.intent.hookAgenda.avoidNewHookFamilies).toContain("relationship");
-    expect(result.intent.hookAgenda.pressureMap).toEqual([]);
-
-    const intentMarkdown = await readFile(result.runtimePath, "utf-8");
-    expect(intentMarkdown).toContain("## Hook Agenda");
-    expect(intentMarkdown).toContain("ready-payoff");
-    expect(intentMarkdown).toContain("stale-debt");
-  });
-
-  it("builds stale debt agenda from broader active hooks than the retrieval subset", async () => {
-    const stateDir = join(storyDir, "state");
-    await mkdir(stateDir, { recursive: true });
-
-    await Promise.all([
-      writeFile(
-        join(stateDir, "manifest.json"),
-        JSON.stringify({
-          schemaVersion: 2,
-          language: "en",
-          lastAppliedChapter: 25,
-          projectionVersion: 1,
-          migrationWarnings: [],
-        }, null, 2),
-        "utf-8",
-      ),
-      writeFile(
-        join(stateDir, "current_state.json"),
-        JSON.stringify({
-          chapter: 25,
-          facts: [],
-        }, null, 2),
-        "utf-8",
-      ),
-      writeFile(
-        join(stateDir, "chapter_summaries.json"),
-        JSON.stringify({
-          rows: [],
-        }, null, 2),
-        "utf-8",
-      ),
-      writeFile(
-        join(stateDir, "hooks.json"),
-        JSON.stringify({
-          hooks: [
-            {
-              hookId: "recent-route",
-              startChapter: 23,
-              type: "route",
-              status: "open",
-              lastAdvancedChapter: 25,
-              expectedPayoff: "Recent route payoff",
-              notes: "Keep the route central.",
-            },
-            {
-              hookId: "recent-guild",
-              startChapter: 22,
-              type: "politics",
-              status: "progressing",
-              lastAdvancedChapter: 24,
-              expectedPayoff: "Guild pressure payoff",
-              notes: "Guild pressure remains active.",
-            },
-            {
-              hookId: "recent-token",
-              startChapter: 21,
-              type: "artifact",
-              status: "open",
-              lastAdvancedChapter: 23,
-              expectedPayoff: "Token route payoff",
-              notes: "Token route remains active.",
-            },
-            {
-              hookId: "stale-omega",
-              startChapter: 3,
-              type: "relationship",
-              status: "open",
-              lastAdvancedChapter: 8,
-              expectedPayoff: "Old debt payoff",
-              notes: "Dormant unresolved line.",
-            },
-            {
-              hookId: "stale-sable",
-              startChapter: 4,
-              type: "mystery",
-              status: "open",
-              lastAdvancedChapter: 9,
-              expectedPayoff: "Archive payoff",
-              notes: "Another dormant unresolved line.",
-            },
-          ],
-        }, null, 2),
-        "utf-8",
-      ),
-    ]);
-
-    book = {
-      ...book,
-      genre: "other",
-      language: "en",
-    };
-
-    const planner = new PlannerAgent({
-      client: {} as ConstructorParameters<typeof PlannerAgent>[0]["client"],
-      model: "test-model",
-      projectRoot: root,
-      bookId: book.id,
-    });
-
-    const result = await planner.planChapter({
-      book,
-      bookDir,
-      chapterNumber: 26,
-      externalContext: "Keep the chapter on the route pressure.",
-    });
-
-    // Lifecycle scheduling: stale-omega and stale-sable are stale+overdue → staleDebt;
-    // recent-token and recent-route are not stale/overdue → mustAdvance (by advancePressure).
-    expect(result.intent.hookAgenda.staleDebt).toEqual(["stale-omega", "stale-sable"]);
-    expect(result.intent.hookAgenda.mustAdvance).toEqual(["recent-token", "recent-route"]);
-    expect(result.intent.hookAgenda.avoidNewHookFamilies).toEqual(expect.arrayContaining([
-      "relationship",
-      "mystery",
-    ]));
-    expect(result.intent.hookAgenda.pressureMap).toEqual([]);
-  });
-
-  it("renders hook budget from total active hooks instead of the selected hook snapshot", async () => {
-    const stateDir = join(storyDir, "state");
-    await mkdir(stateDir, { recursive: true });
-
-    const hooks = Array.from({ length: 12 }, (_, index) => ({
-      hookId: `hook-${index + 1}`,
-      startChapter: index + 1,
-      type: index < 6 ? "route" : "mystery",
-      status: "open",
-      lastAdvancedChapter: index < 6 ? 25 - index : 12 - index,
-      expectedPayoff: index < 6 ? "Route debt payoff" : "Dormant mystery payoff",
-      notes: index < 6
-        ? `Route pressure thread ${index + 1} stays relevant.`
-        : `Dormant thread ${index + 1} should not be selected into the primary context.`,
-    }));
-
-    await Promise.all([
-      writeFile(
-        join(stateDir, "manifest.json"),
-        JSON.stringify({
-          schemaVersion: 2,
-          language: "en",
-          lastAppliedChapter: 25,
-          projectionVersion: 1,
-          migrationWarnings: [],
-        }, null, 2),
-        "utf-8",
-      ),
-      writeFile(
-        join(stateDir, "current_state.json"),
-        JSON.stringify({
-          chapter: 25,
-          facts: [],
-        }, null, 2),
-        "utf-8",
-      ),
-      writeFile(
-        join(stateDir, "chapter_summaries.json"),
-        JSON.stringify({
-          rows: [],
-        }, null, 2),
-        "utf-8",
-      ),
-      writeFile(
-        join(stateDir, "hooks.json"),
-        JSON.stringify({ hooks }, null, 2),
-        "utf-8",
-      ),
-    ]);
-
-    book = {
-      ...book,
-      genre: "other",
-      language: "en",
-      targetChapters: 40,
-    };
-
-    const planner = new PlannerAgent({
-      client: {} as ConstructorParameters<typeof PlannerAgent>[0]["client"],
-      model: "test-model",
-      projectRoot: root,
-      bookId: book.id,
-    });
-
-    const result = await planner.planChapter({
-      book,
-      bookDir,
-      chapterNumber: 26,
-      externalContext: "Keep the chapter on the route pressure.",
-    });
-
-    const intentMarkdown = await readFile(result.runtimePath, "utf-8");
-    expect(intentMarkdown).toContain("12 active hooks");
-    expect(intentMarkdown).not.toContain("8 active hooks");
-  });
-
-  it("matches multi-line 章节范围 format and distributes beats across chapters", async () => {
-    await writeFile(
-      join(storyDir, "volume_outline.md"),
-      [
-        "## 已有章节部分",
-        "",
-        "### 卷一:开端",
-        "- **章节范围**:1-3章",
-        "- **核心冲突**:初始设定",
-        "- **关键转折**:",
-        "  1. 主角登场并发现异常",
-        "  2. 遭遇第一次危机",
-        "  3. 获得关键线索",
-        "",
-        "## 后续预测部分",
-        "",
-        "### 卷二:深入",
-        "- **章节范围**:4-8章",
-        "- **核心冲突**:深入调查",
-        "- **关键转折**:",
-        "  1. 抵达新地点,发现物证",
-        "  2. 与对手首次正面交锋",
-        "  3. 盟友背叛,局势逆转",
-        "  4. 主角被迫转入地下",
-        "  5. 发现幕后真相的第一条线",
-        "",
-      ].join("\n"),
-      "utf-8",
-    );
-
-    const planner = new PlannerAgent({
-      client: {} as ConstructorParameters<typeof PlannerAgent>[0]["client"],
-      model: "test-model",
-      projectRoot: root,
-      bookId: book.id,
-    });
-
-    // Chapter 4 should get beat 0 (first beat of 卷二): "抵达新地点"
-    const result4 = await planner.planChapter({
-      book,
-      bookDir,
-      chapterNumber: 4,
-    });
-    expect(result4.intent.outlineNode).toContain("抵达新地点");
-
-    // Chapter 6 should get beat 2 (third beat): "盟友背叛"
-    const result6 = await planner.planChapter({
-      book,
-      bookDir,
-      chapterNumber: 6,
-    });
-    expect(result6.intent.outlineNode).toContain("盟友背叛");
-
-    // Chapter 8 should get beat 4 (fifth beat): "幕后真相"
-    const result8 = await planner.planChapter({
-      book,
-      bookDir,
-      chapterNumber: 8,
-    });
-    expect(result8.intent.outlineNode).toContain("幕后真相");
-
-    // Chapter 5 should get beat 1: "正面交锋"
-    const result5 = await planner.planChapter({
-      book,
-      bookDir,
-      chapterNumber: 5,
-    });
-    expect(result5.intent.outlineNode).toContain("正面交锋");
-  });
-
-  it("uses the LLM chapter brief when planner output is valid", async () => {
-    const planner = new PlannerAgent({
-      client: {} as ConstructorParameters<typeof PlannerAgent>[0]["client"],
-      model: "test-model",
-      projectRoot: root,
-      bookId: book.id,
-    });
-
-    vi.spyOn(planner as unknown as { chat: (...args: unknown[]) => Promise<{ content: string }> }, "chat")
-      .mockResolvedValue({
-        content: JSON.stringify({
-          chapter: 3,
-          goal: "Force the warehouse confrontation before the guild escapes.",
-          chapterType: "confrontation",
-          isGoldenOpening: true,
-          beatOutline: [
-            { phase: "opening", instruction: "Open inside the warehouse argument." },
-            { phase: "development", instruction: "Push the unsigned transfer sheet into the scene." },
-            { phase: "hook", instruction: "End with the escape route changing hands." },
-          ],
-          hookPlan: [
-            {
-              hookId: "H019",
-              movement: "advance",
-              targetEffect: "Turn the mentor debt into an immediate cost.",
-            },
-          ],
-          dormantReason: "Hold the canal line until the next chapter.",
-          propsAndSetting: ["warehouse transfer sheet", "wet dock", "A Sheng"],
-        }),
-      });
-
-    const result = await planner.planChapter({
-      book,
-      bookDir,
-      chapterNumber: 3,
-    });
-
-    expect(result.brief.chapterType).toBe("confrontation");
-    expect(result.intent.goal).toContain("warehouse confrontation");
-    expect(result.intent.arcDirective).toContain("opening: Open inside the warehouse argument.");
-    expect(result.intent.sceneDirective).toContain("warehouse transfer sheet");
-    await expect(readFile(result.runtimePath, "utf-8")).resolves.toContain("## Chapter Brief");
-  });
-
-  it("throws when the LLM chapter brief is invalid", async () => {
-    const planner = new PlannerAgent({
-      client: {} as ConstructorParameters<typeof PlannerAgent>[0]["client"],
-      model: "test-model",
-      projectRoot: root,
-      bookId: book.id,
-    });
-
-    vi.spyOn(planner as unknown as { chat: (...args: unknown[]) => Promise<{ content: string }> }, "chat")
-      .mockResolvedValue({
-        content: "{\"bad\":true}",
-      });
-
-    await expect(planner.planChapter({
-      book,
-      bookDir,
-      chapterNumber: 3,
-    })).rejects.toThrow("Planner LLM returned invalid ChapterBrief JSON");
-  });
-});

+ 9 - 18
packages/core/src/__tests__/writer.test.ts

@@ -901,14 +901,12 @@ describe("WriterAgent", () => {
         },
         },
         bookDir,
         bookDir,
         chapterNumber: 4,
         chapterNumber: 4,
-        chapterBrief: {
+        chapterMemo: {
           chapter: 4,
           chapter: 4,
           goal: "Force Mara back toward the ledger trail.",
           goal: "Force Mara back toward the ledger trail.",
-          chapterType: "confrontation",
           isGoldenOpening: false,
           isGoldenOpening: false,
-          beatOutline: [{ phase: "opening" as const, instruction: "Open with tension at the archive window." }],
-          hookPlan: [{ hookId: "ledger-fragment", movement: "advance" as const, targetEffect: "Mara is forced to confront the ledger." }],
-          propsAndSetting: ["archive window"],
+          body: "",
+          hookRefs: ["ledger-fragment"],
         },
         },
         contextPackage: {
         contextPackage: {
           chapter: 4,
           chapter: 4,
@@ -1033,14 +1031,12 @@ describe("WriterAgent", () => {
         },
         },
         bookDir,
         bookDir,
         chapterNumber: 4,
         chapterNumber: 4,
-        chapterBrief: {
+        chapterMemo: {
           chapter: 4,
           chapter: 4,
           goal: "Push Mara back toward the archive ledger.",
           goal: "Push Mara back toward the archive ledger.",
-          chapterType: "confrontation",
           isGoldenOpening: false,
           isGoldenOpening: false,
-          beatOutline: [{ phase: "opening" as const, instruction: "Open with tension at the archive." }],
-          hookPlan: [{ hookId: "ledger-fragment", movement: "advance" as const, targetEffect: "Mara is forced to confront the ledger." }],
-          propsAndSetting: ["archive"],
+          body: "",
+          hookRefs: ["ledger-fragment"],
         },
         },
         contextPackage: {
         contextPackage: {
           chapter: 4,
           chapter: 4,
@@ -1184,17 +1180,12 @@ describe("WriterAgent", () => {
         },
         },
         bookDir,
         bookDir,
         chapterNumber: 4,
         chapterNumber: 4,
-        chapterBrief: {
+        chapterMemo: {
           chapter: 4,
           chapter: 4,
           goal: "Push Mara back toward the archive ledger.",
           goal: "Push Mara back toward the archive ledger.",
-          chapterType: "confrontation",
           isGoldenOpening: false,
           isGoldenOpening: false,
-          beatOutline: [{ phase: "opening" as const, instruction: "本章要做的是推进 ledger-fragment tension at the archive." }],
-          hookPlan: [
-            { hookId: "mentor-oath", movement: "advance" as const, targetEffect: "Advance the oath debt with H001 ledger-fragment callback." },
-            { hookId: "ledger-fragment", movement: "partial-payoff" as const, targetEffect: "Partially resolve stale-ledger." },
-          ],
-          propsAndSetting: ["archive"],
+          body: "本章要做的是推进 ledger-fragment tension at the archive.",
+          hookRefs: ["mentor-oath", "ledger-fragment"],
         },
         },
         contextPackage: {
         contextPackage: {
           chapter: 4,
           chapter: 4,

+ 24 - 42
packages/core/src/agents/composer.ts

@@ -95,17 +95,22 @@ async function collectSelectedContext(
   language: "zh" | "en",
   language: "zh" | "en",
 ): Promise<ContextPackage["selectedContext"]> {
 ): Promise<ContextPackage["selectedContext"]> {
     const retrievalHints = deriveRetrievalHints(plan);
     const retrievalHints = deriveRetrievalHints(plan);
-    const chapterBriefEntry = [{
-      source: "runtime/chapter_brief",
-      reason: "Carry the planner's concrete chapter execution brief directly into governed writing.",
-      excerpt: [
-        `type=${plan.brief.chapterType}`,
-        ...plan.brief.beatOutline.map((beat) => `${beat.phase}: ${beat.instruction}`),
-        ...(plan.brief.propsAndSetting.length > 0
-          ? [`props=${plan.brief.propsAndSetting.join(", ")}`]
-          : []),
-      ].join(" | "),
-    }];
+    const memoBodyExcerpt = plan.memo.body.trim();
+    const chapterMemoEntry = memoBodyExcerpt.length > 0
+      ? [{
+          source: "runtime/chapter_memo",
+          reason: "Carry the planner's chapter memo into governed writing.",
+          excerpt: [
+            `goal=${plan.memo.goal}`,
+            plan.memo.isGoldenOpening ? "golden-opening=true" : undefined,
+            memoBodyExcerpt,
+          ].filter(Boolean).join(" | "),
+        }]
+      : [{
+          source: "runtime/chapter_memo",
+          reason: "Carry the planner's chapter memo into governed writing.",
+          excerpt: `goal=${plan.memo.goal}`,
+        }];
 
 
     const entries = await Promise.all([
     const entries = await Promise.all([
       maybeContextSource(storyDir, "current_focus.md", "Current task focus for this chapter."),
       maybeContextSource(storyDir, "current_focus.md", "Current task focus for this chapter."),
@@ -145,12 +150,11 @@ async function collectSelectedContext(
     ]);
     ]);
     const trailEntries = await buildRecentChapterTrailEntries(storyDir, plan.intent.chapter);
     const trailEntries = await buildRecentChapterTrailEntries(storyDir, plan.intent.chapter);
 
 
-    const planningAnchor = plan.intent.conflicts.length > 0 ? undefined : plan.intent.outlineNode;
     const memorySelection = await retrieveMemorySelection({
     const memorySelection = await retrieveMemorySelection({
       bookDir: dirname(storyDir),
       bookDir: dirname(storyDir),
       chapterNumber: plan.intent.chapter,
       chapterNumber: plan.intent.chapter,
       goal: plan.intent.goal,
       goal: plan.intent.goal,
-      outlineNode: planningAnchor,
+      outlineNode: plan.intent.outlineNode,
       mustKeep: retrievalHints,
       mustKeep: retrievalHints,
     });
     });
     const hookDebtEntries = await buildHookDebtEntries(
     const hookDebtEntries = await buildHookDebtEntries(
@@ -186,7 +190,7 @@ async function collectSelectedContext(
     }));
     }));
 
 
     return [
     return [
-      ...chapterBriefEntry,
+      ...chapterMemoEntry,
       ...entries.filter((entry): entry is NonNullable<typeof entry> => entry !== null),
       ...entries.filter((entry): entry is NonNullable<typeof entry> => entry !== null),
       ...trailEntries,
       ...trailEntries,
       ...hookDebtEntries,
       ...hookDebtEntries,
@@ -199,10 +203,10 @@ async function collectSelectedContext(
 
 
 function deriveRetrievalHints(plan: PlanChapterOutput): string[] {
 function deriveRetrievalHints(plan: PlanChapterOutput): string[] {
   return [
   return [
-    ...plan.brief.propsAndSetting,
-    ...plan.brief.hookPlan.flatMap((entry) => [entry.hookId, entry.targetEffect]),
-    plan.brief.goal,
-  ].filter(Boolean);
+    plan.intent.goal,
+    plan.intent.outlineNode,
+    ...plan.memo.hookRefs,
+  ].filter((value): value is string => Boolean(value));
 }
 }
 
 
 async function buildRecentChapterTrailEntries(
 async function buildRecentChapterTrailEntries(
@@ -311,15 +315,7 @@ async function buildHookDebtEntries(
     }>,
     }>,
   language: "zh" | "en",
   language: "zh" | "en",
 ): Promise<ContextPackage["selectedContext"]> {
 ): Promise<ContextPackage["selectedContext"]> {
-    const targetHookIds = [
-      ...new Set([
-        ...plan.brief.hookPlan.map((entry) => entry.hookId),
-        ...plan.intent.hookAgenda.pressureMap.map((entry) => entry.hookId),
-        ...plan.intent.hookAgenda.eligibleResolve,
-        ...plan.intent.hookAgenda.mustAdvance,
-        ...plan.intent.hookAgenda.staleDebt,
-      ]),
-    ];
+    const targetHookIds = [...new Set(plan.memo.hookRefs)];
     if (targetHookIds.length === 0) {
     if (targetHookIds.length === 0) {
       return [];
       return [];
     }
     }
@@ -336,7 +332,7 @@ async function buildHookDebtEntries(
 
 
       const seedSummary = findHookSummary(summaries, hook.hookId, hook.startChapter, "seed");
       const seedSummary = findHookSummary(summaries, hook.hookId, hook.startChapter, "seed");
       const latestSummary = findHookSummary(summaries, hook.hookId, hook.lastAdvancedChapter, "latest");
       const latestSummary = findHookSummary(summaries, hook.hookId, hook.lastAdvancedChapter, "latest");
-      const role = describeHookAgendaRole(plan, hook.hookId, language);
+      const role = language === "en" ? "memo-referenced debt" : "备忘引用旧债";
       const promise = hook.expectedPayoff || (language === "en" ? "(unspecified)" : "(未写明)");
       const promise = hook.expectedPayoff || (language === "en" ? "(unspecified)" : "(未写明)");
       const seedBeat = seedSummary
       const seedBeat = seedSummary
         ? renderHookDebtBeat(seedSummary)
         ? renderHookDebtBeat(seedSummary)
@@ -413,20 +409,6 @@ async function readFileOrDefault(path: string): Promise<string> {
   }
   }
 }
 }
 
 
-function describeHookAgendaRole(
-  plan: PlanChapterOutput,
-  hookId: string,
-  language: "zh" | "en",
-): string {
-  if (plan.intent.hookAgenda.eligibleResolve.includes(hookId)) {
-    return language === "en" ? "payoff-ready debt" : "可兑现旧债";
-  }
-  if (plan.intent.hookAgenda.staleDebt.includes(hookId)) {
-    return language === "en" ? "high-pressure debt" : "高压旧债";
-  }
-  return language === "en" ? "mainline debt" : "主要旧债";
-}
-
 function findHookSummary(
 function findHookSummary(
   summaries: ReadonlyArray<ReturnType<typeof parseChapterSummariesMarkdown>[number]>,
   summaries: ReadonlyArray<ReturnType<typeof parseChapterSummariesMarkdown>[number]>,
   hookId: string,
   hookId: string,

+ 11 - 245
packages/core/src/agents/planner-prompts.ts

@@ -1,5 +1,4 @@
 import type { PlanningMaterials } from "../utils/planning-materials.js";
 import type { PlanningMaterials } from "../utils/planning-materials.js";
-import type { ObjectiveCycleAnalysis } from "../utils/chapter-cadence.js";
 
 
 export interface PlannerPromptInput {
 export interface PlannerPromptInput {
   readonly chapterNumber: number;
   readonly chapterNumber: number;
@@ -7,210 +6,26 @@ export interface PlannerPromptInput {
   readonly genreName: string;
   readonly genreName: string;
   readonly language: "zh" | "en";
   readonly language: "zh" | "en";
   readonly materials: PlanningMaterials;
   readonly materials: PlanningMaterials;
-  readonly cycleAnalysis?: ObjectiveCycleAnalysis;
 }
 }
 
 
+/**
+ * Phase 1 transitional planner prompts.
+ *
+ * The previous prompts instructed the LLM to output ChapterBrief JSON with
+ * beat outlines, cycle phases, and hook agendas. Phase 3 (new.txt) will
+ * replace these with a seven-section memo prompt. For now the planner runs
+ * deterministically without invoking the LLM, so these prompts are unused —
+ * kept only as stable exports for external tooling and future Phase 3 work.
+ */
 export function buildPlannerSystemPrompt(language: "zh" | "en"): string {
 export function buildPlannerSystemPrompt(language: "zh" | "en"): string {
   if (language === "en") {
   if (language === "en") {
-    return EN_SYSTEM_PROMPT;
+    return "You are the InkOS chapter planner. Produce a structured chapter memo.";
   }
   }
-  return ZH_SYSTEM_PROMPT;
-}
-
-const ZH_SYSTEM_PROMPT = `你是 InkOS 的章节规划器。
-你的任务不是把”这一章该发生什么”翻译成 JSON,
-而是决定这一章写”对”——让它成为一个合格的微型故事。
-
-## 核心原则(是世界观,不是检查表)
-
-### 关于故事本身
-1. 每章是微型故事:有目标、有阻力、有代价、有新状态。不写流水账,不写纯铺垫。
-2. 从冲突现场开始:角色进入本章时已经在应对正在发生的事。不从铺垫、日常早晨或心理独白开始。
-3. beat 是画面不是标签:用具体动作或感官,不用抽象情绪词。
-
-### 关于冲突
-4. 冲突有三层:外部(人 vs 人 / 势力 / 环境)、内部(人 vs 自我)、理念(价值观碰撞)。本章冲突至少落在一层。
-5. 冲突必须有起因、有后果。拒绝三种无意义冲突:
-   - 无缘无故(角色没理由)
-   - 重复(上章刚打过同类)
-   - 无后果(打完没人失去、没人得到、没人改变)
-6. 冲突不要平推。同级冲突不解决第二次;要么升级,要么换对象,要么换冲突面。
-
-### 关于 hook 和信息
-7. hook 是织网不是堆彩蛋:这一章只动 1-2 根线。开一根有理由,收一根有前情。其他线按兵不动,用 dormantReason 写出取舍。
-8. 渐进披露:读者此刻需要知道的才写。不要一次把底牌翻完。
-
-### 关于配角和情绪
-9. 配角有目的:推动剧情,不是充场面。B 面只在需要时露。
-10. 爽点有代价,刀子有余温。
-11. 节奏由目标周期驱动,不由情绪档位决定。每 3-5 章是一个小周期:
-    蓄压(铺新阻力、新信息)→ 升级(冲突加码、缺口扩大)→ 爆发(爽点落地、承诺兑现、释放超预期)→ 后效(写改变:关系变了、地位变了、代价显现了)。
-    不要机械轮换——如果故事需要连续蓄压就连续蓄压,但 5 章内必须有一次爆发。
-
-## 开场打开方式(工具箱,非强制)
-
-可用下面任一种,也可以创造新的:
-悬念式 / 冲突式 / 信息差式 / 倒叙式 / 承接式
-
-## 你不做什么
-
-- 不重复 volume_outline 里已经写过的事件
-- 不发号施令(”本章必须”、”要让读者”)
-- 不写元语言(”第一章”、”前几章”、”小爽点”、”伏笔”、”爽点”、”黄金三章”、”金手指”、”本章”、”这一章”)
-- 不在 beat 里用抽象词(决心、勇敢、冷峻、震惊)
-- 不列 hook 充数;这章只动 1 根就只写 1 根
-- 不追求任何固定配比
-
-## 示例(正反参照)
-
-### goal
-❌ “建立主角的复仇决心”
-❌ “让读者感受到压迫感”
-✅ “用三年寿命换妹妹一条命,必须今晚决定”
-✅ “从掌门手里偷回母亲的骨灰坛”
-
-### beat.instruction
-❌ “主角决心坚定地进入冲突”
-❌ “气氛变得紧张而压抑”
-✅ “主角把母亲的药瓶踩碎在玄关,转身出门”
-✅ “对话中断——外面的脚步声走到门口停住了”
-
-### 开场
-❌ 清晨,林天从梦中醒来,回想起昨天发生的事
-❌ 叙事介绍三大势力的对立关系作为背景
-✅ 从碎石下爬出,听见脚步声逼近
-✅ 对话进行到一半:”所以那东西根本就不在青云山?”
-
-### hookPlan
-❌ 5 根 hook 全部 movement: “advance”
-✅ 1 根 partial-payoff,dormantReason: 其他线在等读者消化上波震撼
-
-## 输出
-
-纯 JSON,符合 ChapterBrief schema。不输出任何解释。`;
-
-const EN_SYSTEM_PROMPT = `You are the InkOS chapter planner.
-Your job is not to translate “what should happen” into JSON,
-but to decide that this chapter is written *right* — making it a proper micro-story.
-
-## Core Principles (worldview, not checklist)
-
-### Story
-1. Each chapter is a micro-story: goal, obstacle, cost, new state. No filler, no pure setup.
-2. Start from the conflict scene: the character is already reacting to something in progress. No setup, no waking up, no internal monologue openings.
-3. Beats are pictures, not labels: use concrete actions or sensory detail, not abstract emotions.
-
-### Conflict
-4. Conflict has three layers: external (person vs person / faction / environment), internal (person vs self), ideological (value clash). This chapter's conflict must land on at least one layer.
-5. Conflict must have cause and consequence. Reject three types of meaningless conflict:
-   - Unprovoked (character has no reason)
-   - Repetitive (same type as the previous chapter)
-   - Inconsequential (no one loses, gains, or changes)
-6. Don't repeat the same conflict level. If the protagonist already solved one at this tier, escalate, change the target, or switch the conflict dimension.
-
-### Hooks and information
-7. Hooks are weaving, not dumping: move only 1-2 lines this chapter. Open one with reason, close one with setup. Others stay dormant — explain why in dormantReason.
-8. Gradual disclosure: only tell what the reader needs now. Don't flip all worldbuilding cards at once.
-
-### Supporting cast and emotion
-9. Supporting characters have purpose: they drive plot, not fill scenes. Show their B-side only when needed.
-10. Payoffs have costs; knife-twists leave warmth.
-11. Rhythm is driven by objective cycles, not mood buckets. Every 3-5 chapters form a mini-arc:
-    build-up (new obstacles, new info) → escalation (raise stakes, widen gaps) → climax (deliver on promises, exceed expectations) → aftermath (write change: relationships shifted, status changed, costs visible).
-    Don't rotate mechanically — if the story needs consecutive pressure-building, do it. But there must be at least one payoff within every 5 chapters.
-
-## Opening types (toolbox, not mandatory)
-
-Use any of these, or invent your own:
-Suspense / Conflict-in-progress / Information-gap / In-medias-res / Continuation
-
-## What you do NOT do
-
-- Don't repeat events already in volume_outline
-- Don't issue commands (“this chapter must”, “make the reader”)
-- Don't emit meta-language (“Chapter 1”, “earlier chapters”, “mini payoff”, “foreshadowing”, “cool moment”, “golden opening”, “cheat code”, “this chapter”)
-- Don't use abstract words in beats (determination, bravery, cold, shock)
-- Don't pad hookPlan; if only 1 hook moves, write only 1
-- Don't aim for any fixed ratio
-
-## Examples (positive / negative)
-
-### goal
-❌ “Establish the protagonist's resolve for revenge”
-❌ “Let the reader feel the oppression”
-✅ “Trade three years of life for his sister's survival — must decide tonight”
-✅ “Steal back mother's urn from the sect leader”
-
-### beat.instruction
-❌ “Protagonist enters the conflict with determination”
-❌ “The atmosphere becomes tense and oppressive”
-✅ “Protagonist crushes mother's pill bottle on the doorstep, turns and walks out”
-✅ “Conversation cuts off — footsteps stop right outside the door”
-
-### opening
-❌ Dawn. Lin Tian wakes from a dream, recalling yesterday's events.
-❌ Narrative introduces the three-faction standoff as background.
-✅ Crawl out from under rubble; footsteps closing in.
-✅ Mid-conversation: “So that thing was never on Qingyun Mountain?”
-
-### hookPlan
-❌ 5 hooks all with movement: “advance”
-✅ 1 partial-payoff, dormantReason: other lines wait while the reader absorbs the last shock
-
-## Output
-
-Pure JSON conforming to ChapterBrief schema. No explanations.`;
-
-function renderRecentSummaries(materials: PlanningMaterials): string {
-  if (materials.recentSummaries.length === 0) {
-    return "(none)";
-  }
-
-  return materials.recentSummaries
-    .map((summary) => [
-      `- Ch${summary.chapter}: ${summary.title || "(untitled)"}`,
-      summary.events,
-      summary.stateChanges,
-      summary.hookActivity,
-      summary.chapterType,
-    ].filter(Boolean).join(" | "))
-    .join("\n");
-}
-
-function renderActiveHooks(materials: PlanningMaterials): string {
-  if (materials.activeHooks.length === 0) {
-    return "(none)";
-  }
-
-  return materials.activeHooks
-    .slice(0, 6)
-    .map((hook) => `- ${hook.hookId} | ${hook.type} | ${hook.expectedPayoff} | last=${hook.lastAdvancedChapter} | timing=${hook.payoffTiming ?? "unspecified"}`)
-    .join("\n");
-}
-
-function renderCycleContext(
-  cycleAnalysis: ObjectiveCycleAnalysis | undefined,
-  language: "zh" | "en",
-): string {
-  if (!cycleAnalysis) return "(not available)";
-  const isEn = language === "en";
-  const phaseLabel = isEn ? "Suggested Phase" : "建议阶段";
-  const reasonLabel = isEn ? "Reasoning" : "依据";
-  const debtLabel = isEn ? "Hook Debt" : "伏笔债务";
-  const payoffLabel = isEn ? "Payoff Due" : "需要兑现";
-  const streakLabel = isEn ? "Chapters in Current Phase" : "当前阶段连续章数";
-  return [
-    `- ${phaseLabel}: ${cycleAnalysis.phase}`,
-    `- ${reasonLabel}: ${cycleAnalysis.reasoning}`,
-    `- ${streakLabel}: ${cycleAnalysis.chaptersInCurrentPhase}`,
-    `- ${payoffLabel}: ${cycleAnalysis.payoffDue ? (isEn ? "yes" : "是") : (isEn ? "no" : "否")}`,
-    `- ${debtLabel}: ${cycleAnalysis.hookDebtLevel}`,
-  ].join("\n");
+  return "你是 InkOS 的章节规划器,产出结构化的章节备忘。";
 }
 }
 
 
 export function buildPlannerUserPrompt(input: PlannerPromptInput): string {
 export function buildPlannerUserPrompt(input: PlannerPromptInput): string {
   const { materials } = input;
   const { materials } = input;
-
   return [
   return [
     `## Chapter`,
     `## Chapter`,
     `${input.chapterNumber}/${input.targetChapters}`,
     `${input.chapterNumber}/${input.targetChapters}`,
@@ -233,54 +48,5 @@ export function buildPlannerUserPrompt(input: PlannerPromptInput): string {
     "## Story Bible",
     "## Story Bible",
     materials.storyBible,
     materials.storyBible,
     "",
     "",
-    "## Previous Ending Hook",
-    materials.previousEndingHook ?? "(none)",
-    "",
-    "## Previous Ending Excerpt",
-    materials.previousEndingExcerpt ?? "(none)",
-    "",
-    "## Recent Chronicle",
-    renderRecentSummaries(materials),
-    "",
-    "## Active Hooks",
-    renderActiveHooks(materials),
-    "",
-    "## Cycle Context",
-    renderCycleContext(input.cycleAnalysis, input.language),
-    "",
-    "## Output Requirements",
-    input.language === "zh"
-      ? ZH_OUTPUT_REQUIREMENTS
-      : EN_OUTPUT_REQUIREMENTS,
   ].join("\n");
   ].join("\n");
 }
 }
-
-const ZH_OUTPUT_REQUIREMENTS = `按核心原则产出 ChapterBrief JSON。严格使用以下字段名和类型:
-
-- goal (string): 本章主角想要的具体动作。不是情绪,不是抽象状态。
-- chapterType (string): 从下列选一个——冲突 / 推进 / 过渡 / 反转 / 爽点 / 刀子 / 揭露 / 黄金开场
-- beatOutline (array): 3-5 个对象,每个必须有 phase 和 instruction 两个字段。
-  phase 从 "opening" / "development" / "reversal" / "payoff" / "hook" 选。
-  instruction 是一句场景描述,不是标签。
-- hookPlan (array): 只列这章实际会动的 hook,一般 1-2 个。
-  每个对象必须有 hookId (string)、movement (string)、targetEffect (string) 三个字段。
-  其他 active hook 省略,用 dormantReason 解释为什么按兵不动。
-- propsAndSetting (string[]): 本章出现在页面上的人/物/地,平铺为字符串数组。
-- cyclePhase (string, 可选): 本章在小目标周期中的位置——"蓄压" / "升级" / "爆发" / "后效"
-- dormantReason (string, 可选): 其他 hook 按兵不动的原因。
-- isGoldenOpening (boolean): 前 3 章设 true,其他 false。`;
-
-const EN_OUTPUT_REQUIREMENTS = `Produce ChapterBrief JSON following the core principles. Use EXACTLY these field names and types:
-
-- goal (string): The concrete action the protagonist wants this chapter. Not an emotion, not an abstract state.
-- chapterType (string): Pick one — conflict / progression / transition / reversal / payoff / knife-twist / reveal / golden-opening
-- beatOutline (array): 3-5 objects, each with phase (string) and instruction (string).
-  phase from "opening" / "development" / "reversal" / "payoff" / "hook".
-  instruction is a scene-level description, not a label.
-- hookPlan (array): Only list hooks that actually move this chapter, typically 1-2.
-  Each object must have hookId (string), movement (string), targetEffect (string).
-  Omit other active hooks; explain in dormantReason why they stay dormant.
-- propsAndSetting (string[]): People/items/locations on-page, as a flat string array.
-- cyclePhase (string, optional): This chapter's position in the mini objective cycle — "build-up" / "escalation" / "climax" / "aftermath"
-- dormantReason (string, optional): Why other hooks stay dormant.
-- isGoldenOpening (boolean): true for first 5 chapters, false otherwise.`;

+ 59 - 519
packages/core/src/agents/planner.ts

@@ -4,29 +4,19 @@ import { BaseAgent } from "./base.js";
 import type { BookConfig } from "../models/book.js";
 import type { BookConfig } from "../models/book.js";
 import { parseBookRules } from "../models/book-rules.js";
 import { parseBookRules } from "../models/book-rules.js";
 import {
 import {
-  ChapterBriefSchema,
   ChapterIntentSchema,
   ChapterIntentSchema,
-  type ChapterBrief,
-  type ChapterConflict,
+  ChapterMemoSchema,
   type ChapterIntent,
   type ChapterIntent,
+  type ChapterMemo,
 } from "../models/input-governance.js";
 } from "../models/input-governance.js";
 import {
 import {
-  parseChapterSummariesMarkdown,
   renderHookSnapshot,
   renderHookSnapshot,
   renderSummarySnapshot,
   renderSummarySnapshot,
 } from "../utils/memory-retrieval.js";
 } from "../utils/memory-retrieval.js";
-import { analyzeChapterCadence, analyzeObjectiveCycle, type ObjectiveCycleAnalysis } from "../utils/chapter-cadence.js";
-import { buildPlannerHookAgenda } from "../utils/hook-agenda.js";
 import {
 import {
   gatherPlanningMaterials,
   gatherPlanningMaterials,
   loadPlanningSeedMaterials,
   loadPlanningSeedMaterials,
-  type PlanningMaterials,
 } from "../utils/planning-materials.js";
 } from "../utils/planning-materials.js";
-import {
-  buildPlannerSystemPrompt,
-  buildPlannerUserPrompt,
-  type PlannerPromptInput,
-} from "./planner-prompts.js";
 
 
 export interface PlanChapterInput {
 export interface PlanChapterInput {
   readonly book: BookConfig;
   readonly book: BookConfig;
@@ -37,12 +27,24 @@ export interface PlanChapterInput {
 
 
 export interface PlanChapterOutput {
 export interface PlanChapterOutput {
   readonly intent: ChapterIntent;
   readonly intent: ChapterIntent;
-  readonly brief: ChapterBrief;
+  readonly memo: ChapterMemo;
   readonly intentMarkdown: string;
   readonly intentMarkdown: string;
   readonly plannerInputs: ReadonlyArray<string>;
   readonly plannerInputs: ReadonlyArray<string>;
   readonly runtimePath: string;
   readonly runtimePath: string;
 }
 }
 
 
+/**
+ * Phase 1 transitional planner.
+ *
+ * Phase 3 (new.txt) will replace this with an LLM-driven memo generator that
+ * fills `ChapterMemo.body` with prose across the seven narrative sections.
+ * For now the planner produces:
+ *   - a simplified ChapterIntent (goal + outline + keep/avoid/style)
+ *   - a stub ChapterMemo with empty body
+ *
+ * The pipeline still runs end-to-end; the writer simply reads the minimal
+ * intent until the memo rewrite lands.
+ */
 export class PlannerAgent extends BaseAgent {
 export class PlannerAgent extends BaseAgent {
   get name(): string {
   get name(): string {
     return "planner";
     return "planner";
@@ -58,7 +60,6 @@ export class PlannerAgent extends BaseAgent {
       chapterNumber: input.chapterNumber,
       chapterNumber: input.chapterNumber,
     });
     });
     const outlineNode = this.findOutlineNode(seedMaterials.volumeOutline, input.chapterNumber);
     const outlineNode = this.findOutlineNode(seedMaterials.volumeOutline, input.chapterNumber);
-    const matchedOutlineAnchor = this.hasMatchedOutlineAnchor(seedMaterials.volumeOutline, input.chapterNumber);
     const goal = this.deriveGoal(
     const goal = this.deriveGoal(
       input.externalContext,
       input.externalContext,
       seedMaterials.currentFocus,
       seedMaterials.currentFocus,
@@ -70,13 +71,11 @@ export class PlannerAgent extends BaseAgent {
     const mustKeep = this.collectMustKeep(seedMaterials.currentState, seedMaterials.storyBible);
     const mustKeep = this.collectMustKeep(seedMaterials.currentState, seedMaterials.storyBible);
     const mustAvoid = this.collectMustAvoid(seedMaterials.currentFocus, parsedRules.rules.prohibitions);
     const mustAvoid = this.collectMustAvoid(seedMaterials.currentFocus, parsedRules.rules.prohibitions);
     const styleEmphasis = this.collectStyleEmphasis(seedMaterials.authorIntent, seedMaterials.currentFocus);
     const styleEmphasis = this.collectStyleEmphasis(seedMaterials.authorIntent, seedMaterials.currentFocus);
-    const conflicts = this.collectConflicts(input.externalContext, seedMaterials.currentFocus, outlineNode, seedMaterials.volumeOutline);
-    const planningAnchor = conflicts.length > 0 ? undefined : outlineNode;
     const materials = await gatherPlanningMaterials({
     const materials = await gatherPlanningMaterials({
       bookDir: input.bookDir,
       bookDir: input.bookDir,
       chapterNumber: input.chapterNumber,
       chapterNumber: input.chapterNumber,
       goal,
       goal,
-      outlineNode: planningAnchor,
+      outlineNode,
       mustKeep,
       mustKeep,
       seed: seedMaterials,
       seed: seedMaterials,
     });
     });
@@ -84,50 +83,37 @@ export class PlannerAgent extends BaseAgent {
     const activeHookCount = memorySelection.activeHooks.filter(
     const activeHookCount = memorySelection.activeHooks.filter(
       (hook) => hook.status !== "resolved" && hook.status !== "deferred",
       (hook) => hook.status !== "resolved" && hook.status !== "deferred",
     ).length;
     ).length;
-    const hookAgenda = buildPlannerHookAgenda({
-      hooks: memorySelection.activeHooks,
-      chapterNumber: input.chapterNumber,
-      targetChapters: input.book.targetChapters,
-      language: input.book.language ?? "zh",
-    });
-    const { cycleAnalysis, ...directives } = this.buildStructuredDirectives({
-      chapterNumber: input.chapterNumber,
-      language: input.book.language,
-      volumeOutline: seedMaterials.volumeOutline,
-      outlineNode,
-      matchedOutlineAnchor,
-      chapterSummaries: materials.chapterSummariesRaw,
-      hookAgenda: {
-        eligibleResolve: hookAgenda.eligibleResolve,
-        staleDebt: hookAgenda.staleDebt,
-      },
-    });
 
 
-    const brief = await this.planChapterBrief({
-      input,
-      outlineNode: planningAnchor,
-      materials,
-      cycleAnalysis,
-    });
+    const arcContext = this.buildArcContext(
+      input.book.language,
+      seedMaterials.volumeOutline,
+      outlineNode,
+    );
 
 
     const intent = ChapterIntentSchema.parse({
     const intent = ChapterIntentSchema.parse({
       chapter: input.chapterNumber,
       chapter: input.chapterNumber,
-      goal: brief.goal,
+      goal,
       outlineNode,
       outlineNode,
-      ...directives,
+      arcContext,
       mustKeep,
       mustKeep,
       mustAvoid,
       mustAvoid,
-      styleEmphasis: this.mergeBriefStyleEmphasis(styleEmphasis, brief),
-      sceneDirective: this.buildSceneDirectiveFromBrief(brief),
-      arcDirective: this.buildArcDirectiveFromBrief(brief),
-      conflicts,
-      hookAgenda,
+      styleEmphasis,
+    });
+
+    // Phase 1 stub memo. Phase 3 rewrites this with full prose.
+    const isGoldenOpening = input.chapterNumber <= 3;
+    const memo = ChapterMemoSchema.parse({
+      chapter: input.chapterNumber,
+      goal,
+      isGoldenOpening,
+      body: "",
+      hookRefs: [],
     });
     });
 
 
     const runtimePath = join(runtimeDir, `chapter-${String(input.chapterNumber).padStart(4, "0")}.intent.md`);
     const runtimePath = join(runtimeDir, `chapter-${String(input.chapterNumber).padStart(4, "0")}.intent.md`);
     const intentMarkdown = this.renderIntentMarkdown(
     const intentMarkdown = this.renderIntentMarkdown(
       intent,
       intent,
-      brief,
+      memo,
       input.book.language ?? "zh",
       input.book.language ?? "zh",
       renderHookSnapshot(memorySelection.hooks, input.book.language ?? "zh"),
       renderHookSnapshot(memorySelection.hooks, input.book.language ?? "zh"),
       renderSummarySnapshot(memorySelection.summaries, input.book.language ?? "zh"),
       renderSummarySnapshot(memorySelection.summaries, input.book.language ?? "zh"),
@@ -137,188 +123,23 @@ export class PlannerAgent extends BaseAgent {
 
 
     return {
     return {
       intent,
       intent,
-      brief,
+      memo,
       intentMarkdown,
       intentMarkdown,
       plannerInputs: materials.plannerInputs,
       plannerInputs: materials.plannerInputs,
       runtimePath,
       runtimePath,
     };
     };
   }
   }
 
 
-  private async planChapterBrief(params: {
-    readonly input: PlanChapterInput;
-    readonly outlineNode: string | undefined;
-    readonly materials: PlanningMaterials;
-    readonly cycleAnalysis?: ObjectiveCycleAnalysis;
-  }): Promise<ChapterBrief> {
-    const language = this.isChineseLanguage(params.input.book.language) ? "zh" : "en";
-    const response = await this.chat([
-      {
-        role: "system",
-        content: buildPlannerSystemPrompt(language),
-      },
-      {
-        role: "user",
-        content: buildPlannerUserPrompt({
-          chapterNumber: params.input.chapterNumber,
-          targetChapters: params.input.book.targetChapters,
-          genreName: params.input.book.genre,
-          language,
-          materials: {
-            ...params.materials,
-            outlineNode: params.outlineNode,
-          },
-          cycleAnalysis: params.cycleAnalysis,
-        }),
-      },
-    ], {
-      temperature: 0.2,
-      maxTokens: 4096,
-    });
-
-    this.log?.info(`[planner] Raw brief response length: ${response.content.length} chars`);
-    const parsed = this.tryParseChapterBrief(response.content, params.input.chapterNumber);
-    if (!parsed) {
-      this.log?.warn(`[planner] Full raw output:\n${response.content}`);
-      throw new Error(`Planner LLM returned invalid ChapterBrief JSON. Raw output: ${response.content.slice(0, 500)}`);
-    }
-    return parsed;
-  }
-
-  private tryParseChapterBrief(text: string, chapterNumber: number): ChapterBrief | null {
-    const inject = (t: string) => this.injectChapterNumber(t, chapterNumber);
-    const direct = this.tryParseExactChapterBrief(inject(text));
-    if (direct) {
-      return direct;
-    }
-    const candidate = extractBalancedJsonObject(text);
-    if (!candidate) {
-      return null;
-    }
-    return this.tryParseExactChapterBrief(inject(candidate));
-  }
-
-  /** Ensure the parsed JSON has a chapter field — LLMs often omit it. */
-  private injectChapterNumber(text: string, chapterNumber: number): string {
-    try {
-      const obj = JSON.parse(text);
-      if (typeof obj === "object" && obj !== null && obj.chapter === undefined) {
-        obj.chapter = chapterNumber;
-        return JSON.stringify(obj);
-      }
-    } catch { /* not valid JSON yet, pass through */ }
-    return text;
-  }
-
-  private tryParseExactChapterBrief(text: string): ChapterBrief | null {
-    try {
-      const raw = JSON.parse(text);
-      const normalized = this.normalizeBriefJson(raw);
-      return ChapterBriefSchema.parse(normalized);
-    } catch (err) {
-      if (err instanceof Error && err.name === "ZodError") {
-        this.log?.warn(`[planner] Zod validation errors: ${err.message}`);
-      }
-      return null;
-    }
-  }
-
-  private static readonly VALID_MOVEMENTS = new Set([
-    "quiet-hold", "refresh", "advance", "partial-payoff", "full-payoff",
-  ]);
-
-  /**
-   * Normalize common LLM deviations from the ChapterBrief schema:
-   * - hookPlan[].note → hookPlan[].targetEffect
-   * - hookPlan[].movement unknown values → "advance"
-   * - propsAndSetting as object → flatten to string[]
-   */
-  private normalizeBriefJson(raw: Record<string, unknown>): Record<string, unknown> {
-    // Normalize hookPlan
-    if (Array.isArray(raw.hookPlan)) {
-      raw.hookPlan = raw.hookPlan.map((hook: Record<string, unknown>) => {
-        const normalized = { ...hook };
-        // rename note → targetEffect
-        if (normalized.note && !normalized.targetEffect) {
-          normalized.targetEffect = normalized.note;
-          delete normalized.note;
-        }
-        // clamp unknown movements to "advance"
-        if (typeof normalized.movement === "string" && !PlannerAgent.VALID_MOVEMENTS.has(normalized.movement)) {
-          normalized.movement = "advance";
-        }
-        return normalized;
-      });
-    }
-
-    // Normalize propsAndSetting: flatten object to string[]
-    if (raw.propsAndSetting && !Array.isArray(raw.propsAndSetting)) {
-      const obj = raw.propsAndSetting as Record<string, unknown>;
-      const flattened: string[] = [];
-      for (const values of Object.values(obj)) {
-        if (Array.isArray(values)) {
-          flattened.push(...values.filter((v): v is string => typeof v === "string"));
-        } else if (typeof values === "string") {
-          flattened.push(values);
-        }
-      }
-      raw.propsAndSetting = flattened;
-    }
-
-    return raw;
-  }
-
-  private buildStructuredDirectives(input: {
-    readonly chapterNumber: number;
-    readonly language?: string;
-    readonly volumeOutline: string;
-    readonly outlineNode: string | undefined;
-    readonly matchedOutlineAnchor: boolean;
-    readonly chapterSummaries: string;
-    readonly hookAgenda?: {
-      readonly eligibleResolve: ReadonlyArray<string>;
-      readonly staleDebt: ReadonlyArray<string>;
-    };
-  }): Pick<ChapterIntent, "sceneDirective" | "arcDirective" | "moodDirective" | "titleDirective"> & {
-    readonly cycleAnalysis?: ObjectiveCycleAnalysis;
-  } {
-    const language = this.isChineseLanguage(input.language) ? "zh" as const : "en" as const;
-    const recentSummaries = parseChapterSummariesMarkdown(input.chapterSummaries)
-      .filter((summary) => summary.chapter < input.chapterNumber)
-      .sort((left, right) => left.chapter - right.chapter)
-      .slice(-5);
-    const cadenceRows = recentSummaries.map((summary) => ({
-      chapter: summary.chapter,
-      title: summary.title,
-      mood: summary.mood,
-      chapterType: summary.chapterType,
-    }));
-    const cadence = analyzeChapterCadence({
-      language,
-      rows: cadenceRows,
-    });
-
-    const cycleAnalysis = input.hookAgenda
-      ? analyzeObjectiveCycle({
-          rows: cadenceRows,
-          hookAgenda: input.hookAgenda,
-          language,
-        })
-      : undefined;
-
-    return {
-      arcDirective: this.buildArcDirective(
-        input.language,
-        input.volumeOutline,
-        input.outlineNode,
-        input.matchedOutlineAnchor,
-      ),
-      sceneDirective: this.buildSceneDirective(input.language, cadence),
-      moodDirective: cycleAnalysis
-        ? this.buildCycleDirective(input.language, cycleAnalysis)
-        : this.buildMoodDirective(input.language, cadence),
-      titleDirective: this.buildTitleDirective(input.language, cadence),
-      cycleAnalysis,
-    };
+  private buildArcContext(
+    language: string | undefined,
+    volumeOutline: string,
+    outlineNode: string | undefined,
+  ): string | undefined {
+    if (!outlineNode) return undefined;
+    if (volumeOutline === "(文件尚未创建)") return undefined;
+    return this.isChineseLanguage(language)
+      ? `卷纲节点:${outlineNode}`
+      : `Outline node: ${outlineNode}`;
   }
   }
 
 
   private deriveGoal(
   private deriveGoal(
@@ -378,40 +199,6 @@ export class PlannerAgent extends BaseAgent {
     ]).slice(0, 4);
     ]).slice(0, 4);
   }
   }
 
 
-  private collectConflicts(
-    externalContext: string | undefined,
-    currentFocus: string,
-    outlineNode: string | undefined,
-    volumeOutline: string,
-  ): ChapterConflict[] {
-    const outlineText = outlineNode ?? volumeOutline;
-    if (!outlineText || outlineText === "(文件尚未创建)") return [];
-    if (externalContext) {
-      const indicatesOverride = /ignore|skip|defer|instead|不要|别|先别|暂停/i.test(externalContext);
-      if (!indicatesOverride && this.hasKeywordOverlap(externalContext, outlineText)) return [];
-
-      return [
-        {
-          type: "outline_vs_request",
-          resolution: "allow local outline deferral",
-        },
-      ];
-    }
-
-    const localOverride = this.extractLocalOverrideGoal(currentFocus);
-    if (!localOverride || !outlineNode) {
-      return [];
-    }
-
-    return [
-      {
-        type: "outline_vs_current_focus",
-        resolution: "allow explicit current focus override",
-        detail: localOverride,
-      },
-    ];
-  }
-
   private extractFirstDirective(content?: string): string | undefined {
   private extractFirstDirective(content?: string): string | undefined {
     if (!content) return undefined;
     if (!content) return undefined;
     return content
     return content
@@ -484,88 +271,6 @@ export class PlannerAgent extends BaseAgent {
     return this.extractListItems(focusSection, limit);
     return this.extractListItems(focusSection, limit);
   }
   }
 
 
-  private buildArcDirective(
-    language: string | undefined,
-    volumeOutline: string,
-    outlineNode: string | undefined,
-    matchedOutlineAnchor: boolean,
-  ): string | undefined {
-    if (matchedOutlineAnchor || !outlineNode || volumeOutline === "(文件尚未创建)") {
-      return undefined;
-    }
-
-    return this.isChineseLanguage(language)
-      ? "不要继续依赖卷纲的 fallback 指令,必须把本章推进到新的弧线节点或地点变化。"
-      : "Do not keep leaning on the outline fallback. Force this chapter toward a fresh arc beat or location change.";
-  }
-
-  private buildSceneDirective(
-    language: string | undefined,
-    cadence: ReturnType<typeof analyzeChapterCadence>,
-  ): string | undefined {
-    if (cadence.scenePressure?.pressure !== "high") {
-      return undefined;
-    }
-    const repeatedType = cadence.scenePressure.repeatedType;
-
-    return this.isChineseLanguage(language)
-      ? `最近章节连续停留在“${repeatedType}”,本章必须更换场景容器、地点或行动方式。`
-      : `Recent chapters are stuck in repeated ${repeatedType} beats. Change the scene container, location, or action pattern this chapter.`;
-  }
-
-  private buildMoodDirective(
-    language: string | undefined,
-    cadence: ReturnType<typeof analyzeChapterCadence>,
-  ): string | undefined {
-    if (cadence.moodPressure?.pressure !== "high") {
-      return undefined;
-    }
-    const moods = cadence.moodPressure.recentMoods;
-
-    return this.isChineseLanguage(language)
-      ? `最近${moods.length}章情绪持续高压(${moods.slice(0, 3).join("、")}),本章必须降调——安排日常/喘息/温情/幽默场景,让读者呼吸。`
-      : `The last ${moods.length} chapters have been relentlessly tense (${moods.slice(0, 3).join(", ")}). This chapter must downshift — write a quieter scene with warmth, humor, or breathing room.`;
-  }
-
-  private buildCycleDirective(
-    language: string | undefined,
-    cycle: ObjectiveCycleAnalysis,
-  ): string | undefined {
-    const isZh = this.isChineseLanguage(language);
-    switch (cycle.phase) {
-      case "蓄压":
-        return isZh
-          ? "本章铺压制,新阻力或新信息——不急着爆发。"
-          : "This chapter builds pressure — introduce new obstacles or information. Don't rush to climax.";
-      case "升级":
-        return isZh
-          ? "本章升级冲突,已有缺口还没释放——加码不松手。"
-          : "This chapter escalates conflict — existing gaps are unresolved. Raise the stakes, don't relent.";
-      case "爆发":
-        return isZh
-          ? "本章有一个承诺必须兑现——让积累的压力落地,给读者超预期的释放。"
-          : "This chapter must deliver on a promise — release accumulated pressure with a payoff that exceeds expectations.";
-      case "后效":
-        return isZh
-          ? "上一章刚爆发,写改变:关系变了、地位变了、代价显现了。"
-          : "Previous chapter just climaxed — write the aftermath: changed relationships, shifted status, visible costs.";
-    }
-  }
-
-  private buildTitleDirective(
-    language: string | undefined,
-    cadence: ReturnType<typeof analyzeChapterCadence>,
-  ): string | undefined {
-    if (cadence.titlePressure?.pressure !== "high") {
-      return undefined;
-    }
-    const repeatedToken = cadence.titlePressure.repeatedToken;
-
-    return this.isChineseLanguage(language)
-      ? `标题不要再围绕“${repeatedToken}”重复命名,换一个新的意象或动作焦点。`
-      : `Avoid another ${repeatedToken}-centric title. Pick a new image or action focus for this chapter title.`;
-  }
-
   private renderHookBudget(activeCount: number, language: "zh" | "en"): string {
   private renderHookBudget(activeCount: number, language: "zh" | "en"): string {
     const cap = 12;
     const cap = 12;
     if (activeCount < 10) {
     if (activeCount < 10) {
@@ -670,9 +375,6 @@ export class PlannerAgent extends BaseAgent {
         return inlineContent;
         return inlineContent;
       }
       }
 
 
-      // For "章节范围" format, the volume title is above this line.
-      // Collect the heading + all content below until the next heading,
-      // then pick the beat corresponding to this specific chapter.
       const rangeStart = Number(match[1]);
       const rangeStart = Number(match[1]);
       const sectionContent = this.extractSectionAroundRange(lines, index);
       const sectionContent = this.extractSectionAroundRange(lines, index);
       if (sectionContent) {
       if (sectionContent) {
@@ -725,36 +427,25 @@ export class PlannerAgent extends BaseAgent {
     return cleaned;
     return cleaned;
   }
   }
 
 
-  /**
-   * For "章节范围:13-17章" format, extract the full section:
-   * look upward for the volume heading, then collect everything
-   * from the heading down to the next heading of the same or higher level.
-   */
   private extractSectionAroundRange(lines: ReadonlyArray<string>, rangeLineIndex: number): string | undefined {
   private extractSectionAroundRange(lines: ReadonlyArray<string>, rangeLineIndex: number): string | undefined {
-    // Walk backward to find the nearest heading (### or ##)
-    // Only activate for "章节范围" style outlines where the heading is above.
     let headingIndex = -1;
     let headingIndex = -1;
     for (let i = rangeLineIndex - 1; i >= 0; i--) {
     for (let i = rangeLineIndex - 1; i >= 0; i--) {
       if (lines[i]!.startsWith("#")) {
       if (lines[i]!.startsWith("#")) {
         headingIndex = i;
         headingIndex = i;
         break;
         break;
       }
       }
-      // Stop if we hit another range/anchor line (means no heading above)
       if (this.matchAnyRangeOutlineLine(lines[i]!) || this.matchAnyExactOutlineLine(lines[i]!)) {
       if (this.matchAnyRangeOutlineLine(lines[i]!) || this.matchAnyExactOutlineLine(lines[i]!)) {
         break;
         break;
       }
       }
     }
     }
 
 
-    // If no heading found above, this isn't the "章节范围" format — bail out
     if (headingIndex < 0) {
     if (headingIndex < 0) {
       return undefined;
       return undefined;
     }
     }
 
 
-    // Determine the heading level to know where this section ends
     const headingLine = lines[headingIndex]!;
     const headingLine = lines[headingIndex]!;
     const headingLevel = headingLine.match(/^(#+)/)?.[1]?.length ?? 3;
     const headingLevel = headingLine.match(/^(#+)/)?.[1]?.length ?? 3;
 
 
-    // Collect lines from heading to next same-or-higher heading
     const sectionLines: string[] = [];
     const sectionLines: string[] = [];
     for (let i = headingIndex; i < lines.length; i++) {
     for (let i = headingIndex; i < lines.length; i++) {
       if (i > headingIndex) {
       if (i > headingIndex) {
@@ -770,18 +461,12 @@ export class PlannerAgent extends BaseAgent {
     return content.length > 0 ? content : undefined;
     return content.length > 0 ? content : undefined;
   }
   }
 
 
-  /**
-   * Extract the Nth numbered beat from a section.
-   * Beats are lines starting with "1.", "2.", "3." etc. in "关键转折" blocks.
-   * beatIndex=0 → 1st beat, beatIndex=1 → 2nd beat, etc.
-   */
   private extractNumberedBeat(section: string, beatIndex: number): string | undefined {
   private extractNumberedBeat(section: string, beatIndex: number): string | undefined {
     if (beatIndex < 0) return undefined;
     if (beatIndex < 0) return undefined;
 
 
     const beats: string[] = [];
     const beats: string[] = [];
     for (const line of section.split("\n")) {
     for (const line of section.split("\n")) {
       const trimmed = line.trim();
       const trimmed = line.trim();
-      // Match "1. ...", "2. ..." or "1) ..." patterns
       if (/^\d+[.)]\s/.test(trimmed)) {
       if (/^\d+[.)]\s/.test(trimmed)) {
         beats.push(trimmed.replace(/^\d+[.)]\s*/, ""));
         beats.push(trimmed.replace(/^\d+[.)]\s*/, ""));
       }
       }
@@ -815,14 +500,6 @@ export class PlannerAgent extends BaseAgent {
     return undefined;
     return undefined;
   }
   }
 
 
-  private hasMatchedOutlineAnchor(volumeOutline: string, chapterNumber: number): boolean {
-    const lines = volumeOutline.split("\n").map((line) => line.trim()).filter(Boolean);
-    return lines.some((line) =>
-      this.matchExactOutlineLine(line, chapterNumber) !== undefined
-      || this.matchRangeOutlineLine(line, chapterNumber) !== undefined,
-    );
-  }
-
   private matchExactOutlineLine(line: string, chapterNumber: number): RegExpMatchArray | undefined {
   private matchExactOutlineLine(line: string, chapterNumber: number): RegExpMatchArray | undefined {
     const patterns = [
     const patterns = [
       new RegExp(`^(?:#+\\s*)?(?:[-*]\\s+)?(?:\\*\\*)?Chapter\\s*${chapterNumber}(?!\\d|\\s*[-~–—]\\s*\\d)(?:[::-])?(?:\\*\\*)?\\s*(.*)$`, "i"),
       new RegExp(`^(?:#+\\s*)?(?:[-*]\\s+)?(?:\\*\\*)?Chapter\\s*${chapterNumber}(?!\\d|\\s*[-~–—]\\s*\\d)(?:[::-])?(?:\\*\\*)?\\s*(.*)$`, "i"),
@@ -859,7 +536,6 @@ export class PlannerAgent extends BaseAgent {
     const patterns = [
     const patterns = [
       /^(?:#+\s*)?(?:[-*]\s+)?(?:\*\*)?Chapter\s*(\d+)\s*[-~–—]\s*(\d+)\b(?:[::-])?(?:\*\*)?\s*(.*)$/i,
       /^(?:#+\s*)?(?:[-*]\s+)?(?:\*\*)?Chapter\s*(\d+)\s*[-~–—]\s*(\d+)\b(?:[::-])?(?:\*\*)?\s*(.*)$/i,
       /^(?:#+\s*)?(?:[-*]\s+)?(?:\*\*)?第\s*(\d+)\s*[-~–—]\s*(\d+)\s*章(?:[::-])?(?:\*\*)?\s*(.*)$/i,
       /^(?:#+\s*)?(?:[-*]\s+)?(?:\*\*)?第\s*(\d+)\s*[-~–—]\s*(\d+)\s*章(?:[::-])?(?:\*\*)?\s*(.*)$/i,
-      // Match "**章节范围**:13-17章" / "- **章节范围**:13-17章" format
       /^(?:[-*]\s+)?(?:\*\*)?章节范围(?:\*\*)?[::]\s*(\d+)\s*[-~–—]\s*(\d+)\s*章\s*(.*)$/,
       /^(?:[-*]\s+)?(?:\*\*)?章节范围(?:\*\*)?[::]\s*(\d+)\s*[-~–—]\s*(\d+)\s*章\s*(.*)$/,
       /^(?:[-*]\s+)?(?:\*\*)?Chapter\s*[Rr]ange(?:\*\*)?[::]\s*(\d+)\s*[-~–—]\s*(\d+)\b\s*(.*)$/i,
       /^(?:[-*]\s+)?(?:\*\*)?Chapter\s*[Rr]ange(?:\*\*)?[::]\s*(\d+)\s*[-~–—]\s*(\d+)\b\s*(.*)$/i,
     ];
     ];
@@ -883,31 +559,14 @@ export class PlannerAgent extends BaseAgent {
     return chapterNumber >= lower && chapterNumber <= upper;
     return chapterNumber >= lower && chapterNumber <= upper;
   }
   }
 
 
-  private hasKeywordOverlap(left: string, right: string): boolean {
-    const keywords = this.extractKeywords(left);
-    if (keywords.length === 0) return false;
-    const normalizedRight = right.toLowerCase();
-    return keywords.some((keyword) => normalizedRight.includes(keyword.toLowerCase()));
-  }
-
-  private extractKeywords(content: string): string[] {
-    const english = content.match(/[a-z]{4,}/gi) ?? [];
-    const chinese = content.match(/[\u4e00-\u9fff]{2,4}/g) ?? [];
-    return this.unique([...english, ...chinese]);
-  }
-
   private renderIntentMarkdown(
   private renderIntentMarkdown(
     intent: ChapterIntent,
     intent: ChapterIntent,
-    brief: ChapterBrief | undefined,
+    memo: ChapterMemo,
     language: "zh" | "en",
     language: "zh" | "en",
     pendingHooks: string,
     pendingHooks: string,
     chapterSummaries: string,
     chapterSummaries: string,
     activeHookCount: number,
     activeHookCount: number,
   ): string {
   ): string {
-    const conflictLines = intent.conflicts.length > 0
-      ? intent.conflicts.map((conflict) => `- ${conflict.type}: ${conflict.resolution}`).join("\n")
-      : "- none";
-
     const mustKeep = intent.mustKeep.length > 0
     const mustKeep = intent.mustKeep.length > 0
       ? intent.mustKeep.map((item) => `- ${item}`).join("\n")
       ? intent.mustKeep.map((item) => `- ${item}`).join("\n")
       : "- none";
       : "- none";
@@ -919,36 +578,8 @@ export class PlannerAgent extends BaseAgent {
     const styleEmphasis = intent.styleEmphasis.length > 0
     const styleEmphasis = intent.styleEmphasis.length > 0
       ? intent.styleEmphasis.map((item) => `- ${item}`).join("\n")
       ? intent.styleEmphasis.map((item) => `- ${item}`).join("\n")
       : "- none";
       : "- none";
-    const directives = [
-      intent.arcDirective ? `- arc: ${intent.arcDirective}` : undefined,
-      intent.sceneDirective ? `- scene: ${intent.sceneDirective}` : undefined,
-      intent.moodDirective ? `- mood: ${intent.moodDirective}` : undefined,
-      intent.titleDirective ? `- title: ${intent.titleDirective}` : undefined,
-    ].filter(Boolean).join("\n") || "- none";
-    const isEn = language === "en";
-    const hookAgenda = [
-      isEn ? "### Resolve — must deliver a concrete payoff this chapter" : "### 兑现(本章必须落地具体回收)",
-      intent.hookAgenda.eligibleResolve.length > 0
-        ? intent.hookAgenda.eligibleResolve.map((item) => `- ${item}`).join("\n")
-        : isEn ? "- none this round" : "- 本轮无",
-      "",
-      isEn ? "### Advance — must show visible progress this chapter" : "### 推进(本章必须有可见进展)",
-      intent.hookAgenda.mustAdvance.length > 0
-        ? intent.hookAgenda.mustAdvance.map((item) => `- ${item}`).join("\n")
-        : isEn ? "- none this round" : "- 本轮无",
-      "",
-      isEn ? "### Stale Debt — overdue hooks, prioritize clearing" : "### 逾期债务(积压伏笔,优先清理)",
-      intent.hookAgenda.staleDebt.length > 0
-        ? intent.hookAgenda.staleDebt.map((item) => `- ${item}`).join("\n")
-        : isEn ? "- none" : "- 无",
-      "",
-      isEn ? "### Do Not Open — avoid new hooks in these families" : "### 禁开新坑(以下类型不得新增伏笔)",
-      intent.hookAgenda.avoidNewHookFamilies.length > 0
-        ? intent.hookAgenda.avoidNewHookFamilies.map((item) => `- ${item}`).join("\n")
-        : isEn ? "- none" : "- 无",
-      "",
-      this.renderHookBudget(activeHookCount, language),
-    ].join("\n");
+
+    const memoBody = memo.body.trim() ? memo.body : (language === "en" ? "(empty — Phase 3 fills this)" : "(空——待 Phase 3 填充)");
 
 
     return [
     return [
       "# Chapter Intent",
       "# Chapter Intent",
@@ -959,27 +590,9 @@ export class PlannerAgent extends BaseAgent {
       "## Outline Node",
       "## Outline Node",
       intent.outlineNode ?? "(not found)",
       intent.outlineNode ?? "(not found)",
       "",
       "",
-      ...(brief ? [
-        "## Chapter Brief",
-        `- chapterType: ${brief.chapterType}`,
-        `- isGoldenOpening: ${brief.isGoldenOpening ? "true" : "false"}`,
-        ...(brief.cyclePhase ? [`- cyclePhase: ${brief.cyclePhase}`] : []),
-        `- dormantReason: ${brief.dormantReason ?? "(none)"}`,
-        "",
-        "### Beat Outline",
-        ...brief.beatOutline.map((beat) => `- ${beat.phase}: ${beat.instruction}`),
-        "",
-        "### Hook Plan",
-        ...(brief.hookPlan.length > 0
-          ? brief.hookPlan.map((item) => `- ${item.hookId}: ${item.movement} -> ${item.targetEffect}`)
-          : ["- none"]),
-        "",
-        "### Props And Setting",
-        ...(brief.propsAndSetting.length > 0
-          ? brief.propsAndSetting.map((item) => `- ${item}`)
-          : ["- none"]),
-        "",
-      ] : []),
+      "## Arc Context",
+      intent.arcContext ?? "(none)",
+      "",
       "## Must Keep",
       "## Must Keep",
       mustKeep,
       mustKeep,
       "",
       "",
@@ -989,14 +602,13 @@ export class PlannerAgent extends BaseAgent {
       "## Style Emphasis",
       "## Style Emphasis",
       styleEmphasis,
       styleEmphasis,
       "",
       "",
-      "## Structured Directives",
-      directives,
+      "## Chapter Memo",
+      `- isGoldenOpening: ${memo.isGoldenOpening ? "true" : "false"}`,
       "",
       "",
-      "## Hook Agenda",
-      hookAgenda,
+      "### Body",
+      memoBody,
       "",
       "",
-      "## Conflicts",
-      conflictLines,
+      this.renderHookBudget(activeHookCount, language),
       "",
       "",
       "## Pending Hooks Snapshot",
       "## Pending Hooks Snapshot",
       pendingHooks,
       pendingHooks,
@@ -1007,26 +619,6 @@ export class PlannerAgent extends BaseAgent {
     ].join("\n");
     ].join("\n");
   }
   }
 
 
-  private buildSceneDirectiveFromBrief(brief: ChapterBrief): string {
-    const props = brief.propsAndSetting.length > 0
-      ? ` Use these on-page anchors: ${brief.propsAndSetting.join(", ")}.`
-      : "";
-    return `Run this as a ${brief.chapterType} chapter.${props}`.trim();
-  }
-
-  private buildArcDirectiveFromBrief(brief: ChapterBrief): string {
-    return brief.beatOutline
-      .map((beat) => `${beat.phase}: ${beat.instruction}`)
-      .join(" | ");
-  }
-
-  private mergeBriefStyleEmphasis(styleEmphasis: ReadonlyArray<string>, brief: ChapterBrief): string[] {
-    const derived = brief.isGoldenOpening
-      ? ["Honor golden-opening pacing without leaking chapter-planning meta language."]
-      : [];
-    return this.unique([...styleEmphasis, ...derived]);
-  }
-
   private unique(values: ReadonlyArray<string>): string[] {
   private unique(values: ReadonlyArray<string>): string[] {
     return [...new Set(values.map((value) => value.trim()).filter(Boolean))];
     return [...new Set(values.map((value) => value.trim()).filter(Boolean))];
   }
   }
@@ -1035,64 +627,12 @@ export class PlannerAgent extends BaseAgent {
     return (language ?? "zh").toLowerCase().startsWith("zh");
     return (language ?? "zh").toLowerCase().startsWith("zh");
   }
   }
 
 
-  private async readFileOrDefault(path: string): Promise<string> {
+  // Kept for potential subclasses reading seed files directly.
+  protected async readFileOrDefault(path: string): Promise<string> {
     try {
     try {
       return await readFile(path, "utf-8");
       return await readFile(path, "utf-8");
     } catch {
     } catch {
       return "(文件尚未创建)";
       return "(文件尚未创建)";
     }
     }
   }
   }
-
-}
-
-function extractBalancedJsonObject(text: string): string | null {
-  const start = text.indexOf("{");
-  if (start < 0) {
-    return null;
-  }
-
-  let depth = 0;
-  let inString = false;
-  let escaped = false;
-
-  for (let index = start; index < text.length; index += 1) {
-    const char = text[index]!;
-
-    if (inString) {
-      if (escaped) {
-        escaped = false;
-        continue;
-      }
-      if (char === "\\") {
-        escaped = true;
-        continue;
-      }
-      if (char === "\"") {
-        inString = false;
-      }
-      continue;
-    }
-
-    if (char === "\"") {
-      inString = true;
-      continue;
-    }
-
-    if (char === "{") {
-      depth += 1;
-      continue;
-    }
-
-    if (char === "}") {
-      depth -= 1;
-      if (depth === 0) {
-        return text.slice(start, index + 1);
-      }
-      if (depth < 0) {
-        return null;
-      }
-    }
-  }
-
-  return null;
 }
 }

+ 10 - 8
packages/core/src/agents/reviser.ts

@@ -3,7 +3,7 @@ import type { GenreProfile } from "../models/genre-profile.js";
 import type { BookRules } from "../models/book-rules.js";
 import type { BookRules } from "../models/book-rules.js";
 import type { LengthSpec } from "../models/length-governance.js";
 import type { LengthSpec } from "../models/length-governance.js";
 import type { AuditIssue } from "./continuity.js";
 import type { AuditIssue } from "./continuity.js";
-import type { ChapterBrief, ContextPackage, RuleStack } from "../models/input-governance.js";
+import type { ChapterIntent, ChapterMemo, ContextPackage, RuleStack } from "../models/input-governance.js";
 import { readGenreProfile, readBookLanguage, readBookRules } from "./rules-reader.js";
 import { readGenreProfile, readBookLanguage, readBookRules } from "./rules-reader.js";
 import { countChapterLength } from "../utils/length-metrics.js";
 import { countChapterLength } from "../utils/length-metrics.js";
 import { buildGovernedMemoryEvidenceBlocks } from "../utils/governed-context.js";
 import { buildGovernedMemoryEvidenceBlocks } from "../utils/governed-context.js";
@@ -16,7 +16,7 @@ import {
 import { applySpotFixPatches, parseSpotFixPatches } from "../utils/spot-fix-patches.js";
 import { applySpotFixPatches, parseSpotFixPatches } from "../utils/spot-fix-patches.js";
 import {
 import {
   buildNarrativeIntentBrief,
   buildNarrativeIntentBrief,
-  renderBriefAsNarrativeBlock,
+  renderMemoAsNarrativeBlock,
   renderNarrativeSelectedContext,
   renderNarrativeSelectedContext,
   sanitizeNarrativeEvidenceBlock,
   sanitizeNarrativeEvidenceBlock,
 } from "../utils/narrative-control.js";
 } from "../utils/narrative-control.js";
@@ -116,7 +116,8 @@ export class ReviserAgent extends BaseAgent {
     genre?: string,
     genre?: string,
     options?: {
     options?: {
       chapterIntent?: string;
       chapterIntent?: string;
-      chapterBrief?: ChapterBrief;
+      chapterMemo?: ChapterMemo;
+      chapterIntentData?: ChapterIntent;
       contextPackage?: ContextPackage;
       contextPackage?: ContextPackage;
       ruleStack?: RuleStack;
       ruleStack?: RuleStack;
       lengthSpec?: LengthSpec;
       lengthSpec?: LengthSpec;
@@ -238,7 +239,7 @@ export class ReviserAgent extends BaseAgent {
       ? `\n## 同人正典参照(修稿专用)\n本书为同人作品。修改时参照正典角色档案和世界规则,不可违反正典事实。角色对话必须保留原作语癖。\n${fanficCanon}\n`
       ? `\n## 同人正典参照(修稿专用)\n本书为同人作品。修改时参照正典角色档案和世界规则,不可违反正典事实。角色对话必须保留原作语癖。\n${fanficCanon}\n`
       : "";
       : "";
     const reducedControlBlock = options?.contextPackage && options.ruleStack
     const reducedControlBlock = options?.contextPackage && options.ruleStack
-      ? this.buildReducedControlBlock(options.chapterBrief, options.chapterIntent, options.contextPackage, options.ruleStack)
+      ? this.buildReducedControlBlock(options.chapterMemo, options.chapterIntentData, options.chapterIntent, options.contextPackage, options.ruleStack)
       : "";
       : "";
     // Length guardrail only in legacy modes — auto mode delegates length to normalize.
     // Length guardrail only in legacy modes — auto mode delegates length to normalize.
     const lengthGuidanceBlock = mode !== "auto" && options?.lengthSpec
     const lengthGuidanceBlock = mode !== "auto" && options?.lengthSpec
@@ -554,7 +555,8 @@ ${outputFormat}`;
   }
   }
 
 
   private buildReducedControlBlock(
   private buildReducedControlBlock(
-    brief: ChapterBrief | undefined,
+    memo: ChapterMemo | undefined,
+    intent: ChapterIntent | undefined,
     chapterIntent: string | undefined,
     chapterIntent: string | undefined,
     contextPackage: ContextPackage,
     contextPackage: ContextPackage,
     ruleStack: RuleStack,
     ruleStack: RuleStack,
@@ -566,9 +568,9 @@ ${outputFormat}`;
         .map((override) => `- ${override.from} -> ${override.to}: ${override.reason} (${override.target})`)
         .map((override) => `- ${override.from} -> ${override.to}: ${override.reason} (${override.target})`)
         .join("\n")
         .join("\n")
       : "- none";
       : "- none";
-    // Prefer brief-based narrative block; fall back to legacy intent markdown
-    const narrativeBlock = brief
-      ? renderBriefAsNarrativeBlock(brief, "zh")
+    // Prefer memo-based narrative block; fall back to legacy intent markdown
+    const narrativeBlock = memo
+      ? renderMemoAsNarrativeBlock(memo, intent, "zh")
       : chapterIntent
       : chapterIntent
         ? buildNarrativeIntentBrief(chapterIntent, "zh")
         ? buildNarrativeIntentBrief(chapterIntent, "zh")
         : "(无)";
         : "(无)";

+ 11 - 35
packages/core/src/agents/writer.ts

@@ -15,7 +15,7 @@ import {
   type PostWriteViolation,
   type PostWriteViolation,
 } from "./post-write-validator.js";
 } from "./post-write-validator.js";
 import { analyzeAITells } from "./ai-tells.js";
 import { analyzeAITells } from "./ai-tells.js";
-import type { ChapterBrief, ChapterTrace, ContextPackage, RuleStack } from "../models/input-governance.js";
+import type { ChapterIntent, ChapterMemo, ChapterTrace, ContextPackage, RuleStack } from "../models/input-governance.js";
 import type { LengthSpec } from "../models/length-governance.js";
 import type { LengthSpec } from "../models/length-governance.js";
 import type { RuntimeStateDelta } from "../models/runtime-state.js";
 import type { RuntimeStateDelta } from "../models/runtime-state.js";
 import { buildLengthSpec, countChapterLength } from "../utils/length-metrics.js";
 import { buildLengthSpec, countChapterLength } from "../utils/length-metrics.js";
@@ -36,7 +36,7 @@ import { analyzeHookHealth } from "../utils/hook-health.js";
 import { buildEnglishVarianceBrief } from "../utils/long-span-fatigue.js";
 import { buildEnglishVarianceBrief } from "../utils/long-span-fatigue.js";
 import {
 import {
   buildNarrativeIntentBrief,
   buildNarrativeIntentBrief,
-  renderBriefAsNarrativeBlock,
+  renderMemoAsNarrativeBlock,
   renderNarrativeSelectedContext,
   renderNarrativeSelectedContext,
   sanitizeNarrativeEvidenceBlock,
   sanitizeNarrativeEvidenceBlock,
 } from "../utils/narrative-control.js";
 } from "../utils/narrative-control.js";
@@ -49,9 +49,8 @@ export interface WriteChapterInput {
   readonly chapterNumber: number;
   readonly chapterNumber: number;
   readonly externalContext?: string;
   readonly externalContext?: string;
   readonly chapterIntent?: string;
   readonly chapterIntent?: string;
-  readonly chapterBrief?: ChapterBrief;
-  readonly moodDirective?: string;
-  readonly titleDirective?: string;
+  readonly chapterMemo?: ChapterMemo;
+  readonly chapterIntentData?: ChapterIntent;
   readonly contextPackage?: ContextPackage;
   readonly contextPackage?: ContextPackage;
   readonly ruleStack?: RuleStack;
   readonly ruleStack?: RuleStack;
   readonly trace?: ChapterTrace;
   readonly trace?: ChapterTrace;
@@ -190,16 +189,15 @@ export class WriterAgent extends BaseAgent {
     const creativeSystemPrompt = buildWriterSystemPrompt(
     const creativeSystemPrompt = buildWriterSystemPrompt(
       book, genreProfile, bookRules, bookRulesBody, genreBody, styleGuide, styleFingerprint,
       book, genreProfile, bookRules, bookRulesBody, genreBody, styleGuide, styleFingerprint,
       chapterNumber, "creative", fanficContext, resolvedLanguage,
       chapterNumber, "creative", fanficContext, resolvedLanguage,
-      input.chapterBrief ? "governed" : "legacy",
+      input.chapterMemo ? "governed" : "legacy",
       resolvedLengthSpec,
       resolvedLengthSpec,
     );
     );
 
 
-    const creativeUserPrompt = input.chapterBrief && input.contextPackage && input.ruleStack
+    const creativeUserPrompt = input.chapterMemo && input.contextPackage && input.ruleStack
       ? this.buildGovernedUserPrompt({
       ? this.buildGovernedUserPrompt({
           chapterNumber,
           chapterNumber,
-          chapterBrief: input.chapterBrief,
-          moodDirective: input.moodDirective,
-          titleDirective: input.titleDirective,
+          chapterMemo: input.chapterMemo,
+          chapterIntentData: input.chapterIntentData,
           contextPackage: input.contextPackage,
           contextPackage: input.contextPackage,
           ruleStack: input.ruleStack,
           ruleStack: input.ruleStack,
           trace: input.trace,
           trace: input.trace,
@@ -816,9 +814,8 @@ ${lengthRequirementBlock}
 
 
   private buildGovernedUserPrompt(params: {
   private buildGovernedUserPrompt(params: {
     readonly chapterNumber: number;
     readonly chapterNumber: number;
-    readonly chapterBrief: ChapterBrief;
-    readonly moodDirective?: string;
-    readonly titleDirective?: string;
+    readonly chapterMemo: ChapterMemo;
+    readonly chapterIntentData?: ChapterIntent;
     readonly contextPackage: ContextPackage;
     readonly contextPackage: ContextPackage;
     readonly ruleStack: RuleStack;
     readonly ruleStack: RuleStack;
     readonly trace?: ChapterTrace;
     readonly trace?: ChapterTrace;
@@ -853,14 +850,12 @@ ${lengthRequirementBlock}
     const selectedEvidenceBlock = params.selectedEvidenceBlock
     const selectedEvidenceBlock = params.selectedEvidenceBlock
       ? `\n${sanitizeNarrativeEvidenceBlock(params.selectedEvidenceBlock, language)}\n`
       ? `\n${sanitizeNarrativeEvidenceBlock(params.selectedEvidenceBlock, language)}\n`
       : "";
       : "";
-    const briefNarrative = renderBriefAsNarrativeBlock(params.chapterBrief, language);
-    const cadenceBlock = this.buildCadenceDirectiveBlock(params.moodDirective, params.titleDirective, language);
+    const briefNarrative = renderMemoAsNarrativeBlock(params.chapterMemo, params.chapterIntentData, language);
 
 
     if (params.language === "en") {
     if (params.language === "en") {
       return `Write chapter ${params.chapterNumber}.
       return `Write chapter ${params.chapterNumber}.
 
 
 ${briefNarrative}
 ${briefNarrative}
-${cadenceBlock}
 
 
 ## Selected Context
 ## Selected Context
 ${contextSections || "(none)"}
 ${contextSections || "(none)"}
@@ -886,7 +881,6 @@ ${lengthRequirementBlock}
     return `请续写第${params.chapterNumber}章。
     return `请续写第${params.chapterNumber}章。
 
 
 ${briefNarrative}
 ${briefNarrative}
-${cadenceBlock}
 
 
 ## 已选上下文
 ## 已选上下文
 ${contextSections || "(无)"}
 ${contextSections || "(无)"}
@@ -929,24 +923,6 @@ ${lengthRequirementBlock}
     return joined || undefined;
     return joined || undefined;
   }
   }
 
 
-  private buildCadenceDirectiveBlock(
-    moodDirective: string | undefined,
-    titleDirective: string | undefined,
-    language: "zh" | "en",
-  ): string {
-    if (!moodDirective && !titleDirective) return "";
-    const isEn = language === "en";
-    const lines: string[] = [];
-    if (moodDirective) {
-      lines.push(`- ${isEn ? "rhythm" : "节奏"}: ${moodDirective}`);
-    }
-    if (titleDirective) {
-      lines.push(`- ${isEn ? "title" : "标题提示"}: ${titleDirective}`);
-    }
-    const heading = isEn ? "## Cadence Directives" : "## 节奏指令";
-    return `\n${heading}\n${lines.join("\n")}`;
-  }
-
   private buildSettlerGovernedControlBlock(
   private buildSettlerGovernedControlBlock(
     chapterIntent: string,
     chapterIntent: string,
     contextPackage: ContextPackage,
     contextPackage: ContextPackage,

+ 2 - 16
packages/core/src/index.ts

@@ -37,14 +37,7 @@ export {
   RuntimeStateDeltaSchema,
   RuntimeStateDeltaSchema,
 } from "./models/runtime-state.js";
 } from "./models/runtime-state.js";
 export {
 export {
-  type ChapterConflict,
-  type ChapterBeatPhase,
-  type ChapterBeat,
-  type ChapterHookMove,
-  type ChapterBrief,
-  type HookMovement,
-  type HookPressureLevel,
-  type HookPressure,
+  type ChapterMemo,
   type ChapterIntent,
   type ChapterIntent,
   type ContextSource,
   type ContextSource,
   type ContextPackage,
   type ContextPackage,
@@ -55,14 +48,7 @@ export {
   type RuleStackSections,
   type RuleStackSections,
   type RuleStack,
   type RuleStack,
   type ChapterTrace,
   type ChapterTrace,
-  ChapterConflictSchema,
-  ChapterBeatPhaseSchema,
-  ChapterBeatSchema,
-  ChapterHookMoveSchema,
-  ChapterBriefSchema,
-  HookMovementSchema,
-  HookPressureLevelSchema,
-  HookPressureSchema,
+  ChapterMemoSchema,
   ChapterIntentSchema,
   ChapterIntentSchema,
   ContextSourceSchema,
   ContextSourceSchema,
   ContextPackageSchema,
   ContextPackageSchema,

+ 5 - 105
packages/core/src/models/input-governance.ts

@@ -1,123 +1,23 @@
 import { z } from "zod";
 import { z } from "zod";
-import { HookPayoffTimingSchema } from "./runtime-state.js";
 
 
-export const ChapterConflictSchema = z.object({
-  type: z.string().min(1),
-  resolution: z.string().min(1),
-  detail: z.string().optional(),
-});
-
-export type ChapterConflict = z.infer<typeof ChapterConflictSchema>;
-
-export const HookPressurePhaseSchema = z.enum(["opening", "middle", "late"]);
-export type HookPressurePhase = z.infer<typeof HookPressurePhaseSchema>;
-
-export const HookMovementSchema = z.enum([
-  "quiet-hold",
-  "refresh",
-  "advance",
-  "partial-payoff",
-  "full-payoff",
-]);
-export type HookMovement = z.infer<typeof HookMovementSchema>;
-
-export const HookPressureLevelSchema = z.enum(["low", "medium", "high", "critical"]);
-export type HookPressureLevel = z.infer<typeof HookPressureLevelSchema>;
-
-export const HookPressureReasonSchema = z.enum([
-  "fresh-promise",
-  "building-debt",
-  "stale-promise",
-  "ripe-payoff",
-  "overdue-payoff",
-  "long-arc-hold",
-]);
-export type HookPressureReason = z.infer<typeof HookPressureReasonSchema>;
-
-export const HookPressureSchema = z.object({
-  hookId: z.string().min(1),
-  type: z.string().min(1),
-  movement: HookMovementSchema,
-  pressure: HookPressureLevelSchema,
-  payoffTiming: HookPayoffTimingSchema.optional(),
-  phase: HookPressurePhaseSchema,
-  reason: HookPressureReasonSchema,
-  blockSiblingHooks: z.boolean().default(false),
-});
-
-export type HookPressure = z.infer<typeof HookPressureSchema>;
-
-export const HookAgendaSchema = z.object({
-  pressureMap: z.array(HookPressureSchema).default([]),
-  mustAdvance: z.array(z.string().min(1)).default([]),
-  eligibleResolve: z.array(z.string().min(1)).default([]),
-  staleDebt: z.array(z.string().min(1)).default([]),
-  avoidNewHookFamilies: z.array(z.string().min(1)).default([]),
-});
-
-export type HookAgenda = z.infer<typeof HookAgendaSchema>;
-
-export const ChapterBeatPhaseSchema = z.enum([
-  "opening",
-  "development",
-  "reversal",
-  "payoff",
-  "hook",
-]);
-export type ChapterBeatPhase = z.infer<typeof ChapterBeatPhaseSchema>;
-
-export const ChapterBeatSchema = z.object({
-  phase: ChapterBeatPhaseSchema,
-  instruction: z.string().min(1),
-});
-export type ChapterBeat = z.infer<typeof ChapterBeatSchema>;
-
-export const ChapterHookMoveSchema = z.object({
-  hookId: z.string().min(1),
-  movement: HookMovementSchema,
-  targetEffect: z.string().min(1),
-});
-export type ChapterHookMove = z.infer<typeof ChapterHookMoveSchema>;
-
-export const CyclePhaseSchema = z.enum([
-  "蓄压", "升级", "爆发", "后效",
-  "build-up", "escalation", "climax", "aftermath",
-]);
-export type CyclePhase = z.infer<typeof CyclePhaseSchema>;
-
-export const ChapterBriefSchema = z.object({
+export const ChapterMemoSchema = z.object({
   chapter: z.number().int().min(1),
   chapter: z.number().int().min(1),
   goal: z.string().min(1),
   goal: z.string().min(1),
-  chapterType: z.string().min(1),
-  cyclePhase: CyclePhaseSchema.optional(),
   isGoldenOpening: z.boolean().default(false),
   isGoldenOpening: z.boolean().default(false),
-  beatOutline: z.array(ChapterBeatSchema).min(1),
-  hookPlan: z.array(ChapterHookMoveSchema).default([]),
-  dormantReason: z.string().optional(),
-  propsAndSetting: z.array(z.string().min(1)).default([]),
+  body: z.string(),
+  hookRefs: z.array(z.string()).default([]),
 });
 });
 
 
-export type ChapterBrief = z.infer<typeof ChapterBriefSchema>;
+export type ChapterMemo = z.infer<typeof ChapterMemoSchema>;
 
 
 export const ChapterIntentSchema = z.object({
 export const ChapterIntentSchema = z.object({
   chapter: z.number().int().min(1),
   chapter: z.number().int().min(1),
   goal: z.string().min(1),
   goal: z.string().min(1),
   outlineNode: z.string().optional(),
   outlineNode: z.string().optional(),
-  sceneDirective: z.string().min(1).optional(),
-  arcDirective: z.string().min(1).optional(),
-  moodDirective: z.string().min(1).optional(),
-  titleDirective: z.string().min(1).optional(),
+  arcContext: z.string().optional(),
   mustKeep: z.array(z.string()).default([]),
   mustKeep: z.array(z.string()).default([]),
   mustAvoid: z.array(z.string()).default([]),
   mustAvoid: z.array(z.string()).default([]),
   styleEmphasis: z.array(z.string()).default([]),
   styleEmphasis: z.array(z.string()).default([]),
-  conflicts: z.array(ChapterConflictSchema).default([]),
-  hookAgenda: HookAgendaSchema.default({
-    pressureMap: [],
-    mustAdvance: [],
-    eligibleResolve: [],
-    staleDebt: [],
-    avoidNewHookFamilies: [],
-  }),
 });
 });
 
 
 export type ChapterIntent = z.infer<typeof ChapterIntentSchema>;
 export type ChapterIntent = z.infer<typeof ChapterIntentSchema>;

+ 5 - 3
packages/core/src/pipeline/chapter-review-cycle.ts

@@ -1,7 +1,7 @@
 import type { AuditIssue, AuditResult } from "../agents/continuity.js";
 import type { AuditIssue, AuditResult } from "../agents/continuity.js";
 import type { ReviseMode, ReviseOutput } from "../agents/reviser.js";
 import type { ReviseMode, ReviseOutput } from "../agents/reviser.js";
 import type { WriteChapterOutput } from "../agents/writer.js";
 import type { WriteChapterOutput } from "../agents/writer.js";
-import type { ChapterBrief, ContextPackage, RuleStack } from "../models/input-governance.js";
+import type { ChapterIntent, ChapterMemo, ContextPackage, RuleStack } from "../models/input-governance.js";
 import type { LengthSpec } from "../models/length-governance.js";
 import type { LengthSpec } from "../models/length-governance.js";
 import { countChapterLength, isOutsideSoftRange } from "../utils/length-metrics.js";
 import { countChapterLength, isOutsideSoftRange } from "../utils/length-metrics.js";
 
 
@@ -13,7 +13,8 @@ export interface ChapterReviewCycleUsage {
 
 
 export interface ChapterReviewCycleControlInput {
 export interface ChapterReviewCycleControlInput {
   readonly chapterIntent: string;
   readonly chapterIntent: string;
-  readonly chapterBrief?: ChapterBrief;
+  readonly chapterMemo?: ChapterMemo;
+  readonly chapterIntentData?: ChapterIntent;
   readonly contextPackage: ContextPackage;
   readonly contextPackage: ContextPackage;
   readonly ruleStack: RuleStack;
   readonly ruleStack: RuleStack;
 }
 }
@@ -58,7 +59,8 @@ export async function runChapterReviewCycle(params: {
       genre?: string,
       genre?: string,
       options?: {
       options?: {
         chapterIntent?: string;
         chapterIntent?: string;
-        chapterBrief?: ChapterBrief;
+        chapterMemo?: ChapterMemo;
+        chapterIntentData?: ChapterIntent;
         contextPackage?: ContextPackage;
         contextPackage?: ContextPackage;
         ruleStack?: RuleStack;
         ruleStack?: RuleStack;
         lengthSpec?: LengthSpec;
         lengthSpec?: LengthSpec;

+ 12 - 200
packages/core/src/pipeline/persisted-governed-plan.ts

@@ -1,209 +1,21 @@
-import { readFile } from "node:fs/promises";
-import { join, relative } from "node:path";
+import { relative } from "node:path";
 import type { PlanChapterOutput } from "../agents/planner.js";
 import type { PlanChapterOutput } from "../agents/planner.js";
-import { ChapterBriefSchema, ChapterIntentSchema } from "../models/input-governance.js";
 
 
+/**
+ * Phase 1: persisted governed plans are disabled.
+ *
+ * The previous implementation serialized ChapterBrief/HookAgenda into the
+ * intent markdown, but those schemas no longer exist. Rather than write a
+ * migration, we return null so the pipeline regenerates the plan from scratch.
+ * Phase 3 will introduce a new memo-based persistence format if needed.
+ */
 export async function loadPersistedPlan(
 export async function loadPersistedPlan(
-  bookDir: string,
-  chapterNumber: number,
+  _bookDir: string,
+  _chapterNumber: number,
 ): Promise<PlanChapterOutput | null> {
 ): Promise<PlanChapterOutput | null> {
-  const runtimePath = join(
-    bookDir,
-    "story",
-    "runtime",
-    `chapter-${String(chapterNumber).padStart(4, "0")}.intent.md`,
-  );
-
-  try {
-    const intentMarkdown = await readFile(runtimePath, "utf-8");
-    const sections = parseIntentSections(intentMarkdown);
-    const goal = readIntentScalar(sections, "Goal");
-    if (!goal || isInvalidPersistedIntentScalar(goal)) return null;
-
-    const outlineNode = readIntentScalar(sections, "Outline Node");
-    if (outlineNode && outlineNode !== "(not found)" && isInvalidPersistedIntentScalar(outlineNode)) {
-      return null;
-    }
-    const conflicts = readIntentList(sections, "Conflicts")
-      .map((line) => {
-        const separator = line.indexOf(":");
-        if (separator < 0) return null;
-
-        const type = line.slice(0, separator).trim();
-        const resolution = line.slice(separator + 1).trim();
-        if (!type || !resolution) return null;
-        return { type, resolution };
-      })
-      .filter((conflict): conflict is { type: string; resolution: string } => conflict !== null);
-
-    const brief = parsePersistedChapterBrief(sections, chapterNumber);
-    if (!brief) {
-      return null;
-    }
-
-    // Restore structured directives (mood, scene, arc, title) from persisted markdown
-    const directiveLines = readIntentList(sections, "Structured Directives");
-    const directiveMap = new Map<string, string>();
-    for (const line of directiveLines) {
-      const sep = line.indexOf(":");
-      if (sep > 0) {
-        directiveMap.set(line.slice(0, sep).trim(), line.slice(sep + 1).trim());
-      }
-    }
-
-    return {
-      intent: ChapterIntentSchema.parse({
-        chapter: chapterNumber,
-        goal,
-        outlineNode: outlineNode && outlineNode !== "(not found)" ? outlineNode : undefined,
-        mustKeep: readIntentList(sections, "Must Keep"),
-        mustAvoid: readIntentList(sections, "Must Avoid"),
-        styleEmphasis: readIntentList(sections, "Style Emphasis"),
-        sceneDirective: directiveMap.get("scene"),
-        arcDirective: directiveMap.get("arc"),
-        moodDirective: directiveMap.get("mood"),
-        titleDirective: directiveMap.get("title"),
-        conflicts,
-      }),
-      brief,
-      intentMarkdown,
-      plannerInputs: [runtimePath],
-      runtimePath,
-    };
-  } catch {
-    return null;
-  }
+  return null;
 }
 }
 
 
 export function relativeToBookDir(bookDir: string, absolutePath: string): string {
 export function relativeToBookDir(bookDir: string, absolutePath: string): string {
   return relative(bookDir, absolutePath).replaceAll("\\", "/");
   return relative(bookDir, absolutePath).replaceAll("\\", "/");
 }
 }
-
-function parseIntentSections(markdown: string): Map<string, string[]> {
-  const sections = new Map<string, string[]>();
-  let current: string | null = null;
-
-  for (const line of markdown.split("\n")) {
-    if (line.startsWith("## ")) {
-      current = line.slice(3).trim();
-      sections.set(current, []);
-      continue;
-    }
-
-    if (!current) continue;
-    sections.get(current)?.push(line);
-  }
-
-  return sections;
-}
-
-function readIntentScalar(sections: Map<string, string[]>, name: string): string | undefined {
-  const lines = sections.get(name) ?? [];
-  const value = lines.map((line) => line.trim()).find((line) => line.length > 0);
-  return value && value !== "- none" ? value : undefined;
-}
-
-function readIntentList(sections: Map<string, string[]>, name: string): string[] {
-  return (sections.get(name) ?? [])
-    .map((line) => line.trim())
-    .filter((line) => line.startsWith("-") && line !== "- none")
-    .map((line) => line.replace(/^-\s*/, ""));
-}
-
-function parsePersistedChapterBrief(
-  sections: Map<string, string[]>,
-  chapterNumber: number,
-) {
-  const lines = sections.get("Chapter Brief") ?? [];
-  if (lines.length === 0) {
-    return undefined;
-  }
-
-  const scalarMap = new Map<string, string>();
-  const beatOutline: Array<{ phase: "opening" | "development" | "reversal" | "payoff" | "hook"; instruction: string }> = [];
-  const hookPlan: Array<{ hookId: string; movement: "quiet-hold" | "refresh" | "advance" | "partial-payoff" | "full-payoff"; targetEffect: string }> = [];
-  const propsAndSetting: string[] = [];
-
-  let currentSubsection: string | null = null;
-  for (const rawLine of lines) {
-    const line = rawLine.trim();
-    if (!line) continue;
-
-    if (line.startsWith("### ")) {
-      currentSubsection = line.slice(4).trim();
-      continue;
-    }
-
-    if (currentSubsection === null && line.startsWith("- ")) {
-      const separator = line.indexOf(":");
-      if (separator > 2) {
-        scalarMap.set(line.slice(2, separator).trim(), line.slice(separator + 1).trim());
-      }
-      continue;
-    }
-
-    if (currentSubsection === "Beat Outline" && line.startsWith("- ")) {
-      const separator = line.indexOf(":");
-      if (separator > 2) {
-        const phase = line.slice(2, separator).trim();
-        const instruction = line.slice(separator + 1).trim();
-        if (phase && instruction) {
-          beatOutline.push({
-            phase: phase as "opening" | "development" | "reversal" | "payoff" | "hook",
-            instruction,
-          });
-        }
-      }
-      continue;
-    }
-
-    if (currentSubsection === "Hook Plan" && line.startsWith("- ") && line !== "- none") {
-      const [left, right] = line.slice(2).split("->").map((part) => part.trim());
-      if (left && right) {
-        const [hookId, movement] = left.split(":").map((part) => part.trim());
-        if (hookId && movement) {
-          hookPlan.push({
-            hookId,
-            movement: movement as "quiet-hold" | "refresh" | "advance" | "partial-payoff" | "full-payoff",
-            targetEffect: right,
-          });
-        }
-      }
-      continue;
-    }
-
-    if (currentSubsection === "Props And Setting" && line.startsWith("- ") && line !== "- none") {
-      propsAndSetting.push(line.replace(/^-\s*/, ""));
-    }
-  }
-
-  if (beatOutline.length === 0) {
-    return undefined;
-  }
-
-  const cyclePhaseRaw = scalarMap.get("cyclePhase");
-  const validCyclePhases = new Set(["蓄压", "升级", "爆发", "后效", "build-up", "escalation", "climax", "aftermath"]);
-  const cyclePhase = cyclePhaseRaw && validCyclePhases.has(cyclePhaseRaw) ? cyclePhaseRaw : undefined;
-
-  return ChapterBriefSchema.parse({
-    chapter: chapterNumber,
-    goal: readIntentScalar(sections, "Goal") ?? scalarMap.get("goal"),
-    chapterType: scalarMap.get("chapterType") ?? "unspecified",
-    isGoldenOpening: scalarMap.get("isGoldenOpening") === "true",
-    cyclePhase,
-    beatOutline,
-    hookPlan,
-    propsAndSetting,
-    dormantReason: scalarMap.get("dormantReason"),
-  });
-}
-
-function 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)
-  );
-}

+ 9 - 8
packages/core/src/pipeline/runner.ts

@@ -728,7 +728,7 @@ export class PipelineRunner {
       chapterNumber,
       chapterNumber,
       intentPath: relativeToBookDir(bookDir, plan.runtimePath),
       intentPath: relativeToBookDir(bookDir, plan.runtimePath),
       goal: plan.intent.goal,
       goal: plan.intent.goal,
-      conflicts: plan.intent.conflicts.map((conflict) => `${conflict.type}: ${conflict.resolution}`),
+      conflicts: [],
     };
     };
   }
   }
 
 
@@ -752,7 +752,7 @@ export class PipelineRunner {
       chapterNumber,
       chapterNumber,
       intentPath: relativeToBookDir(bookDir, plan.runtimePath),
       intentPath: relativeToBookDir(bookDir, plan.runtimePath),
       goal: plan.intent.goal,
       goal: plan.intent.goal,
-      conflicts: plan.intent.conflicts.map((conflict) => `${conflict.type}: ${conflict.resolution}`),
+      conflicts: [],
       contextPath: relativeToBookDir(bookDir, composed.contextPath),
       contextPath: relativeToBookDir(bookDir, composed.contextPath),
       ruleStackPath: relativeToBookDir(bookDir, composed.ruleStackPath),
       ruleStackPath: relativeToBookDir(bookDir, composed.ruleStackPath),
       tracePath: relativeToBookDir(bookDir, composed.tracePath),
       tracePath: relativeToBookDir(bookDir, composed.tracePath),
@@ -908,7 +908,8 @@ export class PipelineRunner {
         reviseControlInput
         reviseControlInput
           ? {
           ? {
               chapterIntent: reviseControlInput.plan.intentMarkdown,
               chapterIntent: reviseControlInput.plan.intentMarkdown,
-              chapterBrief: reviseControlInput.plan.brief,
+              chapterMemo: reviseControlInput.plan.memo,
+              chapterIntentData: reviseControlInput.plan.intent,
               contextPackage: reviseControlInput.composed.contextPackage,
               contextPackage: reviseControlInput.composed.contextPackage,
               ruleStack: reviseControlInput.composed.ruleStack,
               ruleStack: reviseControlInput.composed.ruleStack,
               lengthSpec,
               lengthSpec,
@@ -1169,7 +1170,8 @@ export class PipelineRunner {
     const reducedControlInput = writeInput.chapterIntent && writeInput.contextPackage && writeInput.ruleStack
     const reducedControlInput = writeInput.chapterIntent && writeInput.contextPackage && writeInput.ruleStack
       ? {
       ? {
           chapterIntent: writeInput.chapterIntent,
           chapterIntent: writeInput.chapterIntent,
-          chapterBrief: writeInput.chapterBrief,
+          chapterMemo: writeInput.chapterMemo,
+          chapterIntentData: writeInput.chapterIntentData,
           contextPackage: writeInput.contextPackage,
           contextPackage: writeInput.contextPackage,
           ruleStack: writeInput.ruleStack,
           ruleStack: writeInput.ruleStack,
         }
         }
@@ -2068,7 +2070,7 @@ ${matrix}`,
     bookDir: string,
     bookDir: string,
     chapterNumber: number,
     chapterNumber: number,
     externalContext?: string,
     externalContext?: string,
-  ): Promise<Pick<WriteChapterInput, "externalContext" | "chapterIntent" | "chapterBrief" | "moodDirective" | "titleDirective" | "contextPackage" | "ruleStack" | "trace">> {
+  ): Promise<Pick<WriteChapterInput, "externalContext" | "chapterIntent" | "chapterMemo" | "chapterIntentData" | "contextPackage" | "ruleStack" | "trace">> {
     if ((this.config.inputGovernanceMode ?? "v2") === "legacy") {
     if ((this.config.inputGovernanceMode ?? "v2") === "legacy") {
       return { externalContext };
       return { externalContext };
     }
     }
@@ -2083,9 +2085,8 @@ ${matrix}`,
 
 
     return {
     return {
       chapterIntent: plan.intentMarkdown,
       chapterIntent: plan.intentMarkdown,
-      chapterBrief: plan.brief,
-      moodDirective: plan.intent.moodDirective,
-      titleDirective: plan.intent.titleDirective,
+      chapterMemo: plan.memo,
+      chapterIntentData: plan.intent,
       contextPackage: composed.contextPackage,
       contextPackage: composed.contextPackage,
       ruleStack: composed.ruleStack,
       ruleStack: composed.ruleStack,
       trace: composed.trace,
       trace: composed.trace,

+ 4 - 7
packages/core/src/utils/context-assembly.ts

@@ -10,6 +10,8 @@ import {
 import type { PlanChapterOutput } from "../agents/planner.js";
 import type { PlanChapterOutput } from "../agents/planner.js";
 
 
 export function buildGovernedRuleStack(plan: PlanChapterOutput, chapterNumber: number): RuleStack {
 export function buildGovernedRuleStack(plan: PlanChapterOutput, chapterNumber: number): RuleStack {
+  void plan;
+  void chapterNumber;
   return RuleStackSchema.parse({
   return RuleStackSchema.parse({
     layers: [
     layers: [
       { id: "L1", name: "hard_facts", precedence: 100, scope: "global" },
       { id: "L1", name: "hard_facts", precedence: 100, scope: "global" },
@@ -27,12 +29,7 @@ export function buildGovernedRuleStack(plan: PlanChapterOutput, chapterNumber: n
       { from: "L4", to: "L2", allowed: false, scope: "current_chapter" },
       { from: "L4", to: "L2", allowed: false, scope: "current_chapter" },
       { from: "L4", to: "L1", allowed: false, scope: "current_chapter" },
       { from: "L4", to: "L1", allowed: false, scope: "current_chapter" },
     ],
     ],
-    activeOverrides: plan.intent.conflicts.map((conflict) => ({
-      from: "L4",
-      to: "L3",
-      target: plan.intent.outlineNode ?? `chapter_${chapterNumber}`,
-      reason: conflict.resolution,
-    })),
+    activeOverrides: [],
   });
   });
 }
 }
 
 
@@ -47,6 +44,6 @@ export function buildGovernedTrace(params: {
     plannerInputs: params.plan.plannerInputs,
     plannerInputs: params.plan.plannerInputs,
     composerInputs: params.composerInputs,
     composerInputs: params.composerInputs,
     selectedSources: params.contextPackage.selectedContext.map((entry) => entry.source),
     selectedSources: params.contextPackage.selectedContext.map((entry) => entry.source),
-    notes: params.plan.intent.conflicts.map((conflict) => conflict.resolution),
+    notes: [],
   });
   });
 }
 }

+ 1 - 1
packages/core/src/utils/governed-working-set.ts

@@ -5,7 +5,7 @@ import {
 } from "./memory-retrieval.js";
 } from "./memory-retrieval.js";
 import {
 import {
   isHookWithinChapterWindow,
   isHookWithinChapterWindow,
-} from "./hook-agenda.js";
+} from "./hook-lifecycle.js";
 
 
 export function buildGovernedHookWorkingSet(params: {
 export function buildGovernedHookWorkingSet(params: {
   readonly hooksMarkdown: string;
   readonly hooksMarkdown: string;

+ 0 - 173
packages/core/src/utils/hook-agenda.ts

@@ -1,173 +0,0 @@
-import type { HookAgenda } from "../models/input-governance.js";
-import type { HookRecord, HookStatus } from "../models/runtime-state.js";
-import type { StoredHook } from "../state/memory-db.js";
-import { describeHookLifecycle, resolveHookPayoffTiming } from "./hook-lifecycle.js";
-import { HOOK_AGENDA_LIMITS, HOOK_AGENDA_LOAD_THRESHOLDS, type HookAgendaLoad } from "./hook-policy.js";
-
-export const DEFAULT_HOOK_LOOKAHEAD_CHAPTERS = 3;
-
-/**
- * Build the hook agenda using lifecycle-aware scheduling.
- * Uses payoffTiming profiles to differentiate short/long hooks:
- * - immediate hooks trigger resolve pressure after 1-3 chapters
- * - slow-burn hooks stay dormant until middle phase
- * - endgame hooks only resolve in late phase
- */
-export function buildPlannerHookAgenda(params: {
-  readonly hooks: ReadonlyArray<StoredHook>;
-  readonly chapterNumber: number;
-  readonly targetChapters?: number;
-  readonly language?: "zh" | "en";
-  readonly maxMustAdvance?: number;
-  readonly maxEligibleResolve?: number;
-  readonly maxStaleDebt?: number;
-}): HookAgenda {
-  const agendaHooks = params.hooks
-    .map(normalizeStoredHook)
-    .filter((hook) => !isFuturePlannedHook(hook, params.chapterNumber, 0))
-    .filter((hook) => hook.status !== "resolved" && hook.status !== "deferred");
-
-  // Compute lifecycle for each hook
-  const withLifecycle = agendaHooks.map((hook) => ({
-    hook,
-    lifecycle: describeHookLifecycle({
-      payoffTiming: hook.payoffTiming,
-      expectedPayoff: hook.expectedPayoff,
-      notes: hook.notes,
-      startChapter: hook.startChapter,
-      lastAdvancedChapter: hook.lastAdvancedChapter,
-      status: hook.status,
-      chapterNumber: params.chapterNumber,
-      targetChapters: params.targetChapters,
-    }),
-  }));
-
-  // Determine agenda load
-  const load = resolveAgendaLoad(withLifecycle);
-  const limits = HOOK_AGENDA_LIMITS[load];
-
-  // eligibleResolve: lifecycle says readyToResolve, sorted by resolvePressure desc
-  const eligibleResolveHooks = withLifecycle
-    .filter((entry) => entry.lifecycle.readyToResolve)
-    .sort((left, right) => right.lifecycle.resolvePressure - left.lifecycle.resolvePressure)
-    .slice(0, params.maxEligibleResolve ?? limits.eligibleResolve);
-
-  // staleDebt: lifecycle says stale or overdue, sorted by advancePressure desc
-  const resolveIds = new Set(eligibleResolveHooks.map((entry) => entry.hook.hookId));
-  const staleDebtHooks = withLifecycle
-    .filter((entry) => (entry.lifecycle.stale || entry.lifecycle.overdue) && !resolveIds.has(entry.hook.hookId))
-    .sort((left, right) => right.lifecycle.advancePressure - left.lifecycle.advancePressure)
-    .slice(0, params.maxStaleDebt ?? limits.staleDebt);
-
-  // mustAdvance: highest advancePressure, excluding those already in resolve/stale
-  const usedIds = new Set([
-    ...resolveIds,
-    ...staleDebtHooks.map((entry) => entry.hook.hookId),
-  ]);
-  const mustAdvanceHooks = withLifecycle
-    .filter((entry) => !usedIds.has(entry.hook.hookId))
-    .sort((left, right) => right.lifecycle.advancePressure - left.lifecycle.advancePressure)
-    .slice(0, params.maxMustAdvance ?? limits.mustAdvance);
-
-  // avoidNewHookFamilies: families with stale/overdue/pressured hooks
-  const pressuredFamilies = withLifecycle
-    .filter((entry) => entry.lifecycle.stale || entry.lifecycle.overdue)
-    .map((entry) => entry.hook.type.trim())
-    .filter(Boolean);
-  const avoidNewHookFamilies = [...new Set(pressuredFamilies)].slice(0, limits.avoidFamilies);
-
-  return {
-    pressureMap: [],
-    mustAdvance: mustAdvanceHooks.map((entry) => entry.hook.hookId),
-    eligibleResolve: eligibleResolveHooks.map((entry) => entry.hook.hookId),
-    staleDebt: staleDebtHooks.map((entry) => entry.hook.hookId),
-    avoidNewHookFamilies,
-  };
-}
-
-function resolveAgendaLoad(
-  entries: ReadonlyArray<{ lifecycle: ReturnType<typeof describeHookLifecycle> }>,
-): HookAgendaLoad {
-  const readyCount = entries.filter((e) => e.lifecycle.readyToResolve).length;
-  const staleCount = entries.filter((e) => e.lifecycle.stale).length;
-  const overdueCount = entries.filter((e) => e.lifecycle.overdue).length;
-  const pressuredCount = entries.filter((e) => e.lifecycle.stale || e.lifecycle.overdue).length;
-
-  if (
-    readyCount >= HOOK_AGENDA_LOAD_THRESHOLDS.heavyReadyCount
-    || staleCount >= HOOK_AGENDA_LOAD_THRESHOLDS.heavyStaleCount
-    || overdueCount >= HOOK_AGENDA_LOAD_THRESHOLDS.heavyCriticalCount
-    || pressuredCount >= HOOK_AGENDA_LOAD_THRESHOLDS.heavyPressuredCount
-  ) {
-    return "heavy";
-  }
-
-  if (
-    readyCount >= HOOK_AGENDA_LOAD_THRESHOLDS.mediumReadyCount
-    || staleCount >= HOOK_AGENDA_LOAD_THRESHOLDS.mediumStaleCount
-    || overdueCount >= HOOK_AGENDA_LOAD_THRESHOLDS.mediumCriticalCount
-  ) {
-    return "medium";
-  }
-
-  return "light";
-}
-
-function normalizeStoredHook(hook: StoredHook): HookRecord {
-  return {
-    hookId: hook.hookId,
-    startChapter: Math.max(0, hook.startChapter),
-    type: hook.type,
-    status: normalizeStoredHookStatus(hook.status),
-    lastAdvancedChapter: Math.max(0, hook.lastAdvancedChapter),
-    expectedPayoff: hook.expectedPayoff,
-    payoffTiming: resolveHookPayoffTiming(hook),
-    notes: hook.notes,
-  };
-}
-
-function normalizeStoredHookStatus(status: string): HookStatus {
-  if (/^(resolved|closed|done|已回收|已解决)$/i.test(status.trim())) return "resolved";
-  if (/^(deferred|paused|hold|延后|延期|搁置|暂缓)$/i.test(status.trim())) return "deferred";
-  if (/^(progressing|advanced|重大推进|持续推进)$/i.test(status.trim())) return "progressing";
-  return "open";
-}
-
-export function filterActiveHooks(hooks: ReadonlyArray<StoredHook>): StoredHook[] {
-  return hooks.filter((hook) => normalizeStoredHookStatus(hook.status) !== "resolved");
-}
-
-export function isFuturePlannedHook(
-  hook: StoredHook,
-  chapterNumber: number,
-  lookahead: number = DEFAULT_HOOK_LOOKAHEAD_CHAPTERS,
-): boolean {
-  return hook.lastAdvancedChapter <= 0 && hook.startChapter > chapterNumber + lookahead;
-}
-
-export function isHookWithinChapterWindow(
-  hook: StoredHook,
-  chapterNumber: number,
-  recentWindow: number = 5,
-  lookahead: number = DEFAULT_HOOK_LOOKAHEAD_CHAPTERS,
-): boolean {
-  const recentCutoff = Math.max(0, chapterNumber - recentWindow);
-
-  if (hook.lastAdvancedChapter > 0 && hook.lastAdvancedChapter >= recentCutoff) {
-    return true;
-  }
-
-  if (hook.lastAdvancedChapter > 0) {
-    return false;
-  }
-
-  if (hook.startChapter <= 0) {
-    return true;
-  }
-
-  if (hook.startChapter >= recentCutoff && hook.startChapter <= chapterNumber) {
-    return true;
-  }
-
-  return hook.startChapter > chapterNumber && hook.startChapter <= chapterNumber + lookahead;
-}

+ 49 - 0
packages/core/src/utils/hook-lifecycle.ts

@@ -1,4 +1,5 @@
 import type { HookPayoffTiming } from "../models/runtime-state.js";
 import type { HookPayoffTiming } from "../models/runtime-state.js";
+import type { StoredHook } from "../state/memory-db.js";
 import {
 import {
   HOOK_ACTIVITY_THRESHOLDS,
   HOOK_ACTIVITY_THRESHOLDS,
   HOOK_PHASE_THRESHOLDS,
   HOOK_PHASE_THRESHOLDS,
@@ -8,6 +9,54 @@ import {
   type HookPhase,
   type HookPhase,
 } from "./hook-policy.js";
 } from "./hook-policy.js";
 
 
+export const DEFAULT_HOOK_LOOKAHEAD_CHAPTERS = 3;
+
+function normalizeStoredHookStatus(status: string): "resolved" | "deferred" | "progressing" | "open" {
+  if (/^(resolved|closed|done|已回收|已解决)$/i.test(status.trim())) return "resolved";
+  if (/^(deferred|paused|hold|延后|延期|搁置|暂缓)$/i.test(status.trim())) return "deferred";
+  if (/^(progressing|advanced|重大推进|持续推进)$/i.test(status.trim())) return "progressing";
+  return "open";
+}
+
+export function filterActiveHooks(hooks: ReadonlyArray<StoredHook>): StoredHook[] {
+  return hooks.filter((hook) => normalizeStoredHookStatus(hook.status) !== "resolved");
+}
+
+export function isFuturePlannedHook(
+  hook: StoredHook,
+  chapterNumber: number,
+  lookahead: number = DEFAULT_HOOK_LOOKAHEAD_CHAPTERS,
+): boolean {
+  return hook.lastAdvancedChapter <= 0 && hook.startChapter > chapterNumber + lookahead;
+}
+
+export function isHookWithinChapterWindow(
+  hook: StoredHook,
+  chapterNumber: number,
+  recentWindow: number = 5,
+  lookahead: number = DEFAULT_HOOK_LOOKAHEAD_CHAPTERS,
+): boolean {
+  const recentCutoff = Math.max(0, chapterNumber - recentWindow);
+
+  if (hook.lastAdvancedChapter > 0 && hook.lastAdvancedChapter >= recentCutoff) {
+    return true;
+  }
+
+  if (hook.lastAdvancedChapter > 0) {
+    return false;
+  }
+
+  if (hook.startChapter <= 0) {
+    return true;
+  }
+
+  if (hook.startChapter >= recentCutoff && hook.startChapter <= chapterNumber) {
+    return true;
+  }
+
+  return hook.startChapter > chapterNumber && hook.startChapter <= chapterNumber + lookahead;
+}
+
 const LABELS: Record<"zh" | "en", Record<HookPayoffTiming, string>> = {
 const LABELS: Record<"zh" | "en", Record<HookPayoffTiming, string>> = {
   en: {
   en: {
     immediate: "immediate",
     immediate: "immediate",

+ 0 - 38
packages/core/src/utils/hook-policy.ts

@@ -1,7 +1,6 @@
 import type { HookPayoffTiming } from "../models/runtime-state.js";
 import type { HookPayoffTiming } from "../models/runtime-state.js";
 
 
 export type HookPhase = "opening" | "middle" | "late";
 export type HookPhase = "opening" | "middle" | "late";
-export type HookAgendaLoad = "light" | "medium" | "heavy";
 
 
 export interface HookLifecycleProfile {
 export interface HookLifecycleProfile {
   readonly earliestResolveAge: number;
   readonly earliestResolveAge: number;
@@ -82,43 +81,6 @@ export const HOOK_ACTIVITY_THRESHOLDS = {
   freshPromiseAge: 1,
   freshPromiseAge: 1,
 } as const;
 } as const;
 
 
-export const HOOK_AGENDA_LIMITS: Record<HookAgendaLoad, {
-  readonly staleDebt: number;
-  readonly mustAdvance: number;
-  readonly eligibleResolve: number;
-  readonly avoidFamilies: number;
-}> = {
-  light: {
-    staleDebt: 1,
-    mustAdvance: 2,
-    eligibleResolve: 1,
-    avoidFamilies: 2,
-  },
-  medium: {
-    staleDebt: 2,
-    mustAdvance: 2,
-    eligibleResolve: 1,
-    avoidFamilies: 3,
-  },
-  heavy: {
-    staleDebt: 3,
-    mustAdvance: 3,
-    eligibleResolve: 2,
-    avoidFamilies: 4,
-  },
-};
-
-export const HOOK_AGENDA_LOAD_THRESHOLDS = {
-  heavyReadyCount: 3,
-  heavyStaleCount: 4,
-  heavyCriticalCount: 3,
-  heavyPressuredCount: 6,
-  mediumReadyCount: 2,
-  mediumStaleCount: 2,
-  mediumCriticalCount: 1,
-  mediumPressuredFamilies: 3,
-} as const;
-
 export const HOOK_VISIBILITY_WINDOWS: Record<HookPayoffTiming, number> = {
 export const HOOK_VISIBILITY_WINDOWS: Record<HookPayoffTiming, number> = {
   immediate: 5,
   immediate: 5,
   "near-term": 5,
   "near-term": 5,

+ 2 - 4
packages/core/src/utils/memory-retrieval.ts

@@ -8,11 +8,10 @@ import {
 import { MemoryDB, type Fact, type StoredHook, type StoredSummary } from "../state/memory-db.js";
 import { MemoryDB, type Fact, type StoredHook, type StoredSummary } from "../state/memory-db.js";
 import { bootstrapStructuredStateFromMarkdown } from "../state/state-bootstrap.js";
 import { bootstrapStructuredStateFromMarkdown } from "../state/state-bootstrap.js";
 import {
 import {
-  buildPlannerHookAgenda,
   filterActiveHooks,
   filterActiveHooks,
   isFuturePlannedHook,
   isFuturePlannedHook,
   isHookWithinChapterWindow,
   isHookWithinChapterWindow,
-} from "./hook-agenda.js";
+} from "./hook-lifecycle.js";
 import {
 import {
   parseChapterSummariesMarkdown,
   parseChapterSummariesMarkdown,
   parseCurrentStateFacts,
   parseCurrentStateFacts,
@@ -21,10 +20,9 @@ import {
   renderSummarySnapshot,
   renderSummarySnapshot,
 } from "./story-markdown.js";
 } from "./story-markdown.js";
 export {
 export {
-  buildPlannerHookAgenda,
   isFuturePlannedHook,
   isFuturePlannedHook,
   isHookWithinChapterWindow,
   isHookWithinChapterWindow,
-} from "./hook-agenda.js";
+} from "./hook-lifecycle.js";
 export {
 export {
   parseChapterSummariesMarkdown,
   parseChapterSummariesMarkdown,
   parseCurrentStateFacts,
   parseCurrentStateFacts,

+ 16 - 49
packages/core/src/utils/narrative-control.ts

@@ -1,4 +1,4 @@
-import type { ChapterBrief, ContextPackage } from "../models/input-governance.js";
+import type { ChapterIntent, ChapterMemo, ContextPackage } from "../models/input-governance.js";
 
 
 const HOOK_ID_PATTERN = /\bH\d+\b/gi;
 const HOOK_ID_PATTERN = /\bH\d+\b/gi;
 const HOOK_SLUG_PATTERN = /\b[a-z]+(?:-[a-z]+){1,3}\b/g;
 const HOOK_SLUG_PATTERN = /\b[a-z]+(?:-[a-z]+){1,3}\b/g;
@@ -40,68 +40,35 @@ export function sanitizeNarrativeControlText(
 }
 }
 
 
 /**
 /**
- * Render a ChapterBrief into a sanitized narrative control block for the
- * writer / reviser prompt. This replaces the old buildNarrativeIntentBrief
- * path: the brief is the single source of chapter intent.
+ * Render a ChapterMemo + optional ChapterIntent into a sanitized narrative
+ * control block for the writer / reviser prompt. Phase 1 transitional: memo
+ * body is typically empty until Phase 3 fills it with new.txt 7-section prose.
  */
  */
-export function renderBriefAsNarrativeBlock(
-  brief: ChapterBrief,
+export function renderMemoAsNarrativeBlock(
+  memo: ChapterMemo,
+  intent: ChapterIntent | undefined,
   language: "zh" | "en" = "zh",
   language: "zh" | "en" = "zh",
 ): string {
 ): string {
-  // Sanitize free-text fields only. Enum/identifier fields (chapterType,
-  // beat.phase, hook.movement, hookId) are passed through verbatim to
-  // avoid HOOK_SLUG_PATTERN corrupting hyphenated values like
-  // "knife-twist", "partial-payoff", "golden-opening".
   const s = (text: string) => sanitizeNarrativeControlText(text, language);
   const s = (text: string) => sanitizeNarrativeControlText(text, language);
   const isEn = language === "en";
   const isEn = language === "en";
-
   const sections: string[] = [];
   const sections: string[] = [];
 
 
-  // Goal — free text, sanitize
-  sections.push(`## ${isEn ? "Goal" : "目标"}\n- ${s(brief.goal)}`);
-
-  // Chapter type — enum value, do NOT sanitize
-  sections.push(`## ${isEn ? "Chapter Type" : "章节类型"}\n- ${brief.chapterType}`);
-
-  // Beat outline — phase is enum, instruction is free text
-  if (brief.beatOutline.length > 0) {
-    const beats = brief.beatOutline
-      .map((beat) => `- ${beat.phase}: ${s(beat.instruction)}`)
-      .join("\n");
-    sections.push(`## ${isEn ? "Beat Outline" : "节拍大纲"}\n${beats}`);
-  }
-
-  // Hook plan — hookId and movement are identifiers, targetEffect is free text
-  if (brief.hookPlan.length > 0) {
-    const hooks = brief.hookPlan
-      .map((hook) => `- ${hook.hookId} | ${hook.movement} | ${s(hook.targetEffect)}`)
-      .join("\n");
-    sections.push(`## ${isEn ? "Hook Plan" : "伏笔计划"}\n${hooks}`);
-  }
+  sections.push(`## ${isEn ? "Goal" : "目标"}\n- ${s(memo.goal)}`);
 
 
-  // Dormant reason — free text, sanitize
-  if (brief.dormantReason) {
-    sections.push(
-      `## ${isEn ? "Dormant Hooks" : "按兵不动的伏笔"}\n- ${s(brief.dormantReason)}`,
-    );
+  if (intent?.arcContext) {
+    sections.push(`## ${isEn ? "Arc Context" : "弧线背景"}\n- ${s(intent.arcContext)}`);
   }
   }
 
 
-  // Props and setting — proper nouns, do NOT sanitize
-  if (brief.propsAndSetting.length > 0) {
-    sections.push(
-      `## ${isEn ? "Props & Setting" : "道具与场景"}\n- ${brief.propsAndSetting.join(", ")}`,
-    );
+  if (memo.body.trim().length > 0) {
+    sections.push(`## ${isEn ? "Memo" : "本章备忘"}\n${s(memo.body)}`);
   }
   }
 
 
-  // Cycle phase
-  if (brief.cyclePhase) {
-    sections.push(
-      `## ${isEn ? "Cycle Phase" : "周期阶段"}\n- ${brief.cyclePhase}`,
-    );
+  if (memo.hookRefs.length > 0) {
+    const hooks = memo.hookRefs.map((hook) => `- ${hook}`).join("\n");
+    sections.push(`## ${isEn ? "Hook Refs" : "关联伏笔"}\n${hooks}`);
   }
   }
 
 
-  // Golden opening flag
-  if (brief.isGoldenOpening) {
+  if (memo.isGoldenOpening) {
     sections.push(
     sections.push(
       `## ${isEn ? "Golden Opening" : "黄金开场"}\n- ${isEn ? "This is a golden opening chapter — prioritize hook-dense, high-tempo pacing." : "本章是黄金开场章——优先钩子密集、高节奏。"}`,
       `## ${isEn ? "Golden Opening" : "黄金开场"}\n- ${isEn ? "This is a golden opening chapter — prioritize hook-dense, high-tempo pacing." : "本章是黄金开场章——优先钩子密集、高节奏。"}`,
     );
     );