@dzhechkov/harness-core 0.3.82 → 0.3.86
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/dist/skill-drift.d.ts +38 -11
- package/dist/skill-drift.d.ts.map +1 -1
- package/dist/skill-drift.js +102 -37
- package/dist/skill-drift.js.map +1 -1
- package/package.json +5 -5
- package/src/skill-drift.ts +132 -41
package/dist/skill-drift.d.ts
CHANGED
|
@@ -57,26 +57,44 @@ export interface SweepResult {
|
|
|
57
57
|
/** Skills that byte-differ but are allowlisted (intentional) — surfaced for transparency, not gated. */
|
|
58
58
|
readonly allowlisted: readonly DriftedSkill[];
|
|
59
59
|
}
|
|
60
|
+
/**
|
|
61
|
+
* How the canonical was resolved for a {@link syncCanonicalSkill} run.
|
|
62
|
+
* - `from` — an explicit `--from <dir>` was supplied.
|
|
63
|
+
* - `skills-meta` — no `--from`; `skills-meta/<skill>` exists and was used.
|
|
64
|
+
* - `auto` — no explicit canonical; the WRITE path auto-detected the most-complete copy
|
|
65
|
+
* (opt-in `--auto` ONLY — never a bare default).
|
|
66
|
+
* - `none` — no canonical could be resolved. `--check` still runs a canonical-free peer
|
|
67
|
+
* comparison; a bare write refuses (safe-by-default).
|
|
68
|
+
*/
|
|
69
|
+
export type CanonicalSource = 'from' | 'skills-meta' | 'auto' | 'none';
|
|
60
70
|
/** Options for {@link syncCanonicalSkill}. */
|
|
61
71
|
export interface SyncCanonicalOptions {
|
|
62
72
|
/** Report drift only, write NOTHING (CI mode). */
|
|
63
73
|
readonly check?: boolean;
|
|
64
74
|
/** Override the canonical source dir (default: `skills-meta/<skill>`). */
|
|
65
75
|
readonly from?: string;
|
|
76
|
+
/**
|
|
77
|
+
* Opt-in for the WRITE path ONLY: when no `--from`/`skills-meta` canonical exists, auto-detect the
|
|
78
|
+
* most-complete copy as canonical instead of refusing. A HEURISTIC — the CLI prints a loud warning
|
|
79
|
+
* naming the pick + the exact overwrite list. Never affects the read-only `check` path.
|
|
80
|
+
*/
|
|
81
|
+
readonly auto?: boolean;
|
|
66
82
|
}
|
|
67
|
-
/** Result of a canonical-wins sync (or a `check:true` dry-run). */
|
|
83
|
+
/** Result of a canonical-wins sync (or a `check:true` dry-run / canonical-free peer check). */
|
|
68
84
|
export interface SyncResult {
|
|
69
|
-
/** Resolved canonical dir (abs). */
|
|
85
|
+
/** Resolved canonical dir (abs). Empty `''` in canonical-free peer / refuse modes. */
|
|
70
86
|
readonly canonical: string;
|
|
71
|
-
/** Whether
|
|
87
|
+
/** Whether a canonical source dir was resolved. `false` ⇒ `resolvedFrom === 'none'`. */
|
|
72
88
|
readonly canonicalExists: boolean;
|
|
73
|
-
/**
|
|
89
|
+
/** How the canonical resolved: `from` | `skills-meta` | `auto` | `none`. */
|
|
90
|
+
readonly resolvedFrom: CanonicalSource;
|
|
91
|
+
/** # copies compared — non-canonical copies in resolved modes; ALL peers in canonical-free mode. */
|
|
74
92
|
readonly copies: number;
|
|
75
|
-
/** # copies overwritten (0 when `check:true
|
|
93
|
+
/** # copies overwritten (0 when `check:true`, in peer mode, or when a bare write refuses). */
|
|
76
94
|
readonly synced: number;
|
|
77
|
-
/** # copies that differ from canonical. */
|
|
95
|
+
/** # copies/files that differ (from canonical in resolved modes; between peers in canonical-free mode). */
|
|
78
96
|
readonly drifted: number;
|
|
79
|
-
/** Abs paths of copies written (empty when `check:true` — proves no writes). */
|
|
97
|
+
/** Abs paths of copies written (empty when `check:true` / peer / refuse — proves no writes). */
|
|
80
98
|
readonly wrote: readonly string[];
|
|
81
99
|
}
|
|
82
100
|
/**
|
|
@@ -87,13 +105,22 @@ export interface SyncResult {
|
|
|
87
105
|
*/
|
|
88
106
|
export declare function sweepSkillDrift(root: string, opts?: SweepOptions): SweepResult;
|
|
89
107
|
/**
|
|
90
|
-
* Heal one skill: treat `
|
|
91
|
-
*
|
|
108
|
+
* Heal one skill: treat the resolved canonical (`--from` → `skills-meta/<skill>` → `--auto`
|
|
109
|
+
* most-complete copy) as authoritative and overwrite every other copy in the monorepo, proving
|
|
110
|
+
* byte-identity. Pure port of `scripts/sync-canonical-skill.mjs`, extended with a canonical-free path.
|
|
92
111
|
*
|
|
93
112
|
* `check:true` writes NOTHING (`wrote` stays empty) and only reports the drift count.
|
|
94
113
|
* Default overwrites drifting copies; a subsequent {@link sweepSkillDrift} then reports 0 drift.
|
|
95
|
-
*
|
|
96
|
-
*
|
|
114
|
+
*
|
|
115
|
+
* When NO canonical resolves (`resolvedFrom === 'none'` — no `--from`, no `skills-meta`, no `--auto`):
|
|
116
|
+
* • `check:true` → CANONICAL-FREE peer check: `drifted` = # files that differ ACROSS the copies
|
|
117
|
+
* (byte-identical copies ⇒ `drifted === 0`). Reuses {@link comparePeers} — the exact sweep logic.
|
|
118
|
+
* • write (bare) → REFUSES: returns `wrote:[]`, `synced:0`, mutates NOTHING. Safe-by-default: the
|
|
119
|
+
* tool never guesses a canonical for a write, because a wrong pick destroys the good copy. The
|
|
120
|
+
* operator must pass `--from`, run `--check`, or opt in to `--auto` (which the CLI announces).
|
|
121
|
+
*
|
|
122
|
+
* This function never throws / never `process.exit`s — the CLI owns exit codes, printing, and the
|
|
123
|
+
* loud `--auto` warning.
|
|
97
124
|
*/
|
|
98
125
|
export declare function syncCanonicalSkill(root: string, skill: string, opts?: SyncCanonicalOptions): SyncResult;
|
|
99
126
|
//# sourceMappingURL=skill-drift.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"skill-drift.d.ts","sourceRoot":"","sources":["../src/skill-drift.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAMH,oEAAoE;AACpE,MAAM,WAAW,YAAY;IAC3B,yDAAyD;IACzD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,0CAA0C;IAC1C,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,+DAA+D;IAC/D,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,kEAAkE;IAClE,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,2EAA2E;IAC3E,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,gGAAgG;IAChG,QAAQ,CAAC,SAAS,EAAE,SAAS,MAAM,EAAE,CAAC;CACvC;AAED,2CAA2C;AAC3C,MAAM,WAAW,YAAY;IAC3B;;;;;;;OAOG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,UAAU,GAAG,KAAK,CAAC;IACpC,iIAAiI;IACjI,QAAQ,CAAC,SAAS,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CACxC;AAED,wDAAwD;AACxD,MAAM,WAAW,WAAW;IAC1B,uDAAuD;IACvD,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,4GAA4G;IAC5G,QAAQ,CAAC,OAAO,EAAE,SAAS,YAAY,EAAE,CAAC;IAC1C,wGAAwG;IACxG,QAAQ,CAAC,WAAW,EAAE,SAAS,YAAY,EAAE,CAAC;CAC/C;AAED,8CAA8C;AAC9C,MAAM,WAAW,oBAAoB;IACnC,kDAAkD;IAClD,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC;IACzB,0EAA0E;IAC1E,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"skill-drift.d.ts","sourceRoot":"","sources":["../src/skill-drift.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAMH,oEAAoE;AACpE,MAAM,WAAW,YAAY;IAC3B,yDAAyD;IACzD,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IACtB,0CAA0C;IAC1C,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,+DAA+D;IAC/D,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,kEAAkE;IAClE,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,2EAA2E;IAC3E,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,gGAAgG;IAChG,QAAQ,CAAC,SAAS,EAAE,SAAS,MAAM,EAAE,CAAC;CACvC;AAED,2CAA2C;AAC3C,MAAM,WAAW,YAAY;IAC3B;;;;;;;OAOG;IACH,QAAQ,CAAC,KAAK,CAAC,EAAE,UAAU,GAAG,KAAK,CAAC;IACpC,iIAAiI;IACjI,QAAQ,CAAC,SAAS,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;CACxC;AAED,wDAAwD;AACxD,MAAM,WAAW,WAAW;IAC1B,uDAAuD;IACvD,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,4GAA4G;IAC5G,QAAQ,CAAC,OAAO,EAAE,SAAS,YAAY,EAAE,CAAC;IAC1C,wGAAwG;IACxG,QAAQ,CAAC,WAAW,EAAE,SAAS,YAAY,EAAE,CAAC;CAC/C;AAED;;;;;;;;GAQG;AACH,MAAM,MAAM,eAAe,GAAG,MAAM,GAAG,aAAa,GAAG,MAAM,GAAG,MAAM,CAAC;AAEvE,8CAA8C;AAC9C,MAAM,WAAW,oBAAoB;IACnC,kDAAkD;IAClD,QAAQ,CAAC,KAAK,CAAC,EAAE,OAAO,CAAC;IACzB,0EAA0E;IAC1E,QAAQ,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC;IACvB;;;;OAIG;IACH,QAAQ,CAAC,IAAI,CAAC,EAAE,OAAO,CAAC;CACzB;AAED,+FAA+F;AAC/F,MAAM,WAAW,UAAU;IACzB,sFAAsF;IACtF,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,wFAAwF;IACxF,QAAQ,CAAC,eAAe,EAAE,OAAO,CAAC;IAClC,4EAA4E;IAC5E,QAAQ,CAAC,YAAY,EAAE,eAAe,CAAC;IACvC,oGAAoG;IACpG,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,8FAA8F;IAC9F,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,2GAA2G;IAC3G,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,gGAAgG;IAChG,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;CACnC;AA6HD;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,GAAE,YAAiB,GAAG,WAAW,CA2ClF;AAED;;;;;;;;;;;;;;;;;GAiBG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,GAAE,oBAAyB,GAAG,UAAU,CA6D3G"}
|
package/dist/skill-drift.js
CHANGED
|
@@ -20,10 +20,10 @@
|
|
|
20
20
|
* @packageDocumentation
|
|
21
21
|
*/
|
|
22
22
|
import { readdirSync, statSync, readFileSync, writeFileSync, mkdirSync, rmSync, existsSync } from 'node:fs';
|
|
23
|
-
import { join, relative, dirname, basename } from 'node:path';
|
|
23
|
+
import { join, relative, resolve, dirname, basename } from 'node:path';
|
|
24
24
|
import { createHash } from 'node:crypto';
|
|
25
25
|
const SKILL_MANIFEST = 'SKILL.md';
|
|
26
|
-
const IGNORED_ENTRIES = new Set(['node_modules', '__pycache__', '.DS_Store']);
|
|
26
|
+
const IGNORED_ENTRIES = new Set(['node_modules', '__pycache__', '.DS_Store', 'run-history.json']);
|
|
27
27
|
/** md5 of a file's bytes (identical to both prototype scripts ⇒ identical drift verdicts). */
|
|
28
28
|
function md5(path) {
|
|
29
29
|
return createHash('md5').update(readFileSync(path)).digest('hex');
|
|
@@ -49,6 +49,67 @@ function walk(dir) {
|
|
|
49
49
|
}
|
|
50
50
|
return out;
|
|
51
51
|
}
|
|
52
|
+
/**
|
|
53
|
+
* Byte-compare a set of skill copies against EACH OTHER (canonical-free). Builds the union of
|
|
54
|
+
* relative file paths across every copy, hashes each `(copy × rel)`, and counts how many relative
|
|
55
|
+
* paths disagree (`driftFiles`) and how many `(copy × rel)` pairs are absent from a copy
|
|
56
|
+
* (`missingFiles`). `driftFiles > 0` ⇔ the copies are NOT byte-identical to one another.
|
|
57
|
+
*
|
|
58
|
+
* This is the exact per-skill comparison {@link sweepSkillDrift} performs; it is extracted verbatim
|
|
59
|
+
* so BOTH the CI sweep AND the canonical-free `sync-canonical --check` share one implementation and
|
|
60
|
+
* yield the same verdict. Behavior-preserving refactor — no numbers change.
|
|
61
|
+
*/
|
|
62
|
+
function comparePeers(copies) {
|
|
63
|
+
const relFiles = new Set();
|
|
64
|
+
for (const c of copies)
|
|
65
|
+
for (const f of walk(c))
|
|
66
|
+
relFiles.add(relative(c, f));
|
|
67
|
+
let driftFiles = 0;
|
|
68
|
+
let missingFiles = 0;
|
|
69
|
+
for (const rel of relFiles) {
|
|
70
|
+
const hashes = new Set();
|
|
71
|
+
for (const c of copies) {
|
|
72
|
+
const p = join(c, rel);
|
|
73
|
+
if (existsSync(p))
|
|
74
|
+
hashes.add(md5(p));
|
|
75
|
+
else {
|
|
76
|
+
missingFiles++;
|
|
77
|
+
hashes.add('__MISSING__');
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (hashes.size > 1)
|
|
81
|
+
driftFiles++;
|
|
82
|
+
}
|
|
83
|
+
return { driftFiles, missingFiles, totalFiles: relFiles.size };
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Deterministic auto-pick of a canonical from a set of copies: the copy with the MOST files wins,
|
|
87
|
+
* tie-broken lexicographically on sorted path (so the same drift always auto-picks the same
|
|
88
|
+
* canonical, across runs and machines). "Most complete" is a HEURISTIC, not a correctness oracle —
|
|
89
|
+
* hence it is only ever reached behind an explicit `--auto` opt-in plus a loud warning.
|
|
90
|
+
*/
|
|
91
|
+
function pickMostComplete(copies) {
|
|
92
|
+
return [...copies].sort().reduce((best, c) => (walk(c).length > walk(best).length ? c : best));
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Resolve WHICH dir is canonical for a sync/check run, and HOW it resolved. Precedence is identical
|
|
96
|
+
* for read and write paths:
|
|
97
|
+
* 1. `opts.from` → `'from'`
|
|
98
|
+
* 2. `skills-meta/<skill>` → `'skills-meta'`
|
|
99
|
+
* 3. `opts.auto` (write) → `'auto'` (most-complete copy)
|
|
100
|
+
* 4. otherwise → `'none'` (no canonical — `--check` compares copies to each other;
|
|
101
|
+
* a bare write refuses)
|
|
102
|
+
*/
|
|
103
|
+
function resolveCanonical(root, skill, copies, opts) {
|
|
104
|
+
if (opts.from !== undefined)
|
|
105
|
+
return { canonical: resolve(opts.from), resolvedFrom: 'from' };
|
|
106
|
+
const meta = join(root, 'packages/@dzhechkov/skills-meta', skill);
|
|
107
|
+
if (existsSync(meta))
|
|
108
|
+
return { canonical: meta, resolvedFrom: 'skills-meta' };
|
|
109
|
+
if (opts.auto === true && copies.length >= 1)
|
|
110
|
+
return { canonical: pickMostComplete(copies), resolvedFrom: 'auto' };
|
|
111
|
+
return { canonical: null, resolvedFrom: 'none' };
|
|
112
|
+
}
|
|
52
113
|
/**
|
|
53
114
|
* Every skill dir (a dir containing `SKILL.md`) under `packages/` + `.claude/skills`, excluding
|
|
54
115
|
* `node_modules` / `__pycache__`. Ported verbatim from `scripts/drift-sweep-skills.mjs`.
|
|
@@ -66,6 +127,10 @@ function findSkillDirs(root, scope = 'all') {
|
|
|
66
127
|
catch {
|
|
67
128
|
continue;
|
|
68
129
|
}
|
|
130
|
+
// Drift-guard scope = SKILL.md-bearing dirs ONLY. `templates/docs/<name>/` mirrors carry
|
|
131
|
+
// skill-shaped NAMES but are rendered/derived documentation (no SKILL.md) and are therefore
|
|
132
|
+
// intentionally excluded — they are not skill definitions, so there is nothing to keep in sync.
|
|
133
|
+
// (Verified: no `packages/**/templates/docs/**` dir carries a SKILL.md. See ADR-001, D1.)
|
|
69
134
|
if (entries.includes(SKILL_MANIFEST))
|
|
70
135
|
dirs.push(d);
|
|
71
136
|
for (const e of entries) {
|
|
@@ -110,33 +175,15 @@ export function sweepSkillDrift(root, opts = {}) {
|
|
|
110
175
|
continue;
|
|
111
176
|
duplicated++;
|
|
112
177
|
const copies = [...unsorted].sort();
|
|
113
|
-
//
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
for (const f of walk(c))
|
|
117
|
-
relFiles.add(relative(c, f));
|
|
118
|
-
let driftFiles = 0;
|
|
119
|
-
let missingFiles = 0;
|
|
120
|
-
for (const rel of relFiles) {
|
|
121
|
-
const hashes = new Set();
|
|
122
|
-
for (const c of copies) {
|
|
123
|
-
const p = join(c, rel);
|
|
124
|
-
if (existsSync(p))
|
|
125
|
-
hashes.add(md5(p));
|
|
126
|
-
else {
|
|
127
|
-
missingFiles++;
|
|
128
|
-
hashes.add('__MISSING__');
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
if (hashes.size > 1)
|
|
132
|
-
driftFiles++;
|
|
133
|
-
}
|
|
178
|
+
// Per-skill byte-comparison of every copy against each other (extracted to `comparePeers`
|
|
179
|
+
// so the canonical-free `sync-canonical --check` reuses the EXACT same logic).
|
|
180
|
+
const { driftFiles, missingFiles, totalFiles } = comparePeers(copies);
|
|
134
181
|
if (driftFiles > 0) {
|
|
135
182
|
const entry = {
|
|
136
183
|
name,
|
|
137
184
|
copies: copies.length,
|
|
138
185
|
driftFiles,
|
|
139
|
-
totalFiles
|
|
186
|
+
totalFiles,
|
|
140
187
|
missingFiles,
|
|
141
188
|
locations: copies,
|
|
142
189
|
};
|
|
@@ -149,28 +196,46 @@ export function sweepSkillDrift(root, opts = {}) {
|
|
|
149
196
|
return { duplicated, drifted, allowlisted };
|
|
150
197
|
}
|
|
151
198
|
/**
|
|
152
|
-
* Heal one skill: treat `
|
|
153
|
-
*
|
|
199
|
+
* Heal one skill: treat the resolved canonical (`--from` → `skills-meta/<skill>` → `--auto`
|
|
200
|
+
* most-complete copy) as authoritative and overwrite every other copy in the monorepo, proving
|
|
201
|
+
* byte-identity. Pure port of `scripts/sync-canonical-skill.mjs`, extended with a canonical-free path.
|
|
154
202
|
*
|
|
155
203
|
* `check:true` writes NOTHING (`wrote` stays empty) and only reports the drift count.
|
|
156
204
|
* Default overwrites drifting copies; a subsequent {@link sweepSkillDrift} then reports 0 drift.
|
|
157
|
-
*
|
|
158
|
-
*
|
|
205
|
+
*
|
|
206
|
+
* When NO canonical resolves (`resolvedFrom === 'none'` — no `--from`, no `skills-meta`, no `--auto`):
|
|
207
|
+
* • `check:true` → CANONICAL-FREE peer check: `drifted` = # files that differ ACROSS the copies
|
|
208
|
+
* (byte-identical copies ⇒ `drifted === 0`). Reuses {@link comparePeers} — the exact sweep logic.
|
|
209
|
+
* • write (bare) → REFUSES: returns `wrote:[]`, `synced:0`, mutates NOTHING. Safe-by-default: the
|
|
210
|
+
* tool never guesses a canonical for a write, because a wrong pick destroys the good copy. The
|
|
211
|
+
* operator must pass `--from`, run `--check`, or opt in to `--auto` (which the CLI announces).
|
|
212
|
+
*
|
|
213
|
+
* This function never throws / never `process.exit`s — the CLI owns exit codes, printing, and the
|
|
214
|
+
* loud `--auto` warning.
|
|
159
215
|
*/
|
|
160
216
|
export function syncCanonicalSkill(root, skill, opts = {}) {
|
|
161
217
|
const check = opts.check === true;
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
218
|
+
// The healer/checker operates on EXACTLY what the detector sees (same roots via findSkillDirs) —
|
|
219
|
+
// otherwise a copy could be silently healed but never gated, or vice-versa.
|
|
220
|
+
const allCopies = findSkillDirs(root, 'all')
|
|
221
|
+
.filter((d) => basename(d) === skill)
|
|
222
|
+
.sort();
|
|
223
|
+
const { canonical, resolvedFrom } = resolveCanonical(root, skill, allCopies, opts);
|
|
224
|
+
// No canonical resolved → asymmetric read/write handling (ADR-001 D3/D4).
|
|
225
|
+
if (canonical === null) {
|
|
226
|
+
if (check) {
|
|
227
|
+
// CANONICAL-FREE peer check: are the copies byte-identical to EACH OTHER? (<2 ⇒ vacuously so.)
|
|
228
|
+
const drifted = allCopies.length < 2 ? 0 : comparePeers(allCopies).driftFiles;
|
|
229
|
+
return { canonical: '', canonicalExists: false, resolvedFrom, copies: allCopies.length, synced: 0, drifted, wrote: [] };
|
|
230
|
+
}
|
|
231
|
+
// Bare WRITE with no resolvable canonical → REFUSE. Mutates nothing (`wrote:[]` proves it).
|
|
232
|
+
return { canonical: '', canonicalExists: false, resolvedFrom, copies: allCopies.length, synced: 0, drifted: 0, wrote: [] };
|
|
165
233
|
}
|
|
166
234
|
const canonFiles = walk(canonical)
|
|
167
235
|
.map((p) => relative(canonical, p))
|
|
168
236
|
.sort();
|
|
169
|
-
//
|
|
170
|
-
|
|
171
|
-
const copies = findSkillDirs(root, 'all')
|
|
172
|
-
.filter((d) => basename(d) === skill && relative(canonical, d) !== '')
|
|
173
|
-
.sort();
|
|
237
|
+
// Every <skill>/ dir except the canonical itself.
|
|
238
|
+
const copies = allCopies.filter((d) => relative(canonical, d) !== '');
|
|
174
239
|
let drifted = 0;
|
|
175
240
|
let synced = 0;
|
|
176
241
|
const wrote = [];
|
|
@@ -205,6 +270,6 @@ export function syncCanonicalSkill(root, skill, opts = {}) {
|
|
|
205
270
|
synced++;
|
|
206
271
|
wrote.push(copy);
|
|
207
272
|
}
|
|
208
|
-
return { canonical, canonicalExists: true, copies: copies.length, synced, drifted, wrote };
|
|
273
|
+
return { canonical, canonicalExists: true, resolvedFrom, copies: copies.length, synced, drifted, wrote };
|
|
209
274
|
}
|
|
210
275
|
//# sourceMappingURL=skill-drift.js.map
|
package/dist/skill-drift.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"skill-drift.js","sourceRoot":"","sources":["../src/skill-drift.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,EAAE,WAAW,EAAE,QAAQ,EAAE,YAAY,EAAE,aAAa,EAAE,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,MAAM,SAAS,CAAC;AAC5G,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,OAAO,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;
|
|
1
|
+
{"version":3,"file":"skill-drift.js","sourceRoot":"","sources":["../src/skill-drift.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dzhechkov/harness-core",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.86",
|
|
4
4
|
"description": "Shared harness logic - skill loading, additive apply, and the init/sync/verify/doctor operations.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -31,12 +31,12 @@
|
|
|
31
31
|
"@dzhechkov/adapter-opencode": "^0.2.0",
|
|
32
32
|
"yaml": "^2.0.0",
|
|
33
33
|
"@dzhechkov/adapter-agents-md": "0.1.1",
|
|
34
|
-
"@dzhechkov/adapter-copilot": "0.1.1",
|
|
35
34
|
"@dzhechkov/adapter-gemini": "0.1.1",
|
|
36
|
-
"@dzhechkov/adapter-
|
|
35
|
+
"@dzhechkov/adapter-copilot": "0.1.1",
|
|
37
36
|
"@dzhechkov/adapter-windsurf": "0.1.1",
|
|
38
|
-
"@dzhechkov/
|
|
39
|
-
"@dzhechkov/memory": "0.2.9"
|
|
37
|
+
"@dzhechkov/adapter-cursor": "0.1.1",
|
|
38
|
+
"@dzhechkov/memory": "0.2.9",
|
|
39
|
+
"@dzhechkov/core": "0.2.14"
|
|
40
40
|
},
|
|
41
41
|
"peerDependenciesMeta": {
|
|
42
42
|
"@ruvector/rvf": {
|
package/src/skill-drift.ts
CHANGED
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
23
|
import { readdirSync, statSync, readFileSync, writeFileSync, mkdirSync, rmSync, existsSync } from 'node:fs';
|
|
24
|
-
import { join, relative, dirname, basename } from 'node:path';
|
|
24
|
+
import { join, relative, resolve, dirname, basename } from 'node:path';
|
|
25
25
|
import { createHash } from 'node:crypto';
|
|
26
26
|
|
|
27
27
|
/** One shared skill whose copies byte-differ (`driftFiles > 0`). */
|
|
@@ -65,32 +65,51 @@ export interface SweepResult {
|
|
|
65
65
|
readonly allowlisted: readonly DriftedSkill[];
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
+
/**
|
|
69
|
+
* How the canonical was resolved for a {@link syncCanonicalSkill} run.
|
|
70
|
+
* - `from` — an explicit `--from <dir>` was supplied.
|
|
71
|
+
* - `skills-meta` — no `--from`; `skills-meta/<skill>` exists and was used.
|
|
72
|
+
* - `auto` — no explicit canonical; the WRITE path auto-detected the most-complete copy
|
|
73
|
+
* (opt-in `--auto` ONLY — never a bare default).
|
|
74
|
+
* - `none` — no canonical could be resolved. `--check` still runs a canonical-free peer
|
|
75
|
+
* comparison; a bare write refuses (safe-by-default).
|
|
76
|
+
*/
|
|
77
|
+
export type CanonicalSource = 'from' | 'skills-meta' | 'auto' | 'none';
|
|
78
|
+
|
|
68
79
|
/** Options for {@link syncCanonicalSkill}. */
|
|
69
80
|
export interface SyncCanonicalOptions {
|
|
70
81
|
/** Report drift only, write NOTHING (CI mode). */
|
|
71
82
|
readonly check?: boolean;
|
|
72
83
|
/** Override the canonical source dir (default: `skills-meta/<skill>`). */
|
|
73
84
|
readonly from?: string;
|
|
85
|
+
/**
|
|
86
|
+
* Opt-in for the WRITE path ONLY: when no `--from`/`skills-meta` canonical exists, auto-detect the
|
|
87
|
+
* most-complete copy as canonical instead of refusing. A HEURISTIC — the CLI prints a loud warning
|
|
88
|
+
* naming the pick + the exact overwrite list. Never affects the read-only `check` path.
|
|
89
|
+
*/
|
|
90
|
+
readonly auto?: boolean;
|
|
74
91
|
}
|
|
75
92
|
|
|
76
|
-
/** Result of a canonical-wins sync (or a `check:true` dry-run). */
|
|
93
|
+
/** Result of a canonical-wins sync (or a `check:true` dry-run / canonical-free peer check). */
|
|
77
94
|
export interface SyncResult {
|
|
78
|
-
/** Resolved canonical dir (abs). */
|
|
95
|
+
/** Resolved canonical dir (abs). Empty `''` in canonical-free peer / refuse modes. */
|
|
79
96
|
readonly canonical: string;
|
|
80
|
-
/** Whether
|
|
97
|
+
/** Whether a canonical source dir was resolved. `false` ⇒ `resolvedFrom === 'none'`. */
|
|
81
98
|
readonly canonicalExists: boolean;
|
|
82
|
-
/**
|
|
99
|
+
/** How the canonical resolved: `from` | `skills-meta` | `auto` | `none`. */
|
|
100
|
+
readonly resolvedFrom: CanonicalSource;
|
|
101
|
+
/** # copies compared — non-canonical copies in resolved modes; ALL peers in canonical-free mode. */
|
|
83
102
|
readonly copies: number;
|
|
84
|
-
/** # copies overwritten (0 when `check:true
|
|
103
|
+
/** # copies overwritten (0 when `check:true`, in peer mode, or when a bare write refuses). */
|
|
85
104
|
readonly synced: number;
|
|
86
|
-
/** # copies that differ from canonical. */
|
|
105
|
+
/** # copies/files that differ (from canonical in resolved modes; between peers in canonical-free mode). */
|
|
87
106
|
readonly drifted: number;
|
|
88
|
-
/** Abs paths of copies written (empty when `check:true` — proves no writes). */
|
|
107
|
+
/** Abs paths of copies written (empty when `check:true` / peer / refuse — proves no writes). */
|
|
89
108
|
readonly wrote: readonly string[];
|
|
90
109
|
}
|
|
91
110
|
|
|
92
111
|
const SKILL_MANIFEST = 'SKILL.md';
|
|
93
|
-
const IGNORED_ENTRIES = new Set(['node_modules', '__pycache__', '.DS_Store']);
|
|
112
|
+
const IGNORED_ENTRIES = new Set(['node_modules', '__pycache__', '.DS_Store', 'run-history.json']);
|
|
94
113
|
|
|
95
114
|
/** md5 of a file's bytes (identical to both prototype scripts ⇒ identical drift verdicts). */
|
|
96
115
|
function md5(path: string): string {
|
|
@@ -115,6 +134,69 @@ function walk(dir: string): string[] {
|
|
|
115
134
|
return out;
|
|
116
135
|
}
|
|
117
136
|
|
|
137
|
+
/**
|
|
138
|
+
* Byte-compare a set of skill copies against EACH OTHER (canonical-free). Builds the union of
|
|
139
|
+
* relative file paths across every copy, hashes each `(copy × rel)`, and counts how many relative
|
|
140
|
+
* paths disagree (`driftFiles`) and how many `(copy × rel)` pairs are absent from a copy
|
|
141
|
+
* (`missingFiles`). `driftFiles > 0` ⇔ the copies are NOT byte-identical to one another.
|
|
142
|
+
*
|
|
143
|
+
* This is the exact per-skill comparison {@link sweepSkillDrift} performs; it is extracted verbatim
|
|
144
|
+
* so BOTH the CI sweep AND the canonical-free `sync-canonical --check` share one implementation and
|
|
145
|
+
* yield the same verdict. Behavior-preserving refactor — no numbers change.
|
|
146
|
+
*/
|
|
147
|
+
function comparePeers(copies: readonly string[]): { driftFiles: number; missingFiles: number; totalFiles: number } {
|
|
148
|
+
const relFiles = new Set<string>();
|
|
149
|
+
for (const c of copies) for (const f of walk(c)) relFiles.add(relative(c, f));
|
|
150
|
+
|
|
151
|
+
let driftFiles = 0;
|
|
152
|
+
let missingFiles = 0;
|
|
153
|
+
for (const rel of relFiles) {
|
|
154
|
+
const hashes = new Set<string>();
|
|
155
|
+
for (const c of copies) {
|
|
156
|
+
const p = join(c, rel);
|
|
157
|
+
if (existsSync(p)) hashes.add(md5(p));
|
|
158
|
+
else {
|
|
159
|
+
missingFiles++;
|
|
160
|
+
hashes.add('__MISSING__');
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
if (hashes.size > 1) driftFiles++;
|
|
164
|
+
}
|
|
165
|
+
return { driftFiles, missingFiles, totalFiles: relFiles.size };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Deterministic auto-pick of a canonical from a set of copies: the copy with the MOST files wins,
|
|
170
|
+
* tie-broken lexicographically on sorted path (so the same drift always auto-picks the same
|
|
171
|
+
* canonical, across runs and machines). "Most complete" is a HEURISTIC, not a correctness oracle —
|
|
172
|
+
* hence it is only ever reached behind an explicit `--auto` opt-in plus a loud warning.
|
|
173
|
+
*/
|
|
174
|
+
function pickMostComplete(copies: readonly string[]): string {
|
|
175
|
+
return [...copies].sort().reduce((best, c) => (walk(c).length > walk(best).length ? c : best));
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Resolve WHICH dir is canonical for a sync/check run, and HOW it resolved. Precedence is identical
|
|
180
|
+
* for read and write paths:
|
|
181
|
+
* 1. `opts.from` → `'from'`
|
|
182
|
+
* 2. `skills-meta/<skill>` → `'skills-meta'`
|
|
183
|
+
* 3. `opts.auto` (write) → `'auto'` (most-complete copy)
|
|
184
|
+
* 4. otherwise → `'none'` (no canonical — `--check` compares copies to each other;
|
|
185
|
+
* a bare write refuses)
|
|
186
|
+
*/
|
|
187
|
+
function resolveCanonical(
|
|
188
|
+
root: string,
|
|
189
|
+
skill: string,
|
|
190
|
+
copies: readonly string[],
|
|
191
|
+
opts: SyncCanonicalOptions,
|
|
192
|
+
): { canonical: string | null; resolvedFrom: CanonicalSource } {
|
|
193
|
+
if (opts.from !== undefined) return { canonical: resolve(opts.from), resolvedFrom: 'from' };
|
|
194
|
+
const meta = join(root, 'packages/@dzhechkov/skills-meta', skill);
|
|
195
|
+
if (existsSync(meta)) return { canonical: meta, resolvedFrom: 'skills-meta' };
|
|
196
|
+
if (opts.auto === true && copies.length >= 1) return { canonical: pickMostComplete(copies), resolvedFrom: 'auto' };
|
|
197
|
+
return { canonical: null, resolvedFrom: 'none' };
|
|
198
|
+
}
|
|
199
|
+
|
|
118
200
|
/**
|
|
119
201
|
* Every skill dir (a dir containing `SKILL.md`) under `packages/` + `.claude/skills`, excluding
|
|
120
202
|
* `node_modules` / `__pycache__`. Ported verbatim from `scripts/drift-sweep-skills.mjs`.
|
|
@@ -131,6 +213,10 @@ function findSkillDirs(root: string, scope: 'packages' | 'all' = 'all'): string[
|
|
|
131
213
|
} catch {
|
|
132
214
|
continue;
|
|
133
215
|
}
|
|
216
|
+
// Drift-guard scope = SKILL.md-bearing dirs ONLY. `templates/docs/<name>/` mirrors carry
|
|
217
|
+
// skill-shaped NAMES but are rendered/derived documentation (no SKILL.md) and are therefore
|
|
218
|
+
// intentionally excluded — they are not skill definitions, so there is nothing to keep in sync.
|
|
219
|
+
// (Verified: no `packages/**/templates/docs/**` dir carries a SKILL.md. See ADR-001, D1.)
|
|
134
220
|
if (entries.includes(SKILL_MANIFEST)) dirs.push(d);
|
|
135
221
|
for (const e of entries) {
|
|
136
222
|
if (IGNORED_ENTRIES.has(e)) continue;
|
|
@@ -173,31 +259,16 @@ export function sweepSkillDrift(root: string, opts: SweepOptions = {}): SweepRes
|
|
|
173
259
|
duplicated++;
|
|
174
260
|
const copies = [...unsorted].sort();
|
|
175
261
|
|
|
176
|
-
//
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
let driftFiles = 0;
|
|
181
|
-
let missingFiles = 0;
|
|
182
|
-
for (const rel of relFiles) {
|
|
183
|
-
const hashes = new Set<string>();
|
|
184
|
-
for (const c of copies) {
|
|
185
|
-
const p = join(c, rel);
|
|
186
|
-
if (existsSync(p)) hashes.add(md5(p));
|
|
187
|
-
else {
|
|
188
|
-
missingFiles++;
|
|
189
|
-
hashes.add('__MISSING__');
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
if (hashes.size > 1) driftFiles++;
|
|
193
|
-
}
|
|
262
|
+
// Per-skill byte-comparison of every copy against each other (extracted to `comparePeers`
|
|
263
|
+
// so the canonical-free `sync-canonical --check` reuses the EXACT same logic).
|
|
264
|
+
const { driftFiles, missingFiles, totalFiles } = comparePeers(copies);
|
|
194
265
|
|
|
195
266
|
if (driftFiles > 0) {
|
|
196
267
|
const entry: DriftedSkill = {
|
|
197
268
|
name,
|
|
198
269
|
copies: copies.length,
|
|
199
270
|
driftFiles,
|
|
200
|
-
totalFiles
|
|
271
|
+
totalFiles,
|
|
201
272
|
missingFiles,
|
|
202
273
|
locations: copies,
|
|
203
274
|
};
|
|
@@ -212,31 +283,51 @@ export function sweepSkillDrift(root: string, opts: SweepOptions = {}): SweepRes
|
|
|
212
283
|
}
|
|
213
284
|
|
|
214
285
|
/**
|
|
215
|
-
* Heal one skill: treat `
|
|
216
|
-
*
|
|
286
|
+
* Heal one skill: treat the resolved canonical (`--from` → `skills-meta/<skill>` → `--auto`
|
|
287
|
+
* most-complete copy) as authoritative and overwrite every other copy in the monorepo, proving
|
|
288
|
+
* byte-identity. Pure port of `scripts/sync-canonical-skill.mjs`, extended with a canonical-free path.
|
|
217
289
|
*
|
|
218
290
|
* `check:true` writes NOTHING (`wrote` stays empty) and only reports the drift count.
|
|
219
291
|
* Default overwrites drifting copies; a subsequent {@link sweepSkillDrift} then reports 0 drift.
|
|
220
|
-
*
|
|
221
|
-
*
|
|
292
|
+
*
|
|
293
|
+
* When NO canonical resolves (`resolvedFrom === 'none'` — no `--from`, no `skills-meta`, no `--auto`):
|
|
294
|
+
* • `check:true` → CANONICAL-FREE peer check: `drifted` = # files that differ ACROSS the copies
|
|
295
|
+
* (byte-identical copies ⇒ `drifted === 0`). Reuses {@link comparePeers} — the exact sweep logic.
|
|
296
|
+
* • write (bare) → REFUSES: returns `wrote:[]`, `synced:0`, mutates NOTHING. Safe-by-default: the
|
|
297
|
+
* tool never guesses a canonical for a write, because a wrong pick destroys the good copy. The
|
|
298
|
+
* operator must pass `--from`, run `--check`, or opt in to `--auto` (which the CLI announces).
|
|
299
|
+
*
|
|
300
|
+
* This function never throws / never `process.exit`s — the CLI owns exit codes, printing, and the
|
|
301
|
+
* loud `--auto` warning.
|
|
222
302
|
*/
|
|
223
303
|
export function syncCanonicalSkill(root: string, skill: string, opts: SyncCanonicalOptions = {}): SyncResult {
|
|
224
304
|
const check = opts.check === true;
|
|
225
|
-
const canonical = opts.from ?? join(root, 'packages/@dzhechkov/skills-meta', skill);
|
|
226
305
|
|
|
227
|
-
|
|
228
|
-
|
|
306
|
+
// The healer/checker operates on EXACTLY what the detector sees (same roots via findSkillDirs) —
|
|
307
|
+
// otherwise a copy could be silently healed but never gated, or vice-versa.
|
|
308
|
+
const allCopies = findSkillDirs(root, 'all')
|
|
309
|
+
.filter((d) => basename(d) === skill)
|
|
310
|
+
.sort();
|
|
311
|
+
|
|
312
|
+
const { canonical, resolvedFrom } = resolveCanonical(root, skill, allCopies, opts);
|
|
313
|
+
|
|
314
|
+
// No canonical resolved → asymmetric read/write handling (ADR-001 D3/D4).
|
|
315
|
+
if (canonical === null) {
|
|
316
|
+
if (check) {
|
|
317
|
+
// CANONICAL-FREE peer check: are the copies byte-identical to EACH OTHER? (<2 ⇒ vacuously so.)
|
|
318
|
+
const drifted = allCopies.length < 2 ? 0 : comparePeers(allCopies).driftFiles;
|
|
319
|
+
return { canonical: '', canonicalExists: false, resolvedFrom, copies: allCopies.length, synced: 0, drifted, wrote: [] };
|
|
320
|
+
}
|
|
321
|
+
// Bare WRITE with no resolvable canonical → REFUSE. Mutates nothing (`wrote:[]` proves it).
|
|
322
|
+
return { canonical: '', canonicalExists: false, resolvedFrom, copies: allCopies.length, synced: 0, drifted: 0, wrote: [] };
|
|
229
323
|
}
|
|
230
324
|
|
|
231
325
|
const canonFiles = walk(canonical)
|
|
232
326
|
.map((p) => relative(canonical, p))
|
|
233
327
|
.sort();
|
|
234
328
|
|
|
235
|
-
//
|
|
236
|
-
|
|
237
|
-
const copies = findSkillDirs(root, 'all')
|
|
238
|
-
.filter((d) => basename(d) === skill && relative(canonical, d) !== '')
|
|
239
|
-
.sort();
|
|
329
|
+
// Every <skill>/ dir except the canonical itself.
|
|
330
|
+
const copies = allCopies.filter((d) => relative(canonical, d) !== '');
|
|
240
331
|
|
|
241
332
|
let drifted = 0;
|
|
242
333
|
let synced = 0;
|
|
@@ -269,5 +360,5 @@ export function syncCanonicalSkill(root: string, skill: string, opts: SyncCanoni
|
|
|
269
360
|
wrote.push(copy);
|
|
270
361
|
}
|
|
271
362
|
|
|
272
|
-
return { canonical, canonicalExists: true, copies: copies.length, synced, drifted, wrote };
|
|
363
|
+
return { canonical, canonicalExists: true, resolvedFrom, copies: copies.length, synced, drifted, wrote };
|
|
273
364
|
}
|