@lifeaitools/rdc-skills 0.36.1 → 0.37.1
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/.github/workflows/publish.yml +12 -1
- package/CHANGELOG.md +60 -0
- package/README.md +1 -0
- package/RELEASE.md +11 -8
- package/clauth-plugin.json +1 -1
- package/commands/compare-compliance.md +5 -1
- package/commands/quick-check.md +3 -1
- package/git-sha.json +1 -1
- package/guides/review-gate.md +133 -0
- package/hooks/require-work-item-on-commit.js +11 -0
- package/package.json +4 -3
- package/scripts/lib/manifest-versions.mjs +154 -0
- package/scripts/self-test.mjs +11 -6
- package/scripts/solid-score.mjs +35 -6
- package/scripts/sync-manifest-versions.mjs +67 -0
- package/skills/build/SKILL.md +24 -21
- package/skills/clean-code-analyzer/SKILL.md +6 -0
- package/skills/fixit/SKILL.md +21 -20
- package/skills/overnight/SKILL.md +1 -1
- package/skills/release/SKILL.md +1 -1
- package/skills/review/SKILL.md +39 -32
- package/tests/lib/solid-score-diff.test.mjs +103 -0
- package/tests/manifest-version-parity.test.mjs +155 -0
- package/tests/require-work-item-on-commit.test.mjs +9 -0
- package/.rdc/lessons/2026-08-17-fixit-codex-mcp-only-skills.md +0 -24
- package/.rdc/lessons/2026-09-17-release-mcp-registry-split.md +0 -34
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* sync-manifest-versions.mjs — copy package.json's version into every other place the
|
|
4
|
+
* package declares it (scripts/lib/manifest-versions.mjs holds the list).
|
|
5
|
+
*
|
|
6
|
+
* Wired as the npm `version` lifecycle script (with --stage), so
|
|
7
|
+
* `npm version patch|minor|major` bumps package.json and then every manifest in the
|
|
8
|
+
* same step, and stages them for the version commit. Before this, plugin.json was a
|
|
9
|
+
* hand edit and clauth-plugin.json was never bumped at all (0.35.10 through 0.37.0).
|
|
10
|
+
*
|
|
11
|
+
* node scripts/sync-manifest-versions.mjs rewrite what differs, print it
|
|
12
|
+
* node scripts/sync-manifest-versions.mjs --stage the same, then `git add` every
|
|
13
|
+
* declaration file (npm version)
|
|
14
|
+
* node scripts/sync-manifest-versions.mjs --check change nothing; exit 1 on drift
|
|
15
|
+
*/
|
|
16
|
+
import { execFileSync } from 'node:child_process';
|
|
17
|
+
import { realpathSync } from 'node:fs';
|
|
18
|
+
import path from 'node:path';
|
|
19
|
+
import { fileURLToPath } from 'node:url';
|
|
20
|
+
import { VERSION_DECLARATIONS, describeValue, mismatches, syncDeclarations } from './lib/manifest-versions.mjs';
|
|
21
|
+
|
|
22
|
+
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
23
|
+
const log = (line) => console.log(`[sync-manifest-versions] ${line}`);
|
|
24
|
+
|
|
25
|
+
/** One spelling per directory: long names (not 8.3), and case-folded on Windows. */
|
|
26
|
+
function canonical(dir) {
|
|
27
|
+
const real = realpathSync.native(path.resolve(dir));
|
|
28
|
+
return process.platform === 'win32' ? real.toLowerCase() : real;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* True only when this package root IS the top of a git work tree. A package unpacked
|
|
33
|
+
* inside some other repository must not stage into that repository's index.
|
|
34
|
+
*/
|
|
35
|
+
function ownsGitWorkTree() {
|
|
36
|
+
try {
|
|
37
|
+
const top = execFileSync('git', ['rev-parse', '--show-toplevel'], {
|
|
38
|
+
cwd: root, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'],
|
|
39
|
+
}).trim();
|
|
40
|
+
return canonical(top) === canonical(root);
|
|
41
|
+
} catch {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
if (process.argv.includes('--check')) {
|
|
47
|
+
const drift = mismatches(root);
|
|
48
|
+
for (const d of drift) {
|
|
49
|
+
console.error(`[sync-manifest-versions] ${d.label} is ${describeValue(d)}, package.json is ${d.expected}`);
|
|
50
|
+
}
|
|
51
|
+
if (drift.length) process.exit(1);
|
|
52
|
+
log('every declaration matches package.json');
|
|
53
|
+
} else {
|
|
54
|
+
const changed = syncDeclarations(root);
|
|
55
|
+
for (const c of changed) log(`${c.label}: ${c.from} -> ${c.to}`);
|
|
56
|
+
if (!changed.length) log('nothing to change');
|
|
57
|
+
if (process.argv.includes('--stage')) {
|
|
58
|
+
// npm version skips its own git steps outside a work tree; so does this.
|
|
59
|
+
if (!ownsGitWorkTree()) {
|
|
60
|
+
log('this package is not the top of a git work tree — nothing staged');
|
|
61
|
+
} else {
|
|
62
|
+
const files = [...new Set(VERSION_DECLARATIONS.map((d) => d.file))];
|
|
63
|
+
execFileSync('git', ['add', '--', ...files], { cwd: root, stdio: 'inherit' });
|
|
64
|
+
log(`staged ${files.join(', ')}`);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
}
|
package/skills/build/SKILL.md
CHANGED
|
@@ -644,27 +644,30 @@ Read the task title and description, then:
|
|
|
644
644
|
|
|
645
645
|
⛔ **NO wave may dispatch until the previous wave's code-review pass clears.** Memory `feedback_code_review_per_wave.md` — Davesend 2026-05-24 incident.
|
|
646
646
|
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
-
|
|
666
|
-
|
|
667
|
-
|
|
647
|
+
Run the deterministic review gate in `guides/review-gate.md` over the wave's
|
|
648
|
+
merged diff, with `BASE=<wave-base-sha>` and `SCOPE` set to each package or
|
|
649
|
+
app the wave touched. That covers `rdc-solid-score --diff`, the repo linter on
|
|
650
|
+
changed files, `tsc`, scoped tests, and the UI spec when a route changed. It
|
|
651
|
+
takes one to two minutes; an open-ended reviewer agent took 4–10.
|
|
652
|
+
Approved: direct operator instruction — "we need to tune our code reviews to be
|
|
653
|
+
specific -- if you ran the validator for solid and lint etc... it should take 1-2
|
|
654
|
+
minutes". 2026-09-21, Dave, in session.
|
|
655
|
+
|
|
656
|
+
Dispatch `pr-review-toolkit:code-reviewer` only for a **named question** the
|
|
657
|
+
validators cannot answer, scoped to the files it touches (the guide's
|
|
658
|
+
"When an LLM reviewer is still right" section). Never dispatch it with a bare
|
|
659
|
+
"review the diff".
|
|
660
|
+
|
|
661
|
+
**Gate outcome:**
|
|
662
|
+
- A red gate step, or a critical/high finding from a named-question pass →
|
|
663
|
+
reopen the affected work items to `todo` with the `REVIEW_GATE` receipt and
|
|
664
|
+
the failing output in notes. Fix in a new wave before continuing.
|
|
665
|
+
- Medium/low findings from a named-question pass → append them to each work
|
|
666
|
+
item's `implementation_report.flags`. Do not reopen; the validator sees them.
|
|
667
|
+
- All steps green → log the `REVIEW_GATE` receipt with `verdict: "CLEAN"` and
|
|
668
|
+
proceed to the next wave.
|
|
669
|
+
|
|
670
|
+
Under `RDC_TEST=1`: echo `[RDC_TEST] skipping review gate` and proceed.
|
|
668
671
|
|
|
669
672
|
10. **Mandatory validator gate (runs after ALL waves complete — before any work item closes):**
|
|
670
673
|
|
|
@@ -65,6 +65,12 @@ matches the numeric-suffix heuristic) alongside real hits (`data`, `temp`,
|
|
|
65
65
|
`bin.rdc-clean-code-score`; falls back to
|
|
66
66
|
`node <rdc-skills-install-path>/scripts/clean-code-score.mjs`.)
|
|
67
67
|
|
|
68
|
+
**In a monorepo, always pass `--project-root <package or app dir>`.** Without
|
|
69
|
+
it, the dead-export scan walks the whole git repo. On regen-root that ran out
|
|
70
|
+
of 4 GB of heap after 18 s (exit 134); scoped to `apps/prtrust` it took 7.9 s.
|
|
71
|
+
This scorer has no `--diff` and reports absolute counts, so it is advisory,
|
|
72
|
+
not part of the merge gate (`guides/review-gate.md`).
|
|
73
|
+
|
|
68
74
|
2. **Read `results`** — per-unit findings for N1 (cryptic names), N2 (heuristic
|
|
69
75
|
meaningless-distinction names, low confidence), N4 (magic numbers), N7
|
|
70
76
|
(generic class/function names), F1 (>20 statements), F2 (>3 params), E1
|
package/skills/fixit/SKILL.md
CHANGED
|
@@ -111,26 +111,27 @@ fi
|
|
|
111
111
|
|
|
112
112
|
⛔ **No fixit closes without a code-review pass.** Even single-file changes go through review.
|
|
113
113
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
-
|
|
130
|
-
|
|
131
|
-
-
|
|
132
|
-
|
|
133
|
-
|
|
114
|
+
Run the deterministic review gate in `guides/review-gate.md` with
|
|
115
|
+
`BASE=HEAD~1` (or the commit before the fixit's first commit) and `SCOPE` set
|
|
116
|
+
to the touched package. For a fixit this usually takes seconds.
|
|
117
|
+
Approved: direct operator instruction — "we need to tune our code reviews to be
|
|
118
|
+
specific -- if you ran the validator for solid and lint etc... it should take 1-2
|
|
119
|
+
minutes". 2026-09-21, Dave, in session.
|
|
120
|
+
|
|
121
|
+
Dispatch `pr-review-toolkit:code-reviewer` only for a **named question** the
|
|
122
|
+
validators cannot answer, scoped to the files it touches. Never dispatch it with
|
|
123
|
+
a bare "review the commit".
|
|
124
|
+
|
|
125
|
+
**Gate outcome:**
|
|
126
|
+
- A red gate step, or a critical/high finding from a named-question pass → fix
|
|
127
|
+
it in this same fixit session. Do not escalate the fix to rdc:build; the
|
|
128
|
+
original fixit owns the cleanup. Re-commit and re-run the gate until it is clean.
|
|
129
|
+
- Medium/low findings from a named-question pass → record them in
|
|
130
|
+
`implementation_report.flags` and proceed to close.
|
|
131
|
+
- All steps green → quote the `REVIEW_GATE` receipt in the implementation report
|
|
132
|
+
and proceed to close.
|
|
133
|
+
|
|
134
|
+
Under `RDC_TEST=1`: echo `[RDC_TEST] skipping review gate` and proceed.
|
|
134
135
|
|
|
135
136
|
### 5.7 Deliver to done (MANDATORY — a fixit is not done until the change is LIVE)
|
|
136
137
|
|
|
@@ -144,7 +144,7 @@ After each wave: check `BUILD_STATUS`. If `escalated: true`, log the escalation
|
|
|
144
144
|
in the overnight doc and continue — don't stop the loop.
|
|
145
145
|
After each wave and after every resumed epic, require the `runOrchestrator()` receipt from `rdc:build` **where the orchestrator applies** — the target contains `corpus/_shared/build/phase-manifest.json` (today: regen-root; see `rdc:build` §1a). Elsewhere, require the contract checklist (`rdc-work check`) instead; an absent orchestrator is not a hold. `admission_refocus` or `pipeline_blocked` means the epic is held for an explicitly requested Design Review or validator closure; log that state and do not hand-reconstruct a dispatch wave.
|
|
146
146
|
|
|
147
|
-
**Mandatory code-review gate inherited from rdc:build (Step 9b).** Every wave inside `rdc:build` runs
|
|
147
|
+
**Mandatory code-review gate inherited from rdc:build (Step 9b).** Every wave inside `rdc:build` runs the deterministic review gate (`guides/review-gate.md`: solid-score --diff, linter on changed files, tsc, scoped tests, UI spec) before the next wave dispatches. It takes one to two minutes, where an open-ended reviewer took 4–10, which matters most across an overnight queue. A red step reopens the affected work items to `todo` and the next wave fixes them. Overnight does not skip or weaken this gate. If a wave's gate escalates twice, the advisor decides; otherwise the loop continues.
|
|
148
148
|
|
|
149
149
|
### 3d. Review
|
|
150
150
|
|
package/skills/release/SKILL.md
CHANGED
|
@@ -41,7 +41,7 @@ rdc:release: <repo> vX.Y.Z -> vA.B.C
|
|
|
41
41
|
[ ] Dry-run gate handled
|
|
42
42
|
[ ] Version files updated
|
|
43
43
|
[ ] Tests/self-test passed
|
|
44
|
-
[ ] Mandatory release
|
|
44
|
+
[ ] Mandatory release review gate (`guides/review-gate.md`, BASE=<last-released-tag>): solid-score --diff, linter on changed files, tsc, scoped tests. Block the release on any red step; LLM reviewer only for a named question.
|
|
45
45
|
[ ] Commit created
|
|
46
46
|
[ ] Tag created
|
|
47
47
|
[ ] Branch and tag pushed
|
package/skills/review/SKILL.md
CHANGED
|
@@ -12,8 +12,9 @@ description: rdc:review () - [--unattended] — post-build gate over tsc, tests,
|
|
|
12
12
|
> **Sandbox contract:** This skill honors `RDC_TEST=1` per `guides/agent-bootstrap.md` § RDC_TEST Sandbox Contract. Destructive external calls short-circuit under the flag.
|
|
13
13
|
|
|
14
14
|
> **Standard being enforced:** `docs/CODING-STANDARDS.md` (SOLID/Clean-Architecture —
|
|
15
|
-
> regen-root; skip if absent).
|
|
15
|
+
> regen-root; skip if absent). Steps 8b and 8c check it, via this package's
|
|
16
16
|
> own `rdc-solid-score` (also invoked by ATF's Test-Ladder as a consumer, not an owner).
|
|
17
|
+
> Step 8b runs the deterministic review gate, `guides/review-gate.md`.
|
|
17
18
|
|
|
18
19
|
|
|
19
20
|
# rdc:review — Quality Gate
|
|
@@ -84,41 +85,47 @@ description: rdc:review () - [--unattended] — post-build gate over tsc, tests,
|
|
|
84
85
|
- Never silently leave orphaned tasks
|
|
85
86
|
- **⛔ Raw `UPDATE work_items SET parent_id = ...` is forbidden** — bypasses RLS and all constraint checks
|
|
86
87
|
|
|
87
|
-
8b. **Mandatory code-review gate —
|
|
88
|
-
|
|
89
|
-
⛔ **No CLEAN verdict without a
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
88
|
+
8b. **Mandatory code-review gate — deterministic validators on the diff:**
|
|
89
|
+
|
|
90
|
+
⛔ **No CLEAN verdict without a review-gate pass.** Run `guides/review-gate.md`
|
|
91
|
+
over the diff under review, with `BASE` set to the review's base ref and `SCOPE`
|
|
92
|
+
set to each modified package from step 1. Its checks are
|
|
93
|
+
`rdc-solid-score --diff`, the repo linter on changed files (`--write`, then
|
|
94
|
+
check), `tsc`, scoped tests, and the UI spec for changed routes. It finishes in
|
|
95
|
+
one to two minutes and gives the same answer twice.
|
|
96
|
+
Approved: direct operator instruction — "we need to tune our code reviews to be
|
|
97
|
+
specific -- if you ran the validator for solid and lint etc... it should take 1-2
|
|
98
|
+
minutes". 2026-09-21, Dave, in session.
|
|
99
|
+
|
|
100
|
+
Dispatch `pr-review-toolkit:code-reviewer` only for a **named question** the
|
|
101
|
+
validators cannot answer, such as auth bypass on a protected path or a return
|
|
102
|
+
figure in public copy. Scope it to the files that question touches, following
|
|
103
|
+
the guide's "When an LLM reviewer is still right" section. A bare "review the
|
|
104
|
+
diff" dispatch is retired.
|
|
105
|
+
|
|
106
|
+
**Gate outcome:**
|
|
107
|
+
- A red gate step, or a critical/high finding from a named-question pass →
|
|
108
|
+
the verdict cannot be CLEAN. Interactive: report and pause. Unattended:
|
|
109
|
+
fix it if the fix is mechanical (`--write` lint fixes, a missing await, an
|
|
110
|
+
obvious null check); otherwise escalate via the advisor and mark HAS_ISSUES.
|
|
111
|
+
- Medium/low findings from a named-question pass → record them in the review
|
|
112
|
+
report's "Issues" section. The verdict can still be CLEAN.
|
|
113
|
+
- All steps green → quote the `REVIEW_GATE` receipt and continue.
|
|
114
|
+
|
|
115
|
+
Under `RDC_TEST=1`: echo `[RDC_TEST] skipping review gate` and continue.
|
|
112
116
|
|
|
113
117
|
8c. **Form/fit/function gate — solid-validator + architecture-reviewer:**
|
|
114
118
|
|
|
115
|
-
⛔ **No CLEAN verdict without this pass either.** Step 8b
|
|
116
|
-
|
|
117
|
-
shape
|
|
118
|
-
architecturally wrong
|
|
119
|
-
|
|
120
|
-
|
|
119
|
+
⛔ **No CLEAN verdict without this pass either.** Step 8b's lint, tsc and
|
|
120
|
+
tests catch convention drift and broken behavior. This step catches a
|
|
121
|
+
different failure shape: code that is logically correct and
|
|
122
|
+
convention-clean but architecturally wrong. The `Harness` case is the
|
|
123
|
+
example: a use-case orchestrator that reimplements what sibling packages
|
|
124
|
+
exist to do, scores fine on every generic metric, and fails the one check
|
|
125
|
+
that names the actual rule.
|
|
121
126
|
|
|
127
|
+
Reuse the `rdc-solid-score` JSON from step 8b rather than running the scorer
|
|
128
|
+
twice. If 8b ran without the repo's boundary config, re-run it with the config:
|
|
122
129
|
```bash
|
|
123
130
|
rdc-solid-score <modified-package-path> --diff origin/main --config <repo>/.solid-score.yml --format json
|
|
124
131
|
```
|
|
@@ -0,0 +1,103 @@
|
|
|
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 { tmpdir } from 'node:os';
|
|
5
|
+
import { dirname, join } from 'node:path';
|
|
6
|
+
import test from 'node:test';
|
|
7
|
+
import { fileURLToPath } from 'node:url';
|
|
8
|
+
|
|
9
|
+
import { isAbsentAtRef } from '../../scripts/solid-score.mjs';
|
|
10
|
+
|
|
11
|
+
const SCORER = join(dirname(fileURLToPath(import.meta.url)), '..', '..', 'scripts', 'solid-score.mjs');
|
|
12
|
+
|
|
13
|
+
// ── isAbsentAtRef: git's two "not at this ref" messages, and nothing else ──
|
|
14
|
+
|
|
15
|
+
test('isAbsentAtRef: "does not exist in" (absent at ref and on disk) is absent', () => {
|
|
16
|
+
assert.equal(isAbsentAtRef("fatal: path 'src/a.ts' does not exist in 'origin/develop'\n"), true);
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
test('isAbsentAtRef: "exists on disk, but not in" (a NEW working-tree file) is absent', () => {
|
|
20
|
+
// The message every --diff over a change that adds a file produces; it was
|
|
21
|
+
// unmatched until 2026-09-21 and crashed the gate.
|
|
22
|
+
assert.equal(isAbsentAtRef("fatal: path 'apps/x/tests/ui/home.spec.ts' exists on disk, but not in 'origin/develop'\n"), true);
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
test('isAbsentAtRef: a bad ref is NOT absent — it must be rethrown', () => {
|
|
26
|
+
assert.equal(isAbsentAtRef("fatal: invalid object name 'no-such-ref'.\n"), false);
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test('isAbsentAtRef: a path-form mismatch is NOT absent — it must be rethrown', () => {
|
|
30
|
+
assert.equal(
|
|
31
|
+
isAbsentAtRef("fatal: path 'src/a.ts' exists, but not 'a.ts'\nhint: Did you mean 'HEAD:src/a.ts' aka 'HEAD:./a.ts'?\n"),
|
|
32
|
+
false,
|
|
33
|
+
);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test('isAbsentAtRef: empty or missing stderr is NOT absent', () => {
|
|
37
|
+
assert.equal(isAbsentAtRef(''), false);
|
|
38
|
+
assert.equal(isAbsentAtRef(undefined), false);
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// ── end to end: --diff over a change that adds a file ──
|
|
42
|
+
|
|
43
|
+
function git(cwd, ...args) {
|
|
44
|
+
return execFileSync('git', ['-c', 'user.email=t@example.com', '-c', 'user.name=t', '-c', 'commit.gpgsign=false', ...args], {
|
|
45
|
+
cwd,
|
|
46
|
+
encoding: 'utf8',
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const CLEAN_CLASS = (name) => `export class ${name} {\n private n = 0;\n add(x: number): number {\n this.n += x;\n return this.n;\n }\n}\n`;
|
|
51
|
+
|
|
52
|
+
test('solid-score --diff scores a new file as a new unit instead of crashing', () => {
|
|
53
|
+
const dir = mkdtempSync(join(tmpdir(), 'solid-diff-'));
|
|
54
|
+
try {
|
|
55
|
+
git(dir, 'init', '-q');
|
|
56
|
+
writeFileSync(join(dir, 'a.ts'), CLEAN_CLASS('Alpha'));
|
|
57
|
+
git(dir, 'add', 'a.ts');
|
|
58
|
+
git(dir, 'commit', '-q', '-m', 'base');
|
|
59
|
+
|
|
60
|
+
// One untracked new file, and one committed-after-base new file: both are
|
|
61
|
+
// "exists on disk, but not in <ref>" at the base ref.
|
|
62
|
+
writeFileSync(join(dir, 'b.ts'), CLEAN_CLASS('Beta'));
|
|
63
|
+
writeFileSync(join(dir, 'c.ts'), CLEAN_CLASS('Gamma'));
|
|
64
|
+
git(dir, 'add', 'c.ts');
|
|
65
|
+
git(dir, 'commit', '-q', '-m', 'add c');
|
|
66
|
+
|
|
67
|
+
const run = spawnSync(process.execPath, [SCORER, dir, '--diff', 'HEAD~1', '--format', 'json'], { encoding: 'utf8' });
|
|
68
|
+
assert.equal(run.status, 0, `scorer exited ${run.status}: ${run.stderr}`);
|
|
69
|
+
const out = JSON.parse(run.stdout);
|
|
70
|
+
const scored = out.results.map((r) => r.file).sort();
|
|
71
|
+
assert.deepEqual(scored, ['b.ts', 'c.ts'], 'both new files are scored');
|
|
72
|
+
assert.deepEqual(out.regressions, [], 'clean new units are not regressions');
|
|
73
|
+
assert.deepEqual(out.removedFiles, [], 'nothing was deleted');
|
|
74
|
+
} finally {
|
|
75
|
+
rmSync(dir, { recursive: true, force: true });
|
|
76
|
+
}
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
test('solid-score --diff reports a deleted file in removedFiles instead of crashing', () => {
|
|
80
|
+
// A deleted file is listed by `git diff --name-only <ref>`; before
|
|
81
|
+
// 2026-09-21 it went to extractUnits and the gate died with ENOENT.
|
|
82
|
+
const dir = mkdtempSync(join(tmpdir(), 'solid-diff-del-'));
|
|
83
|
+
try {
|
|
84
|
+
git(dir, 'init', '-q');
|
|
85
|
+
writeFileSync(join(dir, 'a.ts'), CLEAN_CLASS('Alpha'));
|
|
86
|
+
writeFileSync(join(dir, 'gone.ts'), CLEAN_CLASS('Gone'));
|
|
87
|
+
git(dir, 'add', 'a.ts', 'gone.ts');
|
|
88
|
+
git(dir, 'commit', '-q', '-m', 'base');
|
|
89
|
+
|
|
90
|
+
// One deletion committed after base, one edit left uncommitted.
|
|
91
|
+
git(dir, 'rm', '-q', 'gone.ts');
|
|
92
|
+
git(dir, 'commit', '-q', '-m', 'remove gone');
|
|
93
|
+
writeFileSync(join(dir, 'a.ts'), `${CLEAN_CLASS('Alpha')}\nexport const touched = 1;\n`);
|
|
94
|
+
|
|
95
|
+
const run = spawnSync(process.execPath, [SCORER, dir, '--diff', 'HEAD~1', '--format', 'json'], { encoding: 'utf8' });
|
|
96
|
+
assert.equal(run.status, 0, `scorer exited ${run.status}: ${run.stderr}`);
|
|
97
|
+
const out = JSON.parse(run.stdout);
|
|
98
|
+
assert.deepEqual(out.removedFiles, ['gone.ts'], 'the deletion is reported, not dropped');
|
|
99
|
+
assert.deepEqual(out.results.map((r) => r.file), ['a.ts'], 'only the surviving changed file is scored');
|
|
100
|
+
} finally {
|
|
101
|
+
rmSync(dir, { recursive: true, force: true });
|
|
102
|
+
}
|
|
103
|
+
});
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
// Every place the package declares its version equals package.json.
|
|
2
|
+
//
|
|
3
|
+
// 0.37.0 shipped with clauth-plugin.json at 0.35.10 and package.json `claude.version`
|
|
4
|
+
// at 0.10.0. The only check, self-test, compared plugin.json alone, so both drifted for
|
|
5
|
+
// releases without a single red. This test reads the shared declaration list, and it
|
|
6
|
+
// also scans the repository, so a new manifest with a `version` cannot slip past the list.
|
|
7
|
+
import { test } from 'node:test';
|
|
8
|
+
import assert from 'node:assert/strict';
|
|
9
|
+
import { execFileSync } from 'node:child_process';
|
|
10
|
+
import { cpSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
|
|
11
|
+
import os from 'node:os';
|
|
12
|
+
import path from 'node:path';
|
|
13
|
+
import { fileURLToPath } from 'node:url';
|
|
14
|
+
import {
|
|
15
|
+
VERSION_DECLARATIONS, VERSION_SOURCE, describeValue, label, mismatches, readDeclarations, replaceJsonString,
|
|
16
|
+
syncDeclarations,
|
|
17
|
+
} from '../scripts/lib/manifest-versions.mjs';
|
|
18
|
+
|
|
19
|
+
const ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* The JSON files this repository holds: tracked, plus new files git does not ignore.
|
|
23
|
+
* Read from git, not the disk, so local reports, stamps and editor JSONC that git
|
|
24
|
+
* ignores can neither fail this test on one machine nor pass it on another.
|
|
25
|
+
*/
|
|
26
|
+
function jsonFilesWithTopLevelVersion() {
|
|
27
|
+
const listed = execFileSync('git', ['ls-files', '-z', '--cached', '--others', '--exclude-standard', '--', '*.json'], {
|
|
28
|
+
cwd: ROOT, encoding: 'utf8',
|
|
29
|
+
}).split('\0').filter(Boolean);
|
|
30
|
+
return [...new Set(listed)].filter((file) => {
|
|
31
|
+
const json = JSON.parse(readFileSync(path.join(ROOT, file), 'utf8'));
|
|
32
|
+
return json && typeof json === 'object' && !Array.isArray(json) && typeof json.version === 'string';
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
test('every version declaration equals package.json', () => {
|
|
37
|
+
const drift = mismatches(ROOT);
|
|
38
|
+
assert.deepEqual(
|
|
39
|
+
drift.map((d) => `${d.label} = ${describeValue(d)}`),
|
|
40
|
+
[],
|
|
41
|
+
`package.json is ${readDeclarations(ROOT).source}; run \`node scripts/sync-manifest-versions.mjs\``,
|
|
42
|
+
);
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
test('no JSON file declares a top-level version the list does not know about', () => {
|
|
46
|
+
const known = new Set([VERSION_SOURCE.file, ...VERSION_DECLARATIONS.filter((d) => d.keyPath.length === 1).map((d) => d.file)]);
|
|
47
|
+
const found = jsonFilesWithTopLevelVersion();
|
|
48
|
+
// Positive controls: a root file, and one inside a dot-directory.
|
|
49
|
+
assert.ok(found.includes('package.json'), 'the scan must find package.json');
|
|
50
|
+
assert.ok(found.includes('.claude-plugin/plugin.json'), 'the scan must find .claude-plugin/plugin.json');
|
|
51
|
+
assert.deepEqual(found.filter((f) => !known.has(f)), [], 'add each to VERSION_DECLARATIONS in scripts/lib/manifest-versions.mjs');
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
test('the declaration list names every file the fleet reads a version from', () => {
|
|
55
|
+
const labels = VERSION_DECLARATIONS.map(label);
|
|
56
|
+
for (const expected of ['.claude-plugin/plugin.json version', 'clauth-plugin.json version', 'package.json claude.version']) {
|
|
57
|
+
assert.ok(labels.includes(expected), `${expected} must be a declaration`);
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test('npm version runs the sync hook, which stages every declaration file', () => {
|
|
62
|
+
const pkg = JSON.parse(readFileSync(path.join(ROOT, 'package.json'), 'utf8'));
|
|
63
|
+
assert.equal(pkg.scripts?.version, 'node scripts/sync-manifest-versions.mjs --stage');
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
test('replaceJsonString edits only the named key and keeps the formatting', () => {
|
|
67
|
+
const text = '{\n "name": "x",\n "version": "1.0.0",\n "packages": {\n "": { "version": "1.0.0" },\n "node_modules/a": {\n "version": "1.0.0"\n }\n }\n}\n';
|
|
68
|
+
const nested = replaceJsonString(text, ['packages', '', 'version'], '1.0.0', '2.0.0');
|
|
69
|
+
assert.equal(nested, text.replace('{ "version": "1.0.0" }', '{ "version": "2.0.0" }'));
|
|
70
|
+
const top = replaceJsonString(text, ['version'], '1.0.0', '2.0.0');
|
|
71
|
+
assert.equal(top, text.replace('"version": "1.0.0",', '"version": "2.0.0",'));
|
|
72
|
+
assert.throws(() => replaceJsonString(text, ['version'], '9.9.9', '2.0.0'), /could not locate/);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
function copyManifests(tmp) {
|
|
76
|
+
for (const file of new Set([VERSION_SOURCE.file, ...VERSION_DECLARATIONS.map((d) => d.file)])) {
|
|
77
|
+
mkdirSync(path.dirname(path.join(tmp, file)), { recursive: true });
|
|
78
|
+
cpSync(path.join(ROOT, file), path.join(tmp, file));
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
test('an unreadable declaration is reported on its own and hides no other', () => {
|
|
83
|
+
const tmp = mkdtempSync(path.join(os.tmpdir(), 'rdc-version-read-'));
|
|
84
|
+
try {
|
|
85
|
+
copyManifests(tmp);
|
|
86
|
+
writeFileSync(path.join(tmp, 'clauth-plugin.json'), '{ "version": ');
|
|
87
|
+
const drift = mismatches(tmp);
|
|
88
|
+
assert.deepEqual(drift.map((d) => d.label), ['clauth-plugin.json version']);
|
|
89
|
+
assert.match(describeValue(drift[0]), /^\(unreadable: /);
|
|
90
|
+
} finally {
|
|
91
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
92
|
+
}
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test('sync writes nothing when any declaration cannot be set', () => {
|
|
96
|
+
const tmp = mkdtempSync(path.join(os.tmpdir(), 'rdc-version-sync-'));
|
|
97
|
+
try {
|
|
98
|
+
copyManifests(tmp);
|
|
99
|
+
const pkgFile = path.join(tmp, 'package.json');
|
|
100
|
+
writeFileSync(pkgFile, readFileSync(pkgFile, 'utf8').replace(/"version": "[^"]+"/, '"version": "9.8.7"'));
|
|
101
|
+
const clauth = path.join(tmp, 'clauth-plugin.json');
|
|
102
|
+
writeFileSync(clauth, readFileSync(clauth, 'utf8').replace(/"version": "[^"]+"/, '"version": 1'));
|
|
103
|
+
const before = readFileSync(path.join(tmp, '.claude-plugin', 'plugin.json'), 'utf8');
|
|
104
|
+
assert.throws(() => syncDeclarations(tmp), /clauth-plugin\.json version is 1, not a version string/);
|
|
105
|
+
assert.equal(readFileSync(path.join(tmp, '.claude-plugin', 'plugin.json'), 'utf8'), before, 'plugin.json must be untouched');
|
|
106
|
+
} finally {
|
|
107
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
test('the hook stages nothing into a repository the package is merely inside', () => {
|
|
112
|
+
const parent = mkdtempSync(path.join(os.tmpdir(), 'rdc-version-parent-'));
|
|
113
|
+
try {
|
|
114
|
+
const pkg = path.join(parent, 'vendor', 'rdc-skills');
|
|
115
|
+
copyManifests(pkg);
|
|
116
|
+
for (const file of ['scripts/sync-manifest-versions.mjs', 'scripts/lib/manifest-versions.mjs']) {
|
|
117
|
+
mkdirSync(path.dirname(path.join(pkg, file)), { recursive: true });
|
|
118
|
+
cpSync(path.join(ROOT, file), path.join(pkg, file));
|
|
119
|
+
}
|
|
120
|
+
execFileSync('git', ['init', '-q'], { cwd: parent });
|
|
121
|
+
const out = execFileSync(process.execPath, ['scripts/sync-manifest-versions.mjs', '--stage'], { cwd: pkg, encoding: 'utf8' });
|
|
122
|
+
assert.match(out, /nothing staged/);
|
|
123
|
+
assert.equal(execFileSync('git', ['diff', '--cached', '--name-only'], { cwd: parent, encoding: 'utf8' }), '');
|
|
124
|
+
} finally {
|
|
125
|
+
rmSync(parent, { recursive: true, force: true });
|
|
126
|
+
}
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test('npm version in a throwaway copy moves every declaration and stages it', () => {
|
|
130
|
+
const tmp = mkdtempSync(path.join(os.tmpdir(), 'rdc-version-hook-'));
|
|
131
|
+
try {
|
|
132
|
+
copyManifests(tmp);
|
|
133
|
+
for (const file of ['scripts/sync-manifest-versions.mjs', 'scripts/lib/manifest-versions.mjs']) {
|
|
134
|
+
mkdirSync(path.dirname(path.join(tmp, file)), { recursive: true });
|
|
135
|
+
cpSync(path.join(ROOT, file), path.join(tmp, file));
|
|
136
|
+
}
|
|
137
|
+
execFileSync('git', ['init', '-q'], { cwd: tmp });
|
|
138
|
+
// npm is a .cmd shim on Windows; run it through cmd.exe rather than `shell: true`,
|
|
139
|
+
// which newer Node deprecates for argument arrays (DEP0190).
|
|
140
|
+
const npmArgs = ['version', '9.8.7', '--no-git-tag-version', '--ignore-scripts=false'];
|
|
141
|
+
const [cmd, args] = process.platform === 'win32'
|
|
142
|
+
? ['cmd.exe', ['/d', '/s', '/c', 'npm', ...npmArgs]]
|
|
143
|
+
: ['npm', npmArgs];
|
|
144
|
+
execFileSync(cmd, args, { cwd: tmp, stdio: 'pipe' });
|
|
145
|
+
const { source, declarations } = readDeclarations(tmp);
|
|
146
|
+
assert.equal(source, '9.8.7');
|
|
147
|
+
assert.deepEqual(declarations.filter((d) => d.value !== '9.8.7').map((d) => `${d.label} = ${d.value}`), []);
|
|
148
|
+
const staged = execFileSync('git', ['diff', '--cached', '--name-only'], { cwd: tmp, encoding: 'utf8' }).split('\n').filter(Boolean);
|
|
149
|
+
for (const file of new Set(VERSION_DECLARATIONS.map((d) => d.file))) {
|
|
150
|
+
assert.ok(staged.includes(file), `${file} must be staged for the version commit (staged: ${staged.join(', ')})`);
|
|
151
|
+
}
|
|
152
|
+
} finally {
|
|
153
|
+
rmSync(tmp, { recursive: true, force: true });
|
|
154
|
+
}
|
|
155
|
+
});
|
|
@@ -219,8 +219,17 @@ EOF
|
|
|
219
219
|
['heredoc with a quoted delimiter passes', 'git commit -F - <<"MSG"\nfix(a): b\nMSG', 'pass'],
|
|
220
220
|
['heredoc carrying only a UUID passes', "git commit -F - <<'EOF'\nwip\n\nWork-Item: f27ff5fa-0000-4000-8000-000000000000\nEOF", 'pass'],
|
|
221
221
|
['-m still passes', 'git commit -m "fix(x): y"', 'pass'],
|
|
222
|
+
// -m "$(cat <<'EOF' … EOF)" — the -m regex used to match the FIRST
|
|
223
|
+
// unrelated quote inside the substitution (the heredoc delimiter's own
|
|
224
|
+
// opening quote) and return early with literal shell text as the
|
|
225
|
+
// "message", never reaching the heredoc fallback below. Same defect
|
|
226
|
+
// class as the 2026-08-29 fix, one wrapper layer further out. Filed as
|
|
227
|
+
// regen-root#243.
|
|
228
|
+
['-m "$(cat <<EOF …)" substitution passes', "git commit -m \"$(cat <<'EOF'\nfix(guards): correct the heredoc substitution match\n\nBody line two.\nEOF\n)\"", 'pass'],
|
|
229
|
+
['-m "$(cat <<-EOF …)" dash-form substitution passes', "git commit -m \"$(cat <<-'EOF'\nfix(a): b\nEOF\n)\"", 'pass'],
|
|
222
230
|
// Negative controls — reading the message is not a bypass.
|
|
223
231
|
['heredoc without a type or UUID still blocks', "git commit -F - <<'EOF'\nrandom words\nEOF", 'block'],
|
|
232
|
+
['-m "$(cat <<EOF …)" without a type or UUID still blocks', "git commit -m \"$(cat <<'EOF'\nrandom words\nEOF\n)\"", 'block'],
|
|
224
233
|
['a bare commit still blocks', 'git commit', 'block'],
|
|
225
234
|
// Honest about what it cannot see, rather than calling it absent.
|
|
226
235
|
['a real stdin pipe reports unreadable', 'cat msg.txt | git commit -q -F -', 'unreadable'],
|
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
---
|
|
2
|
-
id: 2026-08-17-fixit-codex-mcp-only-skills
|
|
3
|
-
date: "2026-08-17"
|
|
4
|
-
skill: fixit
|
|
5
|
-
session: codex-mcp-startup-20260817
|
|
6
|
-
scope: simple
|
|
7
|
-
status: applied
|
|
8
|
-
area: skill
|
|
9
|
-
links:
|
|
10
|
-
commits:
|
|
11
|
-
- 263231a
|
|
12
|
-
memory: []
|
|
13
|
-
work_items:
|
|
14
|
-
- dc076f35-236b-4f8d-be3f-646a66514ba5
|
|
15
|
-
---
|
|
16
|
-
|
|
17
|
-
## What happened
|
|
18
|
-
Codex's real numbered-lane startup completed MCP initialization but emitted a red skill-budget error on every launch. The machine had 36 RDC skill directories in each of the global `.codex/skills` and `.agents/skills` roots while the same 36-skill catalog was already registered through the RDC-Skills MCP endpoint.
|
|
19
|
-
|
|
20
|
-
## Root cause
|
|
21
|
-
`scripts/install-rdc-skills.js` registered the MCP endpoint and then copied the full RDC catalog into every detected Codex skill directory. Codex loaded those duplicate surfaces together, inflating startup metadata to 113 skills. The same install also left a legacy RDC marketplace block that Codex rejected as an unsupported plugin source.
|
|
22
|
-
|
|
23
|
-
## The fix / rule
|
|
24
|
-
Codex receives RDC skills exclusively through MCP. The installer now verifies the MCP config before purging legacy file copies, fails closed if the replacement cannot be established, removes the obsolete marketplace/plugin blocks with CRLF coverage, and verifies that live Codex skill roots contain zero RDC duplicates. Implemented in commit `263231a`.
|