@1aboveio/skills 1.0.14 → 1.0.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.codex-plugin/plugin.json +1 -1
- package/.codex-plugin/resources.json +164 -160
- package/package.json +4 -4
- package/skills/app-dev/airflow/SKILL.md +1 -1
- package/skills/app-dev/android-development/SKILL.md +30 -2
- package/skills/app-dev/android-development/references/diagnosis.md +55 -0
- package/skills/app-dev/android-development/references/feature-work.md +15 -0
- package/skills/app-dev/android-development/references/init.md +57 -6
- package/skills/app-dev/android-development/references/setup.md +8 -2
- package/skills/app-dev/android-development/references/ui-journeys.md +58 -0
- package/skills/app-dev/android-development/scripts/device-run.sh +93 -2
- package/skills/app-dev/android-development/scripts/failure_capture.py +331 -0
- package/skills/app-dev/android-development/scripts/init.mjs +234 -17
- package/skills/app-dev/android-development/scripts/maestro_failure.py +401 -0
- package/skills/app-dev/android-development/scripts/test_failure_capture.py +378 -0
- package/skills/app-dev/android-development/scripts/test_maestro_failure.py +462 -0
- package/skills/app-dev/app-debug/SKILL.md +1 -1
- package/skills/app-dev/better-auth/SKILL.md +1 -1
- package/skills/app-dev/data-catalog/SKILL.md +53 -16
- package/skills/app-dev/data-catalog/scripts/persist-conventions-core.mjs +231 -21
- package/skills/app-dev/data-catalog/scripts/persist-conventions.mjs +12 -1
- package/skills/app-dev/data-quality/SKILL.md +1 -1
- package/skills/app-dev/ios-foundation/SKILL.md +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 +10 -1
- package/skills/cicd/github-flow/scripts/persist-conventions-core.mjs +231 -21
- package/skills/cicd/github-flow/scripts/persist-conventions.mjs +18 -1
- package/skills/cicd/google-cloud/SKILL.md +1 -1
- package/skills/cicd/mergify/SKILL.md +1 -1
- package/skills/cicd/podman/SKILL.md +1 -1
- package/skills/content/article-review/SKILL.md +1 -1
- package/skills/content/article-to-image/SKILL.md +1 -1
- package/skills/content/flowchart-svg/SKILL.md +1 -1
- package/skills/content/image-gen/SKILL.md +1 -1
- package/skills/content/mermaid/SKILL.md +1 -1
- package/skills/content/wechat-article-writer/SKILL.md +1 -1
- package/skills/content/wechat-publisher/SKILL.md +1 -1
- package/skills/content/wechat-search/SKILL.md +1 -1
- package/skills/content/xai-search/SKILL.md +1 -1
- package/skills/core/distill-writing/SKILL.md +7 -1
- package/skills/core/doc-to-md/SKILL.md +1 -1
- package/skills/core/editorial-review/SKILL.md +1 -1
- package/skills/core/house-style/SKILL.md +1 -1
- package/skills/core/house-style/references/persistence.md +8 -3
- package/skills/core/house-style/scripts/init-guidance.mjs +1 -1
- package/skills/core/plain-language/SKILL.md +1 -1
- package/skills/core/polish-cn/SKILL.md +1 -1
- package/skills/core/reg-parser/SKILL.md +1 -1
- package/skills/core/terminology/SKILL.md +1 -1
- package/skills/core/terminology/references/init.md +11 -0
- package/skills/core/write-doc/SKILL.md +1 -1
- package/skills/core/write-report/SKILL.md +1 -1
- package/skills/engineering/better-review/SKILL.md +1 -1
- package/skills/engineering/e2e-test/SKILL.md +1 -1
- package/skills/engineering/engineering/SKILL.md +19 -4
- package/skills/engineering/engineering/references/init-scope.md +5 -2
- package/skills/engineering/engineering/references/scope-management.md +4 -2
- package/skills/engineering/engineering/scripts/init.mjs +34 -9
- package/skills/engineering/engineering/scripts/persist-conventions-core.mjs +231 -21
- package/skills/engineering/engineering/scripts/persist-conventions.mjs +48 -21
- package/skills/engineering/engineering-runtime/SKILL.md +1 -1
- package/skills/engineering/engineering-runtime/coherence/workflow.json +13 -13
- package/skills/engineering/ensure-coverage/SKILL.md +1 -1
- package/skills/engineering/golden-path/SKILL.md +1 -1
- package/skills/engineering/harness-runtime/SKILL.md +4 -3
- package/skills/engineering/harness-runtime/agent-catalog.mjs +928 -0
- package/skills/engineering/harness-runtime/contracts.md +2 -0
- package/skills/engineering/harness-runtime/references/model-catalog.md +7 -1
- package/skills/engineering/resolve-issues/SKILL.md +9 -6
- package/skills/engineering/resolve-issues/references/delivery-progress.md +2 -0
- package/skills/engineering/resolve-issues/references/expire.md +6 -3
- package/skills/engineering/resolve-issues/references/implementation.md +6 -1
- package/skills/engineering/resolve-issues/references/preflight.md +26 -17
- package/skills/engineering/resolve-issues/references/review.md +39 -4
- package/skills/engineering/resolve-issues/references/shared-modules.md +88 -20
- package/skills/engineering/resolve-issues/references/task-plan.md +35 -1
- package/skills/engineering/resolve-issues/scripts/preflight-models.mjs +382 -592
- package/skills/engineering/resolve-release/SKILL.md +1 -1
- package/skills/engineering/rush-issues/SKILL.md +4 -4
- package/skills/engineering/rush-release/SKILL.md +1 -1
- package/skills/engineering/test-quality/SKILL.md +3 -1
- package/skills/engineering/test-quality/scripts/persist-conventions-core.mjs +231 -21
- package/skills/engineering/test-quality/scripts/persist-conventions.mjs +13 -1
- package/skills/risk-compliance/compliance-audit/SKILL.md +1 -1
- package/skills/risk-compliance/contract-compare/SKILL.md +1 -1
- package/skills/risk-compliance/contract-review/SKILL.md +1 -1
- package/skills/risk-compliance/doc-authenticity/SKILL.md +1 -1
- package/skills/risk-compliance/fraud-detection/SKILL.md +1 -1
- package/skills/risk-compliance/payment-analysis/SKILL.md +1 -1
- package/skills/risk-compliance/portfolio-payment-analysis/SKILL.md +1 -1
- package/skills/risk-compliance/similar-domain/SKILL.md +1 -1
- package/skills/risk-compliance/tld-detection/SKILL.md +1 -1
- package/skills/engineering/resolve-issues/scripts/discover-models.mjs +0 -9
- package/skills/engineering/resolve-issues/scripts/model-catalog.mjs +0 -9
|
@@ -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.48.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.
|
|
4
|
+
release-version: 0.48.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.
|
|
4
|
+
release-version: 0.48.0
|
|
5
5
|
---
|
|
6
6
|
**Updates:** Once/session run `node <skill-base-dir>/release-check.mjs`.
|
|
7
7
|
# Engineering
|
|
@@ -140,7 +140,7 @@ 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 pointer to `CONTRIBUTING.md
|
|
143
|
+
| `AGENTS.md` | Missing pointer to `CONTRIBUTING.md` and the short skill task routing rule; no policy sections |
|
|
144
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),
|
|
@@ -152,12 +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
|
|
155
|
+
Project policy lives in `CONTRIBUTING.md`; `AGENTS.md` carries only the loaders. A
|
|
156
156
|
`Feature delivery` section already in `AGENTS.md`, including one an earlier init wrote,
|
|
157
157
|
stays where it is: init neither migrates nor deletes it, and it does not add a competing
|
|
158
158
|
copy to `CONTRIBUTING.md`. Existing `CONTRIBUTING.md` policy wins the same way.
|
|
159
159
|
Initialization moves no existing rules between files and refreshes neither.
|
|
160
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
|
+
|
|
161
174
|
Existing sections, including legacy `conventions:tracker` blocks written by
|
|
162
175
|
`contributing`, are preserved byte-for-byte. Repeated init is not a refresh and
|
|
163
176
|
leaves manual edits unchanged. No force/update mode is implied. If policy needs to
|
|
@@ -192,7 +205,9 @@ Before creating work:
|
|
|
192
205
|
product acceptance.
|
|
193
206
|
|
|
194
207
|
The retained tracker-only helper supports explicit requests to persist or update
|
|
195
|
-
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:
|
|
196
211
|
|
|
197
212
|
```sh
|
|
198
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 and tracker routing, written as `CONTRIBUTING.md` sections; `AGENTS.md` gets
|
|
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 |
|
|
@@ -38,7 +38,10 @@ 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
40
|
Project policy is persisted in `CONTRIBUTING.md`, so `AGENTS.md` keeps only the short
|
|
41
|
-
|
|
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
|
|
42
45
|
stays there: init does not migrate or delete it, and it does not add a competing copy
|
|
43
46
|
to `CONTRIBUTING.md`. Delivery, CI, and Data Catalog conventions require separate
|
|
44
47
|
explicit setup with their owning skills. Setup may point to these capabilities without
|
|
@@ -44,8 +44,10 @@ comments before recording their hashes. Do not rewrap existing text or claim it
|
|
|
44
44
|
owned. Keep existing valid records and add only new entries; deduplicate entries.
|
|
45
45
|
|
|
46
46
|
Project policy sections, including `Feature delivery` (`conventions:feature-delivery`),
|
|
47
|
-
live in `CONTRIBUTING.md`; `AGENTS.md` holds only the
|
|
48
|
-
|
|
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.
|
|
49
51
|
|
|
50
52
|
Set `createdFile` only when this save created that file. Preserve unrelated bytes
|
|
51
53
|
and existing links. Do not create a sidecar, global settings entry, or project file
|
|
@@ -3,9 +3,21 @@ import fs from 'node:fs'
|
|
|
3
3
|
import path from 'node:path'
|
|
4
4
|
import { spawnSync } from 'node:child_process'
|
|
5
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
|
-
|
|
49
|
-
|
|
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) {
|
|
@@ -65,6 +78,7 @@ function sectionState(current, title, id) {
|
|
|
65
78
|
return Boolean(starts) || current.split(/\r?\n/).some(line => line.trim() === `## ${title}`)
|
|
66
79
|
}
|
|
67
80
|
|
|
81
|
+
|
|
68
82
|
function addSection(current, title, id, body) {
|
|
69
83
|
if (sectionState(current, title, id)) return current
|
|
70
84
|
return append(current, `<!-- conventions:${id}:start -->\n${body.trim()}\n<!-- conventions:${id}:end -->`)
|
|
@@ -121,7 +135,7 @@ function planInitialization(root, options) {
|
|
|
121
135
|
throw new Error('agent instructions and CONTRIBUTING.md must use distinct files')
|
|
122
136
|
}
|
|
123
137
|
|
|
124
|
-
// AGENTS.md loads
|
|
138
|
+
// AGENTS.md routes tasks and loads policy; it does not carry the policy itself.
|
|
125
139
|
const agentBase = agents.current ?? '# Agent instructions\n'
|
|
126
140
|
// An earlier release put Feature delivery in AGENTS.md. That policy stays where it is:
|
|
127
141
|
// init neither migrates it nor adds a competing copy to CONTRIBUTING.md.
|
|
@@ -130,6 +144,9 @@ function planInitialization(root, options) {
|
|
|
130
144
|
if (!hasLoader(agentText, agents, contributing)) {
|
|
131
145
|
agentText = append(agentText, `## Contributing\n\nFollow [CONTRIBUTING.md](${linkFrom(agents, contributing)}) for this project's engineering conventions and existing delivery requirements.`)
|
|
132
146
|
}
|
|
147
|
+
const routing = withTaskEntries(agentText, taskLoaders)
|
|
148
|
+
if (routing.conflicts.length) throw new Error(routing.conflicts.join('; '))
|
|
149
|
+
agentText = routing.text
|
|
133
150
|
|
|
134
151
|
let contributingText = contributing.current ?? '# Contributing\n'
|
|
135
152
|
const featureDeliveryInContributing = sectionState(contributingText, 'Feature delivery', 'feature-delivery')
|
|
@@ -144,14 +161,22 @@ function planInitialization(root, options) {
|
|
|
144
161
|
contributingText = addSection(contributingText, 'Issue tracking', 'tracker',
|
|
145
162
|
trackerBody(options.githubRepo ?? githubRepo(root), options.linearProjects))
|
|
146
163
|
|
|
164
|
+
const entries = [
|
|
165
|
+
{ ...agents, next: agentText },
|
|
166
|
+
{ ...contributing, next: contributingText },
|
|
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
|
+
}
|
|
147
173
|
return {
|
|
148
|
-
entries
|
|
149
|
-
|
|
150
|
-
|
|
174
|
+
entries,
|
|
175
|
+
notes: [
|
|
176
|
+
...(featureDeliveryInAgents
|
|
177
|
+
? ['Feature delivery stays in AGENTS.md; no competing section added to CONTRIBUTING.md']
|
|
178
|
+
: []),
|
|
151
179
|
],
|
|
152
|
-
notes: featureDeliveryInAgents
|
|
153
|
-
? ['Feature delivery stays in AGENTS.md; no competing section added to CONTRIBUTING.md']
|
|
154
|
-
: [],
|
|
155
180
|
}
|
|
156
181
|
}
|
|
157
182
|
|
|
@@ -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
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
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
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
87
|
-
|
|
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
|
-
|
|
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
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
const
|
|
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
|
-
|
|
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
|
}
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
// persist-conventions — the tracker routing rules as the `## Issue tracking`
|
|
2
|
-
// section of a repo's CONTRIBUTING.md (../SKILL.md,
|
|
2
|
+
// section of a repo's CONTRIBUTING.md (../SKILL.md, Tracker routing).
|
|
3
3
|
// Previews unless --write; --if-missing leaves an existing section alone.
|
|
4
|
+
// --write also adds the skill's task-entry loader for issue and spec creation to
|
|
5
|
+
// AGENTS.md (and to a regular CLAUDE.md that does not reach AGENTS.md).
|
|
4
6
|
//
|
|
5
7
|
// node persist-conventions.mjs [--repo-root <dir>] [--github-repo <owner/name>]
|
|
6
8
|
// [--linear-project <name>]... [--if-missing] [--write]
|
|
@@ -10,11 +12,24 @@ import fs from 'node:fs'
|
|
|
10
12
|
import path from 'node:path'
|
|
11
13
|
import { spawnSync } from 'node:child_process'
|
|
12
14
|
import { fileURLToPath } from 'node:url'
|
|
13
|
-
import { CONTRIBUTING_FILE,
|
|
15
|
+
import { CONTRIBUTING_FILE, markers, persistIndex, runCli } from './persist-conventions-core.mjs'
|
|
14
16
|
|
|
15
17
|
const ID = 'tracker'
|
|
16
18
|
const TEMPLATE = fs.readFileSync(fileURLToPath(new URL('../references/tracker-conventions.md', import.meta.url)), 'utf8')
|
|
17
19
|
|
|
20
|
+
// Scoped to issue and spec creation: this is tracker persistence, not a general
|
|
21
|
+
// invitation to load the engineering defaults into every task.
|
|
22
|
+
const LOADER = {
|
|
23
|
+
id: 'tracker',
|
|
24
|
+
skill: 'engineering',
|
|
25
|
+
title: 'Tracker task entry',
|
|
26
|
+
body: [
|
|
27
|
+
'Before creating an issue, spec, epic, or sub-issue, read and apply the installed',
|
|
28
|
+
"`engineering` skill's tracker routing. This setup installs tracker routing only;",
|
|
29
|
+
"the project's routing rules stay in its contributor guide. It selects no other workflow.",
|
|
30
|
+
].join('\n'),
|
|
31
|
+
}
|
|
32
|
+
|
|
18
33
|
function githubRepoFromRemote(url) {
|
|
19
34
|
const m = /github\.com[:/]([\w.-]+\/[\w.-]+?)(?:\.git)?\/?$/.exec(url.trim())
|
|
20
35
|
return m ? m[1] : null
|
|
@@ -27,22 +42,18 @@ function renderBody(githubRepo, linearProjects) {
|
|
|
27
42
|
return TEMPLATE.replaceAll('{{githubRepo}}', githubRepo).replaceAll('{{linearProjects}}', linear)
|
|
28
43
|
}
|
|
29
44
|
|
|
30
|
-
// The section stays as it is; only a missing AGENTS.md
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
if (
|
|
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)
|
|
45
|
+
// The section stays as it is; only a missing AGENTS.md pointer or CLAUDE.md link and
|
|
46
|
+
// the missing task-entry loader are added.
|
|
47
|
+
function indexOnly(repoRoot, section, write, log) {
|
|
48
|
+
const results = persistIndex(repoRoot, section, { write })
|
|
49
|
+
const conflicts = results.flatMap(result => result.conflicts ?? [])
|
|
50
|
+
for (const result of results) {
|
|
51
|
+
for (const conflict of result.conflicts ?? []) log(` ${result.file}: ${conflict}`)
|
|
52
|
+
if (result.action === 'blocked') log(` ${result.file}: blocked`)
|
|
53
|
+
else if (result.action !== 'unchanged') log(` ${result.file}: ${write ? 'written' : 'would change'}`)
|
|
44
54
|
}
|
|
45
|
-
|
|
55
|
+
const changed = results.filter((result) => !['unchanged', 'blocked'].includes(result.action)).length
|
|
56
|
+
return { summary: changed ? `${changed} index file(s) ${write ? 'written' : 'would change'}` : 'nothing written', blocked: conflicts.length > 0 }
|
|
46
57
|
}
|
|
47
58
|
|
|
48
59
|
function main(argv, { log = console.log } = {}) {
|
|
@@ -73,12 +84,28 @@ function main(argv, { log = console.log } = {}) {
|
|
|
73
84
|
const file = path.join(repoRoot, CONTRIBUTING_FILE)
|
|
74
85
|
const current = fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : ''
|
|
75
86
|
if (current.includes(markers(ID)[0])) {
|
|
76
|
-
|
|
77
|
-
|
|
87
|
+
const section = sectionFor(githubRepo, linearProjects)
|
|
88
|
+
const indexed = indexOnly(repoRoot, section, argv.includes('--write'), log)
|
|
89
|
+
log(`persist-conventions: Issue tracking: section already present; ${indexed.summary}`)
|
|
90
|
+
return indexed.blocked ? 2 : 0
|
|
78
91
|
}
|
|
79
92
|
}
|
|
80
|
-
const section =
|
|
93
|
+
const section = sectionFor(githubRepo, linearProjects)
|
|
81
94
|
return runCli(rest, section, { log })
|
|
82
95
|
}
|
|
83
96
|
|
|
84
|
-
|
|
97
|
+
function sectionFor(githubRepo, linearProjects) {
|
|
98
|
+
return {
|
|
99
|
+
id: ID,
|
|
100
|
+
owner: 'engineering',
|
|
101
|
+
title: 'Issue tracking',
|
|
102
|
+
body: renderBody(githubRepo, linearProjects),
|
|
103
|
+
loader: LOADER,
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
try { process.exitCode = main(process.argv.slice(2)) }
|
|
108
|
+
catch (error) {
|
|
109
|
+
console.error(`persist-conventions: setup refused: ${error.message}`)
|
|
110
|
+
process.exitCode = 2
|
|
111
|
+
}
|
|
@@ -3,7 +3,7 @@ name: engineering-runtime
|
|
|
3
3
|
description: Internal support package for shared Engineering Workflow executable behavior. Never select or invoke this package as a user capability.
|
|
4
4
|
disable-model-invocation: true
|
|
5
5
|
user-invocable: false
|
|
6
|
-
release-version: 0.
|
|
6
|
+
release-version: 0.48.0
|
|
7
7
|
---
|
|
8
8
|
**Updates:** Once/session run `node <skill-base-dir>/release-check.mjs`.
|
|
9
9
|
# Engineering Runtime
|