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
12 KiB
ADDED Requirements
Requirement: Wiki tools use direct REST calls, not the SDK
The wiki tool handlers SHALL be implemented on top of the raw-HTTP helpers in
pkg/forgejo (DoJSON / DoJSONList) and SHALL NOT import or depend on any
forgejo-sdk wiki type or method. The operation/wiki package SHALL compile in the
default build (no //go:build wiki tag) and SHALL be registered from
operation/operation.go via RegisterWikiTool.
Scenario: Default build includes wiki tools
- WHEN the server is built with
make build(no build tags) - THEN the binary SHALL compile
- AND
tools/listSHALL includelist_wiki_pages,get_wiki_page,get_wiki_revisions,create_wiki_page,update_wiki_page, anddelete_wiki_page
Scenario: No SDK wiki dependency
- WHEN the
operation/wikipackage is compiled - THEN it SHALL NOT reference any
forgejo-sdkwiki symbol
Requirement: List wiki pages is bounded and resumable
list_wiki_pages SHALL accept required owner and repo string parameters and optional
page (1-indexed, default 1) and limit (default server page size) integer parameters.
It SHALL call GET /repos/{owner}/{repo}/wiki/pages via DoJSONList and return each
page's title, page_name, and sub_url, plus a page echo and a has_next boolean.
Because the raw-HTTP helper does not expose response headers (so X-Total-Count /
Link are unreachable), has_next SHALL be derived by over-fetching one extra row:
the handler requests limit + 1 items, sets has_next = (rows_received > limit), and
returns at most limit rows. This mirrors the existing embedded-list pattern in
operation/issue/resources.go. A repository with no wiki SHALL return an empty list, not
an error (the 404→empty mapping is correct only for this list endpoint).
Scenario: Repository with pages
- WHEN a client calls
list_wiki_pagesfor a repo whose wiki has pages - THEN the response SHALL list each page's
title,page_name, andsub_url - AND SHALL include the echoed
pageand ahas_nextboolean
Scenario: More pages than limit signals has_next
- WHEN a client calls
list_wiki_pageswithlimit=Nagainst a wiki with more thanNpages - THEN the response SHALL contain exactly
Npages - AND
has_nextSHALL betrue(derived from theN+1-th over-fetched row)
Scenario: Repository without a wiki returns empty list
- WHEN a client calls
list_wiki_pagesfor a repo with no wiki (upstream404) - THEN the response SHALL be an empty page list
- AND SHALL NOT be an error
Requirement: Get wiki page decodes content and bounds by a deterministic line range
get_wiki_page SHALL accept required owner, repo, and page_name parameters and
optional 1-indexed inclusive start_line / end_line integers (the same parameter
names get_file_content exposes). It SHALL call
GET /repos/{owner}/{repo}/wiki/page/{pageName}, base64-decode the content_base64
field into UTF-8 markdown, and return the page title, decoded content, commit_sha,
and total_lines.
total_lines is a field introduced by this tool (get_file_content does not expose
it). To guarantee one line-range dialect across both tools, the line operations SHALL
call the same routine get_file_content uses (currently the unexported sliceLines
in operation/repo/file.go); since a cross-package call requires it, the routine SHALL be
exported in place (repo.SliceLines) or lifted to a shared package, with
get_file_content left calling the identical routine (no behavior change). It SHALL NOT
be copy-pasted (a divergent copy would re-break the single-dialect guarantee). total_lines
SHALL be defined as len(strings.Split(decoded, "\n")) — the count produced by that same
split. The split is on "\n", so a trailing newline yields a final empty line that is
counted, and CRLF content keeps its "\r" on each line. When start_line/end_line are supplied,
content SHALL contain only that inclusive range (clamped to the file extent) and the
response SHALL echo the returned range. An inverted range after clamping SHALL be
reported as an error, matching sliceLines.
Scenario: Full page read reports total lines
- WHEN a client calls
get_wiki_pagewithout a line range - THEN
contentSHALL be the full decoded markdown - AND
total_linesSHALL equallen(strings.Split(content, "\n"))
Scenario: Line range slices the content
- WHEN a client calls
get_wiki_pagewithstart_line=1andend_line=5on a page whosetotal_linesexceeds 5 - THEN
contentSHALL contain exactly lines 1–5 under the shared split rule - AND the response SHALL report
total_linesand echo the returned range so the caller can request the remainder
Scenario: Line counting is identical for CRLF and LF content
- WHEN
get_wiki_pagedecodes a body"a\r\nb\r\n"and another body"a\nb\n" - THEN both SHALL report the same
total_lines(3, including the trailing empty line) - AND the count SHALL be produced by the same split routine
get_file_contentuses
Scenario: Undecodable content is an explicit error
- WHEN the upstream
content_base64field is not valid base64 - THEN the tool SHALL return an error
- AND SHALL NOT return the raw base64 string as content
Requirement: Get wiki revisions is bounded, resumable, and errors on missing pages
get_wiki_revisions SHALL accept required owner, repo, page_name and optional
page / limit parameters, call GET /repos/{owner}/{repo}/wiki/revisions/{pageName}
via DoJSON (not DoJSONList), and return each revision's sha, author, and
message, plus the echoed page and a has_next boolean derived by the same limit+1
over-fetch as list_wiki_pages.
Unlike list_wiki_pages, a 404 here means the page does not exist (every existing wiki
page has at least one revision), so it SHALL be reported as a not-found error, not as
an empty list.
Scenario: Page with multiple revisions
- WHEN a client calls
get_wiki_revisionsfor a page edited more than once - THEN the response SHALL list each revision's
sha,author, andmessage - AND SHALL include the echoed
pageandhas_next
Scenario: Revisions of a nonexistent page is an error
- WHEN a client calls
get_wiki_revisionsfor a page that does not exist (upstream404) - THEN the tool SHALL return a not-found error
- AND SHALL NOT return an empty revision list
Requirement: Create wiki page base64-encodes content
create_wiki_page SHALL accept required owner, repo, title, content and optional
message parameters. It SHALL base64-encode content into the content_base64 field of
a POST /repos/{owner}/{repo}/wiki/new request. When message is empty the handler
SHALL supply a default commit message naming the page. The response SHALL surface the
server-assigned page_name (the server normalizes the title into a page name), so
callers can address the page in subsequent calls. Callers SHALL use the returned
page_name verbatim for get/update/delete; they MUST NOT derive it from title
(the server may transform it, e.g. spaces → dashes).
The behavior of POST …/wiki/new against an already-existing title is fixed by live
verification. If the upstream rejects a duplicate (e.g. 409/422), create_wiki_page
SHALL surface a guided error naming update_wiki_page as the way to modify an existing
page (it SHALL NOT leak an opaque transport error). If the upstream instead overwrites,
create_wiki_page's description SHALL warn that creating an existing title replaces it,
so callers do not silently clobber a page by confusing create with update.
Scenario: Create with default message
- WHEN a client calls
create_wiki_pagewith a title and content but no message - THEN the request body SHALL carry the base64-encoded content
- AND a non-empty default commit message naming the page SHALL be sent
- AND the response SHALL include the server-assigned
page_name
Scenario: Create on an existing title is not a silent clobber
- WHEN a client calls
create_wiki_pagewith atitlethat already exists - THEN the tool SHALL either return a guided error pointing at
update_wiki_page(if the upstream rejects the duplicate) - OR report success only if the upstream's documented behavior is overwrite, in which case the destructive replacement SHALL be stated in the tool description
Requirement: Update wiki page base64-encodes content and never silently renames
update_wiki_page SHALL accept required owner, repo, page_name, content and
optional title / message parameters. It SHALL base64-encode content and send a
PATCH /repos/{owner}/{repo}/wiki/page/{pageName}. When title is omitted, the update
SHALL NOT silently rename the page: the page addressed by page_name SHALL remain
reachable under the same name after the edit. The mechanism that preserves the name
(server-side retention vs. echoing the existing title) is fixed by live verification
(see the live-verification tasks).
update_wiki_page performs a read-modify-write whose last writer wins. Whether the
upstream PATCH …/wiki/page/{pageName} accepts an optimistic-concurrency precondition
(e.g. a base commit_sha / If-Match) is fixed by live verification. If the API accepts
such a field, update_wiki_page SHALL accept an optional last_commit_sha parameter
(sourced from get_wiki_page's commit_sha) and forward it so a stale write is rejected
rather than silently clobbering a concurrent edit. If the API accepts no precondition, the
tool SHALL document the lost-update window in its description so callers know concurrent
edits overwrite without warning. (This mirrors update_file/delete_file, which require
a base sha.)
Scenario: Update without retitling preserves the page name
- WHEN a client calls
update_wiki_pagewith new content but notitle - THEN the page SHALL remain reachable under its original
page_nameafter the edit - AND the request body SHALL carry the base64-encoded new content
Scenario: Stale update is rejected when the API supports a precondition
- WHEN the upstream accepts a base-commit precondition
- AND a client calls
update_wiki_pagewith alast_commit_shathat is no longer current - THEN the tool SHALL surface the upstream conflict as an error
- AND SHALL NOT silently overwrite the newer revision
Requirement: Delete wiki page
delete_wiki_page SHALL accept required owner, repo, page_name parameters and send
DELETE /repos/{owner}/{repo}/wiki/page/{pageName}. Any 2xx response SHALL be reported
as success (204 No Content is the canonical case; the handler MUST NOT hard-require
exactly 204, since some instances return 200).
Scenario: Delete existing page
- WHEN a client calls
delete_wiki_pagefor an existing page - AND the upstream returns a
2xx - THEN the tool SHALL report success
Scenario: Unauthorized write maps to error
- WHEN a wiki write tool is called with a token lacking write access (upstream
403) - THEN the tool SHALL return an error reflecting the unauthorized status
Requirement: Wiki tools and their bounds are documented for discovery
Every wiki tool SHALL carry an mcp.WithDescription that states its purpose and names
its bound parameters. The README tool table SHALL include a Wiki group listing all six
tools, and AGENTS.md SHALL note the operation/wiki/ package. Bound parameters
(page, limit, start_line, end_line) SHALL appear in both the tool description and
the README per docs/design/output-bounding.md.
To keep the surface legible to an agent picking a tool without trial-and-error, the descriptions SHALL also state the discovery affordances surfaced by the agent-ergonomics review:
AGENTS.mdSHALL state the naming conventionlist_*enumerates /get_*fetches one entity by name (so an agent does not guessget_wiki_pages).create_wiki_page's description SHALL tell callers to address the page by the returnedpage_name, never by deriving it fromtitle.get_wiki_page's description SHALL state thattotal_linesis always returned (with or without a range), so an agent can call once unbounded to learn the size, then request astart_line/end_linewindow for a large page.
Scenario: README documents the bounds
- WHEN a reader views the README tool table
- THEN a
Wikigroup SHALL list all six wiki tools - AND the bound parameters of
list_wiki_pages,get_wiki_page, andget_wiki_revisionsSHALL be named in their description column