@lifeaitools/rdc-skills 0.37.2 → 0.37.4
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/.claude-plugin/plugin.json +1 -1
- package/clauth-plugin.json +1 -1
- package/git-sha.json +1 -1
- package/guides/lessons-learned-spec.md +16 -14
- package/package.json +2 -2
- package/scripts/rdc-release.mjs +91 -0
- package/skills/housekeeping/SKILL.md +8 -5
- package/skills/release/SKILL.md +13 -7
- package/skills/rpms-filemap/SKILL.cloud.md +1 -1
- package/skills/rpms-filemap/SKILL.md +1 -1
- package/tests/release-guard.test.mjs +93 -0
- package/tests/release-skill-contract.test.mjs +14 -0
package/clauth-plugin.json
CHANGED
package/git-sha.json
CHANGED
|
@@ -35,7 +35,7 @@ propose-rule for one domain. This generalizes that pattern fleet-wide.
|
|
|
35
35
|
|
|
36
36
|
### Why lessons are not files any more
|
|
37
37
|
|
|
38
|
-
Until 2026-09 every lesson was a markdown file in the capturing repository's
|
|
38
|
+
Until 2026-09-17 every lesson was a markdown file in the capturing repository's
|
|
39
39
|
`.rdc/lessons/`. A census of one repository found 315 of them: 157 already enforced in code,
|
|
40
40
|
where the file only repeated what a guard or test already guaranteed, and 32 enforced
|
|
41
41
|
nowhere, sitting in a directory the owner of the fix never reads. A lesson filed where it
|
|
@@ -48,6 +48,13 @@ So the record now follows the fix:
|
|
|
48
48
|
- **An unencoded lesson is a GitHub issue in the repository the fix belongs to**, labelled
|
|
49
49
|
`lesson` + `proposal`. **An issue proposes; a work item commits.**
|
|
50
50
|
|
|
51
|
+
### Retired on-disk log
|
|
52
|
+
|
|
53
|
+
The old lesson log and its `.session-*.done` sentinels were retired from the active
|
|
54
|
+
regen-root tree by `247571602` on 2026-09-17. They remain recoverable from Git history;
|
|
55
|
+
they are not a conversion backlog and must not be recreated. The active record is now the
|
|
56
|
+
owning-repository lesson issue, its work item when accepted, and the enforcing commit.
|
|
57
|
+
|
|
51
58
|
---
|
|
52
59
|
|
|
53
60
|
## When to capture (at skill exit)
|
|
@@ -218,9 +225,8 @@ the command exits 1. Treat that as an incomplete triage, never as "no lessons th
|
|
|
218
225
|
API/MCP changes, skill-contract changes affecting multiple skills). When unsure, it is
|
|
219
226
|
architectural.
|
|
220
227
|
3. Route per the mode below.
|
|
221
|
-
4.
|
|
222
|
-
|
|
223
|
-
closed-won't-fix counts with issue URLs, plus backlog files converted and remaining.
|
|
228
|
+
4. Summarize in the housekeeping report: open / recurred / accepted / closed-fixed /
|
|
229
|
+
closed-won't-fix counts with issue URLs.
|
|
224
230
|
|
|
225
231
|
### Attended mode (default — a human is present)
|
|
226
232
|
|
|
@@ -255,17 +261,13 @@ reason, is the audit trail.
|
|
|
255
261
|
|
|
256
262
|
---
|
|
257
263
|
|
|
258
|
-
##
|
|
259
|
-
|
|
260
|
-
Files written under the old procedure are retired by state: an encoded lesson's file is
|
|
261
|
-
deleted and links to it point at the enforcing artifact; an unencoded one becomes an issue in
|
|
262
|
-
its owning repository. **The files that remain — doc-only lessons and product lessons — are a
|
|
263
|
-
backlog to convert, not a place to capture.** Each is done when a guard or test carries it:
|
|
264
|
-
ship that artifact, point any live link at it, and delete the file in the same commit.
|
|
264
|
+
## Historical records and archive policy
|
|
265
265
|
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
266
|
+
Do not create, scan, or modify `.rdc/lessons/` as part of normal capture or triage. The
|
|
267
|
+
retired files are preserved by Git history. Lesson issues are also never deleted: a closed
|
|
268
|
+
issue retains its evidence, comments, labels, and linked commit and can be reopened if the
|
|
269
|
+
lesson recurs. Close an issue only after the fixing commit lands or with a clear won't-fix /
|
|
270
|
+
duplicate reason; leave an unaccepted `proposal` issue open as the active queue.
|
|
269
271
|
|
|
270
272
|
---
|
|
271
273
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lifeaitools/rdc-skills",
|
|
3
|
-
"version": "0.37.
|
|
3
|
+
"version": "0.37.4",
|
|
4
4
|
"description": "RDC typed-agent dispatch skill suite for Claude Code - plan, build, review, overnight builds",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"claude-code",
|
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
"type": "plugin",
|
|
20
20
|
"skills": "skills/",
|
|
21
21
|
"guides": "guides/",
|
|
22
|
-
"version": "0.37.
|
|
22
|
+
"version": "0.37.4",
|
|
23
23
|
"commands": "commands/"
|
|
24
24
|
},
|
|
25
25
|
"bin": {
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Commit and tag one declared release change set.
|
|
4
|
+
*
|
|
5
|
+
* The wrapper owns the only documented commit/tag path so the staged-path
|
|
6
|
+
* guard runs while there is still something to reject.
|
|
7
|
+
*/
|
|
8
|
+
import { execFileSync, spawnSync } from 'node:child_process';
|
|
9
|
+
import path from 'node:path';
|
|
10
|
+
|
|
11
|
+
function fail(message) {
|
|
12
|
+
process.stderr.write(`rdc-release: ${message}\n`);
|
|
13
|
+
process.exit(2);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function git(repo, args, options = {}) {
|
|
17
|
+
return execFileSync('git', args, { cwd: repo, encoding: 'utf8', ...options });
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
function readArgs(argv) {
|
|
21
|
+
const result = { allow: [], dryRun: false };
|
|
22
|
+
for (let index = 0; index < argv.length; index += 1) {
|
|
23
|
+
const arg = argv[index];
|
|
24
|
+
if (arg === '--dry-run') {
|
|
25
|
+
result.dryRun = true;
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
if (!['--repo', '--allow', '--message', '--tag'].includes(arg)) fail(`unknown argument ${arg}`);
|
|
29
|
+
const value = argv[index + 1];
|
|
30
|
+
if (!value || value.startsWith('--')) fail(`${arg} requires a value`);
|
|
31
|
+
index += 1;
|
|
32
|
+
if (arg === '--allow') result.allow.push(value);
|
|
33
|
+
else result[arg.slice(2)] = value;
|
|
34
|
+
}
|
|
35
|
+
return result;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function normalizedPath(value) {
|
|
39
|
+
const normalized = value.replaceAll('\\', '/');
|
|
40
|
+
if (!normalized || path.posix.isAbsolute(normalized) || normalized.split('/').includes('..')) {
|
|
41
|
+
fail(`--allow must be a repository-relative path: ${value}`);
|
|
42
|
+
}
|
|
43
|
+
return normalized.replace(/^\.\//, '');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const options = readArgs(process.argv.slice(2));
|
|
47
|
+
if (!options.repo || !options.message || !options.tag || options.allow.length === 0) {
|
|
48
|
+
fail('usage: rdc-release --repo <path> --allow <path> [--allow <path>] --message <message> --tag <tag> [--dry-run]');
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const repo = path.resolve(options.repo);
|
|
52
|
+
try {
|
|
53
|
+
git(repo, ['rev-parse', '--is-inside-work-tree']);
|
|
54
|
+
} catch {
|
|
55
|
+
fail(`not a Git worktree: ${repo}`);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const allowed = new Set(options.allow.map(normalizedPath));
|
|
59
|
+
// A rename is two release-relevant paths: the removed source and the added
|
|
60
|
+
// destination. Disable Git's similarity presentation so neither can disappear
|
|
61
|
+
// behind a single destination path in the allowlist check.
|
|
62
|
+
const staged = git(repo, ['diff', '--cached', '--no-renames', '--name-only', '-z'])
|
|
63
|
+
.split('\0')
|
|
64
|
+
.filter(Boolean)
|
|
65
|
+
.map((value) => value.replaceAll('\\', '/'));
|
|
66
|
+
if (staged.length === 0) fail('no staged release paths; run this before committing');
|
|
67
|
+
|
|
68
|
+
const stagedSet = new Set(staged);
|
|
69
|
+
const undeclared = staged.filter((file) => !allowed.has(file));
|
|
70
|
+
const missing = [...allowed].filter((file) => !stagedSet.has(file));
|
|
71
|
+
if (undeclared.length || missing.length) {
|
|
72
|
+
const details = [
|
|
73
|
+
undeclared.length ? `undeclared staged path(s): ${undeclared.join(', ')}` : null,
|
|
74
|
+
missing.length ? `declared path(s) not staged: ${missing.join(', ')}` : null,
|
|
75
|
+
].filter(Boolean);
|
|
76
|
+
fail(details.join('; '));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const tagCheck = spawnSync('git', ['check-ref-format', `refs/tags/${options.tag}`], { cwd: repo, encoding: 'utf8' });
|
|
80
|
+
if (tagCheck.status !== 0) fail(`invalid tag: ${options.tag}`);
|
|
81
|
+
const existingTag = spawnSync('git', ['rev-parse', '--verify', '--quiet', `refs/tags/${options.tag}`], { cwd: repo, encoding: 'utf8' });
|
|
82
|
+
if (existingTag.status === 0) fail(`tag already exists: ${options.tag}`);
|
|
83
|
+
|
|
84
|
+
if (options.dryRun) {
|
|
85
|
+
process.stdout.write(`rdc-release: dry-run passed for ${staged.length} declared path(s); would commit and tag ${options.tag}\n`);
|
|
86
|
+
process.exit(0);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
git(repo, ['commit', '-m', options.message], { stdio: 'inherit' });
|
|
90
|
+
git(repo, ['tag', options.tag], { stdio: 'inherit' });
|
|
91
|
+
process.stdout.write(`rdc-release: committed ${staged.length} declared path(s) and created ${options.tag}\n`);
|
|
@@ -185,11 +185,14 @@ When the weekly triage runs unattended, follow `.rdc/guides/lessons-learned-spec
|
|
|
185
185
|
- **architectural with a single clear correct fix** (records an already-learned lesson — e.g. "add a gate", "encode X as a test") → accept, route through `rdc:plan` → `rdc:build` (or `rdc:fixit` if genuinely <5 files).
|
|
186
186
|
- **architectural and genuinely ambiguous** (multiple valid approaches, real tradeoffs) → write a `human_items` row (`item_type='decision'`, with options in `suggested_agent_prompt`, `source_type='lesson'`, `source_fingerprint` = the issue URL for dedupe), accept the issue against a linked `work_item`. Decided in the morning. This is the asynchronous equivalent of the attended interview and honors `.claude/rules/architectural-change-approval.md`.
|
|
187
187
|
|
|
188
|
-
###
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
188
|
+
### Retired lesson log
|
|
189
|
+
|
|
190
|
+
Do not scan, recreate, or convert `.rdc/lessons/*.md`. The on-disk log and its sentinels
|
|
191
|
+
were retired from the active tree by regen-root commit `247571602`; Git history preserves
|
|
192
|
+
them for recovery. New and unresolved lessons live as owning-repository `lesson` + `proposal`
|
|
193
|
+
issues. Issues are closed, never deleted — a closed issue with its fixing commit or won't-fix
|
|
194
|
+
reason is the archive. Report open / recurred / accepted / closed-fixed / closed-won't-fix
|
|
195
|
+
counts with issue URLs in the housekeeping report.
|
|
193
196
|
|
|
194
197
|
## Rules
|
|
195
198
|
- Never run `pnpm build` — not needed for this audit
|
package/skills/release/SKILL.md
CHANGED
|
@@ -118,13 +118,19 @@ Use repo-local package tooling when available. Examples:
|
|
|
118
118
|
npm version patch --no-git-tag-version
|
|
119
119
|
npm test
|
|
120
120
|
git add package.json package-lock.json
|
|
121
|
-
|
|
122
|
-
|
|
121
|
+
node scripts/rdc-release.mjs --repo <repo-path> \
|
|
122
|
+
--allow package.json --allow package-lock.json \
|
|
123
|
+
--message "chore(release): vA.B.C" --tag vA.B.C
|
|
123
124
|
git push origin HEAD
|
|
124
125
|
git push origin vA.B.C
|
|
125
126
|
npm view <package-name> version
|
|
126
127
|
```
|
|
127
128
|
|
|
129
|
+
The wrapper owns this documented commit-and-tag step. It compares the exact
|
|
130
|
+
staged paths with the declared `--allow` set and refuses undeclared or missing
|
|
131
|
+
paths before creating either a commit or a tag. Use `--dry-run` to prove the
|
|
132
|
+
set without mutating the repository.
|
|
133
|
+
|
|
128
134
|
Never use `--force` or bypass hooks. If a hook fails, fix the cause.
|
|
129
135
|
|
|
130
136
|
## ⛔ Standalone-repo staging guard (no `git add -A`)
|
|
@@ -136,11 +142,11 @@ doc-sync/scope guard to catch contamination. In them:
|
|
|
136
142
|
(`git add skills/<name>/SKILL.md package.json ...`).
|
|
137
143
|
- Run `git status --porcelain` FIRST. If untracked files exist that are not part
|
|
138
144
|
of the declared change, STOP — list or stash them; do not sweep them in.
|
|
139
|
-
- **
|
|
140
|
-
|
|
141
|
-
untracked skill files into a tagged release that CI published
|
|
142
|
-
noticed (retired lesson 2026-06-08-release-git-add-all-swept-untracked-wip).
|
|
143
|
-
dirty-tree contamination class as a lockfile generated against a dirty tree.
|
|
145
|
+
- **Release-scope guard: run `scripts/rdc-release.mjs` before committing. Refuse
|
|
146
|
+
undeclared staged paths before creating the commit or tag.** A broad add swept
|
|
147
|
+
4 pre-existing untracked skill files into a tagged release that CI published
|
|
148
|
+
before anyone noticed (retired lesson 2026-06-08-release-git-add-all-swept-untracked-wip).
|
|
149
|
+
Same dirty-tree contamination class as a lockfile generated against a dirty tree.
|
|
144
150
|
|
|
145
151
|
## ⛔ Cross-platform prepack + verify the PUBLISHED tarball
|
|
146
152
|
|
|
@@ -91,7 +91,7 @@ If you are touching an entity, read its generated `_context.md` before any local
|
|
|
91
91
|
| `operational-door.rdc.plans` | `.rdc/areas/**/{plans,research,reports,decisions,logs,arch,guides}/**` | `authored` | One Home Per Fact, One Direction | `allow_declared_home`<br />`path_conformance` | RDC planning workflow | `active` |
|
|
92
92
|
| `operational-door.rdc.plans.compatibility` | `.rdc/plans/**` | `generated` | One Home Per Fact, One Direction | `allow_declared_home`<br />`path_conformance`<br />Retire only through the approved area-first-v1 activation receipt. | RDC path migration compiler | `active` |
|
|
93
93
|
| `operational-door.rdc.reports` | `.rdc/reports/**` | `generated` | One Direction | `allow_declared_home`<br />`path_conformance` | rdc:report | `active` |
|
|
94
|
-
| `operational-door.rdc.
|
|
94
|
+
| `operational-door.rdc.lesson-issues` | `GitHub: owning repository issues labelled lesson` | `stateful` | One Direction, One Home Per Fact | `rdc-lesson submit/accept/close`<br />`issue labels: proposal -> accepted -> closed` | RDC lesson capture workflow | `active` |
|
|
95
95
|
| `operational-door.rpms-filemap.rule` | `.claude/rules/rpms-filemap.md`<br />created_by: `scripts/rpms/gen-filemap.ts` | `generated` | One Home Per Fact, One Direction, One Door | `block_manual_edit`<br />`projection_drift` | scripts/rpms/gen-filemap.ts | `active` |
|
|
96
96
|
|
|
97
97
|
### App Guides
|
|
@@ -97,7 +97,7 @@ If you are touching an entity, read its generated `_context.md` before any local
|
|
|
97
97
|
| `operational-door.rdc.plans` | `.rdc/areas/**/{plans,research,reports,decisions,logs,arch,guides}/**` | `authored` | One Home Per Fact, One Direction | `allow_declared_home`<br />`path_conformance` | RDC planning workflow | `active` |
|
|
98
98
|
| `operational-door.rdc.plans.compatibility` | `.rdc/plans/**` | `generated` | One Home Per Fact, One Direction | `allow_declared_home`<br />`path_conformance`<br />Retire only through the approved area-first-v1 activation receipt. | RDC path migration compiler | `active` |
|
|
99
99
|
| `operational-door.rdc.reports` | `.rdc/reports/**` | `generated` | One Direction | `allow_declared_home`<br />`path_conformance` | rdc:report | `active` |
|
|
100
|
-
| `operational-door.rdc.
|
|
100
|
+
| `operational-door.rdc.lesson-issues` | `GitHub: owning repository issues labelled lesson` | `stateful` | One Direction, One Home Per Fact | `rdc-lesson submit/accept/close`<br />`issue labels: proposal -> accepted -> closed` | RDC lesson capture workflow | `active` |
|
|
101
101
|
| `operational-door.rpms-filemap.rule` | `.claude/rules/rpms-filemap.md`<br />created_by: `scripts/rpms/gen-filemap.ts` | `generated` | One Home Per Fact, One Direction, One Door | `block_manual_edit`<br />`projection_drift` | scripts/rpms/gen-filemap.ts | `active` |
|
|
102
102
|
|
|
103
103
|
### App Guides
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { execFileSync, spawnSync } from 'node:child_process';
|
|
3
|
+
import { mkdtempSync, rmSync, writeFileSync } from 'node:fs';
|
|
4
|
+
import path from 'node:path';
|
|
5
|
+
import { test } from 'node:test';
|
|
6
|
+
import { tmpdir } from 'node:os';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
|
|
9
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
10
|
+
const SCRIPT = path.join(ROOT, 'scripts', 'rdc-release.mjs');
|
|
11
|
+
|
|
12
|
+
function git(repo, args) {
|
|
13
|
+
return execFileSync('git', args, { cwd: repo, encoding: 'utf8' });
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function fixture() {
|
|
17
|
+
const repo = mkdtempSync(path.join(tmpdir(), 'rdc-release-guard-'));
|
|
18
|
+
git(repo, ['init']);
|
|
19
|
+
git(repo, ['config', 'user.email', 'test@example.invalid']);
|
|
20
|
+
git(repo, ['config', 'user.name', 'Release Guard Test']);
|
|
21
|
+
writeFileSync(path.join(repo, 'README.md'), 'base\n');
|
|
22
|
+
git(repo, ['add', 'README.md']);
|
|
23
|
+
git(repo, ['commit', '-m', 'base']);
|
|
24
|
+
return { repo, cleanup: () => rmSync(repo, { recursive: true, force: true }) };
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function run(repo, args) {
|
|
28
|
+
return spawnSync(process.execPath, [SCRIPT, '--repo', repo, ...args], { encoding: 'utf8' });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
test('release guard rejects an extra staged path and creates no tag', (t) => {
|
|
32
|
+
const sandbox = fixture();
|
|
33
|
+
t.after(sandbox.cleanup);
|
|
34
|
+
writeFileSync(path.join(sandbox.repo, 'package.json'), '{"version":"1.0.1"}\n');
|
|
35
|
+
writeFileSync(path.join(sandbox.repo, 'unrelated.md'), 'do not ship\n');
|
|
36
|
+
git(sandbox.repo, ['add', 'package.json', 'unrelated.md']);
|
|
37
|
+
|
|
38
|
+
const result = run(sandbox.repo, ['--allow', 'package.json', '--message', 'release: v1.0.1', '--tag', 'v1.0.1']);
|
|
39
|
+
assert.equal(result.status, 2, result.stderr);
|
|
40
|
+
assert.match(result.stderr, /undeclared staged path\(s\): unrelated\.md/);
|
|
41
|
+
assert.equal(git(sandbox.repo, ['tag', '--list']).trim(), '');
|
|
42
|
+
assert.equal(git(sandbox.repo, ['log', '--format=%s', '-1']).trim(), 'base');
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test('release guard rejects a rename whose removed source is not declared', (t) => {
|
|
46
|
+
const sandbox = fixture();
|
|
47
|
+
t.after(sandbox.cleanup);
|
|
48
|
+
git(sandbox.repo, ['mv', 'README.md', 'package.json']);
|
|
49
|
+
|
|
50
|
+
const result = run(sandbox.repo, ['--allow', 'package.json', '--message', 'release: v1.0.1', '--tag', 'v1.0.1']);
|
|
51
|
+
assert.equal(result.status, 2, result.stderr);
|
|
52
|
+
assert.match(result.stderr, /undeclared staged path\(s\): README\.md/);
|
|
53
|
+
assert.equal(git(sandbox.repo, ['tag', '--list']).trim(), '');
|
|
54
|
+
assert.equal(git(sandbox.repo, ['log', '--format=%s', '-1']).trim(), 'base');
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test('release guard rejects a declared path that was not staged', (t) => {
|
|
58
|
+
const sandbox = fixture();
|
|
59
|
+
t.after(sandbox.cleanup);
|
|
60
|
+
writeFileSync(path.join(sandbox.repo, 'package.json'), '{"version":"1.0.1"}\n');
|
|
61
|
+
git(sandbox.repo, ['add', 'package.json']);
|
|
62
|
+
|
|
63
|
+
const result = run(sandbox.repo, ['--allow', 'package.json', '--allow', 'README.md', '--message', 'release: v1.0.1', '--tag', 'v1.0.1']);
|
|
64
|
+
assert.equal(result.status, 2, result.stderr);
|
|
65
|
+
assert.match(result.stderr, /declared path\(s\) not staged: README\.md/);
|
|
66
|
+
assert.equal(git(sandbox.repo, ['tag', '--list']).trim(), '');
|
|
67
|
+
assert.equal(git(sandbox.repo, ['log', '--format=%s', '-1']).trim(), 'base');
|
|
68
|
+
});
|
|
69
|
+
|
|
70
|
+
test('release guard commits and tags only the declared staged set', (t) => {
|
|
71
|
+
const sandbox = fixture();
|
|
72
|
+
t.after(sandbox.cleanup);
|
|
73
|
+
writeFileSync(path.join(sandbox.repo, 'package.json'), '{"version":"1.0.1"}\n');
|
|
74
|
+
git(sandbox.repo, ['add', 'package.json']);
|
|
75
|
+
|
|
76
|
+
const result = run(sandbox.repo, ['--allow', 'package.json', '--message', 'release: v1.0.1', '--tag', 'v1.0.1']);
|
|
77
|
+
assert.equal(result.status, 0, result.stderr);
|
|
78
|
+
assert.equal(git(sandbox.repo, ['tag', '--list']).trim(), 'v1.0.1');
|
|
79
|
+
assert.deepEqual(git(sandbox.repo, ['diff-tree', '--no-commit-id', '--name-only', '-r', 'HEAD']).trim().split('\n'), ['package.json']);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test('release guard dry-run validates the exact set without committing or tagging', (t) => {
|
|
83
|
+
const sandbox = fixture();
|
|
84
|
+
t.after(sandbox.cleanup);
|
|
85
|
+
writeFileSync(path.join(sandbox.repo, 'package.json'), '{"version":"1.0.1"}\n');
|
|
86
|
+
git(sandbox.repo, ['add', 'package.json']);
|
|
87
|
+
|
|
88
|
+
const result = run(sandbox.repo, ['--allow', 'package.json', '--message', 'release: v1.0.1', '--tag', 'v1.0.1', '--dry-run']);
|
|
89
|
+
assert.equal(result.status, 0, result.stderr);
|
|
90
|
+
assert.match(result.stdout, /dry-run passed/);
|
|
91
|
+
assert.equal(git(sandbox.repo, ['tag', '--list']).trim(), '');
|
|
92
|
+
assert.equal(git(sandbox.repo, ['status', '--short']).trim(), 'A package.json');
|
|
93
|
+
});
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { readFileSync } from 'node:fs';
|
|
3
|
+
import path from 'node:path';
|
|
4
|
+
import { test } from 'node:test';
|
|
5
|
+
import { fileURLToPath } from 'node:url';
|
|
6
|
+
|
|
7
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
8
|
+
const skill = readFileSync(path.join(ROOT, 'skills', 'release', 'SKILL.md'), 'utf8');
|
|
9
|
+
|
|
10
|
+
test('release skill routes the scoped commit and tag through the executable guard', () => {
|
|
11
|
+
assert.match(skill, /node scripts\/rdc-release\.mjs --repo <repo-path>/);
|
|
12
|
+
assert.match(skill, /--allow package\.json --allow package-lock\.json/);
|
|
13
|
+
assert.match(skill, /staged paths.*before creating either a commit or a tag/is);
|
|
14
|
+
});
|