@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.
@@ -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 the canonical source dir exists. `false` ⇒ nothing was done (CLI → non-zero exit). */
87
+ /** Whether a canonical source dir was resolved. `false` ⇒ `resolvedFrom === 'none'`. */
72
88
  readonly canonicalExists: boolean;
73
- /** # non-canonical copies found. */
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 `from ?? skills-meta/<skill>` as canonical and overwrite every other copy in
91
- * the monorepo, proving byte-identity. Pure port of `scripts/sync-canonical-skill.mjs`.
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
- * When the canonical dir does not exist, returns `canonicalExists:false` and does nothing (the CLI
96
- * turns that into a non-zero exit)this function never throws / never `process.exit`s.
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;CACxB;AAED,mEAAmE;AACnE,MAAM,WAAW,UAAU;IACzB,oCAAoC;IACpC,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,iGAAiG;IACjG,QAAQ,CAAC,eAAe,EAAE,OAAO,CAAC;IAClC,oCAAoC;IACpC,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,kDAAkD;IAClD,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,2CAA2C;IAC3C,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IACzB,gFAAgF;IAChF,QAAQ,CAAC,KAAK,EAAE,SAAS,MAAM,EAAE,CAAC;CACnC;AA0DD;;;;;GAKG;AACH,wBAAgB,eAAe,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,GAAE,YAAiB,GAAG,WAAW,CA0DlF;AAED;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,MAAM,EAAE,IAAI,GAAE,oBAAyB,GAAG,UAAU,CAkD3G"}
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"}
@@ -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
- // Union of relative file paths across every copy.
114
- const relFiles = new Set();
115
- for (const c of copies)
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: relFiles.size,
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 `from ?? skills-meta/<skill>` as canonical and overwrite every other copy in
153
- * the monorepo, proving byte-identity. Pure port of `scripts/sync-canonical-skill.mjs`.
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
- * When the canonical dir does not exist, returns `canonicalExists:false` and does nothing (the CLI
158
- * turns that into a non-zero exit)this function never throws / never `process.exit`s.
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
- const canonical = opts.from ?? join(root, 'packages/@dzhechkov/skills-meta', skill);
163
- if (!existsSync(canonical)) {
164
- return { canonical, canonicalExists: false, copies: 0, synced: 0, drifted: 0, wrote: [] };
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
- // The healer heals EXACTLY what the detector sees (same roots via findSkillDirs) — otherwise a copy
170
- // could be silently healed but never gated, or vice-versa. Every <skill>/ dir except the canonical.
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
@@ -1 +1 @@
1
- {"version":3,"file":"skill-drift.js","sourceRoot":"","sources":["../src/skill-drift.ts"],"names":[],"mappings":"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"}
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.82",
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-cursor": "0.1.1",
35
+ "@dzhechkov/adapter-copilot": "0.1.1",
37
36
  "@dzhechkov/adapter-windsurf": "0.1.1",
38
- "@dzhechkov/core": "0.2.14",
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": {
@@ -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 the canonical source dir exists. `false` ⇒ nothing was done (CLI → non-zero exit). */
97
+ /** Whether a canonical source dir was resolved. `false` ⇒ `resolvedFrom === 'none'`. */
81
98
  readonly canonicalExists: boolean;
82
- /** # non-canonical copies found. */
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
- // Union of relative file paths across every copy.
177
- const relFiles = new Set<string>();
178
- for (const c of copies) for (const f of walk(c)) relFiles.add(relative(c, f));
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: relFiles.size,
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 `from ?? skills-meta/<skill>` as canonical and overwrite every other copy in
216
- * the monorepo, proving byte-identity. Pure port of `scripts/sync-canonical-skill.mjs`.
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
- * When the canonical dir does not exist, returns `canonicalExists:false` and does nothing (the CLI
221
- * turns that into a non-zero exit)this function never throws / never `process.exit`s.
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
- if (!existsSync(canonical)) {
228
- return { canonical, canonicalExists: false, copies: 0, synced: 0, drifted: 0, wrote: [] };
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
- // The healer heals EXACTLY what the detector sees (same roots via findSkillDirs) — otherwise a copy
236
- // could be silently healed but never gated, or vice-versa. Every <skill>/ dir except the canonical.
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
  }