{"record":{"id":"b090d4532d4e472b","repo":"gastownhall/beads","slug":"no-timeout-set","errorCode":null,"errorMessage":"no timeout set","messagePattern":"no timeout set","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"cmd/bd/gate.go","lineNumber":1166,"sourceCode":"\n\t// Evaluate status\n\tswitch status.State {\n\tcase \"MERGED\":\n\t\treturn true, false, fmt.Sprintf(\"PR '%s' was merged\", status.Title), nil\n\tcase \"CLOSED\":\n\t\treturn false, true, fmt.Sprintf(\"PR '%s' was closed without merging\", status.Title), nil\n\tcase \"OPEN\":\n\t\treturn false, false, fmt.Sprintf(\"PR '%s' is still open\", status.Title), nil\n\tdefault:\n\t\treturn false, false, fmt.Sprintf(\"PR '%s' state: %s\", status.Title, status.State), nil\n\t}\n}\n\n// checkTimer checks a timer gate for expiration\n// Note: timers resolve but never escalate (escalated is always false by design)\nfunc checkTimer(gate *types.Issue, now time.Time) (resolved, escalated bool, reason string, err error) { //nolint:unparam // escalated intentionally always false\n\tif gate.Timeout == 0 {\n\t\treturn false, false, \"timer gate without timeout configured\", fmt.Errorf(\"no timeout set\")\n\t}\n\n\texpiresAt := gate.CreatedAt.Add(gate.Timeout)\n\tif now.After(expiresAt) {\n\t\texpired := now.Sub(expiresAt).Round(time.Second)\n\t\treturn true, false, fmt.Sprintf(\"timer expired %s ago\", expired), nil\n\t}\n\n\tremaining := expiresAt.Sub(now).Round(time.Second)\n\treturn false, false, fmt.Sprintf(\"expires in %s\", remaining), nil\n}\n\n// issueGetter is the one storage method checkBeadGate needs, split out so\n// tests can fake the lookup without standing up a Dolt store.\ntype issueGetter interface {\n\tGetIssue(ctx context.Context, id string) (*types.Issue, error)\n}\n","sourceCodeStart":1148,"sourceCodeEnd":1184,"githubUrl":"https://github.com/gastownhall/beads/blob/71377f276968b452ee607177637970a4ff888584/cmd/bd/gate.go#L1148-L1184","documentation":"Returned by checkTimer when a timer gate has Timeout == 0 (cmd/bd/gate.go:1166). A timer gate without a configured duration can never expire, so evaluation fails with 'no timeout set' and the reason 'timer gate without timeout configured'.","triggerScenarios":"A gate of type timer is created or evaluated with gate.Timeout left at its zero value — e.g. created without a --timeout flag or with a timeout that failed to parse into a duration.","commonSituations":"Manual gate creation forgetting the timeout field, YAML/JSON config omitting 'timeout', a type mismatch causing the duration to silently default to 0, programmatic creation passing an unset time.Duration.","solutions":["Set a timeout on the gate (e.g. `bd update <gate-id>` with a valid duration, or recreate with `--timeout 30m`)","Verify the gate type is really 'timer' — other gate types don't need Timeout","Check creation code/flags so the duration is parsed and assigned before persisting","Validate Timeout > 0 in your gate-creation wrapper before saving"],"exampleFix":"// before\nbd create \"wait for CI\" --type timer   // no timeout\n// after\nbd create \"wait for CI\" --type timer --timeout 30m","handlingStrategy":"validation","validationCode":"if gate.Type == \"timer\" && gate.Timeout <= 0 {\n    return errors.New(\"timer gate requires a positive timeout\")\n}","typeGuard":null,"tryCatchPattern":"if _, _, _, err := checkTimer(gate, time.Now()); err != nil && err.Error() == \"no timeout set\" {\n    // fix the gate config: set --timeout and re-create/update the gate\n}","preventionTips":["Always pass --timeout when creating timer gates","Validate Timeout > 0 in gate-creation helpers","Confirm durations parse correctly (e.g. '30m' not '30')"],"tags":["timer","gate","config","validation"],"backgroundTag":"missing-timeout","analyzedSha":"71377f276968b452ee607177637970a4ff888584","analyzedAt":"2026-08-30T18:55:39.744Z","schemaVersion":2},"datasetVersion":"2026-08-30T23:17:21.991Z"}