@1aboveio/skills 1.0.13 → 1.0.15

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (106) hide show
  1. package/.codex-plugin/plugin.json +1 -1
  2. package/.codex-plugin/resources.json +157 -154
  3. package/README.md +1 -1
  4. package/package.json +5 -5
  5. package/skills/app-dev/airflow/SKILL.md +1 -1
  6. package/skills/app-dev/android-development/SKILL.md +127 -0
  7. package/skills/app-dev/android-development/references/android-conventions.md +117 -0
  8. package/skills/app-dev/{android-foundation → android-development}/references/ci.md +10 -6
  9. package/skills/app-dev/{android-foundation → android-development}/references/diagnosis.md +44 -15
  10. package/skills/app-dev/android-development/references/feature-work.md +186 -0
  11. package/skills/app-dev/android-development/references/framework-choice.md +88 -0
  12. package/skills/app-dev/{android-foundation → android-development}/references/gotchas.md +43 -6
  13. package/skills/app-dev/android-development/references/init.md +126 -0
  14. package/skills/app-dev/android-development/references/setup.md +389 -0
  15. package/skills/app-dev/android-development/references/workspace.md +156 -0
  16. package/skills/app-dev/android-development/scripts/init.mjs +477 -0
  17. package/skills/app-dev/app-debug/SKILL.md +1 -1
  18. package/skills/app-dev/better-auth/SKILL.md +1 -1
  19. package/skills/app-dev/data-catalog/SKILL.md +53 -16
  20. package/skills/app-dev/data-catalog/scripts/persist-conventions-core.mjs +231 -21
  21. package/skills/app-dev/data-catalog/scripts/persist-conventions.mjs +12 -1
  22. package/skills/app-dev/data-quality/SKILL.md +1 -1
  23. package/skills/app-dev/ios-foundation/SKILL.md +2 -2
  24. package/skills/app-dev/monorepo/SKILL.md +1 -1
  25. package/skills/app-dev/nextjs-fullstack/SKILL.md +1 -1
  26. package/skills/app-dev/prisma-setup/SKILL.md +1 -1
  27. package/skills/app-dev/pyspark/SKILL.md +1 -1
  28. package/skills/app-dev/python-backend/SKILL.md +1 -1
  29. package/skills/app-dev/shadcn/SKILL.md +1 -1
  30. package/skills/app-dev/zod-v4/SKILL.md +1 -1
  31. package/skills/cicd/cloud-build/SKILL.md +1 -1
  32. package/skills/cicd/cloud-debug/SKILL.md +1 -1
  33. package/skills/cicd/cloud-deploy/SKILL.md +1 -1
  34. package/skills/cicd/docker/SKILL.md +1 -1
  35. package/skills/cicd/github-actions/SKILL.md +1 -1
  36. package/skills/cicd/github-flow/SKILL.md +10 -1
  37. package/skills/cicd/github-flow/scripts/persist-conventions-core.mjs +231 -21
  38. package/skills/cicd/github-flow/scripts/persist-conventions.mjs +18 -1
  39. package/skills/cicd/google-cloud/SKILL.md +1 -1
  40. package/skills/cicd/mergify/SKILL.md +1 -1
  41. package/skills/cicd/podman/SKILL.md +1 -1
  42. package/skills/content/article-review/SKILL.md +1 -1
  43. package/skills/content/article-to-image/SKILL.md +1 -1
  44. package/skills/content/flowchart-svg/SKILL.md +1 -1
  45. package/skills/content/image-gen/SKILL.md +1 -1
  46. package/skills/content/mermaid/SKILL.md +1 -1
  47. package/skills/content/wechat-article-writer/SKILL.md +1 -1
  48. package/skills/content/wechat-publisher/SKILL.md +1 -1
  49. package/skills/content/wechat-search/SKILL.md +1 -1
  50. package/skills/content/xai-search/SKILL.md +1 -1
  51. package/skills/core/distill-writing/SKILL.md +7 -1
  52. package/skills/core/doc-to-md/SKILL.md +1 -1
  53. package/skills/core/editorial-review/SKILL.md +1 -1
  54. package/skills/core/house-style/SKILL.md +1 -1
  55. package/skills/core/house-style/references/persistence.md +8 -3
  56. package/skills/core/house-style/scripts/init-guidance.mjs +1 -1
  57. package/skills/core/plain-language/SKILL.md +1 -1
  58. package/skills/core/polish-cn/SKILL.md +1 -1
  59. package/skills/core/reg-parser/SKILL.md +1 -1
  60. package/skills/core/terminology/SKILL.md +1 -1
  61. package/skills/core/terminology/references/init.md +11 -0
  62. package/skills/core/write-doc/SKILL.md +1 -1
  63. package/skills/core/write-report/SKILL.md +1 -1
  64. package/skills/engineering/better-review/SKILL.md +1 -1
  65. package/skills/engineering/e2e-test/SKILL.md +1 -1
  66. package/skills/engineering/engineering/SKILL.md +25 -4
  67. package/skills/engineering/engineering/references/init-scope.md +10 -3
  68. package/skills/engineering/engineering/references/scope-management.md +11 -0
  69. package/skills/engineering/engineering/scripts/init.mjs +65 -17
  70. package/skills/engineering/engineering/scripts/persist-conventions-core.mjs +231 -21
  71. package/skills/engineering/engineering/scripts/persist-conventions.mjs +48 -21
  72. package/skills/engineering/engineering-runtime/SKILL.md +1 -1
  73. package/skills/engineering/engineering-runtime/coherence/workflow.json +13 -13
  74. package/skills/engineering/ensure-coverage/SKILL.md +1 -1
  75. package/skills/engineering/golden-path/SKILL.md +1 -1
  76. package/skills/engineering/harness-runtime/SKILL.md +1 -1
  77. package/skills/engineering/resolve-issues/SKILL.md +1 -1
  78. package/skills/engineering/resolve-issues/references/exploration.md +1 -1
  79. package/skills/engineering/resolve-issues/references/review.md +27 -4
  80. package/skills/engineering/resolve-release/SKILL.md +1 -1
  81. package/skills/engineering/rush-issues/SKILL.md +1 -1
  82. package/skills/engineering/rush-release/SKILL.md +1 -1
  83. package/skills/engineering/test-quality/SKILL.md +3 -1
  84. package/skills/engineering/test-quality/scripts/persist-conventions-core.mjs +231 -21
  85. package/skills/engineering/test-quality/scripts/persist-conventions.mjs +13 -1
  86. package/skills/risk-compliance/compliance-audit/SKILL.md +1 -1
  87. package/skills/risk-compliance/contract-compare/SKILL.md +1 -1
  88. package/skills/risk-compliance/contract-review/SKILL.md +1 -1
  89. package/skills/risk-compliance/doc-authenticity/SKILL.md +1 -1
  90. package/skills/risk-compliance/fraud-detection/SKILL.md +1 -1
  91. package/skills/risk-compliance/payment-analysis/SKILL.md +1 -1
  92. package/skills/risk-compliance/portfolio-payment-analysis/SKILL.md +1 -1
  93. package/skills/risk-compliance/similar-domain/SKILL.md +1 -1
  94. package/skills/risk-compliance/tld-detection/SKILL.md +1 -1
  95. package/skills/app-dev/android-foundation/SKILL.md +0 -248
  96. package/skills/app-dev/android-foundation/references/delivery-focus.md +0 -31
  97. package/skills/app-dev/android-foundation/references/feature-work.md +0 -50
  98. package/skills/app-dev/android-foundation/references/setup.md +0 -188
  99. package/skills/app-dev/android-foundation/references/workspace.md +0 -81
  100. /package/skills/app-dev/{android-foundation → android-development}/LICENSE +0 -0
  101. /package/skills/app-dev/{android-foundation → android-development}/assets/android-ci-job.yml +0 -0
  102. /package/skills/app-dev/{android-foundation → android-development}/release-check.mjs +0 -0
  103. /package/skills/app-dev/{android-foundation → android-development}/scripts/device-run.sh +0 -0
  104. /package/skills/app-dev/{android-foundation → android-development}/scripts/workspace-runtime/bin/workspaces.mjs +0 -0
  105. /package/skills/app-dev/{android-foundation → android-development}/scripts/workspace-runtime/native-workspace.mjs +0 -0
  106. /package/skills/app-dev/{android-foundation → android-development}/scripts/workspace-runtime/workspace-lifecycle.mjs +0 -0
@@ -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.45.2
7
+ release-version: 0.47.0
8
8
  ---
9
9
  **Updates:** Once/session run `node <skill-base-dir>/release-check.mjs`.
10
10
  # Better Review
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: e2e-test
3
3
  description: "Playwright end-to-end guidance for affected browser journeys, render health and presentation checks, with route manifests and sweeps for broader audits. 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.45.2
4
+ release-version: 0.47.0
5
5
  ---
6
6
  **Updates:** Once/session run `node <skill-base-dir>/release-check.mjs`.
7
7
  # E2E Testing Standard
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  name: engineering
3
3
  description: "Engineering conventions: feature-delivery priorities, focused verification and review, operational boundaries, and tracker routing. Standalone /engineering offers Global, Project, or Session setup and targeted resets for configured scopes; /engineering init keeps explicit project setup. Use when setting up AGENTS.md or CONTRIBUTING.md, saving or resetting engineering defaults, or before creating an engineering issue, spec, epic, ticket, or sub-issue on GitHub or Linear. Reads existing policy without writing during ordinary work. NOT the issue-to-PR workflow (resolve-issues), release execution (resolve-release), Git delivery (github-flow), or CI setup (github-actions)."
4
- release-version: 0.45.2
4
+ release-version: 0.47.0
5
5
  ---
6
6
  **Updates:** Once/session run `node <skill-base-dir>/release-check.mjs`.
7
7
  # Engineering
@@ -140,8 +140,8 @@ resolve the destination before any issue creation. Never infer a Linear project.
140
140
 
141
141
  | File or section | Init owns |
142
142
  | --- | --- |
143
- | `AGENTS.md` | Missing `Feature delivery` section and a pointer to `CONTRIBUTING.md` |
144
- | `CONTRIBUTING.md` | Missing `Engineering conventions`, `Testing conventions`, and `Issue tracking` sections |
143
+ | `AGENTS.md` | Missing pointer to `CONTRIBUTING.md` and the short skill task routing rule; no policy sections |
144
+ | `CONTRIBUTING.md` | Missing `Feature delivery`, `Engineering conventions`, `Testing conventions`, and `Issue tracking` sections |
145
145
 
146
146
  The templates are [feature delivery](references/feature-delivery.md),
147
147
  [engineering conventions](references/engineering-conventions.md),
@@ -152,6 +152,25 @@ without invoking its writer. `test-quality` owns the testing rules and deliberat
152
152
  updates; init only adds a missing section. The [skill review](references/init-scope.md)
153
153
  explains what is included and what remains opt-in.
154
154
 
155
+ Project policy lives in `CONTRIBUTING.md`; `AGENTS.md` carries only the loaders. A
156
+ `Feature delivery` section already in `AGENTS.md`, including one an earlier init wrote,
157
+ stays where it is: init neither migrates nor deletes it, and it does not add a competing
158
+ copy to `CONTRIBUTING.md`. Existing `CONTRIBUTING.md` policy wins the same way.
159
+ Initialization moves no existing rules between files and refreshes neither.
160
+
161
+ Besides the guide pointer, `AGENTS.md` gains short task routing rules: read this
162
+ skill before engineering implementation, review, planning, or issue creation, and read
163
+ `test-quality` when writing or reviewing tests. It names the skills because a
164
+ `CONTRIBUTING.md` link only loads policy, not the skills that apply it. Init adds it
165
+ only for a missing skill loader; a rule for one skill does not configure the other.
166
+ Equivalent active rules stay unchanged, and an unrecognized routing section requires
167
+ reconciliation rather than a competing rule. Loading does not authorize running init,
168
+ writing files, or calling other workflows; setup stays an explicit request. Verify
169
+ that supported entrypoints reach both loaders and contributor guidance; report partial
170
+ setup if a separate entrypoint misses either. Init still does not edit `CLAUDE.md`.
171
+ Existing task-only or explicit-only rules do not establish broader coverage; preserve
172
+ them and report a conflict where the requested loader would contradict their limit.
173
+
155
174
  Existing sections, including legacy `conventions:tracker` blocks written by
156
175
  `contributing`, are preserved byte-for-byte. Repeated init is not a refresh and
157
176
  leaves manual edits unchanged. No force/update mode is implied. If policy needs to
@@ -186,7 +205,9 @@ Before creating work:
186
205
  product acceptance.
187
206
 
188
207
  The retained tracker-only helper supports explicit requests to persist or update
189
- that section:
208
+ that section and its scope-limited `engineering` task-entry loader in agent
209
+ entrypoints. That loader covers issue/spec creation only; it does not configure
210
+ the general engineering-work loader installed by Project/init:
190
211
 
191
212
  ```sh
192
213
  node "$SKILL_DIR/scripts/persist-conventions.mjs" --repo-root <project-root> \
@@ -15,7 +15,7 @@ not initialization sources; pinned external skills keep their upstream ownership
15
15
 
16
16
  | Skill | Include in guidance | Keep as a separate action |
17
17
  | --- | --- | --- |
18
- | engineering (formerly contributing) | Feature priorities, guidance lookup, tracker routing | Tracker refresh or project-policy changes |
18
+ | engineering (formerly contributing) | Feature priorities, guidance lookup and tracker routing, written as `CONTRIBUTING.md` sections; `AGENTS.md` gets the guide pointer and a short rule to load `engineering` for engineering work and `test-quality` for tests | Tracker refresh or project-policy changes |
19
19
  | resolve-issues | Agreed outcome, bounded supporting work, explicit stopping point | Issue planning, workers, PRs, scheduling |
20
20
  | rush-issues | Same delivery principles as resolve-issues | Shortcut workflow invocation |
21
21
  | ensure-coverage | Smallest sufficient proof, stable acceptance, required checks preserved | Ledger adoption, broad audits, new gates |
@@ -37,8 +37,15 @@ not call other skills' convention writers: they own their sections and may need
37
37
  additional decisions. Init reads `test-quality`'s canonical template directly;
38
38
  the testing owner and `conventions:testing` section identity remain unchanged.
39
39
  Existing project sections, including old testing and tracker blocks, stay unchanged.
40
- Delivery, CI, and Data Catalog conventions require separate explicit setup with
41
- their owning skills. Setup may point to these capabilities without requiring them.
40
+ Project policy is persisted in `CONTRIBUTING.md`, so `AGENTS.md` keeps only the short
41
+ loaders: the guide pointer, plus a rule naming the skills to read for engineering work
42
+ and for tests. An existing equivalent rule is preserved rather than duplicated, and the
43
+ rule loads skills without authorizing setup. A `Feature delivery` section written into
44
+ `AGENTS.md` by an earlier release
45
+ stays there: init does not migrate or delete it, and it does not add a competing copy
46
+ to `CONTRIBUTING.md`. Delivery, CI, and Data Catalog conventions require separate
47
+ explicit setup with their owning skills. Setup may point to these capabilities without
48
+ requiring them.
42
49
 
43
50
  Stack-specific tools, commands, cloud projects, release branches, required gates,
44
51
  and golden paths are project facts, not defaults to manufacture. Read established
@@ -43,6 +43,12 @@ new global defaults or new loading pointer in unique `engineering:<id>:start/end
43
43
  comments before recording their hashes. Do not rewrap existing text or claim it as
44
44
  owned. Keep existing valid records and add only new entries; deduplicate entries.
45
45
 
46
+ Project policy sections, including `Feature delivery` (`conventions:feature-delivery`),
47
+ live in `CONTRIBUTING.md`; `AGENTS.md` holds only the guide pointer and skill task-entry
48
+ loaders. Task-entry blocks use their stable `conventions:<id>-task-entry` markers;
49
+ record only blocks newly added by this setup, independently for each skill. Record a
50
+ new guide pointer as an `engineering:<id>` loading addition, never as a policy section.
51
+
46
52
  Set `createdFile` only when this save created that file. Preserve unrelated bytes
47
53
  and existing links. Do not create a sidecar, global settings entry, or project file
48
54
  for a session choice. A repeated save with no additions leaves files unchanged.
@@ -73,6 +79,11 @@ scope's saved defaults, not a reinstall, refresh, or reset of every scope.
73
79
  5. Remove owned loading pointers only if they are unchanged and no longer needed
74
80
  by retained guidance. Keep a pointer that still loads user policy. Remove an
75
81
  empty ownership record and any empty wrapper heading added by this save.
82
+ For a legacy `Feature delivery` section in `AGENTS.md`, honor a valid existing
83
+ ownership record and unchanged hash under step 3, even if an earlier initializer
84
+ recorded it. Preserve and report unrecorded or edited policy under step 4; do not
85
+ infer ownership from its location or migrate it during reset. Keep the pointer
86
+ while retained `CONTRIBUTING.md` policy still needs it.
76
87
  Delete a file only if `createdFile` proves this skill created it and nothing
77
88
  except the original generated title and whitespace remains. Preserve pre-existing
78
89
  files, symlinks, hand edits, and unrelated sections; never delete an instruction
@@ -2,10 +2,22 @@
2
2
  import fs from 'node:fs'
3
3
  import path from 'node:path'
4
4
  import { spawnSync } from 'node:child_process'
5
- import { fileURLToPath } from 'node:url'
5
+ import { fileURLToPath, pathToFileURL } from 'node:url'
6
+ import { withTaskEntries } from './persist-conventions-core.mjs'
6
7
 
7
8
  const reference = name => fs.readFileSync(fileURLToPath(new URL(`../references/${name}.md`, import.meta.url)), 'utf8').trim()
8
9
  const usage = 'node init.mjs [--repo-root <dir>] [--github-repo <owner/name>] [--linear-project <name>]... [--write]'
10
+ const taskLoaders = [
11
+ {
12
+ id: 'engineering', skill: 'engineering', title: 'Engineering skills',
13
+ contexts: ['implementation', 'review', 'planning', 'issue creation'],
14
+ body: 'Before engineering implementation, review, planning, or issue creation, read and apply the installed `engineering` skill. After context loss, reload its rules before continuing. Loading it does not authorize initialization, project writes, or unrelated workflows.',
15
+ },
16
+ {
17
+ id: 'testing', skill: 'test-quality', title: 'Testing task entry',
18
+ body: 'When writing, changing, or reviewing tests, read and apply the installed `test-quality` skill. After context loss, reload its rules before continuing. Follow the testing conventions in the contributor guide; loading the skill adds no new gates or write permissions.',
19
+ },
20
+ ]
9
21
 
10
22
  function parseArgs(argv) {
11
23
  const options = { write: false, linearProjects: [] }
@@ -45,8 +57,9 @@ function inspectTarget(root, name) {
45
57
  if (relative === '..' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
46
58
  throw new Error(`${name} resolves outside the selected project`)
47
59
  }
48
- if (!fs.statSync(target).isFile()) throw new Error(`${name} is not a regular file`)
49
- return { name, file, target, current: new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(fs.readFileSync(target)) }
60
+ const info = fs.statSync(target)
61
+ if (!info.isFile()) throw new Error(`${name} is not a regular file`)
62
+ return { name, file, target, nlink: info.nlink, current: new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(fs.readFileSync(target)) }
50
63
  }
51
64
 
52
65
  function append(current, body) {
@@ -54,7 +67,7 @@ function append(current, body) {
54
67
  return `${current}${separator}${body.trim()}\n`
55
68
  }
56
69
 
57
- function preserveOrAdd(current, title, id, body) {
70
+ function sectionState(current, title, id) {
58
71
  const start = `<!-- conventions:${id}:start -->`
59
72
  const end = `<!-- conventions:${id}:end -->`
60
73
  const starts = current.split(start).length - 1
@@ -62,8 +75,13 @@ function preserveOrAdd(current, title, id, body) {
62
75
  if (starts !== ends || starts > 1 || (starts && current.indexOf(start) > current.indexOf(end))) {
63
76
  throw new Error(`malformed ${title} section markers; preserve and repair existing guidance first`)
64
77
  }
65
- if (starts || current.split(/\r?\n/).some(line => line.trim() === `## ${title}`)) return current
66
- return append(current, `${start}\n${body}\n${end}`)
78
+ return Boolean(starts) || current.split(/\r?\n/).some(line => line.trim() === `## ${title}`)
79
+ }
80
+
81
+
82
+ function addSection(current, title, id, body) {
83
+ if (sectionState(current, title, id)) return current
84
+ return append(current, `<!-- conventions:${id}:start -->\n${body.trim()}\n<!-- conventions:${id}:end -->`)
67
85
  }
68
86
 
69
87
  function linkFrom(from, to) {
@@ -73,11 +91,14 @@ function linkFrom(from, to) {
73
91
  function hasLoader(current, from, to) {
74
92
  const links = /\[[^\]\r\n]*\]\(\s*(?:<([^>]+)>|([^\s)]+))(?:\s+["'][^"'\r\n]*["'])?\s*\)/g
75
93
  for (const match of current.matchAll(links)) {
76
- const href = (match[1] ?? match[2]).split(/[?#]/)[0]
94
+ const href = match[1] ?? match[2]
77
95
  if (!href || /^[a-z][a-z\d+.-]*:|^\/\//i.test(href)) continue
78
96
  let target
79
97
  try {
80
- target = path.resolve(path.dirname(from.target), decodeURIComponent(href))
98
+ const link = new URL(href, pathToFileURL(from.target))
99
+ if (link.hash) continue
100
+ link.search = ''
101
+ target = fileURLToPath(link)
81
102
  } catch {
82
103
  continue
83
104
  }
@@ -114,23 +135,49 @@ function planInitialization(root, options) {
114
135
  throw new Error('agent instructions and CONTRIBUTING.md must use distinct files')
115
136
  }
116
137
 
117
- let agentText = preserveOrAdd(agents.current ?? '# Agent instructions\n', 'Feature delivery',
118
- 'feature-delivery', reference('feature-delivery'))
138
+ // AGENTS.md routes tasks and loads policy; it does not carry the policy itself.
139
+ const agentBase = agents.current ?? '# Agent instructions\n'
140
+ // An earlier release put Feature delivery in AGENTS.md. That policy stays where it is:
141
+ // init neither migrates it nor adds a competing copy to CONTRIBUTING.md.
142
+ const featureDeliveryInAgents = sectionState(agentBase, 'Feature delivery', 'feature-delivery')
143
+ let agentText = agentBase
119
144
  if (!hasLoader(agentText, agents, contributing)) {
120
145
  agentText = append(agentText, `## Contributing\n\nFollow [CONTRIBUTING.md](${linkFrom(agents, contributing)}) for this project's engineering conventions and existing delivery requirements.`)
121
146
  }
122
-
123
- let contributingText = preserveOrAdd(contributing.current ?? '# Contributing\n', 'Engineering conventions',
147
+ const routing = withTaskEntries(agentText, taskLoaders)
148
+ if (routing.conflicts.length) throw new Error(routing.conflicts.join('; '))
149
+ agentText = routing.text
150
+
151
+ let contributingText = contributing.current ?? '# Contributing\n'
152
+ const featureDeliveryInContributing = sectionState(contributingText, 'Feature delivery', 'feature-delivery')
153
+ if (!featureDeliveryInAgents && !featureDeliveryInContributing) {
154
+ contributingText = addSection(contributingText, 'Feature delivery', 'feature-delivery',
155
+ reference('feature-delivery'))
156
+ }
157
+ contributingText = addSection(contributingText, 'Engineering conventions',
124
158
  'engineering', reference('engineering-conventions'))
125
159
  const testing = fs.readFileSync(new URL('../../test-quality/references/testing-conventions.md', import.meta.url), 'utf8').trim()
126
- contributingText = preserveOrAdd(contributingText, 'Testing conventions', 'testing', testing)
127
- contributingText = preserveOrAdd(contributingText, 'Issue tracking', 'tracker',
160
+ contributingText = addSection(contributingText, 'Testing conventions', 'testing', testing)
161
+ contributingText = addSection(contributingText, 'Issue tracking', 'tracker',
128
162
  trackerBody(options.githubRepo ?? githubRepo(root), options.linearProjects))
129
163
 
130
- return [
164
+ const entries = [
131
165
  { ...agents, next: agentText },
132
166
  { ...contributing, next: contributingText },
133
167
  ]
168
+ for (const entry of entries) {
169
+ if (entry.current !== entry.next && entry.nlink > 1) {
170
+ throw new Error(`${entry.name} is hard-linked; setup requires exclusive ownership`)
171
+ }
172
+ }
173
+ return {
174
+ entries,
175
+ notes: [
176
+ ...(featureDeliveryInAgents
177
+ ? ['Feature delivery stays in AGENTS.md; no competing section added to CONTRIBUTING.md']
178
+ : []),
179
+ ],
180
+ }
134
181
  }
135
182
 
136
183
  function main(argv) {
@@ -142,13 +189,14 @@ function main(argv) {
142
189
  const selected = options.repoRoot ?? gitOutput(process.cwd(), ['rev-parse', '--show-toplevel']) ?? process.cwd()
143
190
  const root = fs.realpathSync(selected)
144
191
  if (!fs.statSync(root).isDirectory()) throw new Error('project root is not a directory')
145
- const plan = planInitialization(root, options)
192
+ const { entries, notes } = planInitialization(root, options)
146
193
  console.log(`engineering init: ${options.write ? 'write' : 'preview only'} (${root})`)
147
- for (const entry of plan) {
194
+ for (const entry of entries) {
148
195
  const action = entry.current === entry.next ? 'unchanged' : entry.current === null ? 'create' : 'update'
149
196
  if (options.write && action !== 'unchanged') fs.writeFileSync(entry.target, entry.next)
150
197
  console.log(` ${entry.name}: ${!options.write && action !== 'unchanged' ? 'would ' : ''}${action}`)
151
198
  }
199
+ for (const note of notes) console.log(` note: ${note}`)
152
200
  console.log('Existing sections are preserved; initialization does not refresh project policy or authorize delivery actions.')
153
201
  }
154
202
 
@@ -13,6 +13,12 @@
13
13
  // A section sits between `conventions:<id>` markers, so a re-run replaces only
14
14
  // that block and keeps everything else, including other skills' sections.
15
15
  // Nothing is written without --write: the user triggers the write, not an agent.
16
+ //
17
+ // The guide pointer and a skill's task-entry loader are independent. A link to
18
+ // CONTRIBUTING.md says where the policy lives; it does not load the skill that applies
19
+ // it. So a repo whose AGENTS.md (or regular CLAUDE.md) already names the guide still
20
+ // gets the loader, and a repo that already loads the skill keeps its own rule.
21
+ // engineering's init.mjs reuses `withTaskEntries` for its own two loaders.
16
22
 
17
23
  import fs from 'node:fs'
18
24
  import path from 'node:path'
@@ -20,6 +26,8 @@ import path from 'node:path'
20
26
  export const CONTRIBUTING_FILE = 'CONTRIBUTING.md'
21
27
  const MENTIONS_CONTRIBUTING = /(?:^|[^\w-])CONTRIBUTING\.md/
22
28
 
29
+ const TASK_ENTRY = 'task-entry'
30
+
23
31
  export const AGENTS_SECTION = [
24
32
  '## Contributing',
25
33
  '',
@@ -69,26 +77,218 @@ export function renderClaude(current) {
69
77
  return `${current.replace(/\s*$/, '')}\n\n${AGENTS_SECTION}`
70
78
  }
71
79
 
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],
80
+ export function taskEntryMarkers(id) {
81
+ return [`<!-- conventions:${id}-${TASK_ENTRY}:start -->`, `<!-- conventions:${id}-${TASK_ENTRY}:end -->`]
82
+ }
83
+
84
+ // Pure: the managed loader block. A short rule in an entrypoint; the conventions it
85
+ // points at stay in CONTRIBUTING.md.
86
+ export function taskEntryBlock({ id, title, body }) {
87
+ const [start, end] = taskEntryMarkers(id)
88
+ const recovery = body.includes('After context loss') ? '' : '\nAfter context loss, reload this skill\'s rules before continuing.'
89
+ return `${start}\n## ${title}\n\n${body.trim()}${recovery}\n${end}\n`
90
+ }
91
+
92
+ const escape = (text) => text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
93
+ function withoutExamples(text) {
94
+ let fence = null
95
+ let comment = false
96
+ const active = text.split('\n').map(line => {
97
+ if (fence) {
98
+ const match = /^ {0,3}(`{3,}|~{3,})/.exec(line)
99
+ if (match && match[1][0] === fence[0] && match[1].length >= fence.length
100
+ && line.slice(match[0].length).trim() === '') fence = null
101
+ return ''
102
+ }
103
+ let masked = ''
104
+ let cursor = 0
105
+ while (cursor < line.length) {
106
+ const marker = line.indexOf(comment ? '-->' : '<!--', cursor)
107
+ const end = marker < 0 ? line.length : marker
108
+ masked += comment ? ' '.repeat(end - cursor) : line.slice(cursor, end)
109
+ if (marker < 0) break
110
+ const length = comment ? 3 : 4
111
+ masked += ' '.repeat(length)
112
+ cursor = marker + length
113
+ comment = !comment
114
+ }
115
+ const match = /^ {0,3}(`{3,}|~{3,})/.exec(masked)
116
+ if (match) { fence = match[1]; return '' }
117
+ return /^(?: {4}|\t|\s*>)/.test(line) ? '' : line
118
+ }).join('\n')
119
+ if (fence) throw new Error('unclosed Markdown code fence; repair guidance before setup')
120
+ if (comment) {
121
+ throw new Error('unclosed HTML comment; repair guidance before setup')
122
+ }
123
+ return active
124
+ }
125
+
126
+ // Keep code-form skill names, but not inline examples containing whole instructions.
127
+ const visible = (text) => withoutExamples(text).replace(/<!--[\s\S]*?-->/g, '')
128
+ .replace(/(`+)([^`]*?)\1/g, (span, _ticks, content) => /\s/.test(content) ? '' : span)
129
+
130
+ function hasHeading(text, title) {
131
+ return new RegExp(`^#{1,6}\\s+${escape(title)}\\s*$`, 'm').test(visible(text))
132
+ }
133
+
134
+ // A hand-written equivalent ("read and apply the `test-quality` skill") already loads
135
+ // the skill, so the managed block would repeat it. A guide link never matches.
136
+ function loadsSkill(text, skill, contexts = []) {
137
+ const name = escape(skill)
138
+ const instruction = new RegExp(`\\b(?:read|load|apply|use|follow)\\s+(?:and\\s+(?:apply|read)\\s+)?(?:(?:the|installed)\\s+){0,2}(?:\`${name}\`|${name}\\s+skills?\\b)`, 'i')
139
+ return visible(text).split(/\n\s*\n/).some(paragraph =>
140
+ instruction.test(paragraph)
141
+ && (!contexts.length || contexts.every(word => new RegExp(`\\b${escape(word)}\\b`, 'i').test(paragraph))))
142
+ }
143
+
144
+ // Pure: `{ text, conflicts }` — each loader's block added once, where it is missing.
145
+ // An equivalent existing rule is preserved; malformed markers and an unmanaged section
146
+ // under the same heading are reported instead of written over.
147
+ export function withTaskEntries(current, loaders = []) {
148
+ let text = current
149
+ const conflicts = []
150
+ for (const loader of loaders) {
151
+ const body = visible(text)
152
+ const denied = new RegExp(`\\b(?:do not|don't|never|must not)\\s+(?:(?:automatically|ever)\\s+)?(?:read|load|apply|use|follow)\\b[^.\\n]{0,120}\\b${escape(loader.skill)}\\b`, 'i')
153
+ if (denied.test(body)) {
154
+ conflicts.push(`an existing rule forbids loading "${loader.skill}"; reconcile it before setup`)
155
+ continue
156
+ }
157
+ const restricted = new RegExp(`\\b${escape(loader.skill)}\\b[^.\\n]{0,100}\\bonly\\s+(?:when|if)\\s+(?:explicitly|the user)`, 'i')
158
+ if (restricted.test(body)) {
159
+ conflicts.push(`an existing explicit-only rule restricts "${loader.skill}"; reconcile it before setup`)
160
+ continue
161
+ }
162
+ if (loader.contexts?.length && visible(text).split(/\n\s*\n/).some(paragraph =>
163
+ loadsSkill(paragraph, loader.skill) && /\b(?:for|during)\b[^.\n]{0,80}\bonly\s*[,;:]/i.test(paragraph))) {
164
+ conflicts.push(`an existing task-only rule restricts "${loader.skill}"; reconcile it before setup`)
165
+ continue
166
+ }
167
+ const [start, end] = taskEntryMarkers(loader.id)
168
+ const active = withoutExamples(text)
169
+ const starts = active.split(start).length - 1
170
+ const ends = active.split(end).length - 1
171
+ if (starts || ends) {
172
+ // Already written: a re-run leaves the block as it is rather than refreshing it.
173
+ if (starts !== 1 || ends !== 1 || active.indexOf(start) > active.indexOf(end)) {
174
+ conflicts.push(`malformed "${loader.title}" markers; repair them by hand`)
175
+ } else if (!loadsSkill(active.slice(active.indexOf(start), active.indexOf(end)), loader.skill, loader.contexts)) {
176
+ conflicts.push(`the preserved "${loader.title}" block has no applicable skill loader; reconcile it before setup`)
177
+ }
178
+ continue
179
+ }
180
+ if (loadsSkill(text, loader.skill, loader.contexts)) continue
181
+ if (hasHeading(text, loader.title)) {
182
+ conflicts.push(`an existing "${loader.title}" section needs manual reconciliation; no competing block added`)
183
+ continue
184
+ }
185
+ text = `${text.replace(/\s*$/, '')}\n\n${taskEntryBlock(loader)}`
186
+ }
187
+ return { text, conflicts }
188
+ }
189
+
190
+ function indexEntries(section) {
191
+ const loaders = section.loader ? [section.loader] : []
192
+ return [
193
+ { name: 'AGENTS.md', render: renderAgents, loaders },
194
+ // A regular CLAUDE.md that already points at AGENTS.md inherits that file's loaders.
195
+ { name: 'CLAUDE.md', render: renderClaude, loaders, reachesAgents: true },
78
196
  ]
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'
197
+ }
198
+
199
+ function reachesSelectedAgents(text, repoRoot, file) {
200
+ const body = visible(text)
201
+ const expected = path.join(repoRoot, 'AGENTS.md')
202
+ const canonical = name => fs.existsSync(name) ? fs.realpathSync(name) : path.resolve(name)
203
+ const affirmative = body.split('\n').filter(line => !/\b(?:do not|don't|never|must not)\b/i.test(line)).join('\n')
204
+ const references = [...affirmative.matchAll(/\[[^\]]*\]\((?:<([^>]+)>|([^\s)]+))\)/g)]
205
+ .map(match => match[1] ?? match[2])
206
+ for (const match of affirmative.matchAll(/\b(?:read|load|follow|see|consult)\s+`?((?:[^\s`]+\/)?AGENTS\.md)`?/gi)) {
207
+ references.push(match[1])
208
+ }
209
+ return references.some(ref => {
210
+ if (/^[a-z][a-z\d+.-]*:|^\/\//i.test(ref)) return false
211
+ try {
212
+ const target = path.resolve(path.dirname(file), decodeURIComponent(ref.split(/[?#]/)[0]))
213
+ return canonical(target) === canonical(expected)
214
+ } catch { return false }
215
+ })
216
+ }
217
+
218
+ function apply(repoRoot, entries, write) {
219
+ repoRoot = fs.realpathSync(repoRoot)
220
+ const readText = file => new TextDecoder('utf-8', { fatal: true, ignoreBOM: true }).decode(fs.readFileSync(file))
221
+ const plannedContent = new Map()
222
+ const plan = entries.map((entry) => {
223
+ const file = path.join(repoRoot, entry.name)
224
+ if (fs.lstatSync(file, { throwIfNoEntry: false })?.isSymbolicLink()) {
225
+ let conflicts = []
226
+ try {
227
+ const target = fs.realpathSync(file)
228
+ const relative = path.relative(repoRoot, target)
229
+ if (relative === '..' || relative.startsWith(`..${path.sep}`) || path.isAbsolute(relative)) {
230
+ conflicts = ['linked entrypoint is outside the selected project; verify its task loader separately']
231
+ } else {
232
+ const current = plannedContent.get(target) ?? readText(file)
233
+ const checked = withTaskEntries(current, entry.loaders)
234
+ conflicts = checked.conflicts
235
+ if (checked.text !== current && !(entry.reachesAgents && reachesSelectedAgents(current, repoRoot, target))) {
236
+ conflicts.push('linked entrypoint lacks the task loader; explicit reconciliation is required')
237
+ }
238
+ }
239
+ } catch { conflicts = ['linked entrypoint could not be verified; setup is partial'] }
240
+ return { file: entry.name, action: 'unchanged', conflicts }
241
+ }
242
+ const current = fs.existsSync(file) ? readText(file) : null
243
+ const base = entry.render(current)
244
+ let next = base
245
+ let conflicts = []
246
+ if (base !== LINK_TO_AGENTS) {
247
+ const applied = withTaskEntries(base, entry.loaders)
248
+ const inherits = entry.reachesAgents && reachesSelectedAgents(current ?? '', repoRoot, file)
249
+ next = inherits ? base : applied.text
250
+ conflicts = applied.conflicts
251
+ }
85
252
  if (next === LINK_TO_AGENTS) {
86
- if (write) fs.symlinkSync('AGENTS.md', file)
87
- return { file: name, action: 'link' }
253
+ return { file: entry.name, action: 'link', conflicts, next }
254
+ }
255
+ const action = current == null ? 'create' : next === current ? 'unchanged' : 'update'
256
+ if (current !== null && action !== 'unchanged' && fs.statSync(file).nlink > 1) {
257
+ conflicts.push('hard-linked guidance cannot be changed without exclusive ownership')
88
258
  }
89
- if (write && action !== 'unchanged') fs.writeFileSync(file, next)
90
- return { file: name, action }
259
+ plannedContent.set(file, next)
260
+ return { file: entry.name, action, conflicts, next }
91
261
  })
262
+ const blocked = plan.some(entry => entry.conflicts.length)
263
+ if (write && !blocked) {
264
+ for (const entry of plan.filter(entry => entry.action !== 'unchanged')) {
265
+ const file = path.join(repoRoot, entry.file)
266
+ fs.accessSync(entry.action === 'create' || entry.action === 'link' ? repoRoot : file, fs.constants.W_OK)
267
+ }
268
+ for (const entry of plan) {
269
+ const file = path.join(repoRoot, entry.file)
270
+ if (entry.next === LINK_TO_AGENTS) fs.symlinkSync('AGENTS.md', file)
271
+ else if (entry.action !== 'unchanged') fs.writeFileSync(file, entry.next)
272
+ }
273
+ }
274
+ return plan.map(({ next: _next, ...entry }) => ({
275
+ ...entry, action: blocked && entry.action !== 'unchanged' ? 'blocked' : entry.action,
276
+ }))
277
+ }
278
+
279
+ export function persist(repoRoot, section, { write = false } = {}) {
280
+ const block = sectionBlock(section)
281
+ const entries = [
282
+ { name: CONTRIBUTING_FILE, render: (text) => renderConventions(text, section.id, block), loaders: [] },
283
+ ...indexEntries(section),
284
+ ]
285
+ return apply(repoRoot, entries, write)
286
+ }
287
+
288
+ // The index half alone: for a section that is already present, only the guide pointer
289
+ // and the task-entry loader are due.
290
+ export function persistIndex(repoRoot, section, { write = false } = {}) {
291
+ return apply(repoRoot, indexEntries(section), write)
92
292
  }
93
293
 
94
294
  // `section` = { id, owner, title, body }. Exit 0 on success, 2 on a usage error.
@@ -107,12 +307,22 @@ export function runCli(argv, section, { cwd = process.cwd(), log = console.log }
107
307
  log(`persist-conventions: not a directory: ${repoRoot}`)
108
308
  return 2
109
309
  }
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
310
+ let results
311
+ try { results = persist(repoRoot, section, { write }) }
312
+ catch (error) { log(`persist-conventions: setup refused: ${error.message}`); return 2 }
313
+ const done = { create: 'created', link: 'linked to AGENTS.md', update: 'updated', unchanged: 'unchanged', blocked: 'blocked' }
314
+ const conflicts = []
315
+ for (const r of results) {
316
+ for (const conflict of r.conflicts ?? []) conflicts.push(`${r.file}: ${conflict}`)
317
+ log(` ${r.file}: ${write || r.action === 'unchanged' ? done[r.action] : `would ${r.action}`}`)
318
+ }
319
+ const changed = results.filter((r) => !['unchanged', 'blocked'].includes(r.action)).length
114
320
  log(write
115
321
  ? `persist-conventions: ${section.title}: ${changed} file(s) written, ${results.length - changed} unchanged`
116
322
  : `persist-conventions: ${section.title}: preview only, ${changed} file(s) would change; re-run with --write to write`)
117
- return 0
323
+ if (conflicts.length) {
324
+ for (const conflict of conflicts) log(` conflict: ${conflict}`)
325
+ log(`persist-conventions: ${section.title}: ${conflicts.length} conflict(s); no files written; reconcile before setup`)
326
+ }
327
+ return conflicts.length ? 2 : 0
118
328
  }