Files
coorl-lost-cities/scripts/librarian_promote.py
T
coolguyandClaude Opus 4.7 f5ce49682e Close first librarian promote round-trip + add --accept flag
scripts/librarian_promote.py gains a --accept flag that copies the
generated draft to the suggested docs/research/ target in the same
invocation. This is explicit per-invocation opt-in, not auto-apply:
the operator types --accept knowingly, the cp is still a deliberate
acceptance decision, just expressed in one command instead of two.

docs/research/option-a-bench-result.md is the first promoted note,
generated by gemini from
docs/archive/option-a-bench-result-2026-05-07.md and accepted
verbatim. Spot-check verified that cited file:line locations match
current source (traversal.pyx:473-475 and
inference_server.py:226-228 carry the cited code), the Last-verified
header reflects today's date and HEAD, and the durable conclusion
(sync-blocking policy boundary as the structural ceiling, not IPC
plumbing) is preserved.

This closes the end-to-end loop the librarian was designed for:
oversize check surfaced docs/performance.md, the routing pass split
durable analysis into the archive entry, the promote dispatcher
turned the archive entry into a research draft, and the human
accept step landed it as a tracked research note. Took one LLM call.

Removes the now-resolved ignore-list entry for
docs/research/option-a-bench-result.md (the file exists; the
forward reference is real).

scripts/librarian.sh exits 0 against the working tree.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-08 00:31:59 +09:00

194 lines
6.4 KiB
Python

"""Stage 2 LLM dispatcher: draft a research note from an archive entry.
Reads `docs/archive/<name>.md`, assembles a prompt by stitching
`scripts/librarian-prompt.md` (the system prompt) onto the archive
body, and dispatches to the LLM CLI selected by the LIBRARIAN_LLM
environment variable. The LLM's stdout is captured to
`runs/tmp/librarian-promote-<timestamp>-draft.md`. The dispatcher
never writes into `docs/research/` directly — the operator reviews
the draft and copies/edits it themselves.
Backends:
LIBRARIAN_LLM=claude (default; invokes `claude -p`)
LIBRARIAN_LLM=codex (invokes `codex exec`)
LIBRARIAN_LLM=gemini (invokes `gemini -p`)
Usage:
uv run python scripts/librarian_promote.py docs/archive/foo.md
uv run python scripts/librarian_promote.py docs/archive/foo.md --show-prompt
LIBRARIAN_LLM=codex uv run python scripts/librarian_promote.py docs/archive/foo.md
"""
from __future__ import annotations
import argparse
import os
import re
import shutil
import subprocess
import sys
from datetime import datetime
from pathlib import Path
LLM_COMMANDS = {
"claude": ["claude", "-p"],
"codex": ["codex", "exec"],
"gemini": ["gemini", "-p", ""],
}
DATE_SUFFIX = re.compile(r"-\d{4}-\d{2}-\d{2}$")
def _repo_root() -> Path:
current = Path(__file__).resolve()
for parent in current.parents:
if (parent / "pyproject.toml").is_file():
return parent
raise RuntimeError("pyproject.toml을 찾을 수 없어 repository root를 판정할 수 없습니다.")
def _assemble_prompt(
system_prompt: str,
rel_archive: Path,
archive_body: str,
rel_target: Path,
) -> str:
return (
f"{system_prompt}\n\n"
"---\n\n"
"Task: Draft a `docs/research/` note from the archive entry below.\n"
"Follow the rules in your system prompt above (style template, "
"`Last verified:` and `Source:` headers, `file:line` citations "
"verified against the current tree, ~1 page, prose over bullet "
"soup).\n\n"
f"**Source archive:** `{rel_archive}`\n"
f"**Suggested target filename:** `{rel_target}`\n\n"
"If the archive does not contain a durable conclusion (e.g. it "
"is a one-off bench result with no general lesson), respond "
"with a single line `SKIP: <reason>` instead of a draft.\n\n"
"Output: the markdown content of the new file only. No "
"preamble, no code fences around the whole thing, no "
"explanation after. Begin with the H1 header line.\n\n"
"---\n\n"
"Archive body:\n\n"
f"{archive_body}\n"
)
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__.splitlines()[0])
parser.add_argument(
"archive_path",
help="Path to a docs/archive/*.md entry to promote.",
)
parser.add_argument(
"--show-prompt",
action="store_true",
help="Print the assembled prompt to stdout and exit; do not call the LLM.",
)
parser.add_argument(
"--accept",
action="store_true",
help=(
"After the draft lands in runs/tmp/, also copy it verbatim to the "
"suggested docs/research/ target. Explicit per-invocation opt-in: "
"still propose-only by design (you are choosing acceptance "
"knowingly), not auto-apply."
),
)
args = parser.parse_args()
root = _repo_root()
archive = (root / args.archive_path).resolve()
if not archive.is_file():
print(f"Archive not found: {args.archive_path}", file=sys.stderr)
return 1
try:
rel_archive = archive.relative_to(root)
except ValueError:
print(f"Archive must live under repo root: {archive}", file=sys.stderr)
return 1
if not str(rel_archive).startswith("docs/archive/"):
print(
f"Refusing: archive must live under docs/archive/: {rel_archive}",
file=sys.stderr,
)
return 1
stem = DATE_SUFFIX.sub("", archive.stem)
target = root / "docs" / "research" / f"{stem}.md"
rel_target = target.relative_to(root)
if target.exists():
print(
f"Refusing: research counterpart already exists: {rel_target}",
file=sys.stderr,
)
return 1
system_prompt = (root / "scripts" / "librarian-prompt.md").read_text(encoding="utf-8")
archive_body = archive.read_text(encoding="utf-8")
prompt = _assemble_prompt(system_prompt, rel_archive, archive_body, rel_target)
if args.show_prompt:
sys.stdout.write(prompt)
return 0
backend = os.environ.get("LIBRARIAN_LLM", "claude").lower()
cmd = LLM_COMMANDS.get(backend)
if cmd is None:
print(
f"Unknown LIBRARIAN_LLM={backend}; supported: {sorted(LLM_COMMANDS)}",
file=sys.stderr,
)
return 1
timestamp = datetime.now().strftime("%Y-%m-%d_%H%M%S")
out_dir = root / "runs" / "tmp"
out_dir.mkdir(parents=True, exist_ok=True)
prompt_path = out_dir / f"librarian-promote-{timestamp}-prompt.md"
draft_path = out_dir / f"librarian-promote-{timestamp}-draft.md"
prompt_path.write_text(prompt, encoding="utf-8")
print(
f"Dispatching to {backend} (prompt saved to {prompt_path.relative_to(root)})",
file=sys.stderr,
)
result = subprocess.run(
cmd,
input=prompt,
capture_output=True,
text=True,
check=False,
)
if result.returncode != 0:
print(f"LLM call failed (exit {result.returncode}):", file=sys.stderr)
print(result.stderr, file=sys.stderr)
return result.returncode
draft_path.write_text(result.stdout, encoding="utf-8")
print(f"Draft written to: {draft_path.relative_to(root)}")
if args.accept:
if target.exists():
print(
f"Refusing --accept: {rel_target} appeared while the LLM ran.",
file=sys.stderr,
)
return 1
shutil.copyfile(draft_path, target)
print(f"Copied to: {rel_target}")
print("Next: review the diff and `git add` + commit if you're satisfied.")
else:
print(f"Suggested target on accept: {rel_target}")
print()
print("Next: review the draft. To accept verbatim:")
print(f" cp {draft_path.relative_to(root)} {rel_target}")
print("Or rerun with --accept to copy in the same step.")
return 0
if __name__ == "__main__":
raise SystemExit(main())