Skip to content

T-6HFR-orchestrator-categorized-in-flight-limits

Status: closed/done · Impact: high · Complexity: medium

/sdlc:orchestrate today enforces a hard-coded “parallelism cap of 2” inside its body. That counter conflates three structurally different states — actively-implementing tasks, tasks with PRs open awaiting human review, and stale closed worktrees the close-out missed — and gates new dispatch off all three lumped together. The result is the orchestrator stops starting new work the moment two PRs are open, even though no implementation sub-agents are actually running. Fix the conflation by counting categories separately (via a deterministic helper script), with each category’s limit configurable in sdlc.yaml. Defaults: max_implementations: 5, max_awaiting_review: 20.

  • plugin/skills/orchestrate/SKILL.md defines the cap in two places: a ### Parallelism cap — 2 preamble section, and the body of Step 3 (“Count the currently-in-flight task-work runs… If that count is already at the parallelism cap of 2, do not dispatch this tick”). Counting rule: “anything that has a worktree under .claude/worktrees/ and a matching feat/<basename> branch counts as active.”
  • The “parallelism cap” naming is misleading — the orchestrator does not cap parallel sub-agents in general. Step 2 dispatches one pr-check sub-agent per open PR with no upper bound; close-out and conflict-resolution sub-agents fire freely. Only Step 3’s new-implementation dispatch is gated.
  • plugin/skills/orchestrate/invariants.yaml pins "parallelism cap of 2" as a required_phrase against the preamble — locking the misleading name and the hard-coded literal.
  • sdlc.yaml at the project root already carries per-project SDLC configuration (today: quality_checks: only). plugin/conventions/sdlc-yaml.md documents the shape. No orchestrator: block exists yet.
  • The counting logic is inline shell in the skill body (ls .claude/worktrees/ + git branch --list 'feat/*' + intersection); there is no script. Every category I care about (implementing / awaiting-review / stale) is derivable from existing artifacts (docs/planning/tasks/<basename>.md frontmatter status, worktree dir, local branch presence, gh pr list for that branch) but nothing aggregates them today.
  • 2026-05-20-orchestrate-validates-worktree-task-status (planning/draft) covered the stale-worktree subset of this concern; consolidated into this task and the file deleted (no associated commits or branch).
  • A new deterministic helper script at plugin/scripts/count_inflight_tasks.py (PEP-723 self-bootstrapping like the other plugin scripts) that walks .claude/worktrees/, matches against feat/<basename> branches, reads each task file’s status: frontmatter, and queries gh pr list --search "head:feat/<basename>". Emits one JSON object per in-flight task with fields {basename, worktree_path, branch, task_status, open_pr_number_or_null, category}. Categories: implementing (status in-progress, no open PR), awaiting-review (status in-progress, open PR exists), stale (status starts with closed/ — should have been torn down).

  • sdlc.yaml gains an orchestrator: block:

    orchestrator:
    max_implementations: 5 # cap on simultaneous task-work sub-agents
    max_awaiting_review: 20 # cap on open PRs from task-work
  • /sdlc:orchestrate SKILL.md restructure:

    • Step 3 becomes “Count in-flight” (extracts the cap-check as its own numbered step, shelling out to count_inflight_tasks.py, reads limits from sdlc.yaml). Step 4 becomes the renumbered dispatch.
    • “Parallelism cap” naming retired throughout. New term: “in-flight limits” (plural; two categories).
    • The preamble’s ### Parallelism cap — 2 section deleted; its content folded into the new Step 3 prose.
    • Step 4 (dispatch) reads the script’s implementing count and the configured max_implementations; dispatches up to max_implementations - implementing_count new task-works.
    • The awaiting-review limit gates a separate signal: if at or above max_awaiting_review, the digest line records awaiting-review-cap-reached so the user knows the review queue is saturated; new implementations can still start (they don’t consume review-queue slots until they reach Step 10).
    • stale entries appear in the digest as a warning so the user (or the orchestrator’s next tick) can prompt a manual teardown. Stale tasks do NOT count toward either limit.
  1. Convention update first. Extend plugin/conventions/sdlc-yaml.md with the orchestrator: block shape, default values, and per-field semantics (what each limit gates). Pin the default values (5, 20) in prose so they’re discoverable without running the orchestrator.
  2. Write plugin/scripts/count_inflight_tasks.py. PEP-723 header for self-bootstrapping. Args: --project-root (default cwd), --json (default), --format=summary (one-line per category counts). Read sdlc.yaml to apply project-specific limits to the summary output; emit raw category counts regardless of limits. Walk .claude/worktrees/ first, then for each entry check feat branch + task file status + gh pr list (cached per-tick if possible; otherwise one gh call per worktree). Idempotent and side-effect-free.
  3. Update /sdlc:orchestrate SKILL.md. Extract the cap-check as a new numbered Step 3 (“Count in-flight”). Renumber the dispatch step to Step 4. Rewrite the prose to: shell out to count_inflight_tasks.py --json, parse the output, compare against configured max_implementations for dispatch (Step 4) and max_awaiting_review for the digest annotation. Delete the preamble ### Parallelism cap — 2 section; the new step is the only canonical location.
  4. Update plugin/skills/orchestrate/invariants.yaml. Remove "parallelism cap of 2" from required_phrases. Add "in-flight limits" as a required phrase scoped to the new Step 3. Add a required_tool_refs: entry for count_inflight_tasks.py so future skill drift away from the script trips the lint.
  5. Update the digest format. The digest line gains two optional fields: inflight={implementing,awaiting-review,stale} shorthand counts and caps-reached=<list> when any limit gates. Backward-compatible append; existing parsers won’t break since fields are key=value.
  6. Smoke test. Run /sdlc:orchestrate against current repo state (post-this-PR-merge) and confirm the new step categorizes correctly: 0 implementing, 0 awaiting-review (PRs all close-out’d), 0 stale (spawn-from-rust-path-opener worktree is on a non-feat/ branch and should not appear in the counter at all). Verify the dispatch still picks ready tasks correctly and the digest carries the new fields.
  7. Delete absorbed task. Once this task is implementation-ready and the spec is locked, delete docs/planning/tasks/2026-05-20-orchestrate-validates-worktree-task-status.md (handled in the consolidating commit on main; no separate PR needed since that file has no associated branch).
  • plugin/scripts/count_inflight_tasks.py (new) — categorizing in-flight counter.
  • sdlc.yaml — add orchestrator: block with max_implementations: 5 and max_awaiting_review: 20.
  • plugin/conventions/sdlc-yaml.md — document the new block shape and defaults.
  • plugin/skills/orchestrate/SKILL.md — restructure cap-check as Step 3, renumber dispatch to Step 4, drop the parallelism-cap preamble section.
  • plugin/skills/orchestrate/invariants.yaml — swap "parallelism cap of 2" for the new required phrases; add required_tool_refs for the script.
  • docs/planning/tasks/2026-05-20-orchestrate-validates-worktree-task-status.md — delete (consolidated here).
  • AC-1: plugin/scripts/count_inflight_tasks.py --json against a repo with a known fixture (one implementing worktree, one PR-open worktree, one closed/done stale worktree) emits exactly three JSON objects with the correct category for each.
  • AC-2: sdlc.yaml at the repo root contains an orchestrator: block with max_implementations: 5 and max_awaiting_review: 20. plugin/conventions/sdlc-yaml.md documents this block.
  • AC-3: Running /sdlc:orchestrate against a repo at 5 implementing tasks does NOT dispatch a 6th task-work; the digest line records caps-reached=max_implementations. (agent-manual smoke test with a contrived fixture or by raising the count manually.)
  • AC-4: Running /sdlc:orchestrate against a repo at 20 awaiting-review PRs still allows up to max_implementations new task-work dispatches; the digest line records caps-reached=max_awaiting_review as a warning, not a block. (agent-manual.)
  • AC-5: Stale entries (worktree exists, task file already closed/done) appear in the digest under a stale=<slugs> field and do NOT count against either limit. (agent-manual against a contrived stale fixture.)
  • AC-6: plugin/skills/orchestrate/SKILL.md contains no "parallelism cap" phrase; the ### Parallelism cap — 2 preamble section is gone. invariants.yaml no longer has "parallelism cap of 2" in required_phrases. lint_skill_prose.py plugin/skills/orchestrate/SKILL.md exits 0.
  • AC-7: docs/planning/tasks/2026-05-20-orchestrate-validates-worktree-task-status.md is removed in the same PR. PR description notes the consolidation.
  • Auto-cleanup of stale worktrees by the orchestrator. Stale entries surface in the digest; teardown stays a human-driven action (or a separate /sdlc:task-close-out invocation) for this PR. A follow-up could automate the stale-teardown loop.
  • Changing the default limit values after rollout. Defaults (5, 20) are picked here; tuning is a project-level config decision per sdlc.yaml.
  • A web UI / dashboard for in-flight visualization. The JSON output is the dashboard.
  • Caching the gh pr list calls across ticks. Each tick re-queries; PR state is cheap enough.
  • Renaming the orchestrate skill’s other steps (sync / reconcile / digest / notify / return). Only the cap-check extraction renames step numbers; the other steps’ content is unchanged beyond renumbering.
  • none (depends on plugin/scripts/, gh, sdlc.yaml, plugin/conventions/sdlc-yaml.md — all already shipped)

Surfaced 2026-05-20 in conversation while running /loop /sdlc:orchestrate through tick 6. User observed: with the current cap, “once two issues are PRed it will [be] stuck right?” The follow-up clarified that the orchestrator continues to reconcile PRs (Step 2 is uncapped) but can’t start new implementations (Step 3 is cap-gated). The conflation of “actively implementing” vs “PR awaiting review” turns the cap into a review-cadence throttle, which isn’t what the name suggests and isn’t well-tuned. User also asked to scope the cap explicitly to tasks (not general sub-agent count), pull the limits into sdlc.yaml config, and back the counting logic with a deterministic script — three threads that bundle naturally into one task. Consolidates 2026-05-20-orchestrate-validates-worktree-task-status (the stale-worktree concern, absorbed under the stale category).

Captured by /sdlc:task-work on 2026-05-20. PR: pending.

  • AC-1: agent-manual — built a contrived fixture in a fresh tmpdir (git init, three worktrees + matching feat/ branches + task files with in-progress, closed/done, planning/proposed statuses, plus one docs/ worktree to verify skip). Ran count_inflight_tasks.py --no-gh --format json. Result: exactly three JSON tasks[] entries — implementing, stale, other — and the non-feat/ worktree was correctly skipped. PR-bearing awaiting-review was verified by inspection of classify() (live gh against a fixture PR was out of scope; the test against the real repo will exercise the path once a PR opens).
  • AC-2: auto — sdlc.yaml now contains the orchestrator: block with max_implementations: 5 and max_awaiting_review: 20; plugin/conventions/sdlc-yaml.md carries the matching section. Verified by grep -n orchestrator sdlc.yaml plugin/conventions/sdlc-yaml.md.
  • AC-3: agent-manual — rewrote the tmpdir fixture’s sdlc.yaml to max_implementations: 1 while one implementing task was present. Counter reported caps_reached: [max_implementations] and the summary line carried caps-reached=max_implementations. The SKILL.md Step 4 prose explicitly gates dispatch on implementing >= max_implementations, so the orchestrator would not dispatch a 6th task-work in the live equivalent.
  • AC-4: agent-manual — same fixture, set max_awaiting_review: 0 to force the cap reached. Counter flagged the limit in caps_reached but Step 4’s gate is exclusively max_implementations, so dispatch logic remains unaffected — verified by reading Step 4 prose end-to-end. A positive end-to-end test (20 open PRs) was not contrived; the asymmetric gating is encoded both in the SKILL.md and pinned by invariants.yaml’s max_awaiting_review required-phrase against Step 3.
  • AC-5: agent-manual — the same fixture’s stale worktree (closed/done task, worktree present) appears in the tasks[] list with category: "stale" and does not contribute to either caps_reached entry under default limits. The summary line shows stale=1 independently of the cap state.
  • AC-6: auto — grep -ni "parallelism cap" against the new SKILL.md returns no matches; lint_skill_prose.py plugin/skills/orchestrate/SKILL.md exits 0 against the rewritten invariants.yaml. The ### Parallelism cap — 2 preamble section is gone; "parallelism cap of 2" is removed from required_phrases (and the broader "parallelism cap" is now in forbidden_phrases to prevent drift back).
  • AC-7: auto — docs/planning/tasks/2026-05-20-orchestrate-validates-worktree-task-status.md was deleted in the consolidating commit on main (51d5323 docs(tasks): consolidate orchestrator cap concerns into categorized-in-flight-limits) before this feat branch branched. Verified absent: ls returns ENOENT. The PR description notes this consolidation.
  • The PEP-723 self-bootstrapping shebang pattern made the new counter script immediately runnable without venv setup — copying the shape from audit_entities.py got it off the ground in one pass.
  • lint_skill_prose.py’s section: anchoring caught the renumber on the first run: pinning max_implementations and max_awaiting_review to section: "3." automatically verifies they live in the canonical home, not scattered across the file.
  • The contrived fixture approach (tmpdir + git init + bare branches
    • plain task files) needs zero gh mocking with --no-gh, and the script’s degraded-no-PR path makes that a meaningful test of the implementing vs awaiting-review boundary by construction (no PR → always implementing).
  • Initial file edits landed in /Users/sksizer/Developer/dev/plugin/conventions/sdlc-yaml.md (main repo) instead of the worktree path, because the sub-agent was given the plugin root as an absolute path that points to the main repo’s plugin checkout, not a per-worktree copy. Had to diff-and-revert in main, then re-apply via git apply inside the worktree. Future task-work briefings should call out explicitly that every edit must use the worktree-prefixed absolute path, and ideally Step 6’s sub-agent contract could include a one-line “edits outside the worktree root will be rejected” guard (mechanically: a pre-commit hook on main that refuses commits with the active-worktree’s basename in the path). → T-1CL4-worktree-scope-guard-pre-commit
  • The SKILL.md tells task-work to run ${CLAUDE_PLUGIN_ROOT}scripts/run_quality_checks.py but the brief told me to ignore that text and use the configured list directly. The two paths converge in practice (the executor reads sdlc.yaml), but the prose drift between the canonical SKILL.md and the per-project context is a smell. A /sdlc:task-work doc audit pass would catch cases where the canonical procedure isn’t actually how a project runs. → T-E69Y-skill-md-runtime-drift-audit
  • AC-4’s positive case (20 awaiting-review entries) was not end-to-end tested because contriving 20 fake PRs requires either a real gh instance or a gh mock; neither is part of the current test infrastructure. A --gh-fixture <json-file> flag on the counter would let future smoke tests inject PR state without touching GitHub. → T-BM56-pr-check-mock-state-flag (linked-existing; same shape, different script)
  • T-1CL4-worktree-scope-guard-pre-commit — created. Pre-commit hook on main that refuses edits to paths owned by an active worktree (carve-out for docs/planning/tasks/).
  • T-E69Y-skill-md-runtime-drift-audit — created. Audits SKILL.md shell-out invocations against each referenced script’s --help to catch documentation drift.
  • T-BM56-pr-check-mock-state-flag — linked-existing. Parallel concept (mock gh state) for the counter script’s awaiting-review end-to-end coverage; the existing task’s pattern can generalize when picked up.

← Back to Tasks