Skip to content

[DSPX-3302] (6/6) feature-orchestrate skill + cells-of-effort spec#455

Draft
dmihalcik-virtru wants to merge 2 commits into
DSPX-3302-05-claude-pluginfrom
DSPX-3302-06-feature-orchestrate
Draft

[DSPX-3302] (6/6) feature-orchestrate skill + cells-of-effort spec#455
dmihalcik-virtru wants to merge 2 commits into
DSPX-3302-05-claude-pluginfrom
DSPX-3302-06-feature-orchestrate

Conversation

@dmihalcik-virtru
Copy link
Copy Markdown
Member

Summary

Adds feature-orchestrate — the second half of the multi-repo feature workflow that feature-design (PR 5) started. Reads a feature spec at xtest/features/<name>.yaml, creates git worktrees, and fans claude -p subagents out in topological waves to implement each cell of work in parallel where possible.

What it does

  • feature-orchestrate skill (tests/.claude/skills/feature-orchestrate/SKILL.md) — thin wrapper that surfaces a dry-run plan, asks the user to confirm, then dispatches.
  • otdf-sdk-mgr orchestrate run <spec> (tests/otdf-sdk-mgr/src/otdf_sdk_mgr/cli_orchestrate.py) — the engine. Parses the spec, topologically sorts cells by depends_on, creates worktrees at ~/Documents/GitHub/worktrees/<JIRA-KEY>-<cell-key>/, dispatches one claude -p subagent per cell. Cells in the same wave run in parallel via ThreadPoolExecutor. Each per-cell prompt embeds the full spec body for cross-cell context. Flags: --dry-run, --only <cell> (repeatable), --timeout, --model.

Spec schema change (cells, not repos)

The Feature spec format evolves from "one entry per repo" to "one entry per cell of effort." The platform monorepo holds proto definitions, the Go SDK, KAS service code, and shared libs, so a single feature often touches multiple cells inside one repo. Each cell now has a path: (which sibling repo to worktree from), a branch:, a todo: list, and an optional depends_on: edge.

Canonical example: every SDK cell declares depends_on: [platform-proto] whenever the feature changes wire format — the proto cell regenerates Go/Java/JS bindings before the SDKs can adopt them.

feature-design's SKILL.md Step 2/3 is updated in this PR to teach the new cell shape. Branches are now per-cell (<JIRA-KEY>-<cell-key>) so concurrent worktrees of the same repo don't collide.

Subagent dispatch

Each per-cell subagent:

  • Runs claude -p --model sonnet --permission-mode acceptEdits inside its worktree.
  • Gets a tailored prompt: the full spec body + this cell's path / branch / todo + house-style commit guidance (subject embeds (DSPX-XXXX), no Jira: footer, Co-Authored-By: Claude).
  • Implements its todo, runs repo-local checks, commits, opens a draft PR via gh pr create --draft, prints the PR URL as the last line of stdout.
  • Has a 30-minute default timeout (--timeout to override).

The orchestrator pre-writes a minimal .claude/settings.json into each worktree if one isn't already there, allowlisting the repo-type-appropriate test commands (go/make/buf for platform, mvn for java-sdk, npm for web-sdk) plus universal git / gh pr create. User-committed .claude/settings.json files are left alone.

Stack

  1. (base) Shared schema — chore(xtest): Shared Scenario/Instance Pydantic schema in otdf-sdk-mgr #450
  2. (base) Platform installer + install scenario — [DSPX-3302] (2/5) Manage platform service + install scenario in otdf-sdk-mgr #451
  3. (base) otdf-local multi-instance refactor — [DSPX-3302] (3/5) otdf-local multi-instance refactor #452
  4. (base) xtest conftest integration — [DSPX-3302] (4/5) xtest conftest: --scenario and --instance flags #453
  5. (base) Claude plugin + schema dump CLI — [DSPX-3302] (5/5) Claude plugin: bug-repro skills for OpenTDF #454
  6. This PR — Feature orchestrator

This PR is stacked on #454; merge that one first.

Test plan

  • Unit tests pass: cd tests/otdf-sdk-mgr && uv run pytest tests/test_orchestrate.py (10 new tests covering topological sort, cycle detection, diamond dependencies, schema validation, worktree path resolution)
  • Dry-run on a synthetic feature spec: uv run otdf-sdk-mgr orchestrate run <spec> --dry-run — verify topo order and per-cell worktree paths print correctly
  • Single-cell run: uv run otdf-sdk-mgr orchestrate run <spec> --only platform-proto — verify worktree creation, subagent dispatch, and PR URL extraction on one cell
  • Full run on a small synthetic spec (~2 cells with depends_on) — verify wave ordering and parallel dispatch within a wave
  • Verify a worktree on the wrong branch causes a clean error rather than disturbing user work
  • Inspect a per-cell transcript at .claude/tmp/runs/<KEY>-<cell>.jsonl — confirm the subagent invoked gh pr create --draft and printed a PR URL

Out of scope (deferred follow-ups)

  • feature-orchestrate status <spec> — polls gh pr list to update on PR progress per cell.
  • feature-orchestrate --retry <cell-key> — re-launch a failed subagent (v1 just reports the failure and tells the user to re-invoke with --only <cell>).
  • Pydantic Feature model + schema-dump entry (informal YAML for v1).
  • Cross-PR linking automation — each subagent's PR body references the parent Jira, but we don't auto-update other cells' PRs when one merges.
  • Non-Anthropic models for subagents (v1 pins --model sonnet).
  • Subagent permission scope refinement — v1 uses --permission-mode acceptEdits + auto-written .claude/settings.json; a tighter per-repo allowlist is a follow-up.

Jira: https://virtru.atlassian.net/browse/DSPX-3302

🤖 Generated with Claude Code

…-3302)

Implements the second half of the multi-repo feature workflow. feature-design
(landed in PR 5) authors the spec and tests-side artifacts; feature-orchestrate
reads that spec, creates git worktrees, and fans claude -p subagents out in
topological waves so each cell of work proceeds in parallel where possible.

Spec schema change (informal — no Pydantic model yet):

The platform monorepo holds proto definitions, the Go SDK, KAS service code,
and shared libs, so a single feature often touches multiple "cells of effort"
inside one repo. The spec now expresses work as cells, not repos. Each cell
has a `path:` (which sibling repo to worktree from), a `branch:`, a `todo:`
list, and an optional `depends_on:` edge. Canonical example: every SDK cell
declares `depends_on: [platform-proto]` whenever the feature changes wire
format — the proto cell regenerates Go/Java/JS bindings before the SDKs can
adopt them.

- `otdf-sdk-mgr orchestrate run <spec>` (new CLI verb in `cli_orchestrate.py`):
  parses the spec, topologically sorts cells by `depends_on`, creates worktrees
  at `~/Documents/GitHub/worktrees/<JIRA-KEY>-<cell-key>/`, and dispatches one
  `claude -p` subagent per cell. Cells in the same wave run in parallel via
  `ThreadPoolExecutor`. Per-cell prompts embed the full spec body for
  cross-cell context. `--dry-run`, `--only <cell>`, `--timeout`, `--model`
  flags. Per-cell stdout captured to `.claude/tmp/runs/<KEY>-<cell>.jsonl`.
- Per-worktree `.claude/settings.json` auto-written if absent, with a minimal
  allowlist tailored to the repo (`go`/`make`/`buf` for platform, `mvn` for
  java-sdk, `npm` for web-sdk) plus universal `git`/`gh pr create`. Skipped
  if the user has committed their own settings.
- `feature-orchestrate` SKILL.md: thin wrapper that surfaces the dry-run plan,
  asks the user to confirm, then dispatches.
- `feature-design` SKILL.md Step 2/3: teaches the cell shape, `path:`,
  `depends_on:`, and the proto-blocks-SDK pattern as the canonical example.
  Branches are now per-cell (`<JIRA-KEY>-<cell-key>`) so concurrent worktrees
  of the same repo don't collide.
- 10 new unit tests (`test_orchestrate.py`): load + validation, topological
  waves with skip semantics, cycle detection (incl. diamond), worktree path
  resolution. All passing alongside the existing 65.
- `xtest/features/{README,CLAUDE}.md`: cell-aware terminology.
- `settings.json` + `plugin.json`: `Bash(claude -p *)`, `Bash(git worktree *)`,
  `Bash(gh pr create *)` allowlists; `Skill(feature-orchestrate)` registered.

Out of scope for this PR: status command, --retry, Pydantic model for Feature,
cross-PR linking automation, non-Sonnet subagent models. See plan file for
the full follow-up list.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented May 18, 2026

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: f4082717-fc23-4ee9-b888-13b6f51347c1

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch DSPX-3302-06-feature-orchestrate

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link
Copy Markdown
Contributor

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request introduces the feature-orchestrate skill and a CLI command to automate multi-repo feature implementation using a 'cells of effort' model. This approach allows for granular task management within repositories via git worktrees and dependency-based parallel execution. Feedback from the reviewer focuses on enhancing the orchestrator's reliability and portability, specifically suggesting improvements for git branch handling during worktree setup, consolidating exception handling in subagents, providing configurability for root directories, broadening error catching for YAML parsing, and implementing a cap on parallel worker threads to prevent resource exhaustion.

Comment on lines +193 to +195
subprocess.check_call(
["git", "-C", str(repo), "worktree", "add", str(wt), "-b", cell.branch],
)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The git worktree add -b <branch> command will fail if the branch already exists in the repository. This is a common scenario when retrying a failed orchestration run where the worktree was removed but the branch remains. It's safer to check if the branch exists first and only use -b if it doesn't.

    # Check if branch exists in the source repo
    has_branch = subprocess.run(
        ["git", "-C", str(repo), "rev-parse", "--verify", cell.branch],
        capture_output=True,
    ).returncode == 0

    cmd = ["git", "-C", str(repo), "worktree", "add", str(wt)]
    if not has_branch:
        cmd.extend(["-b", cell.branch])
    else:
        cmd.append(cell.branch)
    subprocess.check_call(cmd)

Comment on lines +305 to +343
try:
wt = ensure_worktree(spec, cell)
except Exception as e:
return CellResult(cell, Path(), Path(), False, None, f"worktree: {e}")

ensure_subagent_settings(wt, cell.path)

transcripts_dir.mkdir(parents=True, exist_ok=True)
transcript = transcripts_dir / f"{spec.jira or spec.name}-{cell.key}.jsonl"

cmd = [
"claude", "-p",
"--model", model,
"--permission-mode", "acceptEdits",
"--output-format", "stream-json",
"--verbose",
build_prompt(spec, cell),
]
try:
with transcript.open("w", encoding="utf-8") as out:
completed = subprocess.run(
cmd,
cwd=wt,
stdout=out,
stderr=subprocess.STDOUT,
timeout=timeout_s,
)
except subprocess.TimeoutExpired:
return CellResult(cell, wt, transcript, False, None, f"timed out after {timeout_s}s")

if completed.returncode != 0:
return CellResult(cell, wt, transcript, False, None, f"exit {completed.returncode}")

pr_url: str | None = None
for line in transcript.read_text(encoding="utf-8").splitlines():
m = PR_URL_RE.search(line)
if m:
pr_url = m.group(0)
return CellResult(cell, wt, transcript, True, pr_url, None)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The current exception handling in run_cell is fragmented and misses several potential failure points (e.g., ensure_subagent_settings, transcript.open, or OSError from subprocess.run if the claude binary is missing). Wrapping the entire logic in a single try...except block ensures that any failure is captured and reported as a CellResult error, preventing thread crashes in the orchestrator.

    wt = Path()
    transcript = Path()
    try:
        wt = ensure_worktree(spec, cell)
        ensure_subagent_settings(wt, cell.path)

        transcripts_dir.mkdir(parents=True, exist_ok=True)
        transcript = transcripts_dir / f"{spec.jira or spec.name}-{cell.key}.jsonl"

        cmd = [
            "claude", "-p",
            "--model", model,
            "--permission-mode", "acceptEdits",
            "--output-format", "stream-json",
            "--verbose",
            build_prompt(spec, cell),
        ]
        with transcript.open("w", encoding="utf-8") as out:
            completed = subprocess.run(
                cmd,
                cwd=wt,
                stdout=out,
                stderr=subprocess.STDOUT,
                timeout=timeout_s,
            )

        if completed.returncode != 0:
            return CellResult(cell, wt, transcript, False, None, f"exit {completed.returncode}")

        pr_url: str | None = None
        for line in transcript.read_text(encoding="utf-8").splitlines():
            m = PR_URL_RE.search(line)
            if m:
                pr_url = m.group(0)
        return CellResult(cell, wt, transcript, True, pr_url, None)

    except subprocess.TimeoutExpired:
        return CellResult(cell, wt, transcript, False, None, f"timed out after {timeout_s}s")
    except Exception as e:
        return CellResult(cell, wt, transcript, False, None, str(e))

Comment on lines +159 to +160
OPENTDF_ROOT = Path.home() / "Documents/GitHub/opentdf"
WORKTREES_ROOT = Path.home() / "Documents/GitHub/worktrees"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

These paths are hardcoded to a specific directory structure in the user's home directory. This makes the tool less portable and assumes a specific environment setup. Consider making these configurable via environment variables or CLI options (e.g., --opentdf-root and --worktrees-root).

Comment on lines +377 to +381
try:
spec = load_spec(spec_path)
except ValueError as e:
typer.echo(f"Error: {e}", err=True)
raise typer.Exit(1) from e
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The load_spec function uses ruamel.yaml.load, which can raise various YAMLError exceptions if the input file is malformed. The current try...except only catches ValueError. Catching a broader exception would make the CLI more robust against invalid YAML files.

Suggested change
try:
spec = load_spec(spec_path)
except ValueError as e:
typer.echo(f"Error: {e}", err=True)
raise typer.Exit(1) from e
try:
spec = load_spec(spec_path)
except Exception as e:
typer.echo(f"Error: {e}", err=True)
raise typer.Exit(1) from e

)
failed.add(skipped.key)

with concurrent.futures.ThreadPoolExecutor(max_workers=max(1, len(runnable))) as ex:
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Spawning one thread (and one claude -p process) per cell in a wave without a cap could lead to resource exhaustion (CPU/Memory) or API rate limiting if a feature has many independent cells. It's recommended to cap the max_workers to a reasonable value (e.g., 8).

        max_workers = min(8, len(runnable)) if runnable else 1
        with concurrent.futures.ThreadPoolExecutor(max_workers=max_workers) as ex:

…X-3302)

Change Write(xtest/bug_*_test.py) to Write(xtest/**) — wildcards must sit
at path boundaries, not in filename patterns. Consolidates and simplifies
xtest write permissions across both settings.json and plugin.json.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
@sonarqubecloud
Copy link
Copy Markdown

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant