skilldrop-cli 0.16.29 → 0.16.30

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 (174) hide show
  1. package/bin/skilldrop.js +160 -33
  2. package/catalogue.json +7 -3
  3. package/model-routing.json +16 -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/skills/codebase-tour/SKILL.md +115 -0
  19. package/packs/dev-team/skills/codebase-tour/evals/eval_queries.json +13 -0
  20. package/packs/dev-team/skills/codebase-tour/evals/evals.json +77 -0
  21. package/packs/dev-team/skills/codebase-tour/evals/files/1/.github/workflows/ci.yml +23 -0
  22. package/packs/dev-team/skills/codebase-tour/evals/files/1/README.md +23 -0
  23. package/packs/dev-team/skills/codebase-tour/evals/files/1/openapi.yaml +21 -0
  24. package/packs/dev-team/skills/codebase-tour/evals/files/1/package-lock.json +7 -0
  25. package/packs/dev-team/skills/codebase-tour/evals/files/1/package.json +29 -0
  26. package/packs/dev-team/skills/codebase-tour/evals/files/1/src/config.ts +20 -0
  27. package/packs/dev-team/skills/codebase-tour/evals/files/1/src/domain/shipments/filter.ts +24 -0
  28. package/packs/dev-team/skills/codebase-tour/evals/files/1/src/domain/shipments/repository.ts +15 -0
  29. package/packs/dev-team/skills/codebase-tour/evals/files/1/src/errors.ts +9 -0
  30. package/packs/dev-team/skills/codebase-tour/evals/files/1/src/generated/api-types.ts +9 -0
  31. package/packs/dev-team/skills/codebase-tour/evals/files/1/src/http/handlers/shipments.ts +38 -0
  32. package/packs/dev-team/skills/codebase-tour/evals/files/1/src/http/routes.ts +7 -0
  33. package/packs/dev-team/skills/codebase-tour/evals/files/1/src/jobs/export-shipments.ts +22 -0
  34. package/packs/dev-team/skills/codebase-tour/evals/files/1/src/server.ts +18 -0
  35. package/packs/dev-team/skills/codebase-tour/evals/files/1/tests/domain/filter.test.ts +16 -0
  36. package/packs/dev-team/skills/codebase-tour/evals/files/1/tests/http/shipments.test.ts +18 -0
  37. package/packs/dev-team/skills/codebase-tour/evals/files/1/tsconfig.json +10 -0
  38. package/packs/dev-team/skills/codebase-tour/evals/files/1/vitest.config.ts +5 -0
  39. package/packs/dev-team/skills/codebase-tour/evals/files/2/.github/workflows/ci.yml +16 -0
  40. package/packs/dev-team/skills/codebase-tour/evals/files/2/README.md +16 -0
  41. package/packs/dev-team/skills/codebase-tour/evals/files/2/noxfile.py +14 -0
  42. package/packs/dev-team/skills/codebase-tour/evals/files/2/pyproject.toml +22 -0
  43. package/packs/dev-team/skills/codebase-tour/evals/files/2/src/ledgerline/__init__.py +3 -0
  44. package/packs/dev-team/skills/codebase-tour/evals/files/2/src/ledgerline/_version.py +4 -0
  45. package/packs/dev-team/skills/codebase-tour/evals/files/2/src/ledgerline/cli.py +32 -0
  46. package/packs/dev-team/skills/codebase-tour/evals/files/2/src/ledgerline/detect.py +15 -0
  47. package/packs/dev-team/skills/codebase-tour/evals/files/2/src/ledgerline/errors.py +2 -0
  48. package/packs/dev-team/skills/codebase-tour/evals/files/2/src/ledgerline/money.py +14 -0
  49. package/packs/dev-team/skills/codebase-tour/evals/files/2/src/ledgerline/parsers/__init__.py +8 -0
  50. package/packs/dev-team/skills/codebase-tour/evals/files/2/src/ledgerline/parsers/acme.py +18 -0
  51. package/packs/dev-team/skills/codebase-tour/evals/files/2/src/ledgerline/parsers/base.py +24 -0
  52. package/packs/dev-team/skills/codebase-tour/evals/files/2/src/ledgerline/parsers/harbor.py +13 -0
  53. package/packs/dev-team/skills/codebase-tour/evals/files/2/tests/fixtures/acme_sample.csv +3 -0
  54. package/packs/dev-team/skills/codebase-tour/evals/files/2/tests/test_acme.py +23 -0
  55. package/packs/dev-team/skills/codebase-tour/evals/files/2/tests/test_detect.py +13 -0
  56. package/packs/dev-team/skills/codebase-tour/evals/files/3/.github/workflows/ci.yml +28 -0
  57. package/packs/dev-team/skills/codebase-tour/evals/files/3/Makefile +16 -0
  58. package/packs/dev-team/skills/codebase-tour/evals/files/3/README.md +16 -0
  59. package/packs/dev-team/skills/codebase-tour/evals/files/3/cmd/tidepool/main.go +24 -0
  60. package/packs/dev-team/skills/codebase-tour/evals/files/3/go.mod +5 -0
  61. package/packs/dev-team/skills/codebase-tour/evals/files/3/internal/config/config.go +38 -0
  62. package/packs/dev-team/skills/codebase-tour/evals/files/3/internal/dispatch/dispatcher.go +52 -0
  63. package/packs/dev-team/skills/codebase-tour/evals/files/3/internal/dispatch/dispatcher_test.go +42 -0
  64. package/packs/dev-team/skills/codebase-tour/evals/files/3/internal/dispatch/sender.go +32 -0
  65. package/packs/dev-team/skills/codebase-tour/evals/files/3/internal/replay/replay.go +31 -0
  66. package/packs/dev-team/skills/codebase-tour/evals/files/3/internal/store/db.go +35 -0
  67. package/packs/dev-team/skills/codebase-tour/evals/files/3/internal/store/query.sql +11 -0
  68. package/packs/dev-team/skills/codebase-tour/evals/files/3/internal/store/query.sql.go +30 -0
  69. package/packs/dev-team/skills/codebase-tour/evals/files/3/internal/store/schema.sql +6 -0
  70. package/packs/dev-team/skills/codebase-tour/evals/files/3/internal/store/store_integration_test.go +21 -0
  71. package/packs/dev-team/skills/codebase-tour/evals/files/3/sqlc.yaml +9 -0
  72. package/packs/dev-team/skills/codebase-tour/evals/files/4/.env.example +4 -0
  73. package/packs/dev-team/skills/codebase-tour/evals/files/4/.github/workflows/ci.yml +16 -0
  74. package/packs/dev-team/skills/codebase-tour/evals/files/4/Makefile +16 -0
  75. package/packs/dev-team/skills/codebase-tour/evals/files/4/README.md +23 -0
  76. package/packs/dev-team/skills/codebase-tour/evals/files/4/data/rates.csv +4 -0
  77. package/packs/dev-team/skills/codebase-tour/evals/files/4/pyproject.toml +19 -0
  78. package/packs/dev-team/skills/codebase-tour/evals/files/4/scripts/gen_rates.py +29 -0
  79. package/packs/dev-team/skills/codebase-tour/evals/files/4/src/shipcalc/__init__.py +0 -0
  80. package/packs/dev-team/skills/codebase-tour/evals/files/4/src/shipcalc/api/__init__.py +0 -0
  81. package/packs/dev-team/skills/codebase-tour/evals/files/4/src/shipcalc/api/preview.py +14 -0
  82. package/packs/dev-team/skills/codebase-tour/evals/files/4/src/shipcalc/api/routes.py +36 -0
  83. package/packs/dev-team/skills/codebase-tour/evals/files/4/src/shipcalc/config.py +15 -0
  84. package/packs/dev-team/skills/codebase-tour/evals/files/4/src/shipcalc/discounts.py +8 -0
  85. package/packs/dev-team/skills/codebase-tour/evals/files/4/src/shipcalc/generated/__init__.py +0 -0
  86. package/packs/dev-team/skills/codebase-tour/evals/files/4/src/shipcalc/generated/rates_table.py +8 -0
  87. package/packs/dev-team/skills/codebase-tour/evals/files/4/src/shipcalc/main.py +8 -0
  88. package/packs/dev-team/skills/codebase-tour/evals/files/4/src/shipcalc/shipping.py +18 -0
  89. package/packs/dev-team/skills/codebase-tour/evals/files/4/src/shipcalc/tax.py +7 -0
  90. package/packs/dev-team/skills/codebase-tour/evals/files/4/tests/conftest.py +3 -0
  91. package/packs/dev-team/skills/codebase-tour/evals/files/4/tests/unit/test_discounts.py +11 -0
  92. package/packs/dev-team/skills/codebase-tour/evals/files/4/tests/unit/test_tax.py +13 -0
  93. package/packs/dev-team/skills/codebase-tour/evals/files/5/.env.example +3 -0
  94. package/packs/dev-team/skills/codebase-tour/evals/files/5/.github/workflows/ci.yml +13 -0
  95. package/packs/dev-team/skills/codebase-tour/evals/files/5/README.md +18 -0
  96. package/packs/dev-team/skills/codebase-tour/evals/files/5/package.json +13 -0
  97. package/packs/dev-team/skills/codebase-tour/evals/files/5/packages/api/openapi.json +30 -0
  98. package/packs/dev-team/skills/codebase-tour/evals/files/5/packages/api/package.json +15 -0
  99. package/packs/dev-team/skills/codebase-tour/evals/files/5/packages/api/src/db.js +19 -0
  100. package/packs/dev-team/skills/codebase-tour/evals/files/5/packages/api/src/flags.js +8 -0
  101. package/packs/dev-team/skills/codebase-tour/evals/files/5/packages/api/src/generated/schema.js +32 -0
  102. package/packs/dev-team/skills/codebase-tour/evals/files/5/packages/api/src/middleware/validate.js +20 -0
  103. package/packs/dev-team/skills/codebase-tour/evals/files/5/packages/api/src/routes/bookings.js +15 -0
  104. package/packs/dev-team/skills/codebase-tour/evals/files/5/packages/api/src/server.js +15 -0
  105. package/packs/dev-team/skills/codebase-tour/evals/files/5/packages/api/src/services/bookings.js +23 -0
  106. package/packs/dev-team/skills/codebase-tour/evals/files/5/packages/api/src/services/pricing.js +9 -0
  107. package/packs/dev-team/skills/codebase-tour/evals/files/5/packages/api/test/pricing.test.js +15 -0
  108. package/packs/dev-team/skills/codebase-tour/evals/files/5/packages/web/package.json +9 -0
  109. package/packs/dev-team/skills/codebase-tour/evals/files/5/packages/web/src/bookingForm.js +11 -0
  110. package/packs/dev-team/skills/codebase-tour/evals/files/5/packages/web/src/format.js +7 -0
  111. package/packs/dev-team/skills/codebase-tour/evals/files/5/packages/web/test/format.test.js +12 -0
  112. package/packs/dev-team/skills/codebase-tour/evals/files/5/scripts/gen-schema.mjs +27 -0
  113. package/packs/dev-team/skills/codebase-tour/examples/refund-window.md +158 -0
  114. package/packs/dev-team/skills/codebase-tour/manifest.json +47 -0
  115. package/packs/dev-team/skills/codebase-tour/scripts/repo_facts.py +403 -0
  116. package/packs/dev-team/skills/codebase-tour/templates/codebase-tour.md +85 -0
  117. package/packs/dev-team/skills/dependency-upgrade-review/SKILL.md +101 -0
  118. package/packs/dev-team/skills/dependency-upgrade-review/evals/eval_queries.json +11 -0
  119. package/packs/dev-team/skills/dependency-upgrade-review/evals/evals.json +77 -0
  120. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/1/.github/workflows/ci.yml +20 -0
  121. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/1/pr.diff +87 -0
  122. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/1/quillmark-CHANGELOG.md +24 -0
  123. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/1/src/render/comment.js +6 -0
  124. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/1/src/render/note.js +8 -0
  125. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/1/test/note.test.js +12 -0
  126. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/2/.github/workflows/ci.yml +23 -0
  127. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/2/.github/workflows/release.yml +23 -0
  128. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/2/pr.diff +32 -0
  129. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/2/release-notes.md +20 -0
  130. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/4/.github/workflows/ci.yml +18 -0
  131. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/4/Dockerfile +8 -0
  132. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/4/PR.md +36 -0
  133. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/4/pr.diff +47 -0
  134. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/4/src/ingest/client.py +24 -0
  135. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/4/src/ingest/reader.py +32 -0
  136. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/4/tests/test_reader.py +15 -0
  137. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/4/upstream/quillstream-CHANGELOG.md +37 -0
  138. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/5/.github/workflows/ci.yml +22 -0
  139. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/5/.github/workflows/release.yml +25 -0
  140. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/5/.nvmrc +1 -0
  141. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/5/PR-412.md +16 -0
  142. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/5/PR-413.md +15 -0
  143. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/5/package.json +21 -0
  144. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/5/pr-412.diff +26 -0
  145. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/5/pr-413.diff +84 -0
  146. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/5/scripts/release.mjs +18 -0
  147. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/5/upstream/release-notary-CHANGELOG.md +16 -0
  148. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/5/upstream/setup-toolchain-RELEASES.md +22 -0
  149. package/packs/dev-team/skills/dependency-upgrade-review/examples/inputs/ledgerline.diff +38 -0
  150. package/packs/dev-team/skills/dependency-upgrade-review/examples/ledgerline-tidyparse.md +91 -0
  151. package/packs/dev-team/skills/dependency-upgrade-review/manifest.json +50 -0
  152. package/packs/dev-team/skills/dependency-upgrade-review/reference.md +99 -0
  153. package/packs/dev-team/skills/dependency-upgrade-review/scripts/upgrade_diff.py +718 -0
  154. package/packs/product-manager/loops/discover/LOOP.md +5 -4
  155. package/packs/product-manager/loops/discover/loop.json +2 -2
  156. package/packs/product-manager/skills/feedback-synthesis/SKILL.md +182 -0
  157. package/packs/product-manager/skills/feedback-synthesis/evals/eval_queries.json +46 -0
  158. package/packs/product-manager/skills/feedback-synthesis/evals/evals.json +76 -0
  159. package/packs/product-manager/skills/feedback-synthesis/evals/files/1/interview-notes.md +47 -0
  160. package/packs/product-manager/skills/feedback-synthesis/evals/files/1/nps-verbatims.csv +13 -0
  161. package/packs/product-manager/skills/feedback-synthesis/evals/files/1/support-tickets.csv +36 -0
  162. package/packs/product-manager/skills/feedback-synthesis/evals/files/3/churn-exit-survey.csv +11 -0
  163. package/packs/product-manager/skills/feedback-synthesis/evals/files/3/sales-call-notes.md +55 -0
  164. package/packs/product-manager/skills/feedback-synthesis/evals/files/4/csat_comments_q3.csv +17 -0
  165. package/packs/product-manager/skills/feedback-synthesis/evals/files/4/support_tickets_q3.csv +49 -0
  166. package/packs/product-manager/skills/feedback-synthesis/evals/files/5/app_reviews_last_60_days.csv +32 -0
  167. package/packs/product-manager/skills/feedback-synthesis/evals/files/5/interview_notes_round1.md +48 -0
  168. package/packs/product-manager/skills/feedback-synthesis/examples/booking-app-feedback.md +139 -0
  169. package/packs/product-manager/skills/feedback-synthesis/examples/inputs/booking-app-coded.csv +17 -0
  170. package/packs/product-manager/skills/feedback-synthesis/manifest.json +53 -0
  171. package/packs/product-manager/skills/feedback-synthesis/reference.md +89 -0
  172. package/packs/product-manager/skills/feedback-synthesis/scripts/theme_counts.py +355 -0
  173. package/packs/product-manager/skills/feedback-synthesis/templates/coding-sheet.csv +6 -0
  174. package/packs/product-manager/skills/feedback-synthesis/templates/synthesis.md +42 -0
package/bin/skilldrop.js CHANGED
@@ -71,8 +71,10 @@ Usage:
71
71
  in SKILL.md before you trust a catalog (RFC-0022)
72
72
  skilldrop new-skill <name> --pack <p> scaffold a skill in the catalog you're in (for catalog authors;
73
73
  --tier light|standard|heavy, default standard)
74
- skilldrop loop-stats [<file>] [--days N] summarise the opt-in loop run log (SKILLDROP_LOOP_LOG):
75
- which gates pass first time, loop back, or block. Local only.
74
+ skilldrop loop-stats [<file>...] [--days N] [--json | --html <out>] summarise the opt-in loop
75
+ run log (SKILLDROP_LOOP_LOG): which gates pass first time, loop
76
+ back, or block. Several files (logs or others' --json) combine
77
+ into one view; --html writes a page that loads nothing. Local only.
76
78
  skilldrop init-catalogue <dir> [--pack <name>] start a private catalogue for your team's skills
77
79
  skilldrop package <dir> [--pack a,b] [--skills x,y] [--agents] copy a vetted subset of a catalogue
78
80
  into a standalone one to host internally (writes MIRROR.json)
@@ -147,7 +149,7 @@ function parseArgs(argv) {
147
149
  for (let i = 0; i < argv.length; i++) {
148
150
  const a = argv[i];
149
151
  if (a === "--pack" || a === "--ide" || a === "--dest" || a === "--from" || a === "--panel" || a === "--profile" || a === "--tier" || a === "--skills" || a === "--days" ||
150
- a === "--policy" || a === "--merge" || a === "--label" || a === "--last") out.flags[a.slice(2)] = argv[++i];
152
+ a === "--policy" || a === "--merge" || a === "--label" || a === "--last" || a === "--html") out.flags[a.slice(2)] = argv[++i];
151
153
  else if (a.startsWith("--")) out.flags[a.slice(2)] = true;
152
154
  else out._.push(a);
153
155
  }
@@ -1789,47 +1791,172 @@ function newSkill(args) {
1789
1791
 
1790
1792
  /* loop-stats: summarise the opt-in run log loops append to when SKILLDROP_LOOP_LOG is set.
1791
1793
  Local only — the file is the user's, nothing is sent anywhere. Answers the questions a team
1792
- asks of its loops: which gates pass first time, which keep looping back, which block. */
1794
+ asks of its loops: which gates pass first time, which keep looping back, which block.
1795
+ Several inputs are combined: each is a run log (JSONL) or another person's `--json` summary,
1796
+ so a team can pool its numbers by passing files around, never through a service. */
1797
+ const LOOP_CLASSES = ["pass", "conditional", "revise", "redirect", "blocked", "unknown"];
1798
+
1799
+ function isoWeek(ts) {
1800
+ const d = new Date(ts);
1801
+ if (isNaN(d)) return null;
1802
+ const t = new Date(Date.UTC(d.getUTCFullYear(), d.getUTCMonth(), d.getUTCDate()));
1803
+ t.setUTCDate(t.getUTCDate() + 4 - (t.getUTCDay() || 7)); // the week's Thursday
1804
+ const y = t.getUTCFullYear();
1805
+ const wk = Math.ceil(((t - Date.UTC(y, 0, 1)) / 864e5 + 1) / 7);
1806
+ return `${y}-W${String(wk).padStart(2, "0")}`;
1807
+ }
1808
+
1793
1809
  function loopStats(args) {
1794
- const file = args._[0] || process.env.SKILLDROP_LOOP_LOG || path.join(".skilldrop", "loop-log.jsonl");
1795
- const raw = readIfPresent(file, null);
1796
- if (raw === null)
1797
- die(`no run log at ${file}.\n Turn it on: export SKILLDROP_LOOP_LOG="$PWD/.skilldrop/loop-log.jsonl" — each loop then appends one line per gate verdict.`);
1810
+ const inputs = args._.length ? args._ : [process.env.SKILLDROP_LOOP_LOG || path.join(".skilldrop", "loop-log.jsonl")];
1798
1811
  const days = Number(args.flags.days) || 0;
1799
1812
  const since = days ? Date.now() - days * 864e5 : 0;
1800
- const rows = [], bad = [];
1801
- raw.split(/\r?\n/).forEach((line, i) => {
1802
- if (!line.trim()) return;
1803
- try {
1804
- const r = JSON.parse(line);
1805
- if (!r.loop || !r.verdict) throw new Error("missing loop or verdict");
1806
- if (since && Date.parse(r.ts) < since) return;
1807
- rows.push(r);
1808
- } catch (e) { bad.push(i + 1); }
1809
- });
1810
1813
  const terms = (() => { try { return readJSON(path.join(ROOT, "contracts", "terminals.json")); } catch (e) { return {}; } })();
1811
1814
  // Verdict classes come from the shared vocabulary (RFC-0028), so a new verdict word is
1812
1815
  // classified the same way here as in the loops.
1813
1816
  const classOf = (v) => ((terms.verdicts || {})[v] || {}).class || (/BLOCK/.test(v) ? "blocked" : "unknown");
1814
1817
  const by = {};
1815
- for (const r of rows) {
1816
- const k = `${r.loop} ${r.gate || "-"} (${r.stage || "?"})`;
1817
- const g = by[k] = by[k] || { n: 0, verdicts: {}, firstPass: 0, firsts: 0, blocked: 0, rounds: [] };
1818
- g.n++; g.verdicts[r.verdict] = (g.verdicts[r.verdict] || 0) + 1;
1819
- const cls = classOf(r.verdict);
1820
- if (Number(r.round) === 1) { g.firsts++; if (cls === "pass" || cls === "conditional") g.firstPass++; }
1821
- if (cls === "blocked") g.blocked++;
1822
- if (r.round) g.rounds.push(Number(r.round));
1823
- }
1824
- if (args.flags.json) return emitJSON({ file, entries: rows.length, skipped: bad.length, gates: by });
1825
- console.log(`${rows.length} gate verdict(s) in ${file}${days ? ` from the last ${days} days` : ""}\n`);
1818
+ let entries = 0, skipped = 0, summaries = 0;
1819
+ const gateOf = (k, r) => by[k] = by[k] || { loop: r.loop, gate: r.gate || null, stage: r.stage || null, n: 0,
1820
+ verdicts: {}, firstPass: 0, firsts: 0, blocked: 0, maxRound: 0, rounds: {}, weeks: {} };
1821
+ const keyOf = (r) => `${r.loop} ${r.gate || "-"} (${r.stage || "?"})`;
1822
+
1823
+ for (const file of inputs) {
1824
+ const raw = readIfPresent(file, null);
1825
+ if (raw === null)
1826
+ die(`no run log at ${file}.\n Turn it on: export SKILLDROP_LOOP_LOG="$PWD/.skilldrop/loop-log.jsonl" — each loop then appends one line per gate verdict.`);
1827
+ let summary = null;
1828
+ try { const o = JSON.parse(raw); if (o && typeof o === "object" && o.gates) summary = o; } catch (e) { /* a run log */ }
1829
+ if (summary) {
1830
+ // Someone else's `loop-stats --json`: add its counts in. Its own --days already applied.
1831
+ summaries++;
1832
+ entries += Number(summary.entries) || 0; skipped += Number(summary.skipped) || 0;
1833
+ for (const [k, s] of Object.entries(summary.gates)) {
1834
+ const m = /^(\S+)\s+(\S+)\s+\((.*)\)$/.exec(k) || [];
1835
+ const g = gateOf(k, { loop: s.loop || m[1] || k, gate: s.gate || (m[2] !== "-" ? m[2] : null), stage: s.stage || m[3] });
1836
+ g.n += Number(s.n) || 0; g.firstPass += Number(s.firstPass) || 0; g.firsts += Number(s.firsts) || 0;
1837
+ g.blocked += Number(s.blocked) || 0;
1838
+ for (const [v, n] of Object.entries(s.verdicts || {})) g.verdicts[v] = (g.verdicts[v] || 0) + Number(n);
1839
+ // 0.16.29 and earlier wrote `rounds` as a list of round numbers; later, as counts.
1840
+ const rounds = Array.isArray(s.rounds) ? s.rounds.reduce((a, r) => (a[r] = (a[r] || 0) + 1, a), {}) : s.rounds || {};
1841
+ for (const [r, n] of Object.entries(rounds)) g.rounds[r] = (g.rounds[r] || 0) + Number(n);
1842
+ g.maxRound = Math.max(g.maxRound, Number(s.maxRound) || 0, ...Object.keys(rounds).map(Number));
1843
+ for (const [w, cls] of Object.entries(s.weeks || {})) {
1844
+ const into = g.weeks[w] = g.weeks[w] || {};
1845
+ for (const [c, n] of Object.entries(cls)) into[c] = (into[c] || 0) + Number(n);
1846
+ }
1847
+ }
1848
+ continue;
1849
+ }
1850
+ raw.split(/\r?\n/).forEach((line) => {
1851
+ if (!line.trim()) return;
1852
+ let r;
1853
+ try {
1854
+ r = JSON.parse(line);
1855
+ if (!r.loop || !r.verdict) throw new Error("missing loop or verdict");
1856
+ } catch (e) { skipped++; return; }
1857
+ if (since && Date.parse(r.ts) < since) return;
1858
+ entries++;
1859
+ const g = gateOf(keyOf(r), r);
1860
+ g.n++; g.verdicts[r.verdict] = (g.verdicts[r.verdict] || 0) + 1;
1861
+ const cls = classOf(r.verdict);
1862
+ if (Number(r.round) === 1) { g.firsts++; if (cls === "pass" || cls === "conditional") g.firstPass++; }
1863
+ if (cls === "blocked") g.blocked++;
1864
+ if (r.round) { g.rounds[r.round] = (g.rounds[r.round] || 0) + 1; g.maxRound = Math.max(g.maxRound, Number(r.round)); }
1865
+ const w = isoWeek(r.ts);
1866
+ if (w) { const into = g.weeks[w] = g.weeks[w] || {}; into[cls] = (into[cls] || 0) + 1; }
1867
+ });
1868
+ }
1869
+ const sources = inputs.length;
1870
+ if (args.flags.json)
1871
+ return emitJSON({ sources, entries, skipped, ...(days ? { days } : {}), gates: by });
1872
+ if (args.flags.html) {
1873
+ const out = typeof args.flags.html === "string" ? args.flags.html : "loop-stats.html";
1874
+ fs.writeFileSync(out, loopStatsHtml(by, { sources, entries, days, classOf }));
1875
+ console.log(`wrote ${out}: ${entries} gate verdict(s) from ${sources} file(s). It loads nothing from the network; open it in any browser.`);
1876
+ return;
1877
+ }
1878
+ const from = sources === 1 ? inputs[0] : `${sources} files`;
1879
+ console.log(`${entries} gate verdict(s) in ${from}${days ? ` from the last ${days} days` : ""}\n`);
1826
1880
  for (const [k, g] of Object.entries(by).sort()) {
1827
1881
  const mix = Object.entries(g.verdicts).sort((a, b) => b[1] - a[1]).map(([v, n]) => `${v} ${n}`).join(", ");
1828
1882
  const fp = g.firsts ? `${Math.round(100 * g.firstPass / g.firsts)}% pass first time` : "no first-round entries";
1829
- const maxRound = g.rounds.length ? Math.max(...g.rounds) : 0;
1830
- console.log(`${k}\n ${g.n} verdict(s): ${mix}\n ${fp} · ${g.blocked} blocked · longest run ${maxRound} round(s)`);
1831
- }
1832
- if (bad.length) console.log(`\n${bad.length} line(s) skipped as unreadable: ${bad.slice(0, 10).join(", ")}${bad.length > 10 ? "…" : ""}`);
1883
+ console.log(`${k}\n ${g.n} verdict(s): ${mix}\n ${fp} · ${g.blocked} blocked · longest run ${g.maxRound} round(s)`);
1884
+ }
1885
+ if (summaries && days) console.log(`\n--days applies to run logs only; the ${summaries} summary file(s) were counted as written.`);
1886
+ if (skipped) console.log(`\n${skipped} line(s) skipped as unreadable.`);
1887
+ }
1888
+
1889
+ /* The dashboard: one self-contained page, no script, and a policy that lets it load nothing,
1890
+ so it can be opened from an email or a shared drive and sent on without leaking anything. */
1891
+ function loopStatsHtml(by, { sources, entries, days, classOf }) {
1892
+ const esc = (x) => String(x).replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;");
1893
+ const label = { pass: "passed", conditional: "passed with conditions", revise: "sent back",
1894
+ redirect: "redirected", blocked: "blocked", unknown: "other" };
1895
+ const allWeeks = [...new Set(Object.values(by).flatMap((g) => Object.keys(g.weeks)))].sort().slice(-12);
1896
+ const bar = (counts, total) => LOOP_CLASSES.filter((c) => counts[c]).map((c) =>
1897
+ `<span class="seg ${c}" style="width:${(100 * counts[c] / total).toFixed(2)}%" title="${esc(`${counts[c]} ${label[c]}`)}"></span>`).join("");
1898
+ const loops = {};
1899
+ for (const [k, g] of Object.entries(by)) (loops[g.loop] = loops[g.loop] || []).push([k, g]);
1900
+ const sections = Object.keys(loops).sort().map((loop) => {
1901
+ const rows = loops[loop].sort((a, b) => String(a[1].gate).localeCompare(String(b[1].gate))).map(([, g]) => {
1902
+ const byClass = {};
1903
+ for (const [v, n] of Object.entries(g.verdicts)) byClass[classOf(v)] = (byClass[classOf(v)] || 0) + n;
1904
+ const fp = g.firsts ? Math.round(100 * g.firstPass / g.firsts) : null;
1905
+ const mix = Object.entries(g.verdicts).sort((a, b) => b[1] - a[1]).map(([v, n]) => `${esc(v)} ${n}`).join(", ");
1906
+ const peak = Math.max(1, ...allWeeks.map((w) => Object.values(g.weeks[w] || {}).reduce((a, b) => a + b, 0)));
1907
+ const spark = allWeeks.map((w) => {
1908
+ const c = g.weeks[w] || {}, t = Object.values(c).reduce((a, b) => a + b, 0);
1909
+ return `<span class="wk" title="${esc(`${w}: ${t} verdict(s)`)}"><span class="col" style="height:${(100 * t / peak).toFixed(0)}%">${t ? bar(c, t).replace(/width:/g, "height:") : ""}</span></span>`;
1910
+ }).join("");
1911
+ return `<tr><th scope="row">${esc(g.gate || "-")}<small>${esc(g.stage || "")}</small></th><td class="num">${g.n}</td>` +
1912
+ `<td class="num">${fp === null ? "–" : fp + "%"}</td><td class="num">${g.blocked}</td><td class="num">${g.maxRound}</td>` +
1913
+ `<td><div class="mix" role="img" aria-label="${esc(mix)}">${bar(byClass, g.n)}</div><small>${mix}</small></td>` +
1914
+ `<td><div class="spark" role="img" aria-label="verdicts per week">${spark}</div></td></tr>`;
1915
+ }).join("\n");
1916
+ return `<section><h2>${esc(loop)}</h2><table><thead><tr><th>Gate</th><th>Verdicts</th><th>Pass first time</th>` +
1917
+ `<th>Blocked</th><th>Longest run</th><th>Mix</th><th>Per week</th></tr></thead><tbody>\n${rows}\n</tbody></table></section>`;
1918
+ }).join("\n");
1919
+ const legend = LOOP_CLASSES.filter((c) => c !== "unknown").map((c) => `<span><i class="seg ${c}"></i>${label[c]}</span>`).join("");
1920
+ const weeks = allWeeks.length ? `${allWeeks[0]} to ${allWeeks[allWeeks.length - 1]}` : "no dated entries";
1921
+ return `<!doctype html>
1922
+ <html lang="en"><head><meta charset="utf-8">
1923
+ <meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'">
1924
+ <meta name="viewport" content="width=device-width,initial-scale=1">
1925
+ <title>Loop gates: where work gets stuck</title>
1926
+ <style>
1927
+ :root{color-scheme:light dark;--bg:#fafaf9;--fg:#17171a;--muted:#5c5c58;--line:#e4e4e0;--card:#fff}
1928
+ @media (prefers-color-scheme:dark){:root{--bg:#16161b;--fg:#ececf1;--muted:#a3a3ad;--line:#2f2f38;--card:#1d1d23}}
1929
+ body{margin:0;background:var(--bg);color:var(--fg);font:15px/1.5 -apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,Helvetica,Arial,sans-serif}
1930
+ main{max-width:1100px;margin:0 auto;padding:2rem 1.2rem}
1931
+ h1{font-size:1.6rem;margin:0 0 .3rem}p.sub{color:var(--muted);margin:0 0 1.2rem}
1932
+ section{background:var(--card);border:1px solid var(--line);border-radius:12px;padding:1rem 1.2rem;margin:0 0 1rem;overflow-x:auto}
1933
+ h2{font-size:1.1rem;margin:0 0 .6rem;font-family:ui-monospace,SFMono-Regular,Menlo,Consolas,monospace}
1934
+ table{border-collapse:collapse;width:100%;table-layout:fixed;min-width:820px}
1935
+ thead th:nth-child(1){width:9%}thead th:nth-child(2),thead th:nth-child(4),thead th:nth-child(5){width:9%}thead th:nth-child(3){width:11%}thead th:nth-child(7){width:17%}th,td{text-align:left;padding:.45rem .5rem;border-top:1px solid var(--line);vertical-align:top}
1936
+ thead th{border-top:0;font-size:.78rem;color:var(--muted);font-weight:600}
1937
+ th small,td small{display:block;color:var(--muted);font-size:.78rem;font-weight:400}
1938
+ td.num{text-align:right;font-variant-numeric:tabular-nums;white-space:nowrap}
1939
+ .mix{display:flex;height:14px;border-radius:7px;overflow:hidden;background:var(--line)}
1940
+ .seg{display:block}.pass{background:#15803d}.conditional{background:#65a30d}.revise{background:#d97706}
1941
+ .redirect{background:#7c3aed}.blocked{background:#b91c1c}.unknown{background:#78716c}
1942
+ .spark{display:flex;align-items:flex-end;gap:2px;height:36px;}
1943
+ .wk{flex:1;height:100%;display:flex;align-items:flex-end}.col{display:flex;flex-direction:column-reverse;width:100%;min-height:1px;background:var(--line)}
1944
+ .legend{display:flex;flex-wrap:wrap;gap:.4rem 1rem;margin:0 0 1rem;color:var(--muted);font-size:.85rem}
1945
+ .legend i{display:inline-block;width:.8rem;height:.8rem;border-radius:2px;margin-right:.35rem;vertical-align:-1px}
1946
+ ul{margin:.3rem 0 0;padding-left:1.2rem}
1947
+ </style></head><body><main>
1948
+ <h1>Loop gates: where work gets stuck</h1>
1949
+ <p class="sub">${entries} gate verdict(s) from ${sources} file(s)${days ? `, the last ${days} days` : ""} · weeks shown: ${esc(weeks)} · generated ${esc(new Date().toISOString().slice(0, 10))} by skilldrop loop-stats. Nothing here came from or goes to a server.</p>
1950
+ <div class="legend">${legend}</div>
1951
+ ${sections || "<section><p>No gate verdicts yet.</p></section>"}
1952
+ <section><h2>Reading it</h2><ul>
1953
+ <li><b>A gate that rarely passes first time</b> usually points upstream: look at what the stage before it hands over.</li>
1954
+ <li><b>Frequent blocked</b> means a missing input. The loop's output names what was missing.</li>
1955
+ <li><b>A gate that always passes</b> may not be checking anything. Read a few of its reviews.</li>
1956
+ <li>Don't rank people by these numbers. A hard change loops more than an easy one.</li>
1957
+ </ul></section>
1958
+ </main></body></html>
1959
+ `;
1833
1960
  }
1834
1961
 
1835
1962
  /* init-catalogue <dir>: start a private catalogue (an internal mirror or a team's own skills)
package/catalogue.json CHANGED
@@ -17,7 +17,8 @@
17
17
  "nfr-spec",
18
18
  "success-metrics",
19
19
  "user-story-splitter",
20
- "user-journey-map"
20
+ "user-journey-map",
21
+ "feedback-synthesis"
21
22
  ]
22
23
  },
23
24
  "design-the-system": {
@@ -38,7 +39,8 @@
38
39
  "agent-loop-design",
39
40
  "subagent-design",
40
41
  "agent-budget",
41
- "terraform-module"
42
+ "terraform-module",
43
+ "mcp-pairing"
42
44
  ]
43
45
  },
44
46
  "build-and-review-software": {
@@ -60,7 +62,9 @@
60
62
  "llm-eval-harness",
61
63
  "eval-harness-generator",
62
64
  "backlog-triage",
63
- "tracker-brief-sync"
65
+ "tracker-brief-sync",
66
+ "dependency-upgrade-review",
67
+ "codebase-tour"
64
68
  ]
65
69
  },
66
70
  "build-on-the-claude-api": {
@@ -190,6 +190,10 @@
190
190
  "tier": "standard",
191
191
  "rationale": "A stdlib script does the aggregation; the model maps columns, checks evidence and computes savings with shown arithmetic against a fixed category checklist, which is bounded judgment."
192
192
  },
193
+ "codebase-tour": {
194
+ "tier": "standard",
195
+ "rationale": "Reading a repo and citing what it finds is generation over evidence; every claim is checked against a file, so a mid tier is enough."
196
+ },
193
197
  "content-design": {
194
198
  "tier": "standard",
195
199
  "rationale": "Rewrites source text from user needs under fixed plain-language rules, with a script for the readability numbers. Prioritising questions needs judgment, but facts come from the source."
@@ -222,6 +226,10 @@
222
226
  "tier": "standard",
223
227
  "rationale": "A stdlib script computes every number; the model maps columns, checks data quality and interprets weekly trends against fixed DORA definitions, which is bounded judgment."
224
228
  },
229
+ "dependency-upgrade-review": {
230
+ "tier": "standard",
231
+ "rationale": "Reads a version diff, changelogs and CI evidence against a fixed checklist; the merge verdict follows from what is verified, not open-ended judgment."
232
+ },
225
233
  "design-doc": {
226
234
  "tier": "standard",
227
235
  "rationale": "Synthesizes a design doc from a brief. Escalate to heavy via large-input or ambiguous-input when alternatives are genuinely contested."
@@ -254,6 +262,10 @@
254
262
  "tier": "standard",
255
263
  "rationale": "Code + test generation against acceptance criteria. The adversarial pass delegates to devils-advocate (heavy) where available; the loop itself is generation."
256
264
  },
265
+ "feedback-synthesis": {
266
+ "tier": "standard",
267
+ "rationale": "Coding feedback into themes is bounded judgment; the counts by distinct person and segment come from its script."
268
+ },
257
269
  "figma-diagrams": {
258
270
  "tier": "standard",
259
271
  "rationale": "API calls + spec generation. Bounded transformation."
@@ -298,6 +310,10 @@
298
310
  "tier": "standard",
299
311
  "rationale": "Writes short copy to fit and fills a spec for a deterministic builder. Bounded generation."
300
312
  },
313
+ "mcp-pairing": {
314
+ "tier": "standard",
315
+ "rationale": "Matching a loop's stages to the context each needs and the narrowest access that provides it is bounded judgment over a short checklist."
316
+ },
301
317
  "md-to-docx": {
302
318
  "tier": "standard",
303
319
  "rationale": "A stdlib script does the conversion; the model restructures the Markdown so it converts cleanly (heading levels, footnotes, HTML), reads each warning and explains the losses. Bounded editing, no open-ended generation."
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "skilldrop-cli",
3
- "version": "0.16.29",
3
+ "version": "0.16.30",
4
4
  "description": "Portable AI-agent skills, measured against the agent without them. ADRs, PRDs, runbooks, decks and reviews, installable into Claude Code, Cursor, Kiro, and any IDE that takes custom instructions. One command per skill or per role-based pack.",
5
5
  "author": {
6
6
  "name": "Sanjay Ananthanarayan",
@@ -0,0 +1,93 @@
1
+ ---
2
+ name: mcp-pairing
3
+ description: Recommend which MCP servers to connect for a loop, workflow or task, stage by stage, with the narrowest access that works — what outside context or action each stage needs, the server that provides it (only ones the user named or the vendor officially publishes), the scopes to grant (read-only by default, never write on a review or gate stage), what it must not be able to do, the export-file fallback that keeps the stage running without it, and which stages need no server at all. Use when the user asks "which MCP servers should we hook up for this loop", "what should our agent connect to", "what token scopes does this stage need", "is this MCP setup too broad", or wants to wire GitHub, GitLab, Jira, Linear, Slack, Sentry, Grafana or a database into an agent workflow.
4
+ ---
5
+
6
+ # mcp-pairing
7
+
8
+ Turns a loop, workflow or task into a per-stage connection plan: which stages get an MCP
9
+ server, which get none, and the exact access each one holds. An MCP server is a capability
10
+ handed to the agent with someone's credentials, so the default answer for every stage is
11
+ **fewer servers, read-only, scoped to one project**, and every grant beyond that is argued
12
+ from the stage's output. The plan stays honest about names: it never invents a server,
13
+ package or URL.
14
+
15
+ Complements `agent-threat-model`, which audits a finished capability surface for data
16
+ exfiltration paths. This skill decides what the surface should be in the first place;
17
+ that one attacks it.
18
+
19
+ ## How to respond
20
+
21
+ **Four rules come before the steps and outrank them:**
22
+
23
+ - **Answer what was asked, first.** Open with the plan in plain words: how many servers, which ones, read or write, and which stages need none. The table comes after.
24
+ - **Use only what you were given.** Name only the tools, servers, stages and scopes the input contains, plus vendors' own official servers under the naming rule in step 3. Never invent a package, endpoint, URL or version. What the plan needs and doesn't have is listed once at the end under "To confirm".
25
+ - **Deliver from what you have.** When the request names a loop, workflow or task, state the assumptions in a line and produce the plan. When it names nothing to pair, ask for it in one or two plain sentences and say what the plan will contain once it arrives.
26
+ - **Write for someone who has never heard of this skill.** No skill names, no paths from this folder, no internal terms, nothing about how the run was set up. A next step is one plain sentence at the end that describes the work.
27
+
28
+ 1. **List the stages and what each one produces.** From a named skilldrop loop, use its stage list and the stage types in [`reference.md`](reference.md) (generate, verify, gate). From a described workflow, write the stages as verbs with one output each — ✅ *"triage: one ticket labelled with severity"* — ❌ *"handle tickets"*. Mark each stage's **type** and its **cadence** (per change, daily, weekly, monthly). Ask at most 2 questions, spent on the tools in use and who can write into them; everything else is tagged `[assumption]`.
29
+
30
+ 2. **Name the outside context or action each stage needs, by category.** Categories: tracker issues, repo / merge request / diff, CI status, docs and wiki, logs / metrics / traces, error events, design files, chat history, database read, cloud billing, web. A stage that works from the previous stage's output needs nothing outside — write **None** and move on. ✅ *"implement — None: works from the shaped story and the local checkout"*.
31
+
32
+ 3. **Pick the server for each need, or write the category.** Name a server only when (a) the user named it, or (b) it is the vendor's own server for a tool the user named — then say how to confirm it: publisher is the vendor, source or docs live on the vendor's domain or organization, and a version or commit is pinned for a local server. Never write a URL, package name or install command the user did not give. ✅ *"GitLab's own MCP server — confirm GitLab publishes it in GitLab's docs before installing"* — ❌ *"`npx @gitlab/mcp-server`"*. When no official server is known, write the category and the five vetting checks from [`reference.md`](reference.md): publisher, source available, pinned version, tools exposed, and whether it can run code or reach the network beyond its own API.
33
+
34
+ 4. **Grant read-only by default, per stage, not per session.** The scope is the narrowest token scope, one repository or project, one database role on a replica — ✅ *"GitLab token, `read_api`, project `payments-web` only"* — ❌ *"GitLab token with `api`"*. Write access goes **only** to the stage whose output is that write (posting the release, opening the draft merge request), on a **separate credential** from the read one, and is the user's choice to grant; otherwise a person does the write. When the vendor offers no scope narrower than full API access for that write, a person does it. Never grant write to a **verify** or **gate** stage: a review that can merge, approve, close or comment is no longer a review. No admin, owner, org-wide or `*` scopes, ever.
35
+
36
+ 5. **Write what each server must not be able to do.** Name the concrete actions to exclude — merge, approve, push to the default branch, transition or close issues, post to channels, run SQL writes, delete, change settings. "Read-only" alone is not enough when the vendor's read-only mode still exposes a tool the stage doesn't need; list the toolsets to turn off.
37
+
38
+ 6. **Split untrusted reading from write or exec.** Issue text, merge request and PR comments, tickets, chat messages, web pages, log lines and design-file comments are **untrusted input**: anyone who can write there can write instructions. A stage that reads untrusted content and also holds a write tool, a shell, or a network tool is the dangerous combination. Fix it by splitting the stage (one reader that returns a file, one actor that reads only that file), dropping the write to a person, or dropping the server. ✅ *"implement runs tests (exec), so it gets no tracker server; it reads the story file shape wrote, which a person skims first"*.
39
+
40
+ 7. **Cut every server that doesn't earn its attack surface.** A server earns its place when the input is too large, too structured or too live to paste at the stage's cadence: dozens of items (a release's 40 merge requests), fields that matter (a cycle's issues with status and owner), or data that changes by the minute (a live incident). A handful of items fetched weekly or less, or a step that saves a single copy-paste, gets the export file instead. Say which servers were cut and why.
41
+
42
+ 8. **Give every stage its export-file fallback.** Name the file a person produces without a server — the diff saved to a file, the tracker filter exported as CSV, the dashboard panel's data downloaded, the query result saved as CSV — so the loop still runs where no server is allowed.
43
+
44
+ 9. **Place the secrets.** Tokens live in environment variables or the tool's secret store; a committed config file holds variable names, never values. A project-scoped config that contains a token stays out of git. Read and write credentials are separate variables. Point at OAuth where the vendor offers it, so no long-lived token exists.
45
+
46
+ 10. **Emit with [`templates/mcp-pairing.md`](templates/mcp-pairing.md)**: the plan in two or three sentences, the per-stage pairing table (stage, needs, server, grant, must not, fallback, risk), the credential list, servers cut, vetting checks for anything not confirmed official, and "To confirm". When the plan still puts private data, untrusted content and any way to send data out in one session, end with one sentence recommending an exfiltration-path review of the finished setup.
47
+
48
+ **Non-interactive runs** (subagent, CI, headless): unnamed tools become categories tagged `[assumption]`, unknown cadence is assumed weekly, and unknown writers into a source make it untrusted. If no loop, workflow or task is given at all, emit `BLOCKED: need the loop, workflow or task the servers are for`, then say in plain words that the plan needs its stages and the tools the team uses — a pairing for invented stages pairs nothing.
49
+
50
+ ## Useful references in this skill
51
+
52
+ - [`reference.md`](reference.md) — stage types and their access ceiling, needs by category with the usual export fallback, the vetting checklist, known vendor read-only controls, the cut rule, and the skilldrop loops' stages
53
+ - [`templates/mcp-pairing.md`](templates/mcp-pairing.md) — output skeleton
54
+ - [`examples/build-loop-github-jira-datadog.md`](examples/build-loop-github-jira-datadog.md) — worked example: the build loop for a team on GitHub, Jira and Datadog; two read-only servers, one optional write credential, Datadog cut
55
+
56
+ ## Quality bar
57
+
58
+ - **Every stage has a row,** including the ones that get **None**. A plan that lists only servers hides which stages hold nothing.
59
+ - **No verify or gate stage holds a write scope.** Not comment, not approve, not label.
60
+ - **Every write grant names the stage whose output it is** and sits on its own credential.
61
+ - **No invented names.** Every server, package, URL and scope in the plan came from the user or is a vendor's own server with the confirm step written beside it.
62
+ - **Every grant is narrower than "read-only":** a scope, a project or repository, a role.
63
+ - **Every stage that reads untrusted content is checked** against write, exec and network tools in the same session, and the combination is split or dropped.
64
+ - **Every stage has an export-file fallback** a person can produce today.
65
+ - **At least one server is argued against** or the plan says why each one earns its place.
66
+ - **No token value appears anywhere,** and the config placement keeps tokens out of git.
67
+
68
+ ## When to use this skill
69
+
70
+ - ✅ "Which MCP servers should we hook up for our release loop?"
71
+ - ✅ Wiring a tracker, repo host, chat, observability tool or database into a loop or recurring agent workflow
72
+ - ✅ Deciding token scopes, project limits and database roles for an agent's servers, stage by stage
73
+ - ✅ Tightening an existing MCP config that grew one server and one wide token at a time
74
+
75
+ ## When NOT to use this skill
76
+
77
+ - ❌ Threat-modelling a finished agent's tools and data for exfiltration paths — `agent-threat-model`
78
+ - ❌ Designing the loop itself: stages, exit criteria, caps, human gates — `agent-loop-design`
79
+ - ❌ Deciding how to split work across subagents and what each one is told — `subagent-design`
80
+ - ❌ Writing the organization's rules for which AI tools and data are allowed — `ai-usage-policy`
81
+ - ❌ Setting up one tracker step by step (Jira, Linear, GitHub Projects) — that is the per-tracker integration guide in the skilldrop docs, not a plan
82
+
83
+ ## Anti-patterns to avoid
84
+
85
+ - ❌ **One session-wide token for the whole loop.** The verify stage then holds the implement stage's write scope.
86
+ - ❌ **A review stage that posts its own comments or merges.** The verdict goes to a person or to CI; the reviewer holds read only.
87
+ - ❌ **Inventing the server.** `@vendor/mcp-server` written from memory is how a typosquatted package gets installed with a production token.
88
+ - ❌ **"Read-only, so safe."** Read-only stops writes to the vendor; it does nothing about untrusted text steering a stage that has a shell.
89
+ - ❌ **Connecting everything the team uses.** A monthly report does not need a live Slack server with history across every channel.
90
+ - ❌ **A token in `.mcp.json`.** Committed files hold `${VAR}` references, never values.
91
+ - ❌ **Admin scope "to get it working".** A 403 means the scope is wrong for that tool; widen to the one permission named in the error, on that stage's credential only.
92
+ - ❌ **Showing the machinery.** Don't mention this skill, its files or its steps in the reply.
93
+ - ❌ **A bare `BLOCKED` line.** Keep it, then say in plain words what is missing and what the plan will contain.
@@ -0,0 +1,13 @@
1
+ [
2
+ { "query": "Which MCP servers should we hook up for our release loop? We use GitLab, Linear and Grafana", "should_trigger": true },
3
+ { "query": "What token scopes does each stage of our build loop need for the GitHub MCP server?", "should_trigger": true },
4
+ { "query": "Here's our .mcp.json — is this too much access for a weekly status report?", "should_trigger": true },
5
+ { "query": "We want the on-call agent to read Sentry and Slack. What should we connect and with what permissions?", "should_trigger": true },
6
+ { "query": "Should our research workflow get a web browsing server and write access to Notion?", "should_trigger": true },
7
+
8
+ { "query": "Threat-model this agent: it reads support tickets, queries our customer DB and can send email", "should_trigger": false },
9
+ { "query": "Design an agent loop that keeps our runbooks up to date, with exit criteria and a human gate", "should_trigger": false },
10
+ { "query": "How should I split this code migration across parallel subagents?", "should_trigger": false },
11
+ { "query": "Write our company's policy on which AI tools staff may use with client data", "should_trigger": false },
12
+ { "query": "Walk me through connecting Jira to Claude Code step by step", "should_trigger": false }
13
+ ]
@@ -0,0 +1,76 @@
1
+ {
2
+ "skill_name": "mcp-pairing",
3
+ "evals": [
4
+ {
5
+ "id": 1,
6
+ "prompt": "Which MCP servers should we hook up for our release loop? We use GitLab, Linear and Grafana. The loop is plan the rollout, check launch readiness, write the release notes, then a go/no-go call. We release about every two weeks.",
7
+ "assertions": [
8
+ "It opens with the plan in plain words — how many servers, which ones, read or write, and which stages need none — before any table",
9
+ "Every one of the four stages (plan, readiness, release notes, go/no-go) has its own row, including any that get no server",
10
+ "No write scope is granted to the readiness check or the go/no-go stage; the go/no-go decision stays with a person",
11
+ "Every grant is narrower than 'read-only': a named scope such as GitLab read_api, one project or team, or a Grafana Viewer role, with no admin or api-wide scope",
12
+ "Any write (publishing the release or notes) sits on a separate credential used only by the release-notes stage, or is handed to a person",
13
+ "It does not invent server names, packages, install commands or URLs the user didn't give; vendor servers are named only as the vendor's own, with how to confirm the publisher",
14
+ "Merge request text, comments or Linear issue text are called untrusted, and no stage that reads them is left holding write, exec or network tools",
15
+ "Every stage gets an export-file fallback, and at least one server is cut or argued for against the fortnightly cadence"
16
+ ]
17
+ },
18
+ {
19
+ "id": 2,
20
+ "prompt": "Our on-call agent uses the config in mcp.json for two steps: drafting incident updates for the #inc channel, and writing the postmortem afterwards from the timeline, the Jira tickets and some database queries. A teammate set it up and it works, but I'm nervous. Tighten it — tell me what each step should actually be connected to.",
21
+ "assertions": [
22
+ "It flags the token values written into the config file and says to move them to environment variables or the tool's secret store, with the committed file holding variable names only",
23
+ "It drops the admin scopes (admin:org, Slack admin) and the broad repo and workflow scopes, naming the narrower scope each step needs",
24
+ "It flags the unpinned community packages (@harbourline/jira-mcp, @harbourline/pg-mcp@latest, @harbourline/slack-mcp) and gives the vetting checks: publisher, source available, pinned version, tools exposed, code or network reach",
25
+ "It replaces the app_owner connection to the primary database with a read-only role on a replica, scoped to what the postmortem needs, or with an exported query result",
26
+ "It says the Slack posting is the one write the incident-update step's output justifies, on a separate credential from history reading, or hands posting to a person, and the postmortem step gets no write",
27
+ "It calls Slack messages and Jira ticket text untrusted and does not leave a step that reads them holding the write or database tools it doesn't need",
28
+ "It does not invent replacement package names, URLs or install commands the user didn't give",
29
+ "Each step has an export-file fallback, such as the incident channel's thread exported to a file or the query result saved as CSV"
30
+ ]
31
+ },
32
+ {
33
+ "id": 3,
34
+ "prompt": "Once a month we research one competitor: plan the question, gather and summarise sources, then weigh the explanations for what they're doing. We want to give the agent a web browsing MCP server, Notion's MCP with write access so it can file the summary in our research space, and Google Drive so it can read our past reports. Good idea?",
35
+ "assertions": [
36
+ "It answers the question directly in the opening lines, with a clear verdict on the setup as proposed",
37
+ "Web pages are called untrusted input, and the stage that browses is not given Notion write or any other write or exec tool in the same session",
38
+ "The weigh-the-explanations stage is treated as a verify stage and gets read-only access or none",
39
+ "Notion write, if kept at all, goes only to the stage whose output is filing the summary, scoped to the one research space on its own credential; otherwise a person files it",
40
+ "Google Drive access is narrowed to the folder of past reports, read-only, or replaced by exported files",
41
+ "Given the monthly cadence, at least one server is cut in favour of an export file or saved pages, with the reason stated",
42
+ "It does not invent server names, packages or URLs the user didn't give, and names Notion's or Google's server only with how to confirm the publisher"
43
+ ]
44
+ },
45
+ {
46
+ "id": 4,
47
+ "held_out": "2026-10-20",
48
+ "prompt": "Our team runs a coding agent through the PR loop in docs/pr-loop.md. Someone set up the .mcp.json in the repo root a few months ago and it's committed so everyone gets the same tools. Before we roll this out to two more teams, can you check it? I want to know if it's too broad, what each stage actually needs, and what you'd change. Don't rewrite the loop itself, just the MCP side.",
49
+ "assertions": [
50
+ "Flags the GitHub token (and the Jira/Slack/DB secrets) pasted in plain text in a committed .mcp.json as an immediate problem: says to treat the tokens as exposed and rotate them, and to move secrets out of the file (environment variables or a secret store, referenced instead of stored inline).",
51
+ "Flags the classic PAT with admin:org and delete_repo (and workflow) as far too broad, and recommends a narrowly scoped token instead (e.g. a fine-grained token or app limited to example-org/orders-service with only the repository permissions the implement stage needs: contents and pull requests write, plus read for actions/checks).",
52
+ "Says the review stage must be read-only: the 'github-reviewer' entry with all toolsets and the ability to approve/merge contradicts the loop's own rule that only a human merges, and should become a read-only connection (diff, CI results, ticket) with no merge, approve, push or write tools.",
53
+ "Identifies the Jira server 'jira-mcp-plus-unofficial@latest' and the community Slack/Postgres packages as unknown-origin, unpinned third-party code that gets credentials, and recommends replacing them with the vendor's official server or the user's vetted one (or at minimum pinning and reviewing them), not trusting them as-is.",
54
+ "Recommends removing the Postgres connection entirely (or replacing it with a schema-only/read-only path such as a schema dump or migration files), since no stage in the loop needs a database and the current config gives the agent the prod owner role on the orders database.",
55
+ "Narrows Slack to what the announce stage actually needs: posting to the single #team-orders-dev channel (chat:write, invited to that channel), dropping history-reading scopes, users:read.email, chat:write.public and admin; and notes that the merge gate stage needs no MCP server at all.",
56
+ "Gives a per-stage breakdown (pick up, implement, review, merge gate, announce) stating for each what access it needs, what it must not be able to do, and a fallback if the server is unavailable (e.g. pasted or exported ticket text for pick-up, gh CLI or a saved diff/CI log for review, a human posting the Slack line).",
57
+ "Does not invent server package names, registry URLs, image names or MCP tool names that weren't in the user's files; where it recommends an official server it either names one the user already referenced (ghcr.io/github/github-mcp-server) or describes it generically and tells the user to confirm the vendor-published source."
58
+ ]
59
+ },
60
+ {
61
+ "id": 5,
62
+ "held_out": "2026-10-20",
63
+ "prompt": "I'm the platform lead for the payments group. We want an agent to do first-pass on-call triage, roughly the flow in docs/oncall-triage.md. Nothing is wired up yet. We use self-hosted Sentry, Grafana Cloud, GitLab, Linear and Postgres. Which MCP servers should we connect for this, with what token scopes / roles, and is there anything you'd keep it away from? Security is going to review whatever you propose, so be specific.",
64
+ "assertions": [
65
+ "Organises the answer by the five stages (gather, correlate, check data, ticket, postmortem draft) and, for each, states what outside data or action the stage needs and which of the user's named systems supplies it.",
66
+ "Recommends read-only access for Sentry, Grafana and GitLab: e.g. Sentry token limited to reading events/issues/projects for payments-api (no project write/admin, no resolving issues), a Grafana service account with Viewer role, and a GitLab token with read-only scopes (read_api / read_repository) rather than the full 'api' scope.",
67
+ "For the database stage, recommends connecting only to the read replica (never the primary) with a dedicated read-only role, and calls out that the PCI card-data columns (pan_last4, card_token, billing_address) must be excluded, e.g. via a restricted view or column-level grants, rather than trusting the agent to avoid them; may also suggest statement timeouts/row limits.",
68
+ "Treats the ticket stage as the only stage that genuinely needs write access, and limits it to creating/updating issues in the PAY team in Linear (no admin, no access to other teams, no deletion), possibly noting a human could approve or the ticket could be created by a person from the agent's draft.",
69
+ "States that the postmortem draft stage needs no MCP server, since it writes markdown from what earlier stages already gathered.",
70
+ "Gives a no-server fallback per stage that keeps the loop working, e.g. exported Sentry event JSON, Grafana panel CSV/screenshot, a pasted git log or MR diff, psql output pasted by the on-call, and a markdown ticket draft the on-call files by hand.",
71
+ "Explicitly lists what each connection must not be able to do (e.g. no deploys, rollbacks or pipeline triggers from GitLab, no alert silencing or dashboard edits in Grafana, no writes or DDL on Postgres, no reading card data), in a form a security reviewer could check.",
72
+ "Does not invent MCP server package names, repository URLs or tool names for Sentry, Grafana, GitLab, Linear or Postgres; it either says to use the vendor's officially published server (to be confirmed by the user) or describes the capability generically, and notes that self-hosted Sentry may need verification that the official server supports a custom host."
73
+ ]
74
+ }
75
+ ]
76
+ }
@@ -0,0 +1,32 @@
1
+ {
2
+ "mcpServers": {
3
+ "github": {
4
+ "type": "http",
5
+ "url": "https://github-mcp.example.invalid/mcp",
6
+ "headers": { "Authorization": "Bearer tok_FAKE_9f3c_example_not_real" },
7
+ "note": "classic token, scopes: repo, admin:org, workflow"
8
+ },
9
+ "jira": {
10
+ "command": "npx",
11
+ "args": ["-y", "@harbourline/jira-mcp"],
12
+ "env": {
13
+ "JIRA_URL": "https://brightwell.atlassian.example.invalid",
14
+ "JIRA_TOKEN": "tok_FAKE_41aa_example_not_real"
15
+ },
16
+ "note": "found on a blog post, does create/update/transition/comment"
17
+ },
18
+ "postgres": {
19
+ "command": "npx",
20
+ "args": ["-y", "@harbourline/pg-mcp@latest"],
21
+ "env": {
22
+ "DATABASE_URL": "postgres://app_owner:FAKE_PASSWORD@orders-primary.internal.example.invalid:5432/orders"
23
+ }
24
+ },
25
+ "slack": {
26
+ "command": "npx",
27
+ "args": ["-y", "@harbourline/slack-mcp"],
28
+ "env": { "SLACK_TOKEN": "tok_FAKE_77de_example_not_real" },
29
+ "note": "bot scopes: channels:history, groups:history, im:history, chat:write, admin"
30
+ }
31
+ }
32
+ }
@@ -0,0 +1,47 @@
1
+ {
2
+ "mcpServers": {
3
+ "github": {
4
+ "command": "docker",
5
+ "args": ["run", "-i", "--rm", "-e", "GITHUB_PERSONAL_ACCESS_TOKEN", "ghcr.io/github/github-mcp-server"],
6
+ "env": {
7
+ "GITHUB_PERSONAL_ACCESS_TOKEN": "ghp_EXAMPLEEXAMPLEEXAMPLEEXAMPLE0001"
8
+ },
9
+ "_comment": "classic PAT, scopes: repo, workflow, admin:org, delete_repo -- easiest to just give it everything"
10
+ },
11
+ "github-reviewer": {
12
+ "command": "docker",
13
+ "args": ["run", "-i", "--rm", "-e", "GITHUB_PERSONAL_ACCESS_TOKEN", "ghcr.io/github/github-mcp-server"],
14
+ "env": {
15
+ "GITHUB_PERSONAL_ACCESS_TOKEN": "ghp_EXAMPLEEXAMPLEEXAMPLEEXAMPLE0001",
16
+ "GITHUB_TOOLSETS": "all"
17
+ },
18
+ "_comment": "used by the review stage so it can approve + merge when it's happy"
19
+ },
20
+ "jira": {
21
+ "command": "npx",
22
+ "args": ["-y", "jira-mcp-plus-unofficial@latest"],
23
+ "env": {
24
+ "JIRA_URL": "https://example-team.atlassian.net",
25
+ "JIRA_EMAIL": "build-bot@example.com",
26
+ "JIRA_API_TOKEN": "EXAMPLE-JIRA-TOKEN-DO-NOT-USE"
27
+ },
28
+ "_comment": "found this on a forum thread, works better than the official one"
29
+ },
30
+ "slack": {
31
+ "command": "npx",
32
+ "args": ["-y", "slack-mcp-server-community"],
33
+ "env": {
34
+ "SLACK_BOT_TOKEN": "xoxb-EXAMPLE-EXAMPLE-EXAMPLE",
35
+ "_scopes": "chat:write, chat:write.public, channels:history, groups:history, im:history, users:read.email, admin"
36
+ }
37
+ },
38
+ "postgres": {
39
+ "command": "npx",
40
+ "args": ["-y", "some-postgres-mcp"],
41
+ "env": {
42
+ "DATABASE_URL": "postgresql://app_owner:EXAMPLEPASSWORD@prod-db.internal.example:5432/orders"
43
+ },
44
+ "_comment": "handy for the implement stage to check schemas"
45
+ }
46
+ }
47
+ }
@@ -0,0 +1,12 @@
1
+ # Our agent PR loop
2
+
3
+ The coding agent runs this loop for every ticket in the "Agent-ready" Jira column.
4
+
5
+ 1. **Pick up** - read the Jira ticket (description, acceptance criteria, linked tickets). Move it to "In Progress".
6
+ 2. **Implement** - branch off `main`, write the change and tests, push the branch, open a draft PR on GitHub.
7
+ 3. **Review** - a second agent session reads the PR diff, the CI results and the ticket, and decides if it meets the acceptance criteria.
8
+ 4. **Merge gate** - a human tech lead approves and merges. Nothing merges without a human.
9
+ 5. **Announce** - post a one-line summary with the PR link to `#team-orders-dev` in Slack.
10
+
11
+ Repo: one GitHub repo, `example-org/orders-service`. CI is GitHub Actions.
12
+ The `.mcp.json` lives in the repo root and is committed so everyone gets the same setup.