Skip to content

Commit 22e1f52

Browse files
committed
fix: always clone into code-repo/<name>/, support multi-value --repo/--pr, qualify agent names
Three changes: 1. resolve_source.py: single repos now clone into code-repo/<repo_name>/ instead of flat into code-repo/. Consistent with multi-repo behavior. 2. resolve_source.py: --repo and --pr accept multiple space-delimited values (nargs="+"). Multiple --repo values each get cloned into their own subdirectory with primary + additional_repos in the result. 3. All workflow step skills: use fully qualified agent names (docs-tools:<agent>) in prose, descriptions, and subagent_type fields to prevent "agent not found" errors from bare name dispatch. Co-Authored-By: Claude Opus 4.6 <[email protected]> rh-pre-commit.version: 2.3.2 rh-pre-commit.check-secrets: ENABLED
1 parent b314fdd commit 22e1f52

8 files changed

Lines changed: 106 additions & 73 deletions

File tree

plugins/docs-tools/skills/docs-orchestrator/SKILL.md

Lines changed: 9 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
name: docs-orchestrator
33
description: Documentation workflow orchestrator. Reads the step list from .claude/docs-workflow.yaml (or the plugin default). Runs steps sequentially, manages progress state, handles iteration and confirmation gates. Claude is the orchestrator — the YAML is a step list, not a workflow engine.
44

5-
argument-hint: <ticket> [--workflow <name>] [--pr <url>]... [--source-code-repo <url-or-path>] [--mkdocs] [--draft] [--docs-repo-path <path>] [--create-jira <PROJECT>] [--create-merge-request]
5+
argument-hint: <ticket> [--workflow <name>] [--pr <url>...] [--source-code-repo <url-or-path>...] [--mkdocs] [--draft] [--docs-repo-path <path>] [--create-jira <PROJECT>] [--create-merge-request]
66

77
allowed-tools: Read, Write, Glob, Grep, Edit, Bash, Skill, AskUserQuestion
88
---
@@ -31,11 +31,11 @@ When displaying available options to the user (e.g., on skill load or when askin
3131

3232
- `$1` — JIRA ticket ID (required). If missing, STOP and ask the user.
3333
- `--workflow <name>` — Use `.claude/docs-<name>.yaml` instead of `docs-workflow.yaml`. Allows running alternative pipelines (e.g., writing-only, review-only). Falls back to the plugin default at `skills/docs-orchestrator/defaults/docs-workflow.yaml` if no project-level YAML exists
34-
- `--pr <url>` — PR/MR URLs (repeatable, accumulated into a list). Accepts GitHub PRs (`gh` CLI) and GitLab MRs (`glab` CLI). Used both as requirements input (agent reads diffs/descriptions) and for source repo resolution (repo URL and branch derived from the first PR/MR). When multiple PRs from different repos are provided, all repos are resolved and treated equally as source material
34+
- `--pr <url>...` — PR/MR URLs (space-delimited, one or more). Accepts GitHub PRs (`gh` CLI) and GitLab MRs (`glab` CLI). Used both as requirements input (agent reads diffs/descriptions) and for source repo resolution (repo URL and branch derived from the first PR/MR). When multiple PRs from different repos are provided, all repos are resolved and treated equally as source material
3535
- `--mkdocs` — Use Material for MkDocs format instead of AsciiDoc. Propagates to the writing step (generates `.md` with MkDocs front matter) and style-review step (applies Markdown-appropriate rules). Sets `options.format` to `"mkdocs"` in the progress file
3636
- `--draft` — Write documentation to the staging area (`.claude/docs/<ticket>/writing/`) instead of directly into the repo. Uses DRAFT placement mode: no framework detection, no file placement into the target repo. Without this flag, UPDATE-IN-PLACE is the default
3737
- `--docs-repo-path <path>` — Target documentation repository for UPDATE-IN-PLACE mode. The docs-writer explores this directory for framework detection (Antora, MkDocs, Docusaurus, etc.) and writes files there instead of the current working directory. Propagates to `writing` and `create-merge-request` steps (mapped to their internal `--repo-path` flag). **Precedence**: if both `--docs-repo-path` and `--draft` are passed, `--docs-repo-path` wins — log a warning and ignore `--draft`
38-
- `--source-code-repo <url-or-path>` — Source code repository for code evidence and requirements enrichment. Accepts remote URLs (https://, git@, ssh:// — shallow-cloned to `.claude/docs/<ticket>/code-repo/`) or local paths (used directly). Passed to requirements, code-evidence, and writing steps (mapped to their internal `--repo` flag). Without `--pr`, the entire repo is the subject matter; with `--pr`, the PR branch is checked out so code-evidence reflects the PR's state. Takes highest priority in source resolution, overriding `source.yaml` and PR-derived URLs
38+
- `--source-code-repo <url-or-path>...` — Source code repository/repositories for code evidence and requirements enrichment (space-delimited, one or more). Accepts remote URLs (https://, git@, ssh:// — each shallow-cloned to `.claude/docs/<ticket>/code-repo/<repo_name>/`) or local paths (used directly). The first repo is treated as primary; additional repos are returned as `additional_repos` in the result. Passed to requirements, code-evidence, and writing steps (mapped to their internal `--repo` flag). Without `--pr`, the entire repo is the subject matter; with `--pr`, the PR branch is checked out on the primary repo so code-evidence reflects the PR's state. Takes highest priority in source resolution, overriding `source.yaml` and PR-derived URLs
3939
- `--create-jira <PROJECT>` — Create a linked JIRA ticket in the specified project after the planning step completes. Runs the standalone `docs-workflow-create-jira` workflow (use `--workflow workflow-create-jira`). Requires `JIRA_API_TOKEN` to be set
4040
- `--create-merge-request` — Create a branch, commit, push, and open a merge request or pull request after reviews complete. Activates the `create-merge-request` workflow step (guarded by `when: create_merge_request`). Off by default
4141

@@ -50,8 +50,7 @@ When displaying available options to the user (e.g., on skill load or when askin
5050

5151
# Multiple PRs from different repos, written to a separate docs repo
5252
/docs-orchestrator PROJ-123 \
53-
--pr https://github.com/org/backend/pull/10 \
54-
--pr https://gitlab.example.com/org/frontend/-/merge_requests/5 \
53+
--pr https://github.com/org/backend/pull/10 https://gitlab.example.com/org/frontend/-/merge_requests/5 \
5554
--docs-repo-path /home/user/docs-repo
5655

5756
# Source repo without PRs, draft mode, with merge request creation
@@ -87,8 +86,8 @@ Run the script with whatever source information is available from CLI args:
8786
```bash
8887
python3 ${CLAUDE_SKILL_DIR}/scripts/resolve_source.py \
8988
--base-path <base_path> \
90-
[--repo <url-or-path>] \
91-
[--pr <url>]...
89+
[--repo <url-or-path>...] \
90+
[--pr <url>...]
9291
```
9392

9493
The script checks sources in priority order:
@@ -103,7 +102,7 @@ The script outputs JSON to stdout:
103102
```json
104103
{
105104
"status": "resolved",
106-
"repo_path": ".claude/docs/proj-123/code-repo",
105+
"repo_path": ".claude/docs/proj-123/code-repo/operator",
107106
"repo_url": "https://github.com/org/operator",
108107
"ref": "pr-branch-name",
109108
"scope": null
@@ -225,8 +224,8 @@ Use this absolute `BASE_PATH` for the progress file's `base_path` field and for
225224
```
226225
.claude/docs/proj-123/
227226
source.yaml (per-ticket source config, if applicable)
228-
code-repo/ (single repo: flat clone; multi-repo: subdirs)
229-
<repo-name>/ (only when multiple repos are resolved)
227+
code-repo/
228+
<repo-name>/ (each repo gets its own subdirectory)
230229
requirements/
231230
requirements.md
232231
step-result.json (sidecar: title)

plugins/docs-tools/skills/docs-orchestrator/scripts/resolve_source.py

Lines changed: 80 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -165,6 +165,11 @@ def _normalize_git_url(url):
165165
return url.rstrip("/").removesuffix(".git")
166166

167167

168+
def _repo_name_from_url(url):
169+
"""Extract the repository name from a git URL."""
170+
return _normalize_git_url(url).split("/")[-1]
171+
172+
168173
def _resolve_pr_info(pr_url):
169174
"""Extract repo URL and branch from a GitHub PR or GitLab MR URL.
170175
@@ -390,7 +395,7 @@ def _write_source_yaml(base_path, repo, ref):
390395
def _resolve_multiple_prs(pr_urls, base_path):
391396
"""Resolve and clone repos from a list of PR/MR URLs.
392397
393-
Groups PRs by repo, clones each repo into code-repo/<repo_name>/,
398+
Groups PRs by repo, clones each into code-repo/<repo_name>/,
394399
and returns a success result with primary + additional repos.
395400
"""
396401
# Group PRs by normalized repo URL
@@ -413,17 +418,12 @@ def _resolve_multiple_prs(pr_urls, base_path):
413418

414419
resolved_repos = []
415420
errors = []
416-
use_subdirs = len(repo_groups) > 1
417-
418421
for normalized, info in repo_groups.items():
419422
repo_url = info["repo_url"]
420423
ref = info["ref"]
421424

422-
if use_subdirs:
423-
repo_name = normalized.split("/")[-1]
424-
repo_clone_dir = base_path / "code-repo" / repo_name
425-
else:
426-
repo_clone_dir = base_path / "code-repo"
425+
repo_name = _repo_name_from_url(repo_url)
426+
repo_clone_dir = base_path / "code-repo" / repo_name
427427

428428
if repo_clone_dir.exists():
429429
if not _verify_existing_clone(repo_clone_dir, ref, expected_repo_url=repo_url):
@@ -482,23 +482,24 @@ def _success(repo_path, repo_url=None, ref=None, scope=None, discovered_repos=No
482482
return result
483483

484484

485-
def resolve(args):
486-
"""Main resolution logic. Returns a result dict."""
487-
base_path = Path(args.base_path)
488-
clone_dir = base_path / "code-repo"
485+
def _resolve_explicit_repos(repo_values, pr_urls, base_path):
486+
"""Resolve one or more explicit --repo values.
489487
490-
# Collect PR URLs from args
491-
pr_urls = args.pr or []
488+
Clones each remote repo into code-repo/<repo_name>/.
489+
For a single repo with PRs, the first PR's branch is checked out.
490+
Returns primary + additional repos when multiple are given.
491+
"""
492+
resolved_repos = []
493+
errors = []
492494

493-
# --- Priority 1: Explicit --repo flag ---
494-
if args.repo:
495-
repo_value = args.repo
495+
for i, repo_value in enumerate(repo_values):
496496
ref = None
497-
scope = None
498497

499498
if _is_remote_url(repo_value):
500-
# If PRs provided, get the branch from the first PR
501-
if pr_urls:
499+
clone_dir = base_path / "code-repo" / _repo_name_from_url(repo_value)
500+
501+
# First repo gets the PR branch (if any)
502+
if i == 0 and pr_urls:
502503
try:
503504
_, pr_branch = _resolve_pr_info(pr_urls[0])
504505
ref = pr_branch
@@ -508,37 +509,68 @@ def resolve(args):
508509
file=sys.stderr,
509510
)
510511

511-
# Clone or verify
512512
if clone_dir.exists():
513513
if not _verify_existing_clone(clone_dir, ref, expected_repo_url=repo_value):
514-
return {
515-
"status": "error",
516-
"message": (
517-
f"Existing clone at {clone_dir} is invalid "
518-
"or points to a different repo."
519-
),
520-
}
514+
errors.append(
515+
f"Existing clone at {clone_dir} is invalid "
516+
"or points to a different repo."
517+
)
518+
continue
521519
else:
522520
if not _clone_repo(repo_value, clone_dir, ref):
523-
return {
524-
"status": "error",
525-
"message": (
526-
f"Cannot clone {repo_value}. "
527-
"For private repos, ensure gh is authenticated."
528-
),
529-
}
521+
errors.append(
522+
f"Cannot clone {repo_value}. "
523+
"For private repos, ensure gh is authenticated."
524+
)
525+
continue
530526

531-
_write_source_yaml(base_path, repo_value, ref)
532-
return _success(clone_dir, repo_url=repo_value, ref=ref, scope=scope)
527+
resolved_repos.append({
528+
"repo_path": str(clone_dir),
529+
"repo_url": repo_value,
530+
"ref": ref,
531+
})
533532
else:
534-
# Local path
535533
local = Path(repo_value)
536534
if not local.exists() or not local.is_dir():
537-
return {
538-
"status": "error",
539-
"message": f"Source repo path does not exist: {repo_value}",
540-
}
541-
return _success(local, ref=ref, scope=scope)
535+
errors.append(f"Source repo path does not exist: {repo_value}")
536+
continue
537+
resolved_repos.append({
538+
"repo_path": str(local),
539+
"repo_url": None,
540+
"ref": None,
541+
})
542+
543+
if not resolved_repos:
544+
return {
545+
"status": "error",
546+
"message": f"Could not resolve any repos. Errors: {'; '.join(errors)}",
547+
}
548+
549+
primary = resolved_repos[0]
550+
_write_source_yaml(base_path, primary.get("repo_url") or primary["repo_path"], primary["ref"])
551+
552+
result = _success(
553+
primary["repo_path"],
554+
repo_url=primary.get("repo_url"),
555+
ref=primary["ref"],
556+
)
557+
if len(resolved_repos) > 1:
558+
result["additional_repos"] = resolved_repos[1:]
559+
if errors:
560+
result["warnings"] = errors
561+
return result
562+
563+
564+
def resolve(args):
565+
"""Main resolution logic. Returns a result dict."""
566+
base_path = Path(args.base_path)
567+
568+
# Collect PR URLs from args
569+
pr_urls = args.pr or []
570+
571+
# --- Priority 1: Explicit --repo flag ---
572+
if args.repo:
573+
return _resolve_explicit_repos(args.repo, pr_urls, base_path)
542574

543575
# --- Priority 2: source.yaml ---
544576
source_config = _read_source_yaml(base_path)
@@ -556,6 +588,7 @@ def resolve(args):
556588
pass
557589

558590
if _is_remote_url(repo_value):
591+
clone_dir = base_path / "code-repo" / _repo_name_from_url(repo_value)
559592
if clone_dir.exists():
560593
if not _verify_existing_clone(clone_dir, ref, expected_repo_url=repo_value):
561594
return {
@@ -611,12 +644,13 @@ def main():
611644
)
612645
parser.add_argument(
613646
"--repo",
614-
help="Source repo URL or local path",
647+
nargs="+",
648+
help="Source repo URL(s) or local path(s), space-delimited",
615649
)
616650
parser.add_argument(
617651
"--pr",
618-
action="append",
619-
help="PR/MR URL (repeatable)",
652+
nargs="+",
653+
help="PR/MR URL(s), space-delimited",
620654
)
621655
parser.add_argument(
622656
"--scan-requirements",

plugins/docs-tools/skills/docs-workflow-planning/SKILL.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
---
22
name: docs-workflow-planning
3-
description: Create a documentation plan from requirements analysis output. Dispatches the docs-planner agent. Invoked by the orchestrator.
3+
description: Create a documentation plan from requirements analysis output. Dispatches the docs-tools:docs-planner agent. Invoked by the orchestrator.
44
argument-hint: <ticket> --base-path <path>
55
allowed-tools: Read, Write, Glob, Grep, Edit, Bash, Skill, Agent
66
---
@@ -43,7 +43,7 @@ mkdir -p "$OUTPUT_DIR"
4343

4444
### 2. Dispatch agent
4545

46-
**You MUST use the Agent tool** to invoke the `docs-planner` subagent. Do NOT read the agent's markdown file or attempt to perform the agent's work yourself — the agent has a specialized system prompt and must run as an isolated subagent.
46+
**You MUST use the Agent tool** to invoke the `docs-tools:docs-planner` subagent. Do NOT read the agent's markdown file or attempt to perform the agent's work yourself — the agent has a specialized system prompt and must run as an isolated subagent.
4747

4848
**Agent tool parameters:**
4949
- `subagent_type`: `docs-tools:docs-planner`

plugins/docs-tools/skills/docs-workflow-requirements/SKILL.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ Step skill for the docs-orchestrator pipeline. Follows the step skill contract:
1111

1212
This skill uses a two-pass architecture to analyze documentation requirements:
1313

14-
1. **Discovery pass** — A single `requirements-discoverer` agent enumerates requirements from JIRA, PRs, and specs, producing a JSON skeleton
15-
2. **Deep analysis pass** — One `requirements-analyst` agent per requirement, all running in parallel, each performing thorough analysis with a clean context window
14+
1. **Discovery pass** — A single `docs-tools:requirements-discoverer` agent enumerates requirements from JIRA, PRs, and specs, producing a JSON skeleton
15+
2. **Deep analysis pass** — One `docs-tools:requirements-analyst` agent per requirement, all running in parallel, each performing thorough analysis with a clean context window
1616
3. **Merge** — The orchestrator assembles per-requirement JSON results into the standard `requirements.md` format
1717

1818
## Arguments
@@ -45,7 +45,7 @@ mkdir -p "$OUTPUT_DIR"
4545

4646
### 2. Pass 1 — Discovery
4747

48-
Dispatch one `requirements-discoverer` agent to enumerate requirements from all sources.
48+
Dispatch one `docs-tools:requirements-discoverer` agent to enumerate requirements from all sources.
4949

5050
```
5151
Agent:
@@ -83,7 +83,7 @@ If `requirements` is empty, write a minimal `requirements.md` noting that no req
8383

8484
### 4. Pass 2 — Fan out deep analysis
8585

86-
For each requirement in the discovery skeleton, dispatch one `requirements-analyst` agent. Launch ALL agents in a **single message** (parallel execution).
86+
For each requirement in the discovery skeleton, dispatch one `docs-tools:requirements-analyst` agent. Launch ALL agents in a **single message** (parallel execution).
8787

8888
For each requirement, use:
8989

0 commit comments

Comments
 (0)