testeiya 0.7.0 → 0.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 (59) hide show
  1. package/PIPELINES.md +332 -0
  2. package/README.md +114 -1
  3. package/dist/prompt/testomat.io.js +2 -2
  4. package/dist/prompt/testomat.io.js.map +1 -1
  5. package/dist/src/args.js +104 -1
  6. package/dist/src/args.js.map +1 -1
  7. package/dist/src/checkpoint.js +0 -21
  8. package/dist/src/checkpoint.js.map +1 -1
  9. package/dist/src/cli.js +42 -1
  10. package/dist/src/cli.js.map +1 -1
  11. package/dist/src/git.js +68 -0
  12. package/dist/src/git.js.map +1 -0
  13. package/dist/src/mcp.js +4 -20
  14. package/dist/src/mcp.js.map +1 -1
  15. package/dist/src/output.js +16 -3
  16. package/dist/src/output.js.map +1 -1
  17. package/dist/src/pipeline/command-stage.js +27 -0
  18. package/dist/src/pipeline/command-stage.js.map +1 -0
  19. package/dist/src/pipeline/diagram.js +103 -0
  20. package/dist/src/pipeline/diagram.js.map +1 -0
  21. package/dist/src/pipeline/gate.js +59 -0
  22. package/dist/src/pipeline/gate.js.map +1 -0
  23. package/dist/src/pipeline/launcher.js +74 -0
  24. package/dist/src/pipeline/launcher.js.map +1 -0
  25. package/dist/src/pipeline/log.js +70 -0
  26. package/dist/src/pipeline/log.js.map +1 -0
  27. package/dist/src/pipeline/pipeline-file.js +242 -0
  28. package/dist/src/pipeline/pipeline-file.js.map +1 -0
  29. package/dist/src/pipeline/pipeline.js +415 -0
  30. package/dist/src/pipeline/pipeline.js.map +1 -0
  31. package/dist/src/pipeline/report.js +61 -0
  32. package/dist/src/pipeline/report.js.map +1 -0
  33. package/dist/src/pipeline/spec.js +2 -0
  34. package/dist/src/pipeline/spec.js.map +1 -0
  35. package/dist/src/pipeline/stage.js +150 -0
  36. package/dist/src/pipeline/stage.js.map +1 -0
  37. package/dist/src/pipeline/state.js +70 -0
  38. package/dist/src/pipeline/state.js.map +1 -0
  39. package/dist/src/pipeline/task-stage.js +84 -0
  40. package/dist/src/pipeline/task-stage.js.map +1 -0
  41. package/dist/src/pipeline/variables.js +41 -0
  42. package/dist/src/pipeline/variables.js.map +1 -0
  43. package/dist/src/run.js +6 -3
  44. package/dist/src/run.js.map +1 -1
  45. package/mcp-tools.json +1092 -2110
  46. package/package.json +7 -5
  47. package/pipelines/api-testing.yml +39 -0
  48. package/pipelines/issue-test-cases.yml +41 -0
  49. package/pipelines/pr-review.yml +43 -0
  50. package/pipelines/regression.yml +39 -0
  51. package/pipelines/web-testing.yml +50 -0
  52. package/prompt/testomat.io.ts +5 -5
  53. package/skills/skills.lock.json +2 -2
  54. package/skills/testomatio/test-management/improve-test-cases/SKILL.md +1 -1
  55. package/skills/testomatio/test-management/qa-test-code-coverage/SKILL.md +2 -2
  56. package/skills/testomatio/test-management/qa-write-test-cases/SKILL.md +3 -3
  57. package/skills/testomatio/test-management/scan-automation-project/SKILL.md +8 -8
  58. package/skills/testomatio/test-management/sync-test-cases-with-tms/SKILL.md +9 -9
  59. package/skills/testomatio/test-management/sync-test-cases-with-tms/references/TESTOMATIO_CLI.md +4 -4
package/PIPELINES.md ADDED
@@ -0,0 +1,332 @@
1
+ # Pipeline file format
2
+
3
+ A pipeline is a QA flow in one YAML file: agent tasks and shell commands, run in
4
+ order, passing files through one folder. One command runs it:
5
+
6
+ ```bash
7
+ testeiya pipeline <name|path.yml> [options]
8
+ ```
9
+
10
+ This page is the reference for the file. The [README](README.md#pipelines) shows
11
+ how to use pipelines in CI.
12
+
13
+ ## A complete example
14
+
15
+ ```yaml
16
+ name: regression
17
+ description: Run the tests a change can break, then explain the failures.
18
+
19
+ summary: |
20
+ Lead with failures this change caused. Flaky tests go last.
21
+
22
+ env:
23
+ BASE_BRANCH: ${BASE_BRANCH:-origin/main}
24
+
25
+ gates:
26
+ coverage:
27
+ file: [coverage.tests.yml]
28
+ message: No coverage.tests.yml. Create one with /qa-test-code-coverage.
29
+ testomatio:
30
+ env: [TESTOMATIO]
31
+ message: No Testomat.io project. Set TESTOMATIO to the project's API key.
32
+ local:
33
+ run: test -z "$REMOTE"
34
+ message: Tests run on the Testomat.io CI profile. No local log to read.
35
+ test-command:
36
+ env: [TEST_COMMAND]
37
+ message: No test command. Set TEST_COMMAND, e.g. "npx playwright test".
38
+
39
+ when: [coverage]
40
+
41
+ stages:
42
+ - id: tests
43
+ when: [testomatio]
44
+ run: |
45
+ npx -y @testomatio/reporter run --kind detect --warn \
46
+ --filter "coverage:file=coverage.tests.yml,diff=$BASE_BRANCH" \
47
+ ${REMOTE:+--remote "$REMOTE"} ${TEST_COMMAND:+"$TEST_COMMAND"}
48
+ timeout: 30m
49
+
50
+ - id: failures
51
+ when: [local, test-command]
52
+ session: triage
53
+ task: |
54
+ Read the test run in tests.log. For each failure, say if this change caused it,
55
+ if it is flaky, or if the test is out of date. /debug-fix-failed-flaky-autotests
56
+ Fail the run if this change caused a failure.
57
+ inputs: [tests.log]
58
+ ```
59
+
60
+ ## Top level
61
+
62
+ | Key | Required | Meaning |
63
+ |---|---|---|
64
+ | `name` | no | Shown in logs and `pipeline.json`. Default: the file name |
65
+ | `description` | no | One line, shown by `testeiya pipeline` |
66
+ | `summary` | no | Extra instructions for the final summary |
67
+ | `env` | no | Variables for every stage. Values expand `${NAME}` |
68
+ | `gates` | no | Named checks, defined once. See [Gates](#gates) |
69
+ | `when` | no | Gates for the whole run. One fails: nothing runs, exit 0 |
70
+ | `stages` | yes | The stages, run in order. At least one |
71
+
72
+ An unknown key is an error, found before anything runs.
73
+
74
+ ## Stages
75
+
76
+ | Key | Meaning |
77
+ |---|---|
78
+ | `id` | Required. Lowercase words joined by dashes. Unique. Names the stage's files. `pipeline` and `summary` are reserved |
79
+ | `task` | An agent stage: the task text. `/skill` names expand |
80
+ | `run` | A command stage: a shell command, run with `sh` in the checkout |
81
+ | `when` | Gates this stage needs. One fails: the stage is skipped with its message |
82
+ | `session` | A named, persistent conversation. See [Sessions](#sessions) |
83
+ | `inputs` | Files the stage reads, from the pipeline folder |
84
+ | `outputs` | Files the stage must leave in the pipeline folder |
85
+ | `each_line` | Run once per non-empty line of this file. See [Loops](#loops) |
86
+ | `each_file` | Run once per file matching this mask |
87
+ | `stopOn` | `fail`, `gate`, or both as a list. See [Stopping](#stopping) |
88
+ | `timeout` | `90s`, `30m`, `1h`, or seconds. Per item in a loop. Default: none |
89
+
90
+ - A stage has exactly one of `task` or `run`.
91
+ - A stage has at most one of `each_line` or `each_file`.
92
+ - A list key also takes a single value: `when: staging` is `when: [staging]`.
93
+ - Explorbot, the reporter and test runners are plain `run` commands. They have
94
+ no stage type of their own.
95
+
96
+ ## Gates
97
+
98
+ A gate is a named check with a message. It is defined once under `gates:` and
99
+ named in `when:`, at the top level or in a stage.
100
+
101
+ ```yaml
102
+ gates:
103
+ staging:
104
+ env: [EXPLORBOT_URL]
105
+ message: No staging URL. Set EXPLORBOT_URL to test in a browser.
106
+ ```
107
+
108
+ | Check | Passes when |
109
+ |---|---|
110
+ | `env: [A, B]` | every variable is set and not empty |
111
+ | `file: [path]` | every path exists, relative to the checkout |
112
+ | `run: <command>` | the command exits 0 within 30 seconds. Its output is hidden |
113
+ | `changes: {…}` | the changed files match, see [Changes](#changes) |
114
+
115
+ - A gate needs a `message` and at least one check. All its checks must pass.
116
+ - Every gate is checked once, before the first stage. `--dry-run` prints them all.
117
+ - A name in `when` that `gates` does not define is an error.
118
+ - `ai` is built in: a model and a provider key resolve. The summary needs it, so
119
+ a pipeline without one exits 2 before anything runs.
120
+
121
+ ### Gates from the CI job
122
+
123
+ The job can add a gate without editing the file:
124
+
125
+ ```bash
126
+ testeiya pipeline regression --gate 'pr:test -n "$PR_NUMBER"'
127
+ ```
128
+
129
+ - `--gate NAME:COMMAND` splits on the first colon. It is a `run` gate added to
130
+ the top-level `when`.
131
+ - When it fails the whole run is skipped and exits 0.
132
+ - It is checked before `--output`, so `-o "gh:pr#$PR_NUMBER"` is fine when the
133
+ number only exists when the gate passes.
134
+ - A name the file already defines is an error.
135
+
136
+ ### Checking a pipeline
137
+
138
+ ```bash
139
+ testeiya pipeline-check regression --set TEST_COMMAND="npx playwright test"
140
+ ```
141
+
142
+ - Validates the file the way a run does, then checks every gate it declares,
143
+ used or not, plus `ai` and any `--gate`.
144
+ - Prints each gate as passed or not, with its `message` when it does not pass.
145
+ - Runs no stage and spends no tokens. Takes `--set`, `--gate`, `--dir`, `--model`
146
+ and `--json`.
147
+ - Exit code: `0` every gate passes, `1` one does not, `2` the file is invalid.
148
+ - `--diagram` also writes `<name>.diagram.md` to the current folder: a Mermaid
149
+ chart of the stages in order, the files they pass on (dotted), and the gates
150
+ each one needs, green when it passes and red when it does not.
151
+
152
+ ### Changes
153
+
154
+ ```yaml
155
+ gates:
156
+ api-changed:
157
+ changes:
158
+ paths: ["src/api/**", "openapi.yaml"] # at least one changed file matches
159
+ ignore: ["**/*.md", "**/*.lock"] # these never count
160
+ min_files: 3 # at least this many count
161
+ message: No API code changed.
162
+ ```
163
+
164
+ Every key is optional. All given must hold, and at least one file must count.
165
+
166
+ - First round: the diff from the merge base with the target branch. The branch
167
+ comes from `GITHUB_BASE_REF`, `CI_MERGE_REQUEST_TARGET_BRANCH_NAME`,
168
+ `BITBUCKET_PR_DESTINATION_BRANCH`, then `BASE_BRANCH`, then `origin/main`.
169
+ - Later rounds: the diff since the commit the last round ran on.
170
+ - Git cannot say (a shallow clone): a pipeline that uses `changes` exits 2.
171
+ Check out with `fetch-depth: 0`.
172
+ - The list is written to `changes.txt` in the pipeline folder.
173
+
174
+ ## Variables
175
+
176
+ - `${NAME}` and `${NAME:-default}` expand in `task`, `env`, `outputs` and gate
177
+ values. Unset is empty. `run` goes to the shell, which uses the same rules and
178
+ more, like `${NAME:+word}`.
179
+ - `--set KEY=VALUE` and the job's own environment reach every stage. `--set` wins
180
+ over `env:`.
181
+ - The runner sets:
182
+
183
+ | Variable | Value |
184
+ |---|---|
185
+ | `PIPELINE_DIR` | absolute path of the pipeline folder |
186
+ | `PIPELINE_NAME` | the pipeline's name |
187
+ | `PIPELINE_ROUND` | 1, 2, … |
188
+ | `ITEM` | in a loop: the line, or the file's path |
189
+ | `ITEM_NAME` | in an `each_file` loop: the file name without its extension |
190
+ | `PIPELINE_RESUME` | `1` when a command stage is retried |
191
+
192
+ `TESTEIYA_FOLLOW_UP`, `TESTEIYA_THREAD` and `TESTEIYA_SESSION_FILE` are removed:
193
+ they belong to a single task.
194
+
195
+ ## Files
196
+
197
+ Every stage runs in the checkout. Paths in `inputs`, `outputs` and loops resolve
198
+ against the pipeline folder, `$PIPELINE_DIR`. A path ending in `/` is a folder.
199
+
200
+ Each stage leaves its own files:
201
+
202
+ | File | Stage | Content |
203
+ |---|---|---|
204
+ | `<id>.md` | agent | the stage's report |
205
+ | `<id>.json` | agent | status, reason, tokens, session id |
206
+ | `<id>.log` | command | stdout and stderr |
207
+
208
+ - A later stage reads them by name: `inputs: [review.md]`.
209
+ - An `outputs` entry may not reuse one of these names.
210
+ - Every input must come from an earlier stage, from the runner (`changes.txt`),
211
+ or exist in the folder at start.
212
+ - After a stage, every `outputs` entry must exist, or the stage is `broken`.
213
+ - An agent stage's task gets a block that names the folder, the files to read,
214
+ the files to write, and inputs a skipped stage did not produce.
215
+ - A command stage whose inputs all come from skipped stages is skipped too. An
216
+ agent stage still runs and is told what is missing.
217
+
218
+ ## Loops
219
+
220
+ ```yaml
221
+ - id: explore
222
+ each_line: endpoints.txt
223
+ run: npx -y explorbot api explore "$ITEM"
224
+
225
+ - id: sync
226
+ each_file: test-cases/*.md
227
+ task: Sync ${ITEM} to Testomat.io. /sync-test-cases-with-tms
228
+ ```
229
+
230
+ - `each_line` runs once per non-empty line. `each_file` runs once per matching
231
+ file, sorted, with `*` and `**`.
232
+ - Both resolve against the pipeline folder and are matched when the stage
233
+ starts, so earlier stages can write them.
234
+ - Nothing to loop over: the stage is skipped.
235
+ - Items run one after another. A failed item does not stop the others. The stage
236
+ takes the worst item's status.
237
+ - Each item writes `<id>.1.md`, `<id>.2.md`, … or `<id>.1.log`, …, joined into
238
+ `<id>.md` or `<id>.log` at the end.
239
+ - An agent loop with a `session` runs every item in that conversation.
240
+
241
+ ## Sessions
242
+
243
+ - `session: <name>` keeps the conversation in `.sessions/<name>.jsonl` in the
244
+ pipeline folder.
245
+ - Stages with the same name share one conversation. Different names never see
246
+ each other's reasoning, only each other's files.
247
+ - No `session`: a fresh conversation, saved nowhere.
248
+ - A session lasts as long as the folder. Kept between rounds, a stage on round 2
249
+ continues its round 1 work.
250
+
251
+ ## The summary
252
+
253
+ A pipeline with more than one stage ends with a summary. It is not written in
254
+ the file: the runner adds it.
255
+
256
+ - It runs the `pipeline-report` skill with the `summary:` text, reads every
257
+ stage's files, and writes `summary.md`.
258
+ - Its report is the only thing sent to `--output`. Every other stage runs with
259
+ `--no-thread` and posts nothing.
260
+ - It explains the pipeline's verdict and never changes it.
261
+ - It runs after the last stage, and after a stage that stopped the pipeline. Not
262
+ after Ctrl-C, and not when the run was skipped.
263
+ - A one-stage pipeline has no summary. That stage's report is the report.
264
+
265
+ ## Stopping
266
+
267
+ | Status | Agent stage | Command stage |
268
+ |---|---|---|
269
+ | `pass` | verdict pass | exit 0 |
270
+ | `fail` | verdict fail | non-zero exit |
271
+ | `broken` | crashed, or an `outputs` file is missing | an `outputs` file is missing |
272
+ | `timeout` | ran past `timeout` | same |
273
+ | `skipped` | a gate in `when` failed, or nothing to loop over | same, or its inputs were skipped |
274
+ | `not-run` | an earlier stage stopped the pipeline | same |
275
+
276
+ - `broken` and `timeout` always stop the pipeline.
277
+ - `stopOn: fail` stops it when this stage fails.
278
+ - `stopOn: gate` stops it when this stage is skipped by a gate.
279
+
280
+ Exit code: `0` pass or skipped, `1` any fail, broken or timeout, `2` bad usage
281
+ or no AI provider, `130` interrupted. `--exit-zero` forgives `fail` only.
282
+
283
+ ## Rounds
284
+
285
+ Without `--dir` a pipeline runs once in a temp folder. Keep `--dir` between
286
+ runs, as a CI cache per pull request, and each run is a round:
287
+
288
+ | Folder holds | Same commit | New commit |
289
+ |---|---|---|
290
+ | nothing | round 1 | round 1 |
291
+ | a stopped round | retry from the stage that stopped | new round |
292
+ | a finished round | the same round again | new round |
293
+
294
+ - A retry keeps stages that passed or failed and uses the folder's copy of the
295
+ file, so the stages cannot change half-way.
296
+ - A new round clears the last round's files, keeps `.sessions/`, and counts
297
+ `changes` from the last round's commit. Stages with a session are told what
298
+ changed. The summary updates its comment.
299
+ - `--dir` may not be the checkout or a folder above it.
300
+
301
+ `pipeline.json` in the folder holds the round, each stage's status and the
302
+ round history. `--json` prints it.
303
+
304
+ ## In CI
305
+
306
+ Find what only the job knows in a step of its own, then run the pipeline in one
307
+ command:
308
+
309
+ ```yaml
310
+ - name: Find pull request
311
+ env:
312
+ GH_TOKEN: ${{ github.token }}
313
+ run: |
314
+ gh api "repos/$GITHUB_REPOSITORY/commits/$GITHUB_SHA/pulls" \
315
+ --jq '.[0] // empty | "PR_NUMBER=\(.number)\nPR_TITLE=\(.title)\nPR_URL=\(.html_url)"' \
316
+ >> "$GITHUB_ENV"
317
+
318
+ - name: Run regression
319
+ env:
320
+ TESTOMATIO: ${{ secrets.TESTOMATIO }}
321
+ GH_TOKEN: ${{ github.token }}
322
+ OPENROUTER_API_KEY: ${{ secrets.OPENROUTER_API_KEY }}
323
+ TESTEIYA_MODEL: openrouter/anthropic/claude-haiku-4.5
324
+ run: |
325
+ npx -y testeiya@latest pipeline regression \
326
+ --gate 'pr:test -n "$PR_NUMBER"' \
327
+ --set REMOTE=github \
328
+ --set BASE_BRANCH=${{ github.event.before }} \
329
+ --set "TESTOMATIO_TITLE=PR #$PR_NUMBER $PR_TITLE" \
330
+ --set "TESTOMATIO_DESCRIPTION=$PR_URL" \
331
+ -o "gh:pr#$PR_NUMBER"
332
+ ```
package/README.md CHANGED
@@ -81,6 +81,7 @@ testeiya models anthropic # list what your key can reach
81
81
  ```bash
82
82
  testeiya task "<task>" # run one task, deliver a report, exit
83
83
  testeiya ask "<question>" # answer a question, no report
84
+ testeiya pipeline <name> # run several stages in a row, see Pipelines
84
85
  testeiya skills # list the skills bundled with this package
85
86
  testeiya sessions # list saved sessions for this folder
86
87
  ```
@@ -187,6 +188,117 @@ TESTOMATIO=tstmt_xxx testeiya task \
187
188
  --output audits/$(date +%F).md
188
189
  ```
189
190
 
191
+ ## Pipelines
192
+
193
+ Some QA flows take more than one run, and not every step is the agent: review a
194
+ pull request, run Explorbot against staging, then explain what broke. A
195
+ pipeline is that flow in one YAML file, run by one command.
196
+
197
+ ```bash
198
+ testeiya pipeline # list the bundled pipelines
199
+ testeiya pipeline web-testing -o gh:pr-comment
200
+ testeiya pipeline .github/testeiya/my-flow.yml --dry-run
201
+ testeiya pipeline-check .github/testeiya/my-flow.yml # check the file and every gate
202
+ testeiya pipeline-check regression --diagram # also draw it as a Mermaid chart
203
+ ```
204
+
205
+ | Pipeline | What it does |
206
+ |---|---|
207
+ | `pr-review` | requirements, QA review, test cases, sync to Testomat.io |
208
+ | `issue-test-cases` | an issue or epic, with its sub-issues, becomes test cases; set `ISSUE_URL` |
209
+ | `web-testing` | review, an Explorbot plan, `explorbot test`, what broke |
210
+ | `api-testing` | review lists changed endpoints, Explorbot tests each one |
211
+ | `regression` | runs the tests `coverage.tests.yml` maps to the change, explains failures |
212
+
213
+ To change one, copy it from `pipelines/` into your repo, edit it, and pass the
214
+ path.
215
+
216
+ ### The file
217
+
218
+ ```yaml
219
+ name: web-testing
220
+ description: Review a pull request, test it in a browser, explain what broke.
221
+
222
+ gates:
223
+ staging:
224
+ env: [EXPLORBOT_URL]
225
+ message: No staging URL. Set EXPLORBOT_URL to test in a browser.
226
+ browser:
227
+ run: ls "${PLAYWRIGHT_BROWSERS_PATH:-$HOME/.cache/ms-playwright}" | grep -q chromium
228
+ message: No browser. Run npx playwright install --with-deps chromium.
229
+
230
+ env:
231
+ EXPLORBOT_OUTPUT: ${PIPELINE_DIR}/explorbot
232
+
233
+ stages:
234
+ - id: review
235
+ session: qa
236
+ task: Review this pull request. Write browser scenarios to plan.md. /explorbot-plan
237
+ outputs: [plan.md]
238
+ - id: explore
239
+ when: [staging, browser]
240
+ run: npx -y explorbot test "$PIPELINE_DIR/plan.md" '*'
241
+ timeout: 30m
242
+ - id: results
243
+ session: qa
244
+ task: Read the Explorbot report under explorbot/ and say what broke.
245
+ inputs: [explorbot/]
246
+ ```
247
+
248
+ - A stage is a `task` for the agent or a `run` shell command.
249
+ - Stages pass files through one folder, `$PIPELINE_DIR`. An agent stage writes
250
+ `<id>.md` and `<id>.json`, a command writes `<id>.log`. `inputs` and
251
+ `outputs` name the rest.
252
+ - `gates` are named checks: `env`, `file`, `run`, or `changes` (`paths`,
253
+ `ignore`, `min_files`). `when:` names the gates a stage needs. One fails and
254
+ the stage is skipped with its message. In the top-level `when:` it skips the
255
+ whole run.
256
+ - `session: <name>` keeps a conversation. Stages with the same name share it.
257
+ No `session` is a fresh one.
258
+ - `each_line: file.txt` and `each_file: cases/*.md` run a stage once per item,
259
+ as `$ITEM`.
260
+ - `stopOn: fail` stops the pipeline when the stage fails, `stopOn: gate` when it
261
+ was skipped. A broken or timed-out stage always stops it.
262
+
263
+ The full format is in [PIPELINES.md](PIPELINES.md).
264
+
265
+ A pipeline with more than one stage ends with a summary written by the
266
+ `pipeline-report` skill. It is the only thing posted to `--output`. Every other
267
+ stage runs with `--no-thread`, so nothing else touches the pull request.
268
+
269
+ ### Rounds
270
+
271
+ Keep `--dir` between runs, as a CI cache per pull request, and each run is a
272
+ round. A re-run on the same commit retries from the stage that stopped. A new
273
+ commit starts a new round: `changes` count from the last round's commit, stages
274
+ with a session are told what changed, and the summary updates its comment.
275
+
276
+ ```yaml
277
+ - uses: actions/cache/restore@v4
278
+ with:
279
+ path: ${{ runner.temp }}/qa
280
+ key: qa-pr-${{ github.event.number }}-${{ github.run_id }}
281
+ restore-keys: qa-pr-${{ github.event.number }}-
282
+ - run: npx testeiya@latest pipeline pr-review --dir ${{ runner.temp }}/qa -o gh:pr-comment --exit-zero
283
+ - uses: actions/cache/save@v4
284
+ if: always()
285
+ with:
286
+ path: ${{ runner.temp }}/qa
287
+ key: qa-pr-${{ github.event.number }}-${{ github.run_id }}-${{ github.run_attempt }}
288
+ ```
289
+
290
+ Stages ignore `TESTEIYA_FOLLOW_UP`, `TESTEIYA_THREAD` and `TESTEIYA_SESSION_FILE`:
291
+ those belong to a single task. Use `--thread` on the pipeline instead.
292
+
293
+ `--set KEY=VALUE` sets a variable for every stage. `--gate NAME:COMMAND` adds a
294
+ check from the CI job: when the command fails, the whole run is skipped and exits
295
+ 0, before any output is checked. Use it for what only the job knows, like
296
+ whether this push belongs to a pull request. `--dry-run` prints every
297
+ gate and stage and spends nothing. `testeiya pipeline-check` checks the file and
298
+ every gate it declares, used or not, and prints the message of each one that
299
+ does not pass. `--diagram` also writes the pipeline as a Mermaid chart.
300
+ `pipeline.json` in the folder holds each stage's status; `--json` prints it.
301
+
190
302
  ## CI setup
191
303
 
192
304
  Testeiya needs three things in any CI system: a provider key, a model name, and
@@ -394,7 +506,8 @@ testeiya task "<task>" --output report.md --output run.json --output gh:pr-comme
394
506
  | `gh:pr#123` | posted on that pull request |
395
507
 
396
508
  Posting uses the GitHub CLI and resolves the PR number from `GITHUB_EVENT_PATH`,
397
- `GITHUB_REF`, or `gh pr view` — so inside Actions it just works. Every
509
+ `GITHUB_REF`, or `gh pr view`. On a push with no PR of its own, like a merge to
510
+ main, it posts on the PR that brought the commit in. Inside Actions it just works. Every
398
511
  destination is checked before the run starts, so a missing `gh` costs no tokens.
399
512
 
400
513
  `--footer "<text>"` adds a line under the report and `--header "<text>"` adds
@@ -37,11 +37,11 @@ export function testomatio(options) {
37
37
  }
38
38
  function dynamicData(tms) {
39
39
  if (tms === "mcp-proxy") {
40
- return "Get them through the `mcp` tool: search it for the operation you need, then call that operation. The most common reads are also registered as tools of their own; use those directly when they fit.";
40
+ return "Get them through the Testomat.io tools, one per entity (`tests`, `runs`, `testruns`, `issues`, …); pick the operation with its `command` param. The `mcp` tool searches them when the name is unclear.";
41
41
  }
42
42
  if (tms === "cli-only") {
43
43
  return "There are no Testomat.io tools in this session. Get them from the REST API: `curl` with the `TESTOMATIO` token as the Authorization header against `$TESTOMATIO_URL/api/v2`. Keep to documented endpoints; never invent paths or parameters.";
44
44
  }
45
- return "Get them through the `testomatio-<slug>` MCP tools, one set per project.";
45
+ return "Get them through the `testomatio-<slug>` MCP tools, one set per project and one tool per entity; pick the operation with its `command` param.";
46
46
  }
47
47
  //# sourceMappingURL=testomat.io.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"testomat.io.js","sourceRoot":"","sources":["../../prompt/testomat.io.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,MAAM,QAAQ,CAAC;AAE5B;;;;GAIG;AACH,MAAM,UAAU,UAAU,CAAC,OAA0B;IACnD,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,CAAC;QACvB,OAAO,MAAM,CAAA;;;;;;;KAOZ,CAAC;IACJ,CAAC;IAED,IAAI,GAAG,GAAG,EAAE,CAAC;IACb,IAAI,OAAO,CAAC,UAAU;QAAE,GAAG,GAAG,OAAO,OAAO,CAAC,UAAU,KAAK,CAAC;IAC7D,OAAO,MAAM,CAAA;;;;;6FAK8E,WAAW,CAAC,OAAO,CAAC,GAAG,CAAC;;;;;;;;8IAQyB,GAAG;;GAE9I,CAAC;AACJ,CAAC;AAED,SAAS,WAAW,CAAC,GAAc;IACjC,IAAI,GAAG,KAAK,WAAW,EAAE,CAAC;QACxB,OAAO,qMAAqM,CAAC;IAC/M,CAAC;IACD,IAAI,GAAG,KAAK,UAAU,EAAE,CAAC;QACvB,OAAO,8OAA8O,CAAC;IACxP,CAAC;IACD,OAAO,0EAA0E,CAAC;AACpF,CAAC"}
1
+ {"version":3,"file":"testomat.io.js","sourceRoot":"","sources":["../../prompt/testomat.io.ts"],"names":[],"mappings":"AAAA,OAAO,MAAM,MAAM,QAAQ,CAAC;AAE5B;;;;GAIG;AACH,MAAM,UAAU,UAAU,CAAC,OAA0B;IACnD,IAAI,CAAC,OAAO,CAAC,SAAS,EAAE,CAAC;QACvB,OAAO,MAAM,CAAA;;;;;;;KAOZ,CAAC;IACJ,CAAC;IAED,IAAI,GAAG,GAAG,EAAE,CAAC;IACb,IAAI,OAAO,CAAC,UAAU;QAAE,GAAG,GAAG,OAAO,OAAO,CAAC,UAAU,KAAK,CAAC;IAC7D,OAAO,MAAM,CAAA;;;;;6FAK8E,WAAW,CAAC,OAAO,CAAC,GAAG,CAAC;;;;;;;;8IAQyB,GAAG;;GAE9I,CAAC;AACJ,CAAC;AAED,SAAS,WAAW,CAAC,GAAc;IACjC,IAAI,GAAG,KAAK,WAAW,EAAE,CAAC;QACxB,OAAO,wMAAwM,CAAC;IAClN,CAAC;IACD,IAAI,GAAG,KAAK,UAAU,EAAE,CAAC;QACvB,OAAO,8OAA8O,CAAC;IACxP,CAAC;IACD,OAAO,+IAA+I,CAAC;AACzJ,CAAC"}
package/dist/src/args.js CHANGED
@@ -4,6 +4,7 @@ export const WELCOME = `
4
4
 
5
5
  testeiya task "analyze this pr and find inconsistencies"
6
6
  testeiya ask "is this feature ready"
7
+ testeiya pipeline web-testing
7
8
 
8
9
  testeiya doctor check your setup
9
10
  testeiya --help options and environment
@@ -14,6 +15,8 @@ export const USAGE = `testeiya, an AI testing agent
14
15
  Usage:
15
16
  testeiya task "<task>" run one task and write a report
16
17
  testeiya ask "<question>" answer a question
18
+ testeiya pipeline <name> run a multi-stage pipeline, or list them with no name
19
+ testeiya pipeline-check <name> check a pipeline file and its gates, run nothing
17
20
  testeiya doctor check what a run would resolve
18
21
  testeiya models [pattern] list the models your key can reach
19
22
  testeiya skills [pattern] list the skills bundled with this package
@@ -35,8 +38,15 @@ Options:
35
38
  --header <text> line added above the report
36
39
  --footer <text> line added under the report
37
40
  --no-default-footer do not sign the report
41
+ --no-thread keep out of the comment thread: post nothing, read no comments
38
42
  --json machine-readable output
39
43
  --probe doctor only: test the key with one request
44
+
45
+ Pipeline options:
46
+ --dir <path> the pipeline folder; keep it between runs for rounds
47
+ --dry-run show gates and stages, spend no tokens
48
+ --set KEY=VALUE set a variable for every stage, repeatable
49
+ --gate NAME:COMMAND skip the whole run unless COMMAND exits 0, repeatable
40
50
  -h, --help show this
41
51
  -v, --version show version
42
52
 
@@ -173,6 +183,28 @@ Skills
173
183
  loaded before the run, e.g. "/qa-thinking". A name we do not ship is left
174
184
  alone, so a stray "/word" in the task is just a word.
175
185
 
186
+ Pipelines
187
+
188
+ A pipeline is several stages in one YAML file: agent tasks and shell commands,
189
+ run in order, passing files through one folder. "testeiya pipeline" lists the
190
+ bundled ones; pass a path to run your own.
191
+
192
+ testeiya pipeline web-testing -o gh:pr-comment
193
+ testeiya pipeline .github/testeiya/regression.yml --set TEST_COMMAND="npx playwright test"
194
+
195
+ --gate adds a check from the CI job, e.g. --gate 'pr:test -n "$PR_NUMBER"'.
196
+ When it fails the whole run is skipped and exits 0.
197
+
198
+ A stage whose gate fails is skipped with the gate's message. A final summary
199
+ reads every stage and is the only thing posted. Keep --dir between runs, as a
200
+ CI cache per pull request, and a re-run on the same commit retries from the
201
+ stage that stopped, while a new commit starts a round told what changed.
202
+ --dry-run shows the plan and spends nothing. "testeiya pipeline-check" checks
203
+ the file and every gate it declares, and prints the message of each gate that
204
+ does not pass. It exits 1 when one does not, 2 when the file is invalid.
205
+ --diagram also writes <name>.diagram.md: a Mermaid chart of the stages, the
206
+ files they pass on, and the gates, coloured by the check.
207
+
176
208
  Testomat.io
177
209
 
178
210
  Set TESTOMATIO to a project API key and the agent reads and writes that
@@ -180,7 +212,7 @@ Testomat.io
180
212
  Add the project id, with --project or TESTOMATIO_PROJECT_ID, and it also gets
181
213
  the Testomat.io tools. The MCP server needs the id: a token alone does not say
182
214
  which project to talk to.`;
183
- export const COMMANDS = ["task", "ask", "models", "skills", "doctor", "sessions", "help"];
215
+ export const COMMANDS = ["task", "ask", "pipeline", "pipeline-check", "models", "skills", "doctor", "sessions", "help"];
184
216
  export function parseCliArgs(argv) {
185
217
  const first = argv[0];
186
218
  if (!first)
@@ -212,6 +244,28 @@ export function parseCliArgs(argv) {
212
244
  args.prompt = text;
213
245
  if ((first === "models" || first === "skills") && text)
214
246
  args.pattern = text;
247
+ if ((first === "pipeline" || first === "pipeline-check") && text)
248
+ args.pipeline = text;
249
+ if (first === "pipeline-check" && !text)
250
+ return { error: "pipeline-check needs a pipeline name or a .yml path" };
251
+ if (typeof values.dir === "string")
252
+ args.dir = values.dir;
253
+ if (values["dry-run"])
254
+ args.dryRun = true;
255
+ if (values.diagram)
256
+ args.diagram = true;
257
+ if (Array.isArray(values.set)) {
258
+ const sets = parseSets(values.set);
259
+ if (typeof sets === "string")
260
+ return { error: sets };
261
+ args.sets = sets;
262
+ }
263
+ if (Array.isArray(values.gate)) {
264
+ const gates = parseGates(values.gate);
265
+ if (typeof gates === "string")
266
+ return { error: gates };
267
+ args.gates = gates;
268
+ }
215
269
  if (Array.isArray(values.output))
216
270
  args.outputs = values.output;
217
271
  if (values.json)
@@ -246,6 +300,8 @@ export function parseCliArgs(argv) {
246
300
  args.footer = values.footer;
247
301
  if (values["no-default-footer"])
248
302
  args.noDefaultFooter = true;
303
+ if (values["no-thread"])
304
+ args.noThread = true;
249
305
  if (args.session && (args.continueLast || args.resume || args.name || args.noSession)) {
250
306
  return { error: "--session already names and continues a session" };
251
307
  }
@@ -273,17 +329,64 @@ const RUN_OPTIONS = {
273
329
  header: { type: "string" },
274
330
  footer: { type: "string" },
275
331
  "no-default-footer": { type: "boolean" },
332
+ "no-thread": { type: "boolean" },
333
+ help: HELP_FLAG,
334
+ };
335
+ const PIPELINE_OPTIONS = {
336
+ output: RUN_OPTIONS.output,
337
+ json: JSON_FLAG,
338
+ model: { type: "string" },
339
+ project: { type: "string" },
340
+ thread: { type: "string" },
341
+ "exit-zero": { type: "boolean" },
342
+ header: { type: "string" },
343
+ footer: { type: "string" },
344
+ "no-default-footer": { type: "boolean" },
345
+ dir: { type: "string" },
346
+ "dry-run": { type: "boolean" },
347
+ set: { type: "string", multiple: true },
348
+ gate: { type: "string", multiple: true },
276
349
  help: HELP_FLAG,
277
350
  };
278
351
  const OPTIONS = {
279
352
  task: RUN_OPTIONS,
280
353
  ask: RUN_OPTIONS,
354
+ pipeline: PIPELINE_OPTIONS,
355
+ "pipeline-check": {
356
+ json: JSON_FLAG,
357
+ model: { type: "string" },
358
+ dir: PIPELINE_OPTIONS.dir,
359
+ set: PIPELINE_OPTIONS.set,
360
+ gate: PIPELINE_OPTIONS.gate,
361
+ diagram: { type: "boolean" },
362
+ help: HELP_FLAG,
363
+ },
281
364
  models: { json: JSON_FLAG, help: HELP_FLAG },
282
365
  skills: { json: JSON_FLAG, help: HELP_FLAG },
283
366
  doctor: { json: JSON_FLAG, probe: { type: "boolean" }, model: { type: "string" }, help: HELP_FLAG },
284
367
  sessions: { json: JSON_FLAG, help: HELP_FLAG },
285
368
  help: {},
286
369
  };
370
+ function parseSets(values) {
371
+ const sets = {};
372
+ for (const value of values) {
373
+ const equals = value.indexOf("=");
374
+ if (equals <= 0)
375
+ return `--set takes KEY=VALUE, got "${value}"`;
376
+ sets[value.slice(0, equals)] = value.slice(equals + 1);
377
+ }
378
+ return sets;
379
+ }
380
+ function parseGates(values) {
381
+ const gates = {};
382
+ for (const value of values) {
383
+ const colon = value.indexOf(":");
384
+ if (colon <= 0 || !value.slice(colon + 1).trim())
385
+ return `--gate takes NAME:COMMAND, got "${value}"`;
386
+ gates[value.slice(0, colon)] = value.slice(colon + 1);
387
+ }
388
+ return gates;
389
+ }
287
390
  function isCommand(value) {
288
391
  return COMMANDS.includes(value);
289
392
  }