skilldrop-cli 0.16.29 → 0.16.31

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 (231) hide show
  1. package/bin/skilldrop.js +160 -33
  2. package/catalogue.json +10 -4
  3. package/model-routing.json +24 -0
  4. package/package.json +1 -1
  5. package/packs/ai-engineering/skills/mcp-pairing/SKILL.md +93 -0
  6. package/packs/ai-engineering/skills/mcp-pairing/evals/eval_queries.json +13 -0
  7. package/packs/ai-engineering/skills/mcp-pairing/evals/evals.json +76 -0
  8. package/packs/ai-engineering/skills/mcp-pairing/evals/files/2/mcp.json +32 -0
  9. package/packs/ai-engineering/skills/mcp-pairing/evals/files/4/.mcp.json +47 -0
  10. package/packs/ai-engineering/skills/mcp-pairing/evals/files/4/docs/pr-loop.md +12 -0
  11. package/packs/ai-engineering/skills/mcp-pairing/evals/files/5/docs/oncall-triage.md +18 -0
  12. package/packs/ai-engineering/skills/mcp-pairing/examples/build-loop-github-jira-datadog.md +69 -0
  13. package/packs/ai-engineering/skills/mcp-pairing/manifest.json +17 -0
  14. package/packs/ai-engineering/skills/mcp-pairing/reference.md +136 -0
  15. package/packs/ai-engineering/skills/mcp-pairing/templates/mcp-pairing.md +43 -0
  16. package/packs/dev-team/loops/build/LOOP.md +4 -2
  17. package/packs/dev-team/loops/build/loop.json +2 -2
  18. package/packs/dev-team/loops/release/LOOP.md +7 -5
  19. package/packs/dev-team/loops/release/loop.json +2 -2
  20. package/packs/dev-team/skills/codebase-tour/SKILL.md +115 -0
  21. package/packs/dev-team/skills/codebase-tour/evals/eval_queries.json +13 -0
  22. package/packs/dev-team/skills/codebase-tour/evals/evals.json +77 -0
  23. package/packs/dev-team/skills/codebase-tour/evals/files/1/.github/workflows/ci.yml +23 -0
  24. package/packs/dev-team/skills/codebase-tour/evals/files/1/README.md +23 -0
  25. package/packs/dev-team/skills/codebase-tour/evals/files/1/openapi.yaml +21 -0
  26. package/packs/dev-team/skills/codebase-tour/evals/files/1/package-lock.json +7 -0
  27. package/packs/dev-team/skills/codebase-tour/evals/files/1/package.json +29 -0
  28. package/packs/dev-team/skills/codebase-tour/evals/files/1/src/config.ts +20 -0
  29. package/packs/dev-team/skills/codebase-tour/evals/files/1/src/domain/shipments/filter.ts +24 -0
  30. package/packs/dev-team/skills/codebase-tour/evals/files/1/src/domain/shipments/repository.ts +15 -0
  31. package/packs/dev-team/skills/codebase-tour/evals/files/1/src/errors.ts +9 -0
  32. package/packs/dev-team/skills/codebase-tour/evals/files/1/src/generated/api-types.ts +9 -0
  33. package/packs/dev-team/skills/codebase-tour/evals/files/1/src/http/handlers/shipments.ts +38 -0
  34. package/packs/dev-team/skills/codebase-tour/evals/files/1/src/http/routes.ts +7 -0
  35. package/packs/dev-team/skills/codebase-tour/evals/files/1/src/jobs/export-shipments.ts +22 -0
  36. package/packs/dev-team/skills/codebase-tour/evals/files/1/src/server.ts +18 -0
  37. package/packs/dev-team/skills/codebase-tour/evals/files/1/tests/domain/filter.test.ts +16 -0
  38. package/packs/dev-team/skills/codebase-tour/evals/files/1/tests/http/shipments.test.ts +18 -0
  39. package/packs/dev-team/skills/codebase-tour/evals/files/1/tsconfig.json +10 -0
  40. package/packs/dev-team/skills/codebase-tour/evals/files/1/vitest.config.ts +5 -0
  41. package/packs/dev-team/skills/codebase-tour/evals/files/2/.github/workflows/ci.yml +16 -0
  42. package/packs/dev-team/skills/codebase-tour/evals/files/2/README.md +16 -0
  43. package/packs/dev-team/skills/codebase-tour/evals/files/2/noxfile.py +14 -0
  44. package/packs/dev-team/skills/codebase-tour/evals/files/2/pyproject.toml +22 -0
  45. package/packs/dev-team/skills/codebase-tour/evals/files/2/src/ledgerline/__init__.py +3 -0
  46. package/packs/dev-team/skills/codebase-tour/evals/files/2/src/ledgerline/_version.py +4 -0
  47. package/packs/dev-team/skills/codebase-tour/evals/files/2/src/ledgerline/cli.py +32 -0
  48. package/packs/dev-team/skills/codebase-tour/evals/files/2/src/ledgerline/detect.py +15 -0
  49. package/packs/dev-team/skills/codebase-tour/evals/files/2/src/ledgerline/errors.py +2 -0
  50. package/packs/dev-team/skills/codebase-tour/evals/files/2/src/ledgerline/money.py +14 -0
  51. package/packs/dev-team/skills/codebase-tour/evals/files/2/src/ledgerline/parsers/__init__.py +8 -0
  52. package/packs/dev-team/skills/codebase-tour/evals/files/2/src/ledgerline/parsers/acme.py +18 -0
  53. package/packs/dev-team/skills/codebase-tour/evals/files/2/src/ledgerline/parsers/base.py +24 -0
  54. package/packs/dev-team/skills/codebase-tour/evals/files/2/src/ledgerline/parsers/harbor.py +13 -0
  55. package/packs/dev-team/skills/codebase-tour/evals/files/2/tests/fixtures/acme_sample.csv +3 -0
  56. package/packs/dev-team/skills/codebase-tour/evals/files/2/tests/test_acme.py +23 -0
  57. package/packs/dev-team/skills/codebase-tour/evals/files/2/tests/test_detect.py +13 -0
  58. package/packs/dev-team/skills/codebase-tour/evals/files/3/.github/workflows/ci.yml +28 -0
  59. package/packs/dev-team/skills/codebase-tour/evals/files/3/Makefile +16 -0
  60. package/packs/dev-team/skills/codebase-tour/evals/files/3/README.md +16 -0
  61. package/packs/dev-team/skills/codebase-tour/evals/files/3/cmd/tidepool/main.go +24 -0
  62. package/packs/dev-team/skills/codebase-tour/evals/files/3/go.mod +5 -0
  63. package/packs/dev-team/skills/codebase-tour/evals/files/3/internal/config/config.go +38 -0
  64. package/packs/dev-team/skills/codebase-tour/evals/files/3/internal/dispatch/dispatcher.go +52 -0
  65. package/packs/dev-team/skills/codebase-tour/evals/files/3/internal/dispatch/dispatcher_test.go +42 -0
  66. package/packs/dev-team/skills/codebase-tour/evals/files/3/internal/dispatch/sender.go +32 -0
  67. package/packs/dev-team/skills/codebase-tour/evals/files/3/internal/replay/replay.go +31 -0
  68. package/packs/dev-team/skills/codebase-tour/evals/files/3/internal/store/db.go +35 -0
  69. package/packs/dev-team/skills/codebase-tour/evals/files/3/internal/store/query.sql +11 -0
  70. package/packs/dev-team/skills/codebase-tour/evals/files/3/internal/store/query.sql.go +30 -0
  71. package/packs/dev-team/skills/codebase-tour/evals/files/3/internal/store/schema.sql +6 -0
  72. package/packs/dev-team/skills/codebase-tour/evals/files/3/internal/store/store_integration_test.go +21 -0
  73. package/packs/dev-team/skills/codebase-tour/evals/files/3/sqlc.yaml +9 -0
  74. package/packs/dev-team/skills/codebase-tour/evals/files/4/.env.example +4 -0
  75. package/packs/dev-team/skills/codebase-tour/evals/files/4/.github/workflows/ci.yml +16 -0
  76. package/packs/dev-team/skills/codebase-tour/evals/files/4/Makefile +16 -0
  77. package/packs/dev-team/skills/codebase-tour/evals/files/4/README.md +23 -0
  78. package/packs/dev-team/skills/codebase-tour/evals/files/4/data/rates.csv +4 -0
  79. package/packs/dev-team/skills/codebase-tour/evals/files/4/pyproject.toml +19 -0
  80. package/packs/dev-team/skills/codebase-tour/evals/files/4/scripts/gen_rates.py +29 -0
  81. package/packs/dev-team/skills/codebase-tour/evals/files/4/src/shipcalc/__init__.py +0 -0
  82. package/packs/dev-team/skills/codebase-tour/evals/files/4/src/shipcalc/api/__init__.py +0 -0
  83. package/packs/dev-team/skills/codebase-tour/evals/files/4/src/shipcalc/api/preview.py +14 -0
  84. package/packs/dev-team/skills/codebase-tour/evals/files/4/src/shipcalc/api/routes.py +36 -0
  85. package/packs/dev-team/skills/codebase-tour/evals/files/4/src/shipcalc/config.py +15 -0
  86. package/packs/dev-team/skills/codebase-tour/evals/files/4/src/shipcalc/discounts.py +8 -0
  87. package/packs/dev-team/skills/codebase-tour/evals/files/4/src/shipcalc/generated/__init__.py +0 -0
  88. package/packs/dev-team/skills/codebase-tour/evals/files/4/src/shipcalc/generated/rates_table.py +8 -0
  89. package/packs/dev-team/skills/codebase-tour/evals/files/4/src/shipcalc/main.py +8 -0
  90. package/packs/dev-team/skills/codebase-tour/evals/files/4/src/shipcalc/shipping.py +18 -0
  91. package/packs/dev-team/skills/codebase-tour/evals/files/4/src/shipcalc/tax.py +7 -0
  92. package/packs/dev-team/skills/codebase-tour/evals/files/4/tests/conftest.py +3 -0
  93. package/packs/dev-team/skills/codebase-tour/evals/files/4/tests/unit/test_discounts.py +11 -0
  94. package/packs/dev-team/skills/codebase-tour/evals/files/4/tests/unit/test_tax.py +13 -0
  95. package/packs/dev-team/skills/codebase-tour/evals/files/5/.env.example +3 -0
  96. package/packs/dev-team/skills/codebase-tour/evals/files/5/.github/workflows/ci.yml +13 -0
  97. package/packs/dev-team/skills/codebase-tour/evals/files/5/README.md +18 -0
  98. package/packs/dev-team/skills/codebase-tour/evals/files/5/package.json +13 -0
  99. package/packs/dev-team/skills/codebase-tour/evals/files/5/packages/api/openapi.json +30 -0
  100. package/packs/dev-team/skills/codebase-tour/evals/files/5/packages/api/package.json +15 -0
  101. package/packs/dev-team/skills/codebase-tour/evals/files/5/packages/api/src/db.js +19 -0
  102. package/packs/dev-team/skills/codebase-tour/evals/files/5/packages/api/src/flags.js +8 -0
  103. package/packs/dev-team/skills/codebase-tour/evals/files/5/packages/api/src/generated/schema.js +32 -0
  104. package/packs/dev-team/skills/codebase-tour/evals/files/5/packages/api/src/middleware/validate.js +20 -0
  105. package/packs/dev-team/skills/codebase-tour/evals/files/5/packages/api/src/routes/bookings.js +15 -0
  106. package/packs/dev-team/skills/codebase-tour/evals/files/5/packages/api/src/server.js +15 -0
  107. package/packs/dev-team/skills/codebase-tour/evals/files/5/packages/api/src/services/bookings.js +23 -0
  108. package/packs/dev-team/skills/codebase-tour/evals/files/5/packages/api/src/services/pricing.js +9 -0
  109. package/packs/dev-team/skills/codebase-tour/evals/files/5/packages/api/test/pricing.test.js +15 -0
  110. package/packs/dev-team/skills/codebase-tour/evals/files/5/packages/web/package.json +9 -0
  111. package/packs/dev-team/skills/codebase-tour/evals/files/5/packages/web/src/bookingForm.js +11 -0
  112. package/packs/dev-team/skills/codebase-tour/evals/files/5/packages/web/src/format.js +7 -0
  113. package/packs/dev-team/skills/codebase-tour/evals/files/5/packages/web/test/format.test.js +12 -0
  114. package/packs/dev-team/skills/codebase-tour/evals/files/5/scripts/gen-schema.mjs +27 -0
  115. package/packs/dev-team/skills/codebase-tour/examples/refund-window.md +158 -0
  116. package/packs/dev-team/skills/codebase-tour/manifest.json +47 -0
  117. package/packs/dev-team/skills/codebase-tour/scripts/repo_facts.py +403 -0
  118. package/packs/dev-team/skills/codebase-tour/templates/codebase-tour.md +85 -0
  119. package/packs/dev-team/skills/dependency-upgrade-review/SKILL.md +101 -0
  120. package/packs/dev-team/skills/dependency-upgrade-review/evals/eval_queries.json +11 -0
  121. package/packs/dev-team/skills/dependency-upgrade-review/evals/evals.json +77 -0
  122. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/1/.github/workflows/ci.yml +20 -0
  123. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/1/pr.diff +87 -0
  124. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/1/quillmark-CHANGELOG.md +24 -0
  125. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/1/src/render/comment.js +6 -0
  126. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/1/src/render/note.js +8 -0
  127. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/1/test/note.test.js +12 -0
  128. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/2/.github/workflows/ci.yml +23 -0
  129. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/2/.github/workflows/release.yml +23 -0
  130. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/2/pr.diff +32 -0
  131. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/2/release-notes.md +20 -0
  132. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/4/.github/workflows/ci.yml +18 -0
  133. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/4/Dockerfile +8 -0
  134. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/4/PR.md +36 -0
  135. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/4/pr.diff +47 -0
  136. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/4/src/ingest/client.py +24 -0
  137. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/4/src/ingest/reader.py +32 -0
  138. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/4/tests/test_reader.py +15 -0
  139. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/4/upstream/quillstream-CHANGELOG.md +37 -0
  140. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/5/.github/workflows/ci.yml +22 -0
  141. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/5/.github/workflows/release.yml +25 -0
  142. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/5/.nvmrc +1 -0
  143. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/5/PR-412.md +16 -0
  144. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/5/PR-413.md +15 -0
  145. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/5/package.json +21 -0
  146. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/5/pr-412.diff +26 -0
  147. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/5/pr-413.diff +84 -0
  148. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/5/scripts/release.mjs +18 -0
  149. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/5/upstream/release-notary-CHANGELOG.md +16 -0
  150. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/5/upstream/setup-toolchain-RELEASES.md +22 -0
  151. package/packs/dev-team/skills/dependency-upgrade-review/examples/inputs/ledgerline.diff +38 -0
  152. package/packs/dev-team/skills/dependency-upgrade-review/examples/ledgerline-tidyparse.md +91 -0
  153. package/packs/dev-team/skills/dependency-upgrade-review/manifest.json +50 -0
  154. package/packs/dev-team/skills/dependency-upgrade-review/reference.md +99 -0
  155. package/packs/dev-team/skills/dependency-upgrade-review/scripts/upgrade_diff.py +718 -0
  156. package/packs/dev-team/skills/refactor-plan/SKILL.md +168 -0
  157. package/packs/dev-team/skills/refactor-plan/evals/eval_queries.json +46 -0
  158. package/packs/dev-team/skills/refactor-plan/evals/evals.json +106 -0
  159. package/packs/dev-team/skills/refactor-plan/evals/files/1/debt-register-row.md +5 -0
  160. package/packs/dev-team/skills/refactor-plan/evals/files/1/in-flight-prs.md +7 -0
  161. package/packs/dev-team/skills/refactor-plan/evals/files/1/order_service.py +74 -0
  162. package/packs/dev-team/skills/refactor-plan/evals/files/1/test-coverage.txt +14 -0
  163. package/packs/dev-team/skills/refactor-plan/evals/files/3/imports.txt +22 -0
  164. package/packs/dev-team/skills/refactor-plan/evals/files/3/register.md +3 -0
  165. package/packs/dev-team/skills/refactor-plan/evals/files/4/docs/open-prs.md +6 -0
  166. package/packs/dev-team/skills/refactor-plan/evals/files/4/docs/tech-debt-register.md +7 -0
  167. package/packs/dev-team/skills/refactor-plan/evals/files/4/reports/monthly_revenue.py +44 -0
  168. package/packs/dev-team/skills/refactor-plan/evals/files/4/src/billing/invoice.py +233 -0
  169. package/packs/dev-team/skills/refactor-plan/evals/files/4/src/partners/statement.py +29 -0
  170. package/packs/dev-team/skills/refactor-plan/evals/files/4/tests/billing/test_payments.py +13 -0
  171. package/packs/dev-team/skills/refactor-plan/evals/files/5/ci-notes.md +5 -0
  172. package/packs/dev-team/skills/refactor-plan/evals/files/5/open-prs.txt +4 -0
  173. package/packs/dev-team/skills/refactor-plan/evals/files/5/scripts/nightly-digest.js +36 -0
  174. package/packs/dev-team/skills/refactor-plan/evals/files/5/src/auth/passwordReset.ts +16 -0
  175. package/packs/dev-team/skills/refactor-plan/evals/files/5/src/notify/index.ts +2 -0
  176. package/packs/dev-team/skills/refactor-plan/evals/files/5/src/notify/legacyMailer.ts +149 -0
  177. package/packs/dev-team/skills/refactor-plan/evals/files/5/src/orders/onOrderShipped.ts +15 -0
  178. package/packs/dev-team/skills/refactor-plan/evals/files/5/tests/notify/legacyMailer.test.ts +7 -0
  179. package/packs/dev-team/skills/refactor-plan/evals/files/6/billing-service/src/main/kotlin/billing/DunningJob.kt +29 -0
  180. package/packs/dev-team/skills/refactor-plan/evals/files/6/billing-service/src/main/kotlin/billing/InvoiceService.kt +157 -0
  181. package/packs/dev-team/skills/refactor-plan/evals/files/6/billing-service/src/main/kotlin/billing/pdf/InvoicePdfRenderer.kt +36 -0
  182. package/packs/dev-team/skills/refactor-plan/evals/files/6/billing-service/src/test/kotlin/billing/InvoiceServiceTest.kt +40 -0
  183. package/packs/dev-team/skills/refactor-plan/evals/files/6/docs/adr-007-persistence.md +26 -0
  184. package/packs/dev-team/skills/refactor-plan/evals/files/6/docs/open-prs.md +9 -0
  185. package/packs/dev-team/skills/refactor-plan/evals/files/7/notes/open-work.md +19 -0
  186. package/packs/dev-team/skills/refactor-plan/evals/files/7/web/src/components/CheckoutPanel.tsx +207 -0
  187. package/packs/dev-team/skills/refactor-plan/evals/files/7/web/src/components/MiniCart.tsx +33 -0
  188. package/packs/dev-team/skills/refactor-plan/evals/files/7/web/src/components/__tests__/CheckoutPanel.test.tsx +13 -0
  189. package/packs/dev-team/skills/refactor-plan/evals/files/7/web/src/pages/OrderReview.tsx +24 -0
  190. package/packs/dev-team/skills/refactor-plan/examples/search-ranker.md +104 -0
  191. package/packs/dev-team/skills/refactor-plan/manifest.json +36 -0
  192. package/packs/dev-team/skills/refactor-plan/reference.md +149 -0
  193. package/packs/dev-team/skills/refactor-plan/templates/refactor-plan.md +82 -0
  194. package/packs/dev-team/skills/rollout-plan/SKILL.md +157 -0
  195. package/packs/dev-team/skills/rollout-plan/evals/eval_queries.json +12 -0
  196. package/packs/dev-team/skills/rollout-plan/evals/evals.json +109 -0
  197. package/packs/dev-team/skills/rollout-plan/evals/files/2/metrics-snapshot.txt +11 -0
  198. package/packs/dev-team/skills/rollout-plan/evals/files/2/pr-description.md +28 -0
  199. package/packs/dev-team/skills/rollout-plan/evals/files/4/PR_DESCRIPTION.md +24 -0
  200. package/packs/dev-team/skills/rollout-plan/evals/files/4/metrics_snapshot.txt +7 -0
  201. package/packs/dev-team/skills/rollout-plan/evals/files/5/docs/tax-engine-v2.md +24 -0
  202. package/packs/dev-team/skills/rollout-plan/evals/files/6/flags/smart_reminders_v2.yaml +14 -0
  203. package/packs/dev-team/skills/rollout-plan/evals/files/6/notes/push-pipeline.md +10 -0
  204. package/packs/dev-team/skills/rollout-plan/evals/files/7/docs/ratelimiter-v2.md +21 -0
  205. package/packs/dev-team/skills/rollout-plan/evals/files/7/ops/traffic-summary.txt +9 -0
  206. package/packs/dev-team/skills/rollout-plan/examples/sync-engine-workspaces.md +102 -0
  207. package/packs/dev-team/skills/rollout-plan/manifest.json +35 -0
  208. package/packs/dev-team/skills/rollout-plan/reference.md +100 -0
  209. package/packs/dev-team/skills/rollout-plan/scripts/canary_math.py +261 -0
  210. package/packs/dev-team/skills/rollout-plan/templates/rollout-plan.md +50 -0
  211. package/packs/product-manager/loops/discover/LOOP.md +5 -4
  212. package/packs/product-manager/loops/discover/loop.json +2 -2
  213. package/packs/product-manager/skills/feedback-synthesis/SKILL.md +182 -0
  214. package/packs/product-manager/skills/feedback-synthesis/evals/eval_queries.json +46 -0
  215. package/packs/product-manager/skills/feedback-synthesis/evals/evals.json +76 -0
  216. package/packs/product-manager/skills/feedback-synthesis/evals/files/1/interview-notes.md +47 -0
  217. package/packs/product-manager/skills/feedback-synthesis/evals/files/1/nps-verbatims.csv +13 -0
  218. package/packs/product-manager/skills/feedback-synthesis/evals/files/1/support-tickets.csv +36 -0
  219. package/packs/product-manager/skills/feedback-synthesis/evals/files/3/churn-exit-survey.csv +11 -0
  220. package/packs/product-manager/skills/feedback-synthesis/evals/files/3/sales-call-notes.md +55 -0
  221. package/packs/product-manager/skills/feedback-synthesis/evals/files/4/csat_comments_q3.csv +17 -0
  222. package/packs/product-manager/skills/feedback-synthesis/evals/files/4/support_tickets_q3.csv +49 -0
  223. package/packs/product-manager/skills/feedback-synthesis/evals/files/5/app_reviews_last_60_days.csv +32 -0
  224. package/packs/product-manager/skills/feedback-synthesis/evals/files/5/interview_notes_round1.md +48 -0
  225. package/packs/product-manager/skills/feedback-synthesis/examples/booking-app-feedback.md +139 -0
  226. package/packs/product-manager/skills/feedback-synthesis/examples/inputs/booking-app-coded.csv +17 -0
  227. package/packs/product-manager/skills/feedback-synthesis/manifest.json +53 -0
  228. package/packs/product-manager/skills/feedback-synthesis/reference.md +89 -0
  229. package/packs/product-manager/skills/feedback-synthesis/scripts/theme_counts.py +355 -0
  230. package/packs/product-manager/skills/feedback-synthesis/templates/coding-sheet.csv +6 -0
  231. package/packs/product-manager/skills/feedback-synthesis/templates/synthesis.md +42 -0
@@ -0,0 +1,261 @@
1
+ #!/usr/bin/env python3
2
+ """How long each exposure step of a canary or flag rollout must run before its
3
+ numbers can detect the regression you care about.
4
+
5
+ Uses the normal approximation to the two-proportion z-test. The flag-on cohort
6
+ (treatment) is compared with a concurrent flag-off cohort (control). By default
7
+ the control is everyone not exposed (100% - exposure); pass --control-pct to fix
8
+ it at a holdback size instead.
9
+
10
+ For each step it prints the units (requests or users) each arm needs and the
11
+ hours that takes at the given traffic, flags steps that cannot reach that in
12
+ --max-hours, says the smallest regression the step *can* detect in that window,
13
+ and suggests the smallest exposure that reaches sufficiency in the window.
14
+
15
+ Rates and regressions accept a fraction (0.008), a percent ("0.8%") or, for an
16
+ absolute regression, percentage points ("0.3pp").
17
+
18
+ Usage:
19
+ python3 canary_math.py --baseline 0.8% --detect-abs 0.3pp --traffic 9000
20
+ python3 canary_math.py --baseline 3.1% --detect-rel 10% --direction down \\
21
+ --traffic 9000 --steps 1,5,25,50 --unit users
22
+ python3 canary_math.py ... --json
23
+
24
+ Exit codes: 0 printed, 2 bad input.
25
+ """
26
+
27
+ import argparse
28
+ import json
29
+ import math
30
+ import sys
31
+ from statistics import NormalDist
32
+
33
+ Z = NormalDist()
34
+
35
+
36
+ def fail(msg):
37
+ sys.stderr.write("error: " + msg + "\n")
38
+ sys.exit(2)
39
+
40
+
41
+ def parse_rate(text, name, allow_pp=False):
42
+ s = str(text).strip().lower().replace(" ", "")
43
+ try:
44
+ if s.endswith("pp"):
45
+ if not allow_pp:
46
+ fail(name + ": 'pp' only applies to --detect-abs")
47
+ return float(s[:-2]) / 100.0
48
+ if s.endswith("%"):
49
+ return float(s[:-1]) / 100.0
50
+ v = float(s)
51
+ except ValueError:
52
+ fail(name + ": not a number: " + str(text))
53
+ return v
54
+
55
+
56
+ def fmt_pct(x, digits=2):
57
+ return ("{:." + str(digits) + "f}").format(x * 100).rstrip("0").rstrip(".") + "%"
58
+
59
+
60
+ def fmt_pp(x):
61
+ return "{:.3f}".format(x * 100).rstrip("0").rstrip(".") + " pp"
62
+
63
+
64
+ def fmt_hours(h):
65
+ if h is None:
66
+ return "n/a"
67
+ if h < 1:
68
+ return "{:.0f} min".format(max(1, math.ceil(h * 60)))
69
+ if h < 48:
70
+ return "{:.1f} h".format(h)
71
+ return "{:.1f} h ({:.1f} d)".format(h, h / 24)
72
+
73
+
74
+ def z_values(alpha, power, one_sided):
75
+ za = Z.inv_cdf(1 - alpha) if one_sided else Z.inv_cdf(1 - alpha / 2)
76
+ zb = Z.inv_cdf(power)
77
+ return za, zb
78
+
79
+
80
+ def equal_arm_n(p1, p2, za, zb):
81
+ """Units per arm for two equal arms."""
82
+ pbar = (p1 + p2) / 2
83
+ num = za * math.sqrt(2 * pbar * (1 - pbar)) + zb * math.sqrt(p1 * (1 - p1) + p2 * (1 - p2))
84
+ return math.ceil((num / (p2 - p1)) ** 2)
85
+
86
+
87
+ def hours_needed(p1, p2, e, c, traffic, za, zb):
88
+ """Hours until a treatment arm of share e and control of share c is powered.
89
+
90
+ n1 = traffic*e*t (treatment, rate p2), n2 = traffic*c*t (control, rate p1).
91
+ Both arms scale with t, so the power equation solves in closed form.
92
+ """
93
+ if e <= 0 or c <= 0:
94
+ return None
95
+ pbar = (e * p2 + c * p1) / (e + c)
96
+ a = za * math.sqrt(pbar * (1 - pbar) * (1 / e + 1 / c))
97
+ b = zb * math.sqrt(p2 * (1 - p2) / e + p1 * (1 - p1) / c)
98
+ return ((a + b) / abs(p2 - p1)) ** 2 / traffic
99
+
100
+
101
+ def detectable_in(p1, direction, e, c, traffic, za, zb, hours):
102
+ """Smallest absolute regression detectable in `hours` (bisection)."""
103
+ if e <= 0 or c <= 0:
104
+ return None
105
+ hi = (1 - p1) if direction == "up" else p1
106
+ hi *= 0.999
107
+ lo = 1e-9
108
+ if hours_needed(p1, p1 + hi if direction == "up" else p1 - hi, e, c, traffic, za, zb) > hours:
109
+ return None
110
+ for _ in range(80):
111
+ mid = (lo + hi) / 2
112
+ p2 = p1 + mid if direction == "up" else p1 - mid
113
+ if hours_needed(p1, p2, e, c, traffic, za, zb) <= hours:
114
+ hi = mid
115
+ else:
116
+ lo = mid
117
+ return hi
118
+
119
+
120
+ def smallest_sufficient_exposure(p1, p2, traffic, za, zb, max_hours, control_pct):
121
+ """Smallest exposure (0.1% steps, up to 50%) that is powered within max_hours."""
122
+ for tenth in range(1, 501):
123
+ e = tenth / 1000.0
124
+ c = control_pct if control_pct is not None else 1 - e
125
+ if e + c > 1 + 1e-9:
126
+ break
127
+ h = hours_needed(p1, p2, e, c, traffic, za, zb)
128
+ if h is not None and h <= max_hours:
129
+ return e, h
130
+ return None, None
131
+
132
+
133
+ def main():
134
+ ap = argparse.ArgumentParser(description="Hours each canary/flag exposure step must run to detect a regression.")
135
+ ap.add_argument("--baseline", required=True, help="baseline rate of the metric in control, e.g. 0.8%% or 0.008")
136
+ g = ap.add_mutually_exclusive_group(required=True)
137
+ g.add_argument("--detect-abs", help="smallest regression worth catching, absolute: 0.3pp or 0.003")
138
+ g.add_argument("--detect-rel", help="smallest regression worth catching, relative to baseline: 25%% or 0.25")
139
+ ap.add_argument("--traffic", required=True, type=float, help="units per hour across all users (requests or users)")
140
+ ap.add_argument("--unit", default="requests", help="what --traffic counts; use the unit the flag randomises on (default: requests)")
141
+ ap.add_argument("--steps", default="1,5,25,50,100", help="exposure percentages, comma-separated (default: 1,5,25,50,100)")
142
+ ap.add_argument("--control-pct", help="fixed control (holdback) size in percent; default: everyone not exposed")
143
+ ap.add_argument("--direction", choices=["up", "down"], default="up",
144
+ help="up = a rise is the regression (error rate, default); down = a fall is (conversion)")
145
+ ap.add_argument("--alpha", type=float, default=0.05, help="false-alarm rate (default 0.05)")
146
+ ap.add_argument("--power", type=float, default=0.8, help="chance of catching a real regression (default 0.8)")
147
+ ap.add_argument("--one-sided", action="store_true", help="one-sided test (only the regression direction); default two-sided")
148
+ ap.add_argument("--max-hours", type=float, default=72.0, help="longest sensible step (default 72)")
149
+ ap.add_argument("--json", action="store_true", help="print JSON instead of a markdown table")
150
+ args = ap.parse_args()
151
+
152
+ p1 = parse_rate(args.baseline, "--baseline")
153
+ if not 0 < p1 < 1:
154
+ fail("--baseline must be between 0 and 100%")
155
+ if args.detect_abs is not None:
156
+ d = parse_rate(args.detect_abs, "--detect-abs", allow_pp=True)
157
+ else:
158
+ d = parse_rate(args.detect_rel, "--detect-rel") * p1
159
+ if d <= 0:
160
+ fail("the regression to detect must be greater than zero")
161
+ p2 = p1 + d if args.direction == "up" else p1 - d
162
+ if not 0 < p2 < 1:
163
+ fail("baseline plus the regression falls outside 0-100%")
164
+ if args.traffic <= 0:
165
+ fail("--traffic must be greater than zero")
166
+ if not 0 < args.alpha < 1 or not 0 < args.power < 1:
167
+ fail("--alpha and --power must be between 0 and 1")
168
+ if args.max_hours <= 0:
169
+ fail("--max-hours must be greater than zero")
170
+ control = None
171
+ if args.control_pct is not None:
172
+ control = parse_rate(args.control_pct.rstrip("%") + "%", "--control-pct")
173
+ if not 0 < control < 1:
174
+ fail("--control-pct must be between 0 and 100")
175
+ try:
176
+ steps = [float(s.strip().rstrip("%")) / 100.0 for s in args.steps.split(",") if s.strip()]
177
+ except ValueError:
178
+ fail("--steps must be comma-separated percentages, e.g. 1,5,25,50,100")
179
+ if not steps or any(not 0 < s <= 1 for s in steps):
180
+ fail("--steps must be percentages between 0 and 100")
181
+
182
+ za, zb = z_values(args.alpha, args.power, args.one_sided)
183
+ n_equal = equal_arm_n(p1, p2, za, zb)
184
+
185
+ rows = []
186
+ for e in steps:
187
+ c = control if control is not None else 1 - e
188
+ row = {"exposure_pct": round(e * 100, 4), "control_pct": round(c * 100, 4)}
189
+ if e + c > 1 + 1e-9:
190
+ row.update(status="invalid", note="exposure plus control exceeds 100%")
191
+ elif c <= 1e-9:
192
+ row.update(status="no-control", hours=None,
193
+ note="no concurrent control at 100%: promote on the previous step's comparison, "
194
+ "or keep a holdback (--control-pct) to keep comparing")
195
+ else:
196
+ h = hours_needed(p1, p2, e, c, args.traffic, za, zb)
197
+ row["hours"] = round(h, 3)
198
+ row["treatment_units"] = math.ceil(h * args.traffic * e)
199
+ row["control_units"] = math.ceil(h * args.traffic * c)
200
+ if h <= args.max_hours:
201
+ row["status"] = "sufficient"
202
+ else:
203
+ row["status"] = "insufficient"
204
+ mde = detectable_in(p1, args.direction, e, c, args.traffic, za, zb, args.max_hours)
205
+ row["detectable_in_max_hours_abs"] = None if mde is None else round(mde, 6)
206
+ rows.append(row)
207
+
208
+ best_e, best_h = smallest_sufficient_exposure(p1, p2, args.traffic, za, zb, args.max_hours, control)
209
+
210
+ result = {
211
+ "baseline": p1, "regression_abs": round(d, 6), "regressed_rate": round(p2, 6),
212
+ "direction": args.direction, "alpha": args.alpha, "power": args.power,
213
+ "test": "one-sided" if args.one_sided else "two-sided",
214
+ "traffic_per_hour": args.traffic, "unit": args.unit, "max_hours": args.max_hours,
215
+ "equal_arm_units_per_arm": n_equal, "steps": rows,
216
+ "smallest_sufficient_exposure_pct": None if best_e is None else round(best_e * 100, 1),
217
+ "smallest_sufficient_exposure_hours": None if best_h is None else round(best_h, 2),
218
+ }
219
+ if args.json:
220
+ print(json.dumps(result, indent=2))
221
+ return
222
+
223
+ u = args.unit
224
+ print("# Canary sufficiency")
225
+ print()
226
+ print("- Metric: baseline {} -> regression to catch {} ({} rate {}), {} test, alpha {}, power {}".format(
227
+ fmt_pct(p1, 3), fmt_pp(d), "rising to" if args.direction == "up" else "falling to",
228
+ fmt_pct(p2, 3), result["test"], args.alpha, args.power))
229
+ print("- Traffic: {:,.0f} {} per hour; control = {}".format(
230
+ args.traffic, u, "fixed {} holdback".format(fmt_pct(control)) if control is not None else "everyone not exposed"))
231
+ print("- Equal arms would need {:,} {} per arm".format(n_equal, u))
232
+ print()
233
+ print("| Exposure | Control | Treatment {u} needed | Control {u} needed | Minimum time | Status |".format(u=u))
234
+ print("|---|---|---|---|---|---|")
235
+ for r in rows:
236
+ exp = fmt_pct(r["exposure_pct"] / 100)
237
+ ctl = fmt_pct(r["control_pct"] / 100)
238
+ if r["status"] in ("no-control", "invalid"):
239
+ print("| {} | {} | - | - | n/a | {} |".format(exp, ctl, r["note"]))
240
+ continue
241
+ status = "ok" if r["status"] == "sufficient" else "INSUFFICIENT: over {:g} h".format(args.max_hours)
242
+ if r["status"] == "insufficient":
243
+ m = r.get("detectable_in_max_hours_abs")
244
+ status += "; in {:g} h it can only catch {}".format(
245
+ args.max_hours, "nothing measurable" if m is None else ">= " + fmt_pp(m))
246
+ print("| {} | {} | {:,} | {:,} | {} | {} |".format(
247
+ exp, ctl, r["treatment_units"], r["control_units"], fmt_hours(r["hours"]), status))
248
+ print()
249
+ if best_e is None:
250
+ print("No exposure up to 50% reaches sufficiency within {:g} h. Catch a larger regression, "
251
+ "run longer, or pick a metric with a higher baseline rate.".format(args.max_hours))
252
+ else:
253
+ print("Smallest exposure that reaches sufficiency within {:g} h: {} (about {}).".format(
254
+ args.max_hours, fmt_pct(best_e, 1), fmt_hours(best_h)))
255
+ print()
256
+ print("Minimum time is statistics only. A step also runs through at least one daily peak "
257
+ "and any delayed effect (nightly jobs, next-day emails) before it is promoted.")
258
+
259
+
260
+ if __name__ == "__main__":
261
+ main()
@@ -0,0 +1,50 @@
1
+ # Rollout plan — {change name}
2
+
3
+ **{Two lines: the answer to the user's question, the input fact that settles it, and the shape of the plan.}**
4
+ ✅ *"No: at 2,000 users an hour a 1% step needs about 20 days to see a 0.3 pp error rise, so Tuesday-to-Friday proves nothing. Run internal for a day, 10% for 3 days, 50% for a day, and reach 100% the Wednesday after."*
5
+
6
+ **Binding constraint:** {the fact that paces the plan: a partner cap, a billing or batch cycle, a deadline, the segments the flag can target, a shadow run, or volume. "None: traffic is high, the default ladder applies."}
7
+ **Flag:** `{key}`, evaluated {server / client / edge}, targets by {user / account / tenant / %} · **Assumptions:** {one line, each tagged `[assumption]`}
8
+
9
+ ## Ladder
10
+
11
+ | Step | Dates | Who is in it, and why | Judged on | Promote when |
12
+ |---|---|---|---|---|
13
+ | 0 | {Mon 7 Sep – Tue 8 Sep} | {internal / test accounts: crashes at no customer cost} | crashes only | {no abort for 24 h} |
14
+ | 1 | {…} | {named cohort or `hash(salt + id) mod 100 < 10`; why these; who is held back} | {units or cycle this step sees, in one phrase} | {every threshold below holds at the step's end} |
15
+ | 2 | {…} | | | |
16
+ | {n} | {date} | everyone | {holdback or the last comparison} | — |
17
+
18
+ {One sentence of arithmetic, with its inputs: what the smallest useful step is and why.}
19
+ {Deadline line when there is one: ✅ *"100% on 14 Sep, 9 days before the deadline; one failed cycle misses it."*}
20
+
21
+ ## Thresholds
22
+
23
+ All compare the flag-on cohort with the flag-off cohort over the same window. Counts, not
24
+ percentages, where a step sees fewer than about 100 of the unit.
25
+
26
+ | Metric | Baseline | Promote if | Abort (any time, {automatic / manual}) |
27
+ |---|---|---|---|
28
+ | {decision metric} | {from input} | {≤ control + 0.3 pp / zero unexplained} | {> 3× control for 5 min → flag off} |
29
+ | {guardrail} | | | |
30
+
31
+ - **Hold:** {sample short or metric between threshold and abort → extend once by {time or cycle}, then roll back}.
32
+ - **Watcher:** {role} · **Sign-off before each promotion:** {role, if one is needed} · **Override:** {one role, reason written down}.
33
+
34
+ ## What turning the flag off does not undo
35
+
36
+ Flag off: {who}, takes effect in {seconds / cache TTL / next app launch}. Only rows this change touches:
37
+
38
+ | What | Undone? | Handling |
39
+ |---|---|---|
40
+ | {messages sent / money moved / records written in a new format / third-party records} | no | {correction or reissue path, reversal procedure, what keeps reading the new format} |
41
+
42
+ **Point of no return:** {the step and why / none}.
43
+
44
+ ## Flag removal
45
+
46
+ **Owner:** {name from the input, else role} · **Date:** {date} · **Gate:** {100% for 14 days or one full cycle, no rollback} · **Removes:** `{key}`, the old path, its config and dashboards · **Keeps:** {any reader the old records need}.
47
+
48
+ ## To confirm
49
+
50
+ - {at most 4 facts, each one that would change a step}
@@ -13,7 +13,7 @@ script: no check can tell you that you solved the wrong problem well.
13
13
 
14
14
  | # | Stage | Type | Skills | Gate |
15
15
  |---|---|---|---|---|
16
- | 1 | `gather` | generate | [`requirements-interview`](../../skills/requirements-interview/SKILL.md), [`user-journey-map`](../../skills/user-journey-map/SKILL.md), [`strategy-analysis`](../../skills/strategy-analysis/SKILL.md) | — |
16
+ | 1 | `gather` | generate | [`requirements-interview`](../../skills/requirements-interview/SKILL.md), [`user-journey-map`](../../skills/user-journey-map/SKILL.md), [`strategy-analysis`](../../skills/strategy-analysis/SKILL.md), [`feedback-synthesis`](../../skills/feedback-synthesis/SKILL.md) | — |
17
17
  | 2 | `structure` | generate | [`brief-intake`](../../../core/skills/brief-intake/SKILL.md) | — |
18
18
  | 3 | `specify` | generate | [`prd-draft`](../../skills/prd-draft/SKILL.md) | — |
19
19
  | 4 | `ratify` | gate | [`doc-critique`](../../../core/skills/doc-critique/SKILL.md) | **G0** human |
@@ -22,8 +22,9 @@ script: no check can tell you that you solved the wrong problem well.
22
22
 
23
23
  1. **Gather from whichever direction the signal arrives.** A stakeholder conversation →
24
24
  `requirements-interview`. An existing experience with known friction → `user-journey-map`.
25
- An open strategic question → `strategy-analysis`. Run one; running all three on the same
26
- input produces three views of the same thing and no more information.
25
+ An open strategic question → `strategy-analysis`. A pile of tickets, survey verbatims or
26
+ interview notes already collected → `feedback-synthesis`. Run one; running several on the
27
+ same input produces several views of the same thing and no more information.
27
28
  2. **Structure once.** `brief-intake` tags every field `[explicit]` / `[implied]` /
28
29
  `[inferred]` / `[missing]` with verbatim quotes. The tags are the deliverable — an
29
30
  untagged brief hides which parts are somebody's assumption.
@@ -75,7 +76,7 @@ which end in `BLOCKED`.
75
76
 
76
77
  ## Anti-patterns to avoid
77
78
 
78
- - ❌ **Running all three `gather` skills to be thorough.** Three lenses on one input is not three inputs; pick the one that matches where the signal came from.
79
+ - ❌ **Running every `gather` skill to be thorough.** Three lenses on one input is not three inputs; pick the one that matches where the signal came from.
79
80
  - ❌ **Letting `prd-draft` fill a `[missing]` field.** That converts an open question into a fact nobody agreed to.
80
81
  - ❌ **Treating `doc-critique`'s `SHIP IT` as ratification.** The critique judges the document; G0 judges the problem.
81
82
  - ❌ **Returning `REVISE` to `specify`.** Re-writing the PRD from the same thin brief reproduces the same gaps in better prose.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$comment": "RFC-0028. The first loop of the lifecycle: nothing is designed or built until a human has ratified what problem is being solved. Cheapest loop to redo and the most expensive to skip.",
3
3
  "name": "discover",
4
- "version": "0.2.0",
4
+ "version": "0.3.0",
5
5
  "description": "Take raw signal — interviews, journeys, a strategy question — to a requirement a human has ratified, so design starts from an agreed problem rather than an assumed one. Use when the user is deciding what to build, scoping a feature, or turning stakeholder conversations into requirements.",
6
6
  "entrypoint": "LOOP.md",
7
7
  "kind": "loop",
@@ -11,7 +11,7 @@
11
11
  "id": "gather",
12
12
  "type": "generate",
13
13
  "intent": "Collect the raw signal from whichever direction it arrives: a stakeholder conversation, an existing user journey, or a strategic question.",
14
- "skills": ["requirements-interview", "user-journey-map", "strategy-analysis"]
14
+ "skills": ["requirements-interview", "user-journey-map", "strategy-analysis", "feedback-synthesis"]
15
15
  },
16
16
  {
17
17
  "id": "structure",
@@ -0,0 +1,182 @@
1
+ ---
2
+ name: feedback-synthesis
3
+ description: Turn raw customer feedback — interview notes, support tickets, NPS or CSAT verbatims, app-store reviews, sales-call notes — into ranked themes a product decision can rest on, each a problem statement rather than a feature request, counted by distinct people or accounts not mentions, with segments, severity kept separate from frequency, verbatim quotes with source IDs, what people value, where sources disagree, what the sample cannot tell, and the questions for the next interviews. A stdlib script computes the counts. Use when the user says "what are customers telling us", "synthesise this feedback", "find the themes in these tickets / reviews / NPS comments", "affinity map these interview notes", or "what should we fix first based on feedback".
4
+ ---
5
+
6
+ # feedback-synthesis
7
+
8
+ Feedback arrives as a pile of tickets, survey comments, reviews and notes, and the loudest
9
+ thing in the pile is rarely the most important. This skill turns the pile into a short list of
10
+ problems worth acting on, with numbers that are counted rather than guessed and quotes that can
11
+ be traced to the item they came from. It feeds discovery: the themes go to `brief-intake` to
12
+ become a brief, and the open questions go into the next round of interviews.
13
+
14
+ ## How to respond
15
+
16
+ **Five rules come before the steps and outrank them:**
17
+
18
+ - **Answer what was asked, first.** Open with what to act on and why, in plain words. If the
19
+ data contradicts the user's premise ("the CSV export is our biggest problem"), say so in the
20
+ opening lines, with the count that shows it.
21
+ - **Count people, not mentions.** One customer filing six tickets is one customer. Every number
22
+ in the reply is a count of distinct people or accounts, computed, and says which.
23
+ - **Use only what the input contains.** No quote, count, segment, customer name, revenue figure
24
+ or reason that isn't in the feedback. What is missing goes in **What this can't tell us** and
25
+ the interview questions, never into a theme.
26
+ - **Deliver from what there is.** Feedback without IDs, segments or a stated decision still gets
27
+ a synthesis, with the limit named once. Ask first only when there is no feedback at all.
28
+ - **Write for someone who has never heard of this skill.** No skill names, file paths, script
29
+ output, theme codes or "S2" shorthand without its meaning. A next step is one plain sentence
30
+ at the end.
31
+
32
+ 1. **Read every item before coding any.** List the sources and, for each, what an item is
33
+ (a ticket, a response, an interview, a review), what identifies the person (account ID,
34
+ respondent ID, reviewer name, nothing), and what segment fields exist (plan, role, region,
35
+ tenure, company size, score). Decide the counting unit: **accounts** for B2B feedback where
36
+ account IDs exist (two staff from one customer are one account), **people** otherwise.
37
+ Match people across sources only on a shared ID: an interviewee whose account ID appears in
38
+ the tickets is the same account. Anonymous rows are never merged by guesswork. A rep's or
39
+ interviewer's guess about what someone needs ("they'll probably need SSO") is not that
40
+ person's feedback: leave it uncoded and say so if it matters to the answer.
41
+
42
+ 2. **Write the codebook as problem statements.** 4–12 themes, each one sentence saying who hits
43
+ what and what it costs them, plus a short code. A theme must be something the next interview
44
+ could prove wrong.
45
+ - ✅ *"Clients in the EU can't complete card payments; the error says authentication failed
46
+ and payment arrives weeks later by transfer."*
47
+ - ❌ *"Payment issues"* / *"Users want a better experience"* / *"Improve usability"* — banned:
48
+ nothing could disprove them.
49
+ - **A feature request is evidence of a need, not the need.** Rewrite it into the job behind
50
+ it and keep the request as a quote. ✅ *"Changing a rate means editing every recurring
51
+ invoice by hand — 30 to 58 invoices per price change"*, with "Please add bulk edit"
52
+ quoted as evidence. ❌ Theme title *"Bulk edit"*. Never let the loudest proposed solution
53
+ become the title.
54
+ - **A request with no stated reason is not a theme.** Three "add dark mode" comments with no
55
+ context go under **Requests with no stated need**, and the reason becomes an interview
56
+ question. Don't invent the need.
57
+ - Code what people value as themes too, prefixed `+` (`+reminders`). These become **What
58
+ not to break**.
59
+
60
+ 3. **Rate severity per item, separately from frequency.** From what happened to the person, not
61
+ how angry they sound; capitals and exclamation marks don't raise it.
62
+
63
+ | Level | Meaning | Example |
64
+ |---|---|---|
65
+ | **S1 — blocker** | A core job can't be done, or money, data, security or compliance is at stake | Clients can't pay; data lost; can't log in |
66
+ | **S2 — major** | Done only through a costly recurring workaround, or the person says they'll leave over it | Reformatting an export by hand every month |
67
+ | **S3 — minor** | Slower or annoying; the workaround is quick | An extra click to find a report |
68
+ | **S4 — cosmetic** | Looks, wording, preference, no stated cost | Blurry logo on a PDF |
69
+
70
+ More rules and edge cases in [`reference.md`](reference.md).
71
+
72
+ 4. **Fill the coding sheet and run the counts.** One row per item in the shape of
73
+ [`templates/coding-sheet.csv`](templates/coding-sheet.csv): `item_id`, `person_id` (blank
74
+ when the source has none), `source`, the segment columns, `themes` separated by `;` (a code
75
+ may carry its own severity as `code:S1`), and `severity`. When a raw segment is too fine to
76
+ show a pattern, add a derived column beside it (country → `market` EU / UK / US; employee
77
+ count → `size` band). Then run:
78
+
79
+ ```bash
80
+ # Claude Code
81
+ python3 "${CLAUDE_SKILL_DIR}/scripts/theme_counts.py" coded.csv --person-label account -o counts.md
82
+ # Other IDEs (from the skill folder)
83
+ python3 scripts/theme_counts.py coded.csv --person-label account -o counts.md
84
+ ```
85
+
86
+ It prints per theme the distinct people with an ID, the items with no ID (beside the count,
87
+ never added to it), mentions, repeat filers, sources, the segment breakdown with a flag when
88
+ a theme sits in one segment only, the ranking severity, and the items with no theme. If no script can run, count by hand from the sheet and show the
89
+ tally per theme. Above about 300 items, code the most recent full period or a stated sample,
90
+ and say the counts cover that sample.
91
+
92
+ 5. **Read the counts, then fix the codebook.** Re-read the unthemed items: three or more about
93
+ the same thing is a missing theme. Where one person drives most of a theme's mentions, the
94
+ theme is ranked on its people count. Where a theme lives in one segment, that is a finding
95
+ (✅ *"Only EU accounts: DE, FR, NL"*), not a footnote. Rerun after any recode.
96
+
97
+ 6. **Rank: severity first, then distinct people.** A theme's severity is the worst level at
98
+ least two people reached; a theme raised by one person takes that person's level and is
99
+ labelled *one report — confirm before acting*. The script's ranking severity column applies
100
+ this rule. A rare issue that stops payment outranks a common cosmetic gripe: say so in those
101
+ words when the ranking differs from the volume order.
102
+
103
+ 7. **Write the reply in the shape of [`templates/synthesis.md`](templates/synthesis.md)**,
104
+ sized to the ask (a few bullets when asked for bullets):
105
+ - **Headline** — what to act on first and why, correcting the premise if needed.
106
+ - **Themes, ranked** — problem statement; distinct people or accounts, plus mentions when
107
+ they differ; segments; severity in words; sources.
108
+ - **Evidence per theme** — 1–3 verbatim quotes, each followed by its source ID:
109
+ *"Took most of a Friday." (I-2)*. The quote is an exact substring of the item; cut with
110
+ "…", never reword, never stitch two items into one quote. An interviewer's or rep's summary
111
+ is reported without quotation marks and attributed to them. The requests behind the theme
112
+ are quoted here too.
113
+ - **What not to break** — what people value, with counts and quotes.
114
+ - **Where the sources disagree** — ✅ *"Interviewees call reminders the reason they stay;
115
+ four accounts report reminders going to a former contact."* Both sides, counted.
116
+ - **What this can't tell us** — each source's sampling bias in one line, and the
117
+ conclusions it rules out. No percentage of the user base from a self-selected sample:
118
+ ✅ *"7 of the 29 accounts that raised a ticket"* — ❌ *"24% of customers"*. Anonymous rows
119
+ that may overlap with identified ones are reported beside the count, never added to it:
120
+ ✅ *"3 accounts, plus one anonymous NPS comment that may be one of them"*.
121
+ - **Questions for the next interviews** — 3–6, each tied to a theme, open, non-leading, and
122
+ at least one that could disconfirm the top theme. ✅ *"Walk me through the last time a
123
+ client couldn't pay you. What happened next?"* — ❌ *"Would a fix for card payments help?"*
124
+
125
+ 8. **Keep the machinery out of the reply.** Theme codes, the coding sheet and the script's report
126
+ are working files; save them only when the user asked. The reply carries the answer.
127
+
128
+ **Non-interactive runs** (subagent, CI, headless): with no feedback items at all, emit
129
+ `BLOCKED: need the feedback itself (tickets, survey verbatims, interview notes or reviews)`,
130
+ because a synthesis of what customers "usually say" is fabrication. A missing decision or
131
+ audience becomes an `[assumption]` stated at the top (default: *"feeds next quarter's
132
+ prioritisation"*). Missing person IDs or segment fields are not blockers: proceed, and state
133
+ that the counts are mentions or unsegmented.
134
+
135
+ ## Useful references in this skill
136
+
137
+ - [`reference.md`](reference.md) — the severity scale with edge cases, sampling bias by source, rewriting requests into problems, codebook rules, counting across sources
138
+ - [`templates/coding-sheet.csv`](templates/coding-sheet.csv) — the CSV the script reads
139
+ - [`templates/synthesis.md`](templates/synthesis.md) — the reply skeleton
140
+ - [`scripts/theme_counts.py`](scripts/theme_counts.py) — distinct-person counts per theme, segments, severity, unthemed items (stdlib, Python 3.9+)
141
+ - [`examples/booking-app-feedback.md`](examples/booking-app-feedback.md) — worked example: 16 items from three sources, the coding sheet, the real script output, and the finished reply
142
+
143
+ ## Quality bar
144
+
145
+ - **Every count is distinct people or accounts,** computed from the coded items, and says which unit; mentions appear only beside it.
146
+ - **The ranking puts severity before frequency** and says so where the two disagree.
147
+ - **Every theme title is a falsifiable problem statement** naming who and what it costs; no theme is titled with a proposed feature.
148
+ - **Every quote is verbatim with a source ID;** paraphrase never sits inside quotation marks.
149
+ - **Segments are reported per theme,** and a one-segment theme is called out.
150
+ - **Each source's sampling bias is named,** and no percentage of the whole user base is drawn from a self-selected sample.
151
+ - **What people value and where the sources disagree** are both present.
152
+ - **Interview questions are tied to themes,** open, and include one that could disconfirm the top theme.
153
+ - **Nothing is invented:** no quote, count, segment, reason or customer that isn't in the input.
154
+
155
+ ## When to use this skill
156
+
157
+ - ✅ A pile of support tickets, NPS or CSAT comments, reviews or interview notes, and the question "what are customers telling us?"
158
+ - ✅ Checking a stakeholder's claim ("everyone asks for SSO") against the feedback before it goes into a brief
159
+ - ✅ Affinity-mapping interview notes after a discovery round
160
+ - ✅ Choosing what to fix first from a quarter's feedback
161
+
162
+ ## When NOT to use this skill
163
+
164
+ - ❌ Planning or running the interviews themselves, or writing the question guide — `requirements-interview`
165
+ - ❌ Mapping the stages of a journey a customer goes through — `user-journey-map`
166
+ - ❌ Answering a research question from papers, reports or articles — `source-synthesis`
167
+ - ❌ Ranking rival explanations for a metric change ("why did churn rise in March?") — `hypothesis-comparison`
168
+ - ❌ Turning agreed findings into a brief for a PRD or design doc — `brief-intake`
169
+ - ❌ Cleaning up a tracker backlog: duplicates, stale tickets, ordering — `backlog-triage`
170
+
171
+ ## Anti-patterns to avoid
172
+
173
+ - ❌ **Counting tickets as customers.** "CSV export: 7 tickets, our biggest issue" when six came from one account.
174
+ - ❌ **Ranking by volume alone.** Seven blurry-logo complaints above three accounts whose clients can't pay.
175
+ - ❌ **The feature as the theme.** "Users want bulk edit" — the theme is the hours lost re-pricing recurring invoices.
176
+ - ❌ **Inventing the need behind a bare request.** "Users want dark mode for eye strain" when nobody said why.
177
+ - ❌ **Quotation marks around a summary.** A rep's note or the agent's paraphrase presented as the customer's words.
178
+ - ❌ **Percentages of the base from a self-selected sample.** "40% of users want an Apple Watch app" from fifteen reviews.
179
+ - ❌ **Merging anonymous rows into identified counts** by guessing who wrote them.
180
+ - ❌ **Only problems.** Leaving out what people value invites the fix that breaks it.
181
+ - ❌ **Vague themes.** "Pricing concerns", "usability", "better experience": name who, what happened, and what it cost.
182
+ - ❌ **Leading interview questions.** "Would you use bulk edit?" confirms nothing.
@@ -0,0 +1,46 @@
1
+ [
2
+ {
3
+ "query": "Here are 200 support tickets and last quarter's NPS comments. What are customers actually telling us?",
4
+ "should_trigger": true
5
+ },
6
+ {
7
+ "query": "Affinity map these eight interview notes into themes with quotes",
8
+ "should_trigger": true
9
+ },
10
+ {
11
+ "query": "Find the themes in our app store reviews and tell me what to fix first",
12
+ "should_trigger": true
13
+ },
14
+ {
15
+ "query": "Sales says every prospect wants SSO. Check that against the call notes and churn survey before it goes in the brief",
16
+ "should_trigger": true
17
+ },
18
+ {
19
+ "query": "Synthesise this CSAT export into the top customer pain points by segment",
20
+ "should_trigger": true
21
+ },
22
+ {
23
+ "query": "Write me an interview guide for talking to churned customers next week",
24
+ "should_trigger": false
25
+ },
26
+ {
27
+ "query": "Map our customer's onboarding journey from sign-up to first invoice, with pains at each stage",
28
+ "should_trigger": false
29
+ },
30
+ {
31
+ "query": "Summarise what these five industry reports say about four-day weeks",
32
+ "should_trigger": false
33
+ },
34
+ {
35
+ "query": "Churn jumped 3 points in August. Which of these explanations fits the evidence best?",
36
+ "should_trigger": false
37
+ },
38
+ {
39
+ "query": "Clean up our Jira backlog: duplicates, stale tickets and what to pull next",
40
+ "should_trigger": false
41
+ },
42
+ {
43
+ "query": "Turn this Slack thread about the export bug into a brief for the PRD",
44
+ "should_trigger": false
45
+ }
46
+ ]