gfcd2ih4eu7m21eqd6a27jubr4fu2bsc · 506 rows
| id | issue_id | event_type | actor | old_value | new_value | comment | created_at |
|---|---|---|---|---|---|---|---|
| 019f7cdf-2e5c-74fd-bf26-105aacf83db2 | ah-efe | label_removed | Eugene Blikh | NULL | NULL | Removed label: milestone:v0.1.0 | 2026-07-20T03:13:49Z |
| 019f7cdf-2f63-79f6-8bb2-265a57ced2ee | ah-efe | label_added | Eugene Blikh | NULL | NULL | Added label: milestone:v0.2.0 | 2026-07-20T03:13:49Z |
| 019f7cdf-6fba-75e7-965f-79478aa1b878 | ah-1cx | label_removed | Eugene Blikh | NULL | NULL | Removed label: milestone:v0.1.0 | 2026-07-20T03:14:05Z |
| 019f7cdf-70c3-7ecd-8e47-c24bfda0df97 | ah-efe | label_removed | Eugene Blikh | NULL | NULL | Removed label: milestone:v0.2.0 | 2026-07-20T03:14:06Z |
| 019f7cea-e517-7557-9476-bacfa9af34c6 | ah-wd4 | created | Eugene Blikh | NULL | 2026-07-20T03:26:36Z | ||
| 019f7cf1-3477-79bc-be78-cf15a6ec2977 | ah-wd4 | updated | Eugene Blikh | {"id":"ah-wd4","title":"DECISION: move the task board from Vikunja to beads+Dolt (full swap vs hybrid mirror)","description":"Three independent Opus researchers audited this on 2026-07-20 (board contract / beads capabilities / human workflow). Consensus: writing the adapter is the EASY part; the cost is infrastructure and the human write path.\n\nFEASIBILITY (good news): ports.Board is only 6 methods (Snapshot, MoveToBucket, Comment, Comments, CreateTask, SwapLabel), 22 call sites all in internal/reconcile, and everything else (attempts, runs, lineage, Q\u0026A links) lives in the SQLite store. domain/ is Vikunja-free; bucket names are domain constants. Adapter est. a few hundred lines, minus the 238-line markdown-\u003eHTML converter which a plain-text board does not need. All 9 canonical buckets ARE expressible via beads custom statuses with active/wip/done/frozen categories, wired into the ready_issues view at SQL level. bd ready --claim is a real compare-and-swap (ErrAlreadyClaimed) — stronger than what we have today. Comments live in their own table, NOT mixed with the events audit log, so the ask-user answer detector is safe from machine-generated lines.\n\nBLOCKERS (the real cost):\n1. NO Go library (all packages are internal/), no MCP, no HTTP daemon. The only interfaces are fork/exec of bd --json (~250ms warm) or raw MySQL to a dolt sql-server.\n2. Embedded mode is single-process and BLOCKS UNBOUNDEDLY — measured bd count waiting 43s behind a 40s external DB hold, no timeout knob. Upstream design doc calls multi-process embedded 'unsupported'. Migrating to dolt sql-server mode is MANDATORY (backup + bd init --server + restore; different data dir; a new server process to supervise).\n3. Human write path. Vikunja is the human INPUT surface, not just storage; the viewer is read-only. The killer interaction is ask-user: a card parked in Question (or a live agent polling /api/tool/answer against a 30m timeout) waits on a human comment. Today that is typed from any device in \u003c=20s; under beads it is laptop-only bd comment behind a ~5min auto-push debounce (~17% of a run timeout per exchange). Lowering the interval does not fix the failure CLASS: a local write that reports success and is invisible to the daemon.\n4. Every write is a Dolt commit — a comment per state change plus per-heartbeat progress = write amplification into a version-controlled DAG that auto-pushes. bd batch help names this; bd compact/gc/flatten are the cleanup treadmill.\n5. int64 task IDs are load-bearing (branch task-\u003cid\u003e, zellij session, archive filename, tool-token binding, HTTP API, notification URL). Beads ids are strings (ah-1cx.1). Recommended fix: repo-wide int64-\u003estring (mechanical, compiler-verified, ~10 files) over a synthetic mapping table that can drift.\n6. Snapshot must NEVER be partial: a card missing from a snapshot is treated as vanished and the daemon KILLS the live run and cancels the record. Any adapter must enumerate transactionally or prove completeness (the Vikunja adapter refuses a truncated bucket rather than dropping tasks). Watch bd list default limits.\n7. Same-field concurrent updates are last-writer-wins with no optimistic locking (upstream open question #3); only --claim has CAS. Contradicts the SPEC principle that human intent wins and the daemon aligns.\n8. No change notification, by product charter ('Beads does not need sub-second sync'). Poll the events table by created_at. NOTE: this is NOT a real blocker for us — the Vikunja webhook is explicitly only an acceleration of the 20s poll, and agent-completion latency rides the separate run-exit poke.\n\nWHAT IMPROVES: Task Spec escapes the rich-text editor (plainTextFromHTML exists ONLY to undo Vikunja HTML mangling of YAML frontmatter — becomes deletable); first-class deferred/--defer beats an unmapped Someday column; dependency-aware bd ready for free; agent and human share one tracker beside the code; full history/diff/branching. Attachments are a non-issue (zero code references).\n\nOPTIONS:\nA) HYBRID MIRROR (low risk): keep Vikunja as the board, add a one-way exporter into beads for reporting/milestones. No reconcile changes at all. Note the milestone viewer at dolt.srht.bigb.es/~bigbes/agents-dev/view/milestones already delivers most of this value today.\nB) FULL SWAP, gated on prerequisites: migrate beads to dolt sql-server mode; daemon speaks MySQL directly (or bd --json --sandbox with pushes on its own timer); map ready-\u003estatus open (REQUIRED: the CAS claim hardcodes status='open'); use metadata JSON for daemon-private state; events.created_at as poll cursor; batch comment writes + scheduled compaction; int64-\u003estring ids; AND build a human write path — 3 POST endpoints on the existing httpapi (comment / status / label) reusing the proven bearer-token pattern, plus a small form in the viewer. Because the daemon runs on agent-1 next to the authoritative working set, writes through it have ZERO sync latency and take the laptop out of the write path entirely.\n\nRECOMMENDATION: do not swap while the viewer is read-only. Either stay on Vikunja, or commit to option B including the write path — the write path is what makes it viable, not the adapter. Implementation beads to be filed once this decision is made.","notes":"Researcher evidence is summarized here rather than linked; the three reports were transcript-only. Key measured facts to re-verify before acting: bd 1.1.0 embedded blocking (43s observed), bd has no non-internal Go packages, claim.go hardcodes status='open' in the CAS UPDATE, auto-push debounce default 5m / timeout 30s.","status":"open","priority":3,"issue_type":"decision","owner":"bigbes@gmail.com","created_at":"2026-07-20T00:26:37Z","created_by":"Eugene Blikh","updated_at":"2026-07-20T00:26:37Z"} | {"notes":"CORRECTION 2026-07-20 (verified against the v1.1.0 source zip from proxy.golang.org): blocker #1 'NO Go library' is WRONG. github.com/steveyegge/beads has a root package beads.go documented as 'a minimal public API for extending bd with custom orchestration', MIT licensed. It re-exports the internal layer via type aliases (Storage, Transaction, RemoteStore, SyncStore, Issue, Comment, Event, IssueFilter, WorkFilter, status/type constants) and exposes Open(ctx, dbPath), OpenFromConfig(ctx, beadsDir), FindBeadsDir, FindDatabasePath.\n\nThe Storage interface covers the Board port almost 1:1: SearchIssues/GetReadyWork -\u003e Snapshot; UpdateIssue(id, {status}) -\u003e MoveToBucket; AddIssueComment/GetIssueComments (typed, ordered) -\u003e Comment/Comments; CreateIssue -\u003e CreateTask; AddLabel+RemoveLabel -\u003e SwapLabel.\n\nThree risks in the description are downgraded by this API:\n- Write amplification: RunInTransaction(ctx, commitMsg, fn) batches many writes into ONE Dolt commit, rolls back on error or panic. Also makes SwapLabel ATOMIC — better than the current Vikunja adapter, which documents a deliberate non-atomic add-then-remove.\n- Change notification: GetAllEventsSince(ctx, since time.Time) is a typed poll cursor; no hand-rolled SQL over the events table needed.\n- Embedded-vs-server: OpenFromConfig respects dolt_mode in metadata.json, so switching is configuration, not code, and the daemon holds the connection instead of fork/exec-ing a ~250ms CLI. RemoteStore (via type assertion) exposes Push/Pull so the daemon controls push timing itself rather than inheriting the 5-min auto-push debounce.\n\nSTILL TO VERIFY before relying on it: which call enumerates ALL issues for Snapshot (there is no plain ListIssues — likely SearchIssues with an empty query + IssueFilter) and whether IssueFilter applies a default limit. A silently truncated snapshot makes the daemon treat missing cards as vanished and KILL live runs, so this needs an explicit completeness guarantee.\n\nUNCHANGED: the human write path is still the real blocker, and the recommendation stands — do not swap while the viewer is read-only."} | NULL | 2026-07-20T03:33:30Z |
| 019f7e7f-0ba7-75a1-bef6-9198f4bf61ad | ah-wd4 | updated | Eugene Blikh | {"id":"ah-wd4","title":"DECISION: move the task board from Vikunja to beads+Dolt (full swap vs hybrid mirror)","description":"Three independent Opus researchers audited this on 2026-07-20 (board contract / beads capabilities / human workflow). Consensus: writing the adapter is the EASY part; the cost is infrastructure and the human write path.\n\nFEASIBILITY (good news): ports.Board is only 6 methods (Snapshot, MoveToBucket, Comment, Comments, CreateTask, SwapLabel), 22 call sites all in internal/reconcile, and everything else (attempts, runs, lineage, Q\u0026A links) lives in the SQLite store. domain/ is Vikunja-free; bucket names are domain constants. Adapter est. a few hundred lines, minus the 238-line markdown-\u003eHTML converter which a plain-text board does not need. All 9 canonical buckets ARE expressible via beads custom statuses with active/wip/done/frozen categories, wired into the ready_issues view at SQL level. bd ready --claim is a real compare-and-swap (ErrAlreadyClaimed) — stronger than what we have today. Comments live in their own table, NOT mixed with the events audit log, so the ask-user answer detector is safe from machine-generated lines.\n\nBLOCKERS (the real cost):\n1. NO Go library (all packages are internal/), no MCP, no HTTP daemon. The only interfaces are fork/exec of bd --json (~250ms warm) or raw MySQL to a dolt sql-server.\n2. Embedded mode is single-process and BLOCKS UNBOUNDEDLY — measured bd count waiting 43s behind a 40s external DB hold, no timeout knob. Upstream design doc calls multi-process embedded 'unsupported'. Migrating to dolt sql-server mode is MANDATORY (backup + bd init --server + restore; different data dir; a new server process to supervise).\n3. Human write path. Vikunja is the human INPUT surface, not just storage; the viewer is read-only. The killer interaction is ask-user: a card parked in Question (or a live agent polling /api/tool/answer against a 30m timeout) waits on a human comment. Today that is typed from any device in \u003c=20s; under beads it is laptop-only bd comment behind a ~5min auto-push debounce (~17% of a run timeout per exchange). Lowering the interval does not fix the failure CLASS: a local write that reports success and is invisible to the daemon.\n4. Every write is a Dolt commit — a comment per state change plus per-heartbeat progress = write amplification into a version-controlled DAG that auto-pushes. bd batch help names this; bd compact/gc/flatten are the cleanup treadmill.\n5. int64 task IDs are load-bearing (branch task-\u003cid\u003e, zellij session, archive filename, tool-token binding, HTTP API, notification URL). Beads ids are strings (ah-1cx.1). Recommended fix: repo-wide int64-\u003estring (mechanical, compiler-verified, ~10 files) over a synthetic mapping table that can drift.\n6. Snapshot must NEVER be partial: a card missing from a snapshot is treated as vanished and the daemon KILLS the live run and cancels the record. Any adapter must enumerate transactionally or prove completeness (the Vikunja adapter refuses a truncated bucket rather than dropping tasks). Watch bd list default limits.\n7. Same-field concurrent updates are last-writer-wins with no optimistic locking (upstream open question #3); only --claim has CAS. Contradicts the SPEC principle that human intent wins and the daemon aligns.\n8. No change notification, by product charter ('Beads does not need sub-second sync'). Poll the events table by created_at. NOTE: this is NOT a real blocker for us — the Vikunja webhook is explicitly only an acceleration of the 20s poll, and agent-completion latency rides the separate run-exit poke.\n\nWHAT IMPROVES: Task Spec escapes the rich-text editor (plainTextFromHTML exists ONLY to undo Vikunja HTML mangling of YAML frontmatter — becomes deletable); first-class deferred/--defer beats an unmapped Someday column; dependency-aware bd ready for free; agent and human share one tracker beside the code; full history/diff/branching. Attachments are a non-issue (zero code references).\n\nOPTIONS:\nA) HYBRID MIRROR (low risk): keep Vikunja as the board, add a one-way exporter into beads for reporting/milestones. No reconcile changes at all. Note the milestone viewer at dolt.srht.bigb.es/~bigbes/agents-dev/view/milestones already delivers most of this value today.\nB) FULL SWAP, gated on prerequisites: migrate beads to dolt sql-server mode; daemon speaks MySQL directly (or bd --json --sandbox with pushes on its own timer); map ready-\u003estatus open (REQUIRED: the CAS claim hardcodes status='open'); use metadata JSON for daemon-private state; events.created_at as poll cursor; batch comment writes + scheduled compaction; int64-\u003estring ids; AND build a human write path — 3 POST endpoints on the existing httpapi (comment / status / label) reusing the proven bearer-token pattern, plus a small form in the viewer. Because the daemon runs on agent-1 next to the authoritative working set, writes through it have ZERO sync latency and take the laptop out of the write path entirely.\n\nRECOMMENDATION: do not swap while the viewer is read-only. Either stay on Vikunja, or commit to option B including the write path — the write path is what makes it viable, not the adapter. Implementation beads to be filed once this decision is made.","notes":"CORRECTION 2026-07-20 (verified against the v1.1.0 source zip from proxy.golang.org): blocker #1 'NO Go library' is WRONG. github.com/steveyegge/beads has a root package beads.go documented as 'a minimal public API for extending bd with custom orchestration', MIT licensed. It re-exports the internal layer via type aliases (Storage, Transaction, RemoteStore, SyncStore, Issue, Comment, Event, IssueFilter, WorkFilter, status/type constants) and exposes Open(ctx, dbPath), OpenFromConfig(ctx, beadsDir), FindBeadsDir, FindDatabasePath.\n\nThe Storage interface covers the Board port almost 1:1: SearchIssues/GetReadyWork -\u003e Snapshot; UpdateIssue(id, {status}) -\u003e MoveToBucket; AddIssueComment/GetIssueComments (typed, ordered) -\u003e Comment/Comments; CreateIssue -\u003e CreateTask; AddLabel+RemoveLabel -\u003e SwapLabel.\n\nThree risks in the description are downgraded by this API:\n- Write amplification: RunInTransaction(ctx, commitMsg, fn) batches many writes into ONE Dolt commit, rolls back on error or panic. Also makes SwapLabel ATOMIC — better than the current Vikunja adapter, which documents a deliberate non-atomic add-then-remove.\n- Change notification: GetAllEventsSince(ctx, since time.Time) is a typed poll cursor; no hand-rolled SQL over the events table needed.\n- Embedded-vs-server: OpenFromConfig respects dolt_mode in metadata.json, so switching is configuration, not code, and the daemon holds the connection instead of fork/exec-ing a ~250ms CLI. RemoteStore (via type assertion) exposes Push/Pull so the daemon controls push timing itself rather than inheriting the 5-min auto-push debounce.\n\nSTILL TO VERIFY before relying on it: which call enumerates ALL issues for Snapshot (there is no plain ListIssues — likely SearchIssues with an empty query + IssueFilter) and whether IssueFilter applies a default limit. A silently truncated snapshot makes the daemon treat missing cards as vanished and KILL live runs, so this needs an explicit completeness guarantee.\n\nUNCHANGED: the human write path is still the real blocker, and the recommendation stands — do not swap while the viewer is read-only.","status":"open","priority":3,"issue_type":"decision","owner":"bigbes@gmail.com","created_at":"2026-07-20T00:26:37Z","created_by":"Eugene Blikh","updated_at":"2026-07-20T00:33:30Z"} | {"design":"HANDOFF DESIGN — human write path for a beads-backed board. Written 2026-07-20 for a worker with\nno prior conversation context. Read this whole section before touching code.\n\n== WHY THIS EXISTS ==\nVikunja is not merely storage for agenthubd: it is the surface through which a HUMAN expresses\nintent, which the daemon then observes. SPEC.md:36 and :41-43 state the contract — the board is\ndesired state owned by the human, the SQLite store plus runtime is actual state owned by the\ndaemon, and \"the daemon never fights a human drag: human intent wins\". Replacing the board with\nbeads+Dolt while the only web UI is READ-ONLY removes the human's write surface. That is the sole\nremaining blocker to the migration; everything else is tractable work (see the description).\n\n== THE COMPLETE HUMAN INTENT VOCABULARY (do not add verbs beyond these without re-deriving) ==\nEvery human gesture the daemon can observe reduces to four writes. Evidence is by interaction:\n\n1. COMMENT — the only latency-critical write. Two cases:\n a. Card parked in Question: reconcile.go:1151 handleQuestion -\u003e Comments() -\u003e tools.go:263-278\n detectAnswer. It finds the LAST comment containing marker \"agent-hub:awaiting-answer\"\n (reconcile.go:1105); if ANY comment follows it, that trailing comment IS the answer.\n b. Mid-run: tools.go:210-238 ToolAskUser parks the card while the agent stays LIVE polling\n GET /api/tool/answer (tools.go:245-252). The run is burning against cfg.Timeout (default 30m,\n enforced in check()). Delivery latency here is on the critical path of a running agent.\n CONSEQUENCE: any write path slower than ~1 min materially degrades (b). A 5-minute\n auto-push debounce consumes ~17% of a default run budget per exchange.\n\n2. STATUS CHANGE — highest-frequency gesture; five human actions share this one operation:\n trigger work (drag to Ready -\u003e reconcile.go:234-235 handleReady + domain.CanClaim);\n promote an agent-created task out of Triage (created by tools.go:344-350 into\n cfg.AgentTasks.TargetBucket, default triage; Triage is a PARKED bucket, reconcile.go:238-244 —\n never a claim source); cancel/kill (reconcile.go:254-256 handleTerminal:1525-1560 -\u003e runner.Kill,\n outcome killed); route from In Review; park out of the way (unmapped bucket, reconcile.go:228-232).\n\n3. CREATE TASK — title + description + initial status. The description carries the Task Spec YAML\n frontmatter (role/model/skills/timeout), parsed by internal/spec/frontmatter.go:36-58.\n\n4. EDIT DESCRIPTION / LABELS — easy to under-rate. When the daemon REJECTS a Task Spec it bounces\n the card to Triage (reconcile.go:380) with a comment that literally instructs: \"Fix the Task\n Spec in the description, then drag the card back to Ready\" (comments.go:51-59). Without an edit\n path a rejected card is unrecoverable from any device that lacks the bd CLI. Labels are the same\n operation class: the type:\u003cname\u003e label selects the task-type preset (spec.go:194-217) and is read\n by verdict routing (routing.go:23-42). Exactly one type:* label is legal.\n\nNOT needed: assignees, priorities, due dates, attachments, ordering, reactions. The daemon reads\nnone of them (grep -rni attachment internal/ returns zero hits). BoardTask carries only\n{ID, Title, Description, Bucket, Labels, UpdatedAt} and UpdatedAt has zero readers.\n\n== TWO DELIVERY SHAPES — evaluate SHAPE A FIRST, it may be nearly free ==\n\nSHAPE A: one shared Dolt sql-server; no new code.\n Run dolt sql-server on agent-1 beside the daemon; point every bd client at it over the network.\n bd supports this explicitly: 'bd dolt set host \u003cip\u003e [--update-config]', plus port/user/database,\n BEADS_DOLT_SERVER_MODE=1, bd init --server (see bd dolt --help; docs/DOLT.md in the beads source\n says server mode \"connects to a running dolt sql-server for multi-client access ... enables\n concurrent agents\"). With ONE database there is no push, no pull, no debounce, no divergence and\n no merge conflicts. Solves every desk interaction at ~zero engineering cost.\n DOES NOT solve: any device without bd + network access to the server (i.e. phone).\n Costs: a supervised sql-server process; network exposure of the DB port; migration from embedded\n to server mode is backup + 'bd init --server' + restore with a DIFFERENT data dir\n (.beads/dolt/ vs .beads/embeddeddolt/) — not a flag flip.\n\nSHAPE B: HTTP write endpoints on the daemon's existing httpapi.\n POST /api/v1/board/:id/comment {\"text\": \"...\"} -\u003e interaction 1 (DO FIRST)\n POST /api/v1/board/:id/status {\"status\": \"open\"} -\u003e interaction 2 (DO SECOND)\n POST /api/v1/board {\"title\",\"description\",\"status\"} -\u003e interaction 3\n PATCH /api/v1/board/:id {\"description\",\"labels\"} -\u003e interaction 4\n Why the daemon and not the viewer: the daemon runs ON agent-1 next to the authoritative Dolt\n working set, so a write through it has ZERO sync latency — it mutates the DB the reconciler reads\n and pushes on the daemon's own schedule. This takes the laptop out of the write path, which is\n what eliminates the failure CLASS (a local write that reports success and is invisible to the\n daemon). Lowering the auto-push interval only narrows the window; it does not remove the class.\n MINIMUM VIABLE SLICE = comment + status. Those two cover the blocker and the highest-frequency\n gesture. Create/edit can lag because filing new work is a desk activity anyway.\n\n== NON-OBVIOUS COSTS OF SHAPE B (largest hidden cost; read before estimating) ==\n- The daemon is LOOPBACK-ONLY today: config.example.yaml line 1, listen: \"127.0.0.1:9100\".\n A human-facing write API means binding off-loopback, which drags in TLS and a real auth story.\n- Auth machinery to REUSE, not reinvent: internal/reconcile/tools.go:60-137 mints per-task 256-bit\n bearer tokens with a constant-time compare; internal/httpapi/httpapi.go:311-353 does HMAC-SHA256\n verification for the Vikunja webhook. What is genuinely NEW is an OPERATOR token with a different\n lifetime and scope than a per-run token. Do not reuse per-task tokens for humans.\n- TWO WRITE PATHS CAN DIVERGE: if the laptop keeps writing a LOCAL Dolt DB while the HTTP API writes\n agent-1's, the merge problem returns. Shape A avoids this by construction. If shipping B alone,\n point the laptop's bd at agent-1 as well, or consciously accept Dolt merges.\n- Viewer integration: wire the existing read-only viewer's issue rows to POST at these endpoints\n (https://dolt.srht.bigb.es/~bigbes/agents-dev/view/milestones). It already renders id/title/\n priority/type/status and milestone progress; it is a sourcehut-style page with Log in/Register\n in the nav, so an auth context may already exist there.\n\n== INVARIANTS ANY IMPLEMENTATION MUST NOT BREAK ==\n1. NEVER return a partial board from Snapshot. A card missing from a snapshot is treated as VANISHED:\n reconcile.go:1659 handleVanished KILLS the live run and marks the record cancelled. The Vikunja\n adapter refuses a truncated bucket rather than dropping tasks (vikunja/board.go:95-99). See the\n open spike on SearchIssues/IssueFilter default limits — this is the gating unknown.\n2. Do NOT let machine-generated audit lines into the COMMENT stream. detectAnswer takes the LAST\n comment unconditionally, so an injected \"status changed to X\" line would be consumed as the\n human's answer. Beads keeps comments in their own table separate from the events audit log, so\n this is currently safe — preserve that separation.\n3. Move-then-comment, never comment-then-move (reconcile.go:376-379, :413-414, :1371-1373, :1494-1496).\n Only a successful move earns a comment, so a persistently failing move cannot spam one comment\n per tick.\n4. Persist-before-move; never assume a write landed and never re-read to confirm. Every failed move\n converges on a later tick (heal branch reconcile.go:286-299, alignCardToRecord:1436). This\n tolerance is what makes a non-transactional board safe.\n5. 'ready' MUST map to beads status 'open'. The atomic claim CAS hardcodes it:\n internal/storage/issueops/claim.go:47-58 UPDATE ... WHERE id=? AND status='open'. A custom\n 'ready:active' status would appear in bd ready but would NOT be claimable.\n\n== BEADS PUBLIC API (use it; do NOT import internal/ and do NOT shell out to the CLI) ==\nRoot package github.com/steveyegge/beads (MIT). Verified against the v1.1.0 source zip.\n Open(ctx, dbPath) / OpenFromConfig(ctx, beadsDir) -- the latter respects dolt_mode in\n metadata.json, so embedded-vs-server is CONFIGURATION not code.\n Storage interface maps ~1:1 to ports.Board:\n SearchIssues / GetReadyWork -\u003e Snapshot (SEE SPIKE: default limit unverified)\n UpdateIssue(id, {\"status\": ...}) -\u003e MoveToBucket\n AddIssueComment / GetIssueComments (typed, ordered) -\u003e Comment / Comments\n CreateIssue -\u003e CreateTask\n AddLabel + RemoveLabel -\u003e SwapLabel\n RunInTransaction(ctx, commitMsg, fn) batches many writes into ONE Dolt commit with rollback on\n error or panic — use it to (a) kill write amplification and (b) make SwapLabel ATOMIC, which is\n strictly better than the current Vikunja adapter's documented non-atomic add-then-remove.\n GetAllEventsSince(ctx, since time.Time) is a typed change-feed cursor — no hand-rolled SQL.\n RemoteStore (via type assertion) exposes Push/Pull so the daemon controls push timing itself.\nEscape hatches if the public API ever falls short, in order of preference: direct SQL (bd's own docs\nrecommend this for extensions); vendor the MIT-licensed code; a shim module declared under\ngithub.com/steveyegge/beads/\u003cx\u003e plus a replace directive (Go's internal rule is a path-prefix check\non the IMPORTING package path, so this legally compiles). internal/ carries no compat guarantees."} | NULL | 2026-07-20T10:48:03Z |
| 019f7e7f-5c6c-7bfe-bdf4-90566080bcc6 | ah-wd4.1 | created | Eugene Blikh | NULL | 2026-07-20T10:48:23Z | ||
| 019fcf18-22d8-7dda-83c2-527acdc041f0 | ah-tqc | claimed | Eugene Blikh | {"id":"ah-tqc","title":"Runner: an errored-final-turn pi run (exit 0, zero tokens) finalizes as success","description":"Live incident 2026-07-19 (task 4, Q\u0026A smoke): pi --mode json made exactly one model call, the litellm proxy 403'd it (key not allowed for model 'coder'), pi recorded stopReason:error with zero usage and EXITED 0 in ~0.87s. The daemon trusts the exit code: outcome=success, card advanced to in_review, Q\u0026A delivery then correctly reported 'no answer.json' — a broken model config masquerades as a successful run. Fix direction: Status/finalize (or ahub-run) should inspect the tail of events.jsonl — a run whose final assistant turn has stopReason:error (or whose agent_end follows zero completed tool/text turns) should finalize as outcome=error regardless of exit code. Second finding to fold in: /api/v1/status shows cost_usd=0 for ALL runs including real multi-minute ones (tasks 1-2), so the usage.cost.total accumulation from events.jsonl appears broken on pi 0.73.x — re-verify the event shape and fix the cost parse.","status":"open","priority":2,"issue_type":"bug","owner":"bigbes@gmail.com","created_at":"2026-07-19T05:38:11Z","created_by":"Eugene Blikh","updated_at":"2026-07-19T05:38:11Z","labels":["milestone:v0.1.0"]} | {"assignee":"Eugene Blikh","status":"in_progress"} | NULL | 2026-08-05T02:24:53Z |
| 019fcf1e-9c53-7036-9f0d-4a4bfa72b31a | ah-tqc | updated | Eugene Blikh | {"id":"ah-tqc","title":"Runner: an errored-final-turn pi run (exit 0, zero tokens) finalizes as success","description":"Live incident 2026-07-19 (task 4, Q\u0026A smoke): pi --mode json made exactly one model call, the litellm proxy 403'd it (key not allowed for model 'coder'), pi recorded stopReason:error with zero usage and EXITED 0 in ~0.87s. The daemon trusts the exit code: outcome=success, card advanced to in_review, Q\u0026A delivery then correctly reported 'no answer.json' — a broken model config masquerades as a successful run. Fix direction: Status/finalize (or ahub-run) should inspect the tail of events.jsonl — a run whose final assistant turn has stopReason:error (or whose agent_end follows zero completed tool/text turns) should finalize as outcome=error regardless of exit code. Second finding to fold in: /api/v1/status shows cost_usd=0 for ALL runs including real multi-minute ones (tasks 1-2), so the usage.cost.total accumulation from events.jsonl appears broken on pi 0.73.x — re-verify the event shape and fix the cost parse.","status":"in_progress","priority":2,"issue_type":"bug","assignee":"Eugene Blikh","owner":"bigbes@gmail.com","created_at":"2026-07-19T05:38:11Z","created_by":"Eugene Blikh","updated_at":"2026-08-04T23:24:53Z","started_at":"2026-08-04T23:24:53Z","labels":["milestone:v0.1.0"]} | {"notes":"FINDING 1 (errored-final-turn ⇒ false success) FIXED in 665e805. Root cause confirmed as filed: applyExit mapped exit.json's code straight to the §9 outcome, and pi exits 0 even when its only assistant turn was rejected by the provider (stopReason 'error', zero usage). Fix: ParseEvents now also records the LAST assistant message_end's stopReason/errorMessage plus an assistant-message count, exposed as EventStreamInfo.Errored(); applyExit takes the parsed stream and maps exit 0 + Errored() to domain.OutcomeError while still recording the true exit code on the run row. The stream is POSITIVE evidence only — an absent, unreadable, or assistant-turn-less events.jsonl leaves the exit code's verdict alone, so a genuine success can never be flipped by a missing file. Both Status completion branches were factored into (*PiZellij).finalizeFromExit so the ah-2ef re-read and the ah-wka grace-clear can no longer drift apart between them; it WARNs with the provider's rejection text whenever it overrules a zero exit. SPEC §9 'Status resolution' updated to match. Re-verified against a REAL pi 0.82.1 capture that the pinned message_end shape (message.usage.cost.total, message.stopReason, message.errorMessage) is unchanged since the 0.70.2 fixtures.\n\nFINDING 2 (cost_usd=0 on every live run) STILL OPEN. Ruled out: the parser. A real pi 0.82.1 run against the direct deepseek provider produces message.usage.cost.total exactly where ParseEvents reads it, and the summation is correct (see ah-1cx.4). Leading hypothesis: agent-1 runs pi through a CUSTOM 'litellm' provider (models are named litellm/\u003cmodel\u003e), and pi prices a response from its own per-provider model registry — a custom OpenAI-compatible provider has no pricing metadata, so every cost field comes back 0. That would make cost_usd=0 pi's behaviour, not our bug, and the fix would be to source cost from LiteLLM instead. NEEDS LIVE EVIDENCE from agent-1 (an events.jsonl from a real run: is message.usage.cost.total literally 0, or is the usage block shaped differently under the litellm provider?) — the ssh probe is blocked by the local permission classifier, so this needs an operator '!' handoff."} | NULL | 2026-08-05T02:31:57Z |
| 019fcf1e-f613-790c-8f99-35fe20082165 | ah-1cx.8 | created | Eugene Blikh | NULL | 2026-08-05T02:32:20Z | ||
| 019fcf1f-4d39-7ee8-b1e9-4a14bb842846 | ah-1cx.8 | label_added | Eugene Blikh | NULL | NULL | Added label: milestone:v0.1.0 | 2026-08-05T02:32:42Z |
| 019fcf1f-4ea7-7756-bd3e-4a8733778ba2 | ah-1cx.1 | status_changed | Eugene Blikh | {"id":"ah-1cx.1","title":"runner.Status re-parses full events.jsonl every poll, stalling the loop on large runs","description":"Status unconditionally calls ParseEventsFile (reads to EOF, decodes every line) on every 20s poll for every in-progress task (runner.go:209-219), but the parsed pi-session/cost are display-only until finalize, which re-reads via completionFactsFromEvents anyway. On a multi-hundred-MB events stream (ah-07g observed 202MB) this blocks the single-goroutine reconcile loop for seconds per tick, starving supervision of every other task. Fix: skip the full parse for a still-running run — read only the first line for the session id (or defer session/cost to finalize) and keep the cheap os.Stat mtime for the watchdog. Found by core-runtime audit 2026-07-20.","status":"open","priority":2,"issue_type":"bug","owner":"bigbes@gmail.com","created_at":"2026-07-19T23:37:28Z","created_by":"Eugene Blikh","updated_at":"2026-07-19T23:37:28Z","labels":["milestone:v0.1.0"]} | {"status":"in_progress"} | NULL | 2026-08-05T02:32:43Z |
| 019fcf1f-4f41-7897-a3e3-d9700e0fabb2 | ah-1cx.2 | status_changed | Eugene Blikh | {"id":"ah-1cx.2","title":"Delegation persist failure orphans children: parent re-finalizes to In Review, never Blocked","description":"When delegateAndBlock has already created child cards and recorded lineage but its UpsertTask(Blocked) fails (reconcile.go:921-930), the stored record stays in_progress while the run row is finished. Next tick adoptOrFail takes the interrupted-finalize path and re-finalizes with firstFinalize=false, so delegation is skipped and the card lands In Review — the children run to completion but the parent never joins or resumes to integrate them. The code comment claims the heal path converges to Blocked; it does not (only the sibling MoveToBucket-failure case converges via alignCardToRecord). Fix: re-attempt the Blocked persist (retry signal) instead of letting a finished-row re-finalize strand the children. Rare trigger (store write failure at exactly that step); confirmed by reading, core-runtime audit 2026-07-20.","status":"open","priority":3,"issue_type":"bug","owner":"bigbes@gmail.com","created_at":"2026-07-19T23:37:32Z","created_by":"Eugene Blikh","updated_at":"2026-07-19T23:37:32Z","labels":["milestone:v0.1.0"]} | {"status":"in_progress"} | NULL | 2026-08-05T02:32:43Z |
| 019fcf1f-4fd9-72b3-8460-d1f0909255a7 | ah-1cx.3 | status_changed | Eugene Blikh | {"id":"ah-1cx.3","title":"config/wiring: whitespace-only notifier field passes validation then fatally aborts boot","description":"Ntfy/Telegram blocks are stored untrimmed (config.go:453) and present() decides configuration by trimmed emptiness, but reconcileDeps (cmd/agenthubd/main.go:292,298) selects the notifier with raw non-empty checks. A whitespace-only ntfy.url (literal or via VAR expansion) is absent to validation yet present to wiring: config.Load succeeds, then ntfy.New rejects it and run() exits 1 — killing a deployment whose telegram block is fully valid. This contradicts present()'s own documented invariant. Fix: trim Ntfy/Telegram in resolve (as yonote/mem0 already are) or make reconcileDeps use present(). Found by adapter audit 2026-07-20.","status":"open","priority":3,"issue_type":"bug","owner":"bigbes@gmail.com","created_at":"2026-07-19T23:37:36Z","created_by":"Eugene Blikh","updated_at":"2026-07-19T23:37:36Z","labels":["milestone:v0.1.0"]} | {"status":"in_progress"} | NULL | 2026-08-05T02:32:43Z |
| 019fcf1f-5070-7036-98e4-42f415b30492 | ah-1cx.4 | status_changed | Eugene Blikh | {"id":"ah-1cx.4","title":"runner: verify F9 cost model — is pi message cost.total per-message or cumulative?","description":"costFrom/ParseEvents SUM message.usage.cost.total across all assistant message_end events (events.go:120-128), but no captured multi-message fixture proves the field is per-message; testdata/pi-events-two-messages.jsonl is hand-built (TODO from closed ah-nyl.11). If cost.total is actually cumulative, every multi-turn run's reported cost is inflated and both costFrom and the fixture must be re-pinned to take the last message's value. Capture a real multi-assistant-message pi --mode json run and settle it. Distinct from sibling ah-tqc (errored-turn-as-success + cost_usd parse); marker-sweep audit 2026-07-20.","status":"open","priority":3,"issue_type":"bug","owner":"bigbes@gmail.com","created_at":"2026-07-19T23:37:39Z","created_by":"Eugene Blikh","updated_at":"2026-07-19T23:37:39Z","labels":["milestone:v0.1.0"]} | {"status":"in_progress"} | NULL | 2026-08-05T02:32:43Z |
| 019fcf1f-5107-7674-9f10-a86672b359ad | ah-1cx.5 | status_changed | Eugene Blikh | {"id":"ah-1cx.5","title":"vikunja markdown: URL autolink collides with bold / trailing-ampersand entity","description":"renderInline (markdown.go:203-211) runs escape, then URL-autolink, then bold. urlRe greedily swallows a trailing ** into the URL and boldRe then matches across the emitted anchor markup, producing interleaved broken tags for input like: see **http://example.com/x** now. Separately a URL ending in a bare ampersand becomes a split, broken amp-entity. Both reproduced against the exact regexes; the realistic mid-URL query case renders fine, so severity is low. Fix: constrain the URL match at **/entity boundaries or reorder the passes; add cases to markdown_test.go. Adapter audit 2026-07-20.","status":"open","priority":4,"issue_type":"bug","owner":"bigbes@gmail.com","created_at":"2026-07-19T23:37:48Z","created_by":"Eugene Blikh","updated_at":"2026-07-19T23:37:48Z","labels":["milestone:v0.2.0"]} | {"status":"in_progress"} | NULL | 2026-08-05T02:32:43Z |
| 019fcf1f-51ad-7055-83d7-8670e48c7551 | ah-1cx.6 | status_changed | Eugene Blikh | {"id":"ah-1cx.6","title":"config: validate vikunja.url and web_url as http(s) at load, like the other URL fields","description":"validate() only checks Vikunja.URL for non-emptiness (config.go:587-589) while agentsview/yonote/mem0/ntfy/telegram all get validateHTTPURL; web_url is never checked at all. A malformed board URL loads cleanly and fails one layer later at vikunja.New with different diagnostics. Run validateHTTPURL on Vikunja.URL and the explicit WebURL in validate() for consistent config-time errors. Adapter audit 2026-07-20.","status":"open","priority":4,"issue_type":"bug","owner":"bigbes@gmail.com","created_at":"2026-07-19T23:37:51Z","created_by":"Eugene Blikh","updated_at":"2026-07-19T23:37:51Z","labels":["milestone:v0.2.0"]} | {"status":"in_progress"} | NULL | 2026-08-05T02:32:44Z |
| 019fcf1f-524b-74be-9542-15abd8801050 | ah-efe.2 | status_changed | Eugene Blikh | {"id":"ah-efe.2","title":"SPEC section 12: config surface missing Stage 3-4 + Yonote blocks; ask-user wrongly marked future work","description":"Section 12 lacks archive_dir, task_types on_success/on_changes_requested + routing.max_auto_routes, agentsview, yonote (claim_budget, publish_collection_id, nested qa block), mem0, tools_api, and the designer/validator/fixer roles — all shipped and validated in internal/config + config.example.yaml (which agree with each other; only SPEC lags). Also line 784 still calls the ahub tool ask-user CLI-over-HTTP variant future work, directly contradicting delivered ah-0ge.6 and SPEC section 5 principle 5 — delete the stale parenthetical. SPEC-gap audit 2026-07-20.","status":"open","priority":3,"issue_type":"chore","owner":"bigbes@gmail.com","created_at":"2026-07-19T23:38:12Z","created_by":"Eugene Blikh","updated_at":"2026-07-19T23:38:12Z","labels":["milestone:v0.1.0"]} | {"status":"in_progress"} | NULL | 2026-08-05T02:32:44Z |
| 019fcf22-526e-71d4-bd23-1770eaaace27 | ah-wd4.1 | status_changed | Eugene Blikh | {"id":"ah-wd4.1","title":"SPIKE (gating): does the beads Storage API enumerate ALL issues, or silently paginate?","description":"GATING UNKNOWN for any beads-backed Board adapter — settle this before writing adapter code, and before ah-wd4 can be decided on technical grounds.\n\nPROBLEM: the beads public API (root package github.com/steveyegge/beads, MIT) has NO plain ListIssues. Methods returning issue lists are: SearchIssues(ctx, query, types.IssueFilter), GetReadyWork(ctx, types.WorkFilter), GetIssuesByIDs, GetIssuesByLabel, GetDependencies/GetDependents, ListWisps. A Snapshot implementation would have to use SearchIssues with an empty query plus a filter.\n\nWHY IT IS GATING: agenthubd treats a card missing from a Snapshot as VANISHED — internal/reconcile/reconcile.go:1659 handleVanished KILLS the live run and marks the record cancelled. A silently truncated enumeration therefore destroys running work rather than merely returning less data. The Vikunja adapter defends against exactly this by refusing a truncated bucket instead of dropping tasks (internal/vikunja/board.go:95-99).\n\nWHAT TO DETERMINE:\n1. Does types.IssueFilter (and WorkFilter) have a Limit/Offset field, and what is the ZERO-VALUE behaviour — unlimited, or a silent default (e.g. 50/100)?\n2. Does SearchIssues with an empty query return every issue regardless of status, including closed/deferred/frozen ones? (bd list hides done/frozen categories by default — check whether that filtering lives in the CLI or in the storage layer.)\n3. Is there a transactional way to enumerate, or a count to cross-check against (GetStatistics returns types.Statistics — does it carry a total issue count usable as a completeness assertion)?\n4. If no guarantee exists: can RunInTransaction wrap a count + list so the pair is consistent?\n\nHOW: source is at github.com/steveyegge/beads v1.1.0 (proxy.golang.org zip; module root has beads.go re-exporting internal via type aliases). Read internal/types for IssueFilter/WorkFilter and internal/storage/dolt for the SearchIssues SQL. Then PROVE it empirically with a throwaway Go program against a COPY of the DB (copy .beads/embeddeddolt elsewhere; do not write to the live one) — this repo has 84+ issues, so create a scratch DB with \u003e200 to expose a default limit. Report the exact zero-value semantics and the recommended Snapshot implementation with its completeness guarantee.\n\nDELIVERABLE: a written answer to 1-4 plus the recommended Snapshot approach. Do NOT write adapter code.","status":"open","priority":2,"issue_type":"task","owner":"bigbes@gmail.com","created_at":"2026-07-20T07:48:24Z","created_by":"Eugene Blikh","updated_at":"2026-07-20T07:48:24Z"} | {"notes":"SOURCE ANALYSIS DONE (beads v1.1.2, the version installed locally; clone checked out at tag 20e493e5). Empirical confirmation is running separately.\n\nQ1 — Limit zero-value. types.IssueFilter has a plain 'Limit int' (no Offset; keyset pagination instead, via AfterCreatedAt/AfterID over the (created_at DESC, id ASC) total order). The SQL builder gates the clause on 'filter.Limit \u003e 0' — internal/storage/issueops/search.go:100-110 emits NO LIMIT at all for the zero value and takes the branch its own comment calls 'Pattern A: full 47-column scan (used for unlimited queries)'. Limit \u003e 0 instead takes 'Pattern B', a cheap SELECT id + LIMIT then batch hydration. So the zero value is UNLIMITED, not a silent default page size. GetReadyWork likewise has an explicit unlimited branch for Limit \u003c= 0 (issueops/ready_work.go:165) rather than a loop that would return nothing.\n\nQ2 — status visibility. The closed-hiding is a CLI behaviour, NOT a storage behaviour: cmd/bd/list_filter.go:157, cmd/bd/search.go:114, cmd/bd/query.go:136 and cmd/bd/gate*.go are what set ExcludeStatus. sqlbuild.BuildIssueFilterClauses (internal/storage/sqlbuild/filter.go:53-72) emits a status predicate ONLY when Status, Statuses or ExcludeStatus is explicitly populated. A zero-value IssueFilter therefore returns every status, closed and deferred included.\n\nQ3 — completeness cross-check, with a trap. types.Statistics (types.go:1181) carries TotalIssues, so a count exists. BUT SearchIssuesInTx (issueops/search.go:18-78) queries the 'issues' table AND merges the 'wisps' table unless filter.SkipWisps, preferring the wisp record on an ID collision — so a naive len(result)==TotalIssues assertion is only valid if TotalIssues counts the same union. Being verified empirically. Second trap in the same function: when Limit \u003e 0 the limit is applied to EACH table separately before the merge, so a limited query can return MORE rows than Limit. Neither trap bites a Limit=0 snapshot, but both must be documented in whatever adapter gets written.\n\nQ4 — RunInTransaction exists on the public Storage surface (documented in beads.go:23; the root package re-exports the internal types by alias, e.g. IssueFilter = types.IssueFilter), so a count+list pair can be wrapped in one transaction. Being verified empirically.\n\nVERDICT SO FAR: the gating fear (silent pagination destroying live runs via handleVanished) does NOT appear to be real at the source level — SearchIssues(ctx, \"\", IssueFilter{}) is an honest full enumeration. Pending the \u003e600-issue scale test that proves no default page size hides at a larger corpus than this repo's 86 issues.","status":"in_progress"} | NULL | 2026-08-05T02:36:00Z |
| 019fcf23-7414-72ab-ba62-4224bef1e24c | ah-efe.4 | closed | Eugene Blikh | Done in 199d85e. Both files were untracked bd-setup leftovers; now tracked. CLAUDE.md's three scaffold stubs are filled and mirrored into AGENTS.md: Build and Test from the justfile plus the GOFLAGS=-mod=mod go.sum trap this dev env has; Architecture Overview as the reconciler-first summary from SPEC section 1 with docs/SPEC.md named authoritative; Conventions recording the practices the repo actually follows (one bead per change with the durable record in its notes, the [bead-id] commit-message suffix, the deliberately high comment density, the milestone-is-a-release rule). The bd-generated managed blocks in both files were left untouched, and the two files remain independent rather than symlinked. | NULL | 2026-08-05T02:37:15Z | |
| 019fcf24-3fc8-7845-af76-454fc201721c | ah-efe.2 | closed | Eugene Blikh | Done in 54b2176. Section 12 had drifted ~2 stages behind the code. ADDED: archive_dir; the on_success/on_changes_requested routing keys and routing.max_auto_routes; the designer/validator/fixer roles and their design→review→fix task types; and the whole agentsview, yonote (claim_budget, publish_collection_id, nested qa), mem0 and tools_api blocks — all of which already existed in internal/config and config.example.yaml, which agreed with each other. Each got its own validation bullet with the real default (archive_dir → <work_root>/archive, max_auto_routes → 4, claim_budget → 90s, qa.bucket → ready, qa.poll_interval → 60s, tools_api → ON via a *bool), plus a lead-in naming the shared presence pattern the optional integration blocks follow. CORRECTED two outright errors: the parenthetical calling the ahub tool ask-user CLI-over-HTTP variant future work (it shipped in 07bd9d4/03f4496 — the file channel and the mid-run channel are now presented as two transports over identical board state), and the bucket-map comment calling blocked/question parked when §10 has made them real states since Stage 4 (triage is now the one parked bucket, map reordered to match domain.CanonicalBuckets()). NEW PROSE on what the config gates: the mid-run tools API, the three Yonote lanes, the mem0 lifecycle, and the Context Pack documented as deliberately having NO config surface (order and budgets are package constants). Defaults spot-verified against internal/config/config.go before commit. | NULL | 2026-08-05T02:38:07Z | |
| 019fcf24-7ac2-7ce8-b900-bd82324486a9 | ah-efe.5 | created | Eugene Blikh | NULL | 2026-08-05T02:38:22Z | ||
| 019fcf24-a127-7991-9b28-c95c292977ed | ah-efe.3 | updated | Eugene Blikh | {"id":"ah-efe.3","title":"SPEC sections 1/4/11: external-services table, repo layout tree, HTTP surface sweep","description":"Low-priority staleness bundle: section 1 services table omits ntfy/Prism and Yonote; section 4 layout tree omits internal/mem0, ctxpack, yonote, deps; section 11 (and the httpapi package doc-comment) lists only healthz/run-exit/status/webhook, missing the three Stage-4 /api/tool/* routes. One sweep commit. SPEC-gap audit 2026-07-20.","status":"open","priority":4,"issue_type":"chore","owner":"bigbes@gmail.com","created_at":"2026-07-19T23:38:15Z","created_by":"Eugene Blikh","updated_at":"2026-07-19T23:38:15Z","labels":["milestone:v0.2.0"]} | {"notes":"Concrete §11 defects found during the §12 refresh (ah-efe.2), to fold into this bead's work:\n- The §11 heading says 'Stages 1-2' but internal/httpapi now also serves the Stage 4 tools surface.\n- The endpoint list omits POST /api/tool/task-create, POST /api/tool/ask-user and GET /api/tool/answer entirely. Document that they are BEARER-authenticated (unlike /api/v1/status, which stays unauthenticated and loopback-trusted) and that they are registered only when Deps.Tools is wired — i.e. only when tools_api is on.\nNothing contradicting §12 was found in §5/§6 (ah-efe.1); §5 principle 5 ('Agent-facing tools are CLI-over-HTTP (Stage 4)') is consistent with the delivered surface. §9 and §14 defects were split into ah-efe.5."} | NULL | 2026-08-05T02:38:32Z |
| 019fcf25-2d5d-7488-8f7d-0e292417c3cb | ah-efe.1 | status_changed | Eugene Blikh | {"id":"ah-efe.1","title":"SPEC section 5/6: refresh domain model and port interfaces to delivered reality","description":"Normative sections are stale: CanClaim omits ready as a claimable state (transitions.go:19 allows it for the ah-4el verdict-routing re-claim path); the State const block is missing blocked/question; TaskRecord lacks AutoRoutes, RunSummary lacks Summary/Verdict; RequestedTask/QARequest/PublishDoc types are absent; Board is missing CreateTask/Comments/SwapLabel, Runner missing RequestedTasks/AskedQuestion/Answer/PublishRequests/ArchiveWorktree, Store missing the lineage/kv/qa methods, StartSpec gained Artifacts+ToolToken. Sync both sections to internal/domain + internal/ports or add an explicit Stage 3-4 additions subsection. SPEC-gap audit 2026-07-20.","status":"open","priority":3,"issue_type":"chore","owner":"bigbes@gmail.com","created_at":"2026-07-19T23:38:08Z","created_by":"Eugene Blikh","updated_at":"2026-07-19T23:38:08Z","labels":["milestone:v0.2.0"]} | {"status":"in_progress"} | NULL | 2026-08-05T02:39:07Z |
| 019fcf25-2dff-7da8-8087-d57e47b09629 | ah-efe.3 | status_changed | Eugene Blikh | {"id":"ah-efe.3","title":"SPEC sections 1/4/11: external-services table, repo layout tree, HTTP surface sweep","description":"Low-priority staleness bundle: section 1 services table omits ntfy/Prism and Yonote; section 4 layout tree omits internal/mem0, ctxpack, yonote, deps; section 11 (and the httpapi package doc-comment) lists only healthz/run-exit/status/webhook, missing the three Stage-4 /api/tool/* routes. One sweep commit. SPEC-gap audit 2026-07-20.","notes":"Concrete §11 defects found during the §12 refresh (ah-efe.2), to fold into this bead's work:\n- The §11 heading says 'Stages 1-2' but internal/httpapi now also serves the Stage 4 tools surface.\n- The endpoint list omits POST /api/tool/task-create, POST /api/tool/ask-user and GET /api/tool/answer entirely. Document that they are BEARER-authenticated (unlike /api/v1/status, which stays unauthenticated and loopback-trusted) and that they are registered only when Deps.Tools is wired — i.e. only when tools_api is on.\nNothing contradicting §12 was found in §5/§6 (ah-efe.1); §5 principle 5 ('Agent-facing tools are CLI-over-HTTP (Stage 4)') is consistent with the delivered surface. §9 and §14 defects were split into ah-efe.5.","status":"open","priority":4,"issue_type":"chore","owner":"bigbes@gmail.com","created_at":"2026-07-19T23:38:15Z","created_by":"Eugene Blikh","updated_at":"2026-08-04T23:38:32Z","labels":["milestone:v0.2.0"]} | {"status":"in_progress"} | NULL | 2026-08-05T02:39:08Z |
| 019fcf25-2e9f-7d52-9418-fb451786694c | ah-efe.5 | status_changed | Eugene Blikh | {"id":"ah-efe.5","title":"SPEC sections 9 and 14: .task/ layout stale, Stage 3-4 still written as roadmap","description":"Found while refreshing §12 (ah-efe.2). Neither section is covered by the existing ah-efe children (.1 covers §5/6, .3 covers §1/4/11), so they would silently stay stale.\n\nSECTION 9 (runner):\n- The .task/ layout comment at ~line 396 reads 'question.json # reserved for Stage 4 ask-user'. Stage 4 ask-user is DELIVERED; the file is live, not reserved.\n- The same listing omits five files the code actually writes or reads: tasks.json, summary.md, publish.json, answer.json, and tool-auth.json (mode 0600, written by internal/runner/taskfiles.go).\n\nSECTION 14 (roadmap):\n- Stages 3 and 4 are still phrased as forward-looking roadmap items while Stage 2 is marked 'Delivered'. The mem0 lifecycle, Context Pack, agent tools API, AgentsView deep links and the Yonote lanes have all shipped; the stage markers need the same treatment Stage 2's got.\n- The Stage 4 item promises 'ask-user→Question loop with pi --session resumes'. The delivered mid-run path instead keeps the process alive by BLOCKING in the CLI (2s polls until --deadline), so that phrasing describes a superseded design and should be corrected rather than just re-marked as done.","acceptance_criteria":"§9's .task/ listing matches what internal/runner/taskfiles.go actually writes, with no 'reserved' markers on delivered files. §14's Stage 3 and Stage 4 entries carry accurate delivered/not-delivered markers, and the ask-user description matches the blocking-CLI design that actually shipped.","status":"open","priority":3,"issue_type":"chore","owner":"bigbes@gmail.com","created_at":"2026-08-04T23:38:22Z","created_by":"Eugene Blikh","updated_at":"2026-08-04T23:38:22Z"} | {"status":"in_progress"} | NULL | 2026-08-05T02:39:08Z |
| 019fcf27-643e-782d-a052-a620b8e39e4c | ah-1cx.2 | closed | Eugene Blikh | Fixed in 67dcf3c. Confirmed by REPRODUCTION, not just reading: finalize computes firstFinalize from run.State != finished and the delegation branch was gated on it; when delegateAndBlock's UpsertTask(Blocked) failed, the run row was already finished (UpdateRun runs first) while the task record stayed in_progress, so the next tick's adoptOrFail interrupted-finalize path re-entered finalize with firstFinalize=false, skipped delegation entirely, and routed the parent to In Review behind live children. The old comment claiming the heal path converges to Blocked was WRONG — alignCardToRecord only covers the sibling MoveToBucket-failure case, where the record is already Blocked. Fix: delegateAndBlock arms a durable intent marker in the existing kv store (delegation_park/<task>/<attempt>, value armed:<n>/alarmed:<n> where n is the parent's child count BEFORE this delegation) ahead of createChildTasks, so the only write that can fail before any child exists is the marker itself, and that failure cancels the delegation outright instead of half-landing it. The park tail is extracted into an idempotent parkParentBlocked used by both the first finalization and a new refinalize-only reparkDelegatedParent, which re-attempts the flip from the marker and NEVER calls createChildTasks — so a heal cannot become a duplicate-child bug. The before-count is what distinguishes a real pending join from fire-and-forget children created by the mid-run ToolTaskCreate channel and from an armed-but-created-nothing delegation; both disarm and finalize to In Review as before. Failure is now loud and recoverable: error log, delegation_park_failed audit event, and a once-per-attempt card comment (guarded by the marker's alarmed flag so a broken store cannot spam the card), with the card held in In Progress so the loop retries each tick. Run-row immutability untouched. Cover: TestDelegationParkPersistFailure (4 subtests) + TestDelegationParkGuards (4 subtests). | NULL | 2026-08-05T02:41:33Z | |
| 019fcf27-aa32-7340-bfec-5a07cab35cf3 | ah-1cx.3 | closed | Eugene Blikh | Fixed in a8f264e. Root cause: the optional blocks' present() predicates decide configuration by TRIMMED emptiness, but resolve copied Ntfy/Telegram straight out of the YAML untrimmed and reconcileDeps (cmd/agenthubd/main.go:292,298) selected the notifier with raw non-empty tests — so a whitespace-only ntfy.url was ABSENT to validation and PRESENT to wiring: config.Load succeeded, ntfy.New then rejected the value and run() exited 1, killing a deployment whose telegram block was perfectly valid. Fixed at the root by making 'set but whitespace-only' a load-time error instead of a value that silently reads as unset: resolve now scans the RAW decoded document (before any trimming erases the evidence) via blankScanFields()+validateNoBlankValues() and reports '<field> is set to whitespace only: remove the key to leave it unset, or give it a real value'. The scan is deliberately GENERIC, covering every field a present()/required check reads through TrimSpace: vikunja.token, vikunja.webhook_secret, both agentsview fields, both ntfy fields, telegram.token/api_base, yonote.base_url/token/publish_collection_id/qa.role/qa.bucket/qa.collections[i], both mem0 fields — collections entries indexed by name because trimNonEmpty would otherwise drop a blank one and silently shrink the watched set. resolve also now trims the four notifier strings the way yonote/mem0 base_urls already were, so the value the adapter constructor receives is exactly the value present() judged; this also stops a merely PADDED value (a ${NTFY_URL} picking up a trailing newline from the env file) from reaching ntfy.New. reconcileDeps keeps its raw tests but now carries a comment recording why they are equivalent to present() and that any new notifier field must be trimmed at load rather than compared there. Cover: TestLoadBlankValues — the original scenario, the ${VAR}-expands-to-blank variant, a table over all sibling fields, and padded-but-real values surviving trimmed with present() true. | NULL | 2026-08-05T02:41:51Z | |
| 019fcf27-ac17-72ce-87e5-681f3b9a2ae7 | ah-1cx.6 | closed | Eugene Blikh | Fixed in a8f264e (same commit as ah-1cx.3 — the hunks interleave in config.go). validate() checked vikunja.url for non-emptiness only while agentsview/yonote/mem0/ntfy/telegram all ran their base URLs through the existing validateHTTPURL helper, and vikunja.web_url was never checked at all; a malformed board URL loaded cleanly and failed one layer later inside vikunja.New, with different wording and no config-file context. The required-check is now strings.TrimSpace(...)=='' (so a blank url still reports 'vikunja.url is required' rather than a confusing scheme error) and the non-empty path REUSES validateHTTPURL — no second implementation was introduced. web_url is validated in the same branch rather than beside it because resolve DERIVES web_url from url when the key is omitted: checking it unconditionally would make one bad url report itself twice under two names. Ruling url out first guarantees the derived value is valid, so anything reported against vikunja.web_url is an explicit key the operator actually wrote. Cover: TestLoadVikunjaURLValidation — scheme-less url, non-http scheme, host-less url, blank url, explicit bad web_url, accepted explicit web_url, and an assertion that a bad url does NOT also echo as vikunja.web_url. | NULL | 2026-08-05T02:41:51Z | |
| 019fcf27-dd07-78a2-8108-ab6252ec3812 | ah-1cx.5 | closed | Eugene Blikh | Fixed in c2e29c1. renderInline ran escape → autolink → bold, and BOTH later passes collided with the anchor the middle pass had already emitted. urlRe treats '*' as an ordinary URL character, so 'see **http://example.com/x** now' swallowed the closing ** into both the href and the link text, and boldRe then matched from the opening ** across the emitted markup, yielding interleaved <strong>/<a> garbage. Separately, because the pass runs on ALREADY-ESCAPED text, a URL ending in a bare & became …& and urlRe — which excludes a trailing ';' as sentence punctuation — stopped at …&, splitting the entity across </a>; same for a URL followed by > or '. Fix: reorder so bold runs FIRST (the ** markers are consumed while still adjacent to the URL, and urlRe's class already excludes the '<' of the resulting <strong> tag, so the link nests strictly inside the bold), and replace the ReplaceAllString autolink with autolinkURLs, which re-cuts every match through the new splitURLTail. splitURLTail alternates two rules until neither fires: push a trailing INCOMPLETE entity (an & with no ; after it) out of the anchor so the ; left in the surrounding text re-joins it, then re-apply the trailing-punctuation rule ('.,;:!?)]' plus '*', so an unpaired bold marker can never ride into an href) to the newly exposed last character. A complete entity mid-URL (?a=1&b=2) contains its ; and is untouched, keeping the realistic query-string case rendering as before; a match trimmed down to a bare scheme (http://&) is left unlinked instead of emitting a dud anchor. Seven table cases added; all five collision cases were written first and confirmed RED against the old converter. | NULL | 2026-08-05T02:42:04Z | |
| 019fcf28-8304-7d69-bfa8-d5fdc59279b5 | ah-1cx.9 | created | Eugene Blikh | NULL | 2026-08-05T02:42:46Z | ||
| 019fcf28-dc17-7f17-873d-f69215717282 | ah-1cx.10 | created | Eugene Blikh | NULL | 2026-08-05T02:43:09Z | ||
| 019fcf28-df25-7a9e-b4b7-d7ca76c37a41 | ah-1cx.11 | created | Eugene Blikh | NULL | 2026-08-05T02:43:10Z | ||
| 019fcf28-e1fd-7c29-9c19-24c1561763d9 | ah-1cx.12 | created | Eugene Blikh | NULL | 2026-08-05T02:43:10Z | ||
| 019fcf29-5cb0-7c15-944a-607975e1066e | ah-1cx.13 | created | Eugene Blikh | NULL | 2026-08-05T02:43:42Z | ||
| 019fcf29-5fbc-772f-8cd9-8efa0218d6e0 | ah-1cx.14 | created | Eugene Blikh | NULL | 2026-08-05T02:43:43Z | ||
| 019fcf29-62e3-78ba-9cb7-2a0f1ebdf966 | ah-1cx.15 | created | Eugene Blikh | NULL | 2026-08-05T02:43:43Z | ||
| 019fcf29-a21f-7f26-b3cc-c9e46cec2c7f | ah-jzv | created | Eugene Blikh | NULL | 2026-08-05T02:44:00Z | ||
| 019fcf29-a3d0-7c19-b72c-69e777b395c2 | ah-1jy | created | Eugene Blikh | NULL | 2026-08-05T02:44:00Z | ||
| 019fcf2b-1d12-73b9-a18f-ba9fd80987cd | ah-1cx.1 | closed | Eugene Blikh | Fixed in f11ef03. Status's per-poll read of events.jsonl is now asymmetric: the still-running path does only the os.Stat it already needed for LastEvent (the stall-watchdog input) plus PeekSessionID, a new bounded head probe that reads at most 256 KiB / 8 leading lines looking for the {"type":"session","id":...} event pi writes as line 1. The full ParseEventsFile no longer sits on the 20s poll path, so a 202 MB stream (ah-07g) costs a running poll the same as an empty one and can no longer freeze the single-goroutine reconcile loop. CostUSD is deliberately left at zero for a running run — cost is a whole-stream sum with no cheap mid-run answer, and the reconciler only persists cost at finalize — and PiSession is explicitly best-effort ("" while the head is unwritten or torn), which is safe because the reconciler only overwrites a stored session id with a non-empty one. Every path that COMPLETES a run still takes the full parse: both exit.json branches via finalizeFromExit (ah-2ef's durable pinning, untouched) and — newly — the crash-confirmed branch, since a confirmed crash writes an immutable finished row and previously relied on the top-of-Status parse for the failure comment's session id and cost. The reader loop was extracted into scanEventLines(r, fn) with an early-stop callback, so the F9a over-long-line rule (skip the line, keep scanning; never bufio.Scanner) is shared by the full parse and the probe and cannot drift; the probe's byte budget is deliberately 4x the read buffer so a leading line wider than the buffer is stepped over rather than ending the probe. Cover: TestStatusRunningDoesNotParseWholeEventStream builds an 8 MB stream with the id on line 1 and a fat cost-bearing turn at the end, asserting Running ⇒ id resolved + CostUSD==0 + LastEvent==mtime, then drops exit.json and asserts the same stream yields the full cost; TestPeekSessionIDIsBounded proves boundedness structurally. Mutation-checked. KNOWN REGRESSION SPLIT OUT: kill-finalized runs now record CostUSD 0 — see the dedicated bead. | NULL | 2026-08-05T02:45:37Z | |
| 019fcf2b-766e-7d52-b56c-ab27e2e3af7c | ah-1cx.4 | closed | Eugene Blikh | SETTLED in f11ef03: message.usage.cost.total is PER-MESSAGE, not cumulative, so ParseEvents's summation is correct. TODO(ah-nyl.11) removed from both costFrom's doc comment and TestParseEventsTwoAssistantMessagesSumsPerMessageCost. Evidence is a REAL two-assistant-message, one-tool-call run captured from pi 0.82.1 (deepseek/deepseek-v4-flash, exit 0), committed as testdata/pi-events-multi-real.jsonl; the 123 message_update lines (95% of the raw 166 KB, ignored by the parser) were stripped and the trimmed 20-line fixture verified to parse byte-for-byte identically to the raw capture. The proof recorded in costFrom is ARITHMETIC rather than assertive: one flat set of unit rates ($0.14 / $0.28 / $0.0028 per Mtok for input / output / cacheRead) explains each message's cost from that message's OWN token counts alone — msg1 97*1.4e-7=0.00001358 and msg2 85*1.4e-7=0.0000119; msg1 70*2.8e-7=0.0000196 and msg2 79*2.8e-7=0.00002212 — so msg2's total contains no part of msg1's, which a cumulative field could not manage. TestParseEventsRealMultiMessageCapture pins session id, CostUSD == 0.0000374808+0.0000386792, AssistantMessages == 2, FinalStopReason == 'stop' (the first turn ended in toolUse). The hand-built pi-events-two-messages.jsonl and its test are KEPT but re-framed as the pi 0.70.2 shape pin with round numbers. Two incidental findings now documented in the events.go header: turn_end repeats its turn's usage verbatim (so the not-double-counted rule is right), and 0.82.1 adds a trailing agent_settled event plus assistant message_start events that carry a stopReason and a zeroed usage block — the latter would skew AssistantMessages/FinalStopReason if the message_end type check were ever loosened. | NULL | 2026-08-05T02:45:59Z | |
| 019fcf2b-7862-7b00-86f6-5efca769ccda | ah-tqc | closed | Eugene Blikh | BOTH findings resolved. Finding 1 (errored-final-turn ⇒ false success): fixed in 665e805, then COMPLETED in f11ef03 after an adversarial review disassembled the installed pi 0.82.x and found the first pass implemented only half of pi's actual contract. Errored() is now two-armed: (a) the final assistant turn's stopReason is one pi itself calls unusable — 'error' OR 'aborted', since pi's own predicate at print-mode.js:105 is 'error || aborted' and that block lives inside 'if (mode === "text")', which is precisely why json mode exits 0 on both; (b) SessionID != "" && AssistantMessages == 0, the arm the bead named but the first pass never implemented (AssistantMessages was being computed for exactly this and never consulted). Arm (b) closes an EMPIRICALLY REPRODUCED hole: print-mode.js:94 guards the whole run with 'if (initialMessage)', so a blank or whitespace-only rendered PROMPT.md — which prepareTaskDir writes verbatim, unchecked, with nothing in reconcile guarding it either — makes pi do nothing and exit 0 with a one-line stream, sending the card to In Review, publishing an empty review branch and firing the Q&A answer lane. The SessionID != "" gate is kept and heavily commented because it separates 'file present and parsed, zero turns' (evidence) from 'nothing to parse' (ignorance) — the conflation the original positive-evidence-only rationale made. finalizeFromExit's warn log gained a stream_verdict attribute so an operator can tell a provider rejection (pi's text in pi_error) from a did-nothing run. SPEC §9 and domain.Outcome's enum comment (c8083c2) both updated. Finding 2 (cost_usd=0 on every live run) was SPLIT OUT to its own bead — the parser is ruled out, and the leading hypothesis is that pi has no pricing metadata for the custom 'litellm' provider; it needs live evidence from agent-1. Review follow-ups also split out: the failure comment renders 'exit code 0' and drops the provider's rejection text; nothing rejects an empty prompt at the source. | NULL | 2026-08-05T02:46:00Z | |
| 019fcf2b-b0cb-729f-86c8-2a97e7613e88 | ah-1cx.16 | created | Eugene Blikh | NULL | 2026-08-05T02:46:14Z | ||
| 019fcf2e-2259-7a48-b50e-9281afb2489e | ah-1qq | status_changed | Eugene Blikh | {"id":"ah-1qq","title":"Release infra: version stamping via ldflags + first tagged release","description":"The daemon hardcodes version = dev (cmd/agenthubd/main.go:44) and nothing stamps it: the repo has zero git tags and neither the justfile nor .build.yml passes -ldflags -X main.version. Live agent-1 therefore logs version=dev on every boot and there is no way to tell which build is deployed. Add ldflags stamping (git describe) to the justfile and the CI deploy lane, surface the version in the status API, and cut the first tag. Release-blocking for v0.1.0. Filed 2026-07-20.","status":"open","priority":2,"issue_type":"task","owner":"bigbes@gmail.com","created_at":"2026-07-20T00:10:40Z","created_by":"Eugene Blikh","updated_at":"2026-07-20T00:10:40Z","labels":["milestone:v0.1.0"]} | {"notes":"STAMPING DONE in 1c96c00; only the tag itself remains, and it is deliberately blocked on the rest of milestone v0.1.0 (a milestone is frozen at its tag, so nothing may be added after).\n\nWhat landed: the justfile derives 'git describe --tags --always --dirty' into a version variable and passes -X main.version to go build ./cmd/... — ONE flag stamps all three mains, because -X's 'main' resolves per link. --always degrades to a short sha before the first tag exists, --dirty marks a build made from an uncommitted tree so a hand-built binary can never be mistaken for a release, and a '|| echo dev' arm covers a tree with no git at all. 'just show-version' prints what the tree would stamp. .build.yml does the same and then runs 'ahub version' as a build-time assertion that the stamp actually landed — so a broken stamp fails CI instead of silently deploying a 'dev' binary.\n\nahub-run gained a version var and a --version flag (answered before the required-flag check, since it is a question about the binary not a request to supervise anything): it is the binary that runs INSIDE the pane, so that is how a human attached to a keep-pane shell identifies the build that supervised the run in front of them.\n\nGET /api/v1/status now carries the build string via httpapi.Deps.Version, which defaults to 'dev' when empty so a test server or unstamped build never reports blank — a blank version reads as a serialization bug rather than as 'nobody stamped this'. The boot log line is invisible once the journal has rotated, and status is the one surface an operator can reach without shell access to the box. Cover: TestStatusVersion, both arms.\n\nVerified end to end locally: just build produced binaries reporting f11ef03-dirty.\n\nREMAINING: cut v0.1.0 once the milestone's other beads close, then confirm on agent-1 that the deployed binary reports the tag rather than a sha.","status":"in_progress"} | NULL | 2026-08-05T02:48:55Z |
| 019fcf2e-f507-7bdb-a2c5-ed1f180d3e14 | ah-efe.5 | closed | Eugene Blikh | Done in 119b84d. §9's .task/ listing was three files short of reality and mislabelled a delivered one: question.json was marked 'reserved for Stage 4' although the ask-user file channel has shipped, and tasks.json, summary.md, publish.json, answer.json and tool-auth.json were absent entirely. Verified against internal/runner/taskfiles.go and runner.go prepareTaskDir; the listing now carries all eleven entries plus the two directories the code actually manages (artifacts/, publish/), with tool-auth.json marked mode 0600 and written only when a token was minted. A new paragraph states the inbound/supervisor/outbound split, that a missing outbound file always means 'not requested' rather than an error, and that Start clears every attempt-scoped file (stale exit.json, all outbound files, publish/, artifacts/, tool-auth.json) and truncates events.jsonl — with the concrete failure each clearing prevents. §14's Stage 3 and Stage 4 were still forward-looking roadmap while Stage 2 read 'Delivered'; both are now marked delivered in Stage 2's style. Two roadmap promises are CORRECTED rather than ticked off: (1) the post-run summarizer role was NEVER BUILT — no such role or prompt exists; the shipped design substitutes agent-authored .task/summary.md plus mem0's infer=true extraction, and accepting that substitution is the open decision on ah-ydx; (2) 'ask-user→Question loop with pi --session resumes' was a superseded design — there is no pi --session resume anywhere: the file channel parks in Question and a human answer starts a NEW attempt with a freshly rendered prompt, while the mid-run path never stops the run at all (ahub tool ask-user blocks in the CLI polling GET /api/tool/answer every 2s until --deadline, default 5m). Stage 4 also now states plainly that the roadmap's memory search/add tools do not exist and nothing tracks them, and that there is no content-level dedup of requested children. | NULL | 2026-08-05T02:49:48Z | |
| 019fcf2f-2e24-7599-9d36-9454c1236a47 | ah-efe.3 | closed | Eugene Blikh | Done in 119b84d. §1's services table omitted the ntfy/Prism gateway and Yonote, and described AgentsView's auth as 'pg push from client side' without saying the daemon never calls it. The table now has eight rows ordered by stage, distinguishes Telegram's token-in-URL from ntfy's token-in-header (the §15 redaction distinction), marks Yonote as a BOT token at 1.47.1, and a new paragraph states that only Vikunja is mandatory, that telegram/ntfy are the mutually exclusive notifier options, and that AgentsView receives NOTHING from agenthubd — the daemon only renders <base_url>/sessions/<machine>~pi:<id> links. §4's tree was missing internal/mem0, internal/ctxpack, internal/yonote and internal/deps, still showed cmd/ahub/main.go alone, and omitted skills/; all added, with a new paragraph explaining that mem0/ctxpack/yonote are deliberately NOT ports (pure renderer; optional single-consumer clients consumed via local interfaces in reconcile), and the dependency note corrected to include go.bigb.es/auxilia and testify while confirming Stages 2-4 added no new external dependency. §11's heading claimed 'Stages 1-2' and its list stopped at four routes; it now covers all seven, documents POST /api/tool/task-create, POST /api/tool/ask-user and GET /api/tool/answer with their real bodies and status codes, states that all three are BEARER-authenticated with the per-task token (constant-time compare, uniform 401 that echoes nothing) while GET /api/v1/status stays unauthenticated and loopback-trusted, and that they register only when Deps.Tools is wired — i.e. only when tools_api is on, the same gate shape as webhook_secret. The endpoint list was swept against the real mux.HandleFunc registrations; no other route exists. Also fixed §3's binaries table, which still described ahub tool as a future subcommand, and folded in ah-1qq's new status version field. UNVERIFIED, left alone: §1 lists sourcehut as git@git.srht.bigb.es while §12's example review_remote is git@srht.bigb.es:~bigbes/demo-repo — one of the two is wrong and it needs an operator to say which. | NULL | 2026-08-05T02:50:03Z | |
| 019fcf2f-6ebd-70bb-a3ee-b0bed76ac904 | ah-efe.1 | closed | Eugene Blikh | Done in 119b84d. §5/§6 contradicted nothing in §12 but were roughly two stages behind the code. §5's State block was missing StateBlocked and StateQuestion (the two Stage-4 real states); the RunState* and Verdict* const blocks were absent; TaskRecord lacked AutoRoutes; RunSummary lacked Summary/Verdict; and RequestedTask, QARequest and PublishDoc did not appear at all — every one now does, field-for-field against internal/domain/types.go. OutcomeError's comment still read 'exit != 0', which 665e805 (ah-tqc) falsified: a zero exit contradicted by the event stream's last message_end stopReason 'error' now also produces error, so §5 was contradicting the §9 prose that same commit added. Corrected, and Run.State now names the RunState* constants instead of bare strings. The CanClaim paragraph omitted 'ready' — claimable since the ah-4el verdict-routing re-claim path — so it now lists the full claimable set, names the deliberately-unclaimable complement {in_progress, blocked, question} and why, and mentions States()/Valid()/CanonicalBuckets(). §6's preamble claimed reconcile imports only domain/ports/config/spec; it now records the three Stage-3/4 additions (ctxpack, mem0, yonote) and why none is a port. Board gained CreateTask/Comments/SwapLabel plus NewTask; StartSpec gained Artifacts and ToolToken plus the Artifact type; Runner gained RequestedTasks/AskedQuestion/Answer/PublishRequests (documented as one zero-value-means-not-requested family) plus ArchiveWorktree and ArchiveResult; Store gained the lineage (RecordChild/GenerationOf/ParentOf/ChildIDsOf), kv (GetKV/SetKV) and Q&A (CreateQA/QAByTask/QAByComment/MarkQAAnswered) methods. §5 principle 5 was already correct and left alone. Every signature and comment is traceable to internal/domain/ or internal/ports/ as of this commit. | NULL | 2026-08-05T02:50:20Z | |
| 019fcf2f-70a8-7334-bb33-2ffe8d0886d0 | ah-efe | closed | Eugene Blikh | Epic complete: all five children closed (ah-efe.1 §5/§6, ah-efe.2 §12, ah-efe.3 §1/§4/§11, ah-efe.4 CLAUDE.md/AGENTS.md, ah-efe.5 §9/§14). SPEC.md now describes delivered reality across every section the epic covered, and three outright errors were corrected rather than just filled in: ask-user marked future work when it shipped, blocked/question called parked when they are real states since Stage 4, and the 'pi --session resume' design that was superseded by a blocking CLI poll. Two SPEC gaps found during the sweep fall OUTSIDE this epic's scope and were filed separately: §7 (storage schema, stale by four migrations) and §15 (security notes, missing the whole Stage-4 credential surface). | NULL | 2026-08-05T02:50:20Z | |
| 019fcf2f-f560-72f5-956a-81e2dedb09c5 | ah-wd4.1 | closed | Eugene Blikh | ANSWERED — the gating fear is NOT real. beads v1.1.2, verified by source reading AND by measurement against copies of the live DB plus scratch DBs of 700 and 2500 issues. Q1 Limit zero-value: UNLIMITED, no silent default. IssueFilter.Limit is a plain int and the SQL builder gates the clause on Limit > 0 (issueops/search.go:100-110, 'Pattern A: full scan (used for unlimited queries)'). Measured whole: 87/87 on the live copy, 700/700, 2500/2500. Explicit limits track exactly and saturate only at the true row count — 1,5,50,100,250,500,1000 all exact on the 2500-row DB, so there is no round-number cap hiding anywhere. IterIssues drains identically. WorkFilter{Limit:0} and {Limit:-1} both return the full 240 ready set, so GetReadyWork has a real unlimited branch too. NEW FINDING NOT IN THE BRIEF: IssueFilter.Offset is DEAD on this read path — grep finds zero uses in issueops/ and sqlbuild/, and empirically Offset:0/5/10 with Limit:5 all return page 0. A hand-rolled Limit+Offset pager against beads would loop on page 0 forever. Only the internal/storage/domain/db builder honours Offset. Q2 status visibility: an empty query returns EVERY status, unconditionally. Live copy: closed=65, in_progress=9, open=13. Scale DB: 140 each of blocked/closed/deferred/in_progress/open. The hiding is entirely CLI-side (cmd/bd/list_filter.go:150-159, which also excludes pinned and every custom status categorised done or frozen, and only when --status/--all/--ready/--pinned are all absent). Same DB: 'bd list --json' 560, 'bd list --all -n 0' 700, API zero-filter 740 — the last gap being wisps, which the CLI list path skips. Q3 completeness cross-check — the suspicion was RIGHT and it is not theoretical. Statistics.TotalIssues is 'SELECT COUNT(*) FROM issues' and nothing else (issueops/statistics.go:16-32); the wisps table is never touched. On a 700-issue + 40-wisp DB: SearchIssues=740, CountIssues=740, TotalIssues=700. So len(result)==stats.TotalIssues is WRONG and passes on agent-hub today only because our wisps table is empty — a latent bug the current data hides. The correct cross-check is Storage.CountIssues, explicitly built to mirror SearchIssues's wisps-merge semantics ('GH#4387 count/list parity contract'); parity verified across 15 filter shapes including label-driven DISTINCT paths. Two hazards measured: (i) with Limit > 0 the limit is applied PER TABLE then merged, so Limit:100 returns 140; (ii) a cross-table duplicate ID makes CountIssues exceed SearchIssues by exactly the number of dups (741 vs 740) — and the comment at count.go:52-58 claiming SearchIssuesInTx 'errors loudly' on that is STALE for v1.1.2: search.go:60-72 silently prefers the wisp record. Q4 RunInTransaction: NO, not through the public API. Two independent blockers, both tested. storage.Transaction exposes SearchIssues but no CountIssues, no GetStatistics, no Iter* — one enumeration primitive and nothing to check it against. And embedded Dolt holds a process-exclusive lock for the transaction's duration, so store-level calls from inside the callback fail with 'the database is locked by another dolt process'. A transaction does give an atomic snapshot (read-your-writes confirmed, clean rollback, zero extra Dolt commits), but the snapshot cannot be independently verified from within it. RECOMMENDED SNAPSHOT (see the bead's design field for the full form): build the filter in ONE place with Limit and Offset pinned to 0 and never caller-supplied; do count → list → count; assert (n==len(list) || n2==len(list)) and that IDs are unique; on failure REFUSE with an error rather than returning a short list, exactly as internal/vikunja/board.go:95-99 does — reconcile.go handleVanished kills the live run for any record missing from a snapshot, and ports.go already specifies that a Snapshot error makes the reconciler skip the whole iteration, so a refusal costs one poll interval while a short list destroys running work. Do NOT set SkipWisps by default (NoHistory beads live in wisps with ephemeral=0 and would become invisible ⇒ killed runs) and do NOT wrap the snapshot in RunInTransaction (no verifiability gained, and a process-exclusive lock on a poll loop starves every other store call). Perf is a non-issue at board scale: 126 ms to list 2500 rows, 59 ms for 740, 34 ms to count. Build note for whoever writes the adapter: the beads module needs CGO_ENABLED=1 AND ICU headers (go-icu-regex fails with 'unicode/regex.h' not found otherwise) — on this mac, CGO_CFLAGS/CXXFLAGS=-I/opt/homebrew/opt/icu4c@77/include and CGO_LDFLAGS=-L.../lib -licuuc -licui18n -licudata. | NULL | 2026-08-05T02:50:54Z |