@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.
- package/.codex-plugin/plugin.json +1 -1
- package/.codex-plugin/resources.json +157 -154
- package/README.md +1 -1
- package/package.json +5 -5
- package/skills/app-dev/airflow/SKILL.md +1 -1
- package/skills/app-dev/android-development/SKILL.md +127 -0
- package/skills/app-dev/android-development/references/android-conventions.md +117 -0
- package/skills/app-dev/{android-foundation → android-development}/references/ci.md +10 -6
- package/skills/app-dev/{android-foundation → android-development}/references/diagnosis.md +44 -15
- package/skills/app-dev/android-development/references/feature-work.md +186 -0
- package/skills/app-dev/android-development/references/framework-choice.md +88 -0
- package/skills/app-dev/{android-foundation → android-development}/references/gotchas.md +43 -6
- package/skills/app-dev/android-development/references/init.md +126 -0
- package/skills/app-dev/android-development/references/setup.md +389 -0
- package/skills/app-dev/android-development/references/workspace.md +156 -0
- package/skills/app-dev/android-development/scripts/init.mjs +477 -0
- package/skills/app-dev/app-debug/SKILL.md +1 -1
- package/skills/app-dev/better-auth/SKILL.md +1 -1
- package/skills/app-dev/data-catalog/SKILL.md +53 -16
- package/skills/app-dev/data-catalog/scripts/persist-conventions-core.mjs +231 -21
- package/skills/app-dev/data-catalog/scripts/persist-conventions.mjs +12 -1
- package/skills/app-dev/data-quality/SKILL.md +1 -1
- package/skills/app-dev/ios-foundation/SKILL.md +2 -2
- package/skills/app-dev/monorepo/SKILL.md +1 -1
- package/skills/app-dev/nextjs-fullstack/SKILL.md +1 -1
- package/skills/app-dev/prisma-setup/SKILL.md +1 -1
- package/skills/app-dev/pyspark/SKILL.md +1 -1
- package/skills/app-dev/python-backend/SKILL.md +1 -1
- package/skills/app-dev/shadcn/SKILL.md +1 -1
- package/skills/app-dev/zod-v4/SKILL.md +1 -1
- package/skills/cicd/cloud-build/SKILL.md +1 -1
- package/skills/cicd/cloud-debug/SKILL.md +1 -1
- package/skills/cicd/cloud-deploy/SKILL.md +1 -1
- package/skills/cicd/docker/SKILL.md +1 -1
- package/skills/cicd/github-actions/SKILL.md +1 -1
- package/skills/cicd/github-flow/SKILL.md +10 -1
- package/skills/cicd/github-flow/scripts/persist-conventions-core.mjs +231 -21
- package/skills/cicd/github-flow/scripts/persist-conventions.mjs +18 -1
- package/skills/cicd/google-cloud/SKILL.md +1 -1
- package/skills/cicd/mergify/SKILL.md +1 -1
- package/skills/cicd/podman/SKILL.md +1 -1
- package/skills/content/article-review/SKILL.md +1 -1
- package/skills/content/article-to-image/SKILL.md +1 -1
- package/skills/content/flowchart-svg/SKILL.md +1 -1
- package/skills/content/image-gen/SKILL.md +1 -1
- package/skills/content/mermaid/SKILL.md +1 -1
- package/skills/content/wechat-article-writer/SKILL.md +1 -1
- package/skills/content/wechat-publisher/SKILL.md +1 -1
- package/skills/content/wechat-search/SKILL.md +1 -1
- package/skills/content/xai-search/SKILL.md +1 -1
- package/skills/core/distill-writing/SKILL.md +7 -1
- package/skills/core/doc-to-md/SKILL.md +1 -1
- package/skills/core/editorial-review/SKILL.md +1 -1
- package/skills/core/house-style/SKILL.md +1 -1
- package/skills/core/house-style/references/persistence.md +8 -3
- package/skills/core/house-style/scripts/init-guidance.mjs +1 -1
- package/skills/core/plain-language/SKILL.md +1 -1
- package/skills/core/polish-cn/SKILL.md +1 -1
- package/skills/core/reg-parser/SKILL.md +1 -1
- package/skills/core/terminology/SKILL.md +1 -1
- package/skills/core/terminology/references/init.md +11 -0
- package/skills/core/write-doc/SKILL.md +1 -1
- package/skills/core/write-report/SKILL.md +1 -1
- package/skills/engineering/better-review/SKILL.md +1 -1
- package/skills/engineering/e2e-test/SKILL.md +1 -1
- package/skills/engineering/engineering/SKILL.md +25 -4
- package/skills/engineering/engineering/references/init-scope.md +10 -3
- package/skills/engineering/engineering/references/scope-management.md +11 -0
- package/skills/engineering/engineering/scripts/init.mjs +65 -17
- package/skills/engineering/engineering/scripts/persist-conventions-core.mjs +231 -21
- package/skills/engineering/engineering/scripts/persist-conventions.mjs +48 -21
- package/skills/engineering/engineering-runtime/SKILL.md +1 -1
- package/skills/engineering/engineering-runtime/coherence/workflow.json +13 -13
- package/skills/engineering/ensure-coverage/SKILL.md +1 -1
- package/skills/engineering/golden-path/SKILL.md +1 -1
- package/skills/engineering/harness-runtime/SKILL.md +1 -1
- package/skills/engineering/resolve-issues/SKILL.md +1 -1
- package/skills/engineering/resolve-issues/references/exploration.md +1 -1
- package/skills/engineering/resolve-issues/references/review.md +27 -4
- package/skills/engineering/resolve-release/SKILL.md +1 -1
- package/skills/engineering/rush-issues/SKILL.md +1 -1
- package/skills/engineering/rush-release/SKILL.md +1 -1
- package/skills/engineering/test-quality/SKILL.md +3 -1
- package/skills/engineering/test-quality/scripts/persist-conventions-core.mjs +231 -21
- package/skills/engineering/test-quality/scripts/persist-conventions.mjs +13 -1
- package/skills/risk-compliance/compliance-audit/SKILL.md +1 -1
- package/skills/risk-compliance/contract-compare/SKILL.md +1 -1
- package/skills/risk-compliance/contract-review/SKILL.md +1 -1
- package/skills/risk-compliance/doc-authenticity/SKILL.md +1 -1
- package/skills/risk-compliance/fraud-detection/SKILL.md +1 -1
- package/skills/risk-compliance/payment-analysis/SKILL.md +1 -1
- package/skills/risk-compliance/portfolio-payment-analysis/SKILL.md +1 -1
- package/skills/risk-compliance/similar-domain/SKILL.md +1 -1
- package/skills/risk-compliance/tld-detection/SKILL.md +1 -1
- package/skills/app-dev/android-foundation/SKILL.md +0 -248
- package/skills/app-dev/android-foundation/references/delivery-focus.md +0 -31
- package/skills/app-dev/android-foundation/references/feature-work.md +0 -50
- package/skills/app-dev/android-foundation/references/setup.md +0 -188
- package/skills/app-dev/android-foundation/references/workspace.md +0 -81
- /package/skills/app-dev/{android-foundation → android-development}/LICENSE +0 -0
- /package/skills/app-dev/{android-foundation → android-development}/assets/android-ci-job.yml +0 -0
- /package/skills/app-dev/{android-foundation → android-development}/release-check.mjs +0 -0
- /package/skills/app-dev/{android-foundation → android-development}/scripts/device-run.sh +0 -0
- /package/skills/app-dev/{android-foundation → android-development}/scripts/workspace-runtime/bin/workspaces.mjs +0 -0
- /package/skills/app-dev/{android-foundation → android-development}/scripts/workspace-runtime/native-workspace.mjs +0 -0
- /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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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
|
|
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
|
-
|
|
31
|
-
|
|
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
|
|
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
|
|
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.
|