# llms-full (private-aware)
> Built from GitHub files and website pages. Large files may be truncated.

--- scripts/build_install_oraclepack.md ---
```bash
# path: scripts/build_install_oraclepack.sh
#!/usr/bin/env bash
set -euo pipefail

# Builds oraclepack for:
# - WSL/Linux (oraclepack)
# - Windows amd64 (oraclepack.exe)
# Then installs:
# - WSL binary -> ~/.local/bin/
# - Windows exe -> /mnt/c/Users/<winuser>/.local/bin/
# And writes a Git Bash wrapper:
# - /mnt/c/Users/<winuser>/bin/oraclepack  (calls WSL binary via wsl.exe)

usage() {
  cat <<'USAGE'
Usage:
  scripts/build_install_oraclepack.sh [options]

Options:
  --repo-root <path>      Repo root (default: git toplevel, else current dir)
  --win-user <name>       Windows username (default: auto-detect, else "user")
  --wsl-distro <name>     WSL distro name for wsl.exe -d (default: $WSL_DISTRO_NAME or "Ubuntu-24.04")
  --wsl-user <name>       WSL username for wsl.exe -u (default: current user)
  --no-linux              Skip building/installing Linux binary
  --no-windows            Skip building/installing Windows .exe
  --no-wrapper            Skip writing the Git Bash wrapper script
  --cgo <0|1>             Set CGO_ENABLED (default: leave unchanged)

Examples:
  scripts/build_install_oraclepack.sh
  scripts/build_install_oraclepack.sh --win-user Alice --wsl-distro Ubuntu-24.04
USAGE
}

die() { echo "error: $*" >&2; exit 1; }

have() { command -v "$1" >/dev/null 2>&1; }

detect_repo_root() {
  if have git && git rev-parse --show-toplevel >/dev/null 2>&1; then
    git rev-parse --show-toplevel
  else
    pwd
  fi
}

detect_win_user() {
  # Best-effort: ask Windows for %USERNAME% via cmd.exe (works in most WSL setups).
  if have cmd.exe; then
    local u
    u="$(cmd.exe /c "echo %USERNAME%" 2>/dev/null | tr -d '\r' | tail -n 1 || true)"
    if [[ -n "${u:-}" && "${u:-}" != "%USERNAME%" ]]; then
      echo "$u"
      return
    fi
  fi
  echo "user"
}

REPO_ROOT=""
WIN_USER=""
WSL_DISTRO="${WSL_DISTRO_NAME:-Ubuntu-24.04}"
WSL_USER="$(whoami)"
DO_LINUX=1
DO_WINDOWS=1
DO_WRAPPER=1
CGO=""

while [[ $# -gt 0 ]]; do
  case "$1" in
    --repo-root)  REPO_ROOT="${2:-}"; shift 2;;
    --win-user)   WIN_USER="${2:-}"; shift 2;;
    --wsl-distro) WSL_DISTRO="${2:-}"; shift 2;;
    --wsl-user)   WSL_USER="${2:-}"; shift 2;;
    --no-linux)   DO_LINUX=0; shift;;
    --no-windows) DO_WINDOWS=0; shift;;
    --no-wrapper) DO_WRAPPER=0; shift;;
    --cgo)        CGO="${2:-}"; shift 2;;
    -h|--help)    usage; exit 0;;
    *)            die "unknown option: $1 (use --help)";;
  esac
done

[[ -n "$REPO_ROOT" ]] || REPO_ROOT="$(detect_repo_root)"
[[ -d "$REPO_ROOT" ]] || die "repo root not found: $REPO_ROOT"

[[ -n "$WIN_USER" ]] || WIN_USER="$(detect_win_user)"

have go || die "go not found in PATH"

# Paths
LINUX_OUT="$REPO_ROOT/oraclepack"
WIN_OUT="$REPO_ROOT/oraclepack.exe"

LINUX_INSTALL_DIR="$HOME/.local/bin"
LINUX_INSTALL_PATH="$LINUX_INSTALL_DIR/oraclepack"

WIN_HOME="/mnt/c/Users/$WIN_USER"
WIN_LOCAL_BIN_DIR="$WIN_HOME/.local/bin"
WIN_LOCAL_BIN_PATH="$WIN_LOCAL_BIN_DIR/oraclepack.exe"

WIN_GITBASH_BIN_DIR="$WIN_HOME/bin"
WIN_GITBASH_WRAPPER_PATH="$WIN_GITBASH_BIN_DIR/oraclepack"

# Optional CGO toggle
if [[ -n "$CGO" ]]; then
  [[ "$CGO" == "0" || "$CGO" == "1" ]] || die "--cgo must be 0 or 1"
  export CGO_ENABLED="$CGO"
fi

cd "$REPO_ROOT"

# Build binaries
if [[ "$DO_LINUX" -eq 1 ]]; then
  echo "==> Building Linux (WSL) binary: $LINUX_OUT"
  go build -o "$LINUX_OUT" ./cmd/oraclepack
fi

if [[ "$DO_WINDOWS" -eq 1 ]]; then
  echo "==> Building Windows amd64 exe: $WIN_OUT"
  GOOS=windows GOARCH=amd64 go build -o "$WIN_OUT" ./cmd/oraclepack
fi

# Install binaries
if [[ "$DO_LINUX" -eq 1 ]]; then
  echo "==> Installing Linux binary -> $LINUX_INSTALL_PATH"
  mkdir -p "$LINUX_INSTALL_DIR"
  cp -f "$LINUX_OUT" "$LINUX_INSTALL_PATH"
fi

if [[ "$DO_WINDOWS" -eq 1 ]]; then
  echo "==> Installing Windows exe -> $WIN_LOCAL_BIN_PATH"
  mkdir -p "$WIN_LOCAL_BIN_DIR"
  cp -f "$WIN_OUT" "$WIN_LOCAL_BIN_PATH"
fi

# Write Git Bash wrapper (stored on Windows filesystem)
if [[ "$DO_WRAPPER" -eq 1 ]]; then
  echo "==> Writing Git Bash wrapper -> $WIN_GITBASH_WRAPPER_PATH"
  mkdir -p "$WIN_GITBASH_BIN_DIR"

  cat > "$WIN_GITBASH_WRAPPER_PATH" <<EOF
#!/usr/bin/env bash
set -euo pipefail

# Git for Windows (Git Bash) path-conversion off for this exec call.
# Required so /home/... is not rewritten into C:/Program Files/Git/...
MSYS_NO_PATHCONV=1 exec wsl.exe -d ${WSL_DISTRO@Q} -u ${WSL_USER@Q} -- ${LINUX_INSTALL_PATH@Q} "\$@"
EOF

  # Ensure LF line endings (in case editor/tool wrote CRLF) and best-effort executable bit.
  sed -i 's/\r$//' "$WIN_GITBASH_WRAPPER_PATH" || true
  chmod +x "$WIN_GITBASH_WRAPPER_PATH" 2>/dev/null || true

  echo "==> Note: In Git Bash, ensure ~/bin is in PATH (so 'oraclepack' resolves to this wrapper)."
fi

echo "==> Done."
echo "    WSL binary:     $LINUX_INSTALL_PATH"
echo "    Windows exe:    $WIN_LOCAL_BIN_PATH"
echo "    Git Bash wrap:  $WIN_GITBASH_WRAPPER_PATH"
```

Run (from repo root in WSL):

```bash
chmod +x scripts/build_install_oraclepack.sh
scripts/build_install_oraclepack.sh
```

Optional example:

```bash
scripts/build_install_oraclepack.sh --win-user user --wsl-distro Ubuntu-24.04 --wsl-user user
```


--- docs/oracle-pack-2026-01-09.md ---
# Oracle Pack — Unknown (Gold Stage 1)

## Parsed args
- codebase_name: Unknown
- constraints: None
- non_goals: None
- team_size: Unknown
- deadline: Unknown
- out_dir: docs/oracle-questions-2026-01-09
- oracle_cmd: oracle
- oracle_flags: --files-report
- engine: None
- model: None
- extra_files: (none)

Notes:
- Template is the contract. Keep the pack runner-ingestible.
- Exactly one fenced bash block in this whole document.
- Exactly 20 steps, numbered 01..20.
- Each step includes: ROI= impact= confidence= effort= horizon= category= reference=
- Categories must be exactly the fixed set used in Coverage check.

## Commands
```bash
# Optional preflight pattern:
# - Add --dry-run summary to preview what will be sent, and keep --files-report enabled when available.

# 01) ROI=4.2 impact=High confidence=High effort=Low horizon=Immediate category=contracts/interfaces reference=internal/cli/root.go
oracle \
  --files-report \
  --write-output "docs/oracle-questions-2026-01-09/01-contracts-interfaces-surface.md" \
  -f "README.md" \
  -f "internal/cli/root.go" \
  -p "$(cat <<'PROMPT'
Strategist question #01

Reference: internal/cli/root.go
Category: contracts/interfaces
Horizon: Immediate
ROI: 4.2 (impact=High, confidence=High, effort=Low)

Question:
Identify the primary public interface(s) of this system (CLI commands, flags, and pack format surface). For each, list the key inputs/outputs and where they are defined in the code.

Rationale (one sentence):
We need a trustworthy map of the system's outside-facing contract before deeper planning.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–6 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 02) ROI=3.6 impact=High confidence=Medium effort=Medium horizon=Immediate category=contracts/interfaces reference=internal/tools/types.go
oracle \
  --files-report \
  --write-output "docs/oracle-questions-2026-01-09/02-contracts-interfaces-integration.md" \
  -f "internal/tools/types.go" \
  -f "internal/exec/runner.go" \
  -p "$(cat <<'PROMPT'
Strategist question #02

Reference: internal/tools/types.go
Category: contracts/interfaces
Horizon: Immediate
ROI: 3.6 (impact=High, confidence=Medium, effort=Medium)

Question:
What are the top integration points with external tools or systems (oracle CLI, shell environment, filesystem, optional task-master/codex/gemini prefixes)? For each, point to the contract or config that declares it.

Rationale (one sentence):
Integration boundaries drive risk, deployment needs, and test strategy.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–6 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 03) ROI=3.4 impact=High confidence=Medium effort=Medium horizon=NearTerm category=invariants reference=internal/pack/parser.go
oracle \
  --files-report \
  --write-output "docs/oracle-questions-2026-01-09/03-invariants-domain.md" \
  -f "internal/pack/parser.go" \
  -f "internal/pack/types.go" \
  -p "$(cat <<'PROMPT'
Strategist question #03

Reference: internal/pack/parser.go
Category: invariants
Horizon: NearTerm
ROI: 3.4 (impact=High, confidence=Medium, effort=Medium)

Question:
List the system's most important invariants about pack structure and step metadata (e.g., numbering, ROI parsing, header format). For each, show where it is enforced or where enforcement is missing.

Rationale (one sentence):
Invariants define correctness and are the backbone of reliable changes.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–8 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 04) ROI=3.1 impact=Medium confidence=Medium effort=Medium horizon=NearTerm category=invariants reference=internal/pack/output_check.go
oracle \
  --files-report \
  --write-output "docs/oracle-questions-2026-01-09/04-invariants-validation.md" \
  -f "internal/pack/output_check.go" \
  -f "internal/pack/parser.go" \
  -p "$(cat <<'PROMPT'
Strategist question #04

Reference: internal/pack/output_check.go
Category: invariants
Horizon: NearTerm
ROI: 3.1 (impact=Medium, confidence=Medium, effort=Medium)

Question:
Where does validation happen (pack parsing, output section validation, tool detection)? Identify the validation boundaries and the most likely gaps that could cause inconsistent state or false positives.

Rationale (one sentence):
Knowing validation boundaries prevents regressions and reduces correctness risk.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–8 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 05) ROI=3.0 impact=Medium confidence=Medium effort=Low horizon=NearTerm category=caching/state reference=internal/state/types.go
oracle \
  --files-report \
  --write-output "docs/oracle-questions-2026-01-09/05-caching-state-layers.md" \
  -f "internal/state/types.go" \
  -f "internal/state/persist.go" \
  -p "$(cat <<'PROMPT'
Strategist question #05

Reference: internal/state/types.go
Category: caching/state
Horizon: NearTerm
ROI: 3.0 (impact=Medium, confidence=Medium, effort=Low)

Question:
What stateful components exist (run state, report artifacts, in-memory tracking within a run)? For each, describe lifecycle, persistence, and where it is implemented.

Rationale (one sentence):
State and caching are common sources of subtle bugs and performance issues.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–8 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 06) ROI=2.6 impact=Medium confidence=Low effort=Medium horizon=NearTerm category=caching/state reference=internal/app/run.go
oracle \
  --files-report \
  --write-output "docs/oracle-questions-2026-01-09/06-caching-state-consistency.md" \
  -f "internal/app/run.go" \
  -f "internal/state/persist.go" \
  -p "$(cat <<'PROMPT'
Strategist question #06

Reference: internal/app/run.go
Category: caching/state
Horizon: NearTerm
ROI: 2.6 (impact=Medium, confidence=Low, effort=Medium)

Question:
Identify the top consistency risks between in-memory run state and persisted state/report files (e.g., partial writes, skipped updates, resume behavior). Where are the knobs/configs that influence state persistence?

Rationale (one sentence):
Consistency failure modes often surface as "random bugs" and are expensive to debug.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–8 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 07) ROI=2.4 impact=Medium confidence=Low effort=Medium horizon=NearTerm category=background jobs reference=internal/app/run.go
oracle \
  --files-report \
  --write-output "docs/oracle-questions-2026-01-09/07-background-jobs-discovery.md" \
  -f "internal/app/run.go" \
  -f "internal/tui/tui.go" \
  -p "$(cat <<'PROMPT'
Strategist question #07

Reference: internal/app/run.go
Category: background jobs
Horizon: NearTerm
ROI: 2.4 (impact=Medium, confidence=Low, effort=Medium)

Question:
What background jobs/workers/scheduled tasks exist (if any)? For each, identify trigger mechanism, payload, retries, idempotency, and where it is defined.

Rationale (one sentence):
Background work affects reliability, cost, and operational complexity.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–8 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 08) ROI=2.2 impact=Medium confidence=Low effort=High horizon=NearTerm category=background jobs reference=internal/exec/runner.go
oracle \
  --files-report \
  --write-output "docs/oracle-questions-2026-01-09/08-background-jobs-reliability.md" \
  -f "internal/exec/runner.go" \
  -f "internal/app/run.go" \
  -p "$(cat <<'PROMPT'
Strategist question #08

Reference: internal/exec/runner.go
Category: background jobs
Horizon: NearTerm
ROI: 2.2 (impact=Medium, confidence=Low, effort=High)

Question:
Where are the main reliability controls for any background or long-running work (timeouts, retries, cancellation, concurrency limits), and what is missing or inconsistent?

Rationale (one sentence):
Reliability controls prevent incident loops and data corruption.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–8 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 09) ROI=3.3 impact=High confidence=Medium effort=Low horizon=Immediate category=observability reference=internal/report/generate.go
oracle \
  --files-report \
  --write-output "docs/oracle-questions-2026-01-09/09-observability-signals.md" \
  -f "internal/report/generate.go" \
  -f "internal/report/types.go" \
  -p "$(cat <<'PROMPT'
Strategist question #09

Reference: internal/report/generate.go
Category: observability
Horizon: Immediate
ROI: 3.3 (impact=High, confidence=Medium, effort=Low)

Question:
What observability signals exist (run reports, warnings, stdout/stderr logs), and what are the primary identifiers for correlating a step/run across components? Point to the code/config that defines them.

Rationale (one sentence):
You cannot operate or improve what you cannot measure or debug quickly.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–8 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 10) ROI=3.0 impact=High confidence=Medium effort=Medium horizon=Immediate category=observability reference=internal/report/io.go
oracle \
  --files-report \
  --write-output "docs/oracle-questions-2026-01-09/10-observability-gaps.md" \
  -f "internal/report/io.go" \
  -f "internal/exec/runner.go" \
  -p "$(cat <<'PROMPT'
Strategist question #10

Reference: internal/report/io.go
Category: observability
Horizon: Immediate
ROI: 3.0 (impact=High, confidence=Medium, effort=Medium)

Question:
Where are the biggest observability gaps (missing structured logs, missing metrics, missing trace spans)? Recommend the smallest additions that would most improve debugging.

Rationale (one sentence):
Targeted observability improvements compound across all future changes.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–8 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 11) ROI=3.2 impact=High confidence=Medium effort=Low horizon=Immediate category=permissions reference=internal/cli/root.go
oracle \
  --files-report \
  --write-output "docs/oracle-questions-2026-01-09/11-permissions-model.md" \
  -f "internal/cli/root.go" \
  -f "internal/app/app.go" \
  -p "$(cat <<'PROMPT'
Strategist question #11

Reference: internal/cli/root.go
Category: permissions
Horizon: Immediate
ROI: 3.2 (impact=High, confidence=Medium, effort=Low)

Question:
What is the permission model (roles/scopes/claims/ACLs), and where is it defined? If none, explain what access assumptions the CLI makes (filesystem, shell, external tools).

Rationale (one sentence):
Permission rules are a high-risk area with security and product impact.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–8 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 12) ROI=2.8 impact=High confidence=Low effort=Medium horizon=Immediate category=permissions reference=internal/cli/run.go
oracle \
  --files-report \
  --write-output "docs/oracle-questions-2026-01-09/12-permissions-enforcement.md" \
  -f "internal/cli/run.go" \
  -f "internal/app/run.go" \
  -p "$(cat <<'PROMPT'
Strategist question #12

Reference: internal/cli/run.go
Category: permissions
Horizon: Immediate
ROI: 2.8 (impact=High, confidence=Low, effort=Medium)

Question:
Where are permissions or access checks enforced (if at all)? Identify enforcement chokepoints and any bypass risks for filesystem or shell execution.

Rationale (one sentence):
Enforcement consistency prevents privilege escalation and policy drift.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–8 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 13) ROI=2.5 impact=Medium confidence=Low effort=Medium horizon=NearTerm category=migrations reference=internal/state/types.go
oracle \
  --files-report \
  --write-output "docs/oracle-questions-2026-01-09/13-migrations-schema.md" \
  -f "internal/state/types.go" \
  -f "internal/state/persist.go" \
  -p "$(cat <<'PROMPT'
Strategist question #13

Reference: internal/state/types.go
Category: migrations
Horizon: NearTerm
ROI: 2.5 (impact=Medium, confidence=Low, effort=Medium)

Question:
How are schema/config migrations handled (state file schema versions, report schema changes, pack format evolution)? Identify tooling, version fields, and how migrations are applied during upgrades.

Rationale (one sentence):
Migration mechanics are critical for safe releases and rollbacks.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–8 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 14) ROI=2.2 impact=Medium confidence=Low effort=High horizon=NearTerm category=migrations reference=internal/state/persist.go
oracle \
  --files-report \
  --write-output "docs/oracle-questions-2026-01-09/14-migrations-compat.md" \
  -f "internal/state/persist.go" \
  -f "internal/state/types.go" \
  -p "$(cat <<'PROMPT'
Strategist question #14

Reference: internal/state/persist.go
Category: migrations
Horizon: NearTerm
ROI: 2.2 (impact=Medium, confidence=Low, effort=High)

Question:
What are the backward/forward compatibility expectations for state and report files (e.g., loading older state, changing schema_version)? Identify where compatibility is ensured or currently risky.

Rationale (one sentence):
Compatibility strategy prevents outages during upgrades.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–8 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 15) ROI=3.1 impact=High confidence=Medium effort=Medium horizon=NearTerm category=UX flows reference=internal/tui/tui.go
oracle \
  --files-report \
  --write-output "docs/oracle-questions-2026-01-09/15-ux-flows-primary.md" \
  -f "internal/tui/tui.go" \
  -f "internal/cli/run.go" \
  -p "$(cat <<'PROMPT'
Strategist question #15

Reference: internal/tui/tui.go
Category: UX flows
Horizon: NearTerm
ROI: 3.1 (impact=High, confidence=Medium, effort=Medium)

Question:
What are the primary user/operator flows (TUI navigation, step selection, overrides, run/resume)? Map each to the main components/modules involved and note key state transitions.

Rationale (one sentence):
Flow maps reveal critical paths and help prioritize work with user impact.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 16) ROI=2.7 impact=Medium confidence=Medium effort=Medium horizon=NearTerm category=UX flows reference=internal/tui/overrides_flow.go
oracle \
  --files-report \
  --write-output "docs/oracle-questions-2026-01-09/16-ux-flows-edgecases.md" \
  -f "internal/tui/overrides_flow.go" \
  -f "internal/tui/tui.go" \
  -p "$(cat <<'PROMPT'
Strategist question #16

Reference: internal/tui/overrides_flow.go
Category: UX flows
Horizon: NearTerm
ROI: 2.7 (impact=Medium, confidence=Medium, effort=Medium)

Question:
For the primary flows, what are the top edge cases and "gotchas" (validation failures, canceled overrides, partial runs, retries)? Identify where these cases are handled and where they are missing.

Rationale (one sentence):
Edge-case handling is where many UX and reliability issues originate.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 17) ROI=3.4 impact=High confidence=Medium effort=Low horizon=Immediate category=failure modes reference=internal/errors/errors.go
oracle \
  --files-report \
  --write-output "docs/oracle-questions-2026-01-09/17-failure-modes-taxonomy.md" \
  -f "internal/errors/errors.go" \
  -f "internal/exec/runner.go" \
  -p "$(cat <<'PROMPT'
Strategist question #17

Reference: internal/errors/errors.go
Category: failure modes
Horizon: Immediate
ROI: 3.4 (impact=High, confidence=Medium, effort=Low)

Question:
What is the failure-mode taxonomy of this system (invalid pack, execution failure, config errors, subprocess failures)? Identify where failures are classified/handled and what is surfaced to users/operators.

Rationale (one sentence):
Explicit failure handling prevents incidents and reduces user-facing errors.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 18) ROI=3.0 impact=High confidence=Medium effort=Medium horizon=Immediate category=failure modes reference=internal/app/run.go
oracle \
  --files-report \
  --write-output "docs/oracle-questions-2026-01-09/18-failure-modes-resilience.md" \
  -f "internal/app/run.go" \
  -f "internal/exec/runner.go" \
  -p "$(cat <<'PROMPT'
Strategist question #18

Reference: internal/app/run.go
Category: failure modes
Horizon: Immediate
ROI: 3.0 (impact=High, confidence=Medium, effort=Medium)

Question:
What resilience mechanisms exist (retry loops, output verification retries, stop-on-fail, resume), and which critical paths lack them? Where are these configured?

Rationale (one sentence):
Resilience patterns determine real-world reliability under stress.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 19) ROI=2.6 impact=Medium confidence=Low effort=Medium horizon=NearTerm category=feature flags reference=internal/overrides/types.go
oracle \
  --files-report \
  --write-output "docs/oracle-questions-2026-01-09/19-feature-flags-inventory.md" \
  -f "internal/overrides/types.go" \
  -f "internal/cli/run.go" \
  -p "$(cat <<'PROMPT'
Strategist question #19

Reference: internal/overrides/types.go
Category: feature flags
Horizon: NearTerm
ROI: 2.6 (impact=Medium, confidence=Low, effort=Medium)

Question:
What feature-flag or override system exists (flag injection, step targeting, ROI filtering)? Inventory the flags/overrides and identify where they are defined, evaluated, and documented.

Rationale (one sentence):
Flags enable safe rollout and experimentation and reduce release risk.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 20) ROI=2.4 impact=Medium confidence=Low effort=High horizon=NearTerm category=feature flags reference=internal/overrides/merge.go
oracle \
  --files-report \
  --write-output "docs/oracle-questions-2026-01-09/20-feature-flags-rollout.md" \
  -f "internal/overrides/merge.go" \
  -f "internal/tui/overrides_flow.go" \
  -p "$(cat <<'PROMPT'
Strategist question #20

Reference: internal/overrides/merge.go
Category: feature flags
Horizon: NearTerm
ROI: 2.4 (impact=Medium, confidence=Low, effort=High)

Question:
Describe the flag/override lifecycle (how overrides are created, validated, applied to steps, and retired). Identify minimum guardrails needed to prevent override drift.

Rationale (one sentence):
A disciplined rollout lifecycle reduces long-term complexity and operational risk.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"
```

Coverage check
--------------

Mark each as OK or Missing(<which step ids>):

*   contracts/interfaces: OK

*   invariants: OK

*   caching/state: OK

*   background jobs: OK

*   observability: OK

*   permissions: OK

*   migrations: OK

*   UX flows: OK

*   failure modes: OK

*   feature flags: OK


--- .taskmaster/docs/action-pack-artifact-gates-headless-steps_prd.md ---
1. Overview

Problem statement. Oraclepack Action Packs are executed as literal shell steps (documented by the tickets as `bash -lc ...` in the repo root), but oraclepack’s current “special handling” (command detection, flag injection/overrides, and validation via `oracle --dry-run summary`) is anchored to commands that begin with `oracle`, causing steps that use other CLIs (`tm`/`task-master`, `codex`, `gemini`) to be silently excluded from override/validation pipelines. This breaks Action Pack workflows that rely on non-oracle tools and placeholder steps in `ticket-action-pack.md` (notably steps 09–13 and 16) that must be replaced by headless/non-interactive automation while remaining ingestible as a 20-step single-`bash`-fence pack.

Who has the problem. Developers using oraclepack (CLI/TUI and CI) to run Action Packs for ticket-to-PR workflows, especially in environments where codex/gemini may be absent or interactive behavior can stall runs.

Why current solutions fail.

* Detection/validation is regex-anchored to `oracle` (`^(\\s*)(oracle)\\b` noted in tickets), so non-oracle steps can be skipped rather than validated.
* Placeholder steps in the action pack template prevent end-to-end execution with expected artifacts, and missing binaries on PATH can fail runs unless explicitly guarded.

Success metrics (measurable).

* Validation coverage: 0 “silently excluded” steps due solely to lacking an `oracle ...` prefix; steps beginning with `tm`/`task-master`, `codex`, or `gemini` must be detected and included in validation output.
* Artifact gates: for the Action Pack workflow, required artifacts are produced or an explicit “skipped due to missing binary” record is emitted:

  * Step 09: `.oraclepack/ticketify/next.json`
  * Step 10: `.oraclepack/ticketify/codex-implement.md`
  * Step 11: `.oraclepack/ticketify/codex-verify.md` OR `.oraclepack/ticketify/gemini-review.json`
  * Step 16: `.oraclepack/ticketify/PR.md`
* Headless execution: new steps do not block on interactivity in CI (codex uses non-interactive `codex exec` for automation). ([OpenAI Developers][1])
* Backwards compatibility: existing behavior for `oracle ...` steps remains unchanged; steps 01–07 semantics in `ticket-action-pack.md` remain unchanged.

Constraints, integrations, assumptions.

* Execution semantics remain “literal shell via `bash -lc ...`” (so environment/login-shell behaviors remain relevant).  ([gnu.org][2])
* The Action Pack “pack contract” remains: exactly one fenced `bash` block; exactly 20 steps; steps are self-contained; output files are written as shown; artifact paths are stable unless versioned.
* Tooling integration targets: `tm`/`task-master`, OpenAI Codex CLI (`codex exec`), and `gemini` CLI (exact flags/overrides for non-oracle tools are not fully specified by the provided evidence; treat as an explicit open question and implement detection + guardrails first).  ([OpenAI Developers][1])

1. Capability Tree (Functional Decomposition)

Capability: Pack specification and template management

* Feature: Action Pack execution semantics documentation (MVP)

  * Description: Publish user-facing docs for how Action Pack steps execute and what oraclepack “special handling” applies to.
  * Inputs: Pack file content; current execution implementation behavior.
  * Outputs: Documentation pages and CLI/TUI help text updates.
  * Behavior: Document that steps execute as `bash -lc ...` in repo root and that special handling currently applies by command classification; explicitly list supported command prefixes and how they’re handled.
* Feature: `ticket-action-pack.md` template replacement (MVP)

  * Description: Replace placeholder steps 09–13 and 16 with headless `gemini` + non-interactive `codex exec` automation while keeping the pack contract.
  * Inputs: Template source; required artifact contract.
  * Outputs: Updated `ticket-action-pack.md` content that remains ingestible.
  * Behavior: Keep steps 01–07 unchanged; ensure steps produce required artifacts; add `command -v` guards to degrade gracefully when binaries are missing.

Capability: Step classification and dispatch

* Feature: Multi-tool command detection (MVP)

  * Description: Expand step command detection beyond `oracle` to include `tm`/`task-master`, `codex`, and `gemini`.
  * Inputs: Step shell text.
  * Outputs: Classified command prefix + normalized “tool kind” for each step.
  * Behavior: Replace oracle-anchored regex with a multi-prefix detector; preserve existing classification for `oracle ...` commands.
* Feature: Tool-specific dispatch policy (non-MVP, gated by open questions)

  * Description: Define what override injection means per tool and how it is applied.
  * Inputs: Tool kind; user config; step command.
  * Outputs: Possibly transformed command + metadata describing applied overrides.
  * Behavior: For `oracle`, preserve existing injection/behavior; for other tools, initially perform detection-only unless explicit policy is defined.

Capability: Execution engine and environment guards

* Feature: Headless execution mode selection (MVP)

  * Description: Ensure steps use non-interactive invocations when specified by the pack (e.g., `codex exec` for Codex automation).
  * Inputs: Step definition; tool kind; environment.
  * Outputs: Executed process result and logs.
  * Behavior: Run steps as `bash -lc "<step>"`; when using codex, prefer `codex exec` semantics suitable for scripts/CI.  ([OpenAI Developers][1])
* Feature: Missing-binary graceful skip (MVP)

  * Description: When `codex`/`gemini`/`tm` is missing, record an explicit skip instead of failing the whole run (where the step is designed to be optional under missing tools).
  * Inputs: Tool kind; PATH lookup result.
  * Outputs: Step result marked Skipped with reason.
  * Behavior: Enforce “skip vs block” rules per step; do not hide the skip; write into state/report.

Capability: Validation and override pipeline

* Feature: Validation inclusion for non-oracle tools (MVP)

  * Description: Ensure steps using `tm`/`codex`/`gemini` are included in validation output (not skipped because they are non-oracle).
  * Inputs: Pack; classified steps; installed tool availability.
  * Outputs: Validation report listing status per step and per-tool checks.
  * Behavior: Keep current oracle-only validation intact; add additional validators that (at minimum) check tool presence + required artifact gates for relevant steps.
* Feature: Artifact gate validation (MVP)

  * Description: Validate that specific steps produce required files under `.oraclepack/ticketify/` (or emit explicit skip records).
  * Inputs: Step results; filesystem state.
  * Outputs: Pass/fail/skip gate status per required artifact.
  * Behavior: Treat artifact existence requirements as acceptance gates; align with CI/canary gating.

Capability: Artifact contract and run state/reporting

* Feature: Standardized `.oraclepack/ticketify/` outputs (MVP)

  * Description: Standardize and document expected output artifact names and locations for the Action Pack workflow.
  * Inputs: Step definitions; pack run.
  * Outputs: `.oraclepack/ticketify/next.json`, `codex-implement.md`, `codex-verify.md`/`gemini-review.json`, `PR.md`.
  * Behavior: Do not move or rename without versioning; enforce stability via validation gates.
* Feature: Persistent run state + report (MVP)

  * Description: Persist per-step results and a summary report to support resumability and UX flows.
  * Inputs: Pack run events.
  * Outputs: `ticket-action-pack.state.json` and `ticket-action-pack.report.json` (names referenced by the tickets/UX notes).
  * Behavior: Track step status (Succeeded/Failed/Skipped/Blocked), logs pointers, timestamps, and artifact gate outcomes.

Capability: UX and operational observability

* Feature: Run journey states in TUI/CLI (MVP)

  * Description: Represent the “Selected → Parsed/Validated → Ready → Running(step N) → {Succeeded|Failed|Skipped|Blocked} → Completed/Resumable” state machine.
  * Inputs: State/report files; runner events.
  * Outputs: CLI/TUI rendering and exit codes suitable for CI.
  * Behavior: Display step-level status and explicit skip reasons (missing tool, ROI filtering, etc.).
* Feature: Operational alerts/telemetry hooks (non-MVP)

  * Description: Track known failure modes (missing PATH binaries, interactivity stalls, ROI-filter skips).
  * Inputs: Run events.
  * Outputs: Metrics/log events.
  * Behavior: Emit structured events suitable for CI parsing; highlight ROI-filter behavior explicitly.

1. Repository Structure + Module Definitions (Structural Decomposition)

Assumption: oraclepack is implemented in Go (ticket hints reference `**/*report*.go`, `**/*state*.go`). If the repo language differs, keep module boundaries and public exports, but translate file layout accordingly.

Proposed structure (new or refactored packages only; avoid “utils” buckets):

oraclepack/

* cmd/oraclepack/

  * main.go
  * cli.go (wire commands)
* internal/foundation/

  * errors.go
  * config.go
  * fs.go
  * clock.go
* internal/shell/

  * runner.go
  * env.go
* internal/pack/

  * types.go
  * parser.go
  * linter.go
* internal/dispatch/

  * detector.go
  * policy.go
* internal/tools/

  * types.go
  * oracle.go
  * codex.go
  * gemini.go
  * taskmaster.go
* internal/artifacts/

  * contract.go
  * gates.go
* internal/state/

  * models.go
  * writer.go
  * reader.go
  * report.go
* internal/validate/

  * validate.go
  * oracle_validator.go
  * tool_presence_validator.go
  * artifact_gate_validator.go
* internal/templates/

  * ticket_action_pack.go (template source or embed logic)
  * render.go
* internal/tui/ (if applicable)

  * screens.go
  * run_flow.go

Module definitions (single responsibility + exports):

Module: internal/foundation

* Responsibility: Common primitives (errors, config, filesystem IO abstraction, clock).
* Exports:

  * type `Config`
  * `LoadConfig() (Config, error)`
  * `ReadFile(path) ([]byte, error)`, `WriteFileAtomic(path, data) error`
  * `Now() time.Time`
  * typed errors (e.g., `ErrMissingBinary`)

Module: internal/shell

* Responsibility: Execute shell steps using the oraclepack contract (`bash -lc ...`) and capture outputs.
* Exports:

  * `RunBashLoginCommand(cmd string, env EnvSpec) (ExecResult, error)`
  * `DetectBinary(name string) (path string, ok bool)`
    Notes: `bash -lc` semantics rely on login-shell startup behavior; document and test accordingly. ([gnu.org][2])

Module: internal/pack

* Responsibility: Parse pack documents into typed steps and enforce pack-level contracts where applicable.
* Exports:

  * `ParsePack(content []byte) (Pack, error)`
  * `LintPack(pack Pack) ([]LintIssue, error)`
  * types: `Pack`, `Step`, `StepID`, `StepMeta`

Module: internal/dispatch

* Responsibility: Classify a step’s primary tool and apply dispatch policy decisions (detection-first).
* Exports:

  * `DetectTool(step Step) ToolKind`
  * `BuildExecutionPlan(pack Pack) ExecutionPlan`
  * `ApplyPolicy(step Step, cfg Config) (Step, PolicyNotes)` (oracle-preserving; other tools may be no-op initially)

Module: internal/tools

* Responsibility: Tool-specific knowledge that is safe to centralize (command builders, presence checks, recommended non-interactive forms).
* Exports:

  * `NonInteractiveHint(kind ToolKind) string`
  * `PresenceCheck(kind ToolKind) (ok bool, details string)`
  * `BuildCodexExec(prompt string, flags CodexFlags) string` (explicitly uses `codex exec` for automation). ([OpenAI Developers][1])

Module: internal/artifacts

* Responsibility: Define and evaluate artifact contracts (expected outputs, gating rules).
* Exports:

  * `ActionPackArtifactContract() ArtifactContract`
  * `EvaluateGates(contract ArtifactContract, fs FS) GateResults`
  * types: `ArtifactContract`, `ArtifactGate`, `GateResult`

Module: internal/state

* Responsibility: Persist and load run state and a summarized report.
* Exports:

  * `WriteState(path string, state RunState) error`
  * `ReadState(path string) (RunState, error)`
  * `WriteReport(path string, report RunReport) error`
  * types: `RunState`, `RunReport`, `StepResult`, `StepStatus`

Module: internal/validate

* Responsibility: Compose validators and produce a validation report.
* Exports:

  * `ValidatePack(pack Pack, cfg Config) (ValidationReport, error)`
  * validators:

    * `ValidateOracleDryRun(...)`
    * `ValidateToolPresence(...)`
    * `ValidateArtifactGates(...)`
      Back-compat: oracle dry-run behavior remains intact; new validators add coverage for non-oracle steps.

Module: internal/templates

* Responsibility: Render official pack templates (including updated `ticket-action-pack`).
* Exports:

  * `RenderTicketActionPack(params TemplateParams) ([]byte, error)`

1. Dependency Chain (layers, explicit “Depends on: […]”)

Foundation layer (no dependencies)

* internal/foundation: Depends on: []
* internal/shell: Depends on: [internal/foundation] (uses config/env/error/fs abstractions)

Pack layer

* internal/pack: Depends on: [internal/foundation]
* internal/state: Depends on: [internal/foundation]

Tool knowledge layer

* internal/tools: Depends on: [internal/foundation]

Dispatch layer

* internal/dispatch: Depends on: [internal/pack, internal/tools, internal/foundation]

Artifacts layer

* internal/artifacts: Depends on: [internal/foundation]

Validation layer

* internal/validate: Depends on: [internal/pack, internal/dispatch, internal/tools, internal/artifacts, internal/foundation]

Template layer

* internal/templates: Depends on: [internal/foundation, internal/artifacts] (template aligns to artifact contract)

Orchestration/CLI layer

* cmd/oraclepack: Depends on: [internal/pack, internal/dispatch, internal/shell, internal/state, internal/validate, internal/templates, internal/foundation]

TUI layer (optional)

* internal/tui: Depends on: [cmd/oraclepack wiring or internal/* interfaces, internal/state, internal/validate]

1. Development Phases (Phase 0…N; entry/exit criteria; tasks with dependencies + acceptance criteria + test strategy)

Phase 0: Foundation
Entry criteria: repo builds/tests run.
Tasks:

* [ ] Implement internal/foundation (depends on: none)

  * Acceptance criteria: config + fs helpers compile; typed errors exist.
  * Test strategy: unit tests for config parsing; atomic write semantics.
* [ ] Implement internal/shell runner wrapper for `bash -lc` (depends on: internal/foundation)

  * Acceptance criteria: can execute a trivial command and capture stdout/stderr/exit code deterministically.
  * Test strategy: unit tests using temp dirs; integration test executing `bash -lc "echo ok"`; document login-shell implications. ([gnu.org][2])
    Exit criteria: other modules can execute shell commands via a stable interface.

Phase 1: Pack parse + state/report models
Entry criteria: Phase 0 complete.
Tasks:

* [ ] Implement internal/pack ParsePack + step model (depends on: internal/foundation)

  * Acceptance criteria: parses step IDs and metadata; detects malformed packs.
  * Test strategy: unit tests with fixtures including 20-step contract variants.
* [ ] Implement internal/state RunState/RunReport + JSON read/write (depends on: internal/foundation)

  * Acceptance criteria: can write/read `*.state.json` and `*.report.json`; preserves step status transitions.
  * Test strategy: round-trip tests; schema validation tests.
    Exit criteria: CLI can parse a pack and write an empty initialized state/report file set.

Phase 2: Multi-tool detection and tool presence checks
Entry criteria: Phase 1 complete.
Tasks:

* [ ] Implement internal/tools ToolKind + presence checks (depends on: internal/foundation)

  * Acceptance criteria: `oracle`, `tm`/`task-master`, `codex`, `gemini` kinds; presence results include details.
  * Test strategy: unit tests with PATH manipulation via test harness.
* [ ] Implement internal/dispatch DetectTool (depends on: internal/pack, internal/tools)

  * Acceptance criteria: steps are classified even when not starting with `oracle`; preserves `oracle` behavior.
  * Test strategy: table-driven tests with step strings that begin with whitespace and each prefix.
    Exit criteria: validation/execution can identify non-oracle steps reliably.

Phase 3: Artifact contract and Action Pack template updates
Entry criteria: Phase 2 complete.
Tasks:

* [ ] Implement internal/artifacts Action Pack contract + gate evaluation (depends on: internal/foundation)

  * Acceptance criteria: gates cover `.oraclepack/ticketify/{next.json,codex-implement.md,codex-verify.md|gemini-review.json,PR.md}`.
  * Test strategy: unit tests that create/miss files and verify gate statuses.
* [ ] Update internal/templates to render updated `ticket-action-pack` steps 09–13 and 16 (depends on: internal/foundation, internal/artifacts)

  * Acceptance criteria: template preserves 20-step single-fence contract; steps 01–07 unchanged; new steps use `command -v` guards and include non-interactive codex invocation guidance (`codex exec`).  ([OpenAI Developers][1])
  * Test strategy: golden-file tests for rendered template; lint tests for contract invariants.
    Exit criteria: a “golden” `ticket-action-pack.md` fixture exists and matches required artifacts and contract.

Phase 4: Validation pipeline expansion (compat-safe)
Entry criteria: Phase 3 complete.
Tasks:

* [ ] Extend internal/validate to include non-oracle steps (depends on: internal/pack, internal/dispatch, internal/tools, internal/artifacts)

  * Acceptance criteria: validation output includes `tm`/`codex`/`gemini` steps; oracle dry-run validation remains unchanged.
  * Test strategy: integration tests for (a) oracle-only pack, (b) pack with only `codex` prefix, ensuring no silent exclusion.
* [ ] Implement artifact gate validation for relevant steps (depends on: internal/artifacts, internal/state)

  * Acceptance criteria: artifact gates pass/fail/skip recorded; explicit skip records when binaries missing.
  * Test strategy: integration tests in two environments: with and without `codex`/`gemini` on PATH.
    Exit criteria: `oraclepack validate` can be used as a CI gate for Action Pack artifacts.

Phase 5: Execution UX and documentation surfaces
Entry criteria: Phase 4 complete.
Tasks:

* [ ] Implement explicit step statuses and journey rendering (depends on: internal/state, internal/validate, internal/shell)

  * Acceptance criteria: statuses include Succeeded/Failed/Skipped/Blocked; reason strings are shown; resume boundary is represented.
  * Test strategy: end-to-end test running subset steps (09–11, 16) with `--no-tui` and verifying state/report outputs.
* [ ] Publish docs for execution semantics and tool handling (depends on: internal/dispatch behavior finalized)

  * Acceptance criteria: docs clearly state `bash -lc` execution semantics and command classification rules; includes how to run codex in automation using `codex exec`.  ([OpenAI Developers][1])
    Exit criteria: end-to-end usable MVP: updated Action Pack template + validation gates + explicit skips + stable artifacts.

1. User Experience

Personas.

* CI maintainer: wants deterministic validation gates and non-interactive runs.
* Developer running local TUI/CLI: wants clear step-by-step progress, resumability, and artifact locations.

Key flows.

* Validate Action Pack.

  * User runs `oraclepack validate <pack>`.
  * Output includes per-step tool classification and validation coverage; no non-oracle steps are omitted.
  * Artifact gates reported for steps with expected outputs.
* Run Action Pack (headless).

  * User runs `oraclepack run --no-tui <pack>`.
  * State transitions follow: Selected → Parsed/Validated → Ready → Running(step N) → Step status → Completed/Resumable.
  * If `codex`/`gemini` is missing, affected steps are Skipped with explicit reasons; run continues where safe.
* Inspect artifacts.

  * User checks `.oraclepack/ticketify/` for standardized outputs; report links or enumerates them.

UI/UX notes tied to capabilities.

* Always show “why skipped” (missing binary vs ROI filtering vs policy) to avoid the “it didn’t run” ambiguity.
* Treat artifact gates as first-class status indicators in both CLI and TUI.

1. Technical Architecture

System components.

* Pack parser: converts pack markdown to typed steps.
* Dispatcher: classifies steps by tool prefix; preserves existing oracle behavior.
* Shell runner: executes `bash -lc` commands, capturing stdout/stderr and exit codes (login-shell semantics affect startup files and env). ([gnu.org][2])
* Validators:

  * Oracle validator: current `oracle --dry-run summary` behavior for oracle steps (unchanged).
  * Tool presence validator: checks `tm`/`codex`/`gemini` availability.
  * Artifact gate validator: asserts required outputs exist or a skip record exists.
* State/report store: writes `ticket-action-pack.state.json` and `ticket-action-pack.report.json`.

Data models (minimum).

* Pack: `{id, title, steps[]}`
* Step: `{id, meta{ROI, impact, confidence, effort, horizon, category, reference}, shell, toolKind}`
* StepResult: `{stepId, status, exitCode, startedAt, endedAt, toolKind, skipReason?, artifactsProduced[], logsRef?}`
* RunState: `{packId, currentStepId?, resultsByStepId, environment{toolPresence}}`
* RunReport: `{summaryCounts, gates[], failures[], warnings[]}`
* ArtifactContract: list of required outputs per step (Action Pack specific).

External tools / APIs.

* Codex CLI: use `codex exec` for non-interactive automation; it is explicitly positioned for scripts/CI and supports JSONL output for machine consumption. ([OpenAI Developers][1])
* (Out of scope for MVP, but present as tickets): MCP publishing/exposure work exists; not enough evidence included here to define interfaces safely.  Background context on MCP: it is an open standard for connecting assistants to tools/data via MCP servers/clients. ([Anthropic][3])

Key decisions and trade-offs.

* Decision: Implement “detection + validation inclusion” for non-oracle tools before “override injection” for them.

  * Rationale: Tickets explicitly call out silent exclusion as a problem; they also state unknowns about what overrides should mean for `tm`/`codex`/`gemini`.
  * Trade-off: Less automation initially, but avoids incorrect injection behavior and preserves compatibility.
* Decision: Enforce artifact gates via validation rather than relying on tool-specific dry-runs for all tools.

  * Rationale: Required artifacts are explicit rollout gates in the tickets.

1. Test Strategy

Test pyramid targets.

* Unit tests: ~70% (parsers, detectors, gate evaluators, state read/write).
* Integration tests: ~25% (validate/run flows with temp repos and PATH manipulation).
* End-to-end tests: ~5% (run the “golden” Action Pack subset in two environments).

Coverage minimums.

* Line: 85%+
* Branch: 80%+
* Critical modules (dispatch, validate, artifacts, state): 90%+

Critical scenarios per module.

* internal/dispatch

  * Happy path: detect `oracle`, `tm`, `task-master`, `codex`, `gemini` with leading whitespace.
  * Error cases: empty step shell; multiple commands; ambiguous prefixes.
* internal/validate

  * Oracle-only pack: behavior unchanged.
  * Non-oracle-only pack: steps included (no silent exclusion).
  * Mixed pack: oracle validator runs for oracle steps; presence and artifact validators run for others.
* internal/artifacts

  * Gates pass when files exist.
  * Gates fail when files missing and step not skipped.
  * Gates skip when binary missing and skip record written.
* internal/state

  * Round-trip JSON persistence and status transitions.
* E2E (per ticket recommendation)

  * Run `oraclepack validate` then `oraclepack run --no-tui` for steps 09–11 and 16 in environments with/without `codex`/`gemini` on PATH; assert artifact-path acceptance criteria.

1. Risks and Mitigations

Risk: Undefined override semantics for non-oracle tools.

* Impact: High (wrong behavior or broken workflows).
* Likelihood: High (explicitly called out as unknown).
* Mitigation: Detection-first + artifact gates + presence checks; introduce per-tool overrides only when specified by tickets/spec.
* Fallback: Keep non-oracle override injection as no-op with explicit warnings.

Risk: Interactivity stalls in CI even with “headless” flags.

* Impact: High.
* Likelihood: Medium.
* Mitigation: Prefer `codex exec` (non-interactive mode) for Codex automation; add timeouts and “Blocked” status with guidance. ([OpenAI Developers][1])

Risk: Missing binaries on PATH cause failures instead of explicit skips.

* Impact: Medium–High.
* Likelihood: High.
* Mitigation: Standard `command -v` guards in template; presence validator; explicit skip records.

Risk: ROI filtering hides required steps.

* Impact: Medium.
* Likelihood: Medium.
* Mitigation: Ensure required steps have ROI metadata or document running with ROI threshold = 0 during canary; surface ROI-based skips explicitly.

Risk: Artifact path drift breaks consumers.

* Impact: High.
* Likelihood: Medium.
* Mitigation: Treat `.oraclepack/ticketify/*` paths as versioned contract; add validation gate tests and golden fixtures.

1. Appendix

Evidence used (provided).

* Public surface changes and back-compat constraints for Action Packs: execution semantics (`bash -lc`), oracle-only special handling, need multi-tool detection, validation inclusion, template step replacements, and standardized artifacts.
* Validation/monitoring gates and recommended experiment using a golden fixture + PATH/no-PATH environments.
* UX journey states and state/report filenames.

External references (behavioral grounding).

* Bash login-shell startup behaviors relevant to `bash -lc` execution. ([gnu.org][2])
* Codex non-interactive mode via `codex exec` for scripts/CI. ([OpenAI Developers][1])
* MCP high-level definition (not MVP-scoped here, but referenced by tickets as future work). ([Anthropic][3])

Open questions (explicit).

* What exact override/flag injection policy is desired for `tm`/`task-master`, `codex`, and `gemini` (beyond presence checks and artifact gates)?
* Where is the authoritative generator/template source for `ticket-action-pack.md` in the codebase (tickets suggest it is not provided in the current evidence set)?

[1]: https://developers.openai.com/codex/noninteractive/ "Non-interactive mode"
[2]: https://www.gnu.org/s/bash/manual/html_node/Bash-Startup-Files.html "Bash Startup Files (Bash Reference Manual)"
[3]: https://www.anthropic.com/news/model-context-protocol "Introducing the Model Context Protocol \ Anthropic"


--- .taskmaster/docs/PRD.md ---
## 1) Overview

### Problem

You have “oracle question packs” stored as Markdown that contain a single fenced `bash` block with:

* a **prelude** (e.g., `set -euo pipefail`, `out_dir=...`, `mkdir -p ...`)
* **numbered steps** `# 01)` … `# 20)` that each run an `oracle ...` invocation

Today you can run these with an interactive Bash script, but you want a **polished, first-class CLI/TUI** built with Charm’s ecosystem.

### Target users

* Engineers / operators running packs repeatedly, wanting safe confirm-before-run, logs, resume, and good terminal UX.
* Maintainers who want pack validation and predictable behavior across machines/CI.

### Success metrics

* **Safety**: zero accidental executions (every step is explicitly confirmed unless `--yes` is set).
* **Reliability**: deterministic step parsing; clear non-zero exit codes on failure.
* **Observability**: each step yields a timestamped log file + a machine-readable run summary (JSON).
* **Usability**: fast navigation/filtering, preview of command and `--write-output`, resume after quit/crash.

### Constraints / assumptions

* Pack format is “one ` ```bash ` fence + numbered `# NN)` headers” as in your current pack output .
* `oracle` is installed and available in `PATH`, or provided via `--oracle-bin`.
* Default shell is `bash`; Windows support may require WSL/Git Bash or using `--shell` explicitly.

### “How hard is it?”

* **Low** effort for a “nice enough” wrapper using **gum** prompts around your existing script. Gum is explicitly designed to add “glamorous” prompts to shell scripts without writing Go. ([GitHub][1])
* **Moderate** effort for a **production-grade compiled CLI** with a full-screen TUI using **Bubble Tea** + **Bubbles** + **Lip Gloss**, with robust parsing, streaming logs, resume/state, and packaging polish. Bubble Tea is a Go TUI framework (Elm-style architecture) suited for full-window apps. ([GitHub][2])

**This PRD specifies the “most polished” option (compiled Go CLI + Bubble Tea TUI).**

---

## 2) Capability Tree (Functional Decomposition)

### Capability: Pack ingestion

#### Feature: Load pack from Markdown (MVP)

* **Description**: Read a Markdown pack file and extract the first ` ```bash ` fenced block.
* **Inputs**: `pack_path`
* **Outputs**: `bash_block_text`
* **Behavior**: Find the first matching fence start/end; error if missing/empty.

#### Feature: Parse steps + prelude (MVP)

* **Description**: Split extracted bash block into prelude and ordered step blocks based on `# NN)` headers.
* **Inputs**: `bash_block_text`
* **Outputs**: `Prelude`, `[]Step{number,title,body}`
* **Behavior**: Everything before first `# NN)` is prelude; each subsequent header starts a new step.

#### Feature: Detect derived metadata (MVP)

* **Description**: Best-effort parse of `out_dir="..."` (prelude) and `--write-output "..."` (step body).
* **Inputs**: `Prelude`, `Step.body`
* **Outputs**: `Pack.outDir?`, `Step.writeOutput?`
* **Behavior**: Regex-based extraction; never blocks execution if missing.

#### Feature: Validate pack structure (MVP)

* **Description**: Validate required structure and produce actionable errors.
* **Inputs**: `Pack`
* **Outputs**: `[]ValidationIssue`
* **Behavior**: Enforce “>=1 step”, step numbers are two-digit, no duplicates; warn on missing `--write-output`.

---

### Capability: Execution orchestration

#### Feature: Execute prelude once per run (MVP)

* **Description**: Run prelude in the chosen shell before running step bodies.
* **Inputs**: `Prelude`, `shell`, `workdir`, `env`
* **Outputs**: `PreludeResult`
* **Behavior**: If prelude fails, stop and mark run failed.

#### Feature: Execute a single step with streaming logs (MVP)

* **Description**: Run one step in a subprocess and capture stdout/stderr lines.
* **Inputs**: `Step`, `shell`, `workdir`, `env`
* **Outputs**: `StepResult{exitCode, startedAt, endedAt, logPath}`
* **Behavior**: Stream lines to UI/console and persist to a log file.

#### Feature: Inject flags into oracle invocation (MVP)

* **Description**: Optionally inject user-provided flags after `oracle` in each step.
* **Inputs**: `Step.body`, `injectFlags`
* **Outputs**: transformed `Step.body`
* **Behavior**: Rewrite only lines beginning with `oracle` (conservative, predictable).

#### Feature: Stop/continue policy on failures (MVP)

* **Description**: Define behavior when a step fails.
* **Inputs**: `StepResult`, `policy`
* **Outputs**: next action (stop / prompt / continue)
* **Behavior**: Default: stop and require explicit user action to continue.

---

### Capability: User interaction (TUI + CLI)

#### Feature: Full-screen TUI step list (MVP)

* **Description**: Show steps with status (Pending/Running/OK/Failed/Skipped) and metadata.
* **Inputs**: `Pack`, `RunState`
* **Outputs**: terminal UI
* **Behavior**: Navigate, filter, select; show `--write-output` and command preview.

#### Feature: Confirm-before-run per step (MVP)

* **Description**: Require confirmation before executing a step (unless `--yes`).
* **Inputs**: selected `Step`, `mode`
* **Outputs**: run / skip decision
* **Behavior**: Prompt with clear consequences and output path.

#### Feature: Plain mode (no TUI) (MVP)

* **Description**: Provide `--no-tui` mode that prompts in-line (or runs with `--yes`).
* **Inputs**: same as TUI
* **Outputs**: console output
* **Behavior**: Deterministic text output and exit codes.

#### Feature: Markdown rendering in preview (Non-MVP)

* **Description**: Render relevant Markdown (e.g., step prompt text) in a styled terminal view.
* **Inputs**: markdown snippet
* **Outputs**: ANSI-rendered text
* **Behavior**: Use Glamour for stylesheet-based terminal Markdown rendering. ([GitHub][3])

---

### Capability: State, resume, and reporting

#### Feature: Persist run state (MVP)

* **Description**: Save step statuses and pointers so a run can be resumed.
* **Inputs**: `RunState`
* **Outputs**: `state.json` in a deterministic directory
* **Behavior**: Atomic writes; load on startup when `--resume` is set.

#### Feature: Machine-readable summary report (MVP)

* **Description**: Write `run.json` with per-step results and log paths.
* **Inputs**: `RunState`, `Pack`
* **Outputs**: JSON file
* **Behavior**: Stable schema version; include environment metadata (shell, cwd, start time).

---

### Capability: Packaging and operational polish

#### Feature: Shell completions + man/help text (Non-MVP)

* **Description**: Provide completions and high-quality help UX.
* **Inputs**: CLI command model
* **Outputs**: completion scripts, man page
* **Behavior**: Generated during release; consistent flag naming.

#### Feature: Release automation (Non-MVP)

* **Description**: Produce multi-platform binaries and package manifests (brew/scoop).
* **Inputs**: tags/releases
* **Outputs**: release artifacts
* **Behavior**: Use GoReleaser capabilities for Homebrew/Scoop publishing. ([GoReleaser][4])

---

## 3) Repository Structure + Module Definitions (Structural Decomposition)

### Proposed repository layout

```
orpack/
  cmd/orpack/
    main.go
  internal/
    app/            # application composition (wiring)
    cli/            # command model, flags, help text
    pack/           # markdown fence extraction + step parsing + validation
    exec/           # subprocess execution + streaming + log persistence
    state/          # run state model + persistence
    report/         # run summary JSON schema + writer
    tui/            # Bubble Tea model/view; Bubbles components usage
    render/         # markdown-to-ANSI rendering (Glamour)
    errors/         # typed errors + exit codes
```

### Module: `internal/pack`

* **Responsibility**: Parse pack Markdown into `Pack{Prelude, Steps}` and validate format.
* **Exports**:

  * `Load(path string) (Pack, error)`
  * `Validate(p Pack) []Issue`
  * `DeriveMetadata(p *Pack)` (fills `outDir`, `writeOutput` best-effort)

### Module: `internal/exec`

* **Responsibility**: Execute prelude/steps in a shell; stream output; write log files.
* **Exports**:

  * `Runner{Shell, WorkDir, Env, InjectFlags}`
  * `RunPrelude(ctx, prelude string) (Result, error)`
  * `RunStep(ctx, prelude string, step Step, logDir string, onLine func(Line)) (Result, error)`

### Module: `internal/state`

* **Responsibility**: Track statuses and persist/restore run state.
* **Exports**:

  * `RunState` (pack hash, start time, per-step status/result)
  * `LoadState(path string) (RunState, error)`
  * `SaveStateAtomic(path string, s RunState) error`

### Module: `internal/report`

* **Responsibility**: Write stable JSON summary for automation/CI.
* **Exports**:

  * `ReportV1`
  * `WriteReport(path string, r ReportV1) error`

### Module: `internal/tui`

* **Responsibility**: Full-screen terminal UX using Bubble Tea + Bubbles + Lip Gloss.
* **Exports**:

  * `Run(p Pack, runner *exec.Runner, opts Options) error`
* **Notes**: Bubble Tea provides the stateful TUI architecture; Bubbles provides ready components; Lip Gloss provides styling/layout. ([GitHub][2])

### Module: `internal/render`

* **Responsibility**: Render Markdown snippets to ANSI for previews/help panes.
* **Exports**:

  * `RenderMarkdown(md string, style Style) (string, error)`
* **Notes**: Use Glamour for stylesheet-based terminal markdown rendering. ([GitHub][3])

### Module: `internal/cli`

* **Responsibility**: Parse args, define subcommands, map flags to app behavior.
* **Exports**:

  * `Execute(argv []string) int` (returns exit code)

### Module: `internal/errors`

* **Responsibility**: Centralize error types and exit code mapping.
* **Exports**:

  * `type Code int`
  * `ExitCode(err error) int`

### Module: `internal/app`

* **Responsibility**: Wire modules into a runnable application.
* **Exports**:

  * `New(...) *App`
  * `RunTUI(...) error`
  * `RunPlain(...) error`
  * `Validate(...) error`

---

## 4) Dependency Chain (layers, explicit “Depends on: […]”)

### Foundation Layer

* **errors**: exit codes and typed errors. Depends on: []
* **pack**: parsing + validation. Depends on: [errors]
* **state**: state model + persistence. Depends on: [errors]
* **report**: report schema + writer. Depends on: [errors]
* **render**: markdown rendering adapter. Depends on: [errors]

### Execution Layer

* **exec**: shell execution + streaming + logs. Depends on: [errors, pack]

### Application Layer

* **app**: composition and run orchestration. Depends on: [pack, exec, state, report, render, errors]

### Interaction Layer

* **cli**: command/flags and entrypoints. Depends on: [app, errors]
* **tui**: Bubble Tea UI. Depends on: [app, pack, state, render, errors]

(Acyclic by construction; `app` is the orchestration hub, `errors` is the base.)

---

## 5) Development Phases (Phase 0…N; entry/exit criteria; tasks with dependencies + acceptance criteria + test strategy)

### Phase 0: Foundation

**Entry criteria**: repo initialized; CI runs `go test ./...`

* **Task**: Implement `internal/errors`

  * Depends on: []
  * Acceptance criteria: stable exit codes; unit tests for mapping
  * Test strategy: unit tests
* **Task**: Implement `internal/pack` parsing + validation

  * Depends on: [errors]
  * Acceptance criteria: parses packs shaped like your current output ; clear issues for missing fence/steps
  * Test strategy: golden tests with fixture Markdown files; fuzz-ish tests for malformed inputs
* **Task**: Implement `internal/state` persistence

  * Depends on: [errors]
  * Acceptance criteria: atomic save/load; schema version field present
  * Test strategy: unit tests with temp dirs; corruption handling tests
* **Exit criteria**: `pack.Load + Validate`, `state.Save/Load`, exit codes proven by tests.

### Phase 1: Execution core

**Entry criteria**: Phase 0 complete

* **Task**: Implement `internal/exec` runner (prelude + step)

  * Depends on: [errors, pack]
  * Acceptance criteria: streams stdout/stderr; writes log file; returns accurate exit code
  * Test strategy: integration tests using small deterministic shell scripts (`printf`, `exit 1`)
* **Task**: Implement flag injection transform

  * Depends on: [pack]
  * Acceptance criteria: only modifies intended `oracle` lines; does not rewrite arbitrary commands
  * Test strategy: unit tests with representative step bodies
* **Exit criteria**: can execute a single parsed step and produce logs deterministically.

### Phase 2: Non-TUI CLI (usable early)

**Entry criteria**: Phase 1 complete

* **Task**: Implement `internal/app` orchestration (plain mode)

  * Depends on: [pack, exec, state, report, errors]
  * Acceptance criteria: `orpack run --no-tui` supports confirm/skip, stop-on-fail policy, writes state + report
  * Test strategy: integration tests running the CLI against fixture packs (mock oracle via dummy commands)
* **Task**: Implement `internal/cli` with subcommands

  * Depends on: [app, errors]
  * Acceptance criteria: `run`, `validate`, `list` (optional) produce predictable text output
  * Test strategy: CLI snapshot tests (stdout/stderr) + exit code assertions
* **Exit criteria**: tool is useful without TUI.

### Phase 3: TUI (polish)

**Entry criteria**: Phase 2 complete

* **Task**: Implement `internal/tui` step list + status rendering

  * Depends on: [app, pack, state, errors]
  * Acceptance criteria: navigate/filter steps; run/skip; show per-step metadata
  * Test strategy: model-level tests for update messages and state transitions
* **Task**: Live log viewport

  * Depends on: [tui, exec]
  * Acceptance criteria: streaming output appears during execution; persisted logs still written
  * Test strategy: integration-ish tests using a fake line stream injected into model
* **Exit criteria**: full-screen TUI supports the primary workflow.

### Phase 4: Rendering + reporting polish

**Entry criteria**: Phase 3 complete

* **Task**: Implement `internal/render` (Markdown preview)

  * Depends on: [errors]
  * Acceptance criteria: renders markdown snippets to ANSI using Glamour. ([GitHub][3])
  * Test strategy: golden tests (ANSI output normalized)
* **Task**: Stabilize report schema v1

  * Depends on: [report, state]
  * Acceptance criteria: `run.json` includes per-step results, log paths, pack hash, schema version
  * Test strategy: schema conformance tests
* **Exit criteria**: consistent artifacts for both humans and automation.

### Phase 5: Packaging and distribution

**Entry criteria**: stable CLI behavior + docs

* **Task**: Add release pipeline (GoReleaser)

  * Depends on: [cli]
  * Acceptance criteria: reproducible builds; generates Homebrew/Scoop metadata as configured. ([GoReleaser][4])
  * Test strategy: CI dry-run of release config
* **Exit criteria**: users can install via release artifacts with minimal friction.

---

## 6) User Experience

### Personas

* **Operator**: runs packs, wants minimal mistakes; values confirmations, resume, clear failure context.
* **Maintainer**: authors packs; values `validate`, actionable parse errors, predictable numbering.

### Key flows

1. **Validate a pack**

   * `orpack validate pack.md` → prints issues + derived summary (`out_dir`, step count, detected outputs).
2. **Run in TUI**

   * `orpack run pack.md` → list steps → select → preview command and `--write-output` → confirm → stream logs → mark OK/Failed.
3. **Resume**

   * `orpack run --resume pack.md` → loads saved state → cursor moves to next pending step.
4. **Plain mode / CI**

   * `orpack run --no-tui --yes pack.md` → runs remaining steps deterministically; writes `run.json`.

### UX notes tied to capabilities

* Always display the detected `--write-output` prominently before confirmation.
* Provide explicit “Stop on failure / Continue?” behavior; never silently continue after errors.
* Keep keybindings discoverable (help line), consistent with common Bubble Tea apps. ([GitHub][2])

---

## 7) Technical Architecture

### System components

* **Parser**: Markdown → bash fence → prelude/steps.
* **Executor**: builds script = prelude + step; runs via `bash -lc` (default); streams lines.
* **State/Report**: write `state.json` (resume) + `run.json` (audit).
* **TUI**: Bubble Tea program + Bubbles list/viewport/spinner; Lip Gloss layout/styling. ([GitHub][2])
* **Markdown rendering**: Glamour (optional) for previews/help. ([GitHub][3])

### Data models

* `Pack{path, hash, outDir?, prelude, steps[]}`
* `Step{number, title, body, writeOutput?}`
* `RunState{schemaVersion, packHash, startedAt, steps: map[number]StepState}`
* `StepState{status, lastResult?}`
* `Result{exitCode, startedAt, endedAt, logPath}`

### External integrations

* `oracle` binary (invoked as-is)
* shell runtime (`bash` default)
* filesystem for logs/state/report

### Key decisions and trade-offs

* **Compiled Go + Bubble Tea** over shell-only: enables richer UX, better structure, cross-platform releases; more implementation surface area. Bubble Tea is explicitly intended for rich TUIs. ([GitHub][2])
* **Gum as fallback/alternative**: fastest path but limited extensibility; still valuable for “shell-first” users. ([GitHub][1])
* **Glamour for Markdown rendering**: consistent ANSI rendering with stylesheets for preview panes. ([GitHub][3])

---

## 8) Test Strategy

### Test pyramid targets

* **Unit**: 70% (parser, validators, injection transform, state/report serialization)
* **Integration**: 25% (executor running controlled shell snippets; CLI end-to-end with fixture packs)
* **E2E**: 5% (smoke tests in CI for major OS targets as feasible)

### Coverage minimums

* Pack parsing + validation: near-100% branch coverage (high risk surface)
* Executor: focus on error paths (timeouts, non-zero exits, stderr handling)

### Critical scenarios per module

* **pack**

  * Missing/empty fence; multiple fences; malformed headers; duplicate step numbers; no steps.
* **exec**

  * Prelude fails; step fails; long output; stderr-only output; non-UTF8 handling strategy (define behavior).
* **state/report**

  * Partial runs; resume from saved state; atomic write; schema version mismatch.
* **tui**

  * Status transitions; run/skip flows; failure stop policy; log rendering under high volume.

### Integration points

* Parsing → execution uses the same step bodies as read from pack fixtures derived from your known format.

---

## 9) Risks and Mitigations

### Risk: Pack format drift

* **Impact**: High
* **Likelihood**: Medium
* **Mitigation**: strict `validate` with clear errors; keep parser conservative; add fixture packs as regression tests.
* **Fallback**: allow `--format=legacy|strict` modes if needed.

### Risk: Executing arbitrary shell content (safety/security)

* **Impact**: High
* **Likelihood**: Medium
* **Mitigation**: confirm-by-default; show command preview; provide `--dry-run` and `--print-script` modes; never auto-run without `--yes`.
* **Fallback**: sandboxing is out-of-scope; document threat model.

### Risk: Cross-platform shell assumptions

* **Impact**: Medium
* **Likelihood**: Medium
* **Mitigation**: `--shell` flag; detect Windows and warn; document WSL/Git Bash expectations.
* **Fallback**: support `pwsh` only if pack bodies are compatible (likely not for bash-specific prelude).

### Risk: High-volume output overwhelms TUI

* **Impact**: Medium
* **Likelihood**: Medium
* **Mitigation**: ring buffer for in-memory viewport; always persist full logs to disk; provide “open log file path” UX.

---

## 10) Appendix

### Source format baseline

* Your existing pack structure and interactive-run expectations are exemplified in the uploaded script/pack discussion.

### Charm ecosystem references

* Bubble Tea (TUI framework; Elm-style architecture). ([GitHub][2])
* Bubbles (production-used components for Bubble Tea). ([GitHub][5])
* Lip Gloss (terminal styling/layout). ([GitHub][6])
* Gum (glamorous shell prompts without writing Go). ([GitHub][1])
* Glow (Markdown in terminal) and Glamour (Markdown rendering library for CLI apps). ([GitHub][7])
* GoReleaser packaging docs (Homebrew/Scoop/Actions). ([GoReleaser][4])

[1]: https://github.com/charmbracelet/gum?utm_source=chatgpt.com "charmbracelet/gum: A tool for glamorous shell scripts"
[2]: https://github.com/charmbracelet/bubbletea?utm_source=chatgpt.com "charmbracelet/bubbletea: A powerful little TUI framework"
[3]: https://github.com/charmbracelet/glamour?utm_source=chatgpt.com "charmbracelet/glamour: Stylesheet-based markdown ..."
[4]: https://goreleaser.com/customization/homebrew_casks/?utm_source=chatgpt.com "Homebrew Casks"
[5]: https://github.com/charmbracelet/bubbles?utm_source=chatgpt.com "charmbracelet/bubbles: TUI components for Bubble Tea"
[6]: https://github.com/charmbracelet/lipgloss?utm_source=chatgpt.com "charmbracelet/lipgloss: Style definitions for nice terminal ..."
[7]: https://github.com/charmbracelet/glow?utm_source=chatgpt.com "charmbracelet/glow: Render markdown on the CLI, with ..."


--- .taskmaster/docs/_PRD.md ---
## 1) Overview

### Problem

`oraclepack` is a Bubble Tea TUI for running “oracle packs” (bash steps embedded in Markdown) with state/reporting and automatic flag injection into `oracle` commands.
Today, changing **oracle CLI flags** or the **ChatGPT project URL/endpoint** typically requires editing configs or re-invoking the CLI differently per run, and there is no TUI flow to (a) pick overrides, (b) choose which steps they apply to, and (c) validate the override set before running.

### Who has it (target users)

* Developers using `oraclepack` to run repeatable AI-assisted workflows but needing per-run customization (different ChatGPT project folders, different oracle flags for debugging, different remotes).
* Pack authors who want packs to remain stable while allowing users to safely tweak runtime behavior.

### Why current solutions fail

* Current injection is global and line-based (`InjectFlags` rewrites lines starting with `oracle`), which can break common multi-line `oracle \` formatting and cannot selectively apply overrides per step.
* No TUI affordance exists to stage overrides and confirm/validate them before returning to the main run screen.

### Success metrics

* Override workflow completion rate: ≥ 95% of users can (flags + URL + step targeting) and return to main run screen without errors.
* Preflight prevention: ≥ 90% of invalid flag/url combinations are caught by validation before any step execution.
* UX efficiency: Configure overrides in ≤ 30 seconds for typical cases (multi-select flags + pick steps + confirm).
* Execution correctness: Overrides apply only to selected steps; non-selected steps run with baseline behavior.

Assumptions (explicit):

* Upstream `oracle` supports `--dry-run` and `--chatgpt-url` for safe validation and URL targeting. ([GitHub][1])
* Overrides are **ephemeral per TUI session/run** (no persistence beyond the run) unless added later (out of scope).

---

## 2) Capability Tree (Functional Decomposition)

### Capability: Runtime Overrides Management (MVP)

Manage a staged set of runtime overrides (flags + project URL) and their step targets.

#### Feature: Overrides data model

* **Description**: Represent user-selected flags, selected project URL, and selected step targets.
* **Inputs**: Baseline runner flags (current), user selections, pack step IDs.
* **Outputs**: `RuntimeOverrides` object usable by TUI + execution.
* **Behavior**: Store “added flags”, “removed flags”, `chatgptURL` (optional), and `applyToSteps` set.

#### Feature: Merge baseline flags + runtime overrides (step-aware)

* **Description**: Compute effective flags per step.
* **Inputs**: Baseline flags (existing `Runner.OracleFlags`), overrides, step ID.
* **Outputs**: Effective flag list and any key-value flag pairs (e.g., `--chatgpt-url <url>`).
* **Behavior**: If step not targeted, return baseline; else baseline + adds − removes, plus URL flag injection when set.

---

### Capability: Flag Picker UI (MVP)

Allow choosing additional oracle flags at runtime.

#### Feature: Multi-select flags picker

* **Description**: Pick known/common flags (multi-select) and optionally remove baseline flags.
* **Inputs**: Baseline flags, a curated list of known flags, current overrides.
* **Outputs**: Updated overrides (added/removed flags).
* **Behavior**: Toggle selection; show “added” vs “removed” vs “baseline unchanged”.

---

### Capability: Project URL (Endpoint) UI (MVP)

Allow entering/selecting a ChatGPT project URL for browser runs.

#### Feature: Project URL input screen

* **Description**: Enter a ChatGPT project URL string.
* **Inputs**: Text input (URL), existing selected URL.
* **Outputs**: Updated overrides `chatgptURL`.
* **Behavior**: Basic validation (non-empty, looks like URL); store exactly as entered.

#### Feature: Project URL selection menu (optional within MVP if simple list is kept in-memory)

* **Description**: Choose among recently entered URLs within the same session.
* **Inputs**: Session list, selected value.
* **Outputs**: Updated overrides `chatgptURL`.
* **Behavior**: Select one; allow “clear”.

Upstream basis: `oracle` supports `--chatgpt-url` and config `browser.chatgptUrl` for targeting a ChatGPT project folder. ([GitHub][1])

---

### Capability: Step Targeting UI (MVP)

Choose which steps receive overrides.

#### Feature: Multi-select step picker

* **Description**: Select pack steps that should receive runtime overrides.
* **Inputs**: Pack steps (IDs/titles), current selection set.
* **Outputs**: Updated overrides `applyToSteps`.
* **Behavior**: Toggle step selection; “select all / none”.

---

### Capability: Confirmation + Mode 2 Validation (MVP)

Validate selected overrides against upstream `oracle` before returning to main run screen.

#### Feature: Confirmation summary screen

* **Description**: Show flags added/removed, URL, and affected steps before applying.
* **Inputs**: Overrides + pack step list.
* **Outputs**: User confirmation or cancel.
* **Behavior**: Render a diff-style summary; confirm triggers validation.

#### Feature: Mode 2 validation runner

* **Description**: Validate that the override set is accepted by upstream `oracle` CLI.
* **Inputs**: Pack steps targeted, effective flags per targeted step, extracted oracle invocations.
* **Outputs**: Pass/fail with actionable error text including step + failing invocation.
* **Behavior**: For each targeted step, run each `oracle ...` invocation with overrides plus `--dry-run summary` to ensure CLI parsing succeeds without spending tokens. ([GitHub][1])

Requirement basis: “Mode 2 validation” must run before returning to run screen; failures must block readiness and show actionable errors.

---

### Capability: Execution Integration (MVP)

Apply overrides during actual run.

#### Feature: Step execution uses effective flags (step-aware)

* **Description**: Inject computed flags only into targeted steps.
* **Inputs**: Step code, step ID, effective flags.
* **Outputs**: Executed step with correct oracle invocations.
* **Behavior**: Replace current global injection behavior with per-step effective flags. Current injection is via `Runner.RunStep` → `InjectFlags`.

#### Feature: Hardened oracle injection (multi-line tolerant)

* **Description**: Inject flags without breaking common `oracle \` multi-line formatting.
* **Inputs**: Step script text, flags to inject.
* **Outputs**: Rewritten script.
* **Behavior**: Detect oracle invocations across line continuations and inject before trailing `\` on the invocation line when present.

Risk basis: current line-based approach can break multi-line commands.

---

## 3) Repository Structure + Module Definitions (Structural Decomposition)

Current structure includes `internal/exec`, `internal/tui`, `internal/app`, etc.

### Proposed additions/changes

```
internal/
  overrides/
    types.go
    merge.go
    validate.go
  exec/
    inject.go          (extend/replace with multiline-safe injector)
    oracle_scan.go     (new: extract oracle invocations)
    oracle_validate.go (new: mode-2 validation runner)
  tui/
    overrides_flow.go  (new: state machine for overrides screens)
    overrides_flags.go (new: flags picker model)
    overrides_steps.go (new: step picker model)
    overrides_url.go   (new: project URL input/selector)
    overrides_confirm.go (new: summary + validation screen)
```

### Module: `internal/overrides`

* **Maps to capability**: Runtime Overrides Management
* **Responsibility**: Own the runtime override data model and step-aware flag resolution.
* **Exports**:

  * `type RuntimeOverrides`
  * `func (o RuntimeOverrides) EffectiveFlags(stepID string, baseline []string) []string`
  * `func (o RuntimeOverrides) Targeted(stepID string) bool`
  * `func (o RuntimeOverrides) Summary(packSteps []pack.Step) OverridesSummary`

### Module: `internal/exec/oracle_scan.go`

* **Maps to capability**: Mode 2 Validation (oracle invocation extraction)
* **Responsibility**: Extract oracle invocations from bash step code robustly enough for validation + injection.
* **Exports**:

  * `type OracleInvocation { Raw string; Display string }`
  * `func ExtractOracleInvocations(script string) []OracleInvocation`

### Module: `internal/exec/inject.go` (updated)

* **Maps to capability**: Execution Integration + Hardened Injection
* **Responsibility**: Inject flags into oracle invocations without breaking multi-line formatting.
* **Exports**:

  * `func InjectFlags(script string, flags []string) string` (same signature; improved implementation)

### Module: `internal/exec/oracle_validate.go`

* **Maps to capability**: Mode 2 validation runner
* **Responsibility**: Execute safe validations (`--dry-run summary`) for extracted invocations with overrides.
* **Exports**:

  * `type ValidationError { StepID, Invocation string; Output string }`
  * `func ValidateOverrides(ctx context.Context, shell string, workDir string, env []string, steps []pack.Step, baseline []string, ov overrides.RuntimeOverrides) error`

### Module: `internal/tui/overrides_flow.go` (+ related files)

* **Maps to capability**: Flag Picker UI, Project URL UI, Step Targeting UI, Confirmation UX
* **Responsibility**: Bubble Tea models for the overrides wizard and integration back to main TUI model.
* **Exports**:

  * `func NewOverridesFlowModel(...) tea.Model` (or integrated into existing `tui.Model` as substates)
  * Messages: `OverridesAppliedMsg`, `OverridesCancelledMsg`, `OverridesValidationFailedMsg`

### Module changes: `internal/tui/tui.go`

* **Maps to capability**: Execution Integration + UX integration
* **Responsibility change**: Add a new view state for overrides flow; show current overrides status on main screen; pass overrides into runner step execution path.
* **Exports**: unchanged (existing `NewModel`), but internal state extended.

---

## 4) Dependency Chain (layers, explicit “Depends on: […]”)

### Foundation Layer (Phase 0)

* **internal/overrides**: override types + merge logic.
  Depends on: []
* **internal/exec/oracle_scan**: extract oracle invocations.
  Depends on: []
* **internal/exec/inject (improved)**: multiline-safe injection.
  Depends on: [internal/exec/oracle_scan] (for shared detection rules)

### Validation Layer (Phase 1)

* **internal/exec/oracle_validate**: mode-2 validation runner (`--dry-run summary`). ([GitHub][1])
  Depends on: [internal/overrides, internal/exec/oracle_scan]

### TUI Flow Layer (Phase 2)

* **internal/tui/overrides_* models**: flags picker, url input, step picker, confirm/validate screen.
  Depends on: [internal/overrides, internal/exec/oracle_validate]

### Integration Layer (Phase 3)

* **internal/tui/tui.go integration**: entrypoint keybinding/menu, apply overrides to step execution, display summary.
  Depends on: [internal/tui/overrides_*]
* **internal/app / internal/exec runner integration**: ensure `RunStep` can accept per-step effective flags (or TUI updates Runner flags before each step).
  Depends on: [internal/overrides, internal/exec/inject]

No cycles: all dependencies flow from overrides/scan → validate → tui flow → integration.

---

## 5) Development Phases (Phase 0…N; entry/exit criteria; tasks with dependencies + acceptance criteria + test strategy)

### Phase 0: Foundations (Overrides model + scanning + injection)

**Entry criteria**: Current pack parsing and TUI build clean.
**Tasks**:

* [ ] Implement `internal/overrides` data model + step-aware merge

  * **Depends on**: []
  * **Acceptance criteria**: Given baseline flags and overrides, `EffectiveFlags(stepID)` matches expected for targeted vs non-targeted steps.
  * **Test strategy**: Unit tests for add/remove precedence, empty sets, step targeting.

* [ ] Implement `internal/exec/oracle_scan.ExtractOracleInvocations`

  * **Depends on**: []
  * **Acceptance criteria**: Extracts oracle invocations from (a) single-line `oracle ...`, (b) `oracle \` + continued lines, (c) indented variants.
  * **Test strategy**: Unit tests with representative scripts; snapshot extracted `Display`.

* [ ] Upgrade `internal/exec/InjectFlags` to be multiline-tolerant

  * **Depends on**: [internal/exec/oracle_scan]
  * **Acceptance criteria**: Injects flags into multiline `oracle \` form without breaking trailing backslash; preserves non-oracle lines.
  * **Test strategy**: Extend `inject_test.go` with multiline continuation fixtures (current tests exist).

**Exit criteria**: Per-step effective flag computation and injection behave correctly under unit tests.

---

### Phase 1: Mode 2 validation runner

**Goal**: Validate overrides safely before returning to run screen.
**Entry criteria**: Phase 0 complete.

**Tasks**:

* [ ] Implement `internal/exec/oracle_validate.ValidateOverrides` using `--dry-run summary`

  * **Depends on**: [internal/overrides, internal/exec/oracle_scan]
  * **Acceptance criteria**:

    * For each targeted step, each extracted oracle invocation is executed with overrides + `--dry-run summary` and fails fast on first error.
    * Error includes step ID, invocation, and captured output.
  * **Test strategy**:

    * Unit tests for command construction (pure functions).
    * Integration test with a fake `oracle` binary on PATH (test harness script) that asserts received args.

Upstream basis for dry run: `oracle --dry-run [summary|json|full]` previews without sending. ([GitHub][1])

**Exit criteria**: Validation produces deterministic pass/fail with actionable error payload.

---

### Phase 2: Overrides wizard TUI

**Goal**: Provide the requested picker flow and confirmation/validation gating.
**Entry criteria**: Phase 1 complete.

**Tasks**:

* [ ] Add overrides flow state machine + entrypoint from main run screen

  * **Depends on**: [internal/overrides, internal/exec/oracle_validate]
  * **Acceptance criteria**: From main steps screen, user can enter overrides flow and return (cancel or apply).
  * **Test strategy**: Bubble Tea model tests for state transitions (message-driven).

* [ ] Implement flags picker (multi-select)

  * **Depends on**: [overrides flow state machine]
  * **Acceptance criteria**: Select/deselect flags; mark removed baseline flags; navigation works.
  * **Test strategy**: Model tests simulate keypresses; verify overrides state.

* [ ] Implement step picker (multi-select)

  * **Depends on**: [overrides flow state machine]
  * **Acceptance criteria**: Can select any subset; “all/none” supported.
  * **Test strategy**: Model tests; verify `applyToSteps` set.

* [ ] Implement project URL input/selection menu

  * **Depends on**: [overrides flow state machine]
  * **Acceptance criteria**: Enter URL; clear URL; appears in summary.
  * **Test strategy**: Model tests for textinput.

* [ ] Implement confirmation screen that runs validation on confirm

  * **Depends on**: [flags picker, step picker, url input, internal/exec/oracle_validate]
  * **Acceptance criteria**:

    * Summary includes flags added/removed, affected steps, selected URL.
    * Confirm triggers validation; failure shows error and does not apply.
  * **Test strategy**: Model tests with stubbed validator (inject interface).

**Exit criteria**: Full wizard: flags → steps → confirm/validate → return to run screen.

---

### Phase 3: Execution integration + UX polish

**Goal**: Overrides affect actual execution exactly as selected.

**Tasks**:

* [ ] Apply step-aware effective flags during `runStep` execution path

  * **Depends on**: [Phase 0 injection, Phase 2 applied overrides messages]
  * **Acceptance criteria**: Running a non-targeted step uses baseline flags only; targeted step includes overrides.
  * **Test strategy**: Integration test with fake `oracle` that logs args for selected step only.

* [ ] Display staged overrides summary on main run screen

  * **Depends on**: [Phase 2]
  * **Acceptance criteria**: Main view indicates overrides active and shows brief summary (e.g., “Overrides: +2 flags, URL set, 3 steps”).
  * **Test strategy**: View snapshot tests (string contains markers).

**Exit criteria**: End-to-end usable: configure overrides in TUI, validated, then run steps with correct behavior.

---

## 6) User Experience

### Personas

* **Pack runner**: Wants to run a known pack but direct output into a specific ChatGPT project folder and enable debug flags temporarily.
* **Pack author**: Wants users to safely override runtime settings without editing the pack or shared config.

### Key flows

1. **Open overrides flow** from main steps screen (new keybinding/menu entry).
2. **Flags picker**: multi-select adds/removes.
3. **Steps picker**: choose steps receiving overrides.
4. **Project URL menu**: enter/select/clear URL (separate screen within flow).
5. **Confirm**: show full summary; confirm runs validation.
6. **Return to main run screen**: overrides staged; run steps as normal.

### UI/UX notes (tied to capabilities)

* Always show an “Overrides staged” indicator on the main screen to avoid hidden behavior changes.
* Validation errors must include: step ID + the failing oracle invocation + raw output.
* Keep “full config menu” out of scope; only runtime pickers + project URL.

---

## 7) Technical Architecture

### System components

* **TUI (Bubble Tea)**: collects overrides and step targets; triggers validation; applies overrides to execution.
* **Overrides core**: step-aware merge + summary formatting.
* **Exec layer**:

  * Oracle invocation extraction
  * Multiline-safe injection
  * Mode 2 validation runner (`oracle --dry-run summary ...`) ([GitHub][1])
* **Runner**: executes rewritten step scripts (current `Runner.RunStep` injects flags).

### Data models

* `RuntimeOverrides`:

  * `AddedFlags []string`
  * `RemovedFlags []string`
  * `ChatGPTURL string` (optional)
  * `ApplyToSteps map[string]bool`
* `OverridesSummary`:

  * counts + formatted lists for confirm screen and main screen indicator

### Key decision: validate via `--dry-run summary` (Mode 2)

* **Rationale**: Upstream `oracle` provides a dry-run mode specifically to preview without sending tokens, so it exercises real CLI parsing. ([GitHub][1])
* **Trade-offs**: Requires executing `oracle` during validation; may still read files and can fail if referenced paths/vars are unresolved.
* **Alternative**: `oracle --help`-based validation (weaker; doesn’t validate combos). Not chosen for MVP.

### Key decision: heuristic multiline-safe injection (not full shell parsing)

* **Rationale**: Addresses the most common breakage (`oracle \` continuation) without introducing a full bash AST parser.
* **Trade-offs**: Some exotic shell constructs may not be perfectly handled; mitigate with tests and clear error output.

---

## 8) Test Strategy

### Test pyramid targets

* Unit: ~70% (overrides merge, scan, injection)
* Integration: ~25% (validation runner + fake oracle binary, runner integration)
* E2E: ~5% (optional: simulate TUI flow at message level)

### Coverage minimums

* Unit-tested modules (overrides, scan, inject): ≥ 90% line coverage
* Validation runner: ≥ 80% line coverage with integration fixtures

### Critical scenarios (by module)

* **internal/overrides**

  * Happy: targeted step gets baseline+adds−removes + url flag
  * Edge: empty overrides; remove non-existent flag; applyToSteps empty
* **internal/exec/oracle_scan**

  * Happy: single-line oracle
  * Edge: multiline with backslashes and indentation
  * Error: no oracle invocations → validation is a no-op success (if targeted steps contain none)
* **internal/exec/inject**

  * Multiline injection preserves `\`
  * Does not inject into non-oracle commands
* **internal/exec/oracle_validate**

  * Fails fast and returns structured error with output
  * Uses `--dry-run summary` consistently ([GitHub][1])
* **internal/tui overrides flow**

  * Cancel returns without applying
  * Confirm runs validator; failure stays in flow with readable error
  * Apply sends message to main model and updates indicator

---

## 9) Risks and Mitigations

### Risk: Injection breaks uncommon shell patterns

* **Impact**: Medium (step execution could change meaning)
* **Likelihood**: Medium
* **Mitigation**: Focus on well-defined heuristics (start-of-command + multiline `\`), add fixture tests for real packs, provide clear docs/limitations.
* **Fallback**: Allow disabling runtime injection for a step (not in MVP; could be added later).

### Risk: Validation executes `oracle` but fails due to environment/path assumptions

* **Impact**: Medium
* **Likelihood**: Medium
* **Mitigation**: Validation runs only extracted oracle invocations; capture and display full output; allow user to adjust overrides/targets.
* **Fallback**: Provide “skip validation” escape hatch (not in MVP unless necessary; conflicts with requirement).

### Risk: Upstream `oracle` flags change

* **Impact**: Medium
* **Likelihood**: Low/Medium
* **Mitigation**: Treat the flags list in the picker as curated (best-effort) and allow manual entry for advanced flags; keep validation authoritative.
* **Fallback**: Update curated list independently; validation remains source of truth.

---

## 10) Appendix

### References

* Runtime overrides requirements and acceptance criteria.
* Current `oraclepack` code structure, current injection path (`Runner.RunStep` → `InjectFlags`).
* `oraclepack` product description and existing CLI behavior.
* Upstream `oracle` CLI supports `--dry-run` and `--chatgpt-url` configuration patterns. ([GitHub][1])

### Glossary

* **Oracle Pack**: Markdown file containing one `bash` block with numbered steps executed by `oraclepack`.
* **Mode 2 validation**: For this PRD, defined as executing extracted oracle invocations with overrides using `--dry-run summary` to validate CLI parsing without sending. ([GitHub][1])

### Open questions

* Should project URL injection also support remote browser service flags (`--remote-host/--remote-token`) in addition to `--chatgpt-url`? Upstream supports these. ([GitHub][1])
* Should entered project URLs be persisted across runs (state/config) or remain session-only? (Currently assumed session-only per scope.)

[1]: https://github.com/steipete/oracle "GitHub - steipete/oracle: Ask the oracle when you're stuck. Invoke GPT-5 Pro with a custom context and files."


--- .taskmaster/docs/prd-2026-01-09.md ---
# Oraclepack PRD

## Overview

### Problem Statement

Oraclepack is a runner for strict, 20-step Oracle Packs, but its pipeline is brittle and incomplete: pack shape drift causes validation failures, execution context (workdir/state output paths) is inconsistent, and post-run outputs are not reliably structured for automated follow-on work. Action Packs are perceived as "oracle-only," so they do not deterministically dispatch real executors (codex/gemini/tm), and TUI/MCP flows for PRD generation and tool routing are incomplete. The result is high friction, low determinism, and poor automation handoff.

### Target Users

- Workflow engineers generating and running Oracle Packs and Action Packs in CI/headless modes.
- Developers using oraclepack TUI to run multi-step workflows and manage ChatGPT project URLs.
- Maintainers distributing oraclepack and oraclepack-mcp-server to teams.

### Success Metrics

- TODO: Define baseline failure rate for `oraclepack validate` and reduce invalid-pack runs by X%.
- TODO: Ensure 0 new `*.state.json` / `*.report.json` / `*.chatgpt-urls.json` are created in repo roots by default.
- TODO: Action Pack runs produce expected artifacts under `.oraclepack/ticketify/` in >= 95% of runs when tools are available.
- TODO: MCP run parity: 100% of CLI run flags relevant to execution context are available via MCP tools.

## Capability Tree (Functional Decomposition)

### Capability: Pack Authoring and Validation

Ensure packs are always runner-ingestible and structurally safe.

#### Feature: Strict pack-shape validation

- **Description**: Enforce the single-bash-fence + 20-step invariant on pack validation.
- **Inputs**: Pack Markdown path.
- **Outputs**: Validation pass/fail + errors.
- **Behavior**: Count fences, require exactly one ```bash fence, require 20 sequential `# NN)` steps; reject other fences.
- **MVP**: Yes.

#### Feature: Schema-first manifest + deterministic renderer

- **Description**: Validate a manifest JSON schema and render a compliant pack.
- **Inputs**: Manifest JSON (schema_version, kind, out_dir, steps[20]).
- **Outputs**: Rendered pack Markdown + schema validation report.
- **Behavior**: Enforce 20 steps with IDs 01..20, render one bash fence with out_dir prelude and step bodies.
- **MVP**: No.

#### Feature: Bash safety lint for orphaned flags

- **Description**: Prevent `-p/--prompt` lines from becoming standalone commands.
- **Inputs**: Extracted bash fence text.
- **Outputs**: Validation pass/fail + lint diagnostics.
- **Behavior**: Run `bash -n` and a custom detector for orphaned flags; optional shellcheck integration.
- **MVP**: Yes.

#### Feature: Attachment preflight

- **Description**: Verify `-f` attachment paths exist before execution.
- **Inputs**: Pack steps + workdir.
- **Outputs**: Preflight report with missing files list.
- **Behavior**: Extract invocations, ensure referenced files exist under resolved workdir; fail or warn per mode.
- **MVP**: Yes.

### Capability: Execution and State Management

Make execution deterministic, resumable, and non-invasive to repo roots.

#### Feature: Deterministic workdir resolution

- **Description**: Resolve workdir to repo root when `--work-dir` is absent.
- **Inputs**: packPath, optional work_dir flag.
- **Outputs**: Resolved WorkDir.
- **Behavior**: Infer repo root from packPath; use it as WorkDir unless overridden.
- **MVP**: Yes.

#### Feature: Externalized state/report storage

- **Description**: Store run state/report and URL data outside the repo root by default.
- **Inputs**: pack base name, env/flag overrides.
- **Outputs**: Paths for state/report/config stores.
- **Behavior**: Use XDG config/state/cache defaults; allow `--state-dir` or `ORACLEPACK_STATE_DIR` override.
- **MVP**: Yes.

#### Feature: Run manifest and step index

- **Description**: Emit deterministic run metadata for automation.
- **Inputs**: Pack, run context, step execution results.
- **Outputs**: `run.json`, `steps.json`, report state.
- **Behavior**: Persist pack hash, git SHA, step statuses, output paths; idempotent resume based on outputs.
- **MVP**: No.

#### Feature: Resume/rerun semantics

- **Description**: Resume from failed/interrupted steps and support rerun modes.
- **Inputs**: run state + outputs + rerun policy.
- **Outputs**: Updated state/report.
- **Behavior**: Skip succeeded steps by default; support `--rerun all|failed|01,03`.
- **MVP**: No.

### Capability: Action Pack Workflow and Tool Dispatch

Enable action execution beyond oracle-only flows.

#### Feature: Canonical action artifacts

- **Description**: Standardize `_tickets_index.json`, `_actions.json`, `_actions.md`, and PRD artifacts.
- **Inputs**: Ticket data, oracle outputs, Task Master outputs.
- **Outputs**: `.oraclepack/ticketify/_tickets_index.json`, `_actions.json`, `_actions.md`, `.taskmaster/docs/tickets_prd.md`.
- **Behavior**: Deterministic paths; stable schemas for machine processing.
- **MVP**: Yes.

#### Feature: Action Pack implement dispatcher

- **Description**: Dispatch executor commands (codex/gemini/tm) based on `_actions.json`.
- **Inputs**: `_actions.json`, tooling config, top_n.
- **Outputs**: `.oraclepack/ticketify/next.json`, `codex-implement.md`, `codex-verify.md` and/or `gemini-review.json`, `PR.md`.
- **Behavior**: Replace placeholder steps 09-13 and 16 with headless non-interactive commands; guard missing binaries with skip semantics.
- **MVP**: Yes (guarded, opt-in).

#### Feature: Tool-agnostic overrides/validation (dispatcher v2)

- **Description**: Expand command detection and validation beyond `oracle`.
- **Inputs**: Step scripts + registry of tool prefixes.
- **Outputs**: Validation results + targeted overrides.
- **Behavior**: Detect tm/task-master, codex, gemini; preserve existing oracle-only behavior unless opt-in flag is set.
- **MVP**: No (opt-in rollout).

#### Feature: Taskify agent-mode generation

- **Description**: Add `agent-mode` to taskify-generated packs without changing 20-step contract.
- **Inputs**: taskify params, mode=codex|gemini.
- **Outputs**: Modified Action Pack with swapped step slot after Task Master expansion.
- **Behavior**: Swap existing autopilot step slot, keep total steps at 20.
- **MVP**: No.

### Capability: TUI and PRD Routing

Improve TUI URL selection and PRD generation flow.

#### Feature: PRD Generator URL selection

- **Description**: Add `PRD Generator` URL entry in picker with defaulting.
- **Inputs**: URL picker storage, project/global scope.
- **Outputs**: Selected ChatGPT URL for run overrides.
- **Behavior**: Allow project/global scope, default selection, no URL hardcoding in packs.
- **MVP**: Yes.

#### Feature: Headless chatgpt-url CLI override

- **Description**: Enable `--chatgpt-url` or `--chatgpt-url-name` for headless runs.
- **Inputs**: CLI flags.
- **Outputs**: Applied ChatGPT URL override.
- **Behavior**: Apply URL override without TUI.
- **MVP**: Yes.

#### Feature: PRD micro-pack / call mode

- **Description**: Run PRD generation without treating `tickets_prd.md` as a pack.
- **Inputs**: `tickets_prd.md`, optional `prd_context.md`.
- **Outputs**: `final_prd.md` in Task Master docs.
- **Behavior**: Generate a valid micro-pack or `oraclepack call` path with a single oracle invocation and attachments.
- **MVP**: Yes.

### Capability: MCP Server Parity and Distribution

Ensure MCP tools match CLI capabilities and are easy to install.

#### Feature: MCP run flag passthrough

- **Description**: Expose `work_dir`, `oracle_bin`, `out_dir` via MCP run tool.
- **Inputs**: MCP tool parameters.
- **Outputs**: CLI invocation with flags applied.
- **Behavior**: Append flags to CLI command for `oraclepack_run_pack`.
- **MVP**: Yes.

#### Feature: Pack generation tools via MCP/CLI

- **Description**: Provide `oraclepack generate` and MCP tools for grouped packs.
- **Inputs**: tickets/codebase paths.
- **Outputs**: `out_dir/packs/*.md`, `_groups.json`.
- **Behavior**: Deterministic discovery + grouping + validation after generation.
- **MVP**: No.

#### Feature: Simplified MCP distribution

- **Description**: Publish `oraclepack-mcp-server` for short configs.
- **Inputs**: PyPI/uv tooling, MCP bundle metadata.
- **Outputs**: PATH executable, uvx config example, optional `.mcpb` bundle.
- **Behavior**: Provide short config without venv absolute path.
- **MVP**: No.

### Capability: Pipeline Actionizer (Stage 3)

Create a deterministic stage that converts outputs into actionable work.

#### Feature: Actionizer command

- **Description**: Convert 20 oracle outputs into normalized backlog artifacts.
- **Inputs**: run dir outputs, run metadata.
- **Outputs**: `normalized.jsonl`, `backlog.md`, `change-plan.md`, optional `taskmaster.json`.
- **Behavior**: Deduplicate, categorize, assign stable IDs; idempotent regeneration.
- **MVP**: No.

## Repository Structure + Module Definitions (Structural Decomposition)

### Module: `internal/pack`

- **Maps to capability**: Pack Authoring and Validation
- **Responsibility**: Parse/validate pack structure and provide pack data model.
- **File structure**:

  ```
  internal/pack/
  |-- parser.go
  |-- types.go
  `-- output_check.go
  ```

- **Exports**:
  - `ParsePack(path string) (*Pack, error)` - parse the first bash fence into steps.
  - `ValidatePack(pack *Pack) error` - enforce single-fence + 20-step constraints.
  - `ValidateStrict(pack *Pack) error` - optional strict validators (orphaned flags, ROI, etc).

### Module: `internal/exec`

- **Maps to capability**: Execution and Tool Dispatch
- **Responsibility**: Extract/validate invocations and run steps safely.
- **File structure**:

  ```
  internal/exec/
  |-- oracle_scan.go
  |-- oracle_validate.go
  |-- runner.go
  `-- sanitize.go
  ```

- **Exports**:
  - `ExtractInvocations(step Step) ([]Invocation, error)`
  - `InjectOverrides(step Step, overrides Overrides) (Step, error)`
  - `PreflightAttachments(invocations []Invocation, workdir string) error`
  - `RunStep(ctx RunContext, step Step) StepResult`

### Module: `internal/state`

- **Maps to capability**: Execution and State Management
- **Responsibility**: Resolve state/report paths, persist run state.
- **File structure**:

  ```
  internal/state/
  |-- persist.go
  `-- types.go
  ```

- **Exports**:
  - `ResolveStateDir() (string, error)`
  - `ResolveConfigDir() (string, error)`
  - `LoadState(path string) (*RunState, error)`
  - `SaveState(path string, state *RunState) error`

### Module: `internal/report`

- **Maps to capability**: Execution and Actionizer Metadata
- **Responsibility**: Build deterministic run manifests and reports.
- **File structure**:

  ```
  internal/report/
  |-- generate.go
  `-- types.go
  ```

- **Exports**:
  - `GenerateReport(run RunContext, results []StepResult) (*Report, error)`
  - `WriteRunManifest(path string, manifest RunManifest) error`

### Module: `internal/cli`

- **Maps to capability**: CLI Feature Surface
- **Responsibility**: CLI commands, flags, and config wiring.
- **File structure**:

  ```
  internal/cli/
  |-- root.go
  |-- cmds.go
  `-- run.go
  ```

- **Exports**:
  - `NewRootCmd() *cobra.Command`
  - `RegisterRunFlags(cmd *cobra.Command)`

### Module: `internal/app`

- **Maps to capability**: Execution Orchestration
- **Responsibility**: Orchestrate validate -> preflight -> run -> report.
- **File structure**:

  ```
  internal/app/
  |-- app.go
  `-- run.go
  ```

- **Exports**:
  - `RunPack(ctx AppContext, packPath string) error`

### Module: `internal/tui`

- **Maps to capability**: TUI and PRD Routing
- **Responsibility**: URL picker/store, overrides wizard, PRD flow UI.
- **File structure**:

  ```
  internal/tui/
  |-- tui.go
  |-- url_picker.go
  `-- overrides_*.go
  ```

- **Exports**:
  - `RunTUI(ctx TUIContext) error`
  - `PickChatGPTURL(store URLStore) (URLSelection, error)`

### Module: `oraclepack-mcp-server/oraclepack_mcp_server`

- **Maps to capability**: MCP Server Parity and Distribution
- **Responsibility**: MCP tools for running packs and taskify flows.
- **File structure**:

  ```
  oraclepack-mcp-server/oraclepack_mcp_server/
  |-- server.py
  |-- oraclepack_cli.py
  `-- taskify.py
  ```

- **Exports**:
  - `oraclepack_run_pack(...)` MCP tool
  - `oraclepack_generate_*` MCP tools (future)

## Dependency Chain (Structural Layers)

### Layer 0 (Foundations)

- `internal/errors`
- `internal/pack` (types + parser)

### Layer 1 (State + Validation)

- `internal/state` (XDG resolution)
- `internal/pack` (strict validation)
- `internal/overrides` (types)

### Layer 2 (Execution Primitives)

- `internal/exec` (invocation scan, preflight, runner)
- `internal/report` (run/step manifests)
- `internal/render` (manifest -> pack)

### Layer 3 (Orchestration)

- `internal/app` (validate -> preflight -> run -> report)

### Layer 4 (Interfaces)

- `internal/cli`
- `internal/tui`

### Layer 5 (External Integration)

- `oraclepack-mcp-server/oraclepack_mcp_server`

## Development Phases

### Phase 0: Validation and Deterministic Execution Foundations

- **Tasks**:
  - Enforce strict pack-shape validation (single bash fence, 20 steps).
  - Add bash safety lint for orphaned flags.
  - Add workdir resolution and `--work-dir` flag.
  - Add XDG-based state/config/cache resolution and `--state-dir` override.
- **Dependencies**: Layer 0 -> Layer 2.
- **Acceptance Criteria**:
  - `oraclepack validate` rejects extra fences and wrong step counts.
  - Pack with orphaned `-p` lines fails validation.
  - `oraclepack run` defaults to repo-root workdir; `--work-dir` overrides it.
  - State/report outputs no longer appear in repo root by default.
- **Test Strategy**:
  - Unit tests for `ValidatePack` and orphaned-flag detection.
  - Integration test validating a fixture pack with invalid fences/steps.

### Phase 1: Preflight, Reporting, and Resume

- **Tasks**:
  - Add attachment preflight for `-f` paths.
  - Emit `run.json` and `steps.json` with stable schema.
  - Implement resume/rerun semantics.
- **Dependencies**: Phase 0 complete.
- **Acceptance Criteria**:
  - Missing attachments produce actionable preflight errors.
  - Each run writes run/step manifests with deterministic paths.
  - Resume skips previously successful steps by default.
- **Test Strategy**:
  - Integration test: partial run -> resume -> correct step continuation.

### Phase 2: Action Pack Automation + Dispatcher

- **Tasks**:
  - Standardize `_actions.json` schema with `executor`, `exec_prompt`, `inputs`.
  - Replace placeholder steps in `ticket-action-pack.md` with headless codex/gemini guarded commands (opt-in).
  - Add agent-mode option for taskify packs (swap step slot).
  - Add tool-availability guards and skip semantics.
- **Dependencies**: Phase 1 (stable execution + state).
- **Acceptance Criteria**:
  - Action Pack runs produce expected artifacts under `.oraclepack/ticketify/` when tools exist.
  - Missing codex/gemini results in "skipped" outputs, not hard failures.
  - Agent-mode preserves 20-step contract.
- **Test Strategy**:
  - Fixture pack with mocked tools to verify skip behavior and artifact creation.

### Phase 3: TUI + MCP Surface + Distribution

- **Tasks**:
  - Add PRD Generator URL entry and selection flows.
  - Add `--chatgpt-url` / `--chatgpt-url-name` CLI overrides.
  - Add micro-pack or `oraclepack call` path for PRD generation.
  - Extend MCP `oraclepack_run_pack` with `work_dir`, `oracle_bin`, `out_dir`.
  - Document short config and publish MCP server distribution.
- **Dependencies**: Phase 0 for core execution; Phase 2 optional for PRD integration.
- **Acceptance Criteria**:
  - TUI supports selecting PRD Generator URL and running PRD flow without invalid-pack errors.
  - MCP tools run with explicit workdir/oracle_bin/out_dir.
  - Documentation includes short MCP config examples.
- **Test Strategy**:
  - TUI unit tests for URL picker defaults.
  - MCP integration test invoking run with explicit flags.

## User Experience

### Personas

- **Workflow Engineer**: Builds packs and runs them in CI/headless mode; needs deterministic outputs and resume.
- **TUI Operator**: Runs packs interactively and manages ChatGPT project URLs.
- **Integrator**: Uses MCP server and expects concise configuration and parity with CLI.

### Key Flows

- **Run a pack (CLI)**: validate -> preflight -> run -> report; state stored outside repo.
- **Run Action Pack (CLI/TUI)**: steps 01-07 generate Task Master artifacts; optional implement mode dispatches codex/gemini; outputs appear under `.oraclepack/ticketify/`.
- **PRD generation (TUI/Headless)**: select PRD Generator URL -> run micro-pack with `tickets_prd.md` + `prd_context.md` -> write `final_prd.md`.
- **MCP run**: use `oraclepack_run_pack` with explicit `work_dir` and `oracle_bin` to avoid PATH issues.

### UI/UX Notes

- Clearly label oracle-only override validation vs non-oracle steps.
- Show "skipped due to missing tool" vs "skipped by ROI filter."
- Provide a dedicated TUI option for PRD generation that avoids running `tickets_prd.md` as a pack.

## Technical Architecture

### Components

- **Pack Parser/Validator**: Enforces single-fence and 20-step schema; strict lint for bash safety.
- **Execution Engine**: Runs steps via `bash -lc`, performs preflight checks, injects overrides, writes state/report.
- **State/Report Store**: XDG-based location, run.json + steps.json.
- **Action Pack Dispatcher**: Reads `_actions.json`, selects executor, runs headless tool commands, writes deterministic artifacts.
- **TUI URL Store**: Global/project scoped URL persistence; CLI override support.
- **MCP Server**: Wraps CLI and exposes run/generate tools with parity flags.

### Data Models

- **Pack**: `Pack{Steps[20], Prelude}` with single bash fence.
- **Run Manifest**: `run.json` (pack hash, git SHA, versions, timestamps).
- **Step Manifest**: `steps.json` (step_id, command_hash, output_path, status).
- **Action Artifacts**: `_tickets_index.json`, `_actions.json`, `_actions.md`, `next.json`, `codex-implement.md`, `codex-verify.md`, `gemini-review.json`, `PR.md`.
- **URL Store**: global/project JSON config for ChatGPT URLs.

### APIs / Integrations

- **Oracle CLI**: validation (`--dry-run summary`) and execution.
- **Task Master CLI**: parse PRD, analyze complexity, expand tasks.
- **Codex CLI / Gemini CLI**: non-interactive executor steps for implementation/verification.
- **MCP**: `oraclepack_run_pack`, future `oraclepack_generate_*` tools.

### Decisions and Trade-offs

- Preserve pack schema (single bash fence, 20 steps) and enforce with validators; do not relax.
- Opt-in dispatcher expansion for non-oracle tools to preserve backward compatibility.
- Use deterministic paths under `.oraclepack/` for action artifacts; use XDG for run state defaults.

## Test Strategy

- **Unit tests**: parser/validator fence counting; orphaned-flag detector; XDG path resolution.
- **Integration tests**: run -> resume flow; missing attachment preflight; ROI filtering skip reporting.
- **Fixture packs**: valid/invalid packs with multiple fences, bad numbering, missing ROI.
- **Tool-availability tests**: simulate missing codex/gemini with command guards and verify skip semantics.
- **MCP tests**: run tool with explicit `work_dir` and `oracle_bin` flags.

## Risks and Mitigations

- **Tool availability and interactivity**: codex/gemini/tm may not be installed or may block; mitigate with `command -v` guards, headless flags, and skip reporting.
- **Backward compatibility**: expanding dispatch/validation beyond oracle risks behavior changes; mitigate with feature flags and opt-in modes.
- **Schema drift**: generator or templates may emit invalid packs; mitigate with strict validation and fixture tests.
- **State path changes**: moving state/report outputs may confuse users; mitigate with migration notes and override flags.
- **ROI filtering skips required steps**: mitigate by enforcing ROI metadata for critical steps and warning when filters are active.
- **Unspecified override semantics for non-oracle tools**: keep oracle-only behavior as default and document gaps.

## Appendix

### Sources Consulted

- `.tickets/*` (actions, mcp, PRD-TUI, misc, other)
- `docs/oracle-questions-2026-01-08/actions/*.md`
- `README.md`

### Open Questions

- TODO: Define success metric baselines and thresholds (validation pass rate, run failure rate).
- TODO: Confirm desired override semantics for non-oracle tools (which flags, how to validate).
- TODO: Confirm canonical schemas for `_tickets_index.json`, `_actions.json`, and `run.json`.
- TODO: Confirm whether manifest-first pack generation is required in MVP.

## Task-Master Integration Notes (Optional)

- Capability Tree items map to Task Master tasks; Features map to subtasks.
- Dependencies map to task dependencies; phases map to priorities.
- Action Pack artifacts (`_actions.json`) should align with Task Master task schemas for downstream automation.


--- docs/oracle-questions-2026-01-09-030000/actions/01-contracts-interfaces-ticket-surface-direct-answer.md ---
Direct answer

* Clarify/document Action Pack execution semantics (CLI/TUI docs surface): steps run as `bash -lc ...` in the project root, and oraclepack’s “special handling” (flag injection/override validation) applies only to commands beginning with `oracle`; non-`oracle` tools (`tm`/`task-master`, `codex`, `gemini`) run directly as shell commands.  
* Dispatcher/command-detection contract expansion: broaden detection beyond the oracle-anchored regex (`^(\\s*)(oracle)\\b`) so steps containing `tm`/`task-master`, `codex`, and `gemini` can be treated as first-class command targets for dispatch/override/validation inclusion. Back-compat: preserve existing behavior for `oracle ...` commands. 
* Override-validation behavior change (TUI/validate surface): today validation targets only oracle invocations (runs `oracle --dry-run summary`) and skips steps without oracle lines; tickets imply extending/restructuring validation so steps containing `tm`/`task-master`, `codex`, `gemini` are not silently excluded purely due to prefix mismatch. Back-compat: do not regress the current oracle-only validation flow.  
* `ticket-action-pack.md` content contract change: replace placeholder steps (explicitly 09–13 and 16) with headless `gemini` + non-interactive `codex exec` automation. Back-compat: keep the pack “oraclepack-ingestible” (single bash fence, `# NN)` steps) and keep Steps 01–07 semantics unchanged.  
* New standardized output artifact interface under `.oraclepack/ticketify/` for the agent loop: Step 09 `next.json`, Step 10 `codex-implement.md`, Step 11 `codex-verify.md` and/or `gemini-review.json`, Step 16 `PR.md`. Back-compat: paths are treated as required/stable outputs (don’t rename/move without versioning).  
* Tool-availability/skip semantics become part of the operational contract for these steps: add `command -v ...` guards and documented “skip” behavior to avoid hard failures when `codex`/`gemini` are missing or interactive (blocking risk). Back-compat: default runs should not newly fail just because optional tools aren’t installed.  
* Taskify Action Pack generator surface change: add an opt-in “agent-mode” (suggested `mode=codex` / `mode=gemini`) that swaps the existing autopilot entrypoint step slot after Task Master expansion, while keeping the 20-step contract intact. Back-compat: default mode remains current behavior; agent-mode must not add steps. 
* Output/reporting contract stability: tickets explicitly name run outputs/artifacts (e.g., `.oraclepack/ticketify/_tickets_index.json`, `_actions.json`, `.taskmaster/docs/tickets_prd.md`, `.oraclepack/ticketify/tm-complexity.json`, plus `ticket-action-pack.state.json` / `ticket-action-pack.report.json`) and restate project-root execution (no chdir to `out_dir`). Back-compat: keep these names/locations stable to avoid breaking consumers and user expectations. 


--- docs/oracle-questions-2026-01-09-030000/actions/01-contracts-interfaces-ticket-surface-missing-evidence.md ---
Missing evidence

* `.tickets/actions/*.md` (full raw tickets, not summaries)
* `ticket-action-pack.md` (and its generator/template source): `**/*ticket-action-pack*.md`, `**/*ticketify*pack*.md`, `**/templates/**`, `**/assets/**`
* “taskify” Action Pack generator implementation (for proposed `mode=codex|gemini`): `**/*taskify*.*`, `**/taskify/**`, `**/oraclepack-taskify/**`
* Oraclepack CLI/TUI public surface (commands/flags/help) and wiring: `**/cmd/**`, `**/internal/cli/**`, `**/*root*.go`, `**/*commands*.go`
* Oraclepack dispatcher / command-detection / override-injection / validation pipeline (regex currently `^(\\s*)(oracle)\\b` per tickets): `**/*dispatch*.*`, `**/*detect*.*`, `**/*override*.*`, `**/*validate*.*`, `**/internal/**`
* Oraclepack TUI override-validation flow (to assess user-visible behavior/back-compat): `**/*tui*.*`, `**/internal/**tui**/**`
* Docs explicitly referenced by the tickets: `**/oraclepack-tui.md`, `**/oracle_pack_and_taskify-skills.md`


--- docs/oracle-questions-2026-01-09-030000/actions/01-contracts-interfaces-ticket-surface-next-experiment.md ---
Next smallest concrete experiment

* Run `rg -n --hidden --no-ignore-vcs 'ticket-action-pack\.md|Action Pack|override validation|dry-run summary|\^\(\\\\s\*\)\(oracle\)\\\\b|\btm\b|\btask-master\b|\bcodex\b|\bgemini\b' .`


--- .taskmaster/templates/example_prd.txt ---
<context>
# Overview  
[Provide a high-level overview of your product here. Explain what problem it solves, who it's for, and why it's valuable.]

# Core Features  
[List and describe the main features of your product. For each feature, include:
- What it does
- Why it's important
- How it works at a high level]

# User Experience  
[Describe the user journey and experience. Include:
- User personas
- Key user flows
- UI/UX considerations]
</context>
<PRD>
# Technical Architecture  
[Outline the technical implementation details:
- System components
- Data models
- APIs and integrations
- Infrastructure requirements]

# Development Roadmap  
[Break down the development process into phases:
- MVP requirements
- Future enhancements
- Do not think about timelines whatsoever -- all that matters is scope and detailing exactly what needs to be build in each phase so it can later be cut up into tasks]

# Logical Dependency Chain
[Define the logical order of development:
- Which features need to be built first (foundation)
- Getting as quickly as possible to something usable/visible front end that works
- Properly pacing and scoping each feature so it is atomic but can also be built upon and improved as development approaches]

# Risks and Mitigations  
[Identify potential risks and how they'll be addressed:
- Technical challenges
- Figuring out the MVP that we can build upon
- Resource constraints]

# Appendix  
[Include any additional information:
- Research findings
- Technical specifications]
</PRD>

--- .taskmaster/templates/example_prd_rpg.txt ---
<rpg-method>
# Repository Planning Graph (RPG) Method - PRD Template

This template teaches you (AI or human) how to create structured, dependency-aware PRDs using the RPG methodology from Microsoft Research. The key insight: separate WHAT (functional) from HOW (structural), then connect them with explicit dependencies.

## Core Principles

1. **Dual-Semantics**: Think functional (capabilities) AND structural (code organization) separately, then map them
2. **Explicit Dependencies**: Never assume - always state what depends on what
3. **Topological Order**: Build foundation first, then layers on top
4. **Progressive Refinement**: Start broad, refine iteratively

## How to Use This Template

- Follow the instructions in each `<instruction>` block
- Look at `<example>` blocks to see good vs bad patterns
- Fill in the content sections with your project details
- The AI reading this will learn the RPG method by following along
- Task Master will parse the resulting PRD into dependency-aware tasks

## Recommended Tools for Creating PRDs

When using this template to **create** a PRD (not parse it), use **code-context-aware AI assistants** for best results:

**Why?** The AI needs to understand your existing codebase to make good architectural decisions about modules, dependencies, and integration points.

**Recommended tools:**
- **Claude Code** (claude-code CLI) - Best for structured reasoning and large contexts
- **Cursor/Windsurf** - IDE integration with full codebase context
- **Gemini CLI** (gemini-cli) - Massive context window for large codebases
- **Codex/Grok CLI** - Strong code generation with context awareness

**Note:** Once your PRD is created, `task-master parse-prd` works with any configured AI model - it just needs to read the PRD text itself, not your codebase.
</rpg-method>

---

<overview>
<instruction>
Start with the problem, not the solution. Be specific about:
- What pain point exists?
- Who experiences it?
- Why existing solutions don't work?
- What success looks like (measurable outcomes)?

Keep this section focused - don't jump into implementation details yet.
</instruction>

## Problem Statement
[Describe the core problem. Be concrete about user pain points.]

## Target Users
[Define personas, their workflows, and what they're trying to achieve.]

## Success Metrics
[Quantifiable outcomes. Examples: "80% task completion via autopilot", "< 5% manual intervention rate"]

</overview>

---

<functional-decomposition>
<instruction>
Now think about CAPABILITIES (what the system DOES), not code structure yet.

Step 1: Identify high-level capability domains
- Think: "What major things does this system do?"
- Examples: Data Management, Core Processing, Presentation Layer

Step 2: For each capability, enumerate specific features
- Use explore-exploit strategy:
  * Exploit: What features are REQUIRED for core value?
  * Explore: What features make this domain COMPLETE?

Step 3: For each feature, define:
- Description: What it does in one sentence
- Inputs: What data/context it needs
- Outputs: What it produces/returns
- Behavior: Key logic or transformations

<example type="good">
Capability: Data Validation
  Feature: Schema validation
    - Description: Validate JSON payloads against defined schemas
    - Inputs: JSON object, schema definition
    - Outputs: Validation result (pass/fail) + error details
    - Behavior: Iterate fields, check types, enforce constraints

  Feature: Business rule validation
    - Description: Apply domain-specific validation rules
    - Inputs: Validated data object, rule set
    - Outputs: Boolean + list of violated rules
    - Behavior: Execute rules sequentially, short-circuit on failure
</example>

<example type="bad">
Capability: validation.js
  (Problem: This is a FILE, not a CAPABILITY. Mixing structure into functional thinking.)

Capability: Validation
  Feature: Make sure data is good
  (Problem: Too vague. No inputs/outputs. Not actionable.)
</example>
</instruction>

## Capability Tree

### Capability: [Name]
[Brief description of what this capability domain covers]

#### Feature: [Name]
- **Description**: [One sentence]
- **Inputs**: [What it needs]
- **Outputs**: [What it produces]
- **Behavior**: [Key logic]

#### Feature: [Name]
- **Description**:
- **Inputs**:
- **Outputs**:
- **Behavior**:

### Capability: [Name]
...

</functional-decomposition>

---

<structural-decomposition>
<instruction>
NOW think about code organization. Map capabilities to actual file/folder structure.

Rules:
1. Each capability maps to a module (folder or file)
2. Features within a capability map to functions/classes
3. Use clear module boundaries - each module has ONE responsibility
4. Define what each module exports (public interface)

The goal: Create a clear mapping between "what it does" (functional) and "where it lives" (structural).

<example type="good">
Capability: Data Validation
  → Maps to: src/validation/
    ├── schema-validator.js      (Schema validation feature)
    ├── rule-validator.js         (Business rule validation feature)
    └── index.js                  (Public exports)

Exports:
  - validateSchema(data, schema)
  - validateRules(data, rules)
</example>

<example type="bad">
Capability: Data Validation
  → Maps to: src/utils.js
  (Problem: "utils" is not a clear module boundary. Where do I find validation logic?)

Capability: Data Validation
  → Maps to: src/validation/everything.js
  (Problem: One giant file. Features should map to separate files for maintainability.)
</example>
</instruction>

## Repository Structure

```
project-root/
├── src/
│   ├── [module-name]/       # Maps to: [Capability Name]
│   │   ├── [file].js        # Maps to: [Feature Name]
│   │   └── index.js         # Public exports
│   └── [module-name]/
├── tests/
└── docs/
```

## Module Definitions

### Module: [Name]
- **Maps to capability**: [Capability from functional decomposition]
- **Responsibility**: [Single clear purpose]
- **File structure**:
  ```
  module-name/
  ├── feature1.js
  ├── feature2.js
  └── index.js
  ```
- **Exports**:
  - `functionName()` - [what it does]
  - `ClassName` - [what it does]

</structural-decomposition>

---

<dependency-graph>
<instruction>
This is THE CRITICAL SECTION for Task Master parsing.

Define explicit dependencies between modules. This creates the topological order for task execution.

Rules:
1. List modules in dependency order (foundation first)
2. For each module, state what it depends on
3. Foundation modules should have NO dependencies
4. Every non-foundation module should depend on at least one other module
5. Think: "What must EXIST before I can build this module?"

<example type="good">
Foundation Layer (no dependencies):
  - error-handling: No dependencies
  - config-manager: No dependencies
  - base-types: No dependencies

Data Layer:
  - schema-validator: Depends on [base-types, error-handling]
  - data-ingestion: Depends on [schema-validator, config-manager]

Core Layer:
  - algorithm-engine: Depends on [base-types, error-handling]
  - pipeline-orchestrator: Depends on [algorithm-engine, data-ingestion]
</example>

<example type="bad">
- validation: Depends on API
- API: Depends on validation
(Problem: Circular dependency. This will cause build/runtime issues.)

- user-auth: Depends on everything
(Problem: Too many dependencies. Should be more focused.)
</example>
</instruction>

## Dependency Chain

### Foundation Layer (Phase 0)
No dependencies - these are built first.

- **[Module Name]**: [What it provides]
- **[Module Name]**: [What it provides]

### [Layer Name] (Phase 1)
- **[Module Name]**: Depends on [[module-from-phase-0], [module-from-phase-0]]
- **[Module Name]**: Depends on [[module-from-phase-0]]

### [Layer Name] (Phase 2)
- **[Module Name]**: Depends on [[module-from-phase-1], [module-from-foundation]]

[Continue building up layers...]

</dependency-graph>

---

<implementation-roadmap>
<instruction>
Turn the dependency graph into concrete development phases.

Each phase should:
1. Have clear entry criteria (what must exist before starting)
2. Contain tasks that can be parallelized (no inter-dependencies within phase)
3. Have clear exit criteria (how do we know phase is complete?)
4. Build toward something USABLE (not just infrastructure)

Phase ordering follows topological sort of dependency graph.

<example type="good">
Phase 0: Foundation
  Entry: Clean repository
  Tasks:
    - Implement error handling utilities
    - Create base type definitions
    - Setup configuration system
  Exit: Other modules can import foundation without errors

Phase 1: Data Layer
  Entry: Phase 0 complete
  Tasks:
    - Implement schema validator (uses: base types, error handling)
    - Build data ingestion pipeline (uses: validator, config)
  Exit: End-to-end data flow from input to validated output
</example>

<example type="bad">
Phase 1: Build Everything
  Tasks:
    - API
    - Database
    - UI
    - Tests
  (Problem: No clear focus. Too broad. Dependencies not considered.)
</example>
</instruction>

## Development Phases

### Phase 0: [Foundation Name]
**Goal**: [What foundational capability this establishes]

**Entry Criteria**: [What must be true before starting]

**Tasks**:
- [ ] [Task name] (depends on: [none or list])
  - Acceptance criteria: [How we know it's done]
  - Test strategy: [What tests prove it works]

- [ ] [Task name] (depends on: [none or list])

**Exit Criteria**: [Observable outcome that proves phase complete]

**Delivers**: [What can users/developers do after this phase?]

---

### Phase 1: [Layer Name]
**Goal**:

**Entry Criteria**: Phase 0 complete

**Tasks**:
- [ ] [Task name] (depends on: [[tasks-from-phase-0]])
- [ ] [Task name] (depends on: [[tasks-from-phase-0]])

**Exit Criteria**:

**Delivers**:

---

[Continue with more phases...]

</implementation-roadmap>

---

<test-strategy>
<instruction>
Define how testing will be integrated throughout development (TDD approach).

Specify:
1. Test pyramid ratios (unit vs integration vs e2e)
2. Coverage requirements
3. Critical test scenarios
4. Test generation guidelines for Surgical Test Generator

This section guides the AI when generating tests during the RED phase of TDD.

<example type="good">
Critical Test Scenarios for Data Validation module:
  - Happy path: Valid data passes all checks
  - Edge cases: Empty strings, null values, boundary numbers
  - Error cases: Invalid types, missing required fields
  - Integration: Validator works with ingestion pipeline
</example>
</instruction>

## Test Pyramid

```
        /\
       /E2E\       ← [X]% (End-to-end, slow, comprehensive)
      /------\
     /Integration\ ← [Y]% (Module interactions)
    /------------\
   /  Unit Tests  \ ← [Z]% (Fast, isolated, deterministic)
  /----------------\
```

## Coverage Requirements
- Line coverage: [X]% minimum
- Branch coverage: [X]% minimum
- Function coverage: [X]% minimum
- Statement coverage: [X]% minimum

## Critical Test Scenarios

### [Module/Feature Name]
**Happy path**:
- [Scenario description]
- Expected: [What should happen]

**Edge cases**:
- [Scenario description]
- Expected: [What should happen]

**Error cases**:
- [Scenario description]
- Expected: [How system handles failure]

**Integration points**:
- [What interactions to test]
- Expected: [End-to-end behavior]

## Test Generation Guidelines
[Specific instructions for Surgical Test Generator about what to focus on, what patterns to follow, project-specific test conventions]

</test-strategy>

---

<architecture>
<instruction>
Describe technical architecture, data models, and key design decisions.

Keep this section AFTER functional/structural decomposition - implementation details come after understanding structure.
</instruction>

## System Components
[Major architectural pieces and their responsibilities]

## Data Models
[Core data structures, schemas, database design]

## Technology Stack
[Languages, frameworks, key libraries]

**Decision: [Technology/Pattern]**
- **Rationale**: [Why chosen]
- **Trade-offs**: [What we're giving up]
- **Alternatives considered**: [What else we looked at]

</architecture>

---

<risks>
<instruction>
Identify risks that could derail development and how to mitigate them.

Categories:
- Technical risks (complexity, unknowns)
- Dependency risks (blocking issues)
- Scope risks (creep, underestimation)
</instruction>

## Technical Risks
**Risk**: [Description]
- **Impact**: [High/Medium/Low - effect on project]
- **Likelihood**: [High/Medium/Low]
- **Mitigation**: [How to address]
- **Fallback**: [Plan B if mitigation fails]

## Dependency Risks
[External dependencies, blocking issues]

## Scope Risks
[Scope creep, underestimation, unclear requirements]

</risks>

---

<appendix>
## References
[Papers, documentation, similar systems]

## Glossary
[Domain-specific terms]

## Open Questions
[Things to resolve during development]
</appendix>

---

<task-master-integration>
# How Task Master Uses This PRD

When you run `task-master parse-prd <file>.txt`, the parser:

1. **Extracts capabilities** → Main tasks
   - Each `### Capability:` becomes a top-level task

2. **Extracts features** → Subtasks
   - Each `#### Feature:` becomes a subtask under its capability

3. **Parses dependencies** → Task dependencies
   - `Depends on: [X, Y]` sets task.dependencies = ["X", "Y"]

4. **Orders by phases** → Task priorities
   - Phase 0 tasks = highest priority
   - Phase N tasks = lower priority, properly sequenced

5. **Uses test strategy** → Test generation context
   - Feeds test scenarios to Surgical Test Generator during implementation

**Result**: A dependency-aware task graph that can be executed in topological order.

## Why RPG Structure Matters

Traditional flat PRDs lead to:
- ❌ Unclear task dependencies
- ❌ Arbitrary task ordering
- ❌ Circular dependencies discovered late
- ❌ Poorly scoped tasks

RPG-structured PRDs provide:
- ✅ Explicit dependency chains
- ✅ Topological execution order
- ✅ Clear module boundaries
- ✅ Validated task graph before implementation

## Tips for Best Results

1. **Spend time on dependency graph** - This is the most valuable section for Task Master
2. **Keep features atomic** - Each feature should be independently testable
3. **Progressive refinement** - Start broad, use `task-master expand` to break down complex tasks
4. **Use research mode** - `task-master parse-prd --research` leverages AI for better task generation
</task-master-integration>


--- skills/oraclepack-codebase-pack-grouped/references/attachment-minimization.md ---
# Attachment minimization rules (Codebase Stage 1 — Direct Attach)

Objective: keep each group pack focused and portable.

## Code attachments

- Code files are attached directly in each step via `${code_args[@]}`.
- Use `group_max_files` (default 200) to bound per-pack file count.
- If a group is larger than the cap, split into multiple packs (part 1..N).
- Prefer code_glob + include_exts to avoid irrelevant files.

## Non-code attachments (extra_files)

- Keep explicit non-code attachments to **0–1 per step**.
- Prefer a single high-signal file (e.g., README, architecture doc).

## extra_files (literal append)

- If `extra_files` is provided, append it literally to every oracle command.
- It may include additional `-f/--file` flags.
- Place `extra_files` on its own line with a comment:
  - `# extra_files appended literally`


--- skills/oraclepack-tickets-pack-grouped/references/attachment-minimization.md ---
# Attachment minimization rules (Grouped Tickets Stage 1 — Direct Attach)

Objective: keep each group pack focused and portable.

## Ticket attachments

- Ticket files are attached directly in each step via `${ticket_args[@]}`.
- Use `group_max_files` (default 25) to bound per-pack ticket count.
- If a group is larger than the cap, split into multiple packs (part 1..N).

## Non-ticket attachments (repo evidence)

- Keep explicit non-ticket attachments to **0–1 per step**.
- Prefer a single high-signal file that clarifies contracts or a key code path.

## extra_files (literal append)

- If `extra_files` is provided, append it literally to every oracle command.
- It may include additional `-f/--file` flags.
- Place `extra_files` on its own line with a comment:
  - `# extra_files appended literally`



--- skills/oraclepack-codebase-pack-grouped/references/codebase-grouping.md ---
# Codebase grouping rules (Stage 1 — Direct Attach)

Objective: deterministically split a target codebase into topic/domain groups and produce one Stage-1 pack per group.

## Grouping behavior

- Primary grouping: by top-level subdirectory under `code_root`.
- Loose files (root-level or outside `code_root`) are assigned via token overlap (Jaccard) against existing groups.
- If no group scores above `group_min_score`, loose files fall into a `root` group.

## Determinism

- File discovery is lexicographically sorted.
- Group names are derived from directory names; sharded parts are `group_name part N`.
- Group slug is a normalized lowercase `a-z0-9-` token.

## Limits

- `code_max_files` caps total discovered files before grouping.
- `group_max_files` and `group_max_chars` cap each group pack; groups split into part 1..N.

## Exclusions

- Ignore directories include `.git`, `node_modules`, `dist`, `build`, `.venv`, and other common build outputs.
- Additional ignore names can be provided via `ignore_dirs` (comma-separated).
- Use `exclude_glob` to drop specific paths.


--- skills/oraclepack-codebase-pack-grouped/references/codebase-pack-template.md ---
# Oracle Pack — {{codebase_name}} (Grouped Codebase Stage 1 — Direct Attach)

## Parsed args
- codebase_name: {{codebase_name}}
- out_dir: {{out_dir}}
- oracle_cmd: {{oracle_cmd}}
- oracle_flags: {{oracle_flags}}
- extra_files: {{extra_files}}
- code_root: {{code_root}}
- code_glob: {{code_glob}}
- code_paths: {{code_paths}}
- code_max_files: {{code_max_files}}
- group_name: {{group_name}}
- group_slug: {{group_slug}}
- group_mode: {{group_mode}}
- group_min_score: {{group_min_score}}
- group_max_files: {{group_max_files}}
- group_max_chars: {{group_max_chars}}
- ignore_dirs: {{ignore_dirs}}
- include_exts: {{include_exts}}
- exclude_glob: {{exclude_glob}}
- mode: {{mode}}

Notes (contract):
- Exactly one fenced `bash` block in this document.
- No other ``` fences anywhere.
- Exactly 20 steps, numbered 01..20 in order.
- Step header: `# NN) ROI=... impact=... confidence=... effort=... horizon=... category=... reference=...`
- Every step includes: `--write-output "{{out_dir}}/NN-<slug>.md"` (double quotes required).
- Steps must be self-contained and must not rely on shell variables created in previous steps.
- Each step must attach code files directly (no bundle dependency).
- Pack ends with a Coverage check section listing all 10 categories as OK or Missing(<step ids>).

```bash
set -euo pipefail

mkdir -p "{{out_dir}}"

# 01) ROI=4.8 impact=6 confidence=0.80 effort=1 horizon=Immediate category=contracts/interfaces reference={{group_slug}}

# code files attached directly (deterministic; self-contained)
mapfile -t __code_files < <(python3 - <<'PY'
from __future__ import annotations
import json

files = json.loads(r'''{{group_files_json}}''')
for p in files:
    print(p)
PY
)

code_args=()
for p in "${__code_files[@]}"; do
  code_args+=(-f "$p")
done

if [ "${#code_args[@]}" -eq 0 ]; then
  echo "WARNING: no code files resolved for group '{{group_name}}'." >&2
fi

# extra_files appended literally (may be empty; may include -f/--file):
{{oracle_cmd}}   {{oracle_flags}}   --write-output "{{out_dir}}/01-contracts-interfaces-public-surface.md"   "${code_args[@]}"   {{extra_files}}   -p "$(cat <<'PROMPT'
Strategist question #01  (codebase-driven, group: {{group_name}})

Reference: {{group_slug}}
Category: contracts/interfaces
Horizon: Immediate
ROI: 4.8 (impact=6, confidence=0.80, effort=1)

Question:
Using the attached code files as primary evidence, map the public surface area (CLI/TUI/API/interfaces/contracts). Call out backwards-compat constraints.

Constraints: None
Non-goals: None

Answer format:
Use headings exactly: ### Direct answer / ### Risks and unknowns / ### Next experiment / ### Missing evidence.
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 02) ROI=4.6 impact=6 confidence=0.78 effort=1 horizon=Immediate category=contracts/interfaces reference={{group_slug}}

# code files attached directly (deterministic; self-contained)
mapfile -t __code_files < <(python3 - <<'PY'
from __future__ import annotations
import json

files = json.loads(r'''{{group_files_json}}''')
for p in files:
    print(p)
PY
)

code_args=()
for p in "${__code_files[@]}"; do
  code_args+=(-f "$p")
done

if [ "${#code_args[@]}" -eq 0 ]; then
  echo "WARNING: no code files resolved for group '{{group_name}}'." >&2
fi

# extra_files appended literally (may be empty; may include -f/--file):
{{oracle_cmd}}   {{oracle_flags}}   --write-output "{{out_dir}}/02-contracts-interfaces-integrations.md"   "${code_args[@]}"   {{extra_files}}   -p "$(cat <<'PROMPT'
Strategist question #02  (codebase-driven, group: {{group_name}})

Reference: {{group_slug}}
Category: contracts/interfaces
Horizon: Immediate
ROI: 4.6 (impact=6, confidence=0.78, effort=1)

Question:
Using the attached code files as primary evidence, identify external integrations implied by this area; required config/contract changes; failure/timeout behavior; minimal compat-safe rollout.

Constraints: None
Non-goals: None

Answer format:
Use headings exactly: ### Direct answer / ### Risks and unknowns / ### Next experiment / ### Missing evidence.
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 03) ROI=5.1 impact=7 confidence=0.74 effort=1 horizon=NearTerm category=invariants reference={{group_slug}}

# code files attached directly (deterministic; self-contained)
mapfile -t __code_files < <(python3 - <<'PY'
from __future__ import annotations
import json

files = json.loads(r'''{{group_files_json}}''')
for p in files:
    print(p)
PY
)

code_args=()
for p in "${__code_files[@]}"; do
  code_args+=(-f "$p")
done

if [ "${#code_args[@]}" -eq 0 ]; then
  echo "WARNING: no code files resolved for group '{{group_name}}'." >&2
fi

# extra_files appended literally (may be empty; may include -f/--file):
{{oracle_cmd}}   {{oracle_flags}}   --write-output "{{out_dir}}/03-invariants-invariant-map.md"   "${code_args[@]}"   {{extra_files}}   -p "$(cat <<'PROMPT'
Strategist question #03  (codebase-driven, group: {{group_name}})

Reference: {{group_slug}}
Category: invariants
Horizon: NearTerm
ROI: 5.1 (impact=7, confidence=0.74, effort=1)

Question:
Using the attached code files, map invariants and critical assumptions (data shape, ordering, idempotency, contracts). Identify the weakest or least-tested invariant.

Constraints: None
Non-goals: None

Answer format:
Use headings exactly: ### Direct answer / ### Risks and unknowns / ### Next experiment / ### Missing evidence.
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 04) ROI=4.7 impact=6 confidence=0.76 effort=1 horizon=NearTerm category=caching/state reference={{group_slug}}

# code files attached directly (deterministic; self-contained)
mapfile -t __code_files < <(python3 - <<'PY'
from __future__ import annotations
import json

files = json.loads(r'''{{group_files_json}}''')
for p in files:
    print(p)
PY
)

code_args=()
for p in "${__code_files[@]}"; do
  code_args+=(-f "$p")
done

if [ "${#code_args[@]}" -eq 0 ]; then
  echo "WARNING: no code files resolved for group '{{group_name}}'." >&2
fi

# extra_files appended literally (may be empty; may include -f/--file):
{{oracle_cmd}}   {{oracle_flags}}   --write-output "{{out_dir}}/04-caching-state-reads-writes.md"   "${code_args[@]}"   {{extra_files}}   -p "$(cat <<'PROMPT'
Strategist question #04  (codebase-driven, group: {{group_name}})

Reference: {{group_slug}}
Category: caching/state
Horizon: NearTerm
ROI: 4.7 (impact=6, confidence=0.76, effort=1)

Question:
Using the attached code files, identify stateful reads/writes and any caches (in-memory, disk, external). Note invalidation boundaries and any silent staleness risks.

Constraints: None
Non-goals: None

Answer format:
Use headings exactly: ### Direct answer / ### Risks and unknowns / ### Next experiment / ### Missing evidence.
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 05) ROI=4.4 impact=6 confidence=0.70 effort=1 horizon=NearTerm category=background jobs reference={{group_slug}}

# code files attached directly (deterministic; self-contained)
mapfile -t __code_files < <(python3 - <<'PY'
from __future__ import annotations
import json

files = json.loads(r'''{{group_files_json}}''')
for p in files:
    print(p)
PY
)

code_args=()
for p in "${__code_files[@]}"; do
  code_args+=(-f "$p")
done

if [ "${#code_args[@]}" -eq 0 ]; then
  echo "WARNING: no code files resolved for group '{{group_name}}'." >&2
fi

# extra_files appended literally (may be empty; may include -f/--file):
{{oracle_cmd}}   {{oracle_flags}}   --write-output "{{out_dir}}/05-background-jobs-queues.md"   "${code_args[@]}"   {{extra_files}}   -p "$(cat <<'PROMPT'
Strategist question #05  (codebase-driven, group: {{group_name}})

Reference: {{group_slug}}
Category: background jobs
Horizon: NearTerm
ROI: 4.4 (impact=6, confidence=0.70, effort=1)

Question:
Using the attached code files, list any background jobs/queues/cron tasks. Note retries, idempotency, and failure modes.

Constraints: None
Non-goals: None

Answer format:
Use headings exactly: ### Direct answer / ### Risks and unknowns / ### Next experiment / ### Missing evidence.
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 06) ROI=4.5 impact=6 confidence=0.75 effort=1 horizon=Immediate category=observability reference={{group_slug}}

# code files attached directly (deterministic; self-contained)
mapfile -t __code_files < <(python3 - <<'PY'
from __future__ import annotations
import json

files = json.loads(r'''{{group_files_json}}''')
for p in files:
    print(p)
PY
)

code_args=()
for p in "${__code_files[@]}"; do
  code_args+=(-f "$p")
done

if [ "${#code_args[@]}" -eq 0 ]; then
  echo "WARNING: no code files resolved for group '{{group_name}}'." >&2
fi

# extra_files appended literally (may be empty; may include -f/--file):
{{oracle_cmd}}   {{oracle_flags}}   --write-output "{{out_dir}}/06-observability-logging-metrics.md"   "${code_args[@]}"   {{extra_files}}   -p "$(cat <<'PROMPT'
Strategist question #06  (codebase-driven, group: {{group_name}})

Reference: {{group_slug}}
Category: observability
Horizon: Immediate
ROI: 4.5 (impact=6, confidence=0.75, effort=1)

Question:
Using the attached code files, identify logging/metrics/tracing in this area. Call out missing signals for debugging incidents.

Constraints: None
Non-goals: None

Answer format:
Use headings exactly: ### Direct answer / ### Risks and unknowns / ### Next experiment / ### Missing evidence.
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 07) ROI=4.3 impact=6 confidence=0.68 effort=1 horizon=NearTerm category=permissions reference={{group_slug}}

# code files attached directly (deterministic; self-contained)
mapfile -t __code_files < <(python3 - <<'PY'
from __future__ import annotations
import json

files = json.loads(r'''{{group_files_json}}''')
for p in files:
    print(p)
PY
)

code_args=()
for p in "${__code_files[@]}"; do
  code_args+=(-f "$p")
done

if [ "${#code_args[@]}" -eq 0 ]; then
  echo "WARNING: no code files resolved for group '{{group_name}}'." >&2
fi

# extra_files appended literally (may be empty; may include -f/--file):
{{oracle_cmd}}   {{oracle_flags}}   --write-output "{{out_dir}}/07-permissions-authz.md"   "${code_args[@]}"   {{extra_files}}   -p "$(cat <<'PROMPT'
Strategist question #07  (codebase-driven, group: {{group_name}})

Reference: {{group_slug}}
Category: permissions
Horizon: NearTerm
ROI: 4.3 (impact=6, confidence=0.68, effort=1)

Question:
Using the attached code files, identify authorization and permission checks. Note any missing checks or implicit trust boundaries.

Constraints: None
Non-goals: None

Answer format:
Use headings exactly: ### Direct answer / ### Risks and unknowns / ### Next experiment / ### Missing evidence.
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 08) ROI=4.2 impact=6 confidence=0.66 effort=1 horizon=NearTerm category=migrations reference={{group_slug}}

# code files attached directly (deterministic; self-contained)
mapfile -t __code_files < <(python3 - <<'PY'
from __future__ import annotations
import json

files = json.loads(r'''{{group_files_json}}''')
for p in files:
    print(p)
PY
)

code_args=()
for p in "${__code_files[@]}"; do
  code_args+=(-f "$p")
done

if [ "${#code_args[@]}" -eq 0 ]; then
  echo "WARNING: no code files resolved for group '{{group_name}}'." >&2
fi

# extra_files appended literally (may be empty; may include -f/--file):
{{oracle_cmd}}   {{oracle_flags}}   --write-output "{{out_dir}}/08-migrations-backfills.md"   "${code_args[@]}"   {{extra_files}}   -p "$(cat <<'PROMPT'
Strategist question #08  (codebase-driven, group: {{group_name}})

Reference: {{group_slug}}
Category: migrations
Horizon: NearTerm
ROI: 4.2 (impact=6, confidence=0.66, effort=1)

Question:
Using the attached code files, identify migrations/backfills/data-shape changes implied in this area. Note rollout risks.

Constraints: None
Non-goals: None

Answer format:
Use headings exactly: ### Direct answer / ### Risks and unknowns / ### Next experiment / ### Missing evidence.
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 09) ROI=4.1 impact=6 confidence=0.64 effort=1 horizon=NearTerm category=UX flows reference={{group_slug}}

# code files attached directly (deterministic; self-contained)
mapfile -t __code_files < <(python3 - <<'PY'
from __future__ import annotations
import json

files = json.loads(r'''{{group_files_json}}''')
for p in files:
    print(p)
PY
)

code_args=()
for p in "${__code_files[@]}"; do
  code_args+=(-f "$p")
done

if [ "${#code_args[@]}" -eq 0 ]; then
  echo "WARNING: no code files resolved for group '{{group_name}}'." >&2
fi

# extra_files appended literally (may be empty; may include -f/--file):
{{oracle_cmd}}   {{oracle_flags}}   --write-output "{{out_dir}}/09-ux-flows.md"   "${code_args[@]}"   {{extra_files}}   -p "$(cat <<'PROMPT'
Strategist question #09  (codebase-driven, group: {{group_name}})

Reference: {{group_slug}}
Category: UX flows
Horizon: NearTerm
ROI: 4.1 (impact=6, confidence=0.64, effort=1)

Question:
Using the attached code files, describe the main user flows in this area. Note any fragile or confusing steps.

Constraints: None
Non-goals: None

Answer format:
Use headings exactly: ### Direct answer / ### Risks and unknowns / ### Next experiment / ### Missing evidence.
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 10) ROI=4.0 impact=6 confidence=0.62 effort=1 horizon=NearTerm category=failure modes reference={{group_slug}}

# code files attached directly (deterministic; self-contained)
mapfile -t __code_files < <(python3 - <<'PY'
from __future__ import annotations
import json

files = json.loads(r'''{{group_files_json}}''')
for p in files:
    print(p)
PY
)

code_args=()
for p in "${__code_files[@]}"; do
  code_args+=(-f "$p")
done

if [ "${#code_args[@]}" -eq 0 ]; then
  echo "WARNING: no code files resolved for group '{{group_name}}'." >&2
fi

# extra_files appended literally (may be empty; may include -f/--file):
{{oracle_cmd}}   {{oracle_flags}}   --write-output "{{out_dir}}/10-failure-modes.md"   "${code_args[@]}"   {{extra_files}}   -p "$(cat <<'PROMPT'
Strategist question #10  (codebase-driven, group: {{group_name}})

Reference: {{group_slug}}
Category: failure modes
Horizon: NearTerm
ROI: 4.0 (impact=6, confidence=0.62, effort=1)

Question:
Using the attached code files, enumerate likely failure modes (network, data, validation, retries). Note missing handling.

Constraints: None
Non-goals: None

Answer format:
Use headings exactly: ### Direct answer / ### Risks and unknowns / ### Next experiment / ### Missing evidence.
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 11) ROI=3.8 impact=6 confidence=0.60 effort=1 horizon=NearTerm category=feature flags reference={{group_slug}}

# code files attached directly (deterministic; self-contained)
mapfile -t __code_files < <(python3 - <<'PY'
from __future__ import annotations
import json

files = json.loads(r'''{{group_files_json}}''')
for p in files:
    print(p)
PY
)

code_args=()
for p in "${__code_files[@]}"; do
  code_args+=(-f "$p")
done

if [ "${#code_args[@]}" -eq 0 ]; then
  echo "WARNING: no code files resolved for group '{{group_name}}'." >&2
fi

# extra_files appended literally (may be empty; may include -f/--file):
{{oracle_cmd}}   {{oracle_flags}}   --write-output "{{out_dir}}/11-feature-flags.md"   "${code_args[@]}"   {{extra_files}}   -p "$(cat <<'PROMPT'
Strategist question #11  (codebase-driven, group: {{group_name}})

Reference: {{group_slug}}
Category: feature flags
Horizon: NearTerm
ROI: 3.8 (impact=6, confidence=0.60, effort=1)

Question:
Using the attached code files, identify any feature flags or config toggles. Note rollout/rollback behavior and gaps.

Constraints: None
Non-goals: None

Answer format:
Use headings exactly: ### Direct answer / ### Risks and unknowns / ### Next experiment / ### Missing evidence.
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 12) ROI=3.9 impact=6 confidence=0.62 effort=1 horizon=NearTerm category=caching/state reference={{group_slug}}

# code files attached directly (deterministic; self-contained)
mapfile -t __code_files < <(python3 - <<'PY'
from __future__ import annotations
import json

files = json.loads(r'''{{group_files_json}}''')
for p in files:
    print(p)
PY
)

code_args=()
for p in "${__code_files[@]}"; do
  code_args+=(-f "$p")
done

if [ "${#code_args[@]}" -eq 0 ]; then
  echo "WARNING: no code files resolved for group '{{group_name}}'." >&2
fi

# extra_files appended literally (may be empty; may include -f/--file):
{{oracle_cmd}}   {{oracle_flags}}   --write-output "{{out_dir}}/12-caching-state-consistency.md"   "${code_args[@]}"   {{extra_files}}   -p "$(cat <<'PROMPT'
Strategist question #12  (codebase-driven, group: {{group_name}})

Reference: {{group_slug}}
Category: caching/state
Horizon: NearTerm
ROI: 3.9 (impact=6, confidence=0.62, effort=1)

Question:
Using the attached code files, identify consistency boundaries (read-after-write, eventual vs strong). Note any mismatches across layers.

Constraints: None
Non-goals: None

Answer format:
Use headings exactly: ### Direct answer / ### Risks and unknowns / ### Next experiment / ### Missing evidence.
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 13) ROI=3.7 impact=6 confidence=0.58 effort=1 horizon=MidTerm category=observability reference={{group_slug}}

# code files attached directly (deterministic; self-contained)
mapfile -t __code_files < <(python3 - <<'PY'
from __future__ import annotations
import json

files = json.loads(r'''{{group_files_json}}''')
for p in files:
    print(p)
PY
)

code_args=()
for p in "${__code_files[@]}"; do
  code_args+=(-f "$p")
done

if [ "${#code_args[@]}" -eq 0 ]; then
  echo "WARNING: no code files resolved for group '{{group_name}}'." >&2
fi

# extra_files appended literally (may be empty; may include -f/--file):
{{oracle_cmd}}   {{oracle_flags}}   --write-output "{{out_dir}}/13-observability-gaps.md"   "${code_args[@]}"   {{extra_files}}   -p "$(cat <<'PROMPT'
Strategist question #13  (codebase-driven, group: {{group_name}})

Reference: {{group_slug}}
Category: observability
Horizon: MidTerm
ROI: 3.7 (impact=6, confidence=0.58, effort=1)

Question:
Using the attached code files, identify observability gaps that will block triage or SLA guarantees.

Constraints: None
Non-goals: None

Answer format:
Use headings exactly: ### Direct answer / ### Risks and unknowns / ### Next experiment / ### Missing evidence.
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 14) ROI=3.6 impact=6 confidence=0.56 effort=1 horizon=MidTerm category=permissions reference={{group_slug}}

# code files attached directly (deterministic; self-contained)
mapfile -t __code_files < <(python3 - <<'PY'
from __future__ import annotations
import json

files = json.loads(r'''{{group_files_json}}''')
for p in files:
    print(p)
PY
)

code_args=()
for p in "${__code_files[@]}"; do
  code_args+=(-f "$p")
done

if [ "${#code_args[@]}" -eq 0 ]; then
  echo "WARNING: no code files resolved for group '{{group_name}}'." >&2
fi

# extra_files appended literally (may be empty; may include -f/--file):
{{oracle_cmd}}   {{oracle_flags}}   --write-output "{{out_dir}}/14-permissions-gaps.md"   "${code_args[@]}"   {{extra_files}}   -p "$(cat <<'PROMPT'
Strategist question #14  (codebase-driven, group: {{group_name}})

Reference: {{group_slug}}
Category: permissions
Horizon: MidTerm
ROI: 3.6 (impact=6, confidence=0.56, effort=1)

Question:
Using the attached code files, identify authorization edge cases or privilege escalations to test.

Constraints: None
Non-goals: None

Answer format:
Use headings exactly: ### Direct answer / ### Risks and unknowns / ### Next experiment / ### Missing evidence.
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 15) ROI=3.5 impact=6 confidence=0.54 effort=1 horizon=MidTerm category=migrations reference={{group_slug}}

# code files attached directly (deterministic; self-contained)
mapfile -t __code_files < <(python3 - <<'PY'
from __future__ import annotations
import json

files = json.loads(r'''{{group_files_json}}''')
for p in files:
    print(p)
PY
)

code_args=()
for p in "${__code_files[@]}"; do
  code_args+=(-f "$p")
done

if [ "${#code_args[@]}" -eq 0 ]; then
  echo "WARNING: no code files resolved for group '{{group_name}}'." >&2
fi

# extra_files appended literally (may be empty; may include -f/--file):
{{oracle_cmd}}   {{oracle_flags}}   --write-output "{{out_dir}}/15-migrations-risk.md"   "${code_args[@]}"   {{extra_files}}   -p "$(cat <<'PROMPT'
Strategist question #15  (codebase-driven, group: {{group_name}})

Reference: {{group_slug}}
Category: migrations
Horizon: MidTerm
ROI: 3.5 (impact=6, confidence=0.54, effort=1)

Question:
Using the attached code files, identify any migration risks, data backfill triggers, or state shape changes that require careful sequencing.

Constraints: None
Non-goals: None

Answer format:
Use headings exactly: ### Direct answer / ### Risks and unknowns / ### Next experiment / ### Missing evidence.
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 16) ROI=3.4 impact=6 confidence=0.52 effort=1 horizon=MidTerm category=UX flows reference={{group_slug}}

# code files attached directly (deterministic; self-contained)
mapfile -t __code_files < <(python3 - <<'PY'
from __future__ import annotations
import json

files = json.loads(r'''{{group_files_json}}''')
for p in files:
    print(p)
PY
)

code_args=()
for p in "${__code_files[@]}"; do
  code_args+=(-f "$p")
done

if [ "${#code_args[@]}" -eq 0 ]; then
  echo "WARNING: no code files resolved for group '{{group_name}}'." >&2
fi

# extra_files appended literally (may be empty; may include -f/--file):
{{oracle_cmd}}   {{oracle_flags}}   --write-output "{{out_dir}}/16-ux-flow-gaps.md"   "${code_args[@]}"   {{extra_files}}   -p "$(cat <<'PROMPT'
Strategist question #16  (codebase-driven, group: {{group_name}})

Reference: {{group_slug}}
Category: UX flows
Horizon: MidTerm
ROI: 3.4 (impact=6, confidence=0.52, effort=1)

Question:
Using the attached code files, identify UX or developer flow bottlenecks; propose smallest flow test to validate.

Constraints: None
Non-goals: None

Answer format:
Use headings exactly: ### Direct answer / ### Risks and unknowns / ### Next experiment / ### Missing evidence.
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 17) ROI=3.3 impact=6 confidence=0.50 effort=1 horizon=MidTerm category=failure modes reference={{group_slug}}

# code files attached directly (deterministic; self-contained)
mapfile -t __code_files < <(python3 - <<'PY'
from __future__ import annotations
import json

files = json.loads(r'''{{group_files_json}}''')
for p in files:
    print(p)
PY
)

code_args=()
for p in "${__code_files[@]}"; do
  code_args+=(-f "$p")
done

if [ "${#code_args[@]}" -eq 0 ]; then
  echo "WARNING: no code files resolved for group '{{group_name}}'." >&2
fi

# extra_files appended literally (may be empty; may include -f/--file):
{{oracle_cmd}}   {{oracle_flags}}   --write-output "{{out_dir}}/17-failure-modes-debt.md"   "${code_args[@]}"   {{extra_files}}   -p "$(cat <<'PROMPT'
Strategist question #17  (codebase-driven, group: {{group_name}})

Reference: {{group_slug}}
Category: failure modes
Horizon: MidTerm
ROI: 3.3 (impact=6, confidence=0.50, effort=1)

Question:
Using the attached code files, list failure handling debt or missing retries/rollbacks and rank by user impact.

Constraints: None
Non-goals: None

Answer format:
Use headings exactly: ### Direct answer / ### Risks and unknowns / ### Next experiment / ### Missing evidence.
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 18) ROI=3.2 impact=6 confidence=0.48 effort=1 horizon=LongTerm category=feature flags reference={{group_slug}}

# code files attached directly (deterministic; self-contained)
mapfile -t __code_files < <(python3 - <<'PY'
from __future__ import annotations
import json

files = json.loads(r'''{{group_files_json}}''')
for p in files:
    print(p)
PY
)

code_args=()
for p in "${__code_files[@]}"; do
  code_args+=(-f "$p")
done

if [ "${#code_args[@]}" -eq 0 ]; then
  echo "WARNING: no code files resolved for group '{{group_name}}'." >&2
fi

# extra_files appended literally (may be empty; may include -f/--file):
{{oracle_cmd}}   {{oracle_flags}}   --write-output "{{out_dir}}/18-feature-flags-roadmap.md"   "${code_args[@]}"   {{extra_files}}   -p "$(cat <<'PROMPT'
Strategist question #18  (codebase-driven, group: {{group_name}})

Reference: {{group_slug}}
Category: feature flags
Horizon: LongTerm
ROI: 3.2 (impact=6, confidence=0.48, effort=1)

Question:
Using the attached code files, identify where staged rollouts or flags should exist but do not.

Constraints: None
Non-goals: None

Answer format:
Use headings exactly: ### Direct answer / ### Risks and unknowns / ### Next experiment / ### Missing evidence.
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 19) ROI=3.1 impact=6 confidence=0.46 effort=1 horizon=LongTerm category=background jobs reference={{group_slug}}

# code files attached directly (deterministic; self-contained)
mapfile -t __code_files < <(python3 - <<'PY'
from __future__ import annotations
import json

files = json.loads(r'''{{group_files_json}}''')
for p in files:
    print(p)
PY
)

code_args=()
for p in "${__code_files[@]}"; do
  code_args+=(-f "$p")
done

if [ "${#code_args[@]}" -eq 0 ]; then
  echo "WARNING: no code files resolved for group '{{group_name}}'." >&2
fi

# extra_files appended literally (may be empty; may include -f/--file):
{{oracle_cmd}}   {{oracle_flags}}   --write-output "{{out_dir}}/19-background-jobs-scale.md"   "${code_args[@]}"   {{extra_files}}   -p "$(cat <<'PROMPT'
Strategist question #19  (codebase-driven, group: {{group_name}})

Reference: {{group_slug}}
Category: background jobs
Horizon: LongTerm
ROI: 3.1 (impact=6, confidence=0.46, effort=1)

Question:
Using the attached code files, identify long-term scaling risks in background processing or async pipelines.

Constraints: None
Non-goals: None

Answer format:
Use headings exactly: ### Direct answer / ### Risks and unknowns / ### Next experiment / ### Missing evidence.
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 20) ROI=3.0 impact=6 confidence=0.44 effort=1 horizon=LongTerm category=contracts/interfaces reference={{group_slug}}

# code files attached directly (deterministic; self-contained)
mapfile -t __code_files < <(python3 - <<'PY'
from __future__ import annotations
import json

files = json.loads(r'''{{group_files_json}}''')
for p in files:
    print(p)
PY
)

code_args=()
for p in "${__code_files[@]}"; do
  code_args+=(-f "$p")
done

if [ "${#code_args[@]}" -eq 0 ]; then
  echo "WARNING: no code files resolved for group '{{group_name}}'." >&2
fi

# extra_files appended literally (may be empty; may include -f/--file):
{{oracle_cmd}}   {{oracle_flags}}   --write-output "{{out_dir}}/20-contracts-interfaces-roadmap.md"   "${code_args[@]}"   {{extra_files}}   -p "$(cat <<'PROMPT'
Strategist question #20  (codebase-driven, group: {{group_name}})

Reference: {{group_slug}}
Category: contracts/interfaces
Horizon: LongTerm
ROI: 3.0 (impact=6, confidence=0.44, effort=1)

Question:
Using the attached code files, identify longer-term public surface changes likely needed in this area.

Constraints: None
Non-goals: None

Answer format:
Use headings exactly: ### Direct answer / ### Risks and unknowns / ### Next experiment / ### Missing evidence.
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"
```

Coverage check:
- contracts/interfaces: OK (01,02,20)
- invariants: OK (03)
- caching/state: OK (04,12)
- background jobs: OK (05,19)
- observability: OK (06,13)
- permissions: OK (07,14)
- migrations: OK (08,15)
- UX flows: OK (09,16)
- failure modes: OK (10,17)
- feature flags: OK (11,18)


--- skills/oraclepack-tickets-pack-grouped/references/ticket-grouping.md ---
# Ticket grouping (deterministic, inferred)

Objective: split tickets into focused topic/domain groups and generate one pack per group.

## Inputs

- `ticket_root` (default `.tickets`)
- `ticket_glob` (default `**/*.md`, relative to `ticket_root`)
- `ticket_paths` (optional; comma-separated explicit files; if present, ignore `ticket_glob`)
- `group_mode` (default `subdir+infer`)
- `group_min_score` (default `0.08`)
- `group_max_files` (default `25`; max tickets per pack; >0)
- `group_max_chars` (default `200000`; max total chars per pack; >0)
- `dedupe_mode` (default `report`; one of `off`, `report`, `prune`, `merge`)
- `dedupe_jaccard` (default `0.55`)
- `dedupe_overlap_hi` (default `0.80`)
- `dedupe_overlap_lo` (default `0.70`)
- `dedupe_delta_min` (default `0.15`)
- `dedupe_body_chars` (default `2000`)

## Deterministic grouping rules

1) Collect tickets:
- If `ticket_paths` is non-empty: split on commas, trim whitespace, use exactly that list.
- Else: glob `ticket_root/ticket_glob`.
- Always sort lexicographically by path string.

2) Detect possible duplicates (if `dedupe_mode != off`):
- Signature: filename stem + first heading + first `dedupe_body_chars` chars.
- Compute `jaccard` + `overlap` between tickets.
- Duplicate edge rule:
  - `overlap >= dedupe_overlap_hi` OR (`jaccard >= dedupe_jaccard` AND `overlap >= dedupe_overlap_lo`)
- Connected components become duplicate clusters.
- Canonical: largest content length; tie-break lexicographic.
- Delta vs redundant:
  - delta if unique token ratio >= `dedupe_delta_min` OR heading differs materially.
  - redundant otherwise.

3) Seed groups by subdir:
- For any path under `ticket_root/<group>/...`, assign to group `<group>`.
- Tickets directly under `ticket_root/` are "loose".

4) Infer loose tickets into groups (if any groups exist):
- Build a token set for each group from:
  - group name tokens
  - ticket filenames (stem tokens)
  - first Markdown heading line (if present)
- For each loose ticket, compute Jaccard overlap score with each group token set.
- If `max_score >= group_min_score`, assign to the best group (stable tie-break by group name).
- Otherwise, assign to `misc`.

5) If no groups exist:
- Put all tickets into a single group named `root`.

6) Merge duplicates into primary group:
- `report`: attach all tickets in the cluster to the canonical’s group.
- `prune`: attach canonical + delta only; drop redundant from attachments.
- `merge`: create `out_dir/_ticket_merges/cluster-XXXX.md` and attach only the merged file.
- Emit `_dupes_possible.json`, `_duplicates.json`, and `_dedupe_plan.json`.

7) Split oversized groups:
- If a group exceeds `group_max_files` or `group_max_chars`, split into parts (1..N)
  in sorted order, chunked deterministically.

Hard rule: do not use mtimes, file sizes, or external ML services.

## Required outputs

- `_groups.json`: mapping of group -> list of ticket paths (lexicographic order)
- Pack file per group (and part), each self-contained and direct-attach
- `manifest.json`: groups with pack path + attached vs original ticket lists


--- skills/oraclepack-tickets-pack-grouped/references/tickets-pack-template.md ---
# Oracle Pack — {{codebase_name}} (Grouped Tickets Stage 1 — Direct Attach)

## Parsed args
- codebase_name: {{codebase_name}}
- out_dir: {{out_dir}}
- oracle_cmd: {{oracle_cmd}}
- oracle_flags: {{oracle_flags}}
- extra_files: {{extra_files}}
- ticket_root: {{ticket_root}}
- ticket_glob: {{ticket_glob}}
- ticket_paths: {{ticket_paths}}
- ticket_max_files: {{ticket_max_files}}
- group_name: {{group_name}}
- group_slug: {{group_slug}}
- mode: {{mode}}

Notes (contract):
- Exactly one fenced `bash` block in this document.
- No other ``` fences anywhere.
- Exactly 20 steps, numbered 01..20 in order.
- Step header: `# NN) ROI=... impact=... confidence=... effort=... horizon=... category=... reference=...`
- Every step includes: `--write-output "{{out_dir}}/NN-<slug>.md"` (double quotes required).
- Steps must be self-contained and must not rely on shell variables created in previous steps.
- Each step must attach tickets directly (no `_tickets_bundle.md` dependency).
- Pack ends with a Coverage check section listing all 10 categories as OK or Missing(<step ids>).

```bash
set -euo pipefail

mkdir -p "{{out_dir}}"

# 01) ROI=4.8 impact=6 confidence=0.80 effort=1 horizon=Immediate category=contracts/interfaces reference={{group_slug}}

# tickets attached directly (deterministic; self-contained)
mapfile -t __tickets < <(python3 - <<'PY'
from __future__ import annotations
from pathlib import Path

TICKET_ROOT = "{{ticket_root}}"
TICKET_GLOB = "{{ticket_glob}}"
TICKET_PATHS = "{{ticket_paths}}".strip()
MAX = int("{{ticket_max_files}}")

root = Path(TICKET_ROOT)

def lex_sorted(ps):
    return sorted((str(p) for p in ps), key=lambda s: s)

if TICKET_PATHS:
    tickets = [Path(p.strip()) for p in TICKET_PATHS.split(",") if p.strip()]
else:
    tickets = list(root.glob(TICKET_GLOB)) if root.exists() else []

tickets = [Path(p) for p in lex_sorted(tickets)]
if MAX and MAX > 0:
    tickets = tickets[:MAX]

for p in tickets:
    print(str(p))
PY
)

ticket_args=()
for p in "${__tickets[@]}"; do
  ticket_args+=(-f "$p")
done

if [ "${#ticket_args[@]}" -eq 0 ]; then
  echo "WARNING: no tickets resolved for group '{{group_name}}'." >&2
fi

# extra_files appended literally (may be empty; may include -f/--file):
{{oracle_cmd}}   {{oracle_flags}}   --write-output "{{out_dir}}/01-contracts-interfaces-ticket-surface.md"   "${ticket_args[@]}"   {{extra_files}}   -p "$(cat <<'PROMPT'
Strategist question #01  (ticket-driven, group: {{group_name}})

Reference: {{group_slug}}
Category: contracts/interfaces
Horizon: Immediate
ROI: 4.8 (impact=6, confidence=0.80, effort=1)

Question:
Using the attached tickets as the primary context, list the public surface changes implied by the tickets (CLI/TUI/API/interfaces/contracts); call out backwards-compat constraints.

Constraints: None
Non-goals: None

Answer format:
Use headings exactly: ### Direct answer / ### Risks and unknowns / ### Next experiment / ### Missing evidence.
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 02) ROI=4.6 impact=6 confidence=0.78 effort=1 horizon=Immediate category=contracts/interfaces reference={{group_slug}}

# tickets attached directly (deterministic; self-contained)
mapfile -t __tickets < <(python3 - <<'PY'
from __future__ import annotations
from pathlib import Path

TICKET_ROOT = "{{ticket_root}}"
TICKET_GLOB = "{{ticket_glob}}"
TICKET_PATHS = "{{ticket_paths}}".strip()
MAX = int("{{ticket_max_files}}")

root = Path(TICKET_ROOT)

def lex_sorted(ps):
    return sorted((str(p) for p in ps), key=lambda s: s)

if TICKET_PATHS:
    tickets = [Path(p.strip()) for p in TICKET_PATHS.split(",") if p.strip()]
else:
    tickets = list(root.glob(TICKET_GLOB)) if root.exists() else []

tickets = [Path(p) for p in lex_sorted(tickets)]
if MAX and MAX > 0:
    tickets = tickets[:MAX]

for p in tickets:
    print(str(p))
PY
)

ticket_args=()
for p in "${__tickets[@]}"; do
  ticket_args+=(-f "$p")
done

if [ "${#ticket_args[@]}" -eq 0 ]; then
  echo "WARNING: no tickets resolved for group '{{group_name}}'." >&2
fi

# extra_files appended literally (may be empty; may include -f/--file):
{{oracle_cmd}}   {{oracle_flags}}   --write-output "{{out_dir}}/02-contracts-interfaces-integration-points.md"   "${ticket_args[@]}"   {{extra_files}}   -p "$(cat <<'PROMPT'
Strategist question #02  (ticket-driven, group: {{group_name}})

Reference: {{group_slug}}
Category: contracts/interfaces
Horizon: Immediate
ROI: 4.6 (impact=6, confidence=0.78, effort=1)

Question:
Using the attached tickets as the primary context, identify external integrations implied by the tickets; required config/contract changes; failure/timeout behavior; minimal compat-safe rollout.

Constraints: None
Non-goals: None

Answer format:
Use headings exactly: ### Direct answer / ### Risks and unknowns / ### Next experiment / ### Missing evidence.
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 03) ROI=5.1 impact=7 confidence=0.74 effort=1 horizon=NearTerm category=invariants reference={{group_slug}}

# tickets attached directly (deterministic; self-contained)
mapfile -t __tickets < <(python3 - <<'PY'
from __future__ import annotations
from pathlib import Path

TICKET_ROOT = "{{ticket_root}}"
TICKET_GLOB = "{{ticket_glob}}"
TICKET_PATHS = "{{ticket_paths}}".strip()
MAX = int("{{ticket_max_files}}")

root = Path(TICKET_ROOT)

def lex_sorted(ps):
    return sorted((str(p) for p in ps), key=lambda s: s)

if TICKET_PATHS:
    tickets = [Path(p.strip()) for p in TICKET_PATHS.split(",") if p.strip()]
else:
    tickets = list(root.glob(TICKET_GLOB)) if root.exists() else []

tickets = [Path(p) for p in lex_sorted(tickets)]
if MAX and MAX > 0:
    tickets = tickets[:MAX]

for p in tickets:
    print(str(p))
PY
)

ticket_args=()
for p in "${__tickets[@]}"; do
  ticket_args+=(-f "$p")
done

if [ "${#ticket_args[@]}" -eq 0 ]; then
  echo "WARNING: no tickets resolved for group '{{group_name}}'." >&2
fi

# extra_files appended literally (may be empty; may include -f/--file):
{{oracle_cmd}}   {{oracle_flags}}   --write-output "{{out_dir}}/03-invariants-invariant-map.md"   "${ticket_args[@]}"   {{extra_files}}   -p "$(cat <<'PROMPT'
Strategist question #03  (ticket-driven, group: {{group_name}})

Reference: {{group_slug}}
Category: invariants
Horizon: NearTerm
ROI: 5.1 (impact=7, confidence=0.74, effort=1)

Question:
Using the attached tickets as the primary context, extract system invariants implied by tickets (inputs/outputs, pack schema rules, step execution rules) and where to enforce them.

Constraints: None
Non-goals: None

Answer format:
Use headings exactly: ### Direct answer / ### Risks and unknowns / ### Next experiment / ### Missing evidence.
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 04) ROI=5.0 impact=7 confidence=0.72 effort=2 horizon=NearTerm category=invariants reference={{group_slug}}

# tickets attached directly (deterministic; self-contained)
mapfile -t __tickets < <(python3 - <<'PY'
from __future__ import annotations
from pathlib import Path

TICKET_ROOT = "{{ticket_root}}"
TICKET_GLOB = "{{ticket_glob}}"
TICKET_PATHS = "{{ticket_paths}}".strip()
MAX = int("{{ticket_max_files}}")

root = Path(TICKET_ROOT)

def lex_sorted(ps):
    return sorted((str(p) for p in ps), key=lambda s: s)

if TICKET_PATHS:
    tickets = [Path(p.strip()) for p in TICKET_PATHS.split(",") if p.strip()]
else:
    tickets = list(root.glob(TICKET_GLOB)) if root.exists() else []

tickets = [Path(p) for p in lex_sorted(tickets)]
if MAX and MAX > 0:
    tickets = tickets[:MAX]

for p in tickets:
    print(str(p))
PY
)

ticket_args=()
for p in "${__tickets[@]}"; do
  ticket_args+=(-f "$p")
done

if [ "${#ticket_args[@]}" -eq 0 ]; then
  echo "WARNING: no tickets resolved for group '{{group_name}}'." >&2
fi

# extra_files appended literally (may be empty; may include -f/--file):
{{oracle_cmd}}   {{oracle_flags}}   --write-output "{{out_dir}}/04-invariants-validation-boundaries.md"   "${ticket_args[@]}"   {{extra_files}}   -p "$(cat <<'PROMPT'
Strategist question #04  (ticket-driven, group: {{group_name}})

Reference: {{group_slug}}
Category: invariants
Horizon: NearTerm
ROI: 5.0 (impact=7, confidence=0.72, effort=2)

Question:
Using the attached tickets as the primary context, identify validation boundaries that must exist (ticket parsing, pack generation, pack validation); propose minimal validation plan.

Constraints: None
Non-goals: None

Answer format:
Use headings exactly: ### Direct answer / ### Risks and unknowns / ### Next experiment / ### Missing evidence.
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 05) ROI=4.4 impact=6 confidence=0.78 effort=2 horizon=NearTerm category=caching/state reference={{group_slug}}

# tickets attached directly (deterministic; self-contained)
mapfile -t __tickets < <(python3 - <<'PY'
from __future__ import annotations
from pathlib import Path

TICKET_ROOT = "{{ticket_root}}"
TICKET_GLOB = "{{ticket_glob}}"
TICKET_PATHS = "{{ticket_paths}}".strip()
MAX = int("{{ticket_max_files}}")

root = Path(TICKET_ROOT)

def lex_sorted(ps):
    return sorted((str(p) for p in ps), key=lambda s: s)

if TICKET_PATHS:
    tickets = [Path(p.strip()) for p in TICKET_PATHS.split(",") if p.strip()]
else:
    tickets = list(root.glob(TICKET_GLOB)) if root.exists() else []

tickets = [Path(p) for p in lex_sorted(tickets)]
if MAX and MAX > 0:
    tickets = tickets[:MAX]

for p in tickets:
    print(str(p))
PY
)

ticket_args=()
for p in "${__tickets[@]}"; do
  ticket_args+=(-f "$p")
done

if [ "${#ticket_args[@]}" -eq 0 ]; then
  echo "WARNING: no tickets resolved for group '{{group_name}}'." >&2
fi

# extra_files appended literally (may be empty; may include -f/--file):
{{oracle_cmd}}   {{oracle_flags}}   --write-output "{{out_dir}}/05-caching-state-state-artifacts.md"   "${ticket_args[@]}"   {{extra_files}}   -p "$(cat <<'PROMPT'
Strategist question #05  (ticket-driven, group: {{group_name}})

Reference: {{group_slug}}
Category: caching/state
Horizon: NearTerm
ROI: 4.4 (impact=6, confidence=0.78, effort=2)

Question:
Using the attached tickets as the primary context, identify state/artifacts that must be produced and preserved; schema/format expectations; stability/back-compat requirements.

Constraints: None
Non-goals: None

Answer format:
Use headings exactly: ### Direct answer / ### Risks and unknowns / ### Next experiment / ### Missing evidence.
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 06) ROI=4.2 impact=6 confidence=0.75 effort=2 horizon=NearTerm category=caching/state reference={{group_slug}}

# tickets attached directly (deterministic; self-contained)
mapfile -t __tickets < <(python3 - <<'PY'
from __future__ import annotations
from pathlib import Path

TICKET_ROOT = "{{ticket_root}}"
TICKET_GLOB = "{{ticket_glob}}"
TICKET_PATHS = "{{ticket_paths}}".strip()
MAX = int("{{ticket_max_files}}")

root = Path(TICKET_ROOT)

def lex_sorted(ps):
    return sorted((str(p) for p in ps), key=lambda s: s)

if TICKET_PATHS:
    tickets = [Path(p.strip()) for p in TICKET_PATHS.split(",") if p.strip()]
else:
    tickets = list(root.glob(TICKET_GLOB)) if root.exists() else []

tickets = [Path(p) for p in lex_sorted(tickets)]
if MAX and MAX > 0:
    tickets = tickets[:MAX]

for p in tickets:
    print(str(p))
PY
)

ticket_args=()
for p in "${__tickets[@]}"; do
  ticket_args+=(-f "$p")
done

if [ "${#ticket_args[@]}" -eq 0 ]; then
  echo "WARNING: no tickets resolved for group '{{group_name}}'." >&2
fi

# extra_files appended literally (may be empty; may include -f/--file):
{{oracle_cmd}}   {{oracle_flags}}   --write-output "{{out_dir}}/06-caching-state-cache-keys.md"   "${ticket_args[@]}"   {{extra_files}}   -p "$(cat <<'PROMPT'
Strategist question #06  (ticket-driven, group: {{group_name}})

Reference: {{group_slug}}
Category: caching/state
Horizon: NearTerm
ROI: 4.2 (impact=6, confidence=0.75, effort=2)

Question:
Using the attached tickets as the primary context, identify any caching opportunities/risks (discovery caches, pack outputs, oracle outputs); define cache keys, invalidation, and correctness risks.

Constraints: None
Non-goals: None

Answer format:
Use headings exactly: ### Direct answer / ### Risks and unknowns / ### Next experiment / ### Missing evidence.
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 07) ROI=4.3 impact=6 confidence=0.70 effort=2 horizon=MidTerm category=background jobs reference={{group_slug}}

# tickets attached directly (deterministic; self-contained)
mapfile -t __tickets < <(python3 - <<'PY'
from __future__ import annotations
from pathlib import Path

TICKET_ROOT = "{{ticket_root}}"
TICKET_GLOB = "{{ticket_glob}}"
TICKET_PATHS = "{{ticket_paths}}".strip()
MAX = int("{{ticket_max_files}}")

root = Path(TICKET_ROOT)

def lex_sorted(ps):
    return sorted((str(p) for p in ps), key=lambda s: s)

if TICKET_PATHS:
    tickets = [Path(p.strip()) for p in TICKET_PATHS.split(",") if p.strip()]
else:
    tickets = list(root.glob(TICKET_GLOB)) if root.exists() else []

tickets = [Path(p) for p in lex_sorted(tickets)]
if MAX and MAX > 0:
    tickets = tickets[:MAX]

for p in tickets:
    print(str(p))
PY
)

ticket_args=()
for p in "${__tickets[@]}"; do
  ticket_args+=(-f "$p")
done

if [ "${#ticket_args[@]}" -eq 0 ]; then
  echo "WARNING: no tickets resolved for group '{{group_name}}'." >&2
fi

# extra_files appended literally (may be empty; may include -f/--file):
{{oracle_cmd}}   {{oracle_flags}}   --write-output "{{out_dir}}/07-background-jobs-job-model.md"   "${ticket_args[@]}"   {{extra_files}}   -p "$(cat <<'PROMPT'
Strategist question #07  (ticket-driven, group: {{group_name}})

Reference: {{group_slug}}
Category: background jobs
Horizon: MidTerm
ROI: 4.3 (impact=6, confidence=0.70, effort=2)

Question:
Using the attached tickets as the primary context, identify any background/async work implied (jobs, queues, long-running operations); define responsibilities and interfaces.

Constraints: None
Non-goals: None

Answer format:
Use headings exactly: ### Direct answer / ### Risks and unknowns / ### Next experiment / ### Missing evidence.
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 08) ROI=4.0 impact=6 confidence=0.68 effort=3 horizon=MidTerm category=background jobs reference={{group_slug}}

# tickets attached directly (deterministic; self-contained)
mapfile -t __tickets < <(python3 - <<'PY'
from __future__ import annotations
from pathlib import Path

TICKET_ROOT = "{{ticket_root}}"
TICKET_GLOB = "{{ticket_glob}}"
TICKET_PATHS = "{{ticket_paths}}".strip()
MAX = int("{{ticket_max_files}}")

root = Path(TICKET_ROOT)

def lex_sorted(ps):
    return sorted((str(p) for p in ps), key=lambda s: s)

if TICKET_PATHS:
    tickets = [Path(p.strip()) for p in TICKET_PATHS.split(",") if p.strip()]
else:
    tickets = list(root.glob(TICKET_GLOB)) if root.exists() else []

tickets = [Path(p) for p in lex_sorted(tickets)]
if MAX and MAX > 0:
    tickets = tickets[:MAX]

for p in tickets:
    print(str(p))
PY
)

ticket_args=()
for p in "${__tickets[@]}"; do
  ticket_args+=(-f "$p")
done

if [ "${#ticket_args[@]}" -eq 0 ]; then
  echo "WARNING: no tickets resolved for group '{{group_name}}'." >&2
fi

# extra_files appended literally (may be empty; may include -f/--file):
{{oracle_cmd}}   {{oracle_flags}}   --write-output "{{out_dir}}/08-background-jobs-queue-failure.md"   "${ticket_args[@]}"   {{extra_files}}   -p "$(cat <<'PROMPT'
Strategist question #08  (ticket-driven, group: {{group_name}})

Reference: {{group_slug}}
Category: background jobs
Horizon: MidTerm
ROI: 4.0 (impact=6, confidence=0.68, effort=3)

Question:
Using the attached tickets as the primary context, define how background failures are handled (retries, idempotency, poison messages); define observability hooks.

Constraints: None
Non-goals: None

Answer format:
Use headings exactly: ### Direct answer / ### Risks and unknowns / ### Next experiment / ### Missing evidence.
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 09) ROI=4.7 impact=7 confidence=0.76 effort=1 horizon=Immediate category=observability reference={{group_slug}}

# tickets attached directly (deterministic; self-contained)
mapfile -t __tickets < <(python3 - <<'PY'
from __future__ import annotations
from pathlib import Path

TICKET_ROOT = "{{ticket_root}}"
TICKET_GLOB = "{{ticket_glob}}"
TICKET_PATHS = "{{ticket_paths}}".strip()
MAX = int("{{ticket_max_files}}")

root = Path(TICKET_ROOT)

def lex_sorted(ps):
    return sorted((str(p) for p in ps), key=lambda s: s)

if TICKET_PATHS:
    tickets = [Path(p.strip()) for p in TICKET_PATHS.split(",") if p.strip()]
else:
    tickets = list(root.glob(TICKET_GLOB)) if root.exists() else []

tickets = [Path(p) for p in lex_sorted(tickets)]
if MAX and MAX > 0:
    tickets = tickets[:MAX]

for p in tickets:
    print(str(p))
PY
)

ticket_args=()
for p in "${__tickets[@]}"; do
  ticket_args+=(-f "$p")
done

if [ "${#ticket_args[@]}" -eq 0 ]; then
  echo "WARNING: no tickets resolved for group '{{group_name}}'." >&2
fi

# extra_files appended literally (may be empty; may include -f/--file):
{{oracle_cmd}}   {{oracle_flags}}   --write-output "{{out_dir}}/09-observability-logging-metrics.md"   "${ticket_args[@]}"   {{extra_files}}   -p "$(cat <<'PROMPT'
Strategist question #09  (ticket-driven, group: {{group_name}})

Reference: {{group_slug}}
Category: observability
Horizon: Immediate
ROI: 4.7 (impact=7, confidence=0.76, effort=1)

Question:
Using the attached tickets as the primary context, define what logging/metrics must exist to debug pack generation + step execution; propose minimal instrumentation plan.

Constraints: None
Non-goals: None

Answer format:
Use headings exactly: ### Direct answer / ### Risks and unknowns / ### Next experiment / ### Missing evidence.
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 10) ROI=4.5 impact=7 confidence=0.74 effort=2 horizon=Immediate category=observability reference={{group_slug}}

# tickets attached directly (deterministic; self-contained)
mapfile -t __tickets < <(python3 - <<'PY'
from __future__ import annotations
from pathlib import Path

TICKET_ROOT = "{{ticket_root}}"
TICKET_GLOB = "{{ticket_glob}}"
TICKET_PATHS = "{{ticket_paths}}".strip()
MAX = int("{{ticket_max_files}}")

root = Path(TICKET_ROOT)

def lex_sorted(ps):
    return sorted((str(p) for p in ps), key=lambda s: s)

if TICKET_PATHS:
    tickets = [Path(p.strip()) for p in TICKET_PATHS.split(",") if p.strip()]
else:
    tickets = list(root.glob(TICKET_GLOB)) if root.exists() else []

tickets = [Path(p) for p in lex_sorted(tickets)]
if MAX and MAX > 0:
    tickets = tickets[:MAX]

for p in tickets:
    print(str(p))
PY
)

ticket_args=()
for p in "${__tickets[@]}"; do
  ticket_args+=(-f "$p")
done

if [ "${#ticket_args[@]}" -eq 0 ]; then
  echo "WARNING: no tickets resolved for group '{{group_name}}'." >&2
fi

# extra_files appended literally (may be empty; may include -f/--file):
{{oracle_cmd}}   {{oracle_flags}}   --write-output "{{out_dir}}/10-observability-tracing.md"   "${ticket_args[@]}"   {{extra_files}}   -p "$(cat <<'PROMPT'
Strategist question #10  (ticket-driven, group: {{group_name}})

Reference: {{group_slug}}
Category: observability
Horizon: Immediate
ROI: 4.5 (impact=7, confidence=0.74, effort=2)

Question:
Using the attached tickets as the primary context, define tracing/correlation strategy across pack steps and downstream tools; identify required IDs and propagation.

Constraints: None
Non-goals: None

Answer format:
Use headings exactly: ### Direct answer / ### Risks and unknowns / ### Next experiment / ### Missing evidence.
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 11) ROI=4.1 impact=6 confidence=0.70 effort=2 horizon=NearTerm category=permissions reference={{group_slug}}

# tickets attached directly (deterministic; self-contained)
mapfile -t __tickets < <(python3 - <<'PY'
from __future__ import annotations
from pathlib import Path

TICKET_ROOT = "{{ticket_root}}"
TICKET_GLOB = "{{ticket_glob}}"
TICKET_PATHS = "{{ticket_paths}}".strip()
MAX = int("{{ticket_max_files}}")

root = Path(TICKET_ROOT)

def lex_sorted(ps):
    return sorted((str(p) for p in ps), key=lambda s: s)

if TICKET_PATHS:
    tickets = [Path(p.strip()) for p in TICKET_PATHS.split(",") if p.strip()]
else:
    tickets = list(root.glob(TICKET_GLOB)) if root.exists() else []

tickets = [Path(p) for p in lex_sorted(tickets)]
if MAX and MAX > 0:
    tickets = tickets[:MAX]

for p in tickets:
    print(str(p))
PY
)

ticket_args=()
for p in "${__tickets[@]}"; do
  ticket_args+=(-f "$p")
done

if [ "${#ticket_args[@]}" -eq 0 ]; then
  echo "WARNING: no tickets resolved for group '{{group_name}}'." >&2
fi

# extra_files appended literally (may be empty; may include -f/--file):
{{oracle_cmd}}   {{oracle_flags}}   --write-output "{{out_dir}}/11-permissions-authz-gaps.md"   "${ticket_args[@]}"   {{extra_files}}   -p "$(cat <<'PROMPT'
Strategist question #11  (ticket-driven, group: {{group_name}})

Reference: {{group_slug}}
Category: permissions
Horizon: NearTerm
ROI: 4.1 (impact=6, confidence=0.70, effort=2)

Question:
Using the attached tickets as the primary context, identify permission/authz boundaries implied by tickets (file access, command execution, network); propose safe defaults.

Constraints: None
Non-goals: None

Answer format:
Use headings exactly: ### Direct answer / ### Risks and unknowns / ### Next experiment / ### Missing evidence.
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 12) ROI=3.9 impact=6 confidence=0.68 effort=2 horizon=NearTerm category=permissions reference={{group_slug}}

# tickets attached directly (deterministic; self-contained)
mapfile -t __tickets < <(python3 - <<'PY'
from __future__ import annotations
from pathlib import Path

TICKET_ROOT = "{{ticket_root}}"
TICKET_GLOB = "{{ticket_glob}}"
TICKET_PATHS = "{{ticket_paths}}".strip()
MAX = int("{{ticket_max_files}}")

root = Path(TICKET_ROOT)

def lex_sorted(ps):
    return sorted((str(p) for p in ps), key=lambda s: s)

if TICKET_PATHS:
    tickets = [Path(p.strip()) for p in TICKET_PATHS.split(",") if p.strip()]
else:
    tickets = list(root.glob(TICKET_GLOB)) if root.exists() else []

tickets = [Path(p) for p in lex_sorted(tickets)]
if MAX and MAX > 0:
    tickets = tickets[:MAX]

for p in tickets:
    print(str(p))
PY
)

ticket_args=()
for p in "${__tickets[@]}"; do
  ticket_args+=(-f "$p")
done

if [ "${#ticket_args[@]}" -eq 0 ]; then
  echo "WARNING: no tickets resolved for group '{{group_name}}'." >&2
fi

# extra_files appended literally (may be empty; may include -f/--file):
{{oracle_cmd}}   {{oracle_flags}}   --write-output "{{out_dir}}/12-permissions-secrets-config.md"   "${ticket_args[@]}"   {{extra_files}}   -p "$(cat <<'PROMPT'
Strategist question #12  (ticket-driven, group: {{group_name}})

Reference: {{group_slug}}
Category: permissions
Horizon: NearTerm
ROI: 3.9 (impact=6, confidence=0.68, effort=2)

Question:
Using the attached tickets as the primary context, identify secrets/config handling needs (API keys, tokens); propose secure config discovery and redaction.

Constraints: None
Non-goals: None

Answer format:
Use headings exactly: ### Direct answer / ### Risks and unknowns / ### Next experiment / ### Missing evidence.
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 13) ROI=3.8 impact=6 confidence=0.66 effort=3 horizon=MidTerm category=migrations reference={{group_slug}}

# tickets attached directly (deterministic; self-contained)
mapfile -t __tickets < <(python3 - <<'PY'
from __future__ import annotations
from pathlib import Path

TICKET_ROOT = "{{ticket_root}}"
TICKET_GLOB = "{{ticket_glob}}"
TICKET_PATHS = "{{ticket_paths}}".strip()
MAX = int("{{ticket_max_files}}")

root = Path(TICKET_ROOT)

def lex_sorted(ps):
    return sorted((str(p) for p in ps), key=lambda s: s)

if TICKET_PATHS:
    tickets = [Path(p.strip()) for p in TICKET_PATHS.split(",") if p.strip()]
else:
    tickets = list(root.glob(TICKET_GLOB)) if root.exists() else []

tickets = [Path(p) for p in lex_sorted(tickets)]
if MAX and MAX > 0:
    tickets = tickets[:MAX]

for p in tickets:
    print(str(p))
PY
)

ticket_args=()
for p in "${__tickets[@]}"; do
  ticket_args+=(-f "$p")
done

if [ "${#ticket_args[@]}" -eq 0 ]; then
  echo "WARNING: no tickets resolved for group '{{group_name}}'." >&2
fi

# extra_files appended literally (may be empty; may include -f/--file):
{{oracle_cmd}}   {{oracle_flags}}   --write-output "{{out_dir}}/13-migrations-schema-migrations.md"   "${ticket_args[@]}"   {{extra_files}}   -p "$(cat <<'PROMPT'
Strategist question #13  (ticket-driven, group: {{group_name}})

Reference: {{group_slug}}
Category: migrations
Horizon: MidTerm
ROI: 3.8 (impact=6, confidence=0.66, effort=3)

Question:
Using the attached tickets as the primary context, identify any required migrations (schema/format/CLI flags); define migration strategy and compat approach.

Constraints: None
Non-goals: None

Answer format:
Use headings exactly: ### Direct answer / ### Risks and unknowns / ### Next experiment / ### Missing evidence.
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 14) ROI=3.7 impact=6 confidence=0.64 effort=3 horizon=MidTerm category=migrations reference={{group_slug}}

# tickets attached directly (deterministic; self-contained)
mapfile -t __tickets < <(python3 - <<'PY'
from __future__ import annotations
from pathlib import Path

TICKET_ROOT = "{{ticket_root}}"
TICKET_GLOB = "{{ticket_glob}}"
TICKET_PATHS = "{{ticket_paths}}".strip()
MAX = int("{{ticket_max_files}}")

root = Path(TICKET_ROOT)

def lex_sorted(ps):
    return sorted((str(p) for p in ps), key=lambda s: s)

if TICKET_PATHS:
    tickets = [Path(p.strip()) for p in TICKET_PATHS.split(",") if p.strip()]
else:
    tickets = list(root.glob(TICKET_GLOB)) if root.exists() else []

tickets = [Path(p) for p in lex_sorted(tickets)]
if MAX and MAX > 0:
    tickets = tickets[:MAX]

for p in tickets:
    print(str(p))
PY
)

ticket_args=()
for p in "${__tickets[@]}"; do
  ticket_args+=(-f "$p")
done

if [ "${#ticket_args[@]}" -eq 0 ]; then
  echo "WARNING: no tickets resolved for group '{{group_name}}'." >&2
fi

# extra_files appended literally (may be empty; may include -f/--file):
{{oracle_cmd}}   {{oracle_flags}}   --write-output "{{out_dir}}/14-migrations-backfill-plan.md"   "${ticket_args[@]}"   {{extra_files}}   -p "$(cat <<'PROMPT'
Strategist question #14  (ticket-driven, group: {{group_name}})

Reference: {{group_slug}}
Category: migrations
Horizon: MidTerm
ROI: 3.7 (impact=6, confidence=0.64, effort=3)

Question:
Using the attached tickets as the primary context, define any needed backfill/one-time transforms; estimate risks; define verification plan.

Constraints: None
Non-goals: None

Answer format:
Use headings exactly: ### Direct answer / ### Risks and unknowns / ### Next experiment / ### Missing evidence.
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 15) ROI=4.6 impact=6 confidence=0.74 effort=1 horizon=Immediate category=UX flows reference={{group_slug}}

# tickets attached directly (deterministic; self-contained)
mapfile -t __tickets < <(python3 - <<'PY'
from __future__ import annotations
from pathlib import Path

TICKET_ROOT = "{{ticket_root}}"
TICKET_GLOB = "{{ticket_glob}}"
TICKET_PATHS = "{{ticket_paths}}".strip()
MAX = int("{{ticket_max_files}}")

root = Path(TICKET_ROOT)

def lex_sorted(ps):
    return sorted((str(p) for p in ps), key=lambda s: s)

if TICKET_PATHS:
    tickets = [Path(p.strip()) for p in TICKET_PATHS.split(",") if p.strip()]
else:
    tickets = list(root.glob(TICKET_GLOB)) if root.exists() else []

tickets = [Path(p) for p in lex_sorted(tickets)]
if MAX and MAX > 0:
    tickets = tickets[:MAX]

for p in tickets:
    print(str(p))
PY
)

ticket_args=()
for p in "${__tickets[@]}"; do
  ticket_args+=(-f "$p")
done

if [ "${#ticket_args[@]}" -eq 0 ]; then
  echo "WARNING: no tickets resolved for group '{{group_name}}'." >&2
fi

# extra_files appended literally (may be empty; may include -f/--file):
{{oracle_cmd}}   {{oracle_flags}}   --write-output "{{out_dir}}/15-ux-flows-user-journeys.md"   "${ticket_args[@]}"   {{extra_files}}   -p "$(cat <<'PROMPT'
Strategist question #15  (ticket-driven, group: {{group_name}})

Reference: {{group_slug}}
Category: UX flows
Horizon: Immediate
ROI: 4.6 (impact=6, confidence=0.74, effort=1)

Question:
Using the attached tickets as the primary context, identify UX/TUI workflows implied by tickets; define user journey states and expected outputs.

Constraints: None
Non-goals: None

Answer format:
Use headings exactly: ### Direct answer / ### Risks and unknowns / ### Next experiment / ### Missing evidence.
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 16) ROI=4.3 impact=6 confidence=0.72 effort=2 horizon=Immediate category=UX flows reference={{group_slug}}

# tickets attached directly (deterministic; self-contained)
mapfile -t __tickets < <(python3 - <<'PY'
from __future__ import annotations
from pathlib import Path

TICKET_ROOT = "{{ticket_root}}"
TICKET_GLOB = "{{ticket_glob}}"
TICKET_PATHS = "{{ticket_paths}}".strip()
MAX = int("{{ticket_max_files}}")

root = Path(TICKET_ROOT)

def lex_sorted(ps):
    return sorted((str(p) for p in ps), key=lambda s: s)

if TICKET_PATHS:
    tickets = [Path(p.strip()) for p in TICKET_PATHS.split(",") if p.strip()]
else:
    tickets = list(root.glob(TICKET_GLOB)) if root.exists() else []

tickets = [Path(p) for p in lex_sorted(tickets)]
if MAX and MAX > 0:
    tickets = tickets[:MAX]

for p in tickets:
    print(str(p))
PY
)

ticket_args=()
for p in "${__tickets[@]}"; do
  ticket_args+=(-f "$p")
done

if [ "${#ticket_args[@]}" -eq 0 ]; then
  echo "WARNING: no tickets resolved for group '{{group_name}}'." >&2
fi

# extra_files appended literally (may be empty; may include -f/--file):
{{oracle_cmd}}   {{oracle_flags}}   --write-output "{{out_dir}}/16-ux-flows-edge-cases.md"   "${ticket_args[@]}"   {{extra_files}}   -p "$(cat <<'PROMPT'
Strategist question #16  (ticket-driven, group: {{group_name}})

Reference: {{group_slug}}
Category: UX flows
Horizon: Immediate
ROI: 4.3 (impact=6, confidence=0.72, effort=2)

Question:
Using the attached tickets as the primary context, identify edge cases in UX flows (cancel, resume, partial runs); define minimal UX behavior.

Constraints: None
Non-goals: None

Answer format:
Use headings exactly: ### Direct answer / ### Risks and unknowns / ### Next experiment / ### Missing evidence.
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 17) ROI=4.9 impact=7 confidence=0.78 effort=1 horizon=Immediate category=failure modes reference={{group_slug}}

# tickets attached directly (deterministic; self-contained)
mapfile -t __tickets < <(python3 - <<'PY'
from __future__ import annotations
from pathlib import Path

TICKET_ROOT = "{{ticket_root}}"
TICKET_GLOB = "{{ticket_glob}}"
TICKET_PATHS = "{{ticket_paths}}".strip()
MAX = int("{{ticket_max_files}}")

root = Path(TICKET_ROOT)

def lex_sorted(ps):
    return sorted((str(p) for p in ps), key=lambda s: s)

if TICKET_PATHS:
    tickets = [Path(p.strip()) for p in TICKET_PATHS.split(",") if p.strip()]
else:
    tickets = list(root.glob(TICKET_GLOB)) if root.exists() else []

tickets = [Path(p) for p in lex_sorted(tickets)]
if MAX and MAX > 0:
    tickets = tickets[:MAX]

for p in tickets:
    print(str(p))
PY
)

ticket_args=()
for p in "${__tickets[@]}"; do
  ticket_args+=(-f "$p")
done

if [ "${#ticket_args[@]}" -eq 0 ]; then
  echo "WARNING: no tickets resolved for group '{{group_name}}'." >&2
fi

# extra_files appended literally (may be empty; may include -f/--file):
{{oracle_cmd}}   {{oracle_flags}}   --write-output "{{out_dir}}/17-failure-modes-timeouts-retries.md"   "${ticket_args[@]}"   {{extra_files}}   -p "$(cat <<'PROMPT'
Strategist question #17  (ticket-driven, group: {{group_name}})

Reference: {{group_slug}}
Category: failure modes
Horizon: Immediate
ROI: 4.9 (impact=7, confidence=0.78, effort=1)

Question:
Using the attached tickets as the primary context, define timeouts/retries behavior for external calls; define failure classification and operator actions.

Constraints: None
Non-goals: None

Answer format:
Use headings exactly: ### Direct answer / ### Risks and unknowns / ### Next experiment / ### Missing evidence.
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 18) ROI=4.4 impact=7 confidence=0.74 effort=2 horizon=Immediate category=failure modes reference={{group_slug}}

# tickets attached directly (deterministic; self-contained)
mapfile -t __tickets < <(python3 - <<'PY'
from __future__ import annotations
from pathlib import Path

TICKET_ROOT = "{{ticket_root}}"
TICKET_GLOB = "{{ticket_glob}}"
TICKET_PATHS = "{{ticket_paths}}".strip()
MAX = int("{{ticket_max_files}}")

root = Path(TICKET_ROOT)

def lex_sorted(ps):
    return sorted((str(p) for p in ps), key=lambda s: s)

if TICKET_PATHS:
    tickets = [Path(p.strip()) for p in TICKET_PATHS.split(",") if p.strip()]
else:
    tickets = list(root.glob(TICKET_GLOB)) if root.exists() else []

tickets = [Path(p) for p in lex_sorted(tickets)]
if MAX and MAX > 0:
    tickets = tickets[:MAX]

for p in tickets:
    print(str(p))
PY
)

ticket_args=()
for p in "${__tickets[@]}"; do
  ticket_args+=(-f "$p")
done

if [ "${#ticket_args[@]}" -eq 0 ]; then
  echo "WARNING: no tickets resolved for group '{{group_name}}'." >&2
fi

# extra_files appended literally (may be empty; may include -f/--file):
{{oracle_cmd}}   {{oracle_flags}}   --write-output "{{out_dir}}/18-failure-modes-rollback-plan.md"   "${ticket_args[@]}"   {{extra_files}}   -p "$(cat <<'PROMPT'
Strategist question #18  (ticket-driven, group: {{group_name}})

Reference: {{group_slug}}
Category: failure modes
Horizon: Immediate
ROI: 4.4 (impact=7, confidence=0.74, effort=2)

Question:
Using the attached tickets as the primary context, define rollback plan for partial runs and how to preserve artifacts; define 'safe to re-run' semantics.

Constraints: None
Non-goals: None

Answer format:
Use headings exactly: ### Direct answer / ### Risks and unknowns / ### Next experiment / ### Missing evidence.
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 19) ROI=4.0 impact=6 confidence=0.70 effort=2 horizon=NearTerm category=feature flags reference={{group_slug}}

# tickets attached directly (deterministic; self-contained)
mapfile -t __tickets < <(python3 - <<'PY'
from __future__ import annotations
from pathlib import Path

TICKET_ROOT = "{{ticket_root}}"
TICKET_GLOB = "{{ticket_glob}}"
TICKET_PATHS = "{{ticket_paths}}".strip()
MAX = int("{{ticket_max_files}}")

root = Path(TICKET_ROOT)

def lex_sorted(ps):
    return sorted((str(p) for p in ps), key=lambda s: s)

if TICKET_PATHS:
    tickets = [Path(p.strip()) for p in TICKET_PATHS.split(",") if p.strip()]
else:
    tickets = list(root.glob(TICKET_GLOB)) if root.exists() else []

tickets = [Path(p) for p in lex_sorted(tickets)]
if MAX and MAX > 0:
    tickets = tickets[:MAX]

for p in tickets:
    print(str(p))
PY
)

ticket_args=()
for p in "${__tickets[@]}"; do
  ticket_args+=(-f "$p")
done

if [ "${#ticket_args[@]}" -eq 0 ]; then
  echo "WARNING: no tickets resolved for group '{{group_name}}'." >&2
fi

# extra_files appended literally (may be empty; may include -f/--file):
{{oracle_cmd}}   {{oracle_flags}}   --write-output "{{out_dir}}/19-feature-flags-flag-plan.md"   "${ticket_args[@]}"   {{extra_files}}   -p "$(cat <<'PROMPT'
Strategist question #19  (ticket-driven, group: {{group_name}})

Reference: {{group_slug}}
Category: feature flags
Horizon: NearTerm
ROI: 4.0 (impact=6, confidence=0.70, effort=2)

Question:
Using the attached tickets as the primary context, define feature-flag strategy for rollout (scopes, defaults, telemetry); ensure compat for existing users.

Constraints: None
Non-goals: None

Answer format:
Use headings exactly: ### Direct answer / ### Risks and unknowns / ### Next experiment / ### Missing evidence.
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 20) ROI=3.8 impact=6 confidence=0.68 effort=2 horizon=NearTerm category=feature flags reference={{group_slug}}

# tickets attached directly (deterministic; self-contained)
mapfile -t __tickets < <(python3 - <<'PY'
from __future__ import annotations
from pathlib import Path

TICKET_ROOT = "{{ticket_root}}"
TICKET_GLOB = "{{ticket_glob}}"
TICKET_PATHS = "{{ticket_paths}}".strip()
MAX = int("{{ticket_max_files}}")

root = Path(TICKET_ROOT)

def lex_sorted(ps):
    return sorted((str(p) for p in ps), key=lambda s: s)

if TICKET_PATHS:
    tickets = [Path(p.strip()) for p in TICKET_PATHS.split(",") if p.strip()]
else:
    tickets = list(root.glob(TICKET_GLOB)) if root.exists() else []

tickets = [Path(p) for p in lex_sorted(tickets)]
if MAX and MAX > 0:
    tickets = tickets[:MAX]

for p in tickets:
    print(str(p))
PY
)

ticket_args=()
for p in "${__tickets[@]}"; do
  ticket_args+=(-f "$p")
done

if [ "${#ticket_args[@]}" -eq 0 ]; then
  echo "WARNING: no tickets resolved for group '{{group_name}}'." >&2
fi

# extra_files appended literally (may be empty; may include -f/--file):
{{oracle_cmd}}   {{oracle_flags}}   --write-output "{{out_dir}}/20-feature-flags-compat-rollout.md"   "${ticket_args[@]}"   {{extra_files}}   -p "$(cat <<'PROMPT'
Strategist question #20  (ticket-driven, group: {{group_name}})

Reference: {{group_slug}}
Category: feature flags
Horizon: NearTerm
ROI: 3.8 (impact=6, confidence=0.68, effort=2)

Question:
Using the attached tickets as the primary context, define minimal compat-safe rollout plan and guardrails; include fallback behavior and monitoring gates.

Constraints: None
Non-goals: None

Answer format:
Use headings exactly: ### Direct answer / ### Risks and unknowns / ### Next experiment / ### Missing evidence.
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

```

## Coverage check
- contracts/interfaces: OK
- invariants: OK
- caching/state: OK
- background jobs: OK
- observability: OK
- permissions: OK
- migrations: OK
- UX flows: OK
- failure modes: OK
- feature flags: OK


--- skills/oraclepack-tickets-pack-grouped/references/tickets-pack-template-bundle.md ---
# Oracle Pack — {{codebase_name}} (Tickets Stage 1)

## Parsed args
- codebase_name: {{codebase_name}}
- out_dir: {{out_dir}}
- oracle_cmd: {{oracle_cmd}}
- oracle_flags: {{oracle_flags}}
- extra_files: {{extra_files}}
- ticket_root: {{ticket_root}}
- ticket_glob: {{ticket_glob}}
- ticket_paths: {{ticket_paths}}
- ticket_bundle_path: {{ticket_bundle_path}}
- mode: {{mode}}

Notes (contract):
- Exactly one fenced `bash` block in this document.
- No other ``` fences anywhere.
- Exactly 20 steps, numbered 01..20 in order.
- Step header: `# NN) ROI=... impact=... confidence=... effort=... horizon=... category=... reference=...`
- Every step includes: `--write-output "{{out_dir}}/NN-<slug>.md"` (double quotes required).
- Steps must be self-contained and must not rely on shell variables created in previous steps.
- `## Coverage check` MUST be outside the bash fence (after the closing ```).

```bash
# Prelude (allowed inside the single bash fence)
# - Creates out_dir deterministically
# - Builds ticket_bundle_path deterministically from ticket_root/ticket_glob OR ticket_paths
# - Uses lexicographic ordering only (no mtime/timestamps)

set -euo pipefail

mkdir -p "{{out_dir}}"

python3 - <<'PY'
from __future__ import annotations

import sys
from pathlib import Path

CODEBASE_NAME = "{{codebase_name}}"
OUT_DIR = Path("{{out_dir}}")
TICKET_ROOT = Path("{{ticket_root}}")
TICKET_GLOB = "{{ticket_glob}}"
TICKET_PATHS_RAW = "{{ticket_paths}}".strip()
BUNDLE_PATH = Path("{{ticket_bundle_path}}")

def _read_text(p: Path) -> str:
    return p.read_text(encoding="utf-8", errors="replace")

def _title_from_md(text: str) -> str:
    for ln in text.splitlines():
        s = ln.strip()
        if s.startswith("# "):
            return s[2:].strip() or "Untitled"
    for ln in text.splitlines():
        s = ln.strip()
        if s:
            return s[:80]
    return "Untitled"

def _select_paths() -> list[Path]:
    if TICKET_PATHS_RAW:
        items = [Path(x.strip()) for x in TICKET_PATHS_RAW.split(",") if x.strip()]
        items = sorted(items, key=lambda p: str(p))
        return items

    if not TICKET_ROOT.exists():
        return []

    items = sorted(TICKET_ROOT.glob(TICKET_GLOB), key=lambda p: str(p))
    return items

paths = _select_paths()

BUNDLE_PATH.parent.mkdir(parents=True, exist_ok=True)

lines: list[str] = []
lines.append(f"# Tickets Bundle — {CODEBASE_NAME if CODEBASE_NAME else 'Unknown'}")
lines.append("")
lines.append("## Selection")
lines.append(f"- ticket_root: {TICKET_ROOT}")
lines.append(f"- ticket_glob: {TICKET_GLOB}")
lines.append(f"- ticket_paths: {TICKET_PATHS_RAW if TICKET_PATHS_RAW else '(none)'}")
lines.append("- ordering: lexicographic by path")
lines.append("")

if not paths:
    warn = (
        "## WARNING: No tickets found\n\n"
        "No ticket files were selected.\n\n"
        "What was attempted:\n"
        f"- ticket_root: {TICKET_ROOT}\n"
        f"- ticket_glob: {TICKET_GLOB}\n"
        f"- ticket_paths: {TICKET_PATHS_RAW if TICKET_PATHS_RAW else '(none)'}\n\n"
        "Next: provide explicit ticket_paths or create tickets under ticket_root.\n"
    )
    lines.append(warn)
    print(f"[WARN] No tickets selected; bundle will contain only WARNING.", file=sys.stderr)
else:
    lines.append("## Tickets")
    lines.append("")
    for p in paths:
        lines.append("---")
        lines.append(f"### {_title_from_md(_read_text(p))}")
        lines.append(f"- path: {p}")
        lines.append("")
        try:
            txt = _read_text(p)
        except Exception as e:
            lines.append(f"[ERROR reading file: {e}]")
            lines.append("")
            continue

        # Simple truncation policy: keep first 4000 chars if large.
        if len(txt) > 4000:
            lines.append(txt[:4000])
            lines.append("\n[... truncated ...]\n")
        else:
            lines.append(txt)

        lines.append("")

BUNDLE_PATH.write_text("\n".join(lines).rstrip() + "\n", encoding="utf-8")
print(f"[OK] Wrote ticket bundle: {BUNDLE_PATH}")
PY

# 01) ROI=8.0 impact=9 confidence=0.9 effort=1 horizon=Immediate category=contracts/interfaces reference=Unknown
{{oracle_cmd}} {{oracle_flags}} \
  --write-output "{{out_dir}}/01-contracts-interfaces-surface.md" \
  -f "{{ticket_bundle_path}}" {{extra_files}} \
  -p "$(cat <<'PROMPT'
Strategist question #01
Category: contracts/interfaces

Using the attached tickets bundle as the primary evidence, identify the primary public interface(s) implied by the tickets (CLI commands, APIs, file contracts, or user workflows).
For each interface:
- list key inputs/outputs
- list the exact files/modules likely defining it (if unknown, say Unknown)

Answer format:
1) Direct answer (1–6 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 02) ROI=7.8 impact=8 confidence=0.9 effort=1 horizon=Immediate category=contracts/interfaces reference=Unknown
{{oracle_cmd}} {{oracle_flags}} \
  --write-output "{{out_dir}}/02-contracts-interfaces-dependencies.md" \
  -f "{{ticket_bundle_path}}" {{extra_files}} \
  -p "$(cat <<'PROMPT'
Strategist question #02
Category: contracts/interfaces

From the attached tickets bundle, infer which external dependencies/services the system must integrate with (CLIs, APIs, SaaS, databases).
For each dependency:
- what contract is required (auth, endpoints, file formats)
- what configuration surface is implied

Answer format:
1) Direct answer (1–6 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 03) ROI=7.6 impact=8 confidence=0.85 effort=2 horizon=Immediate category=invariants reference=Unknown
{{oracle_cmd}} {{oracle_flags}} \
  --write-output "{{out_dir}}/03-invariants-must-always-hold.md" \
  -f "{{ticket_bundle_path}}" {{extra_files}} \
  -p "$(cat <<'PROMPT'
Strategist question #03
Category: invariants

Based on the attached tickets bundle, list the invariants that must always hold (data constraints, ordering constraints, security invariants, idempotency).
For each invariant:
- what breaks if violated
- where it should be enforced (layer/module; if unknown, Unknown)

Answer format:
1) Direct answer (1–6 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 04) ROI=7.2 impact=8 confidence=0.8 effort=2 horizon=Immediate category=invariants reference=Unknown
{{oracle_cmd}} {{oracle_flags}} \
  --write-output "{{out_dir}}/04-invariants-input-validation.md" \
  -f "{{ticket_bundle_path}}" {{extra_files}} \
  -p "$(cat <<'PROMPT'
Strategist question #04
Category: invariants

Using the attached tickets bundle, identify what inputs must be validated (CLI args, config fields, payloads, file paths).
For each input:
- validation rules implied
- failure message/behavior implied

Answer format:
1) Direct answer (1–6 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 05) ROI=7.0 impact=7 confidence=0.85 effort=2 horizon=Near category=caching/state reference=Unknown
{{oracle_cmd}} {{oracle_flags}} \
  --write-output "{{out_dir}}/05-caching-state-state-model.md" \
  -f "{{ticket_bundle_path}}" {{extra_files}} \
  -p "$(cat <<'PROMPT'
Strategist question #05
Category: caching/state

From the attached tickets bundle, infer what state must be persisted or cached (files, DB, in-memory, remote).
For each state item:
- read/write lifecycle
- consistency model implied
- failure recovery requirements

Answer format:
1) Direct answer (1–6 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 06) ROI=6.8 impact=7 confidence=0.8 effort=2 horizon=Near category=caching/state reference=Unknown
{{oracle_cmd}} {{oracle_flags}} \
  --write-output "{{out_dir}}/06-caching-state-cache-invalidation.md" \
  -f "{{ticket_bundle_path}}" {{extra_files}} \
  -p "$(cat <<'PROMPT'
Strategist question #06
Category: caching/state

Using the attached tickets bundle, identify caching risks: staleness, invalidation, keying, or race conditions implied by the tickets.
Propose a minimal caching strategy consistent with the tickets.

Answer format:
1) Direct answer (1–6 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 07) ROI=6.9 impact=8 confidence=0.75 effort=3 horizon=Near category=background jobs reference=Unknown
{{oracle_cmd}} {{oracle_flags}} \
  --write-output "{{out_dir}}/07-background-jobs-what-runs-async.md" \
  -f "{{ticket_bundle_path}}" {{extra_files}} \
  -p "$(cat <<'PROMPT'
Strategist question #07
Category: background jobs

From the attached tickets bundle, determine what work should run asynchronously/background (schedulers, queues, cron, long-running tasks).
For each job:
- trigger
- inputs/outputs
- retry/backoff requirements

Answer format:
1) Direct answer (1–6 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 08) ROI=6.6 impact=7 confidence=0.75 effort=3 horizon=Near category=background jobs reference=Unknown
{{oracle_cmd}} {{oracle_flags}} \
  --write-output "{{out_dir}}/08-background-jobs-idempotency.md" \
  -f "{{ticket_bundle_path}}" {{extra_files}} \
  -p "$(cat <<'PROMPT'
Strategist question #08
Category: background jobs

Using the attached tickets bundle, list the idempotency and concurrency constraints implied for background jobs.
Recommend minimal safeguards (dedupe keys, locks, at-least-once handling) aligned with tickets.

Answer format:
1) Direct answer (1–6 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 09) ROI=7.4 impact=8 confidence=0.8 effort=2 horizon=Immediate category=observability reference=Unknown
{{oracle_cmd}} {{oracle_flags}} \
  --write-output "{{out_dir}}/09-observability-logs-metrics-traces.md" \
  -f "{{ticket_bundle_path}}" {{extra_files}} \
  -p "$(cat <<'PROMPT'
Strategist question #09
Category: observability

From the attached tickets bundle, infer required observability: logs, metrics, traces, and user-visible diagnostics.
List:
- what to log/measure
- cardinality risks
- minimal dashboards/alerts implied

Answer format:
1) Direct answer (1–6 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 10) ROI=7.0 impact=7 confidence=0.8 effort=2 horizon=Near category=observability reference=Unknown
{{oracle_cmd}} {{oracle_flags}} \
  --write-output "{{out_dir}}/10-observability-error-taxonomy.md" \
  -f "{{ticket_bundle_path}}" {{extra_files}} \
  -p "$(cat <<'PROMPT'
Strategist question #10
Category: observability

Using the attached tickets bundle, define an error taxonomy consistent with ticket failure modes:
- user errors vs system errors
- retryable vs non-retryable
- how errors should surface (CLI exit codes, UI states, logs)

Answer format:
1) Direct answer (1–6 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 11) ROI=7.6 impact=9 confidence=0.75 effort=3 horizon=Immediate category=permissions reference=Unknown
{{oracle_cmd}} {{oracle_flags}} \
  --write-output "{{out_dir}}/11-permissions-authz-model.md" \
  -f "{{ticket_bundle_path}}" {{extra_files}} \
  -p "$(cat <<'PROMPT'
Strategist question #11
Category: permissions

From the attached tickets bundle, infer the permissions model (roles, capabilities, scopes).
List:
- what operations require permissions
- how permissions are granted/revoked
- audit requirements implied

Answer format:
1) Direct answer (1–6 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 12) ROI=7.0 impact=8 confidence=0.75 effort=3 horizon=Near category=permissions reference=Unknown
{{oracle_cmd}} {{oracle_flags}} \
  --write-output "{{out_dir}}/12-permissions-secret-handling.md" \
  -f "{{ticket_bundle_path}}" {{extra_files}} \
  -p "$(cat <<'PROMPT'
Strategist question #12
Category: permissions

Using the attached tickets bundle, identify sensitive data/secret handling needs.
Recommend:
- where secrets come from (env, files, vault)
- redaction rules
- least-privilege defaults

Answer format:
1) Direct answer (1–6 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 13) ROI=7.2 impact=8 confidence=0.8 effort=2 horizon=Near category=migrations reference=Unknown
{{oracle_cmd}} {{oracle_flags}} \
  --write-output "{{out_dir}}/13-migrations-data-changes.md" \
  -f "{{ticket_bundle_path}}" {{extra_files}} \
  -p "$(cat <<'PROMPT'
Strategist question #13
Category: migrations

From the attached tickets bundle, infer any data/schema/config migrations needed.
For each migration:
- trigger/versioning
- rollout plan
- rollback strategy

Answer format:
1) Direct answer (1–6 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 14) ROI=6.8 impact=7 confidence=0.8 effort=2 horizon=Near category=migrations reference=Unknown
{{oracle_cmd}} {{oracle_flags}} \
  --write-output "{{out_dir}}/14-migrations-compatibility.md" \
  -f "{{ticket_bundle_path}}" {{extra_files}} \
  -p "$(cat <<'PROMPT'
Strategist question #14
Category: migrations

Using the attached tickets bundle, identify backwards/forwards compatibility requirements during migration windows.
Recommend minimal compatibility shims or staged rollout steps.

Answer format:
1) Direct answer (1–6 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 15) ROI=7.4 impact=8 confidence=0.8 effort=2 horizon=Immediate category=UX flows reference=Unknown
{{oracle_cmd}} {{oracle_flags}} \
  --write-output "{{out_dir}}/15-ux-flows-primary-journeys.md" \
  -f "{{ticket_bundle_path}}" {{extra_files}} \
  -p "$(cat <<'PROMPT'
Strategist question #15
Category: UX flows

From the attached tickets bundle, map the primary user journeys implied by tickets.
For each journey:
- entry points
- steps/screens/commands
- success criteria and user feedback

Answer format:
1) Direct answer (1–6 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 16) ROI=6.9 impact=7 confidence=0.8 effort=2 horizon=Near category=UX flows reference=Unknown
{{oracle_cmd}} {{oracle_flags}} \
  --write-output "{{out_dir}}/16-ux-flows-edge-cases.md" \
  -f "{{ticket_bundle_path}}" {{extra_files}} \
  -p "$(cat <<'PROMPT'
Strategist question #16
Category: UX flows

Using the attached tickets bundle, list UX edge cases and failure UX:
- partial completion
- retries
- cancellation
- timeouts
- conflict resolution

Answer format:
1) Direct answer (1–6 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 17) ROI=7.8 impact=9 confidence=0.8 effort=2 horizon=Immediate category=failure modes reference=Unknown
{{oracle_cmd}} {{oracle_flags}} \
  --write-output "{{out_dir}}/17-failure-modes-top-risks.md" \
  -f "{{ticket_bundle_path}}" {{extra_files}} \
  -p "$(cat <<'PROMPT'
Strategist question #17
Category: failure modes

From the attached tickets bundle, enumerate the most likely failure modes.
For each failure mode:
- detection signal
- mitigation
- user-visible behavior

Answer format:
1) Direct answer (1–6 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 18) ROI=7.0 impact=8 confidence=0.75 effort=3 horizon=Near category=failure modes reference=Unknown
{{oracle_cmd}} {{oracle_flags}} \
  --write-output "{{out_dir}}/18-failure-modes-test-plan.md" \
  -f "{{ticket_bundle_path}}" {{extra_files}} \
  -p "$(cat <<'PROMPT'
Strategist question #18
Category: failure modes

Using the attached tickets bundle, propose a minimal test plan that covers the highest-risk failure modes.
Include:
- unit vs integration coverage split
- fixtures/mocks needed
- one smallest test to write first

Answer format:
1) Direct answer (1–6 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 19) ROI=7.3 impact=8 confidence=0.8 effort=2 horizon=Near category=feature flags reference=Unknown
{{oracle_cmd}} {{oracle_flags}} \
  --write-output "{{out_dir}}/19-feature-flags-needed.md" \
  -f "{{ticket_bundle_path}}" {{extra_files}} \
  -p "$(cat <<'PROMPT'
Strategist question #19
Category: feature flags

From the attached tickets bundle, infer where feature flags or staged rollouts are needed.
For each flag:
- what it gates
- default value
- sunset plan

Answer format:
1) Direct answer (1–6 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 20) ROI=6.8 impact=7 confidence=0.8 effort=2 horizon=Near category=feature flags reference=Unknown
{{oracle_cmd}} {{oracle_flags}} \
  --write-output "{{out_dir}}/20-feature-flags-observability.md" \
  -f "{{ticket_bundle_path}}" {{extra_files}} \
  -p "$(cat <<'PROMPT'
Strategist question #20
Category: feature flags

Using the attached tickets bundle, propose how to observe/validate a flagged rollout:
- success metrics
- rollback triggers
- logging/alert changes while enabled

Answer format:
1) Direct answer (1–6 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"
```

## Coverage check

*   contracts/interfaces: OK
*   invariants: OK
*   caching/state: OK
*   background jobs: OK
*   observability: OK
*   permissions: OK
*   migrations: OK
*   UX flows: OK
*   failure modes: OK
*   feature flags: OK

```

--- oraclepack-mcp-server/oraclepack_mcp_server/security.py ---
import os
from pathlib import Path
from typing import List, Optional
from .config import settings

class SecurityError(Exception):
    """Raised for security-related violations."""
    pass

def is_exec_enabled() -> bool:
    """Returns True if execution tools are explicitly enabled."""
    return settings.enable_exec

def validate_path(path: str | Path) -> Path:
    """
    Resolves a path and ensures it resides within one of the allowed roots.
    Returns the resolved Path if valid, otherwise raises SecurityError.
    """
    p = Path(path)
    # Always normalize the path to remove .. and other noise
    try:
        # resolve() is best but it follows symlinks and requires existence for full resolution on some platforms.
        # abspath + normpath is a good fallback for non-existent files.
        resolved_p = Path(os.path.abspath(os.path.normpath(p)))
    except Exception as e:
        raise SecurityError(f"Could not resolve path '{path}': {e}")

    # Check if resolved_p starts with any of the allowed roots
    is_allowed = False
    for root in settings.allowed_roots:
        try:
            resolved_root = Path(os.path.abspath(os.path.normpath(root)))
            # commonpath returns the common prefix. 
            # If resolved_p is under resolved_root, commonpath should be resolved_root.
            common = os.path.commonpath([str(resolved_root), str(resolved_p)])
            if common == str(resolved_root):
                is_allowed = True
                break
        except ValueError:
            # Different drives on Windows or other incompatibilities
            continue

    if not is_allowed:
        raise SecurityError(f"Access to path '{path}' is not allowed by ORACLEPACK_ALLOWED_ROOTS.")

    return resolved_p

def safe_read_file(path: str | Path) -> tuple[str, bool]:
    """
    Validates the path and reads its content up to max_read_bytes.
    Returns (content, truncated).
    """
    resolved_path = validate_path(path)
    
    if not resolved_path.exists():
        raise SecurityError(f"Path '{path}' does not exist.")
    if not resolved_path.is_file():
        raise SecurityError(f"Path '{path}' is not a file.")

    with open(resolved_path, "rb") as f:
        content_bytes = f.read(settings.max_read_bytes + 1)
        truncated = len(content_bytes) > settings.max_read_bytes
        content = content_bytes[:settings.max_read_bytes].decode("utf-8", errors="replace")
        return content, truncated

--- oraclepack-mcp-server/tests/test_security.py ---
import os
import pytest
from pathlib import Path
from oraclepack_mcp_server.security import validate_path, is_exec_enabled, SecurityError, safe_read_file
from oraclepack_mcp_server.config import Settings, settings

def test_is_exec_enabled(monkeypatch):
    # Test with default (False)
    monkeypatch.setattr(settings, "enable_exec", False)
    assert is_exec_enabled() is False
    
    # Test with True
    monkeypatch.setattr(settings, "enable_exec", True)
    assert is_exec_enabled() is True

def test_validate_path_allowed(tmp_path, monkeypatch):
    # Setup tmp_path as an allowed root
    monkeypatch.setattr(settings, "allowed_roots", [tmp_path])
    
    # Path inside allowed root
    test_file = tmp_path / "test.txt"
    test_file.touch()
    
    assert validate_path(test_file) == test_file.resolve()
    assert validate_path(str(test_file)) == test_file.resolve()

def test_validate_path_denied(tmp_path, monkeypatch):
    # Setup allowed root
    root1 = tmp_path / "root1"
    root1.mkdir()
    monkeypatch.setattr(settings, "allowed_roots", [root1])
    
    # Path outside allowed root
    outside_file = tmp_path / "outside.txt"
    outside_file.touch()
    
    with pytest.raises(SecurityError, match="not allowed"):
        validate_path(outside_file)

def test_validate_path_traversal(tmp_path, monkeypatch):
    root1 = tmp_path / "root1"
    root1.mkdir()
    monkeypatch.setattr(settings, "allowed_roots", [root1])
    
    # Try to traverse out
    traversal_path = root1 / ".." / "outside.txt"
    
    with pytest.raises(SecurityError, match="not allowed"):
        validate_path(traversal_path)

def test_safe_read_file(tmp_path, monkeypatch):
    monkeypatch.setattr(settings, "allowed_roots", [tmp_path])
    monkeypatch.setattr(settings, "max_read_bytes", 10)
    
    test_file = tmp_path / "large.txt"
    test_file.write_text("0123456789ABCDE") # 15 chars
    
    content, truncated = safe_read_file(test_file)
    assert content == "0123456789"
    assert truncated is True
    
    small_file = tmp_path / "small.txt"
    small_file.write_text("hello")
    content, truncated = safe_read_file(small_file)
    assert content == "hello"
    assert truncated is False


--- README.md ---
# oraclepack

<p align="center">
  <a href="https://github.com/acidicsoil/oraclepack/actions/workflows/ci.yml"><img alt="CI" src="https://github.com/acidicsoil/oraclepack/actions/workflows/ci.yml/badge.svg" /></a>
  <a href="https://github.com/acidicsoil/oraclepack/actions/workflows/release.yml"><img alt="Release" src="https://github.com/acidicsoil/oraclepack/actions/workflows/release.yml/badge.svg" /></a>
  <a href="https://github.com/acidicsoil/oraclepack/releases/latest"><img alt="Release Version" src="https://img.shields.io/github/v/release/acidicsoil/oraclepack?sort=semver" /></a>
  <a href="https://github.com/acidicsoil/oraclepack/blob/main/LICENSE"><img alt="License" src="https://img.shields.io/badge/license-MIT-green" /></a>
  <a href="https://github.com/acidicsoil/oraclepack/blob/main/go.mod"><img alt="Go Version" src="https://img.shields.io/github/go-mod/go-version/acidicsoil/oraclepack" /></a>
</p>

`oraclepack` is a polished, TUI-driven wrapper/runner for **Oracle Packs**—interactive bash workflows embedded in Markdown utilizing [oracle](https://github.com/steipete/oracle). It lets teams ship runbooks, audits, migrations, and LLM evaluation scripts as self-documenting `.md` files that can be validated, resumed, and executed with real-time feedback.

## 🎯 Project Scope & Purpose

Oraclepack is built to make **multi-step operational workflows** reproducible and safe:

- **Runbooks you can execute**: keep instructions and commands in one Markdown file.
- **LLM evaluation flows**: wrap `oracle` CLI calls in steps and validate them with dry runs.
- **Team-friendly automation**: share a pack as documentation, then execute it as a guided TUI.
- **Repeatable ops**: state + report files make it easy to resume or audit past runs.

## 🚀 Features

- **Interactive TUI**: browse steps, view live output, and manage execution with keyboard shortcuts.
- **Run All / Resume**: execute sequentially or continue from the last successful step.
- **Overrides Wizard**: select which steps receive extra official `oracle` flags and validate via dry-run.
- **Step Preview**: view a full step (no truncation), toggle wrap, and copy contents.
- **ROI Filtering**: include/exclude steps by ROI with over/under mode.
- **Project URL Management**: save ChatGPT project URLs globally or per project and pick quickly.
- **State + Report Files**: persistent `.state.json` and `.report.json` outputs for traceability.
- **Plain Mode**: run without TUI for CI or automated environments.
- **Markdown Native**: packs live in a single `.md` file with a bash block.

## 📦 Installation

### Install oracle

Install oracle and setup/configure. Get it [here!](https://github.com/steipete/oracle)

### Setting up for creating oracle-packs

### From Source

Ensure you have [Go](https://go.dev/) 1.24+ installed:

```bash
git clone https://github.com/user/oraclepack.git
cd oraclepack
go build -o oraclepack ./cmd/oraclepack
```

### Building for Windows (.exe)

#### On Windows (PowerShell/CMD)

```powershell
go build -o oraclepack.exe ./cmd/oraclepack
```

#### Cross-Compiling for Windows (from Linux/macOS)

```bash
GOOS=windows GOARCH=amd64 go build -o oraclepack.exe ./cmd/oraclepack
```

### Global Installation (Run from Anywhere)

To run `oraclepack` from any directory, move the binary to a location in your system's `PATH`.

#### Linux & macOS

```bash
# Move the binary to /usr/local/bin (requires sudo)
sudo mv oraclepack /usr/local/bin/

# OR install to your local bin (no sudo required)
mkdir -p ~/.local/bin
mv oraclepack ~/.local/bin/
# Note: Ensure ~/.local/bin is in your shell's PATH
```

#### Windows (PowerShell)

1. Create a directory for your tools (e.g., `C:\Tools`).
2. Move `oraclepack.exe` into that directory.
3. Add that directory to your PATH:

```powershell
$env:Path += ";C:\Tools"
[Environment]::SetEnvironmentVariable("Path", $env:Path, [EnvironmentVariableTarget]::User)
```

```bash
oraclepack completion bash >& oraclepack.completion.sh
mkdir -p ~/.local/share/bash-completion/completions
install -m 0644 oraclepack.completion.sh \
  ~/.local/share/bash-completion/completions/oraclepack

# reload shell
exec bash

```

## Fix: Git Bash wrapper with path-conversion disabled

Note: Git Bash may rewrite `/home/...` paths unless path-conversion is disabled. Use `MSYS_NO_PATHCONV=1` when invoking oraclepack from Git Bash/Windows.

### Run in Git Bash on Windows

```bash
cat > "$HOME/bin/oraclepack" <<'EOF'
#!/usr/bin/env bash
set -euo pipefail

# Git for Windows (Git Bash) path-conversion off for this exec call.
# Required so /home/... is not rewritten into C:/Program Files/Git/...
MSYS_NO_PATHCONV=1 exec wsl.exe -d Ubuntu-24.04 -u user -- /home/user/.local/bin/oraclepack "$@"
EOF

# ensure LF line endings + executable
sed -i 's/\r$//' "$HOME/bin/oraclepack"
chmod +x "$HOME/bin/oraclepack"
hash -r

```

#### WSL (Windows Subsystem for Linux)

Follow the **Linux** instructions above.

## 🛠 Usage

### 1. Run a Pack (Interactive TUI)

```bash
oraclepack run examples/setup-project.md
```

### 2. Run All Steps Sequentially

```bash
oraclepack run examples/setup-project.md --run-all
```

### 3. Resume a Previous Run

```bash
oraclepack run examples/setup-project.md --resume
```

### 4. Plain Mode (Non-Interactive)

```bash
oraclepack run examples/setup-project.md --no-tui
```

### 5. List Steps

```bash
oraclepack list examples/setup-project.md
```

### 6. Validate a Pack

```bash
oraclepack validate examples/setup-project.md
```

### 7. Verify Outputs (No Execution)

```bash
oraclepack verify-outputs examples/setup-project.md
```

## ⚙️ Execution Semantics

- Packs are executed as literal shell scripts via `bash -lc`, so your login shell config and PATH are respected.
- Supported tool prefixes in steps: `oracle`, `tm`, `task-master`, `codex`, `gemini`.
- For Codex automation, use non-interactive `codex exec` in pack steps.
- Artifact gates can validate expected outputs (missing tools are skipped; missing artifacts after a tool runs are treated as failures).

### Output verification (optional, disabled by default)

Enable with `--output-verify` or `ORACLEPACK_OUTPUT_VERIFY=true`.
Strict heading checks are controlled by `--output-require-headings` or `ORACLEPACK_OUTPUT_REQUIRE_HEADINGS=true`.
Chunk verification behavior is controlled by `--output-chunk-mode` or `ORACLEPACK_OUTPUT_CHUNK_MODE=auto|single|multi`.

### Environment variables

```env
ORACLEPACK_OUTPUT_VERIFY=false
ORACLEPACK_OUTPUT_RETRIES=0
ORACLEPACK_OUTPUT_REQUIRE_HEADINGS=false
ORACLEPACK_OUTPUT_CHUNK_MODE=auto
```

When strict headings are enabled, oraclepack checks for section headings in the output. The matcher is tolerant of common variants (e.g., missing `###`, “Risks/unknowns”, “Next smallest concrete experiment”).

- Required headings: `### Direct answer`, `### Risks and unknowns`, `### Next experiment`, `### Missing evidence`
- Direct-only variant: include `Answer format` plus `Return only: Direct answer` in the prompt; oraclepack will require only `### Direct answer`.
- Multi-output suffix scheme: if multiple `--write-output` paths are present, oraclepack expects per-file headings:
  - `-direct-answer` → `### Direct answer`
  - `-risks-unknowns` → `### Risks and unknowns`
  - `-next-experiment` → `### Next experiment`
  - `-missing-evidence` → `### Missing evidence`

### Chunk verification modes

- `auto` (default): if multiple `--write-output` paths exist, treat as chunked; otherwise single.
- `single`: always treat as a single output (first `--write-output` path).
- `multi`: force chunked validation when multiple outputs are present; otherwise fall back to single.

### Browser Mode Reliability

If your pack uses browser mode, consider increasing timeouts to reduce flaky steps:
- `--browser-timeout 30s` or `1m`
- `--browser-input-timeout 30s` or `1m`

### CLI Flags (run)

```bash
oraclepack run <pack.md> \
  --roi-threshold 2.0 \
  --roi-mode over \
  --run-all \
  --resume \
  --stop-on-fail=true \
  --output-verify \
  --output-retries 1 \
  --output-require-headings \
  --output-chunk-mode auto \
  --no-tui \
  --out-dir ./out
```

`oraclepack` expects the `oracle` CLI to be available on your PATH. Overrides let you append official `oracle` flags at runtime.

### CI Example

```yaml
- name: Validate pack
  run: oraclepack validate docs/oracle-pack.md
- name: Verify outputs
  run: oraclepack verify-outputs docs/oracle-pack.md
```

## ⌨️ TUI Controls (Core)

- `enter`: run selected step
- `a`: run all visible steps sequentially
- `f`: set ROI threshold
- `m`: toggle ROI mode (over/under)
- `v`: step preview (full view)
- `o`: overrides wizard (oracle flags + step targeting)
- `u`: edit ChatGPT project URL
- `U`: open saved URL picker (project/global)
- `q`: quit

### Step Preview Controls

- `b` / `esc`: back to list
- `t`: wrap/un-wrap preview
- `c`: copy step contents (falls back to temp file if clipboard fails)

## 🧭 Overrides Wizard

The overrides flow lets you:

- Select official `oracle` flags (e.g., `--files-report`, `--render`, `--render-plain`, `--copy`, `--wait`).
- Target which steps receive those flags.
- Validate the overridden commands with `oracle --dry-run summary` before running.

## 🔗 Project URL Management

Oraclepack can store ChatGPT project URLs in two scopes:

- **Project scope**: `your-pack.chatgpt-urls.json`
- **Global scope**: `~/.oraclepack/chatgpt-urls.json`

Use `U` to pick, add, edit, delete, or set a default URL.

## 📝 Oracle Pack Format

An Oracle Pack is a Markdown file containing exactly one `bash` code block. Steps are identified by a specific header pattern: `# NN)`.

````markdown
# Project Setup Pack

This pack sets up the development environment.

```bash
# Prelude: Variables defined here are available to all steps
out_dir="dist"

# 01) Initialize dependencies
npm install

# 02) Build the project
npm run build

# 03) Run oracle query (ROI=4.5)
oracle query "check-integrity"
```
````

### Rules

1. Steps must start with `# NN)` (e.g., `# 01)`, `# 02)`).
2. Step numbering must be sequential starting from `01`.
3. Packs must include **exactly 20 steps**.
4. The first bash code block in the file is the one executed.
5. Everything before the first `# 01)` is the **prelude**, which runs once.
6. Optional ROI tags (`ROI=2.5`) can be embedded in the step header for filtering.

## 📊 Reports and State

- **State File:** `[pack-name].state.json` tracks step statuses and filter settings.
- **Report File:** `[pack-name].report.json` includes timing, exit codes, and metadata for each step.

## 🤝 Contributing

1. Fork the repo
2. Create your feature branch (`git checkout -b feature/amazing-feature`)
3. Commit your changes (`git commit -m 'Add some amazing feature'`)
4. Push to the branch (`git push origin feature/amazing-feature`)
5. Open a Pull Request

## 📄 License

Distributed under the MIT License. See `LICENSE` for more information.


## Links discovered
- [oracle](https://github.com/steipete/oracle)
- [here!](https://github.com/steipete/oracle)
- [Go](https://go.dev/)
- [<img alt="CI" src="https://github.com/acidicsoil/oraclepack/actions/workflows/ci.yml/badge.svg" />](https://github.com/acidicsoil/oraclepack/actions/workflows/ci.yml)
- [<img alt="Release" src="https://github.com/acidicsoil/oraclepack/actions/workflows/release.yml/badge.svg" />](https://github.com/acidicsoil/oraclepack/actions/workflows/release.yml)
- [<img alt="Release Version" src="https://img.shields.io/github/v/release/acidicsoil/oraclepack?sort=semver" />](https://github.com/acidicsoil/oraclepack/releases/latest)
- [<img alt="License" src="https://img.shields.io/badge/license-MIT-green" />](https://github.com/acidicsoil/oraclepack/blob/main/LICENSE)
- [<img alt="Go Version" src="https://img.shields.io/github/go-mod/go-version/acidicsoil/oraclepack" />](https://github.com/acidicsoil/oraclepack/blob/main/go.mod)

--- internal/templates/ticket-action-pack.md ---
# Ticket Action Pack

```bash
out_dir=".oraclepack/ticketify"
--write-output

# 01)
echo "Build tickets index"

# 02)
echo "Generate actions json"

# 03)
echo "Generate tickets PRD"

# 04)
echo "Prep taskmaster inputs"

# 05)
task-master parse-prd .taskmaster/docs/prd.md

# 06)
task-master analyze-complexity --research

# 07)
task-master expand --all --research

# 08)
echo "Prepare headless automation"

# 09)
if command -v gemini >/dev/null 2>&1; then
  gemini run "Select next tasks" --write-output ".oraclepack/ticketify/next.json"
else
  echo "Skipped: gemini missing"
fi

# 10)
if command -v codex >/dev/null 2>&1; then
  codex exec "Implement tasks" --write-output ".oraclepack/ticketify/codex-implement.md"
else
  echo "Skipped: codex missing"
fi

# 11)
if command -v codex >/dev/null 2>&1; then
  codex exec "Verify changes" --write-output ".oraclepack/ticketify/codex-verify.md"
else
  echo "Skipped: codex missing"
fi

# 12)
if command -v gemini >/dev/null 2>&1; then
  gemini run "Review outputs" --write-output ".oraclepack/ticketify/gemini-review.json"
else
  echo "Skipped: gemini missing"
fi

# 13)
if command -v codex >/dev/null 2>&1; then
  codex exec "Prepare fixes" --write-output ".oraclepack/ticketify/codex-fixes.md"
else
  echo "Skipped: codex missing"
fi

# 14)
echo "Summarize results"

# 15)
echo "Prepare release notes"

# 16)
if command -v codex >/dev/null 2>&1; then
  codex exec "Draft PR description" --write-output ".oraclepack/ticketify/PR.md"
else
  echo "Skipped: codex missing"
fi

# 17)
echo "Finalize checklist"

# 18)
echo "Post-run cleanup"

# 19)
echo "Audit artifacts"

# 20)
echo "Done"
```


--- .ruler/AGENTS.md ---
# AGENTS.md

Centralised AI agent instructions. Add coding guidelines, style guides, and project context here.

Ruler concatenates all .md files in this directory (and subdirectories), starting with AGENTS.md (if present), then remaining files in sorted order.


--- .ruler/skill-usage.md ---
## Skills usage

You have a library of reusable skill prompts stored under `$CODEX_HOME/skills/` (commonly `~/.codex/skills/`).

Treat each **skill folder** in `$CODEX_HOME/skills/` as a named skill:

- A folder `$CODEX_HOME/skills/<SKILL_NAME>/SKILL.md` defines the canonical flow and constraints for the `<SKILL_NAME>` skill.
- Skill folders may also include `scripts/`, `references/`, and `assets/` that the assistant should use when the skill requires them.
- These skills are the primary reference for how to handle common or important task types.

General rule:

- Before starting work on any task, briefly classify it (for example: architecture, implementation, refactoring, performance, reliability, data, documentation, tests, tooling, pack generation, etc.).
- If there is a relevant skill under `$CODEX_HOME/skills/` for that class of task, base the approach on the instructions in that skill instead of inventing new, ad-hoc instructions.
- When a skill exists for a task type, follow its steps, constraints, and return format as the default behavior.

Task-type rule:

- When working on any task that corresponds to an existing skill:
  - Consult the corresponding `$CODEX_HOME/skills/<SKILL_NAME>/SKILL.md` as the first step.
  - Let the skill’s instructions drive the approach (checks to perform, constraints to respect, preferred output format).
  - Only add additional reasoning or deviations after the skill’s instructions have been applied.

Reporting rule:

- When following a skill, explicitly mention which skill is being used (for example: “Using the guidance from `$CODEX_HOME/skills/<SKILL_NAME>/SKILL.md`”) so the link between behavior and skill remains clear.
- Do not modify or overwrite skill files themselves unless explicitly instructed to adjust the underlying skill behavior.

---

## Oraclepack Stage-1 pack generation (grouped mini-packs)

When the user asks to generate **oraclepack Stage-1 question packs** (runner-ingestible Markdown packs with strict schema: single `bash` fence, exactly 20 steps, deterministic attachments, and a Coverage check), prefer these skills over ad-hoc prompting:

1) `$CODEX_HOME/skills/oraclepack-tickets-pack-grouped/SKILL.md`
   - Use when the task is **ticket-driven** (user references `.tickets/`, “tickets as primary context”, “group by ticket topic/domain”, or wants per-topic mini-packs from tickets).
   - Produces multiple per-topic/per-domain packs grouped by ticket subdirs + deterministic inference for loose tickets.
   - Packs must be direct-attach (no bundle dependency) and validated/linted per the skill.

2) `$CODEX_HOME/skills/oraclepack-codebase-pack-grouped/SKILL.md`
   - Use when the task is **codebase-driven** (user wants per-topic/per-domain mini-packs derived from repository structure/modules, and tickets are not the primary context).
   - Produces multiple per-topic/per-domain packs grouped by codebase subdirs + deterministic inference for loose files.
   - Packs must be direct-attach and validated/linted per the skill.

Selection decision (apply in order):

- If the user mentions tickets or `.tickets/` or provides “attached tickets” as primary context → use `oraclepack-tickets-pack-grouped`.
- Else → use `oraclepack-codebase-pack-grouped`.

Mandatory follow-through for either skill:

- Run the skill’s required validation and attachment linting for every generated pack.
- Enforce the skill’s size-control guidance (dry-run the largest pack and reduce scope if needed).
- Report the produced pack paths (e.g., `{{out_dir}}/packs/*.md`) and any group manifests the skill generates (e.g., `{{out_dir}}/_groups.json`), and explicitly state which skill drove the result.


--- .ruler/tm-AGENTS.md ---
# Agent Instructions

## Task Master AI and Workflow Instructions
**Import Task Master's development workflow commands and guidelines, treat as if import is in the main AGENT.md file.**
@./.taskmaster/AGENTS.md @./.rules/dev_workflow.md @./.rules/rules.md @./.rules/self_improve.md @./.rules/taskmaster.md


--- .rules/dev_workflow.md ---
---
description: Guide for using Taskmaster to manage task-driven development workflows
---

# Taskmaster Development Workflow

This guide outlines the standard process for using Taskmaster to manage software development projects. It is written as a set of instructions for you, the AI agent.

- **Your Default Stance**: For most projects, the user can work directly within the `master` task context. Your initial actions should operate on this default context unless a clear pattern for multi-context work emerges.
- **Your Goal**: Your role is to elevate the user's workflow by intelligently introducing advanced features like **Tagged Task Lists** when you detect the appropriate context. Do not force tags on the user; suggest them as a helpful solution to a specific need.

## The Basic Loop
The fundamental development cycle you will facilitate is:
1.  **`list`**: Show the user what needs to be done.
2.  **`next`**: Help the user decide what to work on.
3.  **`show <id>`**: Provide details for a specific task.
4.  **`expand <id>`**: Break down a complex task into smaller, manageable subtasks.
5.  **Implement**: The user writes the code and tests.
6.  **`update-subtask`**: Log progress and findings on behalf of the user.
7.  **`set-status`**: Mark tasks and subtasks as `done` as work is completed.
8.  **Repeat**.

All your standard command executions should operate on the user's current task context, which defaults to `master`.

---

## Standard Development Workflow Process

### Simple Workflow (Default Starting Point)

For new projects or when users are getting started, operate within the `master` tag context:

-   Start new projects by running `initialize_project` tool / `task-master init` or `parse_prd` / `task-master parse-prd --input='<prd-file.txt>'` (see @`taskmaster.md`) to generate initial tasks.json with tagged structure
-   Configure rule sets during initialization with `--rules` flag (e.g., `task-master init --rules <AGENT>,windsurf`) or manage them later with `task-master rules add/remove` commands
-   Begin coding sessions with `get_tasks` / `task-master list` (see @`taskmaster.md`) to see current tasks, status, and IDs
-   Determine the next task to work on using `next_task` / `task-master next` (see @`taskmaster.md`)
-   Analyze task complexity with `analyze_project_complexity` / `task-master analyze-complexity --research` (see @`taskmaster.md`) before breaking down tasks
-   Review complexity report using `complexity_report` / `task-master complexity-report` (see @`taskmaster.md`)
-   Select tasks based on dependencies (all marked 'done'), priority level, and ID order
-   View specific task details using `get_task` / `task-master show <id>` (see @`taskmaster.md`) to understand implementation requirements
-   Break down complex tasks using `expand_task` / `task-master expand --id=<id> --force --research` (see @`taskmaster.md`) with appropriate flags like `--force` (to replace existing subtasks) and `--research`
-   Implement code following task details, dependencies, and project standards
-   Mark completed tasks with `set_task_status` / `task-master set-status --id=<id> --status=done` (see @`taskmaster.md`)
-   Update dependent tasks when implementation differs from original plan using `update` / `task-master update --from=<id> --prompt="..."` or `update_task` / `task-master update-task --id=<id> --prompt="..."` (see @`taskmaster.md`)

---

## Leveling Up: Agent-Led Multi-Context Workflows

While the basic workflow is powerful, your primary opportunity to add value is by identifying when to introduce **Tagged Task Lists**. These patterns are your tools for creating a more organized and efficient development environment for the user, especially if you detect agentic or parallel development happening across the same session.

**Critical Principle**: Most users should never see a difference in their experience. Only introduce advanced workflows when you detect clear indicators that the project has evolved beyond simple task management.

### When to Introduce Tags: Your Decision Patterns

Here are the patterns to look for. When you detect one, you should propose the corresponding workflow to the user.

#### Pattern 1: Simple Git Feature Branching
This is the most common and direct use case for tags.

- **Trigger**: The user creates a new git branch (e.g., `git checkout -b feature/user-auth`).
- **Your Action**: Propose creating a new tag that mirrors the branch name to isolate the feature's tasks from `master`.
- **Your Suggested Prompt**: *"I see you've created a new branch named 'feature/user-auth'. To keep all related tasks neatly organized and separate from your main list, I can create a corresponding task tag for you. This helps prevent merge conflicts in your `tasks.json` file later. Shall I create the 'feature-user-auth' tag?"*
- **Tool to Use**: `task-master add-tag --from-branch`

#### Pattern 2: Team Collaboration
- **Trigger**: The user mentions working with teammates (e.g., "My teammate Alice is handling the database schema," or "I need to review Bob's work on the API.").
- **Your Action**: Suggest creating a separate tag for the user's work to prevent conflicts with shared master context.
- **Your Suggested Prompt**: *"Since you're working with Alice, I can create a separate task context for your work to avoid conflicts. This way, Alice can continue working with the master list while you have your own isolated context. When you're ready to merge your work, we can coordinate the tasks back to master. Shall I create a tag for your current work?"*
- **Tool to Use**: `task-master add-tag my-work --copy-from-current --description="My tasks while collaborating with Alice"`

#### Pattern 3: Experiments or Risky Refactors
- **Trigger**: The user wants to try something that might not be kept (e.g., "I want to experiment with switching our state management library," or "Let's refactor the old API module, but I want to keep the current tasks as a reference.").
- **Your Action**: Propose creating a sandboxed tag for the experimental work.
- **Your Suggested Prompt**: *"This sounds like a great experiment. To keep these new tasks separate from our main plan, I can create a temporary 'experiment-zustand' tag for this work. If we decide not to proceed, we can simply delete the tag without affecting the main task list. Sound good?"*
- **Tool to Use**: `task-master add-tag experiment-zustand --description="Exploring Zustand migration"`

#### Pattern 4: Large Feature Initiatives (PRD-Driven)
This is a more structured approach for significant new features or epics.

- **Trigger**: The user describes a large, multi-step feature that would benefit from a formal plan.
- **Your Action**: Propose a comprehensive, PRD-driven workflow.
- **Your Suggested Prompt**: *"This sounds like a significant new feature. To manage this effectively, I suggest we create a dedicated task context for it. Here's the plan: I'll create a new tag called 'feature-xyz', then we can draft a Product Requirements Document (PRD) together to scope the work. Once the PRD is ready, I'll automatically generate all the necessary tasks within that new tag. How does that sound?"*
- **Your Implementation Flow**:
    1.  **Create an empty tag**: `task-master add-tag feature-xyz --description "Tasks for the new XYZ feature"`. You can also start by creating a git branch if applicable, and then create the tag from that branch.
    2.  **Collaborate & Create PRD**: Work with the user to create a detailed PRD file (e.g., `.taskmaster/docs/feature-xyz-prd.txt`).
    3.  **Parse PRD into the new tag**: `task-master parse-prd .taskmaster/docs/feature-xyz-prd.txt --tag feature-xyz`
    4.  **Prepare the new task list**: Follow up by suggesting `analyze-complexity` and `expand-all` for the newly created tasks within the `feature-xyz` tag.

#### Pattern 5: Version-Based Development
Tailor your approach based on the project maturity indicated by tag names.

- **Prototype/MVP Tags** (`prototype`, `mvp`, `poc`, `v0.x`):
  - **Your Approach**: Focus on speed and functionality over perfection
  - **Task Generation**: Create tasks that emphasize "get it working" over "get it perfect"
  - **Complexity Level**: Lower complexity, fewer subtasks, more direct implementation paths
  - **Research Prompts**: Include context like "This is a prototype - prioritize speed and basic functionality over optimization"
  - **Example Prompt Addition**: *"Since this is for the MVP, I'll focus on tasks that get core functionality working quickly rather than over-engineering."*

- **Production/Mature Tags** (`v1.0+`, `production`, `stable`):
  - **Your Approach**: Emphasize robustness, testing, and maintainability
  - **Task Generation**: Include comprehensive error handling, testing, documentation, and optimization
  - **Complexity Level**: Higher complexity, more detailed subtasks, thorough implementation paths
  - **Research Prompts**: Include context like "This is for production - prioritize reliability, performance, and maintainability"
  - **Example Prompt Addition**: *"Since this is for production, I'll ensure tasks include proper error handling, testing, and documentation."*

### Advanced Workflow (Tag-Based & PRD-Driven)

**When to Transition**: Recognize when the project has evolved (or has initiated a project which existing code) beyond simple task management. Look for these indicators:
- User mentions teammates or collaboration needs
- Project has grown to 15+ tasks with mixed priorities
- User creates feature branches or mentions major initiatives
- User initializes Taskmaster on an existing, complex codebase
- User describes large features that would benefit from dedicated planning

**Your Role in Transition**: Guide the user to a more sophisticated workflow that leverages tags for organization and PRDs for comprehensive planning.

#### Master List Strategy (High-Value Focus)
Once you transition to tag-based workflows, the `master` tag should ideally contain only:
- **High-level deliverables** that provide significant business value
- **Major milestones** and epic-level features
- **Critical infrastructure** work that affects the entire project
- **Release-blocking** items

**What NOT to put in master**:
- Detailed implementation subtasks (these go in feature-specific tags' parent tasks)
- Refactoring work (create dedicated tags like `refactor-auth`)
- Experimental features (use `experiment-*` tags)
- Team member-specific tasks (use person-specific tags)

#### PRD-Driven Feature Development

**For New Major Features**:
1. **Identify the Initiative**: When user describes a significant feature
2. **Create Dedicated Tag**: `add_tag feature-[name] --description="[Feature description]"`
3. **Collaborative PRD Creation**: Work with user to create comprehensive PRD in `.taskmaster/docs/feature-[name]-prd.txt`
4. **Parse & Prepare**:
   - `parse_prd .taskmaster/docs/feature-[name]-prd.txt --tag=feature-[name]`
   - `analyze_project_complexity --tag=feature-[name] --research`
   - `expand_all --tag=feature-[name] --research`
5. **Add Master Reference**: Create a high-level task in `master` that references the feature tag

**For Existing Codebase Analysis**:
When users initialize Taskmaster on existing projects:
1. **Codebase Discovery**: Use your native tools for producing deep context about the code base. You may use `research` tool with `--tree` and `--files` to collect up to date information using the existing architecture as context.
2. **Collaborative Assessment**: Work with user to identify improvement areas, technical debt, or new features
3. **Strategic PRD Creation**: Co-author PRDs that include:
   - Current state analysis (based on your codebase research)
   - Proposed improvements or new features
   - Implementation strategy considering existing code
4. **Tag-Based Organization**: Parse PRDs into appropriate tags (`refactor-api`, `feature-dashboard`, `tech-debt`, etc.)
5. **Master List Curation**: Keep only the most valuable initiatives in master

The parse-prd's `--append` flag enables the user to parse multiple PRDs within tags or across tags. PRDs should be focused and the number of tasks they are parsed into should be strategically chosen relative to the PRD's complexity and level of detail.

### Workflow Transition Examples

**Example 1: Simple → Team-Based**
```
User: "Alice is going to help with the API work"
Your Response: "Great! To avoid conflicts, I'll create a separate task context for your work. Alice can continue with the master list while you work in your own context. When you're ready to merge, we can coordinate the tasks back together."
Action: add_tag my-api-work --copy-from-current --description="My API tasks while collaborating with Alice"
```

**Example 2: Simple → PRD-Driven**
```
User: "I want to add a complete user dashboard with analytics, user management, and reporting"
Your Response: "This sounds like a major feature that would benefit from detailed planning. Let me create a dedicated context for this work and we can draft a PRD together to ensure we capture all requirements."
Actions:
1. add_tag feature-dashboard --description="User dashboard with analytics and management"
2. Collaborate on PRD creation
3. parse_prd dashboard-prd.txt --tag=feature-dashboard
4. Add high-level "User Dashboard" task to master
```

**Example 3: Existing Project → Strategic Planning**
```
User: "I just initialized Taskmaster on my existing React app. It's getting messy and I want to improve it."
Your Response: "Let me research your codebase to understand the current architecture, then we can create a strategic plan for improvements."
Actions:
1. research "Current React app architecture and improvement opportunities" --tree --files=src/
2. Collaborate on improvement PRD based on findings
3. Create tags for different improvement areas (refactor-components, improve-state-management, etc.)
4. Keep only major improvement initiatives in master
```

---

## Primary Interaction: MCP Server vs. CLI

Taskmaster offers two primary ways to interact:

1.  **MCP Server (Recommended for Integrated Tools)**:
    - For AI agents and integrated development environments (like <AGENT>), interacting via the **MCP server is the preferred method**.
    - The MCP server exposes Taskmaster functionality through a set of tools (e.g., `get_tasks`, `add_subtask`).
    - This method offers better performance, structured data exchange, and richer error handling compared to CLI parsing.
    - Refer to @`mcp.md` for details on the MCP architecture and available tools.
    - A comprehensive list and description of MCP tools and their corresponding CLI commands can be found in @`taskmaster.md`.
    - **Restart the MCP server** if core logic in `scripts/modules` or MCP tool/direct function definitions change.
    - **Note**: MCP tools fully support tagged task lists with complete tag management capabilities.

2.  **`task-master` CLI (For Users & Fallback)**:
    - The global `task-master` command provides a user-friendly interface for direct terminal interaction.
    - It can also serve as a fallback if the MCP server is inaccessible or a specific function isn't exposed via MCP.
    - Install globally with `npm install -g task-master-ai` or use locally via `npx task-master-ai ...`.
    - The CLI commands often mirror the MCP tools (e.g., `task-master list` corresponds to `get_tasks`).
    - Refer to @`taskmaster.md` for a detailed command reference.
    - **Tagged Task Lists**: CLI fully supports the new tagged system with seamless migration.

## How the Tag System Works (For Your Reference)

- **Data Structure**: Tasks are organized into separate contexts (tags) like "master", "feature-branch", or "v2.0".
- **Silent Migration**: Existing projects automatically migrate to use a "master" tag with zero disruption.
- **Context Isolation**: Tasks in different tags are completely separate. Changes in one tag do not affect any other tag.
- **Manual Control**: The user is always in control. There is no automatic switching. You facilitate switching by using `use-tag <name>`.
- **Full CLI & MCP Support**: All tag management commands are available through both the CLI and MCP tools for you to use. Refer to @`taskmaster.md` for a full command list.

---

## Task Complexity Analysis

-   Run `analyze_project_complexity` / `task-master analyze-complexity --research` (see @`taskmaster.md`) for comprehensive analysis
-   Review complexity report via `complexity_report` / `task-master complexity-report` (see @`taskmaster.md`) for a formatted, readable version.
-   Focus on tasks with highest complexity scores (8-10) for detailed breakdown
-   Use analysis results to determine appropriate subtask allocation
-   Note that reports are automatically used by the `expand_task` tool/command

## Task Breakdown Process

-   Use `expand_task` / `task-master expand --id=<id>`. It automatically uses the complexity report if found, otherwise generates default number of subtasks.
-   Use `--num=<number>` to specify an explicit number of subtasks, overriding defaults or complexity report recommendations.
-   Add `--research` flag to leverage Perplexity AI for research-backed expansion.
-   Add `--force` flag to clear existing subtasks before generating new ones (default is to append).
-   Use `--prompt="<context>"` to provide additional context when needed.
-   Review and adjust generated subtasks as necessary.
-   Use `expand_all` tool or `task-master expand --all` to expand multiple pending tasks at once, respecting flags like `--force` and `--research`.
-   If subtasks need complete replacement (regardless of the `--force` flag on `expand`), clear them first with `clear_subtasks` / `task-master clear-subtasks --id=<id>`.

## Implementation Drift Handling

-   When implementation differs significantly from planned approach
-   When future tasks need modification due to current implementation choices
-   When new dependencies or requirements emerge
-   Use `update` / `task-master update --from=<futureTaskId> --prompt='<explanation>\nUpdate context...' --research` to update multiple future tasks.
-   Use `update_task` / `task-master update-task --id=<taskId> --prompt='<explanation>\nUpdate context...' --research` to update a single specific task.

## Task Status Management

-   Use 'pending' for tasks ready to be worked on
-   Use 'done' for completed and verified tasks
-   Use 'deferred' for postponed tasks
-   Add custom status values as needed for project-specific workflows

## Task Structure Fields

- **id**: Unique identifier for the task (Example: `1`, `1.1`)
- **title**: Brief, descriptive title (Example: `"Initialize Repo"`)
- **description**: Concise summary of what the task involves (Example: `"Create a new repository, set up initial structure."`)
- **status**: Current state of the task (Example: `"pending"`, `"done"`, `"deferred"`)
- **dependencies**: IDs of prerequisite tasks (Example: `[1, 2.1]`)
    - Dependencies are displayed with status indicators (✅ for completed, ⏱️ for pending)
    - This helps quickly identify which prerequisite tasks are blocking work
- **priority**: Importance level (Example: `"high"`, `"medium"`, `"low"`)
- **details**: In-depth implementation instructions (Example: `"Use GitHub client ID/secret, handle callback, set session token."`)
- **testStrategy**: Verification approach (Example: `"Deploy and call endpoint to confirm 'Hello World' response."`)
- **subtasks**: List of smaller, more specific tasks (Example: `[{"id": 1, "title": "Configure OAuth", ...}]`)
- Refer to task structure details (previously linked to `tasks.md`).

## Configuration Management (Updated)

Taskmaster configuration is managed through two main mechanisms:

1.  **`.taskmaster/config.json` File (Primary):**
    *   Located in the project root directory.
    *   Stores most configuration settings: AI model selections (main, research, fallback), parameters (max tokens, temperature), logging level, default subtasks/priority, project name, etc.
    *   **Tagged System Settings**: Includes `global.defaultTag` (defaults to "master") and `tags` section for tag management configuration.
    *   **Managed via `task-master models --setup` command.** Do not edit manually unless you know what you are doing.
    *   **View/Set specific models via `task-master models` command or `models` MCP tool.**
    *   Created automatically when you run `task-master models --setup` for the first time or during tagged system migration.

2.  **Environment Variables (`.env` / `mcp.json`):**
    *   Used **only** for sensitive API keys and specific endpoint URLs.
    *   Place API keys (one per provider) in a `.env` file in the project root for CLI usage.
    *   For MCP/<AGENT> integration, configure these keys in the `env` section of `.<AGENT>/mcp.json`.
    *   Available keys/variables: See `assets/env.example` or the Configuration section in the command reference (previously linked to `taskmaster.md`).

3.  **`.taskmaster/state.json` File (Tagged System State):**
    *   Tracks current tag context and migration status.
    *   Automatically created during tagged system migration.
    *   Contains: `currentTag`, `lastSwitched`, `migrationNoticeShown`.

**Important:** Non-API key settings (like model selections, `MAX_TOKENS`, `TASKMASTER_LOG_LEVEL`) are **no longer configured via environment variables**. Use the `task-master models` command (or `--setup` for interactive configuration) or the `models` MCP tool.
**If AI commands FAIL in MCP** verify that the API key for the selected provider is present in the `env` section of `.<AGENT>/mcp.json`.
**If AI commands FAIL in CLI** verify that the API key for the selected provider is present in the `.env` file in the root of the project.

## Rules Management

Taskmaster supports multiple AI coding assistant rule sets that can be configured during project initialization or managed afterward:

- **Available Profiles**: Claude Code, <AGENT>, Codex, <AGENT>, Roo Code, Trae, Windsurf (claude, <AGENT>, codex, <AGENT>, roo, trae, windsurf)
- **During Initialization**: Use `task-master init --rules <AGENT>,windsurf` to specify which rule sets to include
- **After Initialization**: Use `task-master rules add <profiles>` or `task-master rules remove <profiles>` to manage rule sets
- **Interactive Setup**: Use `task-master rules setup` to launch an interactive prompt for selecting rule profiles
- **Default Behavior**: If no `--rules` flag is specified during initialization, all available rule profiles are included
- **Rule Structure**: Each profile creates its own directory (e.g., `.<AGENT>/rules`, `.roo/rules`) with appropriate configuration files

## Determining the Next Task

- Run `next_task` / `task-master next` to show the next task to work on.
- The command identifies tasks with all dependencies satisfied
- Tasks are prioritized by priority level, dependency count, and ID
- The command shows comprehensive task information including:
    - Basic task details and description
    - Implementation details
    - Subtasks (if they exist)
    - Contextual suggested actions
- Recommended before starting any new development work
- Respects your project's dependency structure
- Ensures tasks are completed in the appropriate sequence
- Provides ready-to-use commands for common task actions

## Viewing Specific Task Details

- Run `get_task` / `task-master show <id>` to view a specific task.
- Use dot notation for subtasks: `task-master show 1.2` (shows subtask 2 of task 1)
- Displays comprehensive information similar to the next command, but for a specific task
- For parent tasks, shows all subtasks and their current status
- For subtasks, shows parent task information and relationship
- Provides contextual suggested actions appropriate for the specific task
- Useful for examining task details before implementation or checking status

## Managing Task Dependencies

- Use `add_dependency` / `task-master add-dependency --id=<id> --depends-on=<id>` to add a dependency.
- Use `remove_dependency` / `task-master remove-dependency --id=<id> --depends-on=<id>` to remove a dependency.
- The system prevents circular dependencies and duplicate dependency entries
- Dependencies are checked for existence before being added or removed
- Task files are automatically regenerated after dependency changes
- Dependencies are visualized with status indicators in task listings and files

## Task Reorganization

- Use `move_task` / `task-master move --from=<id> --to=<id>` to move tasks or subtasks within the hierarchy
- This command supports several use cases:
  - Moving a standalone task to become a subtask (e.g., `--from=5 --to=7`)
  - Moving a subtask to become a standalone task (e.g., `--from=5.2 --to=7`)
  - Moving a subtask to a different parent (e.g., `--from=5.2 --to=7.3`)
  - Reordering subtasks within the same parent (e.g., `--from=5.2 --to=5.4`)
  - Moving a task to a new, non-existent ID position (e.g., `--from=5 --to=25`)
  - Moving multiple tasks at once using comma-separated IDs (e.g., `--from=10,11,12 --to=16,17,18`)
- The system includes validation to prevent data loss:
  - Allows moving to non-existent IDs by creating placeholder tasks
  - Prevents moving to existing task IDs that have content (to avoid overwriting)
  - Validates source tasks exist before attempting to move them
- The system maintains proper parent-child relationships and dependency integrity
- Task files are automatically regenerated after the move operation
- This provides greater flexibility in organizing and refining your task structure as project understanding evolves
- This is especially useful when dealing with potential merge conflicts arising from teams creating tasks on separate branches. Solve these conflicts very easily by moving your tasks and keeping theirs.

## Iterative Subtask Implementation

Once a task has been broken down into subtasks using `expand_task` or similar methods, follow this iterative process for implementation:

1.  **Understand the Goal (Preparation):**
    *   Use `get_task` / `task-master show <subtaskId>` (see @`taskmaster.md`) to thoroughly understand the specific goals and requirements of the subtask.

2.  **Initial Exploration & Planning (Iteration 1):**
    *   This is the first attempt at creating a concrete implementation plan.
    *   Explore the codebase to identify the precise files, functions, and even specific lines of code that will need modification.
    *   Determine the intended code changes (diffs) and their locations.
    *   Gather *all* relevant details from this exploration phase.

3.  **Log the Plan:**
    *   Run `update_subtask` / `task-master update-subtask --id=<subtaskId> --prompt='<detailed plan>'`.
    *   Provide the *complete and detailed* findings from the exploration phase in the prompt. Include file paths, line numbers, proposed diffs, reasoning, and any potential challenges identified. Do not omit details. The goal is to create a rich, timestamped log within the subtask's `details`.

4.  **Verify the Plan:**
    *   Run `get_task` / `task-master show <subtaskId>` again to confirm that the detailed implementation plan has been successfully appended to the subtask's details.

5.  **Begin Implementation:**
    *   Set the subtask status using `set_task_status` / `task-master set-status --id=<subtaskId> --status=in-progress`.
    *   Start coding based on the logged plan.

6.  **Refine and Log Progress (Iteration 2+):**
    *   As implementation progresses, you will encounter challenges, discover nuances, or confirm successful approaches.
    *   **Before appending new information**: Briefly review the *existing* details logged in the subtask (using `get_task` or recalling from context) to ensure the update adds fresh insights and avoids redundancy.
    *   **Regularly** use `update_subtask` / `task-master update-subtask --id=<subtaskId> --prompt='<update details>\n- What worked...\n- What didn't work...'` to append new findings.
    *   **Crucially, log:**
        *   What worked ("fundamental truths" discovered).
        *   What didn't work and why (to avoid repeating mistakes).
        *   Specific code snippets or configurations that were successful.
        *   Decisions made, especially if confirmed with user input.
        *   Any deviations from the initial plan and the reasoning.
    *   The objective is to continuously enrich the subtask's details, creating a log of the implementation journey that helps the AI (and human developers) learn, adapt, and avoid repeating errors.

7.  **Review & Update Rules (Post-Implementation):**
    *   Once the implementation for the subtask is functionally complete, review all code changes and the relevant chat history.
    *   Identify any new or modified code patterns, conventions, or best practices established during the implementation.
    *   Create new or update existing rules following internal guidelines (previously linked to `cursor_rules.md` and `self_improve.md`).

8.  **Mark Task Complete:**
    *   After verifying the implementation and updating any necessary rules, mark the subtask as completed: `set_task_status` / `task-master set-status --id=<subtaskId> --status=done`.

9.  **Commit Changes (If using Git):**
    *   Stage the relevant code changes and any updated/new rule files (`git add .`).
    *   Craft a comprehensive Git commit message summarizing the work done for the subtask, including both code implementation and any rule adjustments.
    *   Execute the commit command directly in the terminal (e.g., `git commit -m 'feat(module): Implement feature X for subtask <subtaskId>\n\n- Details about changes...\n- Updated rule Y for pattern Z'`).
    *   Consider if a Changeset is needed according to internal versioning guidelines (previously linked to `changeset.md`). If so, run `npm run changeset`, stage the generated file, and amend the commit or create a new one.

10. **Proceed to Next Subtask:**
    *   Identify the next subtask (e.g., using `next_task` / `task-master next`).

## Code Analysis & Refactoring Techniques

- **Top-Level Function Search**:
    - Useful for understanding module structure or planning refactors.
    - Use grep/ripgrep to find exported functions/constants:
      `rg "export (async function|function|const) \w+"` or similar patterns.
    - Can help compare functions between files during migrations or identify potential naming conflicts.

---
*This workflow provides a general guideline. Adapt it based on your specific project needs and team practices.*

--- .rules/rules.md ---
---
description: Guidelines for creating and maintaining AGENT rules to ensure consistency and effectiveness.
---

- **Required Rule Structure:**
  ```markdown
  ---
  description: Clear, one-line description of what the rule enforces
  ---

  - **Main Points in Bold**
    - Sub-points with details
    - Examples and explanations
  ```

- **File References:**
  - Use `[filename](md:path/to/file)` ([filename](md:filename)) to reference files
  - Example: [prisma.md](.ruler/prisma.md) for rule references
  - Example: [schema.prisma](md:prisma/schema.prisma) for code references

- **Code Examples:**
  - Use language-specific code blocks
  ```typescript
  // ✅ DO: Show good examples
  const goodExample = true;

  // ❌ DON'T: Show anti-patterns
  const badExample = false;
  ```

- **Rule Content Guidelines:**
  - Start with high-level overview
  - Include specific, actionable requirements
  - Show examples of correct implementation
  - Reference existing code when possible
  - Keep rules DRY by referencing other rules

- **Rule Maintenance:**
  - Update rules when new patterns emerge
  - Add examples from actual codebase
  - Remove outdated patterns
  - Cross-reference related rules

- **Best Practices:**
  - Use bullet points for clarity
  - Keep descriptions concise
  - Include both DO and DON'T examples
  - Reference actual code over theoretical examples
  - Use consistent formatting across rules

## Links discovered
- [filename](https://github.com/AcidicSoil/oraclepack/blob/main/.rules/md:path/to/file.md)
- [filename](https://github.com/AcidicSoil/oraclepack/blob/main/.rules/md:filename.md)
- [prisma.md](https://github.com/AcidicSoil/oraclepack/blob/main/.rules/.ruler/prisma.md)
- [schema.prisma](https://github.com/AcidicSoil/oraclepack/blob/main/.rules/md:prisma/schema.prisma)

--- .rules/self_improve.md ---
---
description: Guidelines for continuously improving  rules based on emerging code patterns and best practices.
---

- **Rule Improvement Triggers:**
  - New code patterns not covered by existing rules
  - Repeated similar implementations across files
  - Common error patterns that could be prevented
  - New libraries or tools being used consistently
  - Emerging best practices in the codebase

- **Analysis Process:**
  - Compare new code with existing rules
  - Identify patterns that should be standardized
  - Look for references to external documentation
  - Check for consistent error handling patterns
  - Monitor test patterns and coverage

- **Rule Updates:**
  - **Add New Rules When:**
    - A new technology/pattern is used in 3+ files
    - Common bugs could be prevented by a rule
    - Code reviews repeatedly mention the same feedback
    - New security or performance patterns emerge

  - **Modify Existing Rules When:**
    - Better examples exist in the codebase
    - Additional edge cases are discovered
    - Related rules have been updated
    - Implementation details have changed

- **Example Pattern Recognition:**
  ```typescript
  // If you see repeated patterns like:
  const data = await prisma.user.findMany({
    select: { id: true, email: true },
    where: { status: 'ACTIVE' }
  });

  // Consider adding to [prisma.md](.ruler/prisma.md):
  // - Standard select fields
  // - Common where conditions
  // - Performance optimization patterns
  ```

- **Rule Quality Checks:**
  - Rules should be actionable and specific
  - Examples should come from actual code
  - References should be up to date
  - Patterns should be consistently enforced

- **Continuous Improvement:**
  - Monitor code review comments
  - Track common development questions
  - Update rules after major refactors
  - Add links to relevant documentation
  - Cross-reference related rules

- **Rule Deprecation:**
  - Mark outdated patterns as deprecated
  - Remove rules that no longer apply
  - Update references to deprecated rules
  - Document migration paths for old patterns

- **Documentation Updates:**
  - Keep examples synchronized with code
  - Update references to external docs
  - Maintain links between related rules
  - Document breaking changes
Follow [.ruler.md](.ruler/rules.md) for proper rule formatting and structure.


## Links discovered
- [prisma.md](https://github.com/AcidicSoil/oraclepack/blob/main/.rules/.ruler/prisma.md)
- [.ruler.md](https://github.com/AcidicSoil/oraclepack/blob/main/.rules/.ruler/rules.md)

--- .rules/taskmaster.md ---
---
description: Comprehensive reference for Taskmaster MCP tools and CLI commands.
---

# Taskmaster Tool & Command Reference

This document provides a detailed reference for interacting with Taskmaster, covering both the recommended MCP tools, suitable for integrations like <AGENT>, and the corresponding `task-master` CLI commands, designed for direct user interaction or fallback.

**Note:** For interacting with Taskmaster programmatically or via integrated tools, using the **MCP tools is strongly recommended** due to better performance, structured data, and error handling. The CLI commands serve as a user-friendly alternative and fallback.

**Important:** Several MCP tools involve AI processing... The AI-powered tools include `parse_prd`, `analyze_project_complexity`, `update_subtask`, `update_task`, `update`, `expand_all`, `expand_task`, and `add_task`.

**🏷️ Tagged Task Lists System:** Task Master now supports **tagged task lists** for multi-context task management. This allows you to maintain separate, isolated lists of tasks for different features, branches, or experiments. Existing projects are seamlessly migrated to use a default "master" tag. Most commands now support a `--tag <name>` flag to specify which context to operate on. If omitted, commands use the currently active tag.

---

## Initialization & Setup

### 1. Initialize Project (`init`)

*   **MCP Tool:** `initialize_project`
*   **CLI Command:** `task-master init [options]`
*   **Description:** `Set up the basic Taskmaster file structure and configuration in the current directory for a new project.`
*   **Key CLI Options:**
    *   `--name <name>`: `Set the name for your project in Taskmaster's configuration.`
    *   `--description <text>`: `Provide a brief description for your project.`
    *   `--version <version>`: `Set the initial version for your project, e.g., '0.1.0'.`
    *   `-y, --yes`: `Initialize Taskmaster quickly using default settings without interactive prompts.`
*   **Usage:** Run this once at the beginning of a new project.
*   **MCP Variant Description:** `Set up the basic Taskmaster file structure and configuration in the current directory for a new project by running the 'task-master init' command.`
*   **Key MCP Parameters/Options:**
    *   `projectName`: `Set the name for your project.` (CLI: `--name <name>`)
    *   `projectDescription`: `Provide a brief description for your project.` (CLI: `--description <text>`)
    *   `projectVersion`: `Set the initial version for your project, e.g., '0.1.0'.` (CLI: `--version <version>`)
    *   `authorName`: `Author name.` (CLI: `--author <author>`)
    *   `skipInstall`: `Skip installing dependencies. Default is false.` (CLI: `--skip-install`)
    *   `addAliases`: `Add shell aliases tm, taskmaster, hamster, and ham. Default is false.` (CLI: `--aliases`)
    *   `yes`: `Skip prompts and use defaults/provided arguments. Default is false.` (CLI: `-y, --yes`)
*   **Usage:** Run this once at the beginning of a new project, typically via an integrated tool like <AGENT>. Operates on the current working directory of the MCP server.
*   **Important:** Once complete, you *MUST* parse a prd in order to generate tasks. There will be no tasks files until then. The next step after initializing should be to create a PRD using the example PRD in .taskmaster/templates/example_prd.txt.
*   **Tagging:** Use the `--tag` option to parse the PRD into a specific, non-default tag context. If the tag doesn't exist, it will be created automatically. Example: `task-master parse-prd spec.txt --tag=new-feature`.

### 2. Parse PRD (`parse_prd`)

*   **MCP Tool:** `parse_prd`
*   **CLI Command:** `task-master parse-prd [file] [options]`
*   **Description:** `Parse a Product Requirements Document, PRD, or text file with Taskmaster to automatically generate an initial set of tasks in tasks.json.`
*   **Key Parameters/Options:**
    *   `input`: `Path to your PRD or requirements text file that Taskmaster should parse for tasks.` (CLI: `[file]` positional or `-i, --input <file>`)
    *   `output`: `Specify where Taskmaster should save the generated 'tasks.json' file. Defaults to '.taskmaster/tasks/tasks.json'.` (CLI: `-o, --output <file>`)
    *   `numTasks`: `Approximate number of top-level tasks Taskmaster should aim to generate from the document.` (CLI: `-n, --num-tasks <number>`)
    *   `force`: `Use this to allow Taskmaster to overwrite an existing 'tasks.json' without asking for confirmation.` (CLI: `-f, --force`)
*   **Usage:** Useful for bootstrapping a project from an existing requirements document.
*   **Notes:** Task Master will strictly adhere to any specific requirements mentioned in the PRD, such as libraries, database schemas, frameworks, tech stacks, etc., while filling in any gaps where the PRD isn't fully specified. Tasks are designed to provide the most direct implementation path while avoiding over-engineering.
*   **Important:** This MCP tool makes AI calls and can take up to a minute to complete. Please inform users to hang tight while the operation is in progress. If the user does not have a PRD, suggest discussing their idea and then use the example PRD in `.taskmaster/templates/example_prd.txt` as a template for creating the PRD based on their idea, for use with `parse-prd`.

---

## AI Model Configuration

### 2. Manage Models (`models`)
*   **MCP Tool:** `models`
*   **CLI Command:** `task-master models [options]`
*   **Description:** `View the current AI model configuration or set specific models for different roles (main, research, fallback). Allows setting custom model IDs for Ollama and OpenRouter.`
*   **Key MCP Parameters/Options:**
    *   `setMain <model_id>`: `Set the primary model ID for task generation/updates.` (CLI: `--set-main <model_id>`)
    *   `setResearch <model_id>`: `Set the model ID for research-backed operations.` (CLI: `--set-research <model_id>`)
    *   `setFallback <model_id>`: `Set the model ID to use if the primary fails.` (CLI: `--set-fallback <model_id>`)
    *   `ollama <boolean>`: `Indicates the set model ID is a custom Ollama model.` (CLI: `--ollama`)
    *   `openrouter <boolean>`: `Indicates the set model ID is a custom OpenRouter model.` (CLI: `--openrouter`)
    *   `listAvailableModels <boolean>`: `If true, lists available models not currently assigned to a role.` (CLI: No direct equivalent; CLI lists available automatically)
    *   `projectRoot <string>`: `Optional. Absolute path to the project root directory.` (CLI: Determined automatically)
*   **Key CLI Options:**
    *   `--set-main <model_id>`: `Set the primary model.`
    *   `--set-research <model_id>`: `Set the research model.`
    *   `--set-fallback <model_id>`: `Set the fallback model.`
    *   `--ollama`: `Specify that the provided model ID is for Ollama (use with --set-*).`
    *   `--openrouter`: `Specify that the provided model ID is for OpenRouter (use with --set-*). Validates against OpenRouter API.`
    *   `--bedrock`: `Specify that the provided model ID is for AWS Bedrock (use with --set-*).`
    *   `--setup`: `Run interactive setup to configure models, including custom Ollama/OpenRouter IDs.`
*   **Usage (MCP):** Call without set flags to get current config. Use `setMain`, `setResearch`, or `setFallback` with a valid model ID to update the configuration. Use `listAvailableModels: true` to get a list of unassigned models. To set a custom model, provide the model ID and set `ollama: true` or `openrouter: true`.
*   **Usage (CLI):** Run without flags to view current configuration and available models. Use set flags to update specific roles. Use `--setup` for guided configuration, including custom models. To set a custom model via flags, use `--set-<role>=<model_id>` along with either `--ollama` or `--openrouter`.
*   **Notes:** Configuration is stored in `.taskmaster/config.json` in the project root. This command/tool modifies that file. Use `listAvailableModels` or `task-master models` to see internally supported models. OpenRouter custom models are validated against their live API. Ollama custom models are not validated live.
*   **API note:** API keys for selected AI providers (based on their model) need to exist in the mcp.json file to be accessible in MCP context. The API keys must be present in the local .env file for the CLI to be able to read them.
*   **Model costs:** The costs in supported models are expressed in dollars. An input/output value of 3 is $3.00. A value of 0.8 is $0.80.
*   **Warning:** DO NOT MANUALLY EDIT THE .taskmaster/config.json FILE. Use the included commands either in the MCP or CLI format as needed. Always prioritize MCP tools when available and use the CLI as a fallback.

---

## Task Listing & Viewing

### 3. Get Tasks (`get_tasks`)

*   **MCP Tool:** `get_tasks`
*   **CLI Command:** `task-master list [options]`
*   **Description:** `List your Taskmaster tasks, optionally filtering by status and showing subtasks.`
*   **Key Parameters/Options:**
    *   `status`: `Show only Taskmaster tasks matching this status (or multiple statuses, comma-separated), e.g., 'pending' or 'done,in-progress'.` (CLI: `-s, --status <status>`)
    *   `withSubtasks`: `Include subtasks indented under their parent tasks in the list.` (CLI: `--with-subtasks`)
    *   `tag`: `Specify which tag context to list tasks from. Defaults to the current active tag.` (CLI: `--tag <name>`)
    *   `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file <file>`)
    *   `watch`: `Watch for changes and auto-refresh the list in real-time. Works with file storage (fs.watch) and API storage (Supabase Realtime).` (CLI: `-w, --watch`)
*   **Usage:** Get an overview of the project status, often used at the start of a work session. Use `--watch` to keep the list live-updating as tasks change.

### 4. Get Next Task (`next_task`)

*   **MCP Tool:** `next_task`
*   **CLI Command:** `task-master next [options]`
*   **Description:** `Ask Taskmaster to show the next available task you can work on, based on status and completed dependencies.`
*   **Key Parameters/Options:**
    *   `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file <file>`)
    *   `tag`: `Specify which tag context to use. Defaults to the current active tag.` (CLI: `--tag <name>`)
*   **Usage:** Identify what to work on next according to the plan.

### 5. Get Task Details (`get_task`)

*   **MCP Tool:** `get_task`
*   **CLI Command:** `task-master show [id] [options]`
*   **Description:** `Display detailed information for one or more specific Taskmaster tasks or subtasks by ID.`
*   **Key Parameters/Options:**
    *   `id`: `Required. The ID of the Taskmaster task (e.g., '15'), subtask (e.g., '15.2'), or a comma-separated list of IDs ('1,5,10.2') you want to view.` (CLI: `[id]` positional or `-i, --id <id>`)
    *   `tag`: `Specify which tag context to get the task(s) from. Defaults to the current active tag.` (CLI: `--tag <name>`)
    *   `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file <file>`)
*   **Usage:** Understand the full details for a specific task. When multiple IDs are provided, a summary table is shown.
*   **CRITICAL INFORMATION** If you need to collect information from multiple tasks, use comma-separated IDs (i.e. 1,2,3) to receive an array of tasks. Do not needlessly get tasks one at a time if you need to get many as that is wasteful.

---

## Task Creation & Modification

### 6. Add Task (`add_task`)

*   **MCP Tool:** `add_task`
*   **CLI Command:** `task-master add-task [options]`
*   **Description:** `Add a new task to Taskmaster by describing it; AI will structure it.`
*   **Key Parameters/Options:**
    *   `prompt`: `Required. Describe the new task you want Taskmaster to create, e.g., "Implement user authentication using JWT".` (CLI: `-p, --prompt <text>`)
    *   `dependencies`: `Specify the IDs of any Taskmaster tasks that must be completed before this new one can start, e.g., '12,14'.` (CLI: `-d, --dependencies <ids>`)
    *   `priority`: `Set the priority for the new task: 'high', 'medium', or 'low'. Default is 'medium'.` (CLI: `--priority <priority>`)
    *   `research`: `Enable Taskmaster to use the research role for potentially more informed task creation.` (CLI: `-r, --research`)
    *   `tag`: `Specify which tag context to add the task to. Defaults to the current active tag.` (CLI: `--tag <name>`)
    *   `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file <file>`)
*   **Usage:** Quickly add newly identified tasks during development.
*   **Important:** This MCP tool makes AI calls and can take up to a minute to complete. Please inform users to hang tight while the operation is in progress.

### 7. Add Subtask (`add_subtask`)

*   **MCP Tool:** `add_subtask`
*   **CLI Command:** `task-master add-subtask [options]`
*   **Description:** `Add a new subtask to a Taskmaster parent task, or convert an existing task into a subtask.`
*   **Key Parameters/Options:**
    *   `id` / `parent`: `Required. The ID of the Taskmaster task that will be the parent.` (MCP: `id`, CLI: `-p, --parent <id>`)
    *   `taskId`: `Use this if you want to convert an existing top-level Taskmaster task into a subtask of the specified parent.` (CLI: `-i, --task-id <id>`)
    *   `title`: `Required if not using taskId. The title for the new subtask Taskmaster should create.` (CLI: `-t, --title <title>`)
    *   `description`: `A brief description for the new subtask.` (CLI: `-d, --description <text>`)
    *   `details`: `Provide implementation notes or details for the new subtask.` (CLI: `--details <text>`)
    *   `dependencies`: `Specify IDs of other tasks or subtasks, e.g., '15' or '16.1', that must be done before this new subtask.` (CLI: `--dependencies <ids>`)
    *   `status`: `Set the initial status for the new subtask. Default is 'pending'.` (CLI: `-s, --status <status>`)
    *   `generate`: `Enable Taskmaster to regenerate markdown task files after adding the subtask.` (CLI: `--generate`)
    *   `tag`: `Specify which tag context to operate on. Defaults to the current active tag.` (CLI: `--tag <name>`)
    *   `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file <file>`)
*   **Usage:** Break down tasks manually or reorganize existing tasks.

### 8. Update Tasks (`update`)

*   **MCP Tool:** `update`
*   **CLI Command:** `task-master update [options]`
*   **Description:** `Update multiple upcoming tasks in Taskmaster based on new context or changes, starting from a specific task ID.`
*   **Key Parameters/Options:**
    *   `from`: `Required. The ID of the first task Taskmaster should update. All tasks with this ID or higher that are not 'done' will be considered.` (CLI: `--from <id>`)
    *   `prompt`: `Required. Explain the change or new context for Taskmaster to apply to the tasks, e.g., "We are now using React Query instead of Redux Toolkit for data fetching".` (CLI: `-p, --prompt <text>`)
    *   `research`: `Enable Taskmaster to use the research role for more informed updates. Requires appropriate API key.` (CLI: `-r, --research`)
    *   `tag`: `Specify which tag context to operate on. Defaults to the current active tag.` (CLI: `--tag <name>`)
    *   `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file <file>`)
*   **Usage:** Handle significant implementation changes or pivots that affect multiple future tasks. Example CLI: `task-master update --from='18' --prompt='Switching to React Query.\nNeed to refactor data fetching...'`
*   **Important:** This MCP tool makes AI calls and can take up to a minute to complete. Please inform users to hang tight while the operation is in progress.

### 9. Update Task (`update_task`)

*   **MCP Tool:** `update_task`
*   **CLI Command:** `task-master update-task [options]`
*   **Description:** `Modify a specific Taskmaster task by ID, incorporating new information or changes. By default, this replaces the existing task details.`
*   **Key Parameters/Options:**
    *   `id`: `Required. The specific ID of the Taskmaster task, e.g., '15', you want to update.` (CLI: `-i, --id <id>`)
    *   `prompt`: `Required. Explain the specific changes or provide the new information Taskmaster should incorporate into this task.` (CLI: `-p, --prompt <text>`)
    *   `append`: `If true, appends the prompt content to the task's details with a timestamp, rather than replacing them. Behaves like update-subtask.` (CLI: `--append`)
    *   `research`: `Enable Taskmaster to use the research role for more informed updates. Requires appropriate API key.` (CLI: `-r, --research`)
    *   `tag`: `Specify which tag context the task belongs to. Defaults to the current active tag.` (CLI: `--tag <name>`)
    *   `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file <file>`)
*   **Usage:** Refine a specific task based on new understanding. Use `--append` to log progress without creating subtasks.
*   **Important:** This MCP tool makes AI calls and can take up to a minute to complete. Please inform users to hang tight while the operation is in progress.

### 10. Update Subtask (`update_subtask`)

*   **MCP Tool:** `update_subtask`
*   **CLI Command:** `task-master update-subtask [options]`
*   **Description:** `Append timestamped notes or details to a specific Taskmaster subtask without overwriting existing content. Intended for iterative implementation logging.`
*   **Key Parameters/Options:**
    *   `id`: `Required. The ID of the Taskmaster subtask, e.g., '5.2', to update with new information.` (CLI: `-i, --id <id>`)
    *   `prompt`: `Required. The information, findings, or progress notes to append to the subtask's details with a timestamp.` (CLI: `-p, --prompt <text>`)
    *   `research`: `Enable Taskmaster to use the research role for more informed updates. Requires appropriate API key.` (CLI: `-r, --research`)
    *   `tag`: `Specify which tag context the subtask belongs to. Defaults to the current active tag.` (CLI: `--tag <name>`)
    *   `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file <file>`)
*   **Usage:** Log implementation progress, findings, and discoveries during subtask development. Each update is timestamped and appended to preserve the implementation journey.
*   **Important:** This MCP tool makes AI calls and can take up to a minute to complete. Please inform users to hang tight while the operation is in progress.

### 11. Set Task Status (`set_task_status`)

*   **MCP Tool:** `set_task_status`
*   **CLI Command:** `task-master set-status [options]`
*   **Description:** `Update the status of one or more Taskmaster tasks or subtasks, e.g., 'pending', 'in-progress', 'done'.`
*   **Key Parameters/Options:**
    *   `id`: `Required. The ID(s) of the Taskmaster task(s) or subtask(s), e.g., '15', '15.2', or '16,17.1', to update.` (CLI: `-i, --id <id>`)
    *   `status`: `Required. The new status to set, e.g., 'done', 'pending', 'in-progress', 'review', 'cancelled'.` (CLI: `-s, --status <status>`)
    *   `tag`: `Specify which tag context to operate on. Defaults to the current active tag.` (CLI: `--tag <name>`)
    *   `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file <file>`)
*   **Usage:** Mark progress as tasks move through the development cycle.

### 12. Remove Task (`remove_task`)

*   **MCP Tool:** `remove_task`
*   **CLI Command:** `task-master remove-task [options]`
*   **Description:** `Permanently remove a task or subtask from the Taskmaster tasks list.`
*   **Key Parameters/Options:**
    *   `id`: `Required. The ID of the Taskmaster task, e.g., '5', or subtask, e.g., '5.2', to permanently remove.` (CLI: `-i, --id <id>`)
    *   `yes`: `Skip the confirmation prompt and immediately delete the task.` (CLI: `-y, --yes`)
    *   `tag`: `Specify which tag context to operate on. Defaults to the current active tag.` (CLI: `--tag <name>`)
    *   `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file <file>`)
*   **Usage:** Permanently delete tasks or subtasks that are no longer needed in the project.
*   **Notes:** Use with caution as this operation cannot be undone. Consider using 'blocked', 'cancelled', or 'deferred' status instead if you just want to exclude a task from active planning but keep it for reference. The command automatically cleans up dependency references in other tasks.

---

## Task Structure & Breakdown

### 13. Expand Task (`expand_task`)

*   **MCP Tool:** `expand_task`
*   **CLI Command:** `task-master expand [options]`
*   **Description:** `Use Taskmaster's AI to break down a complex task into smaller, manageable subtasks. Appends subtasks by default.`
*   **Key Parameters/Options:**
    *   `id`: `The ID of the specific Taskmaster task you want to break down into subtasks.` (CLI: `-i, --id <id>`)
    *   `num`: `Optional: Suggests how many subtasks Taskmaster should aim to create. Uses complexity analysis/defaults otherwise.` (CLI: `-n, --num <number>`)
    *   `research`: `Enable Taskmaster to use the research role for more informed subtask generation. Requires appropriate API key.` (CLI: `-r, --research`)
    *   `prompt`: `Optional: Provide extra context or specific instructions to Taskmaster for generating the subtasks.` (CLI: `-p, --prompt <text>`)
    *   `force`: `Optional: If true, clear existing subtasks before generating new ones. Default is false (append).` (CLI: `--force`)
    *   `tag`: `Specify which tag context the task belongs to. Defaults to the current active tag.` (CLI: `--tag <name>`)
    *   `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file <file>`)
*   **Usage:** Generate a detailed implementation plan for a complex task before starting coding. Automatically uses complexity report recommendations if available and `num` is not specified.
*   **Important:** This MCP tool makes AI calls and can take up to a minute to complete. Please inform users to hang tight while the operation is in progress.

### 14. Expand All Tasks (`expand_all`)

*   **MCP Tool:** `expand_all`
*   **CLI Command:** `task-master expand --all [options]` (Note: CLI uses the `expand` command with the `--all` flag)
*   **Description:** `Tell Taskmaster to automatically expand all eligible pending/in-progress tasks based on complexity analysis or defaults. Appends subtasks by default.`
*   **Key Parameters/Options:**
    *   `num`: `Optional: Suggests how many subtasks Taskmaster should aim to create per task.` (CLI: `-n, --num <number>`)
    *   `research`: `Enable research role for more informed subtask generation. Requires appropriate API key.` (CLI: `-r, --research`)
    *   `prompt`: `Optional: Provide extra context for Taskmaster to apply generally during expansion.` (CLI: `-p, --prompt <text>`)
    *   `force`: `Optional: If true, clear existing subtasks before generating new ones for each eligible task. Default is false (append).` (CLI: `--force`)
    *   `tag`: `Specify which tag context to expand. Defaults to the current active tag.` (CLI: `--tag <name>`)
    *   `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file <file>`)
*   **Usage:** Useful after initial task generation or complexity analysis to break down multiple tasks at once.
*   **Important:** This MCP tool makes AI calls and can take up to a minute to complete. Please inform users to hang tight while the operation is in progress.

### 15. Clear Subtasks (`clear_subtasks`)

*   **MCP Tool:** `clear_subtasks`
*   **CLI Command:** `task-master clear-subtasks [options]`
*   **Description:** `Remove all subtasks from one or more specified Taskmaster parent tasks.`
*   **Key Parameters/Options:**
    *   `id`: `The ID(s) of the Taskmaster parent task(s) whose subtasks you want to remove, e.g., '15' or '16,18'. Required unless using 'all'.` (CLI: `-i, --id <ids>`)
    *   `all`: `Tell Taskmaster to remove subtasks from all parent tasks.` (CLI: `--all`)
    *   `tag`: `Specify which tag context to operate on. Defaults to the current active tag.` (CLI: `--tag <name>`)
    *   `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file <file>`)
*   **Usage:** Used before regenerating subtasks with `expand_task` if the previous breakdown needs replacement.

### 16. Remove Subtask (`remove_subtask`)

*   **MCP Tool:** `remove_subtask`
*   **CLI Command:** `task-master remove-subtask [options]`
*   **Description:** `Remove a subtask from its Taskmaster parent, optionally converting it into a standalone task.`
*   **Key Parameters/Options:**
    *   `id`: `Required. The ID(s) of the Taskmaster subtask(s) to remove, e.g., '15.2' or '16.1,16.3'.` (CLI: `-i, --id <id>`)
    *   `convert`: `If used, Taskmaster will turn the subtask into a regular top-level task instead of deleting it.` (CLI: `-c, --convert`)
    *   `generate`: `Enable Taskmaster to regenerate markdown task files after removing the subtask.` (CLI: `--generate`)
    *   `tag`: `Specify which tag context to operate on. Defaults to the current active tag.` (CLI: `--tag <name>`)
    *   `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file <file>`)
*   **Usage:** Delete unnecessary subtasks or promote a subtask to a top-level task.

### 17. Move Task (`move_task`)

*   **MCP Tool:** `move_task`
*   **CLI Command:** `task-master move [options]`
*   **Description:** `Move a task or subtask to a new position within the task hierarchy.`
*   **Key Parameters/Options:**
    *   `from`: `Required. ID of the task/subtask to move (e.g., "5" or "5.2"). Can be comma-separated for multiple tasks.` (CLI: `--from <id>`)
    *   `to`: `Required. ID of the destination (e.g., "7" or "7.3"). Must match the number of source IDs if comma-separated.` (CLI: `--to <id>`)
    *   `tag`: `Specify which tag context to operate on. Defaults to the current active tag.` (CLI: `--tag <name>`)
    *   `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file <file>`)
*   **Usage:** Reorganize tasks by moving them within the hierarchy. Supports various scenarios like:
    *   Moving a task to become a subtask
    *   Moving a subtask to become a standalone task
    *   Moving a subtask to a different parent
    *   Reordering subtasks within the same parent
    *   Moving a task to a new, non-existent ID (automatically creates placeholders)
    *   Moving multiple tasks at once with comma-separated IDs
*   **Validation Features:**
    *   Allows moving tasks to non-existent destination IDs (creates placeholder tasks)
    *   Prevents moving to existing task IDs that already have content (to avoid overwriting)
    *   Validates that source tasks exist before attempting to move them
    *   Maintains proper parent-child relationships
*   **Example CLI:** `task-master move --from=5.2 --to=7.3` to move subtask 5.2 to become subtask 7.3.
*   **Example Multi-Move:** `task-master move --from=10,11,12 --to=16,17,18` to move multiple tasks to new positions.
*   **Common Use:** Resolving merge conflicts in tasks.json when multiple team members create tasks on different branches.

---

## Dependency Management

### 18. Add Dependency (`add_dependency`)

*   **MCP Tool:** `add_dependency`
*   **CLI Command:** `task-master add-dependency [options]`
*   **Description:** `Define a dependency in Taskmaster, making one task a prerequisite for another.`
*   **Key Parameters/Options:**
    *   `id`: `Required. The ID of the Taskmaster task that will depend on another.` (CLI: `-i, --id <id>`)
    *   `dependsOn`: `Required. The ID of the Taskmaster task that must be completed first, the prerequisite.` (CLI: `-d, --depends-on <id>`)
    *   `tag`: `Specify which tag context to operate on. Defaults to the current active tag.` (CLI: `--tag <name>`)
    *   `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file <path>`)
*   **Usage:** Establish the correct order of execution between tasks.

### 19. Remove Dependency (`remove_dependency`)

*   **MCP Tool:** `remove_dependency`
*   **CLI Command:** `task-master remove-dependency [options]`
*   **Description:** `Remove a dependency relationship between two Taskmaster tasks.`
*   **Key Parameters/Options:**
    *   `id`: `Required. The ID of the Taskmaster task you want to remove a prerequisite from.` (CLI: `-i, --id <id>`)
    *   `dependsOn`: `Required. The ID of the Taskmaster task that should no longer be a prerequisite.` (CLI: `-d, --depends-on <id>`)
    *   `tag`: `Specify which tag context to operate on. Defaults to the current active tag.` (CLI: `--tag <name>`)
    *   `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file <file>`)
*   **Usage:** Update task relationships when the order of execution changes.

### 20. Validate Dependencies (`validate_dependencies`)

*   **MCP Tool:** `validate_dependencies`
*   **CLI Command:** `task-master validate-dependencies [options]`
*   **Description:** `Check your Taskmaster tasks for dependency issues (like circular references or links to non-existent tasks) without making changes.`
*   **Key Parameters/Options:**
    *   `tag`: `Specify which tag context to validate. Defaults to the current active tag.` (CLI: `--tag <name>`)
    *   `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file <file>`)
*   **Usage:** Audit the integrity of your task dependencies.

### 21. Fix Dependencies (`fix_dependencies`)

*   **MCP Tool:** `fix_dependencies`
*   **CLI Command:** `task-master fix-dependencies [options]`
*   **Description:** `Automatically fix dependency issues (like circular references or links to non-existent tasks) in your Taskmaster tasks.`
*   **Key Parameters/Options:**
    *   `tag`: `Specify which tag context to fix dependencies in. Defaults to the current active tag.` (CLI: `--tag <name>`)
    *   `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file <file>`)
*   **Usage:** Clean up dependency errors automatically.

---

## Analysis & Reporting

### 22. Analyze Project Complexity (`analyze_project_complexity`)

*   **MCP Tool:** `analyze_project_complexity`
*   **CLI Command:** `task-master analyze-complexity [options]`
*   **Description:** `Have Taskmaster analyze your tasks to determine their complexity and suggest which ones need to be broken down further.`
*   **Key Parameters/Options:**
    *   `output`: `Where to save the complexity analysis report. Default is '.taskmaster/reports/task-complexity-report.json' (or '..._tagname.json' if a tag is used).` (CLI: `-o, --output <file>`)
    *   `threshold`: `The minimum complexity score (1-10) that should trigger a recommendation to expand a task.` (CLI: `-t, --threshold <number>`)
    *   `research`: `Enable research role for more accurate complexity analysis. Requires appropriate API key.` (CLI: `-r, --research`)
    *   `tag`: `Specify which tag context to analyze. Defaults to the current active tag.` (CLI: `--tag <name>`)
    *   `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file <file>`)
*   **Usage:** Used before breaking down tasks to identify which ones need the most attention.
*   **Important:** This MCP tool makes AI calls and can take up to a minute to complete. Please inform users to hang tight while the operation is in progress.

### 23. View Complexity Report (`complexity_report`)

*   **MCP Tool:** `complexity_report`
*   **CLI Command:** `task-master complexity-report [options]`
*   **Description:** `Display the task complexity analysis report in a readable format.`
*   **Key Parameters/Options:**
    *   `tag`: `Specify which tag context to show the report for. Defaults to the current active tag.` (CLI: `--tag <name>`)
    *   `file`: `Path to the complexity report (default: '.taskmaster/reports/task-complexity-report.json').` (CLI: `-f, --file <file>`)
*   **Usage:** Review and understand the complexity analysis results after running analyze-complexity.

---

## File Management

### 24. Generate Task Files (`generate`)

*   **MCP Tool:** `generate`
*   **CLI Command:** `task-master generate [options]`
*   **Description:** `Create or update individual Markdown files for each task based on your tasks.json.`
*   **Key Parameters/Options:**
    *   `output`: `The directory where Taskmaster should save the task files (default: in a 'tasks' directory).` (CLI: `-o, --output <directory>`)
    *   `tag`: `Specify which tag context to generate files for. Defaults to the current active tag.` (CLI: `--tag <name>`)
    *   `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file <file>`)
*   **Usage:** Run this after making changes to tasks.json to keep individual task files up to date. This command is now manual and no longer runs automatically.

---

## AI-Powered Research

### 25. Research (`research`)

*   **MCP Tool:** `research`
*   **CLI Command:** `task-master research [options]`
*   **Description:** `Perform AI-powered research queries with project context to get fresh, up-to-date information beyond the AI's knowledge cutoff.`
*   **Key Parameters/Options:**
    *   `query`: `Required. Research query/prompt (e.g., "What are the latest best practices for React Query v5?").` (CLI: `[query]` positional or `-q, --query <text>`)
    *   `taskIds`: `Comma-separated list of task/subtask IDs from the current tag context (e.g., "15,16.2,17").` (CLI: `-i, --id <ids>`)
    *   `filePaths`: `Comma-separated list of file paths for context (e.g., "src/api.js,docs/readme.md").` (CLI: `-f, --files <paths>`)
    *   `customContext`: `Additional custom context text to include in the research.` (CLI: `-c, --context <text>`)
    *   `includeProjectTree`: `Include project file tree structure in context (default: false).` (CLI: `--tree`)
    *   `detailLevel`: `Detail level for the research response: 'low', 'medium', 'high' (default: medium).` (CLI: `--detail <level>`)
    *   `saveTo`: `Task or subtask ID (e.g., "15", "15.2") to automatically save the research conversation to.` (CLI: `--save-to <id>`)
    *   `saveFile`: `If true, saves the research conversation to a markdown file in '.taskmaster/docs/research/'.` (CLI: `--save-file`)
    *   `noFollowup`: `Disables the interactive follow-up question menu in the CLI.` (CLI: `--no-followup`)
    *   `tag`: `Specify which tag context to use for task-based context gathering. Defaults to the current active tag.` (CLI: `--tag <name>`)
    *   `projectRoot`: `The directory of the project. Must be an absolute path.` (CLI: Determined automatically)
*   **Usage:** **This is a POWERFUL tool that agents should use FREQUENTLY** to:
    *   Get fresh information beyond knowledge cutoff dates
    *   Research latest best practices, library updates, security patches
    *   Find implementation examples for specific technologies
    *   Validate approaches against current industry standards
    *   Get contextual advice based on project files and tasks
*   **When to Consider Using Research:**
    *   **Before implementing any task** - Research current best practices
    *   **When encountering new technologies** - Get up-to-date implementation guidance (libraries, apis, etc)
    *   **For security-related tasks** - Find latest security recommendations
    *   **When updating dependencies** - Research breaking changes and migration guides
    *   **For performance optimization** - Get current performance best practices
    *   **When debugging complex issues** - Research known solutions and workarounds
*   **Research + Action Pattern:**
    *   Use `research` to gather fresh information
    *   Use `update_subtask` to commit findings with timestamps
    *   Use `update_task` to incorporate research into task details
    *   Use `add_task` with research flag for informed task creation
*   **Important:** This MCP tool makes AI calls and can take up to a minute to complete. The research provides FRESH data beyond the AI's training cutoff, making it invaluable for current best practices and recent developments.

---

## Tag Management

This new suite of commands allows you to manage different task contexts (tags).

### 26. List Tags (`tags`)

*   **MCP Tool:** `list_tags`
*   **CLI Command:** `task-master tags [options]`
*   **Description:** `List all available tags with task counts, completion status, and other metadata.`
*   **Key Parameters/Options:**
    *   `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file <file>`)
    *   `--show-metadata`: `Include detailed metadata in the output (e.g., creation date, description).` (CLI: `--show-metadata`)

### 27. Add Tag (`add_tag`)

*   **MCP Tool:** `add_tag`
*   **CLI Command:** `task-master add-tag <tagName> [options]`
*   **Description:** `Create a new, empty tag context, or copy tasks from another tag.`
*   **Key Parameters/Options:**
    *   `tagName`: `Name of the new tag to create (alphanumeric, hyphens, underscores).` (CLI: `<tagName>` positional)
    *   `--from-branch`: `Creates a tag with a name derived from the current git branch, ignoring the <tagName> argument.` (CLI: `--from-branch`)
    *   `--copy-from-current`: `Copy tasks from the currently active tag to the new tag.` (CLI: `--copy-from-current`)
    *   `--copy-from <tag>`: `Copy tasks from a specific source tag to the new tag.` (CLI: `--copy-from <tag>`)
    *   `--description <text>`: `Provide an optional description for the new tag.` (CLI: `-d, --description <text>`)
    *   `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file <file>`)

### 28. Delete Tag (`delete_tag`)

*   **MCP Tool:** `delete_tag`
*   **CLI Command:** `task-master delete-tag <tagName> [options]`
*   **Description:** `Permanently delete a tag and all of its associated tasks.`
*   **Key Parameters/Options:**
    *   `tagName`: `Name of the tag to delete.` (CLI: `<tagName>` positional)
    *   `--yes`: `Skip the confirmation prompt.` (CLI: `-y, --yes`)
    *   `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file <file>`)

### 29. Use Tag (`use_tag`)

*   **MCP Tool:** `use_tag`
*   **CLI Command:** `task-master use-tag <tagName>`
*   **Description:** `Switch your active task context to a different tag.`
*   **Key Parameters/Options:**
    *   `tagName`: `Name of the tag to switch to.` (CLI: `<tagName>` positional)
    *   `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file <file>`)

### 30. Rename Tag (`rename_tag`)

*   **MCP Tool:** `rename_tag`
*   **CLI Command:** `task-master rename-tag <oldName> <newName>`
*   **Description:** `Rename an existing tag.`
*   **Key Parameters/Options:**
    *   `oldName`: `The current name of the tag.` (CLI: `<oldName>` positional)
    *   `newName`: `The new name for the tag.` (CLI: `<newName>` positional)
    *   `file`: `Path to your Taskmaster 'tasks.json' file. Default relies on auto-detection.` (CLI: `-f, --file <file>`)

### 31. Copy Tag (`copy_tag`)

*   **MCP Tool:** `copy_tag`
*   **CLI Command:** `task-master copy-tag <sourceName> <targetName> [options]`
*   **Description:** `Copy an entire tag context, including all its tasks and metadata, to a new tag.`
*   **Key Parameters/Options:**
    *   `sourceName`: `Name of the tag to copy from.` (CLI: `<sourceName>` positional)
    *   `targetName`: `Name of the new tag to create.` (CLI: `<targetName>` positional)
    *   `--description <text>`: `Optional description for the new tag.` (CLI: `-d, --description <text>`)

---

## Miscellaneous

### 32. Sync Readme (`sync-readme`) -- experimental

*   **MCP Tool:** N/A
*   **CLI Command:** `task-master sync-readme [options]`
*   **Description:** `Exports your task list to your project's README.md file, useful for showcasing progress.`
*   **Key Parameters/Options:**
    *   `status`: `Filter tasks by status (e.g., 'pending', 'done').` (CLI: `-s, --status <status>`)
    *   `withSubtasks`: `Include subtasks in the export.` (CLI: `--with-subtasks`)
    *   `tag`: `Specify which tag context to export from. Defaults to the current active tag.` (CLI: `--tag <name>`)

---

## Environment Variables Configuration (Updated)

Taskmaster primarily uses the **`.taskmaster/config.json`** file (in project root) for configuration (models, parameters, logging level, etc.), managed via `task-master models --setup`.

Environment variables are used **only** for sensitive API keys related to AI providers and specific overrides like the Ollama base URL:

*   **API Keys (Required for corresponding provider):**
    *   `ANTHROPIC_API_KEY`
    *   `PERPLEXITY_API_KEY`
    *   `OPENAI_API_KEY`
    *   `GOOGLE_API_KEY`
    *   `MISTRAL_API_KEY`
    *   `AZURE_OPENAI_API_KEY` (Requires `AZURE_OPENAI_ENDPOINT` too)
    *   `OPENROUTER_API_KEY`
    *   `XAI_API_KEY`
    *   `OLLAMA_API_KEY` (Requires `OLLAMA_BASE_URL` too)
*   **Endpoints (Optional/Provider Specific inside .taskmaster/config.json):**
    *   `AZURE_OPENAI_ENDPOINT`
    *   `OLLAMA_BASE_URL` (Default: `http://localhost:11434/api`)

**Set API keys** in your **`.env`** file in the project root (for CLI use) or within the `env` section of your **`<AGENT>/mcp.json`** file (for MCP/<AGENT> integration). All other settings (model choice, max tokens, temperature, log level, custom endpoints) are managed in `.taskmaster/config.json` via `task-master models` command or `models` MCP tool.

---

## MCP Tool Tiers

Default: `core` (7 tools). Set via `TASK_MASTER_TOOLS` env var in MCP config.

| Tier | Count | Tools |
|------|-------|-------|
| `core` | 7 | `get_tasks`, `next_task`, `get_task`, `set_task_status`, `update_subtask`, `parse_prd`, `expand_task` |
| `standard` | 14 | core + `initialize_project`, `analyze_project_complexity`, `expand_all`, `add_subtask`, `remove_task`, `add_task`, `complexity_report` |
| `all` | 44+ | standard + dependencies, tags, research, autopilot, scoping, models, rules |

**Upgrade when tool unavailable:** Edit MCP config (`<AGENT>/mcp.json`, `.mcp.json`, or `.vscode/mcp.json`), change `TASK_MASTER_TOOLS` from `"core"` to `"standard"` or `"all"`, restart MCP.

---

For details on how these commands fit into the development process, see the [dev_workflow.md](<AGENT>rules/dev_workflow.md).

## Links discovered
- [dev_workflow.md](https://github.com/AcidicSoil/oraclepack/blob/main/.rules/<AGENT>rules/dev_workflow.md)

--- .taskmaster/AGENTS.md ---
# Task Master AI - Agent Integration Guide

## Essential Commands

### Core Workflow Commands

```bash
# Project Setup
task-master init                                    # Initialize Task Master in current project
task-master parse-prd .taskmaster/docs/prd.md       # Generate tasks from PRD document
task-master models --setup                        # Configure AI models interactively

# Daily Development Workflow
task-master list                                   # Show all tasks with status
task-master next                                   # Get next available task to work on
task-master show <id>                             # View detailed task information (e.g., task-master show 1.2)
task-master set-status --id=<id> --status=done    # Mark task complete

# Task Management
task-master add-task --prompt="description" --research        # Add new task with AI assistance
task-master expand --id=<id> --research --force              # Break task into subtasks
task-master update-task --id=<id> --prompt="changes"         # Update specific task
task-master update --from=<id> --prompt="changes"            # Update multiple tasks from ID onwards
task-master update-subtask --id=<id> --prompt="notes"        # Add implementation notes to subtask

# Analysis & Planning
task-master analyze-complexity --research          # Analyze task complexity
task-master complexity-report                      # View complexity analysis
task-master expand --all --research               # Expand all eligible tasks

# Dependencies & Organization
task-master add-dependency --id=<id> --depends-on=<id>       # Add task dependency
task-master move --from=<id> --to=<id>                       # Reorganize task hierarchy
task-master validate-dependencies                            # Check for dependency issues
task-master generate                                         # Update task markdown files (usually auto-called)
```

## Key Files & Project Structure

### Core Files

- `.taskmaster/tasks/tasks.json` - Main task data file (auto-managed)
- `.taskmaster/config.json` - AI model configuration (use `task-master models` to modify)
- `.taskmaster/docs/prd.md` - Product Requirements Document for parsing (`.md` extension recommended for better editor support)
- `.taskmaster/tasks/*.txt` - Individual task files (auto-generated from tasks.json)
- `.env` - API keys for CLI usage

**PRD File Format:** While both `.txt` and `.md` extensions work, **`.md` is recommended** because:
- Markdown syntax highlighting in editors improves readability
- Proper rendering when previewing in VS Code, GitHub, or other tools
- Better collaboration through formatted documentation

### Claude Code Integration Files

- `CLAUDE.md` - Auto-loaded context for Claude Code (this file)
- `.claude/settings.json` - Claude Code tool allowlist and preferences
- `.claude/commands/` - Custom slash commands for repeated workflows
- `.mcp.json` - MCP server configuration (project-specific)

### Directory Structure

```
project/
├── .taskmaster/
│   ├── tasks/              # Task files directory
│   │   ├── tasks.json      # Main task database
│   │   ├── task-1.md      # Individual task files
│   │   └── task-2.md
│   ├── docs/              # Documentation directory
│   │   ├── prd.md         # Product requirements (.md recommended)
│   ├── reports/           # Analysis reports directory
│   │   └── task-complexity-report.json
│   ├── templates/         # Template files
│   │   └── example_prd.md  # Example PRD template (.md recommended)
│   └── config.json        # AI models & settings
├── .claude/
│   ├── settings.json      # Claude Code configuration
│   └── commands/         # Custom slash commands
├── .env                  # API keys
├── .mcp.json            # MCP configuration
└── CLAUDE.md            # This file - auto-loaded by Claude Code
```

## MCP Integration

Task Master provides an MCP server that Claude Code can connect to. Configure in `.mcp.json`:

```json
{
  "mcpServers": {
    "task-master-ai": {
      "command": "npx",
      "args": ["-y", "task-master-ai"],
      "env": {
        "TASK_MASTER_TOOLS": "core",
        "ANTHROPIC_API_KEY": "your_key_here",
        "PERPLEXITY_API_KEY": "your_key_here",
        "OPENAI_API_KEY": "OPENAI_API_KEY_HERE",
        "GOOGLE_API_KEY": "GOOGLE_API_KEY_HERE",
        "XAI_API_KEY": "XAI_API_KEY_HERE",
        "OPENROUTER_API_KEY": "OPENROUTER_API_KEY_HERE",
        "MISTRAL_API_KEY": "MISTRAL_API_KEY_HERE",
        "AZURE_OPENAI_API_KEY": "AZURE_OPENAI_API_KEY_HERE",
        "OLLAMA_API_KEY": "OLLAMA_API_KEY_HERE"
      }
    }
  }
}
```

### MCP Tool Tiers

Default: `core` (7 tools). Set via `TASK_MASTER_TOOLS` env var.

| Tier | Count | Tools |
|------|-------|-------|
| `core` | 7 | `get_tasks`, `next_task`, `get_task`, `set_task_status`, `update_subtask`, `parse_prd`, `expand_task` |
| `standard` | 14 | core + `initialize_project`, `analyze_project_complexity`, `expand_all`, `add_subtask`, `remove_task`, `add_task`, `complexity_report` |
| `all` | 44+ | standard + dependencies, tags, research, autopilot, scoping, models, rules |

**Upgrade when tool unavailable:** Edit MCP config, change `TASK_MASTER_TOOLS` from `"core"` to `"standard"` or `"all"`, restart MCP.

### Essential MCP Tools

```javascript
help; // = shows available taskmaster commands
// Project setup
initialize_project; // = task-master init
parse_prd; // = task-master parse-prd

// Daily workflow
get_tasks; // = task-master list
next_task; // = task-master next
get_task; // = task-master show <id>
set_task_status; // = task-master set-status

// Task management
add_task; // = task-master add-task
expand_task; // = task-master expand
update_task; // = task-master update-task
update_subtask; // = task-master update-subtask
update; // = task-master update

// Analysis
analyze_project_complexity; // = task-master analyze-complexity
complexity_report; // = task-master complexity-report
```

## Claude Code Workflow Integration

### Standard Development Workflow

#### 1. Project Initialization

```bash
# Initialize Task Master
task-master init

# Create or obtain PRD, then parse it (use .md extension for better editor support)
task-master parse-prd .taskmaster/docs/prd.md

# Analyze complexity and expand tasks
task-master analyze-complexity --research
task-master expand --all --research
```

If tasks already exist, another PRD can be parsed (with new information only!) using parse-prd with --append flag. This will add the generated tasks to the existing list of tasks..

#### 2. Daily Development Loop

```bash
# Start each session
task-master next                           # Find next available task
task-master show <id>                     # Review task details

# During implementation, check in code context into the tasks and subtasks
task-master update-subtask --id=<id> --prompt="implementation notes..."

# Complete tasks
task-master set-status --id=<id> --status=done
```

#### 3. Multi-Claude Workflows

For complex projects, use multiple Claude Code sessions:

```bash
# Terminal 1: Main implementation
cd project && claude

# Terminal 2: Testing and validation
cd project-test-worktree && claude

# Terminal 3: Documentation updates
cd project-docs-worktree && claude
```

### Custom Slash Commands

Create `.claude/commands/taskmaster-next.md`:

```markdown
Find the next available Task Master task and show its details.

Steps:

1. Run `task-master next` to get the next task
2. If a task is available, run `task-master show <id>` for full details
3. Provide a summary of what needs to be implemented
4. Suggest the first implementation step
```

Create `.claude/commands/taskmaster-complete.md`:

```markdown
Complete a Task Master task: $ARGUMENTS

Steps:

1. Review the current task with `task-master show $ARGUMENTS`
2. Verify all implementation is complete
3. Run any tests related to this task
4. Mark as complete: `task-master set-status --id=$ARGUMENTS --status=done`
5. Show the next available task with `task-master next`
```

## Tool Allowlist Recommendations

Add to `.claude/settings.json`:

```json
{
  "allowedTools": [
    "Edit",
    "Bash(task-master *)",
    "Bash(git commit:*)",
    "Bash(git add:*)",
    "Bash(npm run *)",
    "mcp__task_master_ai__*"
  ]
}
```

## Configuration & Setup

### API Keys Required

At least **one** of these API keys must be configured:

- `ANTHROPIC_API_KEY` (Claude models) - **Recommended**
- `PERPLEXITY_API_KEY` (Research features) - **Highly recommended**
- `OPENAI_API_KEY` (GPT models)
- `GOOGLE_API_KEY` (Gemini models)
- `MISTRAL_API_KEY` (Mistral models)
- `OPENROUTER_API_KEY` (Multiple models)
- `XAI_API_KEY` (Grok models)

An API key is required for any provider used across any of the 3 roles defined in the `models` command.

### Model Configuration

```bash
# Interactive setup (recommended)
task-master models --setup

# Set specific models
task-master models --set-main claude-3-5-sonnet-20241022
task-master models --set-research perplexity-llama-3.1-sonar-large-128k-online
task-master models --set-fallback gpt-4o-mini
```

## Task Structure & IDs

### Task ID Format

- Main tasks: `1`, `2`, `3`, etc.
- Subtasks: `1.1`, `1.2`, `2.1`, etc.
- Sub-subtasks: `1.1.1`, `1.1.2`, etc.

### Task Status Values

- `pending` - Ready to work on
- `in-progress` - Currently being worked on
- `done` - Completed and verified
- `deferred` - Postponed
- `cancelled` - No longer needed
- `blocked` - Waiting on external factors

### Task Fields

```json
{
  "id": "1.2",
  "title": "Implement user authentication",
  "description": "Set up JWT-based auth system",
  "status": "pending",
  "priority": "high",
  "dependencies": ["1.1"],
  "details": "Use bcrypt for hashing, JWT for tokens...",
  "testStrategy": "Unit tests for auth functions, integration tests for login flow",
  "subtasks": []
}
```

## Claude Code Best Practices with Task Master

### Context Management

- Use `/clear` between different tasks to maintain focus
- This CLAUDE.md file is automatically loaded for context
- Use `task-master show <id>` to pull specific task context when needed

### Iterative Implementation

1. `task-master show <subtask-id>` - Understand requirements
2. Explore codebase and plan implementation
3. `task-master update-subtask --id=<id> --prompt="detailed plan"` - Log plan
4. `task-master set-status --id=<id> --status=in-progress` - Start work
5. Implement code following logged plan
6. `task-master update-subtask --id=<id> --prompt="what worked/didn't work"` - Log progress
7. `task-master set-status --id=<id> --status=done` - Complete task

### Complex Workflows with Checklists

For large migrations or multi-step processes:

1. Create a markdown PRD file describing the new changes: `touch task-migration-checklist.md` (prds can be .txt or .md)
2. Use Taskmaster to parse the new prd with `task-master parse-prd --append` (also available in MCP)
3. Use Taskmaster to expand the newly generated tasks into subtasks. Consdier using `analyze-complexity` with the correct --to and --from IDs (the new ids) to identify the ideal subtask amounts for each task. Then expand them.
4. Work through items systematically, checking them off as completed
5. Use `task-master update-subtask` to log progress on each task/subtask and/or updating/researching them before/during implementation if getting stuck

### Git Integration

Task Master works well with `gh` CLI:

```bash
# Create PR for completed task
gh pr create --title "Complete task 1.2: User authentication" --body "Implements JWT auth system as specified in task 1.2"

# Reference task in commits
git commit -m "feat: implement JWT auth (task 1.2)"
```

### Parallel Development with Git Worktrees

```bash
# Create worktrees for parallel task development
git worktree add ../project-auth feature/auth-system
git worktree add ../project-api feature/api-refactor

# Run Claude Code in each worktree
cd ../project-auth && claude    # Terminal 1: Auth work
cd ../project-api && claude     # Terminal 2: API work
```

## Troubleshooting

### AI Commands Failing

```bash
# Check API keys are configured
cat .env                           # For CLI usage

# Verify model configuration
task-master models

# Test with different model
task-master models --set-fallback gpt-4o-mini
```

### MCP Connection Issues

- Check `.mcp.json` configuration
- Verify Node.js installation
- Use `--mcp-debug` flag when starting Claude Code
- Use CLI as fallback if MCP unavailable

### Task File Sync Issues

```bash
# Regenerate task files from tasks.json
task-master generate

# Fix dependency issues
task-master fix-dependencies
```

DO NOT RE-INITIALIZE. That will not do anything beyond re-adding the same Taskmaster core files.

## Important Notes

### AI-Powered Operations

These commands make AI calls and may take up to a minute:

- `parse_prd` / `task-master parse-prd`
- `analyze_project_complexity` / `task-master analyze-complexity`
- `expand_task` / `task-master expand`
- `expand_all` / `task-master expand --all`
- `add_task` / `task-master add-task`
- `update` / `task-master update`
- `update_task` / `task-master update-task`
- `update_subtask` / `task-master update-subtask`

### File Management

- Never manually edit `tasks.json` - use commands instead
- Never manually edit `.taskmaster/config.json` - use `task-master models`
- Task markdown files in `tasks/` are auto-generated
- Run `task-master generate` after manual changes to tasks.json

### Claude Code Session Management

- Use `/clear` frequently to maintain focused context
- Create custom slash commands for repeated Task Master workflows
- Configure tool allowlist to streamline permissions
- Use headless mode for automation: `claude -p "task-master next"`

### Multi-Task Updates

- Use `update --from=<id>` to update multiple future tasks
- Use `update-task --id=<id>` for single task updates
- Use `update-subtask --id=<id>` for implementation logging

### Research Mode

- Add `--research` flag for research-based AI enhancement
- Requires a research model API key like Perplexity (`PERPLEXITY_API_KEY`) in environment
- Provides more informed task creation and updates
- Recommended for complex technical tasks

---

_This guide ensures Claude Code has immediate access to Task Master's essential functionality for agentic development workflows._


--- .taskmaster/tm_help.txt ---

╭─────────────────────╮
│                     │
│   Task Master CLI   │
│                     │
╰─────────────────────╯


╭─────────────────────────────────╮
│  Project Setup & Configuration  │
╰─────────────────────────────────╯
    init                      [--name=<name>]                          Initialize a new project with Task       
                              [--description=<desc>] [-y]              Master structure                         
    models                                                             View current AI model configuration and  
                                                                       available models                         
    models --setup                                                     Run interactive setup to configure AI    
                                                                       models                                   
    models --set-main         <model_id>                               Set the primary model for task           
                                                                       generation                               
    models                    <model_id>                               Set the model for research operations    
    --set-research                                                                                              
    models                    <model_id>                               Set the fallback model (optional)        
    --set-fallback                                                                                              


╭───────────────────╮
│  Task Generation  │
╰───────────────────╯
    parse-prd                 --input=<file.txt> [--num-tasks=10]      Generate tasks from a PRD document       
    generate                                                           Create individual task files from        
                                                                       tasks.json                               


╭───────────────────╮
│  Task Management  │
╰───────────────────╯
    list                      [<status>|all] [--with-subtasks]         List all tasks - use "all" to show with  
                                                                       subtasks                                 
    set-status                <id> <status>                            Update task status (pending, done,       
                                                                       in-progress, review, deferred,           
                                                                       cancelled)                               
    sync-readme               [--with-subtasks]                        Export tasks to README.md with           
                              [--status=<status>]                      professional formatting                  
    update                    --from=<id> --prompt="<context>"         Update multiple tasks based on new       
                                                                       requirements                             
    update-task               <id> <prompt...>                         Update a single task (no quotes needed   
                                                                       for multi-word prompts)                  
    update-subtask            --id=<parentId.subtaskId>                Append additional information to a       
                              --prompt="<context>"                     subtask                                  
    add-task                  --prompt="<text>"                        Add a new task using AI                  
                              [--dependencies=<ids>]                                                            
                              [--priority=<priority>]                                                           
    remove-task               --id=<id> [-y]                           Permanently remove a task or subtask     


╭──────────────────────╮
│  Subtask Management  │
╰──────────────────────╯
    add-subtask               --parent=<id> --title="<title>"          Add a new subtask to a parent task       
                              [--description="<desc>"]                                                          
    add-subtask               --parent=<id> --task-id=<id>             Convert an existing task into a subtask  
    remove-subtask            --id=<parentId.subtaskId>                Remove a subtask (optionally convert to  
                              [--convert]                              standalone task)                         
    clear-subtasks            --id=<id>                                Remove all subtasks from specified tasks 
    clear-subtasks --all                                               Remove subtasks from all tasks           


╭─────────────────────────────╮
│  Task Analysis & Breakdown  │
╰─────────────────────────────╯
    analyze-complexity        [--research] [--threshold=5]             Analyze tasks and generate expansion     
                                                                       recommendations                          
    complexity-report         [--file=<path>]                          Display the complexity analysis report   
    expand                    --id=<id> [--num=5] [--research]         Break down tasks into detailed subtasks  
                              [--prompt="<context>"]                                                            
    expand --all              [--force] [--research]                   Expand all pending tasks with subtasks   
    research                  "<prompt>" [-i=<task_ids>]               Perform AI-powered research queries with 
                              [-f=<file_paths>] [-c="<context>"]       project context                          
                              [--tree] [-s=<save_file>]                                                         
                              [-d=<detail_level>]                                                               


╭─────────────────────────────╮
│  Task Navigation & Viewing  │
╰─────────────────────────────╯
    next                                                               Show the next task to work on based on   
                                                                       dependencies                             
    show                      <id>                                     Display detailed information about a     
                                                                       specific task                            


╭──────────────────╮
│  Tag Management  │
╰──────────────────╯
    tags                      [--show-metadata]                        List all available tags with task counts 
    add-tag                   <tagName> [--copy-from-current]          Create a new tag context for organizing  
                              [--copy-from=<tag>] [-d="<desc>"]        tasks                                    
    use-tag                   <tagName>                                Switch to a different tag context        
    delete-tag                <tagName> [--yes]                        Delete an existing tag and all its tasks 
    rename-tag                <oldName> <newName>                      Rename an existing tag                   
    copy-tag                  <sourceName> <targetName>                Copy an existing tag to create a new tag 
                              [-d="<desc>"]                            with the same tasks                      


╭─────────────────────────╮
│  Dependency Management  │
╰─────────────────────────╯
    add-dependency            --id=<id> --depends-on=<id>              Add a dependency to a task               
    remove-dependency         --id=<id> --depends-on=<id>              Remove a dependency from a task          
    validate-dependenci…                                               Identify invalid dependencies without    
                                                                       fixing them                              
    fix-dependencies                                                   Fix invalid dependencies automatically   


╭─────────────────╮
│  Configuration  │
╰─────────────────╯
    .taskmaster/config.json        AI model configuration file (project root)         Managed by models cmd     
    API Keys (.env)                API keys for AI providers (ANTHROPIC_API_KEY,      Required in .env file     
                                   etc.)                                                                        
    MCP Keys (mcp.json)            API keys for Cursor integration                    Required in .cursor/      


╭────────────────────────────────────────────────────────────────────────────────────────╮
│                                                                                        │
│   Quick Start:                                                                         │
│                                                                                        │
│   1. Create Project: task-master init                                                  │
│   2. Setup Models: task-master models --setup                                          │
│   3. Parse PRD: task-master parse-prd --input=<prd-file>                               │
│   4. List Tasks: task-master list                                                      │
│   5. Find Next Task: task-master next                                                  │
│                                                                                        │
╰────────────────────────────────────────────────────────────────────────────────────────╯


--- .taskmaster/tm_tags-help.txt ---
Usage: task-master tags [options] [command]

Manage tags for task organization

Options:
  -h, --help                        display help for command

Commands:
  list [options]                    List all tags with statistics (default
                                    action)
  add [options] <name>              Create a new tag
  use <name>                        Switch to a different tag
  remove [options] <name>           Remove a tag
  rename <oldName> <newName>        Rename a tag
  copy [options] <source> <target>  Copy a tag with all its tasks


--- .tickets/Oraclepack-CLI-agents.md ---

In Codex/Gemini CLI, the “live agent” inference is the model deciding what actions to take given the skill context (skills are discovered/activated and the model can inspect skill assets). [OpenAI Developers+2OpenAI Developers+2](https://developers.openai.com/codex/custom-prompts/?utm_source=chatgpt.com) In an MCP setup, the “live agent” inference is the model deciding which tools to call (and with what arguments) based on the tools’ schemas. [Model Context Protocol+3Model Context Protocol+3Model Context Protocol+3](https://modelcontextprotocol.io/specification/2025-11-25?utm_source=chatgpt.com)

Below is an “extensive” set of additional, unique LLM decision points you can introduce (or explicitly formalize) beyond “classify tickets/domains then feed into grouping”.

| ID | Stage | Class | LLM decision point | Inputs considered | Output / action produced |
| --- | --- | --- | --- | --- | --- |
| DP-01 | Pre-gen | Routing | Choose generator mode (tickets-grouped vs codebase-grouped vs gold) | User goal, repo state, available skills/tools | Selected generator + params |
| DP-02 | Pre-gen | Scope | Select root(s) to scan (ticket\_root/code\_root) | Working dir tree, config defaults | Root paths |
| DP-03 | Pre-gen | Scope | Decide include/exclude glob rules | Repo conventions, noise directories | Include/exclude patterns |
| DP-04 | Pre-gen | Scope | Decide whether to treat “loose” items as first-class candidates | Ticket placement quality, density | Loose-ticket policy |
| DP-05 | Pre-gen | Governance | Decide whether to require “strict schema mode” for outputs | Target environment strictness | Enforce extra validation gates |
| DP-06 | Pre-gen | Budgeting | Choose max pack size strategy (by tokens/bytes/files) | Model context limits, file sizes | Sharding plan |
| DP-07 | Pre-gen | Budgeting | Choose per-pack cap: number of tickets/files | Expected coherence vs coverage | Caps per pack |
| DP-08 | Pre-gen | Normalization | Decide ticket/file canonical title extraction rule | Noisy headings, filenames | Canonical titles for clustering |
| DP-09 | Pre-gen | Normalization | Choose text preprocessing (stopwords, stemming, code tokens) | Domain vocabulary | Feature extraction policy |
| DP-10 | Pre-gen | Dedup | Decide duplicate threshold policy (strict vs lenient) | Similarity signals | Threshold values |
| DP-11 | Pre-gen | Dedup | Decide duplicate merge strategy (merge vs link vs pick canonical) | Diff size, recency, metadata | Merge plan + canonical selection |
| DP-12 | Pre-gen | Dedup | Decide what “differences” to preserve when merging duplicates | Patch-like deltas | Delta retention format |
| DP-13 | Pre-gen | Clustering | Decide whether to use hierarchical topic taxonomy vs flat groups | Repo size, domain diversity | Taxonomy mode |
| DP-14 | Pre-gen | Clustering | Decide group count target | Ticket count, entropy | K groups target |
| DP-15 | Pre-gen | Clustering | Decide grouping algorithm choice (heuristic vs LLM-labeled vs hybrid) | Determinism needs, accuracy | Algorithm selection |
| DP-16 | Pre-gen | Clustering | Decide “ambiguous” assignment policy (multi-home vs best-fit) | Similarity ties | Assignment rule |
| DP-17 | Pre-gen | Clustering | Decide whether to create an “Unsorted / Needs triage” pack | Low-confidence items | Extra pack creation |
| DP-18 | Pre-gen | Naming | Generate group names optimized for human scanning | Group summaries | Group name strings |
| DP-19 | Pre-gen | Ordering | Decide pack order (by dependency, ROI, urgency, confidence) | Ticket metadata, heuristics | Pack sequence |
| DP-20 | Pre-gen | Ordering | Decide ticket order within pack (chronological vs dependency graph) | References among tickets | Ordered ticket list |
| DP-21 | Pre-gen | Context | Select “context bundle” files to attach per pack | Code references, paths mentioned | Attachment list |
| DP-22 | Pre-gen | Context | Decide whether to attach full files vs excerpts/summaries | Token budget, file size | Attachment granularity |
| DP-23 | Pre-gen | Context | Decide whether to generate a synthetic “pack brief” doc | Need for shared framing | Brief content |
| DP-24 | Pre-gen | Context | Decide whether to include prior run artifacts (outputs) as inputs | Existing out\_dir contents | Reuse plan |
| DP-25 | Pre-gen | Template | Choose template variant (tickets vs codebase vs mixed) | Pack type, audience | Template choice |
| DP-26 | Pre-gen | Template | Decide whether to inject organization standards into prompts (not pack shape) | Policy bundles, style rules | Prompt preamble content |
| DP-27 | Pre-gen | Template | Decide step allocation across subtopics (how many steps per subdomain) | Group composition | Step budget map |
| DP-28 | Pre-gen | Prompting | Choose prompt “stance” (audit-first vs implement-first vs design-first) | Risk, stage | Prompt style per step |
| DP-29 | Pre-gen | Prompting | Choose “evidence bar” (strict citations vs lightweight) | Audience and stakes | Evidence requirements |
| DP-30 | Pre-gen | Prompting | Decide per-step required outputs (files changed, diffs, JSON, etc.) | Tooling integration, downstream parser | Output spec per step |
| DP-31 | Pre-gen | Prompting | Decide if each step should be self-contained or rely on previous step outputs | Runtime environment, caching | Dependency policy |
| DP-32 | Pre-gen | Prompting | Decide whether to add “ask-user” gates for missing critical inputs | Missing file detection | Gate instructions in prompts |
| DP-33 | Pre-gen | Tooling | Choose which MCP tools to call during generation (list/validate/generate) | Tool availability, cost | Tool call plan [Model Context Protocol+1](https://modelcontextprotocol.io/specification/2025-06-18/server/tools?utm_source=chatgpt.com) |
| DP-34 | Pre-gen | Tooling | Decide oracle model/engine selection and parameters | Cost, latency, quality | `oracle` flags (model/temperature) |
| DP-35 | Pre-gen | Tooling | Decide whether to preflight `oracle` invocations (`--dry-run` summary) | Compatibility risk | Preflight on/off |
| DP-36 | Pre-gen | Compliance | Decide redaction policy for sensitive strings in prompts/attachments | Secrets risk | Redaction rules |
| DP-37 | Pre-gen | Observability | Decide trace/correlation ID scheme for packs/steps | Downstream logging needs | ID format + propagation plan |
| DP-38 | Pre-gen | Observability | Decide which metrics/log events must be emitted per stage | Debuggability requirements | Instrumentation checklist |
| DP-39 | Runtime | Execution planning | Decide run strategy (run-all vs selective) | Confidence map, cost | Step subset |
| DP-40 | Runtime | Execution planning | Decide concurrency / rate-limiting policy | Provider limits | Parallelism level |
| DP-41 | Runtime | Execution gating | Decide whether to halt on first failure vs continue collecting failures | Failure criticality | Fail-fast/continue |
| DP-42 | Runtime | Recovery | Decide retry policy per failure type (tool error vs content error) | Error classification | Retry plan |
| DP-43 | Runtime | Recovery | Decide “prompt patch” for retries (tighten constraints vs broaden context) | Failure analysis | Revised prompt text |
| DP-44 | Runtime | Recovery | Decide when to escalate to user for clarification | Low confidence / missing inputs | User question(s) |
| DP-45 | Runtime | Consistency | Decide whether to re-run earlier steps when later steps reveal contradictions | Cross-step inconsistency | Re-run selection |
| DP-46 | Runtime | Quality control | Decide acceptance criteria for a step output (format, completeness) | Output parsing/validation | Pass/fail verdict |
| DP-47 | Runtime | Quality control | Decide whether to auto-validate produced artifacts (lint/tests/validate) | Available checks | Validation commands |
| DP-48 | Runtime | Post-processing | Decide how to synthesize step outputs into a final report | Audience, required structure | Summary artifact |
| DP-49 | Runtime | Post-processing | Decide whether to generate PR/patch vs documentation-only output | Repo permissions, user intent | Output mode |
| DP-50 | Runtime | Post-processing | Decide how to resolve conflicting recommendations across steps | Conflicts detected | Resolution rationale |
| DP-51 | Runtime | Governance | Decide whether outputs meet policy/security standards before writing | Policy bundle | Block/allow + edits |
| DP-52 | Runtime | Caching | Decide whether to reuse cached groupings/packs vs regenerate | Cache keys, repo diffs | Cache decision |
| DP-53 | Runtime | Caching | Decide cache invalidation scope (one pack vs all) | Changed inputs | Invalidation plan |
| DP-54 | Runtime | Observability | Decide incident-style annotation (root cause, repro, next action) on failures | Error logs, outputs | Failure note artifact |
| DP-55 | Runtime | Observability | Decide what artifacts to persist (manifests, intermediate summaries) | Debug needs | Persist list |
| DP-56 | Runtime | Packaging | Decide how to shard outputs into “mini-packs” for follow-on runs | Token limits, dependencies | Mini-pack plan |
| DP-57 | Runtime | Packaging | Decide naming/versioning for generated packs | Date, domain, stability | Pack names + version tags |
| DP-58 | Runtime | UX | Decide “next best tool call” in MCP (validate vs list vs run vs regenerate) | Current state | Tool invocation [Model Context Protocol+1](https://modelcontextprotocol.io/docs/develop/build-server?utm_source=chatgpt.com) |
| DP-59 | Runtime | UX | Decide whether to present diffs, file lists, or narrative only | Reviewer preference | Presentation format |
| DP-60 | Runtime | Learning loop | Decide whether to extract new org heuristics from this run into a reusable profile | Repeated patterns | Proposed profile snippet |

If you want these to be real, enforceable decision points (not ad hoc), the typical pattern is: make each decision point produce a small structured artifact (JSON/YAML) and then make the deterministic scripts consume it, keeping the emitted pack Markdown schema unchanged. This fits both “skills” (model chooses actions using skill assets) [Gemini CLI+1](https://geminicli.com/docs/cli/skills/?utm_source=chatgpt.com) and MCP (model-controlled tool calls with typed schemas). [Model Context Protocol+1](https://modelcontextprotocol.io/specification/2025-06-18/server/tools?utm_source=chatgpt.com)

---



## Links discovered
- [OpenAI Developers+2OpenAI Developers+2](https://developers.openai.com/codex/custom-prompts/?utm_source=chatgpt.com)
- [Model Context Protocol+3Model Context Protocol+3Model Context Protocol+3](https://modelcontextprotocol.io/specification/2025-11-25?utm_source=chatgpt.com)
- [Model Context Protocol+1](https://modelcontextprotocol.io/specification/2025-06-18/server/tools?utm_source=chatgpt.com)
- [Model Context Protocol+1](https://modelcontextprotocol.io/docs/develop/build-server?utm_source=chatgpt.com)
- [Gemini CLI+1](https://geminicli.com/docs/cli/skills/?utm_source=chatgpt.com)

--- codefetch/oraclepack-op-mcp.md ---
<filetree>
Project Structure:
├── internal
│   ├── app
│   │   ├── app.go
│   │   ├── app_test.go
│   │   ├── run.go
│   │   └── run_test.go
│   ├── artifacts
│   │   ├── contract.go
│   │   └── contract_test.go
│   ├── cli
│   │   ├── cmds.go
│   │   ├── root.go
│   │   ├── run.go
│   │   └── verify_outputs.go
│   ├── config
│   │   ├── defaults.go
│   │   └── resolve.go
│   ├── dispatch
│   │   ├── classify.go
│   │   └── classify_test.go
│   ├── errors
│   │   ├── errors.go
│   │   └── errors_test.go
│   ├── exec
│   │   ├── flags.go
│   │   ├── inject.go
│   │   ├── inject_test.go
│   │   ├── oracle_scan.go
│   │   ├── oracle_scan_test.go
│   │   ├── oracle_validate.go
│   │   ├── oracle_validate_test.go
│   │   ├── runner.go
│   │   ├── runner_test.go
│   │   ├── sanitize.go
│   │   ├── sanitize_test.go
│   │   └── stream.go
│   ├── foundation
│   │   ├── atomic.go
│   │   ├── atomic_test.go
│   │   ├── clock.go
│   │   ├── clock_test.go
│   │   ├── config.go
│   │   ├── config_test.go
│   │   ├── errors.go
│   │   └── errors_test.go
│   ├── overrides
│   │   ├── merge.go
│   │   ├── merge_test.go
│   │   └── types.go
│   ├── pack
│   │   ├── bash_syntax_validator.go
│   │   ├── bash_syntax_validator_test.go
│   │   ├── bash_tooling_checks.go
│   │   ├── metadata.go
│   │   ├── output_expectations.go
│   │   ├── output_expectations_test.go
│   │   ├── output_validator.go
│   │   ├── output_validator_test.go
│   │   ├── parser.go
│   │   ├── parser_test.go
│   │   └── verify_report.go
│   ├── render
│   │   ├── render.go
│   │   └── render_test.go
│   ├── report
│   │   ├── generate.go
│   │   ├── io.go
│   │   ├── io_test.go
│   │   ├── report_test.go
│   │   └── types.go
│   ├── shell
│   │   ├── detect.go
│   │   ├── detect_test.go
│   │   ├── engine.go
│   │   ├── engine_test.go
│   │   ├── runner.go
│   │   └── runner_test.go
│   ├── state
│   │   ├── io.go
│   │   ├── io_test.go
│   │   ├── persist.go
│   │   ├── state_test.go
│   │   └── types.go
│   ├── templates
│   │   ├── template_test.go
│   │   ├── ticket-action-pack.md
│   │   └── ticket_action_pack.go
│   ├── tools
│   │   ├── types.go
│   │   └── types_test.go
│   ├── tui
│   │   ├── clipboard.go
│   │   ├── filter_test.go
│   │   ├── overrides_confirm.go
│   │   ├── overrides_flags.go
│   │   ├── overrides_flow.go
│   │   ├── overrides_steps.go
│   │   ├── overrides_url.go
│   │   ├── preview_test.go
│   │   ├── tui.go
│   │   ├── tui_test.go
│   │   ├── url_picker.go
│   │   ├── url_store.go
│   │   └── url_store_test.go
│   ├── types
│   │   ├── pack.go
│   │   ├── pack_test.go
│   │   └── verification.go
│   └── validate
│       ├── artifact_gate.go
│       ├── artifact_gate_test.go
│       ├── composite.go
│       ├── composite_test.go
│       ├── oracle.go
│       ├── presence.go
│       ├── presence_test.go
│       ├── report.go
│       └── types.go
└── oraclepack-mcp-server
    ├── .pytest_cache
    │   ├── v
    │   │   └── cache
    │   │       ├── lastfailed
    │   │       └── nodeids
    │   └── CACHEDIR.TAG
    ├── oraclepack_mcp_server
    │   ├── __init__.py
    │   ├── __main__.py
    │   ├── config.py
    │   ├── oraclepack_cli.py
    │   ├── security.py
    │   ├── server.py
    │   └── taskify.py
    ├── oraclepack_mcp_server.egg-info
    │   ├── PKG-INFO
    │   ├── SOURCES.txt
    │   ├── dependency_links.txt
    │   ├── entry_points.txt
    │   ├── requires.txt
    │   └── top_level.txt
    ├── tests
    │   ├── test_cli.py
    │   ├── test_config.py
    │   ├── test_integration.py
    │   ├── test_security.py
    │   └── test_taskify.py
    ├── inspector.config.json
    ├── pyproject.toml
    └── requirements.txt

</filetree>

<source_code>
oraclepack-mcp-server/inspector.config.json
```
{
  "mcpServers": {
    "oraclepack": {
      "command": "/home/user/projects/temp/oraclepack/oraclepack-mcp-server/venv/bin/python",
      "args": ["-m", "oraclepack_mcp_server", "--transport", "stdio"],
      "env": {
        "ORACLEPACK_BIN": "oraclepack",
        "ORACLEPACK_ALLOWED_ROOTS": "/home/user/projects/temp/oraclepack",
        "ORACLEPACK_ENABLE_EXEC": "1"
      }
    }
  }
}
```

oraclepack-mcp-server/pyproject.toml
```
[build-system]
requires = ["setuptools>=61.0"]
build-backend = "setuptools.build_meta"

[project]
name = "oraclepack-mcp-server"
version = "0.1.0"
description = "MCP wrapper for oraclepack CLI"
authors = [
    { name = "Oraclepack Contributor" }
]
dependencies = [
    "mcp[cli]>=0.1.0",
    "pydantic-settings>=2.0.0",
    "pydantic>=2.0.0",
]
requires-python = ">=3.10"

[project.scripts]
oraclepack-mcp = "oraclepack_mcp_server.__main__:main"
```

oraclepack-mcp-server/requirements.txt
```
mcp[cli]
pydantic-settings
pydantic>=2.0
```

oraclepack-mcp-server/.pytest_cache/CACHEDIR.TAG
```
Signature: 8a477f597d28d172789f06886806bc55
# This file is a cache directory tag created by pytest.
# For information about cache directory tags, see:
#	https://bford.info/cachedir/spec.html
```

oraclepack-mcp-server/oraclepack_mcp_server/__init__.py
```
```

oraclepack-mcp-server/oraclepack_mcp_server/__main__.py
```
import argparse
import asyncio
from .server import mcp

def main():
    parser = argparse.ArgumentParser(description="Oraclepack MCP Server")
    parser.add_argument(
        "--transport", 
        choices=["stdio", "streamable-http"], 
        default="stdio",
        help="MCP transport to use (default: stdio)"
    )
    parser.add_argument(
        "--host", 
        default="localhost",
        help="Host to bind for streamable-http (default: localhost)"
    )
    parser.add_argument(
        "--port", 
        type=int, 
        default=8000,
        help="Port to bind for streamable-http (default: 8000)"
    )
    
    args = parser.parse_args()
    
    if args.transport == "stdio":
        mcp.run(transport="stdio")
    elif args.transport == "streamable-http":
        # FastMCP.run(transport="sse") is what maps to streamable-http in python SDK
        mcp.run(transport="sse", host=args.host, port=args.port)

if __name__ == "__main__":
    main()
```

oraclepack-mcp-server/oraclepack_mcp_server/config.py
```
import os
from pathlib import Path
from typing import List, Union, Any
from pydantic import Field, field_validator
from pydantic_settings import BaseSettings, SettingsConfigDict

class Settings(BaseSettings):
    model_config = SettingsConfigDict(
        env_prefix="ORACLEPACK_",
        env_file=".env",
        extra="ignore"
    )

    bin: str = Field(default="oraclepack", description="Path to the oraclepack binary")
    # Use Union to prevent pydantic-settings from forcing JSON decode
    allowed_roots: Any = Field(
        default_factory=lambda: [Path.cwd()],
        description="List of allowed filesystem roots"
    )
    workdir: Path = Field(default_factory=Path.cwd, description="Working directory for execution")
    enable_exec: bool = Field(default=False, description="Enable execution tools")
    max_read_bytes: int = Field(default=65536, description="Max bytes for file read operations")
    character_limit: int = Field(default=32000, description="Max characters for tool outputs")

    @field_validator("allowed_roots", mode="before")
    @classmethod
    def parse_allowed_roots(cls, v: Any) -> List[Path]:
        if isinstance(v, str):
            # Support both colon and comma separation
            delimiter = ":" if ":" in v else ","
            return [Path(p.strip()) for p in v.split(delimiter) if p.strip()]
        if isinstance(v, list):
            return [Path(p) if isinstance(p, (str, Path)) else p for p in v]
        return v

settings = Settings()
```

oraclepack-mcp-server/oraclepack_mcp_server/oraclepack_cli.py
```
import asyncio
import time
from dataclasses import dataclass
from typing import List, Optional
from .config import settings

@dataclass
class OraclepackResult:
    ok: bool
    exit_code: int
    duration_s: float
    stdout: str
    stderr: str
    stdout_truncated: bool
    stderr_truncated: bool
    error: Optional[str] = None

def truncate_output(text: str, limit: int) -> tuple[str, bool]:
    """Truncates text to limit and returns (truncated_text, is_truncated)."""
    if len(text) > limit:
        return text[:limit], True
    return text, False

async def run_oraclepack(args: List[str], timeout: float = 3600.0) -> OraclepackResult:
    """
    Runs the oraclepack CLI with the given arguments.
    Handles timeouts and output truncation.
    """
    start_time = time.time()
    
    cmd = [settings.bin] + args
    
    try:
        # Create the subprocess
        process = await asyncio.create_subprocess_exec(
            *cmd,
            stdout=asyncio.subprocess.PIPE,
            stderr=asyncio.subprocess.PIPE,
            cwd=settings.workdir
        )
        
        try:
            # Wait for completion with timeout
            stdout_bytes, stderr_bytes = await asyncio.wait_for(process.communicate(), timeout=timeout)
            exit_code = process.returncode
        except asyncio.TimeoutError:
            # Handle timeout
            process.kill()
            await process.wait() # Ensure process is cleaned up
            stdout_bytes, stderr_bytes = b"", b"Timed out after " + str(timeout).encode() + b"s"
            exit_code = 124 # Standard timeout exit code
            
    except Exception as e:
        duration = time.time() - start_time
        return OraclepackResult(
            ok=False,
            exit_code=-1,
            duration_s=duration,
            stdout="",
            stderr="",
            stdout_truncated=False,
            stderr_truncated=False,
            error=str(e)
        )

    duration = time.time() - start_time
    
    stdout_raw = stdout_bytes.decode("utf-8", errors="replace")
    stderr_raw = stderr_bytes.decode("utf-8", errors="replace")
    
    stdout, stdout_truncated = truncate_output(stdout_raw, settings.character_limit)
    stderr, stderr_truncated = truncate_output(stderr_raw, settings.character_limit)
    
    return OraclepackResult(
        ok=(exit_code == 0),
        exit_code=exit_code,
        duration_s=duration,
        stdout=stdout,
        stderr=stderr,
        stdout_truncated=stdout_truncated,
        stderr_truncated=stderr_truncated
    )
```

oraclepack-mcp-server/oraclepack_mcp_server/security.py
```
import os
from pathlib import Path
from typing import List, Optional
from .config import settings

class SecurityError(Exception):
    """Raised for security-related violations."""
    pass

def is_exec_enabled() -> bool:
    """Returns True if execution tools are explicitly enabled."""
    return settings.enable_exec

def validate_path(path: str | Path) -> Path:
    """
    Resolves a path and ensures it resides within one of the allowed roots.
    Returns the resolved Path if valid, otherwise raises SecurityError.
    """
    p = Path(path)
    # Always normalize the path to remove .. and other noise
    try:
        # resolve() is best but it follows symlinks and requires existence for full resolution on some platforms.
        # abspath + normpath is a good fallback for non-existent files.
        resolved_p = Path(os.path.abspath(os.path.normpath(p)))
    except Exception as e:
        raise SecurityError(f"Could not resolve path '{path}': {e}")

    # Check if resolved_p starts with any of the allowed roots
    is_allowed = False
    for root in settings.allowed_roots:
        try:
            resolved_root = Path(os.path.abspath(os.path.normpath(root)))
            # commonpath returns the common prefix. 
            # If resolved_p is under resolved_root, commonpath should be resolved_root.
            common = os.path.commonpath([str(resolved_root), str(resolved_p)])
            if common == str(resolved_root):
                is_allowed = True
                break
        except ValueError:
            # Different drives on Windows or other incompatibilities
            continue

    if not is_allowed:
        raise SecurityError(f"Access to path '{path}' is not allowed by ORACLEPACK_ALLOWED_ROOTS.")

    return resolved_p

def safe_read_file(path: str | Path) -> tuple[str, bool]:
    """
    Validates the path and reads its content up to max_read_bytes.
    Returns (content, truncated).
    """
    resolved_path = validate_path(path)
    
    if not resolved_path.exists():
        raise SecurityError(f"Path '{path}' does not exist.")
    if not resolved_path.is_file():
        raise SecurityError(f"Path '{path}' is not a file.")

    with open(resolved_path, "rb") as f:
        content_bytes = f.read(settings.max_read_bytes + 1)
        truncated = len(content_bytes) > settings.max_read_bytes
        content = content_bytes[:settings.max_read_bytes].decode("utf-8", errors="replace")
        return content, truncated
```

oraclepack-mcp-server/oraclepack_mcp_server/server.py
```
import logging
import sys
import os
from mcp.server.fastmcp import FastMCP
from mcp.types import ToolAnnotations
from .config import settings
from . import security
from . import oraclepack_cli
from . import taskify

# Configure logging to stderr to avoid interleaving with stdio transport
logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
    stream=sys.stderr
)
logger = logging.getLogger("oraclepack-mcp-server")

# Initialize FastMCP
mcp = FastMCP("Oraclepack")

@mcp.tool(annotations=ToolAnnotations(readOnlyHint=True))
async def oraclepack_read_file(path: str) -> str:
    """
    Reads a file within allowed roots.
    Enforces ORACLEPACK_ALLOWED_ROOTS and ORACLEPACK_MAX_READ_BYTES.
    """
    content, truncated = security.safe_read_file(path)
    if truncated:
        return f"--- TRUNCATED (Max {settings.max_read_bytes} bytes) ---\n{content}"
    return content

@mcp.tool(annotations=ToolAnnotations(readOnlyHint=True))
async def oraclepack_list_packs(directory: str = ".") -> str:
    """Lists available oracle packs (*.md) in a directory."""
    resolved_dir = security.validate_path(directory)
    if not resolved_dir.is_dir():
        return f"Path '{directory}' is not a directory."
    
    packs = list(resolved_dir.glob("*.md"))
    if not packs:
        return f"No oracle packs found in '{directory}'."
    
    return "\n".join([p.name for p in packs])

@mcp.tool(annotations=ToolAnnotations(readOnlyHint=True))
async def oraclepack_validate_pack(pack_path: str) -> str:
    """Validates an oracle pack using the Go CLI."""
    resolved_path = security.validate_path(pack_path)
    result = await oraclepack_cli.run_oraclepack(["validate", str(resolved_path)])
    if not result.ok:
        return f"Validation failed:\n{result.stderr or result.error}"
    return "Pack is valid."

@mcp.tool(annotations=ToolAnnotations(readOnlyHint=True))
async def oraclepack_list_steps(pack_path: str) -> str:
    """Lists steps in an oracle pack."""
    resolved_path = security.validate_path(pack_path)
    result = await oraclepack_cli.run_oraclepack(["list", str(resolved_path)])
    if not result.ok:
        return f"Failed to list steps:\n{result.stderr or result.error}"
    return result.stdout

@mcp.tool(annotations=ToolAnnotations(destructiveHint=True, openWorldHint=True))
async def oraclepack_run_pack(pack_path: str, yes: bool = True, run_all: bool = True) -> str:
    """
    Runs an oracle pack. REQUIRES ORACLEPACK_ENABLE_EXEC=1.
    """
    if not security.is_exec_enabled():
        return "Execution is disabled. Set ORACLEPACK_ENABLE_EXEC=1 to enable."
    
    resolved_path = security.validate_path(pack_path)
    args = ["run", str(resolved_path), "--no-tui"]
    if yes: args.append("--yes")
    if run_all: args.append("--run-all")
    
    result = await oraclepack_cli.run_oraclepack(args)
    
    # Verbose Payload Rendering
    output = [f"# Execution Report: {pack_path}"]
    output.append(f"**Status**: {'✅ SUCCESS' if result.ok else '❌ FAILED'}")
    output.append(f"**Exit Code**: {result.exit_code}")
    output.append(f"**Duration**: {result.duration_s:.2f}s")
    
    if result.error:
        output.append(f"\n### Error\n{result.error}")
    
    if result.stdout:
        output.append("\n### Standard Output")
        output.append(f"```\n{result.stdout}\n```")
        if result.stdout_truncated:
            output.append("*Note: Output was truncated.*")
            
    if result.stderr:
        output.append("\n### Standard Error")
        output.append(f"```\n{result.stderr}\n```")
        if result.stderr_truncated:
            output.append("*Note: Error output was truncated.*")
            
    # Add artifact summary if successful or partially successful
    parent_dir = resolved_path.parent
    summary = taskify.artifacts_summary(parent_dir)
    output.append("\n### Artifacts Summary")
    for name, info in summary["artifacts"].items():
        if info:
            output.append(f"- {name}: FOUND")
        else:
            output.append(f"- {name}: NOT FOUND")
        
    return "\n".join(output)

@mcp.tool(annotations=ToolAnnotations(readOnlyHint=True))
async def oraclepack_taskify_detect_stage2(path: str = "auto") -> str:
    """Detects Stage-2 outputs."""
    out_dir, mode = taskify.detect_stage2(path, os.getcwd())
    if not out_dir:
        return f"Could not detect Stage-2 outputs in mode '{mode}'."
[TRUNCATED]
```

oraclepack-mcp-server/oraclepack_mcp_server/taskify.py
```
import os
import re
import glob
from pathlib import Path
from typing import List, Dict, Optional, Set, Tuple, Any
from dataclasses import dataclass, field

@dataclass
class Stage2ValidationResult:
    ok: bool
    missing: List[str] = field(default_factory=list)
    ambiguous: Dict[str, List[str]] = field(default_factory=dict)
    valid_files: List[str] = field(default_factory=list)

@dataclass
class ActionPackValidationResult:
    ok: bool
    error: Optional[str] = None
    steps: List[str] = field(default_factory=list)

def validate_stage2_dir(out_dir: str | Path) -> Stage2ValidationResult:
    """
    Enforces exactly one file per prefix 01..20.
    Returns Stage2ValidationResult with missing and ambiguous sets.
    """
    out_dir = Path(out_dir)
    missing = []
    ambiguous = {}
    valid_files = []
    
    for i in range(1, 21):
        pfx = f"{i:02d}"
        matches = list(out_dir.glob(f"{pfx}-*.md"))
        
        if not matches:
            missing.append(pfx)
        elif len(matches) > 1:
            ambiguous[pfx] = [m.name for m in matches]
        else:
            valid_files.append(matches[0].name)
            
    return Stage2ValidationResult(
        ok=(not missing and not ambiguous),
        missing=missing,
        ambiguous=ambiguous,
        valid_files=valid_files
    )

def detect_stage2(stage2_path: str, repo_root: str | Path) -> Tuple[Optional[Path], str]:
    """
    Resolves out_dir for Stage-2.
    Supports explicit dir, explicit file, and 'auto'.
    Returns (out_dir, mode).
    """
    repo_root = Path(repo_root)
    
    if stage2_path == "auto":
        candidates = [
            Path.cwd() / "oracle-out",
            repo_root / "oracle-out"
        ]
        
        docs_dir = repo_root / "docs"
        if docs_dir.exists():
            q_dirs = sorted(list(docs_dir.glob("oracle-questions-*")), reverse=True)
            for qd in q_dirs:
                candidates.append(qd / "oracle-out")
                
        for c in candidates:
            if c.exists() and c.is_dir():
                return c, "auto"
        return None, "auto"

    p = Path(stage2_path)
    if p.is_dir():
        return p, "explicit_dir"
    
    if p.is_file():
        parent = p.parent
        if (parent / "oracle-out").exists():
            return parent / "oracle-out", "explicit_file"
        return parent, "explicit_file"

    return None, "unknown"

def validate_action_pack(file_path: str | Path) -> ActionPackValidationResult:
    """
    Validates Stage-3 Action Pack constraints:
    - Single bash code fence
    - Step headers # NN)
    """
    try:
        content = Path(file_path).read_text()
    except Exception as e:
        return ActionPackValidationResult(ok=False, error=f"Read error: {e}")

    fences = re.findall(r"```bash", content)
    if len(fences) == 0:
        return ActionPackValidationResult(ok=False, error="No ```bash code fence found.")
    if len(fences) > 1:
        return ActionPackValidationResult(ok=False, error="Multiple ```bash code fences found. Only one is allowed.")

    steps = re.findall(r"^#\s*(\d+)\)", content, re.MULTILINE)
    if not steps:
        return ActionPackValidationResult(ok=False, error="No step headers (e.g. '# 01)') found.")

    return ActionPackValidationResult(ok=True, steps=steps)

def artifacts_summary(out_dir: str | Path) -> Dict[str, Any]:
    """
    Summarizes key Stage-3 artifacts.
    """
    out_dir = Path(out_dir)
    summary = {
        "out_dir": str(out_dir),
        "artifacts": {}
    }
    
    important_files = [
        "_actions.json",
        "_actions.md",
        "_tickets_index.json",
        "ticket-action-pack.md",
        "tm-complexity.json",
        "PRD.md",
        "prd.md",
        "tickets_prd.md"
    ]
    
    for f in important_files:
        found = False
        for search_path in [out_dir / f, out_dir.parent / f, out_dir.parent.parent / f]:
            if search_path.exists():
                summary["artifacts"][f] = {
                    "path": str(search_path),
                    "size": search_path.stat().st_size
                }
                found = True
                break
        if not found:
            summary["artifacts"][f] = None
            
    return summary

def generate_agent_prompt(pack_path: str, steps: List[str]) -> str:
    """
    Generates a prompt for an agent to run an action pack.
    """
    return f"""
# Oraclepack Action Pack Instructions

You are about to run an Oraclepack Action Pack: `{pack_path}`.
This pack contains {len(steps)} steps: {', '.join(steps)}.

## Recommended Workflow

1. **Verify**: Use `oraclepack_taskify_validate_action_pack` to ensure structure is correct.
[TRUNCATED]
```

oraclepack-mcp-server/oraclepack_mcp_server.egg-info/PKG-INFO
```
Metadata-Version: 2.4
Name: oraclepack-mcp-server
Version: 0.1.0
Summary: MCP wrapper for oraclepack CLI
Author: Oraclepack Contributor
Requires-Python: >=3.10
Requires-Dist: mcp[cli]>=0.1.0
Requires-Dist: pydantic-settings>=2.0.0
Requires-Dist: pydantic>=2.0.0
```

oraclepack-mcp-server/oraclepack_mcp_server.egg-info/SOURCES.txt
```
README.md
pyproject.toml
oraclepack_mcp_server/__init__.py
oraclepack_mcp_server/__main__.py
oraclepack_mcp_server/config.py
oraclepack_mcp_server/oraclepack_cli.py
oraclepack_mcp_server/security.py
oraclepack_mcp_server/server.py
oraclepack_mcp_server/taskify.py
oraclepack_mcp_server.egg-info/PKG-INFO
oraclepack_mcp_server.egg-info/SOURCES.txt
oraclepack_mcp_server.egg-info/dependency_links.txt
oraclepack_mcp_server.egg-info/entry_points.txt
oraclepack_mcp_server.egg-info/requires.txt
oraclepack_mcp_server.egg-info/top_level.txt
tests/test_config.py
```

oraclepack-mcp-server/oraclepack_mcp_server.egg-info/dependency_links.txt
```

```

oraclepack-mcp-server/oraclepack_mcp_server.egg-info/entry_points.txt
```
[console_scripts]
oraclepack-mcp = oraclepack_mcp_server.__main__:main
```

oraclepack-mcp-server/oraclepack_mcp_server.egg-info/requires.txt
```
mcp[cli]>=0.1.0
pydantic-settings>=2.0.0
pydantic>=2.0.0
```

oraclepack-mcp-server/oraclepack_mcp_server.egg-info/top_level.txt
```
oraclepack_mcp_server
```

oraclepack-mcp-server/tests/test_cli.py
```
import asyncio
import pytest
from unittest.mock import AsyncMock, patch, MagicMock
from oraclepack_mcp_server.oraclepack_cli import run_oraclepack, OraclepackResult
from oraclepack_mcp_server.config import settings

@pytest.mark.asyncio
async def test_run_oraclepack_success():
    # Mock process.communicate
    mock_stdout = b"success output"
    mock_stderr = b""
    
    with patch("asyncio.create_subprocess_exec", new_callable=AsyncMock) as mock_exec:
        mock_process = AsyncMock()
        mock_process.communicate.return_value = (mock_stdout, mock_stderr)
        mock_process.returncode = 0
        mock_exec.return_value = mock_process
        
        result = await run_oraclepack(["list", "pack.md"])
        
        assert result.ok is True
        assert result.exit_code == 0
        assert result.stdout == "success output"
        assert result.stdout_truncated is False

@pytest.mark.asyncio
async def test_run_oraclepack_timeout():
    with patch("asyncio.create_subprocess_exec", new_callable=AsyncMock) as mock_exec:
        mock_process = AsyncMock()
        
        async def slow_communicate():
            await asyncio.sleep(10)
            return (b"", b"")
            
        mock_process.communicate.side_effect = slow_communicate
        mock_exec.return_value = mock_process
        
        # Run with short timeout
        result = await run_oraclepack(["run", "pack.md"], timeout=0.1)
        
        assert result.ok is False
        assert result.exit_code == 124
        assert "Timed out" in result.stderr

@pytest.mark.asyncio
async def test_run_oraclepack_truncation(monkeypatch):
    monkeypatch.setattr(settings, "character_limit", 5)
    
    mock_stdout = b"1234567890"
    mock_stderr = b""
    
    with patch("asyncio.create_subprocess_exec", new_callable=AsyncMock) as mock_exec:
        mock_process = AsyncMock()
        mock_process.communicate.return_value = (mock_stdout, mock_stderr)
        mock_process.returncode = 0
        mock_exec.return_value = mock_process
        
        result = await run_oraclepack(["list"])
        
        assert result.stdout == "12345"
        assert result.stdout_truncated is True
```

oraclepack-mcp-server/tests/test_config.py
```
import os
import pytest
from pathlib import Path
from oraclepack_mcp_server.config import Settings

def test_default_config():
    # Clear env vars that might interfere
    for key in os.environ:
        if key.startswith("ORACLEPACK_"):
            del os.environ[key]
    
    # Reload settings or create a new instance
    # Note: the 'settings' object is already instantiated, so we test a new instance
    s = Settings()
    assert s.bin == "oraclepack"
    assert s.enable_exec is False
    assert s.character_limit == 32000
    assert Path.cwd() in s.allowed_roots

def test_env_override():
    os.environ["ORACLEPACK_BIN"] = "/custom/path/oraclepack"
    os.environ["ORACLEPACK_ENABLE_EXEC"] = "1"
    os.environ["ORACLEPACK_ALLOWED_ROOTS"] = "/tmp:/var/log"
    os.environ["ORACLEPACK_CHARACTER_LIMIT"] = "1000"
    
    s = Settings()
    assert s.bin == "/custom/path/oraclepack"
    assert s.enable_exec is True
    assert Path("/tmp") in s.allowed_roots
    assert Path("/var/log") in s.allowed_roots
    assert s.character_limit == 1000
    
    # Cleanup
    del os.environ["ORACLEPACK_BIN"]
    del os.environ["ORACLEPACK_ENABLE_EXEC"]
    del os.environ["ORACLEPACK_ALLOWED_ROOTS"]
    del os.environ["ORACLEPACK_CHARACTER_LIMIT"]
```

oraclepack-mcp-server/tests/test_integration.py
```
import asyncio
import pytest
import sys
from mcp.client.session import ClientSession
from mcp.client.stdio import stdio_client, StdioServerParameters
from mcp.types import CallToolRequestParams
import os
from pathlib import Path

@pytest.mark.asyncio
async def test_server_tools_list():
    """
    Spins up the server via stdio and checks if it can list its tools.
    """
    # Path to our package
    server_params = StdioServerParameters(
        command=sys.executable,
        args=["-m", "oraclepack_mcp_server", "--transport", "stdio"],
        env={**os.environ, "PYTHONPATH": str(Path(__file__).parent.parent)}
    )
    
    async with stdio_client(server_params) as (read, write):
        async with ClientSession(read, write) as session:
            await session.initialize()
            
            tools = await session.list_tools()
            tool_names = [t.name for t in tools.tools]
            
            assert "oraclepack_read_file" in tool_names
            assert "oraclepack_validate_pack" in tool_names
            assert "oraclepack_run_pack" in tool_names
            assert "oraclepack_taskify_detect_stage2" in tool_names
            assert "oraclepack_taskify_validate_stage2" in tool_names
            assert "oraclepack_taskify_validate_action_pack" in tool_names
            assert "oraclepack_taskify_artifacts_summary" in tool_names
            assert "oraclepack_taskify_generate_prompt" in tool_names

@pytest.mark.asyncio
async def test_oraclepack_read_file_unauthorized(tmp_path):
    """
    Verifies that the server enforces allowed roots.
    """
    root1 = tmp_path / "allowed"
    root1.mkdir()
    outside = tmp_path / "outside.txt"
    outside.touch()
    
    server_params = StdioServerParameters(
        command=sys.executable,
        args=["-m", "oraclepack_mcp_server", "--transport", "stdio"],
        env={
            **os.environ, 
            "PYTHONPATH": str(Path(__file__).parent.parent),
            "ORACLEPACK_ALLOWED_ROOTS": str(root1)
        }
    )
    
    async with stdio_client(server_params) as (read, write):
        async with ClientSession(read, write) as session:
            await session.initialize()
            
            # Try to read file outside allowed root
            result = await session.call_tool("oraclepack_read_file", {"path": str(outside)})
            
            # FastMCP returns the result even if there was an internal exception string returned
            assert "Access to path" in result.content[0].text
            assert "is not allowed" in result.content[0].text
```

oraclepack-mcp-server/tests/test_security.py
```
import os
import pytest
from pathlib import Path
from oraclepack_mcp_server.security import validate_path, is_exec_enabled, SecurityError, safe_read_file
from oraclepack_mcp_server.config import Settings, settings

def test_is_exec_enabled(monkeypatch):
    # Test with default (False)
    monkeypatch.setattr(settings, "enable_exec", False)
    assert is_exec_enabled() is False
    
    # Test with True
    monkeypatch.setattr(settings, "enable_exec", True)
    assert is_exec_enabled() is True

def test_validate_path_allowed(tmp_path, monkeypatch):
    # Setup tmp_path as an allowed root
    monkeypatch.setattr(settings, "allowed_roots", [tmp_path])
    
    # Path inside allowed root
    test_file = tmp_path / "test.txt"
    test_file.touch()
    
    assert validate_path(test_file) == test_file.resolve()
    assert validate_path(str(test_file)) == test_file.resolve()

def test_validate_path_denied(tmp_path, monkeypatch):
    # Setup allowed root
    root1 = tmp_path / "root1"
    root1.mkdir()
    monkeypatch.setattr(settings, "allowed_roots", [root1])
    
    # Path outside allowed root
    outside_file = tmp_path / "outside.txt"
    outside_file.touch()
    
    with pytest.raises(SecurityError, match="not allowed"):
        validate_path(outside_file)

def test_validate_path_traversal(tmp_path, monkeypatch):
    root1 = tmp_path / "root1"
    root1.mkdir()
    monkeypatch.setattr(settings, "allowed_roots", [root1])
    
    # Try to traverse out
    traversal_path = root1 / ".." / "outside.txt"
    
    with pytest.raises(SecurityError, match="not allowed"):
        validate_path(traversal_path)

def test_safe_read_file(tmp_path, monkeypatch):
    monkeypatch.setattr(settings, "allowed_roots", [tmp_path])
    monkeypatch.setattr(settings, "max_read_bytes", 10)
    
    test_file = tmp_path / "large.txt"
    test_file.write_text("0123456789ABCDE") # 15 chars
    
    content, truncated = safe_read_file(test_file)
    assert content == "0123456789"
    assert truncated is True
    
    small_file = tmp_path / "small.txt"
    small_file.write_text("hello")
    content, truncated = safe_read_file(small_file)
    assert content == "hello"
    assert truncated is False
```

oraclepack-mcp-server/tests/test_taskify.py
```
import pytest
from pathlib import Path
from oraclepack_mcp_server.taskify import validate_stage2_dir, detect_stage2, validate_action_pack

def test_validate_stage2_dir_ok(tmp_path):
    # Create 01..20 files
    for i in range(1, 21):
        (tmp_path / f"{i:02d}-test.md").touch()
    
    result = validate_stage2_dir(tmp_path)
    assert result.ok is True
    assert len(result.valid_files) == 20

def test_validate_stage2_dir_missing(tmp_path):
    # Missing 05 and 10
    for i in range(1, 21):
        if i in [5, 10]: continue
        (tmp_path / f"{i:02d}-test.md").touch()
    
    result = validate_stage2_dir(tmp_path)
    assert result.ok is False
    assert "05" in result.missing
    assert "10" in result.missing

def test_validate_stage2_dir_ambiguous(tmp_path):
    # Double 01
    (tmp_path / "01-a.md").touch()
    (tmp_path / "01-b.md").touch()
    for i in range(2, 21):
        (tmp_path / f"{i:02d}-test.md").touch()
        
    result = validate_stage2_dir(tmp_path)
    assert result.ok is False
    assert "01" in result.ambiguous
    assert len(result.ambiguous["01"]) == 2

def test_validate_action_pack_ok(tmp_path):
    pack_file = tmp_path / "pack.md"
    pack_file.write_text("""
# My Action Pack
# 01) Step One
```bash
echo hello
```
# 02) Step Two
""")
    result = validate_action_pack(pack_file)
    assert result.ok is True
    assert result.steps == ["01", "02"]

def test_validate_action_pack_multiple_fences(tmp_path):
    pack_file = tmp_path / "pack.md"
    pack_file.write_text("""
```bash
echo one
```
```bash
echo two
```
""")
    result = validate_action_pack(pack_file)
    assert result.ok is False
    assert "Multiple" in result.error

def test_detect_stage2_auto(tmp_path, monkeypatch):
    oracle_out = tmp_path / "oracle-out"
    oracle_out.mkdir()
    
    # Mock current working directory or just pass repo_root
    result_dir, mode = detect_stage2("auto", tmp_path)
    assert result_dir == oracle_out
    assert mode == "auto"
```

oraclepack-mcp-server/.pytest_cache/v/cache/lastfailed
```
{}
```

oraclepack-mcp-server/.pytest_cache/v/cache/nodeids
```
[
  "tests/test_cli.py::test_run_oraclepack_success",
  "tests/test_cli.py::test_run_oraclepack_timeout",
  "tests/test_cli.py::test_run_oraclepack_truncation",
  "tests/test_config.py::test_default_config",
  "tests/test_config.py::test_env_override",
  "tests/test_integration.py::test_oraclepack_read_file_unauthorized",
  "tests/test_integration.py::test_server_tools_list",
  "tests/test_security.py::test_is_exec_enabled",
  "tests/test_security.py::test_safe_read_file",
  "tests/test_security.py::test_validate_path_allowed",
  "tests/test_security.py::test_validate_path_denied",
  "tests/test_security.py::test_validate_path_traversal",
  "tests/test_taskify.py::test_detect_stage2_auto",
  "tests/test_taskify.py::test_validate_action_pack_multiple_fences",
  "tests/test_taskify.py::test_validate_action_pack_ok",
  "tests/test_taskify.py::test_validate_stage2_dir_ambiguous",
  "tests/test_taskify.py::test_validate_stage2_dir_missing",
  "tests/test_taskify.py::test_validate_stage2_dir_ok"
]
```

internal/app/app.go
```
package app

import (
	"fmt"
	"os"

	"github.com/user/oraclepack/internal/exec"
	"github.com/user/oraclepack/internal/pack"
	"github.com/user/oraclepack/internal/state"
	"github.com/user/oraclepack/internal/types"
)

// Config holds application-wide configuration.
type Config struct {
	PackPath              string
	StatePath             string
	ReportPath            string
	StopOnFail            bool
	Resume                bool
	Verbose               bool
	DryRun                bool
	OracleFlags           []string
	WorkDir               string
	OutDir                string // CLI override for output directory
	ROIThreshold          float64
	ROIMode               string // "over" or "under"
	OutputVerify          bool
	OutputRetries         int
	OutputRequireHeadings bool
	OutputChunkMode       string
}

// App orchestrates the execution flow.
type App struct {
	Config Config
	Pack   *types.Pack
	State  *state.RunState
	Runner *exec.Runner
}

// New creates a new application instance.
func New(cfg Config) *App {
	return &App{
		Config: cfg,
		Runner: exec.NewRunner(exec.RunnerOptions{
			WorkDir:     cfg.WorkDir,
			OracleFlags: cfg.OracleFlags,
		}),
	}
}

// LoadPack loads and validates the pack.
func (a *App) LoadPack() error {
	data, err := os.ReadFile(a.Config.PackPath)
	if err != nil {
		return err
	}

	p, err := pack.Parse(data)
	if err != nil {
		return err
	}

	if err := pack.Validate(p); err != nil {
		return err
	}

	a.Pack = p
	a.Pack.Source = a.Config.PackPath
	return nil
}

// LoadState loads or initializes the state.
func (a *App) LoadState() error {
	if a.Config.Resume {
		s, err := state.LoadState(a.Config.StatePath)
		if err == nil {
			a.State = s
			return nil
		}
	}

	a.State = &state.RunState{
		SchemaVersion: 1,
		StepStatuses:  make(map[string]state.StepStatus),
	}
	return nil
}

// Prepare resolves configuration and prepares the runtime environment.
func (a *App) Prepare() error {
	if a.Pack == nil {
		if err := a.LoadPack(); err != nil {
			return err
		}
	}

	// Resolve Output Directory
	// Precedence: CLI > Pack > Default (.)
	outDir := a.Config.OutDir
	if outDir == "" && a.Pack.OutDir != "" {
		outDir = a.Pack.OutDir
	}
	if outDir == "" {
		outDir = "."
	}

	// Provision Directory
	if err := os.MkdirAll(outDir, 0755); err != nil {
		return fmt.Errorf("failed to create output directory %s: %w", outDir, err)
	}

	// Update Runner
	// We do NOT set WorkDir to outDir, so execution happens in the project root.
	// This preserves relative path resolution for -f flags.
	// a.Runner.WorkDir = outDir

	// Add out_dir to Env so scripts can reference it
	a.Runner.Env = append(a.Runner.Env, fmt.Sprintf("out_dir=%s", outDir))

	return nil
}
```

internal/app/app_test.go
```
package app

import (
	"bytes"
	"context"
	"fmt"
	"os"
	"testing"
)

func TestApp_RunPlain(t *testing.T) {
	steps := buildSteps(20, "echo")
	packContent := `
# Test Pack
` + "```" + `bash
` + steps + `
` + "```" + `
`
	packFile := "test.md"
	stateFile := "test_state.json"
	reportFile := "test_report.json"
	defer os.Remove(packFile)
	defer os.Remove(stateFile)
	defer os.Remove(reportFile)

	os.WriteFile(packFile, []byte(packContent), 0644)

	cfg := Config{
		PackPath:   packFile,
		StatePath:  stateFile,
		ReportPath: reportFile,
	}

	a := New(cfg)
	if err := a.Prepare(); err != nil {
		t.Fatalf("Prepare failed: %v", err)
	}
	if err := a.LoadState(); err != nil {
		t.Fatalf("LoadState failed: %v", err)
	}

	var out bytes.Buffer
	err := a.RunPlain(context.Background(), &out)
	if err != nil {
		t.Fatalf("RunPlain failed: %v", err)
	}

	output := out.String()
	if !contains(output, "step 1") || !contains(output, "step 2") {
		t.Errorf("output missing steps: %s", output)
	}

	if _, err := os.Stat(stateFile); os.IsNotExist(err) {
		t.Error("state file was not created")
	}

	if _, err := os.Stat(reportFile); os.IsNotExist(err) {
		t.Error("report file was not created")
	}
}

func contains(s, substr string) bool {
	return len(s) >= len(substr) && (s == substr || (len(substr) > 0 && (s[:len(substr)] == substr || contains(s[1:], substr))))
}

func buildSteps(count int, cmd string) string {
	var b bytes.Buffer
	for i := 1; i <= count; i++ {
		if i < 10 {
			b.WriteString("# 0")
		} else {
			b.WriteString("# ")
		}
		b.WriteString(fmt.Sprintf("%d)\n", i))
		b.WriteString(cmd)
		b.WriteString(fmt.Sprintf(" \"step %d\"\n", i))
	}
	return b.String()
}
```

internal/app/run.go
```
package app

import (
	"context"
	"fmt"
	"io"
	"path/filepath"
	"strings"
	"time"

	"github.com/user/oraclepack/internal/pack"
	"github.com/user/oraclepack/internal/report"
	"github.com/user/oraclepack/internal/state"
	"github.com/user/oraclepack/internal/types"
)

func (a *App) RunPlain(ctx context.Context, out io.Writer) error {
	// Assumes a.Prepare() and a.LoadState() have been called by the CLI entrypoint.
	if a.Pack == nil {
		return fmt.Errorf("pack not loaded")
	}
	if a.State == nil {
		return fmt.Errorf("state not loaded")
	}

	if a.State.StartTime.IsZero() {
		a.State.StartTime = time.Now()
	}

	fmt.Fprintf(out, "Running pack: %s\n", a.Config.PackPath)
	fmt.Fprintf(out, "Output directory: %s\n", a.Runner.WorkDir)

	// Prelude
	if a.Pack.Prelude.Code != "" {
		fmt.Fprintln(out, "Executing prelude...")
		err := a.Runner.RunPrelude(ctx, &a.Pack.Prelude, out)
		a.recordWarnings()
		if err != nil {
			return fmt.Errorf("prelude failed: %w", err)
		}
	}

	for _, step := range a.Pack.Steps {
		a.State.CurrentStep = step.Number
		// Filter by ROI
		if a.Config.ROIThreshold > 0 {
			if a.Config.ROIMode == "under" {
				// "under" is strictly less than
				if step.ROI >= a.Config.ROIThreshold {
					fmt.Fprintf(out, "Skipping step %s (ROI %.2f >= %.2f)\n", step.ID, step.ROI, a.Config.ROIThreshold)
					continue
				}
			} else {
				// "over" is greater than or equal to (3.3 or higher)
				if step.ROI < a.Config.ROIThreshold {
					fmt.Fprintf(out, "Skipping step %s (ROI %.2f < %.2f)\n", step.ID, step.ROI, a.Config.ROIThreshold)
					continue
				}
			}
		}

		// Check resume
		if s, ok := a.State.StepStatuses[step.ID]; ok && s.Status == state.StatusSuccess {
			fmt.Fprintf(out, "Skipping step %s (already succeeded)\n", step.ID)
			continue
		}

		fmt.Fprintf(out, "\n>>> Step %s: %s\n", step.ID, step.OriginalLine)

		status := state.StepStatus{
			Status:    state.StatusRunning,
			StartedAt: time.Now(),
		}
		a.State.StepStatuses[step.ID] = status
		a.saveState()

		// Execute
		err := a.runStepWithOutputVerification(ctx, &step, out)
		a.recordWarnings()

		status.EndedAt = time.Now()
		if err != nil {
			status.Status = state.StatusFailed
			status.Error = err.Error()
			a.State.StepStatuses[step.ID] = status
			a.saveState()

			if a.Config.StopOnFail {
				a.finalize(out)
				return err
			}
			continue
		}

		status.Status = state.StatusSuccess
		status.ExitCode = 0
		a.State.StepStatuses[step.ID] = status
		a.saveState()
	}

	a.finalize(out)
	return nil
}

func (a *App) runStepWithOutputVerification(ctx context.Context, step *types.Step, out io.Writer) error {
	retries := a.Config.OutputRetries
	if retries < 0 {
		retries = 0
	}
	for attempt := 0; attempt <= retries; attempt++ {
		err := a.Runner.RunStep(ctx, step, out)
		if err != nil {
			return err
		}
		if !a.Config.OutputVerify {
			return nil
		}
		outputFailures := pack.VerifyStepOutputs(step, a.Config.OutputRequireHeadings, a.Config.OutputChunkMode)
		if len(outputFailures) == 0 {
			return nil
		}
		var failures []string
		for _, failure := range outputFailures {
			if failure.Error != "" {
				failures = append(failures, fmt.Sprintf("%s error: %s", failure.Path, failure.Error))
				continue
			}
			if len(failure.MissingTokens) > 0 {
				failures = append(failures, fmt.Sprintf("%s missing: %s", failure.Path, strings.Join(failure.MissingTokens, ", ")))
			}
		}
		if len(failures) == 0 {
			return nil
		}
		if attempt == retries {
			return fmt.Errorf(
				"output verification failed for step %s: %s",
				step.ID,
				strings.Join(failures, "; "),
			)
		}
		fmt.Fprintf(out, "⚠ output verification failed for step %s (%s); re-running (%d/%d)...\n",
			step.ID, strings.Join(failures, "; "), attempt+1, retries)
	}
	return nil
}

func (a *App) recordWarnings() {
	if a.State == nil || a.Runner == nil {
		return
	}
	warnings := a.Runner.DrainWarnings()
	if len(warnings) == 0 {
		return
	}
	for _, w := range warnings {
		a.State.Warnings = append(a.State.Warnings, state.Warning{
			Scope:   w.Scope,
			StepID:  w.StepID,
			Line:    w.Line,
			Token:   w.Token,
			Message: w.Message,
		})
	}
[TRUNCATED]
```

internal/app/run_test.go
```
package app

import (
	"bytes"
	"context"
	"os"
	"strconv"
	"strings"
	"testing"
)

func TestApp_RunPlain_ROI(t *testing.T) {
	steps := buildROISteps()
	packContent := `
# ROI Test Pack
` + "```" + `bash
` + steps + `
` + "```" + `
`
	packFile := "roi_test.md"
	defer os.Remove(packFile)
	os.WriteFile(packFile, []byte(packContent), 0644)

	// Test Case 1: Filter OVER 3.3 (Should run 5.0 and 3.3)
	t.Run("Filter Over 3.3", func(t *testing.T) {
		var out bytes.Buffer
		cfg := Config{
			PackPath:     packFile,
			ROIThreshold: 3.3,
			ROIMode:      "over",
		}
		app := New(cfg)
		if err := app.Prepare(); err != nil {
			t.Fatalf("Prepare failed: %v", err)
		}
		if err := app.LoadState(); err != nil {
			t.Fatalf("LoadState failed: %v", err)
		}
		if err := app.RunPlain(context.Background(), &out); err != nil {
			t.Fatalf("RunPlain failed: %v", err)
		}
		output := out.String()
		if !strings.Contains(output, "Step 01") {
			t.Error("expected Step 01 (5.0) to run")
		}
		if !strings.Contains(output, "Step 02") {
			t.Error("expected Step 02 (3.3) to run (inclusive)")
		}
		if strings.Contains(output, "Step 03") && !strings.Contains(output, "Skipping step 03") {
			t.Error("expected Step 03 (1.0) to be skipped")
		}
	})

	// Test Case 2: Filter UNDER 3.3 (Should run 1.0 only)
	t.Run("Filter Under 3.3", func(t *testing.T) {
		var out bytes.Buffer
		cfg := Config{
			PackPath:     packFile,
			ROIThreshold: 3.3,
			ROIMode:      "under",
		}
		app := New(cfg)
		if err := app.Prepare(); err != nil {
			t.Fatalf("Prepare failed: %v", err)
		}
		if err := app.LoadState(); err != nil {
			t.Fatalf("LoadState failed: %v", err)
		}
		if err := app.RunPlain(context.Background(), &out); err != nil {
			t.Fatalf("RunPlain failed: %v", err)
		}
		output := out.String()
		if strings.Contains(output, "Step 01") && !strings.Contains(output, "Skipping step 01") {
			t.Error("expected Step 01 (5.0) to be skipped")
		}
		if strings.Contains(output, "Step 02") && !strings.Contains(output, "Skipping step 02") {
			t.Error("expected Step 02 (3.3) to be skipped (exclusive)")
		}
		if !strings.Contains(output, "Step 03") {
			t.Error("expected Step 03 (1.0) to run")
		}
	})
}

func buildROISteps() string {
	var b strings.Builder
	for i := 1; i <= 20; i++ {
		id := i
		if id < 10 {
			b.WriteString("# 0")
		} else {
			b.WriteString("# ")
		}
		b.WriteString(strconv.Itoa(id))
		if i == 1 {
			b.WriteString(") ROI=5.0\n")
			b.WriteString("echo \"high\"\n\n")
			continue
		}
		if i == 2 {
			b.WriteString(") ROI=3.3\n")
			b.WriteString("echo \"threshold\"\n\n")
			continue
		}
		if i == 3 {
			b.WriteString(") ROI=1.0\n")
			b.WriteString("echo \"low\"\n\n")
			continue
		}
		b.WriteString(")\n")
		b.WriteString("echo \"step ")
		b.WriteString(strconv.Itoa(id))
		b.WriteString("\"\n\n")
	}
	return b.String()
}
```

internal/artifacts/contract.go
```
package artifacts

import (
	"fmt"
	"os"
	"path/filepath"

	"github.com/user/oraclepack/internal/foundation"
)

// Contract maps step IDs to required artifact paths.
type Contract map[string][]string

// DefaultContract returns the standard artifact contract.
func DefaultContract() Contract {
	base := ".oraclepack/ticketify"
	return Contract{
		"09": {filepath.Join(base, "next.json")},
		"10": {filepath.Join(base, "codex-implement.md")},
		"11": {filepath.Join(base, "codex-verify.md")},
		"12": {filepath.Join(base, "PR.md")},
	}
}

// EvaluateGates checks required artifacts for a given step.
func EvaluateGates(stepID string, contract Contract) error {
	paths, ok := contract[stepID]
	if !ok || len(paths) == 0 {
		return nil
	}
	var missing []string
	for _, p := range paths {
		info, err := os.Stat(p)
		if err != nil || info.IsDir() || info.Size() == 0 {
			missing = append(missing, p)
		}
	}
	if len(missing) > 0 {
		return fmt.Errorf("%w: %v", foundation.ErrArtifactMissing, missing)
	}
	return nil
}
```

internal/artifacts/contract_test.go
```
package artifacts

import (
	"os"
	"path/filepath"
	"testing"
)

func TestEvaluateGates(t *testing.T) {
	dir := t.TempDir()
	base := filepath.Join(dir, ".oraclepack", "ticketify")
	if err := os.MkdirAll(base, 0755); err != nil {
		t.Fatalf("mkdir: %v", err)
	}
	contract := Contract{
		"09": {filepath.Join(base, "next.json")},
	}

	// Missing file should error.
	if err := EvaluateGates("09", contract); err == nil {
		t.Fatal("expected missing artifact error")
	}

	// Create file and verify pass.
	path := filepath.Join(base, "next.json")
	if err := os.WriteFile(path, []byte("ok"), 0644); err != nil {
		t.Fatalf("write: %v", err)
	}
	if err := EvaluateGates("09", contract); err != nil {
		t.Fatalf("expected no error, got %v", err)
	}
}
```

internal/cli/cmds.go
```
package cli

import (
	"fmt"
	"os"

	"github.com/spf13/cobra"
	"github.com/user/oraclepack/internal/app"
	"github.com/user/oraclepack/internal/pack"
	"github.com/user/oraclepack/internal/validate"
)

var validateCmd = &cobra.Command{
	Use:   "validate [pack.md]",
	Short: "Validate an oracle pack",
	Args:  cobra.ExactArgs(1),
	RunE: func(cmd *cobra.Command, args []string) error {
		out := cmd.OutOrStdout()
		data, err := os.ReadFile(args[0])
		if err != nil {
			return err
		}
		p, err := pack.Parse(data)
		if err != nil {
			return err
		}
		if err := pack.Validate(p); err != nil {
			return err
		}
		findings, warning, err := pack.CheckPackScripts(p)
		if err != nil {
			return err
		}
		if warning != "" {
			fmt.Fprintf(out, "Warning: %s\n", warning)
		}
		if len(findings) > 0 {
			for _, finding := range findings {
				if finding.StepID != "" {
					fmt.Fprintf(out, "Step %s line %d: %s\n", finding.StepID, finding.Line, finding.Message)
				} else {
					fmt.Fprintf(out, "Line %d: %s\n", finding.Line, finding.Message)
				}
			}
			return fmt.Errorf("bash syntax validation failed")
		}
		cv := validate.CompositeValidator{}
		results := cv.ValidatePack(p)
		fmt.Fprintf(out, "Validated %d steps\n", len(results))
		for _, r := range results {
			fmt.Fprintf(out, "Step %s [%s] %s", r.StepID, r.ToolKind.Name(), r.Status)
			if r.Error != "" {
				fmt.Fprintf(out, " (%s)", r.Error)
			}
			fmt.Fprintln(out)
		}
		return nil
	},
}

var listCmd = &cobra.Command{
	Use:   "list [pack.md]",
	Short: "List steps in an oracle pack",
	Args:  cobra.ExactArgs(1),
	RunE: func(cmd *cobra.Command, args []string) error {
		cfg := app.Config{PackPath: args[0]}
		a := app.New(cfg)
		if err := a.LoadPack(); err != nil {
			return err
		}
		for _, s := range a.Pack.Steps {
			fmt.Printf("%s: %s\n", s.ID, s.OriginalLine)
		}
		return nil
	},
}

func init() {
	rootCmd.AddCommand(validateCmd)
	rootCmd.AddCommand(listCmd)
}
```

internal/cli/root.go
```
package cli

import (
	"fmt"
	"os"

	"github.com/spf13/cobra"
	"github.com/user/oraclepack/internal/errors"
)

var (
	noTUI     bool
	oracleBin string
	outDir    string
)

var rootCmd = &cobra.Command{
	Use:   "oraclepack",
	Short: "Oracle Pack Runner",
	Long:  `A polished TUI-driven runner for oracle-based interactive bash steps.`,
}

// Execute adds all child commands to the root command and sets flags appropriately.
func Execute() {
	if err := rootCmd.Execute(); err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(errors.ExitCode(err))
	}
}

func init() {
	rootCmd.PersistentFlags().BoolVar(&noTUI, "no-tui", false, "Disable the TUI and run in plain terminal mode")
	rootCmd.PersistentFlags().StringVar(&oracleBin, "oracle-bin", "oracle", "Path to the oracle binary")
	rootCmd.PersistentFlags().StringVarP(&outDir, "out-dir", "o", "", "Output directory for step execution")
}
```

internal/cli/run.go
```
package cli

import (
	"context"
	"fmt"
	"path/filepath"
	"strings"

	tea "github.com/charmbracelet/bubbletea"
	"github.com/spf13/cobra"
	"github.com/user/oraclepack/internal/app"
	"github.com/user/oraclepack/internal/config"
	"github.com/user/oraclepack/internal/pack"
	"github.com/user/oraclepack/internal/tui"
)

var (
	yes                   bool
	resume                bool
	stopOnFail            bool
	roiThreshold          float64
	roiMode               string
	runAll                bool
	outputVerify          bool
	outputRetries         int
	outputRequireHeadings bool
	outputChunkMode       string
)

var runCmd = &cobra.Command{
	Use:   "run [pack.md]",
	Short: "Run an oracle pack",
	Args:  cobra.ExactArgs(1),
	RunE: func(cmd *cobra.Command, args []string) error {
		packPath := args[0]

		// Setup paths
		base := strings.TrimSuffix(filepath.Base(packPath), filepath.Ext(packPath))
		statePath := base + ".state.json"
		reportPath := base + ".report.json"

		resolvedVerify, err := config.ResolveOutputVerify(outputVerify, cmd.Flags().Changed("output-verify"))
		if err != nil {
			return err
		}
		resolvedRetries, err := config.ResolveOutputRetries(outputRetries, cmd.Flags().Changed("output-retries"))
		if err != nil {
			return err
		}
		resolvedRequireHeadings, err := config.ResolveOutputRequireHeadings(outputRequireHeadings, cmd.Flags().Changed("output-require-headings"))
		if err != nil {
			return err
		}
		resolvedChunkMode, err := config.ResolveOutputChunkMode(outputChunkMode, cmd.Flags().Changed("output-chunk-mode"))
		if err != nil {
			return err
		}

		cfg := app.Config{
			PackPath:              packPath,
			StatePath:             statePath,
			ReportPath:            reportPath,
			Resume:                resume,
			StopOnFail:            stopOnFail,
			WorkDir:               ".",
			OutDir:                outDir,
			ROIThreshold:          roiThreshold,
			ROIMode:               roiMode,
			OutputVerify:          resolvedVerify,
			OutputRetries:         resolvedRetries,
			OutputRequireHeadings: resolvedRequireHeadings,
			OutputChunkMode:       resolvedChunkMode,
		}

		a := app.New(cfg)
		// Prepare the application (loads pack, resolves out_dir, provisions env)
		if err := a.Prepare(); err != nil {
			return err
		}

		if err := a.LoadState(); err != nil {
			return err
		}

		findings, warning, err := pack.CheckPackScripts(a.Pack)
		if err != nil {
			return err
		}
		if warning != "" {
			fmt.Fprintf(cmd.OutOrStdout(), "Warning: %s\n", warning)
		}
		if len(findings) > 0 {
			for _, finding := range findings {
				if finding.StepID != "" {
					fmt.Fprintf(cmd.OutOrStdout(), "Step %s line %d: %s\n", finding.StepID, finding.Line, finding.Message)
				} else {
					fmt.Fprintf(cmd.OutOrStdout(), "Line %d: %s\n", finding.Line, finding.Message)
				}
			}
			return fmt.Errorf("bash syntax validation failed")
		}

		if noTUI {
			out := cmd.OutOrStdout()
			fmt.Fprintf(out, "[Selected] %s\n", packPath)
			fmt.Fprintln(out, "[Ready] Parsed and validated pack")
			err := a.RunPlain(context.Background(), out)
			if err != nil {
				fmt.Fprintf(out, "[Completed] Failed: %v\n", err)
				return err
			}
			fmt.Fprintln(out, "[Completed] Success")
			return nil
		}

		m := tui.NewModel(a.Pack, a.Runner, a.State, cfg.StatePath, cfg.ROIThreshold, cfg.ROIMode, runAll, cfg.OutputVerify, cfg.OutputRetries, cfg.OutputRequireHeadings, cfg.OutputChunkMode)
		p := tea.NewProgram(m, tea.WithAltScreen())
		_, err = p.Run()
		return err
	},
}

func init() {
	runCmd.Flags().BoolVarP(&yes, "yes", "y", false, "Auto-approve all steps")
	runCmd.Flags().BoolVar(&resume, "resume", false, "Resume from last successful step")
	runCmd.Flags().BoolVar(&stopOnFail, "stop-on-fail", true, "Stop execution if a step fails")
	runCmd.Flags().Float64Var(&roiThreshold, "roi-threshold", 0.0, "Filter steps by ROI threshold")
	runCmd.Flags().StringVar(&roiMode, "roi-mode", "over", "ROI filter mode ('over' or 'under')")
	runCmd.Flags().BoolVar(&runAll, "run-all", false, "Automatically run all steps sequentially on start")
	runCmd.Flags().BoolVar(&outputVerify, "output-verify", config.DefaultOutputVerify, "Verify --write-output files contain required answer sections")
	runCmd.Flags().IntVar(&outputRetries, "output-retries", config.DefaultOutputRetries, "Retries for output verification failures")
	runCmd.Flags().BoolVar(&outputRequireHeadings, "output-require-headings", config.DefaultOutputRequireHeadings, "Require strict output headings when verifying outputs")
	runCmd.Flags().StringVar(&outputChunkMode, "output-chunk-mode", config.DefaultOutputChunkMode, "Output chunk verification mode: auto|single|multi")
	rootCmd.AddCommand(runCmd)
}
```

internal/cli/verify_outputs.go
```
package cli

import (
	"fmt"
	"os"

	"github.com/spf13/cobra"
	"github.com/user/oraclepack/internal/config"
	"github.com/user/oraclepack/internal/pack"
)

var (
	verifyOutputsEnabled      bool
	verifyOutputsRequireHeads bool
	verifyOutputsChunkMode    string
)

var verifyOutputsCmd = &cobra.Command{
	Use:   "verify-outputs [pack.md]",
	Short: "Verify --write-output files without executing steps",
	Args:  cobra.ExactArgs(1),
	RunE: func(cmd *cobra.Command, args []string) error {
		out := cmd.OutOrStdout()
		data, err := os.ReadFile(args[0])
		if err != nil {
			return err
		}

		p, err := pack.Parse(data)
		if err != nil {
			return err
		}
		if err := pack.Validate(p); err != nil {
			return err
		}

		verifyEnabled, err := config.ResolveOutputVerify(verifyOutputsEnabled, cmd.Flags().Changed("output-verify"))
		if err != nil {
			return err
		}
		if !verifyEnabled {
			fmt.Fprintln(out, "Output verification disabled (ORACLEPACK_OUTPUT_VERIFY=false).")
			return nil
		}
		requireHeadings, err := config.ResolveOutputRequireHeadings(verifyOutputsRequireHeads, cmd.Flags().Changed("output-require-headings"))
		if err != nil {
			return err
		}
		chunkMode, err := config.ResolveOutputChunkMode(verifyOutputsChunkMode, cmd.Flags().Changed("output-chunk-mode"))
		if err != nil {
			return err
		}

		report := pack.VerifyReport{
			TotalSteps: len(p.Steps),
		}

		for i := range p.Steps {
			step := &p.Steps[i]
			failures := pack.VerifyStepOutputs(step, requireHeadings, chunkMode)
			if len(failures) == 0 {
				continue
			}
			report.CheckedSteps++
			for _, failure := range failures {
				failure.StepID = step.ID
				report.Failures = append(report.Failures, failure)
			}
		}

		fmt.Fprint(out, pack.FormatVerifyReport(report))
		if len(report.Failures) > 0 {
			return fmt.Errorf("output verification failed")
		}
		return nil
	},
}

func init() {
	verifyOutputsCmd.Flags().BoolVar(&verifyOutputsEnabled, "output-verify", config.DefaultOutputVerify, "Verify --write-output files contain required answer sections")
	verifyOutputsCmd.Flags().BoolVar(&verifyOutputsRequireHeads, "output-require-headings", config.DefaultOutputRequireHeadings, "Require strict output headings when verifying outputs")
	verifyOutputsCmd.Flags().StringVar(&verifyOutputsChunkMode, "output-chunk-mode", config.DefaultOutputChunkMode, "Output chunk verification mode: auto|single|multi")
	rootCmd.AddCommand(verifyOutputsCmd)
}
```

internal/config/defaults.go
```
package config

const (
	EnvOutputVerify          = "ORACLEPACK_OUTPUT_VERIFY"
	EnvOutputRetries         = "ORACLEPACK_OUTPUT_RETRIES"
	EnvOutputRequireHeadings = "ORACLEPACK_OUTPUT_REQUIRE_HEADINGS"
	EnvOutputChunkMode       = "ORACLEPACK_OUTPUT_CHUNK_MODE"
)

const (
	DefaultOutputVerify          = false
	DefaultOutputRetries         = 0
	DefaultOutputRequireHeadings = false
	DefaultOutputChunkMode       = "auto"
)
```

internal/config/resolve.go
```
package config

import (
	"fmt"
	"os"
	"strconv"
	"strings"
)

// ResolveOutputVerify applies precedence: CLI flag > env var > default.
func ResolveOutputVerify(flagValue bool, flagSet bool) (bool, error) {
	if flagSet {
		return flagValue, nil
	}
	if val, ok := os.LookupEnv(EnvOutputVerify); ok {
		parsed, err := parseBoolish(val)
		if err != nil {
			return DefaultOutputVerify, fmt.Errorf("invalid %s: %w", EnvOutputVerify, err)
		}
		return parsed, nil
	}
	return DefaultOutputVerify, nil
}

// ResolveOutputRetries applies precedence: CLI flag > env var > default.
func ResolveOutputRetries(flagValue int, flagSet bool) (int, error) {
	if flagSet {
		return flagValue, nil
	}
	if val, ok := os.LookupEnv(EnvOutputRetries); ok {
		parsed, err := strconv.Atoi(strings.TrimSpace(val))
		if err != nil {
			return DefaultOutputRetries, fmt.Errorf("invalid %s: %w", EnvOutputRetries, err)
		}
		return parsed, nil
	}
	return DefaultOutputRetries, nil
}

// ResolveOutputRequireHeadings applies precedence: CLI flag > env var > default.
func ResolveOutputRequireHeadings(flagValue bool, flagSet bool) (bool, error) {
	if flagSet {
		return flagValue, nil
	}
	if val, ok := os.LookupEnv(EnvOutputRequireHeadings); ok {
		parsed, err := parseBoolish(val)
		if err != nil {
			return DefaultOutputRequireHeadings, fmt.Errorf("invalid %s: %w", EnvOutputRequireHeadings, err)
		}
		return parsed, nil
	}
	return DefaultOutputRequireHeadings, nil
}

// ResolveOutputChunkMode applies precedence: CLI flag > env var > default.
func ResolveOutputChunkMode(flagValue string, flagSet bool) (string, error) {
	if flagSet {
		return normalizeChunkMode(flagValue)
	}
	if val, ok := os.LookupEnv(EnvOutputChunkMode); ok {
		return normalizeChunkMode(val)
	}
	return normalizeChunkMode(DefaultOutputChunkMode)
}

func parseBoolish(raw string) (bool, error) {
	v := strings.ToLower(strings.TrimSpace(raw))
	switch v {
	case "1", "true", "yes", "on":
		return true, nil
	case "0", "false", "no", "off":
		return false, nil
	default:
		return false, fmt.Errorf("expected boolean (true/false, 1/0, on/off), got %q", raw)
	}
}

func normalizeChunkMode(raw string) (string, error) {
	v := strings.ToLower(strings.TrimSpace(raw))
	switch v {
	case "auto", "single", "multi":
		return v, nil
	case "":
		return DefaultOutputChunkMode, nil
	default:
		return "", fmt.Errorf("invalid %s: expected auto|single|multi, got %q", EnvOutputChunkMode, raw)
	}
}
```

internal/dispatch/classify.go
```
package dispatch

import (
	"regexp"
	"strings"

	"github.com/user/oraclepack/internal/tools"
)

var classifier = regexp.MustCompile(`^(\s*)(oracle|tm|task-master|codex|gemini)\b`)

// Classification describes a parsed command prefix.
type Classification struct {
	Kind    tools.ToolKind
	Prefix  string
	Command string
}

// Classify detects a supported tool prefix and returns the remaining command.
func Classify(line string) (Classification, bool) {
	m := classifier.FindStringSubmatch(line)
	if len(m) < 3 {
		return Classification{}, false
	}
	prefix := m[2]
	kind := toolKindFromPrefix(prefix)
	if kind == nil {
		return Classification{}, false
	}
	trimmed := strings.TrimSpace(line[len(m[1])+len(prefix):])
	return Classification{Kind: *kind, Prefix: prefix, Command: strings.TrimSpace(trimmed)}, true
}

func toolKindFromPrefix(prefix string) *tools.ToolKind {
	var kind tools.ToolKind
	switch prefix {
	case "oracle":
		kind = tools.ToolOracle
	case "tm":
		kind = tools.ToolTM
	case "task-master":
		kind = tools.ToolTaskMaster
	case "codex":
		kind = tools.ToolCodex
	case "gemini":
		kind = tools.ToolGemini
	default:
		return nil
	}
	return &kind
}
```

internal/dispatch/classify_test.go
```
package dispatch

import "testing"

func TestClassify(t *testing.T) {
	tests := []struct {
		line    string
		wantOK  bool
		wantCmd string
	}{
		{"oracle query \"hi\"", true, "query \"hi\""},
		{"  tm list", true, "list"},
		{"task-master next", true, "next"},
		{"codex exec \"x\"", true, "exec \"x\""},
		{"gemini run", true, "run"},
		{"echo hello", false, ""},
	}
	for _, tt := range tests {
		t.Run(tt.line, func(t *testing.T) {
			got, ok := Classify(tt.line)
			if ok != tt.wantOK {
				t.Fatalf("expected ok=%v got %v", tt.wantOK, ok)
			}
			if ok && got.Command != tt.wantCmd {
				t.Fatalf("expected cmd %q got %q", tt.wantCmd, got.Command)
			}
		})
	}
}
```

internal/errors/errors.go
```
package errors

import (
	"errors"
)

var (
	// ErrInvalidPack is returned when the Markdown pack is malformed.
	ErrInvalidPack = errors.New("invalid pack structure")
	// ErrExecutionFailed is returned when a shell command fails.
	ErrExecutionFailed = errors.New("execution failed")
	// ErrConfigInvalid is returned when CLI flags or environment variables are incorrect.
	ErrConfigInvalid = errors.New("invalid configuration")
)

// ExitCode returns the appropriate exit code for a given error.
func ExitCode(err error) int {
	if err == nil {
		return 0
	}

	if errors.Is(err, ErrConfigInvalid) {
		return 2
	}

	if errors.Is(err, ErrInvalidPack) {
		return 3
	}

	if errors.Is(err, ErrExecutionFailed) {
		return 4
	}

	return 1 // Generic error
}
```

internal/errors/errors_test.go
```
package errors

import (
	"errors"
	"fmt"
	"testing"
)

func TestExitCode(t *testing.T) {
	tests := []struct {
		name     string
		err      error
		expected int
	}{
		{"nil error", nil, 0},
		{"generic error", errors.New("generic"), 1},
		{"invalid pack", ErrInvalidPack, 3},
		{"execution failed", ErrExecutionFailed, 4},
		{"config invalid", ErrConfigInvalid, 2},
		{"wrapped invalid pack", fmt.Errorf("wrap: %w", ErrInvalidPack), 3},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if got := ExitCode(tt.err); got != tt.expected {
				t.Errorf("ExitCode() = %v, want %v", got, tt.expected)
			}
		})
	}
}
```

internal/exec/flags.go
```
package exec

import "strings"

// ApplyChatGPTURL ensures a single --chatgpt-url flag is present when url is set.
// It removes any existing --chatgpt-url/--browser-url flags and their values.
func ApplyChatGPTURL(flags []string, url string) []string {
	var out []string
	skipNext := false
	for _, f := range flags {
		if skipNext {
			skipNext = false
			continue
		}
		if f == "--chatgpt-url" || f == "--browser-url" {
			skipNext = true
			continue
		}
		if strings.HasPrefix(f, "--chatgpt-url=") || strings.HasPrefix(f, "--browser-url=") {
			continue
		}
		out = append(out, f)
	}
	if url != "" {
		out = append(out, "--chatgpt-url", url)
	}
	return out
}
```

internal/exec/inject.go
```
package exec

import "strings"

// InjectFlags scans a script and appends flags to any 'oracle' command invocation.
func InjectFlags(script string, flags []string) string {
	if len(flags) == 0 {
		return script
	}

	flagStr := strings.Join(flags, " ")

	lines := strings.Split(script, "\n")
	for i, line := range lines {
		trimmed := strings.TrimSpace(line)
		if strings.HasPrefix(trimmed, "#") {
			continue
		}

		insertIdx := oracleInsertIndex(line)
		if insertIdx == -1 {
			continue
		}

		lines[i] = insertFlagsInLine(line, insertIdx, flagStr)
	}

	return strings.Join(lines, "\n")
}

func oracleInsertIndex(line string) int {
	i := 0
	for i < len(line) && (line[i] == ' ' || line[i] == '\t') {
		i++
	}

	if !strings.HasPrefix(line[i:], "oracle") {
		return -1
	}

	end := i + len("oracle")
	if end < len(line) {
		next := line[end]
		if next != ' ' && next != '\t' {
			return -1
		}
	}

	return end
}

func insertFlagsInLine(line string, insertIdx int, flags string) string {
	prefix := line[:insertIdx]
	rest := line[insertIdx:]
	if rest == "" {
		return prefix + " " + flags
	}
	if rest[0] == ' ' || rest[0] == '\t' {
		return prefix + " " + flags + rest
	}
	return prefix + " " + flags + " " + rest
}
```

internal/exec/inject_test.go
```
package exec

import (
	"testing"
)

func TestInjectFlags(t *testing.T) {
	tests := []struct {
		name     string
		script   string
		flags    []string
		expected string
	}{
		{
			"simple injection",
			"oracle query 'hello'",
			[]string{"--verbose"},
			"oracle --verbose query 'hello'",
		},
		{
			"indented injection",
			"  oracle query 'hello'",
			[]string{"--verbose"},
			"  oracle --verbose query 'hello'",
		},
		{
			"no injection needed",
			"echo 'hello'",
			[]string{"--verbose"},
			"echo 'hello'",
		},
		{
			"multiple lines",
			"echo 'start'\noracle query\necho 'end'",
			[]string{"--debug"},
			"echo 'start'\noracle --debug query\necho 'end'",
		},
		{
			"multiline with continuation",
			"oracle \\\n  --json \\\n  --files",
			[]string{"--flag"},
			"oracle --flag \\\n  --json \\\n  --files",
		},
		{
			"multiline with args and continuation",
			"  oracle arg \\\n  --json",
			[]string{"--flag"},
			"  oracle --flag arg \\\n  --json",
		},
		{
			"commented command",
			"# oracle --json",
			[]string{"--verbose"},
			"# oracle --json",
		},
		{
			"oracle as part of word",
			"coracle query",
			[]string{"--verbose"},
			"coracle query",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got := InjectFlags(tt.script, tt.flags)
			if got != tt.expected {
				t.Errorf("InjectFlags() = %q, want %q", got, tt.expected)
			}
		})
	}
}
```

internal/exec/oracle_scan.go
```
package exec

import (
	"regexp"
	"strings"
)

var oracleCmdRegex = regexp.MustCompile(`^(\s*)(oracle)\b`)

// OracleInvocation represents a detected oracle command in a script.
type OracleInvocation struct {
	StartLine   int    // 0-based start line index
	EndLine     int    // 0-based end line index (inclusive)
	Raw         string // The full command string (joined if multi-line)
	Display     string // A trimmed version for UI display
	Indentation string // The leading whitespace
}

// ExtractOracleInvocations extracts oracle invocations from a script.
func ExtractOracleInvocations(script string) []OracleInvocation {
	var invocations []OracleInvocation
	lines := strings.Split(script, "\n")

	for i := 0; i < len(lines); i++ {
		line := lines[i]
		trimmed := strings.TrimSpace(line)

		// Skip comments
		if strings.HasPrefix(trimmed, "#") {
			continue
		}

		// Check for oracle command
		loc := oracleCmdRegex.FindStringSubmatchIndex(line)
		if loc != nil {
			startLine := i
			// Group 1 is the indentation
			indentation := line[loc[2]:loc[3]]

			var cmdBuilder strings.Builder
			cmdBuilder.WriteString(line)

			endLine := i
			// Handle line continuations
			// Check if line ends with backslash (ignoring trailing whitespace)
			for {
				if endLine+1 >= len(lines) {
					break
				}

				// Check current line for continuation
				currTrimmed := strings.TrimRight(lines[endLine], " \t")
				if !strings.HasSuffix(currTrimmed, "\\") {
					break
				}

				endLine++
				cmdBuilder.WriteString("\n")
				cmdBuilder.WriteString(lines[endLine])
			}

			raw := cmdBuilder.String()
			invocations = append(invocations, OracleInvocation{
				StartLine:   startLine,
				EndLine:     endLine,
				Raw:         raw,
				Display:     strings.TrimSpace(raw),
				Indentation: indentation,
			})

			i = endLine // Advance loop
		}
	}
	return invocations
}
```

internal/exec/oracle_scan_test.go
```
package exec

import (
	"reflect"
	"testing"
)

func TestExtractOracleInvocations(t *testing.T) {
	tests := []struct {
		name   string
		script string
		want   []OracleInvocation
	}{
		{
			name:   "Simple command",
			script: "oracle --json",
			want: []OracleInvocation{
				{StartLine: 0, EndLine: 0, Raw: "oracle --json", Display: "oracle --json", Indentation: ""},
			},
		},
		{
			name:   "Indented command",
			script: "  oracle --json",
			want: []OracleInvocation{
				{StartLine: 0, EndLine: 0, Raw: "  oracle --json", Display: "oracle --json", Indentation: "  "},
			},
		},
		{
			name: "Multiline command",
			script: `oracle \
  --json \
  --files`,
			want: []OracleInvocation{
				{StartLine: 0, EndLine: 2, Raw: `oracle \
  --json \
  --files`, Display: `oracle \
  --json \
  --files`, Indentation: ""},
			},
		},
		{
			name: "Commented command",
			script: `# oracle --json
oracle --real`,
			want: []OracleInvocation{
				{StartLine: 1, EndLine: 1, Raw: "oracle --real", Display: "oracle --real", Indentation: ""},
			},
		},
		{
			name: "Multiple commands",
			script: `
echo start
oracle --one
echo mid
oracle --two
echo end
`,
			want: []OracleInvocation{
				{StartLine: 2, EndLine: 2, Raw: "oracle --one", Display: "oracle --one", Indentation: ""},
				{StartLine: 4, EndLine: 4, Raw: "oracle --two", Display: "oracle --two", Indentation: ""},
			},
		},
		{
			name:   "Oraclepack prefix (should not match)",
			script: "oraclepack run",
			want:   nil,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got := ExtractOracleInvocations(tt.script)
			if !reflect.DeepEqual(got, tt.want) {
				t.Errorf("ExtractOracleInvocations() = %+v, want %+v", got, tt.want)
			}
		})
	}
}
```

internal/exec/oracle_validate.go
```
package exec

import (
	"bytes"
	"context"
	"os"
	"os/exec"
	"strings"

	"github.com/user/oraclepack/internal/overrides"
	"github.com/user/oraclepack/internal/types"
)

// ValidationError captures a failed oracle validation for a step.
type ValidationError struct {
	StepID       string
	Command      string
	ErrorMessage string
}

// ValidateOverrides runs oracle --dry-run summary for targeted steps.
func ValidateOverrides(
	ctx context.Context,
	steps []types.Step,
	over *overrides.RuntimeOverrides,
	baseline []string,
	opts RunnerOptions,
) ([]ValidationError, error) {
	if over == nil || over.ApplyToSteps == nil {
		return nil, nil
	}

	shell := opts.Shell
	if shell == "" {
		shell = "/bin/bash"
	}
	env := append(os.Environ(), opts.Env...)

	var results []ValidationError
	for _, step := range steps {
		if !over.ApplyToSteps[step.ID] {
			continue
		}

		invocations := ExtractOracleInvocations(step.Code)
		if len(invocations) == 0 {
			continue
		}

		flags := over.EffectiveFlags(step.ID, baseline)
		flags = append(flags, "--dry-run", "summary")

		for _, inv := range invocations {
			cmdStr := InjectFlags(inv.Raw, flags)
			msg, err := execDryRun(ctx, shell, opts.WorkDir, env, cmdStr)
			if err == nil {
				continue
			}

			results = append(results, ValidationError{
				StepID:       step.ID,
				Command:      cmdStr,
				ErrorMessage: msg,
			})
		}
	}

	return results, nil
}

func execDryRun(ctx context.Context, shell, workDir string, env []string, command string) (string, error) {
	if pathVal := findEnvValue(env, "PATH"); pathVal != "" {
		command = "export PATH=" + shellQuote(pathVal) + "; " + command
	}

	cmd := exec.CommandContext(ctx, shell, "-lc", command)
	if workDir != "" {
		cmd.Dir = workDir
	}
	cmd.Env = env

	var stdout, stderr bytes.Buffer
	cmd.Stdout = &stdout
	cmd.Stderr = &stderr

	err := cmd.Run()
	if err == nil {
		return stdout.String(), nil
	}
	if stderr.Len() > 0 {
		return strings.TrimSpace(stderr.String()), err
	}
	if stdout.Len() > 0 {
		return strings.TrimSpace(stdout.String()), err
	}
	return err.Error(), err
}

func findEnvValue(env []string, key string) string {
	prefix := key + "="
	for _, entry := range env {
		if strings.HasPrefix(entry, prefix) {
			return strings.TrimPrefix(entry, prefix)
		}
	}
	return ""
}

func shellQuote(value string) string {
	if value == "" {
		return "''"
	}
	return "'" + strings.ReplaceAll(value, "'", "'\\''") + "'"
}
```

internal/exec/oracle_validate_test.go
```
package exec

import (
	"context"
	"os"
	"path/filepath"
	"strings"
	"testing"

	"github.com/user/oraclepack/internal/overrides"
	"github.com/user/oraclepack/internal/types"
)

func TestValidateOverrides_Success(t *testing.T) {
	dir := t.TempDir()
	writeOracleStub(t, dir)

	steps := []types.Step{
		{ID: "01", Code: "oracle --ok"},
	}
	over := &overrides.RuntimeOverrides{
		ApplyToSteps: map[string]bool{"01": true},
	}

	_, err := ValidateOverrides(
		context.Background(),
		steps,
		over,
		[]string{"--base"},
		RunnerOptions{
			WorkDir: dir,
			Env:     []string{"PATH=" + dir + string(os.PathListSeparator) + os.Getenv("PATH")},
		},
	)
	if err != nil {
		t.Fatalf("ValidateOverrides failed: %v", err)
	}
}

func TestValidateOverrides_Error(t *testing.T) {
	dir := t.TempDir()
	writeOracleStub(t, dir)

	steps := []types.Step{
		{ID: "01", Code: "oracle --bad"},
	}
	over := &overrides.RuntimeOverrides{
		ApplyToSteps: map[string]bool{"01": true},
	}

	errs, err := ValidateOverrides(
		context.Background(),
		steps,
		over,
		nil,
		RunnerOptions{
			WorkDir: dir,
			Env:     []string{"PATH=" + dir + string(os.PathListSeparator) + os.Getenv("PATH")},
		},
	)
	if err != nil {
		t.Fatalf("ValidateOverrides failed: %v", err)
	}
	if len(errs) != 1 {
		t.Fatalf("expected 1 validation error, got %d", len(errs))
	}
	msg := errs[0].ErrorMessage
	if !strings.Contains(msg, "invalid flag") && !strings.Contains(msg, "unknown option") {
		t.Fatalf("unexpected error message: %q", msg)
	}
	if !strings.Contains(errs[0].Command, "--dry-run summary") {
		t.Fatalf("expected command to include --dry-run summary, got %q", errs[0].Command)
	}
}

func writeOracleStub(t *testing.T, dir string) {
	t.Helper()
	stub := `#!/bin/sh
has_dry=0
has_summary=0
for arg in "$@"; do
  if [ "$arg" = "--dry-run" ]; then has_dry=1; fi
  if [ "$arg" = "summary" ]; then has_summary=1; fi
  if [ "$arg" = "--bad" ]; then echo "invalid flag" 1>&2; exit 1; fi
done
if [ $has_dry -eq 0 ] || [ $has_summary -eq 0 ]; then
  echo "missing dry run" 1>&2
  exit 1
fi
exit 0
`
	path := filepath.Join(dir, "oracle")
	if err := os.WriteFile(path, []byte(stub), 0o755); err != nil {
		t.Fatalf("write oracle stub: %v", err)
	}
}
```

internal/exec/runner.go
```
package exec

import (
	"context"
	"fmt"
	"io"
	"os"
	"os/exec"

	"github.com/user/oraclepack/internal/errors"
	"github.com/user/oraclepack/internal/overrides"
	"github.com/user/oraclepack/internal/types"
)

// Runner handles the execution of shell scripts.
type Runner struct {
	Shell       string
	WorkDir     string
	Env         []string
	OracleFlags []string
	Overrides   *overrides.RuntimeOverrides
	ChatGPTURL  string
	warnings    []SanitizeWarning
}

// RunnerOptions configures a Runner.
type RunnerOptions struct {
	Shell       string
	WorkDir     string
	Env         []string
	OracleFlags []string
	Overrides   *overrides.RuntimeOverrides
	ChatGPTURL  string
}

// NewRunner creates a new Runner with options.
func NewRunner(opts RunnerOptions) *Runner {
	shell := opts.Shell
	if shell == "" {
		shell = "/bin/bash"
	}

	return &Runner{
		Shell:       shell,
		WorkDir:     opts.WorkDir,
		Env:         append(os.Environ(), opts.Env...),
		OracleFlags: opts.OracleFlags,
		Overrides:   opts.Overrides,
		ChatGPTURL:  opts.ChatGPTURL,
	}
}

// RunPrelude executes the prelude code.
func (r *Runner) RunPrelude(ctx context.Context, p *types.Prelude, logWriter io.Writer) error {
	script, warnings := SanitizeScript(p.Code, "prelude", "")
	r.recordWarnings(warnings, logWriter)
	return r.run(ctx, script, logWriter)
}

// RunStep executes a single step's code.
func (r *Runner) RunStep(ctx context.Context, s *types.Step, logWriter io.Writer) error {
	flags := ApplyChatGPTURL(r.OracleFlags, r.ChatGPTURL)
	if r.Overrides != nil {
		flags = r.Overrides.EffectiveFlags(s.ID, r.OracleFlags)
		flags = ApplyChatGPTURL(flags, r.ChatGPTURL)
	}
	code := InjectFlags(s.Code, flags)
	script, warnings := SanitizeScript(code, "step", s.ID)
	r.recordWarnings(warnings, logWriter)
	return r.run(ctx, script, logWriter)
}

func (r *Runner) recordWarnings(warnings []SanitizeWarning, logWriter io.Writer) {
	if len(warnings) == 0 {
		return
	}
	for _, w := range warnings {
		r.warnings = append(r.warnings, w)
		if logWriter != nil {
			scope := w.Scope
			if scope == "" {
				scope = "script"
			}
			step := ""
			if w.StepID != "" {
				step = " step " + w.StepID
			}
			_, _ = fmt.Fprintf(logWriter, "⚠ oraclepack: sanitized label in %s%s line %d: %s\n", scope, step, w.Line, w.Token)
		}
	}
}

// DrainWarnings returns any sanitizer warnings collected since the last call.
func (r *Runner) DrainWarnings() []SanitizeWarning {
	if len(r.warnings) == 0 {
		return nil
	}
	out := make([]SanitizeWarning, len(r.warnings))
	copy(out, r.warnings)
	r.warnings = nil
	return out
}

func (r *Runner) run(ctx context.Context, script string, logWriter io.Writer) error {
	// We use bash -lc to ensure login shell (paths, aliases, etc)
	cmd := exec.CommandContext(ctx, r.Shell, "-lc", script)
	cmd.Dir = r.WorkDir
	cmd.Env = r.Env

	// Standardize stdout and stderr to the logWriter
	cmd.Stdout = logWriter
	cmd.Stderr = logWriter

	err := cmd.Run()
	if err != nil {
		if ctx.Err() != nil {
			return ctx.Err()
		}
		return fmt.Errorf("%w: %v", errors.ErrExecutionFailed, err)
	}

	return nil
}
```

internal/exec/runner_test.go
```
package exec

import (
	"context"
	"strings"
	"testing"

	"github.com/user/oraclepack/internal/types"
)

func TestRunner_RunStep(t *testing.T) {
	r := NewRunner(RunnerOptions{})

	var lines []string
	lw := &LineWriter{
		Callback: func(line string) {
			lines = append(lines, line)
		},
	}

	step := &types.Step{
		Code: "echo 'hello world'",
	}

	err := r.RunStep(context.Background(), step, lw)
	if err != nil {
		t.Fatalf("RunStep failed: %v", err)
	}
	lw.Close()

	found := false
	for _, l := range lines {
		if strings.TrimSpace(l) == "hello world" {
			found = true
			break
		}
	}

	if !found {
		t.Errorf("expected 'hello world' in output, got: %v", lines)
	}
}

func TestRunner_ContextCancellation(t *testing.T) {
	r := NewRunner(RunnerOptions{})

	ctx, cancel := context.WithCancel(context.Background())
	cancel() // Cancel immediately

	step := &types.Step{
		Code: "sleep 10",
	}

	err := r.RunStep(ctx, step, nil)
	if err != context.Canceled {
		t.Errorf("expected context.Canceled, got %v", err)
	}
}
```

internal/exec/sanitize.go
```
package exec

import (
	osexec "os/exec"
	"regexp"
	"strings"
)

// SanitizeWarning captures a label line that was converted to a safe echo.
type SanitizeWarning struct {
	Scope   string
	StepID  string
	Line    int
	Token   string
	Message string
}

var (
	labelTokenRegex   = regexp.MustCompile(`^[A-Za-z][A-Za-z0-9_-]*$`)
	heredocStartRegex = regexp.MustCompile(`<<-?\s*['"]?([A-Za-z0-9_]+)['"]?`)
)

var shellBuiltins = map[string]bool{
	"alias":    true,
	"bg":       true,
	"break":    true,
	"cd":       true,
	"command":  true,
	"continue": true,
	"declare":  true,
	"dirs":     true,
	"echo":     true,
	"eval":     true,
	"exec":     true,
	"exit":     true,
	"export":   true,
	"fg":       true,
	"getopts":  true,
	"hash":     true,
	"help":     true,
	"jobs":     true,
	"local":    true,
	"popd":     true,
	"printf":   true,
	"pushd":    true,
	"pwd":      true,
	"readonly": true,
	"return":   true,
	"set":      true,
	"shift":    true,
	"source":   true,
	"test":     true,
	"trap":     true,
	"true":     true,
	"type":     true,
	"ulimit":   true,
	"umask":    true,
	"unalias":  true,
	"unset":    true,
	"wait":     true,
	"false":    true,
}

var shellKeywords = map[string]bool{
	"case":     true,
	"do":       true,
	"done":     true,
	"elif":     true,
	"else":     true,
	"esac":     true,
	"fi":       true,
	"for":      true,
	"function": true,
	"if":       true,
	"in":       true,
	"select":   true,
	"then":     true,
	"time":     true,
	"until":    true,
	"while":    true,
}

// SanitizeScript converts bare label-like lines into safe echo statements.
func SanitizeScript(script, scope, stepID string) (string, []SanitizeWarning) {
	if script == "" {
		return script, nil
	}

	lines := strings.Split(script, "\n")
	var warnings []SanitizeWarning
	var heredocEnd string

	for i, line := range lines {
		trimmed := strings.TrimSpace(line)
		if heredocEnd != "" {
			if trimmed == heredocEnd {
				heredocEnd = ""
			}
			continue
		}
		if trimmed == "" || strings.HasPrefix(trimmed, "#") {
			continue
		}

		if end := heredocStartToken(trimmed); end != "" {
			heredocEnd = end
			continue
		}

		fields := strings.Fields(trimmed)
		if len(fields) != 1 {
			continue
		}
		token := fields[0]
		if !labelTokenRegex.MatchString(token) {
			continue
		}
		lower := strings.ToLower(token)
		if shellBuiltins[lower] || shellKeywords[lower] {
			continue
		}
		if _, err := osexec.LookPath(token); err == nil {
			continue
		}

		indent := line[:len(line)-len(strings.TrimLeft(line, " \t"))]
		lines[i] = indent + "echo \"" + token + "\""
		warnings = append(warnings, SanitizeWarning{
			Scope:   scope,
			StepID:  stepID,
			Line:    i + 1,
			Token:   token,
			Message: "Converted bare label to echo",
		})
	}

	return strings.Join(lines, "\n"), warnings
}

func heredocStartToken(line string) string {
	match := heredocStartRegex.FindStringSubmatch(line)
	if len(match) < 2 {
		return ""
	}
	return match[1]
}
```

internal/exec/sanitize_test.go
```
package exec

import "testing"

func TestSanitizeScript_LabelLine(t *testing.T) {
	input := "GenerateReport\noracle --help\n"
	got, warnings := SanitizeScript(input, "step", "01")
	if len(warnings) != 1 {
		t.Fatalf("expected 1 warning, got %d", len(warnings))
	}
	if warnings[0].Token != "GenerateReport" {
		t.Fatalf("expected token GenerateReport, got %s", warnings[0].Token)
	}
	wantPrefix := "echo \"GenerateReport\""
	if got[:len(wantPrefix)] != wantPrefix {
		t.Fatalf("expected sanitized line to start with %q, got %q", wantPrefix, got)
	}
}

func TestSanitizeScript_BuiltinUnchanged(t *testing.T) {
	input := "echo\n"
	got, warnings := SanitizeScript(input, "step", "01")
	if len(warnings) != 0 {
		t.Fatalf("expected no warnings, got %d", len(warnings))
	}
	if got != input {
		t.Fatalf("expected script unchanged, got %q", got)
	}
}

func TestSanitizeScript_HeredocUnchanged(t *testing.T) {
	input := "cat <<'EOF'\nGenerateReport\nEOF\n"
	got, warnings := SanitizeScript(input, "step", "01")
	if len(warnings) != 0 {
		t.Fatalf("expected no warnings, got %d", len(warnings))
	}
	if got != input {
		t.Fatalf("expected heredoc unchanged, got %q", got)
	}
}
```

internal/exec/stream.go
```
package exec

import (
	"io"
)

// LineWriter is an io.Writer that splits output into lines and calls a callback.
type LineWriter struct {
	Callback func(string)
	buffer   []byte
}

func (w *LineWriter) Write(p []byte) (n int, err error) {
	for _, b := range p {
		if b == '\n' {
			w.Callback(string(w.buffer))
			w.buffer = w.buffer[:0]
		} else {
			w.buffer = append(w.buffer, b)
		}
	}
	return len(p), nil
}

// Close flushes any remaining data in the buffer.
func (w *LineWriter) Close() error {
	if len(w.buffer) > 0 {
		w.Callback(string(w.buffer))
		w.buffer = w.buffer[:0]
	}
	return nil
}

// MultiWriter handles multiple writers efficiently.
func MultiWriter(writers ...io.Writer) io.Writer {
	return io.MultiWriter(writers...)
}
```

internal/foundation/atomic.go
```
package foundation

import (
	"fmt"
	"os"
)

// WriteAtomic writes data to path atomically by writing to a temp file and renaming.
func WriteAtomic(path string, data []byte, perm os.FileMode) error {
	tempPath := path + ".tmp"
	if err := os.WriteFile(tempPath, data, perm); err != nil {
		return fmt.Errorf("write temp file: %w", err)
	}
	if err := os.Rename(tempPath, path); err != nil {
		_ = os.Remove(tempPath)
		return fmt.Errorf("rename temp file: %w", err)
	}
	return nil
}
```

internal/foundation/atomic_test.go
```
package foundation

import (
	"os"
	"path/filepath"
	"testing"
)

func TestWriteAtomic(t *testing.T) {
	dir := t.TempDir()
	path := filepath.Join(dir, "out.json")
	if err := WriteAtomic(path, []byte("hello"), 0644); err != nil {
		t.Fatalf("WriteAtomic: %v", err)
	}
	data, err := os.ReadFile(path)
	if err != nil {
		t.Fatalf("read: %v", err)
	}
	if string(data) != "hello" {
		t.Fatalf("unexpected contents: %q", string(data))
	}
}
```

internal/foundation/clock.go
```
package foundation

import "time"

// Clock abstracts time for deterministic testing.
type Clock interface {
	Now() time.Time
}

// RealClock uses the system clock.
type RealClock struct{}

// Now returns the current time.
func (RealClock) Now() time.Time { return time.Now() }

// MockClock returns a fixed time that can be advanced.
type MockClock struct {
	current time.Time
}

// NewMockClock initializes a mock clock with a starting time.
func NewMockClock(start time.Time) *MockClock {
	return &MockClock{current: start}
}

// Now returns the mock time.
func (m *MockClock) Now() time.Time { return m.current }

// Advance moves the mock time forward.
func (m *MockClock) Advance(d time.Duration) {
	m.current = m.current.Add(d)
}
```

internal/foundation/clock_test.go
```
package foundation

import (
	"testing"
	"time"
)

func TestMockClock(t *testing.T) {
	start := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC)
	m := NewMockClock(start)
	if !m.Now().Equal(start) {
		t.Fatalf("expected %v, got %v", start, m.Now())
	}
	m.Advance(2 * time.Hour)
	want := start.Add(2 * time.Hour)
	if !m.Now().Equal(want) {
		t.Fatalf("expected %v, got %v", want, m.Now())
	}
}
```

internal/foundation/config.go
```
package foundation

import (
	"encoding/json"
	"fmt"
	"os"
	"strconv"
)

// Config holds runtime settings that can be loaded from JSON and environment variables.
// Env values always take precedence over JSON values.
type Config struct {
	Name      string  `json:"name" env:"ORACLEPACK_NAME"`
	Retries   int     `json:"retries" env:"ORACLEPACK_RETRIES"`
	Enabled   bool    `json:"enabled" env:"ORACLEPACK_ENABLED"`
	Threshold float64 `json:"threshold" env:"ORACLEPACK_THRESHOLD"`
}

// LoadConfig loads configuration from a JSON file and then applies environment overrides.
// If path is empty, JSON loading is skipped and only env overrides are applied.
func LoadConfig(path string) (Config, error) {
	var cfg Config
	if path != "" {
		data, err := os.ReadFile(path)
		if err != nil {
			return Config{}, fmt.Errorf("read config: %w", err)
		}
		if err := json.Unmarshal(data, &cfg); err != nil {
			return Config{}, fmt.Errorf("parse config: %w", err)
		}
	}

	if v, ok := os.LookupEnv("ORACLEPACK_NAME"); ok {
		cfg.Name = v
	}
	if v, ok := os.LookupEnv("ORACLEPACK_RETRIES"); ok {
		parsed, err := strconv.Atoi(v)
		if err != nil {
			return Config{}, fmt.Errorf("parse ORACLEPACK_RETRIES: %w", err)
		}
		cfg.Retries = parsed
	}
	if v, ok := os.LookupEnv("ORACLEPACK_ENABLED"); ok {
		parsed, err := strconv.ParseBool(v)
		if err != nil {
			return Config{}, fmt.Errorf("parse ORACLEPACK_ENABLED: %w", err)
		}
		cfg.Enabled = parsed
	}
	if v, ok := os.LookupEnv("ORACLEPACK_THRESHOLD"); ok {
		parsed, err := strconv.ParseFloat(v, 64)
		if err != nil {
			return Config{}, fmt.Errorf("parse ORACLEPACK_THRESHOLD: %w", err)
		}
		cfg.Threshold = parsed
	}

	return cfg, nil
}
```

internal/foundation/config_test.go
```
package foundation

import (
	"os"
	"path/filepath"
	"testing"
)

func TestLoadConfigEnvOverrides(t *testing.T) {
	t.Setenv("ORACLEPACK_NAME", "env-name")
	t.Setenv("ORACLEPACK_RETRIES", "5")
	t.Setenv("ORACLEPACK_ENABLED", "true")
	t.Setenv("ORACLEPACK_THRESHOLD", "2.5")

	dir := t.TempDir()
	path := filepath.Join(dir, "config.json")
	if err := os.WriteFile(path, []byte(`{"name":"json-name","retries":1,"enabled":false,"threshold":1.0}`), 0644); err != nil {
		t.Fatalf("write json: %v", err)
	}

	cfg, err := LoadConfig(path)
	if err != nil {
		t.Fatalf("LoadConfig: %v", err)
	}

	if cfg.Name != "env-name" || cfg.Retries != 5 || cfg.Enabled != true || cfg.Threshold != 2.5 {
		t.Fatalf("env overrides not applied: %+v", cfg)
	}
}

func TestLoadConfigJSONOnly(t *testing.T) {
	dir := t.TempDir()
	path := filepath.Join(dir, "config.json")
	if err := os.WriteFile(path, []byte(`{"name":"json-name","retries":3,"enabled":true,"threshold":4.25}`), 0644); err != nil {
		t.Fatalf("write json: %v", err)
	}

	cfg, err := LoadConfig(path)
	if err != nil {
		t.Fatalf("LoadConfig: %v", err)
	}

	if cfg.Name != "json-name" || cfg.Retries != 3 || cfg.Enabled != true || cfg.Threshold != 4.25 {
		t.Fatalf("json load mismatch: %+v", cfg)
	}
}
```

internal/foundation/errors.go
```
package foundation

import "errors"

var (
	// ErrMissingBinary is returned when a required binary is not found on PATH.
	ErrMissingBinary = errors.New("missing binary")
	// ErrArtifactMissing is returned when an expected artifact is absent.
	ErrArtifactMissing = errors.New("artifact missing")
)
```

internal/foundation/errors_test.go
```
package foundation

import (
	"errors"
	"testing"
)

func TestCommonErrors(t *testing.T) {
	if ErrMissingBinary == nil || ErrArtifactMissing == nil {
		t.Fatal("expected error variables to be initialized")
	}
	if !errors.Is(ErrMissingBinary, ErrMissingBinary) {
		t.Fatal("errors.Is failed for ErrMissingBinary")
	}
	if !errors.Is(ErrArtifactMissing, ErrArtifactMissing) {
		t.Fatal("errors.Is failed for ErrArtifactMissing")
	}
}
```

internal/overrides/merge.go
```
package overrides

// EffectiveFlags calculates the final flags for a step.
func (r *RuntimeOverrides) EffectiveFlags(stepID string, baseline []string) []string {
	if r == nil || r.ApplyToSteps == nil || !r.ApplyToSteps[stepID] {
		return baseline
	}

	var effective []string

	// Map for removed flags
	removed := make(map[string]bool)
	for _, f := range r.RemovedFlags {
		removed[f] = true
	}

	// Filter baseline
	for _, flag := range baseline {
		if !removed[flag] {
			effective = append(effective, flag)
		}
	}

	// Append added flags
	effective = append(effective, r.AddedFlags...)

	// Inject ChatGPTURL
	if r.ChatGPTURL != "" {
		effective = append(effective, "--chatgpt-url", r.ChatGPTURL)
	}

	return effective
}
```

internal/overrides/merge_test.go
```
package overrides

import (
	"reflect"
	"testing"
)

func TestEffectiveFlags(t *testing.T) {
	tests := []struct {
		name      string
		overrides *RuntimeOverrides
		stepID    string
		baseline  []string
		want      []string
	}{
		{
			name:      "No overrides (nil)",
			overrides: nil,
			stepID:    "01",
			baseline:  []string{"--json"},
			want:      []string{"--json"},
		},
		{
			name: "Step not targeted",
			overrides: &RuntimeOverrides{
				ApplyToSteps: map[string]bool{"02": true},
				AddedFlags:   []string{"--verbose"},
			},
			stepID:   "01",
			baseline: []string{"--json"},
			want:     []string{"--json"},
		},
		{
			name: "Step targeted: Add flags",
			overrides: &RuntimeOverrides{
				ApplyToSteps: map[string]bool{"01": true},
				AddedFlags:   []string{"--verbose"},
			},
			stepID:   "01",
			baseline: []string{"--json"},
			want:     []string{"--json", "--verbose"},
		},
		{
			name: "Step targeted: Remove flags",
			overrides: &RuntimeOverrides{
				ApplyToSteps: map[string]bool{"01": true},
				RemovedFlags: []string{"--json"},
			},
			stepID:   "01",
			baseline: []string{"--json", "--other"},
			want:     []string{"--other"},
		},
		{
			name: "Step targeted: Add and Remove",
			overrides: &RuntimeOverrides{
				ApplyToSteps: map[string]bool{"01": true},
				AddedFlags:   []string{"--new"},
				RemovedFlags: []string{"--old"},
			},
			stepID:   "01",
			baseline: []string{"--old", "--keep"},
			want:     []string{"--keep", "--new"},
		},
		{
			name: "Step targeted: Inject ChatGPT URL",
			overrides: &RuntimeOverrides{
				ApplyToSteps: map[string]bool{"01": true},
				ChatGPTURL:   "https://chat.openai.com/share/123",
			},
			stepID:   "01",
			baseline: []string{"--json"},
			want:     []string{"--json", "--chatgpt-url", "https://chat.openai.com/share/123"},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got := tt.overrides.EffectiveFlags(tt.stepID, tt.baseline)
			if !reflect.DeepEqual(got, tt.want) {
				t.Errorf("EffectiveFlags() = %v, want %v", got, tt.want)
			}
		})
	}
}
```

internal/overrides/types.go
```
package overrides

// RuntimeOverrides holds configuration for runtime flag modifications.
type RuntimeOverrides struct {
	AddedFlags   []string        // Flags to append (e.g., "--model=gpt-4")
	RemovedFlags []string        // Flags to remove (e.g., "--json")
	ChatGPTURL   string          // Optional URL to inject via --chatgpt-url
	ApplyToSteps map[string]bool // Set of step IDs to apply overrides to. If empty, applies to none.
}
```

internal/pack/bash_syntax_validator.go
```
package pack

import (
	"regexp"
	"strings"

	"github.com/user/oraclepack/internal/types"
)

var orphanFlagRegex = regexp.MustCompile(`^\s*(?:-p|--prompt)(?:\s+.+)?$`)

// FindOrphanedFlags detects lines that contain only flags like -p/--prompt
// without being part of a continued command.
func FindOrphanedFlags(script string) []types.SyntaxFinding {
	if script == "" {
		return nil
	}

	lines := strings.Split(script, "\n")
	var findings []types.SyntaxFinding
	for i, line := range lines {
		trimmed := strings.TrimSpace(line)
		if trimmed == "" || strings.HasPrefix(trimmed, "#") {
			continue
		}
		if !orphanFlagRegex.MatchString(line) {
			continue
		}

		if i > 0 && lineContinues(lines[i-1]) {
			continue
		}

		findings = append(findings, types.SyntaxFinding{
			Line:    i + 1,
			Token:   strings.Fields(trimmed)[0],
			Message: "Orphaned flag without a preceding command or line continuation",
		})
	}
	return findings
}

func lineContinues(line string) bool {
	trimmed := strings.TrimRight(line, " \t")
	return strings.HasSuffix(trimmed, "\\")
}

// CheckPackScripts validates prelude and step scripts for orphaned flags and bash syntax.
func CheckPackScripts(p *types.Pack) ([]types.SyntaxFinding, string, error) {
	if p == nil {
		return nil, "", nil
	}

	var findings []types.SyntaxFinding
	var warnings []string

	// Prelude orphaned flags
	for _, finding := range FindOrphanedFlags(p.Prelude.Code) {
		findings = append(findings, finding)
	}

	// Step orphaned flags and bash -n checks
	for _, step := range p.Steps {
		for _, finding := range FindOrphanedFlags(step.Code) {
			finding.StepID = step.ID
			findings = append(findings, finding)
		}

		syntaxFindings, warning, err := CheckBashSyntax(step.Code)
		if err != nil {
			return findings, warning, err
		}
		if warning != "" {
			warnings = append(warnings, warning)
		}
		for _, finding := range syntaxFindings {
			finding.StepID = step.ID
			findings = append(findings, finding)
		}
	}

	// Prelude bash -n check
	preludeFindings, warning, err := CheckBashSyntax(p.Prelude.Code)
	if err != nil {
		return findings, warning, err
	}
	if warning != "" {
		warnings = append(warnings, warning)
	}
	findings = append(findings, preludeFindings...)

	return findings, strings.Join(uniqueStrings(warnings), "; "), nil
}

func uniqueStrings(values []string) []string {
	if len(values) == 0 {
		return nil
	}
	seen := make(map[string]bool, len(values))
	var out []string
	for _, value := range values {
		if value == "" || seen[value] {
			continue
		}
		seen[value] = true
		out = append(out, value)
	}
	return out
}
```

internal/pack/bash_syntax_validator_test.go
```
package pack

import (
	"os/exec"
	"testing"

	"github.com/user/oraclepack/internal/types"
)

func TestFindOrphanedFlags(t *testing.T) {
	script := "-p \"hello\"\n"
	findings := FindOrphanedFlags(script)
	if len(findings) != 1 {
		t.Fatalf("expected 1 finding, got %d", len(findings))
	}
	if findings[0].Line != 1 {
		t.Fatalf("expected line 1, got %d", findings[0].Line)
	}

	script = "oracle \\\n  -p \"ok\"\n"
	findings = FindOrphanedFlags(script)
	if len(findings) != 0 {
		t.Fatalf("expected no findings for continued line, got %d", len(findings))
	}
}

func TestCheckBashSyntax(t *testing.T) {
	if _, err := exec.LookPath("bash"); err != nil {
		t.Skip("bash not available")
	}

	script := "if true\n  echo hello\n"
	findings, warning, err := CheckBashSyntax(script)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if warning != "" {
		t.Fatalf("expected no warning, got %s", warning)
	}
	if len(findings) == 0 {
		t.Fatal("expected syntax findings")
	}
	if findings[0].Line == 0 {
		t.Fatal("expected line number in syntax finding")
	}
}

func TestCheckPackScriptsReportsStepID(t *testing.T) {
	p := &types.Pack{
		Steps: []types.Step{
			{ID: "01", Code: "-p \"oops\""},
		},
	}
	findings, _, err := CheckPackScripts(p)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if len(findings) != 1 {
		t.Fatalf("expected 1 finding, got %d", len(findings))
	}
	if findings[0].StepID != "01" {
		t.Fatalf("expected step ID 01, got %q", findings[0].StepID)
	}
}
```

internal/pack/bash_tooling_checks.go
```
package pack

import (
	"bytes"
	"os/exec"
	"regexp"
	"strconv"
	"strings"

	"github.com/user/oraclepack/internal/types"
)

var bashLineRegex = regexp.MustCompile(`line\s+(\d+)`)

// CheckBashSyntax runs "bash -n" on the script and returns syntax findings.
// If bash is not found, it returns a warning and no findings.
func CheckBashSyntax(script string) ([]types.SyntaxFinding, string, error) {
	if strings.TrimSpace(script) == "" {
		return nil, "", nil
	}

	if _, err := exec.LookPath("bash"); err != nil {
		return nil, "bash not found on PATH; skipping bash -n syntax check", nil
	}

	cmd := exec.Command("bash", "-n")
	cmd.Stdin = strings.NewReader(script)

	var stderr bytes.Buffer
	cmd.Stderr = &stderr
	cmd.Stdout = &stderr

	if err := cmd.Run(); err != nil {
		msg := strings.TrimSpace(stderr.String())
		line := 0
		if match := bashLineRegex.FindStringSubmatch(msg); len(match) > 1 {
			if parsed, parseErr := strconv.Atoi(match[1]); parseErr == nil {
				line = parsed
			}
		}
		return []types.SyntaxFinding{
			{
				Line:    line,
				Message: msg,
			},
		}, "", nil
	}

	return nil, "", nil
}
```

internal/pack/metadata.go
```
package pack

import (
	"fmt"

	"github.com/user/oraclepack/internal/errors"
	"github.com/user/oraclepack/internal/types"
)

// DeriveMetadata extracts configuration from the prelude.
func DeriveMetadata(p *types.Pack) {
	if p == nil {
		return
	}
	outDirMatch := outDirRegex.FindStringSubmatch(p.Prelude.Code)
	if len(outDirMatch) > 1 {
		p.OutDir = outDirMatch[1]
	}

	if writeOutputRegex.MatchString(p.Prelude.Code) {
		p.WriteOutput = true
	}
}

// Validate checks if the pack follows all rules.
func Validate(p *types.Pack) error {
	if p == nil {
		return fmt.Errorf("%w: pack is nil", errors.ErrInvalidPack)
	}
	if len(p.Steps) == 0 {
		return fmt.Errorf("%w: at least one step is required", errors.ErrInvalidPack)
	}
	if len(p.Steps) != 20 {
		return fmt.Errorf("%w: expected exactly 20 steps, got %d", errors.ErrInvalidPack, len(p.Steps))
	}

	seen := make(map[int]bool)
	for i, step := range p.Steps {
		if step.Number <= 0 {
			return fmt.Errorf("%w: invalid step number %d", errors.ErrInvalidPack, step.Number)
		}
		if seen[step.Number] {
			return fmt.Errorf("%w: duplicate step number %d", errors.ErrInvalidPack, step.Number)
		}
		seen[step.Number] = true

		// Ensure sequential starting from 1
		if step.Number != i+1 {
			return fmt.Errorf("%w: steps must be sequential starting from 1 (expected %d, got %d)", errors.ErrInvalidPack, i+1, step.Number)
		}
	}

	return nil
}
```

internal/pack/output_expectations.go
```
package pack

import (
	"regexp"
	"strings"

	"github.com/user/oraclepack/internal/types"
)

var (
	writeOutputPathRegex = regexp.MustCompile(`(?m)--write-output\s+["']?([^"'\s]+)["']?`)
	answerFormatRegex    = regexp.MustCompile(`(?i)answer\s+format`)
	directOnlyRegex      = regexp.MustCompile(`(?i)return\s+only[:\s]*direct\s+answer`)
)

// DetectOutputContract determines the expected response contract for a step.
func DetectOutputContract(step types.Step) types.OutputContract {
	if step.Code == "" {
		return types.OutputContractUnknown
	}

	hasAnswerFormat := answerFormatRegex.MatchString(step.Code)
	if !hasAnswerFormat {
		return types.OutputContractUnknown
	}

	if directOnlyRegex.MatchString(step.Code) {
		return types.OutputContractDirectAnswerOnly
	}

	return types.OutputContractAllSections
}

// StepOutputExpectations returns a map of output paths to required tokens.
// If no validation is needed, it returns nil.
func StepOutputExpectations(step *types.Step) map[string][]string {
	if step == nil {
		return nil
	}
	paths := ExtractWriteOutputPaths(step.Code)
	if len(paths) == 0 {
		return nil
	}

	if len(paths) > 1 {
		out := map[string][]string{}
		for _, path := range paths {
			switch {
			case strings.Contains(path, "-direct-answer"):
				out[path] = []string{"### Direct answer"}
			case strings.Contains(path, "-risks-unknowns"):
				out[path] = []string{"### Risks and unknowns"}
			case strings.Contains(path, "-next-experiment"):
				out[path] = []string{"### Next experiment"}
			case strings.Contains(path, "-missing-evidence"):
				out[path] = []string{"### Missing evidence"}
			}
		}
		if len(out) == 0 {
			return nil
		}
		return out
	}

	switch DetectOutputContract(*step) {
	case types.OutputContractDirectAnswerOnly:
		return map[string][]string{paths[0]: []string{"### Direct answer"}}
	case types.OutputContractAllSections:
		return map[string][]string{paths[0]: {
			"### Direct answer",
			"### Risks and unknowns",
			"### Next experiment",
			"### Missing evidence",
		}}
	default:
		return nil
	}
}

// StepOutputExpectationsWithMode returns expectations honoring chunk mode.
// chunkMode: auto (default), single (treat as single output), multi (force suffix mapping when multiple outputs).
func StepOutputExpectationsWithMode(step *types.Step, chunkMode string) map[string][]string {
	if step == nil {
		return nil
	}
	paths := ExtractWriteOutputPaths(step.Code)
	if len(paths) == 0 {
		return nil
	}
	mode := strings.ToLower(strings.TrimSpace(chunkMode))
	if mode == "" {
		mode = "auto"
	}

	switch mode {
	case "single":
		// Always treat as a single output (use first path).
		return expectationsForSingle(step, paths[0])
	case "multi":
		if len(paths) > 1 {
			return expectationsForSuffixes(paths)
		}
		return expectationsForSingle(step, paths[0])
	default: // auto
		if len(paths) > 1 {
			return expectationsForSuffixes(paths)
		}
		return expectationsForSingle(step, paths[0])
	}
}

func expectationsForSuffixes(paths []string) map[string][]string {
	out := map[string][]string{}
	for _, path := range paths {
		switch {
		case strings.Contains(path, "-direct-answer"):
			out[path] = []string{"### Direct answer"}
		case strings.Contains(path, "-risks-unknowns"):
			out[path] = []string{"### Risks and unknowns"}
		case strings.Contains(path, "-next-experiment"):
			out[path] = []string{"### Next experiment"}
		case strings.Contains(path, "-missing-evidence"):
			out[path] = []string{"### Missing evidence"}
		}
	}
	if len(out) == 0 {
		return nil
	}
	return out
}

func expectationsForSingle(step *types.Step, path string) map[string][]string {
	switch DetectOutputContract(*step) {
	case types.OutputContractDirectAnswerOnly:
		return map[string][]string{path: []string{"### Direct answer"}}
	case types.OutputContractAllSections:
		return map[string][]string{path: {
			"### Direct answer",
			"### Risks and unknowns",
			"### Next experiment",
			"### Missing evidence",
		}}
	default:
		return nil
	}
}

// ExtractWriteOutputPaths returns all --write-output paths found in the step code.
func ExtractWriteOutputPaths(code string) []string {
	matches := writeOutputPathRegex.FindAllStringSubmatch(code, -1)
	if len(matches) == 0 {
		return nil
	}
	paths := make([]string, 0, len(matches))
	for _, m := range matches {
		if len(m) >= 2 {
			paths = append(paths, m[1])
		}
	}
	return paths
}
```

internal/pack/output_expectations_test.go
```
package pack

import (
	"reflect"
	"testing"

	"github.com/user/oraclepack/internal/types"
)

func TestDetectOutputContract(t *testing.T) {
	step := types.Step{
		Code: "Answer format:\nReturn only: Direct answer\n",
	}
	if got := DetectOutputContract(step); got != types.OutputContractDirectAnswerOnly {
		t.Fatalf("expected direct-answer-only, got %q", got)
	}

	step = types.Step{
		Code: "Answer format:\n### Direct answer\n### Risks and unknowns\n",
	}
	if got := DetectOutputContract(step); got != types.OutputContractAllSections {
		t.Fatalf("expected all-sections, got %q", got)
	}

	step = types.Step{Code: "no format here"}
	if got := DetectOutputContract(step); got != types.OutputContractUnknown {
		t.Fatalf("expected unknown, got %q", got)
	}
}

func TestStepOutputExpectations_SingleOutput(t *testing.T) {
	step := &types.Step{
		Code: `oracle -p "x" --write-output "out.txt"
Answer format:
Return only: Direct answer`,
	}
	expectations := StepOutputExpectations(step)
	want := map[string][]string{"out.txt": []string{"### Direct answer"}}
	if !reflect.DeepEqual(expectations, want) {
		t.Fatalf("unexpected expectations: %#v", expectations)
	}

	step = &types.Step{
		Code: `oracle -p "x" --write-output "out.txt"
Answer format:
### Direct answer
### Risks and unknowns
### Next experiment
### Missing evidence`,
	}
	expectations = StepOutputExpectations(step)
	want = map[string][]string{"out.txt": []string{
		"### Direct answer",
		"### Risks and unknowns",
		"### Next experiment",
		"### Missing evidence",
	}}
	if !reflect.DeepEqual(expectations, want) {
		t.Fatalf("unexpected expectations: %#v", expectations)
	}
}

func TestStepOutputExpectations_MultiOutput(t *testing.T) {
	step := &types.Step{
		Code: `oracle --write-output "out-direct-answer.md" \
  --write-output 'out-risks-unknowns.md' \
  --write-output out-next-experiment.md \
  --write-output out-missing-evidence.md`,
	}
	expectations := StepOutputExpectations(step)
	if len(expectations) != 4 {
		t.Fatalf("expected 4 expectations, got %d", len(expectations))
	}
	if got := expectations["out-direct-answer.md"]; len(got) != 1 || got[0] != "### Direct answer" {
		t.Fatalf("unexpected direct-answer tokens: %#v", got)
	}
	if got := expectations["out-risks-unknowns.md"]; len(got) != 1 || got[0] != "### Risks and unknowns" {
		t.Fatalf("unexpected risks-unknowns tokens: %#v", got)
	}
	if got := expectations["out-next-experiment.md"]; len(got) != 1 || got[0] != "### Next experiment" {
		t.Fatalf("unexpected next-experiment tokens: %#v", got)
	}
	if got := expectations["out-missing-evidence.md"]; len(got) != 1 || got[0] != "### Missing evidence" {
		t.Fatalf("unexpected missing-evidence tokens: %#v", got)
	}
}
```

internal/pack/output_validator.go
```
package pack

import (
	"fmt"
	"os"
	"regexp"
	"strings"

	"github.com/user/oraclepack/internal/types"
)

// ValidateOutputFile checks whether the output file contains the required answer sections.
// It returns ok=false with a populated OutputFailure when validation fails.
func ValidateOutputFile(path string, requiredTokens []string) (bool, types.OutputFailure) {
	data, err := os.ReadFile(path)
	if err != nil {
		return false, types.OutputFailure{
			Path:  path,
			Error: err.Error(),
		}
	}

	content := string(data)
	var missing []string
	for _, tok := range requiredTokens {
		if !containsToken(content, tok) {
			missing = append(missing, tok)
		}
	}

	if len(missing) > 0 {
		return false, types.OutputFailure{
			Path:          path,
			MissingTokens: missing,
		}
	}

	return true, types.OutputFailure{}
}

func containsToken(content, token string) bool {
	if strings.Contains(content, token) {
		return true
	}

	heading := strings.TrimSpace(strings.TrimPrefix(token, "###"))
	if heading == token {
		return false
	}

	alts := []string{heading}
	switch strings.ToLower(heading) {
	case "direct answer":
		alts = append(alts, "answer")
	case "risks and unknowns":
		alts = append(alts, "risks/unknowns", "risks & unknowns")
	case "next experiment":
		alts = append(alts, "next smallest concrete experiment")
	case "missing evidence":
		alts = append(alts, "if evidence is insufficient")
	}

	for _, alt := range alts {
		if alt == "" {
			continue
		}
		pat := `(?im)^\s*#{0,3}\s*` + regexp.QuoteMeta(alt) + `\b`
		if regexp.MustCompile(pat).MatchString(content) {
			return true
		}
	}

	return false
}

// VerifyStepOutputs validates output files for a step. If requireHeadings is false,
// it only checks that output files exist and are non-empty.
func VerifyStepOutputs(step *types.Step, requireHeadings bool, chunkMode string) []types.OutputFailure {
	if step == nil {
		return nil
	}
	paths := ExtractWriteOutputPaths(step.Code)
	if len(paths) == 0 {
		return nil
	}

	if !requireHeadings {
		paths = selectChunkPaths(paths, chunkMode)
		var failures []types.OutputFailure
		for _, path := range paths {
			info, err := os.Stat(path)
			if err != nil {
				failures = append(failures, types.OutputFailure{
					Path:  path,
					Error: err.Error(),
				})
				continue
			}
			if info.Size() == 0 {
				failures = append(failures, types.OutputFailure{
					Path:  path,
					Error: fmt.Sprintf("output file is empty: %s", path),
				})
			}
		}
		return failures
	}

	expectations := StepOutputExpectationsWithMode(step, chunkMode)
	if len(expectations) == 0 {
		return nil
	}
	var failures []types.OutputFailure
	for path, required := range expectations {
		ok, failure := ValidateOutputFile(path, required)
		if !ok {
			failures = append(failures, failure)
		}
	}
	return failures
}

func selectChunkPaths(paths []string, chunkMode string) []string {
	if len(paths) == 0 {
		return paths
	}
	mode := strings.ToLower(strings.TrimSpace(chunkMode))
	if mode == "" {
		mode = "auto"
	}
	switch mode {
	case "single":
		return []string{paths[0]}
	default:
		return paths
	}
}
```

internal/pack/output_validator_test.go
```
package pack

import (
	"os"
	"path/filepath"
	"testing"

	"github.com/user/oraclepack/internal/types"
)

func TestValidateOutputFile_RelaxedHeadings(t *testing.T) {
	content := `Direct answer

Risks/unknowns

Next smallest concrete experiment

If evidence is insufficient`

	dir := t.TempDir()
	path := filepath.Join(dir, "out.md")
	if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
		t.Fatalf("write output: %v", err)
	}

	ok, failure := ValidateOutputFile(path, []string{
		"### Direct answer",
		"### Risks and unknowns",
		"### Next experiment",
		"### Missing evidence",
	})
	if !ok {
		t.Fatalf("expected relaxed headings to pass, failure: %#v", failure)
	}

	step := &types.Step{Code: `oracle --write-output "` + path + `"`}
	failures := VerifyStepOutputs(step, true, "single")
	if len(failures) != 0 {
		t.Fatalf("expected no failures in single chunk mode, got %#v", failures)
	}
}
```

internal/pack/parser.go
```
package pack

import (
	"bufio"
	"fmt"
	"regexp"
	"strconv"
	"strings"

	"github.com/user/oraclepack/internal/errors"
	"github.com/user/oraclepack/internal/types"
)

var (
	bashFenceRegex = regexp.MustCompile("(?s)```bash\n(.*?)\n```")
	// Updated regex to support ")", " —", and " -" separators
	stepHeaderRegex  = regexp.MustCompile(`^#\s*(\d{2})(?:\)|[\s]+[—-])`)
	roiRegex         = regexp.MustCompile(`ROI=(\d+(\.\d+)?)`)
	impactRegex      = regexp.MustCompile(`^#\s*Impact:\s*(.+)$`)
	outDirRegex      = regexp.MustCompile(`(?m)^out_dir=["']?([^"'\s]+)["']?`)
	writeOutputRegex = regexp.MustCompile(`(?m)--write-output`)
)

// Parse reads a Markdown content and returns a Pack.
func Parse(content []byte) (*types.Pack, error) {
	match := bashFenceRegex.FindSubmatch(content)
	if match == nil || len(match) < 2 {
		return nil, fmt.Errorf("%w: no bash code block found", errors.ErrInvalidPack)
	}

	bashCode := string(match[1])
	pack := &types.Pack{}

	scanner := bufio.NewScanner(strings.NewReader(bashCode))
	var currentStep *types.Step
	var preludeLines []string
	var inSteps bool

	for scanner.Scan() {
		line := scanner.Text()
		headerMatch := stepHeaderRegex.FindStringSubmatch(strings.TrimSpace(line))

		if len(headerMatch) > 1 {
			inSteps = true
			if currentStep != nil {
				applyStepMetadata(currentStep)
				pack.Steps = append(pack.Steps, *currentStep)
			}
			num, _ := strconv.Atoi(headerMatch[1])

			// Extract ROI if present
			var roi float64
			cleanedLine := line
			roiMatch := roiRegex.FindStringSubmatch(line)
			if len(roiMatch) > 1 {
				val, err := strconv.ParseFloat(roiMatch[1], 64)
				if err == nil {
					roi = val
					// Remove ROI tag from display title, but keep original line intact?
					// The task says "strip from Step.Title". Step struct currently has `OriginalLine`.
					// I'll assume OriginalLine is what is displayed, or I should add a Title field.
					// Looking at Step struct: ID, Number, Code, OriginalLine.
					// I'll remove it from OriginalLine for now or add a Title field.
					// The existing TUI uses OriginalLine as description.
					// Let's clean OriginalLine for display purposes or add a dedicated Title field.
					// Adding a dedicated Title field seems cleaner but requires struct change.
					// For now, I'll strip it from OriginalLine to match the prompt requirement "cleaner UI display".
					cleanedLine = strings.Replace(cleanedLine, roiMatch[0], "", 1)
					cleanedLine = strings.TrimSpace(cleanedLine)
					// Fix any double spaces or trailing separators if needed, but simple replace is a good start.
				}
			}

			currentStep = &types.Step{
				ID:           headerMatch[1],
				Number:       num,
				OriginalLine: cleanedLine,
				ROI:          roi,
			}
			continue
		}

		if inSteps {
			currentStep.Code += line + "\n"
		} else {
			preludeLines = append(preludeLines, line)
		}
	}

	if currentStep != nil {
		applyStepMetadata(currentStep)
		pack.Steps = append(pack.Steps, *currentStep)
	}

	pack.Prelude.Code = strings.Join(preludeLines, "\n")
	DeriveMetadata(pack)

	return pack, nil
}

func applyStepMetadata(step *types.Step) {
	if step == nil {
		return
	}
	lines := strings.Split(step.Code, "\n")
	for _, line := range lines {
		trimmed := strings.TrimSpace(line)
		if trimmed == "" || !strings.HasPrefix(trimmed, "#") {
			continue
		}
		if step.ROI == 0 {
			if strings.HasPrefix(trimmed, "# ROI:") {
				val := strings.TrimSpace(strings.TrimPrefix(trimmed, "# ROI:"))
				if parsed, err := strconv.ParseFloat(val, 64); err == nil {
					step.ROI = parsed
				}
			}
		}
		if step.Impact == "" {
			if m := impactRegex.FindStringSubmatch(trimmed); len(m) > 1 {
				step.Impact = strings.TrimSpace(m[1])
			}
		}
	}
}
```

internal/pack/parser_test.go
```
package pack

import (
	"strconv"
	"strings"
	"testing"

	"github.com/user/oraclepack/internal/types"
)

func TestParse(t *testing.T) {
	steps := buildSteps(20, "echo")
	content := []byte(`
# My Pack
Some description.

` + "```" + `bash
out_dir="dist"
--write-output

` + steps + `
` + "```" + `
`)

	p, err := Parse(content)
	if err != nil {
		t.Fatalf("Parse failed: %v", err)
	}

	if p.OutDir != "dist" {
		t.Errorf("expected OutDir dist, got %s", p.OutDir)
	}

	if !p.WriteOutput {
		t.Errorf("expected WriteOutput true, got false")
	}

	if len(p.Steps) != 20 {
		t.Errorf("expected 20 steps, got %d", len(p.Steps))
	}

	if p.Steps[0].ID != "01" || p.Steps[0].Number != 1 {
		t.Errorf("step 1 mismatch: %+v", p.Steps[0])
	}

	if err := Validate(p); err != nil {
		t.Errorf("Validate failed: %v", err)
	}
}

func TestParseVariants(t *testing.T) {
	tests := []struct {
		name    string
		content string
	}{
		{
			"em dash",
			`
` + "```" + `bash
# 01 — ROI=...
echo "step 1"
` + "```" + `
`,
		},
		{
			"hyphen",
			`
` + "```" + `bash
# 01 - ROI=...
echo "step 1"
` + "```" + `
`,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			p, err := Parse([]byte(tt.content))
			if err != nil {
				t.Fatalf("Parse failed: %v", err)
			}
			if len(p.Steps) != 1 {
				t.Errorf("expected 1 step, got %d", len(p.Steps))
			}
		})
	}
}

func TestParseROI(t *testing.T) {
	content := []byte(`
` + "```" + `bash
# 01) ROI=4.5 clean me
echo "high value"

# 02) ROI=0.5
echo "low value"

# 03) No ROI
echo "default"
` + "```" + `
`)

	p, err := Parse(content)
	if err != nil {
		t.Fatalf("Parse failed: %v", err)
	}

	if len(p.Steps) != 3 {
		t.Fatalf("expected 3 steps, got %d", len(p.Steps))
	}

	if p.Steps[0].ROI != 4.5 {
		t.Errorf("step 1 ROI mismatch: expected 4.5, got %f", p.Steps[0].ROI)
	}
	if strings.Contains(p.Steps[0].OriginalLine, "ROI=4.5") {
		t.Errorf("step 1 title was not cleaned: %q", p.Steps[0].OriginalLine)
	}

	if p.Steps[1].ROI != 0.5 {
		t.Errorf("step 2 ROI mismatch: expected 0.5, got %f", p.Steps[1].ROI)
	}

	if p.Steps[2].ROI != 0.0 {
		t.Errorf("step 3 ROI mismatch: expected 0.0, got %f", p.Steps[2].ROI)
	}
}

func TestValidateErrors(t *testing.T) {
	base := buildStepSlice(20)
	tests := []struct {
		name    string
		pack    *types.Pack
		wantErr string
	}{
		{
			"no steps",
			&types.Pack{},
			"at least one step is required",
		},
		{
			"duplicate steps",
			&types.Pack{
				Steps: func() []types.Step {
					steps := append([]types.Step(nil), base...)
					steps[1].Number = steps[0].Number
					steps[1].ID = steps[0].ID
					return steps
				}(),
			},
			"duplicate step number 1",
		},
		{
			"wrong step count",
			&types.Pack{
				Steps: []types.Step{
					{Number: 1, ID: "01"},
				},
			},
			"expected exactly 20 steps",
		},
		{
			"non-sequential",
			&types.Pack{
				Steps: func() []types.Step {
					steps := append([]types.Step(nil), base...)
					steps[1].Number = 3
					steps[1].ID = "03"
					return steps
				}(),
			},
			"steps must be sequential starting from 1",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			err := Validate(tt.pack)
			if err == nil {
				t.Error("expected error, got nil")
			} else if !contains(err.Error(), tt.wantErr) {
				t.Errorf("expected error containing %q, got %q", tt.wantErr, err.Error())
			}
		})
	}
}

func contains(s, substr string) bool {
	return len(s) >= len(substr) && (s == substr || (len(substr) > 0 && (s[:len(substr)] == substr || contains(s[1:], substr))))
}

func buildSteps(count int, cmd string) string {
[TRUNCATED]
```

internal/pack/verify_report.go
```
package pack

import (
	"fmt"
	"strings"

	"github.com/user/oraclepack/internal/types"
)

// VerifyReport captures output verification results across a pack.
type VerifyReport struct {
	TotalSteps   int
	CheckedSteps int
	Failures     []types.OutputFailure
}

// FormatVerifyReport renders a human-readable report.
func FormatVerifyReport(report VerifyReport) string {
	var b strings.Builder
	fmt.Fprintf(&b, "Verified outputs for %d/%d steps\n", report.CheckedSteps, report.TotalSteps)
	if len(report.Failures) == 0 {
		b.WriteString("All required output tokens were found.\n")
		return b.String()
	}

	b.WriteString("Missing or invalid outputs:\n")
	for _, failure := range report.Failures {
		stepLabel := failure.StepID
		if stepLabel == "" {
			stepLabel = "unknown step"
		}
		fmt.Fprintf(&b, "- Step %s: %s", stepLabel, failure.Path)
		if failure.Error != "" {
			fmt.Fprintf(&b, " (error: %s)", failure.Error)
		} else if len(failure.MissingTokens) > 0 {
			fmt.Fprintf(&b, " missing %s", strings.Join(failure.MissingTokens, ", "))
		}
		b.WriteString("\n")
	}
	return b.String()
}
```

internal/shell/detect.go
```
package shell

import "os/exec"

// DetectBinary checks PATH for a binary and returns its full path if found.
func DetectBinary(name string) (string, bool) {
	path, err := exec.LookPath(name)
	if err != nil {
		return "", false
	}
	return path, true
}
```

internal/shell/detect_test.go
```
package shell

import "testing"

func TestDetectBinary(t *testing.T) {
	if _, ok := DetectBinary("ls"); !ok {
		t.Fatalf("expected to find ls on PATH")
	}
	if _, ok := DetectBinary("definitely-not-a-real-binary-123"); ok {
		t.Fatalf("expected missing binary to return false")
	}
}
```

internal/shell/engine.go
```
package shell

import (
	"context"
	"fmt"
	"strings"
	"time"

	"github.com/user/oraclepack/internal/dispatch"
	"github.com/user/oraclepack/internal/state"
	"github.com/user/oraclepack/internal/tools"
	"github.com/user/oraclepack/internal/types"
)

// Engine executes pack steps in headless mode.
type Engine struct {
	Pack       *types.Pack
	State      *state.RunState
	StatePath  string
	StopOnFail bool
	Timeout    time.Duration
	Checker    tools.PresenceChecker
}

// Run executes all steps sequentially, updating state on disk.
func (e *Engine) Run(ctx context.Context) error {
	if e.Pack == nil {
		return nil
	}
	if e.State == nil {
		e.State = &state.RunState{
			SchemaVersion: 1,
			StepStatuses:  make(map[string]state.StepStatus),
		}
	}
	if e.State.StepStatuses == nil {
		e.State.StepStatuses = make(map[string]state.StepStatus)
	}

	for i := range e.Pack.Steps {
		step := e.Pack.Steps[i]
		e.State.CurrentStep = step.Number
		status := state.StepStatus{Status: state.StatusRunning, StartedAt: time.Now()}
		e.State.StepStatuses[step.ID] = status
		_ = state.WriteState(e.StatePath, e.State)

		kind := detectToolKind(&step)
		if shouldSkipForMissingTool(kind, e.Checker) {
			status.Status = state.StatusSkipped
			status.Error = "tool missing"
			status.EndedAt = time.Now()
			e.State.StepStatuses[step.ID] = status
			_ = state.WriteState(e.StatePath, e.State)
			continue
		}

		stepCtx := ctx
		if e.Timeout > 0 {
			var cancel context.CancelFunc
			stepCtx, cancel = context.WithTimeout(ctx, e.Timeout)
			defer cancel()
		}

		res, err := RunCommand(stepCtx, step.Code)
		if err == nil && res.ExitCode != 0 {
			err = fmt.Errorf("command failed with exit code %d", res.ExitCode)
		}
		status.EndedAt = time.Now()
		if err != nil {
			status.Status = state.StatusFailed
			status.Error = err.Error()
			e.State.StepStatuses[step.ID] = status
			_ = state.WriteState(e.StatePath, e.State)
			if e.StopOnFail {
				return err
			}
			continue
		}
		status.Status = state.StatusSuccess
		e.State.StepStatuses[step.ID] = status
		_ = state.WriteState(e.StatePath, e.State)
	}
	return nil
}

func detectToolKind(step *types.Step) tools.ToolKind {
	if step == nil {
		return tools.ToolUnknown
	}
	lines := strings.Split(step.Code, "\n")
	for _, line := range lines {
		trimmed := strings.TrimSpace(line)
		if trimmed == "" || strings.HasPrefix(trimmed, "#") {
			continue
		}
		if cls, ok := dispatch.Classify(trimmed); ok {
			return cls.Kind
		}
		break
	}
	return tools.ToolUnknown
}

func shouldSkipForMissingTool(kind tools.ToolKind, checker tools.PresenceChecker) bool {
	if kind == tools.ToolUnknown {
		return false
	}
	meta, ok := tools.Metadata(kind)
	if !ok {
		return false
	}
	if checker == nil {
		_, found := DetectBinary(meta.Name)
		return !found
	}
	_, found := checker.DetectBinary(meta.Name)
	return !found
}
```

internal/shell/engine_test.go
```
package shell

import (
	"context"
	"path/filepath"
	"testing"

	"github.com/user/oraclepack/internal/state"
	"github.com/user/oraclepack/internal/types"
)

type fakeChecker struct {
	found map[string]bool
}

func (f fakeChecker) DetectBinary(name string) (string, bool) {
	return name, f.found[name]
}

func TestEngineSkipsMissingTool(t *testing.T) {
	p := &types.Pack{
		Steps: []types.Step{
			{ID: "01", Number: 1, Code: "codex exec \"hi\""},
		},
	}
	dir := t.TempDir()
	engine := &Engine{
		Pack:      p,
		State:     &state.RunState{SchemaVersion: 1, StepStatuses: map[string]state.StepStatus{}},
		StatePath: filepath.Join(dir, "state.json"),
		Checker:   fakeChecker{found: map[string]bool{"codex": false}},
	}
	if err := engine.Run(context.Background()); err != nil {
		t.Fatalf("Run: %v", err)
	}
	if engine.State.StepStatuses["01"].Status != state.StatusSkipped {
		t.Fatalf("expected skipped, got %s", engine.State.StepStatuses["01"].Status)
	}
}

func TestEngineFailsOnError(t *testing.T) {
	p := &types.Pack{
		Steps: []types.Step{
			{ID: "01", Number: 1, Code: "exit 1"},
		},
	}
	engine := &Engine{
		Pack:       p,
		State:      &state.RunState{SchemaVersion: 1, StepStatuses: map[string]state.StepStatus{}},
		StopOnFail: true,
		Checker:    fakeChecker{found: map[string]bool{}},
	}
	if err := engine.Run(context.Background()); err == nil {
		t.Fatal("expected error, got nil")
	}
	if engine.State.StepStatuses["01"].Status != state.StatusFailed {
		t.Fatalf("expected failed, got %s", engine.State.StepStatuses["01"].Status)
	}
}
```

internal/shell/runner.go
```
package shell

import (
	"bytes"
	"context"
	"fmt"
	"os/exec"
)

// Result captures command output and exit code.
type Result struct {
	Stdout   string
	Stderr   string
	ExitCode int
}

// RunCommand executes a command with login shell semantics using /bin/bash -lc.
func RunCommand(ctx context.Context, cmd string) (Result, error) {
	c := exec.CommandContext(ctx, "/bin/bash", "-lc", cmd)
	var stdout bytes.Buffer
	var stderr bytes.Buffer
	c.Stdout = &stdout
	c.Stderr = &stderr

	err := c.Run()
	exitCode := 0
	if err != nil {
		if ee, ok := err.(*exec.ExitError); ok {
			exitCode = ee.ExitCode()
		} else {
			return Result{}, fmt.Errorf("run command: %w", err)
		}
	}

	return Result{
		Stdout:   stdout.String(),
		Stderr:   stderr.String(),
		ExitCode: exitCode,
	}, nil
}
```

internal/shell/runner_test.go
```
package shell

import (
	"context"
	"strings"
	"testing"
)

func TestRunCommandLoginShell(t *testing.T) {
	res, err := RunCommand(context.Background(), "echo $PATH")
	if err != nil {
		t.Fatalf("RunCommand: %v", err)
	}
	if strings.TrimSpace(res.Stdout) == "" {
		t.Fatalf("expected PATH output, got empty stdout")
	}
	if res.ExitCode != 0 {
		t.Fatalf("expected exit code 0, got %d", res.ExitCode)
	}
}
```

internal/render/render.go
```
package render

import (
	"sync"

	"github.com/charmbracelet/glamour"
	"github.com/user/oraclepack/internal/types"
)

const (
	DefaultStyle = "dark"
	DefaultWidth = 80
)

type rendererKey struct {
	width int
	style string
}

var (
	rendererMu    sync.Mutex
	rendererCache = map[rendererKey]*glamour.TermRenderer{}
)

// RenderMarkdown renders markdown text as ANSI-styled text.
func RenderMarkdown(text string, width int, style string) (string, error) {
	if width <= 0 {
		width = DefaultWidth
	}
	if style == "" {
		style = DefaultStyle
	}

	r, err := rendererFor(width, style)
	if err != nil {
		return "", err
	}

	return r.Render(text)
}

// RenderStepCode renders a step's code block for preview.
func RenderStepCode(s types.Step, width int, style string) (string, error) {
	md := "```bash\n" + s.Code + "\n```"
	return RenderMarkdown(md, width, style)
}

func rendererFor(width int, style string) (*glamour.TermRenderer, error) {
	key := rendererKey{width: width, style: style}

	rendererMu.Lock()
	r := rendererCache[key]
	rendererMu.Unlock()
	if r != nil {
		return r, nil
	}

	opts := []glamour.TermRendererOption{glamour.WithWordWrap(width)}
	if style == "auto" {
		opts = append(opts, glamour.WithAutoStyle())
	} else {
		opts = append(opts, glamour.WithStandardStyle(style))
	}

	r, err := glamour.NewTermRenderer(opts...)
	if err != nil {
		return nil, err
	}

	rendererMu.Lock()
	rendererCache[key] = r
	rendererMu.Unlock()
	return r, nil
}
```

internal/render/render_test.go
```
package render

import (
	"strings"
	"testing"
)

func TestRenderMarkdown(t *testing.T) {
	text := "# Hello\n**bold**"
	got, err := RenderMarkdown(text, 40, DefaultStyle)
	if err != nil {
		t.Fatalf("RenderMarkdown failed: %v", err)
	}

	// ANSI escape codes start with \x1b[
	if !strings.Contains(got, "\x1b[") {
		t.Errorf("expected ANSI codes in output, got: %q", got)
	}
}
```

internal/report/generate.go
```
package report

import (
	"time"

	"github.com/user/oraclepack/internal/state"
)

// GenerateReport creates a ReportV1 from a RunState.
func GenerateReport(s *state.RunState, packName string) *ReportV1 {
	report := &ReportV1{
		PackInfo: PackInfo{
			Name: packName,
			Hash: s.PackHash,
		},
		GeneratedAt: time.Now(),
		Steps:       []StepReport{},
	}

	var totalDuration time.Duration
	success, failure, skipped := 0, 0, 0

	for id, status := range s.StepStatuses {
		duration := status.EndedAt.Sub(status.StartedAt)
		if status.EndedAt.IsZero() || status.StartedAt.IsZero() {
			duration = 0
		}

		totalDuration += duration

		sr := StepReport{
			ID:         id,
			Status:     string(status.Status),
			ExitCode:   status.ExitCode,
			Duration:   duration,
			DurationMs: duration.Milliseconds(),
			Error:      status.Error,
		}
		report.Steps = append(report.Steps, sr)

		switch status.Status {
		case state.StatusSuccess:
			success++
		case state.StatusFailed:
			failure++
		case state.StatusSkipped:
			skipped++
		}
	}

	report.Summary = Summary{
		TotalSteps:      len(s.StepStatuses),
		SuccessCount:    success,
		FailureCount:    failure,
		SkippedCount:    skipped,
		TotalDuration:   totalDuration,
		TotalDurationMs: totalDuration.Milliseconds(),
	}

	if len(s.Warnings) > 0 {
		report.Warnings = make([]Warning, 0, len(s.Warnings))
		for _, w := range s.Warnings {
			report.Warnings = append(report.Warnings, Warning{
				Scope:   w.Scope,
				StepID:  w.StepID,
				Line:    w.Line,
				Token:   w.Token,
				Message: w.Message,
			})
		}
	}

	return report
}
```

internal/report/io.go
```
package report

import (
	"encoding/json"
	"fmt"
	"os"
)

// WriteReport writes a ReportV1 to disk.
func WriteReport(path string, rep *ReportV1) error {
	data, err := json.MarshalIndent(rep, "", "  ")
	if err != nil {
		return fmt.Errorf("marshal report: %w", err)
	}
	if err := os.WriteFile(path, data, 0644); err != nil {
		return fmt.Errorf("write report: %w", err)
	}
	return nil
}
```

internal/report/io_test.go
```
package report

import (
	"os"
	"path/filepath"
	"testing"
)

func TestWriteReport(t *testing.T) {
	dir := t.TempDir()
	path := filepath.Join(dir, "report.json")

	rep := &ReportV1{
		PackInfo: PackInfo{Name: "pack"},
		Summary:  Summary{TotalSteps: 1},
	}
	if err := WriteReport(path, rep); err != nil {
		t.Fatalf("WriteReport: %v", err)
	}

	if _, err := os.Stat(path); err != nil {
		t.Fatalf("expected report file to exist: %v", err)
	}
}
```

internal/report/report_test.go
```
package report

import (
	"testing"
	"time"

	"github.com/user/oraclepack/internal/state"
)

func TestGenerateReport(t *testing.T) {
	s := &state.RunState{
		PackHash: "hash123",
		StepStatuses: map[string]state.StepStatus{
			"01": {
				Status:    state.StatusSuccess,
				StartedAt: time.Now().Add(-1 * time.Second),
				EndedAt:   time.Now(),
			},
		},
	}

	rep := GenerateReport(s, "my-pack")

	if rep.PackInfo.Name != "my-pack" {
		t.Errorf("expected name my-pack, got %s", rep.PackInfo.Name)
	}

	if rep.Summary.TotalSteps != 1 {
		t.Errorf("expected 1 total step, got %d", rep.Summary.TotalSteps)
	}

	if rep.Summary.SuccessCount != 1 {
		t.Errorf("expected 1 success, got %d", rep.Summary.SuccessCount)
	}
}
```

internal/report/types.go
```
package report

import (
	"time"
)

// ReportV1 represents the final machine-readable summary.
type ReportV1 struct {
	Summary     Summary      `json:"summary"`
	PackInfo    PackInfo     `json:"pack_info"`
	Steps       []StepReport `json:"steps"`
	Warnings    []Warning    `json:"warnings,omitempty"`
	GeneratedAt time.Time    `json:"generated_at"`
}

type Summary struct {
	TotalSteps      int           `json:"total_steps"`
	SuccessCount    int           `json:"success_count"`
	FailureCount    int           `json:"failure_count"`
	SkippedCount    int           `json:"skipped_count"`
	TotalDuration   time.Duration `json:"total_duration"`
	TotalDurationMs int64         `json:"total_duration_ms"`
}

type PackInfo struct {
	Name string `json:"name"`
	Hash string `json:"hash"`
}

type StepReport struct {
	ID         string        `json:"id"`
	Status     string        `json:"status"`
	ExitCode   int           `json:"exit_code"`
	Duration   time.Duration `json:"duration"`
	DurationMs int64         `json:"duration_ms"`
	Error      string        `json:"error,omitempty"`
}

// Warning captures non-fatal execution notes surfaced during a run.
type Warning struct {
	Scope   string `json:"scope"`
	StepID  string `json:"step_id,omitempty"`
	Line    int    `json:"line"`
	Token   string `json:"token"`
	Message string `json:"message"`
}
```

internal/state/io.go
```
package state

// Intentionally left without extra imports.

// WriteState writes RunState atomically to disk.
func WriteState(path string, state *RunState) error {
	return SaveStateAtomic(path, state)
}
```

internal/state/io_test.go
```
package state

import (
	"path/filepath"
	"testing"
	"time"
)

func TestWriteStateAndLoadState(t *testing.T) {
	dir := t.TempDir()
	path := filepath.Join(dir, "state.json")

	input := &RunState{
		SchemaVersion: 1,
		PackHash:      "hash",
		StartTime:     time.Now(),
		CurrentStep:   2,
		StepStatuses: map[string]StepStatus{
			"01": {Status: StatusSuccess},
		},
	}

	if err := WriteState(path, input); err != nil {
		t.Fatalf("WriteState: %v", err)
	}

	out, err := LoadState(path)
	if err != nil {
		t.Fatalf("LoadState: %v", err)
	}

	if out.CurrentStep != 2 {
		t.Fatalf("expected CurrentStep 2, got %d", out.CurrentStep)
	}
	if out.StepStatuses["01"].Status != StatusSuccess {
		t.Fatalf("expected status success, got %s", out.StepStatuses["01"].Status)
	}
}
```

internal/state/persist.go
```
package state

import (
	"encoding/json"
	"fmt"
	"os"
)

// SaveStateAtomic saves the state to a file atomically.
func SaveStateAtomic(path string, state *RunState) error {
	data, err := json.MarshalIndent(state, "", "  ")
	if err != nil {
		return fmt.Errorf("marshal state: %w", err)
	}

	tempPath := path + ".tmp"
	if err := os.WriteFile(tempPath, data, 0644); err != nil {
		return fmt.Errorf("write temp file: %w", err)
	}

	if err := os.Rename(tempPath, path); err != nil {
		os.Remove(tempPath)
		return fmt.Errorf("rename temp file: %w", err)
	}

	return nil
}

// LoadState loads the state from a file.
func LoadState(path string) (*RunState, error) {
	data, err := os.ReadFile(path)
	if err != nil {
		if os.IsNotExist(err) {
			return nil, fmt.Errorf("state file not found: %w", err)
		}
		return nil, fmt.Errorf("read state file: %w", err)
	}

	var state RunState
	if err := json.Unmarshal(data, &state); err != nil {
		return nil, fmt.Errorf("unmarshal state: %w", err)
	}

	return &state, nil
}
```

internal/state/state_test.go
```
package state

import (
	"os"
	"testing"
)

func TestStatePersistence(t *testing.T) {
	tmpFile := "test_state.json"
	defer os.Remove(tmpFile)

	s := &RunState{
		SchemaVersion: 1,
		PackHash:      "abc",
		StepStatuses: map[string]StepStatus{
			"01": {Status: StatusSuccess, ExitCode: 0},
		},
	}

	if err := SaveStateAtomic(tmpFile, s); err != nil {
		t.Fatalf("SaveStateAtomic failed: %v", err)
	}

	loaded, err := LoadState(tmpFile)
	if err != nil {
		t.Fatalf("LoadState failed: %v", err)
	}

	if loaded.PackHash != s.PackHash {
		t.Errorf("expected hash %s, got %s", s.PackHash, loaded.PackHash)
	}

	if loaded.StepStatuses["01"].Status != StatusSuccess {
		t.Errorf("expected status success, got %s", loaded.StepStatuses["01"].Status)
	}
}
```

internal/state/types.go
```
package state

import (
	"time"
)

type Status string

const (
	StatusPending Status = "pending"
	StatusRunning Status = "running"
	StatusSuccess Status = "success"
	StatusFailed  Status = "failed"
	StatusSkipped Status = "skipped"
)

// RunState tracks the execution progress of an oracle pack.
type RunState struct {
	SchemaVersion int                   `json:"schema_version"`
	PackHash      string                `json:"pack_hash"`
	StartTime     time.Time             `json:"start_time"`
	CurrentStep   int                   `json:"current_step,omitempty"`
	StepStatuses  map[string]StepStatus `json:"step_statuses"`
	ROIThreshold  float64               `json:"roi_threshold,omitempty"`
	ROIMode       string                `json:"roi_mode,omitempty"`
	Warnings      []Warning             `json:"warnings,omitempty"`
}

// StepStatus holds the outcome of an individual step.
type StepStatus struct {
	Status    Status    `json:"status"`
	ExitCode  int       `json:"exit_code"`
	StartedAt time.Time `json:"started_at"`
	EndedAt   time.Time `json:"ended_at"`
	Error     string    `json:"error,omitempty"`
}

// Warning captures a non-fatal execution note (e.g., sanitized labels).
type Warning struct {
	Scope   string `json:"scope"`
	StepID  string `json:"step_id,omitempty"`
	Line    int    `json:"line"`
	Token   string `json:"token"`
	Message string `json:"message"`
}
```

internal/tools/types.go
```
package tools

// ToolKind identifies a supported tool prefix.
type ToolKind int

const (
	ToolUnknown ToolKind = iota
	ToolOracle
	ToolTM
	ToolTaskMaster
	ToolCodex
	ToolGemini
)

// ToolMetadata captures tool invocation details.
type ToolMetadata struct {
	Name string
	Args []string
}

var registry = map[ToolKind]ToolMetadata{
	ToolUnknown:    {Name: "unknown"},
	ToolOracle:     {Name: "oracle"},
	ToolTM:         {Name: "tm"},
	ToolTaskMaster: {Name: "task-master"},
	ToolCodex:      {Name: "codex", Args: []string{"exec"}},
	ToolGemini:     {Name: "gemini"},
}

// Metadata returns tool metadata if present.
func Metadata(kind ToolKind) (ToolMetadata, bool) {
	meta, ok := registry[kind]
	return meta, ok
}

// Name returns the canonical tool name.
func (k ToolKind) Name() string {
	if meta, ok := registry[k]; ok {
		return meta.Name
	}
	return "unknown"
}

// PresenceChecker abstracts binary detection.
type PresenceChecker interface {
	DetectBinary(name string) (string, bool)
}
```

internal/tools/types_test.go
```
package tools

import "testing"

func TestMetadataRegistry(t *testing.T) {
	meta, ok := Metadata(ToolCodex)
	if !ok {
		t.Fatalf("expected metadata for codex")
	}
	if meta.Name != "codex" {
		t.Fatalf("expected codex name, got %s", meta.Name)
	}
	if len(meta.Args) != 1 || meta.Args[0] != "exec" {
		t.Fatalf("expected codex exec args, got %+v", meta.Args)
	}
	if ToolOracle.Name() != "oracle" {
		t.Fatalf("expected oracle name, got %s", ToolOracle.Name())
	}
}
```

internal/tui/clipboard.go
```
package tui

import (
	"fmt"
	"os"
	"os/exec"
	"runtime"
	"strings"
)

func copyToClipboard(content string) error {
	var cmd *exec.Cmd
	switch runtime.GOOS {
	case "darwin":
		cmd = exec.Command("pbcopy")
	case "linux":
		if _, err := exec.LookPath("wl-copy"); err == nil {
			cmd = exec.Command("wl-copy")
		} else if _, err := exec.LookPath("xclip"); err == nil {
			cmd = exec.Command("xclip", "-selection", "clipboard")
		} else if _, err := exec.LookPath("xsel"); err == nil {
			cmd = exec.Command("xsel", "--clipboard", "--input")
		} else {
			return err
		}
	case "windows":
		cmd = exec.Command("cmd", "/c", "clip")
	default:
		return exec.ErrNotFound
	}

	cmd.Stdin = strings.NewReader(content)
	return cmd.Run()
}

func writeClipboardFallback(content string) (string, error) {
	file, err := os.CreateTemp("", "oraclepack-step-*.txt")
	if err != nil {
		return "", fmt.Errorf("create temp file: %w", err)
	}
	defer file.Close()
	if _, err := file.WriteString(content); err != nil {
		return "", fmt.Errorf("write temp file: %w", err)
	}
	return file.Name(), nil
}
```

internal/tui/filter_test.go
```
package tui

import (
	"os"
	"path/filepath"
	"testing"

	tea "github.com/charmbracelet/bubbletea"
	"github.com/user/oraclepack/internal/exec"
	"github.com/user/oraclepack/internal/state"
	"github.com/user/oraclepack/internal/types"
)

func TestFilterLogic(t *testing.T) {
	// Setup pack with steps having different ROI
	p := &types.Pack{
		Steps: []types.Step{
			{ID: "01", ROI: 1.0, OriginalLine: "Step 1"},
			{ID: "02", ROI: 5.0, OriginalLine: "Step 2"},
			{ID: "03", ROI: 10.0, OriginalLine: "Step 3"},
		},
	}
	r := exec.NewRunner(exec.RunnerOptions{})
	s := &state.RunState{}

	// Initialize model with no filter (threshold 0)
	m := NewModel(p, r, s, "", 0, "over", false, false, 0, false, "auto")

	if len(m.list.Items()) != 3 {
		t.Fatalf("expected 3 items initially, got %d", len(m.list.Items()))
	}

	// Apply filter: ROI >= 5.0
	m.roiThreshold = 5.0
	m.roiMode = "over"
	m = m.refreshList()

	if len(m.list.Items()) != 2 {
		t.Errorf("expected 2 items after filtering >= 5.0, got %d", len(m.list.Items()))
	}

	// Verify items are 02 and 03
	items := m.list.Items()
	if items[0].(item).id != "02" {
		t.Errorf("expected first item to be 02, got %s", items[0].(item).id)
	}
	if items[1].(item).id != "03" {
		t.Errorf("expected second item to be 03, got %s", items[1].(item).id)
	}

	// Apply filter: ROI < 5.0 ("under")
	m.roiThreshold = 5.0
	m.roiMode = "under"
	m = m.refreshList()

	if len(m.list.Items()) != 1 {
		t.Errorf("expected 1 item after filtering < 5.0, got %d", len(m.list.Items()))
	}
	if m.list.Items()[0].(item).id != "01" {
		t.Errorf("expected item to be 01, got %s", m.list.Items()[0].(item).id)
	}
}

func TestROIModeTogglePersists(t *testing.T) {
	dir := t.TempDir()
	statePath := filepath.Join(dir, "state.json")
	p := &types.Pack{
		Steps: []types.Step{
			{ID: "01", ROI: 1.0, OriginalLine: "Step 1"},
		},
	}
	r := exec.NewRunner(exec.RunnerOptions{})
	s := &state.RunState{SchemaVersion: 1}

	m := NewModel(p, r, s, statePath, 0, "over", false, false, 0, false, "auto")

	updated, _ := m.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("m")})
	m2 := updated.(Model)
	if m2.roiMode != "under" {
		t.Fatalf("expected roiMode to toggle to under, got %s", m2.roiMode)
	}

	loaded, err := state.LoadState(statePath)
	if err != nil {
		t.Fatalf("failed to load state: %v", err)
	}
	if loaded.ROIMode != "under" {
		t.Fatalf("expected persisted roiMode under, got %s", loaded.ROIMode)
	}

	if err := os.Remove(statePath); err != nil {
		t.Fatalf("failed to cleanup state file: %v", err)
	}
}
```

internal/tui/overrides_confirm.go
```
package tui

import (
	"fmt"
	"sort"
	"strings"

	"github.com/user/oraclepack/internal/exec"
	"github.com/user/oraclepack/internal/overrides"
)

type ValidationResultMsg struct {
	Errors []exec.ValidationError
	Err    error
}

type OverridesConfirmModel struct {
	validating bool
	errMsg     string
	errors     []exec.ValidationError
}

func (m OverridesConfirmModel) View(over overrides.RuntimeOverrides, baseline []string) string {
	added := strings.Join(over.AddedFlags, ", ")
	if added == "" {
		added = "(none)"
	}
	removed := strings.Join(over.RemovedFlags, ", ")
	if removed == "" {
		removed = "(none)"
	}
	targeted := len(over.ApplyToSteps)
	targetList := formatTargetList(over.ApplyToSteps, 5)
	effective := effectiveFlagsSummary(over, baseline)
	lines := []string{
		"Summary:",
		fmt.Sprintf("Added flags: %s", added),
		fmt.Sprintf("Removed flags: %s", removed),
		fmt.Sprintf("Targeted steps: %d%s", targeted, targetList),
		fmt.Sprintf("Effective flags: %s", effective),
		"",
		"[Enter] Validate  [Esc] Cancel",
	}

	if m.validating {
		lines = append(lines, "", "Validating overrides...")
	}
	if m.errMsg != "" {
		lines = append(lines, "", "Validation failed:", m.errMsg)
	}
	if len(m.errors) > 0 {
		lines = append(lines, "", fmt.Sprintf("Validation errors (%d):", len(m.errors)))
		lines = append(lines, formatValidationErrors(m.errors, 6)...)
	}

	return strings.Join(lines, "\n")
}

func formatTargetList(targets map[string]bool, limit int) string {
	if len(targets) == 0 || limit <= 0 {
		return ""
	}
	ids := make([]string, 0, len(targets))
	for id := range targets {
		ids = append(ids, id)
	}
	sort.Strings(ids)
	if len(ids) <= limit {
		return fmt.Sprintf(" (%s)", strings.Join(ids, ", "))
	}
	return fmt.Sprintf(" (%s, +%d more)", strings.Join(ids[:limit], ", "), len(ids)-limit)
}

func effectiveFlagsSummary(over overrides.RuntimeOverrides, baseline []string) string {
	if len(over.ApplyToSteps) == 0 {
		return "(no steps targeted)"
	}
	var first string
	for id := range over.ApplyToSteps {
		first = id
		break
	}
	flags := over.EffectiveFlags(first, baseline)
	if len(flags) == 0 {
		return "(none)"
	}
	return strings.Join(flags, " ")
}

func formatValidationErrors(errors []exec.ValidationError, limit int) []string {
	if limit <= 0 {
		return nil
	}
	lines := []string{}
	for i, err := range errors {
		if i >= limit {
			lines = append(lines, fmt.Sprintf("- (+%d more)", len(errors)-limit))
			break
		}
		msg := strings.TrimSpace(err.ErrorMessage)
		if msg == "" {
			msg = "(no error message)"
		}
		lines = append(lines, fmt.Sprintf("- Step %s: %s", err.StepID, firstLine(msg)))
	}
	return lines
}

func firstLine(msg string) string {
	if idx := strings.IndexByte(msg, '\n'); idx != -1 {
		return msg[:idx]
	}
	return msg
}
```

internal/tui/overrides_flags.go
```
package tui

import (
	"fmt"
	"io"

	"github.com/charmbracelet/bubbles/list"
	tea "github.com/charmbracelet/bubbletea"
	"github.com/charmbracelet/lipgloss"
)

type FlagItem struct {
	Flag       string
	Desc       string
	IsBaseline bool
	Selected   bool
}

func (i FlagItem) Title() string       { return i.Flag }
func (i FlagItem) Description() string { return i.Desc }
func (i FlagItem) FilterValue() string { return i.Flag }

type FlagsPickerModel struct {
	list list.Model
}

func NewFlagsPickerModel(baseline []string) FlagsPickerModel {
	baselineSet := make(map[string]bool, len(baseline))
	for _, f := range baseline {
		baselineSet[f] = true
	}

	curated := []FlagItem{
		{Flag: "--files-report", Desc: "Show per-file token usage"},
		{Flag: "--render", Desc: "Print assembled markdown bundle"},
		{Flag: "--render-plain", Desc: "Render markdown without ANSI"},
		{Flag: "--copy", Desc: "Copy assembled markdown bundle"},
		{Flag: "--wait", Desc: "Wait for background API runs"},
	}

	items := make([]list.Item, 0, len(curated))
	for _, c := range curated {
		c.IsBaseline = baselineSet[c.Flag]
		if c.IsBaseline {
			c.Selected = true
		}
		items = append(items, c)
	}

	delegate := newFlagsDelegate()
	l := list.New(items, delegate, 0, 0)
	l.Title = "Oracle Flags"
	l.SetFilteringEnabled(true)

	return FlagsPickerModel{list: l}
}

func (m FlagsPickerModel) Init() tea.Cmd {
	return nil
}

func (m FlagsPickerModel) Update(msg tea.Msg) (FlagsPickerModel, tea.Cmd) {
	switch msg := msg.(type) {
	case tea.KeyMsg:
		if msg.String() == " " {
			idx := m.list.Index()
			item, ok := m.list.SelectedItem().(FlagItem)
			if ok && !item.IsBaseline {
				item.Selected = !item.Selected
				_ = m.list.SetItem(idx, item)
			}
		}
	}

	var cmd tea.Cmd
	m.list, cmd = m.list.Update(msg)
	return m, cmd
}

func (m *FlagsPickerModel) SetSize(width, height int) {
	m.list.SetSize(width, height)
}

func (m FlagsPickerModel) View() string {
	return m.list.View()
}

func (m FlagsPickerModel) SelectedFlags() []string {
	var flags []string
	for _, item := range m.list.Items() {
		if fi, ok := item.(FlagItem); ok && fi.Selected && !fi.IsBaseline {
			flags = append(flags, fi.Flag)
		}
	}
	return flags
}

type flagsDelegate struct {
	list.DefaultDelegate
}

func newFlagsDelegate() flagsDelegate {
	d := list.NewDefaultDelegate()
	return flagsDelegate{DefaultDelegate: d}
}

func (d flagsDelegate) Render(w io.Writer, m list.Model, index int, item list.Item) {
	fi, ok := item.(FlagItem)
	if !ok {
		d.DefaultDelegate.Render(w, m, index, item)
		return
	}

	checked := fi.Selected || fi.IsBaseline
	marker := "[ ]"
	if checked {
		marker = "[x]"
	}
	if fi.IsBaseline {
		marker = "[*]"
	}

	label := fi.Flag
	if fi.Desc != "" {
		label = fmt.Sprintf("%s - %s", fi.Flag, fi.Desc)
	}
	if fi.IsBaseline {
		label = label + " (base)"
	}

	line := fmt.Sprintf("%s %s", marker, label)
	if index == m.Index() {
		line = d.Styles.SelectedTitle.Render(line)
	} else {
		line = d.Styles.NormalTitle.Render(line)
	}
	if fi.IsBaseline {
		line = lipgloss.NewStyle().Faint(true).Render(line)
	}

	fmt.Fprintln(w, line)
}
```

internal/tui/overrides_flow.go
```
package tui

import (
	"context"
	"fmt"

	tea "github.com/charmbracelet/bubbletea"
	"github.com/charmbracelet/lipgloss"
	"github.com/user/oraclepack/internal/exec"
	"github.com/user/oraclepack/internal/overrides"
	"github.com/user/oraclepack/internal/types"
)

type OverridesStep int

const (
	OverridesFlags OverridesStep = iota
	OverridesSteps
	OverridesConfirm
)

type OverridesStartedMsg struct{}

type OverridesAppliedMsg struct {
	Overrides overrides.RuntimeOverrides
}

type OverridesCancelledMsg struct{}

type OverridesFlowModel struct {
	step    OverridesStep
	flags   FlagsPickerModel
	steps   StepsPickerModel
	confirm OverridesConfirmModel

	packSteps        []types.Step
	baseline         []string
	runnerOpts       exec.RunnerOptions
	pendingOverrides overrides.RuntimeOverrides
}

func NewOverridesFlowModel(steps []types.Step, baseline []string, opts exec.RunnerOptions) OverridesFlowModel {
	return OverridesFlowModel{
		step:       OverridesFlags,
		flags:      NewFlagsPickerModel(nil),
		steps:      NewStepsPickerModel(steps),
		confirm:    OverridesConfirmModel{},
		packSteps:  steps,
		baseline:   exec.ApplyChatGPTURL(baseline, opts.ChatGPTURL),
		runnerOpts: opts,
	}
}

func (m OverridesFlowModel) Init() tea.Cmd {
	return nil
}

func (m OverridesFlowModel) Update(msg tea.Msg) (OverridesFlowModel, tea.Cmd) {
	var cmd tea.Cmd
	if m.step == OverridesFlags {
		m.flags, cmd = m.flags.Update(msg)
	}
	if m.step == OverridesSteps {
		m.steps, cmd = m.steps.Update(msg)
	}
	if m.step == OverridesConfirm {
		switch v := msg.(type) {
		case ValidationResultMsg:
			m.confirm.validating = false
			m.confirm.errors = v.Errors
			if v.Err != nil {
				m.confirm.errMsg = v.Err.Error()
				return m, nil
			}
			if len(v.Errors) > 0 {
				m.confirm.errMsg = fmt.Sprintf("%d validation errors detected.", len(v.Errors))
				return m, nil
			}
			return m, func() tea.Msg { return OverridesAppliedMsg{Overrides: m.pendingOverrides} }
		}
	}

	switch msg := msg.(type) {
	case tea.KeyMsg:
		switch msg.String() {
		case "esc":
			return m, func() tea.Msg { return OverridesCancelledMsg{} }
		case "shift+tab", "backspace":
			if m.step > OverridesFlags {
				m.step--
			}
		case "enter", "tab":
			if m.step == OverridesConfirm {
				if m.confirm.validating {
					return m, nil
				}
				m.pendingOverrides = m.currentOverrides()
				m.confirm.validating = true
				m.confirm.errMsg = ""
				m.confirm.errors = nil
				return m, m.validateCmd(m.pendingOverrides)
			}
			m.step++
		}
	}

	return m, cmd
}

func (m OverridesFlowModel) View(width, height int) string {
	title := lipgloss.NewStyle().Bold(true).Render("Overrides Wizard")
	step := fmt.Sprintf("Step %d/3", int(m.step)+1)
	body := fmt.Sprintf("Current step: %s\n\n[Enter] Next  [Esc] Cancel", overridesStepName(m.step))

	var content string
	if m.step == OverridesFlags {
		m.flags.SetSize(width-4, height-8)
		content = lipgloss.JoinVertical(lipgloss.Left,
			title,
			step,
			"",
			m.flags.View(),
			"",
			body,
		)
	} else if m.step == OverridesSteps {
		m.steps.SetSize(width-4, height-8)
		content = lipgloss.JoinVertical(lipgloss.Left,
			title,
			step,
			"",
			m.steps.View(),
			"",
			body,
		)
	} else if m.step == OverridesConfirm {
		content = lipgloss.JoinVertical(lipgloss.Left,
			title,
			step,
			"",
			m.confirm.View(m.currentOverrides(), m.baseline),
		)
	} else {
		content = lipgloss.JoinVertical(lipgloss.Left,
			title,
			step,
			"",
			body,
		)
	}

	return lipgloss.Place(width, height, lipgloss.Center, lipgloss.Center, content)
}

func (m OverridesFlowModel) currentOverrides() overrides.RuntimeOverrides {
	return overrides.RuntimeOverrides{
		AddedFlags:   m.flags.SelectedFlags(),
		RemovedFlags: nil,
		ApplyToSteps: m.steps.SelectedSteps(),
	}
}

func (m OverridesFlowModel) validateCmd(over overrides.RuntimeOverrides) tea.Cmd {
	return func() tea.Msg {
		errs, err := exec.ValidateOverrides(context.Background(), m.packSteps, &over, m.baseline, m.runnerOpts)
		return ValidationResultMsg{Errors: errs, Err: err}
	}
}

func overridesStepName(step OverridesStep) string {
	switch step {
	case OverridesFlags:
		return "Flags"
	case OverridesSteps:
		return "Target Steps"
	case OverridesConfirm:
		return "Confirm"
	default:
		return "Unknown"
	}
}
```

internal/tui/overrides_steps.go
```
package tui

import (
	"fmt"
	"io"

	"github.com/charmbracelet/bubbles/list"
	tea "github.com/charmbracelet/bubbletea"
	"github.com/charmbracelet/lipgloss"
	"github.com/user/oraclepack/internal/types"
)

type StepItem struct {
	ID       string
	TitleTxt string
	DescTxt  string
	Selected bool
}

func (i StepItem) Title() string       { return i.TitleTxt }
func (i StepItem) Description() string { return i.DescTxt }
func (i StepItem) FilterValue() string { return i.TitleTxt }

type StepsPickerModel struct {
	list list.Model
}

func NewStepsPickerModel(steps []types.Step) StepsPickerModel {
	items := make([]list.Item, 0, len(steps))
	for _, s := range steps {
		items = append(items, StepItem{
			ID:       s.ID,
			TitleTxt: fmt.Sprintf("Step %s", s.ID),
			DescTxt:  s.OriginalLine,
			Selected: true,
		})
	}

	delegate := newStepsDelegate()
	l := list.New(items, delegate, 0, 0)
	l.Title = "Target Steps"
	l.SetFilteringEnabled(true)

	return StepsPickerModel{list: l}
}

func (m StepsPickerModel) Init() tea.Cmd {
	return nil
}

func (m StepsPickerModel) Update(msg tea.Msg) (StepsPickerModel, tea.Cmd) {
	switch msg := msg.(type) {
	case tea.KeyMsg:
		switch msg.String() {
		case "a", "A":
			m = m.setAll(true)
			return m, nil
		case "i":
			m = m.invert()
			return m, nil
		case "n":
			m = m.setAll(false)
			return m, nil
		case " ":
			idx := m.list.Index()
			item, ok := m.list.SelectedItem().(StepItem)
			if ok {
				item.Selected = !item.Selected
				_ = m.list.SetItem(idx, item)
			}
		}
	}

	var cmd tea.Cmd
	m.list, cmd = m.list.Update(msg)
	return m, cmd
}

func (m *StepsPickerModel) SetSize(width, height int) {
	m.list.SetSize(width, height)
}

func (m StepsPickerModel) View() string {
	help := lipgloss.NewStyle().Faint(true).Render("[space] toggle  [a] all  [i] invert  [n] none")
	return m.list.View() + "\n" + help
}

func (m StepsPickerModel) SelectedSteps() map[string]bool {
	selected := make(map[string]bool)
	for _, item := range m.list.Items() {
		if si, ok := item.(StepItem); ok && si.Selected {
			selected[si.ID] = true
		}
	}
	return selected
}

func (m StepsPickerModel) setAll(value bool) StepsPickerModel {
	for idx, item := range m.list.Items() {
		si, ok := item.(StepItem)
		if !ok {
			continue
		}
		si.Selected = value
		_ = m.list.SetItem(idx, si)
	}
	return m
}

func (m StepsPickerModel) invert() StepsPickerModel {
	for idx, item := range m.list.Items() {
		si, ok := item.(StepItem)
		if !ok {
			continue
		}
		si.Selected = !si.Selected
		_ = m.list.SetItem(idx, si)
	}
	return m
}

type stepsDelegate struct {
	list.DefaultDelegate
}

func newStepsDelegate() stepsDelegate {
	d := list.NewDefaultDelegate()
	return stepsDelegate{DefaultDelegate: d}
}

func (d stepsDelegate) Render(w io.Writer, m list.Model, index int, item list.Item) {
	si, ok := item.(StepItem)
	if !ok {
		d.DefaultDelegate.Render(w, m, index, item)
		return
	}

	marker := "[ ]"
	if si.Selected {
		marker = "[x]"
	}

	label := si.TitleTxt
	if si.DescTxt != "" {
		label = fmt.Sprintf("%s - %s", si.TitleTxt, si.DescTxt)
	}

	line := fmt.Sprintf("%s %s", marker, label)
	if index == m.Index() {
		line = d.Styles.SelectedTitle.Render(line)
	} else {
		line = d.Styles.NormalTitle.Render(line)
	}

	fmt.Fprintln(w, line)
}
```

internal/tui/overrides_url.go
```
package tui

import (
	"strings"

	"github.com/charmbracelet/bubbles/textinput"
	tea "github.com/charmbracelet/bubbletea"
	"github.com/charmbracelet/lipgloss"
)

type URLInputModel struct {
	input textinput.Model
	err   string
}

func NewURLInputModel() URLInputModel {
	ti := textinput.New()
	ti.Placeholder = "https://chat.openai.com/project/..."
	ti.CharLimit = 200
	ti.Width = 50

	return URLInputModel{input: ti}
}

func (m URLInputModel) Init() tea.Cmd {
	return textinput.Blink
}

func (m URLInputModel) Update(msg tea.Msg) (URLInputModel, tea.Cmd) {
	var cmd tea.Cmd
	m.input, cmd = m.input.Update(msg)
	m.err = ""
	if !m.IsValid() {
		m.err = "Invalid URL (must start with http:// or https://)"
	}
	return m, cmd
}

func (m URLInputModel) Value() string {
	return strings.TrimSpace(m.input.Value())
}

func (m URLInputModel) IsValid() bool {
	v := m.Value()
	if v == "" {
		return true
	}
	return strings.HasPrefix(v, "http://") || strings.HasPrefix(v, "https://")
}

func (m URLInputModel) View() string {
	body := m.input.View()
	if m.err != "" {
		body = body + "\n" + lipgloss.NewStyle().Foreground(lipgloss.Color("196")).Render(m.err)
	}
	return body
}

func (m *URLInputModel) SetValue(v string) {
	m.input.SetValue(v)
}

func (m *URLInputModel) Focus() {
	m.input.Focus()
}

func (m *URLInputModel) Blur() {
	m.input.Blur()
}
```

internal/tui/preview_test.go
```
package tui

import (
	"strings"
	"testing"

	"github.com/user/oraclepack/internal/exec"
	"github.com/user/oraclepack/internal/state"
	"github.com/user/oraclepack/internal/types"
)

func TestStepPreviewContentUnwrapped(t *testing.T) {
	p := &types.Pack{
		Steps: []types.Step{
			{ID: "01", OriginalLine: "Step 1", Code: "echo hello"},
		},
	}
	r := exec.NewRunner(exec.RunnerOptions{})
	s := &state.RunState{}
	m := NewModel(p, r, s, "", 0, "over", false, false, 0, false, "auto")
	m.width = 80
	m.previewID = "01"
	m.previewWrap = false

	content := m.stepPreviewContent()
	if !strings.Contains(content, "Step 01") {
		t.Fatalf("expected header to include step id, got %q", content)
	}
	if !strings.Contains(content, "echo hello") {
		t.Fatalf("expected content to include code, got %q", content)
	}
}
```

internal/tui/tui.go
```
package tui

import (
	"context"
	"fmt"
	"strings"
	"time"

	"github.com/charmbracelet/bubbles/list"
	"github.com/charmbracelet/bubbles/spinner"
	"github.com/charmbracelet/bubbles/textinput"
	"github.com/charmbracelet/bubbles/viewport"
	tea "github.com/charmbracelet/bubbletea"
	"github.com/charmbracelet/lipgloss"
	"github.com/user/oraclepack/internal/exec"
	"github.com/user/oraclepack/internal/overrides"
	"github.com/user/oraclepack/internal/pack"
	"github.com/user/oraclepack/internal/render"
	"github.com/user/oraclepack/internal/state"
	"github.com/user/oraclepack/internal/types"
)

type ViewState int

const (
	ViewSteps ViewState = iota
	ViewRunning
	ViewDone
	ViewOverrides
	ViewStepPreview
)

type item struct {
	id     string
	title  string
	desc   string
	status state.Status
}

func (i item) Title() string       { return i.title }
func (i item) Description() string { return i.desc }
func (i item) FilterValue() string { return i.title }

type Model struct {
	list        list.Model
	viewport    viewport.Model
	spinner     spinner.Model
	filterInput textinput.Model
	urlInput    URLInputModel
	urlPicker   URLPickerModel
	pack        *types.Pack
	runner      *exec.Runner
	state       *state.RunState
	statePath   string

	width  int
	height int

	viewState     ViewState
	running       bool
	runAll        bool // State for sequential execution
	currentIdx    int
	autoRun       bool // Config to auto-start on init
	previewID     string
	previewWrap   bool
	previewNotice string

	// Filtering state
	allSteps     []item // Store all items to support dynamic filtering
	roiThreshold float64
	roiMode      string
	isFiltering  bool
	isEditingURL bool
	isPickingURL bool

	overridesFlow         OverridesFlowModel
	appliedOverrides      *overrides.RuntimeOverrides
	chatGPTURL            string
	outputVerify          bool
	outputRetries         int
	outputRequireHeadings bool
	outputChunkMode       string

	err      error
	logLines []string
	logChan  chan string
}

func NewModel(p *types.Pack, r *exec.Runner, s *state.RunState, statePath string, roiThreshold float64, roiMode string, autoRun bool, outputVerify bool, outputRetries int, outputRequireHeadings bool, outputChunkMode string) Model {
	if s != nil {
		if s.ROIThreshold > 0 {
			roiThreshold = s.ROIThreshold
		}
		if s.ROIMode != "" {
			roiMode = s.ROIMode
		}
	}
	var allItems []item
	for _, step := range p.Steps {
		allItems = append(allItems, item{
			id:    step.ID,
			title: fmt.Sprintf("Step %s", step.ID),
			desc:  step.OriginalLine,
		})
	}

	ti := textinput.New()
	ti.Placeholder = "Enter ROI threshold (e.g. 2.5)"
	ti.CharLimit = 10
	ti.Width = 20

	l := list.New([]list.Item{}, list.NewDefaultDelegate(), 0, 0)
	l.Title = "Oracle Pack Steps"

	sp := spinner.New()
	sp.Spinner = spinner.Dot
	sp.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("205"))

	vp := viewport.New(0, 0)
	vp.SetContent("Press Enter to run selected, 'a' to run all filtered steps, 'f' to set ROI threshold, 'm' to toggle ROI mode, 'v' to view step, 'o' to configure overrides, 'u' for ChatGPT URL, 'U' to pick a saved URL.")

	projectPath := ProjectURLStorePath(statePath, p.Source)
	globalPath := GlobalURLStorePath()
	urlPicker := NewURLPickerModel(projectPath, globalPath)
	resolvedURL := r.ChatGPTURL
	if resolvedURL == "" {
		resolvedURL = urlPicker.DefaultURL()
	}
	if resolvedURL != "" {
		r.ChatGPTURL = resolvedURL
	}

	m := Model{
		list:                  l,
		viewport:              vp,
		spinner:               sp,
		filterInput:           ti,
		urlInput:              NewURLInputModel(),
		urlPicker:             urlPicker,
		pack:                  p,
		runner:                r,
		state:                 s,
		statePath:             statePath,
		autoRun:               autoRun,
		allSteps:              allItems,
		roiThreshold:          roiThreshold,
		roiMode:               roiMode,
		logChan:               make(chan string, 100),
		viewState:             ViewSteps,
		overridesFlow:         NewOverridesFlowModel(p.Steps, r.OracleFlags, RunnerOptionsFromRunner(r)),
		chatGPTURL:            resolvedURL,
		previewWrap:           true,
		outputVerify:          outputVerify,
		outputRetries:         outputRetries,
		outputRequireHeadings: outputRequireHeadings,
		outputChunkMode:       outputChunkMode,
	}
	m.urlInput.SetValue(resolvedURL)
	m.urlInput.Blur()

	// Apply initial filter
	return m.refreshList()
}

func (m Model) refreshList() Model {
	var filtered []list.Item
	for _, it := range m.allSteps {
		// Find the original step to check ROI
		var step *types.Step
		for _, s := range m.pack.Steps {
			if s.ID == it.id {
				step = &s
				break
			}
		}
		if step == nil {
			continue
		}

		if m.roiThreshold > 0 {
			if m.roiMode == "under" {
				if step.ROI >= m.roiThreshold {
					continue
				}
			} else {
				if step.ROI < m.roiThreshold {
					continue
				}
			}
		}
		filtered = append(filtered, it)
	}
	m.list.SetItems(filtered)
	return m
}

type StartAutoRunMsg struct{}

func (m Model) Init() tea.Cmd {
[TRUNCATED]
```

internal/tui/tui_test.go
```
package tui

import (
	"testing"

	"github.com/user/oraclepack/internal/exec"
	"github.com/user/oraclepack/internal/state"
	"github.com/user/oraclepack/internal/types"
)

func TestInitAutoRun(t *testing.T) {
	p := &types.Pack{
		Steps: []types.Step{
			{ID: "01", Number: 1, Code: "echo hello"},
		},
	}
	r := exec.NewRunner(exec.RunnerOptions{})
	s := &state.RunState{}

	// Test case 1: autoRun = true
	modelAuto := NewModel(p, r, s, "", 0, "over", true, false, 0, false, "auto")
	cmdAuto := modelAuto.Init()

	if cmdAuto == nil {
		t.Fatal("expected Init cmd to be non-nil when autoRun is true")
	}
	// Note: We can't easily assert the content of a Batch command in a unit test.

	// Test case 2: autoRun = false
	modelManual := NewModel(p, r, s, "", 0, "over", false, false, 0, false, "auto")
	// Even with autoRun false, we have textinput.Blink, so Init is not nil.
	cmdManual := modelManual.Init()
	if cmdManual == nil {
		t.Fatal("expected Init cmd to be non-nil due to textinput.Blink")
	}
}
```

internal/tui/url_picker.go
```
package tui

import (
	"fmt"
	"strings"

	"github.com/charmbracelet/bubbles/list"
	"github.com/charmbracelet/bubbles/textinput"
	tea "github.com/charmbracelet/bubbletea"
	"github.com/charmbracelet/lipgloss"
)

type URLPickedMsg struct {
	URL string
}

type URLPickerCancelledMsg struct{}

type urlItem struct {
	name  string
	url   string
	scope string
}

func (i urlItem) Title() string       { return i.name }
func (i urlItem) Description() string { return fmt.Sprintf("%s • %s", i.scope, i.url) }
func (i urlItem) FilterValue() string { return i.name }

type URLPickerModel struct {
	list list.Model

	projectPath string
	globalPath  string
	project     URLStore
	global      URLStore

	editing   bool
	editName  textinput.Model
	editURL   textinput.Model
	editScope string
	editIdx   int
	editIsNew bool

	errMsg string
}

func NewURLPickerModel(projectPath, globalPath string) URLPickerModel {
	project, _ := LoadURLStore(projectPath)
	global, _ := LoadURLStore(globalPath)

	items := makeURLItems(project, global)
	l := list.New(items, list.NewDefaultDelegate(), 0, 0)
	l.Title = "ChatGPT Project URLs"
	l.SetFilteringEnabled(true)
	selectDefault(&l, project, global)

	name := textinput.New()
	name.Placeholder = "Name (e.g., Core Project)"
	name.CharLimit = 60
	name.Width = 40

	url := textinput.New()
	url.Placeholder = "https://chatgpt.com/g/.../project"
	url.CharLimit = 200
	url.Width = 60

	return URLPickerModel{
		list:        l,
		projectPath: projectPath,
		globalPath:  globalPath,
		project:     project,
		global:      global,
		editName:    name,
		editURL:     url,
	}
}

func (m URLPickerModel) Init() tea.Cmd {
	return nil
}

func (m URLPickerModel) Update(msg tea.Msg) (URLPickerModel, tea.Cmd) {
	if m.editing {
		return m.updateEdit(msg)
	}

	switch msg := msg.(type) {
	case tea.KeyMsg:
		switch msg.String() {
		case "esc":
			return m, func() tea.Msg { return URLPickerCancelledMsg{} }
		case "enter":
			item, ok := m.list.SelectedItem().(urlItem)
			if !ok {
				return m, nil
			}
			m.touch(item)
			return m, func() tea.Msg { return URLPickedMsg{URL: item.url} }
		case "a":
			m.startEdit(urlScopeProject, "", "", true)
			return m, nil
		case "e":
			item, ok := m.list.SelectedItem().(urlItem)
			if !ok {
				return m, nil
			}
			m.startEdit(item.scope, item.name, item.url, false)
			return m, nil
		case "d":
			item, ok := m.list.SelectedItem().(urlItem)
			if !ok {
				return m, nil
			}
			m.delete(item)
			return m, nil
		case "s":
			item, ok := m.list.SelectedItem().(urlItem)
			if !ok {
				return m, nil
			}
			m.setDefault(item)
			return m, nil
		}
	}

	var cmd tea.Cmd
	m.list, cmd = m.list.Update(msg)
	return m, cmd
}

func (m *URLPickerModel) SetSize(width, height int) {
	m.list.SetSize(width, height-4)
}

func (m URLPickerModel) View() string {
	if m.editing {
		return m.editView()
	}

	help := lipgloss.NewStyle().Faint(true).Render("[enter] use  [a] add  [e] edit  [d] delete  [s] default  [esc] cancel")
	return m.list.View() + "\n" + help
}

func makeURLItems(project URLStore, global URLStore) []list.Item {
	var items []list.Item
	for _, it := range project.Items {
		items = append(items, urlItem{name: it.Name, url: it.URL, scope: urlScopeProject})
	}
	for _, it := range global.Items {
		items = append(items, urlItem{name: it.Name, url: it.URL, scope: urlScopeGlobal})
	}
	return items
}

func selectDefault(l *list.Model, project URLStore, global URLStore) {
	if l == nil {
		return
	}
	name, scope := defaultNameScope(project, global)
	if name == "" {
		return
	}
	for idx, item := range l.Items() {
		if it, ok := item.(urlItem); ok && it.name == name && it.scope == scope {
			l.Select(idx)
			return
		}
	}
}

func defaultNameScope(project URLStore, global URLStore) (string, string) {
	if project.Default != "" {
		return project.Default, urlScopeProject
	}
	if global.Default != "" {
		return global.Default, urlScopeGlobal
	}
	return "", ""
}

func (m URLPickerModel) DefaultURL() string {
	name, scope := defaultNameScope(m.project, m.global)
	if name == "" {
		return ""
	}
	store := m.storeFor(scope)
	if store == nil {
		return ""
	}
	for _, it := range store.Items {
		if it.Name == name {
			return it.URL
		}
	}
	return ""
}

func (m *URLPickerModel) refresh() {
	m.list.SetItems(makeURLItems(m.project, m.global))
	selectDefault(&m.list, m.project, m.global)
}

[TRUNCATED]
```

internal/tui/url_store.go
```
package tui

import (
	"encoding/json"
	"errors"
	"os"
	"path/filepath"
	"strings"
	"time"
)

const (
	urlScopeProject = "project"
	urlScopeGlobal  = "global"
)

type URLItem struct {
	Name     string `json:"name"`
	URL      string `json:"url"`
	LastUsed string `json:"lastUsed,omitempty"`
}

type URLStore struct {
	Default string    `json:"default"`
	Items   []URLItem `json:"items"`
}

func LoadURLStore(path string) (URLStore, error) {
	if path == "" {
		return URLStore{}, nil
	}
	data, err := os.ReadFile(path)
	if err != nil {
		if errors.Is(err, os.ErrNotExist) {
			return URLStore{}, nil
		}
		return URLStore{}, err
	}
	var store URLStore
	if err := json.Unmarshal(data, &store); err != nil {
		return URLStore{}, err
	}
	return store, nil
}

func SaveURLStore(path string, store URLStore) error {
	if path == "" {
		return nil
	}
	if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
		return err
	}
	data, err := json.MarshalIndent(store, "", "  ")
	if err != nil {
		return err
	}
	return os.WriteFile(path, data, 0o644)
}

func ProjectURLStorePath(statePath, packSource string) string {
	if statePath != "" {
		base := strings.TrimSuffix(statePath, ".state.json")
		return base + ".chatgpt-urls.json"
	}
	if packSource == "" {
		return ""
	}
	return packSource + ".chatgpt-urls.json"
}

func GlobalURLStorePath() string {
	home, err := os.UserHomeDir()
	if err != nil || home == "" {
		return ""
	}
	return filepath.Join(home, ".oraclepack", "chatgpt-urls.json")
}

func nowRFC3339() string {
	return time.Now().UTC().Format(time.RFC3339)
}

func isValidURL(value string) bool {
	v := strings.TrimSpace(value)
	if v == "" {
		return false
	}
	return strings.HasPrefix(v, "http://") || strings.HasPrefix(v, "https://")
}
```

internal/tui/url_store_test.go
```
package tui

import (
	"path/filepath"
	"testing"
)

func TestURLStoreSaveLoad(t *testing.T) {
	dir := t.TempDir()
	path := filepath.Join(dir, "urls.json")

	store := URLStore{
		Default: "Primary",
		Items: []URLItem{{
			Name: "Primary",
			URL:  "https://chatgpt.com/g/primary",
		}},
	}

	if err := SaveURLStore(path, store); err != nil {
		t.Fatalf("failed to save store: %v", err)
	}

	loaded, err := LoadURLStore(path)
	if err != nil {
		t.Fatalf("failed to load store: %v", err)
	}

	if loaded.Default != store.Default {
		t.Fatalf("expected default %q, got %q", store.Default, loaded.Default)
	}
	if len(loaded.Items) != 1 || loaded.Items[0].URL != store.Items[0].URL {
		t.Fatalf("loaded items mismatch: %+v", loaded.Items)
	}
}

func TestURLPickerDefaultURLPrefersProject(t *testing.T) {
	dir := t.TempDir()
	projectPath := filepath.Join(dir, "project.json")
	globalPath := filepath.Join(dir, "global.json")

	project := URLStore{
		Default: "Project",
		Items: []URLItem{{
			Name: "Project",
			URL:  "https://chatgpt.com/g/project",
		}},
	}
	global := URLStore{
		Default: "Global",
		Items: []URLItem{{
			Name: "Global",
			URL:  "https://chatgpt.com/g/global",
		}},
	}

	if err := SaveURLStore(projectPath, project); err != nil {
		t.Fatalf("failed to save project store: %v", err)
	}
	if err := SaveURLStore(globalPath, global); err != nil {
		t.Fatalf("failed to save global store: %v", err)
	}

	picker := NewURLPickerModel(projectPath, globalPath)
	if got := picker.DefaultURL(); got != project.Items[0].URL {
		t.Fatalf("expected project default URL %q, got %q", project.Items[0].URL, got)
	}
}
```

internal/templates/template_test.go
```
package templates

import (
	"os"
	"testing"

	"github.com/user/oraclepack/internal/pack"
)

func TestRenderTicketActionPack(t *testing.T) {
	got := RenderTicketActionPack()
	if got == "" {
		t.Fatal("expected non-empty template")
	}

	// Golden comparison
	data, err := os.ReadFile("ticket-action-pack.md")
	if err != nil {
		t.Fatalf("read template: %v", err)
	}
	if string(data) != got {
		t.Fatalf("template mismatch with golden file")
	}

	// Ensure pack is parseable and validates 20-step contract.
	p, err := pack.Parse([]byte(got))
	if err != nil {
		t.Fatalf("Parse failed: %v", err)
	}
	if err := pack.Validate(p); err != nil {
		t.Fatalf("Validate failed: %v", err)
	}
	if len(p.Steps) != 20 {
		t.Fatalf("expected 20 steps, got %d", len(p.Steps))
	}
}
```

internal/templates/ticket-action-pack.md
```
# Ticket Action Pack

```bash
out_dir=".oraclepack/ticketify"
--write-output

# 01)
echo "Build tickets index"

# 02)
echo "Generate actions json"

# 03)
echo "Generate tickets PRD"

# 04)
echo "Prep taskmaster inputs"

# 05)
task-master parse-prd .taskmaster/docs/prd.md

# 06)
task-master analyze-complexity --research

# 07)
task-master expand --all --research

# 08)
echo "Prepare headless automation"

# 09)
if command -v gemini >/dev/null 2>&1; then
  gemini run "Select next tasks" --write-output ".oraclepack/ticketify/next.json"
else
  echo "Skipped: gemini missing"
fi

# 10)
if command -v codex >/dev/null 2>&1; then
  codex exec "Implement tasks" --write-output ".oraclepack/ticketify/codex-implement.md"
else
  echo "Skipped: codex missing"
fi

# 11)
if command -v codex >/dev/null 2>&1; then
  codex exec "Verify changes" --write-output ".oraclepack/ticketify/codex-verify.md"
else
  echo "Skipped: codex missing"
fi

# 12)
if command -v gemini >/dev/null 2>&1; then
  gemini run "Review outputs" --write-output ".oraclepack/ticketify/gemini-review.json"
else
  echo "Skipped: gemini missing"
fi

# 13)
if command -v codex >/dev/null 2>&1; then
  codex exec "Prepare fixes" --write-output ".oraclepack/ticketify/codex-fixes.md"
else
  echo "Skipped: codex missing"
fi

# 14)
echo "Summarize results"

# 15)
echo "Prepare release notes"

# 16)
if command -v codex >/dev/null 2>&1; then
  codex exec "Draft PR description" --write-output ".oraclepack/ticketify/PR.md"
else
  echo "Skipped: codex missing"
fi

# 17)
echo "Finalize checklist"

# 18)
echo "Post-run cleanup"

# 19)
echo "Audit artifacts"

# 20)
echo "Done"
```
```

internal/templates/ticket_action_pack.go
```
package templates

import _ "embed"

//go:embed ticket-action-pack.md
var ticketActionPack string

// RenderTicketActionPack returns the canonical ticket action pack template.
func RenderTicketActionPack() string {
	return ticketActionPack
}
```

internal/types/pack.go
```
package types

// Pack represents a parsed oracle pack.
type Pack struct {
	Prelude     Prelude `json:"prelude" yaml:"prelude"`
	Steps       []Step  `json:"steps" yaml:"steps"`
	Source      string  `json:"source,omitempty" yaml:"source,omitempty"`
	OutDir      string  `json:"out_dir,omitempty" yaml:"out_dir,omitempty"`
	WriteOutput bool    `json:"write_output" yaml:"write_output"`
}

// Prelude contains the shell code that runs before any steps.
type Prelude struct {
	Code string `json:"code" yaml:"code"`
}

// Step represents an individual executable step within the pack.
type Step struct {
	ID           string  `json:"id" yaml:"id"`                             // e.g., "01"
	Number       int     `json:"number" yaml:"number"`                     // e.g., 1
	Code         string  `json:"code" yaml:"code"`                         // The bash code
	OriginalLine string  `json:"original_line" yaml:"original_line"`       // The header line, e.g., "# 01)"
	ROI          float64 `json:"roi,omitempty" yaml:"roi,omitempty"`       // Return on Investment value extracted from header
	Impact       string  `json:"impact,omitempty" yaml:"impact,omitempty"` // Optional impact metadata extracted from step comments
}
```

internal/types/pack_test.go
```
package types

import (
	"encoding/json"
	"reflect"
	"testing"

	"github.com/goccy/go-yaml"
)

func TestPackJSONRoundTrip(t *testing.T) {
	original := Pack{
		Prelude: Prelude{Code: "echo prelude"},
		Steps: []Step{
			{
				ID:           "01",
				Number:       1,
				Code:         "echo hello",
				OriginalLine: "# 01) Example",
				ROI:          3.2,
				Impact:       "High",
			},
		},
		Source:      "pack.md",
		OutDir:      "dist",
		WriteOutput: true,
	}

	data, err := json.Marshal(original)
	if err != nil {
		t.Fatalf("json marshal: %v", err)
	}

	var decoded Pack
	if err := json.Unmarshal(data, &decoded); err != nil {
		t.Fatalf("json unmarshal: %v", err)
	}

	if !reflect.DeepEqual(original, decoded) {
		t.Fatalf("json round-trip mismatch: %#v != %#v", original, decoded)
	}
}

func TestPackYAMLRoundTrip(t *testing.T) {
	original := Pack{
		Prelude: Prelude{Code: "echo prelude"},
		Steps: []Step{
			{
				ID:           "02",
				Number:       2,
				Code:         "echo yaml",
				OriginalLine: "# 02) Example",
				ROI:          1.1,
				Impact:       "Low",
			},
		},
		Source:      "pack.yaml",
		OutDir:      "out",
		WriteOutput: false,
	}

	data, err := yaml.Marshal(original)
	if err != nil {
		t.Fatalf("yaml marshal: %v", err)
	}

	var decoded Pack
	if err := yaml.Unmarshal(data, &decoded); err != nil {
		t.Fatalf("yaml unmarshal: %v", err)
	}

	if !reflect.DeepEqual(original, decoded) {
		t.Fatalf("yaml round-trip mismatch: %#v != %#v", original, decoded)
	}
}
```

internal/types/verification.go
```
package types

// OutputContract describes the expected response shape for a step.
type OutputContract string

const (
	OutputContractUnknown          OutputContract = ""
	OutputContractAllSections      OutputContract = "all_sections"
	OutputContractDirectAnswerOnly OutputContract = "direct_answer_only"
	OutputContractChunkedBySuffix  OutputContract = "chunked_by_suffix"
)

// OutputFailure captures a missing or invalid output artifact.
type OutputFailure struct {
	StepID        string   `json:"step_id,omitempty" yaml:"step_id,omitempty"`
	Path          string   `json:"path,omitempty" yaml:"path,omitempty"`
	MissingTokens []string `json:"missing_tokens,omitempty" yaml:"missing_tokens,omitempty"`
	Error         string   `json:"error,omitempty" yaml:"error,omitempty"`
}

// SyntaxFinding captures a structural or syntax issue in generated bash.
type SyntaxFinding struct {
	StepID  string `json:"step_id,omitempty" yaml:"step_id,omitempty"`
	Line    int    `json:"line" yaml:"line"`
	Token   string `json:"token,omitempty" yaml:"token,omitempty"`
	Message string `json:"message" yaml:"message"`
}
```

internal/validate/artifact_gate.go
```
package validate

import (
	"errors"

	"github.com/user/oraclepack/internal/artifacts"
	"github.com/user/oraclepack/internal/foundation"
	"github.com/user/oraclepack/internal/state"
	"github.com/user/oraclepack/internal/tools"
	"github.com/user/oraclepack/internal/types"
)

// ArtifactGateValidator checks expected artifacts after a step.
type ArtifactGateValidator struct {
	Contract artifacts.Contract
}

func (v ArtifactGateValidator) Validate(step *types.Step, kind tools.ToolKind, toolPresent bool) (state.Status, string) {
	if step == nil {
		return state.StatusSuccess, ""
	}
	contract := v.Contract
	if contract == nil {
		contract = artifacts.DefaultContract()
	}
	if !toolPresent {
		if _, ok := contract[step.ID]; ok {
			return state.StatusSkipped, "tool missing; skipping artifact gate"
		}
		return state.StatusSuccess, ""
	}
	err := artifacts.EvaluateGates(step.ID, contract)
	if err == nil {
		return state.StatusSuccess, ""
	}
	if errors.Is(err, foundation.ErrArtifactMissing) {
		return state.StatusFailed, err.Error()
	}
	return state.StatusFailed, err.Error()
}
```

internal/validate/artifact_gate_test.go
```
package validate

import (
	"os"
	"path/filepath"
	"testing"

	"github.com/user/oraclepack/internal/artifacts"
	"github.com/user/oraclepack/internal/state"
	"github.com/user/oraclepack/internal/tools"
	"github.com/user/oraclepack/internal/types"
)

func TestArtifactGateValidator(t *testing.T) {
	dir := t.TempDir()
	path := filepath.Join(dir, "next.json")
	contract := artifacts.Contract{"09": {path}}
	step := &types.Step{ID: "09"}
	v := ArtifactGateValidator{Contract: contract}

	status, _ := v.Validate(step, tools.ToolCodex, true)
	if status != state.StatusFailed {
		t.Fatalf("expected failed for missing artifact, got %s", status)
	}

	if err := os.WriteFile(path, []byte("ok"), 0644); err != nil {
		t.Fatalf("write: %v", err)
	}
	status, _ = v.Validate(step, tools.ToolCodex, true)
	if status != state.StatusSuccess {
		t.Fatalf("expected success, got %s", status)
	}

	status, _ = v.Validate(step, tools.ToolCodex, false)
	if status != state.StatusSkipped {
		t.Fatalf("expected skipped when tool missing, got %s", status)
	}
}
```

internal/validate/composite.go
```
package validate

import (
	"github.com/user/oraclepack/internal/state"
	"github.com/user/oraclepack/internal/types"
)

// CompositeValidator coordinates multiple validators for all steps.
type CompositeValidator struct {
	ToolPresence ToolPresenceValidator
	OracleDryRun OracleDryRunValidator
	ArtifactGate ArtifactGateValidator
}

// ValidatePack validates all steps in a pack and returns per-step results.
func (v CompositeValidator) ValidatePack(p *types.Pack) []StepResult {
	if p == nil {
		return nil
	}
	results := make([]StepResult, 0, len(p.Steps))
	for i := range p.Steps {
		step := &p.Steps[i]
		kind := DetectToolKind(step)

		status, reason, toolPresent := v.ToolPresence.Validate(step, kind)
		if status == state.StatusSkipped {
			results = append(results, StepResult{
				StepID:   step.ID,
				ToolKind: kind,
				Status:   status,
				Error:    reason,
			})
			continue
		}

		if oracleStatus, oracleErr := v.OracleDryRun.Validate(step, kind); oracleStatus == state.StatusFailed {
			results = append(results, StepResult{
				StepID:   step.ID,
				ToolKind: kind,
				Status:   oracleStatus,
				Error:    oracleErr,
			})
			continue
		}

		gateStatus, gateErr := v.ArtifactGate.Validate(step, kind, toolPresent)
		finalStatus := gateStatus
		errMsg := gateErr
		if finalStatus == "" {
			finalStatus = state.StatusSuccess
		}
		results = append(results, StepResult{
			StepID:   step.ID,
			ToolKind: kind,
			Status:   finalStatus,
			Error:    errMsg,
		})
	}
	return results
}
```

internal/validate/composite_test.go
```
package validate

import (
	"testing"

	"github.com/user/oraclepack/internal/state"
	"github.com/user/oraclepack/internal/types"
)

func TestCompositeValidator(t *testing.T) {
	p := &types.Pack{
		Steps: []types.Step{
			{ID: "01", Code: "codex exec \"hi\""},
		},
	}
	cv := CompositeValidator{
		ToolPresence: ToolPresenceValidator{Checker: fakeChecker{found: map[string]bool{"codex": false}}},
	}
	results := cv.ValidatePack(p)
	if len(results) != 1 {
		t.Fatalf("expected 1 result, got %d", len(results))
	}
	if results[0].Status != state.StatusSkipped {
		t.Fatalf("expected skipped, got %s", results[0].Status)
	}
}
```

internal/validate/oracle.go
```
package validate

import (
	"github.com/user/oraclepack/internal/state"
	"github.com/user/oraclepack/internal/tools"
	"github.com/user/oraclepack/internal/types"
)

// OracleDryRunValidator is a placeholder for oracle-specific validation.
type OracleDryRunValidator struct{}

// Validate returns success for non-oracle tools and no-op for oracle until wired.
func (OracleDryRunValidator) Validate(step *types.Step, kind tools.ToolKind) (state.Status, string) {
	if kind != tools.ToolOracle {
		return state.StatusSuccess, ""
	}
	return state.StatusSuccess, ""
}
```

internal/validate/presence.go
```
package validate

import (
	"fmt"

	"github.com/user/oraclepack/internal/shell"
	"github.com/user/oraclepack/internal/state"
	"github.com/user/oraclepack/internal/tools"
	"github.com/user/oraclepack/internal/types"
)

// ToolPresenceValidator checks that tool binaries exist on PATH.
type ToolPresenceValidator struct {
	Checker tools.PresenceChecker
}

// Validate returns skipped if the tool is missing.
func (v ToolPresenceValidator) Validate(step *types.Step, kind tools.ToolKind) (state.Status, string, bool) {
	if kind == tools.ToolUnknown {
		return state.StatusSuccess, "", true
	}
	meta, ok := tools.Metadata(kind)
	if !ok {
		return state.StatusSuccess, "", true
	}
	checker := v.Checker
	if checker == nil {
		checker = shellChecker{}
	}
	if _, found := checker.DetectBinary(meta.Name); !found {
		return state.StatusSkipped, fmt.Sprintf("tool %s not found on PATH", meta.Name), false
	}
	return state.StatusSuccess, "", true
}

type shellChecker struct{}

func (shellChecker) DetectBinary(name string) (string, bool) {
	return shell.DetectBinary(name)
}
```

internal/validate/presence_test.go
```
package validate

import (
	"testing"

	"github.com/user/oraclepack/internal/state"
	"github.com/user/oraclepack/internal/tools"
	"github.com/user/oraclepack/internal/types"
)

type fakeChecker struct {
	found map[string]bool
}

func (f fakeChecker) DetectBinary(name string) (string, bool) {
	return name, f.found[name]
}

func TestToolPresenceValidator(t *testing.T) {
	step := &types.Step{ID: "01", Code: "codex exec \"hi\""}
	v := ToolPresenceValidator{Checker: fakeChecker{found: map[string]bool{"codex": false}}}
	status, reason, present := v.Validate(step, tools.ToolCodex)
	if status != state.StatusSkipped || present {
		t.Fatalf("expected skipped/missing, got status=%s present=%v", status, present)
	}
	if reason == "" {
		t.Fatalf("expected reason for missing tool")
	}
}
```

internal/validate/report.go
```
package validate

// ValidationReport captures results for all steps.
type ValidationReport struct {
	Steps []StepResult
}
```

internal/validate/types.go
```
package validate

import (
	"strings"

	"github.com/user/oraclepack/internal/dispatch"
	"github.com/user/oraclepack/internal/state"
	"github.com/user/oraclepack/internal/tools"
	"github.com/user/oraclepack/internal/types"
)

// StepResult captures validation output for a step.
type StepResult struct {
	StepID   string
	ToolKind tools.ToolKind
	Status   state.Status
	Error    string
}

// DetectToolKind scans a step for a known tool prefix.
func DetectToolKind(step *types.Step) tools.ToolKind {
	if step == nil {
		return tools.ToolUnknown
	}
	lines := strings.Split(step.Code, "\n")
	for _, line := range lines {
		trimmed := strings.TrimSpace(line)
		if trimmed == "" || strings.HasPrefix(trimmed, "#") {
			continue
		}
		if cls, ok := dispatch.Classify(trimmed); ok {
			return cls.Kind
		}
		break
	}
	return tools.ToolUnknown
}
```

</source_code>

--- codefetch/oraclepack-question-responses.md ---
<filetree>
Project Structure:
└── docs
    ├── oracle-questions-2026-01-09-030000
    │   ├── actions
    │   │   ├── 01-contracts-interfaces-ticket-surface-direct-answer.md
    │   │   ├── 01-contracts-interfaces-ticket-surface-missing-evidence.md
    │   │   ├── 01-contracts-interfaces-ticket-surface-next-experiment.md
    │   │   └── 01-contracts-interfaces-ticket-surface-risks-unknowns.md
    │   ├── packs
    │   │   ├── actions.md
    │   │   ├── mcp.md
    │   │   ├── misc.md
    │   │   ├── other.md
    │   │   ├── prd-tui.md
    │   │   └── raw-exports.md
    │   ├── _groups.json
    │   └── manifest.json
    └── oracle-pack-2026-01-09.md

</filetree>

<source_code>
docs/oracle-pack-2026-01-09.md
```
# Oracle Pack — Unknown (Gold Stage 1)

## Parsed args
- codebase_name: Unknown
- constraints: None
- non_goals: None
- team_size: Unknown
- deadline: Unknown
- out_dir: docs/oracle-questions-2026-01-09
- oracle_cmd: oracle
- oracle_flags: --files-report
- engine: None
- model: None
- extra_files: (none)

Notes:
- Template is the contract. Keep the pack runner-ingestible.
- Exactly one fenced bash block in this whole document.
- Exactly 20 steps, numbered 01..20.
- Each step includes: ROI= impact= confidence= effort= horizon= category= reference=
- Categories must be exactly the fixed set used in Coverage check.

## Commands
```bash
# Optional preflight pattern:
# - Add --dry-run summary to preview what will be sent, and keep --files-report enabled when available.

# 01) ROI=4.2 impact=High confidence=High effort=Low horizon=Immediate category=contracts/interfaces reference=internal/cli/root.go
oracle \
  --files-report \
  --write-output "docs/oracle-questions-2026-01-09/01-contracts-interfaces-surface.md" \
  -f "README.md" \
  -f "internal/cli/root.go" \
  -p "$(cat <<'PROMPT'
Strategist question #01

Reference: internal/cli/root.go
Category: contracts/interfaces
Horizon: Immediate
ROI: 4.2 (impact=High, confidence=High, effort=Low)

Question:
Identify the primary public interface(s) of this system (CLI commands, flags, and pack format surface). For each, list the key inputs/outputs and where they are defined in the code.

Rationale (one sentence):
We need a trustworthy map of the system's outside-facing contract before deeper planning.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–6 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 02) ROI=3.6 impact=High confidence=Medium effort=Medium horizon=Immediate category=contracts/interfaces reference=internal/tools/types.go
oracle \
  --files-report \
  --write-output "docs/oracle-questions-2026-01-09/02-contracts-interfaces-integration.md" \
  -f "internal/tools/types.go" \
  -f "internal/exec/runner.go" \
  -p "$(cat <<'PROMPT'
Strategist question #02

Reference: internal/tools/types.go
Category: contracts/interfaces
Horizon: Immediate
ROI: 3.6 (impact=High, confidence=Medium, effort=Medium)

Question:
What are the top integration points with external tools or systems (oracle CLI, shell environment, filesystem, optional task-master/codex/gemini prefixes)? For each, point to the contract or config that declares it.

Rationale (one sentence):
Integration boundaries drive risk, deployment needs, and test strategy.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–6 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 03) ROI=3.4 impact=High confidence=Medium effort=Medium horizon=NearTerm category=invariants reference=internal/pack/parser.go
oracle \
  --files-report \
  --write-output "docs/oracle-questions-2026-01-09/03-invariants-domain.md" \
  -f "internal/pack/parser.go" \
  -f "internal/pack/types.go" \
  -p "$(cat <<'PROMPT'
Strategist question #03

Reference: internal/pack/parser.go
Category: invariants
Horizon: NearTerm
ROI: 3.4 (impact=High, confidence=Medium, effort=Medium)

Question:
List the system's most important invariants about pack structure and step metadata (e.g., numbering, ROI parsing, header format). For each, show where it is enforced or where enforcement is missing.

Rationale (one sentence):
Invariants define correctness and are the backbone of reliable changes.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–8 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 04) ROI=3.1 impact=Medium confidence=Medium effort=Medium horizon=NearTerm category=invariants reference=internal/pack/output_check.go
oracle \
  --files-report \
  --write-output "docs/oracle-questions-2026-01-09/04-invariants-validation.md" \
  -f "internal/pack/output_check.go" \
  -f "internal/pack/parser.go" \
  -p "$(cat <<'PROMPT'
Strategist question #04

Reference: internal/pack/output_check.go
Category: invariants
Horizon: NearTerm
ROI: 3.1 (impact=Medium, confidence=Medium, effort=Medium)

Question:
Where does validation happen (pack parsing, output section validation, tool detection)? Identify the validation boundaries and the most likely gaps that could cause inconsistent state or false positives.

Rationale (one sentence):
Knowing validation boundaries prevents regressions and reduces correctness risk.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–8 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 05) ROI=3.0 impact=Medium confidence=Medium effort=Low horizon=NearTerm category=caching/state reference=internal/state/types.go
oracle \
  --files-report \
  --write-output "docs/oracle-questions-2026-01-09/05-caching-state-layers.md" \
  -f "internal/state/types.go" \
  -f "internal/state/persist.go" \
  -p "$(cat <<'PROMPT'
Strategist question #05

Reference: internal/state/types.go
Category: caching/state
Horizon: NearTerm
ROI: 3.0 (impact=Medium, confidence=Medium, effort=Low)

Question:
What stateful components exist (run state, report artifacts, in-memory tracking within a run)? For each, describe lifecycle, persistence, and where it is implemented.

Rationale (one sentence):
State and caching are common sources of subtle bugs and performance issues.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–8 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 06) ROI=2.6 impact=Medium confidence=Low effort=Medium horizon=NearTerm category=caching/state reference=internal/app/run.go
oracle \
  --files-report \
  --write-output "docs/oracle-questions-2026-01-09/06-caching-state-consistency.md" \
  -f "internal/app/run.go" \
  -f "internal/state/persist.go" \
  -p "$(cat <<'PROMPT'
Strategist question #06

Reference: internal/app/run.go
Category: caching/state
Horizon: NearTerm
ROI: 2.6 (impact=Medium, confidence=Low, effort=Medium)

Question:
Identify the top consistency risks between in-memory run state and persisted state/report files (e.g., partial writes, skipped updates, resume behavior). Where are the knobs/configs that influence state persistence?

Rationale (one sentence):
Consistency failure modes often surface as "random bugs" and are expensive to debug.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–8 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 07) ROI=2.4 impact=Medium confidence=Low effort=Medium horizon=NearTerm category=background jobs reference=internal/app/run.go
oracle \
  --files-report \
  --write-output "docs/oracle-questions-2026-01-09/07-background-jobs-discovery.md" \
  -f "internal/app/run.go" \
  -f "internal/tui/tui.go" \
  -p "$(cat <<'PROMPT'
Strategist question #07

Reference: internal/app/run.go
Category: background jobs
Horizon: NearTerm
ROI: 2.4 (impact=Medium, confidence=Low, effort=Medium)

Question:
What background jobs/workers/scheduled tasks exist (if any)? For each, identify trigger mechanism, payload, retries, idempotency, and where it is defined.

Rationale (one sentence):
Background work affects reliability, cost, and operational complexity.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–8 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 08) ROI=2.2 impact=Medium confidence=Low effort=High horizon=NearTerm category=background jobs reference=internal/exec/runner.go
oracle \
  --files-report \
  --write-output "docs/oracle-questions-2026-01-09/08-background-jobs-reliability.md" \
  -f "internal/exec/runner.go" \
  -f "internal/app/run.go" \
  -p "$(cat <<'PROMPT'
Strategist question #08

Reference: internal/exec/runner.go
Category: background jobs
Horizon: NearTerm
ROI: 2.2 (impact=Medium, confidence=Low, effort=High)

Question:
Where are the main reliability controls for any background or long-running work (timeouts, retries, cancellation, concurrency limits), and what is missing or inconsistent?

Rationale (one sentence):
Reliability controls prevent incident loops and data corruption.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–8 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 09) ROI=3.3 impact=High confidence=Medium effort=Low horizon=Immediate category=observability reference=internal/report/generate.go
oracle \
  --files-report \
  --write-output "docs/oracle-questions-2026-01-09/09-observability-signals.md" \
  -f "internal/report/generate.go" \
  -f "internal/report/types.go" \
  -p "$(cat <<'PROMPT'
Strategist question #09

Reference: internal/report/generate.go
Category: observability
Horizon: Immediate
ROI: 3.3 (impact=High, confidence=Medium, effort=Low)

Question:
What observability signals exist (run reports, warnings, stdout/stderr logs), and what are the primary identifiers for correlating a step/run across components? Point to the code/config that defines them.

Rationale (one sentence):
You cannot operate or improve what you cannot measure or debug quickly.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–8 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 10) ROI=3.0 impact=High confidence=Medium effort=Medium horizon=Immediate category=observability reference=internal/report/io.go
oracle \
  --files-report \
  --write-output "docs/oracle-questions-2026-01-09/10-observability-gaps.md" \
  -f "internal/report/io.go" \
  -f "internal/exec/runner.go" \
  -p "$(cat <<'PROMPT'
Strategist question #10

Reference: internal/report/io.go
Category: observability
Horizon: Immediate
ROI: 3.0 (impact=High, confidence=Medium, effort=Medium)

Question:
Where are the biggest observability gaps (missing structured logs, missing metrics, missing trace spans)? Recommend the smallest additions that would most improve debugging.

Rationale (one sentence):
Targeted observability improvements compound across all future changes.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–8 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 11) ROI=3.2 impact=High confidence=Medium effort=Low horizon=Immediate category=permissions reference=internal/cli/root.go
oracle \
  --files-report \
  --write-output "docs/oracle-questions-2026-01-09/11-permissions-model.md" \
  -f "internal/cli/root.go" \
  -f "internal/app/app.go" \
  -p "$(cat <<'PROMPT'
Strategist question #11

Reference: internal/cli/root.go
Category: permissions
Horizon: Immediate
ROI: 3.2 (impact=High, confidence=Medium, effort=Low)

Question:
What is the permission model (roles/scopes/claims/ACLs), and where is it defined? If none, explain what access assumptions the CLI makes (filesystem, shell, external tools).

Rationale (one sentence):
Permission rules are a high-risk area with security and product impact.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–8 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 12) ROI=2.8 impact=High confidence=Low effort=Medium horizon=Immediate category=permissions reference=internal/cli/run.go
oracle \
  --files-report \
  --write-output "docs/oracle-questions-2026-01-09/12-permissions-enforcement.md" \
  -f "internal/cli/run.go" \
  -f "internal/app/run.go" \
  -p "$(cat <<'PROMPT'
Strategist question #12

Reference: internal/cli/run.go
Category: permissions
Horizon: Immediate
ROI: 2.8 (impact=High, confidence=Low, effort=Medium)

Question:
Where are permissions or access checks enforced (if at all)? Identify enforcement chokepoints and any bypass risks for filesystem or shell execution.

Rationale (one sentence):
Enforcement consistency prevents privilege escalation and policy drift.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–8 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 13) ROI=2.5 impact=Medium confidence=Low effort=Medium horizon=NearTerm category=migrations reference=internal/state/types.go
oracle \
  --files-report \
  --write-output "docs/oracle-questions-2026-01-09/13-migrations-schema.md" \
  -f "internal/state/types.go" \
  -f "internal/state/persist.go" \
  -p "$(cat <<'PROMPT'
Strategist question #13

Reference: internal/state/types.go
Category: migrations
Horizon: NearTerm
ROI: 2.5 (impact=Medium, confidence=Low, effort=Medium)

Question:
How are schema/config migrations handled (state file schema versions, report schema changes, pack format evolution)? Identify tooling, version fields, and how migrations are applied during upgrades.

Rationale (one sentence):
Migration mechanics are critical for safe releases and rollbacks.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–8 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 14) ROI=2.2 impact=Medium confidence=Low effort=High horizon=NearTerm category=migrations reference=internal/state/persist.go
oracle \
  --files-report \
  --write-output "docs/oracle-questions-2026-01-09/14-migrations-compat.md" \
  -f "internal/state/persist.go" \
  -f "internal/state/types.go" \
  -p "$(cat <<'PROMPT'
Strategist question #14

Reference: internal/state/persist.go
Category: migrations
Horizon: NearTerm
ROI: 2.2 (impact=Medium, confidence=Low, effort=High)

Question:
What are the backward/forward compatibility expectations for state and report files (e.g., loading older state, changing schema_version)? Identify where compatibility is ensured or currently risky.

Rationale (one sentence):
Compatibility strategy prevents outages during upgrades.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–8 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 15) ROI=3.1 impact=High confidence=Medium effort=Medium horizon=NearTerm category=UX flows reference=internal/tui/tui.go
oracle \
  --files-report \
  --write-output "docs/oracle-questions-2026-01-09/15-ux-flows-primary.md" \
  -f "internal/tui/tui.go" \
  -f "internal/cli/run.go" \
  -p "$(cat <<'PROMPT'
Strategist question #15

Reference: internal/tui/tui.go
Category: UX flows
Horizon: NearTerm
ROI: 3.1 (impact=High, confidence=Medium, effort=Medium)

Question:
What are the primary user/operator flows (TUI navigation, step selection, overrides, run/resume)? Map each to the main components/modules involved and note key state transitions.

Rationale (one sentence):
Flow maps reveal critical paths and help prioritize work with user impact.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 16) ROI=2.7 impact=Medium confidence=Medium effort=Medium horizon=NearTerm category=UX flows reference=internal/tui/overrides_flow.go
oracle \
  --files-report \
  --write-output "docs/oracle-questions-2026-01-09/16-ux-flows-edgecases.md" \
  -f "internal/tui/overrides_flow.go" \
  -f "internal/tui/tui.go" \
  -p "$(cat <<'PROMPT'
Strategist question #16

Reference: internal/tui/overrides_flow.go
Category: UX flows
Horizon: NearTerm
ROI: 2.7 (impact=Medium, confidence=Medium, effort=Medium)

Question:
For the primary flows, what are the top edge cases and "gotchas" (validation failures, canceled overrides, partial runs, retries)? Identify where these cases are handled and where they are missing.

Rationale (one sentence):
Edge-case handling is where many UX and reliability issues originate.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 17) ROI=3.4 impact=High confidence=Medium effort=Low horizon=Immediate category=failure modes reference=internal/errors/errors.go
oracle \
  --files-report \
  --write-output "docs/oracle-questions-2026-01-09/17-failure-modes-taxonomy.md" \
  -f "internal/errors/errors.go" \
  -f "internal/exec/runner.go" \
  -p "$(cat <<'PROMPT'
Strategist question #17

Reference: internal/errors/errors.go
Category: failure modes
Horizon: Immediate
ROI: 3.4 (impact=High, confidence=Medium, effort=Low)

Question:
What is the failure-mode taxonomy of this system (invalid pack, execution failure, config errors, subprocess failures)? Identify where failures are classified/handled and what is surfaced to users/operators.

Rationale (one sentence):
Explicit failure handling prevents incidents and reduces user-facing errors.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 18) ROI=3.0 impact=High confidence=Medium effort=Medium horizon=Immediate category=failure modes reference=internal/app/run.go
oracle \
  --files-report \
  --write-output "docs/oracle-questions-2026-01-09/18-failure-modes-resilience.md" \
  -f "internal/app/run.go" \
  -f "internal/exec/runner.go" \
  -p "$(cat <<'PROMPT'
Strategist question #18

Reference: internal/app/run.go
Category: failure modes
Horizon: Immediate
ROI: 3.0 (impact=High, confidence=Medium, effort=Medium)

Question:
What resilience mechanisms exist (retry loops, output verification retries, stop-on-fail, resume), and which critical paths lack them? Where are these configured?

Rationale (one sentence):
Resilience patterns determine real-world reliability under stress.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 19) ROI=2.6 impact=Medium confidence=Low effort=Medium horizon=NearTerm category=feature flags reference=internal/overrides/types.go
oracle \
  --files-report \
  --write-output "docs/oracle-questions-2026-01-09/19-feature-flags-inventory.md" \
  -f "internal/overrides/types.go" \
  -f "internal/cli/run.go" \
  -p "$(cat <<'PROMPT'
Strategist question #19

Reference: internal/overrides/types.go
Category: feature flags
Horizon: NearTerm
ROI: 2.6 (impact=Medium, confidence=Low, effort=Medium)

Question:
What feature-flag or override system exists (flag injection, step targeting, ROI filtering)? Inventory the flags/overrides and identify where they are defined, evaluated, and documented.

Rationale (one sentence):
Flags enable safe rollout and experimentation and reduce release risk.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 20) ROI=2.4 impact=Medium confidence=Low effort=High horizon=NearTerm category=feature flags reference=internal/overrides/merge.go
oracle \
  --files-report \
  --write-output "docs/oracle-questions-2026-01-09/20-feature-flags-rollout.md" \
  -f "internal/overrides/merge.go" \
  -f "internal/tui/overrides_flow.go" \
  -p "$(cat <<'PROMPT'
Strategist question #20

Reference: internal/overrides/merge.go
Category: feature flags
Horizon: NearTerm
ROI: 2.4 (impact=Medium, confidence=Low, effort=High)

Question:
Describe the flag/override lifecycle (how overrides are created, validated, applied to steps, and retired). Identify minimum guardrails needed to prevent override drift.

Rationale (one sentence):
A disciplined rollout lifecycle reduces long-term complexity and operational risk.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"
```

Coverage check
--------------

Mark each as OK or Missing(<which step ids>):

*   contracts/interfaces: OK

*   invariants: OK

*   caching/state: OK

*   background jobs: OK

*   observability: OK

*   permissions: OK

*   migrations: OK

*   UX flows: OK

*   failure modes: OK

*   feature flags: OK
```

docs/oracle-questions-2026-01-09-030000/_groups.json
```
{
  "PRD-TUI": [
    ".tickets/PRD-TUI/Oraclepack TUI Integration.md",
    ".tickets/PRD-TUI/PRD-generator URL routing.md"
  ],
  "actions": [
    ".tickets/actions/Enable Action Packs Dispatch.md",
    ".tickets/actions/Improving Oraclepack Workflow.md",
    ".tickets/actions/Oraclepack Action Pack Integration.md",
    ".tickets/actions/Oraclepack Action Pack Issue.md",
    ".tickets/actions/Oraclepack Action Packs.md",
    ".tickets/actions/Oraclepack Compatibility Issues.md"
  ],
  "mcp": [
    ".tickets/mcp/Expose Oraclepack as MCP.md",
    ".tickets/mcp/MCP Server for Oraclepack.md",
    ".tickets/mcp/gaps-still-not-covered.md",
    ".tickets/mcp/gaps_part2-mcp-builder.md",
    ".tickets/mcp/oraclepack-MCP.md",
    ".tickets/mcp/oraclepack_mcp_server.md"
  ],
  "misc": [
    ".tickets/Formalize LLM Decision Points.md",
    ".tickets/Oraclepack CLI MCP Parity.md",
    ".tickets/Oraclepack File Storage.md",
    ".tickets/Oraclepack Parity Automation.md",
    ".tickets/Oraclepack Schema Approach.md",
    ".tickets/Oraclepack bash fix.md",
    ".tickets/Oraclepack output verification issues.md",
    ".tickets/Oraclepack-CLI-agents.md",
    ".tickets/Publish OraclePack MCP.md"
  ],
  "other": [
    ".tickets/other/Oraclepack Pipeline Improvements.md",
    ".tickets/other/Oraclepack Prompt Generator.md",
    ".tickets/other/Oraclepack Workflow Enhancement.md",
    ".tickets/other/Verbose Payload Rendering TUI.md"
  ],
  "raw-exports": [
    ".tickets/raw-exports/Output verification failure.md"
  ]
}
```

docs/oracle-questions-2026-01-09-030000/manifest.json
```
{
  "groups": [
    {
      "attached_paths": [
        ".tickets/PRD-TUI/Oraclepack TUI Integration.md",
        ".tickets/PRD-TUI/PRD-generator URL routing.md"
      ],
      "group": "PRD-TUI",
      "original_tickets": [
        ".tickets/PRD-TUI/Oraclepack TUI Integration.md",
        ".tickets/PRD-TUI/PRD-generator URL routing.md"
      ],
      "out_dir": "docs/oracle-questions-2026-01-09-030000/prd-tui",
      "pack_path": "docs/oracle-questions-2026-01-09-030000/packs/prd-tui.md",
      "part": 1,
      "slug": "prd-tui"
    },
    {
      "attached_paths": [
        ".tickets/actions/Enable Action Packs Dispatch.md",
        ".tickets/actions/Improving Oraclepack Workflow.md",
        ".tickets/actions/Oraclepack Action Pack Integration.md",
        ".tickets/actions/Oraclepack Action Pack Issue.md",
        ".tickets/actions/Oraclepack Action Packs.md",
        ".tickets/actions/Oraclepack Compatibility Issues.md"
      ],
      "group": "actions",
      "original_tickets": [
        ".tickets/actions/Enable Action Packs Dispatch.md",
        ".tickets/actions/Improving Oraclepack Workflow.md",
        ".tickets/actions/Oraclepack Action Pack Integration.md",
        ".tickets/actions/Oraclepack Action Pack Issue.md",
        ".tickets/actions/Oraclepack Action Packs.md",
        ".tickets/actions/Oraclepack Compatibility Issues.md"
      ],
      "out_dir": "docs/oracle-questions-2026-01-09-030000/actions",
      "pack_path": "docs/oracle-questions-2026-01-09-030000/packs/actions.md",
      "part": 1,
      "slug": "actions"
    },
    {
      "attached_paths": [
        ".tickets/mcp/Expose Oraclepack as MCP.md",
        ".tickets/mcp/MCP Server for Oraclepack.md",
        ".tickets/mcp/gaps-still-not-covered.md",
        ".tickets/mcp/gaps_part2-mcp-builder.md",
        ".tickets/mcp/oraclepack-MCP.md",
        ".tickets/mcp/oraclepack_mcp_server.md"
      ],
      "group": "mcp",
      "original_tickets": [
        ".tickets/mcp/Expose Oraclepack as MCP.md",
        ".tickets/mcp/MCP Server for Oraclepack.md",
        ".tickets/mcp/gaps-still-not-covered.md",
        ".tickets/mcp/gaps_part2-mcp-builder.md",
        ".tickets/mcp/oraclepack-MCP.md",
        ".tickets/mcp/oraclepack_mcp_server.md"
      ],
      "out_dir": "docs/oracle-questions-2026-01-09-030000/mcp",
      "pack_path": "docs/oracle-questions-2026-01-09-030000/packs/mcp.md",
      "part": 1,
      "slug": "mcp"
    },
    {
      "attached_paths": [
        ".tickets/Formalize LLM Decision Points.md",
        ".tickets/Oraclepack CLI MCP Parity.md",
        ".tickets/Oraclepack File Storage.md",
        ".tickets/Oraclepack Parity Automation.md",
        ".tickets/Oraclepack Schema Approach.md",
        ".tickets/Oraclepack bash fix.md",
        ".tickets/Oraclepack output verification issues.md",
        ".tickets/Oraclepack-CLI-agents.md",
        ".tickets/Publish OraclePack MCP.md"
      ],
      "group": "misc",
      "original_tickets": [
        ".tickets/Formalize LLM Decision Points.md",
        ".tickets/Oraclepack CLI MCP Parity.md",
        ".tickets/Oraclepack File Storage.md",
        ".tickets/Oraclepack Parity Automation.md",
        ".tickets/Oraclepack Schema Approach.md",
        ".tickets/Oraclepack bash fix.md",
        ".tickets/Oraclepack output verification issues.md",
        ".tickets/Oraclepack-CLI-agents.md",
        ".tickets/Publish OraclePack MCP.md"
      ],
      "out_dir": "docs/oracle-questions-2026-01-09-030000/misc",
      "pack_path": "docs/oracle-questions-2026-01-09-030000/packs/misc.md",
      "part": 1,
      "slug": "misc"
    },
    {
      "attached_paths": [
        ".tickets/other/Oraclepack Pipeline Improvements.md",
        ".tickets/other/Oraclepack Prompt Generator.md",
        ".tickets/other/Oraclepack Workflow Enhancement.md",
        ".tickets/other/Verbose Payload Rendering TUI.md"
      ],
      "group": "other",
      "original_tickets": [
        ".tickets/other/Oraclepack Pipeline Improvements.md",
        ".tickets/other/Oraclepack Prompt Generator.md",
        ".tickets/other/Oraclepack Workflow Enhancement.md",
        ".tickets/other/Verbose Payload Rendering TUI.md"
      ],
      "out_dir": "docs/oracle-questions-2026-01-09-030000/other",
      "pack_path": "docs/oracle-questions-2026-01-09-030000/packs/other.md",
      "part": 1,
      "slug": "other"
    },
    {
      "attached_paths": [
        ".tickets/raw-exports/Output verification failure.md"
      ],
      "group": "raw-exports",
      "original_tickets": [
        ".tickets/raw-exports/Output verification failure.md"
      ],
      "out_dir": "docs/oracle-questions-2026-01-09-030000/raw-exports",
      "pack_path": "docs/oracle-questions-2026-01-09-030000/packs/raw-exports.md",
      "part": 1,
      "slug": "raw-exports"
    }
  ]
}
```

docs/oracle-questions-2026-01-09-030000/actions/01-contracts-interfaces-ticket-surface-direct-answer.md
```
Direct answer

* Clarify/document Action Pack execution semantics (CLI/TUI docs surface): steps run as `bash -lc ...` in the project root, and oraclepack’s “special handling” (flag injection/override validation) applies only to commands beginning with `oracle`; non-`oracle` tools (`tm`/`task-master`, `codex`, `gemini`) run directly as shell commands.  
* Dispatcher/command-detection contract expansion: broaden detection beyond the oracle-anchored regex (`^(\\s*)(oracle)\\b`) so steps containing `tm`/`task-master`, `codex`, and `gemini` can be treated as first-class command targets for dispatch/override/validation inclusion. Back-compat: preserve existing behavior for `oracle ...` commands. 
* Override-validation behavior change (TUI/validate surface): today validation targets only oracle invocations (runs `oracle --dry-run summary`) and skips steps without oracle lines; tickets imply extending/restructuring validation so steps containing `tm`/`task-master`, `codex`, `gemini` are not silently excluded purely due to prefix mismatch. Back-compat: do not regress the current oracle-only validation flow.  
* `ticket-action-pack.md` content contract change: replace placeholder steps (explicitly 09–13 and 16) with headless `gemini` + non-interactive `codex exec` automation. Back-compat: keep the pack “oraclepack-ingestible” (single bash fence, `# NN)` steps) and keep Steps 01–07 semantics unchanged.  
* New standardized output artifact interface under `.oraclepack/ticketify/` for the agent loop: Step 09 `next.json`, Step 10 `codex-implement.md`, Step 11 `codex-verify.md` and/or `gemini-review.json`, Step 16 `PR.md`. Back-compat: paths are treated as required/stable outputs (don’t rename/move without versioning).  
* Tool-availability/skip semantics become part of the operational contract for these steps: add `command -v ...` guards and documented “skip” behavior to avoid hard failures when `codex`/`gemini` are missing or interactive (blocking risk). Back-compat: default runs should not newly fail just because optional tools aren’t installed.  
* Taskify Action Pack generator surface change: add an opt-in “agent-mode” (suggested `mode=codex` / `mode=gemini`) that swaps the existing autopilot entrypoint step slot after Task Master expansion, while keeping the 20-step contract intact. Back-compat: default mode remains current behavior; agent-mode must not add steps. 
* Output/reporting contract stability: tickets explicitly name run outputs/artifacts (e.g., `.oraclepack/ticketify/_tickets_index.json`, `_actions.json`, `.taskmaster/docs/tickets_prd.md`, `.oraclepack/ticketify/tm-complexity.json`, plus `ticket-action-pack.state.json` / `ticket-action-pack.report.json`) and restate project-root execution (no chdir to `out_dir`). Back-compat: keep these names/locations stable to avoid breaking consumers and user expectations. 
```

docs/oracle-questions-2026-01-09-030000/actions/01-contracts-interfaces-ticket-surface-missing-evidence.md
```
Missing evidence

* `.tickets/actions/*.md` (full raw tickets, not summaries)
* `ticket-action-pack.md` (and its generator/template source): `**/*ticket-action-pack*.md`, `**/*ticketify*pack*.md`, `**/templates/**`, `**/assets/**`
* “taskify” Action Pack generator implementation (for proposed `mode=codex|gemini`): `**/*taskify*.*`, `**/taskify/**`, `**/oraclepack-taskify/**`
* Oraclepack CLI/TUI public surface (commands/flags/help) and wiring: `**/cmd/**`, `**/internal/cli/**`, `**/*root*.go`, `**/*commands*.go`
* Oraclepack dispatcher / command-detection / override-injection / validation pipeline (regex currently `^(\\s*)(oracle)\\b` per tickets): `**/*dispatch*.*`, `**/*detect*.*`, `**/*override*.*`, `**/*validate*.*`, `**/internal/**`
* Oraclepack TUI override-validation flow (to assess user-visible behavior/back-compat): `**/*tui*.*`, `**/internal/**tui**/**`
* Docs explicitly referenced by the tickets: `**/oraclepack-tui.md`, `**/oracle_pack_and_taskify-skills.md`
```

docs/oracle-questions-2026-01-09-030000/actions/01-contracts-interfaces-ticket-surface-next-experiment.md
```
Next smallest concrete experiment

* Run `rg -n --hidden --no-ignore-vcs 'ticket-action-pack\.md|Action Pack|override validation|dry-run summary|\^\(\\\\s\*\)\(oracle\)\\\\b|\btm\b|\btask-master\b|\bcodex\b|\bgemini\b' .`
```

docs/oracle-questions-2026-01-09-030000/actions/01-contracts-interfaces-ticket-surface-risks-unknowns.md
```
Risks/unknowns

* Override injection/validation expansion beyond `oracle` is underspecified: which overrides/flags apply to `tm`/`task-master`, `codex`, and `gemini`, and what “validation” means for each tool are not defined; this can easily produce partial/incorrect behavior.
* Dispatcher intent is ambiguous: tickets do not specify whether the dispatcher should “interpret actions” (semantic dispatch) vs only broaden prefix-based detection to include non-`oracle` commands.
* User-facing docs/TUI discoverability is unclear: where the “Action Pack execution semantics + failure modes” documentation should live (README vs `oraclepack-tui.md` vs TUI help text) is not specified, risking continued user confusion about what is/ isn’t routed/validated.
* Action Pack “implement” execution path has key unknowns: where `top_n` is defined/how ranking works, and the exact `<out_dir>/_actions.json` location/structure needed for deterministic dispatch are not provided. 
* Taskify “agent-mode” surface is incomplete: the tickets don’t specify how `mode=codex|gemini` is selected (CLI flag vs TUI option vs config) or which exact step slot replaces the autopilot entrypoint while keeping the 20-step contract intact. 
* `ticket-action-pack.md` placeholder replacement is underspecified in places: whether Step 11 defaults to Codex verification, Gemini diff review, or both, and whether Steps 12–13 must change are not defined, increasing the risk of a half-automated pack that still looks “successful” but doesn’t produce expected artifacts. 
* Operational “skip” contract for missing tools is not defined: tickets require `command -v` guards and “skip” behavior when `codex`/`gemini` are absent, but don’t specify exit codes/status/report semantics, creating inconsistency across CLI/TUI reporting and resume behavior.
* Runner/TUI behavior gaps remain undefined for reliability: signal handling (SIGINT propagation, atomic state/log flush), the full resume contract (what reruns, how interrupted prelude is treated), and how to avoid UX implying non-existent guarantees for non-`oracle` override/validation are not specified. 
```

docs/oracle-questions-2026-01-09-030000/packs/actions.md
```
# Oracle Pack — Unknown (Grouped Tickets Stage 1 — Direct Attach)

## Parsed args
- codebase_name: Unknown
- out_dir: docs/oracle-questions-2026-01-09-030000/actions
- oracle_cmd: oracle
- oracle_flags: --files-report
- extra_files: 
- ticket_root: .tickets
- ticket_glob: **/*.md
- ticket_paths: .tickets/actions/Enable Action Packs Dispatch.md,.tickets/actions/Improving Oraclepack Workflow.md,.tickets/actions/Oraclepack Action Pack Integration.md,.tickets/actions/Oraclepack Action Pack Issue.md,.tickets/actions/Oraclepack Action Packs.md,.tickets/actions/Oraclepack Compatibility Issues.md
- ticket_max_files: 6
- group_name: actions
- group_slug: actions
- mode: tickets-grouped-direct

Notes (contract):
- Exactly one fenced `bash` block in this document.
- No other ``` fences anywhere.
- Exactly 20 steps, numbered 01..20 in order.
- Step header: `# NN) ROI=... impact=... confidence=... effort=... horizon=... category=... reference=...`
- Every step includes: `--write-output "docs/oracle-questions-2026-01-09-030000/actions/NN-<slug>.md"` (double quotes required).
- Steps must be self-contained and must not rely on shell variables created in previous steps.
- Each step must attach tickets directly (no `_tickets_bundle.md` dependency).
- Pack ends with a Coverage check section listing all 10 categories as OK or Missing(<step ids>).

```bash
set -euo pipefail

mkdir -p "docs/oracle-questions-2026-01-09-030000/actions"

# 01) ROI=4.8 impact=6 confidence=0.80 effort=1 horizon=Immediate category=contracts/interfaces reference=actions

# tickets attached directly (deterministic; self-contained)
mapfile -t __tickets < <(python3 - <<'PY'
from __future__ import annotations
from pathlib import Path
import fnmatch

TICKET_ROOT = ".tickets"
TICKET_GLOB = "**/*.md"
TICKET_PATHS = ".tickets/actions/Enable Action Packs Dispatch.md,.tickets/actions/Improving Oraclepack Workflow.md,.tickets/actions/Oraclepack Action Pack Integration.md,.tickets/actions/Oraclepack Action Pack Issue.md,.tickets/actions/Oraclepack Action Packs.md,.tickets/actions/Oraclepack Compatibility Issues.md".strip()
MAX = int("6")


root = Path(TICKET_ROOT)

def read_gitignore(start: Path):
    cur = start.resolve()
    if cur.is_file():
        cur = cur.parent
    while True:
        p = cur / ".gitignore"
        if p.exists():
            lines = []
            for ln in p.read_text(encoding="utf-8", errors="replace").splitlines():
                s = ln.strip()
                if not s or s.startswith("#"):
                    continue
                lines.append(s)
            return cur, lines
        if cur.parent == cur:
            return start.resolve(), []
        cur = cur.parent


def gitignore_match(rel_posix: str, name: str, pattern: str) -> bool:
    neg = pattern.startswith("!")
    if neg:
        pattern = pattern[1:]
    if not pattern:
        return False

    anchored = pattern.startswith("/")
    if anchored:
        pattern = pattern.lstrip("/")

    dir_only = pattern.endswith("/")
    if dir_only:
        pattern = pattern.rstrip("/")

    if "/" not in pattern:
        if dir_only:
            return any(part == pattern for part in rel_posix.split("/"))
        return fnmatch.fnmatch(name, pattern)

    target = rel_posix
    if anchored:
        if dir_only:
            return target.startswith(pattern + "/")
        return fnmatch.fnmatch(target, pattern)
    if dir_only:
        return f"/{pattern}/" in f"/{target}/"
    return fnmatch.fnmatch(target, f"**/{pattern}") or fnmatch.fnmatch(target, pattern)


def is_gitignored(path: Path, git_root: Path, patterns: list[str]) -> bool:
    try:
        rel = path.resolve().relative_to(git_root)
    except Exception:
        rel = path
    rel_posix = rel.as_posix()
    name = rel_posix.split("/")[-1]
    ignored = False
    for pat in patterns:
        neg = pat.startswith("!")
        if gitignore_match(rel_posix, name, pat):
            ignored = not neg
    return ignored


git_root, git_patterns = read_gitignore(root)
def lex_sorted(ps):
    return sorted((str(p) for p in ps), key=lambda s: s)

if TICKET_PATHS:
    tickets = [Path(p.strip()) for p in TICKET_PATHS.split(",") if p.strip()]
else:
    tickets = list(root.glob(TICKET_GLOB)) if root.exists() else []

tickets = [Path(p) for p in lex_sorted(tickets)]
tickets = [p for p in tickets if not is_gitignored(p, git_root, git_patterns)]
if MAX and MAX > 0:
    tickets = tickets[:MAX]

for p in tickets:
    print(str(p))
PY
)

ticket_args=()
for p in "${__tickets[@]}"; do
  ticket_args+=(-f "$p")
done

if [ "${#ticket_args[@]}" -eq 0 ]; then
  echo "WARNING: no tickets resolved for group 'actions'." >&2
fi

# extra_files appended literally (may be empty; may include -f/--file):
oracle   --files-report   --write-output "docs/oracle-questions-2026-01-09-030000/actions/01-contracts-interfaces-ticket-surface-direct-answer.md"   "${ticket_args[@]}"      -p "$(cat <<'PROMPT'
Strategist question #01  (ticket-driven, group: actions)

Reference: actions
Category: contracts/interfaces
Horizon: Immediate
ROI: 4.8 (impact=6, confidence=0.80, effort=1)

Question:
Using the attached tickets as the primary context, list the public surface changes implied by the tickets (CLI/TUI/API/interfaces/contracts); call out backwards-compat constraints.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.

Output section: Direct answer
Start with heading: Direct answer
Return only: Direct answer (1–10 bullets, evidence-cited)
PROMPT
)"

oracle   --files-report   --write-output "docs/oracle-questions-2026-01-09-030000/actions/01-contracts-interfaces-ticket-surface-risks-unknowns.md"   "${ticket_args[@]}"      -p "$(cat <<'PROMPT'
Strategist question #01  (ticket-driven, group: actions)

Reference: actions
Category: contracts/interfaces
Horizon: Immediate
ROI: 4.8 (impact=6, confidence=0.80, effort=1)

Question:
Using the attached tickets as the primary context, list the public surface changes implied by the tickets (CLI/TUI/API/interfaces/contracts); call out backwards-compat constraints.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.

Output section: Risks/unknowns
Start with heading: Risks/unknowns
Return only: Risks/unknowns (bullets)
PROMPT
)"

oracle   --files-report   --write-output "docs/oracle-questions-2026-01-09-030000/actions/01-contracts-interfaces-ticket-surface-next-experiment.md"   "${ticket_args[@]}"      -p "$(cat <<'PROMPT'
Strategist question #01  (ticket-driven, group: actions)

Reference: actions
Category: contracts/interfaces
Horizon: Immediate
ROI: 4.8 (impact=6, confidence=0.80, effort=1)

Question:
Using the attached tickets as the primary context, list the public surface changes implied by the tickets (CLI/TUI/API/interfaces/contracts); call out backwards-compat constraints.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.

Output section: Next smallest concrete experiment
Start with heading: Next smallest concrete experiment
Return only: Next smallest concrete experiment (exactly one action)
PROMPT
)"

oracle   --files-report   --write-output "docs/oracle-questions-2026-01-09-030000/actions/01-contracts-interfaces-ticket-surface-missing-evidence.md"   "${ticket_args[@]}"      -p "$(cat <<'PROMPT'
Strategist question #01  (ticket-driven, group: actions)

Reference: actions
Category: contracts/interfaces
Horizon: Immediate
ROI: 4.8 (impact=6, confidence=0.80, effort=1)

Question:
Using the attached tickets as the primary context, list the public surface changes implied by the tickets (CLI/TUI/API/interfaces/contracts); call out backwards-compat constraints.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.

Output section: Missing evidence
Start with heading: Missing evidence
Return only: If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"


# 02) ROI=4.6 impact=6 confidence=0.78 effort=1 horizon=Immediate category=contracts/interfaces reference=actions

# tickets attached directly (deterministic; self-contained)
mapfile -t __tickets < <(python3 - <<'PY'
from __future__ import annotations
from pathlib import Path
import fnmatch

TICKET_ROOT = ".tickets"
TICKET_GLOB = "**/*.md"
TICKET_PATHS = ".tickets/actions/Enable Action Packs Dispatch.md,.tickets/actions/Improving Oraclepack Workflow.md,.tickets/actions/Oraclepack Action Pack Integration.md,.tickets/actions/Oraclepack Action Pack Issue.md,.tickets/actions/Oraclepack Action Packs.md,.tickets/actions/Oraclepack Compatibility Issues.md".strip()
MAX = int("6")


root = Path(TICKET_ROOT)

def read_gitignore(start: Path):
    cur = start.resolve()
    if cur.is_file():
        cur = cur.parent
    while True:
        p = cur / ".gitignore"
        if p.exists():
            lines = []
            for ln in p.read_text(encoding="utf-8", errors="replace").splitlines():
                s = ln.strip()
                if not s or s.startswith("#"):
                    continue
                lines.append(s)
            return cur, lines
        if cur.parent == cur:
            return start.resolve(), []
        cur = cur.parent


def gitignore_match(rel_posix: str, name: str, pattern: str) -> bool:
    neg = pattern.startswith("!")
    if neg:
        pattern = pattern[1:]
    if not pattern:
        return False

    anchored = pattern.startswith("/")
    if anchored:
        pattern = pattern.lstrip("/")

    dir_only = pattern.endswith("/")
    if dir_only:
        pattern = pattern.rstrip("/")

    if "/" not in pattern:
        if dir_only:
            return any(part == pattern for part in rel_posix.split("/"))
        return fnmatch.fnmatch(name, pattern)

    target = rel_posix
    if anchored:
        if dir_only:
            return target.startswith(pattern + "/")
        return fnmatch.fnmatch(target, pattern)
    if dir_only:
        return f"/{pattern}/" in f"/{target}/"
    return fnmatch.fnmatch(target, f"**/{pattern}") or fnmatch.fnmatch(target, pattern)


def is_gitignored(path: Path, git_root: Path, patterns: list[str]) -> bool:
    try:
        rel = path.resolve().relative_to(git_root)
    except Exception:
        rel = path
    rel_posix = rel.as_posix()
    name = rel_posix.split("/")[-1]
    ignored = False
    for pat in patterns:
        neg = pat.startswith("!")
        if gitignore_match(rel_posix, name, pat):
            ignored = not neg
    return ignored


git_root, git_patterns = read_gitignore(root)
def lex_sorted(ps):
    return sorted((str(p) for p in ps), key=lambda s: s)

if TICKET_PATHS:
    tickets = [Path(p.strip()) for p in TICKET_PATHS.split(",") if p.strip()]
else:
    tickets = list(root.glob(TICKET_GLOB)) if root.exists() else []

tickets = [Path(p) for p in lex_sorted(tickets)]
tickets = [p for p in tickets if not is_gitignored(p, git_root, git_patterns)]
if MAX and MAX > 0:
    tickets = tickets[:MAX]

for p in tickets:
    print(str(p))
PY
)

ticket_args=()
for p in "${__tickets[@]}"; do
  ticket_args+=(-f "$p")
done

if [ "${#ticket_args[@]}" -eq 0 ]; then
  echo "WARNING: no tickets resolved for group 'actions'." >&2
fi

# extra_files appended literally (may be empty; may include -f/--file):
oracle   --files-report   --write-output "docs/oracle-questions-2026-01-09-030000/actions/02-contracts-interfaces-integration-points-direct-answer.md"   "${ticket_args[@]}"      -p "$(cat <<'PROMPT'
Strategist question #02  (ticket-driven, group: actions)

Reference: actions
Category: contracts/interfaces
Horizon: Immediate
ROI: 4.6 (impact=6, confidence=0.78, effort=1)

Question:
Using the attached tickets as the primary context, identify external integrations implied by the tickets; required config/contract changes; failure/timeout behavior; minimal compat-safe rollout.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.

Output section: Direct answer
Start with heading: Direct answer
Return only: Direct answer (1–10 bullets, evidence-cited)
PROMPT
)"

oracle   --files-report   --write-output "docs/oracle-questions-2026-01-09-030000/actions/02-contracts-interfaces-integration-points-risks-unknowns.md"   "${ticket_args[@]}"      -p "$(cat <<'PROMPT'
Strategist question #02  (ticket-driven, group: actions)

Reference: actions
Category: contracts/interfaces
Horizon: Immediate
ROI: 4.6 (impact=6, confidence=0.78, effort=1)

Question:
Using the attached tickets as the primary context, identify external integrations implied by the tickets; required config/contract changes; failure/timeout behavior; minimal compat-safe rollout.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.

Output section: Risks/unknowns
Start with heading: Risks/unknowns
Return only: Risks/unknowns (bullets)
PROMPT
)"

oracle   --files-report   --write-output "docs/oracle-questions-2026-01-09-030000/actions/02-contracts-interfaces-integration-points-next-experiment.md"   "${ticket_args[@]}"      -p "$(cat <<'PROMPT'
Strategist question #02  (ticket-driven, group: actions)

Reference: actions
Category: contracts/interfaces
Horizon: Immediate
ROI: 4.6 (impact=6, confidence=0.78, effort=1)

Question:
Using the attached tickets as the primary context, identify external integrations implied by the tickets; required config/contract changes; failure/timeout behavior; minimal compat-safe rollout.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.

Output section: Next smallest concrete experiment
Start with heading: Next smallest concrete experiment
Return only: Next smallest concrete experiment (exactly one action)
PROMPT
)"

oracle   --files-report   --write-output "docs/oracle-questions-2026-01-09-030000/actions/02-contracts-interfaces-integration-points-missing-evidence.md"   "${ticket_args[@]}"      -p "$(cat <<'PROMPT'
Strategist question #02  (ticket-driven, group: actions)

Reference: actions
Category: contracts/interfaces
Horizon: Immediate
ROI: 4.6 (impact=6, confidence=0.78, effort=1)

Question:
Using the attached tickets as the primary context, identify external integrations implied by the tickets; required config/contract changes; failure/timeout behavior; minimal compat-safe rollout.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.

Output section: Missing evidence
Start with heading: Missing evidence
Return only: If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"


# 03) ROI=5.1 impact=7 confidence=0.74 effort=1 horizon=NearTerm category=invariants reference=actions

# tickets attached directly (deterministic; self-contained)
mapfile -t __tickets < <(python3 - <<'PY'
from __future__ import annotations
from pathlib import Path
import fnmatch

TICKET_ROOT = ".tickets"
TICKET_GLOB = "**/*.md"
TICKET_PATHS = ".tickets/actions/Enable Action Packs Dispatch.md,.tickets/actions/Improving Oraclepack Workflow.md,.tickets/actions/Oraclepack Action Pack Integration.md,.tickets/actions/Oraclepack Action Pack Issue.md,.tickets/actions/Oraclepack Action Packs.md,.tickets/actions/Oraclepack Compatibility Issues.md".strip()
MAX = int("6")


root = Path(TICKET_ROOT)

def read_gitignore(start: Path):
    cur = start.resolve()
    if cur.is_file():
        cur = cur.parent
    while True:
        p = cur / ".gitignore"
        if p.exists():
            lines = []
            for ln in p.read_text(encoding="utf-8", errors="replace").splitlines():
                s = ln.strip()
                if not s or s.startswith("#"):
                    continue
                lines.append(s)
            return cur, lines
        if cur.parent == cur:
            return start.resolve(), []
        cur = cur.parent


def gitignore_match(rel_posix: str, name: str, pattern: str) -> bool:
    neg = pattern.startswith("!")
    if neg:
        pattern = pattern[1:]
    if not pattern:
        return False

    anchored = pattern.startswith("/")
    if anchored:
        pattern = pattern.lstrip("/")

    dir_only = pattern.endswith("/")
    if dir_only:
        pattern = pattern.rstrip("/")

    if "/" not in pattern:
        if dir_only:
            return any(part == pattern for part in rel_posix.split("/"))
        return fnmatch.fnmatch(name, pattern)

    target = rel_posix
    if anchored:
        if dir_only:
            return target.startswith(pattern + "/")
        return fnmatch.fnmatch(target, pattern)
    if dir_only:
        return f"/{pattern}/" in f"/{target}/"
    return fnmatch.fnmatch(target, f"**/{pattern}") or fnmatch.fnmatch(target, pattern)


def is_gitignored(path: Path, git_root: Path, patterns: list[str]) -> bool:
    try:
        rel = path.resolve().relative_to(git_root)
    except Exception:
        rel = path
    rel_posix = rel.as_posix()
    name = rel_posix.split("/")[-1]
    ignored = False
    for pat in patterns:
        neg = pat.startswith("!")
        if gitignore_match(rel_posix, name, pat):
            ignored = not neg
    return ignored


git_root, git_patterns = read_gitignore(root)
def lex_sorted(ps):
    return sorted((str(p) for p in ps), key=lambda s: s)

if TICKET_PATHS:
    tickets = [Path(p.strip()) for p in TICKET_PATHS.split(",") if p.strip()]
else:
    tickets = list(root.glob(TICKET_GLOB)) if root.exists() else []

tickets = [Path(p) for p in lex_sorted(tickets)]
tickets = [p for p in tickets if not is_gitignored(p, git_root, git_patterns)]
if MAX and MAX > 0:
    tickets = tickets[:MAX]

for p in tickets:
    print(str(p))
PY
)

ticket_args=()
for p in "${__tickets[@]}"; do
  ticket_args+=(-f "$p")
done

if [ "${#ticket_args[@]}" -eq 0 ]; then
  echo "WARNING: no tickets resolved for group 'actions'." >&2
fi

# extra_files appended literally (may be empty; may include -f/--file):
oracle   --files-report   --write-output "docs/oracle-questions-2026-01-09-030000/actions/03-invariants-invariant-map-direct-answer.md"   "${ticket_args[@]}"      -p "$(cat <<'PROMPT'
Strategist question #03  (ticket-driven, group: actions)

Reference: actions
Category: invariants
Horizon: NearTerm
ROI: 5.1 (impact=7, confidence=0.74, effort=1)

Question:
Using the attached tickets as the primary context, extract system invariants implied by tickets (inputs/outputs, pack schema rules, step execution rules) and where to enforce them.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.

Output section: Direct answer
Start with heading: Direct answer
Return only: Direct answer (1–10 bullets, evidence-cited)
PROMPT
)"

oracle   --files-report   --write-output "docs/oracle-questions-2026-01-09-030000/actions/03-invariants-invariant-map-risks-unknowns.md"   "${ticket_args[@]}"      -p "$(cat <<'PROMPT'
Strategist question #03  (ticket-driven, group: actions)

Reference: actions
Category: invariants
Horizon: NearTerm
ROI: 5.1 (impact=7, confidence=0.74, effort=1)

Question:
Using the attached tickets as the primary context, extract system invariants implied by tickets (inputs/outputs, pack schema rules, step execution rules) and where to enforce them.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.

Output section: Risks/unknowns
Start with heading: Risks/unknowns
Return only: Risks/unknowns (bullets)
PROMPT
)"

oracle   --files-report   --write-output "docs/oracle-questions-2026-01-09-030000/actions/03-invariants-invariant-map-next-experiment.md"   "${ticket_args[@]}"      -p "$(cat <<'PROMPT'
Strategist question #03  (ticket-driven, group: actions)

Reference: actions
Category: invariants
Horizon: NearTerm
ROI: 5.1 (impact=7, confidence=0.74, effort=1)

Question:
Using the attached tickets as the primary context, extract system invariants implied by tickets (inputs/outputs, pack schema rules, step execution rules) and where to enforce them.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.

Output section: Next smallest concrete experiment
Start with heading: Next smallest concrete experiment
Return only: Next smallest concrete experiment (exactly one action)
PROMPT
)"

oracle   --files-report   --write-output "docs/oracle-questions-2026-01-09-030000/actions/03-invariants-invariant-map-missing-evidence.md"   "${ticket_args[@]}"      -p "$(cat <<'PROMPT'
Strategist question #03  (ticket-driven, group: actions)

Reference: actions
Category: invariants
Horizon: NearTerm
ROI: 5.1 (impact=7, confidence=0.74, effort=1)

Question:
Using the attached tickets as the primary context, extract system invariants implied by tickets (inputs/outputs, pack schema rules, step execution rules) and where to enforce them.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.

Output section: Missing evidence
Start with heading: Missing evidence
Return only: If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"


# 04) ROI=5.0 impact=7 confidence=0.72 effort=2 horizon=NearTerm category=invariants reference=actions

# tickets attached directly (deterministic; self-contained)
mapfile -t __tickets < <(python3 - <<'PY'
from __future__ import annotations
from pathlib import Path
import fnmatch

TICKET_ROOT = ".tickets"
TICKET_GLOB = "**/*.md"
TICKET_PATHS = ".tickets/actions/Enable Action Packs Dispatch.md,.tickets/actions/Improving Oraclepack Workflow.md,.tickets/actions/Oraclepack Action Pack Integration.md,.tickets/actions/Oraclepack Action Pack Issue.md,.tickets/actions/Oraclepack Action Packs.md,.tickets/actions/Oraclepack Compatibility Issues.md".strip()
MAX = int("6")


root = Path(TICKET_ROOT)

def read_gitignore(start: Path):
    cur = start.resolve()
    if cur.is_file():
        cur = cur.parent
    while True:
        p = cur / ".gitignore"
        if p.exists():
            lines = []
            for ln in p.read_text(encoding="utf-8", errors="replace").splitlines():
                s = ln.strip()
                if not s or s.startswith("#"):
                    continue
                lines.append(s)
            return cur, lines
        if cur.parent == cur:
            return start.resolve(), []
        cur = cur.parent


def gitignore_match(rel_posix: str, name: str, pattern: str) -> bool:
    neg = pattern.startswith("!")
    if neg:
        pattern = pattern[1:]
    if not pattern:
        return False

    anchored = pattern.startswith("/")
    if anchored:
        pattern = pattern.lstrip("/")

    dir_only = pattern.endswith("/")
    if dir_only:
        pattern = pattern.rstrip("/")

    if "/" not in pattern:
        if dir_only:
            return any(part == pattern for part in rel_posix.split("/"))
        return fnmatch.fnmatch(name, pattern)

    target = rel_posix
    if anchored:
        if dir_only:
            return target.startswith(pattern + "/")
        return fnmatch.fnmatch(target, pattern)
    if dir_only:
        return f"/{pattern}/" in f"/{target}/"
    return fnmatch.fnmatch(target, f"**/{pattern}") or fnmatch.fnmatch(target, pattern)


def is_gitignored(path: Path, git_root: Path, patterns: list[str]) -> bool:
    try:
        rel = path.resolve().relative_to(git_root)
    except Exception:
        rel = path
    rel_posix = rel.as_posix()
    name = rel_posix.split("/")[-1]
    ignored = False
    for pat in patterns:
        neg = pat.startswith("!")
        if gitignore_match(rel_posix, name, pat):
            ignored = not neg
    return ignored


git_root, git_patterns = read_gitignore(root)
def lex_sorted(ps):
    return sorted((str(p) for p in ps), key=lambda s: s)

if TICKET_PATHS:
    tickets = [Path(p.strip()) for p in TICKET_PATHS.split(",") if p.strip()]
else:
    tickets = list(root.glob(TICKET_GLOB)) if root.exists() else []

tickets = [Path(p) for p in lex_sorted(tickets)]
tickets = [p for p in tickets if not is_gitignored(p, git_root, git_patterns)]
if MAX and MAX > 0:
    tickets = tickets[:MAX]

for p in tickets:
    print(str(p))
PY
)

ticket_args=()
for p in "${__tickets[@]}"; do
  ticket_args+=(-f "$p")
done

if [ "${#ticket_args[@]}" -eq 0 ]; then
  echo "WARNING: no tickets resolved for group 'actions'." >&2
fi

# extra_files appended literally (may be empty; may include -f/--file):
[TRUNCATED]
```

docs/oracle-questions-2026-01-09-030000/packs/mcp.md
```
# Oracle Pack — Unknown (Grouped Tickets Stage 1 — Direct Attach)

## Parsed args
- codebase_name: Unknown
- out_dir: docs/oracle-questions-2026-01-09-030000/mcp
- oracle_cmd: oracle
- oracle_flags: --files-report
- extra_files: 
- ticket_root: .tickets
- ticket_glob: **/*.md
- ticket_paths: .tickets/mcp/Expose Oraclepack as MCP.md,.tickets/mcp/MCP Server for Oraclepack.md,.tickets/mcp/gaps-still-not-covered.md,.tickets/mcp/gaps_part2-mcp-builder.md,.tickets/mcp/oraclepack-MCP.md,.tickets/mcp/oraclepack_mcp_server.md
- ticket_max_files: 6
- group_name: mcp
- group_slug: mcp
- mode: tickets-grouped-direct

Notes (contract):
- Exactly one fenced `bash` block in this document.
- No other ``` fences anywhere.
- Exactly 20 steps, numbered 01..20 in order.
- Step header: `# NN) ROI=... impact=... confidence=... effort=... horizon=... category=... reference=...`
- Every step includes: `--write-output "docs/oracle-questions-2026-01-09-030000/mcp/NN-<slug>.md"` (double quotes required).
- Steps must be self-contained and must not rely on shell variables created in previous steps.
- Each step must attach tickets directly (no `_tickets_bundle.md` dependency).
- Pack ends with a Coverage check section listing all 10 categories as OK or Missing(<step ids>).

```bash
set -euo pipefail

mkdir -p "docs/oracle-questions-2026-01-09-030000/mcp"

# 01) ROI=4.8 impact=6 confidence=0.80 effort=1 horizon=Immediate category=contracts/interfaces reference=mcp

# tickets attached directly (deterministic; self-contained)
mapfile -t __tickets < <(python3 - <<'PY'
from __future__ import annotations
from pathlib import Path
import fnmatch

TICKET_ROOT = ".tickets"
TICKET_GLOB = "**/*.md"
TICKET_PATHS = ".tickets/mcp/Expose Oraclepack as MCP.md,.tickets/mcp/MCP Server for Oraclepack.md,.tickets/mcp/gaps-still-not-covered.md,.tickets/mcp/gaps_part2-mcp-builder.md,.tickets/mcp/oraclepack-MCP.md,.tickets/mcp/oraclepack_mcp_server.md".strip()
MAX = int("6")


root = Path(TICKET_ROOT)

def read_gitignore(start: Path):
    cur = start.resolve()
    if cur.is_file():
        cur = cur.parent
    while True:
        p = cur / ".gitignore"
        if p.exists():
            lines = []
            for ln in p.read_text(encoding="utf-8", errors="replace").splitlines():
                s = ln.strip()
                if not s or s.startswith("#"):
                    continue
                lines.append(s)
            return cur, lines
        if cur.parent == cur:
            return start.resolve(), []
        cur = cur.parent


def gitignore_match(rel_posix: str, name: str, pattern: str) -> bool:
    neg = pattern.startswith("!")
    if neg:
        pattern = pattern[1:]
    if not pattern:
        return False

    anchored = pattern.startswith("/")
    if anchored:
        pattern = pattern.lstrip("/")

    dir_only = pattern.endswith("/")
    if dir_only:
        pattern = pattern.rstrip("/")

    if "/" not in pattern:
        if dir_only:
            return any(part == pattern for part in rel_posix.split("/"))
        return fnmatch.fnmatch(name, pattern)

    target = rel_posix
    if anchored:
        if dir_only:
            return target.startswith(pattern + "/")
        return fnmatch.fnmatch(target, pattern)
    if dir_only:
        return f"/{pattern}/" in f"/{target}/"
    return fnmatch.fnmatch(target, f"**/{pattern}") or fnmatch.fnmatch(target, pattern)


def is_gitignored(path: Path, git_root: Path, patterns: list[str]) -> bool:
    try:
        rel = path.resolve().relative_to(git_root)
    except Exception:
        rel = path
    rel_posix = rel.as_posix()
    name = rel_posix.split("/")[-1]
    ignored = False
    for pat in patterns:
        neg = pat.startswith("!")
        if gitignore_match(rel_posix, name, pat):
            ignored = not neg
    return ignored


git_root, git_patterns = read_gitignore(root)
def lex_sorted(ps):
    return sorted((str(p) for p in ps), key=lambda s: s)

if TICKET_PATHS:
    tickets = [Path(p.strip()) for p in TICKET_PATHS.split(",") if p.strip()]
else:
    tickets = list(root.glob(TICKET_GLOB)) if root.exists() else []

tickets = [Path(p) for p in lex_sorted(tickets)]
tickets = [p for p in tickets if not is_gitignored(p, git_root, git_patterns)]
if MAX and MAX > 0:
    tickets = tickets[:MAX]

for p in tickets:
    print(str(p))
PY
)

ticket_args=()
for p in "${__tickets[@]}"; do
  ticket_args+=(-f "$p")
done

if [ "${#ticket_args[@]}" -eq 0 ]; then
  echo "WARNING: no tickets resolved for group 'mcp'." >&2
fi

# extra_files appended literally (may be empty; may include -f/--file):
oracle   --files-report   --write-output "docs/oracle-questions-2026-01-09-030000/mcp/01-contracts-interfaces-ticket-surface-direct-answer.md"   "${ticket_args[@]}"      -p "$(cat <<'PROMPT'
Strategist question #01  (ticket-driven, group: mcp)

Reference: mcp
Category: contracts/interfaces
Horizon: Immediate
ROI: 4.8 (impact=6, confidence=0.80, effort=1)

Question:
Using the attached tickets as the primary context, list the public surface changes implied by the tickets (CLI/TUI/API/interfaces/contracts); call out backwards-compat constraints.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.

Output section: Direct answer
Start with heading: Direct answer
Return only: Direct answer (1–10 bullets, evidence-cited)
PROMPT
)"

oracle   --files-report   --write-output "docs/oracle-questions-2026-01-09-030000/mcp/01-contracts-interfaces-ticket-surface-risks-unknowns.md"   "${ticket_args[@]}"      -p "$(cat <<'PROMPT'
Strategist question #01  (ticket-driven, group: mcp)

Reference: mcp
Category: contracts/interfaces
Horizon: Immediate
ROI: 4.8 (impact=6, confidence=0.80, effort=1)

Question:
Using the attached tickets as the primary context, list the public surface changes implied by the tickets (CLI/TUI/API/interfaces/contracts); call out backwards-compat constraints.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.

Output section: Risks/unknowns
Start with heading: Risks/unknowns
Return only: Risks/unknowns (bullets)
PROMPT
)"

oracle   --files-report   --write-output "docs/oracle-questions-2026-01-09-030000/mcp/01-contracts-interfaces-ticket-surface-next-experiment.md"   "${ticket_args[@]}"      -p "$(cat <<'PROMPT'
Strategist question #01  (ticket-driven, group: mcp)

Reference: mcp
Category: contracts/interfaces
Horizon: Immediate
ROI: 4.8 (impact=6, confidence=0.80, effort=1)

Question:
Using the attached tickets as the primary context, list the public surface changes implied by the tickets (CLI/TUI/API/interfaces/contracts); call out backwards-compat constraints.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.

Output section: Next smallest concrete experiment
Start with heading: Next smallest concrete experiment
Return only: Next smallest concrete experiment (exactly one action)
PROMPT
)"

oracle   --files-report   --write-output "docs/oracle-questions-2026-01-09-030000/mcp/01-contracts-interfaces-ticket-surface-missing-evidence.md"   "${ticket_args[@]}"      -p "$(cat <<'PROMPT'
Strategist question #01  (ticket-driven, group: mcp)

Reference: mcp
Category: contracts/interfaces
Horizon: Immediate
ROI: 4.8 (impact=6, confidence=0.80, effort=1)

Question:
Using the attached tickets as the primary context, list the public surface changes implied by the tickets (CLI/TUI/API/interfaces/contracts); call out backwards-compat constraints.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.

Output section: Missing evidence
Start with heading: Missing evidence
Return only: If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"


# 02) ROI=4.6 impact=6 confidence=0.78 effort=1 horizon=Immediate category=contracts/interfaces reference=mcp

# tickets attached directly (deterministic; self-contained)
mapfile -t __tickets < <(python3 - <<'PY'
from __future__ import annotations
from pathlib import Path
import fnmatch

TICKET_ROOT = ".tickets"
TICKET_GLOB = "**/*.md"
TICKET_PATHS = ".tickets/mcp/Expose Oraclepack as MCP.md,.tickets/mcp/MCP Server for Oraclepack.md,.tickets/mcp/gaps-still-not-covered.md,.tickets/mcp/gaps_part2-mcp-builder.md,.tickets/mcp/oraclepack-MCP.md,.tickets/mcp/oraclepack_mcp_server.md".strip()
MAX = int("6")


root = Path(TICKET_ROOT)

def read_gitignore(start: Path):
    cur = start.resolve()
    if cur.is_file():
        cur = cur.parent
    while True:
        p = cur / ".gitignore"
        if p.exists():
            lines = []
            for ln in p.read_text(encoding="utf-8", errors="replace").splitlines():
                s = ln.strip()
                if not s or s.startswith("#"):
                    continue
                lines.append(s)
            return cur, lines
        if cur.parent == cur:
            return start.resolve(), []
        cur = cur.parent


def gitignore_match(rel_posix: str, name: str, pattern: str) -> bool:
    neg = pattern.startswith("!")
    if neg:
        pattern = pattern[1:]
    if not pattern:
        return False

    anchored = pattern.startswith("/")
    if anchored:
        pattern = pattern.lstrip("/")

    dir_only = pattern.endswith("/")
    if dir_only:
        pattern = pattern.rstrip("/")

    if "/" not in pattern:
        if dir_only:
            return any(part == pattern for part in rel_posix.split("/"))
        return fnmatch.fnmatch(name, pattern)

    target = rel_posix
    if anchored:
        if dir_only:
            return target.startswith(pattern + "/")
        return fnmatch.fnmatch(target, pattern)
    if dir_only:
        return f"/{pattern}/" in f"/{target}/"
    return fnmatch.fnmatch(target, f"**/{pattern}") or fnmatch.fnmatch(target, pattern)


def is_gitignored(path: Path, git_root: Path, patterns: list[str]) -> bool:
    try:
        rel = path.resolve().relative_to(git_root)
    except Exception:
        rel = path
    rel_posix = rel.as_posix()
    name = rel_posix.split("/")[-1]
    ignored = False
    for pat in patterns:
        neg = pat.startswith("!")
        if gitignore_match(rel_posix, name, pat):
            ignored = not neg
    return ignored


git_root, git_patterns = read_gitignore(root)
def lex_sorted(ps):
    return sorted((str(p) for p in ps), key=lambda s: s)

if TICKET_PATHS:
    tickets = [Path(p.strip()) for p in TICKET_PATHS.split(",") if p.strip()]
else:
    tickets = list(root.glob(TICKET_GLOB)) if root.exists() else []

tickets = [Path(p) for p in lex_sorted(tickets)]
tickets = [p for p in tickets if not is_gitignored(p, git_root, git_patterns)]
if MAX and MAX > 0:
    tickets = tickets[:MAX]

for p in tickets:
    print(str(p))
PY
)

ticket_args=()
for p in "${__tickets[@]}"; do
  ticket_args+=(-f "$p")
done

if [ "${#ticket_args[@]}" -eq 0 ]; then
  echo "WARNING: no tickets resolved for group 'mcp'." >&2
fi

# extra_files appended literally (may be empty; may include -f/--file):
oracle   --files-report   --write-output "docs/oracle-questions-2026-01-09-030000/mcp/02-contracts-interfaces-integration-points-direct-answer.md"   "${ticket_args[@]}"      -p "$(cat <<'PROMPT'
Strategist question #02  (ticket-driven, group: mcp)

Reference: mcp
Category: contracts/interfaces
Horizon: Immediate
ROI: 4.6 (impact=6, confidence=0.78, effort=1)

Question:
Using the attached tickets as the primary context, identify external integrations implied by the tickets; required config/contract changes; failure/timeout behavior; minimal compat-safe rollout.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.

Output section: Direct answer
Start with heading: Direct answer
Return only: Direct answer (1–10 bullets, evidence-cited)
PROMPT
)"

oracle   --files-report   --write-output "docs/oracle-questions-2026-01-09-030000/mcp/02-contracts-interfaces-integration-points-risks-unknowns.md"   "${ticket_args[@]}"      -p "$(cat <<'PROMPT'
Strategist question #02  (ticket-driven, group: mcp)

Reference: mcp
Category: contracts/interfaces
Horizon: Immediate
ROI: 4.6 (impact=6, confidence=0.78, effort=1)

Question:
Using the attached tickets as the primary context, identify external integrations implied by the tickets; required config/contract changes; failure/timeout behavior; minimal compat-safe rollout.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.

Output section: Risks/unknowns
Start with heading: Risks/unknowns
Return only: Risks/unknowns (bullets)
PROMPT
)"

oracle   --files-report   --write-output "docs/oracle-questions-2026-01-09-030000/mcp/02-contracts-interfaces-integration-points-next-experiment.md"   "${ticket_args[@]}"      -p "$(cat <<'PROMPT'
Strategist question #02  (ticket-driven, group: mcp)

Reference: mcp
Category: contracts/interfaces
Horizon: Immediate
ROI: 4.6 (impact=6, confidence=0.78, effort=1)

Question:
Using the attached tickets as the primary context, identify external integrations implied by the tickets; required config/contract changes; failure/timeout behavior; minimal compat-safe rollout.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.

Output section: Next smallest concrete experiment
Start with heading: Next smallest concrete experiment
Return only: Next smallest concrete experiment (exactly one action)
PROMPT
)"

oracle   --files-report   --write-output "docs/oracle-questions-2026-01-09-030000/mcp/02-contracts-interfaces-integration-points-missing-evidence.md"   "${ticket_args[@]}"      -p "$(cat <<'PROMPT'
Strategist question #02  (ticket-driven, group: mcp)

Reference: mcp
Category: contracts/interfaces
Horizon: Immediate
ROI: 4.6 (impact=6, confidence=0.78, effort=1)

Question:
Using the attached tickets as the primary context, identify external integrations implied by the tickets; required config/contract changes; failure/timeout behavior; minimal compat-safe rollout.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.

Output section: Missing evidence
Start with heading: Missing evidence
Return only: If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"


# 03) ROI=5.1 impact=7 confidence=0.74 effort=1 horizon=NearTerm category=invariants reference=mcp

# tickets attached directly (deterministic; self-contained)
mapfile -t __tickets < <(python3 - <<'PY'
from __future__ import annotations
from pathlib import Path
import fnmatch

TICKET_ROOT = ".tickets"
TICKET_GLOB = "**/*.md"
TICKET_PATHS = ".tickets/mcp/Expose Oraclepack as MCP.md,.tickets/mcp/MCP Server for Oraclepack.md,.tickets/mcp/gaps-still-not-covered.md,.tickets/mcp/gaps_part2-mcp-builder.md,.tickets/mcp/oraclepack-MCP.md,.tickets/mcp/oraclepack_mcp_server.md".strip()
MAX = int("6")


root = Path(TICKET_ROOT)

def read_gitignore(start: Path):
    cur = start.resolve()
    if cur.is_file():
        cur = cur.parent
    while True:
        p = cur / ".gitignore"
        if p.exists():
            lines = []
            for ln in p.read_text(encoding="utf-8", errors="replace").splitlines():
                s = ln.strip()
                if not s or s.startswith("#"):
                    continue
                lines.append(s)
            return cur, lines
        if cur.parent == cur:
            return start.resolve(), []
        cur = cur.parent


def gitignore_match(rel_posix: str, name: str, pattern: str) -> bool:
    neg = pattern.startswith("!")
    if neg:
        pattern = pattern[1:]
    if not pattern:
        return False

    anchored = pattern.startswith("/")
    if anchored:
        pattern = pattern.lstrip("/")

    dir_only = pattern.endswith("/")
    if dir_only:
        pattern = pattern.rstrip("/")

    if "/" not in pattern:
        if dir_only:
            return any(part == pattern for part in rel_posix.split("/"))
        return fnmatch.fnmatch(name, pattern)

    target = rel_posix
    if anchored:
        if dir_only:
            return target.startswith(pattern + "/")
        return fnmatch.fnmatch(target, pattern)
    if dir_only:
        return f"/{pattern}/" in f"/{target}/"
    return fnmatch.fnmatch(target, f"**/{pattern}") or fnmatch.fnmatch(target, pattern)


def is_gitignored(path: Path, git_root: Path, patterns: list[str]) -> bool:
    try:
        rel = path.resolve().relative_to(git_root)
    except Exception:
        rel = path
    rel_posix = rel.as_posix()
    name = rel_posix.split("/")[-1]
    ignored = False
    for pat in patterns:
        neg = pat.startswith("!")
        if gitignore_match(rel_posix, name, pat):
            ignored = not neg
    return ignored


git_root, git_patterns = read_gitignore(root)
def lex_sorted(ps):
    return sorted((str(p) for p in ps), key=lambda s: s)

if TICKET_PATHS:
    tickets = [Path(p.strip()) for p in TICKET_PATHS.split(",") if p.strip()]
else:
    tickets = list(root.glob(TICKET_GLOB)) if root.exists() else []

tickets = [Path(p) for p in lex_sorted(tickets)]
tickets = [p for p in tickets if not is_gitignored(p, git_root, git_patterns)]
if MAX and MAX > 0:
    tickets = tickets[:MAX]

for p in tickets:
    print(str(p))
PY
)

ticket_args=()
for p in "${__tickets[@]}"; do
  ticket_args+=(-f "$p")
done

if [ "${#ticket_args[@]}" -eq 0 ]; then
  echo "WARNING: no tickets resolved for group 'mcp'." >&2
fi

# extra_files appended literally (may be empty; may include -f/--file):
oracle   --files-report   --write-output "docs/oracle-questions-2026-01-09-030000/mcp/03-invariants-invariant-map-direct-answer.md"   "${ticket_args[@]}"      -p "$(cat <<'PROMPT'
Strategist question #03  (ticket-driven, group: mcp)

Reference: mcp
Category: invariants
Horizon: NearTerm
ROI: 5.1 (impact=7, confidence=0.74, effort=1)

Question:
Using the attached tickets as the primary context, extract system invariants implied by tickets (inputs/outputs, pack schema rules, step execution rules) and where to enforce them.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.

Output section: Direct answer
Start with heading: Direct answer
Return only: Direct answer (1–10 bullets, evidence-cited)
PROMPT
)"

oracle   --files-report   --write-output "docs/oracle-questions-2026-01-09-030000/mcp/03-invariants-invariant-map-risks-unknowns.md"   "${ticket_args[@]}"      -p "$(cat <<'PROMPT'
Strategist question #03  (ticket-driven, group: mcp)

Reference: mcp
Category: invariants
Horizon: NearTerm
ROI: 5.1 (impact=7, confidence=0.74, effort=1)

Question:
Using the attached tickets as the primary context, extract system invariants implied by tickets (inputs/outputs, pack schema rules, step execution rules) and where to enforce them.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.

Output section: Risks/unknowns
Start with heading: Risks/unknowns
Return only: Risks/unknowns (bullets)
PROMPT
)"

oracle   --files-report   --write-output "docs/oracle-questions-2026-01-09-030000/mcp/03-invariants-invariant-map-next-experiment.md"   "${ticket_args[@]}"      -p "$(cat <<'PROMPT'
Strategist question #03  (ticket-driven, group: mcp)

Reference: mcp
Category: invariants
Horizon: NearTerm
ROI: 5.1 (impact=7, confidence=0.74, effort=1)

Question:
Using the attached tickets as the primary context, extract system invariants implied by tickets (inputs/outputs, pack schema rules, step execution rules) and where to enforce them.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.

Output section: Next smallest concrete experiment
Start with heading: Next smallest concrete experiment
Return only: Next smallest concrete experiment (exactly one action)
PROMPT
)"

oracle   --files-report   --write-output "docs/oracle-questions-2026-01-09-030000/mcp/03-invariants-invariant-map-missing-evidence.md"   "${ticket_args[@]}"      -p "$(cat <<'PROMPT'
Strategist question #03  (ticket-driven, group: mcp)

Reference: mcp
Category: invariants
Horizon: NearTerm
ROI: 5.1 (impact=7, confidence=0.74, effort=1)

Question:
Using the attached tickets as the primary context, extract system invariants implied by tickets (inputs/outputs, pack schema rules, step execution rules) and where to enforce them.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.

Output section: Missing evidence
Start with heading: Missing evidence
Return only: If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"


# 04) ROI=5.0 impact=7 confidence=0.72 effort=2 horizon=NearTerm category=invariants reference=mcp

# tickets attached directly (deterministic; self-contained)
mapfile -t __tickets < <(python3 - <<'PY'
from __future__ import annotations
from pathlib import Path
import fnmatch

TICKET_ROOT = ".tickets"
TICKET_GLOB = "**/*.md"
TICKET_PATHS = ".tickets/mcp/Expose Oraclepack as MCP.md,.tickets/mcp/MCP Server for Oraclepack.md,.tickets/mcp/gaps-still-not-covered.md,.tickets/mcp/gaps_part2-mcp-builder.md,.tickets/mcp/oraclepack-MCP.md,.tickets/mcp/oraclepack_mcp_server.md".strip()
MAX = int("6")


root = Path(TICKET_ROOT)

def read_gitignore(start: Path):
    cur = start.resolve()
    if cur.is_file():
        cur = cur.parent
    while True:
        p = cur / ".gitignore"
        if p.exists():
            lines = []
            for ln in p.read_text(encoding="utf-8", errors="replace").splitlines():
                s = ln.strip()
                if not s or s.startswith("#"):
                    continue
                lines.append(s)
            return cur, lines
        if cur.parent == cur:
            return start.resolve(), []
        cur = cur.parent


def gitignore_match(rel_posix: str, name: str, pattern: str) -> bool:
    neg = pattern.startswith("!")
    if neg:
        pattern = pattern[1:]
    if not pattern:
        return False

    anchored = pattern.startswith("/")
    if anchored:
        pattern = pattern.lstrip("/")

    dir_only = pattern.endswith("/")
    if dir_only:
        pattern = pattern.rstrip("/")

    if "/" not in pattern:
        if dir_only:
            return any(part == pattern for part in rel_posix.split("/"))
        return fnmatch.fnmatch(name, pattern)

    target = rel_posix
    if anchored:
        if dir_only:
            return target.startswith(pattern + "/")
        return fnmatch.fnmatch(target, pattern)
    if dir_only:
        return f"/{pattern}/" in f"/{target}/"
    return fnmatch.fnmatch(target, f"**/{pattern}") or fnmatch.fnmatch(target, pattern)


def is_gitignored(path: Path, git_root: Path, patterns: list[str]) -> bool:
    try:
        rel = path.resolve().relative_to(git_root)
    except Exception:
        rel = path
    rel_posix = rel.as_posix()
    name = rel_posix.split("/")[-1]
    ignored = False
    for pat in patterns:
        neg = pat.startswith("!")
        if gitignore_match(rel_posix, name, pat):
            ignored = not neg
    return ignored


git_root, git_patterns = read_gitignore(root)
def lex_sorted(ps):
    return sorted((str(p) for p in ps), key=lambda s: s)

if TICKET_PATHS:
    tickets = [Path(p.strip()) for p in TICKET_PATHS.split(",") if p.strip()]
else:
    tickets = list(root.glob(TICKET_GLOB)) if root.exists() else []

tickets = [Path(p) for p in lex_sorted(tickets)]
tickets = [p for p in tickets if not is_gitignored(p, git_root, git_patterns)]
if MAX and MAX > 0:
    tickets = tickets[:MAX]

for p in tickets:
    print(str(p))
PY
)

ticket_args=()
for p in "${__tickets[@]}"; do
  ticket_args+=(-f "$p")
done

if [ "${#ticket_args[@]}" -eq 0 ]; then
  echo "WARNING: no tickets resolved for group 'mcp'." >&2
fi

# extra_files appended literally (may be empty; may include -f/--file):
[TRUNCATED]
```

docs/oracle-questions-2026-01-09-030000/packs/misc.md
```
# Oracle Pack — Unknown (Grouped Tickets Stage 1 — Direct Attach)

## Parsed args
- codebase_name: Unknown
- out_dir: docs/oracle-questions-2026-01-09-030000/misc
- oracle_cmd: oracle
- oracle_flags: --files-report
- extra_files: 
- ticket_root: .tickets
- ticket_glob: **/*.md
- ticket_paths: .tickets/Formalize LLM Decision Points.md,.tickets/Oraclepack CLI MCP Parity.md,.tickets/Oraclepack File Storage.md,.tickets/Oraclepack Parity Automation.md,.tickets/Oraclepack Schema Approach.md,.tickets/Oraclepack bash fix.md,.tickets/Oraclepack output verification issues.md,.tickets/Oraclepack-CLI-agents.md,.tickets/Publish OraclePack MCP.md
- ticket_max_files: 9
- group_name: misc
- group_slug: misc
- mode: tickets-grouped-direct

Notes (contract):
- Exactly one fenced `bash` block in this document.
- No other ``` fences anywhere.
- Exactly 20 steps, numbered 01..20 in order.
- Step header: `# NN) ROI=... impact=... confidence=... effort=... horizon=... category=... reference=...`
- Every step includes: `--write-output "docs/oracle-questions-2026-01-09-030000/misc/NN-<slug>.md"` (double quotes required).
- Steps must be self-contained and must not rely on shell variables created in previous steps.
- Each step must attach tickets directly (no `_tickets_bundle.md` dependency).
- Pack ends with a Coverage check section listing all 10 categories as OK or Missing(<step ids>).

```bash
set -euo pipefail

mkdir -p "docs/oracle-questions-2026-01-09-030000/misc"

# 01) ROI=4.8 impact=6 confidence=0.80 effort=1 horizon=Immediate category=contracts/interfaces reference=misc

# tickets attached directly (deterministic; self-contained)
mapfile -t __tickets < <(python3 - <<'PY'
from __future__ import annotations
from pathlib import Path
import fnmatch

TICKET_ROOT = ".tickets"
TICKET_GLOB = "**/*.md"
TICKET_PATHS = ".tickets/Formalize LLM Decision Points.md,.tickets/Oraclepack CLI MCP Parity.md,.tickets/Oraclepack File Storage.md,.tickets/Oraclepack Parity Automation.md,.tickets/Oraclepack Schema Approach.md,.tickets/Oraclepack bash fix.md,.tickets/Oraclepack output verification issues.md,.tickets/Oraclepack-CLI-agents.md,.tickets/Publish OraclePack MCP.md".strip()
MAX = int("9")


root = Path(TICKET_ROOT)

def read_gitignore(start: Path):
    cur = start.resolve()
    if cur.is_file():
        cur = cur.parent
    while True:
        p = cur / ".gitignore"
        if p.exists():
            lines = []
            for ln in p.read_text(encoding="utf-8", errors="replace").splitlines():
                s = ln.strip()
                if not s or s.startswith("#"):
                    continue
                lines.append(s)
            return cur, lines
        if cur.parent == cur:
            return start.resolve(), []
        cur = cur.parent


def gitignore_match(rel_posix: str, name: str, pattern: str) -> bool:
    neg = pattern.startswith("!")
    if neg:
        pattern = pattern[1:]
    if not pattern:
        return False

    anchored = pattern.startswith("/")
    if anchored:
        pattern = pattern.lstrip("/")

    dir_only = pattern.endswith("/")
    if dir_only:
        pattern = pattern.rstrip("/")

    if "/" not in pattern:
        if dir_only:
            return any(part == pattern for part in rel_posix.split("/"))
        return fnmatch.fnmatch(name, pattern)

    target = rel_posix
    if anchored:
        if dir_only:
            return target.startswith(pattern + "/")
        return fnmatch.fnmatch(target, pattern)
    if dir_only:
        return f"/{pattern}/" in f"/{target}/"
    return fnmatch.fnmatch(target, f"**/{pattern}") or fnmatch.fnmatch(target, pattern)


def is_gitignored(path: Path, git_root: Path, patterns: list[str]) -> bool:
    try:
        rel = path.resolve().relative_to(git_root)
    except Exception:
        rel = path
    rel_posix = rel.as_posix()
    name = rel_posix.split("/")[-1]
    ignored = False
    for pat in patterns:
        neg = pat.startswith("!")
        if gitignore_match(rel_posix, name, pat):
            ignored = not neg
    return ignored


git_root, git_patterns = read_gitignore(root)
def lex_sorted(ps):
    return sorted((str(p) for p in ps), key=lambda s: s)

if TICKET_PATHS:
    tickets = [Path(p.strip()) for p in TICKET_PATHS.split(",") if p.strip()]
else:
    tickets = list(root.glob(TICKET_GLOB)) if root.exists() else []

tickets = [Path(p) for p in lex_sorted(tickets)]
tickets = [p for p in tickets if not is_gitignored(p, git_root, git_patterns)]
if MAX and MAX > 0:
    tickets = tickets[:MAX]

for p in tickets:
    print(str(p))
PY
)

ticket_args=()
for p in "${__tickets[@]}"; do
  ticket_args+=(-f "$p")
done

if [ "${#ticket_args[@]}" -eq 0 ]; then
  echo "WARNING: no tickets resolved for group 'misc'." >&2
fi

# extra_files appended literally (may be empty; may include -f/--file):
oracle   --files-report   --write-output "docs/oracle-questions-2026-01-09-030000/misc/01-contracts-interfaces-ticket-surface-direct-answer.md"   "${ticket_args[@]}"      -p "$(cat <<'PROMPT'
Strategist question #01  (ticket-driven, group: misc)

Reference: misc
Category: contracts/interfaces
Horizon: Immediate
ROI: 4.8 (impact=6, confidence=0.80, effort=1)

Question:
Using the attached tickets as the primary context, list the public surface changes implied by the tickets (CLI/TUI/API/interfaces/contracts); call out backwards-compat constraints.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.

Output section: Direct answer
Start with heading: Direct answer
Return only: Direct answer (1–10 bullets, evidence-cited)
PROMPT
)"

oracle   --files-report   --write-output "docs/oracle-questions-2026-01-09-030000/misc/01-contracts-interfaces-ticket-surface-risks-unknowns.md"   "${ticket_args[@]}"      -p "$(cat <<'PROMPT'
Strategist question #01  (ticket-driven, group: misc)

Reference: misc
Category: contracts/interfaces
Horizon: Immediate
ROI: 4.8 (impact=6, confidence=0.80, effort=1)

Question:
Using the attached tickets as the primary context, list the public surface changes implied by the tickets (CLI/TUI/API/interfaces/contracts); call out backwards-compat constraints.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.

Output section: Risks/unknowns
Start with heading: Risks/unknowns
Return only: Risks/unknowns (bullets)
PROMPT
)"

oracle   --files-report   --write-output "docs/oracle-questions-2026-01-09-030000/misc/01-contracts-interfaces-ticket-surface-next-experiment.md"   "${ticket_args[@]}"      -p "$(cat <<'PROMPT'
Strategist question #01  (ticket-driven, group: misc)

Reference: misc
Category: contracts/interfaces
Horizon: Immediate
ROI: 4.8 (impact=6, confidence=0.80, effort=1)

Question:
Using the attached tickets as the primary context, list the public surface changes implied by the tickets (CLI/TUI/API/interfaces/contracts); call out backwards-compat constraints.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.

Output section: Next smallest concrete experiment
Start with heading: Next smallest concrete experiment
Return only: Next smallest concrete experiment (exactly one action)
PROMPT
)"

oracle   --files-report   --write-output "docs/oracle-questions-2026-01-09-030000/misc/01-contracts-interfaces-ticket-surface-missing-evidence.md"   "${ticket_args[@]}"      -p "$(cat <<'PROMPT'
Strategist question #01  (ticket-driven, group: misc)

Reference: misc
Category: contracts/interfaces
Horizon: Immediate
ROI: 4.8 (impact=6, confidence=0.80, effort=1)

Question:
Using the attached tickets as the primary context, list the public surface changes implied by the tickets (CLI/TUI/API/interfaces/contracts); call out backwards-compat constraints.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.

Output section: Missing evidence
Start with heading: Missing evidence
Return only: If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"


# 02) ROI=4.6 impact=6 confidence=0.78 effort=1 horizon=Immediate category=contracts/interfaces reference=misc

# tickets attached directly (deterministic; self-contained)
mapfile -t __tickets < <(python3 - <<'PY'
from __future__ import annotations
from pathlib import Path
import fnmatch

TICKET_ROOT = ".tickets"
TICKET_GLOB = "**/*.md"
TICKET_PATHS = ".tickets/Formalize LLM Decision Points.md,.tickets/Oraclepack CLI MCP Parity.md,.tickets/Oraclepack File Storage.md,.tickets/Oraclepack Parity Automation.md,.tickets/Oraclepack Schema Approach.md,.tickets/Oraclepack bash fix.md,.tickets/Oraclepack output verification issues.md,.tickets/Oraclepack-CLI-agents.md,.tickets/Publish OraclePack MCP.md".strip()
MAX = int("9")


root = Path(TICKET_ROOT)

def read_gitignore(start: Path):
    cur = start.resolve()
    if cur.is_file():
        cur = cur.parent
    while True:
        p = cur / ".gitignore"
        if p.exists():
            lines = []
            for ln in p.read_text(encoding="utf-8", errors="replace").splitlines():
                s = ln.strip()
                if not s or s.startswith("#"):
                    continue
                lines.append(s)
            return cur, lines
        if cur.parent == cur:
            return start.resolve(), []
        cur = cur.parent


def gitignore_match(rel_posix: str, name: str, pattern: str) -> bool:
    neg = pattern.startswith("!")
    if neg:
        pattern = pattern[1:]
    if not pattern:
        return False

    anchored = pattern.startswith("/")
    if anchored:
        pattern = pattern.lstrip("/")

    dir_only = pattern.endswith("/")
    if dir_only:
        pattern = pattern.rstrip("/")

    if "/" not in pattern:
        if dir_only:
            return any(part == pattern for part in rel_posix.split("/"))
        return fnmatch.fnmatch(name, pattern)

    target = rel_posix
    if anchored:
        if dir_only:
            return target.startswith(pattern + "/")
        return fnmatch.fnmatch(target, pattern)
    if dir_only:
        return f"/{pattern}/" in f"/{target}/"
    return fnmatch.fnmatch(target, f"**/{pattern}") or fnmatch.fnmatch(target, pattern)


def is_gitignored(path: Path, git_root: Path, patterns: list[str]) -> bool:
    try:
        rel = path.resolve().relative_to(git_root)
    except Exception:
        rel = path
    rel_posix = rel.as_posix()
    name = rel_posix.split("/")[-1]
    ignored = False
    for pat in patterns:
        neg = pat.startswith("!")
        if gitignore_match(rel_posix, name, pat):
            ignored = not neg
    return ignored


git_root, git_patterns = read_gitignore(root)
def lex_sorted(ps):
    return sorted((str(p) for p in ps), key=lambda s: s)

if TICKET_PATHS:
    tickets = [Path(p.strip()) for p in TICKET_PATHS.split(",") if p.strip()]
else:
    tickets = list(root.glob(TICKET_GLOB)) if root.exists() else []

tickets = [Path(p) for p in lex_sorted(tickets)]
tickets = [p for p in tickets if not is_gitignored(p, git_root, git_patterns)]
if MAX and MAX > 0:
    tickets = tickets[:MAX]

for p in tickets:
    print(str(p))
PY
)

ticket_args=()
for p in "${__tickets[@]}"; do
  ticket_args+=(-f "$p")
done

if [ "${#ticket_args[@]}" -eq 0 ]; then
  echo "WARNING: no tickets resolved for group 'misc'." >&2
fi

# extra_files appended literally (may be empty; may include -f/--file):
oracle   --files-report   --write-output "docs/oracle-questions-2026-01-09-030000/misc/02-contracts-interfaces-integration-points-direct-answer.md"   "${ticket_args[@]}"      -p "$(cat <<'PROMPT'
Strategist question #02  (ticket-driven, group: misc)

Reference: misc
Category: contracts/interfaces
Horizon: Immediate
ROI: 4.6 (impact=6, confidence=0.78, effort=1)

Question:
Using the attached tickets as the primary context, identify external integrations implied by the tickets; required config/contract changes; failure/timeout behavior; minimal compat-safe rollout.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.

Output section: Direct answer
Start with heading: Direct answer
Return only: Direct answer (1–10 bullets, evidence-cited)
PROMPT
)"

oracle   --files-report   --write-output "docs/oracle-questions-2026-01-09-030000/misc/02-contracts-interfaces-integration-points-risks-unknowns.md"   "${ticket_args[@]}"      -p "$(cat <<'PROMPT'
Strategist question #02  (ticket-driven, group: misc)

Reference: misc
Category: contracts/interfaces
Horizon: Immediate
ROI: 4.6 (impact=6, confidence=0.78, effort=1)

Question:
Using the attached tickets as the primary context, identify external integrations implied by the tickets; required config/contract changes; failure/timeout behavior; minimal compat-safe rollout.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.

Output section: Risks/unknowns
Start with heading: Risks/unknowns
Return only: Risks/unknowns (bullets)
PROMPT
)"

oracle   --files-report   --write-output "docs/oracle-questions-2026-01-09-030000/misc/02-contracts-interfaces-integration-points-next-experiment.md"   "${ticket_args[@]}"      -p "$(cat <<'PROMPT'
Strategist question #02  (ticket-driven, group: misc)

Reference: misc
Category: contracts/interfaces
Horizon: Immediate
ROI: 4.6 (impact=6, confidence=0.78, effort=1)

Question:
Using the attached tickets as the primary context, identify external integrations implied by the tickets; required config/contract changes; failure/timeout behavior; minimal compat-safe rollout.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.

Output section: Next smallest concrete experiment
Start with heading: Next smallest concrete experiment
Return only: Next smallest concrete experiment (exactly one action)
PROMPT
)"

oracle   --files-report   --write-output "docs/oracle-questions-2026-01-09-030000/misc/02-contracts-interfaces-integration-points-missing-evidence.md"   "${ticket_args[@]}"      -p "$(cat <<'PROMPT'
Strategist question #02  (ticket-driven, group: misc)

Reference: misc
Category: contracts/interfaces
Horizon: Immediate
ROI: 4.6 (impact=6, confidence=0.78, effort=1)

Question:
Using the attached tickets as the primary context, identify external integrations implied by the tickets; required config/contract changes; failure/timeout behavior; minimal compat-safe rollout.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.

Output section: Missing evidence
Start with heading: Missing evidence
Return only: If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"


# 03) ROI=5.1 impact=7 confidence=0.74 effort=1 horizon=NearTerm category=invariants reference=misc

# tickets attached directly (deterministic; self-contained)
mapfile -t __tickets < <(python3 - <<'PY'
from __future__ import annotations
from pathlib import Path
import fnmatch

TICKET_ROOT = ".tickets"
TICKET_GLOB = "**/*.md"
TICKET_PATHS = ".tickets/Formalize LLM Decision Points.md,.tickets/Oraclepack CLI MCP Parity.md,.tickets/Oraclepack File Storage.md,.tickets/Oraclepack Parity Automation.md,.tickets/Oraclepack Schema Approach.md,.tickets/Oraclepack bash fix.md,.tickets/Oraclepack output verification issues.md,.tickets/Oraclepack-CLI-agents.md,.tickets/Publish OraclePack MCP.md".strip()
MAX = int("9")


root = Path(TICKET_ROOT)

def read_gitignore(start: Path):
    cur = start.resolve()
    if cur.is_file():
        cur = cur.parent
    while True:
        p = cur / ".gitignore"
        if p.exists():
            lines = []
            for ln in p.read_text(encoding="utf-8", errors="replace").splitlines():
                s = ln.strip()
                if not s or s.startswith("#"):
                    continue
                lines.append(s)
            return cur, lines
        if cur.parent == cur:
            return start.resolve(), []
        cur = cur.parent


def gitignore_match(rel_posix: str, name: str, pattern: str) -> bool:
    neg = pattern.startswith("!")
    if neg:
        pattern = pattern[1:]
    if not pattern:
        return False

    anchored = pattern.startswith("/")
    if anchored:
        pattern = pattern.lstrip("/")

    dir_only = pattern.endswith("/")
    if dir_only:
        pattern = pattern.rstrip("/")

    if "/" not in pattern:
        if dir_only:
            return any(part == pattern for part in rel_posix.split("/"))
        return fnmatch.fnmatch(name, pattern)

    target = rel_posix
    if anchored:
        if dir_only:
            return target.startswith(pattern + "/")
        return fnmatch.fnmatch(target, pattern)
    if dir_only:
        return f"/{pattern}/" in f"/{target}/"
    return fnmatch.fnmatch(target, f"**/{pattern}") or fnmatch.fnmatch(target, pattern)


def is_gitignored(path: Path, git_root: Path, patterns: list[str]) -> bool:
    try:
        rel = path.resolve().relative_to(git_root)
    except Exception:
        rel = path
    rel_posix = rel.as_posix()
    name = rel_posix.split("/")[-1]
    ignored = False
    for pat in patterns:
        neg = pat.startswith("!")
        if gitignore_match(rel_posix, name, pat):
            ignored = not neg
    return ignored


git_root, git_patterns = read_gitignore(root)
def lex_sorted(ps):
    return sorted((str(p) for p in ps), key=lambda s: s)

if TICKET_PATHS:
    tickets = [Path(p.strip()) for p in TICKET_PATHS.split(",") if p.strip()]
else:
    tickets = list(root.glob(TICKET_GLOB)) if root.exists() else []

tickets = [Path(p) for p in lex_sorted(tickets)]
tickets = [p for p in tickets if not is_gitignored(p, git_root, git_patterns)]
if MAX and MAX > 0:
    tickets = tickets[:MAX]

for p in tickets:
    print(str(p))
PY
)

ticket_args=()
for p in "${__tickets[@]}"; do
  ticket_args+=(-f "$p")
done

if [ "${#ticket_args[@]}" -eq 0 ]; then
  echo "WARNING: no tickets resolved for group 'misc'." >&2
fi

# extra_files appended literally (may be empty; may include -f/--file):
oracle   --files-report   --write-output "docs/oracle-questions-2026-01-09-030000/misc/03-invariants-invariant-map-direct-answer.md"   "${ticket_args[@]}"      -p "$(cat <<'PROMPT'
Strategist question #03  (ticket-driven, group: misc)

Reference: misc
Category: invariants
Horizon: NearTerm
ROI: 5.1 (impact=7, confidence=0.74, effort=1)

Question:
Using the attached tickets as the primary context, extract system invariants implied by tickets (inputs/outputs, pack schema rules, step execution rules) and where to enforce them.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.

Output section: Direct answer
Start with heading: Direct answer
Return only: Direct answer (1–10 bullets, evidence-cited)
PROMPT
)"

oracle   --files-report   --write-output "docs/oracle-questions-2026-01-09-030000/misc/03-invariants-invariant-map-risks-unknowns.md"   "${ticket_args[@]}"      -p "$(cat <<'PROMPT'
Strategist question #03  (ticket-driven, group: misc)

Reference: misc
Category: invariants
Horizon: NearTerm
ROI: 5.1 (impact=7, confidence=0.74, effort=1)

Question:
Using the attached tickets as the primary context, extract system invariants implied by tickets (inputs/outputs, pack schema rules, step execution rules) and where to enforce them.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.

Output section: Risks/unknowns
Start with heading: Risks/unknowns
Return only: Risks/unknowns (bullets)
PROMPT
)"

oracle   --files-report   --write-output "docs/oracle-questions-2026-01-09-030000/misc/03-invariants-invariant-map-next-experiment.md"   "${ticket_args[@]}"      -p "$(cat <<'PROMPT'
Strategist question #03  (ticket-driven, group: misc)

Reference: misc
Category: invariants
Horizon: NearTerm
ROI: 5.1 (impact=7, confidence=0.74, effort=1)

Question:
Using the attached tickets as the primary context, extract system invariants implied by tickets (inputs/outputs, pack schema rules, step execution rules) and where to enforce them.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.

Output section: Next smallest concrete experiment
Start with heading: Next smallest concrete experiment
Return only: Next smallest concrete experiment (exactly one action)
PROMPT
)"

oracle   --files-report   --write-output "docs/oracle-questions-2026-01-09-030000/misc/03-invariants-invariant-map-missing-evidence.md"   "${ticket_args[@]}"      -p "$(cat <<'PROMPT'
Strategist question #03  (ticket-driven, group: misc)

Reference: misc
Category: invariants
Horizon: NearTerm
ROI: 5.1 (impact=7, confidence=0.74, effort=1)

Question:
Using the attached tickets as the primary context, extract system invariants implied by tickets (inputs/outputs, pack schema rules, step execution rules) and where to enforce them.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.

Output section: Missing evidence
Start with heading: Missing evidence
Return only: If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"


# 04) ROI=5.0 impact=7 confidence=0.72 effort=2 horizon=NearTerm category=invariants reference=misc

# tickets attached directly (deterministic; self-contained)
mapfile -t __tickets < <(python3 - <<'PY'
from __future__ import annotations
from pathlib import Path
import fnmatch

TICKET_ROOT = ".tickets"
TICKET_GLOB = "**/*.md"
TICKET_PATHS = ".tickets/Formalize LLM Decision Points.md,.tickets/Oraclepack CLI MCP Parity.md,.tickets/Oraclepack File Storage.md,.tickets/Oraclepack Parity Automation.md,.tickets/Oraclepack Schema Approach.md,.tickets/Oraclepack bash fix.md,.tickets/Oraclepack output verification issues.md,.tickets/Oraclepack-CLI-agents.md,.tickets/Publish OraclePack MCP.md".strip()
MAX = int("9")


root = Path(TICKET_ROOT)

def read_gitignore(start: Path):
    cur = start.resolve()
    if cur.is_file():
        cur = cur.parent
    while True:
        p = cur / ".gitignore"
        if p.exists():
            lines = []
            for ln in p.read_text(encoding="utf-8", errors="replace").splitlines():
                s = ln.strip()
                if not s or s.startswith("#"):
                    continue
                lines.append(s)
            return cur, lines
        if cur.parent == cur:
            return start.resolve(), []
        cur = cur.parent


def gitignore_match(rel_posix: str, name: str, pattern: str) -> bool:
    neg = pattern.startswith("!")
    if neg:
        pattern = pattern[1:]
    if not pattern:
        return False

    anchored = pattern.startswith("/")
    if anchored:
        pattern = pattern.lstrip("/")

    dir_only = pattern.endswith("/")
    if dir_only:
        pattern = pattern.rstrip("/")

    if "/" not in pattern:
        if dir_only:
            return any(part == pattern for part in rel_posix.split("/"))
        return fnmatch.fnmatch(name, pattern)

    target = rel_posix
    if anchored:
        if dir_only:
            return target.startswith(pattern + "/")
        return fnmatch.fnmatch(target, pattern)
    if dir_only:
        return f"/{pattern}/" in f"/{target}/"
    return fnmatch.fnmatch(target, f"**/{pattern}") or fnmatch.fnmatch(target, pattern)


def is_gitignored(path: Path, git_root: Path, patterns: list[str]) -> bool:
    try:
        rel = path.resolve().relative_to(git_root)
    except Exception:
        rel = path
    rel_posix = rel.as_posix()
    name = rel_posix.split("/")[-1]
    ignored = False
    for pat in patterns:
        neg = pat.startswith("!")
        if gitignore_match(rel_posix, name, pat):
            ignored = not neg
    return ignored


git_root, git_patterns = read_gitignore(root)
def lex_sorted(ps):
    return sorted((str(p) for p in ps), key=lambda s: s)

if TICKET_PATHS:
    tickets = [Path(p.strip()) for p in TICKET_PATHS.split(",") if p.strip()]
else:
    tickets = list(root.glob(TICKET_GLOB)) if root.exists() else []

tickets = [Path(p) for p in lex_sorted(tickets)]
tickets = [p for p in tickets if not is_gitignored(p, git_root, git_patterns)]
if MAX and MAX > 0:
    tickets = tickets[:MAX]

for p in tickets:
    print(str(p))
PY
)

ticket_args=()
for p in "${__tickets[@]}"; do
  ticket_args+=(-f "$p")
done

[TRUNCATED]
```

docs/oracle-questions-2026-01-09-030000/packs/other.md
```
# Oracle Pack — Unknown (Grouped Tickets Stage 1 — Direct Attach)

## Parsed args
- codebase_name: Unknown
- out_dir: docs/oracle-questions-2026-01-09-030000/other
- oracle_cmd: oracle
- oracle_flags: --files-report
- extra_files: 
- ticket_root: .tickets
- ticket_glob: **/*.md
- ticket_paths: .tickets/other/Oraclepack Pipeline Improvements.md,.tickets/other/Oraclepack Prompt Generator.md,.tickets/other/Oraclepack Workflow Enhancement.md,.tickets/other/Verbose Payload Rendering TUI.md
- ticket_max_files: 4
- group_name: other
- group_slug: other
- mode: tickets-grouped-direct

Notes (contract):
- Exactly one fenced `bash` block in this document.
- No other ``` fences anywhere.
- Exactly 20 steps, numbered 01..20 in order.
- Step header: `# NN) ROI=... impact=... confidence=... effort=... horizon=... category=... reference=...`
- Every step includes: `--write-output "docs/oracle-questions-2026-01-09-030000/other/NN-<slug>.md"` (double quotes required).
- Steps must be self-contained and must not rely on shell variables created in previous steps.
- Each step must attach tickets directly (no `_tickets_bundle.md` dependency).
- Pack ends with a Coverage check section listing all 10 categories as OK or Missing(<step ids>).

```bash
set -euo pipefail

mkdir -p "docs/oracle-questions-2026-01-09-030000/other"

# 01) ROI=4.8 impact=6 confidence=0.80 effort=1 horizon=Immediate category=contracts/interfaces reference=other

# tickets attached directly (deterministic; self-contained)
mapfile -t __tickets < <(python3 - <<'PY'
from __future__ import annotations
from pathlib import Path
import fnmatch

TICKET_ROOT = ".tickets"
TICKET_GLOB = "**/*.md"
TICKET_PATHS = ".tickets/other/Oraclepack Pipeline Improvements.md,.tickets/other/Oraclepack Prompt Generator.md,.tickets/other/Oraclepack Workflow Enhancement.md,.tickets/other/Verbose Payload Rendering TUI.md".strip()
MAX = int("4")


root = Path(TICKET_ROOT)

def read_gitignore(start: Path):
    cur = start.resolve()
    if cur.is_file():
        cur = cur.parent
    while True:
        p = cur / ".gitignore"
        if p.exists():
            lines = []
            for ln in p.read_text(encoding="utf-8", errors="replace").splitlines():
                s = ln.strip()
                if not s or s.startswith("#"):
                    continue
                lines.append(s)
            return cur, lines
        if cur.parent == cur:
            return start.resolve(), []
        cur = cur.parent


def gitignore_match(rel_posix: str, name: str, pattern: str) -> bool:
    neg = pattern.startswith("!")
    if neg:
        pattern = pattern[1:]
    if not pattern:
        return False

    anchored = pattern.startswith("/")
    if anchored:
        pattern = pattern.lstrip("/")

    dir_only = pattern.endswith("/")
    if dir_only:
        pattern = pattern.rstrip("/")

    if "/" not in pattern:
        if dir_only:
            return any(part == pattern for part in rel_posix.split("/"))
        return fnmatch.fnmatch(name, pattern)

    target = rel_posix
    if anchored:
        if dir_only:
            return target.startswith(pattern + "/")
        return fnmatch.fnmatch(target, pattern)
    if dir_only:
        return f"/{pattern}/" in f"/{target}/"
    return fnmatch.fnmatch(target, f"**/{pattern}") or fnmatch.fnmatch(target, pattern)


def is_gitignored(path: Path, git_root: Path, patterns: list[str]) -> bool:
    try:
        rel = path.resolve().relative_to(git_root)
    except Exception:
        rel = path
    rel_posix = rel.as_posix()
    name = rel_posix.split("/")[-1]
    ignored = False
    for pat in patterns:
        neg = pat.startswith("!")
        if gitignore_match(rel_posix, name, pat):
            ignored = not neg
    return ignored


git_root, git_patterns = read_gitignore(root)
def lex_sorted(ps):
    return sorted((str(p) for p in ps), key=lambda s: s)

if TICKET_PATHS:
    tickets = [Path(p.strip()) for p in TICKET_PATHS.split(",") if p.strip()]
else:
    tickets = list(root.glob(TICKET_GLOB)) if root.exists() else []

tickets = [Path(p) for p in lex_sorted(tickets)]
tickets = [p for p in tickets if not is_gitignored(p, git_root, git_patterns)]
if MAX and MAX > 0:
    tickets = tickets[:MAX]

for p in tickets:
    print(str(p))
PY
)

ticket_args=()
for p in "${__tickets[@]}"; do
  ticket_args+=(-f "$p")
done

if [ "${#ticket_args[@]}" -eq 0 ]; then
  echo "WARNING: no tickets resolved for group 'other'." >&2
fi

# extra_files appended literally (may be empty; may include -f/--file):
oracle   --files-report   --write-output "docs/oracle-questions-2026-01-09-030000/other/01-contracts-interfaces-ticket-surface-direct-answer.md"   "${ticket_args[@]}"      -p "$(cat <<'PROMPT'
Strategist question #01  (ticket-driven, group: other)

Reference: other
Category: contracts/interfaces
Horizon: Immediate
ROI: 4.8 (impact=6, confidence=0.80, effort=1)

Question:
Using the attached tickets as the primary context, list the public surface changes implied by the tickets (CLI/TUI/API/interfaces/contracts); call out backwards-compat constraints.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.

Output section: Direct answer
Start with heading: Direct answer
Return only: Direct answer (1–10 bullets, evidence-cited)
PROMPT
)"

oracle   --files-report   --write-output "docs/oracle-questions-2026-01-09-030000/other/01-contracts-interfaces-ticket-surface-risks-unknowns.md"   "${ticket_args[@]}"      -p "$(cat <<'PROMPT'
Strategist question #01  (ticket-driven, group: other)

Reference: other
Category: contracts/interfaces
Horizon: Immediate
ROI: 4.8 (impact=6, confidence=0.80, effort=1)

Question:
Using the attached tickets as the primary context, list the public surface changes implied by the tickets (CLI/TUI/API/interfaces/contracts); call out backwards-compat constraints.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.

Output section: Risks/unknowns
Start with heading: Risks/unknowns
Return only: Risks/unknowns (bullets)
PROMPT
)"

oracle   --files-report   --write-output "docs/oracle-questions-2026-01-09-030000/other/01-contracts-interfaces-ticket-surface-next-experiment.md"   "${ticket_args[@]}"      -p "$(cat <<'PROMPT'
Strategist question #01  (ticket-driven, group: other)

Reference: other
Category: contracts/interfaces
Horizon: Immediate
ROI: 4.8 (impact=6, confidence=0.80, effort=1)

Question:
Using the attached tickets as the primary context, list the public surface changes implied by the tickets (CLI/TUI/API/interfaces/contracts); call out backwards-compat constraints.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.

Output section: Next smallest concrete experiment
Start with heading: Next smallest concrete experiment
Return only: Next smallest concrete experiment (exactly one action)
PROMPT
)"

oracle   --files-report   --write-output "docs/oracle-questions-2026-01-09-030000/other/01-contracts-interfaces-ticket-surface-missing-evidence.md"   "${ticket_args[@]}"      -p "$(cat <<'PROMPT'
Strategist question #01  (ticket-driven, group: other)

Reference: other
Category: contracts/interfaces
Horizon: Immediate
ROI: 4.8 (impact=6, confidence=0.80, effort=1)

Question:
Using the attached tickets as the primary context, list the public surface changes implied by the tickets (CLI/TUI/API/interfaces/contracts); call out backwards-compat constraints.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.

Output section: Missing evidence
Start with heading: Missing evidence
Return only: If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"


# 02) ROI=4.6 impact=6 confidence=0.78 effort=1 horizon=Immediate category=contracts/interfaces reference=other

# tickets attached directly (deterministic; self-contained)
mapfile -t __tickets < <(python3 - <<'PY'
from __future__ import annotations
from pathlib import Path
import fnmatch

TICKET_ROOT = ".tickets"
TICKET_GLOB = "**/*.md"
TICKET_PATHS = ".tickets/other/Oraclepack Pipeline Improvements.md,.tickets/other/Oraclepack Prompt Generator.md,.tickets/other/Oraclepack Workflow Enhancement.md,.tickets/other/Verbose Payload Rendering TUI.md".strip()
MAX = int("4")


root = Path(TICKET_ROOT)

def read_gitignore(start: Path):
    cur = start.resolve()
    if cur.is_file():
        cur = cur.parent
    while True:
        p = cur / ".gitignore"
        if p.exists():
            lines = []
            for ln in p.read_text(encoding="utf-8", errors="replace").splitlines():
                s = ln.strip()
                if not s or s.startswith("#"):
                    continue
                lines.append(s)
            return cur, lines
        if cur.parent == cur:
            return start.resolve(), []
        cur = cur.parent


def gitignore_match(rel_posix: str, name: str, pattern: str) -> bool:
    neg = pattern.startswith("!")
    if neg:
        pattern = pattern[1:]
    if not pattern:
        return False

    anchored = pattern.startswith("/")
    if anchored:
        pattern = pattern.lstrip("/")

    dir_only = pattern.endswith("/")
    if dir_only:
        pattern = pattern.rstrip("/")

    if "/" not in pattern:
        if dir_only:
            return any(part == pattern for part in rel_posix.split("/"))
        return fnmatch.fnmatch(name, pattern)

    target = rel_posix
    if anchored:
        if dir_only:
            return target.startswith(pattern + "/")
        return fnmatch.fnmatch(target, pattern)
    if dir_only:
        return f"/{pattern}/" in f"/{target}/"
    return fnmatch.fnmatch(target, f"**/{pattern}") or fnmatch.fnmatch(target, pattern)


def is_gitignored(path: Path, git_root: Path, patterns: list[str]) -> bool:
    try:
        rel = path.resolve().relative_to(git_root)
    except Exception:
        rel = path
    rel_posix = rel.as_posix()
    name = rel_posix.split("/")[-1]
    ignored = False
    for pat in patterns:
        neg = pat.startswith("!")
        if gitignore_match(rel_posix, name, pat):
            ignored = not neg
    return ignored


git_root, git_patterns = read_gitignore(root)
def lex_sorted(ps):
    return sorted((str(p) for p in ps), key=lambda s: s)

if TICKET_PATHS:
    tickets = [Path(p.strip()) for p in TICKET_PATHS.split(",") if p.strip()]
else:
    tickets = list(root.glob(TICKET_GLOB)) if root.exists() else []

tickets = [Path(p) for p in lex_sorted(tickets)]
tickets = [p for p in tickets if not is_gitignored(p, git_root, git_patterns)]
if MAX and MAX > 0:
    tickets = tickets[:MAX]

for p in tickets:
    print(str(p))
PY
)

ticket_args=()
for p in "${__tickets[@]}"; do
  ticket_args+=(-f "$p")
done

if [ "${#ticket_args[@]}" -eq 0 ]; then
  echo "WARNING: no tickets resolved for group 'other'." >&2
fi

# extra_files appended literally (may be empty; may include -f/--file):
oracle   --files-report   --write-output "docs/oracle-questions-2026-01-09-030000/other/02-contracts-interfaces-integration-points-direct-answer.md"   "${ticket_args[@]}"      -p "$(cat <<'PROMPT'
Strategist question #02  (ticket-driven, group: other)

Reference: other
Category: contracts/interfaces
Horizon: Immediate
ROI: 4.6 (impact=6, confidence=0.78, effort=1)

Question:
Using the attached tickets as the primary context, identify external integrations implied by the tickets; required config/contract changes; failure/timeout behavior; minimal compat-safe rollout.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.

Output section: Direct answer
Start with heading: Direct answer
Return only: Direct answer (1–10 bullets, evidence-cited)
PROMPT
)"

oracle   --files-report   --write-output "docs/oracle-questions-2026-01-09-030000/other/02-contracts-interfaces-integration-points-risks-unknowns.md"   "${ticket_args[@]}"      -p "$(cat <<'PROMPT'
Strategist question #02  (ticket-driven, group: other)

Reference: other
Category: contracts/interfaces
Horizon: Immediate
ROI: 4.6 (impact=6, confidence=0.78, effort=1)

Question:
Using the attached tickets as the primary context, identify external integrations implied by the tickets; required config/contract changes; failure/timeout behavior; minimal compat-safe rollout.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.

Output section: Risks/unknowns
Start with heading: Risks/unknowns
Return only: Risks/unknowns (bullets)
PROMPT
)"

oracle   --files-report   --write-output "docs/oracle-questions-2026-01-09-030000/other/02-contracts-interfaces-integration-points-next-experiment.md"   "${ticket_args[@]}"      -p "$(cat <<'PROMPT'
Strategist question #02  (ticket-driven, group: other)

Reference: other
Category: contracts/interfaces
Horizon: Immediate
ROI: 4.6 (impact=6, confidence=0.78, effort=1)

Question:
Using the attached tickets as the primary context, identify external integrations implied by the tickets; required config/contract changes; failure/timeout behavior; minimal compat-safe rollout.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.

Output section: Next smallest concrete experiment
Start with heading: Next smallest concrete experiment
Return only: Next smallest concrete experiment (exactly one action)
PROMPT
)"

oracle   --files-report   --write-output "docs/oracle-questions-2026-01-09-030000/other/02-contracts-interfaces-integration-points-missing-evidence.md"   "${ticket_args[@]}"      -p "$(cat <<'PROMPT'
Strategist question #02  (ticket-driven, group: other)

Reference: other
Category: contracts/interfaces
Horizon: Immediate
ROI: 4.6 (impact=6, confidence=0.78, effort=1)

Question:
Using the attached tickets as the primary context, identify external integrations implied by the tickets; required config/contract changes; failure/timeout behavior; minimal compat-safe rollout.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.

Output section: Missing evidence
Start with heading: Missing evidence
Return only: If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"


# 03) ROI=5.1 impact=7 confidence=0.74 effort=1 horizon=NearTerm category=invariants reference=other

# tickets attached directly (deterministic; self-contained)
mapfile -t __tickets < <(python3 - <<'PY'
from __future__ import annotations
from pathlib import Path
import fnmatch

TICKET_ROOT = ".tickets"
TICKET_GLOB = "**/*.md"
TICKET_PATHS = ".tickets/other/Oraclepack Pipeline Improvements.md,.tickets/other/Oraclepack Prompt Generator.md,.tickets/other/Oraclepack Workflow Enhancement.md,.tickets/other/Verbose Payload Rendering TUI.md".strip()
MAX = int("4")


root = Path(TICKET_ROOT)

def read_gitignore(start: Path):
    cur = start.resolve()
    if cur.is_file():
        cur = cur.parent
    while True:
        p = cur / ".gitignore"
        if p.exists():
            lines = []
            for ln in p.read_text(encoding="utf-8", errors="replace").splitlines():
                s = ln.strip()
                if not s or s.startswith("#"):
                    continue
                lines.append(s)
            return cur, lines
        if cur.parent == cur:
            return start.resolve(), []
        cur = cur.parent


def gitignore_match(rel_posix: str, name: str, pattern: str) -> bool:
    neg = pattern.startswith("!")
    if neg:
        pattern = pattern[1:]
    if not pattern:
        return False

    anchored = pattern.startswith("/")
    if anchored:
        pattern = pattern.lstrip("/")

    dir_only = pattern.endswith("/")
    if dir_only:
        pattern = pattern.rstrip("/")

    if "/" not in pattern:
        if dir_only:
            return any(part == pattern for part in rel_posix.split("/"))
        return fnmatch.fnmatch(name, pattern)

    target = rel_posix
    if anchored:
        if dir_only:
            return target.startswith(pattern + "/")
        return fnmatch.fnmatch(target, pattern)
    if dir_only:
        return f"/{pattern}/" in f"/{target}/"
    return fnmatch.fnmatch(target, f"**/{pattern}") or fnmatch.fnmatch(target, pattern)


def is_gitignored(path: Path, git_root: Path, patterns: list[str]) -> bool:
    try:
        rel = path.resolve().relative_to(git_root)
    except Exception:
        rel = path
    rel_posix = rel.as_posix()
    name = rel_posix.split("/")[-1]
    ignored = False
    for pat in patterns:
        neg = pat.startswith("!")
        if gitignore_match(rel_posix, name, pat):
            ignored = not neg
    return ignored


git_root, git_patterns = read_gitignore(root)
def lex_sorted(ps):
    return sorted((str(p) for p in ps), key=lambda s: s)

if TICKET_PATHS:
    tickets = [Path(p.strip()) for p in TICKET_PATHS.split(",") if p.strip()]
else:
    tickets = list(root.glob(TICKET_GLOB)) if root.exists() else []

tickets = [Path(p) for p in lex_sorted(tickets)]
tickets = [p for p in tickets if not is_gitignored(p, git_root, git_patterns)]
if MAX and MAX > 0:
    tickets = tickets[:MAX]

for p in tickets:
    print(str(p))
PY
)

ticket_args=()
for p in "${__tickets[@]}"; do
  ticket_args+=(-f "$p")
done

if [ "${#ticket_args[@]}" -eq 0 ]; then
  echo "WARNING: no tickets resolved for group 'other'." >&2
fi

# extra_files appended literally (may be empty; may include -f/--file):
oracle   --files-report   --write-output "docs/oracle-questions-2026-01-09-030000/other/03-invariants-invariant-map-direct-answer.md"   "${ticket_args[@]}"      -p "$(cat <<'PROMPT'
Strategist question #03  (ticket-driven, group: other)

Reference: other
Category: invariants
Horizon: NearTerm
ROI: 5.1 (impact=7, confidence=0.74, effort=1)

Question:
Using the attached tickets as the primary context, extract system invariants implied by tickets (inputs/outputs, pack schema rules, step execution rules) and where to enforce them.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.

Output section: Direct answer
Start with heading: Direct answer
Return only: Direct answer (1–10 bullets, evidence-cited)
PROMPT
)"

oracle   --files-report   --write-output "docs/oracle-questions-2026-01-09-030000/other/03-invariants-invariant-map-risks-unknowns.md"   "${ticket_args[@]}"      -p "$(cat <<'PROMPT'
Strategist question #03  (ticket-driven, group: other)

Reference: other
Category: invariants
Horizon: NearTerm
ROI: 5.1 (impact=7, confidence=0.74, effort=1)

Question:
Using the attached tickets as the primary context, extract system invariants implied by tickets (inputs/outputs, pack schema rules, step execution rules) and where to enforce them.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.

Output section: Risks/unknowns
Start with heading: Risks/unknowns
Return only: Risks/unknowns (bullets)
PROMPT
)"

oracle   --files-report   --write-output "docs/oracle-questions-2026-01-09-030000/other/03-invariants-invariant-map-next-experiment.md"   "${ticket_args[@]}"      -p "$(cat <<'PROMPT'
Strategist question #03  (ticket-driven, group: other)

Reference: other
Category: invariants
Horizon: NearTerm
ROI: 5.1 (impact=7, confidence=0.74, effort=1)

Question:
Using the attached tickets as the primary context, extract system invariants implied by tickets (inputs/outputs, pack schema rules, step execution rules) and where to enforce them.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.

Output section: Next smallest concrete experiment
Start with heading: Next smallest concrete experiment
Return only: Next smallest concrete experiment (exactly one action)
PROMPT
)"

oracle   --files-report   --write-output "docs/oracle-questions-2026-01-09-030000/other/03-invariants-invariant-map-missing-evidence.md"   "${ticket_args[@]}"      -p "$(cat <<'PROMPT'
Strategist question #03  (ticket-driven, group: other)

Reference: other
Category: invariants
Horizon: NearTerm
ROI: 5.1 (impact=7, confidence=0.74, effort=1)

Question:
Using the attached tickets as the primary context, extract system invariants implied by tickets (inputs/outputs, pack schema rules, step execution rules) and where to enforce them.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.

Output section: Missing evidence
Start with heading: Missing evidence
Return only: If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"


# 04) ROI=5.0 impact=7 confidence=0.72 effort=2 horizon=NearTerm category=invariants reference=other

# tickets attached directly (deterministic; self-contained)
mapfile -t __tickets < <(python3 - <<'PY'
from __future__ import annotations
from pathlib import Path
import fnmatch

TICKET_ROOT = ".tickets"
TICKET_GLOB = "**/*.md"
TICKET_PATHS = ".tickets/other/Oraclepack Pipeline Improvements.md,.tickets/other/Oraclepack Prompt Generator.md,.tickets/other/Oraclepack Workflow Enhancement.md,.tickets/other/Verbose Payload Rendering TUI.md".strip()
MAX = int("4")


root = Path(TICKET_ROOT)

def read_gitignore(start: Path):
    cur = start.resolve()
    if cur.is_file():
        cur = cur.parent
    while True:
        p = cur / ".gitignore"
        if p.exists():
            lines = []
            for ln in p.read_text(encoding="utf-8", errors="replace").splitlines():
                s = ln.strip()
                if not s or s.startswith("#"):
                    continue
                lines.append(s)
            return cur, lines
        if cur.parent == cur:
            return start.resolve(), []
        cur = cur.parent


def gitignore_match(rel_posix: str, name: str, pattern: str) -> bool:
    neg = pattern.startswith("!")
    if neg:
        pattern = pattern[1:]
    if not pattern:
        return False

    anchored = pattern.startswith("/")
    if anchored:
        pattern = pattern.lstrip("/")

    dir_only = pattern.endswith("/")
    if dir_only:
        pattern = pattern.rstrip("/")

    if "/" not in pattern:
        if dir_only:
            return any(part == pattern for part in rel_posix.split("/"))
        return fnmatch.fnmatch(name, pattern)

    target = rel_posix
    if anchored:
        if dir_only:
            return target.startswith(pattern + "/")
        return fnmatch.fnmatch(target, pattern)
    if dir_only:
        return f"/{pattern}/" in f"/{target}/"
    return fnmatch.fnmatch(target, f"**/{pattern}") or fnmatch.fnmatch(target, pattern)


def is_gitignored(path: Path, git_root: Path, patterns: list[str]) -> bool:
    try:
        rel = path.resolve().relative_to(git_root)
    except Exception:
        rel = path
    rel_posix = rel.as_posix()
    name = rel_posix.split("/")[-1]
    ignored = False
    for pat in patterns:
        neg = pat.startswith("!")
        if gitignore_match(rel_posix, name, pat):
            ignored = not neg
    return ignored


git_root, git_patterns = read_gitignore(root)
def lex_sorted(ps):
    return sorted((str(p) for p in ps), key=lambda s: s)

if TICKET_PATHS:
    tickets = [Path(p.strip()) for p in TICKET_PATHS.split(",") if p.strip()]
else:
    tickets = list(root.glob(TICKET_GLOB)) if root.exists() else []

tickets = [Path(p) for p in lex_sorted(tickets)]
tickets = [p for p in tickets if not is_gitignored(p, git_root, git_patterns)]
if MAX and MAX > 0:
    tickets = tickets[:MAX]

for p in tickets:
    print(str(p))
PY
)

ticket_args=()
for p in "${__tickets[@]}"; do
  ticket_args+=(-f "$p")
done

if [ "${#ticket_args[@]}" -eq 0 ]; then
  echo "WARNING: no tickets resolved for group 'other'." >&2
fi

# extra_files appended literally (may be empty; may include -f/--file):
oracle   --files-report   --write-output "docs/oracle-questions-2026-01-09-030000/other/04-invariants-validation-boundaries-direct-answer.md"   "${ticket_args[@]}"      -p "$(cat <<'PROMPT'
Strategist question #04  (ticket-driven, group: other)

Reference: other
Category: invariants
Horizon: NearTerm
ROI: 5.0 (impact=7, confidence=0.72, effort=2)

Question:
Using the attached tickets as the primary context, identify validation boundaries that must exist (ticket parsing, pack generation, pack validation); propose minimal validation plan.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–10 bullets, evidence-cited)
[TRUNCATED]
```

docs/oracle-questions-2026-01-09-030000/packs/prd-tui.md
```
# Oracle Pack — Unknown (Grouped Tickets Stage 1 — Direct Attach)

## Parsed args
- codebase_name: Unknown
- out_dir: docs/oracle-questions-2026-01-09-030000/prd-tui
- oracle_cmd: oracle
- oracle_flags: --files-report
- extra_files: 
- ticket_root: .tickets
- ticket_glob: **/*.md
- ticket_paths: .tickets/PRD-TUI/Oraclepack TUI Integration.md,.tickets/PRD-TUI/PRD-generator URL routing.md
- ticket_max_files: 2
- group_name: PRD-TUI
- group_slug: prd-tui
- mode: tickets-grouped-direct

Notes (contract):
- Exactly one fenced `bash` block in this document.
- No other ``` fences anywhere.
- Exactly 20 steps, numbered 01..20 in order.
- Step header: `# NN) ROI=... impact=... confidence=... effort=... horizon=... category=... reference=...`
- Every step includes: `--write-output "docs/oracle-questions-2026-01-09-030000/prd-tui/NN-<slug>.md"` (double quotes required).
- Steps must be self-contained and must not rely on shell variables created in previous steps.
- Each step must attach tickets directly (no `_tickets_bundle.md` dependency).
- Pack ends with a Coverage check section listing all 10 categories as OK or Missing(<step ids>).

```bash
set -euo pipefail

mkdir -p "docs/oracle-questions-2026-01-09-030000/prd-tui"

# 01) ROI=4.8 impact=6 confidence=0.80 effort=1 horizon=Immediate category=contracts/interfaces reference=prd-tui

# tickets attached directly (deterministic; self-contained)
mapfile -t __tickets < <(python3 - <<'PY'
from __future__ import annotations
from pathlib import Path
import fnmatch

TICKET_ROOT = ".tickets"
TICKET_GLOB = "**/*.md"
TICKET_PATHS = ".tickets/PRD-TUI/Oraclepack TUI Integration.md,.tickets/PRD-TUI/PRD-generator URL routing.md".strip()
MAX = int("2")


root = Path(TICKET_ROOT)

def read_gitignore(start: Path):
    cur = start.resolve()
    if cur.is_file():
        cur = cur.parent
    while True:
        p = cur / ".gitignore"
        if p.exists():
            lines = []
            for ln in p.read_text(encoding="utf-8", errors="replace").splitlines():
                s = ln.strip()
                if not s or s.startswith("#"):
                    continue
                lines.append(s)
            return cur, lines
        if cur.parent == cur:
            return start.resolve(), []
        cur = cur.parent


def gitignore_match(rel_posix: str, name: str, pattern: str) -> bool:
    neg = pattern.startswith("!")
    if neg:
        pattern = pattern[1:]
    if not pattern:
        return False

    anchored = pattern.startswith("/")
    if anchored:
        pattern = pattern.lstrip("/")

    dir_only = pattern.endswith("/")
    if dir_only:
        pattern = pattern.rstrip("/")

    if "/" not in pattern:
        if dir_only:
            return any(part == pattern for part in rel_posix.split("/"))
        return fnmatch.fnmatch(name, pattern)

    target = rel_posix
    if anchored:
        if dir_only:
            return target.startswith(pattern + "/")
        return fnmatch.fnmatch(target, pattern)
    if dir_only:
        return f"/{pattern}/" in f"/{target}/"
    return fnmatch.fnmatch(target, f"**/{pattern}") or fnmatch.fnmatch(target, pattern)


def is_gitignored(path: Path, git_root: Path, patterns: list[str]) -> bool:
    try:
        rel = path.resolve().relative_to(git_root)
    except Exception:
        rel = path
    rel_posix = rel.as_posix()
    name = rel_posix.split("/")[-1]
    ignored = False
    for pat in patterns:
        neg = pat.startswith("!")
        if gitignore_match(rel_posix, name, pat):
            ignored = not neg
    return ignored


git_root, git_patterns = read_gitignore(root)
def lex_sorted(ps):
    return sorted((str(p) for p in ps), key=lambda s: s)

if TICKET_PATHS:
    tickets = [Path(p.strip()) for p in TICKET_PATHS.split(",") if p.strip()]
else:
    tickets = list(root.glob(TICKET_GLOB)) if root.exists() else []

tickets = [Path(p) for p in lex_sorted(tickets)]
tickets = [p for p in tickets if not is_gitignored(p, git_root, git_patterns)]
if MAX and MAX > 0:
    tickets = tickets[:MAX]

for p in tickets:
    print(str(p))
PY
)

ticket_args=()
for p in "${__tickets[@]}"; do
  ticket_args+=(-f "$p")
done

if [ "${#ticket_args[@]}" -eq 0 ]; then
  echo "WARNING: no tickets resolved for group 'PRD-TUI'." >&2
fi

# extra_files appended literally (may be empty; may include -f/--file):
oracle   --files-report   --write-output "docs/oracle-questions-2026-01-09-030000/prd-tui/01-contracts-interfaces-ticket-surface-direct-answer.md"   "${ticket_args[@]}"      -p "$(cat <<'PROMPT'
Strategist question #01  (ticket-driven, group: PRD-TUI)

Reference: prd-tui
Category: contracts/interfaces
Horizon: Immediate
ROI: 4.8 (impact=6, confidence=0.80, effort=1)

Question:
Using the attached tickets as the primary context, list the public surface changes implied by the tickets (CLI/TUI/API/interfaces/contracts); call out backwards-compat constraints.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.

Output section: Direct answer
Start with heading: Direct answer
Return only: Direct answer (1–10 bullets, evidence-cited)
PROMPT
)"

oracle   --files-report   --write-output "docs/oracle-questions-2026-01-09-030000/prd-tui/01-contracts-interfaces-ticket-surface-risks-unknowns.md"   "${ticket_args[@]}"      -p "$(cat <<'PROMPT'
Strategist question #01  (ticket-driven, group: PRD-TUI)

Reference: prd-tui
Category: contracts/interfaces
Horizon: Immediate
ROI: 4.8 (impact=6, confidence=0.80, effort=1)

Question:
Using the attached tickets as the primary context, list the public surface changes implied by the tickets (CLI/TUI/API/interfaces/contracts); call out backwards-compat constraints.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.

Output section: Risks/unknowns
Start with heading: Risks/unknowns
Return only: Risks/unknowns (bullets)
PROMPT
)"

oracle   --files-report   --write-output "docs/oracle-questions-2026-01-09-030000/prd-tui/01-contracts-interfaces-ticket-surface-next-experiment.md"   "${ticket_args[@]}"      -p "$(cat <<'PROMPT'
Strategist question #01  (ticket-driven, group: PRD-TUI)

Reference: prd-tui
Category: contracts/interfaces
Horizon: Immediate
ROI: 4.8 (impact=6, confidence=0.80, effort=1)

Question:
Using the attached tickets as the primary context, list the public surface changes implied by the tickets (CLI/TUI/API/interfaces/contracts); call out backwards-compat constraints.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.

Output section: Next smallest concrete experiment
Start with heading: Next smallest concrete experiment
Return only: Next smallest concrete experiment (exactly one action)
PROMPT
)"

oracle   --files-report   --write-output "docs/oracle-questions-2026-01-09-030000/prd-tui/01-contracts-interfaces-ticket-surface-missing-evidence.md"   "${ticket_args[@]}"      -p "$(cat <<'PROMPT'
Strategist question #01  (ticket-driven, group: PRD-TUI)

Reference: prd-tui
Category: contracts/interfaces
Horizon: Immediate
ROI: 4.8 (impact=6, confidence=0.80, effort=1)

Question:
Using the attached tickets as the primary context, list the public surface changes implied by the tickets (CLI/TUI/API/interfaces/contracts); call out backwards-compat constraints.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.

Output section: Missing evidence
Start with heading: Missing evidence
Return only: If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"


# 02) ROI=4.6 impact=6 confidence=0.78 effort=1 horizon=Immediate category=contracts/interfaces reference=prd-tui

# tickets attached directly (deterministic; self-contained)
mapfile -t __tickets < <(python3 - <<'PY'
from __future__ import annotations
from pathlib import Path
import fnmatch

TICKET_ROOT = ".tickets"
TICKET_GLOB = "**/*.md"
TICKET_PATHS = ".tickets/PRD-TUI/Oraclepack TUI Integration.md,.tickets/PRD-TUI/PRD-generator URL routing.md".strip()
MAX = int("2")


root = Path(TICKET_ROOT)

def read_gitignore(start: Path):
    cur = start.resolve()
    if cur.is_file():
        cur = cur.parent
    while True:
        p = cur / ".gitignore"
        if p.exists():
            lines = []
            for ln in p.read_text(encoding="utf-8", errors="replace").splitlines():
                s = ln.strip()
                if not s or s.startswith("#"):
                    continue
                lines.append(s)
            return cur, lines
        if cur.parent == cur:
            return start.resolve(), []
        cur = cur.parent


def gitignore_match(rel_posix: str, name: str, pattern: str) -> bool:
    neg = pattern.startswith("!")
    if neg:
        pattern = pattern[1:]
    if not pattern:
        return False

    anchored = pattern.startswith("/")
    if anchored:
        pattern = pattern.lstrip("/")

    dir_only = pattern.endswith("/")
    if dir_only:
        pattern = pattern.rstrip("/")

    if "/" not in pattern:
        if dir_only:
            return any(part == pattern for part in rel_posix.split("/"))
        return fnmatch.fnmatch(name, pattern)

    target = rel_posix
    if anchored:
        if dir_only:
            return target.startswith(pattern + "/")
        return fnmatch.fnmatch(target, pattern)
    if dir_only:
        return f"/{pattern}/" in f"/{target}/"
    return fnmatch.fnmatch(target, f"**/{pattern}") or fnmatch.fnmatch(target, pattern)


def is_gitignored(path: Path, git_root: Path, patterns: list[str]) -> bool:
    try:
        rel = path.resolve().relative_to(git_root)
    except Exception:
        rel = path
    rel_posix = rel.as_posix()
    name = rel_posix.split("/")[-1]
    ignored = False
    for pat in patterns:
        neg = pat.startswith("!")
        if gitignore_match(rel_posix, name, pat):
            ignored = not neg
    return ignored


git_root, git_patterns = read_gitignore(root)
def lex_sorted(ps):
    return sorted((str(p) for p in ps), key=lambda s: s)

if TICKET_PATHS:
    tickets = [Path(p.strip()) for p in TICKET_PATHS.split(",") if p.strip()]
else:
    tickets = list(root.glob(TICKET_GLOB)) if root.exists() else []

tickets = [Path(p) for p in lex_sorted(tickets)]
tickets = [p for p in tickets if not is_gitignored(p, git_root, git_patterns)]
if MAX and MAX > 0:
    tickets = tickets[:MAX]

for p in tickets:
    print(str(p))
PY
)

ticket_args=()
for p in "${__tickets[@]}"; do
  ticket_args+=(-f "$p")
done

if [ "${#ticket_args[@]}" -eq 0 ]; then
  echo "WARNING: no tickets resolved for group 'PRD-TUI'." >&2
fi

# extra_files appended literally (may be empty; may include -f/--file):
oracle   --files-report   --write-output "docs/oracle-questions-2026-01-09-030000/prd-tui/02-contracts-interfaces-integration-points-direct-answer.md"   "${ticket_args[@]}"      -p "$(cat <<'PROMPT'
Strategist question #02  (ticket-driven, group: PRD-TUI)

Reference: prd-tui
Category: contracts/interfaces
Horizon: Immediate
ROI: 4.6 (impact=6, confidence=0.78, effort=1)

Question:
Using the attached tickets as the primary context, identify external integrations implied by the tickets; required config/contract changes; failure/timeout behavior; minimal compat-safe rollout.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.

Output section: Direct answer
Start with heading: Direct answer
Return only: Direct answer (1–10 bullets, evidence-cited)
PROMPT
)"

oracle   --files-report   --write-output "docs/oracle-questions-2026-01-09-030000/prd-tui/02-contracts-interfaces-integration-points-risks-unknowns.md"   "${ticket_args[@]}"      -p "$(cat <<'PROMPT'
Strategist question #02  (ticket-driven, group: PRD-TUI)

Reference: prd-tui
Category: contracts/interfaces
Horizon: Immediate
ROI: 4.6 (impact=6, confidence=0.78, effort=1)

Question:
Using the attached tickets as the primary context, identify external integrations implied by the tickets; required config/contract changes; failure/timeout behavior; minimal compat-safe rollout.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.

Output section: Risks/unknowns
Start with heading: Risks/unknowns
Return only: Risks/unknowns (bullets)
PROMPT
)"

oracle   --files-report   --write-output "docs/oracle-questions-2026-01-09-030000/prd-tui/02-contracts-interfaces-integration-points-next-experiment.md"   "${ticket_args[@]}"      -p "$(cat <<'PROMPT'
Strategist question #02  (ticket-driven, group: PRD-TUI)

Reference: prd-tui
Category: contracts/interfaces
Horizon: Immediate
ROI: 4.6 (impact=6, confidence=0.78, effort=1)

Question:
Using the attached tickets as the primary context, identify external integrations implied by the tickets; required config/contract changes; failure/timeout behavior; minimal compat-safe rollout.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.

Output section: Next smallest concrete experiment
Start with heading: Next smallest concrete experiment
Return only: Next smallest concrete experiment (exactly one action)
PROMPT
)"

oracle   --files-report   --write-output "docs/oracle-questions-2026-01-09-030000/prd-tui/02-contracts-interfaces-integration-points-missing-evidence.md"   "${ticket_args[@]}"      -p "$(cat <<'PROMPT'
Strategist question #02  (ticket-driven, group: PRD-TUI)

Reference: prd-tui
Category: contracts/interfaces
Horizon: Immediate
ROI: 4.6 (impact=6, confidence=0.78, effort=1)

Question:
Using the attached tickets as the primary context, identify external integrations implied by the tickets; required config/contract changes; failure/timeout behavior; minimal compat-safe rollout.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.

Output section: Missing evidence
Start with heading: Missing evidence
Return only: If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"


# 03) ROI=5.1 impact=7 confidence=0.74 effort=1 horizon=NearTerm category=invariants reference=prd-tui

# tickets attached directly (deterministic; self-contained)
mapfile -t __tickets < <(python3 - <<'PY'
from __future__ import annotations
from pathlib import Path
import fnmatch

TICKET_ROOT = ".tickets"
TICKET_GLOB = "**/*.md"
TICKET_PATHS = ".tickets/PRD-TUI/Oraclepack TUI Integration.md,.tickets/PRD-TUI/PRD-generator URL routing.md".strip()
MAX = int("2")


root = Path(TICKET_ROOT)

def read_gitignore(start: Path):
    cur = start.resolve()
    if cur.is_file():
        cur = cur.parent
    while True:
        p = cur / ".gitignore"
        if p.exists():
            lines = []
            for ln in p.read_text(encoding="utf-8", errors="replace").splitlines():
                s = ln.strip()
                if not s or s.startswith("#"):
                    continue
                lines.append(s)
            return cur, lines
        if cur.parent == cur:
            return start.resolve(), []
        cur = cur.parent


def gitignore_match(rel_posix: str, name: str, pattern: str) -> bool:
    neg = pattern.startswith("!")
    if neg:
        pattern = pattern[1:]
    if not pattern:
        return False

    anchored = pattern.startswith("/")
    if anchored:
        pattern = pattern.lstrip("/")

    dir_only = pattern.endswith("/")
    if dir_only:
        pattern = pattern.rstrip("/")

    if "/" not in pattern:
        if dir_only:
            return any(part == pattern for part in rel_posix.split("/"))
        return fnmatch.fnmatch(name, pattern)

    target = rel_posix
    if anchored:
        if dir_only:
            return target.startswith(pattern + "/")
        return fnmatch.fnmatch(target, pattern)
    if dir_only:
        return f"/{pattern}/" in f"/{target}/"
    return fnmatch.fnmatch(target, f"**/{pattern}") or fnmatch.fnmatch(target, pattern)


def is_gitignored(path: Path, git_root: Path, patterns: list[str]) -> bool:
    try:
        rel = path.resolve().relative_to(git_root)
    except Exception:
        rel = path
    rel_posix = rel.as_posix()
    name = rel_posix.split("/")[-1]
    ignored = False
    for pat in patterns:
        neg = pat.startswith("!")
        if gitignore_match(rel_posix, name, pat):
            ignored = not neg
    return ignored


git_root, git_patterns = read_gitignore(root)
def lex_sorted(ps):
    return sorted((str(p) for p in ps), key=lambda s: s)

if TICKET_PATHS:
    tickets = [Path(p.strip()) for p in TICKET_PATHS.split(",") if p.strip()]
else:
    tickets = list(root.glob(TICKET_GLOB)) if root.exists() else []

tickets = [Path(p) for p in lex_sorted(tickets)]
tickets = [p for p in tickets if not is_gitignored(p, git_root, git_patterns)]
if MAX and MAX > 0:
    tickets = tickets[:MAX]

for p in tickets:
    print(str(p))
PY
)

ticket_args=()
for p in "${__tickets[@]}"; do
  ticket_args+=(-f "$p")
done

if [ "${#ticket_args[@]}" -eq 0 ]; then
  echo "WARNING: no tickets resolved for group 'PRD-TUI'." >&2
fi

# extra_files appended literally (may be empty; may include -f/--file):
oracle   --files-report   --write-output "docs/oracle-questions-2026-01-09-030000/prd-tui/03-invariants-invariant-map-direct-answer.md"   "${ticket_args[@]}"      -p "$(cat <<'PROMPT'
Strategist question #03  (ticket-driven, group: PRD-TUI)

Reference: prd-tui
Category: invariants
Horizon: NearTerm
ROI: 5.1 (impact=7, confidence=0.74, effort=1)

Question:
Using the attached tickets as the primary context, extract system invariants implied by tickets (inputs/outputs, pack schema rules, step execution rules) and where to enforce them.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.

Output section: Direct answer
Start with heading: Direct answer
Return only: Direct answer (1–10 bullets, evidence-cited)
PROMPT
)"

oracle   --files-report   --write-output "docs/oracle-questions-2026-01-09-030000/prd-tui/03-invariants-invariant-map-risks-unknowns.md"   "${ticket_args[@]}"      -p "$(cat <<'PROMPT'
Strategist question #03  (ticket-driven, group: PRD-TUI)

Reference: prd-tui
Category: invariants
Horizon: NearTerm
ROI: 5.1 (impact=7, confidence=0.74, effort=1)

Question:
Using the attached tickets as the primary context, extract system invariants implied by tickets (inputs/outputs, pack schema rules, step execution rules) and where to enforce them.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.

Output section: Risks/unknowns
Start with heading: Risks/unknowns
Return only: Risks/unknowns (bullets)
PROMPT
)"

oracle   --files-report   --write-output "docs/oracle-questions-2026-01-09-030000/prd-tui/03-invariants-invariant-map-next-experiment.md"   "${ticket_args[@]}"      -p "$(cat <<'PROMPT'
Strategist question #03  (ticket-driven, group: PRD-TUI)

Reference: prd-tui
Category: invariants
Horizon: NearTerm
ROI: 5.1 (impact=7, confidence=0.74, effort=1)

Question:
Using the attached tickets as the primary context, extract system invariants implied by tickets (inputs/outputs, pack schema rules, step execution rules) and where to enforce them.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.

Output section: Next smallest concrete experiment
Start with heading: Next smallest concrete experiment
Return only: Next smallest concrete experiment (exactly one action)
PROMPT
)"

oracle   --files-report   --write-output "docs/oracle-questions-2026-01-09-030000/prd-tui/03-invariants-invariant-map-missing-evidence.md"   "${ticket_args[@]}"      -p "$(cat <<'PROMPT'
Strategist question #03  (ticket-driven, group: PRD-TUI)

Reference: prd-tui
Category: invariants
Horizon: NearTerm
ROI: 5.1 (impact=7, confidence=0.74, effort=1)

Question:
Using the attached tickets as the primary context, extract system invariants implied by tickets (inputs/outputs, pack schema rules, step execution rules) and where to enforce them.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.

Output section: Missing evidence
Start with heading: Missing evidence
Return only: If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"


# 04) ROI=5.0 impact=7 confidence=0.72 effort=2 horizon=NearTerm category=invariants reference=prd-tui

# tickets attached directly (deterministic; self-contained)
mapfile -t __tickets < <(python3 - <<'PY'
from __future__ import annotations
from pathlib import Path
import fnmatch

TICKET_ROOT = ".tickets"
TICKET_GLOB = "**/*.md"
TICKET_PATHS = ".tickets/PRD-TUI/Oraclepack TUI Integration.md,.tickets/PRD-TUI/PRD-generator URL routing.md".strip()
MAX = int("2")


root = Path(TICKET_ROOT)

def read_gitignore(start: Path):
    cur = start.resolve()
    if cur.is_file():
        cur = cur.parent
    while True:
        p = cur / ".gitignore"
        if p.exists():
            lines = []
            for ln in p.read_text(encoding="utf-8", errors="replace").splitlines():
                s = ln.strip()
                if not s or s.startswith("#"):
                    continue
                lines.append(s)
            return cur, lines
        if cur.parent == cur:
            return start.resolve(), []
        cur = cur.parent


def gitignore_match(rel_posix: str, name: str, pattern: str) -> bool:
    neg = pattern.startswith("!")
    if neg:
        pattern = pattern[1:]
    if not pattern:
        return False

    anchored = pattern.startswith("/")
    if anchored:
        pattern = pattern.lstrip("/")

    dir_only = pattern.endswith("/")
    if dir_only:
        pattern = pattern.rstrip("/")

    if "/" not in pattern:
        if dir_only:
            return any(part == pattern for part in rel_posix.split("/"))
        return fnmatch.fnmatch(name, pattern)

    target = rel_posix
    if anchored:
        if dir_only:
            return target.startswith(pattern + "/")
        return fnmatch.fnmatch(target, pattern)
    if dir_only:
        return f"/{pattern}/" in f"/{target}/"
    return fnmatch.fnmatch(target, f"**/{pattern}") or fnmatch.fnmatch(target, pattern)


def is_gitignored(path: Path, git_root: Path, patterns: list[str]) -> bool:
    try:
        rel = path.resolve().relative_to(git_root)
    except Exception:
        rel = path
    rel_posix = rel.as_posix()
    name = rel_posix.split("/")[-1]
    ignored = False
    for pat in patterns:
        neg = pat.startswith("!")
        if gitignore_match(rel_posix, name, pat):
            ignored = not neg
    return ignored


git_root, git_patterns = read_gitignore(root)
def lex_sorted(ps):
    return sorted((str(p) for p in ps), key=lambda s: s)

if TICKET_PATHS:
    tickets = [Path(p.strip()) for p in TICKET_PATHS.split(",") if p.strip()]
else:
    tickets = list(root.glob(TICKET_GLOB)) if root.exists() else []

tickets = [Path(p) for p in lex_sorted(tickets)]
tickets = [p for p in tickets if not is_gitignored(p, git_root, git_patterns)]
if MAX and MAX > 0:
    tickets = tickets[:MAX]

for p in tickets:
    print(str(p))
PY
)

ticket_args=()
for p in "${__tickets[@]}"; do
  ticket_args+=(-f "$p")
done

if [ "${#ticket_args[@]}" -eq 0 ]; then
  echo "WARNING: no tickets resolved for group 'PRD-TUI'." >&2
fi

# extra_files appended literally (may be empty; may include -f/--file):
oracle   --files-report   --write-output "docs/oracle-questions-2026-01-09-030000/prd-tui/04-invariants-validation-boundaries-direct-answer.md"   "${ticket_args[@]}"      -p "$(cat <<'PROMPT'
Strategist question #04  (ticket-driven, group: PRD-TUI)

Reference: prd-tui
Category: invariants
Horizon: NearTerm
ROI: 5.0 (impact=7, confidence=0.72, effort=2)

Question:
Using the attached tickets as the primary context, identify validation boundaries that must exist (ticket parsing, pack generation, pack validation); propose minimal validation plan.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–10 bullets, evidence-cited)
[TRUNCATED]
```

docs/oracle-questions-2026-01-09-030000/packs/raw-exports.md
```
# Oracle Pack — Unknown (Grouped Tickets Stage 1 — Direct Attach)

## Parsed args
- codebase_name: Unknown
- out_dir: docs/oracle-questions-2026-01-09-030000/raw-exports
- oracle_cmd: oracle
- oracle_flags: --files-report
- extra_files: 
- ticket_root: .tickets
- ticket_glob: **/*.md
- ticket_paths: .tickets/raw-exports/Output verification failure.md
- ticket_max_files: 1
- group_name: raw-exports
- group_slug: raw-exports
- mode: tickets-grouped-direct

Notes (contract):
- Exactly one fenced `bash` block in this document.
- No other ``` fences anywhere.
- Exactly 20 steps, numbered 01..20 in order.
- Step header: `# NN) ROI=... impact=... confidence=... effort=... horizon=... category=... reference=...`
- Every step includes: `--write-output "docs/oracle-questions-2026-01-09-030000/raw-exports/NN-<slug>.md"` (double quotes required).
- Steps must be self-contained and must not rely on shell variables created in previous steps.
- Each step must attach tickets directly (no `_tickets_bundle.md` dependency).
- Pack ends with a Coverage check section listing all 10 categories as OK or Missing(<step ids>).

```bash
set -euo pipefail

mkdir -p "docs/oracle-questions-2026-01-09-030000/raw-exports"

# 01) ROI=4.8 impact=6 confidence=0.80 effort=1 horizon=Immediate category=contracts/interfaces reference=raw-exports

# tickets attached directly (deterministic; self-contained)
mapfile -t __tickets < <(python3 - <<'PY'
from __future__ import annotations
from pathlib import Path
import fnmatch

TICKET_ROOT = ".tickets"
TICKET_GLOB = "**/*.md"
TICKET_PATHS = ".tickets/raw-exports/Output verification failure.md".strip()
MAX = int("1")


root = Path(TICKET_ROOT)

def read_gitignore(start: Path):
    cur = start.resolve()
    if cur.is_file():
        cur = cur.parent
    while True:
        p = cur / ".gitignore"
        if p.exists():
            lines = []
            for ln in p.read_text(encoding="utf-8", errors="replace").splitlines():
                s = ln.strip()
                if not s or s.startswith("#"):
                    continue
                lines.append(s)
            return cur, lines
        if cur.parent == cur:
            return start.resolve(), []
        cur = cur.parent


def gitignore_match(rel_posix: str, name: str, pattern: str) -> bool:
    neg = pattern.startswith("!")
    if neg:
        pattern = pattern[1:]
    if not pattern:
        return False

    anchored = pattern.startswith("/")
    if anchored:
        pattern = pattern.lstrip("/")

    dir_only = pattern.endswith("/")
    if dir_only:
        pattern = pattern.rstrip("/")

    if "/" not in pattern:
        if dir_only:
            return any(part == pattern for part in rel_posix.split("/"))
        return fnmatch.fnmatch(name, pattern)

    target = rel_posix
    if anchored:
        if dir_only:
            return target.startswith(pattern + "/")
        return fnmatch.fnmatch(target, pattern)
    if dir_only:
        return f"/{pattern}/" in f"/{target}/"
    return fnmatch.fnmatch(target, f"**/{pattern}") or fnmatch.fnmatch(target, pattern)


def is_gitignored(path: Path, git_root: Path, patterns: list[str]) -> bool:
    try:
        rel = path.resolve().relative_to(git_root)
    except Exception:
        rel = path
    rel_posix = rel.as_posix()
    name = rel_posix.split("/")[-1]
    ignored = False
    for pat in patterns:
        neg = pat.startswith("!")
        if gitignore_match(rel_posix, name, pat):
            ignored = not neg
    return ignored


git_root, git_patterns = read_gitignore(root)
def lex_sorted(ps):
    return sorted((str(p) for p in ps), key=lambda s: s)

if TICKET_PATHS:
    tickets = [Path(p.strip()) for p in TICKET_PATHS.split(",") if p.strip()]
else:
    tickets = list(root.glob(TICKET_GLOB)) if root.exists() else []

tickets = [Path(p) for p in lex_sorted(tickets)]
tickets = [p for p in tickets if not is_gitignored(p, git_root, git_patterns)]
if MAX and MAX > 0:
    tickets = tickets[:MAX]

for p in tickets:
    print(str(p))
PY
)

ticket_args=()
for p in "${__tickets[@]}"; do
  ticket_args+=(-f "$p")
done

if [ "${#ticket_args[@]}" -eq 0 ]; then
  echo "WARNING: no tickets resolved for group 'raw-exports'." >&2
fi

# extra_files appended literally (may be empty; may include -f/--file):
oracle   --files-report   --write-output "docs/oracle-questions-2026-01-09-030000/raw-exports/01-contracts-interfaces-ticket-surface-direct-answer.md"   "${ticket_args[@]}"      -p "$(cat <<'PROMPT'
Strategist question #01  (ticket-driven, group: raw-exports)

Reference: raw-exports
Category: contracts/interfaces
Horizon: Immediate
ROI: 4.8 (impact=6, confidence=0.80, effort=1)

Question:
Using the attached tickets as the primary context, list the public surface changes implied by the tickets (CLI/TUI/API/interfaces/contracts); call out backwards-compat constraints.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.

Output section: Direct answer
Start with heading: Direct answer
Return only: Direct answer (1–10 bullets, evidence-cited)
PROMPT
)"

oracle   --files-report   --write-output "docs/oracle-questions-2026-01-09-030000/raw-exports/01-contracts-interfaces-ticket-surface-risks-unknowns.md"   "${ticket_args[@]}"      -p "$(cat <<'PROMPT'
Strategist question #01  (ticket-driven, group: raw-exports)

Reference: raw-exports
Category: contracts/interfaces
Horizon: Immediate
ROI: 4.8 (impact=6, confidence=0.80, effort=1)

Question:
Using the attached tickets as the primary context, list the public surface changes implied by the tickets (CLI/TUI/API/interfaces/contracts); call out backwards-compat constraints.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.

Output section: Risks/unknowns
Start with heading: Risks/unknowns
Return only: Risks/unknowns (bullets)
PROMPT
)"

oracle   --files-report   --write-output "docs/oracle-questions-2026-01-09-030000/raw-exports/01-contracts-interfaces-ticket-surface-next-experiment.md"   "${ticket_args[@]}"      -p "$(cat <<'PROMPT'
Strategist question #01  (ticket-driven, group: raw-exports)

Reference: raw-exports
Category: contracts/interfaces
Horizon: Immediate
ROI: 4.8 (impact=6, confidence=0.80, effort=1)

Question:
Using the attached tickets as the primary context, list the public surface changes implied by the tickets (CLI/TUI/API/interfaces/contracts); call out backwards-compat constraints.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.

Output section: Next smallest concrete experiment
Start with heading: Next smallest concrete experiment
Return only: Next smallest concrete experiment (exactly one action)
PROMPT
)"

oracle   --files-report   --write-output "docs/oracle-questions-2026-01-09-030000/raw-exports/01-contracts-interfaces-ticket-surface-missing-evidence.md"   "${ticket_args[@]}"      -p "$(cat <<'PROMPT'
Strategist question #01  (ticket-driven, group: raw-exports)

Reference: raw-exports
Category: contracts/interfaces
Horizon: Immediate
ROI: 4.8 (impact=6, confidence=0.80, effort=1)

Question:
Using the attached tickets as the primary context, list the public surface changes implied by the tickets (CLI/TUI/API/interfaces/contracts); call out backwards-compat constraints.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.

Output section: Missing evidence
Start with heading: Missing evidence
Return only: If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"


# 02) ROI=4.6 impact=6 confidence=0.78 effort=1 horizon=Immediate category=contracts/interfaces reference=raw-exports

# tickets attached directly (deterministic; self-contained)
mapfile -t __tickets < <(python3 - <<'PY'
from __future__ import annotations
from pathlib import Path
import fnmatch

TICKET_ROOT = ".tickets"
TICKET_GLOB = "**/*.md"
TICKET_PATHS = ".tickets/raw-exports/Output verification failure.md".strip()
MAX = int("1")


root = Path(TICKET_ROOT)

def read_gitignore(start: Path):
    cur = start.resolve()
    if cur.is_file():
        cur = cur.parent
    while True:
        p = cur / ".gitignore"
        if p.exists():
            lines = []
            for ln in p.read_text(encoding="utf-8", errors="replace").splitlines():
                s = ln.strip()
                if not s or s.startswith("#"):
                    continue
                lines.append(s)
            return cur, lines
        if cur.parent == cur:
            return start.resolve(), []
        cur = cur.parent


def gitignore_match(rel_posix: str, name: str, pattern: str) -> bool:
    neg = pattern.startswith("!")
    if neg:
        pattern = pattern[1:]
    if not pattern:
        return False

    anchored = pattern.startswith("/")
    if anchored:
        pattern = pattern.lstrip("/")

    dir_only = pattern.endswith("/")
    if dir_only:
        pattern = pattern.rstrip("/")

    if "/" not in pattern:
        if dir_only:
            return any(part == pattern for part in rel_posix.split("/"))
        return fnmatch.fnmatch(name, pattern)

    target = rel_posix
    if anchored:
        if dir_only:
            return target.startswith(pattern + "/")
        return fnmatch.fnmatch(target, pattern)
    if dir_only:
        return f"/{pattern}/" in f"/{target}/"
    return fnmatch.fnmatch(target, f"**/{pattern}") or fnmatch.fnmatch(target, pattern)


def is_gitignored(path: Path, git_root: Path, patterns: list[str]) -> bool:
    try:
        rel = path.resolve().relative_to(git_root)
    except Exception:
        rel = path
    rel_posix = rel.as_posix()
    name = rel_posix.split("/")[-1]
    ignored = False
    for pat in patterns:
        neg = pat.startswith("!")
        if gitignore_match(rel_posix, name, pat):
            ignored = not neg
    return ignored


git_root, git_patterns = read_gitignore(root)
def lex_sorted(ps):
    return sorted((str(p) for p in ps), key=lambda s: s)

if TICKET_PATHS:
    tickets = [Path(p.strip()) for p in TICKET_PATHS.split(",") if p.strip()]
else:
    tickets = list(root.glob(TICKET_GLOB)) if root.exists() else []

tickets = [Path(p) for p in lex_sorted(tickets)]
tickets = [p for p in tickets if not is_gitignored(p, git_root, git_patterns)]
if MAX and MAX > 0:
    tickets = tickets[:MAX]

for p in tickets:
    print(str(p))
PY
)

ticket_args=()
for p in "${__tickets[@]}"; do
  ticket_args+=(-f "$p")
done

if [ "${#ticket_args[@]}" -eq 0 ]; then
  echo "WARNING: no tickets resolved for group 'raw-exports'." >&2
fi

# extra_files appended literally (may be empty; may include -f/--file):
oracle   --files-report   --write-output "docs/oracle-questions-2026-01-09-030000/raw-exports/02-contracts-interfaces-integration-points-direct-answer.md"   "${ticket_args[@]}"      -p "$(cat <<'PROMPT'
Strategist question #02  (ticket-driven, group: raw-exports)

Reference: raw-exports
Category: contracts/interfaces
Horizon: Immediate
ROI: 4.6 (impact=6, confidence=0.78, effort=1)

Question:
Using the attached tickets as the primary context, identify external integrations implied by the tickets; required config/contract changes; failure/timeout behavior; minimal compat-safe rollout.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.

Output section: Direct answer
Start with heading: Direct answer
Return only: Direct answer (1–10 bullets, evidence-cited)
PROMPT
)"

oracle   --files-report   --write-output "docs/oracle-questions-2026-01-09-030000/raw-exports/02-contracts-interfaces-integration-points-risks-unknowns.md"   "${ticket_args[@]}"      -p "$(cat <<'PROMPT'
Strategist question #02  (ticket-driven, group: raw-exports)

Reference: raw-exports
Category: contracts/interfaces
Horizon: Immediate
ROI: 4.6 (impact=6, confidence=0.78, effort=1)

Question:
Using the attached tickets as the primary context, identify external integrations implied by the tickets; required config/contract changes; failure/timeout behavior; minimal compat-safe rollout.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.

Output section: Risks/unknowns
Start with heading: Risks/unknowns
Return only: Risks/unknowns (bullets)
PROMPT
)"

oracle   --files-report   --write-output "docs/oracle-questions-2026-01-09-030000/raw-exports/02-contracts-interfaces-integration-points-next-experiment.md"   "${ticket_args[@]}"      -p "$(cat <<'PROMPT'
Strategist question #02  (ticket-driven, group: raw-exports)

Reference: raw-exports
Category: contracts/interfaces
Horizon: Immediate
ROI: 4.6 (impact=6, confidence=0.78, effort=1)

Question:
Using the attached tickets as the primary context, identify external integrations implied by the tickets; required config/contract changes; failure/timeout behavior; minimal compat-safe rollout.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.

Output section: Next smallest concrete experiment
Start with heading: Next smallest concrete experiment
Return only: Next smallest concrete experiment (exactly one action)
PROMPT
)"

oracle   --files-report   --write-output "docs/oracle-questions-2026-01-09-030000/raw-exports/02-contracts-interfaces-integration-points-missing-evidence.md"   "${ticket_args[@]}"      -p "$(cat <<'PROMPT'
Strategist question #02  (ticket-driven, group: raw-exports)

Reference: raw-exports
Category: contracts/interfaces
Horizon: Immediate
ROI: 4.6 (impact=6, confidence=0.78, effort=1)

Question:
Using the attached tickets as the primary context, identify external integrations implied by the tickets; required config/contract changes; failure/timeout behavior; minimal compat-safe rollout.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.

Output section: Missing evidence
Start with heading: Missing evidence
Return only: If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"


# 03) ROI=5.1 impact=7 confidence=0.74 effort=1 horizon=NearTerm category=invariants reference=raw-exports

# tickets attached directly (deterministic; self-contained)
mapfile -t __tickets < <(python3 - <<'PY'
from __future__ import annotations
from pathlib import Path
import fnmatch

TICKET_ROOT = ".tickets"
TICKET_GLOB = "**/*.md"
TICKET_PATHS = ".tickets/raw-exports/Output verification failure.md".strip()
MAX = int("1")


root = Path(TICKET_ROOT)

def read_gitignore(start: Path):
    cur = start.resolve()
    if cur.is_file():
        cur = cur.parent
    while True:
        p = cur / ".gitignore"
        if p.exists():
            lines = []
            for ln in p.read_text(encoding="utf-8", errors="replace").splitlines():
                s = ln.strip()
                if not s or s.startswith("#"):
                    continue
                lines.append(s)
            return cur, lines
        if cur.parent == cur:
            return start.resolve(), []
        cur = cur.parent


def gitignore_match(rel_posix: str, name: str, pattern: str) -> bool:
    neg = pattern.startswith("!")
    if neg:
        pattern = pattern[1:]
    if not pattern:
        return False

    anchored = pattern.startswith("/")
    if anchored:
        pattern = pattern.lstrip("/")

    dir_only = pattern.endswith("/")
    if dir_only:
        pattern = pattern.rstrip("/")

    if "/" not in pattern:
        if dir_only:
            return any(part == pattern for part in rel_posix.split("/"))
        return fnmatch.fnmatch(name, pattern)

    target = rel_posix
    if anchored:
        if dir_only:
            return target.startswith(pattern + "/")
        return fnmatch.fnmatch(target, pattern)
    if dir_only:
        return f"/{pattern}/" in f"/{target}/"
    return fnmatch.fnmatch(target, f"**/{pattern}") or fnmatch.fnmatch(target, pattern)


def is_gitignored(path: Path, git_root: Path, patterns: list[str]) -> bool:
    try:
        rel = path.resolve().relative_to(git_root)
    except Exception:
        rel = path
    rel_posix = rel.as_posix()
    name = rel_posix.split("/")[-1]
    ignored = False
    for pat in patterns:
        neg = pat.startswith("!")
        if gitignore_match(rel_posix, name, pat):
            ignored = not neg
    return ignored


git_root, git_patterns = read_gitignore(root)
def lex_sorted(ps):
    return sorted((str(p) for p in ps), key=lambda s: s)

if TICKET_PATHS:
    tickets = [Path(p.strip()) for p in TICKET_PATHS.split(",") if p.strip()]
else:
    tickets = list(root.glob(TICKET_GLOB)) if root.exists() else []

tickets = [Path(p) for p in lex_sorted(tickets)]
tickets = [p for p in tickets if not is_gitignored(p, git_root, git_patterns)]
if MAX and MAX > 0:
    tickets = tickets[:MAX]

for p in tickets:
    print(str(p))
PY
)

ticket_args=()
for p in "${__tickets[@]}"; do
  ticket_args+=(-f "$p")
done

if [ "${#ticket_args[@]}" -eq 0 ]; then
  echo "WARNING: no tickets resolved for group 'raw-exports'." >&2
fi

# extra_files appended literally (may be empty; may include -f/--file):
oracle   --files-report   --write-output "docs/oracle-questions-2026-01-09-030000/raw-exports/03-invariants-invariant-map-direct-answer.md"   "${ticket_args[@]}"      -p "$(cat <<'PROMPT'
Strategist question #03  (ticket-driven, group: raw-exports)

Reference: raw-exports
Category: invariants
Horizon: NearTerm
ROI: 5.1 (impact=7, confidence=0.74, effort=1)

Question:
Using the attached tickets as the primary context, extract system invariants implied by tickets (inputs/outputs, pack schema rules, step execution rules) and where to enforce them.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.

Output section: Direct answer
Start with heading: Direct answer
Return only: Direct answer (1–10 bullets, evidence-cited)
PROMPT
)"

oracle   --files-report   --write-output "docs/oracle-questions-2026-01-09-030000/raw-exports/03-invariants-invariant-map-risks-unknowns.md"   "${ticket_args[@]}"      -p "$(cat <<'PROMPT'
Strategist question #03  (ticket-driven, group: raw-exports)

Reference: raw-exports
Category: invariants
Horizon: NearTerm
ROI: 5.1 (impact=7, confidence=0.74, effort=1)

Question:
Using the attached tickets as the primary context, extract system invariants implied by tickets (inputs/outputs, pack schema rules, step execution rules) and where to enforce them.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.

Output section: Risks/unknowns
Start with heading: Risks/unknowns
Return only: Risks/unknowns (bullets)
PROMPT
)"

oracle   --files-report   --write-output "docs/oracle-questions-2026-01-09-030000/raw-exports/03-invariants-invariant-map-next-experiment.md"   "${ticket_args[@]}"      -p "$(cat <<'PROMPT'
Strategist question #03  (ticket-driven, group: raw-exports)

Reference: raw-exports
Category: invariants
Horizon: NearTerm
ROI: 5.1 (impact=7, confidence=0.74, effort=1)

Question:
Using the attached tickets as the primary context, extract system invariants implied by tickets (inputs/outputs, pack schema rules, step execution rules) and where to enforce them.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.

Output section: Next smallest concrete experiment
Start with heading: Next smallest concrete experiment
Return only: Next smallest concrete experiment (exactly one action)
PROMPT
)"

oracle   --files-report   --write-output "docs/oracle-questions-2026-01-09-030000/raw-exports/03-invariants-invariant-map-missing-evidence.md"   "${ticket_args[@]}"      -p "$(cat <<'PROMPT'
Strategist question #03  (ticket-driven, group: raw-exports)

Reference: raw-exports
Category: invariants
Horizon: NearTerm
ROI: 5.1 (impact=7, confidence=0.74, effort=1)

Question:
Using the attached tickets as the primary context, extract system invariants implied by tickets (inputs/outputs, pack schema rules, step execution rules) and where to enforce them.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.

Output section: Missing evidence
Start with heading: Missing evidence
Return only: If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"


# 04) ROI=5.0 impact=7 confidence=0.72 effort=2 horizon=NearTerm category=invariants reference=raw-exports

# tickets attached directly (deterministic; self-contained)
mapfile -t __tickets < <(python3 - <<'PY'
from __future__ import annotations
from pathlib import Path
import fnmatch

TICKET_ROOT = ".tickets"
TICKET_GLOB = "**/*.md"
TICKET_PATHS = ".tickets/raw-exports/Output verification failure.md".strip()
MAX = int("1")


root = Path(TICKET_ROOT)

def read_gitignore(start: Path):
    cur = start.resolve()
    if cur.is_file():
        cur = cur.parent
    while True:
        p = cur / ".gitignore"
        if p.exists():
            lines = []
            for ln in p.read_text(encoding="utf-8", errors="replace").splitlines():
                s = ln.strip()
                if not s or s.startswith("#"):
                    continue
                lines.append(s)
            return cur, lines
        if cur.parent == cur:
            return start.resolve(), []
        cur = cur.parent


def gitignore_match(rel_posix: str, name: str, pattern: str) -> bool:
    neg = pattern.startswith("!")
    if neg:
        pattern = pattern[1:]
    if not pattern:
        return False

    anchored = pattern.startswith("/")
    if anchored:
        pattern = pattern.lstrip("/")

    dir_only = pattern.endswith("/")
    if dir_only:
        pattern = pattern.rstrip("/")

    if "/" not in pattern:
        if dir_only:
            return any(part == pattern for part in rel_posix.split("/"))
        return fnmatch.fnmatch(name, pattern)

    target = rel_posix
    if anchored:
        if dir_only:
            return target.startswith(pattern + "/")
        return fnmatch.fnmatch(target, pattern)
    if dir_only:
        return f"/{pattern}/" in f"/{target}/"
    return fnmatch.fnmatch(target, f"**/{pattern}") or fnmatch.fnmatch(target, pattern)


def is_gitignored(path: Path, git_root: Path, patterns: list[str]) -> bool:
    try:
        rel = path.resolve().relative_to(git_root)
    except Exception:
        rel = path
    rel_posix = rel.as_posix()
    name = rel_posix.split("/")[-1]
    ignored = False
    for pat in patterns:
        neg = pat.startswith("!")
        if gitignore_match(rel_posix, name, pat):
            ignored = not neg
    return ignored


git_root, git_patterns = read_gitignore(root)
def lex_sorted(ps):
    return sorted((str(p) for p in ps), key=lambda s: s)

if TICKET_PATHS:
    tickets = [Path(p.strip()) for p in TICKET_PATHS.split(",") if p.strip()]
else:
    tickets = list(root.glob(TICKET_GLOB)) if root.exists() else []

tickets = [Path(p) for p in lex_sorted(tickets)]
tickets = [p for p in tickets if not is_gitignored(p, git_root, git_patterns)]
if MAX and MAX > 0:
    tickets = tickets[:MAX]

for p in tickets:
    print(str(p))
PY
)

ticket_args=()
for p in "${__tickets[@]}"; do
  ticket_args+=(-f "$p")
done

if [ "${#ticket_args[@]}" -eq 0 ]; then
  echo "WARNING: no tickets resolved for group 'raw-exports'." >&2
fi

# extra_files appended literally (may be empty; may include -f/--file):
oracle   --files-report   --write-output "docs/oracle-questions-2026-01-09-030000/raw-exports/04-invariants-validation-boundaries-direct-answer.md"   "${ticket_args[@]}"      -p "$(cat <<'PROMPT'
Strategist question #04  (ticket-driven, group: raw-exports)

Reference: raw-exports
Category: invariants
Horizon: NearTerm
ROI: 5.0 (impact=7, confidence=0.72, effort=2)

Question:
Using the attached tickets as the primary context, identify validation boundaries that must exist (ticket parsing, pack generation, pack validation); propose minimal validation plan.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.

Output section: Direct answer
Start with heading: Direct answer
[TRUNCATED]
```

</source_code>

--- codefetch/oraclepack-tickets.md ---
<filetree>
Project Structure:
└── .tickets
    ├── actions
    │   ├── Enable Action Packs Dispatch.md
    │   ├── Improving Oraclepack Workflow.md
    │   ├── Oraclepack Action Pack Integration.md
    │   ├── Oraclepack Action Pack Issue.md
    │   ├── Oraclepack Action Packs.md
    │   └── Oraclepack Compatibility Issues.md
    ├── mcp
    │   ├── Expose Oraclepack as MCP.md
    │   ├── MCP Server for Oraclepack.md
    │   ├── gaps-still-not-covered.md
    │   ├── gaps_part2-mcp-builder.md
    │   ├── oraclepack-MCP.md
    │   └── oraclepack_mcp_server.md
    ├── other
    │   ├── Oraclepack Pipeline Improvements.md
    │   ├── Oraclepack Prompt Generator.md
    │   ├── Oraclepack Workflow Enhancement.md
    │   └── Verbose Payload Rendering TUI.md
    ├── PRD-TUI
    │   ├── Oraclepack TUI Integration.md
    │   └── PRD-generator URL routing.md
    ├── Formalize LLM Decision Points.md
    ├── Oraclepack CLI MCP Parity.md
    ├── Oraclepack File Storage.md
    ├── Oraclepack Parity Automation.md
    ├── Oraclepack Schema Approach.md
    ├── Oraclepack bash fix.md
    ├── Oraclepack output verification issues.md
    ├── Oraclepack-CLI-agents.md
    └── Publish OraclePack MCP.md

</filetree>

<source_code>
.tickets/Formalize LLM Decision Points.md
```
Parent Ticket:

* Title: Formalize enforceable LLM decision points for oraclepack generation/execution across Skills and MCP
* Summary:

  * The ticket enumerates a comprehensive set of pre-generation and runtime decision points (DP-01..DP-60) intended to improve pack generation and step execution quality. It also notes a preferred way to make these decision points “real, enforceable” by emitting small structured artifacts (JSON/YAML) that deterministic scripts consume, while keeping the emitted pack Markdown schema unchanged.
* Source:

  * Link/ID: Not provided
  * Original ticket excerpt (≤25 words) capturing the overall theme: “an ‘extensive’ set of additional, unique LLM decision points you can introduce (or explicitly formalize)…”
* Global Constraints:

  * “keeping the emitted pack Markdown schema unchanged”
* Global Environment:

  * Unknown
* Global Evidence:

  * OpenAI Codex “Custom Prompts” documentation. ([OpenAI Developers][1])
  * Gemini CLI “Agent Skills” documentation. ([Gemini CLI][2])
  * MCP specification revision 2025-11-25. ([Model Context Protocol][3])
  * MCP tools specification (server/tools) revision 2025-06-18. ([Model Context Protocol][4])

Split Plan:

* Coverage Map:

  * “In Codex/Gemini CLI, the ‘live agent’ inference…” → Info-only
  * “In an MCP setup, the ‘live agent’ inference…” → Info-only
  * “Below is an ‘extensive’ set…” → Info-only
  * DP-01 Choose generator mode (tickets-grouped vs codebase-grouped vs gold) → T2
  * DP-02 Select root(s) to scan (ticket_root/code_root) → T2
  * DP-03 Decide include/exclude glob rules → T2
  * DP-04 Decide whether to treat “loose” items as first-class candidates → T2
  * DP-05 Decide whether to require “strict schema mode” for outputs → T2
  * DP-06 Choose max pack size strategy (by tokens/bytes/files) → T2
  * DP-07 Choose per-pack cap: number of tickets/files → T2
  * DP-08 Decide ticket/file canonical title extraction rule → T3
  * DP-09 Choose text preprocessing (stopwords, stemming, code tokens) → T3
  * DP-10 Decide duplicate threshold policy (strict vs lenient) → T3
  * DP-11 Decide duplicate merge strategy (merge vs link vs pick canonical) → T3
  * DP-12 Decide what “differences” to preserve when merging duplicates → T3
  * DP-13 Decide hierarchical topic taxonomy vs flat groups → T4
  * DP-14 Decide group count target → T4
  * DP-15 Decide grouping algorithm choice (heuristic vs LLM-labeled vs hybrid) → T4
  * DP-16 Decide “ambiguous” assignment policy (multi-home vs best-fit) → T4
  * DP-17 Decide whether to create an “Unsorted / Needs triage” pack → T4
  * DP-18 Generate group names optimized for human scanning → T4
  * DP-19 Decide pack order (dependency, ROI, urgency, confidence) → T4
  * DP-20 Decide ticket order within pack (chronological vs dependency graph) → T4
  * DP-21 Select “context bundle” files to attach per pack → T5
  * DP-22 Decide whether to attach full files vs excerpts/summaries → T5
  * DP-23 Decide whether to generate a synthetic “pack brief” doc → T5
  * DP-24 Decide whether to include prior run artifacts (outputs) as inputs → T5
  * DP-25 Choose template variant (tickets vs codebase vs mixed) → T5
  * DP-26 Decide whether to inject organization standards into prompts → T5
  * DP-27 Decide step allocation across subtopics (steps per subdomain) → T5
  * DP-28 Choose prompt “stance” (audit-first vs implement-first vs design-first) → T5
  * DP-29 Choose “evidence bar” (strict citations vs lightweight) → T5
  * DP-30 Decide per-step required outputs (files changed, diffs, JSON, etc.) → T5
  * DP-31 Decide self-contained steps vs relying on previous outputs → T5
  * DP-32 Decide whether to add “ask-user” gates for missing critical inputs → T5
  * DP-33 Choose which MCP tools to call during generation (list/validate/generate) → T6
  * DP-34 Decide oracle model/engine selection and parameters → T6
  * DP-35 Decide whether to preflight oracle invocations (“--dry-run”) → T6
  * DP-36 Decide redaction policy for sensitive strings → T6
  * DP-37 Decide trace/correlation ID scheme for packs/steps → T6
  * DP-38 Decide which metrics/log events must be emitted per stage → T6
  * DP-39 Decide run strategy (run-all vs selective) → T7
  * DP-40 Decide concurrency / rate-limiting policy → T7
  * DP-41 Decide fail-fast vs continue collecting failures → T7
  * DP-42 Decide retry policy per failure type → T7
  * DP-43 Decide “prompt patch” for retries → T7
  * DP-44 Decide when to escalate to user for clarification → T7
  * DP-45 Decide whether to re-run earlier steps on contradictions → T7
  * DP-46 Decide acceptance criteria for a step output (format, completeness) → T8
  * DP-47 Decide whether to auto-validate produced artifacts (lint/tests/validate) → T8
  * DP-48 Decide how to synthesize step outputs into a final report → T9
  * DP-49 Decide PR/patch vs documentation-only output → T9
  * DP-50 Decide how to resolve conflicting recommendations across steps → T9
  * DP-51 Decide whether outputs meet policy/security standards before writing → T9
  * DP-52 Decide whether to reuse cached groupings/packs vs regenerate → T10
  * DP-53 Decide cache invalidation scope (one pack vs all) → T10
  * DP-54 Decide incident-style annotation on failures → T10
  * DP-55 Decide what artifacts to persist (manifests, intermediate summaries) → T10
  * DP-56 Decide how to shard outputs into “mini-packs” for follow-on runs → T10
  * DP-57 Decide naming/versioning for generated packs → T10
  * DP-58 Decide “next best tool call” in MCP (validate vs list vs run vs regenerate) → T10
  * DP-59 Decide whether to present diffs, file lists, or narrative only → T10
  * DP-60 Decide whether to extract new org heuristics into a reusable profile → T10
  * “make each decision point produce a small structured artifact (JSON/YAML)…” → T1
* Dependencies:

  * T2 depends on T1 because decision outputs should be emitted/consumed as “small structured artifact (JSON/YAML)”.
  * T3 depends on T1 because decision outputs should be emitted/consumed as “small structured artifact (JSON/YAML)”.
  * T4 depends on T1 because decision outputs should be emitted/consumed as “small structured artifact (JSON/YAML)”.
  * T5 depends on T1 because decision outputs should be emitted/consumed as “small structured artifact (JSON/YAML)”.
  * T6 depends on T1 because decision outputs should be emitted/consumed as “small structured artifact (JSON/YAML)”.
  * T7 depends on T1 because decision outputs should be emitted/consumed as “small structured artifact (JSON/YAML)”.
  * T8 depends on T1 because decision outputs should be emitted/consumed as “small structured artifact (JSON/YAML)”.
  * T9 depends on T1 because decision outputs should be emitted/consumed as “small structured artifact (JSON/YAML)”.
  * T10 depends on T1 because decision outputs should be emitted/consumed as “small structured artifact (JSON/YAML)”.

```ticket T1
T# Title: Make decision points enforceable via structured decision artifacts while keeping pack Markdown schema unchanged
Type: chore
Target Area: Pack generation/execution control plane (decision capture + deterministic consumption)
Summary:
- Establish the “typical pattern” described: each decision point emits a small structured artifact (JSON/YAML) and deterministic scripts consume it. Ensure the emitted pack Markdown schema remains unchanged. This provides a consistent way to formalize the DP-01..DP-60 decisions for both Skills-based flows and MCP tool-calling flows.
In Scope:
- “make each decision point produce a small structured artifact (JSON/YAML)”
- “make the deterministic scripts consume it”
- “keeping the emitted pack Markdown schema unchanged”
- Applicability across “skills” and “MCP”
Out of Scope:
- Not provided
Current Behavior (Actual):
- Not provided
Expected Behavior:
- Decision points emit a small structured artifact (JSON/YAML).
- Deterministic scripts consume the artifact.
- Emitted pack Markdown schema remains unchanged.
Reproduction Steps:
- Not provided
Requirements / Constraints:
- Keep emitted pack Markdown schema unchanged.
Evidence:
- Text: “make each decision point produce a small structured artifact (JSON/YAML)… deterministic scripts consume it… pack Markdown schema unchanged.”
Open Items / Unknowns:
- Exact schema/fields for the structured artifact (JSON vs YAML specifics not provided).
- Where deterministic consumption is implemented (locations not provided).
Risks / Dependencies:
- Not provided
Acceptance Criteria:
- A structured decision artifact format exists that can represent DP outputs.
- Deterministic scripts consume the decision artifact to drive generation/execution flow.
- No change to the emitted pack Markdown schema is required to adopt the pattern.
Priority & Severity (if inferable from input text):
- Priority: Not provided
- Severity: Not provided
Source:
- “make each decision point produce a small structured artifact (JSON/YAML)”
- “make the deterministic scripts consume it”
- “keeping the emitted pack Markdown schema unchanged”
```

```ticket T2
T# Title: Implement pre-generation routing, scope, governance, and budgeting decision points (DP-01..DP-07)
Type: enhancement
Target Area: Pre-gen decision layer (routing/scope/governance/budgeting)
Summary:
- Add explicit, formalized pre-generation decisions for selecting generator mode, scan roots, include/exclude rules, loose-item handling, strict schema mode, and pack sizing/caps. Each decision produces the specified output/action for use by pack generation.
In Scope:
- DP-01 Choose generator mode (tickets-grouped vs codebase-grouped vs gold)
- DP-02 Select root(s) to scan (ticket_root/code_root)
- DP-03 Decide include/exclude glob rules
- DP-04 Decide whether to treat “loose” items as first-class candidates
- DP-05 Decide whether to require “strict schema mode” for outputs
- DP-06 Choose max pack size strategy (by tokens/bytes/files)
- DP-07 Choose per-pack cap: number of tickets/files
Out of Scope:
- Not provided
Current Behavior (Actual):
- Not provided
Expected Behavior:
- Generator mode can be selected and recorded.
- Scan roots and include/exclude patterns can be selected and recorded.
- Loose-item handling policy and strict schema mode selection can be decided and recorded.
- Pack sizing strategy and per-pack caps can be decided and recorded.
Reproduction Steps:
- Not provided
Requirements / Constraints:
- Decisions should produce the “Output / action produced” listed for each DP (e.g., “Selected generator + params”, “Root paths”, “Include/exclude patterns”, “Sharding plan”, “Caps per pack”).
Evidence:
- DP-01..DP-07 rows (routing/scope/governance/budgeting)
Open Items / Unknowns:
- What “gold” mode entails (not provided).
- Default config values and where they are defined (not provided).
- Token/byte/file limits used for sizing (not provided).
Risks / Dependencies:
- Depends on T1 if adopting the structured decision artifact pattern.
Acceptance Criteria:
- DP-01 emits “Selected generator + params”.
- DP-02 emits “Root paths”.
- DP-03 emits “Include/exclude patterns”.
- DP-04 emits a “Loose-ticket policy”.
- DP-05 emits an “Enforce extra validation gates” decision.
- DP-06 emits a “Sharding plan”.
- DP-07 emits “Caps per pack”.
Priority & Severity (if inferable from input text):
- Priority: Not provided
- Severity: Not provided
Source:
- “Choose generator mode (tickets-grouped vs codebase-grouped vs gold)”
- “Select root(s) to scan (ticket_root/code_root)”
- “Choose max pack size strategy (by tokens/bytes/files)”
```

```ticket T3
T# Title: Implement pre-generation normalization and deduplication decision points (DP-08..DP-12)
Type: enhancement
Target Area: Pre-gen text normalization + dedup decisions
Summary:
- Add explicit, formalized decisions for canonical title extraction, preprocessing policy, duplicate thresholds, merge strategy, and delta preservation when merging duplicates. Each decision produces the specified output/action for use during grouping/packing.
In Scope:
- DP-08 Decide ticket/file canonical title extraction rule
[TRUNCATED]
```

.tickets/Oraclepack CLI MCP Parity.md
```
Parent Ticket:

* Title: Oraclepack CLI/MCP parity with grouped-pack skills and reliable artifact generation
* Summary: Tighten `oraclepack` validation and execution context so generated packs run without common failures (pack shape drift, workdir/path drift, oracle CLI flag drift). Add MCP passthrough for key run flags, add preflight checks, align pack templates with `out_dir`, and add a first-class `oraclepack generate` surface (wrapper first, native generator later) matching grouped-pack skill outputs.
* Source:

  * Link/ID: Not provided
  * Original ticket excerpt (≤25 words) capturing the overall theme: “get our oraclepack CLI/MCP up to date so artifacts … run through oraclepack without any errors”
* Global Constraints:

  * Packs must have exactly one ` ```bash … ``` ` fence and no other code fences.
  * Packs must have exactly 20 steps (01..20) with correct step header format.
  * Generation should be deterministic (e.g., lexicographic discovery) per described grouping rules.
* Global Environment:

  * Unknown
* Global Evidence:

  * References to: `internal/pack/parser.go` / `pack.Validate()`, `app.Config.WorkDir`, `ExtractOracleInvocations`, `ValidateOverrides`, MCP tool `oraclepack_run_pack`.
  * Commands mentioned: `oraclepack validate`, `oraclepack list`, `oraclepack run --no-tui --yes --run-all`, `oracle … --dry-run summary`, `python3 scripts/validate_pack.py`.

Split Plan:

* Coverage Map:

  * “Make `oraclepack validate` enforce … ‘strict pack shape’ …” → T1
  * “In `internal/pack/parser.go` (or `pack.Validate()`), count … fences …” → T1
  * “there isn’t exactly one ` ```bash … ``` ` block …” → T1
  * “any other code fences exist …” → T1
  * “Fix workdir determinism … missing `-f` files …” → T2
  * “Add a persistent `--work-dir` flag … `app.Config.WorkDir`.” → T2
  * “Default behavior when `--work-dir` is omitted:” → T2
  * “auto-detect repo root from `packPath` … set WorkDir …” → T2
  * “In the MCP server, extend `oraclepack_run_pack` to accept …” → T3
  * “Expose `--oracle-bin` (and optionally `--out-dir`) through MCP” → T3
  * “Add MCP tool params: … `oracle_bin` … `out_dir` … `work_dir` …” → T3
  * “Append `--oracle-bin …` / `--out-dir …` / `--work-dir …` …” → T3
  * “Add preflight checks that match how the packs fail …” → T4
  * “Missing attachment file preflight … verify every `-f <path>` exists …” → T4
  * “Oracle flag drift preflight … `ValidateOverrides` …” → T5
  * “Wire this into `oraclepack run` … `--preflight-oracle` …” → T5
  * “Extract invocations (already implemented).” → T5
  * “Inject flags safely (already implemented).” → T5
  * “Execute `--dry-run summary` and block …” → T5
  * “Align pack templates with … `out_dir` model …” → T6
  * “In the pack prelude … include `out_dir="<resolved default>"` …” → T6
  * “Use `--write-output "$out_dir/…"` and `mkdir -p "$out_dir"`.” → T6
  * “Minimum validation pipeline to prevent regressions …” → T10
  * “Generation-time … `python3 scripts/validate_pack.py …`” → T10
  * “Oraclepack-level: `oraclepack validate …`” → T10
  * “Parse sanity: `oraclepack list …` prints 20 …” → T10
  * “Execution smoke … stub `oracle` via `--oracle-bin` …” → T10
  * “Add a new CLI command …” → T7
  * “`oraclepack generate tickets-grouped …`” → T7
  * “`oraclepack generate codebase-grouped …`” → T7
  * “Implementation: … shell out … generate_grouped_packs.py” → T7
  * “After generation, call `oraclepack validate` …” → T7
  * “Embed the exact templates … into the `oraclepack` binary …” → T8
  * “Port the deterministic grouping rules … `internal/generate/*` …” → T8
  * “discover files deterministically …” → T8
  * “group by subdir + infer loose files …” → T8
  * “split oversized groups into ‘part N’ packs” → T8
  * “Render packs … fail on unresolved placeholders …” → T8
  * “Validate with the existing pack parser/validator constraints:” → T8
  * “parser requires a ` ```bash ... ``` ` code block … ROI …” → T8
  * “generator should always emit exactly 20 steps … one bash fence …” → T8
  * “CLI shape: add `oraclepack generate` via Cobra … flags …” → T7
  * “MCP: expose generation as tools …” → T9
  * “`oraclepack_generate_tickets_grouped(…) -> {packs…, manifest…}`” → T9
  * “`oraclepack_generate_codebase_grouped(…) -> {packs…, manifest…}`” → T9
  * “What the generator must output … `out_dir/packs/*.md` … `_groups.json` …” → T7
  * “Each pack must remain runner-ingestible …” → T7
  * “Minimal acceptance test (proves parity) … generate … validate/list … run …” → T10
  * Standalone “...” truncation markers in the source text → Info-only
* Dependencies:

  * T3 depends on T2 because MCP `work_dir` passthrough requires a working CLI `--work-dir` behavior.
  * T4 depends on T2 because attachment existence is checked relative to the resolved WorkDir.
  * T7 depends on T1 because generation “fail fast” relies on strict `oraclepack validate` shape enforcement.
  * T9 depends on T7 because MCP generation can initially delegate to the CLI `oraclepack generate` surface.
  * T10 depends on T7 because the parity acceptance flow starts from `oraclepack generate …`.
* Split Tickets:

````ticket T1
T# Title: Enforce strict pack-shape validation in `oraclepack validate`
Type: chore
Target Area: `oraclepack validate`; `internal/pack/parser.go` and/or `pack.Validate()`
Summary:
  Tighten pack validation to match the grouped-pack skill contract by enforcing code-fence invariants. This prevents “shape drift” (extra fences, wrong fence language) from reaching execution. The goal is early, actionable failure during `oraclepack validate`.
In Scope:
  - Count all triple-backtick fences during validation.
  - Fail validation if there is not exactly one ` ```bash … ``` ` block.
  - Fail validation if any other code fences exist.
Out of Scope:
  - Not provided
Current Behavior (Actual):
  - Parser extracts a bash block via regex and parses steps via header regex, but does not inherently guarantee “exactly one code fence and no other fences.”
Expected Behavior:
  - `oraclepack validate` rejects packs that contain multiple fences, non-bash fences, or missing the single required `bash` fence.
Reproduction Steps:
  - Not provided
Requirements / Constraints:
  - Enforce “exactly one `bash` fence and no other fences.”
Evidence:
  - References: “pack parser extracts the bash block via a regex … doesn’t inherently guarantee ‘exactly one code fence and no other fences.’”
Open Items / Unknowns:
  - Exact location of current validation entrypoint (whether `internal/pack/parser.go` or `pack.Validate()` is canonical) not provided.
Risks / Dependencies:
  - Not provided
Acceptance Criteria:
  - `oraclepack validate` fails when a pack has 0 ` ```bash ` fences.
  - `oraclepack validate` fails when a pack has 2+ code fences.
  - `oraclepack validate` fails when a non-bash code fence exists alongside the bash fence.
  - `oraclepack validate` passes when exactly one ` ```bash … ``` ` fence exists and no other code fences exist.
Priority & Severity (if inferable from input text):
  - Priority: Not provided
  - Severity: Not provided
Source:
  - “Make `oraclepack validate` enforce … ‘strict pack shape’…”
  - “count all triple-backtick fences and fail if …”
  - “there isn’t exactly one ` ```bash … ``` ` block …”
````

```ticket T2
T# Title: Make `oraclepack run` workdir deterministic for relative `-f` paths
Type: bug
Target Area: `oraclepack run`; CLI config `app.Config.WorkDir`; repo-root detection from `packPath`
Summary:
  Fix failures caused by running packs from the wrong working directory, which breaks relative `-f` attachments. Add a persistent `--work-dir` flag and a default workdir inference strategy when the flag is omitted. This makes pack execution consistent regardless of where the pack file lives.
In Scope:
  - Add a persistent CLI `--work-dir` flag.
  - Plumb `--work-dir` into `app.Config.WorkDir`.
  - Default behavior when omitted: auto-detect repo root from `packPath` and set WorkDir to that root (not the pack’s directory).
Out of Scope:
  - MCP server tool signature changes (handled separately).
Current Behavior (Actual):
  - `oraclepack run` hardcodes `WorkDir: "."`, causing missing `-f` files when packs reference repo-root-relative files from other working directories.
Expected Behavior:
  - Running a pack resolves relative paths consistently under the inferred repo root, unless `--work-dir` is explicitly provided.
Reproduction Steps:
  - Not provided
Requirements / Constraints:
  - Default must “auto-detect repo root from `packPath` … and set WorkDir to that root.”
Evidence:
  - “Fix workdir determinism … missing `-f` files”
  - “`oraclepack run` currently hardcodes `WorkDir: "."`…”
Open Items / Unknowns:
  - Exact repo-root detection signals (e.g., `.git/`, `go.mod`, etc.) are referenced but truncated in the source text.
Risks / Dependencies:
  - Not provided
Acceptance Criteria:
  - CLI exposes `--work-dir` and it sets the runner workdir.
  - When `--work-dir` is omitted, WorkDir is inferred from `packPath` and is not the pack’s directory.
  - Packs referencing repo-root-relative `-f` attachments do not fail solely due to execution from a different current directory.
Priority & Severity (if inferable from input text):
  - Priority: Not provided
  - Severity: Not provided
Source:
  - “Fix workdir determinism …”
  - “Add a persistent `--work-dir` flag … `app.Config.WorkDir`.”
  - “auto-detect repo root from `packPath` … set WorkDir …”
```

```ticket T3
T# Title: Expose run flags through MCP: `work_dir`, `oracle_bin`, `out_dir`
Type: enhancement
Target Area: MCP server (FastMCP); tool `oraclepack_run_pack`; CLI invocation wiring
Summary:
  The MCP wrapper must pass through key execution-context flags so packs run reliably in MCP environments (where `oracle` may not be on PATH and workdir may differ). Extend the MCP tool signature to accept `work_dir`, `oracle_bin`, and `out_dir`, and forward them to the CLI invocation.
In Scope:
  - Extend MCP `oraclepack_run_pack` tool to accept `work_dir`.
  - Add MCP tool params: `oracle_bin: Optional[str]`, `out_dir: Optional[str]`, `work_dir: Optional[str]`.
  - Append `--oracle-bin <path>` / `--out-dir <dir>` / `--work-dir <dir>` when calling the CLI.
Out of Scope:
  - Implementing CLI semantics for `--work-dir` (handled in T2).
Current Behavior (Actual):
  - MCP wrapper only passes `run … --no-tui [--yes] [--run-all]` and does not expose `--oracle-bin` / `--out-dir` / `--work-dir`, causing failures when `oracle` is not on PATH or workdir differs.
Expected Behavior:
  - MCP can run packs with explicit oracle binary, output directory, and working directory controls.
Reproduction Steps:
  - Not provided
Requirements / Constraints:
  - MCP signature changes must follow the FastMCP tool-parameter pattern (per source text).
Evidence:
  - “The MCP wrapper doesn’t expose it today; if `oracle` isn’t on PATH … runs will fail …”
  - “extend `oraclepack_run_pack` to accept `work_dir` …”
Open Items / Unknowns:
  - Exact MCP server file/module containing `oraclepack_run_pack` definition not provided.
Risks / Dependencies:
  - Depends on T2 for `--work-dir` behavior to exist in CLI.
Acceptance Criteria:
  - MCP tool `oraclepack_run_pack` accepts `work_dir`, `oracle_bin`, and `out_dir` parameters.
  - MCP invocation forwards those parameters to the CLI as `--work-dir`, `--oracle-bin`, and `--out-dir`.
  - Running via MCP does not require `oracle` to be on PATH when `oracle_bin` is supplied.
Priority & Severity (if inferable from input text):
  - Priority: Not provided
  - Severity: Not provided
Source:
  - “Expose `--oracle-bin` (and optionally `--out-dir`) through MCP”
  - “Add MCP tool params: `oracle_bin … out_dir … work_dir …`”
  - “Append `--oracle-bin …` / `--out-dir …` / `--work-dir …` …”
```

```ticket T4
T# Title: Add preflight to fail fast on missing `-f` attachment files
Type: enhancement
Target Area: `oraclepack run` preflight; oracle invocation scanning via `ExtractOracleInvocations`; path resolution via WorkDir
Summary:
  Add a preflight check that detects missing attachment files referenced by `oracle … -f <path>` before executing steps. This turns late runtime failures into early, actionable errors that identify the step number and missing path. The check must resolve paths relative to the resolved WorkDir.
In Scope:
[TRUNCATED]
```

.tickets/Oraclepack File Storage.md
```
Parent Ticket:

* Title: Stop oraclepack from writing run state/config JSON files into the project working directory
* Summary: oraclepack currently writes per-pack `*.state.json`, `*.report.json`, and `*.chatgpt-urls.json` files into the repo/working directory. The requested change is to store these as config/state/cache outside the repo root (prefer XDG base dirs) and/or under a dedicated project-local `.oraclepack/` directory to avoid clutter.
* Source:

  * Link/ID: Not provided
  * Original ticket excerpt (≤25 words) capturing the overall theme: “Move state/report outputs out of CWD by default… Stop producing per-pack `*.chatgpt-urls.json` by default.”
* Global Constraints:

  * Treat outputs as **config/state/cache** and store outside repo root using XDG base dirs (per ticket text).
  * Use Go `os.UserConfigDir()` / `os.UserCacheDir()` for cross-platform defaults (per ticket text).
  * No `UserStateDir()` in Go stdlib; implement `$XDG_STATE_HOME` fallback (per ticket text).
* Global Environment:

  * Unknown
* Global Evidence:

  * Current filenames mentioned: `<packBase>.state.json`, `<packBase>.report.json`, `<sameBase>.chatgpt-urls.json`.
  * XDG Base Directory spec reference (background).
  * Go `os.UserConfigDir` / `os.UserCacheDir` reference (background).

Split Plan:

* Coverage Map:

  * Original item: “`oraclepack run` … derives filenames from the pack basename and writes them to the current working directory: `statePath := <packBase>.state.json`, `reportPath := <packBase>.report.json`”

    * Assigned Ticket ID: T2
  * Original item: “The TUI ‘ChatGPT URL picker’ then creates `<sameBase>.chatgpt-urls.json` next to the state file (or next to the pack file if statePath is empty).”

    * Assigned Ticket ID: T3
  * Original item: “It also defaults edits to **project scope**, so it will keep generating project-scoped stores unless the user explicitly switches to global.”

    * Assigned Ticket ID: T3
  * Original item: “Treat these as **config/state/cache** and store them outside the repo root using standard base dirs: … `$XDG_CONFIG_HOME` … `$XDG_STATE_HOME` … `$XDG_CACHE_HOME`…”

    * Assigned Ticket ID: T1
  * Original item: “In Go, you should use `os.UserConfigDir()` / `os.UserCacheDir()`… (There’s no `UserStateDir()`… implement XDG_STATE_HOME fallback…)”

    * Assigned Ticket ID: T1
  * Original item: “Move state/report outputs out of CWD by default… Update `internal/cli/run.go`… Make the directory overridable with a flag/env (e.g., `--state-dir` / `ORACLEPACK_STATE_DIR`).”

    * Assigned Ticket ID: T2
  * Original item: “Stop producing per-pack `*.chatgpt-urls.json` by default… Best UX default: change … default save scope to **global**…”

    * Assigned Ticket ID: T3
  * Original item: “Keep ‘project scope’ as an opt-in mode, but write it to a single per-project location (e.g., `<repo>/.oraclepack/chatgpt-urls.json`), not `<packName>.chatgpt-urls.json`.”

    * Assigned Ticket ID: T3
  * Original item: “Acceptable alternative (project-local…): `<repo>/.oraclepack/state/*.state.json` … `<repo>/.oraclepack/chatgpt-urls.json` … add `.oraclepack/` to `.gitignore`.”

    * Assigned Ticket ID: T4
  * Original item: “Immediate workaround (no code changes): Add these to `.gitignore`: `*.state.json`, `*.report.json`, `*.chatgpt-urls.json`.”

    * Assigned Ticket ID: T4
* Dependencies:

  * T2 depends on T1 because T2 needs an agreed/default “oraclepack state dir” location strategy (XDG-based) to write into.
  * T3 depends on T1 because T3 needs a global config location strategy (XDG-based) for URL persistence.
* Split Tickets:

```ticket T1
T# Title: Define XDG-based directory strategy for oraclepack config/state/cache
Type: chore
Target Area: Config/state path resolution (shared utility / helpers)
Summary:
- Define the standard locations where oraclepack stores user config, run state, and cache so outputs stop polluting the repo root.
- The ticket requires using XDG base dirs and Go’s cross-platform helpers where applicable, with an explicit fallback for state.
In Scope:
- Adopt XDG directory categories as the guiding model:
  - Config: `$XDG_CONFIG_HOME` (default `~/.config`)
  - State: `$XDG_STATE_HOME` (default `~/.local/state`)
  - Cache: `$XDG_CACHE_HOME` (default `~/.cache`)
- Use Go `os.UserConfigDir()` / `os.UserCacheDir()` for cross-platform defaults (per ticket text).
- Implement a state-dir resolver that honors `$XDG_STATE_HOME` and falls back when not set (since Go stdlib has no `UserStateDir()`).
Out of Scope:
- Not provided
Current Behavior (Actual):
- Not provided
Expected Behavior:
- oraclepack has a single, consistent mechanism to determine:
  - “oraclepack config dir” (for user prefs like URL lists)
  - “oraclepack state dir” (for resume/run state)
  - “oraclepack cache dir” (for non-essential cached data)
Reproduction Steps:
- Not provided
Requirements / Constraints:
- Treat outputs as config/state/cache; store outside repo root using standard base dirs (per ticket text).
- Use `os.UserConfigDir()` / `os.UserCacheDir()` where applicable (per ticket text).
- Implement `$XDG_STATE_HOME` fallback logic (per ticket text).
Evidence:
- “Treat these as config/state/cache and store them outside the repo root using standard base dirs…” (parent ticket)
- “In Go, you should use os.UserConfigDir() / os.UserCacheDir()… There’s no UserStateDir()…” (parent ticket)
Open Items / Unknowns:
- Exact package/file locations for where to place the shared directory-resolution logic: Unknown
Risks / Dependencies:
- Not provided
Acceptance Criteria:
- A single directory-resolution mechanism exists for config/state/cache categories as described in scope.
- The state-dir resolution honors `$XDG_STATE_HOME` when set and has a documented fallback when unset.
- The config/cache resolution uses Go’s `os.UserConfigDir()` / `os.UserCacheDir()` (or equivalent wrapper) per ticket text.
Priority & Severity (if inferable from input text):
- Priority: Not provided
- Severity: Not provided
Source:
- “Treat these as config/state/cache and store them outside the repo root using standard base dirs…”
- “In Go, you should use os.UserConfigDir() / os.UserCacheDir()… There’s no UserStateDir()…”
```

```ticket T2
T# Title: Move run-generated state/report JSON outputs out of CWD and add state-dir override
Type: enhancement
Target Area: Run command output paths (`internal/cli/run.go`)
Summary:
- oraclepack currently writes `<packBase>.state.json` and `<packBase>.report.json` into the current working directory.
- Update the run pathing so these files are written under a dedicated “oraclepack state dir” by default, with an override via flag/env.
In Scope:
- Change default output location for:
  - `<packBase>.state.json`
  - `<packBase>.report.json`
  from current working directory to a dedicated “oraclepack state dir”.
- Update `internal/cli/run.go` to compute state/report paths under that state dir (per ticket text).
- Add override via flag and env:
  - `--state-dir`
  - `ORACLEPACK_STATE_DIR`
Out of Scope:
- Not provided
Current Behavior (Actual):
- `<packBase>.state.json` and `<packBase>.report.json` are written to the current working directory.
Expected Behavior:
- By default, running oraclepack does not create `*.state.json` / `*.report.json` in the repo root / CWD.
- By default, state/report files are written under the dedicated oraclepack state dir.
- Setting `--state-dir` or `ORACLEPACK_STATE_DIR` writes state/report files under the specified directory.
Reproduction Steps:
1) Run `oraclepack run` from a repo root (or any working directory).
2) Observe creation of `<packBase>.state.json` and `<packBase>.report.json` in the working directory.
Requirements / Constraints:
- Must be overridable by `--state-dir` / `ORACLEPACK_STATE_DIR` (per ticket text).
- Should use the state-dir strategy defined in T1 for the default state dir.
Evidence:
- “`oraclepack run` … writes them to the current working directory: `statePath := <packBase>.state.json`, `reportPath := <packBase>.report.json`”
- “Update `internal/cli/run.go` … Make the directory overridable with a flag/env (e.g., `--state-dir` / `ORACLEPACK_STATE_DIR`).”
Open Items / Unknowns:
- Whether state/report filenames must remain exactly `<packBase>.state.json` / `<packBase>.report.json` or can change: Not provided
Risks / Dependencies:
- Depends on T1 for default state-dir resolution strategy.
Acceptance Criteria:
- Running oraclepack with no overrides does not create `*.state.json` or `*.report.json` in the current working directory.
- With no overrides, state/report files are written under the resolved oraclepack state dir.
- With `--state-dir=<dir>`, state/report files are written under `<dir>`.
- With `ORACLEPACK_STATE_DIR=<dir>`, state/report files are written under `<dir>`.
Priority & Severity (if inferable from input text):
- Priority: Not provided
- Severity: Not provided
Source:
- “`oraclepack run` … writes … `<packBase>.state.json` … `<packBase>.report.json` … to the current working directory”
- “Move state/report outputs out of CWD by default… Update internal/cli/run.go… `--state-dir` / `ORACLEPACK_STATE_DIR`”
```

```ticket T3
T# Title: Stop generating per-pack `*.chatgpt-urls.json`; default URL picker persistence to global store
Type: enhancement
Target Area: TUI “ChatGPT URL picker” persistence
Summary:
- The TUI URL picker currently creates `<sameBase>.chatgpt-urls.json` near the pack/state file and defaults edits to project scope.
- Change it so the default save scope is global (one file), while keeping project scope as an opt-in that writes to a single stable per-project path.
In Scope:
- Remove/avoid creating `<sameBase>.chatgpt-urls.json` “next to the state file (or next to the pack file…)” (per ticket text).
- Change the URL picker default save scope to **global** (per ticket text).
- Keep “project scope” as opt-in, but store at a single stable path:
  - `<repo>/.oraclepack/chatgpt-urls.json`
  rather than `<packName>.chatgpt-urls.json` (per ticket text).
- Persist the global URL store to a single global location:
  - Per ticket text, an existing global store path is referenced: `~/.oraclepack/chatgpt-urls.json`.
Out of Scope:
- Not provided
Current Behavior (Actual):
- URL picker creates `<sameBase>.chatgpt-urls.json` next to the state file (or pack file).
- URL picker defaults edits to project scope.
Expected Behavior:
- Using the URL picker does not create `<packBase>.chatgpt-urls.json` files.
- Default persistence is global (one stable file).
- Project scope, if selected, writes only to `<repo>/.oraclepack/chatgpt-urls.json`.
Reproduction Steps:
1) Use the TUI “ChatGPT URL picker” during a run.
2) Observe `<sameBase>.chatgpt-urls.json` being created near pack/state file.
Requirements / Constraints:
- Default save scope should be global (per ticket text).
- Project scope must be opt-in and must not create per-pack URL json files (per ticket text).
- Global persistence location:
  - Conflicting guidance exists: ticket recommends XDG config dir generally, but also references existing `~/.oraclepack/chatgpt-urls.json` path.
Evidence:
- “The TUI ‘ChatGPT URL picker’ then creates `<sameBase>.chatgpt-urls.json`…”
- “It also defaults edits to project scope…”
- “Stop producing per-pack `*.chatgpt-urls.json` by default… change … default save scope to global…”
- “Keep ‘project scope’ as an opt-in… write it to `<repo>/.oraclepack/chatgpt-urls.json`… not `<packName>.chatgpt-urls.json`.”
Open Items / Unknowns:
- Whether to keep `~/.oraclepack/chatgpt-urls.json` as the global path or migrate to `$XDG_CONFIG_HOME/...` (both appear in the parent ticket guidance).
- Exact file/path of the “URL picker” implementation: Not provided
Risks / Dependencies:
- Depends on T1 if migrating global storage to XDG config dir.
Acceptance Criteria:
- After using the URL picker, no `<packBase>.chatgpt-urls.json` is created near the pack/state/CWD.
- Default behavior persists URLs to exactly one global store (stable path; not per-pack).
- When “project scope” is selected, URLs persist to `<repo>/.oraclepack/chatgpt-urls.json` (single per-project file).
Priority & Severity (if inferable from input text):
- Priority: Not provided
- Severity: Not provided
Source:
- “The TUI ‘ChatGPT URL picker’ then creates `<sameBase>.chatgpt-urls.json`…”
- “Best UX default: change … default save scope to global…”
[TRUNCATED]
```

.tickets/Oraclepack Parity Automation.md
```
Parent Ticket:

* Title: Keep oraclepack in parity with upstream oracle updates
* Summary: Define and automate a process so oraclepack (wrapper) stays compatible as upstream oracle changes. Approach centers on pinning the upstream oracle version, minimizing wrapper coupling, adding contract tests, and automating upgrade PRs + canary checks.
* Source:

  * Link/ID: Not provided
  * Original ticket excerpt (≤25 words) capturing the overall theme: “keep parity with updates with a target repo as their repo updates/changes… wrapper around oracle… keep… up to date”
* Global Constraints:

  * Not provided
* Global Environment:

  * Node 22+ (per referenced upstream distribution note)
  * GitHub Actions (Dependabot + scheduled workflows)
  * Go toolchain used in CI example (version shown as 1.24.x)
* Global Evidence:

  * Dependabot configuration requires `.github/dependabot.yml` and supports npm + GitHub Actions ecosystems. ([GitHub Docs][1])
  * Renovate npm manager documentation (alternative automation). ([Renovate Docs][2])
  * peter-evans/create-pull-request GitHub Action (PR automation). ([GitHub][3])
  * Upstream Sync marketplace action (fork-sync scenario). ([GitHub][4])
  * Git submodule/subtree background (vendor option scenario). ([Atlassian][5])
  * Full original discussion text.

Split Plan:

* Coverage Map:

  * Original item: “treat upstream as a versioned dependency… automate detection/compat testing/PRs/releases” → Assigned Ticket ID: T1
  * Original item: “pins `@steipete/oracle`… runs deterministically” → Assigned Ticket ID: T1
  * Original item: “log/record the exact `oracle` version used into its `.state.json`/`.report.json`” → Assigned Ticket ID: T1
  * Original item: “Vendor via Node workspace… execute `node_modules/.bin/oracle`” → Assigned Ticket ID: T1
  * Original item: “Or run `npx -y @steipete/oracle@<pinned>`” → Assigned Ticket ID: T1
  * Original item: “Reduce parity surface area… pass-through custom args/flags forwarded unchanged” → Assigned Ticket ID: T2
  * Original item: “Only model/enumerate upstream flags where you must (Overrides Wizard)” → Assigned Ticket ID: T2
  * Original item: “If you do enumerate, auto-discover rather than hardcode” → Assigned Ticket ID: T3
  * Original item: “Add a contract test suite that detects upstream CLI surface changes early” → Assigned Ticket ID: T4
  * Original item: “Snapshot tests… capture `oracle --help` and `oracle <critical-subcommand> --help`… diff snapshots in CI” → Assigned Ticket ID: T4
  * Original item: “Behavioral ‘golden path’ tests… run a small pack fixture… assert only on wrapper-owned artifacts” → Assigned Ticket ID: T4
  * Original item: “Automate updates: Dependabot/Renovate” → Assigned Ticket ID: T5
  * Original item: “Scheduled ‘canary’ workflow… run against `@steipete/oracle@latest`” → Assigned Ticket ID: T7
  * Original item: “Release discipline: compatibility statement… gate releases on contract tests + canary” → Assigned Ticket ID: T8
  * Original item: “Concrete GitHub config examples: `.github/dependabot.yml`” → Assigned Ticket ID: T5
  * Original item: “Concrete workflow example: `.github/workflows/bump-oracle.yml` using `peter-evans/create-pull-request`” → Assigned Ticket ID: T6
  * Original item: “If you want ‘sync the fork with upstream’… use fork-sync/upstream-sync workflow/action” → Assigned Ticket ID: Info-only
  * Original item: “If you ever decide to vendor upstream code… use subtree/submodule… submodules don’t auto-track branches” → Assigned Ticket ID: Info-only
  * Original item: “Minimal next step… pinned version file + CI contract suite + enable Dependabot weekly updates” → Assigned Ticket ID: T1 (pin), T4 (contract suite), T5 (Dependabot)
* Dependencies:

  * T4 depends on T1 because contract tests are described as running against a pinned/deterministic upstream version.
  * T6 depends on T4 because the bump workflow is described as running unit + contract tests before opening a PR.
  * T7 depends on T4 because the canary job is described as running the integration/contract suite.
  * T8 depends on T4 and T7 because release gating is described as using contract tests and the canary signal.
* Split Tickets:

```ticket T1
T# Title: Pin upstream oracle version and record the runtime oracle version used
Type: enhancement
Target Area: Dependency boundary / runtime invocation / run artifacts (.state.json/.report.json)
Summary:
  Establish a deterministic upstream boundary by pinning the upstream `@steipete/oracle` version used by oraclepack runs. Ensure the exact oracle version used is recorded in wrapper-owned artifacts so reports/bugs are attributable to a specific upstream version.
In Scope:
  - Add a single source of truth for the pinned upstream oracle version (example given: `tools/oracle-version.txt`).
  - Ensure oraclepack uses the pinned `@steipete/oracle` version deterministically at runtime.
  - Record/log the exact oracle version used into `.state.json` and/or `.report.json` (as referenced).
  - Support one of the described execution modes:
    - Node workspace vendoring (`node_modules/.bin/oracle`), or
    - `npx -y @steipete/oracle@<pinned>` execution.
Out of Scope:
  - Upstream fork syncing and vendoring upstream source code (subtree/submodule) approaches.
Current Behavior (Actual):
  - Not provided
Expected Behavior:
  - oraclepack runs use a deterministic pinned oracle version rather than an implicitly installed/global/latest version.
  - oraclepack run artifacts include the upstream oracle version used.
Reproduction Steps:
  - Not provided
Requirements / Constraints:
  - Upstream oracle is referenced as an npm package `@steipete/oracle` and expects Node 22+ (as noted).
Evidence:
  - Not provided
Open Items / Unknowns:
  - Whether oraclepack currently shells out to a global oracle, vendors a local dependency, or uses npx (not provided).
  - Whether both `.state.json` and `.report.json` exist and their current schema/paths (not provided).
Risks / Dependencies:
  - Depends on CI/runtime environment having Node available if execution uses node workspace or npx.
Acceptance Criteria:
  - A pinned oracle version is stored in a single source-of-truth file (example: `tools/oracle-version.txt`).
  - oraclepack execution uses the pinned version deterministically (no implicit “latest”).
  - `.state.json` and/or `.report.json` emitted by a run includes the oracle version used.
Priority & Severity (if inferable from input text):
  - Priority: Not provided
  - Severity: Not provided
Source:
  - “pins `@steipete/oracle`… runs that version deterministically”
  - “logs/records the exact `oracle` version used into its `.state.json`/`.report.json`”
  - “Vendor via Node workspace… or run `npx -y @steipete/oracle@<pinned>`”
```

```ticket T2
T# Title: Add pass-through forwarding for upstream oracle args/flags to reduce wrapper coupling
Type: enhancement
Target Area: CLI wrapper interface (oraclepack → oracle invocation)
Summary:
  Reduce breakage risk by defaulting to a “pass-through” model where oraclepack forwards custom args/flags to upstream oracle unchanged. Only model/enumerate upstream flags where oraclepack explicitly needs structured UX (e.g., an overrides wizard).
In Scope:
  - Provide a mechanism for users/config to supply freeform upstream args/flags that oraclepack forwards to oracle unchanged.
  - Define/implement the boundary: which flags are wrapper-owned vs upstream pass-through.
  - Keep modeled/enumerated upstream flags limited to where explicitly required (example given: Overrides Wizard).
Out of Scope:
  - Auto-discovery of upstream flags/commands for modeled UX (handled in T3).
Current Behavior (Actual):
  - Not provided
Expected Behavior:
  - oraclepack can forward arbitrary upstream oracle args/flags without needing wrapper updates for each new upstream option.
  - oraclepack only hard-couples to upstream flag semantics where required for wrapper UX.
Reproduction Steps:
  - Not provided
Requirements / Constraints:
  - Not provided
Evidence:
  - Not provided
Open Items / Unknowns:
  - Current oraclepack surface area that models upstream flags vs forwards arguments (not provided).
  - “Overrides Wizard” location/implementation details in oraclepack (not provided).
Risks / Dependencies:
  - Not provided
Acceptance Criteria:
  - A user-visible way exists to pass through upstream oracle args/flags unchanged.
  - Wrapper-owned flags are not silently forwarded (clear separation in behavior/docs/help output).
  - Existing wrapper features that depend on specific upstream flags continue to function as before.
Priority & Severity (if inferable from input text):
  - Priority: Not provided
  - Severity: Not provided
Source:
  - “Reduce ‘parity surface area’… Prefer: ‘pass-through’ custom args/flags… forwards… unchanged”
  - “Only model/enumerate upstream flags where you must (e.g., your Overrides Wizard)”
```

```ticket T3
T# Title: Auto-discover upstream oracle CLI flags/commands used by modeled wrapper UX (avoid hardcoding)
Type: enhancement
Target Area: Wrapper UX that enumerates upstream options (e.g., Overrides Wizard)
Summary:
  Where oraclepack must enumerate upstream oracle flags/commands for structured UX, implement auto-discovery so upstream additions/changes do not require manual hardcoded updates. This specifically targets the stated risk of wrappers breaking when they re-model upstream too strictly.
In Scope:
  - Implement auto-discovery of upstream CLI surface for any wrapper UX that enumerates upstream flags/commands.
  - Ensure the enumerated list updates when the pinned upstream oracle version changes (ties to T1).
Out of Scope:
  - General pass-through forwarding (handled in T2).
Current Behavior (Actual):
  - Not provided
Expected Behavior:
  - Wrapper UX that enumerates upstream options derives its list from upstream oracle, not a hardcoded list.
Reproduction Steps:
  - Not provided
Requirements / Constraints:
  - Not provided
Evidence:
  - Not provided
Open Items / Unknowns:
  - Which wrapper flows enumerate upstream options (only “Overrides Wizard” is referenced; details not provided).
  - The discovery mechanism source (help output vs another interface) is not specified.
Risks / Dependencies:
  - Depends on T1 for a stable/pinned upstream version target during discovery.
Acceptance Criteria:
  - For any wrapper UX that enumerates upstream flags/commands, the list is derived from upstream oracle rather than hardcoded constants.
  - Updating the pinned upstream oracle version results in the enumerated UX reflecting new/changed upstream options without manual list edits.
Priority & Severity (if inferable from input text):
  - Priority: Not provided
  - Severity: Not provided
Source:
  - “If you do enumerate, auto-discover rather than hardcode”
  - “The main way wrappers break is when they re-model upstream flags/commands too strictly”
```

```ticket T4
T# Title: Add contract test suite for upstream oracle CLI surface changes (snapshot + golden path)
Type: tests
Target Area: CI tests / compatibility checks between oraclepack and upstream oracle
Summary:
  Add tests that fail when upstream oracle changes in ways that could break oraclepack. Include snapshot diffs for help output and a small “golden path” integration fixture that asserts on stable, wrapper-owned artifacts.
In Scope:
  - Snapshot tests capturing `oracle --help` output and `oracle <critical-subcommand> --help` output for a small set of relied-upon commands, then diff snapshots in CI.
  - A “golden path” test that runs a small pack fixture to exercise oraclepack integration points (dry-run path; API path if keys exist; browser path optionally skipped in CI as described).
  - Assertions focus on wrapper-owned artifacts (exit codes, oraclepack report/state schema) rather than upstream human text output.
Out of Scope:
  - Automation that updates the pinned upstream version (handled in T5/T6).
Current Behavior (Actual):
  - Not provided
Expected Behavior:
  - CI provides early signal when upstream oracle help/CLI surface changes.
  - CI validates at least one end-to-end wrapper “golden path” behavior against the pinned upstream version.
Reproduction Steps:
  - Not provided
Requirements / Constraints:
  - Tests should target the pinned upstream oracle version (deterministic) rather than “latest”.
Evidence:
  - Not provided
Open Items / Unknowns:
  - The “critical subcommands” list oraclepack relies on (examples referenced, but exact dependency set not provided).
  - Where oraclepack stores `.state.json`/`.report.json` and current schema stability guarantees (not provided).
Risks / Dependencies:
  - Depends on T1 for deterministic upstream version selection in tests.
Acceptance Criteria:
  - Snapshot tests exist for `oracle --help` and at least one `oracle <subcommand> --help`, and are executed in CI with diffs detected.
  - A runnable golden-path fixture exists and asserts only on wrapper-owned artifacts (exit code and/or `.state.json`/`.report.json`).
[TRUNCATED]
```

.tickets/Oraclepack Schema Approach.md
```
Parent Ticket:

* Title: Adopt a schema-driven approach to prevent oraclepack run failures
* Summary:

  * Current runs fail because structure is inferred from Markdown heuristics (e.g., exactly one ```bash fence, sequential step headers, exactly 20 steps).
  * Proposal: generate a machine-validated **manifest** (JSON Schema) and **deterministically render** the Markdown pack; optionally add stricter linting for Markdown-only packs.
* Source:

  * Link/ID (if present) or “Not provided”
  * Original ticket excerpt (≤25 words) capturing the overall theme

    * “separate ‘machine-validated structure’ from ‘human-readable Markdown’ … generate only a JSON manifest … then a deterministic renderer produces the Markdown pack.”
* Global Constraints:

  * Keep existing oraclepack Markdown contract / backward-compatible (“keep the existing Markdown contract for oraclepack execution”).
  * Steps must be exactly 20; step IDs must be sequential 01..20.
* Global Environment:

  * Unknown
* Global Evidence:

  * Error text: “invalid pack structure: no bash code block found”.
  * Pack constraints referenced: “Exactly one ```bash fence”, “Exactly 20 steps”, “sequential step numbers”.

Split Plan:

* Coverage Map:

  * Original item: “separate ‘machine-validated structure’ from ‘human-readable Markdown.’”

    * Assigned Ticket ID: T1
  * Original item: “AI generates only a JSON manifest that must validate against a JSON Schema; renderer produces Markdown pack.”

    * Assigned Ticket ID: T1
  * Original item: “Prevent missing/multiple ```bash fences (root cause of ‘invalid pack structure: no bash code block found’).”

    * Assigned Ticket ID: T3
  * Original item: “Prevent non-sequential steps (Go validator requires sequential step numbers).”

    * Assigned Ticket ID: T1
  * Original item: “Prevent wrong step count (enforce exactly 20 in schema).”

    * Assigned Ticket ID: T1
  * Original item: “Minimal ‘Pack Manifest v1’ JSON Schema (Draft 2020-12) with schema_version/kind/out_dir/write_output/steps; step fields id/title/bash plus roi/impact/confidence/effort/horizon/category/reference.”

    * Assigned Ticket ID: T1
  * Original item: “Rendering rule (deterministic): one ```bash fence; prelude out_dir=…; optional --write-output; each step ‘# 01) …’ with body.”

    * Assigned Ticket ID: T2
  * Original item: “If Markdown-only: add explicit schema/lint mode (exactly one ```bash fence; exactly 20 steps; sequential 01..20; optional header tokens).”

    * Assigned Ticket ID: T3
  * Original item: “Stage-2 directory contract: exactly one file per prefix 01-*.md … 20-*.md.”

    * Assigned Ticket ID: T3
  * Original item: “Action pack lint (Stage 3): one ```bash fence; enforce 01..20 exact count.”

    * Assigned Ticket ID: T3
  * Original item: “CI checks: validate(manifest.json) → render(pack.md) → oraclepack validate pack.md → (optional) dry-run checks.”

    * Assigned Ticket ID: T4
* Dependencies:

  * T2 depends on T1 because the renderer needs the validated “Pack Manifest v1” structure as input.
  * T4 depends on T1 and T2 because CI runs “validate(manifest.json) → render(pack.md)”.
* Split Tickets:

```ticket T1
T# Title: Define and validate “Pack Manifest v1” schema (manifest-first)
Type: chore
Target Area: Pack authoring contract (manifest JSON + JSON Schema validation)
Summary:
- Introduce a manifest-first source of truth: the AI produces a JSON manifest that must validate against a JSON Schema.
- The schema enforces step count (exactly 20) and step IDs (01..20) to prevent structural runner failures.
- This separates machine-validated structure from the Markdown pack to reduce malformed packs.
In Scope:
- Define “Pack Manifest v1” JSON Schema (Draft 2020-12) with required fields: schema_version (const 1), kind (enum), out_dir, steps (min/max 20).
- Define step object constraints: required id/title/bash; id pattern for 01..20; optional roi/impact/confidence/effort/horizon/category/reference.
- Validate manifests against the schema before rendering/using them.
Out of Scope:
- Not provided
Current Behavior (Actual):
- Runner infers structure from Markdown heuristics; malformed structure can cause run-time validation errors.
- Step count and sequential numbering can be violated if not enforced early.
Expected Behavior:
- A manifest that does not conform (wrong count, wrong IDs, missing required fields) is rejected by schema validation.
- Manifests accepted by validation always contain exactly 20 steps with valid IDs and required fields.
Reproduction Steps:
1) Provide a manifest with fewer than 20 steps.
2) Provide a manifest with a non-matching step id (e.g., "21" or "1").
3) Validate manifest and confirm it fails schema validation.
Requirements / Constraints:
- schema_version must be 1.
- steps must be exactly 20 (minItems=20, maxItems=20).
- step id must match 01..20 via pattern.
Evidence:
- “the AI generates only a JSON manifest that must validate against a JSON Schema” (ticket text)
- “Wrong step count (you can enforce exactly 20 in schema rather than ‘hoping’ the model did it).”
Open Items / Unknowns:
- Exact location/path conventions for storing manifest.json are not provided.
- How/where validation is invoked (CLI, CI, library) is not provided.
Risks / Dependencies:
- Risk: keeping backward compatibility requires rendering to the existing Markdown pack contract (handled in T2).
Acceptance Criteria:
- [ ] A JSON Schema exists for “Pack Manifest v1” with required fields and constraints as described.
- [ ] A manifest with != 20 steps fails validation.
- [ ] A manifest with an invalid step id fails validation.
- [ ] A manifest missing required fields (schema_version/kind/out_dir/steps) fails validation.
Priority & Severity (if inferable from input text):
- Priority: Not provided
- Severity: Not provided
Source:
- “Manifest-first + JSON Schema (then render Markdown)”
- “minItems: 20, maxItems: 20”
- “id … pattern: ^(0[1-9]|1[0-9]|20)$”
```

````ticket T2
T# Title: Implement deterministic renderer from manifest → oraclepack Markdown pack
Type: enhancement
Target Area: Pack rendering (manifest → Markdown pack)
Summary:
- Add a deterministic rendering rule that converts a validated manifest into a Markdown pack that always satisfies oraclepack’s structural expectations.
- This prevents issues like missing/multiple bash fences and malformed step formatting by making Markdown a compiled artifact.
In Scope:
- Render exactly one fenced code block labeled `bash` in the entire document.
- Render prelude lines including: `out_dir="..."` and optional `--write-output` as described.
- Render each step with header `# NN) ...` and step body from `bash` content.
Out of Scope:
- Not provided
Current Behavior (Actual):
- Markdown is the primary artifact; structure can be malformed by generation, causing downstream failures.
Expected Behavior:
- Renderer output always includes exactly one `bash` fence and emits all 20 steps in order.
- Pack contains the required prelude line(s) described in the ticket text.
Reproduction Steps:
1) Validate a manifest (per T1).
2) Render to Markdown.
3) Confirm output contains exactly one `bash` fence and step headers for 01..20 in sequence.
Requirements / Constraints:
- Output must be runner-ingestible per the described structural rules (single bash fence, 20 steps, sequential).
Evidence:
- “Rendering rule (deterministic) … emits exactly: one ```bash fence … prelude lines … then each step: # 01) … Step body = bash”
Open Items / Unknowns:
- Exact step title formatting beyond “# NN) …” is not provided.
- Whether additional header tokens (ROI=…) are required at render time is not provided.
Risks / Dependencies:
- Depends on T1 (renderer assumes manifest structure/constraints).
Acceptance Criteria:
- [ ] Given a valid manifest, renderer produces a Markdown pack with exactly one ```bash fenced block.
- [ ] Output contains 20 step headers numbered 01..20 in order.
- [ ] Output includes the `out_dir="..."` prelude line.
- [ ] Renderer can conditionally include the optional `--write-output` line when present in the manifest.
Priority & Severity (if inferable from input text):
- Priority: Not provided
- Severity: Not provided
Source:
- “From this manifest, your renderer emits exactly: One ```bash fence”
- “Prelude lines: out_dir="..." … optional --write-output”
- “Then each step: # 01) … Step body = bash”
````

````ticket T3
T# Title: Add stricter lint/validation for Markdown-only packs and Stage-2/Stage-3 outputs
Type: chore
Target Area: Pack linting/validation (Markdown packs + output directory contract)
Summary:
- If the project keeps Markdown-only as a supported input path, add an explicit lint/validation mode that enforces the same structural contract.
- Extend checks to Stage-2 output directory naming expectations and Stage-3 action pack constraints to reduce “runner infers structure” failures.
In Scope:
- Pack-level lint (Stage 1) enforcing:
  - Exactly one ```bash fence.
  - Exactly 20 steps.
  - Step IDs exactly 01..20 and sequential.
  - Optional enforcement of required header tokens (ROI= impact= confidence= … reference=) as described.
- Stage-2 directory contract lint:
  - Exactly one file per prefix 01-*.md … 20-*.md.
- Stage-3 action pack lint:
  - Exactly one ```bash fence.
  - Enforce 01..20 and exact count.
Out of Scope:
- Not provided
Current Behavior (Actual):
- Common failure mode noted: “invalid pack structure: no bash code block found.”
- Existing checks may be incomplete (“your current check only ensures ‘some’ step headers exist” per ticket text).
Expected Behavior:
- Markdown packs that violate the contract are rejected early with lint errors before execution.
- Stage-2 outputs and Stage-3 action packs are validated against exact-count and naming/structure rules.
Reproduction Steps:
1) Create a Markdown pack with no `bash` fenced block → lint should fail.
2) Create a Markdown pack with 19 steps or non-sequential IDs → lint should fail.
3) Create an output directory missing `07-*.md` or containing duplicates for a prefix → lint should fail.
Requirements / Constraints:
- Enforce: one ```bash fence, exactly 20 steps, sequential 01..20.
Evidence:
- “Missing / multiple ```bash fences (common root cause of ‘invalid pack structure: no bash code block found’).”
- “Add an explicit schema/lint mode … Exactly one ```bash fence … Exactly 20 steps … Step IDs exactly 01..20”
- “Stage-2 directory contract … Exactly one file per prefix 01-*.md … 20-*.md”
- “Action pack lint (Stage 3) … Enforce 01..20 and exact count”
Open Items / Unknowns:
- Exact current validator behaviors and what already exists vs missing are not provided.
Risks / Dependencies:
- Risk: enforcing optional header tokens could break existing packs if not already standardized.
Acceptance Criteria:
- [ ] Lint fails when no `bash` fence exists and surfaces a clear error.
- [ ] Lint fails when step count != 20.
- [ ] Lint fails when step IDs are not exactly 01..20 sequential.
- [ ] Stage-2 lint fails when any step output prefix 01..20 is missing or duplicated.
- [ ] Stage-3 lint fails when action pack does not have exactly one `bash` fence or correct 01..20 steps.
Priority & Severity (if inferable from input text):
- Priority: Not provided
- Severity: Not provided
Source:
- “invalid pack structure: no bash code block found”
- “Pack-level lint (Stage 1) … Exactly one ```bash fence … Exactly 20 steps”
- “Stage-2 directory contract … Exactly one file per prefix 01-*.md … 20-*.md”
````

```ticket T4
T# Title: Add CI validation pipeline for manifest-first workflow (validate → render → oraclepack validate → optional dry-run)
Type: chore
Target Area: CI checks / pipeline gating
Summary:
- Add CI checks that gate merges/runs on structural correctness by validating the manifest, rendering Markdown deterministically, and validating the rendered pack with oraclepack tooling.
- This formalizes the “Markdown is compiled artifact” approach and reduces runtime surprises.
In Scope:
- CI sequence as described:
  - validate(manifest.json)
  - render(pack.md)
  - oraclepack validate pack.md
  - optional dry-run checks
Out of Scope:
- Not provided
Current Behavior (Actual):
- Pack structural issues can slip into execution time if not validated earlier.
Expected Behavior:
- CI fails fast when manifest validation fails or rendered pack fails oraclepack validation.
Reproduction Steps:
1) Commit a manifest with 19 steps; CI should fail at validate(manifest.json).
2) Commit a manifest that renders an invalid pack (if possible); CI should fail at oraclepack validate.
Requirements / Constraints:
[TRUNCATED]
```

.tickets/Oraclepack bash fix.md
```
Parent Ticket:

* Title: Prevent oraclepack pack failures caused by orphaned `-p/--prompt` lines in generated bash steps
* Summary: Generated oraclepack markdown packs can emit a multiline `oracle ...` command where `-p "$(cat <<'PROMPT' ...)"` starts on a new line without a continuation, causing Bash to treat `-p` as a standalone command and fail (`exit status 127`). The fix requires making pack generation structurally safe and adding validator guardrails that fail fast on regressions.
* Source:

  * Link/ID: Bash command syntax fix.md
  * Original ticket excerpt (≤25 words) capturing the overall theme: “make the generator/template structurally unable to emit orphaned flags… and make oraclepack validate fail fast”
* Global Constraints:

  * “never put `-p/--prompt` (or any flag) on its own line”
  * “no inline comments at end of an `oracle ...` line”
* Global Environment:

  * Unknown
* Global Evidence:

  * Error: `bash: line 59: -p: command not found` + `exit status 127`
  * Reference pack: `oracle-pack-2026-01-08-tickets-direct.md` (pattern repeated across steps)

Split Plan:

* Coverage Map:

  * “`bash: line 59: -p: command not found` + `exit status 127`” → T1
  * “`-p "$(cat <<'PROMPT' ...)"` is on the next line without `\` … repeated in others” → T1
  * “Minimal fix: add a continuation backslash, or put `-p` on the same line” → T1
  * “Optional comment goes ABOVE the command, not inline” → T1
  * “Wherever you render `oracle ... "${ticket_args[@]}" # extra_files ...` then newline then `-p ...` … change it” → T1
  * “Permanent template rule: never put `-p/--prompt` on its own line; build prompt first, then call oracle on a single command line” → T1
  * “Enforce: no inline comments at end of an `oracle ...` line” → T1
  * “If you must wrap long lines, require explicit `\` continuations and disallow comments on continued lines” → T1
  * “Add checks to `oraclepack validate` after extracting the single `bash` fence” → T2
  * “Add `bash -n` syntax check” → T2
  * “Add `shellcheck` static analysis” → T2
  * “Custom ‘orphaned flag line’ detector (regex + continuation exceptions)” → T2
  * “Ensure `oraclepack run` always calls `validate` first (or at minimum in TUI Run/Rerun paths)” → T3
  * “Add CI/pre-commit: run `oraclepack validate` on any generated/modified pack” → T3
  * “Offer: point at exact rendering pattern + canonical snippet” → Non-actionable / Info-only
* Dependencies:

  * T3 depends on T2 because “Make validate unavoidable” is intended to enforce the added validator checks that catch regressions.
* Split Tickets:

```ticket T1
T# Title: Make pack generation structurally safe (no orphaned `-p/--prompt` lines)
Type: bug
Target Area: Pack generator/template that emits oraclepack Markdown steps (tickets-direct pack generation)
Summary:
- Generated packs can split an `oracle ...` invocation across lines such that `-p "$(cat <<'PROMPT' ...)"` starts on a new line without a continuation.
- Bash then executes `-p` as a standalone command, causing `command not found` and `exit status 127`.
- Update generation patterns so prompts are built safely and the `oracle` command remains a single logical command (or uses correct continuations without inline comment footguns).
In Scope:
- Eliminate multiline `oracle` invocations that place `-p/--prompt` on its own line.
- Apply the “minimal fix” pattern where multiline is unavoidable: add a line-continuation `\` (and ensure comments do not break continuation).
- Enforce generator rule: no inline trailing comments on `oracle ...` lines (comments/newlines can terminate the command unexpectedly).
- Adopt canonical “build prompt first, then call oracle” step shape as the standard emission pattern.
Out of Scope:
- Not provided
Current Behavior (Actual):
- `oracle ...` command is terminated by a newline, then `-p "$(cat <<'PROMPT' ...)"` appears on the next line without `\`, so Bash treats `-p` as a command and fails.
Expected Behavior:
- Generated bash steps never emit orphaned flag lines (e.g., `-p`, `-f`, `--prompt`) that can be interpreted as standalone shell commands.
- Generated `oracle` invocations are either a single logical line or correctly continued (without inline comments breaking continuation).
Reproduction Steps:
1. Run the generated pack `oracle-pack-2026-01-08-tickets-direct.md`.
2. Observe the step where `-p` begins a new line without a continuation and the shell errors.
Requirements / Constraints:
- “never put `-p/--prompt` (or any flag) on its own line”
- “no inline comments at end of an `oracle ...` line”
- If wrapping is necessary: require explicit `\` continuations and disallow comments on continued lines.
Evidence:
- Error: `bash: line 59: -p: command not found` + `exit status 127`
- Pattern described: `-p "$(cat <<'PROMPT' ...)"` on next line without `\` in `oracle-pack-2026-01-08-tickets-direct.md`
Open Items / Unknowns:
- Exact location(s) of the emitting template(s): Unknown / Not provided
- Whether multiple generators/templates emit the pattern beyond tickets-direct: Unknown / Not provided
Risks / Dependencies:
- Risk: Partial fixes (only adding `\`) may regress if inline comments or formatting are reintroduced.
Acceptance Criteria:
- Generated packs do not contain any step where a line begins with `-p` / `--prompt` intended as a continuation of `oracle` without an explicit safe structure.
- Running the regenerated tickets-direct pack no longer produces `-p: command not found` / `exit status 127` for the previously failing steps.
- Generator output follows one of:
  - prompt built first + `oracle ... --prompt "$prompt"` as a single logical command, OR
  - explicit `\` continuation with no inline trailing comments on continued lines.
Priority & Severity (if inferable from input text):
- Priority: Not provided
- Severity: Not provided
Source:
- “`bash: line 59: -p: command not found` + `exit status 127`”
- “the `-p "$(cat <<'PROMPT' ...)"` part is on the next line without a line-continuation (`\`)”
- “Permanent template rule: never put `-p/--prompt` (or any flag) on its own line”
```

```ticket T2
T# Title: Add validator guardrails (bash-lint + orphaned-flag detection) to fail fast
Type: enhancement
Target Area: `oraclepack validate` (after extracting the single `bash` fence)
Summary:
- Even with a safer generator, regressions can reintroduce orphaned flag lines that only fail at runtime.
- Add validation checks that detect bash syntax issues and the specific “orphaned flag line” class before execution.
- Validation should clearly fail on suspicious standalone flag lines unless safely continued.
In Scope:
- Run `bash -n` against the extracted bash step script(s) as a syntax sanity check.
- Run `shellcheck` static analysis on the extracted bash script(s).
- Implement the custom “orphaned flag line” detector:
  - Fail if a non-heredoc line matches `^\s*-(p|f)\b` or `^\s*--(prompt|file|write-output)\b`
  - Unless the previous non-empty line ends with a legal continuation (`\`, `|`, `&&`, `||`, `(`, etc.)
Out of Scope:
- Making `validate` mandatory in all run paths (handled separately)
Current Behavior (Actual):
- Not provided
Expected Behavior:
- `oraclepack validate` fails fast with a clear error when a pack contains likely orphaned flag lines (e.g., `-p ...`) outside permitted continuation contexts.
- `oraclepack validate` reports bash syntax issues before execution.
Reproduction Steps:
1. Create/modify a pack step where `-p` is on its own line without a valid continuation.
2. Run `oraclepack validate`.
Requirements / Constraints:
- Checks are added “after extracting the single `bash` fence”.
- Orphaned-flag detector must ignore heredoc bodies (“non-heredoc line”).
Evidence:
- “Add these checks to `oraclepack validate` after extracting the single `bash` fence”
- Detector specification (regex + continuation exceptions) provided in ticket text.
Open Items / Unknowns:
- Availability/installation method for `shellcheck` in the execution environment: Unknown / Not provided
- Exact current structure of `oraclepack validate` and how it extracts bash fence: Unknown / Not provided
Risks / Dependencies:
- Risk: False positives if continuation heuristics are too strict; must match the specified allowed continuations.
Acceptance Criteria:
- `oraclepack validate` includes `bash -n` and fails on invalid bash syntax in the extracted script(s).
- `oraclepack validate` runs `shellcheck` and surfaces failures per project policy (pass/fail behavior not specified in ticket text).
- `oraclepack validate` fails when a non-heredoc line begins with `-p`, `-f`, `--prompt`, `--file`, or `--write-output` and the previous non-empty line does not end with an allowed continuation token.
- `oraclepack validate` does not falsely flag valid heredoc prompt bodies.
Priority & Severity (if inferable from input text):
- Priority: Not provided
- Severity: Not provided
Source:
- “Add these checks to `oraclepack validate` after extracting the single `bash` fence”
- “`bash -n` syntax check (cheap sanity)”
- “Custom ‘orphaned flag line’ detector… `^\s*-(p|f)\b` … unless… ends with… (`\`, `|`, `&&`, `||`, `(`, etc.)”
```

```ticket T3
T# Title: Make validation unavoidable in normal use (run/TUI) and add CI/pre-commit gate
Type: chore
Target Area: `oraclepack run` execution flow, TUI “Run/Rerun” paths, and repo automation (CI/pre-commit)
Summary:
- Validation guardrails are only effective if they run consistently before pack execution.
- Ensure `oraclepack run` calls `validate` first (or at minimum in TUI Run/Rerun paths).
- Add automated gating so modified/generated packs are validated before being executed/merged.
In Scope:
- Ensure `oraclepack run` always calls `validate` first.
- Ensure TUI “Run/Rerun” paths invoke `validate` first (at minimum).
- Add CI/pre-commit step to run `oraclepack validate` on generated/modified packs.
Out of Scope:
- Implementing the validator checks themselves (handled separately)
Current Behavior (Actual):
- Not provided
Expected Behavior:
- Running a pack via CLI or TUI triggers validation first, preventing execution of invalid packs.
- CI/pre-commit blocks changes that introduce invalid pack structure detectable by `oraclepack validate`.
Reproduction Steps:
1. Introduce a known-invalid pattern (e.g., orphaned `-p` line) into a pack.
2. Attempt to run via `oraclepack run` and via TUI Run/Rerun.
3. Attempt to commit/CI-run with the invalid pack present.
Requirements / Constraints:
- “Ensure `oraclepack run` always calls `validate` first (or at minimum in TUI ‘Run/Rerun’ paths).”
- “Add CI/pre-commit: run `oraclepack validate` on any generated/modified pack.”
Evidence:
- The ticket text specifies making validation unavoidable and adding CI/pre-commit gating.
Open Items / Unknowns:
- Existing CI/pre-commit tooling and where to hook validation: Unknown / Not provided
- Exact TUI entrypoints for Run/Rerun: Unknown / Not provided
Risks / Dependencies:
- Depends on `oraclepack validate` providing the intended guardrails to justify making it mandatory.
Acceptance Criteria:
- `oraclepack run` invokes `validate` before executing pack steps.
- TUI Run/Rerun paths invoke `validate` before execution (at minimum).
- CI/pre-commit configuration exists to run `oraclepack validate` on generated/modified packs and fails on validation errors.
Priority & Severity (if inferable from input text):
- Priority: Not provided
- Severity: Not provided
Source:
- “Make validate unavoidable in normal use”
- “Ensure `oraclepack run` always calls `validate` first (or at minimum in TUI ‘Run/Rerun’ paths).”
- “Add CI/pre-commit: run `oraclepack validate` on any generated/modified pack.”
```
```

.tickets/Oraclepack output verification issues.md
```
Parent Ticket:

* Title: oraclepack output verification failures and verification configurability gaps
* Summary: Runs fail with `output verification failed for step 01` due to oraclepack’s post-step validator requiring section tokens (triggered by “Answer format”) that are not present in the `--write-output` file, plus cases where the output file is not created/readable on retry. The ticket also includes requests to make verification easier to disable/configure (including via `.env`) and to support “verify later” workflows.
* Source:

  * Link/ID: Not provided
  * Original ticket excerpt (≤25 words) capturing the overall theme: “output verification failed for step 01 … verifier is expecting more sections than you are actually emitting.”
* Global Constraints:

  * Do not add “wait time” as a validator fix when the failure is due to content/contract mismatch.
* Global Environment:

  * Unknown
* Global Evidence:

  * Error context: “output verification failed for step 01: docs/…” and validator behavior around “Answer format” token requirements.
  * Noted token set required when “Answer format” is present: “direct answer”, “risks unknowns”, “next smallest concrete experiment”, “if evidence is insufficient”.
  * Suggested multi-file suffix scheme: `-direct-answer`, `-risks-unknowns`, `-next-experiment`, `-missing-evidence`.

Split Plan:

* Coverage Map:

  * “oraclepack’s ‘output verification’ path … expects the file to exist and … contain certain section tokens” → T1
  * “single `--write-output` + phrase ‘Answer format’ causes verifier to require all four section tokens” → T1
  * “If your step instructs ‘Return only: Direct answer’ … verification fails” → T1
  * “On the re-run, the output file isn’t being written at all” → T4
  * “Don’t add a longer wait time … step command already blocks until CLI exits” → Info-only
  * “If oracle browser run is incomplete/truncated … increase oracle `--browser-timeout` / `--browser-input-timeout` or switch to API” → T4
  * “How to make the validator optional … `--output-verify=false` and `--output-retries`” → T4
  * “Align chunking with verifier … split outputs into multiple `--write-output` files using suffixes” → T4
  * “Edge: ‘Missing evidence’ heading may fail if required token is literally ‘If evidence is insufficient’” → T4
  * “Add ‘verify later’ workflow: new `oraclepack verify-outputs <pack.md>` command” → T3
  * “No built-in env var toggle for output verification; toggle is CLI flag today” → T2
  * “Option: wrapper script to inject `--output-verify=false`” → T2
  * “Option: add explicit env var support like `ORACLEPACK_OUTPUT_VERIFY` / `ORACLEPACK_OUTPUT_RETRIES`” → T2
  * “Which skill produced the pack … matches oraclepack-gold-pack (Gold Stage 1)” → Info-only
* Dependencies:

  * Not provided
* Split Tickets:

```ticket T1
T1 Title: Make output verification contract-aware for “Direct answer only” single-output steps
Type: enhancement
Target Area: oraclepack run-step output verification (single `--write-output` expectations)
Summary:
  The validator currently requires four section tokens whenever the step text includes “Answer format”, which fails when the produced output contains only “Direct answer” (e.g., when the prompt says “Return only: Direct answer”). Adjust validation so a single-output step can be treated as “direct answer only” when the step explicitly declares that constraint, avoiding false failures.
In Scope:
  - Update expectation logic so “Answer format” does not always imply all four tokens for single-output steps.
  - Support a “Direct answer only” contract when the step text explicitly indicates that output constraint (e.g., “Return only: Direct answer”).
Out of Scope:
  - Not provided
Current Behavior (Actual):
  - If a step contains “Answer format” and has exactly one `--write-output`, the output file is required to contain all of:
    - direct answer
    - risks unknowns
    - next smallest concrete experiment
    - if evidence is insufficient
  - Steps that output only “Direct answer …” fail verification.
Expected Behavior:
  - If a step explicitly constrains output to “Direct answer only”, the validator should not require the other three section tokens for that step’s single `--write-output` file.
  - Steps that truly require all four sections should continue to be validated against all four tokens.
Reproduction Steps:
  1) Run a step with a prompt that includes “Answer format:” and also includes “Return only: Direct answer”.
  2) Produce an output file containing only the “Direct answer” section.
  3) Observe `output verification failed for step 01` due to missing tokens.
Requirements / Constraints:
  - Must preserve existing “all four tokens required” behavior when the step is not explicitly “Direct answer only”.
Evidence:
  - Error symptom: `output verification failed for step 01: docs/...`
  - Required tokens set when “Answer format” is present: “direct answer”, “risks unknowns”, “next smallest concrete experiment”, “if evidence is insufficient”.
Open Items / Unknowns:
  - Exact mechanism used today to detect “Answer format” and to compute required tokens (unknown in provided text).
Risks / Dependencies:
  - Not provided
Acceptance Criteria:
  - A single-output step that includes “Answer format” but explicitly states “Return only: Direct answer” passes verification when the file contains “Direct answer” and omits the other sections.
  - A single-output step that includes “Answer format” without “Direct answer only” constraint continues to fail verification if any of the other required tokens are missing.
Priority & Severity (if inferable from input text):
  - Priority: Not provided
  - Severity: Not provided
Source:
  - “single `--write-output ...` + a prompt that includes the phrase ‘Answer format’ causes the verifier to require all four section tokens”
  - “If your step instructs ‘Return only: Direct answer’ … verification fails”
  - “change … verification logic to accept a ‘direct answer only’ contract”
```

```ticket T2
T2 Title: Add `.env` / environment-variable control for output verification defaults
Type: enhancement
Target Area: oraclepack CLI configuration (env var support for `--output-verify` / `--output-retries`)
Summary:
  Output verification can be toggled via CLI flags, but there is no built-in environment variable that can be set in `.env` to control verification on/off. Add explicit env-var support (as suggested) to allow `.env` driven defaults, while keeping CLI flags available.
In Scope:
  - Implement an environment variable toggle for output verification (suggested name: `ORACLEPACK_OUTPUT_VERIFY`).
  - Implement an environment variable for output retries (suggested name: `ORACLEPACK_OUTPUT_RETRIES`).
  - Define precedence so the env vars provide defaults without removing CLI flag control (exact precedence not specified in provided text; implement per existing CLI config patterns).
Out of Scope:
  - Not provided
Current Behavior (Actual):
  - Output verification is toggled by CLI flag (`--output-verify`), not an env var.
  - No built-in `.env` variable exists to turn verification on/off.
Expected Behavior:
  - Users can set a `.env` environment variable to control output verification behavior without modifying command lines everywhere.
Reproduction Steps:
  1) Attempt to set an env var in `.env` to disable output verification.
  2) Observe there is “no built-in environment variable in the Go `oraclepack` CLI to toggle output verification.”
Requirements / Constraints:
  - Must not remove existing CLI flag support for `--output-verify` / `--output-retries`.
Evidence:
  - “There is no built-in environment variable … The toggle is currently a CLI flag (`--output-verify`), not an env var.”
  - Suggested addition: “Implement something like `ORACLEPACK_OUTPUT_VERIFY=0/1` (and optionally `ORACLEPACK_OUTPUT_RETRIES`).”
Open Items / Unknowns:
  - Current CLI config/env binding conventions in the Go CLI (unknown in provided text).
Risks / Dependencies:
  - Not provided
Acceptance Criteria:
  - Setting `ORACLEPACK_OUTPUT_VERIFY=0` results in verification being disabled by default for `oraclepack run` when the user does not pass an explicit `--output-verify` flag.
  - Setting `ORACLEPACK_OUTPUT_VERIFY=1` results in verification being enabled by default under the same conditions.
  - Setting `ORACLEPACK_OUTPUT_RETRIES=<N>` results in the default retries being `<N>` when the user does not pass an explicit `--output-retries` flag.
Priority & Severity (if inferable from input text):
  - Priority: Not provided
  - Severity: Not provided
Source:
  - “what environment variable can we set in our .env to turn the verify output on/off?”
  - “There is no built-in environment variable … The toggle is currently a CLI flag (`--output-verify`), not an env var.”
  - “Implement something like `ORACLEPACK_OUTPUT_VERIFY=0/1` (and optionally `ORACLEPACK_OUTPUT_RETRIES`).”
```

```ticket T3
T3 Title: Add a standalone `verify-outputs` command to validate pack outputs without executing steps
Type: enhancement
Target Area: oraclepack CLI commands (output verification workflow)
Summary:
  Disabling output verification allows runs to proceed, but then verification is tied to execution and won’t be re-applied later (e.g., with `--resume`). Add a command that only validates outputs using the same expectation logic, enabling “run now, verify later” workflows.
In Scope:
  - Add a command: `oraclepack verify-outputs <pack.md>`.
  - For each step, compute output expectations and validate the referenced `--write-output` files against required tokens.
  - Exit non-zero when validation fails and print failures (step + file + missing token set).
Out of Scope:
  - Not provided
Current Behavior (Actual):
  - Verification is performed as part of step execution; disabling verification allows success without later validation.
Expected Behavior:
  - `verify-outputs` validates outputs for a pack without executing any step commands.
Reproduction Steps:
  1) Run `oraclepack run <pack.md> --output-verify=false`.
  2) Attempt to validate outputs later without re-running steps.
  3) Observe verification is tied to execution in the current workflow.
Requirements / Constraints:
  - Must reuse the existing expectation/token normalization logic already used during execution (as described).
Evidence:
  - Suggested command: “`oraclepack verify-outputs <pack.md>` … compute `pack.StepOutputExpectations(step)` and run `pack.ValidateOutputFile(path, requiredTokens)`.”
Open Items / Unknowns:
  - Exact function names/locations of expectation and validation logic in the codebase (unknown in provided text).
Risks / Dependencies:
  - Not provided
Acceptance Criteria:
  - Running `oraclepack verify-outputs <pack.md>` does not execute step commands.
  - The command validates each `--write-output` file against the expected token set and reports missing tokens.
  - The command exits with a non-zero status when any step output fails validation.
Priority & Severity (if inferable from input text):
  - Priority: Not provided
  - Severity: Not provided
Source:
  - “If you want ‘run now, verify later’ … add a new command that only validates outputs without executing steps.”
  - “`oraclepack verify-outputs <pack.md>` … reuses the same expectation logic and token normalization.”
```

```ticket T4
T4 Title: Update pack/templates and documentation to avoid validator mismatches and improve reliability in browser mode
Type: docs
Target Area: pack authoring guidance (Gold Stage packs), output chunking conventions, oracle CLI invocation guidance
Summary:
  The failures are driven by mismatch between what the validator requires (triggered by “Answer format”) and what is actually written to `--write-output`, plus fragility in oracle browser automation runs. Update templates and documentation to (a) ensure outputs include required section tokens when “Answer format” is used, or (b) use multi-file chunking with the documented filename suffixes, and (c) document browser-timeout mitigations and path preconditions for `--write-output`.
In Scope:
  - Document that when “Answer format” is present, the validator requires all four tokens in a single-output file unless using chunked outputs.
  - Provide template guidance for two supported patterns:
    - Single file per step: output must include headings/phrases that satisfy all required tokens (“Direct answer”, “Risks/unknowns”, “Next smallest concrete experiment”, “If evidence is insufficient”).
    - Chunked outputs: use multiple `--write-output` files with suffixes:
      - `-direct-answer`
      - `-risks-unknowns`
      - `-next-experiment`
      - `-missing-evidence`
  - Document the “Missing evidence” vs “If evidence is insufficient” token mismatch risk (token is literal).
[TRUNCATED]
```

.tickets/Oraclepack-CLI-agents.md
```

In Codex/Gemini CLI, the “live agent” inference is the model deciding what actions to take given the skill context (skills are discovered/activated and the model can inspect skill assets). [OpenAI Developers+2OpenAI Developers+2](https://developers.openai.com/codex/custom-prompts/?utm_source=chatgpt.com) In an MCP setup, the “live agent” inference is the model deciding which tools to call (and with what arguments) based on the tools’ schemas. [Model Context Protocol+3Model Context Protocol+3Model Context Protocol+3](https://modelcontextprotocol.io/specification/2025-11-25?utm_source=chatgpt.com)

Below is an “extensive” set of additional, unique LLM decision points you can introduce (or explicitly formalize) beyond “classify tickets/domains then feed into grouping”.

| ID | Stage | Class | LLM decision point | Inputs considered | Output / action produced |
| --- | --- | --- | --- | --- | --- |
| DP-01 | Pre-gen | Routing | Choose generator mode (tickets-grouped vs codebase-grouped vs gold) | User goal, repo state, available skills/tools | Selected generator + params |
| DP-02 | Pre-gen | Scope | Select root(s) to scan (ticket\_root/code\_root) | Working dir tree, config defaults | Root paths |
| DP-03 | Pre-gen | Scope | Decide include/exclude glob rules | Repo conventions, noise directories | Include/exclude patterns |
| DP-04 | Pre-gen | Scope | Decide whether to treat “loose” items as first-class candidates | Ticket placement quality, density | Loose-ticket policy |
| DP-05 | Pre-gen | Governance | Decide whether to require “strict schema mode” for outputs | Target environment strictness | Enforce extra validation gates |
| DP-06 | Pre-gen | Budgeting | Choose max pack size strategy (by tokens/bytes/files) | Model context limits, file sizes | Sharding plan |
| DP-07 | Pre-gen | Budgeting | Choose per-pack cap: number of tickets/files | Expected coherence vs coverage | Caps per pack |
| DP-08 | Pre-gen | Normalization | Decide ticket/file canonical title extraction rule | Noisy headings, filenames | Canonical titles for clustering |
| DP-09 | Pre-gen | Normalization | Choose text preprocessing (stopwords, stemming, code tokens) | Domain vocabulary | Feature extraction policy |
| DP-10 | Pre-gen | Dedup | Decide duplicate threshold policy (strict vs lenient) | Similarity signals | Threshold values |
| DP-11 | Pre-gen | Dedup | Decide duplicate merge strategy (merge vs link vs pick canonical) | Diff size, recency, metadata | Merge plan + canonical selection |
| DP-12 | Pre-gen | Dedup | Decide what “differences” to preserve when merging duplicates | Patch-like deltas | Delta retention format |
| DP-13 | Pre-gen | Clustering | Decide whether to use hierarchical topic taxonomy vs flat groups | Repo size, domain diversity | Taxonomy mode |
| DP-14 | Pre-gen | Clustering | Decide group count target | Ticket count, entropy | K groups target |
| DP-15 | Pre-gen | Clustering | Decide grouping algorithm choice (heuristic vs LLM-labeled vs hybrid) | Determinism needs, accuracy | Algorithm selection |
| DP-16 | Pre-gen | Clustering | Decide “ambiguous” assignment policy (multi-home vs best-fit) | Similarity ties | Assignment rule |
| DP-17 | Pre-gen | Clustering | Decide whether to create an “Unsorted / Needs triage” pack | Low-confidence items | Extra pack creation |
| DP-18 | Pre-gen | Naming | Generate group names optimized for human scanning | Group summaries | Group name strings |
| DP-19 | Pre-gen | Ordering | Decide pack order (by dependency, ROI, urgency, confidence) | Ticket metadata, heuristics | Pack sequence |
| DP-20 | Pre-gen | Ordering | Decide ticket order within pack (chronological vs dependency graph) | References among tickets | Ordered ticket list |
| DP-21 | Pre-gen | Context | Select “context bundle” files to attach per pack | Code references, paths mentioned | Attachment list |
| DP-22 | Pre-gen | Context | Decide whether to attach full files vs excerpts/summaries | Token budget, file size | Attachment granularity |
| DP-23 | Pre-gen | Context | Decide whether to generate a synthetic “pack brief” doc | Need for shared framing | Brief content |
| DP-24 | Pre-gen | Context | Decide whether to include prior run artifacts (outputs) as inputs | Existing out\_dir contents | Reuse plan |
| DP-25 | Pre-gen | Template | Choose template variant (tickets vs codebase vs mixed) | Pack type, audience | Template choice |
| DP-26 | Pre-gen | Template | Decide whether to inject organization standards into prompts (not pack shape) | Policy bundles, style rules | Prompt preamble content |
| DP-27 | Pre-gen | Template | Decide step allocation across subtopics (how many steps per subdomain) | Group composition | Step budget map |
| DP-28 | Pre-gen | Prompting | Choose prompt “stance” (audit-first vs implement-first vs design-first) | Risk, stage | Prompt style per step |
| DP-29 | Pre-gen | Prompting | Choose “evidence bar” (strict citations vs lightweight) | Audience and stakes | Evidence requirements |
| DP-30 | Pre-gen | Prompting | Decide per-step required outputs (files changed, diffs, JSON, etc.) | Tooling integration, downstream parser | Output spec per step |
| DP-31 | Pre-gen | Prompting | Decide if each step should be self-contained or rely on previous step outputs | Runtime environment, caching | Dependency policy |
| DP-32 | Pre-gen | Prompting | Decide whether to add “ask-user” gates for missing critical inputs | Missing file detection | Gate instructions in prompts |
| DP-33 | Pre-gen | Tooling | Choose which MCP tools to call during generation (list/validate/generate) | Tool availability, cost | Tool call plan [Model Context Protocol+1](https://modelcontextprotocol.io/specification/2025-06-18/server/tools?utm_source=chatgpt.com) |
| DP-34 | Pre-gen | Tooling | Decide oracle model/engine selection and parameters | Cost, latency, quality | `oracle` flags (model/temperature) |
| DP-35 | Pre-gen | Tooling | Decide whether to preflight `oracle` invocations (`--dry-run` summary) | Compatibility risk | Preflight on/off |
| DP-36 | Pre-gen | Compliance | Decide redaction policy for sensitive strings in prompts/attachments | Secrets risk | Redaction rules |
| DP-37 | Pre-gen | Observability | Decide trace/correlation ID scheme for packs/steps | Downstream logging needs | ID format + propagation plan |
| DP-38 | Pre-gen | Observability | Decide which metrics/log events must be emitted per stage | Debuggability requirements | Instrumentation checklist |
| DP-39 | Runtime | Execution planning | Decide run strategy (run-all vs selective) | Confidence map, cost | Step subset |
| DP-40 | Runtime | Execution planning | Decide concurrency / rate-limiting policy | Provider limits | Parallelism level |
| DP-41 | Runtime | Execution gating | Decide whether to halt on first failure vs continue collecting failures | Failure criticality | Fail-fast/continue |
| DP-42 | Runtime | Recovery | Decide retry policy per failure type (tool error vs content error) | Error classification | Retry plan |
| DP-43 | Runtime | Recovery | Decide “prompt patch” for retries (tighten constraints vs broaden context) | Failure analysis | Revised prompt text |
| DP-44 | Runtime | Recovery | Decide when to escalate to user for clarification | Low confidence / missing inputs | User question(s) |
| DP-45 | Runtime | Consistency | Decide whether to re-run earlier steps when later steps reveal contradictions | Cross-step inconsistency | Re-run selection |
| DP-46 | Runtime | Quality control | Decide acceptance criteria for a step output (format, completeness) | Output parsing/validation | Pass/fail verdict |
| DP-47 | Runtime | Quality control | Decide whether to auto-validate produced artifacts (lint/tests/validate) | Available checks | Validation commands |
| DP-48 | Runtime | Post-processing | Decide how to synthesize step outputs into a final report | Audience, required structure | Summary artifact |
| DP-49 | Runtime | Post-processing | Decide whether to generate PR/patch vs documentation-only output | Repo permissions, user intent | Output mode |
| DP-50 | Runtime | Post-processing | Decide how to resolve conflicting recommendations across steps | Conflicts detected | Resolution rationale |
| DP-51 | Runtime | Governance | Decide whether outputs meet policy/security standards before writing | Policy bundle | Block/allow + edits |
| DP-52 | Runtime | Caching | Decide whether to reuse cached groupings/packs vs regenerate | Cache keys, repo diffs | Cache decision |
| DP-53 | Runtime | Caching | Decide cache invalidation scope (one pack vs all) | Changed inputs | Invalidation plan |
| DP-54 | Runtime | Observability | Decide incident-style annotation (root cause, repro, next action) on failures | Error logs, outputs | Failure note artifact |
| DP-55 | Runtime | Observability | Decide what artifacts to persist (manifests, intermediate summaries) | Debug needs | Persist list |
| DP-56 | Runtime | Packaging | Decide how to shard outputs into “mini-packs” for follow-on runs | Token limits, dependencies | Mini-pack plan |
| DP-57 | Runtime | Packaging | Decide naming/versioning for generated packs | Date, domain, stability | Pack names + version tags |
| DP-58 | Runtime | UX | Decide “next best tool call” in MCP (validate vs list vs run vs regenerate) | Current state | Tool invocation [Model Context Protocol+1](https://modelcontextprotocol.io/docs/develop/build-server?utm_source=chatgpt.com) |
| DP-59 | Runtime | UX | Decide whether to present diffs, file lists, or narrative only | Reviewer preference | Presentation format |
| DP-60 | Runtime | Learning loop | Decide whether to extract new org heuristics from this run into a reusable profile | Repeated patterns | Proposed profile snippet |

[TRUNCATED]
```

.tickets/Publish OraclePack MCP.md
```
Parent Ticket:

* Title: Publish/distribute `oraclepack-mcp-server` to avoid long-form MCP client configuration
* Summary: Replace the hardcoded venv interpreter path in MCP client configs with a portable, short config, and optionally enable one-click installation for supported clients.
* Source:

  * Link/ID (if present) or “Not provided”: `oraclepack-op-mcp.md`
  * Original ticket excerpt (≤25 words) capturing the overall theme: “publish this so we do not have to use the long form configuration for configuring mcp clients”
* Global Constraints:

  * Eliminate reliance on an absolute venv interpreter path in MCP client configuration.
  * Preserve required env variables (`ORACLEPACK_BIN`, `ORACLEPACK_ALLOWED_ROOTS`, `ORACLEPACK_ENABLE_EXEC`) in examples.
* Global Environment:

  * Unknown
* Global Evidence:

  * Current MCP client config example (shows venv path + args + env).

Split Plan:

* Coverage Map:

  * Original item: “how do I publish this so we do not have to use the long form configuration for configuring mcp clients?”

    * Assigned Ticket ID: T1
  * Original item: Current config uses venv interpreter path: `"command": "/home/user/.../venv/bin/python"`

    * Assigned Ticket ID: T1
  * Original item: Current args: `["-m", "oraclepack_mcp_server", "--transport", "stdio"]`

    * Assigned Ticket ID: T1
  * Original item: Current env vars: `ORACLEPACK_BIN`, `ORACLEPACK_ALLOWED_ROOTS`, `ORACLEPACK_ENABLE_EXEC`

    * Assigned Ticket ID: T1
  * Original item: Option 1: “Publish a Python package so the MCP command is just a PATH executable” + `uv build`, `uv publish`, `uv tool install ...`

    * Assigned Ticket ID: T1
  * Original item: Option 1 config snippet (command becomes `oraclepack-mcp-server`, args `--transport stdio`, env preserved)

    * Assigned Ticket ID: T1
  * Original item: Note: “If you want to reduce env too, prefer absolute paths…”

    * Assigned Ticket ID: T1
  * Original item: Option 2: “No install short config: run via uvx” + config snippet using `"command": "uvx"`

    * Assigned Ticket ID: T2
  * Original item: Option 2 note: aligns with `server.json` PyPI example using `runtimeHint: "uvx"`

    * Assigned Ticket ID: T4
  * Original item: Option 3a: “Claude Desktop: ship a .mcpb bundle” + `mcpb init`, `mcpb pack` + distribute `.mcpb`

    * Assigned Ticket ID: T3
  * Original item: Option 3b: “publish to Official MCP Registry (and GitHub MCP Registry)” via `server.json` and `mcp-publisher` steps

    * Assigned Ticket ID: T4
  * Original item: Recommendation section (choose Option 1 for no venv path; Option 3 for no manual config)

    * Assigned Ticket ID: Info-only
  * Original item: Note: “standardize the executable name to match the PyPI identifier”

    * Assigned Ticket ID: T1
* Dependencies:

  * T2 depends on T1 because the `uvx` approach runs the published package name (`oraclepack-mcp-server`).
  * T4 depends on T1 because the described registry publishing path references a PyPI stdio server example.
* Split Tickets:

```ticket T1
T# Title: Publish `oraclepack-mcp-server` as a PATH executable (PyPI + uv tools) and update config example
Type: enhancement
Target Area: Distribution/packaging for MCP server (`oraclepack-mcp-server`) + MCP client config examples
Summary:
- Publish the MCP server as a Python package so MCP clients can invoke it via a normal command on PATH instead of a venv interpreter path.
- Provide the shorter MCP client configuration example that uses `command: "oraclepack-mcp-server"` and preserves required env vars.
In Scope:
- Publish steps using uv:
  - `uv build`
  - `uv publish`
- Install guidance via uv tools:
  - `uv tool install oraclepack-mcp-server`
- Update the MCP client config example to:
  - `"command": "oraclepack-mcp-server"`
  - `"args": ["--transport", "stdio"]`
  - Keep env: `ORACLEPACK_BIN`, `ORACLEPACK_ALLOWED_ROOTS`, `ORACLEPACK_ENABLE_EXEC`
- Naming guidance: standardize executable name to match the PyPI identifier (e.g., `oraclepack-mcp-server`).
- Guidance note: prefer absolute paths for env values if trying to reduce/env-stabilize in hosts with undefined working directory.
Out of Scope:
- “One-click install” packaging and registry publishing (handled in other tickets).
Current Behavior (Actual):
- MCP client config points at a venv interpreter path and runs `-m oraclepack_mcp_server`:
  - `"command": "/home/user/.../venv/bin/python"`
Expected Behavior:
- MCP client config can run a PATH command directly:
  - `"command": "oraclepack-mcp-server"`
  - No venv absolute path required.
Reproduction Steps:
- Not provided
Requirements / Constraints:
- Preserve required env variables in examples:
  - `ORACLEPACK_BIN`
  - `ORACLEPACK_ALLOWED_ROOTS`
  - `ORACLEPACK_ENABLE_EXEC`
Evidence:
- Current config snippet includes venv interpreter path and env vars:
  - `"command": "/home/user/projects/temp/oraclepack/oraclepack-mcp-server/venv/bin/python"`
  - `"args": ["-m", "oraclepack_mcp_server", "--transport", "stdio"]`
  - `ORACLEPACK_BIN`, `ORACLEPACK_ALLOWED_ROOTS`, `ORACLEPACK_ENABLE_EXEC`
Open Items / Unknowns:
- Package metadata and repository details for publishing (Unknown / Not provided).
- Desired final executable name if it differs from `oraclepack-mcp-server` (Unknown / Not provided).
Risks / Dependencies:
- Not provided
Acceptance Criteria:
- A published distribution path exists that does not require MCP clients to reference a venv interpreter path.
- Documentation/config example shows:
  - `"command": "oraclepack-mcp-server"`
  - `"args": ["--transport", "stdio"]`
  - env variables preserved as shown in the source text.
- Executable naming guidance is documented (“match the PyPI identifier”).
Priority & Severity (if inferable from input text):
- Priority: Not provided
- Severity: Not provided
Source:
- “publish/distribute paths that eliminate the venv absolute path”
- “Publish a Python package so the MCP `command` is just a PATH executable”
- “uv build / uv publish … uv tool install oraclepack-mcp-server”
```

```ticket T2
T# Title: Add “no install” MCP config option using `uvx`
Type: docs
Target Area: MCP client configuration documentation/examples
Summary:
- Provide a short MCP client configuration that runs the server via `uvx` so users don’t need a pre-created local venv path in config.
- Keep required environment variables in the example config.
In Scope:
- Document the `uvx`-based MCP client config example:
  - `"command": "uvx"`
  - `"args": ["oraclepack-mcp-server", "--transport", "stdio"]`
  - env: `ORACLEPACK_BIN`, `ORACLEPACK_ALLOWED_ROOTS`, `ORACLEPACK_ENABLE_EXEC`
Out of Scope:
- Publishing to PyPI (handled in T1).
- Registry publishing via `server.json`/`mcp-publisher` (handled in T4).
Current Behavior (Actual):
- Config is “long-form” due to a venv interpreter path.
Expected Behavior:
- Users can use a short config that invokes `uvx` with the package name and stdio transport args.
Reproduction Steps:
- Not provided
Requirements / Constraints:
- Preserve required env variables in the example config.
Evidence:
- Option 2 config snippet:
  - `"command": "uvx"`
  - `"args": ["oraclepack-mcp-server", "--transport", "stdio"]`
Open Items / Unknowns:
- Whether target MCP clients/hosts support `uvx` invocation in their MCP server configuration (Unknown / Not provided).
Risks / Dependencies:
- Depends on T1 (published package name referenced by `uvx`).
Acceptance Criteria:
- Documentation includes the `uvx` config snippet exactly as described in the source text.
- Documentation explicitly retains the required env variable keys used in the source text.
Priority & Severity (if inferable from input text):
- Priority: Not provided
- Severity: Not provided
Source:
- “No install short config: run via `uvx`”
- `"command": "uvx", "args": ["oraclepack-mcp-server", "--transport", "stdio"]`
```

```ticket T3
T# Title: Create and distribute a `.mcpb` bundle for Claude Desktop installation
Type: enhancement
Target Area: MCP Bundle packaging for Claude Desktop distribution
Summary:
- Package the local MCP server as a `.mcpb` bundle so users can install via a UI flow in supported clients (Claude Desktop mentioned).
- Document the bundle creation commands and distribution approach.
In Scope:
- Use MCPB tooling steps as described:
  - `npm install -g @anthropic-ai/mcpb`
  - `mcpb init`
  - `mcpb pack`
- Distribute the resulting `.mcpb` (example given: GitHub Releases).
- Document that users install via Claude Desktop Extensions UI flow (per source text).
Out of Scope:
- PyPI publishing and `uv` tooling approach (handled in T1).
- Official/GitHub MCP registry publishing (handled in T4).
Current Behavior (Actual):
- Users must configure MCP clients manually with JSON.
Expected Behavior:
- Users can install the server via a `.mcpb` bundle in clients that support MCP bundles (Claude Desktop mentioned).
Reproduction Steps:
- Not provided
Requirements / Constraints:
- Follow the described `.mcpb` workflow (init + pack).
Evidence:
- “Claude Desktop: ship a `.mcpb` bundle … mcpb init … mcpb pack … Distribute the resulting `.mcpb`”
Open Items / Unknowns:
- Bundle manifest contents and exact server entrypoints required by MCPB for this server (Unknown / Not provided).
Risks / Dependencies:
- Not provided
Acceptance Criteria:
- A `.mcpb` bundle can be produced using the documented CLI steps.
- Documentation explains how to obtain the `.mcpb` (distribution channel mentioned) and install it in Claude Desktop (Extensions UI flow mentioned).
Priority & Severity (if inferable from input text):
- Priority: Not provided
- Severity: Not provided
Source:
- “Claude Desktop: ship a `.mcpb` bundle”
- “mcpb init … mcpb pack”
- “Distribute the resulting `.mcpb` (e.g., GitHub Releases)”
```

```ticket T4
T# Title: Publish `server.json` via `mcp-publisher` for MCP Registry / GitHub MCP Registry discovery
Type: enhancement
Target Area: Registry publishing metadata (`server.json`) + publishing workflow (`mcp-publisher`)
Summary:
- Enable “one-click install” in supported clients by publishing a `server.json` descriptor via `mcp-publisher`, targeting the Official MCP Registry and GitHub MCP Registry (as described).
- Document the high-level publishing steps and ownership proof requirement as stated.
In Scope:
- Generate `server.json` using `mcp-publisher init`.
- Follow the publishing sequence as described:
  1) Install `mcp-publisher`
  2) `mcp-publisher init` to generate `server.json`
  3) Prove package ownership (PyPI: add `mcp-name: ...` to README)
  4) `mcp-publisher login github`
  5) `mcp-publisher publish`
- Ensure `server.json` aligns with the described PyPI stdio example capabilities (mentions `environmentVariables` and `runtimeHint: "uvx"`).
Out of Scope:
- `.mcpb` bundling (handled in T3).
- Core PyPI package publishing steps (handled in T1).
Current Behavior (Actual):
- Users must manually configure MCP clients using JSON and local paths.
Expected Behavior:
- Server is discoverable/installable via registry mechanisms described (VS Code / ecosystem via registry).
Reproduction Steps:
- Not provided
Requirements / Constraints:
- Include the described package ownership proof metadata for PyPI (README `mcp-name: ...`).
Evidence:
- “publish a `server.json` via `mcp-publisher`”
- Step list including `mcp-publisher init`, README `mcp-name: ...`, `login github`, `publish`
- Note about PyPI example supporting `environmentVariables` and `runtimeHint: "uvx"`
Open Items / Unknowns:
- Final server identifier/name to use for `mcp-name: ...` (Unknown / Not provided).
- Which registries/clients are in scope beyond “VS Code / ecosystem” phrasing (Unknown / Not provided).
Risks / Dependencies:
- Depends on T1 if the published registry entry targets a PyPI package distribution (as described).
Acceptance Criteria:
- A `server.json` exists generated/maintained via `mcp-publisher init` per the described workflow.
- Documentation includes the stated publishing steps and the PyPI ownership proof requirement (`mcp-name: ...` in README).
- Documentation notes the described `runtimeHint: "uvx"` alignment for the PyPI stdio example.
Priority & Severity (if inferable from input text):
- Priority: Not provided
- Severity: Not provided
Source:
- “publish to the Official MCP Registry (and GitHub MCP Registry)”
- “publish a `server.json` via `mcp-publisher`”
- “Prove package ownership … add `mcp-name: ...` to your README”
```
```

.tickets/PRD-TUI/Oraclepack TUI Integration.md
```
Parent Ticket:

* Title: Oraclepack TUI integration for PRD Generator Project URL + ticketify PRD run flow
* Summary: Enable oraclepack to route a ticket-derived PRD artifact (`tickets_prd.md`) to a specific “PRD Generator” ChatGPT Project URL via the TUI and automated runs. Address the current failure when attempting to run `tickets_prd.md` as a pack (“no bash code block found”) by introducing supported execution paths (micro-pack and/or single-shot call), and optionally generating a richer PRD context bundle + dedicated PRD-generator pack from ticketify outputs.
* Source:

  * Link/ID: Oraclepack TUI Integration (1).md
  * Original ticket excerpt (≤25 words) capturing the overall theme: “utilize our specific prd-generator from one of many gpt project urls… add that project url… as an option in the tui.”
* Global Constraints:

  * `tickets_prd.md` is content/artifact and cannot be executed as a pack unless wrapped in a valid oraclepack structure (requires a `bash` fenced block).
  * Pack parsing requires a ` ```bash … ``` ` fenced code block; otherwise error: “invalid pack structure: no bash code block found”.
  * Avoid hardcoding ChatGPT Project URLs into packs/repos; prefer selecting/storing via the TUI URL picker/store.
  * Support “one of many project urls” including per-step targeting for PRD generation steps.
  * “Simple oracle calls” should be possible without sending entire multi-step packs.
* Global Environment:

  * Unknown
* Global Evidence:

  * Error string: `Error: invalid pack structure: no bash code block found`.
  * “Projects in ChatGPT” (Projects retain chats/files within a project). ([OpenAI Help Center][1])
  * CommonMark: fenced code blocks support an “info string” after the opening fence (language identifier). ([CommonMark Spec][2])

Split Plan:

* Coverage Map:

  1. Original item: “add that project url and send it with our oraclepack in an automated manner… option in the tui.”
     Assigned Ticket ID: T1
  2. Original item: “simple way of utilizing oracle for simpler calls… do not require entire packs being sent.”
     Assigned Ticket ID: T4
  3. Original item: “oraclepack will not allow the `tickets_prd.md`… `invalid pack structure: no bash code block found`.”
     Assigned Ticket ID: T3
  4. Original item: “Add a new entry in the URL picker… Name: `PRD Generator`… Scope: `project`… or `global`.”
     Assigned Ticket ID: T1
  5. Original item: “Set it as default (`s`)…”
     Assigned Ticket ID: T1
  6. Original item: “Headless/CI: add a CLI flag… `oraclepack run --chatgpt-url <url>` (or `--chatgpt-url-name <saved-name>`).”
     Assigned Ticket ID: T2
  7. Original item: “Using multiple project URLs… `RuntimeOverrides` supports `ChatGPTURL` and `ApplyToSteps`… missing piece is Overrides Wizard UI.”
     Assigned Ticket ID: T5
  8. Original item: “Add a new wizard step: ‘ChatGPT URL’… reuse URLPickerModel… write to `pendingOverrides.ChatGPTURL`.”
     Assigned Ticket ID: T5
  9. Original item: “Option A: ‘micro-pack’… attach `tickets_prd.md`… run `oracle` once…”
     Assigned Ticket ID: T3
  10. Original item: “Option B: add `oraclepack call`… pick URL preset… pick files… run one `oracle` invocation… bypass `internal/pack/parser.go`…”
      Assigned Ticket ID: T4
  11. Original item: “Better idea: `tickets_prd.md` artifact parsed into a valid oraclepack… sent to PRD-generator project url… add missing context as part of stage.”
      Assigned Ticket ID: T6
  12. Original item: “Generate deterministic `prd_context.md`… feature summary, prioritized requirements, user stories + AC, constraints/deps/out-of-scope/risks/open questions, keep vs rewrite.”
      Assigned Ticket ID: T6
  13. Original item: “Generate `.oraclepack/ticketify/prd-generator.pack.md`… attach `tickets_prd.md` + `prd_context.md`… `--write-output ".taskmaster/docs/final_prd.md"`.”
      Assigned Ticket ID: T6
  14. Original item: “Wire it into the TUI… toggle `[ ] Generate enhanced PRD via PRD Generator Project`… prompt pick URL… run pack… apply `RuntimeOverrides{ChatGPTURL: <picked>, ApplyToSteps: {"01": true}}`.”
      Assigned Ticket ID: T7
  15. Original item: “Static context in ChatGPT Project; dynamic context in `prd_context.md` attachment.”
      Assigned Ticket ID: T6
* Dependencies:

  * T7 depends on T6 because the TUI flow runs the generated `prd-generator.pack.md` and attaches `prd_context.md`.
* Split Tickets:

```ticket T1
T# Title:
- Add/select PRD Generator ChatGPT Project URL in TUI URL picker (project/global scope + default)

Type:
- enhancement

Target Area:
- oraclepack TUI URL picker/store (“ChatGPT Project URLs”)

Summary:
- Provide a standard way to store and select a “PRD Generator” ChatGPT Project URL from the existing URL picker/store, with project vs global scope guidance and the ability to set it as the default. This enables routing PRD-generation runs through the intended ChatGPT Project without hardcoding URLs into packs.

In Scope:
- Ensure the TUI supports creating/selecting an entry named `PRD Generator` with a ChatGPT Project URL.
- Support scope selection: `project` (repo-specific) vs `global` (shared), as described.
- Support setting the selected entry as the default (`s`) for the relevant scope.

Out of Scope:
- Not provided

Current Behavior (Actual):
- Not provided

Expected Behavior:
- User can add/select `PRD Generator` as a named URL entry.
- User can set it as default for the current workflow/repo.

Reproduction Steps:
- Not provided

Requirements / Constraints:
- Do not hardcode URLs into packs/repos (prefer picker/store selection).
- Preserve support for project vs global URL scoping.

Evidence:
- “Add a new entry in the URL picker… Name: `PRD Generator`… Scope: `project`… `global`…”
- “Set it as default (`s`)…”

Open Items / Unknowns:
- Actual PRD Generator ChatGPT Project URL value(s) (not provided).
- Whether a predefined/seeded entry is required vs user-created entry (not provided).

Risks / Dependencies:
- Not provided

Acceptance Criteria:
- [ ] TUI allows creating/selecting a `PRD Generator` URL entry.
- [ ] TUI allows choosing `project` vs `global` scope for the entry.
- [ ] TUI allows setting the entry as default (`s`) and that default is subsequently used when selected.

Priority & Severity (if inferable from input text):
- Priority: Not provided
- Severity: Not provided

Source:
- “Add a new entry in the URL picker: … Name: `PRD Generator`”
- “Scope: `project`… `global`…”
- “Set it as default (`s`)…”
```

```ticket T2
T# Title:
- Add headless/CI CLI override for ChatGPT URL selection during runs

Type:
- enhancement

Target Area:
- oraclepack CLI run command (headless / `--no-tui` runs)

Summary:
- Add a CLI override so headless/CI executions can force a ChatGPT URL (or saved URL name) rather than relying on interactive selection. This supports automated routing to the PRD Generator project URL.

In Scope:
- Add CLI support equivalent to: `oraclepack run --chatgpt-url <url>` and/or `--chatgpt-url-name <saved-name>`.
- Ensure the provided value sets the runner’s ChatGPT URL for the run.

Out of Scope:
- Not provided

Current Behavior (Actual):
- “Right now, the TUI resolves a URL; but there isn’t a CLI flag … that forces it in `--no-tui` runs.”

Expected Behavior:
- Headless/CI runs can specify a ChatGPT URL (or saved name) and have it applied for the run.

Reproduction Steps:
- Not provided

Requirements / Constraints:
- Must work without requiring the interactive TUI URL picker.
- Must not require editing packs to include URLs.

Evidence:
- “Headless/CI: add a CLI flag to override the picker”
- “Add `oraclepack run --chatgpt-url <url>` (or `--chatgpt-url-name <saved-name>`)...”

Open Items / Unknowns:
- Exact CLI flag naming/shape (both `--chatgpt-url` and `--chatgpt-url-name` are suggested; final selection not provided).

Risks / Dependencies:
- Not provided

Acceptance Criteria:
- [ ] `oraclepack run --chatgpt-url <url>` applies the provided URL for the run.
- [ ] If `--chatgpt-url-name <saved-name>` is implemented, it resolves to a stored URL and applies it for the run.
- [ ] Works in non-interactive/headless mode.

Priority & Severity (if inferable from input text):
- Priority: Not provided
- Severity: Not provided

Source:
- “Headless/CI: add a CLI flag to override the picker”
- “Add `oraclepack run --chatgpt-url <url>` (or `--chatgpt-url-name <saved-name>`)...”
```

````ticket T3
T# Title:
- Provide a micro-pack wrapper to run PRD generation using tickets_prd.md (valid bash fence pack)

Type:
- enhancement

Target Area:
- oraclepack pack inputs (micro-pack file used to call `oracle` with `tickets_prd.md`)

Summary:
- Introduce a minimal, valid oraclepack “micro-pack” that wraps PRD generation as an executable pack step. This avoids attempting to run `tickets_prd.md` directly (which fails pack validation) while enabling attaching `tickets_prd.md` to a single `oracle` call.

In Scope:
- Provide a one-step (or 2–3 step) pack that:
  - Is valid for oraclepack parsing (contains a `bash` fenced code block).
  - Attaches `tickets_prd.md`.
  - Runs `oracle` once to generate a PRD output.
- Ensure this flow can be routed to the selected PRD Generator ChatGPT Project URL (via existing URL selection/overrides mechanism).

Out of Scope:
- Full “single-shot call” mode that bypasses pack parsing (see T4).

Current Behavior (Actual):
- Running `tickets_prd.md` as a pack fails with: “Error: invalid pack structure: no bash code block found”.

Expected Behavior:
- A micro-pack wrapper can be run successfully by oraclepack and performs the PRD-generation oracle call using `tickets_prd.md` as an attachment.

Reproduction Steps:
1) Attempt to run `tickets_prd.md` as a pack and observe “no bash code block found”.
2) Run the micro-pack wrapper pack and observe it parses and executes.

Requirements / Constraints:
- Pack must include a ` ```bash … ``` ` fenced block to be parseable.
- Must attach `tickets_prd.md` to the oracle invocation.

Evidence:
- Error: “invalid pack structure: no bash code block found”
- “Generate a 1-step pack… attach `tickets_prd.md`… run `oracle` once…”

Open Items / Unknowns:
- Final location/name of the micro-pack file (example name provided: `prd-generator-call.pack.md`).
- Exact prompt text and output path conventions for the oracle call (not fully specified).

Risks / Dependencies:
- Not provided

Acceptance Criteria:
- [ ] A micro-pack file exists and is parseable by oraclepack (contains a `bash` fenced block).
- [ ] Running the micro-pack executes a single `oracle` call that includes `tickets_prd.md` as an attachment.
- [ ] The flow does not require running `tickets_prd.md` directly as a pack.

Priority & Severity (if inferable from input text):
- Priority: Not provided
- Severity: Not provided

Source:
- “Generate a 1-step pack… attach `tickets_prd.md`…”
- “Create a new file… valid pack… run `oracle` while attaching `tickets_prd.md`.”
- “Error: invalid pack structure: no bash code block found”
````

```ticket T4
T# Title:
- Add single-shot oracle invocation mode (CLI/TUI) that bypasses pack parsing

Type:
- enhancement

Target Area:
- oraclepack CLI + TUI (“single-call” mode)

Summary:
- Add a new execution path for “simple oracle calls” that does not require a full pack file or pack parsing. The user can select a ChatGPT URL preset, attach files such as `tickets_prd.md`, provide a prompt/template, and run exactly one `oracle` invocation.

In Scope:
- Implement a new subcommand such as `oraclepack call` (or `oraclepack oracle`) that:
  - Lets the user pick a ChatGPT URL preset.
  - Lets the user specify attachments (e.g., `tickets_prd.md`).
  - Runs one `oracle …` invocation.
- Implement a corresponding TUI flow/screen for “Single Oracle Call” with:
  - URL preset selection
  - attachments selection
  - prompt/template input
  - run
- Ensure this path bypasses pack parsing requirements (no need for a `bash` fenced block).

Out of Scope:
- Generating ticket-derived context bundle (`prd_context.md`) and a dedicated PRD generator pack (see T6/T7).

Current Behavior (Actual):
[TRUNCATED]
```

.tickets/PRD-TUI/PRD-generator URL routing.md
```
Title:

* Add PRD-generator project URL routing + ticketfy→PRD “micro-pack” generation; avoid `tickets_prd.md` pack-parse failure

Summary:

* `oraclepack-ticketfy` generates `tickets_prd.md`, but oraclepack cannot execute it as a pack and fails with `Error: invalid pack structure: no bash code block found`.
* Need an automated way (via TUI option) to route a PRD-generation run to a specific “PRD generator” ChatGPT Project URL (one of many project URLs), without sending entire packs for simple calls.

Background / Context:

* User wants oraclepack (TUI) to support selecting/storing multiple ChatGPT Project URLs and sending the chosen URL “with oraclepack” for PRD generation.
* Current understanding (per assistant): runner injects selected URL into `oracle …` invocations via `--chatgpt-url <selected>`, and there is a URL picker/store concept with defaults.
* User proposes: parse/transform `tickets_prd.md` (artifact) into a valid oraclepack that calls `oracle` against the PRD-generator project URL, and include additional context missing from `tickets_prd.md` as part of that stage.

Current Behavior (Actual):

* Running/using `tickets_prd.md` as a pack fails validation with: `Error: invalid pack structure: no bash code block found`.
* PRD generation flow is not currently integrated as a first-class TUI option that:

  * selects a PRD-generator ChatGPT Project URL “from one of many project urls”, and
  * runs a lightweight, single-purpose call without requiring a full pack workflow.
* Missing UI support (per assistant) for per-step ChatGPT URL selection in Overrides Wizard; URL selection appears global for the run today.

Expected Behavior:

* `tickets_prd.md` should be usable as input to PRD generation without being treated as a runnable pack.
* TUI should offer an option to run PRD generation routed to a specific PRD-generator ChatGPT Project URL (selectable from stored URLs), optionally scoped to only the PRD step(s).
* Workflow should support “simple oracle calls” that do not require sending entire packs.

Requirements:

* Do not attempt to execute `tickets_prd.md` directly as an oraclepack pack; instead, generate a valid runnable pack that calls `oracle` and attaches `tickets_prd.md`.
* Add an automated way (TUI option) to select and apply a PRD-generator ChatGPT Project URL for the PRD generation flow.
* Support “one of many project urls” (multiple ChatGPT Project URLs) and the ability to target PRD generation steps specifically (per-step URL override).
* Include “missing context” needed by the PRD generator as part of the stage (ticket-derived context bundle alongside `tickets_prd.md`).
* Provide a lightweight path for single-shot oracle calls (CLI/TUI) that does not require pack parsing (per assistant suggestion: new `oraclepack call` mode).

Out of Scope:

* Not provided.

Reproduction Steps:

1. Generate `tickets_prd.md` via `oraclepack-ticketfy`.
2. Attempt to run `tickets_prd.md` through oraclepack as if it were a runnable pack.
3. Observe error: `Error: invalid pack structure: no bash code block found`.

Environment:

* OS: Unknown
* oraclepack version/commit: Unknown
* oracle CLI version: Unknown
* ticketfy skill version: Unknown
* TUI vs no-TUI: Unknown

Evidence:

* Error message: `Error: invalid pack structure: no bash code block found`.
* Proposed workaround “micro-pack” example (per assistant) that wraps an `oracle` call and attaches `.taskmaster/docs/tickets_prd.md`.
* Suggested architecture (per assistant):

  * Add Overrides Wizard step “ChatGPT URL” writing to `pendingOverrides.ChatGPTURL`.
  * Generate `.oraclepack/ticketify/prd_context.md` and `.oraclepack/ticketify/prd-generator.pack.md`.
  * Optional new command path bypassing `internal/pack/parser.go` (pack parsing) via `oraclepack call ...`.

Decisions / Agreements:

* Constraint acknowledged: `tickets_prd.md` is an artifact and not runnable as a pack; running it directly will fail due to missing ` ```bash … ``` ` fenced block (per assistant).
* Preferred direction (user): convert artifact → valid oraclepack routed to PRD-generator project URL with added context for higher-quality PRD generation.

Open Items / Unknowns:

* Exact location/path of generated `tickets_prd.md` in the repo (example paths were suggested, but actual is not provided).
* How ChatGPT Project URLs are stored/serialized (format, file path, scope rules) in the current implementation: Not provided.
* Whether headless/CI runs need a CLI override flag (`--chatgpt-url` or `--chatgpt-url-name`): implied as needed, but exact requirement not confirmed.

Risks / Dependencies:

* Must preserve strict pack schema expectations (bash fenced block requirement); any solution that weakens validation could impact runner ingest reliability.
* Per-step URL routing depends on runtime overrides and a TUI flow to author/apply them (missing UI support noted).
* PRD generator quality depends on providing adequate context (new context bundle artifact needed).

Acceptance Criteria:

* [ ] Attempting to run `tickets_prd.md` directly is no longer part of the intended flow; documentation/UX guides user to PRD-generation pathway instead.
* [ ] Ticketfy stage outputs (or an adjacent stage) include:

  * [ ] a deterministic PRD context bundle artifact (e.g., `prd_context.md`), and
  * [ ] a valid runnable PRD generator pack (single `bash` fence) that attaches `tickets_prd.md` (+ context bundle) and invokes `oracle`.
* [ ] TUI exposes an option like “Generate enhanced PRD via PRD Generator Project” that:

  * [ ] lets user select a stored ChatGPT Project URL (PRD Generator),
  * [ ] applies it to the PRD generation run (optionally step-targeted).
* [ ] (If implemented) `oraclepack call` (single-shot) can run an `oracle` invocation with selected ChatGPT URL + attachments, without requiring pack parsing.

Priority & Severity (if inferable from text):

* Not provided.

Labels (optional):

* enhancement
* tui
* workflow
* prd
* url-routing
* pack-validation
```

.tickets/actions/Enable Action Packs Dispatch.md
```
Title:

* Enable oraclepack Action Packs to dispatch to non-oracle executors (codex/gemini/task-master/tm)

Summary:

* Current oraclepack usage feels “oracle-only” because certain UX features (flag injection and overrides validation) are hard-coded to `oracle` invocations, while the user needs Action Packs that actually execute work via other agents/tools (e.g., `codex exec`, `gemini`, `task-master`, `tm`). Update the Stage-3 Action Pack generation and/or oraclepack runner logic so Action Packs can deterministically run the correct executor commands for each action item.

Background / Context:

* User concern: “oraclepack is a wrapper around `oracle`” and adding more `oracle` calls won’t implement tasks; Action Packs must run the real tools/agents used in their workflow (examples: `codex exec ...`, `tm ...`, `gemini ...`).
* Current behavior explanation: oraclepack executes shell steps but has special handling only for lines starting with `oracle` (detection regex, flag injection, and overrides validation via `oracle --dry-run summary`).
* Stage-3 Action Pack template already runs non-oracle tools (`task-master …` and `tm autopilot`) and performs guarded checks for autopilot.
* Referenced repos/tools: Gemini CLI, Claude Task Master, OpenAI Codex, steipete/oracle.
* Referenced code/assets: `oraclepack-tui.md`, `oracle_pack_and_taskify-skills.md` (not included in transcript).

Current Behavior (Actual):

* Oraclepack “nice UX features” are oracle-specific:

  * Detects invocations using a regex anchored to literal `oracle`.
  * Injects selected flags only into `oracle …` lines.
  * Validates overrides by running `oracle --dry-run summary` only for detected oracle invocations.
* Action Packs can run arbitrary shell commands, but oraclepack’s overrides/validation UX does not generalize to other tools (codex/gemini/task-master/tm).

Expected Behavior:

* Generated Action Packs can deterministically dispatch execution to the intended executor per action item (e.g., `codex exec …`, `gemini -p …`, `task-master …`, `tm …`) rather than relying on more `oracle` calls.
* Optional: oraclepack’s overrides/flag-injection UX can recognize additional command prefixes beyond `oracle` (if desired).

Requirements:

* Update the Stage-3 generator (“oraclepack-taskify” / Stage-3 Action Pack template) to support configurable tool command strings beyond existing `oracle_cmd`, `task_master_cmd`, `tm_cmd`:

  * Add `codex_cmd` (default `codex`)
  * Add `gemini_cmd` (default `gemini`)
  * Optional: add `autopilot_cmd` (default `${tm_cmd} autopilot`).
* Extend `_actions.json` schema to include an executor plan:

  * `tooling`: include `{ oracle_cmd, task_master_cmd, codex_cmd, gemini_cmd }`
  * Per action item: `executor` (`"codex" | "gemini" | "tm" | "manual"`), `exec_prompt` (deterministic; no code fences), optional `inputs` (paths/globs).
* Add a new Action Pack execution path for implementation:

  * Either add `mode=implement`, or add a new “Step 09” gated by `MODE == implement`.
  * Step reads `<out_dir>/_actions.json`, selects top N items (using existing `top_n`), then dispatches:

    * `codex exec …` when `executor == codex`
    * `gemini -p …` when `executor == gemini`.
* Safety constraint:

  * Keep defaults strict (avoid “yolo” execution by default); Gemini approval/tool execution should remain conservative unless explicitly opted in.
* Optional (nice-to-have): generalize oraclepack’s oracle-specific detection/injection:

  * Generalize `ExtractOracleInvocations` / `InjectFlags` to recognize a registry of prefixes (`oracle`, `codex`, `gemini`, `task-master`, `tm`).
  * Add per-tool override sets (so “Oracle Flags” aren’t incorrectly applied to other tools).

Out of Scope:

* Not provided.

Reproduction Steps:

* Not provided.

Environment:

* OS: Unknown
* oraclepack version/commit: Unknown
* Shell/runner context: Unknown
* Stage-3 generator version/commit: Unknown

Evidence:

* User statement of need: actionpacks must call their agents/tools (examples: `codex exec ...`, `tm ...`, `gemini ...`).
* Oraclepack oracle-specific UX behavior (regex detection, flag injection, `oracle --dry-run summary` validation).
* Proposed schema + implement mode/Step 09 dispatcher design.
* Attachment: Oraclepack Action Pack Integration.md

Decisions / Agreements:

* “Fastest path” identified in transcript: upgrade Stage-3 Action Packs to include an executor dispatch step and extend `_actions.json` with `executor` metadata; modifying oraclepack core is optional.

Open Items / Unknowns:

* Current `_actions.json` schema (exact fields/types) is not provided.
* Current Stage-3 Action Pack template structure (exact steps and modes) is not provided beyond `backlog|pipelines|autopilot`.
* Exact locations/implementations of `ExtractOracleInvocations` / `InjectFlags` in `oraclepack-tui.md` are not provided in this transcript.
* Expected non-interactive invocation patterns/flags for each tool in this project (codex/gemini/task-master/tm) beyond the examples are not provided.

Risks / Dependencies:

* Dependency on external CLIs and their execution/approval modes (especially Gemini CLI) with safety implications; defaults must remain conservative.
* If oraclepack overrides/validation stays oracle-only, users may expect those UX features to apply to non-oracle commands; requires clear separation or per-tool override support.

Acceptance Criteria:

* `_actions.json` produced by the Stage-3 generator includes:

  * `tooling` with `{ oracle_cmd, task_master_cmd, codex_cmd, gemini_cmd }`
  * Per-item `executor` and `exec_prompt` (and optional `inputs`).
* Action Pack supports `implement` execution (via `mode=implement` or Step 09) that:

  * Reads `<out_dir>/_actions.json`
  * Selects top N actions
  * Runs `codex exec …` for `executor=codex` and `gemini -p …` for `executor=gemini` deterministically.
* Defaults do not enable unrestricted/automatic tool execution without opt-in (conservative approval/safety posture).
* (Optional) oraclepack runner recognizes non-oracle prefixes for invocation detection and does not incorrectly apply oracle-specific overrides to other tools.

Priority & Severity (if inferable from text):

* Priority: Not provided
* Severity: Not provided

Labels (optional):

* enhancement
* action-pack
* executor-dispatch
* cli-integration
* oraclepack
* taskify
```

.tickets/actions/Improving Oraclepack Workflow.md
```
Title:

* Add deterministic chaining + structured outputs to oraclepack (fix prelude semantics and enable stage-3 “actions” workflow)

Summary:

* The current oraclepack workflow is a 2-stage pipeline (pack generation → oraclepack execution) but has a “disconnect” after execution: the 20 Markdown outputs are not machine-consumable for automated follow-on work, blocking a seamless next stage for actionable implementation.

    Workflow Improvement Suggestions

* Additionally, the runner executes the pack prelude and each step in separate `bash -lc` processes, so prelude-defined variables do not persist into steps, creating a mismatch between template expectations and runtime behavior.

    Workflow Improvement Suggestions

Background / Context:

* Stage 1: a Codex “skill” or Gemini CLI “command” generates a single Markdown “oracle-pack” document whose machine-critical content is a single fenced `bash` block containing 20 numbered steps that call `oracle` with `--write-output ...`.

    Workflow Improvement Suggestions

* Stage 2: oraclepack parses that Markdown by extracting the first \`\`\`bash fence and splitting steps by a step-header pattern, then runs a “prelude” and each step in separate shells, producing 20 output files.

    Workflow Improvement Suggestions

* Goal: improve workflow productivity and longer runtimes with minimal human interaction, including automatically passing the final 20 results into a next stage that yields actionable implementation steps.

    Workflow Improvement Suggestions

Current Behavior (Actual):

* Prelude variables do not persist into step execution because prelude and steps run in separate `bash -lc` processes; packs must use explicit paths instead of relying on prelude variables like `$out_dir`.

    Workflow Improvement Suggestions

* The 20 `oracle` outputs are human-readable Markdown but lack a machine-friendly handoff artifact (e.g., JSON/YAML), making automated downstream processing brittle.

    Workflow Improvement Suggestions

* Parser/run is vulnerable to “format drift” from pack generators (extra code fences, missing/incorrect headers, multiple `bash` fences—parser grabs the first).

    Workflow Improvement Suggestions

Expected Behavior:

* Pack prelude semantics should match user/template expectations (either reliably shared across steps or explicitly non-shared with enforced guidance).

    Workflow Improvement Suggestions

* After a run, oraclepack should produce a deterministic, machine-readable “handoff” that enables an immediate next stage without manual intervention (“without missing a beat”).

    Workflow Improvement Suggestions

* Pack ingestion should be resilient and self-validating (fail fast on drift and contract violations).

    Workflow Improvement Suggestions

Requirements:

* Prelude semantics (choose and make official):

    Workflow Improvement Suggestions

  * Option A: “Prelude is prep-only” (no shared vars): update pack template guidance accordingly.

  * Option B: “Prelude is sourced into every step”: implement by prefixing each step script with prelude content (or run all steps in a single long-lived shell session).

* Stage-1 → Stage-2 contract hardening (“self-healing”):

    Workflow Improvement Suggestions

  * Standardize step headers to the conservative form `# NN)`.

  * Enforce exactly one fenced `bash` block per pack (or validation error).

  * Run `oraclepack validate` immediately after pack generation (wrapper/scriptable convention).

* Add a machine-readable “run index” artifact per run:

    Workflow Improvement Suggestions

  * Must include per-step `--write-output` paths, exit codes, timestamps.

  * Include parsed metadata when available (ROI/category/reference from step header line).

* Add a first-class “chain” capability to generate an “actions” next stage:

    Workflow Improvement Suggestions

  * Proposed interface: `oraclepack chain <pack.md> --mode actions`.

  * Must synthesize: `oracle-out/_summary.md` (human) and `oracle-out/_actions.json` (machine).

  * `_actions.json` should normalize each item with at least: `category`, `roi`, `reference`, `recommended_action` (“Next smallest concrete experiment”), `missing_artifacts[]`, `risk_notes[]`.

        Workflow Improvement Suggestions

  * Emit a follow-on pack: `docs/oracle-actions-YYYY-MM-DD.md`.

        Workflow Improvement Suggestions

* Execution/runtime considerations:

  * Keep compatibility with non-interactive operation (`--no-tui`, `--run-all`) and stop-on-fail behavior so chaining can run in CI.

        Workflow Improvement Suggestions

  * Optional: opt-in parallel execution with a small concurrency cap if provider/rate limits allow.

        Workflow Improvement Suggestions

* Improve pack input/context usage:

  * Support “focus + exclusion” inputs (e.g., `focus_categories=permissions,migrations`, `exclude_paths=docs,node_modules,dist`) without changing pack shape.

        Workflow Improvement Suggestions

  * Treat “extra\_files” as a deliberate context bundle (org standards/ADRs/threat model/coding conventions) appended to commands.

        Workflow Improvement Suggestions

Out of Scope:

* Not provided

Reproduction Steps:

1. Generate an oracle-pack whose prelude defines `out_dir="..."` and steps reference `$out_dir/...`.

2. Run oraclepack on the pack.

3. Observe that step shells do not see prelude-defined variables (because each step runs in a separate `bash -lc`), requiring explicit paths instead.

    Workflow Improvement Suggestions

Environment:

* OS: Unknown

* oraclepack: Unknown version (Go wrapper around `oracle`)

    Workflow Improvement Suggestions

* Shell execution model: `bash -lc` per step (per assistant analysis)

    Workflow Improvement Suggestions

* Pack generators: Codex skill and Gemini CLI command workflows

    Workflow Improvement Suggestions

Evidence:

* “oraclepack executes **prelude and steps in separate `bash -lc` processes**, so variables defined in the prelude do **not** persist into the step shells.”

    Workflow Improvement Suggestions

* Format drift risks listed: extra code fences, missing/incorrect step headers, multiple `bash` fences (parser grabs the first).

    Workflow Improvement Suggestions

* Proposed chain command + structured artifacts: `_summary.md`, `_actions.json`, and `docs/oracle-actions-YYYY-MM-DD.md`.

    Workflow Improvement Suggestions

Decisions / Agreements:

* None explicitly finalized; two alternative fixes for prelude semantics were presented (runner fix vs template fix), but no selection recorded.

    Workflow Improvement Suggestions

Open Items / Unknowns:

* Which prelude semantic is intended as the official contract (“prep-only” vs “sourced into every step”).

    Workflow Improvement Suggestions

* Exact current parser rules (regex specifics), validation behavior, and current report/state outputs (whether a run index already exists).

    Workflow Improvement Suggestions

* Whether Task Master integration is desired as a required dependency or just an optional downstream consumer.

    Workflow Improvement Suggestions

Risks / Dependencies:

* Dependency on consistent pack formatting across multiple generators (Codex/Gemini); drift can break parsing/validation.

    Workflow Improvement Suggestions

* If parallelism is added, provider rate limits and error handling may complicate deterministic runs.

    Workflow Improvement Suggestions

* Downstream automation quality depends on having a structured index/JSON handoff rather than parsing free-form Markdown answers.

    Workflow Improvement Suggestions

Acceptance Criteria:

* Running a pack that defines variables in the prelude and references them in steps behaves according to the selected official contract:

  * If “sourced prelude”: steps can reference prelude-defined variables successfully.

  * If “prep-only”: validation or guidance prevents packs from relying on prelude vars (or template guidance is updated and enforced).

        Workflow Improvement Suggestions

* `oraclepack validate` reliably fails on packs with:

  * multiple `bash` fences,

  * missing/incorrect `# NN)` step headers (per enforced convention),

  * other contract-breaking drift conditions.

        Workflow Improvement Suggestions

* After a run, oraclepack emits a machine-readable run index that includes per-step output paths, exit codes, and timestamps.

    Workflow Improvement Suggestions

* `oraclepack chain <pack.md> --mode actions` produces:

  * `oracle-out/_summary.md`,

  * `oracle-out/_actions.json` with the specified normalized fields,

  * `docs/oracle-actions-YYYY-MM-DD.md` suitable for immediate stage-2 execution.

        Workflow Improvement Suggestions

* Chaining works in non-interactive mode and respects stop-on-fail behavior to support CI usage.

    Workflow Improvement Suggestions

Priority & Severity (if inferable from text):

* Priority: Not provided

* Severity: Not provided

Labels (optional):

* enhancement

* workflow

* automation

* cli

* parsing

* schema

* ci

---
```

.tickets/actions/Oraclepack Action Pack Integration.md
```
Parent Ticket:

* Title: Oraclepack Action Pack Integration: dispatch Action Packs to external agents/tools (Codex/Gemini/Task Master)
* Summary:

  * Current concern: oraclepack is perceived as “oracle-only,” and adding more `oracle` calls won’t implement tasks.
  * Desired outcome: Action Packs should run the correct agent/tool commands (e.g., `codex exec …`, `tm …`, `gemini …`) and be tool-agnostic in how they dispatch work.
  * Optional scope: extend oraclepack’s oracle-specific UX (flag injection / validation) to support non-`oracle` commands.
* Source:

  * Link/ID (if present) or “Not provided”: Not provided
  * Original ticket excerpt (≤25 words) capturing the overall theme: “make it so our actionpacks will perform the correct calls to the agents… Example ‘codex exec …’, ‘tm …’, ‘gemini …’”
* Global Constraints:

  * Not provided
* Global Environment:

  * Tools referenced as available/on PATH in discussion: `oracle`, `codex`, `gemini`, `task-master`, `tm`
  * Action Pack modes referenced: `backlog|pipelines|autopilot` (and proposed `implement`)
  * Runner behavior referenced: steps execute as `bash`; stdin/TTY not attached (impacting interactive CLIs)
* Global Evidence:

  * Referenced files: `oracle_pack_and_taskify-skills.md`, `oraclepack-tui.md`
  * Referenced tool repos: `https://github.com/google-gemini/gemini-cli`, `https://github.com/eyaltoledano/claude-task-master`, `https://github.com/openai/codex`, `https://github.com/steipete/oracle`
  * Mentioned artifacts/screens: “screenshot of oraclepack consuming oraclepack-taskify artifacts”; “oracle-actions-pack-2026-01-07.md”

Split Plan:

* Coverage Map:

  * “oraclepack is a wrapper around `oracle`… can not see how… more oracle calls will help us implement” → Info-only
  * “make it so our actionpacks will perform the correct calls… ‘codex exec …’, ‘tm …’, ‘gemini …’” → T3
  * “oraclepack… special logic only for lines that start with `oracle`… detect… inject flags… overrides validation” → T5
  * “Stage-3 skill already supports… `oracle_cmd`, `task_master_cmd`, `tm_cmd`… Extend… `codex_cmd`, `gemini_cmd`, optionally `autopilot_cmd`” → T1
  * “Extend `_actions.json`… include… `tooling`… per item `executor`, `exec_prompt`, `inputs`” → T2
  * “Add an ‘implement’ mode (or a Step 09)… reads… `_actions.json`… selects top N… dispatches… `codex exec…` / `gemini -p…`” → T3
  * “Keep safety defaults strict (do not ‘yolo’ by default)” → T4
  * “interactive CLIs… may fail/hang because oraclepack doesn’t provide stdin/TTY” → T4
  * “This particular Action Pack does not call `codex`, `gemini`…” → Info-only
  * “Optional: improve oraclepack UX to ‘understand’ non-oracle commands… registry of prefixes… per-tool override sets” → T5
* Dependencies:

  * T3 depends on T2 because the proposed dispatcher reads `_actions.json` fields like `executor` / `exec_prompt`.
  * T2 depends on T1 because the proposed `_actions.json.tooling` includes `codex_cmd` / `gemini_cmd` command strings.
* Split Tickets:

```ticket T1
T# Title:
- Add Codex/Gemini command configuration to Stage-3 generator (alongside oracle/task-master/tm)

Type:
- enhancement

Target Area:
- Stage-3 generator / skill template that currently supports `oracle_cmd`, `task_master_cmd`, `tm_cmd`

Summary:
- Extend the Stage-3 generation inputs to support additional tool command strings so Action Packs can invoke Codex and Gemini explicitly.
- This is intended to mirror the existing configurable command pattern already used for `oracle` and Task Master tools.

In Scope:
- Add `codex_cmd` (default `codex`) to the generator inputs.
- Add `gemini_cmd` (default `gemini`) to the generator inputs.
- Add optional `autopilot_cmd` (default `${tm_cmd} autopilot`) to the generator inputs.
- Ensure generated Action Pack uses these command variables where relevant.

Out of Scope:
- Not provided

Current Behavior (Actual):
- Generator supports configurable tool command strings: `oracle_cmd`, `task_master_cmd`, `tm_cmd`.

Expected Behavior:
- Generator also supports configurable tool command strings: `codex_cmd`, `gemini_cmd` (and optional `autopilot_cmd`), enabling Action Packs to call these tools directly.

Reproduction Steps:
- Not provided

Requirements / Constraints:
- Preserve the existing configurable command pattern already used for `oracle_cmd`, `task_master_cmd`, `tm_cmd`.
- Defaults as stated in the ticket text (`codex`, `gemini`, `${tm_cmd} autopilot`).

Evidence:
- References to existing pattern: `oracle_cmd`, `task_master_cmd`, `tm_cmd`
- Proposed additions: `codex_cmd`, `gemini_cmd`, `autopilot_cmd`

Open Items / Unknowns:
- Where the Stage-3 generator defines/declares these args (exact file/path not provided).
- How generated artifacts currently surface tool command strings (exact schema not provided).

Risks / Dependencies:
- Depends on T2 if these command strings must also be emitted into `_actions.json.tooling`.

Acceptance Criteria:
- Stage-3 generator accepts `codex_cmd` and `gemini_cmd` (and optional `autopilot_cmd`) as inputs.
- Defaults match the ticket text when values are not provided.
- Existing inputs (`oracle_cmd`, `task_master_cmd`, `tm_cmd`) remain supported and unchanged.
- Generated Action Pack artifacts include/use these variables (where the template expects tool invocations).

Priority & Severity (if inferable from input text):
- Priority: Not provided
- Severity: Not provided

Source:
- “Your Stage-3 skill already supports… `oracle_cmd`, `task_master_cmd`, `tm_cmd`”
- “Extend that same pattern for: `codex_cmd`… `gemini_cmd`… optionally `autopilot_cmd`”
```

```ticket T2
T# Title:
- Extend `_actions.json` to include per-item executor metadata and tool command strings

Type:
- enhancement

Target Area:
- Canonical actions artifacts (`_actions.json` / `_actions.md`) generated from Stage-2 outputs

Summary:
- Add explicit executor metadata to each action item so downstream Action Pack steps can deterministically decide which tool to run.
- Add a `tooling` object to carry tool command strings (including Codex/Gemini) for use by the dispatcher.

In Scope:
- Extend `_actions.json` to include `tooling` with: `{ oracle_cmd, task_master_cmd, codex_cmd, gemini_cmd }`.
- For each action item, add:
  - `executor`: `"codex" | "gemini" | "tm" | "manual"`
  - `exec_prompt`: short instruction string (no code fences; deterministic)
  - `inputs`: optional list of paths/globs (as referenced by ticket text)
- Ensure `_actions.md` can be produced from the JSON after the schema extension (format details not provided in ticket).

Out of Scope:
- Not provided

Current Behavior (Actual):
- `_actions.json` exists and includes `tooling` and per-item fields such as `recommended_next_action`, `acceptance_criteria` (as described in ticket text).

Expected Behavior:
- `_actions.json` includes the new `tooling` and per-item executor fields so execution can be dispatched without guessing.
- `_actions.md` can still be generated from `_actions.json`.

Reproduction Steps:
- Not provided

Requirements / Constraints:
- `exec_prompt` must be short and deterministic (“no code fences” stated in ticket text).
- `inputs` is optional and can be derived from “missing_artifacts / repo anchors” (source details not provided).

Evidence:
- Proposed schema fields: `tooling`, `executor`, `exec_prompt`, `inputs`
- Existing per-item fields referenced: `recommended_next_action`, `acceptance_criteria`

Open Items / Unknowns:
- Exact current `_actions.json` schema and where it is defined (not provided).
- How `_actions.md` is rendered from `_actions.json` (not provided).

Risks / Dependencies:
- T3 depends on these fields being present to implement the dispatcher logic.

Acceptance Criteria:
- `_actions.json` includes `tooling` with the listed command keys.
- Each action item includes `executor` and `exec_prompt`; `inputs` is present when available and omitted/empty when not.
- The executor enum matches the ticket text: `codex|gemini|tm|manual`.
- `_actions.md` generation continues to work with the extended JSON structure.

Priority & Severity (if inferable from input text):
- Priority: Not provided
- Severity: Not provided

Source:
- “Extend `_actions.json` to include an executor plan per item”
- “Add fields like… `tooling`: `{ oracle_cmd, task_master_cmd, codex_cmd, gemini_cmd }`”
- “per item… `executor`… `exec_prompt`… `inputs`”
```

```ticket T3
T# Title:
- Add `implement` dispatch mode (or Step 09) to Action Pack template to run Codex/Gemini based on `_actions.json`

Type:
- enhancement

Target Area:
- Stage-3 Action Pack template (modes: `backlog|pipelines|autopilot`) and step execution flow

Summary:
- Introduce an execution path that reads `_actions.json`, selects the top N actions, and dispatches to the appropriate tool based on `executor`.
- This is intended to make Action Packs perform actual implementation calls (e.g., `codex exec …`, `gemini -p …`) instead of only producing planning artifacts.

In Scope:
- Add either:
  - `mode=implement`, or
  - a new Step 09 gated by `MODE == implement`.
- In that mode/step:
  - Read `<out_dir>/_actions.json`.
  - Select the top N items (based on existing `top_n` concept referenced in ticket text).
  - Dispatch:
    - `codex exec …` for items with executor `codex`
    - `gemini -p …` for items with executor `gemini`
  - (Other executor values referenced in ticket text: `tm`, `manual` — behavior not specified beyond inclusion in enum.)

Out of Scope:
- Not provided

Current Behavior (Actual):
- Action Pack modes exist: `backlog|pipelines|autopilot`.
- Current example pack “does not call `codex`, `gemini`, etc. at all” (per ticket text).

Expected Behavior:
- When `MODE=implement`, the Action Pack executes tool commands for actions according to `_actions.json.executor`.

Reproduction Steps:
- Not provided

Requirements / Constraints:
- Use `_actions.json` as the source of truth for executor decisions (as described).
- Top-N selection should use the existing `top_n` referenced in ticket text.

Evidence:
- Proposed mode/step: “Add an ‘implement’ mode (or a Step 09)”
- Proposed dispatch: “`codex exec …`… `gemini -p …`”
- Top-N: “selects the top N items (you already have `top_n`)”

Open Items / Unknowns:
- Where `top_n` is currently defined and how ranking is determined (not provided).
- Exact location/structure for `<out_dir>/_actions.json` (not provided).

Risks / Dependencies:
- Depends on T2 because dispatcher requires `executor`/`exec_prompt` data in `_actions.json`.
- Depends on T1 if dispatcher uses `codex_cmd` / `gemini_cmd` variables.

Acceptance Criteria:
- Action Pack supports a distinct execution path for `implement` (as a mode or gated step).
- In `implement`, the Action Pack reads `_actions.json` and dispatches at least `codex` and `gemini` executors using the stated command forms.
- Existing modes (`backlog|pipelines|autopilot`) remain unchanged in behavior.
- If `_actions.json` is missing/unreadable, the implement path fails clearly (exact messaging not provided).

Priority & Severity (if inferable from input text):
- Priority: Not provided
- Severity: Not provided

Source:
- “Add an ‘implement’ mode (or a Step 09)… reads `<out_dir>/_actions.json`”
- “dispatches deterministically: `codex exec …`… `gemini -p …`”
- “This particular Action Pack does not call `codex`, `gemini`, etc. at all”
```

```ticket T4
T# Title:
- Add non-interactive and safety guardrails for Codex/Gemini execution in Action Packs

Type:
- enhancement

Target Area:
- Action Pack execution behavior when running non-`oracle` tools (Codex/Gemini), including runner constraints around stdin/TTY

Summary:
- The ticket notes that oraclepack runs step scripts without attaching stdin/TTY, which can break or hang interactive CLIs.
- Add guardrails so Action Packs use non-interactive invocation patterns and keep defaults safe (explicitly: do not “yolo” by default).

In Scope:
- Ensure Codex invocation is non-interactive (ticket references `codex exec …` as the intended entrypoint).
- Ensure Gemini invocation is non-interactive (ticket references `gemini -p/--prompt` as intended).
- Apply safety defaults: “Keep safety defaults strict (do not ‘yolo’ by default)” (flag specifics not mandated beyond this phrase).
- Reflect/acknowledge runner constraint: “does not attach stdin/TTY” and the resulting failure mode for interactive tools.

Out of Scope:
- Not provided

Current Behavior (Actual):
- Runner behavior described: executes scripts via `bash -lc`, does not attach stdin/TTY; interactive CLIs may fail or stall.

Expected Behavior:
- Action Packs avoid interactive-only command forms and use non-interactive command forms for Codex/Gemini.
- Defaults remain conservative and do not enable “yolo” behavior by default.

Reproduction Steps:
- Not provided

Requirements / Constraints:
[TRUNCATED]
```

.tickets/actions/Oraclepack Action Pack Issue.md
```
Parent Ticket:

* Title: Oraclepack Action Pack compatibility and non-`oracle` command handling clarity
* Summary: Clarify what happens when running `oraclepack-taskify`-generated Action Packs in current oraclepack, especially the difference between “executing the pack” vs “dispatching/wrapping non-`oracle` commands.” Capture current limitations: oraclepack’s special handling (flag injection + override validation) targets `oracle` invocations only; other CLIs run as plain shell commands and may fail/block depending on PATH and interactivity.
* Source:

  * Link/ID: Not provided
  * Original ticket excerpt (≤25 words) capturing the overall theme: “Two meanings of ‘run these actionpack artifacts’… execute the pack file vs dispatch non-`oracle` commands.”
* Global Constraints:

  * Action Pack is described as “oraclepack-ingestible” with “single ```bash fence” and “# NN) step headers.”
* Global Environment:

  * Unknown
* Global Evidence:

  * docs.task-master.dev link(s) referenced in ticket text
  * developers.openai.com Codex CLI link(s) referenced in ticket text
  * docs.cloud.google.com / google-gemini.github.io Gemini CLI link(s) referenced in ticket text

Split Plan:

* Coverage Map:

  * Original item: “Can our current oraclepack run these actionpack artifacts… generated from the oraclepack-taskify skill?” → Assigned Ticket ID: T1
  * Original item: “Action Pack… ‘oraclepack-ingestible’ (single ```bash fence, # NN) step headers, deterministic paths…)” → Assigned Ticket ID: T1
  * Original item: “How you’d run it… `oraclepack validate …` / `oraclepack run …`” → Assigned Ticket ID: T1
  * Original item: “Actionpacks calling other agents/tools (codex exec, tm autopilot, gemini)… will run them as long as installed and usable non-interactively” → Assigned Ticket ID: T1
  * Original item: “Why did another reviewer say otherwise?” → Assigned Ticket ID: T1
  * Original item: “oraclepack runs each step as a bash script (bash -lc <step script>)” → Assigned Ticket ID: T1
  * Original item: “injects flags into commands that begin with oracle… does not match tm/task-master/codex/gemini” → Assigned Ticket ID: T3
  * Original item: “TUI ‘override validation’ only targets oracle… steps without oracle invocations are skipped” → Assigned Ticket ID: T2
  * Original item: “If binary isn’t installed/on PATH → step fails; if CLI is interactive → it will block” → Assigned Ticket ID: T1
  * Original item: “Only way it ‘gets confused’ is if you expect oracle output text to magically invoke Codex/Gemini” → Assigned Ticket ID: T1
  * Original item: “Two meanings… execute vs dispatch/apply oraclepack overrides to non-oracle commands” → Assigned Ticket ID: T1
  * Original item: “Reviewer answered ‘No’ because broader goal is dispatcher behavior… not interpret actions” → Assigned Ticket ID: Info-only
  * Original item: “Reconciliation: both statements can be true” → Assigned Ticket ID: Info-only
* Dependencies:

  * Not provided
* Split Tickets:

````ticket T1
T# Title: Document current Action Pack execution semantics and operator expectations
Type: docs
Target Area: oraclepack documentation / runbook for Action Packs
Summary:
  Clarify what “running an Action Pack” means in current oraclepack: steps execute as shell via `bash -lc`, and any `tm`/`codex`/`gemini` lines run as plain shell commands. Document the two meanings that caused reviewer disagreement: executing the pack vs dispatching/applying oracle-specific override logic to non-`oracle` commands. Include operator-facing notes on common failure modes already described (PATH missing binaries, interactive CLIs blocking, environment guardrails for autopilot).
In Scope:
  - Explain: “oraclepack executes each step’s body as shell via `bash -lc <command>`.”
  - Explain: non-`oracle` commands (`tm`, `task-master`, `codex`, `gemini`) are executed “directly as normal shell commands.”
  - Provide the exact run examples already given (`oraclepack validate …` and `oraclepack run …`).
  - Capture the limitation: oraclepack’s oracle-specific overrides/validation do not apply to non-`oracle` commands.
  - Document noted failure/blocking conditions:
    - Missing binaries not on PATH.
    - Interactive CLIs blocking waiting for input.
    - Autopilot “fail fast” environment issues (e.g., not in git repo, dirty tree) as stated.
Out of Scope:
  - Implementing dispatcher behavior for non-`oracle` tools (not specified beyond “broader goal” mention).
Current Behavior (Actual):
  - Steps are run as `bash -lc <step script>`.
  - Non-`oracle` commands run directly; no automatic dispatch/wrapping is applied.
Expected Behavior:
  - Operators can correctly predict:
    - What will execute (literal commands in the pack).
    - What will not happen automatically (no “magic” invocation of Codex/Gemini from oracle output text).
Reproduction Steps:
  - Not provided
Requirements / Constraints:
  - Preserve stated Action Pack constraints: “single ```bash fence” and “# NN) step headers” (as described).
Evidence:
  - Included links mentioned in ticket text:
    - docs.task-master.dev references
    - developers.openai.com Codex CLI references
    - Gemini CLI references (docs.cloud.google.com / google-gemini.github.io)
Open Items / Unknowns:
  - Exact location(s) where this documentation should live (Not provided).
  - Whether this should be surfaced in CLI help text vs README vs TUI (Not provided).
Risks / Dependencies:
  - Not provided
Acceptance Criteria:
  - Documentation explicitly states:
    - Packs execute step bodies via `bash -lc`.
    - Non-`oracle` commands run as-is and are not routed through oracle-specific logic.
    - Common failure modes listed in the ticket text (PATH missing, interactive blocking, autopilot environment guards).
  - Documentation includes the exact run commands already provided (`oraclepack validate …`, `oraclepack run …`).
Priority & Severity (if inferable from input text):
  - Priority: Not provided
  - Severity: Not provided
Source:
  - “oraclepack executes each step’s body as shell via `bash -lc <command>`.”
  - “Will oraclepack dispatch non-`oracle` commands…? No… only targets commands that start with `oracle`.”
  - “If the CLI is interactive → it will block waiting for input.”
````

```ticket T2
T# Title: Make TUI override validation behavior explicit for steps without `oracle` invocations
Type: enhancement
Target Area: TUI overrides flow / override validation messaging
Summary:
  The ticket text states that TUI “override validation” runs `oracle --dry-run summary` on detected `oracle` invocations and skips steps without `oracle` calls. Make this behavior explicit in the TUI so operators do not misinterpret skipped steps as “validated,” especially when packs include `tm`/`codex`/`gemini` commands.
In Scope:
  - Surface an explicit note/state in the overrides validation flow indicating:
    - Validation applies only to detected `oracle` invocations.
    - Steps without `oracle` invocations are skipped by this validator.
Out of Scope:
  - Adding validation implementations for `tm`/`codex`/`gemini` (not described in ticket text).
Current Behavior (Actual):
  - “Override validation… only targets `oracle` commands… Steps without oracle invocations are skipped.”
Expected Behavior:
  - TUI clearly communicates when steps are skipped (and why), avoiding operator confusion.
Reproduction Steps:
  - Not provided
Requirements / Constraints:
  - Must preserve current behavior described (validate only `oracle` invocations) unless changed elsewhere (Not provided).
Evidence:
  - “The TUI ‘override validation’ also only targets `oracle` commands… Steps without oracle invocations are skipped…”
Open Items / Unknowns:
  - Exact UI copy/placement and which screen(s) in the TUI should show this (Not provided).
Risks / Dependencies:
  - Not provided
Acceptance Criteria:
  - When override validation runs, the UI explicitly indicates:
    - It validates only `oracle` invocations (via `oracle --dry-run summary`, as stated).
    - Steps without `oracle` invocations are skipped (and shown as skipped).
Priority & Severity (if inferable from input text):
  - Priority: Not provided
  - Severity: Not provided
Source:
  - “The TUI ‘override validation’ also only targets `oracle` commands… runs `oracle --dry-run summary`…”
  - “Steps without oracle invocations are skipped by that validator.”
```

```ticket T3
T# Title: Extend runtime flag-injection matching beyond `oracle` invocations (configurable prefixes)
Type: enhancement
Target Area: command rewriting / runtime override injection
Summary:
  The ticket text states oraclepack injects flags only into commands that begin with `oracle` (regex anchored to `^(\s*)(oracle)\b`) and does not match `tm`, `task-master`, `codex`, or `gemini`. Add support for matching additional command prefixes (or a configurable list) so override injection is not limited to `oracle` only, aligning with packs that include other CLIs.
In Scope:
  - Expand the “inject flags” behavior beyond `oracle`-only matching, as motivated by:
    - “does not match `tm`, `task-master`, `codex`, `gemini`, etc.”
  - Preserve anchored/prefix-based matching semantics as described (no broad substring matching implied).
Out of Scope:
  - Defining tool-specific semantics for what flags should be injected for each CLI (Not provided).
  - Implementing dispatcher logic that changes execution from “literal shell command” to “interpreted actions” (Not provided).
Current Behavior (Actual):
  - “Injects flags into commands that begin with `oracle`… regex anchored to `^(\s*)(oracle)\b`.”
  - “It does not match `tm`, `task-master`, `codex`, `gemini`, etc.”
Expected Behavior:
  - Flag injection can apply to non-`oracle` command prefixes as configured/defined (details not provided in ticket text).
Reproduction Steps:
  - Not provided
Requirements / Constraints:
  - Must not break execution of packs that rely on current `oracle`-only behavior (Not provided).
Evidence:
  - “injects flags into commands that begin with `oracle` (regex anchored to `^(\s*)(oracle)\b`). It does not match `tm`, `task-master`, `codex`, `gemini`, etc.”
Open Items / Unknowns:
  - Which non-`oracle` commands should be included first (list not provided beyond examples).
  - Where configuration should live (Not provided).
  - Whether injection should be opt-in per pack/step or global (Not provided).
Risks / Dependencies:
  - Risk: unintended rewriting of commands if prefix matching is overly broad (mitigate via anchored matching; exact approach not provided).
Acceptance Criteria:
  - There is a documented/configured way to include additional command prefixes for injection beyond `oracle`.
  - Existing `oracle` prefix injection continues to work unchanged.
  - Demonstrably, a command beginning with one added prefix is recognized for injection (exact flags and CLI semantics: Not provided).
Priority & Severity (if inferable from input text):
  - Priority: Not provided
  - Severity: Not provided
Source:
  - “injects flags into commands that begin with `oracle` (regex is anchored to `^(\\s*)(oracle)\\b`).”
  - “It does not match `tm`, `task-master`, `codex`, `gemini`, etc.”
```
```

.tickets/actions/Oraclepack Action Packs.md
```
Parent Ticket:

* Title: Oraclepack Action Packs: tool-agnostic execution (Codex/Gemini/Task Master) instead of oracle-only flows
* Summary:

  * Current pain point: oraclepack is a wrapper around `oracle`, and “more oracle calls” won’t implement taskified work; Action Packs must run real tool commands (e.g., `codex`, `gemini`, `tm`, `task-master`).
  * Key idea: keep oracle as “planner” and make Action Packs do deterministic executor dispatch via `_actions.json` metadata and a new implement mode/step.
* Source:

  * Link/ID (if present) or “Not provided”: Uploaded file: Oraclepack Action Pack Integration.md
  * Original ticket excerpt (≤25 words) capturing the overall theme: “make it so our actionpacks will perform the correct calls… Example ‘codex exec …’, ‘tm …’, ‘gemini …’”.
* Global Constraints:

  * Action Packs should be “tool-agnostic” (dispatch to `codex`, `gemini`, `tm`, etc.).
  * `exec_prompt` should be short and deterministic (explicitly “no code fences”).
  * Safety defaults must be strict (“do not yolo by default”); conservative approval/tool execution unless opted-in.
  * Optional/“nice-to-have”: generalize oraclepack’s oracle-specific overrides UX for other command prefixes.
* Global Environment:

  * Unknown
* Global Evidence:

  * Mentioned repos/tools:

    * `https://github.com/google-gemini/gemini-cli`
    * `https://github.com/eyaltoledano/claude-task-master`
    * `https://github.com/openai/codex`
    * `https://github.com/steipete/oracle`
  * Reference docs cited/used in the ticket text:

    * Codex CLI reference (supports “codex exec”). ([OpenAI Developers][1])
    * Gemini CLI docs (tools/approval concepts referenced by the ticket). ([Gemini CLI][2])
    * Claude Task Master repository. ([GitHub][3])

Split Plan:

* Coverage Map:

  * “make it so our actionpacks will perform the correct calls… ‘codex exec …’, ‘tm …’, ‘gemini …’” → T3
  * Oraclepack has special logic only for lines starting with `oracle` (regex detection / flag injection / validation) → T5
  * “Stage-3 Action Pack template already runs non-oracle tools (`task-master …` and `tm autopilot`) … guarded branch checks” → T3
  * Stage-3 skill supports `oracle_cmd`, `task_master_cmd`, `tm_cmd` → T1
  * “Extend that same pattern for `codex_cmd`, `gemini_cmd`, optionally `autopilot_cmd`” → T1
  * “Extend `_actions.json` … `executor`, `exec_prompt` (no code fences), `inputs`, plus expanded `tooling`” → T2
  * “Add an ‘implement’ mode (or Step 09)… reads `<out_dir>/_actions.json`… selects top N (`top_n`)… dispatches” → T3
  * “Keep safety defaults strict (do not ‘yolo’ by default)” → T4
  * “Minimal changes… add args… update Prompt A… update Action Pack template” → T1, T2, T3
  * “Optional: improve oraclepack UX… registry of command prefixes… per-tool override sets” → T5
  * “Concrete command patterns… `codex exec`, `gemini -p`, Task Master pipeline commands” → Info-only
  * “If you want, I can propose the exact schema delta… Step 09 bash logic” → Info-only
* Dependencies:

  * T3 depends on T2 because the implement/dispatcher step reads `_actions.json` and needs `executor`/`exec_prompt` metadata.
  * T2 depends on T1 because `_actions.json.tooling` expansion references new tool command fields (`codex_cmd`, `gemini_cmd`, optional `autopilot_cmd`).
  * T4 depends on T3 because safety defaults/opt-ins apply to the implement/dispatcher execution path.
  * T5 is independent (optional) but may follow T3 if you want the TUI/overrides UX to apply to non-oracle commands.
* Split Tickets:

```ticket T1
T1 Title:
- Extend oraclepack-taskify Stage-3 generator to accept and propagate executor CLI command configs (codex/gemini/autopilot)

Type:
- enhancement

Target Area:
- oraclepack-taskify (Stage-3 generator inputs/args and emitted configs)

Summary:
- The Stage-3 generator already supports configurable command strings for `oracle_cmd`, `task_master_cmd`, and `tm_cmd`.
- Extend the same configuration pattern to include `codex_cmd` and `gemini_cmd`, and optionally `autopilot_cmd`, so Action Packs can invoke the intended executors without hard-coding tool names.

In Scope:
- Add generator inputs/args for:
  - `codex_cmd` (default `codex`)
  - `gemini_cmd` (default `gemini`)
  - optional `autopilot_cmd` (default `${tm_cmd} autopilot`)
- Ensure generated artifacts carry these command strings for later use by the Action Pack execution steps.

Out of Scope:
- Modifying oraclepack core/TUI behavior (handled in T5)
- Implement-mode dispatcher logic (handled in T3)

Current Behavior (Actual):
- Stage-3 skill supports configurable tool command strings:
  - `oracle_cmd`, `task_master_cmd`, `tm_cmd`

Expected Behavior:
- Stage-3 generator also supports `codex_cmd`, `gemini_cmd`, and optionally `autopilot_cmd`, using the same configuration pattern.

Reproduction Steps:
- Not provided

Requirements / Constraints:
- Preserve the existing “configurable tool command strings” pattern already used by the Stage-3 generator.
- Defaults as stated in the ticket text.

Evidence:
- References: “Your Stage-3 skill already supports… `oracle_cmd`, `task_master_cmd`, `tm_cmd`… Extend… `codex_cmd`… `gemini_cmd`… optionally `autopilot_cmd`…”

Open Items / Unknowns:
- Where these args are defined/passed in the current generator (file paths not provided).
- Whether additional executors beyond codex/gemini/tm are needed (not provided).

Risks / Dependencies:
- Depends on T2 if `_actions.json.tooling` is expanded to include these command strings.

Acceptance Criteria:
- Stage-3 generator accepts `codex_cmd` and `gemini_cmd` (and optional `autopilot_cmd`) inputs.
- Defaults match: `codex`, `gemini`, and `${tm_cmd} autopilot` (optional).
- Generated outputs expose these command strings for downstream Action Pack steps.

Priority & Severity (if inferable from input text):
- Priority: Not provided
- Severity: Not provided

Source:
- “Your Stage-3 skill already supports configurable tool command strings: `oracle_cmd`, `task_master_cmd`, `tm_cmd`”
- “Extend that same pattern for: `codex_cmd`… `gemini_cmd`… optionally `autopilot_cmd`”
```

```ticket T2
T2 Title:
- Extend `_actions.json` schema + Prompt A to emit per-item executor metadata (executor/exec_prompt/inputs) and expanded tooling

Type:
- enhancement

Target Area:
- Canonical actions schema (`_actions.json`) and “Prompt A” (canonical actions synthesis)

Summary:
- The current actions schema has `tooling` (oracle/task-master) and per-item fields like `recommended_next_action` and `acceptance_criteria`.
- Add executor planning fields per action item so Action Packs can deterministically select and run the correct executor (`codex`, `gemini`, `tm`, or manual), and include relevant inputs.

In Scope:
- Update `_actions.json` to add:
  - `tooling`: `{ oracle_cmd, task_master_cmd, codex_cmd, gemini_cmd }`
  - per-item fields:
    - `executor`: `"codex" | "gemini" | "tm" | "manual"`
    - `exec_prompt`: short instruction string (explicitly “no code fences; deterministic”)
    - `inputs`: optional list of paths/globs (from `missing_artifacts` / repo anchors)
- Update “Prompt A” to emit the above fields for each action item.

Out of Scope:
- Implement-mode dispatcher logic that consumes `_actions.json` (handled in T3)
- oraclepack core UX changes (handled in T5)

Current Behavior (Actual):
- `_actions.json` has `tooling` (oracle/task-master) and per-item fields like `recommended_next_action`, `acceptance_criteria`.

Expected Behavior:
- `_actions.json` includes explicit executor plan per item (`executor`, `exec_prompt`, optional `inputs`) and expanded `tooling` including codex/gemini command strings.

Reproduction Steps:
- Not provided

Requirements / Constraints:
- `exec_prompt` must be short and deterministic; explicitly “no code fences”.
- Executor enum values and tooling fields as written in the ticket text.

Evidence:
- References: “Extend `_actions.json` to include an executor plan per item… `executor`… `exec_prompt`… `inputs`…”

Open Items / Unknowns:
- The exact existing `_actions.json` schema shape and where it’s validated (not provided).
- Whether `missing_artifacts` / repo anchors already exist in the schema (not provided).

Risks / Dependencies:
- Depends on T1 if `tooling` is expected to include the new `codex_cmd`/`gemini_cmd` command strings.
- Required by T3 since the dispatcher reads `_actions.json`.

Acceptance Criteria:
- `_actions.json` schema includes `tooling` with `{ oracle_cmd, task_master_cmd, codex_cmd, gemini_cmd }`.
- Each action item can include:
  - `executor` with allowed values: `codex`, `gemini`, `tm`, `manual`
  - `exec_prompt` (no code fences requirement captured)
  - optional `inputs` list
- Prompt A output includes these fields per item.

Priority & Severity (if inferable from input text):
- Priority: Not provided
- Severity: Not provided

Source:
- “Extend `_actions.json` to include an executor plan per item”
- “`executor`: ‘codex’ | ‘gemini’ | ‘tm’ | ‘manual’”
- “`exec_prompt`: a short instruction string (no code fences; deterministic)”
```

```ticket T3
T3 Title:
- Add Action Pack “implement” mode (or Step 09) to dispatch executor commands based on `_actions.json`

Type:
- enhancement

Target Area:
- Stage-3 Action Pack template (runner-ingestible actionpack artifacts)

Summary:
- Action Packs must execute real tool commands to implement tasks; adding more `oracle` calls only produces more analysis/synthesis.
- Add an implement execution path that reads `_actions.json`, selects top N actions (`top_n`), and dispatches to the specified executor (e.g., `codex exec …`, `gemini -p …`) using per-item metadata.

In Scope:
- Add either:
  - `mode=implement`, or
  - a new Step 09 guarded by `if MODE == implement`
- Implement-mode behavior:
  - Read `<out_dir>/_actions.json`
  - Select the top N items (uses existing `top_n`)
  - Dispatch deterministically:
    - `codex exec …` for items with `executor=codex`
    - `gemini -p …` for items with `executor=gemini`
- Keep existing modes (`backlog|pipelines|autopilot`) intact.

Out of Scope:
- Changing oraclepack core logic for overrides/flag injection/validation (handled in T5)

Current Behavior (Actual):
- Implementation happens only when the Action Pack runs real tool commands (e.g., `task-master …`, `tm autopilot`).
- The Action Pack already runs non-oracle tools (`task-master …` and `tm autopilot`) and includes guarded branch checks for autopilot.

Expected Behavior:
- An implement mode/step exists that consumes `_actions.json` and runs executor-specific commands deterministically based on per-item metadata.

Reproduction Steps:
- Not provided

Requirements / Constraints:
- Must read `<out_dir>/_actions.json`.
- Must use existing `top_n` to select items.
- Must dispatch based on `executor` field.
- Preserve deterministic behavior as described (stable ordering / fail-fast preflight referenced as desired properties).

Evidence:
- References: “Add an ‘implement’ mode (or a Step 09)… reads `<out_dir>/_actions.json`… selects the top N… dispatches deterministically…”

Open Items / Unknowns:
- The exact Action Pack template file path and step numbering constraints (not provided).
- How “top N” is currently computed/ordered (not provided).

Risks / Dependencies:
- Depends on T2 since implement mode reads `_actions.json` executor metadata.
- Safety defaults for tool execution are addressed in T4.

Acceptance Criteria:
- Action Pack supports `mode=implement` (or an equivalent Step 09 guarded behavior).
- Implement mode:
  - Reads `<out_dir>/_actions.json`
  - Selects top N via `top_n`
  - Runs `codex exec …` for `executor=codex`
  - Runs `gemini -p …` for `executor=gemini`
- Existing `backlog|pipelines|autopilot` behavior remains unchanged.

Priority & Severity (if inferable from input text):
- Priority: Not provided
- Severity: Not provided

Source:
- “implementation only happens when the Action Pack runs real tool commands”
- “Add an ‘implement’ mode (or a Step 09)… reads `<out_dir>/_actions.json`… selects the top N… dispatches deterministically”
```

```ticket T4
T4 Title:
- Enforce conservative safety defaults for executor dispatch (no auto-approve “yolo” behavior unless opted-in)

Type:
- chore

Target Area:
- Action Pack implement/dispatcher execution path (gemini/codex dispatch configuration)

Summary:
- The ticket explicitly calls out safety: executor tools may run commands or perform actions; defaults should be conservative.
- Add/confirm guardrails so the implement/dispatcher mode does not auto-approve tool execution by default, and requires explicit opt-in for riskier behaviors.

In Scope:
- Ensure implement/dispatcher mode defaults are “strict” and not “yolo by default”.
- Ensure any approval/tool-execution modes are conservative unless a user opts in (mechanism not specified in the ticket text).

Out of Scope:
- Defining new security models or sandboxing systems beyond what’s stated (not provided).

[TRUNCATED]
```

.tickets/actions/Oraclepack Compatibility Issues.md
```
Parent Ticket:

* Title: Oraclepack Actionpack Compatibility: non-`oracle` tools execution, dispatcher/overrides gaps, and adding Codex/Gemini headless steps
* Summary:

  * There is confusion about whether oraclepack can run Action Packs that include non-`oracle` commands (e.g., `task-master` / `tm`, `codex`, `gemini`). The current behavior is that oraclepack executes each step as shell (`bash -lc ...`) and only applies oracle-specific injection/validation to commands that begin with `oracle`. The request also includes adding headless `gemini` + non-interactive `codex exec` automation into the placeholder steps of `ticket-action-pack.md`, and optionally extending this pattern to taskify-generated packs.
* Source:

  * Link/ID: Not provided
  * Original ticket excerpt (≤25 words) capturing the overall theme: “injects flags into commands that begin with `oracle`… does not match `tm`, `codex`, `gemini`… won’t dispatch/wrap non-`oracle` commands” (per file)
* Global Constraints:

  * Action Pack is “oraclepack-ingestible” (single `bash` fence, `# NN)` steps) (per file)
  * Do not assume oraclepack overrides apply to non-`oracle` commands (per file)
* Global Environment:

  * Steps run via `bash -lc ...` in the project root; oraclepack does not change `WorkDir` to `out_dir` (per file)
  * ROI filter behavior may skip steps with no `ROI=` if a threshold > 0 is used (per file)
* Global Evidence:

  * References: `oracle_pack_and_taskify-skills.md`, `oraclepack-tui.md`, `ticket-action-pack.md` (per file)
  * Noted behaviors: oracle-only regex targeting; override validation runs `oracle --dry-run summary` on detected oracle invocations (per file)
  * Mentioned outputs: `.oraclepack/ticketify/_tickets_index.json`, `_actions.json`, `_actions.md`, `.taskmaster/docs/tickets_prd.md`, `.oraclepack/ticketify/tm-complexity.json`, `ticket-action-pack.state.json`, `ticket-action-pack.report.json` (per file)

Split Plan:

* Coverage Map:

  * “oraclepack runs each step as a bash script (`bash -lc <step script>`).” → T1
  * “injects flags into commands that begin with `oracle`… regex anchored to `^(\\s*)(oracle)\\b`.” → T2
  * “TUI override validation… runs `oracle --dry-run summary`… skips steps without oracle invocations.” → T2
  * “`tm` / `task-master` run directly… not routed through oracle.” → T1
  * “If you manually add `codex` / `gemini` lines… oraclepack will try to run them directly.” → T1
  * “If CLI isn’t installed/on PATH → step fails; if interactive → blocks.” → T3
  * “If you don’t add those commands… pack mainly uses `oracle` + Task Master; won’t ‘implement’ via Codex/Gemini.” → T1
  * `ticket-action-pack.md` Step 01 writes `.oraclepack/ticketify/_tickets_index.json` → Info-only
  * `ticket-action-pack.md` Step 02 writes `_actions.json` + `_actions.md` → Info-only
  * `ticket-action-pack.md` Step 03 writes `.taskmaster/docs/tickets_prd.md` → Info-only
  * `ticket-action-pack.md` Steps 05–07 run `task-master parse-prd`, `analyze-complexity`, `expand --all` and write `.oraclepack/ticketify/tm-complexity.json` → Info-only
  * “Steps 08–20 are placeholders/notes (echo guidance).” → T3
  * “Best insertion points… placeholder steps (09–13 and 16).” → T3
  * Step 09: Gemini headless selects next target, writes `.oraclepack/ticketify/next.json` → T3
  * Step 10: `codex exec` implements selected action, writes `.oraclepack/ticketify/codex-implement.md` → T3
  * Step 11: verification via `codex exec` and/or Gemini diff review, writes `.oraclepack/ticketify/codex-verify.md` and/or `.oraclepack/ticketify/gemini-review.json` → T3
  * Step 16: Gemini drafts PR body, writes `.oraclepack/ticketify/PR.md` → T3
  * “Optional… add an agent-mode to oraclepack-taskify packs… keep 20-step contract intact.” → T4
  * “Key constraint… overrides only target commands that begin with `oracle`; codex/gemini won’t inherit unless wrap/extend oraclepack.” → T2
  * Failure notes: missing `.tickets/`, missing `task-master` / provider keys, ROI filter gotcha → T1
* Dependencies:

  * T3 depends on T2 because codex/gemini steps will not participate in oraclepack override injection/validation unless oraclepack is extended beyond `oracle`-prefixed commands (per file).
  * T4 depends on T2 for the same reason (per file).
* Split Tickets:

```ticket T1
T# Title:
- Clarify current oraclepack Action Pack execution semantics (and common failure modes)

Type:
- docs

Target Area:
- oraclepack CLI/TUI user-facing documentation (exact file(s) not provided)

Summary:
- Document the current behavior that oraclepack executes Action Pack steps as `bash -lc ...` and only applies oracle-specific behavior to `oracle`-prefixed commands. Capture practical implications for running packs containing `task-master`/`tm`, `codex`, and `gemini`, including common failure modes and the ROI filter gotcha noted in the ticket content.

In Scope:
- Document that steps execute as shell (`bash -lc ...`) and whatever commands appear in the step body are executed.
- Document that non-`oracle` commands (`task-master`/`tm`, `codex`, `gemini`) run directly and are not routed through oracle.
- Document the “interactive CLI can block” and “missing binary on PATH fails the step” implications.
- Document `ticket-action-pack.md` likely failure points called out: missing `.tickets/`, missing `task-master`/provider configuration/API keys, ROI filter gotcha.
- Document that Steps 08–20 are placeholders unless replaced with real commands.

Out of Scope:
- Changing oraclepack dispatcher / override injection logic.
- Editing `ticket-action-pack.md` steps to add new automation.

Current Behavior (Actual):
- Confusion among reviewers/users about whether oraclepack “runs everything through oracle.”
- Running packs without `oracle ...` commands results in no oracle-specific override behavior being applied.

Expected Behavior:
- A clear, discoverable doc section explains what oraclepack does/does not do with non-`oracle` commands and how to interpret failures.

Reproduction Steps:
- Not provided.

Requirements / Constraints:
- Preserve the current semantics described in the ticket content (no implied change to execution model).

Evidence:
- “oraclepack runs each step as a bash script (`bash -lc <step script>`).”
- “If the CLI is interactive → it will block waiting for input.”
- “ROI filter gotcha… steps with no `ROI=`… may be skipped.”

Open Items / Unknowns:
- Where documentation should live (README, `oraclepack-tui.md`, or other) is not provided.
- Whether this should be shown in TUI help text vs repository docs is not provided.

Risks / Dependencies:
- Not provided.

Acceptance Criteria:
- Documentation explicitly states:
  - Steps execute via `bash -lc ...` and run the literal commands present.
  - Only `oracle`-prefixed commands receive oraclepack’s special handling.
  - Non-`oracle` tools (`tm`/`task-master`, `codex`, `gemini`) run directly (PATH/interactive caveats included).
  - The listed failure modes and ROI filter gotcha are described with practical guidance.
- Documentation includes a short “What to expect after running `ticket-action-pack.md`” section referencing the artifact paths named in the ticket content.

Priority & Severity (if inferable from input text):
- Priority: Not provided
- Severity: Not provided

Source:
- “oraclepack runs each step as a bash script (`bash -lc <step script>`).”
- “injects flags into commands that begin with `oracle`… does not match `tm`, `codex`, `gemini`.”
- “ROI filter gotcha… steps with no `ROI=`… may be skipped.”
```

```ticket T2
T# Title:
- Extend oraclepack override injection/validation beyond `oracle`-prefixed commands (dispatcher changes)

Type:
- enhancement

Target Area:
- oraclepack command detection + overrides/validation pipeline (regex/dispatch behavior; exact file paths not provided)

Summary:
- The current oraclepack behavior applies oracle-specific transforms only to commands that begin with `oracle` and the TUI override validation only targets oracle invocations. Implement “dispatcher changes” so that non-`oracle` commands (explicitly referenced: `tm`/`task-master`, `codex`, `gemini`) can participate in the same override/validation flow, or otherwise be handled explicitly as first-class command targets.

In Scope:
- Update command detection so it is not limited to `oracle` (currently anchored to `^(\\s*)(oracle)\\b` per the ticket text).
- Update override validation so it does not only run `oracle --dry-run summary` on detected oracle invocations and skip steps without oracle invocations.
- Ensure steps containing `tm`/`task-master`, `codex`, and/or `gemini` can be detected for dispatcher/validation purposes (as described in the ticket content).

Out of Scope:
- Adding new `codex exec` / `gemini` automation steps to specific packs (handled in T3).
- Changing Task Master’s behavior or requirements.

Current Behavior (Actual):
- Flag injection “only… injects flags into commands that begin with `oracle`… does not match `tm`, `task-master`, `codex`, `gemini`.”
- TUI override validation “only targets `oracle` commands… runs `oracle --dry-run summary`… steps without oracle invocations are skipped.”

Expected Behavior:
- Dispatcher/override handling is not limited to `oracle`-prefixed commands for the explicitly mentioned tool commands, so non-`oracle` steps are not silently excluded from override/validation.

Reproduction Steps:
- Not provided.

Requirements / Constraints:
- Must preserve existing `oracle` command behavior.
- Must address the limitation called out: overrides/validation currently only target `oracle` commands.

Evidence:
- “injects flags into commands that begin with `oracle` (regex… `^(\\s*)(oracle)\\b`).”
- “override validation… runs `oracle --dry-run summary`… steps without oracle invocations are skipped.”
- “codex/gemini won’t inherit oraclepack overrides unless you wrap them yourself or extend oraclepack.”

Open Items / Unknowns:
- Exact desired behavior for applying overrides to `tm`/`task-master`, `codex`, and `gemini` is not provided (which flags apply, how validation works).
- Whether the dispatcher should “interpret actions” vs only broaden prefix-based detection is not provided.

Risks / Dependencies:
- Risk: unclear spec for how overrides should apply to each non-`oracle` tool could lead to partial/incorrect behavior.

Acceptance Criteria:
- A pack step containing at least one of the referenced non-`oracle` command prefixes (`tm`/`task-master`, `codex`, `gemini`) is no longer automatically excluded from the override/validation pipeline solely due to not starting with `oracle`.
- Existing behavior for `oracle`-prefixed commands remains unchanged.

Priority & Severity (if inferable from input text):
- Priority: Not provided
- Severity: Not provided

Source:
- “injects flags into commands that begin with `oracle`… does not match `tm`, `codex`, `gemini`.”
- “override validation… only targets `oracle` commands… steps without oracle invocations are skipped.”
- “codex/gemini won’t inherit oraclepack overrides unless you wrap them yourself or extend oraclepack.”
```

```ticket T3
T# Title:
- Replace `ticket-action-pack.md` placeholder steps with headless Gemini + non-interactive Codex automation

Type:
- enhancement

Target Area:
- `ticket-action-pack.md` (ticketify Action Pack content/template; exact generator location not provided)

Summary:
- Steps 08–20 are described as placeholders/notes that only echo guidance. Replace specific placeholder steps (explicitly called out: 09–13 and 16) to add end-to-end automation using headless `gemini` and `codex exec`, producing machine-readable and human-readable artifacts under `.oraclepack/ticketify/`.

In Scope:
- Step 09: Add headless `gemini` selection that writes `.oraclepack/ticketify/next.json`.
- Step 10: Add non-interactive `codex exec` implementation that consumes `next.json` and writes `.oraclepack/ticketify/codex-implement.md`.
- Step 11: Add verification automation via `codex exec` and/or Gemini diff review:
  - `.oraclepack/ticketify/codex-verify.md` and/or `.oraclepack/ticketify/gemini-review.json`.
- Step 16: Add PR draft automation that writes `.oraclepack/ticketify/PR.md`.
- Include command-availability guards and “skip” behavior as shown in the referenced step snippets (e.g., `command -v ...` checks) to avoid hard failures when tools are missing.

Out of Scope:
- Changing Steps 01–07 semantics (ticket discovery/actions/PRD/Task Master parse/complexity/expand).
- Extending oraclepack’s override injection/validation to cover `codex`/`gemini` (handled in T2).

Current Behavior (Actual):
[TRUNCATED]
```

.tickets/other/Oraclepack Pipeline Improvements.md
```
Title:

* Implement deterministic oraclepack pipeline improvements (strict validation, run manifests, resume/caching, Stage 3 “Actionizer”)

Summary:

* The current two-stage oraclepack workflow (Stage 1 pack generation → Stage 2 execution) is “weakly connected” and lacks deterministic handoff metadata, robust resume/retry, and an automated Stage 3 that converts 20 outputs into actionable engineering work.

    Oracle Pack Workflow Analysis

* This ticket proposes additive, backward-compatible enhancements to oraclepack and the Stage 1 generator prompts so runs are reproducible, CI-friendly, and produce machine-readable artifacts suitable for automation.

    Oracle Pack Workflow Analysis

Background / Context:

* Workflow context:

  * Stage 1: Codex skill or Gemini CLI slash command generates a single Markdown oracle question pack under `docs/*oracle-pack*.md`, following a strict oraclepack schema and containing exactly 20 `oracle ...` commands.

        Oracle Pack Workflow Analysis

  * Stage 2: oraclepack (Go wrapper around `@steipete/oracle`) executes the 20 commands and writes per-question outputs (via `--write-output`).

        Oracle Pack Workflow Analysis

  * Stage 3 is currently missing: outputs are not automatically turned into actionable implementation work.

        Oracle Pack Workflow Analysis

* Non-negotiable constraints:

  * No schema-breaking changes to the oraclepack Markdown pack schema without a backward-compatible migration path and validator-safe proof.

  * Automation must be deterministic and reproducible (no interactive steps in the critical path).

  * Stage 1 output must remain a single-pack deliverable that oraclepack can validate/run (no extra blocks/headers; no schema drift).

  * Prefer minimal file attachments per question; avoid broad globs unless unavoidable.

  * Optimize for longer runtimes with minimal human interaction (batching, resume/retry, caching, stable outputs, CI-friendly).

        Oracle Pack Workflow Analysis

Current Behavior (Actual):

* Stage 1 (generation) failure modes / friction points:

  * Packs can drift from the strict schema (extra fenced blocks, step-like headers, missing fields, wrong ordering, wrong count ≠ 20), causing ingestion/validation issues.

        Oracle Pack Workflow Analysis

  * Attachments may be bloated (broad globs, “just in case” files), increasing token cost and reducing signal-to-noise.

        Oracle Pack Workflow Analysis

  * ROI scoring can be inconsistent (unstable prioritization vs stated rationale).

        Oracle Pack Workflow Analysis

  * Coverage duplication across 20 questions (overlapping targets) wastes runs/budget.

        Oracle Pack Workflow Analysis

* Stage 2 (execution) failure modes / friction points:

  * Resume/retry semantics are weak (reruns may re-execute completed steps; partial failures require manual selection).

        Oracle Pack Workflow Analysis

  * Output determinism gaps: inconsistent output paths/slugs/out\_dir naming undermine CI diffs and Stage 3 discovery.

        Oracle Pack Workflow Analysis

  * Concurrency/rate limiting is not first-class (provider throttling/timeouts lead to nondeterministic failures).

        Oracle Pack Workflow Analysis

* Cross-stage handoff issues:

  * Missing traceability between pack ↔ outputs (no explicit manifest tying outputs to pack hash, git SHA, tool versions, provider/model settings).

        Oracle Pack Workflow Analysis

  * Stage 2 may be bypassed (pack executed “by hand”), losing wrapper state/report and consistent run directory.

        Oracle Pack Workflow Analysis

Expected Behavior:

* Stage 1 packs are always validator-safe and deterministic (single pack, exactly 20 oracle invocations, no schema drift).

    Oracle Pack Workflow Analysis

* Stage 2 produces stable, discoverable, machine-readable run artifacts that bind pack ↔ outputs and enable idempotent resume/rerun.

    Oracle Pack Workflow Analysis

* Stage 3 (“Actionizer”) exists and deterministically converts the 20 outputs into actionable engineering work artifacts (backlog + change plan + optional issue export), without duplicating work on reruns.

    Oracle Pack Workflow Analysis

* CI can run validate → run → actionize non-interactively with structured outputs and policy-driven exit codes.

    Oracle Pack Workflow Analysis

Requirements:

* Validation / linting (additive, backward-compatible):

  * Add `oraclepack validate --strict --json` that reports counts (steps=20, bash\_blocks=1, oracle\_invocations=20), ordering checks (ROI desc; ties effort asc), and required fields presence.

        Oracle Pack Workflow Analysis

* Deterministic run directory + manifest:

  * On `run`, create `.oraclepack/runs/<pack_id>/` and emit `run.json` + `steps.json`.

        Oracle Pack Workflow Analysis

  * `pack_id = YYYY-MM-DD__<gitshort>__<packhash8>`.

        Oracle Pack Workflow Analysis

  * `run.json` must include: `pack_path`, `pack_hash`, `git_sha`, `oraclepack_version`, `oracle_version`, `created_at`.

        Oracle Pack Workflow Analysis

  * `steps.json` must include: `step_id` (01..20), `reference`, `category`, `roi`, `command_hash`, `output_path`, `output_hash`, `status` (pending|ok|failed|skipped).

        Oracle Pack Workflow Analysis

* Resume/rerun semantics:

  * Make resume default: if `run.json` exists, skip steps whose output exists and matches recorded hash.

  * Support explicit overrides: `--rerun all|failed|01,03,07`.

        Oracle Pack Workflow Analysis

* Concurrency and rate limiting:

  * Add global `--max-parallel N` and optionally per-provider caps via config.

  * Implement exponential backoff + jitter on transient errors (e.g., 429/503) with a retry budget.

        Oracle Pack Workflow Analysis

* Deterministic caching (optional initially):

  * Implement caching keyed by `sha256(prompt + attached_file_hashes + oracle_flags + model)`, stored under `.oraclepack/cache/<invocation_key>.md`; rerun reuses cached outputs when key matches.

        Oracle Pack Workflow Analysis

* Stage 3 (“Actionizer”) design and artifacts:

  * Implement `oraclepack actionize --run-dir .oraclepack/runs/<pack_id>`.

  * Inputs: `run.json` + 20 outputs under `.oraclepack/runs/<pack_id>/outputs/`.

        Oracle Pack Workflow Analysis

  * Deterministic processing: normalize outputs → dedupe → categorize via fixed taxonomy → generate action tasks, including blocked/conflict handling.

        Oracle Pack Workflow Analysis

  * Outputs under `.oraclepack/runs/<pack_id>/actionizer/`:

    * `normalized.jsonl`, `backlog.md`, `change-plan.md`

    * Optional: `github-issues.json`, `taskmaster.json`.

            Oracle Pack Workflow Analysis

  * Idempotency: stable IDs derived from `pack_hash` (e.g., `OP3-<packhash8>-<issue_index>-<task_index>`), stable paths, byte-identical regeneration when inputs unchanged.

        Oracle Pack Workflow Analysis

* Stage 1 prompt/skill improvements (without breaking schema):

  * Embed structured mini-metadata inside each `-p` prompt text (not new pack headers), e.g., `QuestionId`, `Category`, `Reference`, `ExpectedArtifacts`.

        Oracle Pack Workflow Analysis

  * Enforce deterministic attachment minimization heuristics (reference file + 0–2 neighbors; avoid broad globs unless evidence demands).

        Oracle Pack Workflow Analysis

  * Standardize generator prompt across Codex skills and Gemini CLI commands using a single canonical prompt file in repo.

        Oracle Pack Workflow Analysis

* CI-native mode:

  * Provide `oraclepack run --ci --non-interactive --json-log` and `oraclepack actionize --ci`.

  * CI policy can fail build if validation fails, completion rate below threshold, or action yield below threshold (threshold values: Not provided).

        Oracle Pack Workflow Analysis

* Security/safety:

  * Path safety: prevent `--write-output` from escaping run dir (reject `..` traversal).

        Oracle Pack Workflow Analysis

Out of Scope:

* Breaking changes to the existing oraclepack Markdown pack schema (unless a backward-compatible migration path and validator-safe proof are provided).

    Oracle Pack Workflow Analysis

Reproduction Steps:

1. Generate a pack via Stage 1 and save to `docs/oracle-pack-YYYY-MM-DD.md`.

    Oracle Pack Workflow Analysis

2. Run `oraclepack validate docs/oracle-pack-YYYY-MM-DD.md` and observe schema drift / strictness gaps (exact current validator behavior: Unknown).

    Oracle Pack Workflow Analysis

3. Execute the pack, interrupt mid-run, rerun, and observe whether completed steps are skipped (current behavior: weak/unclear).

    Oracle Pack Workflow Analysis

4. Compare two runs on the same commit and observe output path/slug stability and traceability artifacts (manifest missing today).

    Oracle Pack Workflow Analysis

Environment:

* Tooling:

  * oraclepack (Go wrapper around `@steipete/oracle`).

        Oracle Pack Workflow Analysis

  * Stage 1 generators: Codex skills or Gemini CLI slash commands.

        Oracle Pack Workflow Analysis

* Repository/OS/versions: Unknown (git SHA, oraclepack version, oracle version, provider/model settings not provided in the conversation; also identified as missing traceability today).

    Oracle Pack Workflow Analysis

Evidence:

* Proposed stable artifact layout and handoff contract:

    Oracle Pack Workflow Analysis

  * `docs/oracle-pack-YYYY-MM-DD.md`

  * `.oraclepack/runs/<pack_id>/run.json`

  * `.oraclepack/runs/<pack_id>/steps.json`

  * `.oraclepack/runs/<pack_id>/outputs/01-<slug>.md … 20-<slug>.md`

  * `.oraclepack/runs/<pack_id>/actionizer/{normalized.jsonl, backlog.md, change-plan.md}`

* Proposed commands (some flag names explicitly “proposed where not already present”):

    Oracle Pack Workflow Analysis

  * `oraclepack validate --strict docs/oracle-pack-YYYY-MM-DD.md --json > .oraclepack/validate.json`

  * `oraclepack run docs/oracle-pack-YYYY-MM-DD.md --max-parallel 4 --resume --ci`

  * `oraclepack actionize --run-dir .oraclepack/runs/<pack_id> --ci`

* Example Stage 3 output record shape (JSONL line):

    Oracle Pack Workflow Analysis

Decisions / Agreements:

* Do not break the oraclepack Markdown pack schema; any change must be backward-compatible with a validator-safe proof.

    Oracle Pack Workflow Analysis

* Stage 3 (“Actionizer”) is required and should be implemented as a first-class oraclepack subcommand (`actionize`) producing deterministic artifacts with idempotent reruns.

    Oracle Pack Workflow Analysis

* Traceability and determinism should be achieved via additive sidecar files (e.g., `run.json`, `steps.json`) rather than pack schema changes.

    Oracle Pack Workflow Analysis

Open Items / Unknowns:

* Current oraclepack CLI surface area:

  * Whether `validate --strict`, `--json`, `run --ci`, `--resume`, `--json-log`, and `actionize` exist today vs need implementation (conversation notes some flags are “proposed”).

        Oracle Pack Workflow Analysis

* Current on-disk state/report formats and locations (“state lives today (intended): oraclepack state/report + per-step outputs”; exact paths not provided).

    Oracle Pack Workflow Analysis

* Threshold definitions for CI policy (“completion rate < threshold”, “action yield < threshold”): Not provided.

    Oracle Pack Workflow Analysis

* Exact strict pack schema invariants enforced today (beyond “strict output contract” and “exactly 20” requirement): Not provided in this conversation (referenced as external inputs).

    Oracle Pack Workflow Analysis

Risks / Dependencies:

* Risk: filesystem layout changes may affect existing users; mitigation is additive behavior that preserves current out\_dir behavior.

    Oracle Pack Workflow Analysis

* Risk: caching correctness depends on hashing all attached file contents; incomplete hashing risks “cache poisoning.”

    Oracle Pack Workflow Analysis

* Risk: provider throttling/timeouts require robust transient-error classification for backoff/retry behavior.

    Oracle Pack Workflow Analysis

* Dependency: Stage 3 quality depends on stable, parseable structure in per-question outputs; mitigated by deterministic normalization heuristics and improved Stage 1 prompt shaping.

Acceptance Criteria:

* Validation:

  * `oraclepack validate --strict --json` deterministically reports schema invariants (20 steps, 20 oracle invocations, schema drift detection) and can gate CI.

        Oracle Pack Workflow Analysis

* Run determinism and traceability:

  * Running a pack produces `.oraclepack/runs/<pack_id>/{run.json,steps.json,outputs/}` with stable `pack_id` and stable output naming.

  * `run.json` includes required metadata fields; `steps.json` includes required per-step fields and statuses.

        Oracle Pack Workflow Analysis

* Resume/rerun:

  * Interrupting a run mid-way and rerunning resumes without re-executing completed steps (validated via output hashes and statuses).

  * `--rerun failed|all|<step list>` behaves as specified.

        Oracle Pack Workflow Analysis

* Concurrency/rate limiting:

  * `--max-parallel N` bounds concurrency; transient failures (e.g., throttling/timeouts) are retried with backoff within a retry budget and recorded in step status.

        Oracle Pack Workflow Analysis

* Caching (if implemented):

  * Rerunning on unchanged inputs (same prompt, same attached file digests, same flags/model) results in zero provider calls and identical outputs.

        Oracle Pack Workflow Analysis

* Stage 3 “Actionizer”:

  * `oraclepack actionize --run-dir ...` generates deterministic artifacts under `actionizer/` (`normalized.jsonl`, `backlog.md`, `change-plan.md`).

  * Reruns do not duplicate tasks (stable IDs) and produce byte-identical output when inputs unchanged.

  * Missing/contradictory answers produce explicit `blocked`/`conflict` tasks with required evidence patterns.

* CI mode:

[TRUNCATED]
```

.tickets/other/Oraclepack Prompt Generator.md
```
* Title: Create oraclepack-style prompt/skill generator for tickets and .tickets
* Summary:

  * Need a reusable prompt (and/or “skill” template) that can generate an oraclepack-style prompt/skill specifically for “tickets” and/or “.tickets”.
  * Must support the existing placeholder-driven wrapper pattern (e.g., `{user-idea}`, `{project-in-question}`, `{PAIN-POINT}`, `{REFERENCE-FILE}`, `{CAPABILITY}`, `{TARGET-AGENT}`, `{OPTIMIZE-PROMPT}`), including optional fields and “infer from context” behavior as described.
  * Also need guidance on what to change in the current skill and what other viable integration options exist (within the constraints already used in prior work).
* Source:

  * Link/ID: Not provided
  * Original ticket excerpt: “prompt that can create an oraclepack prompt/skill but for our tickets and/or .tickets”
* Global Constraints:

  * Optional inputs may be omitted; proceed by inferring from context or requesting missing info within the generated prompt template (“Not always provided…”).
  * “Pain point” is optional; proceed without it if absent.
  * `{REFERENCE-FILE}` may be provided as additional constraints/spec content.
* Global Environment:

  * Unknown
* Global Evidence:

  * Existing wrapper pattern + MCP prompt/tool/resource publication precedent captured in: `/mnt/data/MCP server implementation.md`

Split Plan:

* Coverage Map:

  * Original item: “We need a prompt that can create an oraclepack prompt/skill but for our tickets and/or .tickets.”

    * Assigned Ticket ID: T2
  * Original item: “What could we do to our current skill…”

    * Assigned Ticket ID: T3
  * Original item: “…and/or what else are our options for this request?”

    * Assigned Ticket ID: T4
  * Original item: Wrapper placeholders + optionality rules (“Not always provided…”, “Our pain point…”, `{REFERENCE-FILE}`, `{TARGET-AGENT}`, `{CAPABILITY}`, `{OPTIMIZE-PROMPT}`)

    * Assigned Ticket ID: T1
  * Original item: “optimized prompt that will get the {TARGET-AGENT} to find us a solution for adding capabilities…”

    * Assigned Ticket ID: T2
* Dependencies:

  * T2 depends on T1 because the prompt/skill generator must align to the placeholder schema + optionality rules.
  * T3 depends on T2 because “current skill” changes should incorporate the finalized ticket prompt/skill template.
  * T5 depends on T2 and T3 because examples/validation need the final template and integration approach.

````ticket T1
T1 Title: Define ticket/.tickets prompt input schema and placeholder mapping
Type: docs
Target Area: Ticket input model (tickets and/or .tickets) + wrapper placeholders
Summary:
  - Define the canonical set of inputs and placeholders required to generate an oraclepack-style ticket prompt/skill.
  - Preserve the existing wrapper’s rules around optional inputs and context inference.
  - Provide a clear mapping between “tickets/.tickets” fields (if any) and wrapper placeholders without inventing unspecified fields.
In Scope:
  - Enumerate required vs optional placeholders: {user-idea}, {project-in-question}, {ADDITIONAL-INFORMATION}, {PAIN-POINT}, {REFERENCE-FILE}, {CAPABILITY}, {TARGET-AGENT}, {OPTIMIZE-PROMPT}.
  - Document handling rules explicitly stated: optional fields, “infer from context”, and behavior when pain point is absent.
  - Clarify what “tickets” vs “.tickets” means in this system using “Unknown/Not provided” where details are missing.
Out of Scope:
  - Defining new ticket fields beyond what is provided.
  - Implementing tooling or code changes (covered elsewhere).
Current Behavior (Actual):
  - Placeholder set and optionality rules exist in the wrapper pattern, but ticket/.tickets-specific mapping is not defined.
Expected Behavior:
  - A documented, stable mapping that the ticket prompt/skill generator can follow.
Reproduction Steps:
  - Not provided
Requirements / Constraints:
  - Do not add new placeholders or required fields beyond what is already used in the wrapper.
  - Preserve optionality rules: proceed safely when PAIN-POINT or additional info is absent.
Evidence:
  - Reference wrapper placeholders and prompt-engineer wrapper structure as precedent. (/mnt/data/MCP server implementation.md) :contentReference[oaicite:1]{index=1}
Open Items / Unknowns:
  - Exact structure/format of “tickets” and “.tickets” (not provided).
  - Whether “.tickets” is a file extension, folder convention, or schema name (not provided).
Risks / Dependencies:
  - Risk of mismatch between ticket data shape and placeholder mapping if .tickets format is not standardized.
Acceptance Criteria:
  - A single written spec exists that lists:
    - All placeholders and which are optional.
    - Rules for missing fields (“infer from context” as described).
    - How ticket/.tickets inputs populate placeholders (or explicitly “Unknown” where not provided).
Priority & Severity (if inferable from input text):
  - Priority: Not provided
  - Severity: Not provided
Source:
  - “Not always provided, inference from context…”
  - “Our pain point: {PAIN-POINT} … if not just continue…”
  - “```md {REFERENCE-FILE}.md”
````

```ticket T2
T2 Title: Author oraclepack-style prompt/skill template for ticket and .tickets generation
Type: enhancement
Target Area: Prompt/skill template content (oraclepack-style) for tickets/.tickets
Summary:
  - Create the actual prompt/skill template that produces an oraclepack-style prompt/skill when given a ticket or .tickets input.
  - The template must use the existing wrapper structure and placeholders, and must instruct the TARGET-AGENT to generate the desired capability for the project/tool in question.
  - Ensure the template explicitly supports optional inputs and reference-file injection as described.
In Scope:
  - Produce the “ticket prompt-engineer wrapper” template that mirrors the existing wrapper pattern but targets tickets/.tickets.
  - Include all placeholders: {user-idea}, {project-in-question}, {ADDITIONAL-INFORMATION}, {PAIN-POINT}, {REFERENCE-FILE}, {CAPABILITY}, {TARGET-AGENT}, {OPTIMIZE-PROMPT}.
  - Ensure the prompt text includes the “optimized prompt that will get the {TARGET-AGENT}…” requirement, scoped to tickets/.tickets.
Out of Scope:
  - Any new MCP server requirements, tools, or resource URI schemes not explicitly requested for tickets/.tickets.
Current Behavior (Actual):
  - There is no ticket/.tickets-specific oraclepack-style prompt/skill generator template.
Expected Behavior:
  - A single reusable prompt/skill template exists that can be filled with placeholders to drive a TARGET-AGENT to create ticket/.tickets capabilities.
Reproduction Steps:
  - Not provided
Requirements / Constraints:
  - Must follow the wrapper’s optionality rules and placeholder usage.
  - Must treat {REFERENCE-FILE} content as “additional constraints/spec” when present.
Evidence:
  - Wrapper structure and placeholder set captured in existing reference prompt material. :contentReference[oaicite:2]{index=2}
Open Items / Unknowns:
  - Where this template will live (file path/naming) in the current repo/tooling (not provided).
Risks / Dependencies:
  - Depends on T1 for a stable placeholder-to-ticket mapping.
Acceptance Criteria:
  - Template includes:
    - All stated placeholders.
    - Explicit instruction to proceed when optional fields are missing.
    - A clearly stated “question to the prompt-engineer: {OPTIMIZE-PROMPT}” section.
  - Template is copy/paste ready and self-contained.
Priority & Severity (if inferable from input text):
  - Priority: Not provided
  - Severity: Not provided
Source:
  - “create an oraclepack prompt/skill but for our tickets and/or .tickets”
  - “optimized prompt that will get the {TARGET-AGENT}… giving it {CAPABILITY}”
  - “Our question to the prompt-engineer: {OPTIMIZE-PROMPT}”
```

```ticket T3
T3 Title: Update current skill to support ticket/.tickets prompt-skill generation
Type: enhancement
Target Area: Existing “current skill” (location/name not provided)
Summary:
  - Identify changes required to the existing skill so it can generate or host the new tickets/.tickets oraclepack-style prompt/skill template.
  - Ensure the current skill can accept the ticket/.tickets inputs and populate the standardized placeholders.
  - Preserve existing behavior for non-ticket use cases (if any), since only ticket support is being added.
In Scope:
  - Incorporate the finalized template from T2 into the current skill workflow.
  - Add/adjust input handling so the current skill can be driven by “tickets and/or .tickets” as the source material.
  - Ensure optional inputs (pain point, additional information, reference file) remain optional in the workflow.
Out of Scope:
  - Designing a brand-new system if the current skill can be extended (unless extension is impossible; not provided).
Current Behavior (Actual):
  - Current skill does not explicitly support generating oraclepack-style prompts/skills for tickets/.tickets (per request).
Expected Behavior:
  - Current skill can produce the tickets/.tickets oraclepack-style prompt/skill using the same wrapper placeholder mechanism.
Reproduction Steps:
  - Not provided
Requirements / Constraints:
  - Do not remove or break existing skill behavior (implied by “current skill” extension request).
Evidence:
  - “What could we do to our current skill…”
Open Items / Unknowns:
  - Current skill name, file path, and execution context (not provided).
  - How tickets/.tickets are currently stored or passed into the system (not provided).
Risks / Dependencies:
  - Depends on T2 for the template content.
Acceptance Criteria:
  - Current skill supports a ticket/.tickets-driven flow that results in the T2 template with placeholders populated (or explicitly left blank when optional).
  - No regression to existing skill behaviors (validation method not provided; document what was exercised).
Priority & Severity (if inferable from input text):
  - Priority: Not provided
  - Severity: Not provided
Source:
  - “What could we do to our current skill…”
  - “prompt… for our tickets and/or .tickets”
```

```ticket T4
T4 Title: Document integration options for delivering the tickets/.tickets prompt-skill capability
Type: docs
Target Area: Delivery/integration approach (within existing patterns)
Summary:
  - Provide a concise options write-up for how to deliver and reuse the tickets/.tickets prompt/skill generator, aligned to the existing approach patterns already in use.
  - Focus on the two explicitly requested dimensions: changes to the current skill and “other options” for fulfilling the request.
  - Avoid committing to new systems; frame as documented options with constraints and unknowns.
In Scope:
  - Option 1: Extend current skill to include tickets/.tickets support (ties to T3).
  - Option 2: Provide a standalone tickets/.tickets prompt/skill template artifact that can be consumed independently (ties to T2).
  - List constraints/unknowns impacting option choice (e.g., unknown .tickets format, unknown current-skill location).
Out of Scope:
  - Implementing the chosen option (handled by T3 and/or T2).
Current Behavior (Actual):
  - No documented approach exists for how tickets/.tickets prompt/skill generation should be delivered.
Expected Behavior:
  - A short decision-ready document exists describing the options and what each requires, without adding new requirements.
Reproduction Steps:
  - Not provided
Requirements / Constraints:
  - Options must stay within what’s already requested (modify current skill and/or alternative ways to package the prompt/skill).
Evidence:
  - “what else are our options for this request?”
Open Items / Unknowns:
  - Whether the user prefers a single consolidated skill vs multiple dedicated skills (not provided).
Risks / Dependencies:
  - Depends on T1/T2 clarity to accurately describe what each option would deliver.
Acceptance Criteria:
  - Document lists at least:
    - “Modify current skill” option (summary, prerequisites, impact).
    - “Standalone template” option (summary, prerequisites, impact).
    - Explicit unknowns that block a final choice.
Priority & Severity (if inferable from input text):
  - Priority: Not provided
  - Severity: Not provided
Source:
  - “What could we do to our current skill…”
  - “…what else are our options for this request?”
```

````ticket T5
T5 Title: Add examples and validation checks for ticket/.tickets prompt-skill generation
Type: tests
Target Area: Examples + validation of generated prompt/skill output
Summary:
  - Provide concrete example inputs (ticket and/or .tickets) and the expected generated prompt/skill output shape for validation.
  - Ensure examples exercise optional fields (missing PAIN-POINT, missing ADDITIONAL-INFORMATION, with/without REFERENCE-FILE).
  - Add lightweight validation criteria to confirm generated output preserves placeholders and wrapper structure.
In Scope:
  - Example cases covering:
    - Only {user-idea} + {project-in-question}
    - With {PAIN-POINT}
    - With {REFERENCE-FILE}
  - Validation checklist for generated output structure (placeholders present; optional fields handled).
Out of Scope:
  - End-to-end integration tests that require specific repo tooling not provided.
Current Behavior (Actual):
  - No examples/validation for tickets/.tickets prompt-skill generation are defined.
Expected Behavior:
[TRUNCATED]
```

.tickets/other/Oraclepack Workflow Enhancement.md
```
Title:

* Stabilize oraclepack “oracle-pack” pipeline and add profile-based context + Stage-3 synthesis for actionable follow-through

Summary:

* The current two-step workflow generates an `oracle-pack` Markdown file (20 `oracle` calls) via Codex skills/Gemini CLI commands, then runs it through the `oraclepack` Go wrapper to produce outputs and state/report artifacts.

    Workflow Optimization for Oracl…

    Workflow Optimization for Oracl…

* A key failure mode is schema/format drift in the pack file (human-doc + machine-ingest combined), which can break ingestion; an example drift is step headers using an em dash (`# 01 — ROI=…`) while the documented contract expects `# NN)`.

    Workflow Optimization for Oracl…

* Requested outcome: improve workflow continuity, enable richer context injection without breaking the strict pack contract, and add an automatic next stage that turns the “final twenty questions/answers” into actionable implementation steps with minimal human interaction.

    Workflow Optimization for Oracl…

    Workflow Optimization for Oracl…

Background / Context:

* Workflow stages:

  * Stage 1: LLM authoring creates `docs/oracle-pack-YYYY-MM-DD.md` containing 20 `oracle` commands (with ROI metadata and per-step output paths).

        Workflow Optimization for Oracl…

  * Stage 2: `oraclepack` executes the pack to produce 20 outputs under `oracle-out/...` plus state/report JSON artifacts.

        Workflow Optimization for Oracl…

* `oraclepack` is a wrapper around `oracle` intended for batched/bulk requests.

    Workflow Optimization for Oracl…

* Core concern: “disconnection” after the 20-question output; desire to chain into a useful, actionable implementation stage.

    Workflow Optimization for Oracl…

    Workflow Optimization for Oracl…

Current Behavior (Actual):

* Pack file acts as both:

  * Human documentation, and

  * A strict machine-ingest contract, making formatting drift a pipeline-breaking event.

        Workflow Optimization for Oracl…

* Documented/expected step-header schema (`# NN)`) can drift to alternative formats (example: `# 01 — ROI=…`), risking parse/validation failures.

    Workflow Optimization for Oracl…

* High-risk edits include adding additional code fences (especially additional bash fences) or introducing lines that accidentally match the step-header pattern.

    Workflow Optimization for Oracl…

Expected Behavior:

* Packs remain schema-stable and reliably parse/validate/run across providers (Codex skills, Gemini CLI commands).

    Workflow Optimization for Oracl…

* Richer “skill context” can be injected without changing the pack’s ingest shape (no added code fences / no header drift).

    Workflow Optimization for Oracl…

* After Stage 2 produces 20 outputs + report JSON, a subsequent stage can automatically convert results into actionable implementation steps.

    Workflow Optimization for Oracl…

Requirements:

* Preserve the non-negotiable pack contract:

  * Pack is Markdown containing exactly one `bash` code block; the first bash block is executed.

        Workflow Optimization for Oracl…

  * Steps are identified via header pattern `# NN)` with sequential numbering starting at `01`.

        Workflow Optimization for Oracl…

  * Prelude content before the first step header executes once.

        Workflow Optimization for Oracl…

* Standardize Stage-1 generation to the strict header form `# NN)` (avoid em dash variants).

    Workflow Optimization for Oracl…

* Add a hard gate between Stage 1 and Stage 2:

  * Make `oraclepack validate` mandatory before `oraclepack run` (prevent schema drift reaching execution).

        Workflow Optimization for Oracl…

* Provide schema-safe extensibility for context injection:

  * Allow context to be injected via `oracle -p` prompt text and/or `oracle -f` file/directory attachments (preferred for larger context).

        Workflow Optimization for Oracl…

  * Use prelude variables and templating only if it does not interfere with header parsing.

        Workflow Optimization for Oracl…

  * Avoid adding extra code fences or lines resembling step headers.

        Workflow Optimization for Oracl…

* Implement “Context Profiles” as file-backed bundles:

  * Add `skills/oracle-pack/references/profiles/<name>.md` and inject via `oracle -f "$profile_file"` plus a short prompt preamble (“Follow the attached profile standards”).

  * Add an optional `profile` input to the Stage-1 skill/command, with backwards-compatible behavior when absent.

        Workflow Optimization for Oracl…

* Add a first-class Stage 3 synthesis step:

  * Provide a command shape such as `oraclepack synthesize --in oracle-out --report pack.report.json --out docs/implementation-pack-YYYY-MM-DD.md` that reads the 20 outputs, extracts proposed changes/file targets/tests, and emits a new validated pack for implementation.

        Workflow Optimization for Oracl…

  * Support minimal-interaction execution for Stage 3 (e.g., headless usage via Codex/Gemini CLI).

        Workflow Optimization for Oracl…

Out of Scope:

* Not provided.

Reproduction Steps:

* Not provided.

Environment:

* `oraclepack` Go program wrapping `oracle` CLI.

* Stage-1 generation tools mentioned: Codex skills, Gemini CLI commands.

    Workflow Optimization for Oracl…

* OS/CI details: Unknown.

Evidence:

* Attachment: “Workflow Optimization for Oraclepack.md”.

    Workflow Optimization for Oracl…

    Workflow Optimization for Oracl…

* Example schema drift called out: step headers using `# 01 — ROI=…` vs documented `# NN)`.

    Workflow Optimization for Oracl…

* Proposed validation/run sequence:

  * `oraclepack validate docs/oracle-pack-YYYY-MM-DD.md`

  * `oraclepack list docs/oracle-pack-YYYY-MM-DD.md`

  * `oraclepack run docs/oracle-pack-YYYY-MM-DD.md --no-tui --run-all --stop-on-fail=true --out-dir oracle-out`

        Workflow Optimization for Oracl…

Decisions / Agreements:

* Treat the pack as a stable intermediate representation (IR) and keep context flowing only through `-p` and `-f` to avoid breaking the ingest contract.

* Prefer “Context Profiles” as a file-backed mechanism located under `skills/oracle-pack/references/profiles/`.

* Add a validation gate (`validate` before `run`) to reduce pipeline breakage from formatting drift.

    Workflow Optimization for Oracl…

Open Items / Unknowns:

* Exact current parser/validator behavior regarding em dash header variants (whether it currently accepts them) is not provided; only that it is avoidable schema drift.

    Workflow Optimization for Oracl…

* Exact filenames/paths for current `SKILL.md` and template files in the repo are referenced conceptually but not provided in full.

    Workflow Optimization for Oracl…

* Whether `oraclepack synthesize` already exists or is a new feature request is not provided; it is described as a proposed product shape.

    Workflow Optimization for Oracl…

Risks / Dependencies:

* Dependency on `oracle` CLI flags and behavior (`-p/--prompt`, `-f/--file`, `--write-output`, `--files-report`, `--dry-run`).

    Workflow Optimization for Oracl…

* Risk of pack invalidation from added code fences, additional bash blocks, or accidental header-like lines.

    Workflow Optimization for Oracl…

* Cross-provider consistency risk (Codex skills vs Gemini CLI commands) unless Stage 1 is standardized around a shared template/profile mechanism.

    Workflow Optimization for Oracl…

Acceptance Criteria:

* Pack schema stability

  * Packs validate when they contain exactly one bash block and step headers are strictly `# NN)` starting at `01`.

  * Stage-1 generation output uses `# NN)` (no em dash variant) across providers.

        Workflow Optimization for Oracl…

* Validation gate

  * Workflow includes a required `oraclepack validate` pass before any `oraclepack run`.

        Workflow Optimization for Oracl…

* Context Profiles

  * A `profile` selection results in `oracle -f "$profile_file"` being attached per step without adding new code fences or breaking parsing.

        Workflow Optimization for Oracl…

  * Absence of `profile` preserves current behavior (backwards compatible).

        Workflow Optimization for Oracl…

* Stage 3 synthesis

  * A synthesis step can consume `oracle-out` outputs + report JSON and emit a follow-on implementation pack intended to be validated and run.

        Workflow Optimization for Oracl…

Priority & Severity (if inferable from text):

* Not provided.

Labels (optional):

* enhancement

* workflow

* cli

* parsing

* validation

* context-bundles

* automation

---
```

.tickets/other/Verbose Payload Rendering TUI.md
```
Title:

* Add verbose payload rendering in TUI to display full prepared scripts/flags for oraclepack runs

Summary:

* The TUI should support a verbose mode that prints the full “prepared payload” being executed for oraclepack runs, including effective flags (post overrides and `--chatgpt-url` injection) and the entire script passed to execution.

    Verbose TUI Payload Rendering

* This is needed to verify exactly what payloads are being sent/executed during oraclepack runs and to support tests that confirm the rendered payload contents.

    Verbose TUI Payload Rendering

Background / Context:

* Proposed approach: add a reusable “prepared payload” layer to `internal/exec.Runner` (prepare prelude/step scripts after overrides + flag injection + sanitization), then have the TUI emit these prepared payload blocks to its log viewport immediately before execution.

    Verbose TUI Payload Rendering

* Files implicated by the proposal include `internal/exec/runner.go`, `internal/tui/tui.go`, `internal/cli/run.go`, plus new helpers/tests under `internal/tui/` and `internal/exec/`.

    Verbose TUI Payload Rendering

Current Behavior (Actual):

* The TUI does not provide a verbose rendering that shows the full prepared payload (full script + effective flags + extracted `oracle …` invocations) being executed for oraclepack runs.

    Verbose TUI Payload Rendering

Expected Behavior:

* When verbose payload logging is enabled, the TUI log viewport prints a payload block before each step runs that includes: effective oracle flags, extracted oracle invocations (full lines), and the full prepared script that will be executed.

    Verbose TUI Payload Rendering

* Verbose payload logging can be enabled via CLI flag (e.g., `--verbose-payload` with `-v`) and toggled in the TUI via a keybinding (proposed: `p`).

    Verbose TUI Payload Rendering

Requirements:

* Exec runner: expose “prepared payload” APIs:

  * `PreparePreludePayload(p *pack.Prelude) PreparedPreludePayload`

  * `PrepareStepPayload(s *pack.Step) PreparedStepPayload`

  * `RunPreparedPrelude(...)` / `RunPreparedStep(...)` to execute the prepared scripts.

        Verbose TUI Payload Rendering

* Prepared payload structures must include:

  * `Script` (sanitized, post injection),

  * `EffectiveFlags` (for steps),

  * `OracleInvocations` extracted from the prepared script,

  * sanitizer `Warnings`.

        Verbose TUI Payload Rendering

* TUI formatting helper:

  * Add `internal/tui/verbose_payload.go` to format payload blocks (header, effective flags, oracle invocations, then full script).

        Verbose TUI Payload Rendering

* TUI integration:

  * Add a `verbosePayload bool` toggle to the TUI model.

  * In the run flow, call `PrepareStepPayload` and, when enabled, push formatted payload lines into `logChan` before `RunPreparedStep`.

  * Add keybinding `p` to toggle `verbosePayload`.

        Verbose TUI Payload Rendering

* CLI wiring:

  * Add `--verbose-payload` / `-v` flag and pass it into `tui.NewModel(..., verbosePayload)`.

        Verbose TUI Payload Rendering

* Tests:

  * New `internal/exec/runner_payload_test.go` verifying prepared payload includes effective flags and injected oracle command text.

  * New `internal/tui/verbose_payload_test.go` verifying formatted lines include flags, invocation, and script content.

  * Update existing TUI tests to include the new `NewModel` arg.

        Verbose TUI Payload Rendering

Out of Scope:

* Not provided.

Reproduction Steps:

* Not provided.

Environment:

* Language/runtime: Go (per referenced `.go` files and packages).

    Verbose TUI Payload Rendering

* TUI framework: Bubble Tea (`tea.NewProgram(...)` referenced).

    Verbose TUI Payload Rendering

* OS / terminal / versions: Unknown.

Evidence:

* Proposed change list and implementation sketch in: /mnt/data/Verbose TUI Payload Rendering.md

    Verbose TUI Payload Rendering

* Proposed file tree changes:

  * `internal/exec/runner.go` (modify)

  * `internal/exec/runner_payload_test.go` (new)

  * `internal/tui/verbose_payload.go` (new)

  * `internal/tui/verbose_payload_test.go` (new)

  * `internal/tui/tui.go` (modify)

  * `internal/cli/run.go` (modify)

  * Update TUI tests to pass new model arg.

        Verbose TUI Payload Rendering

Decisions / Agreements:

* Adopt a “prepared payload” abstraction in `exec.Runner` to ensure the TUI logs exactly what will run after overrides, injection, and sanitization.

    Verbose TUI Payload Rendering

* Add both CLI flag control (`--verbose-payload` / `-v`) and an in-TUI toggle (proposed key: `p`).

    Verbose TUI Payload Rendering

Open Items / Unknowns:

* Exact existing TUI run flow for prelude execution (whether/where prelude runs in TUI) is not provided; proposal notes “if you also execute a prelude… do the same.”

    Verbose TUI Payload Rendering

* Exact current `NewModel(...)` signature call sites and all affected tests/files beyond those listed are not fully enumerated (some are referenced as examples).

    Verbose TUI Payload Rendering

Risks / Dependencies:

* Not provided.

Acceptance Criteria:

* Running the TUI with `--verbose-payload` causes each executed step to prepend a log block that includes:

  * “payload (step <id>)” header,

  * “effective oracle flags: …” line,

  * extracted “oracle invocations:” section (or explicit none found),

  * full “script:” content (not truncated),

  * “end payload” footer.

        Verbose TUI Payload Rendering

* Toggling `p` in the TUI flips payload logging on/off for subsequent step executions.

    Verbose TUI Payload Rendering

* `Runner.PrepareStepPayload` produces:

  * effective flags reflecting overrides and `--chatgpt-url`,

  * a prepared script containing the injected oracle invocation with those flags.

        Verbose TUI Payload Rendering

* New tests (`runner_payload_test.go`, `verbose_payload_test.go`) pass, and existing TUI tests compile and pass after updating `NewModel` call signature.

    Verbose TUI Payload Rendering

Priority & Severity (if inferable from text):

* Not provided.

Labels (optional):

* enhancement

* tui

* logging

* exec-runner

* cli

* testing

* go

---
```

.tickets/mcp/Expose Oraclepack as MCP.md
```
Parent Ticket:

* Title: Expose oraclepack as MCP tools (with Taskify Stage-2/Stage-3 helpers)
* Summary: Provide an MCP server that exposes `oraclepack` CLI capabilities (validate/list/run) plus helper tools for Stage-2 detection/validation and Stage-3 action-pack validation/execution/artifact summarization, with secure-by-default controls (allowlisted filesystem roots, execution gating, timeouts, truncation) and support for stdio + streamable-http transports.
* Source:

  * Link/ID (if present) or “Not provided”: /mnt/data/MCP tools for Oraclepack.md
  * Original ticket excerpt (≤25 words) capturing the overall theme: “Expose `oraclepack` … as **MCP tools**, so an agent can … run packs … validate Stage-2 … validate Stage-3 … summarize artifacts.”
* Global Constraints:

  * Support MCP transports: “stdio” and “streamable-http”.
  * Security defaults: “deny-by-default execution”, “allowlisted roots”, “stdout/stderr truncation and timeouts”.
* Global Environment:

  * Unknown
* Global Evidence:

  * MCP tool list and env var config list.
  * Reference implementation structure (README/requirements and Python modules).

Split Plan:

* Coverage Map:

  * “Expose `oraclepack` (validate/list/run) … as **MCP tools**” → T1
  * “run packs non-interactively (`--no-tui --yes --run-all`)” → T5
  * “validate Stage-2 outputs (01-*.md..20-*.md)” → T4
  * “validate Stage-3 Action Packs (single ```bash fence, step headers…)” → T7
  * “summarize Stage-3 artifacts (`_actions.json`, PRD, Task Master outputs, etc.)” → T7
  * “Tools: oraclepack_validate_pack / oraclepack_list_steps / oraclepack_run_pack …” → T5
  * “Tools: oraclepack_read_file …” → T5
  * “Tools: … taskify_detect_stage2 / taskify_validate_stage2 …” → T5
  * “Tools: … taskify_validate_action_pack / taskify_artifacts_summary …” → T5
  * “Tools: … taskify_run_action_pack …” → T5
  * “Transports: stdio … streamable-http …” → T6
  * “Tool annotations: readOnlyHint / destructiveHint / openWorldHint …” → T6
  * “Security defaults: ORACLEPACK_ENABLE_EXEC=1 gating …” → T2
  * “Security defaults: allowlisted filesystem roots …” → T2
  * “Security defaults: truncation and timeouts …” → T3
  * “Env vars: ORACLEPACK_ALLOWED_ROOTS / BIN / WORKDIR / ENABLE_EXEC / CHARACTER_LIMIT / MAX_READ_BYTES” → T2
  * “Typical Stage-3 usage: detect/validate → validate action pack → run → summarize” → Info-only
  * “Reference implementation tree (README, requirements.txt, modules list)” → Info-only
  * Links to MCP specs / python-sdk repo mentioned → Info-only
* Dependencies:

  * T5 depends on T2 because server tools must enforce allowed roots and execution gating.
  * T5 depends on T3 because `oraclepack_*run*` tools need subprocess execution with timeouts/truncation.
  * T5 depends on T4 because `oraclepack_taskify_*stage2*` tools call Stage-2 detection/validation.
  * T5 depends on T7 because `oraclepack_taskify_*action_pack*` tools call action-pack validation/summarization helpers.
  * T6 depends on T5 because annotations/transport-hardening apply to the MCP server surface.
* Split Tickets:

```ticket T1
T# Title: Scaffold oraclepack MCP server project (README + packaging entrypoints)
Type: chore
Target Area: oraclepack-mcp-server repo scaffolding (README.md, requirements.txt, __init__.py, __main__.py)
Summary:
- Create the MCP server project structure that exposes `oraclepack` + Taskify helpers as MCP tools, including installation and run instructions and the tool list.
- Ensure the package has an executable entrypoint to start the MCP server with selectable transport.
In Scope:
- Create/maintain project tree with:
  - README describing purpose, install, configuration env vars, run modes, tools list, and typical Stage-3 usage.
  - requirements.txt listing dependencies.
  - Python package layout with `oraclepack_mcp_server/__init__.py` and `oraclepack_mcp_server/__main__.py`.
- CLI args in `__main__.py` to accept `--transport` with choices `stdio` and `streamable-http`.
Out of Scope:
- Implementing tool logic (handled in other tickets).
Current Behavior (Actual):
- Not provided.
Expected Behavior:
- A runnable package that starts an MCP server with a chosen transport.
Reproduction Steps:
- Not provided.
Requirements / Constraints:
- Must support `--transport` choices: `stdio`, `streamable-http`.
Evidence:
- Project tree and entrypoint snippet showing `choices=["stdio", "streamable-http"]`. :contentReference[oaicite:26]{index=26}
Open Items / Unknowns:
- Exact repository root / packaging approach (pip package vs repo-local module): Not provided.
Risks / Dependencies:
- Not provided.
Acceptance Criteria:
- [ ] Repository includes README.md, requirements.txt, and `oraclepack_mcp_server` package directory.
- [ ] Running `python -m oraclepack_mcp_server --transport stdio` is supported (starts server process).
- [ ] Running `python -m oraclepack_mcp_server --transport streamable-http` is supported (starts server process).
Priority & Severity (if inferable from input text):
- Priority: Not provided
- Severity: Not provided
Source:
- “oraclepack-mcp-server (MCP wrapper for oraclepack + taskify helpers) … tree … README.md … requirements.txt … __main__.py” :contentReference[oaicite:27]{index=27}
- “choices=[‘stdio’, ‘streamable-http’] … mcp.run(transport=args.transport)” :contentReference[oaicite:28]{index=28}
```

```ticket T2
T# Title: Implement config + filesystem security controls (allowed roots, exec gating, max read bytes)
Type: chore
Target Area: oraclepack_mcp_server/config.py and oraclepack_mcp_server/security.py
Summary:
- Implement secure-by-default configuration for the MCP server, driven by environment variables, including allowlisted filesystem roots and explicit execution enablement.
- Provide safe path resolution under allowed roots and bounded file reads for tool operations.
In Scope:
- Env-driven config including:
  - `ORACLEPACK_ALLOWED_ROOTS` (colon-separated roots)
  - `ORACLEPACK_BIN`
  - `ORACLEPACK_WORKDIR`
  - `ORACLEPACK_ENABLE_EXEC`
  - `ORACLEPACK_CHARACTER_LIMIT`
  - `ORACLEPACK_MAX_READ_BYTES`
- Path allowlisting:
  - Resolve requested file paths and ensure they live under at least one allowed root.
  - Raise an explicit “path not allowed” error on violation.
- Safe file reads:
  - Read text/bytes with max size enforcement and “truncated” indicator.
Out of Scope:
- Subprocess execution and output truncation (handled in T3).
Current Behavior (Actual):
- Not provided.
Expected Behavior:
- Server loads config from env and enforces filesystem access boundaries for read tools.
Reproduction Steps:
- Not provided.
Requirements / Constraints:
- Execution must be deny-by-default unless `ORACLEPACK_ENABLE_EXEC=1`.
- Filesystem access must be restricted to allowlisted roots.
Evidence:
- Env var list and semantics. :contentReference[oaicite:29]{index=29}
- Security guidance: “deny-by-default execution … allowlisted roots”. :contentReference[oaicite:30]{index=30}
Open Items / Unknowns:
- Whether Windows path separator support is required for `ORACLEPACK_ALLOWED_ROOTS`: Not provided.
Risks / Dependencies:
- Not provided.
Acceptance Criteria:
- [ ] Config loader reads the listed env vars and applies defaults as documented.
- [ ] Path resolution rejects paths outside allowed roots.
- [ ] File reads enforce max bytes and indicate truncation.
- [ ] Exec gating flag is available for run tools to check.
Priority & Severity (if inferable from input text):
- Priority: Not provided
- Severity: Not provided
Source:
- “Environment variables: ORACLEPACK_ALLOWED_ROOTS … ORACLEPACK_ENABLE_EXEC … ORACLEPACK_MAX_READ_BYTES …” :contentReference[oaicite:31]{index=31}
- “Hard deny-by-default execution … Restrict filesystem access to allowlisted roots …” :contentReference[oaicite:32]{index=32}
```

```ticket T3
T# Title: Implement oraclepack subprocess runner with timeouts and stdout/stderr truncation
Type: chore
Target Area: oraclepack_mcp_server/oraclepack_cli.py (subprocess execution)
Summary:
- Provide a subprocess wrapper to invoke the `oraclepack` CLI with a hard timeout and bounded stdout/stderr capture to prevent wedging the MCP server.
- Return structured results including exit code, duration, and truncation indicators.
In Scope:
- Async subprocess execution wrapper (create subprocess, capture stdout/stderr).
- Timeout behavior:
  - Kill process on timeout and return an explicit “Timed out after {timeout}s” error result.
- Character-limit truncation for stdout/stderr based on configured limit.
Out of Scope:
- MCP tool registration (handled in T5).
Current Behavior (Actual):
- Not provided.
Expected Behavior:
- Running oraclepack commands yields deterministic, bounded outputs suitable for returning via MCP tools.
Reproduction Steps:
- Not provided.
Requirements / Constraints:
- Must enforce “stdout/stderr truncation and timeouts”.
Evidence:
- Guidance: “Enforce stdout/stderr truncation and timeouts …”. :contentReference[oaicite:33]{index=33}
- Runner timeout error example snippet. :contentReference[oaicite:34]{index=34}
Open Items / Unknowns:
- Default timeout values per tool beyond examples (3600/7200) are not provided outside snippets.
Risks / Dependencies:
- Not provided.
Acceptance Criteria:
- [ ] Runner returns: ok, exit_code, duration_s, stdout, stderr, stdout_truncated, stderr_truncated.
- [ ] Timeout produces exit_code=124 (or equivalent) and includes a timeout message.
- [ ] Outputs are truncated to configured character limit and flags are set accordingly.
Priority & Severity (if inferable from input text):
- Priority: Not provided
- Severity: Not provided
Source:
- “Enforce stdout/stderr truncation and timeouts so a pack can’t wedge the server process.” :contentReference[oaicite:35]{index=35}
- “Timed out after {timeout_s}s: …” return structure snippet. :contentReference[oaicite:36]{index=36}
```

```ticket T4
T# Title: Implement Taskify Stage-2 detection and validation (01-*.md..20-*.md + single-pack form)
Type: chore
Target Area: oraclepack_mcp_server/taskify.py (Stage-2 detection + validation)
Summary:
- Implement deterministic detection of Stage-2 outputs for agents, supporting both directory-form outputs (01-*.md..20-*.md) and a single-pack input file form.
- Provide validation that ensures exactly one match per prefix 01..20 and returns missing/ambiguous details.
In Scope:
- `validate_stage2_dir(out_dir)`:
  - For each prefix 01..20, glob `{pfx}-*.md`
  - Return missing patterns and ambiguous prefix matches.
- `detect_stage2(stage2_path, repo_root)`:
  - Support explicit dir path.
  - Support explicit file path with out_dir rules:
    - If under `docs/oracle-questions-YYYY-MM-DD/…`, use sibling `oracle-out` under that docs subtree; else default `repo_root/oracle-out`.
  - Support “auto” discovery (best-effort, deterministic ordering), including checking `repo_root/oracle-out`.
Out of Scope:
- Stage-3 action pack validation (handled in T7).
Current Behavior (Actual):
- Not provided.
Expected Behavior:
- Agents can resolve and validate Stage-2 outputs deterministically for downstream Stage-3 workflows.
Reproduction Steps:
- Not provided.
Requirements / Constraints:
- “Detect Stage-2 outputs (dir-form 01-*.md..20-*.md OR single-pack form)”.
- “Validate Stage-2 outputs (exactly one match per prefix 01..20)”.
Evidence:
- Requirements text for Stage-2 detection/validation. :contentReference[oaicite:37]{index=37}
- Validation logic snippet for 01..20 and ambiguous/missing. :contentReference[oaicite:38]{index=38}
- Out-dir rule snippet referencing `docs/oracle-questions-YYYY-MM-DD/…` → `oracle-out`. :contentReference[oaicite:39]{index=39}
Open Items / Unknowns:
- Full “auto discovery” search order beyond checking `repo_root/oracle-out`: Not provided in visible excerpts.
Risks / Dependencies:
- Not provided.
Acceptance Criteria:
- [ ] Validation returns ok=false with `missing` when any prefix has no matches.
- [ ] Validation returns ok=false with `ambiguous` when any prefix has >1 match.
- [ ] Validation returns ok=true only when exactly one match exists for every prefix 01..20.
- [ ] Detection supports explicit dir and explicit file resolution and produces an `out_dir`.
- [ ] Detection supports “auto” and returns deterministic results for the same filesystem state.
Priority & Severity (if inferable from input text):
- Priority: Not provided
- Severity: Not provided
Source:
- “Detect Stage-2 outputs (dir-form 01-*.md..20-*.md OR single-pack form) … Validate … exactly one match per prefix 01..20.” :contentReference[oaicite:40]{index=40}
- “out_dir rules … if under docs/oracle-questions-YYYY-MM-DD/ … then …/oracle-out else oracle-out” :contentReference[oaicite:41]{index=41}
```

```ticket T5
T# Title: Implement MCP tools for oraclepack and taskify helper operations
Type: enhancement
Target Area: oraclepack_mcp_server/server.py (MCP tool registration + schemas + formatting)
Summary:
- Implement the MCP server surface that maps `oraclepack` CLI operations and Taskify helper functions into callable MCP tools.
- Provide consistent response formatting (markdown/json) and ensure run tools respect execution gating.
In Scope:
- Tool schemas/inputs covering:
  - `oraclepack_validate_pack`
  - `oraclepack_list_steps`
  - `oraclepack_run_pack` (gated)
  - `oraclepack_read_file`
  - `oraclepack_taskify_detect_stage2`
  - `oraclepack_taskify_validate_stage2`
  - `oraclepack_taskify_validate_action_pack`
  - `oraclepack_taskify_artifacts_summary`
  - `oraclepack_taskify_run_action_pack` (gated)
- Response formatting:
  - Support JSON and Markdown result formats (including stdout/stderr blocks and truncation notes).
[TRUNCATED]
```

.tickets/mcp/MCP Server for Oraclepack.md
```
Title:

* Add MCP server that exposes `oraclepack` + Taskify Stage-2/Stage-3 helpers as tools for agents

Summary:

* Agents need real-time access to `oraclepack` capabilities via MCP so they can validate, inspect, and run packs, then consume Taskify artifacts produced by the `oracle_pack_and_taskify-skills.md` workflow.
* Implement a secure-by-default Python MCP server that wraps the `oraclepack` CLI and adds deterministic helpers for Stage-2 detection/validation and Stage-3 Action Pack validation/execution + artifact summarization.

Background / Context:

* Request: “give access to agents/assistants the following oraclepack tool as a MCP tool… so they can perform actions using the artifacts generated from `oracle_pack_and_taskify-skills.md` … in real time.”
* Proposed reference implementation (from the conversation) is a Python project named `oraclepack-mcp-server` using FastMCP (MCP Python SDK), supporting `stdio` and `streamable-http` transports.
* Stage-2 outputs are expected to be 20 markdown files matching `01-*.md` … `20-*.md`; Stage-2 “single-pack form” needs out-dir resolution rules when the pack lives under `docs/oracle-questions-YYYY-MM-DD/...`.

Current Behavior (Actual):

* No MCP tool surface is available for `oraclepack` and Taskify workflows (agents cannot call validated tools to run/inspect packs and artifacts). (per user)

Expected Behavior:

* Agents can use MCP tools to:

  * Validate and inspect packs.
  * Run packs non-interactively to generate artifacts.
  * Detect/validate Stage-2 outputs and validate Stage-3 Action Packs before execution.
  * Summarize Stage-3 artifacts for immediate downstream consumption.

Requirements:

* MCP server implementation

  * Provide a Python MCP server project structure (e.g., `oraclepack-mcp-server/` with `oraclepack_mcp_server/` package).
  * Support transports:

    * `stdio`
    * `streamable-http`
* Tool surface (MCP tools)

  * `oraclepack_validate_pack`
  * `oraclepack_list_steps`
  * `oraclepack_run_pack` (execution gated)
  * `oraclepack_read_file`
  * `oraclepack_taskify_detect_stage2`
  * `oraclepack_taskify_validate_stage2`
  * `oraclepack_taskify_validate_action_pack`
  * `oraclepack_taskify_artifacts_summary`
  * `oraclepack_taskify_run_action_pack` (execution gated)
* Execution + safety controls

  * Deny-by-default execution; require `ORACLEPACK_ENABLE_EXEC=1` to enable “run” tools.
  * Restrict filesystem reads to allowlisted roots via `ORACLEPACK_ALLOWED_ROOTS` (colon-separated); block paths outside allowed roots.
  * Enforce timeouts and truncate stdout/stderr (`ORACLEPACK_CHARACTER_LIMIT`) and cap file read sizes (`ORACLEPACK_MAX_READ_BYTES`).
* Stage-2 reliability helpers

  * Validate Stage-2 directory contains exactly one match per prefix `01`..`20` (missing/ambiguous detection).
  * Deterministic Stage-2 detection:

    * Accept explicit dir or file.
    * If file is under `docs/oracle-questions-YYYY-MM-DD/...`, set out-dir to `docs/oracle-questions-YYYY-MM-DD/oracle-out`; otherwise default `repo_root/oracle-out`.
* Stage-3 reliability helpers

  * Validate Action Pack structure constraints before executing (e.g., “single ```bash fence, step headers”).
  * Summarize produced artifacts (examples cited: `_actions.json`, PRD path, Task Master outputs).
* Agent UX metadata

  * Apply MCP tool annotations:

    * validate/list/read: `readOnlyHint: true`
    * run: `destructiveHint: true`, `openWorldHint: true`

Out of Scope:

* Not provided.

Reproduction Steps:

* Not provided.

Environment:

* Language/runtime: Python (MCP server), wraps external `oraclepack` CLI.
* OS: Unknown
* Deployment: Unknown (local stdio vs HTTP service)
* MCP SDK version: Unknown (example uses `mcp>=1.0.0`, `pydantic>=2.0.0`).

Evidence:

* Conversation transcript + proposed reference implementation: `/mnt/data/MCP tools for Oraclepack.md`.
* Proposed env vars and tool list (deny-by-default exec, allowed roots, transports).
* Stage-2 directory validation (`01-*.md..20-*.md`) and Stage-2 out-dir resolution logic for `docs/oracle-questions-YYYY-MM-DD`.

Decisions / Agreements:

* Use a Python MCP server (FastMCP / MCP Python SDK) to expose `oraclepack` CLI + Taskify helpers.
* Support both `stdio` and `streamable-http` transports.
* Default-secure posture: execution gated by `ORACLEPACK_ENABLE_EXEC`, filesystem access constrained by allowlisted roots, truncation + timeout enforced.

Open Items / Unknowns:

* Where this MCP server should live (same repo as `oraclepack` vs separate repo) is not provided.
* Authentication / origin validation requirements for `streamable-http` deployment are mentioned conceptually but concrete requirements are not provided.
* Exact definition of “artifacts summary” contents/format beyond examples (`_actions.json`, PRD, Task Master outputs) is not provided.
* Whether `oraclepack_run_pack` must always use `--no-tui --yes --run-all` vs configurable flags is not provided (example suggests non-interactive flags).

Risks / Dependencies:

* Dependency on external `oraclepack` binary path/config (`ORACLEPACK_BIN`) and correct working directory (`ORACLEPACK_WORKDIR`).
* Security risk if exec is enabled without strict path/root controls and timeouts; mitigations are required as above.
* Stage-2 ambiguity risk when multiple `NN-*.md` match the same prefix; must report ambiguity deterministically and fail validation.

Acceptance Criteria:

* [ ] MCP server starts successfully in both `stdio` and `streamable-http` modes.
* [ ] All listed tools are exposed with the documented names.
* [ ] When `ORACLEPACK_ENABLE_EXEC!=1`, run tools refuse to execute and return a clear error; validate/list/read tools still work.
* [ ] `oraclepack_read_file` rejects paths outside `ORACLEPACK_ALLOWED_ROOTS`.
* [ ] Stage-2 validation enforces exactly one file per prefix `01`..`20` and returns `missing` and `ambiguous` sets when invalid.
* [ ] Stage-2 detection resolves out-dir correctly for both:

  * explicit dir inputs
  * single-pack file under `docs/oracle-questions-YYYY-MM-DD/...` → `docs/oracle-questions-YYYY-MM-DD/oracle-out`
  * otherwise default `repo_root/oracle-out`.
* [ ] stdout/stderr truncation and timeouts are enforced on CLI subprocess execution.
* [ ] Tool annotations are applied as specified for read-only vs destructive tools.

Priority & Severity (if inferable from text):

* Not provided.

Labels (optional):

* enhancement
* mcp
* oraclepack
* cli-wrapper
* taskify
* security
* tooling
```

.tickets/mcp/gaps-still-not-covered.md
```
## Gaps still not covered in the current oraclepack MCP proposal

### Transport + deployment correctness

* **`--transport streamable-http` is wired to the wrong FastMCP transport.** The proposal claims `transport="sse"` “maps to streamable-http”, but FastMCP supports Streamable HTTP explicitly and documents SSE as being superseded.  ([GitHub][1])
* **No production-grade HTTP hardening (auth, TLS posture, DNS-rebinding mitigations).** MCP security guidance explicitly warns about local HTTP servers (SSE/Streamable HTTP) without auth and recommends stdio or authenticated IPC/HTTP with mitigations. ([Model Context Protocol][2])
* **Dependency is unpinned despite a high-severity DNS rebinding advisory in the Python SDK.** The proposal uses `mcp[cli]>=0.1.0` (no minimum safe version). The advisory indicates affected versions and a patched release.  ([GitHub][3])

### Security model gaps (filesystem + execution)

* **Symlink escape is not addressed.** `validate_path()` normalizes with `abspath/normpath` and then `safe_read_file()` opens the path; this pattern typically allows “inside-root symlink → outside-root target” unless you resolve and check the realpath. No test covers symlink traversal.
* **Execution is only gated by a boolean env flag, without least-privilege scoping.** The server exposes “run pack” as open-world/destructive when enabled, but does not add per-tool scoping, allowlists, or authorization flows for HTTP mode.  ([Model Context Protocol][2])

### Parity gaps vs oraclepack TUI/runner workflows

* **No URL/project selection tooling exposed.** The TUI has explicit URL store + picker plumbing (the thing you need for “choose PRD-generator project URL”), but MCP doesn’t expose tools to list/select/manage those URLs.
* **No runtime overrides wizard parity.** The TUI supports an overrides flow (per-step flag add/remove, targeting, validation), but MCP doesn’t expose “get overrides / set overrides / validate overrides / apply and run”.
* **No structured access to run state/report artifacts.** MCP returns raw stdout/stderr strings and truncates them; it doesn’t provide first-class tools/resources for reading the oraclepack state/report outputs as structured objects.

### Execution control + UX gaps for agents

* **No step-level execution controls.** The MCP API offers `oraclepack_run_pack(... run_all=True)` but does not provide “run step N”, “run subset”, “resume”, “re-run failed only”, etc.
* **No streaming logs / cancellation.** `run_oraclepack()` waits for completion, then returns (with truncation). There’s no incremental progress, no cancellation hook, and timeout uses a hard kill only.

### Validation gaps (especially for “ticket-action-pack.md” style artifacts)

* **Action-pack validation in MCP is weaker than your existing validator script.** The MCP `validate_action_pack` logic is comparatively minimal, while the repo’s `validate-action-pack.sh` encodes stricter structural rules; MCP does not currently match those guardrails.

### Capability gaps (authoring/editing)

* **No “write/update pack” capability.** The server can read/list/validate/run packs, but cannot create/edit packs or write back transformed artifacts (which is often needed for “tickets → pack” workflows).

[1]: https://github.com/modelcontextprotocol/python-sdk?utm_source=chatgpt.com "GitHub - modelcontextprotocol/python-sdk: The official Python SDK for Model Context Protocol servers and clients"
[2]: https://modelcontextprotocol.io/specification/2025-11-25/basic/security_best_practices?utm_source=chatgpt.com "Security Best Practices - Model Context Protocol"
[3]: https://github.com/advisories/GHSA-9h52-p55h-vw2f?utm_source=chatgpt.com "Model Context Protocol (MCP) Python SDK does not enable DNS rebinding protection by default · CVE-2025-66416 · GitHub Advisory Database · GitHub"
```

.tickets/mcp/gaps_part2-mcp-builder.md
```
## Still needed to make “oraclepack as an MCP tool” production-ready

### 1) Fix transport + deployment mode choice (local vs remote)

* **Use the correct Streamable HTTP transport string** (`transport="streamable-http"`) and stop treating SSE as the Streamable HTTP equivalent. The Python SDK’s examples use `mcp.run(transport="streamable-http")`. ([GitHub][1])
* If you intend to run this as a **remote** MCP server (for multi-client / “real time” usage), implement the Streamable HTTP security requirements:

  * validate `Origin` header (DNS rebinding protection)
  * bind to `127.0.0.1` when local
  * require authentication ([Model Context Protocol][2])
* If you intend “agents/assistants” to run it **locally**, default to **stdio** and keep Streamable HTTP optional. (The MCP spec defines stdio + Streamable HTTP as the standard transports.) ([Model Context Protocol][2])

### 2) Bring `oraclepack_run_pack` up to parity with the Go CLI flags

Your current MCP tool only exposes `yes` and `run_all`.
But the CLI supports additional run-time controls (at least `--resume`, `--stop-on-fail`, ROI threshold/mode, plus the persistent `--oracle-bin` and `--out-dir`).
To avoid capability gaps (and ad-hoc “extra args” escape hatches), expose these explicitly in the tool schema.

### 3) Make Stage-2 auto-discovery match the **oraclepack-taskify** contract

The Stage 3 skill is strict about:

* deterministic discovery (lexicographic / ISO-date ordering; no mtimes)
* directory-form must contain **exactly one** `01-*.md` … `20-*.md`, else fail fast with explicit errors
  Also, the Action Pack template itself searches locations including `docs/oracle-out` and `docs/oracle-questions-*/…`.
  So the MCP-side “detect stage2” logic should:
* search the same ordered locations
* validate a candidate before returning it (not “first directory that exists”)
* prefer newest by lexicographic rules when multiple date-stamped runs exist

### 4) Tighten Action Pack validation to exactly match the skill’s validator

The skill’s validator requires:

* **exactly one** ```bash fence and **no other** fences
* sequential `# NN)` step headers inside the bash block
  If your Python validator is looser than `validate-action-pack.sh`, you’ll get drift (packs “validate” in MCP but fail in real usage).

### 5) Add “artifact-first” read tools for Stage-3 outputs (so assistants can act in real time)

Stage 3 produces canonical machine/human artifacts like:

* `<out_dir>/_actions.json`, `<out_dir>/_actions.md`
* `.taskmaster/docs/oracle-actions-prd.md`
* `<out_dir>/tm-complexity.json`
  To enable “agents/assistants” to use them immediately, add read-only tools like:
* list latest runs / outputs (by stable ordering)
* read + parse `_actions.json` (return structured JSON, not only text)
* read Task Master outputs (tasks.json location(s) you expect)

### 6) Operational hardening (especially if exec is enabled)

You already gate execution behind an env flag (`ORACLEPACK_ENABLE_EXEC`).
Still needed:

* enforce allowed roots not just for reads, but also for **where packs are allowed to write** (at minimum, validate/normalize `out_dir`)
* timeouts + output truncation + concurrency limits (oraclepack can run arbitrary bash steps)
* clear error taxonomy in tool responses (so clients can recover deterministically)

### 7) Client onboarding configs (so assistants can actually connect)

Depending on the target clients:

* **Codex**: document config via `~/.codex/config.toml` and/or `codex mcp` commands. ([OpenAI Developers][3])
* **Inspector**: document using the inspector to test/debug and export server configs (`mcp.json`). ([GitHub][4])
* **ChatGPT / Responses API remote MCP** (if that’s a goal): document remote server URL + auth patterns. ([OpenAI Platform][5])

### 8) Acceptance tests that lock the contracts

Minimum tests to prevent regressions:

* Stage-2 detection test matrix (oracle-out, docs/oracle-out, docs/oracle-questions-*/…, single-pack form)
* Action Pack validator parity with `validate-action-pack.sh`
* run-pack flag mapping correctness (`--resume`, ROI options, `--out-dir`, etc.)

---

## Minimum set to ship safely

1. Correct Streamable HTTP wiring (`transport="streamable-http"`) + security requirements if remote ([GitHub][1])
2. Expand `oraclepack_run_pack` to match the CLI surface
3. Stage-2 detection + Action Pack validation exactly match the oraclepack-taskify skill scripts
4. Add read tools for `_actions.json` / PRD / Task Master outputs so agents can “use artifacts in real time”

[1]: https://github.com/modelcontextprotocol/python-sdk "GitHub - modelcontextprotocol/python-sdk: The official Python SDK for Model Context Protocol servers and clients"
[2]: https://modelcontextprotocol.io/specification/2025-06-18/basic/transports "Transports - Model Context Protocol"
[3]: https://developers.openai.com/codex/mcp/?utm_source=chatgpt.com "Model Context Protocol"
[4]: https://github.com/modelcontextprotocol/inspector "GitHub - modelcontextprotocol/inspector: Visual testing tool for MCP servers"
[5]: https://platform.openai.com/docs/guides/tools-connectors-mcp?utm_source=chatgpt.com "Connectors and MCP servers | OpenAI API"
```

.tickets/mcp/oraclepack-MCP.md
```
## MCP surface for `oraclepack` (so agents can act on Taskify artifacts)

### What to expose as MCP tools

Map the existing `oraclepack` CLI capabilities (validate/list/run + flags like `--no-tui`, `--out-dir`, `--oracle-bin`) into MCP tools so an agent can run packs non-interactively and then inspect the resulting state/report/artifacts.

Add a small “taskify helper” layer to make the **Stage-2 → Stage-3** workflow deterministic for agents:

* **Detect Stage-2 outputs** (dir-form `01-*.md..20-*.md` OR single-pack form) using the ordered resolver rules described in your skill.
* **Validate Stage-2 outputs** (exactly one match per prefix 01..20).
* **Validate Stage-3 Action Pack** structure constraints (single ```bash fence, step headers `# NN)`, etc.) before executing anything.
* **Summarize Stage-3 artifacts** (e.g., `_actions.json`, `_actions.md`, PRD path, `tm-complexity.json`, pipelines doc) so agents can immediately consume them.

### Transport choices (real-time vs local)

* **stdio** is simplest for local agent runtimes; it requires MCP messages only on stdout (logs must go to stderr). ([Model Context Protocol][1])
* **Streamable HTTP** is better for “real-time” multi-client usage; implement Origin validation and bind to localhost + auth to avoid DNS rebinding and local-network abuse. ([Model Context Protocol][1])

### Tool UX metadata (important for agents)

Use MCP **tool annotations** so clients can correctly present approval UX:

* mark validate/list/read tools as `readOnlyHint: true`
* mark run tools as `destructiveHint: true`, `openWorldHint: true` (they can touch filesystem, run processes, etc.) ([Model Context Protocol][2])

### Security defaults (recommended)

* Hard **deny-by-default execution**: require an env flag (e.g. `ORACLEPACK_ENABLE_EXEC=1`) to enable “run” tools.
* Restrict filesystem access to **allowlisted roots** to avoid path traversal and accidental exfiltration of secrets.
* Enforce stdout/stderr truncation and timeouts so a pack can’t wedge the server process.

## Reference implementation

I provided a complete, drop-in **Python MCP server** (FastMCP from the official MCP Python SDK) that exposes:

* `oraclepack_validate_pack`, `oraclepack_list_steps`, `oraclepack_run_pack`
* `oraclepack_taskify_detect_stage2`, `oraclepack_taskify_validate_stage2`
* `oraclepack_taskify_validate_action_pack`, `oraclepack_taskify_artifacts_summary`
* `oraclepack_taskify_run_action_pack`

It supports **stdio** and **streamable-http** transports per the official SDK. ([GitHub][3])

The full multi-file project is in the canvas titled **“oraclepack-mcp-server”**.

[1]: https://modelcontextprotocol.io/specification/2025-03-26/basic/transports "Transports - Model Context Protocol"
[2]: https://modelcontextprotocol.io/legacy/concepts/tools "Tools - Model Context Protocol"
[3]: https://github.com/modelcontextprotocol/python-sdk "GitHub - modelcontextprotocol/python-sdk: The official Python SDK for Model Context Protocol servers and clients"
```

.tickets/mcp/oraclepack_mcp_server.md
```
# oraclepack-mcp-server (MCP wrapper for oraclepack + taskify helpers)

## tree

```text
oraclepack-mcp-server/
  README.md
  requirements.txt
  oraclepack_mcp_server/
    __init__.py
    __main__.py
    config.py
    security.py
    oraclepack_cli.py
    taskify.py
    server.py
```

---

```md
<!-- path: oraclepack-mcp-server/README.md -->
# oraclepack-mcp-server

Expose `oraclepack` (validate/list/run) plus a few Stage-3 (oraclepack-taskify) helpers as **MCP tools**, so an agent can:
- validate and inspect packs
- run packs non-interactively (`--no-tui --yes --run-all`) to produce artifacts
- validate Stage-2 outputs (01-*.md..20-*.md)
- validate Stage-3 Action Packs (single ```bash fence, step headers, etc.)
- summarize Stage-3 artifacts (`_actions.json`, PRD, Task Master outputs, etc.)

## Install

```bash
python -m venv .venv
source .venv/bin/activate
pip install -r requirements.txt
```

## Configure (recommended)

Environment variables:

- `ORACLEPACK_ALLOWED_ROOTS` (default: current working directory)
  - Colon-separated list of allowed filesystem roots the server may read from.
  - Example: `ORACLEPACK_ALLOWED_ROOTS=/repo:/tmp/oracle-out`
- `ORACLEPACK_BIN` (default: `oraclepack`) – path to the oraclepack CLI
- `ORACLEPACK_WORKDIR` (default: current working directory)
  - Where packs are executed from (important for relative paths).
- `ORACLEPACK_ENABLE_EXEC` (default: `0`)
  - Must be `1` to enable `oraclepack_run_pack` and `oraclepack_taskify_run_action_pack`.
- `ORACLEPACK_CHARACTER_LIMIT` (default: `25000`) – truncate large stdout/stderr
- `ORACLEPACK_MAX_READ_BYTES` (default: `500000`) – max bytes read from a file

## Run (stdio)

```bash
# Stdio transport is the simplest local integration.
python -m oraclepack_mcp_server --transport stdio
```

## Run (Streamable HTTP)

```bash
python -m oraclepack_mcp_server --transport streamable-http
```

## Tools

- `oraclepack_validate_pack`
- `oraclepack_list_steps`
- `oraclepack_run_pack` (requires `ORACLEPACK_ENABLE_EXEC=1`)
- `oraclepack_read_file`
- `oraclepack_taskify_detect_stage2`
- `oraclepack_taskify_validate_stage2`
- `oraclepack_taskify_validate_action_pack`
- `oraclepack_taskify_artifacts_summary`
- `oraclepack_taskify_run_action_pack` (requires `ORACLEPACK_ENABLE_EXEC=1`)

## Typical Stage-3 usage

1) Detect/validate Stage-2 outputs (directory or single-pack)
2) Validate the Action Pack markdown
3) Run the Action Pack via `oraclepack run ...`
4) Summarize produced artifacts
```

```txt
# path: oraclepack-mcp-server/requirements.txt
mcp>=1.0.0
pydantic>=2.0.0
```

```python
# path: oraclepack-mcp-server/oraclepack_mcp_server/__init__.py
__all__ = []
```

```python
# path: oraclepack-mcp-server/oraclepack_mcp_server/config.py
from __future__ import annotations

import os
from dataclasses import dataclass
from pathlib import Path


def _truthy(value: str | None) -> bool:
    if value is None:
        return False
    return value.strip().lower() in {"1", "true", "yes", "y", "on"}


@dataclass(frozen=True)
class ServerConfig:
    allowed_roots: tuple[Path, ...]
    oraclepack_bin: str
    workdir: Path
    enable_exec: bool
    character_limit: int
    max_read_bytes: int


def load_config() -> ServerConfig:
    # Allowed roots: colon-separated. Default to CWD.
    roots_raw = os.environ.get("ORACLEPACK_ALLOWED_ROOTS")
    if roots_raw:
        roots = tuple(Path(p).expanduser().resolve() for p in roots_raw.split(":") if p.strip())
    else:
        roots = (Path.cwd().resolve(),)

    oraclepack_bin = os.environ.get("ORACLEPACK_BIN", "oraclepack")
    workdir = Path(os.environ.get("ORACLEPACK_WORKDIR", str(Path.cwd()))).expanduser().resolve()

    enable_exec = _truthy(os.environ.get("ORACLEPACK_ENABLE_EXEC", "0"))

    character_limit = int(os.environ.get("ORACLEPACK_CHARACTER_LIMIT", "25000"))
    max_read_bytes = int(os.environ.get("ORACLEPACK_MAX_READ_BYTES", "500000"))

    return ServerConfig(
        allowed_roots=roots,
        oraclepack_bin=oraclepack_bin,
        workdir=workdir,
        enable_exec=enable_exec,
        character_limit=character_limit,
        max_read_bytes=max_read_bytes,
    )
```

```python
# path: oraclepack-mcp-server/oraclepack_mcp_server/security.py
from __future__ import annotations

from pathlib import Path


class PathNotAllowedError(ValueError):
    pass


def resolve_under_roots(path: Path, allowed_roots: tuple[Path, ...]) -> Path:
    """Resolve a path and enforce it lives under at least one allowed root."""
    p = path.expanduser().resolve()

    for root in allowed_roots:
        r = root.expanduser().resolve()
        try:
            p.relative_to(r)
            return p
        except ValueError:
            continue

    raise PathNotAllowedError(
        f"Path not allowed (outside allowed roots). path={p} roots={[str(r) for r in allowed_roots]}"
    )


def safe_read_text(path: Path, max_bytes: int) -> tuple[str, bool]:
    """Read up to max_bytes from a file as UTF-8 (replace errors)."""
    data = path.read_bytes()
    truncated = False
    if len(data) > max_bytes:
        data = data[:max_bytes]
        truncated = True
    return data.decode("utf-8", errors="replace"), truncated


def safe_read_bytes(path: Path, max_bytes: int) -> tuple[bytes, bool]:
    data = path.read_bytes()
    truncated = False
    if len(data) > max_bytes:
        data = data[:max_bytes]
        truncated = True
    return data, truncated
```

```python
# path: oraclepack-mcp-server/oraclepack_mcp_server/oraclepack_cli.py
from __future__ import annotations

import asyncio
import os
import time
from dataclasses import dataclass
from pathlib import Path


@dataclass
class CmdResult:
    ok: bool
    exit_code: int
    duration_s: float
    stdout: str
    stderr: str
    stdout_truncated: bool
    stderr_truncated: bool


def _truncate(s: str, limit: int) -> tuple[str, bool]:
    if limit <= 0:
        return s, False
    if len(s) <= limit:
        return s, False
    return s[:limit], True


async def run_cmd(
    argv: list[str],
    cwd: Path,
    timeout_s: int,
    env: dict[str, str] | None,
    character_limit: int,
) -> CmdResult:
    start = time.time()

    proc = await asyncio.create_subprocess_exec(
        *argv,
        cwd=str(cwd),
        env=(os.environ | (env or {})),
        stdout=asyncio.subprocess.PIPE,
        stderr=asyncio.subprocess.PIPE,
    )

    try:
        out_b, err_b = await asyncio.wait_for(proc.communicate(), timeout=timeout_s)
    except asyncio.TimeoutError:
        proc.kill()
        await proc.communicate()
        duration = time.time() - start
        return CmdResult(
            ok=False,
            exit_code=124,
            duration_s=duration,
            stdout="",
            stderr=f"Timed out after {timeout_s}s: {' '.join(argv)}",
            stdout_truncated=False,
            stderr_truncated=False,
        )

    duration = time.time() - start
    out = out_b.decode("utf-8", errors="replace") if out_b else ""
    err = err_b.decode("utf-8", errors="replace") if err_b else ""

    out, out_tr = _truncate(out, character_limit)
    err, err_tr = _truncate(err, character_limit)

    exit_code = proc.returncode if proc.returncode is not None else 1
    return CmdResult(
        ok=(exit_code == 0),
        exit_code=exit_code,
        duration_s=duration,
        stdout=out,
        stderr=err,
        stdout_truncated=out_tr,
        stderr_truncated=err_tr,
    )
```

```python
# path: oraclepack-mcp-server/oraclepack_mcp_server/taskify.py
from __future__ import annotations

import re
from dataclasses import dataclass
from datetime import date
from pathlib import Path


@dataclass
class Stage2Resolution:
    kind: str  # "dir" | "file"
    stage2_path: Path
    out_dir: Path
    notes: list[str]


PREFIXES = [f"{i:02d}" for i in range(1, 21)]


def _is_iso_date(s: str) -> bool:
    # Minimal heuristic for YYYY-MM-DD
    return bool(re.fullmatch(r"\d{4}-\d{2}-\d{2}", s))


def validate_stage2_dir(out_dir: Path) -> dict:
    missing: list[str] = []
    ambiguous: dict[str, list[str]] = {}
    selected: dict[str, str] = {}

    for pfx in PREFIXES:
        matches = sorted(out_dir.glob(f"{pfx}-*.md"))
        if len(matches) == 0:
            missing.append(f"{pfx}-*.md")
        elif len(matches) > 1:
            ambiguous[pfx] = [m.name for m in matches]
        else:
            selected[pfx] = matches[0].name

    ok = (not missing) and (not ambiguous)
    return {
        "ok": ok,
        "out_dir": str(out_dir),
        "selected": selected,
        "missing": missing,
        "ambiguous": ambiguous,
    }


def _lexi_newest(paths: list[Path]) -> Path | None:
    # Deterministic: lexicographic max
    return sorted(paths, key=lambda p: p.name)[-1] if paths else None


def detect_stage2(stage2_path: str, repo_root: Path) -> Stage2Resolution:
    notes: list[str] = []

    if stage2_path != "auto":
        p = (repo_root / stage2_path).expanduser()
        if p.exists() and p.is_dir():
            return Stage2Resolution(kind="dir", stage2_path=p.resolve(), out_dir=p.resolve(), notes=["explicit dir"])
        if p.exists() and p.is_file():
            # out_dir rules from skill: if under docs/oracle-questions-YYYY-MM-DD/ then parent/oracle-out else oracle-out
            p_res = p.resolve()
            out = repo_root / "oracle-out"
            parts = list(p_res.parts)
            if "docs" in parts:
                try:
                    idx = parts.index("docs")
                    # docs/oracle-questions-YYYY-MM-DD/.../oracle-pack-YYYY-MM-DD.md
                    if idx + 1 < len(parts) and parts[idx + 1].startswith("oracle-questions-"):
                        out = Path(*parts[: idx + 2]) / "oracle-out"
                except ValueError:
                    pass
            return Stage2Resolution(kind="file", stage2_path=p_res, out_dir=out.resolve(), notes=["explicit file"])
        raise FileNotFoundError(f"stage2_path not found: {p}")

    # auto discovery (best-effort, deterministic ordering)
    searched: list[str] = []

    # 1) repo_root/oracle-out
    candidate = repo_root / "oracle-out"
    searched.append(str(candidate))
    if candidate.is_dir():
        v = validate_stage2_dir(candidate)
        if v["ok"]:
            notes.append("auto: selected repo_root/oracle-out")
            return Stage2Resolution(kind="dir", stage2_path=candidate.resolve(), out_dir=candidate.resolve(), notes=notes)

    # 2) docs/oracle-questions-YYYY-MM-DD/oracle-out (newest by lexicographic date suffix)
    docs = repo_root / "docs"
    if docs.is_dir():
        qdirs = [p for p in docs.glob("oracle-questions-*") if p.is_dir()]
        # deterministic: sort by name and take newest
        newest_q = _lexi_newest(qdirs)
        if newest_q:
            candidate = newest_q / "oracle-out"
            searched.append(str(candidate))
            if candidate.is_dir():
                v = validate_stage2_dir(candidate)
                if v["ok"]:
                    notes.append(f"auto: selected {candidate}")
                    return Stage2Resolution(kind="dir", stage2_path=candidate.resolve(), out_dir=candidate.resolve(), notes=notes)

    # 3) single-pack form (newer): look for docs/oracle-pack-*.md or docs/oraclepacks/oracle-pack-*.md
    file_candidates: list[Path] = []
    if docs.is_dir():
        file_candidates += list(docs.glob("oracle-pack-*.md"))
        file_candidates += list((docs / "oraclepacks").glob("oracle-pack-*.md"))
        file_candidates += list(docs.glob("oracle-questions-*/oraclepacks/oracle-pack-*.md"))

    newest_file = _lexi_newest(sorted([p for p in file_candidates if p.is_file()], key=lambda p: p.name))
    if newest_file:
        notes.append(f"auto: selected single-pack {newest_file}")
        out = repo_root / "oracle-out"
        # If under docs/oracle-questions-YYYY-MM-DD/..., default out_dir there.
        if "docs" in newest_file.parts:
            try:
                idx = newest_file.parts.index("docs")
                if idx + 1 < len(newest_file.parts) and newest_file.parts[idx + 1].startswith("oracle-questions-"):
                    out = Path(*newest_file.parts[: idx + 2]) / "oracle-out"
            except ValueError:
                pass
        return Stage2Resolution(kind="file", stage2_path=newest_file.resolve(), out_dir=out.resolve(), notes=notes)

    raise FileNotFoundError(
        "stage2_path=auto could not resolve Stage-2 outputs. Searched:\n- " + "\n- ".join(searched)
    )


def validate_action_pack(pack_path: Path) -> dict:
    text = pack_path.read_text(encoding="utf-8", errors="replace")

    bash_fence = re.findall(r"(?m)^\s*```bash\s*$", text)
    any_fence = re.findall(r"(?m)^\s*```\s*", text)

    errors: list[str] = []
    if len(bash_fence) != 1:
        errors.append(f"expected exactly one ```bash fence; found {len(bash_fence)}")
    if len(any_fence) != 2:
        # One opening and one closing fence expected, and it must be a bash fence.
        errors.append(f"expected no other code fences; found {len(any_fence)} total fences")

    # Extract bash block content if possible
    bash_block = ""
    m = re.search(r"```bash\s*\n(?P<body>[\s\S]*?)\n```\s*", text)
    if m:
        bash_block = m.group("body")

    # Validate step headers inside bash fence
[TRUNCATED]
```

</source_code>

## Links discovered
- [OpenAI Developers+2OpenAI Developers+2](https://developers.openai.com/codex/custom-prompts/?utm_source=chatgpt.com)
- [Model Context Protocol+3Model Context Protocol+3Model Context Protocol+3](https://modelcontextprotocol.io/specification/2025-11-25?utm_source=chatgpt.com)
- [Model Context Protocol+1](https://modelcontextprotocol.io/specification/2025-06-18/server/tools?utm_source=chatgpt.com)
- [Model Context Protocol+1](https://modelcontextprotocol.io/docs/develop/build-server?utm_source=chatgpt.com)

--- codefetch/oraclepack-tui.md ---
<filetree>
Project Structure:
└── internal
    ├── app
    │   ├── app.go
    │   ├── app_test.go
    │   ├── run.go
    │   └── run_test.go
    ├── artifacts
    │   ├── contract.go
    │   └── contract_test.go
    ├── cli
    │   ├── cmds.go
    │   ├── root.go
    │   ├── run.go
    │   └── verify_outputs.go
    ├── config
    │   ├── defaults.go
    │   └── resolve.go
    ├── dispatch
    │   ├── classify.go
    │   └── classify_test.go
    ├── errors
    │   ├── errors.go
    │   └── errors_test.go
    ├── exec
    │   ├── flags.go
    │   ├── inject.go
    │   ├── inject_test.go
    │   ├── oracle_scan.go
    │   ├── oracle_scan_test.go
    │   ├── oracle_validate.go
    │   ├── oracle_validate_test.go
    │   ├── runner.go
    │   ├── runner_test.go
    │   ├── sanitize.go
    │   ├── sanitize_test.go
    │   └── stream.go
    ├── foundation
    │   ├── atomic.go
    │   ├── atomic_test.go
    │   ├── clock.go
    │   ├── clock_test.go
    │   ├── config.go
    │   ├── config_test.go
    │   ├── errors.go
    │   └── errors_test.go
    ├── overrides
    │   ├── merge.go
    │   ├── merge_test.go
    │   └── types.go
    ├── pack
    │   ├── bash_syntax_validator.go
    │   ├── bash_syntax_validator_test.go
    │   ├── bash_tooling_checks.go
    │   ├── metadata.go
    │   ├── output_expectations.go
    │   ├── output_expectations_test.go
    │   ├── output_validator.go
    │   ├── output_validator_test.go
    │   ├── parser.go
    │   ├── parser_test.go
    │   └── verify_report.go
    ├── render
    │   ├── render.go
    │   └── render_test.go
    ├── report
    │   ├── generate.go
    │   ├── io.go
    │   ├── io_test.go
    │   ├── report_test.go
    │   └── types.go
    ├── shell
    │   ├── detect.go
    │   ├── detect_test.go
    │   ├── engine.go
    │   ├── engine_test.go
    │   ├── runner.go
    │   └── runner_test.go
    ├── state
    │   ├── io.go
    │   ├── io_test.go
    │   ├── persist.go
    │   ├── state_test.go
    │   └── types.go
    ├── templates
    │   ├── template_test.go
    │   ├── ticket-action-pack.md
    │   └── ticket_action_pack.go
    ├── tools
    │   ├── types.go
    │   └── types_test.go
    ├── tui
    │   ├── clipboard.go
    │   ├── filter_test.go
    │   ├── overrides_confirm.go
    │   ├── overrides_flags.go
    │   ├── overrides_flow.go
    │   ├── overrides_steps.go
    │   ├── overrides_url.go
    │   ├── preview_test.go
    │   ├── tui.go
    │   ├── tui_test.go
    │   ├── url_picker.go
    │   ├── url_store.go
    │   └── url_store_test.go
    ├── types
    │   ├── pack.go
    │   ├── pack_test.go
    │   └── verification.go
    └── validate
        ├── artifact_gate.go
        ├── artifact_gate_test.go
        ├── composite.go
        ├── composite_test.go
        ├── oracle.go
        ├── presence.go
        ├── presence_test.go
        ├── report.go
        └── types.go

</filetree>

<source_code>
internal/app/app.go
```
package app

import (
	"fmt"
	"os"

	"github.com/user/oraclepack/internal/exec"
	"github.com/user/oraclepack/internal/pack"
	"github.com/user/oraclepack/internal/state"
	"github.com/user/oraclepack/internal/types"
)

// Config holds application-wide configuration.
type Config struct {
	PackPath              string
	StatePath             string
	ReportPath            string
	StopOnFail            bool
	Resume                bool
	Verbose               bool
	DryRun                bool
	OracleFlags           []string
	WorkDir               string
	OutDir                string // CLI override for output directory
	ROIThreshold          float64
	ROIMode               string // "over" or "under"
	OutputVerify          bool
	OutputRetries         int
	OutputRequireHeadings bool
	OutputChunkMode       string
}

// App orchestrates the execution flow.
type App struct {
	Config Config
	Pack   *types.Pack
	State  *state.RunState
	Runner *exec.Runner
}

// New creates a new application instance.
func New(cfg Config) *App {
	return &App{
		Config: cfg,
		Runner: exec.NewRunner(exec.RunnerOptions{
			WorkDir:     cfg.WorkDir,
			OracleFlags: cfg.OracleFlags,
		}),
	}
}

// LoadPack loads and validates the pack.
func (a *App) LoadPack() error {
	data, err := os.ReadFile(a.Config.PackPath)
	if err != nil {
		return err
	}

	p, err := pack.Parse(data)
	if err != nil {
		return err
	}

	if err := pack.Validate(p); err != nil {
		return err
	}

	a.Pack = p
	a.Pack.Source = a.Config.PackPath
	return nil
}

// LoadState loads or initializes the state.
func (a *App) LoadState() error {
	if a.Config.Resume {
		s, err := state.LoadState(a.Config.StatePath)
		if err == nil {
			a.State = s
			return nil
		}
	}

	a.State = &state.RunState{
		SchemaVersion: 1,
		StepStatuses:  make(map[string]state.StepStatus),
	}
	return nil
}

// Prepare resolves configuration and prepares the runtime environment.
func (a *App) Prepare() error {
	if a.Pack == nil {
		if err := a.LoadPack(); err != nil {
			return err
		}
	}

	// Resolve Output Directory
	// Precedence: CLI > Pack > Default (.)
	outDir := a.Config.OutDir
	if outDir == "" && a.Pack.OutDir != "" {
		outDir = a.Pack.OutDir
	}
	if outDir == "" {
		outDir = "."
	}

	// Provision Directory
	if err := os.MkdirAll(outDir, 0755); err != nil {
		return fmt.Errorf("failed to create output directory %s: %w", outDir, err)
	}

	// Update Runner
	// We do NOT set WorkDir to outDir, so execution happens in the project root.
	// This preserves relative path resolution for -f flags.
	// a.Runner.WorkDir = outDir

	// Add out_dir to Env so scripts can reference it
	a.Runner.Env = append(a.Runner.Env, fmt.Sprintf("out_dir=%s", outDir))

	return nil
}
```

internal/app/app_test.go
```
package app

import (
	"bytes"
	"context"
	"fmt"
	"os"
	"testing"
)

func TestApp_RunPlain(t *testing.T) {
	steps := buildSteps(20, "echo")
	packContent := `
# Test Pack
` + "```" + `bash
` + steps + `
` + "```" + `
`
	packFile := "test.md"
	stateFile := "test_state.json"
	reportFile := "test_report.json"
	defer os.Remove(packFile)
	defer os.Remove(stateFile)
	defer os.Remove(reportFile)

	os.WriteFile(packFile, []byte(packContent), 0644)

	cfg := Config{
		PackPath:   packFile,
		StatePath:  stateFile,
		ReportPath: reportFile,
	}

	a := New(cfg)
	if err := a.Prepare(); err != nil {
		t.Fatalf("Prepare failed: %v", err)
	}
	if err := a.LoadState(); err != nil {
		t.Fatalf("LoadState failed: %v", err)
	}

	var out bytes.Buffer
	err := a.RunPlain(context.Background(), &out)
	if err != nil {
		t.Fatalf("RunPlain failed: %v", err)
	}

	output := out.String()
	if !contains(output, "step 1") || !contains(output, "step 2") {
		t.Errorf("output missing steps: %s", output)
	}

	if _, err := os.Stat(stateFile); os.IsNotExist(err) {
		t.Error("state file was not created")
	}

	if _, err := os.Stat(reportFile); os.IsNotExist(err) {
		t.Error("report file was not created")
	}
}

func contains(s, substr string) bool {
	return len(s) >= len(substr) && (s == substr || (len(substr) > 0 && (s[:len(substr)] == substr || contains(s[1:], substr))))
}

func buildSteps(count int, cmd string) string {
	var b bytes.Buffer
	for i := 1; i <= count; i++ {
		if i < 10 {
			b.WriteString("# 0")
		} else {
			b.WriteString("# ")
		}
		b.WriteString(fmt.Sprintf("%d)\n", i))
		b.WriteString(cmd)
		b.WriteString(fmt.Sprintf(" \"step %d\"\n", i))
	}
	return b.String()
}
```

internal/app/run.go
```
package app

import (
	"context"
	"fmt"
	"io"
	"path/filepath"
	"strings"
	"time"

	"github.com/user/oraclepack/internal/pack"
	"github.com/user/oraclepack/internal/report"
	"github.com/user/oraclepack/internal/state"
	"github.com/user/oraclepack/internal/types"
)

func (a *App) RunPlain(ctx context.Context, out io.Writer) error {
	// Assumes a.Prepare() and a.LoadState() have been called by the CLI entrypoint.
	if a.Pack == nil {
		return fmt.Errorf("pack not loaded")
	}
	if a.State == nil {
		return fmt.Errorf("state not loaded")
	}

	if a.State.StartTime.IsZero() {
		a.State.StartTime = time.Now()
	}

	fmt.Fprintf(out, "Running pack: %s\n", a.Config.PackPath)
	fmt.Fprintf(out, "Output directory: %s\n", a.Runner.WorkDir)

	// Prelude
	if a.Pack.Prelude.Code != "" {
		fmt.Fprintln(out, "Executing prelude...")
		err := a.Runner.RunPrelude(ctx, &a.Pack.Prelude, out)
		a.recordWarnings()
		if err != nil {
			return fmt.Errorf("prelude failed: %w", err)
		}
	}

	for _, step := range a.Pack.Steps {
		a.State.CurrentStep = step.Number
		// Filter by ROI
		if a.Config.ROIThreshold > 0 {
			if a.Config.ROIMode == "under" {
				// "under" is strictly less than
				if step.ROI >= a.Config.ROIThreshold {
					fmt.Fprintf(out, "Skipping step %s (ROI %.2f >= %.2f)\n", step.ID, step.ROI, a.Config.ROIThreshold)
					continue
				}
			} else {
				// "over" is greater than or equal to (3.3 or higher)
				if step.ROI < a.Config.ROIThreshold {
					fmt.Fprintf(out, "Skipping step %s (ROI %.2f < %.2f)\n", step.ID, step.ROI, a.Config.ROIThreshold)
					continue
				}
			}
		}

		// Check resume
		if s, ok := a.State.StepStatuses[step.ID]; ok && s.Status == state.StatusSuccess {
			fmt.Fprintf(out, "Skipping step %s (already succeeded)\n", step.ID)
			continue
		}

		fmt.Fprintf(out, "\n>>> Step %s: %s\n", step.ID, step.OriginalLine)

		status := state.StepStatus{
			Status:    state.StatusRunning,
			StartedAt: time.Now(),
		}
		a.State.StepStatuses[step.ID] = status
		a.saveState()

		// Execute
		err := a.runStepWithOutputVerification(ctx, &step, out)
		a.recordWarnings()

		status.EndedAt = time.Now()
		if err != nil {
			status.Status = state.StatusFailed
			status.Error = err.Error()
			a.State.StepStatuses[step.ID] = status
			a.saveState()

			if a.Config.StopOnFail {
				a.finalize(out)
				return err
			}
			continue
		}

		status.Status = state.StatusSuccess
		status.ExitCode = 0
		a.State.StepStatuses[step.ID] = status
		a.saveState()
	}

	a.finalize(out)
	return nil
}

func (a *App) runStepWithOutputVerification(ctx context.Context, step *types.Step, out io.Writer) error {
	retries := a.Config.OutputRetries
	if retries < 0 {
		retries = 0
	}
	for attempt := 0; attempt <= retries; attempt++ {
		err := a.Runner.RunStep(ctx, step, out)
		if err != nil {
			return err
		}
		if !a.Config.OutputVerify {
			return nil
		}
		outputFailures := pack.VerifyStepOutputs(step, a.Config.OutputRequireHeadings, a.Config.OutputChunkMode)
		if len(outputFailures) == 0 {
			return nil
		}
		var failures []string
		for _, failure := range outputFailures {
			if failure.Error != "" {
				failures = append(failures, fmt.Sprintf("%s error: %s", failure.Path, failure.Error))
				continue
			}
			if len(failure.MissingTokens) > 0 {
				failures = append(failures, fmt.Sprintf("%s missing: %s", failure.Path, strings.Join(failure.MissingTokens, ", ")))
			}
		}
		if len(failures) == 0 {
[TRUNCATED]
```

internal/app/run_test.go
```
package app

import (
	"bytes"
	"context"
	"os"
	"strconv"
	"strings"
	"testing"
)

func TestApp_RunPlain_ROI(t *testing.T) {
	steps := buildROISteps()
	packContent := `
# ROI Test Pack
` + "```" + `bash
` + steps + `
` + "```" + `
`
	packFile := "roi_test.md"
	defer os.Remove(packFile)
	os.WriteFile(packFile, []byte(packContent), 0644)

	// Test Case 1: Filter OVER 3.3 (Should run 5.0 and 3.3)
	t.Run("Filter Over 3.3", func(t *testing.T) {
		var out bytes.Buffer
		cfg := Config{
			PackPath:     packFile,
			ROIThreshold: 3.3,
			ROIMode:      "over",
		}
		app := New(cfg)
		if err := app.Prepare(); err != nil {
			t.Fatalf("Prepare failed: %v", err)
		}
		if err := app.LoadState(); err != nil {
			t.Fatalf("LoadState failed: %v", err)
		}
		if err := app.RunPlain(context.Background(), &out); err != nil {
			t.Fatalf("RunPlain failed: %v", err)
		}
		output := out.String()
		if !strings.Contains(output, "Step 01") {
			t.Error("expected Step 01 (5.0) to run")
		}
		if !strings.Contains(output, "Step 02") {
			t.Error("expected Step 02 (3.3) to run (inclusive)")
		}
		if strings.Contains(output, "Step 03") && !strings.Contains(output, "Skipping step 03") {
			t.Error("expected Step 03 (1.0) to be skipped")
		}
	})

	// Test Case 2: Filter UNDER 3.3 (Should run 1.0 only)
	t.Run("Filter Under 3.3", func(t *testing.T) {
		var out bytes.Buffer
		cfg := Config{
			PackPath:     packFile,
			ROIThreshold: 3.3,
			ROIMode:      "under",
		}
		app := New(cfg)
		if err := app.Prepare(); err != nil {
			t.Fatalf("Prepare failed: %v", err)
		}
		if err := app.LoadState(); err != nil {
			t.Fatalf("LoadState failed: %v", err)
		}
		if err := app.RunPlain(context.Background(), &out); err != nil {
			t.Fatalf("RunPlain failed: %v", err)
		}
		output := out.String()
		if strings.Contains(output, "Step 01") && !strings.Contains(output, "Skipping step 01") {
			t.Error("expected Step 01 (5.0) to be skipped")
		}
		if strings.Contains(output, "Step 02") && !strings.Contains(output, "Skipping step 02") {
			t.Error("expected Step 02 (3.3) to be skipped (exclusive)")
		}
		if !strings.Contains(output, "Step 03") {
			t.Error("expected Step 03 (1.0) to run")
		}
	})
}

func buildROISteps() string {
	var b strings.Builder
	for i := 1; i <= 20; i++ {
		id := i
		if id < 10 {
			b.WriteString("# 0")
		} else {
			b.WriteString("# ")
		}
		b.WriteString(strconv.Itoa(id))
		if i == 1 {
			b.WriteString(") ROI=5.0\n")
			b.WriteString("echo \"high\"\n\n")
			continue
		}
		if i == 2 {
			b.WriteString(") ROI=3.3\n")
			b.WriteString("echo \"threshold\"\n\n")
			continue
		}
		if i == 3 {
			b.WriteString(") ROI=1.0\n")
			b.WriteString("echo \"low\"\n\n")
			continue
		}
		b.WriteString(")\n")
		b.WriteString("echo \"step ")
		b.WriteString(strconv.Itoa(id))
		b.WriteString("\"\n\n")
	}
	return b.String()
}
```

internal/artifacts/contract.go
```
package artifacts

import (
	"fmt"
	"os"
	"path/filepath"

	"github.com/user/oraclepack/internal/foundation"
)

// Contract maps step IDs to required artifact paths.
type Contract map[string][]string

// DefaultContract returns the standard artifact contract.
func DefaultContract() Contract {
	base := ".oraclepack/ticketify"
	return Contract{
		"09": {filepath.Join(base, "next.json")},
		"10": {filepath.Join(base, "codex-implement.md")},
		"11": {filepath.Join(base, "codex-verify.md")},
		"12": {filepath.Join(base, "PR.md")},
	}
}

// EvaluateGates checks required artifacts for a given step.
func EvaluateGates(stepID string, contract Contract) error {
	paths, ok := contract[stepID]
	if !ok || len(paths) == 0 {
		return nil
	}
	var missing []string
	for _, p := range paths {
		info, err := os.Stat(p)
		if err != nil || info.IsDir() || info.Size() == 0 {
			missing = append(missing, p)
		}
	}
	if len(missing) > 0 {
		return fmt.Errorf("%w: %v", foundation.ErrArtifactMissing, missing)
	}
	return nil
}
```

internal/artifacts/contract_test.go
```
package artifacts

import (
	"os"
	"path/filepath"
	"testing"
)

func TestEvaluateGates(t *testing.T) {
	dir := t.TempDir()
	base := filepath.Join(dir, ".oraclepack", "ticketify")
	if err := os.MkdirAll(base, 0755); err != nil {
		t.Fatalf("mkdir: %v", err)
	}
	contract := Contract{
		"09": {filepath.Join(base, "next.json")},
	}

	// Missing file should error.
	if err := EvaluateGates("09", contract); err == nil {
		t.Fatal("expected missing artifact error")
	}

	// Create file and verify pass.
	path := filepath.Join(base, "next.json")
	if err := os.WriteFile(path, []byte("ok"), 0644); err != nil {
		t.Fatalf("write: %v", err)
	}
	if err := EvaluateGates("09", contract); err != nil {
		t.Fatalf("expected no error, got %v", err)
	}
}
```

internal/cli/cmds.go
```
package cli

import (
	"fmt"
	"os"

	"github.com/spf13/cobra"
	"github.com/user/oraclepack/internal/app"
	"github.com/user/oraclepack/internal/pack"
	"github.com/user/oraclepack/internal/validate"
)

var validateCmd = &cobra.Command{
	Use:   "validate [pack.md]",
	Short: "Validate an oracle pack",
	Args:  cobra.ExactArgs(1),
	RunE: func(cmd *cobra.Command, args []string) error {
		out := cmd.OutOrStdout()
		data, err := os.ReadFile(args[0])
		if err != nil {
			return err
		}
		p, err := pack.Parse(data)
		if err != nil {
			return err
		}
		if err := pack.Validate(p); err != nil {
			return err
		}
		findings, warning, err := pack.CheckPackScripts(p)
		if err != nil {
			return err
		}
		if warning != "" {
			fmt.Fprintf(out, "Warning: %s\n", warning)
		}
		if len(findings) > 0 {
			for _, finding := range findings {
				if finding.StepID != "" {
					fmt.Fprintf(out, "Step %s line %d: %s\n", finding.StepID, finding.Line, finding.Message)
				} else {
					fmt.Fprintf(out, "Line %d: %s\n", finding.Line, finding.Message)
				}
			}
			return fmt.Errorf("bash syntax validation failed")
		}
		cv := validate.CompositeValidator{}
		results := cv.ValidatePack(p)
		fmt.Fprintf(out, "Validated %d steps\n", len(results))
		for _, r := range results {
			fmt.Fprintf(out, "Step %s [%s] %s", r.StepID, r.ToolKind.Name(), r.Status)
			if r.Error != "" {
				fmt.Fprintf(out, " (%s)", r.Error)
			}
			fmt.Fprintln(out)
		}
		return nil
	},
}

var listCmd = &cobra.Command{
	Use:   "list [pack.md]",
	Short: "List steps in an oracle pack",
	Args:  cobra.ExactArgs(1),
	RunE: func(cmd *cobra.Command, args []string) error {
		cfg := app.Config{PackPath: args[0]}
		a := app.New(cfg)
		if err := a.LoadPack(); err != nil {
			return err
		}
		for _, s := range a.Pack.Steps {
			fmt.Printf("%s: %s\n", s.ID, s.OriginalLine)
		}
		return nil
	},
}

func init() {
	rootCmd.AddCommand(validateCmd)
	rootCmd.AddCommand(listCmd)
}
```

internal/cli/root.go
```
package cli

import (
	"fmt"
	"os"

	"github.com/spf13/cobra"
	"github.com/user/oraclepack/internal/errors"
)

var (
	noTUI     bool
	oracleBin string
	outDir    string
)

var rootCmd = &cobra.Command{
	Use:   "oraclepack",
	Short: "Oracle Pack Runner",
	Long:  `A polished TUI-driven runner for oracle-based interactive bash steps.`,
}

// Execute adds all child commands to the root command and sets flags appropriately.
func Execute() {
	if err := rootCmd.Execute(); err != nil {
		fmt.Fprintln(os.Stderr, err)
		os.Exit(errors.ExitCode(err))
	}
}

func init() {
	rootCmd.PersistentFlags().BoolVar(&noTUI, "no-tui", false, "Disable the TUI and run in plain terminal mode")
	rootCmd.PersistentFlags().StringVar(&oracleBin, "oracle-bin", "oracle", "Path to the oracle binary")
	rootCmd.PersistentFlags().StringVarP(&outDir, "out-dir", "o", "", "Output directory for step execution")
}
```

internal/cli/run.go
```
package cli

import (
	"context"
	"fmt"
	"path/filepath"
	"strings"

	tea "github.com/charmbracelet/bubbletea"
	"github.com/spf13/cobra"
	"github.com/user/oraclepack/internal/app"
	"github.com/user/oraclepack/internal/config"
	"github.com/user/oraclepack/internal/pack"
	"github.com/user/oraclepack/internal/tui"
)

var (
	yes                   bool
	resume                bool
	stopOnFail            bool
	roiThreshold          float64
	roiMode               string
	runAll                bool
	outputVerify          bool
	outputRetries         int
	outputRequireHeadings bool
	outputChunkMode       string
)

var runCmd = &cobra.Command{
	Use:   "run [pack.md]",
	Short: "Run an oracle pack",
	Args:  cobra.ExactArgs(1),
	RunE: func(cmd *cobra.Command, args []string) error {
		packPath := args[0]

		// Setup paths
		base := strings.TrimSuffix(filepath.Base(packPath), filepath.Ext(packPath))
		statePath := base + ".state.json"
		reportPath := base + ".report.json"

		resolvedVerify, err := config.ResolveOutputVerify(outputVerify, cmd.Flags().Changed("output-verify"))
		if err != nil {
			return err
		}
		resolvedRetries, err := config.ResolveOutputRetries(outputRetries, cmd.Flags().Changed("output-retries"))
		if err != nil {
			return err
		}
		resolvedRequireHeadings, err := config.ResolveOutputRequireHeadings(outputRequireHeadings, cmd.Flags().Changed("output-require-headings"))
		if err != nil {
			return err
		}
		resolvedChunkMode, err := config.ResolveOutputChunkMode(outputChunkMode, cmd.Flags().Changed("output-chunk-mode"))
		if err != nil {
			return err
		}

		cfg := app.Config{
			PackPath:              packPath,
			StatePath:             statePath,
			ReportPath:            reportPath,
			Resume:                resume,
			StopOnFail:            stopOnFail,
			WorkDir:               ".",
			OutDir:                outDir,
			ROIThreshold:          roiThreshold,
			ROIMode:               roiMode,
			OutputVerify:          resolvedVerify,
			OutputRetries:         resolvedRetries,
			OutputRequireHeadings: resolvedRequireHeadings,
			OutputChunkMode:       resolvedChunkMode,
		}

		a := app.New(cfg)
		// Prepare the application (loads pack, resolves out_dir, provisions env)
		if err := a.Prepare(); err != nil {
			return err
		}

		if err := a.LoadState(); err != nil {
			return err
		}

		findings, warning, err := pack.CheckPackScripts(a.Pack)
		if err != nil {
			return err
		}
		if warning != "" {
			fmt.Fprintf(cmd.OutOrStdout(), "Warning: %s\n", warning)
		}
		if len(findings) > 0 {
			for _, finding := range findings {
				if finding.StepID != "" {
					fmt.Fprintf(cmd.OutOrStdout(), "Step %s line %d: %s\n", finding.StepID, finding.Line, finding.Message)
				} else {
					fmt.Fprintf(cmd.OutOrStdout(), "Line %d: %s\n", finding.Line, finding.Message)
				}
			}
			return fmt.Errorf("bash syntax validation failed")
		}

		if noTUI {
			out := cmd.OutOrStdout()
			fmt.Fprintf(out, "[Selected] %s\n", packPath)
			fmt.Fprintln(out, "[Ready] Parsed and validated pack")
			err := a.RunPlain(context.Background(), out)
			if err != nil {
				fmt.Fprintf(out, "[Completed] Failed: %v\n", err)
				return err
			}
			fmt.Fprintln(out, "[Completed] Success")
			return nil
		}

		m := tui.NewModel(a.Pack, a.Runner, a.State, cfg.StatePath, cfg.ROIThreshold, cfg.ROIMode, runAll, cfg.OutputVerify, cfg.OutputRetries, cfg.OutputRequireHeadings, cfg.OutputChunkMode)
		p := tea.NewProgram(m, tea.WithAltScreen())
		_, err = p.Run()
		return err
	},
}

func init() {
	runCmd.Flags().BoolVarP(&yes, "yes", "y", false, "Auto-approve all steps")
	runCmd.Flags().BoolVar(&resume, "resume", false, "Resume from last successful step")
	runCmd.Flags().BoolVar(&stopOnFail, "stop-on-fail", true, "Stop execution if a step fails")
	runCmd.Flags().Float64Var(&roiThreshold, "roi-threshold", 0.0, "Filter steps by ROI threshold")
	runCmd.Flags().StringVar(&roiMode, "roi-mode", "over", "ROI filter mode ('over' or 'under')")
	runCmd.Flags().BoolVar(&runAll, "run-all", false, "Automatically run all steps sequentially on start")
[TRUNCATED]
```

internal/cli/verify_outputs.go
```
package cli

import (
	"fmt"
	"os"

	"github.com/spf13/cobra"
	"github.com/user/oraclepack/internal/config"
	"github.com/user/oraclepack/internal/pack"
)

var (
	verifyOutputsEnabled      bool
	verifyOutputsRequireHeads bool
	verifyOutputsChunkMode    string
)

var verifyOutputsCmd = &cobra.Command{
	Use:   "verify-outputs [pack.md]",
	Short: "Verify --write-output files without executing steps",
	Args:  cobra.ExactArgs(1),
	RunE: func(cmd *cobra.Command, args []string) error {
		out := cmd.OutOrStdout()
		data, err := os.ReadFile(args[0])
		if err != nil {
			return err
		}

		p, err := pack.Parse(data)
		if err != nil {
			return err
		}
		if err := pack.Validate(p); err != nil {
			return err
		}

		verifyEnabled, err := config.ResolveOutputVerify(verifyOutputsEnabled, cmd.Flags().Changed("output-verify"))
		if err != nil {
			return err
		}
		if !verifyEnabled {
			fmt.Fprintln(out, "Output verification disabled (ORACLEPACK_OUTPUT_VERIFY=false).")
			return nil
		}
		requireHeadings, err := config.ResolveOutputRequireHeadings(verifyOutputsRequireHeads, cmd.Flags().Changed("output-require-headings"))
		if err != nil {
			return err
		}
		chunkMode, err := config.ResolveOutputChunkMode(verifyOutputsChunkMode, cmd.Flags().Changed("output-chunk-mode"))
		if err != nil {
			return err
		}

		report := pack.VerifyReport{
			TotalSteps: len(p.Steps),
		}

		for i := range p.Steps {
			step := &p.Steps[i]
			failures := pack.VerifyStepOutputs(step, requireHeadings, chunkMode)
			if len(failures) == 0 {
				continue
			}
			report.CheckedSteps++
			for _, failure := range failures {
				failure.StepID = step.ID
				report.Failures = append(report.Failures, failure)
			}
		}

		fmt.Fprint(out, pack.FormatVerifyReport(report))
		if len(report.Failures) > 0 {
			return fmt.Errorf("output verification failed")
		}
		return nil
	},
}

func init() {
	verifyOutputsCmd.Flags().BoolVar(&verifyOutputsEnabled, "output-verify", config.DefaultOutputVerify, "Verify --write-output files contain required answer sections")
	verifyOutputsCmd.Flags().BoolVar(&verifyOutputsRequireHeads, "output-require-headings", config.DefaultOutputRequireHeadings, "Require strict output headings when verifying outputs")
	verifyOutputsCmd.Flags().StringVar(&verifyOutputsChunkMode, "output-chunk-mode", config.DefaultOutputChunkMode, "Output chunk verification mode: auto|single|multi")
	rootCmd.AddCommand(verifyOutputsCmd)
}
```

internal/config/defaults.go
```
package config

const (
	EnvOutputVerify          = "ORACLEPACK_OUTPUT_VERIFY"
	EnvOutputRetries         = "ORACLEPACK_OUTPUT_RETRIES"
	EnvOutputRequireHeadings = "ORACLEPACK_OUTPUT_REQUIRE_HEADINGS"
	EnvOutputChunkMode       = "ORACLEPACK_OUTPUT_CHUNK_MODE"
)

const (
	DefaultOutputVerify          = false
	DefaultOutputRetries         = 0
	DefaultOutputRequireHeadings = false
	DefaultOutputChunkMode       = "auto"
)
```

internal/config/resolve.go
```
package config

import (
	"fmt"
	"os"
	"strconv"
	"strings"
)

// ResolveOutputVerify applies precedence: CLI flag > env var > default.
func ResolveOutputVerify(flagValue bool, flagSet bool) (bool, error) {
	if flagSet {
		return flagValue, nil
	}
	if val, ok := os.LookupEnv(EnvOutputVerify); ok {
		parsed, err := parseBoolish(val)
		if err != nil {
			return DefaultOutputVerify, fmt.Errorf("invalid %s: %w", EnvOutputVerify, err)
		}
		return parsed, nil
	}
	return DefaultOutputVerify, nil
}

// ResolveOutputRetries applies precedence: CLI flag > env var > default.
func ResolveOutputRetries(flagValue int, flagSet bool) (int, error) {
	if flagSet {
		return flagValue, nil
	}
	if val, ok := os.LookupEnv(EnvOutputRetries); ok {
		parsed, err := strconv.Atoi(strings.TrimSpace(val))
		if err != nil {
			return DefaultOutputRetries, fmt.Errorf("invalid %s: %w", EnvOutputRetries, err)
		}
		return parsed, nil
	}
	return DefaultOutputRetries, nil
}

// ResolveOutputRequireHeadings applies precedence: CLI flag > env var > default.
func ResolveOutputRequireHeadings(flagValue bool, flagSet bool) (bool, error) {
	if flagSet {
		return flagValue, nil
	}
	if val, ok := os.LookupEnv(EnvOutputRequireHeadings); ok {
		parsed, err := parseBoolish(val)
		if err != nil {
			return DefaultOutputRequireHeadings, fmt.Errorf("invalid %s: %w", EnvOutputRequireHeadings, err)
		}
		return parsed, nil
	}
	return DefaultOutputRequireHeadings, nil
}

// ResolveOutputChunkMode applies precedence: CLI flag > env var > default.
func ResolveOutputChunkMode(flagValue string, flagSet bool) (string, error) {
	if flagSet {
		return normalizeChunkMode(flagValue)
	}
	if val, ok := os.LookupEnv(EnvOutputChunkMode); ok {
		return normalizeChunkMode(val)
	}
	return normalizeChunkMode(DefaultOutputChunkMode)
}

func parseBoolish(raw string) (bool, error) {
	v := strings.ToLower(strings.TrimSpace(raw))
	switch v {
	case "1", "true", "yes", "on":
		return true, nil
	case "0", "false", "no", "off":
		return false, nil
	default:
		return false, fmt.Errorf("expected boolean (true/false, 1/0, on/off), got %q", raw)
	}
}

func normalizeChunkMode(raw string) (string, error) {
	v := strings.ToLower(strings.TrimSpace(raw))
	switch v {
	case "auto", "single", "multi":
		return v, nil
	case "":
		return DefaultOutputChunkMode, nil
	default:
		return "", fmt.Errorf("invalid %s: expected auto|single|multi, got %q", EnvOutputChunkMode, raw)
	}
}
```

internal/dispatch/classify.go
```
package dispatch

import (
	"regexp"
	"strings"

	"github.com/user/oraclepack/internal/tools"
)

var classifier = regexp.MustCompile(`^(\s*)(oracle|tm|task-master|codex|gemini)\b`)

// Classification describes a parsed command prefix.
type Classification struct {
	Kind    tools.ToolKind
	Prefix  string
	Command string
}

// Classify detects a supported tool prefix and returns the remaining command.
func Classify(line string) (Classification, bool) {
	m := classifier.FindStringSubmatch(line)
	if len(m) < 3 {
		return Classification{}, false
	}
	prefix := m[2]
	kind := toolKindFromPrefix(prefix)
	if kind == nil {
		return Classification{}, false
	}
	trimmed := strings.TrimSpace(line[len(m[1])+len(prefix):])
	return Classification{Kind: *kind, Prefix: prefix, Command: strings.TrimSpace(trimmed)}, true
}

func toolKindFromPrefix(prefix string) *tools.ToolKind {
	var kind tools.ToolKind
	switch prefix {
	case "oracle":
		kind = tools.ToolOracle
	case "tm":
		kind = tools.ToolTM
	case "task-master":
		kind = tools.ToolTaskMaster
	case "codex":
		kind = tools.ToolCodex
	case "gemini":
		kind = tools.ToolGemini
	default:
		return nil
	}
	return &kind
}
```

internal/dispatch/classify_test.go
```
package dispatch

import "testing"

func TestClassify(t *testing.T) {
	tests := []struct {
		line    string
		wantOK  bool
		wantCmd string
	}{
		{"oracle query \"hi\"", true, "query \"hi\""},
		{"  tm list", true, "list"},
		{"task-master next", true, "next"},
		{"codex exec \"x\"", true, "exec \"x\""},
		{"gemini run", true, "run"},
		{"echo hello", false, ""},
	}
	for _, tt := range tests {
		t.Run(tt.line, func(t *testing.T) {
			got, ok := Classify(tt.line)
			if ok != tt.wantOK {
				t.Fatalf("expected ok=%v got %v", tt.wantOK, ok)
			}
			if ok && got.Command != tt.wantCmd {
				t.Fatalf("expected cmd %q got %q", tt.wantCmd, got.Command)
			}
		})
	}
}
```

internal/errors/errors.go
```
package errors

import (
	"errors"
)

var (
	// ErrInvalidPack is returned when the Markdown pack is malformed.
	ErrInvalidPack = errors.New("invalid pack structure")
	// ErrExecutionFailed is returned when a shell command fails.
	ErrExecutionFailed = errors.New("execution failed")
	// ErrConfigInvalid is returned when CLI flags or environment variables are incorrect.
	ErrConfigInvalid = errors.New("invalid configuration")
)

// ExitCode returns the appropriate exit code for a given error.
func ExitCode(err error) int {
	if err == nil {
		return 0
	}

	if errors.Is(err, ErrConfigInvalid) {
		return 2
	}

	if errors.Is(err, ErrInvalidPack) {
		return 3
	}

	if errors.Is(err, ErrExecutionFailed) {
		return 4
	}

	return 1 // Generic error
}
```

internal/errors/errors_test.go
```
package errors

import (
	"errors"
	"fmt"
	"testing"
)

func TestExitCode(t *testing.T) {
	tests := []struct {
		name     string
		err      error
		expected int
	}{
		{"nil error", nil, 0},
		{"generic error", errors.New("generic"), 1},
		{"invalid pack", ErrInvalidPack, 3},
		{"execution failed", ErrExecutionFailed, 4},
		{"config invalid", ErrConfigInvalid, 2},
		{"wrapped invalid pack", fmt.Errorf("wrap: %w", ErrInvalidPack), 3},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			if got := ExitCode(tt.err); got != tt.expected {
				t.Errorf("ExitCode() = %v, want %v", got, tt.expected)
			}
		})
	}
}
```

internal/exec/flags.go
```
package exec

import "strings"

// ApplyChatGPTURL ensures a single --chatgpt-url flag is present when url is set.
// It removes any existing --chatgpt-url/--browser-url flags and their values.
func ApplyChatGPTURL(flags []string, url string) []string {
	var out []string
	skipNext := false
	for _, f := range flags {
		if skipNext {
			skipNext = false
			continue
		}
		if f == "--chatgpt-url" || f == "--browser-url" {
			skipNext = true
			continue
		}
		if strings.HasPrefix(f, "--chatgpt-url=") || strings.HasPrefix(f, "--browser-url=") {
			continue
		}
		out = append(out, f)
	}
	if url != "" {
		out = append(out, "--chatgpt-url", url)
	}
	return out
}
```

internal/exec/inject.go
```
package exec

import "strings"

// InjectFlags scans a script and appends flags to any 'oracle' command invocation.
func InjectFlags(script string, flags []string) string {
	if len(flags) == 0 {
		return script
	}

	flagStr := strings.Join(flags, " ")

	lines := strings.Split(script, "\n")
	for i, line := range lines {
		trimmed := strings.TrimSpace(line)
		if strings.HasPrefix(trimmed, "#") {
			continue
		}

		insertIdx := oracleInsertIndex(line)
		if insertIdx == -1 {
			continue
		}

		lines[i] = insertFlagsInLine(line, insertIdx, flagStr)
	}

	return strings.Join(lines, "\n")
}

func oracleInsertIndex(line string) int {
	i := 0
	for i < len(line) && (line[i] == ' ' || line[i] == '\t') {
		i++
	}

	if !strings.HasPrefix(line[i:], "oracle") {
		return -1
	}

	end := i + len("oracle")
	if end < len(line) {
		next := line[end]
		if next != ' ' && next != '\t' {
			return -1
		}
	}

	return end
}

func insertFlagsInLine(line string, insertIdx int, flags string) string {
	prefix := line[:insertIdx]
	rest := line[insertIdx:]
	if rest == "" {
		return prefix + " " + flags
	}
	if rest[0] == ' ' || rest[0] == '\t' {
		return prefix + " " + flags + rest
	}
	return prefix + " " + flags + " " + rest
}
```

internal/exec/inject_test.go
```
package exec

import (
	"testing"
)

func TestInjectFlags(t *testing.T) {
	tests := []struct {
		name     string
		script   string
		flags    []string
		expected string
	}{
		{
			"simple injection",
			"oracle query 'hello'",
			[]string{"--verbose"},
			"oracle --verbose query 'hello'",
		},
		{
			"indented injection",
			"  oracle query 'hello'",
			[]string{"--verbose"},
			"  oracle --verbose query 'hello'",
		},
		{
			"no injection needed",
			"echo 'hello'",
			[]string{"--verbose"},
			"echo 'hello'",
		},
		{
			"multiple lines",
			"echo 'start'\noracle query\necho 'end'",
			[]string{"--debug"},
			"echo 'start'\noracle --debug query\necho 'end'",
		},
		{
			"multiline with continuation",
			"oracle \\\n  --json \\\n  --files",
			[]string{"--flag"},
			"oracle --flag \\\n  --json \\\n  --files",
		},
		{
			"multiline with args and continuation",
			"  oracle arg \\\n  --json",
			[]string{"--flag"},
			"  oracle --flag arg \\\n  --json",
		},
		{
			"commented command",
			"# oracle --json",
			[]string{"--verbose"},
			"# oracle --json",
		},
		{
			"oracle as part of word",
			"coracle query",
			[]string{"--verbose"},
			"coracle query",
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got := InjectFlags(tt.script, tt.flags)
			if got != tt.expected {
				t.Errorf("InjectFlags() = %q, want %q", got, tt.expected)
			}
		})
	}
}
```

internal/exec/oracle_scan.go
```
package exec

import (
	"regexp"
	"strings"
)

var oracleCmdRegex = regexp.MustCompile(`^(\s*)(oracle)\b`)

// OracleInvocation represents a detected oracle command in a script.
type OracleInvocation struct {
	StartLine   int    // 0-based start line index
	EndLine     int    // 0-based end line index (inclusive)
	Raw         string // The full command string (joined if multi-line)
	Display     string // A trimmed version for UI display
	Indentation string // The leading whitespace
}

// ExtractOracleInvocations extracts oracle invocations from a script.
func ExtractOracleInvocations(script string) []OracleInvocation {
	var invocations []OracleInvocation
	lines := strings.Split(script, "\n")

	for i := 0; i < len(lines); i++ {
		line := lines[i]
		trimmed := strings.TrimSpace(line)

		// Skip comments
		if strings.HasPrefix(trimmed, "#") {
			continue
		}

		// Check for oracle command
		loc := oracleCmdRegex.FindStringSubmatchIndex(line)
		if loc != nil {
			startLine := i
			// Group 1 is the indentation
			indentation := line[loc[2]:loc[3]]

			var cmdBuilder strings.Builder
			cmdBuilder.WriteString(line)

			endLine := i
			// Handle line continuations
			// Check if line ends with backslash (ignoring trailing whitespace)
			for {
				if endLine+1 >= len(lines) {
					break
				}

				// Check current line for continuation
				currTrimmed := strings.TrimRight(lines[endLine], " \t")
				if !strings.HasSuffix(currTrimmed, "\\") {
					break
				}

				endLine++
				cmdBuilder.WriteString("\n")
				cmdBuilder.WriteString(lines[endLine])
			}

			raw := cmdBuilder.String()
			invocations = append(invocations, OracleInvocation{
				StartLine:   startLine,
				EndLine:     endLine,
				Raw:         raw,
				Display:     strings.TrimSpace(raw),
				Indentation: indentation,
			})

			i = endLine // Advance loop
		}
	}
	return invocations
}
```

internal/exec/oracle_scan_test.go
```
package exec

import (
	"reflect"
	"testing"
)

func TestExtractOracleInvocations(t *testing.T) {
	tests := []struct {
		name   string
		script string
		want   []OracleInvocation
	}{
		{
			name:   "Simple command",
			script: "oracle --json",
			want: []OracleInvocation{
				{StartLine: 0, EndLine: 0, Raw: "oracle --json", Display: "oracle --json", Indentation: ""},
			},
		},
		{
			name:   "Indented command",
			script: "  oracle --json",
			want: []OracleInvocation{
				{StartLine: 0, EndLine: 0, Raw: "  oracle --json", Display: "oracle --json", Indentation: "  "},
			},
		},
		{
			name: "Multiline command",
			script: `oracle \
  --json \
  --files`,
			want: []OracleInvocation{
				{StartLine: 0, EndLine: 2, Raw: `oracle \
  --json \
  --files`, Display: `oracle \
  --json \
  --files`, Indentation: ""},
			},
		},
		{
			name: "Commented command",
			script: `# oracle --json
oracle --real`,
			want: []OracleInvocation{
				{StartLine: 1, EndLine: 1, Raw: "oracle --real", Display: "oracle --real", Indentation: ""},
			},
		},
		{
			name: "Multiple commands",
			script: `
echo start
oracle --one
echo mid
oracle --two
echo end
`,
			want: []OracleInvocation{
				{StartLine: 2, EndLine: 2, Raw: "oracle --one", Display: "oracle --one", Indentation: ""},
				{StartLine: 4, EndLine: 4, Raw: "oracle --two", Display: "oracle --two", Indentation: ""},
			},
		},
		{
			name:   "Oraclepack prefix (should not match)",
			script: "oraclepack run",
			want:   nil,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got := ExtractOracleInvocations(tt.script)
			if !reflect.DeepEqual(got, tt.want) {
				t.Errorf("ExtractOracleInvocations() = %+v, want %+v", got, tt.want)
			}
		})
	}
}
```

internal/exec/oracle_validate.go
```
package exec

import (
	"bytes"
	"context"
	"os"
	"os/exec"
	"strings"

	"github.com/user/oraclepack/internal/overrides"
	"github.com/user/oraclepack/internal/types"
)

// ValidationError captures a failed oracle validation for a step.
type ValidationError struct {
	StepID       string
	Command      string
	ErrorMessage string
}

// ValidateOverrides runs oracle --dry-run summary for targeted steps.
func ValidateOverrides(
	ctx context.Context,
	steps []types.Step,
	over *overrides.RuntimeOverrides,
	baseline []string,
	opts RunnerOptions,
) ([]ValidationError, error) {
	if over == nil || over.ApplyToSteps == nil {
		return nil, nil
	}

	shell := opts.Shell
	if shell == "" {
		shell = "/bin/bash"
	}
	env := append(os.Environ(), opts.Env...)

	var results []ValidationError
	for _, step := range steps {
		if !over.ApplyToSteps[step.ID] {
			continue
		}

		invocations := ExtractOracleInvocations(step.Code)
		if len(invocations) == 0 {
			continue
		}

		flags := over.EffectiveFlags(step.ID, baseline)
		flags = append(flags, "--dry-run", "summary")

		for _, inv := range invocations {
			cmdStr := InjectFlags(inv.Raw, flags)
			msg, err := execDryRun(ctx, shell, opts.WorkDir, env, cmdStr)
			if err == nil {
				continue
			}

			results = append(results, ValidationError{
				StepID:       step.ID,
				Command:      cmdStr,
				ErrorMessage: msg,
			})
		}
	}

	return results, nil
}

func execDryRun(ctx context.Context, shell, workDir string, env []string, command string) (string, error) {
	if pathVal := findEnvValue(env, "PATH"); pathVal != "" {
		command = "export PATH=" + shellQuote(pathVal) + "; " + command
	}

	cmd := exec.CommandContext(ctx, shell, "-lc", command)
	if workDir != "" {
		cmd.Dir = workDir
	}
	cmd.Env = env

	var stdout, stderr bytes.Buffer
	cmd.Stdout = &stdout
	cmd.Stderr = &stderr

	err := cmd.Run()
	if err == nil {
		return stdout.String(), nil
	}
	if stderr.Len() > 0 {
		return strings.TrimSpace(stderr.String()), err
	}
	if stdout.Len() > 0 {
		return strings.TrimSpace(stdout.String()), err
	}
	return err.Error(), err
}

func findEnvValue(env []string, key string) string {
	prefix := key + "="
	for _, entry := range env {
		if strings.HasPrefix(entry, prefix) {
			return strings.TrimPrefix(entry, prefix)
		}
	}
	return ""
}

func shellQuote(value string) string {
	if value == "" {
		return "''"
	}
	return "'" + strings.ReplaceAll(value, "'", "'\\''") + "'"
}
```

internal/exec/oracle_validate_test.go
```
package exec

import (
	"context"
	"os"
	"path/filepath"
	"strings"
	"testing"

	"github.com/user/oraclepack/internal/overrides"
	"github.com/user/oraclepack/internal/types"
)

func TestValidateOverrides_Success(t *testing.T) {
	dir := t.TempDir()
	writeOracleStub(t, dir)

	steps := []types.Step{
		{ID: "01", Code: "oracle --ok"},
	}
	over := &overrides.RuntimeOverrides{
		ApplyToSteps: map[string]bool{"01": true},
	}

	_, err := ValidateOverrides(
		context.Background(),
		steps,
		over,
		[]string{"--base"},
		RunnerOptions{
			WorkDir: dir,
			Env:     []string{"PATH=" + dir + string(os.PathListSeparator) + os.Getenv("PATH")},
		},
	)
	if err != nil {
		t.Fatalf("ValidateOverrides failed: %v", err)
	}
}

func TestValidateOverrides_Error(t *testing.T) {
	dir := t.TempDir()
	writeOracleStub(t, dir)

	steps := []types.Step{
		{ID: "01", Code: "oracle --bad"},
	}
	over := &overrides.RuntimeOverrides{
		ApplyToSteps: map[string]bool{"01": true},
	}

	errs, err := ValidateOverrides(
		context.Background(),
		steps,
		over,
		nil,
		RunnerOptions{
			WorkDir: dir,
			Env:     []string{"PATH=" + dir + string(os.PathListSeparator) + os.Getenv("PATH")},
		},
	)
	if err != nil {
		t.Fatalf("ValidateOverrides failed: %v", err)
	}
	if len(errs) != 1 {
		t.Fatalf("expected 1 validation error, got %d", len(errs))
	}
	msg := errs[0].ErrorMessage
	if !strings.Contains(msg, "invalid flag") && !strings.Contains(msg, "unknown option") {
		t.Fatalf("unexpected error message: %q", msg)
	}
	if !strings.Contains(errs[0].Command, "--dry-run summary") {
		t.Fatalf("expected command to include --dry-run summary, got %q", errs[0].Command)
	}
}

func writeOracleStub(t *testing.T, dir string) {
	t.Helper()
	stub := `#!/bin/sh
has_dry=0
has_summary=0
for arg in "$@"; do
  if [ "$arg" = "--dry-run" ]; then has_dry=1; fi
  if [ "$arg" = "summary" ]; then has_summary=1; fi
  if [ "$arg" = "--bad" ]; then echo "invalid flag" 1>&2; exit 1; fi
done
if [ $has_dry -eq 0 ] || [ $has_summary -eq 0 ]; then
  echo "missing dry run" 1>&2
  exit 1
fi
exit 0
`
	path := filepath.Join(dir, "oracle")
	if err := os.WriteFile(path, []byte(stub), 0o755); err != nil {
		t.Fatalf("write oracle stub: %v", err)
	}
}
```

internal/exec/runner.go
```
package exec

import (
	"context"
	"fmt"
	"io"
	"os"
	"os/exec"

	"github.com/user/oraclepack/internal/errors"
	"github.com/user/oraclepack/internal/overrides"
	"github.com/user/oraclepack/internal/types"
)

// Runner handles the execution of shell scripts.
type Runner struct {
	Shell       string
	WorkDir     string
	Env         []string
	OracleFlags []string
	Overrides   *overrides.RuntimeOverrides
	ChatGPTURL  string
	warnings    []SanitizeWarning
}

// RunnerOptions configures a Runner.
type RunnerOptions struct {
	Shell       string
	WorkDir     string
	Env         []string
	OracleFlags []string
	Overrides   *overrides.RuntimeOverrides
	ChatGPTURL  string
}

// NewRunner creates a new Runner with options.
func NewRunner(opts RunnerOptions) *Runner {
	shell := opts.Shell
	if shell == "" {
		shell = "/bin/bash"
	}

	return &Runner{
		Shell:       shell,
		WorkDir:     opts.WorkDir,
		Env:         append(os.Environ(), opts.Env...),
		OracleFlags: opts.OracleFlags,
		Overrides:   opts.Overrides,
		ChatGPTURL:  opts.ChatGPTURL,
	}
}

// RunPrelude executes the prelude code.
func (r *Runner) RunPrelude(ctx context.Context, p *types.Prelude, logWriter io.Writer) error {
	script, warnings := SanitizeScript(p.Code, "prelude", "")
	r.recordWarnings(warnings, logWriter)
	return r.run(ctx, script, logWriter)
}

// RunStep executes a single step's code.
func (r *Runner) RunStep(ctx context.Context, s *types.Step, logWriter io.Writer) error {
	flags := ApplyChatGPTURL(r.OracleFlags, r.ChatGPTURL)
	if r.Overrides != nil {
		flags = r.Overrides.EffectiveFlags(s.ID, r.OracleFlags)
		flags = ApplyChatGPTURL(flags, r.ChatGPTURL)
	}
	code := InjectFlags(s.Code, flags)
	script, warnings := SanitizeScript(code, "step", s.ID)
	r.recordWarnings(warnings, logWriter)
	return r.run(ctx, script, logWriter)
}

func (r *Runner) recordWarnings(warnings []SanitizeWarning, logWriter io.Writer) {
	if len(warnings) == 0 {
		return
	}
	for _, w := range warnings {
		r.warnings = append(r.warnings, w)
		if logWriter != nil {
			scope := w.Scope
			if scope == "" {
				scope = "script"
			}
			step := ""
			if w.StepID != "" {
				step = " step " + w.StepID
			}
			_, _ = fmt.Fprintf(logWriter, "⚠ oraclepack: sanitized label in %s%s line %d: %s\n", scope, step, w.Line, w.Token)
		}
	}
}

// DrainWarnings returns any sanitizer warnings collected since the last call.
func (r *Runner) DrainWarnings() []SanitizeWarning {
	if len(r.warnings) == 0 {
		return nil
	}
	out := make([]SanitizeWarning, len(r.warnings))
	copy(out, r.warnings)
	r.warnings = nil
	return out
}

func (r *Runner) run(ctx context.Context, script string, logWriter io.Writer) error {
	// We use bash -lc to ensure login shell (paths, aliases, etc)
	cmd := exec.CommandContext(ctx, r.Shell, "-lc", script)
	cmd.Dir = r.WorkDir
	cmd.Env = r.Env

	// Standardize stdout and stderr to the logWriter
	cmd.Stdout = logWriter
	cmd.Stderr = logWriter

	err := cmd.Run()
	if err != nil {
		if ctx.Err() != nil {
			return ctx.Err()
		}
		return fmt.Errorf("%w: %v", errors.ErrExecutionFailed, err)
	}

	return nil
}
```

internal/exec/runner_test.go
```
package exec

import (
	"context"
	"strings"
	"testing"

	"github.com/user/oraclepack/internal/types"
)

func TestRunner_RunStep(t *testing.T) {
	r := NewRunner(RunnerOptions{})

	var lines []string
	lw := &LineWriter{
		Callback: func(line string) {
			lines = append(lines, line)
		},
	}

	step := &types.Step{
		Code: "echo 'hello world'",
	}

	err := r.RunStep(context.Background(), step, lw)
	if err != nil {
		t.Fatalf("RunStep failed: %v", err)
	}
	lw.Close()

	found := false
	for _, l := range lines {
		if strings.TrimSpace(l) == "hello world" {
			found = true
			break
		}
	}

	if !found {
		t.Errorf("expected 'hello world' in output, got: %v", lines)
	}
}

func TestRunner_ContextCancellation(t *testing.T) {
	r := NewRunner(RunnerOptions{})

	ctx, cancel := context.WithCancel(context.Background())
	cancel() // Cancel immediately

	step := &types.Step{
		Code: "sleep 10",
	}

	err := r.RunStep(ctx, step, nil)
	if err != context.Canceled {
		t.Errorf("expected context.Canceled, got %v", err)
	}
}
```

internal/exec/sanitize.go
```
package exec

import (
	osexec "os/exec"
	"regexp"
	"strings"
)

// SanitizeWarning captures a label line that was converted to a safe echo.
type SanitizeWarning struct {
	Scope   string
	StepID  string
	Line    int
	Token   string
	Message string
}

var (
	labelTokenRegex   = regexp.MustCompile(`^[A-Za-z][A-Za-z0-9_-]*$`)
	heredocStartRegex = regexp.MustCompile(`<<-?\s*['"]?([A-Za-z0-9_]+)['"]?`)
)

var shellBuiltins = map[string]bool{
	"alias":    true,
	"bg":       true,
	"break":    true,
	"cd":       true,
	"command":  true,
	"continue": true,
	"declare":  true,
	"dirs":     true,
	"echo":     true,
	"eval":     true,
	"exec":     true,
	"exit":     true,
	"export":   true,
	"fg":       true,
	"getopts":  true,
	"hash":     true,
	"help":     true,
	"jobs":     true,
	"local":    true,
	"popd":     true,
	"printf":   true,
	"pushd":    true,
	"pwd":      true,
	"readonly": true,
	"return":   true,
	"set":      true,
	"shift":    true,
	"source":   true,
	"test":     true,
	"trap":     true,
	"true":     true,
	"type":     true,
	"ulimit":   true,
	"umask":    true,
	"unalias":  true,
	"unset":    true,
	"wait":     true,
	"false":    true,
}

var shellKeywords = map[string]bool{
	"case":     true,
	"do":       true,
	"done":     true,
	"elif":     true,
	"else":     true,
	"esac":     true,
	"fi":       true,
	"for":      true,
	"function": true,
	"if":       true,
	"in":       true,
	"select":   true,
	"then":     true,
	"time":     true,
	"until":    true,
	"while":    true,
}

// SanitizeScript converts bare label-like lines into safe echo statements.
func SanitizeScript(script, scope, stepID string) (string, []SanitizeWarning) {
	if script == "" {
		return script, nil
	}

	lines := strings.Split(script, "\n")
	var warnings []SanitizeWarning
	var heredocEnd string

	for i, line := range lines {
		trimmed := strings.TrimSpace(line)
		if heredocEnd != "" {
			if trimmed == heredocEnd {
				heredocEnd = ""
			}
			continue
		}
		if trimmed == "" || strings.HasPrefix(trimmed, "#") {
			continue
		}

		if end := heredocStartToken(trimmed); end != "" {
			heredocEnd = end
			continue
		}

		fields := strings.Fields(trimmed)
		if len(fields) != 1 {
			continue
		}
		token := fields[0]
		if !labelTokenRegex.MatchString(token) {
			continue
		}
		lower := strings.ToLower(token)
		if shellBuiltins[lower] || shellKeywords[lower] {
			continue
		}
		if _, err := osexec.LookPath(token); err == nil {
			continue
		}

		indent := line[:len(line)-len(strings.TrimLeft(line, " \t"))]
		lines[i] = indent + "echo \"" + token + "\""
		warnings = append(warnings, SanitizeWarning{
			Scope:   scope,
			StepID:  stepID,
			Line:    i + 1,
			Token:   token,
			Message: "Converted bare label to echo",
		})
	}

	return strings.Join(lines, "\n"), warnings
}

func heredocStartToken(line string) string {
	match := heredocStartRegex.FindStringSubmatch(line)
	if len(match) < 2 {
		return ""
	}
	return match[1]
}
```

internal/exec/sanitize_test.go
```
package exec

import "testing"

func TestSanitizeScript_LabelLine(t *testing.T) {
	input := "GenerateReport\noracle --help\n"
	got, warnings := SanitizeScript(input, "step", "01")
	if len(warnings) != 1 {
		t.Fatalf("expected 1 warning, got %d", len(warnings))
	}
	if warnings[0].Token != "GenerateReport" {
		t.Fatalf("expected token GenerateReport, got %s", warnings[0].Token)
	}
	wantPrefix := "echo \"GenerateReport\""
	if got[:len(wantPrefix)] != wantPrefix {
		t.Fatalf("expected sanitized line to start with %q, got %q", wantPrefix, got)
	}
}

func TestSanitizeScript_BuiltinUnchanged(t *testing.T) {
	input := "echo\n"
	got, warnings := SanitizeScript(input, "step", "01")
	if len(warnings) != 0 {
		t.Fatalf("expected no warnings, got %d", len(warnings))
	}
	if got != input {
		t.Fatalf("expected script unchanged, got %q", got)
	}
}

func TestSanitizeScript_HeredocUnchanged(t *testing.T) {
	input := "cat <<'EOF'\nGenerateReport\nEOF\n"
	got, warnings := SanitizeScript(input, "step", "01")
	if len(warnings) != 0 {
		t.Fatalf("expected no warnings, got %d", len(warnings))
	}
	if got != input {
		t.Fatalf("expected heredoc unchanged, got %q", got)
	}
}
```

internal/exec/stream.go
```
package exec

import (
	"io"
)

// LineWriter is an io.Writer that splits output into lines and calls a callback.
type LineWriter struct {
	Callback func(string)
	buffer   []byte
}

func (w *LineWriter) Write(p []byte) (n int, err error) {
	for _, b := range p {
		if b == '\n' {
			w.Callback(string(w.buffer))
			w.buffer = w.buffer[:0]
		} else {
			w.buffer = append(w.buffer, b)
		}
	}
	return len(p), nil
}

// Close flushes any remaining data in the buffer.
func (w *LineWriter) Close() error {
	if len(w.buffer) > 0 {
		w.Callback(string(w.buffer))
		w.buffer = w.buffer[:0]
	}
	return nil
}

// MultiWriter handles multiple writers efficiently.
func MultiWriter(writers ...io.Writer) io.Writer {
	return io.MultiWriter(writers...)
}
```

internal/foundation/atomic.go
```
package foundation

import (
	"fmt"
	"os"
)

// WriteAtomic writes data to path atomically by writing to a temp file and renaming.
func WriteAtomic(path string, data []byte, perm os.FileMode) error {
	tempPath := path + ".tmp"
	if err := os.WriteFile(tempPath, data, perm); err != nil {
		return fmt.Errorf("write temp file: %w", err)
	}
	if err := os.Rename(tempPath, path); err != nil {
		_ = os.Remove(tempPath)
		return fmt.Errorf("rename temp file: %w", err)
	}
	return nil
}
```

internal/foundation/atomic_test.go
```
package foundation

import (
	"os"
	"path/filepath"
	"testing"
)

func TestWriteAtomic(t *testing.T) {
	dir := t.TempDir()
	path := filepath.Join(dir, "out.json")
	if err := WriteAtomic(path, []byte("hello"), 0644); err != nil {
		t.Fatalf("WriteAtomic: %v", err)
	}
	data, err := os.ReadFile(path)
	if err != nil {
		t.Fatalf("read: %v", err)
	}
	if string(data) != "hello" {
		t.Fatalf("unexpected contents: %q", string(data))
	}
}
```

internal/foundation/clock.go
```
package foundation

import "time"

// Clock abstracts time for deterministic testing.
type Clock interface {
	Now() time.Time
}

// RealClock uses the system clock.
type RealClock struct{}

// Now returns the current time.
func (RealClock) Now() time.Time { return time.Now() }

// MockClock returns a fixed time that can be advanced.
type MockClock struct {
	current time.Time
}

// NewMockClock initializes a mock clock with a starting time.
func NewMockClock(start time.Time) *MockClock {
	return &MockClock{current: start}
}

// Now returns the mock time.
func (m *MockClock) Now() time.Time { return m.current }

// Advance moves the mock time forward.
func (m *MockClock) Advance(d time.Duration) {
	m.current = m.current.Add(d)
}
```

internal/foundation/clock_test.go
```
package foundation

import (
	"testing"
	"time"
)

func TestMockClock(t *testing.T) {
	start := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC)
	m := NewMockClock(start)
	if !m.Now().Equal(start) {
		t.Fatalf("expected %v, got %v", start, m.Now())
	}
	m.Advance(2 * time.Hour)
	want := start.Add(2 * time.Hour)
	if !m.Now().Equal(want) {
		t.Fatalf("expected %v, got %v", want, m.Now())
	}
}
```

internal/foundation/config.go
```
package foundation

import (
	"encoding/json"
	"fmt"
	"os"
	"strconv"
)

// Config holds runtime settings that can be loaded from JSON and environment variables.
// Env values always take precedence over JSON values.
type Config struct {
	Name      string  `json:"name" env:"ORACLEPACK_NAME"`
	Retries   int     `json:"retries" env:"ORACLEPACK_RETRIES"`
	Enabled   bool    `json:"enabled" env:"ORACLEPACK_ENABLED"`
	Threshold float64 `json:"threshold" env:"ORACLEPACK_THRESHOLD"`
}

// LoadConfig loads configuration from a JSON file and then applies environment overrides.
// If path is empty, JSON loading is skipped and only env overrides are applied.
func LoadConfig(path string) (Config, error) {
	var cfg Config
	if path != "" {
		data, err := os.ReadFile(path)
		if err != nil {
			return Config{}, fmt.Errorf("read config: %w", err)
		}
		if err := json.Unmarshal(data, &cfg); err != nil {
			return Config{}, fmt.Errorf("parse config: %w", err)
		}
	}

	if v, ok := os.LookupEnv("ORACLEPACK_NAME"); ok {
		cfg.Name = v
	}
	if v, ok := os.LookupEnv("ORACLEPACK_RETRIES"); ok {
		parsed, err := strconv.Atoi(v)
		if err != nil {
			return Config{}, fmt.Errorf("parse ORACLEPACK_RETRIES: %w", err)
		}
		cfg.Retries = parsed
	}
	if v, ok := os.LookupEnv("ORACLEPACK_ENABLED"); ok {
		parsed, err := strconv.ParseBool(v)
		if err != nil {
			return Config{}, fmt.Errorf("parse ORACLEPACK_ENABLED: %w", err)
		}
		cfg.Enabled = parsed
	}
	if v, ok := os.LookupEnv("ORACLEPACK_THRESHOLD"); ok {
		parsed, err := strconv.ParseFloat(v, 64)
		if err != nil {
			return Config{}, fmt.Errorf("parse ORACLEPACK_THRESHOLD: %w", err)
		}
		cfg.Threshold = parsed
	}

	return cfg, nil
}
```

internal/foundation/config_test.go
```
package foundation

import (
	"os"
	"path/filepath"
	"testing"
)

func TestLoadConfigEnvOverrides(t *testing.T) {
	t.Setenv("ORACLEPACK_NAME", "env-name")
	t.Setenv("ORACLEPACK_RETRIES", "5")
	t.Setenv("ORACLEPACK_ENABLED", "true")
	t.Setenv("ORACLEPACK_THRESHOLD", "2.5")

	dir := t.TempDir()
	path := filepath.Join(dir, "config.json")
	if err := os.WriteFile(path, []byte(`{"name":"json-name","retries":1,"enabled":false,"threshold":1.0}`), 0644); err != nil {
		t.Fatalf("write json: %v", err)
	}

	cfg, err := LoadConfig(path)
	if err != nil {
		t.Fatalf("LoadConfig: %v", err)
	}

	if cfg.Name != "env-name" || cfg.Retries != 5 || cfg.Enabled != true || cfg.Threshold != 2.5 {
		t.Fatalf("env overrides not applied: %+v", cfg)
	}
}

func TestLoadConfigJSONOnly(t *testing.T) {
	dir := t.TempDir()
	path := filepath.Join(dir, "config.json")
	if err := os.WriteFile(path, []byte(`{"name":"json-name","retries":3,"enabled":true,"threshold":4.25}`), 0644); err != nil {
		t.Fatalf("write json: %v", err)
	}

	cfg, err := LoadConfig(path)
	if err != nil {
		t.Fatalf("LoadConfig: %v", err)
	}

	if cfg.Name != "json-name" || cfg.Retries != 3 || cfg.Enabled != true || cfg.Threshold != 4.25 {
		t.Fatalf("json load mismatch: %+v", cfg)
	}
}
```

internal/foundation/errors.go
```
package foundation

import "errors"

var (
	// ErrMissingBinary is returned when a required binary is not found on PATH.
	ErrMissingBinary = errors.New("missing binary")
	// ErrArtifactMissing is returned when an expected artifact is absent.
	ErrArtifactMissing = errors.New("artifact missing")
)
```

internal/foundation/errors_test.go
```
package foundation

import (
	"errors"
	"testing"
)

func TestCommonErrors(t *testing.T) {
	if ErrMissingBinary == nil || ErrArtifactMissing == nil {
		t.Fatal("expected error variables to be initialized")
	}
	if !errors.Is(ErrMissingBinary, ErrMissingBinary) {
		t.Fatal("errors.Is failed for ErrMissingBinary")
	}
	if !errors.Is(ErrArtifactMissing, ErrArtifactMissing) {
		t.Fatal("errors.Is failed for ErrArtifactMissing")
	}
}
```

internal/overrides/merge.go
```
package overrides

// EffectiveFlags calculates the final flags for a step.
func (r *RuntimeOverrides) EffectiveFlags(stepID string, baseline []string) []string {
	if r == nil || r.ApplyToSteps == nil || !r.ApplyToSteps[stepID] {
		return baseline
	}

	var effective []string

	// Map for removed flags
	removed := make(map[string]bool)
	for _, f := range r.RemovedFlags {
		removed[f] = true
	}

	// Filter baseline
	for _, flag := range baseline {
		if !removed[flag] {
			effective = append(effective, flag)
		}
	}

	// Append added flags
	effective = append(effective, r.AddedFlags...)

	// Inject ChatGPTURL
	if r.ChatGPTURL != "" {
		effective = append(effective, "--chatgpt-url", r.ChatGPTURL)
	}

	return effective
}
```

internal/overrides/merge_test.go
```
package overrides

import (
	"reflect"
	"testing"
)

func TestEffectiveFlags(t *testing.T) {
	tests := []struct {
		name      string
		overrides *RuntimeOverrides
		stepID    string
		baseline  []string
		want      []string
	}{
		{
			name:      "No overrides (nil)",
			overrides: nil,
			stepID:    "01",
			baseline:  []string{"--json"},
			want:      []string{"--json"},
		},
		{
			name: "Step not targeted",
			overrides: &RuntimeOverrides{
				ApplyToSteps: map[string]bool{"02": true},
				AddedFlags:   []string{"--verbose"},
			},
			stepID:   "01",
			baseline: []string{"--json"},
			want:     []string{"--json"},
		},
		{
			name: "Step targeted: Add flags",
			overrides: &RuntimeOverrides{
				ApplyToSteps: map[string]bool{"01": true},
				AddedFlags:   []string{"--verbose"},
			},
			stepID:   "01",
			baseline: []string{"--json"},
			want:     []string{"--json", "--verbose"},
		},
		{
			name: "Step targeted: Remove flags",
			overrides: &RuntimeOverrides{
				ApplyToSteps: map[string]bool{"01": true},
				RemovedFlags: []string{"--json"},
			},
			stepID:   "01",
			baseline: []string{"--json", "--other"},
			want:     []string{"--other"},
		},
		{
			name: "Step targeted: Add and Remove",
			overrides: &RuntimeOverrides{
				ApplyToSteps: map[string]bool{"01": true},
				AddedFlags:   []string{"--new"},
				RemovedFlags: []string{"--old"},
			},
			stepID:   "01",
			baseline: []string{"--old", "--keep"},
			want:     []string{"--keep", "--new"},
		},
		{
			name: "Step targeted: Inject ChatGPT URL",
			overrides: &RuntimeOverrides{
				ApplyToSteps: map[string]bool{"01": true},
				ChatGPTURL:   "https://chat.openai.com/share/123",
			},
			stepID:   "01",
			baseline: []string{"--json"},
			want:     []string{"--json", "--chatgpt-url", "https://chat.openai.com/share/123"},
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			got := tt.overrides.EffectiveFlags(tt.stepID, tt.baseline)
			if !reflect.DeepEqual(got, tt.want) {
				t.Errorf("EffectiveFlags() = %v, want %v", got, tt.want)
			}
		})
	}
}
```

internal/overrides/types.go
```
package overrides

// RuntimeOverrides holds configuration for runtime flag modifications.
type RuntimeOverrides struct {
	AddedFlags   []string        // Flags to append (e.g., "--model=gpt-4")
	RemovedFlags []string        // Flags to remove (e.g., "--json")
	ChatGPTURL   string          // Optional URL to inject via --chatgpt-url
	ApplyToSteps map[string]bool // Set of step IDs to apply overrides to. If empty, applies to none.
}
```

internal/pack/bash_syntax_validator.go
```
package pack

import (
	"regexp"
	"strings"

	"github.com/user/oraclepack/internal/types"
)

var orphanFlagRegex = regexp.MustCompile(`^\s*(?:-p|--prompt)(?:\s+.+)?$`)

// FindOrphanedFlags detects lines that contain only flags like -p/--prompt
// without being part of a continued command.
func FindOrphanedFlags(script string) []types.SyntaxFinding {
	if script == "" {
		return nil
	}

	lines := strings.Split(script, "\n")
	var findings []types.SyntaxFinding
	for i, line := range lines {
		trimmed := strings.TrimSpace(line)
		if trimmed == "" || strings.HasPrefix(trimmed, "#") {
			continue
		}
		if !orphanFlagRegex.MatchString(line) {
			continue
		}

		if i > 0 && lineContinues(lines[i-1]) {
			continue
		}

		findings = append(findings, types.SyntaxFinding{
			Line:    i + 1,
			Token:   strings.Fields(trimmed)[0],
			Message: "Orphaned flag without a preceding command or line continuation",
		})
	}
	return findings
}

func lineContinues(line string) bool {
	trimmed := strings.TrimRight(line, " \t")
	return strings.HasSuffix(trimmed, "\\")
}

// CheckPackScripts validates prelude and step scripts for orphaned flags and bash syntax.
func CheckPackScripts(p *types.Pack) ([]types.SyntaxFinding, string, error) {
	if p == nil {
		return nil, "", nil
	}

	var findings []types.SyntaxFinding
	var warnings []string

	// Prelude orphaned flags
	for _, finding := range FindOrphanedFlags(p.Prelude.Code) {
		findings = append(findings, finding)
	}

	// Step orphaned flags and bash -n checks
	for _, step := range p.Steps {
		for _, finding := range FindOrphanedFlags(step.Code) {
			finding.StepID = step.ID
			findings = append(findings, finding)
		}

		syntaxFindings, warning, err := CheckBashSyntax(step.Code)
		if err != nil {
			return findings, warning, err
		}
		if warning != "" {
			warnings = append(warnings, warning)
		}
		for _, finding := range syntaxFindings {
			finding.StepID = step.ID
			findings = append(findings, finding)
		}
	}

	// Prelude bash -n check
	preludeFindings, warning, err := CheckBashSyntax(p.Prelude.Code)
	if err != nil {
		return findings, warning, err
	}
	if warning != "" {
		warnings = append(warnings, warning)
	}
	findings = append(findings, preludeFindings...)

	return findings, strings.Join(uniqueStrings(warnings), "; "), nil
}

func uniqueStrings(values []string) []string {
	if len(values) == 0 {
		return nil
	}
	seen := make(map[string]bool, len(values))
	var out []string
	for _, value := range values {
		if value == "" || seen[value] {
			continue
		}
		seen[value] = true
		out = append(out, value)
	}
	return out
}
```

internal/pack/bash_syntax_validator_test.go
```
package pack

import (
	"os/exec"
	"testing"

	"github.com/user/oraclepack/internal/types"
)

func TestFindOrphanedFlags(t *testing.T) {
	script := "-p \"hello\"\n"
	findings := FindOrphanedFlags(script)
	if len(findings) != 1 {
		t.Fatalf("expected 1 finding, got %d", len(findings))
	}
	if findings[0].Line != 1 {
		t.Fatalf("expected line 1, got %d", findings[0].Line)
	}

	script = "oracle \\\n  -p \"ok\"\n"
	findings = FindOrphanedFlags(script)
	if len(findings) != 0 {
		t.Fatalf("expected no findings for continued line, got %d", len(findings))
	}
}

func TestCheckBashSyntax(t *testing.T) {
	if _, err := exec.LookPath("bash"); err != nil {
		t.Skip("bash not available")
	}

	script := "if true\n  echo hello\n"
	findings, warning, err := CheckBashSyntax(script)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if warning != "" {
		t.Fatalf("expected no warning, got %s", warning)
	}
	if len(findings) == 0 {
		t.Fatal("expected syntax findings")
	}
	if findings[0].Line == 0 {
		t.Fatal("expected line number in syntax finding")
	}
}

func TestCheckPackScriptsReportsStepID(t *testing.T) {
	p := &types.Pack{
		Steps: []types.Step{
			{ID: "01", Code: "-p \"oops\""},
		},
	}
	findings, _, err := CheckPackScripts(p)
	if err != nil {
		t.Fatalf("unexpected error: %v", err)
	}
	if len(findings) != 1 {
		t.Fatalf("expected 1 finding, got %d", len(findings))
	}
	if findings[0].StepID != "01" {
		t.Fatalf("expected step ID 01, got %q", findings[0].StepID)
	}
}
```

internal/pack/bash_tooling_checks.go
```
package pack

import (
	"bytes"
	"os/exec"
	"regexp"
	"strconv"
	"strings"

	"github.com/user/oraclepack/internal/types"
)

var bashLineRegex = regexp.MustCompile(`line\s+(\d+)`)

// CheckBashSyntax runs "bash -n" on the script and returns syntax findings.
// If bash is not found, it returns a warning and no findings.
func CheckBashSyntax(script string) ([]types.SyntaxFinding, string, error) {
	if strings.TrimSpace(script) == "" {
		return nil, "", nil
	}

	if _, err := exec.LookPath("bash"); err != nil {
		return nil, "bash not found on PATH; skipping bash -n syntax check", nil
	}

	cmd := exec.Command("bash", "-n")
	cmd.Stdin = strings.NewReader(script)

	var stderr bytes.Buffer
	cmd.Stderr = &stderr
	cmd.Stdout = &stderr

	if err := cmd.Run(); err != nil {
		msg := strings.TrimSpace(stderr.String())
		line := 0
		if match := bashLineRegex.FindStringSubmatch(msg); len(match) > 1 {
			if parsed, parseErr := strconv.Atoi(match[1]); parseErr == nil {
				line = parsed
			}
		}
		return []types.SyntaxFinding{
			{
				Line:    line,
				Message: msg,
			},
		}, "", nil
	}

	return nil, "", nil
}
```

internal/pack/metadata.go
```
package pack

import (
	"fmt"

	"github.com/user/oraclepack/internal/errors"
	"github.com/user/oraclepack/internal/types"
)

// DeriveMetadata extracts configuration from the prelude.
func DeriveMetadata(p *types.Pack) {
	if p == nil {
		return
	}
	outDirMatch := outDirRegex.FindStringSubmatch(p.Prelude.Code)
	if len(outDirMatch) > 1 {
		p.OutDir = outDirMatch[1]
	}

	if writeOutputRegex.MatchString(p.Prelude.Code) {
		p.WriteOutput = true
	}
}

// Validate checks if the pack follows all rules.
func Validate(p *types.Pack) error {
	if p == nil {
		return fmt.Errorf("%w: pack is nil", errors.ErrInvalidPack)
	}
	if len(p.Steps) == 0 {
		return fmt.Errorf("%w: at least one step is required", errors.ErrInvalidPack)
	}
	if len(p.Steps) != 20 {
		return fmt.Errorf("%w: expected exactly 20 steps, got %d", errors.ErrInvalidPack, len(p.Steps))
	}

	seen := make(map[int]bool)
	for i, step := range p.Steps {
		if step.Number <= 0 {
			return fmt.Errorf("%w: invalid step number %d", errors.ErrInvalidPack, step.Number)
		}
		if seen[step.Number] {
			return fmt.Errorf("%w: duplicate step number %d", errors.ErrInvalidPack, step.Number)
		}
		seen[step.Number] = true

		// Ensure sequential starting from 1
		if step.Number != i+1 {
			return fmt.Errorf("%w: steps must be sequential starting from 1 (expected %d, got %d)", errors.ErrInvalidPack, i+1, step.Number)
		}
	}

	return nil
}
```

internal/pack/output_expectations.go
```
package pack

import (
	"regexp"
	"strings"

	"github.com/user/oraclepack/internal/types"
)

var (
	writeOutputPathRegex = regexp.MustCompile(`(?m)--write-output\s+["']?([^"'\s]+)["']?`)
	answerFormatRegex    = regexp.MustCompile(`(?i)answer\s+format`)
	directOnlyRegex      = regexp.MustCompile(`(?i)return\s+only[:\s]*direct\s+answer`)
)

// DetectOutputContract determines the expected response contract for a step.
func DetectOutputContract(step types.Step) types.OutputContract {
	if step.Code == "" {
		return types.OutputContractUnknown
	}

	hasAnswerFormat := answerFormatRegex.MatchString(step.Code)
	if !hasAnswerFormat {
		return types.OutputContractUnknown
	}

	if directOnlyRegex.MatchString(step.Code) {
		return types.OutputContractDirectAnswerOnly
	}

	return types.OutputContractAllSections
}

// StepOutputExpectations returns a map of output paths to required tokens.
// If no validation is needed, it returns nil.
func StepOutputExpectations(step *types.Step) map[string][]string {
	if step == nil {
		return nil
	}
	paths := ExtractWriteOutputPaths(step.Code)
	if len(paths) == 0 {
		return nil
	}

	if len(paths) > 1 {
		out := map[string][]string{}
		for _, path := range paths {
			switch {
			case strings.Contains(path, "-direct-answer"):
				out[path] = []string{"### Direct answer"}
			case strings.Contains(path, "-risks-unknowns"):
				out[path] = []string{"### Risks and unknowns"}
			case strings.Contains(path, "-next-experiment"):
				out[path] = []string{"### Next experiment"}
			case strings.Contains(path, "-missing-evidence"):
				out[path] = []string{"### Missing evidence"}
			}
		}
		if len(out) == 0 {
			return nil
		}
		return out
	}

	switch DetectOutputContract(*step) {
	case types.OutputContractDirectAnswerOnly:
		return map[string][]string{paths[0]: []string{"### Direct answer"}}
	case types.OutputContractAllSections:
		return map[string][]string{paths[0]: {
			"### Direct answer",
			"### Risks and unknowns",
			"### Next experiment",
			"### Missing evidence",
		}}
	default:
		return nil
	}
}

// StepOutputExpectationsWithMode returns expectations honoring chunk mode.
// chunkMode: auto (default), single (treat as single output), multi (force suffix mapping when multiple outputs).
func StepOutputExpectationsWithMode(step *types.Step, chunkMode string) map[string][]string {
	if step == nil {
		return nil
	}
	paths := ExtractWriteOutputPaths(step.Code)
	if len(paths) == 0 {
		return nil
	}
	mode := strings.ToLower(strings.TrimSpace(chunkMode))
	if mode == "" {
		mode = "auto"
	}

	switch mode {
	case "single":
		// Always treat as a single output (use first path).
		return expectationsForSingle(step, paths[0])
	case "multi":
		if len(paths) > 1 {
			return expectationsForSuffixes(paths)
		}
		return expectationsForSingle(step, paths[0])
	default: // auto
		if len(paths) > 1 {
			return expectationsForSuffixes(paths)
		}
		return expectationsForSingle(step, paths[0])
	}
}

func expectationsForSuffixes(paths []string) map[string][]string {
	out := map[string][]string{}
	for _, path := range paths {
		switch {
		case strings.Contains(path, "-direct-answer"):
			out[path] = []string{"### Direct answer"}
		case strings.Contains(path, "-risks-unknowns"):
			out[path] = []string{"### Risks and unknowns"}
		case strings.Contains(path, "-next-experiment"):
			out[path] = []string{"### Next experiment"}
		case strings.Contains(path, "-missing-evidence"):
			out[path] = []string{"### Missing evidence"}
		}
	}
	if len(out) == 0 {
		return nil
	}
	return out
}

func expectationsForSingle(step *types.Step, path string) map[string][]string {
	switch DetectOutputContract(*step) {
	case types.OutputContractDirectAnswerOnly:
		return map[string][]string{path: []string{"### Direct answer"}}
	case types.OutputContractAllSections:
		return map[string][]string{path: {
			"### Direct answer",
			"### Risks and unknowns",
			"### Next experiment",
			"### Missing evidence",
		}}
	default:
		return nil
	}
}

// ExtractWriteOutputPaths returns all --write-output paths found in the step code.
func ExtractWriteOutputPaths(code string) []string {
	matches := writeOutputPathRegex.FindAllStringSubmatch(code, -1)
[TRUNCATED]
```

internal/pack/output_expectations_test.go
```
package pack

import (
	"reflect"
	"testing"

	"github.com/user/oraclepack/internal/types"
)

func TestDetectOutputContract(t *testing.T) {
	step := types.Step{
		Code: "Answer format:\nReturn only: Direct answer\n",
	}
	if got := DetectOutputContract(step); got != types.OutputContractDirectAnswerOnly {
		t.Fatalf("expected direct-answer-only, got %q", got)
	}

	step = types.Step{
		Code: "Answer format:\n### Direct answer\n### Risks and unknowns\n",
	}
	if got := DetectOutputContract(step); got != types.OutputContractAllSections {
		t.Fatalf("expected all-sections, got %q", got)
	}

	step = types.Step{Code: "no format here"}
	if got := DetectOutputContract(step); got != types.OutputContractUnknown {
		t.Fatalf("expected unknown, got %q", got)
	}
}

func TestStepOutputExpectations_SingleOutput(t *testing.T) {
	step := &types.Step{
		Code: `oracle -p "x" --write-output "out.txt"
Answer format:
Return only: Direct answer`,
	}
	expectations := StepOutputExpectations(step)
	want := map[string][]string{"out.txt": []string{"### Direct answer"}}
	if !reflect.DeepEqual(expectations, want) {
		t.Fatalf("unexpected expectations: %#v", expectations)
	}

	step = &types.Step{
		Code: `oracle -p "x" --write-output "out.txt"
Answer format:
### Direct answer
### Risks and unknowns
### Next experiment
### Missing evidence`,
	}
	expectations = StepOutputExpectations(step)
	want = map[string][]string{"out.txt": []string{
		"### Direct answer",
		"### Risks and unknowns",
		"### Next experiment",
		"### Missing evidence",
	}}
	if !reflect.DeepEqual(expectations, want) {
		t.Fatalf("unexpected expectations: %#v", expectations)
	}
}

func TestStepOutputExpectations_MultiOutput(t *testing.T) {
	step := &types.Step{
		Code: `oracle --write-output "out-direct-answer.md" \
  --write-output 'out-risks-unknowns.md' \
  --write-output out-next-experiment.md \
  --write-output out-missing-evidence.md`,
	}
	expectations := StepOutputExpectations(step)
	if len(expectations) != 4 {
		t.Fatalf("expected 4 expectations, got %d", len(expectations))
	}
	if got := expectations["out-direct-answer.md"]; len(got) != 1 || got[0] != "### Direct answer" {
		t.Fatalf("unexpected direct-answer tokens: %#v", got)
	}
	if got := expectations["out-risks-unknowns.md"]; len(got) != 1 || got[0] != "### Risks and unknowns" {
		t.Fatalf("unexpected risks-unknowns tokens: %#v", got)
	}
	if got := expectations["out-next-experiment.md"]; len(got) != 1 || got[0] != "### Next experiment" {
		t.Fatalf("unexpected next-experiment tokens: %#v", got)
	}
	if got := expectations["out-missing-evidence.md"]; len(got) != 1 || got[0] != "### Missing evidence" {
		t.Fatalf("unexpected missing-evidence tokens: %#v", got)
	}
}
```

internal/pack/output_validator.go
```
package pack

import (
	"fmt"
	"os"
	"regexp"
	"strings"

	"github.com/user/oraclepack/internal/types"
)

// ValidateOutputFile checks whether the output file contains the required answer sections.
// It returns ok=false with a populated OutputFailure when validation fails.
func ValidateOutputFile(path string, requiredTokens []string) (bool, types.OutputFailure) {
	data, err := os.ReadFile(path)
	if err != nil {
		return false, types.OutputFailure{
			Path:  path,
			Error: err.Error(),
		}
	}

	content := string(data)
	var missing []string
	for _, tok := range requiredTokens {
		if !containsToken(content, tok) {
			missing = append(missing, tok)
		}
	}

	if len(missing) > 0 {
		return false, types.OutputFailure{
			Path:          path,
			MissingTokens: missing,
		}
	}

	return true, types.OutputFailure{}
}

func containsToken(content, token string) bool {
	if strings.Contains(content, token) {
		return true
	}

	heading := strings.TrimSpace(strings.TrimPrefix(token, "###"))
	if heading == token {
		return false
	}

	alts := []string{heading}
	switch strings.ToLower(heading) {
	case "direct answer":
		alts = append(alts, "answer")
	case "risks and unknowns":
		alts = append(alts, "risks/unknowns", "risks & unknowns")
	case "next experiment":
		alts = append(alts, "next smallest concrete experiment")
	case "missing evidence":
		alts = append(alts, "if evidence is insufficient")
	}

	for _, alt := range alts {
		if alt == "" {
			continue
		}
		pat := `(?im)^\s*#{0,3}\s*` + regexp.QuoteMeta(alt) + `\b`
		if regexp.MustCompile(pat).MatchString(content) {
			return true
		}
	}

	return false
}

// VerifyStepOutputs validates output files for a step. If requireHeadings is false,
// it only checks that output files exist and are non-empty.
func VerifyStepOutputs(step *types.Step, requireHeadings bool, chunkMode string) []types.OutputFailure {
	if step == nil {
		return nil
	}
	paths := ExtractWriteOutputPaths(step.Code)
	if len(paths) == 0 {
		return nil
	}

	if !requireHeadings {
		paths = selectChunkPaths(paths, chunkMode)
		var failures []types.OutputFailure
		for _, path := range paths {
			info, err := os.Stat(path)
			if err != nil {
				failures = append(failures, types.OutputFailure{
					Path:  path,
					Error: err.Error(),
				})
				continue
			}
			if info.Size() == 0 {
				failures = append(failures, types.OutputFailure{
					Path:  path,
					Error: fmt.Sprintf("output file is empty: %s", path),
				})
			}
		}
		return failures
	}

	expectations := StepOutputExpectationsWithMode(step, chunkMode)
	if len(expectations) == 0 {
		return nil
	}
	var failures []types.OutputFailure
	for path, required := range expectations {
		ok, failure := ValidateOutputFile(path, required)
		if !ok {
			failures = append(failures, failure)
		}
	}
	return failures
}

func selectChunkPaths(paths []string, chunkMode string) []string {
	if len(paths) == 0 {
		return paths
	}
	mode := strings.ToLower(strings.TrimSpace(chunkMode))
	if mode == "" {
		mode = "auto"
	}
	switch mode {
	case "single":
		return []string{paths[0]}
	default:
		return paths
	}
}
```

internal/pack/output_validator_test.go
```
package pack

import (
	"os"
	"path/filepath"
	"testing"

	"github.com/user/oraclepack/internal/types"
)

func TestValidateOutputFile_RelaxedHeadings(t *testing.T) {
	content := `Direct answer

Risks/unknowns

Next smallest concrete experiment

If evidence is insufficient`

	dir := t.TempDir()
	path := filepath.Join(dir, "out.md")
	if err := os.WriteFile(path, []byte(content), 0o644); err != nil {
		t.Fatalf("write output: %v", err)
	}

	ok, failure := ValidateOutputFile(path, []string{
		"### Direct answer",
		"### Risks and unknowns",
		"### Next experiment",
		"### Missing evidence",
	})
	if !ok {
		t.Fatalf("expected relaxed headings to pass, failure: %#v", failure)
	}

	step := &types.Step{Code: `oracle --write-output "` + path + `"`}
	failures := VerifyStepOutputs(step, true, "single")
	if len(failures) != 0 {
		t.Fatalf("expected no failures in single chunk mode, got %#v", failures)
	}
}
```

internal/pack/parser.go
```
package pack

import (
	"bufio"
	"fmt"
	"regexp"
	"strconv"
	"strings"

	"github.com/user/oraclepack/internal/errors"
	"github.com/user/oraclepack/internal/types"
)

var (
	bashFenceRegex = regexp.MustCompile("(?s)```bash\n(.*?)\n```")
	// Updated regex to support ")", " —", and " -" separators
	stepHeaderRegex  = regexp.MustCompile(`^#\s*(\d{2})(?:\)|[\s]+[—-])`)
	roiRegex         = regexp.MustCompile(`ROI=(\d+(\.\d+)?)`)
	impactRegex      = regexp.MustCompile(`^#\s*Impact:\s*(.+)$`)
	outDirRegex      = regexp.MustCompile(`(?m)^out_dir=["']?([^"'\s]+)["']?`)
	writeOutputRegex = regexp.MustCompile(`(?m)--write-output`)
)

// Parse reads a Markdown content and returns a Pack.
func Parse(content []byte) (*types.Pack, error) {
	match := bashFenceRegex.FindSubmatch(content)
	if match == nil || len(match) < 2 {
		return nil, fmt.Errorf("%w: no bash code block found", errors.ErrInvalidPack)
	}

	bashCode := string(match[1])
	pack := &types.Pack{}

	scanner := bufio.NewScanner(strings.NewReader(bashCode))
	var currentStep *types.Step
	var preludeLines []string
	var inSteps bool

	for scanner.Scan() {
		line := scanner.Text()
		headerMatch := stepHeaderRegex.FindStringSubmatch(strings.TrimSpace(line))

		if len(headerMatch) > 1 {
			inSteps = true
			if currentStep != nil {
				applyStepMetadata(currentStep)
				pack.Steps = append(pack.Steps, *currentStep)
			}
			num, _ := strconv.Atoi(headerMatch[1])

			// Extract ROI if present
			var roi float64
			cleanedLine := line
			roiMatch := roiRegex.FindStringSubmatch(line)
			if len(roiMatch) > 1 {
				val, err := strconv.ParseFloat(roiMatch[1], 64)
				if err == nil {
					roi = val
					// Remove ROI tag from display title, but keep original line intact?
					// The task says "strip from Step.Title". Step struct currently has `OriginalLine`.
					// I'll assume OriginalLine is what is displayed, or I should add a Title field.
					// Looking at Step struct: ID, Number, Code, OriginalLine.
					// I'll remove it from OriginalLine for now or add a Title field.
					// The existing TUI uses OriginalLine as description.
					// Let's clean OriginalLine for display purposes or add a dedicated Title field.
					// Adding a dedicated Title field seems cleaner but requires struct change.
					// For now, I'll strip it from OriginalLine to match the prompt requirement "cleaner UI display".
					cleanedLine = strings.Replace(cleanedLine, roiMatch[0], "", 1)
					cleanedLine = strings.TrimSpace(cleanedLine)
					// Fix any double spaces or trailing separators if needed, but simple replace is a good start.
				}
			}

			currentStep = &types.Step{
				ID:           headerMatch[1],
				Number:       num,
				OriginalLine: cleanedLine,
				ROI:          roi,
			}
			continue
		}

		if inSteps {
			currentStep.Code += line + "\n"
		} else {
			preludeLines = append(preludeLines, line)
		}
	}

	if currentStep != nil {
		applyStepMetadata(currentStep)
		pack.Steps = append(pack.Steps, *currentStep)
	}

	pack.Prelude.Code = strings.Join(preludeLines, "\n")
	DeriveMetadata(pack)

	return pack, nil
}

func applyStepMetadata(step *types.Step) {
	if step == nil {
		return
	}
	lines := strings.Split(step.Code, "\n")
	for _, line := range lines {
		trimmed := strings.TrimSpace(line)
		if trimmed == "" || !strings.HasPrefix(trimmed, "#") {
			continue
		}
		if step.ROI == 0 {
			if strings.HasPrefix(trimmed, "# ROI:") {
				val := strings.TrimSpace(strings.TrimPrefix(trimmed, "# ROI:"))
				if parsed, err := strconv.ParseFloat(val, 64); err == nil {
					step.ROI = parsed
				}
			}
		}
		if step.Impact == "" {
[TRUNCATED]
```

internal/pack/parser_test.go
```
package pack

import (
	"strconv"
	"strings"
	"testing"

	"github.com/user/oraclepack/internal/types"
)

func TestParse(t *testing.T) {
	steps := buildSteps(20, "echo")
	content := []byte(`
# My Pack
Some description.

` + "```" + `bash
out_dir="dist"
--write-output

` + steps + `
` + "```" + `
`)

	p, err := Parse(content)
	if err != nil {
		t.Fatalf("Parse failed: %v", err)
	}

	if p.OutDir != "dist" {
		t.Errorf("expected OutDir dist, got %s", p.OutDir)
	}

	if !p.WriteOutput {
		t.Errorf("expected WriteOutput true, got false")
	}

	if len(p.Steps) != 20 {
		t.Errorf("expected 20 steps, got %d", len(p.Steps))
	}

	if p.Steps[0].ID != "01" || p.Steps[0].Number != 1 {
		t.Errorf("step 1 mismatch: %+v", p.Steps[0])
	}

	if err := Validate(p); err != nil {
		t.Errorf("Validate failed: %v", err)
	}
}

func TestParseVariants(t *testing.T) {
	tests := []struct {
		name    string
		content string
	}{
		{
			"em dash",
			`
` + "```" + `bash
# 01 — ROI=...
echo "step 1"
` + "```" + `
`,
		},
		{
			"hyphen",
			`
` + "```" + `bash
# 01 - ROI=...
echo "step 1"
` + "```" + `
`,
		},
	}

	for _, tt := range tests {
		t.Run(tt.name, func(t *testing.T) {
			p, err := Parse([]byte(tt.content))
			if err != nil {
				t.Fatalf("Parse failed: %v", err)
			}
			if len(p.Steps) != 1 {
				t.Errorf("expected 1 step, got %d", len(p.Steps))
			}
		})
	}
}

func TestParseROI(t *testing.T) {
	content := []byte(`
` + "```" + `bash
# 01) ROI=4.5 clean me
echo "high value"

# 02) ROI=0.5
echo "low value"

# 03) No ROI
echo "default"
` + "```" + `
`)

	p, err := Parse(content)
	if err != nil {
		t.Fatalf("Parse failed: %v", err)
	}

	if len(p.Steps) != 3 {
		t.Fatalf("expected 3 steps, got %d", len(p.Steps))
	}

	if p.Steps[0].ROI != 4.5 {
		t.Errorf("step 1 ROI mismatch: expected 4.5, got %f", p.Steps[0].ROI)
	}
	if strings.Contains(p.Steps[0].OriginalLine, "ROI=4.5") {
		t.Errorf("step 1 title was not cleaned: %q", p.Steps[0].OriginalLine)
	}

	if p.Steps[1].ROI != 0.5 {
		t.Errorf("step 2 ROI mismatch: expected 0.5, got %f", p.Steps[1].ROI)
	}

	if p.Steps[2].ROI != 0.0 {
		t.Errorf("step 3 ROI mismatch: expected 0.0, got %f", p.Steps[2].ROI)
	}
}

func TestValidateErrors(t *testing.T) {
	base := buildStepSlice(20)
	tests := []struct {
		name    string
		pack    *types.Pack
		wantErr string
	}{
		{
			"no steps",
			&types.Pack{},
			"at least one step is required",
		},
		{
			"duplicate steps",
			&types.Pack{
				Steps: func() []types.Step {
					steps := append([]types.Step(nil), base...)
					steps[1].Number = steps[0].Number
					steps[1].ID = steps[0].ID
					return steps
				}(),
			},
			"duplicate step number 1",
		},
		{
			"wrong step count",
			&types.Pack{
				Steps: []types.Step{
					{Number: 1, ID: "01"},
				},
			},
			"expected exactly 20 steps",
		},
		{
			"non-sequential",
			&types.Pack{
				Steps: func() []types.Step {
					steps := append([]types.Step(nil), base...)
[TRUNCATED]
```

internal/pack/verify_report.go
```
package pack

import (
	"fmt"
	"strings"

	"github.com/user/oraclepack/internal/types"
)

// VerifyReport captures output verification results across a pack.
type VerifyReport struct {
	TotalSteps   int
	CheckedSteps int
	Failures     []types.OutputFailure
}

// FormatVerifyReport renders a human-readable report.
func FormatVerifyReport(report VerifyReport) string {
	var b strings.Builder
	fmt.Fprintf(&b, "Verified outputs for %d/%d steps\n", report.CheckedSteps, report.TotalSteps)
	if len(report.Failures) == 0 {
		b.WriteString("All required output tokens were found.\n")
		return b.String()
	}

	b.WriteString("Missing or invalid outputs:\n")
	for _, failure := range report.Failures {
		stepLabel := failure.StepID
		if stepLabel == "" {
			stepLabel = "unknown step"
		}
		fmt.Fprintf(&b, "- Step %s: %s", stepLabel, failure.Path)
		if failure.Error != "" {
			fmt.Fprintf(&b, " (error: %s)", failure.Error)
		} else if len(failure.MissingTokens) > 0 {
			fmt.Fprintf(&b, " missing %s", strings.Join(failure.MissingTokens, ", "))
		}
		b.WriteString("\n")
	}
	return b.String()
}
```

internal/render/render.go
```
package render

import (
	"sync"

	"github.com/charmbracelet/glamour"
	"github.com/user/oraclepack/internal/types"
)

const (
	DefaultStyle = "dark"
	DefaultWidth = 80
)

type rendererKey struct {
	width int
	style string
}

var (
	rendererMu    sync.Mutex
	rendererCache = map[rendererKey]*glamour.TermRenderer{}
)

// RenderMarkdown renders markdown text as ANSI-styled text.
func RenderMarkdown(text string, width int, style string) (string, error) {
	if width <= 0 {
		width = DefaultWidth
	}
	if style == "" {
		style = DefaultStyle
	}

	r, err := rendererFor(width, style)
	if err != nil {
		return "", err
	}

	return r.Render(text)
}

// RenderStepCode renders a step's code block for preview.
func RenderStepCode(s types.Step, width int, style string) (string, error) {
	md := "```bash\n" + s.Code + "\n```"
	return RenderMarkdown(md, width, style)
}

func rendererFor(width int, style string) (*glamour.TermRenderer, error) {
	key := rendererKey{width: width, style: style}

	rendererMu.Lock()
	r := rendererCache[key]
	rendererMu.Unlock()
	if r != nil {
		return r, nil
	}

	opts := []glamour.TermRendererOption{glamour.WithWordWrap(width)}
	if style == "auto" {
		opts = append(opts, glamour.WithAutoStyle())
	} else {
		opts = append(opts, glamour.WithStandardStyle(style))
	}

	r, err := glamour.NewTermRenderer(opts...)
	if err != nil {
		return nil, err
	}

	rendererMu.Lock()
	rendererCache[key] = r
	rendererMu.Unlock()
	return r, nil
}
```

internal/render/render_test.go
```
package render

import (
	"strings"
	"testing"
)

func TestRenderMarkdown(t *testing.T) {
	text := "# Hello\n**bold**"
	got, err := RenderMarkdown(text, 40, DefaultStyle)
	if err != nil {
		t.Fatalf("RenderMarkdown failed: %v", err)
	}

	// ANSI escape codes start with \x1b[
	if !strings.Contains(got, "\x1b[") {
		t.Errorf("expected ANSI codes in output, got: %q", got)
	}
}
```

internal/report/generate.go
```
package report

import (
	"time"

	"github.com/user/oraclepack/internal/state"
)

// GenerateReport creates a ReportV1 from a RunState.
func GenerateReport(s *state.RunState, packName string) *ReportV1 {
	report := &ReportV1{
		PackInfo: PackInfo{
			Name: packName,
			Hash: s.PackHash,
		},
		GeneratedAt: time.Now(),
		Steps:       []StepReport{},
	}

	var totalDuration time.Duration
	success, failure, skipped := 0, 0, 0

	for id, status := range s.StepStatuses {
		duration := status.EndedAt.Sub(status.StartedAt)
		if status.EndedAt.IsZero() || status.StartedAt.IsZero() {
			duration = 0
		}

		totalDuration += duration

		sr := StepReport{
			ID:         id,
			Status:     string(status.Status),
			ExitCode:   status.ExitCode,
			Duration:   duration,
			DurationMs: duration.Milliseconds(),
			Error:      status.Error,
		}
		report.Steps = append(report.Steps, sr)

		switch status.Status {
		case state.StatusSuccess:
			success++
		case state.StatusFailed:
			failure++
		case state.StatusSkipped:
			skipped++
		}
	}

	report.Summary = Summary{
		TotalSteps:      len(s.StepStatuses),
		SuccessCount:    success,
		FailureCount:    failure,
		SkippedCount:    skipped,
		TotalDuration:   totalDuration,
		TotalDurationMs: totalDuration.Milliseconds(),
	}

	if len(s.Warnings) > 0 {
		report.Warnings = make([]Warning, 0, len(s.Warnings))
		for _, w := range s.Warnings {
			report.Warnings = append(report.Warnings, Warning{
				Scope:   w.Scope,
				StepID:  w.StepID,
				Line:    w.Line,
				Token:   w.Token,
				Message: w.Message,
			})
		}
	}

	return report
}
```

internal/report/io.go
```
package report

import (
	"encoding/json"
	"fmt"
	"os"
)

// WriteReport writes a ReportV1 to disk.
func WriteReport(path string, rep *ReportV1) error {
	data, err := json.MarshalIndent(rep, "", "  ")
	if err != nil {
		return fmt.Errorf("marshal report: %w", err)
	}
	if err := os.WriteFile(path, data, 0644); err != nil {
		return fmt.Errorf("write report: %w", err)
	}
	return nil
}
```

internal/report/io_test.go
```
package report

import (
	"os"
	"path/filepath"
	"testing"
)

func TestWriteReport(t *testing.T) {
	dir := t.TempDir()
	path := filepath.Join(dir, "report.json")

	rep := &ReportV1{
		PackInfo: PackInfo{Name: "pack"},
		Summary:  Summary{TotalSteps: 1},
	}
	if err := WriteReport(path, rep); err != nil {
		t.Fatalf("WriteReport: %v", err)
	}

	if _, err := os.Stat(path); err != nil {
		t.Fatalf("expected report file to exist: %v", err)
	}
}
```

internal/report/report_test.go
```
package report

import (
	"testing"
	"time"

	"github.com/user/oraclepack/internal/state"
)

func TestGenerateReport(t *testing.T) {
	s := &state.RunState{
		PackHash: "hash123",
		StepStatuses: map[string]state.StepStatus{
			"01": {
				Status:    state.StatusSuccess,
				StartedAt: time.Now().Add(-1 * time.Second),
				EndedAt:   time.Now(),
			},
		},
	}

	rep := GenerateReport(s, "my-pack")

	if rep.PackInfo.Name != "my-pack" {
		t.Errorf("expected name my-pack, got %s", rep.PackInfo.Name)
	}

	if rep.Summary.TotalSteps != 1 {
		t.Errorf("expected 1 total step, got %d", rep.Summary.TotalSteps)
	}

	if rep.Summary.SuccessCount != 1 {
		t.Errorf("expected 1 success, got %d", rep.Summary.SuccessCount)
	}
}
```

internal/report/types.go
```
package report

import (
	"time"
)

// ReportV1 represents the final machine-readable summary.
type ReportV1 struct {
	Summary     Summary      `json:"summary"`
	PackInfo    PackInfo     `json:"pack_info"`
	Steps       []StepReport `json:"steps"`
	Warnings    []Warning    `json:"warnings,omitempty"`
	GeneratedAt time.Time    `json:"generated_at"`
}

type Summary struct {
	TotalSteps      int           `json:"total_steps"`
	SuccessCount    int           `json:"success_count"`
	FailureCount    int           `json:"failure_count"`
	SkippedCount    int           `json:"skipped_count"`
	TotalDuration   time.Duration `json:"total_duration"`
	TotalDurationMs int64         `json:"total_duration_ms"`
}

type PackInfo struct {
	Name string `json:"name"`
	Hash string `json:"hash"`
}

type StepReport struct {
	ID         string        `json:"id"`
	Status     string        `json:"status"`
	ExitCode   int           `json:"exit_code"`
	Duration   time.Duration `json:"duration"`
	DurationMs int64         `json:"duration_ms"`
	Error      string        `json:"error,omitempty"`
}

// Warning captures non-fatal execution notes surfaced during a run.
type Warning struct {
	Scope   string `json:"scope"`
	StepID  string `json:"step_id,omitempty"`
	Line    int    `json:"line"`
	Token   string `json:"token"`
	Message string `json:"message"`
}
```

internal/shell/detect.go
```
package shell

import "os/exec"

// DetectBinary checks PATH for a binary and returns its full path if found.
func DetectBinary(name string) (string, bool) {
	path, err := exec.LookPath(name)
	if err != nil {
		return "", false
	}
	return path, true
}
```

internal/shell/detect_test.go
```
package shell

import "testing"

func TestDetectBinary(t *testing.T) {
	if _, ok := DetectBinary("ls"); !ok {
		t.Fatalf("expected to find ls on PATH")
	}
	if _, ok := DetectBinary("definitely-not-a-real-binary-123"); ok {
		t.Fatalf("expected missing binary to return false")
	}
}
```

internal/shell/engine.go
```
package shell

import (
	"context"
	"fmt"
	"strings"
	"time"

	"github.com/user/oraclepack/internal/dispatch"
	"github.com/user/oraclepack/internal/state"
	"github.com/user/oraclepack/internal/tools"
	"github.com/user/oraclepack/internal/types"
)

// Engine executes pack steps in headless mode.
type Engine struct {
	Pack       *types.Pack
	State      *state.RunState
	StatePath  string
	StopOnFail bool
	Timeout    time.Duration
	Checker    tools.PresenceChecker
}

// Run executes all steps sequentially, updating state on disk.
func (e *Engine) Run(ctx context.Context) error {
	if e.Pack == nil {
		return nil
	}
	if e.State == nil {
		e.State = &state.RunState{
			SchemaVersion: 1,
			StepStatuses:  make(map[string]state.StepStatus),
		}
	}
	if e.State.StepStatuses == nil {
		e.State.StepStatuses = make(map[string]state.StepStatus)
	}

	for i := range e.Pack.Steps {
		step := e.Pack.Steps[i]
		e.State.CurrentStep = step.Number
		status := state.StepStatus{Status: state.StatusRunning, StartedAt: time.Now()}
		e.State.StepStatuses[step.ID] = status
		_ = state.WriteState(e.StatePath, e.State)

		kind := detectToolKind(&step)
		if shouldSkipForMissingTool(kind, e.Checker) {
			status.Status = state.StatusSkipped
			status.Error = "tool missing"
			status.EndedAt = time.Now()
			e.State.StepStatuses[step.ID] = status
			_ = state.WriteState(e.StatePath, e.State)
			continue
		}

		stepCtx := ctx
		if e.Timeout > 0 {
			var cancel context.CancelFunc
			stepCtx, cancel = context.WithTimeout(ctx, e.Timeout)
			defer cancel()
		}

		res, err := RunCommand(stepCtx, step.Code)
		if err == nil && res.ExitCode != 0 {
			err = fmt.Errorf("command failed with exit code %d", res.ExitCode)
		}
		status.EndedAt = time.Now()
		if err != nil {
			status.Status = state.StatusFailed
			status.Error = err.Error()
			e.State.StepStatuses[step.ID] = status
			_ = state.WriteState(e.StatePath, e.State)
			if e.StopOnFail {
				return err
			}
			continue
		}
		status.Status = state.StatusSuccess
		e.State.StepStatuses[step.ID] = status
		_ = state.WriteState(e.StatePath, e.State)
	}
	return nil
}

func detectToolKind(step *types.Step) tools.ToolKind {
	if step == nil {
		return tools.ToolUnknown
	}
	lines := strings.Split(step.Code, "\n")
	for _, line := range lines {
		trimmed := strings.TrimSpace(line)
		if trimmed == "" || strings.HasPrefix(trimmed, "#") {
			continue
		}
		if cls, ok := dispatch.Classify(trimmed); ok {
			return cls.Kind
		}
		break
	}
	return tools.ToolUnknown
}

func shouldSkipForMissingTool(kind tools.ToolKind, checker tools.PresenceChecker) bool {
	if kind == tools.ToolUnknown {
		return false
	}
	meta, ok := tools.Metadata(kind)
	if !ok {
		return false
	}
	if checker == nil {
		_, found := DetectBinary(meta.Name)
		return !found
	}
	_, found := checker.DetectBinary(meta.Name)
	return !found
}
```

internal/shell/engine_test.go
```
package shell

import (
	"context"
	"path/filepath"
	"testing"

	"github.com/user/oraclepack/internal/state"
	"github.com/user/oraclepack/internal/types"
)

type fakeChecker struct {
	found map[string]bool
}

func (f fakeChecker) DetectBinary(name string) (string, bool) {
	return name, f.found[name]
}

func TestEngineSkipsMissingTool(t *testing.T) {
	p := &types.Pack{
		Steps: []types.Step{
			{ID: "01", Number: 1, Code: "codex exec \"hi\""},
		},
	}
	dir := t.TempDir()
	engine := &Engine{
		Pack:      p,
		State:     &state.RunState{SchemaVersion: 1, StepStatuses: map[string]state.StepStatus{}},
		StatePath: filepath.Join(dir, "state.json"),
		Checker:   fakeChecker{found: map[string]bool{"codex": false}},
	}
	if err := engine.Run(context.Background()); err != nil {
		t.Fatalf("Run: %v", err)
	}
	if engine.State.StepStatuses["01"].Status != state.StatusSkipped {
		t.Fatalf("expected skipped, got %s", engine.State.StepStatuses["01"].Status)
	}
}

func TestEngineFailsOnError(t *testing.T) {
	p := &types.Pack{
		Steps: []types.Step{
			{ID: "01", Number: 1, Code: "exit 1"},
		},
	}
	engine := &Engine{
		Pack:       p,
		State:      &state.RunState{SchemaVersion: 1, StepStatuses: map[string]state.StepStatus{}},
		StopOnFail: true,
		Checker:    fakeChecker{found: map[string]bool{}},
	}
	if err := engine.Run(context.Background()); err == nil {
		t.Fatal("expected error, got nil")
	}
	if engine.State.StepStatuses["01"].Status != state.StatusFailed {
		t.Fatalf("expected failed, got %s", engine.State.StepStatuses["01"].Status)
	}
}
```

internal/shell/runner.go
```
package shell

import (
	"bytes"
	"context"
	"fmt"
	"os/exec"
)

// Result captures command output and exit code.
type Result struct {
	Stdout   string
	Stderr   string
	ExitCode int
}

// RunCommand executes a command with login shell semantics using /bin/bash -lc.
func RunCommand(ctx context.Context, cmd string) (Result, error) {
	c := exec.CommandContext(ctx, "/bin/bash", "-lc", cmd)
	var stdout bytes.Buffer
	var stderr bytes.Buffer
	c.Stdout = &stdout
	c.Stderr = &stderr

	err := c.Run()
	exitCode := 0
	if err != nil {
		if ee, ok := err.(*exec.ExitError); ok {
			exitCode = ee.ExitCode()
		} else {
			return Result{}, fmt.Errorf("run command: %w", err)
		}
	}

	return Result{
		Stdout:   stdout.String(),
		Stderr:   stderr.String(),
		ExitCode: exitCode,
	}, nil
}
```

internal/shell/runner_test.go
```
package shell

import (
	"context"
	"strings"
	"testing"
)

func TestRunCommandLoginShell(t *testing.T) {
	res, err := RunCommand(context.Background(), "echo $PATH")
	if err != nil {
		t.Fatalf("RunCommand: %v", err)
	}
	if strings.TrimSpace(res.Stdout) == "" {
		t.Fatalf("expected PATH output, got empty stdout")
	}
	if res.ExitCode != 0 {
		t.Fatalf("expected exit code 0, got %d", res.ExitCode)
	}
}
```

internal/state/io.go
```
package state

// Intentionally left without extra imports.

// WriteState writes RunState atomically to disk.
func WriteState(path string, state *RunState) error {
	return SaveStateAtomic(path, state)
}
```

internal/state/io_test.go
```
package state

import (
	"path/filepath"
	"testing"
	"time"
)

func TestWriteStateAndLoadState(t *testing.T) {
	dir := t.TempDir()
	path := filepath.Join(dir, "state.json")

	input := &RunState{
		SchemaVersion: 1,
		PackHash:      "hash",
		StartTime:     time.Now(),
		CurrentStep:   2,
		StepStatuses: map[string]StepStatus{
			"01": {Status: StatusSuccess},
		},
	}

	if err := WriteState(path, input); err != nil {
		t.Fatalf("WriteState: %v", err)
	}

	out, err := LoadState(path)
	if err != nil {
		t.Fatalf("LoadState: %v", err)
	}

	if out.CurrentStep != 2 {
		t.Fatalf("expected CurrentStep 2, got %d", out.CurrentStep)
	}
	if out.StepStatuses["01"].Status != StatusSuccess {
		t.Fatalf("expected status success, got %s", out.StepStatuses["01"].Status)
	}
}
```

internal/state/persist.go
```
package state

import (
	"encoding/json"
	"fmt"
	"os"
)

// SaveStateAtomic saves the state to a file atomically.
func SaveStateAtomic(path string, state *RunState) error {
	data, err := json.MarshalIndent(state, "", "  ")
	if err != nil {
		return fmt.Errorf("marshal state: %w", err)
	}

	tempPath := path + ".tmp"
	if err := os.WriteFile(tempPath, data, 0644); err != nil {
		return fmt.Errorf("write temp file: %w", err)
	}

	if err := os.Rename(tempPath, path); err != nil {
		os.Remove(tempPath)
		return fmt.Errorf("rename temp file: %w", err)
	}

	return nil
}

// LoadState loads the state from a file.
func LoadState(path string) (*RunState, error) {
	data, err := os.ReadFile(path)
	if err != nil {
		if os.IsNotExist(err) {
			return nil, fmt.Errorf("state file not found: %w", err)
		}
		return nil, fmt.Errorf("read state file: %w", err)
	}

	var state RunState
	if err := json.Unmarshal(data, &state); err != nil {
		return nil, fmt.Errorf("unmarshal state: %w", err)
	}

	return &state, nil
}
```

internal/state/state_test.go
```
package state

import (
	"os"
	"testing"
)

func TestStatePersistence(t *testing.T) {
	tmpFile := "test_state.json"
	defer os.Remove(tmpFile)

	s := &RunState{
		SchemaVersion: 1,
		PackHash:      "abc",
		StepStatuses: map[string]StepStatus{
			"01": {Status: StatusSuccess, ExitCode: 0},
		},
	}

	if err := SaveStateAtomic(tmpFile, s); err != nil {
		t.Fatalf("SaveStateAtomic failed: %v", err)
	}

	loaded, err := LoadState(tmpFile)
	if err != nil {
		t.Fatalf("LoadState failed: %v", err)
	}

	if loaded.PackHash != s.PackHash {
		t.Errorf("expected hash %s, got %s", s.PackHash, loaded.PackHash)
	}

	if loaded.StepStatuses["01"].Status != StatusSuccess {
		t.Errorf("expected status success, got %s", loaded.StepStatuses["01"].Status)
	}
}
```

internal/state/types.go
```
package state

import (
	"time"
)

type Status string

const (
	StatusPending Status = "pending"
	StatusRunning Status = "running"
	StatusSuccess Status = "success"
	StatusFailed  Status = "failed"
	StatusSkipped Status = "skipped"
)

// RunState tracks the execution progress of an oracle pack.
type RunState struct {
	SchemaVersion int                   `json:"schema_version"`
	PackHash      string                `json:"pack_hash"`
	StartTime     time.Time             `json:"start_time"`
	CurrentStep   int                   `json:"current_step,omitempty"`
	StepStatuses  map[string]StepStatus `json:"step_statuses"`
	ROIThreshold  float64               `json:"roi_threshold,omitempty"`
	ROIMode       string                `json:"roi_mode,omitempty"`
	Warnings      []Warning             `json:"warnings,omitempty"`
}

// StepStatus holds the outcome of an individual step.
type StepStatus struct {
	Status    Status    `json:"status"`
	ExitCode  int       `json:"exit_code"`
	StartedAt time.Time `json:"started_at"`
	EndedAt   time.Time `json:"ended_at"`
	Error     string    `json:"error,omitempty"`
}

// Warning captures a non-fatal execution note (e.g., sanitized labels).
type Warning struct {
	Scope   string `json:"scope"`
	StepID  string `json:"step_id,omitempty"`
	Line    int    `json:"line"`
	Token   string `json:"token"`
	Message string `json:"message"`
}
```

internal/templates/template_test.go
```
package templates

import (
	"os"
	"testing"

	"github.com/user/oraclepack/internal/pack"
)

func TestRenderTicketActionPack(t *testing.T) {
	got := RenderTicketActionPack()
	if got == "" {
		t.Fatal("expected non-empty template")
	}

	// Golden comparison
	data, err := os.ReadFile("ticket-action-pack.md")
	if err != nil {
		t.Fatalf("read template: %v", err)
	}
	if string(data) != got {
		t.Fatalf("template mismatch with golden file")
	}

	// Ensure pack is parseable and validates 20-step contract.
	p, err := pack.Parse([]byte(got))
	if err != nil {
		t.Fatalf("Parse failed: %v", err)
	}
	if err := pack.Validate(p); err != nil {
		t.Fatalf("Validate failed: %v", err)
	}
	if len(p.Steps) != 20 {
		t.Fatalf("expected 20 steps, got %d", len(p.Steps))
	}
}
```

internal/templates/ticket-action-pack.md
```
# Ticket Action Pack

```bash
out_dir=".oraclepack/ticketify"
--write-output

# 01)
echo "Build tickets index"

# 02)
echo "Generate actions json"

# 03)
echo "Generate tickets PRD"

# 04)
echo "Prep taskmaster inputs"

# 05)
task-master parse-prd .taskmaster/docs/prd.md

# 06)
task-master analyze-complexity --research

# 07)
task-master expand --all --research

# 08)
echo "Prepare headless automation"

# 09)
if command -v gemini >/dev/null 2>&1; then
  gemini run "Select next tasks" --write-output ".oraclepack/ticketify/next.json"
else
  echo "Skipped: gemini missing"
fi

# 10)
if command -v codex >/dev/null 2>&1; then
  codex exec "Implement tasks" --write-output ".oraclepack/ticketify/codex-implement.md"
else
  echo "Skipped: codex missing"
fi

# 11)
if command -v codex >/dev/null 2>&1; then
  codex exec "Verify changes" --write-output ".oraclepack/ticketify/codex-verify.md"
else
  echo "Skipped: codex missing"
fi

# 12)
if command -v gemini >/dev/null 2>&1; then
  gemini run "Review outputs" --write-output ".oraclepack/ticketify/gemini-review.json"
else
  echo "Skipped: gemini missing"
fi

# 13)
if command -v codex >/dev/null 2>&1; then
  codex exec "Prepare fixes" --write-output ".oraclepack/ticketify/codex-fixes.md"
else
  echo "Skipped: codex missing"
fi

# 14)
echo "Summarize results"

# 15)
echo "Prepare release notes"

# 16)
if command -v codex >/dev/null 2>&1; then
  codex exec "Draft PR description" --write-output ".oraclepack/ticketify/PR.md"
else
  echo "Skipped: codex missing"
fi

# 17)
echo "Finalize checklist"

# 18)
echo "Post-run cleanup"

# 19)
echo "Audit artifacts"

# 20)
echo "Done"
```
```

internal/templates/ticket_action_pack.go
```
package templates

import _ "embed"

//go:embed ticket-action-pack.md
var ticketActionPack string

// RenderTicketActionPack returns the canonical ticket action pack template.
func RenderTicketActionPack() string {
	return ticketActionPack
}
```

internal/tools/types.go
```
package tools

// ToolKind identifies a supported tool prefix.
type ToolKind int

const (
	ToolUnknown ToolKind = iota
	ToolOracle
	ToolTM
	ToolTaskMaster
	ToolCodex
	ToolGemini
)

// ToolMetadata captures tool invocation details.
type ToolMetadata struct {
	Name string
	Args []string
}

var registry = map[ToolKind]ToolMetadata{
	ToolUnknown:    {Name: "unknown"},
	ToolOracle:     {Name: "oracle"},
	ToolTM:         {Name: "tm"},
	ToolTaskMaster: {Name: "task-master"},
	ToolCodex:      {Name: "codex", Args: []string{"exec"}},
	ToolGemini:     {Name: "gemini"},
}

// Metadata returns tool metadata if present.
func Metadata(kind ToolKind) (ToolMetadata, bool) {
	meta, ok := registry[kind]
	return meta, ok
}

// Name returns the canonical tool name.
func (k ToolKind) Name() string {
	if meta, ok := registry[k]; ok {
		return meta.Name
	}
	return "unknown"
}

// PresenceChecker abstracts binary detection.
type PresenceChecker interface {
	DetectBinary(name string) (string, bool)
}
```

internal/tools/types_test.go
```
package tools

import "testing"

func TestMetadataRegistry(t *testing.T) {
	meta, ok := Metadata(ToolCodex)
	if !ok {
		t.Fatalf("expected metadata for codex")
	}
	if meta.Name != "codex" {
		t.Fatalf("expected codex name, got %s", meta.Name)
	}
	if len(meta.Args) != 1 || meta.Args[0] != "exec" {
		t.Fatalf("expected codex exec args, got %+v", meta.Args)
	}
	if ToolOracle.Name() != "oracle" {
		t.Fatalf("expected oracle name, got %s", ToolOracle.Name())
	}
}
```

internal/tui/clipboard.go
```
package tui

import (
	"fmt"
	"os"
	"os/exec"
	"runtime"
	"strings"
)

func copyToClipboard(content string) error {
	var cmd *exec.Cmd
	switch runtime.GOOS {
	case "darwin":
		cmd = exec.Command("pbcopy")
	case "linux":
		if _, err := exec.LookPath("wl-copy"); err == nil {
			cmd = exec.Command("wl-copy")
		} else if _, err := exec.LookPath("xclip"); err == nil {
			cmd = exec.Command("xclip", "-selection", "clipboard")
		} else if _, err := exec.LookPath("xsel"); err == nil {
			cmd = exec.Command("xsel", "--clipboard", "--input")
		} else {
			return err
		}
	case "windows":
		cmd = exec.Command("cmd", "/c", "clip")
	default:
		return exec.ErrNotFound
	}

	cmd.Stdin = strings.NewReader(content)
	return cmd.Run()
}

func writeClipboardFallback(content string) (string, error) {
	file, err := os.CreateTemp("", "oraclepack-step-*.txt")
	if err != nil {
		return "", fmt.Errorf("create temp file: %w", err)
	}
	defer file.Close()
	if _, err := file.WriteString(content); err != nil {
		return "", fmt.Errorf("write temp file: %w", err)
	}
	return file.Name(), nil
}
```

internal/tui/filter_test.go
```
package tui

import (
	"os"
	"path/filepath"
	"testing"

	tea "github.com/charmbracelet/bubbletea"
	"github.com/user/oraclepack/internal/exec"
	"github.com/user/oraclepack/internal/state"
	"github.com/user/oraclepack/internal/types"
)

func TestFilterLogic(t *testing.T) {
	// Setup pack with steps having different ROI
	p := &types.Pack{
		Steps: []types.Step{
			{ID: "01", ROI: 1.0, OriginalLine: "Step 1"},
			{ID: "02", ROI: 5.0, OriginalLine: "Step 2"},
			{ID: "03", ROI: 10.0, OriginalLine: "Step 3"},
		},
	}
	r := exec.NewRunner(exec.RunnerOptions{})
	s := &state.RunState{}

	// Initialize model with no filter (threshold 0)
	m := NewModel(p, r, s, "", 0, "over", false, false, 0, false, "auto")

	if len(m.list.Items()) != 3 {
		t.Fatalf("expected 3 items initially, got %d", len(m.list.Items()))
	}

	// Apply filter: ROI >= 5.0
	m.roiThreshold = 5.0
	m.roiMode = "over"
	m = m.refreshList()

	if len(m.list.Items()) != 2 {
		t.Errorf("expected 2 items after filtering >= 5.0, got %d", len(m.list.Items()))
	}

	// Verify items are 02 and 03
	items := m.list.Items()
	if items[0].(item).id != "02" {
		t.Errorf("expected first item to be 02, got %s", items[0].(item).id)
	}
	if items[1].(item).id != "03" {
		t.Errorf("expected second item to be 03, got %s", items[1].(item).id)
	}

	// Apply filter: ROI < 5.0 ("under")
	m.roiThreshold = 5.0
	m.roiMode = "under"
	m = m.refreshList()

	if len(m.list.Items()) != 1 {
		t.Errorf("expected 1 item after filtering < 5.0, got %d", len(m.list.Items()))
	}
	if m.list.Items()[0].(item).id != "01" {
		t.Errorf("expected item to be 01, got %s", m.list.Items()[0].(item).id)
	}
}

func TestROIModeTogglePersists(t *testing.T) {
	dir := t.TempDir()
	statePath := filepath.Join(dir, "state.json")
	p := &types.Pack{
		Steps: []types.Step{
			{ID: "01", ROI: 1.0, OriginalLine: "Step 1"},
		},
	}
	r := exec.NewRunner(exec.RunnerOptions{})
	s := &state.RunState{SchemaVersion: 1}

	m := NewModel(p, r, s, statePath, 0, "over", false, false, 0, false, "auto")

	updated, _ := m.Update(tea.KeyMsg{Type: tea.KeyRunes, Runes: []rune("m")})
	m2 := updated.(Model)
	if m2.roiMode != "under" {
		t.Fatalf("expected roiMode to toggle to under, got %s", m2.roiMode)
	}

	loaded, err := state.LoadState(statePath)
	if err != nil {
		t.Fatalf("failed to load state: %v", err)
	}
	if loaded.ROIMode != "under" {
		t.Fatalf("expected persisted roiMode under, got %s", loaded.ROIMode)
	}

	if err := os.Remove(statePath); err != nil {
		t.Fatalf("failed to cleanup state file: %v", err)
	}
}
```

internal/tui/overrides_confirm.go
```
package tui

import (
	"fmt"
	"sort"
	"strings"

	"github.com/user/oraclepack/internal/exec"
	"github.com/user/oraclepack/internal/overrides"
)

type ValidationResultMsg struct {
	Errors []exec.ValidationError
	Err    error
}

type OverridesConfirmModel struct {
	validating bool
	errMsg     string
	errors     []exec.ValidationError
}

func (m OverridesConfirmModel) View(over overrides.RuntimeOverrides, baseline []string) string {
	added := strings.Join(over.AddedFlags, ", ")
	if added == "" {
		added = "(none)"
	}
	removed := strings.Join(over.RemovedFlags, ", ")
	if removed == "" {
		removed = "(none)"
	}
	targeted := len(over.ApplyToSteps)
	targetList := formatTargetList(over.ApplyToSteps, 5)
	effective := effectiveFlagsSummary(over, baseline)
	lines := []string{
		"Summary:",
		fmt.Sprintf("Added flags: %s", added),
		fmt.Sprintf("Removed flags: %s", removed),
		fmt.Sprintf("Targeted steps: %d%s", targeted, targetList),
		fmt.Sprintf("Effective flags: %s", effective),
		"",
		"[Enter] Validate  [Esc] Cancel",
	}

	if m.validating {
		lines = append(lines, "", "Validating overrides...")
	}
	if m.errMsg != "" {
		lines = append(lines, "", "Validation failed:", m.errMsg)
	}
	if len(m.errors) > 0 {
		lines = append(lines, "", fmt.Sprintf("Validation errors (%d):", len(m.errors)))
		lines = append(lines, formatValidationErrors(m.errors, 6)...)
	}

	return strings.Join(lines, "\n")
}

func formatTargetList(targets map[string]bool, limit int) string {
	if len(targets) == 0 || limit <= 0 {
		return ""
	}
	ids := make([]string, 0, len(targets))
	for id := range targets {
		ids = append(ids, id)
	}
	sort.Strings(ids)
	if len(ids) <= limit {
		return fmt.Sprintf(" (%s)", strings.Join(ids, ", "))
	}
	return fmt.Sprintf(" (%s, +%d more)", strings.Join(ids[:limit], ", "), len(ids)-limit)
}

func effectiveFlagsSummary(over overrides.RuntimeOverrides, baseline []string) string {
	if len(over.ApplyToSteps) == 0 {
		return "(no steps targeted)"
	}
	var first string
	for id := range over.ApplyToSteps {
		first = id
		break
	}
	flags := over.EffectiveFlags(first, baseline)
	if len(flags) == 0 {
		return "(none)"
	}
	return strings.Join(flags, " ")
}

func formatValidationErrors(errors []exec.ValidationError, limit int) []string {
	if limit <= 0 {
		return nil
	}
	lines := []string{}
	for i, err := range errors {
		if i >= limit {
			lines = append(lines, fmt.Sprintf("- (+%d more)", len(errors)-limit))
			break
		}
		msg := strings.TrimSpace(err.ErrorMessage)
		if msg == "" {
			msg = "(no error message)"
		}
		lines = append(lines, fmt.Sprintf("- Step %s: %s", err.StepID, firstLine(msg)))
	}
	return lines
}

func firstLine(msg string) string {
	if idx := strings.IndexByte(msg, '\n'); idx != -1 {
		return msg[:idx]
	}
	return msg
}
```

internal/tui/overrides_flags.go
```
package tui

import (
	"fmt"
	"io"

	"github.com/charmbracelet/bubbles/list"
	tea "github.com/charmbracelet/bubbletea"
	"github.com/charmbracelet/lipgloss"
)

type FlagItem struct {
	Flag       string
	Desc       string
	IsBaseline bool
	Selected   bool
}

func (i FlagItem) Title() string       { return i.Flag }
func (i FlagItem) Description() string { return i.Desc }
func (i FlagItem) FilterValue() string { return i.Flag }

type FlagsPickerModel struct {
	list list.Model
}

func NewFlagsPickerModel(baseline []string) FlagsPickerModel {
	baselineSet := make(map[string]bool, len(baseline))
	for _, f := range baseline {
		baselineSet[f] = true
	}

	curated := []FlagItem{
		{Flag: "--files-report", Desc: "Show per-file token usage"},
		{Flag: "--render", Desc: "Print assembled markdown bundle"},
		{Flag: "--render-plain", Desc: "Render markdown without ANSI"},
		{Flag: "--copy", Desc: "Copy assembled markdown bundle"},
		{Flag: "--wait", Desc: "Wait for background API runs"},
	}

	items := make([]list.Item, 0, len(curated))
	for _, c := range curated {
		c.IsBaseline = baselineSet[c.Flag]
		if c.IsBaseline {
			c.Selected = true
		}
		items = append(items, c)
	}

	delegate := newFlagsDelegate()
	l := list.New(items, delegate, 0, 0)
	l.Title = "Oracle Flags"
	l.SetFilteringEnabled(true)

	return FlagsPickerModel{list: l}
}

func (m FlagsPickerModel) Init() tea.Cmd {
	return nil
}

func (m FlagsPickerModel) Update(msg tea.Msg) (FlagsPickerModel, tea.Cmd) {
	switch msg := msg.(type) {
	case tea.KeyMsg:
		if msg.String() == " " {
			idx := m.list.Index()
			item, ok := m.list.SelectedItem().(FlagItem)
			if ok && !item.IsBaseline {
				item.Selected = !item.Selected
				_ = m.list.SetItem(idx, item)
			}
		}
	}

	var cmd tea.Cmd
	m.list, cmd = m.list.Update(msg)
	return m, cmd
}

func (m *FlagsPickerModel) SetSize(width, height int) {
	m.list.SetSize(width, height)
}

func (m FlagsPickerModel) View() string {
	return m.list.View()
}

func (m FlagsPickerModel) SelectedFlags() []string {
	var flags []string
	for _, item := range m.list.Items() {
		if fi, ok := item.(FlagItem); ok && fi.Selected && !fi.IsBaseline {
			flags = append(flags, fi.Flag)
		}
	}
	return flags
}

type flagsDelegate struct {
	list.DefaultDelegate
}

func newFlagsDelegate() flagsDelegate {
	d := list.NewDefaultDelegate()
	return flagsDelegate{DefaultDelegate: d}
}

func (d flagsDelegate) Render(w io.Writer, m list.Model, index int, item list.Item) {
	fi, ok := item.(FlagItem)
	if !ok {
		d.DefaultDelegate.Render(w, m, index, item)
		return
	}

	checked := fi.Selected || fi.IsBaseline
	marker := "[ ]"
	if checked {
		marker = "[x]"
	}
	if fi.IsBaseline {
		marker = "[*]"
	}

	label := fi.Flag
	if fi.Desc != "" {
		label = fmt.Sprintf("%s - %s", fi.Flag, fi.Desc)
	}
	if fi.IsBaseline {
		label = label + " (base)"
	}

	line := fmt.Sprintf("%s %s", marker, label)
	if index == m.Index() {
		line = d.Styles.SelectedTitle.Render(line)
	} else {
		line = d.Styles.NormalTitle.Render(line)
	}
	if fi.IsBaseline {
		line = lipgloss.NewStyle().Faint(true).Render(line)
	}

	fmt.Fprintln(w, line)
}
```

internal/tui/overrides_flow.go
```
package tui

import (
	"context"
	"fmt"

	tea "github.com/charmbracelet/bubbletea"
	"github.com/charmbracelet/lipgloss"
	"github.com/user/oraclepack/internal/exec"
	"github.com/user/oraclepack/internal/overrides"
	"github.com/user/oraclepack/internal/types"
)

type OverridesStep int

const (
	OverridesFlags OverridesStep = iota
	OverridesSteps
	OverridesConfirm
)

type OverridesStartedMsg struct{}

type OverridesAppliedMsg struct {
	Overrides overrides.RuntimeOverrides
}

type OverridesCancelledMsg struct{}

type OverridesFlowModel struct {
	step    OverridesStep
	flags   FlagsPickerModel
	steps   StepsPickerModel
	confirm OverridesConfirmModel

	packSteps        []types.Step
	baseline         []string
	runnerOpts       exec.RunnerOptions
	pendingOverrides overrides.RuntimeOverrides
}

func NewOverridesFlowModel(steps []types.Step, baseline []string, opts exec.RunnerOptions) OverridesFlowModel {
	return OverridesFlowModel{
		step:       OverridesFlags,
		flags:      NewFlagsPickerModel(nil),
		steps:      NewStepsPickerModel(steps),
		confirm:    OverridesConfirmModel{},
		packSteps:  steps,
		baseline:   exec.ApplyChatGPTURL(baseline, opts.ChatGPTURL),
		runnerOpts: opts,
	}
}

func (m OverridesFlowModel) Init() tea.Cmd {
	return nil
}

func (m OverridesFlowModel) Update(msg tea.Msg) (OverridesFlowModel, tea.Cmd) {
	var cmd tea.Cmd
	if m.step == OverridesFlags {
		m.flags, cmd = m.flags.Update(msg)
	}
	if m.step == OverridesSteps {
		m.steps, cmd = m.steps.Update(msg)
	}
	if m.step == OverridesConfirm {
		switch v := msg.(type) {
		case ValidationResultMsg:
			m.confirm.validating = false
			m.confirm.errors = v.Errors
			if v.Err != nil {
				m.confirm.errMsg = v.Err.Error()
				return m, nil
			}
			if len(v.Errors) > 0 {
				m.confirm.errMsg = fmt.Sprintf("%d validation errors detected.", len(v.Errors))
				return m, nil
			}
			return m, func() tea.Msg { return OverridesAppliedMsg{Overrides: m.pendingOverrides} }
		}
	}

	switch msg := msg.(type) {
	case tea.KeyMsg:
		switch msg.String() {
		case "esc":
			return m, func() tea.Msg { return OverridesCancelledMsg{} }
		case "shift+tab", "backspace":
			if m.step > OverridesFlags {
				m.step--
			}
		case "enter", "tab":
			if m.step == OverridesConfirm {
				if m.confirm.validating {
					return m, nil
				}
				m.pendingOverrides = m.currentOverrides()
				m.confirm.validating = true
				m.confirm.errMsg = ""
				m.confirm.errors = nil
				return m, m.validateCmd(m.pendingOverrides)
			}
			m.step++
		}
	}

	return m, cmd
}

func (m OverridesFlowModel) View(width, height int) string {
	title := lipgloss.NewStyle().Bold(true).Render("Overrides Wizard")
	step := fmt.Sprintf("Step %d/3", int(m.step)+1)
	body := fmt.Sprintf("Current step: %s\n\n[Enter] Next  [Esc] Cancel", overridesStepName(m.step))

	var content string
	if m.step == OverridesFlags {
		m.flags.SetSize(width-4, height-8)
		content = lipgloss.JoinVertical(lipgloss.Left,
			title,
			step,
			"",
			m.flags.View(),
			"",
			body,
		)
	} else if m.step == OverridesSteps {
		m.steps.SetSize(width-4, height-8)
		content = lipgloss.JoinVertical(lipgloss.Left,
			title,
			step,
			"",
			m.steps.View(),
			"",
			body,
		)
	} else if m.step == OverridesConfirm {
		content = lipgloss.JoinVertical(lipgloss.Left,
			title,
			step,
			"",
			m.confirm.View(m.currentOverrides(), m.baseline),
		)
	} else {
		content = lipgloss.JoinVertical(lipgloss.Left,
			title,
			step,
			"",
			body,
		)
	}

	return lipgloss.Place(width, height, lipgloss.Center, lipgloss.Center, content)
}

func (m OverridesFlowModel) currentOverrides() overrides.RuntimeOverrides {
	return overrides.RuntimeOverrides{
		AddedFlags:   m.flags.SelectedFlags(),
		RemovedFlags: nil,
		ApplyToSteps: m.steps.SelectedSteps(),
	}
}

func (m OverridesFlowModel) validateCmd(over overrides.RuntimeOverrides) tea.Cmd {
	return func() tea.Msg {
		errs, err := exec.ValidateOverrides(context.Background(), m.packSteps, &over, m.baseline, m.runnerOpts)
		return ValidationResultMsg{Errors: errs, Err: err}
	}
}

func overridesStepName(step OverridesStep) string {
[TRUNCATED]
```

internal/tui/overrides_steps.go
```
package tui

import (
	"fmt"
	"io"

	"github.com/charmbracelet/bubbles/list"
	tea "github.com/charmbracelet/bubbletea"
	"github.com/charmbracelet/lipgloss"
	"github.com/user/oraclepack/internal/types"
)

type StepItem struct {
	ID       string
	TitleTxt string
	DescTxt  string
	Selected bool
}

func (i StepItem) Title() string       { return i.TitleTxt }
func (i StepItem) Description() string { return i.DescTxt }
func (i StepItem) FilterValue() string { return i.TitleTxt }

type StepsPickerModel struct {
	list list.Model
}

func NewStepsPickerModel(steps []types.Step) StepsPickerModel {
	items := make([]list.Item, 0, len(steps))
	for _, s := range steps {
		items = append(items, StepItem{
			ID:       s.ID,
			TitleTxt: fmt.Sprintf("Step %s", s.ID),
			DescTxt:  s.OriginalLine,
			Selected: true,
		})
	}

	delegate := newStepsDelegate()
	l := list.New(items, delegate, 0, 0)
	l.Title = "Target Steps"
	l.SetFilteringEnabled(true)

	return StepsPickerModel{list: l}
}

func (m StepsPickerModel) Init() tea.Cmd {
	return nil
}

func (m StepsPickerModel) Update(msg tea.Msg) (StepsPickerModel, tea.Cmd) {
	switch msg := msg.(type) {
	case tea.KeyMsg:
		switch msg.String() {
		case "a", "A":
			m = m.setAll(true)
			return m, nil
		case "i":
			m = m.invert()
			return m, nil
		case "n":
			m = m.setAll(false)
			return m, nil
		case " ":
			idx := m.list.Index()
			item, ok := m.list.SelectedItem().(StepItem)
			if ok {
				item.Selected = !item.Selected
				_ = m.list.SetItem(idx, item)
			}
		}
	}

	var cmd tea.Cmd
	m.list, cmd = m.list.Update(msg)
	return m, cmd
}

func (m *StepsPickerModel) SetSize(width, height int) {
	m.list.SetSize(width, height)
}

func (m StepsPickerModel) View() string {
	help := lipgloss.NewStyle().Faint(true).Render("[space] toggle  [a] all  [i] invert  [n] none")
	return m.list.View() + "\n" + help
}

func (m StepsPickerModel) SelectedSteps() map[string]bool {
	selected := make(map[string]bool)
	for _, item := range m.list.Items() {
		if si, ok := item.(StepItem); ok && si.Selected {
			selected[si.ID] = true
		}
	}
	return selected
}

func (m StepsPickerModel) setAll(value bool) StepsPickerModel {
	for idx, item := range m.list.Items() {
		si, ok := item.(StepItem)
		if !ok {
			continue
		}
		si.Selected = value
		_ = m.list.SetItem(idx, si)
	}
	return m
}

func (m StepsPickerModel) invert() StepsPickerModel {
	for idx, item := range m.list.Items() {
		si, ok := item.(StepItem)
		if !ok {
			continue
		}
		si.Selected = !si.Selected
		_ = m.list.SetItem(idx, si)
	}
	return m
}

type stepsDelegate struct {
	list.DefaultDelegate
}

func newStepsDelegate() stepsDelegate {
	d := list.NewDefaultDelegate()
	return stepsDelegate{DefaultDelegate: d}
}

func (d stepsDelegate) Render(w io.Writer, m list.Model, index int, item list.Item) {
	si, ok := item.(StepItem)
	if !ok {
		d.DefaultDelegate.Render(w, m, index, item)
		return
	}

	marker := "[ ]"
	if si.Selected {
		marker = "[x]"
	}

	label := si.TitleTxt
	if si.DescTxt != "" {
		label = fmt.Sprintf("%s - %s", si.TitleTxt, si.DescTxt)
	}

	line := fmt.Sprintf("%s %s", marker, label)
	if index == m.Index() {
		line = d.Styles.SelectedTitle.Render(line)
	} else {
		line = d.Styles.NormalTitle.Render(line)
	}

	fmt.Fprintln(w, line)
}
```

internal/tui/overrides_url.go
```
package tui

import (
	"strings"

	"github.com/charmbracelet/bubbles/textinput"
	tea "github.com/charmbracelet/bubbletea"
	"github.com/charmbracelet/lipgloss"
)

type URLInputModel struct {
	input textinput.Model
	err   string
}

func NewURLInputModel() URLInputModel {
	ti := textinput.New()
	ti.Placeholder = "https://chat.openai.com/project/..."
	ti.CharLimit = 200
	ti.Width = 50

	return URLInputModel{input: ti}
}

func (m URLInputModel) Init() tea.Cmd {
	return textinput.Blink
}

func (m URLInputModel) Update(msg tea.Msg) (URLInputModel, tea.Cmd) {
	var cmd tea.Cmd
	m.input, cmd = m.input.Update(msg)
	m.err = ""
	if !m.IsValid() {
		m.err = "Invalid URL (must start with http:// or https://)"
	}
	return m, cmd
}

func (m URLInputModel) Value() string {
	return strings.TrimSpace(m.input.Value())
}

func (m URLInputModel) IsValid() bool {
	v := m.Value()
	if v == "" {
		return true
	}
	return strings.HasPrefix(v, "http://") || strings.HasPrefix(v, "https://")
}

func (m URLInputModel) View() string {
	body := m.input.View()
	if m.err != "" {
		body = body + "\n" + lipgloss.NewStyle().Foreground(lipgloss.Color("196")).Render(m.err)
	}
	return body
}

func (m *URLInputModel) SetValue(v string) {
	m.input.SetValue(v)
}

func (m *URLInputModel) Focus() {
	m.input.Focus()
}

func (m *URLInputModel) Blur() {
	m.input.Blur()
}
```

internal/tui/preview_test.go
```
package tui

import (
	"strings"
	"testing"

	"github.com/user/oraclepack/internal/exec"
	"github.com/user/oraclepack/internal/state"
	"github.com/user/oraclepack/internal/types"
)

func TestStepPreviewContentUnwrapped(t *testing.T) {
	p := &types.Pack{
		Steps: []types.Step{
			{ID: "01", OriginalLine: "Step 1", Code: "echo hello"},
		},
	}
	r := exec.NewRunner(exec.RunnerOptions{})
	s := &state.RunState{}
	m := NewModel(p, r, s, "", 0, "over", false, false, 0, false, "auto")
	m.width = 80
	m.previewID = "01"
	m.previewWrap = false

	content := m.stepPreviewContent()
	if !strings.Contains(content, "Step 01") {
		t.Fatalf("expected header to include step id, got %q", content)
	}
	if !strings.Contains(content, "echo hello") {
		t.Fatalf("expected content to include code, got %q", content)
	}
}
```

internal/tui/tui.go
```
package tui

import (
	"context"
	"fmt"
	"strings"
	"time"

	"github.com/charmbracelet/bubbles/list"
	"github.com/charmbracelet/bubbles/spinner"
	"github.com/charmbracelet/bubbles/textinput"
	"github.com/charmbracelet/bubbles/viewport"
	tea "github.com/charmbracelet/bubbletea"
	"github.com/charmbracelet/lipgloss"
	"github.com/user/oraclepack/internal/exec"
	"github.com/user/oraclepack/internal/overrides"
	"github.com/user/oraclepack/internal/pack"
	"github.com/user/oraclepack/internal/render"
	"github.com/user/oraclepack/internal/state"
	"github.com/user/oraclepack/internal/types"
)

type ViewState int

const (
	ViewSteps ViewState = iota
	ViewRunning
	ViewDone
	ViewOverrides
	ViewStepPreview
)

type item struct {
	id     string
	title  string
	desc   string
	status state.Status
}

func (i item) Title() string       { return i.title }
func (i item) Description() string { return i.desc }
func (i item) FilterValue() string { return i.title }

type Model struct {
	list        list.Model
	viewport    viewport.Model
	spinner     spinner.Model
	filterInput textinput.Model
	urlInput    URLInputModel
	urlPicker   URLPickerModel
	pack        *types.Pack
	runner      *exec.Runner
	state       *state.RunState
	statePath   string

	width  int
	height int

	viewState     ViewState
	running       bool
	runAll        bool // State for sequential execution
	currentIdx    int
	autoRun       bool // Config to auto-start on init
	previewID     string
	previewWrap   bool
	previewNotice string

	// Filtering state
	allSteps     []item // Store all items to support dynamic filtering
	roiThreshold float64
	roiMode      string
	isFiltering  bool
	isEditingURL bool
	isPickingURL bool

	overridesFlow         OverridesFlowModel
	appliedOverrides      *overrides.RuntimeOverrides
	chatGPTURL            string
	outputVerify          bool
	outputRetries         int
	outputRequireHeadings bool
	outputChunkMode       string

	err      error
	logLines []string
	logChan  chan string
}

func NewModel(p *types.Pack, r *exec.Runner, s *state.RunState, statePath string, roiThreshold float64, roiMode string, autoRun bool, outputVerify bool, outputRetries int, outputRequireHeadings bool, outputChunkMode string) Model {
	if s != nil {
		if s.ROIThreshold > 0 {
			roiThreshold = s.ROIThreshold
		}
		if s.ROIMode != "" {
			roiMode = s.ROIMode
		}
	}
	var allItems []item
	for _, step := range p.Steps {
		allItems = append(allItems, item{
			id:    step.ID,
			title: fmt.Sprintf("Step %s", step.ID),
			desc:  step.OriginalLine,
		})
	}

	ti := textinput.New()
	ti.Placeholder = "Enter ROI threshold (e.g. 2.5)"
	ti.CharLimit = 10
	ti.Width = 20

	l := list.New([]list.Item{}, list.NewDefaultDelegate(), 0, 0)
	l.Title = "Oracle Pack Steps"

	sp := spinner.New()
	sp.Spinner = spinner.Dot
	sp.Style = lipgloss.NewStyle().Foreground(lipgloss.Color("205"))

	vp := viewport.New(0, 0)
	vp.SetContent("Press Enter to run selected, 'a' to run all filtered steps, 'f' to set ROI threshold, 'm' to toggle ROI mode, 'v' to view step, 'o' to configure overrides, 'u' for ChatGPT URL, 'U' to pick a saved URL.")

	projectPath := ProjectURLStorePath(statePath, p.Source)
	globalPath := GlobalURLStorePath()
	urlPicker := NewURLPickerModel(projectPath, globalPath)
	resolvedURL := r.ChatGPTURL
	if resolvedURL == "" {
		resolvedURL = urlPicker.DefaultURL()
	}
	if resolvedURL != "" {
		r.ChatGPTURL = resolvedURL
	}

	m := Model{
		list:                  l,
		viewport:              vp,
		spinner:               sp,
		filterInput:           ti,
		urlInput:              NewURLInputModel(),
		urlPicker:             urlPicker,
		pack:                  p,
		runner:                r,
		state:                 s,
		statePath:             statePath,
		autoRun:               autoRun,
		allSteps:              allItems,
		roiThreshold:          roiThreshold,
		roiMode:               roiMode,
		logChan:               make(chan string, 100),
		viewState:             ViewSteps,
		overridesFlow:         NewOverridesFlowModel(p.Steps, r.OracleFlags, RunnerOptionsFromRunner(r)),
		chatGPTURL:            resolvedURL,
		previewWrap:           true,
		outputVerify:          outputVerify,
		outputRetries:         outputRetries,
		outputRequireHeadings: outputRequireHeadings,
		outputChunkMode:       outputChunkMode,
	}
	m.urlInput.SetValue(resolvedURL)
[TRUNCATED]
```

internal/tui/tui_test.go
```
package tui

import (
	"testing"

	"github.com/user/oraclepack/internal/exec"
	"github.com/user/oraclepack/internal/state"
	"github.com/user/oraclepack/internal/types"
)

func TestInitAutoRun(t *testing.T) {
	p := &types.Pack{
		Steps: []types.Step{
			{ID: "01", Number: 1, Code: "echo hello"},
		},
	}
	r := exec.NewRunner(exec.RunnerOptions{})
	s := &state.RunState{}

	// Test case 1: autoRun = true
	modelAuto := NewModel(p, r, s, "", 0, "over", true, false, 0, false, "auto")
	cmdAuto := modelAuto.Init()

	if cmdAuto == nil {
		t.Fatal("expected Init cmd to be non-nil when autoRun is true")
	}
	// Note: We can't easily assert the content of a Batch command in a unit test.

	// Test case 2: autoRun = false
	modelManual := NewModel(p, r, s, "", 0, "over", false, false, 0, false, "auto")
	// Even with autoRun false, we have textinput.Blink, so Init is not nil.
	cmdManual := modelManual.Init()
	if cmdManual == nil {
		t.Fatal("expected Init cmd to be non-nil due to textinput.Blink")
	}
}
```

internal/tui/url_picker.go
```
package tui

import (
	"fmt"
	"strings"

	"github.com/charmbracelet/bubbles/list"
	"github.com/charmbracelet/bubbles/textinput"
	tea "github.com/charmbracelet/bubbletea"
	"github.com/charmbracelet/lipgloss"
)

type URLPickedMsg struct {
	URL string
}

type URLPickerCancelledMsg struct{}

type urlItem struct {
	name  string
	url   string
	scope string
}

func (i urlItem) Title() string       { return i.name }
func (i urlItem) Description() string { return fmt.Sprintf("%s • %s", i.scope, i.url) }
func (i urlItem) FilterValue() string { return i.name }

type URLPickerModel struct {
	list list.Model

	projectPath string
	globalPath  string
	project     URLStore
	global      URLStore

	editing   bool
	editName  textinput.Model
	editURL   textinput.Model
	editScope string
	editIdx   int
	editIsNew bool

	errMsg string
}

func NewURLPickerModel(projectPath, globalPath string) URLPickerModel {
	project, _ := LoadURLStore(projectPath)
	global, _ := LoadURLStore(globalPath)

	items := makeURLItems(project, global)
	l := list.New(items, list.NewDefaultDelegate(), 0, 0)
	l.Title = "ChatGPT Project URLs"
	l.SetFilteringEnabled(true)
	selectDefault(&l, project, global)

	name := textinput.New()
	name.Placeholder = "Name (e.g., Core Project)"
	name.CharLimit = 60
	name.Width = 40

	url := textinput.New()
	url.Placeholder = "https://chatgpt.com/g/.../project"
	url.CharLimit = 200
	url.Width = 60

	return URLPickerModel{
		list:        l,
		projectPath: projectPath,
		globalPath:  globalPath,
		project:     project,
		global:      global,
		editName:    name,
		editURL:     url,
	}
}

func (m URLPickerModel) Init() tea.Cmd {
	return nil
}

func (m URLPickerModel) Update(msg tea.Msg) (URLPickerModel, tea.Cmd) {
	if m.editing {
		return m.updateEdit(msg)
	}

	switch msg := msg.(type) {
	case tea.KeyMsg:
		switch msg.String() {
		case "esc":
			return m, func() tea.Msg { return URLPickerCancelledMsg{} }
		case "enter":
			item, ok := m.list.SelectedItem().(urlItem)
			if !ok {
				return m, nil
			}
			m.touch(item)
			return m, func() tea.Msg { return URLPickedMsg{URL: item.url} }
		case "a":
			m.startEdit(urlScopeProject, "", "", true)
			return m, nil
		case "e":
			item, ok := m.list.SelectedItem().(urlItem)
			if !ok {
				return m, nil
			}
			m.startEdit(item.scope, item.name, item.url, false)
			return m, nil
		case "d":
			item, ok := m.list.SelectedItem().(urlItem)
			if !ok {
				return m, nil
			}
			m.delete(item)
			return m, nil
		case "s":
			item, ok := m.list.SelectedItem().(urlItem)
			if !ok {
				return m, nil
			}
			m.setDefault(item)
			return m, nil
		}
	}

	var cmd tea.Cmd
	m.list, cmd = m.list.Update(msg)
	return m, cmd
}

func (m *URLPickerModel) SetSize(width, height int) {
	m.list.SetSize(width, height-4)
}

func (m URLPickerModel) View() string {
	if m.editing {
		return m.editView()
	}

	help := lipgloss.NewStyle().Faint(true).Render("[enter] use  [a] add  [e] edit  [d] delete  [s] default  [esc] cancel")
	return m.list.View() + "\n" + help
}

func makeURLItems(project URLStore, global URLStore) []list.Item {
	var items []list.Item
	for _, it := range project.Items {
		items = append(items, urlItem{name: it.Name, url: it.URL, scope: urlScopeProject})
	}
	for _, it := range global.Items {
		items = append(items, urlItem{name: it.Name, url: it.URL, scope: urlScopeGlobal})
	}
	return items
}

func selectDefault(l *list.Model, project URLStore, global URLStore) {
	if l == nil {
		return
	}
	name, scope := defaultNameScope(project, global)
	if name == "" {
		return
	}
	for idx, item := range l.Items() {
[TRUNCATED]
```

internal/tui/url_store.go
```
package tui

import (
	"encoding/json"
	"errors"
	"os"
	"path/filepath"
	"strings"
	"time"
)

const (
	urlScopeProject = "project"
	urlScopeGlobal  = "global"
)

type URLItem struct {
	Name     string `json:"name"`
	URL      string `json:"url"`
	LastUsed string `json:"lastUsed,omitempty"`
}

type URLStore struct {
	Default string    `json:"default"`
	Items   []URLItem `json:"items"`
}

func LoadURLStore(path string) (URLStore, error) {
	if path == "" {
		return URLStore{}, nil
	}
	data, err := os.ReadFile(path)
	if err != nil {
		if errors.Is(err, os.ErrNotExist) {
			return URLStore{}, nil
		}
		return URLStore{}, err
	}
	var store URLStore
	if err := json.Unmarshal(data, &store); err != nil {
		return URLStore{}, err
	}
	return store, nil
}

func SaveURLStore(path string, store URLStore) error {
	if path == "" {
		return nil
	}
	if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil {
		return err
	}
	data, err := json.MarshalIndent(store, "", "  ")
	if err != nil {
		return err
	}
	return os.WriteFile(path, data, 0o644)
}

func ProjectURLStorePath(statePath, packSource string) string {
	if statePath != "" {
		base := strings.TrimSuffix(statePath, ".state.json")
		return base + ".chatgpt-urls.json"
	}
	if packSource == "" {
		return ""
	}
	return packSource + ".chatgpt-urls.json"
}

func GlobalURLStorePath() string {
	home, err := os.UserHomeDir()
	if err != nil || home == "" {
		return ""
	}
	return filepath.Join(home, ".oraclepack", "chatgpt-urls.json")
}

func nowRFC3339() string {
	return time.Now().UTC().Format(time.RFC3339)
}

func isValidURL(value string) bool {
	v := strings.TrimSpace(value)
	if v == "" {
		return false
	}
	return strings.HasPrefix(v, "http://") || strings.HasPrefix(v, "https://")
}
```

internal/tui/url_store_test.go
```
package tui

import (
	"path/filepath"
	"testing"
)

func TestURLStoreSaveLoad(t *testing.T) {
	dir := t.TempDir()
	path := filepath.Join(dir, "urls.json")

	store := URLStore{
		Default: "Primary",
		Items: []URLItem{{
			Name: "Primary",
			URL:  "https://chatgpt.com/g/primary",
		}},
	}

	if err := SaveURLStore(path, store); err != nil {
		t.Fatalf("failed to save store: %v", err)
	}

	loaded, err := LoadURLStore(path)
	if err != nil {
		t.Fatalf("failed to load store: %v", err)
	}

	if loaded.Default != store.Default {
		t.Fatalf("expected default %q, got %q", store.Default, loaded.Default)
	}
	if len(loaded.Items) != 1 || loaded.Items[0].URL != store.Items[0].URL {
		t.Fatalf("loaded items mismatch: %+v", loaded.Items)
	}
}

func TestURLPickerDefaultURLPrefersProject(t *testing.T) {
	dir := t.TempDir()
	projectPath := filepath.Join(dir, "project.json")
	globalPath := filepath.Join(dir, "global.json")

	project := URLStore{
		Default: "Project",
		Items: []URLItem{{
			Name: "Project",
			URL:  "https://chatgpt.com/g/project",
		}},
	}
	global := URLStore{
		Default: "Global",
		Items: []URLItem{{
			Name: "Global",
			URL:  "https://chatgpt.com/g/global",
		}},
	}

	if err := SaveURLStore(projectPath, project); err != nil {
		t.Fatalf("failed to save project store: %v", err)
	}
	if err := SaveURLStore(globalPath, global); err != nil {
		t.Fatalf("failed to save global store: %v", err)
	}

	picker := NewURLPickerModel(projectPath, globalPath)
	if got := picker.DefaultURL(); got != project.Items[0].URL {
		t.Fatalf("expected project default URL %q, got %q", project.Items[0].URL, got)
	}
}
```

internal/types/pack.go
```
package types

// Pack represents a parsed oracle pack.
type Pack struct {
	Prelude     Prelude `json:"prelude" yaml:"prelude"`
	Steps       []Step  `json:"steps" yaml:"steps"`
	Source      string  `json:"source,omitempty" yaml:"source,omitempty"`
	OutDir      string  `json:"out_dir,omitempty" yaml:"out_dir,omitempty"`
	WriteOutput bool    `json:"write_output" yaml:"write_output"`
}

// Prelude contains the shell code that runs before any steps.
type Prelude struct {
	Code string `json:"code" yaml:"code"`
}

// Step represents an individual executable step within the pack.
type Step struct {
	ID           string  `json:"id" yaml:"id"`                             // e.g., "01"
	Number       int     `json:"number" yaml:"number"`                     // e.g., 1
	Code         string  `json:"code" yaml:"code"`                         // The bash code
	OriginalLine string  `json:"original_line" yaml:"original_line"`       // The header line, e.g., "# 01)"
	ROI          float64 `json:"roi,omitempty" yaml:"roi,omitempty"`       // Return on Investment value extracted from header
	Impact       string  `json:"impact,omitempty" yaml:"impact,omitempty"` // Optional impact metadata extracted from step comments
}
```

internal/types/pack_test.go
```
package types

import (
	"encoding/json"
	"reflect"
	"testing"

	"github.com/goccy/go-yaml"
)

func TestPackJSONRoundTrip(t *testing.T) {
	original := Pack{
		Prelude: Prelude{Code: "echo prelude"},
		Steps: []Step{
			{
				ID:           "01",
				Number:       1,
				Code:         "echo hello",
				OriginalLine: "# 01) Example",
				ROI:          3.2,
				Impact:       "High",
			},
		},
		Source:      "pack.md",
		OutDir:      "dist",
		WriteOutput: true,
	}

	data, err := json.Marshal(original)
	if err != nil {
		t.Fatalf("json marshal: %v", err)
	}

	var decoded Pack
	if err := json.Unmarshal(data, &decoded); err != nil {
		t.Fatalf("json unmarshal: %v", err)
	}

	if !reflect.DeepEqual(original, decoded) {
		t.Fatalf("json round-trip mismatch: %#v != %#v", original, decoded)
	}
}

func TestPackYAMLRoundTrip(t *testing.T) {
	original := Pack{
		Prelude: Prelude{Code: "echo prelude"},
		Steps: []Step{
			{
				ID:           "02",
				Number:       2,
				Code:         "echo yaml",
				OriginalLine: "# 02) Example",
				ROI:          1.1,
				Impact:       "Low",
			},
		},
		Source:      "pack.yaml",
		OutDir:      "out",
		WriteOutput: false,
	}

	data, err := yaml.Marshal(original)
	if err != nil {
		t.Fatalf("yaml marshal: %v", err)
	}

	var decoded Pack
	if err := yaml.Unmarshal(data, &decoded); err != nil {
		t.Fatalf("yaml unmarshal: %v", err)
	}

	if !reflect.DeepEqual(original, decoded) {
		t.Fatalf("yaml round-trip mismatch: %#v != %#v", original, decoded)
	}
}
```

internal/types/verification.go
```
package types

// OutputContract describes the expected response shape for a step.
type OutputContract string

const (
	OutputContractUnknown          OutputContract = ""
	OutputContractAllSections      OutputContract = "all_sections"
	OutputContractDirectAnswerOnly OutputContract = "direct_answer_only"
	OutputContractChunkedBySuffix  OutputContract = "chunked_by_suffix"
)

// OutputFailure captures a missing or invalid output artifact.
type OutputFailure struct {
	StepID        string   `json:"step_id,omitempty" yaml:"step_id,omitempty"`
	Path          string   `json:"path,omitempty" yaml:"path,omitempty"`
	MissingTokens []string `json:"missing_tokens,omitempty" yaml:"missing_tokens,omitempty"`
	Error         string   `json:"error,omitempty" yaml:"error,omitempty"`
}

// SyntaxFinding captures a structural or syntax issue in generated bash.
type SyntaxFinding struct {
	StepID  string `json:"step_id,omitempty" yaml:"step_id,omitempty"`
	Line    int    `json:"line" yaml:"line"`
	Token   string `json:"token,omitempty" yaml:"token,omitempty"`
	Message string `json:"message" yaml:"message"`
}
```

internal/validate/artifact_gate.go
```
package validate

import (
	"errors"

	"github.com/user/oraclepack/internal/artifacts"
	"github.com/user/oraclepack/internal/foundation"
	"github.com/user/oraclepack/internal/state"
	"github.com/user/oraclepack/internal/tools"
	"github.com/user/oraclepack/internal/types"
)

// ArtifactGateValidator checks expected artifacts after a step.
type ArtifactGateValidator struct {
	Contract artifacts.Contract
}

func (v ArtifactGateValidator) Validate(step *types.Step, kind tools.ToolKind, toolPresent bool) (state.Status, string) {
	if step == nil {
		return state.StatusSuccess, ""
	}
	contract := v.Contract
	if contract == nil {
		contract = artifacts.DefaultContract()
	}
	if !toolPresent {
		if _, ok := contract[step.ID]; ok {
			return state.StatusSkipped, "tool missing; skipping artifact gate"
		}
		return state.StatusSuccess, ""
	}
	err := artifacts.EvaluateGates(step.ID, contract)
	if err == nil {
		return state.StatusSuccess, ""
	}
	if errors.Is(err, foundation.ErrArtifactMissing) {
		return state.StatusFailed, err.Error()
	}
	return state.StatusFailed, err.Error()
}
```

internal/validate/artifact_gate_test.go
```
package validate

import (
	"os"
	"path/filepath"
	"testing"

	"github.com/user/oraclepack/internal/artifacts"
	"github.com/user/oraclepack/internal/state"
	"github.com/user/oraclepack/internal/tools"
	"github.com/user/oraclepack/internal/types"
)

func TestArtifactGateValidator(t *testing.T) {
	dir := t.TempDir()
	path := filepath.Join(dir, "next.json")
	contract := artifacts.Contract{"09": {path}}
	step := &types.Step{ID: "09"}
	v := ArtifactGateValidator{Contract: contract}

	status, _ := v.Validate(step, tools.ToolCodex, true)
	if status != state.StatusFailed {
		t.Fatalf("expected failed for missing artifact, got %s", status)
	}

	if err := os.WriteFile(path, []byte("ok"), 0644); err != nil {
		t.Fatalf("write: %v", err)
	}
	status, _ = v.Validate(step, tools.ToolCodex, true)
	if status != state.StatusSuccess {
		t.Fatalf("expected success, got %s", status)
	}

	status, _ = v.Validate(step, tools.ToolCodex, false)
	if status != state.StatusSkipped {
		t.Fatalf("expected skipped when tool missing, got %s", status)
	}
}
```

internal/validate/composite.go
```
package validate

import (
	"github.com/user/oraclepack/internal/state"
	"github.com/user/oraclepack/internal/types"
)

// CompositeValidator coordinates multiple validators for all steps.
type CompositeValidator struct {
	ToolPresence ToolPresenceValidator
	OracleDryRun OracleDryRunValidator
	ArtifactGate ArtifactGateValidator
}

// ValidatePack validates all steps in a pack and returns per-step results.
func (v CompositeValidator) ValidatePack(p *types.Pack) []StepResult {
	if p == nil {
		return nil
	}
	results := make([]StepResult, 0, len(p.Steps))
	for i := range p.Steps {
		step := &p.Steps[i]
		kind := DetectToolKind(step)

		status, reason, toolPresent := v.ToolPresence.Validate(step, kind)
		if status == state.StatusSkipped {
			results = append(results, StepResult{
				StepID:   step.ID,
				ToolKind: kind,
				Status:   status,
				Error:    reason,
			})
			continue
		}

		if oracleStatus, oracleErr := v.OracleDryRun.Validate(step, kind); oracleStatus == state.StatusFailed {
			results = append(results, StepResult{
				StepID:   step.ID,
				ToolKind: kind,
				Status:   oracleStatus,
				Error:    oracleErr,
			})
			continue
		}

		gateStatus, gateErr := v.ArtifactGate.Validate(step, kind, toolPresent)
		finalStatus := gateStatus
		errMsg := gateErr
		if finalStatus == "" {
			finalStatus = state.StatusSuccess
		}
		results = append(results, StepResult{
			StepID:   step.ID,
			ToolKind: kind,
			Status:   finalStatus,
			Error:    errMsg,
		})
	}
	return results
}
```

internal/validate/composite_test.go
```
package validate

import (
	"testing"

	"github.com/user/oraclepack/internal/state"
	"github.com/user/oraclepack/internal/types"
)

func TestCompositeValidator(t *testing.T) {
	p := &types.Pack{
		Steps: []types.Step{
			{ID: "01", Code: "codex exec \"hi\""},
		},
	}
	cv := CompositeValidator{
		ToolPresence: ToolPresenceValidator{Checker: fakeChecker{found: map[string]bool{"codex": false}}},
	}
	results := cv.ValidatePack(p)
	if len(results) != 1 {
		t.Fatalf("expected 1 result, got %d", len(results))
	}
	if results[0].Status != state.StatusSkipped {
		t.Fatalf("expected skipped, got %s", results[0].Status)
	}
}
```

internal/validate/oracle.go
```
package validate

import (
	"github.com/user/oraclepack/internal/state"
	"github.com/user/oraclepack/internal/tools"
	"github.com/user/oraclepack/internal/types"
)

// OracleDryRunValidator is a placeholder for oracle-specific validation.
type OracleDryRunValidator struct{}

// Validate returns success for non-oracle tools and no-op for oracle until wired.
func (OracleDryRunValidator) Validate(step *types.Step, kind tools.ToolKind) (state.Status, string) {
	if kind != tools.ToolOracle {
		return state.StatusSuccess, ""
	}
	return state.StatusSuccess, ""
}
```

internal/validate/presence.go
```
package validate

import (
	"fmt"

	"github.com/user/oraclepack/internal/shell"
	"github.com/user/oraclepack/internal/state"
	"github.com/user/oraclepack/internal/tools"
	"github.com/user/oraclepack/internal/types"
)

// ToolPresenceValidator checks that tool binaries exist on PATH.
type ToolPresenceValidator struct {
	Checker tools.PresenceChecker
}

// Validate returns skipped if the tool is missing.
func (v ToolPresenceValidator) Validate(step *types.Step, kind tools.ToolKind) (state.Status, string, bool) {
	if kind == tools.ToolUnknown {
		return state.StatusSuccess, "", true
	}
	meta, ok := tools.Metadata(kind)
	if !ok {
		return state.StatusSuccess, "", true
	}
	checker := v.Checker
	if checker == nil {
		checker = shellChecker{}
	}
	if _, found := checker.DetectBinary(meta.Name); !found {
		return state.StatusSkipped, fmt.Sprintf("tool %s not found on PATH", meta.Name), false
	}
	return state.StatusSuccess, "", true
}

type shellChecker struct{}

func (shellChecker) DetectBinary(name string) (string, bool) {
	return shell.DetectBinary(name)
}
```

internal/validate/presence_test.go
```
package validate

import (
	"testing"

	"github.com/user/oraclepack/internal/state"
	"github.com/user/oraclepack/internal/tools"
	"github.com/user/oraclepack/internal/types"
)

type fakeChecker struct {
	found map[string]bool
}

func (f fakeChecker) DetectBinary(name string) (string, bool) {
	return name, f.found[name]
}

func TestToolPresenceValidator(t *testing.T) {
	step := &types.Step{ID: "01", Code: "codex exec \"hi\""}
	v := ToolPresenceValidator{Checker: fakeChecker{found: map[string]bool{"codex": false}}}
	status, reason, present := v.Validate(step, tools.ToolCodex)
	if status != state.StatusSkipped || present {
		t.Fatalf("expected skipped/missing, got status=%s present=%v", status, present)
	}
	if reason == "" {
		t.Fatalf("expected reason for missing tool")
	}
}
```

internal/validate/report.go
```
package validate

// ValidationReport captures results for all steps.
type ValidationReport struct {
	Steps []StepResult
}
```

internal/validate/types.go
```
package validate

import (
	"strings"

	"github.com/user/oraclepack/internal/dispatch"
	"github.com/user/oraclepack/internal/state"
	"github.com/user/oraclepack/internal/tools"
	"github.com/user/oraclepack/internal/types"
)

// StepResult captures validation output for a step.
type StepResult struct {
	StepID   string
	ToolKind tools.ToolKind
	Status   state.Status
	Error    string
}

// DetectToolKind scans a step for a known tool prefix.
func DetectToolKind(step *types.Step) tools.ToolKind {
	if step == nil {
		return tools.ToolUnknown
	}
	lines := strings.Split(step.Code, "\n")
	for _, line := range lines {
		trimmed := strings.TrimSpace(line)
		if trimmed == "" || strings.HasPrefix(trimmed, "#") {
			continue
		}
		if cls, ok := dispatch.Classify(trimmed); ok {
			return cls.Kind
		}
		break
	}
	return tools.ToolUnknown
}
```

</source_code>

--- oraclepack-mcp-server/README.md ---
# Oraclepack MCP Server

MCP wrapper for the `oraclepack` Go CLI and Taskify helper tools.

## Features

- **CLI Wrapping**: Exposes `validate`, `list`, and `run` commands as MCP tools.
- **Taskify Helpers**: Stage-2 detection/validation and Stage-3 action-pack validation/summarization.
- **Security**: Allowlisted filesystem roots, execution gating, timeouts, and output truncation.
- **Transports**: Supports both `stdio` and `streamable-http`.

## Installation

```bash
pip install -r requirements.txt
```

## Configuration

The server is configured via environment variables:

| Variable | Description | Default |
|----------|-------------|---------|
| `ORACLEPACK_BIN` | Path to the `oraclepack` binary | `oraclepack` |
| `ORACLEPACK_ALLOWED_ROOTS` | Colon-separated list of allowed filesystem roots | Current directory |
| `ORACLEPACK_WORKDIR` | Working directory for execution | Current directory |
| `ORACLEPACK_ENABLE_EXEC` | Set to `1` to enable execution tools | `0` (Disabled) |
| `ORACLEPACK_CHARACTER_LIMIT` | Max characters for tool outputs | `32000` |
| `ORACLEPACK_MAX_READ_BYTES` | Max bytes for file read operations | `65536` (64KB) |

## Connecting to Agents

### Codex (config.toml)

Add this to your `~/.codex/config.toml`:

```toml
[mcp_servers.oraclepack]
command = "python"
args = ["-m", "oraclepack_mcp_server", "--transport", "stdio"]
cwd = "/path/to/oraclepack-mcp-server"
startup_timeout_sec = 60.0

[mcp_servers.oraclepack.env]
ORACLEPACK_BIN = "oraclepack"
ORACLEPACK_ALLOWED_ROOTS = "."
ORACLEPACK_ENABLE_EXEC = "1"
```

### Claude Desktop

Add this to your `claude_desktop_config.json`:

```json
{
  "mcpServers": {
    "oraclepack": {
      "command": "python",
      "args": ["-m", "oraclepack_mcp_server", "--transport", "stdio"],
      "env": {
        "ORACLEPACK_BIN": "/path/to/your/oraclepack",
        "ORACLEPACK_ALLOWED_ROOTS": "/path/to/your/projects",
        "ORACLEPACK_ENABLE_EXEC": "1"
      }
    }
  }
}
```

### Gemini CLI

Add this to your `.mcp.json`:

```json
{
  "mcpServers": {
    "oraclepack": {
      "command": "python",
      "args": ["-m", "oraclepack_mcp_server", "--transport", "stdio"],
      "env": {
        "ORACLEPACK_BIN": "oraclepack",
        "ORACLEPACK_ALLOWED_ROOTS": ".",
        "ORACLEPACK_ENABLE_EXEC": "1"
      }
    }
  }
}
```

## Tools

- `oraclepack_read_file`: Read a file within allowed roots.
- `oraclepack_list_packs`: List available oracle packs (*.md).
- `oraclepack_validate_pack`: Validate an oracle pack.
- `oraclepack_list_steps`: List steps in an oracle pack.
- `oraclepack_run_pack`: Run an oracle pack (requires `ORACLEPACK_ENABLE_EXEC=1`).
- `oraclepack_taskify_detect_stage2`: Detect Stage-2 outputs.
- `oraclepack_taskify_validate_stage2`: Validate Stage-2 directory structure.
- `oraclepack_taskify_validate_action_pack`: Validate Stage-3 action pack structure.
- `oraclepack_taskify_artifacts_summary`: Summarize Stage-3 artifacts.
- `oraclepack_taskify_run_action_pack`: Run a Stage-3 action pack (requires `ORACLEPACK_ENABLE_EXEC=1`).
- `oraclepack_taskify_generate_prompt`: Generate instructions for an agent to run an action pack.

### MCP Inspector

```bash
npx @modelcontextprotocol/inspector --config ./inspector.config.json --server oraclepack
```

Use the payloads in `docs/mcp-inspector-payloads.md` to verify specific tool behaviors.


--- oraclepack-mcp-server/requirements.txt ---
mcp[cli]
pydantic-settings
pydantic>=2.0


--- oraclepack-mcp-server/oraclepack_mcp_server/config.py ---
import os
from pathlib import Path
from typing import List, Union, Any
from pydantic import Field, field_validator
from pydantic_settings import BaseSettings, SettingsConfigDict

class Settings(BaseSettings):
    model_config = SettingsConfigDict(
        env_prefix="ORACLEPACK_",
        env_file=".env",
        extra="ignore"
    )

    bin: str = Field(default="oraclepack", description="Path to the oraclepack binary")
    # Use Union to prevent pydantic-settings from forcing JSON decode
    allowed_roots: Any = Field(
        default_factory=lambda: [Path.cwd()],
        description="List of allowed filesystem roots"
    )
    workdir: Path = Field(default_factory=Path.cwd, description="Working directory for execution")
    enable_exec: bool = Field(default=False, description="Enable execution tools")
    max_read_bytes: int = Field(default=65536, description="Max bytes for file read operations")
    character_limit: int = Field(default=32000, description="Max characters for tool outputs")

    @field_validator("allowed_roots", mode="before")
    @classmethod
    def parse_allowed_roots(cls, v: Any) -> List[Path]:
        if isinstance(v, str):
            # Support both colon and comma separation
            delimiter = ":" if ":" in v else ","
            return [Path(p.strip()) for p in v.split(delimiter) if p.strip()]
        if isinstance(v, list):
            return [Path(p) if isinstance(p, (str, Path)) else p for p in v]
        return v

settings = Settings()


--- oraclepack-mcp-server/oraclepack_mcp_server.egg-info/dependency_links.txt ---



--- oraclepack-mcp-server/oraclepack_mcp_server.egg-info/entry_points.txt ---
[console_scripts]
oraclepack-mcp = oraclepack_mcp_server.__main__:main


--- oraclepack-mcp-server/oraclepack_mcp_server/__init__.py ---


--- oraclepack-mcp-server/oraclepack_mcp_server/__main__.py ---
import argparse
import asyncio
from .server import mcp

def main():
    parser = argparse.ArgumentParser(description="Oraclepack MCP Server")
    parser.add_argument(
        "--transport", 
        choices=["stdio", "streamable-http"], 
        default="stdio",
        help="MCP transport to use (default: stdio)"
    )
    parser.add_argument(
        "--host", 
        default="localhost",
        help="Host to bind for streamable-http (default: localhost)"
    )
    parser.add_argument(
        "--port", 
        type=int, 
        default=8000,
        help="Port to bind for streamable-http (default: 8000)"
    )
    
    args = parser.parse_args()
    
    if args.transport == "stdio":
        mcp.run(transport="stdio")
    elif args.transport == "streamable-http":
        # FastMCP.run(transport="sse") is what maps to streamable-http in python SDK
        mcp.run(transport="sse", host=args.host, port=args.port)

if __name__ == "__main__":
    main()


--- oraclepack-mcp-server/oraclepack_mcp_server/oraclepack_cli.py ---
import asyncio
import time
from dataclasses import dataclass
from typing import List, Optional
from .config import settings

@dataclass
class OraclepackResult:
    ok: bool
    exit_code: int
    duration_s: float
    stdout: str
    stderr: str
    stdout_truncated: bool
    stderr_truncated: bool
    error: Optional[str] = None

def truncate_output(text: str, limit: int) -> tuple[str, bool]:
    """Truncates text to limit and returns (truncated_text, is_truncated)."""
    if len(text) > limit:
        return text[:limit], True
    return text, False

async def run_oraclepack(args: List[str], timeout: float = 3600.0) -> OraclepackResult:
    """
    Runs the oraclepack CLI with the given arguments.
    Handles timeouts and output truncation.
    """
    start_time = time.time()
    
    cmd = [settings.bin] + args
    
    try:
        # Create the subprocess
        process = await asyncio.create_subprocess_exec(
            *cmd,
            stdout=asyncio.subprocess.PIPE,
            stderr=asyncio.subprocess.PIPE,
            cwd=settings.workdir
        )
        
        try:
            # Wait for completion with timeout
            stdout_bytes, stderr_bytes = await asyncio.wait_for(process.communicate(), timeout=timeout)
            exit_code = process.returncode
        except asyncio.TimeoutError:
            # Handle timeout
            process.kill()
            await process.wait() # Ensure process is cleaned up
            stdout_bytes, stderr_bytes = b"", b"Timed out after " + str(timeout).encode() + b"s"
            exit_code = 124 # Standard timeout exit code
            
    except Exception as e:
        duration = time.time() - start_time
        return OraclepackResult(
            ok=False,
            exit_code=-1,
            duration_s=duration,
            stdout="",
            stderr="",
            stdout_truncated=False,
            stderr_truncated=False,
            error=str(e)
        )

    duration = time.time() - start_time
    
    stdout_raw = stdout_bytes.decode("utf-8", errors="replace")
    stderr_raw = stderr_bytes.decode("utf-8", errors="replace")
    
    stdout, stdout_truncated = truncate_output(stdout_raw, settings.character_limit)
    stderr, stderr_truncated = truncate_output(stderr_raw, settings.character_limit)
    
    return OraclepackResult(
        ok=(exit_code == 0),
        exit_code=exit_code,
        duration_s=duration,
        stdout=stdout,
        stderr=stderr,
        stdout_truncated=stdout_truncated,
        stderr_truncated=stderr_truncated
    )


--- oraclepack-mcp-server/oraclepack_mcp_server.egg-info/requires.txt ---
mcp[cli]>=0.1.0
pydantic-settings>=2.0.0
pydantic>=2.0.0


--- oraclepack-mcp-server/oraclepack_mcp_server/server.py ---
import logging
import sys
import os
from mcp.server.fastmcp import FastMCP
from mcp.types import ToolAnnotations
from .config import settings
from . import security
from . import oraclepack_cli
from . import taskify

# Configure logging to stderr to avoid interleaving with stdio transport
logging.basicConfig(
    level=logging.INFO,
    format="%(asctime)s - %(name)s - %(levelname)s - %(message)s",
    stream=sys.stderr
)
logger = logging.getLogger("oraclepack-mcp-server")

# Initialize FastMCP
mcp = FastMCP("Oraclepack")

@mcp.tool(annotations=ToolAnnotations(readOnlyHint=True))
async def oraclepack_read_file(path: str) -> str:
    """
    Reads a file within allowed roots.
    Enforces ORACLEPACK_ALLOWED_ROOTS and ORACLEPACK_MAX_READ_BYTES.
    """
    content, truncated = security.safe_read_file(path)
    if truncated:
        return f"--- TRUNCATED (Max {settings.max_read_bytes} bytes) ---\n{content}"
    return content

@mcp.tool(annotations=ToolAnnotations(readOnlyHint=True))
async def oraclepack_list_packs(directory: str = ".") -> str:
    """Lists available oracle packs (*.md) in a directory."""
    resolved_dir = security.validate_path(directory)
    if not resolved_dir.is_dir():
        return f"Path '{directory}' is not a directory."
    
    packs = list(resolved_dir.glob("*.md"))
    if not packs:
        return f"No oracle packs found in '{directory}'."
    
    return "\n".join([p.name for p in packs])

@mcp.tool(annotations=ToolAnnotations(readOnlyHint=True))
async def oraclepack_validate_pack(pack_path: str) -> str:
    """Validates an oracle pack using the Go CLI."""
    resolved_path = security.validate_path(pack_path)
    result = await oraclepack_cli.run_oraclepack(["validate", str(resolved_path)])
    if not result.ok:
        return f"Validation failed:\n{result.stderr or result.error}"
    return "Pack is valid."

@mcp.tool(annotations=ToolAnnotations(readOnlyHint=True))
async def oraclepack_list_steps(pack_path: str) -> str:
    """Lists steps in an oracle pack."""
    resolved_path = security.validate_path(pack_path)
    result = await oraclepack_cli.run_oraclepack(["list", str(resolved_path)])
    if not result.ok:
        return f"Failed to list steps:\n{result.stderr or result.error}"
    return result.stdout

@mcp.tool(annotations=ToolAnnotations(destructiveHint=True, openWorldHint=True))
async def oraclepack_run_pack(pack_path: str, yes: bool = True, run_all: bool = True) -> str:
    """
    Runs an oracle pack. REQUIRES ORACLEPACK_ENABLE_EXEC=1.
    """
    if not security.is_exec_enabled():
        return "Execution is disabled. Set ORACLEPACK_ENABLE_EXEC=1 to enable."
    
    resolved_path = security.validate_path(pack_path)
    args = ["run", str(resolved_path), "--no-tui"]
    if yes: args.append("--yes")
    if run_all: args.append("--run-all")
    
    result = await oraclepack_cli.run_oraclepack(args)
    
    # Verbose Payload Rendering
    output = [f"# Execution Report: {pack_path}"]
    output.append(f"**Status**: {'✅ SUCCESS' if result.ok else '❌ FAILED'}")
    output.append(f"**Exit Code**: {result.exit_code}")
    output.append(f"**Duration**: {result.duration_s:.2f}s")
    
    if result.error:
        output.append(f"\n### Error\n{result.error}")
    
    if result.stdout:
        output.append("\n### Standard Output")
        output.append(f"```\n{result.stdout}\n```")
        if result.stdout_truncated:
            output.append("*Note: Output was truncated.*")
            
    if result.stderr:
        output.append("\n### Standard Error")
        output.append(f"```\n{result.stderr}\n```")
        if result.stderr_truncated:
            output.append("*Note: Error output was truncated.*")
            
    # Add artifact summary if successful or partially successful
    parent_dir = resolved_path.parent
    summary = taskify.artifacts_summary(parent_dir)
    output.append("\n### Artifacts Summary")
    for name, info in summary["artifacts"].items():
        if info:
            output.append(f"- {name}: FOUND")
        else:
            output.append(f"- {name}: NOT FOUND")
        
    return "\n".join(output)

@mcp.tool(annotations=ToolAnnotations(readOnlyHint=True))
async def oraclepack_taskify_detect_stage2(path: str = "auto") -> str:
    """Detects Stage-2 outputs."""
    out_dir, mode = taskify.detect_stage2(path, os.getcwd())
    if not out_dir:
        return f"Could not detect Stage-2 outputs in mode '{mode}'."
    return f"Detected Stage-2 directory: {out_dir} (Mode: {mode})"

@mcp.tool(annotations=ToolAnnotations(readOnlyHint=True))
async def oraclepack_taskify_validate_stage2(out_dir: str) -> str:
    """Validates Stage-2 directory structure (prefixes 01..20)."""
    resolved_dir = security.validate_path(out_dir)
    result = taskify.validate_stage2_dir(resolved_dir)
    if result.ok:
        return f"Stage-2 directory is valid. Found {len(result.valid_files)} files."
    
    output = ["Stage-2 validation failed:"]
    if result.missing:
        output.append(f"Missing prefixes: {', '.join(result.missing)}")
    if result.ambiguous:
        output.append("Ambiguous prefixes (multiple matches):")
        for pfx, matches in result.ambiguous.items():
            output.append(f"  {pfx}: {', '.join(matches)}")
            
    return "\n".join(output)

@mcp.tool(annotations=ToolAnnotations(readOnlyHint=True))
async def oraclepack_taskify_validate_action_pack(file_path: str) -> str:
    """Validates Stage-3 action pack structure."""
    resolved_path = security.validate_path(file_path)
    result = taskify.validate_action_pack(resolved_path)
    if result.ok:
        return f"Action pack is valid. Detected steps: {', '.join(result.steps)}"
    return f"Action pack validation failed: {result.error}"

@mcp.tool(annotations=ToolAnnotations(readOnlyHint=True))
async def oraclepack_taskify_artifacts_summary(out_dir: str) -> str:
    """Summarizes Stage-3 artifacts."""
    resolved_dir = security.validate_path(out_dir)
    summary = taskify.artifacts_summary(resolved_dir)
    
    output = [f"Artifacts Summary for {summary['out_dir']}:"]
    for name, info in summary["artifacts"].items():
        if info:
            output.append(f"- {name}: FOUND ({info['size']} bytes) at {info['path']}")
        else:
            output.append(f"- {name}: NOT FOUND")
            
    return "\n".join(output)

@mcp.tool(annotations=ToolAnnotations(destructiveHint=True, openWorldHint=True))
async def oraclepack_taskify_run_action_pack(file_path: str) -> str:
    """
    Runs a Stage-3 action pack. REQUIRES ORACLEPACK_ENABLE_EXEC=1.
    """
    # Simply wraps oraclepack_run_pack with action pack defaults
    return await oraclepack_run_pack(file_path, yes=True, run_all=True)

@mcp.tool(annotations=ToolAnnotations(readOnlyHint=True))
async def oraclepack_taskify_generate_prompt(file_path: str) -> str:
    """Generates instructions for an agent to run an action pack."""
    resolved_path = security.validate_path(file_path)
    result = taskify.validate_action_pack(resolved_path)
    if not result.ok:
        return f"Validation failed: {result.error}"
    return taskify.generate_agent_prompt(file_path, result.steps)


--- skills/oraclepack-codebase-pack-grouped_skill.md ---
<filetree>
Project Structure:
└── skills
    └── oraclepack-codebase-pack-grouped
        ├── references
        │   ├── attachment-minimization.md
        │   ├── codebase-grouping.md
        │   └── codebase-pack-template.md
        ├── scripts
        │   ├── generate_grouped_packs.py
        │   ├── lint_attachments.py
        │   └── validate_pack.py
        └── SKILL.md

</filetree>

<source_code>
skills/oraclepack-codebase-pack-grouped/SKILL.md
```
---
name: oraclepack-codebase-pack-grouped
description: Generate multiple runner-ingestible oraclepack Stage-1 packs grouped by codebase topic/domain (subdir + deterministic inference) with direct code attachments. Use when the user wants per-topic/per-domain mini-packs for a target repo/project/codebase instead of ticket folders, with strict 20-step schema and validation.
---

# oraclepack-codebase-pack-grouped (Stage 1)

## Goal

Produce **multiple** codebase-driven Stage-1 packs, one per inferred topic/domain, with direct code attachments. Each pack is schema-safe and self-contained.

## Use this skill

Use when the user wants separate packs per topic/domain grouped by a target repo/project/codebase, not a `.tickets/` folder.

## Inputs (parse trailing KEY=value; last-one-wins)

Supported keys (defaults in parentheses):
- `codebase_name` (`Unknown`)
- `out_dir` (`docs/oracle-questions-YYYY-MM-DD`)
- `oracle_cmd` (`oracle`)
- `oracle_flags` (`--files-report`)
- `extra_files` (empty; appended literally)
- `code_root` (`.`)
- `code_glob` (`**/*`)
- `code_paths` (empty; comma-separated explicit files; if present, ignore glob)
- `code_max_files` (`200`)
- `group_mode` (`subdir+infer`)
- `group_min_score` (`0.10`)
- `group_max_files` (`200`)
- `group_max_chars` (`200000`)
- `ignore_dirs` (empty; comma-separated; merged with defaults)
- `include_exts` (empty; uses default extension allowlist)
- `exclude_glob` (empty; comma-separated glob patterns)
- `mode` (`codebase-grouped-direct`)

Notes:
- `YYYY-MM-DD` is computed at pack generation time for default `out_dir`.
- If oracle flag support is uncertain, omit unsupported flags; never invent flags.

## Workflow (deterministic)

1) Read:
- `references/codebase-grouping.md`
- `references/attachment-minimization.md`
- `references/codebase-pack-template.md`

2) Ask user if custom args are needed (numbered picker):

```
1) Use defaults (no args)
2) Provide custom args
```

If `2`, ask for KEY=value args and run with those; otherwise run with defaults.

3) Generate packs (deterministic grouping + per-group pack files):

```bash
python3 /home/user/.codex/skills/oraclepack-codebase-pack-grouped/scripts/generate_grouped_packs.py \
  codebase_name=oraclepack \
  out_dir=docs/oracle-questions-2026-01-08
```

Outputs:
- `{{out_dir}}/packs/*.md` (one pack per group/part)
- `{{out_dir}}/_groups.json` (group -> file list)

4) Size control (mandatory; fail fast):
- Run `oracle --dry-run summary --files-report ...` for the **largest** group pack (or each pack if unsure).
- Enforce caps:
  - browser: ≤ 60,000 tokens total input per step
  - api: ≤ 180,000 tokens total input per step
- If exceeded, reduce via `group_max_files`, `code_max_files`, or `include_exts`.

5) Validate every pack (mandatory):

```bash
python3 /home/user/.codex/skills/oraclepack-codebase-pack-grouped/scripts/validate_pack.py <pack.md>
python3 /home/user/.codex/skills/oraclepack-codebase-pack-grouped/scripts/lint_attachments.py <pack.md>
```

## Failure behavior

- If no files resolve, packs still generate with empty attachments.
- Step 01 prompt must request exact missing file/path pattern(s).

## Output contract

Each pack MUST:
- Have exactly one `bash` fence
- Have exactly 20 steps (01..20)
- Include ROI header tokens
- Include `--write-output` with a group-specific `out_dir`
- Attach code files directly via `${code_args[@]}`
- End with Coverage check outside the bash fence
```

skills/oraclepack-codebase-pack-grouped/references/attachment-minimization.md
```
# Attachment minimization rules (Codebase Stage 1 — Direct Attach)

Objective: keep each group pack focused and portable.

## Code attachments

- Code files are attached directly in each step via `${code_args[@]}`.
- Use `group_max_files` (default 200) to bound per-pack file count.
- If a group is larger than the cap, split into multiple packs (part 1..N).
- Prefer code_glob + include_exts to avoid irrelevant files.

## Non-code attachments (extra_files)

- Keep explicit non-code attachments to **0–1 per step**.
- Prefer a single high-signal file (e.g., README, architecture doc).

## extra_files (literal append)

- If `extra_files` is provided, append it literally to every oracle command.
- It may include additional `-f/--file` flags.
- Place `extra_files` on its own line with a comment:
  - `# extra_files appended literally`
```

skills/oraclepack-codebase-pack-grouped/references/codebase-grouping.md
```
# Codebase grouping rules (Stage 1 — Direct Attach)

Objective: deterministically split a target codebase into topic/domain groups and produce one Stage-1 pack per group.

## Grouping behavior

- Primary grouping: by top-level subdirectory under `code_root`.
- Loose files (root-level or outside `code_root`) are assigned via token overlap (Jaccard) against existing groups.
- If no group scores above `group_min_score`, loose files fall into a `root` group.

## Determinism

- File discovery is lexicographically sorted.
- Group names are derived from directory names; sharded parts are `group_name part N`.
- Group slug is a normalized lowercase `a-z0-9-` token.

## Limits

- `code_max_files` caps total discovered files before grouping.
- `group_max_files` and `group_max_chars` cap each group pack; groups split into part 1..N.

## Exclusions

- Ignore directories include `.git`, `node_modules`, `dist`, `build`, `.venv`, and other common build outputs.
- Additional ignore names can be provided via `ignore_dirs` (comma-separated).
- Use `exclude_glob` to drop specific paths.
```

skills/oraclepack-codebase-pack-grouped/references/codebase-pack-template.md
```
# Oracle Pack — {{codebase_name}} (Grouped Codebase Stage 1 — Direct Attach)

## Parsed args
- codebase_name: {{codebase_name}}
- out_dir: {{out_dir}}
- oracle_cmd: {{oracle_cmd}}
- oracle_flags: {{oracle_flags}}
- extra_files: {{extra_files}}
- code_root: {{code_root}}
- code_glob: {{code_glob}}
- code_paths: {{code_paths}}
- code_max_files: {{code_max_files}}
- group_name: {{group_name}}
- group_slug: {{group_slug}}
- group_mode: {{group_mode}}
- group_min_score: {{group_min_score}}
- group_max_files: {{group_max_files}}
- group_max_chars: {{group_max_chars}}
- ignore_dirs: {{ignore_dirs}}
- include_exts: {{include_exts}}
- exclude_glob: {{exclude_glob}}
- mode: {{mode}}

Notes (contract):
- Exactly one fenced `bash` block in this document.
- No other ``` fences anywhere.
- Exactly 20 steps, numbered 01..20 in order.
- Step header: `# NN) ROI=... impact=... confidence=... effort=... horizon=... category=... reference=...`
- Every step includes: `--write-output "{{out_dir}}/NN-<slug>.md"` (double quotes required).
- Steps must be self-contained and must not rely on shell variables created in previous steps.
- Each step must attach code files directly (no bundle dependency).
- Pack ends with a Coverage check section listing all 10 categories as OK or Missing(<step ids>).

```bash
set -euo pipefail

mkdir -p "{{out_dir}}"

# 01) ROI=4.8 impact=6 confidence=0.80 effort=1 horizon=Immediate category=contracts/interfaces reference={{group_slug}}

# code files attached directly (deterministic; self-contained)
mapfile -t __code_files < <(python3 - <<'PY'
from __future__ import annotations
import json

files = json.loads(r'''{{group_files_json}}''')
for p in files:
    print(p)
PY
)

code_args=()
for p in "${__code_files[@]}"; do
  code_args+=(-f "$p")
done

if [ "${#code_args[@]}" -eq 0 ]; then
  echo "WARNING: no code files resolved for group '{{group_name}}'." >&2
fi

# extra_files appended literally (may be empty; may include -f/--file):
{{oracle_cmd}}   {{oracle_flags}}   --write-output "{{out_dir}}/01-contracts-interfaces-public-surface.md"   "${code_args[@]}"   {{extra_files}}   -p "$(cat <<'PROMPT'
Strategist question #01  (codebase-driven, group: {{group_name}})

Reference: {{group_slug}}
Category: contracts/interfaces
Horizon: Immediate
ROI: 4.8 (impact=6, confidence=0.80, effort=1)

Question:
Using the attached code files as primary evidence, map the public surface area (CLI/TUI/API/interfaces/contracts). Call out backwards-compat constraints.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 02) ROI=4.6 impact=6 confidence=0.78 effort=1 horizon=Immediate category=contracts/interfaces reference={{group_slug}}

# code files attached directly (deterministic; self-contained)
mapfile -t __code_files < <(python3 - <<'PY'
from __future__ import annotations
import json

files = json.loads(r'''{{group_files_json}}''')
for p in files:
    print(p)
PY
)

code_args=()
for p in "${__code_files[@]}"; do
  code_args+=(-f "$p")
done

if [ "${#code_args[@]}" -eq 0 ]; then
  echo "WARNING: no code files resolved for group '{{group_name}}'." >&2
fi

# extra_files appended literally (may be empty; may include -f/--file):
{{oracle_cmd}}   {{oracle_flags}}   --write-output "{{out_dir}}/02-contracts-interfaces-integrations.md"   "${code_args[@]}"   {{extra_files}}   -p "$(cat <<'PROMPT'
Strategist question #02  (codebase-driven, group: {{group_name}})

Reference: {{group_slug}}
Category: contracts/interfaces
Horizon: Immediate
ROI: 4.6 (impact=6, confidence=0.78, effort=1)

Question:
Using the attached code files as primary evidence, identify external integrations implied by this area; required config/contract changes; failure/timeout behavior; minimal compat-safe rollout.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 03) ROI=5.1 impact=7 confidence=0.74 effort=1 horizon=NearTerm category=invariants reference={{group_slug}}

# code files attached directly (deterministic; self-contained)
mapfile -t __code_files < <(python3 - <<'PY'
from __future__ import annotations
import json

files = json.loads(r'''{{group_files_json}}''')
for p in files:
    print(p)
PY
)

code_args=()
for p in "${__code_files[@]}"; do
  code_args+=(-f "$p")
done

if [ "${#code_args[@]}" -eq 0 ]; then
  echo "WARNING: no code files resolved for group '{{group_name}}'." >&2
fi

# extra_files appended literally (may be empty; may include -f/--file):
{{oracle_cmd}}   {{oracle_flags}}   --write-output "{{out_dir}}/03-invariants-invariant-map.md"   "${code_args[@]}"   {{extra_files}}   -p "$(cat <<'PROMPT'
Strategist question #03  (codebase-driven, group: {{group_name}})

Reference: {{group_slug}}
Category: invariants
Horizon: NearTerm
ROI: 5.1 (impact=7, confidence=0.74, effort=1)

Question:
Using the attached code files, map invariants and critical assumptions (data shape, ordering, idempotency, contracts). Identify the weakest or least-tested invariant.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 04) ROI=4.7 impact=6 confidence=0.76 effort=1 horizon=NearTerm category=caching/state reference={{group_slug}}

# code files attached directly (deterministic; self-contained)
mapfile -t __code_files < <(python3 - <<'PY'
from __future__ import annotations
import json

files = json.loads(r'''{{group_files_json}}''')
for p in files:
    print(p)
PY
)

code_args=()
for p in "${__code_files[@]}"; do
  code_args+=(-f "$p")
done

if [ "${#code_args[@]}" -eq 0 ]; then
  echo "WARNING: no code files resolved for group '{{group_name}}'." >&2
fi

# extra_files appended literally (may be empty; may include -f/--file):
{{oracle_cmd}}   {{oracle_flags}}   --write-output "{{out_dir}}/04-caching-state-reads-writes.md"   "${code_args[@]}"   {{extra_files}}   -p "$(cat <<'PROMPT'
Strategist question #04  (codebase-driven, group: {{group_name}})

Reference: {{group_slug}}
Category: caching/state
Horizon: NearTerm
ROI: 4.7 (impact=6, confidence=0.76, effort=1)

Question:
Using the attached code files, identify stateful reads/writes and any caches (in-memory, disk, external). Note invalidation boundaries and any silent staleness risks.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 05) ROI=4.4 impact=6 confidence=0.70 effort=1 horizon=NearTerm category=background jobs reference={{group_slug}}

# code files attached directly (deterministic; self-contained)
mapfile -t __code_files < <(python3 - <<'PY'
from __future__ import annotations
import json

files = json.loads(r'''{{group_files_json}}''')
for p in files:
    print(p)
PY
)

code_args=()
for p in "${__code_files[@]}"; do
  code_args+=(-f "$p")
done

if [ "${#code_args[@]}" -eq 0 ]; then
  echo "WARNING: no code files resolved for group '{{group_name}}'." >&2
fi

# extra_files appended literally (may be empty; may include -f/--file):
{{oracle_cmd}}   {{oracle_flags}}   --write-output "{{out_dir}}/05-background-jobs-queues.md"   "${code_args[@]}"   {{extra_files}}   -p "$(cat <<'PROMPT'
Strategist question #05  (codebase-driven, group: {{group_name}})

Reference: {{group_slug}}
Category: background jobs
Horizon: NearTerm
ROI: 4.4 (impact=6, confidence=0.70, effort=1)

Question:
Using the attached code files, list any background jobs/queues/cron tasks. Note retries, idempotency, and failure modes.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 06) ROI=4.5 impact=6 confidence=0.75 effort=1 horizon=Immediate category=observability reference={{group_slug}}

# code files attached directly (deterministic; self-contained)
mapfile -t __code_files < <(python3 - <<'PY'
from __future__ import annotations
import json

files = json.loads(r'''{{group_files_json}}''')
for p in files:
    print(p)
PY
)

code_args=()
for p in "${__code_files[@]}"; do
  code_args+=(-f "$p")
done

if [ "${#code_args[@]}" -eq 0 ]; then
  echo "WARNING: no code files resolved for group '{{group_name}}'." >&2
fi

# extra_files appended literally (may be empty; may include -f/--file):
{{oracle_cmd}}   {{oracle_flags}}   --write-output "{{out_dir}}/06-observability-logging-metrics.md"   "${code_args[@]}"   {{extra_files}}   -p "$(cat <<'PROMPT'
Strategist question #06  (codebase-driven, group: {{group_name}})

Reference: {{group_slug}}
Category: observability
Horizon: Immediate
ROI: 4.5 (impact=6, confidence=0.75, effort=1)

Question:
Using the attached code files, identify logging/metrics/tracing in this area. Call out missing signals for debugging incidents.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 07) ROI=4.3 impact=6 confidence=0.68 effort=1 horizon=NearTerm category=permissions reference={{group_slug}}

# code files attached directly (deterministic; self-contained)
mapfile -t __code_files < <(python3 - <<'PY'
from __future__ import annotations
import json

files = json.loads(r'''{{group_files_json}}''')
for p in files:
    print(p)
PY
)

code_args=()
for p in "${__code_files[@]}"; do
  code_args+=(-f "$p")
done

if [ "${#code_args[@]}" -eq 0 ]; then
  echo "WARNING: no code files resolved for group '{{group_name}}'." >&2
fi

# extra_files appended literally (may be empty; may include -f/--file):
{{oracle_cmd}}   {{oracle_flags}}   --write-output "{{out_dir}}/07-permissions-authz.md"   "${code_args[@]}"   {{extra_files}}   -p "$(cat <<'PROMPT'
Strategist question #07  (codebase-driven, group: {{group_name}})

Reference: {{group_slug}}
Category: permissions
Horizon: NearTerm
ROI: 4.3 (impact=6, confidence=0.68, effort=1)

Question:
Using the attached code files, identify authorization and permission checks. Note any missing checks or implicit trust boundaries.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 08) ROI=4.2 impact=6 confidence=0.66 effort=1 horizon=NearTerm category=migrations reference={{group_slug}}

# code files attached directly (deterministic; self-contained)
mapfile -t __code_files < <(python3 - <<'PY'
from __future__ import annotations
import json

files = json.loads(r'''{{group_files_json}}''')
for p in files:
    print(p)
PY
)

code_args=()
for p in "${__code_files[@]}"; do
  code_args+=(-f "$p")
done

if [ "${#code_args[@]}" -eq 0 ]; then
  echo "WARNING: no code files resolved for group '{{group_name}}'." >&2
fi

# extra_files appended literally (may be empty; may include -f/--file):
{{oracle_cmd}}   {{oracle_flags}}   --write-output "{{out_dir}}/08-migrations-backfills.md"   "${code_args[@]}"   {{extra_files}}   -p "$(cat <<'PROMPT'
Strategist question #08  (codebase-driven, group: {{group_name}})

Reference: {{group_slug}}
Category: migrations
Horizon: NearTerm
ROI: 4.2 (impact=6, confidence=0.66, effort=1)

Question:
Using the attached code files, identify migrations/backfills/data-shape changes implied in this area. Note rollout risks.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 09) ROI=4.1 impact=6 confidence=0.64 effort=1 horizon=NearTerm category=UX flows reference={{group_slug}}

# code files attached directly (deterministic; self-contained)
mapfile -t __code_files < <(python3 - <<'PY'
from __future__ import annotations
import json

files = json.loads(r'''{{group_files_json}}''')
for p in files:
    print(p)
PY
)

code_args=()
for p in "${__code_files[@]}"; do
  code_args+=(-f "$p")
done

if [ "${#code_args[@]}" -eq 0 ]; then
  echo "WARNING: no code files resolved for group '{{group_name}}'." >&2
fi

# extra_files appended literally (may be empty; may include -f/--file):
{{oracle_cmd}}   {{oracle_flags}}   --write-output "{{out_dir}}/09-ux-flows.md"   "${code_args[@]}"   {{extra_files}}   -p "$(cat <<'PROMPT'
Strategist question #09  (codebase-driven, group: {{group_name}})

Reference: {{group_slug}}
Category: UX flows
Horizon: NearTerm
ROI: 4.1 (impact=6, confidence=0.64, effort=1)

Question:
Using the attached code files, describe the main user flows in this area. Note any fragile or confusing steps.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 10) ROI=4.0 impact=6 confidence=0.62 effort=1 horizon=NearTerm category=failure modes reference={{group_slug}}

# code files attached directly (deterministic; self-contained)
mapfile -t __code_files < <(python3 - <<'PY'
from __future__ import annotations
import json

files = json.loads(r'''{{group_files_json}}''')
for p in files:
    print(p)
PY
)

code_args=()
for p in "${__code_files[@]}"; do
  code_args+=(-f "$p")
done

if [ "${#code_args[@]}" -eq 0 ]; then
  echo "WARNING: no code files resolved for group '{{group_name}}'." >&2
fi

# extra_files appended literally (may be empty; may include -f/--file):
{{oracle_cmd}}   {{oracle_flags}}   --write-output "{{out_dir}}/10-failure-modes.md"   "${code_args[@]}"   {{extra_files}}   -p "$(cat <<'PROMPT'
Strategist question #10  (codebase-driven, group: {{group_name}})

Reference: {{group_slug}}
Category: failure modes
Horizon: NearTerm
ROI: 4.0 (impact=6, confidence=0.62, effort=1)

Question:
Using the attached code files, enumerate likely failure modes (network, data, validation, retries). Note missing handling.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 11) ROI=3.8 impact=6 confidence=0.60 effort=1 horizon=NearTerm category=feature flags reference={{group_slug}}

# code files attached directly (deterministic; self-contained)
mapfile -t __code_files < <(python3 - <<'PY'
from __future__ import annotations
import json

files = json.loads(r'''{{group_files_json}}''')
for p in files:
    print(p)
PY
)

code_args=()
for p in "${__code_files[@]}"; do
  code_args+=(-f "$p")
done

if [ "${#code_args[@]}" -eq 0 ]; then
  echo "WARNING: no code files resolved for group '{{group_name}}'." >&2
fi

# extra_files appended literally (may be empty; may include -f/--file):
{{oracle_cmd}}   {{oracle_flags}}   --write-output "{{out_dir}}/11-feature-flags.md"   "${code_args[@]}"   {{extra_files}}   -p "$(cat <<'PROMPT'
Strategist question #11  (codebase-driven, group: {{group_name}})

Reference: {{group_slug}}
Category: feature flags
Horizon: NearTerm
ROI: 3.8 (impact=6, confidence=0.60, effort=1)

Question:
Using the attached code files, identify any feature flags or config toggles. Note rollout/rollback behavior and gaps.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 12) ROI=3.9 impact=6 confidence=0.62 effort=1 horizon=NearTerm category=caching/state reference={{group_slug}}

# code files attached directly (deterministic; self-contained)
mapfile -t __code_files < <(python3 - <<'PY'
from __future__ import annotations
import json

files = json.loads(r'''{{group_files_json}}''')
for p in files:
    print(p)
PY
)

code_args=()
for p in "${__code_files[@]}"; do
  code_args+=(-f "$p")
done

if [ "${#code_args[@]}" -eq 0 ]; then
  echo "WARNING: no code files resolved for group '{{group_name}}'." >&2
fi

# extra_files appended literally (may be empty; may include -f/--file):
{{oracle_cmd}}   {{oracle_flags}}   --write-output "{{out_dir}}/12-caching-state-consistency.md"   "${code_args[@]}"   {{extra_files}}   -p "$(cat <<'PROMPT'
Strategist question #12  (codebase-driven, group: {{group_name}})

Reference: {{group_slug}}
Category: caching/state
Horizon: NearTerm
ROI: 3.9 (impact=6, confidence=0.62, effort=1)

Question:
Using the attached code files, identify consistency boundaries (read-after-write, eventual vs strong). Note any mismatches across layers.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 13) ROI=3.7 impact=6 confidence=0.58 effort=1 horizon=MidTerm category=observability reference={{group_slug}}

# code files attached directly (deterministic; self-contained)
mapfile -t __code_files < <(python3 - <<'PY'
from __future__ import annotations
import json

files = json.loads(r'''{{group_files_json}}''')
for p in files:
    print(p)
PY
)

code_args=()
for p in "${__code_files[@]}"; do
  code_args+=(-f "$p")
done

if [ "${#code_args[@]}" -eq 0 ]; then
  echo "WARNING: no code files resolved for group '{{group_name}}'." >&2
fi

# extra_files appended literally (may be empty; may include -f/--file):
{{oracle_cmd}}   {{oracle_flags}}   --write-output "{{out_dir}}/13-observability-gaps.md"   "${code_args[@]}"   {{extra_files}}   -p "$(cat <<'PROMPT'
Strategist question #13  (codebase-driven, group: {{group_name}})

Reference: {{group_slug}}
Category: observability
Horizon: MidTerm
ROI: 3.7 (impact=6, confidence=0.58, effort=1)

Question:
Using the attached code files, identify observability gaps that will block triage or SLA guarantees.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 14) ROI=3.6 impact=6 confidence=0.56 effort=1 horizon=MidTerm category=permissions reference={{group_slug}}

# code files attached directly (deterministic; self-contained)
mapfile -t __code_files < <(python3 - <<'PY'
from __future__ import annotations
import json

files = json.loads(r'''{{group_files_json}}''')
for p in files:
    print(p)
PY
)

code_args=()
for p in "${__code_files[@]}"; do
  code_args+=(-f "$p")
done

if [ "${#code_args[@]}" -eq 0 ]; then
  echo "WARNING: no code files resolved for group '{{group_name}}'." >&2
fi

# extra_files appended literally (may be empty; may include -f/--file):
{{oracle_cmd}}   {{oracle_flags}}   --write-output "{{out_dir}}/14-permissions-gaps.md"   "${code_args[@]}"   {{extra_files}}   -p "$(cat <<'PROMPT'
Strategist question #14  (codebase-driven, group: {{group_name}})

Reference: {{group_slug}}
Category: permissions
Horizon: MidTerm
ROI: 3.6 (impact=6, confidence=0.56, effort=1)

Question:
Using the attached code files, identify authorization edge cases or privilege escalations to test.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 15) ROI=3.5 impact=6 confidence=0.54 effort=1 horizon=MidTerm category=migrations reference={{group_slug}}

# code files attached directly (deterministic; self-contained)
mapfile -t __code_files < <(python3 - <<'PY'
from __future__ import annotations
import json

files = json.loads(r'''{{group_files_json}}''')
for p in files:
    print(p)
PY
)

code_args=()
for p in "${__code_files[@]}"; do
  code_args+=(-f "$p")
done

if [ "${#code_args[@]}" -eq 0 ]; then
  echo "WARNING: no code files resolved for group '{{group_name}}'." >&2
fi

# extra_files appended literally (may be empty; may include -f/--file):
{{oracle_cmd}}   {{oracle_flags}}   --write-output "{{out_dir}}/15-migrations-risk.md"   "${code_args[@]}"   {{extra_files}}   -p "$(cat <<'PROMPT'
Strategist question #15  (codebase-driven, group: {{group_name}})

Reference: {{group_slug}}
Category: migrations
Horizon: MidTerm
ROI: 3.5 (impact=6, confidence=0.54, effort=1)

Question:
Using the attached code files, identify any migration risks, data backfill triggers, or state shape changes that require careful sequencing.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 16) ROI=3.4 impact=6 confidence=0.52 effort=1 horizon=MidTerm category=UX flows reference={{group_slug}}

# code files attached directly (deterministic; self-contained)
mapfile -t __code_files < <(python3 - <<'PY'
from __future__ import annotations
import json

files = json.loads(r'''{{group_files_json}}''')
for p in files:
    print(p)
PY
)

code_args=()
for p in "${__code_files[@]}"; do
  code_args+=(-f "$p")
done

if [ "${#code_args[@]}" -eq 0 ]; then
  echo "WARNING: no code files resolved for group '{{group_name}}'." >&2
fi

# extra_files appended literally (may be empty; may include -f/--file):
{{oracle_cmd}}   {{oracle_flags}}   --write-output "{{out_dir}}/16-ux-flow-gaps.md"   "${code_args[@]}"   {{extra_files}}   -p "$(cat <<'PROMPT'
Strategist question #16  (codebase-driven, group: {{group_name}})

Reference: {{group_slug}}
Category: UX flows
Horizon: MidTerm
ROI: 3.4 (impact=6, confidence=0.52, effort=1)

Question:
Using the attached code files, identify UX or developer flow bottlenecks; propose smallest flow test to validate.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 17) ROI=3.3 impact=6 confidence=0.50 effort=1 horizon=MidTerm category=failure modes reference={{group_slug}}

# code files attached directly (deterministic; self-contained)
mapfile -t __code_files < <(python3 - <<'PY'
from __future__ import annotations
import json

files = json.loads(r'''{{group_files_json}}''')
for p in files:
    print(p)
PY
)

code_args=()
for p in "${__code_files[@]}"; do
  code_args+=(-f "$p")
done

if [ "${#code_args[@]}" -eq 0 ]; then
  echo "WARNING: no code files resolved for group '{{group_name}}'." >&2
fi

# extra_files appended literally (may be empty; may include -f/--file):
{{oracle_cmd}}   {{oracle_flags}}   --write-output "{{out_dir}}/17-failure-modes-debt.md"   "${code_args[@]}"   {{extra_files}}   -p "$(cat <<'PROMPT'
Strategist question #17  (codebase-driven, group: {{group_name}})

Reference: {{group_slug}}
Category: failure modes
Horizon: MidTerm
ROI: 3.3 (impact=6, confidence=0.50, effort=1)

Question:
Using the attached code files, list failure handling debt or missing retries/rollbacks and rank by user impact.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 18) ROI=3.2 impact=6 confidence=0.48 effort=1 horizon=LongTerm category=feature flags reference={{group_slug}}

# code files attached directly (deterministic; self-contained)
mapfile -t __code_files < <(python3 - <<'PY'
from __future__ import annotations
import json

files = json.loads(r'''{{group_files_json}}''')
for p in files:
    print(p)
PY
)

code_args=()
for p in "${__code_files[@]}"; do
  code_args+=(-f "$p")
done

if [ "${#code_args[@]}" -eq 0 ]; then
  echo "WARNING: no code files resolved for group '{{group_name}}'." >&2
fi

# extra_files appended literally (may be empty; may include -f/--file):
{{oracle_cmd}}   {{oracle_flags}}   --write-output "{{out_dir}}/18-feature-flags-roadmap.md"   "${code_args[@]}"   {{extra_files}}   -p "$(cat <<'PROMPT'
Strategist question #18  (codebase-driven, group: {{group_name}})

Reference: {{group_slug}}
Category: feature flags
Horizon: LongTerm
ROI: 3.2 (impact=6, confidence=0.48, effort=1)

Question:
Using the attached code files, identify where staged rollouts or flags should exist but do not.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 19) ROI=3.1 impact=6 confidence=0.46 effort=1 horizon=LongTerm category=background jobs reference={{group_slug}}

# code files attached directly (deterministic; self-contained)
mapfile -t __code_files < <(python3 - <<'PY'
from __future__ import annotations
import json

files = json.loads(r'''{{group_files_json}}''')
for p in files:
    print(p)
PY
)

code_args=()
for p in "${__code_files[@]}"; do
  code_args+=(-f "$p")
done

if [ "${#code_args[@]}" -eq 0 ]; then
  echo "WARNING: no code files resolved for group '{{group_name}}'." >&2
fi

# extra_files appended literally (may be empty; may include -f/--file):
{{oracle_cmd}}   {{oracle_flags}}   --write-output "{{out_dir}}/19-background-jobs-scale.md"   "${code_args[@]}"   {{extra_files}}   -p "$(cat <<'PROMPT'
Strategist question #19  (codebase-driven, group: {{group_name}})

Reference: {{group_slug}}
Category: background jobs
Horizon: LongTerm
ROI: 3.1 (impact=6, confidence=0.46, effort=1)

Question:
Using the attached code files, identify long-term scaling risks in background processing or async pipelines.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 20) ROI=3.0 impact=6 confidence=0.44 effort=1 horizon=LongTerm category=contracts/interfaces reference={{group_slug}}

# code files attached directly (deterministic; self-contained)
mapfile -t __code_files < <(python3 - <<'PY'
from __future__ import annotations
import json

files = json.loads(r'''{{group_files_json}}''')
for p in files:
    print(p)
PY
)

code_args=()
for p in "${__code_files[@]}"; do
  code_args+=(-f "$p")
done

if [ "${#code_args[@]}" -eq 0 ]; then
  echo "WARNING: no code files resolved for group '{{group_name}}'." >&2
fi

# extra_files appended literally (may be empty; may include -f/--file):
{{oracle_cmd}}   {{oracle_flags}}   --write-output "{{out_dir}}/20-contracts-interfaces-roadmap.md"   "${code_args[@]}"   {{extra_files}}   -p "$(cat <<'PROMPT'
Strategist question #20  (codebase-driven, group: {{group_name}})

Reference: {{group_slug}}
Category: contracts/interfaces
Horizon: LongTerm
ROI: 3.0 (impact=6, confidence=0.44, effort=1)

Question:
Using the attached code files, identify longer-term public surface changes likely needed in this area.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"
```

Coverage check:
- contracts/interfaces: OK (01,02,20)
- invariants: OK (03)
- caching/state: OK (04,12)
- background jobs: OK (05,19)
- observability: OK (06,13)
- permissions: OK (07,14)
- migrations: OK (08,15)
- UX flows: OK (09,16)
- failure modes: OK (10,17)
- feature flags: OK (11,18)
```

skills/oraclepack-codebase-pack-grouped/scripts/generate_grouped_packs.py
```
#!/usr/bin/env python3
from __future__ import annotations

import datetime as _dt
import fnmatch
import json
import re
import sys
from pathlib import Path
from typing import Dict, Iterable, List, Tuple

STOPWORDS = {
    "the", "and", "for", "with", "from", "this", "that", "into", "over", "under", "when",
    "then", "than", "else", "only", "must", "should", "could", "would", "will", "shall",
    "repo", "repos", "code", "codebase", "project", "oraclepack", "oracle", "pack", "packs",
}

DEFAULT_IGNORE_DIRS = {
    ".git",
    ".hg",
    ".svn",
    "node_modules",
    "dist",
    "build",
    ".next",
    ".venv",
    "venv",
    "coverage",
    "target",
}

DEFAULT_INCLUDE_EXTS = {
    ".py", ".ts", ".tsx", ".js", ".jsx", ".go", ".rs", ".java", ".kt", ".cpp", ".c",
    ".h", ".hpp", ".cs", ".rb", ".php", ".swift", ".scala", ".sql", ".md", ".yaml",
    ".yml", ".json", ".toml", ".ini", ".sh", ".ps1", ".tf", ".proto",
}


def _parse_kv_args(argv: List[str]) -> Dict[str, str]:
    args: Dict[str, str] = {}
    for raw in argv:
        if "=" not in raw:
            continue
        k, v = raw.split("=", 1)
        args[k.strip()] = v.strip()
    return args


def _today() -> str:
    return _dt.date.today().isoformat()


def _slugify(s: str) -> str:
    s = s.strip().lower()
    s = re.sub(r"[^a-z0-9]+", "-", s)
    s = re.sub(r"-+", "-", s).strip("-")
    return s or "group"


def _tokenize(text: str) -> List[str]:
    text = text.lower()
    text = re.sub(r"[^a-z0-9]+", " ", text)
    toks = [t for t in text.split() if len(t) >= 3 and t not in STOPWORDS]
    return toks


def _group_by_subdir(paths: Iterable[Path], code_root: str) -> Tuple[Dict[str, List[Path]], List[Path]]:
    root = Path(code_root).resolve()
    groups: Dict[str, List[Path]] = {}
    loose: List[Path] = []
    for p in paths:
        try:
            rel = p.resolve().relative_to(root)
        except ValueError:
            loose.append(p)
            continue
        if len(rel.parts) >= 2:
            key = rel.parts[0]
            groups.setdefault(key, []).append(p)
        else:
            loose.append(p)
    return groups, loose


def _group_tokens(group_name: str, paths: Iterable[Path]) -> set:
    tokens = set(_tokenize(group_name))
    for p in paths:
        tokens.update(_tokenize(p.stem))
    return tokens


def _file_tokens(p: Path) -> set:
    toks = set(_tokenize(p.stem))
    toks.update(_tokenize(str(p.parent.name)))
    return toks


def _jaccard(a: set, b: set) -> float:
    if not a or not b:
        return 0.0
    inter = a.intersection(b)
    union = a.union(b)
    return float(len(inter)) / float(len(union))


def _collect_paths(
    code_root: str,
    code_glob: str,
    code_paths: str,
    include_exts: str,
    exclude_glob: str,
    ignore_dirs: str,
) -> List[Path]:
    if code_paths:
        parts = [p.strip() for p in code_paths.split(",") if p.strip()]
        return [Path(p) for p in parts]

    root = Path(code_root)
    if not root.exists():
        return []

    ignore = {p.strip() for p in ignore_dirs.split(",") if p.strip()}
    ignore = ignore.union(DEFAULT_IGNORE_DIRS)

    include = {e.strip().lower() for e in include_exts.split(",") if e.strip()}
    if not include_exts.strip():
        include = set(DEFAULT_INCLUDE_EXTS)

    excludes = [g.strip() for g in exclude_glob.split(",") if g.strip()]

    out: List[Path] = []
    for p in root.glob(code_glob):
        if p.is_dir():
            continue
        parts = set(p.parts)
        if parts.intersection(ignore):
            continue
        if excludes and any(fnmatch.fnmatch(str(p), g) for g in excludes):
            continue
        if include:
            ext = p.suffix.lower()
            if ext not in include:
                continue
        out.append(p)

    return out


def _cap_group(paths: List[Path], max_files: int, max_chars: int) -> List[List[Path]]:
    chunks: List[List[Path]] = []
    current: List[Path] = []
    size = 0

    for p in paths:
        p_size = 0
        try:
            p_size = p.stat().st_size
        except FileNotFoundError:
            p_size = 0

        if max_files and len(current) >= max_files:
            chunks.append(current)
            current = []
            size = 0

        if max_chars and current and size + p_size > max_chars:
            chunks.append(current)
            current = []
            size = 0

        current.append(p)
        size += p_size

    if current:
        chunks.append(current)

    return chunks


def main() -> None:
    args = _parse_kv_args(sys.argv[1:])

    codebase_name = args.get("codebase_name", "Unknown")
    out_dir = args.get("out_dir", f"docs/oracle-questions-{_today()}")
    oracle_cmd = args.get("oracle_cmd", "oracle")
    oracle_flags = args.get("oracle_flags", "--files-report")
    extra_files = args.get("extra_files", "")
    code_root = args.get("code_root", ".")
    code_glob = args.get("code_glob", "**/*")
    code_paths = args.get("code_paths", "")
    code_max_files = int(args.get("code_max_files", "200"))
    group_mode = args.get("group_mode", "subdir+infer")
    group_min_score = float(args.get("group_min_score", "0.10"))
    group_max_files = int(args.get("group_max_files", "200"))
    group_max_chars = int(args.get("group_max_chars", "200000"))
    ignore_dirs = args.get("ignore_dirs", "")
    include_exts = args.get("include_exts", "")
    exclude_glob = args.get("exclude_glob", "")
    mode = args.get("mode", "codebase-grouped-direct")

    template_path = Path(__file__).resolve().parents[1] / "references" / "codebase-pack-template.md"
    if not template_path.exists():
        raise SystemExit(f"[ERROR] Template not found: {template_path}")

    paths = _collect_paths(code_root, code_glob, code_paths, include_exts, exclude_glob, ignore_dirs)
    paths = sorted(paths, key=lambda p: str(p))
    if code_max_files and code_max_files > 0:
        paths = paths[:code_max_files]

    groups: Dict[str, List[Path]] = {}
    loose: List[Path] = []

    if "subdir" in group_mode:
        groups, loose = _group_by_subdir(paths, code_root)
    else:
        loose = list(paths)

    if "infer" in group_mode and loose:
        group_tokens = {k: _group_tokens(k, v) for k, v in groups.items()}
        for p in loose:
            best = None
            best_score = 0.0
            pt = _file_tokens(p)
            for g, gt in group_tokens.items():
                score = _jaccard(pt, gt)
                if score > best_score:
                    best_score = score
                    best = g
            if best and best_score >= group_min_score:
                groups.setdefault(best, []).append(p)
            else:
                groups.setdefault("root", []).append(p)
    else:
        if loose:
            groups.setdefault("root", []).extend(loose)

    if not groups:
        groups = {"root": []}

    out_dir_path = Path(out_dir)
    packs_dir = out_dir_path / "packs"
    packs_dir.mkdir(parents=True, exist_ok=True)

    rendered_groups: Dict[str, List[str]] = {}

    template = template_path.read_text(encoding="utf-8")
    for group_name in sorted(groups.keys()):
        files = sorted(groups[group_name], key=lambda p: str(p))
        chunks = _cap_group(files, group_max_files, group_max_chars)
        for idx, chunk in enumerate(chunks, start=1):
            part_suffix = f" part {idx}" if len(chunks) > 1 else ""
            full_group_name = f"{group_name}{part_suffix}"
            group_slug = _slugify(full_group_name)
            pack_path = packs_dir / f"{group_slug}.md"

            rendered = template
            rendered = rendered.replace("{{codebase_name}}", codebase_name)
            rendered = rendered.replace("{{out_dir}}", str(out_dir))
            rendered = rendered.replace("{{oracle_cmd}}", oracle_cmd)
            rendered = rendered.replace("{{oracle_flags}}", oracle_flags)
            rendered = rendered.replace("{{extra_files}}", extra_files)
            rendered = rendered.replace("{{code_root}}", code_root)
            rendered = rendered.replace("{{code_glob}}", code_glob)
            rendered = rendered.replace("{{code_paths}}", code_paths)
            rendered = rendered.replace("{{code_max_files}}", str(code_max_files))
            rendered = rendered.replace("{{group_name}}", full_group_name)
            rendered = rendered.replace("{{group_slug}}", group_slug)
            rendered = rendered.replace("{{group_mode}}", group_mode)
            rendered = rendered.replace("{{group_min_score}}", str(group_min_score))
            rendered = rendered.replace("{{group_max_files}}", str(group_max_files))
            rendered = rendered.replace("{{group_max_chars}}", str(group_max_chars))
            rendered = rendered.replace("{{ignore_dirs}}", ignore_dirs)
            rendered = rendered.replace("{{include_exts}}", include_exts)
            rendered = rendered.replace("{{exclude_glob}}", exclude_glob)
            rendered = rendered.replace("{{mode}}", mode)
            rendered = rendered.replace(
                "{{group_files_json}}",
                json.dumps([str(p) for p in chunk], indent=2),
            )

            pack_path.write_text(rendered, encoding="utf-8")
            rendered_groups.setdefault(full_group_name, []).append(str(pack_path))

    groups_json = {
        "code_root": code_root,
        "groups": {k: [str(p) for p in v] for k, v in groups.items()},
        "packs": rendered_groups,
    }
    (out_dir_path / "_groups.json").write_text(json.dumps(groups_json, indent=2), encoding="utf-8")


if __name__ == "__main__":
    main()
```

skills/oraclepack-codebase-pack-grouped/scripts/lint_attachments.py
```
import argparse
import re
import sys
from dataclasses import dataclass
from pathlib import Path
from typing import List, Tuple


@dataclass
class Step:
    n: str
    lines: List[str]


def _read_text(path: Path) -> str:
    try:
        return path.read_text(encoding="utf-8")
    except UnicodeDecodeError:
        return path.read_text(encoding="utf-8", errors="replace")


def _extract_bash_fence(lines: List[str]) -> List[str]:
    fence_idxs = [i for i, ln in enumerate(lines) if ln.startswith("```")]
    if len(fence_idxs) != 2:
        raise ValueError(f"Expected exactly one fenced block (2 fence lines). Found {len(fence_idxs)}.")
    open_i, close_i = fence_idxs
    if lines[open_i].rstrip("\n") != "```bash":
        raise ValueError("Opening fence must be exactly ```bash.")
    if lines[close_i].rstrip("\n") != "```":
        raise ValueError("Closing fence must be exactly ```.")
    return [ln.rstrip("\n") for ln in lines[open_i + 1 : close_i]]


def _parse_steps(fence_lines: List[str]) -> List[Step]:
    header_re = re.compile(r"^#\s*(\d{2})\)\s+")
    header_idxs: List[Tuple[int, str]] = []
    for i, ln in enumerate(fence_lines):
        m = header_re.match(ln)
        if m:
            header_idxs.append((i, m.group(1)))

    if not header_idxs:
        raise ValueError("No step headers found inside bash fence.")

    steps: List[Step] = []
    for idx, (start_i, n) in enumerate(header_idxs):
        end_i = header_idxs[idx + 1][0] if idx + 1 < len(header_idxs) else len(fence_lines)
        steps.append(Step(n=n, lines=fence_lines[start_i:end_i]))
    return steps


def lint(path: Path) -> None:
    raw = _read_text(path)
    lines = raw.splitlines(True)
    fence = _extract_bash_fence(lines)
    steps = _parse_steps(fence)

    errors: List[str] = []
    for step in steps:
        joined = "\n".join(step.lines)

        if "_tickets_bundle" in joined:
            errors.append(f"Step {step.n}: found '_tickets_bundle' reference (codebase packs must attach code files directly).")

        if re.search(r"mapfile\s+-t\s+__code_files\s+<\s+<\(", joined) is None:
            errors.append(f"Step {step.n}: missing mapfile code discovery stanza.")

        if re.search(r"code_args=\(\)", joined) is None or re.search(r"code_args\+\=\(\s*(-f|--file)\b", joined) is None:
            errors.append(f"Step {step.n}: missing code_args builder (code_args+=(-f \"$p\")).")

        if re.search(r"\$\{code_args\[@\]\}", joined) is None:
            errors.append(f"Step {step.n}: missing ${'{'}code_args[@]{'}'} usage in oracle invocation.")

    if errors:
        for e in errors:
            print(f"[ERROR] {e}", file=sys.stderr)
        sys.exit(1)

    print("[OK] Direct-code lint passed.")


def main() -> None:
    p = argparse.ArgumentParser(description="Lint codebase-driven Stage-1 packs (direct-code mode).")
    p.add_argument("pack_path", help="Path to the Markdown pack file")
    args = p.parse_args()

    path = Path(args.pack_path)
    if not path.exists():
        print(f"[ERROR] File not found: {path}", file=sys.stderr)
        sys.exit(1)

    lint(path)


if __name__ == "__main__":
    main()
```

skills/oraclepack-codebase-pack-grouped/scripts/validate_pack.py
```
from pathlib import Path
import runpy

COMMON = Path(__file__).resolve().parents[2] / "oraclepack-tickets-pack-common" / "scripts" / "validate_pack.py"
if not COMMON.exists():
    raise SystemExit(f"[ERROR] Shared validator not found: {COMMON}")

runpy.run_path(str(COMMON), run_name="__main__")
```

</source_code>

--- skills/oraclepack-codebase-pack-grouped_skill_usage_summary.md ---
1) Summary

----------

* Use this skill when you want to extend Codex with a repeatable, task-specific workflow packaged as a “skill” (instructions + optional scripts/resources), rather than re-prompting the same multi-step process each time. [OpenAI Developers+1](https://developers.openai.com/codex/skills/)

* Best fit for generating multiple, deterministic, per-domain/per-topic “mini-packs” for a codebase (grouped output + validation), using a skill folder that includes `SKILL.md` plus optional scripts/references.

    oraclepack-codebase-pack-groupe…

    [OpenAI Developers+1](https://developers.openai.com/codex/skills/)

* Improves reliability via progressive disclosure: Codex loads only skill metadata upfront, then reads full instructions/resources only when invoked (explicitly or implicitly). [OpenAI Developers+1](https://developers.openai.com/codex/skills/)

* Supports portability/versioning: skills are folders of instructions/scripts/resources that can be reused across compatible agents and checked into repo/user/admin scopes. [OpenAI Developers+1](https://developers.openai.com/codex/skills/)

1) Optimal usage moments mapped to lifecycle phases

---------------------------------------------------

Discovery / Scoping: When you need fast, repeatable extraction of “what’s here” across a codebase, but want results split by domain/topic to avoid monolithic context.

Architecture / Design: When you want deterministic grouping and standardized pack structure so downstream analysis is consistent and auditable.

Implementation: When code changes frequently and you want a one-command regeneration of grouped packs with the same schema and constraints.

Testing / Validation: When you need enforceable checks (schema, attachments, size limits) before trusting generated packs or using them in CI.

Release / Rollout: When you want a stable, version-controlled artifact set (per group) to gate releases, document surfaces, or capture invariants.

Operations / Maintenance: When incidents or regressions are localized to a subsystem and you want to quickly re-slice context by directory/domain and re-run analysis consistently.

(Invocation model note: Codex can use a skill by explicit invocation (e.g., selecting from `/skills` or typing `$…`) or by implicitly selecting it when the task matches the skill description.) [OpenAI Developers](https://developers.openai.com/codex/skills/)

1) Example library (12 examples)

--------------------------------

| Phase | Goal | Inputs required (files, constraints, audience, format) | Raw prompt (before) | Optimized prompt (after) | Acceptance criteria |
| --- | --- | --- | --- | --- | --- |
| Discovery / Scoping | Generate grouped packs for an unfamiliar repo | {context} repo path; constraints on size; audience = eng; format = commands + outputs | “Make oracle packs for this repo.” | “Use $oraclepack-codebase-pack-grouped to generate Stage-1 grouped packs for {codebase\_name}. {inputs}: code\_root={code\_root}, out\_dir={out\_dir}, group\_mode=subdir+infer. {constraints}: cap group\_max\_files={group\_max\_files}, group\_max\_chars={group\_max\_chars}, include\_exts={include\_exts}. {acceptance\_criteria}: produce packs/\*.md + \_groups.json and list largest group. {format}: steps + exact commands. {deadline}: {deadline}.” | Mentions explicit skill use; provides concrete KEY=value inputs; defines caps; requests exact artifacts and where they land. |
| Discovery / Scoping | Identify public surface area per subsystem | {context} desired subsystems; constraints; audience = platform team; format = pack set + “how to run” | “Analyze the API surface.” | “Use $oraclepack-codebase-pack-grouped to split the codebase into per-domain packs. Then describe which pack(s) cover public surfaces (CLI/TUI/API). {inputs}: code\_root={code\_root}, exclude\_glob={exclude\_glob}. {format}: (1) generated pack list by group (2) recommended run order (3) what each pack will answer. {acceptance\_criteria}: each group mapped to expected surface area.” | Output ties groups to surfaces, not just “generated packs”; includes exclusions to avoid noise. |
| Architecture / Design | Tune grouping rules to match team domains | {context} desired domain map; constraints on determinism; audience = tech leads; format = updated parameters | “Group these files better.” | “Using the existing grouping approach, propose a deterministic parameter set for $oraclepack-codebase-pack-grouped that aligns with {context} domain boundaries. {inputs}: current repo layout summary, known domain dirs, known ‘loose files’. {constraints}: deterministic ordering; no manual per-file curation unless via code\_paths. {format}: recommended KEY=value args + rationale. {acceptance\_criteria}: explains how loose files resolve; avoids non-deterministic heuristics.” | Provides a concrete args profile and explains deterministic behavior for “loose” files. |
| Architecture / Design | Define skill storage/scope strategy for a mono-repo | {context} mono-repo layout; constraints on precedence; audience = tooling; format = recommended locations | “Where should we put this skill?” | “Recommend where to store this skill so teams can override safely. {inputs}: {context} repo structure + ownership boundaries. {constraints}: follow Codex skill scopes/precedence; prefer version-controlled repo skills. {format}: path recommendations and why (repo root vs nested), plus override plan. {acceptance\_criteria}: uses the documented skill locations and precedence model.” [OpenAI Developers](https://developers.openai.com/codex/skills/) | Uses Codex scope model (repo/user/admin/system) and explains override/precedence. [OpenAI Developers](https://developers.openai.com/codex/skills/) |
| Implementation | Regenerate packs after refactor with minimal noise | {context} refactor areas; constraints on excluding build outputs; audience = eng; format = commands | “Re-run packs after changes.” | “Use $oraclepack-codebase-pack-grouped to regenerate packs for {codebase\_name} focusing on {context}. {inputs}: code\_root={code\_root}, include\_exts={include\_exts}, ignore\_dirs={ignore\_dirs}, exclude\_glob={exclude\_glob}. {constraints}: code\_max\_files={code\_max\_files}; deterministic sort. {format}: exact python command + expected output paths. {acceptance\_criteria}: excludes build/venv artifacts; outputs stable pack slugs.” | Shows exact invocation, includes ignore/exclude controls, and expects stable outputs. |
| Implementation | Create a narrow “single-area” pack for a hotfix | {context} explicit paths; constraints; audience = eng; format = one pack | “Make a pack just for these files.” | “Use $oraclepack-codebase-pack-grouped but force explicit file selection. {inputs}: code\_paths={code\_paths\_csv}, out\_dir={out\_dir}, group\_mode=subdir (or infer off). {constraints}: group\_max\_files={group\_max\_files}. {format}: one generated pack plus how to validate it. {acceptance\_criteria}: pack attaches only the provided files; no extra discovery.” | Uses code\_paths to override globbing; verifies only specified files are included. |
| Testing / Validation | Validate every pack is schema-safe and directly attachable | {context} out\_dir; constraints; audience = CI/tooling; format = command list | “Check these packs.” | “For packs in {out\_dir}/packs, run the skill’s validators and report failures. {inputs}: {out\_dir}. {constraints}: must fail fast; include exact failing step(s). {format}: shell commands + pass/fail summary. {acceptance\_criteria}: every pack checked; failures actionable.” | Includes concrete validation actions and expects actionable failure output. |
| Testing / Validation | Enforce size/token caps by splitting groups | {context} largest group; constraints = token budgets; audience = eng; format = recommended caps + rerun | “This pack is too big.” | “Use $oraclepack-codebase-pack-grouped and adjust caps to meet {constraints} size limits. {inputs}: current group stats, group\_max\_files, group\_max\_chars. {format}: new KEY=value args + rerun command + expected part naming. {acceptance\_criteria}: largest group splits into part 1..N deterministically; no missing files.” | Produces a parameter change and deterministic split strategy, not vague advice. |
| Release / Rollout | Produce release-ready, versioned analysis artifacts | {context} release tag/branch; constraints on reproducibility; audience = release mgr; format = artifact checklist | “Generate release docs.” | “Generate grouped Stage-1 packs for {context} release candidate using $oraclepack-codebase-pack-grouped. {inputs}: code\_root={code\_root}, out\_dir={out\_dir}. {constraints}: reproducible outputs; no env-specific paths beyond out\_dir. {format}: artifact checklist (packs + \_groups.json) + how to run them in order. {acceptance\_criteria}: re-running yields same pack set and slugs for same tree.” | Emphasizes reproducibility and a concrete artifact set. |
| Release / Rollout | Gate release on invariants/contract deltas per domain | {context} “what changed”; constraints; audience = eng/release; format = pack mapping + run plan | “Tell me if contracts changed.” | “Use $oraclepack-codebase-pack-grouped to slice by domain, then outline how each pack will be used to detect contracts/interfaces changes across {context}. {inputs}: baseline vs head refs (described), out\_dir. {format}: (1) which groups matter (2) run sequence (3) what output files to diff. {acceptance\_criteria}: explicit diff targets per group; minimal false positives via excludes.” | Produces a concrete diff plan tied to group outputs. |
| Operations / Maintenance | Incident triage localized to one subsystem | {context} incident area; constraints = speed; audience = oncall; format = narrow regeneration + run | “Help me debug this module.” | “Use $oraclepack-codebase-pack-grouped to generate packs limited to {context} subsystem. {inputs}: code\_root={code\_root}, exclude\_glob={exclude\_glob}, include\_exts={include\_exts}. {constraints}: smaller caps for speed. {format}: exact command + identify which pack(s) to run first. {acceptance\_criteria}: minimal scope; outputs point to likely failure modes and missing signals.” | Keeps scope tight and yields a prioritized run order for the most relevant packs. |
| Operations / Maintenance | Maintain the skill as a reusable capability across teams | {context} ownership model; constraints = portability; audience = platform; format = maintenance playbook | “How do we keep this skill usable?” | “Propose a maintenance playbook for this skill folder so it stays portable and discoverable. {inputs}: {context} team boundaries + repo layout. {constraints}: skill must remain a folder of instructions/scripts/resources; version-controlled; documented invocation. {format}: checklist for updates + review gates. {acceptance\_criteria}: aligns to Agent Skills concept (discoverable folder bundle) and Codex skill loading/scopes.” [Agent Skills+1](https://agentskills.io/home) | Playbook explicitly reflects the “skill folder” model and Codex scope/precedence rules. [Agent Skills+1](https://agentskills.io/home) |

1) Non-goals / anti-patterns (do not use this skill)

----------------------------------------------------

1. Ticket-driven packaging: if the primary inputs are `.tickets/` or issue threads (not repo code), use a ticket-pack skill instead of a codebase-grouped skill.

    oraclepack-codebase-pack-groupe…

2. One-off questions where a full grouped pack set is overkill (e.g., “what does this function do?”).

3. Non-deterministic or manual grouping requirements (e.g., “group by whatever seems interesting today”) where reproducibility is required.

4. Situations where you cannot provide/attach or point to the underlying codebase paths (no evidence to pack).

5. Tasks that primarily require editing/building code rather than generating/validating grouped analysis artifacts (use a coding/implementation workflow instead of a packaging workflow).

---


## Links discovered
- [OpenAI Developers+1](https://developers.openai.com/codex/skills/)
- [OpenAI Developers](https://developers.openai.com/codex/skills/)
- [Agent Skills+1](https://agentskills.io/home)

--- skills/oraclepack-tickets-pack-grouped_skill.md ---
<filetree>
Project Structure:
└── skills
    └── oraclepack-tickets-pack-grouped
        ├── references
        │   ├── attachment-minimization.md
        │   ├── ticket-grouping.md
        │   ├── tickets-pack-template-bundle.md
        │   └── tickets-pack-template.md
        ├── scripts
        │   ├── generate_grouped_packs.py
        │   ├── lint_attachments.py
        │   ├── render_group_packs.py
        │   ├── shard_tickets.py
        │   ├── validate_pack.py
        │   └── validate_shards.py
        └── SKILL.md

</filetree>

<source_code>
skills/oraclepack-tickets-pack-grouped/SKILL.md
```
---
name: oraclepack-tickets-pack-grouped
description: Generate multiple runner-ingestible oraclepack Stage-1 packs grouped by ticket topic/domain (subdir + deterministic inference) with direct ticket attachments. Use when the user wants per-topic/per-domain mini-packs, grouped via subdirectory discovery and inferred assignment of loose tickets, with strict 20-step schema and validation.
---

# oraclepack-tickets-pack-grouped (Stage 1)

## Goal

Produce **multiple** ticket-driven Stage-1 packs, one per inferred topic/domain, with direct ticket attachments. Each pack is schema-safe and self-contained.

## Use this skill

Use when the user wants separate packs per topic/domain, grouped by `.tickets/` subdirectories plus deterministic inference for loose tickets.

## Inputs (parse trailing KEY=value; last-one-wins)

Supported keys (defaults in parentheses):
- `codebase_name` (`Unknown`)
- `out_dir` (`docs/oracle-questions-YYYY-MM-DD`)
- `oracle_cmd` (`oracle`)
- `oracle_flags` (`--files-report`)
- `extra_files` (empty; appended literally)
- `ticket_root` (`.tickets`)
- `ticket_glob` (`**/*.md`)
- `ticket_paths` (empty; comma-separated explicit files; if present, ignore glob)
- `ticket_max_files` (`25`)
- `group_mode` (`subdir+infer`)
- `group_min_score` (`0.08`)
- `group_max_files` (`25`)
- `group_max_chars` (`200000`)
- `dedupe_mode` (`report`)
- `dedupe_jaccard` (`0.55`)
- `dedupe_overlap_hi` (`0.80`)
- `dedupe_overlap_lo` (`0.70`)
- `dedupe_delta_min` (`0.15`)
- `dedupe_body_chars` (`2000`)
- `mode` (`tickets-grouped-direct`)

Notes:
- `YYYY-MM-DD` is computed at pack generation time for default `out_dir`.
- If oracle flag support is uncertain, omit unsupported flags; never invent flags.

## Workflow (deterministic)

1) Read:
- `references/ticket-grouping.md`
- `references/attachment-minimization.md`
- `references/tickets-pack-template.md`

2) Ask user if custom args are needed (numbered picker):

```
1) Use defaults (no args)
2) Provide custom args
```

If `2`, ask for KEY=value args and run with those; otherwise run with defaults.

3) Generate packs (deterministic grouping + per-group pack files):

```bash
python3 /home/user/.codex/skills/oraclepack-tickets-pack-grouped/scripts/generate_grouped_packs.py \
  codebase_name=oraclepack \
  out_dir=docs/oracle-questions-2026-01-08
```

Outputs:
- `{{out_dir}}/packs/*.md` (one pack per group/part)
- `{{out_dir}}/_groups.json` (group -> ticket list)

4) Size control (mandatory; fail fast):
- Run `oracle --dry-run summary --files-report ...` for the **largest** group pack (or each pack if unsure).
- Enforce caps:
  - browser: ≤ 60,000 tokens total input per step
  - api: ≤ 180,000 tokens total input per step
- If exceeded, reduce via `group_max_files` or use explicit `ticket_paths`.

5) Validate every pack (mandatory):

```bash
python3 /home/user/.codex/skills/oraclepack-tickets-pack-grouped/scripts/validate_pack.py <pack.md>
python3 /home/user/.codex/skills/oraclepack-tickets-pack-grouped/scripts/lint_attachments.py <pack.md>
```

## Sharded packs workflow (topic/domain mini-packs)

Use this when you want a manifest-driven, sharded pack per topic/domain with bundle attachments:

First ask the user which args mode to use:

```
1) Use defaults (no args)
2) Provide custom args
```

If `2`, collect args and use them in the commands below.

```bash
python3 /home/user/.codex/skills/oraclepack-tickets-pack-grouped/scripts/shard_tickets.py \\
  --ticket-root .tickets \\
  --out-dir docs/oracle-questions-sharded

python3 /home/user/.codex/skills/oraclepack-tickets-pack-grouped/scripts/render_group_packs.py \\
  --manifest docs/oracle-questions-sharded/manifest.json \\
  --out-dir docs/oracle-questions-sharded

python3 /home/user/.codex/skills/oraclepack-tickets-pack-grouped/scripts/validate_shards.py \\
  --manifest docs/oracle-questions-sharded/manifest.json
```

## Failure behavior

- If no tickets resolve, packs still generate with empty attachments.
- Step 01 prompt must request exact missing ticket file/path pattern(s).

## Output contract

Each pack MUST:
- Have exactly one `bash` fence
- Have exactly 20 steps (01..20)
- Include ROI header tokens
- Include `--write-output` with a group-specific `out_dir`
- Attach tickets directly via `${ticket_args[@]}`
- End with Coverage check outside the bash fence
```

skills/oraclepack-tickets-pack-grouped/references/attachment-minimization.md
```
# Attachment minimization rules (Grouped Tickets Stage 1 — Direct Attach)

Objective: keep each group pack focused and portable.

## Ticket attachments

- Ticket files are attached directly in each step via `${ticket_args[@]}`.
- Use `group_max_files` (default 25) to bound per-pack ticket count.
- If a group is larger than the cap, split into multiple packs (part 1..N).

## Non-ticket attachments (repo evidence)

- Keep explicit non-ticket attachments to **0–1 per step**.
- Prefer a single high-signal file that clarifies contracts or a key code path.

## extra_files (literal append)

- If `extra_files` is provided, append it literally to every oracle command.
- It may include additional `-f/--file` flags.
- Place `extra_files` on its own line with a comment:
  - `# extra_files appended literally`

```

skills/oraclepack-tickets-pack-grouped/references/ticket-grouping.md
```
# Ticket grouping (deterministic, inferred)

Objective: split tickets into focused topic/domain groups and generate one pack per group.

## Inputs

- `ticket_root` (default `.tickets`)
- `ticket_glob` (default `**/*.md`, relative to `ticket_root`)
- `ticket_paths` (optional; comma-separated explicit files; if present, ignore `ticket_glob`)
- `group_mode` (default `subdir+infer`)
- `group_min_score` (default `0.08`)
- `group_max_files` (default `25`; max tickets per pack; >0)
- `group_max_chars` (default `200000`; max total chars per pack; >0)
- `dedupe_mode` (default `report`; one of `off`, `report`, `prune`, `merge`)
- `dedupe_jaccard` (default `0.55`)
- `dedupe_overlap_hi` (default `0.80`)
- `dedupe_overlap_lo` (default `0.70`)
- `dedupe_delta_min` (default `0.15`)
- `dedupe_body_chars` (default `2000`)

## Deterministic grouping rules

1) Collect tickets:
- If `ticket_paths` is non-empty: split on commas, trim whitespace, use exactly that list.
- Else: glob `ticket_root/ticket_glob`.
- Always sort lexicographically by path string.

2) Detect possible duplicates (if `dedupe_mode != off`):
- Signature: filename stem + first heading + first `dedupe_body_chars` chars.
- Compute `jaccard` + `overlap` between tickets.
- Duplicate edge rule:
  - `overlap >= dedupe_overlap_hi` OR (`jaccard >= dedupe_jaccard` AND `overlap >= dedupe_overlap_lo`)
- Connected components become duplicate clusters.
- Canonical: largest content length; tie-break lexicographic.
- Delta vs redundant:
  - delta if unique token ratio >= `dedupe_delta_min` OR heading differs materially.
  - redundant otherwise.

3) Seed groups by subdir:
- For any path under `ticket_root/<group>/...`, assign to group `<group>`.
- Tickets directly under `ticket_root/` are "loose".

4) Infer loose tickets into groups (if any groups exist):
- Build a token set for each group from:
  - group name tokens
  - ticket filenames (stem tokens)
  - first Markdown heading line (if present)
- For each loose ticket, compute Jaccard overlap score with each group token set.
- If `max_score >= group_min_score`, assign to the best group (stable tie-break by group name).
- Otherwise, assign to `misc`.

5) If no groups exist:
- Put all tickets into a single group named `root`.

6) Merge duplicates into primary group:
- `report`: attach all tickets in the cluster to the canonical’s group.
- `prune`: attach canonical + delta only; drop redundant from attachments.
- `merge`: create `out_dir/_ticket_merges/cluster-XXXX.md` and attach only the merged file.
- Emit `_dupes_possible.json`, `_duplicates.json`, and `_dedupe_plan.json`.

7) Split oversized groups:
- If a group exceeds `group_max_files` or `group_max_chars`, split into parts (1..N)
  in sorted order, chunked deterministically.

Hard rule: do not use mtimes, file sizes, or external ML services.

## Required outputs

- `_groups.json`: mapping of group -> list of ticket paths (lexicographic order)
- Pack file per group (and part), each self-contained and direct-attach
- `manifest.json`: groups with pack path + attached vs original ticket lists
```

skills/oraclepack-tickets-pack-grouped/references/tickets-pack-template-bundle.md
```
# Oracle Pack — {{codebase_name}} (Tickets Stage 1)

## Parsed args
- codebase_name: {{codebase_name}}
- out_dir: {{out_dir}}
- oracle_cmd: {{oracle_cmd}}
- oracle_flags: {{oracle_flags}}
- extra_files: {{extra_files}}
- ticket_root: {{ticket_root}}
- ticket_glob: {{ticket_glob}}
- ticket_paths: {{ticket_paths}}
- ticket_bundle_path: {{ticket_bundle_path}}
- mode: {{mode}}

Notes (contract):
- Exactly one fenced `bash` block in this document.
- No other ``` fences anywhere.
- Exactly 20 steps, numbered 01..20 in order.
- Step header: `# NN) ROI=... impact=... confidence=... effort=... horizon=... category=... reference=...`
- Every step includes: `--write-output "{{out_dir}}/NN-<slug>.md"` (double quotes required).
- Steps must be self-contained and must not rely on shell variables created in previous steps.
- `## Coverage check` MUST be outside the bash fence (after the closing ```).

```bash
# Prelude (allowed inside the single bash fence)
# - Creates out_dir deterministically
# - Builds ticket_bundle_path deterministically from ticket_root/ticket_glob OR ticket_paths
# - Uses lexicographic ordering only (no mtime/timestamps)

set -euo pipefail

mkdir -p "{{out_dir}}"

python3 - <<'PY'
from __future__ import annotations

import sys
from pathlib import Path

CODEBASE_NAME = "{{codebase_name}}"
OUT_DIR = Path("{{out_dir}}")
TICKET_ROOT = Path("{{ticket_root}}")
TICKET_GLOB = "{{ticket_glob}}"
TICKET_PATHS_RAW = "{{ticket_paths}}".strip()
BUNDLE_PATH = Path("{{ticket_bundle_path}}")

def _read_text(p: Path) -> str:
    return p.read_text(encoding="utf-8", errors="replace")

def _title_from_md(text: str) -> str:
    for ln in text.splitlines():
        s = ln.strip()
        if s.startswith("# "):
            return s[2:].strip() or "Untitled"
    for ln in text.splitlines():
        s = ln.strip()
        if s:
            return s[:80]
    return "Untitled"

def _select_paths() -> list[Path]:
    if TICKET_PATHS_RAW:
        items = [Path(x.strip()) for x in TICKET_PATHS_RAW.split(",") if x.strip()]
        items = sorted(items, key=lambda p: str(p))
        return items

    if not TICKET_ROOT.exists():
        return []

    items = sorted(TICKET_ROOT.glob(TICKET_GLOB), key=lambda p: str(p))
    return items

paths = _select_paths()

BUNDLE_PATH.parent.mkdir(parents=True, exist_ok=True)

lines: list[str] = []
lines.append(f"# Tickets Bundle — {CODEBASE_NAME if CODEBASE_NAME else 'Unknown'}")
lines.append("")
lines.append("## Selection")
lines.append(f"- ticket_root: {TICKET_ROOT}")
lines.append(f"- ticket_glob: {TICKET_GLOB}")
lines.append(f"- ticket_paths: {TICKET_PATHS_RAW if TICKET_PATHS_RAW else '(none)'}")
lines.append("- ordering: lexicographic by path")
lines.append("")

if not paths:
    warn = (
        "## WARNING: No tickets found\n\n"
        "No ticket files were selected.\n\n"
        "What was attempted:\n"
        f"- ticket_root: {TICKET_ROOT}\n"
        f"- ticket_glob: {TICKET_GLOB}\n"
        f"- ticket_paths: {TICKET_PATHS_RAW if TICKET_PATHS_RAW else '(none)'}\n\n"
        "Next: provide explicit ticket_paths or create tickets under ticket_root.\n"
    )
    lines.append(warn)
    print(f"[WARN] No tickets selected; bundle will contain only WARNING.", file=sys.stderr)
else:
    lines.append("## Tickets")
    lines.append("")
    for p in paths:
        lines.append("---")
        lines.append(f"### {_title_from_md(_read_text(p))}")
        lines.append(f"- path: {p}")
        lines.append("")
        try:
            txt = _read_text(p)
        except Exception as e:
            lines.append(f"[ERROR reading file: {e}]")
            lines.append("")
            continue

        # Simple truncation policy: keep first 4000 chars if large.
        if len(txt) > 4000:
            lines.append(txt[:4000])
            lines.append("\n[... truncated ...]\n")
        else:
            lines.append(txt)

        lines.append("")

BUNDLE_PATH.write_text("\n".join(lines).rstrip() + "\n", encoding="utf-8")
print(f"[OK] Wrote ticket bundle: {BUNDLE_PATH}")
PY

# 01) ROI=8.0 impact=9 confidence=0.9 effort=1 horizon=Immediate category=contracts/interfaces reference=Unknown
{{oracle_cmd}} {{oracle_flags}} \
  --write-output "{{out_dir}}/01-contracts-interfaces-surface.md" \
  -f "{{ticket_bundle_path}}" {{extra_files}} \
  -p "$(cat <<'PROMPT'
Strategist question #01
Category: contracts/interfaces

Using the attached tickets bundle as the primary evidence, identify the primary public interface(s) implied by the tickets (CLI commands, APIs, file contracts, or user workflows).
For each interface:
- list key inputs/outputs
- list the exact files/modules likely defining it (if unknown, say Unknown)

Answer format:
1) Direct answer (1–6 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 02) ROI=7.8 impact=8 confidence=0.9 effort=1 horizon=Immediate category=contracts/interfaces reference=Unknown
{{oracle_cmd}} {{oracle_flags}} \
  --write-output "{{out_dir}}/02-contracts-interfaces-dependencies.md" \
  -f "{{ticket_bundle_path}}" {{extra_files}} \
  -p "$(cat <<'PROMPT'
Strategist question #02
Category: contracts/interfaces

From the attached tickets bundle, infer which external dependencies/services the system must integrate with (CLIs, APIs, SaaS, databases).
For each dependency:
- what contract is required (auth, endpoints, file formats)
- what configuration surface is implied

Answer format:
1) Direct answer (1–6 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 03) ROI=7.6 impact=8 confidence=0.85 effort=2 horizon=Immediate category=invariants reference=Unknown
{{oracle_cmd}} {{oracle_flags}} \
  --write-output "{{out_dir}}/03-invariants-must-always-hold.md" \
  -f "{{ticket_bundle_path}}" {{extra_files}} \
  -p "$(cat <<'PROMPT'
Strategist question #03
Category: invariants

Based on the attached tickets bundle, list the invariants that must always hold (data constraints, ordering constraints, security invariants, idempotency).
For each invariant:
- what breaks if violated
- where it should be enforced (layer/module; if unknown, Unknown)

Answer format:
1) Direct answer (1–6 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 04) ROI=7.2 impact=8 confidence=0.8 effort=2 horizon=Immediate category=invariants reference=Unknown
{{oracle_cmd}} {{oracle_flags}} \
  --write-output "{{out_dir}}/04-invariants-input-validation.md" \
  -f "{{ticket_bundle_path}}" {{extra_files}} \
  -p "$(cat <<'PROMPT'
Strategist question #04
Category: invariants

Using the attached tickets bundle, identify what inputs must be validated (CLI args, config fields, payloads, file paths).
For each input:
- validation rules implied
- failure message/behavior implied

Answer format:
1) Direct answer (1–6 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 05) ROI=7.0 impact=7 confidence=0.85 effort=2 horizon=Near category=caching/state reference=Unknown
{{oracle_cmd}} {{oracle_flags}} \
  --write-output "{{out_dir}}/05-caching-state-state-model.md" \
  -f "{{ticket_bundle_path}}" {{extra_files}} \
  -p "$(cat <<'PROMPT'
Strategist question #05
Category: caching/state

From the attached tickets bundle, infer what state must be persisted or cached (files, DB, in-memory, remote).
For each state item:
- read/write lifecycle
- consistency model implied
- failure recovery requirements

Answer format:
1) Direct answer (1–6 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 06) ROI=6.8 impact=7 confidence=0.8 effort=2 horizon=Near category=caching/state reference=Unknown
{{oracle_cmd}} {{oracle_flags}} \
  --write-output "{{out_dir}}/06-caching-state-cache-invalidation.md" \
  -f "{{ticket_bundle_path}}" {{extra_files}} \
  -p "$(cat <<'PROMPT'
Strategist question #06
Category: caching/state

Using the attached tickets bundle, identify caching risks: staleness, invalidation, keying, or race conditions implied by the tickets.
Propose a minimal caching strategy consistent with the tickets.

Answer format:
1) Direct answer (1–6 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 07) ROI=6.9 impact=8 confidence=0.75 effort=3 horizon=Near category=background jobs reference=Unknown
{{oracle_cmd}} {{oracle_flags}} \
  --write-output "{{out_dir}}/07-background-jobs-what-runs-async.md" \
  -f "{{ticket_bundle_path}}" {{extra_files}} \
  -p "$(cat <<'PROMPT'
Strategist question #07
Category: background jobs

From the attached tickets bundle, determine what work should run asynchronously/background (schedulers, queues, cron, long-running tasks).
For each job:
- trigger
- inputs/outputs
- retry/backoff requirements

Answer format:
1) Direct answer (1–6 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 08) ROI=6.6 impact=7 confidence=0.75 effort=3 horizon=Near category=background jobs reference=Unknown
{{oracle_cmd}} {{oracle_flags}} \
  --write-output "{{out_dir}}/08-background-jobs-idempotency.md" \
  -f "{{ticket_bundle_path}}" {{extra_files}} \
  -p "$(cat <<'PROMPT'
Strategist question #08
Category: background jobs

Using the attached tickets bundle, list the idempotency and concurrency constraints implied for background jobs.
Recommend minimal safeguards (dedupe keys, locks, at-least-once handling) aligned with tickets.

Answer format:
1) Direct answer (1–6 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 09) ROI=7.4 impact=8 confidence=0.8 effort=2 horizon=Immediate category=observability reference=Unknown
{{oracle_cmd}} {{oracle_flags}} \
  --write-output "{{out_dir}}/09-observability-logs-metrics-traces.md" \
  -f "{{ticket_bundle_path}}" {{extra_files}} \
  -p "$(cat <<'PROMPT'
Strategist question #09
Category: observability

From the attached tickets bundle, infer required observability: logs, metrics, traces, and user-visible diagnostics.
List:
- what to log/measure
- cardinality risks
- minimal dashboards/alerts implied

Answer format:
1) Direct answer (1–6 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 10) ROI=7.0 impact=7 confidence=0.8 effort=2 horizon=Near category=observability reference=Unknown
{{oracle_cmd}} {{oracle_flags}} \
  --write-output "{{out_dir}}/10-observability-error-taxonomy.md" \
  -f "{{ticket_bundle_path}}" {{extra_files}} \
  -p "$(cat <<'PROMPT'
Strategist question #10
Category: observability

Using the attached tickets bundle, define an error taxonomy consistent with ticket failure modes:
- user errors vs system errors
- retryable vs non-retryable
- how errors should surface (CLI exit codes, UI states, logs)

Answer format:
1) Direct answer (1–6 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 11) ROI=7.6 impact=9 confidence=0.75 effort=3 horizon=Immediate category=permissions reference=Unknown
{{oracle_cmd}} {{oracle_flags}} \
  --write-output "{{out_dir}}/11-permissions-authz-model.md" \
  -f "{{ticket_bundle_path}}" {{extra_files}} \
  -p "$(cat <<'PROMPT'
Strategist question #11
Category: permissions

From the attached tickets bundle, infer the permissions model (roles, capabilities, scopes).
List:
- what operations require permissions
- how permissions are granted/revoked
- audit requirements implied

Answer format:
1) Direct answer (1–6 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 12) ROI=7.0 impact=8 confidence=0.75 effort=3 horizon=Near category=permissions reference=Unknown
{{oracle_cmd}} {{oracle_flags}} \
  --write-output "{{out_dir}}/12-permissions-secret-handling.md" \
  -f "{{ticket_bundle_path}}" {{extra_files}} \
  -p "$(cat <<'PROMPT'
Strategist question #12
Category: permissions

Using the attached tickets bundle, identify sensitive data/secret handling needs.
Recommend:
- where secrets come from (env, files, vault)
- redaction rules
- least-privilege defaults

Answer format:
1) Direct answer (1–6 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 13) ROI=7.2 impact=8 confidence=0.8 effort=2 horizon=Near category=migrations reference=Unknown
{{oracle_cmd}} {{oracle_flags}} \
  --write-output "{{out_dir}}/13-migrations-data-changes.md" \
  -f "{{ticket_bundle_path}}" {{extra_files}} \
  -p "$(cat <<'PROMPT'
Strategist question #13
Category: migrations

From the attached tickets bundle, infer any data/schema/config migrations needed.
For each migration:
- trigger/versioning
- rollout plan
- rollback strategy

Answer format:
1) Direct answer (1–6 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 14) ROI=6.8 impact=7 confidence=0.8 effort=2 horizon=Near category=migrations reference=Unknown
{{oracle_cmd}} {{oracle_flags}} \
  --write-output "{{out_dir}}/14-migrations-compatibility.md" \
  -f "{{ticket_bundle_path}}" {{extra_files}} \
  -p "$(cat <<'PROMPT'
Strategist question #14
Category: migrations

Using the attached tickets bundle, identify backwards/forwards compatibility requirements during migration windows.
Recommend minimal compatibility shims or staged rollout steps.

Answer format:
1) Direct answer (1–6 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 15) ROI=7.4 impact=8 confidence=0.8 effort=2 horizon=Immediate category=UX flows reference=Unknown
{{oracle_cmd}} {{oracle_flags}} \
  --write-output "{{out_dir}}/15-ux-flows-primary-journeys.md" \
  -f "{{ticket_bundle_path}}" {{extra_files}} \
  -p "$(cat <<'PROMPT'
Strategist question #15
Category: UX flows

From the attached tickets bundle, map the primary user journeys implied by tickets.
For each journey:
- entry points
- steps/screens/commands
- success criteria and user feedback

Answer format:
1) Direct answer (1–6 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 16) ROI=6.9 impact=7 confidence=0.8 effort=2 horizon=Near category=UX flows reference=Unknown
{{oracle_cmd}} {{oracle_flags}} \
  --write-output "{{out_dir}}/16-ux-flows-edge-cases.md" \
  -f "{{ticket_bundle_path}}" {{extra_files}} \
  -p "$(cat <<'PROMPT'
Strategist question #16
Category: UX flows

Using the attached tickets bundle, list UX edge cases and failure UX:
- partial completion
- retries
- cancellation
- timeouts
- conflict resolution

Answer format:
1) Direct answer (1–6 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 17) ROI=7.8 impact=9 confidence=0.8 effort=2 horizon=Immediate category=failure modes reference=Unknown
{{oracle_cmd}} {{oracle_flags}} \
  --write-output "{{out_dir}}/17-failure-modes-top-risks.md" \
  -f "{{ticket_bundle_path}}" {{extra_files}} \
  -p "$(cat <<'PROMPT'
Strategist question #17
Category: failure modes

From the attached tickets bundle, enumerate the most likely failure modes.
For each failure mode:
- detection signal
- mitigation
- user-visible behavior

Answer format:
1) Direct answer (1–6 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 18) ROI=7.0 impact=8 confidence=0.75 effort=3 horizon=Near category=failure modes reference=Unknown
{{oracle_cmd}} {{oracle_flags}} \
  --write-output "{{out_dir}}/18-failure-modes-test-plan.md" \
  -f "{{ticket_bundle_path}}" {{extra_files}} \
  -p "$(cat <<'PROMPT'
Strategist question #18
Category: failure modes

Using the attached tickets bundle, propose a minimal test plan that covers the highest-risk failure modes.
Include:
- unit vs integration coverage split
- fixtures/mocks needed
- one smallest test to write first

Answer format:
1) Direct answer (1–6 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 19) ROI=7.3 impact=8 confidence=0.8 effort=2 horizon=Near category=feature flags reference=Unknown
{{oracle_cmd}} {{oracle_flags}} \
  --write-output "{{out_dir}}/19-feature-flags-needed.md" \
  -f "{{ticket_bundle_path}}" {{extra_files}} \
  -p "$(cat <<'PROMPT'
Strategist question #19
Category: feature flags

From the attached tickets bundle, infer where feature flags or staged rollouts are needed.
For each flag:
- what it gates
- default value
- sunset plan

Answer format:
1) Direct answer (1–6 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 20) ROI=6.8 impact=7 confidence=0.8 effort=2 horizon=Near category=feature flags reference=Unknown
{{oracle_cmd}} {{oracle_flags}} \
  --write-output "{{out_dir}}/20-feature-flags-observability.md" \
  -f "{{ticket_bundle_path}}" {{extra_files}} \
  -p "$(cat <<'PROMPT'
Strategist question #20
Category: feature flags

Using the attached tickets bundle, propose how to observe/validate a flagged rollout:
- success metrics
- rollback triggers
- logging/alert changes while enabled

Answer format:
1) Direct answer (1–6 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"
```

## Coverage check

*   contracts/interfaces: OK
*   invariants: OK
*   caching/state: OK
*   background jobs: OK
*   observability: OK
*   permissions: OK
*   migrations: OK
*   UX flows: OK
*   failure modes: OK
*   feature flags: OK

```
```

skills/oraclepack-tickets-pack-grouped/references/tickets-pack-template.md
```
# Oracle Pack — {{codebase_name}} (Grouped Tickets Stage 1 — Direct Attach)

## Parsed args
- codebase_name: {{codebase_name}}
- out_dir: {{out_dir}}
- oracle_cmd: {{oracle_cmd}}
- oracle_flags: {{oracle_flags}}
- extra_files: {{extra_files}}
- ticket_root: {{ticket_root}}
- ticket_glob: {{ticket_glob}}
- ticket_paths: {{ticket_paths}}
- ticket_max_files: {{ticket_max_files}}
- group_name: {{group_name}}
- group_slug: {{group_slug}}
- mode: {{mode}}

Notes (contract):
- Exactly one fenced `bash` block in this document.
- No other ``` fences anywhere.
- Exactly 20 steps, numbered 01..20 in order.
- Step header: `# NN) ROI=... impact=... confidence=... effort=... horizon=... category=... reference=...`
- Every step includes: `--write-output "{{out_dir}}/NN-<slug>.md"` (double quotes required).
- Steps must be self-contained and must not rely on shell variables created in previous steps.
- Each step must attach tickets directly (no `_tickets_bundle.md` dependency).
- Pack ends with a Coverage check section listing all 10 categories as OK or Missing(<step ids>).

```bash
set -euo pipefail

mkdir -p "{{out_dir}}"

# 01) ROI=4.8 impact=6 confidence=0.80 effort=1 horizon=Immediate category=contracts/interfaces reference={{group_slug}}

# tickets attached directly (deterministic; self-contained)
mapfile -t __tickets < <(python3 - <<'PY'
from __future__ import annotations
from pathlib import Path

TICKET_ROOT = "{{ticket_root}}"
TICKET_GLOB = "{{ticket_glob}}"
TICKET_PATHS = "{{ticket_paths}}".strip()
MAX = int("{{ticket_max_files}}")

root = Path(TICKET_ROOT)

def lex_sorted(ps):
    return sorted((str(p) for p in ps), key=lambda s: s)

if TICKET_PATHS:
    tickets = [Path(p.strip()) for p in TICKET_PATHS.split(",") if p.strip()]
else:
    tickets = list(root.glob(TICKET_GLOB)) if root.exists() else []

tickets = [Path(p) for p in lex_sorted(tickets)]
if MAX and MAX > 0:
    tickets = tickets[:MAX]

for p in tickets:
    print(str(p))
PY
)

ticket_args=()
for p in "${__tickets[@]}"; do
  ticket_args+=(-f "$p")
done

if [ "${#ticket_args[@]}" -eq 0 ]; then
  echo "WARNING: no tickets resolved for group '{{group_name}}'." >&2
fi

# extra_files appended literally (may be empty; may include -f/--file):
{{oracle_cmd}}   {{oracle_flags}}   --write-output "{{out_dir}}/01-contracts-interfaces-ticket-surface.md"   "${ticket_args[@]}"   {{extra_files}}   -p "$(cat <<'PROMPT'
Strategist question #01  (ticket-driven, group: {{group_name}})

Reference: {{group_slug}}
Category: contracts/interfaces
Horizon: Immediate
ROI: 4.8 (impact=6, confidence=0.80, effort=1)

Question:
Using the attached tickets as the primary context, list the public surface changes implied by the tickets (CLI/TUI/API/interfaces/contracts); call out backwards-compat constraints.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 02) ROI=4.6 impact=6 confidence=0.78 effort=1 horizon=Immediate category=contracts/interfaces reference={{group_slug}}

# tickets attached directly (deterministic; self-contained)
mapfile -t __tickets < <(python3 - <<'PY'
from __future__ import annotations
from pathlib import Path

TICKET_ROOT = "{{ticket_root}}"
TICKET_GLOB = "{{ticket_glob}}"
TICKET_PATHS = "{{ticket_paths}}".strip()
MAX = int("{{ticket_max_files}}")

root = Path(TICKET_ROOT)

def lex_sorted(ps):
    return sorted((str(p) for p in ps), key=lambda s: s)

if TICKET_PATHS:
    tickets = [Path(p.strip()) for p in TICKET_PATHS.split(",") if p.strip()]
else:
    tickets = list(root.glob(TICKET_GLOB)) if root.exists() else []

tickets = [Path(p) for p in lex_sorted(tickets)]
if MAX and MAX > 0:
    tickets = tickets[:MAX]

for p in tickets:
    print(str(p))
PY
)

ticket_args=()
for p in "${__tickets[@]}"; do
  ticket_args+=(-f "$p")
done

if [ "${#ticket_args[@]}" -eq 0 ]; then
  echo "WARNING: no tickets resolved for group '{{group_name}}'." >&2
fi

# extra_files appended literally (may be empty; may include -f/--file):
{{oracle_cmd}}   {{oracle_flags}}   --write-output "{{out_dir}}/02-contracts-interfaces-integration-points.md"   "${ticket_args[@]}"   {{extra_files}}   -p "$(cat <<'PROMPT'
Strategist question #02  (ticket-driven, group: {{group_name}})

Reference: {{group_slug}}
Category: contracts/interfaces
Horizon: Immediate
ROI: 4.6 (impact=6, confidence=0.78, effort=1)

Question:
Using the attached tickets as the primary context, identify external integrations implied by the tickets; required config/contract changes; failure/timeout behavior; minimal compat-safe rollout.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 03) ROI=5.1 impact=7 confidence=0.74 effort=1 horizon=NearTerm category=invariants reference={{group_slug}}

# tickets attached directly (deterministic; self-contained)
mapfile -t __tickets < <(python3 - <<'PY'
from __future__ import annotations
from pathlib import Path

TICKET_ROOT = "{{ticket_root}}"
TICKET_GLOB = "{{ticket_glob}}"
TICKET_PATHS = "{{ticket_paths}}".strip()
MAX = int("{{ticket_max_files}}")

root = Path(TICKET_ROOT)

def lex_sorted(ps):
    return sorted((str(p) for p in ps), key=lambda s: s)

if TICKET_PATHS:
    tickets = [Path(p.strip()) for p in TICKET_PATHS.split(",") if p.strip()]
else:
    tickets = list(root.glob(TICKET_GLOB)) if root.exists() else []

tickets = [Path(p) for p in lex_sorted(tickets)]
if MAX and MAX > 0:
    tickets = tickets[:MAX]

for p in tickets:
    print(str(p))
PY
)

ticket_args=()
for p in "${__tickets[@]}"; do
  ticket_args+=(-f "$p")
done

if [ "${#ticket_args[@]}" -eq 0 ]; then
  echo "WARNING: no tickets resolved for group '{{group_name}}'." >&2
fi

# extra_files appended literally (may be empty; may include -f/--file):
{{oracle_cmd}}   {{oracle_flags}}   --write-output "{{out_dir}}/03-invariants-invariant-map.md"   "${ticket_args[@]}"   {{extra_files}}   -p "$(cat <<'PROMPT'
Strategist question #03  (ticket-driven, group: {{group_name}})

Reference: {{group_slug}}
Category: invariants
Horizon: NearTerm
ROI: 5.1 (impact=7, confidence=0.74, effort=1)

Question:
Using the attached tickets as the primary context, extract system invariants implied by tickets (inputs/outputs, pack schema rules, step execution rules) and where to enforce them.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 04) ROI=5.0 impact=7 confidence=0.72 effort=2 horizon=NearTerm category=invariants reference={{group_slug}}

# tickets attached directly (deterministic; self-contained)
mapfile -t __tickets < <(python3 - <<'PY'
from __future__ import annotations
from pathlib import Path

TICKET_ROOT = "{{ticket_root}}"
TICKET_GLOB = "{{ticket_glob}}"
TICKET_PATHS = "{{ticket_paths}}".strip()
MAX = int("{{ticket_max_files}}")

root = Path(TICKET_ROOT)

def lex_sorted(ps):
    return sorted((str(p) for p in ps), key=lambda s: s)

if TICKET_PATHS:
    tickets = [Path(p.strip()) for p in TICKET_PATHS.split(",") if p.strip()]
else:
    tickets = list(root.glob(TICKET_GLOB)) if root.exists() else []

tickets = [Path(p) for p in lex_sorted(tickets)]
if MAX and MAX > 0:
    tickets = tickets[:MAX]

for p in tickets:
    print(str(p))
PY
)

ticket_args=()
for p in "${__tickets[@]}"; do
  ticket_args+=(-f "$p")
done

if [ "${#ticket_args[@]}" -eq 0 ]; then
  echo "WARNING: no tickets resolved for group '{{group_name}}'." >&2
fi

# extra_files appended literally (may be empty; may include -f/--file):
{{oracle_cmd}}   {{oracle_flags}}   --write-output "{{out_dir}}/04-invariants-validation-boundaries.md"   "${ticket_args[@]}"   {{extra_files}}   -p "$(cat <<'PROMPT'
Strategist question #04  (ticket-driven, group: {{group_name}})

Reference: {{group_slug}}
Category: invariants
Horizon: NearTerm
ROI: 5.0 (impact=7, confidence=0.72, effort=2)

Question:
Using the attached tickets as the primary context, identify validation boundaries that must exist (ticket parsing, pack generation, pack validation); propose minimal validation plan.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 05) ROI=4.4 impact=6 confidence=0.78 effort=2 horizon=NearTerm category=caching/state reference={{group_slug}}

# tickets attached directly (deterministic; self-contained)
mapfile -t __tickets < <(python3 - <<'PY'
from __future__ import annotations
from pathlib import Path

TICKET_ROOT = "{{ticket_root}}"
TICKET_GLOB = "{{ticket_glob}}"
TICKET_PATHS = "{{ticket_paths}}".strip()
MAX = int("{{ticket_max_files}}")

root = Path(TICKET_ROOT)

def lex_sorted(ps):
    return sorted((str(p) for p in ps), key=lambda s: s)

if TICKET_PATHS:
    tickets = [Path(p.strip()) for p in TICKET_PATHS.split(",") if p.strip()]
else:
    tickets = list(root.glob(TICKET_GLOB)) if root.exists() else []

tickets = [Path(p) for p in lex_sorted(tickets)]
if MAX and MAX > 0:
    tickets = tickets[:MAX]

for p in tickets:
    print(str(p))
PY
)

ticket_args=()
for p in "${__tickets[@]}"; do
  ticket_args+=(-f "$p")
done

if [ "${#ticket_args[@]}" -eq 0 ]; then
  echo "WARNING: no tickets resolved for group '{{group_name}}'." >&2
fi

# extra_files appended literally (may be empty; may include -f/--file):
{{oracle_cmd}}   {{oracle_flags}}   --write-output "{{out_dir}}/05-caching-state-state-artifacts.md"   "${ticket_args[@]}"   {{extra_files}}   -p "$(cat <<'PROMPT'
Strategist question #05  (ticket-driven, group: {{group_name}})

Reference: {{group_slug}}
Category: caching/state
Horizon: NearTerm
ROI: 4.4 (impact=6, confidence=0.78, effort=2)

Question:
Using the attached tickets as the primary context, identify state/artifacts that must be produced and preserved; schema/format expectations; stability/back-compat requirements.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 06) ROI=4.2 impact=6 confidence=0.75 effort=2 horizon=NearTerm category=caching/state reference={{group_slug}}

# tickets attached directly (deterministic; self-contained)
mapfile -t __tickets < <(python3 - <<'PY'
from __future__ import annotations
from pathlib import Path

TICKET_ROOT = "{{ticket_root}}"
TICKET_GLOB = "{{ticket_glob}}"
TICKET_PATHS = "{{ticket_paths}}".strip()
MAX = int("{{ticket_max_files}}")

root = Path(TICKET_ROOT)

def lex_sorted(ps):
    return sorted((str(p) for p in ps), key=lambda s: s)

if TICKET_PATHS:
    tickets = [Path(p.strip()) for p in TICKET_PATHS.split(",") if p.strip()]
else:
    tickets = list(root.glob(TICKET_GLOB)) if root.exists() else []

tickets = [Path(p) for p in lex_sorted(tickets)]
if MAX and MAX > 0:
    tickets = tickets[:MAX]

for p in tickets:
    print(str(p))
PY
)

ticket_args=()
for p in "${__tickets[@]}"; do
  ticket_args+=(-f "$p")
done

if [ "${#ticket_args[@]}" -eq 0 ]; then
  echo "WARNING: no tickets resolved for group '{{group_name}}'." >&2
fi

# extra_files appended literally (may be empty; may include -f/--file):
{{oracle_cmd}}   {{oracle_flags}}   --write-output "{{out_dir}}/06-caching-state-cache-keys.md"   "${ticket_args[@]}"   {{extra_files}}   -p "$(cat <<'PROMPT'
Strategist question #06  (ticket-driven, group: {{group_name}})

Reference: {{group_slug}}
Category: caching/state
Horizon: NearTerm
ROI: 4.2 (impact=6, confidence=0.75, effort=2)

Question:
Using the attached tickets as the primary context, identify any caching opportunities/risks (discovery caches, pack outputs, oracle outputs); define cache keys, invalidation, and correctness risks.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 07) ROI=4.3 impact=6 confidence=0.70 effort=2 horizon=MidTerm category=background jobs reference={{group_slug}}

# tickets attached directly (deterministic; self-contained)
mapfile -t __tickets < <(python3 - <<'PY'
from __future__ import annotations
from pathlib import Path

TICKET_ROOT = "{{ticket_root}}"
TICKET_GLOB = "{{ticket_glob}}"
TICKET_PATHS = "{{ticket_paths}}".strip()
MAX = int("{{ticket_max_files}}")

root = Path(TICKET_ROOT)

def lex_sorted(ps):
    return sorted((str(p) for p in ps), key=lambda s: s)

if TICKET_PATHS:
    tickets = [Path(p.strip()) for p in TICKET_PATHS.split(",") if p.strip()]
else:
    tickets = list(root.glob(TICKET_GLOB)) if root.exists() else []

tickets = [Path(p) for p in lex_sorted(tickets)]
if MAX and MAX > 0:
    tickets = tickets[:MAX]

for p in tickets:
    print(str(p))
PY
)

ticket_args=()
for p in "${__tickets[@]}"; do
  ticket_args+=(-f "$p")
done

if [ "${#ticket_args[@]}" -eq 0 ]; then
  echo "WARNING: no tickets resolved for group '{{group_name}}'." >&2
fi

# extra_files appended literally (may be empty; may include -f/--file):
{{oracle_cmd}}   {{oracle_flags}}   --write-output "{{out_dir}}/07-background-jobs-job-model.md"   "${ticket_args[@]}"   {{extra_files}}   -p "$(cat <<'PROMPT'
Strategist question #07  (ticket-driven, group: {{group_name}})

Reference: {{group_slug}}
Category: background jobs
Horizon: MidTerm
ROI: 4.3 (impact=6, confidence=0.70, effort=2)

Question:
Using the attached tickets as the primary context, identify any background/async work implied (jobs, queues, long-running operations); define responsibilities and interfaces.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 08) ROI=4.0 impact=6 confidence=0.68 effort=3 horizon=MidTerm category=background jobs reference={{group_slug}}

# tickets attached directly (deterministic; self-contained)
mapfile -t __tickets < <(python3 - <<'PY'
from __future__ import annotations
from pathlib import Path

TICKET_ROOT = "{{ticket_root}}"
TICKET_GLOB = "{{ticket_glob}}"
TICKET_PATHS = "{{ticket_paths}}".strip()
MAX = int("{{ticket_max_files}}")

root = Path(TICKET_ROOT)

def lex_sorted(ps):
    return sorted((str(p) for p in ps), key=lambda s: s)

if TICKET_PATHS:
    tickets = [Path(p.strip()) for p in TICKET_PATHS.split(",") if p.strip()]
else:
    tickets = list(root.glob(TICKET_GLOB)) if root.exists() else []

tickets = [Path(p) for p in lex_sorted(tickets)]
if MAX and MAX > 0:
    tickets = tickets[:MAX]

for p in tickets:
    print(str(p))
PY
)

ticket_args=()
for p in "${__tickets[@]}"; do
  ticket_args+=(-f "$p")
done

if [ "${#ticket_args[@]}" -eq 0 ]; then
  echo "WARNING: no tickets resolved for group '{{group_name}}'." >&2
fi

# extra_files appended literally (may be empty; may include -f/--file):
{{oracle_cmd}}   {{oracle_flags}}   --write-output "{{out_dir}}/08-background-jobs-queue-failure.md"   "${ticket_args[@]}"   {{extra_files}}   -p "$(cat <<'PROMPT'
Strategist question #08  (ticket-driven, group: {{group_name}})

Reference: {{group_slug}}
Category: background jobs
Horizon: MidTerm
ROI: 4.0 (impact=6, confidence=0.68, effort=3)

Question:
Using the attached tickets as the primary context, define how background failures are handled (retries, idempotency, poison messages); define observability hooks.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 09) ROI=4.7 impact=7 confidence=0.76 effort=1 horizon=Immediate category=observability reference={{group_slug}}

# tickets attached directly (deterministic; self-contained)
mapfile -t __tickets < <(python3 - <<'PY'
from __future__ import annotations
from pathlib import Path

TICKET_ROOT = "{{ticket_root}}"
TICKET_GLOB = "{{ticket_glob}}"
TICKET_PATHS = "{{ticket_paths}}".strip()
MAX = int("{{ticket_max_files}}")

root = Path(TICKET_ROOT)

def lex_sorted(ps):
    return sorted((str(p) for p in ps), key=lambda s: s)

if TICKET_PATHS:
    tickets = [Path(p.strip()) for p in TICKET_PATHS.split(",") if p.strip()]
else:
    tickets = list(root.glob(TICKET_GLOB)) if root.exists() else []

tickets = [Path(p) for p in lex_sorted(tickets)]
if MAX and MAX > 0:
    tickets = tickets[:MAX]

for p in tickets:
    print(str(p))
PY
)

ticket_args=()
for p in "${__tickets[@]}"; do
  ticket_args+=(-f "$p")
done

if [ "${#ticket_args[@]}" -eq 0 ]; then
  echo "WARNING: no tickets resolved for group '{{group_name}}'." >&2
fi

# extra_files appended literally (may be empty; may include -f/--file):
{{oracle_cmd}}   {{oracle_flags}}   --write-output "{{out_dir}}/09-observability-logging-metrics.md"   "${ticket_args[@]}"   {{extra_files}}   -p "$(cat <<'PROMPT'
Strategist question #09  (ticket-driven, group: {{group_name}})

Reference: {{group_slug}}
Category: observability
Horizon: Immediate
ROI: 4.7 (impact=7, confidence=0.76, effort=1)

Question:
Using the attached tickets as the primary context, define what logging/metrics must exist to debug pack generation + step execution; propose minimal instrumentation plan.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 10) ROI=4.5 impact=7 confidence=0.74 effort=2 horizon=Immediate category=observability reference={{group_slug}}

# tickets attached directly (deterministic; self-contained)
mapfile -t __tickets < <(python3 - <<'PY'
from __future__ import annotations
from pathlib import Path

TICKET_ROOT = "{{ticket_root}}"
TICKET_GLOB = "{{ticket_glob}}"
TICKET_PATHS = "{{ticket_paths}}".strip()
MAX = int("{{ticket_max_files}}")

root = Path(TICKET_ROOT)

def lex_sorted(ps):
    return sorted((str(p) for p in ps), key=lambda s: s)

if TICKET_PATHS:
    tickets = [Path(p.strip()) for p in TICKET_PATHS.split(",") if p.strip()]
else:
    tickets = list(root.glob(TICKET_GLOB)) if root.exists() else []

tickets = [Path(p) for p in lex_sorted(tickets)]
if MAX and MAX > 0:
    tickets = tickets[:MAX]

for p in tickets:
    print(str(p))
PY
)

ticket_args=()
for p in "${__tickets[@]}"; do
  ticket_args+=(-f "$p")
done

if [ "${#ticket_args[@]}" -eq 0 ]; then
  echo "WARNING: no tickets resolved for group '{{group_name}}'." >&2
fi

# extra_files appended literally (may be empty; may include -f/--file):
{{oracle_cmd}}   {{oracle_flags}}   --write-output "{{out_dir}}/10-observability-tracing.md"   "${ticket_args[@]}"   {{extra_files}}   -p "$(cat <<'PROMPT'
Strategist question #10  (ticket-driven, group: {{group_name}})

Reference: {{group_slug}}
Category: observability
Horizon: Immediate
ROI: 4.5 (impact=7, confidence=0.74, effort=2)

Question:
Using the attached tickets as the primary context, define tracing/correlation strategy across pack steps and downstream tools; identify required IDs and propagation.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 11) ROI=4.1 impact=6 confidence=0.70 effort=2 horizon=NearTerm category=permissions reference={{group_slug}}

# tickets attached directly (deterministic; self-contained)
mapfile -t __tickets < <(python3 - <<'PY'
from __future__ import annotations
from pathlib import Path

TICKET_ROOT = "{{ticket_root}}"
TICKET_GLOB = "{{ticket_glob}}"
TICKET_PATHS = "{{ticket_paths}}".strip()
MAX = int("{{ticket_max_files}}")

root = Path(TICKET_ROOT)

def lex_sorted(ps):
    return sorted((str(p) for p in ps), key=lambda s: s)

if TICKET_PATHS:
    tickets = [Path(p.strip()) for p in TICKET_PATHS.split(",") if p.strip()]
else:
    tickets = list(root.glob(TICKET_GLOB)) if root.exists() else []

tickets = [Path(p) for p in lex_sorted(tickets)]
if MAX and MAX > 0:
    tickets = tickets[:MAX]

for p in tickets:
    print(str(p))
PY
)

ticket_args=()
for p in "${__tickets[@]}"; do
  ticket_args+=(-f "$p")
done

if [ "${#ticket_args[@]}" -eq 0 ]; then
  echo "WARNING: no tickets resolved for group '{{group_name}}'." >&2
fi

# extra_files appended literally (may be empty; may include -f/--file):
{{oracle_cmd}}   {{oracle_flags}}   --write-output "{{out_dir}}/11-permissions-authz-gaps.md"   "${ticket_args[@]}"   {{extra_files}}   -p "$(cat <<'PROMPT'
Strategist question #11  (ticket-driven, group: {{group_name}})

Reference: {{group_slug}}
Category: permissions
Horizon: NearTerm
ROI: 4.1 (impact=6, confidence=0.70, effort=2)

Question:
Using the attached tickets as the primary context, identify permission/authz boundaries implied by tickets (file access, command execution, network); propose safe defaults.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 12) ROI=3.9 impact=6 confidence=0.68 effort=2 horizon=NearTerm category=permissions reference={{group_slug}}

# tickets attached directly (deterministic; self-contained)
mapfile -t __tickets < <(python3 - <<'PY'
from __future__ import annotations
from pathlib import Path

TICKET_ROOT = "{{ticket_root}}"
TICKET_GLOB = "{{ticket_glob}}"
TICKET_PATHS = "{{ticket_paths}}".strip()
MAX = int("{{ticket_max_files}}")

root = Path(TICKET_ROOT)

def lex_sorted(ps):
    return sorted((str(p) for p in ps), key=lambda s: s)

if TICKET_PATHS:
    tickets = [Path(p.strip()) for p in TICKET_PATHS.split(",") if p.strip()]
else:
    tickets = list(root.glob(TICKET_GLOB)) if root.exists() else []

tickets = [Path(p) for p in lex_sorted(tickets)]
if MAX and MAX > 0:
    tickets = tickets[:MAX]

for p in tickets:
    print(str(p))
PY
)

ticket_args=()
for p in "${__tickets[@]}"; do
  ticket_args+=(-f "$p")
done

if [ "${#ticket_args[@]}" -eq 0 ]; then
  echo "WARNING: no tickets resolved for group '{{group_name}}'." >&2
fi

# extra_files appended literally (may be empty; may include -f/--file):
{{oracle_cmd}}   {{oracle_flags}}   --write-output "{{out_dir}}/12-permissions-secrets-config.md"   "${ticket_args[@]}"   {{extra_files}}   -p "$(cat <<'PROMPT'
Strategist question #12  (ticket-driven, group: {{group_name}})

Reference: {{group_slug}}
Category: permissions
Horizon: NearTerm
ROI: 3.9 (impact=6, confidence=0.68, effort=2)

Question:
Using the attached tickets as the primary context, identify secrets/config handling needs (API keys, tokens); propose secure config discovery and redaction.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 13) ROI=3.8 impact=6 confidence=0.66 effort=3 horizon=MidTerm category=migrations reference={{group_slug}}

# tickets attached directly (deterministic; self-contained)
mapfile -t __tickets < <(python3 - <<'PY'
from __future__ import annotations
from pathlib import Path

TICKET_ROOT = "{{ticket_root}}"
TICKET_GLOB = "{{ticket_glob}}"
TICKET_PATHS = "{{ticket_paths}}".strip()
MAX = int("{{ticket_max_files}}")

root = Path(TICKET_ROOT)

def lex_sorted(ps):
    return sorted((str(p) for p in ps), key=lambda s: s)

if TICKET_PATHS:
    tickets = [Path(p.strip()) for p in TICKET_PATHS.split(",") if p.strip()]
else:
    tickets = list(root.glob(TICKET_GLOB)) if root.exists() else []

tickets = [Path(p) for p in lex_sorted(tickets)]
if MAX and MAX > 0:
    tickets = tickets[:MAX]

for p in tickets:
    print(str(p))
PY
)

ticket_args=()
for p in "${__tickets[@]}"; do
  ticket_args+=(-f "$p")
done

if [ "${#ticket_args[@]}" -eq 0 ]; then
  echo "WARNING: no tickets resolved for group '{{group_name}}'." >&2
fi

# extra_files appended literally (may be empty; may include -f/--file):
{{oracle_cmd}}   {{oracle_flags}}   --write-output "{{out_dir}}/13-migrations-schema-migrations.md"   "${ticket_args[@]}"   {{extra_files}}   -p "$(cat <<'PROMPT'
Strategist question #13  (ticket-driven, group: {{group_name}})

Reference: {{group_slug}}
Category: migrations
Horizon: MidTerm
ROI: 3.8 (impact=6, confidence=0.66, effort=3)

Question:
Using the attached tickets as the primary context, identify any required migrations (schema/format/CLI flags); define migration strategy and compat approach.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 14) ROI=3.7 impact=6 confidence=0.64 effort=3 horizon=MidTerm category=migrations reference={{group_slug}}

# tickets attached directly (deterministic; self-contained)
mapfile -t __tickets < <(python3 - <<'PY'
from __future__ import annotations
from pathlib import Path

TICKET_ROOT = "{{ticket_root}}"
TICKET_GLOB = "{{ticket_glob}}"
TICKET_PATHS = "{{ticket_paths}}".strip()
MAX = int("{{ticket_max_files}}")

root = Path(TICKET_ROOT)

def lex_sorted(ps):
    return sorted((str(p) for p in ps), key=lambda s: s)

if TICKET_PATHS:
    tickets = [Path(p.strip()) for p in TICKET_PATHS.split(",") if p.strip()]
else:
    tickets = list(root.glob(TICKET_GLOB)) if root.exists() else []

tickets = [Path(p) for p in lex_sorted(tickets)]
if MAX and MAX > 0:
    tickets = tickets[:MAX]

for p in tickets:
    print(str(p))
PY
)

ticket_args=()
for p in "${__tickets[@]}"; do
  ticket_args+=(-f "$p")
done

if [ "${#ticket_args[@]}" -eq 0 ]; then
  echo "WARNING: no tickets resolved for group '{{group_name}}'." >&2
fi

# extra_files appended literally (may be empty; may include -f/--file):
{{oracle_cmd}}   {{oracle_flags}}   --write-output "{{out_dir}}/14-migrations-backfill-plan.md"   "${ticket_args[@]}"   {{extra_files}}   -p "$(cat <<'PROMPT'
Strategist question #14  (ticket-driven, group: {{group_name}})

Reference: {{group_slug}}
Category: migrations
Horizon: MidTerm
ROI: 3.7 (impact=6, confidence=0.64, effort=3)

Question:
Using the attached tickets as the primary context, define any needed backfill/one-time transforms; estimate risks; define verification plan.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 15) ROI=4.6 impact=6 confidence=0.74 effort=1 horizon=Immediate category=UX flows reference={{group_slug}}

# tickets attached directly (deterministic; self-contained)
mapfile -t __tickets < <(python3 - <<'PY'
from __future__ import annotations
from pathlib import Path

TICKET_ROOT = "{{ticket_root}}"
TICKET_GLOB = "{{ticket_glob}}"
TICKET_PATHS = "{{ticket_paths}}".strip()
MAX = int("{{ticket_max_files}}")

root = Path(TICKET_ROOT)

def lex_sorted(ps):
    return sorted((str(p) for p in ps), key=lambda s: s)

if TICKET_PATHS:
    tickets = [Path(p.strip()) for p in TICKET_PATHS.split(",") if p.strip()]
else:
    tickets = list(root.glob(TICKET_GLOB)) if root.exists() else []

tickets = [Path(p) for p in lex_sorted(tickets)]
if MAX and MAX > 0:
    tickets = tickets[:MAX]

for p in tickets:
    print(str(p))
PY
)

ticket_args=()
for p in "${__tickets[@]}"; do
  ticket_args+=(-f "$p")
done

if [ "${#ticket_args[@]}" -eq 0 ]; then
  echo "WARNING: no tickets resolved for group '{{group_name}}'." >&2
fi

# extra_files appended literally (may be empty; may include -f/--file):
{{oracle_cmd}}   {{oracle_flags}}   --write-output "{{out_dir}}/15-ux-flows-user-journeys.md"   "${ticket_args[@]}"   {{extra_files}}   -p "$(cat <<'PROMPT'
Strategist question #15  (ticket-driven, group: {{group_name}})

Reference: {{group_slug}}
Category: UX flows
Horizon: Immediate
ROI: 4.6 (impact=6, confidence=0.74, effort=1)

Question:
Using the attached tickets as the primary context, identify UX/TUI workflows implied by tickets; define user journey states and expected outputs.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 16) ROI=4.3 impact=6 confidence=0.72 effort=2 horizon=Immediate category=UX flows reference={{group_slug}}

# tickets attached directly (deterministic; self-contained)
mapfile -t __tickets < <(python3 - <<'PY'
from __future__ import annotations
from pathlib import Path

TICKET_ROOT = "{{ticket_root}}"
TICKET_GLOB = "{{ticket_glob}}"
TICKET_PATHS = "{{ticket_paths}}".strip()
MAX = int("{{ticket_max_files}}")

root = Path(TICKET_ROOT)

def lex_sorted(ps):
    return sorted((str(p) for p in ps), key=lambda s: s)

if TICKET_PATHS:
    tickets = [Path(p.strip()) for p in TICKET_PATHS.split(",") if p.strip()]
else:
    tickets = list(root.glob(TICKET_GLOB)) if root.exists() else []

tickets = [Path(p) for p in lex_sorted(tickets)]
if MAX and MAX > 0:
    tickets = tickets[:MAX]

for p in tickets:
    print(str(p))
PY
)

ticket_args=()
for p in "${__tickets[@]}"; do
  ticket_args+=(-f "$p")
done

if [ "${#ticket_args[@]}" -eq 0 ]; then
  echo "WARNING: no tickets resolved for group '{{group_name}}'." >&2
fi

# extra_files appended literally (may be empty; may include -f/--file):
{{oracle_cmd}}   {{oracle_flags}}   --write-output "{{out_dir}}/16-ux-flows-edge-cases.md"   "${ticket_args[@]}"   {{extra_files}}   -p "$(cat <<'PROMPT'
Strategist question #16  (ticket-driven, group: {{group_name}})

Reference: {{group_slug}}
Category: UX flows
Horizon: Immediate
ROI: 4.3 (impact=6, confidence=0.72, effort=2)

Question:
Using the attached tickets as the primary context, identify edge cases in UX flows (cancel, resume, partial runs); define minimal UX behavior.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 17) ROI=4.9 impact=7 confidence=0.78 effort=1 horizon=Immediate category=failure modes reference={{group_slug}}

# tickets attached directly (deterministic; self-contained)
mapfile -t __tickets < <(python3 - <<'PY'
from __future__ import annotations
from pathlib import Path

TICKET_ROOT = "{{ticket_root}}"
TICKET_GLOB = "{{ticket_glob}}"
TICKET_PATHS = "{{ticket_paths}}".strip()
MAX = int("{{ticket_max_files}}")

root = Path(TICKET_ROOT)

def lex_sorted(ps):
    return sorted((str(p) for p in ps), key=lambda s: s)

if TICKET_PATHS:
    tickets = [Path(p.strip()) for p in TICKET_PATHS.split(",") if p.strip()]
else:
    tickets = list(root.glob(TICKET_GLOB)) if root.exists() else []

tickets = [Path(p) for p in lex_sorted(tickets)]
if MAX and MAX > 0:
    tickets = tickets[:MAX]

for p in tickets:
    print(str(p))
PY
)

ticket_args=()
for p in "${__tickets[@]}"; do
  ticket_args+=(-f "$p")
done

if [ "${#ticket_args[@]}" -eq 0 ]; then
  echo "WARNING: no tickets resolved for group '{{group_name}}'." >&2
fi

# extra_files appended literally (may be empty; may include -f/--file):
{{oracle_cmd}}   {{oracle_flags}}   --write-output "{{out_dir}}/17-failure-modes-timeouts-retries.md"   "${ticket_args[@]}"   {{extra_files}}   -p "$(cat <<'PROMPT'
Strategist question #17  (ticket-driven, group: {{group_name}})

Reference: {{group_slug}}
Category: failure modes
Horizon: Immediate
ROI: 4.9 (impact=7, confidence=0.78, effort=1)

Question:
Using the attached tickets as the primary context, define timeouts/retries behavior for external calls; define failure classification and operator actions.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 18) ROI=4.4 impact=7 confidence=0.74 effort=2 horizon=Immediate category=failure modes reference={{group_slug}}

# tickets attached directly (deterministic; self-contained)
mapfile -t __tickets < <(python3 - <<'PY'
from __future__ import annotations
from pathlib import Path

TICKET_ROOT = "{{ticket_root}}"
TICKET_GLOB = "{{ticket_glob}}"
TICKET_PATHS = "{{ticket_paths}}".strip()
MAX = int("{{ticket_max_files}}")

root = Path(TICKET_ROOT)

def lex_sorted(ps):
    return sorted((str(p) for p in ps), key=lambda s: s)

if TICKET_PATHS:
    tickets = [Path(p.strip()) for p in TICKET_PATHS.split(",") if p.strip()]
else:
    tickets = list(root.glob(TICKET_GLOB)) if root.exists() else []

tickets = [Path(p) for p in lex_sorted(tickets)]
if MAX and MAX > 0:
    tickets = tickets[:MAX]

for p in tickets:
    print(str(p))
PY
)

ticket_args=()
for p in "${__tickets[@]}"; do
  ticket_args+=(-f "$p")
done

if [ "${#ticket_args[@]}" -eq 0 ]; then
  echo "WARNING: no tickets resolved for group '{{group_name}}'." >&2
fi

# extra_files appended literally (may be empty; may include -f/--file):
{{oracle_cmd}}   {{oracle_flags}}   --write-output "{{out_dir}}/18-failure-modes-rollback-plan.md"   "${ticket_args[@]}"   {{extra_files}}   -p "$(cat <<'PROMPT'
Strategist question #18  (ticket-driven, group: {{group_name}})

Reference: {{group_slug}}
Category: failure modes
Horizon: Immediate
ROI: 4.4 (impact=7, confidence=0.74, effort=2)

Question:
Using the attached tickets as the primary context, define rollback plan for partial runs and how to preserve artifacts; define 'safe to re-run' semantics.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 19) ROI=4.0 impact=6 confidence=0.70 effort=2 horizon=NearTerm category=feature flags reference={{group_slug}}

# tickets attached directly (deterministic; self-contained)
mapfile -t __tickets < <(python3 - <<'PY'
from __future__ import annotations
from pathlib import Path

TICKET_ROOT = "{{ticket_root}}"
TICKET_GLOB = "{{ticket_glob}}"
TICKET_PATHS = "{{ticket_paths}}".strip()
MAX = int("{{ticket_max_files}}")

root = Path(TICKET_ROOT)

def lex_sorted(ps):
    return sorted((str(p) for p in ps), key=lambda s: s)

if TICKET_PATHS:
    tickets = [Path(p.strip()) for p in TICKET_PATHS.split(",") if p.strip()]
else:
    tickets = list(root.glob(TICKET_GLOB)) if root.exists() else []

tickets = [Path(p) for p in lex_sorted(tickets)]
if MAX and MAX > 0:
    tickets = tickets[:MAX]

for p in tickets:
    print(str(p))
PY
)

ticket_args=()
for p in "${__tickets[@]}"; do
  ticket_args+=(-f "$p")
done

if [ "${#ticket_args[@]}" -eq 0 ]; then
  echo "WARNING: no tickets resolved for group '{{group_name}}'." >&2
fi

# extra_files appended literally (may be empty; may include -f/--file):
{{oracle_cmd}}   {{oracle_flags}}   --write-output "{{out_dir}}/19-feature-flags-flag-plan.md"   "${ticket_args[@]}"   {{extra_files}}   -p "$(cat <<'PROMPT'
Strategist question #19  (ticket-driven, group: {{group_name}})

Reference: {{group_slug}}
Category: feature flags
Horizon: NearTerm
ROI: 4.0 (impact=6, confidence=0.70, effort=2)

Question:
Using the attached tickets as the primary context, define feature-flag strategy for rollout (scopes, defaults, telemetry); ensure compat for existing users.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

# 20) ROI=3.8 impact=6 confidence=0.68 effort=2 horizon=NearTerm category=feature flags reference={{group_slug}}

# tickets attached directly (deterministic; self-contained)
mapfile -t __tickets < <(python3 - <<'PY'
from __future__ import annotations
from pathlib import Path

TICKET_ROOT = "{{ticket_root}}"
TICKET_GLOB = "{{ticket_glob}}"
TICKET_PATHS = "{{ticket_paths}}".strip()
MAX = int("{{ticket_max_files}}")

root = Path(TICKET_ROOT)

def lex_sorted(ps):
    return sorted((str(p) for p in ps), key=lambda s: s)

if TICKET_PATHS:
    tickets = [Path(p.strip()) for p in TICKET_PATHS.split(",") if p.strip()]
else:
    tickets = list(root.glob(TICKET_GLOB)) if root.exists() else []

tickets = [Path(p) for p in lex_sorted(tickets)]
if MAX and MAX > 0:
    tickets = tickets[:MAX]

for p in tickets:
    print(str(p))
PY
)

ticket_args=()
for p in "${__tickets[@]}"; do
  ticket_args+=(-f "$p")
done

if [ "${#ticket_args[@]}" -eq 0 ]; then
  echo "WARNING: no tickets resolved for group '{{group_name}}'." >&2
fi

# extra_files appended literally (may be empty; may include -f/--file):
{{oracle_cmd}}   {{oracle_flags}}   --write-output "{{out_dir}}/20-feature-flags-compat-rollout.md"   "${ticket_args[@]}"   {{extra_files}}   -p "$(cat <<'PROMPT'
Strategist question #20  (ticket-driven, group: {{group_name}})

Reference: {{group_slug}}
Category: feature flags
Horizon: NearTerm
ROI: 3.8 (impact=6, confidence=0.68, effort=2)

Question:
Using the attached tickets as the primary context, define minimal compat-safe rollout plan and guardrails; include fallback behavior and monitoring gates.

Constraints: None
Non-goals: None

Answer format:
1) Direct answer (1–10 bullets, evidence-cited)
2) Risks/unknowns (bullets)
3) Next smallest concrete experiment (exactly one action)
4) If evidence is insufficient, name the exact missing file/path pattern(s) to attach next.
PROMPT
)"

```

## Coverage check
- contracts/interfaces: OK
- invariants: OK
- caching/state: OK
- background jobs: OK
- observability: OK
- permissions: OK
- migrations: OK
- UX flows: OK
- failure modes: OK
- feature flags: OK
```

skills/oraclepack-tickets-pack-grouped/scripts/generate_grouped_packs.py
```
#!/usr/bin/env python3
from __future__ import annotations

import datetime as _dt
import math
import json
import re
import sys
from pathlib import Path
from typing import Dict, Iterable, List, Tuple

STOPWORDS = {
    "the", "and", "for", "with", "from", "this", "that", "into", "over", "under", "when",
    "then", "than", "else", "only", "must", "should", "could", "would", "will", "shall",
    "ticket", "tickets", "oraclepack", "oracle", "pack", "packs",
}


def _parse_kv_args(argv: List[str]) -> Dict[str, str]:
    args: Dict[str, str] = {}
    for raw in argv:
        if "=" not in raw:
            continue
        k, v = raw.split("=", 1)
        args[k.strip()] = v.strip()
    return args


def _today() -> str:
    return _dt.date.today().isoformat()


def _slugify(s: str) -> str:
    s = s.strip().lower()
    s = re.sub(r"[^a-z0-9]+", "-", s)
    s = re.sub(r"-+", "-", s).strip("-")
    return s or "group"


def _tokenize(text: str) -> List[str]:
    text = text.lower()
    text = re.sub(r"[^a-z0-9]+", " ", text)
    toks = [t for t in text.split() if len(t) >= 3 and t not in STOPWORDS]
    return toks


def _normalize_title(text: str) -> str:
    text = text.strip().lower()
    text = re.sub(r"[^a-z0-9]+", " ", text)
    text = re.sub(r"\s+", " ", text).strip()
    return text


def _read_heading(path: Path) -> str:
    try:
        for line in path.read_text(encoding="utf-8", errors="replace").splitlines():
            if line.startswith("#"):
                return line.lstrip("#").strip()
    except FileNotFoundError:
        return ""
    return ""


def _collect_ticket_paths(ticket_root: str, ticket_glob: str, ticket_paths: str) -> List[Path]:
    if ticket_paths:
        parts = [p.strip() for p in ticket_paths.split(",") if p.strip()]
        return [Path(p) for p in parts]
    root = Path(ticket_root)
    if not root.exists():
        return []
    return [Path(p) for p in root.glob(ticket_glob)]


def _read_signature(path: Path, max_lines: int = 40) -> Tuple[str, str]:
    heading = ""
    lines: List[str] = []
    try:
        for line in path.read_text(encoding="utf-8", errors="replace").splitlines():
            if not heading and line.startswith("#"):
                heading = line.lstrip("#").strip()
            if line.strip():
                lines.append(line.strip())
            if len(lines) >= max_lines:
                break
    except FileNotFoundError:
        pass
    return heading, " ".join(lines)


def _read_text(path: Path) -> str:
    try:
        return path.read_text(encoding="utf-8", errors="replace")
    except FileNotFoundError:
        return ""


def _group_by_subdir(paths: Iterable[Path], ticket_root: str) -> Tuple[Dict[str, List[Path]], List[Path]]:
    root = Path(ticket_root)
    groups: Dict[str, List[Path]] = {}
    loose: List[Path] = []
    for p in paths:
        try:
            rel = p.relative_to(root)
        except ValueError:
            loose.append(p)
            continue
        if len(rel.parts) >= 2:
            key = rel.parts[0]
            groups.setdefault(key, []).append(p)
        else:
            loose.append(p)
    return groups, loose


def _group_tokens(group_name: str, paths: Iterable[Path]) -> set:
    tokens = set(_tokenize(group_name))
    for p in paths:
        tokens.update(_tokenize(p.stem))
        tokens.update(_tokenize(_read_heading(p)))
    return tokens


def _ticket_tokens(p: Path) -> set:
    toks = set(_tokenize(p.stem))
    heading, snippet = _read_signature(p)
    toks.update(_tokenize(heading))
    toks.update(_tokenize(snippet))
    return toks


def _signature_tokens(p: Path, body_chars: int) -> set:
    heading = _read_heading(p)
    body = _read_text(p)
    body = body[:body_chars]
    toks = set(_tokenize(p.stem))
    toks.update(_tokenize(heading))
    toks.update(_tokenize(body))
    return toks


def _jaccard(a: set, b: set) -> float:
    if not a or not b:
        return 0.0
    inter = a.intersection(b)
    union = a.union(b)
    return float(len(inter)) / float(len(union))


def _overlap(a: set, b: set) -> float:
    if not a or not b:
        return 0.0
    inter = a.intersection(b)
    denom = min(len(a), len(b))
    if denom == 0:
        return 0.0
    return float(len(inter)) / float(denom)


def _clusters_from_edges(nodes: List[str], edges: Dict[str, List[str]]) -> List[List[str]]:
    seen = set()
    clusters: List[List[str]] = []
    for n in nodes:
        if n in seen:
            continue
        stack = [n]
        comp = []
        seen.add(n)
        while stack:
            cur = stack.pop()
            comp.append(cur)
            for nxt in edges.get(cur, []):
                if nxt not in seen:
                    seen.add(nxt)
                    stack.append(nxt)
        clusters.append(sorted(comp))
    return clusters


def _dedupe_clusters(
    paths: List[Path],
    body_chars: int,
    jaccard_hi: float,
    overlap_hi: float,
    overlap_lo: float,
    delta_min: float,
) -> Tuple[List[List[str]], Dict[str, str], Dict[str, Dict[str, object]], Dict[Tuple[str, str], Dict[str, float]]]:
    tokens: Dict[str, set] = {}
    sizes: Dict[str, int] = {}
    titles: Dict[str, str] = {}
    for p in paths:
        key = str(p)
        tokens[key] = _signature_tokens(p, body_chars)
        sizes[key] = len(_read_text(p))
        titles[key] = _normalize_title(_read_heading(p))

    nodes = sorted(tokens.keys())
    edges: Dict[str, List[str]] = {n: [] for n in nodes}
    pair_scores: Dict[Tuple[str, str], Dict[str, float]] = {}

    for i, a in enumerate(nodes):
        for b in nodes[i + 1 :]:
            jac = _jaccard(tokens[a], tokens[b])
            ov = _overlap(tokens[a], tokens[b])
            pair_scores[(a, b)] = {"jaccard": jac, "overlap": ov}
            if ov >= overlap_hi or (jac >= jaccard_hi and ov >= overlap_lo):
                edges[a].append(b)
                edges[b].append(a)

    clusters = _clusters_from_edges(nodes, edges)
    cluster_meta: Dict[str, Dict[str, object]] = {}
    dup_map: Dict[str, str] = {}

    for idx, members in enumerate(clusters, start=1):
        if len(members) == 1:
            continue
        # canonical: largest content length, then lexicographic
        canon = sorted(
            members,
            key=lambda m: (-sizes.get(m, 0), m),
        )[0]
        deltas: List[str] = []
        redundant: List[str] = []
        for m in members:
            if m == canon:
                continue
            unique = tokens[m] - tokens[canon]
            unique_ratio = float(len(unique)) / float(max(1, len(tokens[m])))
            heading_diff = titles.get(m, "") != titles.get(canon, "")
            if unique_ratio >= delta_min or heading_diff:
                deltas.append(m)
            else:
                redundant.append(m)
            dup_map[m] = canon

        cluster_meta[str(idx)] = {
            "canonical": canon,
            "members": members,
            "deltas": sorted(deltas),
            "redundant": sorted(redundant),
        }

    return clusters, dup_map, cluster_meta, pair_scores


def _infer_groups(
    groups: Dict[str, List[Path]],
    loose: List[Path],
    min_score: float,
) -> Dict[str, List[Path]]:
    if not groups:
        return {"root": list(loose)}

    group_tokens = {k: _group_tokens(k, v) for k, v in groups.items()}
    for p in loose:
        tokens = _ticket_tokens(p)
        best = None
        best_score = -1.0
        for name in sorted(group_tokens.keys()):
            score = _jaccard(tokens, group_tokens[name])
            if score > best_score:
                best_score = score
                best = name
        if best is not None and best_score >= min_score:
            groups.setdefault(best, []).append(p)
        else:
            groups.setdefault("misc", []).append(p)
    return groups


def _chunk(paths: List[Path], size: int) -> List[List[Path]]:
    if size <= 0:
        return [paths]
    return [paths[i : i + size] for i in range(0, len(paths), size)]


def _chunk_by_limits(
    paths: List[Path],
    max_files: int,
    max_chars: int,
) -> List[List[Path]]:
    if max_files <= 0 and max_chars <= 0:
        return [paths]
    chunks: List[List[Path]] = []
    cur: List[Path] = []
    cur_chars = 0
    for p in paths:
        size = len(_read_text(p))
        if cur:
            if (max_files > 0 and len(cur) >= max_files) or (
                max_chars > 0 and cur_chars + size > max_chars
            ):
                chunks.append(cur)
                cur = []
                cur_chars = 0
        cur.append(p)
        cur_chars += size
    if cur:
        chunks.append(cur)
    return chunks


def _render_template(template: str, mapping: Dict[str, str]) -> str:
    out = template
    for key, val in mapping.items():
        out = out.replace("{{" + key + "}}", val)
    unresolved = sorted(set(re.findall(r"\{\{([^}]+)\}\}", out)))
    if unresolved:
        raise ValueError(f"Unresolved template placeholders: {unresolved}")
    return out


def _write_merge_file(
    out_dir: Path,
    cluster_id: str,
    canonical: str,
    deltas: List[str],
    redundant: List[str],
    body_chars: int,
) -> Path:
    merge_dir = out_dir / "_ticket_merges"
    merge_dir.mkdir(parents=True, exist_ok=True)
    path = merge_dir / f"cluster-{int(cluster_id):04d}.md"

    def _cap(text: str) -> str:
        if len(text) <= body_chars:
            return text
        return text[:body_chars] + "\n[... truncated ...]\n"

    lines: List[str] = []
    lines.append(f"# Ticket Merge Cluster {cluster_id}")
    lines.append("")
    lines.append("## Canonical")
    lines.append(f"- path: {canonical}")
    lines.append("")
    lines.append(_cap(_read_text(Path(canonical))))
    lines.append("")

    members = deltas + redundant
    if members:
        lines.append("## Also reported in")
        for m in members:
            lines.append(f"- {m}")
        lines.append("")

    if deltas:
        lines.append("## Unique details from related tickets")
        for m in deltas:
            text = _read_text(Path(m))
            toks = _signature_tokens(Path(m), body_chars)
            canon_toks = _signature_tokens(Path(canonical), body_chars)
            unique = toks - canon_toks
            sel: List[str] = []
            for ln in text.splitlines():
                lnt = _tokenize(ln)
                if any(t in unique for t in lnt):
                    sel.append(ln)
                if len(sel) >= 60:
                    break
            lines.append(f"### {m}")
            if sel:
                lines.extend(sel)
            else:
                lines.append("(no unique lines detected within cap)")
            lines.append("")

    path.write_text("\n".join(lines), encoding="utf-8")
    return path


def main() -> int:
    if len(sys.argv) == 1:
        print("Select how to run:")
        print("1) Use defaults (no args)")
        print("2) Provide custom args (show usage)")
        choice = input("Enter choice [1-2]: ").strip() or "1"
        if choice == "2":
            print("Usage: generate_grouped_packs.py key=value [key=value ...]")
            return 0

    args = _parse_kv_args(sys.argv[1:])
    codebase_name = args.get("codebase_name", "Unknown")
    out_dir = args.get("out_dir", f"docs/oracle-questions-{_today()}")
    oracle_cmd = args.get("oracle_cmd", "oracle")
    oracle_flags = args.get("oracle_flags", "--files-report")
    extra_files = args.get("extra_files", "")
    ticket_root = args.get("ticket_root", ".tickets")
    ticket_glob = args.get("ticket_glob", "**/*.md")
    ticket_paths = args.get("ticket_paths", "")
    ticket_max_files = args.get("ticket_max_files", "25")
    group_mode = args.get("group_mode", "subdir+infer")
    group_min_score = float(args.get("group_min_score", "0.08"))
    group_max_files = int(args.get("group_max_files", "25"))
    group_max_chars = int(args.get("group_max_chars", "200000"))
    dedupe_mode = args.get("dedupe_mode", "report")
    dedupe_jaccard = float(args.get("dedupe_jaccard", "0.55"))
    dedupe_overlap_hi = float(args.get("dedupe_overlap_hi", "0.80"))
    dedupe_overlap_lo = float(args.get("dedupe_overlap_lo", "0.70"))
    dedupe_delta_min = float(args.get("dedupe_delta_min", "0.15"))
    dedupe_body_chars = int(args.get("dedupe_body_chars", "2000"))
    mode = args.get("mode", "tickets-grouped-direct")

    template_path = Path(__file__).resolve().parent.parent / "references" / "tickets-pack-template.md"
    template = template_path.read_text(encoding="utf-8")

    paths = _collect_ticket_paths(ticket_root, ticket_glob, ticket_paths)
    paths = sorted((str(p) for p in paths))
    paths = [Path(p) for p in paths]

    original_paths = list(paths)
    dup_map: Dict[str, str] = {}
    cluster_meta: Dict[str, Dict[str, object]] = {}
    dup_pairs: Dict[Tuple[str, str], Dict[str, float]] = {}
    if dedupe_mode != "off":
        _clusters, dup_map, cluster_meta, dup_pairs = _dedupe_clusters(
            paths,
            body_chars=dedupe_body_chars,
            jaccard_hi=dedupe_jaccard,
            overlap_hi=dedupe_overlap_hi,
            overlap_lo=dedupe_overlap_lo,
            delta_min=dedupe_delta_min,
        )

    # Build grouping base: canonical tickets + singletons
    canonical_set = {meta["canonical"] for meta in cluster_meta.values()}
    dup_set = set(dup_map.keys())
    base_paths: List[Path] = []
    for p in paths:
        sp = str(p)
        if sp in dup_set:
            continue
        base_paths.append(p)

    groups: Dict[str, List[Path]] = {}
    loose: List[Path] = []
    if "subdir" in group_mode:
        groups, loose = _group_by_subdir(base_paths, ticket_root)
    else:
        loose = list(base_paths)

    if "infer" in group_mode:
        groups = _infer_groups(groups, loose, group_min_score)
    else:
        groups.setdefault("misc", []).extend(loose)

    dedupe_plan: Dict[str, Dict[str, object]] = {}
    merge_files: Dict[str, str] = {}
    if cluster_meta:
        primary_to_group: Dict[str, str] = {}
        for gname in groups:
            for p in groups[gname]:
                primary_to_group[str(p)] = gname

        for cluster_id, meta in sorted(cluster_meta.items(), key=lambda x: int(x[0])):
            canonical = meta["canonical"]
            deltas = list(meta["deltas"])
            redundant = list(meta["redundant"])
            gname = primary_to_group.get(canonical, "misc")

            if dedupe_mode == "merge":
                merge_path = _write_merge_file(
                    Path(out_dir),
                    cluster_id=cluster_id,
                    canonical=canonical,
                    deltas=deltas,
                    redundant=redundant,
                    body_chars=dedupe_body_chars,
                )
                merge_files[canonical] = str(merge_path)
                # Replace canonical in group with merge file
                groups[gname] = [p for p in groups[gname] if str(p) != canonical]
                groups[gname].append(merge_path)
            else:
                # report/prune: append related tickets to canonical group
                keep = deltas if dedupe_mode == "prune" else deltas + redundant
                for p in keep:
                    groups.setdefault(gname, []).append(Path(p))

            dedupe_plan[cluster_id] = {
                "canonical": canonical,
                "group": gname,
                "deltas": sorted(deltas),
                "redundant": sorted(redundant),
                "mode": dedupe_mode,
            }

    # Ensure stable order
    for k in sorted(groups.keys()):
        groups[k] = sorted((str(p) for p in groups[k]))
        groups[k] = [Path(p) for p in groups[k]]

    original_set = {str(p) for p in original_paths}
    assignment: Dict[str, str] = {}
    for gname, gpaths in groups.items():
        for p in gpaths:
            sp = str(p)
            if sp in original_set:
                if sp in assignment:
                    raise SystemExit(f"[ERROR] Ticket assigned to multiple groups: {sp}")
                assignment[sp] = gname

    for meta in dedupe_plan.values():
        gname = meta["group"]
        for sp in [meta["canonical"]] + meta["deltas"] + meta["redundant"]:
            if sp not in assignment:
                assignment[sp] = gname

    missing = sorted(original_set - set(assignment.keys()))
    if missing:
        raise SystemExit(f"[ERROR] Tickets missing group assignment: {missing}")

    base_out = Path(out_dir)
    packs_dir = base_out / "packs"
    packs_dir.mkdir(parents=True, exist_ok=True)

    grouping_report: Dict[str, List[str]] = {}
    manifest_groups: List[Dict[str, object]] = []
    group_originals: Dict[str, List[str]] = {g: [] for g in groups.keys()}
    for ticket, gname in assignment.items():
        group_originals.setdefault(gname, []).append(ticket)
    for group_name in sorted(groups.keys()):
        group_paths = groups[group_name]
        grouping_report[group_name] = [str(p) for p in group_paths]

        parts = _chunk_by_limits(group_paths, group_max_files, group_max_chars)
        for idx, part in enumerate(parts, start=1):
            part_suffix = f"-part-{idx:02d}" if len(parts) > 1 else ""
            group_slug = _slugify(group_name + part_suffix)

            pack_out_dir = str(base_out / group_slug)
            pack_file = packs_dir / f"{group_slug}.md"

            mapping = {
                "codebase_name": codebase_name,
                "out_dir": pack_out_dir,
                "oracle_cmd": oracle_cmd,
                "oracle_flags": oracle_flags,
                "extra_files": extra_files,
                "ticket_root": ticket_root,
                "ticket_glob": ticket_glob,
                "ticket_paths": ",".join(str(p) for p in part),
                "ticket_max_files": str(min(len(part), max(1, group_max_files))),
                "group_name": group_name,
                "group_slug": group_slug,
                "mode": mode,
            }

            content = _render_template(template, mapping)
            pack_file.write_text(content, encoding="utf-8")

            manifest_groups.append(
                {
                    "group": group_name,
                    "slug": group_slug,
                    "part": idx,
                    "pack_path": str(pack_file),
                    "out_dir": pack_out_dir,
                    "attached_paths": [str(p) for p in part],
                    "original_tickets": sorted(group_originals.get(group_name, [])),
                }
            )

    (base_out / "_groups.json").write_text(
        json.dumps(grouping_report, indent=2, sort_keys=True),
        encoding="utf-8",
    )

    if dup_map:
        (base_out / "_duplicates.json").write_text(
            json.dumps(dup_map, indent=2, sort_keys=True),
            encoding="utf-8",
        )

    if dedupe_plan:
        (base_out / "_dedupe_plan.json").write_text(
            json.dumps(dedupe_plan, indent=2, sort_keys=True),
            encoding="utf-8",
        )

    if cluster_meta:
        pairs_out = [
            {"a": a, "b": b, **scores} for (a, b), scores in sorted(dup_pairs.items())
        ]
        (base_out / "_dupes_possible.json").write_text(
            json.dumps({"clusters": cluster_meta, "pairs": pairs_out}, indent=2, sort_keys=True),
            encoding="utf-8",
        )

    (base_out / "manifest.json").write_text(
        json.dumps({"groups": manifest_groups}, indent=2, sort_keys=True),
        encoding="utf-8",
    )

    print(f"[OK] wrote packs to: {packs_dir}")
    print(f"[OK] wrote grouping map: {base_out / '_groups.json'}")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
```

skills/oraclepack-tickets-pack-grouped/scripts/lint_attachments.py
```
import argparse
import re
import sys
from dataclasses import dataclass
from pathlib import Path
from typing import List, Tuple


@dataclass
class Step:
    n: str
    lines: List[str]


def _read_text(path: Path) -> str:
    try:
        return path.read_text(encoding="utf-8")
    except UnicodeDecodeError:
        return path.read_text(encoding="utf-8", errors="replace")


def _extract_bash_fence(lines: List[str]) -> List[str]:
    fence_idxs = [i for i, ln in enumerate(lines) if ln.startswith("```")]
    if len(fence_idxs) != 2:
        raise ValueError(f"Expected exactly one fenced block (2 fence lines). Found {len(fence_idxs)}.")
    open_i, close_i = fence_idxs
    if lines[open_i].rstrip("\n") != "```bash":
        raise ValueError("Opening fence must be exactly ```bash.")
    if lines[close_i].rstrip("\n") != "```":
        raise ValueError("Closing fence must be exactly ```.")
    return [ln.rstrip("\n") for ln in lines[open_i + 1 : close_i]]


def _parse_steps(fence_lines: List[str]) -> List[Step]:
    header_re = re.compile(r"^#\s*(\d{2})\)\s+")
    header_idxs: List[Tuple[int, str]] = []
    for i, ln in enumerate(fence_lines):
        m = header_re.match(ln)
        if m:
            header_idxs.append((i, m.group(1)))

    if not header_idxs:
        raise ValueError("No step headers found inside bash fence.")

    steps: List[Step] = []
    for idx, (start_i, n) in enumerate(header_idxs):
        end_i = header_idxs[idx + 1][0] if idx + 1 < len(header_idxs) else len(fence_lines)
        steps.append(Step(n=n, lines=fence_lines[start_i:end_i]))
    return steps


def lint(path: Path) -> None:
    raw = _read_text(path)
    lines = raw.splitlines(True)
    fence = _extract_bash_fence(lines)
    steps = _parse_steps(fence)

    errors: List[str] = []
    for step in steps:
        joined = "\n".join(step.lines)

        if "_tickets_bundle" in joined:
            errors.append(f"Step {step.n}: found '_tickets_bundle' reference (direct-ticket packs must not use bundle).")

        if re.search(r"mapfile\s+-t\s+__tickets\s+<\s+<\(", joined) is None:
            errors.append(f"Step {step.n}: missing mapfile ticket discovery stanza.")

        if re.search(r"ticket_args=\(\)", joined) is None or re.search(r"ticket_args\+\=\(\s*(-f|--file)\b", joined) is None:
            errors.append(f"Step {step.n}: missing ticket_args builder (ticket_args+=(-f \"$p\")).")

        if re.search(r"\$\{ticket_args\[@\]\}", joined) is None:
            errors.append(f"Step {step.n}: missing ${'{'}ticket_args[@]{'}'} usage in oracle invocation.")

        # Heuristic: ensure we did not hardcode a non-existent bundle path.
        if re.search(r'(?<!\S)(-f|--file)(?!\S)\s+"[^"\n]*_tickets_bundle', joined):
            errors.append(f"Step {step.n}: contains a hardcoded _tickets_bundle attachment.")

    if errors:
        for e in errors:
            print(f"[ERROR] {e}", file=sys.stderr)
        sys.exit(1)

    print("[OK] Direct-ticket lint passed.")


def main() -> None:
    p = argparse.ArgumentParser(description="Lint ticket-driven Stage-1 packs (direct-ticket mode).")
    p.add_argument("pack_path", help="Path to the Markdown pack file")
    args = p.parse_args()

    path = Path(args.pack_path)
    if not path.exists():
        print(f"[ERROR] File not found: {path}", file=sys.stderr)
        sys.exit(1)

    lint(path)


if __name__ == "__main__":
    main()
```

skills/oraclepack-tickets-pack-grouped/scripts/render_group_packs.py
```
#!/usr/bin/env python3
from __future__ import annotations

import argparse
import json
import re
from pathlib import Path
from typing import Dict


def _render_template(template: str, mapping: Dict[str, str]) -> str:
    out = template
    for key, val in mapping.items():
        out = out.replace("{{" + key + "}}", val)
    unresolved = sorted(set(re.findall(r"\{\{([^}]+)\}\}", out)))
    if unresolved:
        raise ValueError(f"Unresolved template placeholders: {unresolved}")
    return out


def main() -> int:
    if len(sys.argv) == 1:
        print("Select how to run:")
        print("1) Use defaults (no args)")
        print("2) Provide custom args (show usage)")
        choice = input("Enter choice [1-2]: ").strip() or "1"
        if choice == "2":
            print("Usage: render_group_packs.py --manifest manifest.json --out-dir out")
            return 0

    p = argparse.ArgumentParser(description="Render group-specific bundle packs from manifest.")
    p.add_argument("--manifest", default="manifest.json")
    p.add_argument("--out-dir", default="docs/oracle-questions-sharded")
    p.add_argument("--template", default="/home/user/.codex/skills/oraclepack-tickets-pack-grouped/references/tickets-pack-template-bundle.md")
    p.add_argument("--codebase-name", default="Unknown")
    p.add_argument("--oracle-cmd", default="oracle")
    p.add_argument("--oracle-flags", default="--files-report")
    p.add_argument("--extra-files", default="")
    p.add_argument("--ticket-root", default=".tickets")
    p.add_argument("--ticket-glob", default="**/*.md")
    p.add_argument("--mode", default="tickets-bundle")
    args = p.parse_args()

    manifest_path = Path(args.manifest)
    if not manifest_path.exists():
        raise SystemExit(f"[ERROR] manifest not found: {manifest_path}")

    manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
    template = Path(args.template).read_text(encoding="utf-8")

    out_dir = Path(args.out_dir)
    packs_dir = out_dir / "packs"
    packs_dir.mkdir(parents=True, exist_ok=True)

    for group in manifest.get("groups", []):
        slug = group["slug"]
        tickets = group["tickets"]
        pack_dir = packs_dir / slug
        pack_dir.mkdir(parents=True, exist_ok=True)

        pack_path = pack_dir / f"oracle-pack_{slug}.md"
        bundle_path = pack_dir / f"tickets_bundle_{slug}.md"
        out_run_dir = pack_dir / "out"

        mapping = {
            "codebase_name": args.codebase_name,
            "out_dir": str(out_run_dir),
            "oracle_cmd": args.oracle_cmd,
            "oracle_flags": args.oracle_flags,
            "extra_files": args.extra_files,
            "ticket_root": args.ticket_root,
            "ticket_glob": args.ticket_glob,
            "ticket_paths": ",".join(tickets),
            "ticket_bundle_path": str(bundle_path),
            "mode": args.mode,
        }

        content = _render_template(template, mapping)
        pack_path.write_text(content, encoding="utf-8")
        group["pack_path"] = str(pack_path)

    manifest_path.write_text(json.dumps(manifest, indent=2, sort_keys=True), encoding="utf-8")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
```

skills/oraclepack-tickets-pack-grouped/scripts/shard_tickets.py
```
#!/usr/bin/env python3
from __future__ import annotations

import argparse
import json
import math
import re
from dataclasses import dataclass
from pathlib import Path
from typing import Dict, List, Tuple

STOPWORDS = {
    "the", "and", "for", "with", "from", "this", "that", "into", "over", "under", "when",
    "then", "than", "else", "only", "must", "should", "could", "would", "will", "shall",
    "ticket", "tickets", "oraclepack", "oracle", "pack", "packs",
}

SECTION_KEYS = {"summary", "acceptance", "criteria", "background", "context"}


@dataclass
class Ticket:
    path: Path
    text: str
    tokens: List[str]
    vector: List[float]


def _tokenize(text: str) -> List[str]:
    text = text.lower()
    text = re.sub(r"[^a-z0-9]+", " ", text)
    return [t for t in text.split() if len(t) >= 3 and t not in STOPWORDS]


def _read_text(path: Path) -> str:
    try:
        return path.read_text(encoding="utf-8", errors="replace")
    except FileNotFoundError:
        return ""


def _extract_repr(text: str, stem: str, max_chars: int) -> str:
    lines = text.splitlines()
    heading = ""
    sections: List[str] = []
    capture = False
    for line in lines:
        s = line.strip()
        if not heading and s.startswith("#"):
            heading = s.lstrip("#").strip()
        if s.startswith("#"):
            key = s.lstrip("#").strip().lower()
            capture = any(k in key for k in SECTION_KEYS)
            continue
        if capture and s:
            sections.append(s)
        if len(" ".join(sections)) >= max_chars:
            break
    body = " ".join(sections)
    base = " ".join([stem, heading, body])
    return base[:max_chars]


def _tfidf_vectors(texts: List[str]) -> Tuple[List[List[float]], List[str]]:
    docs = [
        [tok for tok in _tokenize(t)]
        for t in texts
    ]
    vocab: Dict[str, int] = {}
    df: Dict[str, int] = {}
    for toks in docs:
        seen = set()
        for tok in toks:
            if tok not in vocab:
                vocab[tok] = len(vocab)
            if tok not in seen:
                df[tok] = df.get(tok, 0) + 1
                seen.add(tok)

    n_docs = len(docs)
    idf = [0.0] * len(vocab)
    for tok, idx in vocab.items():
        idf[idx] = math.log((1 + n_docs) / (1 + df.get(tok, 1))) + 1.0

    vectors: List[List[float]] = []
    for toks in docs:
        tf: Dict[int, float] = {}
        for tok in toks:
            tf[vocab[tok]] = tf.get(vocab[tok], 0.0) + 1.0
        vec = [0.0] * len(vocab)
        for idx, count in tf.items():
            vec[idx] = count * idf[idx]
        # L2 normalize
        norm = math.sqrt(sum(v * v for v in vec)) or 1.0
        vec = [v / norm for v in vec]
        vectors.append(vec)

    inv_vocab = [None] * len(vocab)
    for tok, idx in vocab.items():
        inv_vocab[idx] = tok
    return vectors, inv_vocab


def _cosine(a: List[float], b: List[float]) -> float:
    return sum(x * y for x, y in zip(a, b))


def _centroid(vectors: List[List[float]]) -> List[float]:
    if not vectors:
        return []
    dim = len(vectors[0])
    out = [0.0] * dim
    for v in vectors:
        for i, val in enumerate(v):
            out[i] += val
    n = float(len(vectors)) or 1.0
    out = [v / n for v in out]
    norm = math.sqrt(sum(v * v for v in out)) or 1.0
    return [v / norm for v in out]


def _kmeans_split(vectors: List[List[float]], k: int, iters: int = 10) -> List[List[int]]:
    if k <= 1:
        return [list(range(len(vectors)))]
    # deterministic init: first k vectors
    centroids = [vectors[i][:] for i in range(k)]
    for _ in range(iters):
        clusters = [[] for _ in range(k)]
        for idx, v in enumerate(vectors):
            best = 0
            best_score = -1.0
            for c_idx, c in enumerate(centroids):
                score = _cosine(v, c)
                if score > best_score:
                    best_score = score
                    best = c_idx
            clusters[best].append(idx)
        new_centroids = []
        for cluster in clusters:
            if cluster:
                new_centroids.append(_centroid([vectors[i] for i in cluster]))
            else:
                new_centroids.append(centroids[len(new_centroids)])
        centroids = new_centroids
    return clusters


def main() -> int:
    if len(sys.argv) == 1:
        print("Select how to run:")
        print("1) Use defaults (no args)")
        print("2) Provide custom args (show usage)")
        choice = input("Enter choice [1-2]: ").strip() or "1"
        if choice == "2":
            print("Usage: shard_tickets.py --ticket-root .tickets --out-dir out")
            return 0

    p = argparse.ArgumentParser(description="Shard tickets into topic/domain groups.")
    p.add_argument("--ticket-root", default=".tickets")
    p.add_argument("--ticket-glob", default="**/*.md")
    p.add_argument("--ticket-paths", default="")
    p.add_argument("--out-dir", default="docs/oracle-questions-sharded")
    p.add_argument("--min-sim", type=float, default=0.15)
    p.add_argument("--max-group-size", type=int, default=25)
    p.add_argument("--min-group-size", type=int, default=1)
    p.add_argument("--max-bundle-chars", type=int, default=200000)
    p.add_argument("--repr-chars", type=int, default=2000)
    p.add_argument("--use-llm-for-ambiguous", action="store_true")
    args = p.parse_args()

    ticket_root = Path(args.ticket_root)
    if args.ticket_paths:
        paths = [Path(p.strip()) for p in args.ticket_paths.split(",") if p.strip()]
    else:
        paths = sorted(ticket_root.glob(args.ticket_glob), key=lambda p: str(p)) if ticket_root.exists() else []

    texts: List[str] = []
    tickets: List[Ticket] = []
    for pth in paths:
        txt = _read_text(pth)
        rep = _extract_repr(txt, pth.stem, args.repr_chars)
        texts.append(rep)

    vectors, vocab = _tfidf_vectors(texts)
    for pth, txt, vec in zip(paths, texts, vectors):
        tickets.append(Ticket(path=pth, text=txt, tokens=_tokenize(txt), vector=vec))

    groups: Dict[str, List[int]] = {}
    loose: List[int] = []
    for idx, t in enumerate(tickets):
        try:
            rel = t.path.relative_to(ticket_root)
        except ValueError:
            loose.append(idx)
            continue
        if len(rel.parts) >= 2:
            g = rel.parts[0]
            groups.setdefault(g, []).append(idx)
        else:
            loose.append(idx)

    # Compute centroids for subdir groups
    centroids: Dict[str, List[float]] = {}
    for g, idxs in groups.items():
        centroids[g] = _centroid([tickets[i].vector for i in idxs])

    # Assign loose tickets by similarity
    reasons: Dict[int, Dict[str, object]] = {}
    for idx in loose:
        best_g = None
        best_sim = -1.0
        for g, c in centroids.items():
            sim = _cosine(tickets[idx].vector, c)
            if sim > best_sim:
                best_sim = sim
                best_g = g
        if best_g is not None and best_sim >= args.min_sim:
            groups.setdefault(best_g, []).append(idx)
            reasons[idx] = {"assigned_to": best_g, "sim": best_sim, "reason": "tfidf"}
        else:
            groups.setdefault("misc", []).append(idx)
            reasons[idx] = {
                "assigned_to": "misc",
                "sim": best_sim,
                "reason": "ambiguous" if not args.use_llm_for_ambiguous else "ambiguous_llm_needed",
            }

    # Merge small groups
    if args.min_group_size > 1 and len(groups) > 1:
        for g in sorted(list(groups.keys())):
            if g == "misc":
                continue
            if len(groups[g]) < args.min_group_size:
                # merge into nearest group
                g_centroid = _centroid([tickets[i].vector for i in groups[g]])
                best_g = None
                best_sim = -1.0
                for og, c in centroids.items():
                    if og == g:
                        continue
                    sim = _cosine(g_centroid, c)
                    if sim > best_sim:
                        best_sim = sim
                        best_g = og
                if best_g:
                    groups.setdefault(best_g, []).extend(groups[g])
                    del groups[g]

    # Split large groups using deterministic kmeans
    final_groups: Dict[str, List[int]] = {}
    for g in sorted(groups.keys()):
        idxs = groups[g]
        idxs_sorted = sorted(idxs, key=lambda i: str(tickets[i].path))
        total_chars = sum(len(_read_text(tickets[i].path)) for i in idxs_sorted)
        k = max(
            1,
            math.ceil(len(idxs_sorted) / max(1, args.max_group_size)),
            math.ceil(total_chars / max(1, args.max_bundle_chars)),
        )
        if k <= 1:
            final_groups[g] = idxs_sorted
            continue
        clusters = _kmeans_split([tickets[i].vector for i in idxs_sorted], k)
        part = 1
        for cluster in clusters:
            if not cluster:
                continue
            slug = f"{g}-part-{part:02d}"
            final_groups[slug] = [idxs_sorted[i] for i in cluster]
            part += 1

    # Build manifest
    out_dir = Path(args.out_dir)
    out_dir.mkdir(parents=True, exist_ok=True)
    manifest_groups = []
    for g in sorted(final_groups.keys()):
        idxs = final_groups[g]
        vecs = [tickets[i].vector for i in idxs]
        centroid = _centroid(vecs)
        top_terms = []
        for i, score in sorted(enumerate(centroid), key=lambda x: -x[1])[:8]:
            if score <= 0:
                continue
            top_terms.append(vocab[i])
        sims = []
        for i in idxs:
            sims.append(_cosine(tickets[i].vector, centroid))
        conf = sum(sims) / float(len(sims)) if sims else 0.0

        manifest_groups.append(
            {
                "slug": g,
                "tickets": [str(tickets[i].path) for i in idxs],
                "keywords": top_terms,
                "confidence": conf,
            }
        )

    manifest = {"groups": manifest_groups}
    (out_dir / "manifest.json").write_text(json.dumps(manifest, indent=2, sort_keys=True), encoding="utf-8")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
```

skills/oraclepack-tickets-pack-grouped/scripts/validate_pack.py
```
from pathlib import Path
import runpy

COMMON = Path(__file__).resolve().parents[2] / "oraclepack-tickets-pack-common" / "scripts" / "validate_pack.py"
if not COMMON.exists():
    raise SystemExit(f"[ERROR] Shared validator not found: {COMMON}")

runpy.run_path(str(COMMON), run_name="__main__")
```

skills/oraclepack-tickets-pack-grouped/scripts/validate_shards.py
```
#!/usr/bin/env python3
from __future__ import annotations

import argparse
import json
import subprocess
from pathlib import Path
from typing import Dict


def _read_text(path: Path) -> str:
    try:
        return path.read_text(encoding="utf-8", errors="replace")
    except FileNotFoundError:
        return ""


def main() -> int:
    if len(sys.argv) == 1:
        print("Select how to run:")
        print("1) Use defaults (no args)")
        print("2) Provide custom args (show usage)")
        choice = input("Enter choice [1-2]: ").strip() or "1"
        if choice == "2":
            print("Usage: validate_shards.py --manifest manifest.json")
            return 0

    p = argparse.ArgumentParser(description="Validate sharded packs manifest.")
    p.add_argument("--manifest", default="manifest.json")
    p.add_argument("--max-bundle-chars", type=int, default=200000)
    p.add_argument(
        "--validator",
        default="/home/user/.codex/skills/oraclepack-tickets-pack-common/scripts/validate_pack.py",
    )
    args = p.parse_args()

    manifest_path = Path(args.manifest)
    if not manifest_path.exists():
        raise SystemExit(f"[ERROR] manifest not found: {manifest_path}")

    manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
    counts: Dict[str, int] = {}

    for group in manifest.get("groups", []):
        for t in group.get("tickets", []):
            counts[t] = counts.get(t, 0) + 1

    bad = [t for t, c in counts.items() if c != 1]
    if bad:
        raise SystemExit(f"[ERROR] Tickets assigned !=1 times: {bad}")

    for group in manifest.get("groups", []):
        pack_path = Path(group.get("pack_path", ""))
        if not pack_path.exists():
            raise SystemExit(f"[ERROR] pack missing: {pack_path}")

        # validate pack
        subprocess.run(
            [
                "python3",
                args.validator,
                "--mode",
                "bundle",
                str(pack_path),
            ],
            check=True,
        )

        # size check
        total = 0
        for t in group.get("tickets", []):
            total += len(_read_text(Path(t)))
        if total > args.max_bundle_chars:
            raise SystemExit(
                f"[ERROR] group '{group.get('slug')}' exceeds max bundle chars: {total} > {args.max_bundle_chars}"
            )

    print("[OK] Sharded packs manifest validated.")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
```

</source_code>

--- skills/oraclepack-tickets-pack-grouped_skill_usage_summary.md ---
1) Summary

----------

* Use this skill to generate multiple **runner-ingestible “Stage-1” oracle packs** from `.tickets/`, split into focused **topic/domain mini-packs** with **direct ticket attachments** (no bundle dependency).

    oraclepack-tickets-pack-grouped…

* Improves reliability by enforcing a **deterministic workflow**, a strict **20-step pack schema**, and mandatory **pack + attachment lint validation**.

    oraclepack-tickets-pack-grouped…

* Reduces noise by optionally detecting **possible duplicate tickets** (Jaccard + overlap) and applying a chosen dedupe mode (`report/prune/merge`).

    oraclepack-tickets-pack-grouped…

* Fits the Agent Skills model: skills package instructions/resources/scripts so agents can load procedural knowledge “on demand” and execute repeatable workflows consistently. [OpenAI Developers+1](https://developers.openai.com/codex/skills/)

1) Optimal usage moments (mapped to lifecycle phases)

-----------------------------------------------------

Intake / Backlog triage
Use when you have many tickets (often across subdirs) and need them organized into coherent, runnable analysis packs per domain/topic. The skill’s deterministic grouping (`subdir+infer`) and optional dedupe are designed for this.

oraclepack-tickets-pack-grouped…

Requirements / Discovery
Use when you need to derive interfaces, invariants, state, caching, etc. from tickets as primary evidence, but want results separated by domain to avoid cross-contamination. The produced packs standardize the question set across 20 steps/categories.

oraclepack-tickets-pack-grouped…

Architecture / Design
Use when teams want per-area “design brief” outputs (contracts, validation boundaries, rollout flags) generated consistently for each domain group, enabling parallel review.

oraclepack-tickets-pack-grouped…

Implementation planning / Task execution setup
Use when you’re preparing an agent workflow that depends on stable, validated artifacts (pack files + manifests) and needs predictable inputs/outputs. This aligns with “skills as packaged workflows + scripts/resources.” [OpenAI Developers+1](https://developers.openai.com/codex/skills/)

QA / Validation gates
Use when you need a guardrail that packs are schema-safe before execution (exact fence expectations, step count, direct-ticket attachment checks).

oraclepack-tickets-pack-grouped…

Release / Ops / Maintenance
Use when tickets evolve continuously and you need regeneration with deterministic behavior (lexicographic ordering, stable tie-breaks, explicit caps) to keep outputs comparable across runs.

oraclepack-tickets-pack-grouped…

1) Example library

------------------

| Phase | Goal | Inputs required (files, constraints, audience, format) | Raw prompt (before) | Optimized prompt (after) | Acceptance criteria |
| --- | --- | --- | --- | --- | --- |
| Intake / triage | Split `.tickets/` into domain packs automatically | Files: `.tickets/**/*.md`; Constraints: deterministic grouping; Format: packs + manifest | “Group my tickets and make oracle packs.” | “Use **$oraclepack-tickets-pack-grouped**. {context} {inputs}: ticket\_root=.tickets ticket\_glob=\*\*/_.md group\_mode=subdir+infer dedupe\_mode=report out\_dir={out\_dir}. {constraints}: group\_max\_files=25 group\_max\_chars=200000. {format}: produce packs/_.md + \_groups.json + manifest.json. {acceptance\_criteria}: deterministic grouping, no bundle attachments, packs validate. {deadline}” | Multiple packs created per group; `_groups.json` + `manifest.json` exist; validation/lint passes. |
| Intake / triage | Detect possible duplicates before grouping | Files: `.tickets/**/*.md`; Constraints: report-only; Audience: maintainers | “Find duplicate tickets.” | “Use **$oraclepack-tickets-pack-grouped**. {context} {inputs}: ticket\_root=.tickets dedupe\_mode=report dedupe\_jaccard=0.55 dedupe\_overlap\_hi=0.80 dedupe\_overlap\_lo=0.70 dedupe\_body\_chars=2000. {constraints}: do not drop tickets; emit duplicate reports. {format}: ensure \_dupes\_possible.json + \_duplicates.json + \_dedupe\_plan.json. {acceptance\_criteria}: clusters + canonical + delta/redundant identified. {deadline}” | Duplicate artifacts emitted; clusters include canonical and classification; no tickets lost from assignments. |
| Requirements / discovery | Generate per-domain “public surface changes” outputs | Files: `.tickets/**`; Audience: product/eng; Format: per-pack outputs | “Answer strategist Q1 from these tickets.” | “Use **$oraclepack-tickets-pack-grouped**. {context} {inputs}: ticket\_root=.tickets group\_mode=subdir+infer out\_dir={out\_dir}. {constraints}: each pack must be 20 steps; direct `-f` ticket attachments only. {format}: one pack per domain producing 01–20 outputs under each group out\_dir. {acceptance\_criteria}: step 01 answers contracts/interfaces with evidence-cited bullets. {deadline}” | Packs exist per domain; each pack contains step 01 prompt; outputs pathing is group-specific. |
| Requirements / discovery | Keep groups small enough for model limits | Files: `.tickets/**`; Constraints: strict caps | “Make sure it fits context windows.” | “Use **$oraclepack-tickets-pack-grouped**. {context} {inputs}: ticket\_root=.tickets group\_max\_files=15 group\_max\_chars=120000. {constraints}: enforce size control; split oversize groups into -part-XX packs. {format}: packs per group/part + manifest updated. {acceptance\_criteria}: no group exceeds caps; parts are deterministically chunked. {deadline}” | Large groups are split; pack names include part suffix; manifest records attached\_paths per part. |
| Architecture / design | Produce validation boundary plan per domain | Files: `.tickets/**`; Audience: platform team | “Propose validation boundaries.” | “Use **$oraclepack-tickets-pack-grouped**. {context} {inputs}: ticket\_root=.tickets out\_dir={out\_dir}. {constraints}: keep non-ticket attachments to minimum; direct tickets only. {format}: ensure step 04 (validation boundaries) exists in each domain pack. {acceptance\_criteria}: step 04 returns boundaries + minimal plan + one concrete experiment. {deadline}” | Each pack includes step 04; step output format matches required 4 sections; content is domain-scoped. |
| Architecture / design | Produce caching/state model and cache invalidation per domain | Files: `.tickets/**`; Audience: infra | “Analyze caching and state.” | “Use **$oraclepack-tickets-pack-grouped**. {context} {inputs}: ticket\_root=.tickets out\_dir={out\_dir}. {constraints}: preserve determinism; use dedupe\_mode=prune to reduce redundancy. {format}: ensure steps 05 and 06 are present and write outputs. {acceptance\_criteria}: defines cache keys + invalidation + correctness risks; evidence-cited. {deadline}” | Step 05/06 exist for every pack; dedupe reduces redundant attachments; outputs are written per group. |
| Implementation planning | Generate packs for only a subset of tickets (explicit list) | Files: explicit paths list; Constraint: ignore glob | “Only use these tickets.” | “Use **$oraclepack-tickets-pack-grouped**. {context} {inputs}: ticket\_paths={ticket\_list\_csv} (comma-separated), ticket\_root=.tickets, ticket\_glob ignored. {constraints}: lexicographic ordering; ticket\_max\_files={n}. {format}: packs generated from explicit list only. {acceptance\_criteria}: no other tickets included; manifest lists only provided paths. {deadline}” | Packs include only the explicit files; order is stable; validation passes. |
| Implementation planning | Append consistent non-ticket evidence files to each oracle call | Files: repo evidence file(s); Constraint: 0–1 extra per step | “Add these extra files everywhere.” | “Use **$oraclepack-tickets-pack-grouped**. {context} {inputs}: ticket\_root=.tickets extra\_files={extra\_files\_flags}. {constraints}: extra\_files appended literally, on its own line comment; keep explicit non-ticket attachments minimal. {format}: regenerate packs and confirm extra\_files included in each step invocation. {acceptance\_criteria}: every step includes extra\_files line; direct tickets still attached. {deadline}” | Every step includes extra files exactly as provided; no bundle references introduced; lint passes. |
| QA / validation | Validate every generated pack before running | Files: generated packs; Constraint: fail fast | “Make sure the packs are valid.” | “Use **$oraclepack-tickets-pack-grouped**. {context} {inputs}: out\_dir={out\_dir}. {constraints}: run validate\_pack.py and lint\_attachments.py on every packs/\*.md; fail on first error. {format}: report pass/fail per pack with file paths. {acceptance\_criteria}: all packs pass schema + direct-attach lint. {deadline}” | Validator and linter run; failures identify exact step and issue; all packs pass or errors are actionable. |
| QA / validation | Enforce “no bundle dependency” in direct-ticket mode | Files: packs/\*.md | “Make sure it doesn’t use a tickets bundle.” | “Use **$oraclepack-tickets-pack-grouped**. {context} {inputs}: out\_dir={out\_dir}. {constraints}: direct-ticket mode only; reject any `_tickets_bundle` references. {format}: lint output listing any violating steps/lines. {acceptance\_criteria}: zero `_tickets_bundle` matches across packs. {deadline}” | Lint reports zero bundle references; any violation is pinpointed to step and offending line. |
| Release / ops | Regenerate packs deterministically on a schedule (same grouping) | Files: `.tickets/**`; Constraint: stable outputs | “Rebuild the packs after ticket changes.” | “Use **$oraclepack-tickets-pack-grouped**. {context} {inputs}: ticket\_root=.tickets out\_dir={out\_dir}. {constraints}: deterministic ordering only; no mtime/size-based heuristics; stable tie-breaks. {format}: regenerate packs + update manifest; keep group slugs stable except when splits change. {acceptance\_criteria}: repeated runs with unchanged tickets produce identical pack content. {deadline}” | With unchanged inputs, outputs are byte-stable (or explainable diffs only); grouping remains consistent. |
| Release / ops | Switch dedupe strategy to reduce attachment load | Files: `.tickets/**`; Constraint: minimize attachments | “Reduce duplicates in packs.” | “Use **$oraclepack-tickets-pack-grouped**. {context} {inputs}: ticket\_root=.tickets dedupe\_mode=merge dedupe\_body\_chars=2000. {constraints}: create \_ticket\_merges/cluster-XXXX.md and attach merged file only. {format}: manifest must show attached\_paths reflect merge outputs. {acceptance\_criteria}: merged clusters created; redundant tickets no longer attached directly. {deadline}” | Merge files exist; packs attach merge outputs; dedupe plan reflects merge mode; validation passes. |

1) Non-goals / anti-patterns (do NOT use this skill)

----------------------------------------------------

1. You want a single monolithic pack across all tickets (use a non-grouped tickets pack skill instead).

    oraclepack-tickets-pack-grouped…

2. You need non-deterministic or ML/service-based clustering (this skill explicitly requires deterministic rules and no external ML services).

    oraclepack-tickets-pack-grouped…

3. You want to rewrite ticket contents or author new tickets as the primary output (this skill’s contract is pack generation + validation).

    oraclepack-tickets-pack-grouped…

4. You can’t tolerate the fixed “Stage-1 / 20-step” schema (this skill enforces a strict 20-step contract and validation).

    oraclepack-tickets-pack-grouped…

5. You need interactive, ad-hoc investigation rather than a repeatable workflow package (skills are meant to standardize repeatable workflows via instructions/scripts/resources). [OpenAI Developers+1](https://developers.openai.com/codex/skills/)

---


## Links discovered
- [OpenAI Developers+1](https://developers.openai.com/codex/skills/)

--- skills/oraclepack-codebase-pack-grouped/SKILL.md ---
---
name: oraclepack-codebase-pack-grouped
description: Generate multiple runner-ingestible oraclepack Stage-1 packs grouped by codebase topic/domain (subdir + deterministic inference) with direct code attachments. Use when the user wants per-topic/per-domain mini-packs for a target repo/project/codebase instead of ticket folders, with strict 20-step schema and validation.
---

# oraclepack-codebase-pack-grouped (Stage 1)

## Goal

Produce **multiple** codebase-driven Stage-1 packs, one per inferred topic/domain, with direct code attachments. Each pack is schema-safe and self-contained.

## Use this skill

Use when the user wants separate packs per topic/domain grouped by a target repo/project/codebase, not a `.tickets/` folder.

## Inputs (parse trailing KEY=value; last-one-wins)

Supported keys (defaults in parentheses):
- `codebase_name` (`Unknown`)
- `out_dir` (`docs/oracle-questions-YYYY-MM-DD`)
- `oracle_cmd` (`oracle`)
- `oracle_flags` (`--files-report`)
- `extra_files` (empty; appended literally)
- `code_root` (`.`)
- `code_glob` (`**/*`)
- `code_paths` (empty; comma-separated explicit files; if present, ignore glob)
- `code_max_files` (`200`)
- `group_mode` (`subdir+infer`)
- `group_min_score` (`0.10`)
- `group_max_files` (`200`)
- `group_max_chars` (`200000`)
- `ignore_dirs` (empty; comma-separated; merged with defaults)
- `include_exts` (empty; uses default extension allowlist)
- `exclude_glob` (empty; comma-separated glob patterns)
- `mode` (`codebase-grouped-direct`)

Notes:
- `YYYY-MM-DD` is computed at pack generation time for default `out_dir`.
- If oracle flag support is uncertain, omit unsupported flags; never invent flags.

## Workflow (deterministic)

1) Read:
- `references/codebase-grouping.md`
- `references/attachment-minimization.md`
- `references/codebase-pack-template.md`

2) Ask user if custom args are needed (numbered picker):

```
1) Use defaults (no args)
2) Provide custom args
```

If `2`, ask for KEY=value args and run with those; otherwise run with defaults.

3) Generate packs (deterministic grouping + per-group pack files):

```bash
python3 /home/user/.codex/skills/oraclepack-codebase-pack-grouped/scripts/generate_grouped_packs.py \
  codebase_name=oraclepack \
  out_dir=docs/oracle-questions-2026-01-08
```

Outputs:
- `{{out_dir}}/packs/*.md` (one pack per group/part)
- `{{out_dir}}/_groups.json` (group -> file list)

4) Size control (mandatory; fail fast):
- Run `oracle --dry-run summary --files-report ...` for the **largest** group pack (or each pack if unsure).
- Enforce caps:
  - browser: ≤ 60,000 tokens total input per step
  - api: ≤ 180,000 tokens total input per step
- If exceeded, reduce via `group_max_files`, `code_max_files`, or `include_exts`.

5) Validate every pack (mandatory):

```bash
python3 /home/user/.codex/skills/oraclepack-codebase-pack-grouped/scripts/validate_pack.py <pack.md>
python3 /home/user/.codex/skills/oraclepack-codebase-pack-grouped/scripts/lint_attachments.py <pack.md>
```

## Failure behavior

- If no files resolve, packs still generate with empty attachments.
- Step 01 prompt must request exact missing file/path pattern(s).

## Output contract

Each pack MUST:
- Have exactly one `bash` fence
- Have exactly 20 steps (01..20)
- Include ROI header tokens
- Include `--write-output` with a group-specific `out_dir`
- Attach code files directly via `${code_args[@]}`
- End with Coverage check outside the bash fence


--- skills/oraclepack-tickets-pack-grouped/SKILL.md ---
---
name: oraclepack-tickets-pack-grouped
description: Generate multiple runner-ingestible oraclepack Stage-1 packs grouped by ticket topic/domain (subdir + deterministic inference) with direct ticket attachments. Use when the user wants per-topic/per-domain mini-packs, grouped via subdirectory discovery and inferred assignment of loose tickets, with strict 20-step schema and validation.
---

# oraclepack-tickets-pack-grouped (Stage 1)

## Goal

Produce **multiple** ticket-driven Stage-1 packs, one per inferred topic/domain, with direct ticket attachments. Each pack is schema-safe and self-contained.

## Use this skill

Use when the user wants separate packs per topic/domain, grouped by `.tickets/` subdirectories plus deterministic inference for loose tickets.

## Inputs (parse trailing KEY=value; last-one-wins)

Supported keys (defaults in parentheses):
- `codebase_name` (`Unknown`)
- `out_dir` (`docs/oracle-questions-YYYY-MM-DD`)
- `oracle_cmd` (`oracle`)
- `oracle_flags` (`--files-report`)
- `extra_files` (empty; appended literally)
- `ticket_root` (`.tickets`)
- `ticket_glob` (`**/*.md`)
- `ticket_paths` (empty; comma-separated explicit files; if present, ignore glob)
- `ticket_max_files` (`25`)
- `group_mode` (`subdir+infer`)
- `group_min_score` (`0.08`)
- `group_max_files` (`25`)
- `group_max_chars` (`200000`)
- `dedupe_mode` (`report`)
- `dedupe_jaccard` (`0.55`)
- `dedupe_overlap_hi` (`0.80`)
- `dedupe_overlap_lo` (`0.70`)
- `dedupe_delta_min` (`0.15`)
- `dedupe_body_chars` (`2000`)
- `mode` (`tickets-grouped-direct`)

Notes:
- `YYYY-MM-DD` is computed at pack generation time for default `out_dir`.
- If oracle flag support is uncertain, omit unsupported flags; never invent flags.

## Workflow (deterministic)

1) Read:
- `references/ticket-grouping.md`
- `references/attachment-minimization.md`
- `references/tickets-pack-template.md`

2) Ask user if custom args are needed (numbered picker):

```
1) Use defaults (no args)
2) Provide custom args
```

If `2`, ask for KEY=value args and run with those; otherwise run with defaults.

3) Generate packs (deterministic grouping + per-group pack files):

```bash
python3 /home/user/.codex/skills/oraclepack-tickets-pack-grouped/scripts/generate_grouped_packs.py \
  codebase_name=oraclepack \
  out_dir=docs/oracle-questions-2026-01-08
```

Outputs:
- `{{out_dir}}/packs/*.md` (one pack per group/part)
- `{{out_dir}}/_groups.json` (group -> ticket list)

4) Size control (mandatory; fail fast):
- Run `oracle --dry-run summary --files-report ...` for the **largest** group pack (or each pack if unsure).
- Enforce caps:
  - browser: ≤ 60,000 tokens total input per step
  - api: ≤ 180,000 tokens total input per step
- If exceeded, reduce via `group_max_files` or use explicit `ticket_paths`.

5) Validate every pack (mandatory):

```bash
python3 /home/user/.codex/skills/oraclepack-tickets-pack-grouped/scripts/validate_pack.py <pack.md>
python3 /home/user/.codex/skills/oraclepack-tickets-pack-grouped/scripts/lint_attachments.py <pack.md>
```

## Sharded packs workflow (topic/domain mini-packs)

Use this when you want a manifest-driven, sharded pack per topic/domain with bundle attachments:

First ask the user which args mode to use:

```
1) Use defaults (no args)
2) Provide custom args
```

If `2`, collect args and use them in the commands below.

```bash
python3 /home/user/.codex/skills/oraclepack-tickets-pack-grouped/scripts/shard_tickets.py \\
  --ticket-root .tickets \\
  --out-dir docs/oracle-questions-sharded

python3 /home/user/.codex/skills/oraclepack-tickets-pack-grouped/scripts/render_group_packs.py \\
  --manifest docs/oracle-questions-sharded/manifest.json \\
  --out-dir docs/oracle-questions-sharded

python3 /home/user/.codex/skills/oraclepack-tickets-pack-grouped/scripts/validate_shards.py \\
  --manifest docs/oracle-questions-sharded/manifest.json
```

## Failure behavior

- If no tickets resolve, packs still generate with empty attachments.
- Step 01 prompt must request exact missing ticket file/path pattern(s).

## Output contract

Each pack MUST:
- Have exactly one `bash` fence
- Have exactly 20 steps (01..20)
- Include ROI header tokens
- Include `--write-output` with a group-specific `out_dir`
- Attach tickets directly via `${ticket_args[@]}`
- End with Coverage check outside the bash fence


--- skills/oraclepack-codebase-pack-grouped/scripts/generate_grouped_packs.py ---
#!/usr/bin/env python3
from __future__ import annotations

import datetime as _dt
import fnmatch
import json
import re
import sys
from pathlib import Path
from typing import Dict, Iterable, List, Tuple

STOPWORDS = {
    "the", "and", "for", "with", "from", "this", "that", "into", "over", "under", "when",
    "then", "than", "else", "only", "must", "should", "could", "would", "will", "shall",
    "repo", "repos", "code", "codebase", "project", "oraclepack", "oracle", "pack", "packs",
}

DEFAULT_IGNORE_DIRS = {
    ".git",
    ".hg",
    ".svn",
    "node_modules",
    "dist",
    "build",
    ".next",
    ".venv",
    "venv",
    "coverage",
    "target",
}

DEFAULT_INCLUDE_EXTS = {
    ".py", ".ts", ".tsx", ".js", ".jsx", ".go", ".rs", ".java", ".kt", ".cpp", ".c",
    ".h", ".hpp", ".cs", ".rb", ".php", ".swift", ".scala", ".sql", ".md", ".yaml",
    ".yml", ".json", ".toml", ".ini", ".sh", ".ps1", ".tf", ".proto",
}


def _parse_kv_args(argv: List[str]) -> Dict[str, str]:
    args: Dict[str, str] = {}
    for raw in argv:
        if "=" not in raw:
            continue
        k, v = raw.split("=", 1)
        args[k.strip()] = v.strip()
    return args


def _today() -> str:
    return _dt.date.today().isoformat()


def _slugify(s: str) -> str:
    s = s.strip().lower()
    s = re.sub(r"[^a-z0-9]+", "-", s)
    s = re.sub(r"-+", "-", s).strip("-")
    return s or "group"


def _tokenize(text: str) -> List[str]:
    text = text.lower()
    text = re.sub(r"[^a-z0-9]+", " ", text)
    toks = [t for t in text.split() if len(t) >= 3 and t not in STOPWORDS]
    return toks


def _group_by_subdir(paths: Iterable[Path], code_root: str) -> Tuple[Dict[str, List[Path]], List[Path]]:
    root = Path(code_root).resolve()
    groups: Dict[str, List[Path]] = {}
    loose: List[Path] = []
    for p in paths:
        try:
            rel = p.resolve().relative_to(root)
        except ValueError:
            loose.append(p)
            continue
        if len(rel.parts) >= 2:
            key = rel.parts[0]
            groups.setdefault(key, []).append(p)
        else:
            loose.append(p)
    return groups, loose


def _group_tokens(group_name: str, paths: Iterable[Path]) -> set:
    tokens = set(_tokenize(group_name))
    for p in paths:
        tokens.update(_tokenize(p.stem))
    return tokens


def _file_tokens(p: Path) -> set:
    toks = set(_tokenize(p.stem))
    toks.update(_tokenize(str(p.parent.name)))
    return toks


def _jaccard(a: set, b: set) -> float:
    if not a or not b:
        return 0.0
    inter = a.intersection(b)
    union = a.union(b)
    return float(len(inter)) / float(len(union))


def _collect_paths(
    code_root: str,
    code_glob: str,
    code_paths: str,
    include_exts: str,
    exclude_glob: str,
    ignore_dirs: str,
) -> List[Path]:
    if code_paths:
        parts = [p.strip() for p in code_paths.split(",") if p.strip()]
        return [Path(p) for p in parts]

    root = Path(code_root)
    if not root.exists():
        return []

    ignore = {p.strip() for p in ignore_dirs.split(",") if p.strip()}
    ignore = ignore.union(DEFAULT_IGNORE_DIRS)

    include = {e.strip().lower() for e in include_exts.split(",") if e.strip()}
    if not include_exts.strip():
        include = set(DEFAULT_INCLUDE_EXTS)

    excludes = [g.strip() for g in exclude_glob.split(",") if g.strip()]

    out: List[Path] = []
    for p in root.glob(code_glob):
        if p.is_dir():
            continue
        parts = set(p.parts)
        if parts.intersection(ignore):
            continue
        if excludes and any(fnmatch.fnmatch(str(p), g) for g in excludes):
            continue
        if include:
            ext = p.suffix.lower()
            if ext not in include:
                continue
        out.append(p)

    return out


def _cap_group(paths: List[Path], max_files: int, max_chars: int) -> List[List[Path]]:
    chunks: List[List[Path]] = []
    current: List[Path] = []
    size = 0

    for p in paths:
        p_size = 0
        try:
            p_size = p.stat().st_size
        except FileNotFoundError:
            p_size = 0

        if max_files and len(current) >= max_files:
            chunks.append(current)
            current = []
            size = 0

        if max_chars and current and size + p_size > max_chars:
            chunks.append(current)
            current = []
            size = 0

        current.append(p)
        size += p_size

    if current:
        chunks.append(current)

    return chunks


def main() -> None:
    args = _parse_kv_args(sys.argv[1:])

    codebase_name = args.get("codebase_name", "Unknown")
    out_dir = args.get("out_dir", f"docs/oracle-questions-{_today()}")
    oracle_cmd = args.get("oracle_cmd", "oracle")
    oracle_flags = args.get("oracle_flags", "--files-report")
    extra_files = args.get("extra_files", "")
    code_root = args.get("code_root", ".")
    code_glob = args.get("code_glob", "**/*")
    code_paths = args.get("code_paths", "")
    code_max_files = int(args.get("code_max_files", "200"))
    group_mode = args.get("group_mode", "subdir+infer")
    group_min_score = float(args.get("group_min_score", "0.10"))
    group_max_files = int(args.get("group_max_files", "200"))
    group_max_chars = int(args.get("group_max_chars", "200000"))
    ignore_dirs = args.get("ignore_dirs", "")
    include_exts = args.get("include_exts", "")
    exclude_glob = args.get("exclude_glob", "")
    mode = args.get("mode", "codebase-grouped-direct")

    template_path = Path(__file__).resolve().parents[1] / "references" / "codebase-pack-template.md"
    if not template_path.exists():
        raise SystemExit(f"[ERROR] Template not found: {template_path}")

    paths = _collect_paths(code_root, code_glob, code_paths, include_exts, exclude_glob, ignore_dirs)
    paths = sorted(paths, key=lambda p: str(p))
    if code_max_files and code_max_files > 0:
        paths = paths[:code_max_files]

    groups: Dict[str, List[Path]] = {}
    loose: List[Path] = []

    if "subdir" in group_mode:
        groups, loose = _group_by_subdir(paths, code_root)
    else:
        loose = list(paths)

    if "infer" in group_mode and loose:
        group_tokens = {k: _group_tokens(k, v) for k, v in groups.items()}
        for p in loose:
            best = None
            best_score = 0.0
            pt = _file_tokens(p)
            for g, gt in group_tokens.items():
                score = _jaccard(pt, gt)
                if score > best_score:
                    best_score = score
                    best = g
            if best and best_score >= group_min_score:
                groups.setdefault(best, []).append(p)
            else:
                groups.setdefault("root", []).append(p)
    else:
        if loose:
            groups.setdefault("root", []).extend(loose)

    if not groups:
        groups = {"root": []}

    out_dir_path = Path(out_dir)
    packs_dir = out_dir_path / "packs"
    packs_dir.mkdir(parents=True, exist_ok=True)

    rendered_groups: Dict[str, List[str]] = {}

    template = template_path.read_text(encoding="utf-8")
    for group_name in sorted(groups.keys()):
        files = sorted(groups[group_name], key=lambda p: str(p))
        chunks = _cap_group(files, group_max_files, group_max_chars)
        for idx, chunk in enumerate(chunks, start=1):
            part_suffix = f" part {idx}" if len(chunks) > 1 else ""
            full_group_name = f"{group_name}{part_suffix}"
            group_slug = _slugify(full_group_name)
            pack_path = packs_dir / f"{group_slug}.md"

            rendered = template
            rendered = rendered.replace("{{codebase_name}}", codebase_name)
            rendered = rendered.replace("{{out_dir}}", str(out_dir))
            rendered = rendered.replace("{{oracle_cmd}}", oracle_cmd)
            rendered = rendered.replace("{{oracle_flags}}", oracle_flags)
            rendered = rendered.replace("{{extra_files}}", extra_files)
            rendered = rendered.replace("{{code_root}}", code_root)
            rendered = rendered.replace("{{code_glob}}", code_glob)
            rendered = rendered.replace("{{code_paths}}", code_paths)
            rendered = rendered.replace("{{code_max_files}}", str(code_max_files))
            rendered = rendered.replace("{{group_name}}", full_group_name)
            rendered = rendered.replace("{{group_slug}}", group_slug)
            rendered = rendered.replace("{{group_mode}}", group_mode)
            rendered = rendered.replace("{{group_min_score}}", str(group_min_score))
            rendered = rendered.replace("{{group_max_files}}", str(group_max_files))
            rendered = rendered.replace("{{group_max_chars}}", str(group_max_chars))
            rendered = rendered.replace("{{ignore_dirs}}", ignore_dirs)
            rendered = rendered.replace("{{include_exts}}", include_exts)
            rendered = rendered.replace("{{exclude_glob}}", exclude_glob)
            rendered = rendered.replace("{{mode}}", mode)
            rendered = rendered.replace(
                "{{group_files_json}}",
                json.dumps([str(p) for p in chunk], indent=2),
            )

            pack_path.write_text(rendered, encoding="utf-8")
            rendered_groups.setdefault(full_group_name, []).append(str(pack_path))

    groups_json = {
        "code_root": code_root,
        "groups": {k: [str(p) for p in v] for k, v in groups.items()},
        "packs": rendered_groups,
    }
    (out_dir_path / "_groups.json").write_text(json.dumps(groups_json, indent=2), encoding="utf-8")


if __name__ == "__main__":
    main()


--- skills/oraclepack-tickets-pack-grouped/scripts/generate_grouped_packs.py ---
#!/usr/bin/env python3
from __future__ import annotations

import datetime as _dt
import math
import json
import re
import sys
from pathlib import Path
from typing import Dict, Iterable, List, Tuple

STOPWORDS = {
    "the", "and", "for", "with", "from", "this", "that", "into", "over", "under", "when",
    "then", "than", "else", "only", "must", "should", "could", "would", "will", "shall",
    "ticket", "tickets", "oraclepack", "oracle", "pack", "packs",
}


def _parse_kv_args(argv: List[str]) -> Dict[str, str]:
    args: Dict[str, str] = {}
    for raw in argv:
        if "=" not in raw:
            continue
        k, v = raw.split("=", 1)
        args[k.strip()] = v.strip()
    return args


def _today() -> str:
    return _dt.date.today().isoformat()


def _slugify(s: str) -> str:
    s = s.strip().lower()
    s = re.sub(r"[^a-z0-9]+", "-", s)
    s = re.sub(r"-+", "-", s).strip("-")
    return s or "group"


def _tokenize(text: str) -> List[str]:
    text = text.lower()
    text = re.sub(r"[^a-z0-9]+", " ", text)
    toks = [t for t in text.split() if len(t) >= 3 and t not in STOPWORDS]
    return toks


def _normalize_title(text: str) -> str:
    text = text.strip().lower()
    text = re.sub(r"[^a-z0-9]+", " ", text)
    text = re.sub(r"\s+", " ", text).strip()
    return text


def _read_heading(path: Path) -> str:
    try:
        for line in path.read_text(encoding="utf-8", errors="replace").splitlines():
            if line.startswith("#"):
                return line.lstrip("#").strip()
    except FileNotFoundError:
        return ""
    return ""


def _collect_ticket_paths(ticket_root: str, ticket_glob: str, ticket_paths: str) -> List[Path]:
    if ticket_paths:
        parts = [p.strip() for p in ticket_paths.split(",") if p.strip()]
        return [Path(p) for p in parts]
    root = Path(ticket_root)
    if not root.exists():
        return []
    return [Path(p) for p in root.glob(ticket_glob)]


def _read_signature(path: Path, max_lines: int = 40) -> Tuple[str, str]:
    heading = ""
    lines: List[str] = []
    try:
        for line in path.read_text(encoding="utf-8", errors="replace").splitlines():
            if not heading and line.startswith("#"):
                heading = line.lstrip("#").strip()
            if line.strip():
                lines.append(line.strip())
            if len(lines) >= max_lines:
                break
    except FileNotFoundError:
        pass
    return heading, " ".join(lines)


def _read_text(path: Path) -> str:
    try:
        return path.read_text(encoding="utf-8", errors="replace")
    except FileNotFoundError:
        return ""


def _group_by_subdir(paths: Iterable[Path], ticket_root: str) -> Tuple[Dict[str, List[Path]], List[Path]]:
    root = Path(ticket_root)
    groups: Dict[str, List[Path]] = {}
    loose: List[Path] = []
    for p in paths:
        try:
            rel = p.relative_to(root)
        except ValueError:
            loose.append(p)
            continue
        if len(rel.parts) >= 2:
            key = rel.parts[0]
            groups.setdefault(key, []).append(p)
        else:
            loose.append(p)
    return groups, loose


def _group_tokens(group_name: str, paths: Iterable[Path]) -> set:
    tokens = set(_tokenize(group_name))
    for p in paths:
        tokens.update(_tokenize(p.stem))
        tokens.update(_tokenize(_read_heading(p)))
    return tokens


def _ticket_tokens(p: Path) -> set:
    toks = set(_tokenize(p.stem))
    heading, snippet = _read_signature(p)
    toks.update(_tokenize(heading))
    toks.update(_tokenize(snippet))
    return toks


def _signature_tokens(p: Path, body_chars: int) -> set:
    heading = _read_heading(p)
    body = _read_text(p)
    body = body[:body_chars]
    toks = set(_tokenize(p.stem))
    toks.update(_tokenize(heading))
    toks.update(_tokenize(body))
    return toks


def _jaccard(a: set, b: set) -> float:
    if not a or not b:
        return 0.0
    inter = a.intersection(b)
    union = a.union(b)
    return float(len(inter)) / float(len(union))


def _overlap(a: set, b: set) -> float:
    if not a or not b:
        return 0.0
    inter = a.intersection(b)
    denom = min(len(a), len(b))
    if denom == 0:
        return 0.0
    return float(len(inter)) / float(denom)


def _clusters_from_edges(nodes: List[str], edges: Dict[str, List[str]]) -> List[List[str]]:
    seen = set()
    clusters: List[List[str]] = []
    for n in nodes:
        if n in seen:
            continue
        stack = [n]
        comp = []
        seen.add(n)
        while stack:
            cur = stack.pop()
            comp.append(cur)
            for nxt in edges.get(cur, []):
                if nxt not in seen:
                    seen.add(nxt)
                    stack.append(nxt)
        clusters.append(sorted(comp))
    return clusters


def _dedupe_clusters(
    paths: List[Path],
    body_chars: int,
    jaccard_hi: float,
    overlap_hi: float,
    overlap_lo: float,
    delta_min: float,
) -> Tuple[List[List[str]], Dict[str, str], Dict[str, Dict[str, object]], Dict[Tuple[str, str], Dict[str, float]]]:
    tokens: Dict[str, set] = {}
    sizes: Dict[str, int] = {}
    titles: Dict[str, str] = {}
    for p in paths:
        key = str(p)
        tokens[key] = _signature_tokens(p, body_chars)
        sizes[key] = len(_read_text(p))
        titles[key] = _normalize_title(_read_heading(p))

    nodes = sorted(tokens.keys())
    edges: Dict[str, List[str]] = {n: [] for n in nodes}
    pair_scores: Dict[Tuple[str, str], Dict[str, float]] = {}

    for i, a in enumerate(nodes):
        for b in nodes[i + 1 :]:
            jac = _jaccard(tokens[a], tokens[b])
            ov = _overlap(tokens[a], tokens[b])
            pair_scores[(a, b)] = {"jaccard": jac, "overlap": ov}
            if ov >= overlap_hi or (jac >= jaccard_hi and ov >= overlap_lo):
                edges[a].append(b)
                edges[b].append(a)

    clusters = _clusters_from_edges(nodes, edges)
    cluster_meta: Dict[str, Dict[str, object]] = {}
    dup_map: Dict[str, str] = {}

    for idx, members in enumerate(clusters, start=1):
        if len(members) == 1:
            continue
        # canonical: largest content length, then lexicographic
        canon = sorted(
            members,
            key=lambda m: (-sizes.get(m, 0), m),
        )[0]
        deltas: List[str] = []
        redundant: List[str] = []
        for m in members:
            if m == canon:
                continue
            unique = tokens[m] - tokens[canon]
            unique_ratio = float(len(unique)) / float(max(1, len(tokens[m])))
            heading_diff = titles.get(m, "") != titles.get(canon, "")
            if unique_ratio >= delta_min or heading_diff:
                deltas.append(m)
            else:
                redundant.append(m)
            dup_map[m] = canon

        cluster_meta[str(idx)] = {
            "canonical": canon,
            "members": members,
            "deltas": sorted(deltas),
            "redundant": sorted(redundant),
        }

    return clusters, dup_map, cluster_meta, pair_scores


def _infer_groups(
    groups: Dict[str, List[Path]],
    loose: List[Path],
    min_score: float,
) -> Dict[str, List[Path]]:
    if not groups:
        return {"root": list(loose)}

    group_tokens = {k: _group_tokens(k, v) for k, v in groups.items()}
    for p in loose:
        tokens = _ticket_tokens(p)
        best = None
        best_score = -1.0
        for name in sorted(group_tokens.keys()):
            score = _jaccard(tokens, group_tokens[name])
            if score > best_score:
                best_score = score
                best = name
        if best is not None and best_score >= min_score:
            groups.setdefault(best, []).append(p)
        else:
            groups.setdefault("misc", []).append(p)
    return groups


def _chunk(paths: List[Path], size: int) -> List[List[Path]]:
    if size <= 0:
        return [paths]
    return [paths[i : i + size] for i in range(0, len(paths), size)]


def _chunk_by_limits(
    paths: List[Path],
    max_files: int,
    max_chars: int,
) -> List[List[Path]]:
    if max_files <= 0 and max_chars <= 0:
        return [paths]
    chunks: List[List[Path]] = []
    cur: List[Path] = []
    cur_chars = 0
    for p in paths:
        size = len(_read_text(p))
        if cur:
            if (max_files > 0 and len(cur) >= max_files) or (
                max_chars > 0 and cur_chars + size > max_chars
            ):
                chunks.append(cur)
                cur = []
                cur_chars = 0
        cur.append(p)
        cur_chars += size
    if cur:
        chunks.append(cur)
    return chunks


def _render_template(template: str, mapping: Dict[str, str]) -> str:
    out = template
    for key, val in mapping.items():
        out = out.replace("{{" + key + "}}", val)
    unresolved = sorted(set(re.findall(r"\{\{([^}]+)\}\}", out)))
    if unresolved:
        raise ValueError(f"Unresolved template placeholders: {unresolved}")
    return out


def _write_merge_file(
    out_dir: Path,
    cluster_id: str,
    canonical: str,
    deltas: List[str],
    redundant: List[str],
    body_chars: int,
) -> Path:
    merge_dir = out_dir / "_ticket_merges"
    merge_dir.mkdir(parents=True, exist_ok=True)
    path = merge_dir / f"cluster-{int(cluster_id):04d}.md"

    def _cap(text: str) -> str:
        if len(text) <= body_chars:
            return text
        return text[:body_chars] + "\n[... truncated ...]\n"

    lines: List[str] = []
    lines.append(f"# Ticket Merge Cluster {cluster_id}")
    lines.append("")
    lines.append("## Canonical")
    lines.append(f"- path: {canonical}")
    lines.append("")
    lines.append(_cap(_read_text(Path(canonical))))
    lines.append("")

    members = deltas + redundant
    if members:
        lines.append("## Also reported in")
        for m in members:
            lines.append(f"- {m}")
        lines.append("")

    if deltas:
        lines.append("## Unique details from related tickets")
        for m in deltas:
            text = _read_text(Path(m))
            toks = _signature_tokens(Path(m), body_chars)
            canon_toks = _signature_tokens(Path(canonical), body_chars)
            unique = toks - canon_toks
            sel: List[str] = []
            for ln in text.splitlines():
                lnt = _tokenize(ln)
                if any(t in unique for t in lnt):
                    sel.append(ln)
                if len(sel) >= 60:
                    break
            lines.append(f"### {m}")
            if sel:
                lines.extend(sel)
            else:
                lines.append("(no unique lines detected within cap)")
            lines.append("")

    path.write_text("\n".join(lines), encoding="utf-8")
    return path


def main() -> int:
    if len(sys.argv) == 1:
        print("Select how to run:")
        print("1) Use defaults (no args)")
        print("2) Provide custom args (show usage)")
        choice = input("Enter choice [1-2]: ").strip() or "1"
        if choice == "2":
            print("Usage: generate_grouped_packs.py key=value [key=value ...]")
            return 0

    args = _parse_kv_args(sys.argv[1:])
    codebase_name = args.get("codebase_name", "Unknown")
    out_dir = args.get("out_dir", f"docs/oracle-questions-{_today()}")
    oracle_cmd = args.get("oracle_cmd", "oracle")
    oracle_flags = args.get("oracle_flags", "--files-report")
    extra_files = args.get("extra_files", "")
    ticket_root = args.get("ticket_root", ".tickets")
    ticket_glob = args.get("ticket_glob", "**/*.md")
    ticket_paths = args.get("ticket_paths", "")
    ticket_max_files = args.get("ticket_max_files", "25")
    group_mode = args.get("group_mode", "subdir+infer")
    group_min_score = float(args.get("group_min_score", "0.08"))
    group_max_files = int(args.get("group_max_files", "25"))
    group_max_chars = int(args.get("group_max_chars", "200000"))
    dedupe_mode = args.get("dedupe_mode", "report")
    dedupe_jaccard = float(args.get("dedupe_jaccard", "0.55"))
    dedupe_overlap_hi = float(args.get("dedupe_overlap_hi", "0.80"))
    dedupe_overlap_lo = float(args.get("dedupe_overlap_lo", "0.70"))
    dedupe_delta_min = float(args.get("dedupe_delta_min", "0.15"))
    dedupe_body_chars = int(args.get("dedupe_body_chars", "2000"))
    mode = args.get("mode", "tickets-grouped-direct")

    template_path = Path(__file__).resolve().parent.parent / "references" / "tickets-pack-template.md"
    template = template_path.read_text(encoding="utf-8")

    paths = _collect_ticket_paths(ticket_root, ticket_glob, ticket_paths)
    paths = sorted((str(p) for p in paths))
    paths = [Path(p) for p in paths]

    original_paths = list(paths)
    dup_map: Dict[str, str] = {}
    cluster_meta: Dict[str, Dict[str, object]] = {}
    dup_pairs: Dict[Tuple[str, str], Dict[str, float]] = {}
    if dedupe_mode != "off":
        _clusters, dup_map, cluster_meta, dup_pairs = _dedupe_clusters(
            paths,
            body_chars=dedupe_body_chars,
            jaccard_hi=dedupe_jaccard,
            overlap_hi=dedupe_overlap_hi,
            overlap_lo=dedupe_overlap_lo,
            delta_min=dedupe_delta_min,
        )

    # Build grouping base: canonical tickets + singletons
    canonical_set = {meta["canonical"] for meta in cluster_meta.values()}
    dup_set = set(dup_map.keys())
    base_paths: List[Path] = []
    for p in paths:
        sp = str(p)
        if sp in dup_set:
            continue
        base_paths.append(p)

    groups: Dict[str, List[Path]] = {}
    loose: List[Path] = []
    if "subdir" in group_mode:
        groups, loose = _group_by_subdir(base_paths, ticket_root)
    else:
        loose = list(base_paths)

    if "infer" in group_mode:
        groups = _infer_groups(groups, loose, group_min_score)
    else:
        groups.setdefault("misc", []).extend(loose)

    dedupe_plan: Dict[str, Dict[str, object]] = {}
    merge_files: Dict[str, str] = {}
    if cluster_meta:
        primary_to_group: Dict[str, str] = {}
        for gname in groups:
            for p in groups[gname]:
                primary_to_group[str(p)] = gname

        for cluster_id, meta in sorted(cluster_meta.items(), key=lambda x: int(x[0])):
            canonical = meta["canonical"]
            deltas = list(meta["deltas"])
            redundant = list(meta["redundant"])
            gname = primary_to_group.get(canonical, "misc")

            if dedupe_mode == "merge":
                merge_path = _write_merge_file(
                    Path(out_dir),
                    cluster_id=cluster_id,
                    canonical=canonical,
                    deltas=deltas,
                    redundant=redundant,
                    body_chars=dedupe_body_chars,
                )
                merge_files[canonical] = str(merge_path)
                # Replace canonical in group with merge file
                groups[gname] = [p for p in groups[gname] if str(p) != canonical]
                groups[gname].append(merge_path)
            else:
                # report/prune: append related tickets to canonical group
                keep = deltas if dedupe_mode == "prune" else deltas + redundant
                for p in keep:
                    groups.setdefault(gname, []).append(Path(p))

            dedupe_plan[cluster_id] = {
                "canonical": canonical,
                "group": gname,
                "deltas": sorted(deltas),
                "redundant": sorted(redundant),
                "mode": dedupe_mode,
            }

    # Ensure stable order
    for k in sorted(groups.keys()):
        groups[k] = sorted((str(p) for p in groups[k]))
        groups[k] = [Path(p) for p in groups[k]]

    original_set = {str(p) for p in original_paths}
    assignment: Dict[str, str] = {}
    for gname, gpaths in groups.items():
        for p in gpaths:
            sp = str(p)
            if sp in original_set:
                if sp in assignment:
                    raise SystemExit(f"[ERROR] Ticket assigned to multiple groups: {sp}")
                assignment[sp] = gname

    for meta in dedupe_plan.values():
        gname = meta["group"]
        for sp in [meta["canonical"]] + meta["deltas"] + meta["redundant"]:
            if sp not in assignment:
                assignment[sp] = gname

    missing = sorted(original_set - set(assignment.keys()))
    if missing:
        raise SystemExit(f"[ERROR] Tickets missing group assignment: {missing}")

    base_out = Path(out_dir)
    packs_dir = base_out / "packs"
    packs_dir.mkdir(parents=True, exist_ok=True)

    grouping_report: Dict[str, List[str]] = {}
    manifest_groups: List[Dict[str, object]] = []
    group_originals: Dict[str, List[str]] = {g: [] for g in groups.keys()}
    for ticket, gname in assignment.items():
        group_originals.setdefault(gname, []).append(ticket)
    for group_name in sorted(groups.keys()):
        group_paths = groups[group_name]
        grouping_report[group_name] = [str(p) for p in group_paths]

        parts = _chunk_by_limits(group_paths, group_max_files, group_max_chars)
        for idx, part in enumerate(parts, start=1):
            part_suffix = f"-part-{idx:02d}" if len(parts) > 1 else ""
            group_slug = _slugify(group_name + part_suffix)

            pack_out_dir = str(base_out / group_slug)
            pack_file = packs_dir / f"{group_slug}.md"

            mapping = {
                "codebase_name": codebase_name,
                "out_dir": pack_out_dir,
                "oracle_cmd": oracle_cmd,
                "oracle_flags": oracle_flags,
                "extra_files": extra_files,
                "ticket_root": ticket_root,
                "ticket_glob": ticket_glob,
                "ticket_paths": ",".join(str(p) for p in part),
                "ticket_max_files": str(min(len(part), max(1, group_max_files))),
                "group_name": group_name,
                "group_slug": group_slug,
                "mode": mode,
            }

            content = _render_template(template, mapping)
            pack_file.write_text(content, encoding="utf-8")

            manifest_groups.append(
                {
                    "group": group_name,
                    "slug": group_slug,
                    "part": idx,
                    "pack_path": str(pack_file),
                    "out_dir": pack_out_dir,
                    "attached_paths": [str(p) for p in part],
                    "original_tickets": sorted(group_originals.get(group_name, [])),
                }
            )

    (base_out / "_groups.json").write_text(
        json.dumps(grouping_report, indent=2, sort_keys=True),
        encoding="utf-8",
    )

    if dup_map:
        (base_out / "_duplicates.json").write_text(
            json.dumps(dup_map, indent=2, sort_keys=True),
            encoding="utf-8",
        )

    if dedupe_plan:
        (base_out / "_dedupe_plan.json").write_text(
            json.dumps(dedupe_plan, indent=2, sort_keys=True),
            encoding="utf-8",
        )

    if cluster_meta:
        pairs_out = [
            {"a": a, "b": b, **scores} for (a, b), scores in sorted(dup_pairs.items())
        ]
        (base_out / "_dupes_possible.json").write_text(
            json.dumps({"clusters": cluster_meta, "pairs": pairs_out}, indent=2, sort_keys=True),
            encoding="utf-8",
        )

    (base_out / "manifest.json").write_text(
        json.dumps({"groups": manifest_groups}, indent=2, sort_keys=True),
        encoding="utf-8",
    )

    print(f"[OK] wrote packs to: {packs_dir}")
    print(f"[OK] wrote grouping map: {base_out / '_groups.json'}")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())


--- skills/oraclepack-codebase-pack-grouped/scripts/lint_attachments.py ---
import argparse
import re
import sys
from dataclasses import dataclass
from pathlib import Path
from typing import List, Tuple


@dataclass
class Step:
    n: str
    lines: List[str]


def _read_text(path: Path) -> str:
    try:
        return path.read_text(encoding="utf-8")
    except UnicodeDecodeError:
        return path.read_text(encoding="utf-8", errors="replace")


def _extract_bash_fence(lines: List[str]) -> List[str]:
    fence_idxs = [i for i, ln in enumerate(lines) if ln.startswith("```")]
    if len(fence_idxs) != 2:
        raise ValueError(f"Expected exactly one fenced block (2 fence lines). Found {len(fence_idxs)}.")
    open_i, close_i = fence_idxs
    if lines[open_i].rstrip("\n") != "```bash":
        raise ValueError("Opening fence must be exactly ```bash.")
    if lines[close_i].rstrip("\n") != "```":
        raise ValueError("Closing fence must be exactly ```.")
    return [ln.rstrip("\n") for ln in lines[open_i + 1 : close_i]]


def _parse_steps(fence_lines: List[str]) -> List[Step]:
    header_re = re.compile(r"^#\s*(\d{2})\)\s+")
    header_idxs: List[Tuple[int, str]] = []
    for i, ln in enumerate(fence_lines):
        m = header_re.match(ln)
        if m:
            header_idxs.append((i, m.group(1)))

    if not header_idxs:
        raise ValueError("No step headers found inside bash fence.")

    steps: List[Step] = []
    for idx, (start_i, n) in enumerate(header_idxs):
        end_i = header_idxs[idx + 1][0] if idx + 1 < len(header_idxs) else len(fence_lines)
        steps.append(Step(n=n, lines=fence_lines[start_i:end_i]))
    return steps


def lint(path: Path) -> None:
    raw = _read_text(path)
    lines = raw.splitlines(True)
    fence = _extract_bash_fence(lines)
    steps = _parse_steps(fence)

    errors: List[str] = []
    for step in steps:
        joined = "\n".join(step.lines)

        if "_tickets_bundle" in joined:
            errors.append(f"Step {step.n}: found '_tickets_bundle' reference (codebase packs must attach code files directly).")

        if re.search(r"mapfile\s+-t\s+__code_files\s+<\s+<\(", joined) is None:
            errors.append(f"Step {step.n}: missing mapfile code discovery stanza.")

        if re.search(r"code_args=\(\)", joined) is None or re.search(r"code_args\+\=\(\s*(-f|--file)\b", joined) is None:
            errors.append(f"Step {step.n}: missing code_args builder (code_args+=(-f \"$p\")).")

        if re.search(r"\$\{code_args\[@\]\}", joined) is None:
            errors.append(f"Step {step.n}: missing ${'{'}code_args[@]{'}'} usage in oracle invocation.")

    if errors:
        for e in errors:
            print(f"[ERROR] {e}", file=sys.stderr)
        sys.exit(1)

    print("[OK] Direct-code lint passed.")


def main() -> None:
    p = argparse.ArgumentParser(description="Lint codebase-driven Stage-1 packs (direct-code mode).")
    p.add_argument("pack_path", help="Path to the Markdown pack file")
    args = p.parse_args()

    path = Path(args.pack_path)
    if not path.exists():
        print(f"[ERROR] File not found: {path}", file=sys.stderr)
        sys.exit(1)

    lint(path)


if __name__ == "__main__":
    main()


--- skills/oraclepack-tickets-pack-grouped/scripts/lint_attachments.py ---
import argparse
import re
import sys
from dataclasses import dataclass
from pathlib import Path
from typing import List, Tuple


@dataclass
class Step:
    n: str
    lines: List[str]


def _read_text(path: Path) -> str:
    try:
        return path.read_text(encoding="utf-8")
    except UnicodeDecodeError:
        return path.read_text(encoding="utf-8", errors="replace")


def _extract_bash_fence(lines: List[str]) -> List[str]:
    fence_idxs = [i for i, ln in enumerate(lines) if ln.startswith("```")]
    if len(fence_idxs) != 2:
        raise ValueError(f"Expected exactly one fenced block (2 fence lines). Found {len(fence_idxs)}.")
    open_i, close_i = fence_idxs
    if lines[open_i].rstrip("\n") != "```bash":
        raise ValueError("Opening fence must be exactly ```bash.")
    if lines[close_i].rstrip("\n") != "```":
        raise ValueError("Closing fence must be exactly ```.")
    return [ln.rstrip("\n") for ln in lines[open_i + 1 : close_i]]


def _parse_steps(fence_lines: List[str]) -> List[Step]:
    header_re = re.compile(r"^#\s*(\d{2})\)\s+")
    header_idxs: List[Tuple[int, str]] = []
    for i, ln in enumerate(fence_lines):
        m = header_re.match(ln)
        if m:
            header_idxs.append((i, m.group(1)))

    if not header_idxs:
        raise ValueError("No step headers found inside bash fence.")

    steps: List[Step] = []
    for idx, (start_i, n) in enumerate(header_idxs):
        end_i = header_idxs[idx + 1][0] if idx + 1 < len(header_idxs) else len(fence_lines)
        steps.append(Step(n=n, lines=fence_lines[start_i:end_i]))
    return steps


def lint(path: Path) -> None:
    raw = _read_text(path)
    lines = raw.splitlines(True)
    fence = _extract_bash_fence(lines)
    steps = _parse_steps(fence)

    errors: List[str] = []
    for step in steps:
        joined = "\n".join(step.lines)

        if "_tickets_bundle" in joined:
            errors.append(f"Step {step.n}: found '_tickets_bundle' reference (direct-ticket packs must not use bundle).")

        if re.search(r"mapfile\s+-t\s+__tickets\s+<\s+<\(", joined) is None:
            errors.append(f"Step {step.n}: missing mapfile ticket discovery stanza.")

        if re.search(r"ticket_args=\(\)", joined) is None or re.search(r"ticket_args\+\=\(\s*(-f|--file)\b", joined) is None:
            errors.append(f"Step {step.n}: missing ticket_args builder (ticket_args+=(-f \"$p\")).")

        if re.search(r"\$\{ticket_args\[@\]\}", joined) is None:
            errors.append(f"Step {step.n}: missing ${'{'}ticket_args[@]{'}'} usage in oracle invocation.")

        # Heuristic: ensure we did not hardcode a non-existent bundle path.
        if re.search(r'(?<!\S)(-f|--file)(?!\S)\s+"[^"\n]*_tickets_bundle', joined):
            errors.append(f"Step {step.n}: contains a hardcoded _tickets_bundle attachment.")

    if errors:
        for e in errors:
            print(f"[ERROR] {e}", file=sys.stderr)
        sys.exit(1)

    print("[OK] Direct-ticket lint passed.")


def main() -> None:
    p = argparse.ArgumentParser(description="Lint ticket-driven Stage-1 packs (direct-ticket mode).")
    p.add_argument("pack_path", help="Path to the Markdown pack file")
    args = p.parse_args()

    path = Path(args.pack_path)
    if not path.exists():
        print(f"[ERROR] File not found: {path}", file=sys.stderr)
        sys.exit(1)

    lint(path)


if __name__ == "__main__":
    main()
