@1aboveio/skills 1.0.15 → 1.0.17

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 (97) hide show
  1. package/.codex-plugin/plugin.json +1 -1
  2. package/.codex-plugin/resources.json +156 -152
  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 +30 -1
  6. package/skills/app-dev/android-development/references/diagnosis.md +50 -0
  7. package/skills/app-dev/android-development/references/feature-work.md +3 -2
  8. package/skills/app-dev/android-development/references/gotchas.md +13 -0
  9. package/skills/app-dev/android-development/references/init.md +31 -2
  10. package/skills/app-dev/android-development/references/setup.md +16 -7
  11. package/skills/app-dev/android-development/references/ui-journeys.md +130 -0
  12. package/skills/app-dev/android-development/scripts/device-run.sh +93 -2
  13. package/skills/app-dev/android-development/scripts/failure_capture.py +331 -0
  14. package/skills/app-dev/android-development/scripts/init.mjs +129 -9
  15. package/skills/app-dev/android-development/scripts/maestro_failure.py +401 -0
  16. package/skills/app-dev/android-development/scripts/test_failure_capture.py +378 -0
  17. package/skills/app-dev/android-development/scripts/test_maestro_failure.py +462 -0
  18. package/skills/app-dev/app-debug/SKILL.md +1 -1
  19. package/skills/app-dev/better-auth/SKILL.md +1 -1
  20. package/skills/app-dev/data-catalog/SKILL.md +1 -1
  21. package/skills/app-dev/data-quality/SKILL.md +1 -1
  22. package/skills/app-dev/ios-foundation/SKILL.md +1 -1
  23. package/skills/app-dev/monorepo/SKILL.md +1 -1
  24. package/skills/app-dev/nextjs-fullstack/SKILL.md +1 -1
  25. package/skills/app-dev/prisma-setup/SKILL.md +1 -1
  26. package/skills/app-dev/pyspark/SKILL.md +1 -1
  27. package/skills/app-dev/python-backend/SKILL.md +1 -1
  28. package/skills/app-dev/shadcn/SKILL.md +1 -1
  29. package/skills/app-dev/zod-v4/SKILL.md +1 -1
  30. package/skills/cicd/cloud-build/SKILL.md +1 -1
  31. package/skills/cicd/cloud-debug/SKILL.md +1 -1
  32. package/skills/cicd/cloud-deploy/SKILL.md +1 -1
  33. package/skills/cicd/docker/SKILL.md +1 -1
  34. package/skills/cicd/github-actions/SKILL.md +1 -1
  35. package/skills/cicd/github-flow/SKILL.md +1 -1
  36. package/skills/cicd/google-cloud/SKILL.md +1 -1
  37. package/skills/cicd/mergify/SKILL.md +1 -1
  38. package/skills/cicd/podman/SKILL.md +1 -1
  39. package/skills/content/article-review/SKILL.md +1 -1
  40. package/skills/content/article-to-image/SKILL.md +1 -1
  41. package/skills/content/flowchart-svg/SKILL.md +1 -1
  42. package/skills/content/image-gen/SKILL.md +1 -1
  43. package/skills/content/mermaid/SKILL.md +1 -1
  44. package/skills/content/wechat-article-writer/SKILL.md +1 -1
  45. package/skills/content/wechat-publisher/SKILL.md +1 -1
  46. package/skills/content/wechat-search/SKILL.md +1 -1
  47. package/skills/content/xai-search/SKILL.md +1 -1
  48. package/skills/core/distill-writing/SKILL.md +1 -1
  49. package/skills/core/doc-to-md/SKILL.md +1 -1
  50. package/skills/core/editorial-review/SKILL.md +1 -1
  51. package/skills/core/house-style/SKILL.md +1 -1
  52. package/skills/core/plain-language/SKILL.md +1 -1
  53. package/skills/core/polish-cn/SKILL.md +1 -1
  54. package/skills/core/reg-parser/SKILL.md +1 -1
  55. package/skills/core/terminology/SKILL.md +1 -1
  56. package/skills/core/write-doc/SKILL.md +1 -1
  57. package/skills/core/write-report/SKILL.md +1 -1
  58. package/skills/engineering/better-review/SKILL.md +1 -1
  59. package/skills/engineering/e2e-test/SKILL.md +1 -1
  60. package/skills/engineering/engineering/SKILL.md +1 -1
  61. package/skills/engineering/engineering-runtime/SKILL.md +1 -1
  62. package/skills/engineering/engineering-runtime/coherence/workflow.json +13 -13
  63. package/skills/engineering/ensure-coverage/SKILL.md +1 -1
  64. package/skills/engineering/golden-path/SKILL.md +1 -1
  65. package/skills/engineering/harness-runtime/SKILL.md +4 -3
  66. package/skills/engineering/harness-runtime/agent-catalog.mjs +929 -0
  67. package/skills/engineering/harness-runtime/contracts.md +2 -0
  68. package/skills/engineering/harness-runtime/references/model-catalog.md +7 -1
  69. package/skills/engineering/resolve-issues/SKILL.md +18 -13
  70. package/skills/engineering/resolve-issues/references/delivery-progress.md +2 -0
  71. package/skills/engineering/resolve-issues/references/expire.md +6 -3
  72. package/skills/engineering/resolve-issues/references/exploration.md +60 -18
  73. package/skills/engineering/resolve-issues/references/implementation.md +7 -2
  74. package/skills/engineering/resolve-issues/references/preflight.md +65 -31
  75. package/skills/engineering/resolve-issues/references/review.md +22 -9
  76. package/skills/engineering/resolve-issues/references/shared-modules.md +142 -20
  77. package/skills/engineering/resolve-issues/references/task-plan.md +62 -8
  78. package/skills/engineering/resolve-issues/scripts/findings-reuse.mjs +1 -1
  79. package/skills/engineering/resolve-issues/scripts/next-actions.mjs +7 -7
  80. package/skills/engineering/resolve-issues/scripts/plan-sync.mjs +1 -1
  81. package/skills/engineering/resolve-issues/scripts/preflight-models.mjs +382 -592
  82. package/skills/engineering/resolve-issues/scripts/size-check.mjs +6 -5
  83. package/skills/engineering/resolve-release/SKILL.md +1 -1
  84. package/skills/engineering/rush-issues/SKILL.md +4 -4
  85. package/skills/engineering/rush-release/SKILL.md +1 -1
  86. package/skills/engineering/test-quality/SKILL.md +1 -1
  87. package/skills/risk-compliance/compliance-audit/SKILL.md +1 -1
  88. package/skills/risk-compliance/contract-compare/SKILL.md +1 -1
  89. package/skills/risk-compliance/contract-review/SKILL.md +1 -1
  90. package/skills/risk-compliance/doc-authenticity/SKILL.md +1 -1
  91. package/skills/risk-compliance/fraud-detection/SKILL.md +1 -1
  92. package/skills/risk-compliance/payment-analysis/SKILL.md +1 -1
  93. package/skills/risk-compliance/portfolio-payment-analysis/SKILL.md +1 -1
  94. package/skills/risk-compliance/similar-domain/SKILL.md +1 -1
  95. package/skills/risk-compliance/tld-detection/SKILL.md +1 -1
  96. package/skills/engineering/resolve-issues/scripts/discover-models.mjs +0 -9
  97. 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.17",
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": "4ae04aa79316f8a7cc9df45582d19e332321316012dac6843be1eb21d2b1a385",
32
+ "releaseIdentity": "d4a51368dcc414b3136d3b6f51a6aeeeea77429a101eb65f36f7878e00032494"
33
33
  },
34
34
  "pi": {
35
35
  "skills": [
@@ -260,7 +260,7 @@
260
260
  ]
261
261
  }
262
262
  ],
263
- "digest": "6ad0dafe4a400162776b1bf6d7c9e5ab31bf37a86d18b035fcadc4c11d06f799"
263
+ "digest": "ae9b2ca5b33484b68e892b41976bcc054562385ef7dc7db94bf86ab42e369d0d"
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.1
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.1
5
5
  ---
6
6
  **Updates:** Once/session run `node <skill-base-dir>/release-check.mjs`.
7
7
  # Android development
@@ -43,6 +43,33 @@ 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. Offer Maestro for new
64
+ single-device journeys when the repo has no journey tool. Keep an existing sufficient
65
+ harness; adding Maestro needs a concrete benefit and repo authorization, not just an
66
+ installed CLI. Maestro uses the same work loop and validation ladder. Run it on the
67
+ session's own emulator (`maestro --device <serial>`) and convert its output with
68
+ [maestro_failure.py](scripts/maestro_failure.py), which writes the record above. Read
69
+ [UI journeys](references/ui-journeys.md) before selecting or running Maestro flows, for
70
+ starting state, permissions, device ownership, execution evidence and the cases that
71
+ stay custom.
72
+
46
73
  ## Validation ladder
47
74
 
48
75
  Read [feature work](references/feature-work.md) for selection, ownership and handoff.
@@ -123,5 +150,7 @@ higher levels still pending rather than claiming acceptance.
123
150
  | 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
151
  | 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
152
  | Feature/fix validation and handoff | [feature-work.md](references/feature-work.md) |
153
+ | Device failure record and evidence capture | [diagnosis.md](references/diagnosis.md#record-the-failed-device-case) |
154
+ | UI journey tool (Maestro) and its evidence converter | [UI journeys](references/ui-journeys.md) |
126
155
  | Failure reproduction and correction | [diagnosis.md](references/diagnosis.md), then the relevant [gotcha](references/gotchas.md) |
127
156
  | 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
@@ -173,8 +173,9 @@ at L2. Required CI and L3 obligations remain assigned, not silently waived or ma
173
173
  rule for unreproduced or intermittent failures; report uncertainty until evidence
174
174
  supports the cause.
175
175
  - Report source SHA, commands, results and remaining gaps at review handoff. For device
176
- checks, include the selected scenarios, `PASS` markers and cleanup exit. For a failure,
177
- name the earliest failing checkpoint and link retained evidence; preserve failed attempts.
176
+ checks, include the selected scenarios, per-scenario results (`PASS` markers or the
177
+ journey tool's report) and cleanup exit. For a failure, name the earliest failing
178
+ checkpoint and link retained evidence; preserve failed attempts.
178
179
  Separate build duration, device-check duration and total worker time when discussing cost.
179
180
  Reuse existing logs to distinguish cold/warm builds, build count, ABI, test packaging,
180
181
  runtime startup and edit-to-feedback time. Do not infer a task's cost from total job time,
@@ -20,8 +20,10 @@ the product.
20
20
  | Backend tool, CI only | a Backend-backed step gets a 5xx in CI only; the log names a missing program (`Browser was not found at the configured executablePath`) |
21
21
  | adb transport, mid-journey | `connection terminated: read failed` in the adb trace after boot, then lost `adb reverse`: usually guest RAM |
22
22
  | UiAutomator server | `RemoteDisconnected` while the adb transport stays up: the on-device server was relaunched |
23
+ | Maestro driver | driver readiness fails while a setup uiautomator2 process is still alive: see [device ownership](ui-journeys.md#device-ownership) |
23
24
  | Journey harness | a wait right after a navigation times out while the destination is on screen |
24
25
  | Journey harness, short screen | a wait for a label times out only on a short or landscape screen, which shows the content above it |
26
+ | Journey adapter | an offscreen target stays hidden while scrolls move away from it |
25
27
  | Product assertion | a screen or state mismatch with a screenshot |
26
28
 
27
29
  Then match the signal against [gotchas.md](gotchas.md). If nothing
@@ -143,6 +145,17 @@ matches, [confirm the suspected cause](#confirm-a-suspected-cause) before changi
143
145
 
144
146
  ## Journey harness
145
147
 
148
+ ### A scroll adapter moves away from the target
149
+
150
+ - **Signal:** a custom comparison adapter times out on an offscreen target even though it
151
+ sends scroll gestures; a fake UI that exposes all targets can still pass.
152
+ - **Cause:** Maestro's requested scroll direction describes the content to reach, not the
153
+ finger movement. Mapping `DOWN` to a downward finger swipe reverses the intended scroll.
154
+ - **Fix:** for `DOWN` toward later content, swipe the finger up; for `UP` toward earlier
155
+ content, swipe the finger down. In the adapter test, keep the target offscreen until the
156
+ correct gesture moves it into view. Wrong-direction gestures must not reveal it merely
157
+ because a gesture count was reached. Check both directions where the adapter supports them.
158
+
146
159
  ### A navigation check passes on the previous screen
147
160
 
148
161
  - **Signal:** intermittently, a wait right after the next tap times out even though the
@@ -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,11 +225,14 @@ 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
- Expect every selected scenario's `PASS` marker, screenshots, and backend and Metro logs
226
- when the selected variant uses those services. Preserve the failed checkpoint's evidence
227
- before cleanup. A release build with embedded assets does not require Metro by default.
231
+ Expect every selected scenario's result (`PASS` marker or the journey tool's report),
232
+ required screenshots, and backend and Metro logs when the selected variant uses those
233
+ services. For Maestro's result and screenshot rules, read [UI journeys](ui-journeys.md#evidence).
234
+ Preserve the failed checkpoint's evidence before cleanup. A release build with embedded
235
+ assets does not require Metro by default.
228
236
 
229
237
  ## Owned development session
230
238
 
@@ -379,8 +387,9 @@ For setup proof only. Record:
379
387
  - Each stage's exit status and duration.
380
388
  - The APK's SHA-256 (`shasum -a 256` or `sha256sum`) and its per-ABI `lib/` entries
381
389
  (`unzip -l`), checked on the APK itself rather than inferred from Gradle flags.
382
- - The journey's `PASS` markers and the cleanup results: database dropped, emulator exited,
383
- container removed, no listeners left on the backend or Metro ports.
390
+ - The journey's per-scenario results (`PASS` markers or the journey tool's report) and
391
+ cleanup results: database dropped, emulator exited, container removed, no listeners
392
+ left on the backend or Metro ports.
384
393
  - Earlier failed or incomplete attempts. A later pass does not erase them.
385
394
 
386
395
  State what the run does not prove: tablet, accessibility (TalkBack), physical devices,
@@ -0,0 +1,130 @@
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 | Reuse it when sufficient. Add Maestro only for a concrete benefit with repo authorization; an installed CLI alone is not a reason to add or migrate journeys. |
12
+ | Coordinated multi-device case | Keep the custom harness. Maestro sharding runs independent flows, and a shard cannot depend on another. |
13
+
14
+ Maestro changes the UI journey driver, not the [work loop or validation ladder](../SKILL.md#work-loop).
15
+ Use focused flows at the applicable L0/L1 check, candidate-APK journeys at L2, and assigned
16
+ acceptance journeys at L3. Source tests and required backend/native checks keep their roles.
17
+
18
+ ## Starting state and permissions
19
+
20
+ Declare each flow's intended starting app state and permissions in its YAML or owned
21
+ fixture setup, and check included subflows and hooks for resets:
22
+
23
+ - For a warm-runtime check, keep the running app or use `launchApp: {stopApp: false}`
24
+ to foreground it. Plain `launchApp` stops and relaunches the app by default, so it
25
+ cannot preserve the process whose state the check needs.
26
+ - For restart, upgrade or fresh-install checks, use the checkpoint's required setup.
27
+ `clearState: true` and `clearState` clear Android app data; they do not install the
28
+ candidate APK or prove a fresh installation or upgrade. Clear only authorized disposable
29
+ state. If a reset removes debug endpoint preferences, restore them through the existing
30
+ owner and verify the app's actual code source before continuing.
31
+ - Set permissions for permission-sensitive journeys. `launchApp` allows all permissions
32
+ by default; use the required denied or unset state to exercise denial or prompts.
33
+ An auto-granted run does not prove the permission-request journey.
34
+
35
+ ## Device ownership
36
+
37
+ Run against the emulator the session owns and name it with `maestro --device <serial>`.
38
+ Use the status handling under [Evidence](#evidence) inside the existing journey owner.
39
+
40
+ Each session installs and starts Maestro's driver packages (`dev.mobile.maestro`,
41
+ `dev.mobile.maestro.test`) through `am instrument`. That cost lands on the session's first
42
+ flow, not on every step, so run every flow of the case in one session instead of one session
43
+ per flow.
44
+
45
+ Keep mutating checks exclusive on that device, including existing harness and Maestro
46
+ runs. Reuse the existing runtime owner; Maestro is not a second emulator, backend or
47
+ cleanup owner.
48
+
49
+ If setup started a `setupUiAutomator` uiautomator2 process, have its owner verify ownership,
50
+ stop it and wait for it to exit before starting Maestro; a live setup driver can block
51
+ Maestro driver readiness. Restart that setup driver only after Maestro has fully exited,
52
+ if it is still needed. Never stop a peer's process; coordinate with its owner instead.
53
+
54
+ When comparing a custom journey adapter with Maestro, check scroll direction and prove
55
+ offscreen targets with the [scroll-direction gotcha](gotchas.md#a-scroll-adapter-moves-away-from-the-target).
56
+
57
+ ## Evidence
58
+
59
+ Record the selected flows, executed assertions, run exit status, app/source identity and
60
+ cleanup outcome. Accept the harness's `PASS` markers or Maestro's per-flow results/JUnit
61
+ report as scenario results; a zero exit with no selected flows is not a pass. For JUnit,
62
+ use `--format junit --output <owned-report-path>`; the report is separate from
63
+ `--test-output-dir`. Capture successful checkpoints with `takeScreenshot` when the
64
+ assignment requires visual evidence; normal passing flows have no automatic screenshots.
65
+
66
+ `--test-output-dir` receives one session folder holding one self-contained folder per flow:
67
+ `manifest.json`, `commands.json` (one entry per executed step), the failing step's
68
+ `screenshots/` image and `screen-hierarchy/` JSON, and `logs/device-logcat.txt`. Convert that
69
+ into the [device failure record](diagnosis.md#record-the-failed-device-case) instead of
70
+ adding the journey helper:
71
+
72
+ Give each case/attempt a new owned `LOG_DIR`, shared with its existing launcher so its
73
+ partial-record fallback looks in the same directory. Retain earlier attempts separately;
74
+ reusing a runtime does not mean reusing a failure-record destination. Confirm the pinned
75
+ Maestro supports these flags and output layout before using the example.
76
+
77
+ Run this body as the existing owner's journey command. Guard both commands so `set -e`
78
+ cannot skip conversion or cleanup after a failure. The owner retains its bounded cleanup
79
+ after the journey exits; this is not a new launcher or an acceptance checker.
80
+
81
+ ```bash
82
+ maestro_status=0
83
+ maestro --device "$ANDROID_SERIAL" test --test-output-dir "$LOG_DIR/maestro" \
84
+ --format junit --output "$LOG_DIR/report.xml" ./maestro-flows || maestro_status=$?
85
+ conversion_status=0
86
+ python3 <skill-base-dir>/scripts/maestro_failure.py --maestro-dir "$LOG_DIR/maestro" \
87
+ --evidence-dir "$LOG_DIR" --case "$DEVICE_CASE" || conversion_status=$?
88
+ printf 'maestro_exit=%s conversion_exit=%s\n' "$maestro_status" "$conversion_status"
89
+ if [ "$maestro_status" -ne 0 ]; then
90
+ exit "$maestro_status"
91
+ fi
92
+ exit "$conversion_status"
93
+ ```
94
+
95
+ Convert this invocation's finalized output before teardown. If startup or driver failure
96
+ left no usable failed-step output, retain raw CLI logs and the original status and use
97
+ the existing owner's partial-record fallback before cleanup. Conversion errors are
98
+ evidence-processing failures, not replacements for the original test failure.
99
+
100
+ It takes the session folder or one flow folder, writes `failure.json` into `--evidence-dir`
101
+ (the flow folder by default) and prints the one `NATIVE_FAILURE` line for the first failed
102
+ step. A passing flow writes nothing, and an existing record is never replaced. The artifact
103
+ fields name the Maestro files (`screenshots/…`, `screen-hierarchy/…`, `logs/…`) rather than
104
+ copying them into the adb names, so the failing frame and its hierarchy stay paired.
105
+
106
+ Converter exit zero means processing completed, not that tests passed: a recorded failure
107
+ also returns zero, and empty or incomplete step output may produce no record. Establish
108
+ success from the executed assertions and per-flow results above, never from the converter
109
+ or absence of `NATIVE_FAILURE`.
110
+
111
+ ## Agents
112
+
113
+ `maestro mcp` gives an agent `list_devices`, `inspect_screen`, `take_screenshot` and `run`.
114
+ Explore the screen and draft a repeatable YAML flow with it. Proof comes from executing
115
+ the flow against the identified candidate with assertions that establish the claimed
116
+ behavior and retained results. Committing YAML or finding selectors in MCP does not prove
117
+ the journey passed.
118
+
119
+ ## Limits
120
+
121
+ - Coordinated multi-device cases, exact data or payment proofs, and process-death checks stay
122
+ on custom harness code.
123
+ - Authentication codes still come from the harness's disposable database, passed into a flow
124
+ with `maestro test -e` or resolved by its `runScript`; do not add a second fixture path.
125
+
126
+ ## Sources
127
+
128
+ - [Launch state and permissions](https://docs.maestro.dev/reference/commands-available/launchapp)
129
+ - [Android state clearing](https://docs.maestro.dev/reference/commands-available/clearstate)
130
+ - [Reports and artifacts](https://docs.maestro.dev/maestro-flows/workspace-management/test-reports-and-artifacts)
@@ -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[@]}"