@mmerterden/multi-agent-pipeline 20.27.2 → 20.29.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 (99) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/README.md +20 -6
  3. package/README.tr.md +6 -5
  4. package/docs/adr/0008-installer-modularization-and-secret-leak-defense.md +0 -1
  5. package/docs/architecture.md +4 -4
  6. package/docs/ecosystem.md +5 -5
  7. package/docs/facts.json +14 -12
  8. package/docs/features.md +2 -0
  9. package/install/_dev-only-files.mjs +1 -0
  10. package/install/catalog-history.json +1 -1
  11. package/install/claude.mjs +112 -0
  12. package/install/index.mjs +0 -2
  13. package/install/templates/claude-hooks.json +30 -1
  14. package/manifest.json +101 -67
  15. package/package.json +4 -1
  16. package/pipeline/agents/code-reviewer.md +1 -0
  17. package/pipeline/agents/test-gen-analyst.md +98 -0
  18. package/pipeline/commands/multi-agent/analysis/SKILL.md +3 -1
  19. package/pipeline/commands/multi-agent/resume/SKILL.md +2 -1
  20. package/pipeline/commands/multi-agent/sync/SKILL.md +3 -3
  21. package/pipeline/commands/multi-agent/test-gen/SKILL.md +124 -0
  22. package/pipeline/contract/types/index.d.ts +1 -1
  23. package/pipeline/lib/hook-registration.mjs +252 -0
  24. package/pipeline/lib/prefs-value.mjs +44 -0
  25. package/pipeline/lib/run-completion.mjs +148 -0
  26. package/pipeline/multi-agent-refs/analysis/render.md +4 -4
  27. package/pipeline/multi-agent-refs/cross-cli-contract.md +11 -5
  28. package/pipeline/multi-agent-refs/features/doctor.md +24 -0
  29. package/pipeline/multi-agent-refs/features/test-classification.md +76 -0
  30. package/pipeline/multi-agent-refs/features/usage-reporting.md +17 -0
  31. package/pipeline/multi-agent-refs/help/en.md +1 -0
  32. package/pipeline/multi-agent-refs/help/tr.md +1 -0
  33. package/pipeline/multi-agent-refs/phases/operations.md +19 -2
  34. package/pipeline/multi-agent-refs/phases/phase-0-init.md +6 -2
  35. package/pipeline/multi-agent-refs/phases/phase-1-plan.md +2 -2
  36. package/pipeline/multi-agent-refs/phases/phase-2-dev.md +7 -8
  37. package/pipeline/multi-agent-refs/phases/phase-3-review.md +5 -5
  38. package/pipeline/multi-agent-refs/phases/phase-4-commit.md +7 -6
  39. package/pipeline/multi-agent-refs/phases/phase-5-report.md +10 -8
  40. package/pipeline/multi-agent-refs/test-gen/stages.md +227 -0
  41. package/pipeline/preferences-template.json +10 -0
  42. package/pipeline/rules/figma-pipeline.md +2 -0
  43. package/pipeline/schemas/agent-state.schema.json +20 -5
  44. package/pipeline/schemas/phases.json +3 -2
  45. package/pipeline/schemas/phases.schema.json +5 -1
  46. package/pipeline/schemas/prefs.schema.json +51 -0
  47. package/pipeline/schemas/runs-index.schema.json +1 -1
  48. package/pipeline/schemas/test-gen-discovery.schema.json +73 -0
  49. package/pipeline/schemas/test-gen-gaps.schema.json +122 -0
  50. package/pipeline/schemas/test-gen-inventory.schema.json +59 -0
  51. package/pipeline/schemas/test-gen-results.schema.json +44 -0
  52. package/pipeline/scripts/_active-run.mjs +158 -0
  53. package/pipeline/scripts/_autopilot-outcomes.mjs +1 -0
  54. package/pipeline/scripts/_guard_rules.py +605 -0
  55. package/pipeline/scripts/_handle-patterns.mjs +166 -0
  56. package/pipeline/scripts/_http-post.mjs +141 -0
  57. package/pipeline/scripts/_stack-routing.mjs +4 -0
  58. package/pipeline/scripts/_test-gaming.mjs +227 -0
  59. package/pipeline/scripts/_unified-diff.mjs +104 -0
  60. package/pipeline/scripts/_worktree-snapshot.mjs +113 -0
  61. package/pipeline/scripts/agent-guard.py +118 -7
  62. package/pipeline/scripts/agent-guard.sh +50 -3
  63. package/pipeline/scripts/argv-secret-scan.mjs +114 -0
  64. package/pipeline/scripts/autopilot-runner.mjs +8 -2
  65. package/pipeline/scripts/confirm-command.mjs +70 -0
  66. package/pipeline/scripts/doctor.mjs +105 -0
  67. package/pipeline/scripts/evidence-gate.mjs +82 -7
  68. package/pipeline/scripts/gate-ledger.mjs +163 -7
  69. package/pipeline/scripts/handle-diff-audit.mjs +188 -0
  70. package/pipeline/scripts/plan-coverage-gate.mjs +9 -8
  71. package/pipeline/scripts/pre-commit-check.sh +12 -0
  72. package/pipeline/scripts/routine-registry.mjs +5 -1
  73. package/pipeline/scripts/run-scope-guard.mjs +188 -0
  74. package/pipeline/scripts/run-scope-guard.sh +23 -0
  75. package/pipeline/scripts/run-stop-guard.mjs +246 -0
  76. package/pipeline/scripts/run-stop-guard.sh +25 -0
  77. package/pipeline/scripts/run-ui-tests.sh +27 -3
  78. package/pipeline/scripts/test-classify.mjs +296 -0
  79. package/pipeline/scripts/test-gen-gate.mjs +409 -0
  80. package/pipeline/scripts/test-gen-state.mjs +313 -0
  81. package/pipeline/scripts/test-integrity-gate.mjs +76 -11
  82. package/pipeline/scripts/uninstall.mjs +2 -0
  83. package/pipeline/scripts/usage-register-beacon.mjs +239 -0
  84. package/pipeline/scripts/usage-register.mjs +128 -33
  85. package/pipeline/scripts/usage-report.mjs +43 -33
  86. package/pipeline/scripts/validate-state.mjs +26 -0
  87. package/pipeline/scripts/write-state.mjs +114 -0
  88. package/pipeline/skills/.skill-manifest.json +23 -3
  89. package/pipeline/skills/.skills-index.json +57 -2
  90. package/pipeline/skills/shared/README.md +10 -5
  91. package/pipeline/skills/shared/core/multi-agent-sync/SKILL.md +3 -3
  92. package/pipeline/skills/shared/core/multi-agent-test-gen/SKILL.md +120 -0
  93. package/pipeline/skills/shared/external/fix-review-verdicts/SKILL.md +133 -0
  94. package/pipeline/skills/shared/external/handle-request/SKILL.md +97 -0
  95. package/pipeline/skills/shared/external/scenario-gap-analysis/SKILL.md +129 -0
  96. package/pipeline/skills/shared/external/scenario-gap-analysis/references/scenario-schema.md +60 -0
  97. package/pipeline/skills/shared/external/test-result-triage/SKILL.md +160 -0
  98. package/pipeline/skills/skills-index.md +7 -2
  99. package/install/_telemetry.mjs +0 -61
@@ -25,7 +25,7 @@ Progress emission per `$HOME/.claude/multi-agent-refs/progress-contract.md` -
25
25
 
26
26
  #### Input contract
27
27
 
28
- Phase 4 consumes the latest Phase 3 triage output object conforming to `$HOME/.claude/schemas/triage-output.schema.json` (`accepted`, `deferred`, `rejected` buckets) plus the working-tree mutations from Phase 2. The commit body cites only `accepted` findings that were resolved; `deferred` items must be linked in the PR description as follow-up work, never silently dropped. If the triage output reports any unresolved blocking accepted finding, Phase 4 refuses to commit - the pipeline returns to Phase 2 for rework.
28
+ Phase 4 consumes the latest Phase 3 triage output (`$HOME/.claude/schemas/triage-output.schema.json` (`accepted`, `deferred`, `rejected` buckets) and the Phase 2 working tree. The commit body cites only `accepted` findings that were resolved; `deferred` items must be linked in the PR description as follow-up work, never silently dropped. An unresolved blocking accepted finding sends the run back to Phase 2.
29
29
 
30
30
  "Build passes" below is evidence-gated: Phase 4 trusts the `buildStatus.ok` Phase 2 / Phase 3 recorded through `$HOME/.claude/scripts/evidence-gate.mjs` (a pass with no build/test log behind it is unverified and blocks the commit). The secret scanner (`pre-commit-check.sh`) runs on the staged diff as the final pre-commit gate.
31
31
 
@@ -44,7 +44,7 @@ Exit 0 passes. Exit 2 with `.verdict` `not-applicable` in `$PC` is `COVERAGE_NA`
44
44
 
45
45
  A step is accounted for when `completed`, `skipped` + `skipReason`, or `failed` + `failureReason`; a bare `pending` / `in_progress` is the gap. Every Section 14 `Add new` row must name a file in the tree; an empty `docPath[]` reports that half `skipped`, never `passed`.
46
46
 
47
- `COVERAGE_GAP` (a gap, a zero-step plan, unreadable input): show `$PC` verbatim, then return to Phase 2 or have the user mark each step deliberately (`plan-todos.sh` writes the reason); never proceed without that decision. `COVERAGE_NA`: log `plan-coverage: n/a` and continue. Gated runs: `$HOME/.claude/multi-agent-refs/features/unattended-gates.md`.
47
+ `COVERAGE_GAP` (a gap, a zero-step plan, unreadable input): show `$PC` verbatim, then return to Phase 2 or have the user mark each step deliberately (`plan-todos.sh` writes the reason); never proceed without that decision, and re-run the gate once the steps are marked. `COVERAGE_NA`: log `plan-coverage: n/a` and continue. Gated runs: `$HOME/.claude/multi-agent-refs/features/unattended-gates.md`.
48
48
 
49
49
  #### Step 0 - Multi-Repo Integration Build
50
50
 
@@ -84,7 +84,7 @@ Branch **deterministically**, no implicit fallback. Read `agent-state.json` and
84
84
 
85
85
  **Instruction-driven path:**
86
86
 
87
- 1. Ask: "Want to commit?" (skip in autopilot)
87
+ 1. Ask: "Want to commit?" (skip in autopilot; operations.md "Attended waits")
88
88
  2. Read commit instruction file (e.g. `.instructions/figma/figma-iteration-commit/SKILL.md`)
89
89
  3. Follow its flow (build validation, test, submodule commits, push, PR, issue management)
90
90
  4. Log: "Phase 4: instruction-driven commit - file={instructionFiles.commit}"
@@ -111,8 +111,9 @@ Branch **deterministically**, no implicit fallback. Read `agent-state.json` and
111
111
  - Commit message uses same convention but scope reflects submodule: `{type}({submodule-scope}): {description} [{jiraId}]`
112
112
  6. Commit with convention: `{type}({scope}): {description} [{jiraId}]` or `[#{shortId}]`. When `node $HOME/.claude/scripts/repo-profile.mjs resolve commit.format --repo "$PROJECT_ROOT"` returns `use: true`, its `ticketPosition` places the ticket (`prefix` first, `suffix` last; `none` keeps this default unless confirmed or manual), `ticketStyle` its form and `scopeRequired` the scope. A profile hook with `amends-commit` or `writes-files` rewrites the commit: re-read the SHA after committing ([`features/repo-profile.md`]($HOME/.claude/multi-agent-refs/features/repo-profile.md)). **Local-only repos** (`projects[i].provider == "local"`) have no ticket, and that wins over any profile placement: the subject keeps the conventional prefix with no ticket, and the taskId (`LOCAL-...`) goes in the body footer: `Local-Task: {taskId}`.
113
113
  6a. **Valven (required):** `features/valven.md`.
114
+ 6b. **Record the commit**: operations.md "Completion evidence".
114
115
  7. Push to remote - **skip per-repo when `projects[i].provider == "local"`** (no `git push`, no upstream config). Log `Phase 4: local-only commit {sha} (no push)` for that repo.
115
- 8. Ask: "Want to open a Pull Request?"
116
+ 8. Ask: "Want to open a Pull Request?" (attended wait, as in step 1)
116
117
  - **Skip the prompt entirely when `state.offlineOnly == true`** (all repos local - no PR target exists). Proceed to Phase 5 with `pr.status = "skipped-offline"` for each project.
117
118
  - In mixed mode (some local, some remote), only prompt for the remote-backed repos; local ones auto-skip.
118
119
  - No -> Phase 5
@@ -126,7 +127,7 @@ Branch **deterministically**, no implicit fallback. Read `agent-state.json` and
126
127
  --task-id "$TASK_ID" --project "$PROJECT" --branch "$BRANCH")
127
128
  ```
128
129
 
129
- The script salvages before removing, keeps the branch, and does **not** check it out - the user's HEAD and uncommitted work stay put. On success it stamps `worktreeRemovedAt`, `artifactsPath` and `worktreePath: null` into the **salvaged** `agent-state.json` itself. Do NOT write those via `write-state.mjs "$STATE_FILE"`: in single-repo mode that path went away with the worktree, so the write fails, the fields land nowhere, and Phase 5 reads a dead path and silently skips the triage ingest. After a removal re-point `STATE_FILE` at `$(jq -r .artifactsPath <<< "$FIN")/agent-state.json`. Exit 3 is a safe skip (uncommitted changes, unpushed or detached HEAD, `--local`, cwd inside the tree, preference off): report and continue. Never `--force`. Contract: [`features/worktree-finalize.md`]($HOME/.claude/multi-agent-refs/features/worktree-finalize.md).
130
+ The script salvages before removing, keeps the branch, and does **not** check it out - the user's HEAD and uncommitted work stay put. On success it stamps `worktreeRemovedAt`, `artifactsPath` and `worktreePath: null` into the **salvaged** `agent-state.json` itself. Do NOT write those via `write-state.mjs "$STATE_FILE"`: that path left with the worktree, and Phase 5 would skip the triage ingest. After a removal re-point `STATE_FILE` at `$(jq -r .artifactsPath <<< "$FIN")/agent-state.json`. Exit 3 is a safe skip (uncommitted changes, unpushed or detached HEAD, `--local`, cwd inside the tree, preference off): report and continue. Never `--force`. Contract: [`features/worktree-finalize.md`]($HOME/.claude/multi-agent-refs/features/worktree-finalize.md).
130
131
 
131
132
  10. **Issue body update** (GitHub Issue only): if the issue body has a `### Pull Requests` section and/or a `### Progress` table, fill in the PR URL(s) (one row per submodule - e.g. `- **common:** {url}`, `- **uicomponents:** {url}`) and flip the Implementation / Testing / Code Connect flags from Pending to Done using whatever marker the template uses (match in place, do NOT introduce new markers). Apply it with `gh issue edit {issueNo} --body-file <file>`, preserving every other section unchanged.
132
133
  11. **NEVER close or resolve the issue** - neither GitHub Issue nor Jira. Issues require team review (4 approvals) before closing. Only post a comment with commit/PR URLs.
@@ -271,7 +272,7 @@ For each repo in `state.projects[]` (sequentially, in dependency order from Phas
271
272
  `repo-scope` is the repo's short name (e.g. `common`, `uicomponents`). The subject and `[tracker-id]` are identical across all repos.
272
273
  3. `git add` (specific files only - same rules as single-repo Step 5)
273
274
  4. `git -C "$WT_PATH" commit -m "$MESSAGE"` (heredoc body) - using identity already pinned in Phase 0 Step 8.
274
- 5. Capture `state.projects[i].commit = "<sha>"`.
275
+ 5. Capture `state.projects[i].commit = {"sha": "<sha>"}`.
275
276
 
276
277
  ##### Push-must-succeed policy (gated by `prefs.global.settings.pushMustSucceed`, default `true`)
277
278
 
@@ -211,21 +211,23 @@ $HOME/.claude/scripts/log-metric.sh "$TASK_ID" 5 task.completed \
211
211
  duration_ms=$TOTAL_DURATION
212
212
  ```
213
213
 
214
- **Operational reporting** (`features/usage-reporting.md`). Mark the run finished in the state, then emit the completed event; both never block the run:
214
+ **Operational reporting** (`features/usage-reporting.md`): mark the run finished, then report it. Neither blocks:
215
215
 
216
216
  ```bash
217
- printf '{"status":"complete","currentPhase":5,"finishedAt":"%s"}' "$(date -u +%Y-%m-%dT%H:%M:%SZ)" \
218
- | node $HOME/.claude/scripts/write-state.mjs "$STATE_FILE" >/dev/null 2>&1 || true
219
- node $HOME/.claude/scripts/usage-report.mjs --state "$STATE_FILE" --event completed >/dev/null 2>&1 || true
217
+ STATE_FILE="${STATE_FILE:-$(node $HOME/.claude/scripts/usage-report.mjs --task-id "$TASK_ID" --print-state)}"
218
+ TS=$(date -u +%Y-%m-%dT%H:%M:%SZ)
219
+ WS_ERR=$(printf '{"status":"complete","currentPhase":5,"finishedAt":"%s"}' "$TS" \
220
+ | node $HOME/.claude/scripts/write-state.mjs "$STATE_FILE" 2>&1 >/dev/null); WS_RC=$?
221
+ [ "$WS_RC" = 0 ] && node $HOME/.claude/scripts/usage-report.mjs --task-id "$TASK_ID" --event completed >/dev/null 2>&1
220
222
  ```
221
223
 
222
- Parked on the channels choice instead: emit `--event parked` and leave `status` alone.
224
+ `WS_RC` 6: `$WS_ERR` names what `complete` still lacks; record it (operations.md "Completion evidence") and write again, or halt. Parked on the channels choice instead: emit `--event parked` and leave `status` alone.
223
225
 
224
- **Aggregate before reporting**: run `aggregate-metrics.mjs --since=$(date -u -v-30d +%Y-%m-%d)` and embed output in agent-log.md. Use `--json` for Jira. Aggregator handles missing files gracefully.
226
+ **Aggregate before reporting**: run `aggregate-metrics.mjs --since=$(date -u -v-30d +%Y-%m-%d)` and embed output in agent-log.md. Use `--json` for Jira; missing files are skipped.
225
227
 
226
- **Per-run outcome metrics (evidence corpus):** also emit `run-metrics.mjs --state <agent-state.json>` and append/persist its JSON. It records review iterations, first-pass-clean, reviewer signal-to-noise, consensus verdict and build outcome, so quality across real runs is measured, not asserted.
228
+ **Per-run outcome metrics (evidence corpus):** also emit `run-metrics.mjs --state <agent-state.json>` and append/persist its JSON. It records review iterations, first-pass-clean, reviewer signal-to-noise, consensus verdict and build outcome.
227
229
 
228
- **Cost Breakdown emission (mandatory):** as part of agent-log compose, append the per-task cost block produced by `render-agent-log-cost.sh`:
230
+ **Cost Breakdown emission (required):** as part of agent-log compose, append the per-task cost block produced by `render-agent-log-cost.sh`:
229
231
 
230
232
  ```bash
231
233
  bash $HOME/.claude/scripts/render-agent-log-cost.sh "$TASK_ID" --append "$AGENT_LOG" 2>/dev/null
@@ -0,0 +1,227 @@
1
+ # test-gen stages
2
+
3
+ <!-- toc -->
4
+ - [Artefacts and gates](#artefacts-and-gates)
5
+ - [Read-only stages](#read-only-stages)
6
+ - [1. Inventory](#1-inventory)
7
+ - [Priority](#priority)
8
+ - [2. Discovery](#2-discovery)
9
+ - [3. Gaps](#3-gaps)
10
+ - [4. Handles](#4-handles)
11
+ - [5. Write](#5-write)
12
+ - [6. Run](#6-run)
13
+ - [7. Classify](#7-classify)
14
+ - [8. Triage](#8-triage)
15
+ - [End: the change base](#end-the-change-base)
16
+ <!-- /toc -->
17
+
18
+ The contract behind `/multi-agent:test-gen`. Each stage is a subagent with one
19
+ artefact under `$RUN` (`$HOME/.claude/logs/test-gen/<repo>/<timestamp>/`), and
20
+ no stage starts until the previous artefact has passed its gate. A stage that
21
+ says it is done without its artefact on disk is not done.
22
+
23
+ ## Artefacts and gates
24
+
25
+ | # | Stage | Runs as | Artefact | Gate |
26
+ |---|---|---|---|---|
27
+ | 1 | inventory | orchestrator, with the toolkit inventory tools | `inventory.json` | `test-gen-gate.mjs check --stage inventory`, `evidence-gate.mjs --claim artefact` |
28
+ | - | priority | AskUserQuestion; autopilot takes the top three | `inventory.json` `priority` | `check --stage inventory` again |
29
+ | 2 | discovery | `test-gen-analyst` (read-only) | `discovery.json` | `check --stage discovery`, `evidence-gate.mjs --claim artefact` |
30
+ | 3 | gaps | `coverage_gap`, then `test-gen-analyst` (read-only) | `gaps.json` | `check --stage gaps`, `evidence-gate.mjs --claim artefact` |
31
+ | 4 | handles | general-purpose subagent, product code, handles only | `handles.md` and the stage's delta | `test-gen-gate.mjs handles`, `handle-diff-audit.mjs --baseline` exit 0 |
32
+ | 5 | write | general-purpose subagent, test files only | the test files and `written.json` | `test-gen-gate.mjs written --baseline` |
33
+ | 6 | run | orchestrator | `run-<k>.log` and per-test reports | `evidence-gate.mjs --claim test` |
34
+ | 7 | classify | orchestrator | `results.json` | `check --stage results` |
35
+ | 8 | triage | orchestrator, result by result | `results.json` `triage` | `test-gen-state.mjs revise` |
36
+
37
+ Before each stage take `STAGE_START=$(date -u +%Y-%m-%dT%H:%M:%SZ)`. The
38
+ artefact gate (`--claim artefact --stage-start "$STAGE_START" --evidence <its own
39
+ file>`) rejects a file written before the stage began and a file that is not
40
+ whole (JSON that does not parse). It is applied to the JSON a model writes
41
+ (inventory, discovery, gaps); the scripts write `results.json` and the
42
+ baselines whole.
43
+
44
+ The two stages that may edit the repository (handles, write) start with a
45
+ snapshot: `test-gen-gate.mjs baseline` records the working tree, untracked files
46
+ included, as a git tree without touching it or the index. Their gates read only
47
+ what changed after it, so uncommitted work the person had before the stage is
48
+ never taken for the stage's change, and never reverted to satisfy a gate.
49
+
50
+ ## Read-only stages
51
+
52
+ Discovery and gaps are dispatched with `subagent_type: "test-gen-analyst"`
53
+ (`agents/test-gen-analyst.md`). The persona's grant is an allowlist: Read, Grep,
54
+ Glob and the read-only toolkit tools (`coverage_gap`, the `repo_*` readers, the
55
+ `code_*` language-server queries). No Write, Edit, Bash, Agent or other MCP tool
56
+ is granted, so an analysis stage cannot change the repository however it is
57
+ prompted. It returns JSON; the orchestrator writes it with the Write tool.
58
+ Copilot CLI installs the same persona with the same grant. Codex CLI has no
59
+ per-agent tool grant: there, run the two stages in the main session and change
60
+ nothing during them.
61
+
62
+ ## 1. Inventory
63
+
64
+ Prefer a tool over reading: `design_scenario_inventory` (a screen's states from
65
+ the design), `web_map` (a web app's routes), the UI tree (`ios_get_ui_tree`,
66
+ `android_get_ui_tree`) of each reachable screen, or the OpenAPI spec. Without a
67
+ tool, read the navigation and routes and set `source: "agent"`. Every item has a
68
+ stable `id`: route, screen id, design node id, endpoint path.
69
+
70
+ When a change base exists (`test-gen-state.mjs base-scan` measured), keep the
71
+ items whose `path` is among the scan's `files` or `graph.affectedFiles`, and say
72
+ how many fell away. A scan with `measured: false` narrows nothing: the inventory
73
+ stays whole and the reason is reported.
74
+
75
+ ## Priority
76
+
77
+ Show the items in one table (id, kind, title, existing tests from a quick grep)
78
+ and ask which to cover (AskUserQuestion, multi-select, following
79
+ `multi-agent-refs/picker-contract.md`). Autopilot asks nothing:
80
+ it takes the three items most likely to hide a failure, in this order: no test
81
+ reaches them, they changed since the base, they are flows rather than single
82
+ screens. Record `priority: {mode, selected}`.
83
+
84
+ ## 2. Discovery
85
+
86
+ `test-gen-analyst`, stage `discovery`, with the selected items. It returns test
87
+ roots and runners, the tests that reach each item (joined by identifier), the
88
+ handles already present, and the conventions for new tests. Save as
89
+ `discovery.json` and gate it with `check --stage discovery`.
90
+
91
+ ## 3. Gaps
92
+
93
+ With `coverage_gap` (toolkit >= v3.25.0): call it with the inventory and the
94
+ test root from discovery; its `covered`, `uncovered` and `unmatched` buckets go
95
+ into the analyst's prompt, and `coverage.method` is `coverage_gap`. Without it:
96
+ `coverage.method` is `agent` and every match the analyst makes is
97
+ `method: "inferred"`. Then `test-gen-analyst`, stage `gaps`. Save its answer as
98
+ `gaps.json`. The scenario-gap-analysis skill (ai-common-toolkit) defines the
99
+ buckets and the scenario fields when it is enabled.
100
+
101
+ Before writing any test, show the gaps and the `dataNeeds[]`. Each need is
102
+ named, never filled: the person supplies the value, or names the credential the
103
+ test resolves by `secretRef`. Autopilot writes only the scenarios whose needs
104
+ are already met and lists the rest as blocked by data.
105
+
106
+ ## 4. Handles
107
+
108
+ Only gaps with `handlesNeeded`. Before dispatch:
109
+
110
+ ```bash
111
+ node "$HOME/.claude/scripts/test-gen-gate.mjs" baseline --repo "$REPO" --out "$RUN/handles-baseline.json"
112
+ ```
113
+
114
+ The subagent adds the missing handles and nothing else: iOS
115
+ `accessibilityIdentifier`, Compose `testTag` (or `testTagsAsResourceId` in a
116
+ semantics block), web `data-testid`. Never a list index, never a renamed or
117
+ moved element, never a handle the accessibility tree cannot see. It writes
118
+ `handles.md`, one line per request: `ADDED file:line`, `ALREADY file:line`,
119
+ `COULD NOT ADD <reason>` (the handle-request skill, when enabled). When nothing
120
+ was missing, the first line is `HANDLES: none-needed` followed by the
121
+ `ALREADY file:line` lines that prove it. Then:
122
+
123
+ ```bash
124
+ node "$HOME/.claude/scripts/test-gen-gate.mjs" handles --repo "$REPO" \
125
+ --baseline "$RUN/handles-baseline.json" --handles "$RUN/handles.md"
126
+ node "$HOME/.claude/scripts/handle-diff-audit.mjs" --repo "$REPO" \
127
+ --baseline "$RUN/handles-baseline.json" > "$RUN/handle-audit.json"
128
+ ```
129
+
130
+ `handles` passes when the stage added a handle line, or on
131
+ `HANDLES: none-needed` with at least one cited line, inside the repository, that
132
+ carries a handle. `handle-diff-audit.mjs` must exit 0: 2 means the stage changed
133
+ more than handles (`violations[]` name the file and the lost, extra or
134
+ reordered tokens), so revert those hunks of the stage's own change and rerun; 3
135
+ means it could not read the diff, which is not a pass.
136
+
137
+ ## 5. Write
138
+
139
+ Take a new snapshot first (`test-gen-gate.mjs baseline --out
140
+ "$RUN/write-baseline.json"`), then one subagent per platform, with the gaps,
141
+ discovery's conventions and the handles. With `ui_locator_codegen` (toolkit >= v3.25.0), locators come from the
142
+ UI tree node: `{platform, node}` returns the XCUITest or Compose code, or
143
+ `NO_HANDLE` with the handle to add, which goes back to stage 4. Without it,
144
+ write locators from the handles by hand. A locator is a handle, never a list
145
+ index or a visible string that is translated.
146
+
147
+ Each test asserts the gap's `expectation`. Test data comes from `secretRef` or
148
+ the fixtures the person supplied, never a literal secret. The subagent writes
149
+ `written.json`: `{files: [...], command: "<runs exactly these tests>",
150
+ tests: [{id, file}]}`. Then run the command once with a per-test report and:
151
+
152
+ ```bash
153
+ node "$HOME/.claude/scripts/test-gen-gate.mjs" written --repo "$REPO" \
154
+ --baseline "$RUN/write-baseline.json" --written "$RUN/written.json" \
155
+ --discovery "$RUN/discovery.json" --junit "$RUN/run-1.junit.xml"
156
+ ```
157
+
158
+ (`--xcresult-json` or `--log` where the runner writes no JUnit.) The gate fails
159
+ when the stage changed anything but test files, when a listed file is outside
160
+ the repository, missing, empty or not changed by the stage, when the stage's
161
+ diff carries test gaming (an environment branch, a swallowed error, an inflated
162
+ wait), when the command is none of the runners discovery found, or when the run
163
+ executed zero tests.
164
+
165
+ ## 6. Run
166
+
167
+ Run `written.json`'s command `testStability.repeatCount` times (default 3),
168
+ each run's log and per-test report kept as `run-<k>.log` and
169
+ `run-<k>.junit.xml` (or `.xcresult`). A UI suite goes through
170
+ `run-ui-tests.sh run`, whose `UI_TEST_RESULT` names the report. Each log must
171
+ clear `evidence-gate.mjs --claim test --status passed` before a pass is
172
+ recorded; a failing run is evidence too and is kept.
173
+
174
+ ## 7. Classify
175
+
176
+ `features/test-classification.md`: `test_results_classify` with run 1 as
177
+ `head`, the others as `confirm`, and the project's flake ledger, then
178
+ `test-classify.mjs merge`; without the tool, `test-classify.mjs fallback` over
179
+ the runs. Then:
180
+
181
+ ```bash
182
+ node "$HOME/.claude/scripts/test-gen-gate.mjs" results --classification "$RUN/test-classification.json" \
183
+ --written "$RUN/written.json" --out "$RUN/results.json"
184
+ node "$HOME/.claude/scripts/test-gen-gate.mjs" check --stage results --file "$RUN/results.json"
185
+ ```
186
+
187
+ `results.json` is three-state: `pass`, `fail` (a regression, an unstable test, a
188
+ preexisting failure), `not_measured` (no base, a setup failure); every non-pass
189
+ carries its reason. `preexisting` is a fail here although Gate 3 inherits it: the
190
+ test is new, so red on the base too means the behaviour it asserts is missing,
191
+ which is a finding for triage, not someone else's failure.
192
+
193
+ ## 8. Triage
194
+
195
+ Each `fail` and `not_measured` gets one verdict (the test-result-triage skill,
196
+ when enabled): `product-bug`, `stale-test`, `new-scenario`, `valid` or
197
+ `ambiguous`, and `ambiguous` is the default. `stale-test` needs the `file:line`
198
+ in the diff that changed the behaviour on purpose; a locator that found nothing
199
+ goes back to stage 4 before it may be called stale. A green test whose subject
200
+ changed behaviour is questioned too. Record each verdict, then:
201
+
202
+ ```bash
203
+ node "$HOME/.claude/scripts/test-gen-state.mjs" revise --state "$TG_STATE" --run "$RUN_ID" \
204
+ --test "<id>" --verdict "<verdict>" ${AUTOPILOT:+--autopilot}
205
+ ```
206
+
207
+ | Action | Then |
208
+ |---|---|
209
+ | `apply` | revise the test, rerun from stage 6 for that test |
210
+ | `park` | autopilot stops, because the verdict is `product-bug` (never applied without a person) or the stale-test cap (`prefs.global.testGen.revisionCap`, per run and per day) is reached. Report and end |
211
+ | `ask` | attended `product-bug`: show the evidence and ask whether to open it through `/multi-agent` |
212
+ | `none` | keep the verdict in `results.json` |
213
+
214
+ The count is read and written under a lock, so parallel triage cannot pass the
215
+ cap. A state or prefs file that does not parse is never counted as empty:
216
+ autopilot parks (exit 3), an attended run stops with exit 4 and records nothing.
217
+
218
+ ## End: the change base
219
+
220
+ After the report, record where this run ended so the next one diffs from here:
221
+
222
+ ```bash
223
+ node "$HOME/.claude/scripts/test-gen-state.mjs" base-record --state "$TG_STATE" --repo "$REPO" --run "$RUN_ID"
224
+ ```
225
+
226
+ The reason names the run (`test-gen run <id> finished`). It records which code
227
+ the tests were written against, not that the app is understood.
@@ -106,6 +106,16 @@
106
106
  },
107
107
  "resume": {
108
108
  "autoFix": false
109
+ },
110
+ "testGen": {
111
+ "revisionCap": 20
112
+ },
113
+ "attendedCommitGate": {
114
+ "mode": "warn"
115
+ },
116
+ "readOnlyRepos": [],
117
+ "stopGuard": {
118
+ "enabled": true
109
119
  }
110
120
  },
111
121
  "projects": {},
@@ -98,6 +98,8 @@ Re-fetching Figma in dev phases:
98
98
 
99
99
  The repo carries `pipeline/scripts/smoke-no-mcp-in-dev-phases.sh`, which reads a state file's `state.telemetry.mcpCalls[]`: a Figma tool entry with `phase >= 2` fails it, and a Figma entry with no phase is rejected as unjudgeable. It is a maintainer-side regression check: it runs locally as part of `npm test` and the pre-push hook, not in CI (the CI workflow is dormant) and not after every pipeline run.
100
100
 
101
+ During a run on Claude Code the rule is enforced by a hook: `run-scope-guard.sh` is registered on PreToolUse `mcp__.*[Ff]igma.*` (`install/templates/claude-hooks.json`). For the run bound to the session it denies a Figma call above `thresholds.mcpAllowedThroughPhase` of `schemas/phases.json` (Phase 1, where Step 1.4 captures the evidence record) unless the run is in `analysis` mode, and it appends every allowed call to `telemetry.mcpCalls[]` itself. Copilot CLI and Codex have no equivalent hook; there the record is still the phase's own write.
102
+
101
103
  Memory: [[mcp-only-in-analysis]]
102
104
 
103
105
  ### Phase Order (platform-agnostic)
@@ -38,6 +38,10 @@
38
38
  "type": ["string", "null"],
39
39
  "description": "The Claude Code session id this run was launched under, copied by Phase 0 from MULTI_AGENT_SESSION_ID when the launcher set it. Continuous mode sets it to the id it passed as --session-id, and matches this field to find ITS run's state rather than another run in the same repo. Absent or null on an attended run: nothing sets the variable there."
40
40
  },
41
+ "hostSessionId": {
42
+ "type": ["string", "null"],
43
+ "description": "The host session that made this run's last phase transition: write-state.mjs copies CLAUDE_CODE_SESSION_ID from its environment when a write creates the state or sets currentPhase, and never otherwise, so a second session editing the state (steer) does not take the run over. run-stop-guard.mjs and run-scope-guard.mjs match it against a hook payload's session_id to find the run a hook call belongs to. Absent on a host that sets no such variable."
44
+ },
41
45
  "title": {
42
46
  "type": ["string", "null"],
43
47
  "description": "The issue title Phase 0 fetched (GitHub title, Jira summary). runs-index.mjs carries it as the run's title; absent when the input was free text or nothing was fetched."
@@ -275,9 +279,16 @@
275
279
  }
276
280
  },
277
281
  "waitingFor": {
278
- "type": "string",
279
- "enum": ["maturity", "user-channels-choice", "question"],
280
- "description": "The step a paused run must RE-ENTER, as opposed to the phase it should continue past. resume reads this first and falls back to currentPhase + 1 when it is absent. maturity: Phase 0's maturity step, with the item re-fetched (refs/features/maturity-followup.md). user-channels-choice: Phase 5's channels menu, with the stored channelsInput. question: the step named by pendingQuestion.stepId, answered by `resume <id> --answer <optionId>`; open-questions-gate.mjs writes it as the last step of Phase 1 with stepId phase-1/open-questions."
282
+ "type": ["string", "null"],
283
+ "enum": [
284
+ "maturity",
285
+ "user-channels-choice",
286
+ "local-test",
287
+ "attended-reply",
288
+ "question",
289
+ null
290
+ ],
291
+ "description": "The step a paused run must RE-ENTER, as opposed to the phase it should continue past. resume reads this first and falls back to currentPhase + 1 when it is absent or null (null is how a wait is cleared through write-state). maturity: Phase 0's maturity step, with the item re-fetched (refs/features/maturity-followup.md). user-channels-choice: Phase 5's channels menu, with the stored channelsInput. local-test: Phase 3's user-test gate. attended-reply: a typed reply asked for in plain text inside currentPhase; resume re-enters currentPhase and asks again. question: the step named by pendingQuestion.stepId, answered by `resume <id> --answer <optionId>`, and never written without that pendingQuestion; open-questions-gate.mjs writes it as the last step of Phase 1 with stepId phase-1/open-questions. write-state clears every value but question when a write advances currentPhase."
281
292
  },
282
293
  "workspaceSource": {
283
294
  "type": "string",
@@ -497,7 +508,7 @@
497
508
  "status": {
498
509
  "type": "string",
499
510
  "enum": ["in_progress", "awaiting_input", "paused", "complete", "failed", "abandoned"],
500
- "description": "Overall task status. `abandoned` is written through the locked state writer by the autopilot runner, for a run its session launched that ended with neither a PR nor a parked question, and by a kill; `abandonedBy` names the writer. `awaiting_input` is deliberately distinct from both `in_progress` and `paused`: the run is not broken and not resumable by a retry - it is finished with what it can do alone and is holding for a human answer, which is what Phase 5 does by design after the PR is open. Without a word for it, a run whose work had landed read exactly like one that crashed mid-development, and a state file in the wild had already invented `awaiting-user-test-main-checkout` to say it. `paused` stays what it was: stopped deliberately, resumable, nothing expected from anyone."
511
+ "description": "Overall task status. `complete` is earned, not claimed: write-state.mjs refuses a write that moves the run into it unless currentPhase is the mode's last phase and, for a mode that commits, Phase 4 left evidence (commit.sha, pr, publish, projects[].commit, or phases.4 skipped with notes) and gates[] holds a passing plan-coverage verdict (lib/run-completion.mjs); the autopilot runner files a complete state without that evidence as stopped-incomplete. `abandoned` is written through the locked state writer by the autopilot runner, for a run its session launched that ended with neither a PR nor a parked question, and by a kill; `abandonedBy` names the writer. `awaiting_input` is deliberately distinct from both `in_progress` and `paused`: the run is not broken and not resumable by a retry - it is finished with what it can do alone and is holding for a human answer, which is what Phase 5 does by design after the PR is open. Without a word for it, a run whose work had landed read exactly like one that crashed mid-development, and a state file in the wild had already invented `awaiting-user-test-main-checkout` to say it. `paused` stays what it was: stopped deliberately, resumable, nothing expected from anyone."
501
512
  },
502
513
  "abandonedBy": {
503
514
  "type": "string",
@@ -2059,7 +2070,7 @@
2059
2070
  },
2060
2071
  "gates": {
2061
2072
  "type": "array",
2062
- "description": "Gate ledger, written while the quality gates are active (lib/unattended.mjs gatesActive: MULTI_AGENT_UNATTENDED=1, or state.autopilot true): one entry per verdict, appended by gate-ledger.mjs under the write-state lock. With MULTI_AGENT_UNATTENDED=1 in its environment, pre-commit-check.sh blocks a commit unless the latest entry of every mandatory gate for HEAD is pass or not-applicable. Absent in a run with neither.",
2073
+ "description": "Gate ledger, written while the quality gates are active (lib/unattended.mjs gatesActive: MULTI_AGENT_UNATTENDED=1, or state.autopilot true): one entry per verdict, appended by gate-ledger.mjs under the write-state lock. With MULTI_AGENT_UNATTENDED=1 in its environment, pre-commit-check.sh blocks a commit unless the latest entry of every mandatory gate for HEAD is pass or not-applicable. A run with neither records only the gates in gate-ledger.mjs ATTENDED_RECORDED, each entry marked attended: true, which the unattended commit check never counts.",
2063
2074
  "items": {
2064
2075
  "type": "object",
2065
2076
  "additionalProperties": false,
@@ -2082,6 +2093,10 @@
2082
2093
  "type": ["string", "null"],
2083
2094
  "description": "Repository HEAD the gate ran against."
2084
2095
  },
2096
+ "attended": {
2097
+ "type": "boolean",
2098
+ "description": "True on an entry written while the quality gates were off (an attended run). lib/run-completion.mjs and the attended commit check read it; gate-ledger.mjs checkCommit ignores it."
2099
+ },
2085
2100
  "detail": {
2086
2101
  "type": "object",
2087
2102
  "additionalProperties": true
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
3
  "$id": "https://github.com/mmerterden/multi-agent-pipeline/pipeline/schemas/phases.json",
4
- "description": "The phase contract, in one place: the phase ids and names, which phases each mode runs, the per-phase token ceilings and the phase numbers other code compares against. gen-mode-dispatch.mjs, render-work-summary.sh, the autopilot runner, runs-index.mjs, usage-report.mjs and gen-facts.mjs read it; smoke-phase-contract.sh holds every copy that cannot read it (the phase docs and prose counts, agent-state.schema.json, token-budget.json, phase-tracker.sh, phase-banner.sh, gc-abandoned.sh) to it. Adding or merging a phase means editing here. Ceilings are measured against the current phase documents, never summed from earlier ones: smoke-token-budget.sh fails a ceiling more than 25% above the measurement.",
4
+ "description": "The phase contract, in one place: the phase ids and names, which phases each mode runs, the per-phase token ceilings and the phase numbers other code compares against. gen-mode-dispatch.mjs, render-work-summary.sh, the autopilot runner, runs-index.mjs, usage-report.mjs and gen-facts.mjs read it; smoke-phase-contract.sh holds every copy that cannot read it (the phase docs and prose counts, agent-state.schema.json, token-budget.json, phase-tracker.sh, phase-banner.sh, gc-abandoned.sh) to it. Adding or merging a phase means editing here. A mode with `commits: false` delivers no commit in its Phase 4 (analysis publishes a document), so lib/run-completion.mjs asks it for no commit evidence before `status: complete`; any other mode commits. Ceilings are measured against the current phase documents, never summed from earlier ones: smoke-token-budget.sh fails a ceiling more than 25% above the measurement.",
5
5
  "contractVersion": "2.0.0",
6
6
  "phaseSchema": 2,
7
7
  "phaseSchemaNote": "The phase vocabulary generation. metrics.jsonl is append-only and v19.0.0 renumbered the phases, so every line written from v19.0.0 on carries this number and a line without the field is generation 1. Aggregators pick their name table from it; without it phase 3 means Dev in old rows and Review in new ones and no reader can tell them apart.",
@@ -72,7 +72,8 @@
72
72
  },
73
73
  "analysis": {
74
74
  "phases": [0, 1, 3, 4, 5],
75
- "autopilot": false
75
+ "autopilot": false,
76
+ "commits": false
76
77
  }
77
78
  },
78
79
  "thresholds": {
@@ -50,7 +50,11 @@
50
50
  "items": { "type": "integer", "minimum": 0 },
51
51
  "uniqueItems": true
52
52
  },
53
- "autopilot": { "type": "boolean" }
53
+ "autopilot": { "type": "boolean" },
54
+ "commits": {
55
+ "type": "boolean",
56
+ "description": "false when the mode delivers no commit in its Phase 4; lib/run-completion.mjs then asks for no commit evidence before status complete. Absent means the mode commits."
57
+ }
54
58
  }
55
59
  }
56
60
  },
@@ -1123,6 +1123,20 @@
1123
1123
  }
1124
1124
  }
1125
1125
  },
1126
+ "testGen": {
1127
+ "type": "object",
1128
+ "additionalProperties": false,
1129
+ "description": "/multi-agent:test-gen knobs. Read by test-gen-state.mjs revise.",
1130
+ "properties": {
1131
+ "revisionCap": {
1132
+ "type": "integer",
1133
+ "minimum": 0,
1134
+ "maximum": 500,
1135
+ "default": 20,
1136
+ "description": "Stale-test revisions autopilot may apply, counted per run and per calendar day (UTC), whichever is reached first. At the cap the run parks instead of editing more tests. 0 parks on the first one."
1137
+ }
1138
+ }
1139
+ },
1126
1140
  "testStability": {
1127
1141
  "type": "object",
1128
1142
  "additionalProperties": false,
@@ -1858,6 +1872,43 @@
1858
1872
  "enabled": false
1859
1873
  }
1860
1874
  },
1875
+ "stopGuard": {
1876
+ "type": "object",
1877
+ "additionalProperties": false,
1878
+ "description": "run-stop-guard.sh, the Claude Code Stop hook that holds the session driving a pipeline run from ending its turn mid-phase with no halt or wait recorded. Read first on every Stop; false lets every stop through. Removing the hook from settings.json works too, and install does not add it back.",
1879
+ "properties": {
1880
+ "enabled": {
1881
+ "type": "boolean",
1882
+ "default": true,
1883
+ "description": "true = the hook judges a stop as documented. false = it never blocks."
1884
+ }
1885
+ },
1886
+ "default": {
1887
+ "enabled": true
1888
+ }
1889
+ },
1890
+ "readOnlyRepos": {
1891
+ "type": "array",
1892
+ "items": { "type": "string", "minLength": 1 },
1893
+ "default": [],
1894
+ "description": "Absolute paths (a leading ~ is expanded) of checkouts kept as read-only references: agent-guard.sh refuses an Edit, Write or NotebookEdit whose target is inside one, and a git commit, push, merge, cherry-pick, revert, am, rebase or tag that runs in one (after any cd or git -C on the line). Attended and unattended alike. Empty means no repository is protected this way."
1895
+ },
1896
+ "attendedCommitGate": {
1897
+ "type": "object",
1898
+ "additionalProperties": false,
1899
+ "description": "What pre-commit-check.sh does with an attended `git commit` inside a pipeline run whose gate ledger holds no passing build and test verdict (evidence-gate/build, evidence-gate/test) for HEAD. evidence-gate.mjs records both in an attended run, marked attended. Read by gate-ledger.mjs `evidence`; a commit outside a run, or in a mode that commits nothing, is never checked. Unattended commits are held to the full ledger regardless of this setting.",
1900
+ "properties": {
1901
+ "mode": {
1902
+ "type": "string",
1903
+ "enum": ["off", "warn", "block"],
1904
+ "default": "warn",
1905
+ "description": "warn = say so on stderr and let the commit through. block = refuse the commit (exit 2) until the evidence is recorded. off = no check."
1906
+ }
1907
+ },
1908
+ "default": {
1909
+ "mode": "warn"
1910
+ }
1911
+ },
1861
1912
  "mutationProbe": {
1862
1913
  "type": "object",
1863
1914
  "additionalProperties": false,
@@ -87,7 +87,7 @@
87
87
  "waitingFor": {
88
88
  "type": ["string", "null"],
89
89
  "x-sensitivity": "none",
90
- "description": "The step a paused run re-enters (agent-state waitingFor): maturity, user-channels-choice or question. Added in 1.1.0."
90
+ "description": "The step a paused run re-enters (agent-state waitingFor): maturity, user-channels-choice, local-test, attended-reply or question; null or absent when the run is not waiting. Added in 1.1.0."
91
91
  },
92
92
  "pendingQuestion": {
93
93
  "x-sensitivity": "none",
@@ -0,0 +1,73 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://github.com/mmerterden/multi-agent-pipeline/pipeline/schemas/test-gen-discovery.schema.json",
4
+ "title": "Multi-Agent Pipeline - test-gen discovery",
5
+ "description": "What the repository already has for testing the selected inventory items, $RUN/discovery.json of /multi-agent:test-gen (multi-agent-refs/test-gen/stages.md). Returned by the read-only test-gen-analyst persona and written by the orchestrator. testRoots[].command is what the write stage's run command is checked against (test-gen-gate.mjs written --discovery). Existing tests are joined to items by the identifier the test uses, never by a name. test-gen-gate.mjs check --stage discovery checks this shape without a dependency.",
6
+ "type": "object",
7
+ "additionalProperties": false,
8
+ "required": ["stage", "testRoots", "existing", "handles"],
9
+ "$defs": {
10
+ "fileLine": { "type": "string", "pattern": "^[^\\s:]+:[1-9][0-9]*$" }
11
+ },
12
+ "properties": {
13
+ "stage": { "const": "discovery" },
14
+ "testRoots": {
15
+ "type": "array",
16
+ "items": {
17
+ "type": "object",
18
+ "additionalProperties": false,
19
+ "required": ["path", "runner", "command"],
20
+ "properties": {
21
+ "path": { "type": "string", "minLength": 1 },
22
+ "runner": {
23
+ "enum": [
24
+ "xcuitest",
25
+ "xctest",
26
+ "swift-testing",
27
+ "espresso",
28
+ "compose",
29
+ "junit",
30
+ "playwright",
31
+ "cypress",
32
+ "jest",
33
+ "vitest",
34
+ "pytest",
35
+ "other"
36
+ ]
37
+ },
38
+ "command": { "type": "string", "minLength": 1 },
39
+ "report": { "enum": ["junit", "xcresult", "none"] }
40
+ }
41
+ }
42
+ },
43
+ "existing": {
44
+ "type": "array",
45
+ "items": {
46
+ "type": "object",
47
+ "additionalProperties": false,
48
+ "required": ["inventoryId", "tests"],
49
+ "properties": {
50
+ "inventoryId": { "type": "string", "minLength": 1 },
51
+ "tests": { "type": "array", "items": { "$ref": "#/$defs/fileLine" } },
52
+ "joinedBy": { "type": "string", "minLength": 1 }
53
+ }
54
+ }
55
+ },
56
+ "handles": {
57
+ "type": "array",
58
+ "items": {
59
+ "type": "object",
60
+ "additionalProperties": false,
61
+ "required": ["inventoryId", "file", "line", "handle"],
62
+ "properties": {
63
+ "inventoryId": { "type": "string", "minLength": 1 },
64
+ "file": { "type": "string", "minLength": 1 },
65
+ "line": { "type": "integer", "minimum": 1 },
66
+ "handle": { "type": "string", "minLength": 1 }
67
+ }
68
+ }
69
+ },
70
+ "conventions": { "type": "array", "items": { "type": "string", "minLength": 1 } },
71
+ "notes": { "type": "array", "items": { "type": "string" } }
72
+ }
73
+ }