@1aboveio/skills 1.0.1 → 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 +25 -8
- package/bin/select-core.mjs +182 -50
- package/extensions/hide-copies.ts +21 -0
- package/package.json +116 -3
- 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 +2 -1
- package/skills/app-dev/data-catalog/references/data-catalog-conventions.md +2 -1
- package/skills/app-dev/data-catalog/references/field-definitions.md +2 -1
- package/skills/app-dev/data-catalog/references/format.md +6 -2
- package/skills/app-dev/data-catalog/scripts/data_catalog.py +15 -3
- package/skills/app-dev/data-catalog/scripts/persist-conventions-core.mjs +3 -2
- package/skills/app-dev/data-catalog/scripts/test_data_catalog.py +15 -0
- package/skills/app-dev/data-quality/LICENSE +3 -0
- package/skills/app-dev/data-quality/SKILL.md +96 -0
- package/skills/app-dev/data-quality/agents/openai.yaml +3 -0
- package/skills/app-dev/data-quality/references/checks.md +56 -0
- package/skills/app-dev/data-quality/release-check.mjs +93 -0
- package/skills/app-dev/data-quality/scripts/data-quality.test.mjs +12 -0
- package/skills/app-dev/data-quality/scripts/data_quality.py +405 -0
- package/skills/app-dev/data-quality/scripts/test_data_quality.py +277 -0
- 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/LICENSE +3 -0
- package/skills/core/plain-language/SKILL.md +57 -0
- package/skills/core/plain-language/release-check.mjs +93 -0
- 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,57 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: plain-language
|
|
3
|
+
description: "Session mode: every reply and status line in plain language — lead with the point, everyday words, full sentences, no filler — in English or Chinese, until turned off. Use for /plain-language, \"plain language mode\", \"talk plainly\", \"说人话\", or when the user's instructions say to use plain-language. Turn off with \"/plain-language off\" or \"normal mode\". NOT for writing a document (write-doc) or reviewing one (editorial-review)."
|
|
4
|
+
release-version: 0.40.0
|
|
5
|
+
---
|
|
6
|
+
**Updates:** Once/session run `node <skill-base-dir>/release-check.mjs`.
|
|
7
|
+
# Plain language
|
|
8
|
+
|
|
9
|
+
Write every reply in this style for the rest of the session, including status lines and
|
|
10
|
+
summaries, until the user says `/plain-language off` or "normal mode". A switch to another
|
|
11
|
+
reply style (such as caveman) also ends it.
|
|
12
|
+
|
|
13
|
+
- Invoked with no other request: reply in one line that the mode is on. Otherwise do not
|
|
14
|
+
mention the mode; just answer in it.
|
|
15
|
+
- Invoked with `off`: reply in one line that the mode is off, and stop following this page.
|
|
16
|
+
- Code, commands, file paths, identifiers, error text and commit messages stay exact.
|
|
17
|
+
- Follow the section for the reply's language; a mixed reply follows both.
|
|
18
|
+
|
|
19
|
+
## Every reply
|
|
20
|
+
|
|
21
|
+
- **Lead with the point.** The answer or recommendation first, then the reasons.
|
|
22
|
+
- **Cut every word that does no work.** No greeting, no restating the question, no recap
|
|
23
|
+
of what you just said.
|
|
24
|
+
- **Fit the length to the question.** A short question gets a short answer; add detail
|
|
25
|
+
only when the reader needs it to act.
|
|
26
|
+
- **Use the short everyday word.** Explain a needed technical term in a few words the first
|
|
27
|
+
time. Never leverage, robust, seamless, holistic, streamline, delve.
|
|
28
|
+
- **Name who does what.** "The script deletes the branch", not "the branch is deleted".
|
|
29
|
+
- **State findings plainly; hedge only real uncertainty, and say what it depends on.**
|
|
30
|
+
- **Write full sentences, not telegraphic notes.** Keep if, when, because and but.
|
|
31
|
+
Example: "It is 0 if there were no collections," not "0 when none."
|
|
32
|
+
- **Give every range its endpoints and say which are included.**
|
|
33
|
+
- **When "and" and "or" meet in one condition, group them.** Give each condition its subject.
|
|
34
|
+
- **Make every "it", "this" and "they" point to one obvious noun.**
|
|
35
|
+
- **Never add a number, date, condition or cause you do not have,** and never turn "may"
|
|
36
|
+
into "will".
|
|
37
|
+
- One thought per sentence; most sentences under 25 words.
|
|
38
|
+
- Between tool calls, write one plain sentence on what you are doing and why, or nothing.
|
|
39
|
+
|
|
40
|
+
## 中文回复
|
|
41
|
+
|
|
42
|
+
- 先给结论,再给理由;不写问候式开场、宣传口号和空泛形容。
|
|
43
|
+
- 写完整的句子,不写电报体。示例:`若窗口内无收款,则为 0`,而不是 `无收款为 0`。
|
|
44
|
+
- 区间和阈值写明端点,并写明端点是否包含。
|
|
45
|
+
- 并列的条件逐个写全对象。示例:`客户已认证,且客户是个人或商户`,而不是 `客户是个人或商户且已认证`。
|
|
46
|
+
- 指代可能不清时,用具体名称替代「该」「其」「此」「上述」「相关」。
|
|
47
|
+
- 不用掩盖具体动作的词,改写为实际含义:赋能 → 提供;抓手 → 关键措施;对齐 → 统一、确认一致;拉通 / 打通 → 连接。
|
|
48
|
+
- 不用装饰性连接词:值得注意的是、不难看出、综上所述。
|
|
49
|
+
- 中文与英文、数字之间加一个半角空格。
|
|
50
|
+
|
|
51
|
+
## When a rule makes a reply worse
|
|
52
|
+
|
|
53
|
+
Fix the sentence another way. Never shorten a warning, an irreversible step or an ordered
|
|
54
|
+
procedure to save words.
|
|
55
|
+
|
|
56
|
+
Full guides: [style-en](../house-style/references/style-en.md),
|
|
57
|
+
[style-zh](../house-style/references/style-zh.md) (from `house-style`, when installed).
|
|
@@ -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. */ }
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: polish-cn
|
|
3
3
|
description: "Polish the Chinese wording of a finished document or report, with a script proving every number, date, identifier, link and protected term is unchanged; prefers a Chinese model and records which ran. Use for '中文润色' or as a report skill's last step. NOT for cutting or restructuring content (editorial-review) or translating (write-doc)."
|
|
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
|
# Polish Chinese
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
name: reg-parser
|
|
3
3
|
version: 1.1.0
|
|
4
4
|
description: "Parse regulatory/scheme documents into structured outputs: convert a regulation (AML, VARS, PCI-DSS, KYC, GDPR) into an audit-ready compliance checklist, or trace contract clauses to a mandate source. Suggest proactively when a regulation PDF accompanies a contract review or compliance task. NOT general summarization (doc-to-md), or writing the gap analysis or policy from the checklist (write-doc)."
|
|
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
|
# reg-parser
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: terminology
|
|
3
3
|
description: "Build and keep a project's TERMINOLOGY.md: the preferred term per concept in English, 简体 and 繁體, and the terms to avoid. Use to set it up from the project's documents, when the user settles or corrects a term, or when another writing skill needs the format. NOT for writing documents (write-doc) or the code domain model (domain-modeling)."
|
|
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
|
# Terminology
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: write-doc
|
|
3
3
|
description: "Write or edit a Markdown document in the project's house style, using the nearest WRITING.md and a document template (BRD, PRD, gap analysis, technical spec, integration guide, commercial proposal, compliance policy, risk operations procedure, or the project's own); English, Chinese or bilingual. NOT for learning a style from samples (distill-writing), audit reports from findings (report-writing), or WeChat articles (wechat-article-writer)."
|
|
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
|
# Write a document
|
|
@@ -4,7 +4,7 @@ description: "Slash/explicit-only (/better-review). Single-agent adversarial rev
|
|
|
4
4
|
disable-model-invocation: true
|
|
5
5
|
dependencies:
|
|
6
6
|
- engineering-runtime
|
|
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
|
# Better Review
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: contributing
|
|
3
|
+
description: "The conventions a repo's CONTRIBUTING.md holds, starting with tracker routing: which tracker an issue belongs in (GitHub for engineering, Linear for product), duplicate search, and linking back to the source requirement. Use before creating any issue, spec, epic, ticket or sub-issue on GitHub or Linear, including inside /to-spec, /to-tickets and /triage, and whenever work that started from a Linear issue turns into engineering work. Also writes the rules as the Issue tracking section of the repo's CONTRIBUTING.md. NOT for delivering a PR (github-flow) or splitting work into slices (to-tickets)."
|
|
4
|
+
release-version: 0.40.0
|
|
5
|
+
---
|
|
6
|
+
**Updates:** Once/session run `node <skill-base-dir>/release-check.mjs`.
|
|
7
|
+
# Contributing
|
|
8
|
+
|
|
9
|
+
Rules every engineering skill follows, whatever skill or harness is running, kept as sections of the
|
|
10
|
+
repo's `CONTRIBUTING.md`. Each rule is the authority; another skill's "publish to the configured
|
|
11
|
+
tracker" means the tracker these rules pick.
|
|
12
|
+
|
|
13
|
+
## First use in a repo
|
|
14
|
+
|
|
15
|
+
Scripts live under **this skill's directory**: set `SKILL_DIR` to the directory you read this file
|
|
16
|
+
from, never your CWD. On the first trigger in a repo, write the `## Issue tracking` section into its
|
|
17
|
+
`CONTRIBUTING.md`, creating the file when it is missing:
|
|
18
|
+
|
|
19
|
+
```sh
|
|
20
|
+
node "$SKILL_DIR/scripts/persist-conventions.mjs" --repo-root <repo-root> --if-missing --write \
|
|
21
|
+
[--linear-project "<Linear project>"]... [--github-repo <owner/name>]
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
- **Pass every Linear project whose resources link to this repo.** Example: the `Checkout Risk`
|
|
25
|
+
project lists `https://github.com/acme/widgets`, so pass `--linear-project "Checkout Risk"`.
|
|
26
|
+
- **Pass `--github-repo` only when `origin` is not the GitHub repo.** Example: exit 2 with
|
|
27
|
+
`no GitHub origin remote` means pass it.
|
|
28
|
+
- **`--if-missing` never touches an existing section.** To rewrite it, run without `--if-missing`.
|
|
29
|
+
- **`AGENTS.md` indexes `CONTRIBUTING.md`.** The script adds a `## Contributing` pointer unless
|
|
30
|
+
`AGENTS.md` already names `CONTRIBUTING.md`, creates a missing `AGENTS.md`, and creates a missing
|
|
31
|
+
`CLAUDE.md` as a symlink to `AGENTS.md`, even when the section already exists.
|
|
32
|
+
- Report each file's result line; commit with the change you are making, or leave it for the user
|
|
33
|
+
when you make no other change.
|
|
34
|
+
|
|
35
|
+
## Tracker routing
|
|
36
|
+
|
|
37
|
+
| Work item | Tracker |
|
|
38
|
+
|---|---|
|
|
39
|
+
| Product goals, business requirements, priorities, milestones | Linear |
|
|
40
|
+
| Consulting engagements, reports, research deliverables | Linear |
|
|
41
|
+
| Engineering specs and implementation epics | GitHub |
|
|
42
|
+
| Implementation tickets, technical investigations, refactors, engineering bugs | GitHub |
|
|
43
|
+
| Delivery progress for stakeholders | A summary in Linear that links to GitHub |
|
|
44
|
+
|
|
45
|
+
Before you create an issue:
|
|
46
|
+
|
|
47
|
+
1. **Classify the work; the tracker follows the work, not where the request came from.** Example:
|
|
48
|
+
`/to-spec` run on a conversation about DEV-123 publishes a GitHub issue.
|
|
49
|
+
2. **Resolve the GitHub repo in this order: the repo's `CONTRIBUTING.md` Issue tracking section, the
|
|
50
|
+
GitHub links in the source Linear project's resources, then ask the user. Never fall back to
|
|
51
|
+
Linear.** Example: a project linking two repos gets the repo the work changes; a project linking
|
|
52
|
+
none gets a question, not a Linear ticket.
|
|
53
|
+
3. **Search before you create.** Example: `gh issue list --repo <owner/name> --state all --search
|
|
54
|
+
"<keywords>"`; reuse or comment on a match.
|
|
55
|
+
4. **A Linear issue is a source reference, never a parent for implementation tickets.** Example: put
|
|
56
|
+
`Source: <Linear URL>` in the GitHub epic body and file the tickets as GitHub sub-issues of the
|
|
57
|
+
epic.
|
|
58
|
+
5. **Link back once per epic.** Example: add the GitHub epic URL to the Linear requirement as a
|
|
59
|
+
link; never copy the GitHub tickets into Linear.
|
|
60
|
+
6. **Report progress in Linear as a summary; acceptance stays with the requester.** Example: when the
|
|
61
|
+
last sub-issue closes, comment `Engineering complete: <epic URL>` on the Linear requirement and
|
|
62
|
+
leave its status alone.
|
|
63
|
+
7. **An explicit destination from the user wins.** Example: "file this in Linear" files it in Linear.
|
|
64
|
+
|
|
65
|
+
## Common wrong answers
|
|
66
|
+
|
|
67
|
+
- "The request came from Linear, so the tickets go in Linear." The work type decides, rule 1.
|
|
68
|
+
- "No tracker is configured, so use the one the conversation used." Resolve the repo, rule 2.
|
|
69
|
+
- "Mirror the epic into Linear so stakeholders see it." Post a summary with a link, rule 5.
|
|
70
|
+
- "All sub-issues closed, so the Linear requirement is done." Engineering complete is not accepted, rule 6.
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
## Issue tracking
|
|
2
|
+
|
|
3
|
+
Engineering work for this repo is tracked in GitHub issues on `{{githubRepo}}`. Product work is tracked in Linear{{linearProjects}}.
|
|
4
|
+
|
|
5
|
+
| Work item | Tracker |
|
|
6
|
+
|---|---|
|
|
7
|
+
| Product goals, business requirements, priorities, milestones | Linear |
|
|
8
|
+
| Consulting engagements, reports, research deliverables | Linear |
|
|
9
|
+
| Engineering specs and implementation epics | GitHub |
|
|
10
|
+
| Implementation tickets, technical investigations, refactors, engineering bugs | GitHub |
|
|
11
|
+
| Delivery progress for stakeholders | A summary in Linear that links to GitHub |
|
|
12
|
+
|
|
13
|
+
1. **Classify the work before you create an issue; the tracker follows the work, not where the request came from.** Example: a spec written from a Linear requirement is a GitHub issue on `{{githubRepo}}`.
|
|
14
|
+
2. **A Linear issue is a source reference, never a parent for implementation tickets.** Example: put `Source: https://linear.app/<team>/issue/DEV-123` in the GitHub epic body and make the tickets GitHub sub-issues of that epic; do not create DEV-124 under DEV-123.
|
|
15
|
+
3. **Search GitHub before you create an issue.** Example: `gh issue list --repo {{githubRepo}} --state all --search "<keywords>"`; reuse or comment on a match instead of filing a duplicate.
|
|
16
|
+
4. **Link back to Linear once per epic, not once per ticket.** Example: add the GitHub epic URL to the Linear requirement as a link; do not copy the GitHub tickets into Linear.
|
|
17
|
+
5. **Report engineering progress in Linear as a summary; acceptance stays with the requester.** Example: when the last sub-issue closes, comment "Engineering complete: <epic URL>" on the Linear requirement and leave its status for the requester to move.
|
|
18
|
+
6. **An explicit destination from the user wins.** Example: "file this in Linear" files it in Linear.
|
|
@@ -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))
|