@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
@@ -0,0 +1,462 @@
1
+ """Focused tests for maestro_failure.py: fixture Maestro output, no device, no Maestro.
2
+
3
+ The fixtures are the artifact layout Maestro's docs describe for a Flow folder
4
+ (manifest.json, commands.json, screenshots/, screen-hierarchy/, logs/), with the
5
+ commands.json entry shape taken from Maestro's source (TestOutputWriter and
6
+ CommandDebugMetadata) because the docs name the fields but show no example.
7
+
8
+ Run directly (`python3 test_maestro_failure.py`) or through
9
+ `scripts/maestro-failure.test.mjs`, which is what CI discovers.
10
+ """
11
+
12
+ import contextlib
13
+ import io
14
+ import json
15
+ import os
16
+ import sys
17
+ import tempfile
18
+ import unittest
19
+
20
+ sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
21
+
22
+ from failure_capture import NATIVE_FAILURE_PREFIX, RECORD_KEYS, RECORD_NAME, read_record # noqa: E402
23
+ from maestro_failure import ( # noqa: E402
24
+ MaestroOutputError,
25
+ convert,
26
+ describe_command,
27
+ error_text,
28
+ main,
29
+ )
30
+
31
+ SESSION = "2026-07-31_173617"
32
+ FLOW = "checkout_flow"
33
+ CASE = "checkout smoke"
34
+
35
+ LAUNCHED = {
36
+ "command": {"launchAppCommand": {"appId": "com.example.app", "optional": False}},
37
+ "metadata": {
38
+ "status": "COMPLETED",
39
+ "timestamp": 1700000000000,
40
+ "duration": 1200,
41
+ "sequenceNumber": 0,
42
+ "depth": 0,
43
+ },
44
+ }
45
+
46
+ TAPPED = {
47
+ "command": {
48
+ "tapOnElement": {
49
+ "selector": {"text": "Pay", "optional": False},
50
+ "label": "Tap Pay",
51
+ "optional": False,
52
+ }
53
+ },
54
+ "metadata": {
55
+ "status": "COMPLETED",
56
+ "timestamp": 1700000001300,
57
+ "duration": 300,
58
+ "sequenceNumber": 1,
59
+ "depth": 0,
60
+ },
61
+ }
62
+
63
+ FAILED_RECEIPT = {
64
+ "command": {
65
+ "assertConditionCommand": {
66
+ "condition": {"visible": {"text": "Thank you", "optional": False}},
67
+ "optional": False,
68
+ }
69
+ },
70
+ "metadata": {
71
+ "status": "FAILED",
72
+ "timestamp": 1700000001700,
73
+ "duration": 30000,
74
+ "sequenceNumber": 2,
75
+ "depth": 0,
76
+ "error": {
77
+ "message": 'Assertion is false: "Thank you" is visible',
78
+ "debugMessage": "Screen hierarchy has no match",
79
+ },
80
+ "artifacts": [
81
+ {"type": "SCREEN_HIERARCHY", "path": "screen-hierarchy/step-003-assertCondition.json"},
82
+ {"type": "SCREENSHOT", "path": "screenshots/step-003-assertCondition.png"},
83
+ ],
84
+ },
85
+ }
86
+
87
+
88
+ class MaestroOutputCase(unittest.TestCase):
89
+ def setUp(self):
90
+ self.directory = tempfile.TemporaryDirectory()
91
+ self.root = self.directory.name
92
+ self.output = io.StringIO()
93
+
94
+ def tearDown(self):
95
+ self.directory.cleanup()
96
+
97
+ def write(self, relative, payload):
98
+ path = os.path.join(self.root, relative)
99
+ os.makedirs(os.path.dirname(path), exist_ok=True)
100
+ if isinstance(payload, bytes):
101
+ with open(path, "wb") as stream:
102
+ stream.write(payload)
103
+ else:
104
+ with open(path, "w", encoding="utf-8") as stream:
105
+ stream.write(payload)
106
+ return path
107
+
108
+ def write_json(self, relative, payload):
109
+ return self.write(relative, json.dumps(payload, indent=2))
110
+
111
+ def flow(self, steps, name=FLOW, session=SESSION, artifacts=None, manifest=True):
112
+ """A Flow folder with `steps` in commands.json and the optional artifact files."""
113
+ base = os.path.join(session, name) if session else name
114
+ self.write_json(os.path.join(base, "commands.json"), steps)
115
+ if manifest:
116
+ self.write_json(
117
+ os.path.join(base, "manifest.json"),
118
+ {
119
+ "$schema": "https://storage.googleapis.com/maestro-schemas/artifact-manifest/v1.schema.json",
120
+ "entries": [
121
+ {"kind": "COMMAND_METADATA", "format": "JSON", "relativePath": "commands.json"}
122
+ ],
123
+ },
124
+ )
125
+ for relative, payload in (artifacts or {}).items():
126
+ self.write(os.path.join(base, relative), payload)
127
+ return os.path.join(self.root, base)
128
+
129
+ def failing_flow(self, name=FLOW, session=SESSION):
130
+ """The worked example: a launch and tap that passed, then a failed assertion."""
131
+ return self.flow(
132
+ [LAUNCHED, TAPPED, FAILED_RECEIPT],
133
+ name=name,
134
+ session=session,
135
+ artifacts={
136
+ "screenshots/step-003-assertCondition.png": b"\x89PNG\r\n\x1a\n",
137
+ "screen-hierarchy/step-003-assertCondition.json": '{"attributes": {"text": "Pay now"}}',
138
+ "logs/device-logcat.txt": "07-31 E ActivityManager: no match\n",
139
+ },
140
+ )
141
+
142
+ def run_convert(self, maestro_dir, **options):
143
+ options.setdefault("stream", self.output)
144
+ return convert(maestro_dir, **options)
145
+
146
+ def cli(self, *argv):
147
+ """Run main() and keep its stderr out of the test output."""
148
+ with contextlib.redirect_stderr(io.StringIO()):
149
+ return main(list(argv))
150
+
151
+ def record(self, evidence_dir):
152
+ path = os.path.join(evidence_dir, RECORD_NAME)
153
+ self.assertTrue(os.path.isfile(path), path)
154
+ with open(path, encoding="utf-8") as stream:
155
+ return json.load(stream)
156
+
157
+ def lines(self):
158
+ return [line for line in self.output.getvalue().splitlines() if line]
159
+
160
+
161
+ class FailingFlowTest(MaestroOutputCase):
162
+ def test_the_failing_step_becomes_the_record_and_the_line(self):
163
+ flow_dir = self.failing_flow()
164
+ failure = self.run_convert(flow_dir)
165
+
166
+ self.assertIsNotNone(failure)
167
+ record = self.record(flow_dir)
168
+ self.assertEqual(list(record), list(RECORD_KEYS))
169
+ self.assertEqual(record["case"], FLOW)
170
+ self.assertEqual(record["step"], 'Assert condition "Thank you"')
171
+ self.assertEqual(
172
+ record["error"],
173
+ 'Assertion is false: "Thank you" is visible: Screen hierarchy has no match',
174
+ )
175
+ self.assertEqual(record["screenshot"], "screenshots/step-003-assertCondition.png")
176
+ self.assertEqual(record["uiDump"], "screen-hierarchy/step-003-assertCondition.json")
177
+ self.assertEqual(record["logcatTail"], "logs/device-logcat.txt")
178
+ self.assertEqual(record["evidenceDir"], flow_dir)
179
+ self.assertEqual(record["cleanupErrors"], [])
180
+ for name in (record["screenshot"], record["uiDump"], record["logcatTail"]):
181
+ self.assertTrue(os.path.isfile(os.path.join(flow_dir, name)), name)
182
+
183
+ lines = self.lines()
184
+ self.assertEqual(len(lines), 1)
185
+ self.assertTrue(lines[0].startswith(NATIVE_FAILURE_PREFIX))
186
+ self.assertEqual(
187
+ json.loads(lines[0][len(NATIVE_FAILURE_PREFIX) :]),
188
+ {
189
+ "case": FLOW,
190
+ "step": 'Assert condition "Thank you"',
191
+ "error": record["error"],
192
+ "evidenceDir": flow_dir,
193
+ },
194
+ )
195
+
196
+ def test_an_explicit_case_name_is_used(self):
197
+ flow_dir = self.failing_flow()
198
+ self.run_convert(flow_dir, case=CASE)
199
+ self.assertEqual(self.record(flow_dir)["case"], CASE)
200
+
201
+ def test_a_labelled_step_uses_the_label(self):
202
+ labelled = json.loads(json.dumps(FAILED_RECEIPT))
203
+ labelled["command"] = {
204
+ "tapOnElement": {
205
+ "selector": {"text": "Pay", "optional": False},
206
+ "label": "Tap Pay",
207
+ "optional": False,
208
+ }
209
+ }
210
+ flow_dir = self.flow([TAPPED, labelled])
211
+ self.run_convert(flow_dir)
212
+ self.assertEqual(self.record(flow_dir)["step"], "Tap Pay")
213
+
214
+ def test_the_failing_step_wins_over_a_later_one(self):
215
+ later = json.loads(json.dumps(FAILED_RECEIPT))
216
+ later["metadata"]["timestamp"] = 1700000009000
217
+ later["metadata"]["sequenceNumber"] = 3
218
+ later["metadata"]["error"] = {"message": "second failure"}
219
+ later["metadata"]["artifacts"] = [
220
+ {"type": "SCREENSHOT", "path": "screenshots/step-004-tapOnElement.png"}
221
+ ]
222
+ flow_dir = self.flow(
223
+ [TAPPED, FAILED_RECEIPT, later],
224
+ artifacts={"screenshots/step-003-assertCondition.png": b"png"},
225
+ )
226
+ self.run_convert(flow_dir)
227
+
228
+ record = self.record(flow_dir)
229
+ self.assertEqual(record["step"], 'Assert condition "Thank you"')
230
+ self.assertIn("Assertion is false", record["error"])
231
+ self.assertEqual(record["screenshot"], "screenshots/step-003-assertCondition.png")
232
+
233
+
234
+ class PassingFlowTest(MaestroOutputCase):
235
+ def test_a_passing_flow_writes_nothing(self):
236
+ flow_dir = self.flow(
237
+ [LAUNCHED, TAPPED],
238
+ artifacts={"logs/device-logcat.txt": "07-31 I ActivityManager: clean\n"},
239
+ )
240
+ self.assertIsNone(self.run_convert(flow_dir))
241
+ self.assertFalse(os.path.exists(os.path.join(flow_dir, RECORD_NAME)))
242
+ self.assertEqual(self.output.getvalue(), "")
243
+
244
+ def test_a_flow_with_no_steps_writes_nothing(self):
245
+ flow_dir = self.flow([])
246
+ self.assertIsNone(self.run_convert(flow_dir))
247
+ self.assertEqual(self.output.getvalue(), "")
248
+
249
+ def test_a_folder_without_commands_json_is_not_converted(self):
250
+ self.write(os.path.join(SESSION, FLOW, "manifest.json"), "{}")
251
+ with self.assertRaises(MaestroOutputError):
252
+ self.run_convert(os.path.join(self.root, SESSION))
253
+ self.assertEqual(self.cli("--maestro-dir", os.path.join(self.root, SESSION)), 2)
254
+
255
+ def test_a_missing_folder_is_a_usage_error(self):
256
+ self.assertEqual(self.cli("--maestro-dir", os.path.join(self.root, "absent")), 2)
257
+
258
+
259
+ class ExistingRecordTest(MaestroOutputCase):
260
+ def test_an_existing_record_is_kept_and_its_line_is_printed(self):
261
+ flow_dir = self.failing_flow()
262
+ kept = {
263
+ "case": CASE,
264
+ "step": "tap Pay",
265
+ "error": "RuntimeError: earlier failure",
266
+ "screenshot": None,
267
+ "uiDump": None,
268
+ "logcatTail": None,
269
+ "evidenceDir": flow_dir,
270
+ "cleanupErrors": [],
271
+ }
272
+ path = self.write_json(os.path.join(SESSION, FLOW, RECORD_NAME), kept)
273
+
274
+ failure = self.run_convert(flow_dir)
275
+
276
+ self.assertEqual(failure.case, CASE)
277
+ self.assertEqual(self.record(flow_dir), kept)
278
+ with open(path, encoding="utf-8") as stream:
279
+ self.assertIn("earlier failure", stream.read())
280
+ self.assertEqual(self.lines(), [failure.native_failure_line()])
281
+
282
+ def test_a_record_already_in_an_override_evidence_dir_is_kept(self):
283
+ flow_dir = self.failing_flow()
284
+ evidence_dir = os.path.join(self.root, "run-evidence")
285
+ kept = {"case": CASE, "step": None, "error": "RuntimeError: earlier failure", "cleanupErrors": []}
286
+ self.write_json(os.path.join("run-evidence", RECORD_NAME), kept)
287
+
288
+ self.run_convert(flow_dir, evidence_dir=evidence_dir)
289
+
290
+ self.assertEqual(self.record(evidence_dir)["error"], "RuntimeError: earlier failure")
291
+ # The first failure owns the folder, so no Maestro artifact was copied into it.
292
+ self.assertFalse(os.path.exists(os.path.join(evidence_dir, "screenshots")))
293
+
294
+
295
+ class MissingOptionalFilesTest(MaestroOutputCase):
296
+ def test_absent_artifacts_are_null_and_noted_without_losing_the_error(self):
297
+ flow_dir = self.flow([TAPPED, FAILED_RECEIPT], manifest=False)
298
+ self.run_convert(flow_dir)
299
+
300
+ record = self.record(flow_dir)
301
+ self.assertIsNone(record["screenshot"])
302
+ self.assertIsNone(record["uiDump"])
303
+ self.assertIsNone(record["logcatTail"])
304
+ self.assertEqual(record["step"], 'Assert condition "Thank you"')
305
+ self.assertIn("Assertion is false", record["error"])
306
+ self.assertEqual(len(record["cleanupErrors"]), 3)
307
+ self.assertTrue(record["cleanupErrors"][0].startswith("screenshot: "))
308
+ self.assertTrue(record["cleanupErrors"][1].startswith("uiDump: "))
309
+ self.assertTrue(record["cleanupErrors"][2].startswith("logcatTail: "))
310
+
311
+ def test_the_error_survives_a_listed_artifact_that_is_not_on_disk(self):
312
+ broken = json.loads(json.dumps(FAILED_RECEIPT))
313
+ broken["metadata"]["artifacts"] = [
314
+ {"type": "SCREENSHOT", "path": "screenshots/step-003-assertCondition.png"}
315
+ ]
316
+ flow_dir = self.flow([broken], artifacts={"logs/device-logcat.txt": "log\n"})
317
+ self.run_convert(flow_dir)
318
+
319
+ record = self.record(flow_dir)
320
+ self.assertIsNone(record["screenshot"])
321
+ self.assertEqual(record["logcatTail"], "logs/device-logcat.txt")
322
+ self.assertIn(
323
+ "screenshots/step-003-assertCondition.png is not in the Maestro output",
324
+ " ".join(record["cleanupErrors"]),
325
+ )
326
+ self.assertIn("Assertion is false", record["error"])
327
+
328
+ def test_an_unreadable_commands_json_is_not_silently_a_pass(self):
329
+ flow_dir = self.flow([FAILED_RECEIPT])
330
+ self.write(os.path.join(SESSION, FLOW, "commands.json"), "{not json")
331
+ with self.assertRaises(MaestroOutputError):
332
+ self.run_convert(flow_dir)
333
+
334
+
335
+ class SessionFolderTest(MaestroOutputCase):
336
+ def test_the_session_folder_finds_the_failing_flow(self):
337
+ self.flow([LAUNCHED, TAPPED], name="login_flow")
338
+ failing_flow = self.failing_flow()
339
+ session_dir = os.path.join(self.root, SESSION)
340
+
341
+ failure = self.run_convert(session_dir)
342
+
343
+ self.assertEqual(failure.evidenceDir, failing_flow)
344
+ self.assertEqual(self.record(failing_flow)["case"], FLOW)
345
+
346
+ def test_the_earliest_failing_step_decides(self):
347
+ earlier = json.loads(json.dumps(FAILED_RECEIPT))
348
+ earlier["metadata"]["timestamp"] = 1700000000500
349
+ earlier["metadata"]["error"] = {"message": "earlier failure"}
350
+ earlier_dir = self.flow([LAUNCHED, earlier], name="login_flow")
351
+ later_dir = self.failing_flow(name="checkout_flow")
352
+
353
+ self.run_convert(os.path.join(self.root, SESSION))
354
+
355
+ self.assertEqual(self.record(earlier_dir)["error"], "earlier failure")
356
+ self.assertFalse(os.path.exists(os.path.join(later_dir, RECORD_NAME)))
357
+
358
+ def test_the_output_root_above_the_session_folder_works(self):
359
+ failing_flow = self.failing_flow()
360
+ self.run_convert(self.root)
361
+ self.assertTrue(os.path.isfile(os.path.join(failing_flow, RECORD_NAME)))
362
+
363
+
364
+ class EvidenceDirTest(MaestroOutputCase):
365
+ def test_an_override_evidence_dir_holds_copies_and_relative_names(self):
366
+ flow_dir = self.failing_flow()
367
+ evidence_dir = os.path.join(self.root, "run-evidence")
368
+
369
+ failure = self.run_convert(flow_dir, evidence_dir=evidence_dir)
370
+
371
+ record = self.record(evidence_dir)
372
+ self.assertEqual(record["evidenceDir"], evidence_dir)
373
+ self.assertEqual(failure.evidenceDir, evidence_dir)
374
+ self.assertEqual(record["screenshot"], "screenshots/step-003-assertCondition.png")
375
+ self.assertEqual(record["uiDump"], "screen-hierarchy/step-003-assertCondition.json")
376
+ self.assertEqual(record["logcatTail"], "logs/device-logcat.txt")
377
+ for name in (record["screenshot"], record["uiDump"], record["logcatTail"]):
378
+ self.assertTrue(os.path.isfile(os.path.join(evidence_dir, name)), name)
379
+
380
+
381
+ class DocumentedShapeTest(MaestroOutputCase):
382
+ def test_the_documented_flat_entry_shape_is_accepted(self):
383
+ flow_dir = self.flow(
384
+ [
385
+ {"command": "launchApp", "status": "COMPLETED", "duration": 1200, "sequenceNumber": 0},
386
+ {
387
+ "command": "tapOn: Pay",
388
+ "status": "FAILED",
389
+ "duration": 300,
390
+ "sequenceNumber": 1,
391
+ "error": "Element not found: Pay",
392
+ "artifacts": ["screenshots/step-002-tapOn.png"],
393
+ },
394
+ ],
395
+ artifacts={
396
+ "screenshots/step-002-tapOn.png": b"png",
397
+ "logs/device-logcat.txt": "log\n",
398
+ },
399
+ )
400
+
401
+ self.run_convert(flow_dir)
402
+
403
+ record = self.record(flow_dir)
404
+ self.assertEqual(record["step"], "tapOn: Pay")
405
+ self.assertEqual(record["error"], "Element not found: Pay")
406
+ self.assertEqual(record["screenshot"], "screenshots/step-002-tapOn.png")
407
+ self.assertEqual(record["uiDump"], None)
408
+
409
+ def test_a_step_named_screenshot_is_found_without_an_artifacts_list(self):
410
+ unlisted = json.loads(json.dumps(FAILED_RECEIPT))
411
+ del unlisted["metadata"]["artifacts"]
412
+ unlisted["metadata"]["sequenceNumber"] = 1
413
+ flow_dir = self.flow(
414
+ [unlisted],
415
+ artifacts={
416
+ "screenshots/step-002-assertCondition.png": b"png",
417
+ "screen-hierarchy/step-002-assertCondition.json": "{}",
418
+ "logs/device-logcat.txt": "log\n",
419
+ },
420
+ )
421
+
422
+ self.run_convert(flow_dir)
423
+
424
+ record = self.record(flow_dir)
425
+ self.assertEqual(record["screenshot"], "screenshots/step-002-assertCondition.png")
426
+ self.assertEqual(record["uiDump"], "screen-hierarchy/step-002-assertCondition.json")
427
+
428
+ def test_an_ios_device_log_is_used_when_there_is_no_logcat(self):
429
+ flow_dir = self.flow(
430
+ [FAILED_RECEIPT],
431
+ artifacts={"logs/device-simulator.log": "simulator log\n"},
432
+ )
433
+ self.run_convert(flow_dir)
434
+ self.assertEqual(self.record(flow_dir)["logcatTail"], "logs/device-simulator.log")
435
+
436
+
437
+ class ReadingTest(unittest.TestCase):
438
+ def test_a_command_without_a_label_reads_as_its_type_and_target(self):
439
+ self.assertEqual(describe_command({"scrollCommand": {"optional": False}}), "Scroll")
440
+ self.assertEqual(
441
+ describe_command({"launchAppCommand": {"appId": "com.example.app"}}),
442
+ 'Launch app "com.example.app"',
443
+ )
444
+ self.assertEqual(describe_command("tapOn: Pay"), "tapOn: Pay")
445
+ self.assertIsNone(describe_command(None))
446
+
447
+ def test_error_text_keeps_the_debug_detail(self):
448
+ self.assertEqual(error_text({"error": "boom"}), "boom")
449
+ self.assertEqual(
450
+ error_text({"error": {"message": "boom", "debugMessage": "screen has no Foo"}}),
451
+ "boom: screen has no Foo",
452
+ )
453
+ self.assertEqual(error_text({"error": {"message": "boom"}}), "boom")
454
+ self.assertEqual(error_text({}), "Maestro marked the step FAILED and gave no error message")
455
+
456
+ def test_read_record_ignores_a_non_record(self):
457
+ with tempfile.TemporaryDirectory() as directory:
458
+ self.assertIsNone(read_record(directory))
459
+
460
+
461
+ if __name__ == "__main__":
462
+ unittest.main()
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: app-debug
3
3
  description: "Diagnose runtime failures in deployed Cloud Run services — 5xx/422 errors, tracebacks, Next.js SSR/hydration or Server Action failures, DB/Secret Manager connection errors, cold-start timeouts, container OOM. NOT for build or deploy failures (cloud-debug)."
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
  # App Debug
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: better-auth
3
3
  description: "Add or debug Better Auth for Next.js App Router: login, sessions, protected routes, middleware, OAuth (including Feishu/Lark), cookies, Prisma, and Auth.js migration."
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
  # Better Auth for Next.js
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: data-catalog
3
3
  description: "Column definitions for warehouse tables, written inside the Spark/PySpark or SQL script that builds each table (CATALOG_TABLE / CATALOG_FIELDS, or a `-- @field` comment block), with a generated markdown catalog in docs/catalog/ and a CI check that fails a PR whose catalog is stale. Use when writing or changing a table script that adds, renames, removes, or changes the meaning of an output column or feature; when asked what a warehouse column or feature means; to set a repo up ('init the data catalog', 'generate the data dictionary', '字段字典', 'add catalog CI'); or to audit existing SQL or Spark scripts for undocumented columns. NOT for implementing or tuning the Spark job itself (pyspark), DAG wiring (airflow), or general workflow YAML (github-actions)."
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
  # Data catalog
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: data-quality
3
3
  description: "Check a published warehouse table's data against its data-catalog definitions with read-only SELECTs: columns never filled or stuck at one value, counts below zero, ratios outside 0–1, declared values that never appear and undeclared values that do. Then classify each finding as a wrong definition, wrong code, or wrong data. Use when asked whether a table's data is right, to profile or sanity-check a dwd/ads table, to find dead or constant columns, to check a column's values against its documented list, after a pipeline change ships, or when a data-catalog definition and the data might disagree. NOT for validating a Spark change before it ships (pyspark canary and reconciliation), writing definitions (data-catalog), or monitoring dashboards."
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
  # Data quality
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: ios-foundation
3
3
  description: "Set up and prove an Expo / React Native native iOS workspace from an assigned or fresh isolated workspace to a manual Login smoke on an owned iOS Simulator: isolated workspace and private caches, Xcode/runtime/CocoaPods preflight, frozen install with `pod install --deployment`, platform-scoped prebuild, an ad-hoc-signed Debug simulator build, a private simulator by exact UDID, Metro on a free port, a disposable PostgreSQL backend with owned cleanup, and the evidence record (setup proof only; feature work reports the smoke result). Use for 'set up iOS', 'build the iOS app', 'run the app on the iOS simulator', 'add iOS to the React Native app', 'prove the iOS build', or when a pod install, xcodebuild, simulator boot, Metro connection or Keychain session fails. NOT for Android (android-development), signing, TestFlight or App Store release, or general workflow YAML (github-actions)."
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
  # iOS foundation
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: monorepo
3
3
  description: Turborepo architecture for shared packages, pnpm workspaces, DAL boundaries, and pipelines.
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
  # Turborepo Monorepo Architecture
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: nextjs-fullstack
3
3
  description: "Build or debug Next.js App Router features with React 19 — pages/layouts/route handlers, Server/Client Components, Server Actions, forms, middleware, DAL, strict TypeScript, Zod — plus our conventions: nuqs URL state, SWR cache, Skeleton/Progress + Sonner feedback, drag-and-drop upload, the portal app shell, list pages as filter/sort/paginate tables. shadcn component mechanics: see the shadcn skill."
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
  # Next.js 16 Fullstack Development
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: prisma-setup
3
3
  description: Use when adding or debugging Prisma with PostgreSQL, Next.js, or Turborepo, including schema.prisma, prisma.config.ts, client generation, migrations, seeds, relations, adapters, and DAL/database-layer setup. Triggers on phrases like "Prisma schema", "prisma migrate", "schema.prisma", "seed the database", "PrismaClient error", "relation issue", or "database layer".
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
  # Prisma v7 Setup Guide
@@ -12,7 +12,7 @@ description: >
12
12
  Connect, spark-submit, Hudi commits, backfills, validate-only canaries,
13
13
  cluster reconciliation, or Spark event-log profiling — including warehouse
14
14
  job timeouts, empty Hudi writes, or EMR/Dataproc performance regressions.
15
- release-version: 0.47.0
15
+ release-version: 0.48.0
16
16
  ---
17
17
  **Updates:** Once/session run `node <skill-base-dir>/release-check.mjs`.
18
18
  # PySpark
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: python-backend
3
3
  description: "Python backend architecture for FastAPI, Tortoise ORM, Pydantic Settings, loguru, and uv. Also: uv.lock installs in CI/Docker via a proxy or mirror (`uv sync --frozen` bypasses it), slow uv installs on runners, relocking."
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
  # Python Backend Conventions
@@ -2,7 +2,7 @@
2
2
  name: shadcn
3
3
  description: "Add or debug shadcn/ui in a React/Next.js project: components.json, shadcn init/add, presets, registries, Tailwind styling, base/radix/aria composition, component fixes, and list-index pages — where the default is the DiceUI data table with filter/sort/pagination in the URL via nuqs, not a bare shadcn Table."
4
4
  allowed-tools: Bash(npx shadcn@latest *), Bash(pnpm dlx shadcn@latest *), Bash(bunx --bun shadcn@latest *)
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
  # shadcn/ui
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: zod-v4
3
3
  description: Zod v4 schema conventions and v3 migration for errors, validators, objects, enums, records, and async parsing.
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
  # Zod v3 to v4 Migration
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: cloud-build
3
3
  description: "Build or debug Cloud Build pipelines for Cloud Run: cloudbuild.yaml, triggers, Artifact Registry, builds, migrations, deploys, secrets, VPC, notifications. Use cloud-deploy for staged promotion. NOT for GitHub Actions PR checks (github-actions)."
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
  # Cloud Build Direct Deploy to Cloud Run
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: cloud-debug
3
3
  description: "Debug Cloud Build or Deploy failures: builds, deployments, health checks, and pipeline errors. Inputs: service, build ID, region. NOT for GitHub Actions runs (github-actions) or runtime errors in a running service (app-debug)."
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
  # Cloud Build & Cloud Deploy Debug Skill
@@ -5,7 +5,7 @@ description: >
5
5
  CI/CD pipelines with staged promotion, verification gates, or GitFlow branching.
6
6
  Also use when debugging Cloud Deploy pipeline failures. NOT for simple single-container
7
7
  deploys — use cloud-build instead.
8
- release-version: 0.47.0
8
+ release-version: 0.48.0
9
9
  ---
10
10
  **Updates:** Once/session run `node <skill-base-dir>/release-check.mjs`.
11
11
  # Cloud Deploy + Skaffold + Kustomize for Cloud Run
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: docker
3
3
  description: Docker/Podman container architecture and build patterns. Read when designing or reviewing containerization, Dockerfile structure, multi-stage builds, PyPI mirror support, or docker-compose setup for Python/uv projects, and when a Dockerfile's dependency install misses the layer cache on every build (build args such as a commit SHA or version declared too early) or an image build is slow in CI.
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
  # Docker Build Patterns
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: github-actions
3
3
  description: "Author, review, audit, profile, optimize, or debug GitHub Actions workflows (.github/workflows/*.yml): run-history audits, job and step timing, resource bottlenecks, before/after comparisons, conditional CI by changed path, the one required gate check, PR and merge-queue lanes, sharding, workers, caching, self-hosted runners, timeouts, concurrency, permissions and action pinning, post-deploy verification, npm publishing. Use for 'set up CI', 'CI is slow', 'profile CI', 'optimize our pipeline', 'audit CI for redundant or wasted jobs', 'why is this check stuck expected or never started', 'harden this workflow', or 'browser not found on the runner'. NOT for pushing, opening, or merging a PR, or writing CI conventions into CONTRIBUTING.md (github-flow), queue rules or a check unmet inside Mergify (mergify), Playwright config and specs (e2e-test), cloudbuild.yaml or Cloud Run deploys (cloud-build), or which gates a repo must prove (ensure-coverage)."
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
  # GitHub Actions
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: github-flow
3
3
  description: "Deliver a change after the commit: push, open a PR, wait for checks, and merge to main through the repo's flow (direct push, pull request, or merge queue). Use whenever you commit or finish a change, when CONTRIBUTING.md has a Delivery section, and for asks like 'commit this', 'push it', 'open a PR', 'ship it', 'land it', 'get it merged'. Also: declare a repo's delivery flow, or write the CI conventions and Delivery section into CONTRIBUTING.md on request. NOT for writing workflow YAML (github-actions), Mergify config or a stuck queued PR (mergify), cutting a release (resolve-release), or PRs that resolve-issues or rush-issues deliver."
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
  # GitHub Flow
@@ -5,7 +5,7 @@ description: >
5
5
  project management. Use when configuring GCP resources, managing service
6
6
  accounts, setting up IAP access, configuring VPC networks, managing secrets,
7
7
  or debugging cloud infrastructure.
8
- release-version: 0.47.0
8
+ release-version: 0.48.0
9
9
  ---
10
10
  **Updates:** Once/session run `node <skill-base-dir>/release-check.mjs`.
11
11
  # Google Cloud Platform (GCP) Skill