~bigbes/agents-dev · issues

d4adrmk7cegu9hu2v7c3rqm2na7gqh9c · 88 rows

idcontent_hashtitledescriptiondesignacceptance_criterianotesstatuspriorityissue_typeassigneeestimated_minutescreated_atcreated_byownerupdated_atclosed_atclosed_by_sessionexternal_refspec_idcompaction_levelcompacted_atcompacted_at_commitoriginal_sizesenderephemeralwisp_typepinnedis_templatemol_typework_typesource_systemmetadatasource_repoclose_reasonevent_kindactortargetpayloadawait_typeawait_idtimeout_nswaitershook_beadrole_beadagent_statelast_activityrole_typerigdue_atdefer_untilno_historystarted_atis_blocked
ah-nyl.1526d73898c2195a2502a4cc9764f09b4e985bd97a0e976a09926b5c4133e47e19fix: adopt-guard livelock + neither-state timeout arm (final verification findings)Final composed-semantics verification (@ 825238d) returned FIX-FIRST with two findings, both pre-analyzed with exact fixes. Scope: internal/reconcile/ ONLY. V1 HIGH (reconcile.go:524): adoptOrFail's finished-row branch guard `run.State == RunStateFinished && !st.Running` fires on ANY non-Running status including hard Completed for the NEXT attempt (exit.json{K+1} present but the K+1 row was never persisted — crash before CreateRun or persist failure whose compensating kill raced a fast-exiting pi). Result (empirically proven by the verifier): terminal record -> alignCardToRecord silently bounces ready->failed on every human re-drag, K+1's real result never persisted/commented, Start never issued, stale exit.json never cleaned — livelock against the human. FIX (verifier-tested in scratch against the full suite): change the guard to `!runtimeEvidence(st)` so hard Completed evidence falls through to the adopt branch (which persists the K+1 row from the evidence and finalizes with its true outcome). Add regression test: finished row K + probe returns hard Completed{attempt K+1, exit 0} -> adopts+finalizes K+1 (row created, in_review, one comment), then K+2 claimable; also the terminal-record variant (card in ready, terminal rec, completed evidence -> adopt path, NOT silent bounce). V2 MEDIUM (reconcile.go:409-421): check() switch has no arm for the neither-state (st.Running==false && st.Completed==false — live session, meta gone/mismatched; reachable when the worktree is rm -rf'd mid-run, MetaAttempt=0). Today: no finalize, no timeout, forever — run wedged in in_progress past any deadline (verifier proved 31min past a 30m timeout, zero kills). FIX: add a third arm — when neither-state persists and now > run.StartedAt + cfg.Timeout: runner.Kill + finalize with OutcomeTimeout (same path as the Running-timeout arm); before the deadline, log at warn (observation degraded) and do nothing. Regression test: running row, status neither (MetaAttempt 0), fake clock past deadline -> Kill called, finalized timeout, card failed; before deadline -> no action. V3 INFO (reconcile_test.go:36): statusCrashed fixture sets MetaAttempt 0, but the real runner reports the surviving meta's attempt on crash inference (0 only when the worktree is gone). Update the fixture/helpers so crashed statuses carry a realistic MetaAttempt (parameterize; keep a worktree-gone variant with 0) — hygiene so future MetaAttempt consumers are tested against real shapes. Constraints: internal/reconcile/ only; testify+culpa; go build/vet/test -race -count=1 ./internal/reconcile/... (never ./...); commit 'reconcile: adopt completed evidence, timeout the neither-state' staging only internal/reconcile; hooks bypassed (git -c core.hooksPath=/tmp/nohooks commit); no push; no .beads/. closed0bugNULLNULL2026-07-13T02:43:36ZEugene Blikhbigbes@gmail.com2026-07-13T03:01:04Z2026-07-13T03:01:04ZNULL0NULLNULLNULL000�{}landed 30292cb: adopt-guard via runtimeEvidence + same-tick finalize of hard completed evidence, neither-state timeout arm w/ shared timeoutKill, fixture hygiene; negative controls confirmed0NULLNULLNULL02026-07-13T02:43:37Z0
ah-nyl.1601a6ca2b6997fd15bcf53d2c2b93acfb1fced4d9dbb080e77d33e9711ddf4dd1fix: wiring review findings 1-7 (flag swallowing, agenthubd tests, polish)Hostile review of the wiring commits (e116576/ae38c10/254c0f8) returned FIX-FIRST. Fix ALL seven. Line refs @ HEAD. W1 BLOCKER (cmd/agenthubd/main.go:70, cmd/ahub/main.go:105,200): stdlib flag.Parse stops at the first positional and nothing checks fs.NArg() -> `agenthubd serve --config ./missing.yaml` silently starts with ./agenthub.yaml defaults (exit 0, WRONG config); `ahub status 127.0.0.1:9188` silently queries the default :9100 (wrong daemon, exit 0). FIX: after each fs.Parse (three call sites), reject fs.NArg() > 0 with a usage error to stderr and exit 2. Tests for all three sites. W2 BLOCKER-adjacent (cmd/agenthubd): zero tests despite run() being injection-shaped. ADD minimum: bad flag -> exit 2; positional arg -> exit 2 (W1); missing/broken config -> exit 1; invalid --log-level -> exit 2; connection-refused preflight -> exit 1 AND stderr contains the operator-friendly preflight message. Use the injected args/stderr seams; httptest fake vikunja where needed (views endpoint refusing/absent). Keep each test <2s. W3 LOW (cmd/ahub/main.go:219-230 + internal/config/config.go:422-427): single-problem validate-config prints the path twice and skips the bullet format (culpa.Join(1) returns the bare error so the multi-unwrap loop never fires); also the existing two-problem test passes via Contains even if the multi-unwrap loop is deleted. FIX in ahub only (do not change config's wrap): fallback branch prints the problem without the duplicated prefix; pin the EXACT multi-line output format for the >=2 case and the exact single-line format for the ==1 case with require.Equal. W4 LOW (internal/httpapi/httpapi.go:116-119): wire http.Server.ErrorLog = slog.NewLogLogger(<handler>, slog.LevelError) so per-connection panics/header noise join the structured stream instead of log.Default() raw text. Test optional (constructor wiring assertion is enough). W5 INFO (cmd/agenthubd/main.go:184): the explicit stop() next to defer stop() is load-bearing — it unregisters signal handling so a second SIGTERM during the drain hard-kills (desired escape hatch). Add the pinning comment so a cleanup doesn't remove it. W6 INFO (internal/httpapi run-exit): trailing garbage after the JSON object is accepted (Decode reads one value). Add a dec.More() strictness check -> 400 on trailing content; adjust/add test. W7 INFO (cmd/agenthubd/main.go:117): SIGTERM during the preflight window logs the scary "startup preflight failed ... check vikunja.url" message and exits 1. Branch on errors.Is(err, context.Canceled) -> quiet "shutdown requested during startup" log, still exit 1 (or 0 — pick 1 for "did not reach ready", document in the message). Test if cheap via the run() seam. Constraints: scope = cmd/agenthubd/, cmd/ahub/, internal/httpapi/. A sibling agent is writing e2e/ concurrently — do NOT touch e2e/, internal/<anything else>, docs/, .beads/, go.mod. Conventions: testify, culpa, slog. Build/vet/test -race ONLY your three packages; never ./... . Commits: 'cmd: reject positional args, add agenthubd run() tests' + 'httpapi: strict run-exit decode, slog ErrorLog' (split as sensible); hooks bypassed (git -c core.hooksPath=/tmp/nohooks commit); retry on index.lock; no push. closed0bugNULLNULL2026-07-13T03:34:21ZEugene Blikhbigbes@gmail.com2026-07-13T03:47:12Z2026-07-13T03:47:12ZNULL0NULLNULLNULL000�{}landed c07e697+97133f9: NArg guards x3 w/ tests, agenthubd run() suite (9 tests), exact validate-config formats, slog ErrorLog, strict decode, stop() comment, calm preflight shutdown (found NotifyContext cause never unwraps to Canceled on go1.26)0NULLNULLNULL02026-07-13T03:34:22Z0
ah-nyl.2160899e1969f48c186279e62ca24ed7aa7f362fd1b56dc90ed2247c4cf660322store: SQLite implementation of ports.StoreImplement internal/store per docs/SPEC.md SS7 (read SPEC fully; SS5-7 normative). ports.Store on modernc.org/sqlite. Deliverables: - internal/store/store.go: New(path string) (*Store, error) — opens DB, applies PRAGMAs (WAL, busy_timeout=5000, foreign_keys=on), runs migrations; Close(). - Migrations: embedded schema.sql (embed package), applied under a PRAGMA user_version gate (hand-rolled, target version 1). DDL exactly per SPEC SS7. - All ports.Store methods with the documented semantics: GetTask/LatestRun return (nil, nil) when absent; CreateRun surfaces UNIQUE(task_id, attempt) violation as a distinguishable error (exported sentinel ErrDuplicateRun); UpsertTask insert-or-update by id; timestamps RFC3339 UTC; contexts honored (database/sql *Context variants everywhere). - Compile-time check: var _ ports.Store = (*Store)(nil). Tests (stdlib testing only, t.TempDir() databases): round-trip every method; absent-row nil,nil; duplicate run -> ErrDuplicateRun; upsert updates fields + updated_at; ListTasks ordering deterministic (by id); events append + monotonically increasing seq; migration idempotence (New twice on same file). Constraints: work ONLY under internal/store/. Do not modify go.mod/go.sum (deps are already there), other packages, or .beads/. Build/test ONLY your package: go build ./internal/store/... && go vet ./internal/store/... && go test ./internal/store/... . Commit with 'store: ...' staging only internal/store. Do NOT push. closed1taskNULLNULL2026-07-12T23:34:33ZEugene Blikhbigbes@gmail.com2026-07-13T00:10:13Z2026-07-13T00:10:13ZNULL0NULLNULLNULL000�{}landed e3749f1, green0NULLNULLNULL02026-07-12T23:55:04Z0
ah-nyl.3c5a4d9c93b65bce4f1b5dea36aa896d1dec99ba8a4a1c9579ace421cbb9f8d21vikunja: Board adapter over the REST APIImplement internal/vikunja per docs/SPEC.md SS8 (read SPEC fully; SS6, SS8 normative). ports.Board over the Vikunja 2.3.0 REST API. Deliverables: - New(cfg config.Vikunja, logger *slog.Logger) (*Client, error) storing an http.Client with a sane timeout (~15s). - Bucket resolution per SPEC SS8: locate the kanban view of the configured project, build title->bucketID and bucketID->canonical-name maps from the config buckets mapping; missing configured title = error listing found titles. Resolve lazily on first use and cache; provide a Refresh path when a lookup misses (board edited). - Snapshot(ctx): tasks of the project with canonical bucket names ("" for unmapped buckets), honoring pagination. - MoveToBucket(ctx, taskID, canonical), Comment(ctx, taskID, markdown), each per SPEC SS8 endpoints. - Error style: non-2xx -> error with method, path, status, and <=200 bytes of body. - Compile-time check: var _ ports.Board = (*Client)(nil). IMPORTANT — verify the real API contract before coding: the live OpenAPI JSON is at https://tasks.bigb.es/api/v1/docs.json (Swagger UI at /api/v1/docs) — fetch it (no auth needed for the spec itself) and confirm exact paths, request/response shapes, and pagination headers for: project views list, kanban view tasks, buckets list, bucket task move, comment create (Vikunja uses PUT-for-create), single task get. If the live spec is unreachable, use the upstream docs at https://vikunja.io/docs/ and pin your best understanding in code comments + fixtures. SPEC SS8's endpoint list is the expected shape, not gospel — trust the OpenAPI. Tests: httptest.Server fixtures (JSON canned from the OpenAPI shapes) covering snapshot incl. pagination + unmapped buckets, bucket resolution failure (helpful error), move, comment, non-2xx error rendering. No live-network tests. Constraints: work ONLY under internal/vikunja/. No go.mod changes, no other packages, no .beads/. Build/test ONLY your package (go build/vet/test ./internal/vikunja/...). Commit 'vikunja: ...' staging only internal/vikunja. Do NOT push. closed1taskNULLNULL2026-07-12T23:34:34ZEugene Blikhbigbes@gmail.com2026-07-13T00:10:14Z2026-07-13T00:10:14ZNULL0NULLNULLNULL000�{}landed a1d6038, green; OpenAPI deviation from SPEC noted in package docs (flat paginated view tasks)0NULLNULLNULL02026-07-12T23:55:04Z0
ah-nyl.4ff6bd742ed8006517b016443e69e35e99531123b2198633480125843c1ee2f40runner: pi+zellij implementation + ahub-run supervisorImplement internal/runner + cmd/ahub-run per docs/SPEC.md SS9 (read SPEC fully; SS6, SS9 normative). This is the trickiest package — the SS9 contract (worktree layout, .task/ files, status precedence) is normative; follow it to the letter. Deliverables: - internal/runner: New(cfg *config.Config, logger *slog.Logger) *PiZellij implementing ports.Runner (compile-time check var _ ports.Runner = ...). - Start: create/reuse worktree + branch per SS9 (git -C <repo> worktree add ...; handle existing worktree dir and existing branch for retries), write .task/PROMPT.md and .task/meta.json, worktree-local ignore via the resolved git-dir info/exclude per SS9, ensure zellij session (zellij attach --create-background task-<id>), spawn the pane (zellij --session task-<id> run --cwd <worktree> -- ahub-run --task-id N --attempt K --report-url <url> -- pi --mode json -p @.task/PROMPT.md --model <model> --no-skills --no-extensions [pi_args...]). Return RunInfo. - Status: precedence per SS9 — exit.json => Completed (outcome success/error by code); else session alive in `zellij list-sessions --short` => Running with LastEvent=mtime(events.jsonl); else Completed with OutcomeCrashed, exit -1. Parse PiSession + CostUSD from events.jsonl leniently: scan lines as loose JSON maps; session id from the first object that has a plausible session identifier; cost accumulated from usage/cost fields when present. IMPORTANT: pi 0.70.2 is installed locally — empirically capture a real `pi --mode json -p 'say hi'` JSONL sample (any cheap/configured model, or ask for the shape via `pi --help` + a dry attempt; if no model is invocable offline, mark the parser 'best-effort, fixture-based' and derive fixtures from pi's documented event shape), commit the sample as a testdata fixture, and pin the parser to it. - Kill: zellij kill-session + best-effort delete-session. Summary: git log/diff per SS9. - ALL zellij/pi/git argv construction centralized in commands.go with unit tests asserting exact argv (SS9 requirement). - cmd/ahub-run: supervisor per SS9 contract — tee child stdout to .task/events.jsonl (stderr passthrough), atomic exit.json (tmp+rename), best-effort POST to --report-url (2s timeout, 1 retry), signal-death -> 128+sig, --keep-pane default true iff $ZELLIJ set (then print resume hint + exec $SHELL), false => exit with child code. Tests: stub `zellij`/`pi`/(where sensible `git` is real — use real git with a t.TempDir() repo for worktree tests) as executable scripts prepended to PATH; cover: worktree create+reuse, argv construction, status precedence matrix (exit.json / alive / gone), events parsing from fixture, ahub-run end-to-end via os/exec (tee, atomic exit.json, report POST to httptest, exit-code mapping). No sleeps >100ms; no real zellij sessions in tests. Constraints: work ONLY under internal/runner/ and cmd/ahub-run/. No go.mod changes, no other packages, no .beads/. Build/test ONLY yours: go build ./internal/runner/... ./cmd/ahub-run/... && go vet <same> && go test <same>. Commit 'runner: ...' staging only your paths. Do NOT push. closed1taskNULLNULL2026-07-12T23:35:16ZEugene Blikhbigbes@gmail.com2026-07-13T00:20:36Z2026-07-13T00:20:36ZNULL0NULLNULLNULL000�{}landed 4348d2b, 37 tests race-clean, empirical pi JSONL fixtures + zellij probes; SPEC corrections: git-common-dir exclude path, attach --create-background not idempotent (exit 1 tolerated), retry clears stale exit.json0NULLNULLNULL02026-07-12T23:55:04Z0
ah-nyl.5a229e6751c3b6c7e11fca843ebea6db9830e910a9d3b730b6df4481690de2551reconcile: the control loopImplement internal/reconcile per docs/SPEC.md SS10 (read SPEC fully; SS5, SS6, SS10, SS12 normative). The reconciler imports ONLY internal/domain, internal/ports, internal/config (+ stdlib). Sibling packages (store/vikunja/runner) may not compile yet — you must not import or build them. Deliverables: - New(deps Deps) *Reconciler where Deps{Store ports.Store; Board ports.Board; Runner ports.Runner; Notifier ports.Notifier; Cfg *config.Config; Log *slog.Logger; Now func() time.Time}. - Run(ctx): loop — iterate every cfg.PollInterval, plus immediately when poked; Poke() (non-blocking, coalescing via 1-buffered channel); clean shutdown on ctx cancel. - iterate(ctx) implementing SPEC SS10 verbatim: the bucket switch (ready claim/heal; in_progress adopt-or-fail/check; terminal buckets kill+align), vanish handling for store tasks missing from snapshot, claim ordering (persist BEFORE moving the card), check() with finalize on Completed, timeout kill (Now() - StartedAt > cfg.Timeout -> Kill + OutcomeTimeout), comments per the SS10 templates (claim comment with attach hint; success comment with DiffStat+Commits; failure comment with outcome + fenced tail of events — obtain the tail via Runner.Summary? No: events tail is runner-internal; include what RunStatus/Summary give you: outcome, exit code, diff stat if any, and reference to the worktree path + zellij attach hint. Keep comment builders as small pure funcs with tests). - Prompt rendering per SPEC SS12: text/template over the role prompt file with {ID, Title, Description, Branch, RepoSlug}; render errors -> comment + move to failed (per SS12). - Per-task action errors: log, append event where sensible, continue with other tasks; Snapshot error aborts the iteration (SS10). - Every state-changing action appends a domain.Event via Store.AppendEvent. Tests (the heart of this task — table-driven, in-memory fakes for all four ports, fake clock): claim happy path (verify order: CreateRun+UpsertTask precede MoveToBucket; comment posted), exit-0 finalize -> in_review + summary comment, exit!=0 -> failed + diagnostic comment, timeout -> Kill + failed(timeout), human drag to cancelled mid-run -> Kill + killed + aligned, crash-between-persist-and-move heal (rec in_progress + bucket ready -> just MoveToBucket), adopt (bucket in_progress, no rec, runner reports running), adopt-fail (no runtime -> failed + comment), vanish (in store, not on board -> kill + cancelled), poke triggers immediate iteration, ctx cancel stops Run. Fakes record calls for assertion; no real time.Sleep beyond trivial. Constraints: work ONLY under internal/reconcile/. No go.mod changes, no .beads/. Build/test ONLY: go build ./internal/reconcile/... && go vet ./internal/reconcile/... && go test ./internal/reconcile/... . Commit 'reconcile: ...' staging only internal/reconcile. Do NOT push. closed1taskNULLNULL2026-07-12T23:35:17ZEugene Blikhbigbes@gmail.com2026-07-13T00:15:01Z2026-07-13T00:15:01ZNULL0NULLNULLNULL000�{}landed 2acf7c4, 11/11 scenarios + extras, race-clean, testify+culpa applied; ambiguity rulings recorded in agent report (vanish idempotency, adopt-or-fail refinement, crash-window adopt)0NULLNULLNULL02026-07-12T23:55:05Z0
ah-nyl.60f3627c5d82d75a475ee9648fba27c211ab6ca3b93bbb47bbb18b50ec3c92db4wiring: httpapi + agenthubd + ahub CLIsWire the daemon together per docs/SPEC.md SS3, SS11 (read SPEC fully). All sibling packages now exist — full-tree builds are allowed and expected in THIS task. Deliverables: - internal/httpapi: loopback server per SS11 — GET /healthz; POST /internal/v1/run-exit {task_id,attempt,exit_code} validated -> calls a RunExitHook (func injected by main; it pokes the reconciler); GET /api/v1/status -> JSON {tasks:[TaskRecord+latest Run], generated_at}. stdlib net/http + 1.22 mux patterns; graceful shutdown; tests via httptest. - cmd/agenthubd: flags --config (default ./agenthub.yaml) --log-level; load config, open store, construct vikunja client, runner, no-op notifier (slog), reconciler; start httpapi + reconciler.Run; SIGINT/SIGTERM -> graceful stop (context cancel, http shutdown, store close). slog JSON to stderr. - cmd/ahub: subcommands (stdlib flag, no cobra): `status` (GET /api/v1/status from --addr default 127.0.0.1:9100, human-readable table + --json raw), `validate-config` (load config, print OK or the collected errors, exit code accordingly), `version` (var set via -ldflags, default "dev"). - justfile: verify `just build` produces bin/agenthubd bin/ahub bin/ahub-run (adjust if needed). - Smoke check you must run and make pass: `go build ./... && go vet ./... && go test ./...` (whole tree), then `bin/agenthubd --config config.example.yaml` with a fake VIKUNJA_TOKEN env — it must start, log the bucket-resolution failure gracefully (retry next tick, not crash-loop-exit), and /healthz must answer. Note in the bead comment if SPEC/behavior forced any deviation. Constraints: you own cmd/agenthubd, cmd/ahub, internal/httpapi, plus minimal glue edits elsewhere ONLY if a sibling package has an integration bug you must fix to link (document any such fix in its own commit '<pkg>: fix ...'). No .beads/ changes. Commits: 'httpapi: ...', 'cmd: ...'. Do NOT push. CONVENTION UPDATE (see SPEC §13, commit 5ce35c5+): tests use testify (require/assert); errors via auxilia culpa; agenthubd wires scribe handlers (TintHandler for ahub CLI, JSON or Multi for the daemon); steward MAY be used for daemon lifecycle wiring if it stays simple. testify + auxilia already in go.mod. REVIEW INPUT (data-layer review finding 4): agenthubd startup must PREFLIGHT the board — construct the vikunja client and resolve the configured bucket titles once at startup; a missing title / no-kanban-view error at that point is FATAL with a clear message (SPEC §8 'fatal config error'), while the same error later at runtime stays retryable inside the loop. Wire this into cmd/agenthubd.closed1taskNULLNULL2026-07-12T23:35:55ZEugene Blikhbigbes@gmail.com2026-07-13T03:18:09Z2026-07-13T03:18:09ZNULL0NULLNULLNULL000�{}landed e116576+ae38c10+254c0f8: httpapi 3 endpoints, agenthubd wiring w/ fatal preflight + graceful shutdown, ahub CLI; scribe JSON/Tint handlers; steward skipped (justified); full happy-path smoke against fake vikunja done0NULLNULLNULL02026-07-13T03:01:57Z0
ah-nyl.77c266e5f2bb15c0bb076a169f5f91846b9e15c8d4397026b55d441ea8638463de2e: harness with fake vikunja + stub piBuild the end-to-end harness per docs/SPEC.md SS1/SS14 stage-1 acceptance: prove the full loop without live services. Build tag e2e, directory e2e/, run via `just e2e`. Scenario (single test, subtests per phase): temp dir with (a) a real git repo as the target repo (one commit on master), (b) fake Vikunja: httptest server implementing the subset internal/vikunja uses (views, kanban tasks, buckets, move, comment) over in-memory state you can mutate from the test; (c) stub `pi` script on PATH that reads .task/PROMPT.md, makes a commit in the cwd repo ('stub: change'), emits 2-3 plausible JSONL lines to stdout, exits 0; (d) real zellij is NOT used: stub `zellij` script that for `run` executes the wrapped command directly (background), for attach --create-background no-ops, for list-sessions prints active names from a state file — i.e. simulate sessions with files. (e) real ahub-run and agenthubd binaries built by the test (go build into t.TempDir()). Flow: start agenthubd with a generated config (short poll_interval ~200ms) -> put a task in fake-Vikunja Ready bucket -> wait (poll with deadline, no fixed sleeps) for: card moved to In Progress with claim comment; then card in In Review with a comment containing the diff stat; store db has task in_review + run finished/success (inspect via ahub status --json against the daemon). Negative subtest: stub pi exits 1 -> card lands in Failed with diagnostic comment. Timeout subtest optional (only if cheap with the fake clock — the daemon uses real time; skip if it needs sleeps >2s and note why). Constraints: everything under e2e/ (+ justfile tweak if needed). Full-tree build allowed. If you find integration bugs in other packages, fix them in separate commits '<pkg>: fix ...' with a test where feasible. No .beads/ changes. Commit 'e2e: ...'. Do NOT push. CONVENTION UPDATE (see SPEC §13): tests use testify (require/assert); errors via culpa. Deps already in go.mod.closed2taskNULLNULL2026-07-12T23:35:56ZEugene Blikhbigbes@gmail.com2026-07-13T03:40:07Z2026-07-13T03:40:07ZNULL0NULLNULLNULL000�{}landed 892cbda: hermetic full-loop harness (fake vikunja w/ per-response pagination + preflight support, argv-faithful zellij/pi stubs, real binaries); happy/failure/timeout paths green 4x no flakes, just e2e 3.3s; zero integration bugs found0NULLNULLNULL02026-07-13T03:18:19Z0
ah-nyl.856c1d153e12c3a78d437729c157cf9955e07e9f7fdb9f846e61d0984e74e1941refactor: adopt go.bigb.es/auxilia (scribe/culpa/steward) where it paysPost-MVP, per SPEC SS13: evaluate replacing slog wiring with scribe, error plumbing with culpa, cmd wiring with steward. Load the auxilia skill for API reference. Only adopt where it reduces code; keep diffs reviewable per package. Blocked until stage-1 e2e is green and stable.closed3choreNULLNULL2026-07-12T23:35:57ZEugene Blikhbigbes@gmail.com2026-07-13T00:09:34Z2026-07-13T00:09:34ZNULL0NULLNULLNULL000�{}superseded: auxilia + testify adopted as baseline conventions before wave 2 (user directive); refit of landed packages tracked in a dedicated bead0NULLNULLNULL0NULL0
ah-nyl.9b1c7f86f50a988844039d6deb7d3411e2391e037cc653193d484a51aec520898refit: testify + culpa in domain/config/store/vikunjaThe repo conventions changed after these four packages landed (SPEC SS13 now): tests must use testify (github.com/stretchr/testify require/assert), and errors must be constructed/wrapped via go.bigb.es/auxilia/culpa (New/Errorf/Wrap/Wrapf; keep stdlib errors.New sentinels where callers use errors.Is). Both deps are already in go.mod. Scope — exactly these packages, which are DONE and committed: internal/domain, internal/config, internal/store, internal/vikunja. Do NOT touch internal/runner, internal/reconcile, cmd/ (siblings are working there right now), go.mod/go.sum, docs/, .beads/. Work: 1. Convert all *_test.go in the four packages to testify: require.* for fatal paths (setup, errors), assert.* for value checks where the test can meaningfully continue. Preserve every existing test case and its semantics — this is a mechanical style conversion, not a rewrite; keep table-test structures intact. 2. Convert error construction in non-test code of the four packages from fmt.Errorf to culpa equivalents (fmt.Errorf("...: %w", err) -> culpa.Wrapf/Wrap; fmt.Errorf without %w -> culpa.Errorf/New). Keep exported sentinels (ErrDuplicateRun, ErrRunNotFound) as-is so errors.Is keeps working; culpa-wrapped returns must still satisfy errors.Is against those sentinels where they did before (culpa supports errors.Is chains — verify with the existing duplicate-run test). Multi-error collection in config validation may stay errors.Join or move to culpa.Join — pick what keeps the error text readable and the tests passing with minimal churn. 3. No signature changes, no behavior changes, no coverage loss. Per-package verify: go build/vet/test for ./internal/domain/... ./internal/config/... ./internal/store/... ./internal/vikunja/... (these four only; NEVER ./...). Commit per package or as one commit: 'refit: testify + culpa in <pkgs>' staging only the four package dirs. Hooks bypassed (git -c core.hooksPath=/tmp/nohooks commit). Do NOT push. closed1taskNULLNULL2026-07-13T00:09:36ZEugene Blikhbigbes@gmail.com2026-07-13T00:26:34Z2026-07-13T00:26:34ZNULL0NULLNULLNULL000�{}landed b86928a: testify+culpa across domain/config/store/vikunja, 30 top-level + 37 subtests preserved, errors.Is/As behavior verified0NULLNULLNULL02026-07-13T00:09:36Z0
ah-oeq4c7fd7ca7b85255c43283cb38978e25fe359f7f32e03dd3033ee0860fe52a5bfStage 5: VM deploy, web terminal, virtual keys, spec editor, MCP facadePer SPEC SS14.5: dedicated Proxmox VM (deploy via systemd, precedent remote/basic-vmagent in phoebe-lab); zellij web / ttyd behind Traefik (needs file provider for non-Docker backend); per-task LiteLLM virtual keys with max_budget + /spend attribution; spec-editor page; MCP facade over the agent API; multi-repo + pipeline roles.open4epicNULLNULL2026-07-12T23:36:30ZEugene Blikhbigbes@gmail.com2026-07-12T23:36:30ZNULLNULL0NULLNULLNULL000�{}0NULLNULLNULL0NULL0
ah-oeq.19a15614b6f1f39fdb588f3741ba183394c3e79f12d2cb9c787bea096ba534911Per-task repo selection via Task SpecSPEC line 588 defers this to Stage 5: the daemon binds to a single repo and the Task Spec cannot choose one. Add per-task repo resolution (frontmatter key resolving against the configured repos list) so one board drives multiple repos. Filed under the Stage 5 epic since SPEC labels it Stage 5 even though the epic's original enumeration did not list it. Marker-sweep audit 2026-07-20.open4featureNULLNULL2026-07-19T23:38:20ZEugene Blikhbigbes@gmail.com2026-07-19T23:38:20ZNULLNULL0NULLNULLNULL000�{}0NULLNULLNULL0NULL0
ah-ptu656fcc1b3e50d2d0c581f01969cc197111ed13fffaf60d43d5c82a961ec5f638Yonote Q&A bot loop: poll doc comments, answer via board tasks (post-wave-2.5)Why: the operator wants to ask questions as comments on Yonote docs (e.g. on agent-published reports) and have the bot answer in-thread. What: a polling loop (daemon has no public ingress — loopback-only listen — so webhooks are out for now) over events.list filtered to comments.create since a persisted cursor; qualifying comments spawn a question-answer board card whose description carries the doc URL (ah-2lh materialization then feeds the agent the doc as context); the answer is delivered via comments.create (parentCommentId threading) + optional comments.resolve under the bot identity. EXPLICITLY NOT wave 2.5: needs cursor persistence, a second poll loop, trigger-convention + mention-encoding verification, and a reply-channel decision. Depends on ah-gxa (comments methods; extend with ListEvents when picked up) and ah-25e (reply channel + bot write provisioning).# Yonote Q&A bot loop (post-wave-2.5 — honest scope, NOT part of wave 2.5) Goal: the operator asks questions IN Yonote (comments on docs — e.g. on agent-published reports or on specs); the bot notices, an agent produces the answer, the bot replies in-thread and resolves the comment. ## Verified surface this builds on (2026-07-18, bigbes.yonote.ru 1.47.1) - comments.list {entityId, isResolved filter, threadId, limit ≤100, offset} → {data:{comments:[…]}, pagination, total} — LIVE-verified (nested envelope; items carry threadId, isResolved, threadCommentsCount, attachments). - comments.create {entityType:"document", entityId, text, parentCommentId} → {data:{comment}} — LIVE-verified end-to-end on a probe doc: text stored verbatim (plain string), threading via parentCommentId; a reply's threadId points at the root comment. - comments.resolve {id, isResolved} → data.comment — spec'd (v2-preview), plus the product MCP server exposes yonote_resolve_comment, so the surface is real. - events.list POST {name, actorId, documentId, collectionId, limit, offset, sort/direction} — LIVE-verified (returned {pagination, data:[{id, name, actorId, actor, documentId, collectionId, createdAt, data, modelId}]}); the `name` filter ("objects.verb", e.g. "comments.create") is spec'd — server-side event filtering makes cheap incremental polling possible. - webhookSubscriptions.list responds LIVE (empty list) though it is UNDOCUMENTED in both Yonote OpenAPI specs (Outline heritage; auth.info policies include create/listWebhookSubscription). BUT the daemon binds loopback-only (config validateLoopback) with no public ingress, so v1 transport = POLLING events.list; webhooks stay a documented future option if ingress appears. ## Sketch (design decisions OPEN — settle when picked up) - Poll loop: own ticker (~60s), separate from the reconcile tick; cursor persisted in the store (new kv row, e.g. yonote_cursor = last seen event id/createdAt). MUST verify then: events.list ordering + Sorting params for reliable incremental reads (direction/sort exist in the Pagination /Sorting schemas but were not exercised). - Trigger filter (pick one): (a) new unresolved comments on docs AUTHORED BY THE BOT (createdById == bot user id from AuthInfo) — simplest, covers "questions about agent output"; (b) explicit @mention of the bot — mention ENCODING in comment text is UNVERIFIED (probe stored plain text verbatim; the editor may encode mentions as structured nodes invisible to .text); (c) any unresolved comment in configured collections. - Self-trigger guard: ignore events with actorId == bot user id. - Answer path: reuse the whole existing pipeline — create a board card (existing CreateTask port, agent_tasks bucket conventions) of a question-answer task type whose description contains the doc URL (the materialization bead then auto-exports the doc as context for the agent!) plus the question thread text; the agent writes the answer; the daemon replies via comments.create with parentCommentId = thread root and optionally comments.resolve. Reply delivery needs either a small extension of the publish channel ({"reply_to_comment": …}) or a dedicated .task/answer.json — decide at design time. ## Why not wave 2.5 Needs cursor persistence, a second poll loop, trigger-convention and mention-encoding verification, and a reply file-channel decision — none of which artifact materialization or the publish lane depend on. Client extensions required when picked up: ListEvents (events.list) and possibly webhookSubscriptions.* — add to internal/yonote then, not now. Depends on: internal/yonote client bead (comments methods land there), publish-lane bead (reply channel shape + bot write provisioning). Live evidence (2026-07-18): events.list POST works — {pagination, data:[{id, name, actorId, actor, documentId, collectionId, createdAt, data, modelId}]}, event names 'objects.verb' (observed documents.permanent_delete from the trash-purge cron); the name/actorId/documentId/collectionId request filters are v1-spec'd. comments.create/list verified end-to-end on the probe doc (threading via parentCommentId; replies carry threadId of the root; text stored verbatim). comments.resolve spec'd + present as yonote_resolve_comment in the product's own MCP server. webhookSubscriptions.list responds live (empty, UNDOCUMENTED in both OpenAPI specs — Outline heritage; auth.info policies include createWebhookSubscription) — future option only, blocked on public ingress. UNVERIFIED for pickup: events.list ordering/Sorting params for reliable incremental cursors; @mention encoding inside comment text (probe stored plain text; the editor may use structured nodes invisible in .text — trigger option (a) 'comments on bot-authored docs' avoids the question entirely).closed3featureEugene BlikhNULL2026-07-18T15:04:41ZEugene Blikhbigbes@gmail.com2026-07-18T18:47:53Z2026-07-18T18:47:53ZNULL0NULLNULLNULL000�{}Implemented: events.list poll loop w/ kv cursor + exactly-once spawn, threaded answer delivery via answer.json, config yonote.qa block, store v5, prompts/qa.md. Rollout/live-verify tracked in ah-eje.0NULLNULLNULL02026-07-18T18:19:54Z0
ah-tc27d54ef700ae480aa00a8fdd268e4a073722f55c14bd0fdba7404db32ee366a57agenthubd review-branch push to srht 403 (agent lacks push creds)Live run on agent-1: a successful task lands In Review but the review-branch publish fails: 'git push: fatal: unable to access https://git.srht.bigb.es/~bigbes/agent-demo/: 403'. The config repos[].review_remote is an https srht URL; the agenthub user has no push credential for it. Options: (a) use an SSH review_remote git@git.srht.bigb.es:~bigbes/agent-demo and add the agenthub user's SSH public key to srht with push ACL on agent-demo; (b) an https personal-access-token credential helper for the agenthub user. Non-blocking: the run succeeds and lands In Review regardless; only the review-branch push + review link is missing.closed3taskNULLNULL2026-07-17T21:29:25ZEugene Blikhbigbes@gmail.com2026-07-18T04:51:38Z2026-07-18T04:51:38ZNULL0NULLNULLNULL000�{}Fixed + verified live 2026-07-18. Gave the agenthub user an ed25519 SSH key (/var/lib/agenthub/.ssh/id_ed25519), registered its pubkey on bigbes' srht account via a direct meta GraphQL createSSHKey mutation (hut CLI failed on a fingerprint schema mismatch vs this self-hosted srht version; key id 2). srht git SSH is on PORT 2222 (not 22 — :22 is the host sshd). agenthub ~/.ssh/config maps git.srht.bigb.es -> Port 2222 + IdentityFile + accept-new. Switched the live config review_remote from https to git@git.srht.bigb.es:~bigbes/agent-demo. Verified: agenthub authenticates ('Hi bigbes!'), a manual branch push succeeds, and a fresh daemon run (attempt 2) landed In Review WITH the review link and no 403. SECURITY NOTE: the key is on bigbes' own account -> push access to all ~bigbes repos. Least-privilege alternative (a dedicated agent-hub srht user + per-repo ACL) is deferred; acceptable for the agent-demo sandbox.0NULLNULLNULL0NULL0
ah-tqc0ec5f3f94c113689a7606ace2c0a5475ee68df56a367f991d8d4967233d03efeRunner: an errored-final-turn pi run (exit 0, zero tokens) finalizes as successLive incident 2026-07-19 (task 4, Q&A 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&A 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.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. FINDING 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/<model>), 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.in_progress2bugEugene BlikhNULL2026-07-19T05:38:11ZEugene Blikhbigbes@gmail.com2026-08-04T23:31:58ZNULLNULL0NULLNULLNULL000�{}0NULLNULLNULL02026-08-04T23:24:53Z0
ah-tz04edd2663acbacfce6ea3669699809f3e06dfafddd847485f1f5f0ca6072a0285Surface agent closing summary / review verdict in the In Review commentdomain.RunSummary carries only DiffStat + Commits, so the comment shows diffstat+commits+link but not what was done. Designer and validator both END with a one-line summary/verdict; that line is lost (only in docs/tasks/slug.md). Capture the agent final summary (from exit.json or last event) into RunSummary and render it in successComment. Prereq for verdict-driven automation.closed3featureEugene BlikhNULL2026-07-18T06:30:21ZEugene Blikhbigbes@gmail.com2026-07-18T06:46:05Z2026-07-18T06:46:05ZNULL0NULLNULLNULL000�{}RunSummary gains a Summary field read from .task/summary.md (runner.readSummaryFile, trim+2000-rune tail-truncate, missing=empty). successComment renders it as a blockquote below the header. designer/validator/coder prompts now write their closing summary/verdict there. Verified: unit tests green in isolation AND combined with ah-bkr on master; full-module build OK. Merged (rebased onto master). NOTE: follow-up needed — the blockquote markdown isn't yet handled by ah-bkr's HTML converter.0NULLNULLNULL02026-07-18T06:38:47Z0
ah-wd43ab58d5b62b82edabbd207aa06a161044e6c9bd3964169fe9e78b8c3732034ddDECISION: move the task board from Vikunja to beads+Dolt (full swap vs hybrid mirror)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.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. The Storage interface covers the Board port almost 1:1: SearchIssues/GetReadyWork -> Snapshot; UpdateIssue(id, {status}) -> MoveToBucket; AddIssueComment/GetIssueComments (typed, ordered) -> Comment/Comments; CreateIssue -> CreateTask; AddLabel+RemoveLabel -> SwapLabel. Three risks in the description are downgraded by this API: - 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. - Change notification: GetAllEventsSince(ctx, since time.Time) is a typed poll cursor; no hand-rolled SQL over the events table needed. - 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. STILL 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. UNCHANGED: the human write path is still the real blocker, and the recommendation stands — do not swap while the viewer is read-only.open3decisionNULLNULL2026-07-20T00:26:37ZEugene Blikhbigbes@gmail.com2026-07-20T07:48:03ZNULLNULL0NULLNULLNULL000�{}0NULLNULLNULL0NULL0
ah-wd4.13903707241cf6bc246639a717d12ff22a014c91fc14f4d4363549230335222c4SPIKE (gating): does the beads Storage API enumerate ALL issues, or silently paginate?GATING UNKNOWN for any beads-backed Board adapter — settle this before writing adapter code, and before ah-wd4 can be decided on technical grounds. PROBLEM: 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. WHY 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). WHAT TO DETERMINE: 1. 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)? 2. 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.) 3. 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)? 4. If no guarantee exists: can RunInTransaction wrap a count + list so the pair is consistent? HOW: 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 >200 to expose a default limit. Report the exact zero-value semantics and the recommended Snapshot implementation with its completeness guarantee. DELIVERABLE: a written answer to 1-4 plus the recommended Snapshot approach. Do NOT write adapter code.SOURCE ANALYSIS DONE (beads v1.1.2, the version installed locally; clone checked out at tag 20e493e5). Empirical confirmation is running separately. Q1 — 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 > 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 > 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 <= 0 (issueops/ready_work.go:165) rather than a loop that would return nothing. Q2 — 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. Q3 — 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 > 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. Q4 — 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. VERDICT 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 >600-issue scale test that proves no default page size hides at a larger corpus than this repo's 86 issues.in_progress2taskNULLNULL2026-07-20T07:48:24ZEugene Blikhbigbes@gmail.com2026-08-04T23:36:01ZNULLNULL0NULLNULLNULL000�{}0NULLNULLNULL02026-08-04T23:36:01Z0
ah-wka61844a89efb3d2f3b72967bbcc6f9e305d2ee6cc40146aae71081887bb5527a5runner: Status crash-inference race misclassifies a finished run (e2e flake)Found by the Stage 2 closing review (3-run e2e probe, 1 failure in 3): TestE2E (Stage 1) tasks 101/102 finalized as outcome=crashed / exit_code=-1 even though the daemon log shows the correct run-exit report received (exit_code 0/1). Root cause per static read: PiZellij.Status (internal/runner/runner.go, crash-inference fallback around lines 210-215) infers Completed/OutcomeCrashed whenever it observes neither exit.json for the attempt nor a live zellij session — a visibility window between ahub-run writing exit.json (atomic rename) and the session-liveness check observing the dying session. The e2e stub session dies immediately at child exit, making the window wide; real zellij keep-pane narrows but does not provably close it (kill-session paths, crashes). Pre-existing Stage 1 behavior, NOT introduced by the Stage 2 commits (all six Stage 2 flows were green in all three runs). Direction to evaluate: make the crash inference sticky-read-ordered — check exit.json AGAIN after observing the session dead (dead session THEN a fresh exit.json stat), or require N consecutive dead observations before inferring a crash, or have Status treat session-dead-without-exit.json as the neither-state (no evidence) for one grace tick instead of hard Completed/crashed. Any fix must keep the SPEC section 9 three-state contract and the section 10 adopt semantics intact.The race window is closed or bounded (re-check ordering or grace tick); a regression test reproduces the old misclassification deterministically (stub with delayed exit.json visibility); 10 consecutive full e2e runs greenclosed2bugEugene BlikhNULL2026-07-13T08:37:59ZEugene Blikhbigbes@gmail.com2026-07-13T09:26:45Z2026-07-13T09:26:45ZNULL0NULLNULLNULL000�{}2783d16 + 56f84bf (SPEC section 9 sync): crash inference now confirm-after-grace (re-stat + 5s window returning the neither state). Regression test falsified against old logic; 15/15 independent e2e runs green vs ~1-in-3 pre-fix; -race clean; reviewer merge-ready with the adopt corner independently confirmed unreachable0NULLNULLNULL02026-07-13T08:46:20Z0
ah-xuc19aa9e472e32b714a672302d4a5b9431d5154fa20c3d0f98ef188f68d40ea1b7Stage 2: webhooks, Task Spec, full buckets, Telegram, srht push, watchdogPer SPEC SS14.2: Vikunja webhook receiver (HMAC) as reconciler poke; Task Spec YAML frontmatter (role/model/skills/limits) + label type defaults + validation -> Triage bounce; buckets Triage/Blocked/Question; Telegram notifier (honor HTTPS_PROXY; TG blocked from lab network); push agent/task-N branch to git.srht.bigb.es for review + link in In Review comment; watchdog on stale events.jsonl; pi --skill materialization from /srv/skills.Stage 2 design decisions (coordinator, 2026-07-13): 1. Webhook: POST /webhooks/vikunja on the SAME loopback mux; exposure to Vikunja is infra (ah-k23, tunnel/reverse-proxy). Hex HMAC-SHA256 of the raw body in X-Vikunja-Signature, constant-time compare; secret vikunja.webhook_secret; unset secret = route not registered. Any verified event = Poke(); payload untrusted beyond event_name logging. Poll loop remains the source of truth (Vikunja delivers webhooks once, no retries). 2. Buckets: triage/blocked/question are PARKED buckets — mapped and title-validated, never claim sources, never kill triggers, detached runtime supervision only. Triage is the bounce target for spec validation failures (not Failed — the human fixes and re-drags). Blocked/Question earn real semantics in Stage 4. domain.CanonicalBuckets() = States() + the three; buckets are a superset of states; config requires all nine keys. 3. Task Spec: YAML frontmatter (--- delimited) at the head of the description. Fields: role, model, skills, timeout. Merge precedence: config defaults < type:<name> label preset (config task_types) < frontmatter. Multiple type:* labels or an unknown type = validation error. Unknown frontmatter fields = warnings appended to the claim comment, never a bounce. Prompt renders over the frontmatter-stripped body. 4. Skills: names are safe slugs; resolved to <skills_dir>/<name>; SKILL.md must exist with a non-empty frontmatter description (pi refuses skills without one). pi argv keeps --no-skills --no-extensions and adds explicit --skill <abs> per skill — deterministic set. 5. Per-run timeout: effective value (spec/type override or config default) persisted on the run row (migration v2, runs.timeout_seconds, 0 = config default) so restarts enforce the right deadline. 6. Watchdog: stalled = Running and now - max(events.jsonl mtime, StartedAt) > stall_timeout (default 10m, 0 disables, else >= 1m). Kill + finalize OutcomeStalled/exit -1. Neither-state observation stays deadline-only. 7. Telegram: optional config block {token, chat_id, api_base}; default-transport proxy semantics honor HTTPS_PROXY; notify is best-effort after move+comment on finalize (success/failed/timeout/stalled) and spec bounce; token redacted from errors/logs. Unconfigured = existing slog no-op. 8. Publish: Runner.Publish(ctx, taskID) -> PublishInfo{RemoteURL, Branch, WebURL}; plain git push of agent/task-N to per-repo review_remote (never force); zero info + nil err = not configured; failure degrades to a push-failed line in the In Review comment; re-push on refinalize is idempotent. review_url template with {branch} builds the human link. 9. vikunja.web_url (default: url minus /api/v1) builds human task links for comments and notifications. 10. Scope cuts: NO worker pool / max_parallel in Stage 2 (research mentions it; epic and SPEC roadmap do not). No repo selection via spec (multi-repo is Stage 5). No new external deps. Infra prerequisites tracked in ah-k23: three new board columns (Triage/Blocked/Question), Vikunja webhook target+secret, srht repos + SSH key for the daemon user, TG bot token + proxy path.closed3epicNULLNULL2026-07-12T23:36:28ZEugene Blikhbigbes@gmail.com2026-07-13T08:38:18Z2026-07-13T08:38:18ZNULL0NULLNULLNULL000�{}Stage 2 delivered: 21 commits b3ee57d..HEAD (webhook HMAC poke, Task Spec frontmatter + type-label presets + Triage bounce, parked buckets Triage/Blocked/Question, per-run timeout with store v2, stall watchdog, Telegram notifier via HTTPS_PROXY, review-branch publish to srht remotes with bounded push, e2e flows, SPEC synced to Stages 1-2). Every wave gated by hostile review + empirical validation; final epic-wide gate 11/11. Runtime prerequisites (board columns, webhook target, srht repos, TG bot) remain in ah-k23.0NULLNULLNULL0NULL0
ah-xuc.1a152c1dd582b5477ff993f9f0c070eea86bf5d7994ae57afd24f3cbf8ac4394fStage 2 foundation: domain buckets/outcome, ports contracts, config surfaceEvery Stage 2 feature extends the shared contracts; land them first so later waves stay disjoint (SPEC section 13 isolation rule). No new external deps (crypto/hmac is stdlib, yaml.v3 already present) — go.mod stays untouched for the whole epic. domain: OutcomeStalled ("stalled"); bucket-name constants for triage/blocked/question plus CanonicalBuckets() (States() plus the three — canonical buckets are a superset of states from now on); Run.Timeout time.Duration (0 = use config default). ports: BoardTask.Labels []string (label titles); StartSpec.Skills []string (absolute skill dirs); Runner gains Publish(ctx, taskID) (PublishInfo, error) with PublishInfo{RemoteURL, Branch, WebURL} — zero-value info with nil error means publish not configured (skipped); error only on a real push failure. Document contracts in comments in the existing style. config: vikunja.webhook_secret (optional); vikunja.web_url (optional, default = url with trailing /api/v1 stripped) for human task links; buckets now require all nine canonical keys (validate over domain.CanonicalBuckets()); skills_dir (optional path, tilde-expanded); task_types map[label-name]{role, model, skills, timeout — all optional} with validation (role exists, skill names are safe slugs — reuse validSlug, timeout parses positive, any skills referenced require skills_dir set); telegram optional block {token, chat_id, api_base default https://api.telegram.org} — token and chat_id required when the block is present; stall_timeout duration (default 10m, explicit "0" disables, otherwise at least 1m); repos[*].review_remote (optional git URL) and repos[*].review_url (optional template, must contain {branch} when set). Update config.example.yaml with commented examples of every new key.go test ./internal/domain/... ./internal/config/... green; go vet clean on touched packages; table tests for nine-bucket validation, task_types, telegram block, stall_timeout, review_url template; config.example.yaml stays parseable; commits follow the area convention; only own packages stagedCoordinator refinements: (a) do NOT add a method to the ports.Runner interface — define a SEPARATE port Publisher { Publish(ctx, taskID) (PublishInfo, error) } plus the PublishInfo struct; the runner implements it in ah-xuc.5; this keeps every package (cmd, reconcile fakes) compiling between waves. (b) The nine-bucket requirement invalidates config fixtures outside internal/config: update cmd/agenthubd test fixtures AND the e2e harness fixtures (harness config + fake vikunja board columns Triage/Blocked/Question) in THIS bead — wave 1 runs solo so touching them is safe; goal: go test ./... and go test -tags e2e ./e2e/... stay green at every wave boundary. (c) vikunja.web_url default: vikunja.url with a trailing /api/v1 stripped. (d) stall_timeout: absent = 10m default; explicit "0" = disabled; the current parseDuration rejects nonpositive values, so handle the explicit zero separately. (e) review_url set without review_remote is a config error; review_remote alone is fine.closed2taskEugene BlikhNULL2026-07-13T05:14:51ZEugene Blikhbigbes@gmail.com2026-07-13T05:44:24Z2026-07-13T05:44:24ZNULL0NULLNULLNULL000�{}4 commits landed (3c3e558..7b158c8); hostile review merge-ready with zero findings; empirical validation 8/9 (only pre-existing gofmt debt, pinned to ah-xuc.8)0NULLNULLNULL02026-07-13T05:19:13Z0
ah-xuc.1093e494bccda4f59baacc19a0e3b326af7c363fd21532a445b37b51577c6c63e9cmd/agenthubd: wire webhook secret and Telegram notifierWiring only. agenthubd run(): construct internal/telegram when the config block is present, otherwise keep the existing slog no-op notifier; pass the notifier into reconcile.Deps; pass the webhook secret and a hook invoking Reconciler.Poke into httpapi.Deps. internal/deps updated if that is the wiring point. Startup board preflight: with nine required buckets the existing adapter title lookup already fails fatal with the found-titles list when the Triage/Blocked/Question columns are missing on the board — verify that failure stays readable at startup. ahub needs no changes (validate-config picks up the new keys through the config package).go build ./cmd/... green; go test ./cmd/... green including a run() smoke asserting the webhook route responds when the secret is set and is absent when unset, and that the telegram notifier is selected when configured; go vet cleanCoordinator refinement: also wire the runner as ports.Publisher into reconcile.Deps.closed2taskEugene BlikhNULL2026-07-13T05:16:18ZEugene Blikhbigbes@gmail.com2026-07-13T07:53:56Z2026-07-13T07:53:56ZNULL0NULLNULLNULL000�{}95560cc; reviewer merge-ready (single wiring path, nil-safe, pointer-identity tested); validator all-pass incl. first live daemon runs — readable fatal preflight, telegram deferred past preflight with no I/O0NULLNULLNULL02026-07-13T07:35:21Z0
ah-xuc.11b6a041aecb730cbf5ad794b8bba2833ccb247eeb242441fa1b256c4a6130e930e2e: Stage 2 flows — webhook poke, Triage bounce, skills, publish, watchdog, TelegramExtend the e2e harness (build tag e2e, fake vikunja plus stub pi/zellij, stub-honesty rules from commit b3ee57d). Fake vikunja gains labels on tasks and the nine-bucket board. - webhook poke: long poll_interval, POST a correctly signed payload to /webhooks/vikunja, assert the claim happens promptly (poke, not poll); a bad signature does nothing. - Triage bounce: a Ready task with broken frontmatter lands in Triage with the diagnostic comment and no record or run row. - skills: a task whose spec names skills (fixture SKILL.md dirs under a temp skills_dir) — the stub pi records argv; assert the --skill absolute paths and the retained --no-skills. - publish: repo review_remote points at a local bare repository; after a successful run the In Review comment carries the review link and the bare repo has the agent/task-N ref at the expected commit. - watchdog: stub pi hangs without touching events.jsonl; with a short stall_timeout the run is killed and the card lands in Failed with a stalled comment. - telegram: telegram.api_base points at a fake httptest Bot API; assert sendMessage calls for the in_review and stalled paths, and that the token appears only in the request path, never in daemon logs.go test -tags e2e ./e2e/... green and hermetic (loopback only); each flow asserted through externally observable surfaces — board moves, comments, bare repo refs, fake TG requests — not daemon internalsclosed2taskEugene BlikhNULL2026-07-13T05:16:24ZEugene Blikhbigbes@gmail.com2026-07-13T08:38:17Z2026-07-13T08:38:17ZNULL0NULLNULLNULL000�{}8e29554; reviewer merge-ready with zero findings — stub honesty, hermeticity, timing discipline, full-argv equality all verified; 3-run flake probe: all six Stage 2 flows green in all runs (the one flake found is pre-existing Stage 1, filed as ah-wka)0NULLNULLNULL02026-07-13T07:53:57Z0
ah-xuc.1214d3638d45c9064a76941832983973f494233fccabe6a738101abdf7f06e6d89docs: sync SPEC with Stage 2 behaviorSPEC.md gains normative Stage 2 sections mirroring what landed, in the established style (invariants and failure modes, not narrative): domain deltas (stalled outcome, canonical buckets as a superset of states, Run.Timeout), ports deltas (Labels, Skills, Publish), store v2 migration, vikunja labels, runner skills plus publish (never force-push), reconciler claim spec resolution + Triage bounce + parked buckets + per-run timeout + watchdog + notification ordering, httpapi webhook endpoint (HMAC, disabled when unset), config reference with the full new example, security notes (webhook secret, TG token redaction, srht push scope), roadmap section 14 marks Stage 2 delivered. Cross-check config.example.yaml for drift. AGENTS.md and CLAUDE.md only if conventions changed (mirror both if so — independent files).Each new SPEC section spot-checked against the shipped code; no contradiction with Stage 1 sections; the research-v3 supersede note stays accurateclosed2taskEugene BlikhNULL2026-07-13T05:16:35ZEugene Blikhbigbes@gmail.com2026-07-13T08:38:17Z2026-07-13T08:38:17ZNULL0NULLNULLNULL000�{}e13b65c + b48849f (two wording nits); accuracy reviewer merge-ready — every behavioral claim verified against code; SPEC section-12 example validates through ahub validate-config; pre-existing RunStatus.MetaAttempt drift filed as ah-6u00NULLNULLNULL02026-07-13T07:53:57Z0
ah-xuc.2fa1ad68d45c1cb503c73e2f25b2c07769c319f1d2adc6eefbb30f03d70297a4ainternal/spec: Task Spec frontmatter parse, type-label defaults, validationNew pure package internal/spec (imports domain, config, yaml only — no board/store/runner I/O). Custom fields do not exist in Vikunja, so the machine-readable part of a task lives as YAML frontmatter at the head of the description; labels give per-type defaults. Parse: frontmatter delimited by --- lines at the very start of the description (tolerate CRLF; no frontmatter = empty spec). Known fields: role, model, skills (list of names), timeout (duration string). Unknown fields are collected as warnings, never errors. Returns spec + body (description with frontmatter stripped) + warnings. Resolve(cfg, labels, description): merge precedence config defaults < type:<name> label preset (cfg.TaskTypes) < frontmatter. More than one type:* label is a validation error; a type:* label naming an unknown task type is a validation error. Validation (bounce-class) errors: broken YAML, unknown role, skill name not a safe slug, skills requested while skills_dir unset, missing <skills_dir>/<name>/SKILL.md, SKILL.md frontmatter with an empty description, timeout unparseable or nonpositive. Output: Resolved{RoleName, Model, SkillPaths (absolute), Timeout, Body, Warnings}. Filesystem checks go through a small injected func so most tests need no real skills tree; add one real-FS test with t.TempDir() fixtures. Validation errors must be a typed, human-readable list — the reconciler posts them verbatim in the Triage bounce comment — distinct from internal errors.go test ./internal/spec/... green, go vet clean; table tests cover merge precedence for every field and each bounce class; frontmatter stripping keeps the body exact after the closing delimiterclosed2featureEugene BlikhNULL2026-07-13T05:15:08ZEugene Blikhbigbes@gmail.com2026-07-13T06:22:47Z2026-07-13T06:22:47ZNULL0NULLNULLNULL000�{}d714741 + eb5a4be (review-gap pinning); hostile adversarial review merge-ready (yaml alias bombs bounded, path traversal blocked, error taxonomy sound)0NULLNULLNULL02026-07-13T05:44:24Z0
ah-xuc.3130d6553f11713918d8f32dca70661607918c8ae7bed880ea98a1b417dca5b30vikunja: fetch task labels into BoardTask.LabelsStage 2 type defaults key off Vikunja labels and the adapter currently drops them. wireTask gains the labels array (verify the exact field shape against the live docs.json or the go-vikunja v2.3.0 source, as was done for result-count semantics in commit 7802095). Snapshot copies label titles into ports.BoardTask.Labels ([]string, empty-safe, order as returned). Extend the httptest fixtures with tasks carrying zero, one, and several labels, including one with a type: prefix. No behavior change for existing methods.go test ./internal/vikunja/... green, go vet clean; fixtures cover labeled and label-less tasksclosed2taskEugene BlikhNULL2026-07-13T05:15:10ZEugene Blikhbigbes@gmail.com2026-07-13T06:22:48Z2026-07-13T06:22:48ZNULL0NULLNULLNULL000�{}1e9fc7e; wire shape pinned against upstream go-vikunja v2.3.0; review clean0NULLNULLNULL02026-07-13T05:44:24Z0
ah-xuc.433d26ffac04170d2d8e34c39c29e316a661775ceecd8aa8f20c5d11929a7589dstore: migration v2 — per-run timeout columnThe effective timeout can differ per run once Task Spec overrides land, and a daemon restart must keep enforcing the right deadline, so it is persisted on the run row. Schema user_version 2: runs gains timeout_seconds INTEGER NOT NULL DEFAULT 0 (0 = config default at enforcement time). Fresh databases create straight at v2; existing v1 files get ALTER TABLE on open. Follow the existing hand-rolled migrator (PRAGMA user_version gate, BEGIN IMMEDIATE serialization, first-boot busy retry — keep every one of those properties). CreateRun/UpdateRun/LatestRun round-trip domain.Run.Timeout, stored as integer seconds.go test ./internal/store/... green, go vet clean; tests: fresh create lands at user_version 2; a v1 database upgrades in place preserving rows; timeout round-trips; existing first-boot race tests keep passingclosed2taskEugene BlikhNULL2026-07-13T05:15:22ZEugene Blikhbigbes@gmail.com2026-07-13T06:22:49Z2026-07-13T06:22:49ZNULL0NULLNULLNULL000�{}c04f7d7; step-list migrator preserves all race properties (verified under -race); review clean0NULLNULLNULL02026-07-13T05:44:25Z0
ah-xuc.5b5fbb872b97c09d1baaee1d165892ef0ed8473656472b639900c43d5dbb8e0afrunner: explicit --skill arguments and Publish to the review remoteTwo runner extensions, both behind existing seams. All argv construction stays in commands.go per SPEC section 9. Skills: piArgv gains the resolved skill dirs — one --skill <absolute-path> per entry, appended while keeping --no-skills and --no-extensions (pi 0.70.2 loads explicit --skill paths even under --no-skills, giving a deterministic per-run set). Start threads StartSpec.Skills through. Publish(ctx, taskID): resolve the task worktree, branch, and repo the same way Summary does today. When the repo has no review_remote, return a zero PublishInfo and nil error (publish not configured). Otherwise git -C <worktree> push <review_remote> <branch> with the branch pushed to the same name — plain push, never force (SPEC: agent branches are never force-pushed). On success fill PublishInfo{RemoteURL, Branch, WebURL} where WebURL is the repo review_url with {branch} substituted (empty when review_url unset). Push failures return a wrapped error carrying a stderr prefix.go test ./internal/runner/... green, go vet clean; argv table tests for skills present/absent and push; PATH-shim git stub verifies push argv, success, and failure propagation; a Start test asserts --skill flags reach the pi argvCoordinator refinement: implement ports.Publisher (separate port defined in ah-xuc.1) as a method on the existing runner type — the ports.Runner interface itself does not change.closed2featureEugene BlikhNULL2026-07-13T05:15:26ZEugene Blikhbigbes@gmail.com2026-07-13T06:22:49Z2026-07-13T06:22:49ZNULL0NULLNULLNULL000�{}28f75aa + eae0845; refspec refs/heads/X:refs/heads/X pinned, no-force verified, findWorktree semantics shared with Summary; review clean0NULLNULLNULL02026-07-13T05:44:25Z0
ah-xuc.6e8c8ca06f34dfebba7004095b3bec4be6a1dd7a6c04bce0af09d4edc831fed76telegram: ports.Notifier implementation (Bot API through HTTPS_PROXY)New package internal/telegram implementing ports.Notifier. POST {api_base}/bot{token}/sendMessage with JSON {chat_id, text, disable_web_page_preview: true}. http.Client with a 10s timeout whose transport keeps ProxyFromEnvironment semantics so HTTPS_PROXY is honored (Telegram is blocked from the lab network; egress goes through the proxy) — do not build a bare Transport without the Proxy field. Non-2xx responses and ok:false bodies become errors carrying a short body prefix; the bot token must never appear in logs or error strings — redact the URL when wrapping errors. No retries: the reconciler treats Notify as best-effort. api_base comes from config telegram.api_base so tests and e2e can point it at httptest.go test ./internal/telegram/... green, go vet clean; httptest covers success, HTTP error, ok:false, and token redaction in returned errors; a test asserts proxy resolution from the environment is activeclosed2featureEugene BlikhNULL2026-07-13T05:15:38ZEugene Blikhbigbes@gmail.com2026-07-13T06:22:50Z2026-07-13T06:22:50ZNULL0NULLNULLNULL000�{}e15fe4c; token-leak vectors traced to stdlib source and closed; proxy semantics verified empirically; review clean0NULLNULLNULL02026-07-13T05:44:25Z0
ah-xuc.762a6806491ee4096a8206ec5b958064c69d772d7f8376188ca61bbfc8a354510httpapi: Vikunja webhook receiver with HMAC verificationPOST /webhooks/vikunja as a reconciler poke. Design principle 1: a webhook only triggers an immediate iteration; polling stays the source of truth because Vikunja delivers webhooks once, without retries. Deps gain WebhookSecret string and a Webhook hook (non-blocking; cmd wires it to Reconciler.Poke). Empty secret = feature disabled: the route is not registered at all. Verification: X-Vikunja-Signature carries hex HMAC-SHA256 over the raw request body; compute over the exact bytes read and compare with hmac.Equal; missing or wrong signature = 401 with a terse body that echoes nothing back; cap the body at 256 KiB. After verification decode {event_name} best-effort for the log line only — the payload is otherwise untrusted and unused. Respond 200 fast, call the hook once per verified delivery. Exposing the loopback listener to Vikunja is infra (ah-k23), not this bead.go test ./internal/httpapi/... green, go vet clean; tests: a valid computed signature pokes the hook and returns 200; tampered body, wrong secret, and missing header return 401 without invoking the hook; oversized body rejected; unset secret leaves the route absent and existing routes unaffectedclosed2featureEugene BlikhNULL2026-07-13T05:15:43ZEugene Blikhbigbes@gmail.com2026-07-13T06:22:50Z2026-07-13T06:22:50ZNULL0NULLNULLNULL000�{}61400ae; HMAC discipline verified (full-read-before-verify, uniform 401, constant-time compare); review clean0NULLNULLNULL02026-07-13T05:44:26Z0
ah-xuc.8d48513a80bea7b4ebcfc74514ea6d2a7079500b44f1b1eebca7b445db6e316b7reconcile: spec-driven claim, Triage bounce, parked buckets, per-run timeoutClaim-path integration of Stage 2. Same-package constraint: this bead owns the internal/reconcile edits for the claim path; the finalize-path bead must not start until this one is committed. iterate(): canonical buckets triage/blocked/question become parked buckets — superviseRuntime only (detached supervision persists runtime truth; no claim, no kill, no card moves, no comments). They are never claim sources and never terminal targets. Blocked and Question get real semantics in Stage 4; Stage 2 only reserves and parks them. claim(): resolve the Task Spec via internal/spec from BoardTask.Labels plus Description. A validation failure bounces: MoveToBucket(triage) first, then one diagnostic comment listing the problems verbatim plus a hint to fix the spec and drag back to Ready, then event spec_rejected — move-then-comment discipline so a persistent move failure cannot spam comments; appendEvent dedup applies. No record is persisted and the card never goes to Failed for spec problems. Warnings (unknown fields) never block: append them to the claim comment. Success path: the spec role name resolves through cfg.Roles; model override applies; the prompt renders over the frontmatter-stripped Body; resolved SkillPaths go into StartSpec.Skills; the effective timeout (spec override or config default) goes into StartSpec.Timeout and is persisted as Run.Timeout. check(): deadline enforcement uses run.Timeout when positive, else cfg.Timeout — in both the running arm and the degraded neither-state arm. Extend fakes_test.go (fake runner captures Skills and Timeout; board tasks carry Labels) and cover: each bounce class end to end, merge precedence reaching StartSpec, parked buckets neither claim nor kill while detached supervision still finalizes a finished runtime, per-run timeout enforced at the right boundary with the fake clock — both shorter and longer than the config default.go test ./internal/reconcile/... green, go vet clean; bounce tests assert move-before-comment ordering and zero Failed transitions; parked-bucket tests assert no board writes; timeout tests cover run.Timeout smaller and larger than the config default Additional: pre-existing gofmt drift in internal/reconcile/reconcile_test.go (comment alignment around lines 983-984 and 1345, present since before Stage 2) — run gofmt -w on the files you touch and fold the fix into this bead's commit.closed2featureEugene BlikhNULL2026-07-13T05:16:00ZEugene Blikhbigbes@gmail.com2026-07-13T06:57:17Z2026-07-13T06:57:17ZNULL0NULLNULLNULL000�{}9823250 + 06_pin commit; review merge-ready (all SPEC section-10 invariants traced clean); validator 10/10 incl. e2e byte-identical for spec-less tasks; adopt-over-bounce coverage gap closed0NULLNULLNULL02026-07-13T06:23:38Z0
ah-xuc.95c6568a5b55170ea70ba05fb8c0af96dcfaf33b977424934bdb124f8c46e5c10reconcile: publish review branch, watchdog on stale events, Telegram notificationsFinalize-path integration of Stage 2. Starts only after the claim-path bead is committed (same package). Publish: in finalize, on the success path with moveCard set, call runner.Publish before posting the In Review comment. Zero PublishInfo = not configured, plain comment. Success adds a review link line (WebURL, falling back to RemoteURL plus branch) and appends event published. Failure degrades: the comment still posts with a push-failed line carrying the error, event publish_failed (deduped). A refinalize repeats the push — pushing an up-to-date branch is idempotent and fine. Watchdog: in the check() running arm, when cfg.StallTimeout > 0 and now minus max(st.LastEvent, run.StartedAt) exceeds StallTimeout, the run is stalled: kill and finalize with OutcomeStalled, exit -1 (share the timeoutKill shape). The neither-state arm stays deadline-only — no events are observable there. failureComment renders the stalled outcome distinctly (agent went silent, not merely slow). Notifications: Notify best-effort — log on error, never gate the flow, guard the nil notifier — after a successful move plus comment: finalize with moveCard (success and failure including timeout and stalled) and the spec bounce. Text: outcome emoji, task id and title, outcome word, cost when known, and the human task link built from vikunja web_url. Tests with fakes: the In Review comment contains the review link; publish failure still posts the comment; the stall kill fires just past the boundary and not before; a run with events flowing but past run.Timeout still dies by deadline; notify recorded after move and comment in that order; nil notifier safe; a notify failure does not fail finalize.go test ./internal/reconcile/... green, go vet clean; ordering asserted move then comment then notify; stalled and timeout outcomes distinguishable in comments, events, and notificationsCoordinator refinement: publishing arrives via a new optional reconcile Deps field of type ports.Publisher (nil-guarded like Notifier); finalize publishes only when the field is non-nil. cmd wires the runner into it in ah-xuc.10.closed2featureEugene BlikhNULL2026-07-13T05:16:07ZEugene Blikhbigbes@gmail.com2026-07-13T07:35:20Z2026-07-13T07:35:20ZNULL0NULLNULLNULL000�{}d5d1efc + 64a7216 (review fix: bounded publish ctx via publish_timeout knob, zero-delta recorder covers publish/notify); reviewer merge-ready after fix, validator 10/10 with all 24 subtests by name0NULLNULLNULL02026-07-13T06:57:43Z0
ah-ydx8735805091c14831debfbdb26c8f324f6832e8b80ebcc313e05201a7bd3c85f8Stage 3: mem0 memory lifecycle + Context PackPer SPEC SS14.3: mem0 REST client (X-API-Key, scoping user_id=proj:<slug>/global, run_id=task:<id> — agent_id filter is buggy upstream); load cascade on claim -> .task/CONTEXT.md section; save on Done; post-run summarizer role; Context Pack builder (task+thread+parent-chain+memory+git, ~40k char budget, deterministic order, truncate-from-tail).Audit note (2026-07-20): the SPEC 14.3 'post-run summarizer role' was never implemented as a role — the shipped design substitutes the agent-authored .task/summary.md + mem0 infer=true LLM extraction at Done-time (memory.go). All four ah-ydx children are closed. DECISION NEEDED: if the substitution is accepted, update the epic body + SPEC 14.3 wording and close this epic; otherwise the summarizer role is the one remaining piece.open3epicNULLNULL2026-07-12T23:36:28ZEugene Blikhbigbes@gmail.com2026-07-19T23:38:22ZNULLNULL0NULLNULLNULL000�{}0NULLNULLNULL0NULL0
ah-ydx.1942024e29b162a28d01e0bddbd3bf862f1ff8bf498c45bd03199c5a0dc916489internal/mem0: REST client for live mem0.bigb.esTyped Go client for the self-hosted Mem0 server (LIVE at mem0.bigb.es, phoebe-lab/mem0 stack): X-API-Key auth, add/search/get-all/delete memories. Encode scoping conventions as helpers: user_id='proj:<slug>' | 'global', run_id='task:<id>'; do NOT rely on agent_id filtering (buggy upstream per SPEC §14.3). Stdlib-only, httptest unit tests; live verification (incl. API-key provisioning via dashboard/register) is a follow-up ops step.closed2featureEugene BlikhNULL2026-07-18T13:23:56ZEugene Blikhbigbes@gmail.com2026-07-18T13:41:11Z2026-07-18T13:41:11ZNULL0NULLNULLNULL000�{}internal/mem0 client merged to master (55b9b43); routes confirmed against mem0 v2.0.11 source; live verification against mem0.bigb.es tracked in ah-ydx.30NULLNULLNULL02026-07-18T13:25:07Z0
ah-ydx.2f548f321fdf44dbfaaa3c420a175ff8b3bea76ab1cf76d097509d87eeb461072internal/ctxpack: Context Pack builderDeterministic prompt-context assembler per SPEC §14.3: ordered sections (task, comment thread, parent chain, memory, git log/diffstat) under a ~40k char budget, per-section truncate-from-tail with stable ordering so identical inputs render identical packs. Pure package + table tests first; wiring into runner PROMPT.md rendering is a separate integration step after wave 1 merges.closed2featureEugene BlikhNULL2026-07-18T13:23:56ZEugene Blikhbigbes@gmail.com2026-07-18T13:41:11Z2026-07-18T13:41:11ZNULL0NULLNULLNULL000�{}internal/ctxpack merged to master (342410e); format contract pinned by golden tests; runner integration tracked in ah-ydx.30NULLNULLNULL02026-07-18T13:25:08Z0
ah-ydx.316681b27f4960105b6ca27beabc1e8219b7b6edab3c59c243830102a1c17bd7aMemory lifecycle: load-on-claim CONTEXT section, save-on-DoneWire mem0 into the task loop per SPEC §14.3: on claim, search mem0 (proj scope + global) and render a memory section into the prompt context via ctxpack; on Done/Cancelled terminal scan (shares the ah-07g hook), save run summary/verdict facts back to mem0 (run_id='task:<id>'). Depends on the mem0 client, ctxpack, and the ah-07g terminal-state scan.MEM0 SERVER HEALTHY (2026-07-18, after user-authorized override delete + restart): /configure now reflects env-of-truth (deepseek/deepseek-v4-flash + nvidia/nemotron-embed-1b-v2). FULL live smoke green: ADD infer=false 200/event=ADD; SEARCH 200 with score (embeddings ok); ADD infer=true 200 (LLM extraction ok); LIST/PURGE/EMPTY clean; bogus key 401. Server-side contract fully verified — lifecycle implementation unblocked once ah-07g lands the terminal-state hook. OPERATIONAL RULE: mem0 config is ENV-ONLY; never POST /configure (GET redacts secrets, round-trip corrupts stored creds — happened + fixed today, backup at phoebe:/root/mem0-settings-backup.sql); if a stored override reappears (dashboard onboarding creates one), delete settings row key=config_overrides + restart.closed2featureEugene BlikhNULL2026-07-18T13:23:57ZEugene Blikhbigbes@gmail.com2026-07-18T16:22:33Z2026-07-18T16:22:33ZNULL0NULLNULLNULL000�{}Merged b761cbe: recall-on-claim (## Relevant memories via ctxpack, proj+global scopes, 15s budget, feature-off byte-identical) + save-on-Done (save→archive→prune ordering, infer=true, once-only via worktree signal, Cancelled excluded). Rollout: install MEM0_API_KEY in /etc/agent-hub/env, live-smoke recall block + one memory_saved event.0NULLNULLNULL02026-07-18T15:59:39Z0
ah-ydx.4082c3365aabe053240cf35195442ee1ca6547ba635899ad10f3319fea286fd7eFull Context Pack: thread comments, parent chain, git log sections in promptah-ydx.2's ctxpack renders only the memory section today. SPEC §14.3's full pack adds: board comment thread, parent-chain summaries (delegation), git log/diffstat of the branch — assembled with the memory section under the ~40k budget in canonical order. Backlog until the current prompt shape shows its limits in live runs.closed3featureEugene BlikhNULL2026-07-18T16:22:34ZEugene Blikhbigbes@gmail.com2026-07-18T18:47:54Z2026-07-18T18:47:54ZNULL0NULLNULLNULL000�{}Full Context Pack live: thread + parent-chain + memory + git sections through one 40k-budget ctxpack build on the claim path; context_pack audit event; per-section graceful degradation.0NULLNULLNULL02026-07-18T18:19:54Z0
ah-ziqf6ed15a8569893a586511b86daf9a0cfc59c0fd794875182892371c1c8323d80yonote: comments.resolve 400s — live API requires isResolved boolean in bodyLive re-smoke of the Q&A loop (ah-eje, task 5) proved comments.resolve is broken: the daemon's POST /api/comments.resolve got HTTP 400 invalid_type 'Invalid input: expected boolean, received undefined (isResolved)'. Yonote 1.47.1 requires an isResolved:true field in the resolve payload; our client sends only {id}. Deterministic — every resolve fails, answer delivery is unaffected (non-fatal WARN path worked as designed). Fix: add IsResolved to the resolve wire payload in internal/yonote/comments.go, adjust tests, re-verify live that a fresh qa question ends isResolved:true.closed2bugEugene BlikhNULL2026-07-19T18:11:32ZEugene Blikhbigbes@gmail.com2026-07-19T18:21:38Z2026-07-19T18:21:38ZNULL0NULLNULLNULL000�{}Fixed in 2720611: resolve payload now {id, isResolved:true}. Live-verified (task 6 / comment 732b89b2): resolve succeeded, no WARN, comment isResolved:true.0NULLNULLNULL02026-07-19T18:11:47Z0