vqcqd30a7rgukh7vrrunbag5gh7q4mij · 59 rows
| id | content_hash | title | description | design | acceptance_criteria | notes | status | priority | issue_type | assignee | estimated_minutes | created_at | created_by | owner | updated_at | closed_at | closed_by_session | external_ref | spec_id | compaction_level | compacted_at | compacted_at_commit | original_size | sender | ephemeral | wisp_type | pinned | is_template | mol_type | work_type | source_system | metadata | source_repo | close_reason | event_kind | actor | target | payload | await_type | await_id | timeout_ns | waiters | hook_bead | role_bead | agent_state | last_activity | role_type | rig | due_at | defer_until | no_history | started_at | is_blocked |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| ah-07g | bac07329270d597d113053b517e9dfaf960a2c01112410a47ffc02a9fde7bcf3 | Investigate unbounded .task/events.jsonl (202MB for one design run) | task-2/.task/events.jsonl was 202 MB for a single ~2min run producing a 100-line doc. pi event stream persisted verbatim, no truncation/rotation, under work_root. work_root fills fast. Investigate pi verbosity, whether events.jsonl is needed post-run, add rotation/cap or prune-on-Done. | DECIDED 2026-07-18 (user): 'remove worktree, but backup .task and additional docs'. Policy: - When a card reaches Done or Cancelled and its worktree still exists: archive FIRST, then 'git worktree remove --force'. - Archive = tar.gz containing .task/ (PROMPT.md, summary.md, events.jsonl, *.json) PLUS any files in the worktree that are uncommitted relative to the branch head (untracked + modified) — i.e. everything that would be lost with the worktree. Branch itself is the durable artifact and is NOT touched. - Archive location: config archive_dir, default <work_root>/archive/, file task-<id>.tar.gz (overwrite on re-archive). - Failed/In Review/etc: untouched (zellij attach + debugging still want the worktree). - events.jsonl compresses ~10-20x (repeated JSON context), so backup satisfies keep-for-debug without the 202MB/run disk cost. | INVESTIGATION (2026-07-18): events.jsonl is pi's raw event stream, persisted verbatim by pi in <worktree>/.task/. 210MB for task-2's ~2min run; lines up to ~100KB each (full message/context per event). Worktrees are NOT pruned on terminal state (task-1 and task-2 both still on disk under work_root). Two levers: (a) pi-side — reduce event verbosity / don't persist full context per event (pi is external, needs a flag or upstream change); (b) daemon-side — prune the worktree or at least .task/events.jsonl when a task reaches Done (NOT In Review, where zellij-attach/review still want it). Retention policy is a user call: keep-for-debug vs disk. No code changed yet — needs a retention-policy decision. | in_progress | 3 | task | Eugene Blikh | NULL | 2026-07-18T06:30:22Z | Eugene Blikh | bigbes@gmail.com | 2026-07-18T13:21:59Z | NULL | NULL | 0 | NULL | NULL | NULL | 0 | 0 | 0 | �{} | 0 | NULL | NULL | NULL | 0 | 2026-07-18T13:21:59Z | 0 | ||||||||||||||||||||||
| ah-0ge | 2c909b097105e578055b7ee05ca088aa4e70e7f293e15e7aae83f28ef1a5aeda | Stage 4: agent tools API, question loop, AgentsView links | Per SPEC SS14.4: per-task bearer tokens; ahub tool task-create/ask-user/memory-search/memory-add over loopback HTTP; server-side policies (Triage-only, gen<=2, per-session caps, dedup, project allowlist); question.json -> Question bucket -> comment-answer -> pi --session resume as new attempt; parent auto-Block/unblock on subtask completion; AgentsView: pg push from runner host + /sessions/<id> deep links in comments. | STATUS 2026-07-17. The FILE-CHANNEL agent capabilities of Stage 4 are DONE and committed (9ec9418, 5ad253a): task-create (ah-0ge.1), parent block/unblock join + pi-session-style resume (ah-0ge.2/.3), ask-user question loop (ah-0ge.4). These use .task/*.json + board state instead of the per-task-bearer-token ahub-tool-over-HTTP API, which the file channel makes optional for the 'agent stops then resumes' pattern. REMAINING epic scope is infra/external-service gated and NOT autonomously completable by me: - memory search/add tools -> depend on Stage 3 mem0 (ah-ydx); mem0 REST client is unverifiable without a live mem0 instance and I'd be guessing at its API shape. - AgentsView /sessions/ deep links -> need 'pg push from the runner host' + a deployed AgentsView (ah-k23 infra); the deep-link URL alone is useless without the push. - per-task bearer tokens + ahub tool CLI-over-HTTP -> an alternative transport to the file channel; only needed for mid-run (not stop-and-resume) tools, and lower value now that the file channel covers task-create + ask-user. Recommend: keep this epic open for the HTTP-tools-API + AgentsView items, to be done alongside ah-k23 infra. | open | 3 | epic | NULL | NULL | 2026-07-12T23:36:29Z | Eugene Blikh | bigbes@gmail.com | 2026-07-17T20:14:03Z | NULL | NULL | 0 | NULL | NULL | NULL | 0 | 0 | 0 | �{} | 0 | NULL | NULL | NULL | 0 | NULL | 0 | |||||||||||||||||||||||
| ah-0ge.1 | fe7de8b8454eea7571194b1d573a0f6427b0f4c7b704400ef3ea627f37636484 | Agent-created tasks via .task/tasks.json file channel | First slice of the delegation loop: agent X declares the child roles it needs, X exits, the reconciler creates those tasks. Deliberately NOT the full ah-0ge tools API — no bearer tokens, no ahub tool CLI, no HTTP endpoints. X writes .task/tasks.json on the way out; the reconciler reads it at run end, the same way .task/question.json is already reserved (SPEC:392) in a directory internal/runner/taskfiles.go already owns. A file channel can only be read at run END, which would cripple an interactive ask-user loop but is EXACTLY the semantics wanted here ('agent X stops'). That constraint is what makes this slice small. Server-side policy from the ah-0ge epic still applies: gen<=2, per-session caps, dedup, project allowlist. A child that spawns children that spawn children fills the board. | Child tasks get a role (must exist in cfg.Roles) + a task body. Open: do children land in Ready directly (agent->agent, per the proposal) or in Triage for a human to release (per SPEC §14.4 as written)? Triage is the safer default and can be relaxed later via config. | A run whose agent writes .task/tasks.json with two child role tasks results in two new board tasks with those roles; a malformed or over-cap file bounces the parent rather than creating anything. | IMPLEMENTED 2026-07-17. Agent writes .task/tasks.json ({tasks:[{role,title,body}]}); a SUCCESSFUL run's finalize reads it and creates one board card per entry under its role (frontmatter 'role: X') in the configured target bucket. Decisions made during implementation: - Ready vs Triage: config knob agent_tasks.target_bucket, DEFAULT 'triage' (spec §14.4-aligned, safe: human vets agent-created work). Flip to 'ready' for the full auto agent→agent loop. Documented in config.example.yaml + SPEC §12. - 'Bounce' semantics: malformed/over-cap/unknown-role/empty-title creates NOTHING (atomic) and posts a rejection comment on the parent; the parent's OWN work still finalizes to In Review. Chose comment-not-card-move because moving the parent to Triage collides with the state model (triage is a parked bucket, not a task State, and the parent has a persisted record). Stronger card-move bounce deferred. - gen<=2 cap DEFERRED to ah-0ge.2: needs task lineage (parent link) which doesn't exist yet. Only the per-run COUNT cap (max_per_run, default 5) is enforced now. - Mid-creation transport failure keeps already-created children (can't un-create) and stops; logged. finalize isn't retried post-InReview. New surface: domain.RequestedTask; ports.Board.CreateTask + ports.NewTask; ports.Runner.RequestedTasks; runner.readTasksFile + PiZellij.RequestedTasks; vikunja.Client.CreateTask (PUT /projects/{id}/tasks then MoveToBucket); reconcile.spawnChildTasks/validateRequestedTasks/childDescription; config.AgentTasks. Tests: reconcile (spawn 2, unknown-role bounce, over-cap bounce, malformed bounce), runner (readTasksFile absent/valid/malformed), vikunja (CreateTask HTTP path), config (defaults + 2 validation failures). Full suite + e2e green under GOFLAGS=-mod=readonly. | closed | 3 | feature | Eugene Blikh | NULL | 2026-07-17T14:38:02Z | Eugene Blikh | bigbes@gmail.com | 2026-07-17T16:16:19Z | 2026-07-17T16:16:19Z | NULL | 0 | NULL | NULL | NULL | 0 | 0 | 0 | �{} | Implemented and tested: agent-created child tasks via .task/tasks.json file channel. Acceptance met (two children created; malformed/over-cap bounces atomically). Config knob defaults to Triage; gen-cap deferred to ah-0ge.2. | 0 | NULL | NULL | NULL | 0 | 2026-07-17T15:58:45Z | 0 | ||||||||||||||||||||
| ah-0ge.2 | cc37ff5a5860feb86cb3556e7841450c187af1c9a8c3899f32e4f991134e9e1b | Parent auto-Block/unblock join on child completion | The join half of the delegation loop, per SPEC §14.4 'parent auto-Block/unblock'. When X spawns children it parks in the Blocked bucket — which already exists as a parked bucket that never claims and never kills (SPEC:174) — and returns to Ready when all children reach a terminal state. Needs a parent link (tasks.parent_id in internal/store/schema.sql, or Vikunja task relations) plus somewhere to hold each child's result for the resume payload. UNSPECIFIED IN THE SPEC AND NEEDS A DECISION: partial failure. Y done, Z failed — does X resume with the failure reported, bounce to Failed, or go to Triage? §14 says auto-unblock but not what unblock means when a child died. | X with children Y,Z sits in Blocked while either runs and returns to Ready only once both are terminal; the partial-failure path is decided and covered by a transitions test. | COMPLETED 2026-07-17 together with ah-0ge.3 (they were inseparable — see prior note). The full block→wait→resume join is implemented and tested. STATE MODEL: 'blocked' promoted to a real domain.State (7 states now). domain.States/Valid/CanClaim/CanonicalBuckets updated; BucketBlocked const removed (blocked is a state, not a parked bucket); triage/question remain parked. reconcile iterate() routes the blocked bucket to a new handleBlocked. BEHAVIOR: a successful run that validly delegates now PARKS the parent in Blocked (delegateAndBlock) instead of In Review; children are created in target_bucket (triage=human-gated, ready=full-auto). handleBlocked resumes the parent (new attempt, resumeParent) once EVERY child reaches a terminal bucket (in_review/failed/done/cancelled; a vanished child counts terminal to avoid deadlock). Resume re-runs the role prompt + an appended 'Delegated work completed' section reporting each child's outcome and instructing 'do not re-delegate'. Child-done threshold = In Review (decision). Partial failure = resume-and-report; agent may retry (decision), bounded by MaxPerRun as a per-parent LIFETIME cap (existing ChildIDsOf count + requested) so retries can't loop forever. Depth bounded by MaxGeneration. Tests: domain (7 states); reconcile (spawn→blocks, resume-when-children-finish incl. failed child in prompt, waits-while-pending, + updated ah-0ge.1 spawn/reject tests); store migration/lineage. Full unit suite + e2e green under -mod=readonly. SPEC §9-pseudocode, state model, and §12 updated. NOTE/deviation: resume is a fresh attempt in the parent's existing worktree (which holds its committed work) + resume prompt, NOT a literal 'pi --session' continuation — pi session-file mechanics weren't verifiable here, and the worktree+prompt path is robust and correctness-equivalent for avoiding re-delegation. True --session continuation is a possible future optimization (see ah-0ge.3). | closed | 3 | feature | Eugene Blikh | NULL | 2026-07-17T14:38:03Z | Eugene Blikh | bigbes@gmail.com | 2026-07-17T19:05:05Z | 2026-07-17T19:05:05Z | NULL | 0 | NULL | NULL | NULL | 0 | 0 | 0 | �{} | Block→wait→resume join implemented and tested together: blocked promoted to a real state; delegating parents park in Blocked and resume as a new attempt (in-worktree + resume prompt) once all children reach a terminal bucket. Decisions honored (child-done=In Review; partial-failure=resume-and-report with lifetime-capped retry). Full suite + e2e green. | 0 | NULL | NULL | NULL | 0 | 2026-07-17T17:45:37Z | 0 | |||||||||||||||||||||
| ah-0ge.3 | 5ba4f4620f3f376c331035f6bea0224fc91e067c39719445c159cae5b8477312 | Resume parent via pi --session with child results as a new attempt | Closes the loop: X comes back from Blocked and continues WITH its original context rather than starting cold. runs.pi_session_id is already a column in internal/store/schema.sql and pi supports --session <path|id> (0.73.1, also --session-dir), so this is mostly plumbing — but two things need care. (1) Session survival: X's pi session must outlive the park. Pin --session-dir explicitly rather than relying on a default location, and confirm X's worktree is not reclaimed while parked. (2) Result transfer contract: Y's output has to serialize out of Y's run, through the board, and into X's resume prompt. Needs a defined shape and a size bound — in-process subagents return text into the parent's context for free, this path does not. | Resume is a NEW attempt on X (runs table already keys on (task_id, attempt)), so watchdog/cost/event plumbing is unchanged. Prompt gets an extra rendered section carrying each child's role + result. | X resumes after its children finish, its pi session id is unchanged across the park, the new attempt shows the children's results in the rendered prompt, and an oversized child result is truncated rather than breaking the resume. | COUPLING (2026-07-17): inseparable from ah-0ge.2's behavioral join — see ah-0ge.2 notes. 'Unblock' is only correct as this resume-with-context, else the parent re-runs from scratch and re-spawns children (infinite re-delegation). Recommend implementing ah-0ge.2 (remaining join) + ah-0ge.3 as one unit, starting by promoting 'blocked' to a real domain.State. Lineage foundation (task_parents, ChildIDsOf) already landed under ah-0ge.2. | closed | 3 | feature | NULL | NULL | 2026-07-17T14:38:04Z | Eugene Blikh | bigbes@gmail.com | 2026-07-17T19:05:05Z | 2026-07-17T19:05:05Z | NULL | 0 | NULL | NULL | NULL | 0 | 0 | 0 | �{} | Block→wait→resume join implemented and tested together: blocked promoted to a real state; delegating parents park in Blocked and resume as a new attempt (in-worktree + resume prompt) once all children reach a terminal bucket. Decisions honored (child-done=In Review; partial-failure=resume-and-report with lifetime-capped retry). Full suite + e2e green. | 0 | NULL | NULL | NULL | 0 | NULL | 0 | ||||||||||||||||||||
| ah-0ge.4 | a129a5d69b25f2dc5692877fa06f42251ed75cd3b43a5fd44cf2cb374fa55378 | Ask-user question loop via .task/question.json file channel | File-channel ask-user (SPEC §14.4), mirroring the delegation loop. A successful run writes .task/question.json; the parent parks in a new 'question' state (promoted from parked bucket); the daemon posts the question as a board comment with a sentinel; a human reply resumes the parent as a new attempt with the answer appended. Answer detection is stateless (latest comment lacking the sentinel), so no marker persistence, token, or HTTP endpoint. Deliberately the file-channel form, NOT the ahub-tool-over-HTTP form — same rationale as ah-0ge.1. | A run that writes question.json parks the card in Question with the question commented; a human comment reply resumes the parent as a new attempt carrying the answer. | closed | 3 | feature | NULL | NULL | 2026-07-17T20:14:02Z | Eugene Blikh | bigbes@gmail.com | 2026-07-17T20:14:17Z | 2026-07-17T20:14:17Z | NULL | 0 | NULL | NULL | NULL | 0 | 0 | 0 | �{} | Ask-user question loop implemented and tested (committed 5ad253a): question state, .task/question.json file channel, stateless answer detection via comment sentinel, resume-with-answer. Full suite + e2e green. | 0 | NULL | NULL | NULL | 0 | NULL | 0 | ||||||||||||||||||||||
| ah-0ge.5 | 13cbbe263e9f371367678d1e893b367dba3fad166f059227ee2dbb6d5e9968ff | AgentsView: pg push from agent-1 + per-run session deep links in comments | Make agent runs viewable at agentsview.bigb.es (user asked 'can I view agent logs?'). Two halves: (1) OPS on agent-1 — install go.kenn.io/agentsview (v0.37.5+) for user agenthub, ~/.agentsview/config.toml with [pg] url to phoebe LAN :5433 (published for exactly this; allow_insecure lab tradeoff), systemd timer or 'agentsview pg service' running one-shot AGENTSVIEW_NO_DAEMON=1 pg push (notebook launchd pattern proven: NO_DAEMON avoids 0.37.x writer-daemon deadlock). Verify pi-session source is parsed. (2) DAEMON — finalize comments include the session deep link built from stored PiSessionID (verify central URL shape first). Daemon half must wait for ah-4el merge (same comments.go). | ARCHITECTURE (2026-07-18, after user pushed back on pg-port exposure): use AgentsView's NATIVE HTTP remote sync (v0.37.4+, present in 0.37.5) instead of direct pg push from agent-1 — no fork, no firewall change (phoebe DOCKER-USER allowlists DB ports to 192.168.88.35 only; agent-1 stays out of it). - agent-1: agentsview v0.37.5 (upstream release binary, SHA256-verified) as systemd service, User=agenthub (HOME=/var/lib/agenthub so default pi source .pi/agent/sessions is found), 'serve' bound to LAN, require_auth=true, token via AGENTSVIEW_AUTH_TOKEN EnvironmentFile (root-owned 0600). Archive endpoints are bearer-gated ALWAYS. - phoebe host: same binary as one-shot COLLECTOR on a systemd timer: [[remote_hosts]] {host='agent-1', transport='http', url, token} pulls sessions over HTTP; then AGENTSVIEW_NO_DAEMON=1 pg push to localhost:5433 (phoebe->own docker-proxy is the allowed local path). Notebook launchd flow untouched. - Central agentsview.bigb.es pg serve container: unchanged at 0.37.5. Version-pin ALL nodes at 0.37.5 (pg schema coupling); coordinated bump to 0.38.1 later ('speed up full HTTP sync' is perf-only). - Daemon half (after ah-4el merges): finalize comment gains https://agentsview.bigb.es/sessions/<PiSessionID> deep link, config-gated base URL. VERIFY id shape matches AgentsView session ids once first push lands. - Secrets never enter operator transcript: token minted on agent-1 and piped host-to-host; pg password composed phoebe-locally from the stack .env. | OPS HALF DONE + LIVE (2026-07-18): agent-1 fleet-node daemon (systemd agentsview.service, v0.37.5, token-gated :8080, smoke 401/200 ok) + phoebe host collector (agentsview-collect.timer 10min: HTTP remote sync pull -> pg push loopback 5433). First run pushed 5 agent-1 pi sessions to central pg; id shape agent-1~pi:<pi-session-uuid> => deep link https://agentsview.bigb.es/sessions/agent-1~pi:<PiSessionID>. Infra-as-code committed phoebe-lab 9d4b157 (units + agentsview-node.sh + collector/provision.sh + CLAUDE.md docs, incl. zsh MULTIOS token-leak caution; leaked token was rotated + verified rejected). REMAINING (daemon half, after ah-4el merge): agentsview.base_url config + deep link line in claim/success comments from RunSummary.PiSessionID; verify link renders for a pushed session. | open | 2 | feature | NULL | NULL | 2026-07-18T13:23:54Z | Eugene Blikh | bigbes@gmail.com | 2026-07-18T13:55:10Z | NULL | NULL | 0 | NULL | NULL | NULL | 0 | 0 | 0 | �{} | 0 | NULL | NULL | NULL | 0 | NULL | 1 | ||||||||||||||||||||||
| ah-0ge.6 | b4fcedab02763cd008acf6166f13b5d21f4045245781fa8f20b66db54d065335 | HTTP tools API: per-task bearer tokens + ahub tool CLI over loopback | SPEC §14.4 transport for MID-RUN agent tools (task-create/ask-user without stopping): daemon loopback HTTP with per-task bearer tokens minted at claim (exposed via .task/ env), 'ahub tool <name>' CLI subcommands, server-side policies (Triage-only, gen<=2, per-session caps, dedup, project allowlist). File channel already covers stop-and-resume — this is the deferrable last item of the approved slate. | open | 3 | feature | NULL | NULL | 2026-07-18T13:23:55Z | Eugene Blikh | bigbes@gmail.com | 2026-07-18T13:23:55Z | NULL | NULL | 0 | NULL | NULL | NULL | 0 | 0 | 0 | �{} | 0 | NULL | NULL | NULL | 0 | NULL | 0 | ||||||||||||||||||||||||
| ah-0iz | 7425b905ec10a455e6046127ed987332be425930741cb9673855f0d31f498e0c | markdownToHTML: handle blockquotes (ah-bkr/ah-tz0 integration seam) | ah-tz0 renders the agent summary as a > blockquote in successComment; ah-bkr converts comments to HTML but did not handle blockquote markdown, so the summary would show literal > lines. Add blockquote handling to markdownToHTML. Verified live: Vikunja stores <blockquote>/<strong> intact. | closed | 3 | task | NULL | NULL | 2026-07-18T06:48:12Z | Eugene Blikh | bigbes@gmail.com | 2026-07-18T06:48:12Z | 2026-07-18T06:48:12Z | NULL | 0 | NULL | NULL | NULL | 0 | 0 | 0 | �{} | Added <blockquote> handling to markdownToHTML (mirrors the bullet-run logic; bare > = empty line). Unit tests + live Vikunja round-trip confirm blockquote+strong render. Committed on master. | 0 | NULL | NULL | NULL | 0 | NULL | 0 | |||||||||||||||||||||||
| ah-166 | 70aef8c9b28e9321acde83be3890df0355b088d88dc80415b3e555ce24eadea9 | pi version drift: SPEC pins 0.70.2, installed is 0.73.1 | SPEC §9 says 'Verified against zellij 0.44.3 and pi 0.70.2' and internal/runner/commands.go repeats the pin in its header comment, but the installed pi is 0.73.1 (@mariozechner/pi-coding-agent). Nothing is known to be broken — but every argv note in commands.go ('pi 0.70.2 still loads a path given as --skill even under --no-skills', 'There is no --name flag') is an empirical claim against a version that is no longer the one running. Re-verify the flag behaviours on 0.73.1 and update the pins, or pin the installed version deliberately. | Partial data point 2026-07-17: on installed pi 0.73.1, --skill paths STILL load under --no-skills (verified via catalog probe), so that SPEC §9 argv note survives the 0.70.2->0.73.1 bump. Full flag re-verification (--no-extensions with explicit -e, --mode json event shape, --name absence) still pending. | closed | 3 | task | NULL | NULL | 2026-07-17T14:37:27Z | Eugene Blikh | bigbes@gmail.com | 2026-07-17T15:48:20Z | 2026-07-17T15:48:20Z | NULL | 0 | NULL | NULL | NULL | 0 | 0 | 0 | �{} | Re-verified on pi 0.73.1: no runner-relevant drift. --skill loads under --no-skills; --mode json session-id line and message_end usage.cost.total shape unchanged (confirmed with opencode/claude-haiku-4-5; deepseek reports empty usage as a provider quirk, and the user-echo message_end always had empty usage); no --name flag; -ne keeps explicit -e. Pins annotated in commands.go:8 and SPEC §369. zellij NOT re-checked. | 0 | NULL | NULL | NULL | 0 | NULL | 0 | ||||||||||||||||||||||
| ah-1nu | aa28a50dad31c400da81f00695ec62931cbd81a55e02095fdd26864a8b123ace | BUG: vikunja Snapshot mis-parses real Vikunja 2.3.0 kanban response (buckets-with-tasks, not flat tasks) | Found by the first live board test on agent-1 (Vikunja v2.3.0, project 2). internal/vikunja snapshotOnce GETs /projects/{id}/views/{kanbanView}/tasks and decodes the body as a FLAT []wireTask. But real Vikunja 2.3.0 returns a LIST OF BUCKETS, each with an embedded tasks[] array: item fields are {id(=bucket id), title(=bucket title), project_view_id, limit, count, position, tasks:[...]}. So the daemon reads the 9 buckets as 9 pseudo-tasks with bucket_id=0 (unmapped -> ignored) and NEVER descends into bucket.tasks[]. A task correctly placed in Ready (verified: task 1 nested in bucket 8 with bucket_id 8) is invisible to the daemon; it claims nothing and its store has zero task rows. The unit/e2e fakes (internal/vikunja/vikunja_test.go serveTasks, e2e fake_vikunja) return the FLAT shape the daemon expects, so all tests pass against a fixture that does not match reality — the Snapshot path has never actually run against a real board despite SPEC §9 claiming 'verified against Vikunja 2.3.0'. FIX: snapshotOnce must parse the bucket-with-tasks response — iterate buckets, and for each task in bucket.tasks set BucketID = bucket.id (task.bucket_id is also populated). Reconsider pagination (the kanban response is bucket-structured, likely one page of buckets, tasks possibly paginated per bucket via limit/count/position). Update BOTH fakes to emit the real shape so tests validate reality, and re-verify live. Sample of the real shape is available from the live API. DEPLOY: origin git@git.srht.bigb.es:~bigbes/agents-dev; a push triggers builds.sr.ht CI which scp's the binary to agent-1. Fix is code-only; deploy needs a push. | After the fix, dragging a card to Ready on Vikunja project 2 results in the daemon claiming it (worktree + run) and landing it In Review; fakes emit the bucket-with-tasks shape and a regression test covers it. | FIXED IN CODE + tested 2026-07-18 (committed f0d6634). snapshotOnce now parses []wireBucketTasks (buckets with embedded tasks[]), flattens, maps task.bucket_id->canonical, with a truncation guard replacing the old page-cap. Removed the flat-task pagination machinery (maxPages, pagination-header cross-checks). BOTH fakes (unit vikunja_test + e2e fake_vikunja) rewritten to emit the real bucket-with-tasks shape, so tests validate reality; added flatten/unmapped + truncation regression tests. Full unit suite + e2e green. LIVE re-verification on agent-1 still pending a deploy of the fixed binary. | closed | 1 | bug | NULL | NULL | 2026-07-17T20:44:03Z | Eugene Blikh | bigbes@gmail.com | 2026-07-17T21:29:24Z | 2026-07-17T21:29:24Z | NULL | 0 | NULL | NULL | NULL | 0 | 0 | 0 | �{} | Fixed (f0d6634) AND verified live on agent-1 2026-07-18. After deploying the fixed binary, the daemon claimed task 1 from Ready, created worktree agent/task-1, ran pi (deepseek via LiteLLM), the agent wrote GREETING.md ('Hello from agenthubd.'), committed 7bb3e26, and the task landed In Review with a diff-stat. The Snapshot bug is dead and the bucket-shape parse is proven against real Vikunja 2.3.0. | 0 | NULL | NULL | NULL | 0 | NULL | 0 | |||||||||||||||||||||
| ah-4el | a7f08ae44a7b072a88ceeb4cbb9b41c058403281fc3018ecd03f004f08069332 | Review-verdict-driven transition: changes-requested back to Ready for coder | Every successful run lands In Review; no verdict-aware transition, so a validator changes-requested just sits for a human to drag back. Intentional for now (ah-gs7 human-driven handoff) but user asked for it. Build opt-in loop: parse validator verdict (needs machine-readable verdict), on changes-requested move card back to Ready under coder/fix role with the review as context. Ties into Stage 4 (ah-0ge). | DECIDED 2026-07-18 (user): dedicated FIXER role. Generalized routing design: - task_types.<t> gains on_success: <type> and on_changes_requested: <type> (target is a TASK TYPE name; validated at config load). - Routing = swap board label type:<old> -> type:<target> (Vikunja label API, lookup by title), move card to Ready, comment 'routed to <type> (round n/cap)'. Label-swap failure -> fall back In Review + warn comment. - Verdict channel: first line of .task/summary.md must be 'Verdict: pass' or 'Verdict: changes-requested' (case-insensitive). Parsed into RunSummary.Verdict. Missing/absent verdict = pass-equivalent (lands In Review as today). - changes-requested + on_changes_requested set -> route; pass/none + on_success set -> route; otherwise In Review (current behavior). Failure paths unchanged. - Loop guard: per-task auto_routes counter persisted in store, incremented on EVERY automated route (both edges); config routing.max_auto_routes default 4 (design->review->fix->review = 3). At cap -> In Review + 'routing cap reached' comment. Human manual relabel+Ready still works past cap. - New prompts/fixer.md (address ## Review findings on same branch); validator.md updated to mandate the Verdict first line. - Intended live pipeline: design --on_success--> review --changes-requested--> fix --on_success--> review --pass--> In Review (human). | DESIGN PROPOSAL (held for user sign-off — changes the core reconcile state machine; not live-verifiable while pushes/deploy are blocked). Now unblocked by ah-tz0 (.task/summary.md exists). Proposed: 1. MACHINE-READABLE VERDICT: don't parse free-text. Have the validator write a structured line the daemon can key on — e.g. a first line 'Verdict: pass' | 'Verdict: changes-requested' in .task/summary.md (update prompts/validator.md + validate skill), and add a domain field Verdict (parsed in runner.Summary, empty for non-validator roles). 2. OPT-IN CONFIG: task_types.review gains 'on_changes_requested: <bucket|role>' (default: none = current behavior, lands In Review for a human). When set e.g. to role 'coder', a changes-requested validator run moves the card to Ready under that role instead of In Review, carrying the review as context (the ## Review is already on the branch). 3. LOOP GUARD: reuse the delegation generation cap (max_generation) or add a review_rounds cap so design->review->coder->review can't cycle forever. A 'pass' verdict always lands In Review (human ships it). 4. STATE MACHINE: this is a new transition in internal/reconcile (finalize path) + internal/domain/transitions.go. Cover with reconcile_test.go (injected clock/fakes) AND an e2e flow before shipping. Open questions for user: (a) auto-loop to 'coder' or a dedicated 'fixer' role? (b) opt-in per task_type (proposed) vs global? (c) cap value / mechanism. HOLDING until answered. | in_progress | 3 | feature | Eugene Blikh | NULL | 2026-07-18T06:30:21Z | Eugene Blikh | bigbes@gmail.com | 2026-07-18T13:21:51Z | NULL | NULL | 0 | NULL | NULL | NULL | 0 | 0 | 0 | �{} | 0 | NULL | NULL | NULL | 0 | 2026-07-18T13:21:51Z | 0 | ||||||||||||||||||||||
| ah-6ei | d903b7798442ca41f9cab7dc12f1e9ded4c222c4860ea1cad13f7311720861eb | Live end-to-end verification of designer/validator roles on a real board | ah-gs7 delivered the design/validate skills + designer/validator role prompts + config wiring, all component-verified (skills load in pi 0.73.1; both roles produce the right artifacts headless; config.example loads). The ONE piece not yet exercised is the literal daemon flow: board card labeled type:design -> daemon claims -> designer role runs via LiteLLM -> lands In Review with a Design doc -> moved to Ready under type:review -> validator writes a ## Review. Needs the phoebe-lab infra (Vikunja board + LiteLLM + real pi/zellij). Depends on ah-k23. | On a live board: a type:design card lands In Review with docs/tasks/<slug>.md ## Design; relabeled type:review and set Ready, it produces a ## Review against that same file on the same branch. | closed | 3 | task | Eugene Blikh | NULL | 2026-07-17T19:49:21Z | Eugene Blikh | bigbes@gmail.com | 2026-07-18T06:27:20Z | 2026-07-18T06:27:20Z | NULL | 0 | NULL | NULL | NULL | 0 | 0 | 0 | �{} | Verified live on project 2 / agent-1. Flushed out the real gap: (1) live config lacked design/review task_types+designer/validator roles+skills_dir; (2) NO CI deploy lane existed (.build.yml) and the agent1-deploy key was lost. Built the CI lane (.build.yml, commit 38dd035, build #226 SUCCESS: builds 3 cmds + ships bin/prompts/skills to /opt/agent-hub + restarts), regenerated+registered the deploy key (builds secret cc6bd011), wired designer/validator into the live config. Then ran the flow: task 2 type:design -> designer -> In Review with ## Design (ae7db66); relabel type:review+Ready -> validator -> ## Review changes-requested on the SAME file/branch (374b583, agent/task-2). Validator even caught a real arithmetic bug in the design's edge-case table. | 0 | NULL | NULL | NULL | 0 | 2026-07-18T05:09:01Z | 0 | ||||||||||||||||||||||
| ah-6u0 | aecd5599069260da43810d3d7ad0db775e36028ce4175b282a0cd26f6aa1a911 | docs: SPEC section 6 RunStatus struct omits MetaAttempt | Pre-existing Stage 1 drift found during the Stage 2 SPEC sync (ah-xuc.12): the RunStatus struct listing in SPEC section 6 lacks the MetaAttempt field even though section 9 prose relies on RunStatus.MetaAttempt (the attempt recorded in .task/meta.json, 0 when absent). Make the section 6 struct field-complete to match internal/ports/ports.go. | closed | 4 | task | Eugene Blikh | NULL | 2026-07-13T08:10:10Z | Eugene Blikh | bigbes@gmail.com | 2026-07-13T09:26:46Z | 2026-07-13T09:26:46Z | NULL | 0 | NULL | NULL | NULL | 0 | 0 | 0 | �{} | 279b733: MetaAttempt added to SPEC section 6 RunStatus listing, alignment verified against gofmt; accuracy-reviewed in the follow-up gate | 0 | NULL | NULL | NULL | 0 | 2026-07-13T08:46:20Z | 0 | |||||||||||||||||||||||
| ah-943 | b234e7fe8bc9f64d11011d2b27d1991623feb067d1858d5d2b1d605d27665fd5 | Refresh phoebe-lab agent-hub provisioning template + bootstrap for designer/validator | The live /etc/agent-hub/agenthub.yaml on agent-1 was hand-wired for project 2, repo agent-demo, deepseek model, AND now designer/validator roles + design/review task_types + skills_dir=/opt/agent-hub/skills. The phoebe-lab/agent-hub/config/agenthub.yaml.example (first-boot seed) is stale vs this: still project_id 1, repo demo, coder-only, no skills_dir/task_types. bootstrap.sh also only pre-creates /opt/agent-hub/{bin,prompts} (skills/ gets created by the CI tar overlay, which works but isn't declared). Update the example + bootstrap so a fresh box provisions the full role set. Also: keys/agent1-deploy.pub was regenerated this session (new ed25519, matching builds.sr.ht secret cc6bd011) and is modified-uncommitted in the phoebe-lab repo — commit it or a redeploy/bootstrap reinstalls the OLD dead key. | closed | 3 | task | Eugene Blikh | NULL | 2026-07-18T06:27:20Z | Eugene Blikh | bigbes@gmail.com | 2026-07-18T06:41:21Z | 2026-07-18T06:41:21Z | NULL | 0 | NULL | NULL | NULL | 0 | 0 | 0 | �{} | Provisioning template (config/agenthub.yaml.example) now scaffolds skills_dir + designer/validator roles + design/review task_types; bootstrap.sh pre-creates /opt/agent-hub/skills. Committed phoebe-lab 34bcec3 (local; deploy via labng). Pubkey rotation committed earlier (abab805). | 0 | NULL | NULL | NULL | 0 | 2026-07-18T06:40:04Z | 0 | |||||||||||||||||||||||
| ah-9r4 | c8e9bb1e135799eacb8565a6f41478d32efa61f0c311dedb27b638a4b6f912c0 | DECISION: worktree/branch topology for agent-created child tasks | Blocks the delegation loop. Parent X owns worktree <work_root>/<slug>/task-X on branch agent/task-X. Where does child Y work? git refuses to check the same branch out into two worktrees, so Y cannot enter X's. Branching Y off the default branch means a later merge into X's divergent branch; branching Y off agent/task-X breaks the review/publish path, because gitLogOneline/gitDiffStat (internal/runner/commands.go) compute <default>..HEAD against the repo default branch and would report all of X's work as Y's diff. Proposed split by whether the role writes code: ADVISORY roles (research/review/design — return text, touch no code) get a detached-HEAD worktree at X's tip. git permits many worktrees on the same COMMIT, just not the same BRANCH, so Y reads X's WIP with no collision and no merge. Only WRITER roles need real branches and a merge story — and the advisory-only subset may cover the useful cases, deferring the writer problem entirely. Verify the detached-HEAD claim against the pinned git before building on it. | DECISION (verified on git 2.55.0, 2026-07-17): SPLIT roles by whether they write code. ADVISORY roles (research/review/design — read parent WIP, return TEXT, publish nothing): each child gets a DETACHED-HEAD worktree at the parent's tip commit: git worktree add --detach <child-wt> <parent-tip-sha> Proven: child sees parent's uncommitted-into-branch WIP (the parent's committed tip), runs on 'HEAD (no branch)', and any commits it makes do NOT move the parent's agent/task-<id> branch. Teardown (git worktree remove) leaves only unreferenced commits — no branch leak, no merge. This is the recommended FIRST and possibly ONLY implementation. WRITER roles (produce code that must land on the parent's branch): DEFERRED. They need a real branch plus a merge story, and note that gitDiffStat/gitLogOneline compute <default>..HEAD — a writer child branched off agent/task-<id> would misattribute ALL of the parent's work as the child's diff, so a writer child needs a different diff base (merge-base against the parent branch, not the repo default). Do not build until an advisory-only loop proves insufficient. Confirmed anti-pattern: git refuses 'worktree add <path> agent/task-X' while X's worktree holds that branch ('already used by worktree at ...'), so a child can never simply enter the parent's branch. CODE SEAM for ah-0ge.1: the runner's worktree creation (internal/runner/commands.go gitWorktreeAddNewBranch, base=default) needs a base-override path for child tasks: detached add at the parent tip instead of a new branch off default. Child worktree path can follow the normal <work_root>/<slug>/task-<childid> convention; only the base differs. | A written decision recording: which roles are advisory vs writer, how each child's worktree base is chosen, and what the diffstat base is for a child (if writer roles are in scope at all). | closed | 2 | task | NULL | NULL | 2026-07-17T14:37:26Z | Eugene Blikh | bigbes@gmail.com | 2026-07-17T15:49:21Z | 2026-07-17T15:49:21Z | NULL | 0 | NULL | NULL | NULL | 0 | 0 | 0 | �{} | Decided: advisory children use detached-HEAD worktree at parent tip (verified git 2.55.0); writer children deferred. Full rationale + code seam in the design field. | 0 | NULL | NULL | NULL | 0 | NULL | 0 | |||||||||||||||||||||
| ah-a6f | 6af2a6590cb99466cc1eb91edd91cc384fee7a0da9d277862587d8eeda63d644 | e2e: update comment assertions to HTML (ah-bkr fallout) | ah-bkr converts comments to HTML at the Comment() boundary; the build-tagged e2e suite (not run by ah-bkr subagent, which only tested its package) still asserted the markdown form (branch `x`, outcome `timeout`). Updated 5 assertions to the <code> HTML form. Full e2e green. | closed | 3 | task | NULL | NULL | 2026-07-18T06:54:54Z | Eugene Blikh | bigbes@gmail.com | 2026-07-18T06:54:54Z | 2026-07-18T06:54:54Z | NULL | 0 | NULL | NULL | NULL | 0 | 0 | 0 | �{} | Updated 5 e2e comment assertions (branch/worktree/outcome) from markdown backticks to <code> HTML. Full e2e suite passes (10.95s). Root cause: ah-bkr subagent scoped to its own package; build-tagged e2e wasn't exercised until the integration sweep. | 0 | NULL | NULL | NULL | 0 | NULL | 0 | |||||||||||||||||||||||
| ah-bkr | 71a63b844de91dc83b0eb9f07dc18564b27c29ebdff9d7337726d421f219f268 | Vikunja comments render literal markdown fences — post HTML instead | The In Review notification comment is built as markdown (internal/reconcile/comments.go successComment: triple-backtick fences around the diff stat, backtick spans). Vikunja stores/renders task comments as HTML (TipTap rich-text), so the markdown shows LITERALLY in the UI (screenshot: raw fences and + diffstat shown verbatim). internal/vikunja Comment() sends the string as-is to PUT /tasks/{id}/comments. Fix: render markdown to HTML (or emit HTML) before posting. Affects every daemon comment. | closed | 2 | bug | Eugene Blikh | NULL | 2026-07-18T06:30:20Z | Eugene Blikh | bigbes@gmail.com | 2026-07-18T06:44:12Z | 2026-07-18T06:44:12Z | NULL | 0 | NULL | NULL | NULL | 0 | 0 | 0 | �{} | markdownToHTML converts outbound comments to HTML at the vikunja Comment() boundary (dep-free, escaping + placeholder-safe). Builders still emit markdown (comments_test green). Verified: 89 subtests pass; live Vikunja round-trip confirmed the API stores <p>/<pre><code>/<ul>/<a> intact (markdown showed literally before). Merged to master (f6098a9). | 0 | NULL | NULL | NULL | 0 | 2026-07-18T06:38:47Z | 0 | |||||||||||||||||||||||
| ah-ddd | 2b245b73220440977211c08547be4a22c630b6b314adfc53409f16a96a60f414 | ntfy: notifier for ntfy-compatible endpoints (Prism) | The operator runs Prism (phoebe-lab/prism), an ntfy-compatible notification gateway at https://prism.bigb.es/{topic} that delivers to Telegram and owns the TG proxy egress itself (patched Telego + lab singbox). Publishing to it is a plain lab-local HTTPS POST — no HTTPS_PROXY handling needed in the daemon (prism.bigb.es is inside the NO_PROXY zone). This replaces the operational need for the direct Telegram path; internal/telegram stays as an alternative. New package internal/ntfy implementing ports.Notifier: POST the configured topic URL with Authorization: Bearer <token>, Content-Type application/json, body {"message": <text>} (title omitted — ntfy treats it as optional; per the Prism README the JSON publish shape is {"title","message"}). 10s client timeout, bounded response read, non-2xx = error with a short body prefix, bearer token never in logs or error strings (redact like internal/telegram does), no retries (Notify is best-effort by contract). Config: ntfy block {url, token} — present when either is set, then both required; url must parse as http(s). Configuring BOTH ntfy and telegram is a config error (exactly one notifier; explicit over precedence). config.example.yaml gains a commented block pointing at a Prism topic URL with ${PRISM_API_KEY}. cmd/agenthubd: notifier selection becomes ntfy | telegram | slog no-op (config validation guarantees not-both); construction error fatal at startup like telegram. Docs: SPEC section 4 layout (internal/ntfy), section 6 Notifier comment, section 12 config example + validation rules, section 15 security note (bearer key via env ref, token redaction). | go test ./internal/ntfy/... ./internal/config/... ./cmd/... green with -race on ntfy; httptest covers success, non-2xx, token redaction, context cancellation; config table tests cover both-or-neither, bad url, both-notifiers-configured error; wiring test asserts ntfy selected when configured; full go test ./... and -tags e2e green; SPEC updated in the same style as the Stage 2 sync | closed | 2 | feature | Eugene Blikh | NULL | 2026-07-13T09:44:41Z | Eugene Blikh | bigbes@gmail.com | 2026-07-13T10:20:36Z | 2026-07-13T10:20:36Z | NULL | 0 | NULL | NULL | NULL | 0 | 0 | 0 | �{} | 378216d+6734986+4c3c350+5438f7c + review fix (proxy wording, trimmed presence checks); reviewer merge-ready (redirect bearer-strip verified against stdlib, no token leakage); validator 11/11 incl. live wire probe matching the Prism publish shape exactly | 0 | NULL | NULL | NULL | 0 | 2026-07-13T09:44:55Z | 0 | ||||||||||||||||||||||
| ah-gs7 | c0bcd671b99d03b8fb704cffc0b49ffe839b72cbf1a7a9bd61ccaf8166cc850e | Ultrapack designer/validator roles via config + skills (human-scheduled) | Ultrapack (github.com/bigbes/ultrapack) is an OpenCode pack, but its value is the skills (udesign/uplan/uexecute/uverify/ureview), and pi implements the Agent Skills standard — so the skills load unmodified via --skill with no porting. Stage 2 already delivers --skill materialization, task_types and per-task frontmatter roles, so multi-role work needs ZERO daemon code today: add designer/validator roles to config pointing at ultrapack skill packages under skills_dir, and let a human drag cards between them. State carries between runs in docs/tasks/<slug>.md on the task branch. Do this BEFORE automating the handoff (ah-0ge slice): it validates whether the roles are actually good while the orchestration is still free. Ultrapack agents/ do NOT get ported — see the delegation-loop beads; agents become roles, not subagents. | Roles are {model, prompt} today (internal/config/config.go:72). Fork ultrapack skills into skills_dir packages; rewrite @implementer/@explorer/@reviewer subagent references (uplan/SKILL.md:126-128, uexecute/SKILL.md:37, handsoff/SKILL.md:35) out of the skill bodies — pi has no @-mention dispatch. Designer role MUST use ultrapack's handsoff skill: udesign/SKILL.md says 'Nothing is planned or written until the user approves', which deadlocks a board-driven run until the watchdog kills it as stalled. | A task labeled for the designer role produces a docs/tasks/<slug>.md with a Design section and lands In Review without stalling; dragging it to Ready under the validator role produces a review against that same file on the same branch. | DELIVERED + COMMITTED (982694f) 2026-07-17. Artifacts: skills/design (adapted from ultrapack udesign — autonomous, no wait-for-approval, records ### Decisions), skills/validate (single-agent review distilled from ultrapack ureview's reviewer criteria — confidence>=80, severity-tiered, never edits code), prompts/designer.md + prompts/validator.md (autonomy framing verified to prevent headless stalls), config.example.yaml wiring (skills_dir=skills, designer/validator roles, type:design/type:review task_types), README Roles section. COMPONENT-VERIFIED headless on pi 0.73.1 (opencode/claude-haiku-4-5): both skills load under --no-skills via --skill; designer produced docs/tasks/multiply-and-divide.md with full ## Design + IV/PC/AS/UK + TDD + ### Decisions, clean stop, no stall; validator wrote ## Review with a changes-requested verdict and did NOT edit code. config.example loads (TestLoadExample green). NOT verified: the literal live daemon+board flow (label->task_type->role->run) — infra-gated. Tracked as a follow-up depending on ah-k23. Design notes: udesign/ureview were ADAPTED not vendored verbatim — udesign is collaborative and references ultrapack-only skills (uplan/handsoff/references), and ureview is built around a dispatcher+@reviewer split that doesn't fit a single agent. The @-subagent references live only in the executor skills (uexecute/uplan/handsoff/ureview), so designer+validator needed minimal @-rewrite. | closed | 2 | feature | Eugene Blikh | NULL | 2026-07-17T14:37:25Z | Eugene Blikh | bigbes@gmail.com | 2026-07-17T19:49:23Z | 2026-07-17T19:49:23Z | NULL | 0 | NULL | NULL | NULL | 0 | 0 | 0 | �{} | Designer/validator roles delivered and component-verified (skills + prompts + config, committed 982694f); both roles produce the right artifacts headless on pi 0.73.1. Live daemon+board acceptance tracked as a follow-up gated on ah-k23 infra. | 0 | NULL | NULL | NULL | 0 | 2026-07-17T19:24:42Z | 0 | ||||||||||||||||||||
| ah-k23 | da0056f7dd6aa3670d94ec5f884c39b8591e1894a3ad250858f26540912ac504 | Infra: phoebe-lab wiring for agents-dev | Tracked in ~/data/home/phoebe-lab (deploy via labng). Runtime setup: Vikunja project+buckets+API token+webhook target; LiteLLM virtual key for agents (budget-capped); mem0 m0sk key; srht agent-hub user + repo ACLs; later: Proxmox VM (lab.yml remote entry, basic-vmagent host, VM-local singbox for TG egress — decided: VM-local, not LAN-published), agentsview pg-push firewall allowlist for the VM. | RUNTIME WIRING COMPLETE — full pipeline proven live incl. review-branch publish (2026-07-18). agenthubd on agent-1 runs board tasks end-to-end: Ready -> claim -> worktree -> pi(deepseek/LiteLLM) -> commit -> In Review with diff-stat AND a review link (agent branch pushed to srht). Everything on the bead's critical path is done: VM (agent-1), Vikunja project 2 + 9 buckets, LiteLLM budget-capped virtual key, pi models.json, unit PATH fix (ahub-run), srht push credential (agenthub SSH key, port 2222). Fixes made this session also flushed out and closed the vikunja Snapshot bug (ah-1nu). Deferred sub-items belong to other epics: mem0 m0sk key (Stage 3 ah-ydx), agentsview pg-push firewall allowlist (Stage 5 ah-oeq). VM-local singbox TG egress superseded by Prism/ntfy. Repo edits committed to phoebe-lab (4a2e9b1: unit PATH + CLAUDE.md); live-only changes (config, models.json, ssh key, PATH drop-in) applied on agent-1 and documented in agent-hub/CLAUDE.md. | in_progress | 3 | epic | Eugene Blikh | NULL | 2026-07-12T23:36:30Z | Eugene Blikh | bigbes@gmail.com | 2026-07-18T04:52:29Z | NULL | NULL | 0 | NULL | NULL | NULL | 0 | 0 | 0 | �{} | 0 | NULL | NULL | NULL | 0 | 2026-07-17T20:23:54Z | 0 | |||||||||||||||||||||||
| ah-nyl | 343ac6a94d3bd1074e58a6f7897af90cfc1a84f2e764dc2f17d9f96ba094b31f | Stage 1 MVP: poll-driven happy path | Vikunja task dragged to Ready -> claim -> worktree+branch -> pi in zellij -> In Review with diff summary (or Failed with diagnostics). Everything per docs/SPEC.md (authoritative). Done when: e2e harness green + a real task passes the loop against live Vikunja. | WORKFLOW (user directive): every implementer wave is followed by (a) hostile read-only reviewer agents and (b) empirical validator agents (real zellij/git/binaries, not stubs) BEFORE the next wave dispatches. Findings are triaged into fix beads. | closed | 1 | epic | NULL | NULL | 2026-07-12T23:34:03Z | Eugene Blikh | bigbes@gmail.com | 2026-07-13T04:02:45Z | 2026-07-13T04:02:45Z | NULL | 0 | NULL | NULL | NULL | 0 | 0 | 0 | �{} | Stage 1 complete: all 15 child beads closed; e2e acceptance green (happy/failure/timeout); 3 review rounds + 2 empirical validations converged to SHIP | 0 | NULL | NULL | NULL | 0 | NULL | 0 | ||||||||||||||||||||||
| ah-nyl.1 | faee2c254f4120dc1b198544543cd6073a586bf48f0606929988685660049281 | foundation: go module, domain, ports, config | Create the Go foundation of agents-dev exactly per docs/SPEC.md (read it fully first; SS4-6, SS12-13 are normative for this task). Deliverables: 1. go.mod: module go.bigb.es/agents-dev, go 1.26. Add ALL stage-1 external deps now so siblings never touch go.mod: modernc.org/sqlite, gopkg.in/yaml.v3. Populate go.sum by building a throwaway smoke import (e.g. internal/smoke_test.go importing both, then delete the file but keep go.sum entries; or keep a tiny blank-import file under internal/deps/deps.go with build tag 'deps' — your choice, document it). 2. internal/domain: types + constants from SPEC SS5 (State, Outcome, Task, Repo, TaskRecord, Run, Event, RunSummary) and pure transition helpers, at minimum CanClaim(*TaskRecord) bool per SS5 semantics. Table tests for every helper. 3. internal/ports: interfaces exactly as SPEC SS6 (Board, Runner, Store, Notifier + BoardTask, StartSpec, RunInfo, RunStatus). Doc comments on every method stating error/nil semantics ((nil, nil) for absent, etc.). 4. internal/config: Load(path string) (*Config, error) implementing SPEC SS12: yaml.v3 with KnownFields(true), ${VAR} env expansion in string values (only for vars that exist; unknown var = validation error), "~" expansion in paths, duration parsing, validation that collects ALL problems into one error (repos non-empty, default_repo/default_role resolve, buckets map has all six canonical keys, listen is loopback host:port). Config struct mirrors config.example.yaml at repo root (keep the two in sync; fix the example if you find an inconsistency and note it in the commit message). Tests: golden-load of config.example.yaml with env set, plus failure cases. Rules: stdlib + the two deps only; log/slog if logging is needed (probably not here); no package-level state. Run: go build ./... && go vet ./... && go test ./... (allowed for THIS task only, since you own the whole tree). Commit everything as one or two commits, message style 'domain: ...', 'config: ...'. Do NOT push. Do NOT touch .beads/. Acceptance: go build/vet/test green on a clean checkout; interfaces compile exactly against the names/signatures in SPEC SS6 (parallel siblings will implement them verbatim); config.example.yaml loads. | closed | 0 | task | NULL | NULL | 2026-07-12T23:34:03Z | Eugene Blikh | bigbes@gmail.com | 2026-07-12T23:55:03Z | 2026-07-12T23:55:03Z | NULL | 0 | NULL | NULL | NULL | 0 | 0 | 0 | �{} | Closed | 0 | NULL | NULL | NULL | 0 | 2026-07-12T23:38:10Z | 0 | |||||||||||||||||||||||
| ah-nyl.10 | 867e80be5ab1f94c6768afe0ee66011466dec5fca7e42479d958babdf51ea649 | fix: reconcile review findings 1-6 (FIX-FIRST verdict) | Hostile review of internal/reconcile @ 2acf7c4 returned FIX-FIRST with findings below. Fix ALL six (1-3 blocking, 4-5 behavior, 6 test infrastructure that would have caught 1). Line numbers refer to internal/reconcile/reconcile.go @ 2acf7c4. F1 MAJOR (:294-304,:311-330): a FINISHED latest-run row is fed to check(); runner.Status ignores attempt, so a live re-attempt gets killed on the old row's ancient StartedAt (timeout) and/or the new attempt's result is written onto the previous attempt's row. FIX: check() only when run.State == RunStateRunning; when rec.State==in_progress and latest run is finished, route to adoptOrFail and there create the missing run row as attempt = latestRun.Attempt+1 when adopting a live runtime (instead of skipping CreateRun). F2 MAJOR (:250-262): UpsertTask/CreateRun failure AFTER successful runner.Start leaves the claim retryable -> next tick re-claims same attempt: Start wipes the live attempt's exit.json/events and opens a SECOND pi pane on the same worktree (duplicate paid agents). Same divergence from a daemon crash between Start and persist. FIX: (a) on persist failure after Start, compensate with best-effort runner.Kill before returning (log both errors); (b) before Start in claim, probe runner.Status for the computed attempt — if there is evidence of a live/completed runtime for it, adopt instead of double-starting. F3 MAJOR (:411-412,:216-220): comment is posted BEFORE MoveToBucket in the adoptOrFail fail-branch and the claim render-failure path -> persistent move failure = a new comment every 20s forever; the claim_failed event is also appended even when the move failed. FIX: reorder both sites to move-then-comment (finalize already does this); gate the claim_failed event on move success (mirror adopt_failed). F4 MINOR (:334-347): finalize unconditionally rewrites an already-finished run row on retry (e.g. timeout kill recorded, then UpsertTask failed; next tick Status says crashed -> outcome falsified timeout->crashed, FinishedAt smeared). FIX: when run.State == RunStateFinished, skip UpdateRun and derive the target task state from the stored run.Outcome (success->in_review, else failed). F5 MINOR (:135-136): an in_progress record whose card sits in an UNMAPPED bucket is skipped entirely -> no check(), no timeout; the run burns tokens unobserved. DECISION (fix this way): run check() for any record with State==in_progress regardless of the card's bucket (including unmapped and ready-awaiting-heal); card moves still only happen per the §10 bucket rules — i.e. decouple runtime supervision from card position. F6 MINOR (test honesty, fakes_test.go:290-299): (a) fakeBoard.MoveToBucket must update the task's bucket so consecutive iterations see the moved card; (b) add double-iterate zero-delta assertions (run iterate twice with unchanged inputs after claim, finalize, adopt-fail: second pass produces ZERO new side effects) — this is the level-triggered core property; (c) key fakeRunner status by (taskID, attempt) so per-attempt misattribution (F1) is expressible; add regression tests for F1 (finished run + live re-attempt: no kill, no overwrite; adoption creates attempt+1 row), F2 (persist-failure -> Kill called; probe-adopt on retry), F3 (move fails -> no comment posted, no claim_failed event; comment appears after move succeeds), F4 (finalize retry preserves outcome), F5 (unmapped bucket + in_progress -> check still enforces timeout). Constraints: scope = internal/reconcile/ ONLY (the reviewer's suggested fixes deliberately keep runner untouched). SPEC §10 stays authoritative — where these fixes refine it (F1 routing, F3 ordering, F5 supervision decoupling), implement per THIS bead; the orchestrator will sync SPEC afterwards. Conventions: testify + culpa (SPEC §13). Build/test ONLY ./internal/reconcile/... (never ./...). Verify with -race. Commit 'reconcile: fix review findings (check routing, claim compensation, move-then-comment, finalize guard, unmapped supervision)' staging only internal/reconcile; hooks bypassed (git -c core.hooksPath=/tmp/nohooks commit); retry on index.lock; do NOT push; do NOT touch .beads/. | closed | 0 | bug | NULL | NULL | 2026-07-13T00:36:33Z | Eugene Blikh | bigbes@gmail.com | 2026-07-13T00:56:49Z | 2026-07-13T00:56:49Z | NULL | 0 | NULL | NULL | NULL | 0 | 0 | 0 | �{} | landed e863502: check routing via run.State, adopt creates attempt+1, claim probe-adopt + compensation kill, move-then-comment, finalize finished-row guard, bucket-decoupled supervision, fakes keyed by (task,attempt) + zero-delta assertions; 34 tests -race green | 0 | NULL | NULL | NULL | 0 | 2026-07-13T00:36:34Z | 0 | |||||||||||||||||||||||
| ah-nyl.11 | ccf963c6cdbbcf9043784dbdf1f98f71af8baca8cbd9a58cca23607dc1ff7f92 | fix: runner/ahub-run review findings 1-10 (FIX-FIRST verdict) | Hostile review of internal/runner + cmd/ahub-run @ HEAD (4348d2b) returned FIX-FIRST: findings 1-3 blocking, 4 strongly recommended, 5-9 hardening, 10 test gaps. Fix ALL. Line refs @ 4348d2b. F1 MAJOR (runner.go:201-225, commands.go:45-47): dir missing + branch exists + worktree still REGISTERED (human rm -rf'd it) -> `git worktree add` exits 128 "missing but already registered"; Start wedges forever. FIX: in ensureWorktree, when the worktree dir is missing, run `git -C <repo> worktree prune` before add (reproduced working on git 2.55). Add a regression test that registers a worktree, rm -rf's the dir WITHOUT prune, and asserts Start succeeds. F2 MAJOR (cmd/ahub-run/main.go): no signal handling — SIGTERM/SIGINT/SIGHUP to ahub-run kills the supervisor with NO exit.json; the child pi survives until its next stdout write (SIGPIPE), burning tokens; the zellij session survives so Status says Running for the full 30m timeout, then reports the wrong outcome. FIX: signal.Notify(SIGINT, SIGTERM, SIGHUP); forward the signal to the child (process group where sensible), wait for it, then STILL write exit.json (128+sig) and POST the report. Regression test: signal a running ahub-run, assert child死 + exit.json written with 128+sig. F3 MAJOR (runner.go:103-133, 158-163): Status ignores its attempt arg (`_ = attempt`) and never checks ef.Attempt -> a stale attempt-K supervisor's exit.json completes attempt K+1 with K's exit code; the truncated-then-repopulated events.jsonl attributes K's session id and costs to K+1. Kill swallows every kill-session failure at Debug, so a wedged zellij is indistinguishable from dead. FIX: (a) in Status, ef.Attempt != attempt => treat as no-exit.json and fall through to session check; (b) in Kill, after kill-session verify via list-sessions that the session is gone; if still alive, return an error (Warn+error). Regression tests for both. F4 MAJOR (runner.go:62-97): Start is not idempotent per attempt — after a claim-persist failure the reconciler re-claims the SAME attempt and Start re-prepares (truncating the live events.jsonl) and spawns a duplicate pane. FIX: at the top of Start, if .task/meta.json matches (task_id, attempt) AND exit.json is absent AND the zellij session is alive -> return the existing RunInfo (worktree/branch/session) without re-preparing or re-spawning. Regression test. F5 MINOR (runner.go:344-346): sessionAlive maps non-zero exit + EMPTY output to "no sessions" -> silent zellij failure finalizes a live run as crashed. FIX: only the recognized "no active session" text (or clean exit) means no-sessions; empty output + non-zero exit => return an observation error. Update the test at runner_test.go:396-403 that pins the old hazardous mapping. F6 MINOR (runner.go:406-413): runCmd merges stderr into the parsed stdout buffer -> git warnings corrupt gitResolveCommonDir (exclude entry lands in a bogus silently-created path) and Summary turns stderr into fake commit lines. FIX: separate stdout/stderr buffers; parse stdout only; include stderr in error messages. F7 MINOR (taskfiles.go:61-79): WriteExitFile renames without fsync -> post-power-loss empty-but-present exit.json makes ReadExitFile error forever. FIX: tmp.Sync() before Close/rename. F8 MINOR (internal/config/config.go:201-208 + commands.go:32-34): repo slug is not shape-validated; "/" or ".." in a slug makes worktreePath escape work_root. FIX in internal/config (you MAY touch config for exactly this): reject slugs not matching ^[A-Za-z0-9._-]+$ (and not "." / ".."), with a validation-collected error + test. This is the ONLY change allowed outside runner/ahub-run. F9 MINOR (events.go:49-51, 94-108): (a) a single line >10MB aborts the whole scan silently (message_end lines grow with conversation) -> switch to a reader that SKIPS an over-long line and continues; (b) cost summation semantics are under-pinned — fixtures have one assistant message; if pi's usage.cost.total is cumulative, += double-counts. Without re-running pi: add a two-assistant-message fixture built from the existing captured shape, document the per-message assumption in a comment referencing the fixture provenance, and add a TODO-marked guard test so the fixture is easy to re-pin from a real capture later. F10 test honesty: add the tests named above; also remove/rework TestKillDeadSessionIsNotAnError so it distinguishes "session already gone (ok)" from "kill failed but session alive (error)". Constraints: scope = internal/runner/, cmd/ahub-run/, plus the single F8 validation in internal/config/. Conventions: testify + culpa (SPEC §13). Build/vet/test ONLY your packages (./internal/runner/... ./cmd/ahub-run/... ./internal/config/...) with -race; never ./... . Another fixer is working in internal/reconcile concurrently — do not touch it. Commit 'runner: fix review findings (worktree prune, signals, per-attempt status, idempotent start, hardening)' (+ separate 'config: validate repo slug shape' commit) staging only your paths; hooks bypassed (git -c core.hooksPath=/tmp/nohooks commit); retry on index.lock; do NOT push; do NOT touch .beads/. | F11 (VALIDATOR blocker D1, empirically established on real zellij 0.44.3): sessions that lived past zellij's session-serialization tick (~65s, default config) and then die WITHOUT delete-session (pi/ahub-run crash killing the pane, zellij server death, reboot with cache) remain listed by 'list-sessions --short' as bare names indistinguishable from live sessions -> Status reports Running forever; OutcomeCrashed unreachable for real crashes. FIX: switch session liveness to 'zellij list-sessions --no-formatting' (plain text, dead sessions carry the '(EXITED - attach to resurrect)' suffix) and treat EXITED as NOT alive; parse defensively (name = first whitespace-separated token; EXITED detection by substring); update stub scripts + tests incl. an EXITED-listed case. Kill() keeps kill-then-delete (validator confirmed delete-session removes serialized dead sessions, exit 0). ALSO F12 (validator observation): zellij pane env = SESSION-CREATION-TIME server env, not run-client env — so bare 'ahub-run'/'pi' argv depend on the daemon's PATH at attach --create-background time. Harden: resolve ahub-run and pi to ABSOLUTE paths via exec.LookPath at Start (error clearly if not found) and use those in the pane argv; keeps working under systemd's minimal PATH later. | closed | 0 | bug | NULL | NULL | 2026-07-13T00:37:45Z | Eugene Blikh | bigbes@gmail.com | 2026-07-13T01:05:34Z | 2026-07-13T01:05:34Z | NULL | 0 | NULL | NULL | NULL | 0 | 0 | 0 | �{} | landed 7a489b9 (runner+ahub-run) + b978e0e (config slug): prune-before-add, signal handling w/ pgid, per-attempt status, idempotent start, EXITED-aware liveness via --no-formatting, absolute pane binaries, stream split, fsync, long-line skip; 5 negative controls confirmed | 0 | NULL | NULL | NULL | 0 | 2026-07-13T00:37:46Z | 0 | ||||||||||||||||||||||
| ah-nyl.12 | 7714724648b34e987bf24b58176889b5580a4ff3c05efff661cea5999723a656 | hardening: data-layer review fast-follows (vikunja pagination, cache refresh, config validation) | Data-layer review @ b86928a returned SHIP with fast-follow hardenings. Implement all. Line refs @ b86928a. H1 (from MAJOR-latent finding, internal/vikunja/board.go:28-73): Snapshot must NEVER silently return a partial board (reconcile treats missing tasks as vanished -> kills runs, cancels tasks). Fixes: (a) hitting maxSnapshotPages -> return an error, not partial data; (b) when the x-pagination-total-pages header is absent, do NOT treat a short page (<50) alone as end-of-data — continue until an EMPTY page (and stop on empty page in all cases); (c) if x-pagination-result-count (or total-pages) is present, cross-check the accumulated count and error on mismatch. Tests: header-absent short-page continuation, cap-hit error, mismatch error. H2 (board.go:28-35, 82-88): cache self-refresh on the read path — on a 404 from Snapshot (stale view id) or MoveToBucket's POST (stale bucket id after delete+recreate), refresh views/buckets once and retry once; second failure returns the error. Tests for both. H3 (internal/config/config.go:179-247): validate db and work_root non-empty (validation-collected errors + tests). NOTE: another fixer may have recently touched internal/config (slug validation, commit message 'config: validate repo slug shape') — pull the latest state of the file and integrate cleanly. H4 (config.go:114-124, 300-334): env-expanded plain scalars must stay strings — after substituting ${VAR} in a plain (unquoted) scalar, force the node's tag/style to !!str so a value like "true"/"123"/"null" cannot re-type and spuriously fail KnownFields decode. Test with an unquoted ${VAR} expanding to "true". H5 (internal/store/store.go:371-373): add a short code comment on the timestamp columns noting RFC3339Nano TEXT does not sort lexicographically by instant (variable-width fraction) — any future ORDER BY on time columns must ORDER BY id/seq or normalize width. Comment only, no behavior change. Constraints: scope = internal/vikunja/, internal/config/, internal/store/ (comment only). Conventions: testify + culpa. Build/vet/test -race ONLY those three packages; never ./... . Commits: 'vikunja: harden snapshot pagination and cache refresh' + 'config: require db/work_root, pin env-expanded scalars to !!str'; hooks bypassed; retry on index.lock; no push; no .beads/. | H6 (from store validator, empirically quantified): concurrent store.New on a NONEXISTENT db file -> loser fails with SQLITE_BUSY ~57% (busy_timeout does not cover first-creation/WAL-conversion during connection setup; error surfaces via PingContext, wrapped at store.go:77). Steady-state (existing file) is fully clean (93,867 hammered ops, 0 errors). FIX in internal/store: bounded retry (e.g. up to ~2s with small backoff) around the open/ping/migrate sequence in store.New when the error chain matches SQLITE_BUSY, so daemon + ahub status can race first boot safely. Add a two-process fresh-file race test if cheaply arrangeable in-package (two goroutines with separate Stores on one fresh path is enough to reproduce per the validator). Also note in the package doc that busy_timeout excludes creation. | closed | 1 | task | NULL | NULL | 2026-07-13T00:41:22Z | Eugene Blikh | bigbes@gmail.com | 2026-07-13T01:31:07Z | 2026-07-13T01:31:07Z | NULL | 0 | NULL | NULL | NULL | 0 | 0 | 0 | �{} | landed 8f1c381/4709405/878857f: pagination never-partial guarantees, cache refresh-on-404, db/work_root validation, !!str pinning, sort-order comment, first-boot BUSY retry + migrate TOCTOU serialization (BEGIN IMMEDIATE + in-tx recheck) | 0 | NULL | NULL | NULL | 0 | 2026-07-13T01:05:54Z | 0 | ||||||||||||||||||||||
| ah-nyl.13 | 7cbeacf33cd82863a1322764cd5c5d84db98099b33682f72598735f7aeccfc46 | fix: attempt-evidence composition (phantom adopt) + meta wedge + signal escalation | Fix-verification review of the fix commits found a CRITICAL composition bug plus follow-ons. Findings below; line refs @ HEAD (80b565b). Scope: internal/ports (ONE additive field), internal/runner, cmd/ahub-run, internal/reconcile. CORE RULE (fixes G1+G4): a live zellij session is evidence for attempt K ONLY when .task/meta.json's attempt == K (meta is written by Start(K)). The session task-<N> outlives attempts (keep-pane shell after normal exit; create-background default pane), so session-alive alone is NOT evidence. G1 CRITICAL (reconcile.go:240-252 + runner.go:151-178): re-attempt after a finished run with the session alive: probe Status(task, latest+1) ignores old exit.json (attempt mismatch) and sees the alive session -> Running -> phantom-adopts a run row for attempt K+1 that was never Started -> "timeout" kill ~30min later. Also defeats the interrupted-finalize guard (reconcile.go:486: !st.Running false due to keep-pane shell) so a successful attempt can be republished as "attempt K+1 timeout". FIX in runner.Status: in the session-alive branch read meta.json; meta.attempt == queried attempt -> Running as today; meta.attempt != queried attempt (or meta absent) -> this session is NOT runtime for the queried attempt: report Completed=false, Running=false is not representable... implement as: expose the meta attempt in the status and let Running mean "session alive AND meta matches". Concretely: add field `MetaAttempt int` (0 = unknown/absent) to ports.RunStatus with a doc comment; populate it whenever meta.json is readable; Running=true ONLY when session alive && MetaAttempt == queried attempt; when session alive && MetaAttempt != attempt -> Running=false, Completed=false (a new legitimate "no evidence for this attempt" state — update the ports doc comment for Status accordingly: exactly one of Running/Completed OR neither when the live session belongs to a different attempt). Reconcile: runtimeEvidence stays (Running || Completed-non-crashed) — the neither-state naturally means "no evidence", so claim proceeds with a real Start. Verify the crash-window adopt still works (meta matches -> Running -> adopt). G4 MAJOR (reconcile.go:478-484 + runner.go:151-158): DB-loss adoption probes attempt 1 while the live runtime is attempt K>1 -> with G1's fix alone this becomes "no evidence" -> wrongly fails the card while pi K runs unsupervised. FIX in reconcile.adoptOrFail: when the probed attempt yields no evidence but Status reports a live session with MetaAttempt M > 0 and M != probed attempt, re-probe/adopt attempt M (adopt the ACTUAL in-flight attempt: create run row at attempt M, StartedAt=now). Regression test: nil latest run + live runtime with meta{attempt:3} -> adopts attempt 3, no kill, no fail. G3 MAJOR (runner.go:255-263, taskfiles.go:111-128): torn/corrupt meta.json permanently wedges Start (existingRun hard-errors every tick). FIX: write meta.json atomically (same tmp+fsync+rename helper as exit.json) AND treat unparseable meta.json as absent (warn + decline reuse) in both existingRun and the G1 Status path. Regression test: garbage meta.json -> Start proceeds fresh (after prune/reuse logic), Status doesn't error. G5 MINOR (cmd/ahub-run/main.go:127-136): child ignoring SIGTERM/SIGHUP -> ahub-run waits forever, session killed under it, token burn. FIX: after forwarding the signal, bounded wait (10s) then SIGKILL the child process group; still write exit.json+report. Test with a TERM-ignoring child script. G6 MINOR (reconcile.go:294-298): persistent Start failure (e.g. pi not on PATH) -> error event appended EVERY tick, unbounded, card stuck in ready. FIX: dedup — skip appending when the task's most recent event has identical kind+payload (cheap: track last event per task in-memory in the Reconciler); AND after 5 consecutive start failures for the same (task, attempt), move the card to failed with a comment (move-then-comment) so the operator sees it. Tests for both. G7 MINOR (reconcile.go:486-496): dragging an already-finalized card back to in_progress re-runs finalize with moveCard=true -> duplicate result comment per drag. FIX: fire the interrupted-finalize completion branch with moveCard=true only when rec.State == in_progress; for terminal rec.State just align per the §10 terminal rules. Test: drag in_review card to in_progress with dead session -> no duplicate comment, converges. Constraints: scope exactly internal/ports (RunStatus field + doc), internal/runner, cmd/ahub-run, internal/reconcile. Do NOT touch internal/vikunja (a sibling fixer works there concurrently), internal/store, internal/config, docs/, .beads/, go.mod. Conventions testify+culpa. Build/vet/test -race ONLY ./internal/ports/... ./internal/runner/... ./cmd/ahub-run/... ./internal/reconcile/... ; never ./... . Update the reconcile fakes so an unscripted (task,attempt) Status returns the realistic three-state contract (the zero-value RunStatus masked G1 — make fakes fail loudly on unscripted queries instead). Commits: 'runner: attempt-evidence via meta.json (+atomic meta, signal escalation)' + 'reconcile: adopt actual in-flight attempt, start-failure backoff, refinalize guard'; hooks bypassed; retry on index.lock; no push. | closed | 0 | bug | NULL | NULL | 2026-07-13T01:51:43Z | Eugene Blikh | bigbes@gmail.com | 2026-07-13T02:26:12Z | 2026-07-13T02:26:12Z | NULL | 0 | NULL | NULL | NULL | 0 | 0 | 0 | �{} | landed a0c8858+24cb927: meta-aware attempt evidence (three-state Status w/ MetaAttempt), atomic meta + lenient corrupt handling, adopt actual in-flight attempt, SIGKILL escalation, start-failure dedup+escalation, refinalize bounce-back; fakes panic on unscripted queries; six traces pinned | 0 | NULL | NULL | NULL | 0 | 2026-07-13T01:51:44Z | 0 | |||||||||||||||||||||||
| ah-nyl.14 | 0bb6c4289149cfb59e17de6593561acac7c3bbda385532f6b1c8e02a79121ba9 | fix: vikunja result-count header semantics (verify against source) | Fix-verification review flagged H1's cross-check as likely wrong (CRITICAL-risk): board.go:107-131 compares the ACCUMULATED task total against the last-seen x-pagination-result-count header. If Vikunja's header means "items in the CURRENT response" (per-page) — which is the documented/likely semantics — any board >1 page makes snapshotOnce error on EVERY iteration -> reconcile aborts every pass -> no supervision, no timeouts, daemon effectively stalls at >50 tasks. Also the empty terminal page's headers are checked BEFORE the empty-page break (board.go:104-113), which under per-page semantics errors the headerless path too. STEP 1 — establish the truth from Vikunja 2.3 SOURCE (do not guess): fetch the pagination handler from the upstream repo (github.com/go-vikunja/vikunja, tag v2.3.0 or close; the header is set in pkg/web/handler/ — search for "x-pagination-result-count"). Record the exact semantics (total vs per-page) with a file/line citation in a code comment. STEP 2 — fix accordingly. If per-page (expected): cross-check len(page tasks) vs the header PER RESPONSE (mismatch -> error), keep x-pagination-total-pages as the page-count terminator, keep empty-page as universal terminator, keep cap-hit -> error; move the empty-page break BEFORE any header cross-checks. If genuinely total: keep the accumulated check but STILL move the empty-page break first and add the missing decisive fixtures. Either way add: multi-page fixture WITH result-count headers on every page; empty-terminal-page-with-headers fixture; headerless multi-page fixture (already exists — keep). Constraints: scope = internal/vikunja/ ONLY (a sibling fixer works in runner/reconcile/ports concurrently). Conventions testify+culpa. Build/vet/test -race ./internal/vikunja/... only. Commit 'vikunja: fix result-count semantics per upstream source (<citation>)'; hooks bypassed; retry on index.lock; no push; no .beads/. | closed | 0 | bug | NULL | NULL | 2026-07-13T01:51:44Z | Eugene Blikh | bigbes@gmail.com | 2026-07-13T02:00:03Z | 2026-07-13T02:00:03Z | NULL | 0 | NULL | NULL | NULL | 0 | 0 | 0 | �{} | landed 7802095: per-response result-count check (semantics proven from vikunja v2.3.0 pkg/web/handler/read_all.go with line citations), empty-page break before header checks, decisive multi-page fixtures | 0 | NULL | NULL | NULL | 0 | 2026-07-13T01:51:45Z | 0 | |||||||||||||||||||||||
| ah-nyl.15 | 26d73898c2195a2502a4cc9764f09b4e985bd97a0e976a09926b5c4133e47e19 | fix: 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/. | closed | 0 | bug | NULL | NULL | 2026-07-13T02:43:36Z | Eugene Blikh | bigbes@gmail.com | 2026-07-13T03:01:04Z | 2026-07-13T03:01:04Z | NULL | 0 | NULL | NULL | NULL | 0 | 0 | 0 | �{} | landed 30292cb: adopt-guard via runtimeEvidence + same-tick finalize of hard completed evidence, neither-state timeout arm w/ shared timeoutKill, fixture hygiene; negative controls confirmed | 0 | NULL | NULL | NULL | 0 | 2026-07-13T02:43:37Z | 0 | |||||||||||||||||||||||
| ah-nyl.16 | 01a6ca2b6997fd15bcf53d2c2b93acfb1fced4d9dbb080e77d33e9711ddf4dd1 | fix: 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. | closed | 0 | bug | NULL | NULL | 2026-07-13T03:34:21Z | Eugene Blikh | bigbes@gmail.com | 2026-07-13T03:47:12Z | 2026-07-13T03:47:12Z | NULL | 0 | NULL | NULL | NULL | 0 | 0 | 0 | �{} | 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) | 0 | NULL | NULL | NULL | 0 | 2026-07-13T03:34:22Z | 0 | |||||||||||||||||||||||
| ah-nyl.2 | 160899e1969f48c186279e62ca24ed7aa7f362fd1b56dc90ed2247c4cf660322 | store: SQLite implementation of ports.Store | Implement 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. | closed | 1 | task | NULL | NULL | 2026-07-12T23:34:33Z | Eugene Blikh | bigbes@gmail.com | 2026-07-13T00:10:13Z | 2026-07-13T00:10:13Z | NULL | 0 | NULL | NULL | NULL | 0 | 0 | 0 | �{} | landed e3749f1, green | 0 | NULL | NULL | NULL | 0 | 2026-07-12T23:55:04Z | 0 | |||||||||||||||||||||||
| ah-nyl.3 | c5a4d9c93b65bce4f1b5dea36aa896d1dec99ba8a4a1c9579ace421cbb9f8d21 | vikunja: Board adapter over the REST API | Implement 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. | closed | 1 | task | NULL | NULL | 2026-07-12T23:34:34Z | Eugene Blikh | bigbes@gmail.com | 2026-07-13T00:10:14Z | 2026-07-13T00:10:14Z | NULL | 0 | NULL | NULL | NULL | 0 | 0 | 0 | �{} | landed a1d6038, green; OpenAPI deviation from SPEC noted in package docs (flat paginated view tasks) | 0 | NULL | NULL | NULL | 0 | 2026-07-12T23:55:04Z | 0 | |||||||||||||||||||||||
| ah-nyl.4 | ff6bd742ed8006517b016443e69e35e99531123b2198633480125843c1ee2f40 | runner: pi+zellij implementation + ahub-run supervisor | Implement 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. | closed | 1 | task | NULL | NULL | 2026-07-12T23:35:16Z | Eugene Blikh | bigbes@gmail.com | 2026-07-13T00:20:36Z | 2026-07-13T00:20:36Z | NULL | 0 | NULL | NULL | NULL | 0 | 0 | 0 | �{} | 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.json | 0 | NULL | NULL | NULL | 0 | 2026-07-12T23:55:04Z | 0 | |||||||||||||||||||||||
| ah-nyl.5 | a229e6751c3b6c7e11fca843ebea6db9830e910a9d3b730b6df4481690de2551 | reconcile: the control loop | Implement 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. | closed | 1 | task | NULL | NULL | 2026-07-12T23:35:17Z | Eugene Blikh | bigbes@gmail.com | 2026-07-13T00:15:01Z | 2026-07-13T00:15:01Z | NULL | 0 | NULL | NULL | NULL | 0 | 0 | 0 | �{} | 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) | 0 | NULL | NULL | NULL | 0 | 2026-07-12T23:55:05Z | 0 | |||||||||||||||||||||||
| ah-nyl.6 | 0f3627c5d82d75a475ee9648fba27c211ab6ca3b93bbb47bbb18b50ec3c92db4 | wiring: httpapi + agenthubd + ahub CLIs | Wire 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. | closed | 1 | task | NULL | NULL | 2026-07-12T23:35:55Z | Eugene Blikh | bigbes@gmail.com | 2026-07-13T03:18:09Z | 2026-07-13T03:18:09Z | NULL | 0 | NULL | NULL | NULL | 0 | 0 | 0 | �{} | 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 done | 0 | NULL | NULL | NULL | 0 | 2026-07-13T03:01:57Z | 0 | ||||||||||||||||||||||
| ah-nyl.7 | 7c266e5f2bb15c0bb076a169f5f91846b9e15c8d4397026b55d441ea8638463d | e2e: harness with fake vikunja + stub pi | Build 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. | closed | 2 | task | NULL | NULL | 2026-07-12T23:35:56Z | Eugene Blikh | bigbes@gmail.com | 2026-07-13T03:40:07Z | 2026-07-13T03:40:07Z | NULL | 0 | NULL | NULL | NULL | 0 | 0 | 0 | �{} | 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 found | 0 | NULL | NULL | NULL | 0 | 2026-07-13T03:18:19Z | 0 | ||||||||||||||||||||||
| ah-nyl.8 | 56c1d153e12c3a78d437729c157cf9955e07e9f7fdb9f846e61d0984e74e1941 | refactor: adopt go.bigb.es/auxilia (scribe/culpa/steward) where it pays | Post-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. | closed | 3 | chore | NULL | NULL | 2026-07-12T23:35:57Z | Eugene Blikh | bigbes@gmail.com | 2026-07-13T00:09:34Z | 2026-07-13T00:09:34Z | NULL | 0 | NULL | NULL | NULL | 0 | 0 | 0 | �{} | superseded: auxilia + testify adopted as baseline conventions before wave 2 (user directive); refit of landed packages tracked in a dedicated bead | 0 | NULL | NULL | NULL | 0 | NULL | 0 | |||||||||||||||||||||||
| ah-nyl.9 | b1c7f86f50a988844039d6deb7d3411e2391e037cc653193d484a51aec520898 | refit: testify + culpa in domain/config/store/vikunja | The 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. | closed | 1 | task | NULL | NULL | 2026-07-13T00:09:36Z | Eugene Blikh | bigbes@gmail.com | 2026-07-13T00:26:34Z | 2026-07-13T00:26:34Z | NULL | 0 | NULL | NULL | NULL | 0 | 0 | 0 | �{} | landed b86928a: testify+culpa across domain/config/store/vikunja, 30 top-level + 37 subtests preserved, errors.Is/As behavior verified | 0 | NULL | NULL | NULL | 0 | 2026-07-13T00:09:36Z | 0 | |||||||||||||||||||||||
| ah-oeq | 4c7fd7ca7b85255c43283cb38978e25fe359f7f32e03dd3033ee0860fe52a5bf | Stage 5: VM deploy, web terminal, virtual keys, spec editor, MCP facade | Per 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. | open | 4 | epic | NULL | NULL | 2026-07-12T23:36:30Z | Eugene Blikh | bigbes@gmail.com | 2026-07-12T23:36:30Z | NULL | NULL | 0 | NULL | NULL | NULL | 0 | 0 | 0 | �{} | 0 | NULL | NULL | NULL | 0 | NULL | 0 | ||||||||||||||||||||||||
| ah-tc2 | 7d54ef700ae480aa00a8fdd268e4a073722f55c14bd0fdba7404db32ee366a57 | agenthubd 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. | closed | 3 | task | NULL | NULL | 2026-07-17T21:29:25Z | Eugene Blikh | bigbes@gmail.com | 2026-07-18T04:51:38Z | 2026-07-18T04:51:38Z | NULL | 0 | NULL | NULL | NULL | 0 | 0 | 0 | �{} | 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. | 0 | NULL | NULL | NULL | 0 | NULL | 0 | |||||||||||||||||||||||
| ah-tz0 | 4edd2663acbacfce6ea3669699809f3e06dfafddd847485f1f5f0ca6072a0285 | Surface agent closing summary / review verdict in the In Review comment | domain.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. | closed | 3 | feature | Eugene Blikh | NULL | 2026-07-18T06:30:21Z | Eugene Blikh | bigbes@gmail.com | 2026-07-18T06:46:05Z | 2026-07-18T06:46:05Z | NULL | 0 | NULL | NULL | NULL | 0 | 0 | 0 | �{} | 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. | 0 | NULL | NULL | NULL | 0 | 2026-07-18T06:38:47Z | 0 | |||||||||||||||||||||||
| ah-wka | 61844a89efb3d2f3b72967bbcc6f9e305d2ee6cc40146aae71081887bb5527a5 | runner: 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 green | closed | 2 | bug | Eugene Blikh | NULL | 2026-07-13T08:37:59Z | Eugene Blikh | bigbes@gmail.com | 2026-07-13T09:26:45Z | 2026-07-13T09:26:45Z | NULL | 0 | NULL | NULL | NULL | 0 | 0 | 0 | �{} | 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 unreachable | 0 | NULL | NULL | NULL | 0 | 2026-07-13T08:46:20Z | 0 | ||||||||||||||||||||||
| ah-xuc | 19aa9e472e32b714a672302d4a5b9431d5154fa20c3d0f98ef188f68d40ea1b7 | Stage 2: webhooks, Task Spec, full buckets, Telegram, srht push, watchdog | Per 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. | closed | 3 | epic | NULL | NULL | 2026-07-12T23:36:28Z | Eugene Blikh | bigbes@gmail.com | 2026-07-13T08:38:18Z | 2026-07-13T08:38:18Z | NULL | 0 | NULL | NULL | NULL | 0 | 0 | 0 | �{} | 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. | 0 | NULL | NULL | NULL | 0 | NULL | 0 | ||||||||||||||||||||||
| ah-xuc.1 | a152c1dd582b5477ff993f9f0c070eea86bf5d7994ae57afd24f3cbf8ac4394f | Stage 2 foundation: domain buckets/outcome, ports contracts, config surface | Every 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 staged | Coordinator 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. | closed | 2 | task | Eugene Blikh | NULL | 2026-07-13T05:14:51Z | Eugene Blikh | bigbes@gmail.com | 2026-07-13T05:44:24Z | 2026-07-13T05:44:24Z | NULL | 0 | NULL | NULL | NULL | 0 | 0 | 0 | �{} | 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) | 0 | NULL | NULL | NULL | 0 | 2026-07-13T05:19:13Z | 0 | |||||||||||||||||||||
| ah-xuc.10 | 93e494bccda4f59baacc19a0e3b326af7c363fd21532a445b37b51577c6c63e9 | cmd/agenthubd: wire webhook secret and Telegram notifier | Wiring 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 clean | Coordinator refinement: also wire the runner as ports.Publisher into reconcile.Deps. | closed | 2 | task | Eugene Blikh | NULL | 2026-07-13T05:16:18Z | Eugene Blikh | bigbes@gmail.com | 2026-07-13T07:53:56Z | 2026-07-13T07:53:56Z | NULL | 0 | NULL | NULL | NULL | 0 | 0 | 0 | �{} | 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/O | 0 | NULL | NULL | NULL | 0 | 2026-07-13T07:35:21Z | 0 | |||||||||||||||||||||
| ah-xuc.11 | b6a041aecb730cbf5ad794b8bba2833ccb247eeb242441fa1b256c4a6130e930 | e2e: Stage 2 flows — webhook poke, Triage bounce, skills, publish, watchdog, Telegram | Extend 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 internals | closed | 2 | task | Eugene Blikh | NULL | 2026-07-13T05:16:24Z | Eugene Blikh | bigbes@gmail.com | 2026-07-13T08:38:17Z | 2026-07-13T08:38:17Z | NULL | 0 | NULL | NULL | NULL | 0 | 0 | 0 | �{} | 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) | 0 | NULL | NULL | NULL | 0 | 2026-07-13T07:53:57Z | 0 | ||||||||||||||||||||||
| ah-xuc.12 | 14d3638d45c9064a76941832983973f494233fccabe6a738101abdf7f06e6d89 | docs: sync SPEC with Stage 2 behavior | SPEC.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 accurate | closed | 2 | task | Eugene Blikh | NULL | 2026-07-13T05:16:35Z | Eugene Blikh | bigbes@gmail.com | 2026-07-13T08:38:17Z | 2026-07-13T08:38:17Z | NULL | 0 | NULL | NULL | NULL | 0 | 0 | 0 | �{} | 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-6u0 | 0 | NULL | NULL | NULL | 0 | 2026-07-13T07:53:57Z | 0 | ||||||||||||||||||||||
| ah-xuc.2 | fa1ad68d45c1cb503c73e2f25b2c07769c319f1d2adc6eefbb30f03d70297a4a | internal/spec: Task Spec frontmatter parse, type-label defaults, validation | New 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 delimiter | closed | 2 | feature | Eugene Blikh | NULL | 2026-07-13T05:15:08Z | Eugene Blikh | bigbes@gmail.com | 2026-07-13T06:22:47Z | 2026-07-13T06:22:47Z | NULL | 0 | NULL | NULL | NULL | 0 | 0 | 0 | �{} | d714741 + eb5a4be (review-gap pinning); hostile adversarial review merge-ready (yaml alias bombs bounded, path traversal blocked, error taxonomy sound) | 0 | NULL | NULL | NULL | 0 | 2026-07-13T05:44:24Z | 0 | ||||||||||||||||||||||
| ah-xuc.3 | 130d6553f11713918d8f32dca70661607918c8ae7bed880ea98a1b417dca5b30 | vikunja: fetch task labels into BoardTask.Labels | Stage 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 tasks | closed | 2 | task | Eugene Blikh | NULL | 2026-07-13T05:15:10Z | Eugene Blikh | bigbes@gmail.com | 2026-07-13T06:22:48Z | 2026-07-13T06:22:48Z | NULL | 0 | NULL | NULL | NULL | 0 | 0 | 0 | �{} | 1e9fc7e; wire shape pinned against upstream go-vikunja v2.3.0; review clean | 0 | NULL | NULL | NULL | 0 | 2026-07-13T05:44:24Z | 0 | ||||||||||||||||||||||
| ah-xuc.4 | 33d26ffac04170d2d8e34c39c29e316a661775ceecd8aa8f20c5d11929a7589d | store: migration v2 — per-run timeout column | The 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 passing | closed | 2 | task | Eugene Blikh | NULL | 2026-07-13T05:15:22Z | Eugene Blikh | bigbes@gmail.com | 2026-07-13T06:22:49Z | 2026-07-13T06:22:49Z | NULL | 0 | NULL | NULL | NULL | 0 | 0 | 0 | �{} | c04f7d7; step-list migrator preserves all race properties (verified under -race); review clean | 0 | NULL | NULL | NULL | 0 | 2026-07-13T05:44:25Z | 0 |