Repository navigation
feat(groomer): create bounded child issues idempotently for decomposition plans (#1066) - #1186
itsmiso-ai wants to merge 10 commits into
Conversation
…tion plans (#1066) Extend the GroomingPlan child briefs into complete bounded implementation briefs, add a decomposition policy (no children while material design questions remain, at low confidence, or on a close), and apply the plan through a new idempotent children step: one child issue per brief keyed by GroomingChildClaim(childBriefKey = parent + normalized brief), so a retry after partial failure reuses created children and creates only missing ones. Children start as status/backlog with a machine marker and the full brief; the parent rides the umbrella label on the labels step and gets decomposed/followUpUrls + audit through the shared setDecompositionState helper used by the operator decompose route.
…eation (#1066) Five run-level tests pin the acceptance criteria: one child per brief with the marker, backlink and backlog label plus the parent decomposition audit; exact-retry replay; a failed create converging on retry (reuse + create missing); withheld splits creating nothing. The eval corpus #1066 pending stub becomes a real check: children created once, zero on replay.
…ld claims (#1066) Review round: - the selector excludes umbrella-labeled issues on every path, including a targeted re-groom, so the umbrella must NOT ride the labels step: a decomposition that failed halfway would strand its parent forever. The children step now adds it additively only after every child exists or is reused, and ApplyResult.labels folds it in so the freshness baseline and audit record the real label set. - a fresh null child claim (younger than ACTIVE_CLAIM_MS) is held by another in-flight attempt: creating on top of it could duplicate, so the step fails and a later attempt converges. - a failed children write carries the partial created/reused links on the failed step, so the run record shows children that landed even when a later create threw. - child keys in the application-key intent are sorted: brief order is not identity. - accurate docs: umbrella ordering + createIssue consumers.
…d; order the umbrella last (#1066) Review round 2: - GroomingChildClaim records the claiming applicationKey. A fresh null claim under a DIFFERENT key is another in-flight attempt (skip, retry later); under the SAME key it is my own abandoned create — the GroomingApplication resume CAS already excludes a concurrent same-key attempt — so the retry creates the missing child immediately. The in-memory fakes now stamp updatedAt on claim create like @updatedat, so the convergence tests exercise the real store's semantics. - the children step now ends with the umbrella label: children first, the parent's decomposition state second, the umbrella last. Any failure before the umbrella keeps the parent re-selectable (every selector path excludes umbrella-labeled issues), so a partially applied decomposition always has an automatic retry path.
|
Responses to the automated review (all findings addressed in
|
Superseded by a newer automated review for this pull request.
…ime (#1186 review) The children step's setDecompositionState entry no longer lists the umbrella label: the umbrella is the step's final write, so claiming it in the audit before addLabel lands overstates the parent's labels if that add fails. The umbrella's own success or failure stays visible on the children step record and the run's groom audit.
|
Responses to the second review round (
|
joryirving
left a comment
There was a problem hiding this comment.
Reviewed current head beb6a9b against #1066 and the final store/migration path. The earlier bot findings are fixed, CI is green, and the core claim/retry ordering is much healthier. I still have two in-scope blockers before I’d merge this.
1. #1066 explicitly requires a managed decomposition section on the parent body, but this PR deliberately does not implement it
The settled design says:
Preserve the original parent body/history; add a concise managed decomposition section rather than replacing it wholesale.
This PR instead states “Parent body/history is never written by this change” and treats label + DB state + audit as sufficient. That is a reasonable alternative design, but it is not the issue contract this PR says Closes #1066.
Please either:
- implement the concise decomposition section through the existing managed-body mechanism, without replacing human text; or
- explicitly reconcile #1066 in place before merge, recording the maintainer decision that the existing
umbrella+ decomposed/followUpUrls + audit convention supersedes the managed-body requirement.
I would be fine with the second option if that is the intended architecture, but the PR should not silently close an unmet settled requirement.
2. Child creation does not enforce the “complete bounded implementation brief” safety rule
The prompt tells the model every child must contain a complete brief, but neither validation nor evaluateDecompositionPolicy enforces that before side effects.
Today a decomposition can validate and create a child with, for example:
verifiedCurrentBehavior: nulldesignDecision: nullrelevantPaths: []inScope: []outOfScope: []acceptanceCriteria: []tests: []
validateGroomingPlan only requires at least one child when decomposition.required; the response schema requires the fields to be present, but allows those null/empty values; and evaluateDecompositionPolicy only checks close/confidence/material uncertainty.
That means model compliance, rather than the mutation boundary, is currently the only thing preventing an under-specified child from becoming a real GitHub issue. #1066 specifically says each child must carry enough information for a local implementation worker and its acceptance criteria require the child body to contain the complete bounded implementation brief.
Please add an explicit completeness gate before child creation and pin it with tests. The exact null/empty policy can reflect legitimate cases (for example, dependencies may reasonably be empty, and if designDecision: null is intentionally valid when no design choice exists, document that), but the mutation layer must reject/withhold a child that lacks the information #1066 says is required rather than relying on the prompt.
What checks out
I independently rechecked:
GroomingChildClaimis additive and keyed uniquely;- same-application abandoned claims vs foreign fresh claims now match real Prisma
@updatedAtsemantics; - children are claimed before creation and existing recorded children are reused;
- children → decomposition state → umbrella ordering really does leave a failed pre-umbrella parent re-selectable by the hosted groomer (the selector excludes the
umbrellalabel, not the DBdecomposedflag); - child issues start exactly
status/backlog; - material uncertainty blocks decomposition;
- the state/audit helper is shared with the operator route;
- migration, CI, Security Audit, image build, PR Smoke and AI review are green.
The documented lost-response window between GitHub accepting a child create and saveChild is an accepted residual risk, not a separate blocker from me.
…claim race; soften claim comments (#1186 review)
|
Third pass (
Suite after this round: 166 files, 3852 passed / 16 skipped / 1 todo (pre-existing #1063 eval stub); |
Superseded by a newer automated review for this pull request.
….35.5, source-map-js 1.2.2)
|
|
Superseded by a newer automated review for this pull request.
…f-completeness gate (#1066 human CR) Blocker 1: the children step now writes a concise managed section (dispatch-groomer:decomposition marker pair, one line per child) into the parent body between the decomposition-state write and the final umbrella label, preserving all text outside the section byte-for-byte. It coexists with the enrichment managed section because each writer replaces only its own markers; re-rendering the same child set writes nothing; malformed or oversized bodies refuse only the section write (recorded in the step detail) without blocking the decomposition; model-authored groomer markers are stripped from enrichment content so the namespaces cannot nest. ApplyResult.body reports the real post-apply body for the freshness baseline. Blocker 2: evaluateDecompositionPolicy now enforces the complete bounded implementation brief at the mutation boundary (childBriefCompletenessGaps): non-blank problem/designDecision/verifiedCurrentBehavior and non-empty relevantPaths/inScope/outOfScope/acceptanceCriteria/tests; empty dependencies is legitimate. The system prompt contract matches the gate.
|
Blocker 1 — managed decomposition section. The children step now writes the parent-body section #1066's settled design requires: its own Blocker 2 — completeness gate at the mutation boundary. Docs: decomposition section, gate, refusal semantics, and the accepted consequences (last-write-wins re-plans; a lone well-formed marker pair is Dispatch-owned; decomposed parents are never enriched afterwards). PR body updated: invariant 12 restated (body preserved + managed section, no longer "never written"), invariant 9's order updated to include the section write, new invariant 13 for the completeness gate. An independent review pass over this diff verified both blockers and the freshness/ |
Superseded by a newer automated review for this pull request.
There was a problem hiding this comment.
AI Automated Review
Full PR review.
Analysis engine: MiniMax-M3 (anthropic) — primary route · pr-reviewer-action v3.3.0
Requirement traceability gap: the review itself completed, but one or more in-scope requirements could not be verified against enforcement or test evidence. Approval is withheld until that requirement is covered.
Recommendation: Approve
This PR delivers the full PR 1066 contract: a validated plan that splits an issue into bounded children is now actually applied. The change is well-shaped — the new decomposition step sits in the existing lowest-impact-first apply order, reuses the prior mutation persistence, and is idempotent through the new GroomingChildClaim table. Both human-CR blockers from joryirving are addressed at the current head with substantive implementation (not reconciled away). I did not find anything that would prevent merge.
Change-by-change findings
Contract — ChildBrief widening and parse-defaulted migration. src/lib/groomer/plan.ts widens the brief with designDecision, verifiedCurrentBehavior, relevantPaths, inScope, outOfScope, dependencies, and tests. The parser (parseDraft) uses r.optionalText / r.textList, and childBriefKey canonicalizes nulls and missing list items to null/empty, so old plans and stored plans keep parsing — verified by the round-trips every field of a full child brief and parses an old three-field brief with the new fields defaulted to null/empty tests in plan.test.ts. The plan-schema.test.ts asserts the full schema shape and field bounds. schemaVersion stays 1. Clean.
Policy — apply-time completeness gate. evaluateDecompositionPolicy and childBriefCompletenessGaps in mutation-validator.ts enforce: no close, no low confidence, no material uncertainty, and every brief must be a complete bounded implementation brief (problem/designDecision/verifiedCurrentBehavior non-blank; relevantPaths/inScope/outOfScope/acceptanceCriteria/tests each has at least one non-blank entry; dependencies may legitimately be empty). designDecision and verifiedCurrentBehavior are explicitly rejected when null — the test treats whitespace-only entries as absent covers that. computeMutationDiff is the only path that produces diff.children, and only diff.children !== null opens the children step and createIssue. Tests cover each field-cases iteration, both warnings (close-in-plan, material-uncertainty), whitespace-only lists, and a multi-child both-incomplete report. Clean.
System prompt alignment. The prompt was updated from a three-field example to the full brief shape with the completeness contract ("every field is required and non-blank, every list except dependencies must hold at least one entry"), explicit "designDecision and verifiedCurrentBehavior must not be null: state the settled decision even when it is simply 'no design choice; follow the existing pattern'", and the rule that unresolved design questions belong to the escalation lane. The prompt tests pin both the full shape and the apply-time gate. Clean.
Applier — children step (dispatch#1066). The children step is added between content and close in mutation-applier.ts. It claims each child in GroomingChildClaim keyed by childBriefKey before createIssue, reuses already-created children, then records setDecompositionState with diff.labelsAfter (no umbrella yet) as followUpUrls, then writes the managed decomposition section into the parent body, then adds the umbrella label as the step's final write. Any throw inside that sequence rolls the partial state into PartialStepError.partial.children, so the failed step record carries what landed and a retry can reuse it. Tests cover: created-from-keeps-childs, reuse under same key (replay surfaces links), reuse under fresh different-key claim (no duplicates), partial create + retry converges, held-claim guard, null-claim unordered are aged out, ordering-equal concurrent sibling test, and application-key stability when children are reordered. The two-writer design (content step + children step on the body) is solved by each writer parsing only its own pair, so both managed sections coexist and reverse-nesting is impossible — tested both directions.
Shared decomposition state. setDecompositionState in src/lib/decomposition.ts writes the five Issue columns and the issue_decomposed/issue_reactivated audit entry. The operator POST /api/issues/actions/decompose route was refactored to call it, keeping its auth/validation/response shape; the route tests were updated to mock findUnique (the response now re-reads the issue rather than using the update return value) and setDecompositionState itself is tested directly. Clean.
DB — additive migration. prisma/migrations/20261007000000_add_grooming_child_claim/migration.sql adds GroomingChildClaim with unique childKey, FK to Issue cascade, nullable childNumber/childUrl/applicationKey. prisma/schema.prisma mirrors it. CI: Database migrations succeeded.
Umbrella exclusion. src/lib/issue-filters.ts now imports UMBRELLA_LABEL from decomposition.ts so the Prisma where clause uses the constant — no behavior change, but single source of truth for the label.
Docs. docs/hosted-groomer.md adds the Decomposition section, updates the apply order to step 4 (children) between content and close, and removes "child issue creation (PR 1066)" from the out-of-scope list.
Claim falsification
- Claim 1 (system prompt matches the apply-time gate; completeness contract;
dependenciesis the only list that may be empty; incomplete brief withholds). Held:src/lib/groomer/prompts/system-prompt.tsstates the full contract and namesdependenciesas the only list that may be empty;evaluateDecompositionPolicyenforces the same predicate viachildBriefCompletenessGaps, andcomputeMutationDiffis the only producer ofdiff.children. Verified: 12/12 listed call sites all reference the gate or the contract (the actual sites areprompts/system-prompt.ts,mutation-validator.ts(policy + gaps),mutation-applier.ts(gate consumer), the plan parser (plan.ts) for shape, and the plan-schema test plus plan test plus system-prompt test). - Claim 2 (child claimed in
GroomingChildClaimkeyed bychildBriefKeybefore creation; retries reuse). Held:mutation-applier.tscallsstore.claimChildbefore eachgithub.createIssue; the prisma-storeclaimChildreadsfindUniquefirst and falls back to P2002 loser-reads-winner. Tests cover: exact retry replay, replay re-surfaces links, reuse under same key, reuse under fresh different-key (no duplicates), held-claim under different key withinACTIVE_CLAIM_MS, same-key abandon-then-create-on-top, aged-out null claim proceed. Verified at:prisma/schema.prisma:613,prisma/migrations/20261007000000_add_grooming_child_claim/migration.sql(8/27/21/26 lines), and the applier children step +makePrismaApplicationStoretests. - Claim 3 (children step: every child exists or reused → record decomposition state → write managed section → add
umbrellalabel, in that order). Held: testsapplies the children step's writes: child creates → decomposition state → body section → umbrella labeland the per-step testcreates each child issue, adds the umbrella via addLabel after all children landverify ordering. The umbrella is deliberately NOT indiff.labelsAfter/labelsStep— it is added byaddLabelas the step's final write, andresult.labelsfolds it in for the freshness baseline. - Claim 4 (setDecompositionState is one helper used by both hosted groomer and
POST /api/issues/actions/decompose, route keeps auth/validation/response shape). Held:src/lib/decomposition.ts:131exportssetDecompositionState, called bymutation-applier.ts:1025(children step) androute.ts:72. Route retainsauthorizeRequest,resolveActor,enforceRateLimit, therepo/issueNumber/decomposed/followUpUrls/notevalidation, and the JSON response shape (issueId,decomposed,decomposedAt,followUpUrls). - Claim 5 (children created with exactly
["status/backlog"], never directly ready). Held:CHILD_ISSUE_LABELS = ["status/backlog"]is the only label passed togithub.createIssuein the children step (mutation-applier.ts, the spread[...CHILD_ISSUE_LABELS]). Run test assertsmocks.createIssuewas called withexpect.objectContaining({ labels: ["status/backlog"] }).
Standards Compliance
The repository standards file (AGENTS.md) does not impose specific architectural rules on this change; the PR follows the established groomer subsystem structure (lib/groomer/{plan, plan-schema, mutation-applier, mutation-validator, run, prompts} plus lib/{decomposition, github, github-issues}), keeps applyGroomingMutations as the single producer of side effects, and reuses prisma/auditLog via the small DecompositionStateClient slice — same pattern the existing ApplicationStoreClient slice uses for the application claim. No agent-specific names are introduced in generic docs. Schema relations stay strict (FK to Issue is non-nullable with cascade, no nullable FKs to hide bugs).
Linked Issue Fit
The PR closes PR 1066. Every acceptance criterion is addressed:
- One broad validated plan creates N bounded child issues and links them:
applyStepchildren loop creates one issue per brief; child bodies link back viaParent: <url>. - Re-running the same logical decomposition creates zero duplicate children: covered by exact-retry replay test.
- Partial failure + retry converges: covered by
carries the partial children on a failed create, and converges on a retry of the same keytest. - Every new child starts non-claimable and is independently groomable:
CHILD_ISSUE_LABELS = ["status/backlog"](worker queue excludes by default per AGENTS.md). - Parent becomes non-claimable under existing umbrella/decomposed conventions:
umbrellaadd +decomposed=truevia shared helper. - Child bodies contain the complete bounded implementation brief, not a paraphrase of the parent:
renderChildIssueBodyproduces a deterministic body from the brief, withParent: <url>backlink, complete-brief sections, and a<!-- dispatch-groomer:child=<key> -->marker — verified by therenders a full brief deterministicallytest. - Unresolved architecture prevents child creation: covered by the material-uncertainty test.
- All created/reused child links are auditable from the grooming run and parent:
describeApplicationaddschildrenCreated/childrenReused/childrenErrortoappliedMutations, the parent body's managed section lists each child URL, andissue.auditLogrecords theissue_decomposedaction with thefollowUpUrls.
Tool Harness Findings
The 11 tool calls (10 reads + 1 grep) confirmed: setDecompositionState has exactly two callers (the route and makePrismaApplicationStore.setDecompositionState); childBriefKey is used at the children-claim loop and in computeApplicationKey (sorting for stable key); UMBRELLA_LABEL flows from decomposition.ts into issue-filters.ts and the children step; the children step's ordering and guards match the test expectations and the docs. The prisma/schema.prisma GroomingChildClaim block matches the migration 1:1.
Unknowns or Needs Verification
- A trace against the host platform's compatibility matrix for
sharp0.35.5 /proxy-addr2.0.8 /source-map-js1.2.2 was not run: these are patch-only transitive bumps fromnpm audit fix, CI is green includingnpm audit, and the host platform (Node 24-bookworm-slim) is unchanged — treated as a non-platform-coupled bump. If the maintainers prefer a trace for any of these advisories, it is independent of the PR 1066 feature work. - The Prisma
childNumber Int?column has no explicit@default; a missing child on a stored claim row will read asnulland the children-step logic treats that correctly (held-claim path). This is consistent with the migration file shown.
Files most central to the verdict
src/lib/decomposition.ts— shared persistence + child brief helperssrc/lib/groomer/mutation-applier.ts— children step, ordering, refusal semantics,ApplyResult.bodycorrectnesssrc/lib/groomer/mutation-validator.ts— completeness gatesrc/lib/groomer/run.ts— wirescreateIssueandaddLabelintoGroomerDeps, surfacesparentUrlprisma/migrations/20261007000000_add_grooming_child_claim/migration.sqlandprisma/schema.prisma— additive tablesrc/lib/groomer/prompts/system-prompt.ts— prompt aligned with the gate
Requirement trace (10 unresolved of 12)
10 of 12 requirement(s) not fully traced to enforcement and a test:
req-b97c37782e77— unverifiable (no valid enforcement location)req-6e5a936bda44— unverifiable (no valid enforcement location)req-4ebec684de2a— unverifiable (no valid enforcement location)req-d119fa2f85ec— unverifiable (no valid enforcement location)req-5375bdb1fb31— unverifiable (no valid enforcement location)req-86cc807c2553— unverifiable (no valid enforcement location)req-a2cc1a384692— unverifiable (no valid enforcement location)req-292453f9e943— unverifiable (no valid enforcement location)req-0b656b29cfd9— unverifiable (no valid enforcement location)req-4f4c09cd23c6— unverifiable (no valid enforcement location)
Outstanding Human Change Requests
- @joryirving's change request (beb6a9b, head moved since) is not shown addressed at this head; it needs the reviewer's own re-review.
Approval withheld: the review completed, but an in-scope requirement lacks verifiable enforcement or test evidence, so it is publishing as an advisory comment rather than an approval.
|
State settled at this head:
Requirement trace (the withholding reason on the last AI review). The saffron review on
Settled decisions carried from earlier rounds (unchanged): state→section→umbrella write ordering; claim-before-create with per-claim Remaining (external): re-run AI review on |
|
Re-verification pass at
Remaining (external, unchanged): Settled decisions carried from earlier rounds are unchanged (state→section→umbrella write ordering; claim-before-create with per-claim |
|
Courier: already addressed (run courier-842a9527a4b22885d5f55a6aadcc810a) PR #1186 is already review-ready at head f12da69 (local HEAD == origin/courier/misospace/dispatch/issue-1066, current with main 890a8fa, clean tree). All 13 CI checks green at this head; no unresolved review threads. Both of joryirving's CHANGES_REQUESTED blockers (managed decomposition parent-body section; apply-time childBriefCompletenessGaps gate) are implemented in e90dedf and confirmed addressed by the AI review at f12da69. Local re-validation: 3879 tests passed / 16 skipped / 1 todo, tsc clean, eslint 0 errors (1 pre-existing warning also on main). Remaining gate is a human re-review of the stale CHANGES_REQUESTED, which per run contract stays with the operator/human. Verification comment posted: #1186 (comment) |
Reason: PR review: CHANGES_REQUESTED Total attempts: 138 Attempts by lane:
Failing run(s):
Last attempt: AI Automated ReviewFull PR review. Analysis engine: MiniMax-M3 (anthropic) — primary route · pr-reviewer-action v3.3.0
Findings (1 blocker)
RecommendationRequest changes. The implementation closely tracks the linked issue (PR 1066) and the technical design is internally consistent: idempotent child creation via Claim verification (5 claims)
Change-by-change findings
Standards Compliance (AGENTS.md)
Linked Issue FitLinked issue: PR 1066 — "groomer: create bounded child issues idempotently for issues that require decomposition." Acceptance criteria check (per the requirement ledger):
The settled design additionally says: "Preserve the original parent body/history; add a concise managed decomposition section rather than replacing it wholesale." The PR deliberately does not implement this body decoration and explicitly justifies it in the PR thread ("If maintainers want body decoration, it should be a follow-up that designs the two-writer interaction"). That is a reasonable engineering choice, but it diverges from the settled design. See the Outstanding Human Change Requests section: the human reviewer flagged this exact gap and asked for either the body section or an in-issue reconciliation. That is the deciding signal. Outstanding Human Change RequestsOne outstanding human change request from Tool Harness FindingsThe harness executed 8 read-only calls ( Unknowns / Needs Verification
Required Check Dispositions
Why this is
|
Closes #1066
Summary
The hosted groomer could already describe a decomposition (
decomposition.required+ child briefs) but the mutation layer ignored it. Now a validated plan whose decomposition passes the policy creates the children: one bounded GitHub issue per brief, the parent decorated as an umbrella/decomposed parent, everything idempotent and auditable.src/lib/groomer/plan.ts,plan-schema.ts,prompts/system-prompt.ts).ChildBriefwidens from{title, problem, acceptanceCriteria}to a complete bounded implementation brief:designDecision,verifiedCurrentBehavior,relevantPaths,inScope,outOfScope,dependencies,tests(all optional on the wire and lenient in the parser, so old drafts and stored plans keep parsing;schemaVersionstays 1). The system prompt states the same completeness contract the apply-time gate enforces: every field carries content,dependenciesis the only list that may be empty, and an incomplete brief withholds the whole split.evaluateDecompositionPolicy+childBriefCompletenessGaps,mutation-validator.ts). Children may not be created when the same plan recommends a close, confidence is low, any material uncertainty remains (design choices included — unresolved architecture stays on the escalation path), or any child brief is not a complete bounded implementation brief (human review blocker on groomer: create bounded child issues idempotently for issues that require decomposition #1066's "complete brief" safety rule — enforced at the mutation boundary, not by prompt compliance): non-blankproblem/designDecision/verifiedCurrentBehaviorand at least one non-blank entry inrelevantPaths/inScope/outOfScope/acceptanceCriteria/tests; an emptydependencieslist is legitimate. A failing policy is withheld like close/ready withholdings: recorded inwithheld.decomposition, no children, no decoration.mutation-applier.ts). Newchildrenapply step betweencontentandclose. Each child is claimed inGroomingChildClaimkeyed bychildBriefKey(repo + parent issue + normalized brief) before creation — so an exact retry, a retry under a fresh evidence digest (different application key), and a retry after partial failure all reuse created children and open only the missing ones. After every child exists or is reused the step records the parent's decomposition state, writes the managed decomposition section into the parent body, and then, as its final write, adds theumbrellalabel.src/lib/decomposition.ts).setDecompositionState(Issuedecomposed/decomposedAt/decomposedBy/decomposedNote/followUpUrls+ theissue_decomposedaudit entry) is now one helper used by both the hosted groomer and the operatorPOST /api/issues/actions/decomposeroute — the route keeps its auth/validation/response shape and no longer duplicates the persistence. Child bodies render deterministically from the brief with a hidden<!-- dispatch-groomer:child=<key> -->marker, aParent:backlink, and a footer telling the next groomer the child needs its own pass. Children are created with exactly["status/backlog"]— never directly ready.prisma/schema.prisma+ additive migration20261007000000_add_grooming_child_claim).GroomingChildClaim: uniquechildKey,applicationKeyof the claiming application, FK to the parentIssue(cascade, matchingGroomingApplication), nullablechildNumber/childUrlrecorded when the create lands.Invariants this change keeps, and every other path that enforces the same rule
status/*label after an applied groom ([P1] Groomer re-groom leaves an issue with no status label and the park intact, making it invisible to the queue #941). Enforced incomputeMutationDiff(withDerivedStatus) andtoGroomerOutput(strips foreign statuses). Kept:umbrellaand the child label are non-status/status/backlog-at-creation; the applier test asserts bothlabelsStepderivation branches keep non-status labels; the existing [P1] Groomer re-groom leaves an issue with no status label and the park intact, making it invisible to the queue #941 tests pass unchanged.status/in-progress|in-review) are never moved. Enforced by the skip inrun.tsbefore any apply. Kept: the skip runs before the applier, so an in-flight parent gets no children;mutationPlan.willCreateChildrenis forced false on that path (tested).applyGroomingMutations(preconditions are read-only). Kept: child creation and the parent body section live only in the applier; a dry-run test assertscreateIssueis never called.run.ts(validateApplyPreconditionsgates the whole apply). Kept: children and the section write ride the same gate — they cannot be created on stale evidence (no change to that path; its tests pass).evaluateClosePolicy/evaluateReadyPolicyincomputeMutationDiffunchanged. Kept: the decomposition policy blocks children when a close is recommended, so a close and child creation can never apply in one plan (tested at policy and applier level).status/*oragent/*labels; only priority/type are model-mutable.PLAN_LABELSallowlist inplan.tsunchanged. Theumbrellawrite is deliberately outside the plan channel: applier-only, additive (addIssueLabel), only on the parent, only once children exist.run.tsbaseline comment). Kept:ApplyResult.labelsunions the umbrella when the children step landed, sofreshnessBaselineIssueData,AuditLog.afterLabelsandGroomingRun.labelsAfterrecord the real label set (test asserts the union);ApplyResult.bodylikewise reports the real post-apply body — the section the children step wrote, else the content step's write, else null — so the baseline never claims a body GitHub does not have, including the refused-section and replayed-step cases (tested).POST /api/issues/actions/decompose(operator) and the groomer's children step both callsetDecompositionState; response fields, status codes, actor resolution and the audit notes shape are byte-identical to the previous route implementation (route tests pass; audit-notes equality is asserted indecomposition.test.ts). The groomer's entry records the parent's label set at state-write time — the umbrella, added as the step's final write afterwards, is visible on the children step record and the run's groom audit rather than claimed early.buildUmbrellaIssueExclusionWhere(src/lib/issue-filters.ts, now importing theUMBRELLA_LABELconstant) removes the parent from groomer selection on every path — including a targeted re-groom (selector.tsbypasses only the grooming-state exclusion; verified).agent-queue.ts'sexclude_decomposedhides decomposed parents from worker queues. The children step therefore orders: all children created-or-reused → parent decomposition state recorded (setDecompositionState) → managed decomposition section written →umbrellalabel added (tested ordering). Any failure before the label keeps the parent re-selectable, so every partial decomposition (failed create, failed state write, or failed section write) has an automatic retry path; a completed decomposition excludes the parent exactly like operator-marked umbrellas.AgentReportDedupe,GroomingApplication).GroomingApplication(uniqueapplicationKey) still replays whole applications;GroomingChildClaim(uniquechildKey) deduplicates children across applications, since the evidence digest changes every run. Claims are P2002-tolerant like the precedent (loser-reads-winner tested). Each claim row records its claimingapplicationKey: a fresh null claim (<ACTIVE_CLAIM_MS) under a different key is held by another in-flight attempt (the step fails conservatively and converges on a later retry), while a fresh null claim under the same key is this application's own abandoned create — the resume CAS plus single-replica confinement of the groomer make concurrent same-key attempts not expected — so the retry recreates the missing child immediately (tested; the in-memory fakes stampupdatedAton claim create like Prisma's@updatedAt). The residual window (create accepted by GitHub but the response lost beforesaveChild) is documented in code and mitigated by the child-body marker for discovery — the same crash-consistency class groomer: validate preconditions and apply grooming mutations idempotently #1063 accepted for comments.createIssueinsrc/lib/github-issues.tsis used as-is (no behavior change; doc comment updated to name the new consumer). Children get labels via the create payload; the endpoint auto-creates missing labels, andstatus/backlogis a managed label.<!-- dispatch-groomer:decomposition:start/end -->marker pair, one line per created-or-reused child, written by the children step between the state write and the umbrella. History is never replaced: the section replaces only its own markers in place and appends when absent; everything outside — human text and the groomer: validate preconditions and apply grooming mutations idempotently #1063 enrichment managed section — is kept byte for byte (cross-application preservation tested). The two writers are decoupled: each parses and renders only its own marker pair, so neither can clobber the other; model-authored groomer markers are stripped from enrichment content (as on the comment path) so the namespaces cannot nest. Re-rendering the same child set writes nothing (fixed point, tested); malformed markers or a body that would exceed the ~125k GitHub cap refuse only the section write — recorded in the step detail — so the decomposition itself still lands and a human-broken body never livelocks the parent in selection. The original body text and history are never rewritten wholesale by any path in this change.childBriefCompletenessGapsin the apply-time decomposition policy withholds the whole split when any brief lacks a settleddesignDecision, evidence-backedverifiedCurrentBehavior, or a non-emptyrelevantPaths/inScope/outOfScope/acceptanceCriteria/tests(emptydependenciesis legitimate).computeMutationDiffis the only producer ofdiff.childrenand the only path tocreateIssue, so no path (dry run, in-flight skip, resume) can create a child from an incomplete brief (tested at policy, applier and run level; the system prompt states the same contract).Requirement trace (#1066 ledger -> enforcement + test, line numbers at head
f12da69)Each quoted ledger requirement maps to the gate that enforces it and the assertion that covers it, so both are verifiable against the checkout.
req-6e5a936bda44— enforcement:src/lib/groomer/mutation-applier.ts:378(plan.decomposition.required && plan.decomposition.childBriefs.length > 0gates the children step, which creates one issue per brief and records the parent links); test:src/lib/groomer/mutation-applier.test.ts:739(expect(github.createIssue).toHaveBeenCalledTimes(2)) withfollowUpUrlsasserted atsrc/lib/groomer/mutation-applier.test.ts:769.req-4ebec684de2a— enforcement:src/lib/groomer/mutation-applier.ts:822(if (step === "children" && prior.children?.children) {— a landed children step replays its recorded children instead of creating; dedupe identity is the uniquechildKeyclaim atsrc/lib/groomer/mutation-applier.ts:946-973); test:src/lib/groomer/mutation-applier.test.ts:1025(expect(second.github.createIssue).not.toHaveBeenCalled()for the same briefs under a different application key) plus the zero-duplicates corpus eval.req-d119fa2f85ec— enforcement:src/lib/groomer/mutation-applier.ts:846(...(err instanceof PartialStepError ? err.partial : {}),— the failed step's partial child links persist, so a retry recreates only what is missing); test:src/lib/groomer/mutation-applier.test.ts:980(expect(retry.github.createIssue).toHaveBeenCalledTimes(2) // B and C, not A).req-5375bdb1fb31— enforcement:src/lib/agent-queue.ts:402(claimable: status !== BACKLOG_STATUS && openBlockers.length === 0— astatus/backlogchild never surfaces as claimable) with the creation label setsrc/lib/decomposition.ts:24(CHILD_ISSUE_LABELS = ["status/backlog"]); test:src/lib/groomer/run.test.ts:2671(created child carrieslabels: ["status/backlog"]end-to-end) andsrc/lib/decomposition.test.ts:49.req-86cc807c2553— enforcement:src/lib/agent-queue.ts:318(actionable = actionable.filter((issue) => !issue.decomposed)hides decomposed parents from worker queues) and the sharedsetDecompositionStatewrite atsrc/lib/decomposition.ts:152; test:src/lib/groomer/mutation-applier.test.ts:747(expect(github.addLabel).toHaveBeenCalledWith("org/repo", 42, "umbrella")as the step's final write).req-a2cc1a384692— enforcement:src/lib/groomer/mutation-validator.ts:498(if (brief.designDecision === null || brief.designDecision.trim().length === 0) gaps.push("designDecision");— an incomplete brief withholds the whole split) plus the deterministic full-brief renderrenderChildIssueBodyinsrc/lib/decomposition.ts; test:src/lib/groomer/mutation-validator.test.ts:627(policy names every missing brief field) and the marker/backlink body assertions insrc/lib/groomer/mutation-applier.test.ts.req-292453f9e943— enforcement:src/lib/groomer/mutation-validator.ts:498(symboldesignDecision: an unsettled design choice fails the completeness gate) together with the material-uncertainty rejection atsrc/lib/groomer/mutation-validator.ts:556; test:src/lib/groomer/mutation-applier.test.ts:868(expect(github.createIssue).not.toHaveBeenCalled()when the decomposition is withheld for material uncertainty).req-0b656b29cfd9— enforcement:src/lib/groomer/mutation-applier.ts:1032(followUpUrls: links.map((child) => child.url)persisted through the sharedsetDecompositionState, which also writes theissue_decomposedaudit entry); test:src/lib/decomposition.test.ts:217(audit entryaction: "issue_decomposed") and the run-levelappliedMutations.childrenCreated/childrenReusedassertions insrc/lib/groomer/run.test.ts.req-b97c37782e77— enforcement:src/lib/groomer/mutation-applier.ts:1081(throw new PartialStepError(errorMessage(err), { children: { created, reused } });— the failed step keeps partial child links so the same-key retry recreates only the missing child); test:src/lib/groomer/mutation-applier.test.ts:816(expect(github.createIssue).toHaveBeenCalledTimes(1)after an earlier attempt landed one child).Out of scope (unchanged from the issue)
Implementing children; opening PRs; semantic duplicate closure; cross-repository children; removing a stale
umbrellalabel if an operator later rewrites a decomposed parent's plan (operator edit remains the lever, as for operator-marked umbrellas). Accepted consequences documented indocs/hosted-groomer.md: a re-plan with different child briefs replaces the section andfollowUpUrlswith the new child set (last-write-wins; abandoned children stay open as backlog, traceable by theirParent:backlink); a single well-formed decomposition marker pair in a body is treated as Dispatch-owned and replaced; a decomposed parent is never body-enriched afterwards (the section's fixed prose keeps the body non-sparse). Note: extendingcomputeApplicationKey's intent with the child keys changes every application key once at deploy, costing at most one extra mostly-noop apply per issue — same class as any intent extension.Tests
Full suite: 166 files, 3879 passed / 16 skipped / 1 todo (the remaining todo is the pre-existing #1063 eval stub);
tsc --noEmitandeslintclean; re-verified green after mergingmainatf12da69(lockfile-only merge).plan.test.ts/plan-schema.test.ts/system-prompt.test.ts— ten-field brief round-trip, old three-field brief defaults with no errors, bound errors with field paths, wire-schema kinds, prompt guidance — including the completeness contract matching the apply-time gate.mutation-validator.test.ts— policy matrix (close/low-confidence/material uncertainty each block; none required ⇒ no-op) pluschildBriefCompletenessGaps: each required field's absence/null/empty names that field and child index, whitespace-only entries count as absent, emptydependenciesalone is not a reason, one reason per incomplete child.mutation-applier.test.ts— happy path (3 children, marker + backlink bodies, state→section→umbrella ordering, result-label union, state-write-time audit labels), withheld ⇒ no writes, fresh null claim under a foreign key held vs same key recreates, aged-out claim recreates, mid-loop create failure → failed step keeps partial links → immediate same-key retry converges, same briefs under a different application key → zero creates, application-key reorder stability, resumed application replays landed children and surfaces their links, prisma-store child-claim P2002 loser reads the winner; body section: one write with markers + child links preserving human text, enrichment + section coexisting (one application and across applications, byte-for-byte), identical section ⇒ zero body writes, malformed markers or oversized body ⇒ refused (recorded, umbrella still lands),ApplyResult.bodyreal post-apply body, model-authored decomposition markers stripped from enrichment content.run.test.ts— end-to-end: children created withstatus/backlogonly, parentdecomposed/followUpUrls+issue_decomposedaudit + managed decomposition section in the parent body,appliedMutations.childrenCreated/childrenReused/childrenErrorlinks, dry-run creates nothing, exact retry replays with links re-surfaced, partial failure converges on retry, withheld cases — including an incomplete child brief withholding the split end-to-end.evals— the groomer: create bounded child issues idempotently for issues that require decomposition #1066pendingstub inbroad-needs-decompositionis now a real corpus check: children created once, zero duplicates on replay through the shared claim store (with the faithfulupdatedAtfake).