{"record":{"id":"eb26486044860e9b","repo":"gastownhall/beads","slug":"failed-to-prepare-s-v","errorCode":null,"errorMessage":"Failed to prepare %s: %v","messagePattern":"Failed to prepare (.+?): (.+?)","errorType":"console","errorClass":null,"httpStatus":null,"severity":"warning","filePath":"internal/tracker/engine.go","lineNumber":494,"sourceCode":"\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\n\t\tif existing != nil {\n\t\t\t// Conflict-aware pull: skip updating issues that were locally\n\t\t\t// modified since last sync. Conflict detection (Phase 2) will\n\t\t\t// handle these per the configured resolution strategy.\n\t\t\t// Without this guard, pull silently overwrites local changes\n\t\t\t// before conflict detection can compare timestamps.\n\t\t\tif lastSync != nil && existing.UpdatedAt.After(*lastSync) && !allowOverwriteIDs[existing.ID] && !prelinkedHydrateIDs[existing.ID] {\n\t\t\t\tstats.Skipped++\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\n\t\tif e.PullHooks != nil && e.PullHooks.AfterConvert != nil {\n\t\t\tif err := e.PullHooks.AfterConvert(ctx, &extIssue, conv, ref, existing, opts); err != nil {\n\t\t\t\te.warn(\"Failed to prepare %s: %v\", extIssue.Identifier, err)\n\t\t\t\tstats.Skipped++\n\t\t\t\tcontinue\n\t\t\t}\n\t\t}\n\n\t\tpendingDeps = appendFilteredDependencies(pendingDeps, conv.Dependencies, opts.DependencyTypes, opts.DependencySources)\n\t\tif opts.DryRun {\n\t\t\tdryRunIssue := *conv.Issue\n\t\t\tif strings.TrimSpace(ref) != \"\" {\n\t\t\t\tdryRunIssue.ExternalRef = strPtr(ref)\n\t\t\t}\n\t\t\tdryRunIssues = append(dryRunIssues, &dryRunIssue)\n\t\t}\n\n\t\tif existing != nil && pullIssueEqual(existing, conv.Issue, ref) {\n\t\t\tstats.Skipped++\n\t\t\tcontinue\n\t\t}","sourceCodeStart":476,"sourceCodeEnd":512,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/tracker/engine.go#L476-L512","documentation":"In doPull (internal/tracker/engine.go:494), after an external tracker issue is converted to a beads issue, the engine invokes the configurable PullHooks.AfterConvert hook to let the embedding tracker post-process/validate the conversion. If that hook returns an error, the issue is counted as skipped and this warning is printed with the external identifier. It is a per-issue, non-fatal warning issued by `Sync`/`doPull`.","triggerScenarios":"Any `bd pull`/sync run against a tracker whose engine has PullHooks.AfterConvert set (e.g. the GitLab, Jira, or Linear adapters), when the hook's preparation step fails for a specific issue — custom field mapping errors, label/epic lookup failures, hook-level validation rejecting the converted issue, or context cancellation inside the hook.","commonSituations":"Tracker-specific config mistakes (missing custom-field or epic-link mapping); a remote issue has data the hook cannot normalize (unexpected state, missing required field); an embedded tracker whose hook needs extra API calls that fail due to rate limits or auth.","solutions":["Read the `%v` detail in the warning — it comes from the specific tracker's AfterConvert hook and names the failing preparation step.","Fix the tracker configuration the hook depends on (field mappings, label/epic lookups, credentials) and re-run the pull.","Open or repair the offending remote issue so its data satisfies the hook's expectations, then re-pull.","If the hook is custom/embedded, add error context in AfterConvert or return nil to skip preparation for issues it cannot handle.","Use a dry-run pull first to identify all issues that would hit the hook failure."],"exampleFix":"// before (embedded tracker hook)\nfunc (t *Tracker) AfterConvert(ctx context.Context, ext *types.ExtIssue, conv *ConvertedIssue, ...) error {\n    epicID := ext.Metadata[\"epic_id\"].(string) // panics/wrong type -> error\n}\n// after\nfunc (t *Tracker) AfterConvert(ctx context.Context, ext *types.ExtIssue, conv *ConvertedIssue, ...) error {\n    epicID, _ := ext.Metadata[\"epic_id\"].(string)\n    if epicID == \"\" { return nil } // nothing to prepare\n    ...\n}","handlingStrategy":"validation","validationCode":"// before pulling, dry-run to detect issues that would fail AfterConvert:\nbd pull --dry-run   # issues skipped with 'Failed to prepare' show up without side effects\n// for embedded trackers, ensure required remote fields exist before conversion:\nif ext.Metadata[\"epic_id\"] == nil { /* skip preparation path */ }","typeGuard":"func hasEpicMetadata(m map[string]any) (string, bool) {\n    v, ok := m[\"epic_id\"]\n    if !ok { return \"\", false }\n    s, ok := v.(string)\n    return s, ok && s != \"\"\n}","tryCatchPattern":"// per-issue warnings are not returned as errors; inspect them from the wrapper:\nif err := eng.Sync(ctx, opts); err != nil {\n    log.Printf(\"sync aborted: %v\", err)\n}\n// grep stderr for 'Failed to prepare' and re-pull only those issues after fixing config","preventionTips":["Validate tracker field mappings/config before large pulls.","Use --dry-run pulls after changing tracker configuration.","Keep AfterConvert hooks defensive: tolerate missing/odd remote fields.","Fix malformed remote issues at the source tracker."],"tags":["sync","pull","hooks","issue-conversion"],"backgroundTag":"pull-hook-preparation-failed","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}