~bigbes/agents-dev · issues

1q0knr93f8csl28gm3jeuieg3458gvvc · 98 rows

idcontent_hashtitledescriptiondesignacceptance_criterianotesstatuspriorityissue_typeassigneeestimated_minutescreated_atcreated_byownerupdated_atclosed_atclosed_by_sessionexternal_refspec_idcompaction_levelcompacted_atcompacted_at_commitoriginal_sizesenderephemeralwisp_typepinnedis_templatemol_typework_typesource_systemmetadatasource_repoclose_reasonevent_kindactortargetpayloadawait_typeawait_idtimeout_nswaitershook_beadrole_beadagent_statelast_activityrole_typerigdue_atdefer_untilno_historystarted_atis_blocked
ah-07gbac07329270d597d113053b517e9dfaf960a2c01112410a47ffc02a9fde7bcf3Investigate 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.closed3taskEugene BlikhNULL2026-07-18T06:30:22ZEugene Blikhbigbes@gmail.com2026-07-18T14:48:24Z2026-07-18T14:48:24ZNULL0NULLNULLNULL000�{}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.0NULLNULLNULL02026-07-18T13:21:59Z0
ah-0ge2c909b097105e578055b7ee05ca088aa4e70e7f293e15e7aae83f28ef1a5aedaStage 4: agent tools API, question loop, AgentsView linksPer 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.closed3epicNULLNULL2026-07-12T23:36:29ZEugene Blikhbigbes@gmail.com2026-07-18T17:03:58Z2026-07-18T17:03:58ZNULL0NULLNULLNULL000�{}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.0NULLNULLNULL0NULL0
ah-0ge.1fe7de8b8454eea7571194b1d573a0f6427b0f4c7b704400ef3ea627f37636484Agent-created tasks via .task/tasks.json file channelFirst 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.closed3featureEugene BlikhNULL2026-07-17T14:38:02ZEugene Blikhbigbes@gmail.com2026-07-17T16:16:19Z2026-07-17T16:16:19ZNULL0NULLNULLNULL000�{}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.0NULLNULLNULL02026-07-17T15:58:45Z0
ah-0ge.2cc37ff5a5860feb86cb3556e7841450c187af1c9a8c3899f32e4f991134e9e1bParent auto-Block/unblock join on child completionThe 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).closed3featureEugene BlikhNULL2026-07-17T14:38:03ZEugene Blikhbigbes@gmail.com2026-07-17T19:05:05Z2026-07-17T19:05:05ZNULL0NULLNULLNULL000�{}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.0NULLNULLNULL02026-07-17T17:45:37Z0
ah-0ge.35ba4f4620f3f376c331035f6bea0224fc91e067c39719445c159cae5b8477312Resume parent via pi --session with child results as a new attemptCloses 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.closed3featureNULLNULL2026-07-17T14:38:04ZEugene Blikhbigbes@gmail.com2026-07-17T19:05:05Z2026-07-17T19:05:05ZNULL0NULLNULLNULL000�{}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.0NULLNULLNULL0NULL0
ah-0ge.4a129a5d69b25f2dc5692877fa06f42251ed75cd3b43a5fd44cf2cb374fa55378Ask-user question loop via .task/question.json file channelFile-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.closed3featureNULLNULL2026-07-17T20:14:02ZEugene Blikhbigbes@gmail.com2026-07-17T20:14:17Z2026-07-17T20:14:17ZNULL0NULLNULLNULL000�{}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.0NULLNULLNULL0NULL0
ah-0ge.513cbbe263e9f371367678d1e893b367dba3fad166f059227ee2dbb6d5e9968ffAgentsView: pg push from agent-1 + per-run session deep links in commentsMake 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.closed2featureNULLNULL2026-07-18T13:23:54ZEugene Blikhbigbes@gmail.com2026-07-18T14:48:24Z2026-07-18T14:48:24ZNULL0NULLNULLNULL000�{}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.0NULLNULLNULL0NULL0
ah-0ge.6b4fcedab02763cd008acf6166f13b5d21f4045245781fa8f20b66db54d065335HTTP tools API: per-task bearer tokens + ahub tool CLI over loopbackSPEC §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.closed3featureEugene BlikhNULL2026-07-18T13:23:55ZEugene Blikhbigbes@gmail.com2026-07-18T17:03:57Z2026-07-18T17:03:57ZNULL0NULLNULLNULL000�{}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.0NULLNULLNULL02026-07-18T16:22:45Z0
ah-0iz7425b905ec10a455e6046127ed987332be425930741cb9673855f0d31f498e0cmarkdownToHTML: 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 &gt; lines. Add blockquote handling to markdownToHTML. Verified live: Vikunja stores <blockquote>/<strong> intact.closed3taskNULLNULL2026-07-18T06:48:12ZEugene Blikhbigbes@gmail.com2026-07-18T06:48:12Z2026-07-18T06:48:12ZNULL0NULLNULLNULL000�{}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.0NULLNULLNULL0NULL0
ah-16670aef8c9b28e9321acde83be3890df0355b088d88dc80415b3e555ce24eadea9pi version drift: SPEC pins 0.70.2, installed is 0.73.1SPEC §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.closed3taskNULLNULL2026-07-17T14:37:27ZEugene Blikhbigbes@gmail.com2026-07-17T15:48:20Z2026-07-17T15:48:20ZNULL0NULLNULLNULL000�{}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.0NULLNULLNULL0NULL0
ah-1cx7514a2243a0bce4ceca3334a52c5e91ea6e87dfdcc14147353506ee683681c8eHardening: post-audit bug sweep (2026-07-20)Milestone collecting every confirmed defect from the 2026-07-20 four-way audit (marker sweep, SPEC gap analysis, core-runtime and adapter bug hunts by Opus workers). Scope: runner/reconcile/store correctness, config wiring holes, comment rendering, and the outstanding live-verification tasks. ah-tqc (errored-turn-as-success) is re-parented here.open2epicNULLNULL2026-07-19T23:37:15ZEugene Blikhbigbes@gmail.com2026-07-19T23:37:15ZNULLNULL0NULLNULLNULL000�{}0NULLNULLNULL0NULL0
ah-1cx.1e94360d1f52dbea42a761d52a15cb74d70d7f0ea533c2469487cf945d307328frunner.Status re-parses full events.jsonl every poll, stalling the loop on large runsStatus unconditionally calls ParseEventsFile (reads to EOF, decodes every line) on every 20s poll for every in-progress task (runner.go:209-219), but the parsed pi-session/cost are display-only until finalize, which re-reads via completionFactsFromEvents anyway. On a multi-hundred-MB events stream (ah-07g observed 202MB) this blocks the single-goroutine reconcile loop for seconds per tick, starving supervision of every other task. Fix: skip the full parse for a still-running run — read only the first line for the session id (or defer session/cost to finalize) and keep the cheap os.Stat mtime for the watchdog. Found by core-runtime audit 2026-07-20.closed2bugNULLNULL2026-07-19T23:37:28ZEugene Blikhbigbes@gmail.com2026-08-04T23:45:37Z2026-08-04T23:45:37ZNULL0NULLNULLNULL000�{}Fixed in f11ef03. Status's per-poll read of events.jsonl is now asymmetric: the still-running path does only the os.Stat it already needed for LastEvent (the stall-watchdog input) plus PeekSessionID, a new bounded head probe that reads at most 256 KiB / 8 leading lines looking for the {"type":"session","id":...} event pi writes as line 1. The full ParseEventsFile no longer sits on the 20s poll path, so a 202 MB stream (ah-07g) costs a running poll the same as an empty one and can no longer freeze the single-goroutine reconcile loop. CostUSD is deliberately left at zero for a running run — cost is a whole-stream sum with no cheap mid-run answer, and the reconciler only persists cost at finalize — and PiSession is explicitly best-effort ("" while the head is unwritten or torn), which is safe because the reconciler only overwrites a stored session id with a non-empty one. Every path that COMPLETES a run still takes the full parse: both exit.json branches via finalizeFromExit (ah-2ef's durable pinning, untouched) and — newly — the crash-confirmed branch, since a confirmed crash writes an immutable finished row and previously relied on the top-of-Status parse for the failure comment's session id and cost. The reader loop was extracted into scanEventLines(r, fn) with an early-stop callback, so the F9a over-long-line rule (skip the line, keep scanning; never bufio.Scanner) is shared by the full parse and the probe and cannot drift; the probe's byte budget is deliberately 4x the read buffer so a leading line wider than the buffer is stepped over rather than ending the probe. Cover: TestStatusRunningDoesNotParseWholeEventStream builds an 8 MB stream with the id on line 1 and a fat cost-bearing turn at the end, asserting Running ⇒ id resolved + CostUSD==0 + LastEvent==mtime, then drops exit.json and asserts the same stream yields the full cost; TestPeekSessionIDIsBounded proves boundedness structurally. Mutation-checked. KNOWN REGRESSION SPLIT OUT: kill-finalized runs now record CostUSD 0 — see the dedicated bead.0NULLNULLNULL02026-08-04T23:32:43Z0
ah-1cx.10f4afcd3a4d676d400e4e0f7fe95bf71960d6b6960626598a3471013233594d3c[bug] askAndPark has the same half-landed-park bug ah-1cx.2 fixed for delegationFound while fixing ah-1cx.2 and deliberately left alone there to keep that change reviewable. askAndPark parks the parent in Question the way delegateAndBlock parks it in Blocked, and it has the identical failure mode: if its UpsertTask(Question) — and its Yonote/question sentinel writes — fail AFTER the question comment is posted, the refinalize arm has no marker for it, so the parent routes to In Review and the question is silently dropped. The human is left with a card in review that is actually waiting on an answer nobody will give. ah-1cx.2's shape is the fix: arm a durable kv intent marker BEFORE the first irreversible side effect, extract an idempotent park tail, and add a refinalize-only re-park arm that re-attempts the flip from the marker and never repeats the side effect. See reparkDelegatedParent / parkParentBlocked in internal/reconcile/reconcile.go as of 67dcf3c.A failed UpsertTask(Question) leaves the card in In Progress with a loud comment and audit event, and a later tick re-parks it to Question without re-posting the question comment. Covered by tests mirroring TestDelegationParkPersistFailure.open2bugNULLNULL2026-08-04T23:43:09ZEugene Blikhbigbes@gmail.com2026-08-04T23:43:09ZNULLNULL0NULLNULLNULL000�{}0NULLNULLNULL0NULL0
ah-1cx.1139a21e11c8e682fefa03a720759e6bc1b9c78a3b57baf86938f388843b7556e3[bug] createChildTasks swallows RecordChild failures, orphaning a child from the joinFound while fixing ah-1cx.2. createChildTasks logs and continues when RecordChild fails (internal/reconcile/tools.go:376). A child created on the board whose lineage row is lost is invisible to both ChildIDsOf and the parent's join: the parent parks in Blocked waiting on a set that does not include it, so the child can complete without ever unblocking the parent — and a human sees a blocked card with no visible reason. It also weakens the before-count guard ah-1cx.2 added, which distinguishes a real pending join from fire-and-forget children by comparing the parent's child count before and after delegation.A RecordChild failure either aborts the delegation loudly (card held, comment, audit event) or is retried durably. Either way the parent never parks on an incomplete child set. Covered by a test.open3bugNULLNULL2026-08-04T23:43:10ZEugene Blikhbigbes@gmail.com2026-08-04T23:43:10ZNULLNULL0NULLNULLNULL000�{}0NULLNULLNULL0NULL0
ah-1cx.1287493dc59cdef62816e7d9263a77115f54898c15ccb5682ab6669ef6dc10e0fdreconcile hygiene: no Store.DeleteKV, and the finished audit event re-appends on a stuck refinalizeTwo small findings from the ah-1cx.2 work, neither urgent. 1. Store has no DeleteKV, so ah-1cx.2's delegation-park marker is 'cleared' by writing an empty string. A long-lived instance therefore accumulates one dead kv row per delegating attempt. Bounded and harmless, but a real DeleteKV (in internal/store, outside the reconcile change's file scope) would be cleaner and would let the marker be removed rather than tombstoned. 2. The 'finished' audit event re-appends on every stuck refinalize tick. appendEvent dedups only against the task's MOST RECENT event, and the refinalize path alternates event kinds, so the dedup never fires. Pre-existing, and harmless while refinalize stalls are rare — worth a cheap guard if they ever become common.Store exposes DeleteKV and the delegation-park marker uses it; appendEvent's dedup survives an alternating-kind refinalize loop.open4choreNULLNULL2026-08-04T23:43:11ZEugene Blikhbigbes@gmail.com2026-08-04T23:43:11ZNULLNULL0NULLNULLNULL000�{}0NULLNULLNULL0NULL0
ah-1cx.134b771ca56b604525519b8a0e712adeed7e83551d956c2bf7c7df476e080a1614[bug] kill-finalized runs record CostUSD 0 since the running path stopped parsing the streamRegression introduced by ah-1cx.1 and reported by the agent that made it. Reconciler.killAndFinalize (internal/reconcile/reconcile.go:700) finalizes a stall- or deadline-killed run from the ports.RunStatus of the last RUNNING poll. After ah-1cx.1 that status deliberately carries no cost — the running path does only os.Stat + the bounded PeekSessionID, because a whole-stream cost sum has no cheap mid-run answer. Previously the killed run at least recorded a partial-stream sum; now it records 0. Every other terminal path re-reads the complete stream before finalizing (both exit.json branches via finalizeFromExit, and the crash-confirmed branch, which ah-1cx.1 added for exactly this reason). The kill path is the one that was missed, and it cannot be fixed inside internal/runner alone.The runner needs to expose the finalize-time facts to the kill path. Two candidate shapes: - have ports.Runner.Kill return the EventStreamInfo it can read once the process is down, or - add an exported FinalizeFacts(taskID) the kill path calls after Kill returns. The first is tighter (one round trip, and Kill is already the moment the stream stops growing); the second keeps Kill's signature clean. Note the stream is only guaranteed complete once ahub-run has finished teeing, which a kill does not wait for — so whichever shape is chosen must say plainly whether the cost it records is complete or best-effort, rather than implying the former.A stall-killed and a deadline-killed run both record a non-zero CostUSD when their stream carried one, and the honesty of that number (complete vs best-effort) is stated in the code and in SPEC section 9.open3bugNULLNULL2026-08-04T23:43:42ZEugene Blikhbigbes@gmail.com2026-08-04T23:43:42ZNULLNULL0NULLNULLNULL000�{}0NULLNULLNULL0NULL0
ah-1cx.142bcefeca5810ee4589983de45c6f9eda40eca330f7c51a579954fa4007896a5frunner: the crash-confirmed full parse repeats every poll while finalize keeps failingNoted while fixing ah-1cx.1. ah-1cx.1 added a full ParseEventsFile to Status's crash-confirmed branch, because a confirmed crash writes an immutable finished row and previously relied on the top-of-Status parse for the failure comment's session id and cost. Correct — but once past the grace, every subsequent Status of a still-unfinalized crashed attempt re-reads the whole stream. Bounded in practice (finalize normally succeeds on the first observation), and far less severe than the original ah-1cx.1 bug, which hit every in-progress task on every tick. But it is the one remaining O(stream) call reachable from the poll loop, so it is worth closing once ah-1cx.8's or ah-1cx.9's stream-size work lands — a 202 MB stream re-read in a loop is the same starvation shape.A crashed-but-unfinalized attempt does not re-read the whole event stream on every poll — either the facts are cached per (task, attempt) or the parse moves to the finalize call itself.open4choreNULLNULL2026-08-04T23:43:43ZEugene Blikhbigbes@gmail.com2026-08-04T23:43:43ZNULLNULL0NULLNULLNULL000�{}0NULLNULLNULL0NULL0
ah-1cx.157ea33ea771d8c5c30bad682cadf1d09f6b4388d61a2903ce4a9a77768b12afe0ahub-run should drop message_update while teeing: 95% of every event stream is unreadRoot cause behind the 202 MB event streams (ah-07g) that motivated ah-1cx.1, quantified during the ah-1cx.4 capture. In a fresh real pi 0.82.1 run, message_update events were 123 of 143 lines and about 95% of the 166 KB. They exist so an interactive UI can render a partial message as it streams, and each one embeds the WHOLE partial message — so their volume grows quadratically with message length. Nothing in this codebase reads them: ParseEvents only consumes the session header and message_end. Having ahub-run filter message_update out while teeing events.jsonl (or rotate/compress the file) would shrink real streams by roughly 20×, which would make even the full parse cheap and would retire the tail of this whole class of bug — ah-1cx.1's asymmetric read path, ah-1cx.9's repeated crash-path parse, and the disk footprint of every archived worktree.Decide first what the event stream is FOR. If it is only the daemon's fact source, filtering is free. If it is also a human debugging artifact (someone attaching to the pane, or reading an archived tarball to see what the agent was doing), then message_update is the only record of the streaming text and dropping it loses that — in which case rotation or compression is the right answer instead, or filtering with the raw stream kept separately under a size cap. The archive lane (internal/runner/archive.go) is the other consumer to check. Note ahub-run is the only writer, so this is a one-file change plus a decision.A real multi-minute run's events.jsonl is an order of magnitude smaller, the daemon's parsed facts are unchanged, and whatever is lost for human debugging is stated explicitly in SPEC section 9.open3featureNULLNULL2026-08-04T23:43:44ZEugene Blikhbigbes@gmail.com2026-08-04T23:43:44ZNULLNULL0NULLNULLNULL000�{}0NULLNULLNULL0NULL0
ah-1cx.167eff6608348ecbc852835f358a9a699e40ec1398043f10acc52a4b282570f971runner: reject a blank rendered PROMPT.md at Start instead of burning an attemptFound by the adversarial review of ah-tqc and empirically reproduced against the installed pi 0.82.x. pi's dist/modes/print-mode.js:94 guards the entire run with 'if (initialMessage)', so an empty or whitespace-only prompt makes pi do nothing and exit 0 with a one-line event stream containing only the session header. runner.prepareTaskDir writes s.Prompt verbatim with no non-empty check, and nothing in reconcile guards it either. ah-tqc's second Errored() arm (SessionID != "" && AssistantMessages == 0) now CATCHES this, so the card correctly fails instead of sailing to In Review — but only AFTER the attempt is spent, a worktree is created, a zellij pane is spawned and an attempt number is consumed. A strings.TrimSpace(s.Prompt) == "" rejection in Start would fail it loudly and instantly with an actionable message naming the role and template. Deliberately left out of the ah-tqc fix because the check arguably belongs on the reconcile side of the boundary — the reconciler is what renders the template and knows which role/prompt file produced the empty result, so it can say WHY. Decide which side owns it: a Start-side guard is a cheap backstop that cannot explain itself, a reconcile-side check can name the template but leaves the port unguarded against a future caller.A role whose template renders to whitespace fails before a worktree or pane is created, with an error naming the role and the prompt file. The ah-tqc Errored() arm stays as the backstop and its test still passes.open3bugNULLNULL2026-08-04T23:46:15ZEugene Blikhbigbes@gmail.com2026-08-04T23:46:15ZNULLNULL0NULLNULLNULL000�{}0NULLNULLNULL0NULL0
ah-1cx.2b631a20368ede9c76697a930cc68dfee0415035e0dd41ffb9b8b01666ce3ce31Delegation persist failure orphans children: parent re-finalizes to In Review, never BlockedWhen delegateAndBlock has already created child cards and recorded lineage but its UpsertTask(Blocked) fails (reconcile.go:921-930), the stored record stays in_progress while the run row is finished. Next tick adoptOrFail takes the interrupted-finalize path and re-finalizes with firstFinalize=false, so delegation is skipped and the card lands In Review — the children run to completion but the parent never joins or resumes to integrate them. The code comment claims the heal path converges to Blocked; it does not (only the sibling MoveToBucket-failure case converges via alignCardToRecord). Fix: re-attempt the Blocked persist (retry signal) instead of letting a finished-row re-finalize strand the children. Rare trigger (store write failure at exactly that step); confirmed by reading, core-runtime audit 2026-07-20.closed3bugNULLNULL2026-07-19T23:37:32ZEugene Blikhbigbes@gmail.com2026-08-04T23:41:33Z2026-08-04T23:41:33ZNULL0NULLNULLNULL000�{}Fixed in 67dcf3c. Confirmed by REPRODUCTION, not just reading: finalize computes firstFinalize from run.State != finished and the delegation branch was gated on it; when delegateAndBlock's UpsertTask(Blocked) failed, the run row was already finished (UpdateRun runs first) while the task record stayed in_progress, so the next tick's adoptOrFail interrupted-finalize path re-entered finalize with firstFinalize=false, skipped delegation entirely, and routed the parent to In Review behind live children. The old comment claiming the heal path converges to Blocked was WRONG — alignCardToRecord only covers the sibling MoveToBucket-failure case, where the record is already Blocked. Fix: delegateAndBlock arms a durable intent marker in the existing kv store (delegation_park/<task>/<attempt>, value armed:<n>/alarmed:<n> where n is the parent's child count BEFORE this delegation) ahead of createChildTasks, so the only write that can fail before any child exists is the marker itself, and that failure cancels the delegation outright instead of half-landing it. The park tail is extracted into an idempotent parkParentBlocked used by both the first finalization and a new refinalize-only reparkDelegatedParent, which re-attempts the flip from the marker and NEVER calls createChildTasks — so a heal cannot become a duplicate-child bug. The before-count is what distinguishes a real pending join from fire-and-forget children created by the mid-run ToolTaskCreate channel and from an armed-but-created-nothing delegation; both disarm and finalize to In Review as before. Failure is now loud and recoverable: error log, delegation_park_failed audit event, and a once-per-attempt card comment (guarded by the marker's alarmed flag so a broken store cannot spam the card), with the card held in In Progress so the loop retries each tick. Run-row immutability untouched. Cover: TestDelegationParkPersistFailure (4 subtests) + TestDelegationParkGuards (4 subtests).0NULLNULLNULL02026-08-04T23:32:43Z0
ah-1cx.3b02d6efc693d9bd277063ae566b1ff77db50cfc1652da0418ad14b88bb47ed5cconfig/wiring: whitespace-only notifier field passes validation then fatally aborts bootNtfy/Telegram blocks are stored untrimmed (config.go:453) and present() decides configuration by trimmed emptiness, but reconcileDeps (cmd/agenthubd/main.go:292,298) selects the notifier with raw non-empty checks. A whitespace-only ntfy.url (literal or via VAR expansion) is absent to validation yet present to wiring: config.Load succeeds, then ntfy.New rejects it and run() exits 1 — killing a deployment whose telegram block is fully valid. This contradicts present()'s own documented invariant. Fix: trim Ntfy/Telegram in resolve (as yonote/mem0 already are) or make reconcileDeps use present(). Found by adapter audit 2026-07-20.closed3bugNULLNULL2026-07-19T23:37:36ZEugene Blikhbigbes@gmail.com2026-08-04T23:41:51Z2026-08-04T23:41:51ZNULL0NULLNULLNULL000�{}Fixed in a8f264e. Root cause: the optional blocks' present() predicates decide configuration by TRIMMED emptiness, but resolve copied Ntfy/Telegram straight out of the YAML untrimmed and reconcileDeps (cmd/agenthubd/main.go:292,298) selected the notifier with raw non-empty tests — so a whitespace-only ntfy.url was ABSENT to validation and PRESENT to wiring: config.Load succeeded, ntfy.New then rejected the value and run() exited 1, killing a deployment whose telegram block was perfectly valid. Fixed at the root by making 'set but whitespace-only' a load-time error instead of a value that silently reads as unset: resolve now scans the RAW decoded document (before any trimming erases the evidence) via blankScanFields()+validateNoBlankValues() and reports '<field> is set to whitespace only: remove the key to leave it unset, or give it a real value'. The scan is deliberately GENERIC, covering every field a present()/required check reads through TrimSpace: vikunja.token, vikunja.webhook_secret, both agentsview fields, both ntfy fields, telegram.token/api_base, yonote.base_url/token/publish_collection_id/qa.role/qa.bucket/qa.collections[i], both mem0 fields — collections entries indexed by name because trimNonEmpty would otherwise drop a blank one and silently shrink the watched set. resolve also now trims the four notifier strings the way yonote/mem0 base_urls already were, so the value the adapter constructor receives is exactly the value present() judged; this also stops a merely PADDED value (a ${NTFY_URL} picking up a trailing newline from the env file) from reaching ntfy.New. reconcileDeps keeps its raw tests but now carries a comment recording why they are equivalent to present() and that any new notifier field must be trimmed at load rather than compared there. Cover: TestLoadBlankValues — the original scenario, the ${VAR}-expands-to-blank variant, a table over all sibling fields, and padded-but-real values surviving trimmed with present() true.0NULLNULLNULL02026-08-04T23:32:44Z0
ah-1cx.44cc6b8f0ddcf69ba72aad626c3b1b374102663f9bf71080ea5d8e6a01106fca9runner: verify F9 cost model — is pi message cost.total per-message or cumulative?costFrom/ParseEvents SUM message.usage.cost.total across all assistant message_end events (events.go:120-128), but no captured multi-message fixture proves the field is per-message; testdata/pi-events-two-messages.jsonl is hand-built (TODO from closed ah-nyl.11). If cost.total is actually cumulative, every multi-turn run's reported cost is inflated and both costFrom and the fixture must be re-pinned to take the last message's value. Capture a real multi-assistant-message pi --mode json run and settle it. Distinct from sibling ah-tqc (errored-turn-as-success + cost_usd parse); marker-sweep audit 2026-07-20.closed3bugNULLNULL2026-07-19T23:37:39ZEugene Blikhbigbes@gmail.com2026-08-04T23:46:00Z2026-08-04T23:46:00ZNULL0NULLNULLNULL000�{}SETTLED in f11ef03: message.usage.cost.total is PER-MESSAGE, not cumulative, so ParseEvents's summation is correct. TODO(ah-nyl.11) removed from both costFrom's doc comment and TestParseEventsTwoAssistantMessagesSumsPerMessageCost. Evidence is a REAL two-assistant-message, one-tool-call run captured from pi 0.82.1 (deepseek/deepseek-v4-flash, exit 0), committed as testdata/pi-events-multi-real.jsonl; the 123 message_update lines (95% of the raw 166 KB, ignored by the parser) were stripped and the trimmed 20-line fixture verified to parse byte-for-byte identically to the raw capture. The proof recorded in costFrom is ARITHMETIC rather than assertive: one flat set of unit rates ($0.14 / $0.28 / $0.0028 per Mtok for input / output / cacheRead) explains each message's cost from that message's OWN token counts alone — msg1 97*1.4e-7=0.00001358 and msg2 85*1.4e-7=0.0000119; msg1 70*2.8e-7=0.0000196 and msg2 79*2.8e-7=0.00002212 — so msg2's total contains no part of msg1's, which a cumulative field could not manage. TestParseEventsRealMultiMessageCapture pins session id, CostUSD == 0.0000374808+0.0000386792, AssistantMessages == 2, FinalStopReason == 'stop' (the first turn ended in toolUse). The hand-built pi-events-two-messages.jsonl and its test are KEPT but re-framed as the pi 0.70.2 shape pin with round numbers. Two incidental findings now documented in the events.go header: turn_end repeats its turn's usage verbatim (so the not-double-counted rule is right), and 0.82.1 adds a trailing agent_settled event plus assistant message_start events that carry a stopReason and a zeroed usage block — the latter would skew AssistantMessages/FinalStopReason if the message_end type check were ever loosened.0NULLNULLNULL02026-08-04T23:32:44Z0
ah-1cx.53b52211a9bdaa7c6e5ae2ecf70e7f8f7402cd28f4f4ed3ee2da728743e935122vikunja markdown: URL autolink collides with bold / trailing-ampersand entityrenderInline (markdown.go:203-211) runs escape, then URL-autolink, then bold. urlRe greedily swallows a trailing ** into the URL and boldRe then matches across the emitted anchor markup, producing interleaved broken tags for input like: see **http://example.com/x** now. Separately a URL ending in a bare ampersand becomes a split, broken amp-entity. Both reproduced against the exact regexes; the realistic mid-URL query case renders fine, so severity is low. Fix: constrain the URL match at **/entity boundaries or reorder the passes; add cases to markdown_test.go. Adapter audit 2026-07-20.closed4bugNULLNULL2026-07-19T23:37:48ZEugene Blikhbigbes@gmail.com2026-08-04T23:42:04Z2026-08-04T23:42:04ZNULL0NULLNULLNULL000�{}Fixed in c2e29c1. renderInline ran escape → autolink → bold, and BOTH later passes collided with the anchor the middle pass had already emitted. urlRe treats '*' as an ordinary URL character, so 'see **http://example.com/x** now' swallowed the closing ** into both the href and the link text, and boldRe then matched from the opening ** across the emitted markup, yielding interleaved <strong>/<a> garbage. Separately, because the pass runs on ALREADY-ESCAPED text, a URL ending in a bare & became …&amp; and urlRe — which excludes a trailing ';' as sentence punctuation — stopped at …&amp, splitting the entity across </a>; same for a URL followed by > or '. Fix: reorder so bold runs FIRST (the ** markers are consumed while still adjacent to the URL, and urlRe's class already excludes the '<' of the resulting <strong> tag, so the link nests strictly inside the bold), and replace the ReplaceAllString autolink with autolinkURLs, which re-cuts every match through the new splitURLTail. splitURLTail alternates two rules until neither fires: push a trailing INCOMPLETE entity (an & with no ; after it) out of the anchor so the ; left in the surrounding text re-joins it, then re-apply the trailing-punctuation rule ('.,;:!?)]' plus '*', so an unpaired bold marker can never ride into an href) to the newly exposed last character. A complete entity mid-URL (?a=1&amp;b=2) contains its ; and is untouched, keeping the realistic query-string case rendering as before; a match trimmed down to a bare scheme (http://&) is left unlinked instead of emitting a dud anchor. Seven table cases added; all five collision cases were written first and confirmed RED against the old converter.0NULLNULLNULL02026-08-04T23:32:44Z0
ah-1cx.6d01a5e8265bcd3209e0b76c2b266cc18e3074a5c3393aa9e0624d33d2e7eca68config: validate vikunja.url and web_url as http(s) at load, like the other URL fieldsvalidate() only checks Vikunja.URL for non-emptiness (config.go:587-589) while agentsview/yonote/mem0/ntfy/telegram all get validateHTTPURL; web_url is never checked at all. A malformed board URL loads cleanly and fails one layer later at vikunja.New with different diagnostics. Run validateHTTPURL on Vikunja.URL and the explicit WebURL in validate() for consistent config-time errors. Adapter audit 2026-07-20.closed4bugNULLNULL2026-07-19T23:37:51ZEugene Blikhbigbes@gmail.com2026-08-04T23:41:52Z2026-08-04T23:41:52ZNULL0NULLNULLNULL000�{}Fixed in a8f264e (same commit as ah-1cx.3 — the hunks interleave in config.go). validate() checked vikunja.url for non-emptiness only while agentsview/yonote/mem0/ntfy/telegram all ran their base URLs through the existing validateHTTPURL helper, and vikunja.web_url was never checked at all; a malformed board URL loaded cleanly and failed one layer later inside vikunja.New, with different wording and no config-file context. The required-check is now strings.TrimSpace(...)=='' (so a blank url still reports 'vikunja.url is required' rather than a confusing scheme error) and the non-empty path REUSES validateHTTPURL — no second implementation was introduced. web_url is validated in the same branch rather than beside it because resolve DERIVES web_url from url when the key is omitted: checking it unconditionally would make one bad url report itself twice under two names. Ruling url out first guarantees the derived value is valid, so anything reported against vikunja.web_url is an explicit key the operator actually wrote. Cover: TestLoadVikunjaURLValidation — scheme-less url, non-http scheme, host-less url, blank url, explicit bad web_url, accepted explicit web_url, and an assertion that a bad url does NOT also echo as vikunja.web_url.0NULLNULLNULL02026-08-04T23:32:44Z0
ah-1cx.792b34accfb910f1a0059b4a049f641e03ee0ff15be6d11c31f6045f98fc7fb7ayonote: live-verify collections.add_user bot grant during provisioningThe bot-token runbook's collection-grant step (doc.go:82-85, POST /api/collections.add_user with userId+permission) is the only provisioning call still marked NOT live-verified — it is documented from the v1 spec. Run it once against live bigbes.yonote.ru 1.47.1, confirm the bot gains read_write on a collection, then drop the caveat from the runbook. Until then a fresh deployment cannot trust the publish lane grant step. Marker-sweep audit 2026-07-20.open4taskNULLNULL2026-07-19T23:37:55ZEugene Blikhbigbes@gmail.com2026-07-19T23:37:55ZNULLNULL0NULLNULLNULL000�{}0NULLNULLNULL0NULL0
ah-1cx.8f9af778a32a0fc2ac9d0d9bf63b5430905203491ae53324cebc0c91faecff187[bug] cost_usd is 0 for every live run — pi prices from its own registry, the litellm provider has noneSplit out of ah-tqc's second finding so that bead can close on its first finding. /api/v1/status reports cost_usd=0 for ALL runs on agent-1, including real multi-minute ones (observed on tasks 1-2 during the 2026-07-19 Q&A smoke). The run row's CostUSD comes from ParseEvents summing message.usage.cost.total over assistant message_end events. RULED OUT: the parser. A real pi 0.82.1 capture against the DIRECT deepseek provider (deepseek/deepseek-v4-flash) puts cost.total exactly where ParseEvents reads it, and the summation is arithmetically correct (proven per-message, not cumulative — see ah-1cx.4). LEADING HYPOTHESIS: pi computes cost itself from a per-provider model pricing registry. agent-1 runs every role through a CUSTOM OpenAI-compatible provider named 'litellm' (role models are 'litellm/<model>'), and a custom provider carries no pricing metadata, so pi emits cost{input:0,output:0,total:0} while token counts are still real. If that holds, nothing in agent-hub is broken and the fix is to stop trusting pi for cost: either derive it from token counts against a locally configured price table, or read spend from the LiteLLM proxy (it tracks per-key spend), or drop the field from the status API rather than reporting a confident zero.Settle the hypothesis BEFORE writing code. Needed evidence, from agent-1 (the local ssh probe is blocked by the permission classifier — needs an operator '!' handoff): 1. A real events.jsonl from a completed run: jq -c 'select(.type=="message_end" and .message.role=="assistant") | .message.usage' over the archived stream. Are totalTokens non-zero while every cost.* is 0? That confirms the hypothesis outright. 2. The pi provider config on the box (how the 'litellm' provider is declared) — does pi's config format accept per-model pricing for a custom provider? If it does, the cheapest fix is config-only, on the box, with no code change at all. Only if both come back negative does this become a parser/shape bug.Either (a) pi is configured/patched so cost.total is real for the litellm provider and a live run reports a non-zero cost_usd, or (b) cost is sourced from somewhere trustworthy, or (c) the field is explicitly documented+surfaced as unavailable rather than a silent 0. In all three cases SPEC and the status API description must match what is actually reported.open3bugNULLNULL2026-08-04T23:32:21ZEugene Blikhbigbes@gmail.com2026-08-04T23:32:21ZNULLNULL0NULLNULLNULL000�{}0NULLNULLNULL0NULL0
ah-1cx.93cd7bb97a8755572aeb1bbbdf902779271b259e483d881fd0040b5a780c3c811[bug] failure comment renders 'exit code 0' and drops the provider's rejection textFound by the independent review of 665e805 (ah-tqc). Since ah-tqc, an error outcome can carry exit code 0. failureComment (internal/reconcile/comments.go:209-214) prints the exit code whenever outcome == domain.OutcomeError, and its own doc comment says 'exit code (only for an error outcome, when the process actually produced one)' — an invariant ah-tqc silently broke. For the exact live incident ah-tqc exists to fix, the operator now sees on the card: 🤖 attempt 1 failed · outcome `error` · exit code 0 with no diff stat (there is none) and nothing else. Meanwhile info.FinalErrorMessage — '401 {"type":"error","error":{"type":"ModelError","message":"Model claude-3-5-haiku is not supported"}}', the one string that makes this diagnosable in five seconds instead of an hour — goes only to the daemon's log.Warn in runner.go finalizeFromExit. The card is the operator-facing surface and the whole reason the bug mattered; 'error, exit code 0' reads like a daemon bug rather than a broken model config.Two changes, one of them cross-package. 1. Gate the exit-code clause in failureComment on exitCode != 0, restoring the invariant its doc comment states. Cheap, local to internal/reconcile/comments.go. 2. Thread FinalErrorMessage to the card. It is parsed in internal/runner (EventStreamInfo.FinalErrorMessage) and needs to reach failureComment, so it crosses ports.RunStatus → domain.Run → the comment. Decide deliberately whether it should also be PERSISTED on the run row (useful for /api/v1/status and for a post-hoc audit) or merely passed through to the comment. Persisting means a store migration; passing through means the information is lost on a refinalize, which reads the stored row rather than st. Truncate it — a provider can return a large body, and SPEC's comment-size discipline applies. Do this AFTER the ah-tqc follow-ups land in internal/runner, since they touch the same struct.A run finalized as error with exit code 0 produces a card comment that names the provider's rejection and does NOT claim an exit code. A run with a genuine non-zero exit still shows it. Covered by a reconcile test asserting both shapes.open2bugNULLNULL2026-08-04T23:42:47ZEugene Blikhbigbes@gmail.com2026-08-04T23:42:47ZNULLNULL0NULLNULLNULL000�{}0NULLNULLNULL0NULL0
ah-1jy8ce9b56a889ec3e5b86e47e140a10d94334118b5b72612654819617614e970a9config: trim yonote.token and mem0.api_key at load like the notifier credentialsLeftover from ah-1cx.3, which trimmed the four notifier strings (ntfy.url/token, telegram.token/api_base) at load so the value an adapter receives is exactly the value present() judged. yonote.token and mem0.api_key were not included. A padded credential passes the new blank scan correctly — it is not blank — but reaches the client with leading or trailing whitespace in the auth header and fails at REQUEST time rather than at load, which is the diagnostic gap ah-1cx.3 set out to close. The realistic source is a ${VAR} expansion picking up a trailing newline from an env file. Also cosmetic, from the same family: resolve compares vikunja.web_url untrimmed when deciding whether to derive it from url. Post-ah-1cx.6 a whitespace-only value errors out instead of silently defeating the derivation, so this is now harmless — it is just the last untrimmed comparison left in the file.A padded yonote.token or mem0.api_key is trimmed at load, and no untrimmed string comparison against "" remains in resolve.open4choreNULLNULL2026-08-04T23:44:00ZEugene Blikhbigbes@gmail.com2026-08-04T23:44:00ZNULLNULL0NULLNULLNULL000�{}0NULLNULLNULL0NULL0
ah-1nuaa28a50dad31c400da81f00695ec62931cbd81a55e02095fdd26864a8b123aceBUG: 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.closed1bugNULLNULL2026-07-17T20:44:03ZEugene Blikhbigbes@gmail.com2026-07-17T21:29:24Z2026-07-17T21:29:24ZNULL0NULLNULLNULL000�{}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.0NULLNULLNULL0NULL0
ah-1qqfc68d9a4ec58cc99b6f5568e9d07512e8df1edd1c60b9b1eeb5c8bfd074d38aaRelease infra: version stamping via ldflags + first tagged releaseThe daemon hardcodes version = dev (cmd/agenthubd/main.go:44) and nothing stamps it: the repo has zero git tags and neither the justfile nor .build.yml passes -ldflags -X main.version. Live agent-1 therefore logs version=dev on every boot and there is no way to tell which build is deployed. Add ldflags stamping (git describe) to the justfile and the CI deploy lane, surface the version in the status API, and cut the first tag. Release-blocking for v0.1.0. Filed 2026-07-20.STAMPING DONE in 1c96c00; only the tag itself remains, and it is deliberately blocked on the rest of milestone v0.1.0 (a milestone is frozen at its tag, so nothing may be added after). What landed: the justfile derives 'git describe --tags --always --dirty' into a version variable and passes -X main.version to go build ./cmd/... — ONE flag stamps all three mains, because -X's 'main' resolves per link. --always degrades to a short sha before the first tag exists, --dirty marks a build made from an uncommitted tree so a hand-built binary can never be mistaken for a release, and a '|| echo dev' arm covers a tree with no git at all. 'just show-version' prints what the tree would stamp. .build.yml does the same and then runs 'ahub version' as a build-time assertion that the stamp actually landed — so a broken stamp fails CI instead of silently deploying a 'dev' binary. ahub-run gained a version var and a --version flag (answered before the required-flag check, since it is a question about the binary not a request to supervise anything): it is the binary that runs INSIDE the pane, so that is how a human attached to a keep-pane shell identifies the build that supervised the run in front of them. GET /api/v1/status now carries the build string via httpapi.Deps.Version, which defaults to 'dev' when empty so a test server or unstamped build never reports blank — a blank version reads as a serialization bug rather than as 'nobody stamped this'. The boot log line is invisible once the journal has rotated, and status is the one surface an operator can reach without shell access to the box. Cover: TestStatusVersion, both arms. Verified end to end locally: just build produced binaries reporting f11ef03-dirty. REMAINING: cut v0.1.0 once the milestone's other beads close, then confirm on agent-1 that the deployed binary reports the tag rather than a sha.in_progress2taskNULLNULL2026-07-20T00:10:40ZEugene Blikhbigbes@gmail.com2026-08-04T23:48:55ZNULLNULL0NULLNULLNULL000�{}0NULLNULLNULL02026-08-04T23:48:55Z0
ah-25e81a248a73cc547b68a28816e4f3c67ea4f5ca77bf4c95f40233a17b35b901880Yonote publish lane: .task/publish.json → bot-authored docs at finalizeWhy: 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}.closed2featureEugene BlikhNULL2026-07-18T15:04:25ZEugene Blikhbigbes@gmail.com2026-07-18T16:09:01Z2026-07-18T15:59:08ZNULL0NULLNULLNULL000�{}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.0NULLNULLNULL02026-07-18T15:06:57Z0
ah-2efff98b88593d96664a9626ff5ba7750acc64df6bde5526cca7d05a55ecc89e4aae2e 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).closed3bugEugene BlikhNULL2026-07-18T17:03:58ZEugene Blikhbigbes@gmail.com2026-07-18T18:15:54Z2026-07-18T18:15:54ZNULL0NULLNULLNULL000�{}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.0NULLNULLNULL02026-07-18T17:28:20Z0
ah-2lh6dae3f01f06b06dcd0357975dff946bac3138df4a0a351273573d592977ecb6eClaim-time Yonote artifact materialization into .task/artifacts/ + prompt manifestWhy: 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).closed2featureEugene BlikhNULL2026-07-18T15:04:08ZEugene Blikhbigbes@gmail.com2026-07-18T15:59:08Z2026-07-18T15:59:08ZNULL0NULLNULLNULL000�{}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.0NULLNULLNULL02026-07-18T15:06:57Z0
ah-4ela7f08ae44a7b072a88ceeb4cbb9b41c058403281fc3018ecd03f004f08069332Review-verdict-driven transition: changes-requested back to Ready for coderEvery 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.closed3featureEugene BlikhNULL2026-07-18T06:30:21ZEugene Blikhbigbes@gmail.com2026-07-18T14:17:36Z2026-07-18T14:17:36ZNULL0NULLNULLNULL000�{}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.0NULLNULLNULL02026-07-18T13:21:51Z0
ah-58fac69891cb925ac8c16f81cb926cc68234fbe2a845af62d30f267e19342797b87ahub tool answer without an outstanding question echoes the latest comment as the answerLive 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.closed3bugEugene BlikhNULL2026-07-18T17:10:20ZEugene Blikhbigbes@gmail.com2026-07-18T17:43:23Z2026-07-18T17:43:23ZNULL0NULLNULLNULL000�{}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.0NULLNULLNULL02026-07-18T17:28:20Z0
ah-6eid903b7798442ca41f9cab7dc12f1e9ded4c222c4860ea1cad13f7311720861ebLive end-to-end verification of designer/validator roles on a real boardah-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.closed3taskEugene BlikhNULL2026-07-17T19:49:21ZEugene Blikhbigbes@gmail.com2026-07-18T06:27:20Z2026-07-18T06:27:20ZNULL0NULLNULLNULL000�{}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.0NULLNULLNULL02026-07-18T05:09:01Z0
ah-6u0aecd5599069260da43810d3d7ad0db775e36028ce4175b282a0cd26f6aa1a911docs: SPEC section 6 RunStatus struct omits MetaAttemptPre-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.closed4taskEugene BlikhNULL2026-07-13T08:10:10ZEugene Blikhbigbes@gmail.com2026-07-13T09:26:46Z2026-07-13T09:26:46ZNULL0NULLNULLNULL000�{}279b733: MetaAttempt added to SPEC section 6 RunStatus listing, alignment verified against gofmt; accuracy-reviewed in the follow-up gate0NULLNULLNULL02026-07-13T08:46:20Z0
ah-943b234e7fe8bc9f64d11011d2b27d1991623feb067d1858d5d2b1d605d27665fd5Refresh phoebe-lab agent-hub provisioning template + bootstrap for designer/validatorThe 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.closed3taskEugene BlikhNULL2026-07-18T06:27:20ZEugene Blikhbigbes@gmail.com2026-07-18T06:41:21Z2026-07-18T06:41:21ZNULL0NULLNULLNULL000�{}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).0NULLNULLNULL02026-07-18T06:40:04Z0
ah-9r4c8e9bb1e135799eacb8565a6f41478d32efa61f0c311dedb27b638a4b6f912c0DECISION: worktree/branch topology for agent-created child tasksBlocks 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).closed2taskNULLNULL2026-07-17T14:37:26ZEugene Blikhbigbes@gmail.com2026-07-17T15:49:21Z2026-07-17T15:49:21ZNULL0NULLNULLNULL000�{}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.0NULLNULLNULL0NULL0
ah-a6f6af2a6590cb99466cc1eb91edd91cc384fee7a0da9d277862587d8eeda63d644e2e: 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.closed3taskNULLNULL2026-07-18T06:54:54ZEugene Blikhbigbes@gmail.com2026-07-18T06:54:54Z2026-07-18T06:54:54ZNULL0NULLNULLNULL000�{}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.0NULLNULLNULL0NULL0
ah-bkr71a63b844de91dc83b0eb9f07dc18564b27c29ebdff9d7337726d421f219f268Vikunja comments render literal markdown fences — post HTML insteadThe 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.closed2bugEugene BlikhNULL2026-07-18T06:30:20ZEugene Blikhbigbes@gmail.com2026-07-18T06:44:12Z2026-07-18T06:44:12ZNULL0NULLNULLNULL000�{}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).0NULLNULLNULL02026-07-18T06:38:47Z0
ah-ddd2b245b73220440977211c08547be4a22c630b6b314adfc53409f16a96a60f414ntfy: 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 syncclosed2featureEugene BlikhNULL2026-07-13T09:44:41ZEugene Blikhbigbes@gmail.com2026-07-13T10:20:36Z2026-07-13T10:20:36ZNULL0NULLNULLNULL000�{}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 exactly0NULLNULLNULL02026-07-13T09:44:55Z0
ah-efeb6624fe8ff78d857e161202ca8ce97b775e0f0165a66178ad1a009f8cf78859fDocs: SPEC refresh to Stage 3-4 + Yonote realityMilestone for documentation drift found by the 2026-07-20 SPEC-vs-implementation audit. SPEC.md declares itself authoritative for Stages 1-2 but its normative sections (domain, ports, HTTP surface, config) were never updated as Stages 3-4, verdict routing, Yonote publish/QA and mem0 landed. All children are update-SPEC chores; no code changes.open3epicNULLNULL2026-07-19T23:37:18ZEugene Blikhbigbes@gmail.com2026-07-19T23:37:18ZNULLNULL0NULLNULLNULL000�{}0NULLNULLNULL0NULL0
ah-efe.1884bcca39d4aaee0fdbe2605220ac5c3264a452b21a1eaf9fff99288358faab6SPEC section 5/6: refresh domain model and port interfaces to delivered realityNormative sections are stale: CanClaim omits ready as a claimable state (transitions.go:19 allows it for the ah-4el verdict-routing re-claim path); the State const block is missing blocked/question; TaskRecord lacks AutoRoutes, RunSummary lacks Summary/Verdict; RequestedTask/QARequest/PublishDoc types are absent; Board is missing CreateTask/Comments/SwapLabel, Runner missing RequestedTasks/AskedQuestion/Answer/PublishRequests/ArchiveWorktree, Store missing the lineage/kv/qa methods, StartSpec gained Artifacts+ToolToken. Sync both sections to internal/domain + internal/ports or add an explicit Stage 3-4 additions subsection. SPEC-gap audit 2026-07-20.closed3choreNULLNULL2026-07-19T23:38:08ZEugene Blikhbigbes@gmail.com2026-08-04T23:50:20Z2026-08-04T23:50:20ZNULL0NULLNULLNULL000�{}Done in 119b84d. §5/§6 contradicted nothing in §12 but were roughly two stages behind the code. §5's State block was missing StateBlocked and StateQuestion (the two Stage-4 real states); the RunState* and Verdict* const blocks were absent; TaskRecord lacked AutoRoutes; RunSummary lacked Summary/Verdict; and RequestedTask, QARequest and PublishDoc did not appear at all — every one now does, field-for-field against internal/domain/types.go. OutcomeError's comment still read 'exit != 0', which 665e805 (ah-tqc) falsified: a zero exit contradicted by the event stream's last message_end stopReason 'error' now also produces error, so §5 was contradicting the §9 prose that same commit added. Corrected, and Run.State now names the RunState* constants instead of bare strings. The CanClaim paragraph omitted 'ready' — claimable since the ah-4el verdict-routing re-claim path — so it now lists the full claimable set, names the deliberately-unclaimable complement {in_progress, blocked, question} and why, and mentions States()/Valid()/CanonicalBuckets(). §6's preamble claimed reconcile imports only domain/ports/config/spec; it now records the three Stage-3/4 additions (ctxpack, mem0, yonote) and why none is a port. Board gained CreateTask/Comments/SwapLabel plus NewTask; StartSpec gained Artifacts and ToolToken plus the Artifact type; Runner gained RequestedTasks/AskedQuestion/Answer/PublishRequests (documented as one zero-value-means-not-requested family) plus ArchiveWorktree and ArchiveResult; Store gained the lineage (RecordChild/GenerationOf/ParentOf/ChildIDsOf), kv (GetKV/SetKV) and Q&A (CreateQA/QAByTask/QAByComment/MarkQAAnswered) methods. §5 principle 5 was already correct and left alone. Every signature and comment is traceable to internal/domain/ or internal/ports/ as of this commit.0NULLNULLNULL02026-08-04T23:39:08Z0
ah-efe.2ee6d9e1db341436bcc0f1713a251ccfa45fc1d47a01f9dbd02f7f0efcd7caef2SPEC section 12: config surface missing Stage 3-4 + Yonote blocks; ask-user wrongly marked future workSection 12 lacks archive_dir, task_types on_success/on_changes_requested + routing.max_auto_routes, agentsview, yonote (claim_budget, publish_collection_id, nested qa block), mem0, tools_api, and the designer/validator/fixer roles — all shipped and validated in internal/config + config.example.yaml (which agree with each other; only SPEC lags). Also line 784 still calls the ahub tool ask-user CLI-over-HTTP variant future work, directly contradicting delivered ah-0ge.6 and SPEC section 5 principle 5 — delete the stale parenthetical. SPEC-gap audit 2026-07-20.closed3choreNULLNULL2026-07-19T23:38:12ZEugene Blikhbigbes@gmail.com2026-08-04T23:38:07Z2026-08-04T23:38:07ZNULL0NULLNULLNULL000�{}Done in 54b2176. Section 12 had drifted ~2 stages behind the code. ADDED: archive_dir; the on_success/on_changes_requested routing keys and routing.max_auto_routes; the designer/validator/fixer roles and their design→review→fix task types; and the whole agentsview, yonote (claim_budget, publish_collection_id, nested qa), mem0 and tools_api blocks — all of which already existed in internal/config and config.example.yaml, which agreed with each other. Each got its own validation bullet with the real default (archive_dir → <work_root>/archive, max_auto_routes → 4, claim_budget → 90s, qa.bucket → ready, qa.poll_interval → 60s, tools_api → ON via a *bool), plus a lead-in naming the shared presence pattern the optional integration blocks follow. CORRECTED two outright errors: the parenthetical calling the ahub tool ask-user CLI-over-HTTP variant future work (it shipped in 07bd9d4/03f4496 — the file channel and the mid-run channel are now presented as two transports over identical board state), and the bucket-map comment calling blocked/question parked when §10 has made them real states since Stage 4 (triage is now the one parked bucket, map reordered to match domain.CanonicalBuckets()). NEW PROSE on what the config gates: the mid-run tools API, the three Yonote lanes, the mem0 lifecycle, and the Context Pack documented as deliberately having NO config surface (order and budgets are package constants). Defaults spot-verified against internal/config/config.go before commit.0NULLNULLNULL02026-08-04T23:32:44Z0
ah-efe.363b0d2b6e60ab09dddf7f4c58bb8e06ab9fd1fd7f80ce807c8f484e29595c40fSPEC sections 1/4/11: external-services table, repo layout tree, HTTP surface sweepLow-priority staleness bundle: section 1 services table omits ntfy/Prism and Yonote; section 4 layout tree omits internal/mem0, ctxpack, yonote, deps; section 11 (and the httpapi package doc-comment) lists only healthz/run-exit/status/webhook, missing the three Stage-4 /api/tool/* routes. One sweep commit. SPEC-gap audit 2026-07-20.Concrete §11 defects found during the §12 refresh (ah-efe.2), to fold into this bead's work: - The §11 heading says 'Stages 1-2' but internal/httpapi now also serves the Stage 4 tools surface. - The endpoint list omits POST /api/tool/task-create, POST /api/tool/ask-user and GET /api/tool/answer entirely. Document that they are BEARER-authenticated (unlike /api/v1/status, which stays unauthenticated and loopback-trusted) and that they are registered only when Deps.Tools is wired — i.e. only when tools_api is on. Nothing contradicting §12 was found in §5/§6 (ah-efe.1); §5 principle 5 ('Agent-facing tools are CLI-over-HTTP (Stage 4)') is consistent with the delivered surface. §9 and §14 defects were split into ah-efe.5.closed4choreNULLNULL2026-07-19T23:38:15ZEugene Blikhbigbes@gmail.com2026-08-04T23:50:04Z2026-08-04T23:50:04ZNULL0NULLNULLNULL000�{}Done in 119b84d. §1's services table omitted the ntfy/Prism gateway and Yonote, and described AgentsView's auth as 'pg push from client side' without saying the daemon never calls it. The table now has eight rows ordered by stage, distinguishes Telegram's token-in-URL from ntfy's token-in-header (the §15 redaction distinction), marks Yonote as a BOT token at 1.47.1, and a new paragraph states that only Vikunja is mandatory, that telegram/ntfy are the mutually exclusive notifier options, and that AgentsView receives NOTHING from agenthubd — the daemon only renders <base_url>/sessions/<machine>~pi:<id> links. §4's tree was missing internal/mem0, internal/ctxpack, internal/yonote and internal/deps, still showed cmd/ahub/main.go alone, and omitted skills/; all added, with a new paragraph explaining that mem0/ctxpack/yonote are deliberately NOT ports (pure renderer; optional single-consumer clients consumed via local interfaces in reconcile), and the dependency note corrected to include go.bigb.es/auxilia and testify while confirming Stages 2-4 added no new external dependency. §11's heading claimed 'Stages 1-2' and its list stopped at four routes; it now covers all seven, documents POST /api/tool/task-create, POST /api/tool/ask-user and GET /api/tool/answer with their real bodies and status codes, states that all three are BEARER-authenticated with the per-task token (constant-time compare, uniform 401 that echoes nothing) while GET /api/v1/status stays unauthenticated and loopback-trusted, and that they register only when Deps.Tools is wired — i.e. only when tools_api is on, the same gate shape as webhook_secret. The endpoint list was swept against the real mux.HandleFunc registrations; no other route exists. Also fixed §3's binaries table, which still described ahub tool as a future subcommand, and folded in ah-1qq's new status version field. UNVERIFIED, left alone: §1 lists sourcehut as git@git.srht.bigb.es while §12's example review_remote is git@srht.bigb.es:~bigbes/demo-repo — one of the two is wrong and it needs an operator to say which.0NULLNULLNULL02026-08-04T23:39:08Z0
ah-efe.4dad303fab4c573330090b8655ae49dba0cde42ce8308851bcc37c0007cbb7d7aFill CLAUDE.md Build/Architecture/Conventions placeholder sectionsProject CLAUDE.md still carries three unedited scaffold stubs (Build and Test, Architecture Overview, Conventions and Patterns) while the real commands live in AGENTS.md and the justfile. Populate Build and Test from the justfile, add the one-line architecture summary from SPEC section 1, point Conventions at AGENTS.md. Remember the AGENTS.md/CLAUDE.md mirror rule (independent files, not symlinked). Marker-sweep audit 2026-07-20.closed4choreNULLNULL2026-07-19T23:38:17ZEugene Blikhbigbes@gmail.com2026-08-04T23:37:15Z2026-08-04T23:37:15ZNULL0NULLNULLNULL000�{}Done in 199d85e. Both files were untracked bd-setup leftovers; now tracked. CLAUDE.md's three scaffold stubs are filled and mirrored into AGENTS.md: Build and Test from the justfile plus the GOFLAGS=-mod=mod go.sum trap this dev env has; Architecture Overview as the reconciler-first summary from SPEC section 1 with docs/SPEC.md named authoritative; Conventions recording the practices the repo actually follows (one bead per change with the durable record in its notes, the [bead-id] commit-message suffix, the deliberately high comment density, the milestone-is-a-release rule). The bd-generated managed blocks in both files were left untouched, and the two files remain independent rather than symlinked.0NULLNULLNULL0NULL0
ah-efe.5fa3feda20fc817a1ec9da6732fd71d84828eef584830e729b63d4fd5a5963723SPEC sections 9 and 14: .task/ layout stale, Stage 3-4 still written as roadmapFound while refreshing §12 (ah-efe.2). Neither section is covered by the existing ah-efe children (.1 covers §5/6, .3 covers §1/4/11), so they would silently stay stale. SECTION 9 (runner): - The .task/ layout comment at ~line 396 reads 'question.json # reserved for Stage 4 ask-user'. Stage 4 ask-user is DELIVERED; the file is live, not reserved. - The same listing omits five files the code actually writes or reads: tasks.json, summary.md, publish.json, answer.json, and tool-auth.json (mode 0600, written by internal/runner/taskfiles.go). SECTION 14 (roadmap): - Stages 3 and 4 are still phrased as forward-looking roadmap items while Stage 2 is marked 'Delivered'. The mem0 lifecycle, Context Pack, agent tools API, AgentsView deep links and the Yonote lanes have all shipped; the stage markers need the same treatment Stage 2's got. - The Stage 4 item promises 'ask-user→Question loop with pi --session resumes'. The delivered mid-run path instead keeps the process alive by BLOCKING in the CLI (2s polls until --deadline), so that phrasing describes a superseded design and should be corrected rather than just re-marked as done.§9's .task/ listing matches what internal/runner/taskfiles.go actually writes, with no 'reserved' markers on delivered files. §14's Stage 3 and Stage 4 entries carry accurate delivered/not-delivered markers, and the ask-user description matches the blocking-CLI design that actually shipped.closed3choreNULLNULL2026-08-04T23:38:22ZEugene Blikhbigbes@gmail.com2026-08-04T23:49:49Z2026-08-04T23:49:49ZNULL0NULLNULLNULL000�{}Done in 119b84d. §9's .task/ listing was three files short of reality and mislabelled a delivered one: question.json was marked 'reserved for Stage 4' although the ask-user file channel has shipped, and tasks.json, summary.md, publish.json, answer.json and tool-auth.json were absent entirely. Verified against internal/runner/taskfiles.go and runner.go prepareTaskDir; the listing now carries all eleven entries plus the two directories the code actually manages (artifacts/, publish/), with tool-auth.json marked mode 0600 and written only when a token was minted. A new paragraph states the inbound/supervisor/outbound split, that a missing outbound file always means 'not requested' rather than an error, and that Start clears every attempt-scoped file (stale exit.json, all outbound files, publish/, artifacts/, tool-auth.json) and truncates events.jsonl — with the concrete failure each clearing prevents. §14's Stage 3 and Stage 4 were still forward-looking roadmap while Stage 2 read 'Delivered'; both are now marked delivered in Stage 2's style. Two roadmap promises are CORRECTED rather than ticked off: (1) the post-run summarizer role was NEVER BUILT — no such role or prompt exists; the shipped design substitutes agent-authored .task/summary.md plus mem0's infer=true extraction, and accepting that substitution is the open decision on ah-ydx; (2) 'ask-user→Question loop with pi --session resumes' was a superseded design — there is no pi --session resume anywhere: the file channel parks in Question and a human answer starts a NEW attempt with a freshly rendered prompt, while the mid-run path never stops the run at all (ahub tool ask-user blocks in the CLI polling GET /api/tool/answer every 2s until --deadline, default 5m). Stage 4 also now states plainly that the roadmap's memory search/add tools do not exist and nothing tracks them, and that there is no content-level dedup of requested children.0NULLNULLNULL02026-08-04T23:39:08Z0
ah-eje3e29cf27b2ca9b1b2c758464fedcaa74ec70e478c94f99c19a2161f68613f767Q&A loop rollout: live-verify events.list ordering + comments.resolve, wire qa role/config on agent-1ah-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.closed2taskEugene BlikhNULL2026-07-18T18:47:44ZEugene Blikhbigbes@gmail.com2026-07-19T18:21:38Z2026-07-19T18:21:38ZNULL0NULLNULLNULL000�{}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.0NULLNULLNULL02026-07-18T20:04:59Z0
ah-gs7c0bcd671b99d03b8fb704cffc0b49ffe839b72cbf1a7a9bd61ccaf8166cc850eUltrapack 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.closed2featureEugene BlikhNULL2026-07-17T14:37:25ZEugene Blikhbigbes@gmail.com2026-07-17T19:49:23Z2026-07-17T19:49:23ZNULL0NULLNULLNULL000�{}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.0NULLNULLNULL02026-07-17T19:24:42Z0