@1aboveio/skills 1.0.15 → 1.0.16

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 (90) hide show
  1. package/.codex-plugin/plugin.json +1 -1
  2. package/.codex-plugin/resources.json +149 -145
  3. package/package.json +4 -4
  4. package/skills/app-dev/airflow/SKILL.md +1 -1
  5. package/skills/app-dev/android-development/SKILL.md +28 -1
  6. package/skills/app-dev/android-development/references/diagnosis.md +50 -0
  7. package/skills/app-dev/android-development/references/init.md +31 -2
  8. package/skills/app-dev/android-development/references/setup.md +8 -2
  9. package/skills/app-dev/android-development/references/ui-journeys.md +58 -0
  10. package/skills/app-dev/android-development/scripts/device-run.sh +93 -2
  11. package/skills/app-dev/android-development/scripts/failure_capture.py +331 -0
  12. package/skills/app-dev/android-development/scripts/init.mjs +129 -9
  13. package/skills/app-dev/android-development/scripts/maestro_failure.py +401 -0
  14. package/skills/app-dev/android-development/scripts/test_failure_capture.py +378 -0
  15. package/skills/app-dev/android-development/scripts/test_maestro_failure.py +462 -0
  16. package/skills/app-dev/app-debug/SKILL.md +1 -1
  17. package/skills/app-dev/better-auth/SKILL.md +1 -1
  18. package/skills/app-dev/data-catalog/SKILL.md +1 -1
  19. package/skills/app-dev/data-quality/SKILL.md +1 -1
  20. package/skills/app-dev/ios-foundation/SKILL.md +1 -1
  21. package/skills/app-dev/monorepo/SKILL.md +1 -1
  22. package/skills/app-dev/nextjs-fullstack/SKILL.md +1 -1
  23. package/skills/app-dev/prisma-setup/SKILL.md +1 -1
  24. package/skills/app-dev/pyspark/SKILL.md +1 -1
  25. package/skills/app-dev/python-backend/SKILL.md +1 -1
  26. package/skills/app-dev/shadcn/SKILL.md +1 -1
  27. package/skills/app-dev/zod-v4/SKILL.md +1 -1
  28. package/skills/cicd/cloud-build/SKILL.md +1 -1
  29. package/skills/cicd/cloud-debug/SKILL.md +1 -1
  30. package/skills/cicd/cloud-deploy/SKILL.md +1 -1
  31. package/skills/cicd/docker/SKILL.md +1 -1
  32. package/skills/cicd/github-actions/SKILL.md +1 -1
  33. package/skills/cicd/github-flow/SKILL.md +1 -1
  34. package/skills/cicd/google-cloud/SKILL.md +1 -1
  35. package/skills/cicd/mergify/SKILL.md +1 -1
  36. package/skills/cicd/podman/SKILL.md +1 -1
  37. package/skills/content/article-review/SKILL.md +1 -1
  38. package/skills/content/article-to-image/SKILL.md +1 -1
  39. package/skills/content/flowchart-svg/SKILL.md +1 -1
  40. package/skills/content/image-gen/SKILL.md +1 -1
  41. package/skills/content/mermaid/SKILL.md +1 -1
  42. package/skills/content/wechat-article-writer/SKILL.md +1 -1
  43. package/skills/content/wechat-publisher/SKILL.md +1 -1
  44. package/skills/content/wechat-search/SKILL.md +1 -1
  45. package/skills/content/xai-search/SKILL.md +1 -1
  46. package/skills/core/distill-writing/SKILL.md +1 -1
  47. package/skills/core/doc-to-md/SKILL.md +1 -1
  48. package/skills/core/editorial-review/SKILL.md +1 -1
  49. package/skills/core/house-style/SKILL.md +1 -1
  50. package/skills/core/plain-language/SKILL.md +1 -1
  51. package/skills/core/polish-cn/SKILL.md +1 -1
  52. package/skills/core/reg-parser/SKILL.md +1 -1
  53. package/skills/core/terminology/SKILL.md +1 -1
  54. package/skills/core/write-doc/SKILL.md +1 -1
  55. package/skills/core/write-report/SKILL.md +1 -1
  56. package/skills/engineering/better-review/SKILL.md +1 -1
  57. package/skills/engineering/e2e-test/SKILL.md +1 -1
  58. package/skills/engineering/engineering/SKILL.md +1 -1
  59. package/skills/engineering/engineering-runtime/SKILL.md +1 -1
  60. package/skills/engineering/engineering-runtime/coherence/workflow.json +13 -13
  61. package/skills/engineering/ensure-coverage/SKILL.md +1 -1
  62. package/skills/engineering/golden-path/SKILL.md +1 -1
  63. package/skills/engineering/harness-runtime/SKILL.md +4 -3
  64. package/skills/engineering/harness-runtime/agent-catalog.mjs +928 -0
  65. package/skills/engineering/harness-runtime/contracts.md +2 -0
  66. package/skills/engineering/harness-runtime/references/model-catalog.md +7 -1
  67. package/skills/engineering/resolve-issues/SKILL.md +9 -6
  68. package/skills/engineering/resolve-issues/references/delivery-progress.md +2 -0
  69. package/skills/engineering/resolve-issues/references/expire.md +6 -3
  70. package/skills/engineering/resolve-issues/references/implementation.md +6 -1
  71. package/skills/engineering/resolve-issues/references/preflight.md +26 -17
  72. package/skills/engineering/resolve-issues/references/review.md +21 -9
  73. package/skills/engineering/resolve-issues/references/shared-modules.md +88 -20
  74. package/skills/engineering/resolve-issues/references/task-plan.md +35 -1
  75. package/skills/engineering/resolve-issues/scripts/preflight-models.mjs +382 -592
  76. package/skills/engineering/resolve-release/SKILL.md +1 -1
  77. package/skills/engineering/rush-issues/SKILL.md +4 -4
  78. package/skills/engineering/rush-release/SKILL.md +1 -1
  79. package/skills/engineering/test-quality/SKILL.md +1 -1
  80. package/skills/risk-compliance/compliance-audit/SKILL.md +1 -1
  81. package/skills/risk-compliance/contract-compare/SKILL.md +1 -1
  82. package/skills/risk-compliance/contract-review/SKILL.md +1 -1
  83. package/skills/risk-compliance/doc-authenticity/SKILL.md +1 -1
  84. package/skills/risk-compliance/fraud-detection/SKILL.md +1 -1
  85. package/skills/risk-compliance/payment-analysis/SKILL.md +1 -1
  86. package/skills/risk-compliance/portfolio-payment-analysis/SKILL.md +1 -1
  87. package/skills/risk-compliance/similar-domain/SKILL.md +1 -1
  88. package/skills/risk-compliance/tld-detection/SKILL.md +1 -1
  89. package/skills/engineering/resolve-issues/scripts/discover-models.mjs +0 -9
  90. package/skills/engineering/resolve-issues/scripts/model-catalog.mjs +0 -9
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@1aboveio/skills",
3
- "version": "1.0.15",
3
+ "version": "1.0.16",
4
4
  "description": "Every 1aboveio skill package in one: install once, then choose which packages Pi loads with npx @1aboveio/skills select.",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -28,8 +28,8 @@
28
28
  "nativeCodex": {
29
29
  "schemaVersion": 1,
30
30
  "metadataPath": ".codex-plugin/resources.json",
31
- "metadataDigest": "2bdb58dba1de073baf79efe2c5118ad75c996710344a3cde581976921c1b23ec",
32
- "releaseIdentity": "a8975fa459ebba2a3e2661d861bdb9d43c072c1b01adaf41def9a93523782800"
31
+ "metadataDigest": "efea727f2b53be74c1dad921e4d976e9a4bf22054a5461217bea9a5983de48cc",
32
+ "releaseIdentity": "357a73a8819a5c43f9e6d935ece7e5efc38519d0b83e0984798677c4cb9dfc0d"
33
33
  },
34
34
  "pi": {
35
35
  "skills": [
@@ -260,7 +260,7 @@
260
260
  ]
261
261
  }
262
262
  ],
263
- "digest": "6ad0dafe4a400162776b1bf6d7c9e5ab31bf37a86d18b035fcadc4c11d06f799"
263
+ "digest": "26eb35331224e4dd2e0e8ef13bafd6383af21ae416bcf9e5acc28e68c6b45221"
264
264
  },
265
265
  "publishConfig": {
266
266
  "registry": "https://registry.npmjs.org/",
@@ -2,7 +2,7 @@
2
2
  name: airflow
3
3
  description: Airflow DAG architecture and conventions. Use when designing, writing, reviewing, testing, or debugging Airflow DAGs and tasks, including TaskGroups, data intervals, timezone-aware date parameters, CLI validation, environment-based scheduling, and pytest integrity tests.
4
4
  user-invocable: true
5
- release-version: 0.47.0
5
+ release-version: 0.48.0
6
6
  ---
7
7
  **Updates:** Once/session run `node <skill-base-dir>/release-check.mjs`.
8
8
  # Airflow DAG Development Patterns
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: android-development
3
3
  description: "Prove and debug Expo / React Native apps on an owned Android emulator. Use for Android startup/rendering failures, Metro or ADB problems, UiAutomator/harness failures, APK builds, SDK/NDK/JDK workspace setup, native journey validation, and choosing Kotlin/Compose versus React Native for Android flows. Reproduce failed Android CI checkpoints off CI before fixing and redispatching. Standalone invocation offers Project or Session; init persists repo conventions. NOT for workflow YAML, CI speed or runners (github-actions), browser tests (e2e-test), iOS (ios-foundation), plain Jest tests, or signing/store release."
4
- release-version: 0.47.0
4
+ release-version: 0.48.0
5
5
  ---
6
6
  **Updates:** Once/session run `node <skill-base-dir>/release-check.mjs`.
7
7
  # Android development
@@ -43,6 +43,31 @@ Choose the task, not a fresh environment for every attempt:
43
43
  repo-specific stages in order and record proof. Do not add Expo or a new harness merely
44
44
  to match the examples, or repeat cold setup for ordinary fixes.
45
45
 
46
+ ## Device failure record
47
+
48
+ A failed device case leaves one record and one line, whichever layer saw the failure:
49
+
50
+ - `failure.json` holding `case`, `step`, `error`, `screenshot`, `uiDump`, `logcatTail`,
51
+ `evidenceDir` and `cleanupErrors`, captured at the failing step before teardown.
52
+ - One printed line `NATIVE_FAILURE {"case":…,"step":…,"error":…,"evidenceDir":…}`.
53
+
54
+ Journey code wraps its `wait_for`/`tap` helpers in
55
+ [failure_capture.py](scripts/failure_capture.py), `with recorder.step("tap Pay"):`. Without
56
+ that, [device-run.sh](scripts/device-run.sh) writes a partial record with `step: null` when
57
+ the journey fails and left none. The first failure is never replaced by a cleanup error.
58
+ The contract, other languages and the Maestro/Detox mapping are in
59
+ [diagnosis](references/diagnosis.md#record-the-failed-device-case).
60
+
61
+ ## UI journey tool
62
+
63
+ The skill references Maestro; it does not bundle or install it. A repo with no journey tool
64
+ is offered Maestro for new single-device journeys; a repo with a working harness keeps it
65
+ and adds Maestro flows alongside. Run it on the session's own emulator
66
+ (`maestro --device <serial>`) and convert its output with
67
+ [maestro_failure.py](scripts/maestro_failure.py), which writes the record above. Read
68
+ [UI journeys](references/ui-journeys.md) for the device-ownership cost, evidence layout, MCP
69
+ use and the cases that stay custom.
70
+
46
71
  ## Validation ladder
47
72
 
48
73
  Read [feature work](references/feature-work.md) for selection, ownership and handoff.
@@ -123,5 +148,7 @@ higher levels still pending rather than claiming acceptance.
123
148
  | Parallel warm servers and test-first provenance | [Parallel sessions](references/workspace.md#parallel-development-sessions) and [test-first provenance](references/feature-work.md#run-tests-before-chasing-commit-labels) |
124
149
  | Builds, provisioning, device execution, cleanup and setup proof | [setup.md](references/setup.md); use [device-run.sh](scripts/device-run.sh) only without an existing resource owner |
125
150
  | Feature/fix validation and handoff | [feature-work.md](references/feature-work.md) |
151
+ | Device failure record and evidence capture | [diagnosis.md](references/diagnosis.md#record-the-failed-device-case) |
152
+ | UI journey tool (Maestro) and its evidence converter | [UI journeys](references/ui-journeys.md) |
126
153
  | Failure reproduction and correction | [diagnosis.md](references/diagnosis.md), then the relevant [gotcha](references/gotchas.md) |
127
154
  | CI integration | [ci.md](references/ci.md), [template](assets/android-ci-job.yml), and `github-actions` |
@@ -94,6 +94,56 @@ reproduction command, controlled change, observations, evidence locations, prima
94
94
  cleanup outcomes, and remaining uncertainty. Do not require an APK hash for a compiler
95
95
  failure that produced no APK.
96
96
 
97
+ ## Record the failed device case
98
+
99
+ Capture the failing step where it fails, not after cleanup. One contract covers every
100
+ harness, so a coordinator, a fix brief and a later diagnosis read the same thing.
101
+
102
+ `failure.json` in the evidence directory holds `case`, `step`, `error`, `screenshot`,
103
+ `uiDump`, `logcatTail`, `evidenceDir` and `cleanupErrors`. The three artifact fields are
104
+ file names relative to `evidenceDir`, or `null` when that capture failed; the reason then
105
+ goes to `cleanupErrors` and never replaces `error`. Write the record once: the first
106
+ failure is kept, and neither a later failure nor a cleanup problem overwrites it. Print
107
+ exactly one line, `NATIVE_FAILURE {"case":…,"step":…,"error":…,"evidenceDir":…}`, so a
108
+ reader that only sees the run output still learns the cause and where the evidence is.
109
+
110
+ In Python journeys wrap the step helpers in [failure_capture.py](../scripts/failure_capture.py):
111
+
112
+ ```python
113
+ from failure_capture import FailureRecorder
114
+
115
+ recorder = FailureRecorder(case=os.environ["DEVICE_CASE"], evidence_dir=os.environ["LOG_DIR"])
116
+
117
+ with recorder.step("tap Pay"):
118
+ tap("Pay")
119
+ with recorder.step("wait for receipt"):
120
+ wait_for("Thank you")
121
+ ```
122
+
123
+ `recorder.step` is a context manager and a decorator. On the first exception in a step it
124
+ captures `adb exec-out screencap -p`, `uiautomator dump` and a bounded logcat tail on the
125
+ injected serial, writes the record, prints the line and re-raises the original exception.
126
+ Captures are bounded; a capture failure is a `cleanupErrors` entry. A cancelled run
127
+ (`KeyboardInterrupt`, `SystemExit`) is not a failed case.
128
+
129
+ Other languages implement the same two shapes. When the journey harness cannot report a
130
+ step, fix the harness in the PR so every case records one; do not wrap it in a run-only
131
+ script, and do not add a second failure format.
132
+
133
+ Without an in-journey helper, [device-run.sh](../scripts/device-run.sh) falls back: after a
134
+ non-zero journey exit with no `failure.json` in `LOG_DIR` it captures the screen, hierarchy
135
+ and logcat tail before tearing the device down, writes a partial record with `step: null`,
136
+ and prints `NATIVE_FAILURE`. Its `original_stage_exit=<n> cleanup_exit=<n>` line and exit
137
+ status are unchanged, and a record the journey already wrote is left untouched.
138
+
139
+ A repo that already runs Maestro or Detox maps its failing artifacts to this record instead
140
+ of adding the helper: Maestro's failing-step screenshot, view hierarchy and logcat plus its
141
+ `commands.json` step, and Detox's `failing` artifact preset output. For Maestro, run
142
+ [maestro_failure.py](../scripts/maestro_failure.py) over the flow folder or the session folder
143
+ it sits in; it writes the record and prints the line, and [UI journeys](ui-journeys.md#evidence)
144
+ has the evidence layout. A record is written even when the source is the framework's artifact
145
+ set, and the printed line still has the four fields.
146
+
97
147
  ## Bound work and distinguish progress from waiting
98
148
 
99
149
  Give commands and child waits explicit deadlines appropriate to their purpose. Reserve
@@ -7,12 +7,14 @@ whether anything is persisted.
7
7
  ## Usage
8
8
 
9
9
  ```sh
10
- node {baseDir}/scripts/init.mjs [--repo-root <dir>] [--write]
10
+ node {baseDir}/scripts/init.mjs [--repo-root <dir>] [--install-scripts [<dir>]] [--write]
11
11
  ```
12
12
 
13
13
  - Preview is the default: it prints the planned action per file and writes nothing.
14
14
  - `--write` applies the plan. `--repo-root` selects the project; without it the nearest Git root
15
15
  from the current directory is used, falling back to the current directory when there is none.
16
+ - `--install-scripts` additionally copies the generic script layer into the repo; see
17
+ [Skill scripts](#skill-scripts-reference-or-copy) for the target and the rules.
16
18
  - `--help` prints the usage line.
17
19
  - An unknown argument, an unresolvable root, a malformed marker, an unsafe target or a file that
18
20
  ends inside an unclosed fenced code block or HTML comment exits non-zero with a message and no
@@ -29,6 +31,7 @@ to a newly created Android section; report unresolved commands instead of guessi
29
31
  | --- | --- |
30
32
  | `CONTRIBUTING.md` | The `## Android development` section, wrapped in `<!-- conventions:android:start -->` / `<!-- conventions:android:end -->`, with `references/android-conventions.md` as its body. |
31
33
  | `AGENTS.md` | An `Android task entry` skill-loading block between `conventions:android-task-entry` markers, plus a pointer to the selected `CONTRIBUTING.md` when missing. Check these independently: a contributor-guide link alone is not a skill loader. |
34
+ | `<dir>/device-run.sh`, `<dir>/failure_capture.py` | Only with `--install-scripts`: the generic layer copied with a version header. |
32
35
 
33
36
  Each file is created only when it is absent. The helper never edits `CLAUDE.md`, never writes into
34
37
  a parent project, a home directory or global agent configuration, and never reaches outside the
@@ -107,6 +110,31 @@ Existing guidance wins over the template:
107
110
  and a new pointer is written relative to the real location of `AGENTS.md`. A link to some other
108
111
  file, such as a nested package guide, does not count as this project's loader.
109
112
 
113
+ ## Skill scripts: reference or copy
114
+
115
+ `device-run.sh` and `failure_capture.py` are referenced from the installed skill by
116
+ default: nothing is copied, and the run reads them from the skill directory. Copy them
117
+ into the repo only on an explicit request, with `--install-scripts` (default target
118
+ `scripts/android/`) or `--install-scripts <dir>` for another relative directory inside
119
+ the project.
120
+
121
+ A copy carries one header line naming the skill release and the shipped body's hash, for
122
+ example `# android-development skill 0.47.0 sha256:<64 hex> device-run.sh`. The header
123
+ follows a shebang or a module docstring, so neither stops being first; the rest of the file
124
+ is the shipped script byte for byte. That makes the three outcomes unambiguous:
125
+
126
+ - Missing file: `create`. An unmodified copy of the current skill: `unchanged`, so a rerun
127
+ writes nothing. An unmodified copy of an older skill release: `update` re-stamps the
128
+ header and rewrites nothing a caller added.
129
+ - Any other content, including a copy without the header, is `drift`. It is reported in
130
+ preview and with `--write`, the file is preserved, and the run still succeeds. Refresh it
131
+ by hand; initialization never replaces a local edit.
132
+ - Unsafe targets are refused before anything is written: an absolute path, a `..` segment,
133
+ or a directory whose nearest existing ancestor is a file or resolves outside the root.
134
+
135
+ This is the contract's preservation rule, not an update path: a new skill release never
136
+ rewrites project files, and drift is reported rather than resolved.
137
+
110
138
  ## What it does not do
111
139
 
112
140
  Initialization is not installation or delivery. It does not install dependencies, SDK components
@@ -114,7 +142,8 @@ or devices, provision an emulator, generate CI, open issues, push or release. It
114
142
  discover commands automatically: the initializer follows the command map above and reports
115
143
  what it could not resolve. Adding verified repository-specific bindings to the new section is
116
144
  part of that explicit initialization; a
117
- conflicting existing rule is reported rather than overwritten.
145
+ conflicting existing rule is reported rather than overwritten. Copying the generic scripts
146
+ does not make the repo's existing harness or CI use them.
118
147
 
119
148
  ## Scope and overrides
120
149
 
@@ -211,7 +211,12 @@ Pass each command as separate words, not as one quoted string. The script:
211
211
  uses the separate `JOURNEY_SECONDS` deadline. Leave time for cleanup after that deadline.
212
212
  5. Exports `$DB_URL_VAR`, `$DB_NAME_VAR` (`$DB_NAME_PREFIX` plus a unique suffix) and
213
213
  `ANDROID_EMULATOR_SERIAL`, then runs the journey.
214
- 6. Drops the database, stops the emulator PID (`TERM`, then `KILL` after 10 seconds),
214
+ 6. On a failing journey that left no record, captures the screenshot, UI dump and logcat
215
+ tail into `LOG_DIR` while the device is still up, writes `failure.json` with `step: null`
216
+ and prints one `NATIVE_FAILURE {...}` line. A record the journey's own helper wrote is
217
+ kept; capture problems are listed in its `cleanupErrors`. See the
218
+ [failure record contract](diagnosis.md#record-the-failed-device-case).
219
+ 7. Drops the database, stops the emulator PID (`TERM`, then `KILL` after 10 seconds),
215
220
  removes the container by its CID, and prints `original_stage_exit=<n> cleanup_exit=<n>`.
216
221
  It exits with the journey's status if the journey failed, else the cleanup status.
217
222
 
@@ -220,7 +225,8 @@ group; a wrapper that deliberately creates detached services must stop those ser
220
225
  through its own owner. Send TERM to the launcher and allow its cleanup to finish rather
221
226
  than killing it immediately. The generic runner inherits the caller's ADB endpoint and
222
227
  does not stop that server: use a verified private endpoint on a shared host and close it
223
- through the caller's owner. Capture the command's exit status and final summary line.
228
+ through the caller's owner. Capture the command's exit status, the final summary line and,
229
+ when the journey failed, the `NATIVE_FAILURE` line and its `failure.json`.
224
230
 
225
231
  Expect every selected scenario's `PASS` marker, screenshots, and backend and Metro logs
226
232
  when the selected variant uses those services. Preserve the failed checkpoint's evidence
@@ -0,0 +1,58 @@
1
+ # UI journey tool: Maestro
2
+
3
+ Maestro writes a single-device UI flow as YAML and records per-step failure evidence. The
4
+ skill references it; it does not bundle or install it, and adding it is the repo's decision.
5
+
6
+ ## When to use it
7
+
8
+ | Situation | Recommendation |
9
+ | --- | --- |
10
+ | Repo has no UI journey tool | Offer Maestro for new single-device journeys. It replaces hand-written adb/UiAutomator launch, poll, capture and assertion code. |
11
+ | Repo has a working harness | Keep the harness. Add Maestro flows for new journeys alongside it; do not migrate existing cases. |
12
+ | Coordinated multi-device case | Keep the custom harness. Maestro sharding runs independent flows, and a shard cannot depend on another. |
13
+
14
+ ## Device ownership
15
+
16
+ Run against the emulator the session owns and name it, so Maestro never starts or picks its
17
+ own device:
18
+
19
+ ```bash
20
+ maestro --device "$ANDROID_SERIAL" test --test-output-dir "$LOG_DIR" ./maestro-flows
21
+ ```
22
+
23
+ Each session installs and starts Maestro's driver packages (`dev.mobile.maestro`,
24
+ `dev.mobile.maestro.test`) through `am instrument`. That cost lands on the session's first
25
+ flow, not on every step, so run every flow of the case in one session instead of one session
26
+ per flow.
27
+
28
+ ## Evidence
29
+
30
+ `--test-output-dir` receives one session folder holding one self-contained folder per flow:
31
+ `manifest.json`, `commands.json` (one entry per executed step), the failing step's
32
+ `screenshots/` image and `screen-hierarchy/` JSON, and `logs/device-logcat.txt`. Convert that
33
+ into the [device failure record](diagnosis.md#record-the-failed-device-case) instead of
34
+ adding the journey helper:
35
+
36
+ ```bash
37
+ python3 <skill-base-dir>/scripts/maestro_failure.py --maestro-dir "$LOG_DIR/2026-07-31_173617" \
38
+ --evidence-dir "$LOG_DIR" --case "$DEVICE_CASE"
39
+ ```
40
+
41
+ It takes the session folder or one flow folder, writes `failure.json` into `--evidence-dir`
42
+ (the flow folder by default) and prints the one `NATIVE_FAILURE` line for the first failed
43
+ step. A passing flow writes nothing, and an existing record is never replaced. The artifact
44
+ fields name the Maestro files (`screenshots/…`, `screen-hierarchy/…`, `logs/…`) rather than
45
+ copying them into the adb names, so the failing frame and its hierarchy stay paired.
46
+
47
+ ## Agents
48
+
49
+ `maestro mcp` gives an agent `list_devices`, `inspect_screen`, `take_screenshot` and `run`.
50
+ Explore the screen and draft a flow with it; the committed YAML flow is the proof, not the
51
+ MCP session that found the selectors.
52
+
53
+ ## Limits
54
+
55
+ - Coordinated multi-device cases, exact data or payment proofs, and process-death checks stay
56
+ on custom harness code.
57
+ - Authentication codes still come from the harness's disposable database, passed into a flow
58
+ with `maestro test -e` or resolved by its `runScript`; do not add a second fixture path.
@@ -17,6 +17,13 @@
17
17
  # POSTGRES_IMAGE (postgres:17) DB_URL_VAR (SHARD_ADMIN_URL) DB_NAME_VAR (SHARD_DB_NAME)
18
18
  # LOG_DIR (.cache/logs) BOUNDED_SECONDS (30) BOOT_ATTEMPTS (60) KILL_GRACE_SECONDS (10)
19
19
  # JOURNEY_SECONDS (0 = no deadline; caller chooses a positive whole-journey limit)
20
+ # DEVICE_CASE (unset) LOGCAT_LINES (400)
21
+ #
22
+ # A journey that fails and left no record of its own gets one before teardown:
23
+ # $LOG_DIR/screenshot.png, $LOG_DIR/ui-dump.xml, $LOG_DIR/logcat-tail.txt and a
24
+ # partial $LOG_DIR/failure.json with step null, plus one `NATIVE_FAILURE {json}`
25
+ # line. A record the journey's own helper wrote is kept, and capture problems go
26
+ # to that record's cleanupErrors instead of replacing the original failure.
20
27
  #
21
28
  # ADB_SERVER_SOCKET / ANDROID_ADB_SERVER_PORT are inherited unchanged. The caller
22
29
  # selects a private ADB endpoint before entry; this script never stops that server.
@@ -55,6 +62,7 @@ BOUNDED_SECONDS="${BOUNDED_SECONDS:-30}"
55
62
  BOOT_ATTEMPTS="${BOOT_ATTEMPTS:-60}"
56
63
  KILL_GRACE_SECONDS="${KILL_GRACE_SECONDS:-10}"
57
64
  JOURNEY_SECONDS="${JOURNEY_SECONDS:-0}"
65
+ LOGCAT_LINES="${LOGCAT_LINES:-400}"
58
66
 
59
67
  # Reject invalid limits before creating any owned resources.
60
68
  python3 -c 'import math, sys
@@ -62,8 +70,9 @@ for value in sys.argv[1:4]:
62
70
  number = float(value)
63
71
  if not math.isfinite(number) or number < 0: sys.exit("limits must be finite and non-negative")
64
72
  if float(sys.argv[1]) == 0: sys.exit("BOUNDED_SECONDS must be positive")
65
- if not sys.argv[4].isdigit() or int(sys.argv[4]) < 1: sys.exit("BOOT_ATTEMPTS must be a positive integer")' \
66
- "$BOUNDED_SECONDS" "$KILL_GRACE_SECONDS" "$JOURNEY_SECONDS" "$BOOT_ATTEMPTS" || exit 2
73
+ if not sys.argv[4].isdigit() or int(sys.argv[4]) < 1: sys.exit("BOOT_ATTEMPTS must be a positive integer")
74
+ if not sys.argv[5].isdigit() or int(sys.argv[5]) < 1: sys.exit("LOGCAT_LINES must be a positive integer")' \
75
+ "$BOUNDED_SECONDS" "$KILL_GRACE_SECONDS" "$JOURNEY_SECONDS" "$BOOT_ATTEMPTS" "$LOGCAT_LINES" || exit 2
67
76
  for variable in "$DB_URL_VAR" "$DB_NAME_VAR"; do
68
77
  if ! [[ "$variable" =~ ^[a-zA-Z_][a-zA-Z_0-9]*$ ]]; then
69
78
  echo "invalid database environment variable: $variable" >&2
@@ -88,6 +97,8 @@ db_ready=""
88
97
  container_started=""
89
98
  launch_in_progress=0
90
99
  pending_signal=0
100
+ journey_started=0
101
+ cleanup_errors=()
91
102
  handle_signal() {
92
103
  pending_signal="$1"
93
104
  # Do not abandon a spawned supervisor before its PID has been recorded.
@@ -174,8 +185,86 @@ managed() {
174
185
 
175
186
  bounded() { managed "$BOUNDED_SECONDS" "$@"; }
176
187
 
188
+ capture_artifact() {
189
+ # capture_artifact <file-name> <adb args...>; an empty capture is not evidence.
190
+ local name="$1" file="$LOG_DIR/$1"
191
+ shift
192
+ if bounded adb -s "$ANDROID_EMULATOR_SERIAL" "$@" > "$file" 2>/dev/null && test -s "$file"; then
193
+ return 0
194
+ fi
195
+ cleanup_errors+=("$name: adb -s $ANDROID_EMULATOR_SERIAL $* produced no evidence")
196
+ rm -f "$file"
197
+ }
198
+
199
+ capture_ui_dump() {
200
+ local remote=/sdcard/android-failure.xml file="$LOG_DIR/ui-dump.xml"
201
+ if bounded adb -s "$ANDROID_EMULATOR_SERIAL" shell uiautomator dump "$remote" >/dev/null 2>&1 \
202
+ && bounded adb -s "$ANDROID_EMULATOR_SERIAL" exec-out cat "$remote" > "$file" 2>/dev/null \
203
+ && test -s "$file"; then
204
+ return 0
205
+ fi
206
+ cleanup_errors+=("ui-dump.xml: adb -s $ANDROID_EMULATOR_SERIAL shell uiautomator dump failed")
207
+ rm -f "$file"
208
+ }
209
+
210
+ capture_logcat() {
211
+ # An empty tail is still a capture; a failed one is not.
212
+ local file="$LOG_DIR/logcat-tail.txt"
213
+ if bounded adb -s "$ANDROID_EMULATOR_SERIAL" logcat -d -t "$LOGCAT_LINES" > "$file" 2>/dev/null; then
214
+ return 0
215
+ fi
216
+ cleanup_errors+=("logcat-tail.txt: adb -s $ANDROID_EMULATOR_SERIAL logcat -d -t $LOGCAT_LINES failed")
217
+ rm -f "$file"
218
+ }
219
+
220
+ write_failure_record() {
221
+ local error
222
+ case "$1" in
223
+ 124) error="journey exceeded ${JOURNEY_SECONDS}s (status 124): ${journey_cmd[*]}" ;;
224
+ *) error="journey exited with status $1: ${journey_cmd[*]}" ;;
225
+ esac
226
+ # One writer for the record and its line, so their shapes cannot drift.
227
+ python3 - "$LOG_DIR" "${DEVICE_CASE:-}" "$error" \
228
+ ${cleanup_errors[@]+"${cleanup_errors[@]}"} >&3 <<'PY' || {
229
+ import json, os, sys
230
+ evidence, case, error = os.path.abspath(sys.argv[1]), sys.argv[2] or None, sys.argv[3]
231
+ record = {"case": case, "step": None, "error": error}
232
+ for key, name in (("screenshot", "screenshot.png"), ("uiDump", "ui-dump.xml"), ("logcatTail", "logcat-tail.txt")):
233
+ path = os.path.join(evidence, name)
234
+ record[key] = name if os.path.isfile(path) and os.path.getsize(path) else None
235
+ record["evidenceDir"] = evidence
236
+ record["cleanupErrors"] = sys.argv[4:]
237
+ path = os.path.join(evidence, "failure.json")
238
+ temporary = path + ".tmp"
239
+ with open(temporary, "w", encoding="utf-8") as stream:
240
+ stream.write(json.dumps(record, indent=2) + "\n")
241
+ os.replace(temporary, path)
242
+ print("NATIVE_FAILURE " + json.dumps(
243
+ {key: record[key] for key in ("case", "step", "error", "evidenceDir")}, separators=(",", ":")))
244
+ PY
245
+ echo "failure record not written; keep the evidence in $LOG_DIR" >&3
246
+ return 0
247
+ }
248
+ }
249
+
250
+ capture_failure() {
251
+ # Called before teardown: the emulator and app are still in the failing state.
252
+ local status="$1" cancelled="$2"
253
+ test "$status" -ne 0 || return 0
254
+ test "$journey_started" -eq 1 || return 0
255
+ # A requested stop is not a failed case.
256
+ test "$cancelled" -eq 0 || return 0
257
+ # The journey's own helper already recorded the failing step; keep that record.
258
+ test ! -e "$LOG_DIR/failure.json" || return 0
259
+ capture_artifact screenshot.png exec-out screencap -p
260
+ capture_ui_dump
261
+ capture_logcat
262
+ write_failure_record "$status"
263
+ }
264
+
177
265
  cleanup() {
178
266
  original_status=$?
267
+ cancelled_signal="$pending_signal"
179
268
  trap - EXIT
180
269
  trap '' INT TERM
181
270
  pending_signal=0
@@ -187,6 +276,7 @@ cleanup() {
187
276
  wait "$active_pid" || :
188
277
  active_pid=""
189
278
  fi
279
+ capture_failure "$original_status" "$cancelled_signal"
190
280
  if test -n "$db_ready"; then
191
281
  bounded "${drop_cmd[@]}" || cleanup_status=1
192
282
  fi
@@ -279,4 +369,5 @@ export "$DB_URL_VAR=postgresql://postgres:postgres@127.0.0.1:$db_port/postgres"
279
369
  export "$DB_NAME_VAR=${DB_NAME_PREFIX}$(date +%s)_$$"
280
370
  db_ready=1
281
371
 
372
+ journey_started=1
282
373
  managed "$JOURNEY_SECONDS" "${journey_cmd[@]}"