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.mddefines the cap in two places: a### Parallelism cap — 2preamble 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 matchingfeat/<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.yamlpins"parallelism cap of 2"as arequired_phraseagainst the preamble — locking the misleading name and the hard-coded literal.sdlc.yamlat the project root already carries per-project SDLC configuration (today:quality_checks:only).plugin/conventions/sdlc-yaml.mddocuments the shape. Noorchestrator: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>.mdfrontmatterstatus, worktree dir, local branch presence,gh pr listfor 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).
Proposed
Section titled “Proposed”-
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 againstfeat/<basename>branches, reads each task file’sstatus:frontmatter, and queriesgh 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(statusin-progress, no open PR),awaiting-review(statusin-progress, open PR exists),stale(status starts withclosed/— should have been torn down). -
sdlc.yamlgains anorchestrator:block:orchestrator:max_implementations: 5 # cap on simultaneous task-work sub-agentsmax_awaiting_review: 20 # cap on open PRs from task-work -
/sdlc:orchestrateSKILL.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 fromsdlc.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 — 2section deleted; its content folded into the new Step 3 prose. - Step 4 (dispatch) reads the script’s
implementingcount and the configuredmax_implementations; dispatches up tomax_implementations - implementing_countnew task-works. - The
awaiting-reviewlimit gates a separate signal: if at or abovemax_awaiting_review, the digest line recordsawaiting-review-cap-reachedso 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). staleentries 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.
- Step 3 becomes “Count in-flight” (extracts the cap-check as its own numbered step, shelling out
to
Approach
Section titled “Approach”- Convention update first. Extend
plugin/conventions/sdlc-yaml.mdwith theorchestrator: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. - 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). Readsdlc.yamlto 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 oneghcall per worktree). Idempotent and side-effect-free. - Update
/sdlc:orchestrateSKILL.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 tocount_inflight_tasks.py --json, parse the output, compare against configuredmax_implementationsfor dispatch (Step 4) andmax_awaiting_reviewfor the digest annotation. Delete the preamble### Parallelism cap — 2section; the new step is the only canonical location. - Update
plugin/skills/orchestrate/invariants.yaml. Remove"parallelism cap of 2"fromrequired_phrases. Add"in-flight limits"as a required phrase scoped to the new Step 3. Add arequired_tool_refs:entry forcount_inflight_tasks.pyso future skill drift away from the script trips the lint. - Update the digest format. The digest line gains two optional fields:
inflight={implementing,awaiting-review,stale}shorthand counts andcaps-reached=<list>when any limit gates. Backward-compatible append; existing parsers won’t break since fields are key=value. - Smoke test. Run
/sdlc:orchestrateagainst 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. - 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).
Files to touch
Section titled “Files to touch”plugin/scripts/count_inflight_tasks.py(new)— categorizing in-flight counter.sdlc.yaml— addorchestrator:block withmax_implementations: 5andmax_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; addrequired_tool_refsfor the script.docs/planning/tasks/2026-05-20-orchestrate-validates-worktree-task-status.md— delete (consolidated here).
Acceptance criteria
Section titled “Acceptance criteria”- AC-1:
plugin/scripts/count_inflight_tasks.py --jsonagainst 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 correctcategoryfor each. - AC-2:
sdlc.yamlat the repo root contains anorchestrator:block withmax_implementations: 5andmax_awaiting_review: 20.plugin/conventions/sdlc-yaml.mddocuments this block. - AC-3: Running
/sdlc:orchestrateagainst a repo at 5 implementing tasks does NOT dispatch a 6th task-work; the digest line recordscaps-reached=max_implementations. (agent-manual smoke test with a contrived fixture or by raising the count manually.) - AC-4: Running
/sdlc:orchestrateagainst a repo at 20 awaiting-review PRs still allows up tomax_implementationsnew task-work dispatches; the digest line recordscaps-reached=max_awaiting_reviewas a warning, not a block. (agent-manual.) - AC-5: Stale entries (worktree exists, task file already
closed/done) appear in the digest under astale=<slugs>field and do NOT count against either limit. (agent-manual against a contrived stale fixture.) - AC-6:
plugin/skills/orchestrate/SKILL.mdcontains no"parallelism cap"phrase; the### Parallelism cap — 2preamble section is gone.invariants.yamlno longer has"parallelism cap of 2"inrequired_phrases.lint_skill_prose.py plugin/skills/orchestrate/SKILL.mdexits 0. - AC-7:
docs/planning/tasks/2026-05-20-orchestrate-validates-worktree-task-status.mdis removed in the same PR. PR description notes the consolidation.
Out of scope
Section titled “Out of scope”- 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-outinvocation) 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 listcalls 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.
Dependencies
Section titled “Dependencies”- none (depends on
plugin/scripts/,gh,sdlc.yaml,plugin/conventions/sdlc-yaml.md— all already shipped)
Discovery context
Section titled “Discovery context”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).
Post-mortem
Section titled “Post-mortem”Captured by /sdlc:task-work on 2026-05-20. PR: pending.
Acceptance criteria coverage
Section titled “Acceptance criteria coverage”- AC-1: agent-manual — built a contrived fixture in a fresh tmpdir
(
git init, three worktrees + matchingfeat/branches + task files within-progress,closed/done,planning/proposedstatuses, plus onedocs/worktree to verify skip). Rancount_inflight_tasks.py --no-gh --format json. Result: exactly three JSONtasks[]entries —implementing,stale,other— and the non-feat/worktree was correctly skipped. PR-bearingawaiting-reviewwas verified by inspection ofclassify()(liveghagainst 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.yamlnow contains theorchestrator:block withmax_implementations: 5andmax_awaiting_review: 20;plugin/conventions/sdlc-yaml.mdcarries the matching section. Verified bygrep -n orchestrator sdlc.yaml plugin/conventions/sdlc-yaml.md. - AC-3: agent-manual — rewrote the tmpdir fixture’s
sdlc.yamltomax_implementations: 1while oneimplementingtask was present. Counter reportedcaps_reached: [max_implementations]and the summary line carriedcaps-reached=max_implementations. The SKILL.md Step 4 prose explicitly gates dispatch onimplementing >= 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: 0to force the cap reached. Counter flagged the limit incaps_reachedbut Step 4’s gate is exclusivelymax_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 byinvariants.yaml’smax_awaiting_reviewrequired-phrase against Step 3. - AC-5: agent-manual — the same fixture’s
staleworktree (closed/donetask, worktree present) appears in thetasks[]list withcategory: "stale"and does not contribute to eithercaps_reachedentry under default limits. The summary line showsstale=1independently 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.mdexits 0 against the rewritteninvariants.yaml. The### Parallelism cap — 2preamble section is gone;"parallelism cap of 2"is removed fromrequired_phrases(and the broader"parallelism cap"is now inforbidden_phrasesto prevent drift back). - AC-7: auto —
docs/planning/tasks/2026-05-20-orchestrate-validates-worktree-task-status.mdwas 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:lsreturns ENOENT. The PR description notes this consolidation.
What worked
Section titled “What worked”- The PEP-723 self-bootstrapping shebang pattern made the new
counter script immediately runnable without venv setup — copying
the shape from
audit_entities.pygot it off the ground in one pass. lint_skill_prose.py’ssection:anchoring caught the renumber on the first run: pinningmax_implementationsandmax_awaiting_reviewtosection: "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
ghmocking with--no-gh, and the script’s degraded-no-PR path makes that a meaningful test of theimplementingvsawaiting-reviewboundary by construction (no PR → alwaysimplementing).
- plain task files) needs zero
Friction and automation gaps
Section titled “Friction and automation gaps”- 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 viagit applyinside 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.pybut the brief told me to ignore that text and use the configured list directly. The two paths converge in practice (the executor readssdlc.yaml), but the prose drift between the canonical SKILL.md and the per-project context is a smell. A/sdlc:task-workdoc 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
ghinstance or aghmock; 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)
Spawned follow-up tasks
Section titled “Spawned follow-up tasks”- 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
--helpto catch documentation drift. - T-BM56-pr-check-mock-state-flag — linked-existing.
Parallel concept (mock
ghstate) for the counter script’sawaiting-reviewend-to-end coverage; the existing task’s pattern can generalize when picked up.