@dzhechkov/harness-core 0.7.5 → 0.7.6
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/.dz-manifest.json +47 -27
- package/README.md +38 -1
- package/dist/feature-adr-routing.d.ts.map +1 -1
- package/dist/feature-adr-routing.js +6 -1
- package/dist/feature-adr-routing.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/provenance.d.ts +6 -0
- package/dist/provenance.d.ts.map +1 -1
- package/dist/provenance.js +22 -1
- package/dist/provenance.js.map +1 -1
- package/dist/skill-drift.d.ts +8 -2
- package/dist/skill-drift.d.ts.map +1 -1
- package/dist/skill-drift.js +60 -11
- package/dist/skill-drift.js.map +1 -1
- package/dist/skill-install-roots.d.ts +100 -0
- package/dist/skill-install-roots.d.ts.map +1 -0
- package/dist/skill-install-roots.js +116 -0
- package/dist/skill-install-roots.js.map +1 -0
- package/dist/tg-post.d.ts +22 -0
- package/dist/tg-post.d.ts.map +1 -1
- package/dist/tg-post.js +42 -1
- package/dist/tg-post.js.map +1 -1
- package/package.json +4 -4
- package/sbom.json +76 -26
- package/src/feature-adr-routing.ts +6 -1
- package/src/index.ts +1 -0
- package/src/provenance.ts +23 -1
- package/src/skill-drift.ts +70 -13
- package/src/skill-install-roots.ts +119 -0
- package/src/tg-post.ts +56 -1
package/sbom.json
CHANGED
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
"hashes": [
|
|
26
26
|
{
|
|
27
27
|
"alg": "SHA-256",
|
|
28
|
-
"content": "
|
|
28
|
+
"content": "6e0c7dc75ea18f37464949db1871dc6ab7a97b23e9dfff9af37bcdf19f4fbc83"
|
|
29
29
|
}
|
|
30
30
|
]
|
|
31
31
|
},
|
|
@@ -1445,7 +1445,7 @@
|
|
|
1445
1445
|
"hashes": [
|
|
1446
1446
|
{
|
|
1447
1447
|
"alg": "SHA-256",
|
|
1448
|
-
"content": "
|
|
1448
|
+
"content": "8ee50d5067b71b1d68c57dd8c5f0f0a327ffc282755a13d17565613b9d036f92"
|
|
1449
1449
|
}
|
|
1450
1450
|
]
|
|
1451
1451
|
},
|
|
@@ -1455,7 +1455,7 @@
|
|
|
1455
1455
|
"hashes": [
|
|
1456
1456
|
{
|
|
1457
1457
|
"alg": "SHA-256",
|
|
1458
|
-
"content": "
|
|
1458
|
+
"content": "ccc9f6d9ce8ba5699010c5b590fef6509d80ed928043b8747f5c52980d5c0596"
|
|
1459
1459
|
}
|
|
1460
1460
|
]
|
|
1461
1461
|
},
|
|
@@ -1465,7 +1465,7 @@
|
|
|
1465
1465
|
"hashes": [
|
|
1466
1466
|
{
|
|
1467
1467
|
"alg": "SHA-256",
|
|
1468
|
-
"content": "
|
|
1468
|
+
"content": "b8bfd4707746ca1ded26c2ccc9a4afbe5a9acf529b74e128dadced1631d1adde"
|
|
1469
1469
|
}
|
|
1470
1470
|
]
|
|
1471
1471
|
},
|
|
@@ -1635,7 +1635,7 @@
|
|
|
1635
1635
|
"hashes": [
|
|
1636
1636
|
{
|
|
1637
1637
|
"alg": "SHA-256",
|
|
1638
|
-
"content": "
|
|
1638
|
+
"content": "4eadcf12e18ccfbe77f857d71e52b29d51b35ee8bb9bbdf3a34cfb6724826c10"
|
|
1639
1639
|
}
|
|
1640
1640
|
]
|
|
1641
1641
|
},
|
|
@@ -1645,7 +1645,7 @@
|
|
|
1645
1645
|
"hashes": [
|
|
1646
1646
|
{
|
|
1647
1647
|
"alg": "SHA-256",
|
|
1648
|
-
"content": "
|
|
1648
|
+
"content": "de6cd5f7de860105804f100e487b65a78f2edd67a40645a710ad81c12e1c5b72"
|
|
1649
1649
|
}
|
|
1650
1650
|
]
|
|
1651
1651
|
},
|
|
@@ -1655,7 +1655,7 @@
|
|
|
1655
1655
|
"hashes": [
|
|
1656
1656
|
{
|
|
1657
1657
|
"alg": "SHA-256",
|
|
1658
|
-
"content": "
|
|
1658
|
+
"content": "bdb6933fa24b0e02ed293fe4e9c52910e2ed4f5652ea7bcb0dbbb7dc996cc224"
|
|
1659
1659
|
}
|
|
1660
1660
|
]
|
|
1661
1661
|
},
|
|
@@ -1665,7 +1665,7 @@
|
|
|
1665
1665
|
"hashes": [
|
|
1666
1666
|
{
|
|
1667
1667
|
"alg": "SHA-256",
|
|
1668
|
-
"content": "
|
|
1668
|
+
"content": "b47db4310de5fd7a586e795e10abe108e18cd164d1cb1862e67c227310a68001"
|
|
1669
1669
|
}
|
|
1670
1670
|
]
|
|
1671
1671
|
},
|
|
@@ -2555,7 +2555,7 @@
|
|
|
2555
2555
|
"hashes": [
|
|
2556
2556
|
{
|
|
2557
2557
|
"alg": "SHA-256",
|
|
2558
|
-
"content": "
|
|
2558
|
+
"content": "0531b606a3f4450741ec80efbcb9c32ff0a925496e85e4071cb395eeeeaf9dfb"
|
|
2559
2559
|
}
|
|
2560
2560
|
]
|
|
2561
2561
|
},
|
|
@@ -2565,7 +2565,7 @@
|
|
|
2565
2565
|
"hashes": [
|
|
2566
2566
|
{
|
|
2567
2567
|
"alg": "SHA-256",
|
|
2568
|
-
"content": "
|
|
2568
|
+
"content": "edefc472f25e03e14d27545cd3fbc0a78e59cee49e4cddc397d0f05321feac22"
|
|
2569
2569
|
}
|
|
2570
2570
|
]
|
|
2571
2571
|
},
|
|
@@ -2575,7 +2575,7 @@
|
|
|
2575
2575
|
"hashes": [
|
|
2576
2576
|
{
|
|
2577
2577
|
"alg": "SHA-256",
|
|
2578
|
-
"content": "
|
|
2578
|
+
"content": "a5656ebc2049c324ebe7707e1263beb16ceb0875ee078353203e346eb67fff42"
|
|
2579
2579
|
}
|
|
2580
2580
|
]
|
|
2581
2581
|
},
|
|
@@ -2585,7 +2585,7 @@
|
|
|
2585
2585
|
"hashes": [
|
|
2586
2586
|
{
|
|
2587
2587
|
"alg": "SHA-256",
|
|
2588
|
-
"content": "
|
|
2588
|
+
"content": "74f4d718f0ca68857448c043eb8f439c841604db64ee2d799ddec9f66dc15556"
|
|
2589
2589
|
}
|
|
2590
2590
|
]
|
|
2591
2591
|
},
|
|
@@ -3555,7 +3555,7 @@
|
|
|
3555
3555
|
"hashes": [
|
|
3556
3556
|
{
|
|
3557
3557
|
"alg": "SHA-256",
|
|
3558
|
-
"content": "
|
|
3558
|
+
"content": "01cc22fc645fc6656d6dd5d32d124a6d2ffe29130724a2142c3e12b84b891378"
|
|
3559
3559
|
}
|
|
3560
3560
|
]
|
|
3561
3561
|
},
|
|
@@ -3565,7 +3565,7 @@
|
|
|
3565
3565
|
"hashes": [
|
|
3566
3566
|
{
|
|
3567
3567
|
"alg": "SHA-256",
|
|
3568
|
-
"content": "
|
|
3568
|
+
"content": "c6ab4133ae6464684e14f619cd8175adec744bb775e9724bfd0808752a62692b"
|
|
3569
3569
|
}
|
|
3570
3570
|
]
|
|
3571
3571
|
},
|
|
@@ -3575,7 +3575,7 @@
|
|
|
3575
3575
|
"hashes": [
|
|
3576
3576
|
{
|
|
3577
3577
|
"alg": "SHA-256",
|
|
3578
|
-
"content": "
|
|
3578
|
+
"content": "dfa4e1ef2b211af0957cea622c6066891c34452b4f9092bc143350b150ff6708"
|
|
3579
3579
|
}
|
|
3580
3580
|
]
|
|
3581
3581
|
},
|
|
@@ -3585,7 +3585,47 @@
|
|
|
3585
3585
|
"hashes": [
|
|
3586
3586
|
{
|
|
3587
3587
|
"alg": "SHA-256",
|
|
3588
|
-
"content": "
|
|
3588
|
+
"content": "51ced5700c3efafbb1de270302a789578e80cf80d4859190f5ecdb646378a479"
|
|
3589
|
+
}
|
|
3590
|
+
]
|
|
3591
|
+
},
|
|
3592
|
+
{
|
|
3593
|
+
"type": "file",
|
|
3594
|
+
"name": "dist/skill-install-roots.d.ts",
|
|
3595
|
+
"hashes": [
|
|
3596
|
+
{
|
|
3597
|
+
"alg": "SHA-256",
|
|
3598
|
+
"content": "7aebdacaa5d1641e739628049a5bfcc94d5191bc62dfc70744906c579d0bf52c"
|
|
3599
|
+
}
|
|
3600
|
+
]
|
|
3601
|
+
},
|
|
3602
|
+
{
|
|
3603
|
+
"type": "file",
|
|
3604
|
+
"name": "dist/skill-install-roots.d.ts.map",
|
|
3605
|
+
"hashes": [
|
|
3606
|
+
{
|
|
3607
|
+
"alg": "SHA-256",
|
|
3608
|
+
"content": "948876ad8b0b1cab37d52957e0ffa69afd18a3154a6204f85159e748ed2c3830"
|
|
3609
|
+
}
|
|
3610
|
+
]
|
|
3611
|
+
},
|
|
3612
|
+
{
|
|
3613
|
+
"type": "file",
|
|
3614
|
+
"name": "dist/skill-install-roots.js",
|
|
3615
|
+
"hashes": [
|
|
3616
|
+
{
|
|
3617
|
+
"alg": "SHA-256",
|
|
3618
|
+
"content": "5e218e069af73918ab7d028ce7694640e0cc506f714608873c6b1138587bed1b"
|
|
3619
|
+
}
|
|
3620
|
+
]
|
|
3621
|
+
},
|
|
3622
|
+
{
|
|
3623
|
+
"type": "file",
|
|
3624
|
+
"name": "dist/skill-install-roots.js.map",
|
|
3625
|
+
"hashes": [
|
|
3626
|
+
{
|
|
3627
|
+
"alg": "SHA-256",
|
|
3628
|
+
"content": "6f46e1622b721bb009e7da347487af5a730279cdd26a712d34ed885c366bfebc"
|
|
3589
3629
|
}
|
|
3590
3630
|
]
|
|
3591
3631
|
},
|
|
@@ -3875,7 +3915,7 @@
|
|
|
3875
3915
|
"hashes": [
|
|
3876
3916
|
{
|
|
3877
3917
|
"alg": "SHA-256",
|
|
3878
|
-
"content": "
|
|
3918
|
+
"content": "f05456c49df6e2c4e5162a95b879a71fc2a89f509f2e1c94fd9cae926ef761f0"
|
|
3879
3919
|
}
|
|
3880
3920
|
]
|
|
3881
3921
|
},
|
|
@@ -3885,7 +3925,7 @@
|
|
|
3885
3925
|
"hashes": [
|
|
3886
3926
|
{
|
|
3887
3927
|
"alg": "SHA-256",
|
|
3888
|
-
"content": "
|
|
3928
|
+
"content": "4c2311420512b9e7d61a8eef321a0ed7704428b5b5daaaaba09ac09e2040dfa6"
|
|
3889
3929
|
}
|
|
3890
3930
|
]
|
|
3891
3931
|
},
|
|
@@ -3895,7 +3935,7 @@
|
|
|
3895
3935
|
"hashes": [
|
|
3896
3936
|
{
|
|
3897
3937
|
"alg": "SHA-256",
|
|
3898
|
-
"content": "
|
|
3938
|
+
"content": "c30c63f093e12f152b9d4eac12e1b3921d2f81a27cac062dd783e9a7d91663cf"
|
|
3899
3939
|
}
|
|
3900
3940
|
]
|
|
3901
3941
|
},
|
|
@@ -3905,7 +3945,7 @@
|
|
|
3905
3945
|
"hashes": [
|
|
3906
3946
|
{
|
|
3907
3947
|
"alg": "SHA-256",
|
|
3908
|
-
"content": "
|
|
3948
|
+
"content": "e698d6514ca71031acce40edf875cf0fd2c7acabf3ab336601abec26a0f0bd86"
|
|
3909
3949
|
}
|
|
3910
3950
|
]
|
|
3911
3951
|
},
|
|
@@ -4275,7 +4315,7 @@
|
|
|
4275
4315
|
"hashes": [
|
|
4276
4316
|
{
|
|
4277
4317
|
"alg": "SHA-256",
|
|
4278
|
-
"content": "
|
|
4318
|
+
"content": "18c9057f70d27a1c2183677a0e74a1e032a153b28d0d9292439ed294ab6104d9"
|
|
4279
4319
|
}
|
|
4280
4320
|
]
|
|
4281
4321
|
},
|
|
@@ -4635,7 +4675,7 @@
|
|
|
4635
4675
|
"hashes": [
|
|
4636
4676
|
{
|
|
4637
4677
|
"alg": "SHA-256",
|
|
4638
|
-
"content": "
|
|
4678
|
+
"content": "4a2d4c4c9c952521e0265fc62a01de3a7cd277c1c9eab8be252532bb72d0ae96"
|
|
4639
4679
|
}
|
|
4640
4680
|
]
|
|
4641
4681
|
},
|
|
@@ -4685,7 +4725,7 @@
|
|
|
4685
4725
|
"hashes": [
|
|
4686
4726
|
{
|
|
4687
4727
|
"alg": "SHA-256",
|
|
4688
|
-
"content": "
|
|
4728
|
+
"content": "18a9f1e7f84b56f0c7aa23b096a15c414418ecdd4fafc2f29d38d6dcbf761c84"
|
|
4689
4729
|
}
|
|
4690
4730
|
]
|
|
4691
4731
|
},
|
|
@@ -4915,7 +4955,7 @@
|
|
|
4915
4955
|
"hashes": [
|
|
4916
4956
|
{
|
|
4917
4957
|
"alg": "SHA-256",
|
|
4918
|
-
"content": "
|
|
4958
|
+
"content": "09930294d2b7fcd2938c15b8d8a62a9d1615c064a2479cae1918e0ea4d4ba045"
|
|
4919
4959
|
}
|
|
4920
4960
|
]
|
|
4921
4961
|
},
|
|
@@ -5155,7 +5195,17 @@
|
|
|
5155
5195
|
"hashes": [
|
|
5156
5196
|
{
|
|
5157
5197
|
"alg": "SHA-256",
|
|
5158
|
-
"content": "
|
|
5198
|
+
"content": "64814af9f30ed22e5b3e77e01c73b06995092ca54ac84d8cea327cd1b25402d3"
|
|
5199
|
+
}
|
|
5200
|
+
]
|
|
5201
|
+
},
|
|
5202
|
+
{
|
|
5203
|
+
"type": "file",
|
|
5204
|
+
"name": "src/skill-install-roots.ts",
|
|
5205
|
+
"hashes": [
|
|
5206
|
+
{
|
|
5207
|
+
"alg": "SHA-256",
|
|
5208
|
+
"content": "513710bca94206f9f50abfcf0bd934b315a5af97cad4f5f302b2559d5821848f"
|
|
5159
5209
|
}
|
|
5160
5210
|
]
|
|
5161
5211
|
},
|
|
@@ -5235,7 +5285,7 @@
|
|
|
5235
5285
|
"hashes": [
|
|
5236
5286
|
{
|
|
5237
5287
|
"alg": "SHA-256",
|
|
5238
|
-
"content": "
|
|
5288
|
+
"content": "003d420be10bd0dd61eb6fe5c724d5de6d0c02b72e37bbae5a57c269545e3abb"
|
|
5239
5289
|
}
|
|
5240
5290
|
]
|
|
5241
5291
|
},
|
|
@@ -2029,7 +2029,12 @@ export function planCompletenessGateCmd(repo: string, featureDir: string, tier?:
|
|
|
2029
2029
|
// and the tried paths live OUTSIDE the verdict line so no path can smuggle a second verdict word
|
|
2030
2030
|
// into it.
|
|
2031
2031
|
'echo "K2_GATE_SCRIPT=${GS:-none}"',
|
|
2032
|
-
'echo "K2_GATE_TRIED=${C1
|
|
2032
|
+
'echo "K2_GATE_TRIED=C1(args.gateScript)=${C1:-<unset>} | C2(workspace)=$C2 | C3(target-repo)=$C3"',
|
|
2033
|
+
// A COLLAPSE is not a second candidate. When the workspace was not pinned, WS falls back to the
|
|
2034
|
+
// gate agent own cwd — in the field that WAS the target repo, so C2 and C3 printed the same path
|
|
2035
|
+
// twice and the chain silently degenerated from three candidates to two. Saying so turns a
|
|
2036
|
+
// puzzling duplicate into an instruction. Not verdict-shaped, so the parser anchoring is untouched.
|
|
2037
|
+
'[ "$C2" = "$C3" ] && echo "K2_GATE_NOTE=the workspace candidate resolved to the TARGET repo (WS==repo), so only two distinct candidates were tried; pass args.workspace or args.gateScript when the feature-adr skill is installed outside the target repo"',
|
|
2033
2038
|
'if [ -z "$GS" ]; then echo "K2 plan-completeness: NOT-ESTABLISHED — tooling-missing: no gate script at any candidate on the K2_GATE_TRIED line above"; echo "K2_EXIT=3"; else cd ' + q(repo) + ' && node "$GS" ' + q(featureDir) + t + ' 2>&1; echo "K2_EXIT=$?"; fi',
|
|
2034
2039
|
].join('\n')
|
|
2035
2040
|
}
|
package/src/index.ts
CHANGED
|
@@ -76,6 +76,7 @@ export type { CreateSkillOptions, CreateSkillResult } from './create-skill.js';
|
|
|
76
76
|
export { checkUpstream, checkAllUpstream, discoverSourcePackages, loadSourcesManifest } from './sync-upstream.js';
|
|
77
77
|
export type { SyncUpstreamReport, UpstreamCheckResult, SourcesManifest, SourcePackageInfo } from './sync-upstream.js';
|
|
78
78
|
export { sweepSkillDrift, syncCanonicalSkill } from './skill-drift.js';
|
|
79
|
+
export { SKILL_INSTALL_ROOTS, SKILL_INSTALL_ROOT_BY_TARGET, DEV_SKILL_ROOT, TARGET_ENRICHMENT_ASSETS } from './skill-install-roots.js';
|
|
79
80
|
export type { SweepResult, DriftedSkill, SyncResult, SyncCanonicalOptions } from './skill-drift.js';
|
|
80
81
|
export { benchmarkSkill, benchmarkSkills, compareSkills } from './benchmark.js';
|
|
81
82
|
export { buildRegistry, searchRegistry, filterByCategory, skillPackBaseDirs, discoverSkillPackDirs, discoverVerifiablePackDirs } from './registry.js';
|
package/src/provenance.ts
CHANGED
|
@@ -25,6 +25,12 @@ export type SourceVerdict =
|
|
|
25
25
|
| 'no-source'
|
|
26
26
|
/** The manifest did not declare what kind of source this is — never inferred from its shape. */
|
|
27
27
|
| 'unknown-kind'
|
|
28
|
+
/** A PUBLIC external web URL (http/https). It is already public by construction, so it cannot
|
|
29
|
+
* "leave this machine" — nothing local to protect. Refused only if the URL is malformed. */
|
|
30
|
+
| 'public-url'
|
|
31
|
+
/** A `url` claim whose value is not a well-formed http(s) URL — a scheme this gate will not clear
|
|
32
|
+
* (a file://, a bare word, or a non-URL) must not pass as a public web source. */
|
|
33
|
+
| 'malformed-url'
|
|
28
34
|
/** A store record that the TRACKED public list does not name. Default-deny. */
|
|
29
35
|
| 'not-marked-public'
|
|
30
36
|
/** The path does not resolve. You cannot cite what does not exist. */
|
|
@@ -164,6 +170,18 @@ export function classifySource(claim: SourceClaim, facts: SourceProvenanceFacts)
|
|
|
164
170
|
return at('allowed', 'committed, reviewed, and not refused by the owner\'s boundary');
|
|
165
171
|
}
|
|
166
172
|
|
|
173
|
+
if (claim.kind === 'url') {
|
|
174
|
+
// The channel translates OTHERS' public tweets/posts; their sources are PUBLIC web URLs. Such a
|
|
175
|
+
// URL is already public — it cannot leak off this machine, so there is no local secret to
|
|
176
|
+
// protect (the whole point of the path/record checks). It is cleared iff it is a well-formed
|
|
177
|
+
// http(s) URL; anything else (file://, a bare path, a non-URL) is refused, never inferred.
|
|
178
|
+
let ok = false;
|
|
179
|
+
try { const u = new URL(source); ok = u.protocol === 'http:' || u.protocol === 'https:'; } catch { ok = false; }
|
|
180
|
+
return ok
|
|
181
|
+
? at('public-url', 'a public http(s) web source — already public, nothing local to protect')
|
|
182
|
+
: at('malformed-url', 'kind "url" but the value is not a well-formed http(s) URL — a public web source must be a real http(s) URL');
|
|
183
|
+
}
|
|
184
|
+
|
|
167
185
|
return at('unknown-kind', `the manifest declares kind ${JSON.stringify(claim.kind ?? null)} — a kind this gate cannot check is refused, never inferred from the path's shape`);
|
|
168
186
|
}
|
|
169
187
|
|
|
@@ -187,7 +205,11 @@ export function decideSourceProvenance(manifest: SourceManifest | null, facts: S
|
|
|
187
205
|
return { outcome: 'not-established', exit: 3, claims: [], reason: 'the manifest lists no claims — a draft with nothing to check has not been shown to be safe, only left unchecked' };
|
|
188
206
|
}
|
|
189
207
|
const claims = manifest.claims.map((c) => classifySource(c, facts));
|
|
190
|
-
|
|
208
|
+
// A claim passes iff its verdict is a CLEARED one: a repo-internal source proven safe (allowed),
|
|
209
|
+
// or a public web URL that is already public by construction (public-url — the tg-post channel
|
|
210
|
+
// case). Every other verdict is a refusal.
|
|
211
|
+
const CLEARED = new Set(['allowed', 'public-url']);
|
|
212
|
+
const blocked = claims.filter((c) => !CLEARED.has(c.verdict));
|
|
191
213
|
if (blocked.length > 0) {
|
|
192
214
|
return {
|
|
193
215
|
outcome: 'blocked',
|
package/src/skill-drift.ts
CHANGED
|
@@ -21,9 +21,11 @@
|
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
23
|
import { readdirSync, statSync, readFileSync, writeFileSync, mkdirSync, rmSync, existsSync } from 'node:fs';
|
|
24
|
-
import { join, relative, resolve, dirname, basename } from 'node:path';
|
|
24
|
+
import { join, relative, resolve, dirname, basename, sep } from 'node:path';
|
|
25
25
|
import { createHash } from 'node:crypto';
|
|
26
26
|
|
|
27
|
+
import { DEV_SKILL_ROOT, SKILL_INSTALL_ROOTS, TARGET_ENRICHMENT_ASSETS } from './skill-install-roots.js';
|
|
28
|
+
|
|
27
29
|
/** One shared skill whose copies byte-differ (`driftFiles > 0`). */
|
|
28
30
|
export interface DriftedSkill {
|
|
29
31
|
/** Skill dir basename (e.g. `goap-research-ed25519`). */
|
|
@@ -48,9 +50,15 @@ export interface SweepOptions {
|
|
|
48
50
|
* `.claude/skills/<skill>` dev copies are excluded — the repo's own `dz sync` test treats them as
|
|
49
51
|
* "legitimately lagging" the published version, so counting them makes the gate red-on-arrival.
|
|
50
52
|
* The dangerous drift (goap, brutal-honesty) was always between PUBLISHED packages.
|
|
51
|
-
* - `'
|
|
53
|
+
* - `'installs'` (what the `no-skill-drift` HARD rule uses): packages + every per-target install
|
|
54
|
+
* root EXCEPT {@link DEV_SKILL_ROOT}. Machine-generated installs have no licence to lag, so
|
|
55
|
+
* holding them to byte-identity is a gate that can actually be satisfied — unlike `'all'`,
|
|
56
|
+
* which includes the hand-edited dev tree and is therefore red-on-arrival as a gate.
|
|
57
|
+
* - `'all'`: packages + EVERY per-target skills install root ({@link SKILL_INSTALL_ROOTS}) — the
|
|
58
|
+
* raw sweep the audit script does. A root that is absent, or present without a `SKILL.md`,
|
|
59
|
+
* contributes nothing, so this is inert for a repo that installs only one target.
|
|
52
60
|
*/
|
|
53
|
-
readonly scope?: 'packages' | 'all';
|
|
61
|
+
readonly scope?: 'packages' | 'installs' | 'all';
|
|
54
62
|
/** Skill basenames whose drift is ACCEPTED (documented intentional forks) — reported separately, never counted as gate drift. */
|
|
55
63
|
readonly allowlist?: readonly string[];
|
|
56
64
|
}
|
|
@@ -144,9 +152,42 @@ function walk(dir: string): string[] {
|
|
|
144
152
|
* so BOTH the CI sweep AND the canonical-free `sync-canonical --check` share one implementation and
|
|
145
153
|
* yield the same verdict. Behavior-preserving refactor — no numbers change.
|
|
146
154
|
*/
|
|
155
|
+
/**
|
|
156
|
+
* Is this skill-dir-relative path a TARGET ENRICHMENT asset ({@link TARGET_ENRICHMENT_ASSETS})?
|
|
157
|
+
*
|
|
158
|
+
* Such a file exists in ONE copy by design — `dz init --enrich` writes it into the install tree and
|
|
159
|
+
* the canonical never has it. Counting it as drift makes the gate unsatisfiable; removing it during
|
|
160
|
+
* a heal destroys valid output. Compared with POSIX separators so a Windows `relative()` still matches.
|
|
161
|
+
*/
|
|
162
|
+
function isEnrichmentAsset(rel: string, copyDir: string): boolean {
|
|
163
|
+
const owner = TARGET_ENRICHMENT_ASSETS[rel.split(sep).join('/')];
|
|
164
|
+
if (owner === undefined) return false;
|
|
165
|
+
// The exemption is only valid inside the root that OWNS the asset. Elsewhere the same filename is
|
|
166
|
+
// a misplaced extra file, and waving it through would make the sweep report clean while the healer
|
|
167
|
+
// preserved it. ANCHORED at the skill dir, not searched across the whole absolute path: a repo that
|
|
168
|
+
// itself lives under a directory containing `/.agents/skills/` would otherwise have every copy in
|
|
169
|
+
// it — packages included — classified as codex-owned (cross-family review, 2026-08-25).
|
|
170
|
+
const posix = copyDir.split(sep).join('/');
|
|
171
|
+
return posix.endsWith('/' + owner + '/' + posix.split('/').slice(-1)[0]);
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* The canonical's own file list, with every enrichment-asset NAME removed regardless of where the
|
|
176
|
+
* canonical came from. A canonical picked by `--auto` (or handed in with `--from`) can itself be an
|
|
177
|
+
* enriched install copy; propagating its target-specific metadata into the package and other target
|
|
178
|
+
* copies is never correct, and the exemption above would then stop it ever being cleaned up.
|
|
179
|
+
*/
|
|
180
|
+
function withoutEnrichmentNames(rels: readonly string[]): string[] {
|
|
181
|
+
return rels.filter((r) => TARGET_ENRICHMENT_ASSETS[r.split(sep).join('/')] === undefined);
|
|
182
|
+
}
|
|
183
|
+
|
|
147
184
|
function comparePeers(copies: readonly string[]): { driftFiles: number; missingFiles: number; totalFiles: number } {
|
|
148
185
|
const relFiles = new Set<string>();
|
|
149
|
-
for (const c of copies) for (const f of walk(c))
|
|
186
|
+
for (const c of copies) for (const f of walk(c)) {
|
|
187
|
+
const rel = relative(c, f);
|
|
188
|
+
if (isEnrichmentAsset(rel, c)) continue;
|
|
189
|
+
relFiles.add(rel);
|
|
190
|
+
}
|
|
150
191
|
|
|
151
192
|
let driftFiles = 0;
|
|
152
193
|
let missingFiles = 0;
|
|
@@ -172,7 +213,10 @@ function comparePeers(copies: readonly string[]): { driftFiles: number; missingF
|
|
|
172
213
|
* hence it is only ever reached behind an explicit `--auto` opt-in plus a loud warning.
|
|
173
214
|
*/
|
|
174
215
|
function pickMostComplete(copies: readonly string[]): string {
|
|
175
|
-
|
|
216
|
+
// Enrichment assets do not make a copy more COMPLETE — they make it a target install. Counting
|
|
217
|
+
// them would let an enriched copy win the heuristic on files no other copy is supposed to have.
|
|
218
|
+
const size = (d: string): number => withoutEnrichmentNames(walk(d).map((p) => relative(d, p))).length;
|
|
219
|
+
return [...copies].sort().reduce((best, c) => (size(c) > size(best) ? c : best));
|
|
176
220
|
}
|
|
177
221
|
|
|
178
222
|
/**
|
|
@@ -198,12 +242,24 @@ function resolveCanonical(
|
|
|
198
242
|
}
|
|
199
243
|
|
|
200
244
|
/**
|
|
201
|
-
* Every skill dir (a dir containing `SKILL.md`) under `packages/` +
|
|
202
|
-
* `node_modules` / `__pycache__`.
|
|
245
|
+
* Every skill dir (a dir containing `SKILL.md`) under `packages/` + every per-target install root
|
|
246
|
+
* in {@link SKILL_INSTALL_ROOTS}, excluding `node_modules` / `__pycache__`. Originally ported from
|
|
247
|
+
* `scripts/drift-sweep-skills.mjs`, which searched `.claude/skills` alone — see ADR-001
|
|
248
|
+
* (skill-copy-discovery) for why one hardcoded root let the Codex install drift unseen.
|
|
203
249
|
*/
|
|
204
|
-
function findSkillDirs(root: string, scope: 'packages' | 'all' = 'all'): string[] {
|
|
250
|
+
function findSkillDirs(root: string, scope: 'packages' | 'installs' | 'all' = 'all'): string[] {
|
|
205
251
|
const dirs: string[] = [];
|
|
206
|
-
|
|
252
|
+
// Roots are ANCHORED at the repo root — never a recursive search for `*/skills`. A stale agent
|
|
253
|
+
// worktree under `.claude/worktrees/<id>/` holds a full second copy of `packages/`,
|
|
254
|
+
// `.claude/skills` AND `.agents/skills`; a recursive sweep would report every skill in the repo
|
|
255
|
+
// as drifting against a checkout nobody maintains, and the healer would be entitled to WRITE
|
|
256
|
+
// into it (ADR-001, Option C rejected on exactly this measurement).
|
|
257
|
+
const installRoots = scope === 'installs'
|
|
258
|
+
? SKILL_INSTALL_ROOTS.filter((r) => r !== DEV_SKILL_ROOT)
|
|
259
|
+
: SKILL_INSTALL_ROOTS;
|
|
260
|
+
const roots = scope === 'packages'
|
|
261
|
+
? [join(root, 'packages')]
|
|
262
|
+
: [join(root, 'packages'), ...installRoots.map((r) => join(root, ...r.split('/')))];
|
|
207
263
|
const stack = roots.filter((p) => existsSync(p));
|
|
208
264
|
while (stack.length) {
|
|
209
265
|
const d = stack.pop() as string;
|
|
@@ -322,9 +378,7 @@ export function syncCanonicalSkill(root: string, skill: string, opts: SyncCanoni
|
|
|
322
378
|
return { canonical: '', canonicalExists: false, resolvedFrom, copies: allCopies.length, synced: 0, drifted: 0, wrote: [] };
|
|
323
379
|
}
|
|
324
380
|
|
|
325
|
-
const canonFiles = walk(canonical)
|
|
326
|
-
.map((p) => relative(canonical, p))
|
|
327
|
-
.sort();
|
|
381
|
+
const canonFiles = withoutEnrichmentNames(walk(canonical).map((p) => relative(canonical, p))).sort();
|
|
328
382
|
|
|
329
383
|
// Every <skill>/ dir except the canonical itself.
|
|
330
384
|
const copies = allCopies.filter((d) => relative(canonical, d) !== '');
|
|
@@ -334,7 +388,10 @@ export function syncCanonicalSkill(root: string, skill: string, opts: SyncCanoni
|
|
|
334
388
|
const wrote: string[] = [];
|
|
335
389
|
|
|
336
390
|
for (const copy of copies) {
|
|
337
|
-
|
|
391
|
+
// Target ENRICHMENT assets are excluded from the copy's file set entirely: they exist in the
|
|
392
|
+
// install tree by design, the canonical never has them, and both the drift verdict and the
|
|
393
|
+
// removal pass below must leave them alone.
|
|
394
|
+
const copyFiles = new Set(walk(copy).map((p) => relative(copy, p)).filter((f) => !isEnrichmentAsset(f, copy)));
|
|
338
395
|
let differs = false;
|
|
339
396
|
// Extra files in the copy not present in canonical ⇒ drift.
|
|
340
397
|
for (const f of copyFiles) if (!canonFiles.includes(f)) differs = true;
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where installed skill dirs live — the single source of truth for skill-copy DISCOVERY.
|
|
3
|
+
*
|
|
4
|
+
* The harness installs skills into a per-TARGET tree (`dz install --target <name>`). Five of the
|
|
5
|
+
* ten targets in {@link TARGET_NAMES} emit an agentskills.io-shaped directory containing a
|
|
6
|
+
* `SKILL.md`; the other five (copilot, cursor, windsurf, gemini, agents-md) emit rules/instructions
|
|
7
|
+
* FILES and therefore hold no skill dirs to keep in sync.
|
|
8
|
+
*
|
|
9
|
+
* | Target | Root | Adapter constant |
|
|
10
|
+
* |---|---|---|
|
|
11
|
+
* | `claude-code` | `.claude/skills` | — (the adapter exports no root constant) |
|
|
12
|
+
* | `codex` | `.agents/skills` | `CODEX_SKILLS_ROOT` |
|
|
13
|
+
* | `opencode` | `.opencode/skills` | `OPENCODE_SKILLS_ROOT` |
|
|
14
|
+
* | `hermes` | `.hermes/skills` | `HERMES_SKILLS_ROOT` |
|
|
15
|
+
* | `openclaude` | `.openclaude/skills` | `OPENCLAUDE_SKILLS_ROOT` |
|
|
16
|
+
*
|
|
17
|
+
* **Why this list is data with no imports.** `skill-drift.ts` — the drift detector behind the
|
|
18
|
+
* `no-skill-drift` HARD guard rule — is deliberately dependency-free (`node:fs`/`node:path`/
|
|
19
|
+
* `node:crypto`). `targets.ts`, which owns the adapter registry, imports all ten adapter packages;
|
|
20
|
+
* importing it from the drift sweep would drag ten packages into the guard's load path for a list
|
|
21
|
+
* of five strings. So the list lives here as data, and the link back to the adapters is asserted in
|
|
22
|
+
* `test/skill-install-roots.test.ts`, where importing them costs nothing.
|
|
23
|
+
*
|
|
24
|
+
* **Why this matters (the defect that produced it, MEASURED 2026-08-25).** `findSkillDirs` searched
|
|
25
|
+
* `packages/` + `.claude/skills` only. `.agents/skills/feature-adr` — the Codex install — drifted
|
|
26
|
+
* from the canonical for a day while `dz sync-canonical feature-adr --check` reported "all 3 copies
|
|
27
|
+
* match canonical" and the HARD guard passed. Two feature-adr gates were degraded in that copy: the
|
|
28
|
+
* K1 name-availability section was missing from `SKILL.md`, and the C6 amendment-integrity check
|
|
29
|
+
* was missing from the K2 script. A gate that cannot see a copy cannot gate it.
|
|
30
|
+
*
|
|
31
|
+
* **Honest limit.** A target whose adapter exports no root constant (today: `claude-code`) must be
|
|
32
|
+
* listed here by hand — the completeness test proves every EXPORTED constant is covered, and cannot
|
|
33
|
+
* prove a constant that does not exist is covered.
|
|
34
|
+
*
|
|
35
|
+
* @packageDocumentation
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Files a TARGET legitimately adds INSIDE an installed skill dir — enrichment, not drift.
|
|
40
|
+
*
|
|
41
|
+
* `dz init --enrich` / `dz setup --enrich` write per-target metadata into the skill dir itself
|
|
42
|
+
* (`operations.ts`): codex gets `<skillDir>/agents/openai.yaml` (UI metadata + risk scoring),
|
|
43
|
+
* hermes gets `<skillDir>/hermes-config.yaml`. OpenCode's enrichment lands in
|
|
44
|
+
* `.opencode/agents/<id>.md`, OUTSIDE any skill dir, so it needs no entry here.
|
|
45
|
+
*
|
|
46
|
+
* These paths must be invisible to the drift comparison and untouchable by the healer. Without the
|
|
47
|
+
* exemption, widening discovery to install roots would (a) report permanent, unfixable drift on any
|
|
48
|
+
* enriched install — making the HARD gate unsatisfiable — and (b) worse, let `dz sync-canonical`
|
|
49
|
+
* DELETE the enrichment, because the heal removes every file the canonical does not have. Found by
|
|
50
|
+
* cross-family review (Codex `gpt-5.6-sol`, 2026-08-25) on a latent defect: this repo's own
|
|
51
|
+
* `.agents/` install was never enriched, so no test and no live run would have shown it.
|
|
52
|
+
*
|
|
53
|
+
* Each asset is mapped to the install root that OWNS it, because the exemption must depend on WHERE
|
|
54
|
+
* the file sits, not only on its name: `agents/openai.yaml` under a package copy, or
|
|
55
|
+
* `hermes-config.yaml` outside the hermes root, is a misplaced file — waving it through would let
|
|
56
|
+
* the sweep report clean and the healer preserve it (second cross-family round, 2026-08-25).
|
|
57
|
+
*
|
|
58
|
+
* Keys are relative to the skill dir root and POSIX-separated; values are repo-root-relative install
|
|
59
|
+
* roots from {@link SKILL_INSTALL_ROOT_BY_TARGET}.
|
|
60
|
+
*/
|
|
61
|
+
export const TARGET_ENRICHMENT_ASSETS: Readonly<Record<string, string>> = {
|
|
62
|
+
'agents/openai.yaml': '.agents/skills',
|
|
63
|
+
'hermes-config.yaml': '.hermes/skills',
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* The one install root that is ALSO the repo's hand-edited development tree.
|
|
68
|
+
*
|
|
69
|
+
* `.claude/skills` is where this repo's own skills are authored before they are synced into
|
|
70
|
+
* `packages/`, so it legitimately LAGS the published copies — MEASURED 2026-08-25, `dz drift-check
|
|
71
|
+
* --all` reports three skills drifting for exactly that reason (`decision-mockups` 12/22 files,
|
|
72
|
+
* `idea2prd-manual` 3/11, `observability` 1/5). A byte-identity GATE that included it would be
|
|
73
|
+
* red-on-arrival, which is why `scope: 'packages'` exists at all.
|
|
74
|
+
*
|
|
75
|
+
* Every OTHER root in {@link SKILL_INSTALL_ROOTS} is machine-generated by `dz install --target …`
|
|
76
|
+
* and has no licence to lag: a stale generated copy is the defect this module was written for.
|
|
77
|
+
* That asymmetry is what `scope: 'installs'` encodes.
|
|
78
|
+
*/
|
|
79
|
+
export const DEV_SKILL_ROOT = '.claude/skills';
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Every `--target` name → the repo-root-relative dir into which it installs `SKILL.md`-bearing
|
|
83
|
+
* skill dirs, or `null` for a target that emits rules/instructions FILES and therefore holds no
|
|
84
|
+
* skill dirs to keep in sync.
|
|
85
|
+
*
|
|
86
|
+
* **This map, not the derived list, is the thing a new target must touch.** Its keys are asserted
|
|
87
|
+
* against `TARGET_NAMES` in `test/skill-install-roots.test.ts`, so adding target #11 turns that
|
|
88
|
+
* test RED until someone states whether it installs skill dirs — which is exactly the question
|
|
89
|
+
* nobody was asked when `codex` was added and its install tree went ungated. A test that
|
|
90
|
+
* hand-enumerated the adapter constants would have stayed green through that (raised by
|
|
91
|
+
* cross-family review, Codex `gpt-5.6-sol`, 2026-08-25).
|
|
92
|
+
*
|
|
93
|
+
* Kept as a literal with NO imports: `skill-drift.ts` consumes it and is documented dependency-free,
|
|
94
|
+
* while `targets.ts` — which owns the adapter registry — pulls in all ten adapter packages.
|
|
95
|
+
*/
|
|
96
|
+
export const SKILL_INSTALL_ROOT_BY_TARGET: Readonly<Record<string, string | null>> = {
|
|
97
|
+
'claude-code': '.claude/skills',
|
|
98
|
+
codex: '.agents/skills',
|
|
99
|
+
opencode: '.opencode/skills',
|
|
100
|
+
hermes: '.hermes/skills',
|
|
101
|
+
openclaude: '.openclaude/skills',
|
|
102
|
+
// Rules/instructions FILE targets — no skill dirs, nothing to compare.
|
|
103
|
+
copilot: null,
|
|
104
|
+
'agents-md': null,
|
|
105
|
+
cursor: null,
|
|
106
|
+
gemini: null,
|
|
107
|
+
windsurf: null,
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Repo-root-relative directories that hold installed `SKILL.md`-bearing skill dirs — DERIVED from
|
|
112
|
+
* {@link SKILL_INSTALL_ROOT_BY_TARGET} so the two can never disagree. POSIX separators; consumers
|
|
113
|
+
* `join()` them onto the repo root themselves.
|
|
114
|
+
*
|
|
115
|
+
* Consumed by `findSkillDirs` (`skill-drift.ts`). A root that does not exist, or exists without any
|
|
116
|
+
* `SKILL.md`, contributes nothing — so listing a root the current repo does not use is inert.
|
|
117
|
+
*/
|
|
118
|
+
export const SKILL_INSTALL_ROOTS: readonly string[] = Object.values(SKILL_INSTALL_ROOT_BY_TARGET)
|
|
119
|
+
.filter((r): r is string => r !== null);
|