wiki-viewer 1.4.2 → 1.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.next/standalone/.next/BUILD_ID +1 -1
- package/.next/standalone/.next/app-path-routes-manifest.json +5 -0
- package/.next/standalone/.next/build-manifest.json +3 -3
- package/.next/standalone/.next/prerender-manifest.json +3 -27
- package/.next/standalone/.next/routes-manifest.json +34 -0
- package/.next/standalone/.next/server/app/_global-error/page.js +2 -2
- package/.next/standalone/.next/server/app/_global-error/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_global-error.html +1 -1
- package/.next/standalone/.next/server/app/_global-error.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/__PAGE__.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_full.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_head.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_index.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_global-error.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found/page.js +4 -4
- package/.next/standalone/.next/server/app/_not-found/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/_not-found.html +1 -1
- package/.next/standalone/.next/server/app/_not-found.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found.segments/_full.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found.segments/_head.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found.segments/_index.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found/__PAGE__.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found.segments/_not-found.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/_not-found.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/api/agent/activity/route.js +5 -5
- package/.next/standalone/.next/server/app/api/agent/activity/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/agent/admin/agents/[agentId]/revoke/route.js +6 -6
- package/.next/standalone/.next/server/app/api/agent/admin/agents/[agentId]/revoke/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/agent/admin/agents/route.js +6 -6
- package/.next/standalone/.next/server/app/api/agent/admin/agents/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/agent/admin/registrations/[regId]/approve/route.js +6 -6
- package/.next/standalone/.next/server/app/api/agent/admin/registrations/[regId]/approve/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/agent/admin/registrations/[regId]/deny/route.js +4 -4
- package/.next/standalone/.next/server/app/api/agent/admin/registrations/[regId]/deny/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/agent/admin/registrations/route.js +4 -4
- package/.next/standalone/.next/server/app/api/agent/admin/registrations/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/agent/events/[...path]/route.js +5 -5
- package/.next/standalone/.next/server/app/api/agent/events/[...path]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/agent/files/[...path]/route.js +11 -10
- package/.next/standalone/.next/server/app/api/agent/files/[...path]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/agent/fs/file/[...path]/route/app-paths-manifest.json +3 -0
- package/.next/standalone/.next/server/app/api/agent/fs/file/[...path]/route/build-manifest.json +9 -0
- package/.next/standalone/.next/server/app/api/agent/fs/file/[...path]/route/server-reference-manifest.json +4 -0
- package/.next/standalone/.next/server/app/api/agent/fs/file/[...path]/route.js +17 -0
- package/.next/standalone/.next/server/app/api/agent/fs/file/[...path]/route.js.map +5 -0
- package/.next/standalone/.next/server/app/api/agent/fs/file/[...path]/route.js.nft.json +1 -0
- package/.next/standalone/.next/server/app/api/agent/fs/file/[...path]/route_client-reference-manifest.js +3 -0
- package/.next/standalone/.next/server/app/api/agent/fs/ls/[[...path]]/route/app-paths-manifest.json +3 -0
- package/.next/standalone/.next/server/app/api/agent/fs/ls/[[...path]]/route/build-manifest.json +9 -0
- package/.next/standalone/.next/server/app/api/agent/fs/ls/[[...path]]/route/server-reference-manifest.json +4 -0
- package/.next/standalone/.next/server/app/api/agent/fs/ls/[[...path]]/route.js +15 -0
- package/.next/standalone/.next/server/app/api/agent/fs/ls/[[...path]]/route.js.map +5 -0
- package/.next/standalone/.next/server/app/api/agent/fs/ls/[[...path]]/route.js.nft.json +1 -0
- package/.next/standalone/.next/server/app/api/agent/fs/ls/[[...path]]/route_client-reference-manifest.js +3 -0
- package/.next/standalone/.next/server/app/api/agent/fs/move/route/app-paths-manifest.json +3 -0
- package/.next/standalone/.next/server/app/api/agent/fs/move/route/build-manifest.json +9 -0
- package/.next/standalone/.next/server/app/api/agent/fs/move/route/server-reference-manifest.json +4 -0
- package/.next/standalone/.next/server/app/api/agent/fs/move/route.js +15 -0
- package/.next/standalone/.next/server/app/api/agent/fs/move/route.js.map +5 -0
- package/.next/standalone/.next/server/app/api/agent/fs/move/route.js.nft.json +1 -0
- package/.next/standalone/.next/server/app/api/agent/fs/move/route_client-reference-manifest.js +3 -0
- package/.next/standalone/.next/server/app/api/agent/fs/search/route/app-paths-manifest.json +3 -0
- package/.next/standalone/.next/server/app/api/agent/fs/search/route/build-manifest.json +9 -0
- package/.next/standalone/.next/server/app/api/agent/fs/search/route/server-reference-manifest.json +4 -0
- package/.next/standalone/.next/server/app/api/agent/fs/search/route.js +15 -0
- package/.next/standalone/.next/server/app/api/agent/fs/search/route.js.map +5 -0
- package/.next/standalone/.next/server/app/api/agent/fs/search/route.js.nft.json +1 -0
- package/.next/standalone/.next/server/app/api/agent/fs/search/route_client-reference-manifest.js +3 -0
- package/.next/standalone/.next/server/app/api/agent/internal/span/route.js +8 -8
- package/.next/standalone/.next/server/app/api/agent/internal/span/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/agent/settings/route.js +8 -8
- package/.next/standalone/.next/server/app/api/agent/settings/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/agent/sidecar/[...path]/route.js +7 -7
- package/.next/standalone/.next/server/app/api/agent/sidecar/[...path]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/app-proxy/[...path]/route.js +1 -1
- package/.next/standalone/.next/server/app/api/app-proxy/[...path]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/assets/[...path]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/auth/[...all]/route.js +3 -3
- package/.next/standalone/.next/server/app/api/auth/[...all]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/system/auth-config/route.js +5 -5
- package/.next/standalone/.next/server/app/api/system/auth-config/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/system/auth-settings/route.js +4 -4
- package/.next/standalone/.next/server/app/api/system/auth-settings/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/system/browse/route.js +6 -6
- package/.next/standalone/.next/server/app/api/system/browse/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/system/clear-root/route.js +5 -5
- package/.next/standalone/.next/server/app/api/system/clear-root/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/system/config/route.js +5 -5
- package/.next/standalone/.next/server/app/api/system/config/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/system/pins/route.js +4 -4
- package/.next/standalone/.next/server/app/api/system/pins/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/system/reveal/route.js +4 -4
- package/.next/standalone/.next/server/app/api/system/reveal/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/system/root-status/route.js +4 -4
- package/.next/standalone/.next/server/app/api/system/root-status/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/system/set-root/route.js +5 -5
- package/.next/standalone/.next/server/app/api/system/set-root/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/upload/[...path]/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/wiki/app/route.js +6 -6
- package/.next/standalone/.next/server/app/api/wiki/app/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/wiki/content/route.js +6 -6
- package/.next/standalone/.next/server/app/api/wiki/content/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/wiki/file/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/wiki/folder/route.js +5 -5
- package/.next/standalone/.next/server/app/api/wiki/folder/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/wiki/move/route.js +6 -6
- package/.next/standalone/.next/server/app/api/wiki/move/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/wiki/new-file/route.js +5 -5
- package/.next/standalone/.next/server/app/api/wiki/new-file/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/wiki/page/route.js +5 -5
- package/.next/standalone/.next/server/app/api/wiki/page/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/wiki/presence/route/app-paths-manifest.json +3 -0
- package/.next/standalone/.next/server/app/api/wiki/presence/route/build-manifest.json +9 -0
- package/.next/standalone/.next/server/app/api/wiki/presence/route/server-reference-manifest.json +4 -0
- package/.next/standalone/.next/server/app/api/wiki/presence/route.js +14 -0
- package/.next/standalone/.next/server/app/api/wiki/presence/route.js.map +5 -0
- package/.next/standalone/.next/server/app/api/wiki/presence/route.js.nft.json +1 -0
- package/.next/standalone/.next/server/app/api/wiki/presence/route_client-reference-manifest.js +3 -0
- package/.next/standalone/.next/server/app/api/wiki/route.js +5 -5
- package/.next/standalone/.next/server/app/api/wiki/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/wiki/slugs/route.js +5 -5
- package/.next/standalone/.next/server/app/api/wiki/slugs/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/wiki/upload/route.js +5 -5
- package/.next/standalone/.next/server/app/api/wiki/upload/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/api/wiki/watch/route.js +6 -6
- package/.next/standalone/.next/server/app/api/wiki/watch/route.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/index.html +1 -1
- package/.next/standalone/.next/server/app/index.rsc +2 -2
- package/.next/standalone/.next/server/app/index.segments/__PAGE__.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/index.segments/_full.segment.rsc +2 -2
- package/.next/standalone/.next/server/app/index.segments/_head.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/index.segments/_index.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/index.segments/_tree.segment.rsc +1 -1
- package/.next/standalone/.next/server/app/page.js +4 -4
- package/.next/standalone/.next/server/app/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app/signin/page.js +9 -4
- package/.next/standalone/.next/server/app/signin/page.js.nft.json +1 -1
- package/.next/standalone/.next/server/app/signin/page_client-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/app-paths-manifest.json +5 -0
- package/.next/standalone/.next/server/chunks/0p91_next_dist_esm_build_templates_app-route_0d2jbo5.js +3 -0
- package/.next/standalone/.next/server/chunks/0p91_next_dist_esm_build_templates_app-route_0ytral2.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0269d2e._.js +1 -1
- package/.next/standalone/.next/server/chunks/{[root-of-the-server]__03h3~3e._.js → [root-of-the-server]__04udrya._.js} +2 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__05f6x0i._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__06-dtd0._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__06sek6_._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__09s9qkd._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0d75a5q._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0d_da6q._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0e7si9p._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0hhhvm7._.js +1 -1
- package/.next/standalone/.next/server/chunks/{[root-of-the-server]__04g0j8i._.js → [root-of-the-server]__0kdn1le._.js} +2 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0kk26wc._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0kur4ro._.js +15 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0p12-6~._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0qc2~6l._.js +15 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0s~i040._.js +3 -0
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0w47wil._.js +1 -1
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0xq124z._.js +3 -0
- package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0l..bxz._.js → [root-of-the-server]__0xz-_0v._.js} +2 -2
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__10w2q~o._.js +1 -1
- package/.next/standalone/.next/server/chunks/_079xuln._.js +3 -0
- package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_agent_fs_file_[___path]_route_actions_0gb11t~.js +3 -0
- package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_agent_fs_ls_[[___path]]_route_actions_0wfg4nl.js +3 -0
- package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_agent_fs_move_route_actions_0z34mqq.js +3 -0
- package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_agent_fs_search_route_actions_0ut_ug..js +3 -0
- package/.next/standalone/.next/server/chunks/_next-internal_server_app_api_wiki_presence_route_actions_0x9d7f4.js +3 -0
- package/.next/standalone/.next/server/chunks/{[root-of-the-server]__0d89o98._.js → src_lib_auth_server_ts_00f4e0h._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/0biy_@better-auth_core_dist_121m_lh._.js +5 -0
- package/.next/standalone/.next/server/chunks/ssr/0dhv_better-auth_dist_adapters_kysely-adapter_index_mjs_08ymlcb._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/0dti_kysely_dist_esm_0-t3o9q._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/0dti_kysely_dist_esm_01._58q._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/0p91_next_0f40gy0._.js +6 -0
- package/.next/standalone/.next/server/chunks/ssr/{0p91_next_dist_0~2d1tl._.js → 0p91_next_dist_10ud_sa._.js} +2 -2
- package/.next/standalone/.next/server/chunks/ssr/0sag_@better-auth_memory-adapter_dist_index_mjs_07-_ka6._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/0ugq_@better-auth_kysely-adapter_dist_bun-sqlite-dialect-DzNwOpKv_mjs_0lruvb8._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/0ugq_@better-auth_kysely-adapter_dist_d1-sqlite-dialect-C2B7YsIT_mjs_03n6~tc._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/0ugq_@better-auth_kysely-adapter_dist_index_mjs_0gi5w.w._.js +4 -0
- package/.next/standalone/.next/server/chunks/ssr/0ugq_@better-auth_kysely-adapter_dist_node-sqlite-dialect_mjs_0l4.y~0._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/12y~_mermaid_dist_chunks_mermaid_core_diagram-OG6HWLK6_mjs_0t5sus6._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/12y~_mermaid_dist_chunks_mermaid_core_sankeyDiagram-5OEKKPKP_mjs_11ifrnu._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__026s8~z._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__06mzg2t._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0btcd4o._.js +12 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0c~og-9._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__134_q7l._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/_0pqaawz._.js +3 -3
- package/.next/standalone/.next/server/chunks/ssr/node_modules__pnpm_051tcva._.js +3 -0
- package/.next/standalone/.next/server/chunks/ssr/node_modules__pnpm_0o~d81.._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/node_modules__pnpm_0y9k6h~._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/node_modules__pnpm_0znncy9._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/node_modules__pnpm_12-ni1n._.js +1 -1
- package/.next/standalone/.next/server/chunks/ssr/src_app_signin_00yk0vm._.js +452 -0
- package/.next/standalone/.next/server/chunks/ssr/src_app_signin_signin-form_tsx_0eznlbd._.js +3 -0
- package/.next/standalone/.next/server/functions-config-manifest.json +5 -0
- package/.next/standalone/.next/server/middleware-build-manifest.js +3 -3
- package/.next/standalone/.next/server/middleware-manifest.json +5 -5
- package/.next/standalone/.next/server/pages/404.html +1 -1
- package/.next/standalone/.next/server/pages/500.html +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.js +1 -1
- package/.next/standalone/.next/server/server-reference-manifest.json +1 -1
- package/.next/standalone/.next/static/chunks/09-jtayuhrq~b.js +1 -0
- package/.next/standalone/.next/static/chunks/0uv7ozzpiyp2_.js +1 -0
- package/.next/standalone/.next/static/chunks/135c~rq_ux73w.js +1 -0
- package/.next/standalone/.next/static/chunks/{11s9zwr7edrs7.js → 14luv~jq7e6-n.js} +12 -12
- package/.next/standalone/README.md +79 -16
- package/.next/standalone/agents/bootstrap-prompt.md +12 -1
- package/.next/standalone/agents/wiki-viewer-skill/SKILL.md +199 -6
- package/.next/standalone/docs/agent-fs-plan.md +65 -7
- package/.next/standalone/docs/file-vs-collab-authority.md +124 -0
- package/.next/standalone/package.json +1 -1
- package/.next/standalone/packages/wiki-viewer-mcp/README.md +194 -0
- package/.next/standalone/packages/wiki-viewer-mcp/dist/http-client.js +220 -0
- package/.next/standalone/packages/wiki-viewer-mcp/dist/index.js +472 -0
- package/.next/standalone/packages/wiki-viewer-mcp/dist/register.js +94 -0
- package/.next/standalone/packages/wiki-viewer-mcp/dist/state-cache.js +26 -0
- package/.next/standalone/packages/wiki-viewer-mcp/package-lock.json +1716 -0
- package/.next/standalone/packages/wiki-viewer-mcp/package.json +41 -0
- package/.next/standalone/packages/wiki-viewer-mcp/src/http-client.ts +344 -0
- package/.next/standalone/packages/wiki-viewer-mcp/src/index.ts +575 -0
- package/.next/standalone/packages/wiki-viewer-mcp/src/register.ts +144 -0
- package/.next/standalone/packages/wiki-viewer-mcp/src/state-cache.ts +45 -0
- package/.next/standalone/packages/wiki-viewer-mcp/tsconfig.json +19 -0
- package/.next/standalone/pnpm-lock.yaml +9 -9
- package/.next/standalone/src/app/api/agent/files/[...path]/route.ts +11 -1
- package/.next/standalone/src/app/api/agent/fs/file/[...path]/route.ts +344 -0
- package/.next/standalone/src/app/api/agent/fs/ls/[[...path]]/route.ts +147 -0
- package/.next/standalone/src/app/api/agent/fs/move/route.ts +125 -0
- package/.next/standalone/src/app/api/agent/fs/search/route.ts +236 -0
- package/.next/standalone/src/app/api/agent/register/route.ts +3 -3
- package/.next/standalone/src/app/api/agents/install/route.ts +44 -2
- package/.next/standalone/src/app/api/wiki/move/route.ts +10 -1
- package/.next/standalone/src/app/api/wiki/presence/route.ts +55 -0
- package/.next/standalone/src/app/signin/page.tsx +10 -225
- package/.next/standalone/src/app/signin/signin-form.tsx +238 -0
- package/.next/standalone/src/components/ai-panel/ai-panel.tsx +49 -0
- package/.next/standalone/src/components/editor/editor.tsx +38 -0
- package/.next/standalone/src/lib/proof/audit.ts +79 -0
- package/.next/standalone/src/lib/proof/auth.ts +2 -2
- package/.next/standalone/src/lib/proof/collab-state.ts +79 -0
- package/.next/standalone/src/lib/proof/lease.ts +104 -0
- package/.next/standalone/src/lib/proof/ops-applier.ts +74 -25
- package/.next/standalone/src/lib/proof/raw-fs.ts +198 -0
- package/.next/standalone/src/lib/proof/registry.ts +1 -1
- package/.next/standalone/src/lib/proof/sidecar.ts +38 -1
- package/.next/standalone/src/lib/proof/types.ts +13 -1
- package/.next/standalone/src/tests/proof/agent-fs.test.ts +824 -0
- package/.next/standalone/src/tests/proof/agents-install.test.ts +53 -0
- package/.next/standalone/src/tests/proof/collab-state.test.ts +499 -0
- package/.next/standalone/src/tests/proof/reconcile-sidecar.test.ts +195 -0
- package/.next/standalone/src/tests/proof/sidecar-lifecycle.test.ts +177 -0
- package/.next/standalone/tsconfig.tsbuildinfo +1 -1
- package/README.md +79 -16
- package/package.json +1 -1
- package/.next/standalone/.next/server/app/signin.html +0 -1
- package/.next/standalone/.next/server/app/signin.meta +0 -15
- package/.next/standalone/.next/server/app/signin.rsc +0 -23
- package/.next/standalone/.next/server/app/signin.segments/_full.segment.rsc +0 -23
- package/.next/standalone/.next/server/app/signin.segments/_head.segment.rsc +0 -6
- package/.next/standalone/.next/server/app/signin.segments/_index.segment.rsc +0 -7
- package/.next/standalone/.next/server/app/signin.segments/_tree.segment.rsc +0 -3
- package/.next/standalone/.next/server/app/signin.segments/signin/__PAGE__.segment.rsc +0 -9
- package/.next/standalone/.next/server/app/signin.segments/signin.segment.rsc +0 -5
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__09~~-nd._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__0w87kiz._.js +0 -3
- package/.next/standalone/.next/server/chunks/[root-of-the-server]__12ssbhv._.js +0 -3
- package/.next/standalone/.next/server/chunks/node_modules__pnpm_0g0fhfk._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__088hqzy._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0c147fo._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__0z.1v1z._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/[root-of-the-server]__12zp2dm._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/node_modules__pnpm_0exqvyf._.js +0 -3
- package/.next/standalone/.next/server/chunks/ssr/src_app_signin_page_tsx_1143y49._.js +0 -3
- package/.next/standalone/.next/static/chunks/0-_39e4awbk8y.js +0 -1
- package/.next/standalone/.next/static/chunks/02bxx~2cg~s6~.js +0 -1
- package/.next/standalone/.next/static/chunks/0mj_f8ncf2tks.js +0 -1
- /package/.next/standalone/.next/static/{OwXjVuoXDC_gda_BKji1J → 19j4-_brJLoulXAMekpM2}/_buildManifest.js +0 -0
- /package/.next/standalone/.next/static/{OwXjVuoXDC_gda_BKji1J → 19j4-_brJLoulXAMekpM2}/_clientMiddlewareManifest.js +0 -0
- /package/.next/standalone/.next/static/{OwXjVuoXDC_gda_BKji1J → 19j4-_brJLoulXAMekpM2}/_ssgManifest.js +0 -0
|
@@ -22,8 +22,10 @@
|
|
|
22
22
|
Originally a zero-config single-user tool, it now supports:
|
|
23
23
|
|
|
24
24
|
- Multi-user sign-in (Google OAuth or email + password) for teams of 3 to 10 sharing a VPS.
|
|
25
|
-
- An HTTP
|
|
25
|
+
- An HTTP API for AI agents with **two tiers**: a raw filesystem (read/write/edit/list/search/move/delete for **all file types**) for fast filework, and a Markdown collaboration layer with provenance marks, comments, suggestions, and revision-checked mutations.
|
|
26
|
+
- A **working-vs-collaborating** safety model: when you have a Markdown doc open, agents automatically defer to the reviewable collab path instead of overwriting it.
|
|
26
27
|
- Per-agent registration and scoped tokens. No shared bearer secret.
|
|
28
|
+
- An optional `npx wiki-viewer-mcp` adapter so MCP-capable agents (Claude Code, Cursor, Codex) get native file tools against a remote instance.
|
|
27
29
|
|
|
28
30
|
Single-user, no-auth mode still works. Auth turns on automatically once anyone signs up.
|
|
29
31
|
|
|
@@ -279,7 +281,23 @@ The Markdown editor now sends a `baseRevision` with every save. If another user
|
|
|
279
281
|
|
|
280
282
|
## Working with AI agents
|
|
281
283
|
|
|
282
|
-
wiki-viewer exposes an HTTP
|
|
284
|
+
wiki-viewer exposes an HTTP API so agents (Claude, Cursor, ChatGPT desktop, custom scripts) can work with files in a running instance — locally or remotely — almost as if they were on their own filesystem. There are **two tiers**, sharing one auth/scope/lock spine:
|
|
285
|
+
|
|
286
|
+
- **Tier 1 — Raw filesystem** (`/api/agent/fs/*`): `read`, `write`, `edit`, `list`, `search`, `move`, `delete` for **every file type** (code, configs, PDFs, notebooks, Markdown — anything). Fast, boring, byte-accurate. Mutations are audited.
|
|
287
|
+
- **Tier 2 — Markdown collaboration** (`/api/agent/files/*`): structured block-ops where every AI-authored insert is wrapped in an inline `<proof-span>` mark so the human reviewer can see, accept, or revert each change, plus comments and suggestions. Tier-2 is API-compatible in spirit with [Proof SDK](https://github.com/EveryInc/proof-sdk).
|
|
288
|
+
|
|
289
|
+
### Which tier? Working mode vs collaborating mode
|
|
290
|
+
|
|
291
|
+
Before editing a Markdown file, an agent reads it and checks the **`X-Collab-State`** response header:
|
|
292
|
+
|
|
293
|
+
| `X-Collab-State` | Meaning | Agent uses |
|
|
294
|
+
| ---------------- | ---------------------------------------------------------------- | ------------------------------------------------------------------------------- |
|
|
295
|
+
| `active` | A human has the doc open, or it has pending suggestions/comments | **Tier-2 block-ops** (reviewable). A raw write is rejected `409 COLLAB_ACTIVE`. |
|
|
296
|
+
| `tracked` | Has a collab sidecar, nobody editing now | Prefer Tier-2 for prose; raw OK for mechanical edits |
|
|
297
|
+
| `untracked` | Plain Markdown, never collaborated on | Either tier |
|
|
298
|
+
| `not-markdown` | Any non-`.md` file | **Tier-1 raw only** |
|
|
299
|
+
|
|
300
|
+
This is enforced, not advisory: the collab state is re-checked atomically inside the write lock, so an agent can never silently clobber a doc you just opened. The browser editor sends a presence heartbeat to drive the `active` state. See [`docs/file-vs-collab-authority.md`](docs/file-vs-collab-authority.md) for the full authority model.
|
|
283
301
|
|
|
284
302
|
The protocol is intentionally API-compatible in spirit with [Proof SDK](https://github.com/EveryInc/proof-sdk).
|
|
285
303
|
|
|
@@ -306,7 +324,42 @@ npx skills add anh-chu/wiki-viewer/agents/wiki-viewer-skill
|
|
|
306
324
|
|
|
307
325
|
For any chat agent, paste the bootstrap prompt from `<your-server>/agents/install` (also visible in the AI Panel). The agent fetches `/api/agents/install` and learns the full op vocabulary at runtime.
|
|
308
326
|
|
|
309
|
-
###
|
|
327
|
+
### Tier 1 — raw filesystem
|
|
328
|
+
|
|
329
|
+
Work with any file type. All routes take `Authorization: Bearer <token>` + `X-Agent-Id`, are scope-checked, and reject path traversal, symlink escapes, and anything under `.proof/`.
|
|
330
|
+
|
|
331
|
+
```bash
|
|
332
|
+
# Read (bytes; ETag is the sha256, supports Range, returns X-Collab-State)
|
|
333
|
+
curl -sD- -H "Authorization: Bearer $TOKEN" -H "X-Agent-Id: ai:claude" \
|
|
334
|
+
https://wiki.team.com/api/agent/fs/file/src/util.ts
|
|
335
|
+
|
|
336
|
+
# Write/overwrite (atomic). If-Match: <sha256> is required to overwrite;
|
|
337
|
+
# omit it to create. ?mkdirs=true creates parent dirs. ?force=true overrides (audited).
|
|
338
|
+
curl -s -X PUT -H "Authorization: Bearer $TOKEN" -H "X-Agent-Id: ai:claude" \
|
|
339
|
+
-H "If-Match: <sha256-from-read>" --data-binary @util.ts \
|
|
340
|
+
https://wiki.team.com/api/agent/fs/file/src/util.ts
|
|
341
|
+
|
|
342
|
+
# List a directory (scope-filtered)
|
|
343
|
+
curl -s -H "Authorization: Bearer $TOKEN" -H "X-Agent-Id: ai:claude" \
|
|
344
|
+
"https://wiki.team.com/api/agent/fs/ls/src?recursive=true&limit=500"
|
|
345
|
+
|
|
346
|
+
# Search (server-side; kills round-trips)
|
|
347
|
+
curl -s -X POST -H "Authorization: Bearer $TOKEN" -H "X-Agent-Id: ai:claude" \
|
|
348
|
+
-H "Content-Type: application/json" \
|
|
349
|
+
-d '{"kind":"grep","query":"TODO","glob":"**/*.ts"}' \
|
|
350
|
+
https://wiki.team.com/api/agent/fs/search
|
|
351
|
+
|
|
352
|
+
# Move and delete (delete needs the `delete` scope + If-Match)
|
|
353
|
+
curl -s -X POST -H "Authorization: Bearer $TOKEN" -H "X-Agent-Id: ai:claude" \
|
|
354
|
+
-H "Content-Type: application/json" -d '{"from":"a.md","to":"b.md"}' \
|
|
355
|
+
https://wiki.team.com/api/agent/fs/move
|
|
356
|
+
curl -s -X DELETE -H "Authorization: Bearer $TOKEN" -H "X-Agent-Id: ai:claude" \
|
|
357
|
+
-H "If-Match: <sha256>" https://wiki.team.com/api/agent/fs/file/old.md
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
Mutating a Markdown file via raw write emits a `file.rawWritten` event and re-binds the collab sidecar; if the doc is `active` it is rejected `409 COLLAB_ACTIVE` (use Tier-2 instead). Use the **`npx wiki-viewer-mcp`** adapter to get all of this as standard MCP file tools — it handles `If-Match`, mode-awareness, and edit-via-read-then-write for you.
|
|
361
|
+
|
|
362
|
+
### Tier 2 — op vocabulary (Markdown)
|
|
310
363
|
|
|
311
364
|
Block-level edits (revision-checked, idempotent):
|
|
312
365
|
|
|
@@ -356,15 +409,23 @@ Owner-only (session cookie):
|
|
|
356
409
|
|
|
357
410
|
Agent routes (bearer + `X-Agent-Id`, scope-checked):
|
|
358
411
|
|
|
359
|
-
| Method
|
|
360
|
-
|
|
|
361
|
-
| `GET`
|
|
362
|
-
| `
|
|
363
|
-
| `
|
|
364
|
-
| `
|
|
365
|
-
| `
|
|
366
|
-
| `
|
|
367
|
-
| `GET`
|
|
412
|
+
| Method | Path | Required scope | Tier |
|
|
413
|
+
| -------- | ---------------------------------------- | --------------------- | ---- |
|
|
414
|
+
| `GET` | `/api/agent/fs/file/<path>` | `read` + path match | 1 |
|
|
415
|
+
| `PUT` | `/api/agent/fs/file/<path>` | `mutate` + path match | 1 |
|
|
416
|
+
| `DELETE` | `/api/agent/fs/file/<path>` | `delete` + path match | 1 |
|
|
417
|
+
| `GET` | `/api/agent/fs/ls/<path>` | `read` + path match | 1 |
|
|
418
|
+
| `POST` | `/api/agent/fs/move` | `mutate` (src+dest) | 1 |
|
|
419
|
+
| `POST` | `/api/agent/fs/search` | `read` (per match) | 1 |
|
|
420
|
+
| `GET` | `/api/agent/files/<path.md>` | `read` + path match | 2 |
|
|
421
|
+
| `POST` | `/api/agent/files/<path.md>` | `mutate` + path match | 2 |
|
|
422
|
+
| `GET` | `/api/agent/events/<path.md>?after=<id>` | `read` + path match | 2 |
|
|
423
|
+
| `POST` | `/api/agent/events/<path.md>` | `read` + path match | 2 |
|
|
424
|
+
| `GET` | `/api/agent/sidecar/<path.md>` | `read` + path match | 2 |
|
|
425
|
+
| `GET` | `/api/agent/settings` | `read` | — |
|
|
426
|
+
| `GET` | `/api/agent/activity` | `read` | — |
|
|
427
|
+
|
|
428
|
+
Scopes: `paths` is a glob list (directories work natively, e.g. `notes/**`); `ops` is any of `read`, `mutate` (create/overwrite/move), `delete` (remove). Grant `["read","mutate"]` for edit-but-never-delete.
|
|
368
429
|
|
|
369
430
|
### Full curl trace
|
|
370
431
|
|
|
@@ -373,7 +434,7 @@ Agent routes (bearer + `X-Agent-Id`, scope-checked):
|
|
|
373
434
|
curl -s -X POST https://wiki.team.com/api/agent/register \
|
|
374
435
|
-H "Content-Type: application/json" \
|
|
375
436
|
-d '{"id":"ai:claude","displayName":"Claude",
|
|
376
|
-
"scope":{"paths":["**/*"],"ops":["read","mutate"]}}'
|
|
437
|
+
"scope":{"paths":["**/*"],"ops":["read","mutate","delete"]}}'
|
|
377
438
|
# -> { "registrationId":"reg_abc","pollUrl":"/api/agent/register/reg_abc","status":"pending" }
|
|
378
439
|
|
|
379
440
|
# 2. Owner approves in the AI Panel.
|
|
@@ -406,9 +467,11 @@ Response codes to handle:
|
|
|
406
467
|
|
|
407
468
|
- `401 UNAUTHORIZED` — bad token or `X-Agent-Id`.
|
|
408
469
|
- `403 FORBIDDEN` — out of scope or `by` mismatches identity.
|
|
409
|
-
- `409 STALE_REVISION` — refetch and retry. Response includes a fresh snapshot.
|
|
410
|
-
- `409 BLOCK_NOT_FOUND` — the ref no longer exists.
|
|
411
|
-
- `409 IDEMPOTENCY_KEY_REUSED` — same key, different body.
|
|
470
|
+
- `409 STALE_REVISION` — refetch and retry. Response includes a fresh snapshot. (Tier 2)
|
|
471
|
+
- `409 BLOCK_NOT_FOUND` — the ref no longer exists. (Tier 2)
|
|
472
|
+
- `409 IDEMPOTENCY_KEY_REUSED` — same key, different body. (Tier 2)
|
|
473
|
+
- `409 COLLAB_ACTIVE` — raw write to a Markdown doc a human is editing; use Tier-2 block-ops (response includes the snapshot URL) or `?force=true`. (Tier 1)
|
|
474
|
+
- `412 PRECONDITION_FAILED` — `If-Match` sha mismatch (file changed); re-read and retry. (Tier 1)
|
|
412
475
|
- `429 RATE_LIMITED` — honor `Retry-After`. Default 60 ops/minute per agent.
|
|
413
476
|
|
|
414
477
|
### Agent rate limit override
|
|
@@ -1 +1,12 @@
|
|
|
1
|
-
You are connecting to a running wiki-viewer instance at $WIKI_URL (e.g. `http://localhost:3000`). Fetch `$WIKI_URL/api/agents/install`and follow the instructions there to register, wait for human approval, then
|
|
1
|
+
You are connecting to a running wiki-viewer instance at $WIKI_URL (e.g. `http://localhost:3000`). Fetch `$WIKI_URL/api/agents/install` and follow the instructions there to register, wait for human approval, then work with files via its HTTP API.
|
|
2
|
+
|
|
3
|
+
**Two tiers — pick by `X-Collab-State`:**
|
|
4
|
+
|
|
5
|
+
- **Tier 1 Raw FS** (`/api/agent/fs/*`) — all file types, fast read/write/ls/search. Use for code, binaries, and markdown that isn't being actively co-edited.
|
|
6
|
+
- **Tier 2 Collab** (`/api/agent/files/*.md`) — markdown only, reviewable proof-spans. Use when `X-Collab-State: active` (human has the doc open or it has pending suggestions).
|
|
7
|
+
|
|
8
|
+
**Mode rule:** Every file read returns `X-Collab-State`. If `active` → use Tier-2 block-ops so the human can review. Otherwise → use Tier-1 raw fs. A raw write to an `active` .md is rejected 409 `COLLAB_ACTIVE` with the Tier-2 URL.
|
|
9
|
+
|
|
10
|
+
Set `basis` and `basisDetail` on every Tier-2 content op so the human can see where your changes came from. Prefer `suggestion.add` over direct block ops unless told otherwise.
|
|
11
|
+
|
|
12
|
+
MCP-capable agents: `npx wiki-viewer-mcp` (set `WIKI_VIEWER_URL`, `WIKI_VIEWER_TOKEN`, `WIKI_VIEWER_AGENT_ID`) handles tier routing automatically.
|
|
@@ -8,6 +8,17 @@ license: MIT
|
|
|
8
8
|
|
|
9
9
|
You are working with **wiki-viewer**, a local-first markdown viewer that exposes a Proof-SDK-compatible HTTP API for agents. Files on disk are the source of truth. Every AI-authored edit is wrapped in an inline `<proof-span>` mark so the human can see, accept, or revert your contribution.
|
|
10
10
|
|
|
11
|
+
## Quick orientation: two tiers
|
|
12
|
+
|
|
13
|
+
| Tier | Routes | File types | When to use |
|
|
14
|
+
| ------------------- | ----------------------- | ----------------------------------- | --------------------------------------------------------------------------------- |
|
|
15
|
+
| **Tier 1 — Raw FS** | `/api/agent/fs/*` | All types (markdown, code, binary…) | Fast filework. Read, write, list, move, delete, search. Audited but no review UI. |
|
|
16
|
+
| **Tier 2 — Collab** | `/api/agent/files/*.md` | Markdown only | Reviewable prose. Edits become proof-spans the human can accept/revert. |
|
|
17
|
+
|
|
18
|
+
Both tiers use the same auth (TOFU token). Pick the tier based on `X-Collab-State` (see [Which tier do I use?](#which-tier-do-i-use)).
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
11
22
|
## Discovery
|
|
12
23
|
|
|
13
24
|
The server publishes everything you need at one URL:
|
|
@@ -214,6 +225,180 @@ Acks are advisory; events are never deleted.
|
|
|
214
225
|
| 422 | INVALID_MARKDOWN | op's markdown failed to parse |
|
|
215
226
|
| 429 | RATE_LIMITED | bucket exhausted, honor `Retry-After` header |
|
|
216
227
|
|
|
228
|
+
---
|
|
229
|
+
|
|
230
|
+
## Working with files (raw filesystem — Tier 1)
|
|
231
|
+
|
|
232
|
+
All Tier-1 routes require `Authorization: Bearer <token>` + `X-Agent-Id: ai:<name>` (same as Tier 2). The agent's `scope.paths` glob applies to every surface (reads, ls results, search results, move, delete).
|
|
233
|
+
|
|
234
|
+
### Scope ops
|
|
235
|
+
|
|
236
|
+
When registering, `scope.ops` can include:
|
|
237
|
+
|
|
238
|
+
- `read` — read files + list + search
|
|
239
|
+
- `mutate` — create / overwrite / move
|
|
240
|
+
- `delete` — remove files/dirs (separate from mutate so a human can grant edit-but-never-delete)
|
|
241
|
+
|
|
242
|
+
Example registration with full access:
|
|
243
|
+
|
|
244
|
+
```json
|
|
245
|
+
{
|
|
246
|
+
"id": "ai:claude",
|
|
247
|
+
"scope": { "paths": ["**/*"], "ops": ["read", "mutate", "delete"] }
|
|
248
|
+
}
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
### Read a file
|
|
252
|
+
|
|
253
|
+
```
|
|
254
|
+
GET /api/agent/fs/file/<path>
|
|
255
|
+
```
|
|
256
|
+
|
|
257
|
+
Returns raw bytes. Metadata in response headers:
|
|
258
|
+
|
|
259
|
+
```
|
|
260
|
+
ETag: "<sha256>"
|
|
261
|
+
X-File-Size: <bytes>
|
|
262
|
+
X-File-Mtime: <ISO-8601>
|
|
263
|
+
Content-Type: <mime>
|
|
264
|
+
X-Collab-State: active | tracked | untracked | not-markdown
|
|
265
|
+
X-Collab-Revision: <n>
|
|
266
|
+
X-Collab-Snapshot: /api/agent/files/<path>.md # when applicable
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
Supports `Range` header for large/binary files.
|
|
270
|
+
|
|
271
|
+
### Write a file
|
|
272
|
+
|
|
273
|
+
```
|
|
274
|
+
PUT /api/agent/fs/file/<path>
|
|
275
|
+
If-Match: "<sha256>" # required for overwrites; omit for creates
|
|
276
|
+
Content-Type: <mime>
|
|
277
|
+
<raw bytes body>
|
|
278
|
+
```
|
|
279
|
+
|
|
280
|
+
- **Create** (file doesn't exist): omit `If-Match`. Creating a file that already exists without `If-Match` → 412 (no blind clobber).
|
|
281
|
+
- **Overwrite** (file exists): send `If-Match` with the sha256 you read. Mismatch → 412.
|
|
282
|
+
- `?mkdirs=true` — create parent directories.
|
|
283
|
+
- `?force=true` — bypass `If-Match` check (audited; use sparingly).
|
|
284
|
+
|
|
285
|
+
For `.md` files, also send:
|
|
286
|
+
|
|
287
|
+
```
|
|
288
|
+
If-Collab-Match: <X-Collab-Revision> # prevents writing into a live collab session
|
|
289
|
+
```
|
|
290
|
+
|
|
291
|
+
(See [Which tier do I use?](#which-tier-do-i-use).)
|
|
292
|
+
|
|
293
|
+
Response:
|
|
294
|
+
|
|
295
|
+
```json
|
|
296
|
+
{
|
|
297
|
+
"path": "...",
|
|
298
|
+
"sha256": "...",
|
|
299
|
+
"size": 1234,
|
|
300
|
+
"mtime": "...",
|
|
301
|
+
"created": false
|
|
302
|
+
}
|
|
303
|
+
```
|
|
304
|
+
|
|
305
|
+
### Delete a file
|
|
306
|
+
|
|
307
|
+
```
|
|
308
|
+
DELETE /api/agent/fs/file/<path>
|
|
309
|
+
If-Match: "<sha256>" # required — you must have read the file first
|
|
310
|
+
```
|
|
311
|
+
|
|
312
|
+
Requires `delete` in `scope.ops`. Deletes the `.proof/` sidecar too for `.md` files. For directories: `?recursive=true`.
|
|
313
|
+
|
|
314
|
+
### List a directory
|
|
315
|
+
|
|
316
|
+
```
|
|
317
|
+
GET /api/agent/fs/ls/<path>?recursive=true&limit=500&depth=3
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
Returns `{ entries: [{name, type, path, size, mtime}] }`. Unauthorized paths silently omitted. Excludes `.proof/`.
|
|
321
|
+
|
|
322
|
+
### Move / rename
|
|
323
|
+
|
|
324
|
+
```
|
|
325
|
+
POST /api/agent/fs/move
|
|
326
|
+
Content-Type: application/json
|
|
327
|
+
|
|
328
|
+
{ "from": "notes/old.md", "to": "notes/new.md", "ifMatch": "<sha256>" }
|
|
329
|
+
```
|
|
330
|
+
|
|
331
|
+
Moves the `.proof/` sidecar for `.md` files automatically.
|
|
332
|
+
|
|
333
|
+
### Search (grep / glob)
|
|
334
|
+
|
|
335
|
+
```
|
|
336
|
+
POST /api/agent/fs/search
|
|
337
|
+
Content-Type: application/json
|
|
338
|
+
|
|
339
|
+
{ "kind": "grep", "query": "TODO", "path": "src", "glob": "*.ts", "limit": 100 }
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
```
|
|
343
|
+
POST /api/agent/fs/search
|
|
344
|
+
Content-Type: application/json
|
|
345
|
+
|
|
346
|
+
{ "kind": "glob", "query": "**/*.test.ts" }
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
Server-side: one call replaces dozens of `ls`+`read` round-trips. Results are scope-filtered. Glob dialect: `**`, `*`, `?` only (no braces/negation in v1).
|
|
350
|
+
|
|
351
|
+
### Error codes (Tier 1)
|
|
352
|
+
|
|
353
|
+
| Status | Code | Meaning |
|
|
354
|
+
| ------ | ------------------- | --------------------------------------------------------------------------- |
|
|
355
|
+
| 400 | BAD_REQUEST | Invalid path or body |
|
|
356
|
+
| 401 | UNAUTHORIZED | Bad/missing token or X-Agent-Id |
|
|
357
|
+
| 403 | FORBIDDEN | Scope mismatch or missing `delete` op |
|
|
358
|
+
| 404 | NOT_FOUND | Path doesn't exist |
|
|
359
|
+
| 409 | COLLAB_ACTIVE | Raw PUT rejected — doc is active-collab; use Tier-2 or send If-Collab-Match |
|
|
360
|
+
| 412 | PRECONDITION_FAILED | If-Match sha mismatch (concurrent modification) |
|
|
361
|
+
| 413 | PAYLOAD_TOO_LARGE | File exceeds 50 MB limit |
|
|
362
|
+
| 429 | RATE_LIMITED | Honor `Retry-After` |
|
|
363
|
+
|
|
364
|
+
---
|
|
365
|
+
|
|
366
|
+
## Which tier do I use?
|
|
367
|
+
|
|
368
|
+
Every `GET /api/agent/fs/file/<path>` (and every Tier-2 snapshot read) returns `X-Collab-State`.
|
|
369
|
+
|
|
370
|
+
| `X-Collab-State` | Meaning | Use |
|
|
371
|
+
| ---------------- | --------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------- |
|
|
372
|
+
| `active` | Human has this `.md` open in the editor, OR it has pending suggestions / unresolved comments. | **Tier 2 block-ops only.** A raw `PUT` returns `409 COLLAB_ACTIVE`. |
|
|
373
|
+
| `tracked` | `.md` has a sidecar (prior collab history), no active session. | Prefer Tier-2 for prose/semantic edits. Raw ok for mechanical/whole-file ops (reformat, regenerate). |
|
|
374
|
+
| `untracked` | `.md`, no sidecar yet. | Raw is fine. Tier-2 creates a sidecar (starts provenance tracking). |
|
|
375
|
+
| `not-markdown` | Any non-`.md` file. | **Tier-1 raw only.** Tier 2 does not apply. |
|
|
376
|
+
|
|
377
|
+
**Rule (one sentence):** Read the file, check `X-Collab-State`, use Tier-2 if `active`, else use Tier-1. For non-markdown, always Tier-1.
|
|
378
|
+
|
|
379
|
+
**Editing a `.md` safely:**
|
|
380
|
+
|
|
381
|
+
```
|
|
382
|
+
1. GET /api/agent/fs/file/<path>.md
|
|
383
|
+
→ note ETag (sha256) and X-Collab-Revision + X-Collab-State
|
|
384
|
+
|
|
385
|
+
2a. If X-Collab-State == "active":
|
|
386
|
+
→ POST /api/agent/files/<path>.md (block-ops, Tier-2)
|
|
387
|
+
Use the X-Collab-Snapshot URL. See "Mutating" section below.
|
|
388
|
+
|
|
389
|
+
2b. Otherwise (tracked / untracked):
|
|
390
|
+
→ PUT /api/agent/fs/file/<path>.md (raw Tier-1)
|
|
391
|
+
If-Match: "<ETag from step 1>"
|
|
392
|
+
If-Collab-Match: <X-Collab-Revision from step 1>
|
|
393
|
+
|
|
394
|
+
3. If you get 409 COLLAB_ACTIVE: the doc became active after your read.
|
|
395
|
+
Switch to Tier-2 (block-ops) using the URL in the response body.
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
**MCP-capable agents:** run `npx wiki-viewer-mcp` — it reads `X-Collab-State` automatically and routes to the correct tier. Set env vars `WIKI_VIEWER_URL`, `WIKI_VIEWER_TOKEN`, `WIKI_VIEWER_AGENT_ID`.
|
|
399
|
+
|
|
400
|
+
---
|
|
401
|
+
|
|
217
402
|
## Working style
|
|
218
403
|
|
|
219
404
|
- Read before write. Always GET a fresh snapshot to capture current `revision` and block `ref`s.
|
|
@@ -226,11 +411,19 @@ Acks are advisory; events are never deleted.
|
|
|
226
411
|
## Sample first interaction
|
|
227
412
|
|
|
228
413
|
```
|
|
229
|
-
1. GET /api/agents/install
|
|
230
|
-
2. POST /api/agent/register
|
|
414
|
+
1. GET /api/agents/install # discovery
|
|
415
|
+
2. POST /api/agent/register # register (include "delete" in ops if needed)
|
|
231
416
|
3. tell human: "approve me in the AI Panel"
|
|
232
|
-
4. GET /api/agent/register/<regId>
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
417
|
+
4. GET /api/agent/register/<regId> # poll until approved
|
|
418
|
+
|
|
419
|
+
# Working mode (Tier-1, e.g. code files or untracked .md)
|
|
420
|
+
5. GET /api/agent/fs/ls/src?recursive=true # list
|
|
421
|
+
6. GET /api/agent/fs/file/src/main.ts # read -> capture ETag
|
|
422
|
+
7. PUT /api/agent/fs/file/src/main.ts (If-Match: <sha>) # write
|
|
423
|
+
|
|
424
|
+
# Collaborating mode (Tier-2, active .md)
|
|
425
|
+
5. GET /api/agent/fs/file/notes/plan.md # read -> X-Collab-State: active
|
|
426
|
+
6. GET /api/agent/files/notes/plan.md # read snapshot (Tier-2)
|
|
427
|
+
7. POST /api/agent/files/notes/plan.md # suggest or mutate (Tier-2)
|
|
428
|
+
8. GET /api/agent/events/notes/plan.md?after=<id> # listen for replies
|
|
236
429
|
```
|
|
@@ -62,9 +62,10 @@ All routes reuse `checkAuth` + `enforceScope` + `withFileMutex` + `safeRootPath`
|
|
|
62
62
|
### 2.2 Write (`PUT .../file/<path>`)
|
|
63
63
|
|
|
64
64
|
- Atomic: write temp in **same dir** → `fsync` → `rename`. (`fsync` dir optional but documented.)
|
|
65
|
-
- Preserve mode of existing file. Do **not** create parent dirs unless
|
|
66
|
-
- `If-Match: <sha256>`
|
|
67
|
-
-
|
|
65
|
+
- Preserve mode of existing file. Do **not** create parent dirs unless `?mkdirs=true`.
|
|
66
|
+
- **Create = implicit.** `PUT` to a non-existent path creates the file (write-with-no-prior). Omit `If-Match` for a create; send `If-Match: <sha256>` only when overwriting. A create where the file already exists (no `If-Match`) → 412, so creates can't silently clobber.
|
|
67
|
+
- `If-Match: <sha256>` → 412 on mismatch. **Required by default for overwrites** (see R4); explicit `?force=true` bypasses and is audited.
|
|
68
|
+
- Response: `{path, sha256, size, mtime, created: bool}`.
|
|
68
69
|
|
|
69
70
|
### 2.3 Listing (`GET .../ls`)
|
|
70
71
|
|
|
@@ -82,6 +83,12 @@ All routes reuse `checkAuth` + `enforceScope` + `withFileMutex` + `safeRootPath`
|
|
|
82
83
|
- Checks: source `read`+`mutate`, dest `mutate`.
|
|
83
84
|
- For `.md`: moves the `.proof/<path>.json` sidecar too. Lock ordering: single operation lock, or lock source+dest in **sorted key order** to avoid deadlock.
|
|
84
85
|
|
|
86
|
+
### 2.6 Delete (`DELETE .../file/<path>`)
|
|
87
|
+
|
|
88
|
+
- Requires the `delete` scope op (see §3.5) and `If-Match: <sha256>` — the agent must have read the current file first, so it cannot blind-delete. This `If-Match` requirement **is** the confirmation for v1.
|
|
89
|
+
- `.md`: also deletes the sidecar (R3).
|
|
90
|
+
- Directory delete is opt-in and explicit: `?recursive=true`; without it, deleting a non-empty dir is refused. (No human approval queue in v1 — deferred.)
|
|
91
|
+
|
|
85
92
|
---
|
|
86
93
|
|
|
87
94
|
## 3. Tier 2 — Collab (unchanged)
|
|
@@ -90,14 +97,61 @@ Markdown block-ops (`block.replace/insertAfter/insertBefore/delete/append/prepen
|
|
|
90
97
|
|
|
91
98
|
---
|
|
92
99
|
|
|
100
|
+
## 3.5 Working mode vs Collaborating mode (THE central distinction)
|
|
101
|
+
|
|
102
|
+
The whole design hinges on the agent knowing **when a file is being actively co-edited by a human** (use the reviewable Tier-2 path) versus **when it's just a file to work on** (use fast raw Tier-1). Today nothing tells the agent this; a wrong guess clobbers a human's pending suggestions. We make the mode **discoverable, documented, and enforced** — three layers so the agent cannot miss it.
|
|
103
|
+
|
|
104
|
+
### The signal: `X-Collab-State` header (on every read, both tiers)
|
|
105
|
+
|
|
106
|
+
Every `GET fs/file/<path>` and every Tier-2 snapshot read returns:
|
|
107
|
+
|
|
108
|
+
```
|
|
109
|
+
X-Collab-State: active | tracked | untracked | not-markdown
|
|
110
|
+
X-Collab-Snapshot: /api/agent/files/<path>.md # present when state != not-markdown
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
| State | Meaning | Agent should… |
|
|
114
|
+
| -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
115
|
+
| `active` | `.md` that is **live**: either has review artifacts (pending suggestions / unresolved comments / unreverted proof-spans) **OR has a current human edit lease** (someone has the doc open in the editor). A human is collaborating right now. | **Use Tier-2 block-ops.** Your edits become reviewable suggestions. A raw `PUT` is rejected unless `If-Collab-Match` matches (see R6). |
|
|
116
|
+
| `tracked` | `.md` with a sidecar, no artifacts, no lease. | **Prefer Tier-2 for any semantic/prose edit** so provenance accrues. Raw only for mechanical/whole-file ops (e.g. reformat, regenerate). Not a free pass. |
|
|
117
|
+
| `untracked` | `.md`, no sidecar, no lease — nobody has collaborated. | Raw ok. A reviewable (Tier-2) edit creates a sidecar so future edits are tracked. |
|
|
118
|
+
| `not-markdown` | Any non-`.md` file. | **Tier-1 raw only** (Tier-2 doesn't apply). |
|
|
119
|
+
|
|
120
|
+
**Definition of `active` (two sources, OR'd):**
|
|
121
|
+
|
|
122
|
+
1. **Artifacts:** `pendingSuggestions > 0` OR `unresolvedComments > 0` OR `proofSpanCount > 0` (from sidecar — cheap, already in memory on read).
|
|
123
|
+
2. **Human edit lease:** a short-TTL presence marker (default 90s, heartbeat ~30s) set when a human opens the doc in the editor and refreshed while it stays open. **This closes the false-negative GPT-5.5 caught:** a human who opens a doc but hasn't typed a suggestion yet still reads `active`, so the agent won't blind-write into a live session. Lease lives in-memory (or the existing SQLite), keyed by `(path)`, set by a lightweight `POST /api/wiki/presence` ping the editor already can issue alongside its chokidar SSE connection. No lease + no artifacts → not active.
|
|
124
|
+
|
|
125
|
+
### Layer 2 — documented (manifest + skill)
|
|
126
|
+
|
|
127
|
+
The `/api/agents/install` manifest and the skill state the rule in one sentence:
|
|
128
|
+
|
|
129
|
+
> _"Before editing a `.md`, read it and check `X-Collab-State`. If `active`, use block-ops (Tier 2) so a human can review. Otherwise use raw fs (Tier 1). For non-markdown, always use raw fs."_
|
|
130
|
+
|
|
131
|
+
### Layer 3 — enforced (R4 + R6 backstop)
|
|
132
|
+
|
|
133
|
+
Even a confused or racing agent cannot silently clobber a live session: a raw `PUT` to an `active` `.md` is **rejected with 409 `COLLAB_ACTIVE`** unless it carries a matching `If-Collab-Match` (R6) — and the state is re-checked atomically inside the write mutex, so a session that goes active mid-flight still wins. Any genuinely orphaned suggestion/comment is marked `stale` by reconciliation (R2), never dropped. The 409 returns the Tier-2 snapshot URL, re-teaching the mode.
|
|
134
|
+
|
|
135
|
+
### What the human sees
|
|
136
|
+
|
|
137
|
+
- Agent works in Tier-2 on an `active` doc → normal reviewable suggestions (existing UX).
|
|
138
|
+
- Agent raw-writes an `active` doc anyway → `file.rawWritten by ai:<id>` in the AI Panel feed + any unbindable anchors flagged `stale` (not vanished).
|
|
139
|
+
- Agent works on `untracked`/non-md → silent, fast, audited. No collab noise.
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
93
143
|
## 4. Integration rules (load-bearing)
|
|
94
144
|
|
|
95
145
|
- **R1 — Shared lock.** A raw write/move/delete on a `.md` acquires the **same** `withFileMutex` key the ops-applier uses. No interleaving with block-ops mid-proof-span.
|
|
96
146
|
- **R2 — No fake provenance.** Raw writes never author proof-spans. After a raw write to a tracked `.md`, emit `file.rawWritten` (writer known) and **reconcile the sidecar synchronously, inside the same mutex** (rebuild `refMap`, set fingerprint to `newSha`, mark affected proof refs stale). Raw edits are unmarked by definition; human sees _who_ via the event, not as an accept/revert suggestion.
|
|
97
147
|
- **GOTCHA (do not regress):** do NOT just "bump fingerprint to newSha and let existing reconciliation fire later." The existing reconciliation triggers on `sidecar.fingerprint != file sha256`; if the raw write sets fingerprint current, the mismatch is gone and rebuild never runs. Because the writer is known, reconcile **eagerly within the write**, not lazily on next read.
|
|
98
|
-
- **R3 — Sidecar lifecycle.** Raw `mv`/`rm` of a `.md` moves/deletes its sidecar
|
|
148
|
+
- **R3 — Sidecar lifecycle.** Raw `mv`/`rm` of a `.md` moves/deletes its sidecar. **NOTE:** the human `wiki/move` route currently does a bare `rename` and does NOT move the sidecar — a latent bug that orphans `.proof/` JSON today. Build a shared `moveSidecar`/`deleteSidecar` helper in `sidecar.ts` and wire **both** the raw-fs path and `wiki/move` to it (fixes the existing bug for free).
|
|
99
149
|
- **R4 — Write guard (If-Match by default).** **All** mutating raw ops (`PUT`/`DELETE`/`move`) REQUIRE `If-Match: <sha256>` by default; 412 on mismatch. An explicit `?force=true` bypasses it and is recorded in the audit row. Rationale: agents edit code/config just as much as `.md`; optional concurrency = silent lost updates, and a format-based safety boundary is arbitrary. Cheap because the agent already has the sha from `GET`.
|
|
100
|
-
- **R5 — Scope unification.** Registration `scope.paths` glob
|
|
150
|
+
- **R5 — Scope unification + `delete` op.** Registration `scope.paths` glob already covers **directories natively** (e.g. `notes/**` grants the whole subtree — no file lists). Raw-fs honors the same glob, on every surface (read/ls/search/move/delete results all re-checked). **One addition:** split `delete` out of `mutate`, so `scope.ops` becomes `[read, mutate, delete]`. This lets a human grant "edit but never delete." `mutate` = create/overwrite/move; `delete` = remove. Back-compat: existing `[read, mutate]` agents simply can't delete until re-scoped. Glob dialect (v1): `**`, `*`, `?` only — no braces/negation; advertise this in the manifest.
|
|
151
|
+
- **R6 — Collab-state precondition on raw `.md` writes (closes the TOCTOU race).** `If-Match` protects _bytes_, not _collaboration intent_: a human could create a sidecar/lease after the agent's read but before its raw `PUT`, leaving bytes unchanged so `If-Match` still passes. To prevent silently writing into a session that went `active` mid-flight:
|
|
152
|
+
- Every `.md` read also returns `X-Collab-Revision: <n>` (bumped on any sidecar/lease state change).
|
|
153
|
+
- A raw `PUT` to a `.md` must, **inside the same mutex, re-read collab state immediately before writing**: if the doc is now `active` and the request did not supply a matching `If-Collab-Match: <n>`, **reject with 409 `COLLAB_ACTIVE`** + the Tier-2 snapshot URL. The agent then switches to block-ops. `?force=true` still bypasses (audited) for deliberate overrides.
|
|
154
|
+
- This makes the mode check atomic with the write, not advisory.
|
|
101
155
|
|
|
102
156
|
### Event taxonomy (don't overload)
|
|
103
157
|
|
|
@@ -120,7 +174,8 @@ A full-file `PUT` to a `.md` can strip `<proof-span>` marks and shift block boun
|
|
|
120
174
|
Extend `/api/agents/install` manifest:
|
|
121
175
|
|
|
122
176
|
- Advertise the new `fs/*` routes.
|
|
123
|
-
- Add a `capabilities` block: `{ maxFileBytes, supportsRange, ifMatchRequired:true, forceBypass:true, search:["grep","glob"] }`.
|
|
177
|
+
- Add a `capabilities` block: `{ maxFileBytes, supportsRange, ifMatchRequired:true, forceBypass:true, search:["grep","glob"], globDialect:"**,*,?", scopeOps:["read","mutate","delete"], collabStates:["active","tracked","untracked","not-markdown"], collabPrecondition:"If-Collab-Match" }`.
|
|
178
|
+
- State the **working-vs-collaborating rule** (§3.5) in prose so the agent self-configures its tier choice.
|
|
124
179
|
- Advertise the optional MCP adapter package + version.
|
|
125
180
|
One manifest; agent self-configures. HTTP routes are the canonical contract.
|
|
126
181
|
|
|
@@ -135,6 +190,8 @@ Extend `/api/agents/install` manifest:
|
|
|
135
190
|
- `edit_file` (str-replace) → **client-side**: read → transform → `PUT If-Match`. (Server stays dumb; edit footguns live in the adapter.)
|
|
136
191
|
- `list_directory` → `GET fs/ls`
|
|
137
192
|
- `search` → `POST fs/search`
|
|
193
|
+
- `delete_file` → `DELETE fs/file` (requires `delete` scope + `If-Match`)
|
|
194
|
+
- The shim reads `X-Collab-State` and, when `active`, **warns or routes the edit through the Tier-2 block-op tools** instead of a blind raw write — so even off-the-shelf MCP clients respect collaborating mode.
|
|
138
195
|
- Do **not** make the core app MCP-native first. The app already has HTTP auth/discovery; the shim adapts to Claude Code / Cursor / Codex without poisoning the core.
|
|
139
196
|
|
|
140
197
|
---
|
|
@@ -165,7 +222,8 @@ Extend `/api/agents/install` manifest:
|
|
|
165
222
|
5. `fs/search` (grep/glob, limits).
|
|
166
223
|
6. Extend `/api/agents/install` manifest + capabilities.
|
|
167
224
|
7. `wiki-viewer-mcp` adapter (read/write/edit/list/search).
|
|
168
|
-
8.
|
|
225
|
+
8. **Mode plumbing:** human edit-lease (`POST /api/wiki/presence` + TTL, editor heartbeat), `X-Collab-State` + `X-Collab-Revision` headers on both read paths, R6 atomic re-check + 409 `COLLAB_ACTIVE` on raw `.md` writes. Plus `delete` scope op + create/`?mkdirs`/`?recursive` semantics.
|
|
226
|
+
9. Tests: traversal, symlink escape, If-Match 412, create-collision 412, sidecar move (incl. `wiki/move` bug fix), scope filtering on ls/search, big-file Range, binary skip in grep, **`X-Collab-State` matrix incl. lease-only `active`**, **R6 TOCTOU: doc goes active between read and raw PUT → 409**, stale-anchor after raw `.md` overwrite, `delete` op gating, lease expiry flips active→tracked.
|
|
169
227
|
|
|
170
228
|
```
|
|
171
229
|
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# File vs Collab Authority Model
|
|
2
|
+
|
|
3
|
+
**TL;DR:** File on disk is truth. Tier-1 raw edits are fast + audited but unmarked. Tier-2 block-ops are slower but reviewable. The `X-Collab-State` header tells an agent which tier to use; the server enforces it atomically.
|
|
4
|
+
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
## Source of truth
|
|
8
|
+
|
|
9
|
+
The `.md` file on disk is the canonical document. The `.proof/<path>.json` sidecar holds provenance metadata (comments, suggestions, proof-span refs, fingerprint) but is never the source of truth for content. If the sidecar is lost the document is intact; if the document is lost the sidecar is orphaned.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Two tiers
|
|
14
|
+
|
|
15
|
+
### Tier 1 — Raw FS
|
|
16
|
+
|
|
17
|
+
- Routes: `GET/PUT/DELETE /api/agent/fs/file/<path>`, `GET /api/agent/fs/ls/...`, `POST /api/agent/fs/move`, `POST /api/agent/fs/search`.
|
|
18
|
+
- All file types. Raw bytes in, raw bytes out.
|
|
19
|
+
- Writes are **atomic** (temp-file + rename in same directory).
|
|
20
|
+
- **Audit:** every mutation records a row in the `agent_fs_audit` SQLite table (`path`, `op`, `agentId`, `oldSha`, `newSha`, `forced`, `at`).
|
|
21
|
+
- **Event:** `file.rawWritten` emitted with `{ by: "ai:<id>", path, oldSha, newSha }`. Distinct from `file.externallyEdited` (chokidar, writer unknown).
|
|
22
|
+
- No `<proof-span>` marks. No accept/revert UI. Audit log is the paper trail.
|
|
23
|
+
|
|
24
|
+
### Tier 2 — Collab
|
|
25
|
+
|
|
26
|
+
- Routes: `GET/POST /api/agent/files/<path>.md`, `GET/POST /api/agent/events/<path>.md`.
|
|
27
|
+
- Markdown only.
|
|
28
|
+
- Edits are wrapped in `<proof-span>` marks inline in the `.md` file.
|
|
29
|
+
- Provenance (suggestions, comments, block refs) stored in `.proof/<path>.json` sidecar.
|
|
30
|
+
- Human can accept or revert individual spans in the editor UI.
|
|
31
|
+
- Mandatory `baseRevision` optimistic concurrency + `Idempotency-Key` per request.
|
|
32
|
+
|
|
33
|
+
---
|
|
34
|
+
|
|
35
|
+
## The `X-Collab-State` header
|
|
36
|
+
|
|
37
|
+
Every `GET /api/agent/fs/file/<path>` (Tier-1 read) and every Tier-2 snapshot read returns:
|
|
38
|
+
|
|
39
|
+
```
|
|
40
|
+
X-Collab-State: active | tracked | untracked | not-markdown
|
|
41
|
+
X-Collab-Revision: <n>
|
|
42
|
+
X-Collab-Snapshot: /api/agent/files/<path>.md # present when not not-markdown
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### State machine
|
|
46
|
+
|
|
47
|
+
```
|
|
48
|
+
[file created]
|
|
49
|
+
│
|
|
50
|
+
▼
|
|
51
|
+
untracked ──── first Tier-2 op ────► tracked
|
|
52
|
+
│ │
|
|
53
|
+
│ (Tier-1 writes stay untracked) │ human opens editor
|
|
54
|
+
│ ▼
|
|
55
|
+
│ active
|
|
56
|
+
│ │
|
|
57
|
+
│ editor closes +
|
|
58
|
+
│ no artifacts
|
|
59
|
+
│ │
|
|
60
|
+
│ ▼
|
|
61
|
+
└────────────────────────────────── tracked
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
**`active`** is set by two independent sources (OR'd):
|
|
65
|
+
|
|
66
|
+
1. **Artifacts** — sidecar has `pendingSuggestions > 0` OR `unresolvedComments > 0` OR `proofSpanCount > 0`. Cheap: always in memory when sidecar is loaded.
|
|
67
|
+
2. **Human edit lease** — a short-TTL presence marker (default 90 s, heartbeat ~30 s) set via `POST /api/wiki/presence { path, action: "open" | "heartbeat" | "close" }`. Closes the false-negative: a human who opens a doc but hasn't typed a suggestion yet still reads `active`.
|
|
68
|
+
|
|
69
|
+
`X-Collab-Revision` bumps on any sidecar write OR lease open/close. Used by R6.
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## R6 — atomic TOCTOU race fix
|
|
74
|
+
|
|
75
|
+
`If-Match` (sha256) protects _bytes_. But a human could open the doc between the agent's read and its raw `PUT`, making it `active` while bytes are unchanged — so `If-Match` passes but the write clobbers a live session.
|
|
76
|
+
|
|
77
|
+
**Fix:** a raw `PUT` to any `.md` re-checks `collabState` **inside the same `withFileMutex` call, immediately before writing**. If the state is now `active` and the request did not supply a matching `If-Collab-Match: <revision>`, the write is **rejected 409 `COLLAB_ACTIVE`** with the Tier-2 URL. This check is atomic with the write — not advisory.
|
|
78
|
+
|
|
79
|
+
`?force=true` bypasses both `If-Match` and `If-Collab-Match` and is recorded in the audit row as `forced: true`.
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## R2 — eager sidecar reconciliation
|
|
84
|
+
|
|
85
|
+
After a raw `PUT` to a tracked/active `.md`, the sidecar must be reconciled **synchronously, inside the same mutex**, before the lock is released:
|
|
86
|
+
|
|
87
|
+
1. Re-scan the new file bytes for surviving `<proof-span>` marks.
|
|
88
|
+
2. Re-bind each proof ref to its new block offset; mark unresolvable refs `stale: true`.
|
|
89
|
+
3. Set `sidecar.fingerprint = newSha` (so the lazy mismatch-trigger in `readSnapshot` does NOT re-fire — the reconcile already ran).
|
|
90
|
+
4. Emit `file.rawWritten` event.
|
|
91
|
+
|
|
92
|
+
**Do not** just bump the fingerprint and let reconciliation fire lazily on the next read. The lazy trigger fires only when `sidecar.fingerprint != file sha256`; once the raw write sets fingerprint current, the mismatch is gone and rebuild never runs. Eager reconciliation is the correct invariant.
|
|
93
|
+
|
|
94
|
+
---
|
|
95
|
+
|
|
96
|
+
## Known v1 limitations
|
|
97
|
+
|
|
98
|
+
### (a) Editor does not call `/api/wiki/presence` yet
|
|
99
|
+
|
|
100
|
+
The front-end editor does **not** currently issue `POST /api/wiki/presence` on open/heartbeat/close. Consequence: lease-based `active` never fires. Only artifact-based `active` works (pending suggestions, unresolved comments, proof-spans in sidecar).
|
|
101
|
+
|
|
102
|
+
**Impact:** a human who opens a doc but has no pending review artifacts will see `X-Collab-State: tracked` (or `untracked`), not `active`. An agent raw-writing that doc won't be blocked. This is a safety gap — not a correctness bug (no data loss), but the intended "lock out while human is live-editing" experience is incomplete until the editor heartbeat is wired.
|
|
103
|
+
|
|
104
|
+
**Fix:** call `POST /api/wiki/presence { path, action: "open" }` when the editor mounts the doc and `{ action: "close" }` on unmount. Heartbeat every ~30 s. Tracked as a follow-up task.
|
|
105
|
+
|
|
106
|
+
### (b) Lease store is in-memory (single-process only)
|
|
107
|
+
|
|
108
|
+
The presence lease map lives in the Node.js process memory. In a multi-process deployment (PM2 cluster mode, multiple Next.js instances behind a load balancer) each process has its own lease state and processes do not share it.
|
|
109
|
+
|
|
110
|
+
**Impact:** a human's open lease set by process A is invisible to process B. A raw write handled by process B may not see the `active` state and proceed when it should block.
|
|
111
|
+
|
|
112
|
+
**Fix:** move the lease store to the shared SQLite (`~/.wiki-viewer/auth.db`) with a TTL column and a periodic cleanup job. The table is already shared across the process boundary. Tracked as a follow-up task.
|
|
113
|
+
|
|
114
|
+
---
|
|
115
|
+
|
|
116
|
+
## Sidecar lifecycle rules
|
|
117
|
+
|
|
118
|
+
| Raw-fs op | On non-`.md` | On `.md` |
|
|
119
|
+
| ------------------------ | ------------ | ------------------------------------------------------------------ |
|
|
120
|
+
| `PUT` (create/overwrite) | plain write | acquire mutex → write → reconcile sidecar → emit `file.rawWritten` |
|
|
121
|
+
| `DELETE` | plain delete | delete file + delete `.proof/<path>.json` sidecar |
|
|
122
|
+
| `POST /fs/move` | plain rename | rename file + rename `.proof/<old>.json` → `.proof/<new>.json` |
|
|
123
|
+
|
|
124
|
+
The human `wiki/move` route was previously a bare `rename` that orphaned sidecars. It now uses the same shared `moveSidecar` helper (fixed in Phase 1).
|