@1aboveio/skills 1.0.13 → 1.0.15

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 (106) hide show
  1. package/.codex-plugin/plugin.json +1 -1
  2. package/.codex-plugin/resources.json +157 -154
  3. package/README.md +1 -1
  4. package/package.json +5 -5
  5. package/skills/app-dev/airflow/SKILL.md +1 -1
  6. package/skills/app-dev/android-development/SKILL.md +127 -0
  7. package/skills/app-dev/android-development/references/android-conventions.md +117 -0
  8. package/skills/app-dev/{android-foundation → android-development}/references/ci.md +10 -6
  9. package/skills/app-dev/{android-foundation → android-development}/references/diagnosis.md +44 -15
  10. package/skills/app-dev/android-development/references/feature-work.md +186 -0
  11. package/skills/app-dev/android-development/references/framework-choice.md +88 -0
  12. package/skills/app-dev/{android-foundation → android-development}/references/gotchas.md +43 -6
  13. package/skills/app-dev/android-development/references/init.md +126 -0
  14. package/skills/app-dev/android-development/references/setup.md +389 -0
  15. package/skills/app-dev/android-development/references/workspace.md +156 -0
  16. package/skills/app-dev/android-development/scripts/init.mjs +477 -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 +2 -2
  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 +25 -4
  67. package/skills/engineering/engineering/references/init-scope.md +10 -3
  68. package/skills/engineering/engineering/references/scope-management.md +11 -0
  69. package/skills/engineering/engineering/scripts/init.mjs +65 -17
  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 +1 -1
  77. package/skills/engineering/resolve-issues/SKILL.md +1 -1
  78. package/skills/engineering/resolve-issues/references/exploration.md +1 -1
  79. package/skills/engineering/resolve-issues/references/review.md +27 -4
  80. package/skills/engineering/resolve-release/SKILL.md +1 -1
  81. package/skills/engineering/rush-issues/SKILL.md +1 -1
  82. package/skills/engineering/rush-release/SKILL.md +1 -1
  83. package/skills/engineering/test-quality/SKILL.md +3 -1
  84. package/skills/engineering/test-quality/scripts/persist-conventions-core.mjs +231 -21
  85. package/skills/engineering/test-quality/scripts/persist-conventions.mjs +13 -1
  86. package/skills/risk-compliance/compliance-audit/SKILL.md +1 -1
  87. package/skills/risk-compliance/contract-compare/SKILL.md +1 -1
  88. package/skills/risk-compliance/contract-review/SKILL.md +1 -1
  89. package/skills/risk-compliance/doc-authenticity/SKILL.md +1 -1
  90. package/skills/risk-compliance/fraud-detection/SKILL.md +1 -1
  91. package/skills/risk-compliance/payment-analysis/SKILL.md +1 -1
  92. package/skills/risk-compliance/portfolio-payment-analysis/SKILL.md +1 -1
  93. package/skills/risk-compliance/similar-domain/SKILL.md +1 -1
  94. package/skills/risk-compliance/tld-detection/SKILL.md +1 -1
  95. package/skills/app-dev/android-foundation/SKILL.md +0 -248
  96. package/skills/app-dev/android-foundation/references/delivery-focus.md +0 -31
  97. package/skills/app-dev/android-foundation/references/feature-work.md +0 -50
  98. package/skills/app-dev/android-foundation/references/setup.md +0 -188
  99. package/skills/app-dev/android-foundation/references/workspace.md +0 -81
  100. /package/skills/app-dev/{android-foundation → android-development}/LICENSE +0 -0
  101. /package/skills/app-dev/{android-foundation → android-development}/assets/android-ci-job.yml +0 -0
  102. /package/skills/app-dev/{android-foundation → android-development}/release-check.mjs +0 -0
  103. /package/skills/app-dev/{android-foundation → android-development}/scripts/device-run.sh +0 -0
  104. /package/skills/app-dev/{android-foundation → android-development}/scripts/workspace-runtime/bin/workspaces.mjs +0 -0
  105. /package/skills/app-dev/{android-foundation → android-development}/scripts/workspace-runtime/native-workspace.mjs +0 -0
  106. /package/skills/app-dev/{android-foundation → android-development}/scripts/workspace-runtime/workspace-lifecycle.mjs +0 -0
@@ -0,0 +1,477 @@
1
+ // Explicit Android project setup. Preview by default; adds only missing guidance.
2
+ import fs from 'node:fs'
3
+ import os from 'node:os'
4
+ import path from 'node:path'
5
+ import { spawnSync } from 'node:child_process'
6
+ import { fileURLToPath, pathToFileURL } from 'node:url'
7
+
8
+ const sectionTitle = 'Android development'
9
+ const sectionId = 'android'
10
+ const taskEntryTitle = 'Android task entry'
11
+ const taskEntryBody = `## ${taskEntryTitle}
12
+
13
+ For Android implementation, runtime debugging, native builds, or device validation,
14
+ read and apply the installed \`android-development\` skill before choosing
15
+ Android-specific commands. In issue workflows, load it during unit preflight.
16
+ Delegated Android workers must load it too. After compaction, reload it if its
17
+ instructions are no longer in context.`
18
+ const usage = 'node init.mjs [--repo-root <dir>] [--write]'
19
+
20
+ function parseArgs(argv) {
21
+ const options = { write: false }
22
+ for (let i = 0; i < argv.length; i++) {
23
+ const arg = argv[i]
24
+ if (arg === '--write') options.write = true
25
+ else if (arg === '--help') options.help = true
26
+ else if (arg === '--repo-root') {
27
+ const value = argv[++i]
28
+ if (!value || value.startsWith('--') || /[\r\n]/.test(value)) throw new Error(`missing or invalid value for ${arg}`)
29
+ options.repoRoot = path.resolve(value)
30
+ } else throw new Error(`unknown argument ${arg}`)
31
+ }
32
+ return options
33
+ }
34
+
35
+ function gitOutput(root, args) {
36
+ const result = spawnSync('git', ['-C', root, ...args], { encoding: 'utf8' })
37
+ return result.status === 0 ? result.stdout.trim() : null
38
+ }
39
+
40
+ function contains(outer, inner) {
41
+ const relative = path.relative(outer, inner)
42
+ return relative === '' || (!relative.startsWith(`..${path.sep}`) && relative !== '..' && !path.isAbsolute(relative))
43
+ }
44
+
45
+ function selectRoot(options) {
46
+ const selected = options.repoRoot ?? gitOutput(process.cwd(), ['rev-parse', '--show-toplevel']) ?? process.cwd()
47
+ const root = fs.realpathSync(selected)
48
+ if (!fs.statSync(root).isDirectory()) throw new Error('project root is not a directory')
49
+ // A home directory holds global agent configuration, not project guidance.
50
+ if (contains(root, fs.realpathSync(os.homedir()))) {
51
+ throw new Error('selected root is the home directory or one of its ancestors; pass the project root')
52
+ }
53
+ return { root, selected: options.repoRoot ?? null }
54
+ }
55
+
56
+ function sectionBody() {
57
+ return fs.readFileSync(fileURLToPath(new URL('../references/android-conventions.md', import.meta.url)), 'utf8').trim()
58
+ }
59
+
60
+ function resolveTarget(root, name) {
61
+ const file = path.join(root, name)
62
+ const stat = fs.lstatSync(file, { throwIfNoEntry: false })
63
+ if (!stat) return { name, file, target: file, inode: null, nlink: 0, exists: false, link: false }
64
+ let target
65
+ try {
66
+ target = fs.realpathSync(file)
67
+ } catch {
68
+ throw new Error(`${name} is a dangling or inaccessible link; repair it before initializing`)
69
+ }
70
+ if (!contains(root, target)) throw new Error(`${name} resolves outside the selected project`)
71
+ const info = fs.statSync(target)
72
+ if (!info.isFile()) throw new Error(`${name} is not a regular file`)
73
+ return {
74
+ name,
75
+ file,
76
+ target,
77
+ inode: { dev: info.dev, ino: info.ino },
78
+ nlink: info.nlink,
79
+ exists: true,
80
+ link: stat.isSymbolicLink(),
81
+ }
82
+ }
83
+
84
+ function readGuidance(entry) {
85
+ if (!entry.exists) return { ...entry, current: null }
86
+ let bytes
87
+ try {
88
+ fs.accessSync(entry.target, fs.constants.R_OK)
89
+ bytes = fs.readFileSync(entry.target)
90
+ } catch {
91
+ throw new Error(`${entry.name} is unreadable`)
92
+ }
93
+ let current
94
+ try {
95
+ current = new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(bytes)
96
+ } catch {
97
+ throw new Error(`${entry.name} is not valid UTF-8 guidance`)
98
+ }
99
+ return { ...entry, current }
100
+ }
101
+
102
+ // CLAUDE.md is a protected entrypoint. Learn only its identity, never its content.
103
+ function inspectClaude(root) {
104
+ try {
105
+ const file = path.join(root, 'CLAUDE.md')
106
+ const stat = fs.lstatSync(file, { throwIfNoEntry: false })
107
+ if (!stat) return null
108
+ const target = fs.realpathSync(file)
109
+ if (!contains(root, target)) return null
110
+ const info = fs.statSync(target)
111
+ if (!info.isFile()) return null
112
+ return { file, target, inode: { dev: info.dev, ino: info.ino } }
113
+ } catch {
114
+ return null
115
+ }
116
+ }
117
+
118
+ function sameFile(left, right) {
119
+ if (!left || !right) return false
120
+ if (left.target === right.target) return true
121
+ return Boolean(left.inode && right.inode
122
+ && left.inode.dev === right.inode.dev && left.inode.ino === right.inode.ino)
123
+ }
124
+
125
+ function indentWidth(line) {
126
+ let width = 0
127
+ for (const char of line) {
128
+ if (char === ' ') width += 1
129
+ else if (char === '\t') width += 4 - (width % 4)
130
+ else break
131
+ }
132
+ return width
133
+ }
134
+
135
+ function stripInlineCode(line) {
136
+ const runs = [...line.matchAll(/`+/g)]
137
+ let result = ''
138
+ let cursor = 0
139
+ for (let i = 0; i < runs.length; i++) {
140
+ const opening = runs[i]
141
+ let closing = i + 1
142
+ while (closing < runs.length && runs[closing][0].length !== opening[0].length) closing++
143
+ if (closing === runs.length) continue
144
+ const end = runs[closing].index + runs[closing][0].length
145
+ result += line.slice(cursor, opening.index) + ' '.repeat(end - opening.index)
146
+ cursor = end
147
+ i = closing
148
+ }
149
+ return result + line.slice(cursor)
150
+ }
151
+
152
+ function stripHtmlComments(line, depth = 0) {
153
+ let text = ''
154
+ let index = 0
155
+ const comments = []
156
+ while (index < line.length) {
157
+ const start = line.indexOf('<!--', index)
158
+ const end = line.indexOf('-->', index)
159
+ if (start !== -1 && (end === -1 || start < end)) {
160
+ if (depth === 0) {
161
+ const close = line.indexOf('-->', start + 4)
162
+ if (close !== -1) comments.push(line.slice(start, close + 3))
163
+ }
164
+ text += depth === 0 ? line.slice(index, start) : ' '.repeat(start - index)
165
+ index = start + 4
166
+ depth += 1
167
+ } else if (end !== -1) {
168
+ if (depth > 0) {
169
+ text += ' '.repeat(end + 3 - index)
170
+ depth -= 1
171
+ } else {
172
+ text += line.slice(index, end + 3)
173
+ }
174
+ index = end + 3
175
+ } else {
176
+ text += depth === 0 ? line.slice(index) : ' '.repeat(line.length - index)
177
+ break
178
+ }
179
+ }
180
+ return { text, depth, comments }
181
+ }
182
+
183
+ // Markdown-aware view: inactive lines are fenced code, indented code or HTML comments.
184
+ // Inline code and HTML comments are blanked from active text so examples never count.
185
+ function scanMarkdown(current) {
186
+ const lines = []
187
+ let fence = null
188
+ let commentDepth = 0
189
+ let indented = false
190
+ let previousBlank = true
191
+ for (const raw of current.split('\n')) {
192
+ const line = raw.endsWith('\r') ? raw.slice(0, -1) : raw
193
+ const blank = line.trim() === ''
194
+ let active = true
195
+ let text = line
196
+ let comments = []
197
+ if (fence) {
198
+ active = false
199
+ const close = /^ {0,3}(`{3,}|~{3,})[ \t]*$/.exec(line)
200
+ if (close && close[1][0] === fence.char && close[1].length >= fence.length) fence = null
201
+ } else if (commentDepth > 0) {
202
+ const comment = stripHtmlComments(line, commentDepth)
203
+ text = comment.text
204
+ commentDepth = comment.depth
205
+ comments = comment.comments
206
+ } else {
207
+ const open = /^ {0,3}(`{3,}|~{3,})(.*)$/.exec(line)
208
+ if (open && !(open[1][0] === '`' && open[2].includes('`'))) {
209
+ fence = { char: open[1][0], length: open[1].length }
210
+ active = false
211
+ indented = false
212
+ } else if (blank) {
213
+ indented = false
214
+ } else if (indentWidth(line) >= 4 && (previousBlank || indented)) {
215
+ active = false
216
+ indented = true
217
+ } else {
218
+ indented = false
219
+ const comment = stripHtmlComments(stripInlineCode(line))
220
+ text = comment.text
221
+ commentDepth = comment.depth
222
+ comments = comment.comments
223
+ }
224
+ }
225
+ previousBlank = blank
226
+ lines.push({ raw: line, text: active ? text : '', active, comments })
227
+ }
228
+ // An open fence or comment at the end of the file would swallow anything appended after it.
229
+ return { lines, fence, commentDepth }
230
+ }
231
+
232
+ function markdownView(current) {
233
+ return scanMarkdown(current).lines
234
+ }
235
+
236
+ // End of the paragraph starting at `start`; a span never pairs across a blank line.
237
+ function paragraphEnd(text, start) {
238
+ const blank = /\n[ \t]*\n/g
239
+ blank.lastIndex = start
240
+ const match = blank.exec(text)
241
+ return match ? match.index : text.length
242
+ }
243
+
244
+ // Marks inline code spans so a documented example is never read as a project rule.
245
+ // Runs of equal length pair up inside one paragraph; an unmatched run is literal text, so it
246
+ // never consumes a later rule. The rule text itself stays readable, only its span is marked.
247
+ function inlineCodeMask(text) {
248
+ const mask = new Uint8Array(text.length)
249
+ const runs = [...text.matchAll(/`+/g)]
250
+ let index = 0
251
+ while (index < runs.length) {
252
+ const opening = runs[index]
253
+ const limit = paragraphEnd(text, opening.index)
254
+ let closing = index + 1
255
+ while (closing < runs.length && runs[closing].index < limit
256
+ && runs[closing][0].length !== opening[0].length) closing++
257
+ if (closing === runs.length || runs[closing].index >= limit) {
258
+ index += 1
259
+ continue
260
+ }
261
+ mask.fill(1, opening.index, runs[closing].index + runs[closing][0].length)
262
+ index = closing + 1
263
+ }
264
+ return mask
265
+ }
266
+
267
+ // Only a match that starts outside inline code is an instruction rather than an example.
268
+ // The rule itself keeps its text, because the skill name is normally written as code.
269
+ function matchesOutsideInlineCode(text, pattern) {
270
+ const flags = pattern.flags.includes('g') ? pattern.flags : `${pattern.flags}g`
271
+ const global = new RegExp(pattern.source, flags)
272
+ const mask = inlineCodeMask(text)
273
+ for (const match of text.matchAll(global)) {
274
+ if (!mask[match.index]) return true
275
+ }
276
+ return false
277
+ }
278
+
279
+ function append(current, body, name) {
280
+ const { fence, commentDepth } = scanMarkdown(current)
281
+ if (fence || commentDepth) {
282
+ const construct = fence ? 'an unclosed fenced code block' : 'an unclosed HTML comment'
283
+ throw new Error(`${name} ends inside ${construct}; appended Android guidance would be inactive and appended again on every run. Close it and rerun init; nothing was changed`)
284
+ }
285
+ const separator = current.endsWith('\n\n') ? '' : current.endsWith('\n') ? '\n' : '\n\n'
286
+ return `${current}${separator}${body.trim()}\n`
287
+ }
288
+
289
+ function hasHeading(current, title) {
290
+ const lines = markdownView(current)
291
+ for (let i = 0; i < lines.length; i++) {
292
+ const line = lines[i]
293
+ if (!line.active) continue
294
+ const atx = /^ {0,3}(#{1,6})(?:[ \t]+(.*?))?[ \t]*$/.exec(line.text)
295
+ if (atx) {
296
+ const content = (atx[2] ?? '').replace(/[ \t]+#+[ \t]*$/, '').trim()
297
+ if (atx[1].length === 2 && content === title) return true
298
+ continue
299
+ }
300
+ const next = lines[i + 1]
301
+ if (line.text.trim() === title && next && next.active && /^ {0,3}-+[ \t]*$/.test(next.text)) {
302
+ return true
303
+ }
304
+ }
305
+ return false
306
+ }
307
+
308
+ function countMarkers(lines, start, end) {
309
+ const starts = []
310
+ const ends = []
311
+ let order = 0
312
+ for (const line of lines) {
313
+ for (const comment of line.comments) {
314
+ if (comment === start) starts.push(order)
315
+ else if (comment === end) ends.push(order)
316
+ order++
317
+ }
318
+ }
319
+ return { starts, ends }
320
+ }
321
+
322
+ function preserveOrAdd(current, name, title, id, body) {
323
+ const start = `<!-- conventions:${id}:start -->`
324
+ const end = `<!-- conventions:${id}:end -->`
325
+ const { starts, ends } = countMarkers(markdownView(current), start, end)
326
+ if (starts.length !== ends.length || starts.length > 1 || (starts.length && starts[0] > ends[0])) {
327
+ throw new Error(`malformed ${title} section markers; preserve and repair existing guidance first`)
328
+ }
329
+ if (starts.length || hasHeading(current, title)) return current
330
+ return append(current, `${start}\n${body}\n${end}`, name)
331
+ }
332
+
333
+ function linkFrom(from, to) {
334
+ return path.relative(path.dirname(from.target), to.file).split(path.sep).join('/')
335
+ }
336
+
337
+ function hasLoader(current, from, to) {
338
+ const text = markdownView(current).map(line => line.text).join('\n')
339
+ const links = /\[[^\]\r\n]*\]\(\s*(?:<([^>]+)>|([^\s)]+))(?:\s+["'][^"'\r\n]*["'])?\s*\)/g
340
+ for (const match of text.matchAll(links)) {
341
+ const href = match[1] ?? match[2]
342
+ if (!href || /^[a-z][a-z\d+.-]*:|^\/\//i.test(href)) continue
343
+ let target
344
+ try {
345
+ const link = new URL(href, pathToFileURL(from.target))
346
+ if (link.hash && decodeURIComponent(link.hash) !== '#android-development') continue
347
+ link.hash = ''
348
+ link.search = ''
349
+ target = fileURLToPath(link)
350
+ } catch {
351
+ continue
352
+ }
353
+ if (fs.existsSync(target)) target = fs.realpathSync(target)
354
+ if (target === to.target) return true
355
+ }
356
+ return false
357
+ }
358
+
359
+ function addTaskEntry(current, name) {
360
+ const start = '<!-- conventions:android-task-entry:start -->'
361
+ const end = '<!-- conventions:android-task-entry:end -->'
362
+ const lines = markdownView(current)
363
+ const { starts, ends } = countMarkers(lines, start, end)
364
+ if (starts.length !== ends.length || starts.length > 1 || (starts.length && starts[0] > ends[0])) {
365
+ throw new Error('malformed Android task entry markers; preserve and repair existing guidance first')
366
+ }
367
+ // Only active prose can hold a rule: fenced code, indented code, HTML comments, inline code
368
+ // and fenced examples are documentation. A rule keeps its own inline code, so the skill name
369
+ // written as `android-development` is still matched by its position outside a code span.
370
+ const prose = lines.map(line => line.active && line.text.trim()
371
+ ? stripHtmlComments(line.raw).text : '')
372
+ const active = prose.join('\n')
373
+ // Recognize an existing direct loader, not a mere skill mention or guide link.
374
+ const loader = /^[ \t]*(?:(?:[-+*]|\d+[.)])\s+)?(?:read|load)\s+(?:and\s+apply\s+)?(?:the\s+)?(?:installed\s+)?`?android-development`?\s+skill\s+before\b/im
375
+ const negation = /\b(?:do not|don't|never|must not)\s+(?:automatically\s+)?(?:read|load|apply|use|follow)\b[^.]{0,120}\bandroid-development\b/i
376
+ if (matchesOutsideInlineCode(active, negation)) {
377
+ throw new Error('existing Android skill-loading rule conflicts with task entry; nothing was changed')
378
+ }
379
+ if (starts.length) {
380
+ const from = lines.findIndex(line => line.comments.includes(start))
381
+ const to = lines.findIndex(line => line.comments.includes(end))
382
+ const body = prose.slice(from + 1, to).join('\n')
383
+ if (!matchesOutsideInlineCode(body, loader)) {
384
+ throw new Error('preserved Android task entry has no active loader; nothing was changed')
385
+ }
386
+ return current
387
+ }
388
+ if (matchesOutsideInlineCode(active, loader)) return current
389
+ if (hasHeading(current, taskEntryTitle)) {
390
+ throw new Error('existing Android task entry needs manual reconciliation; nothing was changed')
391
+ }
392
+ return append(current, `${start}\n${taskEntryBody}\n${end}`, name)
393
+ }
394
+
395
+ function planInitialization(root) {
396
+ const claude = inspectClaude(root)
397
+ const [agents, contributing] = ['AGENTS.md', 'CONTRIBUTING.md'].map(name => resolveTarget(root, name))
398
+ if (agents.target === contributing.target) {
399
+ throw new Error('agent instructions and CONTRIBUTING.md must use distinct files')
400
+ }
401
+ if (agents.inode && contributing.inode && agents.inode.dev === contributing.inode.dev
402
+ && agents.inode.ino === contributing.inode.ino) {
403
+ throw new Error('agent instructions and CONTRIBUTING.md must use distinct files')
404
+ }
405
+
406
+ for (const entry of [agents, contributing]) {
407
+ // Reading through a link to the protected entrypoint would read CLAUDE.md.
408
+ if (claude && entry.link && sameFile(entry, claude)) {
409
+ throw new Error(`${entry.name} resolves to the protected CLAUDE.md entrypoint; initialization never reads or edits CLAUDE.md`)
410
+ }
411
+ }
412
+
413
+ const agentsEntry = readGuidance(agents)
414
+ const contributingEntry = readGuidance(contributing)
415
+
416
+ let agentText = addTaskEntry(agentsEntry.current ?? '# Agent instructions\n', agentsEntry.name)
417
+ if (!hasLoader(agentText, agentsEntry, contributingEntry)) {
418
+ const heading = hasHeading(agentText, sectionTitle) ? '' : `## ${sectionTitle}\n\n`
419
+ agentText = append(agentText, `${heading}Follow [CONTRIBUTING.md](${linkFrom(agentsEntry, contributingEntry)}) for this project's Android validation ladder, device proof and required gates.`, agentsEntry.name)
420
+ }
421
+
422
+ const contributingText = preserveOrAdd(contributingEntry.current ?? '# Contributing\n', contributingEntry.name, sectionTitle, sectionId, sectionBody())
423
+
424
+ const plan = [
425
+ { ...agentsEntry, next: agentText },
426
+ { ...contributingEntry, next: contributingText },
427
+ ]
428
+
429
+ for (const entry of plan) {
430
+ if (entry.current === entry.next) continue
431
+ if (claude && sameFile(entry, claude)) {
432
+ throw new Error(`${entry.name} shares its file with the protected CLAUDE.md entrypoint; initialization never edits CLAUDE.md`)
433
+ }
434
+ if (entry.exists && entry.nlink > 1) {
435
+ throw new Error(`${entry.name} is hard-linked to another file; initialization will not change a file it does not exclusively own`)
436
+ }
437
+ }
438
+
439
+ return plan
440
+ }
441
+
442
+ function assertWritable(plan) {
443
+ for (const entry of plan) {
444
+ if (entry.current === entry.next) continue
445
+ const target = entry.current === null ? path.dirname(entry.target) : entry.target
446
+ try {
447
+ fs.accessSync(target, fs.constants.W_OK)
448
+ } catch {
449
+ throw new Error(`${entry.name} is not writable; nothing was changed`)
450
+ }
451
+ }
452
+ }
453
+
454
+ function main(argv) {
455
+ const options = parseArgs(argv)
456
+ if (options.help) {
457
+ console.log(usage)
458
+ return
459
+ }
460
+ const { root } = selectRoot(options)
461
+ const plan = planInitialization(root)
462
+ if (options.write) assertWritable(plan)
463
+ console.log(`android-development init: ${options.write ? 'write' : 'preview only'} (${root})`)
464
+ for (const entry of plan) {
465
+ const action = entry.current === entry.next ? 'unchanged' : entry.current === null ? 'create' : 'update'
466
+ if (options.write && action !== 'unchanged') fs.writeFileSync(entry.target, entry.next)
467
+ console.log(` ${entry.name}: ${!options.write && action !== 'unchanged' ? 'would ' : ''}${action}`)
468
+ }
469
+ console.log('Existing sections are preserved; initialization does not refresh project policy, install tools, provision devices or authorize delivery actions.')
470
+ }
471
+
472
+ try {
473
+ main(process.argv.slice(2))
474
+ } catch (error) {
475
+ console.error(`android-development init: ${error.message}`)
476
+ process.exitCode = 2
477
+ }
@@ -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.45.2
4
+ release-version: 0.47.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.45.2
4
+ release-version: 0.47.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.45.2
4
+ release-version: 0.47.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.