agentleFS
phahadekphahadek/claude-orchestrator/skills/groom/SKILL.md

groom

Skill1 starsChanged 3 months agoMITView on GitHub

Run a Backlog Grooming session for a milestone. Loads full project context deterministically (via the backend's GET /api/groom-context route), explores the code regions tasks touch with a git-fresh cache, presents πŸ”² Backlog tasks in batches for human sign-off, and marks them πŸ—‚οΈ Ready. Use when the user says "groom", "grooming session", "let's groom milestone X", "bring the backlog to Ready", or starts a Backlog Grooming session. Requires a grooming manifest in the central config tree (config/projects/<dir>/grooming.json).

8 other projects use a version of this.

Adopted by

Most recently taken up by

The file

Show its rules and text

We split this skill into the rules it gives an agent

18 rules. 0 are common, 8 are distinctive, and 10 are very specific to this project.

  • 0

    Common

    Most of the 7 edited versions we read keep these.

  • 8

    Distinctive

    This author's own choices. Worth a look.

  • 10

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Very specific:

    Read the repo's grooming manifest from the central config tree β€” <config>/projects/<repo-dir>/grooming.json (the loader resolves the config …

    line 31
  2. Distinctive:

    Never improvise a board id β€” copy it from the board's Notion URL / context.md.

    line 46
  3. Very specific:

    Read the non-architecture context-page bodies from contextPages[].markdown (project context, product design doc, dev setup, future scope β€” …

    line 131
  4. Very specific:

    Read each target task's architecture from targetTasks[].archUnits, dual-read per archSource:

    line 137
  5. Distinctive:

    Keep the package reads in subagents so the main window stays small (the procedure must survive β€” that is the original failure mode).

    line 208
  6. Very specific:

    Follow reference/presentation.md.

    line 223
  7. Distinctive:

    Do not batch feedback. For each item: stage the write β†’ confirm the change in chat β†’ continue.

    line 254
  8. Distinctive:

    If it's a genuine architectural decision you can't resolve, route it (file a πŸ“ Design task + hard-block edge), per Β§ Cite-or-route.

    line 261
  9. Very specific:

    If grooming reveals a design choice that needs human judgment, present options with pros/cons and stop β€” do not pick unilaterally.

    line 268
  10. Distinctive:

    When grooming surfaces body content that is wrong or misleading (a bad symbol, a stale anchor, a self-contradiction), correct it β€” the …

    line 343
  11. Distinctive:

    Only after explicit sign-off on the batch (_"looks good"_, _"ship it"_, _"next"_):

    line 353
  12. Distinctive:

    Gate accretion (πŸ’» Code tasks): Before flipping to Ready, mint the task's stripped runtime/launch-and-observe items onto the milestone gate …

    line 397
  13. Very specific:

    Before flipping to Ready, classify each candidate line the pre-groom body's ## Operational seed section carries as operational-seed (a …

    line 432
  14. Very specific:

    Declared writes validation (πŸ”§ Operational tasks): Before flipping a πŸ”§ Operational task to Ready, check its body for a ## Declared writes …

    line 473
  15. Very specific:

    Do not invent a classification tag the author didn't write: an entry with no tag, or an ambiguous one, is left as-is (it still promotes β€” …

    line 482
  16. Very specific:

    After conversational sign-off in this session, apply each staged intent with node ~/.claude/scripts/staged-intents-client.mjs apply …

    line 501
  17. Very specific:

    If the task has hard-block deps (or had any and now has none), stage a task.setDependsOn intent: {"taskId": "<id>", "dependsOn": ["<id>", …

    line 508
  18. Distinctive:

    Scope is the target milestone only: Do not modify tasks on other boards unless a dependency issue is explicitly identified and the human …

    line 600

β€œCommon” is measured: kept by at least 6 in 10 of the 7 edited versions of this file we could read. β€œVery specific” is our guess from the words: it names a file, a command, a setting or the project itself.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

Backlog Grooming

Grooming brings πŸ”² Backlog tasks up to πŸ—‚οΈ Ready: every open question resolved or explicitly owned, scope verified against the actual code, tests and manual-gate items enumerated β€” then the human signs off, batch by batch.

This skill exists because the procedure is load-bearing but routinely skipped: under context pressure a single agent compresses the expensive Step-1 load and "just decides." Here the deterministic parts are done by a script (nothing to skip) and the judgment parts are gated on human sign-off (nothing to self-grant).

Read reference/anti-patterns.md now β€” those failure modes are the whole reason this skill exists. Read reference/presentation.md before Step 2.


Step 0 β€” Resolve manifest & mode

  1. Read the repo's grooming manifest from the central config tree β€” <config>/projects/<repo-dir>/grooming.json (the loader resolves the config root via $ORCHESTRATOR_CONFIG_DIR / --config-dir / a host-aware default; <repo-dir> is the repo basename, override with --project). If it is missing, stop and tell the human (point them at reference/manifest.example.json). Do not improvise.
  2. Note architectural_control:
    • full β†’ this skill's default behavior (decide-and-rewrite within the rules).
    • low β†’ read reference/low-control.md and follow the investigate-propose-route adaptation instead. The groomer does not own the architecture; resolution biases to proposing and routing to owners.
  3. Determine the milestone (from the user, e.g. "groom M9"). If it isn't yet registered in manifest.milestones (routine right after a new milestone board is created), the loader no longer dead-ends β€” it prints a copy-pasteable entry with the neighbour auto-filled. Add that entry to the manifest, or pass --board <data-source-id> to run immediately and persist the printed snippet afterward. Never improvise a board id β€” copy it from the board's Notion URL / context.md.
  4. Determine mode from the cache dir .skill-cache/grooming/<milestone>/:
    • absent β†’ fresh groom.
    • present β†’ resume: the loader reuses fresh digests and re-explores only what changed. Same procedure either way.

Grooming runs interactively in the main repo on dev β€” never in an ephemeral implementation worktree (the cache must persist). If you find yourself on a feature branch in a worktree, stop and tell the human.


Step 1 β€” Deterministic load (the backend route, not you)

Load through the backend, not by hand-fetching context pages or task bodies yourself β€” that is exactly the step that gets skipped. Do not shell out to Notion directly (no notion-query.mjs / notion-page.mjs, no vendored groom-load.mjs) β€” the backend's GET /api/groom-context route (see packages/backend/src/routes/groomContext.ts) is the sole loader now; it wraps the same loadGroomContext() the dashboard's grooming panel calls, so the skill and the panel always see identical context.

node ~/.claude/scripts/groom-context-client.mjs --milestone <M> [--project <project-id>]

groom-context-client.mjs is the vendored sanctioned node client (curl/wget are off the auto-dispatch allowlist, node is not β€” re-vendored via the /sync-guidelines skill), which calls the loopback, device-authed route with $ORCHESTRATOR_DEVICE_TOKEN (host/port default to 127.0.0.1:3000, overridable via $ORCHESTRATOR_BACKEND_HOST / $ORCHESTRATOR_BACKEND_PORT) and prints the GroomLoadResult bundle as JSON on stdout. If it exits non-zero, stop β€” a partial load means a contaminated groom. Report the error.

The bundle has:

  • contextPages β€” the context pages ({id, title, markdown}), fetched fresh every load.
  • board β€” every non-Done target task ({id, title, status, type, priority, url}), and neighbourBoards β€” the same shape for neighbour milestones (context-only).
  • targetTasks β€” the target board's tasks, each carrying its parsed filesSection / rawMarkdown, plus already-computed judgment seeds: readinessViolations (the shared readiness gate run ahead of time), sizeCheckSeed ({files, loc_method} β€” a deterministic size-check seed), typeCheck (the type/content-mismatch scan), regions (resolved package/file scope), and its dual-read architecture (archSource + archUnits β€” see below).
  • codeWorklist β€” per-package deduped file paths declared across target task bodies (object form: package path β†’ file list).
  • gitFreshness β€” per-package freshness (fresh / stale / missing) vs. the local integration branch.
  • dependencyCandidates β€” per-task dependency candidates (region overlap βˆͺ declared Depends On) for the groomer to confirm β€” never auto-wired.

The bundle does not carry milestone_gate_task_id / milestone_seed_task_id directly β€” derive them from board exactly as the old loader did: the single row with type === '🚦 Gate' (null if absent), and the single πŸ”§ Operational row whose title contains "config-seed" case-insensitively (null if zero or ambiguous if more than one match β€” surface either case rather than guessing). Record both in context-bundle.json for the rest of the session to read.

Persist this session's working state locally exactly as before β€” the route is a pure read, so the skill still owns the on-disk cache under .skill-cache/grooming/<milestone>/: write context-bundle.json (context pages

  • board + neighbours + target tasks + the derived gate/seed task ids) and worklist.json (codeWorklist + gitFreshness) from the bundle with the Write tool, and seed/merge grooming-state.json (per-Backlog-task skeleton, preserved across resumes β€” same merge rule as before: carry forward human-entered fields, always refresh status/title/type from the live bundle, prune entries whose task is no longer a live non-Done target) with the Edit tool. This is what makes resume mode work and is unchanged from before the cutover β€” only the source of the bundle moved from a shell-out to the route.

grooming-state.json is FLAT, keyed by task id β€” and every entry must carry project, milestone, title. The shape is a top-level map { "<taskId>": { … } } (not a nested { milestone, project, tasks: { … } } wrapper) β€” groom-flip-client.mjs reads state[taskId] directly and requires project, milestone, title on that entry (plus hard_block_deps, size_check, type_check, gate_contribution, seed_contribution at flip time). Stamp project, milestone, title on every entry when you seed it β€” including tasks that arrive in a later wave and get merged in mid-session (a wave-added entry seeded without them fails the flip at payload-build with missing required field(s): title, project, milestone).

Read the non-architecture context-page bodies from contextPages[].markdown (project context, product design doc, dev setup, future scope β€” always populated, regardless of archSource). Also read the universal task-authoring standard at config/task-writing.md (it is not a Notion context page β€” the skill reads it from local disk).

Read each target task's architecture from targetTasks[].archUnits, dual-read per archSource:

  • archSource: 'notion' β€” the milestone's fixed Notion architecture pages (Technical Architecture, Coding Guidelines), each {id, title} here and its body available via contextPages[].markdown.
  • archSource: 'store' β€” archUnits[] carries region-intersected arch_unit store units (plus every active invariant), each already {id, title, body} β€” the body is inlined directly in the bundle, no extra fetch needed. If a unit's body is ever absent, dereference it with the mcp__orchestrator__architecture_getUnit tool ({id}), or broaden the selection with mcp__orchestrator__architecture_queryUnits.

This is non-negotiable: resolving a task without the architectural constraints loaded is how grooming produces confidently-wrong decisions.

Disposition the constraints β€” don't just "read the pages." The loader surfaces each task's binding constraints (targetTasks[].bindingConstraints β€” the CONSTRAINT_CATALOG Γ— regions intersection). Before a task can promote, disposition every one of its binding constraints as one of complies / n-a (+ a mandatory why) / conflictβ†’route (naming the routed πŸ“ Design task) β€” recorded in grooming-state.json as constraints_dispositioned and re-derived + enforced server-side (groomGate.ts, FM1). The mandatory architecture read is the disposition, not a page-turn: a constraint you didn't disposition is a constraint you didn't check.


Step 1b β€” Explore the code (cached, judgment where needed)

Grooming decisions made from task bodies alone routinely miss real gaps. Read the code the tasks touch β€” but only once per region, and only what changed.

Per package in worklist.json:

  • fresh β†’ reuse the digest already in code-map.json. Do not re-read.
  • stale or missing β†’ dispatch one Explore subagent scoped to that package. Have it return a structured digest: public surface (classes/functions/signatures another task would import), the conventions in play, and anything that would invalidate a Backlog task's stated assumption. Write the digest to code-map.json keyed by the package path, stamped with worklist.baseline_sha:
    {
      "src/polimarket_analyser/<pkg>": {
        "head_sha": "<baseline_sha>",
        "digest": "...",
        "explored_by_task": ["<id>"],
        "ts": "<iso>"
      }
    }
    (The loader reads code-map.json for freshness; the skill owns writing it.)

Write the cache/state files with the Edit/Write tool β€” never a shell script. code-map.json, grooming-state.json (and the design skill's design-state.json) are ordinary on-disk JSON the loader already seeded. Mutate them with the Edit tool (a unique-string change) or Read + Write the whole file (a structural change). Never write a throwaway script and run it (node _foo.cjs then rm), and never shell out (echo >, cat >, a cd … && … chain). The script route does in three permission-prompting Bash calls (node, rm, the cd && chain) what the Edit tool does in one auto-approved call β€” it is the cause of the constant permission friction, not a workaround for it.

For each task in worklist.unresolved_tasks (declared no resolvable path — common, because real tasks reference code by identifier or prose, not a Files / paths affected section): this is the judgment half the loader deliberately left to you. Read the task body, extract the named symbols / analyzers / tables it mentions, grep the repo to find the real file(s), map to a package, and explore that package as above. If a stable area→package mapping emerges, propose adding it to manifest.area_aliases so the loader resolves it next time — confirm before editing.

Keep the package reads in subagents so the main window stays small (the procedure must survive β€” that is the original failure mode).

Fan out, and falsify the first read. For a milestone of any size, dispatching one read-only Explore agent per stale/missing package (and per unresolved task) in parallel is the default, not a heavyweight option β€” it earns its keep. And falsify before asserting: a first read of "this is already done" / "this is subsumed by X" / "the body's anchor is right" is exactly where grooming goes wrong. A real run caught an "already-done" that was a live ordering bug, a "subsumed" that was a real residual, an internal body contradiction, and a wrong anchor β€” each would have produced bad grooming if trusted. Confirm the claim against the code before you groom on it.


Step 2 β€” Present πŸ”² Backlog in batches

Follow reference/presentation.md. In short:

  • Only πŸ”² Backlog tasks need decisions. Tasks at any other status are shown as context only (name + status), never promoted.
  • Batch by dependency cluster (sequencing comes from the depends-on chain β€” do not invent external sequencing labels). One batch per message.
  • For each Backlog task, give the 4-point summary: what it achieves / open questions (write None. if clean) / automated tests / manual verification. Ground every claim in the code-map digest and the context pages β€” not the task body alone.
  • End the batch with: "Any changes or questions before I mark these Ready and continue?"
  • One batch at a time. Never present the next before the current is signed off.

Presentation is type-dependent β€” a mixed milestone runs both flows:

  • πŸ’» Code (auto-dispatched) β†’ the per-batch, per-task flow above: each task gets its 4-point summary and its own human decision (a wrong Ready launches an unattended worktree, so the stakes demand it).
  • πŸ“ Design / πŸ“‹ Planning (interactive, picked up by /design) β†’ approve-by-standard: present ONE consolidated triage, not per-item asks β€” a clean list (names-only), blocked rows (+ the blocking dep), and needs-attention rows (+ the reason). The human engages only the needs-attention + blocked rows; the clean set promotes by default (visible + veto-able) β€” no per-item or bulk positive stamp. The clean-verdict standard, its taxonomy, and the deterministic floor that can only ever downgrade a proposed clean verdict live in reference/presentation.md Β§ Consolidated triage.

Step 3 β€” Incorporate feedback (one item at a time)

Do not batch feedback. For each item: stage the write β†’ confirm the change in chat β†’ continue.

  • Clarification / correction β†’ patch the affected section (see Β§ Editing a task body below), confirm.
  • New open question β†’ while grooming, resolve it into the spec, don't add a standing ## Open Questions heading to a πŸ’» Code task (a live OQ heading blocks promotion β€” Tier-1). If it's a genuine architectural decision you can't resolve, route it (file a πŸ“ Design task + hard-block edge), per Β§ Cite-or-route.
  • Missing task β†’ draft it inline for review first; stage a task.create intent only after the human okays it.
  • Missing prerequisite (a sibling ingestion/storage/analyzer task the scope doesn't cover) β†’ propose a separate new task. Never silently widen the existing scope.

If grooming reveals a design choice that needs human judgment, present options with pros/cons and stop β€” do not pick unilaterally. Load-bearing constraints from the architecture pages win over task wording (see reference/anti-patterns.md).

Editing a task body β€” task.patchBodySection first, task.updateBody as fallback

Most groom-time body edits touch one section β€” clearing a resolved ## Open Questions, pinning a hedged ## Files / paths affected line, adding a missing Files section. Do these as a targeted, non-lossy patch, not a whole-body rewrite. task.updateBody is whole-body-replace: it forces you to reconstruct Summary + Context-as-BlockModel[] + criteria + Files from scratch β€” slow, and a single bad reconstruction silently drops content. task.patchBodySection edits one section in place.

Stage it like any other intent (staged-intents-client.mjs create task.patchBodySection <payload> <projectId> <groupId>). Payload is a discriminated union on operation, keyed by the exact section heading text:

  • Clear a resolved Open Questions block β†’ {"taskId","section":"Open Questions","operation":"remove"} (or replace its heading with a one-line resolved summary). Remove it β€” don't launder it past the gate with a readiness_override (see Β§ Remove Open Questions, don't override).
  • Pin a hedged Files line β†’ {"taskId","section":"Files / paths affected","operation":"replace", "find":"<hedge line, e.g. corresponding __tests__ (update/new)>","replaceWith":"<the concrete path(s)>"}.
  • Add a missing Files section β†’ {"taskId","section":"Files / paths affected","operation":"append", "content":"<the section lines>"} (a missing Files section is an auto-dispatch-blocking omission β€” FM2).

Availability. task.patchBodySection is granted to the groom/design/ops workflows and merged to dev (PRs #1099/#1102/#1103). It only works against a backend deployed with that support β€” check the deployed SHA (/investigate's loader, or the staged-intents known-kinds) if a create task.patchBodySection 400s as an unknown kind. When it isn't available (older deployed backend), fall back to task.updateBody and reconstruct the whole body carefully β€” re-emit every existing section verbatim; the whole-body replace drops anything you omit.

Inline every value the isolated implementer can't research

The single most-repeated groom-time judgment. A promoted πŸ’» Code task is auto-dispatched into an isolated worktree: that implementer cannot read the skill tree (~/.claude/skills/…), the prod host, or the central config tree. So any value, format, or decision it would otherwise have to look up elsewhere must be inlined into the task body. Before promoting, ask: "what does this body assume the implementer can go read β€” and can they, from an isolated worktree?" If not, inline it. Concrete recurring cases: a required output format (e.g. embed the verbatim five-part closing-synthesis format rather than "follow the /design format"); prod/environment facts (exact schedule, retention, env-var names, ExecStart/User conventions β€” the implementer can't inspect the host); a corrected symbol/anchor/DDL where the body's guess is wrong. This is distinct from "resolve the open questions" β€” a body with every question resolved can still strand an implementer if it points at something only readable from outside the worktree.

Body-authoring gotchas (checklist)

  • Notion code-block language must be from its enum (toml / shell / markdown / …). ini is rejected β€” use toml.
  • The WAF blocks bare <interpreter> … --flag command lines (python -m …, systemctl restart …) β€” the create/patch call fails with a Cloudflare "you have been blocked" HTML error, not a validation error. Describe the command prosaically (backtick the verb/unit), or prefix it so it isn't a bare interpreter line (a full ExecStart=/usr/bin/node … unit line is fine). (Same trap as procedures.md Β§ Notion access.)
  • richText only auto-backticks whole shell-command lines β€” inline a symbol/path that contains a Tier-2 lexical trigger (a deferral phrase like "defer", quoted in prose) and it can trip a lexical false-positive; backtick or rephrase it so the gate doesn't read prose as a live signal.
  • Keep body and payload consistent by hand (FM2). The Files-section gate reads the caller-supplied filesPathsEntries, not the live body β€” so a resolved-looking payload over a still-hedged body will pass the gate but mis-drive the implementer. When you pin a Files line in the body, mirror it in the files_paths_entries you carry into the flip. Don't rely on the gate to re-derive from the body.

Remove Open Questions, don't override

For a πŸ’» Code task, a live ## Open Questions heading is a grooming artifact, not part of the spec β€” and it blocks promotion (readiness Tier-1). The fix is to resolve each question into the body and remove the heading (patchBodySection remove), not to launder the task past the gate with a readiness_override. Lean on the edit, not the escape. (Contrast the interactive πŸ“ Design path, where the OQ heading is legitimate β€” it is the /design worklist β€” and promotion carries the auto-applied override reason; see Step 4 Β§ approve-by-standard. The override is for that case, not for dodging an unresolved Code-task question.)

A wrong body is worse than a vague one β€” fix cost is not a reason to ship the error

When grooming surfaces body content that is wrong or misleading (a bad symbol, a stale anchor, a self-contradiction), correct it β€” the reconstruction/patch cost is never a reason to leave an error in place. A vague body makes an implementer ask; a confidently wrong one sends an isolated implementer down a dead end with no way to notice. Now that patchBodySection makes the fix a one-line patch, the old excuse ("fixing means rebuilding the whole body") is gone.


Step 4 β€” Mark Ready after sign-off

Only after explicit sign-off on the batch ("looks good", "ship it", "next"):

  1. First, record the sign-off in grooming-state.json for each task in that batch: fill achieves, open_questions, tests, manual, confirm regions, fill hard_block_deps (see presentation.md Β§ Dependencies for the hard-block vs soft-order distinction), fill size_check (the size classification β€” see presentation.md Β§ Size check β€” shape { loc, loc_method, files, decision, split_into?, reason? }, where decision is one of: "no_split" (≀500 LoC β€” proceed), "split_now" with split_into: ["<task-id>", …] (over the threshold and splittable β€” you nominate the split; the orchestrator confirms and routes it to a dedicated session, see Β§ Size check), "unsplittable" with reason: "<one-line>" (over the threshold but must land atomically), or { "decision": "n/a" } for Design/Planning tasks) and type_check (the present-and-dispositioned type/content-mismatch scan β€” advisory, never a hard block on its own), determine the Gate accretion and Seed accretion content below for πŸ’» Code tasks (write it into the entry as gate_contribution / seed_contribution β€” these are transient staging fields the flip command below reads and submits; the durable record is still the gate/seed DB marker the flip call writes, keyed by this task's id), and set signoff: { "by": "<human>", "at": "<iso>" }. (signoff, hard_block_deps, and size_check are gated by the promotion hook and must be written in grooming-state.json before the status flip, or the gate blocks the update.)

One-shot Ready-flip: once the entry above is complete, promote the task with a single call β€” no task id (or dependency id) is ever hand-typed on the command line, every id is resolved from the entry itself:

node ~/.claude/scripts/groom-flip-client.mjs .skill-cache/grooming/<M>/grooming-state.json <taskId>

This posts the whole entry to the backend's POST /api/groom/flip route (packages/backend/src/routes/groomFlip.ts, TaskWriteCommands.flipToReady), which runs gate accretion + seed accretion + setDependsOn + setStatus(Ready) as one transaction β€” the same order and the same groomingGate / dependsOn-before-Ready invariants as the per-call path below, but rolling back any accretion already committed if a later step fails, so a failed flip never leaves an orphan gate/seed item or a wrong status. This replaces the six separate calls (gate accrete, seed accrete, stage + apply setDependsOn, stage + apply setStatus) described next β€” read the per-call breakdown when you need to understand or troubleshoot what the one-shot command does, or when the discrete staged-intent surface is needed for something the one-shot command doesn't cover (e.g. reviewing a staged write before applying it).

Gate accretion (πŸ’» Code tasks): Before flipping to Ready, mint the task's stripped runtime/launch-and-observe items onto the milestone gate store. The accretion source is the task body's pre-groom ### πŸ‘οΈ Manual verification section, when present β€” a Code task author writes it only when there are real runtime items to list, so read whatever the section actually contains (never boilerplate β€” that convention is retired). Call the accretion route through the vendored client, never a body-append for the accretion itself:

node ~/.claude/scripts/gate-state-client.mjs accrete \
  '{"project":"<project-id>","taskId":"<task-id>","title":"<task-title>",
    "milestone":"<M>","classification":"<Read-Only|Prod-Mutating|Human-Observation|needs-triage>",
    "items":[{"text":"<item 1>"}, "…"]}'

For a task with no standalone runtime item, call it with "classification":"none" and an empty (or omitted) items array instead. Either way this POSTs to the loopback, device-authed /api/gate/accrete-contribution route (see packages/backend/src/routes/gateState.ts), which mints one gate_item per item on the milestone gate store and records the gate_accretion marker checkGroomingPromotionGate reads before allowing the Ready flip β€” the call itself is the durable record; nothing further needs writing to grooming-state.json.

Confirm the accretion in chat before the Ready-flip.

Once accreted, if the task body still carries a ### πŸ‘οΈ Manual verification section, stage a task.patchBodySection (operation: "remove") that removes it from the body entirely β€” never leave it behind and never replace it with boilerplate (Covered by the Manual Verification Gate.). Fall back to a whole-body task.updateBody only where patchBodySection isn't available (see Β§ Editing a task body). A post-groom πŸ’» Code task carries no manual-verification section at all; runtime/manual verification for it is owned by the gate from that point on, and its absence in the body is the correct post-groom state, not a gap.

Seed accretion (πŸ’» Code tasks): The operational twin of Gate accretion. Before flipping to Ready, classify each candidate line the pre-groom body's ## Operational seed section carries as operational-seed (a genuine prod-data row/flag/default deliberately kept out of this task's own PR β€” e.g. an analyzer_configs row, config-category defaults, alias/cohort flags), in-pr (mislabeled β€” it actually ships in the PR and does not accrete), or needs-triage (unclear, deferred). Mint every operational-seed-classified candidate onto the milestone seed store β€” never the in-pr/needs-triage ones. Call the accretion route through the vendored client, never a task.updateBody body-append:

node ~/.claude/scripts/seed-state-client.mjs accrete \
  '{"project":"<project-id>","taskId":"<task-id>","title":"<task-title>",
    "milestone":"<M>","decision":"seeds","seeds":[{"spec":"<seed 1>"}, "…"]}'

For a task with no operational seed (or every candidate classifies in-pr/needs-triage), call it with "decision":"none" and an empty (or omitted) seeds array instead. This POSTs to the loopback, device-authed /api/seed/accrete-contribution route (see packages/backend/src/routes/seedState.ts), which mints one seed_item per seed on the milestone seed store and records the seed_accretion marker checkGroomingPromotionGate reads before allowing the Ready flip β€” again, the call itself is the durable record.

Once accreted, stage a task.patchBodySection (operation: "replace", find: "None.", replaceWith: "- <seed 1>\n- …") that writes the same seed spec(s) into the task body's own ## Operational seed section, replacing the None. placeholder every task body already carries there β€” the operational twin of Gate accretion's removal, but in the opposite direction: the section gets filled in, not stripped out. Stage this on its own, with no groupId, and let it commit before staging the seed.stage / task.setStatus group β€” never batch it as a same-instant sibling of the arming task.setStatus. This must land on the real page before the Ready-flip's stage-time content-match check runs (checkGroupArmingIntentCompleteness re-derives the section server-side from the stored body and cross-checks it against the accreted seed_item rows); staged in the same group as the arming intent, the check would see only the still-unapplied None. placeholder and pass on an empty comparison β€” the exact self-verifying gap this content-match exists to close. A post-groom πŸ’» Code task carrying seeds shows those seeds' real specs in its own ## Operational seed section; a task with none shows None. β€” both are correct post-groom states, not a gap.

Confirm the accretion in chat before the Ready-flip.

Declared writes validation (πŸ”§ Operational tasks): Before flipping a πŸ”§ Operational task to Ready, check its body for a ## Declared writes section (see task-writing.md Β§ Declared writes for the authoring format). It is optional β€” most Operational tasks carry none, and that is fine β€” but when present it is load-bearing: it is what lets a dispatched ops session's session.requestCapability calls auto-approve instead of parking for a human decision, so a malformed entry silently strands the session on writes that should have sailed through. Validate each bullet parses to a non-empty, backtick-quoted capability string; an empty or tag-only bullet is malformed and blocks the Ready-flip the same way an unresolved ## Open Questions line does β€” send it back for correction rather than promoting around it. Do not invent a classification tag the author didn't write: an entry with no tag, or an ambiguous one, is left as-is (it still promotes β€” readinessGate.ts treats a missing/ambiguous tag as a valid, fail-closed Prod-Mutating entry, never a validation failure) β€” only flag entries where no capability could be parsed at all. This check runs automatically at stage-time too (checkReadiness runs unconditionally on every promotion), so a missed malformed entry is caught server-side regardless β€” but catching it in review avoids a bounced flip.

  1. Then, stage the write per task through the sanctioned device-authed staged-intents CLI client (node ~/.claude/scripts/staged-intents-client.mjs create <kind> <json-payload> <projectId> [groupId] β€” see packages/backend/scripts/staged-intents-client.mjs), never a direct notion-update-page call. This runs in the trusted Remote-Control session, so it stages through the same device-authed surface the dashboard panels use (POST /api/staged-intents, authenticated by $ORCHESTRATOR_DEVICE_TOKEN β€” not the stage-only ORCHESTRATOR_STAGE_TOKEN transport, which is reserved for unattended orchestrator-launched worktree sessions staging through the orchestrator MCP tool surface). After conversational sign-off in this session, apply each staged intent with node ~/.claude/scripts/staged-intents-client.mjs apply <intentId> (POST /api/staged-intents/:id/apply) β€” the readiness/groomGate guards still enforce on apply. This is not a UI panel: the session stages and applies its own writes, but only after the human has signed off in chat.
    • Generate one groupId per task (any stable string, e.g. the task id) so its writes present and apply as a unit.
    • If the task has hard-block deps (or had any and now has none), stage a task.setDependsOn intent: {"taskId": "<id>", "dependsOn": ["<id>", …]} β€” the rendered array of hard-block task IDs. Hard-block deps live in the Depends On property, never in the task body β€” downstream sessions read the property; body sequencing is invisible to them. If existing Depends On held free-form prose (notes, hints, soft-order observations), the canonical array overwrites it β€” soft-order observations are not persisted; they were a batch-level conversation and live in the chat record.
    • Then stage a task.setStatus intent with the same groupId: {"taskId": "<id>", "status": "Ready", "groomingGate": {"size_check": <the recorded size_check>, "type_check": <the recorded type_check>}}. The groomingGate field is required β€” it is the command layer's promotion-gate check (checkGroomingPromotionGate in groomGate.ts, wired into TaskWriteCommands.setStatus), the successor to the retired groom-gate.mjs PreToolUse hook: a missing or undispositioned size_check / type_check blocks the apply with a 409 and an annotation on the staged intent, same failure mode as the old hook.
    • Apply the task.setDependsOn intent before (or together with) the task.setStatus intent β€” the Ready flip is blocked with a 409 unless its group already carries an applied (or concurrently-applied) task.setDependsOn for the same task.
  2. Confirm in chat what was staged (Ready flip and Depends On value) for each task, and that it is now waiting for human apply. Then present the next batch.

Approve-by-standard (interactive πŸ“ Design / πŸ“‹ Planning types)

The numbered flow above is the per-task πŸ’» Code path. Interactive types promote by standard, not per item. After the consolidated triage (Step 2), the human engages only the needs-attention + blocked rows; every task left in the clean set promotes by default (visible + veto-able) β€” you do not collect a per-item positive stamp.

  • Record a triage object per interactive task in grooming-state.json (not a bare string) β€” groomGate.applyTriageFloor reads entry.triage.proposedVerdict, so the entry must be:
    "triage": { "proposedVerdict": "clean" | "blocked" | "needs-attention" }
    Setting triage: "clean" (a string) makes .proposedVerdict undefined and the task floors to needs-attention. proposedVerdict is your judgment call; the required-heading fact is derived server-side from the composed body β€” do not send it. Record it alongside constraints_dispositioned (see Step 1 Β§ constraint disposition). The flip client carries the whole triage object in groomingGate; the server re-derives the deterministic floor and can only ever downgrade a proposed clean (a hard-block dep not βœ… Done β†’ blocked; no ## Open Questions heading present β†’ needs-attention; a routed constraint-conflict β†’ needs-attention). It never upgrades a judged blocked / needs-attention back to clean.
  • The readiness_override reason is auto-applied β€” never hand-typed. For a πŸ“ Design task promoted clean, the backend supplies the standard template reason ("Design task β€” open questions are the /design worklist, resolved at execution; triaged clean in the <milestone> consolidated Design triage"); you record the clean triage verdict, not the reason string.
  • size_check is {"decision": "n/a"} for Design/Planning, and gate/seed accretion does not apply (those are πŸ’» Code artifacts). Per-task disposition records + audited applies are preserved β€” the ceremony removed is only the per-item positive stamp.

Gates last: the milestone's 🚦 Gate task is the final batch, after all code tasks are signed off. Accretion happens incrementally β€” as each πŸ’» Code task is promoted (Gate accretion above), its stripped items are minted onto the milestone gate store via the accretion route, not appended to the Gate task body. The final Gate batch confirms readiness (node ~/.claude/scripts/gate-state-client.mjs readiness --milestone <M>) and presents the Gate for sign-off. The milestone config-seed πŸ”§ Operational task accretes the same way (Seed accretion above) β€” a human runs the seeded items via /ops at milestone end, after the code is merged and deployed.

Re-check the board before finishing β€” don't close on a stale snapshot. Step 1 loaded the board once at the start, but new πŸ”² Backlog tasks can arrive during the session (the operator or another session files them while grooming runs). So before declaring the board groomed, re-query the live board for any πŸ”² Backlog tasks not in the original worklist. If any appeared, continue β€” groom them in fresh batches (Steps 2–4) exactly like the rest; don't defer them to "next time." Repeat the re-query until a pass finds no new Backlog. Only then, when every batch is signed off, confirm the milestone board is fully groomed.

Expect waves when a /design execution session is running concurrently β€” it files follow-on πŸ”² Backlog Code tasks into this board while you groom (a real run saw two waves, 12 + 3 tasks, arrive mid-session). The re-query-until-clean loop is what makes this safe, so treat it as load-bearing, not a formality. Record the set of task ids from the Step-1 worklist so each re-query surfaces the delta (newly-arrived tasks) at a glance rather than by manual diff β€” and remember a wave-added entry needs project/milestone/title stamped like any other (Step 1).


Rules (hard)

See ../_shared/reference/hard-rules.md for the planning-procedure core this skill shares with /design and /ops (deterministic load, the human as the gate, no silent writes, git -C not cd, and cache/state files via the Edit/Write tool) β€” canonical source packages/backend/src/planning/procedureCore.ts. Grooming-specific rules:

  • Source of truth: Notion for architectural rules, decisions, and task definitions. For implemented detail (DDL, signatures, analyzer specs), the code under source_root wins; on intent/rationale, Notion wins.
  • Scope is the target milestone only. Do not modify tasks on other boards unless a dependency issue is explicitly identified and the human approves it.
  • Never mark a βœ… Done or ⏭️ Deferred task Ready. Never retroactively edit any ordinary task already at πŸ—‚οΈ Ready or beyond β€” file a sibling instead. A Ready task may be in-flight: auto-dispatched if πŸ’» Code, human-run if πŸ”§ Operational / πŸ”Ž Investigation / πŸ§ͺ Testing β€” editing it races a live session. This holds for every ordinary type, without exception. The 🚦 Gate is the lone non-ordinary task: an accumulator that, by its type's definition, accretes manual-verification items while sitting at Ready β€” appending to it is its lifecycle, not a modify-a-Ready-task exception.
  • No silent writes (shared rule, groom-specific mechanics): every change is staged through staged-intents-client.mjs β€” never a direct notion-update-page call.
  • Investigate before resolving. Reading the code comes before deciding what's resolved. "Decide at implementation time" is a defer, not a resolve.
  • A πŸ’» Code body must be self-contained for an isolated worktree. The auto-dispatched implementer cannot read the skill tree, the prod host, or the config tree β€” so every value, format, or decision it would have to fetch from any of those must be inlined into the body before promotion. This is a readiness bar distinct from "open questions resolved," and the single most-repeated groom-time judgment (see Step 3 Β§ Inline every value the isolated implementer can't research). A wrong body is worse than a vague one β€” correct errors regardless of fix cost; patchBodySection makes the fix one line.
  • Cite-or-route, never invent. An open question that is an architectural decision is cleared only by (a) citing a locked decision (an arch-page rule or an already-βœ…-Done Design task) or (b) routing to /design β€” file a πŸ“ Design task and add a hard-block Depends On edge to it. Never invent the answer in-grooming. A task whose Depends On names a not-yet-βœ…-Done Design/Planning task is not promotable (the promotion gate blocks it, FM3; for interactive types the triage floor downgrades it to blocked).
  • Per-task sign-off is an invariant for πŸ’» Code. A batched or momentum operator action ("these all look fine") never stands in for the per-task disposition records; πŸ’» Code tasks are always promoted individually. (Interactive Design / Planning types promote approve-by-standard β€” the documented batch flow, not a shortcut around this invariant; see Step 4 Β§ approve-by-standard.)
  • The human is the promotion gate (shared rule) β€” but its granularity is type-dependent, because the stakes are. For auto-dispatched πŸ’» Code, a wrong Ready launches an unattended worktree, so promotion is a per-task human decision, taken individually β€” a batched/momentum action never stands in for it. For interactive πŸ“ Design / πŸ“‹ Planning (which /design picks up, so a wrong Ready is a reversible glance, not a launch), promotion is approve-by-standard: under one consolidated triage the clean set promotes by default (visible + veto-able) β€” no per-item or bulk positive stamp (see Step 2 Β§ consolidated triage and Step 4 Β§ approve-by-standard). The gate is preserved either way; per-task disposition records + audited applies are preserved either way. Reduced ceremony is not reduced rigor β€” the investigation posture (arch-page reading, code exploration, anchor grounding) is the sole non-server backstop once the per-item decision is removed, and stays non-negotiable.
  • Code tasks default to < 500 LoC estimated. The size check is load-bearing, not advisory β€” every Code task carries an explicit Size: line in its presentation header, and size_check is a required field in grooming-state.json that the promotion gate enforces. The 500-LoC default is a split threshold, not a hard ceiling β€” a task over it is nominated for splitting (and a sub-500 task may split on a natural seam); there is no hard file-count ceiling; a task that must land atomically records "unsplittable" + reason. Splitting and merging are orchestrator-driven: a grooming session nominates a split/merge candidate (decision: "split_now" + split_into), never performs it in-session β€” the orchestrator confirms the nomination and routes the split to a dedicated session (detect β†’ confirm β†’ route; see presentation.md Β§ Size check). Design and Planning tasks are sized in open-question count, not LoC; write {"decision": "n/a"} for them.
  • Hard-block dependencies live in the Notion Depends On property, never in the task body. Soft-order observations are batch-level conversation only β€” not persisted. The task.setDependsOn intent is staged with the same groupId as the task.setStatus Ready flip (see Step 4). See presentation.md Β§ Dependencies for the hard-block-vs-soft-order test.

See reference/anti-patterns.md for the failure modes these rules prevent.

Download now (ZIP)

Does it work?

Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.

No reviews yet

People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.

  • It worked
  • Partly
  • It didn't
What did you use it for? e.g. Reviewing pull requests in a Rails app

Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.

Your trail

Files you open will collect here, so you can find them again.

Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.

Now do this for your own.

Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.