cg123jil079do2uuc9b137rmea6kg3cr · 439 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 |