@mmerterden/multi-agent-pipeline 20.16.0 → 20.17.1

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 (60) hide show
  1. package/CHANGELOG.md +42 -0
  2. package/README.md +6 -5
  3. package/README.tr.md +6 -5
  4. package/docs/architecture.md +3 -3
  5. package/docs/ecosystem.md +5 -5
  6. package/docs/facts.json +14 -12
  7. package/docs/features.md +10 -2
  8. package/install/catalog-history.json +1 -1
  9. package/install/claude.mjs +41 -6
  10. package/install/codex.mjs +3 -3
  11. package/manifest.json +62 -49
  12. package/package.json +1 -1
  13. package/pipeline/commands/multi-agent/bug-bash/SKILL.md +154 -0
  14. package/pipeline/commands/multi-agent/review/SKILL.md +13 -9
  15. package/pipeline/commands/multi-agent/setup/SKILL.md +8 -6
  16. package/pipeline/commands/multi-agent/sync/SKILL.md +4 -3
  17. package/pipeline/commands/multi-agent/update/SKILL.md +5 -1
  18. package/pipeline/commands/sim-test.md +71 -6
  19. package/pipeline/lib/plugin-skill-roots.mjs +80 -0
  20. package/pipeline/lib/usage-lifecycle.mjs +83 -0
  21. package/pipeline/multi-agent-refs/cross-cli-contract.md +20 -4
  22. package/pipeline/multi-agent-refs/features/assertion-judge.md +76 -0
  23. package/pipeline/multi-agent-refs/features/exploratory-testing.md +140 -0
  24. package/pipeline/multi-agent-refs/features/skill-conformance.md +1 -1
  25. package/pipeline/multi-agent-refs/features/usage-reporting.md +129 -36
  26. package/pipeline/multi-agent-refs/features/valven.md +30 -3
  27. package/pipeline/multi-agent-refs/help/en.md +2 -0
  28. package/pipeline/multi-agent-refs/help/tr.md +2 -0
  29. package/pipeline/multi-agent-refs/phases/phase-0-init.md +1 -1
  30. package/pipeline/multi-agent-refs/phases/phase-4-commit.md +2 -2
  31. package/pipeline/schemas/bug-bash-verification.schema.json +41 -0
  32. package/pipeline/schemas/explore-run.schema.json +108 -0
  33. package/pipeline/schemas/prefs.schema.json +24 -3
  34. package/pipeline/scripts/_stack-routing.mjs +2 -0
  35. package/pipeline/scripts/bug-bash.mjs +379 -0
  36. package/pipeline/scripts/explore-findings.mjs +410 -0
  37. package/pipeline/scripts/gen-skills-index.mjs +1 -2
  38. package/pipeline/scripts/pr-timeline.mjs +135 -0
  39. package/pipeline/scripts/pre-commit-check.sh +4 -2
  40. package/pipeline/scripts/skill-conformance.mjs +17 -71
  41. package/pipeline/scripts/smoke-cross-cli-behavior.sh +1 -1
  42. package/pipeline/scripts/uninstall.mjs +29 -8
  43. package/pipeline/scripts/usage-identity.mjs +311 -26
  44. package/pipeline/scripts/usage-register.mjs +268 -62
  45. package/pipeline/scripts/usage-report.mjs +59 -7
  46. package/pipeline/scripts/valven-gate.mjs +292 -11
  47. package/pipeline/skills/.skill-manifest.json +18 -10
  48. package/pipeline/skills/.skills-index.json +28 -6
  49. package/pipeline/skills/shared/README.md +6 -4
  50. package/pipeline/skills/shared/core/multi-agent-bug-bash/SKILL.md +150 -0
  51. package/pipeline/skills/shared/core/multi-agent-review/SKILL.md +1 -1
  52. package/pipeline/skills/shared/core/multi-agent-setup/SKILL.md +5 -1
  53. package/pipeline/skills/shared/core/multi-agent-sync/SKILL.md +4 -3
  54. package/pipeline/skills/shared/core/multi-agent-update/SKILL.md +4 -1
  55. package/pipeline/skills/shared/external/e2e-testing/SKILL.md +124 -0
  56. package/pipeline/skills/shared/external/e2e-testing/references/document-fixtures.md +65 -0
  57. package/pipeline/skills/shared/external/valven-rules/SKILL.md +93 -0
  58. package/pipeline/skills/shared/{core → external}/valven-rules/references/rules.yml +52 -6
  59. package/pipeline/skills/skills-index.md +4 -2
  60. package/pipeline/skills/shared/core/valven-rules/SKILL.md +0 -62
@@ -0,0 +1,140 @@
1
+ # Exploratory testing - findings, budgets, stop rules
2
+
3
+ <!-- toc -->
4
+ - [1. The run file](#1-the-run-file)
5
+ - [2. A finding](#2-a-finding)
6
+ - [3. Budget and stop rules](#3-budget-and-stop-rules)
7
+ - [4. Product failure or blocked](#4-product-failure-or-blocked)
8
+ - [5. Driving the app: locators and expect steps](#5-driving-the-app-locators-and-expect-steps)
9
+ - [6. Report](#6-report)
10
+ <!-- /toc -->
11
+
12
+ An exploratory run drives a live app toward a goal and reports what looks
13
+ wrong. What it produces is a set of claims, and this contract keeps them
14
+ checkable: every finding carries what was expected, what was seen and how to
15
+ get there again; every run has a budget and stops on its own; a step that could
16
+ not run is reported apart from a product that ran and was wrong.
17
+
18
+ Consumers: `/multi-agent:test` (the UI Bug Hunter, `commands/sim-test.md`),
19
+ `/multi-agent:bug-bash` (one run per charter). Script:
20
+ `$HOME/.claude/scripts/explore-findings.mjs`. Run file:
21
+ `$HOME/.claude/schemas/explore-run.schema.json`. Assertions that need judgement go
22
+ to a fresh judge: `features/assertion-judge.md`.
23
+
24
+ ## 1. The run file
25
+
26
+ Write the run as it goes to `<log dir>/run.json`: `goal`, `platform`, `budget`,
27
+ `usage`, `steps[]`, `findings[]`. One step is one charter-sized move ("change
28
+ the quantity on the first cart row and read the total"), not one tap.
29
+
30
+ ```json
31
+ {"index": 3, "title": "Change quantity", "status": "failed", "code": "ASSERTION_FAILED",
32
+ "detail": "total stayed 12.00", "trace": "<path of the toolkit's trace.md, if any>"}
33
+ ```
34
+
35
+ `status` is `passed | failed | blocked | exhausted`. `code` is the toolkit's
36
+ code when the step did not pass; it decides how the step counts (section 4).
37
+
38
+ ## 2. A finding
39
+
40
+ | Field | Rule |
41
+ |---|---|
42
+ | `kind` | `issue` (functional defect, fails the run) or `warning` (cosmetic, does not) |
43
+ | `severity` | integer 1-5: 1 trivial, 2 low, 3 medium, 4 high, 5 critical |
44
+ | `title`, `screen` | short, and where it was seen |
45
+ | `expected` | what should have happened, from the spec when there is one (`spec` quotes it) |
46
+ | `observed` | what happened instead |
47
+ | `repro` | the steps that reach it from launch, at least one |
48
+ | `evidence` | at least one `{kind, path}`: `screenshot`, `ui-tree`, `video`, `trace`, `log`, `text` |
49
+ | `step` | the step that reported it |
50
+
51
+ Report a finding the moment its evidence is on disk, not at the end. A finding
52
+ that misses a field is listed under "failed the contract" and cannot fail the
53
+ run: `explore-findings.mjs report` holds it out of the verdict.
54
+
55
+ Secrets never appear in a finding. A credential is named, not written:
56
+ `secret_ref:<name>`. The script refuses `password: <value>`, `token=<value>`
57
+ and the like in any finding text.
58
+
59
+ ## 3. Budget and stop rules
60
+
61
+ | Budget | Default | Range |
62
+ |---|---|---|
63
+ | steps | 8 | 1-12 |
64
+ | minutes (wall clock) | 10 | 3-15 |
65
+ | tokens | 150000 | 20000-400000 |
66
+
67
+ Values outside the range are clamped and named in `clamped`. Before each new
68
+ step run:
69
+
70
+ ```bash
71
+ node "$HOME/.claude/scripts/explore-findings.mjs" check --run "<run.json>"
72
+ ```
73
+
74
+ `{"stop": true}` ends the run with that `reason`:
75
+
76
+ - `step-limit`, `time`, `tokens`: the budget is spent.
77
+ - `stuck`: the last three steps were blocked and none of them reported a
78
+ finding. Three blocked steps in a row means the environment, not the app, is
79
+ in the way, and more steps would spend the budget learning nothing.
80
+
81
+ A failed step does not stop the run; only the rules above do.
82
+
83
+ ## 4. Product failure or blocked
84
+
85
+ | Step | Counts as |
86
+ |---|---|
87
+ | `failed` + `ASSERTION_FAILED` / `ASSERTION_INCONCLUSIVE` | product failure |
88
+ | `CREDENTIAL_MISSING` | blocked: credentials |
89
+ | `ENVIRONMENT_UNAVAILABLE` | blocked: environment |
90
+ | `SEED_DATA_MISSING` | blocked: seed data |
91
+ | `LOCATOR_NOT_FOUND`, `LOCATOR_AMBIGUOUS`, `REPLAY_DIVERGED` | blocked: automation |
92
+ | `BUDGET_EXCEEDED` | exhausted (neither) |
93
+ | `failed` with no code | blocked: unknown (never silently a product failure) |
94
+
95
+ A blocked step is a fact about the test setup. It goes in its own report
96
+ section, grouped by cause, so "the login screen is broken" and "the run had no
97
+ test account" are never the same line.
98
+
99
+ Verdict, from `explore-findings.mjs report`:
100
+
101
+ - **failed**: at least one valid `issue`.
102
+ - **blocked**: no step ran, or every step was blocked, and no `issue`.
103
+ - **passed**: steps ran, no `issue`, not every step blocked. Warnings allowed.
104
+
105
+ ## 5. Driving the app: locators and expect steps
106
+
107
+ With `@mmerterden/multi-agent-toolkit-mcp` ≥ v3.19.0, prefer these over
108
+ coordinates; on an older toolkit, fall back to the UI tree plus frame-centre
109
+ taps as before and say so in the report:
110
+
111
+ - **Locate, then act.** `ui_locate` resolves an element by accessibility
112
+ identifier, by role plus label, or by visible text; `ios_tap_element` /
113
+ `android_tap_element` tap what it resolves. Identifier first, role plus label
114
+ second, text last (text changes with locale). `LOCATOR_NOT_FOUND` and
115
+ `LOCATOR_AMBIGUOUS` are automation blocks, not product findings: narrow the
116
+ locator and retry once before recording the step as blocked.
117
+ - **Assert with expect steps.** In `agent_run_steps`, `expect_text`,
118
+ `expect_element` and `expect_url` decide a deterministic check and return
119
+ `verdict: passed | failed | blocked` with a code. Use them for anything
120
+ exact: a total, a label, a route. Only a visual or semantic claim ("the
121
+ layout is not clipped", "the error explains what to do") goes to the judge.
122
+ - **Secrets by reference.** The type tools take `secret_ref` instead of the
123
+ value; the value never enters the transcript, the run file or a finding.
124
+ - **Traces.** A failed `agent_run_steps` writes a `trace.md`; put its path in
125
+ the step's `trace` and in the finding's `evidence` as `kind: "trace"`.
126
+ - **Replay.** `save_as` stores a step sequence that reached a finding, and
127
+ `replay` runs it again. `REPLAY_DIVERGED` means the app no longer follows the
128
+ recorded path: an automation block, and a hint that the screen changed.
129
+ - **Web sessions.** Pass `session` to keep parallel browser sessions apart
130
+ (one per bug-bash charter).
131
+
132
+ Tool inputs are the tool's own schema; the names above are the contract, the
133
+ argument shapes are read from the server.
134
+
135
+ ## 6. Report
136
+
137
+ `explore-findings.mjs report --run <run.json>` prints the verdict, why the run
138
+ ended, the budget used, issues before warnings with the most severe first,
139
+ blocked steps by cause, and the findings that failed the contract. The UI Bug
140
+ Hunter keeps its own screen table and summary around this block.
@@ -44,7 +44,7 @@ The key sits under `metadata:` because the host skill frontmatter schema has no
44
44
 
45
45
  `skill-conformance.mjs` reads frontmatter across the installed skills trees and loads what declared itself. Nothing in the pipeline names `ios-coding-standard`, `apple-archive-compliance` or any other skill, so a future UIKit, Objective-C, Kotlin or backend registry drops in with zero pipeline change - and a registry that is absent produces a declared coverage gap rather than a silent pass.
46
46
 
47
- **The skills root differs per host**, so discovery is a bounded walk over candidate roots (`<install>/skills`, `<install>/multi-agent-refs/skills` for Codex, `<repo>/pipeline/skills`), installed layouts first. `install/copilot.mjs` copies `scripts/` byte-for-byte with no path rewrite, so a hardcoded `~/.claude/skills` is inert on two of the three hosts. That bug has already shipped here once: dynamic skill loading exited 1 on every real install while passing a smoke that ran from the repo. `skillsRootsSearched` is recorded in the manifest so an empty result is attributable to a root rather than to an absence of registries.
47
+ **The skills root differs per host**, so discovery is a bounded walk over candidate roots and the first registry found under a name wins. On Claude Code the order is each marketplace stack plugin's `skills/` (the checkout, then every cached version, from `lib/plugin-skill-roots.mjs`), `~/.claude/multi-agent-refs/registries` (registries the installer lays down for the pipeline's gates, `valven-rules` among them), `~/.claude/skills`, then `<repo>/pipeline/skills`: a registry that ships in a plugin (`ios-coding-standard`, `valven-rules`) is what Claude Code loads, and a same-named copy an older install left behind must not shadow it. Copilot CLI and Codex load no plugin, so their installed skills dir (`<install>/skills`, `<install>/multi-agent-refs/skills`) comes first and the plugin roots follow. `install/copilot.mjs` copies `scripts/` byte-for-byte with no path rewrite, so a hardcoded `~/.claude/skills` is inert on two of the three hosts. `skillsRootsSearched` is recorded in the manifest so an empty result is attributable to a root rather than to an absence of registries.
48
48
 
49
49
  ## Scope is required, and it is what makes this stack-generic
50
50
 
@@ -13,7 +13,7 @@
13
13
  <!-- /toc -->
14
14
 
15
15
  Reporting needs three things: `usageLog.enabled` true, a token that resolves,
16
- and a logged-in GitHub account that can read the pipeline repository. The token
16
+ and a GitHub account on this machine that can read the pipeline repository. The token
17
17
  is arranged by one call (`$HOME/.claude/scripts/usage-register.mjs`) made from
18
18
  the three places a machine can first become real: **setup**, **update**, and
19
19
  **the Phase 0 exit gate** of a run on a machine that reached neither. Asserted by
@@ -24,7 +24,8 @@ the three places a machine can first become real: **setup**, **update**, and
24
24
  `usage-report.mjs` emits coarse run metadata, one record per run, upserted by
25
25
  run id. Never prompts, never code, never diffs, never token values, never
26
26
  absolute paths, never the repository name (a digest stands in for it). The
27
- registration call sends two fields: the reporting login and the short hostname.
27
+ registration call sends the reporting login and the short hostname, plus the
28
+ signed challenge when the account was found by an SSH key (section 4).
28
29
 
29
30
  Each record carries:
30
31
 
@@ -57,6 +58,21 @@ progress.
57
58
  | `completed` | Phase 5, after it writes `status: complete` and `finishedAt` to the state; the analysis render step |
58
59
  | `halted` | the halt-visibility step in `phases/operations.md` |
59
60
  | `parked` | the park-visibility step in `phases/operations.md`, after a run writes `awaiting_input` or `waitingFor` |
61
+ | `install`, `update` | registration, setup and update, and every accepted run event, when the installed version was not reported yet (see below) |
62
+
63
+ **Install and update records.** Derived, never written by the installer (see
64
+ section 8): the version stamp `install.js` writes (`.pipeline-version`, Claude
65
+ Code's first) says which version is installed and, by its modification time,
66
+ since when; prefs keep the last version reported (`usageLog.reportedVersion`).
67
+ When they differ, `usage-report.mjs --lifecycle` sends one record: `ev`
68
+ (`update` from the reported version; without one, `install` on a machine that
69
+ registered after the stamp and `update` from an unknown version on one that
70
+ registered before it), `v`, `vf`, `hs` (the host trees at that version) and `t`
71
+ (the stamp time), and moves `reportedVersion` forward once the server accepted
72
+ it. The id is derived from the version and the stamp time, so a resend replaces
73
+ the record. It runs after a successful registration, after setup or update finds
74
+ a token already onboarded, and after every accepted run event. The panel lists
75
+ the records under "Kurulumlar" and keeps them out of the run counts.
60
76
 
61
77
  The panel derives a run's status from the last event it received. A run still
62
78
  `in_progress` with no event for **6 hours** is shown as abandoned; a parked run
@@ -65,57 +81,133 @@ is waiting on a person and is never shown as abandoned. The threshold is
65
81
 
66
82
  ## 3. The reporting identity
67
83
 
68
- The login is the GitHub login of the **logged-in account that can read the pipeline
69
- repository** (`usageLog.accessRepo`, default `mmerterden/multi-agent-pipeline`):
84
+ The login is the GitHub login of **the account on this machine that can read the
85
+ pipeline repository** (`usageLog.accessRepo`, default `mmerterden/multi-agent-pipeline`):
70
86
  access to that repository is what grants use of the pipeline, so it also decides
71
- whose name a run carries. `usage-identity.mjs` checks every github.com account
72
- `gh auth status` lists, in the order `usageLog.reportAs` (when logged in), the
73
- active account, then the rest. Each check is `gh api repos/<accessRepo>` with that
74
- account's token in the child's environment as `GH_TOKEN`: never on argv, never
75
- logged, never stored. The first account that can read the repository wins.
87
+ whose name a run carries. `usage-identity.mjs` finds it with no question asked,
88
+ stopping at the first account that can read the repository:
89
+
90
+ | Order | Method (`usageLog.loginMethod`) | Candidates | Access check |
91
+ |-------|------|------------|--------------|
92
+ | 1 | `gh` | every github.com account `gh auth status` lists: `usageLog.reportAs` (when logged in), the active account, then the rest | `gh api repos/<accessRepo>` with that account's token in the child's environment as `GH_TOKEN` |
93
+ | 2 | `credential` | the token git's credential helper holds for https://github.com: `git credential fill` with `protocol=https`, `host=github.com` on stdin, `GIT_TERMINAL_PROMPT=0`, `GCM_INTERACTIVE=never`, askpass disabled, a timeout | `GET /user` names the login, `GET /repos/<accessRepo>` proves access; through `gh api` with `GH_TOKEN`, or straight to the GitHub API from a child process when gh is not installed |
94
+ | 3 | `ssh` | the private keys `ssh -G github.com` lists (`identityfile`, `~` expanded) and `~/.ssh/id_ed25519`, `id_ecdsa`, `id_rsa`, each only when its `.pub` exists; a `.pub` listed on its own stands for a key held by an agent | `ssh -T git@github.com` names the login (`Hi <login>!`; a deploy key's `owner/repo` is skipped), `git ls-remote git@github.com:<accessRepo>.git HEAD` proves access. Both with `-i <key> -o IdentitiesOnly=yes -o BatchMode=yes -o ConnectTimeout=8 -o StrictHostKeyChecking=accept-new` |
95
+
96
+ No token is ever put on an argv, logged or stored: gh and git print it on their
97
+ own stdout, and it travels on only in a child's environment. Batch mode means a
98
+ key that needs a passphrase and is not in ssh-agent is skipped, never prompted
99
+ for. A missing gh, git or ssh binary, a timeout or a failed check only means
100
+ "no candidate"; nothing here fails a caller.
76
101
 
77
102
  Never `identity.name`, which carries a person's real name and sometimes a
78
103
  corporate title, and never simply the active `gh` account, which on a machine
79
104
  with a work account and a personal account is often the one without access.
80
105
 
81
- The answer is cached in prefs (`usageLog.login`, `loginRepo`, `loginCheckedAt`)
82
- for 7 days, or 1 day when no account had access, so a phase boundary costs no
83
- network call. Refresh it after changing accounts or being granted access:
106
+ The answer is cached in prefs (`usageLog.login`, `loginMethod`, `loginKey` for
107
+ the SSH key path, `loginRepo`, `loginCheckedAt`, `loginReason`) for 7 days, or
108
+ 1 day when no account was found, so a phase boundary costs no network call. A
109
+ cached SSH login whose key file is gone is re-checked. Refresh it after changing
110
+ accounts or keys, or being granted access:
84
111
 
85
112
  ```bash
86
113
  node "$HOME/.claude/scripts/usage-identity.mjs" --refresh
87
114
  ```
88
115
 
89
- **No account with access, no report.** The emitter sends nothing and writes one
90
- line to stderr (`usage-report: not sent - none of the logged-in GitHub accounts
91
- (...) can read ...`); registration is skipped with the same reason.
116
+ **No account with access, no report.** Nothing is reported under an account
117
+ that cannot read the repository: the server accepts only the repository owner
118
+ and its collaborators. The emitter sends nothing and writes one line to stderr
119
+ (`usage-report: not sent - no GitHub account on this machine can read ...`,
120
+ naming what was tried); registration is skipped with the same reason. A cache
121
+ entry or registration written by 20.17.0 with `loginFallback` /
122
+ `registeredFallback` set is treated as no account: it is re-checked at once and
123
+ replaced as soon as an account with access is found.
124
+
125
+ **`--quiet` still speaks once.** The Phase 0 backstop discards stdout, so a
126
+ registration that leaves reporting off for a reason the user can fix (a refused
127
+ login, no account with access, a key that cannot sign, an unreachable endpoint)
128
+ prints one stderr line, `multi-agent: usage reporting is off - <reason>`, on
129
+ every run until it is fixed.
130
+
131
+ ### Personal account
132
+
133
+ A registration is pinned: the server binds the token to the login it verified,
134
+ and the machine keeps that login in `usageLog.registeredAs`. Every later record
135
+ is named after it, with no identity check, and finding no account later does not
136
+ undo it (`decideRegistration`). So the personal account has to be reachable only
137
+ once, at the moment of registration.
138
+
139
+ `usage-register.mjs --json` reports `needsPersonalAccount: true` only when the
140
+ machine is not pinned and no account was found at all: no gh account, no
141
+ credential-helper token and no SSH key with access. That is the last resort.
142
+ Setup and update then ask once (AskUserQuestion per
143
+ `$HOME/.claude/multi-agent-refs/picker-contract.md`; label
144
+ `Add personal account (Recommended)` / `Skip`) and offer either fix:
145
+
146
+ - **gh**: note the active account (`gh api user --jq .login`), have the person
147
+ run the browser login for the account that holds access in their own terminal
148
+ (in Claude Code: `! gh auth login --hostname github.com --web`; gh keeps both
149
+ accounts), then switch the work account back so pull requests keep their
150
+ author: `gh auth switch --hostname github.com --user <the noted account>`.
151
+ - **SSH key**: add this machine's public key (for example `~/.ssh/id_ed25519.pub`)
152
+ under the personal account's GitHub Settings > SSH and GPG keys. Nothing else
153
+ changes on the machine.
154
+
155
+ Then `node "$HOME/.claude/scripts/usage-register.mjs" --refresh --json` finds
156
+ the account and pins it. Skip leaves reporting off; the next update asks again.
92
157
 
93
158
  ## 4. The server side
94
159
 
95
160
  `/api/usage/register` requires proof that the caller owns the login it names.
96
- `usage-register.mjs` reads the verified account's token with
97
- `gh auth token --user <login>` (gh prints it on its own stdout; it is never on
98
- another process's argv, never logged, never stored) and sends it as
99
- `Authorization: Bearer <token>`. The body's `u` stays as a hint. The server
100
- resolves the token to a GitHub login, refuses a mismatch, then checks that login
101
- with a server-side token (the repository owner, or a collaborator of the
102
- repository) before it issues a token, and pins the login to that token.
161
+ The proof depends on how the account was found:
162
+
163
+ - **gh**: `gh auth token --user <login>` (gh prints it on its own stdout), sent
164
+ as `Authorization: Bearer <token>`.
165
+ - **credential**: the credential helper's token, fetched again at registration
166
+ (never kept from the identity check), sent the same way.
167
+ - **ssh**: no token at all. (a) `POST <register url>/challenge` with
168
+ `{ "u": <login> }` returns `{ challenge, namespace, expiresAt }`. (b) The exact
169
+ challenge bytes (UTF-8, no trailing newline) are written to a `0600` file in a
170
+ fresh temp dir and signed with `ssh-keygen -Y sign -f <key> -n <namespace>`,
171
+ stdin closed and askpass disabled, so a key that needs a passphrase fails at
172
+ once; then with `-f <key>.pub`, which signs through ssh-agent when the key is
173
+ loaded there. The temp dir is removed afterwards. (c) `POST <register url>`
174
+ with `{ "u", "c", "ssh": { "challenge", "signature" } }` and no
175
+ Authorization header.
176
+
177
+ For a bearer, the server resolves the token to a GitHub login and refuses a
178
+ mismatch. For an SSH proof, the challenge is stateless and HMAC-bound to the
179
+ login, valid for five minutes; the server checks it, then accepts the signature
180
+ only from one of the login's public keys on GitHub
181
+ (`https://github.com/<login>.keys`; ed25519, rsa-sha2-256/512, ecdsa nistp256). Either way it then checks that login with a
182
+ server-side token (the repository owner, or a collaborator of the repository)
183
+ before it issues a token, and pins the login to that token. The body's `u` is a
184
+ hint for a bearer and the claim being proven for a signature.
103
185
 
104
186
  The proof is sent only over https to the default reporting host or a host listed
105
187
  in `usageLog.trustedHosts`, or plain http to loopback (`127.0.0.1`, `localhost`,
106
- `[::1]`) for local development; any other endpoint is refused before the gh
107
- token is read. Redirects are not followed by either call, so neither the gh
108
- token nor the ingest token can be carried to another host. A `401`, or a `403`
109
- for a login mismatch, leaves reporting off with a status line naming the login
110
- that could not be confirmed (`gh auth status` must list that account); a `403`
111
- for a login without access says it needs collaborator access; a decline is
112
- reported as `declined`; gh having no
113
- token for the login sends nothing. `/api/usage/ingest` reports every record
114
- under the pinned login and ignores the name the client sent; a pinned login that
115
- is no longer the owner or a collaborator is dropped. Records from any other
116
- account are hidden from the panel. `usage-register.mjs` replaces a stored token
117
- whose `registeredAs` differs from the verified login, or that predates
118
- verification, because a token reports under the login it was issued for.
188
+ `[::1]`) for local development; any other endpoint is refused before any token
189
+ is read or any key is used. Redirects are not followed by any of the calls, so
190
+ neither a GitHub token, a signature nor the ingest token can be carried to
191
+ another host.
192
+
193
+ | Answer | Status line |
194
+ |--------|-------------|
195
+ | `401` / `403` `login_mismatch` | the login that could not be confirmed (for gh: `gh auth status` must list it) |
196
+ | `401` `ssh_signature_invalid` | the signature did not verify |
197
+ | `401` `ssh_key_not_on_account` | the key is not on that account; add its `.pub` to it |
198
+ | `401` `ssh_key_unsupported` | the key type is not accepted; use an ed25519, rsa or ecdsa-p256 key |
199
+ | `403` (no access) | the login needs collaborator access |
200
+ | challenge `404` / `405` | the endpoint does not accept an SSH key as proof yet |
201
+ | signing failed | the key needs a passphrase and is not in ssh-agent: `ssh-add <key>` |
202
+ | `accepted: false` | `declined` |
203
+
204
+ gh or the credential helper having no token for the login sends nothing.
205
+ `/api/usage/ingest` reports every record under the pinned login and ignores the
206
+ name the client sent; a pinned login that is no longer the owner or a
207
+ collaborator is dropped. Records from any other account are hidden from the
208
+ panel. `usage-register.mjs` replaces a stored token whose `registeredAs` differs
209
+ from the verified login, that predates verification, or that a 20.17.0 fallback
210
+ registration left, because a token reports under the login it was issued for.
119
211
 
120
212
  ## 5. The token
121
213
 
@@ -135,7 +227,8 @@ switch) holds the prefs file's lock and replaces the file atomically at mode
135
227
 
136
228
  `usageLog.optOut: true` blocks registration permanently and is checked before
137
229
  anything else - before the identity check, the network call and any credential
138
- read (the credential store and `gh auth token`).
230
+ read (the credential store, `gh auth token`, `git credential fill` and any SSH
231
+ key).
139
232
 
140
233
  The other silence is not a choice: offline, endpoint down, ingest disabled by the
141
234
  admin, no account with access, or a credential store that refuses the write. That
@@ -1,6 +1,17 @@
1
1
  # Valven delivery rules across the phases
2
2
 
3
- The `valven-rules` registry (`$HOME/.claude/skills/valven-rules/`) holds the rules; `valven-gate.mjs` measures them and `valven-split.mjs` turns an oversized branch into stacked pull requests. This file is the per-phase contract the phase docs point at.
3
+ <!-- toc -->
4
+ - [Phase 1](#phase-1)
5
+ - [Phase 2](#phase-2)
6
+ - [Phase 3](#phase-3)
7
+ - [Phase 4](#phase-4)
8
+ - [Development Behaviors](#development-behaviors)
9
+ - [Old work](#old-work)
10
+ <!-- /toc -->
11
+
12
+ The `valven-rules` skill holds the rules in `references/rules.yml`; `valven-gate.mjs` measures them and `valven-split.mjs` turns an oversized branch into stacked pull requests. This file is the per-phase contract the phase docs point at.
13
+
14
+ The skill ships in `ai-common-toolkit`, the always-on plugin, so the rules reach everyone who installs the marketplace, with or without the pipeline. Claude Code loads it from the plugin (`ai-common-toolkit:valven-rules`); the installer copies it into the skills tree on Copilot CLI and Codex. The gate reads the registry installed with it, so its evaluators and its rules come from one release: `multi-agent-refs/registries/valven-rules/` on Claude Code (laid down by the installer outside the skills dir, so the host does not load the skill twice), the skills tree on Copilot CLI and Codex, the source in a checkout, and the plugin copies only when none of those exists. `skill-conformance.mjs` finds the judgement rules in the plugin on Claude Code and falls back to the same registries dir. The gate prints the path it used as `rulesPath`; when none is found it exits 2, which every step below treats as "could not measure", never as a pass.
4
15
 
5
16
  ## Phase 1
6
17
 
@@ -15,11 +26,11 @@ node $HOME/.claude/scripts/valven-gate.mjs --mode dev --repo "$WORKTREE" --base
15
26
  --out "$WORKTREE/.pipeline/valven-dev.json" >/dev/null
16
27
  ```
17
28
 
18
- **Gate 6** (valven-rules, dev mode). Exit 2 blocks the phase: the rules could not be read or git failed. Exit 1: `VLV-TEST-01` (source changed, no test changed) and `VLV-TRACE-01` (no issue key in the branch) are fixed here before Review. `VLV-SIZE-01`, `VLV-SIZE-02` and `VLV-SCOPE-01` are not fixed by editing: keep building on this branch, set `state.valven.splitRequired = true`, and Phase 4 Step 2.95 splits it into stacked PRs. `VLV-REWORK-01` is reported to the user with the ratio and continues. Contract: `$HOME/.claude/skills/valven-rules/SKILL.md`.
29
+ **Gate 6** (valven-rules, dev mode). Exit 2 blocks the phase: the rules could not be read or git failed. Exit 1: `VLV-TEST-01` (source changed, no test changed) and `VLV-TRACE-01` (no issue key in the branch) are fixed here before Review. `VLV-SIZE-01`, `VLV-SIZE-02` and `VLV-SCOPE-01` are not fixed by editing: keep building on this branch, set `state.valven.splitRequired = true`, and Phase 4 Step 2.95 splits it into stacked PRs. `VLV-REWORK-01` is reported to the user with the ratio and continues. Contract: the `valven-rules` skill.
19
30
 
20
31
  ## Phase 3
21
32
 
22
- Step 1.762, after the owned-path gate. The PR body does not exist yet, so `VLV-PRDESC-01` reports `not-measured`; Phase 4 checks it.
33
+ Step 1.762, after the owned-path gate. The PR body and the pull request do not exist yet, so `VLV-PRDESC-01`, `VLV-CYCLE-01`, `VLV-CYCLE-02`, `VLV-CYCLE-03` and `VLV-PICKUP-01` report `not-measured`; Phase 4 and `/multi-agent:review` measure them.
23
34
 
24
35
  ```bash
25
36
  VG_FILE="$WORKTREE/.pipeline/valven-gate.json"
@@ -55,6 +66,12 @@ VG_RC=$?
55
66
 
56
67
  Log: `Phase 4 Step 2.95: valven {pass|fixed|split N|shipped-whole}`
57
68
 
69
+ The commit-mode report also scores `VLV-CYCLE-01`, from the oldest author date among the commits about to be pushed to now. Phase 4 commits right before the push, so this is what Valven will read from the pushed history, and it is a `suggestion` that never blocks the push. The rule carries information on a pull request whose commits were pushed as they were made, which `/multi-agent:review` measures.
70
+
71
+ ### Opening the pull request
72
+
73
+ Every gate has passed before the push, so the pull request opens ready for review with the default reviewers attached, and the draft/ready prompt recommends READY. A draft delays the reviewers' notification, and `VLV-PICKUP-01` flags a draft older than 3 hours on the next review.
74
+
58
75
  ### PR body
59
76
 
60
77
  **Valven body check (required, every PR).** After the body is written to `/tmp/pr-body-$TASK_ID.md` and before the PR is created or updated:
@@ -67,6 +84,16 @@ jq -r '.findings[] | select(.ruleId == "VLV-PRDESC-01") | .issue' "$WORKTREE/.pi
67
84
 
68
85
  Any `VLV-PRDESC-01` line blocks creation: add the missing why / how-to-verify text and re-run. Other findings here were already handled at Step 2.95.
69
86
 
87
+ ## Development Behaviors
88
+
89
+ `VLV-CYCLE-01` (coding: first commit to the pull request opening), `VLV-CYCLE-02` (pickup: ready for review to the first review) and `VLV-CYCLE-03` (review: first review to approval or merge) are scored into Valven's bands, reported as `results[].score` and collected in `scores`; `VLV-REWORK-01` adds the churn score, `100 - churn %`. A duration in the 50 or 0 band is a `suggestion`, because the time has passed and the pull request cannot change it. `VLV-PICKUP-01` is `important` when the pull request has no reviewer, which the author fixes now; a draft older than 3 hours is a `suggestion` (`draft_severity`), because an unattended run opens a draft on purpose and a person decides when it is ready.
90
+
91
+ The durations need the pull request's timeline, passed with `--pr-meta-file`. On GitHub, `pr-timeline.mjs --repo <owner/repo> --pr <N> --out <file>` writes it: `gh pr view --json` (head and base branch, body, commits, author, open, close and merge times, draft state, review requests, reviews) plus two things only GitHub's GraphQL timeline carries, the time a draft was marked ready (`readyAt`) and each review author's type, so a bot such as Copilot's reviewer is not counted as a person. When that query fails the script keeps the `gh pr view` output and says on stderr that pickup then counts from the opening and bots are not recognised; the summary repeats it.
92
+
93
+ A Bitbucket caller writes `{createdAt, readyAt, isDraft, reviewerCount, firstReviewAt, approvedAt, mergedAt, closedAt}` in ISO 8601: `createdDate`, `draft` and the close time of a declined pull request from the pull request, `reviewerCount` the length of `reviewers[]`, `firstReviewAt` the earliest `COMMENTED`, `REVIEWED` or `APPROVED` activity by someone other than the author, `approvedAt` the earliest `APPROVED`; a field it cannot read is null. Bitbucket records no ready-for-review time, so its `readyAt` is null.
94
+
95
+ The gate leaves out reviews by the author, by bots and pending reviews, and a review left before the pull request was ready: pickup and review time start at ready. An unknown reviewer count stays unknown (`VLV-PICKUP-01` reports it `not-measured`, never "no reviewer"), a draft is not asked for a reviewer, and on a merged or closed pull request the clocks stop there and `VLV-PICKUP-01` does not apply. A file that does not parse, or has no valid `createdAt`, is reported on stderr and the timeline rules report `not-measured`; every other rule is still measured.
96
+
70
97
  ## Old work
71
98
 
72
99
  `VLV-AGE-01` (a commit older than 14 days, by author date so a rebase does not hide it), `VLV-BEHIND-01` (more than 50 commits behind the base, or a merge-base older than 14 days) and `VLV-TRACE-03` (commits carrying another issue's key) mean the branch carries old or foreign work. The answer is never a rebase of the old history: the change moves.
@@ -186,6 +186,8 @@ UI Testing (standalone - not part of pipeline phases):
186
186
  through three gates per platform, and lives at /multi-agent:store-ready. The old
187
187
  /multi-agent:test "store-ready" [path] tag still works and hands off there.
188
188
 
189
+ /multi-agent:bug-bash [scope] Parallel exploratory sessions by charter and persona, each with a budget; a finding is reported only when a repro test fails for its reason
190
+
189
191
  Uses xcrun simctl / adb (native, no external app needed).
190
192
  Booted simulator/emulator required. Auto-detects bundle ID from project.
191
193
 
@@ -185,6 +185,8 @@ UI Testing (standalone - pipeline fazlarından bağımsız):
185
185
  geçirerek doğrular, iOS + Android, ve /multi-agent:store-ready altında. Eski
186
186
  /multi-agent:test "store-ready" [path] etiketi çalışır ve oraya devreder.
187
187
 
188
+ /multi-agent:bug-bash [kapsam] Charter ve persona başına bütçeli paralel keşif oturumları; bulgu, sebebiyle fail olan bir repro testi varsa raporlanır
189
+
188
190
  xcrun simctl / adb kullanır (harici app gerekmez).
189
191
  Booted simulator/emulator şart. Bundle ID proje'den otomatik algılanır.
190
192
 
@@ -681,7 +681,7 @@ Phase 0 owns `agent-state.json`. Do not call
681
681
 
682
682
  ```bash
683
683
  node "$HOME/.claude/scripts/phase0-exit-gate.mjs" "$TASK_ID" --input "$ORIGINAL_INPUT"
684
- node "$HOME/.claude/scripts/usage-register.mjs" --quiet >/dev/null 2>&1 || true
684
+ node "$HOME/.claude/scripts/usage-register.mjs" --quiet >/dev/null || true
685
685
  node "$HOME/.claude/scripts/usage-report.mjs" --task-id "$TASK_ID" --phase 0 --event phase >/dev/null 2>&1 || true
686
686
  ```
687
687
 
@@ -20,7 +20,7 @@ Progress emission per `$HOME/.claude/multi-agent-refs/progress-contract.md` -
20
20
  > │
21
21
  > ┌──────────────── PR payload ─────────────────────────┘
22
22
  > │ title + description (rawfile) + reviewers + fromRef + toRef + draft
23
- > └──► POST (Bitbucket) or gh pr create --draft (GitHub) ──► PR open
23
+ > └──► POST (Bitbucket) or gh pr create (GitHub) ──► PR open
24
24
  > ```
25
25
 
26
26
  #### Input contract
@@ -239,7 +239,7 @@ PR author cannot appear in `--reviewer` - strip from list.
239
239
 
240
240
  ##### Draft vs ready-for-review
241
241
 
242
- Before creating PR, ask: "DRAFT or READY? [1/2], default 1" - DRAFT blocks merge/delays notifications, READY notifies reviewers + CI. Skip in `autopilot` → default READY.
242
+ Before creating PR, ask: "READY or DRAFT? [1/2], default `defaultPrMode` or 1" - READY notifies reviewers + CI, DRAFT blocks merge and notifies later. Skip in `autopilot` → READY.
243
243
 
244
244
  - **Bitbucket**: `"draft": true` in payload (DC 8.x+). Promote: `draft: false` PUT. Older: prefix title `[DRAFT]`.
245
245
  - **GitHub**: `--draft`. Promote: `gh pr ready {N}`.
@@ -0,0 +1,41 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://github.com/mmerterden/multi-agent-pipeline/pipeline/schemas/bug-bash-verification.schema.json",
4
+ "version": "1.0.0",
5
+ "title": "Multi-Agent Pipeline - bug bash verifications",
6
+ "description": "One entry per merged candidate, written by the verifying subagents of /multi-agent:bug-bash. bug-bash.mjs report confirms a candidate only when its repro test failed with ASSERTION_FAILED on the assertion that encodes it (failedAt equals assertion); any other outcome is listed as rejected with the reason.",
7
+ "type": "array",
8
+ "items": {
9
+ "type": "object",
10
+ "required": ["id", "status"],
11
+ "properties": {
12
+ "id": { "type": "string", "pattern": "^BB-[0-9]+$" },
13
+ "status": { "enum": ["confirmed", "rejected"] },
14
+ "reason": {
15
+ "enum": ["artifact", "environment", "design", "fixture", "did-not-reproduce", "duplicate"],
16
+ "description": "Required when rejected. artifact: the explorer misread the screen; environment: a key, service or limit only the local stack lacks; design: the code, tests or copy say it is intended; fixture: the seed data lacks what real records have."
17
+ },
18
+ "note": { "type": "string" },
19
+ "productionRisk": {
20
+ "type": "boolean",
21
+ "description": "An environment rejection whose failure handling production users would also see."
22
+ },
23
+ "rootCause": { "type": "string", "description": "file:line, when the source was read." },
24
+ "repro": {
25
+ "type": "object",
26
+ "required": ["path", "command", "code", "assertion"],
27
+ "properties": {
28
+ "path": { "type": "string", "minLength": 1 },
29
+ "command": { "type": "string", "minLength": 1 },
30
+ "code": {
31
+ "type": ["string", "null"],
32
+ "description": "ASSERTION_FAILED, PASSED, or the failure code the run produced."
33
+ },
34
+ "assertion": { "type": "string", "minLength": 1 },
35
+ "failedAt": { "type": ["string", "null"] },
36
+ "trace": { "type": "string" }
37
+ }
38
+ }
39
+ }
40
+ }
41
+ }