{"record":{"id":"1e5a73ff64abbb9e","repo":"gastownhall/beads","slug":"work-item-d-w","errorCode":null,"errorMessage":"work item %d: %w","messagePattern":"work item (.+?): %w","errorType":"exception","errorClass":null,"httpStatus":null,"severity":"error","filePath":"internal/ado/reconcile.go","lineNumber":135,"sourceCode":"\t_, err := r.Client.FetchWorkItems(ctx, []int{id})\n\tif err == nil {\n\t\treturn // Item exists and is accessible\n\t}\n\n\tidStr := strconv.Itoa(id)\n\n\tvar apiErr *APIError\n\tif errors.As(err, &apiErr) {\n\t\tswitch apiErr.StatusCode {\n\t\tcase http.StatusNotFound:\n\t\t\tresult.Deleted = append(result.Deleted, idStr)\n\t\t\treturn\n\t\tcase http.StatusForbidden:\n\t\t\tresult.Denied = append(result.Denied, idStr)\n\t\t\treturn\n\t\t}\n\t}\n\tresult.Errors = append(result.Errors, fmt.Errorf(\"work item %d: %w\", id, err))\n}\n\nfunc (r *Reconciler) getInterval(ctx context.Context) int {\n\tval, err := r.Store.GetConfig(ctx, configReconcileInterval)\n\tif err != nil || val == \"\" {\n\t\treturn DefaultReconcileInterval\n\t}\n\tn, err := strconv.Atoi(val)\n\tif err != nil || n <= 0 {\n\t\treturn DefaultReconcileInterval\n\t}\n\treturn n\n}\n\nfunc (r *Reconciler) getCounter(ctx context.Context) int {\n\tval, err := r.Store.GetConfig(ctx, configSyncsSinceReconcile)\n\tif err != nil || val == \"\" {\n\t\treturn 0","sourceCodeStart":117,"sourceCodeEnd":153,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/internal/ado/reconcile.go#L117-L153","documentation":"Reconciler.checkSingleItem fetches an individual work item to verify it still exists/is accessible; any error that isn't an explicit 404 (Not Found → Deleted list) or 403 (Forbidden → Denied list) is recorded in result.Errors wrapped as 'work item <id>'. Reconcile continues checking other items and aggregates these errors.","triggerScenarios":"GET of the work item during Reconcile fails with anything other than 404/403 — 401 from a bad/expired PAT, 500 from ADO, timeouts, DNS/network errors, or 400 from a malformed work item URL.","commonSituations":"Expired or revoked Azure DevOps PAT mid-sync; ADO service outage/5xx during reconciliation; corporate proxy breaking TLS; project renamed so the configured project no longer resolves.","solutions":["Unwrap the inner error and check its HTTP status; fix auth (rotate PAT) for 401.","Check Azure DevOps service health if the status is 5xx, then re-run reconcile.","Verify ado.org/ado.project configuration still points at the right org/project.","Retry later for transient network errors — reconcile is safe to re-run."],"exampleFix":"// before: every non-404/403 error is just collected\nresult.Errors = append(result.Errors, fmt.Errorf(\"work item %d: %w\", id, err))\n// after: classify auth errors for clearer reporting\nvar apiErr *APIError\nif errors.As(err, &apiErr) && apiErr.StatusCode == http.StatusUnauthorized {\n    result.Errors = append(result.Errors, fmt.Errorf(\"work item %d: authentication failed, check ado.pat/AZURE_DEVOPS_PAT: %w\", id, err))\n    return\n}\nresult.Errors = append(result.Errors, fmt.Errorf(\"work item %d: %w\", id, err))","handlingStrategy":"try-catch","validationCode":"// Pre-validate config/auth before reconcile\nif os.Getenv(\"AZURE_DEVOPS_PAT\") == \"\" && patConfigEmpty {\n    return fmt.Errorf(\"PAT not configured; reconcile would fail on every item\")\n}","typeGuard":"func isAuthError(err error) bool {\n    var apiErr *APIError\n    return errors.As(err, &apiErr) &&\n        (apiErr.StatusCode == http.StatusUnauthorized || apiErr.StatusCode == http.StatusForbidden)\n}","tryCatchPattern":"result := reconciler.Reconcile(ctx)\nfor _, err := range result.Errors {\n    if isAuthError(err) {\n        return fmt.Errorf(\"reconcile aborted: fix credentials: %w\", err)\n    }\n    log.Printf(\"item-level reconcile issue (continuing): %v\", err)\n}","preventionTips":["Rotate the PAT before expiry and update AZURE_DEVOPS_PAT in CI secrets.","Monitor Azure DevOps service health during scheduled reconcile windows.","Verify org/project config after any project rename.","Schedule reconciles with retry/backoff to ride out transient 5xx."],"tags":["azure-devops","reconcile","http-error","network"],"backgroundTag":"http-request-failed","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}