@1aboveio/skills 1.0.2 → 1.0.3
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/README.md +1 -1
- package/package.json +3 -2
- package/skills/app-dev/airflow/SKILL.md +1 -1
- package/skills/app-dev/android-foundation/SKILL.md +1 -1
- 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 +1 -1
- package/skills/app-dev/data-catalog/scripts/persist-conventions-core.mjs +3 -2
- package/skills/app-dev/data-quality/SKILL.md +1 -1
- package/skills/app-dev/ios-foundation/SKILL.md +1 -1
- 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 +1 -1
- package/skills/cicd/github-flow/scripts/persist-conventions-core.mjs +3 -2
- 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 +1 -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/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/write-doc/SKILL.md +1 -1
- package/skills/engineering/better-review/SKILL.md +1 -1
- package/skills/engineering/contributing/LICENSE +3 -0
- package/skills/engineering/contributing/SKILL.md +70 -0
- package/skills/engineering/contributing/references/tracker-conventions.md +18 -0
- package/skills/engineering/contributing/release-check.mjs +93 -0
- package/skills/engineering/contributing/scripts/persist-conventions-core.mjs +118 -0
- package/skills/engineering/contributing/scripts/persist-conventions.mjs +84 -0
- package/skills/engineering/contributing/scripts/persist-conventions.test.mjs +118 -0
- package/skills/engineering/e2e-test/SKILL.md +1 -1
- package/skills/engineering/engineering-runtime/SKILL.md +1 -1
- package/skills/engineering/engineering-runtime/coherence/workflow.json +29 -14
- package/skills/engineering/engineering-runtime/scripts/workflow-policy.mjs +1 -0
- 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-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 +1 -1
- package/skills/engineering/test-quality/scripts/persist-conventions-core.mjs +3 -2
- 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-analysis/SKILL.md +1 -1
- package/skills/risk-compliance/payment-analysis/SKILL.md +1 -1
- package/skills/risk-compliance/report-writing/SKILL.md +1 -1
- package/skills/risk-compliance/similar-domain/SKILL.md +1 -1
- package/skills/risk-compliance/tld-detection/SKILL.md +1 -1
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawnSync } from 'node:child_process';
|
|
3
|
+
import { mkdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import { homedir } from 'node:os';
|
|
5
|
+
import { dirname, join } from 'node:path';
|
|
6
|
+
import { fileURLToPath } from 'node:url';
|
|
7
|
+
|
|
8
|
+
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
9
|
+
const WEEK_MS = 7 * DAY_MS;
|
|
10
|
+
const here = dirname(fileURLToPath(import.meta.url));
|
|
11
|
+
|
|
12
|
+
function readJson(path, fallback = {}) {
|
|
13
|
+
try { return JSON.parse(readFileSync(path, 'utf8')); } catch { return fallback; }
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function frontmatterValue(text, key) {
|
|
17
|
+
const frontmatter = /^---\n([\s\S]*?)\n---/.exec(text)?.[1] ?? '';
|
|
18
|
+
const match = new RegExp('^' + key + ':\\s*([^\\n]+)$', 'm').exec(frontmatter);
|
|
19
|
+
return match?.[1].trim().replace(/^['"]|['"]$/g, '') ?? null;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function repositoryOf(source) {
|
|
23
|
+
const value = String(source ?? '').trim();
|
|
24
|
+
const match = /^(?:git@github\.com:|https:\/\/github\.com\/)?([A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+?)(?:\.git)?$/.exec(value);
|
|
25
|
+
return match?.[1] ?? null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function stableVersion(value) {
|
|
29
|
+
const match = /^v?(\d+)\.(\d+)\.(\d+)$/.exec(String(value ?? '').trim());
|
|
30
|
+
return match ? match.slice(1).map(Number) : null;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function newer(left, right) {
|
|
34
|
+
for (let index = 0; index < 3; index += 1) {
|
|
35
|
+
if (left[index] !== right[index]) return left[index] > right[index];
|
|
36
|
+
}
|
|
37
|
+
return false;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function writeCache(path, value) {
|
|
41
|
+
try {
|
|
42
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
43
|
+
writeFileSync(path, JSON.stringify(value));
|
|
44
|
+
} catch { /* A read-only cache must not block skill use. */ }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function main(now = Date.now()) {
|
|
48
|
+
if (process.env.ONEABOVEIO_NO_UPDATE_CHECK) return;
|
|
49
|
+
const skill = readFileSync(join(here, 'SKILL.md'), 'utf8');
|
|
50
|
+
const name = frontmatterValue(skill, 'name');
|
|
51
|
+
const localVersion = stableVersion(frontmatterValue(skill, 'release-version'));
|
|
52
|
+
if (!name || !localVersion) return;
|
|
53
|
+
|
|
54
|
+
const lock = readJson(join(process.env.HOME || homedir(), '.agents', '.skill-lock.json'));
|
|
55
|
+
const entry = lock.skills?.[name];
|
|
56
|
+
const repository = repositoryOf(entry?.sourceUrl || entry?.source);
|
|
57
|
+
if (!repository) return;
|
|
58
|
+
|
|
59
|
+
const cacheRoot = process.env.XDG_CACHE_HOME || join(process.env.HOME || homedir(), '.cache');
|
|
60
|
+
const cachePath = join(cacheRoot, '1aboveio-skills', 'release-check.json');
|
|
61
|
+
const all = readJson(cachePath);
|
|
62
|
+
const cache = all[repository] && typeof all[repository] === 'object' ? all[repository] : {};
|
|
63
|
+
|
|
64
|
+
if (!cache.lastCheck || now - cache.lastCheck >= DAY_MS) {
|
|
65
|
+
const result = spawnSync('gh', ['api', 'repos/' + repository + '/releases/latest', '--jq', '.tag_name'], {
|
|
66
|
+
encoding: 'utf8',
|
|
67
|
+
stdio: ['ignore', 'pipe', 'ignore'],
|
|
68
|
+
timeout: 1500,
|
|
69
|
+
});
|
|
70
|
+
cache.lastCheck = now;
|
|
71
|
+
if (result.status === 0 && stableVersion(result.stdout)) cache.latestTag = result.stdout.trim();
|
|
72
|
+
else delete cache.latestTag;
|
|
73
|
+
all[repository] = cache;
|
|
74
|
+
writeCache(cachePath, all);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const latestVersion = stableVersion(cache.latestTag);
|
|
78
|
+
if (!latestVersion || !newer(latestVersion, localVersion)) return;
|
|
79
|
+
if (cache.notifiedTag === cache.latestTag && cache.notifiedAt && now - cache.notifiedAt < WEEK_MS) return;
|
|
80
|
+
|
|
81
|
+
cache.notifiedTag = cache.latestTag;
|
|
82
|
+
cache.notifiedAt = now;
|
|
83
|
+
all[repository] = cache;
|
|
84
|
+
writeCache(cachePath, all);
|
|
85
|
+
const command = `npx skills@1.5.23 add '${repository}#${cache.latestTag}' --global --agent universal claude-code --skill ${name} --yes`;
|
|
86
|
+
process.stdout.write(
|
|
87
|
+
`UPDATE_AVAILABLE: A newer 1AboveIO skills release (${cache.latestTag}) is available; this skill is `
|
|
88
|
+
+ `v${localVersion.join('.')}. Ask once whether to refresh ${name}. On approval run \`${command}\`. `
|
|
89
|
+
+ 'The refreshed instructions apply next session. Either way, continue the current task and do not ask again this session.\n',
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
try { main(); } catch { /* Update checks are advisory and always fail silent. */ }
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// persist-conventions-core — write one skill's section into a repo's
|
|
2
|
+
// CONTRIBUTING.md, point AGENTS.md at the file, and make CLAUDE.md a symlink to
|
|
3
|
+
// AGENTS.md (Claude Code reads CLAUDE.md, not AGENTS.md). Import-only: each skill's
|
|
4
|
+
// `persist-conventions.mjs` is the always-running CLI wrapper that names its
|
|
5
|
+
// section, so no main-module guard here.
|
|
6
|
+
//
|
|
7
|
+
// Byte-identical copies live in test-quality (Testing conventions), github-flow
|
|
8
|
+
// (CI conventions, Delivery), data-catalog (Data catalog conventions) and
|
|
9
|
+
// contributing (Issue tracking);
|
|
10
|
+
// skills/conventions-sections.test.mjs keeps them identical and proves the sections
|
|
11
|
+
// coexist in one file.
|
|
12
|
+
//
|
|
13
|
+
// A section sits between `conventions:<id>` markers, so a re-run replaces only
|
|
14
|
+
// that block and keeps everything else, including other skills' sections.
|
|
15
|
+
// Nothing is written without --write: the user triggers the write, not an agent.
|
|
16
|
+
|
|
17
|
+
import fs from 'node:fs'
|
|
18
|
+
import path from 'node:path'
|
|
19
|
+
|
|
20
|
+
export const CONTRIBUTING_FILE = 'CONTRIBUTING.md'
|
|
21
|
+
const MENTIONS_CONTRIBUTING = /(?:^|[^\w-])CONTRIBUTING\.md/
|
|
22
|
+
|
|
23
|
+
export const AGENTS_SECTION = [
|
|
24
|
+
'## Contributing',
|
|
25
|
+
'',
|
|
26
|
+
`Follow [${CONTRIBUTING_FILE}](${CONTRIBUTING_FILE}) when writing, reviewing, or delivering code, or creating issues: tests, CI, data definitions, issue tracking, and what to do after each commit.`,
|
|
27
|
+
'',
|
|
28
|
+
].join('\n')
|
|
29
|
+
|
|
30
|
+
export function markers(id) {
|
|
31
|
+
return [`<!-- conventions:${id}:start -->`, `<!-- conventions:${id}:end -->`]
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function sectionBlock({ id, owner, body }) {
|
|
35
|
+
const [start, end] = markers(id)
|
|
36
|
+
const note = `<!-- Written by the ${owner} skill: re-run its persist-conventions.mjs --write to update; edit outside these markers. -->`
|
|
37
|
+
return `${start}\n${note}\n\n${body.trim()}\n${end}\n`
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Pure: the new CONTRIBUTING.md text given the current one (or null).
|
|
41
|
+
export function renderConventions(current, id, block) {
|
|
42
|
+
if (current == null) return `# Contributing\n\n${block}`
|
|
43
|
+
const [start, end] = markers(id)
|
|
44
|
+
const from = current.indexOf(start)
|
|
45
|
+
const to = current.indexOf(end)
|
|
46
|
+
if (from !== -1 && to > from) {
|
|
47
|
+
const after = current.slice(to + end.length).replace(/^\r?\n/, '')
|
|
48
|
+
return current.slice(0, from) + block + after
|
|
49
|
+
}
|
|
50
|
+
return `${current.replace(/\s*$/, '')}\n\n${block}`
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Pure: the new AGENTS.md text given the current one (or null).
|
|
54
|
+
export function renderAgents(current) {
|
|
55
|
+
if (current == null) return `# AGENTS.md\n\n${AGENTS_SECTION}`
|
|
56
|
+
if (MENTIONS_CONTRIBUTING.test(current)) return current
|
|
57
|
+
return `${current.replace(/\s*$/, '')}\n\n${AGENTS_SECTION}`
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// A missing CLAUDE.md becomes a symlink to AGENTS.md.
|
|
61
|
+
export const LINK_TO_AGENTS = Symbol('link CLAUDE.md to AGENTS.md')
|
|
62
|
+
|
|
63
|
+
// Pure: the new CLAUDE.md text given the current one (or null). A missing file
|
|
64
|
+
// becomes LINK_TO_AGENTS; a regular file keeps its content and gains the pointer
|
|
65
|
+
// unless it already names AGENTS.md or CONTRIBUTING.md.
|
|
66
|
+
export function renderClaude(current) {
|
|
67
|
+
if (current == null) return LINK_TO_AGENTS
|
|
68
|
+
if (/(?:^|[^\w-])(?:AGENTS|CONTRIBUTING)\.md/.test(current)) return current
|
|
69
|
+
return `${current.replace(/\s*$/, '')}\n\n${AGENTS_SECTION}`
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
export function persist(repoRoot, section, { write = false } = {}) {
|
|
73
|
+
const block = sectionBlock(section)
|
|
74
|
+
const plan = [
|
|
75
|
+
[CONTRIBUTING_FILE, (text) => renderConventions(text, section.id, block)],
|
|
76
|
+
['AGENTS.md', renderAgents],
|
|
77
|
+
['CLAUDE.md', renderClaude],
|
|
78
|
+
]
|
|
79
|
+
return plan.map(([name, render]) => {
|
|
80
|
+
const file = path.join(repoRoot, name)
|
|
81
|
+
if (fs.lstatSync(file, { throwIfNoEntry: false })?.isSymbolicLink()) return { file: name, action: 'unchanged' }
|
|
82
|
+
const current = fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : null
|
|
83
|
+
const next = render(current)
|
|
84
|
+
const action = current == null ? 'create' : next === current ? 'unchanged' : 'update'
|
|
85
|
+
if (next === LINK_TO_AGENTS) {
|
|
86
|
+
if (write) fs.symlinkSync('AGENTS.md', file)
|
|
87
|
+
return { file: name, action: 'link' }
|
|
88
|
+
}
|
|
89
|
+
if (write && action !== 'unchanged') fs.writeFileSync(file, next)
|
|
90
|
+
return { file: name, action }
|
|
91
|
+
})
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// `section` = { id, owner, title, body }. Exit 0 on success, 2 on a usage error.
|
|
95
|
+
export function runCli(argv, section, { cwd = process.cwd(), log = console.log } = {}) {
|
|
96
|
+
let repoRoot = cwd
|
|
97
|
+
let write = false
|
|
98
|
+
for (let i = 0; i < argv.length; i++) {
|
|
99
|
+
if (argv[i] === '--write') write = true
|
|
100
|
+
else if (argv[i] === '--repo-root' && argv[i + 1]) repoRoot = path.resolve(cwd, argv[++i])
|
|
101
|
+
else {
|
|
102
|
+
log(`persist-conventions: unknown argument ${argv[i]}`)
|
|
103
|
+
return 2
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
if (!fs.statSync(repoRoot, { throwIfNoEntry: false })?.isDirectory()) {
|
|
107
|
+
log(`persist-conventions: not a directory: ${repoRoot}`)
|
|
108
|
+
return 2
|
|
109
|
+
}
|
|
110
|
+
const results = persist(repoRoot, section, { write })
|
|
111
|
+
const done = { create: 'created', link: 'linked to AGENTS.md', update: 'updated', unchanged: 'unchanged' }
|
|
112
|
+
for (const r of results) log(` ${r.file}: ${write || r.action === 'unchanged' ? done[r.action] : `would ${r.action}`}`)
|
|
113
|
+
const changed = results.filter((r) => r.action !== 'unchanged').length
|
|
114
|
+
log(write
|
|
115
|
+
? `persist-conventions: ${section.title}: ${changed} file(s) written, ${results.length - changed} unchanged`
|
|
116
|
+
: `persist-conventions: ${section.title}: preview only, ${changed} file(s) would change; re-run with --write to write`)
|
|
117
|
+
return 0
|
|
118
|
+
}
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
// persist-conventions — the tracker routing rules as the `## Issue tracking`
|
|
2
|
+
// section of a repo's CONTRIBUTING.md (../SKILL.md, First use in a repo).
|
|
3
|
+
// Previews unless --write; --if-missing leaves an existing section alone.
|
|
4
|
+
//
|
|
5
|
+
// node persist-conventions.mjs [--repo-root <dir>] [--github-repo <owner/name>]
|
|
6
|
+
// [--linear-project <name>]... [--if-missing] [--write]
|
|
7
|
+
//
|
|
8
|
+
// The GitHub repo defaults to the `origin` remote of --repo-root.
|
|
9
|
+
import fs from 'node:fs'
|
|
10
|
+
import path from 'node:path'
|
|
11
|
+
import { spawnSync } from 'node:child_process'
|
|
12
|
+
import { fileURLToPath } from 'node:url'
|
|
13
|
+
import { CONTRIBUTING_FILE, LINK_TO_AGENTS, markers, renderAgents, renderClaude, runCli } from './persist-conventions-core.mjs'
|
|
14
|
+
|
|
15
|
+
const ID = 'tracker'
|
|
16
|
+
const TEMPLATE = fs.readFileSync(fileURLToPath(new URL('../references/tracker-conventions.md', import.meta.url)), 'utf8')
|
|
17
|
+
|
|
18
|
+
function githubRepoFromRemote(url) {
|
|
19
|
+
const m = /github\.com[:/]([\w.-]+\/[\w.-]+?)(?:\.git)?\/?$/.exec(url.trim())
|
|
20
|
+
return m ? m[1] : null
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function renderBody(githubRepo, linearProjects) {
|
|
24
|
+
const linear = linearProjects.length
|
|
25
|
+
? ` (projects: ${linearProjects.join(', ')})`
|
|
26
|
+
: ''
|
|
27
|
+
return TEMPLATE.replaceAll('{{githubRepo}}', githubRepo).replaceAll('{{linearProjects}}', linear)
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// The section stays as it is; only a missing AGENTS.md index or CLAUDE.md link is added.
|
|
31
|
+
function indexOnly(repoRoot, write, log) {
|
|
32
|
+
let changed = 0
|
|
33
|
+
for (const [name, render] of [['AGENTS.md', renderAgents], ['CLAUDE.md', renderClaude]]) {
|
|
34
|
+
const file = path.join(repoRoot, name)
|
|
35
|
+
if (fs.lstatSync(file, { throwIfNoEntry: false })?.isSymbolicLink()) continue
|
|
36
|
+
const current = fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : null
|
|
37
|
+
const next = render(current)
|
|
38
|
+
if (next === current) continue
|
|
39
|
+
changed++
|
|
40
|
+
log(` ${name}: ${write ? 'written' : 'would change'}`)
|
|
41
|
+
if (!write) continue
|
|
42
|
+
if (next === LINK_TO_AGENTS) fs.symlinkSync('AGENTS.md', file)
|
|
43
|
+
else fs.writeFileSync(file, next)
|
|
44
|
+
}
|
|
45
|
+
return changed ? `${changed} index file(s) ${write ? 'written' : 'would change'}` : 'nothing written'
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function main(argv, { log = console.log } = {}) {
|
|
49
|
+
const rest = []
|
|
50
|
+
const linearProjects = []
|
|
51
|
+
let githubRepo = null
|
|
52
|
+
let ifMissing = false
|
|
53
|
+
let repoRoot = process.cwd()
|
|
54
|
+
for (let i = 0; i < argv.length; i++) {
|
|
55
|
+
const arg = argv[i]
|
|
56
|
+
if (arg === '--linear-project' && argv[i + 1]) linearProjects.push(argv[++i])
|
|
57
|
+
else if (arg === '--github-repo' && argv[i + 1]) githubRepo = argv[++i]
|
|
58
|
+
else if (arg === '--if-missing') ifMissing = true
|
|
59
|
+
else {
|
|
60
|
+
if (arg === '--repo-root' && argv[i + 1]) repoRoot = path.resolve(argv[i + 1])
|
|
61
|
+
rest.push(arg)
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
if (!githubRepo) {
|
|
65
|
+
const remote = spawnSync('git', ['-C', repoRoot, 'remote', 'get-url', 'origin'], { encoding: 'utf8' })
|
|
66
|
+
githubRepo = remote.status === 0 ? githubRepoFromRemote(remote.stdout) : null
|
|
67
|
+
}
|
|
68
|
+
if (!githubRepo) {
|
|
69
|
+
log('persist-conventions: no GitHub origin remote; pass --github-repo <owner/name>')
|
|
70
|
+
return 2
|
|
71
|
+
}
|
|
72
|
+
if (ifMissing) {
|
|
73
|
+
const file = path.join(repoRoot, CONTRIBUTING_FILE)
|
|
74
|
+
const current = fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : ''
|
|
75
|
+
if (current.includes(markers(ID)[0])) {
|
|
76
|
+
log(`persist-conventions: Issue tracking: section already present; ${indexOnly(repoRoot, argv.includes('--write'), log)}`)
|
|
77
|
+
return 0
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
const section = { id: ID, owner: 'contributing', title: 'Issue tracking', body: renderBody(githubRepo, linearProjects) }
|
|
81
|
+
return runCli(rest, section, { log })
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
process.exitCode = main(process.argv.slice(2))
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// Tests for persist-conventions: the real CLI against real files in a temp repo.
|
|
2
|
+
import { test } from 'node:test'
|
|
3
|
+
import assert from 'node:assert/strict'
|
|
4
|
+
import { spawnSync } from 'node:child_process'
|
|
5
|
+
import { lstatSync, mkdtempSync, readFileSync, readlinkSync, rmSync, writeFileSync } from 'node:fs'
|
|
6
|
+
import { tmpdir } from 'node:os'
|
|
7
|
+
import { join } from 'node:path'
|
|
8
|
+
import { fileURLToPath } from 'node:url'
|
|
9
|
+
|
|
10
|
+
import { markers } from './persist-conventions-core.mjs'
|
|
11
|
+
|
|
12
|
+
const CLI = fileURLToPath(new URL('./persist-conventions.mjs', import.meta.url))
|
|
13
|
+
const [START, END] = markers('tracker')
|
|
14
|
+
|
|
15
|
+
function withRepo(fn, remote) {
|
|
16
|
+
const dir = mkdtempSync(join(tmpdir(), 'contributing-'))
|
|
17
|
+
try {
|
|
18
|
+
if (remote) {
|
|
19
|
+
spawnSync('git', ['init', '-q', dir])
|
|
20
|
+
spawnSync('git', ['-C', dir, 'remote', 'add', 'origin', remote])
|
|
21
|
+
}
|
|
22
|
+
return fn(dir, (name) => readFileSync(join(dir, name), 'utf8'))
|
|
23
|
+
} finally {
|
|
24
|
+
rmSync(dir, { recursive: true, force: true })
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function run(dir, ...args) {
|
|
29
|
+
const res = spawnSync(process.execPath, [CLI, '--repo-root', dir, ...args], { encoding: 'utf8' })
|
|
30
|
+
return { code: res.status, out: res.stdout }
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
test('--write creates CONTRIBUTING.md with the section, the repo from origin, and the Linear projects', () => {
|
|
34
|
+
withRepo((dir, read) => {
|
|
35
|
+
const { code } = run(dir, '--linear-project', 'Checkout Risk', '--write')
|
|
36
|
+
assert.equal(code, 0)
|
|
37
|
+
const text = read('CONTRIBUTING.md')
|
|
38
|
+
assert.ok(text.startsWith('# Contributing\n'))
|
|
39
|
+
assert.ok(text.includes(START) && text.includes(END))
|
|
40
|
+
assert.match(text, /GitHub issues on `acme\/widgets`/)
|
|
41
|
+
assert.match(text, /Linear \(projects: Checkout Risk\)/)
|
|
42
|
+
assert.match(text, /gh issue list --repo acme\/widgets/)
|
|
43
|
+
assert.doesNotMatch(text, /\{\{/)
|
|
44
|
+
}, 'git@github.com:acme/widgets.git')
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
test('an https remote and an explicit --github-repo both name the repo', () => {
|
|
48
|
+
withRepo((dir, read) => {
|
|
49
|
+
assert.equal(run(dir, '--write').code, 0)
|
|
50
|
+
assert.match(read('CONTRIBUTING.md'), /GitHub issues on `acme\/widgets`\. Product work is tracked in Linear\./)
|
|
51
|
+
}, 'https://github.com/acme/widgets')
|
|
52
|
+
withRepo((dir, read) => {
|
|
53
|
+
assert.equal(run(dir, '--github-repo', 'acme/other', '--write').code, 0)
|
|
54
|
+
assert.match(read('CONTRIBUTING.md'), /`acme\/other`/)
|
|
55
|
+
})
|
|
56
|
+
})
|
|
57
|
+
|
|
58
|
+
test('without a GitHub remote or --github-repo it refuses with exit 2 and writes nothing', () => {
|
|
59
|
+
withRepo((dir) => {
|
|
60
|
+
const { code, out } = run(dir, '--write')
|
|
61
|
+
assert.equal(code, 2)
|
|
62
|
+
assert.match(out, /--github-repo/)
|
|
63
|
+
assert.throws(() => readFileSync(join(dir, 'CONTRIBUTING.md')))
|
|
64
|
+
})
|
|
65
|
+
})
|
|
66
|
+
|
|
67
|
+
test('--if-missing appends to an existing CONTRIBUTING.md once, then leaves the section alone', () => {
|
|
68
|
+
withRepo((dir, read) => {
|
|
69
|
+
writeFileSync(join(dir, 'CONTRIBUTING.md'), '# Contributing\n\nRun the tests.\n')
|
|
70
|
+
assert.equal(run(dir, '--if-missing', '--write').code, 0)
|
|
71
|
+
const first = read('CONTRIBUTING.md')
|
|
72
|
+
assert.ok(first.startsWith('# Contributing\n\nRun the tests.\n'))
|
|
73
|
+
assert.ok(first.includes(START))
|
|
74
|
+
|
|
75
|
+
const edited = first.replace('acme/widgets`. Product', 'acme/widgets`. Hand-edited. Product')
|
|
76
|
+
writeFileSync(join(dir, 'CONTRIBUTING.md'), edited)
|
|
77
|
+
const { code, out } = run(dir, '--if-missing', '--linear-project', 'Other', '--write')
|
|
78
|
+
assert.equal(code, 0)
|
|
79
|
+
assert.match(out, /already present/)
|
|
80
|
+
assert.equal(read('CONTRIBUTING.md'), edited)
|
|
81
|
+
}, 'git@github.com:acme/widgets.git')
|
|
82
|
+
})
|
|
83
|
+
|
|
84
|
+
test('without --write it previews and writes nothing', () => {
|
|
85
|
+
withRepo((dir) => {
|
|
86
|
+
const { code, out } = run(dir)
|
|
87
|
+
assert.equal(code, 0)
|
|
88
|
+
assert.match(out, /CONTRIBUTING\.md: would create/)
|
|
89
|
+
assert.throws(() => readFileSync(join(dir, 'CONTRIBUTING.md')))
|
|
90
|
+
}, 'git@github.com:acme/widgets.git')
|
|
91
|
+
})
|
|
92
|
+
|
|
93
|
+
test('the first run indexes CONTRIBUTING.md from AGENTS.md and links CLAUDE.md, once', () => {
|
|
94
|
+
withRepo((dir, read) => {
|
|
95
|
+
assert.equal(run(dir, '--if-missing', '--write').code, 0)
|
|
96
|
+
assert.match(read('AGENTS.md'), /\[CONTRIBUTING\.md\]\(CONTRIBUTING\.md\).*issue tracking/)
|
|
97
|
+
assert.ok(lstatSync(join(dir, 'CLAUDE.md')).isSymbolicLink())
|
|
98
|
+
assert.equal(readlinkSync(join(dir, 'CLAUDE.md')), 'AGENTS.md')
|
|
99
|
+
}, 'git@github.com:acme/widgets.git')
|
|
100
|
+
withRepo((dir, read) => {
|
|
101
|
+
writeFileSync(join(dir, 'AGENTS.md'), '# Agents\n\nSee CONTRIBUTING.md for everything.\n')
|
|
102
|
+
assert.equal(run(dir, '--if-missing', '--write').code, 0)
|
|
103
|
+
assert.equal(read('AGENTS.md'), '# Agents\n\nSee CONTRIBUTING.md for everything.\n')
|
|
104
|
+
}, 'git@github.com:acme/widgets.git')
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
test('--if-missing restores a missing AGENTS.md index even when the section is already present', () => {
|
|
108
|
+
withRepo((dir, read) => {
|
|
109
|
+
assert.equal(run(dir, '--write').code, 0)
|
|
110
|
+
const section = read('CONTRIBUTING.md')
|
|
111
|
+
writeFileSync(join(dir, 'AGENTS.md'), '# Agents\n')
|
|
112
|
+
const { code, out } = run(dir, '--if-missing', '--write')
|
|
113
|
+
assert.equal(code, 0)
|
|
114
|
+
assert.match(out, /already present; 1 index file\(s\) written/)
|
|
115
|
+
assert.match(read('AGENTS.md'), /^# Agents\n\n## Contributing\n/)
|
|
116
|
+
assert.equal(read('CONTRIBUTING.md'), section)
|
|
117
|
+
}, 'git@github.com:acme/widgets.git')
|
|
118
|
+
})
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: e2e-test
|
|
3
3
|
description: "Playwright end-to-end standard: the 3-tier suite (render-health / functional / journey), generated route manifest, and presentation sweep. Use when browser routes or user flows ship or change, or a Playwright test flakes. NOT for non-browser tests, defining what to test (ensure-coverage), the curated ALIVE/DEAD golden-path gate (golden-path), or CI workflow YAML (github-actions)."
|
|
4
|
-
release-version: 0.
|
|
4
|
+
release-version: 0.40.0
|
|
5
5
|
---
|
|
6
6
|
**Updates:** Once/session run `node <skill-base-dir>/release-check.mjs`.
|
|
7
7
|
# E2E Testing Standard
|
|
@@ -3,7 +3,7 @@ name: engineering-runtime
|
|
|
3
3
|
description: Internal support package for shared Engineering Workflow executable behavior. Never select or invoke this package as a user capability.
|
|
4
4
|
disable-model-invocation: true
|
|
5
5
|
user-invocable: false
|
|
6
|
-
release-version: 0.
|
|
6
|
+
release-version: 0.40.0
|
|
7
7
|
---
|
|
8
8
|
**Updates:** Once/session run `node <skill-base-dir>/release-check.mjs`.
|
|
9
9
|
# Engineering Runtime
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
"golden-path",
|
|
12
12
|
"test-quality",
|
|
13
13
|
"better-review",
|
|
14
|
+
"contributing",
|
|
14
15
|
"engineering-runtime"
|
|
15
16
|
],
|
|
16
17
|
"dependencies": [
|
|
@@ -368,7 +369,7 @@
|
|
|
368
369
|
"repository": "1aboveio/ai",
|
|
369
370
|
"revision": "main"
|
|
370
371
|
},
|
|
371
|
-
"contentDigest": "
|
|
372
|
+
"contentDigest": "cce3cfb21f16ea64b5b008ef9970b1cdf4cf2b1410f32105e3b305821acf1c1f",
|
|
372
373
|
"digestExcludes": []
|
|
373
374
|
},
|
|
374
375
|
{
|
|
@@ -381,7 +382,7 @@
|
|
|
381
382
|
"repository": "1aboveio/ai",
|
|
382
383
|
"revision": "main"
|
|
383
384
|
},
|
|
384
|
-
"contentDigest": "
|
|
385
|
+
"contentDigest": "c87e5250fe86e4e01f5bf8d4db59f77893de385bba4fc34742429147b22b5253",
|
|
385
386
|
"digestExcludes": []
|
|
386
387
|
},
|
|
387
388
|
{
|
|
@@ -394,7 +395,7 @@
|
|
|
394
395
|
"repository": "1aboveio/ai",
|
|
395
396
|
"revision": "main"
|
|
396
397
|
},
|
|
397
|
-
"contentDigest": "
|
|
398
|
+
"contentDigest": "5e449cef39b924bddd4b24cf64ea63b5b93e745194100d797ddd830bd00a97b7",
|
|
398
399
|
"digestExcludes": []
|
|
399
400
|
},
|
|
400
401
|
{
|
|
@@ -407,7 +408,7 @@
|
|
|
407
408
|
"repository": "1aboveio/ai",
|
|
408
409
|
"revision": "main"
|
|
409
410
|
},
|
|
410
|
-
"contentDigest": "
|
|
411
|
+
"contentDigest": "35b0a26911041468eda83f0b3829f343806d39292a381984de39fef2f813ec4b",
|
|
411
412
|
"digestExcludes": []
|
|
412
413
|
},
|
|
413
414
|
{
|
|
@@ -420,7 +421,7 @@
|
|
|
420
421
|
"repository": "1aboveio/ai",
|
|
421
422
|
"revision": "main"
|
|
422
423
|
},
|
|
423
|
-
"contentDigest": "
|
|
424
|
+
"contentDigest": "b9e265715330e802d711d0a47012bc32c91d882656e227530f51b08976672d20",
|
|
424
425
|
"digestExcludes": []
|
|
425
426
|
},
|
|
426
427
|
{
|
|
@@ -433,7 +434,7 @@
|
|
|
433
434
|
"repository": "1aboveio/ai",
|
|
434
435
|
"revision": "main"
|
|
435
436
|
},
|
|
436
|
-
"contentDigest": "
|
|
437
|
+
"contentDigest": "6b4736a329dcf2d35eeb74ea019aaa39b82fb129fbd735e55d68c3bab063d046",
|
|
437
438
|
"digestExcludes": []
|
|
438
439
|
},
|
|
439
440
|
{
|
|
@@ -446,7 +447,7 @@
|
|
|
446
447
|
"repository": "1aboveio/ai",
|
|
447
448
|
"revision": "main"
|
|
448
449
|
},
|
|
449
|
-
"contentDigest": "
|
|
450
|
+
"contentDigest": "d2645ec3b2b28cc08cd4026d6ffb7e56d43e31031c024004b79a07a5fc59bb25",
|
|
450
451
|
"digestExcludes": []
|
|
451
452
|
},
|
|
452
453
|
{
|
|
@@ -459,7 +460,7 @@
|
|
|
459
460
|
"repository": "1aboveio/ai",
|
|
460
461
|
"revision": "main"
|
|
461
462
|
},
|
|
462
|
-
"contentDigest": "
|
|
463
|
+
"contentDigest": "61708f1a0dd9024f7cc1c99661c074cee6299587220f22ce197d2ec312a79370",
|
|
463
464
|
"digestExcludes": []
|
|
464
465
|
},
|
|
465
466
|
{
|
|
@@ -472,7 +473,7 @@
|
|
|
472
473
|
"repository": "1aboveio/ai",
|
|
473
474
|
"revision": "main"
|
|
474
475
|
},
|
|
475
|
-
"contentDigest": "
|
|
476
|
+
"contentDigest": "ece6ce482174817c58344af70149085a594df54b8e70e8535ec267e26f5e00ab",
|
|
476
477
|
"digestExcludes": []
|
|
477
478
|
},
|
|
478
479
|
{
|
|
@@ -485,7 +486,20 @@
|
|
|
485
486
|
"repository": "1aboveio/ai",
|
|
486
487
|
"revision": "main"
|
|
487
488
|
},
|
|
488
|
-
"contentDigest": "
|
|
489
|
+
"contentDigest": "ddaf8300548bcf2628fa6b6f6933885e05dc0451f5ca0e9bb6629fcca2c673bb",
|
|
490
|
+
"digestExcludes": []
|
|
491
|
+
},
|
|
492
|
+
{
|
|
493
|
+
"installName": "contributing",
|
|
494
|
+
"ownership": "owned",
|
|
495
|
+
"sourcePath": "skills/engineering/contributing",
|
|
496
|
+
"expectedSource": {
|
|
497
|
+
"id": "first-party",
|
|
498
|
+
"type": "first-party",
|
|
499
|
+
"repository": "1aboveio/ai",
|
|
500
|
+
"revision": "main"
|
|
501
|
+
},
|
|
502
|
+
"contentDigest": "00c5971a8363fc1382afa6914707e764c9d3d0cb76a8e3a3ffcfd1d9d5fd5da5",
|
|
489
503
|
"digestExcludes": []
|
|
490
504
|
},
|
|
491
505
|
{
|
|
@@ -498,13 +512,13 @@
|
|
|
498
512
|
"repository": "1aboveio/ai",
|
|
499
513
|
"revision": "main"
|
|
500
514
|
},
|
|
501
|
-
"contentDigest": "
|
|
515
|
+
"contentDigest": "1599a61cc8f82eec8dd1b426604ec55ba65c503e4c793c5e57670e737e765462",
|
|
502
516
|
"digestExcludes": [
|
|
503
517
|
"coherence/workflow.json"
|
|
504
518
|
]
|
|
505
519
|
}
|
|
506
520
|
],
|
|
507
|
-
"releaseIdentity": "
|
|
521
|
+
"releaseIdentity": "5d71cce8b6dec3bb1fe3ad66fa046bdc870d197d110685d2d8981410c56f5686",
|
|
508
522
|
"lifecycleAuthority": "native-skills-cli",
|
|
509
523
|
"repairRecipe": {
|
|
510
524
|
"id": "engineering-workflow-dependency-first",
|
|
@@ -575,16 +589,17 @@
|
|
|
575
589
|
"golden-path",
|
|
576
590
|
"test-quality",
|
|
577
591
|
"better-review",
|
|
592
|
+
"contributing",
|
|
578
593
|
"engineering-runtime"
|
|
579
594
|
],
|
|
580
595
|
"commands": [
|
|
581
596
|
{
|
|
582
597
|
"transport": "https",
|
|
583
|
-
"command": "npx skills@1.5.22 add 1aboveio/ai --global --agent universal claude-code --skill harness-runtime e2e-test ensure-coverage rush-issues resolve-issues rush-release resolve-release golden-path test-quality better-review engineering-runtime && npm --prefix \"$(realpath \"$HOME/.agents/skills/engineering-runtime/scripts\")\" ci --ignore-scripts"
|
|
598
|
+
"command": "npx skills@1.5.22 add 1aboveio/ai --global --agent universal claude-code --skill harness-runtime e2e-test ensure-coverage rush-issues resolve-issues rush-release resolve-release golden-path test-quality better-review contributing engineering-runtime && npm --prefix \"$(realpath \"$HOME/.agents/skills/engineering-runtime/scripts\")\" ci --ignore-scripts"
|
|
584
599
|
},
|
|
585
600
|
{
|
|
586
601
|
"transport": "ssh",
|
|
587
|
-
"command": "npx skills@1.5.22 add git@github.com:1aboveio/ai.git --global --agent universal claude-code --skill harness-runtime e2e-test ensure-coverage rush-issues resolve-issues rush-release resolve-release golden-path test-quality better-review engineering-runtime && npm --prefix \"$(realpath \"$HOME/.agents/skills/engineering-runtime/scripts\")\" ci --ignore-scripts"
|
|
602
|
+
"command": "npx skills@1.5.22 add git@github.com:1aboveio/ai.git --global --agent universal claude-code --skill harness-runtime e2e-test ensure-coverage rush-issues resolve-issues rush-release resolve-release golden-path test-quality better-review contributing engineering-runtime && npm --prefix \"$(realpath \"$HOME/.agents/skills/engineering-runtime/scripts\")\" ci --ignore-scripts"
|
|
588
603
|
}
|
|
589
604
|
],
|
|
590
605
|
"onFailure": {
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: ensure-coverage
|
|
3
3
|
description: "Define or audit behavior-first test criteria — ACs, required test levels, mock/integration/E2E policy — across three axes: breadth (Coverage Ledger), depth (over-mocking), presentation (perceivability of browser surfaces). NOT judging whether a single test is any good (test-quality), authoring browser tests (e2e-test), reviewing a PR (better-review), or delivering issues (resolve-issues)."
|
|
4
4
|
disable-model-invocation: true
|
|
5
|
-
release-version: 0.
|
|
5
|
+
release-version: 0.40.0
|
|
6
6
|
---
|
|
7
7
|
**Updates:** Once/session run `node <skill-base-dir>/release-check.mjs`.
|
|
8
8
|
# Ensure Coverage
|
|
@@ -3,7 +3,7 @@ name: golden-path
|
|
|
3
3
|
description: "Golden-path E2E: the short list of user journeys whose failure blocks a release, kept in a repo as one contract (golden-paths.json) with a generated GOLDEN_PATHS.md and a generated GitHub Actions workflow, and run against a local build or a deployed URL for an ALIVE/DEAD verdict. Use to set golden paths up in a repo ('init golden paths', 'add golden-path CI'), to migrate an older smoke.manifest.json or a hand-built smoke setup ('audit', 'migrate smoke'), to add, change, or remove a golden path, to ask 'is the build alive?' or run them before a PR or release, or when a change touches a flow listed in GOLDEN_PATHS.md. NOT for writing the Playwright specs themselves (e2e-test), deciding what a change must prove (ensure-coverage), or general workflow YAML (github-actions)."
|
|
4
4
|
dependencies:
|
|
5
5
|
- e2e-test
|
|
6
|
-
release-version: 0.
|
|
6
|
+
release-version: 0.40.0
|
|
7
7
|
---
|
|
8
8
|
**Updates:** Once/session run `node <skill-base-dir>/release-check.mjs`.
|
|
9
9
|
# Golden path
|
|
@@ -3,7 +3,7 @@ name: harness-runtime
|
|
|
3
3
|
description: Internal import-only library shared by first-party engineering packages. Not a workflow skill; never select or invoke it as a user capability.
|
|
4
4
|
disable-model-invocation: true
|
|
5
5
|
user-invocable: false
|
|
6
|
-
release-version: 0.
|
|
6
|
+
release-version: 0.40.0
|
|
7
7
|
---
|
|
8
8
|
**Updates:** Once/session run `node <skill-base-dir>/release-check.mjs`.
|
|
9
9
|
# Harness Runtime
|
|
@@ -4,7 +4,7 @@ description: "Slash/explicit-only (/resolve-release). Full GitHub Flow release f
|
|
|
4
4
|
disable-model-invocation: true
|
|
5
5
|
metadata:
|
|
6
6
|
workflow-role: coordinator
|
|
7
|
-
release-version: 0.
|
|
7
|
+
release-version: 0.40.0
|
|
8
8
|
---
|
|
9
9
|
**Updates:** Once/session run `node <skill-base-dir>/release-check.mjs`.
|
|
10
10
|
# Resolve release
|