From bda7e4830bb217f105deee856dfd90ad9556fece Mon Sep 17 00:00:00 2001 From: Jian Weng Date: Wed, 4 Feb 2026 12:15:43 +0300 Subject: [PATCH] feat: append commit provenance footer to lol plan output Summary: - Append commit provenance footer to consensus plans and strip it during refinement. - Document footer behavior across planner and lol plan docs. - Add test coverage for the footer line in stubbed pipeline runs. Files: - docs/cli/lol.md: note consensus footer in lol plan description. - docs/cli/planner.md: document footer append and refine stripping behavior. - src/cli/lol/commands/plan.md: mention footer in command docs. - python/agentize/workflow/planner/__main__.md: describe footer helpers and refine behavior. - python/agentize/workflow/planner/__main__.py: resolve commit hash, append footer, strip footer for refine. - tests/cli/test-lol-plan-pipeline-stubbed.sh: assert consensus footer line. Issue: #794 Tests: - make test-fast TEST_SHELLS="bash zsh" --- docs/cli/lol.md | 2 +- docs/cli/planner.md | 4 +- python/agentize/workflow/planner/__main__.md | 8 ++- python/agentize/workflow/planner/__main__.py | 69 +++++++++++++++++++- src/cli/lol/commands/plan.md | 2 +- tests/cli/test-lol-plan-pipeline-stubbed.sh | 11 ++++ 6 files changed, 89 insertions(+), 7 deletions(-) diff --git a/docs/cli/lol.md b/docs/cli/lol.md index c7228f7..f2e4a1b 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 f4dd90c..8f83701 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 6899f5b..77c2d63 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 0c50fed..08cc05e 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 8bbcf0d..10028b5 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 b67d61e..e6481dc 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"