diff --git a/docs/cli/lol.md b/docs/cli/lol.md index c7228f77..f2e4a1bb 100644 --- a/docs/cli/lol.md +++ b/docs/cli/lol.md @@ -139,7 +139,7 @@ lol plan [--dry-run] [--verbose] [--editor] [--refine [refinement-ins lol plan --refine [refinement-instructions] ``` -Runs the full multi-agent debate pipeline for a feature description, producing a consensus implementation plan. This is the preferred entrypoint for the planner pipeline. +Runs the full multi-agent debate pipeline for a feature description, producing a consensus implementation plan. This is the preferred entrypoint for the planner pipeline. The consensus plan ends with a provenance footer: `Plan based on commit `. #### Options diff --git a/docs/cli/planner.md b/docs/cli/planner.md index f4dd90c9..8f83701b 100644 --- a/docs/cli/planner.md +++ b/docs/cli/planner.md @@ -28,7 +28,7 @@ Skips GitHub issue creation and uses timestamp-based artifact naming. The pipeli ### `--refine [refinement-instructions]` -Refines an existing plan issue by fetching its body from GitHub and rerunning the debate. Optional refinement instructions are appended to the context to steer the agents. Refinement runs still write artifacts with `issue-refine-` prefixes and update the existing issue unless `--dry-run` is set. Requires authenticated `gh` access to read the issue body. +Refines an existing plan issue by fetching its body from GitHub and rerunning the debate. Optional refinement instructions are appended to the context to steer the agents. The fetched issue body has the trailing provenance footer stripped before reuse as debate context. Refinement runs still write artifacts with `issue-refine-` prefixes and update the existing issue unless `--dry-run` is set. Requires authenticated `gh` access to read the issue body. ### `--verbose` (optional flag) @@ -51,7 +51,7 @@ Stage-specific keys override `planner.backend`. Defaults remain `claude:sonnet` ### Default Issue Creation -By default, `lol plan` creates a placeholder GitHub issue before the pipeline runs using a truncated placeholder title (`[plan] placeholder: ...`), and uses `issue-{N}` artifact naming. After the consensus stage completes, the issue body is updated with the final plan, the title is set from the first `Implementation Plan:` or `Consensus Plan:` header in the consensus file (fallback: truncated feature description), and the `agentize:plan` label is applied. +By default, `lol plan` creates a placeholder GitHub issue before the pipeline runs using a truncated placeholder title (`[plan] placeholder: ...`), and uses `issue-{N}` artifact naming. After the consensus stage completes, the issue body is updated with the final plan plus a trailing provenance footer (`Plan based on commit `), the title is set from the first `Implementation Plan:` or `Consensus Plan:` header in the consensus file (fallback: truncated feature description), and the `agentize:plan` label is applied. When `--refine` is used, no placeholder issue is created. The issue body is fetched and reused as debate context, and the issue is updated in-place after the consensus stage (unless `--dry-run` is set). diff --git a/python/agentize/workflow/planner/__main__.md b/python/agentize/workflow/planner/__main__.md index 6899f5b7..77c2d634 100644 --- a/python/agentize/workflow/planner/__main__.md +++ b/python/agentize/workflow/planner/__main__.md @@ -45,8 +45,9 @@ def main(argv: list[str]) -> int ``` CLI entrypoint for the planner backend. Parses args, resolves repo root and backend -configuration, runs stages, publishes plan updates (when enabled), and prints plain-text -progress output. Returns process exit code. +configuration, runs stages, publishes plan updates with a trailing commit provenance +footer (when enabled), and prints plain-text progress output. Refinement fetches strip +the footer before reuse as debate context. Returns process exit code. ## Internal Helpers @@ -68,6 +69,9 @@ progress output. Returns process exit code. - `_issue_create()`, `_issue_fetch()`, `_issue_publish()`: GitHub issue lifecycle for plan publishing. - `_extract_plan_title()`, `_apply_issue_tag()`: Plan title parsing and issue tagging. +- `_resolve_commit_hash()`: Resolves the current repo `HEAD` commit for provenance. +- `_append_plan_footer()`: Appends `Plan based on commit ` to consensus output. +- `_strip_plan_footer()`: Removes the trailing provenance footer from issue bodies. ### Backend selection diff --git a/python/agentize/workflow/planner/__main__.py b/python/agentize/workflow/planner/__main__.py index 0c50fed7..08cc05ea 100644 --- a/python/agentize/workflow/planner/__main__.py +++ b/python/agentize/workflow/planner/__main__.py @@ -369,6 +369,7 @@ def _check_stage_result(result: StageResult) -> None: _PLAN_HEADER_RE = re.compile(r"^#\s*(Implementation|Consensus) Plan:\s*(.+)$") _PLAN_HEADER_HINT_RE = re.compile(r"(Implementation Plan:|Consensus Plan:)", re.IGNORECASE) +_PLAN_FOOTER_RE = re.compile(r"^Plan based on commit (?:[0-9a-f]+|unknown)$") def _resolve_repo_root() -> Path: @@ -394,6 +395,70 @@ def _resolve_repo_root() -> Path: ) +def _resolve_commit_hash(repo_root: Path) -> str: + """Resolve the current git commit hash for provenance.""" + result = subprocess.run( + ["git", "-C", str(repo_root), "rev-parse", "HEAD"], + capture_output=True, + text=True, + ) + if result.returncode != 0: + message = result.stderr.strip() or result.stdout.strip() + if message: + print(f"Warning: Failed to resolve git commit: {message}", file=sys.stderr) + else: + print("Warning: Failed to resolve git commit", file=sys.stderr) + return "unknown" + + commit_hash = result.stdout.strip().lower() + if not commit_hash or not re.fullmatch(r"[0-9a-f]+", commit_hash): + print("Warning: Unable to parse git commit hash, using 'unknown'", file=sys.stderr) + return "unknown" + return commit_hash + + +def _append_plan_footer(consensus_path: Path, commit_hash: str) -> None: + """Append the commit provenance footer to a consensus plan file.""" + footer_line = f"Plan based on commit {commit_hash}" + try: + content = consensus_path.read_text() + except FileNotFoundError: + print( + f"Warning: Consensus plan missing, cannot append footer: {consensus_path}", + file=sys.stderr, + ) + return + + trimmed = content.rstrip("\n") + if trimmed.endswith(footer_line): + return + + with consensus_path.open("a") as handle: + if content and not content.endswith("\n"): + handle.write("\n") + handle.write(f"{footer_line}\n") + + +def _strip_plan_footer(text: str) -> str: + """Strip the trailing commit provenance footer from a plan body.""" + if not text: + return text + + lines = text.splitlines() + had_trailing_newline = text.endswith("\n") + while lines and not lines[-1].strip(): + lines.pop() + if not lines: + return "" + if not _PLAN_FOOTER_RE.match(lines[-1].strip()): + return text + lines.pop() + result = "\n".join(lines) + if had_trailing_newline and result: + result += "\n" + return result + + def _load_planner_backend_config(repo_root: Path, start_dir: Path) -> dict[str, str]: """Load planner backend overrides from .agentize.local.yaml.""" plugin_dir = repo_root / ".claude-plugin" @@ -548,7 +613,7 @@ def _issue_fetch(issue_number: str) -> tuple[str, Optional[str]]: ) issue_url = url_proc.stdout.strip() if url_proc.returncode == 0 else None - return body_proc.stdout, issue_url + return _strip_plan_footer(body_proc.stdout), issue_url def _issue_publish(issue_number: str, title: str, body_file: Path) -> bool: @@ -785,6 +850,8 @@ def _log_writer(message: str) -> None: except ValueError: consensus_display = str(consensus_result.output_path) consensus_path = consensus_result.output_path + commit_hash = _resolve_commit_hash(repo_root) + _append_plan_footer(consensus_path, commit_hash) _log_verbose("") _log("Pipeline complete!") diff --git a/src/cli/lol/commands/plan.md b/src/cli/lol/commands/plan.md index 8bbcf0d1..10028b51 100644 --- a/src/cli/lol/commands/plan.md +++ b/src/cli/lol/commands/plan.md @@ -7,7 +7,7 @@ Planning pipeline entrypoint for multi-agent debate workflows. ### lol plan Runs the debate pipeline for a feature description or refines an existing plan -issue. +issue. The generated consensus plan ends with `Plan based on commit `. **Usage**: ```bash diff --git a/tests/cli/test-lol-plan-pipeline-stubbed.sh b/tests/cli/test-lol-plan-pipeline-stubbed.sh index b67d61e0..e6481dc1 100755 --- a/tests/cli/test-lol-plan-pipeline-stubbed.sh +++ b/tests/cli/test-lol-plan-pipeline-stubbed.sh @@ -135,6 +135,17 @@ for stage in bold critique reducer; do fi done +# Verify consensus output includes commit provenance footer +CONSENSUS_PATH="${PREFIX}-consensus.md" +if [ ! -s "$CONSENSUS_PATH" ]; then + test_fail "Expected consensus .md artifact" +fi +FOOTER_LINE=$(tail -n 1 "$CONSENSUS_PATH") +echo "$FOOTER_LINE" | grep -qE "^Plan based on commit ([0-9a-f]+|unknown)$" || { + echo "Consensus footer line: $FOOTER_LINE" >&2 + test_fail "Consensus plan should end with commit provenance footer" +} + # ── Test 2: --verbose mode outputs detailed stage info ── > "$CALL_LOG"