@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.
Files changed (91) hide show
  1. package/README.md +25 -8
  2. package/bin/select-core.mjs +182 -50
  3. package/extensions/hide-copies.ts +21 -0
  4. package/package.json +116 -3
  5. package/skills/app-dev/airflow/SKILL.md +1 -1
  6. package/skills/app-dev/android-foundation/SKILL.md +1 -1
  7. package/skills/app-dev/app-debug/SKILL.md +1 -1
  8. package/skills/app-dev/better-auth/SKILL.md +1 -1
  9. package/skills/app-dev/data-catalog/SKILL.md +2 -1
  10. package/skills/app-dev/data-catalog/references/data-catalog-conventions.md +2 -1
  11. package/skills/app-dev/data-catalog/references/field-definitions.md +2 -1
  12. package/skills/app-dev/data-catalog/references/format.md +6 -2
  13. package/skills/app-dev/data-catalog/scripts/data_catalog.py +15 -3
  14. package/skills/app-dev/data-catalog/scripts/persist-conventions-core.mjs +3 -2
  15. package/skills/app-dev/data-catalog/scripts/test_data_catalog.py +15 -0
  16. package/skills/app-dev/data-quality/LICENSE +3 -0
  17. package/skills/app-dev/data-quality/SKILL.md +96 -0
  18. package/skills/app-dev/data-quality/agents/openai.yaml +3 -0
  19. package/skills/app-dev/data-quality/references/checks.md +56 -0
  20. package/skills/app-dev/data-quality/release-check.mjs +93 -0
  21. package/skills/app-dev/data-quality/scripts/data-quality.test.mjs +12 -0
  22. package/skills/app-dev/data-quality/scripts/data_quality.py +405 -0
  23. package/skills/app-dev/data-quality/scripts/test_data_quality.py +277 -0
  24. package/skills/app-dev/ios-foundation/SKILL.md +1 -1
  25. package/skills/app-dev/monorepo/SKILL.md +1 -1
  26. package/skills/app-dev/nextjs-fullstack/SKILL.md +1 -1
  27. package/skills/app-dev/prisma-setup/SKILL.md +1 -1
  28. package/skills/app-dev/pyspark/SKILL.md +1 -1
  29. package/skills/app-dev/python-backend/SKILL.md +1 -1
  30. package/skills/app-dev/shadcn/SKILL.md +1 -1
  31. package/skills/app-dev/zod-v4/SKILL.md +1 -1
  32. package/skills/cicd/cloud-build/SKILL.md +1 -1
  33. package/skills/cicd/cloud-debug/SKILL.md +1 -1
  34. package/skills/cicd/cloud-deploy/SKILL.md +1 -1
  35. package/skills/cicd/docker/SKILL.md +1 -1
  36. package/skills/cicd/github-actions/SKILL.md +1 -1
  37. package/skills/cicd/github-flow/SKILL.md +1 -1
  38. package/skills/cicd/github-flow/scripts/persist-conventions-core.mjs +3 -2
  39. package/skills/cicd/google-cloud/SKILL.md +1 -1
  40. package/skills/cicd/mergify/SKILL.md +1 -1
  41. package/skills/cicd/podman/SKILL.md +1 -1
  42. package/skills/content/article-review/SKILL.md +1 -1
  43. package/skills/content/article-to-image/SKILL.md +1 -1
  44. package/skills/content/flowchart-svg/SKILL.md +1 -1
  45. package/skills/content/image-gen/SKILL.md +1 -1
  46. package/skills/content/mermaid/SKILL.md +1 -1
  47. package/skills/content/wechat-article-writer/SKILL.md +1 -1
  48. package/skills/content/wechat-publisher/SKILL.md +1 -1
  49. package/skills/content/wechat-search/SKILL.md +1 -1
  50. package/skills/content/xai-search/SKILL.md +1 -1
  51. package/skills/core/distill-writing/SKILL.md +1 -1
  52. package/skills/core/doc-to-md/SKILL.md +1 -1
  53. package/skills/core/editorial-review/SKILL.md +1 -1
  54. package/skills/core/house-style/SKILL.md +1 -1
  55. package/skills/core/plain-language/LICENSE +3 -0
  56. package/skills/core/plain-language/SKILL.md +57 -0
  57. package/skills/core/plain-language/release-check.mjs +93 -0
  58. package/skills/core/polish-cn/SKILL.md +1 -1
  59. package/skills/core/reg-parser/SKILL.md +1 -1
  60. package/skills/core/terminology/SKILL.md +1 -1
  61. package/skills/core/write-doc/SKILL.md +1 -1
  62. package/skills/engineering/better-review/SKILL.md +1 -1
  63. package/skills/engineering/contributing/LICENSE +3 -0
  64. package/skills/engineering/contributing/SKILL.md +70 -0
  65. package/skills/engineering/contributing/references/tracker-conventions.md +18 -0
  66. package/skills/engineering/contributing/release-check.mjs +93 -0
  67. package/skills/engineering/contributing/scripts/persist-conventions-core.mjs +118 -0
  68. package/skills/engineering/contributing/scripts/persist-conventions.mjs +84 -0
  69. package/skills/engineering/contributing/scripts/persist-conventions.test.mjs +118 -0
  70. package/skills/engineering/e2e-test/SKILL.md +1 -1
  71. package/skills/engineering/engineering-runtime/SKILL.md +1 -1
  72. package/skills/engineering/engineering-runtime/coherence/workflow.json +29 -14
  73. package/skills/engineering/engineering-runtime/scripts/workflow-policy.mjs +1 -0
  74. package/skills/engineering/ensure-coverage/SKILL.md +1 -1
  75. package/skills/engineering/golden-path/SKILL.md +1 -1
  76. package/skills/engineering/harness-runtime/SKILL.md +1 -1
  77. package/skills/engineering/resolve-issues/SKILL.md +1 -1
  78. package/skills/engineering/resolve-release/SKILL.md +1 -1
  79. package/skills/engineering/rush-issues/SKILL.md +1 -1
  80. package/skills/engineering/rush-release/SKILL.md +1 -1
  81. package/skills/engineering/test-quality/SKILL.md +1 -1
  82. package/skills/engineering/test-quality/scripts/persist-conventions-core.mjs +3 -2
  83. package/skills/risk-compliance/compliance-audit/SKILL.md +1 -1
  84. package/skills/risk-compliance/contract-compare/SKILL.md +1 -1
  85. package/skills/risk-compliance/contract-review/SKILL.md +1 -1
  86. package/skills/risk-compliance/doc-authenticity/SKILL.md +1 -1
  87. package/skills/risk-compliance/fraud-analysis/SKILL.md +1 -1
  88. package/skills/risk-compliance/payment-analysis/SKILL.md +1 -1
  89. package/skills/risk-compliance/report-writing/SKILL.md +1 -1
  90. package/skills/risk-compliance/similar-domain/SKILL.md +1 -1
  91. 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.38.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.38.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.38.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.38.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.38.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,3 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 1AboveIO
@@ -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))