@lifeaitools/rdc-skills 0.35.3 → 0.35.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/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,32 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|
|
7
7
|
|
|
8
8
|
---
|
|
9
9
|
|
|
10
|
+
## v0.35.4 — marketplace sync false-positive: untracked file blocked it forever
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
- `syncMarketplaceCheckout()`'s dirty-check used plain `git status --porcelain`,
|
|
14
|
+
which also reports untracked files. A single harmless untracked file in a
|
|
15
|
+
marketplace clone (e.g. a stray local scratch file) made the check treat the
|
|
16
|
+
clone as "has local changes" and permanently refuse to sync it, on every
|
|
17
|
+
single install run, forever — even though `git reset --hard` never touches
|
|
18
|
+
untracked files and there was no real edit to preserve. This is the
|
|
19
|
+
confirmed root cause of the marketplace checkout sitting stale for over a
|
|
20
|
+
week despite repeated installer runs. Fixed to `--untracked-files=no`, which
|
|
21
|
+
checks only tracked-file modifications — genuine local edits still block the
|
|
22
|
+
sync exactly as before (PRESERVE-DIRTY intact); a stray untracked file no
|
|
23
|
+
longer does.
|
|
24
|
+
- Regression test added: a real temp git origin+clone pair reproduces both the
|
|
25
|
+
untracked-cruft case (must sync) and the genuine-local-edit case (must still
|
|
26
|
+
block), so this cannot silently regress.
|
|
27
|
+
- `tests/install-rdc-skills.test.mjs` also had two stale fixtures caught while
|
|
28
|
+
fixing this: `package-lock.json` version drifted from `package.json`
|
|
29
|
+
(0.35.2 vs 0.35.3+), and a hardcoded `skillCount === 36` assertion had rotted
|
|
30
|
+
against the real, current 43 skill directories. The count assertion now
|
|
31
|
+
compares against the actual `skills/*/SKILL.md` directories on disk instead
|
|
32
|
+
of a hardcoded number, so it can't go stale the same way again.
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
10
36
|
## v0.35.3 — coding standards wired into plan/preplan/build/fixit/review
|
|
11
37
|
|
|
12
38
|
### Fixed
|
package/package.json
CHANGED
|
@@ -508,8 +508,16 @@ function syncMarketplaceCheckout(mktDir) {
|
|
|
508
508
|
const git = (...args) =>
|
|
509
509
|
execSync(`git -C "${mktDir}" ${args.join(' ')}`, { encoding: 'utf8', stdio: 'pipe' }).trim();
|
|
510
510
|
try {
|
|
511
|
-
|
|
512
|
-
|
|
511
|
+
// PRESERVE-DIRTY means "don't discard a real local edit to a TRACKED file" —
|
|
512
|
+
// `--untracked-files=no` is deliberate. Plain `git status --porcelain` also
|
|
513
|
+
// reports untracked files (`?? some-cruft.txt`), which `git reset --hard`
|
|
514
|
+
// never touches and never conflicts with. Treating that as "dirty" made this
|
|
515
|
+
// sync silently refuse forever the moment ANY stray untracked file appeared
|
|
516
|
+
// in the clone — which is exactly how this checkout sat 39+ commits behind
|
|
517
|
+
// for over a week (2026-08-23), still warning "left untouched" on every run,
|
|
518
|
+
// while a plain `git status` on the same directory read completely clean.
|
|
519
|
+
if (git('status', '--porcelain', '--untracked-files=no')) {
|
|
520
|
+
warn(`marketplace clone has local changes to tracked files at ${mktDir} — left untouched; it may serve stale commands`);
|
|
513
521
|
return;
|
|
514
522
|
}
|
|
515
523
|
const branch = git('rev-parse', '--abbrev-ref', 'HEAD') || 'master';
|
|
@@ -1347,7 +1355,7 @@ async function main() {
|
|
|
1347
1355
|
// scripts/probe-installed-hooks.mjs verify the real install path instead of
|
|
1348
1356
|
// re-implementing it — a probe that copies its own way proves nothing about what
|
|
1349
1357
|
// ships. Without this guard, requiring the module would run a full install.
|
|
1350
|
-
module.exports = { copyHookFiles, assertHooksLoadable, registerCodexTarget, RDC_ENV_HOOK_TIMEOUT_SEC };
|
|
1358
|
+
module.exports = { copyHookFiles, assertHooksLoadable, registerCodexTarget, syncMarketplaceCheckout, RDC_ENV_HOOK_TIMEOUT_SEC };
|
|
1351
1359
|
|
|
1352
1360
|
if (require.main === module) {
|
|
1353
1361
|
main().catch(e => { fail(e.message); process.exit(1); });
|
|
@@ -44,7 +44,22 @@ assert.match(
|
|
|
44
44
|
const skillCount = Array.isArray(plugin.skills_meta)
|
|
45
45
|
? plugin.skills_meta.length
|
|
46
46
|
: Object.keys(plugin.skills_meta || {}).length;
|
|
47
|
-
|
|
47
|
+
// A hardcoded magic number here (previously 36, silently stale against the
|
|
48
|
+
// real 43) rots the instant a skill is added or removed and stops proving
|
|
49
|
+
// anything — it only proves someone remembered to bump a number. Assert
|
|
50
|
+
// against the actual skills/ directory instead: every dir containing a
|
|
51
|
+
// SKILL.md must be represented in plugin.json's skills_meta, and vice versa.
|
|
52
|
+
const skillsDir = join(REPO_ROOT, 'skills');
|
|
53
|
+
const realSkillDirs = require('node:fs')
|
|
54
|
+
.readdirSync(skillsDir, { withFileTypes: true })
|
|
55
|
+
.filter((e) => e.isDirectory() && existsSync(join(skillsDir, e.name, 'SKILL.md')))
|
|
56
|
+
.map((e) => e.name)
|
|
57
|
+
.sort();
|
|
58
|
+
assert.equal(
|
|
59
|
+
skillCount,
|
|
60
|
+
realSkillDirs.length,
|
|
61
|
+
`plugin.json skills_meta (${skillCount}) must match the actual skill directories on disk (${realSkillDirs.length}): ${realSkillDirs.join(', ')}`,
|
|
62
|
+
);
|
|
48
63
|
assert.match(
|
|
49
64
|
source,
|
|
50
65
|
/Available MCP skills.*\/rdc:\* command shorthands/,
|
|
@@ -97,6 +112,81 @@ try {
|
|
|
97
112
|
rmSync(codexSkills, { recursive: true, force: true });
|
|
98
113
|
}
|
|
99
114
|
|
|
115
|
+
// Regression for the 2026-08-23 incident: a marketplace clone with ZERO real
|
|
116
|
+
// local edits — only a stray untracked file — sat 39+ commits behind for over
|
|
117
|
+
// a week because `git status --porcelain` (which also reports untracked
|
|
118
|
+
// files) was treated as "has local changes, never touch it". `git reset
|
|
119
|
+
// --hard` never touches untracked files, so an untracked file is irrelevant
|
|
120
|
+
// to whether the sync is safe. This builds a real origin + clone pair,
|
|
121
|
+
// reproduces the exact scenario, and asserts the clone actually advances.
|
|
122
|
+
const { syncMarketplaceCheckout } = require(script);
|
|
123
|
+
const gitTestRoot = mkdtempSync(join(tmpdir(), 'rdc-marketplace-sync-'));
|
|
124
|
+
try {
|
|
125
|
+
const originDir = join(gitTestRoot, 'origin');
|
|
126
|
+
const cloneDir = join(gitTestRoot, 'clone');
|
|
127
|
+
const runGit = (cwd, args) => {
|
|
128
|
+
const res = spawnSync('git', args, { cwd, encoding: 'utf8' });
|
|
129
|
+
assert.equal(res.status, 0, `git ${args.join(' ')} failed: ${res.stderr}`);
|
|
130
|
+
return res.stdout.trim();
|
|
131
|
+
};
|
|
132
|
+
|
|
133
|
+
mkdirSync(originDir, { recursive: true });
|
|
134
|
+
runGit(originDir, ['init', '--initial-branch=master']);
|
|
135
|
+
runGit(originDir, ['config', 'user.email', 'test@example.com']);
|
|
136
|
+
runGit(originDir, ['config', 'user.name', 'Test']);
|
|
137
|
+
writeFileSync(join(originDir, 'file.txt'), 'v1\n');
|
|
138
|
+
runGit(originDir, ['add', '.']);
|
|
139
|
+
runGit(originDir, ['commit', '-m', 'v1']);
|
|
140
|
+
|
|
141
|
+
runGit(gitTestRoot, ['clone', originDir, cloneDir]);
|
|
142
|
+
|
|
143
|
+
// Advance origin so the clone is genuinely behind.
|
|
144
|
+
writeFileSync(join(originDir, 'file.txt'), 'v2\n');
|
|
145
|
+
runGit(originDir, ['add', '.']);
|
|
146
|
+
runGit(originDir, ['commit', '-m', 'v2']);
|
|
147
|
+
const originHead = runGit(originDir, ['rev-parse', 'HEAD']);
|
|
148
|
+
|
|
149
|
+
// The exact reproduction: a stray UNTRACKED file, nothing modified/staged.
|
|
150
|
+
writeFileSync(join(cloneDir, 'some-local-cruft.cjs'), 'module.exports = {};\n');
|
|
151
|
+
assert.match(
|
|
152
|
+
runGit(cloneDir, ['status', '--porcelain']),
|
|
153
|
+
/^\?\? some-local-cruft\.cjs$/,
|
|
154
|
+
'precondition: clone must show ONLY an untracked file, nothing modified',
|
|
155
|
+
);
|
|
156
|
+
|
|
157
|
+
syncMarketplaceCheckout(cloneDir);
|
|
158
|
+
|
|
159
|
+
assert.equal(
|
|
160
|
+
runGit(cloneDir, ['rev-parse', 'HEAD']),
|
|
161
|
+
originHead,
|
|
162
|
+
'a clone with only an untracked file must still advance to origin — untracked cruft is not a local edit',
|
|
163
|
+
);
|
|
164
|
+
assert.equal(
|
|
165
|
+
existsSync(join(cloneDir, 'some-local-cruft.cjs')),
|
|
166
|
+
true,
|
|
167
|
+
'the untracked file itself must survive the sync — reset --hard never touches it',
|
|
168
|
+
);
|
|
169
|
+
|
|
170
|
+
// Genuine dirt — a MODIFIED tracked file — must still block the sync.
|
|
171
|
+
writeFileSync(join(originDir, 'file.txt'), 'v3\n');
|
|
172
|
+
runGit(originDir, ['add', '.']);
|
|
173
|
+
runGit(originDir, ['commit', '-m', 'v3']);
|
|
174
|
+
const originHeadV3 = runGit(originDir, ['rev-parse', 'HEAD']);
|
|
175
|
+
writeFileSync(join(cloneDir, 'file.txt'), 'local edit, never committed\n');
|
|
176
|
+
const beforeDirtySync = runGit(cloneDir, ['rev-parse', 'HEAD']);
|
|
177
|
+
|
|
178
|
+
syncMarketplaceCheckout(cloneDir);
|
|
179
|
+
|
|
180
|
+
assert.equal(
|
|
181
|
+
runGit(cloneDir, ['rev-parse', 'HEAD']),
|
|
182
|
+
beforeDirtySync,
|
|
183
|
+
'a real local edit to a TRACKED file must still block the sync (PRESERVE-DIRTY)',
|
|
184
|
+
);
|
|
185
|
+
assert.notEqual(beforeDirtySync, originHeadV3, 'sanity: origin did advance further in this step');
|
|
186
|
+
} finally {
|
|
187
|
+
rmSync(gitTestRoot, { recursive: true, force: true });
|
|
188
|
+
}
|
|
189
|
+
|
|
100
190
|
const packagedRoot = mkdtempSync(join(tmpdir(), 'rdc-packaged-truth-'));
|
|
101
191
|
try {
|
|
102
192
|
writeFileSync(join(packagedRoot, 'package.json'), JSON.stringify({ version: '9.9.9' }));
|