@cyanheads/mcp-ts-core 0.12.6 → 0.12.8

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.
Files changed (235) hide show
  1. package/AGENTS.md +9 -3
  2. package/CLAUDE.md +9 -3
  3. package/README.md +132 -77
  4. package/biome.json +1 -1
  5. package/changelog/0.12.x/0.12.6.md +2 -2
  6. package/changelog/0.12.x/0.12.7.md +39 -0
  7. package/changelog/0.12.x/0.12.8.md +55 -0
  8. package/dist/config/index.d.ts +3 -34
  9. package/dist/config/index.d.ts.map +1 -1
  10. package/dist/config/index.js +4 -26
  11. package/dist/config/index.js.map +1 -1
  12. package/dist/core/app.d.ts +0 -8
  13. package/dist/core/app.d.ts.map +1 -1
  14. package/dist/core/app.js +0 -7
  15. package/dist/core/app.js.map +1 -1
  16. package/dist/core/serverManifest.d.ts +0 -7
  17. package/dist/core/serverManifest.d.ts.map +1 -1
  18. package/dist/core/serverManifest.js +1 -13
  19. package/dist/core/serverManifest.js.map +1 -1
  20. package/dist/core/worker.d.ts +1 -1
  21. package/dist/core/worker.d.ts.map +1 -1
  22. package/dist/core/worker.js.map +1 -1
  23. package/dist/linter/rules/enrichment-rules.js +2 -2
  24. package/dist/linter/rules/enrichment-rules.js.map +1 -1
  25. package/dist/linter/rules/format-parity-rules.d.ts.map +1 -1
  26. package/dist/linter/rules/format-parity-rules.js +14 -36
  27. package/dist/linter/rules/format-parity-rules.js.map +1 -1
  28. package/dist/linter/rules/prompt-rules.d.ts +1 -1
  29. package/dist/linter/rules/prompt-rules.d.ts.map +1 -1
  30. package/dist/linter/rules/prompt-rules.js +2 -19
  31. package/dist/linter/rules/prompt-rules.js.map +1 -1
  32. package/dist/linter/rules/resource-rules.d.ts +1 -1
  33. package/dist/linter/rules/resource-rules.d.ts.map +1 -1
  34. package/dist/linter/rules/resource-rules.js +9 -39
  35. package/dist/linter/rules/resource-rules.js.map +1 -1
  36. package/dist/linter/rules/schema-rules.d.ts +22 -2
  37. package/dist/linter/rules/schema-rules.d.ts.map +1 -1
  38. package/dist/linter/rules/schema-rules.js +28 -5
  39. package/dist/linter/rules/schema-rules.js.map +1 -1
  40. package/dist/linter/rules/tool-rules.d.ts +1 -1
  41. package/dist/linter/rules/tool-rules.d.ts.map +1 -1
  42. package/dist/linter/rules/tool-rules.js +13 -41
  43. package/dist/linter/rules/tool-rules.js.map +1 -1
  44. package/dist/linter/validate.d.ts.map +1 -1
  45. package/dist/linter/validate.js +22 -42
  46. package/dist/linter/validate.js.map +1 -1
  47. package/dist/mcp-server/apps/appBuilders.d.ts.map +1 -1
  48. package/dist/mcp-server/apps/appBuilders.js +2 -16
  49. package/dist/mcp-server/apps/appBuilders.js.map +1 -1
  50. package/dist/mcp-server/handlerContext.d.ts +66 -0
  51. package/dist/mcp-server/handlerContext.d.ts.map +1 -0
  52. package/dist/mcp-server/handlerContext.js +71 -0
  53. package/dist/mcp-server/handlerContext.js.map +1 -0
  54. package/dist/mcp-server/inputRequired.d.ts +7 -1
  55. package/dist/mcp-server/inputRequired.d.ts.map +1 -1
  56. package/dist/mcp-server/inputRequired.js +10 -3
  57. package/dist/mcp-server/inputRequired.js.map +1 -1
  58. package/dist/mcp-server/resources/resource-registration.d.ts +2 -2
  59. package/dist/mcp-server/resources/resource-registration.d.ts.map +1 -1
  60. package/dist/mcp-server/resources/resource-registration.js.map +1 -1
  61. package/dist/mcp-server/resources/utils/resourceHandlerFactory.d.ts +14 -43
  62. package/dist/mcp-server/resources/utils/resourceHandlerFactory.d.ts.map +1 -1
  63. package/dist/mcp-server/resources/utils/resourceHandlerFactory.js +11 -50
  64. package/dist/mcp-server/resources/utils/resourceHandlerFactory.js.map +1 -1
  65. package/dist/mcp-server/tools/tool-registration.d.ts +5 -9
  66. package/dist/mcp-server/tools/tool-registration.d.ts.map +1 -1
  67. package/dist/mcp-server/tools/tool-registration.js +16 -12
  68. package/dist/mcp-server/tools/tool-registration.js.map +1 -1
  69. package/dist/mcp-server/tools/utils/deferredInputSchema.d.ts +39 -0
  70. package/dist/mcp-server/tools/utils/deferredInputSchema.d.ts.map +1 -0
  71. package/dist/mcp-server/tools/utils/deferredInputSchema.js +33 -0
  72. package/dist/mcp-server/tools/utils/deferredInputSchema.js.map +1 -0
  73. package/dist/mcp-server/tools/utils/schemaShape.d.ts +21 -0
  74. package/dist/mcp-server/tools/utils/schemaShape.d.ts.map +1 -1
  75. package/dist/mcp-server/tools/utils/schemaShape.js +8 -6
  76. package/dist/mcp-server/tools/utils/schemaShape.js.map +1 -1
  77. package/dist/mcp-server/tools/utils/toolHandlerFactory.d.ts +25 -45
  78. package/dist/mcp-server/tools/utils/toolHandlerFactory.d.ts.map +1 -1
  79. package/dist/mcp-server/tools/utils/toolHandlerFactory.js +55 -74
  80. package/dist/mcp-server/tools/utils/toolHandlerFactory.js.map +1 -1
  81. package/dist/mcp-server/transports/http/httpErrorHandler.d.ts.map +1 -1
  82. package/dist/mcp-server/transports/http/httpErrorHandler.js +2 -1
  83. package/dist/mcp-server/transports/http/httpErrorHandler.js.map +1 -1
  84. package/dist/mcp-server/transports/http/landing-page/handler.d.ts.map +1 -1
  85. package/dist/mcp-server/transports/http/landing-page/handler.js +2 -1
  86. package/dist/mcp-server/transports/http/landing-page/handler.js.map +1 -1
  87. package/dist/mcp-server/transports/http/protectedResourceMetadata.d.ts.map +1 -1
  88. package/dist/mcp-server/transports/http/protectedResourceMetadata.js +2 -1
  89. package/dist/mcp-server/transports/http/protectedResourceMetadata.js.map +1 -1
  90. package/dist/mcp-server/transports/http/publicOrigin.d.ts +11 -0
  91. package/dist/mcp-server/transports/http/publicOrigin.d.ts.map +1 -0
  92. package/dist/mcp-server/transports/http/publicOrigin.js +13 -0
  93. package/dist/mcp-server/transports/http/publicOrigin.js.map +1 -0
  94. package/dist/mcp-server/transports/http/serverCard.d.ts.map +1 -1
  95. package/dist/mcp-server/transports/http/serverCard.js +2 -1
  96. package/dist/mcp-server/transports/http/serverCard.js.map +1 -1
  97. package/dist/mcp-server/transports/http/sessionIdUtils.d.ts +4 -0
  98. package/dist/mcp-server/transports/http/sessionIdUtils.d.ts.map +1 -1
  99. package/dist/mcp-server/transports/http/sessionIdUtils.js +3 -13
  100. package/dist/mcp-server/transports/http/sessionIdUtils.js.map +1 -1
  101. package/dist/mcp-server/transports/manager.d.ts +0 -3
  102. package/dist/mcp-server/transports/manager.d.ts.map +1 -1
  103. package/dist/mcp-server/transports/manager.js +0 -7
  104. package/dist/mcp-server/transports/manager.js.map +1 -1
  105. package/dist/services/canvas/core/CanvasRegistry.d.ts +14 -0
  106. package/dist/services/canvas/core/CanvasRegistry.d.ts.map +1 -1
  107. package/dist/services/canvas/core/CanvasRegistry.js +3 -2
  108. package/dist/services/canvas/core/CanvasRegistry.js.map +1 -1
  109. package/dist/services/canvas/providers/duckdb/DuckdbProvider.d.ts +16 -0
  110. package/dist/services/canvas/providers/duckdb/DuckdbProvider.d.ts.map +1 -1
  111. package/dist/services/canvas/providers/duckdb/DuckdbProvider.js +78 -103
  112. package/dist/services/canvas/providers/duckdb/DuckdbProvider.js.map +1 -1
  113. package/dist/services/graph/core/GraphService.d.ts +3 -3
  114. package/dist/services/graph/core/GraphService.js +3 -3
  115. package/dist/services/graph/types.d.ts +2 -79
  116. package/dist/services/graph/types.d.ts.map +1 -1
  117. package/dist/services/graph/types.js +2 -2
  118. package/dist/services/index.d.ts +1 -2
  119. package/dist/services/index.d.ts.map +1 -1
  120. package/dist/services/index.js +0 -1
  121. package/dist/services/index.js.map +1 -1
  122. package/dist/services/mirror/sqlite/handle.d.ts.map +1 -1
  123. package/dist/services/mirror/sqlite/handle.js +22 -36
  124. package/dist/services/mirror/sqlite/handle.js.map +1 -1
  125. package/dist/services/speech/core/ISpeechProvider.d.ts +0 -24
  126. package/dist/services/speech/core/ISpeechProvider.d.ts.map +1 -1
  127. package/dist/services/speech/core/ISpeechProvider.js +1 -28
  128. package/dist/services/speech/core/ISpeechProvider.js.map +1 -1
  129. package/dist/services/speech/core/SpeechService.d.ts.map +1 -1
  130. package/dist/services/speech/core/SpeechService.js +5 -8
  131. package/dist/services/speech/core/SpeechService.js.map +1 -1
  132. package/dist/services/speech/providers/elevenlabs.provider.d.ts.map +1 -1
  133. package/dist/services/speech/providers/elevenlabs.provider.js +1 -0
  134. package/dist/services/speech/providers/elevenlabs.provider.js.map +1 -1
  135. package/dist/services/speech/providers/whisper.provider.d.ts.map +1 -1
  136. package/dist/services/speech/providers/whisper.provider.js +4 -2
  137. package/dist/services/speech/providers/whisper.provider.js.map +1 -1
  138. package/dist/services/speech/types.d.ts +2 -19
  139. package/dist/services/speech/types.d.ts.map +1 -1
  140. package/dist/storage/core/providerHelpers.d.ts +52 -0
  141. package/dist/storage/core/providerHelpers.d.ts.map +1 -0
  142. package/dist/storage/core/providerHelpers.js +96 -0
  143. package/dist/storage/core/providerHelpers.js.map +1 -0
  144. package/dist/storage/providers/cloudflare/d1Provider.d.ts.map +1 -1
  145. package/dist/storage/providers/cloudflare/d1Provider.js +1 -4
  146. package/dist/storage/providers/cloudflare/d1Provider.js.map +1 -1
  147. package/dist/storage/providers/cloudflare/kvProvider.d.ts.map +1 -1
  148. package/dist/storage/providers/cloudflare/kvProvider.js +4 -31
  149. package/dist/storage/providers/cloudflare/kvProvider.js.map +1 -1
  150. package/dist/storage/providers/cloudflare/r2Provider.d.ts +1 -1
  151. package/dist/storage/providers/cloudflare/r2Provider.d.ts.map +1 -1
  152. package/dist/storage/providers/cloudflare/r2Provider.js +8 -48
  153. package/dist/storage/providers/cloudflare/r2Provider.js.map +1 -1
  154. package/dist/storage/providers/fileSystem/fileSystemProvider.d.ts +1 -1
  155. package/dist/storage/providers/fileSystem/fileSystemProvider.d.ts.map +1 -1
  156. package/dist/storage/providers/fileSystem/fileSystemProvider.js +17 -86
  157. package/dist/storage/providers/fileSystem/fileSystemProvider.js.map +1 -1
  158. package/dist/storage/providers/inMemory/inMemoryProvider.d.ts.map +1 -1
  159. package/dist/storage/providers/inMemory/inMemoryProvider.js +5 -38
  160. package/dist/storage/providers/inMemory/inMemoryProvider.js.map +1 -1
  161. package/dist/storage/providers/supabase/supabaseProvider.d.ts.map +1 -1
  162. package/dist/storage/providers/supabase/supabaseProvider.js +1 -4
  163. package/dist/storage/providers/supabase/supabaseProvider.js.map +1 -1
  164. package/dist/testing/fuzz.d.ts.map +1 -1
  165. package/dist/testing/fuzz.js +17 -31
  166. package/dist/testing/fuzz.js.map +1 -1
  167. package/dist/testing/index.d.ts.map +1 -1
  168. package/dist/testing/index.js +4 -26
  169. package/dist/testing/index.js.map +1 -1
  170. package/dist/utils/internal/error-handler/types.d.ts +0 -4
  171. package/dist/utils/internal/error-handler/types.d.ts.map +1 -1
  172. package/dist/utils/internal/logger.d.ts.map +1 -1
  173. package/dist/utils/internal/logger.js +2 -16
  174. package/dist/utils/internal/logger.js.map +1 -1
  175. package/dist/utils/internal/performance.d.ts +8 -31
  176. package/dist/utils/internal/performance.d.ts.map +1 -1
  177. package/dist/utils/internal/performance.js +173 -295
  178. package/dist/utils/internal/performance.js.map +1 -1
  179. package/dist/utils/security/idGenerator.d.ts.map +1 -1
  180. package/dist/utils/security/idGenerator.js +24 -43
  181. package/dist/utils/security/idGenerator.js.map +1 -1
  182. package/dist/utils/security/sanitization.d.ts +0 -7
  183. package/dist/utils/security/sanitization.d.ts.map +1 -1
  184. package/dist/utils/security/sanitization.js +4 -31
  185. package/dist/utils/security/sanitization.js.map +1 -1
  186. package/dist/utils/security/sensitiveFields.d.ts +14 -0
  187. package/dist/utils/security/sensitiveFields.d.ts.map +1 -0
  188. package/dist/utils/security/sensitiveFields.js +31 -0
  189. package/dist/utils/security/sensitiveFields.js.map +1 -0
  190. package/dist/utils/telemetry/trace.d.ts +8 -10
  191. package/dist/utils/telemetry/trace.d.ts.map +1 -1
  192. package/dist/utils/telemetry/trace.js +19 -18
  193. package/dist/utils/telemetry/trace.js.map +1 -1
  194. package/dist/utils/types/guards.d.ts +0 -102
  195. package/dist/utils/types/guards.d.ts.map +1 -1
  196. package/dist/utils/types/guards.js +0 -114
  197. package/dist/utils/types/guards.js.map +1 -1
  198. package/package.json +26 -25
  199. package/scripts/check-framework-antipatterns.ts +4 -1
  200. package/scripts/devcheck.ts +303 -33
  201. package/scripts/lint-packaging.ts +28 -6
  202. package/skills/add-provider/SKILL.md +18 -4
  203. package/skills/add-tool/SKILL.md +33 -1
  204. package/skills/api-config/SKILL.md +4 -18
  205. package/skills/api-errors/SKILL.md +2 -1
  206. package/skills/api-services/SKILL.md +1 -1
  207. package/skills/api-services/references/speech.md +1 -2
  208. package/skills/api-telemetry/SKILL.md +2 -2
  209. package/skills/api-utils/SKILL.md +2 -2
  210. package/skills/code-simplifier/SKILL.md +47 -20
  211. package/skills/design-mcp-server/SKILL.md +6 -1
  212. package/skills/field-test/SKILL.md +158 -39
  213. package/skills/git-wrapup/SKILL.md +67 -29
  214. package/skills/orchestrations/SKILL.md +17 -6
  215. package/skills/orchestrations/workflows/field-test-fix.md +6 -4
  216. package/skills/orchestrations/workflows/fix-wrapup-release.md +6 -4
  217. package/skills/orchestrations/workflows/greenfield-build.md +2 -2
  218. package/skills/orchestrations/workflows/maintenance-release.md +4 -2
  219. package/skills/release-and-publish/SKILL.md +101 -23
  220. package/skills/release-pr-review/SKILL.md +147 -0
  221. package/templates/AGENTS.md +4 -2
  222. package/templates/CLAUDE.md +4 -2
  223. package/templates/package.json +6 -6
  224. package/dist/mcp-server/transports/ITransport.d.ts +0 -15
  225. package/dist/mcp-server/transports/ITransport.d.ts.map +0 -1
  226. package/dist/mcp-server/transports/ITransport.js +0 -2
  227. package/dist/mcp-server/transports/ITransport.js.map +0 -1
  228. package/dist/services/llm/types.d.ts +0 -16
  229. package/dist/services/llm/types.d.ts.map +0 -1
  230. package/dist/services/llm/types.js +0 -9
  231. package/dist/services/llm/types.js.map +0 -1
  232. package/dist/utils/internal/health.d.ts +0 -60
  233. package/dist/utils/internal/health.d.ts.map +0 -1
  234. package/dist/utils/internal/health.js +0 -46
  235. package/dist/utils/internal/health.js.map +0 -1
@@ -1,10 +1,10 @@
1
1
  ---
2
2
  name: field-test
3
3
  description: >
4
- Exercise tools, resources, and prompts against a live HTTP server via MCP JSON-RPC over curl. Starts the server, surfaces the catalog, runs real and adversarial inputs, and produces a tight report with concrete findings and numbered follow-up options. Use after adding or modifying definitions, or when the user asks to test, try out, or verify their MCP surface.
4
+ Exercise tools, resources, and prompts against a live HTTP server via MCP JSON-RPC over curl. Starts the server, surfaces the catalog, runs real and adversarial inputs, measures every call (bytes, token estimate, wall-clock) and weighs the catalog, and produces a tight report with concrete findings and numbered follow-up options. Use after adding or modifying definitions, or when the user asks to test, try out, or verify their MCP surface.
5
5
  metadata:
6
6
  author: cyanheads
7
- version: "2.9"
7
+ version: "2.12"
8
8
  audience: external
9
9
  type: debug
10
10
  ---
@@ -17,7 +17,7 @@ Unit tests (`add-test` skill) verify handler logic with mocked context. Field te
17
17
 
18
18
  ### Transport coverage
19
19
 
20
- This skill drives an HTTP server because curl + JSON-RPC is the most reliable harness for shell-based agents. The same handlers run on both transports — only the framing differs — so HTTP exercises the full functional surface.
20
+ This skill drives an HTTP server because curl + JSON-RPC is the most reliable harness for shell-based agents. The same handlers run on both transports — only the framing differs — so HTTP exercises the full functional surface. Both HTTP session modes are covered: a durable `Mcp-Session-Id` session, and the sessionless initialization a `MCP_SESSION_MODE=stateless` server performs.
21
21
 
22
22
  **Stdio coverage is a boot check only — run this before Step 1.** Run `bun run rebuild && bun run start:stdio`, confirm the startup logs look clean (banner, expected tool/resource counts, no errors/warnings, no missing-config gripes), then kill it. Pino logs go to stderr in stdio mode (stdout is reserved for JSON-RPC), so they print straight to the terminal when you run interactively. No need to call tools over stdio — the HTTP pass already covered handler behavior.
23
23
 
@@ -45,7 +45,8 @@ cat > /tmp/<project-name>-field-test-9DJ73-K103L.sh <<'HELPER_EOF'
45
45
  #
46
46
  # Surfaces failures aggressively — field test is for finding things that fail,
47
47
  # so the helper auto-tails logs and prints HTTP status/body on errors instead
48
- # of swallowing them.
48
+ # of swallowing them. It also measures: every mcp_call prints a one-line
49
+ # size/latency reading on stderr, and mcp_catalog_size weighs tools/list.
49
50
 
50
51
  # Usage: mcp_start /path/to/server [startup-timeout-seconds] (default: 30)
51
52
  # Builds, starts the HTTP server in the background, waits for the listen line,
@@ -93,9 +94,29 @@ mcp_start() {
93
94
  echo "ready pid=$pid url=$url port=$port log=$server_log"
94
95
  }
95
96
 
97
+ # Internal: report a failed initialize with the raw exchange, then clean up.
98
+ _mcp_init_fail() {
99
+ local msg="$1"; local body_file="$2"; local hdr="$3"
100
+ echo "init failed — $msg" >&2
101
+ echo "--- response body ---" >&2
102
+ if [ -s "$body_file" ]; then cat "$body_file" >&2; else echo "(empty)" >&2; fi
103
+ echo "--- response headers ---" >&2
104
+ if [ -s "$hdr" ]; then cat "$hdr" >&2; else echo "(none)" >&2; fi
105
+ rm -f "$hdr" "$body_file"
106
+ return 1
107
+ }
108
+
96
109
  # Usage: mcp_init <url>
97
110
  # Runs `initialize`, sends `notifications/initialized`, prints:
98
- # ready sid=<id> protocol=<negotiated-version>
111
+ # ready sid=<id-or-empty> protocol=<negotiated-version> requested=<want> instructions=<bytes>B (HTTP <code>)
112
+ # `instructions=` is the byte size of the server's `instructions` string — it
113
+ # loads into every client session alongside tools/list, so it is the other half
114
+ # of the per-session context tax mcp_catalog_size weighs.
115
+ # The initialize *result* is what decides success — a session ID is optional.
116
+ # A server started with MCP_SESSION_MODE=stateless mints none, and the session
117
+ # header is then omitted from every later request. Capture BOTH `sid` and
118
+ # `protocol`: mcp_call takes the protocol as its 5th arg, which is what carries
119
+ # the negotiated revision when there is no session to carry it.
99
120
  # A negotiated version older than the requested one means the server capped it
100
121
  # — note that in the report; you are then testing an older protocol than a
101
122
  # current client would use.
@@ -105,41 +126,91 @@ mcp_init() {
105
126
  local want="${MCP_FIELD_TEST_PROTOCOL:-2025-11-25}"
106
127
  local hdr; hdr=$(mktemp)
107
128
  local body_file; body_file=$(mktemp)
108
- local code
129
+ local code curl_rc
109
130
  code=$(curl -sS -D "$hdr" -o "$body_file" -w '%{http_code}' -X POST "$url" \
110
131
  -H "Content-Type: application/json" \
111
132
  -H "Accept: application/json, text/event-stream" \
112
133
  -d "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"initialize\",\"params\":{\"protocolVersion\":\"$want\",\"capabilities\":{},\"clientInfo\":{\"name\":\"field-test\",\"version\":\"1.0.0\"}}}")
113
- local sid; sid=$(grep -i '^mcp-session-id:' "$hdr" | awk '{print $2}' | tr -d '\r\n')
114
- if [ -z "$sid" ]; then
115
- echo "init failed — HTTP $code, no Mcp-Session-Id header returned" >&2
116
- echo "--- response body ---" >&2
117
- cat "$body_file" >&2
118
- echo "--- response headers ---" >&2
119
- cat "$hdr" >&2
120
- rm -f "$hdr" "$body_file"
134
+ curl_rc=$?
135
+ if [ "$curl_rc" -ne 0 ] || [ -z "$code" ] || [ "$code" = "000" ]; then
136
+ _mcp_init_fail "transport failure — curl exit $curl_rc, http_code '${code:-none}'; nothing listening at $url" "$body_file" "$hdr"
121
137
  return 1
122
138
  fi
123
- local got; got=$(sed -n 's/^data: //p' "$body_file" | grep -o '"protocolVersion":"[^"]*"' | head -1 | cut -d'"' -f4)
124
- [ -z "$got" ] && got=$(grep -o '"protocolVersion":"[^"]*"' "$body_file" | head -1 | cut -d'"' -f4)
125
- curl -sS -X POST "$url" \
126
- -H "Content-Type: application/json" \
127
- -H "Accept: application/json, text/event-stream" \
128
- -H "Mcp-Session-Id: $sid" \
139
+ [ "$code" -ge 400 ] && { _mcp_init_fail "HTTP $code" "$body_file" "$hdr"; return 1; }
140
+ # Unwrap SSE framing when present; a plain JSON body is used as-is.
141
+ local payload; payload=$(sed -n 's/^data: //p' "$body_file")
142
+ [ -z "$payload" ] && payload=$(cat "$body_file")
143
+ local reply; reply=$(printf '%s\n' "$payload" | grep -E '"(result|error)"' | head -1)
144
+ [ -z "$reply" ] && reply="$payload"
145
+ if printf '%s' "$reply" | grep -q '"error"'; then
146
+ _mcp_init_fail "server returned a JSON-RPC error" "$body_file" "$hdr"
147
+ return 1
148
+ fi
149
+ if ! printf '%s' "$reply" | grep -q '"result"'; then
150
+ _mcp_init_fail "HTTP $code but no JSON-RPC result in the body" "$body_file" "$hdr"
151
+ return 1
152
+ fi
153
+ local got; got=$(printf '%s' "$reply" | grep -o '"protocolVersion":"[^"]*"' | head -1 | cut -d'"' -f4)
154
+ if [ -z "$got" ]; then
155
+ _mcp_init_fail "initialize result declares no protocolVersion" "$body_file" "$hdr"
156
+ return 1
157
+ fi
158
+ local instr; instr=$(printf '%s' "$reply" | jq -r '.result.instructions // "" | utf8bytelength' 2>/dev/null || echo 0)
159
+ local sid; sid=$(grep -i '^mcp-session-id:' "$hdr" | awk '{print $2}' | tr -d '\r\n')
160
+ local init_headers=(-H "Content-Type: application/json" -H "Accept: application/json, text/event-stream" -H "MCP-Protocol-Version: $got")
161
+ [ -n "$sid" ] && init_headers+=(-H "Mcp-Session-Id: $sid")
162
+ curl -sS -X POST "$url" "${init_headers[@]}" \
129
163
  -d '{"jsonrpc":"2.0","method":"notifications/initialized"}' >/dev/null
130
164
  rm -f "$hdr" "$body_file"
131
- echo "ready sid=$sid protocol=${got:-unknown} requested=$want (HTTP $code)"
165
+ echo "ready sid=$sid protocol=$got requested=$want instructions=${instr}B (HTTP $code)"
132
166
  }
133
167
 
134
- # Usage: mcp_call <url> <sid> <method> [JSON_PARAMS]
168
+ # Internal: one stderr line per call — reply bytes, the content/structured
169
+ # split, a token estimate, wall-clock. Bytes are the reply as delivered (SSE
170
+ # framing stripped). `content` is every text block's bytes, `structured` is
171
+ # structuredContent serialized. The token figure is bytes/4 — an estimate, not
172
+ # a tokenizer. Wall-clock is curl's time_total for the whole exchange.
173
+ _mcp_measure() {
174
+ local method="$1"; local params="$2"; local reply="$3"; local code="$4"; local secs="$5"
175
+ local total; total=$(printf '%s' "$reply" | wc -c | tr -d ' ')
176
+ local ms; ms=$(awk -v s="$secs" 'BEGIN { printf "%d", s * 1000 }')
177
+ local label="$method"
178
+ local split=""
179
+ case "$method" in
180
+ tools/call)
181
+ local name; name=$(printf '%s' "$params" | jq -r '.name // empty' 2>/dev/null)
182
+ [ -n "$name" ] && label="$method $name"
183
+ split=$(printf '%s' "$reply" | jq -r '
184
+ (.result // {}) as $r
185
+ | ([$r.content[]? | select(.type == "text") | .text] | join("") | utf8bytelength) as $c
186
+ | (if $r.structuredContent == null then "none" else ($r.structuredContent | tojson | utf8bytelength | tostring) end) as $s
187
+ | "content \($c) · structured \($s)"' 2>/dev/null)
188
+ ;;
189
+ resources/read)
190
+ split=$(printf '%s' "$reply" | jq -r '
191
+ "text \([.result.contents[]? | .text // ""] | join("") | utf8bytelength)"' 2>/dev/null)
192
+ ;;
193
+ esac
194
+ local tok; tok=$(awk -v b="$total" 'BEGIN { if (b >= 1000) printf "~%.1fk", b / 4000; else printf "~%d", b / 4 }')
195
+ echo "⏱ $label · HTTP $code · ${total} B${split:+ ($split)} · $tok tok · ${ms} ms" >&2
196
+ }
197
+
198
+ # Usage: mcp_call <url> <sid> <method> [JSON_PARAMS] [protocol]
135
199
  # Prints the JSON-RPC response. SSE framing is stripped when present, and only
136
200
  # the reply is emitted (a single POST can also carry progress notifications, so
137
201
  # emitting every event would break `| jq .result`). A transport failure or an
138
202
  # HTTP >= 400 prints the details and returns non-zero — it never returns 0 with
139
203
  # empty output. Pipe to `jq`.
204
+ # Every call also prints one measurement line on stderr, e.g.
205
+ # ⏱ tools/call gbif_search_species · HTTP 200 · 18412 B (content 9100 · structured 8900) · ~4.6k tok · 812 ms
206
+ # Read it on every call — it is the size/latency evidence the report cites.
207
+ # `sid` may be empty ('') for a stateless server; the session header is then
208
+ # omitted. Pass the `protocol` mcp_init printed as the 5th arg — with no
209
+ # session carrying the negotiation, MCP-Protocol-Version is what tells the
210
+ # server which revision the request speaks.
140
211
  mcp_call() {
141
- local url="$1"; local sid="$2"; local method="$3"; local params="${4:-}"
142
- [ -z "$url" ] || [ -z "$sid" ] || [ -z "$method" ] && { echo "usage: mcp_call <url> <sid> <method> [params]" >&2; return 1; }
212
+ local url="$1"; local sid="$2"; local method="$3"; local params="${4:-}"; local protocol="${5:-}"
213
+ [ -z "$url" ] || [ -z "$method" ] && { echo "usage: mcp_call <url> <sid> <method> [params] [protocol]" >&2; return 1; }
143
214
  local body
144
215
  if [ -z "$params" ]; then
145
216
  body=$(printf '{"jsonrpc":"2.0","id":%d,"method":"%s"}' "$RANDOM" "$method")
@@ -147,13 +218,13 @@ mcp_call() {
147
218
  body=$(printf '{"jsonrpc":"2.0","id":%d,"method":"%s","params":%s}' "$RANDOM" "$method" "$params")
148
219
  fi
149
220
  local resp_file; resp_file=$(mktemp)
150
- local code curl_rc
151
- code=$(curl -sS -o "$resp_file" -w '%{http_code}' -X POST "$url" \
152
- -H "Content-Type: application/json" \
153
- -H "Accept: application/json, text/event-stream" \
154
- -H "Mcp-Session-Id: $sid" \
155
- -d "$body")
221
+ local stats code secs curl_rc
222
+ local headers=(-H "Content-Type: application/json" -H "Accept: application/json, text/event-stream")
223
+ [ -n "$sid" ] && headers+=(-H "Mcp-Session-Id: $sid")
224
+ [ -n "$protocol" ] && headers+=(-H "MCP-Protocol-Version: $protocol")
225
+ stats=$(curl -sS -o "$resp_file" -w '%{http_code} %{time_total}' -X POST "$url" "${headers[@]}" -d "$body")
156
226
  curl_rc=$?
227
+ read -r code secs <<< "$stats"
157
228
  if [ "$curl_rc" -ne 0 ] || [ -z "$code" ] || [ "$code" = "000" ]; then
158
229
  echo "TRANSPORT FAILURE calling $method — curl exit $curl_rc, http_code '${code:-none}'." >&2
159
230
  echo "Server not reachable at $url (check it is still running: mcp_log <log>)." >&2
@@ -166,14 +237,44 @@ mcp_call() {
166
237
  rm -f "$resp_file"
167
238
  return 1
168
239
  fi
240
+ local reply
169
241
  local sse; sse=$(sed -n 's/^data: //p' "$resp_file")
170
242
  if [ -n "$sse" ]; then
171
- local reply; reply=$(printf '%s\n' "$sse" | grep -E '"(result|error)"')
172
- printf '%s\n' "${reply:-$sse}"
243
+ reply=$(printf '%s\n' "$sse" | grep -E '"(result|error)"')
244
+ reply="${reply:-$sse}"
173
245
  else
174
- cat "$resp_file"
246
+ reply=$(cat "$resp_file")
175
247
  fi
176
248
  rm -f "$resp_file"
249
+ _mcp_measure "$method" "$params" "$reply" "$code" "$secs"
250
+ printf '%s\n' "$reply"
251
+ }
252
+
253
+ # Usage: mcp_catalog_size <url> <sid> [protocol]
254
+ # Weighs the catalog: the bytes of the tools/list reply — what every client
255
+ # loads into context per session before a single call — then each tool's
256
+ # serialized entry, largest first, split into description / inputSchema /
257
+ # outputSchema so the row says WHERE the weight is. A fat outputSchema costs as
258
+ # much as a fat description and is the usual surprise. Prints:
259
+ # catalog: 12 tools · 48210 B · ~12.1k tok
260
+ # <bytes> <~tok> <name> desc <b> · input <b> · output <b|none> (one row per tool)
261
+ mcp_catalog_size() {
262
+ local url="$1"; local sid="$2"; local protocol="${3:-}"
263
+ [ -z "$url" ] && { echo "usage: mcp_catalog_size <url> <sid> [protocol]" >&2; return 1; }
264
+ local reply; reply=$(mcp_call "$url" "$sid" tools/list '' "$protocol") || return 1
265
+ printf '%s' "$reply" | jq -r '
266
+ def tok: if . >= 1000 then "~\(. / 4000 * 10 | round / 10)k" else "~\(. / 4 | floor)" end;
267
+ def bytes_or_none: if . == null then "none" else (tojson | utf8bytelength | tostring) end;
268
+ (.result.tools // []) as $t
269
+ | (. | tojson | utf8bytelength) as $total
270
+ | "catalog: \($t | length) tools · \($total) B · \($total | tok) tok",
271
+ ($t
272
+ | map({name, b: (tojson | utf8bytelength),
273
+ d: ((.description // "") | utf8bytelength),
274
+ i: (.inputSchema | bytes_or_none),
275
+ o: (.outputSchema | bytes_or_none)})
276
+ | sort_by(-.b) | .[]
277
+ | "\(.b)\t\(.b | tok)\t\(.name)\tdesc \(.d) · input \(.i) · output \(.o)")'
177
278
  }
178
279
 
179
280
  # Usage: mcp_log <server-log-path> [N] (default: 50 lines)
@@ -241,11 +342,19 @@ Capture `pid`, `url`, `port`, `log` from the `mcp_start` output — every later
241
342
  mcp_init <url-from-mcp_start>
242
343
  ```
243
344
 
244
- Runs `initialize`, sends `notifications/initialized`, prints `sid=<id>` to capture for `mcp_call`, plus the protocol version the server negotiated.
345
+ Runs `initialize`, sends `notifications/initialized`, prints the `sid` and `protocol` to capture for `mcp_call`, plus `instructions=` — the byte size of the server's `instructions` string, which every client loads per session alongside the catalog (record it with the catalog total in Step 3). Success is decided by the initialize *result*, so a transport failure, a non-2xx status, a JSON-RPC error, a malformed body, or a result with no `protocolVersion` all fail loudly with the raw exchange.
245
346
 
246
347
  The helper requests the newest `initialize`-negotiated revision the SDK supports (`2025-11-25`). **If `protocol=` comes back older than `requested=`, the server capped it** — every call after that exercises an older protocol than a current client would negotiate. Note it as a `bug` finding and check the pinned `@modelcontextprotocol/server` version; don't quietly test the downgraded surface. To deliberately test an older version, set `MCP_FIELD_TEST_PROTOCOL`.
247
348
 
248
- This exercises the **2025-era arm**: `initialize` negotiates the revision, the server mints an `Mcp-Session-Id`, and every later call rides that session. The [2026-07-28 revision](https://modelcontextprotocol.io/specification/2026-07-28/basic/transports) is not in the `initialize` ladder at all — it is selected per request by the `io.modelcontextprotocol/protocolVersion` key in the request's own `_meta` envelope, and carries no session. Curl-based field testing therefore covers the sessionful leg; exercise the per-request leg from a real 2026-era client or an integration test.
349
+ **`sid=` may come back empty — that is a pass, not a failure.** Under `MCP_SESSION_MODE=stateless` the server mints no `Mcp-Session-Id`, and the helper then omits the session header from every later request. Thread the empty value through positionally and pass the negotiated protocol, which is what identifies the revision when no session carries it:
350
+
351
+ ```bash
352
+ mcp_call <url> '' tools/list '' <protocol-from-mcp_init>
353
+ ```
354
+
355
+ To exercise both session modes, start the server twice — once with the project's default, once with `MCP_SESSION_MODE=stateless` — and run the same calls against each.
356
+
357
+ Both modes exercise the **2025-era arm**: `initialize` negotiates the revision, and the session (when there is one) carries it. The [2026-07-28 revision](https://modelcontextprotocol.io/specification/2026-07-28/basic/transports) is a different thing from a sessionless 2025 handshake — it does not initialize at all, and is selected per request by the `io.modelcontextprotocol/protocolVersion` key in the request's own `_meta` envelope. This helper does not reach it; exercise the per-request leg from a real 2026-era client or an integration test.
249
358
 
250
359
  ### 3. Surface the catalog
251
360
 
@@ -254,8 +363,11 @@ This exercises the **2025-era arm**: `initialize` negotiates the revision, the s
254
363
  mcp_call <url> <sid> tools/list | jq '.result.tools[] | {name, description, inputSchema, outputSchema}'
255
364
  mcp_call <url> <sid> resources/list | jq '.result.resources[] | {uri, name, mimeType}'
256
365
  mcp_call <url> <sid> prompts/list | jq '.result.prompts[] | {name, description, arguments}'
366
+ mcp_catalog_size <url> <sid> <protocol>
257
367
  ```
258
368
 
369
+ **Weigh the catalog.** `mcp_catalog_size` prints the `tools/list` bytes — the context every client loads per session before a single call — and each tool's entry, largest first, split into description / `inputSchema` / `outputSchema`. Record the total alongside the `instructions=` bytes from Step 2; together they are the per-session tax. The split says where a heavy tool's weight lives: an `outputSchema` narrating every field of a 60-field record is the common surprise, an over-long description the obvious one. Hand the outliers to `tool-defs-analysis` (its length-outliers pass) rather than trimming blind.
370
+
259
371
  Present a compact catalog to the user: each definition's name + 1-line description. Flag vague or missing descriptions as you go — those feed into the report. Use this to build the test plan.
260
372
 
261
373
  **Audit every description for leaks** — tool description, every parameter `.describe()` in `inputSchema`, and every field `.describe()` in `outputSchema` (the `outputSchema` projection above is what surfaces these; don't skim past it). Three categories:
@@ -277,6 +389,7 @@ Treat any hit as a `ux` finding in the report. The authoring rule lives under *T
277
389
  | Happy path | One realistic input. Output shape matches schema. `content[]` text reads clearly to a human. |
278
390
  | `structuredContent` ↔ `content[]` parity | Dump the whole array (`jq '.result.content'`) and check every `structuredContent` field is surfaced *somewhere* in it — enrichment lands in its own trailing block, not in `content[0]`. Parity gap = client-specific blindness. |
279
391
  | Input error | One invalid input (wrong type or missing required). Error text says *what*, *why*, *how to fix*. |
392
+ | Size & latency | Read the `⏱` line `mcp_call` prints on every call. A happy-path response over **24,000 B** (the framework's `DEFAULT_OUTLINE_BUDGET_BYTES` — the line at which it would outline a document itself) with no truncation disclosure and no retrieval path (cursor, offset, `sections`, canvas handle) is a `ux` finding: the agent pays the whole payload with no way to ask for less. `content` ≈ `structured` with the text starting `{` means the JSON is on the wire twice — a missing `format()`. A call over ~5 s on a happy-path input is worth a `mcp_log` look before calling it upstream latency. |
280
393
 
281
394
  **Situational — add only when triggered**
282
395
 
@@ -309,7 +422,7 @@ Treat any hit as a `ux` finding in the report. The authoring rule lives under *T
309
422
 
310
423
  Use `TaskCreate` — one task per definition. Mark complete as you go. Don't batch.
311
424
 
312
- For each call, capture: input sent, response (trim huge payloads to files), whether `isError: true` appeared, anything surprising (slow response, parity drift, unhelpful text, crash).
425
+ For each call, capture: input sent, the `⏱` line (bytes, split, ms), response (trim huge payloads to files), whether `isError: true` appeared, anything surprising (slow response, parity drift, unhelpful text, crash).
313
426
 
314
427
  When a call surprises you — slow, hangs, returns terse output, surfaces an unhelpful error — run `. /tmp/<project-name>-field-test-<ID>.sh && mcp_log <log>` to tail the server log. The pino startup banner, request handler errors, upstream API call traces, and rate-limit warnings all land in the per-server log (read via `mcp_log`) rather than coming back through `mcp_call`. Don't guess at runtime behavior from response text alone.
315
428
 
@@ -334,12 +447,16 @@ Kills the background server and its port-holding child, removes the server log,
334
447
 
335
448
  ### 7. Report
336
449
 
337
- Three sections. Tight. The user should be able to skim the summary, read details only for what matters, and act on numbered options.
450
+ Four sections. Tight. The user should be able to skim the summary, scan the numbers, read details only for what matters, and act on numbered options.
338
451
 
339
452
  #### Summary (1 paragraph)
340
453
 
341
454
  One paragraph. How many definitions exercised, how many passed clean, how many have issues, and the single most important finding. No tables, no lists.
342
455
 
456
+ #### Size & latency
457
+
458
+ Per-session tax on its own line (`instructions` bytes + catalog bytes, with the heaviest tool named), then one row per tool exercised, sorted by happy-path bytes descending: tool · bytes · ~tok · ms. Over 15 tools, keep every row over 24,000 B plus the three slowest and fold the rest into one line ("N more under budget, median X B"). Numbers only — what they mean goes in Findings.
459
+
343
460
  #### Findings
344
461
 
345
462
  Only include definitions with issues. Group by severity. Each finding is 2–4 lines unless it genuinely needs more. A parity finding cites the full `content[]` dump as its evidence — a quote from one index doesn't establish drift.
@@ -379,8 +496,10 @@ End with:
379
496
 
380
497
  - [ ] Stdio boot check completed — `bun run rebuild && bun run start:stdio` shows clean startup (banner, expected counts, no errors)
381
498
  - [ ] HTTP server built and started; real port parsed from log
382
- - [ ] Session initialized; `notifications/initialized` sent; negotiated protocol version matches the requested one (a downgrade is a finding)
499
+ - [ ] Session initialized (a stateless server returns an empty `sid` — still a pass); `notifications/initialized` sent; negotiated protocol version matches the requested one (a downgrade is a finding)
383
500
  - [ ] Catalog surfaced and presented; descriptions audited for leaks (implementation details, meta-coaching, consumer-aware phrasing)
501
+ - [ ] Catalog weighed (`mcp_catalog_size`); total + `instructions=` bytes recorded for the report
502
+ - [ ] Every call's `⏱` line read; any happy-path response over 24,000 B with no disclosure + retrieval path filed as `ux`
384
503
  - [ ] Universal battery run on every definition (happy path, parity against the full `content[]` array, input error)
385
504
  - [ ] Situational categories applied only when triggered
386
505
  - [ ] **If >15 tools:** sampled 30–40% for situational testing; skipped definitions listed in report
@@ -389,4 +508,4 @@ End with:
389
508
  - [ ] **If any tool truncates, caps, or spills its output:** truncation forced; disclosure + a retrieval path (cursor, offset, selector, canvas handle) verified
390
509
  - [ ] External-state / auth-gated tools handled explicitly (run, skip, or confirm)
391
510
  - [ ] Server stopped (port confirmed free); server log and helper script removed
392
- - [ ] Report: summary paragraph → grouped findings → numbered options
511
+ - [ ] Report: summary paragraph → size & latency table → grouped findings → numbered options
@@ -1,23 +1,35 @@
1
1
  ---
2
2
  name: git-wrapup
3
3
  description: >
4
- Land working-tree changes as logical commits — the work grouped by concern, topped by a release commit (version bump, changelog, regenerated artifacts) and an annotated tag. Verify, commit, tag. Stops at "committed and tagged locally" — no push, no publish. The release-and-publish skill picks up from here. Distilled from the git_wrapup_instructions protocol.
4
+ Land working-tree changes as logical commits — the work grouped by concern, topped by a release commit (version bump, changelog, regenerated artifacts). Verify, commit. Stops at "committed locally on main" — or, when the project releases through a release PR, at "release branch pushed, PR open". No tag, no push to main, no publish: the release-and-publish skill merges, tags, and ships from here. Distilled from the git_wrapup_instructions protocol.
5
5
  metadata:
6
6
  author: cyanheads
7
- version: "1.11"
7
+ version: "1.13"
8
8
  audience: external
9
9
  type: workflow
10
10
  ---
11
11
 
12
12
  ## When to use
13
13
 
14
- Working-tree or staged changes are ready to ship as a new version. This skill lands them as a stack of logical commits — the work grouped by concern, topped by a release commit (version + changelog + tree) — with an annotated tag. It does NOT push or publish — that's a separate step (`release-and-publish`).
14
+ Working-tree or staged changes are ready to ship as a new version. This skill lands them as a stack of logical commits — the work grouped by concern, topped by a release commit (version + changelog + tree). It does NOT tag, push to main, or publish — `release-and-publish` does all three.
15
15
 
16
16
  Common triggers:
17
17
  - Feature work, bug fixes, or dependency updates are done and tested
18
18
  - A maintenance or polish pass left changes in the working tree
19
19
  - An orchestrator says "wrapup this project"
20
20
 
21
+ ## Release PR mode
22
+
23
+ A project can route every release through a pull request — one PR per version, for the audit trail and a stable review target. The mode is declared in the project's `CLAUDE.md`/`AGENTS.md` or in the caller's brief; when neither says anything, there is no release PR and the stack lands on `main` directly.
24
+
25
+ | Mode | Wrapup ends at | Then |
26
+ |:--|:--|:--|
27
+ | *(none — default)* | commit stack on `main`, tree clean | `release-and-publish` tags HEAD and ships |
28
+ | **gated** | commit stack on `release/<version>`, branch pushed, PR open | a review pass on the PR (`release-pr-review` skill), then a separate `release-and-publish` run fast-forwards `main`, tags, and ships |
29
+ | **straight-through** | same as gated | the same agent continues straight into `release-and-publish` |
30
+
31
+ The branch is created at wrapup time, never before: work happens on `main` until the version is known, then the uncommitted tree moves to `release/<version>` in one step (step 7). The commit stack, the release commit, and the tag format are identical in every mode — the PR adds an artifact around them, it does not change them.
32
+
21
33
  ## Pre-wrapup gate checklist
22
34
 
23
35
  Every item must be true before starting wrapup. Committing means releasing — a commit only happens when the work is ready to ship, not just "the edits are done." Each item is a goal to verify.
@@ -69,6 +81,7 @@ Every file that declares a version must be updated. Skip any file that doesn't e
69
81
  - `package.json` — `version`
70
82
  - `server.json` — top-level `version` AND every `packages[].version` entry
71
83
  - `manifest.json` (if present) — `version`. Verify `name` is the bare package name (e.g. `bls-mcp-server`, not `@cyanheads/bls-mcp-server`)
84
+ - `.claude-plugin/plugin.json` and `.codex-plugin/plugin.json` (if present) — `version`. Packaging validation fails on a mismatch; `.codex-plugin/mcp.json` is connection config and carries none
72
85
  - `README.md` — version badge
73
86
  - `CLAUDE.md` / `AGENTS.md` — if they pin a version string
74
87
  - `Dockerfile` — OCI labels if they pin the version
@@ -126,10 +139,19 @@ bun run test:package # only if the script exists — NOT part of test:all
126
139
 
127
140
  ### 7. Commit — group by concern, release artifacts on top
128
141
 
142
+ **Release PR mode only — move to the release branch first, before the first commit:**
143
+
144
+ ```bash
145
+ git branch --show-current # must be main
146
+ git switch -c release/<version> # uncommitted work rides along
147
+ ```
148
+
149
+ Commits never land on `main` in this mode. If a `release/*` branch already exists locally, a prior release PR was never merged — halt and report it rather than stacking a second release on top.
150
+
129
151
  Do NOT `git add -A` into one commit. Group the working tree into a handful of logical commits — never one blob:
130
152
 
131
153
  1. **The work — one commit per concern.** A feature spanning multiple layers splits by layer: runtime/logic, linter/tooling, docs/skills. Unrelated changes (two separate fixes, an incidental doc tweak) are their own commits. Work commits do not carry the version.
132
- 2. **The release commit — last, on top.** Version bumps (`package.json`, `server.json`, README badge, `CLAUDE.md`/`AGENTS.md`), the changelog entry, `CHANGELOG.md`, and `docs/tree.md` go in a single final commit that sits on top of the work stack — never mixed into a feature commit.
154
+ 2. **The release commit — last, on top.** Version bumps (`package.json`, `server.json`, `manifest.json`, the plugin manifests, README badge, `CLAUDE.md`/`AGENTS.md`), the changelog entry, `CHANGELOG.md`, and `docs/tree.md` go in a single final commit that sits on top of the work stack — never mixed into a feature commit.
133
155
 
134
156
  Stage each group explicitly, commit it, then move to the next — the release commit goes last:
135
157
 
@@ -171,66 +193,81 @@ The changelog carries the depth, the tag carries the headline, the commit carrie
171
193
 
172
194
  **Right-size it.** "Group by concern" is not "always split." A genuinely single-concern change — one fix, a dependency bump, a small doc edit — is one work commit plus the release commit; when the change and its version bump are inseparable for a tiny patch, a single commit whose subject leads with the version is fine. The failure mode to prevent is the inverse: a large, multi-layer feature crammed into one commit alongside the release artifacts.
173
195
 
174
- ### 8. Create an annotated tag
196
+ ### 8. Open the release PR (release PR mode only)
197
+
198
+ Skip this step entirely when the project has no release PR mode — go to step 9.
175
199
 
176
200
  ```bash
177
- git tag -a v<version> --cleanup=whitespace -m "<tag message with embedded newlines>"
201
+ git push -u origin release/<version>
202
+ gh pr create --base main --head release/<version> --title "<release commit subject>" --body-file <path-to-body.md>
178
203
  ```
179
204
 
180
- Use `-m` with embedded newlines in the string (the commit `-m`-only constraint applies here too — no heredoc). The tag message renders as the GitHub Release body via `--notes-from-tag`. It must be structured markdown, not a flat string.
205
+ **Title:** the release commit's subject, verbatim — `chore(release): <version> — <theme>`.
181
206
 
182
- `--cleanup=whitespace` is load-bearing. The default cleanup (`strip`) deletes `#`-leading lines as comments, so markdown headers silently vanish from the tag body. `--cleanup=verbatim` is worse: it skips end-of-message normalization, so with tag signing enabled the signature is appended flush against the message's last character — git then can't parse its own signature (the tag reads as unsigned) and the whole `-----BEGIN SSH SIGNATURE-----` block publishes verbatim into the GitHub Release body.
207
+ **Body — always via `--body-file`, never an inline `--body` string** (backticks inside a double-quoted argument are command substitution and silently vanish). Write the file to a scratch location, not into the repo.
183
208
 
184
- Format — a **headline digest**, never a section-by-section changelog mirror:
209
+ The body is the release digest — the same headline digest the annotated tag will carry, plus a gates record. It is written here, reviewed on the PR, and copied into the tag at release time, so it is the one place the release notes get reviewed before they become permanent. Format:
185
210
 
186
211
  ```
187
- <theme — omit version number, GitHub prepends v<VERSION>:>
212
+ <theme — the changelog entry's summary: line, plain prose, one line>
213
+
214
+ ## Changes
188
215
 
189
216
  - <notable user-facing change> (#N)
190
217
  - <notable user-facing change> (#N)
191
218
  - <ONE compact grouped line for the minor/internal changes — build config, repo hygiene, metadata>
192
219
  - deps: `@cyanheads/mcp-ts-core` ^0.10.6 → ^0.10.14 (+ dev-dep bumps)
193
220
 
221
+ ## Gates
222
+
223
+ - `bun run devcheck` — clean
224
+ - `bun run rebuild` — ok
225
+ - `bun run test:all` — <N> passed
226
+ - `bun run test:package` — <N> passed (only where the project defines it)
227
+
194
228
  [CHANGELOG v<version>](https://github.com/<OWNER>/<REPO>/blob/main/changelog/<major.minor>.x/<version>.md)
195
229
  ```
196
230
 
197
231
  **Rules:**
198
- - Subject line omits the version number (GitHub prepends `v<VERSION>:` to the release title)
199
- - **Flat bullets only — never Keep-a-Changelog section headers.** `Added:`/`Changed:`/`Fixed:`/`Dependency bumps:` belong in the changelog file; a tag that mirrors the changelog's structure is wrong even when every line is accurate
200
- - **Complete at headline granularity** — every changelog-worthy change stays visible: notable changes get their own bullet, minor/internal items (build config, repo hygiene, metadata) share ONE grouped compact bullet. Nothing silently dropped, nothing expanded — the changelog carries the depth, the tag carries the existence
201
- - **Deps: one line max**, naming only what earns it (the framework bump, a major); per-package arrows for the rest live in the changelog entry only
202
- - **No gates line** — test counts and devcheck status are changelog detail, not release-body material
203
- - No narrative preamble — bullets under the subject, no paragraph blocks
204
- - No marketing adjectives
205
- - Length is earned — a subject + two bullets + changelog link is a fine tag for a small patch
206
- - **Issue backlinks:** when changes address GitHub issues, include `(#N)` references in the relevant bullets — same as the changelog entry. The backlinks render as clickable links in the GitHub Release body.
207
- - **Changelog link (final line):** end the tag body with a Markdown link to this version's changelog file, so the GitHub Release offers a one-click jump to the full entry — `[CHANGELOG v<version>](https://github.com/<OWNER>/<REPO>/blob/main/changelog/<major.minor>.x/<version>.md)`. Derive `<OWNER>/<REPO>` from the origin remote; the path mirrors the file authored in step 4 (e.g. `changelog/0.10.x/0.10.12.md`). Keep the blank line above it so it renders as its own paragraph, not appended to the gates line.
232
+ - **`## Changes` follows the tag rules exactly** (`release-and-publish` step 4): flat bullets, never Keep-a-Changelog section headers; complete at headline granularity — notable changes get their own bullet, minor/internal items share ONE grouped bullet; deps one line max, naming only what earns it; no narrative, no marketing adjectives. Depth lives in the changelog entry, which is in this PR's diff and linked on the last line.
233
+ - **Every claim traces to the diff and to the changelog entry.** The body is derived from the entry you authored in step 4, never written independently of it.
234
+ - **`## Gates` is the one release surface that carries gate results** — the exact commands from step 6 with their outcomes. It never enters the tag.
235
+ - **Issue references are bare `(#N)` backlinks — never a closing keyword** (`Closes #N`, `Fixes #N`); the merge would close the issue before its close-out comment lands.
236
+ - **Changelog link is the final line**, same form as the tag, blank line above it.
237
+ - Length is earned — a theme, two bullets, gates, and the link is a complete body for a small patch.
238
+
239
+ If the review pass changes what ships, `release-pr-review` updates `## Changes` and `## Gates` to match; `release-and-publish` then lifts `## Changes` plus the final link into the tag verbatim.
240
+
241
+ **Gated mode: halt here.** Report the PR URL, the branch, and the commit stack. Do not tag, do not merge, do not touch `main`. The review pass and `release-and-publish` run as separate steps after this one.
242
+
243
+ **Straight-through mode:** continue directly into `release-and-publish`.
208
244
 
209
245
  ### 9. Verify end state
210
246
 
211
247
  ```bash
212
248
  git log --oneline -8 # confirm the commit stack: work commits + release commit on top
213
- git show v<version> --stat | head -20 # confirm tag points at HEAD (the release commit)
214
249
  git status # must be clean
215
- git tag -l v<version> --format='%(if)%(contents:signature)%(then)signed%(else)unsigned%(end)' # with tag signing enabled, must print "signed"
250
+ git tag --points-at HEAD # must print nothing — tagging is release-and-publish's job
251
+ git branch --show-current # main, or release/<version> in release PR mode
252
+ gh pr view --json number,url,state # release PR mode: OPEN, head = the branch above
216
253
  ```
217
254
 
218
- If the working tree isn't clean or the tag doesn't point at HEAD, something went wrong — investigate before proceeding. `unsigned` under enabled tag signing means the signature didn't parse (see step 8's cleanup note) — delete and recreate the tag before it leaks the signature block into the GitHub Release body.
255
+ If the working tree isn't clean or the release commit isn't at HEAD, something went wrong — investigate before proceeding.
219
256
 
220
- **Do NOT push.** This skill stops here. Use the `release-and-publish` skill for the push + publish workflow.
257
+ **Do NOT tag, push `main`, or publish.** This skill stops here. `release-and-publish` merges the release branch when there is one, creates the tag, pushes, and publishes.
221
258
 
222
259
  ## Constraints
223
260
 
224
- - **Local only.** No `git push`, no remote operations
261
+ - **No push to `main`, no tag, no publish.** The only remote writes this skill makes are the release-branch push and the PR create in release PR mode
225
262
  - **Never stash.** Not for quick checks, not for testing, not for any reason
226
- - **Never destructive.** No `git reset --hard`, `git restore .`, `git clean -f`, `git checkout -- .`
263
+ - **Never destructive.** No `git reset --hard`, `git restore .`, `git clean -f`, `git checkout -- .`, no force-push
227
264
  - **Bash git only.** Drive every git operation through the shell
228
265
  - If `v<version>` already exists as a tag, **halt and report the conflict** — include the version string, existing tag SHA, and current HEAD SHA so the caller can resolve it. Do not delete or move tags without explicit authorization
229
266
 
230
267
  ## Checklist
231
268
 
232
269
  - [ ] Diff reviewed end-to-end before version bump
233
- - [ ] Version bumped in every declaring file (`package.json`, `server.json`, `manifest.json`, README badge, `CLAUDE.md`/`AGENTS.md` if they pin a version)
270
+ - [ ] Version bumped in every declaring file (`package.json`, `server.json`, `manifest.json`, `.claude-plugin/plugin.json`, `.codex-plugin/plugin.json`, README badge, `CLAUDE.md`/`AGENTS.md` if they pin a version)
234
271
  - [ ] GH issues addressed by this work commented with what landed (if working from GH issues)
235
272
  - [ ] Docs updated for any new or changed features
236
273
  - [ ] Changelog authored at `changelog/<major.minor>.x/<version>.md`
@@ -239,8 +276,9 @@ If the working tree isn't clean or the tag doesn't point at HEAD, something went
239
276
  - [ ] `bun run devcheck` passes
240
277
  - [ ] `bun run test:all` (or `test`) passes
241
278
  - [ ] `bun run test:package` passes, when the project defines it — it guards the public-export manifest and `test:all` does not run it
279
+ - [ ] Release PR mode: stack committed on `release/<version>`, never on `main`
242
280
  - [ ] Work grouped into logical commits (large features split by layer); release artifacts (version + changelog + tree) committed separately on top, subject leading with the version
243
281
  - [ ] Every commit carries a body, and every body is one or two lines — none subject-only, none a paragraph
244
- - [ ] Annotated tag `v<version>` with structured markdown message, final line linking this version's changelog file
282
+ - [ ] Release PR mode: branch pushed, PR open — title = release commit subject; body = theme line, `## Changes` in tag rules, `## Gates`, changelog link last (via `--body-file`, no closing keywords)
245
283
  - [ ] Working tree clean
246
- - [ ] Nothing pushed — local only
284
+ - [ ] No tag at HEAD, nothing pushed to `main` — `release-and-publish` owns both
@@ -4,7 +4,7 @@ description: >
4
4
  Pick and run a multi-phase workflow that chains foundational task skills (`git-wrapup`, `release-and-publish`, `maintenance`, `field-test`, `setup`, etc.) end-to-end. Routes user intent to a workflow file under `workflows/` — greenfield builds, maintenance + release, field-test + fix, or known-work + release. Single source for the universal rules (no commits without authorization, no destructive git, no marketing language), the orchestrator posture (own the goal, ground sub-agents in primary sources, verify against the goal), and the sub-agent strategy (orient block, parallel fanout, isolation, normalization) that apply across every workflow. Sub-agents are an optional capability — workflows run linearly when fanout isn't available.
5
5
  metadata:
6
6
  author: cyanheads
7
- version: "1.7"
7
+ version: "1.8"
8
8
  audience: external
9
9
  type: workflow
10
10
  ---
@@ -51,14 +51,14 @@ A workflow file is the orchestrator's playbook for one run. Read it end-to-end b
51
51
 
52
52
  These apply to every workflow. Workflow files don't restate them; the orchestrator carries them forward and restates them in sub-agent prompts where applicable.
53
53
 
54
- 1. **No commits, pushes, tags, branch creation, or destructive ops without explicit user authorization.** Work phases leave the working tree dirty for orchestrator review. Wrap-up and release phases run only after the user authorizes — though once authorized, the authorization is durable through the workflow's end (no re-asking at each phase boundary).
54
+ 1. **No commits, pushes, tags, branch creation, or destructive ops without explicit user authorization.** Work phases leave the working tree dirty for orchestrator review. Wrap-up and release phases run only after the user authorizes — though once authorized, the authorization is durable through the workflow's end (no re-asking at each phase boundary). The `release/<version>` branch and PR that `git-wrapup` creates in release PR mode are part of the authorized release, not a separate ask.
55
55
  2. **No `git stash`, no `git reset --hard`, no `git restore .`, no `git clean -f`, no `git checkout -- .`.** These bypass safety and risk silent data loss. Read-only git (`status`, `diff`, `log`, `show`, `blame`) is always safe.
56
56
  3. **No `--no-verify`, no `--no-gpg-sign`, no bypassing commit hooks.** If a hook fails, investigate the underlying issue.
57
57
  4. **`bun run devcheck` is the handoff gate between phases.** Work phases must hand back a green devcheck. If a phase can't reach green, halt and report the failing step verbatim rather than carrying broken state forward.
58
58
  5. **No marketing adjectives** in commits, tags, READMEs, or changelog entries — no "comprehensive", "robust", "enhanced", "seamless", "improved". State the change, not its quality.
59
59
  6. **One workflow per orchestration run.** Don't interleave two workflows in the same session. If a target needs both (e.g., maintenance surfaces a bug fix that needs field-testing first), sequence them as two workflow runs with a clean handoff in between.
60
60
  7. **`gh release create --notes-from-tag` is incompatible with `--repo`.** Always `cd` into the target repo directory for `gh release` commands.
61
- 8. **Annotated tags only** (`git tag -a`), never lightweight, created with `--cleanup=whitespace` — the default (`strip`) deletes `#`-leading lines as comments; `--cleanup=verbatim` glues the SSH signature into the message (unparseable-as-signed tag, signature block leaks into the release body). Tag annotation subject omits the version number — GitHub prepends `v<VERSION>:` to release titles when using `--notes-from-tag`, so including the version in the subject creates stutter. The tag body's final line is a Markdown link to this version's changelog file — `[CHANGELOG v<VERSION>](https://github.com/<OWNER>/<REPO>/blob/main/changelog/<major.minor>.x/<VERSION>.md)`, separated from the gates line by a blank line — giving the release a one-click jump to the full entry.
61
+ 8. **Annotated tags only** (`git tag -a`), never lightweight, created with `--cleanup=whitespace` — the default (`strip`) deletes `#`-leading lines as comments; `--cleanup=verbatim` glues the SSH signature into the message (unparseable-as-signed tag, signature block leaks into the release body). Tag annotation subject omits the version number — GitHub prepends `v<VERSION>:` to release titles when using `--notes-from-tag`, so including the version in the subject creates stutter. The tag body's final line is a Markdown link to this version's changelog file — `[CHANGELOG v<VERSION>](https://github.com/<OWNER>/<REPO>/blob/main/changelog/<major.minor>.x/<VERSION>.md)`, on its own paragraph — giving the release a one-click jump to the full entry; in release PR mode that line continues with ` · release PR #<N>` so the release also points at its audit trail.
62
62
  9. **Conventional Commits subjects** (`feat|fix|refactor|chore|docs|test|build(scope): message`). One logical concern per commit. The release commit (version bump + changelog + regenerated artifacts) lands on top of a stack of feature/fix commits, never collapsed alongside them.
63
63
  10. **Email on any artifact is the user's domain email**, never a personal address that might appear in git config.
64
64
 
@@ -161,7 +161,17 @@ For N targets in a phase:
161
161
 
162
162
  ### Editor / wrap-up separation
163
163
 
164
- Editing phases and wrap-up phases never go in the same sub-agent. Editing sub-agents make file changes and run devcheck — they do not commit, tag, or push. Wrap-up sub-agents read the working tree, commit, tag, and (when releasing) push and publish — they do not edit source. This separation lets the orchestrator review diffs before they become permanent and keeps the commit graph clean.
164
+ Editing phases and wrap-up phases never go in the same sub-agent. Editing sub-agents make file changes and run devcheck — they do not commit, tag, or push. Wrap-up sub-agents read the working tree, commit, and (when releasing) tag, push and publish — they do not edit source. This separation lets the orchestrator review diffs before they become permanent and keeps the commit graph clean.
165
+
166
+ ### Release PR mode
167
+
168
+ A target can declare that every release goes through a pull request (in its `CLAUDE.md`/`AGENTS.md`, or in the run's brief — mechanics in `git-wrapup`'s "Release PR mode"). The wrap-up + release phase then runs as **three sub-agents in sequence**, with an orchestrator check between each:
169
+
170
+ 1. **Wrap-up** — `git-wrapup`; halts with the stack committed on `release/<version>`, pushed, PR open.
171
+ 2. **Review** — `release-pr-review`; reads the PR range through `code-simplifier` plus a correctness review, lands fixes as fixup commits autosquashed into the stack, force-with-lease pushes the release branch, syncs the PR body, leaves one summary comment. This is the one role that both edits and commits — scoped to the release branch, never `main`, never a tag.
172
+ 3. **Release** — `release-and-publish`; `git merge --ff-only` onto `main` locally, tags `main`'s tip, pushes, publishes. Its brief must state that the review pass is finished — the skill halts without that line, and the orchestrator writes it only after confirming the review agent's report against the PR (`gh pr view --json state,headRefOid`, `git log --oneline main..HEAD`).
173
+
174
+ Straight-through mode drops the review agent: one sub-agent runs wrap-up and release back to back, opening and merging the PR in the same session. Without a declaration there is no PR, and the stack lands on `main` directly.
165
175
 
166
176
  ### Normalization
167
177
 
@@ -186,10 +196,11 @@ Sub-agent self-reports describe intent, not always reality. After every phase th
186
196
  - **Files** — `ls`, `git status`, `git diff --stat`
187
197
  - **Commits** — `git log --oneline -5`
188
198
  - **Tags** — `git tag --points-at HEAD`, `git ls-remote --tags origin`
199
+ - **Release PR** — `gh pr view <N> --json state,headRefOid` (`OPEN` with head == local HEAD between phases; `MERGED` after release), `git ls-remote --heads origin release/<VERSION>` empty after release
189
200
  - **GitHub** — `gh repo view --json visibility`, `gh release view v<VERSION>`, `gh issue list`, `gh issue view <N> --comments` to confirm the fix comment landed
190
201
  - **npm / registries** — `npm view <pkg>@<version>`, registry-specific checks
191
202
  - **Build state** — re-run `bun run devcheck` if the previous phase was supposed to land green
192
- - **Quality** — tag annotation is a headline digest covering every change (flat bullets — notable ones named, minor ones in one grouped bullet; no changelog section headers, deps ≤1 line, no gates line), subject omits the version number, no marketing adjectives, issue backlinks where applicable, changelog link as final line
203
+ - **Quality** — tag annotation is a headline digest covering every change (flat bullets — notable ones named, minor ones in one grouped bullet; no changelog section headers, deps ≤1 line, no gates line), subject omits the version number, no marketing adjectives, issue backlinks where applicable, changelog link as final line (plus ` · release PR #<N>` in release PR mode)
193
204
 
194
205
  If verification disagrees with the sub-agent's report, that's the signal to re-spawn with the actual state and the unmet goal in the prompt — not to trust the report. The goal hasn't changed; only the path needs to.
195
206
 
@@ -200,7 +211,7 @@ If verification disagrees with the sub-agent's report, that's the signal to re-s
200
211
  | Reads, analysis, file edits (working tree only) | Implicit — initial workflow approval covers these |
201
212
  | Local commits, annotated tags | Explicit at workflow start; durable through workflow end |
202
213
  | Push to remote, npm / registry publish, GH release create, Docker push | Explicit at workflow start; durable through workflow end |
203
- | Destructive ops (force push, tag delete, remote branch delete, etc.) | Always re-confirm, never assume |
214
+ | Destructive ops (force push, tag delete, remote branch delete, etc.) | Always re-confirm, never assume — two exceptions ride the release authorization: `release-pr-review`'s `--force-with-lease` on the run's own unmerged `release/<version>` branch, and `release-and-publish` deleting that branch once the PR reports `MERGED` |
204
215
 
205
216
  Pipeline authorization is durable through to completion. Once the user authorizes a workflow run, don't re-ask at each phase boundary — proceed automatically through gates that pass. Conditions that always require a fresh check-in: destructive ops on shared resources, external actions without sign-off, errors that need human judgment.
206
217