Third debate team (NEW-critiques-only + agent-ergonomics lens): 3 CONCEDE-PATCH, 1 WITHDRAWN. Prior C1-C8 surface held; new edges caught: - N1: sliceLines is unexported — add task 2.3a to export repo.SliceLines (or lift to shared pkg), get_file_content unchanged, copy-paste forbidden - N2 WITHDRAWN: feared template dispatch collision disproven — commit/issue/pr/status already coexist; mcp-go v0.17.0 matchesTemplate matches by literal-segment regex. Added optional dispatch regression test (3.5) - N3: update_wiki_page has no commit_sha precondition (vs update_file) — acknowledge lost-update window, task 5.6a (optional last_commit_sha if upstream supports) - N4: create-on-existing-title unspecified — conditional guided-error/overwrite-warning branch, task 5.6b Referee finding (verified vs mcp-go server.go:614): literal-slash resource URIs are rejected by matchesTemplate before ParseWiki, so the guided -32602 cannot fire at the resource layer — encoding guidance + get_wiki_page fallback moved to template description. Agent-ergonomics lens (5 gaps): naming convention note, use-returned-page_name, honest demo reframing (no over-claim of client auto-resolution), %2F dead-end fallback, total_lines-always-returned guidance. openspec validate --strict passes. Refs: #32
9.4 KiB
9.4 KiB
1. Raw-HTTP wiki layer (pkg/forgejo/wiki.go)
- 1.1 Add
WikiPage,WikiPageMeta,WikiCommittypes withcontent_base64andsub_urlJSON tags - 1.2 Implement
ListWikiPages(ctx, owner, repo, page, limit)viaDoJSONListonGET /repos/{o}/{r}/wiki/pages(404 → empty); over-fetchlimit+1so the handler can computehas_next - 1.3 Implement
GetWikiPage(ctx, owner, repo, pageName)viaDoJSON, returning the raw struct (decode happens in handler) - 1.4 Implement
GetWikiPageRevisions(ctx, owner, repo, pageName, page, limit)viaDoJSON(notDoJSONList— a 404 here is page-not-found, not empty); over-fetchlimit+1forhas_next - 1.5 Implement
CreateWikiPage(POST/wiki/new) andEditWikiPage(PATCH/wiki/page/{pageName}), both settingcontent_base64 - 1.6 Implement
DeleteWikiPage(DELETE/wiki/page/{pageName}); treat any2xxas success (do not hard-require204) - 1.7 Escape each path segment; for
pageNameuse the round-trip-correct form confirmed in tasks 5.1–5.2 (do not assumeurl.PathEscape/%20works — a dash-normalized page would 404) - 1.8 Unit tests with an
httptestserver: each method, 404-empty for list, 404-error for revisions, 403 →ErrUnauthorized, 404 →ErrNotFound
2. Wiki tools (operation/wiki/wiki.go)
- 2.1 Remove the
//go:build wikitag and allforgejo-sdkwiki imports - 2.2
list_wiki_pages: addpage/limitparams; return pages +pageecho +has_next - 2.3
get_wiki_page: base64-decode content; reuse the shared line-slicer (see 2.3a) forstart_line/end_line; definetotal_lines = len(strings.Split(decoded, "\n"));total_linesalways returned (with or without a range) so an agent can size-then-window; explicit error on undecodable content - 2.3a Make the line-slicer callable across packages: export it in place as
repo.SliceLines(renamesliceLines,operation/repo/file.go:134) OR lift it to a shared package;get_file_content(file.go:123) MUST call the SAME post-rename routine with existing tests still green. REQUIRED before 2.3 (cross-package call to the unexported symbol does not compile); copy-paste is forbidden — it would re-introduce the C8 line-dialect divergence - 2.4
get_wiki_revisions: new tool;page/limit+has_next; 404 → not-found error - 2.5
create_wiki_page/update_wiki_page: base64-encodecontent; default commit message; surface server-assignedpage_name;updateMUST NOT silently rename whentitleomitted (mechanism per task 5.5); if task 5.6a finds a precondition field, accept optionallast_commit_shaand forward it (lost-update guard, mirroringupdate_file's requiredsha); create on an existing title follows task 5.6b (guided "use update" error on reject, or documented-overwrite warning) - 2.6
delete_wiki_page: new tool - 2.7 Rich
mcp.WithDescriptionper tool naming bound params; reuseoperation/paramsdescriptions, add missing ones - 2.8
RegisterTool(s)registers all six; unit tests per handler (happy path + bound + error), including a CRLF/trailing-newlinetotal_linesparity test ("a\r\nb\r\n"and"a\nb\n"→ same count) and an inverted-range error test
3. Wiki resource template (operation/wiki/resources.go)
- 3.1 Add
WikiParams+ParseWikitooperation/resource/parse.go; strict 4-segment parser; read the page-name segment from the escaped path (u.EscapedPath()/RawPath) thenPathUnescapeonly that segment (so%2Fsurvives as one segment) — MUST NOT reusesplitPath(u.Path)for the page name (decoded → would re-split%2F); reject empty (-32602); literal/→ guided-32602("percent-encode as%2F") - 3.2 Register
forgejo://repo/{owner}/{repo}/wiki/{pageName}withRegisterWikiResourceand a self-describing template description (note the%2F/space encoding rule) - 3.3 Handler: two calls (page + revisions). JSON metadata +
text/markdowndecoded-content sidecar capped atMaxInlineDownloadByteswith aget_wiki_pagemarker + boundedrecent_revisionsviaresource.Bounded(..., "get_wiki_revisions").commit_shafrom the page payload. Secondary revisions-call failure degradesrecent_revisionsto empty and still succeeds the read (perissue/resources.go) - 3.4 Map the primary page call's
403→-32002,404→-32003viaresource.MapForgejoError - 3.5 Unit tests: parse (happy, empty,
%20,%2Fsingle-segment and assertu.EscapedPath()retains%2F— provesRawPathwas populated, not recomputed from decodedPath, literal-slash guided error), read (< cap, > cap with sentinel, oversized-body capped, revisions-subcall-failure-degrades-to-empty, 404), and a dispatch test that registers the wiki template alongside the issue/pr/commit templates and assertsresources/readon…/wiki/Homeroutes to the wiki handler (guards literal-segment disambiguation across an mcp-go upgrade — N2)
4. Wiring & discovery
- 4.1 Wire
RegisterWikiToolintoRegisterToolandRegisterWikiResourceintoRegisterCoreResourcesinoperation/operation.go - 4.2 README: add a
**Wiki**group to the tool table (six tools, bound params named) and a wiki row to the Resources table - 4.3
AGENTS.md: noteoperation/wiki/tools + resource (incl. the sub-page%2Fencoding rule and the naming conventionlist_*enumerates /get_*fetches one entity by name) - 4.4
docs/plans/wiki-support.md: header noting the SDK-contribution path is superseded by direct API calls (this change) - 4.5 CHANGELOG: note the additive wiki surface
- 4.6 (Referee doc-policy, endorsed by both debate sides — non-blocking) Add one line to
docs/design/output-bounding.mdextending the invariant to MCP resource content blocks (a data-proportionalresources/readsidecar MUST be capped with a marker naming a range-bound tool, sinceresources/readcarries no caller knob) — so the next resource author does not re-ship the unbounded-body bug C6 caught
5. Live verification (load-bearing)
- 5.1 Against a live Forgejo/Codeberg repo with wiki enabled: confirm the
content_base64field name on read and write - 5.2 Confirm page-name URL rule (spaces → dashes / encoding) for
get/update/deletepaths, and that a name fromcreate/listround-trips verbatim - 5.3 Confirm list/revisions paging params and the effective
limitceiling; document iflimitis advisory. Also confirm the over-fetch interaction: that requestinglimit+1is honored below the ceiling, that at the ceilinghas_nextis computed from the effective returned row count, and pin maxlimittoceiling-1if the server clamps (so a full page at the cap never falsely reportshas_next:false) - 5.4 If any of 5.1–5.7 differ from the spec, correct the spec deltas before sync/archive
- 5.5 Confirm how
update_wiki_pagepreserves the page name whentitleis omitted (reusing theGetting Startedpage from 5.2: PATCH content-only, GET, read the resulting title — server-side retention vs. echoing the existing title); adopt whichever keeps the page reachable, never silently renaming - 5.6 Confirm the page
GETpayload carriescommit_sha; if not, document the fallback (derive fromrecent_revisions[0]) - 5.7 Confirm the upstream round-trips an encoded slash: create
Guides/Setup, read via resource URI…/wiki/Guides%2FSetupand viaget_wiki_pagepage_name=Guides/Setup. If the server rejects/re-splits%2F(proxy orAllowEncodedSlashesoff), document that resource-URI sub-page access is unsupported there (fallback:get_wiki_pagetool) and correct themcp-resource-wikinote before sync - 5.6a Confirm whether
PATCH …/wiki/page/{pageName}accepts an optimistic-concurrency precondition (basecommit_sha/If-Match). If yes: add optionallast_commit_shatoupdate_wiki_pageand forward it; add a stale-write conflict test. If no: document the lost-update (last-writer-wins) window in the tool description and add a missing-falsification note to design.md Risks (N3) - 5.6b Confirm
POST …/wiki/newbehavior when the title already exists (overwrite vs409/422). If reject: map to a guided "page exists, useupdate_wiki_page" error + test. If overwrite: document the destructive behavior in the create tool description. Correct the spec scenario to the observed branch before sync (N4)
5b. Lens follow-ups (api-contract-drift — operational hardening, may land as separate beads)
- 5b.1 Post-decode guard: if
content_base64decodes empty while metadata indicates a non-empty page, surface an explicit error (guards a future Forgejo field rename instead of returning a silent blank body) - 5b.2 Distinguish "wiki disabled" from "page not found": on a wiki
404, probeGET /repos/{o}/{r}has_wiki; if false return a distinct "wiki is not enabled on this repository" error - 5b.3 Document a tested Forgejo/Gitea version range (support matrix) in the README wiki section
6. Showboat demo
- 6.1 Finalize
demo.mdwith real tool calls and expected response shapes for all eight steps (incl. the resource-URI auto-resolution step) - 6.2 Run the demo end-to-end against a throwaway repo; paste actual outputs back into
demo.md
7. Wrap-up
- 7.1
make build+ full test suite pass - 7.2 Remove
Status/Blockedlabel from Codeberg issue #32 and post a closing comment linking the change - 7.3
openspec validate add-wiki-support --strictpasses; archive the change