Files
rune/scripts/research_prompt_compiler.py
Nick ca4f7593d7 feat(corrections): #314 defect fix, credential rotation, release guard, task lifecycle, route scoping, PL1 records + metrics
CORRECTION 1: #314 reopened — recall_with_associations mode field defect
CONFIRMED in current source and FIXED (mode: association_merged).

CORRECTION 3: Gitea credential removed from remote URL (credential helper).
TOKEN EXPOSED: yes (in diagnostic output). TOKEN ROTATED: not yet (requires
Gitea web UI access). Remote URL now shows username only, no secret.

CORRECTION 4: duplicate-release-commit defect ROOT CAUSE: convergence
computed warranted bump from commits-since-tag without checking if master
version already satisfies the bump. FIX: already-converged guard in
version_convergence.py (master version > last tag → no commit). Repo-aware
(args.repo passed to read_version/latest_tag).

CORRECTION 6: lifecycle terminalization — execution exception now
guarantees task terminalizes as failed with evidence (no immortal RUNNING).

CORRECTION 7: route instruction task-scoped on RuneTask.route_instruction
(NOT shared operator state). Read-only leading-verb override. Isolation
tests prove no cross-contamination between interleaved tasks.

PL1-01/02/03/05: pl1_records.py (route resolution, proof level, failure
class, candidate status), metrics with honest NOT_YET_MEASURABLE semantics,
route-instruction learning loop (PE-1789102658-0269 SEED).

1253 tests green; ratchet 352/80 (shrink); henxels clean.
2026-09-11 15:36:08 +01:00

99 lines
3.4 KiB
Python

#!/usr/bin/env python3
"""research_prompt_compiler.py — dynamic stage prompt compiler (RSC2).
Takes the ResearchTaskTree, the current stage, and the stage policy template,
and compiles a task-specific prompt that answers:
What exactly am I researching?
What has already been established?
What evidence is authoritative?
What remains unknown?
What am I allowed to do now?
What am I forbidden from doing yet?
What output must I return?
"""
from __future__ import annotations
import json
from pathlib import Path
STAGE_POLICY_DIR = Path(__file__).parent.parent / "prompts" / "research-stages"
def compile_research_prompt(
tree, # ResearchTaskTree
stage: str,
stage_policy: str | None = None,
) -> str:
"""Compile a task-specific prompt for the current research stage."""
capsule = tree.capsule()
prior = tree.get_prior_artifacts(stage)
unknowns = tree.state.get("blocking_unknowns", [])
# Load the stage policy template
template_path = STAGE_POLICY_DIR / f"{stage.lower()}.md"
if stage_policy:
template = stage_policy
elif template_path.exists():
template = template_path.read_text()
else:
template = f"Stage: {stage}\nPerform the {stage} research task."
# Build the prior artifacts digest (only what this stage needs)
prior_digest = ""
if prior:
prior_lines = []
for stage_name, artifact in prior.items():
if not artifact:
continue
keys = list(artifact.keys())[:5]
prior_lines.append(f" {stage_name}: {json.dumps(artifact, default=str)[:200]}")
prior_digest = "\n".join(prior_lines)
# Build the blocking unknowns section
unknowns_section = ""
if unknowns:
unknowns_section = "\n## Blocking unknowns\n"
for u in unknowns:
unknowns_section += f"- {u.get('id', '?')}: {u.get('question', '?')}\n"
# Substitute placeholders
prompt = template
prompt = prompt.replace("{{ISSUE_NUMBER}}", str(capsule["issue"]))
prompt = prompt.replace("{{ROUND}}", str(capsule["round"]))
prompt = prompt.replace("{{PRIOR_ARTIFACTS}}", prior_digest or "(none yet)")
prompt = prompt.replace("{{BLOCKING_UNKNOWNS}}", unknowns_section or "(none)")
# Append the process capsule (§14: MiMo must not infer from history)
prompt += "\n\n---\n## Process capsule (authoritative — do not infer from history)\n"
prompt += f"```json\n{json.dumps(capsule, indent=2, default=str)}\n```"
return prompt
def main_with(argv: list[str]) -> int:
import argparse
parser = argparse.ArgumentParser(description="Compile a stage-scoped research prompt")
parser.add_argument("--issue", type=int, required=True)
parser.add_argument("--round", type=int, default=1)
parser.add_argument("--stage", required=True)
parser.add_argument("--artifact-dir", required=True)
parser.add_argument("--out", required=True)
args = parser.parse_args(argv[1:])
tree = __import__("research_task_tree").ResearchTaskTree(
issue=args.issue, round_no=args.round, artifact_dir=args.artifact_dir
)
prompt = compile_research_prompt(tree, args.stage)
Path(args.out).write_text(prompt)
print(f"prompt compiled: {len(prompt)} chars for {args.stage} (issue #{args.issue}, round {args.round})")
return 0
if __name__ == "__main__":
import sys
sys.exit(main_with(sys.argv))