~bigbes/sr-ht-spec · issues

3mqn3kdac3mkpp59n2vr0fd5g305gj8b · 34 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
spec-0m285310dd4858ac34216534b3cb2e7c029a0a7107d1a63c1dc37492e9eac29913dBump gqlparser/gqlgen in specs past the GO-2024-2920 floor and fix broken codegenWhat is wrong: `sourcehut-specs/go.mod` pins `github.com/vektah/gqlparser/v2 v2.5.8` + `github.com/99designs/gqlgen v0.17.36`, and `sourcehut-specs/graph/generate.go`'s `go:generate` directive is unpinned (`go run github.com/99designs/gqlgen generate`). Two failure modes on one pin: gqlparser v2.5.8 is below the GO-2024-2920 floor (a DoS in `parseDirectives`, fixed in v2.5.14); and gqlgen v0.17.36 itself pins `golang.org/x/tools v0.9.3`, which does not compile under this toolchain, so `go generate ./graph` in specs is broken outright — specs' own comment records the cause. Why it matters: specs' `/query` parses only after `graph/server.go::resolveCaller`/`::gate`, so this is not the fully unauthenticated shape the advisory describes — that's the mitigation. But codegen being broken means specs cannot regenerate its own GraphQL resolvers when the schema changes. Sibling that does it correctly: `sourcehut-artifacts`, `sourcehut-bench`, `sourcehut-coverage`, `sourcehut-curator`, `sourcehut-dolt` (and `thistle`, `sr-ht-api`, `sourcehut-federation`) are all on `gqlparser v2.5.36` + `gqlgen v0.17.94`. Fix: bump `sr-ht-core` and `sr-ht-ecore` first (they are libraries and set the floor for every consumer, tracked as separate issues in those repos), then in specs: `go get github.com/vektah/gqlparser/v2@v2.5.36 github.com/99designs/gqlgen@v0.17.94`; pin the `go:generate` line in `graph/generate.go` to `@v0.17.94` like the other five services. Note: confidence verified — go.mod version strings were read directly across all nine services plus core/ecore, split exactly 5/5. Audit: D06 (cross-repo audit, 2026-08-17) open0bugNULLNULL2026-08-17T20:40:04ZEugene Blikhbigbes@gmail.com2026-08-17T20:40:04ZNULLNULL0NULLNULLNULL000�{}0NULLNULLNULL0NULL0
spec-17s6d84b595e3d1d014db62509c689e5cc17443ce843dc12e76c44cc7ab9d6afda0Replace specs' private mcpsrv cache/hostguard copies with sr-ht-ecore/mcphttp once vary is parameterisedWhat is wrong: `sourcehut-specs/mcpsrv/` carries a private, byte-identical copy of the Host guard (`allowHosts`/`hostAllowed`), the commit-time cache wrapper (`cacheWriter`) and the transport options literal that `sr-ht-ecore/mcphttp` was extracted to hold in one place — and specs does not import `mcphttp`. specs' own `mcpsrv/cache.go` doc comment claims the swap is blocked: "It is not imported because it is not resolvable: the ecore commit that adds mcphttp is not published, and this module pins an ecore from before it." Why it matters: that excuse is verifiably false. `mcphttp` landed in ecore commit `3aa1fcf` (2026-08-16 01:53); specs (along with bench/coverage/curator) pins `sr-ht-ecore v0.0.0-20260816184219-89fa694cbf54`, ~20h later — the pinned revision already contains the package. The duplication has already cost what it was predicted to cost: specs is one of the copies missing `Stateless: true` (separate issue, D01) and the identical `Flush` bug this package fixes was independently discovered and fixed twice elsewhere in the family (ecore `3bd158f`/`00d7582`, artifacts `6c44e03`). Sibling that does it correctly: `sr-ht-ecore/mcphttp/{mcphttp,hostguard,cache}.go`. One real blocker exists family-wide: `mcphttp` hardcodes `vary = "Cookie, Authorization"`, and dolt deliberately uses `Authorization` alone — so `vary` needs parameterising in ecore before every service (including dolt) can adopt it; this is being tracked as an ecore-side change, not a specs-side one. Fix: once `mcphttp.PrivateCache`'s `vary` parameter lands in ecore, delete specs' private `privateCache`/`cacheWriter`/`allowHosts`/`hostAllowed`, import `mcphttp`, call `PrivateCache`/`HostGuard`/`StreamableOptions()`, and delete the false "not resolvable" doc comment. Keep the tool-registration code copied — that part of the "copy, don't import" convention is genuinely about business logic, not infrastructure. Note: confidence verified — two doc-comment mentions of `mcphttp`, zero actual imports, confirmed by grep across all nine service trees. Audit: D10 (cross-repo audit, 2026-08-17) open2taskNULLNULL2026-08-17T20:40:05ZEugene Blikhbigbes@gmail.com2026-08-17T20:40:05ZNULLNULL0NULLNULLNULL000�{}0NULLNULLNULL0NULL0
spec-3m9abe1c2fc2de8f0338fb78f23322b8c0c612e963b219e326658b426dfd08245c0Phase 5a: webhooks + notifications on proposal lifecyclePhase 5a: webhooks via FULL core-go convention adoption (user chose to match pages.sr.ht/lists.sr.ht byte-for-byte). Prerequisite: retrofit graph/ onto core-go conventions (database.Model, @access/@private directives, cursor pagination, auth.Middleware + a user model with OAuth2 scopes). Then add gql_proposal_wh_sub/_delivery + webhook SDL/resolvers/delivery on top. Reference: ~/data/home/tmp/pages.sr.ht (same fork). Large graph rewrite, not additive.closed3featureEugene BlikhNULL2026-07-23T07:02:58ZEugene Blikhbigbes@gmail.com2026-07-24T09:50:18Z2026-07-24T09:50:18ZNULL0NULLNULLNULL000�{}Phase 5a webhooks complete: DB + authn→AuthContext bridge + GraphQL surface + core-go server wiring + firing. Verified live: agent REST propose delivers an Ed25519-signed POST whose body is the subscription query executed against the ProposalEvent payload; delivery recorded.0NULLNULLNULL02026-07-23T07:03:00Z0
spec-3vz9f3be3e241f18287f989301fe6cea7e81ffd2c66663911b2ec0f2eec9b4425ffPhase 4: review plane — approve/reject in a browserProposal pages at stable URLs (returned by every write), the inbox, prose diff, approve (merges immediately) / reject, status lifecycle, and the digest of policy-merged firehose content.Productionise prosediff (the Phase 0 spike) into the review UI. CRITICAL requirement from the Phase 0 verdict: 13% of real prose modifications shred into interleaved fragments (similarity <= 0.73), so the UI MUST switch to a two-column old/new view below ~0.75 and inline word diffs above it — building only the inline renderer makes one review in eight unreadable. The prosediff package already emits the (Hash, HeadingPath, Ordinal) comment-anchor tuple. Reads a proposal branch via service.ReadDocumentAtRef (the deliberately-awkward bypass), never the normal read path.closed2featureEugene BlikhNULL2026-07-23T04:09:25ZEugene Blikhbigbes@gmail.com2026-07-23T06:52:55Z2026-07-23T06:52:55ZNULL0NULLNULLNULL000�{}Review plane shipped: proposal page at /~owner/space/p/<id> with the prose diff (inline word-diff above 0.75 similarity, two-column old/new below — the Phase 0 verdict's hard requirement, in web/diff.go), approve(merge)/reject as owner-only CSRF-guarded POSTs, status badges, and the /inbox review queue + policy-merged digest. service.ProposalDiff/InboxProposals/DigestProposals/MergeHuman back it; web.Reader extended. Diff reads the proposal branch by resolving its tip to a sha (pinned read) rather than the ReadDocumentAtRef bypass — cleaner and immutable. Validated: diff renderer unit tests (inline/two-column/escaping), proposal-page + approve/reject handler tests (CSRF, owner-only, stale->409, wrong-space->404), inbox tests, and PG-backed service tests. Follow-up: digest_mark 'since last seen' tracking.0NULLNULLNULL02026-07-23T06:31:53Z0
spec-43wf49e1b7f7a2f68c5465a82fa5abcbda22506cbb29f3a192bd2d3f62617951432Stale apk pins: paste.sr.ht 0.19.3-r0 and hub.sr.ht 0.29.4-r0 aged off the mirrorPre-existing, not caused by spec.sr.ht — but a FULL stack rebuild (post_push) would fail on these two apk layers and leave the stack un-deployable (the 2026-07-11 incident pattern). The self-hosted append-only repo.bigb.es mirror only preserves what it has seen, and it started after these aged off; old versions are unrecoverable.spec.sr.ht was deployed AROUND this with labng push --no-restart + a single-service 'docker compose build spec', so nothing else rebuilt. To fix properly: bump the two pins in versions.env to what the mirror currently ships (paste 0.19.4-r0, hub 0.31.1-r0 as of 2026-07-22). hub 0.29.4->0.31.1 crosses two minors with forward-only migrations — pg_dumpall first per the stack CLAUDE.md. Kept separate from the spec deploy deliberately.closed2bugNULLNULL2026-07-23T04:09:54ZEugene Blikhbigbes@gmail.com2026-07-23T04:52:46Z2026-07-23T04:52:46ZNULL0NULLNULLNULL000�{}Stale apk pins already bumped in the stack versions.env (paste 0.19.4-r0, hub 0.31.1-r0); fixed out of band.0NULLNULLNULL0NULL0
spec-45j55fdf511b2f851497c73bed8b73ff921cb95db951667c8f300dc290e79b12a28Phase 5a-2: webhook tables, SDL, resolvers, delivery on proposal open/merge/rejectOn top of 5a-1: gql_proposal_wh_sub/_delivery tables + migration, WebhookSubscription/Delivery/Payload SDL, create/delete mutations + webhooks queries, and fire via webhooks.Schedule on proposal lifecycle events (service event hook -> queue). Reference pages.sr.ht webhooks/webhooks.go + graph/model/webhooks.go.closed3featureNULLNULL2026-07-24T02:05:29ZEugene Blikhbigbes@gmail.com2026-07-24T09:50:16Z2026-07-24T09:50:16ZNULL0NULLNULLNULL000�{}Webhook tables/SDL/resolvers/delivery all shipped and verified end-to-end.0NULLNULLNULL0NULL0
spec-4eh021baf11dcd3b18d6b17dd6b300612d9814525fef35c9aa29d1a938832b93835Add check-version and check-embedded-css targets to specs' MakefileWhat is wrong: specs' `Makefile` defines no `check-version:` or `check-embedded-css:` target, so its APKBUILD's `build()` runs only `make check-css` and `package()` re-verifies nothing. Why it matters: `check-version` inspects the compiled binary (`go version -m $(CHECK_BIN)`) for `vcs.revision`/`vcs.modified` and fails the build if the tree was dirty; `check-embedded-css` greps the binary for the embedded stylesheet name and diffs it against what is on disk, catching "compiled before `make css` ran". Without these, a dirty/unstamped binary "links, starts and serves" and only ever misreports itself — the family has hit this class for real (a `GOTMPDIR` race in federation, a `go mod download all` incident in bench), and the sha256sum Makefile bug (tracked separately, D08) makes specs hit the CSS half of this on any non-GNU machine with nothing catching it. Sibling that does it correctly: `sourcehut-bench`, `sourcehut-coverage`, `sourcehut-curator`, `sourcehut-tokens` (both targets) and `sourcehut-federation` (`check-version` only) all call these from both `build()` and `package()`, so the gate runs against the artefact that actually ships. Fix: port `check-version`/`check-embedded-css` plus their `CHECK_BIN`/`CHECK_BINS` staged-artefact invocations from `sourcehut-tokens/Makefile` into specs' `Makefile`, and wire both into the APKBUILD's `build()` and `package()`. Templating work, not new design. Note: confidence verified — `grep -n '^check-version:'` matches nothing in specs' Makefile. Audit: D12 (cross-repo audit, 2026-08-17) open2taskNULLNULL2026-08-17T20:40:06ZEugene Blikhbigbes@gmail.com2026-08-17T20:40:06ZNULLNULL0NULLNULLNULL000�{}0NULLNULLNULL0NULL0
spec-8y41985c014f152c1ad343c1ffdd8d2f17c81e8dae9b4772b416ea7cb37ef9a5a4fPhase 5a-1: adopt core-go graph conventions (directives, database.Model, cursors, auth+user model)Retrofit spec's minimal hand-written graph onto core-go's stack, the prerequisite for GraphQL-native webhooks. Per pages.sr.ht: @access/@private directives + impls, core-go database.Model for space/document/proposal/project/search types, cursor pagination, core-go auth.Middleware + a single-owner user table. Blueprint from ~/data/home/tmp/pages.sr.ht.closed3featureNULLNULL2026-07-24T02:05:29ZEugene Blikhbigbes@gmail.com2026-07-24T09:50:17Z2026-07-24T09:50:17ZNULL0NULLNULLNULL000�{}Adopted core-go server stack for /query (faithful path); the convention pieces webhooks needed (auth/database/server context, database.Model for webhook models, gqlgen) are in.0NULLNULLNULL0NULL0
spec-ar46cbe05b1d310f62b1a8ec2e21729ea5c53869f322b7c174a89b699c1608a380bRestart other SourceHut services so spec.sr.ht appears in their navThe switcher entry is config-driven: each service builds its own nav from config sections ending in .sr.ht. spec.sr.ht is in the shared config.ini now, but the OTHER services (git, meta, todo, ...) only pick up the new entry on restart. Not done unprompted because it briefly bounces every service. On phoebe: docker compose restart <the sourcehut services>.2026-07-24: verified still needed, but BLOCKED on host access from this machine. Verified over HTTPS (LAN, read-only): - spec.srht.bigb.es -> HTTP 200, the service itself is up. - git / meta / todo .srht.bigb.es nav all still MISSING a spec entry, so the restart has not happened and is still the remaining step. Access attempts: 'phoebe' does not resolve; srht.bigb.es domains resolve to 192.168.88.18, which answers on :22 but refuses blikh (publickey,password). The working git remote is the restricted 'git' user shell, not a login. No deploy repo here carries an SSH path — deploys to phoebe are pull-based (apk-mirror re-indexes within 15 min), so there is no push-SSH route configured on this Mac. Needs the owner to run it on phoebe directly: docker compose restart Then confirm with: curl -s https://git.srht.bigb.es | grep -c spec.srht.bigb.es # expect >0in_progress2taskEugene BlikhNULL2026-07-23T04:09:53ZEugene Blikhbigbes@gmail.com2026-07-24T20:23:14ZNULLNULL0NULLNULLNULL000�{}0NULLNULLNULL02026-07-24T20:21:42Z0
spec-by6135e33fb1bdf43b87c9230a59444bca23bb9a2fc202adc4cb3cd62f45e4f4e63Phase 5: comments, webhooks, mutations, vector searchThe deferred pile, each unblocked once its prerequisite lands.Inline comments (anchor tuple already emitted by prosediff; settle resolution against the built review UI before committing a schema). Webhooks + notifications (core-go/webhooks is GraphQL-native; the Phase 2 read schema is the foundation). GraphQL mutations once the proposal state machine stops moving. Vector search over the project index (warren's embed/ was deliberately not ported). Per-space token scoping (currently one token; scoping is a column + filter clause). Read-only mounts of external corpora (second-brain, Confluence RFCs) if the boundary ever moves — global IDs keep the door open.open3featureNULLNULL2026-07-23T04:09:27ZEugene Blikhbigbes@gmail.com2026-07-23T04:09:27ZNULLNULL0NULLNULLNULL000�{}0NULLNULLNULL0NULL0
spec-by6.1292b80b0c03e38b7b5d4de814769847496156234ce81eb532ca42f9c8322a01eGraphQL complexity limit on the /query surfacespec.sr.ht authenticates on its own router rather than core-go's WithSchema, so nothing bounds query cost — no depth or complexity limit on /query. Low risk today: the surface is single-user and authenticated. Wire a limit if /query is ever widened beyond owner+agents. Carried over from spec-wcr item 6 (triaged 2026-07-24: a feature, not a latent bug).open4featureNULLNULL2026-07-24T18:12:20ZEugene Blikhbigbes@gmail.com2026-07-24T18:12:20ZNULLNULL0NULLNULLNULL000�{}0NULLNULLNULL0NULL0
spec-by6.2f55791df2245dde867f3eb92def978eed974d73fccc8b5c746e6b91e28bbcf13Attachments: image embeds render as visibly missinggitx walks .md only, so an archive built from a git tree has no attachment index and ![[diagram.png]] resolves to a visibly-missing link. doc.FromPages already accepts an assets map — the gap is producing one. Two options: (a) gitx.WalkBlobs + an asset index threaded through service.Archive, plus a read-plane route serving the blob; (b) adopt Mermaid-by-convention and document that binary attachments are out of scope. Carried over from spec-wcr item 7 (triaged 2026-07-24: a feature, not a latent bug).open3featureNULLNULL2026-07-24T18:12:24ZEugene Blikhbigbes@gmail.com2026-07-24T18:12:24ZNULLNULL0NULLNULLNULL000�{}0NULLNULLNULL0NULL0
spec-by6.3930b48e40e540558361755cb2ef2835d8eaaed9478dd461ca9e239ede1b8639dPhase 5b: inline comments on proposalsClose the propose->critique->revise loop: the owner comments on blocks of a proposed document, the agent reads the comments, replies, and pushes a revision. Anchors re-resolve against the new revision rather than being lost. Design gate cleared 2026-07-24: DESIGN.md deferred the comment schema until the Phase 4 review UI existed. It exists, and three facts from it shape the work: the review page renders only changed blocks (web/diff.go:61); the read view uses a different block model (doc.Renderer/goldmark, web/handlers.go:437) from the review view (prosediff.Segment); no rendered block carries a DOM id.ANCHOR MODEL (as DESIGN.md:793-808 specifies, implemented not revisited): tuple = (doc_id, heading_path, block_ordinal, block_hash). Resolve against a revision by block_hash first; fall back to heading_path + ordinal; when both fail mark the comment OUTDATED rather than relocating it. heading_path is deliberately NOT part of the hash, so renaming a section does not dirty every block beneath it and orphan its comments at once. Anchor on doc_id, not path, so comments survive renames. ANCHOR STATE IS DERIVED, NOT STORED. A comment is not outdated in general, it is outdated AT a revision, and a proposal branch moves under it. The DB stores the tuple; resolution runs at read time against the revision being displayed. SIDE: a comment anchors to the NEW side of a modified block (that is the text under review) and to the OLD side of a pure deletion. SCOPE DECISIONS (settled with owner 2026-07-24): 1. Surface: the review page, ALL blocks — not only changed ones. This changes the diff view rule: renderDocDiff currently skips ChangeEqual entirely, and must instead render unchanged blocks as commentable context. Comments on approved documents (the read view) stay out of scope: that needs prosediff segmentation of the read path plus a goldmark<->prosediff block mapping. 2. Merge gate: an unresolved comment suppresses POLICY AUTO-MERGE only. Manual approve still works — human judgement wins. Rationale: a comment means the owner engaged, so the proposal must not slip through unattended; but a stale comment must not be able to wedge a proposal. 3. Agents: read + reply + revise via spec_comment. Agents may NOT resolve — an agent marking its own critique resolved defeats the gate.A comment survives a revision that reflows its paragraph (hash miss, heading-path+ordinal hit) and is reported as moved, not lost. A comment whose block is deleted is reported outdated, never relocated. An unresolved comment prevents policy auto-merge and does not prevent manual approve. An agent can read and reply to comments on its own proposal but cannot resolve one.closed2featureNULLNULL2026-07-24T18:56:37ZEugene Blikhbigbes@gmail.com2026-07-24T19:59:29Z2026-07-24T19:59:29ZNULL0NULLNULLNULL000�{}All four children done: core+db anchors (1a25367), service API + auto-merge gate (c6e5d66), mcpsrv spec_comment (f82d90a), web commentable diff (472bcb1).0NULLNULLNULL0NULL0
spec-by6.3.143fdfa8ce8f57113f87ef6446eaebab2f3b9552035d5343d06420eea3a45c0d5Comment anchors: core type + db schemaFoundation wave. core.CommentAnchor (doc_id, heading_path, ordinal, hash) plus the anchor-state vocabulary (anchored | moved | outdated) and the resolution function, kept free of a prosediff import so core stays dependency-free — resolution takes the block fields it needs, not the Block type. migrations/0004_comment.sql + schema.sql: the comment table storing the tuple, threading via parent_id, author identity + agent provenance (mirroring proposal's ck_proposal_provenance rule), and resolved timestamp. Anchor state is NOT a column — it is derived per revision at read time. db/comment.go with the insert/list/resolve queries and a test that a reflow keeps the comment and a deletion outdates it.closed2taskEugene BlikhNULL2026-07-24T18:57:02ZEugene Blikhbigbes@gmail.com2026-07-24T19:08:45Z2026-07-24T19:08:45ZNULL0NULLNULLNULL000�{}core.CommentAnchor + ResolveAnchor with 9 tests; migrations/0004_comment.sql + schema.sql; db/comment.go with 8 Postgres-backed tests, all green against real PG 16.0NULLNULLNULL02026-07-24T18:57:37Z0
spec-by6.3.2f41b2b8f032a32187a1792e2db6d3fa537d5f5bf5e429b604c36c114ff708846service: comment API + policy auto-merge gateservice.Comment type, Comment/ListComments/ReplyComment/ResolveComment, and the anchor resolution pass that takes a revision's prosediff blocks and reports each comment as anchored, moved or outdated. Wire the merge gate: an unresolved comment suppresses policy auto-merge (not manual approve) — the check belongs beside the existing auto-merge policy evaluation, and needs a test that a commented proposal stops auto-merging while Merge with human approval still succeeds.closed2taskEugene BlikhNULL2026-07-24T18:57:05ZEugene Blikhbigbes@gmail.com2026-07-24T19:15:24Z2026-07-24T19:15:24ZNULL0NULLNULLNULL000�{}service.CommentOn/ReplyTo/ResolveThread/Threads + AnchorThreads/AnchorOf; auto-merge gate in autoMerges. 10 tests, PG-backed ones green against real PG 16.0NULLNULLNULL02026-07-24T19:09:52Z0
spec-by6.3.3a204884f2d3938edb92e4d846dc3e856ff83a784c830f7c09adf973203b59f72web: commentable diff — context blocks, anchor ids, comment UIrenderDocDiff currently skips ChangeEqual ('the review shows only what changed', web/diff.go:61). It must now render unchanged blocks as commentable context — collapsed or dimmed so the diff still reads as a diff — because the settled scope is that any block of a proposed document can be commented on. Every rendered block gains a stable DOM id derived from the anchor tuple. Then the comment UI itself: existing comments rendered beside their block with their anchor state (moved / outdated shown honestly), a compose form, reply and resolve. Threading is flat per anchor.closed2taskNULLNULL2026-07-24T18:57:14ZEugene Blikhbigbes@gmail.com2026-07-24T19:59:27Z2026-07-24T19:59:27ZNULL0NULLNULLNULL000�{}Context blocks + anchor ids + thread UI; lost-anchor area; stale-block 409. Committed 472bcb1.0NULLNULLNULL0NULL0
spec-by6.3.49a60d8cbdadc7a62b1b132d40e77bcd0d794e4d0a6a37c0c7120b2bc59085889mcpsrv: spec_comment tool (read + reply, no resolve)The agent half of the loop. spec_comment lets an agent list the comments on a proposal it authored and reply to them; it must NOT resolve one, since an agent marking its own critique resolved defeats the auto-merge gate. Reuses the Phase 3 provenance path (agent + agent_session) for reply authorship, and the existing owner+agents read gate from spec-jjo. mcpsrv/mcpsrv.go:39 still documents spec_comment as deliberately absent — update that comment when it lands.Implemented. Divergence from this issue's original text, accepted: reads are NOT narrowed to 'a proposal the agent authored'. Agent identity is self-declared (X-Agent/X-Agent-Session headers) and all agents share one token, so an authorship check would gate on a string the caller chooses — stricter on paper than the owner+agents read plane (spec-jjo) it sits in, while enforcing nothing. Revisit if/when per-agent token scoping lands (spec-by6 backlog). No-resolve is enforced by TYPE: Commenter names only Threads/ReplyTo/GetProposal/ProposalDiff, so service.CommentOn and service.ResolveThread are unreachable from the handler however service/ grows. Writer is now Proposer+Commenter. Extra beyond the issue: replying requires proposal+thread and checks membership, because a thread id is global and service.ReplyTo takes only the id — a mistyped id would otherwise reply on a stranger's proposal.closed3taskNULLNULL2026-07-24T18:57:18ZEugene Blikhbigbes@gmail.com2026-07-24T19:44:55Z2026-07-24T19:44:55ZNULL0NULLNULLNULL000�{}spec_comment: list threads with anchor state + reply; no-resolve enforced by interface. 9 tests green, no DB needed. Committed f82d90a.0NULLNULLNULL0NULL0
spec-by6.3.5ad5850f9dfeb790caa01e085716a402bd3da5888d2d63e1bb407db8b8a422d79Port review UI to a line-numbered unified prose diffReplace the block-card review renderer with a real unified diff: line-numbered gutter, drag-select a line range, comment composer inline. Owner reviewed the card version against a live proposal and rejected it — labels ('ADDED PARAGRAPH') outweighed content on every row, every block carried identical chrome, and on a new file the whole page is green so the cards add noise and no signal. Split view was considered and dropped: prose lines are long and unified reads better for reflowed text. Approved prototype: https://claude.ai/code/artifact/a45e45cb-16b5-42ec-8108-84767ef9f465SELECTION IS BY LINE, ANCHORING IS BY BLOCK. Lines are what the cursor lands on; block hashes are what survive a reflow. The web layer maps a selected line range to its enclosing prosediff block and stores the existing core.CommentAnchor unchanged. service/, db/ and core/ do not move. The composer states what it will anchor to, so the indirection is visible rather than magic. THE BLOCKING PROBLEM — word marks cannot be placed on a line for free. prosediff.Span is {Op, Text, Space} with no source offsets, and Tokenize deliberately drops whitespace ('\n' and ' ' both collapse to Space=true) — which is exactly what makes a rewrap invisible to the differ. So for a ChangeModify block there is no stored answer to 'which line did this word change on'. Recoverable, not free: the span script consumes the old token sequence in order (Equal+Delete) and the new one (Equal+Insert). Re-tokenize each source line of Block.Lines, walk the script, and split a span where it crosses a line boundary. Needs a mapper of roughly 100 lines plus tests. Equal/Insert/Delete blocks need none of this — their lines map 1:1 and carry exact numbers. FALLBACK IF THE MAPPER IS NOT WANTED: render a modified block as a paired old/new region labelled by line RANGE rather than per line, keeping the word marks. Honest and much smaller, but does not match the approved prototype. GUTTER DETAILS SETTLED IN REVIEW: one shared rail ground for both number tracks with a single hairline against the content (a border per cell drew a cage); one --row-lh shared by gutter and prose so numerals sit on the text baseline rather than floating; 34px tracks; the add/delete tint starts at the sign column so the gutter never looks part of the change. HEADING PATH: no per-hunk breadcrumb. It restated headings visible a few rows above, always so on a new file. Replaced by one sticky section readout that appears only once its heading has scrolled past. The path stays in the composer and in each thread's anchor note, where the heading is usually off screen.A reviewer drags across line numbers, comments, and the thread stores an anchor identical to the one the block-card UI would have stored. Word-level marks still appear inside a modified paragraph. Unchanged lines render as context and collapse. An outdated comment still appears and is never attached to a neighbouring block.closed2featureNULLNULL2026-07-25T11:40:01ZEugene Blikhbigbes@gmail.com2026-08-05T01:11:47Z2026-08-05T01:11:47ZNULL0NULLNULLNULL000�{}Closed0NULLNULLNULL0NULL0
spec-by6.4e6be97486fb543a98d133f37a38ca812d16634563b17ea31b10493136a1bde64A code fence whose only change is its info string renders as unchangedprosediff.Block.Lines excludes the fence delimiters, so a change from ```go to ```python produces no line-script difference. Stats counts the block as modified (BlocksModified: 1) while the unified diff table renders every one of its lines as ph-r-eq context. The page therefore says 'this document changed' and then shows nothing that changed. Found by adversarial verification of spec-by6.3.5; pre-existing in the segmenter, not introduced by the line-numbered diff port. It matters more now than it did under the block-card UI, because the table's whole contract is that a row states what happened to that line. Fix is in prosediff: either carry the info string into the block's diffed content, or have the web layer render an explicit marker row when Block.Info differs across a ChangeModify.closed3bugNULLNULL2026-08-05T00:49:06ZEugene Blikhbigbes@gmail.com2026-08-05T01:23:25Z2026-08-05T01:23:25ZNULL0NULLNULLNULL000�{}Closed0NULLNULLNULL0NULL0
spec-by6.5f1d4f50c278ff396379d8a4149912af433b40b1b503afbd90a47ce42c3a7115cprosediff: an equal span's Space flag is read from the old side onlyprosediff/token.go:207, spans(): Span.Space is taken from toks[0].Space, and for an equal run those tokens come from the OLD side. An insertion at the head of a block therefore emits op=insert space=false text="w16 w17" op=equal space=false text="w12 w13" and any renderer joining the script writes the two runs together with no space: {+w16 w17+}w12 w13. Not reachable from the web layer today — the new side's separator is simply not in the data the web layer is handed, and web/diffrows.go's sideSpans already carries a dropped span's separator onto the next kept one for the region rows. It affected the old block-card renderer identically, so it is not a regression from the line-numbered diff port (spec-by6.3.5). Fix shape: OR the two sides' leading-token flags when building an equal span. Found by the property test added in web/diffrows_test.go, which is deliberately blind to spacing (it compares nonce word sequences) and says so.closed3bugNULLNULL2026-08-05T01:10:14ZEugene Blikhbigbes@gmail.com2026-08-05T01:23:07Z2026-08-05T01:23:07ZNULL0NULLNULLNULL000�{}Closed0NULLNULLNULL0NULL0
spec-cnk3ba24801931581c0a5a05f0eda35da7f48a69b697bc04427f1a3b0835a9c6ad2Add middleware.PrivateCache to specs' /query mountWhat is wrong: `sourcehut-specs/cmd/specsrht/main.go::mountGraphQL`'s chi Group mounting `/query` carries RealIP/log/config/database middleware but never `middleware.PrivateCache`. Why it matters: specs' schema answers per-credential and also answers anonymous callers, so the same `/query` URL yields different bodies to different viewers with nothing declaring that. This is ranked below the REST/GOPROXY cache-header findings because POST bodies are not normally cached — it becomes live the moment GET-query support or a GraphQL-aware cache sits in front. Sibling that does it correctly: `sourcehut-curator/graph/graph.go::Mount` and `sourcehut-artifacts/graph/graph.go::privateCache`, both of which set `no-store` + `Vary: Authorization, Cookie` on the `/query` mount. Fix: add `r.Use(middleware.PrivateCache)` to the chi Group in `mountGraphQL` before it registers the `/query` handler. Audit: D14 (cross-repo audit, 2026-08-17) open2taskNULLNULL2026-08-17T20:40:07ZEugene Blikhbigbes@gmail.com2026-08-17T20:40:07ZNULLNULL0NULLNULLNULL000�{}0NULLNULLNULL0NULL0
spec-e4p5b9d3f0d53e5d628a06b2ebe9cb063612e49da724da9f9e81acd88500522c0d6Set Stateless: true on specs' MCP transport to stop the session id acting as a bearer credentialWhat is wrong: `mcpsrv/mcpsrv.go::Handler` builds `&mcp.StreamableHTTPOptions{DisableLocalhostProtection: true}` and never sets `Stateless`, so it defaults to `false` — the MCP transport runs in stateful mode. Why it matters: in the SDK's stateful mode, a tool handler runs under the context of the request that *opened* the session, not the one carrying the call. The credential presented at handshake authorises every later call on that session, so `Mcp-Session-Id` becomes a bearer credential in its own right. A token revoked mid-session keeps working until the client reconnects, and a token presented once and dropped keeps authorising. spec.sr.ht has write tools (`spec_propose`, `spec_comment`) that resolve the acting agent from the request context, and the package's own doc promises "one resolution per request, never a second one here" — a promise only stateless mode keeps. Sibling that does it correctly: `Stateless: true` is a literal in `sourcehut-bench`, `sourcehut-artifacts` and `sourcehut-curator`'s `mcpsrv` packages, a package constant in `sourcehut-coverage`/`sourcehut-dolt`, and hardcoded in `sr-ht-ecore/mcphttp/mcphttp.go::StreamableOptions`. `sourcehut-dolt/mcpsrv::TestIdentityIsPerCallAndNotPerSession` measured the difference against the same SDK version: stateful with "handshake anonymous, call with the OWNER's token" returns 4 results (the call's token is not consulted); stateless returns 6. Fix: add `Stateless: true` to the options literal in `mcpsrv.go::Handler`; port dolt's `TestIdentityIsPerCallAndNotPerSession` (or bench's `TestEachCallIsAuthenticatedOnItsOwn`) to pin it. Better yet, do this as part of adopting `mcphttp.StreamableOptions()` (see the mcphttp-adoption issue) which already hardcodes it. Note: verified that `Stateless` is absent (`grep -rn Stateless sourcehut-specs/mcpsrv/` returns zero matches); the exploitability claim is inferred from dolt's measurement, not run against specs itself. Audit: D01 (cross-repo audit, 2026-08-17) open0bugNULLNULL2026-08-17T20:39:41ZEugene Blikhbigbes@gmail.com2026-08-17T20:39:41ZNULLNULL0NULLNULLNULL000�{}0NULLNULLNULL0NULL0
spec-ejq102df983370943b985b602c02245055bc6077e30f60b82cc027d2d45e5dd7c00spec.sr.ht — reviewable document storage for humans and agentsA third custom Go service on the self-hosted SourceHut instance: agents propose documents, a human reviews and curates, agents read the approved text. One loop — bot produces, human curates, bots consume. Runs at https://spec.srht.bigb.es, deployed on phoebe as srht-spec-1. Code: git.sourcecraft.dev/bigbes/sr-ht-spec. Design: docs/DESIGN.md.DONE and deployed (Phases 0-2): core domain, gitx (bare repos + id-keyed tree-splice merge), db (Postgres schema + global ID registry), authn (unified-login cookie + agent tokens + provenance), the three receive hooks + hook RPC + daemon, service layer + reconciler, doc (warren vault/render on git objects), search (one global bleve index, per-line ru/en routing), projects (saved filter, not container), web read UI, mcpsrv read tools, graph read schema, prosediff (Phase 0 gate PASSED), specsrht space create/list, push->reindex. Live corpus: ~bigbes/rfcs holds SPEC-0001 and NOTE-0001. Verified end-to-end: validating push path, fail-closed reads, id-addressing, bilingual search, GraphQL. NOT DONE: the agent half of the loop (write plane), review UI, comments.open1epicNULLNULL2026-07-23T04:08:44ZEugene Blikhbigbes@gmail.com2026-07-23T04:08:44ZNULLNULL0NULLNULLNULL000�{}0NULLNULLNULL0NULL0
spec-ejq.17c5d8191e17d0c0c294994181f94e0489a8777921f147faceb5f3730098337c5Consolidate owner+agents read predicate into one Principal.CanRead()graph/server.go:161, web/handlers.go:61 and now mcpsrv.Gate each hand-spell 'IsOwner() || IsAgent()'. graph.gate's own comment warns two read surfaces with two policies is how a corpus leaks — there are now three copies. Add authn.Principal.CanRead() and route all three through it so the policy is defined once. Low-risk mechanical refactor; deferred out of spec-jjo to keep that bug fix surgical.closed4taskEugene BlikhNULL2026-07-24T16:45:16ZEugene Blikhbigbes@gmail.com2026-07-24T16:59:34Z2026-07-24T16:59:34ZNULL0NULLNULLNULL000�{}authn.Principal.CanRead() defined once; graph.gate, web.mayRead, mcpsrv.Gate routed through it. coreauth left independent (different semantic). Unit test covers owner/agent true, anon/zero/unknown false. Commit pending push.0NULLNULLNULL02026-07-24T16:57:49Z0
spec-ejq.20c97aa6c887b865c89b63400c0abf70ad355e41556e3ca1b0f0f3a803318b5e7CI publish task fails: build secret apk-ci-s3 is missingEvery recent builds.sr.ht run fails at the publish task while every other task passes. Confirmed on jobs #244 (e97532c), #245 (2dc6b71) and #246 (c2dd1ef): 'scss keygen version build' all SUCCESS, 'publish' FAILED. Pre-existing — #244 and #245 predate the Phase 5b work, so no code change caused it. Consequence: the apk never reaches the Garage repo bucket, so apk-mirror on phoebe has nothing new to re-index and the deployed service cannot be upgraded from CI output.EVIDENCE. .build.yml declares 'secrets: [apk-ci-s3]', documented as a File secret installed at ~/.apk-ci.env carrying APK_CI_S3_ACCESS_KEY / APK_CI_S3_SECRET_KEY for the Garage repo bucket. The publish task's first real line is '. ~/.apk-ci.env'. But 'hut builds secret list' returns exactly four secrets and none is apk-ci-s3: agent1-deploy (SSH key), bencher-api-key, s3-cache-key-secret, s3-cache-key-id. So the file the task sources is never installed. NOT VERIFIED: the exact failure text. The raw log endpoint (https://builds.srht.bigb.es/query/log/246/publish/log) needs a Bearer token, and reading hut's credential file was correctly refused, so the diagnosis rests on the secret list plus the manifest rather than on the log line itself. Read the log to confirm before acting. TWO POSSIBILITIES, needs the owner to distinguish: 1. The secret was deleted or never created — fix is to create a File secret named apk-ci-s3 at ~/.apk-ci.env (mode 600) holding the two S3 keys. 2. builds.sr.ht resolves manifest secrets by UUID, not by name — fix is to replace the name with the secret's UUID in .build.yml. SAME BUG IN THE SIBLING: ~/data/home/sourcehut-compare/.build.yml carries a byte-identical secrets block, so compare.sr.ht's publish is broken the same way and both fix together.A push to master produces a build whose publish task succeeds and uploads the .apk to the Garage repo bucket; apk-mirror on phoebe re-indexes it within 15 minutes.RE-INDEX VERIFIED 2026-08-05, the half the close reason left open. Checked through repo.bigb.es, the public mirror apk-mirror builds from the Garage repo bucket (URL documented in the APKBUILD header; s3.bigb.es itself resolves to 192.168.88.18 and is only reachable from the LAN, which is why the first attempt to verify failed). spec.sr.ht-0.0.78-r0.apk 200 content-length 16059260 last-modified 03:09:39 GMT APKINDEX.tar.gz 200 last-modified 03:15:00 GMT index entry: P:spec.sr.ht V:0.0.78-r0 A:x86_64 S:16059260 The size in the index matches the object byte for byte, and 0.0.78 is 'git rev-list --count cc90b4a' — the commit build #251 ran on. The index was rebuilt 5.5 minutes after the upload, inside the 15-minute window the acceptance criteria name. Both halves of the criteria are therefore met. Correction to the close reason: the package is named spec.sr.ht-0.0.78-r0.apk, not specsrht-0.0.78-r0.apk — pkgname in APKBUILD is spec.sr.ht. The specsrht spelling 404s.closed2bugNULLNULL2026-07-24T21:16:35ZEugene Blikhbigbes@gmail.com2026-08-05T03:20:11Z2026-08-05T03:12:30ZNULL0NULLNULLNULL000�{}Owner created the File secret apk-ci-s3 (/home/build/.apk-ci.env, mode 600) on builds.srht.bigb.es; 'hut builds secret list' now shows it. Resubmit of #250 as build #251 on the same commit cc90b4a: SUCCESS, all five tasks green including publish. VERIFIED: publish runs under 'set -e' and its body is a loop of 'rclone copyto' with an echo per file, so any failed upload would have exited non-zero and failed the task. A green publish is therefore evidence the .apk reached repo/alpine/v3.22/bigbes/x86_64/ — specsrht-0.0.78-r0.apk for this commit. NOT VERIFIED from here: the apk-mirror re-index on phoebe, the second half of the acceptance criteria. s3.bigb.es is not reachable anonymously from this workstation and phoebe host access is blocked (spec-ar4). If the package turns out not to be installable, that is apk-mirror's problem rather than this repo's and wants its own bead. The manifest needed no change: '- apk-ci-s3' resolves by name, as the diagnosis note records. The same account-level secret should also unblock sourcehut-compare, whose .build.yml carries the identical block — its next build will show it.0NULLNULLNULL0NULL0
spec-ejq.3d5e91d9dd2041fbb4e9356037da24d7fdcb5a8456af3220d1a915b49e9eff878Web: owner-only /tokens page for minting and revoking agent tokensAgent tokens can only be issued on the host (db.Store, and now specsrht token create). That makes the remote agent write plane unusable from anywhere without SSH: to hand an agent a credential the owner must be at the machine. Add an owner-only /tokens page — list, mint (plaintext shown once), revoke — going through service/, so the CLI and the page share one implementation of the ACL and the mint.service.IssueAgentToken/ListAgentTokens/RevokeAgentToken are owner-only and refuse an agent with ErrForbidden; GET /tokens lists tokens for the owner and 403s everyone else; POST mints and shows the plaintext exactly once; POST revoke stamps it; both writes carry the same-origin CSRF guard as approve/reject; specsrht token create goes through the same service methodsopen2featureNULLNULL2026-08-05T03:41:50ZEugene Blikhbigbes@gmail.com2026-08-05T03:41:50ZNULLNULL0NULLNULLNULL000�{}0NULLNULLNULL0NULL0
spec-jjo24266167e6658b316bf0de74a8be97019e0447b2d70f6008f89195e84041133cMCP read tools (/mcp) have no auth gate — reads are open behind the Host checkPhase 3 added the resolver middleware to /mcp so spec_propose can resolve the agent, but the read tools (spec_search/spec_read/spec_list) still have no ACL: anyone passing the Host allowlist can read approved content. graph's /query gates to owner+agents (graph.gate); /mcp does not. The design's read plane is fail-closed (owner+agents only). Add the same gate to the MCP surface. Pre-existing since Phase 2; not caused by Phase 3, but now that /mcp resolves a principal the gate is a one-liner. Deliberately left out of Phase 3 to avoid changing Phase 2 read behavior mid-feature.closed3bugEugene BlikhNULL2026-07-23T05:29:15ZEugene Blikhbigbes@gmail.com2026-07-24T16:45:32Z2026-07-24T16:45:32ZNULL0NULLNULLNULL000�{}MCP surface gated to owner+agents (mcpsrv.Gate), mounted inside resolver middleware; tests cover anon 401 / owner+agent pass / fail-closed without middleware. Commit 7cc652d.0NULLNULLNULL02026-07-24T16:42:15Z0
spec-jo978c63cc2f897e1b23f901f7f546a48c6d15c9893adaa375c9d9db5955b96c9d6Make specs' Makefile css recipe use $(SHA256SUM) instead of hardcoded sha256sumWhat is wrong: specs' `Makefile` `css` recipe ends `mv web/static/main.min.css web/static/main.min.$$(sha256sum … | cut -c1-8).css`, hardcoding GNU coreutils' `sha256sum`. Why it matters: on a machine without GNU coreutils (e.g. a bare macOS shell), `sha256sum` does not exist, the command substitution yields an empty string, and `mv` produces `web/static/main.min..css`. That filename does not match the glob `main.min.*.css`, so `assets.Resolve` answers `""` and every page renders unstyled — with no error anywhere (compounded by the missing-stylesheet-is-silent gap tracked separately for artifacts/dolt; specs itself does log a warning per D23's "six that warn" list, so specs is not silent about the empty resolve, only about *why* it's empty). Sibling that does it correctly: `sourcehut-bench/Makefile` and `sourcehut-curator/Makefile` define `SHA256SUM ?= sha256sum` and use `$(SHA256SUM)`, with a comment naming `make SHA256SUM="shasum -a 256" css` as the macOS invocation. Fix: add `SHA256SUM ?= sha256sum` near the top of specs' `Makefile` and substitute `$(SHA256SUM)` in the `css` recipe. Note: confidence verified — `SHA256SUM` vs raw `sha256sum` counted across all nine Makefiles; specs has 0/≥1 (0 uses of the variable, at least one raw call). Audit: D08 (cross-repo audit, 2026-08-17) open1bugNULLNULL2026-08-17T20:40:05ZEugene Blikhbigbes@gmail.com2026-08-17T20:40:05ZNULLNULL0NULLNULLNULL000�{}0NULLNULLNULL0NULL0
spec-mfmb80b8233f618b43a155481c0832d645afb80809f1631e1c2193537ece20855a6Digest should track 'since you last looked' via digest_mark, not just show recent policy-mergesPhase 4 ships the policy-merged digest as 'recent policy-merged proposals' (service.DigestProposals). The design intends it as 'what auto-merged since you last saw the digest', backed by the digest_mark table (db.GetDigestMark/SetDigestMark, already present). Advancing the mark is a write, and the inbox GET is deliberately kept pure, so this needs either a POST 'mark as seen' action or an accepted side-effecting GET. Low priority: the digest already surfaces auto-merged content, which is its core purpose.closed3featureEugene BlikhNULL2026-07-23T06:52:53ZEugene Blikhbigbes@gmail.com2026-07-24T16:54:07Z2026-07-24T16:54:07ZNULL0NULLNULLNULL000�{}Inbox GET reads digest_mark to flag/count new-since-last-looked auto-merges + divider (pure read); explicit POST /inbox/seen advances the mark behind owner-only + same-origin guard. service.DigestMark/MarkDigestSeen added. Commit 865a21f.0NULLNULLNULL02026-07-24T16:49:21Z0
spec-ovo320145689033beb92c4e2ae45841bbce51b1dc4ff2e85c6c070f2784b20f7edeCLI: specsrht doc propose — open a proposal from files on the hostThe two agent write surfaces (REST PUT, mcpsrv spec_propose) are remote and need a bearer token. When the operator and the documents are already on the host, that token is ceremony: the process can open Postgres and the bare repos directly. Add 'specsrht doc propose ~owner/space <file>...' calling service.Propose with a constructed agent principal, so the shared layer still owns If-Match, provenance, the branch cut and the auto-merge gate.doc propose opens a proposal from local files and prints proposal id, branch, base and URL; --as overrides the in-space path; --proposal adds to an open proposal; --base defaults to the approved head; provenance (--agent/--session) is recorded like a remote agent's; flags parse before, after and between positionalsin_progress2featureEugene BlikhNULL2026-08-05T03:23:01ZEugene Blikhbigbes@gmail.com2026-08-05T03:23:07ZNULLNULL0NULLNULLNULL000�{}0NULLNULLNULL02026-08-05T03:23:07Z0
spec-rsb96e8e61df2c62e5f21e9cb272da07c2d3a972f3600d57b6e05dee32e72ea7da5CLI: specsrht token create|list|revokedb.Store has the whole agent-token lifecycle (CreateAgentToken, ListAgentTokens, RevokeAgentToken, GenerateToken/HashToken) but nothing calls it: no CLI subcommand and no web page. A freshly deployed instance therefore has no way to issue the credential the agent write plane (REST PUT and mcpsrv spec_propose) requires, short of hand-writing an INSERT with a sha256 hash. Add a 'token' admin subcommand next to 'space', in the same shape: load config, open the pool, run, exit.specsrht token create <name> mints a token, stores only its hash, and prints the plaintext once; specsrht token list shows id/name/created/revoked; specsrht token revoke <id> stamps it revoked; usage errors are one clear line; unit tests cover argument parsing and row formattingin_progress2featureEugene BlikhNULL2026-08-05T03:14:01ZEugene Blikhbigbes@gmail.com2026-08-05T03:14:05ZNULLNULL0NULLNULLNULL000�{}0NULLNULLNULL02026-08-05T03:14:05Z0
spec-taf5b9d3f0d53e5d628a06b2ebe9cb063612e49da724da9f9e81acd88500522c0d6Set Stateless: true on specs' MCP transport to stop the session id acting as a bearer credentialWhat is wrong: `mcpsrv/mcpsrv.go::Handler` builds `&mcp.StreamableHTTPOptions{DisableLocalhostProtection: true}` and never sets `Stateless`, so it defaults to `false` — the MCP transport runs in stateful mode. Why it matters: in the SDK's stateful mode, a tool handler runs under the context of the request that *opened* the session, not the one carrying the call. The credential presented at handshake authorises every later call on that session, so `Mcp-Session-Id` becomes a bearer credential in its own right. A token revoked mid-session keeps working until the client reconnects, and a token presented once and dropped keeps authorising. spec.sr.ht has write tools (`spec_propose`, `spec_comment`) that resolve the acting agent from the request context, and the package's own doc promises "one resolution per request, never a second one here" — a promise only stateless mode keeps. Sibling that does it correctly: `Stateless: true` is a literal in `sourcehut-bench`, `sourcehut-artifacts` and `sourcehut-curator`'s `mcpsrv` packages, a package constant in `sourcehut-coverage`/`sourcehut-dolt`, and hardcoded in `sr-ht-ecore/mcphttp/mcphttp.go::StreamableOptions`. `sourcehut-dolt/mcpsrv::TestIdentityIsPerCallAndNotPerSession` measured the difference against the same SDK version: stateful with "handshake anonymous, call with the OWNER's token" returns 4 results (the call's token is not consulted); stateless returns 6. Fix: add `Stateless: true` to the options literal in `mcpsrv.go::Handler`; port dolt's `TestIdentityIsPerCallAndNotPerSession` (or bench's `TestEachCallIsAuthenticatedOnItsOwn`) to pin it. Better yet, do this as part of adopting `mcphttp.StreamableOptions()` (see the mcphttp-adoption issue) which already hardcodes it. Note: verified that `Stateless` is absent (`grep -rn Stateless sourcehut-specs/mcpsrv/` returns zero matches); the exploitability claim is inferred from dolt's measurement, not run against specs itself. Audit: D01 (cross-repo audit, 2026-08-17) open0bugNULLNULL2026-08-17T20:40:04ZEugene Blikhbigbes@gmail.com2026-08-17T20:40:04ZNULLNULL0NULLNULLNULL000�{}0NULLNULLNULL0NULL0
spec-wcr49e00deebe86e55d30ead451d2a2418250908bba4a3cc686046e357c75088b29Loose ends from Phases 1-2 (latent bugs, no user impact yet)Real issues agents flagged and deliberately left alone during the parallel build. None blocks anything today; each is a trap for a later phase.1. ProposalBranch derived THREE ways: core.ProposalBranch (canonical), and a third copy in SQL — db.OpenProposal concatenates prefix||serial in its INSERT to allocate id+branch atomically. Shares the prefix constant but not the function; reconcile if they can ever diverge. 2. doc.linkHierarchy passes path.Dir(p.Path) i.e. '.' as fromDir where every other call site uses DirOf ('') — section-proximity lookup misses for root-level parent: targets. Latent; fixing changes resolution behaviour. 3. doc.SplitLog collides entry IDs within a space (two type:log docs -> identical IDs -> second silently overwrites first in one index). Defended in search/ but should be fixed at source in doc/. 4. doc.Scan/DocumentSource are production-dead after service.Archive landed — they're the documented seam but also the API that made web's layering violation writable; delete is a judgement call. 5. mcpsrv ships a beforeCAS test hook in production code (gitx too). 6. No GraphQL complexity limit — service authenticates on its own router, not core-go's WithSchema, so nothing bounds query cost; low risk on a single-user authed surface. 7. Attachments: gitx walks .md only, so image embeds render as visibly-missing; needs WalkBlobs + asset index, or prefer Mermaid by convention.RESOLVED 2026-07-24. All 7 items dispositioned. #1 ProposalBranch 3rd copy in OpenProposal SQL — FIXED (e97532c), guard test. #2 linkHierarchy path.Dir vs DirOf — FIXED. Prior triage called this a judgement call that 'changes resolution behaviour'; that was wrong. Traced it: path.Dir and DirOf differ only for root-level documents, and there the section-proximity step (stemsIn[topSection(fromDir)+'/'+bare]) is subsumed by the same-directory step above it — stemsIn['/x'] is populated only by a root x.md, which byPath['x.md'] already returned. So '.' only ever skipped a lookup that had already answered. VERIFIED by reverting to path.Dir and re-running: identical results. Changed to DirOf as trap-removal, with TestParentResolvesLikeAWikilinkFromTheSameDocument pinning the invariant (parent: resolves to what the same wikilink in the body resolves to). #3 SplitLog id collision — WONTFIX. Already correctly defended in search/extract.go; 'fixing at source' would push index-keyspace uniqueness into doc/, which shouldn't know it is indexed into a shared space index. #4 delete doc.Scan/DocumentSource — DONE. Confirmed production-dead (only consumer was doc's own test fixture). Deleted both; doc/ now owns no way to read a revision, leaving service.Archive as the single route from rev to Archive — closing the seam that made web's layering violation writable. doc test fixture rewritten onto ListDocuments+FromDocuments; TestScanReportsGitErrors relocated to gitx as TestWalkOfAnUnknownRevisionFails (gitx had no unknown-rev coverage). #5 beforeCAS test hook — WONTFIX. Unexported, set only by same-package tests, cannot be set externally. Standard test-seam pattern; mcpsrv has no such hook. #6 GraphQL complexity limit — moved to spec-by6.1 (feature, Phase 5). #7 attachments — moved to spec-by6.2 (feature, Phase 5).closed3choreEugene BlikhNULL2026-07-23T04:09:52ZEugene Blikhbigbes@gmail.com2026-07-24T18:12:42Z2026-07-24T18:12:42ZNULL0NULLNULLNULL000�{}All 7 items dispositioned: 2 fixed (#1,#2), 1 deleted (#4), 2 wontfix (#3,#5), 2 moved to Phase 5 (spec-by6.1, spec-by6.2).0NULLNULLNULL02026-07-24T18:07:15Z0
spec-zqbc07464bc7a9591857c6b728007a315cf1ff427be76a0aa4c2f54c75081d776a2Phase 3: write plane — agents proposeThe agent half of the loop, and the service's whole premise: an agent proposes a document change, gets back a link, a human approves in a browser. Until this ships, agents can only read.Scope: proposals (branch proposals/<id> + Postgres row, state open->merged|rejected); the If-Match tree-splice merge with the four staleness cases and the ancestry check gitx flagged; the single agent token + mandatory provenance trailers; spec_propose / spec_comment over REST + MCP; every write response returns {proposal, url} so the agent can hand over a link. Prereqs found during Phases 1-2: (1) service.ListProposals(space, state) — graph proposal listing is stubbed and db only has ListProposalsByState; (2) IsAncestor(proposalHead, H) check before Merge so an already-merged proposal reports 'already merged' not a confusing 409; (3) auto-merge policy evaluation (.spec.yml AutoMerges) which the reconciler's 'approval=policy' inference also wants. Dispatch like Phases 1-2: proposal orchestration in service/ first and committed, then surface tools fan out. This is where the merge model and reconciler first run under real proposals rather than on paper.closed1featureEugene BlikhNULL2026-07-23T04:08:58ZEugene Blikhbigbes@gmail.com2026-07-23T05:29:29Z2026-07-23T05:29:29ZNULL0NULLNULLNULL000�{}Write plane shipped: service.Propose/Merge/Reject/ListProposals with provenance trailers, auto-merge policy, If-Match staleness + already-merged ancestry check; surfaces graph (Proposals port), mcpsrv spec_propose, and new api/ REST PUT — every write returns {proposal, url}. Validated end-to-end against Postgres. spec_comment deliberately deferred to Phase 5 (spec-by6), which gates the comment schema on the Phase 4 review UI; the approve/reject browser UI is Phase 4 (spec-3vz).0NULLNULLNULL02026-07-23T04:52:59Z0