main · last commit
13 days ago ·
7g0stsfu
ah-25e Yonote publish lane: .task/publish.json → bot-authored docs at finalize
Past Stand
bd reopen ah-25e
| Created by | Eugene Blikh |
| Owner | bigbes@gmail.com |
| Created | 2026-07-18T15:04:25Z |
| Started | 2026-07-18T15:06:57Z |
| Updated | 2026-07-18T16:09:01Z |
| Closed | 2026-07-18T15:59:08Z |
Why: the operator wants agents to publish specs/reports/answers INTO Yonote instead of committing them to the project repo — with the write credential never entering the agent's environment. What: a third finalize-time file channel in the SPEC §14.4 family (tasks.json, question.json): the agent writes markdown under .task/publish/ and declares it in .task/publish.json {documents:[{title, file, collection_id?, parent_url?}]}; on successful finalize the daemon creates the docs via the internal/yonote client under the BOT token (publish:true, target = yonote.publish_collection_id or per-entry override, optional nesting under a referenced doc), comments the resulting URLs on the card, emits a yonote_published event. Publish failures never change the run outcome. wave 2.5 — implement after feat/wave2-archive-links merges. Depends on ah-gxa (client: CreateDocument/DocumentInfo) and ah-2lh (yonote config block + prepareTaskDir channel-clearing site).
# Yonote publish lane: .task/publish.json → bot-authored docs at finalize
Agents publish specs/reports/answers INTO Yonote instead of committing them to the project repo.
The agent declares intent via a file channel; the daemon executes with ITS bot token at finalize
and links results on the card.
## Why file-channel, not direct API access from the agent (DECISION)
The agent must never hold the Yonote token: worktrees run arbitrary generated code; `.task/` is
archived to tar.gz at Done (a token in env or files would fossilize into archives); per-run env
plumbing would put a write-capable credential into every zellij session. The daemon already owns a
finalize-time file-channel protocol with exactly the right semantics — `.task/tasks.json`
(delegation) and `.task/question.json` (ask-user), SPEC §14.4: agent writes declarative intent,
daemon acts with its own credentials, results land as card comments. Publishing is the third
channel of that family. Attribution lands on the BOT user — uniform and auditable.
## Channel format
`.task/publish.json` (attempt-scoped; cleared in prepareTaskDir alongside summary/tasks/question):
{"documents": [
{"title": "Design: frobnicator", "file": "publish/design.md",
"collection_id": "", "parent_url": ""}
]}
- title: required, non-empty, ≤255 chars (Yonote bot/user name limits suggest 255 conventions).
- file: required; RELATIVE path resolved under `.task/` (convention: agents put content in
`.task/publish/<name>.md`). Guard rigor as repo-slug F8: reject absolute paths, filepath.Clean
must stay under .task/, and the opened file must not escape via symlink (os.Root / EvalSymlinks
containment check).
- collection_id: optional uuid overriding the config default target.
- parent_url: optional Yonote doc URL → resolve via yonote.ParseDocRefs(single) + DocumentInfo →
parentDocumentId, so a report can nest under the spec it answers (documents.create
parentDocumentId is v1-spec'd; child creation itself NOT live-verified — verify in tests against
httptest only, live during rollout).
- Caps: maxPublishDocs = 3 per run; maxPublishBytes = 512 KiB per file. No server-side text limit
was verified — cap defensively; an oversize entry FAILS (listed in the comment), never truncated.
Read at SUCCESSFUL finalize only, same site and semantics as readTasksFile: absent → nil (common
case); present-but-unparseable → same bounce treatment as a malformed tasks.json (do not silently
drop agent intent).
## Config
The `yonote` block (introduced by the materialization bead) gains:
publish_collection_id: "…" # uuid; the default target collection
Validation: when set, must be a UUID shape; lane is ACTIVE only when the yonote client is
configured AND publish_collection_id is set. When the client is configured but the collection is
not, publish.json entries all fail visibly with "publishing not configured" in the card comment —
never silently ignored. Operational prerequisite (document next to the config): the BOT must have
read_write on the target collection (collections.add_user — client-bead runbook step 3).
## Daemon flow (finalize, success path, after summary/tasks/question handling)
Per entry (≤3, in file order): resolve target collection (override or default) → resolve
parent_url if set (its failure fails ONLY that entry) → read + cap content →
CreateDocument{Title, CollectionID, ParentDocumentID, Text: content, Publish: true} → record
{Title, absURL = cfg base_url + doc.URL} or {Title, err}.
- Card comment (goes through the existing markdownToHTML comment path):
published to Yonote:
- <Title> → <abs url>
- <Title> → FAILED: <message>
- appendEvent "yonote_published" {"ok": n, "failed": m}.
- Publish failures NEVER change the run outcome — the run already succeeded; publication is a
side effect. Operator retries by re-running, or publishes manually from the archived tar.gz
(the content survives there by construction).
- Idempotency (DECISION): re-running a card re-publishes and creates a NEW doc. Accepted for v1 —
re-runs are rare, docs are cheap, and dedup would need a doc-id echo channel; revisit only if it
annoys in practice (then: UpdateDocument by echoed id).
## Agent contract (role-prompt snippet, config-side; document in deploy notes)
To publish a document to Yonote: write markdown to .task/publish/<name>.md and declare it in
.task/publish.json as {"documents":[{"title":"…","file":"publish/<name>.md"}]}. Publication
happens AFTER your run succeeds; resulting links are posted on the task card.
## Tests
- reader: absent → nil; malformed → tasks.json-parity bounce; >3 entries; empty title; path
guards (absolute, .., symlink escape); oversize file.
- finalize with fake yonote writer (interface seam: CreateDocument + DocumentInfo): 2 ok + 1 fail
→ comment body lines, event payload, run outcome unchanged; lane-off (no collection id) →
"publishing not configured" per entry; parent_url resolve failure isolates the entry;
collection_id override honored.
- prepareTaskDir clears publish.json + the publish/ dir between attempts.
wave 2.5 — implement after feat/wave2-archive-links merges. Depends on: internal/yonote client
bead (CreateDocument/DocumentInfo), materialization bead (yonote config block + prepareTaskDir
channel-clearing site).
ROLLOUT LIVE-SMOKE CHECKLIST (from wave-2.5 implementer, against bigbes.yonote.ru after deploy): 1) mint bot per ah-gxa runbook (POST /api/v2/bots + token, collections.add_user grant — grant call is spec-only, NOT live-verified), AuthInfo under bot token must show IsBot=true (daemon WARNs at startup otherwise); 2) documents.create with parentDocumentId (child nesting unverified live) + bot WRITE rights on publish_collection_id + publish:true attribution to bot; 3) archived-doc export ((archived) marker path); 4) ExportMarkdown empty-string cases (API-created never-edited draft, database-type doc) fall back to .text; 5) comments.create/list/resolve field names (only Q&A bead ah-ptu uses them). SEMANTICS NOTE: publish fires on ROUTED successful runs too (inside firstFinalize&&success before maybeRoute) — a design stage with publish.json publishes before routing to review; intentional. publish_collection_id (UUID) must be chosen by operator at config staging. DEPENDS ON → ✓ ah-2lh: Claim-time Yonote artifact materialization into .task/artifacts/ + prompt manifest ● P2 → ✓ ah-gxa: internal/yonote: light API client (doc resolve/export, create, comments; bot-token auth) ● P2 BLOCKS ← ○ ah-ptu: Yonote Q&A bot loop: poll doc comments, answer via board tasks (post-wave-2.5) ● P3 ROLLOUT FACTS (2026-07-18, live-verified): bot agenthub id=34fbc9a3-ec75-4722-a01f-805db3f3ff1b (v2 create needs username field — used bigbes@gmail.com); token agent-1-daemon installed as YONOTE_TOKEN in /etc/agent-hub/env; auth.info isBot=true CONFIRMED; collections.add_user CONFIRMED; Agents collection 8656642e-0297-4d69-b6fb-4246517a015b = publish_collection_id, bot read_write. CAVEAT: bot sees ALL team-default-readable collections (not grant-scoped) — operator may tighten per-collection perms in UI. Config staging after CI deploy: yonote{base_url: https://bigbes.yonote.ru, token: ${YONOTE_TOKEN}, publish_collection_id: 8656642e-0297-4d69-b6fb-4246517a015b}.
ah-ptu
— Yonote Q&A bot loop: poll doc comments, answer via board tasks (post-wave-2.5)
blocks
ah-2lh
— Claim-time Yonote artifact materialization into .task/artifacts/ + prompt manifest
blocks
closed
ah-gxa
— internal/yonote: light API client (doc resolve/export, create, comments; bot-token auth)
blocks
closed
| id | ah-25e |
| content_hash | 81a248a73cc547b68a28816e4f3c67ea4f5ca77bf4c95f40233a17b35b901880 |
| title | Yonote publish lane: .task/publish.json → bot-authored docs at finalize |
| description | Why: the operator wants agents to publish specs/reports/answers INTO Yonote instead of committing them to the project repo — with the write credential never entering the agent's environment. What: a third finalize-time file channel in the SPEC §14.4 family (tasks.json, question.json): the agent writes markdown under .task/publish/ and declares it in .task/publish.json {documents:[{title, file, collection_id?, parent_url?}]}; on successful finalize the daemon creates the docs via the internal/yonote client under the BOT token (publish:true, target = yonote.publish_collection_id or per-entry override, optional nesting under a referenced doc), comments the resulting URLs on the card, emits a yonote_published event. Publish failures never change the run outcome. wave 2.5 — implement after feat/wave2-archive-links merges. Depends on ah-gxa (client: CreateDocument/DocumentInfo) and ah-2lh (yonote config block + prepareTaskDir channel-clearing site). |
| design | # 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). |
| acceptance_criteria | |
| notes | 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}. |
| status | closed |
| priority | 2 |
| issue_type | feature |
| assignee | Eugene Blikh |
| estimated_minutes | NULL |
| created_at | 2026-07-18T15:04:25Z |
| created_by | Eugene Blikh |
| owner | bigbes@gmail.com |
| updated_at | 2026-07-18T16:09:01Z |
| closed_at | 2026-07-18T15:59:08Z |
| closed_by_session | |
| external_ref | NULL |
| spec_id | |
| compaction_level | 0 |
| compacted_at | NULL |
| compacted_at_commit | NULL |
| original_size | NULL |
| sender | |
| ephemeral | 0 |
| wisp_type | |
| pinned | 0 |
| is_template | 0 |
| mol_type | |
| work_type | |
| source_system | |
| metadata | �{} |
| source_repo | |
| close_reason | 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. |
| event_kind | |
| actor | |
| target | |
| payload | |
| await_type | |
| await_id | |
| timeout_ns | 0 |
| waiters | |
| hook_bead | |
| role_bead | |
| agent_state | |
| last_activity | NULL |
| role_type | |
| rig | |
| due_at | NULL |
| defer_until | NULL |
| no_history | 0 |
| started_at | 2026-07-18T15:06:57Z |
| is_blocked | 0 |
| id | 56546f03-0823-5534-84f8-a1d919c5ce0b |
| issue_id | ah-25e |
| type | blocks |
| created_at | 2026-07-18T18:04:56Z |
| created_by | Eugene Blikh |
| metadata | �{} |
| thread_id | |
| depends_on_issue_id | ah-2lh |
| depends_on_wisp_id | NULL |
| depends_on_external | NULL |
| id | 5b6045e4-6bd4-5f00-bb39-d36a8c263ace |
| issue_id | ah-25e |
| type | blocks |
| created_at | 2026-07-18T18:04:56Z |
| created_by | Eugene Blikh |
| metadata | �{} |
| thread_id | |
| depends_on_issue_id | ah-gxa |
| depends_on_wisp_id | NULL |
| depends_on_external | NULL |
| id | 76375cfe-53a8-5c6d-ba68-4de57738ddc7 |
| issue_id | ah-ptu |
| type | blocks |
| created_at | 2026-07-18T18:04:57Z |
| created_by | Eugene Blikh |
| metadata | �{} |
| thread_id | |
| depends_on_issue_id | ah-25e |
| depends_on_wisp_id | NULL |
| depends_on_external | NULL |
| id | 019f75c1-d49a-7ae1-a057-bc520be235ee |
| issue_id | ah-25e |
| event_type | created |
| actor | Eugene Blikh |
| old_value | |
| new_value | |
| comment | NULL |
| created_at | 2026-07-18T18:04:24Z |
| id | 019f75c4-2645-733c-93ff-b573473c0544 |
| issue_id | ah-25e |
| event_type | claimed |
| actor | Eugene Blikh |
| old_value | {"id":"ah-25e","title":"Yonote publish lane: .task/publish.json → bot-authored docs at finalize","description":"Why: the operator wants agents to publish specs/reports/answers INTO Yonote instead of committing them to the project repo — with the write credential never entering the agent's environment. What: a third finalize-time file channel in the SPEC §14.4 family (tasks.json, question.json): the agent writes markdown under .task/publish/ and declares it in .task/publish.json {documents:[{title, file, collection_id?, parent_url?}]}; on successful finalize the daemon creates the docs via the internal/yonote client under the BOT token (publish:true, target = yonote.publish_collection_id or per-entry override, optional nesting under a referenced doc), comments the resulting URLs on the card, emits a yonote_published event. Publish failures never change the run outcome. wave 2.5 — implement after feat/wave2-archive-links merges. Depends on ah-gxa (client: CreateDocument/DocumentInfo) and ah-2lh (yonote config block + prepareTaskDir channel-clearing site).","design":"# Yonote publish lane: .task/publish.json → bot-authored docs at finalize\n\nAgents publish specs/reports/answers INTO Yonote instead of committing them to the project repo.\nThe agent declares intent via a file channel; the daemon executes with ITS bot token at finalize\nand links results on the card.\n\n## Why file-channel, not direct API access from the agent (DECISION)\n\nThe agent must never hold the Yonote token: worktrees run arbitrary generated code; `.task/` is\narchived to tar.gz at Done (a token in env or files would fossilize into archives); per-run env\nplumbing would put a write-capable credential into every zellij session. The daemon already owns a\nfinalize-time file-channel protocol with exactly the right semantics — `.task/tasks.json`\n(delegation) and `.task/question.json` (ask-user), SPEC §14.4: agent writes declarative intent,\ndaemon acts with its own credentials, results land as card comments. Publishing is the third\nchannel of that family. Attribution lands on the BOT user — uniform and auditable.\n\n## Channel format\n\n`.task/publish.json` (attempt-scoped; cleared in prepareTaskDir alongside summary/tasks/question):\n\n {\"documents\": [\n {\"title\": \"Design: frobnicator\", \"file\": \"publish/design.md\",\n \"collection_id\": \"\", \"parent_url\": \"\"}\n ]}\n\n- title: required, non-empty, ≤255 chars (Yonote bot/user name limits suggest 255 conventions).\n- file: required; RELATIVE path resolved under `.task/` (convention: agents put content in\n `.task/publish/\u003cname\u003e.md`). Guard rigor as repo-slug F8: reject absolute paths, filepath.Clean\n must stay under .task/, and the opened file must not escape via symlink (os.Root / EvalSymlinks\n containment check).\n- collection_id: optional uuid overriding the config default target.\n- parent_url: optional Yonote doc URL → resolve via yonote.ParseDocRefs(single) + DocumentInfo →\n parentDocumentId, so a report can nest under the spec it answers (documents.create\n parentDocumentId is v1-spec'd; child creation itself NOT live-verified — verify in tests against\n httptest only, live during rollout).\n- Caps: maxPublishDocs = 3 per run; maxPublishBytes = 512 KiB per file. No server-side text limit\n was verified — cap defensively; an oversize entry FAILS (listed in the comment), never truncated.\n\nRead at SUCCESSFUL finalize only, same site and semantics as readTasksFile: absent → nil (common\ncase); present-but-unparseable → same bounce treatment as a malformed tasks.json (do not silently\ndrop agent intent).\n\n## Config\n\nThe `yonote` block (introduced by the materialization bead) gains:\n\n publish_collection_id: \"…\" # uuid; the default target collection\n\nValidation: when set, must be a UUID shape; lane is ACTIVE only when the yonote client is\nconfigured AND publish_collection_id is set. When the client is configured but the collection is\nnot, publish.json entries all fail visibly with \"publishing not configured\" in the card comment —\nnever silently ignored. Operational prerequisite (document next to the config): the BOT must have\nread_write on the target collection (collections.add_user — client-bead runbook step 3).\n\n## Daemon flow (finalize, success path, after summary/tasks/question handling)\n\nPer entry (≤3, in file order): resolve target collection (override or default) → resolve\nparent_url if set (its failure fails ONLY that entry) → read + cap content →\nCreateDocument{Title, CollectionID, ParentDocumentID, Text: content, Publish: true} → record\n{Title, absURL = cfg base_url + doc.URL} or {Title, err}.\n\n- Card comment (goes through the existing markdownToHTML comment path):\n published to Yonote:\n - \u003cTitle\u003e → \u003cabs url\u003e\n - \u003cTitle\u003e → FAILED: \u003cmessage\u003e\n- appendEvent \"yonote_published\" {\"ok\": n, \"failed\": m}.\n- Publish failures NEVER change the run outcome — the run already succeeded; publication is a\n side effect. Operator retries by re-running, or publishes manually from the archived tar.gz\n (the content survives there by construction).\n- Idempotency (DECISION): re-running a card re-publishes and creates a NEW doc. Accepted for v1 —\n re-runs are rare, docs are cheap, and dedup would need a doc-id echo channel; revisit only if it\n annoys in practice (then: UpdateDocument by echoed id).\n\n## Agent contract (role-prompt snippet, config-side; document in deploy notes)\n\n To publish a document to Yonote: write markdown to .task/publish/\u003cname\u003e.md and declare it in\n .task/publish.json as {\"documents\":[{\"title\":\"…\",\"file\":\"publish/\u003cname\u003e.md\"}]}. Publication\n happens AFTER your run succeeds; resulting links are posted on the task card.\n\n## Tests\n\n- reader: absent → nil; malformed → tasks.json-parity bounce; \u003e3 entries; empty title; path\n guards (absolute, .., symlink escape); oversize file.\n- finalize with fake yonote writer (interface seam: CreateDocument + DocumentInfo): 2 ok + 1 fail\n → comment body lines, event payload, run outcome unchanged; lane-off (no collection id) →\n \"publishing not configured\" per entry; parent_url resolve failure isolates the entry;\n collection_id override honored.\n- prepareTaskDir clears publish.json + the publish/ dir between attempts.\n\nwave 2.5 — implement after feat/wave2-archive-links merges. Depends on: internal/yonote client\nbead (CreateDocument/DocumentInfo), materialization bead (yonote config block + prepareTaskDir\nchannel-clearing site).\n","notes":"Decision record: direct agent→Yonote API access was REJECTED — worktrees run arbitrary generated code, .task/ fossilizes into Done-archive tar.gz (a token in env/files would be archived), and the existing file-channel protocol (runner/taskfiles.go readTasksFile/readQuestionFile, attempt-scoped clearing in prepareTaskDir) already gives declarative-intent + daemon-credential semantics with card-comment observability. API evidence (ah-gxa notes, one-probe lifecycle): documents.create stores submitted markdown VERBATIM (create response echoed text byte-identical) and returns the site-relative url for the card comment (abs = base_url + url); publish:false honored (draft), so publish:true is explicit; comments.create/list verified for the future reply path. NOT live-verified: parentDocumentId child creation (v1-spec'd; exercise via httptest now, live during rollout), server-side max document text size (none documented — hence the defensive 512 KiB per-file cap), bot write rights on a granted collection (provisioning-time check, ah-gxa runbook step 4).","status":"open","priority":2,"issue_type":"feature","owner":"bigbes@gmail.com","created_at":"2026-07-18T15:04:25Z","created_by":"Eugene Blikh","updated_at":"2026-07-18T15:04:25Z"} |
| new_value | {"assignee":"Eugene Blikh","status":"in_progress"} |
| comment | NULL |
| created_at | 2026-07-18T18:06:56Z |
| id | 019f75f3-e70e-76a7-86e2-16953fca1473 |
| issue_id | ah-25e |
| event_type | updated |
| actor | Eugene Blikh |
| old_value | {"id":"ah-25e","title":"Yonote publish lane: .task/publish.json → bot-authored docs at finalize","description":"Why: the operator wants agents to publish specs/reports/answers INTO Yonote instead of committing them to the project repo — with the write credential never entering the agent's environment. What: a third finalize-time file channel in the SPEC §14.4 family (tasks.json, question.json): the agent writes markdown under .task/publish/ and declares it in .task/publish.json {documents:[{title, file, collection_id?, parent_url?}]}; on successful finalize the daemon creates the docs via the internal/yonote client under the BOT token (publish:true, target = yonote.publish_collection_id or per-entry override, optional nesting under a referenced doc), comments the resulting URLs on the card, emits a yonote_published event. Publish failures never change the run outcome. wave 2.5 — implement after feat/wave2-archive-links merges. Depends on ah-gxa (client: CreateDocument/DocumentInfo) and ah-2lh (yonote config block + prepareTaskDir channel-clearing site).","design":"# Yonote publish lane: .task/publish.json → bot-authored docs at finalize\n\nAgents publish specs/reports/answers INTO Yonote instead of committing them to the project repo.\nThe agent declares intent via a file channel; the daemon executes with ITS bot token at finalize\nand links results on the card.\n\n## Why file-channel, not direct API access from the agent (DECISION)\n\nThe agent must never hold the Yonote token: worktrees run arbitrary generated code; `.task/` is\narchived to tar.gz at Done (a token in env or files would fossilize into archives); per-run env\nplumbing would put a write-capable credential into every zellij session. The daemon already owns a\nfinalize-time file-channel protocol with exactly the right semantics — `.task/tasks.json`\n(delegation) and `.task/question.json` (ask-user), SPEC §14.4: agent writes declarative intent,\ndaemon acts with its own credentials, results land as card comments. Publishing is the third\nchannel of that family. Attribution lands on the BOT user — uniform and auditable.\n\n## Channel format\n\n`.task/publish.json` (attempt-scoped; cleared in prepareTaskDir alongside summary/tasks/question):\n\n {\"documents\": [\n {\"title\": \"Design: frobnicator\", \"file\": \"publish/design.md\",\n \"collection_id\": \"\", \"parent_url\": \"\"}\n ]}\n\n- title: required, non-empty, ≤255 chars (Yonote bot/user name limits suggest 255 conventions).\n- file: required; RELATIVE path resolved under `.task/` (convention: agents put content in\n `.task/publish/\u003cname\u003e.md`). Guard rigor as repo-slug F8: reject absolute paths, filepath.Clean\n must stay under .task/, and the opened file must not escape via symlink (os.Root / EvalSymlinks\n containment check).\n- collection_id: optional uuid overriding the config default target.\n- parent_url: optional Yonote doc URL → resolve via yonote.ParseDocRefs(single) + DocumentInfo →\n parentDocumentId, so a report can nest under the spec it answers (documents.create\n parentDocumentId is v1-spec'd; child creation itself NOT live-verified — verify in tests against\n httptest only, live during rollout).\n- Caps: maxPublishDocs = 3 per run; maxPublishBytes = 512 KiB per file. No server-side text limit\n was verified — cap defensively; an oversize entry FAILS (listed in the comment), never truncated.\n\nRead at SUCCESSFUL finalize only, same site and semantics as readTasksFile: absent → nil (common\ncase); present-but-unparseable → same bounce treatment as a malformed tasks.json (do not silently\ndrop agent intent).\n\n## Config\n\nThe `yonote` block (introduced by the materialization bead) gains:\n\n publish_collection_id: \"…\" # uuid; the default target collection\n\nValidation: when set, must be a UUID shape; lane is ACTIVE only when the yonote client is\nconfigured AND publish_collection_id is set. When the client is configured but the collection is\nnot, publish.json entries all fail visibly with \"publishing not configured\" in the card comment —\nnever silently ignored. Operational prerequisite (document next to the config): the BOT must have\nread_write on the target collection (collections.add_user — client-bead runbook step 3).\n\n## Daemon flow (finalize, success path, after summary/tasks/question handling)\n\nPer entry (≤3, in file order): resolve target collection (override or default) → resolve\nparent_url if set (its failure fails ONLY that entry) → read + cap content →\nCreateDocument{Title, CollectionID, ParentDocumentID, Text: content, Publish: true} → record\n{Title, absURL = cfg base_url + doc.URL} or {Title, err}.\n\n- Card comment (goes through the existing markdownToHTML comment path):\n published to Yonote:\n - \u003cTitle\u003e → \u003cabs url\u003e\n - \u003cTitle\u003e → FAILED: \u003cmessage\u003e\n- appendEvent \"yonote_published\" {\"ok\": n, \"failed\": m}.\n- Publish failures NEVER change the run outcome — the run already succeeded; publication is a\n side effect. Operator retries by re-running, or publishes manually from the archived tar.gz\n (the content survives there by construction).\n- Idempotency (DECISION): re-running a card re-publishes and creates a NEW doc. Accepted for v1 —\n re-runs are rare, docs are cheap, and dedup would need a doc-id echo channel; revisit only if it\n annoys in practice (then: UpdateDocument by echoed id).\n\n## Agent contract (role-prompt snippet, config-side; document in deploy notes)\n\n To publish a document to Yonote: write markdown to .task/publish/\u003cname\u003e.md and declare it in\n .task/publish.json as {\"documents\":[{\"title\":\"…\",\"file\":\"publish/\u003cname\u003e.md\"}]}. Publication\n happens AFTER your run succeeds; resulting links are posted on the task card.\n\n## Tests\n\n- reader: absent → nil; malformed → tasks.json-parity bounce; \u003e3 entries; empty title; path\n guards (absolute, .., symlink escape); oversize file.\n- finalize with fake yonote writer (interface seam: CreateDocument + DocumentInfo): 2 ok + 1 fail\n → comment body lines, event payload, run outcome unchanged; lane-off (no collection id) →\n \"publishing not configured\" per entry; parent_url resolve failure isolates the entry;\n collection_id override honored.\n- prepareTaskDir clears publish.json + the publish/ dir between attempts.\n\nwave 2.5 — implement after feat/wave2-archive-links merges. Depends on: internal/yonote client\nbead (CreateDocument/DocumentInfo), materialization bead (yonote config block + prepareTaskDir\nchannel-clearing site).\n","notes":"Decision record: direct agent→Yonote API access was REJECTED — worktrees run arbitrary generated code, .task/ fossilizes into Done-archive tar.gz (a token in env/files would be archived), and the existing file-channel protocol (runner/taskfiles.go readTasksFile/readQuestionFile, attempt-scoped clearing in prepareTaskDir) already gives declarative-intent + daemon-credential semantics with card-comment observability. API evidence (ah-gxa notes, one-probe lifecycle): documents.create stores submitted markdown VERBATIM (create response echoed text byte-identical) and returns the site-relative url for the card comment (abs = base_url + url); publish:false honored (draft), so publish:true is explicit; comments.create/list verified for the future reply path. NOT live-verified: parentDocumentId child creation (v1-spec'd; exercise via httptest now, live during rollout), server-side max document text size (none documented — hence the defensive 512 KiB per-file cap), bot write rights on a granted collection (provisioning-time check, ah-gxa runbook step 4).","status":"in_progress","priority":2,"issue_type":"feature","assignee":"Eugene Blikh","owner":"bigbes@gmail.com","created_at":"2026-07-18T15:04:25Z","created_by":"Eugene Blikh","updated_at":"2026-07-18T15:06:57Z","started_at":"2026-07-18T15:06:57Z"} |
| new_value | {"notes":"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\u0026A bead ah-ptu uses them). SEMANTICS NOTE: publish fires on ROUTED successful runs too (inside firstFinalize\u0026\u0026success 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."} |
| comment | NULL |
| created_at | 2026-07-18T18:59:06Z |
| id | 019f75f3-ebc5-7e50-a9c7-77bbcec08ecc |
| issue_id | ah-25e |
| event_type | closed |
| actor | Eugene Blikh |
| old_value | |
| new_value | 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. |
| comment | NULL |
| created_at | 2026-07-18T18:59:07Z |
| id | 019f75fc-fa59-73c0-9b78-8f13c2fb7d69 |
| issue_id | ah-25e |
| event_type | updated |
| actor | Eugene Blikh |
| old_value | {"id":"ah-25e","title":"Yonote publish lane: .task/publish.json → bot-authored docs at finalize","description":"Why: the operator wants agents to publish specs/reports/answers INTO Yonote instead of committing them to the project repo — with the write credential never entering the agent's environment. What: a third finalize-time file channel in the SPEC §14.4 family (tasks.json, question.json): the agent writes markdown under .task/publish/ and declares it in .task/publish.json {documents:[{title, file, collection_id?, parent_url?}]}; on successful finalize the daemon creates the docs via the internal/yonote client under the BOT token (publish:true, target = yonote.publish_collection_id or per-entry override, optional nesting under a referenced doc), comments the resulting URLs on the card, emits a yonote_published event. Publish failures never change the run outcome. wave 2.5 — implement after feat/wave2-archive-links merges. Depends on ah-gxa (client: CreateDocument/DocumentInfo) and ah-2lh (yonote config block + prepareTaskDir channel-clearing site).","design":"# Yonote publish lane: .task/publish.json → bot-authored docs at finalize\n\nAgents publish specs/reports/answers INTO Yonote instead of committing them to the project repo.\nThe agent declares intent via a file channel; the daemon executes with ITS bot token at finalize\nand links results on the card.\n\n## Why file-channel, not direct API access from the agent (DECISION)\n\nThe agent must never hold the Yonote token: worktrees run arbitrary generated code; `.task/` is\narchived to tar.gz at Done (a token in env or files would fossilize into archives); per-run env\nplumbing would put a write-capable credential into every zellij session. The daemon already owns a\nfinalize-time file-channel protocol with exactly the right semantics — `.task/tasks.json`\n(delegation) and `.task/question.json` (ask-user), SPEC §14.4: agent writes declarative intent,\ndaemon acts with its own credentials, results land as card comments. Publishing is the third\nchannel of that family. Attribution lands on the BOT user — uniform and auditable.\n\n## Channel format\n\n`.task/publish.json` (attempt-scoped; cleared in prepareTaskDir alongside summary/tasks/question):\n\n {\"documents\": [\n {\"title\": \"Design: frobnicator\", \"file\": \"publish/design.md\",\n \"collection_id\": \"\", \"parent_url\": \"\"}\n ]}\n\n- title: required, non-empty, ≤255 chars (Yonote bot/user name limits suggest 255 conventions).\n- file: required; RELATIVE path resolved under `.task/` (convention: agents put content in\n `.task/publish/\u003cname\u003e.md`). Guard rigor as repo-slug F8: reject absolute paths, filepath.Clean\n must stay under .task/, and the opened file must not escape via symlink (os.Root / EvalSymlinks\n containment check).\n- collection_id: optional uuid overriding the config default target.\n- parent_url: optional Yonote doc URL → resolve via yonote.ParseDocRefs(single) + DocumentInfo →\n parentDocumentId, so a report can nest under the spec it answers (documents.create\n parentDocumentId is v1-spec'd; child creation itself NOT live-verified — verify in tests against\n httptest only, live during rollout).\n- Caps: maxPublishDocs = 3 per run; maxPublishBytes = 512 KiB per file. No server-side text limit\n was verified — cap defensively; an oversize entry FAILS (listed in the comment), never truncated.\n\nRead at SUCCESSFUL finalize only, same site and semantics as readTasksFile: absent → nil (common\ncase); present-but-unparseable → same bounce treatment as a malformed tasks.json (do not silently\ndrop agent intent).\n\n## Config\n\nThe `yonote` block (introduced by the materialization bead) gains:\n\n publish_collection_id: \"…\" # uuid; the default target collection\n\nValidation: when set, must be a UUID shape; lane is ACTIVE only when the yonote client is\nconfigured AND publish_collection_id is set. When the client is configured but the collection is\nnot, publish.json entries all fail visibly with \"publishing not configured\" in the card comment —\nnever silently ignored. Operational prerequisite (document next to the config): the BOT must have\nread_write on the target collection (collections.add_user — client-bead runbook step 3).\n\n## Daemon flow (finalize, success path, after summary/tasks/question handling)\n\nPer entry (≤3, in file order): resolve target collection (override or default) → resolve\nparent_url if set (its failure fails ONLY that entry) → read + cap content →\nCreateDocument{Title, CollectionID, ParentDocumentID, Text: content, Publish: true} → record\n{Title, absURL = cfg base_url + doc.URL} or {Title, err}.\n\n- Card comment (goes through the existing markdownToHTML comment path):\n published to Yonote:\n - \u003cTitle\u003e → \u003cabs url\u003e\n - \u003cTitle\u003e → FAILED: \u003cmessage\u003e\n- appendEvent \"yonote_published\" {\"ok\": n, \"failed\": m}.\n- Publish failures NEVER change the run outcome — the run already succeeded; publication is a\n side effect. Operator retries by re-running, or publishes manually from the archived tar.gz\n (the content survives there by construction).\n- Idempotency (DECISION): re-running a card re-publishes and creates a NEW doc. Accepted for v1 —\n re-runs are rare, docs are cheap, and dedup would need a doc-id echo channel; revisit only if it\n annoys in practice (then: UpdateDocument by echoed id).\n\n## Agent contract (role-prompt snippet, config-side; document in deploy notes)\n\n To publish a document to Yonote: write markdown to .task/publish/\u003cname\u003e.md and declare it in\n .task/publish.json as {\"documents\":[{\"title\":\"…\",\"file\":\"publish/\u003cname\u003e.md\"}]}. Publication\n happens AFTER your run succeeds; resulting links are posted on the task card.\n\n## Tests\n\n- reader: absent → nil; malformed → tasks.json-parity bounce; \u003e3 entries; empty title; path\n guards (absolute, .., symlink escape); oversize file.\n- finalize with fake yonote writer (interface seam: CreateDocument + DocumentInfo): 2 ok + 1 fail\n → comment body lines, event payload, run outcome unchanged; lane-off (no collection id) →\n \"publishing not configured\" per entry; parent_url resolve failure isolates the entry;\n collection_id override honored.\n- prepareTaskDir clears publish.json + the publish/ dir between attempts.\n\nwave 2.5 — implement after feat/wave2-archive-links merges. Depends on: internal/yonote client\nbead (CreateDocument/DocumentInfo), materialization bead (yonote config block + prepareTaskDir\nchannel-clearing site).\n","notes":"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\u0026A bead ah-ptu uses them). SEMANTICS NOTE: publish fires on ROUTED successful runs too (inside firstFinalize\u0026\u0026success 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.","status":"closed","priority":2,"issue_type":"feature","assignee":"Eugene Blikh","owner":"bigbes@gmail.com","created_at":"2026-07-18T15:04:25Z","created_by":"Eugene Blikh","updated_at":"2026-07-18T15:59:08Z","started_at":"2026-07-18T15:06:57Z","closed_at":"2026-07-18T15:59:08Z","close_reason":"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."} |
| new_value | {"notes":"\n ROLLOUT LIVE-SMOKE CHECKLIST (from wave-2.5 implementer, against \n bigbes.yonote.ru after deploy): 1) mint bot per ah-gxa runbook (POST \n /api/v2/bots + token, collections.add_user grant — grant call is spec-only, \n NOT live-verified), AuthInfo under bot token must show IsBot=true (daemon \n WARNs at startup otherwise); 2) documents.create with parentDocumentId \n (child nesting unverified live) + bot WRITE rights on publish_collection_id \n + publish:true attribution to bot; 3) archived-doc export ((archived) marker\n path); 4) ExportMarkdown empty-string cases (API-created never-edited draft,\n database-type doc) fall back to .text; 5) comments.create/list/resolve field\n names (only Q\u0026A bead ah-ptu uses them). SEMANTICS NOTE: publish fires on \n ROUTED successful runs too (inside firstFinalize\u0026\u0026success before maybeRoute)\n — a design stage with publish.json publishes before routing to review; \n intentional. publish_collection_id (UUID) must be chosen by operator at \n config staging. \n\n\n\nDEPENDS ON\n → ✓ ah-2lh: Claim-time Yonote artifact materialization into .task/artifacts/ + prompt manifest ● P2\n → ✓ ah-gxa: internal/yonote: light API client (doc resolve/export, create, comments; bot-token auth) ● P2\n\nBLOCKS\n ← ○ ah-ptu: Yonote Q\u0026A bot loop: poll doc comments, answer via board tasks (post-wave-2.5) ● P3\n\nROLLOUT 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}."} |
| comment | NULL |
| created_at | 2026-07-18T19:09:01Z |
| id | 019f7cda-aa8b-79b0-aee3-fd66aec0213f |
| issue_id | ah-25e |
| event_type | label_added |
| actor | Eugene Blikh |
| old_value | NULL |
| new_value | NULL |
| comment | Added label: milestone:yonote |
| created_at | 2026-07-20T03:08:53Z |
| id | 019f7cdc-2604-77f3-a4c4-2eb4d27129b2 |
| issue_id | ah-25e |
| event_type | label_removed |
| actor | Eugene Blikh |
| old_value | NULL |
| new_value | NULL |
| comment | Removed label: milestone:yonote |
| created_at | 2026-07-20T03:10:30Z |
No comments.
Close reason