{"record":{"id":"e38f967dd8484a01","repo":"gastownhall/beads","slug":"w-s-is-blocked-by-v","errorCode":null,"errorMessage":"%w: %s is blocked by %v","messagePattern":"%w: (.+?) is blocked by (.+?)","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/storage/issueops/close.go","lineNumber":159,"sourceCode":"\t// cell, while a close writes both tiers, so Dolt cannot merge a stale count\n\t// with a new edge.\n\tif err := touchDependencyCoordinationInTx(ctx, tx, id); err != nil {\n\t\treturn 0, err\n\t}\n\topenChildren, err := countOpenChildrenForTargetInTx(ctx, tx, id, targetColumn)\n\tif err != nil {\n\t\treturn 0, err\n\t}\n\tif !force && openChildren > 0 {\n\t\treturn 0, &storage.CloseOpenChildrenError{IssueID: id, OpenChildren: openChildren}\n\t}\n\tif !force && !closed {\n\t\tblocked, blockers, err := IsBlockedInTx(ctx, tx, id)\n\t\tif err != nil {\n\t\t\treturn 0, err\n\t\t}\n\t\tif blocked && len(blockers) > 0 {\n\t\t\treturn 0, fmt.Errorf(\"%w: %s is blocked by %v\", storage.ErrCloseBlocked, id, blockers)\n\t\t}\n\t}\n\treturn openChildren, nil\n}\n\n// EnforceClosePolicyInTx applies the close policy to id without closing it, for\n// a caller that reaches a done status by some other write. It routes the target\n// exactly as a checked close does — probing issues then wisps by ID, preferring\n// the durable row when both hold it — rather than deriving the table from an\n// is-wisp flag, so a dual-resident ID counts children against the same row the\n// close guard would.\n//\n// It returns the observed open-child count. force bypasses both refusals and\n// nothing else; the coordination-cell touch happens either way.\nfunc EnforceClosePolicyInTx(ctx context.Context, tx DBTX, id string, force bool) (int, error) {\n\tclosed, targetColumn, found, err := isClosedInTx(ctx, tx, id)\n\tif err != nil {\n\t\treturn 0, err","sourceCodeStart":141,"sourceCodeEnd":177,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/storage/issueops/close.go#L141-L177","documentation":"The close policy refuses to close an issue that is not already closed when the denormalized is_blocked flag is set AND there is at least one live, open direct blocker (blocks/waits-for/conditional-blocks). The error wraps storage.ErrCloseBlocked and lists the blocking issue ids. Force bypasses this refusal (but not the version check). A bare is_blocked=1 with no live direct blocker is deliberately allowed — the live blocker list self-heals stale flags.","triggerScenarios":"bd close (or CloseIssueCheckedInTx / EnforceClosePolicyInTx) without force on an open issue that has at least one open blocker dependency; also triggered for a status change that reaches done via updateIssueInTx through EnforceClosePolicyInTx.","commonSituations":"Trying to close a parent task whose child still blocks it; workflow automation attempting to close work while dependencies remain open; scripts that close in bulk without ordering by dependency.","solutions":["Close or resolve the listed blocker issues first, then retry the close","Re-run with force=true if the blockers are known-stale or intentionally being overridden","Recompute blocked state (closing a blocker triggers RecomputeIsBlocked) if the blocker list looks wrong","Check dependency direction: a wrongly-added blocks edge can be removed so the target is genuinely unblocked"],"exampleFix":"err := store.CloseIssue(ctx, id, reason, actor)\nvar blocked *storage.CloseBlockedError\nif errors.As(err, &blocked) {\n\t// close blockers first, or override deliberately\n\tfor _, b := range blockers {\n\t\t_ = store.CloseIssue(ctx, b, \"unblocking \"+id, actor)\n\t}\n\terr = store.CloseIssue(ctx, id, reason, actor)\n}","handlingStrategy":"validation","validationCode":"blocked, blockers, err := store.IsBlocked(ctx, id)\nif err != nil {\n\treturn err\n}\nif blocked && len(blockers) > 0 && !force {\n\treturn fmt.Errorf(\"%s must be unblocked first: %v\", id, blockers)\n}","typeGuard":null,"tryCatchPattern":"err := store.CloseIssue(ctx, id, reason, actor)\nvar cbErr *storage.CloseBlockedError\nif errors.As(err, &cbErr) {\n\tfor _, b := range cbErr.Blockers {\n\t\tif err := closeBlocker(ctx, b); err != nil { return err }\n\t}\n\terr = store.CloseIssue(ctx, id, reason, actor)\n}","preventionTips":["Close work in dependency order (leaves before parents)","Check is_blocked/blockers before scripting bulk closes","Use force only for deliberate overrides of known-stale blockers","Recompute blocked state after removing dependency edges"],"tags":["dependencies","close-policy","blocked"],"backgroundTag":"issue-blocked-cannot-close","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}