#!/usr/bin/env bash
# promptbuilder.sh — build a cx-implement.sh prompt file from plan JSONL.
# Deterministic, zero-AI. Extracts the task contract and writes a ready-to-run brief.
#
# CONTRACT:
#   promptbuilder.sh --plan JSONL --task TASK_ID --worktree WT --out PF [--fix-findings JSON]
#   --plan JSONL         : path to session-state/v1 plan JSONL
#   --task TASK_ID       : task id (e.g. w2.p1.t1)
#   --worktree WT        : absolute path to task worktree (embedded in the brief)
#   --out PF             : output prompt file path (written on success)
#   --fix-findings JSON  : (optional) findings JSON array — switches to fix-brief mode
#   Prints PF to stdout on success. exit 0.
#   exit 2 on usage/parse error (prints error to stderr).
set -uo pipefail

usage() { printf 'promptbuilder.sh: %s\n' "$*" >&2; exit 2; }

PLAN=""; TASK_ID=""; WORKTREE=""; OUT=""; FIX_FINDINGS=""
while [[ $# -gt 0 ]]; do
  case "$1" in
    --plan)          PLAN="${2:-}";         shift 2 ;;
    --task)          TASK_ID="${2:-}";      shift 2 ;;
    --worktree)      WORKTREE="${2:-}";     shift 2 ;;
    --out)           OUT="${2:-}";          shift 2 ;;
    --fix-findings)  FIX_FINDINGS="${2:-}"; shift 2 ;;
    *) usage "unknown arg: $1" ;;
  esac
done

[[ -n "$PLAN" && -f "$PLAN" ]]     || usage "--plan missing or not a file: ${PLAN:-<empty>}"
[[ -n "$TASK_ID" ]]                || usage "--task required"
[[ -n "$WORKTREE" && -d "$WORKTREE" ]] || usage "--worktree missing or not a dir: ${WORKTREE:-<empty>}"
[[ -n "$OUT" ]]                    || usage "--out required"

mkdir -p "$(dirname "$OUT")" 2>/dev/null || true

python3 - "$PLAN" "$TASK_ID" "$WORKTREE" "$OUT" "$FIX_FINDINGS" << 'PY'
import sys, json, textwrap, os, re

plan_path, task_id, worktree, out_path, fix_findings_raw = sys.argv[1:]

meta = None; spec_path = ""; plan_doc_path = ""; task = None; all_tasks = []
with open(plan_path, encoding="utf-8") as f:
    for line in f:
        line = line.strip()
        if not line: continue
        try:
            obj = json.loads(line)
        except Exception:
            continue
        t = obj.get("type", "")
        if t == "meta":
            meta = obj
        elif t == "anchor" and "spec" in obj.get("what", "").lower():
            spec_path = obj.get("path", "")
        elif t == "anchor" and "plan" in obj.get("what", "").lower():
            plan_doc_path = obj.get("path", "")
        elif t == "task":
            all_tasks.append(obj)
            if obj.get("id") == task_id:
                task = obj

if task is None:
    sys.stderr.write(f"promptbuilder: task {task_id!r} not found in {plan_path}\n")
    sys.exit(2)

# Repo root: plan_path is <repo>/docs/plans/<file>.jsonl — 3 dirnames up.
repo_root = os.path.dirname(os.path.dirname(os.path.dirname(os.path.abspath(plan_path))))

def extract_pinned_contract():
    """Pull this task's '### Task N' section out of the plan .md doc, matched by
    wave number (task JSONL has no doc-section id; **Wave:** N is the only
    reliable cross-reference the plan-authoring skill guarantees per task)."""
    if not plan_doc_path:
        return ""
    doc_full = os.path.join(repo_root, plan_doc_path)
    if not os.path.exists(doc_full):
        return ""
    try:
        doc_text = open(doc_full, encoding="utf-8").read()
    except Exception:
        return ""

    lines = doc_text.splitlines()
    starts = [i for i, l in enumerate(lines) if l.startswith("### Task ")]
    sections = []
    for idx, start in enumerate(starts):
        end = len(lines)
        for j in range(start + 1, len(lines)):
            if lines[j].startswith("### Task ") or lines[j].startswith("## ") or lines[j].strip() == "---":
                end = j
                break
        text = "\n".join(lines[start:end]).rstrip()
        wave = None
        for l in lines[start:end]:
            m = re.match(r"\*\*Wave:\*\*\s*(\d+)", l.strip())
            if m:
                wave = int(m.group(1))
                break
        sections.append({"wave": wave, "text": text})

    target_wave = task.get("wave")
    if target_wave is None:
        return ""

    # Positional match within the wave: JSONL tasks for this wave, in file order,
    # zipped against doc sections tagged with the same wave, in doc order.
    same_wave_task_ids = [t.get("id") for t in all_tasks if t.get("wave") == target_wave]
    same_wave_sections = [s for s in sections if s["wave"] == target_wave]
    if not same_wave_sections:
        return ""
    try:
        pos = same_wave_task_ids.index(task_id)
    except ValueError:
        pos = 0
    if pos >= len(same_wave_sections):
        pos = len(same_wave_sections) - 1
    return same_wave_sections[pos]["text"]

pinned_contract = extract_pinned_contract()

# Support both schema variants:
#   old: desc + files
#   new (session-state/v1): title + acceptance + files_create + files_modify
desc   = task.get("desc", "")
if not desc:
    title      = task.get("title", "")
    acceptance = task.get("acceptance", "")
    desc = title + (f"\n\nAcceptance: {acceptance}" if acceptance else "")
files  = task.get("files", [])
if not files:
    files = task.get("files_create", []) + task.get("files_modify", [])
slug   = (meta or {}).get("slug", "")

files_str = "\n".join(f"  - {f}" for f in files) if files else "  (see task description)"
spec_note = f"Source of truth: {spec_path}" if spec_path else ""

if fix_findings_raw:
    # Fix-brief mode: address review findings via codex
    try:
        findings = json.loads(fix_findings_raw)
    except Exception:
        findings = [{"file": "-", "issue": fix_findings_raw, "severity": "blocker"}]

    findings_block = json.dumps(findings, indent=2)
    brief = textwrap.dedent(f"""\
        Fix ALL of the following review findings in this worktree.
        Worktree: {worktree}
        Task: {task_id} ({slug})

        FINDINGS (fix every item — do not skip):
        {findings_block}

        Instructions:
        - Fix each finding in the relevant file(s).
        - Add or adjust tests to cover the fix.
        - Run the repo acceptance command and iterate until green.
        - Commit all changes.
        - Do NOT touch files unrelated to the findings.
        """)
else:
    # Implement-brief mode: build task from plan contract
    if pinned_contract:
        contract_block = (
            "PINNED CONTRACT (verbatim from the plan doc — this is the authoritative,\n"
            "human-reviewed spec for this task; the one-line TASK DESCRIPTION above is\n"
            "only a summary. Follow this EXACTLY — exact function/variable/branch names,\n"
            "exact signatures, exact literal strings. Do NOT invent your own naming or\n"
            "shape even if it seems equivalent):\n\n" + pinned_contract
        )
    else:
        contract_block = "(no pinned contract section found in the plan doc for this task — proceed from TASK DESCRIPTION + spec)"

    brief = textwrap.dedent(f"""\
        Implement task {task_id} for project {slug!r}.
        Worktree: {worktree}
        {spec_note}

        TASK DESCRIPTION:
        {desc}

        FILES TO CREATE / MODIFY:
        {files_str}

        {contract_block}

        Instructions:
        - Write full, non-stub implementations — no placeholders.
        - Write tests alongside each file; run the repo acceptance command.
        - Iterate until all tests pass (gate0 green).
        - Reference exemplar files by path — never paste their full content.
        - Commit all changes when green.
        - Do NOT touch files outside the listed scope.
        """)

with open(out_path, "w", encoding="utf-8") as f:
    f.write(brief)

print(out_path)
PY
