main · last commit
13 days ago ·
7g0stsfu
ah-wd4 DECISION: move the task board from Vikunja to beads+Dolt (full swap vs hybrid mirror)
Lined Up
bd update ah-wd4 --claim
bd close ah-wd4
| Created by | Eugene Blikh |
| Owner | bigbes@gmail.com |
| Created | 2026-07-20T00:26:37Z |
| Updated | 2026-08-04T23:51:12Z |
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. FEASIBILITY (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&A 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->HTML 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. BLOCKERS (the real cost): 1. 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. 2. 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). 3. 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 <=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. 4. 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. 5. int64 task IDs are load-bearing (branch task-<id>, zellij session, archive filename, tool-token binding, HTTP API, notification URL). Beads ids are strings (ah-1cx.1). Recommended fix: repo-wide int64->string (mechanical, compiler-verified, ~10 files) over a synthetic mapping table that can drift. 6. 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. 7. 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. 8. 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. WHAT 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). OPTIONS: A) 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. B) 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->status 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->string 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. RECOMMENDATION: 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.
HANDOFF DESIGN — human write path for a beads-backed board. Written 2026-07-20 for a worker with
no prior conversation context. Read this whole section before touching code.
== WHY THIS EXISTS ==
Vikunja is not merely storage for agenthubd: it is the surface through which a HUMAN expresses
intent, which the daemon then observes. SPEC.md:36 and :41-43 state the contract — the board is
desired state owned by the human, the SQLite store plus runtime is actual state owned by the
daemon, and "the daemon never fights a human drag: human intent wins". Replacing the board with
beads+Dolt while the only web UI is READ-ONLY removes the human's write surface. That is the sole
remaining blocker to the migration; everything else is tractable work (see the description).
== THE COMPLETE HUMAN INTENT VOCABULARY (do not add verbs beyond these without re-deriving) ==
Every human gesture the daemon can observe reduces to four writes. Evidence is by interaction:
1. COMMENT — the only latency-critical write. Two cases:
a. Card parked in Question: reconcile.go:1151 handleQuestion -> Comments() -> tools.go:263-278
detectAnswer. It finds the LAST comment containing marker "agent-hub:awaiting-answer"
(reconcile.go:1105); if ANY comment follows it, that trailing comment IS the answer.
b. Mid-run: tools.go:210-238 ToolAskUser parks the card while the agent stays LIVE polling
GET /api/tool/answer (tools.go:245-252). The run is burning against cfg.Timeout (default 30m,
enforced in check()). Delivery latency here is on the critical path of a running agent.
CONSEQUENCE: any write path slower than ~1 min materially degrades (b). A 5-minute
auto-push debounce consumes ~17% of a default run budget per exchange.
2. STATUS CHANGE — highest-frequency gesture; five human actions share this one operation:
trigger work (drag to Ready -> reconcile.go:234-235 handleReady + domain.CanClaim);
promote an agent-created task out of Triage (created by tools.go:344-350 into
cfg.AgentTasks.TargetBucket, default triage; Triage is a PARKED bucket, reconcile.go:238-244 —
never a claim source); cancel/kill (reconcile.go:254-256 handleTerminal:1525-1560 -> runner.Kill,
outcome killed); route from In Review; park out of the way (unmapped bucket, reconcile.go:228-232).
3. CREATE TASK — title + description + initial status. The description carries the Task Spec YAML
frontmatter (role/model/skills/timeout), parsed by internal/spec/frontmatter.go:36-58.
4. EDIT DESCRIPTION / LABELS — easy to under-rate. When the daemon REJECTS a Task Spec it bounces
the card to Triage (reconcile.go:380) with a comment that literally instructs: "Fix the Task
Spec in the description, then drag the card back to Ready" (comments.go:51-59). Without an edit
path a rejected card is unrecoverable from any device that lacks the bd CLI. Labels are the same
operation class: the type:<name> label selects the task-type preset (spec.go:194-217) and is read
by verdict routing (routing.go:23-42). Exactly one type:* label is legal.
NOT needed: assignees, priorities, due dates, attachments, ordering, reactions. The daemon reads
none of them (grep -rni attachment internal/ returns zero hits). BoardTask carries only
{ID, Title, Description, Bucket, Labels, UpdatedAt} and UpdatedAt has zero readers.
== TWO DELIVERY SHAPES — evaluate SHAPE A FIRST, it may be nearly free ==
SHAPE A: one shared Dolt sql-server; no new code.
Run dolt sql-server on agent-1 beside the daemon; point every bd client at it over the network.
bd supports this explicitly: 'bd dolt set host <ip> [--update-config]', plus port/user/database,
BEADS_DOLT_SERVER_MODE=1, bd init --server (see bd dolt --help; docs/DOLT.md in the beads source
says server mode "connects to a running dolt sql-server for multi-client access ... enables
concurrent agents"). With ONE database there is no push, no pull, no debounce, no divergence and
no merge conflicts. Solves every desk interaction at ~zero engineering cost.
DOES NOT solve: any device without bd + network access to the server (i.e. phone).
Costs: a supervised sql-server process; network exposure of the DB port; migration from embedded
to server mode is backup + 'bd init --server' + restore with a DIFFERENT data dir
(.beads/dolt/ vs .beads/embeddeddolt/) — not a flag flip.
SHAPE B: HTTP write endpoints on the daemon's existing httpapi.
POST /api/v1/board/:id/comment {"text": "..."} -> interaction 1 (DO FIRST)
POST /api/v1/board/:id/status {"status": "open"} -> interaction 2 (DO SECOND)
POST /api/v1/board {"title","description","status"} -> interaction 3
PATCH /api/v1/board/:id {"description","labels"} -> interaction 4
Why the daemon and not the viewer: the daemon runs ON agent-1 next to the authoritative Dolt
working set, so a write through it has ZERO sync latency — it mutates the DB the reconciler reads
and pushes on the daemon's own schedule. This takes the laptop out of the write path, which is
what eliminates the failure CLASS (a local write that reports success and is invisible to the
daemon). Lowering the auto-push interval only narrows the window; it does not remove the class.
MINIMUM VIABLE SLICE = comment + status. Those two cover the blocker and the highest-frequency
gesture. Create/edit can lag because filing new work is a desk activity anyway.
== NON-OBVIOUS COSTS OF SHAPE B (largest hidden cost; read before estimating) ==
- The daemon is LOOPBACK-ONLY today: config.example.yaml line 1, listen: "127.0.0.1:9100".
A human-facing write API means binding off-loopback, which drags in TLS and a real auth story.
- Auth machinery to REUSE, not reinvent: internal/reconcile/tools.go:60-137 mints per-task 256-bit
bearer tokens with a constant-time compare; internal/httpapi/httpapi.go:311-353 does HMAC-SHA256
verification for the Vikunja webhook. What is genuinely NEW is an OPERATOR token with a different
lifetime and scope than a per-run token. Do not reuse per-task tokens for humans.
- TWO WRITE PATHS CAN DIVERGE: if the laptop keeps writing a LOCAL Dolt DB while the HTTP API writes
agent-1's, the merge problem returns. Shape A avoids this by construction. If shipping B alone,
point the laptop's bd at agent-1 as well, or consciously accept Dolt merges.
- Viewer integration: wire the existing read-only viewer's issue rows to POST at these endpoints
(https://dolt.srht.bigb.es/~bigbes/agents-dev/view/milestones). It already renders id/title/
priority/type/status and milestone progress; it is a sourcehut-style page with Log in/Register
in the nav, so an auth context may already exist there.
== INVARIANTS ANY IMPLEMENTATION MUST NOT BREAK ==
1. NEVER return a partial board from Snapshot. A card missing from a snapshot is treated as VANISHED:
reconcile.go:1659 handleVanished KILLS the live run and marks the record cancelled. The Vikunja
adapter refuses a truncated bucket rather than dropping tasks (vikunja/board.go:95-99). See the
open spike on SearchIssues/IssueFilter default limits — this is the gating unknown.
2. Do NOT let machine-generated audit lines into the COMMENT stream. detectAnswer takes the LAST
comment unconditionally, so an injected "status changed to X" line would be consumed as the
human's answer. Beads keeps comments in their own table separate from the events audit log, so
this is currently safe — preserve that separation.
3. Move-then-comment, never comment-then-move (reconcile.go:376-379, :413-414, :1371-1373, :1494-1496).
Only a successful move earns a comment, so a persistently failing move cannot spam one comment
per tick.
4. Persist-before-move; never assume a write landed and never re-read to confirm. Every failed move
converges on a later tick (heal branch reconcile.go:286-299, alignCardToRecord:1436). This
tolerance is what makes a non-transactional board safe.
5. 'ready' MUST map to beads status 'open'. The atomic claim CAS hardcodes it:
internal/storage/issueops/claim.go:47-58 UPDATE ... WHERE id=? AND status='open'. A custom
'ready:active' status would appear in bd ready but would NOT be claimable.
== BEADS PUBLIC API (use it; do NOT import internal/ and do NOT shell out to the CLI) ==
Root package github.com/steveyegge/beads (MIT). Verified against the v1.1.0 source zip.
Open(ctx, dbPath) / OpenFromConfig(ctx, beadsDir) -- the latter respects dolt_mode in
metadata.json, so embedded-vs-server is CONFIGURATION not code.
Storage interface maps ~1:1 to ports.Board:
SearchIssues / GetReadyWork -> Snapshot (SEE SPIKE: default limit unverified)
UpdateIssue(id, {"status": ...}) -> MoveToBucket
AddIssueComment / GetIssueComments (typed, ordered) -> Comment / Comments
CreateIssue -> CreateTask
AddLabel + RemoveLabel -> SwapLabel
RunInTransaction(ctx, commitMsg, fn) batches many writes into ONE Dolt commit with rollback on
error or panic — use it to (a) kill write amplification and (b) make SwapLabel ATOMIC, which is
strictly better than the current Vikunja adapter's documented non-atomic add-then-remove.
GetAllEventsSince(ctx, since time.Time) is a typed change-feed cursor — no hand-rolled SQL.
RemoteStore (via type assertion) exposes Push/Pull so the daemon controls push timing itself.
Escape hatches if the public API ever falls short, in order of preference: direct SQL (bd's own docs
recommend this for extensions); vendor the MIT-licensed code; a shim module declared under
github.com/steveyegge/beads/<x> plus a replace directive (Go's internal rule is a path-prefix check
on the IMPORTING package path, so this legally compiles). internal/ carries no compat guarantees.
UNBLOCKED by ah-wd4.1 (closed): the gating technical unknown is settled and it does NOT rule out a beads-backed Board adapter. beads v1.1.2's SearchIssues with a zero-value IssueFilter is an honest full enumeration — measured exact at 87, 700 and 2500 issues with no default page size — and it returns every status including closed and deferred, because the default hiding is CLI-side only. A safe Snapshot is buildable: count → list → count with a parity assertion and a REFUSAL on mismatch, mirroring internal/vikunja/board.go:95-99. See ah-wd4.1's close reason for the exact call, filter, assertion and the four traps (dead Offset, per-table limit fan-out, Statistics.TotalIssues excluding wisps, cross-table duplicate IDs). So the decision is now a PRODUCT decision, not a technical one. The remaining questions for the full-swap-vs-hybrid-mirror call: - Vikunja's kanban board is the human interface. beads has a Dolt web UI, but does dragging a card between buckets have an equivalent? The whole reconciler design rests on 'the board is the desired state owned by the human', and a human moving a card is the primary input. - The Vikunja adapter is delivered, live-proven and defends against truncation already. What does the swap BUY — one datastore instead of two, and beads-native task hierarchy? Weigh that against re-proving a live path that currently works. - A hybrid mirror means two sources of truth for the same card and a sync direction to define. That is usually worse than either pure option unless one side is strictly read-only. - Note the notifier/board split: cards carry comments (result summaries, failure diagnostics, Q&A answers, delegation reports). Does beads have a comment surface a human reads as naturally?
No outgoing dependencies.
ah-wd4.1
— SPIKE (gating): does the beads Storage API enumerate ALL issues, or silently paginate?
parent-child
| id | ah-wd4 |
| content_hash | 3ab58d5b62b82edabbd207aa06a161044e6c9bd3964169fe9e78b8c3732034dd |
| 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. FEASIBILITY (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&A 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->HTML 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. BLOCKERS (the real cost): 1. 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. 2. 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). 3. 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 <=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. 4. 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. 5. int64 task IDs are load-bearing (branch task-<id>, zellij session, archive filename, tool-token binding, HTTP API, notification URL). Beads ids are strings (ah-1cx.1). Recommended fix: repo-wide int64->string (mechanical, compiler-verified, ~10 files) over a synthetic mapping table that can drift. 6. 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. 7. 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. 8. 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. WHAT 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). OPTIONS: A) 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. B) 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->status 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->string 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. RECOMMENDATION: 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. |
| design | HANDOFF DESIGN — human write path for a beads-backed board. Written 2026-07-20 for a worker with no prior conversation context. Read this whole section before touching code. == WHY THIS EXISTS == Vikunja is not merely storage for agenthubd: it is the surface through which a HUMAN expresses intent, which the daemon then observes. SPEC.md:36 and :41-43 state the contract — the board is desired state owned by the human, the SQLite store plus runtime is actual state owned by the daemon, and "the daemon never fights a human drag: human intent wins". Replacing the board with beads+Dolt while the only web UI is READ-ONLY removes the human's write surface. That is the sole remaining blocker to the migration; everything else is tractable work (see the description). == THE COMPLETE HUMAN INTENT VOCABULARY (do not add verbs beyond these without re-deriving) == Every human gesture the daemon can observe reduces to four writes. Evidence is by interaction: 1. COMMENT — the only latency-critical write. Two cases: a. Card parked in Question: reconcile.go:1151 handleQuestion -> Comments() -> tools.go:263-278 detectAnswer. It finds the LAST comment containing marker "agent-hub:awaiting-answer" (reconcile.go:1105); if ANY comment follows it, that trailing comment IS the answer. b. Mid-run: tools.go:210-238 ToolAskUser parks the card while the agent stays LIVE polling GET /api/tool/answer (tools.go:245-252). The run is burning against cfg.Timeout (default 30m, enforced in check()). Delivery latency here is on the critical path of a running agent. CONSEQUENCE: any write path slower than ~1 min materially degrades (b). A 5-minute auto-push debounce consumes ~17% of a default run budget per exchange. 2. STATUS CHANGE — highest-frequency gesture; five human actions share this one operation: trigger work (drag to Ready -> reconcile.go:234-235 handleReady + domain.CanClaim); promote an agent-created task out of Triage (created by tools.go:344-350 into cfg.AgentTasks.TargetBucket, default triage; Triage is a PARKED bucket, reconcile.go:238-244 — never a claim source); cancel/kill (reconcile.go:254-256 handleTerminal:1525-1560 -> runner.Kill, outcome killed); route from In Review; park out of the way (unmapped bucket, reconcile.go:228-232). 3. CREATE TASK — title + description + initial status. The description carries the Task Spec YAML frontmatter (role/model/skills/timeout), parsed by internal/spec/frontmatter.go:36-58. 4. EDIT DESCRIPTION / LABELS — easy to under-rate. When the daemon REJECTS a Task Spec it bounces the card to Triage (reconcile.go:380) with a comment that literally instructs: "Fix the Task Spec in the description, then drag the card back to Ready" (comments.go:51-59). Without an edit path a rejected card is unrecoverable from any device that lacks the bd CLI. Labels are the same operation class: the type:<name> label selects the task-type preset (spec.go:194-217) and is read by verdict routing (routing.go:23-42). Exactly one type:* label is legal. NOT needed: assignees, priorities, due dates, attachments, ordering, reactions. The daemon reads none of them (grep -rni attachment internal/ returns zero hits). BoardTask carries only {ID, Title, Description, Bucket, Labels, UpdatedAt} and UpdatedAt has zero readers. == TWO DELIVERY SHAPES — evaluate SHAPE A FIRST, it may be nearly free == SHAPE A: one shared Dolt sql-server; no new code. Run dolt sql-server on agent-1 beside the daemon; point every bd client at it over the network. bd supports this explicitly: 'bd dolt set host <ip> [--update-config]', plus port/user/database, BEADS_DOLT_SERVER_MODE=1, bd init --server (see bd dolt --help; docs/DOLT.md in the beads source says server mode "connects to a running dolt sql-server for multi-client access ... enables concurrent agents"). With ONE database there is no push, no pull, no debounce, no divergence and no merge conflicts. Solves every desk interaction at ~zero engineering cost. DOES NOT solve: any device without bd + network access to the server (i.e. phone). Costs: a supervised sql-server process; network exposure of the DB port; migration from embedded to server mode is backup + 'bd init --server' + restore with a DIFFERENT data dir (.beads/dolt/ vs .beads/embeddeddolt/) — not a flag flip. SHAPE B: HTTP write endpoints on the daemon's existing httpapi. POST /api/v1/board/:id/comment {"text": "..."} -> interaction 1 (DO FIRST) POST /api/v1/board/:id/status {"status": "open"} -> interaction 2 (DO SECOND) POST /api/v1/board {"title","description","status"} -> interaction 3 PATCH /api/v1/board/:id {"description","labels"} -> interaction 4 Why the daemon and not the viewer: the daemon runs ON agent-1 next to the authoritative Dolt working set, so a write through it has ZERO sync latency — it mutates the DB the reconciler reads and pushes on the daemon's own schedule. This takes the laptop out of the write path, which is what eliminates the failure CLASS (a local write that reports success and is invisible to the daemon). Lowering the auto-push interval only narrows the window; it does not remove the class. MINIMUM VIABLE SLICE = comment + status. Those two cover the blocker and the highest-frequency gesture. Create/edit can lag because filing new work is a desk activity anyway. == NON-OBVIOUS COSTS OF SHAPE B (largest hidden cost; read before estimating) == - The daemon is LOOPBACK-ONLY today: config.example.yaml line 1, listen: "127.0.0.1:9100". A human-facing write API means binding off-loopback, which drags in TLS and a real auth story. - Auth machinery to REUSE, not reinvent: internal/reconcile/tools.go:60-137 mints per-task 256-bit bearer tokens with a constant-time compare; internal/httpapi/httpapi.go:311-353 does HMAC-SHA256 verification for the Vikunja webhook. What is genuinely NEW is an OPERATOR token with a different lifetime and scope than a per-run token. Do not reuse per-task tokens for humans. - TWO WRITE PATHS CAN DIVERGE: if the laptop keeps writing a LOCAL Dolt DB while the HTTP API writes agent-1's, the merge problem returns. Shape A avoids this by construction. If shipping B alone, point the laptop's bd at agent-1 as well, or consciously accept Dolt merges. - Viewer integration: wire the existing read-only viewer's issue rows to POST at these endpoints (https://dolt.srht.bigb.es/~bigbes/agents-dev/view/milestones). It already renders id/title/ priority/type/status and milestone progress; it is a sourcehut-style page with Log in/Register in the nav, so an auth context may already exist there. == INVARIANTS ANY IMPLEMENTATION MUST NOT BREAK == 1. NEVER return a partial board from Snapshot. A card missing from a snapshot is treated as VANISHED: reconcile.go:1659 handleVanished KILLS the live run and marks the record cancelled. The Vikunja adapter refuses a truncated bucket rather than dropping tasks (vikunja/board.go:95-99). See the open spike on SearchIssues/IssueFilter default limits — this is the gating unknown. 2. Do NOT let machine-generated audit lines into the COMMENT stream. detectAnswer takes the LAST comment unconditionally, so an injected "status changed to X" line would be consumed as the human's answer. Beads keeps comments in their own table separate from the events audit log, so this is currently safe — preserve that separation. 3. Move-then-comment, never comment-then-move (reconcile.go:376-379, :413-414, :1371-1373, :1494-1496). Only a successful move earns a comment, so a persistently failing move cannot spam one comment per tick. 4. Persist-before-move; never assume a write landed and never re-read to confirm. Every failed move converges on a later tick (heal branch reconcile.go:286-299, alignCardToRecord:1436). This tolerance is what makes a non-transactional board safe. 5. 'ready' MUST map to beads status 'open'. The atomic claim CAS hardcodes it: internal/storage/issueops/claim.go:47-58 UPDATE ... WHERE id=? AND status='open'. A custom 'ready:active' status would appear in bd ready but would NOT be claimable. == BEADS PUBLIC API (use it; do NOT import internal/ and do NOT shell out to the CLI) == Root package github.com/steveyegge/beads (MIT). Verified against the v1.1.0 source zip. Open(ctx, dbPath) / OpenFromConfig(ctx, beadsDir) -- the latter respects dolt_mode in metadata.json, so embedded-vs-server is CONFIGURATION not code. Storage interface maps ~1:1 to ports.Board: SearchIssues / GetReadyWork -> Snapshot (SEE SPIKE: default limit unverified) UpdateIssue(id, {"status": ...}) -> MoveToBucket AddIssueComment / GetIssueComments (typed, ordered) -> Comment / Comments CreateIssue -> CreateTask AddLabel + RemoveLabel -> SwapLabel RunInTransaction(ctx, commitMsg, fn) batches many writes into ONE Dolt commit with rollback on error or panic — use it to (a) kill write amplification and (b) make SwapLabel ATOMIC, which is strictly better than the current Vikunja adapter's documented non-atomic add-then-remove. GetAllEventsSince(ctx, since time.Time) is a typed change-feed cursor — no hand-rolled SQL. RemoteStore (via type assertion) exposes Push/Pull so the daemon controls push timing itself. Escape hatches if the public API ever falls short, in order of preference: direct SQL (bd's own docs recommend this for extensions); vendor the MIT-licensed code; a shim module declared under github.com/steveyegge/beads/<x> plus a replace directive (Go's internal rule is a path-prefix check on the IMPORTING package path, so this legally compiles). internal/ carries no compat guarantees. |
| acceptance_criteria | |
| notes | UNBLOCKED by ah-wd4.1 (closed): the gating technical unknown is settled and it does NOT rule out a beads-backed Board adapter. beads v1.1.2's SearchIssues with a zero-value IssueFilter is an honest full enumeration — measured exact at 87, 700 and 2500 issues with no default page size — and it returns every status including closed and deferred, because the default hiding is CLI-side only. A safe Snapshot is buildable: count → list → count with a parity assertion and a REFUSAL on mismatch, mirroring internal/vikunja/board.go:95-99. See ah-wd4.1's close reason for the exact call, filter, assertion and the four traps (dead Offset, per-table limit fan-out, Statistics.TotalIssues excluding wisps, cross-table duplicate IDs). So the decision is now a PRODUCT decision, not a technical one. The remaining questions for the full-swap-vs-hybrid-mirror call: - Vikunja's kanban board is the human interface. beads has a Dolt web UI, but does dragging a card between buckets have an equivalent? The whole reconciler design rests on 'the board is the desired state owned by the human', and a human moving a card is the primary input. - The Vikunja adapter is delivered, live-proven and defends against truncation already. What does the swap BUY — one datastore instead of two, and beads-native task hierarchy? Weigh that against re-proving a live path that currently works. - A hybrid mirror means two sources of truth for the same card and a sync direction to define. That is usually worse than either pure option unless one side is strictly read-only. - Note the notifier/board split: cards carry comments (result summaries, failure diagnostics, Q&A answers, delegation reports). Does beads have a comment surface a human reads as naturally? |
| status | open |
| priority | 3 |
| issue_type | decision |
| assignee | NULL |
| estimated_minutes | NULL |
| created_at | 2026-07-20T00:26:37Z |
| created_by | Eugene Blikh |
| owner | bigbes@gmail.com |
| updated_at | 2026-08-04T23:51:12Z |
| closed_at | NULL |
| closed_by_session | |
| external_ref | NULL |
| spec_id | |
| compaction_level | 0 |
| compacted_at | NULL |
| compacted_at_commit | NULL |
| original_size | NULL |
| sender | |
| ephemeral | 0 |
| wisp_type | |
| pinned | 0 |
| is_template | 0 |
| mol_type | |
| work_type | |
| source_system | |
| metadata | �{} |
| source_repo | |
| close_reason | |
| event_kind | |
| actor | |
| target | |
| payload | |
| await_type | |
| await_id | |
| timeout_ns | 0 |
| waiters | |
| hook_bead | |
| role_bead | |
| agent_state | |
| last_activity | NULL |
| role_type | |
| rig | |
| due_at | NULL |
| defer_until | NULL |
| no_history | 0 |
| started_at | NULL |
| is_blocked | 0 |
| issue_id | ah-wd4 |
| label | milestone:v0.2.0 |
| id | 28966c8a-3247-59a5-b7ef-11484e120482 |
| issue_id | ah-wd4.1 |
| type | parent-child |
| created_at | 2026-07-20T10:48:23Z |
| created_by | Eugene Blikh |
| metadata | �{} |
| thread_id | |
| depends_on_issue_id | ah-wd4 |
| depends_on_wisp_id | NULL |
| depends_on_external | NULL |
| id | 019f7cea-e517-7557-9476-bacfa9af34c6 |
| issue_id | ah-wd4 |
| event_type | created |
| actor | Eugene Blikh |
| old_value | |
| new_value | |
| comment | NULL |
| created_at | 2026-07-20T03:26:36Z |
| id | 019f7cf1-3477-79bc-be78-cf15a6ec2977 |
| issue_id | ah-wd4 |
| event_type | updated |
| actor | Eugene Blikh |
| old_value | {"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"} |
| new_value | {"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."} |
| comment | NULL |
| created_at | 2026-07-20T03:33:30Z |
| id | 019f7e7f-0ba7-75a1-bef6-9198f4bf61ad |
| issue_id | ah-wd4 |
| event_type | updated |
| actor | Eugene Blikh |
| old_value | {"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"} |
| new_value | {"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."} |
| comment | NULL |
| created_at | 2026-07-20T10:48:03Z |
| id | 019fcf30-39f7-7203-8fd5-54cceb56cd8e |
| issue_id | ah-wd4 |
| event_type | updated |
| actor | Eugene Blikh |
| old_value | {"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.","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.","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-20T07:48:03Z"} |
| new_value | {"notes":"UNBLOCKED by ah-wd4.1 (closed): the gating technical unknown is settled and it does NOT rule out a beads-backed Board adapter. beads v1.1.2's SearchIssues with a zero-value IssueFilter is an honest full enumeration — measured exact at 87, 700 and 2500 issues with no default page size — and it returns every status including closed and deferred, because the default hiding is CLI-side only. A safe Snapshot is buildable: count → list → count with a parity assertion and a REFUSAL on mismatch, mirroring internal/vikunja/board.go:95-99. See ah-wd4.1's close reason for the exact call, filter, assertion and the four traps (dead Offset, per-table limit fan-out, Statistics.TotalIssues excluding wisps, cross-table duplicate IDs).\n\nSo the decision is now a PRODUCT decision, not a technical one. The remaining questions for the full-swap-vs-hybrid-mirror call:\n- Vikunja's kanban board is the human interface. beads has a Dolt web UI, but does dragging a card between buckets have an equivalent? The whole reconciler design rests on 'the board is the desired state owned by the human', and a human moving a card is the primary input.\n- The Vikunja adapter is delivered, live-proven and defends against truncation already. What does the swap BUY — one datastore instead of two, and beads-native task hierarchy? Weigh that against re-proving a live path that currently works.\n- A hybrid mirror means two sources of truth for the same card and a sync direction to define. That is usually worse than either pure option unless one side is strictly read-only.\n- Note the notifier/board split: cards carry comments (result summaries, failure diagnostics, Q\u0026A answers, delegation reports). Does beads have a comment surface a human reads as naturally?"} |
| comment | NULL |
| created_at | 2026-08-05T02:51:12Z |
| id | 019fcf4e-4bb3-7b5e-a489-66285bfc414d |
| issue_id | ah-wd4 |
| event_type | label_added |
| actor | Eugene Blikh |
| old_value | NULL |
| new_value | NULL |
| comment | Added label: milestone:v0.2.0 |
| created_at | 2026-08-05T03:24:02Z |
No comments.