skillrepo 4.14.0 → 4.14.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/package.json
CHANGED
package/src/commands/update.mjs
CHANGED
|
@@ -65,6 +65,8 @@ import {
|
|
|
65
65
|
*
|
|
66
66
|
* - 304 Not Modified → exit 0, NO output.
|
|
67
67
|
* - 200 with changes → exit 0, ONE line: `[SkillRepo] Library synced: N added, N updated, N removed.`
|
|
68
|
+
* ("Skillset" replaces "Library" when a declared repo ran a scoped
|
|
69
|
+
* skillset sync — #2679.)
|
|
68
70
|
* - Any failure → exit 0, ONE line: `[SkillRepo] Sync failed: <reason>.`
|
|
69
71
|
* - Global-boundary disclosure (#2495): when the sync's summary
|
|
70
72
|
* reports global skills that will also load in this session
|
|
@@ -172,6 +174,7 @@ export async function runUpdate(argv, io = {}) {
|
|
|
172
174
|
const skipped = summary.skipped ?? 0;
|
|
173
175
|
const total =
|
|
174
176
|
summary.added + summary.updated + summary.removed + skipped;
|
|
177
|
+
const subject = syncSubject(summary);
|
|
175
178
|
// Global-boundary disclosure (#2495), computed BEFORE the silent
|
|
176
179
|
// branch below: the quiet 304/zero-delta/throttled session is
|
|
177
180
|
// the COMMON session, and it must still disclose — the line
|
|
@@ -232,7 +235,7 @@ export async function runUpdate(argv, io = {}) {
|
|
|
232
235
|
return;
|
|
233
236
|
}
|
|
234
237
|
stdout.write(
|
|
235
|
-
`[SkillRepo]
|
|
238
|
+
`[SkillRepo] ${subject} synced: ${summary.added} added, ${summary.updated} updated, ${summary.removed} removed` +
|
|
236
239
|
(skipped > 0 ? `, ${skipped} SKIPPED (could not be written)` : "") +
|
|
237
240
|
`.\n`,
|
|
238
241
|
);
|
|
@@ -338,17 +341,28 @@ export async function runUpdate(argv, io = {}) {
|
|
|
338
341
|
printSummary(summary, stdout);
|
|
339
342
|
}
|
|
340
343
|
|
|
344
|
+
// The noun for the scope a sync operated on (#2679). A declared repo's
|
|
345
|
+
// scoped sync carries a top-level `skillsetRef` (stamped by the runSync
|
|
346
|
+
// dispatcher on EVERY scoped return, including the grace/fail-closed path);
|
|
347
|
+
// a whole-library sync does not. Keying off THIS — not the #2665
|
|
348
|
+
// `compliance` object, whose presence excludes the non-fresh-scan paths —
|
|
349
|
+
// is what keeps a scoped grace sync from mislabeling itself "Library".
|
|
350
|
+
function syncSubject(summary) {
|
|
351
|
+
return summary.skillsetRef ? "Skillset" : "Library";
|
|
352
|
+
}
|
|
353
|
+
|
|
341
354
|
function printSummary(s, out) {
|
|
342
355
|
// `skipped` counts here: a run that dropped a skill is NOT "up to date",
|
|
343
356
|
// and saying so was a false statement to the user (#2413 adversarial
|
|
344
357
|
// review — the counter was incremented and never read).
|
|
345
358
|
const skipped = s.skipped ?? 0;
|
|
346
359
|
const total = s.added + s.updated + s.removed + skipped;
|
|
360
|
+
const subject = syncSubject(s);
|
|
347
361
|
if (s.notModified || total === 0) {
|
|
348
|
-
out.write(
|
|
362
|
+
out.write(` ✓ ${subject} is up to date.\n`);
|
|
349
363
|
return;
|
|
350
364
|
}
|
|
351
|
-
out.write(
|
|
365
|
+
out.write(`\n ${subject} sync complete:\n`);
|
|
352
366
|
if (s.added > 0) out.write(` + ${s.added} added\n`);
|
|
353
367
|
if (s.updated > 0) out.write(` ↻ ${s.updated} updated\n`);
|
|
354
368
|
if (s.removed > 0) out.write(` − ${s.removed} removed\n`);
|
package/src/lib/sync.mjs
CHANGED
|
@@ -163,6 +163,24 @@
|
|
|
163
163
|
* the throttled early-exit, and the
|
|
164
164
|
* grace/fail-closed paths (no fresh
|
|
165
165
|
* scans there to state a fact from).
|
|
166
|
+
* @property {string} [skillsetRef] - The skillset this run was scoped to
|
|
167
|
+
* (`declaration.use`), stamped by the
|
|
168
|
+
* `runSync` dispatcher on EVERY return of
|
|
169
|
+
* `runSkillsetScopedSync` — 200, 304,
|
|
170
|
+
* grace/fail-closed, and that body's own
|
|
171
|
+
* per-repo throttle exit. Unlike
|
|
172
|
+
* `compliance.skillsetRef` (a narrower
|
|
173
|
+
* subset), presence means "this run
|
|
174
|
+
* dispatched to the skillset-scoped body".
|
|
175
|
+
* ABSENT on whole-library syncs AND on the
|
|
176
|
+
* machine-global throttled early-exit in
|
|
177
|
+
* `runSync`'s own preamble — that path
|
|
178
|
+
* returns BEFORE the declaration is
|
|
179
|
+
* resolved, so a declared repo never
|
|
180
|
+
* reaches dispatch there (same exclusion
|
|
181
|
+
* `compliance` carries). Sound discriminator
|
|
182
|
+
* for scope-aware copy on any path that
|
|
183
|
+
* actually prints it (#2679).
|
|
166
184
|
* @property {string} syncedAt - ISO timestamp of the sync: the server
|
|
167
185
|
* response `syncedAt` on a 200, or the
|
|
168
186
|
* previously-cached sync timestamp on a
|
|
@@ -947,7 +965,7 @@ export async function runSync(options) {
|
|
|
947
965
|
// extracted verbatim; the golden wire-identity tests arbitrate that
|
|
948
966
|
// its behavior is character-for-character unchanged.
|
|
949
967
|
if (declarationResolution?.status === "declared") {
|
|
950
|
-
|
|
968
|
+
const scopedSummary = await runSkillsetScopedSync({
|
|
951
969
|
serverUrl,
|
|
952
970
|
apiKey,
|
|
953
971
|
vendors,
|
|
@@ -956,6 +974,18 @@ export async function runSync(options) {
|
|
|
956
974
|
minSyncIntervalMs: intervalMs,
|
|
957
975
|
resolution: declarationResolution,
|
|
958
976
|
});
|
|
977
|
+
// Reliable top-level scope marker (#2679). EVERY scoped return is a
|
|
978
|
+
// skillset sync — including the grace/fail-closed path
|
|
979
|
+
// (`serveLastVerified`) and the defensive 304-no-prior branch, where the
|
|
980
|
+
// #2665 `compliance` object is intentionally ABSENT. Presentation
|
|
981
|
+
// (`printSummary`, the session-hook line) keys the "Skillset" vs
|
|
982
|
+
// "Library" noun off THIS field, never off `compliance.skillsetRef`
|
|
983
|
+
// (whose presence rules exclude the non-fresh-scan paths, so keying off
|
|
984
|
+
// it mislabels a scoped grace sync as "Library").
|
|
985
|
+
return {
|
|
986
|
+
...scopedSummary,
|
|
987
|
+
skillsetRef: declarationResolution.declaration.use,
|
|
988
|
+
};
|
|
959
989
|
}
|
|
960
990
|
|
|
961
991
|
return runWholeLibrarySync({
|
|
@@ -988,6 +988,113 @@ describe("runUpdate — skillset-declaration gate (#2362)", () => {
|
|
|
988
988
|
});
|
|
989
989
|
});
|
|
990
990
|
|
|
991
|
+
// ── Scoped skillset sync says "Skillset", not "Library" (#2679) ────────
|
|
992
|
+
//
|
|
993
|
+
// In a declared repo, `update` runs a scoped skillset sync (sync.mjs
|
|
994
|
+
// dispatch) and the summary carries a top-level `skillsetRef` (stamped
|
|
995
|
+
// at the dispatcher on every scoped return), so the printer names that
|
|
996
|
+
// scope. An undeclared repo (whole library) stays byte-identical
|
|
997
|
+
// ("Library"). Sibling to #2672 (`list` vocabulary).
|
|
998
|
+
|
|
999
|
+
describe("runUpdate — scoped skillset sync vocabulary (#2679)", () => {
|
|
1000
|
+
beforeEach(setup);
|
|
1001
|
+
afterEach(teardown);
|
|
1002
|
+
|
|
1003
|
+
function declareRepo() {
|
|
1004
|
+
// .git bounds the declaration walk inside the sandbox project dir.
|
|
1005
|
+
mkdirSync(join(sandbox, "project", ".git"), { recursive: true });
|
|
1006
|
+
writeFileSync(
|
|
1007
|
+
join(sandbox, "project", "skillrepo.json"),
|
|
1008
|
+
JSON.stringify({
|
|
1009
|
+
skillset: { version: 1, name: "checkout", use: "acme/backend-core" },
|
|
1010
|
+
}),
|
|
1011
|
+
);
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
function scopedResponse(skills) {
|
|
1015
|
+
return {
|
|
1016
|
+
skills,
|
|
1017
|
+
removals: [],
|
|
1018
|
+
syncedAt: "2026-08-20T00:00:00.000Z",
|
|
1019
|
+
skillset: {
|
|
1020
|
+
name: "backend-core",
|
|
1021
|
+
updatedAt: "2026-08-20T00:00:00.000Z",
|
|
1022
|
+
skippedExtras: [],
|
|
1023
|
+
},
|
|
1024
|
+
};
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
it("no-op scoped sync prints 'Skillset is up to date', never 'Library'", async () => {
|
|
1028
|
+
declareRepo();
|
|
1029
|
+
server.setSkillsetResponse(scopedResponse([]));
|
|
1030
|
+
await runUpdate(["--key", VALID_KEY, "--url", serverUrl], { stdout });
|
|
1031
|
+
const out = stdout.text();
|
|
1032
|
+
assert.match(out, /Skillset is up to date/);
|
|
1033
|
+
assert.doesNotMatch(out, /Library is up to date/);
|
|
1034
|
+
});
|
|
1035
|
+
|
|
1036
|
+
it("scoped sync that writes a skill prints 'Skillset sync complete'", async () => {
|
|
1037
|
+
declareRepo();
|
|
1038
|
+
server.setSkillsetResponse(scopedResponse([makeSkill("checkout-flow")]));
|
|
1039
|
+
await runUpdate(["--key", VALID_KEY, "--url", serverUrl], { stdout });
|
|
1040
|
+
const out = stdout.text();
|
|
1041
|
+
assert.match(out, /Skillset sync complete/);
|
|
1042
|
+
assert.match(out, /added/);
|
|
1043
|
+
assert.doesNotMatch(out, /Library sync complete/);
|
|
1044
|
+
});
|
|
1045
|
+
|
|
1046
|
+
it("--session-hook scoped sync reports 'Skillset synced', not 'Library synced'", async () => {
|
|
1047
|
+
declareRepo();
|
|
1048
|
+
server.setSkillsetResponse(scopedResponse([makeSkill("checkout-flow")]));
|
|
1049
|
+
await runUpdate(
|
|
1050
|
+
["--session-hook", "--key", VALID_KEY, "--url", serverUrl],
|
|
1051
|
+
{ stdout },
|
|
1052
|
+
);
|
|
1053
|
+
const out = stdout.text();
|
|
1054
|
+
assert.match(out, /\[SkillRepo\] Skillset synced: \d+ added/);
|
|
1055
|
+
assert.doesNotMatch(out, /Library synced/);
|
|
1056
|
+
});
|
|
1057
|
+
|
|
1058
|
+
it("an UNDECLARED repo keeps the 'Library' vocabulary (whole-library scope)", async () => {
|
|
1059
|
+
// Guards against over-broadening: no declaration → no skillsetRef →
|
|
1060
|
+
// the subject must remain "Library".
|
|
1061
|
+
server.setLibraryResponse({ skills: [], removals: [], syncedAt: "x" });
|
|
1062
|
+
await runUpdate(["--key", VALID_KEY, "--url", serverUrl], { stdout });
|
|
1063
|
+
const out = stdout.text();
|
|
1064
|
+
assert.match(out, /Library is up to date/);
|
|
1065
|
+
assert.doesNotMatch(out, /Skillset/);
|
|
1066
|
+
});
|
|
1067
|
+
|
|
1068
|
+
it("declared repo during a registry outage (grace path) still says 'Skillset'", async () => {
|
|
1069
|
+
// Regression for the review finding: the scoped grace path
|
|
1070
|
+
// (serveLastVerified) returns NO `compliance` object, so a discriminator
|
|
1071
|
+
// keyed off compliance.skillsetRef mislabeled it "Library". The reliable
|
|
1072
|
+
// top-level skillsetRef (sync.mjs dispatcher) fixes it. First a
|
|
1073
|
+
// successful scoped sync writes the per-repo verified state (which
|
|
1074
|
+
// requires a server ETag — see the `result.etag` gate in sync.mjs)...
|
|
1075
|
+
declareRepo();
|
|
1076
|
+
server.setSkillsetEtag('W/"ss-' + "a".repeat(64) + '"');
|
|
1077
|
+
server.setSkillsetResponse(scopedResponse([makeSkill("checkout-flow")]));
|
|
1078
|
+
await runUpdate(["--key", VALID_KEY, "--url", serverUrl], { stdout });
|
|
1079
|
+
|
|
1080
|
+
// ...then the registry goes unreachable; the interactive sync (never
|
|
1081
|
+
// throttled) serves last-verified and must keep the "Skillset" noun.
|
|
1082
|
+
server.setSkillsetError({
|
|
1083
|
+
status: 503,
|
|
1084
|
+
body: { error: "upstream flake" },
|
|
1085
|
+
headers: { "Retry-After": "30" },
|
|
1086
|
+
});
|
|
1087
|
+
const graceStdout = createCaptureStream();
|
|
1088
|
+
await runUpdate(
|
|
1089
|
+
["--key", VALID_KEY, "--url", serverUrl],
|
|
1090
|
+
{ stdout: graceStdout },
|
|
1091
|
+
);
|
|
1092
|
+
const graceOut = graceStdout.text();
|
|
1093
|
+
assert.match(graceOut, /Skillset is up to date/);
|
|
1094
|
+
assert.doesNotMatch(graceOut, /Library is up to date/);
|
|
1095
|
+
});
|
|
1096
|
+
});
|
|
1097
|
+
|
|
991
1098
|
// ── Throttle window + declaration interplay through the hook flags ─────
|
|
992
1099
|
// (#2362 prod-readiness QA): a throttled hook sync returns BEFORE the
|
|
993
1100
|
// gate by documented design (#2174 zero-work contract). A repo that
|
|
@@ -1549,7 +1549,9 @@ describe("repo compliance state line (#2665) — always on while non-compliant",
|
|
|
1549
1549
|
{ stdout },
|
|
1550
1550
|
);
|
|
1551
1551
|
const out = stdout.text();
|
|
1552
|
-
|
|
1552
|
+
// Declared repo → scoped sync → the noun is "Skillset", not "Library"
|
|
1553
|
+
// (#2679; a top-level `skillsetRef` is stamped on the scoped path).
|
|
1554
|
+
assert.match(out, /\[SkillRepo\] Skillset synced: 1 added/);
|
|
1553
1555
|
assert.match(
|
|
1554
1556
|
out,
|
|
1555
1557
|
/\[SkillRepo\] This repo does not match its skillset acme\/backend-core \(1 extra skill in the project\) — it is reported as not compliant\./,
|