@rhize/skill-forge 0.12.0 → 0.13.0
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/README.md +45 -4
- package/dist/cli.js +165 -35
- package/dist/cli.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -172,7 +172,18 @@ target skills root until you decide.
|
|
|
172
172
|
| `-t, --target <dir>` | Skills root to promote into. Defaults to the config's `defaultTarget`, then `skillsRoots[0]`. |
|
|
173
173
|
| `--json` | Prints the gate result (profile, safety findings, overlap) as JSON instead of the terminal report box. Implies non-interactive: the decision is made the same way `--yes` makes it (verdict decides promote/hold/reject), never an interactive prompt. |
|
|
174
174
|
| `--ingest` | Pro (free during the 0.x beta). After a successful promote, hands off to a coding agent — see [Ingestion handoff](#ingestion-handoff---ingest). |
|
|
175
|
-
| `--skill-map <path>` | Path to a generated `rhize-plugins` skill map (Phase 4 of that repo's skill-map-graph-substrate plan). When given, ranks the candidate's name/description against every `skill` node in the map — a near-duplicate of an already-shipped marketplace skill is folded into the safety findings as a `HIGH`-severity finding (escalates the verdict to `block`, so it's held rather than silently promoted); a moderate overlap is `MEDIUM` (`warn`). Free (not Pro-gated) — this enforces the marketplace's own curation rule ("close the gap, don't duplicate"), not a premium ranking feature. Missing/unreadable map: printed to stderr, never fatal. |
|
|
175
|
+
| `--skill-map <path>` | Path to a generated `rhize-plugins` skill map (Phase 4 of that repo's skill-map-graph-substrate plan). When given, ranks the candidate's name/description against every `skill` node in the map — a near-duplicate of an already-shipped marketplace skill is folded into the safety findings as a `HIGH`-severity finding (escalates the verdict to `block`, so it's held rather than silently promoted); a moderate overlap is `MEDIUM` (`warn`). Free (not Pro-gated) — this enforces the marketplace's own curation rule ("close the gap, don't duplicate"), not a premium ranking feature. Missing/unreadable map: printed to stderr, never fatal. **Two map variants exist, with different coverage**: the **static** map (`rhize-plugins/generated/skill-map.static.json`) is first-party-only — it only knows about that marketplace's own skills, so it cannot catch a candidate that duplicates an *installed third-party plugin's* skill. The **resolved** map (`~/.claude/context-manager/skill-map.resolved.json`, produced by `rhize-context-manager`) additionally carries the full third-party ecosystem inventory. Point `--skill-map` at the resolved map when you want ecosystem-wide duplicate detection; the static map is enough only when you're curating a single first-party marketplace against itself. |
|
|
176
|
+
|
|
177
|
+
**Bugfix (v0.13.0):** `--skill-map` previously matched nothing against either real map variant — the loader read a `type` field that the real generated artifact never sets (it uses `kind`), so every node was silently invisible to the overlap check. `loadSkillMap()` now normalizes `kind` → `type` at load time; regression coverage lives in `test/mapOverlap.realmap.test.ts` against vendored real-map snapshots. If you were relying on `--skill-map` before v0.13.0, it was a no-op — re-run `add`/`scan` on anything you're unsure about.
|
|
178
|
+
|
|
179
|
+
**Extends-declared overlap exemption**: a candidate can declare `metadata.rhize.extends:
|
|
180
|
+
["<skill-name>" | "<plugin>/<skill-name>"]` in its SKILL.md frontmatter to mark itself a
|
|
181
|
+
deliberate specialization/layering of an existing map skill. When the top `--skill-map` match is
|
|
182
|
+
a skill the candidate declares it extends, that finding is downgraded from a HOLD-level safety
|
|
183
|
+
finding to an informational notice printed above the report — it never escalates the verdict.
|
|
184
|
+
Overlap with any *other* skill (a different match, or a second candidate/skill pair) still holds
|
|
185
|
+
at full severity: the exemption applies per declared pair, not globally, so declaring an
|
|
186
|
+
extension of skill X never waives overlap with skill Y.
|
|
176
187
|
|
|
177
188
|
A promoted skill gets a provenance entry appended to `<target>/SOURCES.md`, and every promote or
|
|
178
189
|
hold decision is recorded to `~/.skill-forge/queue.json` (or `$SKILL_FORGE_HOME/queue.json`) — see
|
|
@@ -478,11 +489,41 @@ It is only ever printed, sanitized, as a suggestion for you (or an agent) to run
|
|
|
478
489
|
**`--skill-map <path>` (Phase 4 of `rhize-plugins`' skill-map-graph-substrate plan)** additionally
|
|
479
490
|
drift-checks every `fork-of` edge in a generated skill map: for each edge it resolves the local
|
|
480
491
|
`skill` node's `path`/`contentHash` and the upstream (`external`) node's `url`/`path`, fetches or
|
|
481
|
-
reads the upstream content, and compares content hashes.
|
|
482
|
-
`drifted`, `upstream-unreachable`, or `local-missing`. Same never-execute posture as the ledger
|
|
492
|
+
reads the upstream content, and compares content hashes. Same never-execute posture as the ledger
|
|
483
493
|
check above: nothing derived from the map — including a fork-of edge's own `driftCheck`
|
|
484
494
|
metadata — is ever executed; only fetch/read/hash/compare. A missing/unreadable map is a warning,
|
|
485
|
-
never fatal.
|
|
495
|
+
never fatal. A node's repo-relative `path` (e.g. `rhize-context-manager/skills/x/SKILL.md`) is
|
|
496
|
+
resolved against cwd, the map's own directory, and that directory's parent (in that order) — not
|
|
497
|
+
cwd alone — so `watch --skill-map <path>` gives correct verdicts regardless of the directory you
|
|
498
|
+
run it from.
|
|
499
|
+
|
|
500
|
+
**Verdict**, per fork-of edge — three-way comparison, four verdict states when both hashes below
|
|
501
|
+
are present on the map, else the older two-way fallback (`in-sync`/`drifted`, `contentHash` vs
|
|
502
|
+
freshly-fetched upstream):
|
|
503
|
+
|
|
504
|
+
| local-normalized vs baseline | upstream-now vs baseline | status | actionable |
|
|
505
|
+
|---|---|---|---|
|
|
506
|
+
| == | == | `in-sync` | no |
|
|
507
|
+
| != | == | `local-only` | no (deliberate fork divergence, e.g. Rhize's added frontmatter) |
|
|
508
|
+
| == | != | `upstream-moved` | yes |
|
|
509
|
+
| != | != | `diverged` | yes |
|
|
510
|
+
|
|
511
|
+
The three-way matrix fires only when the local `skill` node carries `contentHashNormalized` (a
|
|
512
|
+
hash of the file with Rhize-injected frontmatter stripped, computed once by the rhize-plugins
|
|
513
|
+
compiler) **and** the upstream `external` node carries `baselineHash` (the upstream content hash
|
|
514
|
+
as of the last human review, recorded in `rhize-plugins`' SOURCES.md and copied onto the node by
|
|
515
|
+
that compiler — skill-forge never computes or fetches it itself). Either field missing on a given
|
|
516
|
+
edge falls back to the two-way compare for that edge, so older maps keep working. All four
|
|
517
|
+
three-way verdicts — `in-sync`, `local-only`, `upstream-moved`, and `diverged` — carry
|
|
518
|
+
`baselineHash`/`upstreamHash` in the JSON output; the two-way-fallback rows and
|
|
519
|
+
`upstream-unreachable`/`local-missing` never do (no verdict without a successful fetch/read, so
|
|
520
|
+
there is nothing to compare against a baseline). Every row also carries an `actionable` boolean
|
|
521
|
+
(from `isActionable`), so callers don't have to re-derive "needs attention" from `status`/`detail`
|
|
522
|
+
prose — it's `true` for `drifted`, `upstream-moved`, `diverged`, `upstream-unreachable`, and
|
|
523
|
+
`local-missing`; `false` for `in-sync` and `local-only`. **Re-baselining**: after reviewing and
|
|
524
|
+
adopting an `upstream-moved`/`diverged` change, re-run `rhize-plugins`' `scripts/baseline_upstreams.py`
|
|
525
|
+
and commit the updated SOURCES.md — that's the "I reviewed upstream, accept its state" action that
|
|
526
|
+
clears the row back to `in-sync`/`local-only`.
|
|
486
527
|
|
|
487
528
|
### `ingest` + `queue close` (v0.9, Pro)
|
|
488
529
|
|
package/dist/cli.js
CHANGED
|
@@ -3131,6 +3131,33 @@ function extractDescription(block) {
|
|
|
3131
3131
|
}
|
|
3132
3132
|
return stripQuotes(first);
|
|
3133
3133
|
}
|
|
3134
|
+
function extractExtends(block) {
|
|
3135
|
+
const lines = block.split("\n");
|
|
3136
|
+
const idx = lines.findIndex((l) => /^\s*extends:\s*(.*)$/.test(l));
|
|
3137
|
+
if (idx === -1) return void 0;
|
|
3138
|
+
const inline = lines[idx].match(/^\s*extends:\s*(.*)$/)[1].trim();
|
|
3139
|
+
if (inline.startsWith("[")) {
|
|
3140
|
+
const inner = inline.replace(/^\[/, "").replace(/\]\s*$/, "");
|
|
3141
|
+
const items = inner.split(",").map((s) => stripQuotes(s.trim())).filter(Boolean);
|
|
3142
|
+
return items.length ? items : void 0;
|
|
3143
|
+
}
|
|
3144
|
+
if (inline === "") {
|
|
3145
|
+
const items = [];
|
|
3146
|
+
for (let i = idx + 1; i < lines.length; i++) {
|
|
3147
|
+
const line = lines[i];
|
|
3148
|
+
const m = line.match(/^\s*-\s*(.+)$/);
|
|
3149
|
+
if (m) {
|
|
3150
|
+
items.push(stripQuotes(m[1].trim()));
|
|
3151
|
+
} else if (line.trim() === "") {
|
|
3152
|
+
continue;
|
|
3153
|
+
} else {
|
|
3154
|
+
break;
|
|
3155
|
+
}
|
|
3156
|
+
}
|
|
3157
|
+
return items.length ? items : void 0;
|
|
3158
|
+
}
|
|
3159
|
+
return [stripQuotes(inline)];
|
|
3160
|
+
}
|
|
3134
3161
|
function parseFrontmatter(text) {
|
|
3135
3162
|
if (!text.startsWith("---\n")) {
|
|
3136
3163
|
return { validFrontmatter: false };
|
|
@@ -3141,7 +3168,8 @@ function parseFrontmatter(text) {
|
|
|
3141
3168
|
name: extractSimpleField(block, "name"),
|
|
3142
3169
|
version: extractSimpleField(block, "version"),
|
|
3143
3170
|
license: extractSimpleField(block, "license"),
|
|
3144
|
-
description: extractDescription(block)
|
|
3171
|
+
description: extractDescription(block),
|
|
3172
|
+
extends: extractExtends(block)
|
|
3145
3173
|
};
|
|
3146
3174
|
}
|
|
3147
3175
|
function detectLicense(root, fm) {
|
|
@@ -3227,7 +3255,8 @@ function profileSkillDir(inputPath) {
|
|
|
3227
3255
|
resources,
|
|
3228
3256
|
mcpDependencies: mcpDeps,
|
|
3229
3257
|
externalPythonDeps: externalDeps,
|
|
3230
|
-
artifactType: detectArtifactType(root)
|
|
3258
|
+
artifactType: detectArtifactType(root),
|
|
3259
|
+
extends: fm.extends ?? []
|
|
3231
3260
|
};
|
|
3232
3261
|
} finally {
|
|
3233
3262
|
cleanupTmp(tmpDir);
|
|
@@ -3407,9 +3436,13 @@ function loadSkillMap(path) {
|
|
|
3407
3436
|
if (!Array.isArray(obj.nodes) || !Array.isArray(obj.edges)) {
|
|
3408
3437
|
throw new Error(`skill map at ${resolved}: expected "nodes" and "edges" arrays`);
|
|
3409
3438
|
}
|
|
3439
|
+
const nodes = obj.nodes.map((n) => ({
|
|
3440
|
+
...n,
|
|
3441
|
+
type: typeof n.type === "string" ? n.type : n.kind
|
|
3442
|
+
}));
|
|
3410
3443
|
return {
|
|
3411
3444
|
schemaVersion: typeof obj.schemaVersion === "string" ? obj.schemaVersion : void 0,
|
|
3412
|
-
nodes
|
|
3445
|
+
nodes,
|
|
3413
3446
|
edges: obj.edges
|
|
3414
3447
|
};
|
|
3415
3448
|
}
|
|
@@ -3497,6 +3530,25 @@ function mapOverlapFinding(result) {
|
|
|
3497
3530
|
context: top.skillId
|
|
3498
3531
|
};
|
|
3499
3532
|
}
|
|
3533
|
+
function isDeclaredExtension(match, extendsDeclarations) {
|
|
3534
|
+
const qualifiedId = match.skillId.replace(/^[a-z]+:/, "");
|
|
3535
|
+
const bareId = qualifiedId.split("/").pop() ?? qualifiedId;
|
|
3536
|
+
return extendsDeclarations.some((raw) => {
|
|
3537
|
+
const declared = raw.trim();
|
|
3538
|
+
if (!declared) return false;
|
|
3539
|
+
return declared === match.name || declared === bareId || declared === qualifiedId;
|
|
3540
|
+
});
|
|
3541
|
+
}
|
|
3542
|
+
function mapOverlapDecision(result, extendsDeclarations = []) {
|
|
3543
|
+
const top = result.matches[0];
|
|
3544
|
+
if (!top) return {};
|
|
3545
|
+
if (extendsDeclarations.length > 0 && isDeclaredExtension(top, extendsDeclarations)) {
|
|
3546
|
+
return {
|
|
3547
|
+
informational: `declared extension of "${top.name}" (score ${top.score}) \u2014 overlap expected; verify the layering claim in review`
|
|
3548
|
+
};
|
|
3549
|
+
}
|
|
3550
|
+
return { finding: mapOverlapFinding(result) };
|
|
3551
|
+
}
|
|
3500
3552
|
|
|
3501
3553
|
// src/report/render.ts
|
|
3502
3554
|
var SAFETY_VERDICT_MAP = {
|
|
@@ -3623,11 +3675,11 @@ function tryMcpOverlap(profile, mcpTargets) {
|
|
|
3623
3675
|
return void 0;
|
|
3624
3676
|
}
|
|
3625
3677
|
}
|
|
3626
|
-
function
|
|
3678
|
+
function tryMapOverlapDecision(skillMapPath, candidate, extendsDeclarations) {
|
|
3627
3679
|
if (!skillMapPath) return void 0;
|
|
3628
3680
|
try {
|
|
3629
3681
|
const map = loadSkillMap(skillMapPath);
|
|
3630
|
-
return
|
|
3682
|
+
return mapOverlapDecision(scanMapOverlap(candidate, map), extendsDeclarations);
|
|
3631
3683
|
} catch (err) {
|
|
3632
3684
|
console.error(`[skill-forge] skill-map overlap check skipped: ${err.message}`);
|
|
3633
3685
|
return void 0;
|
|
@@ -3653,20 +3705,29 @@ async function runGate(candidatePath, skillsRoot, {
|
|
|
3653
3705
|
const proStatus = proFeatureStatus(proFeatureLabel);
|
|
3654
3706
|
const safety = artifact === "mcp" ? await mcpScan(candidatePath, strictness) : await scanSafety(candidatePath, strictness);
|
|
3655
3707
|
const overlap = !proStatus.enabled ? void 0 : artifact === "mcp" ? tryMcpOverlap(profile, mcpTargets) : tryOverlap(candidatePath, skillsRoot);
|
|
3656
|
-
const
|
|
3657
|
-
|
|
3658
|
-
|
|
3659
|
-
|
|
3708
|
+
const mapDecision = artifact === "mcp" ? void 0 : tryMapOverlapDecision(
|
|
3709
|
+
skillMapPath,
|
|
3710
|
+
{
|
|
3711
|
+
name: "name" in profile ? profile.name : void 0,
|
|
3712
|
+
description: "description" in profile ? profile.description : void 0
|
|
3713
|
+
},
|
|
3714
|
+
profile.kind === "skill" ? profile.extends : []
|
|
3715
|
+
);
|
|
3716
|
+
const mapFinding = mapDecision?.finding;
|
|
3660
3717
|
const combinedSafety = mapFinding ? { verdict: verdictFor([...safety.findings, mapFinding], strictness), findings: [...safety.findings, mapFinding] } : safety;
|
|
3661
3718
|
const advisory = formatAuditAdvisory(readAuditState(), {
|
|
3662
3719
|
targetRoot: skillsRoot || void 0,
|
|
3663
3720
|
candidateName: "name" in profile ? profile.name : void 0
|
|
3664
3721
|
});
|
|
3722
|
+
const notices = [
|
|
3723
|
+
...proStatus.notice ? [proStatus.notice] : [],
|
|
3724
|
+
...mapDecision?.informational ? [mapDecision.informational] : []
|
|
3725
|
+
];
|
|
3665
3726
|
const gateResult = {
|
|
3666
3727
|
profile,
|
|
3667
3728
|
overlap,
|
|
3668
3729
|
overlapLocked: !proStatus.enabled,
|
|
3669
|
-
notices:
|
|
3730
|
+
notices: notices.length > 0 ? notices : void 0,
|
|
3670
3731
|
safety: {
|
|
3671
3732
|
verdict: SAFETY_VERDICT_MAP[combinedSafety.verdict],
|
|
3672
3733
|
findings: mapSafetyFindings(combinedSafety.findings)
|
|
@@ -6406,12 +6467,52 @@ import { join as join26, resolve as resolvePath8 } from "path";
|
|
|
6406
6467
|
// src/gate/skillMapDrift.ts
|
|
6407
6468
|
import { createHash as createHash4 } from "crypto";
|
|
6408
6469
|
import { readFileSync as readFileSync18, existsSync as existsSync23 } from "fs";
|
|
6409
|
-
import { resolve as resolve7 } from "path";
|
|
6470
|
+
import { dirname as dirname10, isAbsolute as isAbsolute2, resolve as resolve7 } from "path";
|
|
6410
6471
|
function sha256(text) {
|
|
6411
6472
|
return createHash4("sha256").update(text, "utf-8").digest("hex");
|
|
6412
6473
|
}
|
|
6413
|
-
function
|
|
6414
|
-
|
|
6474
|
+
function shortHash(hash) {
|
|
6475
|
+
return hash.slice(0, 12);
|
|
6476
|
+
}
|
|
6477
|
+
var ACTIONABLE_STATUSES = /* @__PURE__ */ new Set([
|
|
6478
|
+
"upstream-moved",
|
|
6479
|
+
"diverged",
|
|
6480
|
+
"drifted",
|
|
6481
|
+
"upstream-unreachable",
|
|
6482
|
+
"local-missing"
|
|
6483
|
+
]);
|
|
6484
|
+
function isActionable(status2) {
|
|
6485
|
+
return ACTIONABLE_STATUSES.has(status2);
|
|
6486
|
+
}
|
|
6487
|
+
function threeWayVerdict(localNormalized, upstreamHash, baselineHash) {
|
|
6488
|
+
const localMatchesBaseline = localNormalized === baselineHash;
|
|
6489
|
+
const upstreamMatchesBaseline = upstreamHash === baselineHash;
|
|
6490
|
+
const hashSuffix = `baseline=${shortHash(baselineHash)}\u2026, local-normalized=${shortHash(localNormalized)}\u2026, upstream=${shortHash(upstreamHash)}\u2026`;
|
|
6491
|
+
const TABLE = {
|
|
6492
|
+
TT: { status: "in-sync", detail: `local and upstream both match the recorded baseline (${hashSuffix})` },
|
|
6493
|
+
TF: { status: "upstream-moved", detail: `only upstream changed since baseline \u2014 review for adoption (${hashSuffix})` },
|
|
6494
|
+
FT: { status: "local-only", detail: `only local content changed since baseline \u2014 deliberate fork divergence (${hashSuffix})` },
|
|
6495
|
+
FF: { status: "diverged", detail: `both local and upstream changed since baseline \u2014 review for adoption (${hashSuffix})` }
|
|
6496
|
+
};
|
|
6497
|
+
const key = `${localMatchesBaseline ? "T" : "F"}${upstreamMatchesBaseline ? "T" : "F"}`;
|
|
6498
|
+
const { status: status2, detail } = TABLE[key];
|
|
6499
|
+
return { status: status2, detail, actionable: isActionable(status2) };
|
|
6500
|
+
}
|
|
6501
|
+
function resolveLocalPath(path, mapDir) {
|
|
6502
|
+
const expanded = expandHome(path);
|
|
6503
|
+
if (isAbsolute2(expanded)) return resolve7(expanded);
|
|
6504
|
+
const candidates = [resolve7(expanded)];
|
|
6505
|
+
if (mapDir) {
|
|
6506
|
+
candidates.push(resolve7(mapDir, expanded));
|
|
6507
|
+
candidates.push(resolve7(mapDir, "..", expanded));
|
|
6508
|
+
}
|
|
6509
|
+
for (const candidate of candidates) {
|
|
6510
|
+
if (existsSync23(candidate)) return candidate;
|
|
6511
|
+
}
|
|
6512
|
+
return candidates[0];
|
|
6513
|
+
}
|
|
6514
|
+
function defaultReadLocalFile(path, mapDir) {
|
|
6515
|
+
const resolved = resolveLocalPath(path, mapDir);
|
|
6415
6516
|
if (!existsSync23(resolved)) return void 0;
|
|
6416
6517
|
try {
|
|
6417
6518
|
return readFileSync18(resolved, "utf-8");
|
|
@@ -6435,7 +6536,8 @@ function upstreamLabel(node, edge) {
|
|
|
6435
6536
|
return node?.url ?? node?.path ?? node?.name ?? node?.id ?? edge.to;
|
|
6436
6537
|
}
|
|
6437
6538
|
async function computeMapDrift(map, deps = {}) {
|
|
6438
|
-
const
|
|
6539
|
+
const mapDir = deps.mapPath ? dirname10(resolve7(expandHome(deps.mapPath))) : void 0;
|
|
6540
|
+
const readLocalFile = deps.readLocalFile ?? ((path) => defaultReadLocalFile(path, mapDir));
|
|
6439
6541
|
const fetchUpstream = deps.fetchUpstream ?? defaultFetchUpstream;
|
|
6440
6542
|
const rows = [];
|
|
6441
6543
|
for (const edge of edgesOfType(map, "fork-of")) {
|
|
@@ -6448,13 +6550,20 @@ async function computeMapDrift(map, deps = {}) {
|
|
|
6448
6550
|
skill: skillLabel,
|
|
6449
6551
|
upstream,
|
|
6450
6552
|
status: "local-missing",
|
|
6451
|
-
detail: !localNode ? `fork-of edge "from" node not found in map: ${edge.from}` : 'local skill node missing "path" or "contentHash" \u2014 cannot compare'
|
|
6553
|
+
detail: !localNode ? `fork-of edge "from" node not found in map: ${edge.from}` : 'local skill node missing "path" or "contentHash" \u2014 cannot compare',
|
|
6554
|
+
actionable: isActionable("local-missing")
|
|
6452
6555
|
});
|
|
6453
6556
|
continue;
|
|
6454
6557
|
}
|
|
6455
|
-
const localResolved =
|
|
6558
|
+
const localResolved = resolveLocalPath(localNode.path, mapDir);
|
|
6456
6559
|
if (!existsSync23(localResolved)) {
|
|
6457
|
-
rows.push({
|
|
6560
|
+
rows.push({
|
|
6561
|
+
skill: skillLabel,
|
|
6562
|
+
upstream,
|
|
6563
|
+
status: "local-missing",
|
|
6564
|
+
detail: `local path does not exist: ${localResolved}`,
|
|
6565
|
+
actionable: isActionable("local-missing")
|
|
6566
|
+
});
|
|
6458
6567
|
continue;
|
|
6459
6568
|
}
|
|
6460
6569
|
const upstreamLocation = upstreamNode?.url ?? upstreamNode?.path;
|
|
@@ -6463,24 +6572,45 @@ async function computeMapDrift(map, deps = {}) {
|
|
|
6463
6572
|
skill: skillLabel,
|
|
6464
6573
|
upstream,
|
|
6465
6574
|
status: "upstream-unreachable",
|
|
6466
|
-
detail: 'upstream node has no "url" or "path" to fetch/read'
|
|
6575
|
+
detail: 'upstream node has no "url" or "path" to fetch/read',
|
|
6576
|
+
actionable: isActionable("upstream-unreachable")
|
|
6467
6577
|
});
|
|
6468
6578
|
continue;
|
|
6469
6579
|
}
|
|
6470
6580
|
const upstreamText = isHttpUrl(upstreamLocation) ? await fetchUpstream(upstreamLocation) : readLocalFile(upstreamLocation);
|
|
6471
6581
|
if (upstreamText === void 0) {
|
|
6472
|
-
rows.push({
|
|
6582
|
+
rows.push({
|
|
6583
|
+
skill: skillLabel,
|
|
6584
|
+
upstream,
|
|
6585
|
+
status: "upstream-unreachable",
|
|
6586
|
+
detail: `could not fetch/read upstream: ${upstreamLocation}`,
|
|
6587
|
+
actionable: isActionable("upstream-unreachable")
|
|
6588
|
+
});
|
|
6473
6589
|
continue;
|
|
6474
6590
|
}
|
|
6475
6591
|
const upstreamHash = sha256(upstreamText);
|
|
6592
|
+
const baselineHash = upstreamNode?.baselineHash;
|
|
6593
|
+
const localNormalized = localNode.contentHashNormalized;
|
|
6594
|
+
if (baselineHash && localNormalized) {
|
|
6595
|
+
const { status: status2, detail, actionable } = threeWayVerdict(localNormalized, upstreamHash, baselineHash);
|
|
6596
|
+
rows.push({ skill: skillLabel, upstream, status: status2, detail, actionable, baselineHash, upstreamHash });
|
|
6597
|
+
continue;
|
|
6598
|
+
}
|
|
6476
6599
|
if (upstreamHash === localNode.contentHash) {
|
|
6477
|
-
rows.push({
|
|
6600
|
+
rows.push({
|
|
6601
|
+
skill: skillLabel,
|
|
6602
|
+
upstream,
|
|
6603
|
+
status: "in-sync",
|
|
6604
|
+
detail: `contentHash matches (${shortHash(upstreamHash)}\u2026)`,
|
|
6605
|
+
actionable: isActionable("in-sync")
|
|
6606
|
+
});
|
|
6478
6607
|
} else {
|
|
6479
6608
|
rows.push({
|
|
6480
6609
|
skill: skillLabel,
|
|
6481
6610
|
upstream,
|
|
6482
6611
|
status: "drifted",
|
|
6483
|
-
detail: `recorded ${localNode.contentHash
|
|
6612
|
+
detail: `recorded ${shortHash(localNode.contentHash)}\u2026, upstream now hashes to ${shortHash(upstreamHash)}\u2026`,
|
|
6613
|
+
actionable: isActionable("drifted")
|
|
6484
6614
|
});
|
|
6485
6615
|
}
|
|
6486
6616
|
}
|
|
@@ -6593,10 +6723,10 @@ async function runWatch(config, options = {}, deps = {}) {
|
|
|
6593
6723
|
if (options.skillMap) {
|
|
6594
6724
|
try {
|
|
6595
6725
|
const map = loadSkillMap(options.skillMap);
|
|
6596
|
-
mapDrift = await computeMapDrift(
|
|
6597
|
-
|
|
6598
|
-
options.offline ? { fetchUpstream: async () => void 0 } :
|
|
6599
|
-
);
|
|
6726
|
+
mapDrift = await computeMapDrift(map, {
|
|
6727
|
+
mapPath: options.skillMap,
|
|
6728
|
+
...options.offline ? { fetchUpstream: async () => void 0 } : {}
|
|
6729
|
+
});
|
|
6600
6730
|
} catch (err) {
|
|
6601
6731
|
warnings.push(`skill map at ${options.skillMap}: ${err.message}`);
|
|
6602
6732
|
}
|
|
@@ -6921,7 +7051,7 @@ async function routineCommand(options = {}) {
|
|
|
6921
7051
|
|
|
6922
7052
|
// src/commands/ingest.ts
|
|
6923
7053
|
import { realpathSync as realpathSync6 } from "fs";
|
|
6924
|
-
import { dirname as
|
|
7054
|
+
import { dirname as dirname12, isAbsolute as isAbsolute3, relative as relative2, resolve as resolvePath9 } from "path";
|
|
6925
7055
|
var PRO_FEATURE_LABEL7 = "the pending-ingestion queue";
|
|
6926
7056
|
var VALID_STATUSES = ["ingested", "dismissed"];
|
|
6927
7057
|
function requireProFeature() {
|
|
@@ -6969,12 +7099,12 @@ function realpathOrNull(p) {
|
|
|
6969
7099
|
}
|
|
6970
7100
|
function isUnderRoot(candidate, root) {
|
|
6971
7101
|
const rel = relative2(root, candidate);
|
|
6972
|
-
return rel === "" || !rel.startsWith("..") && !
|
|
7102
|
+
return rel === "" || !rel.startsWith("..") && !isAbsolute3(rel);
|
|
6973
7103
|
}
|
|
6974
7104
|
function validatePendingEntries(entries, config) {
|
|
6975
7105
|
const quarantineRoot = realpathOrNull(resolvePath9(config.quarantineDir));
|
|
6976
7106
|
const skillsRoots = config.skillsRoots.map((r) => realpathOrNull(resolvePath9(r))).filter((r) => r !== null);
|
|
6977
|
-
const mcpTargetDirs = (config.mcpTargets ?? []).map((f) => realpathOrNull(
|
|
7107
|
+
const mcpTargetDirs = (config.mcpTargets ?? []).map((f) => realpathOrNull(dirname12(resolvePath9(f)))).filter((r) => r !== null);
|
|
6978
7108
|
const allowedRoots = [quarantineRoot, ...skillsRoots, ...mcpTargetDirs].filter((r) => r !== null);
|
|
6979
7109
|
const valid = [];
|
|
6980
7110
|
const excluded = [];
|
|
@@ -7048,12 +7178,12 @@ async function queueClose(id, options = {}) {
|
|
|
7048
7178
|
// src/commands/refine.ts
|
|
7049
7179
|
import { createHash as createHash6, randomUUID as randomUUID5 } from "crypto";
|
|
7050
7180
|
import { chmodSync, existsSync as existsSync27, lstatSync as lstatSync4, mkdirSync as mkdirSync9, readFileSync as readFileSync20, realpathSync as realpathSync9, renameSync as renameSync6, rmSync as rmSync9, writeFileSync as writeFileSync9 } from "fs";
|
|
7051
|
-
import { dirname as
|
|
7181
|
+
import { dirname as dirname14, isAbsolute as isAbsolute5, join as join30, relative as relative4, resolve as resolvePath12, sep as sep4 } from "path";
|
|
7052
7182
|
import { createInterface as createInterface6 } from "readline/promises";
|
|
7053
7183
|
|
|
7054
7184
|
// src/refine/context.ts
|
|
7055
7185
|
import { existsSync as existsSync26, realpathSync as realpathSync8, statSync as statSync12 } from "fs";
|
|
7056
|
-
import { isAbsolute as
|
|
7186
|
+
import { isAbsolute as isAbsolute4, join as join29, relative as relative3, resolve as resolvePath11 } from "path";
|
|
7057
7187
|
|
|
7058
7188
|
// src/refine/models.ts
|
|
7059
7189
|
import { randomBytes } from "crypto";
|
|
@@ -7465,7 +7595,7 @@ function realpathOrNull2(p) {
|
|
|
7465
7595
|
}
|
|
7466
7596
|
function isUnderRoot2(candidate, root) {
|
|
7467
7597
|
const rel = relative3(root, candidate);
|
|
7468
|
-
return rel === "" || !rel.startsWith("..") && !
|
|
7598
|
+
return rel === "" || !rel.startsWith("..") && !isAbsolute4(rel);
|
|
7469
7599
|
}
|
|
7470
7600
|
function resolveScopes(config, cwd, skill, userRootFlag) {
|
|
7471
7601
|
assertPlainSkillName(skill);
|
|
@@ -7796,7 +7926,7 @@ function resolveConfiguredRoot3(root) {
|
|
|
7796
7926
|
}
|
|
7797
7927
|
function isUnderRoot3(candidate, root) {
|
|
7798
7928
|
const rel = relative4(root, candidate);
|
|
7799
|
-
return rel === "" || !rel.startsWith("..") && !
|
|
7929
|
+
return rel === "" || !rel.startsWith("..") && !isAbsolute5(rel);
|
|
7800
7930
|
}
|
|
7801
7931
|
function sanitizeDiffForConsole(diff) {
|
|
7802
7932
|
return diff.split("\n").map((line) => sanitizeForMarkdown(line)).join("\n");
|
|
@@ -7956,7 +8086,7 @@ function ensureScopeDirSafe(scopeDir, resolvedCwd) {
|
|
|
7956
8086
|
function writeOverrideFile(scopeDir, relPath, content, resolvedCwd) {
|
|
7957
8087
|
const realScopeDir = ensureScopeDirSafe(scopeDir, resolvedCwd);
|
|
7958
8088
|
const destPath = join30(scopeDir, relPath);
|
|
7959
|
-
const destDir =
|
|
8089
|
+
const destDir = dirname14(destPath);
|
|
7960
8090
|
mkdirSync9(destDir, { recursive: true });
|
|
7961
8091
|
const realDestDir = realpathSync9(destDir);
|
|
7962
8092
|
if (!isUnderRoot3(realDestDir, realScopeDir)) {
|
|
@@ -8313,7 +8443,7 @@ function resolveTargetRealPath(path) {
|
|
|
8313
8443
|
const remainder = relative4(lexicalAncestor, resolvePath12(path));
|
|
8314
8444
|
return remainder === "" ? real : join30(real, remainder);
|
|
8315
8445
|
} catch {
|
|
8316
|
-
const parent =
|
|
8446
|
+
const parent = dirname14(lexicalAncestor);
|
|
8317
8447
|
if (parent === lexicalAncestor) return resolvePath12(path);
|
|
8318
8448
|
lexicalAncestor = parent;
|
|
8319
8449
|
}
|
|
@@ -8357,7 +8487,7 @@ function findLexicalRoot(filePath, config) {
|
|
|
8357
8487
|
for (const root of config.skillsRoots) {
|
|
8358
8488
|
const rootLexical = resolveConfiguredRoot3(root);
|
|
8359
8489
|
const rel = relative4(rootLexical, lexicalTarget);
|
|
8360
|
-
if (rel !== "" && (rel === ".." || rel.startsWith(`..${sep4}`) ||
|
|
8490
|
+
if (rel !== "" && (rel === ".." || rel.startsWith(`..${sep4}`) || isAbsolute5(rel))) continue;
|
|
8361
8491
|
let rootReal;
|
|
8362
8492
|
try {
|
|
8363
8493
|
rootReal = realpathSync9(rootLexical);
|