Skip to content

[P2] Isolate parse_prd tasks per-PRD in a dedicated tag (stop polluting the active master tag) #13

@anombyte93

Description

@anombyte93

Problem

parse_prd pollutes whatever TaskMaster tag is currently active (in practice master). When an effort runs parse-prd and the active tag already holds tasks from a prior effort (e.g. 6 done /inevitable tasks under master), the new PRD's tasks are silently appended alongside them — no isolation, no warning, no per-PRD boundary. Each PRD should get its own dedicated tag so its task graph is isolated from every other effort.

Current Behavior

The parse step lives in skill prose, not in any Python tool of this plugin:

  • skills/generate/SKILL.md "Step 4: Parse tasks via TaskMaster" (lines 189–206) instructs the agent to call, in priority order:
    • mcp__task-master-ai__parse_prd(input=".taskmaster/docs/prd.md", numTasks=<n>) (line 199)
    • mcp__plugin_atlas-go_go__tm_parse_prd(input_path=..., num_tasks=<n>) (line 201)
    • CLI task-master parse-prd --input ... --num-tasks <n> (line 203)
  • All three invocations are tagless. task-master therefore writes into whatever tag is set as currentTag in .taskmaster/state.json (master in the observed run). A repo-wide grep across skills/ for tag-creation/selection language (add-tag, use-tag, --tag, "dedicated tag", "per-PRD", "isolate", "fresh tag") returns ZERO matches — the skill never creates, selects, or warns about a tag before parsing.
  • Step 6 expansion (skills/generate/SKILL.md lines 232–311) shells out to task-master expand --all, which expands every task in the active tag (same tag-blindness). The coverage-verify snippet (lines 287–310) hard-codes the master tag.
  • mcp-server/pipeline.py preflight() (lines 179–234) already reads state.json + tasks.json, computes per-tag tag_counts (line 190), resolves current_tag via _current_tag() (lines 158–164, falling back to currentTag then "master"), and can emit recommended_action="select_taskmaster_tag" with a recommended_tag (lines 211–213). But the parse rule (lines 205–206: elif prd_exists and tasks_count == 0: rec = "parse_prd") only checks the current tag's count being 0 — it never flags that master already holds unrelated tasks before recommending a parse that will append into it.
  • skills/go/SKILL.md (lines 22–31) routes purely on current_phase and discards recommended_action/current_tag/recommended_tag/tag_counts entirely.
  • mcp-server/server.py registers 18 tools (verified @mcp.tool() count = 18); none is parse_prd, tm_parse_prd, expand_tasks, add_tag, or use_tag — confirming the parse/tag behavior is owned by external task-master-ai, not this codebase.
  • mcp-server/taskmaster.py is the only TaskMaster CLI wrapper and does only init (no tag operations).

Note on the two root causes (independent): A separate observed parse_prd exit-1 failure is the claude-code nested-spawn problem (a child claude -p spawn dies inside a nested Claude Code session). That is real but orthogonal — the tag-pollution bug manifests on any successful parse, because the parse invocation carries no tag and writes to the active currentTag. This issue fixes the pollution; the spawn failure is tracked separately.

Expected Behavior

Each PRD's tasks are parsed into a dedicated, fresh, per-PRD tag, leaving every other tag (especially master and prior efforts) physically untouched. Concretely:

  1. Before parsing, the generate flow derives a deterministic slugified per-PRD tag (e.g. site-funnel, inevitable), creates it (idempotent), and selects it.
  2. parse_prd/parse-prd writes into that fresh tag. Because the tag is empty, no --append is needed and master is never modified.
  3. expand --all and the coverage-verify snippet operate on that same dedicated tag (not hard-coded master).
  4. preflight() detects a non-empty/foreign active tag and recommends a fresh dedicated tag instead of silently recommending a parse into a polluted master; the orchestrator consumes that recommendation.

Files to Touch

  • skills/generate/SKILL.mdPRIMARY. In "Step 4: Parse tasks via TaskMaster" (lines 189–206), insert a create+select dedicated-tag step before line 199; thread tag=<slug> into the parse invocations at lines 199/201/203. In Step 6 (lines 232–311), run use-tag <slug> before expand --all, and repoint the master-hard-coded coverage-verify snippet (lines 287–310) at <slug>.
  • mcp-server/pipeline.pySECONDARY. In preflight() make the parse_prd recommendation (lines 205–206) tag-aware: detect a non-empty/foreign active tag (current_counts["total"] > 0) and return a fresh dedicated recommended_tag (or a warn flag) instead of silently recommending parse into master.
  • skills/go/SKILL.mdCONTEXT. Flow (lines 22–31) routes only on current_phase; if preflight starts recommending a dedicated tag, go (or generate) must actually consume recommended_action/recommended_tag, which is dropped today.
  • mcp-server/taskmaster.pyOPTIONAL. If tag selection should be a first-class engine tool, add thin add_tag/use_tag wrappers here mirroring init_taskmaster's subprocess pattern, then register them in mcp-server/server.py.

Researched Fix Approaches

1. [Recommended] — Parse directly into a fresh per-PRD tag (tag=<slug> + auto-create) (confidence: 92%)

  • Library/Config: task-master-ai (claude-task-master). MCP parse_prd param tag ("Tag context to operate on"); CLI flag --tag=<name>. Tag mgmt: MCP add_tag/use_tag or CLI task-master add-tag <name> / use-tag <name>.
  • Pattern: Derive a deterministic slug from the PRD filename/effort title (e.g. site-funnel, inevitable). Sequence: (1) add-tag <slug> (idempotent — treat "already exists" as success); (2) use-tag <slug> (sets state.json.currentTag so list/expand/next operate in-tag); (3) parse into it; (4) expand --all in it.
  • Why: Verified against the live MCP tool schema and TaskMaster's command-reference — parse_prd accepts a first-class tag parameter, and add-tag/use-tag are the documented commands for isolated parallel contexts. A fresh tag is empty, so parse writes into {<slug>:{tasks:[...],metadata:{...}}} and prior tasks under master are physically untouched. Change site matches the code map exactly (insert before skills/generate/SKILL.md line 199; thread tag into 199/201/203 and the Step 6 expand+verify).
  • Risk: (1) The claude-code nested-spawn parse_prd exit-1 is orthogonal and still kills parse inside a nested Claude Code session — tag isolation does not fix the spawn death. (2) parse-prd defaults to overwriting the target tag's task list (the --append flag exists specifically to add instead of overwrite, per claude-task-master issue #207). Guard: if the target slug tag is already non-empty, suffix the slug or require explicit append/force. (3) expand --all has no documented --tag flag — use-tag <slug> FIRST, then expand --all operates on currentTag.
  • Implementation hint:
# MCP
mcp__task-master-ai__add_tag(name="site-funnel", description="Site funnel PRD", projectRoot="<abs>")
mcp__task-master-ai__use_tag(name="site-funnel", projectRoot="<abs>")
mcp__task-master-ai__parse_prd(input=".taskmaster/docs/prd.md", numTasks=<n>, tag="site-funnel", projectRoot="<abs>")
# CLI
task-master add-tag site-funnel --description="Site funnel PRD"
task-master use-tag site-funnel
task-master parse-prd --input .taskmaster/docs/prd.md --num-tasks <n> --tag=site-funnel
task-master expand --all

2. [Alternative] — Preflight guard: detect a polluted/foreign active tag, recommend parse_into_fresh_tag (confidence: 80%)

  • Library/Config: No external API — pure Python in mcp-server/pipeline.py (already imports state.json + tasks.json, computes tag_counts/_current_tag/_recommended_pending_tag). Pairs with Approach 1's calls in the skill.
  • Pattern: Stop steering parse into a non-empty master. Before the elif prd_exists and tasks_count == 0 branch (lines 205–206), add: when prd_exists and the active tag is non-empty and all-done/foreign, return recommended_action="parse_into_fresh_tag" with recommended_tag=<slugified PRD name> instead of parse_prd (which appends/overwrites master) or select_taskmaster_tag (which only points at an EXISTING pending tag). Then teach skills/go (or generate) to consume recommended_tag.
  • Why: preflight() already has ~90% of the machinery (per-tag counts, current_tag resolution, a select_taskmaster_tag branch, a recommended_tag return field). This is the deterministic, testable home for "detect a non-empty/foreign tag before appending," making isolation a property of the orchestrator rather than relying on the LLM to remember.
  • Risk: Inert unless a consumer is wired — go/generate currently drop recommended_action/recommended_tag, so without a matching skill edit it is dead code. Foreign-vs-same-PRD detection is heuristic; safest rule is "active tag non-empty AND (name == 'master' OR all tasks done) ⇒ recommend fresh tag." Over-eager detection could spuriously fork tags on a legitimate resume.
  • Implementation hint:
# in preflight(), before the `elif prd_exists and tasks_count == 0` branch:
polluted = current_counts["total"] > 0 and current_counts["pending"] == 0
if prd_exists and polluted:
    rec = "parse_into_fresh_tag"
    recommended_tag = _slug_from_prd()   # deterministic slug from PRD filename/title
# return recommended_tag alongside existing fields

3. [Fallback] — Thin MCP/CLI tag wrapper in taskmaster.py (add_tag/use_tag/ensure_prd_tag) exposed via server.py (confidence: 68%)

  • Library/Config: Wraps task-master add-tag <name> [--description=...] and task-master use-tag <name> in mcp-server/taskmaster.py (the only existing TM CLI wrapper, currently init-only). Register the new tools in mcp-server/server.py.
  • Pattern: Add add_tag(name, description, project_root) and use_tag(name, project_root) plus a convenience ensure_prd_tag(prd_path) that slugifies+creates+selects, mirroring init_taskmaster's _build_env/_find_binary subprocess pattern. The generate skill then calls one deterministic engine tool before parse instead of three-way priority-ordered prose the agent can skip (grep proved the prose path is empirically skippable — 0 hits).
  • Why: Makes per-PRD isolation a guaranteed idempotent step owned by code, not an instruction the model may ignore. Centralizes slug derivation and the "tag already non-empty?" guard in one tested place. Bonus: add-tag/use-tag invoke no AI model (pure local tasks.json/state.json mutations), so they run fine even inside the nested Claude Code session that breaks parse_prd.
  • Risk: Most invasive (new tools + server.py registration + tests) for a problem Approach 1 solves with a few skill-line edits; duplicates task-master's own CLI surface. Lower confidence only because it is heavier than needed, not because the API is uncertain.
  • Implementation hint:
# taskmaster.py — mirror init_taskmaster's subprocess + _build_env path
def add_tag(name, description=None, *, project_root):
    args = ["add-tag", name]
    if description: args.append(f"--description={description}")
    # run via same subprocess+_build_env path; treat 'already exists' stderr as success
def use_tag(name, *, project_root):
    # run ["use-tag", name]
# then register both in server.py next to init_taskmaster

Reference

task-master-ai (claude-task-master) ships a first-class "Tagged Task Lists" system precisely for this isolation problem. The canonical per-feature workflow in its own tutorial/command-reference is: task-master add-tag <feature> --description=... (or add-tag --from-branch to name the tag after the git branch) → task-master use-tag <feature> → parse/expand within that tag; optionally delete-tag <feature> after merge. Tags give complete isolation: every operation (list/show/add/expand/next) acts only on the active tag (state.json.currentTag), and tasks.json is grouped as {<tag>:{tasks:[...],metadata:{...}}}. Critically, parse-prd defaults to overwriting the target tag's task list — the --append flag exists specifically to add instead of overwrite (GitHub issue #207) — so parsing into a FRESH empty tag is the clean pattern and needs no --append. The MCP parse_prd tool exposes a tag parameter alongside append/force/numTasks/input/output/research/projectRoot, and add_tag/use_tag are dedicated MCP tools (params: name, description, copyFromCurrent, copyFromTag, fromBranch, projectRoot). The Atlas engine currently bypasses all of this: its generate skill calls parse tagless, so TM writes into whatever currentTag is set (master).

Sources:

Acceptance Criteria

  • skills/generate/SKILL.md Step 4 contains a create+select dedicated-tag step (add-tag <slug> / use-tag <slug> or the MCP add_tag/use_tag equivalents) BEFORE the first parse invocation; a repo-wide grep across skills/ for add-tag|use-tag|--tag|tag= now returns ≥1 match (currently 0).
  • All parse invocations in skills/generate/SKILL.md (lines ~199/201/203) carry an explicit per-PRD tag (--tag=<slug> for CLI, tag="<slug>" for MCP).
  • Given a .taskmaster/state.json with currentTag="master" and a tasks.json where master holds N done tasks, running the generate parse flow leaves the master tag's task array unchanged (N tasks, byte-identical) and writes the new PRD's tasks under a separate <slug> key in tasks.json.
  • The slug is deterministic for a given PRD (same PRD filename/title ⇒ same slug across runs) and collision-safe (if <slug> is already non-empty, the flow either suffixes the slug or uses explicit append/force rather than silently overwriting).
  • Step 6's coverage-verify snippet in skills/generate/SKILL.md targets the dedicated <slug> tag, not a hard-coded master.
  • mcp-server/pipeline.py preflight() returns recommended_action != "parse_prd" (e.g. "parse_into_fresh_tag" with a populated recommended_tag) when fed a state.json {currentTag:"master"} + tasks.json {master:{tasks:[6 done]}} and a PRD present; a unit test in the plugin's test suite asserts this.
  • parse_prd succeeds and writes tasks to a fresh tag when invoked from inside a Claude Code / MCP session (i.e. the chosen approach is demonstrated end-to-end producing tasks under <slug> and an unmodified master); if the claude-code nested-spawn exit-1 blocks this, it is documented as the orthogonal failure and a non-model path — add-tag/use-tag selection — is shown to still run in-session.
  • If preflight is taught to recommend a dedicated tag, skills/go/SKILL.md (or skills/generate/SKILL.md) consumes recommended_action/recommended_tag rather than discarding it — verified by tracing the recommendation to an actual add-tag/use-tag/parse call.

Complexity: M

Trust Level: HINT (not specification)

The researched approaches above are starting points. Before implementing:

  1. Verify the library/config exists as stated (e.g. task-master models, confirm parse_prd's tag param and add-tag/use-tag via task-master --help or the MCP schema; read .taskmaster/config.json).
  2. Check that imports/keys match reality (slug derivation source, state.json currentTag semantics, tasks.json tag-grouped shape).
  3. Try the recommended approach — if it works in 1-2 attempts, use it.
  4. If it fails, do NOT keep retrying — research why, explore alternatives.
  5. The acceptance criteria are the real spec, not the approach.

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething isn't workingdxDeveloper experienceenhancementNew feature or requestpreflightPreflight / setup validation

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions