Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion docs/cli/lol.md
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ lol plan [--dry-run] [--verbose] [--editor] [--refine <issue-no> [refinement-ins
lol plan --refine <issue-no> [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 <hash>`.

#### Options

Expand Down
4 changes: 2 additions & 2 deletions docs/cli/planner.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ Skips GitHub issue creation and uses timestamp-based artifact naming. The pipeli

### `--refine <issue-no> [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-<N>` 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-<N>` prefixes and update the existing issue unless `--dry-run` is set. Requires authenticated `gh` access to read the issue body.

### `--verbose` (optional flag)

Expand All @@ -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: <first 50 chars>...`), 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: <first 50 chars>...`), 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 <hash>`), 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).

Expand Down
8 changes: 6 additions & 2 deletions python/agentize/workflow/planner/__main__.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand All @@ -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 <hash>` to consensus output.
- `_strip_plan_footer()`: Removes the trailing provenance footer from issue bodies.

### Backend selection

Expand Down
69 changes: 68 additions & 1 deletion python/agentize/workflow/planner/__main__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand All @@ -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"
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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!")
Expand Down
2 changes: 1 addition & 1 deletion src/cli/lol/commands/plan.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <hash>`.

**Usage**:
```bash
Expand Down
11 changes: 11 additions & 0 deletions tests/cli/test-lol-plan-pipeline-stubbed.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down