Skip to content

Commit 441bd4a

Browse files
Smrutiranjan Patriclaude
andcommitted
feat: quality machinery (thesis, voice, insight gate) + interactive TUI + export fixes
Quality - originality over slop-absence (all three tiers from the code review): - Thesis node: contestable claim + counterargument/rebuttal generated per article, injected into every writer+critic call; critic blocks sections that merely cover - Voice exemplars (brain/users/<uid>/voice/) injected as register to match; /praise saves a great chapter/section there and feeds the learner positive signal - Surgical humanizer: detect tells deterministically, rewrite only flagged sentences, guard every rewrite (citations/numbers/length) - no more wholesale re-generation - Divergent first drafts (best-of-N at varied temps, parallel) + insight score (1-5) with min_insight approval gate + deterministic structural style metrics - Researcher on by default; uncited stats flagged as fabrication risk when off - Critic routed to deepseek-v4-pro (insight scoring needs real judgment) TUI/UX - autonomous and manual runs: - Escalation picker: [f]ix / [i]nstruct / [a]pprove as-is / [g]o autonomous / [r]ead - revise --chapter N --instruction: rewrite one committed unit of a finished piece, patch section + manuscript (books re-run production) - Outline+thesis approval gate after `new` (manual mode, TTY only) - Manual divergent-variant picking via ask callback (Live pauses around input) - Post-run summary card (words/time/tokens/cost/avg insight) + terminal bell - Table read: whole-piece cold read by a skeptical reader -> table_read.md - /auto on|off + run --autonomous/--manual (clears stuck escalations); red toolbar when review pending; draft-opening glimpse in the dashboard Export fixes (all formats): - PDF: code/diagram lines wrap instead of clipping (pre/code CSS, A4, br in pre) - Mermaid blocks render to PNG via mermaid.ink with per-project disk cache (offline re-exports); EPUB packages diagrams as real items; EPUB code-wrap CSS - U+2011/U+202F tofu normalized; [AUTHOR NAME] placeholder dropped; "Section N:" heading prefixes stripped; per-section references consolidated + renumbered 149 tests pass (38 new); ruff clean. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 269192c commit 441bd4a

18 files changed

Lines changed: 1921 additions & 100 deletions

config/models.yaml

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,8 @@ nodes:
88
writer: deepseek/deepseek-v4-pro # prose quality
99
consolidation: deepseek/deepseek-v4-pro # global reasoning across the whole book
1010
toc: deepseek/deepseek-v4-flash
11-
critic: deepseek/deepseek-v4-flash # independent judge - a different model than the writer
11+
critic: deepseek/deepseek-v4-pro # pro-tier judge: insight scoring + thesis checks
12+
# need real judgment, worth the extra cost/latency
1213
summarizer: deepseek/deepseek-v4-flash # chapter summaries + canon extraction
1314
production: deepseek/deepseek-v4-flash # front/back matter + assembly
1415
learner: deepseek/deepseek-v4-flash # distill skills from a finished book
@@ -21,4 +22,5 @@ temperature:
2122
toc: 0.4
2223
critic: 0.2
2324
summarizer: 0.0
24-
humanizer: 0.7
25+
humanizer: 0.3 # surgical line edits - low temp; it must not get creative
26+
writer: 0.9 # explicit base temp; divergent first drafts sample 0.7/1.0/1.2

resume.md

Lines changed: 63 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,70 @@
55
66
## Current status
77

8-
- **Phase:** **Production-ready.** Books and articles both live-validated end-to-end. **111 tests
8+
- **Phase:** **Production-ready.** Books and articles both live-validated end-to-end. **149 tests
99
pass**; ruff clean.
10+
- **New (2026-06-12, session 9b - TUI/UX batch + headroom actually installed):**
11+
- **Escalation picker**: a stalled run now shows the critic's blocking issues and prompts
12+
`[f]ix · [i]nstruct · [a]pprove as-is · [g]o autonomous · [r]ead draft · [s]top` - one
13+
keypress instead of `review --chapter N --instruction "..."`. Backed by new
14+
`orchestrator.approve_escalation()` (commits the stalled draft via the normal commit path).
15+
- **`revise` command** (post-completion loop): `revise --chapter N --instruction "..."`
16+
rewrites ONE committed unit of a finished piece (write→critique→optional fix pass→
17+
humanize), patches the section file AND the assembled manuscript
18+
(`_replace_manuscript_section`); books re-run production. Canon deliberately not
19+
re-extracted. NL chat maps "make section 3 more technical" to it.
20+
- **Outline gate** (manual mode, TTY only): after `new`, the outline + thesis claim are
21+
shown with `[Enter] write · r regenerate · g regenerate with guidance` (max 3 rounds).
22+
- **Manual variant pick**: in manual interactive runs the human chooses among divergent
23+
drafts (glimpse + critic verdict/insight per variant; Enter = critic's pick) via an
24+
`ask` callback threaded through `run()` (Live display pauses/resumes around input).
25+
- **Summary card + bell**: finished runs ring the terminal and show words/time/tokens/
26+
cost/avg-insight (state["insights"] tracked per commit) + table-read pointer.
27+
- **Table read** (`table_read: true`): whole-article cold read by a skeptical
28+
target-audience reader -> `table_read.md` report (boredom/trust/unclear/missing),
29+
report-only, feeds `revise`.
30+
- **Toolbar turns red** (prompt_toolkit HTML) when a review is pending; dashboard now
31+
shows a draft-opening glimpse line per attempt.
32+
- **Headroom fixed**: `headroom-ai` was configured but NOT INSTALLED (silent no-op since
33+
day one). Installed the Windows pure-Python build (0.10.17 + opentelemetry-api +
34+
tiktoken); verified through `llm._compress`. Caveat: transforms target long multi-turn
35+
payloads - expect savings on late-book context, not every call.
36+
- **New (2026-06-12, session 9 - quality machinery: originality over slop-absence):**
37+
Code review concluded the pipeline guaranteed the floor (no slop symptoms) but had no
38+
machinery for the ceiling (a thesis, a voice, a risk). Implemented all three tiers:
39+
- **Thesis node** (`nodes.generate_thesis`, `schemas.Thesis`): contestable claim + stakes +
40+
arguments + steelmanned counterargument/rebuttal + non-goals, generated at `start_article`,
41+
persisted as `thesis.json`/`thesis.md`, injected into every section writer + critic call.
42+
Critic blocks sections that cover the topic without advancing the thesis.
43+
- **Voice exemplars** (`brain.voice_dir`/`voice_exemplars`): drop admired paragraphs in
44+
`brain/users/<uid>/voice/`; they're injected into every writer call as register to MATCH.
45+
New **`/praise [N]`** command saves a committed chapter/section there (falls back to
46+
manuscript extraction for finished articles) - feeds both the writer and the learner
47+
(`nodes.learn` now takes `praised=` positive exemplars).
48+
- **Surgical humanizer** (humanizer.py rewritten): tells detected deterministically
49+
(lexicon regex), ONLY flagged sentences rewritten (structured `LineEdits`), each rewrite
50+
guarded (citations/numbers/length/tell-gone) before splicing. No more wholesale
51+
re-generation of approved prose. "optimize" removed from the lexicon (wrong for technical
52+
prose - the old ban was leaky anyway).
53+
- **Divergent first drafts** (`divergent_drafts: 2` setting): attempt 0 samples N drafts at
54+
temps 0.7/1.0/1.2 in parallel; critic scores all; the winner gets refined. Both loops.
55+
- **Insight gate** (`Critique.insight` 1-5, `min_insight: 3` setting): approve now requires
56+
insight >= bar; correct-but-generic drafts get a sharpening revision note. Deterministic
57+
`structural_report()` (paragraph uniformity, rule-of-three density, specificity density)
58+
feeds the critic as computed evidence. `_crit_better` prefers higher insight.
59+
- **Critic = DeepSeek v4-pro** (user decisions: DeepSeek pro/flash only - no other
60+
providers; then critic upgraded flash→pro since insight scoring + thesis checks need
61+
real judgment). Writer temp now explicit (0.9); humanizer 0.3.
62+
- **Researcher default ON** (`use_researcher: true`); critic treats uncited stats as
63+
fabrication risk when research is off; production logs a warning when [n] citations exist
64+
with an empty source registry.
65+
- Earlier same day: `/auto` autonomous↔manual toggle (clears stuck escalations),
66+
`run --autonomous/--manual`, chat escalation playbook, export fixes (PDF code wrap,
67+
Mermaid→PNG with disk cache, U+2011 tofu, byline, "Section N:" strip, per-section
68+
reference consolidation, EPUB code-wrap CSS).
69+
- **Next:** live-validate a real article run end-to-end with the new machinery (thesis +
70+
divergent drafts + insight gate + cross-family critic); compare output quality against
71+
the voice-agent article; tune `min_insight`/`divergent_drafts` from telemetry cost data.
1072
- **New (2026-06-12, sessions 1-8):**
1173
- **Chat NL flow** - propose abstract → refine with plain English → "run it"/"go ahead"
1274
creates + starts writing in one turn. Fixed the regex that silently dropped every

src/book_agent/brain.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,40 @@ def watch_list(uid: str = "default") -> Path:
7676
return user_dir(uid) / "prefs" / "watch_list.md"
7777

7878

79+
def voice_dir(uid: str = "default") -> Path:
80+
"""Admired writing samples: user-dropped .md/.txt files plus /praise'd passages.
81+
Injected into writer calls as register to MATCH (showing voice beats describing
82+
it) and read by the learner as positive exemplars."""
83+
return user_dir(uid) / "voice"
84+
85+
86+
def voice_exemplars(uid: str = "default", max_chars: int = 2400) -> str | None:
87+
"""Assemble exemplar paragraphs from voice_dir under a character budget.
88+
89+
Takes leading prose paragraphs (skipping headings and code fences) from each
90+
file in name order until the budget is spent, so users control priority by
91+
file naming. Returns None when there are no exemplars.
92+
"""
93+
d = voice_dir(uid)
94+
if not d.exists():
95+
return None
96+
chunks: list[str] = []
97+
total = 0
98+
for p in sorted(list(d.glob("*.md")) + list(d.glob("*.txt"))):
99+
text = read_text(p) or ""
100+
for para in re.split(r"\n\s*\n", text):
101+
para = para.strip()
102+
if not para or para.startswith("#") or para.startswith("```"):
103+
continue
104+
if total + len(para) > max_chars:
105+
break
106+
chunks.append(para)
107+
total += len(para)
108+
if total >= max_chars:
109+
break
110+
return "\n\n".join(chunks) or None
111+
112+
79113
# ── Book scope ───────────────────────────────────────────────────────────────
80114
class BookPaths:
81115
"""All paths for one book. The brain on disk is the source of truth."""

src/book_agent/cli.py

Lines changed: 70 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,35 @@ def _spin(label: str, fn):
9494
return fn()
9595

9696

97+
def _outline_gate(uid, project_id, abstract, create_fn, *, is_article, gate_on: bool):
98+
"""Manual-mode checkpoint after `new`: show the outline (and thesis) BEFORE any
99+
prose is written. Reviewing 6 headings costs the human 30 seconds; a bad outline
100+
costs the whole run. Enter accepts; 'r' regenerates; 'g' regenerates with guidance.
101+
Skipped for autonomous runs and non-interactive stdin (tests, pipes, CI)."""
102+
if not gate_on or not sys.stdin.isatty():
103+
return project_id
104+
for _round in range(3): # cap regenerations - the planner won't improve forever
105+
paths = ArticlePaths(project_id, uid) if is_article else BookPaths(project_id, uid)
106+
outline_md = brain.read_text(paths.outline_md if is_article else paths.toc) or ""
107+
print("\n" + outline_md.strip())
108+
thesis_md = brain.read_text(paths.root / "thesis.md") if is_article else None
109+
if thesis_md:
110+
claim = next((ln for ln in thesis_md.splitlines() if ln.startswith("**Claim:**")), "")
111+
if claim:
112+
print(f"\n{claim}")
113+
ans = input("\n[Enter] start writing · r regenerate outline · "
114+
"g regenerate with guidance: ").strip().lower()
115+
if ans not in ("r", "g"):
116+
return project_id
117+
if ans == "g":
118+
guidance = input("guidance for the planner: ").strip()
119+
if guidance:
120+
abstract = f"{abstract}\n\nAuthor guidance (honor exactly): {guidance}"
121+
orchestrator.delete_book(uid, project_id)
122+
project_id = create_fn(abstract)
123+
return project_id
124+
125+
97126
def _cmd_new_book(args, cfg, settings, uid, abstract):
98127
directions = _spin("planning directions", lambda: nodes.planner_directions(cfg, abstract)).directions
99128
for i, d in enumerate(directions, 1):
@@ -102,11 +131,16 @@ def _cmd_new_book(args, cfg, settings, uid, abstract):
102131
chosen = directions[idx - 1]
103132
chapters = args.chapters or settings.num_chapters
104133
max_rev = args.max_revisions if args.max_revisions is not None else settings.max_revisions
134+
autonomous = _autonomous_value(args, settings)
105135
print(f"\n-> {chosen.title}")
106-
book_id = _spin("building plan + TOC", lambda: orchestrator.start_book(
107-
cfg, settings, uid, abstract, chosen, args.book_id, chapters, max_rev,
108-
autonomous=_autonomous_value(args, settings),
109-
humanize=(False if getattr(args, "no_humanize", False) else None)))
136+
137+
def _create(abs_):
138+
return _spin("building plan + TOC", lambda: orchestrator.start_book(
139+
cfg, settings, uid, abs_, chosen, args.book_id, chapters, max_rev,
140+
autonomous=autonomous,
141+
humanize=(False if getattr(args, "no_humanize", False) else None)))
142+
book_id = _outline_gate(uid, _create(abstract), abstract, _create,
143+
is_article=False, gate_on=not autonomous)
110144
print(f"\n[OK] Created book '{book_id}'.")
111145
print(f" Next: python book.py run --book-id {book_id}")
112146

@@ -119,11 +153,16 @@ def _cmd_new_article(args, cfg, settings, uid, abstract):
119153
chosen = angles[idx - 1]
120154
num_sections = args.chapters or settings.num_sections
121155
max_rev = args.max_revisions if args.max_revisions is not None else settings.max_revisions
156+
autonomous = _autonomous_value(args, settings)
122157
print(f"\n-> {chosen.title}")
123-
article_id = _spin("building outline", lambda: orchestrator.start_article(
124-
cfg, settings, uid, abstract, chosen, args.book_id, num_sections, max_rev,
125-
autonomous=_autonomous_value(args, settings),
126-
humanize=(False if getattr(args, "no_humanize", False) else None)))
158+
159+
def _create(abs_):
160+
return _spin("building outline", lambda: orchestrator.start_article(
161+
cfg, settings, uid, abs_, chosen, args.book_id, num_sections, max_rev,
162+
autonomous=autonomous,
163+
humanize=(False if getattr(args, "no_humanize", False) else None)))
164+
article_id = _outline_gate(uid, _create(abstract), abstract, _create,
165+
is_article=True, gate_on=not autonomous)
127166
print(f"\n[OK] Created article '{article_id}'.")
128167
print(f" Next: python book.py run --book-id {article_id}")
129168

@@ -303,7 +342,8 @@ def cmd_write(args, cfg, settings, uid):
303342

304343
def cmd_run(args, cfg, settings, uid):
305344
orchestrator.run(cfg, uid, _resolve_book(uid, args.book_id),
306-
force=getattr(args, "force", False))
345+
force=getattr(args, "force", False),
346+
autonomous=getattr(args, "autonomous", None))
307347

308348

309349
def cmd_status(args, cfg, settings, uid):
@@ -360,6 +400,15 @@ def cmd_review(args, cfg, settings, uid):
360400
f"--book-id {book_id}")
361401

362402

403+
def cmd_revise(args, cfg, settings, uid):
404+
"""Post-completion revision: rewrite one committed chapter/section to an instruction."""
405+
if args.chapter is None or not args.instruction:
406+
sys.exit('revise needs --chapter and --instruction (e.g. revise --chapter 3 '
407+
'--instruction "more technical, add a benchmark table")')
408+
book_id = _resolve_book(uid, args.book_id)
409+
orchestrator.revise_unit(cfg, uid, book_id, args.chapter, args.instruction)
410+
411+
363412
def cmd_read(args, cfg, settings, uid):
364413
# Articles and books store a manuscript/sections at different paths; pick the
365414
# right one so `read --manuscript` works for both project types.
@@ -504,6 +553,7 @@ def cmd_config(args, cfg, settings, uid):
504553

505554
_COMMANDS = {
506555
"new": cmd_new, "write": cmd_write, "run": cmd_run, "status": cmd_status, "review": cmd_review,
556+
"revise": cmd_revise,
507557
"read": cmd_read, "memory": cmd_memory, "produce": cmd_produce,
508558
"consolidate": cmd_consolidate, "skills": cmd_skills, "config": cmd_config,
509559
"list": cmd_list, "export": cmd_export, "seed-skills": cmd_seed_skills,
@@ -544,12 +594,23 @@ def build_parser(settings):
544594

545595
p_run = sub.add_parser("run", parents=[common], help="Drive the pipeline until done or escalation")
546596
p_run.add_argument("--force", action="store_true", help="Proceed past a consolidation review")
597+
# Tri-state (default None): switch the project's run mode as it resumes. --autonomous
598+
# also unblocks an escalated unit so the run finishes without pausing.
599+
p_run.add_argument("--autonomous", dest="autonomous", action="store_const", const=True,
600+
default=None, help="Stop pausing for review; commit best drafts and finish")
601+
p_run.add_argument("--manual", dest="autonomous", action="store_const", const=False,
602+
help="Re-enable human-in-the-loop review at each chapter/section")
547603
sub.add_parser("status", parents=[common], help="Show run state + open reviews")
548604

549605
p_rev = sub.add_parser("review", parents=[common], help="Answer an escalation")
550606
p_rev.add_argument("--chapter", type=int)
551607
p_rev.add_argument("--instruction")
552608

609+
p_revise = sub.add_parser("revise", parents=[common],
610+
help="Rewrite one committed chapter/section of a finished piece")
611+
p_revise.add_argument("--chapter", type=int, help="Chapter/section number to rewrite")
612+
p_revise.add_argument("--instruction", help="What to change, in your words")
613+
553614
p_read = sub.add_parser("read", parents=[common], help="Print chapter/summary/manuscript")
554615
p_read.add_argument("--chapter", type=int)
555616
p_read.add_argument("--summary", action="store_true")

src/book_agent/config.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,8 +52,11 @@ class Settings:
5252
num_chapters: int = 8
5353
max_revisions: int = 2
5454
consolidate_every: int = 5
55-
use_researcher: bool = False
55+
use_researcher: bool = True # web grounding per unit - off means citations are unverifiable
5656
deep_research: bool = False # multi-source fetch+synthesize (needs use_researcher; plan §15)
57+
divergent_drafts: int = 2 # first-attempt drafts at varied temps; critic picks best (1 = off)
58+
min_insight: int = 3 # critic insight (1-5) required to approve (0 = off)
59+
table_read: bool = True # whole-article cold read by a skeptical reader (report only)
5760
escalate_below_confidence: float = 0.5 # critic confidence below this -> escalate (plan §7)
5861
escalate_on_contradiction: bool = True # consolidation contradictions -> review (plan §9)
5962
autonomous: bool = False # no human-in-the-loop: never pause, commit best draft

0 commit comments

Comments
 (0)