@mmerterden/multi-agent-pipeline 12.7.0 → 12.9.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 (247) hide show
  1. package/CHANGELOG.md +207 -0
  2. package/install/_common.mjs +48 -0
  3. package/install/_dev-only-files.mjs +125 -5
  4. package/install/claude.mjs +14 -8
  5. package/install/copilot.mjs +5 -8
  6. package/package.json +17 -2
  7. package/pipeline/commands/multi-agent/analysis/SKILL.md +1 -1
  8. package/pipeline/lib/credential-store.sh +20 -0
  9. package/pipeline/multi-agent-refs/_account-picker.md +1 -1
  10. package/pipeline/multi-agent-refs/_dev-context.md +1 -1
  11. package/pipeline/multi-agent-refs/_repo-picker.md +1 -1
  12. package/pipeline/multi-agent-refs/features/review-multi-repo.md +65 -0
  13. package/pipeline/multi-agent-refs/features/url-enrichment.md +93 -0
  14. package/pipeline/multi-agent-refs/phases/operations.md +28 -0
  15. package/pipeline/multi-agent-refs/phases/phase-0-init.md +3 -83
  16. package/pipeline/multi-agent-refs/phases/phase-2-planning.md +1 -1
  17. package/pipeline/multi-agent-refs/phases/phase-3-dev.md +1 -2
  18. package/pipeline/multi-agent-refs/phases/phase-4-review.md +50 -62
  19. package/pipeline/schemas/prefs.schema.json +6 -0
  20. package/pipeline/scripts/_smoke-root.sh +61 -0
  21. package/pipeline/scripts/agent-guard.py +57 -3
  22. package/pipeline/scripts/audit-log.sh +25 -0
  23. package/pipeline/scripts/smoke-cross-cli-behavior.sh +21 -7
  24. package/pipeline/eval/golden-tasks/01-ios-bugfix-darkmode/expected/phase-1-analysis.json +0 -25
  25. package/pipeline/eval/golden-tasks/01-ios-bugfix-darkmode/expected/phase-2-plan.json +0 -30
  26. package/pipeline/eval/golden-tasks/01-ios-bugfix-darkmode/expected/phase-4-review.json +0 -20
  27. package/pipeline/eval/golden-tasks/01-ios-bugfix-darkmode/expected/phase-4-triage.json +0 -15
  28. package/pipeline/eval/golden-tasks/01-ios-bugfix-darkmode/metadata.json +0 -14
  29. package/pipeline/eval/golden-tasks/01-ios-bugfix-darkmode/task.json +0 -12
  30. package/pipeline/eval/golden-tasks/02-android-feature-compose/expected/phase-1-analysis.json +0 -29
  31. package/pipeline/eval/golden-tasks/02-android-feature-compose/expected/phase-2-plan.json +0 -43
  32. package/pipeline/eval/golden-tasks/02-android-feature-compose/expected/phase-4-review.json +0 -35
  33. package/pipeline/eval/golden-tasks/02-android-feature-compose/expected/phase-4-triage.json +0 -35
  34. package/pipeline/eval/golden-tasks/02-android-feature-compose/metadata.json +0 -14
  35. package/pipeline/eval/golden-tasks/02-android-feature-compose/task.json +0 -12
  36. package/pipeline/eval/golden-tasks/03-backend-python-ratelimit/expected/phase-1-analysis.json +0 -29
  37. package/pipeline/eval/golden-tasks/03-backend-python-ratelimit/expected/phase-2-plan.json +0 -42
  38. package/pipeline/eval/golden-tasks/03-backend-python-ratelimit/expected/phase-4-review.json +0 -20
  39. package/pipeline/eval/golden-tasks/03-backend-python-ratelimit/expected/phase-4-triage.json +0 -15
  40. package/pipeline/eval/golden-tasks/03-backend-python-ratelimit/metadata.json +0 -14
  41. package/pipeline/eval/golden-tasks/03-backend-python-ratelimit/task.json +0 -12
  42. package/pipeline/eval/golden-tasks/04-frontend-next-hydration/expected/phase-1-analysis.json +0 -29
  43. package/pipeline/eval/golden-tasks/04-frontend-next-hydration/expected/phase-2-plan.json +0 -40
  44. package/pipeline/eval/golden-tasks/04-frontend-next-hydration/expected/phase-4-review.json +0 -20
  45. package/pipeline/eval/golden-tasks/04-frontend-next-hydration/expected/phase-4-triage.json +0 -15
  46. package/pipeline/eval/golden-tasks/04-frontend-next-hydration/metadata.json +0 -14
  47. package/pipeline/eval/golden-tasks/04-frontend-next-hydration/task.json +0 -12
  48. package/pipeline/eval/golden-tasks/05-ios-security-keychain/expected/phase-1-analysis.json +0 -29
  49. package/pipeline/eval/golden-tasks/05-ios-security-keychain/expected/phase-2-plan.json +0 -42
  50. package/pipeline/eval/golden-tasks/05-ios-security-keychain/expected/phase-4-review.json +0 -28
  51. package/pipeline/eval/golden-tasks/05-ios-security-keychain/expected/phase-4-triage.json +0 -27
  52. package/pipeline/eval/golden-tasks/05-ios-security-keychain/metadata.json +0 -14
  53. package/pipeline/eval/golden-tasks/05-ios-security-keychain/task.json +0 -12
  54. package/pipeline/eval/golden-tasks/06-android-refactor-usecase/expected/phase-1-analysis.json +0 -29
  55. package/pipeline/eval/golden-tasks/06-android-refactor-usecase/expected/phase-2-plan.json +0 -41
  56. package/pipeline/eval/golden-tasks/06-android-refactor-usecase/expected/phase-4-review.json +0 -12
  57. package/pipeline/eval/golden-tasks/06-android-refactor-usecase/expected/phase-4-triage.json +0 -6
  58. package/pipeline/eval/golden-tasks/06-android-refactor-usecase/metadata.json +0 -14
  59. package/pipeline/eval/golden-tasks/06-android-refactor-usecase/task.json +0 -12
  60. package/pipeline/eval/golden-tasks/07-backend-node-idempotency/expected/phase-1-analysis.json +0 -29
  61. package/pipeline/eval/golden-tasks/07-backend-node-idempotency/expected/phase-2-plan.json +0 -42
  62. package/pipeline/eval/golden-tasks/07-backend-node-idempotency/expected/phase-4-review.json +0 -28
  63. package/pipeline/eval/golden-tasks/07-backend-node-idempotency/expected/phase-4-triage.json +0 -27
  64. package/pipeline/eval/golden-tasks/07-backend-node-idempotency/metadata.json +0 -14
  65. package/pipeline/eval/golden-tasks/07-backend-node-idempotency/task.json +0 -12
  66. package/pipeline/eval/golden-tasks/08-ios-auth-consensus-unverified/expected/phase-1-analysis.json +0 -25
  67. package/pipeline/eval/golden-tasks/08-ios-auth-consensus-unverified/expected/phase-2-plan.json +0 -31
  68. package/pipeline/eval/golden-tasks/08-ios-auth-consensus-unverified/expected/phase-4-review.json +0 -12
  69. package/pipeline/eval/golden-tasks/08-ios-auth-consensus-unverified/expected/phase-4-triage.json +0 -18
  70. package/pipeline/eval/golden-tasks/08-ios-auth-consensus-unverified/metadata.json +0 -14
  71. package/pipeline/eval/golden-tasks/08-ios-auth-consensus-unverified/task.json +0 -12
  72. package/pipeline/eval/golden-tasks/README.md +0 -65
  73. package/pipeline/eval/intent-cases.json +0 -40
  74. package/pipeline/eval/run-metrics-fixture.json +0 -93
  75. package/pipeline/eval/triage/01-empty-findings/expected.json +0 -6
  76. package/pipeline/eval/triage/01-empty-findings/input.json +0 -5
  77. package/pipeline/eval/triage/01-empty-findings/notes.md +0 -7
  78. package/pipeline/eval/triage/02-real-blocker/expected.json +0 -15
  79. package/pipeline/eval/triage/02-real-blocker/input.json +0 -14
  80. package/pipeline/eval/triage/02-real-blocker/notes.md +0 -7
  81. package/pipeline/eval/triage/03-out-of-scope-defer/expected.json +0 -18
  82. package/pipeline/eval/triage/03-out-of-scope-defer/input.json +0 -14
  83. package/pipeline/eval/triage/03-out-of-scope-defer/notes.md +0 -10
  84. package/pipeline/eval/triage/04-false-positive-reject/expected.json +0 -18
  85. package/pipeline/eval/triage/04-false-positive-reject/input.json +0 -14
  86. package/pipeline/eval/triage/04-false-positive-reject/notes.md +0 -10
  87. package/pipeline/eval/triage/05-mixed-classification/expected.json +0 -43
  88. package/pipeline/eval/triage/05-mixed-classification/input.json +0 -38
  89. package/pipeline/eval/triage/05-mixed-classification/notes.md +0 -17
  90. package/pipeline/eval/triage/06-severity-mismatch/expected.json +0 -15
  91. package/pipeline/eval/triage/06-severity-mismatch/input.json +0 -14
  92. package/pipeline/eval/triage/06-severity-mismatch/notes.md +0 -9
  93. package/pipeline/eval/triage/07-duplicate-reviewers/expected.json +0 -27
  94. package/pipeline/eval/triage/07-duplicate-reviewers/input.json +0 -22
  95. package/pipeline/eval/triage/07-duplicate-reviewers/notes.md +0 -9
  96. package/pipeline/eval/triage/08-style-misclassified/expected.json +0 -18
  97. package/pipeline/eval/triage/08-style-misclassified/input.json +0 -14
  98. package/pipeline/eval/triage/08-style-misclassified/notes.md +0 -9
  99. package/pipeline/eval/triage/09-cascading-finding/expected.json +0 -23
  100. package/pipeline/eval/triage/09-cascading-finding/input.json +0 -22
  101. package/pipeline/eval/triage/09-cascading-finding/notes.md +0 -9
  102. package/pipeline/eval/triage/10-deferred-crossref/expected.json +0 -18
  103. package/pipeline/eval/triage/10-deferred-crossref/input.json +0 -14
  104. package/pipeline/eval/triage/10-deferred-crossref/notes.md +0 -9
  105. package/pipeline/eval/triage/11-vercel-token-leak-blocker/expected.json +0 -27
  106. package/pipeline/eval/triage/11-vercel-token-leak-blocker/input.json +0 -22
  107. package/pipeline/eval/triage/11-vercel-token-leak-blocker/notes.md +0 -14
  108. package/pipeline/eval/triage/README.md +0 -54
  109. package/pipeline/scripts/benchmark-phase-0.sh +0 -128
  110. package/pipeline/scripts/check-md-links.mjs +0 -88
  111. package/pipeline/scripts/eval-golden-tasks-live.mjs +0 -302
  112. package/pipeline/scripts/eval-golden-tasks.mjs +0 -224
  113. package/pipeline/scripts/eval-intent.mjs +0 -107
  114. package/pipeline/scripts/eval-mine-corpus.mjs +0 -211
  115. package/pipeline/scripts/eval-triage.mjs +0 -171
  116. package/pipeline/scripts/fixtures/diff-risk-android.diff +0 -40
  117. package/pipeline/scripts/fixtures/diff-risk-ios.diff +0 -48
  118. package/pipeline/scripts/fixtures/diff-risk-test-removal.diff +0 -40
  119. package/pipeline/scripts/fixtures/install-layout.tsv +0 -19
  120. package/pipeline/scripts/fixtures/pack-expected-count.txt +0 -1
  121. package/pipeline/scripts/fixtures/test-gap-node.diff +0 -30
  122. package/pipeline/scripts/fixtures/test-gap-python.diff +0 -32
  123. package/pipeline/scripts/lint-mcp-refs.mjs +0 -218
  124. package/pipeline/scripts/lint-skills.mjs +0 -154
  125. package/pipeline/scripts/run-smokes.mjs +0 -130
  126. package/pipeline/scripts/scorecard.mjs +0 -258
  127. package/pipeline/scripts/smoke-add-detail.sh +0 -137
  128. package/pipeline/scripts/smoke-agent-guard.sh +0 -74
  129. package/pipeline/scripts/smoke-agent-log-cost.sh +0 -262
  130. package/pipeline/scripts/smoke-agent-model-routing.sh +0 -87
  131. package/pipeline/scripts/smoke-ask-choice.sh +0 -42
  132. package/pipeline/scripts/smoke-autopilot-circuit-breaker.sh +0 -36
  133. package/pipeline/scripts/smoke-bitbucket-contract.sh +0 -255
  134. package/pipeline/scripts/smoke-changelog-version.sh +0 -47
  135. package/pipeline/scripts/smoke-channels-approval-gate.sh +0 -60
  136. package/pipeline/scripts/smoke-channels-flow.sh +0 -130
  137. package/pipeline/scripts/smoke-ci-workflows.sh +0 -88
  138. package/pipeline/scripts/smoke-clarify.sh +0 -148
  139. package/pipeline/scripts/smoke-command-inventory.sh +0 -81
  140. package/pipeline/scripts/smoke-commands-skills-parity.sh +0 -87
  141. package/pipeline/scripts/smoke-community-gates.sh +0 -75
  142. package/pipeline/scripts/smoke-compliance-skills.sh +0 -119
  143. package/pipeline/scripts/smoke-config-hygiene.sh +0 -58
  144. package/pipeline/scripts/smoke-context-budget.sh +0 -72
  145. package/pipeline/scripts/smoke-cost-budget.sh +0 -70
  146. package/pipeline/scripts/smoke-cost-summary.sh +0 -139
  147. package/pipeline/scripts/smoke-cross-phase-cohesion.sh +0 -128
  148. package/pipeline/scripts/smoke-description-tr.sh +0 -82
  149. package/pipeline/scripts/smoke-dev-critic.sh +0 -144
  150. package/pipeline/scripts/smoke-diff-explain.sh +0 -147
  151. package/pipeline/scripts/smoke-diff-risk.sh +0 -190
  152. package/pipeline/scripts/smoke-dynamic-skill-loading.sh +0 -160
  153. package/pipeline/scripts/smoke-eval-live.sh +0 -136
  154. package/pipeline/scripts/smoke-evidence-gate.sh +0 -93
  155. package/pipeline/scripts/smoke-extract-conventions.sh +0 -163
  156. package/pipeline/scripts/smoke-fetchers-offline.sh +0 -448
  157. package/pipeline/scripts/smoke-figma-dispatch.sh +0 -112
  158. package/pipeline/scripts/smoke-gate-hooks.sh +0 -74
  159. package/pipeline/scripts/smoke-gc-tmp.sh +0 -130
  160. package/pipeline/scripts/smoke-gc-worktrees.sh +0 -125
  161. package/pipeline/scripts/smoke-generate-issue.sh +0 -120
  162. package/pipeline/scripts/smoke-handoff-contract.sh +0 -92
  163. package/pipeline/scripts/smoke-identity-isolation.sh +0 -70
  164. package/pipeline/scripts/smoke-install-layout.sh +0 -248
  165. package/pipeline/scripts/smoke-intent-guard.sh +0 -86
  166. package/pipeline/scripts/smoke-issue-comment-template.sh +0 -86
  167. package/pipeline/scripts/smoke-issue-jira-triad.sh +0 -120
  168. package/pipeline/scripts/smoke-keychain.sh +0 -158
  169. package/pipeline/scripts/smoke-language-axis.sh +0 -109
  170. package/pipeline/scripts/smoke-learning-curve.sh +0 -61
  171. package/pipeline/scripts/smoke-learnings-ledger.sh +0 -86
  172. package/pipeline/scripts/smoke-lib-scripts.sh +0 -448
  173. package/pipeline/scripts/smoke-mcp-gate.sh +0 -68
  174. package/pipeline/scripts/smoke-md-links.sh +0 -8
  175. package/pipeline/scripts/smoke-md2confluence.sh +0 -126
  176. package/pipeline/scripts/smoke-metrics-cache-ratio.sh +0 -72
  177. package/pipeline/scripts/smoke-migrate-state.sh +0 -102
  178. package/pipeline/scripts/smoke-mode-dispatch-drift.sh +0 -161
  179. package/pipeline/scripts/smoke-model-fallback.sh +0 -89
  180. package/pipeline/scripts/smoke-multi-repo-integration.sh +0 -116
  181. package/pipeline/scripts/smoke-multi-repo-worktree.sh +0 -61
  182. package/pipeline/scripts/smoke-no-mcp-in-dev-phases.sh +0 -194
  183. package/pipeline/scripts/smoke-no-token-prompt.sh +0 -85
  184. package/pipeline/scripts/smoke-own-punctuation.sh +0 -103
  185. package/pipeline/scripts/smoke-pack-contents.sh +0 -140
  186. package/pipeline/scripts/smoke-pat-audit.sh +0 -128
  187. package/pipeline/scripts/smoke-per-repo-memory.sh +0 -156
  188. package/pipeline/scripts/smoke-phase-0-multi-repo.sh +0 -170
  189. package/pipeline/scripts/smoke-phase-6-multi.sh +0 -79
  190. package/pipeline/scripts/smoke-phase-banner.sh +0 -101
  191. package/pipeline/scripts/smoke-phase-tracker.sh +0 -324
  192. package/pipeline/scripts/smoke-phase0-bridge-contract.sh +0 -241
  193. package/pipeline/scripts/smoke-phase4-gates.sh +0 -45
  194. package/pipeline/scripts/smoke-phase4-triage.sh +0 -229
  195. package/pipeline/scripts/smoke-plan-approval-gate.sh +0 -71
  196. package/pipeline/scripts/smoke-plan-safety.sh +0 -139
  197. package/pipeline/scripts/smoke-plan-todos.sh +0 -196
  198. package/pipeline/scripts/smoke-pr-review-actions.sh +0 -152
  199. package/pipeline/scripts/smoke-pre-commit.sh +0 -170
  200. package/pipeline/scripts/smoke-pref-migration.sh +0 -226
  201. package/pipeline/scripts/smoke-prefs-language.sh +0 -134
  202. package/pipeline/scripts/smoke-progress-contract.sh +0 -127
  203. package/pipeline/scripts/smoke-prune-logs.sh +0 -137
  204. package/pipeline/scripts/smoke-purge.sh +0 -138
  205. package/pipeline/scripts/smoke-push-retry.sh +0 -75
  206. package/pipeline/scripts/smoke-repo-map.sh +0 -300
  207. package/pipeline/scripts/smoke-review-readiness.sh +0 -92
  208. package/pipeline/scripts/smoke-review-watch.sh +0 -146
  209. package/pipeline/scripts/smoke-routines.sh +0 -84
  210. package/pipeline/scripts/smoke-run-aggregator.sh +0 -216
  211. package/pipeline/scripts/smoke-run-metrics.sh +0 -50
  212. package/pipeline/scripts/smoke-search.sh +0 -187
  213. package/pipeline/scripts/smoke-shadow-git.sh +0 -224
  214. package/pipeline/scripts/smoke-skill-authoring.sh +0 -137
  215. package/pipeline/scripts/smoke-skill-language.sh +0 -83
  216. package/pipeline/scripts/smoke-skill-manifest.sh +0 -138
  217. package/pipeline/scripts/smoke-skill-scan.sh +0 -198
  218. package/pipeline/scripts/smoke-source-parity.sh +0 -85
  219. package/pipeline/scripts/smoke-subagent-validators.sh +0 -108
  220. package/pipeline/scripts/smoke-sync-parity.sh +0 -92
  221. package/pipeline/scripts/smoke-tasklist-ordering.sh +0 -112
  222. package/pipeline/scripts/smoke-telemetry.sh +0 -147
  223. package/pipeline/scripts/smoke-test-gap.sh +0 -183
  224. package/pipeline/scripts/smoke-token-budget.sh +0 -67
  225. package/pipeline/scripts/smoke-token-preflight.sh +0 -82
  226. package/pipeline/scripts/smoke-tracker-contract.sh +0 -191
  227. package/pipeline/scripts/smoke-tracker-tokens-invocation.sh +0 -73
  228. package/pipeline/scripts/smoke-triage-memory.sh +0 -174
  229. package/pipeline/scripts/smoke-update-check.sh +0 -135
  230. package/pipeline/scripts/smoke-url-enrichment.sh +0 -70
  231. package/pipeline/scripts/smoke-validate-analysis-doc.sh +0 -161
  232. package/pipeline/scripts/smoke-validator-contradiction.sh +0 -67
  233. package/pipeline/scripts/smoke-validator-gates.sh +0 -164
  234. package/pipeline/scripts/smoke-vercel-deploy-redact.sh +0 -129
  235. package/pipeline/scripts/smoke-verify-by-test.sh +0 -148
  236. package/pipeline/scripts/smoke-wiki-integration.sh +0 -122
  237. package/pipeline/scripts/smoke-work-summary.sh +0 -163
  238. package/pipeline/scripts/smoke-workflow-audit.sh +0 -101
  239. package/pipeline/scripts/smoke-worktree-path-convention.sh +0 -86
  240. package/pipeline/scripts/smoke-wrapper-preservation.sh +0 -68
  241. package/pipeline/scripts/smoke-write-state.sh +0 -159
  242. package/pipeline/scripts/sync-parity-check.sh +0 -135
  243. package/pipeline/scripts/test-gap-rules/android.json +0 -25
  244. package/pipeline/scripts/test-gap-rules/ios.json +0 -34
  245. package/pipeline/scripts/test-gap-rules/node.json +0 -29
  246. package/pipeline/scripts/test-gap-rules/python.json +0 -25
  247. package/pipeline/scripts/validate-schemas.mjs +0 -88
@@ -1,211 +0,0 @@
1
- #!/usr/bin/env node
2
- // eval-mine-corpus.mjs - grow the triage eval set from real production runs.
3
- //
4
- // The pipeline already records every Phase 4 triage decision to the per-repo
5
- // `~/.claude/memory/multi-agent/<repo-slug>/triage-corpus.jsonl` (one line per finding:
6
- // severity/file/line/issue/fix/reviewer + the classification the run gave it,
7
- // plus an optional human `reason`/`reviewer`). Hand-authoring eval fixtures is
8
- // the bottleneck; this closes the loop by turning those real decisions into
9
- // CANDIDATE regression fixtures in the exact shape `eval-triage.mjs` consumes.
10
- //
11
- // It is review-gated on purpose: candidates land in
12
- // `pipeline/eval/triage-candidates/<task_id>/` (which eval-triage does NOT
13
- // walk), each validated against the triage schema. A maintainer reviews them,
14
- // fills scope/diffSummary, and moves the good ones into
15
- // `pipeline/eval/triage/<NN>-<name>/` to adopt them.
16
- //
17
- // Highest-signal cases (a human left a `reason` or `reviewer`) are emitted
18
- // first and marked, since those encode a deliberate triage judgement.
19
- //
20
- // Usage:
21
- // node pipeline/scripts/eval-mine-corpus.mjs # default corpus path
22
- // node pipeline/scripts/eval-mine-corpus.mjs --corpus <path>
23
- // node pipeline/scripts/eval-mine-corpus.mjs --limit 20 # cap candidates
24
- // node pipeline/scripts/eval-mine-corpus.mjs --dry-run # report, write nothing
25
- //
26
- // Exit codes: 0 ok (including "nothing to mine"), 2 usage/setup error.
27
-
28
- import { readdirSync, readFileSync, writeFileSync, mkdirSync, existsSync } from "node:fs";
29
- import { dirname, join, resolve } from "node:path";
30
- import { fileURLToPath } from "node:url";
31
- import { homedir } from "node:os";
32
- import { spawnSync } from "node:child_process";
33
-
34
- const here = dirname(fileURLToPath(import.meta.url));
35
- const root = resolve(here, "..", "..");
36
- const evalTriageDir = join(root, "pipeline", "eval", "triage");
37
- const candidatesDir = join(root, "pipeline", "eval", "triage-candidates");
38
- const validator = join(root, "pipeline", "scripts", "validate-triage.mjs");
39
-
40
- const args = process.argv.slice(2);
41
- const opts = { corpus: null, limit: Infinity, dryRun: false, repoSlug: null };
42
- for (let i = 0; i < args.length; i++) {
43
- if (args[i] === "--corpus") opts.corpus = args[++i];
44
- else if (args[i] === "--repo-slug") opts.repoSlug = args[++i];
45
- else if (args[i] === "--limit") opts.limit = Number(args[++i]) || Infinity;
46
- else if (args[i] === "--dry-run") opts.dryRun = true;
47
- else if (args[i] === "-h" || args[i] === "--help") {
48
- console.log(
49
- "Usage: eval-mine-corpus.mjs [--corpus <path>] [--repo-slug <slug>] [--limit N] [--dry-run]",
50
- );
51
- process.exit(0);
52
- }
53
- }
54
-
55
- // Resolve the corpus path the SAME way triage-memory.mjs writes it:
56
- // ~/.claude/memory/multi-agent/<repo-slug>/triage-corpus.jsonl (per repo).
57
- // (The old default read ~/.claude/logs/.../triage-corpus.jsonl - a flat path
58
- // nothing writes to - so without --corpus it always found nothing.)
59
- function resolveRepoSlug() {
60
- if (opts.repoSlug) return String(opts.repoSlug);
61
- if (process.env.MULTI_AGENT_REPO_SLUG) return process.env.MULTI_AGENT_REPO_SLUG;
62
- const r = spawnSync("git", ["rev-parse", "--show-toplevel"], { encoding: "utf-8" });
63
- if (r.status === 0 && r.stdout.trim()) return r.stdout.trim().split("/").pop();
64
- return "default";
65
- }
66
-
67
- const corpusPath =
68
- opts.corpus ||
69
- join(homedir(), ".claude", "memory", "multi-agent", resolveRepoSlug(), "triage-corpus.jsonl");
70
-
71
- if (!existsSync(corpusPath)) {
72
- console.log(`eval-mine-corpus: no corpus at ${corpusPath} - nothing to mine.`);
73
- process.exit(0);
74
- }
75
-
76
- // Parse the JSONL corpus, skipping blank/malformed lines (never crash on data).
77
- const rows = [];
78
- for (const line of readFileSync(corpusPath, "utf-8").split("\n")) {
79
- const s = line.trim();
80
- if (!s) continue;
81
- try {
82
- const r = JSON.parse(s);
83
- if (r && r.task_id && r.issue && r.classification) rows.push(r);
84
- } catch {
85
- /* skip malformed line */
86
- }
87
- }
88
-
89
- if (rows.length === 0) {
90
- console.log("eval-mine-corpus: corpus has no usable rows - nothing to mine.");
91
- process.exit(0);
92
- }
93
-
94
- // task_ids that already have an adopted fixture: skip so we only propose new cases.
95
- const adopted = new Set();
96
- if (existsSync(evalTriageDir)) {
97
- for (const name of readdirSync(evalTriageDir)) {
98
- const inputPath = join(evalTriageDir, name, "input.json");
99
- if (existsSync(inputPath)) {
100
- try {
101
- const meta = JSON.parse(readFileSync(join(evalTriageDir, name, "meta.json"), "utf-8"));
102
- if (meta.task_id) adopted.add(meta.task_id);
103
- } catch {
104
- /* no meta.json -> can't dedup by task_id, that's fine */
105
- }
106
- }
107
- }
108
- }
109
-
110
- // Group corpus rows by task_id.
111
- const byTask = new Map();
112
- for (const r of rows) {
113
- if (!byTask.has(r.task_id)) byTask.set(r.task_id, []);
114
- byTask.get(r.task_id).push(r);
115
- }
116
-
117
- const toFinding = (r) => ({
118
- severity: r.severity,
119
- file: r.file,
120
- // Triage contract requires an integer line >= 0; corpus may carry null for a
121
- // file-level finding, so coerce to 0 (the schema's file-level sentinel).
122
- line: Number.isInteger(r.line) ? r.line : 0,
123
- issue: r.issue,
124
- fix: r.fix ?? null,
125
- reviewer: r.reviewer ?? null,
126
- });
127
-
128
- // Build a candidate per task. Highest-signal first (has a human reason/reviewer).
129
- const candidates = [];
130
- for (const [taskId, findings] of byTask) {
131
- if (adopted.has(taskId)) continue;
132
- const rawFindings = findings.map(toFinding);
133
- const accepted = [];
134
- const deferred = [];
135
- const rejected = [];
136
- for (const r of findings) {
137
- if (r.classification === "accepted") accepted.push(toFinding(r));
138
- else if (r.classification === "deferred")
139
- deferred.push({ finding: toFinding(r), reason: r.reason || "TODO: reason (from corpus)" });
140
- else if (r.classification === "rejected")
141
- rejected.push({ finding: toFinding(r), reason: r.reason || "TODO: reason (from corpus)" });
142
- }
143
- const hasHumanSignal = findings.some((r) => r.reason || r.reviewer);
144
- const title = findings.find((f) => f.task_title)?.task_title || taskId;
145
- candidates.push({
146
- taskId,
147
- hasHumanSignal,
148
- input: {
149
- rawFindings,
150
- // Corpus does not record scope/diff; a maintainer fills these before adopting.
151
- scope: `TODO: scope for ${title}`,
152
- diffSummary: "TODO: diff summary",
153
- },
154
- expected: {
155
- accepted,
156
- deferred,
157
- rejected,
158
- // Blockers loop back to dev, so a run with an accepted blocker is not approved.
159
- approved: accepted.every((f) => f.severity !== "blocking"),
160
- },
161
- });
162
- }
163
-
164
- candidates.sort((a, b) => Number(b.hasHumanSignal) - Number(a.hasHumanSignal));
165
- const selected = candidates.slice(0, opts.limit);
166
-
167
- if (selected.length === 0) {
168
- console.log("eval-mine-corpus: no new task_ids to propose (all already adopted).");
169
- process.exit(0);
170
- }
171
-
172
- let written = 0;
173
- let invalid = 0;
174
- for (const c of selected) {
175
- const safe = c.taskId.replace(/[^A-Za-z0-9._-]/g, "_");
176
- const dir = join(candidatesDir, safe);
177
- const tag = c.hasHumanSignal ? " [human-signal]" : "";
178
- if (opts.dryRun) {
179
- console.log(` would emit candidate: ${safe} (${c.input.rawFindings.length} findings)${tag}`);
180
- continue;
181
- }
182
- mkdirSync(dir, { recursive: true });
183
- writeFileSync(join(dir, "input.json"), JSON.stringify(c.input, null, 2) + "\n");
184
- writeFileSync(join(dir, "expected.json"), JSON.stringify(c.expected, null, 2) + "\n");
185
- writeFileSync(
186
- join(dir, "meta.json"),
187
- JSON.stringify(
188
- { task_id: c.taskId, source: "eval-mine-corpus", hasHumanSignal: c.hasHumanSignal },
189
- null,
190
- 2,
191
- ) + "\n",
192
- );
193
- // Validate the emitted expected.json against the triage contract; flag if bad.
194
- const res = spawnSync("node", [validator, join(dir, "expected.json")], { encoding: "utf-8" });
195
- const ok = res.status === 0;
196
- if (!ok) invalid++;
197
- console.log(
198
- ` emitted: triage-candidates/${safe}${tag} ${ok ? "(schema OK)" : "(SCHEMA FAIL - review)"}`,
199
- );
200
- written++;
201
- }
202
-
203
- if (!opts.dryRun) {
204
- console.log("");
205
- console.log(
206
- `eval-mine-corpus: ${written} candidate(s) written to pipeline/eval/triage-candidates/`,
207
- );
208
- if (invalid > 0) console.log(` ${invalid} failed schema validation - fix before adopting.`);
209
- console.log(" Review each, fill scope/diffSummary, then move good ones into");
210
- console.log(" pipeline/eval/triage/<NN>-<name>/ to add them to the eval suite.");
211
- }
@@ -1,171 +0,0 @@
1
- #!/usr/bin/env node
2
- // eval-triage.mjs - semantic regression check for Phase 4 triage.
3
- //
4
- // Walks pipeline/eval/triage/<NN>-<name>/ subdirectories. For each case:
5
- // 1. Reads input.json (rawFindings + scope + diffSummary)
6
- // 2. Reads expected.json (the spec-defined triage output)
7
- // 3. Runs validate-triage.mjs against expected.json - must exit cleanly
8
- // 4. Asserts every rawFinding in input is accounted for in expected
9
- // (sum of accepted + deferred[].finding + rejected[].finding == rawFindings)
10
- // 5. Asserts no inventions: every classified finding traces back to a
11
- // raw finding by {file, line, issue} match.
12
- //
13
- // What this catches:
14
- // - Spec drift: someone changes phase-4-review.md section3 in a way that contradicts
15
- // a fixture without updating the fixture.
16
- // - Schema drift: validate-triage.mjs starts rejecting fixtures that used
17
- // to pass.
18
- // - Coverage gap: input has N findings but expected only classifies M < N
19
- // (a real triage agent doing the same would silently lose findings).
20
- //
21
- // What this does NOT catch:
22
- // - Whether a real triage agent would actually produce the expected output.
23
- // That requires API keys + nondeterminism. Use a separate eval harness
24
- // for that. This is a CONTRACT regression test, not a model-quality eval.
25
- //
26
- // Usage:
27
- // node pipeline/scripts/eval-triage.mjs # all cases
28
- // node pipeline/scripts/eval-triage.mjs --case 03-... # one case
29
- // node pipeline/scripts/eval-triage.mjs --json # CI-friendly output
30
- //
31
- // Exit codes: 0 all pass, 1 one or more fail, 2 usage/setup error.
32
-
33
- import { readdirSync, readFileSync, statSync } from "node:fs";
34
- import { dirname, join, resolve } from "node:path";
35
- import { fileURLToPath } from "node:url";
36
- import { spawnSync } from "node:child_process";
37
-
38
- const here = dirname(fileURLToPath(import.meta.url));
39
- const root = resolve(here, "..", "..");
40
- const evalDir = join(root, "pipeline", "eval", "triage");
41
- const validator = join(root, "pipeline", "scripts", "validate-triage.mjs");
42
-
43
- const args = process.argv.slice(2);
44
- const opts = { json: false, case: null };
45
- for (let i = 0; i < args.length; i++) {
46
- if (args[i] === "--json") opts.json = true;
47
- else if (args[i] === "--case") opts.case = args[++i];
48
- else if (args[i] === "-h" || args[i] === "--help") {
49
- console.log("Usage: eval-triage.mjs [--case <name>] [--json]");
50
- process.exit(0);
51
- }
52
- }
53
-
54
- function listCases() {
55
- return readdirSync(evalDir)
56
- .filter((f) => f !== "README.md" && statSync(join(evalDir, f)).isDirectory())
57
- .sort();
58
- }
59
-
60
- // Compare two finding records by stable-key triple.
61
- function findingKey(f) {
62
- return `${f.file}::${f.line}::${f.issue}`;
63
- }
64
-
65
- function runCase(name) {
66
- const dir = join(evalDir, name);
67
- const inputPath = join(dir, "input.json");
68
- const expectedPath = join(dir, "expected.json");
69
- const errors = [];
70
-
71
- let input, expected;
72
- try {
73
- input = JSON.parse(readFileSync(inputPath, "utf-8"));
74
- } catch (e) {
75
- return { name, ok: false, errors: [`input.json: ${e.message}`] };
76
- }
77
- try {
78
- expected = JSON.parse(readFileSync(expectedPath, "utf-8"));
79
- } catch (e) {
80
- return { name, ok: false, errors: [`expected.json: ${e.message}`] };
81
- }
82
-
83
- // 1. Structural sanity via validate-triage.mjs.
84
- const result = spawnSync("node", [validator, expectedPath], {
85
- encoding: "utf-8",
86
- });
87
- // Validator exit codes: 0 valid+clean, 1 invalid, 2 over-rejection,
88
- // 3 contradiction-corrected. For fixtures we expect 0 or 2 (over-rejection
89
- // is structurally valid but caller must pause).
90
- if (result.status !== 0 && result.status !== 2) {
91
- errors.push(
92
- `validator failed (exit ${result.status}); fixture must be either clean (0) or over-rejection (2). stderr: ${result.stderr.trim()}`,
93
- );
94
- }
95
-
96
- // 2. Coverage: every rawFinding in input must appear once in expected.
97
- const raw = input.rawFindings || [];
98
- const rawKeys = new Set(raw.map(findingKey));
99
- const seen = new Set();
100
-
101
- function consume(f, where) {
102
- const k = findingKey(f);
103
- if (!rawKeys.has(k)) {
104
- errors.push(`${where}: invented finding not in input ({${k}})`);
105
- return;
106
- }
107
- if (seen.has(k)) {
108
- errors.push(`${where}: duplicate classification of finding ({${k}})`);
109
- return;
110
- }
111
- seen.add(k);
112
- }
113
-
114
- (expected.accepted || []).forEach((f) => consume(f, "accepted"));
115
- (expected.deferred || []).forEach((d) => consume(d.finding, "deferred"));
116
- (expected.rejected || []).forEach((d) => consume(d.finding, "rejected"));
117
-
118
- // Anything in raw but not classified?
119
- for (const r of raw) {
120
- if (!seen.has(findingKey(r))) {
121
- errors.push(`unclassified raw finding (lost): ${findingKey(r)}`);
122
- }
123
- }
124
-
125
- // 3. Approved consistency: approved===true iff no accepted blocker.
126
- const hasBlocker = (expected.accepted || []).some((f) => f.severity === "blocking");
127
- if (expected.approved === true && hasBlocker) {
128
- errors.push("approved=true but accepted contains a blocking finding");
129
- }
130
- if (expected.approved === false && !hasBlocker) {
131
- errors.push("approved=false but accepted has no blocking finding");
132
- }
133
-
134
- return {
135
- name,
136
- ok: errors.length === 0,
137
- errors,
138
- rawCount: raw.length,
139
- accepted: (expected.accepted || []).length,
140
- deferred: (expected.deferred || []).length,
141
- rejected: (expected.rejected || []).length,
142
- approved: expected.approved,
143
- validatorExit: result.status,
144
- };
145
- }
146
-
147
- const cases = opts.case ? [opts.case] : listCases();
148
- if (cases.length === 0) {
149
- console.error("No cases found under pipeline/eval/triage/");
150
- process.exit(2);
151
- }
152
-
153
- const results = cases.map(runCase);
154
- const failed = results.filter((r) => !r.ok);
155
-
156
- if (opts.json) {
157
- console.log(JSON.stringify({ total: results.length, failed: failed.length, results }, null, 2));
158
- } else {
159
- for (const r of results) {
160
- const mark = r.ok ? "✓" : "✗";
161
- const tally = `raw=${r.rawCount} acc=${r.accepted} def=${r.deferred} rej=${r.rejected} approved=${r.approved} validator=${r.validatorExit}`;
162
- console.log(`${mark} ${r.name.padEnd(34)} ${tally}`);
163
- if (!r.ok) {
164
- for (const e of r.errors) console.log(` ${e}`);
165
- }
166
- }
167
- console.log("");
168
- console.log(`══ eval-triage: ${results.length - failed.length}/${results.length} passed ══`);
169
- }
170
-
171
- process.exit(failed.length === 0 ? 0 : 1);
@@ -1,40 +0,0 @@
1
- diff --git a/app/src/main/java/com/example/auth/AuthRepository.kt b/app/src/main/java/com/example/auth/AuthRepository.kt
2
- index aaa..bbb 100644
3
- --- a/app/src/main/java/com/example/auth/AuthRepository.kt
4
- +++ b/app/src/main/java/com/example/auth/AuthRepository.kt
5
- @@ -10,0 +11,12 @@ class AuthRepository @Inject constructor(
6
- + open fun login(username: String, password: String): Result<Token> {
7
- + if (username.isBlank() || password.isBlank()) return Result.failure(IllegalArgumentException())
8
- + return try {
9
- + val response = api.login(username, password)
10
- + if (response.isSuccessful) Result.success(response.body()!!)
11
- + else Result.failure(HttpException(response))
12
- + } catch (e: IOException) {
13
- + Result.failure(e)
14
- + }
15
- + }
16
- +
17
- + open fun logout() { tokenStore.clear() }
18
- diff --git a/app/src/main/java/com/example/ui/HomeScreen.kt b/app/src/main/java/com/example/ui/HomeScreen.kt
19
- index ccc..ddd 100644
20
- --- a/app/src/main/java/com/example/ui/HomeScreen.kt
21
- +++ b/app/src/main/java/com/example/ui/HomeScreen.kt
22
- @@ -20,0 +21,4 @@ fun HomeScreen(viewModel: HomeViewModel) {
23
- + @Composable fun ItemRow(item: Item) {
24
- + Text(item.title, modifier = Modifier.padding(8.dp))
25
- + }
26
- +
27
- diff --git a/app/src/main/java/com/example/db/migrations/Migration_3_to_4.kt b/app/src/main/java/com/example/db/migrations/Migration_3_to_4.kt
28
- new file mode 100644
29
- index 000..eee
30
- --- /dev/null
31
- +++ b/app/src/main/java/com/example/db/migrations/Migration_3_to_4.kt
32
- @@ -0,0 +1,8 @@
33
- +val MIGRATION_3_TO_4 = object : Migration(3, 4) {
34
- + override fun migrate(database: SupportSQLiteDatabase) {
35
- + database.execSQL("ALTER TABLE user ADD COLUMN created_at INTEGER NOT NULL DEFAULT 0")
36
- + if (database.version < 4) {
37
- + database.execSQL("CREATE INDEX idx_user_created ON user(created_at)")
38
- + }
39
- + }
40
- +}
@@ -1,48 +0,0 @@
1
- diff --git a/MyApp/Sources/Auth/KeychainStore.swift b/MyApp/Sources/Auth/KeychainStore.swift
2
- index 1111111..2222222 100644
3
- --- a/MyApp/Sources/Auth/KeychainStore.swift
4
- +++ b/MyApp/Sources/Auth/KeychainStore.swift
5
- @@ -10,0 +11,18 @@ public final class KeychainStore {
6
- + public func save(token: String, account: String) throws {
7
- + guard !token.isEmpty else { throw KeychainError.emptyToken }
8
- + let query: [String: Any] = [
9
- + kSecClass as String: kSecClassGenericPassword,
10
- + kSecAttrAccount as String: account,
11
- + kSecValueData as String: Data(token.utf8)
12
- + ]
13
- + let status = SecItemAdd(query as CFDictionary, nil)
14
- + if status == errSecDuplicateItem {
15
- + let attrs: [String: Any] = [kSecValueData as String: Data(token.utf8)]
16
- + let updateStatus = SecItemUpdate(query as CFDictionary, attrs as CFDictionary)
17
- + guard updateStatus == errSecSuccess else { throw KeychainError.update(updateStatus) }
18
- + } else if status != errSecSuccess {
19
- + throw KeychainError.add(status)
20
- + }
21
- + }
22
- +
23
- + public func load(account: String) throws -> String { /* impl */ }
24
- diff --git a/MyApp/Sources/Views/SettingsView.swift b/MyApp/Sources/Views/SettingsView.swift
25
- index 3333333..4444444 100644
26
- --- a/MyApp/Sources/Views/SettingsView.swift
27
- +++ b/MyApp/Sources/Views/SettingsView.swift
28
- @@ -42,0 +43,3 @@ struct SettingsView: View {
29
- + Text("Hello")
30
- + .padding()
31
- + .foregroundColor(.secondary)
32
- diff --git a/MyApp/Sources/Networking/ApiClient.swift b/MyApp/Sources/Networking/ApiClient.swift
33
- index 5555555..6666666 100644
34
- --- a/MyApp/Sources/Networking/ApiClient.swift
35
- +++ b/MyApp/Sources/Networking/ApiClient.swift
36
- @@ -22,0 +23,5 @@ public class ApiClient {
37
- + public func fetch<T: Decodable>(_ url: URL) async throws -> T {
38
- + let (data, _) = try await URLSession.shared.data(from: url)
39
- + if data.isEmpty { throw ApiError.empty }
40
- + return try JSONDecoder().decode(T.self, from: data)
41
- + }
42
- diff --git a/MyAppTests/SettingsViewTests.swift b/MyAppTests/SettingsViewTests.swift
43
- index 7777777..8888888 100644
44
- --- a/MyAppTests/SettingsViewTests.swift
45
- +++ b/MyAppTests/SettingsViewTests.swift
46
- @@ -10,0 +11,2 @@ final class SettingsViewTests: XCTestCase {
47
- + func testRenders() throws { /* impl */ }
48
- +
@@ -1,40 +0,0 @@
1
- diff --git a/MyAppTests/LoginViewModelTests.swift b/MyAppTests/LoginViewModelTests.swift
2
- index 1111111..2222222 100644
3
- --- a/MyAppTests/LoginViewModelTests.swift
4
- +++ b/MyAppTests/LoginViewModelTests.swift
5
- @@ -10,30 +10,20 @@ final class LoginViewModelTests: XCTestCase {
6
- func testLoginWithValidCredentials_Succeeds() {
7
- let sut = LoginViewModel(service: MockAuthService())
8
- + sut.retryPolicy = .none
9
- sut.login(email: "user@example.com", password: "correct")
10
- + XCTAssertTrue(sut.isAuthenticated)
11
- }
12
- -
13
- - func testLoginWithInvalidEmail_ShowsError() {
14
- - let sut = LoginViewModel(service: MockAuthService())
15
- - sut.login(email: "not-an-email", password: "irrelevant")
16
- - XCTAssertEqual(sut.errorMessage, "Invalid email")
17
- - }
18
- -
19
- - func testLoginWithExpiredToken_Rejects() {
20
- - let sut = LoginViewModel(service: MockAuthService(tokenState: .expired))
21
- - sut.login(email: "user@example.com", password: "correct")
22
- - XCTAssertFalse(sut.isAuthenticated)
23
- - }
24
- -
25
- - func testLogout_ClearsSession() {
26
- - let sut = LoginViewModel(service: MockAuthService())
27
- - sut.logout()
28
- - XCTAssertNil(sut.session)
29
- - }
30
- }
31
- diff --git a/MyApp/Sources/Auth/LoginViewModel.swift b/MyApp/Sources/Auth/LoginViewModel.swift
32
- index 3333333..4444444 100644
33
- --- a/MyApp/Sources/Auth/LoginViewModel.swift
34
- +++ b/MyApp/Sources/Auth/LoginViewModel.swift
35
- @@ -20,6 +20,8 @@ final class LoginViewModel {
36
- func login(email: String, password: String) {
37
- + guard email.contains("@") else { return }
38
- + service.authenticate(email: email, password: password)
39
- }
40
- }
@@ -1,19 +0,0 @@
1
- .claude/.pipeline-version 1
2
- .claude/CLAUDE.md 1
3
- .claude/agents 8
4
- .claude/commands 48
5
- .claude/lib 27
6
- .claude/multi-agent-preferences.json 1
7
- .claude/multi-agent-refs 53
8
- .claude/rules 12
9
- .claude/schemas 25
10
- .claude/scripts 204
11
- .claude/settings.json 1
12
- .claude/skills 428
13
- .copilot/.pipeline-version 1
14
- .copilot/agents 8
15
- .copilot/copilot-instructions.md 1
16
- .copilot/lib 27
17
- .copilot/schemas 25
18
- .copilot/scripts 204
19
- .copilot/skills 471
@@ -1,30 +0,0 @@
1
- diff --git a/src/lib/parser.ts b/src/lib/parser.ts
2
- index aaaa..bbbb 100644
3
- --- a/src/lib/parser.ts
4
- +++ b/src/lib/parser.ts
5
- @@ -10,0 +11,8 @@
6
- +export function tokenize(input: string): string[] {
7
- + return input.split(/\s+/);
8
- +}
9
- +
10
- +export class Parser {
11
- + parse(tokens: string[]): unknown { return tokens; }
12
- +}
13
- +
14
- diff --git a/src/lib/format.ts b/src/lib/format.ts
15
- index cccc..dddd 100644
16
- --- a/src/lib/format.ts
17
- +++ b/src/lib/format.ts
18
- @@ -5,0 +6,2 @@
19
- +export const formatNumber = (n: number): string => n.toFixed(2);
20
- +
21
- diff --git a/src/lib/__tests__/parser.test.ts b/src/lib/__tests__/parser.test.ts
22
- new file mode 100644
23
- index 000..eeee
24
- --- /dev/null
25
- +++ b/src/lib/__tests__/parser.test.ts
26
- @@ -0,0 +1,4 @@
27
- +import { tokenize } from "../parser";
28
- +test("tokenize splits whitespace", () => {
29
- + expect(tokenize("a b c")).toEqual(["a","b","c"]);
30
- +});
@@ -1,32 +0,0 @@
1
- diff --git a/src/payments/processor.py b/src/payments/processor.py
2
- index 1111..2222 100644
3
- --- a/src/payments/processor.py
4
- +++ b/src/payments/processor.py
5
- @@ -10,0 +11,8 @@ class Processor:
6
- + def charge(self, amount, currency):
7
- + if amount <= 0:
8
- + raise ValueError("amount must be positive")
9
- + return self._gateway.charge(amount, currency)
10
- +
11
- +async def refund(transaction_id):
12
- + return await api.refund(transaction_id)
13
- +
14
- diff --git a/src/payments/utils.py b/src/payments/utils.py
15
- index 3333..4444 100644
16
- --- a/src/payments/utils.py
17
- +++ b/src/payments/utils.py
18
- @@ -5,0 +6,3 @@
19
- +def normalize_currency(code):
20
- + return code.upper().strip()
21
- +
22
- diff --git a/tests/test_processor.py b/tests/test_processor.py
23
- new file mode 100644
24
- index 000..5555
25
- --- /dev/null
26
- +++ b/tests/test_processor.py
27
- @@ -0,0 +1,5 @@
28
- +import pytest
29
- +
30
- +def test_charge_positive_amount():
31
- + p = Processor()
32
- + assert p.charge(10, "USD") is not None