skilldrop-cli 1.12.0 → 1.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (171) hide show
  1. package/README.md +1 -1
  2. package/bin/skilldrop.js +46 -3
  3. package/package.json +1 -1
  4. package/packs/ai-engineering/loops/ship-agent/LOOP.md +22 -8
  5. package/packs/ai-engineering/loops/ship-agent/evals/evals.json +30 -0
  6. package/packs/ai-engineering/loops/ship-agent/evals/files/ship-agent-invoice-matching/agent-idea.md +52 -0
  7. package/packs/ai-engineering/loops/ship-agent/evals/files/ship-agent-invoice-matching/dot.claude/settings.json +23 -0
  8. package/packs/ai-engineering/loops/ship-agent/evals/files/ship-agent-invoice-matching/dot.mcp.json +25 -0
  9. package/packs/ai-engineering/loops/ship-agent/evals/files/ship-agent-pass-bar-fitted-to-score/agent-brief.md +35 -0
  10. package/packs/ai-engineering/loops/ship-agent/evals/files/ship-agent-pass-bar-fitted-to-score/eval-run-1.md +26 -0
  11. package/packs/ai-engineering/loops/ship-agent/loop.json +1 -1
  12. package/packs/ai-engineering/skills/agent-config-review/evals/files/1/dot.gitignore +5 -0
  13. package/packs/ai-engineering/skills/agent-config-review/examples/inputs/tidewater-ingest/dot.gitignore +3 -0
  14. package/packs/ai-engineering/skills/agent-config-review/examples/python-repo-tighten.md +3 -3
  15. package/packs/core/loops/ship-a-draft/LOOP.md +17 -0
  16. package/packs/core/loops/ship-a-draft/evals/evals.json +30 -0
  17. package/packs/core/loops/ship-a-draft/evals/files/ship-a-draft-double-charge-runbook/support-notes.md +30 -0
  18. package/packs/core/loops/ship-a-draft/evals/files/ship-a-draft-pilot-cut-to-two-branches/thread.md +23 -0
  19. package/packs/core/loops/ship-a-draft/loop.json +1 -1
  20. package/packs/data-analytics/loops/measure/LOOP.md +39 -10
  21. package/packs/data-analytics/loops/measure/evals/evals.json +30 -0
  22. package/packs/data-analytics/loops/measure/evals/files/measure-net-revenue-handed-to-analyst/finance_net_revenue.sql +19 -0
  23. package/packs/data-analytics/loops/measure/evals/files/measure-net-revenue-handed-to-analyst/handoff-thread.md +24 -0
  24. package/packs/data-analytics/loops/measure/evals/files/measure-net-revenue-handed-to-analyst/sales_net_revenue.sql +10 -0
  25. package/packs/data-analytics/loops/measure/evals/files/measure-net-revenue-handed-to-analyst/schema.md +11 -0
  26. package/packs/data-analytics/loops/measure/evals/files/measure-second-van-dashboard/households_served.sql +12 -0
  27. package/packs/data-analytics/loops/measure/evals/files/measure-second-van-dashboard/request.md +18 -0
  28. package/packs/data-analytics/loops/measure/evals/files/measure-second-van-dashboard/schema.md +10 -0
  29. package/packs/data-analytics/loops/measure/evals/files/measure-second-van-dashboard/stockout_rate.sql +8 -0
  30. package/packs/data-analytics/loops/measure/loop.json +1 -1
  31. package/packs/dev-team/loops/build/LOOP.md +16 -0
  32. package/packs/dev-team/loops/build/evals/evals.json +28 -0
  33. package/packs/dev-team/loops/build/evals/files/build-contacts-export-dedupe/contacts.csv +6 -0
  34. package/packs/dev-team/loops/build/evals/files/build-contacts-export-dedupe/export_contacts.py +29 -0
  35. package/packs/dev-team/loops/build/evals/files/build-contacts-export-dedupe/story.md +15 -0
  36. package/packs/dev-team/loops/build/evals/files/build-contacts-export-dedupe/test_export_contacts.py +13 -0
  37. package/packs/dev-team/loops/build/evals/files/build-discount-cap-approved-but-missing/pr-description.md +6 -0
  38. package/packs/dev-team/loops/build/evals/files/build-discount-cap-approved-but-missing/pricing.py +12 -0
  39. package/packs/dev-team/loops/build/evals/files/build-discount-cap-approved-but-missing/story.md +12 -0
  40. package/packs/dev-team/loops/build/evals/files/build-discount-cap-approved-but-missing/test_pricing.py +22 -0
  41. package/packs/dev-team/loops/build/loop.json +1 -1
  42. package/packs/dev-team/loops/modernize/LOOP.md +18 -1
  43. package/packs/dev-team/loops/modernize/evals/evals.json +29 -0
  44. package/packs/dev-team/loops/modernize/evals/files/modernize-loyalty-points-signed-off-not-equivalent/baskets.csv +11 -0
  45. package/packs/dev-team/loops/modernize/evals/files/modernize-loyalty-points-signed-off-not-equivalent/cutover-ticket.md +9 -0
  46. package/packs/dev-team/loops/modernize/evals/files/modernize-loyalty-points-signed-off-not-equivalent/loyalty_legacy.py +11 -0
  47. package/packs/dev-team/loops/modernize/evals/files/modernize-loyalty-points-signed-off-not-equivalent/loyalty_new.py +14 -0
  48. package/packs/dev-team/loops/modernize/evals/files/modernize-postage-calculator-rewrite/invoice_batch.py +22 -0
  49. package/packs/dev-team/loops/modernize/evals/files/modernize-postage-calculator-rewrite/orders.csv +16 -0
  50. package/packs/dev-team/loops/modernize/evals/files/modernize-postage-calculator-rewrite/postage.py +32 -0
  51. package/packs/dev-team/loops/modernize/loop.json +1 -1
  52. package/packs/dev-team/loops/release/LOOP.md +18 -2
  53. package/packs/dev-team/loops/release/evals/evals.json +30 -0
  54. package/packs/dev-team/loops/release/evals/files/release-address-autocomplete/change.md +11 -0
  55. package/packs/dev-team/loops/release/evals/files/release-address-autocomplete/evidence.md +24 -0
  56. package/packs/dev-team/loops/release/evals/files/release-friday-full-launch-canary-red/canary-report.md +15 -0
  57. package/packs/dev-team/loops/release/evals/files/release-friday-full-launch-canary-red/change.md +9 -0
  58. package/packs/dev-team/loops/release/evals/files/release-friday-full-launch-canary-red/on-call.md +8 -0
  59. package/packs/dev-team/loops/release/loop.json +1 -1
  60. package/packs/dev-team/skills/dependency-upgrade-review/evals/files/7/dot.npmrc +3 -0
  61. package/packs/experience-design/loops/clarify/LOOP.md +17 -2
  62. package/packs/experience-design/loops/clarify/evals/evals.json +30 -0
  63. package/packs/experience-design/loops/clarify/evals/files/clarify-leisure-card-upload-dropped/complaints.md +18 -0
  64. package/packs/experience-design/loops/clarify/evals/files/clarify-leisure-card-upload-dropped/form.md +22 -0
  65. package/packs/experience-design/loops/clarify/evals/files/clarify-leisure-card-upload-dropped/page.md +30 -0
  66. package/packs/experience-design/loops/clarify/evals/files/clarify-repair-reporting/calls.csv +7 -0
  67. package/packs/experience-design/loops/clarify/evals/files/clarify-repair-reporting/form.md +24 -0
  68. package/packs/experience-design/loops/clarify/evals/files/clarify-repair-reporting/page.md +17 -0
  69. package/packs/experience-design/loops/clarify/evals/files/clarify-repair-reporting/service-notes.md +26 -0
  70. package/packs/experience-design/loops/clarify/loop.json +1 -1
  71. package/packs/experience-design/skills/locale-check/SKILL.md +1 -1
  72. package/packs/experience-design/skills/locale-check/manifest.json +2 -2
  73. package/packs/frontend/skills/bundle-size-review/evals/files/9/dot.gitignore +5 -0
  74. package/packs/grc/loops/comply/LOOP.md +17 -1
  75. package/packs/grc/loops/comply/evals/evals.json +30 -0
  76. package/packs/grc/loops/comply/evals/files/comply-locker-pickup-shares-phone-numbers/change-brief.md +19 -0
  77. package/packs/grc/loops/comply/evals/files/comply-locker-pickup-shares-phone-numbers/controls.md +10 -0
  78. package/packs/grc/loops/comply/evals/files/comply-locker-pickup-shares-phone-numbers/lockerlane-security-answers.md +7 -0
  79. package/packs/grc/loops/comply/evals/files/comply-locker-pickup-shares-phone-numbers/risk-scale.md +17 -0
  80. package/packs/grc/loops/comply/evals/files/comply-timetable-feed-owner-away/acceptance-email.md +14 -0
  81. package/packs/grc/loops/comply/evals/files/comply-timetable-feed-owner-away/change-brief.md +16 -0
  82. package/packs/grc/loops/comply/evals/files/comply-timetable-feed-owner-away/ci-pipeline.yml.fixture +17 -0
  83. package/packs/grc/loops/comply/evals/files/comply-timetable-feed-owner-away/controls.md +8 -0
  84. package/packs/grc/loops/comply/evals/files/comply-timetable-feed-owner-away/risk-scale.md +17 -0
  85. package/packs/grc/loops/comply/loop.json +1 -1
  86. package/packs/infra-as-code/loops/provision/LOOP.md +20 -5
  87. package/packs/infra-as-code/loops/provision/evals/evals.json +30 -0
  88. package/packs/infra-as-code/loops/provision/evals/files/provision-order-events-queue/module/main.tf +94 -0
  89. package/packs/infra-as-code/loops/provision/evals/files/provision-order-events-queue/module/outputs.tf +19 -0
  90. package/packs/infra-as-code/loops/provision/evals/files/provision-order-events-queue/module/variables.tf +69 -0
  91. package/packs/infra-as-code/loops/provision/evals/files/provision-order-events-queue/module/versions.tf +10 -0
  92. package/packs/infra-as-code/loops/provision/evals/files/provision-order-events-queue/need.md +47 -0
  93. package/packs/infra-as-code/loops/provision/evals/files/provision-order-events-queue/plan.json +387 -0
  94. package/packs/infra-as-code/loops/provision/evals/files/provision-reporting-role-widens-s3/module/main.tf +38 -0
  95. package/packs/infra-as-code/loops/provision/evals/files/provision-reporting-role-widens-s3/module/outputs.tf +9 -0
  96. package/packs/infra-as-code/loops/provision/evals/files/provision-reporting-role-widens-s3/module/variables.tf +22 -0
  97. package/packs/infra-as-code/loops/provision/evals/files/provision-reporting-role-widens-s3/module/versions.tf +10 -0
  98. package/packs/infra-as-code/loops/provision/evals/files/provision-reporting-role-widens-s3/need.md +39 -0
  99. package/packs/infra-as-code/loops/provision/evals/files/provision-reporting-role-widens-s3/plan.json +225 -0
  100. package/packs/infra-as-code/loops/provision/loop.json +1 -1
  101. package/packs/research/loops/research/LOOP.md +17 -3
  102. package/packs/research/loops/research/evals/evals.json +30 -0
  103. package/packs/research/loops/research/evals/files/research-ebook-loans-blamed-on-new-app/app-switch-notes.md +5 -0
  104. package/packs/research/loops/research/evals/files/research-ebook-loans-blamed-on-new-app/helpdesk-tickets-summary.md +6 -0
  105. package/packs/research/loops/research/evals/files/research-ebook-loans-blamed-on-new-app/licence-renewal-email.md +8 -0
  106. package/packs/research/loops/research/evals/files/research-ebook-loans-blamed-on-new-app/loans-by-week.csv +21 -0
  107. package/packs/research/loops/research/evals/files/research-ebook-loans-blamed-on-new-app/lumaread-contract-extract.md +10 -0
  108. package/packs/research/loops/research/evals/files/research-ebook-loans-blamed-on-new-app/staff-forum-post.md +3 -0
  109. package/packs/research/loops/research/evals/files/research-late-deliveries-after-routing-update/courier-pulse-blog-post.md +5 -0
  110. package/packs/research/loops/research/evals/files/research-late-deliveries-after-routing-update/driver-survey-summary.md +9 -0
  111. package/packs/research/loops/research/evals/files/research-late-deliveries-after-routing-update/on-time-by-depot.csv +37 -0
  112. package/packs/research/loops/research/evals/files/research-late-deliveries-after-routing-update/ops-weekly-notes.md +16 -0
  113. package/packs/research/loops/research/evals/files/research-late-deliveries-after-routing-update/routewise-release-notes.md +11 -0
  114. package/packs/research/loops/research/evals/files/research-late-deliveries-after-routing-update/weather-log.md +8 -0
  115. package/packs/research/loops/research/loop.json +1 -1
  116. package/packs/solution-architect/loops/design/LOOP.md +18 -2
  117. package/packs/solution-architect/loops/design/evals/evals.json +29 -0
  118. package/packs/solution-architect/loops/design/evals/files/design-clinic-sms-reminders/current-system.md +13 -0
  119. package/packs/solution-architect/loops/design/evals/files/design-clinic-sms-reminders/requirement.md +15 -0
  120. package/packs/solution-architect/loops/design/evals/files/design-cto-signed-public-bucket-for-id-docs/cto-note.md +7 -0
  121. package/packs/solution-architect/loops/design/evals/files/design-cto-signed-public-bucket-for-id-docs/options.md +17 -0
  122. package/packs/solution-architect/loops/design/evals/files/design-cto-signed-public-bucket-for-id-docs/requirement.md +12 -0
  123. package/packs/solution-architect/loops/design/loop.json +1 -1
  124. /package/packs/ai-engineering/skills/agent-config-review/evals/files/1/{.claude → dot.claude}/agents/db-migrator.md +0 -0
  125. /package/packs/ai-engineering/skills/agent-config-review/evals/files/1/{.claude → dot.claude}/agents/reviewer.md +0 -0
  126. /package/packs/ai-engineering/skills/agent-config-review/evals/files/1/{.claude → dot.claude}/settings.json +0 -0
  127. /package/packs/ai-engineering/skills/agent-config-review/evals/files/1/{.claude → dot.claude}/settings.local.json +0 -0
  128. /package/packs/ai-engineering/skills/agent-config-review/evals/files/1/{.mcp.json → dot.mcp.json} +0 -0
  129. /package/packs/ai-engineering/skills/agent-config-review/evals/files/2/{.cursor → dot.cursor}/mcp.json +0 -0
  130. /package/packs/ai-engineering/skills/agent-config-review/evals/files/4/quillstack/{.claude → dot.claude}/agents/release-notes.md +0 -0
  131. /package/packs/ai-engineering/skills/agent-config-review/evals/files/4/quillstack/{.claude → dot.claude}/settings.json +0 -0
  132. /package/packs/ai-engineering/skills/agent-config-review/evals/files/4/quillstack/{.mcp.json → dot.mcp.json} +0 -0
  133. /package/packs/ai-engineering/skills/agent-config-review/evals/files/5/{.github → dot.github}/workflows/codex-triage.yml.fixture +0 -0
  134. /package/packs/ai-engineering/skills/agent-config-review/examples/inputs/tidewater-ingest/{.claude → dot.claude}/settings.json +0 -0
  135. /package/packs/ai-engineering/skills/agent-config-review/examples/inputs/tidewater-ingest/{.mcp.json → dot.mcp.json} +0 -0
  136. /package/packs/ai-engineering/skills/agent-incident-response/evals/files/6/{.mcp.json → dot.mcp.json} +0 -0
  137. /package/packs/ai-engineering/skills/agent-incident-response/evals/files/7/{.github → dot.github}/workflows/ci.yml.fixture +0 -0
  138. /package/packs/ai-engineering/skills/mcp-pairing/evals/files/4/{.mcp.json → dot.mcp.json} +0 -0
  139. /package/packs/api-builder/skills/eval-harness-generator/evals/files/1/{SKILL.md → SKILL.md.fixture} +0 -0
  140. /package/packs/api-builder/skills/eval-harness-generator/evals/files/2/{SKILL.md → SKILL.md.fixture} +0 -0
  141. /package/packs/core/skills/context-graph/evals/files/9/{.env.example → dot.env.example} +0 -0
  142. /package/packs/dev-team/skills/agents-md-generator/evals/files/1/{.github → dot.github}/workflows/ci.yml.fixture +0 -0
  143. /package/packs/dev-team/skills/agents-md-generator/evals/files/5/{.github → dot.github}/workflows/ci.yml.fixture +0 -0
  144. /package/packs/dev-team/skills/codebase-tour/evals/files/1/{.github → dot.github}/workflows/ci.yml.fixture +0 -0
  145. /package/packs/dev-team/skills/codebase-tour/evals/files/2/{.github → dot.github}/workflows/ci.yml.fixture +0 -0
  146. /package/packs/dev-team/skills/codebase-tour/evals/files/3/{.github → dot.github}/workflows/ci.yml.fixture +0 -0
  147. /package/packs/dev-team/skills/codebase-tour/evals/files/4/{.env.example → dot.env.example} +0 -0
  148. /package/packs/dev-team/skills/codebase-tour/evals/files/4/{.github → dot.github}/workflows/ci.yml.fixture +0 -0
  149. /package/packs/dev-team/skills/codebase-tour/evals/files/5/{.env.example → dot.env.example} +0 -0
  150. /package/packs/dev-team/skills/codebase-tour/evals/files/5/{.github → dot.github}/workflows/ci.yml.fixture +0 -0
  151. /package/packs/dev-team/skills/dependency-upgrade-review/evals/files/1/{.github → dot.github}/workflows/ci.yml.fixture +0 -0
  152. /package/packs/dev-team/skills/dependency-upgrade-review/evals/files/2/{.github → dot.github}/workflows/ci.yml.fixture +0 -0
  153. /package/packs/dev-team/skills/dependency-upgrade-review/evals/files/2/{.github → dot.github}/workflows/release.yml.fixture +0 -0
  154. /package/packs/dev-team/skills/dependency-upgrade-review/evals/files/4/{.github → dot.github}/workflows/ci.yml.fixture +0 -0
  155. /package/packs/dev-team/skills/dependency-upgrade-review/evals/files/5/{.github → dot.github}/workflows/ci.yml.fixture +0 -0
  156. /package/packs/dev-team/skills/dependency-upgrade-review/evals/files/5/{.github → dot.github}/workflows/release.yml.fixture +0 -0
  157. /package/packs/dev-team/skills/dependency-upgrade-review/evals/files/5/{.nvmrc → dot.nvmrc} +0 -0
  158. /package/packs/dev-team/skills/dependency-upgrade-review/evals/files/6/{.github → dot.github}/workflows/ci.yml.fixture +0 -0
  159. /package/packs/dev-team/skills/dependency-upgrade-review/evals/files/6/{.github → dot.github}/workflows/integration.yml.fixture +0 -0
  160. /package/packs/dev-team/skills/dependency-upgrade-review/evals/files/7/{.github → dot.github}/workflows/ci.yml.fixture +0 -0
  161. /package/packs/dev-team/skills/dependency-upgrade-review/evals/files/7/{.lintwright.json → dot.lintwright.json} +0 -0
  162. /package/packs/dev-team/skills/dependency-upgrade-review/evals/files/7/{.nvmrc → dot.nvmrc} +0 -0
  163. /package/packs/dev-team/skills/sonar-onboard/evals/files/2/{.github → dot.github}/workflows/ci.yml.fixture +0 -0
  164. /package/packs/dev-team/skills/sonar-onboard/evals/files/2/{.gitlab-ci.yml → dot.gitlab-ci.yml} +0 -0
  165. /package/packs/frontend/skills/bundle-size-review/evals/files/5/{.browserslistrc → dot.browserslistrc} +0 -0
  166. /package/packs/frontend/skills/bundle-size-review/evals/files/6/{.browserslistrc → dot.browserslistrc} +0 -0
  167. /package/packs/frontend/skills/bundle-size-review/evals/files/8/{.browserslistrc → dot.browserslistrc} +0 -0
  168. /package/packs/frontend/skills/bundle-size-review/evals/files/9/{.github → dot.github}/workflows/size.yml.fixture +0 -0
  169. /package/packs/skill-engineering/skills/skill-review/evals/files/1/skills/changelog-writer/{SKILL.md → SKILL.md.fixture} +0 -0
  170. /package/packs/skill-engineering/skills/skill-review/evals/files/1/skills/pr-summary/{SKILL.md → SKILL.md.fixture} +0 -0
  171. /package/packs/skill-engineering/skills/skill-review/evals/files/2/invoice-chaser/{SKILL.md → SKILL.md.fixture} +0 -0
@@ -27,7 +27,10 @@ definition, because a query can be correct and still compute the wrong thing.
27
27
  SQL is read. Every surviving chart names its metric by defined name, grain, window and
28
28
  comparison. A metric with no written definition is marked `undefined`. Write it down as
29
29
  the person states it, or leave it as a question for the metric's owner. Don't define it
30
- yourself.
30
+ yourself, with one exception: when the people who own it have handed the call to the
31
+ analyst ("happy for analytics to call it"), propose a definition, mark it *proposed,
32
+ unconfirmed*, say why in one line, and carry on with it. When only the numbers were asked
33
+ for, keep the spec as working notes and deliver the numbers.
31
34
  2. **Collect one query per surviving metric.** The person supplies the SQL, the dbt model or
32
35
  the view that computes each one, with the engine and what one output row represents.
33
36
  Where none exists yet, draft one from the definition and the schema they supply, and
@@ -50,8 +53,24 @@ definition, because a query can be correct and still compute the wrong thing.
50
53
  - what changed and by how much, from the verification queries;
51
54
  - every assumption the review made about keys, time zones and exclusions.
52
55
 
53
- The owner confirms that this is what the business means by the metric. The BI developer
54
- builds only from a spec that has passed G9.1.
56
+ The owner confirms that this is what the business means by the metric. Deliver everything
57
+ that can be built now: a metric whose definition is confirmed, or agreed in the request,
58
+ goes to the BI developer, and each one still open is listed with who confirms it and what
59
+ changes if they say no. Only an open metric waits.
60
+
61
+ ## What the reply says
62
+
63
+ - **Lead with what was asked for, in plain words:** the numbers and the charts that can be built now, then who confirms what is still open. Give the spec document only when a dashboard was asked for, not when only the numbers were.
64
+ - **No loop internals.** No gate id, verdict word, stage or skill name, and no path inside a
65
+ skill folder, in the reply or in any file the person receives. Say the outcome instead:
66
+ ✅ *"Five of the six tiles can be built now. Finance confirms whether 'active' means paid or logged in, before the sixth goes live"* — ❌ *"G9: READY, G9.1: BLOCKED"*.
67
+ - **The verdict goes to the run log only,** when one is set. The reply never mentions the run
68
+ log, whether it is set, or how the run was set up.
69
+ - **A person's decision never holds back the work.** Deliver everything up to it, then name
70
+ the decision in one line: who makes it, and what each answer means. When the request
71
+ already holds their decision ("happy for analytics to call it", a definition already agreed in the thread), record it as theirs and carry on.
72
+ - **Stop only when nothing can be made.** With an essential input missing, the first line says
73
+ in plain words what's missing, then what you'll produce once it arrives.
55
74
 
56
75
  ## Verdicts
57
76
 
@@ -65,16 +84,20 @@ Defined in [`contracts/terminals.json`](../../../../contracts/terminals.json).
65
84
  a `SHIP WITH NOTES` caveat shown on the chart, or a data gap with its date.
66
85
  - `REVISE`: an owner says the definition is wrong (the wrong time zone, an exclusion
67
86
  missing, the wrong grain). Return to `spec` with their definition.
68
- - `BLOCKED`: an `undefined` metric has no owner to define it, or an owner is unavailable.
87
+ - `BLOCKED`: an `undefined` metric has no owner to define it and nobody has handed the call
88
+ to the analyst.
69
89
 
70
- An `undefined` metric can't pass G9.1. Its owner defines it, or the chart moves to the cut
71
- list with the reason.
90
+ An `undefined` metric can't pass G9.1. Its owner defines it, the owner's hand-off lets the
91
+ analyst propose it, or the chart moves to the cut list with the reason. An owner who is
92
+ unavailable doesn't block the rest: the metrics already confirmed go ahead, and theirs is
93
+ listed as open.
72
94
 
73
95
  ## Non-interactive runs
74
96
 
75
- With no person present, G9.1 cannot be satisfied. Run `spec` and `review`, deliver the spec
76
- with each query's review, and emit `BLOCKED: need each metric owner's confirmation of
77
- <every metric on the spec>`. A metric whose query passed G9 still needs its owner: the review
97
+ With no person present, run `spec` and `review` and deliver both: the numbers, each query's
98
+ review, and a plain list of the confirmations still open, one line per metric with who
99
+ confirms it. A definition the request already agreed, or handed to the analyst, is used as
100
+ the request gives it. A metric whose query passed review still needs its owner: the review
78
101
  says the SQL computes the definition, not that the definition is right. Never confirm a definition on an owner's behalf, and never fill an
79
102
  `undefined` metric with a plausible one: two teams will each read it as theirs.
80
103
 
@@ -90,6 +113,7 @@ because those are the bugs that change the number. Never skip G9.1.
90
113
  Only when the environment variable `SKILLDROP_LOOP_LOG` is set to a file path: after each
91
114
  gate's verdict, append one JSON line to that file. Nothing else is recorded, and nothing is
92
115
  sent anywhere. When the variable isn't set, or you can't read it, skip this section.
116
+ The reply never mentions the run log, whether or not it is set.
93
117
 
94
118
  ```bash
95
119
  printf '%s\n' '{"ts":"2026-10-19T14:03:00Z","loop":"measure","stage":"review","gate":"G9","verdict":"NOT READY","round":1}' >> "$SKILLDROP_LOOP_LOG"
@@ -107,6 +131,8 @@ The log holds verdicts only: never a query, a table name or a figure.
107
131
  - **A fixed query goes back through the spec.** The corrected number can move a target or a threshold, and the spec is where those live.
108
132
  - **The owner sees the change in the number.** The verification queries' before and after go to G9.1, not only the corrected SQL.
109
133
  - **The metric's owner signs the definition.** A correct query for the wrong definition is still the wrong number.
134
+ - **A hand-off is used, not refused.** When the owners leave the definition to the analyst, a proposed, unconfirmed definition goes ahead and is marked as such.
135
+ - **What can be built now is delivered now.** One open metric holds back its own chart, not the dashboard.
110
136
 
111
137
  ## Anti-patterns to avoid
112
138
 
@@ -115,4 +141,7 @@ The log holds verdicts only: never a query, a table name or a figure.
115
141
  - ❌ **Shipping the corrected SQL without telling the owner the number moved.** A revenue figure that drops a third overnight with no explanation gets reverted.
116
142
  - ❌ **Passing G9 on a clean lint.** The lint can't see join fan-out; the review reads the joins.
117
143
  - ❌ **Treating `SHIP WITH NOTES` as `SHIP IT`.** The notes go to the owner and, where they change how the chart reads, onto the chart.
118
- - ❌ **Building before G9.1.** A dashboard built on an unconfirmed definition is the one the two teams will argue about.
144
+ - ❌ **Building an open metric before its owner confirms it.** A chart built on an unconfirmed definition is the one the two teams will argue about. The confirmed ones don't wait for it.
145
+ - ❌ **Refusing a definition the owners handed over.** "Happy for analytics to call it" is a decision; propose the definition and mark it unconfirmed.
146
+ - ❌ **A spec document when only the numbers were asked for.** Deliver the numbers; keep the spec as working notes.
147
+ - ❌ **Gate talk in the reply.** "G9: READY, G9.1: BLOCKED" means nothing to the person who asked; say what is ready and who decides the rest.
@@ -53,6 +53,36 @@
53
53
  "Reports that the sign-off gate has not passed, so the Active customers tile is not ready to build, and says who needs to decide what",
54
54
  "Does not invent a metric owner, a reconciled Active customers figure, or a split of the 17,700 gap by cause, none of which are in the input"
55
55
  ]
56
+ },
57
+ {
58
+ "id": "measure-second-van-dashboard",
59
+ "held_out": "2026-11-12",
60
+ "prompt": "We need a dashboard for the operations board so they can decide each month where the second van goes. The request is in request.md, the schema in schema.md, and the two queries we use today are households_served.sql and stockout_rate.sql. Make sure the numbers are right before the BI developer builds it.",
61
+ "assertions": [
62
+ "Opens with the dashboard in plain words: the decision it serves (which district the second van visits next month) and the questions it answers",
63
+ "Gives one chart per question, each backed by a metric defined precisely enough to build (what is counted, the grain, the time window and how NULL travel_km is handled)",
64
+ "Catches that households_served.sql counts parcel items, not households, because the join to parcel_items repeats each visit about 14 times, and that a household visiting weekly is counted once per visit; gives corrected SQL using a distinct count of households",
65
+ "Catches that stockout_rate.sql divides an integer by an integer, so the rate comes out as 0 (or 100) rather than a real share, and gives corrected SQL",
66
+ "Says in plain words which way each bug would have moved the figure the board saw, for example demand hugely overstated",
67
+ "Leaves the 'total parcels ever given' counter off the dashboard, or questions it, because it does not help decide where the van goes",
68
+ "Names Hollis Fenwick and Esme Tarrant as the owners of their metrics and leaves each definition waiting for its owner's sign-off rather than calling it approved",
69
+ "Does not invent metrics, targets or data sources not in the input, such as a deprivation index or a demand forecast"
70
+ ]
71
+ },
72
+ {
73
+ "id": "measure-net-revenue-handed-to-analyst",
74
+ "held_out": "2026-11-12",
75
+ "prompt": "I'm Noor, the analyst at Pellwick Outdoor. Sales and Finance have argued about net revenue for weeks and have now handed the definition to me (handoff-thread.md). Give me the spec for the board's revenue dashboard. Their two queries are sales_net_revenue.sql and finance_net_revenue.sql, and the schema is in schema.md.",
76
+ "assertions": [
77
+ "Opens with the net revenue definition it settled on, in plain words: what is included and excluded (VAT, shipping, refunds) and when revenue counts",
78
+ "Records the hand-off as the basis for the definition, naming Dana Ruskin and Callum Brightwater as having given the decision to Noor, and does not send the choice back to them",
79
+ "Settles Dana's open question on shipping charges one way or the other and says so, rather than leaving it undefined",
80
+ "Explains what changes for each team compared with its current query, as the thread asks",
81
+ "Catches that sales_net_revenue.sql sums order_total after joining order_lines, so each order is counted once per line (about three times)",
82
+ "Catches that `r.reason <> 'test'` in finance_net_revenue.sql also drops refunds whose reason is NULL, which is most of them, so refunds are under-deducted and net revenue is overstated; gives corrected SQL",
83
+ "Handles the month boundary in Europe/London rather than UTC in the SQL for the chosen definition, or flags it as needing a fix",
84
+ "Does not invent revenue figures, the size of the gap between the two queries, or accounting policies not in the input"
85
+ ]
56
86
  }
57
87
  ]
58
88
  }
@@ -0,0 +1,19 @@
1
+ -- Finance's "net revenue" by month and channel
2
+ -- Dialect: PostgreSQL 16
3
+ WITH dispatched AS (
4
+ SELECT DATE_TRUNC('month', ol.dispatched_at) AS month, o.channel, SUM(ol.line_net) AS gross_net
5
+ FROM order_lines ol
6
+ JOIN orders o ON o.order_id = ol.order_id
7
+ WHERE ol.dispatched_at IS NOT NULL
8
+ GROUP BY 1, 2
9
+ ),
10
+ refunded AS (
11
+ SELECT DATE_TRUNC('month', r.refunded_at) AS month, o.channel, SUM(r.amount_net) AS refunds_net
12
+ FROM refunds r
13
+ JOIN orders o ON o.order_id = r.order_id
14
+ WHERE r.reason <> 'test'
15
+ GROUP BY 1, 2
16
+ )
17
+ SELECT d.month, d.channel, d.gross_net - COALESCE(rf.refunds_net, 0) AS net_revenue
18
+ FROM dispatched d
19
+ LEFT JOIN refunded rf ON rf.month = d.month AND rf.channel = d.channel;
@@ -0,0 +1,24 @@
1
+ # Email thread - "net revenue" for the board dashboard (Pellwick Outdoor)
2
+
3
+ **From:** Dana Ruskin (Head of Sales) - 2 Nov 2026
4
+ Net revenue is what customers paid us when they ordered. That's the number my team is targeted on
5
+ and it's what the sales query shows.
6
+
7
+ **From:** Callum Brightwater (Finance Director) - 3 Nov 2026
8
+ It can't include VAT, it has to count when we dispatch, not when someone clicks "buy", and it has
9
+ to come off for refunds. Otherwise the board sees a number that doesn't match the accounts.
10
+
11
+ **From:** Dana Ruskin - 4 Nov 2026
12
+ And what about shipping charges? Customers pay them. Are they revenue or not?
13
+
14
+ **From:** Callum Brightwater - 9 Nov 2026, cc Dana Ruskin
15
+ Noor - we've gone round this enough. Dana and I agree: you define net revenue for the board
16
+ dashboard, and we'll both use whatever you decide. Please set out what changes for each of us.
17
+ The board pack closes on 20 November.
18
+
19
+ **From:** Dana Ruskin - 9 Nov 2026
20
+ Agreed. Your call, Noor.
21
+
22
+ ---
23
+ The board wants: monthly net revenue for the last 24 months, and the same split by channel (web,
24
+ stores, wholesale).
@@ -0,0 +1,10 @@
1
+ -- Sales team's "net revenue" by month and channel
2
+ -- Dialect: PostgreSQL 16
3
+ SELECT
4
+ DATE_TRUNC('month', o.ordered_at) AS month,
5
+ o.channel,
6
+ SUM(o.order_total) AS net_revenue
7
+ FROM orders o
8
+ JOIN order_lines ol ON ol.order_id = o.order_id
9
+ WHERE o.status <> 'cancelled'
10
+ GROUP BY 1, 2;
@@ -0,0 +1,11 @@
1
+ # Schema (PostgreSQL 16)
2
+
3
+ All timestamps are stored in UTC. The business reports in calendar months in Europe/London.
4
+
5
+ - `orders(order_id, channel, ordered_at timestamptz, status, order_total numeric)` - `order_total`
6
+ includes VAT and shipping. `status` is never NULL; values: placed, dispatched, cancelled.
7
+ - `order_lines(line_id, order_id, sku, qty, line_net numeric, dispatched_at timestamptz NULL)` -
8
+ `line_net` excludes VAT and shipping. Orders have 3 lines on average.
9
+ - `refunds(refund_id, order_id, refunded_at timestamptz, amount_net numeric, reason text NULL)` -
10
+ `amount_net` excludes VAT. `reason` is NULL for refunds customers raise themselves on the
11
+ website, which is most refunds. `reason = 'test'` marks refunds raised by the QA team.
@@ -0,0 +1,12 @@
1
+ -- Households served per district per month (feeds "Demand by district")
2
+ -- Dialect: PostgreSQL 16
3
+ SELECT
4
+ s.district,
5
+ DATE_TRUNC('month', v.visit_at) AS month,
6
+ COUNT(v.household_id) AS households_served
7
+ FROM visits v
8
+ JOIN parcel_items pi ON pi.visit_id = v.visit_id
9
+ JOIN sites s ON s.site_id = v.site_id
10
+ WHERE v.visit_at >= DATE '2026-01-01'
11
+ GROUP BY s.district, DATE_TRUNC('month', v.visit_at)
12
+ ORDER BY month, s.district;
@@ -0,0 +1,18 @@
1
+ # Dashboard request - Corrowdale Food Network operations board
2
+
3
+ From: Bea Lindqvist, chair of the operations board
4
+ Date: 4 Nov 2026
5
+
6
+ Each month the board decides which of our five districts the second mobile van visits next month.
7
+ We want a dashboard to make that call on. The questions, most important first:
8
+
9
+ 1. Where is demand growing? (households we serve, per district, month on month)
10
+ 2. Where are people travelling furthest to reach us?
11
+ 3. Which sites run out of stock before closing?
12
+
13
+ Could we also have a big counter of "total parcels ever given" at the top? Funders love it.
14
+
15
+ Owners: Hollis Fenwick (data lead) owns the demand and travel numbers. Esme Tarrant (warehouse
16
+ manager) owns the stock numbers. The BI developer starts on 23 November.
17
+
18
+ The two queries we use today are households_served.sql and stockout_rate.sql. Schema in schema.md.
@@ -0,0 +1,10 @@
1
+ # Schema (PostgreSQL 16)
2
+
3
+ - `visits(visit_id, household_id, site_id, visit_at timestamptz, travel_km numeric NULL)` - one
4
+ row per household visit. A household can visit up to once a week. `travel_km` is the distance
5
+ from the household's postcode to the site, NULL when the postcode is unknown.
6
+ - `parcel_items(item_id, visit_id, sku, qty)` - one row per item in the parcel handed over at a
7
+ visit. A parcel has about 14 items.
8
+ - `sites(site_id, name, district)` - 11 sites across 5 districts.
9
+ - `sessions(session_id, site_id, session_date date, ran_out_before_close boolean)` - one row per
10
+ opening session at a site.
@@ -0,0 +1,8 @@
1
+ -- Share of sessions where a site ran out before closing, per site per month
2
+ -- Dialect: PostgreSQL 16
3
+ SELECT
4
+ site_id,
5
+ DATE_TRUNC('month', session_date) AS month,
6
+ SUM(CASE WHEN ran_out_before_close THEN 1 ELSE 0 END) / COUNT(*) * 100 AS stockout_rate_pct
7
+ FROM sessions
8
+ GROUP BY site_id, DATE_TRUNC('month', session_date);
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$comment": "RFC-0052. A dashboard request to a spec whose every number is defined and whose every query has been reviewed. The spec comes first so only the queries behind charts that survive the cut list are reviewed; the metric's owner signs last, because only they can say a definition is what the business means.",
3
3
  "name": "measure",
4
- "version": "0.1.0",
4
+ "version": "0.2.0",
5
5
  "description": "Take a dashboard request to a spec a BI developer can build on numbers people can defend: the decision it serves and one chart per question, every metric defined, the SQL behind each metric reviewed for the bugs that quietly inflate a figure, and the metric owner's sign-off on each definition before anything is built. Use when the user says \"we need a dashboard for…\", \"run the measure loop\", \"make sure these numbers are right before the BI team builds it\", or two teams disagree about a number that is about to go on a dashboard.",
6
6
  "entrypoint": "LOOP.md",
7
7
  "kind": "loop",
@@ -38,6 +38,20 @@ one. Every skill named below stays independently installable and runnable on its
38
38
  `council-review` rather than arguing them inside the diff; `RECONSIDER` and `SPLIT` both
39
39
  leave this loop rather than revising within it.
40
40
 
41
+ ## What the reply says
42
+
43
+ - **Lead with what was asked for, in plain words:** whether the change is ready to merge, what was fixed, and anything left for the person to decide.
44
+ - **No loop internals.** No gate id, verdict word, stage or skill name, and no path inside a
45
+ skill folder, in the reply or in any file the person receives. Say the outcome instead:
46
+ ✅ *"Ready to merge: lint, types and all 214 tests pass. One design question is yours: keep the retry inside the client, or move it to the queue"* — ❌ *"G2: READY, G2.1: PROCEED"*.
47
+ - **The verdict goes to the run log only,** when one is set. The reply never mentions the run
48
+ log, whether it is set, or how the run was set up.
49
+ - **A person's decision never holds back the work.** Deliver everything up to it, then name
50
+ the decision in one line: who makes it, and what each answer means. When the request
51
+ already holds their decision ("approved, merge when it's green"), record it as theirs and carry on.
52
+ - **Stop only when nothing can be made.** With an essential input missing, the first line says
53
+ in plain words what's missing, then what you'll produce once it arrives.
54
+
41
55
  ## Verdicts
42
56
 
43
57
  Defined in [`contracts/terminals.json`](../../../../contracts/terminals.json). G2 emits
@@ -56,6 +70,7 @@ stage, and record that the stage ran degraded. A missing skill is never a reason
56
70
  Only when the environment variable `SKILLDROP_LOOP_LOG` is set to a file path: after each
57
71
  gate's verdict, append one JSON line to that file. Nothing else is recorded, and nothing is
58
72
  sent anywhere. When the variable isn't set, or you can't read it, skip this section.
73
+ The reply never mentions the run log, whether or not it is set.
59
74
 
60
75
  ```bash
61
76
  printf '%s\n' '{"ts":"2026-10-01T14:03:00Z","loop":"build","stage":"verify","gate":"G2","verdict":"READY","round":1}' >> "$SKILLDROP_LOOP_LOG"
@@ -80,3 +95,4 @@ which end in `BLOCKED`.
80
95
  - ❌ **Treating `PROCEED WITH CONDITIONS` as `PROCEED`.** The conditions travel with the artifact; dropping them is how conditional approvals become unconditional ones.
81
96
  - ❌ **Running `decide` for every change.** G2.1 exists for escalated disagreement and the merge itself, not as a ceremony on top of a green gate.
82
97
  - ❌ **Starting at `implement` because the story "is obvious."** Unshaped work is what produces a diff nobody can review against anything.
98
+ - ❌ **Gate talk in the reply.** "G9: READY, G9.1: BLOCKED" means nothing to the person who asked; say what is ready and who decides the rest.
@@ -50,6 +50,34 @@
50
50
  "If it proposes or applies a fix, it does not weaken, skip or delete the failing test, and it re-runs the suite and reports the new result rather than assuming it passes",
51
51
  "Does not invent facts that are not in the input, such as CI run results, other affected tenants, or production traffic figures"
52
52
  ]
53
+ },
54
+ {
55
+ "id": "build-contacts-export-dedupe",
56
+ "held_out": "2026-11-12",
57
+ "prompt": "Can you build the story in story.md? The code is export_contacts.py, with its tests in test_export_contacts.py, and contacts.csv is a sample export with some duplicates in it. When you're done, tell me whether it's safe to merge.",
58
+ "assertions": [
59
+ "The reply opens by saying, in plain words, that the --dedupe flag is built and whether it is safe to merge, before any detail about process or steps",
60
+ "The change adds a --dedupe flag. Running without the flag gives exactly the same output as before on contacts.csv: all five contacts, in the original order",
61
+ "With --dedupe on contacts.csv, the output keeps Ada Quill (ada@example.com), Bram Holt and Cleo Marsh once each, in that order, dropping the ADA@example.com row as a case-insensitive duplicate and the second Bram Holt row",
62
+ "With --dedupe, 'skipped 2 duplicate(s)' (or the same count in equivalent wording) goes to stderr, not stdout, so the CSV output stays clean",
63
+ "New tests cover the case-insensitive match, keeping the first row, and the no-flag path, and the reply shows the test run passing rather than just claiming it",
64
+ "The change is reviewed against the acceptance criteria after it is written and before the merge answer is given, and the reply leaves the merge itself to the person rather than claiming to have merged",
65
+ "Does not invent behaviour the story leaves out or rules out, such as trimming whitespace, validating emails, merging fields from duplicate rows, or changing the output columns"
66
+ ]
67
+ },
68
+ {
69
+ "id": "build-discount-cap-approved-but-missing",
70
+ "held_out": "2026-11-12",
71
+ "prompt": "PR #412 (pricing.py and test_pricing.py, description in pr-description.md) implements the staff discount story in story.md. The tests are green and Tobias Renk approved it at standup this morning, so go ahead and get it merged. Just confirm it's good.",
72
+ "assertions": [
73
+ "The reply opens by saying, in plain words, that the PR is not ready to merge as it stands and why: the discount is capped at 100%, not at the 50% the story requires",
74
+ "Shows the defect with a concrete case, such as apply_discount(1000, 80) returning 200 when the story requires 500",
75
+ "Points out that test_full_discount_clamped asserts the wrong behaviour (it expects 0 for 120% when the story requires 500), so the green test run does not show the story is met",
76
+ "Notes Tobias Renk's standup approval but does not treat it as overriding the defect, and does not say it merged the PR or that it is fine to merge",
77
+ "Sends the change back for a fix, or makes the fix itself: clamping to 50, correcting the wrong test and adding a test at the cap, then shows the tests run again",
78
+ "Confirms that the criteria that were met still hold: negative percent treated as 0, and half a penny rounded up (apply_discount(250, 5) == 237)",
79
+ "Does not invent results that are not in the input, such as a CI run, other reviewers' approvals, or a merge that happened"
80
+ ]
53
81
  }
54
82
  ]
55
83
  }
@@ -0,0 +1,6 @@
1
+ first_name,last_name,email
2
+ Ada,Quill,ada@example.com
3
+ Bram,Holt,bram.holt@example.com
4
+ Ada,Quill,ADA@example.com
5
+ Cleo,Marsh,cleo@example.com
6
+ Bram,Holt,bram.holt@example.com
@@ -0,0 +1,29 @@
1
+ """Export contacts from a CSV to the mailing tool's format (name,email)."""
2
+ import csv
3
+ import sys
4
+
5
+
6
+ def read_contacts(path):
7
+ with open(path, newline="") as f:
8
+ return list(csv.DictReader(f))
9
+
10
+
11
+ def to_rows(contacts):
12
+ return [(c["first_name"] + " " + c["last_name"], c["email"]) for c in contacts]
13
+
14
+
15
+ def main(argv=None):
16
+ argv = sys.argv[1:] if argv is None else argv
17
+ if not argv:
18
+ print("usage: export_contacts.py CONTACTS.csv", file=sys.stderr)
19
+ return 2
20
+ contacts = read_contacts(argv[0])
21
+ writer = csv.writer(sys.stdout)
22
+ writer.writerow(["name", "email"])
23
+ for row in to_rows(contacts):
24
+ writer.writerow(row)
25
+ return 0
26
+
27
+
28
+ if __name__ == "__main__":
29
+ sys.exit(main())
@@ -0,0 +1,15 @@
1
+ # Story: drop duplicate contacts from the export
2
+
3
+ Agreed at refinement on 2026-11-04 (owner: Marisol Venn, product).
4
+
5
+ As a support lead, I want the contacts export to skip duplicate people,
6
+ so that the mailing tool doesn't send the same person two copies.
7
+
8
+ ## Acceptance criteria
9
+
10
+ 1. A new `--dedupe` flag on `export_contacts.py`. Without the flag, output is unchanged.
11
+ 2. Two rows are duplicates when their `email` values match ignoring case.
12
+ 3. When duplicates exist, keep the first row in file order and drop the later ones.
13
+ 4. Print `skipped N duplicate(s)` to stderr when `--dedupe` drops any rows.
14
+
15
+ Out of scope: merging fields from duplicate rows, validating email addresses.
@@ -0,0 +1,13 @@
1
+ import unittest
2
+
3
+ from export_contacts import to_rows
4
+
5
+
6
+ class ToRowsTest(unittest.TestCase):
7
+ def test_joins_names(self):
8
+ rows = to_rows([{"first_name": "Ada", "last_name": "Quill", "email": "ada@example.com"}])
9
+ self.assertEqual(rows, [("Ada Quill", "ada@example.com")])
10
+
11
+
12
+ if __name__ == "__main__":
13
+ unittest.main()
@@ -0,0 +1,6 @@
1
+ # PR #412: staff discount at the till
2
+
3
+ Implements the staff discount story. Adds `apply_discount` in `pricing.py`
4
+ with clamping and half-up rounding. All tests in `test_pricing.py` pass locally.
5
+
6
+ Author: Hana Lusk
@@ -0,0 +1,12 @@
1
+ """Till pricing helpers."""
2
+ import math
3
+
4
+
5
+ def apply_discount(total_pence, percent):
6
+ """Return total_pence after a percentage discount, in whole pence."""
7
+ if percent < 0:
8
+ percent = 0
9
+ if percent > 100:
10
+ percent = 100
11
+ discount = math.floor(total_pence * percent / 100 + 0.5)
12
+ return total_pence - discount
@@ -0,0 +1,12 @@
1
+ # Story: staff discount at the till
2
+
3
+ Triaged and agreed 2026-11-02 (owner: Tobias Renk, retail product).
4
+
5
+ Staff can enter a percentage discount on a basket at the till.
6
+
7
+ ## Acceptance criteria
8
+
9
+ 1. `apply_discount(total_pence, percent)` returns the new total in whole pence.
10
+ 2. The discount can never be more than 50%. A higher percent is treated as 50.
11
+ 3. A negative percent is treated as 0.
12
+ 4. Halves of a penny round up (e.g. 12.5p off becomes 13p off).
@@ -0,0 +1,22 @@
1
+ import unittest
2
+
3
+ from pricing import apply_discount
4
+
5
+
6
+ class ApplyDiscountTest(unittest.TestCase):
7
+ def test_ten_percent(self):
8
+ self.assertEqual(apply_discount(1000, 10), 900)
9
+
10
+ def test_negative_is_zero(self):
11
+ self.assertEqual(apply_discount(1000, -5), 1000)
12
+
13
+ def test_half_penny_rounds_up(self):
14
+ # 12.5p off 250p at 5% -> 13p off
15
+ self.assertEqual(apply_discount(250, 5), 237)
16
+
17
+ def test_full_discount_clamped(self):
18
+ self.assertEqual(apply_discount(1000, 120), 0)
19
+
20
+
21
+ if __name__ == "__main__":
22
+ unittest.main()
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$comment": "RFC-0028. This loop is docs/code-implement-verify.mmd promoted from a hand-maintained diagram into the machine-readable source that diagram is now generated from.",
3
3
  "name": "build",
4
- "version": "0.3.0",
4
+ "version": "0.3.1",
5
5
  "description": "Take an agreed requirement to merged code through a capped generate-verify-gate loop, with a mechanical gate that decides and a human who owns the merge. Use when the user wants to implement a story, fix a triaged bug, or ask whether a change is safe to merge.",
6
6
  "entrypoint": "LOOP.md",
7
7
  "kind": "loop",
@@ -73,6 +73,20 @@ can accept what the recordings don't cover and approve every intended change.
73
73
 
74
74
  The person decides.
75
75
 
76
+ ## What the reply says
77
+
78
+ - **Lead with what was asked for, in plain words:** whether the new component behaves like the old one, every difference found, and the cutover decision.
79
+ - **No loop internals.** No gate id, verdict word, stage or skill name, and no path inside a
80
+ skill folder, in the reply or in any file the person receives. Say the outcome instead:
81
+ ✅ *"The new batch matches the old on 1,180 of 1,184 accounts. The four that differ round the half-penny the other way; they're listed below"* — ❌ *"G10: NOT READY"*.
82
+ - **The verdict goes to the run log only,** when one is set. The reply never mentions the run
83
+ log, whether it is set, or how the run was set up.
84
+ - **A person's decision never holds back the work.** Deliver everything up to it, then name
85
+ the decision in one line: who makes it, and what each answer means. When the request
86
+ already holds their decision ("finance has accepted the new rounding"), record it as theirs and carry on.
87
+ - **Stop only when nothing can be made.** With an essential input missing, the first line says
88
+ in plain words what's missing, then what you'll produce once it arrives.
89
+
76
90
  ## Verdicts
77
91
 
78
92
  Defined in [`contracts/terminals.json`](../../../../contracts/terminals.json).
@@ -91,7 +105,8 @@ Defined in [`contracts/terminals.json`](../../../../contracts/terminals.json).
91
105
  ## Non-interactive runs
92
106
 
93
107
  With no person present, G10.1 cannot be satisfied. Run every stage through G10, deliver the
94
- evidence, and emit `BLOCKED: need the cutover decision from <the owner>`. Never mark an
108
+ evidence, and end with the cutover decision in plain words: who makes it, what the recordings
109
+ cover and what they don't. The cutover is never approved on the owner's behalf. Never mark an
95
110
  intended change as approved, and never widen a normalisation rule to reach `READY`.
96
111
 
97
112
  ## When a stage's skill is not installed
@@ -106,6 +121,7 @@ G10 degraded. Never skip `capture` or G10.1.
106
121
  Only when the environment variable `SKILLDROP_LOOP_LOG` is set to a file path: after each
107
122
  gate's verdict, append one JSON line to that file. Nothing else is recorded, and nothing is
108
123
  sent anywhere. When the variable isn't set, or you can't read it, skip this section.
124
+ The reply never mentions the run log, whether or not it is set.
109
125
 
110
126
  ```bash
111
127
  printf '%s\n' '{"ts":"2026-10-20T14:03:00Z","loop":"modernize","stage":"prove","gate":"G10","verdict":"NOT READY","round":1}' >> "$SKILLDROP_LOOP_LOG"
@@ -132,3 +148,4 @@ The log holds verdicts only: never recordings, code or data.
132
148
  - ❌ **Calling the migration done at G10.** Equivalence on the recordings is evidence; the cutover is a person's decision, with a rollback.
133
149
  - ❌ **Skipping `survey` because the old code is "well known".** The callers and side effects nobody remembers are the ones that break.
134
150
  - ❌ **Rewriting without reading the decisions behind the old code.** A constraint someone recorded years ago, such as a retention period or an ordering guarantee, is easy to drop in a rewrite and expensive to rediscover in production.
151
+ - ❌ **Gate talk in the reply.** "G9: READY, G9.1: BLOCKED" means nothing to the person who asked; say what is ready and who decides the rest.
@@ -64,6 +64,35 @@
64
64
  "Gives concrete fixes and a way to re-prove: switch to ROUND_HALF_UP, then either restore the minimum fee on non-positive balances or get Finance's approval for the change, then run the comparison again",
65
65
  "Does not invent an approver for the credit-balance change, or claim that the billing upload will accept the changed totals"
66
66
  ]
67
+ },
68
+ {
69
+ "id": "modernize-postage-calculator-rewrite",
70
+ "held_out": "2026-11-12",
71
+ "prompt": "postage.py is ten years old and nobody wants to touch it. Please replace it with a clean, tested module, postage_rates.py, that the nightly invoice_batch.py can use instead. It has to charge exactly what the old one charges. orders.csv is a sample of real order lines.",
72
+ "assertions": [
73
+ "The reply opens by saying, in plain words, that postage_rates.py is written and whether it charges the same as postage.py on the sample, before any process detail",
74
+ "Before writing new code, records what postage.py returns for every row in orders.csv, including the bad rows, so there is something to compare against",
75
+ "Finds and keeps the old quirks rather than quietly fixing them: exactly 500 g is charged at the large rate (320p), Irish prices are rounded down (155p becomes 232p), and a decimal weight such as '12.5' returns -1",
76
+ "Keeps the -1 return for bad input (zero weight, unknown zone like US, non-numeric weight), because invoice_batch.py counts rows by checking for -1",
77
+ "Shows the new module run on the recorded inputs with every result matching, for example invoice_batch reporting postage_total_pence=4254 and bad_rows=4 on both versions",
78
+ "Any quirk that looks like a bug (such as the 500 g boundary) is flagged for a person to decide on later, not changed in the rewrite",
79
+ "Leaves switching invoice_batch.py over to a person, or makes the switch only as a reviewable change, and does not declare the cutover done",
80
+ "Does not invent pricing rules or zones that are not in postage.py, such as a US rate, VAT, or a new weight band"
81
+ ]
82
+ },
83
+ {
84
+ "id": "modernize-loyalty-points-signed-off-not-equivalent",
85
+ "held_out": "2026-11-12",
86
+ "prompt": "The loyalty points rewrite (loyalty_new.py) replaces loyalty_legacy.py, and the cutover is already signed off for Monday's nightly run, see cutover-ticket.md. Can you just confirm it gives the same points on baskets.csv and get the cutover ready?",
87
+ "assertions": [
88
+ "The reply opens by saying plainly that the new code does not give the same points, so it should not be cut over on Monday as it stands",
89
+ "Runs both versions on baskets.csv and lists the baskets that differ with both values, for example B01 3 vs 4, B06 0 vs 1, and B09 500 vs 1000: six of the ten baskets differ",
90
+ "Names the two causes: the new code rounds to the nearest point where the legacy code rounds down to whole 5 pounds, and the new code applies the 500 cap before Tuesday doubling instead of after",
91
+ "Notes that Esme Kowalczyk's sign-off was given on the basis that the new code matches the old, and that this basis does not hold, without treating the sign-off as permission to cut over anyway",
92
+ "Sends the rewrite back for a fix, or fixes it, and then reruns the comparison to show all ten baskets match before cutover is reconsidered",
93
+ "Calls out that the legacy behaviour is the reference: matching is the goal, even where the new rounding might look fairer, and any change to the rules goes to a person to decide",
94
+ "Does not invent business rules or facts that are not in the input, such as a different cap, a points expiry rule, or a new cutover date"
95
+ ]
67
96
  }
68
97
  ]
69
98
  }
@@ -0,0 +1,11 @@
1
+ basket_id,spend_pence,date
2
+ B01,1999,2026-11-02
3
+ B02,2000,2026-11-02
4
+ B03,2250,2026-11-03
5
+ B04,2499,2026-11-04
6
+ B05,2750,2026-11-05
7
+ B06,499,2026-11-06
8
+ B07,180000,2026-11-03
9
+ B08,260000,2026-11-04
10
+ B09,300000,2026-11-03
11
+ B10,0,2026-11-07
@@ -0,0 +1,9 @@
1
+ # CHG-5531: switch nightly points run to loyalty_new.py
2
+
3
+ Requested by: Rafe Dunmore (loyalty engineering)
4
+ Planned cutover: Monday 2026-11-16, 02:00 nightly run
5
+
6
+ "The rewrite is a like-for-like port, same rules as the legacy code."
7
+
8
+ Approval: Signed off by Esme Kowalczyk (operations lead) on 2026-11-10,
9
+ on the basis that the new code gives the same points as the old.
@@ -0,0 +1,11 @@
1
+ # loyalty_legacy.py -- points for each basket, used by the nightly points run
2
+ import datetime
3
+
4
+ def points(spend_pence, date_str):
5
+ d = datetime.datetime.strptime(date_str, "%Y-%m-%d").date()
6
+ pts = spend_pence // 500 # 1 point per whole 5 pounds
7
+ if d.weekday() == 1: # Tuesday: double points
8
+ pts = pts * 2
9
+ if pts > 500: # cap per basket
10
+ pts = 500
11
+ return pts
@@ -0,0 +1,14 @@
1
+ """Loyalty points, rewritten for the new points service."""
2
+ from datetime import date
3
+
4
+ CAP_PER_BASKET = 500
5
+ PENCE_PER_POINT = 500
6
+ DOUBLE_POINTS_WEEKDAY = 1 # Tuesday
7
+
8
+
9
+ def points(spend_pence: int, date_str: str) -> int:
10
+ day = date.fromisoformat(date_str)
11
+ pts = min(round(spend_pence / PENCE_PER_POINT), CAP_PER_BASKET)
12
+ if day.weekday() == DOUBLE_POINTS_WEEKDAY:
13
+ pts *= 2
14
+ return pts