eefkdm0u6ipbhk8k092drg2n0gsdpkl9 · 69 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. | closed | 3 | task | Eugene Blikh | NULL | 2026-07-18T06:30:22Z | Eugene Blikh | bigbes@gmail.com | 2026-07-18T14:48:24Z | 2026-07-18T14:48:24Z | NULL | 0 | NULL | NULL | NULL | 0 | 0 | 0 | �{} | Merged 3e2eb3c: Done/Cancelled cards get .task/ + uncommitted files archived to archive_dir/task-<id>.tar.gz (stdlib tar+gzip, atomic overwrite, archive-before-remove), then git worktree remove --force; branch kept; worktree_archived/worktree_gone events. Live verify (drag a Done card, check archive) after next CI deploy. | 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. | closed | 3 | epic | NULL | NULL | 2026-07-12T23:36:29Z | Eugene Blikh | bigbes@gmail.com | 2026-07-18T17:03:58Z | 2026-07-18T17:03:58Z | NULL | 0 | NULL | NULL | NULL | 0 | 0 | 0 | �{} | All 6 children done: file-channel tools (.1-.4), AgentsView pipeline + deep links (.5), HTTP tools API (.6). Stage 4 complete pending live rollout smoke. | 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. | closed | 2 | feature | NULL | NULL | 2026-07-18T13:23:54Z | Eugene Blikh | bigbes@gmail.com | 2026-07-18T14:48:24Z | 2026-07-18T14:48:24Z | NULL | 0 | NULL | NULL | NULL | 0 | 0 | 0 | �{} | Both halves done: ops (agent-1 fleet-node daemon + phoebe collector, sessions live in central pg since 2026-07-18) and daemon (a8e7586: config-gated agentsview{base_url,machine}, session deep-link line on success/routed/failure comments, URL pinned agent-1~pi:<id>). Live link render check after next CI deploy + config stage. | 0 | NULL | NULL | NULL | 0 | NULL | 0 | |||||||||||||||||||||
| 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. | closed | 3 | feature | Eugene Blikh | NULL | 2026-07-18T13:23:55Z | Eugene Blikh | bigbes@gmail.com | 2026-07-18T17:03:57Z | 2026-07-18T17:03:57Z | NULL | 0 | NULL | NULL | NULL | 0 | 0 | 0 | �{} | Merged 07bd9d4+03f4496: per-task 256-bit bearer tokens (.task/tool-auth.json, constant-time check, invalidated at finalize, in-memory registry), /api/tool/{task-create,ask-user,answer} reusing file-channel policy code, ahub tool CLI. Additive — file channels intact. Live smoke list in agent report; note stall_timeout must exceed ask-user poll deadline. | 0 | NULL | NULL | NULL | 0 | 2026-07-18T16:22:45Z | 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-25e | 81a248a73cc547b68a28816e4f3c67ea4f5ca77bf4c95f40233a17b35b901880 | Yonote publish lane: .task/publish.json → bot-authored docs at finalize | Why: the operator wants agents to publish specs/reports/answers INTO Yonote instead of committing them to the project repo — with the write credential never entering the agent's environment. What: a third finalize-time file channel in the SPEC §14.4 family (tasks.json, question.json): the agent writes markdown under .task/publish/ and declares it in .task/publish.json {documents:[{title, file, collection_id?, parent_url?}]}; on successful finalize the daemon creates the docs via the internal/yonote client under the BOT token (publish:true, target = yonote.publish_collection_id or per-entry override, optional nesting under a referenced doc), comments the resulting URLs on the card, emits a yonote_published event. Publish failures never change the run outcome. wave 2.5 — implement after feat/wave2-archive-links merges. Depends on ah-gxa (client: CreateDocument/DocumentInfo) and ah-2lh (yonote config block + prepareTaskDir channel-clearing site). | # Yonote publish lane: .task/publish.json → bot-authored docs at finalize Agents publish specs/reports/answers INTO Yonote instead of committing them to the project repo. The agent declares intent via a file channel; the daemon executes with ITS bot token at finalize and links results on the card. ## Why file-channel, not direct API access from the agent (DECISION) The agent must never hold the Yonote token: worktrees run arbitrary generated code; `.task/` is archived to tar.gz at Done (a token in env or files would fossilize into archives); per-run env plumbing would put a write-capable credential into every zellij session. The daemon already owns a finalize-time file-channel protocol with exactly the right semantics — `.task/tasks.json` (delegation) and `.task/question.json` (ask-user), SPEC §14.4: agent writes declarative intent, daemon acts with its own credentials, results land as card comments. Publishing is the third channel of that family. Attribution lands on the BOT user — uniform and auditable. ## Channel format `.task/publish.json` (attempt-scoped; cleared in prepareTaskDir alongside summary/tasks/question): {"documents": [ {"title": "Design: frobnicator", "file": "publish/design.md", "collection_id": "", "parent_url": ""} ]} - title: required, non-empty, ≤255 chars (Yonote bot/user name limits suggest 255 conventions). - file: required; RELATIVE path resolved under `.task/` (convention: agents put content in `.task/publish/<name>.md`). Guard rigor as repo-slug F8: reject absolute paths, filepath.Clean must stay under .task/, and the opened file must not escape via symlink (os.Root / EvalSymlinks containment check). - collection_id: optional uuid overriding the config default target. - parent_url: optional Yonote doc URL → resolve via yonote.ParseDocRefs(single) + DocumentInfo → parentDocumentId, so a report can nest under the spec it answers (documents.create parentDocumentId is v1-spec'd; child creation itself NOT live-verified — verify in tests against httptest only, live during rollout). - Caps: maxPublishDocs = 3 per run; maxPublishBytes = 512 KiB per file. No server-side text limit was verified — cap defensively; an oversize entry FAILS (listed in the comment), never truncated. Read at SUCCESSFUL finalize only, same site and semantics as readTasksFile: absent → nil (common case); present-but-unparseable → same bounce treatment as a malformed tasks.json (do not silently drop agent intent). ## Config The `yonote` block (introduced by the materialization bead) gains: publish_collection_id: "…" # uuid; the default target collection Validation: when set, must be a UUID shape; lane is ACTIVE only when the yonote client is configured AND publish_collection_id is set. When the client is configured but the collection is not, publish.json entries all fail visibly with "publishing not configured" in the card comment — never silently ignored. Operational prerequisite (document next to the config): the BOT must have read_write on the target collection (collections.add_user — client-bead runbook step 3). ## Daemon flow (finalize, success path, after summary/tasks/question handling) Per entry (≤3, in file order): resolve target collection (override or default) → resolve parent_url if set (its failure fails ONLY that entry) → read + cap content → CreateDocument{Title, CollectionID, ParentDocumentID, Text: content, Publish: true} → record {Title, absURL = cfg base_url + doc.URL} or {Title, err}. - Card comment (goes through the existing markdownToHTML comment path): published to Yonote: - <Title> → <abs url> - <Title> → FAILED: <message> - appendEvent "yonote_published" {"ok": n, "failed": m}. - Publish failures NEVER change the run outcome — the run already succeeded; publication is a side effect. Operator retries by re-running, or publishes manually from the archived tar.gz (the content survives there by construction). - Idempotency (DECISION): re-running a card re-publishes and creates a NEW doc. Accepted for v1 — re-runs are rare, docs are cheap, and dedup would need a doc-id echo channel; revisit only if it annoys in practice (then: UpdateDocument by echoed id). ## Agent contract (role-prompt snippet, config-side; document in deploy notes) To publish a document to Yonote: write markdown to .task/publish/<name>.md and declare it in .task/publish.json as {"documents":[{"title":"…","file":"publish/<name>.md"}]}. Publication happens AFTER your run succeeds; resulting links are posted on the task card. ## Tests - reader: absent → nil; malformed → tasks.json-parity bounce; >3 entries; empty title; path guards (absolute, .., symlink escape); oversize file. - finalize with fake yonote writer (interface seam: CreateDocument + DocumentInfo): 2 ok + 1 fail → comment body lines, event payload, run outcome unchanged; lane-off (no collection id) → "publishing not configured" per entry; parent_url resolve failure isolates the entry; collection_id override honored. - prepareTaskDir clears publish.json + the publish/ dir between attempts. wave 2.5 — implement after feat/wave2-archive-links merges. Depends on: internal/yonote client bead (CreateDocument/DocumentInfo), materialization bead (yonote config block + prepareTaskDir channel-clearing site). | ROLLOUT LIVE-SMOKE CHECKLIST (from wave-2.5 implementer, against bigbes.yonote.ru after deploy): 1) mint bot per ah-gxa runbook (POST /api/v2/bots + token, collections.add_user grant — grant call is spec-only, NOT live-verified), AuthInfo under bot token must show IsBot=true (daemon WARNs at startup otherwise); 2) documents.create with parentDocumentId (child nesting unverified live) + bot WRITE rights on publish_collection_id + publish:true attribution to bot; 3) archived-doc export ((archived) marker path); 4) ExportMarkdown empty-string cases (API-created never-edited draft, database-type doc) fall back to .text; 5) comments.create/list/resolve field names (only Q&A bead ah-ptu uses them). SEMANTICS NOTE: publish fires on ROUTED successful runs too (inside firstFinalize&&success before maybeRoute) — a design stage with publish.json publishes before routing to review; intentional. publish_collection_id (UUID) must be chosen by operator at config staging. DEPENDS ON → ✓ ah-2lh: Claim-time Yonote artifact materialization into .task/artifacts/ + prompt manifest ● P2 → ✓ ah-gxa: internal/yonote: light API client (doc resolve/export, create, comments; bot-token auth) ● P2 BLOCKS ← ○ ah-ptu: Yonote Q&A bot loop: poll doc comments, answer via board tasks (post-wave-2.5) ● P3 ROLLOUT FACTS (2026-07-18, live-verified): bot agenthub id=34fbc9a3-ec75-4722-a01f-805db3f3ff1b (v2 create needs username field — used bigbes@gmail.com); token agent-1-daemon installed as YONOTE_TOKEN in /etc/agent-hub/env; auth.info isBot=true CONFIRMED; collections.add_user CONFIRMED; Agents collection 8656642e-0297-4d69-b6fb-4246517a015b = publish_collection_id, bot read_write. CAVEAT: bot sees ALL team-default-readable collections (not grant-scoped) — operator may tighten per-collection perms in UI. Config staging after CI deploy: yonote{base_url: https://bigbes.yonote.ru, token: ${YONOTE_TOKEN}, publish_collection_id: 8656642e-0297-4d69-b6fb-4246517a015b}. | closed | 2 | feature | Eugene Blikh | NULL | 2026-07-18T15:04:25Z | Eugene Blikh | bigbes@gmail.com | 2026-07-18T16:09:01Z | 2026-07-18T15:59:08Z | NULL | 0 | NULL | NULL | NULL | 0 | 0 | 0 | �{} | Merged to master f9a7b4b (3 commits 6c9e259/a25b692/f9a7b4b): yonote client + claim-time artifact materialization + publish lane. Live smoke at rollout per ah-25e notes. | 0 | NULL | NULL | NULL | 0 | 2026-07-18T15:06:57Z | 0 | |||||||||||||||||||||
| ah-2ef | ff98b88593d96664a9626ff5ba7750acc64df6bde5526cca7d05a55ecc89e4aa | e2e suite rare timing flake (1 FAIL in ~5 runs observed 2026-07-18) | During wave-4 verification one 'go test -tags e2e ./e2e' run printed FAIL while 4 adjacent runs passed (12.6-13.3s). Failing subtest name was not captured (output was filtered). The suite has timing-sensitive subtests (500ms-deadline timeout path, stall watchdog). Next occurrence: rerun with -v, capture the failing subtest, then either widen its deadline margins or de-flake the poll. Not tied to wave-4 changes (harness timing predates them). | closed | 3 | bug | Eugene Blikh | NULL | 2026-07-18T17:03:58Z | Eugene Blikh | bigbes@gmail.com | 2026-07-18T18:15:54Z | 2026-07-18T18:15:54Z | NULL | 0 | NULL | NULL | NULL | 0 | 0 | 0 | �{} | Merged (fix/runner, reworded from 5f70d17): Status() read events.jsonl before exit.json confirmed completion; a poll straddling a fast exit froze empty PiSessionID/CostUSD into the immutable run row. Both applyExit sites now re-parse the confirmed-complete stream. Reproduced under stress with instrumentation; 26/26 clean unstressed + 13 race runs clean; e2e x3 green at merge. | 0 | NULL | NULL | NULL | 0 | 2026-07-18T17:28:20Z | 0 | |||||||||||||||||||||||
| ah-2lh | 6dae3f01f06b06dcd0357975dff946bac3138df4a0a351273573d592977ecb6e | Claim-time Yonote artifact materialization into .task/artifacts/ + prompt manifest | Why: the operator authors specs/big documents in Yonote and wants task cards to reference them by URL; the claimed agent must see the CURRENT doc content without the repo ever carrying it. What: optional yonote config block (base_url, token via ${YONOTE_TOKEN} from /etc/agent-hub/env, claim_budget); at claim, scan the frontmatter-stripped description body for https://<yonote-host>/doc/<seg> URLs (works on both raw-markdown and Vikunja TipTap-HTML descriptions), resolve via documents.info, export markdown (with .text fallback), write .task/artifacts/<NN>-<slug>.md via StartSpec.Artifacts in prepareTaskDir, append a '## Reference documents' manifest to the prompt, add an artifacts count to the claim comment + an event. Dead references NEVER fail the claim; every attempt re-exports fresh. wave 2.5 — implement after feat/wave2-archive-links merges. Depends on ah-gxa (client). | # Claim-time Yonote artifact materialization Operator authors specs/big docs in Yonote; a task card references them by URL; at claim the daemon exports each referenced doc to `.task/artifacts/<NN>-<slug>.md` in the worktree and lists them in the prompt. `.task/` is git-excluded (excludeTaskDir) and archived to tar.gz at Done, so artifacts never touch the repo. ## Config (internal/config) New OPTIONAL top-level block, feature fully inert when absent (telegram/ntfy presence pattern): yonote: base_url: "https://bigbes.yonote.ru" # required when present; validateHTTPURL token: "${YONOTE_TOKEN}" # required when present; BOT token (see client bead runbook); ${VAR} from /etc/agent-hub/env via existing expandEnv claim_budget: "90s" # optional; TOTAL wall clock for all exports in one claim; default 90s; positive (parseDuration) `Yonote{BaseURL, Token string; ClaimBudget time.Duration}` + `present()` (trimmed BaseURL or Token non-empty) + validation (both required when present; URL check; strict KnownFields comes free from rawConfig). Trim trailing "/" off BaseURL at resolve (AgentsView precedent). Wiring (internal/deps or cmd wiring, wherever mem0/vikunja clients are built): when present → `yonote.New(BaseURL, Token)`; call AuthInfo once at startup: log identity, WARN if !IsBot; a startup AuthInfo FAILURE logs an error and continues — Yonote outage must never block board work. ## Reference convention (DECISION) Every substring of the card description matching `https?://<host-of-base_url>/doc/<seg>` is a reference; `<seg>` = last path segment `[A-Za-z0-9._~-]+` (query/fragment excluded by charset). Extraction runs over the frontmatter-STRIPPED body (`res.Body` from spec.Resolve — the same text the prompt template receives). Order of first occurrence; dedup by RESOLVED document id (two URL forms of one doc collapse); cap `maxArtifactRefs = 10` (const, not config) — refs past the cap get manifest lines "skipped: over per-task artifact cap". Why bare-URL matching, not an `artifact:` prefix line: (a) descriptions reach the daemon in TWO shapes — raw markdown on daemon-created child cards, and TipTap HTML (`<p>…<a href="URL">text</a></p>`) on operator-edited cards; internal/vikunja passes Description verbatim (board.go), nothing normalizes HTML — a line-anchored convention breaks under HTML rewrapping, host+path substring matching survives both; (b) zero ceremony: paste a link into the card and it just works; (c) false positives are benign — a linked Yonote doc IS relevant context by definition. `/collection/...` and `/share/...` URLs deliberately NOT matched in v1. Resolution: pass `<seg>` verbatim to documents.info (accepts uuid | urlId | slug-urlId — live-verified) → canonical uuid, title, archivedAt. ## Claim flow (internal/reconcile claim(), after renderPrompt succeeds, before runner.Start) refs := yonote.ParseDocRefs(cfg.Yonote.BaseURL, res.Body) // nil client or 0 refs → unchanged behavior ctx2 := context.WithTimeout(ctx, cfg.Yonote.ClaimBudget) // one budget for ALL refs; reconcile loop is single-goroutine (PublishTimeout precedent) serially per ref: doc, err := DocumentInfo(ctx2, ref) // err (404 deleted, 401, timeout…) → unavailable(reason), continue md, err := ExportMarkdown(ctx2, doc.ID) // MUST use doc.ID (uuid) — urlId 400s if err (incl. whiteboard 500) → unavailable(reason) if md == "" → md = doc.Text // API-created never-edited docs keep markdown in .text; database docs are "" both ways if md still "" → unavailable("document exported empty") marker "(archived)" when doc.ArchivedAt != nil — still materialized (archived docs stay readable) Artifact{FileName: fmt.Sprintf("%02d-%s.md", n, slug(doc.Title)), Content: md, Title: doc.Title, SourceURL: cfg.Yonote.BaseURL + doc.URL} budget exhausted → remaining refs unavailable("artifact budget exhausted") slug(): lowercase; non-[a-z0-9] runs → "-"; trim "-"; cap 60 chars; empty → doc.URLID. The NN- ordinal prefix makes collisions impossible. Cyrillic titles will slug to "" often → URLID fallback matters. FAILURE SEMANTICS (spec-style): NOTHING in materialization ever fails or bounces the claim. A dead / archived / empty / oversized-budget reference degrades to a manifest warning line + a claim- comment count + a warn log. Rationale: the doc is context, not a precondition; the operator sees the warning immediately (prompt + card comment) and can fix the link and re-run. No triage bounce (spec is not malformed), no failed state (nothing ran). ## Prompt manifest Reconciler APPENDS to the rendered prompt (role templates untouched; keeps SPEC §12 rendering contract intact): ## Reference documents (.task/artifacts/) Exported from Yonote at claim time; read them before starting; treat as read-only input. 1. "<Title>" — .task/artifacts/01-<slug>.md (source: <abs url>) 2. "<Title>" (archived) — .task/artifacts/02-<slug>.md (source: <abs url>) 3. "<Title or ref>" — UNAVAILABLE (<reason>) (source: <abs url>) ## Ports + runner - ports.StartSpec gains `Artifacts []Artifact`; `type Artifact struct{ FileName, Content, Title, SourceURL string }`. - runner prepareTaskDir: ALWAYS `os.RemoveAll(.task/artifacts)` first (attempt-scoped channel — same rationale as clearing summary/tasks/question: attempt N-1 exports must not leak into attempt N). Then when len(Artifacts)>0: MkdirAll + writeFileAtomic each. Guard: reject FileName containing "/" or ".." (defense in depth; daemon generates them). - RE-CLAIM SEMANTICS (DECISION): every attempt RE-EXPORTS fresh content. Freshness wins over snapshot stability because the whole point is "the current spec", specs get edited between attempts, and each finished attempt's snapshot is already preserved by the Done-archive tar.gz. A doc that died between attempts becomes an UNAVAILABLE manifest line (stale file removed by the RemoveAll), so the agent never reads outdated content silently. ## Observability - Claim comment (existing "attempt N started · …" line) gains, only when refs were found: `· artifacts: N exported[, M unavailable]`. - appendEvent "artifacts" {"exported": N, "unavailable": M} (deduped like existing events). ## Interface seam reconcile depends on a narrow local interface (fake-friendly, matches existing port style): type yonoteExporter interface { DocumentInfo(ctx context.Context, id string) (*yonote.Document, error) ExportMarkdown(ctx context.Context, uuid string) (string, error) } nil = feature off. *yonote.Client satisfies it. ## Tests - config: block absent (inert) / present-partial (errors join) / bad URL / ${VAR} unset problem / budget default + non-positive rejection (mirror telegram/ntfy config tests). - reconcile claim with fake exporter: 2-ref happy path (manifest text, StartSpec.Artifacts, comment suffix, event payload); 404 ref → UNAVAILABLE + claim proceeds; markdown "" → Text fallback; both empty → unavailable; budget timeout → remaining skipped; dedup two URL forms of one doc; TipTap-HTML description with <a href=...>; cap at 10; archived marker; nil exporter → byte-identical prompt to today. - runner: artifacts written under .task/artifacts/; stale dir removed when Artifacts empty; traversal FileName rejected. wave 2.5 — implement after feat/wave2-archive-links merges. Depends on the internal/yonote client bead. | Code-recon evidence: (1) descriptions reach the daemon VERBATIM — internal/vikunja/board.go:110 copies wire Description straight into ports.BoardTask; there is NO html→markdown normalization on read (markdownToHTML in internal/vikunja/markdown.go is write-side, comments only) — so operator-edited cards arrive as TipTap HTML while daemon-created child cards (reconcile.go childDescription) are raw markdown: the URL scanner must handle both, which is why the convention is host+path substring matching, not an 'artifact:' line. (2) Hook point: internal/reconcile/reconcile.go claim(), after renderPrompt success (~line 362-404) and before runner.Start; prompt is plain string concatenation — role templates (SPEC §12) stay untouched. (3) File writing: internal/runner/runner.go prepareTaskDir (~591-633) already clears attempt-scoped channels (summary/tasks/question) — artifacts dir clearing joins that list; writeFileAtomic + excludeTaskDir + Done-archive tar.gz already cover durability/git-exclusion/archival (SPEC §9). (4) API evidence for the fallback chain and failure modes lives in ah-gxa notes: /markdown is uuid-only, returns '' for API-created-never-edited drafts and database docs, 500s for whiteboards, and .text is plaintext after editor edits — hence markdown → .text → UNAVAILABLE. (5) claim runs on the single reconcile goroutine — claim_budget bounds total materialization wall-clock (PublishTimeout precedent, config.go). (6) documents.info of an ARCHIVED doc was NOT live-verifiable (workspace has no archived docs); Outline heritage says it returns the doc with archivedAt set — treated as exportable + '(archived)' marker; implementer verifies by archiving a scratch doc in the UI once. Nested docs: no doc with childrenCount>0 exists in the workspace; /markdown has no children param (single-doc export is spec-consistent) — v1 semantics = SINGLE doc per reference, children never walked (documents.list {parentDocumentId} exists if a future version wants a tree). | closed | 2 | feature | Eugene Blikh | NULL | 2026-07-18T15:04:08Z | Eugene Blikh | bigbes@gmail.com | 2026-07-18T15:59:08Z | 2026-07-18T15:59:08Z | NULL | 0 | NULL | NULL | NULL | 0 | 0 | 0 | �{} | Merged to master f9a7b4b (3 commits 6c9e259/a25b692/f9a7b4b): yonote client + claim-time artifact materialization + publish lane. Live smoke at rollout per ah-25e notes. | 0 | NULL | NULL | NULL | 0 | 2026-07-18T15:06:57Z | 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. | closed | 3 | feature | Eugene Blikh | NULL | 2026-07-18T06:30:21Z | Eugene Blikh | bigbes@gmail.com | 2026-07-18T14:17:36Z | 2026-07-18T14:17:36Z | NULL | 0 | NULL | NULL | NULL | 0 | 0 | 0 | �{} | Merged to master 658f128: verdict-driven auto-routing (on_success/on_changes_requested per task_type, label swap + Ready, AutoRoutes cap 4, fixer role, e2e pipeline test design->review->fix->review->In Review). Live rollout (agent-1 config + type:fix board label) pending next CI deploy — noted in ah-0ge.5/ah-07g wave. | 0 | NULL | NULL | NULL | 0 | 2026-07-18T13:21:51Z | 0 | |||||||||||||||||||||
| ah-58f | ac69891cb925ac8c16f81cb926cc68234fbe2a845af62d30f267e19342797b87 | ahub tool answer without an outstanding question echoes the latest comment as the answer | Live smoke 2026-07-18: on a freshly claimed card (no ask-user issued), 'ahub tool answer' exited 0 and printed the claim comment's raw HTML as the answer. detectAnswer treats any latest-comment-without-sentinel as an answer; it should require that an asked-sentinel comment exists (an outstanding question) before anything counts as an answer, else 204/no-answer. Also consider stripping HTML for CLI output. Low impact (agents call answer after ask-user), found during rollout smoke. | closed | 3 | bug | Eugene Blikh | NULL | 2026-07-18T17:10:20Z | Eugene Blikh | bigbes@gmail.com | 2026-07-18T17:43:23Z | 2026-07-18T17:43:23Z | NULL | 0 | NULL | NULL | NULL | 0 | 0 | 0 | �{} | Merged 75b9a54: detectAnswer now requires an outstanding question (last asked-marker must exist with a comment after it) and returns plain text (dep-free HTML strip). BONUS CATCH: today's ah-bkr markdown→HTML conversion had silently broken raw-sentinel matching against live Vikunja-stored comments (escaped form) — detection now keys on the delimiter-free marker core present in both forms, repairing the live question loop before anyone hit it. | 0 | NULL | NULL | NULL | 0 | 2026-07-18T17:28:20Z | 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-eje | 3e29cf27b2ca9b1b2c758464fedcaa74ec70e478c94f99c19a2161f68613f767 | Q&A loop rollout: live-verify events.list ordering + comments.resolve, wire qa role/config on agent-1 | ah-ptu shipped the Q&A loop code-complete with two surfaces deliberately marked live-UNVERIFIED: (1) events.list Sort/Direction params (the poll tolerates any page order via cursor filtering, but a >100-events-per-poll gap warning fires blind until ordering is confirmed); (2) comments.resolve (spec-only v2-preview; config yonote.qa.resolve defaults off until smoke-tested). Rollout steps: deploy master (CI overlays prompts/qa.md automatically), hand-edit /etc/agent-hub/agenthub.yaml on agent-1 (config is NOT CI-managed): add a qa role (model + prompts/qa.md) and the yonote.qa block (enabled, role, collections default to publish_collection_id), restart agenthubd, then smoke: comment a question on a bot-published doc, watch the card spawn/answer, verify the threaded reply; flip resolve: true and verify comments.resolve works before recommending it. | LIVE FINDINGS (2026-07-19, bigbes.yonote.ru 1.47.1): (1) the instance writes NO audit events for comments — event kinds observed: documents.publish/delete/permanent_delete, revisions.create, collections.create; a fresh comment produced no event at all. (2) events.list 'name' filter is silently IGNORED (requested comments.create, got documents.publish/revisions.create back). (3) Default ordering IS newest-first; offset pages into the past; sort/direction change nothing. => events transport unusable for comment detection; pivoted to documents.list+comments.list sweep (commit f81830c, deployed build #231). Baseline correctly initialized at 2026-07-19T04:50:04.043Z (the pre-pivot probe comment). Spawn verified live: comment d54ceab2 @05:02:32 -> card task 4 @05:02:54 -> run started 05:03:15. comments.resolve still UNVERIFIED (resolve: false in prod config). Config on agent-1 edited by operator (backup agenthub.yaml.bak.20260719-044552): qa role (litellm/coder + /opt/agent-hub/prompts/qa.md) + yonote.qa block watching the Agents collection 8656642e. | closed | 2 | task | Eugene Blikh | NULL | 2026-07-18T18:47:44Z | Eugene Blikh | bigbes@gmail.com | 2026-07-19T18:21:38Z | 2026-07-19T18:21:38Z | NULL | 0 | NULL | NULL | NULL | 0 | 0 | 0 | �{} | Q&A loop live-verified end-to-end three times over. Final smoke (task 6): comment 18:13:59 → card 18:14:32 → 35s real run → threaded bot reply 18:15:27 → source comment resolved → card in_review, exactly-once held throughout. Both rollout defects found and fixed: qa role model litellm/coder→litellm/deepseek/deepseek-v4-flash (config, agent-1), comments.resolve missing isResolved boolean (2720611, ah-ziq). Remaining hardening tracked in ah-tqc. | 0 | NULL | NULL | NULL | 0 | 2026-07-18T20:04:59Z | 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-gxa | dffb086c7b7fb989cce7ee0c79a50575efa7059b98fd57b9c4d41e23de6555fb | internal/yonote: light API client (doc resolve/export, create, comments; bot-token auth) | Why: agenthubd integrates Yonote (Outline-fork docs product, live at bigbes.yonote.ru) in three slices — claim-time artifact export, finalize-time publishing, later Q&A. All need one light client. What: internal/yonote in the internal/mem0 style (New(baseURL, token, opts), ctx-first, typed *APIError, stdlib-only, httptest tests, doc.go pinning contract+quirks), scoped to exactly the integration surface: AuthInfo, DocumentInfo (uuid|urlId|slug-urlId), ExportMarkdown (/api/v2/documents/{uuid}/markdown), Search/List, CreateDocument/UpdateDocument, comments create/list/resolve, pure ParseDocRefs URL helper. Runs under a BOT token (minting runbook in design). wave 2.5 — implement after feat/wave2-archive-links merges. | # internal/yonote — light API client (stdlib-only, mem0 style) Mirror internal/mem0 exactly in shape: `New(baseURL, token string, opts ...Option) *Client` (trailing "/" trimmed, default `http.Client{Timeout: 30s}`, `WithHTTPClient`), ctx-first methods, one `do(ctx, method, path, query, in, out)` round-tripper, typed `*APIError`, package doc.go pinning the contract + quirks, httptest unit tests. No third-party deps. Contract pinned to live bigbes.yonote.ru, x-app-version 1.47.1, verified 2026-07-18. Yonote = Outline fork; API is RPC-style `POST /api/<resource>.<verb>` (JSON body) PLUS a newer `/api/v2/*` REST namespace. Auth on every request: `Authorization: Bearer <token>` (tokens are JWTs; treat as opaque). Success envelope `{data, status, ok, [pagination, count|total, policies]}`. Error envelope `{ok:false, error:"<machine_code>", status:<int, SOMETIMES ABSENT>, message:"<human>"}` — key off the HTTP status, carry error/message. ## Endpoint table (methods to implement; L = live-verified, S = spec-only) | Method | HTTP + path | Request | Response essentials | |------------------------------------------|--------------------------------------------|----------------------------------------------------|---------------------| | AuthInfo(ctx) | POST /api/auth.info | {} | L data.user{id,name,email,isBot,isAdmin,isViewer}, data.team{name,url,subdomain} | | DocumentInfo(ctx, id) | POST /api/documents.info | {"id"} — uuid OR urlId OR slug-urlId (all L) | L data = Document | | ExportMarkdown(ctx, uuid) | GET /api/v2/documents/{uuid}/markdown | path uuid ONLY (urlId → 400 invalid_format, L) | L {"data":"<markdown string>"} — may be "" (see quirks) | | SearchDocuments(ctx, req{Query,Limit}) | POST /api/documents.search | {"query","limit"} | L data[] of {context string, ranking float64, document Document}; top-level count | | ListCollections(ctx, limit, offset) | POST /api/collections.list | {"limit","offset"} | L data[] Collection{ID,Name,URLID,URL,Permission,Sharing}; pagination{limit,offset,nextPath}; top-level count | | ListDocuments(ctx, req) | POST /api/documents.list | {"collectionId","parentDocumentId","limit","offset"} (all optional) | L data[] Document; pagination; top-level total | | CreateDocument(ctx, req) | POST /api/documents.create | {"title"!,"collectionId"!,"text","parentDocumentId","publish"} | L data = Document (text echoed VERBATIM markdown; publish:false ⇒ publishedAt null even with team autoPublishDocs) | | UpdateDocument(ctx, req) | POST /api/documents.update | {"id"!(uuid|urlId),"title","text","append","publish"} | S (v1 spec; same Document envelope) | | CreateComment(ctx, req) | POST /api/comments.create | {"entityType":"document","entityId"!,"text"!,"parentCommentId"} | L data.comment{id,entityId,threadId,text,quote,isResolved,createdById,createdAt,updatedAt} — NESTED under data.comment | | ListComments(ctx, req) | POST /api/comments.list | {"entityId"!,"isResolved"(*bool),"threadId","limit"≤100,"offset"} | L data.comments[] (nested; items also carry threadCommentsCount, attachments[]); pagination; total | | ResolveComment(ctx, id, resolved) | POST /api/comments.resolve | {"id"!,"isResolved"!} | S (v2-preview spec) data.comment | | ParseDocRefs(baseURL, text) []string | (pure, no HTTP) | — | ordered dedup'd doc-id tokens from free text | Deliberately OUT of the client (documented in doc.go): documents.export RPC (now an ASYNC fileOperation on live — see quirks), attachments.* (follow-up if ever needed: attachments.list {documentId} → items carry redirectUrl/url/contentType/size/name; attachments.redirect {id} → 302 signed URL — both spec'd, redirect flow live-verified via fileOperations), events.list + webhookSubscriptions.* (Q&A bead extends the client then), bots CRUD (one-time provisioning, curl runbook below), documents.delete. ## Types Document{ID, URLID, Title, URL (site-relative "/doc/<slug>-<urlId>"), CollectionID string, ParentDocumentID *string, Text string, Type string ("document"|"database"|"row"|"whiteboard"|"embed"), Revision int, ArchivedAt, DeletedAt, PublishedAt *string (keep RFC3339 strings, lossless), CreatedAt, UpdatedAt string, ChildrenCount int}. Collection{ID, URLID, Name, URL, Permission string, Sharing bool}. Comment{ID, EntityID string, ThreadID *string, Text string, Quote *string, IsResolved bool, CreatedByID, CreatedAt, UpdatedAt string}. AuthInfo{User{ID, Name, Email string, IsBot, IsAdmin, IsViewer bool}, Team{Name, URL, Subdomain string}}. ParseDocRefs(baseURL, text): regexp `https?://<escaped-host>/doc/([A-Za-z0-9._~-]+)` where host comes from url.Parse(baseURL). Token = full last path segment (slug-urlId form — documents.info accepts it whole, no urlId slicing needed); query/#fragment excluded by the charset; works on both raw markdown and TipTap HTML (`<a href="...">`); dedup preserving first occurrence. ## Errors `*APIError{StatusCode int, Method, Path, Code, Message, Snippet string}` parsed from the error envelope; `(e *APIError) NotFound() bool` (StatusCode==404). Body snippet capped 2KiB; success-body read capped 8MiB (mem0 constants). Observed error bodies (assert in tests): - 401 authentication_required, message "Unable to decode JWT token" (bad token) / "Authentication or shareId required" (no token) - 404 not_found, "Resource not found" (documents.info, unknown uuid AND unknown urlId) / "Document not found" (v2 markdown) - 400 invalid_format, "Invalid UUID (id)" — NOTE: this body has NO "status" key - 500 internal_server_error (markdown export of a whiteboard doc) ## Quirks for doc.go (all verified live unless noted) 1. Identity: doc URL is `https://<team>.yonote.ru/doc/<slug>-<urlId>`; documents.info resolves uuid | bare urlId | slug-urlId; ALL /api/v2/* paths take the uuid only. 2. Document.Text is NOT reliable markdown: after editor edits it is a PLAINTEXT projection (all markup stripped — verified on a real doc: text had no #/**/`` while /markdown had them). /v2/.../markdown is canonical BUT returned "" for an API-created never-edited draft whose .text still held the submitted markdown verbatim, and returns "" for database-type docs. Whether the draft case is "draft" or "no editor state yet" is UNVERIFIED — callers use the fallback chain (markdown → info.Text → unavailable) which covers both. 3. documents.export RPC diverges from Outline AND from the v1 docs: live it requires a uuid and returns an async fileOperation {state waiting→complete} (poll fileOperations.info, download via fileOperations.redirect → 302 signed Yandex-cloud URL). Not wrapped by this client. 4. type=whiteboard → markdown export 500s; type=database → "" (v2 search/accessible spec says types document|database|row|whiteboard|embed). 5. Pagination: {limit, offset, nextPath}; totals are INCONSISTENT — v1 RPC puts count/total at the top level, /api/v2/* puts total inside pagination. Don't share one envelope type. 6. No rate-limit headers at 1.47.1; no documented limits. No client-side retry (callers own it, mem0 precedent); surface 429 as *APIError if it ever appears. 7. Live (1.47.1) is newer than the published docs (1.38.1). The v2-preview OpenAPI spec exists ONLY embedded in the docs-page JS (yonote.ru/developers?v=2 → _next/static/chunks/pages/ developers-*.js, two JSON.parse('...') literals: v2-preview then legacy v1); yonote.ru/openapi-3.json serves the OLD v1 spec. Re-extract from the chunk when in doubt. 8. Team feature flags seen in auth.info: mcpServer:true + mcpSettings (Yonote hosts a native MCP endpoint at /api/v2/mcp — the operator's claude.ai integration uses it; irrelevant to this client but explains the product's own tool surface). ## Bot-token provisioning runbook (one-time, operator; do BEFORE deploying the integration beads) The daemon must authenticate as a BOT, not as bigbes's personal token (attribution + blast radius). Bots are Yonote-native (not in Outline). Admin-only. As of 2026-07-18 GET /api/v2/bots returns [] — none exist yet. 1. POST /api/v2/bots {"name":"agent-hub","username":"agent-hub"} → data: bot user (grab id; user has isBot:true) 2. POST /api/v2/bots/<botId>/token {"name":"agenthubd"} → data{id,name,secret,...} — `secret` IS the API token, shown ONCE → store as YONOTE_TOKEN in /etc/agent-hub/env 3. Grant collections (bots see nothing by default per Bots tag doc "granted access to collections with read or write permissions"): POST /api/collections.add_user {"id":<collectionId>, "userId":<botId>,"permission":"read"|"read_write"} (v1-spec'd, NOT live-verified). 4. Verify with the minted token: POST /api/auth.info → user.isBot must be true; then documents.info on a granted doc, CreateDocument in the write collection. UNVERIFIED until then: auth.info under a bot token, bot default visibility, bot create rights. Rotation: GET /api/v2/bots/{id}/token (list, no secrets), DELETE /api/v2/bots/{id}/token/{tokenId}. Daemon behavior: on startup call AuthInfo, log identity, WARN when IsBot==false (personal token in prod must be visible, not fatal). ## Test plan (httptest, mem0-test style) Per method: happy path asserting HTTP method, exact path, Bearer header, JSON body; decode into typed result. Error mapping: 401/404 envelopes, 400 body WITHOUT "status" key, non-JSON body → Snippet only. Nested data.comment / data.comments decode. ExportMarkdown "" passthrough (no error). DocumentInfo with the three id forms (same handler, three paths asserted). ParseDocRefs table: bare URL in markdown, TipTap `<a href="https://host/doc/x-abc">`, uuid-in-URL, trailing punctuation/")"/query/fragment, cross-host rejected, http vs https, dedup order, cap-free (cap is bead-2 policy). | Live-verified 2026-07-18 against bigbes.yonote.ru x-app-version 1.47.1 (docs page documents 1.38.1 — live is newer). Evidence: (1) auth.info via Bearer OK — the operator token in use today is PERSONAL (user.isBot=false, isAdmin=true); no bots exist yet (GET /api/v2/bots → []); mint one before deploy (runbook in design). (2) documents.info resolved the same doc by uuid, bare urlId, slug-urlId. (3) /api/v2/documents/{uuid}/markdown returned faithful markdown for an editor-edited doc while documents.info .text for the SAME doc was plaintext with all markup stripped — .text is NOT markdown after editor edits. (4) One-probe-doc lifecycle (created→deleted, approved): documents.create with markdown text echoed text VERBATIM and honored publish:false (publishedAt null despite team autoPublishDocs:true); /markdown for that fresh API-created draft returned '' — hence the markdown→.text fallback chain; comments.create → {data:{comment}} nested, text stored verbatim, comments.list → {data:{comments:[…]}}; documents.delete {permanent:true} → {success:true}; info after delete → 404. (5) documents.export RPC live = ASYNC fileOperation (uuid-only, 400 invalid_format on urlId); fileOperations.info reached state complete and fileOperations.redirect 302'd to a signed Yandex-storage URL (Camera-export.md). SIDE EFFECT LEFT BEHIND: one accidental export fileOperation record (id 6b6e2056-9487-4f91-b10b-818d7c1a486c, doc 'Camera', 604B, complete) lingers in the workspace export history — harmless, operator may delete. (6) Error bodies: 401 authentication_required 'Unable to decode JWT token' / 'Authentication or shareId required'; 404 not_found; 400 invalid_format WITHOUT a status key; 500 internal_server_error on whiteboard markdown export; database-type docs export ''. (7) No rate-limit headers observed. (8) v2-preview OpenAPI spec exists ONLY embedded in the docs-page JS (yonote.ru/developers?v=2 → _next chunk pages/developers-*.js, two JSON.parse single-quoted literals: v2-preview 1.38.1 with /v2/bots + /v2/documents, then legacy v1 0.1.0); yonote.ru/openapi-3.json serves the OLD v1 spec. (9) webhookSubscriptions.list responds live (empty) though undocumented in both specs; events.list live-verified. UNVERIFIED (implementer/provisioning): auth.info under a bot token; bot default visibility + collections.add_user grant; bot create rights; documents.update; comments.resolve; whether /markdown '' means draft-only or missing-editor-state (fallback chain covers both). | closed | 2 | feature | Eugene Blikh | NULL | 2026-07-18T15:03:36Z | Eugene Blikh | bigbes@gmail.com | 2026-07-18T15:59:07Z | 2026-07-18T15:59:07Z | NULL | 0 | NULL | NULL | NULL | 0 | 0 | 0 | �{} | Merged to master f9a7b4b (3 commits 6c9e259/a25b692/f9a7b4b): yonote client + claim-time artifact materialization + publish lane. Live smoke at rollout per ah-25e notes. | 0 | NULL | NULL | NULL | 0 | 2026-07-18T15:06:56Z | 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-ptu | 656fcc1b3e50d2d0c581f01969cc197111ed13fffaf60d43d5c82a961ec5f638 | Yonote Q&A bot loop: poll doc comments, answer via board tasks (post-wave-2.5) | Why: the operator wants to ask questions as comments on Yonote docs (e.g. on agent-published reports) and have the bot answer in-thread. What: a polling loop (daemon has no public ingress — loopback-only listen — so webhooks are out for now) over events.list filtered to comments.create since a persisted cursor; qualifying comments spawn a question-answer board card whose description carries the doc URL (ah-2lh materialization then feeds the agent the doc as context); the answer is delivered via comments.create (parentCommentId threading) + optional comments.resolve under the bot identity. EXPLICITLY NOT wave 2.5: needs cursor persistence, a second poll loop, trigger-convention + mention-encoding verification, and a reply-channel decision. Depends on ah-gxa (comments methods; extend with ListEvents when picked up) and ah-25e (reply channel + bot write provisioning). | # Yonote Q&A bot loop (post-wave-2.5 — honest scope, NOT part of wave 2.5) Goal: the operator asks questions IN Yonote (comments on docs — e.g. on agent-published reports or on specs); the bot notices, an agent produces the answer, the bot replies in-thread and resolves the comment. ## Verified surface this builds on (2026-07-18, bigbes.yonote.ru 1.47.1) - comments.list {entityId, isResolved filter, threadId, limit ≤100, offset} → {data:{comments:[…]}, pagination, total} — LIVE-verified (nested envelope; items carry threadId, isResolved, threadCommentsCount, attachments). - comments.create {entityType:"document", entityId, text, parentCommentId} → {data:{comment}} — LIVE-verified end-to-end on a probe doc: text stored verbatim (plain string), threading via parentCommentId; a reply's threadId points at the root comment. - comments.resolve {id, isResolved} → data.comment — spec'd (v2-preview), plus the product MCP server exposes yonote_resolve_comment, so the surface is real. - events.list POST {name, actorId, documentId, collectionId, limit, offset, sort/direction} — LIVE-verified (returned {pagination, data:[{id, name, actorId, actor, documentId, collectionId, createdAt, data, modelId}]}); the `name` filter ("objects.verb", e.g. "comments.create") is spec'd — server-side event filtering makes cheap incremental polling possible. - webhookSubscriptions.list responds LIVE (empty list) though it is UNDOCUMENTED in both Yonote OpenAPI specs (Outline heritage; auth.info policies include create/listWebhookSubscription). BUT the daemon binds loopback-only (config validateLoopback) with no public ingress, so v1 transport = POLLING events.list; webhooks stay a documented future option if ingress appears. ## Sketch (design decisions OPEN — settle when picked up) - Poll loop: own ticker (~60s), separate from the reconcile tick; cursor persisted in the store (new kv row, e.g. yonote_cursor = last seen event id/createdAt). MUST verify then: events.list ordering + Sorting params for reliable incremental reads (direction/sort exist in the Pagination /Sorting schemas but were not exercised). - Trigger filter (pick one): (a) new unresolved comments on docs AUTHORED BY THE BOT (createdById == bot user id from AuthInfo) — simplest, covers "questions about agent output"; (b) explicit @mention of the bot — mention ENCODING in comment text is UNVERIFIED (probe stored plain text verbatim; the editor may encode mentions as structured nodes invisible to .text); (c) any unresolved comment in configured collections. - Self-trigger guard: ignore events with actorId == bot user id. - Answer path: reuse the whole existing pipeline — create a board card (existing CreateTask port, agent_tasks bucket conventions) of a question-answer task type whose description contains the doc URL (the materialization bead then auto-exports the doc as context for the agent!) plus the question thread text; the agent writes the answer; the daemon replies via comments.create with parentCommentId = thread root and optionally comments.resolve. Reply delivery needs either a small extension of the publish channel ({"reply_to_comment": …}) or a dedicated .task/answer.json — decide at design time. ## Why not wave 2.5 Needs cursor persistence, a second poll loop, trigger-convention and mention-encoding verification, and a reply file-channel decision — none of which artifact materialization or the publish lane depend on. Client extensions required when picked up: ListEvents (events.list) and possibly webhookSubscriptions.* — add to internal/yonote then, not now. Depends on: internal/yonote client bead (comments methods land there), publish-lane bead (reply channel shape + bot write provisioning). | Live evidence (2026-07-18): events.list POST works — {pagination, data:[{id, name, actorId, actor, documentId, collectionId, createdAt, data, modelId}]}, event names 'objects.verb' (observed documents.permanent_delete from the trash-purge cron); the name/actorId/documentId/collectionId request filters are v1-spec'd. comments.create/list verified end-to-end on the probe doc (threading via parentCommentId; replies carry threadId of the root; text stored verbatim). comments.resolve spec'd + present as yonote_resolve_comment in the product's own MCP server. webhookSubscriptions.list responds live (empty, UNDOCUMENTED in both OpenAPI specs — Outline heritage; auth.info policies include createWebhookSubscription) — future option only, blocked on public ingress. UNVERIFIED for pickup: events.list ordering/Sorting params for reliable incremental cursors; @mention encoding inside comment text (probe stored plain text; the editor may use structured nodes invisible in .text — trigger option (a) 'comments on bot-authored docs' avoids the question entirely). | closed | 3 | feature | Eugene Blikh | NULL | 2026-07-18T15:04:41Z | Eugene Blikh | bigbes@gmail.com | 2026-07-18T18:47:53Z | 2026-07-18T18:47:53Z | NULL | 0 | NULL | NULL | NULL | 0 | 0 | 0 | �{} | Implemented: events.list poll loop w/ kv cursor + exactly-once spawn, threaded answer delivery via answer.json, config yonote.qa block, store v5, prompts/qa.md. Rollout/live-verify tracked in ah-eje. | 0 | NULL | NULL | NULL | 0 | 2026-07-18T18:19:54Z | 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-tqc | 0ec5f3f94c113689a7606ace2c0a5475ee68df56a367f991d8d4967233d03efe | Runner: an errored-final-turn pi run (exit 0, zero tokens) finalizes as success | Live incident 2026-07-19 (task 4, Q&A smoke): pi --mode json made exactly one model call, the litellm proxy 403'd it (key not allowed for model 'coder'), pi recorded stopReason:error with zero usage and EXITED 0 in ~0.87s. The daemon trusts the exit code: outcome=success, card advanced to in_review, Q&A delivery then correctly reported 'no answer.json' — a broken model config masquerades as a successful run. Fix direction: Status/finalize (or ahub-run) should inspect the tail of events.jsonl — a run whose final assistant turn has stopReason:error (or whose agent_end follows zero completed tool/text turns) should finalize as outcome=error regardless of exit code. Second finding to fold in: /api/v1/status shows cost_usd=0 for ALL runs including real multi-minute ones (tasks 1-2), so the usage.cost.total accumulation from events.jsonl appears broken on pi 0.73.x — re-verify the event shape and fix the cost parse. | open | 2 | bug | NULL | NULL | 2026-07-19T05:38:11Z | Eugene Blikh | bigbes@gmail.com | 2026-07-19T05:38:11Z | NULL | NULL | 0 | NULL | NULL | NULL | 0 | 0 | 0 | �{} | 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 |