@1aboveio/skills 1.0.6 → 1.0.8

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 (78) hide show
  1. package/package.json +2 -2
  2. package/skills/app-dev/airflow/SKILL.md +1 -1
  3. package/skills/app-dev/android-foundation/SKILL.md +92 -34
  4. package/skills/app-dev/android-foundation/assets/android-ci-job.yml +13 -7
  5. package/skills/app-dev/android-foundation/references/ci.md +23 -13
  6. package/skills/app-dev/android-foundation/references/delivery-focus.md +29 -0
  7. package/skills/app-dev/android-foundation/references/diagnosis.md +136 -0
  8. package/skills/app-dev/android-foundation/references/gotchas.md +20 -11
  9. package/skills/app-dev/android-foundation/references/setup.md +15 -6
  10. package/skills/app-dev/android-foundation/scripts/device-run.sh +148 -42
  11. package/skills/app-dev/android-foundation/scripts/device-run.test.mjs +322 -58
  12. package/skills/app-dev/app-debug/SKILL.md +1 -1
  13. package/skills/app-dev/better-auth/SKILL.md +1 -1
  14. package/skills/app-dev/data-catalog/SKILL.md +1 -1
  15. package/skills/app-dev/data-quality/SKILL.md +1 -1
  16. package/skills/app-dev/ios-foundation/SKILL.md +1 -1
  17. package/skills/app-dev/monorepo/SKILL.md +1 -1
  18. package/skills/app-dev/nextjs-fullstack/SKILL.md +1 -1
  19. package/skills/app-dev/prisma-setup/SKILL.md +1 -1
  20. package/skills/app-dev/pyspark/SKILL.md +1 -1
  21. package/skills/app-dev/python-backend/SKILL.md +1 -1
  22. package/skills/app-dev/shadcn/SKILL.md +1 -1
  23. package/skills/app-dev/zod-v4/SKILL.md +1 -1
  24. package/skills/cicd/cloud-build/SKILL.md +1 -1
  25. package/skills/cicd/cloud-debug/SKILL.md +1 -1
  26. package/skills/cicd/cloud-deploy/SKILL.md +1 -1
  27. package/skills/cicd/docker/SKILL.md +1 -1
  28. package/skills/cicd/github-actions/SKILL.md +1 -1
  29. package/skills/cicd/github-flow/SKILL.md +1 -1
  30. package/skills/cicd/google-cloud/SKILL.md +1 -1
  31. package/skills/cicd/mergify/SKILL.md +1 -1
  32. package/skills/cicd/podman/SKILL.md +1 -1
  33. package/skills/content/article-review/SKILL.md +1 -1
  34. package/skills/content/article-to-image/SKILL.md +1 -1
  35. package/skills/content/flowchart-svg/SKILL.md +1 -1
  36. package/skills/content/image-gen/SKILL.md +1 -1
  37. package/skills/content/mermaid/SKILL.md +1 -1
  38. package/skills/content/wechat-article-writer/SKILL.md +1 -1
  39. package/skills/content/wechat-publisher/SKILL.md +1 -1
  40. package/skills/content/wechat-search/SKILL.md +1 -1
  41. package/skills/content/xai-search/SKILL.md +1 -1
  42. package/skills/core/distill-writing/SKILL.md +22 -4
  43. package/skills/core/distill-writing/references/extraction.md +19 -3
  44. package/skills/core/doc-to-md/SKILL.md +1 -1
  45. package/skills/core/editorial-review/SKILL.md +1 -1
  46. package/skills/core/house-style/SKILL.md +2 -1
  47. package/skills/core/house-style/references/persistence.md +93 -0
  48. package/skills/core/house-style/references/rules.md +15 -6
  49. package/skills/core/plain-language/SKILL.md +40 -4
  50. package/skills/core/polish-cn/SKILL.md +1 -1
  51. package/skills/core/reg-parser/SKILL.md +1 -1
  52. package/skills/core/terminology/SKILL.md +1 -1
  53. package/skills/core/write-doc/SKILL.md +14 -6
  54. package/skills/engineering/better-review/SKILL.md +1 -1
  55. package/skills/engineering/contributing/SKILL.md +1 -1
  56. package/skills/engineering/e2e-test/SKILL.md +1 -1
  57. package/skills/engineering/engineering-runtime/SKILL.md +1 -1
  58. package/skills/engineering/engineering-runtime/coherence/workflow.json +13 -13
  59. package/skills/engineering/ensure-coverage/SKILL.md +1 -1
  60. package/skills/engineering/golden-path/SKILL.md +1 -1
  61. package/skills/engineering/harness-runtime/SKILL.md +1 -1
  62. package/skills/engineering/resolve-issues/SKILL.md +11 -20
  63. package/skills/engineering/resolve-issues/references/delivery-progress.md +63 -0
  64. package/skills/engineering/resolve-issues/references/expire.md +24 -11
  65. package/skills/engineering/resolve-issues/references/implementation.md +9 -1
  66. package/skills/engineering/resolve-issues/references/task-plan.md +7 -4
  67. package/skills/engineering/resolve-release/SKILL.md +1 -1
  68. package/skills/engineering/rush-issues/SKILL.md +5 -2
  69. package/skills/engineering/rush-release/SKILL.md +1 -1
  70. package/skills/engineering/test-quality/SKILL.md +1 -1
  71. package/skills/risk-compliance/compliance-audit/SKILL.md +1 -1
  72. package/skills/risk-compliance/contract-compare/SKILL.md +1 -1
  73. package/skills/risk-compliance/contract-review/SKILL.md +1 -1
  74. package/skills/risk-compliance/doc-authenticity/SKILL.md +1 -1
  75. package/skills/risk-compliance/fraud-analysis/SKILL.md +1 -1
  76. package/skills/risk-compliance/payment-analysis/SKILL.md +1 -1
  77. package/skills/risk-compliance/similar-domain/SKILL.md +1 -1
  78. package/skills/risk-compliance/tld-detection/SKILL.md +1 -1
@@ -1,18 +1,31 @@
1
- // device-run.sh against stand-in emulator, adb and docker binaries: the cleanup
2
- // owns exactly what the run started, keeps the stage's status, and treats a
3
- // stalled command as terminal.
1
+ // Run the real shell lifecycle against fake external executables, never a device
2
+ // or container daemon. Every fixture records its own processes for test cleanup.
4
3
  import { test } from 'node:test';
5
4
  import assert from 'node:assert/strict';
6
- import { spawnSync } from 'node:child_process';
7
- import { chmodSync, mkdirSync, mkdtempSync, readFileSync, writeFileSync, existsSync } from 'node:fs';
5
+ import { spawn, spawnSync } from 'node:child_process';
6
+ import { chmodSync, mkdirSync, mkdtempSync, readFileSync, writeFileSync, existsSync, rmSync } from 'node:fs';
8
7
  import { createServer } from 'node:net';
9
8
  import { tmpdir } from 'node:os';
10
9
  import { dirname, join } from 'node:path';
11
10
  import { fileURLToPath } from 'node:url';
11
+ import { setTimeout as delay } from 'node:timers/promises';
12
12
 
13
- const SCRIPT = join(dirname(fileURLToPath(import.meta.url)), 'device-run.sh');
13
+ const SCRIPT = process.env.DEVICE_RUN_SCRIPT ?? join(dirname(fileURLToPath(import.meta.url)), 'device-run.sh');
14
14
 
15
- async function fakeHost() {
15
+ function alive(pid) {
16
+ const r = spawnSync('ps', ['-o', 'stat=', '-p', String(pid)], { encoding: 'utf8' });
17
+ return r.status === 0 && r.stdout.trim() !== '' && !r.stdout.trim().startsWith('Z');
18
+ }
19
+
20
+ async function until(check, message, milliseconds = 5000) {
21
+ const end = Date.now() + milliseconds;
22
+ while (!check()) {
23
+ assert.ok(Date.now() < end, message);
24
+ await delay(20);
25
+ }
26
+ }
27
+
28
+ async function fakeHost(t) {
16
29
  const root = mkdtempSync(join(tmpdir(), 'device-run-'));
17
30
  const bin = join(root, 'bin');
18
31
  const sdk = join(root, 'sdk');
@@ -20,31 +33,72 @@ async function fakeHost() {
20
33
  mkdirSync(join(sdk, 'emulator'), { recursive: true });
21
34
  const log = join(root, 'calls.log');
22
35
  writeFileSync(log, '');
36
+ const pids = () => existsSync(join(root, 'pids'))
37
+ ? readFileSync(join(root, 'pids'), 'utf8').trim().split('\n').filter(Boolean).map((line) => Number(line.split(' ')[1])) : [];
38
+ t.after(() => {
39
+ for (const pid of pids()) {
40
+ if (alive(pid)) { try { process.kill(pid, 'SIGKILL'); } catch {} }
41
+ }
42
+ rmSync(root, { recursive: true, force: true });
43
+ });
23
44
  const tool = (path, body) => {
24
45
  writeFileSync(path, `#!/usr/bin/env bash\necho "$(basename "$0") $*" >> "$FAKE_LOG"\n${body}\n`);
25
46
  chmodSync(path, 0o755);
26
47
  };
48
+ writeFileSync(join(bin, 'worker'), `#!/usr/bin/env python3
49
+ import os, signal, sys, time
50
+ label = sys.argv[1]
51
+ root = os.environ['FAKE_ROOT']
52
+ def record(name):
53
+ with open(root + '/pids', 'a') as f: f.write('%s %s\\n' % (name, os.getpid()))
54
+ record(label)
55
+ if label in ('startup', 'journey', 'emulator-tree'):
56
+ pid = os.fork()
57
+ if pid == 0:
58
+ signal.signal(signal.SIGTERM, signal.SIG_IGN)
59
+ record(label + '-child')
60
+ open(root + '/' + label + '-child-ready', 'w').close()
61
+ while True: time.sleep(1)
62
+ while not os.path.exists(root + '/' + label + '-child-ready'): time.sleep(0.01)
63
+ if label == 'emulator-tree':
64
+ open(root + '/emulator-ready', 'w').close()
65
+ while not os.path.exists(root + '/release-emulator'): time.sleep(0.01)
66
+ sys.exit(0)
67
+ if os.environ.get('FAKE_EMULATOR_IGNORES_TERM') and label == 'emulator':
68
+ signal.signal(signal.SIGTERM, signal.SIG_IGN)
69
+ open(root + '/' + label + '-ready', 'w').close()
70
+ while True: time.sleep(1)
71
+ `);
72
+ chmodSync(join(bin, 'worker'), 0o755);
27
73
  tool(join(sdk, 'emulator', 'emulator'), `
28
- if test -n "\${FAKE_EMULATOR_IGNORES_TERM:-}"; then trap '' TERM; fi
29
- while :; do sleep 0.1; done`);
74
+ if test -n "\${FAKE_EMULATOR_TREE:-}"; then exec worker emulator-tree; fi
75
+ exec worker emulator`);
30
76
  tool(join(bin, 'adb'), `
77
+ printf 'adb-env %s %s\\n' "\${ADB_SERVER_SOCKET:-}" "\${ANDROID_SERIAL:-}" >> "$FAKE_LOG"
31
78
  case "$*" in
32
- start-server) exit 0 ;;
33
- *getprop*) if test -n "\${FAKE_ADB_HANG:-}"; then sleep 30; fi; echo 1 ;;
79
+ start-server)
80
+ if test -n "\${FAKE_ADB_START_HANG:-}"; then exec worker startup; fi
81
+ if test -n "\${FAKE_ADB_START_SIGNAL:-}"; then kill -TERM $$; fi
82
+ exit 0 ;;
83
+ *getprop*) if test -n "\${FAKE_ADB_HANG:-}"; then exec worker probe; fi; echo 1 ;;
34
84
  esac`);
35
85
  tool(join(bin, 'docker'), `
36
86
  case "$1" in
37
- run) while test $# -gt 0; do if test "$1" = --cidfile; then echo fakecid42 > "$2"; fi; shift; done; echo fakecid42 ;;
87
+ run)
88
+ while test $# -gt 0; do
89
+ if test "$1" = --cidfile && test -z "\${FAKE_NO_CID:-}"; then echo fakecid42 > "$2"; fi
90
+ shift
91
+ done
92
+ if test -n "\${FAKE_DOCKER_RUN_HANG:-}"; then exec worker startup; fi
93
+ echo fakecid42 ;;
38
94
  port) echo 127.0.0.1:55432 ;;
39
95
  exec) exit 0 ;;
40
- rm) exit 0 ;;
41
- ps) exit 0 ;;
96
+ rm) if test -n "\${FAKE_RM_HANG:-}"; then exec worker remove; fi; exit 0 ;;
97
+ ps) if test -n "\${FAKE_CONTAINER_REMAINS:-}"; then echo fakecid42; fi; exit 0 ;;
42
98
  esac`);
43
- return { root, bin, sdk, log, ports: await hostPorts() };
99
+ return { root, bin, sdk, log, pids, ports: await hostPorts() };
44
100
  }
45
101
 
46
- // Runners share the host network, so every port this suite hands the script is one the
47
- // OS reports free right now, never a fixed number another job may already hold.
48
102
  function listen(port) {
49
103
  return new Promise((resolve, reject) => {
50
104
  const server = createServer();
@@ -64,7 +118,8 @@ async function freeEmulatorPort() {
64
118
  for (let port = 5554; port <= 5584; port += 2) {
65
119
  const held = [];
66
120
  try {
67
- held.push(await listen(port), await listen(port + 1));
121
+ held.push(await listen(port));
122
+ held.push(await listen(port + 1));
68
123
  return port;
69
124
  } catch {
70
125
  // taken; try the next pair
@@ -83,90 +138,299 @@ async function hostPorts() {
83
138
  };
84
139
  }
85
140
 
86
- function run(host, { journey = ['bash', '-c', 'exit 0'], env = {} } = {}) {
87
- const drop = ['bash', '-c', 'echo "drop $SHARD_DB_NAME $SHARD_ADMIN_URL" >> "$FAKE_LOG"'];
88
- const result = spawnSync('bash', [SCRIPT, '--drop', ...drop, '--', ...journey], {
141
+ async function run(host, { journey = ['bash', '-c', 'exit 0'], drop, env = {}, signalAt, input = '' } = {}) {
142
+ drop ??= ['bash', '-c', 'echo "drop $SHARD_DB_NAME $SHARD_ADMIN_URL" >> "$FAKE_LOG"'];
143
+ const child = spawn('bash', [SCRIPT, '--drop', ...drop, '--', ...journey], {
89
144
  cwd: host.root,
90
- encoding: 'utf8',
91
- timeout: 60_000,
145
+ detached: true,
92
146
  env: {
93
147
  PATH: `${host.bin}:${process.env.PATH}`,
94
148
  TMPDIR: host.root,
95
149
  FAKE_LOG: host.log,
150
+ FAKE_ROOT: host.root,
96
151
  ANDROID_HOME: host.sdk,
97
152
  SETUP_AVD: 'Test_Phone',
98
153
  ...host.ports,
99
154
  DB_NAME_PREFIX: 'app_e2e_',
100
- KILL_GRACE_SECONDS: '2',
101
- BOUNDED_SECONDS: '5',
155
+ KILL_GRACE_SECONDS: '1',
156
+ BOUNDED_SECONDS: '2',
102
157
  ...env,
103
158
  },
104
159
  });
105
- return { ...result, calls: readFileSync(host.log, 'utf8') };
160
+ child.stdin.end(input);
161
+ let stdout = '', stderr = '';
162
+ child.stdout.on('data', (chunk) => { stdout += chunk; });
163
+ child.stderr.on('data', (chunk) => { stderr += chunk; });
164
+ let expired = false;
165
+ const timer = setTimeout(() => {
166
+ expired = true;
167
+ try { process.kill(-child.pid, 'SIGKILL'); } catch {}
168
+ for (const pid of host.pids()) { try { process.kill(pid, 'SIGKILL'); } catch {} }
169
+ }, 12_000);
170
+ const closed = new Promise((resolve) => child.once('close', (status, signal) => resolve({ status, signal })));
171
+ try {
172
+ if (signalAt) {
173
+ await until(() => existsSync(join(host.root, signalAt)), `never reached ${signalAt}`);
174
+ child.kill('SIGTERM'); // Signal only the launcher, as an external caller does.
175
+ }
176
+ const result = await closed;
177
+ const emulatorLog = join(host.root, '.cache/logs/emulator.log');
178
+ return { ...result, stdout, stderr: stderr + (existsSync(emulatorLog) ? readFileSync(emulatorLog, 'utf8') : ''), expired, calls: readFileSync(host.log, 'utf8') };
179
+ } finally {
180
+ clearTimeout(timer);
181
+ if (child.exitCode === null && child.signalCode === null) {
182
+ try { process.kill(-child.pid, 'SIGKILL'); } catch {}
183
+ }
184
+ }
185
+ }
186
+
187
+ async function stopped(host) {
188
+ await until(() => host.pids().every((pid) => !alive(pid)), 'owned descendants survived cleanup', 1500);
189
+ }
190
+
191
+ function outcome(r, original, cleanup) {
192
+ assert.equal(r.expired, false, `test safety deadline fired: ${r.stderr}`);
193
+ assert.equal(r.status, original || cleanup, r.stderr);
194
+ assert.match(r.stdout, new RegExp(`original_stage_exit=${original} cleanup_exit=${cleanup}\\n$`), r.stderr);
106
195
  }
107
196
 
108
- test('a passing journey cleans up everything it started and exits 0', async () => {
109
- const host = await fakeHost();
197
+ test('a passing journey cleans up everything it started and exits 0', async (t) => {
198
+ const host = await fakeHost(t);
110
199
  const marker = join(host.root, 'journey ran.txt');
111
- const r = run(host, { journey: ['bash', '-c', 'echo "$ANDROID_EMULATOR_SERIAL $SHARD_DB_NAME" > "$1"', 'journey', marker] });
112
- assert.equal(r.status, 0, r.stderr);
113
- assert.match(r.stdout, /original_stage_exit=0 cleanup_exit=0\n$/);
200
+ const r = await run(host, { journey: ['bash', '-c', 'echo "$ANDROID_EMULATOR_SERIAL $SHARD_DB_NAME" > "$1"', 'journey', marker] });
201
+ outcome(r, 0, 0);
114
202
  const port = host.ports.EMULATOR_PORT;
115
203
  assert.match(readFileSync(marker, 'utf8'), new RegExp(`^emulator-${port} app_e2e_\\d+_\\d+\\n$`));
116
- assert.match(r.calls, /^adb start-server$/m);
117
204
  assert.ok(r.calls.indexOf('adb start-server') < r.calls.indexOf(`emulator -avd Test_Phone -port ${port}`));
118
205
  assert.match(r.calls, new RegExp(`^emulator -avd Test_Phone -port ${port} -memory 4096 `, 'm'));
119
206
  assert.match(r.calls, /^drop app_e2e_\d+_\d+ postgresql:\/\/postgres:postgres@127\.0\.0\.1:55432\/postgres$/m);
120
207
  assert.match(r.calls, /^docker rm --force fakecid42$/m);
208
+ await stopped(host);
121
209
  });
122
210
 
123
- test('a failing journey keeps its status and still cleans up', async () => {
124
- const host = await fakeHost();
125
- const r = run(host, { journey: ['bash', '-c', 'exit 42'] });
126
- assert.equal(r.status, 42);
127
- assert.match(r.stdout, /original_stage_exit=42 cleanup_exit=0/);
211
+ test('a failing journey keeps its status and still cleans up', async (t) => {
212
+ const host = await fakeHost(t);
213
+ const r = await run(host, { journey: ['bash', '-c', 'exit 42'] });
214
+ outcome(r, 42, 0);
128
215
  assert.match(r.calls, /^drop app_e2e_/m);
129
216
  assert.match(r.calls, /^docker rm --force fakecid42$/m);
130
217
  });
131
218
 
132
- test('an emulator that ignores TERM is killed and reported as a cleanup failure', async () => {
133
- const host = await fakeHost();
134
- const r = run(host, { env: { FAKE_EMULATOR_IGNORES_TERM: '1' } });
135
- assert.equal(r.status, 1);
136
- assert.match(r.stdout, /original_stage_exit=0 cleanup_exit=1/);
219
+ test('an emulator that ignores TERM is killed and reported as a cleanup failure', async (t) => {
220
+ const host = await fakeHost(t);
221
+ // Wait for the fake to install its handler, not an assumed startup delay.
222
+ const r = await run(host, { env: { FAKE_EMULATOR_IGNORES_TERM: '1' }, journey: ['bash', '-c', 'while ! test -e "$FAKE_ROOT/emulator-ready"; do sleep 0.02; done'] });
223
+ outcome(r, 0, 1);
137
224
  assert.match(r.calls, /^docker rm --force fakecid42$/m);
225
+ await stopped(host);
138
226
  });
139
227
 
140
- test('a stalled readiness probe ends the run with 124 and no journey', async () => {
141
- const host = await fakeHost();
228
+ test('a stalled readiness probe ends the run with 124 and no journey', async (t) => {
229
+ const host = await fakeHost(t);
142
230
  const marker = join(host.root, 'journey-ran');
143
- const started = Date.now();
144
- const r = run(host, { journey: ['touch', marker], env: { FAKE_ADB_HANG: '1', BOUNDED_SECONDS: '1' } });
145
- assert.equal(r.status, 124);
146
- assert.ok(Date.now() - started < 15_000, 'the deadline must not wait for the stalled command\'s children');
147
- assert.match(r.stdout, /original_stage_exit=124 cleanup_exit=0/);
231
+ const r = await run(host, { journey: ['touch', marker], env: { FAKE_ADB_HANG: '1', BOUNDED_SECONDS: '0.5' } });
232
+ outcome(r, 124, 0);
148
233
  assert.equal(existsSync(marker), false);
149
- assert.doesNotMatch(r.calls, /^drop /m, 'no database was handed to the harness, so none is dropped');
234
+ assert.doesNotMatch(r.calls, /^drop /m);
150
235
  assert.match(r.calls, /^docker rm --force fakecid42$/m);
236
+ await stopped(host);
151
237
  });
152
238
 
153
- test('a busy port stops the run before anything starts', async () => {
154
- const host = await fakeHost();
239
+ test('a busy port stops the run before anything starts', async (t) => {
240
+ const host = await fakeHost(t);
155
241
  const server = await listen(Number(host.ports.ANDROID_BACKEND_PORT));
156
242
  try {
157
- const r = run(host);
243
+ const r = await run(host);
158
244
  assert.notEqual(r.status, 0);
159
245
  assert.match(r.stdout, /cleanup_exit=0/);
160
- assert.doesNotMatch(r.calls, /emulator -avd|docker run/);
246
+ assert.equal(r.calls, '');
161
247
  } finally {
162
- server.close();
248
+ await new Promise((resolve) => server.close(resolve));
163
249
  }
164
250
  });
165
251
 
166
- test('an emulator port outside the range adb scans is refused', async () => {
167
- const host = await fakeHost();
168
- const r = run(host, { env: { EMULATOR_PORT: '5588' } });
252
+ test('an emulator port outside the range adb scans is refused', async (t) => {
253
+ const host = await fakeHost(t);
254
+ const r = await run(host, { env: { EMULATOR_PORT: '5588' } });
169
255
  assert.equal(r.status, 2);
170
256
  assert.match(r.stderr, /even port in 5554-5584/);
171
257
  assert.equal(r.calls, '');
172
258
  });
259
+
260
+ test('a timed out docker startup removes its recorded CID and kills descendants', async (t) => {
261
+ const host = await fakeHost(t);
262
+ const r = await run(host, { env: { FAKE_DOCKER_RUN_HANG: '1', BOUNDED_SECONDS: '0.5' } });
263
+ outcome(r, 124, 0);
264
+ assert.match(r.calls, /^docker rm --force fakecid42$/m);
265
+ assert.doesNotMatch(r.calls, /^drop /m);
266
+ await stopped(host);
267
+ });
268
+
269
+ for (const stage of ['startup', 'journey']) {
270
+ test(`SIGTERM during ${stage} stops descendants and preserves 143`, async (t) => {
271
+ const host = await fakeHost(t);
272
+ const r = await run(host, {
273
+ signalAt: `${stage}-ready`,
274
+ journey: ['worker', 'journey'],
275
+ env: stage === 'startup' ? { FAKE_DOCKER_RUN_HANG: '1', BOUNDED_SECONDS: '8' } : {},
276
+ });
277
+ outcome(r, 143, 0);
278
+ assert.match(r.calls, /^docker rm --force fakecid42$/m);
279
+ if (stage === 'journey') assert.match(r.calls, /^drop /m);
280
+ else assert.doesNotMatch(r.calls, /^drop /m);
281
+ await stopped(host);
282
+ });
283
+ }
284
+
285
+ test('a journey deadline kills its whole process group and still drops the database', async (t) => {
286
+ const host = await fakeHost(t);
287
+ const r = await run(host, { journey: ['worker', 'journey'], env: { JOURNEY_SECONDS: '0.5' } });
288
+ outcome(r, 124, 0);
289
+ assert.match(r.calls, /^drop /m);
290
+ assert.match(r.calls, /^docker rm --force fakecid42$/m);
291
+ await stopped(host);
292
+ });
293
+
294
+ test('an emulator descendant is removed even after the emulator leader exits', async (t) => {
295
+ const host = await fakeHost(t);
296
+ const r = await run(host, {
297
+ env: { FAKE_EMULATOR_TREE: '1' },
298
+ journey: ['bash', '-c', 'while ! test -e "$FAKE_ROOT/emulator-ready"; do sleep 0.02; done; touch "$FAKE_ROOT/release-emulator"'],
299
+ });
300
+ assert.equal(r.expired, false);
301
+ await stopped(host);
302
+ outcome(r, 0, 1); // A descendant that requires KILL is a cleanup failure.
303
+ });
304
+
305
+ test('cleanup timeouts do not replace an original failure or skip remaining cleanup', async (t) => {
306
+ const host = await fakeHost(t);
307
+ const r = await run(host, {
308
+ journey: ['bash', '-c', 'exit 42'], drop: ['worker', 'drop'],
309
+ env: { FAKE_RM_HANG: '1', BOUNDED_SECONDS: '0.5' },
310
+ });
311
+ outcome(r, 42, 1);
312
+ assert.match(r.calls, /^docker rm --force fakecid42$/m);
313
+ assert.match(r.calls, /^docker ps /m);
314
+ await stopped(host);
315
+ });
316
+
317
+ test('a command killed by a signal keeps the shell signal status', async (t) => {
318
+ const host = await fakeHost(t);
319
+ const r = await run(host, { journey: ['bash', '-c', 'kill -TERM $$'] });
320
+ outcome(r, 143, 0);
321
+ });
322
+
323
+ test('a missing journey executable keeps shell status 127', async (t) => {
324
+ const host = await fakeHost(t);
325
+ const r = await run(host, { journey: [join(host.root, 'missing')] });
326
+ outcome(r, 127, 0);
327
+ assert.match(r.calls, /^drop /m);
328
+ assert.match(r.calls, /^docker rm --force fakecid42$/m);
329
+ });
330
+
331
+ test('a non-executable journey keeps shell status 126', async (t) => {
332
+ const host = await fakeHost(t);
333
+ const path = join(host.root, 'not executable');
334
+ writeFileSync(path, '#!/bin/sh\nexit 0\n');
335
+ const r = await run(host, { journey: [path] });
336
+ outcome(r, 126, 0);
337
+ });
338
+
339
+ test('a signal-ended startup command preserves shell status 143', async (t) => {
340
+ const host = await fakeHost(t);
341
+ const r = await run(host, { env: { FAKE_ADB_START_SIGNAL: '1' } });
342
+ outcome(r, 143, 0);
343
+ assert.doesNotMatch(r.calls, /emulator -avd|docker run/);
344
+ });
345
+
346
+ test('an unacknowledged docker startup reports uncertain cleanup without guessing an ID', async (t) => {
347
+ const host = await fakeHost(t);
348
+ const r = await run(host, { env: { FAKE_DOCKER_RUN_HANG: '1', FAKE_NO_CID: '1', BOUNDED_SECONDS: '0.5' } });
349
+ outcome(r, 124, 1);
350
+ assert.doesNotMatch(r.calls, /^docker rm |^drop /m);
351
+ await stopped(host);
352
+ });
353
+
354
+ test('a container still listed after removal is a cleanup failure', async (t) => {
355
+ const host = await fakeHost(t);
356
+ const r = await run(host, { env: { FAKE_CONTAINER_REMAINS: '1' } });
357
+ outcome(r, 0, 1);
358
+ });
359
+
360
+ test('journey stdin and literal argument boundaries are preserved', async (t) => {
361
+ const host = await fakeHost(t);
362
+ const r = await run(host, {
363
+ input: 'input line\n',
364
+ journey: ['bash', '-c', 'IFS= read -r line; printf "received <%s> <%s> <%s>\\n" "$line" "$1" "$2"', 'journey', 'a b', '--'],
365
+ });
366
+ outcome(r, 0, 0);
367
+ assert.match(r.stdout, /^received <input line> <a b> <-->$/m);
368
+ });
369
+
370
+ test('cleanup leaves an unrelated process running', async (t) => {
371
+ const host = await fakeHost(t);
372
+ const peer = spawn(join(host.bin, 'worker'), ['peer'], {
373
+ detached: true, stdio: 'ignore', env: { ...process.env, FAKE_ROOT: host.root },
374
+ });
375
+ const exited = new Promise((resolve) => peer.once('close', resolve));
376
+ t.after(async () => { peer.kill('SIGKILL'); await exited; });
377
+ await until(() => existsSync(join(host.root, 'peer-ready')), 'peer did not start');
378
+ const r = await run(host);
379
+ outcome(r, 0, 0);
380
+ assert.equal(alive(peer.pid), true);
381
+ });
382
+
383
+ for (const env of [
384
+ { BOUNDED_SECONDS: '0' }, { BOUNDED_SECONDS: 'NaN' },
385
+ { JOURNEY_SECONDS: '-1' }, { KILL_GRACE_SECONDS: 'infinity' },
386
+ { BOOT_ATTEMPTS: '0' }, { DB_URL_VAR: 'bad-name' }, { DB_NAME_VAR: 'SHARD_ADMIN_URL' },
387
+ ]) {
388
+ test(`invalid settings are refused before startup: ${JSON.stringify(env)}`, async (t) => {
389
+ const host = await fakeHost(t);
390
+ const r = await run(host, { env });
391
+ assert.equal(r.status, 2, r.stderr);
392
+ assert.equal(r.calls, '');
393
+ });
394
+ }
395
+
396
+ for (const assignment of ['active_pid=$!', 'emulator_pid=$!']) {
397
+ test(`TERM between spawn and recording ${assignment} retains ownership`, async (t) => {
398
+ const host = await fakeHost(t);
399
+ const hook = join(host.root, 'launch-race.bash');
400
+ const ready = assignment.startsWith('active') ? 'startup-ready' : 'emulator-ready';
401
+ writeFileSync(hook, `
402
+ set -T
403
+ trap 'if [[ "$BASH_SUBSHELL" = "0" && "$BASH_COMMAND" = "$RACE_ASSIGNMENT" && ! -e "$FAKE_ROOT/race-fired" ]]; then
404
+ printf "supervisor %s\\n" "$!" >> "$FAKE_ROOT/pids"
405
+ touch "$FAKE_ROOT/race-fired"
406
+ while ! test -e "$FAKE_ROOT/$RACE_READY"; do sleep 0.02; done
407
+ kill -TERM "$$"
408
+ fi' DEBUG
409
+ `);
410
+ const r = await run(host, { env: {
411
+ BASH_ENV: hook, RACE_ASSIGNMENT: assignment, RACE_READY: ready,
412
+ ...(assignment.startsWith('active') ? { FAKE_ADB_START_HANG: '1', BOUNDED_SECONDS: '60' } : {}),
413
+ } });
414
+ assert.equal(existsSync(join(host.root, 'race-fired')), true, r.stderr);
415
+ outcome(r, 143, 0);
416
+ assert.doesNotMatch(r.calls, /^docker run/m);
417
+ await stopped(host);
418
+ });
419
+ }
420
+
421
+ // The default commands intentionally use the original environment-variable contract.
422
+ test('caller-selected adb endpoint and custom database names reach both commands', async (t) => {
423
+ const host = await fakeHost(t);
424
+ const command = ['bash', '-c', 'printf "contract %s %s %s %s\\n" "$ADB_SERVER_SOCKET" "$ANDROID_EMULATOR_SERIAL" "$TEST_ADMIN_URL" "$TEST_DB" >> "$FAKE_LOG"'];
425
+ const r = await run(host, {
426
+ journey: command, drop: command,
427
+ env: { ADB_SERVER_SOCKET: 'tcp:127.0.0.1:15037', DB_URL_VAR: 'TEST_ADMIN_URL', DB_NAME_VAR: 'TEST_DB' },
428
+ });
429
+ outcome(r, 0, 0);
430
+ assert.match(r.calls, /^adb-env tcp:127\.0\.0\.1:15037 /m);
431
+ const lines = r.calls.split('\n').filter((line) => line.startsWith('contract '));
432
+ assert.equal(lines.length, 2);
433
+ assert.equal(lines[0], lines[1]);
434
+ assert.match(lines[0], new RegExp(`^contract tcp:127\\.0\\.0\\.1:15037 emulator-${host.ports.EMULATOR_PORT} postgresql://postgres:postgres@127\\.0\\.0\\.1:55432/postgres app_e2e_`));
435
+ assert.doesNotMatch(r.calls, /kill-server|emu kill|system prune/);
436
+ });
@@ -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.42.1
4
+ release-version: 0.43.1
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.42.1
4
+ release-version: 0.43.1
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.42.1
4
+ release-version: 0.43.1
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.42.1
4
+ release-version: 0.43.1
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 a clean checkout to a manual Login smoke on an owned iOS Simulator: isolated worktree 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-foundation), signing, TestFlight or App Store release, or general workflow YAML (github-actions)."
4
- release-version: 0.42.1
4
+ release-version: 0.43.1
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.42.1
4
+ release-version: 0.43.1
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.42.1
4
+ release-version: 0.43.1
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.42.1
4
+ release-version: 0.43.1
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.42.1
15
+ release-version: 0.43.1
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.42.1
4
+ release-version: 0.43.1
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.42.1
5
+ release-version: 0.43.1
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.42.1
4
+ release-version: 0.43.1
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.42.1
4
+ release-version: 0.43.1
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.42.1
4
+ release-version: 0.43.1
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.42.1
8
+ release-version: 0.43.1
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.42.1
4
+ release-version: 0.43.1
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.42.1
4
+ release-version: 0.43.1
5
5
  ---
6
6
  **Updates:** Once/session run `node <skill-base-dir>/release-check.mjs`.
7
7
  # GitHub Actions