@1aboveio/skills 1.0.14 → 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 (105) hide show
  1. package/.codex-plugin/plugin.json +1 -1
  2. package/.codex-plugin/resources.json +164 -160
  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 -2
  6. package/skills/app-dev/android-development/references/diagnosis.md +55 -0
  7. package/skills/app-dev/android-development/references/feature-work.md +15 -0
  8. package/skills/app-dev/android-development/references/init.md +57 -6
  9. package/skills/app-dev/android-development/references/setup.md +8 -2
  10. package/skills/app-dev/android-development/references/ui-journeys.md +58 -0
  11. package/skills/app-dev/android-development/scripts/device-run.sh +93 -2
  12. package/skills/app-dev/android-development/scripts/failure_capture.py +331 -0
  13. package/skills/app-dev/android-development/scripts/init.mjs +234 -17
  14. package/skills/app-dev/android-development/scripts/maestro_failure.py +401 -0
  15. package/skills/app-dev/android-development/scripts/test_failure_capture.py +378 -0
  16. package/skills/app-dev/android-development/scripts/test_maestro_failure.py +462 -0
  17. package/skills/app-dev/app-debug/SKILL.md +1 -1
  18. package/skills/app-dev/better-auth/SKILL.md +1 -1
  19. package/skills/app-dev/data-catalog/SKILL.md +53 -16
  20. package/skills/app-dev/data-catalog/scripts/persist-conventions-core.mjs +231 -21
  21. package/skills/app-dev/data-catalog/scripts/persist-conventions.mjs +12 -1
  22. package/skills/app-dev/data-quality/SKILL.md +1 -1
  23. package/skills/app-dev/ios-foundation/SKILL.md +1 -1
  24. package/skills/app-dev/monorepo/SKILL.md +1 -1
  25. package/skills/app-dev/nextjs-fullstack/SKILL.md +1 -1
  26. package/skills/app-dev/prisma-setup/SKILL.md +1 -1
  27. package/skills/app-dev/pyspark/SKILL.md +1 -1
  28. package/skills/app-dev/python-backend/SKILL.md +1 -1
  29. package/skills/app-dev/shadcn/SKILL.md +1 -1
  30. package/skills/app-dev/zod-v4/SKILL.md +1 -1
  31. package/skills/cicd/cloud-build/SKILL.md +1 -1
  32. package/skills/cicd/cloud-debug/SKILL.md +1 -1
  33. package/skills/cicd/cloud-deploy/SKILL.md +1 -1
  34. package/skills/cicd/docker/SKILL.md +1 -1
  35. package/skills/cicd/github-actions/SKILL.md +1 -1
  36. package/skills/cicd/github-flow/SKILL.md +10 -1
  37. package/skills/cicd/github-flow/scripts/persist-conventions-core.mjs +231 -21
  38. package/skills/cicd/github-flow/scripts/persist-conventions.mjs +18 -1
  39. package/skills/cicd/google-cloud/SKILL.md +1 -1
  40. package/skills/cicd/mergify/SKILL.md +1 -1
  41. package/skills/cicd/podman/SKILL.md +1 -1
  42. package/skills/content/article-review/SKILL.md +1 -1
  43. package/skills/content/article-to-image/SKILL.md +1 -1
  44. package/skills/content/flowchart-svg/SKILL.md +1 -1
  45. package/skills/content/image-gen/SKILL.md +1 -1
  46. package/skills/content/mermaid/SKILL.md +1 -1
  47. package/skills/content/wechat-article-writer/SKILL.md +1 -1
  48. package/skills/content/wechat-publisher/SKILL.md +1 -1
  49. package/skills/content/wechat-search/SKILL.md +1 -1
  50. package/skills/content/xai-search/SKILL.md +1 -1
  51. package/skills/core/distill-writing/SKILL.md +7 -1
  52. package/skills/core/doc-to-md/SKILL.md +1 -1
  53. package/skills/core/editorial-review/SKILL.md +1 -1
  54. package/skills/core/house-style/SKILL.md +1 -1
  55. package/skills/core/house-style/references/persistence.md +8 -3
  56. package/skills/core/house-style/scripts/init-guidance.mjs +1 -1
  57. package/skills/core/plain-language/SKILL.md +1 -1
  58. package/skills/core/polish-cn/SKILL.md +1 -1
  59. package/skills/core/reg-parser/SKILL.md +1 -1
  60. package/skills/core/terminology/SKILL.md +1 -1
  61. package/skills/core/terminology/references/init.md +11 -0
  62. package/skills/core/write-doc/SKILL.md +1 -1
  63. package/skills/core/write-report/SKILL.md +1 -1
  64. package/skills/engineering/better-review/SKILL.md +1 -1
  65. package/skills/engineering/e2e-test/SKILL.md +1 -1
  66. package/skills/engineering/engineering/SKILL.md +19 -4
  67. package/skills/engineering/engineering/references/init-scope.md +5 -2
  68. package/skills/engineering/engineering/references/scope-management.md +4 -2
  69. package/skills/engineering/engineering/scripts/init.mjs +34 -9
  70. package/skills/engineering/engineering/scripts/persist-conventions-core.mjs +231 -21
  71. package/skills/engineering/engineering/scripts/persist-conventions.mjs +48 -21
  72. package/skills/engineering/engineering-runtime/SKILL.md +1 -1
  73. package/skills/engineering/engineering-runtime/coherence/workflow.json +13 -13
  74. package/skills/engineering/ensure-coverage/SKILL.md +1 -1
  75. package/skills/engineering/golden-path/SKILL.md +1 -1
  76. package/skills/engineering/harness-runtime/SKILL.md +4 -3
  77. package/skills/engineering/harness-runtime/agent-catalog.mjs +928 -0
  78. package/skills/engineering/harness-runtime/contracts.md +2 -0
  79. package/skills/engineering/harness-runtime/references/model-catalog.md +7 -1
  80. package/skills/engineering/resolve-issues/SKILL.md +9 -6
  81. package/skills/engineering/resolve-issues/references/delivery-progress.md +2 -0
  82. package/skills/engineering/resolve-issues/references/expire.md +6 -3
  83. package/skills/engineering/resolve-issues/references/implementation.md +6 -1
  84. package/skills/engineering/resolve-issues/references/preflight.md +26 -17
  85. package/skills/engineering/resolve-issues/references/review.md +39 -4
  86. package/skills/engineering/resolve-issues/references/shared-modules.md +88 -20
  87. package/skills/engineering/resolve-issues/references/task-plan.md +35 -1
  88. package/skills/engineering/resolve-issues/scripts/preflight-models.mjs +382 -592
  89. package/skills/engineering/resolve-release/SKILL.md +1 -1
  90. package/skills/engineering/rush-issues/SKILL.md +4 -4
  91. package/skills/engineering/rush-release/SKILL.md +1 -1
  92. package/skills/engineering/test-quality/SKILL.md +3 -1
  93. package/skills/engineering/test-quality/scripts/persist-conventions-core.mjs +231 -21
  94. package/skills/engineering/test-quality/scripts/persist-conventions.mjs +13 -1
  95. package/skills/risk-compliance/compliance-audit/SKILL.md +1 -1
  96. package/skills/risk-compliance/contract-compare/SKILL.md +1 -1
  97. package/skills/risk-compliance/contract-review/SKILL.md +1 -1
  98. package/skills/risk-compliance/doc-authenticity/SKILL.md +1 -1
  99. package/skills/risk-compliance/fraud-detection/SKILL.md +1 -1
  100. package/skills/risk-compliance/payment-analysis/SKILL.md +1 -1
  101. package/skills/risk-compliance/portfolio-payment-analysis/SKILL.md +1 -1
  102. package/skills/risk-compliance/similar-domain/SKILL.md +1 -1
  103. package/skills/risk-compliance/tld-detection/SKILL.md +1 -1
  104. package/skills/engineering/resolve-issues/scripts/discover-models.mjs +0 -9
  105. 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.46.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.46.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.46.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
@@ -12,23 +12,49 @@ and CI fails a pull request when the pages and the scripts disagree.
12
12
 
13
13
  ## First use in a repo
14
14
 
15
- In a git repo with no `data-catalog.json`, set the repo up **before** the task you
16
- were asked to do, whatever it is: an audit, a new column, or a question. Setting up
17
- puts the engine, the CI check, and the conventions in the repo itself, so agents
18
- and people without this skill follow them from then on.
15
+ No `data-catalog.json` means the repo is not set up. That is a fact about the repo,
16
+ not permission to write to it, and the `## Data catalog task entry` rule that loaded
17
+ this skill is not one either: an audit, a review, a column question or a table-script
18
+ change is the task, and it is done from the sources the repo already has.
19
+
20
+ - **Read what exists.** `docs/catalog/<table>.md` when the repo has pages, else the
21
+ script's `CATALOG_TABLE`/`CATALOG_FIELDS`, else its `-- @table`/`-- @field` block,
22
+ else the SQL or DataFrame code. A definition in the script answers the question;
23
+ never hand-write the missing page.
24
+ - **Audit read-only.** `python3 <skill-base-dir>/scripts/data_catalog.py audit
25
+ 'jobs/**/*.py' 'sql/**/*.sql'` needs no config and writes nothing; it exits 2 only
26
+ with neither a config nor globs.
27
+ - **A check you cannot run stays pending.** `generate`, `generate --check` and `drift`
28
+ exit 2 without `data-catalog.json`. Report them pending — "the catalog is not set up
29
+ here" — not "no findings", "green" or "CI passes", and do not run `init --write` to
30
+ make them runnable: running them is the task, making them runnable is setup.
31
+ - **Offer setup once.** One short line: `init --write` with the `--scripts` globs and
32
+ `--languages` you would use, plus `persist-conventions.mjs --write` for the
33
+ conventions section, the `AGENTS.md` pointer and the task-entry rule. Say nothing was
34
+ written, keep working, once per session per repo. Not an approval checkpoint.
35
+ - **Setting up is an explicit request.** "init the data catalog", "generate the data
36
+ dictionary", "字段字典", "add catalog CI", "set up the catalog here" — that
37
+ authorizes these writes in that repo only. An automatic review, the loader rule, a
38
+ handoff from another skill, or having read this section do not.
39
+
40
+ With that request, setup is:
19
41
 
20
42
  1. `python3 <skill-base-dir>/scripts/data_catalog.py audit '<globs>'` to find the
21
43
  table scripts; choose `--scripts` globs that cover them and nothing else.
22
44
  2. Pick `--languages` from the repo's existing docs (Chinese docs: `zh,en`;
23
45
  otherwise `en`). Run `init` with them and `--write` ([Init](#init)).
24
46
  3. `node <skill-base-dir>/scripts/persist-conventions.mjs --repo-root <repo> --write`
25
- writes the `## Data catalog conventions` section of `CONTRIBUTING.md` and points
26
- `AGENTS.md` at it.
47
+ writes the `## Data catalog conventions` section of `CONTRIBUTING.md`, points
48
+ `AGENTS.md` at it, and adds the `## Data catalog task entry` rule that loads this
49
+ skill for column or table-script work. It is the only writer of those files and
50
+ writes only with its own `--write`; an ordinary catalog task never runs it.
27
51
  4. Then do the task. In the reply, list what setup added, and say the Data catalog
28
52
  check runs on PRs but is not required.
29
53
 
30
- Skip setup outside a git repo, or when the user says not to change the repo.
31
- Never document tables during setup unless the task asks for it.
54
+ Outside a git repo there is nothing to set up: say so. Never document tables during
55
+ setup unless the task asks for it. Repeating setup with the same inputs leaves the
56
+ configured files unchanged; report a missing input or a conflicting rule instead of
57
+ claiming success.
32
58
 
33
59
  ## Find your task
34
60
 
@@ -36,7 +62,7 @@ Never document tables during setup unless the task asks for it.
36
62
  |---|---|
37
63
  | Writing or changing a table script | [While coding](#while-coding), then [references/field-definitions.md](references/field-definitions.md) |
38
64
  | "What does column X mean?" | Read `docs/catalog/<table>.md`; with no page, read the script's `CATALOG_FIELDS`, then its SQL |
39
- | The repo has no `data-catalog.json` | [First use in a repo](#first-use-in-a-repo) |
65
+ | The repo has no `data-catalog.json` | Do the task read-only, then offer setup once ([First use in a repo](#first-use-in-a-repo)) |
40
66
  | Find scripts with missing or weak definitions | [Audit](#audit) |
41
67
  | Document an existing table | [Backfill one table](#backfill-one-table) |
42
68
  | CI says the catalog is stale | `python3 <engine> generate`, commit the pages |
@@ -46,8 +72,8 @@ Never document tables during setup unless the task asks for it.
46
72
 
47
73
  `<engine>` is `scripts/data-catalog/data_catalog.py` in the repo, or
48
74
  `<skill-base-dir>/scripts/data_catalog.py` before the repo has a copy. When
49
- `python3 <engine> version` differs from the skill's copy, refresh with
50
- `init --write`.
75
+ `python3 <engine> version` differs from the skill's copy, the refresh is
76
+ `init --write`, which needs a setup request ([First use in a repo](#first-use-in-a-repo)).
51
77
 
52
78
  ## The files
53
79
 
@@ -61,7 +87,7 @@ Never document tables during setup unless the task asks for it.
61
87
  | `scripts/data-catalog/data_catalog.py` | engine copied from this skill | never; refresh with init |
62
88
  | `.github/workflows/data-catalog.yml` | generated CI check | never; rerun init |
63
89
  | `CONTRIBUTING.md` section between `conventions:data-catalog` markers | the conventions, for agents without this skill | never; rerun `persist-conventions.mjs --write`; the rest is yours |
64
- | `AGENTS.md` `## Contributing` pointer | points at `CONTRIBUTING.md` | written once by `persist-conventions.mjs` |
90
+ | `AGENTS.md` `## Contributing` pointer and `## Data catalog task entry` rule | reach `CONTRIBUTING.md` and this skill; the rule routes work and authorizes no write | written once by `persist-conventions.mjs`; re-run `--write` to restore a missing one |
65
91
 
66
92
  ## Rules
67
93
 
@@ -118,6 +144,11 @@ Never document tables during setup unless the task asks for it.
118
144
  PR why it still holds.
119
145
  6. Commit the script and the regenerated pages together.
120
146
 
147
+ A repo with no `data-catalog.json`: steps 4 and 5 cannot run, because both exit 2
148
+ without a config. Write the definitions the change needs, say those checks are
149
+ pending, and offer setup once ([First use in a repo](#first-use-in-a-repo)). Do not
150
+ run `init --write` to make them run.
151
+
121
152
  Example: changing `WHERE status = 'POSTED'` to `status IN ('POSTED', 'SETTLED')`
122
153
  passes CI, and `drift` reports "filters, joins or CTEs changed; no definition
123
154
  changed" until the affected sources say "posted or settled".
@@ -141,6 +172,10 @@ to `data-catalog.json` take effect on the next `init --write`. After a skill upd
141
172
  rerun both `init --write` and `persist-conventions.mjs --write` so the repo's copies
142
173
  match.
143
174
 
175
+ Without `--write`, `init` only prints its plan, so a dry run is always safe. With
176
+ `--write` it is repo setup and needs the user's setup request
177
+ ([First use in a repo](#first-use-in-a-repo)).
178
+
144
179
  The scripts import `from catalog_fields import Field, Table` at run time, so the
145
180
  module must ship with them like any sibling module (for example `--py-files` on
146
181
  `spark-submit`). Check that before merging.
@@ -188,7 +223,8 @@ script is clean when its columns were `unknown`.
188
223
  3. Add `CATALOG_TABLE` and `CATALOG_FIELDS` in contract order. Keep the literal
189
224
  contract while you write.
190
225
  4. Run `generate` until it passes, then derive the contract and run the repo's
191
- schema tests (rule 5).
226
+ schema tests (rule 5). In a repo with no `data-catalog.json` it exits 2: report it
227
+ pending and offer setup once, and do not run `init --write` for it.
192
228
  5. Rerun the audit for that script and fix its writing findings (`repeated-text`,
193
229
  `code-in-description`, `long-definition`, `thin-definition`,
194
230
  `relative-definition`), or leave one only when the text really belongs in that row.
@@ -201,8 +237,9 @@ empty text.
201
237
 
202
238
  ## Done when
203
239
 
204
- - `python3 <engine> generate --check` exits 0.
240
+ - `python3 <engine> generate --check` exits 0, or you said it is pending because the
241
+ repo has no `data-catalog.json`.
205
242
  - `python3 <engine> drift` exits 0, or you said in the PR why each listed definition
206
- still holds.
243
+ still holds, or you reported it pending for the same reason.
207
244
  - `python3 <engine> audit` has no findings for the tables you touched, or you
208
245
  reported the ones left and why.