@mmerterden/multi-agent-pipeline 12.7.0 → 12.8.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 (244) hide show
  1. package/CHANGELOG.md +126 -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/phases/operations.md +28 -0
  13. package/pipeline/multi-agent-refs/phases/phase-0-init.md +1 -0
  14. package/pipeline/multi-agent-refs/phases/phase-2-planning.md +1 -1
  15. package/pipeline/multi-agent-refs/phases/phase-3-dev.md +1 -2
  16. package/pipeline/multi-agent-refs/phases/phase-4-review.md +49 -4
  17. package/pipeline/schemas/prefs.schema.json +6 -0
  18. package/pipeline/scripts/_smoke-root.sh +61 -0
  19. package/pipeline/scripts/audit-log.sh +25 -0
  20. package/pipeline/scripts/smoke-cross-cli-behavior.sh +21 -7
  21. package/pipeline/eval/golden-tasks/01-ios-bugfix-darkmode/expected/phase-1-analysis.json +0 -25
  22. package/pipeline/eval/golden-tasks/01-ios-bugfix-darkmode/expected/phase-2-plan.json +0 -30
  23. package/pipeline/eval/golden-tasks/01-ios-bugfix-darkmode/expected/phase-4-review.json +0 -20
  24. package/pipeline/eval/golden-tasks/01-ios-bugfix-darkmode/expected/phase-4-triage.json +0 -15
  25. package/pipeline/eval/golden-tasks/01-ios-bugfix-darkmode/metadata.json +0 -14
  26. package/pipeline/eval/golden-tasks/01-ios-bugfix-darkmode/task.json +0 -12
  27. package/pipeline/eval/golden-tasks/02-android-feature-compose/expected/phase-1-analysis.json +0 -29
  28. package/pipeline/eval/golden-tasks/02-android-feature-compose/expected/phase-2-plan.json +0 -43
  29. package/pipeline/eval/golden-tasks/02-android-feature-compose/expected/phase-4-review.json +0 -35
  30. package/pipeline/eval/golden-tasks/02-android-feature-compose/expected/phase-4-triage.json +0 -35
  31. package/pipeline/eval/golden-tasks/02-android-feature-compose/metadata.json +0 -14
  32. package/pipeline/eval/golden-tasks/02-android-feature-compose/task.json +0 -12
  33. package/pipeline/eval/golden-tasks/03-backend-python-ratelimit/expected/phase-1-analysis.json +0 -29
  34. package/pipeline/eval/golden-tasks/03-backend-python-ratelimit/expected/phase-2-plan.json +0 -42
  35. package/pipeline/eval/golden-tasks/03-backend-python-ratelimit/expected/phase-4-review.json +0 -20
  36. package/pipeline/eval/golden-tasks/03-backend-python-ratelimit/expected/phase-4-triage.json +0 -15
  37. package/pipeline/eval/golden-tasks/03-backend-python-ratelimit/metadata.json +0 -14
  38. package/pipeline/eval/golden-tasks/03-backend-python-ratelimit/task.json +0 -12
  39. package/pipeline/eval/golden-tasks/04-frontend-next-hydration/expected/phase-1-analysis.json +0 -29
  40. package/pipeline/eval/golden-tasks/04-frontend-next-hydration/expected/phase-2-plan.json +0 -40
  41. package/pipeline/eval/golden-tasks/04-frontend-next-hydration/expected/phase-4-review.json +0 -20
  42. package/pipeline/eval/golden-tasks/04-frontend-next-hydration/expected/phase-4-triage.json +0 -15
  43. package/pipeline/eval/golden-tasks/04-frontend-next-hydration/metadata.json +0 -14
  44. package/pipeline/eval/golden-tasks/04-frontend-next-hydration/task.json +0 -12
  45. package/pipeline/eval/golden-tasks/05-ios-security-keychain/expected/phase-1-analysis.json +0 -29
  46. package/pipeline/eval/golden-tasks/05-ios-security-keychain/expected/phase-2-plan.json +0 -42
  47. package/pipeline/eval/golden-tasks/05-ios-security-keychain/expected/phase-4-review.json +0 -28
  48. package/pipeline/eval/golden-tasks/05-ios-security-keychain/expected/phase-4-triage.json +0 -27
  49. package/pipeline/eval/golden-tasks/05-ios-security-keychain/metadata.json +0 -14
  50. package/pipeline/eval/golden-tasks/05-ios-security-keychain/task.json +0 -12
  51. package/pipeline/eval/golden-tasks/06-android-refactor-usecase/expected/phase-1-analysis.json +0 -29
  52. package/pipeline/eval/golden-tasks/06-android-refactor-usecase/expected/phase-2-plan.json +0 -41
  53. package/pipeline/eval/golden-tasks/06-android-refactor-usecase/expected/phase-4-review.json +0 -12
  54. package/pipeline/eval/golden-tasks/06-android-refactor-usecase/expected/phase-4-triage.json +0 -6
  55. package/pipeline/eval/golden-tasks/06-android-refactor-usecase/metadata.json +0 -14
  56. package/pipeline/eval/golden-tasks/06-android-refactor-usecase/task.json +0 -12
  57. package/pipeline/eval/golden-tasks/07-backend-node-idempotency/expected/phase-1-analysis.json +0 -29
  58. package/pipeline/eval/golden-tasks/07-backend-node-idempotency/expected/phase-2-plan.json +0 -42
  59. package/pipeline/eval/golden-tasks/07-backend-node-idempotency/expected/phase-4-review.json +0 -28
  60. package/pipeline/eval/golden-tasks/07-backend-node-idempotency/expected/phase-4-triage.json +0 -27
  61. package/pipeline/eval/golden-tasks/07-backend-node-idempotency/metadata.json +0 -14
  62. package/pipeline/eval/golden-tasks/07-backend-node-idempotency/task.json +0 -12
  63. package/pipeline/eval/golden-tasks/08-ios-auth-consensus-unverified/expected/phase-1-analysis.json +0 -25
  64. package/pipeline/eval/golden-tasks/08-ios-auth-consensus-unverified/expected/phase-2-plan.json +0 -31
  65. package/pipeline/eval/golden-tasks/08-ios-auth-consensus-unverified/expected/phase-4-review.json +0 -12
  66. package/pipeline/eval/golden-tasks/08-ios-auth-consensus-unverified/expected/phase-4-triage.json +0 -18
  67. package/pipeline/eval/golden-tasks/08-ios-auth-consensus-unverified/metadata.json +0 -14
  68. package/pipeline/eval/golden-tasks/08-ios-auth-consensus-unverified/task.json +0 -12
  69. package/pipeline/eval/golden-tasks/README.md +0 -65
  70. package/pipeline/eval/intent-cases.json +0 -40
  71. package/pipeline/eval/run-metrics-fixture.json +0 -93
  72. package/pipeline/eval/triage/01-empty-findings/expected.json +0 -6
  73. package/pipeline/eval/triage/01-empty-findings/input.json +0 -5
  74. package/pipeline/eval/triage/01-empty-findings/notes.md +0 -7
  75. package/pipeline/eval/triage/02-real-blocker/expected.json +0 -15
  76. package/pipeline/eval/triage/02-real-blocker/input.json +0 -14
  77. package/pipeline/eval/triage/02-real-blocker/notes.md +0 -7
  78. package/pipeline/eval/triage/03-out-of-scope-defer/expected.json +0 -18
  79. package/pipeline/eval/triage/03-out-of-scope-defer/input.json +0 -14
  80. package/pipeline/eval/triage/03-out-of-scope-defer/notes.md +0 -10
  81. package/pipeline/eval/triage/04-false-positive-reject/expected.json +0 -18
  82. package/pipeline/eval/triage/04-false-positive-reject/input.json +0 -14
  83. package/pipeline/eval/triage/04-false-positive-reject/notes.md +0 -10
  84. package/pipeline/eval/triage/05-mixed-classification/expected.json +0 -43
  85. package/pipeline/eval/triage/05-mixed-classification/input.json +0 -38
  86. package/pipeline/eval/triage/05-mixed-classification/notes.md +0 -17
  87. package/pipeline/eval/triage/06-severity-mismatch/expected.json +0 -15
  88. package/pipeline/eval/triage/06-severity-mismatch/input.json +0 -14
  89. package/pipeline/eval/triage/06-severity-mismatch/notes.md +0 -9
  90. package/pipeline/eval/triage/07-duplicate-reviewers/expected.json +0 -27
  91. package/pipeline/eval/triage/07-duplicate-reviewers/input.json +0 -22
  92. package/pipeline/eval/triage/07-duplicate-reviewers/notes.md +0 -9
  93. package/pipeline/eval/triage/08-style-misclassified/expected.json +0 -18
  94. package/pipeline/eval/triage/08-style-misclassified/input.json +0 -14
  95. package/pipeline/eval/triage/08-style-misclassified/notes.md +0 -9
  96. package/pipeline/eval/triage/09-cascading-finding/expected.json +0 -23
  97. package/pipeline/eval/triage/09-cascading-finding/input.json +0 -22
  98. package/pipeline/eval/triage/09-cascading-finding/notes.md +0 -9
  99. package/pipeline/eval/triage/10-deferred-crossref/expected.json +0 -18
  100. package/pipeline/eval/triage/10-deferred-crossref/input.json +0 -14
  101. package/pipeline/eval/triage/10-deferred-crossref/notes.md +0 -9
  102. package/pipeline/eval/triage/11-vercel-token-leak-blocker/expected.json +0 -27
  103. package/pipeline/eval/triage/11-vercel-token-leak-blocker/input.json +0 -22
  104. package/pipeline/eval/triage/11-vercel-token-leak-blocker/notes.md +0 -14
  105. package/pipeline/eval/triage/README.md +0 -54
  106. package/pipeline/scripts/benchmark-phase-0.sh +0 -128
  107. package/pipeline/scripts/check-md-links.mjs +0 -88
  108. package/pipeline/scripts/eval-golden-tasks-live.mjs +0 -302
  109. package/pipeline/scripts/eval-golden-tasks.mjs +0 -224
  110. package/pipeline/scripts/eval-intent.mjs +0 -107
  111. package/pipeline/scripts/eval-mine-corpus.mjs +0 -211
  112. package/pipeline/scripts/eval-triage.mjs +0 -171
  113. package/pipeline/scripts/fixtures/diff-risk-android.diff +0 -40
  114. package/pipeline/scripts/fixtures/diff-risk-ios.diff +0 -48
  115. package/pipeline/scripts/fixtures/diff-risk-test-removal.diff +0 -40
  116. package/pipeline/scripts/fixtures/install-layout.tsv +0 -19
  117. package/pipeline/scripts/fixtures/pack-expected-count.txt +0 -1
  118. package/pipeline/scripts/fixtures/test-gap-node.diff +0 -30
  119. package/pipeline/scripts/fixtures/test-gap-python.diff +0 -32
  120. package/pipeline/scripts/lint-mcp-refs.mjs +0 -218
  121. package/pipeline/scripts/lint-skills.mjs +0 -154
  122. package/pipeline/scripts/run-smokes.mjs +0 -130
  123. package/pipeline/scripts/scorecard.mjs +0 -258
  124. package/pipeline/scripts/smoke-add-detail.sh +0 -137
  125. package/pipeline/scripts/smoke-agent-guard.sh +0 -74
  126. package/pipeline/scripts/smoke-agent-log-cost.sh +0 -262
  127. package/pipeline/scripts/smoke-agent-model-routing.sh +0 -87
  128. package/pipeline/scripts/smoke-ask-choice.sh +0 -42
  129. package/pipeline/scripts/smoke-autopilot-circuit-breaker.sh +0 -36
  130. package/pipeline/scripts/smoke-bitbucket-contract.sh +0 -255
  131. package/pipeline/scripts/smoke-changelog-version.sh +0 -47
  132. package/pipeline/scripts/smoke-channels-approval-gate.sh +0 -60
  133. package/pipeline/scripts/smoke-channels-flow.sh +0 -130
  134. package/pipeline/scripts/smoke-ci-workflows.sh +0 -88
  135. package/pipeline/scripts/smoke-clarify.sh +0 -148
  136. package/pipeline/scripts/smoke-command-inventory.sh +0 -81
  137. package/pipeline/scripts/smoke-commands-skills-parity.sh +0 -87
  138. package/pipeline/scripts/smoke-community-gates.sh +0 -75
  139. package/pipeline/scripts/smoke-compliance-skills.sh +0 -119
  140. package/pipeline/scripts/smoke-config-hygiene.sh +0 -58
  141. package/pipeline/scripts/smoke-context-budget.sh +0 -72
  142. package/pipeline/scripts/smoke-cost-budget.sh +0 -70
  143. package/pipeline/scripts/smoke-cost-summary.sh +0 -139
  144. package/pipeline/scripts/smoke-cross-phase-cohesion.sh +0 -128
  145. package/pipeline/scripts/smoke-description-tr.sh +0 -82
  146. package/pipeline/scripts/smoke-dev-critic.sh +0 -144
  147. package/pipeline/scripts/smoke-diff-explain.sh +0 -147
  148. package/pipeline/scripts/smoke-diff-risk.sh +0 -190
  149. package/pipeline/scripts/smoke-dynamic-skill-loading.sh +0 -160
  150. package/pipeline/scripts/smoke-eval-live.sh +0 -136
  151. package/pipeline/scripts/smoke-evidence-gate.sh +0 -93
  152. package/pipeline/scripts/smoke-extract-conventions.sh +0 -163
  153. package/pipeline/scripts/smoke-fetchers-offline.sh +0 -448
  154. package/pipeline/scripts/smoke-figma-dispatch.sh +0 -112
  155. package/pipeline/scripts/smoke-gate-hooks.sh +0 -74
  156. package/pipeline/scripts/smoke-gc-tmp.sh +0 -130
  157. package/pipeline/scripts/smoke-gc-worktrees.sh +0 -125
  158. package/pipeline/scripts/smoke-generate-issue.sh +0 -120
  159. package/pipeline/scripts/smoke-handoff-contract.sh +0 -92
  160. package/pipeline/scripts/smoke-identity-isolation.sh +0 -70
  161. package/pipeline/scripts/smoke-install-layout.sh +0 -248
  162. package/pipeline/scripts/smoke-intent-guard.sh +0 -86
  163. package/pipeline/scripts/smoke-issue-comment-template.sh +0 -86
  164. package/pipeline/scripts/smoke-issue-jira-triad.sh +0 -120
  165. package/pipeline/scripts/smoke-keychain.sh +0 -158
  166. package/pipeline/scripts/smoke-language-axis.sh +0 -109
  167. package/pipeline/scripts/smoke-learning-curve.sh +0 -61
  168. package/pipeline/scripts/smoke-learnings-ledger.sh +0 -86
  169. package/pipeline/scripts/smoke-lib-scripts.sh +0 -448
  170. package/pipeline/scripts/smoke-mcp-gate.sh +0 -68
  171. package/pipeline/scripts/smoke-md-links.sh +0 -8
  172. package/pipeline/scripts/smoke-md2confluence.sh +0 -126
  173. package/pipeline/scripts/smoke-metrics-cache-ratio.sh +0 -72
  174. package/pipeline/scripts/smoke-migrate-state.sh +0 -102
  175. package/pipeline/scripts/smoke-mode-dispatch-drift.sh +0 -161
  176. package/pipeline/scripts/smoke-model-fallback.sh +0 -89
  177. package/pipeline/scripts/smoke-multi-repo-integration.sh +0 -116
  178. package/pipeline/scripts/smoke-multi-repo-worktree.sh +0 -61
  179. package/pipeline/scripts/smoke-no-mcp-in-dev-phases.sh +0 -194
  180. package/pipeline/scripts/smoke-no-token-prompt.sh +0 -85
  181. package/pipeline/scripts/smoke-own-punctuation.sh +0 -103
  182. package/pipeline/scripts/smoke-pack-contents.sh +0 -140
  183. package/pipeline/scripts/smoke-pat-audit.sh +0 -128
  184. package/pipeline/scripts/smoke-per-repo-memory.sh +0 -156
  185. package/pipeline/scripts/smoke-phase-0-multi-repo.sh +0 -170
  186. package/pipeline/scripts/smoke-phase-6-multi.sh +0 -79
  187. package/pipeline/scripts/smoke-phase-banner.sh +0 -101
  188. package/pipeline/scripts/smoke-phase-tracker.sh +0 -324
  189. package/pipeline/scripts/smoke-phase0-bridge-contract.sh +0 -241
  190. package/pipeline/scripts/smoke-phase4-gates.sh +0 -45
  191. package/pipeline/scripts/smoke-phase4-triage.sh +0 -229
  192. package/pipeline/scripts/smoke-plan-approval-gate.sh +0 -71
  193. package/pipeline/scripts/smoke-plan-safety.sh +0 -139
  194. package/pipeline/scripts/smoke-plan-todos.sh +0 -196
  195. package/pipeline/scripts/smoke-pr-review-actions.sh +0 -152
  196. package/pipeline/scripts/smoke-pre-commit.sh +0 -170
  197. package/pipeline/scripts/smoke-pref-migration.sh +0 -226
  198. package/pipeline/scripts/smoke-prefs-language.sh +0 -134
  199. package/pipeline/scripts/smoke-progress-contract.sh +0 -127
  200. package/pipeline/scripts/smoke-prune-logs.sh +0 -137
  201. package/pipeline/scripts/smoke-purge.sh +0 -138
  202. package/pipeline/scripts/smoke-push-retry.sh +0 -75
  203. package/pipeline/scripts/smoke-repo-map.sh +0 -300
  204. package/pipeline/scripts/smoke-review-readiness.sh +0 -92
  205. package/pipeline/scripts/smoke-review-watch.sh +0 -146
  206. package/pipeline/scripts/smoke-routines.sh +0 -84
  207. package/pipeline/scripts/smoke-run-aggregator.sh +0 -216
  208. package/pipeline/scripts/smoke-run-metrics.sh +0 -50
  209. package/pipeline/scripts/smoke-search.sh +0 -187
  210. package/pipeline/scripts/smoke-shadow-git.sh +0 -224
  211. package/pipeline/scripts/smoke-skill-authoring.sh +0 -137
  212. package/pipeline/scripts/smoke-skill-language.sh +0 -83
  213. package/pipeline/scripts/smoke-skill-manifest.sh +0 -138
  214. package/pipeline/scripts/smoke-skill-scan.sh +0 -198
  215. package/pipeline/scripts/smoke-source-parity.sh +0 -85
  216. package/pipeline/scripts/smoke-subagent-validators.sh +0 -108
  217. package/pipeline/scripts/smoke-sync-parity.sh +0 -92
  218. package/pipeline/scripts/smoke-tasklist-ordering.sh +0 -112
  219. package/pipeline/scripts/smoke-telemetry.sh +0 -147
  220. package/pipeline/scripts/smoke-test-gap.sh +0 -183
  221. package/pipeline/scripts/smoke-token-budget.sh +0 -67
  222. package/pipeline/scripts/smoke-token-preflight.sh +0 -82
  223. package/pipeline/scripts/smoke-tracker-contract.sh +0 -191
  224. package/pipeline/scripts/smoke-tracker-tokens-invocation.sh +0 -73
  225. package/pipeline/scripts/smoke-triage-memory.sh +0 -174
  226. package/pipeline/scripts/smoke-update-check.sh +0 -135
  227. package/pipeline/scripts/smoke-url-enrichment.sh +0 -70
  228. package/pipeline/scripts/smoke-validate-analysis-doc.sh +0 -161
  229. package/pipeline/scripts/smoke-validator-contradiction.sh +0 -67
  230. package/pipeline/scripts/smoke-validator-gates.sh +0 -164
  231. package/pipeline/scripts/smoke-vercel-deploy-redact.sh +0 -129
  232. package/pipeline/scripts/smoke-verify-by-test.sh +0 -148
  233. package/pipeline/scripts/smoke-wiki-integration.sh +0 -122
  234. package/pipeline/scripts/smoke-work-summary.sh +0 -163
  235. package/pipeline/scripts/smoke-workflow-audit.sh +0 -101
  236. package/pipeline/scripts/smoke-worktree-path-convention.sh +0 -86
  237. package/pipeline/scripts/smoke-wrapper-preservation.sh +0 -68
  238. package/pipeline/scripts/smoke-write-state.sh +0 -159
  239. package/pipeline/scripts/sync-parity-check.sh +0 -135
  240. package/pipeline/scripts/test-gap-rules/android.json +0 -25
  241. package/pipeline/scripts/test-gap-rules/ios.json +0 -34
  242. package/pipeline/scripts/test-gap-rules/node.json +0 -29
  243. package/pipeline/scripts/test-gap-rules/python.json +0 -25
  244. 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